1
0
mirror of https://github.com/pgbackrest/pgbackrest.git synced 2026-10-23 21:28:56 +03:00
Files
pgbackrest/build/lib/postgres/render.py
David Steele cc9ff041f4 Represent the values that vary by version as interface data.
A value that varies by version was read inside a function, so two versions got a rendering of it that differed only by that value. pgInterfaceControlVersion() returned PG_CONTROL_VERSION, pgInterfaceWalIs() compared XLOG_PAGE_MAGIC, and pgInterfaceControlIs() compared both PG_CONTROL_VERSION and CATALOG_VERSION_NO, so none of them could ever be shared no matter how little of the version they actually depended on.

Include them in the interface struct instead. A version block captures each as a constant named for it, since a value only has a value inside the block and the struct is built after every block has undefined what it defined. A define cannot do this because it is expanded where it is used rather than where it is made, by which time the value it was given is gone. The constant also names the member it is assigned to, so adding a value is a change to the header and to nothing else.

Each value is captured by a macro of its own, so the analysis that decides which versions can share a function decides the same for a value. One that does not vary between two versions is captured once for both and named for the range it covers, e.g. pgInterfaceControlVersion13_16, which makes what a version shares readable where it is assigned rather than something to work out from the vendored header. The control version is the same from 13 through 16 and the crc offset for 15 and 16.

The functions that only compared a value go with them. Matching pg_control against an interface is now a comparison of the fields that are at the same offset in every version, which pgControlCommon and pgWalCommon already read to report a version that is not supported, and the WAL magic is compared the same way. The comparison is rendered by the generator in the form that fits the versions rendered, so the range that an unreleased version needs is only rendered while there is one and there is never a comparison that no supported version can fail.

Whether a version has been released is set from the declaration, which is where it is stated, rather than derived in the header.

The harness shims the interface so it writes with the types that interface declares rather than declaring them again, and takes the defaults a test does not ask for from the interface the code matches on rather than from a second copy of them.

This brings the binary interface to 22 rendered functions from 62 and the harness to 12 from 32.
2026-08-12 10:16:57 +07:00

334 lines
15 KiB
Python

"""Render PostgreSQL Interface.
Each version gets a block that includes the vendored header again with every name suffixed by the version, which is how one
translation unit carries an interface for every version it supports. The interfaces are rendered newest first so the most likely
match is found first at run time.
A block only renders the functions that no other version already renders the same code for, since a function whose types and
defines all resolve to the same branches for two versions would compile to the same thing twice. The struct then points both
versions at the one that was rendered, so what is shared is visible where it is used.
The values a version carries are macros of the vendored header, which have a value only inside its block, so the block captures
them as constants named for the version and the struct names those.
The interface being rendered is whichever one the declaration was parsed for, so the binary and the test harness are rendered by the
same code from the same versions."""
####################################################################################################################################
import os
from common.error import ToolError
from common.render import bld_comment_block, bld_define, bld_header
from common.storage import file_read, file_write_differs
from postgres.parse import bld_pg_version_num
_VERSION_MODULE = "postgres-version"
_VERSION_DESCRIPTION = "PostgreSQL Version"
# Base a test system id is offset from, which is the base hrnPgSystemId() adds the version to
_SYSTEM_ID_BASE = 10000000000000000000
# Offsets a test asks for a system id at, since a test that needs a second system id for a version varies it by a small amount
_SYSTEM_ID_OFFSET_LIST = (0, 1)
# Prefix of the defines that are generated into the harness header, which is what identifies them there
_SYSTEM_ID_DEFINE = "HRN_PG_SYSTEMID_"
# Header the system id defines are generated into, which is hand-written apart from them
_PATH_SYSTEM_ID = "test/src/harness/postgres.h"
# Column the comment naming the versions that share a rendering is aligned at, which is the column a #define value is aligned at
_SHARE_COLUMN = 68
# Member marking a version that has not been released, which the declaration says rather than the interface header
_UNRELEASED_MEMBER = "unreleased"
# Macro capturing the values of a version as constants named for it, which is rendered in the block since that is the only place
# they have a value
_VALUE_MACRO = "%s_VALUE"
# Macro matching a version against an interface, which is rendered once after the versions when the interface declares it. The
# range form is rendered when a version has not been released, since only then does an interface accept more than one value.
_MATCH_MACRO = "%s_MATCH"
_MATCH_MACRO_RANGE = "%s_MATCH_RANGE"
####################################################################################################################################
def _version_no_dot(version):
"""The version as it appears in a C name, e.g. "9.6" becomes "96"."""
return version.replace(".", "")
####################################################################################################################################
def _version_suffix(bld_pg, function, version):
"""The suffix the rendering of a function is named with, given the version that rendered it.
A rendering only one version uses is named with it, e.g. "14", and a rendering more than one shares is named with the range it
covers, e.g. "14_19", so which versions share it can be read from the name."""
index_list = [idx for idx, pg in enumerate(bld_pg.pg_list) if bld_pg.function_version[function][pg.version] == version]
if len(index_list) == 1:
return _version_no_dot(version)
# Versions sharing a rendering are consecutive, since a branch of the vendored header applies from where it begins until the
# next one begins. A range over versions that are not would name versions that do not share it.
if index_list[-1] - index_list[0] != len(index_list) - 1:
raise ToolError(
"versions %s share %s but are not consecutive"
% (", ".join(bld_pg.pg_list[idx].version for idx in index_list), function)
)
return "%s_%s" % (_version_no_dot(version), _version_no_dot(bld_pg.pg_list[index_list[-1]].version))
####################################################################################################################################
def _function_name(define):
"""The interface function a macro defines, e.g. "PG_INTERFACE_CONTROL" becomes "pgInterfaceControl".
A harness macro reads the same way, e.g. "HRN_PG_INTERFACE_CONTROL" becomes "hrnPgInterfaceControl"."""
result = ""
for idx, part in enumerate(define.split("_")):
part = part.lower()
result += part[:1].upper() + part[1:] if idx != 0 else part
return result
####################################################################################################################################
def _render_macro_list(bld_pg, macro_list, pg):
"""Macros of a list that a version renders, which are the ones no other version already renders the same code for."""
return [macro for macro in macro_list if bld_pg.function_version[macro][pg.version] == pg.version]
####################################################################################################################################
def _render_expand(bld_pg, macro_list, pg):
"""Render the macros a version expands, each saying which other versions share the rendering."""
result = ""
for macro in macro_list:
expand = "%s(%s);" % (macro, _version_suffix(bld_pg, macro, pg.version))
# Versions using this rendering rather than one of their own, which says why they have no rendering of it
share_list = [
pg_share.version
for pg_share in bld_pg.pg_list
if pg_share.version != pg.version and bld_pg.function_version[macro][pg_share.version] == pg.version
]
if share_list != []:
expand += "%*s// Shared with %s" % (_SHARE_COLUMN - len(expand), "", ", ".join(share_list))
result += expand + "\n"
return result
####################################################################################################################################
def _render_interface_auto_c(bld_pg):
"""Render the interface, which is one interface per version plus the struct that finds them."""
interface = bld_pg.interface
result = bld_header(interface.module, interface.description)
# Interfaces, newest first so the most likely match is found first at run time
for pg in reversed(bld_pg.pg_list):
version_no_dot = _version_no_dot(pg.version)
# Values and functions this version renders, which are the ones no other version already renders the same code for
value_list = _render_macro_list(bld_pg, list(dict.fromkeys(value.macro for value in bld_pg.value_list)), pg)
function_list = _render_macro_list(bld_pg, bld_pg.function_list, pg)
result += "\n" + bld_comment_block("PostgreSQL %s interface" % pg.version)
# Only the interface declaring the vendored types sets the version they are declared for. The other renames them to
# what that one declared, which is why they are named the same way here.
if interface.vendor:
result += bld_define("PG_VERSION", "PG_VERSION_%s" % version_no_dot) + "\n\n"
for type in bld_pg.type_list:
result += bld_define(type, "%s_%s" % (type, version_no_dot)) + "\n"
# An unreleased version has no catalog version of its own yet, so it accepts any catalog version up to the maximum
if interface.vendor and not pg.release:
result += "\n#define CATALOG_VERSION_NO_MAX\n"
result += '\n#include "%s"\n\n' % interface.include
# Values this version captures as constants named for it, so the struct at the end can name them
result += _render_expand(bld_pg, value_list, pg)
# A value is read where a function is called, so what a version captures is separated from what it renders
if value_list != [] and function_list != []:
result += "\n"
result += _render_expand(bld_pg, function_list, pg)
# Undefine everything the interface defined so the next one starts clean
result += "\n" + "".join("#undef %s\n" % type for type in bld_pg.type_list)
# Only the interface declaring the vendored types defines what they define, so only it has them to undefine
if interface.vendor:
result += "\n" + "".join("#undef %s\n" % define for define in bld_pg.define_list)
result += "\n" + "".join("#undef %s\n" % function for function in bld_pg.function_list)
# Matches, rendered once each in the form that fits the versions that were rendered. Only a version that has not been released
# accepts a range of values, so the range form is rendered only while there is one.
release = all(pg.release for pg in bld_pg.pg_list)
for macro in [macro for macro in bld_pg.macro_list if macro.endswith(_MATCH_MACRO % "")]:
prefix = macro[: -len(_MATCH_MACRO % "")]
member = _function_name(prefix)[len(interface.prefix) :]
result += "\n" + bld_comment_block("PostgreSQL %s match" % (member[:1].lower() + member[1:]))
result += "%s();\n" % ((_MATCH_MACRO if release else _MATCH_MACRO_RANGE) % prefix)
# Interface struct, newest first so the most likely match is found first
result += "\n" + bld_comment_block(
"PostgreSQL interface struct\n\nA function shared by more than one version is named for the range it covers, so a version"
" may name a\nfunction whose name does not begin with it."
)
result += "static const %s %s[] =\n{\n" % (interface.type, interface.prefix)
for pg in reversed(bld_pg.pg_list):
version_no_dot = _version_no_dot(pg.version)
result += " {\n .version = PG_VERSION_%s,\n" % version_no_dot
# A version that has not been released accepts a range of catalog versions rather than only its own
if interface.unreleased and not pg.release:
result += " .%s = true,\n" % _UNRELEASED_MEMBER
result += "\n"
# An interface reading the values from another one carries none of its own, so there is no group to separate
if bld_pg.value_list != []:
for value in bld_pg.value_list:
member = value.name[len(interface.prefix) :]
result += " .%s = %s%s,\n" % (
member[:1].lower() + member[1:],
value.name,
_version_suffix(bld_pg, value.macro, bld_pg.function_version[value.macro][pg.version]),
)
result += "\n"
for function in bld_pg.function_list:
name = _function_name(function)
member = name[len(interface.prefix) :]
result += " .%s = %s%s,\n" % (
member[:1].lower() + member[1:],
name,
_version_suffix(bld_pg, function, bld_pg.function_version[function][pg.version]),
)
result += " },\n"
return result + "};\n"
####################################################################################################################################
def _render_version_auto_h(bld_pg):
"""Render version.auto.h, which is the version numbers and the strings the errors report them with."""
result = bld_header(_VERSION_MODULE, _VERSION_DESCRIPTION)
result += "#ifndef POSTGRES_VERSION_AUTO_H\n#define POSTGRES_VERSION_AUTO_H\n"
result += "\n" + bld_comment_block("PostgreSQL version constants")
for pg in bld_pg.pg_list:
version_no_dot = _version_no_dot(pg.version)
result += bld_define("PG_VERSION_%s" % version_no_dot, "%u" % bld_pg_version_num(pg.version)) + "\n"
# The newest version is the maximum, which the code uses to reject anything newer than it knows about
result += "\n" + bld_define("PG_VERSION_MAX", "PG_VERSION_%s" % _version_no_dot(bld_pg.pg_list[-1].version)) + "\n"
result += "\n" + bld_comment_block("PostgreSQL version string constants for use in error messages")
for pg in bld_pg.pg_list:
result += bld_define("PG_VERSION_%s_Z" % _version_no_dot(pg.version), '"%s"' % pg.version) + "\n"
return result + "\n#endif\n"
####################################################################################################################################
def _render_system_id(bld_pg):
"""Render the system id defines, which are the system id the harness writes for a version and the strings a test reads it as.
A system id is derived from the version so a test that reports one says which version wrote it. The strings are rendered here
rather than built by the preprocessor because there is no way to make a string of the sum."""
result = ""
for pg in bld_pg.pg_list:
version_no_dot = _version_no_dot(pg.version)
result += (
bld_define(_SYSTEM_ID_DEFINE + version_no_dot, "(%uULL + (uint64_t)PG_VERSION_%s)" % (_SYSTEM_ID_BASE, version_no_dot))
+ "\n"
)
# An offset is a system id a test asks for by adding to the one for the version, which is how a test uses more than one
for offset in _SYSTEM_ID_OFFSET_LIST:
result += (
bld_define(
"%s%s%s_Z" % (_SYSTEM_ID_DEFINE, version_no_dot, "" if offset == 0 else "_%u" % offset),
'"%u"' % (_SYSTEM_ID_BASE + bld_pg_version_num(pg.version) + offset),
)
+ "\n"
)
return result
####################################################################################################################################
def bld_pg_render(path_build, bld_pg):
"""Render the PostgreSQL interfaces."""
file_write_differs(os.path.join(path_build, bld_pg.interface.path_render), _render_interface_auto_c(bld_pg))
####################################################################################################################################
def bld_pg_version_render(path_build, bld_pg):
"""Render the PostgreSQL version constants."""
file_write_differs(os.path.join(path_build, "src/postgres/version.auto.h"), _render_version_auto_h(bld_pg))
####################################################################################################################################
def bld_pg_system_id_render(path_repo, bld_pg):
"""Render the system id defines into the harness header.
They go into the header the rest of the harness declarations are in rather than a generated file of their own, so this replaces
the defines where they are instead of writing a file. Every one of them is replaced by the block, which is rendered where the
first one was."""
path = os.path.join(path_repo, _PATH_SYSTEM_ID)
result = ""
rendered = False
for line in file_read(path).rstrip("\n").split("\n"):
if line.startswith("#define " + _SYSTEM_ID_DEFINE):
if not rendered:
result += _render_system_id(bld_pg)
rendered = True
continue
result += line + "\n"
# A header with none of them is a header this can no longer generate, which is an error rather than a file left as it was
if not rendered:
raise ToolError("unable to find %s defines in '%s'" % (_SYSTEM_ID_DEFINE, path))
file_write_differs(path, result)