EXPLAIN (GENERIC_PLAN): plan a normalized statement, no bound values required #6

Open
hns33 wants to merge 9 commits from hns33/examples:explain-generic-plan into master
First-time contributor

Summary

EXPLAIN today wants a statement it can actually run. Give it a normalized
statement instead — the form the plan cache keeps, with the literals stripped out
and replaced by $1 or a JDBC ? — and analysis stops you cold with
there is no parameter $1. That is exactly the statement a DBA has in hand after
pulling a slow query out of statement_history, and today the only way to see its
plan is to guess the original constants and paste them back in.

This series adds one option, EXPLAIN (GENERIC_PLAN) <normalized SQL>, that plans
the statement the way openGauss plans it the first time it is prepared: the
parameters stay parameters, estimates come from the type's default selectivity, and
nothing is executed. The plan you see is the generic plan the engine would build
for that statement — not an approximation of it.

Nothing about ordinary EXPLAIN changes. The feature is reached only through the
new option, and the ? spelling only lights up behind a GUC that ships off.

Why it doesn't work today, and what each patch does about it

A normalized statement is unexplainable for three unrelated reasons. They are
independent, so the series fixes them independently.

  1. Analysis will not type an unbound $n. The plain analysis path has no
    parameter hooks, so $1 has no type and it errors. — On the GENERIC_PLAN
    path we install openGauss's own variable-parameter hooks
    (parse_variable_parameters before transformTopLevelStmt,
    check_variable_parameters after). These are the hooks PREPARE and the
    extended-query protocol already use, so $1 survives as a Param and its type
    is inferred from context, the same way it would be at prepare time.

  2. The planner would fold the parameter or refuse it. With no bound value there
    is nothing to fold $1 into, and the default cursor options invite a custom
    plan. — We plan with CURSOR_OPT_GENERIC_PLAN and boundParams = NULL. The flag
    already exists in parsenodes_common.h and the custom-vs-generic decision
    already honours it; with no values supplied the parameters cannot collapse to
    constants and the result is a generic plan by construction.

  3. ? is not a parameter token in openGauss. ? is an operator character
    (jsonb ?, ?|, ?&, hstore, geometry), so there is no "bare ? → parameter"
    rule to lean on. — One GUC-gated lexer rule maps a standalone ? to a positional
    parameter, reusing the scanner's own placeholder counter. flex's longest match
    keeps every multi-character ?-operator intact.

Everything past analysis — rewrite, the planner, plan rendering, $n printing,
every FORMAT — is reused unchanged.

Patch series

# Patch What lands
0001 explain: add generic-plan option and node field es.generic_plan on ExplainState; the generic_plan option keyword (explainOptionIsEnabled(stmt->options, "generic_plan")); has_qmark_param on ExplainStmt with copyfuncs/equalfuncs support
0002 parser: JDBC ? placeholder and GUC the GUC-gated ?→positional-param rule in scan.l; scanner placeholder counter; ?/$n/:name mixing detection; the enable_qmark_param GUC and its backing field in knl_session_attr_sql.h
0003 explain: generic-plan analysis and planning the generic-plan branch in transformExplainStmt; the ANALYZE/GENERIC_PLAN mutual exclusion; CURSOR_OPT_GENERIC_PLAN + NULL boundParams in ExplainOneQuery; inferred parameter types carried onto the ExplainState; the three VERBOSE attribute lines
0004 regress: register and add the test explain_generic_plan added to parallel_schedule; self-contained sql/ + expected/ files

The split is deliberate: 0001 is inert scaffolding, 0002 is a lexer change that
stands on its own and stays dormant until its GUC is set, 0003 is the actual
behaviour, 0004 is the test. Each applies and builds on top of the previous.

The option and the GUC

EXPLAIN (GENERIC_PLAN [ ON | OFF ] [, ...]) <normalized SQL>

GENERIC_PLAN is an ordinary boolean EXPLAIN option, so it composes with
VERBOSE, COSTS, FORMAT, and the rest. A repeated option takes its last value,
so analysis and planning never disagree about whether this is a generic plan. It is
mutually exclusive with ANALYZE/PERFORMANCE — a generic plan has no bound values
and cannot be executed, so there are no real rows or timings to report. That
rejection is raised in transformExplainStmt early and again in ExplainQuery
defensively, so both paths agree.

enable_qmark_param   bool   USERSET   default: off

Turn it on and a standalone ? becomes a positional parameter for the current
session. Off, the lexer is byte-for-byte what it was.

What it looks like

openGauss=# EXPLAIN (GENERIC_PLAN, VERBOSE)
openGauss-#   SELECT * FROM orders WHERE uid = $1 AND status = $2;
                                   QUERY PLAN
----------------------------------------------------------------------------------
 Index Scan using orders_uid_idx on public.orders  (cost=0.29..8.31 rows=1 width=44)
   Output: id, uid, status, amount, created_at
   Index Cond: (orders.uid = $1)
   Filter: (orders.status = $2)
   Generic Plan: true
   Parameter Style: dollar
   Parameter Types: $1 integer, $2 text
(7 rows)

The ? form is the same plan; only the scanner input differs:

openGauss=# SET enable_qmark_param = on;
openGauss=# EXPLAIN (GENERIC_PLAN)
openGauss-#   SELECT * FROM orders WHERE uid = ? AND status = ?;

Parameter Types is worth calling out: variable-parameter analysis already produces
the inferred type vector and records it on the Query, so under VERBOSE we just
print it. An operator can confirm that $2 was read as text and not, say,
integer directly, instead of reverse-engineering it from the plan. No extra
analysis cost.

Type inference

There are no new type rules here. Inference is whatever the variable-parameter hooks
already do for PREPARE, which means the generic plan an admin sees is the generic
plan the engine builds for the same statement. The short version:

Statement shape Inferred as
col = $1, $1 = col, col IN ($1,$2), col BETWEEN $1 AND $2 the column's type
$1::T T (an explicit cast wins)
f($1) the argument type
INSERT ... VALUES ($1) the target column's type
bare SELECT $1, $1 IS NULL no context → error + a cast hint

$1 = $2 (both unknown) is not an error: openGauss coerces the unknown-unknown
comparison to text and plans both as text, matching ordinary unknown-literal
handling. Parameters are dense — skip $1 and use $2 and analysis errors, exactly
as a prepared statement would.

Compatibility and the one trade-off

The invasive-looking part is scan.l, so be precise about its blast radius:

  • With enable_qmark_param = off (the default) there is no change to any session.
    A grep of the diff for behaviour outside the new rule turns up nothing; the rule
    is guarded on the GUC.
  • With it on, the single-character jsonb existence operator jsonb ? text is
    shadowed by the placeholder rule for that session. This is the only semantic
    trade-off and it is contained: ?| and ?& are multi-character and flex's
    longest match leaves them alone, and a session that needs single-char ? either
    leaves the GUC off or writes jsonb_exists(col, 'key').
  • ? is a parameter only when it stands alone. Written tight against an operator
    (col=?) flex fuses =? into one operator token — the same longest-match rule
    that protects ?|/?&. Write col = ?. This is spelled out in
    docs/OPERATIONS.md.
  • ?, $n and :name cannot be mixed in one statement; mixing is rejected through
    the scanner's existing placeholder-conflict detection, so we don't invent a second
    numbering scheme.

The two EXPLAIN paths that bypass direct-query planning are guarded so a plan that
is not generic can never be labelled Generic Plan: true: EXECUTE runs a cached
plan the plan cache may resolve to a custom plan (rejected with a hint), and
CREATE MODEL is planned with bound parameters (rejected). CREATE TABLE AS already
requires ANALYZE and so errors on the mutual exclusion first; DECLARE CURSOR
falls through to the direct-query path and is planned generically.

Testing

Two layers, both in the tree.

  • Integration (scripts/run_tests.sh, groups A–M against a running instance):
    the ? and $n spellings, tight-operator col=? handling, jsonb ?/?|/?&
    disambiguation, the ANALYZE+GENERIC_PLAN rejection, ?/$n mixing rejection,
    EXECUTE not being mislabelled, and a full regression group (L1–L5) asserting
    that ordinary EXPLAIN/PREPARE/EXECUTE are untouched.

    ============================================================
     RESULT: 36 passed, 0 failed
    ============================================================
    
  • Upstream regression (explain_generic_plan, registered in
    parallel_schedule, run through pg_regress --use-existing):

    All 1 tests passed (explain_generic_plan)
    

Build/apply is scripted end to end: scripts/apply_to_upstream.sh applies the four
patches to a clean checkout, scripts/build.sh builds debug, scripts/run_regress.sh
runs the registered test. Logs are captured under build/.

Follow-ups I did not do here

  • The GUC is read by the scanner as it lexes the ?, so SET enable_qmark_param = on
    has to happen in an earlier round-trip than the EXPLAIN it governs — you cannot
    set it and use ? in the same multi-statement string. That is a consequence of
    where the rule lives, not a bug, but it surprises people; it is worth a line in the
    client-driver docs if we ever wire this into a JDBC helper.
  • The generic plan is planning-only by definition. If we ever want "generic plan,
    then run it with these values" that is a different feature and a different option,
    not an ANALYZE variant of this one.

Lineage

EXPLAIN (GENERIC_PLAN) for $n is the design PostgreSQL 16 settled on —
variable-parameter inference, CURSOR_OPT_GENERIC_PLAN, no bound values. openGauss
inherits that machinery, so on that baseline this change is small: it adds the JDBC
? spelling behind a default-off GUC, and surfaces the inferred Parameter Types
under VERBOSE. The jsonb ? conflict is handled the only honest way — keep the
GUC off by default and let longest-match protect the multi-character operators.

## Summary `EXPLAIN` today wants a statement it can actually run. Give it a *normalized* statement instead — the form the plan cache keeps, with the literals stripped out and replaced by `$1` or a JDBC `?` — and analysis stops you cold with `there is no parameter $1`. That is exactly the statement a DBA has in hand after pulling a slow query out of `statement_history`, and today the only way to see its plan is to guess the original constants and paste them back in. This series adds one option, `EXPLAIN (GENERIC_PLAN) <normalized SQL>`, that plans the statement the way openGauss plans it the *first* time it is prepared: the parameters stay parameters, estimates come from the type's default selectivity, and nothing is executed. The plan you see is the generic plan the engine would build for that statement — not an approximation of it. Nothing about ordinary `EXPLAIN` changes. The feature is reached only through the new option, and the `?` spelling only lights up behind a GUC that ships off. ## Why it doesn't work today, and what each patch does about it A normalized statement is unexplainable for three unrelated reasons. They are independent, so the series fixes them independently. 1. **Analysis will not type an unbound `$n`.** The plain analysis path has no parameter hooks, so `$1` has no type and it errors. — On the `GENERIC_PLAN` path we install openGauss's own variable-parameter hooks (`parse_variable_parameters` before `transformTopLevelStmt`, `check_variable_parameters` after). These are the hooks `PREPARE` and the extended-query protocol already use, so `$1` survives as a `Param` and its type is inferred from context, the same way it would be at prepare time. 2. **The planner would fold the parameter or refuse it.** With no bound value there is nothing to fold `$1` into, and the default cursor options invite a custom plan. — We plan with `CURSOR_OPT_GENERIC_PLAN` and `boundParams = NULL`. The flag already exists in `parsenodes_common.h` and the custom-vs-generic decision already honours it; with no values supplied the parameters cannot collapse to constants and the result is a generic plan by construction. 3. **`?` is not a parameter token in openGauss.** `?` is an operator character (jsonb `?`, `?|`, `?&`, hstore, geometry), so there is no "bare `?` → parameter" rule to lean on. — One GUC-gated lexer rule maps a standalone `?` to a positional parameter, reusing the scanner's own placeholder counter. flex's longest match keeps every multi-character `?`-operator intact. Everything past analysis — rewrite, the planner, plan rendering, `$n` printing, every `FORMAT` — is reused unchanged. ## Patch series | # | Patch | What lands | |---|-------|-----------| | 0001 | explain: add generic-plan option and node field | `es.generic_plan` on `ExplainState`; the `generic_plan` option keyword (`explainOptionIsEnabled(stmt->options, "generic_plan")`); `has_qmark_param` on `ExplainStmt` with `copyfuncs`/`equalfuncs` support | | 0002 | parser: JDBC `?` placeholder and GUC | the GUC-gated `?`→positional-param rule in `scan.l`; scanner placeholder counter; `?`/`$n`/`:name` mixing detection; the `enable_qmark_param` GUC and its backing field in `knl_session_attr_sql.h` | | 0003 | explain: generic-plan analysis and planning | the generic-plan branch in `transformExplainStmt`; the `ANALYZE`/`GENERIC_PLAN` mutual exclusion; `CURSOR_OPT_GENERIC_PLAN` + `NULL` boundParams in `ExplainOneQuery`; inferred parameter types carried onto the `ExplainState`; the three `VERBOSE` attribute lines | | 0004 | regress: register and add the test | `explain_generic_plan` added to `parallel_schedule`; self-contained `sql/` + `expected/` files | The split is deliberate: 0001 is inert scaffolding, 0002 is a lexer change that stands on its own and stays dormant until its GUC is set, 0003 is the actual behaviour, 0004 is the test. Each applies and builds on top of the previous. ## The option and the GUC ``` EXPLAIN (GENERIC_PLAN [ ON | OFF ] [, ...]) <normalized SQL> ``` `GENERIC_PLAN` is an ordinary boolean `EXPLAIN` option, so it composes with `VERBOSE`, `COSTS`, `FORMAT`, and the rest. A repeated option takes its last value, so analysis and planning never disagree about whether this is a generic plan. It is mutually exclusive with `ANALYZE`/`PERFORMANCE` — a generic plan has no bound values and cannot be executed, so there are no real rows or timings to report. That rejection is raised in `transformExplainStmt` early and again in `ExplainQuery` defensively, so both paths agree. ``` enable_qmark_param bool USERSET default: off ``` Turn it on and a standalone `?` becomes a positional parameter for the current session. Off, the lexer is byte-for-byte what it was. ## What it looks like ``` openGauss=# EXPLAIN (GENERIC_PLAN, VERBOSE) openGauss-# SELECT * FROM orders WHERE uid = $1 AND status = $2; QUERY PLAN ---------------------------------------------------------------------------------- Index Scan using orders_uid_idx on public.orders (cost=0.29..8.31 rows=1 width=44) Output: id, uid, status, amount, created_at Index Cond: (orders.uid = $1) Filter: (orders.status = $2) Generic Plan: true Parameter Style: dollar Parameter Types: $1 integer, $2 text (7 rows) ``` The `?` form is the same plan; only the scanner input differs: ``` openGauss=# SET enable_qmark_param = on; openGauss=# EXPLAIN (GENERIC_PLAN) openGauss-# SELECT * FROM orders WHERE uid = ? AND status = ?; ``` `Parameter Types` is worth calling out: variable-parameter analysis already produces the inferred type vector and records it on the `Query`, so under `VERBOSE` we just print it. An operator can confirm that `$2` was read as `text` and not, say, `integer` directly, instead of reverse-engineering it from the plan. No extra analysis cost. ## Type inference There are no new type rules here. Inference is whatever the variable-parameter hooks already do for `PREPARE`, which means the generic plan an admin sees is the generic plan the engine builds for the same statement. The short version: | Statement shape | Inferred as | |---|---| | `col = $1`, `$1 = col`, `col IN ($1,$2)`, `col BETWEEN $1 AND $2` | the column's type | | `$1::T` | `T` (an explicit cast wins) | | `f($1)` | the argument type | | `INSERT ... VALUES ($1)` | the target column's type | | bare `SELECT $1`, `$1 IS NULL` | no context → **error + a cast hint** | `$1 = $2` (both unknown) is not an error: openGauss coerces the unknown-unknown comparison to `text` and plans both as `text`, matching ordinary unknown-literal handling. Parameters are dense — skip `$1` and use `$2` and analysis errors, exactly as a prepared statement would. ## Compatibility and the one trade-off The invasive-looking part is `scan.l`, so be precise about its blast radius: - With `enable_qmark_param = off` (the default) there is no change to any session. A grep of the diff for behaviour outside the new rule turns up nothing; the rule is guarded on the GUC. - With it **on**, the single-character jsonb existence operator `jsonb ? text` is shadowed by the placeholder rule for that session. This is the only semantic trade-off and it is contained: `?|` and `?&` are multi-character and flex's longest match leaves them alone, and a session that needs single-char `?` either leaves the GUC off or writes `jsonb_exists(col, 'key')`. - `?` is a parameter **only when it stands alone.** Written tight against an operator (`col=?`) flex fuses `=?` into one operator token — the same longest-match rule that protects `?|`/`?&`. Write `col = ?`. This is spelled out in `docs/OPERATIONS.md`. - `?`, `$n` and `:name` cannot be mixed in one statement; mixing is rejected through the scanner's existing placeholder-conflict detection, so we don't invent a second numbering scheme. The two `EXPLAIN` paths that bypass direct-query planning are guarded so a plan that is *not* generic can never be labelled `Generic Plan: true`: `EXECUTE` runs a cached plan the plan cache may resolve to a custom plan (rejected with a hint), and `CREATE MODEL` is planned with bound parameters (rejected). `CREATE TABLE AS` already requires `ANALYZE` and so errors on the mutual exclusion first; `DECLARE CURSOR` falls through to the direct-query path and is planned generically. ## Testing Two layers, both in the tree. - **Integration** (`scripts/run_tests.sh`, groups A–M against a running instance): the `?` and `$n` spellings, tight-operator `col=?` handling, jsonb `?`/`?|`/`?&` disambiguation, the `ANALYZE`+`GENERIC_PLAN` rejection, `?`/`$n` mixing rejection, `EXECUTE` not being mislabelled, and a full regression group (L1–L5) asserting that ordinary `EXPLAIN`/`PREPARE`/`EXECUTE` are untouched. ``` ============================================================ RESULT: 36 passed, 0 failed ============================================================ ``` - **Upstream regression** (`explain_generic_plan`, registered in `parallel_schedule`, run through `pg_regress --use-existing`): ``` All 1 tests passed (explain_generic_plan) ``` Build/apply is scripted end to end: `scripts/apply_to_upstream.sh` applies the four patches to a clean checkout, `scripts/build.sh` builds debug, `scripts/run_regress.sh` runs the registered test. Logs are captured under `build/`. ## Follow-ups I did not do here - The GUC is read by the scanner as it lexes the `?`, so `SET enable_qmark_param = on` has to happen in an earlier round-trip than the `EXPLAIN` it governs — you cannot set it and use `?` in the same multi-statement string. That is a consequence of where the rule lives, not a bug, but it surprises people; it is worth a line in the client-driver docs if we ever wire this into a JDBC helper. - The generic plan is planning-only by definition. If we ever want "generic plan, then run it with these values" that is a different feature and a different option, not an `ANALYZE` variant of this one. ## Lineage `EXPLAIN (GENERIC_PLAN)` for `$n` is the design PostgreSQL 16 settled on — variable-parameter inference, `CURSOR_OPT_GENERIC_PLAN`, no bound values. openGauss inherits that machinery, so on that baseline this change is small: it adds the JDBC `?` spelling behind a default-off GUC, and surfaces the inferred `Parameter Types` under `VERBOSE`. The jsonb `?` conflict is handled the only honest way — keep the GUC off by default and let longest-match protect the multi-character operators.
hns33 added 9 commits 2026-07-02 00:17:19 +08:00
3d91825ae8 docs(generic-plan): add project overview and design
Introduce GenericPlanExplain, an openGauss kernel feature that lets EXPLAIN accept a normalized SQL statement (parameters written as $n or a JDBC ?) and return a generic plan: parameters kept in run-time form, plan estimated from default selectivities, no bound values.

README.md leads with the before/after and an at-a-glance evaluation path. DESIGN.md describes the three obstacles and how each is removed by reusing existing infrastructure, the parameter-type reporting, and why EXECUTE / CREATE MODEL are rejected while CTAS / DECLARE CURSOR need no guard. OPERATIONS.md documents the syntax, the enable_qmark_param GUC, the VERBOSE Parameter Types line, the standalone-token rule for ?, the type-inference cheat-sheet and troubleshooting.
3c479306df feat(generic-plan): add EXPLAIN GENERIC_PLAN option and parse-node field
Add the boolean generic_plan field to ExplainState and a has_qmark_param flag to both ExplainState and the ExplainStmt parse node (with copy/equal support). ExplainState also gains generic_param_count / generic_param_types to carry the context-inferred parameter types through to the VERBOSE output.

EXPLAIN already parses an arbitrary option list into DefElem nodes, so no grammar change is needed to accept GENERIC_PLAN as an option name.
37f8baeb75 feat(generic-plan): parse JDBC '?' placeholders behind enable_qmark_param
Add a GUC-gated scanner rule that maps a standalone '?' to a positional parameter, numbered in scan order from a scanner counter, so the '?' form joins the '$n' form at the same PARAM token and needs no grammar change. The rule sits before the operator rule and matches a single character only, so flex's longest match leaves the multi-character jsonb/hstore operators (?| ?&) untouched; with the GUC off the '?' is returned as the operator exactly as before.

Mixing '?' with '$n' or ':name' in one statement is rejected, reusing the scanner's existing placeholder-conflict tracking. The new USERSET GUC enable_qmark_param (default off) gates the whole rule, so default behaviour is unchanged.
0a0b334fb8 feat(generic-plan): variable-parameter analysis and generic-plan planning
On the GENERIC_PLAN path, transformExplainStmt installs the variable-parameter hooks (parse_variable_parameters) before transforming the inner query and runs check_variable_parameters afterwards, so $n survives as a Param with its type inferred from context; any parameter still un-typed is reported with a cast HINT. ExplainOneQuery then plans with CURSOR_OPT_GENERIC_PLAN and NULL bound params, so the parameters are not folded to constants and a generic plan is produced.

ANALYZE/PERFORMANCE with GENERIC_PLAN is rejected, as is GENERIC_PLAN on EXECUTE and CREATE MODEL (both planned off the generic-plan path, so they must not be labelled generic). Under VERBOSE the inferred types are reported as a Parameter Types line alongside Generic Plan and Parameter Style. A repeated option takes its last value so analysis and planning agree.
1175e0135c test(generic-plan): EXPLAIN (GENERIC_PLAN) regression test
Add the explain_generic_plan regression test as a self-contained patch (it both registers the test in the parallel schedule and adds the sql/expected files, so applying 0001-0004 alone yields a buildable tree; the kernel/ copy mirrors the files for direct reading).

The test exercises the $n generic plan, the VERBOSE attributes (including Parameter Types) in both dollar and question_mark styles, jsonb ?| preservation, ?/$n mixing rejection, the ANALYZE/GENERIC_PLAN mutex, the un-typable-parameter error, the GENERIC_PLAN OFF path and the EXECUTE rejection. The expected output is captured byte-for-byte from the reference build and passes under pg_regress.
cca1191f88 build(generic-plan): glibc/Ubuntu host build portability
Adapt the build so the openEuler gcc 7.3 toolchain produces a working tree on a glibc/Ubuntu host (glibc >= 2.31): use glibc's own gettimeofday() declaration, route the bbox headers to <linux/sysctl.h>, skip the removed <sys/vtimes.h>, honour GS_WITHOUT_READLINE, drop the unrelated MOT engine and pg_probackup, make the AWS SDK copy non-fatal, and make the make job count tunable. A vendored libaio.h covers hosts without libaio-dev.

None of this touches the feature; it is applied by default for the glibc reference build and can be skipped with GS_APPLY_PORTABILITY=0 on a native openEuler host.
752d5e4eba chore(generic-plan): reproduction, build and instance scripts
Add the scripts that reproduce the build from a clean upstream checkout: apply_to_upstream.sh resets the pinned commit and applies the self-contained patch series; build.sh / resume_build.sh drive the gcc 7.3 toolchain (resume_build is for .cpp-only edits -- a header change needs a clean build); init_instance.sh / stop_instance.sh manage a standalone demo instance; run_demo.sh runs the walk-through; run_regress.sh drives the test through the real pg_regress harness. All paths and the build workspace live under build/ (git ignored) so the committed tree stays clean.
4df8201ba9 test(generic-plan): automated suite and normalized-SQL demo
run_tests.sh asserts the mandatory $n / ? generic-plan behaviour plus the surrounding guarantees: context-driven type inference, the reported Parameter Types, the ANALYZE mutex, the cast HINT, jsonb operator preservation, every output FORMAT, the standalone-token rule for '?', ?/$n mixing rejection, the EXECUTE rejection, and zero regression of ordinary EXPLAIN / PREPARE. It opens with sanity checks and keys on deterministic plan text, not timing. demo.sql walks the same behaviour and shows, on a skewed column, why a value-free generic plan beats guessing a literal.
fbee3d0249 docs(generic-plan): change inventory, source verification and results
CHANGES.md lists the per-file edits grouped by patch and notes the self-contained 0004; SOURCE_VERIFICATION.md records that every reused kernel facility was confirmed in the pinned source, the empirically-confirmed behaviours and the special-branch analysis, and the clean-build requirement for the header change; RESULTS.md captures the build, the 36/36 automated suite, the pg_regress pass and the demo output.
This pull request can be merged automatically.
You are not authorized to merge this pull request.
You can also view command line instructions.

Step 1:

From your project repository, check out a new branch and test the changes.
git checkout -b hns33-explain-generic-plan master
git pull explain-generic-plan

Step 2:

Merge the changes and update on Gitea.
git checkout master
git merge --no-ff hns33-explain-generic-plan
git push origin master
Sign in to join this conversation.
No reviewers
No Label
No Milestone
No project
No Assignees
1 Participants
Notifications
Due Date
The due date is invalid or out of range. Please use the format 'yyyy-mm-dd'.

No due date set.

Dependencies

No dependencies set.

Reference: opengaussexamples/examples#6
No description provided.