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..996b46b148d85 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); @@ -121,7 +122,7 @@ plperl_to_hstore(PG_FUNCTION_ARGS) pcount = hv_iterinit(hv); - pairs = palloc(pcount * sizeof(Pairs)); + pairs = palloc_array(Pairs, pcount); i = 0; while ((he = hv_iternext(hv))) 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..f1e483980f4c7 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); @@ -150,7 +151,7 @@ plpython_to_hstore(PG_FUNCTION_ARGS) Py_ssize_t i; Pairs *pairs; - pairs = palloc(pcount * sizeof(*pairs)); + pairs = palloc_array(Pairs, pcount); for (i = 0; i < pcount; i++) { 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..4382f882939aa 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) @@ -434,37 +434,77 @@ boolop(PG_FUNCTION_ARGS) PG_RETURN_BOOL(result); } -static void -findoprnd(ITEM *ptr, int32 *pos) +/* + * Recursively fill the "left" fields of an ITEM array that represents + * a valid postfix tree. + * + * state: only needed for error reporting + * ptr: starting element of array + * pos: in/out argument, the array index this call is responsible to fill + * + * At exit, *pos has been decremented to point before the sub-tree whose + * top is the entry-time value of *pos. + * + * Returns true if okay, false if error (the only possible error is + * overflow of a "left" field). + */ +static bool +findoprnd(WORKSTATE *state, ITEM *ptr, int32 *pos) { + int32 mypos; + /* since this function recurses, it could be driven to stack overflow. */ check_stack_depth(); + /* get the position this call is supposed to update */ + mypos = *pos; + Assert(mypos >= 0); + + /* in all cases, we should decrement *pos to advance over this item */ + (*pos)--; + #ifdef BS_DEBUG - elog(DEBUG3, (ptr[*pos].type == OPR) ? - "%d %c" : "%d %d", *pos, ptr[*pos].val); + elog(DEBUG3, (ptr[mypos].type == OPR) ? + "%d %c" : "%d %d", mypos, ptr[mypos].val); #endif - if (ptr[*pos].type == VAL) + + if (ptr[mypos].type == VAL) { - ptr[*pos].left = 0; - (*pos)--; + /* base case: a VAL has no operand, so just set its left to zero */ + ptr[mypos].left = 0; } - else if (ptr[*pos].val == (int32) '!') + else if (ptr[mypos].val == (int32) '!') { - ptr[*pos].left = -1; - (*pos)--; - findoprnd(ptr, pos); + /* unary operator, likewise easy: operand is just before it */ + ptr[mypos].left = -1; + /* recurse to scan operand */ + if (!findoprnd(state, ptr, pos)) + return false; } else { - ITEM *curitem = &ptr[*pos]; - int32 tmp = *pos; + /* binary operator */ + int32 delta; - (*pos)--; - findoprnd(ptr, pos); - curitem->left = *pos - tmp; - findoprnd(ptr, pos); + /* recurse to scan right operand */ + if (!findoprnd(state, ptr, pos)) + return false; + /* we must fill left with offset to left operand's top */ + /* abs(delta) < QUERYTYPEMAXITEMS, so it can't overflow ... */ + delta = *pos - mypos; + /* ... but it might be too large to fit in the 16-bit left field */ + Assert(delta < 0); + if (unlikely(delta < PG_INT16_MIN)) + ereturn(state->escontext, false, + (errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED), + errmsg("query_int expression is too complex"))); + ptr[mypos].left = (int16) delta; + /* recurse to scan left operand */ + if (!findoprnd(state, ptr, pos)) + return false; } + + return true; } @@ -515,6 +555,7 @@ bqarr_in(PG_FUNCTION_ARGS) query->size = state.num; ptr = GETQUERY(query); + /* fill the query array from the data makepol constructed */ for (i = state.num - 1; i >= 0; i--) { ptr[i].type = state.str->type; @@ -524,8 +565,13 @@ bqarr_in(PG_FUNCTION_ARGS) state.str = tmp; } + /* now fill the "left" fields */ pos = query->size - 1; - findoprnd(ptr, &pos); + if (!findoprnd(&state, ptr, &pos)) + PG_RETURN_NULL(); + /* if successful, findoprnd should have scanned the whole array */ + Assert(pos == -1); + #ifdef BS_DEBUG initStringInfo(&pbuf); for (i = 0; i < query->size; i++) @@ -613,7 +659,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 +697,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..88d2ef0c9177b 100644 --- a/contrib/ltree/ltree_io.c +++ b/contrib/ltree/ltree_io.c @@ -7,6 +7,7 @@ #include +#include "common/int.h" #include "crc32.h" #include "libpq/pqformat.h" #include "ltree.h" @@ -54,7 +55,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 +66,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 +292,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 +312,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; @@ -344,7 +345,12 @@ parse_lquery(const char *buf, struct Node *escontext) lptr++; lptr->start = ptr; state = LQPRS_WAITDELIM; - curqlevel->numvar++; + if (pg_add_u16_overflow(curqlevel->numvar, 1, &curqlevel->numvar)) + ereturn(escontext, NULL, + (errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED), + errmsg("lquery level has too many variants"), + errdetail("Number of variants exceeds the maximum allowed (%d).", + PG_UINT16_MAX))); } else UNCHAR; @@ -542,7 +548,16 @@ parse_lquery(const char *buf, struct Node *escontext) lptr = GETVAR(curqlevel); while (lptr - GETVAR(curqlevel) < curqlevel->numvar) { - cur->totallen += MAXALIGN(LVAR_HDRSIZE + lptr->len); + int newlen = cur->totallen + MAXALIGN(LVAR_HDRSIZE + lptr->len); + + if (newlen > PG_UINT16_MAX) + ereturn(escontext, NULL, + (errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED), + errmsg("lquery level is too large"), + errdetail("Total size of level exceeds the maximum allowed (%d bytes).", + PG_UINT16_MAX))); + cur->totallen = (uint16) newlen; + lrptr->len = lptr->len; lrptr->flag = lptr->flag; lrptr->val = ltree_crc32_sz(lptr->start, lptr->len); 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..b1354b9227c1a 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), @@ -294,33 +294,71 @@ makepol(QPRS_STATE *state) return END; } -static void -findoprnd(ITEM *ptr, int32 *pos) +/* + * Recursively fill the "left" fields of an ITEM array that represents + * a valid postfix tree. + * + * state: only needed for error reporting + * ptr: starting element of array + * pos: in/out argument, the array index this call is responsible to fill + * + * At exit, *pos has been incremented to point after the sub-tree whose + * top is the entry-time value of *pos. + * + * Returns true if okay, false if error (the only possible error is + * overflow of a "left" field). + */ +static bool +findoprnd(QPRS_STATE *state, ITEM *ptr, int32 *pos) { + int32 mypos; + /* since this function recurses, it could be driven to stack overflow. */ check_stack_depth(); - if (ptr[*pos].type == VAL || ptr[*pos].type == VALTRUE) + /* get the position this call is supposed to update */ + mypos = *pos; + + /* in all cases, we should increment *pos to advance over this item */ + (*pos)++; + + if (ptr[mypos].type == VAL || ptr[mypos].type == VALTRUE) { - ptr[*pos].left = 0; - (*pos)++; + /* base case: a VAL has no operand, so just set its left to zero */ + ptr[mypos].left = 0; } - else if (ptr[*pos].val == (int32) '!') + else if (ptr[mypos].val == (int32) '!') { - ptr[*pos].left = 1; - (*pos)++; - findoprnd(ptr, pos); + /* unary operator, likewise easy: operand is just after it */ + ptr[mypos].left = 1; + /* recurse to scan operand */ + if (!findoprnd(state, ptr, pos)) + return false; } else { - ITEM *curitem = &ptr[*pos]; - int32 tmp = *pos; - - (*pos)++; - findoprnd(ptr, pos); - curitem->left = *pos - tmp; - findoprnd(ptr, pos); + /* binary operator */ + int32 delta; + + /* recurse to scan right operand */ + if (!findoprnd(state, ptr, pos)) + return false; + /* we must fill left with offset to left operand's top */ + /* delta can't overflow, see LTXTQUERY_TOO_BIG ... */ + delta = *pos - mypos; + /* ... but it might be too large to fit in the 16-bit left field */ + Assert(delta > 0); + if (unlikely(delta > PG_INT16_MAX)) + ereturn(state->escontext, false, + (errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED), + errmsg("ltxtquery is too large"))); + ptr[mypos].left = (int16) delta; + /* recurse to scan left operand */ + if (!findoprnd(state, ptr, pos)) + return false; } + + return true; } @@ -338,11 +376,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; @@ -396,7 +429,10 @@ queryin(char *buf, struct Node *escontext) /* set left operand's position for every operator */ pos = 0; - findoprnd(ptr, &pos); + if (!findoprnd(&state, ptr, &pos)) + return NULL; + /* if successful, findoprnd should have scanned the whole array */ + Assert(pos == state.num); return query; } @@ -543,7 +579,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 +618,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 +651,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..62c905c6e7c2c 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++] = Int16GetDatum(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..ebcc2b3db5c71 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 @@ -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..23a4b49771d45 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] = Int16GetDatum(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] = Int16GetDatum(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..bf2e6c972202d 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] = Int16GetDatum(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..d0796d5693c0f --- /dev/null +++ b/contrib/pg_stat_statements/expected/plancache.out @@ -0,0 +1,260 @@ +-- +-- 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; +-- +-- Procedure with internal ROLLBACK and the extended query protocol. +-- The PlannedStmt used in pgss_ProcessUtility() is freed by the internal +-- ROLLBACK. +-- +CREATE OR REPLACE PROCEDURE rollback_proc(a INOUT int) AS $$ +BEGIN + ROLLBACK; +END; +$$ LANGUAGE plpgsql; +SELECT pg_stat_statements_reset() IS NOT NULL AS t; + t +--- + t +(1 row) + +CALL rollback_proc($1) \parse stmt_rollback +\bind_named stmt_rollback 1 \g + a +--- + 1 +(1 row) + +\bind_named stmt_rollback 2 \g + a +--- + 2 +(1 row) + +SELECT calls, query FROM pg_stat_statements + WHERE query LIKE '%rollback_proc%' + ORDER BY query COLLATE "C"; + calls | query +-------+------------------------ + 2 | CALL rollback_proc($1) +(1 row) + +DROP PROCEDURE rollback_proc; +-- +-- 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..a2d3ab770cc64 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,9 +1096,10 @@ 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; + PlannedStmtOrigin saved_planOrigin = pstmt->planOrigin; bool enabled = pgss_track_utility && pgss_enabled(nesting_level); /* @@ -1131,7 +1117,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 +1210,8 @@ pgss_ProcessUtility(PlannedStmt *pstmt, const char *queryString, NULL, NULL, 0, - 0); + 0, + saved_planOrigin); } else { @@ -1278,16 +1265,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 +1292,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 +1313,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 +1334,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 +1353,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 +1483,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 +1508,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 +1524,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 +1556,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 +1569,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 +1737,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 +1782,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 +1800,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 +1994,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 +2014,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 +2060,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 +2296,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 +2338,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 +2377,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 +2588,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 +2605,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 +2675,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 +2690,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 +2718,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 +2784,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 +2809,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 +2836,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 +2910,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..948d3e9851801 --- /dev/null +++ b/contrib/pg_stat_statements/sql/plancache.sql @@ -0,0 +1,113 @@ +-- +-- 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; + +-- +-- Procedure with internal ROLLBACK and the extended query protocol. +-- The PlannedStmt used in pgss_ProcessUtility() is freed by the internal +-- ROLLBACK. +-- +CREATE OR REPLACE PROCEDURE rollback_proc(a INOUT int) AS $$ +BEGIN + ROLLBACK; +END; +$$ LANGUAGE plpgsql; +SELECT pg_stat_statements_reset() IS NOT NULL AS t; +CALL rollback_proc($1) \parse stmt_rollback +\bind_named stmt_rollback 1 \g +\bind_named stmt_rollback 2 \g +SELECT calls, query FROM pg_stat_statements + WHERE query LIKE '%rollback_proc%' + ORDER BY query COLLATE "C"; +DROP PROCEDURE rollback_proc; + +-- +-- 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..22bcc3c336192 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) +{ + const char *aa = a; + const char *bb = 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..3d2a8d0519df0 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; + 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 *)); - /* - * 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 = 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; } + } + + *p_appname = appname; - /* Use "postgres_fdw" as fallback_application_name */ - keywords[n] = "fallback_application_name"; - values[n] = "postgres_fdw"; + /* 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; + + /* Verify the set of connection parameters. */ + check_conn_params(keywords, values, user); - keywords[n] = values[n] = NULL; + *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; @@ -1341,6 +1480,11 @@ pgfdw_inval_callback(Datum arg, int cacheid, uint32 hashvalue) * Such connections can't safely be further used. Re-establishing the * connection would change the snapshot and roll back any writes already * performed, so that's not an option, either. Thus, we must abort. + * + * Note: there might be open cursors that use the connection, so even if the + * connection cache entry is marked as such, we will retain it until abort + * cleanup of the main transaction, to ensure such open cursors can safely + * refer to the PGconn for the connection. */ static void pgfdw_reject_incomplete_xact_state_change(ConnCacheEntry *entry) @@ -1351,15 +1495,12 @@ pgfdw_reject_incomplete_xact_state_change(ConnCacheEntry *entry) if (entry->conn == NULL || !entry->changing_xact_state) return; - /* make sure this entry is inactive */ - disconnect_pg_server(entry); - /* find server name to be shown in the message below */ server = GetForeignServer(entry->serverid); ereport(ERROR, (errcode(ERRCODE_CONNECTION_EXCEPTION), - errmsg("connection to server \"%s\" was lost", + errmsg("connection to server \"%s\" cannot be used due to abort cleanup failure", server->servername))); } @@ -1374,6 +1515,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 +1538,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 +1690,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 +1715,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 +1727,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 +1735,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 +1763,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 +2440,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 +2742,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..aaffcf3127192 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 @@ -12590,3 +12977,79 @@ SELECT server_name, -- Clean up \set VERBOSITY default RESET debug_discard_caches; +-- =================================================================== +-- test cleanup of failed connections on abort +-- =================================================================== +CREATE VIEW my_backend_pid (pid) AS SELECT pg_backend_pid(); +CREATE FOREIGN TABLE remote_backend_pid (pid int) + SERVER loopback OPTIONS (table_name 'my_backend_pid'); +CREATE FUNCTION wait_for_backend_termination(int) RETURNS void AS $$ + BEGIN + WHILE (SELECT count(*) FROM pg_stat_activity WHERE pid = $1) > 0 + LOOP + PERFORM pg_stat_clear_snapshot(); + END LOOP; + END +$$ LANGUAGE plpgsql; +SET client_min_messages = 'ERROR'; +BEGIN; +DECLARE c CURSOR FOR SELECT * FROM ft1 ORDER BY c1; +SAVEPOINT s; +SELECT pid AS remote_pid FROM remote_backend_pid \gset +SELECT pg_terminate_backend(:remote_pid); + pg_terminate_backend +---------------------- + t +(1 row) + +SELECT wait_for_backend_termination(:remote_pid); + wait_for_backend_termination +------------------------------ + +(1 row) + +ROLLBACK TO SAVEPOINT s; +SELECT pid FROM remote_backend_pid; +ERROR: connection to server "loopback" cannot be used due to abort cleanup failure +ROLLBACK TO SAVEPOINT s; +RELEASE SAVEPOINT s; +FETCH c; +ERROR: no connection to the server +CONTEXT: remote SQL command: DECLARE c1 CURSOR FOR +SELECT "C 1", c2, c3, c4, c5, c6, c7, c8 FROM "S 1"."T 1" ORDER BY "C 1" ASC NULLS LAST +ABORT; +BEGIN; +DECLARE c CURSOR FOR SELECT * FROM ft1 ORDER BY c1; +FETCH c; + c1 | c2 | c3 | c4 | c5 | c6 | c7 | c8 +----+----+-------------------+------------------------------+--------------------------+----+------------+----- + 1 | 2 | 00001_trig_update | Fri Jan 02 00:00:00 1970 PST | Fri Jan 02 00:00:00 1970 | 1 | 1 | foo +(1 row) + +SAVEPOINT s; +SELECT pid AS remote_pid FROM remote_backend_pid \gset +SELECT pg_terminate_backend(:remote_pid); + pg_terminate_backend +---------------------- + t +(1 row) + +SELECT wait_for_backend_termination(:remote_pid); + wait_for_backend_termination +------------------------------ + +(1 row) + +ROLLBACK TO SAVEPOINT s; +SELECT pid FROM remote_backend_pid; +ERROR: connection to server "loopback" cannot be used due to abort cleanup failure +ROLLBACK TO SAVEPOINT s; +RELEASE SAVEPOINT s; +CLOSE c; +ERROR: no connection to the server +CONTEXT: remote SQL command: CLOSE c1 +ABORT; +RESET client_min_messages; +DROP FUNCTION wait_for_backend_termination(int); +DROP FOREIGN TABLE remote_backend_pid; +DROP VIEW my_backend_pid; 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..0ff4ec23164b1 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..267d3c1a7e7a7 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 -- =================================================================== @@ -4348,3 +4620,54 @@ SELECT server_name, -- Clean up \set VERBOSITY default RESET debug_discard_caches; + +-- =================================================================== +-- test cleanup of failed connections on abort +-- =================================================================== + +CREATE VIEW my_backend_pid (pid) AS SELECT pg_backend_pid(); +CREATE FOREIGN TABLE remote_backend_pid (pid int) + SERVER loopback OPTIONS (table_name 'my_backend_pid'); +CREATE FUNCTION wait_for_backend_termination(int) RETURNS void AS $$ + BEGIN + WHILE (SELECT count(*) FROM pg_stat_activity WHERE pid = $1) > 0 + LOOP + PERFORM pg_stat_clear_snapshot(); + END LOOP; + END +$$ LANGUAGE plpgsql; + +SET client_min_messages = 'ERROR'; + +BEGIN; +DECLARE c CURSOR FOR SELECT * FROM ft1 ORDER BY c1; +SAVEPOINT s; +SELECT pid AS remote_pid FROM remote_backend_pid \gset +SELECT pg_terminate_backend(:remote_pid); +SELECT wait_for_backend_termination(:remote_pid); +ROLLBACK TO SAVEPOINT s; +SELECT pid FROM remote_backend_pid; +ROLLBACK TO SAVEPOINT s; +RELEASE SAVEPOINT s; +FETCH c; +ABORT; + +BEGIN; +DECLARE c CURSOR FOR SELECT * FROM ft1 ORDER BY c1; +FETCH c; +SAVEPOINT s; +SELECT pid AS remote_pid FROM remote_backend_pid \gset +SELECT pg_terminate_backend(:remote_pid); +SELECT wait_for_backend_termination(:remote_pid); +ROLLBACK TO SAVEPOINT s; +SELECT pid FROM remote_backend_pid; +ROLLBACK TO SAVEPOINT s; +RELEASE SAVEPOINT s; +CLOSE c; +ABORT; + +RESET client_min_messages; + +DROP FUNCTION wait_for_backend_termination(int); +DROP FOREIGN TABLE remote_backend_pid; +DROP VIEW my_backend_pid; 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..c44c87bcd96ae 100644 --- a/contrib/spi/refint.c +++ b/contrib/spi/refint.c @@ -171,21 +171,24 @@ check_primary_key(PG_FUNCTION_ARGS) if (plan->nplans <= 0) { SPIPlanPtr pplan; - char sql[8192]; + StringInfoData sql; + + initStringInfo(&sql); /* * Construct query: SELECT 1 FROM _referenced_relation_ WHERE Pkey1 = * $1 [AND Pkey2 = $2 [...]] */ - snprintf(sql, sizeof(sql), "select 1 from %s where ", relname); - for (i = 0; i < nkeys; i++) + appendStringInfo(&sql, "select 1 from %s where ", relname); + for (i = 1; i <= nkeys; i++) { - snprintf(sql + strlen(sql), sizeof(sql) - strlen(sql), "%s = $%d %s", - args[i + nkeys + 1], i + 1, (i < nkeys - 1) ? "and " : ""); + appendStringInfo(&sql, "%s = $%d ", args[i + nkeys], i); + if (i < nkeys) + appendStringInfoString(&sql, "and "); } /* Prepare plan for query */ - pplan = SPI_prepare(sql, nkeys, argtypes); + pplan = SPI_prepare(sql.data, nkeys, argtypes); if (pplan == NULL) /* internal error */ elog(ERROR, "check_primary_key: SPI_prepare returned %s", SPI_result_code_string(SPI_result)); @@ -201,6 +204,8 @@ check_primary_key(PG_FUNCTION_ARGS) sizeof(SPIPlanPtr)); *(plan->splan) = pplan; plan->nplans = 1; + + pfree(sql.data); } /* @@ -321,7 +326,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]); @@ -423,7 +428,6 @@ check_foreign_key(PG_FUNCTION_ARGS) if (plan->nplans <= 0) { SPIPlanPtr pplan; - char sql[8192]; char **args2 = args; plan->splan = (SPIPlanPtr *) MemoryContextAlloc(TopMemoryContext, @@ -431,6 +435,10 @@ check_foreign_key(PG_FUNCTION_ARGS) for (r = 0; r < nrefs; r++) { + StringInfoData sql; + + initStringInfo(&sql); + relname = args2[0]; /*--------- @@ -444,8 +452,7 @@ check_foreign_key(PG_FUNCTION_ARGS) *--------- */ if (action == 'r') - - snprintf(sql, sizeof(sql), "select 1 from %s where ", relname); + appendStringInfo(&sql, "select 1 from %s where ", relname); /*--------- * For 'C'ascade action we construct DELETE query @@ -472,42 +479,23 @@ check_foreign_key(PG_FUNCTION_ARGS) char *nv; int k; - snprintf(sql, sizeof(sql), "update %s set ", relname); + appendStringInfo(&sql, "update %s set ", relname); for (k = 1; k <= nkeys; k++) { - int is_char_type = 0; - char *type; - fn = SPI_fnumber(tupdesc, args_temp[k - 1]); Assert(fn > 0); /* already checked above */ nv = SPI_getvalue(newtuple, tupdesc, fn); - type = SPI_gettype(tupdesc, fn); - - if (strcmp(type, "text") == 0 || - strcmp(type, "varchar") == 0 || - strcmp(type, "char") == 0 || - strcmp(type, "bpchar") == 0 || - strcmp(type, "date") == 0 || - strcmp(type, "timestamp") == 0) - is_char_type = 1; -#ifdef DEBUG_QUERY - elog(DEBUG4, "check_foreign_key Debug value %s type %s %d", - nv, type, is_char_type); -#endif - /* - * is_char_type =1 i set ' ' for define a new value - */ - snprintf(sql + strlen(sql), sizeof(sql) - strlen(sql), - " %s = %s%s%s %s ", - args2[k], (is_char_type > 0) ? "'" : "", - nv, (is_char_type > 0) ? "'" : "", (k < nkeys) ? ", " : ""); + appendStringInfo(&sql, " %s = %s ", + args2[k], quote_literal_cstr(nv)); + if (k < nkeys) + appendStringInfoString(&sql, ", "); } - strcat(sql, " where "); + appendStringInfoString(&sql, " where "); } else /* DELETE */ - snprintf(sql, sizeof(sql), "delete from %s where ", relname); + appendStringInfo(&sql, "delete from %s where ", relname); } /* @@ -518,25 +506,26 @@ check_foreign_key(PG_FUNCTION_ARGS) */ else if (action == 's') { - snprintf(sql, sizeof(sql), "update %s set ", relname); + appendStringInfo(&sql, "update %s set ", relname); for (i = 1; i <= nkeys; i++) { - snprintf(sql + strlen(sql), sizeof(sql) - strlen(sql), - "%s = null%s", - args2[i], (i < nkeys) ? ", " : ""); + appendStringInfo(&sql, "%s = null", args2[i]); + if (i < nkeys) + appendStringInfoString(&sql, ", "); } - strcat(sql, " where "); + appendStringInfoString(&sql, " where "); } /* Construct WHERE qual */ for (i = 1; i <= nkeys; i++) { - snprintf(sql + strlen(sql), sizeof(sql) - strlen(sql), "%s = $%d %s", - args2[i], i, (i < nkeys) ? "and " : ""); + appendStringInfo(&sql, "%s = $%d ", args2[i], i); + if (i < nkeys) + appendStringInfoString(&sql, "and "); } /* Prepare plan for query */ - pplan = SPI_prepare(sql, nkeys, argtypes); + pplan = SPI_prepare(sql.data, nkeys, argtypes); if (pplan == NULL) /* internal error */ elog(ERROR, "check_foreign_key: SPI_prepare returned %s", SPI_result_code_string(SPI_result)); @@ -552,11 +541,14 @@ check_foreign_key(PG_FUNCTION_ARGS) plan->splan[r] = pplan; args2 += nkeys + 1; /* to the next relation */ + +#ifdef DEBUG_QUERY + elog(DEBUG4, "check_foreign_key Debug Query is : %s ", sql.data); +#endif + + pfree(sql.data); } plan->nplans = nrefs; -#ifdef DEBUG_QUERY - elog(DEBUG4, "check_foreign_key Debug Query is : %s ", sql); -#endif } /* @@ -651,7 +643,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..0111124399a8e 100644 --- a/contrib/test_decoding/Makefile +++ b/contrib/test_decoding/Makefile @@ -5,11 +5,11 @@ PGFILEDESC = "test_decoding - example of a logical decoding output plugin" REGRESS = ddl xact rewrite toast permissions decoding_in_xact \ decoding_into_rel binary prepared replorigin time messages \ - spill slot truncate stream stats twophase twophase_stream + repack spill slot truncate stream stats twophase twophase_stream 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/repack.out b/contrib/test_decoding/expected/repack.out new file mode 100644 index 0000000000000..cf93c1f0d3dad --- /dev/null +++ b/contrib/test_decoding/expected/repack.out @@ -0,0 +1,53 @@ +-- Test REPACK (CONCURRENTLY). +-- This test isn't strictly about logical decoding per se, but +-- REPACK (CONCURRENTLY) involves logical decoding and therefore requires +-- to be run under higher than minimal wal_level, so we can't have them in +-- the main regression test suite. +-- Ownership of partitions is checked +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; +CREATE TABLE ptnowner2 PARTITION OF ptnowner FOR VALUES IN (2); +ALTER TABLE ptnowner1 OWNER TO regress_ptnowner; +SET SESSION AUTHORIZATION regress_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 + SELECT oid, relname, relfilenode FROM pg_partition_tree('ptnowner') AS tree + JOIN pg_class AS c ON c.oid=tree.relid; +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 | t + ptnowner2 | t +(3 rows) + +DROP TABLE ptnowner; +DROP ROLE regress_ptnowner; +-- Verify that REPACK (CONCURRENTLY) doesn't lose "attmissingval" columns +CREATE TABLE rpk_missing (id int PRIMARY KEY); +INSERT INTO rpk_missing SELECT generate_series(1, 3); +ALTER TABLE rpk_missing ADD COLUMN a int DEFAULT 42; +SELECT * FROM rpk_missing; + id | a +----+---- + 1 | 42 + 2 | 42 + 3 | 42 +(3 rows) + +REPACK (CONCURRENTLY) rpk_missing; +SELECT * FROM rpk_missing; + id | a +----+---- + 1 | 42 + 2 | 42 + 3 | 42 +(3 rows) + +DROP TABLE rpk_missing; 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..ac655853d269c 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', @@ -34,6 +34,7 @@ tests += { 'replorigin', 'time', 'messages', + 'repack', 'spill', 'slot', 'truncate', @@ -64,6 +65,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/repack.sql b/contrib/test_decoding/sql/repack.sql new file mode 100644 index 0000000000000..6164dd4235fc6 --- /dev/null +++ b/contrib/test_decoding/sql/repack.sql @@ -0,0 +1,34 @@ +-- Test REPACK (CONCURRENTLY). +-- This test isn't strictly about logical decoding per se, but +-- REPACK (CONCURRENTLY) involves logical decoding and therefore requires +-- to be run under higher than minimal wal_level, so we can't have them in +-- the main regression test suite. + +-- Ownership of partitions is checked +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; +CREATE TABLE ptnowner2 PARTITION OF ptnowner FOR VALUES IN (2); +ALTER TABLE ptnowner1 OWNER TO regress_ptnowner; +SET SESSION AUTHORIZATION regress_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 + SELECT oid, relname, relfilenode FROM pg_partition_tree('ptnowner') AS tree + JOIN pg_class AS c ON c.oid=tree.relid; +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; +DROP ROLE regress_ptnowner; + +-- Verify that REPACK (CONCURRENTLY) doesn't lose "attmissingval" columns +CREATE TABLE rpk_missing (id int PRIMARY KEY); +INSERT INTO rpk_missing SELECT generate_series(1, 3); +ALTER TABLE rpk_missing ADD COLUMN a int DEFAULT 42; +SELECT * FROM rpk_missing; +REPACK (CONCURRENTLY) rpk_missing; +SELECT * FROM rpk_missing; +DROP TABLE rpk_missing; 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..eaae02ba936ad --- /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..72038fc835f45 --- /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..3d97e2b53755e --- /dev/null +++ b/doc/src/sgml/func/func-json.sgml @@ -0,0 +1,4086 @@ + + 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. If the query returns + no rows, an empty JSON array is returned. + + + 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..bb41387f873c3 --- /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..6d9636bd125c7 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..7d3c3bb66d833 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 @@ -7036,15 +7058,20 @@ int PQrequestCancel(PGconn *conn); to send simple function calls to the server. - + - This interface is somewhat obsolete, as one can achieve similar + This interface is unsafe and should not be used. When + result_is_int is set to 0, + PQfn may write data beyond the end of + result_buf, regardless of whether the buffer has + enough space for the requested number of bytes. Furthermore, it is + obsolete, as one can achieve similar performance and greater functionality by setting up a prepared statement to define the function call. Then, executing the statement with binary transmission of parameters and results substitutes for a fast-path function call. - + The function PQfnPQfn @@ -9160,12 +9187,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 +9444,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 +9629,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 +10394,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 +10447,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 +10455,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 +10475,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 +10539,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 +10557,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 +10641,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 +10821,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 +11067,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 +11084,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 +11310,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..9e7868487de0c 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 @@ -47,15 +47,15 @@ - 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). @@ -68,7 +68,7 @@ Replicating between PostgreSQL instances on different platforms (for - example Linux to Windows) + example Linux to Windows). @@ -80,7 +80,8 @@ - Sharing a subset of the database between multiple databases. + Firing triggers for individual changes as they arrive on the + subscriber. @@ -89,7 +90,7 @@ The subscriber database behaves in the same way as any other PostgreSQL instance and can be used as a publisher for other databases by defining its - own publications. When the subscriber is treated as read-only by + own publications. When the subscriber is treated as read-only by an application, there will be no conflicts from a single subscription. On the other hand, if there are other writes done either by an application or by other subscribers to the same set of tables, conflicts can arise. @@ -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. @@ -202,8 +211,8 @@ A subscription is the downstream side of logical replication. The node where a subscription is defined is referred to as the subscriber. A subscription defines the connection - to another database and set of publications (one or more) to which it wants - to subscribe. + to another database and the set of publications (one or more) to which it + wants to subscribe. @@ -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) + @@ -945,7 +988,7 @@ HINT: To initiate replication, you must manually create the replication slot, e - If the subscriber is in a release prior to 15, copy pre-existing data + If the subscriber is in a release prior to 15, copying pre-existing data doesn't use row filters even if they are defined in the publication. This is because old releases can only copy the entire table data. @@ -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. 5) AND (c = 'NSW'::text)) "p2" @@ -1069,7 +1112,7 @@ Publications: f | integer | | | Indexes: "t2_pkey" PRIMARY KEY, btree (d) -Publications: +Included in publications: "p2" WHERE (e = 99) "p3" WHERE (d = 10) @@ -1082,12 +1125,12 @@ Publications: i | integer | | | Indexes: "t3_pkey" PRIMARY KEY, btree (g) -Publications: +Included in publications: "p3" WHERE (g = 10) ]]> - 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) @@ -1483,14 +1526,14 @@ Tables: e | text | | | Indexes: "t1_pkey" PRIMARY KEY, btree (id) -Publications: +Included in publications: "p1" (id, a, b, d) - 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 @@ -2156,7 +2458,7 @@ CONTEXT: processing remote data for replication origin "pg_16395" during "INSER Initial Snapshot - The initial data in existing subscribed tables are snapshotted and + The initial data in existing subscribed tables is snapshotted and copied in parallel instances of a special kind of apply process. These special apply processes are dedicated table synchronization workers, spawned for each table to be synchronized. Each table @@ -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..4a21bdb5de779 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..8517d837b780a 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 = [] @@ -135,7 +137,7 @@ endif # Full documentation as html, text # if docs_dep.found() - html = custom_target('html', + html = custom_target('html-custom', input: ['stylesheet.xsl', postgres_full_xml], output: 'html', depfile: 'html.d', @@ -144,7 +146,7 @@ if docs_dep.found() ) alldocs += html - install_doc_html = custom_target('install-html', + install_doc_html = custom_target('install-html-custom', output: 'install-html', command: [ python, install_files, '--prefix', dir_prefix, @@ -187,7 +189,7 @@ endif # if docs_dep.found() # FIXME: implement / consider sqlmansectnum logic - man = custom_target('man', + man = custom_target('man-custom', input: ['stylesheet-man.xsl', postgres_full_xml], output: ['man1', 'man3', 'man7'], depfile: 'man.d', @@ -196,7 +198,7 @@ if docs_dep.found() ) alldocs += man - install_doc_man = custom_target('install-man', + install_doc_man = custom_target('install-man-custom', output: 'install-man', input: man, command: [ 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..b1395ec951e64 --- /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_index.sgml b/doc/src/sgml/ref/alter_index.sgml index 1d42d05d85816..fb7096c16ea64 100644 --- a/doc/src/sgml/ref/alter_index.sgml +++ b/doc/src/sgml/ref/alter_index.sgml @@ -97,6 +97,11 @@ ALTER INDEX ALL IN TABLESPACE name index cannot be dropped by itself, and will automatically be dropped if its parent index is dropped. + + If the named index is already attached to the altered index, the + command will attempt to validate the parent index if the parent is + currently invalid. + 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..e4f0b6b16c7db 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. @@ -272,7 +331,7 @@ ALTER SUBSCRIPTION name RENAME TO < process reports an error if any prepared transactions done by the logical replication worker (from when two_phase parameter was still true) are found. You can resolve - prepared transactions on the publisher node, or manually roll back them + prepared transactions on the publisher node, or manually roll them back on the subscriber, and then try again. The transactions prepared by logical replication worker corresponding to a particular subscription have the following pattern: pg_gid_%u_%u @@ -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..8078de2b1b2b1 --- /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 a new 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..f82d640e6caee 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 @@ -151,7 +157,7 @@ CREATE PUBLICATION name - When a partitioned table is published via schema level publication, all + When a partitioned table is published via a schema-level publication, all of its existing and future partitions are implicitly considered to be part of the publication, regardless of whether they are from the publication schema or not. So, even operations that are performed directly on a @@ -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: @@ -174,7 +238,7 @@ CREATE PUBLICATION name This parameter determines which DML operations will be published by - the new publication to the subscribers. The value is + the new publication to the subscribers. The value is a comma-separated list of operations. The allowed operations are insert, update, delete, and truncate. @@ -214,7 +278,7 @@ CREATE PUBLICATION name If the subscriber is from a release prior to 18, then initial table - synchronization won't copy generated columns even if parameter + synchronization won't copy generated columns even if the parameter publish_generated_columns is stored in the publisher. @@ -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..5e77ddd556f73 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,17 @@ 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 + . @@ -271,14 +317,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 +510,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 +590,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 +658,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 +751,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 + . @@ -760,6 +800,21 @@ PostgreSQL documentation + + + + + 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. + + + + @@ -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..045516464a489 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,14 @@ PostgreSQL documentation - + + (deprecated) - 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..cd5dd0319913b --- /dev/null +++ b/doc/src/sgml/ref/wait_for.sgml @@ -0,0 +1,383 @@ + + + + + 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 written to disk on a standby server, but not yet + necessarily flushed. This is faster than + standby_flush but provides weaker durability + guarantees since the data may still be in operating system + buffers. This is satisfied by WAL already present on the + standby from a base backup, archive restore, or prior + streaming, as well as WAL newly received from the primary. + This mode can only be used during recovery. + + + + + standby_flush: Wait for the WAL containing the + LSN to be flushed to disk on a standby server. This provides + a durability guarantee without waiting for the WAL to be + applied. This is satisfied by WAL already present on the + standby from a base backup, archive restore, or prior + streaming, as well as WAL newly received from the primary. + 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, or to have already been replayed + from WAL present on the standby. The standby_write mode + waits for the WAL to be written to the operating system, or to have + already been replayed, which is faster than flush for newly received + WAL 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. + + + + WAIT FOR compares only the numeric + LSN; it has no notion of which timeline a WAL + record belongs to. This matters when a standby continues recovery + across an upstream timeline switch — for example, a cascading + standby whose upstream gets promoted. In that case + WAIT FOR will return success + as soon as the position used by the selected wait mode reaches or + passes the numeric LSN, regardless of which + timeline that LSN belongs to. Applications that need to + confirm the target refers to the expected timeline must validate + the timeline themselves. + + + + 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..ff66009e046be --- /dev/null +++ b/doc/src/sgml/release-19.sgml @@ -0,0 +1,3417 @@ + + + + + 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) +§ + + + + + + + +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 repeated UTF-8 case-folding operations (Andreas Karlsson) +§ + + + + + + + +Improve performance of hash index bulk-deletion and GIN index vacuuming using streaming reads (Xuneng Zhou) +§ +§ + + + + + + + +Improve sort performance using radix sort (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) +§ + + + + + + + + + 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 written, flushed, or 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) +§ + + + + + + + +Use AVX2 CPU instructions for calculating page checksums (Matthew Sterrett, Andrew Kim) +§ + + + + + + + +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, planner_shutdown_hook, joinrel_setup_hook, and join_path_setup_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 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) +§ + + + + + + + +Change btree_gin to support all btree-supported cross-type comparisons (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..dfa292c2c3a0a 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..c32931edde39d 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..bae16d7fb53b6 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. @@ -3752,7 +3811,7 @@ LWLockPadded *GetNamedLWLockTranche(const char *tranche_name) - Requesting LWLocks After Startup + Requesting LWLocks after Startup There is another, more flexible method of obtaining LWLocks that can be @@ -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..2268cc277bce5 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,104 @@ 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, and fill in + * any values from attmissingval; then form a new tuple and return it. If no + * attributes need to be changed, 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; + + /* + * A short tuple might require values from attmissing val, so activate the + * coding unconditionally in that case. The value might legitimally be + * NULL otherwise, so this is slightly wasteful, but it probably beats + * having to test each attribute for presence of attmissingval each time. + */ + if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) + needs_reform = true; + + /* + * If the column has been dropped but a value is still present, we can + * optimize storage now by getting rid of it. + */ + if (!needs_reform) + { + for (int i = 0; i < newTupDesc->natts; i++) + { + if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, newTupDesc)) + { + needs_reform = true; + break; + } + } + } + + /* Skip work if no changes are needed */ + 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 +2565,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..3df2c752eadef 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) { @@ -587,8 +604,11 @@ btestimateparallelscan(Relation rel, int nkeys, int norderbys) * also require a skip array. * * Every skip array must have space to store its scan key's sk_flags. + * We also need space for each skip array's unused btps_arrElems slot + * (we need to be able to subscript btps_arrElems using a simple + * so->arrayKeys[]-wise offset for any subsequent SAOP arrays). */ - estnbtreeshared = add_size(estnbtreeshared, sizeof(int)); + estnbtreeshared = add_size(estnbtreeshared, sizeof(int) * 2); /* Consider space required to store a datum of opclass input type */ attr = TupleDescCompactAttr(rel->rd_att, attnum - 1); @@ -622,6 +642,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 +1127,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 +1188,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..3aa31e7f80529 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 @@ -1658,13 +1676,13 @@ FinishPreparedTransaction(const char *gid, bool isCommit) LWLockRelease(TwoPhaseStateLock); /* Count the prepared xact as committed or aborted */ - AtEOXact_PgStat(isCommit, false); + AtEOXact_PgStat(isCommit, false, true); /* * 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..94e49c54377f2 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 @@ -314,6 +316,15 @@ typedef struct XactCallbackItem static XactCallbackItem *Xact_callbacks = NULL; +/* + * When true, suppress the pg_stat_database xact_commit/xact_rollback bump + * for the current transaction end. Must only be set via + * AbortCurrentTransactionWithoutXactStats(); assertions in + * StartTransaction() and in the wrapper itself guard against the flag + * leaking across transactions. + */ +static bool xactSkipXactStats = false; + /* * List of add-on start- and end-of-subxact callbacks */ @@ -551,9 +562,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 +573,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 +674,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 +692,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 +1055,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 +1451,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 +1470,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 +1500,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 +1588,7 @@ RecordTransactionCommit(void) */ if (markXidCommitted) { - MyProc->delayChkptFlags &= ~DELAY_CHKPT_START; + MyProc->delayChkptFlags &= ~DELAY_CHKPT_IN_COMMIT; END_CRIT_SECTION(); } @@ -1797,8 +1848,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 +1876,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); /* @@ -2076,6 +2127,7 @@ StartTransaction(void) /* check the current transaction state */ Assert(s->state == TRANS_DEFAULT); + Assert(!xactSkipXactStats); /* * Set the current transaction state information appropriately during @@ -2472,10 +2524,11 @@ CommitTransaction(void) AtEOXact_Files(true); AtEOXact_ComboCid(); AtEOXact_HashTables(true); - AtEOXact_PgStat(true, is_parallel_worker); + AtEOXact_PgStat(true, is_parallel_worker, true); AtEOXact_Snapshot(true, false); AtEOXact_ApplyLauncher(true); AtEOXact_LogicalRepWorkers(true); + AtEOXact_LogicalCtl(); pgstat_report_xact_timestamp(0); ResourceOwnerDelete(TopTransactionResourceOwner); @@ -2515,7 +2568,7 @@ static void PrepareTransaction(void) { TransactionState s = CurrentTransactionState; - TransactionId xid = GetCurrentTransactionId(); + FullTransactionId fxid = GetCurrentFullTransactionId(); GlobalTransaction gxact; TimestampTz prepared_at; @@ -2644,7 +2697,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 +2747,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 +2791,9 @@ PrepareTransaction(void) PostPrepare_smgr(); - PostPrepare_MultiXact(xid); + PostPrepare_MultiXact(fxid); - PostPrepare_PredicateLocks(xid); + PostPrepare_PredicateLocks(fxid); ResourceOwnerRelease(TopTransactionResourceOwner, RESOURCE_RELEASE_LOCKS, @@ -2771,6 +2824,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 +2885,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(); @@ -2990,9 +3049,10 @@ AbortTransaction(void) AtEOXact_Files(false); AtEOXact_ComboCid(); AtEOXact_HashTables(false); - AtEOXact_PgStat(false, is_parallel_worker); + AtEOXact_PgStat(false, is_parallel_worker, !xactSkipXactStats); AtEOXact_ApplyLauncher(false); AtEOXact_LogicalRepWorkers(false); + AtEOXact_LogicalCtl(); pgstat_report_xact_timestamp(0); } @@ -3459,6 +3519,31 @@ AbortCurrentTransaction(void) } } +/* + * AbortCurrentTransactionWithoutXactStats + * + * Like AbortCurrentTransaction(), but do not count the transaction abort in + * pg_stat_database.xact_rollback. This is for internal cleanup aborts that + * release transaction-local resources but do not represent a user-visible + * transaction rollback. + */ +void +AbortCurrentTransactionWithoutXactStats(void) +{ + Assert(!xactSkipXactStats); + + xactSkipXactStats = true; + PG_TRY(); + { + AbortCurrentTransaction(); + } + PG_FINALLY(); + { + xactSkipXactStats = false; + } + PG_END_TRY(); +} + /* * AbortCurrentTransactionInternal - a function doing an iteration of work * regarding handling the current transaction abort. In the case of @@ -3674,7 +3759,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 +4609,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 +4639,7 @@ ReleaseSavepoint(const char *name) if (xact == target) break; xact = xact->parent; - Assert(PointerIsValid(xact)); + Assert(xact); } } @@ -4632,13 +4718,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 +4753,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" */ @@ -5238,6 +5324,11 @@ AbortSubTransaction(void) */ LWLockReleaseAll(); + /* + * Cleanup waiting for LSN if any. + */ + WaitLSNCleanup(); + pgstat_report_wait_end(); pgstat_progress_end_command(); @@ -5688,12 +5779,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 +5997,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 +6150,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 +6221,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 +6234,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 +6373,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 +6511,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..f0434da40c945 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,12 @@ XLogFlush(XLogRecPtr record) /* wake up walsenders now that we've released heavily contended locks */ WalSndWakeupProcessRequests(true, !RecoveryInProgress()); + /* + * Wake up processes waiting for primary flush LSN to reach current flush + * position. + */ + 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 +2964,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 +3093,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 +3123,12 @@ XLogBackgroundFlush(void) /* wake up walsenders now that we've released heavily contended locks */ WalSndWakeupProcessRequests(true, !RecoveryInProgress()); + /* + * Wake up processes waiting for primary flush LSN to reach current flush + * position. + */ + 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 +3144,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, or if the minimum recovery point + * must be updated. * - * Returns true if a flush is still needed. (It may be that someone else - * is already in process of flushing that far, however.) + * 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 +3162,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 +3181,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 +3202,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 +4284,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 +4309,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 +4319,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 +4529,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 +4590,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; @@ -4706,55 +4619,367 @@ ReadControlFile(void) errmsg("\"%s\" must be at least twice \"%s\"", "max_wal_size", "wal_segment_size"))); - UsableBytesInSegment = - (wal_segment_size / XLOG_BLCKSZ * UsableBytesInPage) - - (SizeOfXLogLongPHD - SizeOfXLogShortPHD); + UsableBytesInSegment = + (wal_segment_size / XLOG_BLCKSZ * UsableBytesInPage) - + (SizeOfXLogLongPHD - SizeOfXLogShortPHD); + + 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 +DataChecksumsOff(void) +{ + bool ret; + + SpinLockAcquire(&XLogCtl->info_lck); + ret = (XLogCtl->data_checksum_version == PG_DATA_CHECKSUM_OFF); + SpinLockRelease(&XLogCtl->info_lck); + + return ret; +} + +bool +DataChecksumsOn(void) +{ + bool ret; + + SpinLockAcquire(&XLogCtl->info_lck); + ret = (XLogCtl->data_checksum_version == PG_DATA_CHECKSUM_VERSION); + SpinLockRelease(&XLogCtl->info_lck); + + return ret; +} + +bool +DataChecksumsInProgressOn(void) +{ + bool ret; + + SpinLockAcquire(&XLogCtl->info_lck); + ret = (XLogCtl->data_checksum_version == PG_DATA_CHECKSUM_INPROGRESS_ON); + SpinLockRelease(&XLogCtl->info_lck); + + return ret; +} + +/* + * 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; + + /* + * 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); + + LWLockAcquire(ControlFileLock, LW_EXCLUSIVE); + ControlFile->data_checksum_version = PG_DATA_CHECKSUM_INPROGRESS_ON; + UpdateControlFile(); + LWLockRelease(ControlFileLock); + + barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_ON); + + MyProc->delayChkptFlags &= ~DELAY_CHKPT_START; + END_CRIT_SECTION(); + + 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; + + 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); + + /* + * 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); + + barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_ON); + + MyProc->delayChkptFlags &= ~DELAY_CHKPT_START; + END_CRIT_SECTION(); + + RequestCheckpoint(CHECKPOINT_FORCE | CHECKPOINT_WAIT | CHECKPOINT_FAST); + 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; + + 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, or in the process of being + * 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 || + XLogCtl->data_checksum_version == PG_DATA_CHECKSUM_INPROGRESS_ON) + { + 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); + + LWLockAcquire(ControlFileLock, LW_EXCLUSIVE); + ControlFile->data_checksum_version = PG_DATA_CHECKSUM_INPROGRESS_OFF; + UpdateControlFile(); + LWLockRelease(ControlFileLock); + + barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_OFF); + + MyProc->delayChkptFlags &= ~DELAY_CHKPT_START; + END_CRIT_SECTION(); + + RequestCheckpoint(CHECKPOINT_FORCE | CHECKPOINT_WAIT | CHECKPOINT_FAST); + 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-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); + + LWLockAcquire(ControlFileLock, LW_EXCLUSIVE); + ControlFile->data_checksum_version = PG_DATA_CHECKSUM_OFF; + UpdateControlFile(); + LWLockRelease(ControlFileLock); + + barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_OFF); - CalculateCheckpointSegments(); + MyProc->delayChkptFlags &= ~DELAY_CHKPT_START; + END_CRIT_SECTION(); - /* Make the initdb settings visible as GUC variables, too */ - SetConfigOption("data_checksums", DataChecksumsEnabled() ? "yes" : "no", - PGC_INTERNAL, PGC_S_DYNAMIC_DEFAULT); + RequestCheckpoint(CHECKPOINT_FORCE | CHECKPOINT_WAIT | CHECKPOINT_FAST); + 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); + Assert(InterruptHoldoffCount > 0); + 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 +5047,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 +5236,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 +5271,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 +5289,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 +5332,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 +5367,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 +5426,25 @@ 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 +5455,7 @@ void BootStrapXLOG(uint32 data_checksum_version) { CheckPoint checkPoint; - char *buffer; + PGAlignedXLogBlock buffer; XLogPageHeader page; XLogLongPageHeader longpage; XLogRecord *record; @@ -5247,10 +5484,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 +5498,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 +5513,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 +5521,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 +5537,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 +5568,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 +5608,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 +5616,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 +5861,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 +5890,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 +5920,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 +5929,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 +6000,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 +6024,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 +6316,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 +6326,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 +6428,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 +6438,7 @@ StartupXLOG(void) * disregard. */ Assert(newTLI == endOfRecoveryInfo->lastRecTLI); - Assert(!XLogRecPtrIsInvalid(abortedRecPtr)); + Assert(XLogRecPtrIsValid(abortedRecPtr)); EndOfLog = missingContrecPtr; } @@ -6227,8 +6472,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 +6481,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 +6542,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 +6578,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. * @@ -6355,12 +6650,27 @@ StartupXLOG(void) ControlFile->state = DB_IN_PRODUCTION; SpinLockAcquire(&XLogCtl->info_lck); + ControlFile->data_checksum_version = XLogCtl->data_checksum_version; XLogCtl->SharedRecoveryState = RECOVERY_STATE_DONE; SpinLockRelease(&XLogCtl->info_lck); 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 +6815,7 @@ PerformRecoveryXLogAction(void) else { RequestCheckpoint(CHECKPOINT_END_OF_RECOVERY | - CHECKPOINT_IMMEDIATE | + CHECKPOINT_FAST | CHECKPOINT_WAIT); } @@ -6627,7 +6937,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 +7124,7 @@ ShutdownXLOG(int code, Datum arg) WalSndWaitStopping(); if (RecoveryInProgress()) - CreateRestartPoint(CHECKPOINT_IS_SHUTDOWN | CHECKPOINT_IMMEDIATE); + CreateRestartPoint(CHECKPOINT_IS_SHUTDOWN | CHECKPOINT_FAST); else { /* @@ -6826,10 +7136,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 +7170,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 +7234,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 +7259,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 +7362,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 +7439,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 +7466,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 +7515,14 @@ 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. + */ + SpinLockAcquire(&XLogCtl->info_lck); + checkPoint.dataChecksumState = XLogCtl->data_checksum_version; + SpinLockRelease(&XLogCtl->info_lck); + if (shutdown) { XLogRecPtr curInsert = XLogBytePosToRecPtr(Insert->CurrBytePos); @@ -7243,9 +7575,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 +7610,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 +7642,8 @@ CreateCheckPoint(int flags) checkPoint.nextOid += TransamVariables->oidCount; LWLockRelease(OidGenLock); + 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(); @@ -7456,11 +7801,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 +7835,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 +7879,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 +7934,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 +7985,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 +7996,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 +8026,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 +8095,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 +8171,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 +8281,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 +8293,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 +8310,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 +8360,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 +8421,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 +8501,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 +8530,7 @@ KeepLogSeg(XLogRecPtr recptr, XLogSegNo *logSegNo) * summarized. */ keep = GetOldestUnsummarizedLSN(NULL, NULL); - if (keep != InvalidXLogRecPtr) + if (XLogRecPtrIsValid(keep)) { XLogSegNo unsummarized_segno; @@ -8277,12 +8633,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 +8723,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. @@ -8481,8 +8879,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,12 +8926,10 @@ xlog_redo(XLogReaderState *record) /* ControlFile->checkPointCopy always tracks the latest ckpt XID */ LWLockAcquire(ControlFileLock, LW_EXCLUSIVE); ControlFile->checkPointCopy.nextXid = checkPoint.nextXid; - LWLockRelease(ControlFileLock); + ControlFile->data_checksum_version = checkPoint.dataChecksumState; - /* Update shared-memory copy of checkpoint XID/epoch */ - SpinLockAcquire(&XLogCtl->info_lck); - XLogCtl->ckptFullXid = checkPoint.nextXid; - SpinLockRelease(&XLogCtl->info_lck); + UpdateControlFile(); + LWLockRelease(ControlFileLock); /* * We should've already switched to the new TLI before replaying this @@ -8546,6 +8942,14 @@ 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) { @@ -8591,11 +8995,6 @@ xlog_redo(XLogReaderState *record) ControlFile->checkPointCopy.nextXid = checkPoint.nextXid; 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 +9003,14 @@ 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_OVERWRITE_CONTRECORD) { @@ -8644,6 +9051,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 +9100,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 +9122,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 +9165,98 @@ 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; + SetLocalDataChecksumState(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); + + LWLockAcquire(ControlFileLock, LW_EXCLUSIVE); + ControlFile->data_checksum_version = state.new_checksum_state; + UpdateControlFile(); + LWLockRelease(ControlFileLock); + + /* + * 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 +9430,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 +9508,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 +9527,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 +9553,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 +9728,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 +9741,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 +10081,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 +10101,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 +10145,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 +10160,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..73b78a83fa744 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,19 @@ PerformWalRecovery(void) */ ApplyWalRecord(xlogreader, record, &replayTLI); + /* + * Wake up processes waiting for standby replay, write, or flush + * LSN to reach current replay position. Replay implies that the + * WAL was already written and flushed to disk, so write and flush + * waiters can be woken at the replay position too. + */ + WaitLSNWakeup(WAIT_LSN_TYPE_STANDBY_REPLAY, + XLogRecoveryCtl->lastReplayedEndRecPtr); + WaitLSNWakeup(WAIT_LSN_TYPE_STANDBY_WRITE, + XLogRecoveryCtl->lastReplayedEndRecPtr); + WaitLSNWakeup(WAIT_LSN_TYPE_STANDBY_FLUSH, + XLogRecoveryCtl->lastReplayedEndRecPtr); + /* Exit loop if we reached inclusive recovery target */ if (recoveryStopsAfter(xlogreader)) { @@ -1871,6 +1836,7 @@ PerformWalRecovery(void) recoveryPausesHere(true); /* drop into promote */ + pg_fallthrough; case RECOVERY_TARGET_ACTION_PROMOTE: break; @@ -1880,9 +1846,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 +2019,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 +2058,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 +2067,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 +2095,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 +2157,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 +2172,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 +2190,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 +2221,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 +2259,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 +2294,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 +2378,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 +2587,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 +2755,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 +2876,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 +3112,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 +3143,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 +3181,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 +3285,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 +3382,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 +3399,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 +3518,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 +3655,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 +3707,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 +3853,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 +3862,7 @@ WaitForWALToBecomeAvailable(XLogRecPtr RecPtr, bool randAccess, RequestXLogStreaming(tli, ptr, PrimaryConnInfo, PrimarySlotName, wal_receiver_create_temp_slot); - flushedUpto = 0; + flushedUpto = InvalidXLogRecPtr; } /* @@ -3985,7 +3974,7 @@ WaitForWALToBecomeAvailable(XLogRecPtr RecPtr, bool randAccess, */ if (!streaming_reply_sent) { - WalRcvForceReply(); + WalRcvRequestApplyReply(); streaming_reply_sent = true; } @@ -4051,7 +4040,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 +4166,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 +4323,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 +4748,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 +4833,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 +4994,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 +5049,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..582dde3b06128 --- /dev/null +++ b/src/backend/access/transam/xlogwait.c @@ -0,0 +1,495 @@ +/*------------------------------------------------------------------------- + * + * 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. Provide memory + * barrier semantics before getting the value. + */ +XLogRecPtr +GetCurrentLSNForWaitType(WaitLSNType lsnType) +{ + Assert(lsnType >= 0 && lsnType < WAIT_LSN_TYPE_COUNT); + + /* + * All of the cases below provide memory barrier semantics: + * GetWalRcvWriteRecPtr() and GetFlushRecPtr() have explicit barriers, + * while GetXLogReplayRecPtr() and GetWalRcvFlushRecPtr() use spinlocks. + */ + switch (lsnType) + { + case WAIT_LSN_TYPE_STANDBY_REPLAY: + return GetXLogReplayRecPtr(NULL); + + case WAIT_LSN_TYPE_STANDBY_WRITE: + { + XLogRecPtr recptr = GetWalRcvWriteRecPtr(); + XLogRecPtr replay = GetXLogReplayRecPtr(NULL); + + /* + * Use the replay position as a floor. WAL up to the replay + * point is already on disk from a base backup, archive + * restore, or prior streaming, so there is no reason to wait + * for the walreceiver to re-receive it. + */ + return Max(recptr, replay); + } + + case WAIT_LSN_TYPE_STANDBY_FLUSH: + { + XLogRecPtr recptr = GetWalRcvFlushRecPtr(NULL, NULL); + XLogRecPtr replay = GetXLogReplayRecPtr(NULL); + + /* Same floor as standby_write; see comment above. */ + return Max(recptr, replay); + } + + 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; + } + /* Pairs with pg_atomic_read_membarrier_u64() in WaitLSNWakeup(). */ + pg_atomic_write_membarrier_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). Pairs + * with pg_atomic_write_membarrier_u64() in updateMinWaitedLSN(). + */ + if (XLogRecPtrIsValid(currentLSN) && + pg_atomic_read_membarrier_u64(&waitLSNState->minWaitedLSN[i]) > currentLSN) + return; + + wakeupWaiters(lsnType, currentLSN); +} + +/* + * Clean up any LSN wait state for the current 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")); + + 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..7678ab13f6a8d 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 * * @@ -504,11 +504,15 @@ CheckAttributeNamesTypes(TupleDesc tupdesc, char relkind, */ for (i = 0; i < natts; i++) { - CheckAttributeType(NameStr(TupleDescAttr(tupdesc, i)->attname), - TupleDescAttr(tupdesc, i)->atttypid, - TupleDescAttr(tupdesc, i)->attcollation, + Form_pg_attribute attr = TupleDescAttr(tupdesc, i); + + if (attr->attisdropped) + continue; + CheckAttributeType(NameStr(attr->attname), + attr->atttypid, + attr->attcollation, NIL, /* assume we're creating a new rowtype */ - flags | (TupleDescAttr(tupdesc, i)->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL ? CHKATYPE_IS_VIRTUAL : 0)); + flags | (attr->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL ? CHKATYPE_IS_VIRTUAL : 0)); } } @@ -654,6 +658,16 @@ CheckAttributeType(const char *attname, containing_rowtypes, flags); } + else if (att_typtype == TYPTYPE_MULTIRANGE) + { + /* + * If it's a multirange, recurse to check its plain range type. + */ + CheckAttributeType(attname, get_multirange_range(atttypid), + InvalidOid, /* range types are not collatable */ + containing_rowtypes, + flags); + } else if (OidIsValid((att_typelem = get_element_type(atttypid)))) { /* @@ -664,6 +678,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 +746,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 +1123,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 @@ -1323,12 +1347,14 @@ heap_create_with_catalog(const char *relname, /* * Decide whether to create a pg_type entry for the relation's rowtype. * These types are made except where the use of a relation as such is an - * implementation detail: toast tables, sequences and indexes. + * implementation detail: toast tables, sequences, indexes, and property + * graphs. */ if (!(relkind == RELKIND_SEQUENCE || relkind == RELKIND_TOASTVALUE || relkind == RELKIND_INDEX || - relkind == RELKIND_PARTITIONED_INDEX)) + relkind == RELKIND_PARTITIONED_INDEX || + relkind == RELKIND_PROPGRAPH)) { Oid new_array_oid; ObjectAddress new_type_addr; @@ -2449,7 +2475,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 +2609,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 +2651,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 +2686,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 +2902,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 +2923,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 +3025,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 +3243,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 +3362,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 +3586,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..050b7829eb0ad 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,140 @@ 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; + HeapTuple tuple; + Form_pg_propgraph_element_label pgelform; + ObjectAddress oa; + + rel = table_open(PropgraphElementLabelRelationId, AccessShareLock); + tuple = get_catalog_object_by_oid(rel, + Anum_pg_propgraph_element_label_oid, + object->objectId); + if (!HeapTupleIsValid(tuple)) + { + if (!missing_ok) + elog(ERROR, "could not find tuple for element label %u", object->objectId); + + 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)); + + 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; + HeapTuple tuple; + Form_pg_propgraph_label_property plpform; + ObjectAddress oa; + + rel = table_open(PropgraphLabelPropertyRelationId, AccessShareLock); + tuple = get_catalog_object_by_oid(rel, + Anum_pg_propgraph_label_property_oid, + object->objectId); + if (!HeapTupleIsValid(tuple)) + { + if (!missing_ok) + elog(ERROR, "could not find tuple for label property %u", object->objectId); + + 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)); + + 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 +4392,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 +4518,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 +4885,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 +4978,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 +5100,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 +5144,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 +5160,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 +5367,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 +6145,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 +6462,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 +6518,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..5c457d9aca8de 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,55 @@ 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 *relname; + const char *errormsg; + + if (pri->except) + { + relname = RelationGetQualifiedRelationName(targetrel); + errormsg = gettext_noop("cannot specify relation \"%s\" in the publication EXCEPT clause"); + } + else + { + relname = RelationGetRelationName(targetrel); + 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, relname), + 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, relname), 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, relname), 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, relname), 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, relname), errdetail("This operation is not supported for unlogged tables."))); } @@ -115,8 +133,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 +154,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 +170,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 +308,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 +458,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 +521,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 +536,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 +559,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 +575,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 +625,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 +865,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 +897,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 * +GetRelationIncludedPublications(Oid relid) +{ + return get_relation_publications(relid, false); +} + +/* + * Gets list of publication oids which has relation in the EXCEPT clause. */ List * -GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt) +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 +931,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 +948,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 +964,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 +1030,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 +1074,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 +1096,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 +1193,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 +1271,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 +1302,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. + * + * 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. * - * Returns pubid, relid, column list, row filter for each table. + * 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 +1422,6 @@ pg_get_publication_tables(PG_FUNCTION_ARGS) { TupleDesc tupdesc; MemoryContext oldcontext; - ArrayType *arr; Datum *elems; int nelems, i; @@ -1134,6 +1430,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 +1445,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 +1454,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 +1508,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 +1542,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 +1612,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 +1654,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..cc516e270203c --- /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 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 vertices. + */ + 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 + * vertices. 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 + * GRAPH 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 same label OID as the one being + * checked but a different element OID. + */ + 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 no 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 property 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 property 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..860e2aecbe9b1 --- /dev/null +++ b/src/backend/commands/repack.c @@ -0,0 +1,3738 @@ +/*------------------------------------------------------------------------- + * + * 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 "replication/logicalrelation.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; + + /* The latest column we need to deform to have the tuple identity */ + AttrNumber cc_last_key_attno; + + /* 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 bool identity_key_equal(ChangeContext *chgcxt, + TupleTableSlot *locator, + TupleTableSlot *candidate); +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 a non-deferrable 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 = GetRelationIdentityOrPK(rel); + 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; + HeapTuple classtup; + Form_pg_class classForm; + 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 */ + classtup = SearchSysCache1(RELOID, ObjectIdGetDatum(index->indrelid)); + if (!HeapTupleIsValid(classtup)) + { + UnlockRelationOid(index->indrelid, AccessShareLock); + continue; + } + classForm = (Form_pg_class) GETSTRUCT(classtup); + + /* Skip temp relations belonging to other sessions */ + if (classForm->relpersistence == RELPERSISTENCE_TEMP && + !isTempOrTempToastNamespace(classForm->relnamespace)) + { + ReleaseSysCache(classtup); + UnlockRelationOid(index->indrelid, AccessShareLock); + continue; + } + + ReleaseSysCache(classtup); + + /* 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; + } + + /* Skip temp relations belonging to other sessions */ + if (class->relpersistence == RELPERSISTENCE_TEMP && + !isTempOrTempToastNamespace(class->relnamespace)) + { + 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); + memcpy(value, &chunk_header, VARHDRSZ); + 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 = false; + + /* + * 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); + while (index_getnext_slot(scan, ForwardScanDirection, retrieved)) + { + /* Be wary of temporal constraints */ + if (scan->xs_recheck && !identity_key_equal(chgcxt, locator, retrieved)) + { + CHECK_FOR_INTERRUPTS(); + continue; + } + + retval = true; + break; + } + index_endscan(scan); + + return retval; +} + +/* + * Check whether the candidate tuple matches the locator tuple on all replica + * identity key columns, using the same equality operators as the identity + * index scan. The locator tuple has already been loaded into cc_ident_key. + * + * This is needed to filter lossy index matches, such as GiST multirange scans + * used for temporal constraints. + */ +static bool +identity_key_equal(ChangeContext *chgcxt, TupleTableSlot *locator, + TupleTableSlot *candidate) +{ + slot_getsomeattrs(locator, chgcxt->cc_last_key_attno); + slot_getsomeattrs(candidate, chgcxt->cc_last_key_attno); + + for (int i = 0; i < chgcxt->cc_ident_key_nentries; i++) + { + ScanKey entry = &chgcxt->cc_ident_key[i]; + AttrNumber attno = chgcxt->cc_ident_index->rd_index->indkey.values[i]; + + Assert(attno > 0); + + if (locator->tts_isnull[attno - 1] != candidate->tts_isnull[attno - 1]) + return false; + + if (locator->tts_isnull[attno - 1]) + continue; + + if (!DatumGetBool(FunctionCall2Coll(&entry->sk_func, + entry->sk_collation, + candidate->tts_values[attno - 1], + entry->sk_argument))) + return false; + } + + return true; +} + +/* + * 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; + StrategyNumber eq_strategy; + + entry = &chgcxt->cc_ident_key[i]; + + opfamily = chgcxt->cc_ident_index->rd_opfamily[i]; + opcintype = chgcxt->cc_ident_index->rd_opcintype[i]; + eq_strategy = IndexAmTranslateCompareType(COMPARE_EQ, + chgcxt->cc_ident_index->rd_rel->relam, + opfamily, false); + if (eq_strategy == InvalidStrategy) + elog(ERROR, "could not find equality strategy for index operator family %u for type %u", + opfamily, opcintype); + opno = get_opfamily_member(opfamily, opcintype, opcintype, + eq_strategy); + if (!OidIsValid(opno)) + elog(ERROR, "missing operator %d(%u,%u) in opfamily %u", + eq_strategy, opcintype, opcintype, opfamily); + opcode = get_opcode(opno); + if (!OidIsValid(opcode)) + elog(ERROR, "missing oprcode for operator %u", opno); + + /* Initialize everything but argument. */ + ScanKeyInit(entry, + i + 1, + eq_strategy, opcode, + (Datum) 0); + entry->sk_collation = chgcxt->cc_ident_index->rd_indcollation[i]; + } + } + + /* Determine the last column we must deform to read the identity */ + chgcxt->cc_last_key_attno = InvalidAttrNumber; + for (int i = 0; i < chgcxt->cc_ident_key_nentries; i++) + { + AttrNumber attno = chgcxt->cc_ident_index->rd_index->indkey.values[i]; + + Assert(attno > 0); + chgcxt->cc_last_key_attno = Max(chgcxt->cc_last_key_attno, attno); + } + + 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 (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..7818f667edfa1 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] = + BoolGetDatum(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,30 @@ 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); + char *schemaname_lit = quote_literal_cstr(schemaname); + char *tablename_lit = quote_literal_cstr(tablename); + + appendStringInfo(&cmd, "AND NOT (N.nspname = %s AND C.relname = %s)\n", + schemaname_lit, tablename_lit); - appendStringInfo(&cmd, "AND NOT (N.nspname = '%s' AND C.relname = '%s')\n", - schemaname, tablename); + pfree(schemaname_lit); + pfree(tablename_lit); + } } res = walrcv_exec(wrconn, cmd.data, 1, tableRow); @@ -2150,7 +2795,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 +2818,145 @@ 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) + { + StringInfoData pubnames; + + /* Prepare the list of publication(s) for warning message. */ + 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); + char *schemaname_lit = quote_literal_cstr(schemaname); + char *seqname_lit = quote_literal_cstr(seqname); + + appendStringInfo(&cmd, + "AND NOT (N.nspname = %s AND C.relname = %s)\n", + schemaname_lit, seqname_lit); + + pfree(schemaname_lit); + pfree(seqname_lit); + } + + 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) { - StringInfo pubnames = makeStringInfo(); + StringInfoData pubnames; /* Prepare the list of publication(s) for warning message. */ - GetPublicationsStr(publist, pubnames, false); + 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 +2965,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 +3100,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 +3136,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 +3165,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 +3186,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 +3215,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..88451c9144811 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 *con; + + /* + * 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)); + + con = makeNode(Constraint); + con->contype = CONSTR_CHECK; + con->conname = pstrdup(ccname); + con->deferrable = false; + con->initdeferred = false; + con->is_enforced = ccenforced; + con->skip_validation = !ccvalid; + con->initially_valid = ccvalid; + con->is_no_inherit = ccnoinherit; + con->raw_expr = NULL; + con->cooked_expr = nodeToString(ccbin_node); + con->location = -1; + constraints = lappend(constraints, con); + } + + /* 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("cannot 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..b87b4b40d0763 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,23 @@ 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). + * + * Recompute qs first: the loop above refreshes it after each + * afterTriggerInvokeEvents() call (see comment there), but the "all + * fired" break exits without doing so, leaving qs potentially stale here. + */ + qs = &afterTriggers.query_stack[afterTriggers.query_depth]; + 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 +5284,12 @@ AfterTriggerFreeQuery(AfterTriggersQueryData *qs) { AfterTriggersTableData *table = (AfterTriggersTableData *) lfirst(lc); - ts = table->old_upd_tuplestore; - table->old_upd_tuplestore = NULL; - if (ts) - tuplestore_end(ts); - ts = table->new_upd_tuplestore; - table->new_upd_tuplestore = NULL; + ts = table->old_tuplestore; + table->old_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 +5308,9 @@ AfterTriggerFreeQuery(AfterTriggersQueryData *qs) */ qs->tables = NIL; list_free_deep(tables); + + list_free_deep(qs->batch_callbacks); + qs->batch_callbacks = NIL; } @@ -5253,6 +5350,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 +5359,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 +5429,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 +5581,9 @@ AfterTriggerEndSubXact(bool isCommit) } } } + + /* Reset in case a callback threw an error while firing. */ + afterTriggers.firing_batch_callbacks = false; } /* @@ -5506,17 +5619,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 +5643,7 @@ GetAfterTriggersTransitionTable(int event, */ static void TransitionTableAddTuple(EState *estate, + int event, TransitionCaptureState *transition_capture, ResultRelInfo *relinfo, TupleTableSlot *slot, @@ -5548,9 +5662,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 +5738,7 @@ AfterTriggerEnlargeQueryState(void) qs->events.tailfree = NULL; qs->fdw_tuplestore = NULL; qs->tables = NIL; + qs->batch_callbacks = NIL; ++init_depth; } @@ -5956,6 +6088,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 +6118,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 +6285,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 +6301,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 +6604,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 +6822,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..c4c3cdb5461a1 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 */ @@ -1479,6 +1494,13 @@ DefineRange(ParseState *pstate, CreateRangeStmt *stmt) /* we can look up the subtype name immediately */ multirangeNamespace = QualifiedNameGetCreationNamespace(defGetQualifiedName(defel), &multirangeTypeName); + + /* Check we have creation rights in target namespace */ + aclresult = object_aclcheck(NamespaceRelationId, multirangeNamespace, + GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_SCHEMA, + get_namespace_name(multirangeNamespace)); } else ereport(ERROR, @@ -1653,10 +1675,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 +1752,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 +1785,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 +1855,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 +1869,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 +1926,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 +1967,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 +2006,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 +3018,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 +3059,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 +3079,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 +3130,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 +3192,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 +3235,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 +3261,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 +3274,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 +3288,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 +3300,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 +3484,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..a4abb29cf64ef 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; @@ -1066,6 +1063,11 @@ get_all_vacuum_rels(MemoryContext vac_context, int options) classForm->relkind != RELKIND_PARTITIONED_TABLE) continue; + /* Skip temp relations belonging to other sessions */ + if (classForm->relpersistence == RELPERSISTENCE_TEMP && + !isTempOrTempToastNamespace(classForm->relnamespace)) + continue; + /* check permissions of relation */ if (!vacuum_is_permitted_for_relation(relid, classForm, options)) continue; @@ -1151,8 +1153,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 +1671,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 +1877,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 +1954,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 +1983,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 +2009,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 +2020,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 +2057,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 +2081,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 +2101,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 +2115,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 +2186,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 +2197,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 +2287,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 +2335,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 +2442,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 +2469,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..40a6ffde16b7c --- /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 dval; + + if (timeout_specified) + errorConflictingDefElem(defel, pstate); + timeout_specified = true; + + timeout_str = defGetString(defel); + + if (!parse_real(timeout_str, &dval, 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. + */ + dval = rint(dval); + + /* Range check */ + if (unlikely(isnan(dval) || !FLOAT8_FITS_IN_INT64(dval))) + ereport(ERROR, + errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE), + errmsg("timeout value is out of range")); + + if (dval < 0) + ereport(ERROR, + errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("timeout cannot be negative")); + + timeout = (int64) dval; + } + 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..0634af964a95b 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" @@ -176,6 +178,14 @@ static Datum ExecJustHashInnerVarVirt(ExprState *state, ExprContext *econtext, b static Datum ExecJustHashOuterVarStrict(ExprState *state, ExprContext *econtext, bool *isnull); /* execution helper functions */ +static pg_attribute_always_inline void ExecEvalArrayCompareInternal(FunctionCallInfo fcinfo, + ArrayType *arr, + int16 typlen, + bool typbyval, + char typalign, + bool useOr, + Datum *result, + bool *resultnull); static pg_attribute_always_inline void ExecAggPlainTransByVal(AggState *aggstate, AggStatePerTrans pertrans, AggStatePerGroup pergroup, @@ -2815,7 +2825,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 +2859,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 +2902,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 +3117,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 +3129,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 +3293,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 +3450,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 +3458,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)); @@ -4029,12 +4039,6 @@ ExecEvalScalarArrayOp(ExprState *state, ExprEvalStep *op) int nitems; Datum result; bool resultnull; - int16 typlen; - bool typbyval; - char typalign; - char *s; - bits8 *bitmap; - int bitmask; /* * If the array is NULL then we return NULL --- it's not very meaningful @@ -4083,13 +4087,43 @@ ExecEvalScalarArrayOp(ExprState *state, ExprEvalStep *op) op->d.scalararrayop.element_type = ARR_ELEMTYPE(arr); } - typlen = op->d.scalararrayop.typlen; - typbyval = op->d.scalararrayop.typbyval; - typalign = op->d.scalararrayop.typalign; + ExecEvalArrayCompareInternal(fcinfo, + arr, + op->d.scalararrayop.typlen, + op->d.scalararrayop.typbyval, + op->d.scalararrayop.typalign, + useOr, + &result, + &resultnull); + + *op->resvalue = result; + *op->resnull = resultnull; +} + +/* + * Shared helper for ExecEvalScalarArrayOp() and the NULL-LHS fallback for + * non-strict ExecEvalHashedScalarArrayOp(). + * + * Callers must handle the strict LHS-is-NULL; return NULL fast path prior to + * calling this. + */ +static pg_attribute_always_inline void +ExecEvalArrayCompareInternal(FunctionCallInfo fcinfo, ArrayType *arr, + int16 typlen, bool typbyval, char typalign, + bool useOr, Datum *result, bool *resultnull) +{ + uint8 typalignby = typalign_to_alignby(typalign); + int nitems; + char *s; + uint8 *bitmap; + int bitmask; + bool strictfunc = fcinfo->flinfo->fn_strict; + + nitems = ArrayGetNItems(ARR_NDIM(arr), ARR_DIMS(arr)); /* Initialize result appropriately depending on useOr */ - result = BoolGetDatum(!useOr); - resultnull = false; + *result = BoolGetDatum(!useOr); + *resultnull = false; /* Loop over the array elements */ s = (char *) ARR_DATA_PTR(arr); @@ -4111,7 +4145,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; } @@ -4125,18 +4159,18 @@ ExecEvalScalarArrayOp(ExprState *state, ExprEvalStep *op) else { fcinfo->isnull = false; - thisresult = op->d.scalararrayop.fn_addr(fcinfo); + thisresult = fcinfo->flinfo->fn_addr(fcinfo); } /* Combine results per OR or AND semantics */ if (fcinfo->isnull) - resultnull = true; + *resultnull = true; else if (useOr) { if (DatumGetBool(thisresult)) { - result = BoolGetDatum(true); - resultnull = false; + *result = BoolGetDatum(true); + *resultnull = false; break; /* needn't look at any more elements */ } } @@ -4144,8 +4178,8 @@ ExecEvalScalarArrayOp(ExprState *state, ExprEvalStep *op) { if (!DatumGetBool(thisresult)) { - result = BoolGetDatum(false); - resultnull = false; + *result = BoolGetDatum(false); + *resultnull = false; break; /* needn't look at any more elements */ } } @@ -4161,9 +4195,6 @@ ExecEvalScalarArrayOp(ExprState *state, ExprEvalStep *op) } } } - - *op->resvalue = result; - *op->resnull = resultnull; } /* @@ -4242,7 +4273,7 @@ ExecEvalHashedScalarArrayOp(ExprState *state, ExprEvalStep *op, ExprContext *eco * If the scalar is NULL, and the function is strict, return NULL; no * point in executing the search. */ - if (fcinfo->args[0].isnull && strictfunc) + if (scalar_isnull && strictfunc) { *op->resnull = true; return; @@ -4255,10 +4286,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 +4304,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 +4351,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); } @@ -4340,8 +4373,51 @@ ExecEvalHashedScalarArrayOp(ExprState *state, ExprEvalStep *op, ExprContext *eco * non-strict functions with a null lhs value if no match is found. */ op->d.hashedscalararrayop.has_nulls = has_nulls; + + /* + * When we have a non-strict equality function, check and cache the + * result from looking up a NULL. Non-strict functions are free to + * treat a NULL as equal to any other value, e.g. a 0 or an empty + * string. Here we perform a linear search over the array and cache + * the outcome so that we can use that result any time we receive a + * NULL. + */ + if (!strictfunc) + { + bool null_lhs_result; + + fcinfo->args[0].value = (Datum) 0; + fcinfo->args[0].isnull = true; + + ExecEvalArrayCompareInternal(fcinfo, arr, typlen, typbyval, + typalign, true, &result, + &resultnull); + + null_lhs_result = DatumGetBool(result); + + /* invert non-NULL results for NOT IN */ + if (!resultnull && !inclause) + null_lhs_result = !null_lhs_result; + + op->d.hashedscalararrayop.null_lhs_isnull = resultnull; + op->d.hashedscalararrayop.null_lhs_result = null_lhs_result; + } } + /* + * When looking up an SQL NULL value with non-strict functions, we defer + * to the value we cached when building the hash table. + */ + if (scalar_isnull) + { + Assert(!strictfunc); + + *op->resnull = op->d.hashedscalararrayop.null_lhs_isnull; + *op->resvalue = BoolGetDatum(op->d.hashedscalararrayop.null_lhs_result); + return; + } + + /* Check the hash to see if we have a match. */ hashfound = NULL != saophash_lookup(elements_tab->hashtab, scalar); @@ -4393,7 +4469,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 +4618,7 @@ ExecEvalXmlExpr(ExprState *state, ExprEvalStep *op) *op->resvalue = PointerGetDatum(xmlparse(data, xexpr->xmloption, - preserve_whitespace)); + preserve_whitespace, NULL)); *op->resnull = false; } break; @@ -4736,7 +4812,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 +4821,6 @@ ExecEvalJsonIsPredicate(ExprState *state, ExprEvalStep *op) return; } - exprtype = exprType(pred->expr); - if (exprtype == TEXTOID || exprtype == JSONOID) { text *json = DatumGetTextP(js); @@ -5228,7 +5302,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..3bee818a8b47a 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 * * @@ -234,7 +234,7 @@ ExecLockRows(PlanState *pstate) if (IsolationUsesXactSnapshot()) ereport(ERROR, (errcode(ERRCODE_T_R_SERIALIZATION_FAILURE), - errmsg("could not serialize access due to concurrent update"))); + errmsg("could not serialize access due to concurrent delete"))); /* tuple was deleted so don't return it */ goto lnext; @@ -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..a6cab0c3bf41d 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); @@ -497,7 +542,7 @@ generate_error_response(struct oauth_ctx *ctx, char **output, int *outputlen) ereport(FATAL, errcode(ERRCODE_INTERNAL_ERROR), errmsg("OAuth is not properly configured for this user"), - errdetail_log("The issuer and scope parameters must be set in pg_hba.conf.")); + errdetail_log("The options \"issuer\" and \"scope\" must be set in pg_hba.conf.")); /* * Build a default .well-known URI based on our issuer, unless the HBA has @@ -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; @@ -757,8 +791,8 @@ load_validator_library(const char *libname) */ if (validator_init == NULL) ereport(ERROR, - errmsg("%s module \"%s\" must define the symbol %s", - "OAuth validator", libname, "_PG_oauth_validator_module_init")); + errmsg("OAuth validator module \"%s\" must define the symbol \"%s\"", + libname, "_PG_oauth_validator_module_init")); ValidatorCallbacks = (*validator_init) (); Assert(ValidatorCallbacks); @@ -770,8 +804,8 @@ load_validator_library(const char *libname) */ if (ValidatorCallbacks->magic != PG_OAUTH_VALIDATOR_MAGIC) ereport(ERROR, - errmsg("%s module \"%s\": magic number mismatch", - "OAuth validator", libname), + errmsg("OAuth validator module \"%s\": magic number mismatch", + libname), errdetail("Server has magic number 0x%08X, module has 0x%08X.", PG_OAUTH_VALIDATOR_MAGIC, ValidatorCallbacks->magic)); @@ -781,18 +815,18 @@ load_validator_library(const char *libname) */ if (ValidatorCallbacks->validate_cb == NULL) ereport(ERROR, - errmsg("%s module \"%s\" must provide a %s callback", - "OAuth validator", libname, "validate_cb")); + errmsg("OAuth validator module \"%s\" must provide a \"%s\" callback", + 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; } /* @@ -830,12 +867,12 @@ check_oauth_validator(HbaLine *hbaline, int elevel, char **err_msg) { ereport(elevel, errcode(ERRCODE_CONFIG_FILE_ERROR), - errmsg("oauth_validator_libraries must be set for authentication method %s", - "oauth"), + errmsg("parameter \%s\" must be set for authentication method \"%s\"", + "oauth_validator_libraries", "oauth"), errcontext("line %d of configuration file \"%s\"", line_num, file_name)); - *err_msg = psprintf("oauth_validator_libraries must be set for authentication method %s", - "oauth"); + *err_msg = psprintf("parameter \"%s\" must be set for authentication method \"%s\"", + "oauth_validator_libraries", "oauth"); return false; } @@ -864,10 +901,12 @@ check_oauth_validator(HbaLine *hbaline, int elevel, char **err_msg) ereport(elevel, errcode(ERRCODE_CONFIG_FILE_ERROR), - errmsg("authentication method \"oauth\" requires argument \"validator\" to be set when oauth_validator_libraries contains multiple options"), + errmsg("authentication method \"oauth\" requires option \"validator\" to be set when \"%s\" contains multiple options", + "oauth_validator_libraries"), errcontext("line %d of configuration file \"%s\"", line_num, file_name)); - *err_msg = "authentication method \"oauth\" requires argument \"validator\" to be set when oauth_validator_libraries contains multiple options"; + *err_msg = psprintf("authentication method \"oauth\" requires option \"validator\" to be set when \"%s\" contains multiple options", + "oauth_validator_libraries"); goto done; } @@ -879,11 +918,11 @@ check_oauth_validator(HbaLine *hbaline, int elevel, char **err_msg) ereport(elevel, errcode(ERRCODE_INVALID_PARAMETER_VALUE), - errmsg("validator \"%s\" is not permitted by %s", + errmsg("validator \"%s\" is not permitted by \"%s\"", hbaline->oauth_validator, "oauth_validator_libraries"), errcontext("line %d of configuration file \"%s\"", line_num, file_name)); - *err_msg = psprintf("validator \"%s\" is not permitted by %s", + *err_msg = psprintf("validator \"%s\" is not permitted by \"%s\"", hbaline->oauth_validator, "oauth_validator_libraries"); done: @@ -892,3 +931,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..4bac15fc5c1be 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; @@ -581,7 +579,7 @@ scram_verify_plain_password(const char *username, const char *password, * Compare the secret's Server Key with the one computed from the * user-supplied password. */ - return memcmp(computed_key, server_key, key_length) == 0; + return timingsafe_bcmp(computed_key, server_key, key_length) == 0; } @@ -1132,9 +1130,9 @@ verify_final_nonce(scram_state *state) if (final_nonce_len != client_nonce_len + server_nonce_len) return false; - if (memcmp(state->client_final_nonce, state->client_nonce, client_nonce_len) != 0) + if (timingsafe_bcmp(state->client_final_nonce, state->client_nonce, client_nonce_len) != 0) return false; - if (memcmp(state->client_final_nonce + client_nonce_len, state->server_nonce, server_nonce_len) != 0) + if (timingsafe_bcmp(state->client_final_nonce + client_nonce_len, state->server_nonce, server_nonce_len) != 0) return false; return true; @@ -1188,7 +1186,7 @@ verify_client_proof(scram_state *state) client_StoredKey, &errstr) < 0) elog(ERROR, "could not hash stored key: %s", errstr); - if (memcmp(client_StoredKey, state->StoredKey, state->key_length) != 0) + if (timingsafe_bcmp(client_StoredKey, state->StoredKey, state->key_length) != 0) return false; return true; @@ -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..6ec887b8a4771 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. + */ +HostsFileLoadResult +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..b978497b5d4b2 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; + HostsFileLoadResult 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..739a2e6fa8bd8 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; @@ -230,8 +293,26 @@ md5_crypt_verify(const char *role, const char *shadow_pass, return STATUS_ERROR; } - if (strcmp(client_pass, crypt_pwd) == 0) + if (strlen(client_pass) == strlen(crypt_pwd) && + timingsafe_bcmp(client_pass, crypt_pwd, strlen(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 +365,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)) @@ -292,7 +373,8 @@ plain_crypt_verify(const char *role, const char *shadow_pass, *logdetail = errstr; return STATUS_ERROR; } - if (strcmp(crypt_client_pass, shadow_pass) == 0) + if (strlen(crypt_client_pass) == strlen(shadow_pass) && + timingsafe_bcmp(crypt_client_pass, shadow_pass, strlen(shadow_pass)) == 0) return STATUS_OK; else { 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..2a2e00b372e4d 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; } @@ -997,8 +1002,16 @@ exprCollation(const Node *expr) { const JsonConstructorExpr *ctor = (const JsonConstructorExpr *) expr; + /* + * Collation comes from coercion if present, otherwise from + * func. The func fallback is needed in cases where func + * already produces the final output type and no coercion is + * needed (cf. the JSCTOR_JSON_ARRAY_QUERY case). + */ if (ctor->coercion) coll = exprCollation((Node *) ctor->coercion); + else if (ctor->func) + coll = exprCollation((Node *) ctor->func); else coll = InvalidOid; } @@ -1009,14 +1022,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 +1071,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 */ @@ -1256,8 +1272,11 @@ exprSetCollation(Node *expr, Oid collation) { JsonConstructorExpr *ctor = (JsonConstructorExpr *) expr; + /* See comment in exprCollation() */ if (ctor->coercion) exprSetCollation((Node *) ctor->coercion, collation); + else if (ctor->func) + exprSetCollation((Node *) ctor->func, collation); else Assert(!OidIsValid(collation)); /* result is always a * json[b] type */ @@ -1274,12 +1293,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 +1612,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 +1745,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 +2146,8 @@ expression_tree_walker_impl(Node *node, case T_RangeTblRef: case T_SortGroupClause: case T_CTESearchClause: + case T_GraphLabelRef: + case T_GraphPropertyRef: case T_MergeSupportFunc: /* primitive node types with no expression subnodes */ break; @@ -2571,6 +2591,24 @@ expression_tree_walker_impl(Node *node, return true; } break; + case T_ForPortionOfExpr: + { + ForPortionOfExpr *forPortionOf = (ForPortionOfExpr *) node; + + if (WALK(forPortionOf->rangeVar)) + return true; + if (WALK(forPortionOf->targetFrom)) + return true; + if (WALK(forPortionOf->targetTo)) + return true; + if (WALK(forPortionOf->targetRange)) + return true; + if (WALK(forPortionOf->overlapsExpr)) + return true; + if (WALK(forPortionOf->rangeTargetList)) + return true; + } + break; case T_PartitionPruneStepOp: { PartitionPruneStepOp *opstep = (PartitionPruneStepOp *) node; @@ -2668,6 +2706,28 @@ expression_tree_walker_impl(Node *node, return true; } break; + case T_GraphElementPattern: + { + GraphElementPattern *gep = (GraphElementPattern *) node; + + if (WALK(gep->labelexpr)) + return true; + 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 +2779,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 +2923,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 +3025,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) \ @@ -3007,6 +3075,8 @@ expression_tree_mutator_impl(Node *node, case T_RangeTblRef: case T_SortGroupClause: case T_CTESearchClause: + case T_GraphLabelRef: + case T_GraphPropertyRef: case T_MergeSupportFunc: return copyObject(node); case T_WithCheckOption: @@ -3613,6 +3683,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; @@ -3744,6 +3830,30 @@ expression_tree_mutator_impl(Node *node, return (Node *) newnode; } break; + case T_GraphElementPattern: + { + GraphElementPattern *gep = (GraphElementPattern *) node; + GraphElementPattern *newnode; + + FLATCOPY(newnode, gep, GraphElementPattern); + MUTATE(newnode->labelexpr, gep->labelexpr, Node *); + MUTATE(newnode->subexpr, gep->subexpr, List *); + MUTATE(newnode->whereClause, gep->whereClause, Node *); + newnode->quantifier = list_copy(gep->quantifier); + return (Node *) newnode; + } + break; + case T_GraphPattern: + { + GraphPattern *gp = (GraphPattern *) node; + GraphPattern *newnode; + + FLATCOPY(newnode, gp, GraphPattern); + MUTATE(newnode->path_pattern_list, gp->path_pattern_list, List *); + MUTATE(newnode->whereClause, gp->whereClause, Node *); + return (Node *) newnode; + } + break; default: elog(ERROR, "unrecognized node type: %d", (int) nodeTag(node)); @@ -3794,6 +3904,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 +4024,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 +4662,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 +4832,28 @@ raw_expression_tree_walker_impl(Node *node, return true; } break; + case T_GraphElementPattern: + { + GraphElementPattern *gep = (GraphElementPattern *) node; + + if (WALK(gep->labelexpr)) + return true; + 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 +4979,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..3f5d4fa3182fe 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) { @@ -4265,16 +4226,19 @@ relation_has_unique_index_ext(PlannerInfo *root, RelOptInfo *rel, * The condition's 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 check this first - * since it's probably cheaper than match_index_to_operand(). + * since it's probably the cheapest test. */ if (!list_member_oid(rinfo->mergeopfamilies, 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. + * The index's collation must agree with the clause's input + * collation on equality, else the index's uniqueness does not + * imply uniqueness under the clause's equality semantics. */ + if (!collations_agree_on_equality(ind->indexcollations[c], + exprInputCollation((Node *) rinfo->clause))) + continue; /* OK, see if the condition operand matches the index key */ if (rinfo->outer_is_left) @@ -4284,8 +4248,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 +4265,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 +4360,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..b07cb731401db 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,16 +57,18 @@ 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, List *clause_list, List **extra_clauses); -static Oid distinct_col_search(int colno, List *colnos, List *opids); +static DistinctColInfo *distinct_col_search(int colno, List *distinct_cols); static bool is_innerrel_unique_for(PlannerInfo *root, Relids joinrelids, Relids outerrelids, @@ -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 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. * - * This function updates only parts needed for both left-join removal and - * self-join removal. + * 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. * - * So, start by removing all other bits from attr_needed sets and - * lateral_vars lists. (We already did this above for ph_needed.) + * 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. + * + * 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,26 +1022,27 @@ 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) { Index relid = rel->relid; Query *subquery = root->simple_rte_array[relid]->subquery; - List *colnos = NIL; - List *opids = NIL; + List *distinct_cols = NIL; ListCell *l; /* - * Build the argument lists for query_is_distinct_for: a list of - * output column numbers that the query needs to be distinct over, and - * a list of equality operators that the output columns need to be - * distinct according to. + * Build the argument list for query_is_distinct_for: a list of + * DistinctColInfo entries, each holding an output column number that + * the query needs to be distinct over, the equality operator that the + * column needs to be distinct according to, and that operator's input + * collation. The collation matters because the subquery's own + * DISTINCT / GROUP BY / set-op proves uniqueness under its own + * collation, which need not agree with the operator's. * * (XXX we are not considering restriction clauses attached to the * subquery; is that worth doing?) @@ -1017,18 +1050,18 @@ rel_is_distinct_for(PlannerInfo *root, RelOptInfo *rel, List *clause_list, foreach(l, clause_list) { RestrictInfo *rinfo = lfirst_node(RestrictInfo, l); - Oid op; + OpExpr *opexpr; Var *var; + DistinctColInfo *dcinfo; /* - * Get the equality operator we need uniqueness according to. - * (This might be a cross-type operator and thus not exactly the - * same operator the subquery would consider; that's all right - * since query_is_distinct_for can resolve such cases.) The - * caller's mergejoinability test should have selected only - * OpExprs. + * The caller's mergejoinability test should have selected only + * OpExprs. The operator might be a cross-type operator and thus + * not exactly the same operator the subquery would consider; + * that's all right since query_is_distinct_for can resolve such + * cases. */ - op = castNode(OpExpr, rinfo->clause)->opno; + opexpr = castNode(OpExpr, rinfo->clause); /* caller identified the inner side for us */ if (rinfo->outer_is_left) @@ -1052,11 +1085,14 @@ rel_is_distinct_for(PlannerInfo *root, RelOptInfo *rel, List *clause_list, var->varno != relid || var->varlevelsup != 0) continue; - colnos = lappend_int(colnos, var->varattno); - opids = lappend_oid(opids, op); + dcinfo = palloc(sizeof(DistinctColInfo)); + dcinfo->colno = var->varattno; + dcinfo->opid = opexpr->opno; + dcinfo->collid = opexpr->inputcollid; + distinct_cols = lappend(distinct_cols, dcinfo); } - if (query_is_distinct_for(subquery, colnos, opids)) + if (query_is_distinct_for(subquery, distinct_cols)) return true; } return false; @@ -1100,25 +1136,32 @@ query_supports_distinctness(Query *query) * query is a not-yet-planned subquery (in current usage, it's always from * a subquery RTE, which the planner avoids scribbling on). * - * colnos is an integer list of output column numbers (resno's). We are - * interested in whether rows consisting of just these columns are certain - * to be distinct. "Distinctness" is defined according to whether the - * corresponding upper-level equality operators listed in opids would think - * the values are distinct. (Note: the opids entries could be cross-type - * operators, and thus not exactly the equality operators that the subquery - * would use itself. We use equality_ops_are_compatible() to check - * compatibility. That looks at opfamily membership for index AMs that have - * declared that they support consistent equality semantics within an + * distinct_cols is a list of DistinctColInfo, one per requested output column. + * Each entry names the subquery output column number we want distinct, the + * upper-level equality operator we'll compare values with, and that operator's + * input collation. We are interested in whether rows consisting of just these + * columns are certain to be distinct. + * + * "Distinctness" is defined according to whether the corresponding upper-level + * equality operators would think the values are distinct. (Note: each opid + * could be a cross-type operator, and thus not exactly the equality operator + * that the subquery would use itself. We use equality_ops_are_compatible() to + * check compatibility. That looks at opfamily membership for index AMs that + * have declared that they support consistent equality semantics within an * opfamily, and so should give trustworthy answers for all operators that we * might need to deal with here.) + * + * The collid must also agree on equality with the collation the subquery's own + * DISTINCT/GROUP BY/set-op uses to deduplicate the column, else the subquery's + * distinctness does not carry over to the caller's equality semantics. Two + * collations agree on equality if they match or if both are deterministic (in + * which case both reduce equality to byte-equality; see CREATE COLLATION). */ bool -query_is_distinct_for(Query *query, List *colnos, List *opids) +query_is_distinct_for(Query *query, List *distinct_cols) { ListCell *l; - Oid opid; - - Assert(list_length(colnos) == list_length(opids)); + DistinctColInfo *dcinfo; /* * DISTINCT (including DISTINCT ON) guarantees uniqueness if all the @@ -1134,9 +1177,11 @@ query_is_distinct_for(Query *query, List *colnos, List *opids) TargetEntry *tle = get_sortgroupclause_tle(sgc, query->targetList); - opid = distinct_col_search(tle->resno, colnos, opids); - if (!OidIsValid(opid) || - !equality_ops_are_compatible(opid, sgc->eqop)) + dcinfo = distinct_col_search(tle->resno, distinct_cols); + if (dcinfo == NULL || + !equality_ops_are_compatible(dcinfo->opid, sgc->eqop) || + !collations_agree_on_equality(dcinfo->collid, + exprCollation((Node *) tle->expr))) break; /* exit early if no match */ } if (l == NULL) /* had matches for all? */ @@ -1165,9 +1210,11 @@ query_is_distinct_for(Query *query, List *colnos, List *opids) TargetEntry *tle = get_sortgroupclause_tle(sgc, query->targetList); - opid = distinct_col_search(tle->resno, colnos, opids); - if (!OidIsValid(opid) || - !equality_ops_are_compatible(opid, sgc->eqop)) + dcinfo = distinct_col_search(tle->resno, distinct_cols); + if (dcinfo == NULL || + !equality_ops_are_compatible(dcinfo->opid, sgc->eqop) || + !collations_agree_on_equality(dcinfo->collid, + exprCollation((Node *) tle->expr))) break; /* exit early if no match */ } if (l == NULL) /* had matches for all? */ @@ -1175,6 +1222,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 +1233,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 { @@ -1233,9 +1284,11 @@ query_is_distinct_for(Query *query, List *colnos, List *opids) sgc = (SortGroupClause *) lfirst(lg); lg = lnext(topop->groupClauses, lg); - opid = distinct_col_search(tle->resno, colnos, opids); - if (!OidIsValid(opid) || - !equality_ops_are_compatible(opid, sgc->eqop)) + dcinfo = distinct_col_search(tle->resno, distinct_cols); + if (dcinfo == NULL || + !equality_ops_are_compatible(dcinfo->opid, sgc->eqop) || + !collations_agree_on_equality(dcinfo->collid, + exprCollation((Node *) tle->expr))) break; /* exit early if no match */ } if (l == NULL) /* had matches for all? */ @@ -1257,22 +1310,21 @@ query_is_distinct_for(Query *query, List *colnos, List *opids) /* * distinct_col_search - subroutine for query_is_distinct_for * - * If colno is in colnos, return the corresponding element of opids, - * else return InvalidOid. (Ordinarily colnos would not contain duplicates, - * but if it does, we arbitrarily select the first match.) + * If colno matches the colno field of an entry in distinct_cols, return a + * pointer to that entry; else return NULL. (Ordinarily distinct_cols would + * not contain duplicate colnos, but if it does, we arbitrarily select the + * first match.) */ -static Oid -distinct_col_search(int colno, List *colnos, List *opids) +static DistinctColInfo * +distinct_col_search(int colno, List *distinct_cols) { - ListCell *lc1, - *lc2; - - forboth(lc1, colnos, lc2, opids) + foreach_ptr(DistinctColInfo, dcinfo, distinct_cols) { - if (colno == lfirst_int(lc1)) - return lfirst_oid(lc2); + if (dcinfo->colno == colno) + return dcinfo; } - return InvalidOid; + + return NULL; } @@ -1425,17 +1477,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 +1735,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 +1821,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,17 +2005,22 @@ 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); + /* + * No need to touch all_result_relids or leaf_result_relids: at this point + * those sets contain only parse->resultRelation; inheritance children + * have not been added yet; that happens later in add_other_rels_to_query. + * And remove_self_joins_recurse rejects parse->resultRelation as an SJE + * candidate to preserve the EPQ mechanism. So toRemove->relid cannot be + * a member. + */ + Assert(!bms_is_member(toRemove->relid, root->all_result_relids)); + Assert(!bms_is_member(toRemove->relid, root->leaf_result_relids)); /* * There may be references to the rel in root->fkey_list, but if so, @@ -1979,10 +2033,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 +2198,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 +2250,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 +2285,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 +2296,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 +2307,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 +2333,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 +2415,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..b38422c47a416 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" @@ -80,7 +82,25 @@ typedef struct JoinTreeItem * lateral references */ } JoinTreeItem; - +/* + * Compatibility info for one GROUP BY item, precomputed for use by + * remove_useless_groupby_columns() when matching unique-index columns against + * GROUP BY items. + */ +typedef struct GroupByColInfo +{ + AttrNumber attno; /* var->varattno */ + List *eq_opfamilies; /* mergejoin opfamilies of sgc->eqop */ + Oid coll; /* var->varcollid */ +} GroupByColInfo; + + +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, @@ -413,6 +433,7 @@ remove_useless_groupby_columns(PlannerInfo *root) { Query *parse = root->parse; Bitmapset **groupbyattnos; + List **groupbycols; Bitmapset **surplusvars; bool tryremove = false; ListCell *lc; @@ -429,15 +450,20 @@ remove_useless_groupby_columns(PlannerInfo *root) /* * Scan the GROUP BY clause to find GROUP BY items that are simple Vars. * Fill groupbyattnos[k] with a bitmapset of the column attnos of RTE k - * that are GROUP BY items. + * that are GROUP BY items, and groupbycols[k] with a parallel list of + * GroupByColInfo records. We need the latter so that, when checking a + * unique index against this rel's GROUP BY items, we can verify that the + * index's notion of equality agrees with at least one GROUP BY item per + * index column. */ - groupbyattnos = (Bitmapset **) palloc0(sizeof(Bitmapset *) * - (list_length(parse->rtable) + 1)); + groupbyattnos = palloc0_array(Bitmapset *, list_length(parse->rtable) + 1); + groupbycols = palloc0_array(List *, list_length(parse->rtable) + 1); foreach(lc, root->processed_groupClause) { SortGroupClause *sgc = lfirst_node(SortGroupClause, lc); TargetEntry *tle = get_sortgroupclause_tle(sgc, parse->targetList); Var *var = (Var *) tle->expr; + GroupByColInfo *info; /* * Ignore non-Vars and Vars from other query levels. @@ -463,6 +489,12 @@ remove_useless_groupby_columns(PlannerInfo *root) tryremove |= !bms_is_empty(groupbyattnos[relid]); groupbyattnos[relid] = bms_add_member(groupbyattnos[relid], var->varattno - FirstLowInvalidHeapAttributeNumber); + + info = palloc(sizeof(GroupByColInfo)); + info->attno = var->varattno; + info->eq_opfamilies = get_mergejoin_opfamilies(sgc->eqop); + info->coll = var->varcollid; + groupbycols[relid] = lappend(groupbycols[relid], info); } /* @@ -517,7 +549,7 @@ remove_useless_groupby_columns(PlannerInfo *root) foreach_node(IndexOptInfo, index, rel->indexlist) { Bitmapset *ind_attnos; - bool nulls_check_ok; + bool index_check_ok; /* * Skip any non-unique and deferrable indexes. Predicate indexes @@ -532,9 +564,14 @@ remove_useless_groupby_columns(PlannerInfo *root) continue; ind_attnos = NULL; - nulls_check_ok = true; + index_check_ok = true; for (int i = 0; i < index->nkeycolumns; i++) { + AttrNumber indkey_attno = index->indexkeys[i]; + Oid indkey_opfamily = index->opfamily[i]; + Oid indkey_coll = index->indexcollations[i]; + ListCell *lc2; + /* * We must insist that the index columns are all defined NOT * NULL otherwise duplicate NULLs could exist. However, we @@ -544,20 +581,41 @@ remove_useless_groupby_columns(PlannerInfo *root) * despite the NULL. */ if (!index->nullsnotdistinct && - !bms_is_member(index->indexkeys[i], - rel->notnullattnums)) + !bms_is_member(indkey_attno, rel->notnullattnums)) { - nulls_check_ok = false; + index_check_ok = false; + break; + } + + /* + * The index proves uniqueness only under its own opfamily and + * collation. Require some GROUP BY item on this column to + * use a compatible eqop and collation, the same check + * relation_has_unique_index_for() applies to join clauses. + */ + foreach(lc2, groupbycols[relid]) + { + GroupByColInfo *info = (GroupByColInfo *) lfirst(lc2); + + if (info->attno != indkey_attno) + continue; + if (list_member_oid(info->eq_opfamilies, indkey_opfamily) && + collations_agree_on_equality(indkey_coll, info->coll)) + break; + } + if (lc2 == NULL) + { + index_check_ok = false; break; } ind_attnos = bms_add_member(ind_attnos, - index->indexkeys[i] - + indkey_attno - FirstLowInvalidHeapAttributeNumber); } - if (!nulls_check_ok) + if (!index_check_ok) continue; /* @@ -590,8 +648,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 +685,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 +3482,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 +3518,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 +3583,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..f4689e7c9f8bf 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; @@ -127,9 +134,27 @@ typedef struct * subquery belonging to a set operation */ } standard_qp_extra; +/* + * Context for the find_having_collation_conflicts walker. + * + * ancestor_collids is a stack of inputcollids contributed by collation-aware + * ancestors of the current node. Entries are pushed before recursing into a + * node's children and popped afterwards, so the stack reflects exactly the + * inputcollids on the current root-to-node path. + */ +typedef struct +{ + Index group_rtindex; + List *ancestor_collids; +} having_collation_ctx; + /* Local functions */ static Node *preprocess_expression(PlannerInfo *root, Node *expr, int kind); static void preprocess_qual_conditions(PlannerInfo *root, Node *jtnode); +static Bitmapset *find_having_collation_conflicts(Query *parse, + Index group_rtindex); +static bool having_collation_conflict_walker(Node *node, + having_collation_ctx *ctx); static void grouping_planner(PlannerInfo *root, double tuple_fraction, SetOperationStmt *setops); static grouping_sets_data *preprocess_grouping_sets(PlannerInfo *root); @@ -231,7 +256,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 +291,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 +331,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 +349,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 +359,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 +392,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 +481,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 +661,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 +674,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 +725,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 +743,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,13 +772,16 @@ 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) { PlannerInfo *root; List *newWithCheckOptions; List *newHaving; + Bitmapset *havingCollationConflicts; + int havingIdx; bool hasOuterJoins; bool hasResultRTEs; RelOptInfo *final_rel; @@ -664,6 +792,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 +827,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 +853,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 +888,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 +924,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 +968,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. @@ -1043,6 +1195,27 @@ subquery_planner(PlannerGlobal *glob, Query *parse, PlannerInfo *parent_root, } } + /* + * Before we flatten GROUP Vars, check which HAVING clauses have collation + * conflicts. When GROUP BY uses a nondeterministic collation, values + * that are "equal" for grouping may be distinguishable under a different + * collation. If such a HAVING clause were moved to WHERE, it would + * filter individual rows before grouping, potentially eliminating some + * members of a group and thereby changing aggregate results. + * + * We do this check before flatten_group_exprs because we can easily + * identify grouping expressions by checking whether a Var references + * RTE_GROUP, and such Vars directly carry the GROUP BY collation as their + * varcollid. After flattening, these Vars are replaced by the underlying + * expressions, and we would have to match expressions in the HAVING + * clause back to grouping expressions, which is much more complex. + */ + if (parse->hasGroupRTE) + havingCollationConflicts = + find_having_collation_conflicts(parse, root->group_rtindex); + else + havingCollationConflicts = NULL; + /* * Replace any Vars in the subquery's targetlist and havingQual that * reference GROUP outputs with the underlying grouping expressions. @@ -1064,15 +1237,36 @@ 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.) + * + * We also cannot do this if the HAVING clause uses a different collation + * than the GROUP BY for any grouping expression whose GROUP BY collation + * is nondeterministic. This is detected before flatten_group_exprs (see + * find_having_collation_conflicts above) and recorded in the + * havingCollationConflicts bitmapset. The bitmapset indexes remain valid + * here because flatten_group_exprs uses expression_tree_mutator, which + * preserves the list length and ordering of havingQual. * * 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 +1276,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 @@ -1106,6 +1300,7 @@ subquery_planner(PlannerGlobal *glob, Query *parse, PlannerInfo *parent_root, * as Node *. */ newHaving = NIL; + havingIdx = 0; foreach(l, (List *) parse->havingQual) { Node *havingclause = (Node *) lfirst(l); @@ -1113,14 +1308,18 @@ subquery_planner(PlannerGlobal *glob, Query *parse, PlannerInfo *parent_root, if (contain_agg_clause(havingclause) || contain_volatile_functions(havingclause) || contain_subplans(havingclause) || + bms_is_member(havingIdx, havingCollationConflicts) || (parse->groupClause && parse->groupingSets && bms_is_member(root->group_rtindex, pull_varnos(root, havingclause)))) { /* 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 +1332,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 */ @@ -1145,6 +1345,8 @@ subquery_planner(PlannerGlobal *glob, Query *parse, PlannerInfo *parent_root, /* ... and also keep it in HAVING */ newHaving = lappend(newHaving, havingclause); } + + havingIdx++; } parse->havingQual = (Node *) newHaving; @@ -1336,6 +1538,195 @@ preprocess_qual_conditions(PlannerInfo *root, Node *jtnode) (int) nodeTag(jtnode)); } +/* + * find_having_collation_conflicts + * Identify HAVING clauses that must not be moved to WHERE due to collation + * mismatches with GROUP BY. + * + * This must be called before flatten_group_exprs, while the HAVING clause + * still contains GROUP Vars (Vars referencing RTE_GROUP). These GROUP Vars + * carry the GROUP BY collation as their varcollid. A GROUP Var with a + * nondeterministic varcollid conflicts whenever some collation-aware ancestor + * on its path applies a different inputcollid: that operator would distinguish + * values which the GROUP BY considers equal, so the clause is unsafe to push + * to WHERE. + * + * Returns a Bitmapset of zero-based indexes into the havingQual list for + * clauses that have collation conflicts and must stay in HAVING. + */ +static Bitmapset * +find_having_collation_conflicts(Query *parse, Index group_rtindex) +{ + Bitmapset *result = NULL; + having_collation_ctx ctx; + int idx; + + if (parse->havingQual == NULL) + return NULL; + + ctx.group_rtindex = group_rtindex; + ctx.ancestor_collids = NIL; + + idx = 0; + foreach_ptr(Node, clause, (List *) parse->havingQual) + { + if (having_collation_conflict_walker(clause, &ctx)) + result = bms_add_member(result, idx); + idx++; + Assert(ctx.ancestor_collids == NIL); + } + + return result; +} + +/* + * Walker function for find_having_collation_conflicts. + * + * Walk the clause top-down, maintaining a stack of inputcollids contributed + * by collation-aware ancestors. At each GROUP Var with a nondeterministic + * varcollid, the clause has a conflict if any ancestor's inputcollid differs + * from the GROUP Var's varcollid. Most collation-aware nodes expose their + * inputcollid through exprInputCollation(). Two structural exceptions need + * special handling: + * + * - RowCompareExpr carries one inputcollid per column in inputcollids[], so we + * descend into its (largs[i], rargs[i]) pairs explicitly with the matching + * collation pushed onto the stack. + * + * - A simple CASE (CaseExpr with a non-NULL arg) holds the arg outside the + * WHEN's OpExpr, even though the WHEN's OpExpr is the place where the + * comparison's inputcollid lives. Parse analysis builds each WHEN as + * "OpExpr(CaseTestExpr op val)" -- the CaseTestExpr is a placeholder for + * the arg. Before walking cexpr->arg we therefore push every WHEN's + * inputcollid onto the ancestor stack, so a GROUP Var at the arg is + * checked against the same collations the WHEN comparisons would apply. + * The WHEN bodies and defresult are then walked under the unchanged stack + * so their own collation contexts are picked up by the default path. + */ +static bool +having_collation_conflict_walker(Node *node, having_collation_ctx *ctx) +{ + Oid this_collid; + bool result; + + if (node == NULL) + return false; + + if (IsA(node, Var)) + { + Var *var = (Var *) node; + + /* We should not see any upper-level Vars here */ + Assert(var->varlevelsup == 0); + + if (var->varno == ctx->group_rtindex && + OidIsValid(var->varcollid) && + !get_collation_isdeterministic(var->varcollid)) + { + foreach_oid(collid, ctx->ancestor_collids) + { + if (collid != var->varcollid) + return true; + } + } + return false; + } + + if (IsA(node, RowCompareExpr)) + { + RowCompareExpr *rcexpr = (RowCompareExpr *) node; + ListCell *lc_l; + ListCell *lc_r; + ListCell *lc_c; + + /* + * Each column of a row comparison is compared under its own + * inputcollids[i]. Walk each (largs[i], rargs[i]) pair with that + * collation pushed, so a Var in column i is checked against the + * collation that actually applies to it. + */ + forthree(lc_l, rcexpr->largs, + lc_r, rcexpr->rargs, + lc_c, rcexpr->inputcollids) + { + Oid collid = lfirst_oid(lc_c); + bool found; + + if (OidIsValid(collid)) + ctx->ancestor_collids = lappend_oid(ctx->ancestor_collids, + collid); + + found = having_collation_conflict_walker((Node *) lfirst(lc_l), + ctx) || + having_collation_conflict_walker((Node *) lfirst(lc_r), + ctx); + + if (OidIsValid(collid)) + ctx->ancestor_collids = + list_delete_last(ctx->ancestor_collids); + + if (found) + return true; + } + return false; + } + + if (IsA(node, CaseExpr) && ((CaseExpr *) node)->arg != NULL) + { + CaseExpr *cexpr = (CaseExpr *) node; + int saved_len = list_length(ctx->ancestor_collids); + bool found; + + /* + * Push every WHEN's inputcollid before walking cexpr->arg, since each + * WHEN implicitly compares the arg under that inputcollid. + */ + foreach_node(CaseWhen, cw, cexpr->args) + { + Oid collid = exprInputCollation((Node *) cw->expr); + + if (OidIsValid(collid)) + ctx->ancestor_collids = lappend_oid(ctx->ancestor_collids, + collid); + } + + found = having_collation_conflict_walker((Node *) cexpr->arg, ctx); + + ctx->ancestor_collids = list_truncate(ctx->ancestor_collids, + saved_len); + + if (found) + return true; + + /* + * Walk the WHEN bodies and defresult under the unchanged ancestor + * stack; any inputcollids inside them are picked up by the default + * path. + */ + foreach_node(CaseWhen, cw, cexpr->args) + { + if (having_collation_conflict_walker((Node *) cw->expr, ctx) || + having_collation_conflict_walker((Node *) cw->result, ctx)) + return true; + } + return having_collation_conflict_walker((Node *) cexpr->defresult, + ctx); + } + + this_collid = exprInputCollation(node); + if (OidIsValid(this_collid)) + ctx->ancestor_collids = lappend_oid(ctx->ancestor_collids, + this_collid); + + result = expression_tree_walker(node, having_collation_conflict_walker, + ctx); + + if (OidIsValid(this_collid)) + ctx->ancestor_collids = list_delete_last(ctx->ancestor_collids); + + return result; +} + /* * preprocess_phv_expression * Do preprocessing on a PlaceHolderVar expression that's been pulled up. @@ -1694,9 +2085,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 +2455,6 @@ grouping_planner(PlannerInfo *root, double tuple_fraction, parse->canSetTag, parse->resultRelation, rootRelation, - root->partColsUpdated, resultRelations, updateColnosLists, withCheckOptionLists, @@ -2072,6 +2463,7 @@ grouping_planner(PlannerInfo *root, double tuple_fraction, parse->onConflict, mergeActionLists, mergeJoinConditions, + parse->forPortionOf, assign_special_exec_param(root)); } @@ -2119,10 +2511,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 +2526,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 +2534,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 +3726,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 +4262,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 +4325,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 +4333,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 +4378,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 +4460,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 +5280,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 +5304,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 +5483,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 +5498,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 +5660,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 +6295,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 +7269,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 +7417,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 +7565,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 +7583,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 +7591,7 @@ add_paths_to_grouping_rel(PlannerInfo *root, RelOptInfo *input_rel, path, info->clauses, havingQual, - dNumGroups)); + dNumFinalGroups)); } } @@ -7217,19 +7633,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 +7683,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 +7694,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 +7724,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 +7743,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 +7956,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 +8225,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 +8249,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 +8275,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 +8570,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 +8719,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..cd86311bb0b6c 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 @@ -2913,7 +3244,6 @@ eval_const_expressions_mutator(Node *node, } break; } - case T_JsonValueExpr: { JsonValueExpr *jve = (JsonValueExpr *) node; @@ -2937,7 +3267,21 @@ eval_const_expressions_mutator(Node *node, (Expr *) formatted_expr, copyObject(jve->format)); } + case T_JsonConstructorExpr: + { + JsonConstructorExpr *jce = (JsonConstructorExpr *) node; + /* + * JSCTOR_JSON_ARRAY_QUERY carries a pre-built executable form + * in its func field (a COALESCE-wrapped JSON_ARRAYAGG + * subquery, constructed during parse analysis). Replace the + * node with that expression and continue simplifying. + */ + if (jce->type == JSCTOR_JSON_ARRAY_QUERY) + return eval_const_expressions_mutator((Node *) jce->func, + context); + } + break; case T_SubPlan: case T_AlternativeSubPlan: @@ -3305,10 +3649,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 +3665,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 +3686,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 +3853,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 +3911,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 +3959,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 +3998,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 +4054,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,67 +4586,385 @@ 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 - * - * 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. + * 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. * - * 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? */ + return (Node *) aggref; +} + +/* + * 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); @@ -5049,50 +5797,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 +5851,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 +5881,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 +5892,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 +5904,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 +5937,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 +6105,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 +6137,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 +6159,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 +6167,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 +6177,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 +6203,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 +6228,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 +6311,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..4270c2382c47e 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,132 @@ 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); + } + + /* + * Assign collations to column expressions now since + * assign_query_collations() does not process rangetable entries. + */ + assign_list_collations(pstate, columns); + + 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 +1251,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 +1715,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 +2740,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 +2767,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 +2775,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 +2787,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 +3022,7 @@ transformWindowDefinitions(ParseState *pstate, true /* force SQL99 rules */ ); partitionClause = transformGroupClause(pstate, windef->partitionClause, + false /* not GROUP BY ALL */ , NULL, targetlist, orderClause, @@ -3214,24 +3419,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 +3481,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 +3515,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..f1003e57fb299 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 || @@ -3752,24 +3792,54 @@ transformJsonObjectConstructor(ParseState *pstate, JsonObjectConstructor *ctor) } /* - * Transform JSON_ARRAY(query [FORMAT] [RETURNING] [ON NULL]) into - * (SELECT JSON_ARRAYAGG(a [FORMAT] [RETURNING] [ON NULL]) FROM (query) q(a)) + * Transform JSON_ARRAY(subquery) constructor. + * + * JSON_ARRAY(subquery) is transformed into a JsonConstructorExpr node of type + * JSCTOR_JSON_ARRAY_QUERY. The node carries: + * + * - func: the executable form, which is a COALESCE expression wrapping a + * JSON_ARRAYAGG subquery: + * + * COALESCE((SELECT JSON_ARRAYAGG(a) FROM (subquery) q(a)), '[]') + * + * The COALESCE ensures that an empty result set produces '[]' rather than + * NULL, per the SQL/JSON standard. + * + * - orig_query: the transformed Query of the user's original subquery, so + * that ruleutils.c can deparse the original JSON_ARRAY(SELECT ...) syntax + * for view definitions. */ static Node * transformJsonArrayQueryConstructor(ParseState *pstate, JsonArrayQueryConstructor *ctor) { - SubLink *sublink = makeNode(SubLink); - SelectStmt *select = makeNode(SelectStmt); - RangeSubselect *range = makeNode(RangeSubselect); - Alias *alias = makeNode(Alias); - ResTarget *target = makeNode(ResTarget); - JsonArrayAgg *agg = makeNode(JsonArrayAgg); - ColumnRef *colref = makeNode(ColumnRef); Query *query; ParseState *qpstate; + SubLink *sublink; + SelectStmt *select; + RangeSubselect *range; + Alias *alias; + ResTarget *target; + JsonArrayAgg *agg; + ColumnRef *colref; + Node *exec_expr; + CoalesceExpr *coalesce; + Const *empty_const; + Oid result_type; + int32 result_typmod; + Oid typinput; + Oid typioparam; + int16 typlen; + bool typbyval; + JsonReturning *returning; + List *args; + Node *result; - /* Transform query only for counting target list entries. */ + /* + * Transform a copy of the subquery to validate the single-column + * constraint and to obtain the transformed Query for deparsing. This + * uses a private ParseState so it doesn't affect the main parse context. + */ qpstate = make_parsestate(pstate); query = transformStmt(qpstate, copyObject(ctor->query)); @@ -3782,14 +3852,20 @@ transformJsonArrayQueryConstructor(ParseState *pstate, free_parsestate(qpstate); + /* + * Build the executable form by constructing query: + * + * (SELECT JSON_ARRAYAGG(a [FORMAT] [RETURNING]) FROM (subquery) q(a)) + * + * ... using raw parse tree nodes, then transforming via + * transformExprRecurse. + */ + colref = makeNode(ColumnRef); colref->fields = list_make2(makeString(pstrdup("q")), makeString(pstrdup("a"))); colref->location = ctor->location; - /* - * No formatting necessary, so set formatted_expr to be the same as - * raw_expr. - */ + agg = makeNode(JsonArrayAgg); agg->arg = makeJsonValueExpr((Expr *) colref, (Expr *) colref, ctor->format); agg->absent_on_null = ctor->absent_on_null; @@ -3798,21 +3874,26 @@ transformJsonArrayQueryConstructor(ParseState *pstate, agg->constructor->output = ctor->output; agg->constructor->location = ctor->location; + target = makeNode(ResTarget); target->name = NULL; target->indirection = NIL; target->val = (Node *) agg; target->location = ctor->location; + alias = makeNode(Alias); alias->aliasname = pstrdup("q"); alias->colnames = list_make1(makeString(pstrdup("a"))); + range = makeNode(RangeSubselect); range->lateral = false; range->subquery = ctor->query; range->alias = alias; + select = makeNode(SelectStmt); select->targetList = list_make1(target); select->fromClause = list_make1(range); + sublink = makeNode(SubLink); sublink->subLinkType = EXPR_SUBLINK; sublink->subLinkId = 0; sublink->testexpr = NULL; @@ -3820,7 +3901,51 @@ transformJsonArrayQueryConstructor(ParseState *pstate, sublink->subselect = (Node *) select; sublink->location = ctor->location; - return transformExprRecurse(pstate, (Node *) sublink); + exec_expr = transformExprRecurse(pstate, (Node *) sublink); + + /* + * Wrap in COALESCE so that an empty result set produces '[]' rather than + * NULL. The empty-array constant is created in the output type and + * typmod, so that the COALESCE arguments have consistent types and any + * length restriction from the RETURNING clause is enforced uniformly + * across the empty and non-empty paths. + */ + result_type = exprType(exec_expr); + result_typmod = exprTypmod(exec_expr); + getTypeInputInfo(result_type, &typinput, &typioparam); + get_typlenbyval(result_type, &typlen, &typbyval); + + empty_const = makeConst(result_type, + result_typmod, + exprCollation(exec_expr), + (int) typlen, + OidInputFunctionCall(typinput, "[]", + typioparam, result_typmod), + false, + typbyval); + + coalesce = makeNode(CoalesceExpr); + coalesce->coalescetype = result_type; + coalesce->coalescecollid = exprCollation(exec_expr); + coalesce->args = list_make2(exec_expr, empty_const); + coalesce->location = ctor->location; + + /* + * Build the JSCTOR_JSON_ARRAY_QUERY node. The COALESCE goes in func as + * the executable form; during planning, eval_const_expressions replaces + * the entire node with func. The transformed Query is stored in + * orig_query so that ruleutils.c can deparse the original syntax. + */ + args = list_make1(linitial_node(TargetEntry, query->targetList)->expr); + returning = transformJsonConstructorOutput(pstate, ctor->output, args); + + result = makeJsonConstructorExpr(pstate, JSCTOR_JSON_ARRAY_QUERY, + NIL, (Expr *) coalesce, returning, + false, ctor->absent_on_null, + ctor->location); + ((JsonConstructorExpr *) result)->orig_query = (Node *) query; + + return result; } /* @@ -4051,7 +4176,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 +4199,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 +4229,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 +4406,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 +4624,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 +4658,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 +4682,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 +4700,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 +4720,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 +4739,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 +4765,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 +4773,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 +4851,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 +4867,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 +4887,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..73fbfb541f7ea --- /dev/null +++ b/src/backend/parser/parse_graphtable.c @@ -0,0 +1,394 @@ +/*------------------------------------------------------------------------- + * + * 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 collations here for the reason mentioned in the prologue of + * transformGraphPattern(). + */ + 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 constructed + * list of variables in the GraphTableParseState to resolve property references + * in the WHERE clause. + * + * Since most parts of the GraphPattern do not require collation assignment, we + * assign collations to the required expressions as they are transformed. This + * avoids the need to traverse the whole GraphPattern again and avoids exposing + * it to assign_expr_collations(). + */ +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..a049cc67ed658 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 */ @@ -2019,7 +2042,10 @@ generateClonedIndexStmt(RangeVar *heapRel, Relation source_idx, * extended statistic "source_statsid", for the rel identified by heapRel and * heapRelid. * - * Attribute numbers in expression Vars are adjusted according to attmap. + * stxkeys in the source statistic holds attribute numbers from the parent + * relation. Those attnums, along with the attribute numbers referenced by + * Vars inside the expression tree, are remapped to the new relation's + * numbering according to attmap. */ static CreateStatsStmt * generateClonedExtStatsStmt(RangeVar *heapRel, Oid heapRelid, @@ -2077,7 +2103,8 @@ generateClonedExtStatsStmt(RangeVar *heapRel, Oid heapRelid, StatsElem *selem = makeNode(StatsElem); AttrNumber attnum = statsrec->stxkeys.values[i]; - selem->name = get_attname(heapRelid, attnum, false); + selem->name = + get_attname(heapRelid, attmap->attnums[attnum - 1], false); selem->expr = NULL; def_names = lappend(def_names, selem); @@ -2548,7 +2575,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 +2764,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 +2782,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 +2794,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 +2821,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 +2938,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 +3522,287 @@ 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 that don't have sub-partitions.") + : errhint("ALTER TABLE ... SPLIT PARTITION can only split partitions that 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 that don't have sub-partitions.") + : errhint("ALTER TABLE ... SPLIT PARTITION can only split partitions that 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 that don't have sub-partitions.") + : errhint("ALTER TABLE ... SPLIT PARTITION can only split partitions that 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("cannot specify more than one DEFAULT partition"), + 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("cannot split DEFAULT partition \"%s\"", + get_rel_name(splitPartOid)), + errhint("To split a DEFAULT partition, one of the new partitions must be DEFAULT.")); + + /* + * 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("cannot split non-DEFAULT partition \"%s\"", + get_rel_name(splitPartOid)), + errdetail("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 +4072,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 +4207,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 +4241,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 +4256,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 +4269,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 +4277,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 +4295,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 +4303,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 +4320,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 +4337,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 +4400,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 +4446,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 +4455,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 +4468,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 +4477,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 +4486,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 +4563,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 +4796,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 +4806,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 +5004,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 +5089,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..9b4277a4987ce 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("cannot merge partition \"%s\" together with partition \"%s\"", + second_name->relname, first_name->relname), + errdetail("The 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("cannot split to partition \"%s\" together with partition \"%s\"", + second_name->relname, first_name->relname), + errdetail("The 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 it", + 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 it", + 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..9803a0ee2a141 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); @@ -66,8 +68,44 @@ AuxiliaryProcessMainCommon(void) BaseInit(); + /* + * Prevent consuming interrupts between setting ProcSignalInit and setting + * the initial local data checksum value. If a barrier is emitted, and + * absorbed, before local cached state is initialized the state transition + * can be invalid. + */ + HOLD_INTERRUPTS(); + 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(); + + RESUME_INTERRUPTS(); + + /* + * Initialize the process-local logical info WAL logging state. + * + * This must be called after ProcSignalInit() so that the process can + * participate in procsignal-based barriers that update this state. + */ + InitializeProcessXLogLogicalInfo(); + /* * 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..33430147ff293 --- /dev/null +++ b/src/backend/postmaster/datachecksum_state.c @@ -0,0 +1,1727 @@ +/*------------------------------------------------------------------------- + * + * 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 of 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[9] = +{ + /* + * 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}, + + /* + * If checksums are being enabled when launcher_exit is executed, state is + * set to off since we cannot reach on at that point. + */ + {PG_DATA_CHECKSUM_INPROGRESS_ON, PG_DATA_CHECKSUM_INPROGRESS_OFF}, + + /* + * Transitions that can happen when a new request is made while another is + * currently being processed. + */ + {PG_DATA_CHECKSUM_INPROGRESS_OFF, PG_DATA_CHECKSUM_INPROGRESS_ON}, + {PG_DATA_CHECKSUM_OFF, PG_DATA_CHECKSUM_INPROGRESS_OFF}, +}; + +/* + * 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 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, +}; + +#define CHECK_FOR_ABORT_REQUEST() \ + do { \ + LWLockAcquire(DataChecksumsWorkerLock, LW_SHARED); \ + if (DataChecksumState->launch_operation != operation) \ + abort_requested = true; \ + LWLockRelease(DataChecksumsWorkerLock); \ + } while (0) + + +/***************************************************************************** + * 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) +{ + PreventCommandDuringRecovery("pg_disable_data_checksums()"); + + 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); + + PreventCommandDuringRecovery("pg_enable_data_checksums()"); + + 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 running; + +#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? */ + running = DataChecksumState->launcher_running; + + 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. + */ + if (!running) + { + if ((op == ENABLE_DATACHECKSUMS && DataChecksumsOn()) || + (op == DISABLE_DATACHECKSUMS && DataChecksumsOff())) + { + ereport(LOG, + errmsg("data checksums already in desired state, exiting")); + return; + } + + /* + * 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 + { + ereport(LOG, + 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". + * + * Unlogged relations don't need WAL since they are reset to their + * init fork on recovery. We still dirty the buffer so that the + * checksum is written to disk at the next checkpoint. + * + * The init fork is an exception: it is WAL-logged so the standby can + * materialize the relation after promotion (see + * ResetUnloggedRelations()). Skipping it here would leave the + * standby with a stale init fork that, once copied to the main fork + * on promotion, would fail checksum verification on every read. + */ + START_CRIT_SECTION(); + MarkBufferDirty(buf); + if (RelationNeedsWAL(reln) || forkNum == INIT_FORKNUM) + 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]; + + LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE); + DataChecksumState->success = DATACHECKSUMSWORKER_FAILED; + LWLockRelease(DataChecksumsWorkerLock); + + 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 see a STOPPED status here without it being a failure. + */ + LWLockAcquire(DataChecksumsWorkerLock, LW_SHARED); + if (DataChecksumState->success == DATACHECKSUMSWORKER_SUCCESSFUL) + { + LWLockRelease(DataChecksumsWorkerLock); + pgstat_report_activity(STATE_IDLE, NULL); + LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE); + DataChecksumState->worker_pid = InvalidPid; + LWLockRelease(DataChecksumsWorkerLock); + return DataChecksumState->success; + } + LWLockRelease(DataChecksumsWorkerLock); + + 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. + */ + 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().")); + + LWLockAcquire(DataChecksumsWorkerLock, LW_SHARED); + if (DataChecksumState->success == DATACHECKSUMSWORKER_ABORTED) + ereport(LOG, + errmsg("data checksums processing was aborted in database \"%s\"", + db->dbname)); + LWLockRelease(DataChecksumsWorkerLock); + + 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 a launcher process which has + * performed checksum operations exits. A launcher process which is exiting due + * to a duplicate started launcher does not need to perform any cleanup and + * this function should not be called. Otherwise, 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(); + CHECK_FOR_ABORT_REQUEST(); + + 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) +{ + + 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; + } + + on_shmem_exit(launcher_exit, 0); + 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 */ + LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE); + DataChecksumState->process_shared_catalogs = true; + LWLockRelease(DataChecksumsWorkerLock); + + /* 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. + */ + LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE); + DataChecksumState->process_shared_catalogs = false; + LWLockRelease(DataChecksumsWorkerLock); + } + + 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 exists + * and returns true if it is found and valid, else false. Note, we cannot use + * database_is_invalid_oid here as it will ERROR out, and we want to gracefully + * handle errors. + */ +static bool +DatabaseExists(Oid dboid) +{ + Relation rel; + ScanKeyData skey; + SysScanDesc scan; + bool found; + HeapTuple tuple; + Form_pg_database pg_database_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); + + /* If the Oid exists, ensure that it's not partially dropped */ + if (found) + { + pg_database_tuple = (Form_pg_database) GETSTRUCT(tuple); + if (database_is_invalid_form(pg_database_tuple)) + found = false; + } + + 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. + * + * VacuumUpdateCosts() propagates the values to the variables actually + * read by vacuum_delay_point(). + */ + VacuumCostDelay = DataChecksumState->cost_delay; + VacuumCostLimit = DataChecksumState->cost_limit; + VacuumUpdateCosts(); + VacuumCostBalance = 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) + { + bool costs_updated = false; + + if (!ProcessSingleRelationByOid(reloid, strategy)) + { + aborted = true; + break; + } + + pgstat_progress_update_param(PROGRESS_DATACHECKSUMS_RELS_DONE, + ++rels_done); + CHECK_FOR_INTERRUPTS(); + CHECK_FOR_ABORT_REQUEST(); + + if (abort_requested) + break; + + /* + * Check if the cost settings changed during runtime and if so, update + * to reflect the new values and signal that the access strategy needs + * to be refreshed. + */ + LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE); + if ((DataChecksumState->launch_cost_delay != DataChecksumState->cost_delay) + || (DataChecksumState->launch_cost_limit != DataChecksumState->cost_limit)) + { + costs_updated = true; + VacuumCostDelay = DataChecksumState->launch_cost_delay; + VacuumCostLimit = DataChecksumState->launch_cost_limit; + VacuumUpdateCosts(); + + DataChecksumState->cost_delay = DataChecksumState->launch_cost_delay; + DataChecksumState->cost_limit = DataChecksumState->launch_cost_limit; + } + else + costs_updated = false; + LWLockRelease(DataChecksumsWorkerLock); + + if (costs_updated) + { + FreeAccessStrategy(strategy); + strategy = GetAccessStrategy(BAS_VACUUM); + } + } + + list_free(RelationList); + FreeAccessStrategy(strategy); + + if (aborted || abort_requested) + { + LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE); + DataChecksumState->success = DATACHECKSUMSWORKER_ABORTED; + LWLockRelease(DataChecksumsWorkerLock); + 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); + + CHECK_FOR_INTERRUPTS(); + CHECK_FOR_ABORT_REQUEST(); + + if (aborted || abort_requested) + { + LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE); + DataChecksumState->success = DATACHECKSUMSWORKER_ABORTED; + LWLockRelease(DataChecksumsWorkerLock); + 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_color.c b/src/backend/regex/regc_color.c index 8ae788f519561..1587f452ea3f7 100644 --- a/src/backend/regex/regc_color.c +++ b/src/backend/regex/regc_color.c @@ -218,6 +218,7 @@ newcolor(struct colormap *cm) n = cm->ncds * 2; if (n > MAX_COLOR + 1) n = MAX_COLOR + 1; + /* the MAX_COLOR+1 limit ensures these alloc sizes can't overflow: */ if (cm->cd == cm->cdspace) { newCd = (struct colordesc *) MALLOC(n * sizeof(struct colordesc)); @@ -434,9 +435,8 @@ newhicolorrow(struct colormap *cm, CERR(REG_ESPACE); return 0; } - newarray = (color *) REALLOC(cm->hicolormap, - cm->maxarrayrows * 2 * - cm->hiarraycols * sizeof(color)); + newarray = REALLOC_ARRAY(cm->hicolormap, color, + cm->maxarrayrows * 2 * cm->hiarraycols); if (newarray == NULL) { CERR(REG_ESPACE); @@ -477,9 +477,8 @@ newhicolorcols(struct colormap *cm) CERR(REG_ESPACE); return; } - newarray = (color *) REALLOC(cm->hicolormap, - cm->maxarrayrows * - cm->hiarraycols * 2 * sizeof(color)); + newarray = REALLOC_ARRAY(cm->hicolormap, color, + cm->maxarrayrows * cm->hiarraycols * 2); if (newarray == NULL) { CERR(REG_ESPACE); @@ -652,8 +651,7 @@ subcoloronechr(struct vars *v, * Potentially, we could need two more colormapranges than we have now, if * the given chr is in the middle of some existing range. */ - newranges = (colormaprange *) - MALLOC((cm->numcmranges + 2) * sizeof(colormaprange)); + newranges = MALLOC_ARRAY(colormaprange, cm->numcmranges + 2); if (newranges == NULL) { CERR(REG_ESPACE); @@ -766,8 +764,7 @@ subcoloronerange(struct vars *v, * Potentially, if we have N non-adjacent ranges, we could need as many as * 2N+1 result ranges (consider case where new range spans 'em all). */ - newranges = (colormaprange *) - MALLOC((cm->numcmranges * 2 + 1) * sizeof(colormaprange)); + newranges = MALLOC_ARRAY(colormaprange, cm->numcmranges * 2 + 1); if (newranges == NULL) { CERR(REG_ESPACE); diff --git a/src/backend/regex/regc_cvec.c b/src/backend/regex/regc_cvec.c index 10306215596c9..8dbcf3c55e308 100644 --- a/src/backend/regex/regc_cvec.c +++ b/src/backend/regex/regc_cvec.c @@ -40,6 +40,9 @@ /* * newcvec - allocate a new cvec + * + * Note: in current usage, nchrs and nranges are never so large that we risk + * integer overflow in these size calculations, even with 32-bit size_t. */ static struct cvec * newcvec(int nchrs, /* to hold this many chrs... */ 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_nfa.c b/src/backend/regex/regc_nfa.c index acd2286defd50..92b11116194cf 100644 --- a/src/backend/regex/regc_nfa.c +++ b/src/backend/regex/regc_nfa.c @@ -3523,6 +3523,10 @@ compact(struct nfa *nfa, assert(!NISERR()); + /* + * The REG_MAX_COMPILE_SPACE restriction ensures that integer overflow + * can't occur in this loop nor in the allocation requests below. + */ nstates = 0; narcs = 0; for (s = nfa->states; s != NULL; s = s->next) @@ -3575,6 +3579,12 @@ compact(struct nfa *nfa, case LACON: assert(s->no != cnfa->pre); assert(a->co >= 0); + /* make sure the modified color number will fit */ + if (a->co > MAX_COLOR - cnfa->ncolors) + { + NERR(REG_ECOLORS); + return; + } ca->co = (color) (cnfa->ncolors + a->co); ca->to = a->to->no; ca++; 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..f50acc0857741 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); @@ -561,6 +561,7 @@ moresubs(struct vars *v, assert(wanted > 0 && (size_t) wanted >= v->nsubs); n = (size_t) wanted * 3 / 2 + 1; + /* n is bounded by the number of states, so no chance of overflow here */ if (v->subs == v->sub10) { p = (struct subre **) MALLOC(n * sizeof(struct subre *)); @@ -975,7 +976,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); @@ -2405,8 +2406,8 @@ newlacon(struct vars *v, else { n = v->nlacons; - newlacons = (struct subre *) REALLOC(v->lacons, - (n + 1) * sizeof(struct subre)); + /* better use REALLOC_ARRAY here, as struct subre is big */ + newlacons = REALLOC_ARRAY(v->lacons, struct subre, n + 1); } if (newlacons == NULL) { diff --git a/src/backend/regex/rege_dfa.c b/src/backend/regex/rege_dfa.c index 1f8f2ab1441ff..5b57fed60a912 100644 --- a/src/backend/regex/rege_dfa.c +++ b/src/backend/regex/rege_dfa.c @@ -640,20 +640,29 @@ newdfa(struct vars *v, } else { + /* + * Restrict the ranges of nstates and ncolors enough that the arrays + * we allocate here have no more than INT_MAX members. This protects + * not only the allocation calculations just below, but later indexing + * into these arrays. + */ + if (wordsper >= INT_MAX / (nss + WORK) || + cnfa->ncolors >= INT_MAX / nss) + { + ERR(REG_ETOOBIG); + return NULL; + } d = (struct dfa *) MALLOC(sizeof(struct dfa)); if (d == NULL) { ERR(REG_ESPACE); return NULL; } - d->ssets = (struct sset *) MALLOC(nss * sizeof(struct sset)); - d->statesarea = (unsigned *) MALLOC((nss + WORK) * wordsper * - sizeof(unsigned)); + d->ssets = MALLOC_ARRAY(struct sset, nss); + d->statesarea = MALLOC_ARRAY(unsigned, (nss + WORK) * wordsper); d->work = &d->statesarea[nss * wordsper]; - d->outsarea = (struct sset **) MALLOC(nss * cnfa->ncolors * - sizeof(struct sset *)); - d->incarea = (struct arcp *) MALLOC(nss * cnfa->ncolors * - sizeof(struct arcp)); + d->outsarea = MALLOC_ARRAY(struct sset *, nss * cnfa->ncolors); + d->incarea = MALLOC_ARRAY(struct arcp, nss * cnfa->ncolors); d->ismalloced = true; d->arraysmalloced = true; /* now freedfa() will behave sanely */ diff --git a/src/backend/regex/regexec.c b/src/backend/regex/regexec.c index 2a1d5bebda30f..665aa31bd0383 100644 --- a/src/backend/regex/regexec.c +++ b/src/backend/regex/regexec.c @@ -231,7 +231,7 @@ pg_regexec(regex_t *re, if (v->nmatch <= LOCALMAT) v->pmatch = mat; else - v->pmatch = (regmatch_t *) MALLOC(v->nmatch * sizeof(regmatch_t)); + v->pmatch = MALLOC_ARRAY(regmatch_t, v->nmatch); if (v->pmatch == NULL) return REG_ESPACE; zapallsubs(v->pmatch, v->nmatch); @@ -265,6 +265,7 @@ pg_regexec(regex_t *re, v->subdfas = subdfas; else { + /* ntree is surely less than the number of states, so this is safe: */ v->subdfas = (struct dfa **) MALLOC(n * sizeof(struct dfa *)); if (v->subdfas == NULL) { @@ -279,6 +280,7 @@ pg_regexec(regex_t *re, n = (size_t) v->g->nlacons; if (n > 0) { + /* nlacons is surely less than the number of arcs, so this is safe: */ v->ladfas = (struct dfa **) MALLOC(n * sizeof(struct dfa *)); if (v->ladfas == NULL) { @@ -1163,7 +1165,7 @@ citerdissect(struct vars *v, max_matches = t->max; if (max_matches < min_matches) max_matches = min_matches; - endpts = (chr **) MALLOC((max_matches + 1) * sizeof(chr *)); + endpts = MALLOC_ARRAY(chr *, max_matches + 1); if (endpts == NULL) return REG_ESPACE; endpts[0] = begin; @@ -1370,7 +1372,7 @@ creviterdissect(struct vars *v, max_matches = t->max; if (max_matches < min_matches) max_matches = min_matches; - endpts = (chr **) MALLOC((max_matches + 1) * sizeof(chr *)); + endpts = MALLOC_ARRAY(chr *, max_matches + 1); if (endpts == NULL) return REG_ESPACE; endpts[0] = begin; 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..1f8d67fdd901f 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,44 @@ 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 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; + + /* standard SQL punctuation, not translated */ + if (!first) + appendStringInfoString(buf, ", "); + + appendStringInfoString(buf, tuple_value); + first = false; + } +} + /* * 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 +232,28 @@ 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; + StringInfoData tuple_buf; 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); + initStringInfo(&tuple_buf); - /* 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,16 +262,50 @@ errdetail_apply_conflict(EState *estate, ResultRelInfo *relinfo, Assert(OidIsValid(indexoid) && CheckRelationOidLockedByMe(indexoid, RowExclusiveLock, true)); + if (err_msg->len == 0) + { + append_tuple_value_detail(&tuple_buf, + list_make2(remote_desc, search_desc)); + + if (tuple_buf.len) + appendStringInfo(&err_detail, _("Could not apply remote change: %s.\n"), + tuple_buf.data); + else + appendStringInfo(&err_detail, _("Could not apply remote change.\n")); + + + resetStringInfo(&tuple_buf); + } + + append_tuple_value_detail(&tuple_buf, + list_make2(key_desc, local_desc)); + if (localts) { - if (localorigin == InvalidRepOriginId) - 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)); + if (localorigin == InvalidReplOriginId) + { + if (tuple_buf.len) + appendStringInfo(&err_detail, _("Key already exists in unique index \"%s\", modified locally in transaction %u at %s: %s."), + get_rel_name(indexoid), + localxmin, timestamptz_to_str(localts), + tuple_buf.data); + else + 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."), - get_rel_name(indexoid), origin_name, - localxmin, timestamptz_to_str(localts)); + { + if (tuple_buf.len) + appendStringInfo(&err_detail, _("Key already exists in unique index \"%s\", modified by origin \"%s\" in transaction %u at %s: %s."), + get_rel_name(indexoid), origin_name, + localxmin, timestamptz_to_str(localts), + tuple_buf.data); + else + 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)); + } /* * The origin that modified this row has been removed. This @@ -236,67 +315,171 @@ 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."), - get_rel_name(indexoid), - localxmin, timestamptz_to_str(localts)); + { + if (tuple_buf.len) + appendStringInfo(&err_detail, _("Key already exists in unique index \"%s\", modified by a non-existent origin in transaction %u at %s: %s."), + get_rel_name(indexoid), + localxmin, timestamptz_to_str(localts), + tuple_buf.data); + else + 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."), - get_rel_name(indexoid), localxmin); + { + if (tuple_buf.len) + appendStringInfo(&err_detail, _("Key already exists in unique index \"%s\", modified in transaction %u: %s."), + get_rel_name(indexoid), localxmin, + tuple_buf.data); + else + appendStringInfo(&err_detail, _("Key already exists in unique index \"%s\", modified in transaction %u."), + get_rel_name(indexoid), localxmin); + } break; case CT_UPDATE_ORIGIN_DIFFERS: - if (localorigin == InvalidRepOriginId) - appendStringInfo(&err_detail, _("Updating the row that was modified locally in transaction %u at %s."), - localxmin, timestamptz_to_str(localts)); + append_tuple_value_detail(&tuple_buf, + list_make3(local_desc, remote_desc, + search_desc)); + + if (localorigin == InvalidReplOriginId) + { + if (tuple_buf.len) + appendStringInfo(&err_detail, _("Updating the row that was modified locally in transaction %u at %s: %s."), + localxmin, timestamptz_to_str(localts), + tuple_buf.data); + else + 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."), - origin_name, localxmin, timestamptz_to_str(localts)); + { + if (tuple_buf.len) + appendStringInfo(&err_detail, _("Updating the row that was modified by a different origin \"%s\" in transaction %u at %s: %s."), + origin_name, localxmin, + timestamptz_to_str(localts), + tuple_buf.data); + else + 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."), - localxmin, timestamptz_to_str(localts)); + { + if (tuple_buf.len) + appendStringInfo(&err_detail, _("Updating the row that was modified by a non-existent origin in transaction %u at %s: %s."), + localxmin, timestamptz_to_str(localts), + tuple_buf.data); + else + appendStringInfo(&err_detail, _("Updating the row that was modified by a non-existent origin in transaction %u at %s."), + localxmin, timestamptz_to_str(localts)); + } + + break; + + case CT_UPDATE_DELETED: + append_tuple_value_detail(&tuple_buf, + list_make2(remote_desc, search_desc)); + + if (tuple_buf.len) + appendStringInfo(&err_detail, _("Could not find the row to be updated: %s.\n"), + tuple_buf.data); + else + appendStringInfo(&err_detail, _("Could not find the row to be updated.\n")); + + 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.")); + append_tuple_value_detail(&tuple_buf, + list_make2(remote_desc, search_desc)); + + if (tuple_buf.len) + appendStringInfo(&err_detail, _("Could not find the row to be updated: %s."), + tuple_buf.data); + else + appendStringInfo(&err_detail, _("Could not find the row to be updated.")); + break; case CT_DELETE_ORIGIN_DIFFERS: - if (localorigin == InvalidRepOriginId) - appendStringInfo(&err_detail, _("Deleting the row that was modified locally in transaction %u at %s."), - localxmin, timestamptz_to_str(localts)); + append_tuple_value_detail(&tuple_buf, + list_make3(local_desc, remote_desc, + search_desc)); + + if (localorigin == InvalidReplOriginId) + { + if (tuple_buf.len) + appendStringInfo(&err_detail, _("Deleting the row that was modified locally in transaction %u at %s: %s."), + localxmin, timestamptz_to_str(localts), + tuple_buf.data); + else + 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."), - origin_name, localxmin, timestamptz_to_str(localts)); + { + if (tuple_buf.len) + appendStringInfo(&err_detail, _("Deleting the row that was modified by a different origin \"%s\" in transaction %u at %s: %s."), + origin_name, localxmin, + timestamptz_to_str(localts), + tuple_buf.data); + else + 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."), - localxmin, timestamptz_to_str(localts)); + { + if (tuple_buf.len) + appendStringInfo(&err_detail, _("Deleting the row that was modified by a non-existent origin in transaction %u at %s: %s."), + localxmin, timestamptz_to_str(localts), + tuple_buf.data); + else + appendStringInfo(&err_detail, _("Deleting the row that was modified by a non-existent origin in transaction %u at %s."), + localxmin, timestamptz_to_str(localts)); + } break; case CT_DELETE_MISSING: - appendStringInfoString(&err_detail, _("Could not find the row to be deleted.")); + append_tuple_value_detail(&tuple_buf, + list_make1(search_desc)); + + if (tuple_buf.len) + appendStringInfo(&err_detail, _("Could not find the row to be deleted: %s."), + tuple_buf.data); + else + appendStringInfo(&err_detail, _("Could not find the row to be deleted.")); + 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 +491,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 +522,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 +555,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 +588,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..f953676bfe186 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; } } @@ -2656,15 +2674,33 @@ ReorderBufferProcessTXN(ReorderBuffer *rb, ReorderBufferTXN *txn, * Aborting the current (sub-)transaction as a whole has the right * semantics. We want all locks acquired in here to be released, not * reassigned to the parent and we do not want any database access - * have persistent effects. + * have persistent effects. In the !using_subtxn case this is a + * top-level abort; keep it out of pg_stat_database.xact_rollback. */ - AbortCurrentTransaction(); + if (using_subtxn) + AbortCurrentTransaction(); + else + AbortCurrentTransactionWithoutXactStats(); /* 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 @@ -2705,16 +2741,33 @@ ReorderBufferProcessTXN(ReorderBuffer *rb, ReorderBufferTXN *txn, /* * Force cache invalidation to happen outside of a valid transaction - * to prevent catalog access as we just caught an error. + * to prevent catalog access as we just caught an error. As above, + * keep the top-level abort out of pg_stat_database.xact_rollback. */ - AbortCurrentTransaction(); + if (using_subtxn) + AbortCurrentTransaction(); + else + AbortCurrentTransactionWithoutXactStats(); /* 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 +2830,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 +2890,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 +2913,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 +2929,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 +2981,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 +3007,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 +3025,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 +3047,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 +3057,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 +3102,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 +3113,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 +3201,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 +3260,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 +3280,11 @@ ReorderBufferImmediateInvalidation(ReorderBuffer *rb, uint32 ninvalidations, LocalExecuteInvalidationMessage(&invalidations[i]); if (use_subtxn) + { RollbackAndReleaseCurrentSubTransaction(); + MemoryContextSwitchTo(ccxt); + CurrentResourceOwner = cowner; + } } /* @@ -3421,6 +3482,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 +3551,6 @@ ReorderBufferAddInvalidations(ReorderBuffer *rb, TransactionId xid, { ReorderBufferTXN *txn; MemoryContext oldcontext; - ReorderBufferChange *change; txn = ReorderBufferTXNByXid(rb, xid, true, NULL, lsn, true); @@ -3456,35 +3565,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 +3705,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 +3902,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 +3981,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 +4555,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 +4863,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 +4957,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 +5127,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 +5137,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 +5161,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 +5181,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 +5367,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 +5534,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 +5550,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..e2ff8d77b16e5 --- /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); + + /* + * The sequence data can be NULL due to insufficient privileges or 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; + + 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; + Relation sequence_rel = NULL; + 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..ba9af88c505b8 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 @@ -614,8 +631,13 @@ SnapBuildClearExportedSnapshot(void) */ tmpResOwner = SavedResourceOwnerDuringExport; - /* make sure nothing could have ever happened */ - AbortCurrentTransaction(); + /* + * Make sure nothing could have ever happened. Keep this cleanup abort + * out of pg_stat_database.xact_rollback; we must be at top level so + * the abort reaches AtEOXact_PgStat_Database. + */ + Assert(!IsSubTransaction()); + AbortCurrentTransactionWithoutXactStats(); CurrentResourceOwner = tmpResOwner; } @@ -774,7 +796,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 +816,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 +836,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 +859,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 +1156,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 +1169,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 +1203,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 +1271,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 +1279,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 +1332,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 +1354,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 +1370,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 +1420,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 +1444,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 +1467,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 +1523,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 +1574,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 +1977,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 +2094,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..dd6fc38a41ea0 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,722 @@ 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. Skip this cap when maxretention is zero, + * which means unlimited retention (no timeout). + */ + if (MySubscription->retentionactive && MySubscription->maxretention > 0) + 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 +5153,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 +5170,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 +5299,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 +5308,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 +5375,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 +5605,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 +5638,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 +5650,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 +5664,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 +5695,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 +5740,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 +5770,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 +5779,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 +5794,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 +5828,6 @@ InitializeLogRepWorker(void) } MySubscriptionValid = true; - MemoryContextSwitchTo(oldctx); if (!MySubscription->enabled) { @@ -4707,10 +5838,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 +5877,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 +5900,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 +5970,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 +5979,7 @@ SetupApplyOrSyncWorker(int worker_slot) * the subscription relation state. */ CacheRegisterSyscacheCallback(SUBSCRIPTIONRELMAP, - invalidate_syncing_table_states, + InvalidateSyncingRelStates, (Datum) 0); } @@ -4837,13 +6019,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 +6047,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 +6093,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 +6101,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 +6115,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 +6139,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 +6148,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 +6204,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 +6212,8 @@ clear_subscription_skip_lsn(XLogRecPtr finish_lsn) heap_freetuple(tup); table_close(rel, NoLock); + PopActiveSnapshot(); + if (started_tx) CommitTransactionCommand(); } @@ -5026,13 +6235,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 +6251,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 +6259,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 +6269,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 +6278,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..e0e30579c5902 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,19 @@ 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."), + errdetail_log("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 +365,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 +503,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 +576,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 +686,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 +715,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 +767,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 +808,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 +1037,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 +1087,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..07eac07b9ce4c 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); @@ -302,6 +302,9 @@ WalReceiverMain(const void *startup_data, size_t startup_data_len) if (sender_host) pfree(sender_host); + /* Initialize buffers for processing messages */ + initStringInfo(&reply_message); + first_stream = true; for (;;) { @@ -325,6 +328,7 @@ WalReceiverMain(const void *startup_data, size_t startup_data_len) errdetail("The primary's identifier is %s, the standby's identifier is %s.", primary_sysid, standby_sysid))); } + pfree(primary_sysid); /* * Confirm that the current timeline of the primary is the same or @@ -386,17 +390,27 @@ 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; - /* Initialize LogstreamResult and buffers for processing messages */ + /* + * 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 for processing messages */ LogstreamResult.Write = LogstreamResult.Flush = GetXLogReplayRecPtr(NULL); - initStringInfo(&reply_message); /* Initialize nap wakeup times. */ now = GetCurrentTimestamp(); @@ -404,7 +418,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 +484,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 +494,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 +540,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 +548,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 +596,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 +663,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 +702,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 +713,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 +725,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 +805,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 +841,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 +865,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 +887,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 +943,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 +964,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 */ @@ -963,7 +978,13 @@ XLogWalRcvWrite(char *buf, Size nbytes, XLogRecPtr recptr, TimeLineID tli) } /* Update shared-memory status */ - pg_atomic_write_u64(&WalRcv->writtenUpto, LogstreamResult.Write); + pg_atomic_write_membarrier_u64(&WalRcv->writtenUpto, LogstreamResult.Write); + + /* + * Wake up processes waiting for standby write LSN to reach current write + * position. + */ + WaitLSNWakeup(WAIT_LSN_TYPE_STANDBY_WRITE, LogstreamResult.Write); /* * Close the current segment if it's fully written up in the last cycle of @@ -1004,6 +1025,12 @@ XLogWalRcvFlush(bool dying, TimeLineID tli) } SpinLockRelease(&walrcv->mutex); + /* + * Wake up processes waiting for standby flush LSN to reach current + * flush position. + */ + WaitLSNWakeup(WAIT_LSN_TYPE_STANDBY_FLUSH, LogstreamResult.Flush); + /* Signal the startup process and walsender that new WAL has arrived */ WakeupRecovery(); if (AllowCascadeReplication()) @@ -1014,7 +1041,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 +1049,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 +1103,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 +1147,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 +1167,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 +1179,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 +1275,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 +1388,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 +1414,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 +1493,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 +1512,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 +1534,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..a0ed853e2f60f 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,17 @@ 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; + + /* + * Pairs with pg_atomic_read_membarrier_u64() in + * GetWalRcvWriteRecPtr(). + */ + pg_atomic_write_membarrier_u64(&walrcv->writtenUpto, recptr); } walrcv->receiveStart = recptr; walrcv->receiveStartTLI = tli; @@ -347,14 +368,17 @@ GetWalRcvFlushRecPtr(XLogRecPtr *latestChunkStart, TimeLineID *receiveTLI) /* * Returns the last+1 byte position that walreceiver has written. - * This returns a recently written value without taking a lock. + * + * Use pg_atomic_read_membarrier_u64() to ensure that callers see up-to-date + * shared memory state, matching the barrier semantics provided by the + * spinlock in GetWalRcvFlushRecPtr() and other LSN-position functions. */ XLogRecPtr GetWalRcvWriteRecPtr(void) { WalRcvData *walrcv = WalRcv; - return pg_atomic_read_u64(&walrcv->writtenUpto); + return pg_atomic_read_membarrier_u64(&walrcv->writtenUpto); } /* diff --git a/src/backend/replication/walsender.c b/src/backend/replication/walsender.c index 9fa8beb6103d3..04aa770d981cd 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,16 @@ 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; + +/* + * Set after queueing the CommandComplete message that ends WAL streaming + * during shutdown. This prevents WalSndDone() and WalSndDoneImmediate() + * from queueing the same message twice. + */ +static bool shutdown_stream_done_queued = false; + /* * 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 +260,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 +289,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 +303,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 +420,6 @@ WalSndShutdown(void) whereToSendOutput = DestNone; proc_exit(0); - abort(); /* keep the compiler quiet */ } /* @@ -408,7 +454,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 +480,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 +526,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 +559,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 +571,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 +629,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 +782,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 +810,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 +937,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 +988,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 +1032,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 +1047,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 +1184,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 +1248,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 +1268,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 +1279,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 +1329,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 +1382,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 +1403,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 +1495,7 @@ StartLogicalReplication(StartReplicationCmd *cmd) QueryCompletion qc; /* make sure that our requirements are still fulfilled */ - CheckLogicalDecodingRequirements(); + CheckLogicalDecodingRequirements(false); Assert(!MyReplicationSlot); @@ -1531,7 +1590,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 +1626,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 +1646,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 +1689,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 +1714,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 +1896,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 +1917,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 +1926,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 +2011,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 +2384,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 +2393,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 +2409,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 +2445,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 +2474,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 +2515,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 +2533,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 +2550,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 +2599,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 +2806,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 +2909,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 +2964,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 +3028,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 +3090,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 +3211,7 @@ InitWalSenderSlot(void) SpinLockRelease(&walsnd->mutex); /* don't need the lock anymore */ - MyWalSnd = (WalSnd *) walsnd; + MyWalSnd = walsnd; break; } @@ -3246,12 +3473,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 +3530,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 +3601,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 +3619,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 +3673,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 +3712,51 @@ 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) && + !shutdown_stream_done_queued) + { + QueryCompletion qc; + + /* Try to inform receiver that XLOG streaming is done */ + SetQueryCompletion(&qc, CMDTAG_COPY, 0); + EndCommandExtended(&qc, DestRemote, false, true); + shutdown_stream_done_queued = true; + + /* + * 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,18 +3779,63 @@ 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()) { QueryCompletion qc; + Assert(!shutdown_stream_done_queued); + /* Inform the standby that XLOG streaming is done */ SetQueryCompletion(&qc, CMDTAG_COPY, 0); - EndCommand(&qc, DestRemote, false); - pq_flush(); + EndCommandExtended(&qc, DestRemote, false, true); + shutdown_stream_done_queued = true; + + /* + * Reset last_reply_timestamp so subsequent WalSndComputeSleeptime() + * calls ignore wal_sender_timeout during shutdown. + */ + last_reply_timestamp = 0; + + /* + * Do not call pq_flush() here, since it can block indefinitely while + * waiting for the socket to become writable, preventing + * wal_sender_shutdown_timeout from being enforced. Instead, use the + * walsender nonblocking flush path so the shutdown timeout continues + * to be checked while the send buffer drains. + */ + for (;;) + { + long sleeptime; + + /* + * During shutdown, die if the shutdown timeout expires. Call this + * before WalSndComputeSleeptime() so the timeout is considered + * when computing sleep time. + */ + WalSndCheckShutdownTimeout(); + + if (!pq_is_send_pending()) + break; + + sleeptime = WalSndComputeSleeptime(GetCurrentTimestamp()); + + /* Sleep until something happens or we time out */ + WalSndWait(WL_SOCKET_WRITEABLE, sleeptime, + WAIT_EVENT_WAL_SENDER_WRITE_DATA); + + /* Clear any already-pending wakeups */ + ResetLatch(MyLatch); + + CHECK_FOR_INTERRUPTS(); + + /* Try to flush pending output to the client */ + if (pq_flush_if_writable() != 0) + WalSndShutdown(); + } proc_exit(0); } @@ -3600,6 +3925,8 @@ HandleWalSndInitStopping(void) kill(MyProcPid, SIGTERM); else got_STOPPING = true; + + /* latch will be set by procsignal_sigusr1_handler */ } /* @@ -3624,56 +3951,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; + for (int i = 0; i < NUM_SYNC_REP_WAIT_MODE; i++) + dlist_init(&(WalSndCtl->SyncRepQueue[i])); - WalSndCtl = (WalSndCtlData *) - ShmemInitStruct("Wal Sender Ctl", WalSndShmemSize(), &found); - - 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 +4192,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 +4292,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 +4313,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 +4383,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 +4440,6 @@ WalSndKeepaliveIfNecessary(void) static void LagTrackerWrite(XLogRecPtr lsn, TimestampTz local_flush_time) { - bool buffer_full; int new_write_head; int i; @@ -4145,25 +4461,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 +4500,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..33d4e866d7435 --- /dev/null +++ b/src/backend/rewrite/rewriteGraphTable.c @@ -0,0 +1,1334 @@ +/*------------------------------------------------------------------------- + * + * 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 "miscadmin.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 vertices 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 end up producing no path if any of the element patterns + * yields no elements or the 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 vertices. 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 vertices 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 vertices 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 vertices 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 vertices 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); + + /* Guard against stack overflow due to complex path patterns. */ + check_stack_depth(); + + 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 to avoid + * unprivileged data access through a property graph. 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; + + /* Guard against stack overflow due to many path queries. */ + check_stack_depth(); + + /* 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..1cc4d657231af 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,35 @@ 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); + ArrayType *vals_arr = DatumGetArrayTypeP(stavalues); + ArrayType *nums_arr = DatumGetArrayTypeP(stanumbers); + int nvals = ARR_DIMS(vals_arr)[0]; + int nnums = ARR_DIMS(nums_arr)[0]; + + if (nvals != nnums) + { + ereport(WARNING, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("could not parse \"%s\": incorrect number of elements (same as \"%s\" required)", + "most_common_vals", + "most_common_freqs"))); + result = false; + } + else + { + statatt_set_slot(values, nulls, replaces, + STATISTIC_KIND_MCV, + eq_opr, atttypcoll, + stanumbers, false, stavalues, false); + } } else result = false; @@ -393,18 +405,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 +429,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 +442,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 +464,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 +482,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 +510,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 +535,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 +591,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 +603,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 +613,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..70393d3a9040f --- /dev/null +++ b/src/backend/statistics/extended_stats_funcs.c @@ -0,0 +1,1834 @@ +/*------------------------------------------------------------------------- + * + * 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 (ARR_NDIM(DatumGetArrayTypeP(result)) != 1) + { + ereport(WARNING, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("could not import element \"%s\" in expression %d: must be a one-dimensional array", + element_name, exprnum))); + 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) + { + ArrayType *vals_arr = DatumGetArrayTypeP(stavalues); + ArrayType *nums_arr = DatumGetArrayTypeP(stanumbers); + int nvals = ARR_DIMS(vals_arr)[0]; + int nnums = ARR_DIMS(nums_arr)[0]; + + if (nvals != nnums) + { + ereport(WARNING, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("could not parse \"%s\": incorrect number of elements (same as \"%s\" required)", + "most_common_vals", + "most_common_freqs"))); + goto pg_statistic_error; + } + + 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..a673e3c704b25 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))); - } - - 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); + NameStr(form->relname)), + errdetail_relkind_not_supported(form->relkind))); } - 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); -} - -/* - * 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))); + ReleaseSysCache(tuple); - 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,334 @@ 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 (ARR_NDIM(DatumGetArrayTypeP(result)) != 1) + { + ereport(WARNING, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("\"%s\" must be a one-dimensional array", staname))); + *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..817855e2720f7 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 @@ -562,7 +563,7 @@ pg_flush_data(int fd, off_t offset, off_t nbytes) { int elevel; - if (rc == EINTR) + if (errno == EINTR) goto retry; /* @@ -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..006edab9d77df 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 @@ -231,8 +231,18 @@ XLogRecordPageWithFreeSpace(RelFileLocator rlocator, BlockNumber heapBlk, if (PageIsNew(page)) PageInit(page, BLCKSZ, 0); + /* + * Changes to FSM are usually marked as changed using MarkBufferDirtyHint; + * however, during recovery, it does nothing if checksums are enabled. It + * is assumed that the page should not be dirtied during recovery while + * modifying hints to prevent torn pages, since no new WAL data can be + * generated at this point to store FPI. This is not relevant to the FSM + * case, as its blocks are zeroed when a checksum mismatch occurs. So, we + * need to use regular MarkBufferDirty here to mark the FSM block as + * modified during recovery, otherwise changes to the FSM may be lost. + */ if (fsm_set_avail(page, slot, new_cat)) - MarkBufferDirtyHint(buf, false); + MarkBufferDirty(buf); UnlockReleaseBuffer(buf); } @@ -904,14 +914,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..f1f7cd3a4ff2c 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,737 @@ * 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 "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, + + /* 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 *allocated_size); +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; -static HTAB *ShmemIndex = NULL; /* primary index hashtable for shmem */ + /* 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) + +/* 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) +{ + 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); + } + + 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) { - PGShmemHeader *shmhdr = ShmemSegHdr; - char *aligned; + size_t size; - Assert(shmhdr != NULL); + /* 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 spinlock used by ShmemAlloc. We must use - * ShmemAllocUnlocked, since obviously ShmemAlloc can't be called yet. + * 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. */ - ShmemLock = (slock_t *) ShmemAllocUnlocked(sizeof(slock_t)); + foreach_ptr(ShmemRequest, request, pending_shmem_requests) + { + InitShmemIndexEntry(request); + pfree(request->options); + } + list_free_deep(pending_shmem_requests); + pending_shmem_requests = NIL; + + /* + * Call the subsystem-specific init callbacks to finish initialization of + * all the areas. + */ + 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))); + + /* + * 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; - /* ShmemIndex can't be set up yet (need LWLocks first) */ - shmhdr->index = NULL; - ShmemIndex = (HTAB *) NULL; + /* + * 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 +757,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 +765,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 +784,7 @@ ShmemAllocNoError(Size size) { Size allocated_size; - return ShmemAllocRaw(size, &allocated_size); + return ShmemAllocRaw(size, 0, &allocated_size); } /* @@ -183,8 +794,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 +812,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,249 +852,192 @@ 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; - - /* - * 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. - */ - infoP->dsize = infoP->max_dsize = hash_select_dirsize(max_size); - infoP->alloc = ShmemAllocNoError; - hash_flags |= HASH_SHARED_MEM | HASH_ALLOC | HASH_DIRSIZE; + bool found_any; + bool notfound_any; - /* look it up in the shmem index */ - location = ShmemInitStruct(name, - hash_get_shared_size(infoP, hash_flags), - &found); + Assert(shmem_request_state == SRS_DONE); + shmem_request_state = SRS_REQUESTING; /* - * if it already exists, attach to it rather than allocate and initialize - * new space + * Call the request callback first. The callback makes ShmemRequest*() + * calls for each shmem area, adding them to pending_shmem_requests. */ - if (found) - hash_flags |= HASH_ATTACH; - - /* Pass location of hashtable header to hash_create */ - infoP->hctl = (HASHHDR *) location; + Assert(pending_shmem_requests == NIL); + if (callbacks->request_fn) + callbacks->request_fn(callbacks->opaque_arg); + shmem_request_state = SRS_AFTER_STARTUP_ATTACH_OR_INIT; - return hash_create(name, init_size, infoP, hash_flags); -} - -/* - * ShmemInitStruct -- Create/attach to a structure in shared memory. - * - * This is called during initialization to find or allocate - * a data structure in shared memory. If no other process - * has created the structure, this routine allocates space - * for it. If it exists already, a pointer to the existing - * structure is returned. - * - * 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. - */ -void * -ShmemInitStruct(const char *name, Size size, bool *foundPtr) -{ - ShmemIndexEnt *result; - void *structPtr; + 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 (!ShmemIndex) + /* + * 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. + */ + found_any = notfound_any = false; + foreach_ptr(ShmemRequest, request, pending_shmem_requests) { - 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; - } + if (hash_search(ShmemIndex, request->options->name, HASH_FIND, NULL)) + found_any = 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; + notfound_any = true; } + if (found_any && notfound_any) + elog(ERROR, "found some but not all"); - /* look it up in the shmem index */ - result = (ShmemIndexEnt *) - hash_search(ShmemIndex, name, HASH_ENTER_NULL, foundPtr); - - if (!result) + /* + * Allocate or attach all the shmem areas requested by the request_fn + * callback. + */ + foreach_ptr(ShmemRequest, request, pending_shmem_requests) { - LWLockRelease(ShmemIndexLock); - ereport(ERROR, - (errcode(ERRCODE_OUT_OF_MEMORY), - errmsg("could not create ShmemIndex entry for data structure \"%s\"", - name))); + if (found_any) + AttachShmemIndexEntry(request, false); + else + InitShmemIndexEntry(request); + + pfree(request->options); } + list_free_deep(pending_shmem_requests); + pending_shmem_requests = NIL; - if (*foundPtr) + /* Finish by calling the appropriate subsystem-specific callback */ + if (found_any) { - /* - * 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; + if (callbacks->attach_fn) + callbacks->attach_fn(callbacks->opaque_arg); } else { - Size allocated_size; - - /* 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; + if (callbacks->init_fn) + callbacks->init_fn(callbacks->opaque_arg); } LWLockRelease(ShmemIndexLock); - - Assert(ShmemAddrIsValid(structPtr)); - - Assert(structPtr == (void *) CACHELINEALIGN(structPtr)); - - return structPtr; + shmem_request_state = SRS_DONE; } - /* - * Add two Size values, checking for overflow + * Call all shmem request callbacks. */ -Size -add_size(Size s1, Size s2) +void +ShmemCallRequestCallbacks(void) { - Size result; + ListCell *lc; - result = s1 + s2; - /* We are assuming Size is an unsigned type here... */ - if (result < s1 || result < s2) - ereport(ERROR, - (errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED), - errmsg("requested shared memory size overflows size_t"))); - return result; + 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); + } } /* - * Multiply two Size values, checking for overflow + * ShmemInitStruct -- Create/attach to a structure in shared memory. + * + * This is called during initialization to find or allocate + * a data structure in shared memory. If no other process + * has created the structure, this routine allocates space + * for it. If it exists already, a pointer to the existing + * structure is returned. + * + * Returns: pointer to the object. *foundPtr is set true if the object was + * already in the shmem index (hence, already initialized). + * + * Note: This is a legacy interface, kept for backwards compatibility with + * extensions. Use ShmemRequestStruct() in new code! */ -Size -mul_size(Size s1, Size s2) +void * +ShmemInitStruct(const char *name, Size size, bool *foundPtr) { - Size result; + 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); - if (s1 == 0 || s2 == 0) - return 0; - result = s1 * s2; - /* We are assuming Size is an unsigned type here... */ - if (result / s2 != s1) - ereport(ERROR, - (errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED), - errmsg("requested shared memory size overflows size_t"))); - return result; + LWLockAcquire(ShmemIndexLock, LW_EXCLUSIVE); + + /* + * During postmaster startup, look up the existing entry if any. + */ + *foundPtr = false; + if (IsUnderPostmaster) + *foundPtr = AttachShmemIndexEntry(&request, true); + + /* Initialize it if not found */ + if (!*foundPtr) + InitShmemIndexEntry(&request); + + LWLockRelease(ShmemIndexLock); + + Assert(ptr != NULL); + return ptr; } /* SQL SRF showing allocated shared memory */ @@ -557,15 +1075,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 +1121,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. - * - * 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. + * Shared memory allocations can vary in size and may not align with OS + * memory page boundaries, while NUMA queries work on pages. * - * 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 +1146,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 +1157,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 +1193,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 +1205,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) + { + /* valid NUMA node */ + nodes[s]++; + continue; + } + else if (s == -2) { - elog(ERROR, "invalid NUMA node id outside of allowed range " - "[0, " UINT64_FORMAT "]: %d", max_nodes, s); + /* -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 +1239,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..a810e41a9040e 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,10 +492,11 @@ static int ProcessStartupPacket(Port *port, bool ssl_done, bool gss_done) { int32 len; - char *buf; + char *buf = NULL; ProtocolVersion proto; MemoryContext oldcontext; +retry: pq_startmsgread(); /* @@ -516,7 +517,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 +527,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 +539,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 +555,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 +569,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 +608,17 @@ 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); + buf = NULL; + /* * 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 @@ -632,7 +636,16 @@ ProcessStartupPacket(Port *port, bool ssl_done, bool gss_done) * another SSL negotiation request, and a GSS request should only * follow if SSL was rejected (client may negotiate in either order) */ - return ProcessStartupPacket(port, true, SSLok == 'S'); + ssl_done = true; + if (SSLok == 'S') + { + /* + * We are done with SSL and negotiated correctly, so consider the + * same for GSS. + */ + gss_done = true; + } + goto retry; } else if (proto == NEGOTIATE_GSS_CODE && !gss_done) { @@ -661,14 +674,17 @@ 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); + buf = NULL; + /* * 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 @@ -686,7 +702,16 @@ ProcessStartupPacket(Port *port, bool ssl_done, bool gss_done) * another GSS negotiation request, and an SSL request should only * follow if GSS was rejected (client may negotiate in either order) */ - return ProcessStartupPacket(port, GSSok == 'G', true); + gss_done = true; + if (GSSok == 'G') + { + /* + * We are done with GSS and negotiated correctly, so consider the + * same for SSL. + */ + ssl_done = true; + } + goto retry; } /* Could add additional special packet types here */ @@ -821,6 +846,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 +869,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 +889,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 +916,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 +924,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 +1112,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..bdc3dad335713 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 @@ -165,8 +165,10 @@ CreateDestReceiver(CommandDest dest) * EndCommand - clean up the destination at end of command * ---------------- */ + void -EndCommand(const QueryCompletion *qc, CommandDest dest, bool force_undecorated_output) +EndCommandExtended(const QueryCompletion *qc, CommandDest dest, + bool force_undecorated_output, bool noblock) { char completionTag[COMPLETION_TAG_BUFSIZE]; Size len; @@ -179,7 +181,11 @@ EndCommand(const QueryCompletion *qc, CommandDest dest, bool force_undecorated_o len = BuildQueryCompletionString(completionTag, qc, force_undecorated_output); - pq_putmessage(PqMsg_CommandComplete, completionTag, len + 1); + if (noblock) + pq_putmessage_noblock(PqMsg_CommandComplete, completionTag, len + 1); + else + pq_putmessage(PqMsg_CommandComplete, completionTag, len + 1); + break; case DestNone: case DestDebug: @@ -195,6 +201,12 @@ EndCommand(const QueryCompletion *qc, CommandDest dest, bool force_undecorated_o } } +void +EndCommand(const QueryCompletion *qc, CommandDest dest, bool force_undecorated_output) +{ + EndCommandExtended(qc, dest, force_undecorated_output, false); +} + /* ---------------- * EndReplicationCommand - stripped down version of EndCommand * 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..dbef734a93f15 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,15 @@ 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 in the server log 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_log("Signal sent by PID %d, UID %d.", (int) (pid), (int) (uid))) + /* ---------------- * private typedefs etc * ---------------- @@ -154,10 +169,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 +185,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 +192,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 +662,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 +897,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 +914,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 +1005,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 +1129,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 +1153,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 +1509,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 +1698,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 +1760,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 +2049,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 +2189,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 +2200,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 +2264,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 +2544,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 +2686,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 +2762,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 +3028,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 +3091,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 +3106,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 +3180,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 +3310,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 +3319,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 +3368,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 +3386,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 +3408,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 +3542,7 @@ ProcessInterrupts(void) } } - if (RecoveryConflictPending) + if (pg_atomic_read_u32(&MyProc->pendingRecoveryConflicts) != 0) ProcessRecoveryConflictInterrupts(); if (IdleInTransactionSessionTimeoutPending) @@ -3535,6 +3611,12 @@ ProcessInterrupts(void) if (ParallelApplyMessagePending) ProcessParallelApplyMessages(); + + if (SlotSyncShutdownPending) + ProcessSlotSyncMessage(); + + if (RepackMessagePending) + ProcessRepackMessages(); } /* @@ -3696,7 +3778,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 +3837,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 +3879,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 +3897,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 +3937,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 +3970,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 +3991,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 +4009,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 +4017,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 +4033,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 +4056,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 +4075,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 +4094,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 +4178,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 +4199,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 +4328,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 +4445,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 +5071,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..480915030df71 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; @@ -2627,6 +2583,9 @@ prsd_headline(PG_FUNCTION_ARGS) int max_fragments = 0; bool highlightall = false; ListCell *l; + size_t startsellen; + size_t stopsellen; + size_t fragdelimlen; /* Extract configuration option values */ prs->startsel = NULL; @@ -2716,9 +2675,24 @@ prsd_headline(PG_FUNCTION_ARGS) prs->fragdelim = pstrdup(" ... "); /* Caller will need these lengths, too */ - prs->startsellen = strlen(prs->startsel); - prs->stopsellen = strlen(prs->stopsel); - prs->fragdelimlen = strlen(prs->fragdelim); + startsellen = strlen(prs->startsel); + stopsellen = strlen(prs->stopsel); + fragdelimlen = strlen(prs->fragdelim); + if (startsellen > PG_INT16_MAX) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("value for \"%s\" is too long", "StartSel"))); + if (stopsellen > PG_INT16_MAX) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("value for \"%s\" is too long", "StopSel"))); + if (fragdelimlen > PG_INT16_MAX) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("value for \"%s\" is too long", "FragmentDelimiter"))); + prs->startsellen = startsellen; + prs->stopsellen = stopsellen; + prs->fragdelimlen = fragdelimlen; PG_RETURN_POINTER(prs); } 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..ea9f703c08874 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 @@ -37,11 +37,18 @@ static PgStat_SubXactStatus *pgStatXactStack = NULL; * Called from access/transam/xact.c at top-level transaction commit/abort. */ void -AtEOXact_PgStat(bool isCommit, bool parallel) +AtEOXact_PgStat(bool isCommit, bool parallel, bool count_xact_stats) { PgStat_SubXactStatus *xact_state; - AtEOXact_PgStat_Database(isCommit, parallel); + /* + * Only the database-level xact_commit/xact_rollback counter is gated + * here. Per-relation and subxact stat handling below must still run + * unconditionally so any stats accumulated during the transaction are + * not lost. + */ + if (count_xact_stats) + AtEOXact_PgStat_Database(isCommit, parallel); /* handle transactional stats information */ xact_state = pgStatXactStack; 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..13eed22dd6ac8 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 *) @@ -5561,6 +5576,7 @@ accumArrayResultArr(ArrayBuildStateArr *astate, ndatabytes; char *data; int i; + int newnitems; /* * We disallow accumulating null subarrays. Another plausible definition @@ -5590,6 +5606,14 @@ accumArrayResultArr(ArrayBuildStateArr *astate, nitems = ArrayGetNItems(ndims, dims); ndatabytes = ARR_SIZE(arg) - ARR_DATA_OFFSET(arg); + /* Check that the array doesn't grow too large */ + newnitems = astate->nitems + nitems; + if (newnitems > MaxArraySize) + ereport(ERROR, + (errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED), + errmsg("array size exceeds the maximum allowed (%zu)", + MaxArraySize))); + if (astate->ndims == 0) { /* First input; check/save the dimensionality info */ @@ -5655,8 +5679,6 @@ accumArrayResultArr(ArrayBuildStateArr *astate, /* Deal with null bitmap if needed */ if (astate->nullbitmap || ARR_HASNULL(arg)) { - int newnitems = astate->nitems + nitems; - if (astate->nullbitmap == NULL) { /* @@ -5664,7 +5686,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 +5694,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, @@ -5680,13 +5702,13 @@ accumArrayResultArr(ArrayBuildStateArr *astate, nitems); } - astate->nitems += nitems; + astate->nitems = newnitems; astate->dims[0] += 1; MemoryContextSwitchTo(oldcontext); /* Release detoasted copy if any */ - if ((Pointer) arg != DatumGetPointer(dvalue)) + if (arg != DatumGetPointer(dvalue)) pfree(arg); return astate; @@ -5943,7 +5965,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 +6117,7 @@ array_fill_internal(ArrayType *dims, ArrayType *lbs, int16 elmlen; bool elmbyval; char elmalign; + uint8 elmalignby; ArrayMetaState *my_extra; /* @@ -6189,6 +6212,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 +6227,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 +6237,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 +6251,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 +6282,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 +6292,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 +6314,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 +6348,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 +6424,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 +6469,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 +6531,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 +6578,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 +6885,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 +6909,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 +6934,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..a1f5d4e572ba2 --- /dev/null +++ b/src/backend/utils/adt/bytea.c @@ -0,0 +1,1367 @@ +/*------------------------------------------------------------------------- + * + * 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 +{ + 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->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..d6f55c48f37db --- /dev/null +++ b/src/backend/utils/adt/ddlutils.c @@ -0,0 +1,1187 @@ +/*------------------------------------------------------------------------- + * + * 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 (spcname == NULL) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", + dbform->dattablespace), + errdetail("It may have been concurrently dropped."))); + + 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..79cef8facfd06 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; /* @@ -3943,7 +3904,7 @@ datetime_to_char_body(TmToChar *tmtc, text *fmt, bool is_interval, Oid collid) /* * Allocate workspace for result as C string */ - result = palloc((fmt_len * DCH_MAX_ITEM_SIZ) + 1); + result = palloc(mul_size(fmt_len, DCH_MAX_ITEM_SIZ) + 1); *result = '\0'; if (fmt_len > DCH_CACHE_SIZE) @@ -3954,7 +3915,7 @@ datetime_to_char_body(TmToChar *tmtc, text *fmt, bool is_interval, Oid collid) */ incache = false; - format = (FormatNode *) palloc((fmt_len + 1) * sizeof(FormatNode)); + format = palloc_array(FormatNode, fmt_len + 1); parse_format(format, fmt_str, DCH_keywords, DCH_suff, DCH_index, DCH_FLAG, NULL); @@ -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; @@ -4377,7 +4330,7 @@ datetime_format_has_tz(const char *fmt_str) */ incache = false; - format = (FormatNode *) palloc((fmt_len + 1) * sizeof(FormatNode)); + format = palloc_array(FormatNode, fmt_len + 1); parse_format(format, fmt_str, DCH_keywords, DCH_suff, DCH_index, DCH_FLAG, NULL); @@ -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; @@ -4465,7 +4417,7 @@ do_to_timestamp(text *date_txt, text *fmt, Oid collid, bool std, * Allocate new memory if format picture is bigger than static * cache and do not use cache (call parser always) */ - format = (FormatNode *) palloc((fmt_len + 1) * sizeof(FormatNode)); + format = palloc_array(FormatNode, fmt_len + 1); parse_format(format, fmt_str, DCH_keywords, DCH_suff, DCH_index, DCH_FLAG | (std ? STD_FLAG : 0), NULL); @@ -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; @@ -5013,11 +4953,11 @@ NUM_cache(int len, NUMDesc *Num, text *pars_str, bool *shouldFree) * Allocate new memory if format picture is bigger than static cache * and do not use cache (call parser always) */ - format = (FormatNode *) palloc((len + 1) * sizeof(FormatNode)); + format = palloc_array(FormatNode, len + 1); *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..dc58e9cb0a6de 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(); @@ -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..c62a5e755ea6b 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; @@ -340,7 +340,7 @@ multirange_recv(PG_FUNCTION_ARGS) Oid mltrngtypoid = PG_GETARG_OID(1); int32 typmod = PG_GETARG_INT32(2); MultirangeIOData *cache; - uint32 range_count; + int32 range_count; RangeType **ranges; MultirangeType *ret; StringInfoData tmpbuf; @@ -348,7 +348,8 @@ 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 *)); + /* palloc_array will enforce a more-or-less-sane range_count value */ + ranges = palloc_array(RangeType *, range_count); initStringInfo(&tmpbuf); for (int i = 0; i < range_count; i++) @@ -378,31 +379,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 +486,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 +574,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 +605,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 +631,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 +703,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 +714,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 +723,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 +753,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 +774,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 +782,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 +840,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 +1231,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 +2158,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 +2242,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 +2599,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 +2620,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 +2715,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 +2723,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 +2731,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 +2739,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 +2822,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 +2909,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..7441004909f90 --- /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..02948e7023a27 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); @@ -95,8 +111,8 @@ static size_t strnxfrm_prefix_icu_utf8(char *dest, size_t destsize, const char *src, ssize_t srclen, pg_locale_t locale); static void init_icu_converter(void); -static size_t uchar_length(UConverter *converter, - const char *str, int32_t len); +static int32_t uchar_length(UConverter *converter, + const char *str, int32_t len); static int32_t uchar_convert(UConverter *converter, UChar *dest, int32_t destlen, const char *src, int32_t srclen); @@ -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. * @@ -349,7 +571,7 @@ make_icu_collator(const char *iculocstr, const char *icurules) total = u_strlen(std_rules) + u_strlen(my_rules) + 1; /* avoid leaking collator on OOM */ - all_rules = palloc_extended(sizeof(UChar) * total, MCXT_ALLOC_NO_OOM); + all_rules = palloc_array_extended(UChar, total, MCXT_ALLOC_NO_OOM); if (!all_rules) { ucol_close(collator_std_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); @@ -496,29 +755,21 @@ size_t strnxfrm_icu(char *dest, size_t destsize, const char *src, ssize_t srclen, pg_locale_t locale) { - char sbuf[TEXTBUFLEN]; - char *buf = sbuf; - UChar *uchar; + UChar sbuf[TEXTBUFLEN / sizeof(UChar)]; + UChar *uchar = sbuf; int32_t ulen; - size_t uchar_bsize; Size result_bsize; - Assert(locale->provider == COLLPROVIDER_ICU); - init_icu_converter(); ulen = uchar_length(icu_converter, src, srclen); - uchar_bsize = (ulen + 1) * sizeof(UChar); - - if (uchar_bsize > TEXTBUFLEN) - buf = palloc(uchar_bsize); - - uchar = (UChar *) buf; + if (ulen >= lengthof(sbuf)) + uchar = palloc_array(UChar, ulen + 1); 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); @@ -529,8 +780,8 @@ strnxfrm_icu(char *dest, size_t destsize, const char *src, ssize_t srclen, Assert(result_bsize > 0); result_bsize--; - if (buf != sbuf) - pfree(buf); + if (uchar != sbuf) + pfree(uchar); /* if dest is defined, it should be nul-terminated */ Assert(result_bsize >= destsize || dest[result_bsize] == '\0'); @@ -549,14 +800,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, @@ -607,7 +856,7 @@ icu_to_uchar(UChar **buff_uchar, const char *buff, size_t nbytes) len_uchar = uchar_length(icu_converter, buff, nbytes); - *buff_uchar = palloc((len_uchar + 1) * sizeof(**buff_uchar)); + *buff_uchar = palloc_array(UChar, len_uchar + 1); len_uchar = uchar_convert(icu_converter, *buff_uchar, len_uchar + 1, buff, nbytes); @@ -657,25 +906,25 @@ 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; len_dest = len_source; /* try first with same length */ - *buff_dest = palloc(len_dest * sizeof(**buff_dest)); + *buff_dest = palloc_array(UChar, len_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 */ pfree(*buff_dest); - *buff_dest = palloc(len_dest * sizeof(**buff_dest)); + *buff_dest = palloc_array(UChar, len_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 +932,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 +967,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 +997,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; } /* @@ -739,18 +1014,15 @@ static int strncoll_icu(const char *arg1, ssize_t len1, const char *arg2, ssize_t len2, pg_locale_t locale) { - char sbuf[TEXTBUFLEN]; - char *buf = sbuf; + UChar sbuf[TEXTBUFLEN / sizeof(UChar)]; + UChar *buf = sbuf; int32_t ulen1; int32_t ulen2; - size_t bufsize1; - size_t bufsize2; + size_t bufsize; UChar *uchar1, *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); @@ -761,19 +1033,18 @@ strncoll_icu(const char *arg1, ssize_t len1, ulen1 = uchar_length(icu_converter, arg1, len1); ulen2 = uchar_length(icu_converter, arg2, len2); - bufsize1 = (ulen1 + 1) * sizeof(UChar); - bufsize2 = (ulen2 + 1) * sizeof(UChar); + /* ulen1+1 or ulen2+1 doesn't risk overflow, but summing them might */ + bufsize = add_size(ulen1 + 1, ulen2 + 1); + if (bufsize > lengthof(sbuf)) + buf = palloc_array(UChar, bufsize); - if (bufsize1 + bufsize2 > TEXTBUFLEN) - buf = palloc(bufsize1 + bufsize2); - - uchar1 = (UChar *) buf; - uchar2 = (UChar *) (buf + bufsize1); + uchar1 = buf; + uchar2 = buf + ulen1 + 1; 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); @@ -789,18 +1060,14 @@ strnxfrm_prefix_icu(char *dest, size_t destsize, const char *src, ssize_t srclen, pg_locale_t locale) { - char sbuf[TEXTBUFLEN]; - char *buf = sbuf; + UChar sbuf[TEXTBUFLEN / sizeof(UChar)]; + UChar *uchar = sbuf; UCharIterator iter; uint32_t state[2]; UErrorCode status; - int32_t ulen = -1; - UChar *uchar = NULL; - size_t uchar_bsize; + int32_t ulen; Size result_bsize; - Assert(locale->provider == COLLPROVIDER_ICU); - /* if encoding is UTF8, use more efficient strnxfrm_prefix_icu_utf8 */ Assert(GetDatabaseEncoding() != PG_UTF8); @@ -808,19 +1075,15 @@ strnxfrm_prefix_icu(char *dest, size_t destsize, ulen = uchar_length(icu_converter, src, srclen); - uchar_bsize = (ulen + 1) * sizeof(UChar); - - if (uchar_bsize > TEXTBUFLEN) - buf = palloc(uchar_bsize); - - uchar = (UChar *) buf; + if (ulen >= lengthof(sbuf)) + uchar = palloc_array(UChar, ulen + 1); ulen = uchar_convert(icu_converter, uchar, ulen + 1, src, srclen); 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 +1094,9 @@ strnxfrm_prefix_icu(char *dest, size_t destsize, (errmsg("sort key generation failed: %s", u_errorName(status)))); + if (uchar != sbuf) + pfree(uchar); + return result_bsize; } @@ -865,8 +1131,12 @@ init_icu_converter(void) * Find length, in UChars, of given string if converted to UChar string. * * A length of -1 indicates that the input string is NUL-terminated. + * + * Note: given the assumption that the input string fits in MaxAllocSize, + * the result cannot overflow int32_t. But callers must be careful about + * multiplying the result by sizeof(UChar). */ -static size_t +static int32_t uchar_length(UConverter *converter, const char *str, int32_t len) { UErrorCode status = U_ZERO_ERROR; @@ -892,7 +1162,6 @@ uchar_convert(UConverter *converter, UChar *dest, int32_t destlen, UErrorCode status = U_ZERO_ERROR; int32_t ulen; - status = U_ZERO_ERROR; ulen = ucnv_toUChars(converter, dest, destlen, src, srclen, &status); if (U_FAILURE(status)) ereport(ERROR, diff --git a/src/backend/utils/adt/pg_locale_libc.c b/src/backend/utils/adt/pg_locale_libc.c index 199857e22dbec..7b725e93d9df5 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); - - 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); @@ -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); @@ -737,12 +1061,12 @@ strncoll_libc_win32_utf8(const char *arg1, ssize_t len1, const char *arg2, char *buf = sbuf; char *a1p, *a2p; - int a1len; - int a2len; + size_t a1len, + a2len, + buflen; int r; int result; - Assert(locale->provider == COLLPROVIDER_LIBC); Assert(GetDatabaseEncoding() == PG_UTF8); if (len1 == -1) @@ -750,11 +1074,16 @@ strncoll_libc_win32_utf8(const char *arg1, ssize_t len1, const char *arg2, if (len2 == -1) len2 = strlen(arg2); - a1len = len1 * 2 + 2; - a2len = len2 * 2 + 2; + /* + * In a 32-bit build, twice the input length can overflow size_t, so we + * must be careful. + */ + a1len = add_size(add_size(len1, len1), 2); + a2len = add_size(add_size(len2, len2), 2); + buflen = add_size(a1len, a2len); - if (a1len + a2len > TEXTBUFLEN) - buf = palloc(a1len + a2len); + if (buflen > TEXTBUFLEN) + buf = palloc(buflen); a1p = buf; a2p = buf + a1len; @@ -787,7 +1116,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 +1196,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 +1208,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 +1238,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 +1246,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 +1261,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 +1298,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 +1306,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..7a9dfa9ba3b99 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) @@ -1907,13 +1987,13 @@ pg_stat_reset_shared(PG_FUNCTION_ARGS) ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("unrecognized reset target: \"%s\"", target), - errhint("Target must be \"archiver\", \"bgwriter\", \"checkpointer\", \"io\", \"recovery_prefetch\", \"slru\", or \"wal\"."))); + errhint("Target must be \"archiver\", \"bgwriter\", \"checkpointer\", \"io\", \"lock\", \"recovery_prefetch\", \"slru\", or \"wal\"."))); PG_RETURN_VOID(); } /* - * 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..dc89c6863946b 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 delete"))); + 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..88de5c0481c85 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) { @@ -11688,6 +12281,21 @@ get_json_constructor(JsonConstructorExpr *ctor, deparse_context *context, get_json_agg_constructor(ctor, context, "JSON_ARRAYAGG", false); return; } + else if (ctor->type == JSCTOR_JSON_ARRAY_QUERY) + { + Query *query = castNode(Query, ctor->orig_query); + + appendStringInfo(buf, "JSON_ARRAY("); + + get_query_def(query, buf, context->namespaces, NULL, false, + context->prettyFlags, context->wrapColumn, + context->indentLevel); + + get_json_constructor_options(ctor, buf); + appendStringInfoChar(buf, ')'); + + return; + } switch (ctor->type) { @@ -11795,16 +12403,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 +13099,29 @@ 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 ("); + { + bool first = true; + + foreach_node(TargetEntry, te, rte->graph_table_columns) + { + if (!first) + appendStringInfoString(buf, ", "); + else + first = false; + + 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 +13346,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 * @@ -13214,23 +13876,15 @@ generate_qualified_relation_name(Oid relid) { HeapTuple tp; Form_pg_class reltup; - char *relname; - char *nspname; char *result; tp = SearchSysCache1(RELOID, ObjectIdGetDatum(relid)); if (!HeapTupleIsValid(tp)) elog(ERROR, "cache lookup failed for relation %u", relid); reltup = (Form_pg_class) GETSTRUCT(tp); - relname = NameStr(reltup->relname); - - nspname = get_namespace_name_or_temp(reltup->relnamespace); - if (!nspname) - elog(ERROR, "cache lookup failed for namespace %u", - reltup->relnamespace); - - result = quote_qualified_identifier(nspname, relname); + result = get_qualified_objname(reltup->relnamespace, + NameStr(reltup->relname)); ReleaseSysCache(tp); return result; @@ -13265,6 +13919,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 +13978,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 +14240,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 +14332,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 +14360,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..88ba4e0674569 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) || @@ -1691,15 +1685,19 @@ Datum timeofday(PG_FUNCTION_ARGS) { struct timeval tp; - char templ[128]; - char buf[128]; pg_time_t tt; + struct pg_tm *tm; + char part1[128]; + char part2[128]; + char buf[128 + 128 + 10]; gettimeofday(&tp, NULL); tt = (pg_time_t) tp.tv_sec; - pg_strftime(templ, sizeof(templ), "%a %b %d %H:%M:%S.%%06d %Y %Z", - pg_localtime(&tt, session_timezone)); - snprintf(buf, sizeof(buf), templ, tp.tv_usec); + tm = pg_localtime(&tt, session_timezone); + + pg_strftime(part1, sizeof(part1), "%a %b %d %H:%M:%S", tm); + pg_strftime(part2, sizeof(part2), "%Y %Z", tm); + snprintf(buf, sizeof(buf), "%s.%06d %s", part1, (int) tp.tv_usec, part2); PG_RETURN_TEXT_P(cstring_to_text(buf)); } @@ -2275,33 +2273,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 +2361,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 = timestamp2timestamptz_safe(timestampVal, (Node *) &escontext); + if (escontext.error_occurred) { - /* dt1 is larger than any finite timestamp, but less than infinity */ - return TIMESTAMP_IS_NOEND(dt2) ? -1 : +1; - } - if (overflow < 0) - { - /* 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 +2828,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 +2923,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 +3002,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 +3044,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 +3446,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 +3504,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 +3560,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 +3614,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 +3744,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 +3973,7 @@ makeIntervalAggState(FunctionCallInfo fcinfo) old_context = MemoryContextSwitchTo(agg_context); - state = (IntervalAggState *) palloc0(sizeof(IntervalAggState)); + state = palloc0_object(IntervalAggState); MemoryContextSwitchTo(old_context); @@ -4180,7 +4160,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 +4227,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 +4264,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 +4297,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 +4445,7 @@ timestamptz_age(PG_FUNCTION_ARGS) int tz1; int tz2; - result = (Interval *) palloc(sizeof(Interval)); + result = palloc_object(Interval); /* * Handle infinities. @@ -4762,14 +4742,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 +4759,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 +4934,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 +4988,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 +5009,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 +5118,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 +5159,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 +5169,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 +5292,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 +5318,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 +5332,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 +5347,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 +5375,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 +5630,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 +5686,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 +5904,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 +5957,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 +6248,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 +6428,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 +6456,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 +6467,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 +6486,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 +6496,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 +6538,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 +6695,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 +6779,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..3012ad437af72 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; @@ -313,6 +313,7 @@ calc_rank_or(const float *w, TSVector t, TSQuery q) while (entry - firstentry < nitem) { + /* Identify the weight data to use */ if (entry->haspos) { dimt = POSDATALEN(t, entry); @@ -324,6 +325,33 @@ calc_rank_or(const float *w, TSVector t, TSQuery q) post = posnull.pos; } + /* + * Compute the score for this term, then add it to "res". + * + * The ideal score for a term is the weighted harmonic sum: + * + * resj = sum(wi / i^2, i = 1..noccurrences) + * + * where wi is the weight of the i-th occurrence and weights are + * sorted in descending order so that the highest-weight + * occurrence gets the smallest divisor (i=1) and thus contributes + * the most. + * + * This result should be divided by pi^2/6 ~= 1.64493406685, which + * is the limit of sum(1/i^2, i=1..inf). This normalizes the score + * to the [0, 1] range. + * + * As an approximation for efficiency, we skip doing a sort and + * instead just promote the single highest-weight occurrence to + * position i=1. This is done by taking the raw (unsorted) sum + * resj, subtracting the maximum weight's actual contribution + * wjm/(jm+1)^2, and adding back its corrected contribution + * wjm/1^2 = wjm. + * + * The remaining occurrences are left in their original order. + */ + + /* calculate harmonic sum without re-ordering */ resj = 0.0; wjm = -1.0; jm = 0; @@ -336,14 +364,9 @@ calc_rank_or(const float *w, TSVector t, TSQuery q) jm = j; } } -/* - limit (sum(1/i^2),i=1,inf) = pi^2/6 - resj = sum(wi/i^2),i=1,noccurrence, - wi - should be sorted desc, - don't sort for now, just choose maximum weight. This should be corrected - Oleg Bartunov -*/ - res = res + (wjm + resj - wjm / ((jm + 1) * (jm + 1))) / 1.64493406685; + + /* apply correction as explained above, then add to res */ + res = res + (resj - wjm / ((jm + 1) * (jm + 1)) + wjm) / 1.64493406685; entry++; } @@ -743,7 +766,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 +838,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 +855,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 +901,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..e86e6bc48397e 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. */ @@ -3485,19 +2737,109 @@ textToQualifiedNameList(text *textval) (errcode(ERRCODE_INVALID_NAME), errmsg("invalid name syntax"))); - foreach(l, namelist) + 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 */ + + *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) @@ -6548,25 +5490,25 @@ unicode_normalize_func(PG_FUNCTION_ARGS) text *input = PG_GETARG_TEXT_PP(0); char *formstr = text_to_cstring(PG_GETARG_TEXT_PP(1)); UnicodeNormalizationForm form; - int size; - pg_wchar *input_chars; - pg_wchar *output_chars; + size_t size; + char32_t *input_chars; + char32_t *output_chars; unsigned char *p; text *result; - int i; + size_t 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_array(char32_t, size + 1); 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); @@ -6614,27 +5556,27 @@ unicode_is_normalized(PG_FUNCTION_ARGS) text *input = PG_GETARG_TEXT_PP(0); char *formstr = text_to_cstring(PG_GETARG_TEXT_PP(1)); UnicodeNormalizationForm form; - int size; - pg_wchar *input_chars; - pg_wchar *output_chars; + size_t size; + char32_t *input_chars; + char32_t *output_chars; unsigned char *p; - int i; + size_t i; UnicodeNormalizationQC quickcheck; - int output_size; + size_t output_size; bool result; 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_array(char32_t, size + 1); 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..036086057d784 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,90 @@ 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; +} + +/* + * collations_agree_on_equality + * Return true if the two collations have equivalent notions of equality, + * so that a uniqueness or equality proof established under one side + * carries over to a comparison performed under the other side. + * + * Note: this is equality compatibility only. Do NOT use this to reason + * about ordering. + * + * An InvalidOid on either side denotes the absence of a collation -- that + * side's operation is not collation-sensitive (e.g. a non-collatable column + * type). Absence of a collation cannot conflict with the other side's + * collation, so we treat such pairs as agreeing on equality. This generalizes + * the asymmetric treatment in IndexCollMatchesExprColl(). + * + * Otherwise the collations have equivalent equality if they match, or if both + * are deterministic: by definition a deterministic collation treats two + * strings as equal iff they are byte-wise equal (see CREATE COLLATION), so any + * two deterministic collations share the same equality relation. A mismatch + * involving a nondeterministic collation, however, may mean the two equality + * relations disagree, and the proof is unsound. + */ +bool +collations_agree_on_equality(Oid coll1, Oid coll2) +{ + if (!OidIsValid(coll1) || !OidIsValid(coll2)) + return true; + + if (coll1 == coll2) + return true; + + if (!get_collation_isdeterministic(coll1) || + !get_collation_isdeterministic(coll2)) + return false; + + return true; +} + +/* + * 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 +1312,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 +2573,7 @@ get_type_io_data(Oid typid, { Oid typinput; Oid typoutput; + Oid typcollation; boot_get_type_io_data(typid, typlen, @@ -2490,7 +2582,8 @@ get_type_io_data(Oid typid, typdelim, typioparam, &typinput, - &typoutput); + &typoutput, + &typcollation); switch (which_func) { case IOFunc_input: @@ -3535,6 +3628,26 @@ get_namespace_name_or_temp(Oid nspid) return get_namespace_name(nspid); } +/* + * get_qualified_objname + * Returns a palloc'd string containing the schema-qualified name of the + * object for the given namespace ID and object name. + */ +char * +get_qualified_objname(Oid nspid, char *objname) +{ + char *nspname; + char *result; + + nspname = get_namespace_name_or_temp(nspid); + if (!nspname) + elog(ERROR, "cache lookup failed for namespace %u", nspid); + + result = quote_qualified_identifier(nspname, objname); + + return result; +} + /* ---------- PG_RANGE CACHES ---------- */ /* @@ -3588,6 +3701,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 +3955,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 +3992,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..0572ab424e71e 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; @@ -2145,6 +2135,16 @@ RelationIdGetRelation(Oid relationId) return rd; } +/* + * Returns a schema-qualified name of the relation. + */ +char * +RelationGetQualifiedRelationName(Relation rel) +{ + return get_qualified_objname(RelationGetNamespace(rel), + RelationGetRelationName(rel)); +} + /* ---------------------------------------------------------------- * cache invalidation support routines * ---------------------------------------------------------------- @@ -2896,7 +2896,7 @@ RelationForgetRelation(Oid rid) RelationIdCacheLookup(rid, relation); - if (!PointerIsValid(relation)) + if (!relation) return; /* not in cache, nothing to do */ if (!RelationHasReferenceCountZero(relation)) @@ -2941,7 +2941,7 @@ RelationCacheInvalidateEntry(Oid relationId) RelationIdCacheLookup(relationId, relation); - if (PointerIsValid(relation)) + if (relation) { relcacheInvalsReceived++; RelationFlushRelation(relation); @@ -3184,7 +3184,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 +3579,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 +3627,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 +3693,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 +4448,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 +4659,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 +4671,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 +5644,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 +5671,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 +5764,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 +5794,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 +5834,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 +5983,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 +5991,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 +6023,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 +6297,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 +6318,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 +7017,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..2460e550f96e2 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, @@ -711,7 +715,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(); @@ -749,8 +753,36 @@ InitPostgres(const char *in_dbname, Oid dboid, */ SharedInvalBackendInit(false); + /* + * Prevent consuming interrupts between setting ProcSignalInit and setting + * the initial local data checksum value. If a barrier is emitted, and + * absorbed, before local cached state is initialized the state transition + * can be invalid. + */ + HOLD_INTERRUPTS(); + 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(); + + RESUME_INTERRUPTS(); + /* * Also set up timeout handlers needed for backend operation. We need * these in every case except bootstrap. @@ -798,6 +830,16 @@ InitPostgres(const char *in_dbname, Oid dboid, before_shmem_exit(ShutdownXLOG, 0); } + /* + * Initialize the process-local logical info WAL logging state. + * + * This must be called after ProcSignalInit() so that the process can + * participate in procsignal-based barriers that update this state. + * Furthermore, in !IsUnderPostmaster cases, this must occur after + * StartupXLOG() where the shared state is first established. + */ + InitializeProcessXLogLogicalInfo(); + /* * Initialize the relation cache and the system catalog caches. Note that * no catalog access happens here; we only set up the hashtable structure. @@ -818,9 +860,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 +921,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 +1031,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 +1279,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 +1313,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 +1496,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..afaa058b046c9 --- /dev/null +++ b/src/backend/utils/misc/guc_parameters.dat @@ -0,0 +1,3657 @@ +#---------------------------------------------------------------------- +# +# 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', + show_hook => 'show_data_checksums', +}, + +# 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..930fc45732811 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 * * @@ -21,6 +38,7 @@ #include "postgres.h" +#include "common/int.h" #include "mb/pg_wchar.h" #include "miscadmin.h" #include "utils/memdebug.h" @@ -157,6 +175,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, @@ -166,6 +187,8 @@ static void MemoryContextStatsInternal(MemoryContext context, int level, static void MemoryContextStatsPrint(MemoryContext context, void *passthru, const char *stats_string, bool print_to_stderr); +pg_noreturn static pg_noinline void add_size_error(Size s1, Size s2); +pg_noreturn static pg_noinline void mul_size_error(Size s1, Size s2); /* * You should not do memory allocations within a critical section, because @@ -418,8 +441,6 @@ MemoryContextResetOnly(MemoryContext context) context->methods->reset(context); context->isReset = true; - VALGRIND_DESTROY_MEMPOOL(context); - VALGRIND_CREATE_MEMPOOL(context, 0, false); } } @@ -526,8 +547,6 @@ MemoryContextDeleteOnly(MemoryContext context) context->ident = NULL; context->methods->delete_context(context); - - VALGRIND_DESTROY_MEMPOOL(context); } /* @@ -560,9 +579,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 +594,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 +1189,6 @@ MemoryContextCreate(MemoryContext node, node->nextchild = NULL; node->allowInCritSection = false; } - - VALGRIND_CREATE_MEMPOOL(node, 0, false); } /* @@ -1295,26 +1345,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 +1490,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 +1533,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 +1569,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 +1619,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 +1634,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 +1656,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; } @@ -1637,6 +1718,132 @@ repalloc0(void *pointer, Size oldsize, Size size) return ret; } +/* + * Support for safe calculation of memory request sizes + * + * These functions perform the requested calculation, but throw error if the + * result overflows. + * + * An important property of these functions is that if an argument was a + * negative signed int before promotion (implying overflow in calculating it) + * we will detect that as an error. That happens because we reject results + * larger than SIZE_MAX / 2 later on, in the actual allocation step. + */ +Size +add_size(Size s1, Size s2) +{ + Size result; + + if (unlikely(pg_add_size_overflow(s1, s2, &result))) + add_size_error(s1, s2); + return result; +} + +pg_noreturn static pg_noinline void +add_size_error(Size s1, Size s2) +{ + ereport(ERROR, + (errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED), + errmsg("invalid memory allocation request size %zu + %zu", + s1, s2))); +} + +Size +mul_size(Size s1, Size s2) +{ + Size result; + + if (unlikely(pg_mul_size_overflow(s1, s2, &result))) + mul_size_error(s1, s2); + return result; +} + +pg_noreturn static pg_noinline void +mul_size_error(Size s1, Size s2) +{ + ereport(ERROR, + (errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED), + errmsg("invalid memory allocation request size %zu * %zu", + s1, s2))); +} + +/* + * palloc_mul + * Equivalent to palloc(mul_size(s1, s2)). + */ +void * +palloc_mul(Size s1, Size s2) +{ + /* inline mul_size() for efficiency */ + Size req; + + if (unlikely(pg_mul_size_overflow(s1, s2, &req))) + mul_size_error(s1, s2); + return palloc(req); +} + +/* + * palloc0_mul + * Equivalent to palloc0(mul_size(s1, s2)). + * + * This is comparable to standard calloc's behavior. + */ +void * +palloc0_mul(Size s1, Size s2) +{ + /* inline mul_size() for efficiency */ + Size req; + + if (unlikely(pg_mul_size_overflow(s1, s2, &req))) + mul_size_error(s1, s2); + return palloc0(req); +} + +/* + * palloc_mul_extended + * Equivalent to palloc_extended(mul_size(s1, s2), flags). + */ +void * +palloc_mul_extended(Size s1, Size s2, int flags) +{ + /* inline mul_size() for efficiency */ + Size req; + + if (unlikely(pg_mul_size_overflow(s1, s2, &req))) + mul_size_error(s1, s2); + return palloc_extended(req, flags); +} + +/* + * repalloc_mul + * Equivalent to repalloc(p, mul_size(s1, s2)). + */ +void * +repalloc_mul(void *p, Size s1, Size s2) +{ + /* inline mul_size() for efficiency */ + Size req; + + if (unlikely(pg_mul_size_overflow(s1, s2, &req))) + mul_size_error(s1, s2); + return repalloc(p, req); +} + +/* + * repalloc_mul_extended + * Equivalent to repalloc_extended(p, mul_size(s1, s2), flags). + */ +void * +repalloc_mul_extended(void *p, Size s1, Size s2, int flags) +{ + /* inline mul_size() for efficiency */ + Size req; + + if (unlikely(pg_mul_size_overflow(s1, s2, &req))) + mul_size_error(s1, s2); + return repalloc_extended(p, req, flags); +} + /* * MemoryContextAllocHuge * Allocate (possibly-expansive) space within the specified context. 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..1929521c6a56a 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 * ---------- 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..c0e7527b9ca36 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,459 @@ 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 + (Int64GetDatum(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 { - if (state->base.sortKeys[0].comparator == ssup_datum_unsigned_cmp) + 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; + + Assert(next_level > level); + + 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 (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 with the default order ASC NULLS + * LAST. + */ + 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 +3457,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 +3470,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/nls.mk b/src/bin/pg_basebackup/nls.mk index f75f44dbe643c..6290c5c364974 100644 --- a/src/bin/pg_basebackup/nls.mk +++ b/src/bin/pg_basebackup/nls.mk @@ -21,6 +21,7 @@ GETTEXT_FILES = $(FRONTEND_COMMON_GETTEXT_FILES) \ ../../fe_utils/astreamer_zstd.c \ ../../fe_utils/option_utils.c \ ../../fe_utils/recovery_gen.c \ - ../../fe_utils/string_utils.c + ../../fe_utils/string_utils.c \ + ../../fe_utils/version.c GETTEXT_TRIGGERS = $(FRONTEND_COMMON_GETTEXT_TRIGGERS) simple_prompt GETTEXT_FLAGS = $(FRONTEND_COMMON_GETTEXT_FLAGS) 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..81d3c78b805ac 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 might 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("using 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,28 +1253,36 @@ 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; + char *subname_esc; Assert(conn != NULL); + subname_esc = PQescapeIdentifier(conn, subname, strlen(subname)); + /* * Construct a query string. These commands are allowed to be executed * within a transaction. */ appendPQExpBuffer(query, "ALTER SUBSCRIPTION %s DISABLE;", - subname); + subname_esc); appendPQExpBuffer(query, " ALTER SUBSCRIPTION %s SET (slot_name = NONE);", - subname); - appendPQExpBuffer(query, " DROP SUBSCRIPTION %s;", subname); + subname_esc); + appendPQExpBuffer(query, " DROP SUBSCRIPTION %s;", subname_esc); - pg_log_info("dropping subscription \"%s\" in database \"%s\"", - subname, dbname); + PQfreemem(subname_esc); - 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 +1328,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 +1404,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 +1423,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 +1554,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 +1607,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 +1711,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 +1750,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 +1759,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 +1776,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 +1835,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 +1882,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 +1921,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 +1956,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("preserving existing publication \"%s\" in database \"%s\"", + dbinfo->pubname, dbinfo->dbname); + } + } } /* @@ -1794,8 +2004,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 +2090,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 +2106,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 +2156,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 +2245,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 +2260,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 +2304,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 +2333,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 +2354,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 +2367,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 +2413,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 +2442,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 +2493,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 +2598,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 option %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/nls.mk b/src/bin/pg_checksums/nls.mk index 9f1b5a8b4a2dc..c4a2346e8041a 100644 --- a/src/bin/pg_checksums/nls.mk +++ b/src/bin/pg_checksums/nls.mk @@ -5,6 +5,7 @@ GETTEXT_FILES = $(FRONTEND_COMMON_GETTEXT_FILES) \ ../../common/controldata_utils.c \ ../../common/fe_memutils.c \ ../../common/file_utils.c \ - ../../fe_utils/option_utils.c + ../../fe_utils/option_utils.c \ + ../../fe_utils/version.c GETTEXT_TRIGGERS = $(FRONTEND_COMMON_GETTEXT_TRIGGERS) GETTEXT_FLAGS = $(FRONTEND_COMMON_GETTEXT_FLAGS) 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/nls.mk b/src/bin/pg_combinebackup/nls.mk index 55dc173a17e24..ff56cee6aac74 100644 --- a/src/bin/pg_combinebackup/nls.mk +++ b/src/bin/pg_combinebackup/nls.mk @@ -14,7 +14,8 @@ GETTEXT_FILES = $(FRONTEND_COMMON_GETTEXT_FILES) \ ../../common/file_utils.c \ ../../common/jsonapi.c \ ../../common/parse_manifest.c \ - ../../fe_utils/option_utils.c + ../../fe_utils/option_utils.c \ + ../../fe_utils/version.c GETTEXT_TRIGGERS = $(FRONTEND_COMMON_GETTEXT_TRIGGERS) \ json_token_error:2 \ json_manifest_parse_failure:2 \ 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..c1f43113c533e 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 *excluded_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; - /* Restore will need to write to the target cluster */ - fprintf(OPF, "SET default_transaction_read_only = off;\n\n"); + /* Register the cleanup hook */ + on_exit_close_archive(fout); - /* 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"); + /* Let the archiver know how noisy to be */ + fout->verbose = verbose; - if (!data_only) + /* + * 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); + + /* + * 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. + */ + + /* 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 && !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); + + if (archDumpFormat == archNull) + { + PQfinish(conn); - 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) + { + fclose(OPF); - if (filename) + /* sync the resulting file, errors are not fatal */ + if (dosync) + (void) fsync_fname(filename, false); + } + } + else { - fclose(OPF); + RestoreOptions *ropt; - /* sync the resulting file, errors are not fatal */ - if (dosync && (archDumpFormat == archNull)) - (void) fsync_fname(filename, false); + ropt = NewRestoreOptions(); + SetArchiveOptions(fout, &dopt, ropt); + + /* 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); + + if (archDumpFormat == archNull) + { + fprintf(OPF, "%s", buf->data); - 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 *excluded_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(excluded_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 */ @@ -1957,27 +2406,36 @@ read_dumpall_filters(const char *filename, SimpleStringList *pattern) static ArchiveFormat parseDumpFormat(const char *format) { - ArchiveFormat archDumpFormat; - if (pg_strcasecmp(format, "c") == 0) - archDumpFormat = archCustom; + return archCustom; else if (pg_strcasecmp(format, "custom") == 0) - archDumpFormat = archCustom; + return archCustom; else if (pg_strcasecmp(format, "d") == 0) - archDumpFormat = archDirectory; + return archDirectory; else if (pg_strcasecmp(format, "directory") == 0) - archDumpFormat = archDirectory; + return archDirectory; else if (pg_strcasecmp(format, "p") == 0) - archDumpFormat = archNull; + return archNull; else if (pg_strcasecmp(format, "plain") == 0) - archDumpFormat = archNull; + return archNull; else if (pg_strcasecmp(format, "t") == 0) - archDumpFormat = archTar; + return archTar; else if (pg_strcasecmp(format, "tar") == 0) - archDumpFormat = archTar; + return 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; + return archUnknown; +} + +/* + * 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..48fdcb0fae17a 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); @@ -696,6 +788,7 @@ usage(const char *progname) printf(_(" --no-data do not restore data\n")); printf(_(" --no-data-for-failed-tables do not restore data of tables that could not be\n" " created\n")); + printf(_(" --no-globals do not restore global objects (roles and tablespaces)\n")); printf(_(" --no-policies do not restore row security policies\n")); printf(_(" --no-publications do not restore publications\n")); printf(_(" --no-schema do not restore schema\n")); @@ -704,7 +797,9 @@ usage(const char *progname) printf(_(" --no-subscriptions do not restore subscriptions\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", +