Conversation
|
pg-history: Bedrock call failed: The read operation timed out |
There was a problem hiding this comment.
🔍 OCR found 82 issue(s).
- 80 inline, 2 in summary
📄 src/backend/parser/scan.c
Correctness/security regression in $N parameter parsing. The param pattern is \${decdigit}+ (unbounded digits), but this copies only the first 31 bytes into buf[32]. For a token with >=32 digits, the trailing digits are silently dropped before pg_strtoint32_safe, so e.g. $000...0001 (with enough leading zeros) parses to a WRONG, smaller value instead of either the correct number or a parameter number too large error. The retired flex scanner ran pg_strtoint32_safe over the full yytext+1, which correctly rejected over-long inputs. The very next case (SCAN_TOK_ICONST_*) already does the right thing with palloc(len + 1). Replace the fixed buf[32] with a len-sized palloc'd copy so the whole digit run is parsed.
📄 src/fe_utils/Makefile
These compatibility-shim macros are unused and dangerous. Verified that all three ported scanners — psqlscan.c, psqlscanslash.c, and pgbench/exprscan.c — reference only the ST_-prefixed enum values (ST_INITIAL, ST_XB, ST_XQS, …); none use the bare identifiers. The comment's justification is false: exprscan.c resets via state->start_state = ST_INITIAL (lines 643/722/759), not bare INITIAL. The only occurrence of start_state = INITIAL in the whole frontend tree is inside this very comment.
Defining unscoped single/double-letter macros like xb, xc, xd, xe, xh, xq — and especially INITIAL — in a header transitively included (via psqlscan_emit.h) by psql, pgbench, and fe_utils translation units is namespace pollution that can silently rewrite unrelated local variables, struct members, or parameters, causing hard-to-diagnose miscompilation or build breakage. Since nothing references these names, delete the entire shim block (and the misleading comment above it) rather than keeping a speculative compatibility layer. (confidence: high)
| - -g | ||
| - -std=c11 | ||
| - -I. | ||
| - -I../../../../src/include |
There was a problem hiding this comment.
This relative include path appears incorrect. The .clangd file is at the repository root, and clangd resolves relative paths in CompileFlags.Add relative to the .clangd file's directory. Since src/include lives directly under the repo root (src/include/postgres.h), ../../../../src/include resolves to four directories above the repo root and won't be found. It should likely be -I./src/include (or -Isrc/include).
| - -I../../../../src/include | |
| + - -I./src/include |
| * raised). */ | ||
| extern int scan_lex_handle_unicode(void *user, int pos, char32_t c); | ||
| extern void scan_lex_handle_xeu_second(void *user, int pos, char32_t c); | ||
| extern void scan_lex_handle_xeescape(void *user, int pos, unsigned char c); |
There was a problem hiding this comment.
Prototype mismatch (high confidence). This declaration is missing the int pos parameter. The definition in scan.c is void scan_lex_handle_xeescape(void *user, int pos, unsigned char c) (scan.c:379) and the call site in scan.lex passes three arguments: scan_lex_handle_xeescape(user, SCAN_LEX_OFFSET(matched), (unsigned char) matched[1]) (scan.lex:520). Since scan.c includes this header, the two-argument prototype conflicts with the three-argument definition and will fail to compile ("conflicting types"). Fix the prototype to match.
| extern void scan_lex_handle_xeescape(void *user, int pos, unsigned char c); | |
| extern void scan_lex_handle_xeescape(void *user, int pos, unsigned char c); |
| if (stat(so_path, &st) == 0 && S_ISREG(st.st_mode)) | ||
| { | ||
| ereport(LOG, | ||
| (errmsg("grammar extension cache hit: %s", so_path))); | ||
| goto dlopen_step; | ||
| } |
There was a problem hiding this comment.
Cache-hit path is broken when only the .so survives. On a cache hit you only stat <hex>.so, then goto dlopen_step, which calls build_extension_keyword_map() -> AllocateFile(".h"). The persistent cache artifact is the .so; the .h/.c are byproducts that may have been cleaned up (or never present on a machine that copied only the .so). When <hex>.h is missing, build_extension_keyword_map returns false and the whole pipeline ereport(ERROR)s, defeating the cache entirely and making extensions fail whenever the header is absent. Either stat the .h alongside the .so before treating it as a hit, or persist the resolved (lexeme -> token_code) map rather than re-parsing the header on every load. (confidence: high)
| while (waitpid(pid, &status, 0) < 0) | ||
| { | ||
| if (errno != EINTR) | ||
| { | ||
| pfree(errbuf.data); | ||
| *errmsg_out = psprintf("waitpid() for %s failed: %m", progname); | ||
| return false; | ||
| } | ||
| } |
There was a problem hiding this comment.
These blocking syscalls run in a backend with no interrupt handling. The read() loop only special-cases EINTR (continue), and waitpid() only retries on EINTR; neither calls CHECK_FOR_INTERRUPTS(). A wedged or slow lime/cc makes the backend hang uninterruptibly — a query cancel (SIGINT) or SIGTERM cannot break it because the loop just resumes the syscall. Add CHECK_FOR_INTERRUPTS() in the read loop and around the waitpid retry, and consider a timeout so a stuck subprocess does not pin a connection indefinitely. (confidence: high)
| * read at first-use; OpenPipeStream isn't suitable because | ||
| * we want a tight read with a fixed buffer. | ||
| */ | ||
| pipe = popen("lime -v 2>/dev/null", "r"); |
There was a problem hiding this comment.
lime is launched via execvp(), i.e. a PATH lookup with no absolute path, and resolve_cc() honors $CC, and resolve_lime_version() runs popen("lime -v ...") through /bin/sh. All three execute under the postmaster's privileges at parse time. A poisoned PATH (or attacker-controlled $CC) lets a local user substitute a malicious lime/cc that the server then runs and dlopens. Pin these to absolute, install-time-known paths (or validate them) instead of relying on PATH/$CC, and prefer the project's run_program/OpenPipeStream over shell-based popen. (confidence: moderate)
| if not srcdir.is_dir(): | ||
| sys.exit(f'lime_format_check: srcdir not found: {srcdir}') | ||
|
|
||
| SKIP_PATTERNS = ('build', 'install', '.git', 'tmp_install') |
There was a problem hiding this comment.
The quote-aware scanner treats " and ' symmetrically as string delimiters, but in C these have different semantics: ' introduces a single character (char) literal, not an arbitrary-length string. This works for well-formed bodies, but the else-branch scan (lines below) stops at any ' or ", so a stray/unbalanced quote in an action — e.g. an apostrophe inside a comment, or a char literal containing a quote like '\'' whose escaped inner quote is mis-counted — can desynchronize the quote state. Once desynchronized, all subsequent $N/@N references are either silently skipped or rewritten inside what is actually code, corrupting the generated action with no diagnostic. Since this drives every generated parser action, consider hardening the scanner (track char-literals separately with proper escape rules, and assert balanced quoting) so any malformed input fails loudly rather than producing a subtly wrong .lime.
| error_count_zero = ('0 error(s)' in out | ||
| or 'OK: no diagnostics' in out | ||
| or '✓ No errors or warnings' in out) |
There was a problem hiding this comment.
Fragile substring match causes a false negative: '0 error(s)' in out matches any error count ending in 0 (e.g. 10 error(s), 20 error(s), 100 error(s)), so a grammar with 10/20/... errors would be treated as clean and pass linting — masking real failures. Match the count anchored to the start of the number instead, e.g. parse with a regex like re.search(r'\b([0-9]+) error\(s\)', out) and compare the captured integer to 0.
| if has_failure: | ||
| failures += 1 | ||
| print(f'FAIL {rel}', file=sys.stderr) |
There was a problem hiding this comment.
Behavioral discrepancy with the header comment, which states "non-zero on the first lint failure." This loop continues through all files and exits non-zero only at the end (aggregate). Continuing is arguably the more useful behavior, but the documented contract should be updated to match (e.g. "reports all failures and exits non-zero if any file fails") to avoid misleading maintainers/tooling.
| if args.aot: | ||
| if not args.output_aot: | ||
| sys.exit('--aot requires --aot-output') |
There was a problem hiding this comment.
The --aot-output validation is placed after Lime has already run with -j and after the .c/.h files have been moved. While meson always passes --aot and --aot-output together (so this won't trigger in practice), validating this required-combination right after parse_args() would fail fast and avoid leaving partial outputs. Consider moving the check before building/running the command.
| # - treats Lime's `.out` report as a build artefact worth keeping | ||
| # (mirrored from <outdir>/<basename>.out into <privatedir>) |
There was a problem hiding this comment.
This comment claims the wrapper mirrors Lime's .out report into privatedir, but the code never handles the .out file at all. Additionally, since --privatedir is already Lime's -d output dir, the report is written directly there — making the "mirrored ... into " wording self-contradictory. Please align the comment with the actual implementation (or implement the described mirroring) to avoid misleading future maintainers.
813bde8 to
ed90aaa
Compare
Adds a runtime grammar-extension API enabling extensions to
register new tokens, productions, and reduce callbacks before
the first parse, then rebuilds the SQL parser to incorporate
them. This is a foundation for runtime-extensible SQL dialects
(QUEL revival in contrib/quel as a demonstration; out-of-tree
DSLs like a DuckDB-compat or MongoDB-JSONB syntax via the same
API).
Public API (include/parser/parser_extension.h):
PgGrammarExtension *pg_grammar_ext_create(name, version);
void pg_grammar_ext_add_token(...);
void pg_grammar_ext_add_rule(...);
void pg_grammar_ext_set_precedence(...);
bool pg_grammar_ext_register(ext, &err);
Calls are valid only from _PG_init() of a shared_preload_libraries-
loaded module, before raw_parser() runs for the first time.
Implementation (parser_extension.c):
Track A subprocess pipeline: at first parse, walk the registered
extensions, serialize them into a .lime fragment text alongside
the base gram.lime, fork+exec lime + cc to produce a rebuilt
parser .so, dlopen it, and dispatch base_yyparse through a
function pointer (base_yyparse_fn) that points at the rebuilt
symbol. Cache the .so under $PGDATA/pg_parser_cache/<sha256>.so.
Phase 1 scanner hook in scan.c: extension-registered keywords
that don't appear in the compile-time ScanKeywords table are
caught by pg_grammar_ext_keyword_hook after the base lookup
misses. Returns the rebuilt parser's token code; the rebuild
step ensures the parser tables know about it.
Why [DO NOT MERGE]:
* The API surface is intentionally small but the runtime
re-build (fork + lime + cc + dlopen) is operationally
heavy on cold cache: the first parse after postmaster
start with extensions loaded takes ~9s. Warm cache is
~11ms. Production OLTP overhead with no parsing-bound
workload is 0.5-2%; parser-bound benchmarks see 4-12%.
* The keyword shadowing rules are non-obvious (extensions
cannot override base SQL keywords; the hook fires only on
base lookup miss). Documented in parser_extension.h, but
this constraint surprises authors who expect MySQL-compat
or DuckDB-compat dialects to override SHOW or ATTACH.
* Track B (in-process snapshot patching, no subprocess) is
designed but not implemented. Track A works in production
today; Track B would cut the 9s cold cost to ~5ms but
requires invasive parser.c surgery.
* No -hackers consensus on whether runtime grammar extensions
belong in core at all; this is RFC-quality work for review
and discussion.
Tests: see [DO NOT MERGE] commits below for grammar_ext_compose,
grammar_ext_overlap, dummy_grammar_ext, lime_in_process_smoke,
parser_microbench and contrib/quel that exercise this API.
Five test modules exercising the runtime grammar-extension API:
* dummy_grammar_ext -- minimal smoke test: 1 token,
1 rule, 1 reduce callback.
Verifies end-to-end registry
-> rebuild -> dlopen -> parse
pipeline.
* grammar_ext_compose -- 6 small extensions composed in
8 different load-order
permutations. 22 sub-tests
covering token-name no-op
vs collision, cross-extension
references, precedence,
cache-key determinism, and
base-grammar invariance.
* grammar_ext_overlap -- 5 simulator extensions
(DuckDB-compat, MySQL-compat,
MongoDB-JSONB, pg_infer,
QUEL-lite) loaded
simultaneously. 42 sub-tests
covering one-rebuild-for-all,
13-keyword reachability,
mixed SQL+extension-DSL
sessions, order independence,
subset-load fallthrough.
* lime_in_process_smoke -- exercises the in-process
lime_compile_grammar_in_process
path (Track B Phase 2 Step 1).
* parser_microbench -- direct raw_parser() timing
benchmark: 1738 ns/parse for
SELECT 1, 5207 ns for realistic
OLTP, 5539 ns for DDL on a
debug build.
These modules together demonstrate the API works under realistic
multi-extension composition. They are NOT for upstream merge:
they belong in test/modules as research artifacts, not as part
of the core test surface.
…nsion
Demonstrates the runtime grammar-extension API by reviving the
Berkeley QUEL query language from the original POSTGRES (1986)
as a contrib module. All five Berkeley QUEL forms are
supported via the Lime extension API:
RANGE OF e IS emp -- tuple-variable binding
RETRIEVE (e.name, e.salary) -- SELECT
where e.dept = 'shoe'
RETRIEVE (e.name) BY e.salary DESC -- SELECT ... ORDER BY DESC
REPLACE emp (salary = 50000) -- UPDATE WHERE
where dept='shoe'
APPEND TO emp (name='alice', ...) -- INSERT
DELETE emp where salary < 1000 -- DELETE WHERE
Each form constructs a real PostgreSQL parse-tree node
(SelectStmt / UpdateStmt / InsertStmt / DeleteStmt) at parse
time, flowing through parse_analyze + planner + executor +
EXPLAIN unchanged. 9 SQL/QUEL equivalence assertions in
t/001_quel.pl prove the parser produces identical results
to the equivalent SQL.
Keyword shadowing constraint: extension keywords can't
override base SQL keywords, so QUEL uses a q_-prefix for
words that conflict (q_range, q_of, q_is, q_to, q_by,
q_replace, q_delete, q_into). Documented in the SGML
chapter (doc/src/sgml/quel.sgml) and in
parser_extension.h's pg_grammar_ext_keyword_hook block.
Why [DO NOT MERGE]:
* QUEL itself has no production users. This is a
demonstration of the runtime extension API at a non-trivial
scale (30 rules, 8 token types, 10 keyword tokens), not a
proposal to add Berkeley QUEL to PostgreSQL core.
* The SGML chapter is informative but exceeds what most contrib
modules ship; it includes historical context, a syntax
reference, 6 worked examples, and a Limitations section.
This commit ships QUEL as a research artifact alongside the
runtime extension API. Anyone interested in writing a similar
DSL extension can read contrib/quel as a worked-out example.
…rack B P1)
Add the build-system foundation for in-process grammar extension
(Track B), replacing the fork+lime+cc+dlopen pipeline.
* pglime wrapper: --snapshot flag drives `lime -n`, emitting
<basename>_snapshot.c (the runtime ParserSnapshot builder plus the
embedded grammar source) next to the existing .c/.h/_aot.c.
* meson: lime_snapshot_kw / lime_aot_snapshot_kw variants add the
_snapshot.c output for the backend grammar.
* backend/parser: build gram with the snapshot variant; compile
gram_snapshot.c (which provides base_yyBuildSnapshot()) in its own
static_library so its bare Lime #includes ("snapshot.h",
"snapshot_build.h") resolve against Lime const include/ -- those
basenames collide with PostgreSQL utils/snapshot.h, so the Lime
include directory must not leak onto any other backend TU. The
main parser lib compiles only the .c/.h/_aot.c outputs.
No behaviour change yet: nothing references base_yyBuildSnapshot, so
the snapshot archive is dropped at link time. The in-process compose
and snapshot-driven parse path follow in subsequent commits.
Adopt Lime v1.6.1 and add the runtime push-parse path that runs the
backend grammar entirely in-process -- no subprocess, no C compiler --
proving out Track B before retiring the fork+cc+dlopen pipeline.
* Pin Lime v1.6.1 (flake + meson floor >=1.6.1). v1.6.1 ships
host-reduce (--host-reduce): the generated base_yyHostReduce
wrapper runs the static yy_rule_reduce_fn[] reduce actions over a
runtime ParserSnapshot, threading the %extra_argument (yyscanner)
from the host_reduce user pointer so PostgreSQL action bodies work.
* pglime --host-reduce flag; backend gram emitted with -n --host-reduce
so gram_snapshot.c carries base_yyBuildSnapshot() (host_reduce wired).
* parser_pushparse.c: raw_parser_lime_pushparse() drives parse_begin_-
borrowed / parse_token / parse_end over the base snapshot, with
parse_set_host_reduce(ctx, base_yyHostReduce, yyscanner). Isolated
in its own static_library with Lime const include path (Lime const
snapshot.h basename collides with PostgreSQL utils/snapshot.h).
* raw_parser(): when PG_LIME_PUSHPARSE is set, drive the push path
instead of the static pull parser. Default path unchanged.
Verified on a temp cluster (PG_LIME_PUSHPARSE=1): operator precedence,
string ops, subqueries/VALUES/WHERE/ORDER BY, DDL+DML, aggregates, and
CTEs all parse and execute correctly, matching the pull parser. This is
the kernel of the cc-free Track B parse path.
Force liblime_compiler.a whole into the backend link so lime_compile_grammar_in_process resolves to the real in-process LALR compiler rather than liblime_parser.a constant weak subprocess stub. This is the cc-free compose primitive Track B uses to merge extension grammars into the base snapshot. Verified (compose-ruleno probe, since removed): recompiling the base grammar source in-process is rule-stable (nrule 3612 -> 3612), and an appended extension rule lands at the next index (3612 -> 3613). That fixes the composed host-reduce dispatch: ruleno < base_nrule routes to base_yyHostReduce, ruleno >= base_nrule routes to the extension callback. Symbol check: lime_compile_grammar_in_process is now T (strong), not W.
…ack B P2)
Replace the Phase 4 Track A subprocess pipeline (fork + lime + cc +
dlopen, sha256 .so cache under PGDATA) with in-process composition.
* pg_grammar_ext_lock_parser() now calls pg_grammar_compose_install():
merge the base grammar source (embedded in the snapshot via
lime -n) with the registered extension fragments and compile the
result to a runtime ParserSnapshot via lime_compile_grammar_in_process
-- no subprocess, no C compiler.
* serialize_extension() emits extension rules with empty action
bodies (a snapshot has no compiled action code) and records each
rule_id in append order; the composed snapshot appends extension
rules after the base grammar rules.
* pushparse_host_reduce() routes a reduce by composed rule number:
base rules (< base_nrule) run the generated base actions via
base_yyHostReduce; extension rules route to their PgGrammarReduceFn
via pg_grammar_ext_resolve_reduce.
* The push-parse path applies the same single-char -> named token
translation (SEMI, LPAREN, ...) the pull parser does in
ascii_to_lime_token, and treats parse_token EOF rc==1 as accept.
* Deleted ~800 lines: run_subprocess_pipeline, resolve_cc, run_program,
ensure_cache_dir, sha256_concat_hex, dlopen, the pg_parser_cache and
gram.h-shim machinery, the base_yyparse_fn pointer swap.
Requires Lime v1.6.2 (composition preserves %first_token).
Verified end-to-end (dummy_grammar_ext loaded, PG_LIME_PUSHPARSE): the
composed in-process snapshot parses SELECT 1+2*3 -> 7, string concat,
aggregates over generate_series, and multi-statement CREATE/INSERT/SELECT
-- all correct, no cc, no subprocess. Default pull path unchanged
(regress/isolation/plpgsql green).
… B P3)
Compose the registered grammar extensions into the parser snapshot in
the postmaster, right after process_shared_preload_libraries() has run
every _PG_init(), instead of lazily on the first parse. Backends inherit
the composed snapshot across fork, so no session pays a first-query
compose cost.
* pg_grammar_ext_prewarm(): composes if any extension registered;
a compose failure is FATAL (a broken extension in
shared_preload_libraries stops startup rather than failing every
backend first parse). No-op when none registered or already locked.
* process_shared_preload_libraries() calls it after marking
preload-done. pg_grammar_ext_lock_parser() remains as the lazy
fallback (and the post-prewarm idempotent no-op).
Verified: with an extension loaded, postmaster start absorbs the
in-process compose (~few seconds for the full SQL grammar) and the first
client query measures ~0.5 ms -- warm, no cold-start latency. This
replaces the old ~9 s cc-pipeline first-parse stall.
…ack B P4 tier 0)
Make a registered extension keyword scan as its own token instead of
IDENT, by resolving the keyword name to its external code in the
composed snapshot via Lime v1.7.0 lime_snapshot_token_code().
* pg_grammar_ext_foreach_token() enumerates each registered
extension token (name, lexeme, category).
* After compose, parser_pushparse.c resolves each token NAME to its
composed external code (lime_snapshot_token_code) and builds a
lexeme -> code map, published to scan.c via
pg_grammar_ext_keyword_hook. The scanner emits the extension token
code for a matching identifier lexeme.
* Extension token codes are assigned by the in-process recompile and
are not known at scanner build time, so they must be looked up from
the composed snapshot -- not hard-coded.
Requires Lime v1.7.0 (lime_snapshot_token_code).
Verified: with contrib/quel loaded, bare `retrieve` now scans as
K_QUEL_RETRIEVE and reduces the QUEL rule (fires the extension reduce
callback), while base SQL is unchanged. This covers extension keywords
that do NOT collide with a base SQL keyword (retrieve, append).
Colliding lexemes (range/of/is/to/delete/replace, which are also base
SQL keywords) need context-sensitive resolution via the admissibility
oracle and a live ParseContext in the scanner -- a follow-up (tier 1).
Note: lime/parser.h include guard (PARSER_H) collides with PostgreSQL
parser/parser.h, so lime_snapshot_token_code is forward-declared
locally; reported to the Lime team.
Resolve a lexeme that is BOTH a base SQL keyword and an extension
keyword by asking the admissibility oracle which meaning the parser
would accept in its current state, instead of letting the base keyword
table unconditionally shadow the extension.
* The keyword map records, for a colliding extension lexeme, the base
SQL token code (ScanKeywordLookup over the compiled-in keyword
table).
* The push loop is already interleaved (scan one token, parse_token
consumes it, scan the next), so a live ParseContext is available
when each lexeme is classified. pushparse_resolve_collision() asks
parse_context_token_admissible() for the base and extension codes:
only-extension-admissible emits the extension token (a QUEL verb at
statement start, where the base keyword cannot begin a statement);
otherwise the base meaning is kept. Genuine ambiguity (both
admissible, e.g. DELETE) keeps base pending multi-token fork-resolve.
* contrib/quel drops the mangled lexemes for the oracle-resolvable
verbs: range/of/is/to/into/by/replace are now their real spellings.
delete keeps q_delete until fork-resolve lands.
Verified: range/of/is/to/into/by used in BASE SQL contexts keep their
base meaning (IS NULL, EXTRACT ... FROM, GROUP/ORDER BY, GRANT TO,
SELECT INTO, RANGE window frame all correct), while `range of e is emp`
parses as QUEL at statement start. Base SQL unaffected; regress/
isolation/plpgsql green.
…5, Option A) Grammar extensions register from shared_preload_libraries _PG_init, which runs only at postmaster start; the composed snapshot is built once there (pg_grammar_ext_prewarm) and inherited by every backend across fork. A config reload (SIGHUP / pg_ctl reload / pg_reload_conf) re-reads GUCs as usual and does not recompose the grammar -- the registered extension set is fixed for the postmaster lifetime, exactly like every other shared_preload_libraries extension (the libraries themselves cannot hot-load into a running cluster). Not recomposing also keeps in-flight parse trees safe: a RawStmt and its token strings outlive the parse call, so the snapshot they were parsed against must stay valid. Replaces the obsolete Track A lifecycle comment (dlopen teardown, base-miss-only keyword shadowing, mangled QUEL lexemes) with the current contract: in-process compose at prewarm, keyword codes resolved via lime_snapshot_token_code, and oracle-based override for colliding lexemes. Verified: pg_reload_conf() reloads config normally; base SQL and QUEL (real lexemes) both keep parsing correctly across the reload. A future enhancement (recorded) could let a GUC activate/deactivate an already-loaded dialect across a standard config reload, with a refcounted snapshot swap at the raw_parser boundary.
…ive (Track B)
raw_parser() now uses the in-process push parser whenever a composed
grammar snapshot is installed (raw_parser_lime_active()), instead of
requiring the PG_LIME_PUSHPARSE probe env var. With no grammar
extension loaded it keeps using the in-binary static parser at zero
added cost; PG_LIME_PUSHPARSE still forces the push path for A/B testing
plain SQL.
Validated the push path across every RawParseMode and the multi-token
base_yylex filter:
* RAW_PARSE_TYPE_NAME: casts and to_regtype parse correctly.
* RAW_PARSE_PLPGSQL_EXPR/ASSIGN*: plpgsql functions execute correctly.
* FORMAT_LA (JSON ... RETURNING), NOT_LA (NOT IN / NOT BETWEEN),
NULLS_LA (ORDER BY ... NULLS FIRST), WITH_LA (CTE), USCONST/UIDENT
(U&...): all correct. The mode tokens and the filter ride through
base_yylex transparently.
Regression evidence: the full regress suite (245 subtests), isolation
(129), and plpgsql (13) all pass with PG_LIME_PUSHPARSE forced -- i.e.
every query in PostgreSqL consts regression coverage parsed in-process via the
Lime push parser + host-reduce produces identical results to the static
parser.
…(Track B)
DELETE is the one verb that legitimately begins a statement in both
grammars: base SQL DELETE FROM ... and QUEL delete e where .... At
statement start the admissibility oracle finds both readings valid, so
it cannot settle the collision alone -- but the two diverge at the very
next token (base DELETE is always followed by FROM; QUEL delete by the
relation-variable identifier).
* pushparse_resolve_collision() now reports the both-admissible case
(need_peek) with the two candidate codes instead of silently keeping
base.
* The push loop peeks one token, buffers it (a 1-token pushback so the
peeked token is fed next), and chooses: next == FROM keeps base SQL
DELETE, anything else selects the extension token. Keeping base on
FROM guarantees base SQL DELETE is never stolen.
contrib/quel now uses its real `delete` lexeme; with this, QUEL drops
ALL mangled lexemes (retrieve/append/replace/delete/range/of/is/to/
into/by are all their real spellings).
Verified: DELETE FROM emp WHERE id=1 deletes the row (base), delete e
where e.salary < 1000 reduces the QUEL rule (extension), other QUEL
verbs and base SQL unaffected. Full regress (245) + isolation (129) +
plpgsql (13) pass with the push parser forced -- base DELETE coverage
intact.
… --no-driver (ecpg) mode
…) for Lime scanners)
…p positional map + normalize workaround
Compose one parser snapshot per loaded grammar dialect at postmaster
start instead of a single global snapshot, and let each session pick
which it parses with via a new grammar_dialect GUC. Different backends
of the same server can now parse different grammars concurrently -- e.g.
session A speaking QUEL and session B plain SQL from one postmaster.
Design (Lime-endorsed, no per-session compilation):
* pg_grammar_dialect_prewarm() composes, at postmaster start, the
default "all" snapshot (base SQL + every loaded extension, the prior
single-snapshot behaviour) AND one isolated snapshot per distinct
extension/dialect name (base SQL + only that dialect's rules). All
snapshots are inherited by every backend across fork and are
read-only during a parse (shared safely; Lime TSan-proven).
* grammar_dialect (PGC_USERSET string): ''/'all' -> the default "all"
grammar; 'none' -> base SQL only (fast static parser); a registered
dialect name -> that dialect's isolated snapshot. raw_parser()
selects the active bundle at the parse boundary (a safe swap point:
no in-flight parse tree yet references a snapshot).
* The previously-global reduce-dispatch and scanner-keyword state is
now per-snapshot: each DialectBundle owns its own ruleno->rule_id map
(resolved by stable rule identity, Lime v1.10.0) and lexeme->token
map. The host-reduce dispatcher and keyword hook resolve against the
backend's ACTIVE bundle, not one global map. Rules/tokens belonging
to other dialects self-filter (identity/name lookup misses a
snapshot that lacks them).
The compose-time conflict gate (refuse nconflict>0) and the identity-
based reduce dispatch are preserved. The default no-extension path is
unchanged and stays on the static parser.
Adds contrib/quel/t/002_per_session.pl: two concurrent background_psql
sessions, one grammar_dialect=quel (parses QUEL) and one
grammar_dialect=none (rejects the same QUEL statement, parses base SQL),
proving per-session parsers on one postmaster. All grammar-ext suites
(quel, grammar_ext_overlap, grammar_ext_compose, upsert) and core
regress/isolation/plpgsql pass; 0 warnings.
When a grammar extension is loaded, raw_parser() drives Lime's push
parser over a composed snapshot, running base-grammar reduce actions
through the generated base_yyHostReduce wrapper. Every query on this
path parsed ~2.0-2.6x slower than the AOT static base_yyparse path.
perf record on a PG_LIME_PUSHPARSE=1 backend showed the cost was NOT
grammar composition but the wrapper itself: base_yyHostReduce was 76%
of parse time, of which memset was 63%. The wrapper zeroes a full
stack yyParser on every reduce, and yyParser embeds yystk0[YYSTACKDEPTH]
(the parser's ~1.6 KB initial stack) which no reduce action ever reads
-- only the driver's base_yyparse/yy_shift/yy_reduce touch the stack.
It also zeroes the whole yystk[YYNRHS_MAX+2] scratch (23 entries) when
only yystk[0..nrhs] are used.
The wrapper is emitted by the pinned Lime generator into gram.c, so fix
it in src/tools/pglime (which already owns codegen) right after lime
writes the file:
- zero only the yyParser header (bytes before the last member yystk0)
instead of the whole struct;
- zero only yystk[0..nrhs] instead of all 23 scratch slots.
The rewrite errors out loudly if the generator stops emitting the known
line, so a Lime upgrade cannot silently drop the fix.
Also cache getenv("PG_LIME_PUSHPARSE") once (it showed at ~4% of
push-path time when read per query).
Results (ns/parse, 200k iters, medians; static base_yyparse unchanged):
query static push before push after penalty before -> after
sel1 455 1450 815 2.4x -> 1.8x
join 3420 8000 5610 2.2x -> 1.65x
cte 3050 8300 5220 2.6x -> 1.7x
ins 2255 4900 3465 2.0x -> 1.54x
The residual penalty is Lime's push-interpreter engine (parse_engine_step
/ reduce, ~28% self) in the pinned liblime_parser runtime, plus the
irreducible per-reduce wrapper setup; both are outside this tree.
Correctness: change is location-neutral (proven: a pristine push build
emits the identical error-cursor column). Core regress (static) 245/245
and the grammar-ext suites quel/grammar_ext_overlap/grammar_ext_compose/
upsert (113 subtests, which exercise base_yyHostReduce for all base SQL)
all pass. ninja is warning-clean.
… + composed-push perf
There was a problem hiding this comment.
🔍 OCR found 273 issue(s).
- 25 inline, 248 in summary (inline capped at 25)
📄 contrib/quel/quel_rangetab.c
The header block describes speculative/future behavior rather than what the code does now ("called by xact_handler on transaction abort if we want stricter scoping", "reset is currently only on backend start"), which violates the comment-accuracy discipline (comments describe present behavior; no aspirational prose). Trim to what the code actually does.
📄 .github/workflows/windows-dependencies.yml (L411-L415)
Fragile cross-job artifact wiring: this step hardcodes the artifact name zlib-1.3.1-win64. If the zlib job's matrix version changes (it lives independently in build-zlib's strategy), this download silently references a non-existent artifact and the libxml2 build breaks. Derive the zlib version from a single source (the matrix output) rather than a hardcoded literal.
📄 .github/workflows/windows-dependencies.yml (L504-L504)
create-bundle runs whenever ANY single dependency job succeeds (build-openssl.result == 'success' || build-zlib ... || build-libxml2 ...). Combined with the missing build jobs for other manifest deps, this uploads a partial postgresql-deps-bundle-win64 that the summary still presents as the canonical bundle, with no way for consumers to know it is incomplete. Gate bundle creation on all required dependencies succeeding, or validate bundle completeness against the requested matrix.
📄 contrib/cube/cubeparse_driver.c (L258-L261)
The final end-of-input token is pushed to the parser unconditionally, even after a grammar action executed YYABORT (which only sets extra->aborted = true). Unlike token emission in cube_emit_cb, this call is not gated on extra->aborted. After a soft-error action (e.g. "Different point dimensions", "cube cannot have more than N dimensions") has already called errsave() and aborted, feeding the end token can drive the parser into its %syntax_error/%parse_failure block, which calls cube_yyerror() again. Combined with the missing soft-error guard in cube_yyerror (see other comment), this overwrites the specific error message with a generic "syntax error". Gate the final push on !extra.aborted, matching the emit-loop's own abort check. (high confidence)
💡 Suggested change
Before:
CubeLexFeedEOF(lex, cube_emit_cb, &ctx);
CubeLexFree(lex, pfree);
cube_yy(s->parser, 0, zero_yylval, &extra);
After:
CubeLexFeedEOF(lex, cube_emit_cb, &ctx);
CubeLexFree(lex, pfree);
if (!extra.aborted)
cube_yy(s->parser, 0, zero_yylval, &extra);
📄 contrib/cube/cubeparse_driver.c (L151-L155)
cube_yyerror calls errsave() unconditionally, without first checking whether a soft error was already recorded on escontext. The sibling driver seg_yyerror (contrib/seg/segparse_driver.c) guards with if (SOFT_ERROR_OCCURRED(escontext)) return; for exactly this reason. A cube grammar action can call errsave() with a specific errdetail (e.g. "Different point dimensions in (%s) and (%s).") and then YYABORT; if the parser subsequently reaches %syntax_error/%parse_failure, cube_yyerror runs again and, when details_wanted is set, clobbers the precise message with the generic "invalid input syntax for cube ... syntax error". Add the same SOFT_ERROR_OCCURRED(escontext) early return. (high confidence)
💡 Suggested change
Before:
CubeYyScanner *s = (CubeYyScanner *) yyscanner;
if (s->yytext.len == 0)
{
errsave(escontext,
After:
CubeYyScanner *s = (CubeYyScanner *) yyscanner;
if (SOFT_ERROR_OCCURRED(escontext))
return;
if (s->yytext.len == 0)
{
errsave(escontext,
📄 contrib/cube/cubeparse_driver.c (L261-L265)
cube_yyparse returns 0 (success) even when extra.aborted is true. On a soft-error abort with an ErrorSaveContext, the grammar action left *result unset while recording the error via errsave; the caller cube_in then does PG_RETURN_NDBOX_P(result) on an uninitialized result. This is safe only because the caller is expected to check SOFT_ERROR_OCCURRED on the escontext, but returning 0 (success) contradicts the bison-era contract where a failed parse returned nonzero. Return nonzero when extra.aborted is set so the return value stays meaningful and consistent with the flex/bison version this replaces. (moderate confidence)
💡 Suggested change
Before:
cube_yy(s->parser, 0, zero_yylval, &extra);
cube_yyFree(s->parser, pfree);
return 0;
}
After:
cube_yy(s->parser, 0, zero_yylval, &extra);
cube_yyFree(s->parser, pfree);
return extra.aborted ? 1 : 0;
}
📄 contrib/cube/cubeparse_driver.c (L32-L33)
The file header lists the scanner-lex symbols pulled in from cubescan_lex.h (CubeLexer, CubeLexAlloc, CubeLexFeedBytes, CubeLexFree, CUBE_LEX_OK) but omits CubeLexFeedEOF, which is used at the end of cube_yyparse. Add it to the include comment to keep the comment accurate to what the code actually uses. (high confidence)
💡 Suggested change
Before:
#include "cubescan_lex.h" /* CubeLexer, CubeLexAlloc, CubeLexFeedBytes,
* CubeLexFree, CUBE_LEX_OK */
After:
#include "cubescan_lex.h" /* CubeLexer, CubeLexAlloc, CubeLexFeedBytes,
* CubeLexFeedEOF, CubeLexFree,
* CUBE_LEX_OK */
📄 contrib/pg_plan_advice/pgpa_parser_yytype.h (L23-L25)
Duplicate typedef of the same names under C99. pgpa_ast.h already defines full typedef struct pgpa_advice_item { ... } pgpa_advice_item; (and pgpa_advice_target, pgpa_index_target). Both pgpa_parser_driver.c (includes at lines 32-33) and pgpa_parser.lime (lines 48-49) include pgpa_ast.h and this header in the same translation unit, so each of these three names is typedef'd twice. Redefining a typedef name to the same type is only permitted since C11 (6.7/3); under the tree's C99 baseline it's a constraint violation and errors on strict compilers (e.g. gcc -std=c99 -pedantic-errors, older MSVC) — a portability regression. The comment's stated rationale ("so the union compiles without pulling in pgpa_ast.h") does not hold, since the actual consumers pull in both headers together. Either drop these forward typedefs (the union only needs pointers, and pgpa_ast.h is available), or make the union reference struct pgpa_advice_item * etc. directly without a conflicting typedef. Confidence: high.
💡 Suggested change
Before:
typedef struct pgpa_advice_item pgpa_advice_item;
typedef struct pgpa_advice_target pgpa_advice_target;
typedef struct pgpa_index_target pgpa_index_target;
After:
/*
* The union only needs pointers; use struct tags directly to avoid
* redefining the typedef names that pgpa_ast.h already provides.
*/
📄 contrib/quel/quel--1.0.sql (L20-L21)
Stale/aspirational comment that contradicts the shipped code (moderate confidence). This COMMENT claims feature reachability "is gated on Track B scanner-table updates which are not yet wired." But quel.c contradicts this: the header says "Track B Phase 1 status (LIVE)", and the runtime status string built in _PG_init() reports "keyword override live" and "RETRIEVE / REPLACE / APPEND / DELETE build real PG parse trees that flow through parse_analyze + planner + executor". The TAP test also asserts the success path ("quel registered: 10 tokens"). Per the review rules, comments must describe current behavior, not use "not yet"/"future" tense for behavior that already shipped. Drop the "not yet wired" clause and describe what the function actually returns.
📄 contrib/pg_plan_advice/pgpa_parser_driver.c (L39-L46)
Fragile, non-DRY coupling (moderate confidence). struct GramParseExtra is hand-copied here and must match the definition generated into pgpa_parser.lime (lines 65-71), which declares yyscan_t yyscanner rather than void *yyscanner. They are layout-compatible today only because yyscan_t is typedef void *. If the converter's emitted struct ever changes field order/type/count, pgpa_yy() and pgpa_yy_drain() will read/write mismatched offsets, silently corrupting memory with no compile-time diagnostic. Emit this struct into a shared header included by both the driver and the generated parser, so there is a single source of truth, instead of relying on a comment that says the layout "matches".
📄 contrib/pg_plan_advice/pgpa_parser_driver.c (L187-L190)
Dead work: EmitContext.errmsg is written but never read. The actual diagnostic surfaced by pgpa_yyerror comes solely from s->lex_errmsg (set on the next line). The initStringInfo/resetStringInfo/appendStringInfoString on ctx->errmsg here (and the errmsg field itself, plus its ctx.errmsg.data = NULL init in pgpa_scanner_init) have no consumer. Per the minimal-diff discipline, drop the unused errmsg field and these three lines.
💡 Suggested change
Before:
if (ctx->errmsg.data == NULL)
initStringInfo(&ctx->errmsg);
resetStringInfo(&ctx->errmsg);
appendStringInfoString(&ctx->errmsg, "integer out of range");
After:
ctx->s->lex_errmsg = pstrdup("integer out of range");
📄 contrib/pg_plan_advice/pgpa_parser_driver.c (L294-L301)
Empty conditional. ctx.had_error gates only a comment; the block does nothing. Since the message/text are already stashed on the scanner in the emit callback and surfaced later by pgpa_yyerror, this whole if is dead code. Remove it (and, if had_error has no other reader, drop the field too).
📄 contrib/pg_plan_advice/pgpa_parser_driver.c (L11-L13)
Comment drift (low confidence). The documented interface here does not match the definitions below: pgpa_yylex/pgpa_yyerror are defined with char **err/char **parse_error_msg_p and void *yyscanner, not the yyscan_t scanner shown. Also the union YYSTYPE *lval wording differs from the actual YYSTYPE *. Comments must describe the code as it is now; align this block with the real signatures (or drop the redundant restatement, since the prototypes live in pgpa_ast.h).
📄 contrib/pg_plan_advice/pgpa_parser_driver.c (L371-L371)
Ephemeral development jargon in an enduring comment. "Phase 2j/3 pattern" refers to a private development timeline that means nothing to a future reader of committed code; per the comment-accuracy rules, comments should explain the enduring reason (Lime's push model defers the default reduce until the next lookahead token). Drop the phase label and keep only the substantive explanation that already follows.
💡 Suggested change
Before:
* Drain pending default reduces eagerly (Phase 2j/3 pattern). Without
After:
* Drain pending default reduces eagerly. Without
📄 contrib/quel/quel.c (L232-L236)
Correctness bug (high confidence): four rules registered below in quel_rules[] carry labels that no branch in quel_reduce() matches: "retrieve (bare)", "retrieve into IDENT", "replace IDENT", and "append to IDENT". For those productions quel_reduce falls through to the default *(void **) lhs_out = NULL, so a successfully-parsed QUEL statement of those shapes produces a NULL statement node. That NULL is forwarded unchanged by the quel_stmt/stmt forwarder rules and ends up in the list raw_parser_lime_pushparse() returns, which parse_analyze()/planner will dereference -> crash. Either add builders for these labels or remove the rules so the input is a syntax error instead of a NULL tree. This label-string dispatch also has no compile-time link between quel_rules[] and quel_reduce(); any future drift is undetected.
📄 contrib/quel/quel.c (L224-L228)
Missing arity guard before indexing rhs_values (high confidence). quel_reduce dispatches purely on the string label and each builder then reads fixed rhs_values[] indices (e.g. rhs_values[7] in quel_build_retrieve_into_where). If the label ever mismatches the rule's actual RHS shape (a very easy edit-time mistake given the two lists are kept in sync only by hand), the builder reads out of bounds. Consider asserting the expected nrhs per label in this dispatcher, or driving both the rule table and the dispatch from a single source of truth so drift is caught at compile time.
📄 contrib/quel/quel.c (L47-L53)
Comment/behavior drift (medium). This banner asserts the file registers only "bare-keyword rules (quel_retrieve_stmt ::= K_QUEL_RETRIEVE.)" and that "real QUEL queries parse to K_QUEL_RETRIEVE then fail at the next token until the grammar is extended." That is false for the code below: quel_rules[] registers full RHS shapes (attr_list, WHERE a_expr, BY sortby_list, INTO, UNIQUE) and quel_reduce builds real SelectStmt/UpdateStmt/InsertStmt/DeleteStmt nodes. Aspirational/stale comments like this mislead reviewers; update to describe what the code does now.
📄 contrib/quel/quel.c (L33-L35)
Comment/behavior drift (medium). The "Track A scope" block claims this file registers "six new statement-level rules off stmt with reduce callbacks" and that the parser dispatches "via pg_grammar_ext_dispatch_reduce()". Neither matches the actual code: quel_rules[] registers ~30 rules across stmt/quel_stmt/explainableStmt/quel_retrieve_stmt/quel_attr* etc., and dispatch happens by ruleno identity through pg_grammar_ext_reduce_by_ruleno (per parser_extension.c), not the subprocess dispatch_reduce path this comment describes. Update to reflect the in-process compose path.
📄 contrib/quel/quel.c (L679-L683)
Unverified user-facing claims in the status string (medium). quel_extension_status() returns this message asserting concrete behavior -- "RETRIEVE / REPLACE / APPEND / DELETE build real PG parse trees that flow through parse_analyze + planner + executor and return identical results to equivalent SQL", "multi-tuple-variable joins via FROM synthesis from rangetab", "FROM clause pruned to only tuple-vars referenced by the query". Given the bare/into/replace-bare/append-bare shapes above return NULL trees (see earlier comment), the blanket "RETRIEVE/REPLACE/APPEND/DELETE build real PG parse trees" claim is not universally true. Trim these to what is actually implemented and covered by tests, since users read this via the SQL function.
📄 contrib/quel/quel.c (L674-L675)
Lifetime hazard for quel_lime_text (medium). quel_status_msg is correctly psprintf'd in TopMemoryContext, but quel_lime_text is assigned the raw return of pg_grammar_ext_get_serialized_lime(ext) with no context switch. That pointer is owned by the extension handle's MemoryContext (ext->context, a child of TopMemoryContext per parser_extension.c), so it survives here -- but only as long as ext is never unregistered. On the success path ext is retained (never freed), which is fine; however this coupling is implicit. Add a brief comment stating quel_lime_text is valid only because ext (and thus ext->context) is intentionally never unregistered on the success path, so a future refactor that frees ext doesn't turn this into a use-after-free in quel_serialized_lime().
📄 contrib/quel/quel.c (L705-L709)
Potential small leak of err in the postmaster (low). On the failure path, err (a palloc'd string from pg_grammar_ext_register per its header contract) is embedded into psprintf and into the WARNING, but never pfree'd; it was allocated in CurrentMemoryContext at _PG_init time. It is a one-time startup leak, but for cleanliness pfree(err) after use.
📄 contrib/quel/quel_grammar.h (L117-L118)
Large blocks of these declared builders are dead API: quel_build_retrieve, quel_build_replace, quel_build_append, quel_build_delete, quel_build_create, quel_build_destroy, quel_build_copy, quel_build_define_view, quel_build_remove_view, quel_build_index, and quel_build_help are declared here and defined as stubs in quel_grammar.c, but the dispatch in quel_reduce() (quel.c) never calls any of them -- it only routes to the _simple/_where/_unique/_into/*_by variants. This is speculative scaffolding (YAGNI); remove the unused declarations and their stub definitions. Keep only what the dispatch actually invokes.
📄 contrib/quel/quel_grammar.h (L153-L153)
quel_resolve_tuple_var has no callers anywhere in the tree (it appears only inside a comment in quel_grammar.c). Additionally, this doc comment is self-contradictory: the implementation calls ereport(ERROR) on an unbound tuple variable, which does not return -- it never "Returns NULL". Remove this dead declaration/definition, or if it is intended to stay, fix the contract to state it raises ERROR and does not return.
📄 contrib/quel/quel_grammar.h (L160-L161)
quel_make_column_ref is declared and defined but never called anywhere in the tree. Dead code -- remove the declaration and its definition.
📄 contrib/quel/quel_grammar.h (L169-L169)
quel_implied_from_clause has no callers (only referenced in a comment). Worse, its body is duplicated verbatim by the static quel_synthesize_from() in quel_grammar.c, which is the function actually used by the RETRIEVE builders. This is dead code plus a DRY violation -- drop quel_implied_from_clause entirely and keep the single static helper.
📄 contrib/quel/quel_grammar.h (L71-L72)
The header comment references a "dispatch trampoline in quel.c (quel_dispatch)", but no function named quel_dispatch exists; dispatch is done by quel_reduce(). Stale/inaccurate comment -- fix the name so the doc matches the code.
💡 Suggested change
Before:
* The dispatch trampoline in quel.c (quel_dispatch) takes the rule
* id and forwards to the matching builder below.
After:
* The dispatch trampoline in quel.c (quel_reduce) takes the rule
* label and forwards to the matching builder below.
📄 contrib/quel/quel_grammar.c (L223-L225)
Dead code: this stub is never dispatched. quel.c's reduce switch only registers the "full" builders (quel_build_retrieve_simple/_where/_unique/_into/_by, etc.), never quel_build_retrieve. Same applies to quel_build_replace, quel_build_append, quel_build_delete, quel_build_create, quel_build_destroy, quel_build_copy, quel_build_define_view, quel_build_remove_view, quel_build_index and quel_build_help below -- none are referenced in quel.c. These builders allocate empty/uninitialized parse-tree nodes (e.g. UpdateStmt with NULL relation, DropStmt with empty object list) that would crash the analyzer if ever fired. Per YAGNI/minimal-diff discipline, remove all of this unwired scaffolding; it is a footgun and pure dead code. (high confidence)
📄 contrib/quel/quel_grammar.c (L172-L181)
WIP scaffolding comment references an internal, uncommitted planning document (.agent/notes/quel-full-implementation-plan.md) and describes these functions as "sketches" and "stubs that build the right SHAPE of node but pull concrete values from a TODO," gated on "Phase A of the plan." Internal-path references and forward-looking phase plans do not belong in committed source; this signals unfinished work bundled into a parse path. Remove. (high confidence)
📄 contrib/quel/quel_grammar.c (L143-L143)
quel_implied_from_clause is dead code: its only reference in the tree is a comment in quel_build_retrieve, and it is nearly a verbatim duplicate of quel_synthesize_from (both walk target_list + where_clause, look up rangetab, build the RangeVar list). This violates DRY. Remove quel_implied_from_clause (and its extern declaration in quel_grammar.h). (high confidence)
📄 contrib/quel/quel_grammar.c (L68-L70)
Error-position handling violates PostgreSQL conventions. Do not embed "at character %d" (location + 1) in errmsg text; report the cursor via parser_errposition(pstate, location) as a separate errposition. Also: user-facing strings must be wrapped in _() for translation, and the errhint reads as a complete sentence which is fine, but the errmsg here is otherwise correct (lowercase, no trailing period). Fix the errposition handling. (medium confidence)
📄 contrib/quel/quel_grammar.c (L803-L806)
quel_resolve_target_relation can return NULL (when name is NULL), and this NULL is stored directly into upd->relation. An UpdateStmt/DeleteStmt/InsertStmt with a NULL relation is dereferenced by the analyzer (transformUpdateStmt et al.) and will crash rather than emit a clean user error. Builders that hit a missing target should ereport(ERROR, ...) with an appropriate ERRCODE instead of silently propagating NULL into the parse tree. Same pattern in quel_build_replace_where, quel_build_delete_simple, quel_build_delete_where and quel_build_append_full. (medium confidence)
📄 contrib/quel/quel_grammar.c (L214-L214)
quel_apply_range mutates session-global state (quel_rangetab_set into TopMemoryContext) as a side effect of a reduce callback during raw parsing. If the surrounding statement is syntactically completed here but later rejected (analysis error, or the raw_parser call is aborted), the RANGE binding has already been committed to the session and cannot be rolled back, leaving stale tuple-var bindings. Consider deferring the state mutation to statement execution, or documenting this non-transactional behavior explicitly. (medium confidence)
📄 contrib/quel/quel_rangetab.c (L53-L53)
Comment-vs-code drift and a case-sensitivity correctness bug. The header comment states the table is "indexed by lowercased tuple-variable name", and quel_grammar.h documents QuelRangeEntry.name as "(lowercase)", but no lowercasing happens anywhere. slot_for() hashes the raw name and strcmp()s it verbatim; quel_apply_range() in quel_grammar.c passes the raw IDENT straight to quel_rangetab_set(), and quel_resolve_tuple_var()/quel_implied_from_clause() pass raw names to lookup. Consequently RANGE OF E IS emp followed by e.col will not resolve, contradicting QUEL identifier folding. Either fold to lowercase here (e.g. via pnstrdup + downcase, or asc_tolower) or fix the comments to state the table is case-sensitive and require callers to fold.
📄 contrib/quel/quel_rangetab.c (L178-L178)
Dead/speculative field. lineno is only ever written as 0 here and echoed back verbatim in quel_rangetab_iterate(); no code path ever stores a real line number, yet quel_grammar.h documents it as "line where RANGE was registered". This is unused state (YAGNI). Remove the field from QuelRangeSlot/QuelRangeEntry (and the iterate copy), or actually populate it from the RANGE reduction.
📄 contrib/quel/quel_rangetab.c (L138-L143)
On growth the table is rebuilt by reinserting via the public quel_rangetab_set() after setting g_count = 0, while old_slots lives in TopMemoryContext (long-lived, not reset on error). If pstrdup() inside the reinsert loop hits OOM and ereport(ERROR)s, old_slots is leaked into TopMemoryContext and the table is left partially populated with a wrong g_count. Since this is session-lifetime state that is not torn down by a subtransaction/context reset, the leak persists. Consider allocating the working copy in a short-lived context, or reordering so the table is only swapped in after all reinserts succeed.
📄 contrib/quel/quel_rangetab.c (L46-L48)
Naming convention: file-scope statics use a g_ prefix, which is not the PostgreSQL style. Prefer a subsystem-prefixed snake_case name (e.g. quel_range_slots / quel_range_capacity / quel_range_count) consistent with the rest of the extension.
📄 contrib/quel/t/001_quel.pl (L28-L37)
This helper reinvents slurp_file(), which is already exported by PostgreSQL::Test::Utils (imported on line 25). slurp_file handles binmode/encoding consistently across platforms; the hand-rolled two-arg-less open here omits binmode, so on Windows CRLF translation can perturb regex matches on the log text. Drop log_text and call slurp_file($node->logfile) directly. (Confidence: high)
💡 Suggested change
Before:
sub log_text
{
my ($node) = @_;
my $logfile = $node->logfile;
open(my $fh, '<', $logfile) or die "cannot read $logfile: $!";
local $/;
my $text = <$fh>;
close $fh;
return $text;
}
After:
# Use slurp_file() from PostgreSQL::Test::Utils to read the log.
📄 contrib/quel/t/001_quel.pl (L62-L65)
These two unlike assertions give false confidence: they pass vacuously whenever those strings are never emitted anywhere for any reason (e.g. a code path that was never reached, or a log message that was reworded), so they do not actually prove the grammar was composed in-process rather than via a subprocess/cc. Absence-of-string is not evidence of the intended behavior. Prefer asserting the positive fact via the introspection functions (which you already use) instead of scraping for the negative. (Confidence: moderate)
📄 contrib/seg/seg_gram_yytype.h (L31-L33)
The comment claims RANGE and PLUMIN are text-bearing tokens that read .text, but in segparse.lime these two tokens are only ever used bare (no (X) capture) and never read a semantic value. Only SEGFLOAT and EXTENSION actually consume .text. Trim the list to match the grammar. (low confidence — cosmetic/doc-only; the driver harmlessly sets .text for every token.)
💡 Suggested change
Before:
* YYSTYPE union. text-bearing tokens (SEGFLOAT, RANGE, PLUMIN,
* EXTENSION) read .text; the boundary and deviation non-terminals
* populate .bnd.
After:
* YYSTYPE union. text-bearing tokens (SEGFLOAT, EXTENSION) read
* .text; the boundary and deviation non-terminals populate .bnd.
📄 contrib/quel/t/002_per_session.pl (L86-L87)
Reading the internal {stderr} buffer here is acceptable (it is the only way to inspect the error text, and other in-tree TAP tests do the same), but BackgroundPsql::query() never clears {stderr} after a query -- it only strips the query-separator banner (see BackgroundPsql.pm; contrast the {stdout} clear). Every sanctioned in-tree use of $session->{stderr} (e.g. src/test/modules/test_aio/t/001_aio.pl, src/test/recovery/t/037_invalid_database.pl) resets it to '' right after reading. This test does not, so the error text accumulates on the object. It happens to be harmless today only because no further query/query_safe runs on $sess_base before quit -- but the very next query_safe($sess_base, ...) would misfire, since query_safe dies whenever {stderr} ne "". Add $sess_base->{stderr} = ''; after this assertion to follow the established pattern and avoid a latent footgun.
💡 Suggested change
Before:
like($sess_base->{stderr}, qr/syntax error at or near "range"/,
'session B (none): error is a base-SQL syntax error on the QUEL verb');
After:
like($sess_base->{stderr}, qr/syntax error at or near "range"/,
'session B (none): error is a base-SQL syntax error on the QUEL verb');
$sess_base->{stderr} = '';
📄 contrib/upsert/t/001_upsert.pl (L108-L112)
Windows portability bug: this manual open/slurp of the postmaster log fails on Windows. The postmaster keeps the log file open without the FILE_SHARE_DELETE share flag, so a second reader open fails there. This is exactly why PostgreSQL::Test::Utils::slurp_file uses the Win32 createFile(..., "rwd") path instead of a plain open. Combined with use warnings FATAL => 'all', any read hiccup also becomes a hard abort. Use the framework helper instead.
my $logs = slurp_file($node->logfile);
(slurp_file is exported by PostgreSQL::Test::Utils, already imported above.) Confidence: high.
💡 Suggested change
Before:
my $logfile = $node->logfile;
open(my $fh, '<', $logfile) or die "cannot read $logfile: $!";
local $/;
my $logs = <$fh>;
close $fh;
After:
my $logs = slurp_file($node->logfile);
📄 contrib/upsert/t/001_upsert.pl (L91-L92)
Coverage gap: every case here is a happy path. For a grammar extension whose entire correctness claim is "UPSERT lowers faithfully to INSERT ... ON CONFLICT", the error/edge paths are what actually prove fidelity and are missing:
- ON (...) naming a column with no matching unique index/constraint (INSERT ... ON CONFLICT raises a specific error; UPSERT must raise the same).
- Multiple conflict columns, e.g. a composite PRIMARY KEY.
- A conflict column that is not among the inserted columns (col->name lookup in upsert_make_update_set).
- VALUES arity mismatch vs the column list.
Without at least the missing-unique-index error case, a regression in the lowering would pass this suite. Confidence: high.
📄 contrib/upsert/t/001_upsert.pl (L117-L118)
This negative assertion is effectively a tautology and gives false assurance. The .so-cache code path was removed entirely from parser_extension.c ("The historical Track A path ... has been removed entirely"), so no code can ever emit a /pg_parser_cache/<sha>.so string; the unlike passes unconditionally regardless of whether compose ran in-process. The positive like on "composing grammar in-process" already proves the in-process path. Either drop this line or, if the intent is to guard against regressing to a subprocess/cc, assert the absence of the actual former markers (cf. contrib/quel/t/001_quel.pl, which uses qr/running lime to rebuild parser/ and a bare qr{/pg_parser_cache/}). Confidence: medium.
📄 contrib/seg/segparse_driver.c (L195-L199)
seg_yyparse unconditionally return 0 and never inspects extra.aborted. The grammar in segparse.lime sets extra->aborted = true via its YYABORT/YYERROR shims (e.g. swapped-boundary check, seg_atof failures), and its own comment states the driver "checks [the flag] after each push and stops feeding tokens." Neither happens here: seg_emit_cb keeps feeding tokens after an abort, and seg_yyparse reports success regardless. This breaks the return-value contract the caller seg_in relies on (if (seg_yyparse(...) != 0) seg_yyerror(...)) and diverges from the original bison parser, which returns 1 on YYABORT/YYERROR. Today this is only masked by the soft-error side channel; in any path where aborted is set without a recorded error it returns a garbage SEG as success. Return non-zero when extra.aborted is set (and stop feeding tokens once aborted).
💡 Suggested change
Before:
seg_yy(s->parser, 0, zero_yylval, &extra);
seg_yyFree(s->parser, pfree);
return 0;
}
After:
seg_yy(s->parser, 0, zero_yylval, &extra);
seg_yyFree(s->parser, pfree);
return extra.aborted ? 1 : 0;
}
📄 contrib/seg/segparse_driver.c (L148-L153)
The emit callback feeds every token to seg_yy without checking ctx->extra->aborted. The grammar's own comment promises the driver "stops feeding tokens" once the abort flag is set; continuing to push tokens after YYABORT/YYERROR can drive the push parser through further reductions on a state the grammar considered failed. Guard the push with the abort flag.
💡 Suggested change
Before:
literal = palloc(len + 1);
memcpy(literal, text, len);
literal[len] = '\0';
yylval.text = literal;
seg_yy(s->parser, token, yylval, ctx->extra);
After:
if (ctx->extra->aborted)
return;
literal = palloc(len + 1);
memcpy(literal, text, len);
literal[len] = '\0';
yylval.text = literal;
seg_yy(s->parser, token, yylval, ctx->extra);
📄 contrib/seg/segparse_driver.c (L172-L174)
s->parser and lex are palloc'd but freed manually with seg_yyFree/SegLexFree only on the normal return paths. If seg_yy() triggers a hard-error errsave (=> ereport(ERROR)) or a CHECK_FOR_INTERRUPTS longjmps out of the parse, both frees are skipped and the allocations leak in the current MemoryContext. Since these use palloc (not malloc), the manual frees are only needed for a long-lived context; if the context is short-lived they are redundant churn. Either rely on the MemoryContext for cleanup (drop the manual frees) or protect the allocations with PG_TRY/PG_FINALLY so error paths free them too.
📄 contrib/upsert/upsert.c (L270-L274)
Register-failure path leaks resources and leaves the extension half-registered. On failure this only emits a WARNING; unlike the sibling contrib/quel/quel.c, it never calls pg_grammar_ext_unregister(ext). Per parser_extension.c, the pending compose queue holds borrowed pointers into the extension's memory context, so failing to unregister leaks the TopMemoryContext-child context and can leave a dangling fragment. Add the cleanup call, matching quel.
💡 Suggested change
Before:
if (!pg_grammar_ext_register(ext, &err))
ereport(WARNING,
(errmsg("upsert: register() failed: %s",
err ? err : "(no detail)")));
else
After:
if (!pg_grammar_ext_register(ext, &err))
{
ereport(WARNING,
(errmsg("upsert: register() failed: %s",
err ? err : "(no detail)")));
pg_grammar_ext_unregister(ext);
}
else
📄 flake.nix (L73-L73)
environment.localBinInPath = true; is a NixOS module option, not a valid per-system flake output attribute. It is emitted inside flake-utils.lib.eachDefaultSystem, alongside formatter, devShells, and packages. The standard flake output schema for per-system attributes has no environment.* slot, so this line is silently ignored and has no effect here (it appears copied from a NixOS/home-manager module context). Remove it as dead/misleading configuration.
📄 pg-aliases.sh (L1-L1)
This entire file is out of scope for a PostgreSQL patch. Searching the rest of the change shows nothing references pg-aliases.sh or any of its env vars (PG_BUILD_DIR, PG_SOURCE_DIR, PG_INSTALL_DIR), and it is unrelated to the parser/lexer refactor the other files implement. It is a personal developer-convenience script placed at the repo root (not src/tools/), full of local assumptions (trash, compdb, a fixed port, custom env vars). Unrelated files inflating a diff are a top rejection reason on -hackers. This file should not be part of the patch at all. (high confidence)
📄 pg-aliases.sh (L82-L82)
Destructive rm -rf on an unguarded variable. If PG_DATA_DIR is unset/empty, trash "" fails and the fallback becomes rm -rf ""; worse, a partially-set or wrong value silently wipes an unintended directory. There is no non-empty guard before deletion. Same footgun applies to pg-full-clean (rm -rf "$PG_BUILD_DIR" "$PG_INSTALL_DIR") and pg_clean_for_compiler (rm -rf "$build_dir"). Guard each destructive path, e.g. [ -n "$PG_DATA_DIR" ] || { echo 'PG_DATA_DIR unset' >&2; return 1; } before deleting. (high confidence)
📄 pg-aliases.sh (L7-L7)
Unquoted expansion of $CC in basename $CC word-splits if CC contains flags/spaces (e.g. CC="ccache gcc"), passing multiple args to basename. Quote it: basename "$CC". (moderate confidence)
💡 Suggested change
Before:
local current_compiler="$(basename $CC)"
After:
local current_compiler="$(basename "$CC")"
📄 pg-aliases.sh (L28-L32)
return 1 inside an alias body only works when the alias is expanded within a sourced/function context. When invoked interactively, bash emits return: can only "return" from a function or sourced script. Since most of this file already uses functions, pg-setup should be a shell function too, for both correctness and consistency. (moderate confidence)
📄 pg-aliases.sh (L191-L194)
The generated wrapper embeds $PG_SOURCE_DIR, $PG_BENCH_DIR, and $bindir unquoted into the heredoc, so any of these paths containing spaces produces a broken postgres wrapper (the valgrind args split incorrectly). Quote the interpolated paths in the emitted script, e.g. --suppressions="$PG_SOURCE_DIR/src/tools/valgrind.supp" and "$bindir/postgres". (moderate confidence)
📄 pg-aliases.sh (L515-L515)
for file in $modified_files iterates an unquoted command substitution, so it word-splits on whitespace and undergoes glob expansion; filenames with spaces break. Same pattern in pg-tidy and pg-spell. Prefer a NUL-safe loop, e.g. git diff --name-only -z ... | while IFS= read -r -d '' file. (moderate confidence)
📄 pg-aliases.sh (L417-L417)
Hardcoded --port=40099 will collide with an existing server or a concurrent test run, causing intermittent failures. Make the port configurable (e.g. --port="${PG_TEST_PORT:-40099}"). (moderate confidence)
📄 pg-aliases.sh (L422-L422)
pg-flame-generate and pg-bench-run are referenced by many aliases/functions here (pg-flame, pg-flame-custom, pg-bench, pg-bench-custom, pg-bench-flame) but are not defined in this file or anywhere else in the change. As shipped, all flame-graph and benchmark commands fail with 'command not found'. Either define these helpers or drop the commands that depend on them. (high confidence)
📄 shell.nix (L721-L723)
The clang+musl shell hardcodes --target=x86_64-linux-musl in CFLAGS/CXXFLAGS/LDFLAGS, but this devShell is exposed via flake-utils.lib.eachDefaultSystem in flake.nix, which advertises multiple systems (e.g. aarch64-linux). On a non-x86_64 host, pkgs.pkgsMusl resolves to the host architecture's musl libraries, so -I${pkgs.pkgsMusl.stdenv.cc.libc}/include / -L${pkgs.pkgsMusl.stdenv.cc.libc}/lib point at aarch64 musl while the compiler is told to target x86_64. This produces a broken, mismatched cross-compile toolchain with no evaluation error. Derive the target triple from the system argument (which is currently accepted by this file but never used) instead of hardcoding x86_64.
📄 src/backend/Makefile (L201-L201)
This change removed syncrep_scanner.c (now a committed source), but left repl_scanner.c on this line — and scan.c on the parser line above (line 199) — even though both underwent the same generated->committed transition in this series (their .gitignore entries were dropped and they now appear as committed ADDED files, with only .o targets in their Makefiles). There is no longer any rule to generate scan.c (see src/backend/parser/Makefile: only scan.o, no scan.c: rule) or repl_scanner.c. In a clean/VPATH build from a tarball, $(MAKE) -C parser ... scan.c and $(MAKE) -C replication ... repl_scanner.c ... will fail with "No rule to make target". These stale targets must also be removed for consistency with the syncrep_scanner.c/guc-file.c removals. Confidence: high.
💡 Suggested change
Before:
$(MAKE) -C replication repl_gram.c repl_gram.h repl_scanner.c syncrep_gram.c syncrep_gram.h
After:
$(MAKE) -C parser gram.c gram.h
$(MAKE) -C bootstrap bootparse.c bootparse.h bootscanner.c
$(MAKE) -C replication repl_gram.c repl_gram.h syncrep_gram.c syncrep_gram.h
📄 src/backend/bootstrap/Makefile (L27-L28)
The Make build is broken and out of sync with meson.build. bootscanner.c (compiled to bootscanner.o in OBJS) does #include "bootscanner_lex.h" and links against BootLexAlloc/BootLexFeedBytes/BootLexFeedEOF/BootLexFree/BOOT_LEX_OK, all of which live in the Lime-generated bootscanner_lex.c/bootscanner_lex.h. meson.build generates those from bootscanner.lex (via lime_lex_cmd) and compiles bootscanner_lex.c into boot_parser_sources. This Makefile has no rule to generate bootscanner_lex.c/bootscanner_lex.h from bootscanner.lex, and never compiles bootscanner_lex.o. A ./configure && make build will fail: bootscanner_lex.h does not exist (no rule to build it) and the BootLex* symbols are undefined at link time. Add a generation rule (e.g. bootscanner_lex.c bootscanner_lex.h: bootscanner.lex invoking Lime's lexer mode) and add bootscanner_lex.o to OBJS, matching meson.build. (high confidence)
📄 src/backend/bootstrap/Makefile (L31-L31)
This forced-dependency line omits bootscanner_lex.h, which bootscanner.c includes. Once the generation rule for the Lime lexer exists, bootscanner.o must also depend on bootscanner_lex.h; otherwise a parallel (make -j) build can compile bootscanner.o before the header is generated, producing an intermittent failure.
💡 Suggested change
Before:
bootparse.o bootscanner.o: bootparse.h boot_gram_yytype.h
After:
bootparse.o bootscanner.o: bootparse.h boot_gram_yytype.h bootscanner_lex.h
📄 src/backend/bootstrap/Makefile (L36-L36)
The clean rule removes bootparse.out but not the Lime-generated bootscanner_lex.c / bootscanner_lex.h (produced from bootscanner.lex per meson.build). Once the corresponding Make generation rule is added, these artifacts must be removed here too, otherwise make clean leaves generated files behind. (moderate confidence, contingent on adding the lexer generation rule)
💡 Suggested change
Before:
bootparse.out
After:
bootparse.out \
bootscanner_lex.c \
bootscanner_lex.h
📄 src/backend/bootstrap/boot_gram_yytype.h (L11-L14)
This comment's rationale is stale/inaccurate. It claims the union is kept identical "so that boot_yylex() retains the signature int boot_yylex(union YYSTYPE *, yyscan_t)" and that "boot_yylex() takes union YYSTYPE *". But this change moves the bootstrap parser to a push model: bootscanner.c's own header states "boot_yylex no longer exists -- the parser is fed by the driver loop directly, not by a yylex() pull callback." No boot_yylex definition exists in the new code (only a leftover declaration in bootstrap.h). Comments must describe present behavior; citing a removed function as the reason to preserve the tag/name is misleading. Reword to reference the actual constraint (e.g. the %token_type {YYSTYPE} in bootparse.lime and the boot_yy() push entry point that consumes YYSTYPE by value). (moderate confidence)
📄 src/backend/bootstrap/boot_gram_yytype.h (L33-L35)
Same stale claim as above: boot_yylex() does not exist in the new push-parser design, so "boot_yylex() takes union YYSTYPE *" no longer justifies keeping the tag and name. Update this to describe why the type is named YYSTYPE now (consumed by the Lime-generated boot_yy() and the emit callback in bootscanner.c), not a nonexistent pull-style lexer. (moderate confidence)
📄 src/backend/jit/llvm/llvmjit.c (L1065-L1069)
This rewrite drops the original handling for a pgextern.-prefixed symbol that has no second dot (e.g. pgextern.foo). strrchr(name, '.') will match the prefix dot itself, so *funcname ends up pointing at foo. The modname length then computes as *funcname - name - strlen("pgextern.") - 1, which for pgextern.foo is 9 - 9 - 1 = -1. Since pnstrdup() takes an unsigned Size, that -1 becomes SIZE_MAX, and strnlen() reads the entire string, so *modname incorrectly captures the funcname instead of being NULL. The old code explicitly handled this via the if (lastdot) / else *modname = NULL; *funcname = pstrdup(name) branch. This is a behavioral regression; restore the missing-second-dot handling (confidence: high).
📄 src/backend/jit/llvm/llvmjit.c (L1070-L1070)
Assert(funcname) asserts the parameter pointer (char **), which is always non-NULL and unrelated to the split result; it therefore checks nothing useful. Worse, if the intent was Assert(*funcname) to guard the strrchr result, it is placed after the (*funcname)++ dereference, so it cannot prevent the NULL dereference it appears meant to catch. Remove this assert or fix the logic to validate the strrchr return value before dereferencing (confidence: high).
📄 src/backend/bootstrap/bootscanner.c (L80-L80)
Error line numbers are always wrong. s->yylineno is incremented once per newline during the full stdin slurp below (line 260), so by the time the lexer/parser runs and calls boot_yyerror, s->yylineno always equals the total number of lines in the input. boot_emit_cb and the newline lex rule (LEX_SKIP()) never update it during tokenizing. Every syntax error will therefore report the last line of input rather than the failing line -- a diagnostic regression versus the flex scanner's per-token line tracking. Track the current line inside boot_emit_cb (e.g. count newlines in each consumed span, or from the lexer's position) so the reported line is meaningful. (high confidence)
📄 src/backend/bootstrap/bootscanner.c (L82-L82)
error_seen is dead scaffolding: it is never set by boot_emit_cb (which uses elog(ERROR) in the default case, not this flag) and never read by boot_yyparse (which relies on BootLexFeedBytes's BOOT_LEX_OK return). The comment describing 'set by emit callback on LEX_ERROR' documents behavior that does not exist. Remove the field and its comment.
📄 src/backend/bootstrap/bootscanner.c (L165-L166)
Comment/code drift: the comment says 'pstrdup of the matched text' but the code uses palloc + memcpy, not pstrdup. Either use pstrndup(text, len) (which is exactly this pattern) or fix the comment to describe what the code does.
📄 src/backend/bootstrap/bootscanner.c (L193-L194)
Comment/code drift: the comment claims the keyword text is obtained via 'pstrdup', but the code uses palloc + memcpy below. Fix the comment (or use pstrndup).
📄 src/backend/bootstrap/bootscanner.c (L152-L158)
This switch body is badly misindented and will not pass pgindent cleanly. case ID: and the other labels carry stray leading tabs; the memcpy/literal[len]/break lines are shifted right; and if (len >= 2 ...) has spurious whitespace between if and (. Run pgindent to normalize before submitting.
📄 src/backend/bootstrap/bootscanner.c (L56-L57)
Misleading comment and redundant declaration. DeescapeQuotedString is declared in src/include/utils/guc.h ("This is exported because it is also used by the bootstrap scanner."), not merely "exported from guc-file.c". Re-declaring it locally as extern duplicates the canonical prototype and risks silent drift if the signature ever changes. Include the proper header instead (e.g. #include "utils/guc.h") and drop this line. (high confidence)
📄 src/backend/parser/Makefile (L58-L59)
The Makefile-based build is broken: nothing here generates the Lime scanner. scan.c (the new driver shim) does #include "scan_lex.h" and calls CoreLexAlloc/CoreLexFeedBytes, which come from the generated scan_lex.c/scan_lex.h. The meson build produces these via a custom_target over scan.lex (see src/backend/parser/meson.build scan_lex_gen), but this Makefile has no rule to build scan_lex.c/scan_lex.h from scan.lex, no scan_lex.o in OBJS, and the generic %.c: %.l pattern in Makefile.global does not match .lex. Result: scan.o fails to compile (missing scan_lex.h) and the lexer symbols are unresolved. The Makefile and meson.build are out of sync. Add a generation rule for scan_lex.{c,h} and link scan_lex.o.
Confidence: high.
📄 src/backend/parser/Makefile (L58-L59)
This grammar rule does not match what meson generates and hardcodes the tool. (1) meson invokes Lime via the pglime wrapper with --snapshot --host-reduce (and --aot when enabled), emitting gram_snapshot.c (and gram_aot.c) that parser_extension.c and the AOT path require; the bare lime -d. here emits neither, so the Makefile build lacks base_yyBuildSnapshot() and the runtime grammar-extension framework won't link. (2) lime is hardcoded instead of a configured make variable (e.g. $(LIME)), unlike the retired $(BISON)/$(FLEX) usage -- not portable and untraceable via configure. (3) -d. writes output to the literal current directory; combined with $< = $(srcdir)/gram.lime, this breaks VPATH/out-of-tree builds. The previous rules relied on the VPATH-aware %.c: %.y pattern.
Confidence: high.
📄 src/backend/parser/Makefile (L68-L68)
clean no longer removes the full set of generated artifacts. gram.out is added, but the Lime toolchain also produces gram_snapshot.c (always, per meson's --snapshot) and gram_aot.c (AOT builds), plus the scanner outputs scan_lex.c/scan_lex.h once a generation rule is added. clean/distclean/maintainer-clean must remove every new generated file or stale artifacts survive across rebuilds. Add gram_snapshot.c, gram_aot.c, scan_lex.c, and scan_lex.h to the removal list (and update .gitignore accordingly).
Confidence: high.
📄 src/backend/parser/Makefile (L63-L63)
scan.o now depends on the generated scan_lex.h (via #include "scan_lex.h" in scan.c) and on gram.h. With no explicit dependency and no generation rule for scan_lex.h, a parallel build (make -j) will attempt to compile scan.o before scan_lex.h exists, causing a race/failure. Once a generation rule exists, this line must also list scan_lex.h as a prerequisite for scan.o.
Confidence: high.
📄 src/backend/bootstrap/bootscanner.c (L255-L261)
A read error on stdin is silently treated as end-of-input. fgetc returns EOF both on real EOF and on a read error (ferror(stdin)), so a mid-stream I/O failure will truncate the BKI input without any diagnostic and bootstrap will proceed on partial data. After the loop, check if (ferror(stdin)) ereport(ERROR, ...) to distinguish the two cases. (moderate confidence)
📄 src/backend/parser/gramparse.h (L145-L145)
This function-pointer indirection is dead scaffolding. base_yyparse_fn is defined in parser.c as = base_yyparse and called via (*base_yyparse_fn)(yyscanner), but a tree-wide search shows it is never reassigned anywhere. Nothing dlsym's or overwrites it. Per YAGNI, an indirect call added for a swap that never happens should not ship; it only adds an indirect-call cost on the hot parse path and misleads readers. Either wire the reassignment (and justify it) or drop the indirection and keep the direct base_yyparse() call.
Also a Windows/MSVC concern: this extern is exposed via the gramparse.h include chain. If it were ever referenced cross-module/from an extension it would need PGDLLIMPORT; as-is it is unused, which is the deeper problem. (high confidence)
📄 src/backend/parser/gramparse.h (L140-L143)
This comment is inaccurate and describes behavior that does not exist. It claims "the Phase 4 subprocess pipeline (parser_extension.c) overwrites it after dlsym-ing the rebuilt parser .so." But parser_extension.c's own header states the fork+cc+dlopen path "has been removed entirely; this file is the in-process implementation" and contains no dlopen/dlsym. base_yyparse_fn is never reassigned. Comments must describe what the code does now, not a removed/aspirational ("Phase 4") design. Remove the stale narrative. (high confidence)
📄 src/backend/parser/gramparse.h (L35-L36)
The comment misdescribes the authoritative source. gram.lime has no union YYSTYPE { ... } body inside its %include block; it uses %token_type {YYSTYPE} plus per-symbol %type name {Type} declarations, and Lime derives the union member set from those. So the invariant is not "stay in sync with the union body in the %include block" but "stay in sync with the aggregate of every %type/%token_type declaration in gram.lime" -- a far larger and more error-prone surface. Fix the comment to point at the real source of truth. (high confidence)
📄 src/backend/parser/gramparse.h (L38-L40)
This hand-maintained YYSTYPE duplicates the semantic-value layout that Lime derives from gram.lime's %type declarations, and the correctness of the whole parser depends on the two matching exactly (member set, types, and -- for how Lime lays out its union -- order). There is no StaticAssert or generated cross-check (confirmed: none exists under src/backend/parser). A silent divergence (a %type added to gram.lime but not here, or a type mismatch) yields a mis-typed semantic stack: silent memory corruption, wrong parse trees, or crashes, with no build-time signal. This is a critical DRY violation. Prefer emitting YYSTYPE into a generated header from the same source Lime uses, or add a StaticAssert(sizeof) cross-check, rather than relying on humans to keep two lists identical. (moderate confidence)
📄 src/backend/parser/gramparse.h (L46-L46)
pgindent-breaking formatting: these members are not tab-aligned like the rest of the block (e.g. int\t\t\tival;, bool\t\tboolean;). char chr; uses a single space and no aligned member column, and char\t *str;/const char *keyword; are inconsistent. pgindent will rewrite these; run pgindent so the diff is clean. Style nit, separate from the correctness findings above. (high confidence)
📄 src/backend/parser/parser.c (L45-L45)
Dead scaffolding on a hot path. base_yyparse_fn is initialized to base_yyparse and is never reassigned anywhere in the tree (confirmed: no base_yyparse_fn = writer, and no dlsym/dlopen in the parser). The block comment and gramparse.h claim a "Phase 4 subprocess pipeline" swaps in a dlopen'd base_yyparse -- but parser_extension.c states that Track A (fork lime + cc -> dlopen .so) "has been removed entirely." So this function pointer only ever points at base_yyparse, yet it converts a direct call at line 218 into an indirect call on the query hot path for a feature that does not exist. Per YAGNI, drop the indirection: restore the direct base_yyparse(yyscanner) call and remove base_yyparse_fn (and its extern in gramparse.h). If you keep it, the comment must describe what the code does now, not a removed dlopen path.
📄 src/backend/parser/parser.c (L165-L166)
Grammatical error harms readability: "This points reduce dispatch and the scanner keyword hook at the chosen dialect's ...". The verb is malformed. Suggest "This points reduce dispatch and the scanner keyword hook at" -> "This points reduce dispatch and the scanner keyword hook to the chosen dialect's composed snapshot". Also this narrative comment describes WHAT rather than WHY and asserts "Cheap: a pointer swap plus a short bundle scan" without a benchmark; trim to why the call is at the parse boundary.
💡 Suggested change
Before:
* before deciding push vs static. This points reduce dispatch and the
* scanner keyword hook at the chosen dialect's composed snapshot, or at
After:
* before deciding push vs static. This points reduce dispatch and the
* scanner keyword hook to the chosen dialect's composed snapshot, or at
📄 src/backend/parser/parser.c (L129-L137)
Undocumented environment-variable knob controlling parser dispatch, with an unsupported performance claim. PG_LIME_PUSHPARSE is a hidden A/B switch not exposed as a GUC, not documented in the SGML docs, and not covered by a test. Per pgsql-hackers norms this is speculative scaffolding (a knob nobody asked for) and the inline "~4% of push-path parse time" figure is a performance claim with no reproducible benchmark reference. Remove the env-var path (and raw_parser_force_pushparse) unless a committed benchmark/caller needs it; if kept for testing it must be documented and the perf figure either backed by a benchmark or dropped from the comment.
📄 src/backend/parser/parser.c (L35-L38)
Aspirational/stale comment: this describes a "Phase 4 subprocess pipeline" that produces a "dynamically-rebuilt parser .so" whose base_yyparse is stored "via dlsym()." That path was removed (see parser_extension.c: "The historical Track A path ... has been removed entirely"), and nothing ever writes this slot. Comments must describe current behavior, not removed/future subsystems. Either remove the indirection entirely (preferred) or rewrite this to state it is always base_yyparse.
📄 src/backend/parser/parser.c (L111-L113)
No-op assign hook. assign_grammar_dialect only casts its two parameters to void; the GUC machinery already stores the string into grammar_dialect_string, and selection is applied per-parse in raw_parser(). PostgreSQL convention is to pass NULL as the assign_hook in guc_parameters.dat rather than register a do-nothing hook. Drop this function and set assign_hook => undef (and remove the extern from parser.h) unless a real assignment side effect is needed.
📄 src/backend/parser/parser_extension.c (L692-L693)
Dead branch: both arms of this if/else append the identical ".\n", so the conditional produces no difference in output. Collapse to a single appendStringInfoString(&buf, ".\n"); (keep a comment if the distinction is worth documenting). As written this is misleading dead code. (high confidence)
📄 src/backend/parser/parser_extension.c (L725-L726)
Non-idiomatic and fragile allocation: repalloc(pending ? pending : palloc0(0), ...) allocates a throwaway zero-length chunk only to immediately repalloc it. Use the standard guarded pattern instead: allocate with palloc on first use and repalloc thereafter. There is also no overflow guard on sizeof(PendingExt) * newcap; prefer the established idiom. (moderate confidence)
💡 Suggested change
Before:
pending = repalloc(pending ? pending : palloc0(0),
sizeof(PendingExt) * newcap);
After:
if (pending == NULL)
pending = palloc(sizeof(PendingExt) * newcap);
else
pending = repalloc(pending, sizeof(PendingExt) * newcap);
📄 src/backend/parser/parser_extension.c (L656-L659)
Stale/contradictory comment: the file header (lines 35-36) states the fork+cc+dlopen ".so" path "has been removed entirely" and this is the in-process implementation, yet this comment claims dispatch is resolved by "the rebuilt .so ... at dlopen time (postgres is linked with --export-dynamic)". That flow no longer exists. Update the comment to describe the in-process ParserSnapshot / host-reduce dispatch that the code actually performs. (high confidence)
📄 src/backend/parser/parser_extension.c (L440-L440)
Aspirational/future-tense comment for behavior that is not implemented ("SIGHUP-driven teardown/recompose is a later step; for now document the limitation"). Per project comment discipline, comments must describe current behavior; drop the roadmap wording and state the actual limitation plainly. (moderate confidence)
📄 src/backend/parser/parser_extension.c (L455-L455)
Stale reference to the removed "rebuilt parser .so" mechanism; this function is now called from the in-process host-reduce dispatcher, not from a dlopen'd .so. Reword the comment to match the in-process implementation. (moderate confidence)
📄 src/backend/parser/parser_extension.c (L317-L317)
Style: missing space around the operator; pgindent/convention expects Assert(symbol != NULL);. (high confidence)
💡 Suggested change
Before:
Assert(symbol !=NULL);
After:
Assert(symbol != NULL);
📄 src/backend/parser/parser_extension.c (L909-L911)
Dead code: pg_grammar_ext_pending_fragments has no caller in the tree (the driver uses pg_grammar_ext_dialect_fragments). Per the minimalism/YAGNI discipline, unused exported scaffolding should be removed rather than shipped. If retained, note that the static frag_array is sized to the first npending seen and is never resized, which would be a latent bug should a caller ever appear before registration is frozen. (moderate confidence)
📄 src/backend/parser/parser_extension.c (L518-L520)
ERRCODE_CONFIG_FILE_ERROR is a questionable errcode for an in-process grammar compose failure; this is not a configuration-file parse error. Consider ERRCODE_INTERNAL_ERROR (or a feature-specific code) to avoid misleading operators triaging by SQLSTATE. Same applies to the FATAL variant in pg_grammar_ext_prewarm. (moderate confidence)
📄 src/backend/parser/parser_pushparse.c (L869-L871)
Resource leak on error longjmp: ctx from parse_begin_borrowed() is a Lime-runtime allocation, not tied to a PostgreSQL MemoryContext. Between here and parse_end() below, several calls can ereport/elog(ERROR) and longjmp past parse_end(ctx): base_yylex(), parse_token(), parse_context_token_admissible(), and especially the base grammar reduce actions run via base_yyHostReduce (gram.y actions ereport(ERROR) routinely). Any such error leaks the ParseContext and all Lime-internal buffers on the parser hot path. Wrap the parse loop in PG_TRY/PG_FINALLY (calling parse_end(ctx) in the finally block), or bind ctx's lifetime to a MemoryContext callback, before raising the error. Confidence: high.
📄 src/backend/parser/parser_pushparse.c (L970-L972)
Dead code: error_lloc is written on both error paths but never read; scanner_yyerror() derives the location from the scanner's own state, and the value is only (void)-cast at the error site. Per the minimalism discipline, drop error_lloc, its YYLTYPE error_lloc = 0; declaration, the two assignments, and the (void) error_lloc;. Confidence: high.
📄 src/backend/parser/parser_pushparse.c (L985-L988)
Dead assignment: tree = parse_result(ctx) is immediately discarded via (void) tree; because *result is taken from yyextra->parsetree instead. If parse_result() has a required side effect, say so in the comment and drop the assignment; if it is a pure getter, remove the call, the tree variable, and its void *tree = NULL; declaration. As written this is confusing dead code. Confidence: high.
📄 src/backend/parser/parser_pushparse.c (L747-L751)
Footgun: pushparse_peek_resolves_ext() applies the DELETE-specific discriminator (next != FROM) to every both-admissible collision, ignoring which keyword actually triggered it. pushparse_resolve_collision() sets *need_peek for ANY lexeme where both base and extension tokens are admissible, so a future extension that introduces a second both-admissible verb would be silently mis-resolved by DELETE's FROM rule with no diagnostic. At minimum, gate this on the specific colliding token (pass base_code/ext_code in) or assert that only DELETE can reach here, so a new colliding verb fails loudly rather than mis-parsing. Confidence: moderate.
📄 src/backend/replication/.gitignore (L5-L5)
Missing /repl_gram.out from the ignore list. The Makefile's clean rule removes both repl_gram.out and syncrep_gram.out (they are Lime's generated grammar-report files, analogous to Bison's gram.output). Since the build produces repl_gram.out, it must be git-ignored too, otherwise git status shows it as an untracked file. Add the corresponding entry.
/repl_gram.out
/syncrep_gram.out
💡 Suggested change
Before:
+/syncrep_gram.out
After:
+/repl_gram.out
+/syncrep_gram.out
📄 src/backend/parser/scan.c (L1100-L1102)
Broken indentation/formatting: case OP: is over-indented and if (t->val.str != NULL) has misaligned tabs. This will not survive pgindent and looks like a hand-edit artifact.
More importantly, this block copies only val.str. Keyword-bearing tokens produced by the extension path (SCAN_TOK_IDENT_RAW sets val.keyword = lower where lower is palloc'd via downcase_truncate_identifier during scanner_init) are NOT copied here. Base keywords are safe because GetScanKeyword() returns pointers into the static keyword-string table, but the extension-hook lower pointer lives in the scanner_init-time allocation and its longevity depends on the parser and scanner_init sharing the same MemoryContext. Since raw_parser() runs both in the same context this currently works, but the comment's claim that this block establishes lifetime safety is misleading given keyword payloads bypass it. Fix the indentation and document/handle the keyword payload path.
💡 Suggested change
Before:
case OP:
if (t->val.str != NULL)
yylval_param->str = pstrdup(t->val.str);
After:
case OP:
if (t->val.str != NULL)
yylval_param->str = pstrdup(t->val.str);
📄 src/backend/parser/scan.c (L982-L983)
Error-path resource leak: CoreLexAlloc allocates the lexer, then CoreLexFeedBytes/CoreLexFeedEOF run the emit callback which can ereport(ERROR) directly (e.g. scan_lex_handle_xeescape's "unsafe use of '" path, SCAN_TOK_BAD_UNICODE_ESCAPE, SCAN_TOK_BAD_HEX_ESCAPE). Those longjmp out of scanner_init before CoreLexFree(lex, ...) runs, so any non-palloc resources the lexer owns leak. palloc'd state is reclaimed on context reset, but relying on that is fragile. Wrap the feed loop in PG_TRY/PG_FINALLY (or ensure CoreLexAlloc uses only palloc so the context reset is authoritative) so the lexer is always freed on the error path.
📄 src/backend/parser/scan.c (L1043-L1046)
Magic number and unclear free contract. scanbuf/literalbuf are freed only when their size is >= 8192, but ctx and ctx->tokens are always pfree'd. This 8192 threshold has no explanatory comment and diverges from the historical scanner_finish contract. If a parse-tree node ever retained a pointer into scanbuf (rather than a pstrdup'd copy) this conditional free would be a use-after-free hazard; conversely, buffers < 8192 are never freed and rely on an implicit context reset that is undocumented here. Add a comment explaining the rationale (matching how the retired flex scanner freed only large buffers) or free unconditionally.
📄 src/backend/parser/scan.c (L165-L167)
Integer overflow risk in literal growth. literallen/literalalloc are int (see scanner.h). extra->literallen + len mixes int and size_t, and extra->literallen += (int) len can overflow int for a literal larger than INT_MAX. scan_lex_addlitchar's extra->literalalloc *= 2 has no overflow guard and can wrap to a small/negative value, leading to under-allocation and a subsequent buffer overrun. Guard the growth arithmetic (e.g. reject sizes that would overflow, or use the tree's overflow-checked size helpers).
📄 src/backend/parser/scan.c (L244-L244)
Missing-space operator style (text +i, text +2, text +1, text -ctx->scanbuf) throughout the file reads like unary plus/minus and will be reformatted by pgindent. Should be text + i, text + 2, text + 1, text - ctx->scanbuf, etc. Fix to satisfy the pgindent/whitespace gate.
💡 Suggested change
Before:
slashstar = text +i;
After:
slashstar = text + i;
📄 src/backend/parser/scan.c (L320-L322)
Duplicated \u/\U end-position computation. This identical block (pos>=1 / pos>=0 asymmetry, 6-vs-10 length guess by re-reading scanbuf, clamp to scanbuflen) is repeated verbatim in scan_lex_addunicode, scan_lex_handle_unicode, and scan_lex_handle_xeu_second. Extract a single helper so any fix to the offset arithmetic lands in one place (DRY). The re-reading of scanbuf to guess escape length is also fragile if pos does not point exactly at the backslash.
📄 src/backend/parser/scan_lex_internal.h (L111-L112)
Divergent-shadow-field footgun. saw_non_ascii is duplicated here in ScanLexCtx and in core_yy_extra_type (extra). The accessor macro below (SCAN_LEX_SAW_NON_ASCII) reads/writes this shadow copy, and scan.lex resets it at each string open (xq_open/xe_open: SCAN_LEX_SAW_NON_ASCII(user) = false). However, the escape helpers in scan.c set extra->saw_non_ascii, and the SCONST consumer reads extra->saw_non_ascii (never the shadow). extra->saw_non_ascii is only reset once in scanner_init, never per-string. Result: after any string literal with a non-ASCII escape, extra->saw_non_ascii stays true for the rest of the scan, forcing pg_verifymbstr on every subsequent SCONST. The intended per-string reset silently targets a dead field. Drop this shadow field and make the macro operate on extra->saw_non_ascii so the reset and the read touch the same state.
📄 src/backend/parser/scan_lex_internal.h (L179-L184)
Inaccurate/aspirational comment. This describes the append helpers as working around a limitation via "a parallel C-side accumulator" and hedges that "we can't easily reach LEX_BUF_APPEND from outside an action body." The actual implementation (scan_lex_addlit/scan_lex_addlitchar/scan_lex_litbuf_* in scan.c) writes directly to the authoritative literal buffer (extra->literalbuf/extra->literallen), not a parallel accumulator. Comments must describe what the code does now; rewrite to state that these helpers append to the core scanner's literal buffer.
📄 src/backend/replication/Makefile (L39-L40)
Critical build break: the make build has no rule to generate the Lime lexer output that the checked-in scanner shims require, so src/backend/replication will not build under configure/make (only meson builds).
repl_scanner.c does #include "repl_scanner_lex.h" and syncrep_scanner.c does #include "syncrep_scanner_lex.h". In meson.build these are generated from repl_scanner.lex / syncrep_scanner.lex via lime -X (lime_lex_cmd), producing repl_scanner_lex.{c,h} and syncrep_scanner_lex.{c,h}. This Makefile:
- has no rule invoking
lime -Xto produce*_scanner_lex.{c,h}from the.lexfiles ->repl_scanner.o/syncrep_scanner.ofail to compile (missing header); - never adds
repl_scanner_lex.o/syncrep_scanner_lex.otoOBJS, so the lexer entry points (ReplLexFeedBytes,SyncRepLexFeedBytes, ...) are unlinked; - does not list the generated
*_scanner_lex.has prerequisites of the objects below.
Add the generation rules (mirroring the meson lime -X -d. invocation), add the objects to OBJS, and wire the header deps. This is out of sync with meson.build and violates the 'must stay in sync' constraint. (high confidence)
📄 src/backend/replication/Makefile (L46-L47)
These prerequisite lines omit the generated scanner-lex headers that the shims actually include. repl_scanner.o depends on repl_scanner_lex.h (included by repl_scanner.c) and syncrep_scanner.o depends on syncrep_scanner_lex.h (included by syncrep_scanner.c). Without these, incremental rebuilds after a .lex change won't recompile the scanners. Add the generated *_scanner_lex.h to the respective dependency lists once the generation rule exists. (high confidence)
📄 src/backend/replication/Makefile (L49-L55)
clean no longer removes the generated Lime lexer artifacts. The build produces repl_scanner_lex.{c,h} and syncrep_scanner_lex.{c,h} (see meson lime_lex_cmd outputs), plus a possible .out per grammar; none are removed here. Leftover generated files pollute the tree and can shadow later regenerations. Add them to the clean target (and mirror in .gitignore). (high confidence)
📄 src/backend/replication/Makefile (L39-L40)
The lime program is hardcoded as a bare command instead of a configured make variable (contrast the removed $(BISON)/BISONFLAGS). The meson build resolves it via find_program(get_option('LIME')) through the pglime wrapper, so the two builds can pick up different binaries and the make build cannot be pointed at a non-PATH lime. Use a make variable (e.g. $(LIME)) defined in Makefile.global to stay in sync with meson and avoid a hardcoded tool path. (moderate confidence)
📄 src/backend/replication/repl_gram_yytype.h (L12-L13)
This references a migration-project phase label ("pre-Phase 2b") that describes the change's history rather than what the code does now. Per PostgreSQL comment discipline, header comments should explain the current contract and rationale, not internal project-phase narrative, which goes stale quickly and adds reviewer noise. Suggest describing the invariant directly (the union must stay identical to the historical Bison %union so replication_yylex()'s signature is preserved) without the phase label. Low confidence on impact; non-functional.
💡 Suggested change
Before:
* The union shape matches the Bison %union in the retired grammar
* (pre-Phase 2b repl_gram source); it
After:
* The union shape matches the Bison %union in the previous grammar
* source; it
📄 src/backend/replication/syncrep_parse.h (L55-L62)
Duplicated comment block. There are two consecutive "Opaque scanner state." block comments preceding the SyncRepYyScanner typedef. The first (mentioning a "staging buffer for delimited-identifier collection") is a stale/superseded draft — the second explicitly says no xdbuf field is needed. Keep only the accurate second block; the redundant first block hurts readability and violates the minimal-diff/comment-accuracy discipline.
💡 Suggested change
Before:
/*
* Opaque scanner state. yyscan_t (public typedef: void *) points at one of
* these. The fields track the input cursor, the text of the last matched
* flex-style rule (for yyerror's "at or near" message), and a staging
* buffer for delimited-identifier collection.
*/
/*
* Opaque scanner state. yyscan_t (public typedef: void *) points at one of
After:
/*
* Opaque scanner state. yyscan_t (public typedef: void *) points at one of
📄 src/backend/replication/syncrep_parse.h (L72-L73)
Dead field (YAGNI). pos is only ever written once (s->pos = 0 in syncrep_scanner_init) and never read; the comment itself admits it is "advisory only" and that the lexer drives the actual scan. Unused scaffolding invites drift between comment and code. Remove pos (and drop the s->pos = 0; initializer in syncrep_scanner.c).
📄 src/backend/replication/syncrep_parse.h (L36-L37)
Stale comment. SyncRepToken.str is never "a static literal for the wildcard ''": the emit callback in syncrep_scanner.c only palloc's/pstrdup's str for NAME/NUM tokens, and the '' rule carries no payload (str stays NULL). The comment describes the old flex behavior, not what this code does now. Fix the comment to match.
💡 Suggested change
Before:
char *str; /* token text, palloc'd (or a static literal
* for the wildcard "*") */
After:
char *str; /* token text, palloc'd; NULL for tokens with
* no payload */
📄 src/backend/replication/repl_scanner.c (L153-L160)
UCONST payload is silently corrupted for long or overflowing literals (high confidence). buf is only 32 bytes, and n is clamped to sizeof(buf)-1 == 31, so any digit run of 32+ characters is silently truncated and strtoul parses the wrong number instead of the caller seeing an error. Worse, errno is set to 0 and endp is declared but neither is ever checked, so an out-of-range value (strtoul returns ULONG_MAX with errno==ERANGE) or the narrowing cast to uint32 is accepted silently. The retired flex scanner ran strtoul over the full, NUL-terminated match; this reimplementation is not behavior-equivalent as the header claims, and the bogus uintval flows into the grammar (e.g. TIMELINE %u, which only rejects <= 0). Parse the full token (e.g. via a palloc'd NUL-terminated copy of len bytes, like the SCONST branch) and check errno/endp to reject overflow and trailing garbage.
📄 src/backend/replication/repl_scanner.c (L287-L289)
Eager error at init breaks the caller's replication-vs-SQL fall-through contract (high confidence). exec_replication_command() in walsender.c calls replication_scanner_init() and only then checks replication_scanner_is_replication_command(); if it is not a replication command it resets the context and hands the string to the normal SQL parser. Because init now pre-scans the entire input, a lexer error such as "unterminated quoted string" (xqeof/xdeof rules) or "invalid streaming start location" is raised here via replication_yyerror() -> ereport(ERROR) before is_replication_command() can be consulted. The retired scanner tokenized lazily during yylex, so a plain SQL command like SELECT 'abc fell through and produced the proper SQL error. This misclassifies such inputs and emits a replication-flavored syntax error on the SQL path.
📄 src/backend/replication/repl_scanner.c (L115-L118)
Unchecked multiplication in the growth path (low confidence / defensive). newcap * sizeof(ReplToken) is computed as size_t with no overflow guard, and cap/ntokens are int. A single replication command line is effectively bounded by the protocol message size so this is not readily attacker-reachable, but for robustness and to match the tree's conventions, prefer an overflow-checked allocation (e.g. via MaxAllocSize checks) or reuse an existing growable container.
📄 src/backend/replication/repl_scanner.c (L270-L271)
Dead fields: s->input and s->len are written in replication_scanner_init but never read afterwards (yylex works purely off the token FIFO). Storing the borrowed str pointer with no lifetime ownership is a footgun if future code dereferences it after the caller frees the string. Per YAGNI, drop these fields and use a local length in init.
📄 src/backend/replication/syncrep_scanner.c (L189-L189)
Inconsistent allocation-failure handling: SyncRepLexAlloc's return is NULL-checked (lines 181-185), but syncrep_yyAlloc's return is used unchecked. If syncrep_yyAlloc could return NULL, the subsequent syncrep_yy(ctx.parser, ...) calls would dereference NULL. In practice palloc never returns NULL (it ereports on OOM), which makes the SyncRepLexAlloc NULL check dead defensive code as well. Either both allocators should be treated as non-failing (drop the dead NULL check, per the tree's palloc convention) or both should be checked consistently.
📄 src/backend/replication/syncrep_scanner.c (L133-L133)
Comment artifact: "unterminated-quoted- identifier" contains a stray hyphen and space splitting the word (leftover line-wrap hyphenation). Should read "unterminated-quoted-identifier".
💡 Suggested change
Before:
* scanner-level error (currently only the unterminated-quoted- identifier
After:
* scanner-level error (currently only the unterminated-quoted-identifier
📄 src/backend/utils/adt/Makefile (L138-L142)
The Makefile is out of sync with meson.build and will not build in the autoconf/make path.
meson.build (this same directory) generates the Lime scanner from jsonpath_scan.lex via the jsonpath_scan_lex custom_target, producing jsonpath_scan_lex.c and jsonpath_scan_lex.h, and links jsonpath_scan_lex_gen into the jsonpath library. jsonpath_scan.c (the new driver shim) does #include "jsonpath_scan_lex.h" and relies on the separately-compiled jsonpath_scan_lex.o.
This Makefile has none of that:
- No rule to run Lime on
jsonpath_scan.lex->jsonpath_scan_lex.c/jsonpath_scan_lex.h, so the headerjsonpath_scan.cincludes is never generated (build fails). jsonpath_scan_lex.ois missing from OBJS, so even if generated it would never be compiled/linked (undefinedJsonPathLexAlloc,JsonPathLexFeedBytes, etc.).- The forced-dependency line omits
jsonpath_scan.o: jsonpath_scan_lex.h, risking a parallel (-j) race wherejsonpath_scan.ccompiles before the header exists. cleandoes not remove the generatedjsonpath_scan_lex.c/jsonpath_scan_lex.h.
Add a Lime-lex rule and the jsonpath_scan_lex.o object, mirroring the meson jsonpath_scan_lex target, declare the header dependency, and extend clean. (high confidence)
📄 src/backend/utils/adt/Makefile (L142-L142)
jsonpath_gram_yytype.h does not exist anywhere in the tree and is not produced by any rule. lime -d. jsonpath_gram.lime emits jsonpath_gram.c, jsonpath_gram.h, and jsonpath_gram.out (as the clean rule below confirms) -- not a _yytype.h. It is also not a committed source file (unlike contrib/seg's hand-written seg_gram_yytype.h), and jsonpath_gram.lime's %include block does not reference it either.
As written, make will fail with "No rule to make target 'jsonpath_gram_yytype.h', needed by 'jsonpath_gram.o'". Either the hand-written header is missing from the patch (add it and the corresponding .gitignore entry), or this stale dependency must be dropped. Note meson.build has no equivalent dependency, so this is also a make/meson desync. (high confidence)
📄 src/backend/utils/adt/jsonpath_internal.h (L65-L65)
The hi_surrogate field is dead. It is only ever initialized to -1 (in parsejsonpath, jsonpath_scan.c:809) and never read or updated: the actual surrogate-pair decoding in parseUnicode() uses a function-local hi_surrogate variable scoped per \u... escape. The field, and its comment describing a "pending high surrogate" carried in scanner state, describe behavior that never happens. Per minimalism/YAGNI, drop this field (and its initialization).
Confidence: high.
📄 src/backend/utils/adt/Makefile (L136-L136)
The empty recipe (;) drops the touch $@ safeguard that the original code -- and the parallel src/backend/parser/Makefile in this same series -- deliberately keep. Upstream's own comment there explains why it is essential: it ensures the generated header is marked no older than the .c, otherwise make repeatedly tries to rebuild the header and "causes failures in VPATH builds from tarballs." Lime writes jsonpath_gram.c and jsonpath_gram.h in one invocation, so their mtimes are only equal by chance; with an empty recipe make never bumps the header's timestamp and can loop on regenerating it in VPATH/tarball builds. Keep the touch $@ recipe here as the parser Makefile does. (moderate confidence)
💡 Suggested change
Before:
jsonpath_gram.h: jsonpath_gram.c ;
After:
jsonpath_gram.h: jsonpath_gram.c
touch $@
📄 src/backend/utils/init/miscinit.c (L1866-L1869)
The added comment claims the compose runs "here in the postmaster, before backends fork." That is only true for the non-EXEC_BACKEND fork path. process_shared_preload_libraries() is also called from launch_backend.c (each EXEC_BACKEND/Windows child re-runs it after exec) and from PostgresSingleUserMain() in postgres.c (single-user mode, no postmaster, no fork). On those paths this runs inside the backend, not the postmaster, so a first-query compose cost is in fact paid there. Reword to reflect that the compose happens per-process at preload time and is inherited across fork only on non-EXEC_BACKEND platforms, to avoid a comment that is false on Windows and single-user mode. (moderate confidence)
💡 Suggested change
Before:
* Now that every preload library's _PG_init() has run and registered
* any runtime grammar extensions, compose them into the parser
* snapshot here in the postmaster, before backends fork, so no session
* pays a first-query compose cost. No-op if none registered.
After:
* Now that every preload library's _PG_init() has run and registered
* any runtime grammar extensions, compose them into the parser
* snapshot. On a forking postmaster this runs before backends fork,
* so the composed snapshot is inherited across fork and no session
* pays a first-query compose cost; under EXEC_BACKEND and in
* single-user mode this runs once per process instead. No-op if none
* registered.
📄 src/backend/utils/adt/jsonpath_scan.c (L804-L809)
Dead scaffolding: s->pos and s->hi_surrogate (the struct field) are written here but never read anywhere. The Lime lexer is driven by JsonPathLexFeedBytes(lex, s->input, s->len, ...) which maintains its own internal cursor, and the surrogate state used by parseUnicode/addUnicode is a local hi_surrogate variable (line 401), not s->hi_surrogate. These are leftovers from the removed hand-rolled state machine. Also note s is already zeroed by palloc0_object, so the s->scanstring.* = 0/NULL and s->pos = 0 assignments are redundant. Per minimalism discipline, drop the dead writes (and ideally the dead pos/hi_surrogate fields from the struct in jsonpath_internal.h).
💡 Suggested change
Before:
s->pos = 0;
s->len = len;
s->scanstring.val = NULL;
s->scanstring.len = 0;
s->scanstring.total = 0;
s->hi_surrogate = -1;
After:
s->len = len;
📄 src/backend/utils/adt/jsonpath_scan.c (L661-L661)
Style/pgindent: text +1 has inconsistent spacing and will not pass pgindent cleanly; it should be text + 1. The adjacent comment addstring(true, +1, len-1) is also misleading shorthand that does not match the actual argument order/spacing.
💡 Suggested change
Before:
addstring_internal(true, text +1, (int) len - 1, s);
After:
addstring_internal(true, text + 1, (int) len - 1, s);
📄 src/backend/utils/adt/jsonpath_scan_lex_internal.h (L47-L52)
The sentinel enumeration starts at JP_TOK_BASE + 2; there is no +0 or +1 value defined or used anywhere (verified: no such identifiers exist in the tree). This leaves an unexplained gap that reads like sentinels were removed but the numbering wasn't compacted. Renumber contiguously from JP_TOK_BASE + 1 (or add a comment explaining why +0/+1 are reserved) so the mapping stays self-documenting. Low confidence / low severity.
💡 Suggested change
Before:
#define JP_TOK_STRING_TAKE (JP_TOK_BASE + 2)
#define JP_TOK_VARIABLE_TAKE (JP_TOK_BASE + 3)
#define JP_TOK_NUMERIC_TEXT (JP_TOK_BASE + 4)
#define JP_TOK_INT_TEXT (JP_TOK_BASE + 5)
#define JP_TOK_RAW_CHAR (JP_TOK_BASE + 6)
#define JP_TOK_VARIABLE_BARE (JP_TOK_BASE + 7)
After:
#define JP_TOK_STRING_TAKE (JP_TOK_BASE + 1)
#define JP_TOK_VARIABLE_TAKE (JP_TOK_BASE + 2)
#define JP_TOK_NUMERIC_TEXT (JP_TOK_BASE + 3)
#define JP_TOK_INT_TEXT (JP_TOK_BASE + 4)
#define JP_TOK_RAW_CHAR (JP_TOK_BASE + 5)
#define JP_TOK_VARIABLE_BARE (JP_TOK_BASE + 6)
📄 src/backend/utils/adt/jsonpath_scan_lex_internal.h (L16-L18)
This header omits the "Portions Copyright (c) 1994, Regents of the University of California" line that the sibling jsonpath_internal.h (and the accompanying jsonpath_gram.lime) carry. For consistency with the subsystem's file headers, add the Regents line. Low severity.
💡 Suggested change
Before:
* Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
*
* src/backend/utils/adt/jsonpath_scan_lex_internal.h
After:
* Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
* Portions Copyright (c) 1994, Regents of the University of California
*
* src/backend/utils/adt/jsonpath_scan_lex_internal.h
📄 src/bin/pgbench/.gitignore (L1-L4)
The Makefile's clean rule now removes a new build artifact, exprparse.out (rm -f exprparse.h exprparse.c exprparse.out), which Lime produces in this source directory via the lime -d. $< rule. That generated file is not ignored here, so it will show up as an untracked file in git status after a build. Add /exprparse.out to this .gitignore (the same way the retired flex output used to be ignored).
Removing /exprscan.c itself is correct, since exprscan.c is now a checked-in hand-written source file rather than flex-generated output.
💡 Suggested change
Before:
/exprparse.h
/exprparse.c
-/exprscan.c
/pgbench
After:
/exprparse.h
/exprparse.c
/exprparse.out
/pgbench
📄 src/bin/pgbench/Makefile (L10-L14)
This Makefile is out of sync with meson.build and will not build under the autoconf/make path. meson.build compiles exprscan.lex -> exprscan_lex.c/exprscan_lex.h (custom target exprscan_lex, command lime -X) and links the generated lexer into exprscan_lib. Here there is no rule to generate exprscan_lex.{c,h}, and exprscan_lex.o is absent from OBJS. Since exprscan.c does #include "exprscan_lex.h" (a generated header), and Makefile.global.in has no generic %_lex.c: %.lex rule, make will fail to compile exprscan.o (missing header) and to link pgbench (missing lexer symbols). Add a generation rule for exprscan.lex (mirroring lime -X -d. $<) and add exprscan_lex.o to OBJS. Confidence: high.
📄 src/bin/pgbench/Makefile (L53-L53)
clean/distclean no longer removes the generated lexer outputs. Since exprscan.lex is compiled to exprscan_lex.c/exprscan_lex.h (per meson.build), these generated files must be cleaned here alongside exprparse.*; otherwise stale generated files linger after make clean. Confidence: high.
💡 Suggested change
Before:
rm -f exprparse.h exprparse.c exprparse.out
After:
rm -f exprparse.h exprparse.c exprparse.out exprscan_lex.c exprscan_lex.h
📄 src/backend/utils/misc/guc-file.c (L208-L213)
Silent-data-loss hazard on OOM. If GucLexAlloc() returns NULL, this returns with an empty token FIFO and io_error==false. ParseConfigFp then gets GUC_EOF on the first guc_yylex() call, exits its loop, and returns OK=true -- i.e. the config file is treated as empty-but-valid, silently dropping every setting. The retired flex path did the opposite: a fatal scanner error (incl. malloc failure) siglongjmp'd out, logged at elevel, recorded a config-file error, and set OK=false. This is a behavioral regression that turns an out-of-memory during scanning into a silent misconfiguration. Set an error flag here (like io_error) and surface it as a parse failure in ParseConfigFp instead of returning quietly. (high confidence)
📄 src/backend/utils/misc/guc-file.c (L128-L128)
Missing space after '!=' will fail pgindent / the whitespace gate. Should be 'if (text != NULL)'. (high confidence)
💡 Suggested change
Before:
if (text !=NULL)
After:
if (text != NULL)
📄 src/backend/utils/misc/guc-file.c (L681-L683)
New, differently-worded read-error message contradicts the header's 'byte-identical error messages' claim, and per PostgreSQL conventions a read(2) failure should surface errno via %m so the operator learns the actual cause. The old flex path reported the fatal errmsg; here the underlying error (set only when ferror(fp) is true in guc_slurp_file) is discarded. Consider appending ": %m" and capturing the failure while errno is still valid. (moderate confidence)
📄 src/backend/utils/misc/guc-file.c (L254-L256)
Redundant per-token copy. Each GucToken already owns a palloc'd, NUL-terminated 'text' pointer, yet guc_yylex resetStringInfo()+appendStringInfoString() copies it into tokbuf on every pop solely so GUC_YYTEXT can mirror flex's yytext. GUC_YYTEXT consumers (opt_name/opt_value pstrdup, DeescapeQuotedString, the syntax-error message) all read the current token within the same iteration, so GUC_YYTEXT could point directly at tok->text and the whole tokbuf StringInfo could be dropped, eliminating an extra buffer plus a memcpy per token. (moderate confidence)
📄 src/backend/utils/misc/guc-file.c (L198-L198)
Dead field: s->fp is assigned here but never read afterward (guc_slurp_file receives fp as a parameter, not from the struct). Remove GucScanState.fp per YAGNI. Similarly, 'struct GucEmitContext' wraps a single GucScanState* used exactly once as the callback userdata; passing the GucScanState* directly as guc_emit_cb's 'user' arg would remove the thin wrapper. (low confidence)
📄 src/backend/utils/misc/guc-file.c (L165-L169)
Unbounded doubling with no overflow guard: 'cap *= 2' and 'len + 1 >= cap' can overflow size_t on a pathological input, and the eager-slurp model assumes config files are 'bounded in size' -- but postgresql.auto.conf and included files are not hard-capped. The scan holds the full file buffer plus a palloc'd copy of every token's text simultaneously, versus flex's constant-memory streaming. repalloc's MaxAllocSize check will ereport before a true overflow in practice, but the assumption should be stated/validated rather than assumed. (low confidence)
📄 src/backend/utils/misc/guc-file.c (L77-L80)
The header comment reads as migration narrative ('pre-Phase 5', 'Lime v0.2.2', ~470-line counts, git-diff-friendliness). Per comment-accuracy discipline these references to internal migration phases and tool versions go stale and describe process rather than what the code does now; trim to durable why-comments. (low confidence)
📄 src/bin/pgbench/exprscan.c (L276-L276)
Spacing around binary operators is malformed and will not survive pgindent: text +len, ) -ctx->input_base. This is a whitespace defect that fails git diff --check/pgindent style. Write it as text + len and - ctx->input_base.
💡 Suggested change
Before:
(int) ((text +len) -ctx->input_base);
After:
(int) ((text + len) - ctx->input_base);
📄 src/bin/pgbench/exprscan.c (L311-L311)
Same malformed operator spacing: text +1 should be text + 1. Will not pass pgindent.
💡 Suggested change
Before:
memcpy(s, text +1, nlen);
After:
memcpy(s, text + 1, nlen);
📄 src/bin/pgbench/exprscan.c (L127-L128)
Inconsistent allocation: the initial allocation uses the overflow-checked pg_malloc_array(ExprToken, newcap), but the growth path uses a raw newcap * sizeof(ExprToken) multiplication with no overflow guard. For symmetry and safety use pg_realloc_array(expr_tokens, ExprToken, newcap). (In practice pgbench expressions are a single line so overflow is not reachable, hence low severity, but the asymmetry is a latent footgun.)
💡 Suggested change
Before:
expr_tokens = pg_realloc(expr_tokens,
newcap * sizeof(ExprToken));
After:
expr_tokens = pg_realloc_array(expr_tokens, ExprToken, newcap);
📄 src/bin/psql/Makefile (L61-L61)
This is build-breaking for the make (autoconf) path, and the comment is factually wrong. meson.build in this same change compiles psqlscanslash.lex into psqlscanslash_lex.c/psqlscanslash_lex.h via a custom_target running lime -X -d@OUTDIR@, and the new psqlscanslash.c does #include "psqlscanslash_lex.h" and calls the generated SlashLexAlloc/SlashLexFeedBytes/... symbols. So a codegen rule IS required here; it was not "replaced with nothing." As written, the make build has no rule to produce psqlscanslash_lex.[ch], so psqlscanslash.c fails to compile (missing header), and the generated lexer object is absent from OBJS, so it would also fail to link. Add the equivalent Lime rule (mirroring src/backend/parser/Makefile's gram.c: gram.lime / lime -d. $<), e.g. a psqlscanslash_lex.c: psqlscanslash.lex rule invoking lime -X, plus the .h: .c touch trick, and add psqlscanslash_lex.o to OBJS. The make and meson builds must stay in sync. (high confidence)
💡 Suggested change
Before:
# psqlscanslash.c is hand-rolled (Phase 2h); no codegen rule.
After:
# psqlscanslash_lex.c/.h are generated from psqlscanslash.lex by Lime.
# See notes in src/backend/parser/Makefile about the two-target rule.
psqlscanslash_lex.h: psqlscanslash_lex.c
touch $@
psqlscanslash_lex.c: psqlscanslash.lex
lime -X -d. $<
📄 src/bin/psql/Makefile (L79-L79)
clean/distclean no longer removes the build-generated lexer artifacts. Since the make build must generate psqlscanslash_lex.c and psqlscanslash_lex.h from psqlscanslash.lex (see meson custom_target), these generated files must be cleaned here, otherwise stale generated output is left behind (breaking VPATH/tarball rebuilds). Removing psqlscanslash.c from this list is correct (it is now a committed source), but the generated psqlscanslash_lex.* need to be added. (high confidence)
💡 Suggested change
Before:
rm -f sql_help.h sql_help.c tab-complete.c
After:
rm -f sql_help.h sql_help.c tab-complete.c
rm -f psqlscanslash_lex.c psqlscanslash_lex.h
📄 src/fe_utils/Makefile (L53-L53)
This desyncs the Makefile from meson.build and breaks the make-based build (high confidence). psqlscan.c does #include "psqlscan_lex.h" and calls the Lime-generated lexer (PsqlLexAlloc / PsqlLexFeedBytes / ...). meson.build generates psqlscan_lex.c/.h from psqlscan.lex via a custom_target and compiles the generated .c into the psqlscan static_library. This Makefile, however, only lists psqlscan.o in OBJS and provides NO rule to generate psqlscan_lex.{c,h} from psqlscan.lex, does not compile the generated lexer .c, and does not declare psqlscan.o's dependency on the generated header. The result: psqlscan_lex.h is missing at compile time and the lexer symbols are unresolved at link time. Compare src/bin/pgbench/Makefile, which adds the intended Lime codegen rule (e.g. exprparse.c: exprparse.lime -> lime -d. $<) plus the header dependency. Add the equivalent Lime rule here (generate psqlscan_lex.{c,h} from psqlscan.lex, build the generated .c, and make psqlscan.o depend on psqlscan_lex.h). Separately, the comment is inaccurate: codegen IS still needed (meson does it) - only psqlscan.c itself is hand-rolled, not the lexer.
📄 src/fe_utils/Makefile (L66-L66)
clean/distclean no longer removes the Lime-generated lexer artifacts. meson.build (and the parallel pgbench/psql changes) generate psqlscan_lex.c and psqlscan_lex.h from psqlscan.lex; once a make-side codegen rule exists, these generated files (plus any Lime .out file) must be removed here or a stale/partial generated header will be left behind after make clean/distclean. Note the old line only removed psqlscan.c (the flex output); the new generated names differ. (moderate confidence, contingent on adding the missing codegen rule above.)
📄 src/include/c.h (L1396-L1398)
This refactor silently drops the three #undef USE_AVX2_WITH_RUNTIME_CHECK / USE_AVX512_CRC32C_WITH_RUNTIME_CHECK / USE_AVX512_POPCNT_WITH_RUNTIME_CHECK directives that previously lived in the #else branch. Those #undefs are not dead code: in "universal" macOS builds, pg_config.h may be generated on an x86_64 host and thus define these AVX symbols, but individual translation units get compiled for aarch64. With the guard removed, code such as pg_checksum_block_avx2() in checksum.c (guarded by #ifdef USE_AVX2_WITH_RUNTIME_CHECK, using pg_attribute_target("avx2")) and the AVX-512 CRC paths in pg_crc32c_sse42.c will be compiled for the aarch64 target, where AVX intrinsics/target attributes are invalid — breaking the build. Converting the #if/#else into an #if/#elif chain to preserve the Neon detection is fine, but the #undef block must be retained (e.g. in the #elif/final branch or a separate non-x86_64 block). This is a portability regression on a hard-gate platform (macOS universal / aarch64). Confidence: high.
💡 Suggested change
Before:
#elif defined(__aarch64__) && defined(__ARM_NEON)
#define USE_NEON
#endif
After:
#else /* ! x86_64 */
/*
* In "universal" macOS builds, it's possible for AVX-related symbols to
* get defined if the build host is x86_64, but we mustn't try to build
* that code when cross-compiling to aarch64.
*/
#undef USE_AVX2_WITH_RUNTIME_CHECK
#undef USE_AVX512_CRC32C_WITH_RUNTIME_CHECK
#undef USE_AVX512_POPCNT_WITH_RUNTIME_CHECK
#if defined(__aarch64__) && defined(__ARM_NEON)
#define USE_NEON
#endif
#endif /* x86_64 */
📄 src/bin/psql/psqlscanslash.c (L64-L67)
slash_option_quote is a file-scope global that gets pointed at either the caller's quote argument or the stack-local local_quote. Once psql_scan_slash_option returns, this global holds a dangling pointer (to a returned stack frame or a caller buffer that may be freed). It is only dereferenced by .lex action bodies during slash_scan_run, so there is no active bug today, but this is a latent footgun and a reentrancy regression: the flex-era scanner kept this state per-yyscan_t (re-entrant per PsqlScanState), whereas these process-globals make the slash scanner non-reentrant and unsafe if two PsqlScanStates are ever scanned concurrently or nested. Consider moving this per-scan option state into PsqlScanStateData rather than file-scope globals. (moderate confidence)
📄 src/bin/psql/psqlscanslash.c (L257-L263)
Dead code: the slash lexer never sets ctx.stop_kind = STOP_VAR_EXPAND. Variable substitution in psqlscanslash.lex (rules arg_var_plain/bq_var_plain) is expanded inline via appendPQExpBufferStr into output_buf and then LEX_SKIP(); it never pushes a new buffer through the driver. This entire STOP_VAR_EXPAND branch (including the free()s and psqlscan_push_new_buffer) is copy-pasted from the SQL driver in psqlscan.c and is unreachable here. Per YAGNI, drop it (or fold it into the default case) so the slash driver only handles the stop kinds it actually produces.
📄 src/include/fe_utils/psqlscan_emit.h (L49-L49)
STOP_VAR_RECURSE is dead: it is never assigned anywhere. The recursion case in psql_emit_var_plain() (psqlscan.c) echoes the raw text inline via psqlscan_emit() and returns false; it never sets stop_kind = STOP_VAR_RECURSE. Both drivers only list case STOP_VAR_RECURSE: as a "Not used" branch. Per YAGNI/minimal-diff, drop this enumerator (and the corresponding switch branches) unless a caller is actually wired up. Confidence: high.
📄 src/include/fe_utils/psqlscan_emit.h (L77-L78)
var_text and var_text_len are dead fields: they are never written or read anywhere in psqlscan.c / psqlscanslash.c (grep finds only this declaration). They were meant to back the non-existent STOP_VAR_RECURSE path. The recursion case is handled inline by echoing the raw text, so these fields serve no purpose. Remove them. Confidence: high.
📄 src/include/fe_utils/psqlscan_emit.h (L107-L109)
This comment documents a contract that the implementation does not honor: there is no STOP_VAR_RECURSE result and no var_text echo path. psql_emit_var_plain() handles the recursive-variable case by emitting the raw text inline and returning false (continue), so its only terminating result is STOP_VAR_EXPAND. Update the comment to describe what the code actually does now (STOP_VAR_EXPAND only) rather than an aspirational recurse-via-var_text design. Confidence: high.
📄 src/include/fe_utils/psqlscan_emit.h (L19-L19)
File-header comment references STOP_VAR_RECURSE as one of the terminating stop kinds, but no code path ever produces it (see the enum/struct comments). Trim this list to the stop kinds actually emitted (STOP_SEMI / STOP_BACKSLASH / STOP_VAR_EXPAND / STOP_SLASH_OK). Confidence: high.
📄 src/fe_utils/psqlscan.c (L389-L391)
Calling exit(1) from a shared frontend library shim (this file is linked into psql, pgbench, and pg_dump) is a footgun: it terminates the entire client process with no cleanup on an "impossible" internal state, and diverges from the flex-era scanner behavior. In frontend code the convention is Assert(false) for can't-happen invariants, or pg_fatal() if it must be user-reachable. fprintf(stderr, ...)/exit(1) here is neither. Same issue in psql_classify_eol's default branch below. (moderate confidence)
📄 src/fe_utils/psqlscan.c (L212-L213)
psql_scan_run() returns raw magic integers (0=EOL, 1=SEMI, 2=BACKSLASH) that psql_scan() decodes via a switch. Two functions sharing unnamed return codes is error-prone; a named enum would make misclassification impossible and self-document the contract. (low confidence / style)
📄 src/fe_utils/psqlscan.c (L247-L248)
A fresh lexer is heap-allocated (PsqlLexAlloc via psql_lex_malloc -> pg_malloc) and freed (PsqlLexFree) on every iteration of the streaming loop, and psql_scan() runs this loop on every call. This is a hot path (every line processed by psql/pgbench/pg_dump). The flex-era scanner reused its state object across calls; per-iteration malloc/free of the lexer is a likely performance regression. Consider caching a single lexer in PsqlScanState and resetting it, if the generated API allows. (moderate confidence)
📄 src/include/parser/parser.h (L65-L66)
The GUC check/assign hook declarations should go in src/include/utils/guc_hooks.h, not here. That header explicitly exists for this purpose -- its comment reads: "These functions are scattered throughout the system, but we declare them all here to avoid having to propagate guc.h into a lot of unrelated header files." Every other string GUC hook (check_client_encoding, check_search_path, check_backtrace_functions + assign_backtrace_functions, etc.) is declared there, and the generated guc_tables.c already #include "utils/guc_hooks.h". Moving these two declarations to guc_hooks.h (keeping them ordered by GUC name) also removes the need for the #include "utils/guc.h" added above, matching the existing backslash_quote GUC in this file which declares only its variable here with no guc.h dependency. Confidence: high.
📄 src/include/parser/parser.h (L61-L62)
This justification comment is inaccurate. guc_tables.c obtains these hook declarations via #include "utils/guc_hooks.h", not via parser.h; and the grammar_dialect_string variable is exported here only because that is where it is defined (parser.c). The comment conflates the variable and the hook functions. Since the hooks belong in guc_hooks.h (see the other comment), reword this to describe only the exported variable, e.g. drop the "Declared here because the generated GUC table (guc_tables.c) includes this header" clause. Confidence: high.
📄 src/include/parser/parser.h (L19-L19)
Adding #include "utils/guc.h" to this widely-included parser header is exactly what guc_hooks.h was designed to prevent (per its own comment about not propagating guc.h into unrelated headers). It is needed only for the GucSource type used by the misplaced hook declarations. Once the hook declarations move to guc_hooks.h, this include should be removed. Confidence: high.
📄 src/include/fe_utils/psqlscan_int.h (L215-L217)
The new contract of psqlscan_prepare_buffer is a footgun. It returns void * but the comment admits the return value "is the SAME pointer stored in *txtcopy" -- a redundant dual return. Verified all three callers ignore the return value entirely: psqlscan.c lines 351 and 669 cast it to (void) and use only *txtcopy; there is no remaining caller that stores the handle (the old StackElem.buf field is gone). Since the return value has no consumer, make the function return void and drop the void */txtcopy-are-identical language. Keeping an unused return with a confusing "two ways to get the same pointer" contract invites misuse. Confidence: high.
💡 Suggested change
Before:
extern void *psqlscan_prepare_buffer(PsqlScanState state,
const char *txt, int len,
char **txtcopy);
After:
extern void psqlscan_prepare_buffer(PsqlScanState state,
const char *txt, int len,
char **txtcopy);
📄 src/include/fe_utils/psqlscan_int.h (L208-L213)
This comment documents the removed flex API and a redundant contract rather than the function's current behavior. Comments should explain what the code does now and why, not narrate the port history or restate that two out-params are identical. Trim to a one-line description of the current behavior (allocate a buffer, apply FF-mapping when the encoding is unsafe, store the copy in *txtcopy). Confidence: high.
📄 src/include/fe_utils/psqlscan_int.h (L31-L32)
Aspirational/historical narration in a comment. Per PostgreSQL comment discipline, comments describe what the code does now, not the porting history ("Pre-Phase 2h", "The hand-rolled scanners... flex is no longer involved"). Remove the parenthetical history note; the surrounding text already explains the current mechanism. Confidence: high.
📄 src/include/fe_utils/psqlscan_int.h (L111-L113)
Historical/process narration referencing a prior port state ("Pre-port these owned YY_BUFFER_STATE plus an externally-managed yyscan_t. Now we just track..."). Describe the current StackElem representation only; drop the before/after comparison. Confidence: high.
📄 src/include/parser/parser_extension.h (L206-L209)
Stale/contradictory comment. The file-top comment states the subprocess path was "removed" and the in-process compose (Track B) is "the live implementation"; parser_extension.c likewise removes the Track A subprocess/.so path entirely. Yet this comment describes pg_grammar_ext_dispatch_reduce as ABI "between the host postgres binary and the .so produced by the Phase 4 subprocess pipeline" resolved by "the dlopen'd .so". The function is in fact called in-process (via pg_grammar_ext_reduce_by_ruleno), not from a dlopen'd .so. Per PG comment-accuracy discipline, describe what the code does now: an in-process trampoline. Drop the removed-.so/dlopen/--export-dynamic and "Phase 4 subprocess pipeline" narrative. (high confidence)
📄 src/include/parser/parser_extension.h (L347-L351)
Aspirational/future-tense comment for a path that no longer exists. This describes "the upcoming subprocess pipeline that will concatenate this fragment with the base gram.lime, fork lime to compile, and dlopen the result" -- but that subprocess/compile/dlopen pipeline was removed in favor of the in-process compose (per this file's own header and parser_extension.c). Reword to describe the current use (the dummy_grammar_ext smoke test verifying the converter emits parseable .lime) and drop the removed subprocess-pipeline rationale. (high confidence)
📄 src/interfaces/ecpg/preproc/Makefile (L71-L71)
Build-breaker for default builds. $(PYTHON) in src/Makefile.global is @PYTHON@, which configure only populates when --with-python=yes (see PGAC_PATH_PYTHON, guarded by if test "$with_python" = yes in configure.ac). ecpg is built on every standard build, so in a default build without --with-python this rule expands to an empty command ( <script> $< $@) and fails. The analogous backend rule (src/backend/parser/Makefile) invokes lime from PATH rather than $(PYTHON). Either make python an unconditional configure substitution or invoke the interpreter another way that doesn't depend on --with-python. Confidence: high.
📄 src/interfaces/ecpg/preproc/Makefile (L104-L104)
derived_gram.y is a new generated build artifact (the meson.build even states it is "a build artefact -- not committed") but it is not removed by clean distclean. This leaves stale generated output behind and violates the rule that clean/distclean must remove new artifacts. Add derived_gram.y to the rm list. Confidence: high.
💡 Suggested change
Before:
rm -f preproc.y preproc.c preproc.h c_kwlist_d.h ecpg_kwlist_d.h
After:
rm -f preproc.y preproc.c preproc.h derived_gram.y c_kwlist_d.h ecpg_kwlist_d.h
📄 src/interfaces/ecpg/preproc/pgc_internal.h (L17-L17)
High confidence, correctness. The PGC_TOK_* sentinels occupy the fixed range [1001, 1041]. In pgc.c, scan_emit_cb_dispatch() treats any rule value not matching a PGC_TOK_* case as an already-final parser token (the default: branch emits it verbatim). Those pass-through values come from ScanCKeywordLookup()/ScanECPGKeywordLookup(), which return real Bison %token codes from the generated preproc.h. Bison numbers tokens from 256 upward, and the ecpg grammar plus the ~470 SQL keywords and C keywords pushes the token space well past 1000. If any real grammar token value lands in [1001, 1041], scan_emit_cb_dispatch will mis-decode it as a sentinel and emit the wrong token, causing silent misparsing. The base is chosen with no compile-time guarantee that it sits above the max grammar token value. Add a static assertion (e.g. StaticAssertDecl(PGC_TOK_BASE > , ...)) tied to a generated token-count symbol, or derive PGC_TOK_BASE from YYMAXTOKEN/the last %token, so a future grammar growth cannot silently collide with these sentinels.
📄 src/interfaces/ecpg/preproc/pgc_internal.h (L86-L87)
High confidence, maintainability/correctness. These prototypes pass the scanner's user and lex objects as untyped void *, discarding all compile-time type checking between pgc.lex and pgc.c. In pgc.c every one of these parameters is ignored ((void) user; (void) lex;) or only forwarded, so the erasure buys nothing but removes the compiler's ability to catch a caller passing the wrong pointer -- a footgun for a frontend scanner where a mismatched pointer means use-after-free or wrong-struct dereference. Forward-declare the concrete lexer/user struct types (e.g. struct PgcLexer; / the emit-context type) and use them here instead of void *.
📄 src/interfaces/ecpg/preproc/pgc_internal.h (L18-L18)
Low confidence, style. New PostgreSQL source files use tab indentation, but the PGC_TOK_* macro block is aligned with spaces (the run of spaces before the (PGC_TOK_BASE + N) values). This will not survive pgindent cleanly and diverges from the tab convention used elsewhere in the tree. Align with tabs.
📄 src/interfaces/ecpg/preproc/parser.c (L353-L359)
Critical: ascii_to_lime_token() is a copy of the backend gram.y self-char mapping (LPAREN/RPAREN/COMMA/SEMI/...), but ecpg's grammar does not use these symbolic terminals. ecpg's preproc.y/ecpg.trailer uses raw character-literal terminals ('(', ')', ',', ';', '*', '+', '[', ']', '=', ':', '{', '}'), whose token value IS the ASCII code, and pgc.c already emits those raw ASCII codes (see pgc.c scan_emit_cb_dispatch: out_code = (unsigned char) text[0]). None of LPAREN, RPAREN, LBRACKET, RBRACKET, COMMA, SEMI, COLON, DOT, PLUS, MINUS, STAR, SLASH, PERCENT, CARET, PIPE, LT, GT, EQ, LBRACE, RBRACE is defined as a token in the ecpg grammar (they appear nowhere else in src/interfaces/ecpg/). This is therefore either a hard compile error (undefined identifiers) or, if any happen to resolve, a silent mis-parse: the raw ASCII code that the grammar expects is rewritten into a different value. ecpg must pass these tokens through unchanged, exactly as Bison's base_yylex did. This entire helper should be removed and the token passed straight to base_yyLoc.
📄 src/interfaces/ecpg/preproc/parser.c (L415-L415)
Because ascii_to_lime_token() remaps the correct raw-ASCII token values produced by pgc.c into backend-only symbolic IDs that are not ecpg grammar terminals, this call corrupts every self-char token fed to the parser. Pass the token through unchanged: base_yyLoc(parser, tok, base_yylval, base_yylloc);. The terminating base_yyLoc(parser, 0, ...) for EOF is already correct.
💡 Suggested change
Before:
base_yyLoc(parser, ascii_to_lime_token(tok), base_yylval, base_yylloc);
After:
base_yyLoc(parser, tok, base_yylval, base_yylloc);
📄 src/interfaces/ecpg/preproc/parser.c (L413-L417)
base_yyparse() unconditionally returns 0 except on allocation failure, discarding parse-error status entirely. Bison's base_yyparse returned nonzero on syntax error; here base_yyLoc/base_yy_drain are void, so a Lime-reported syntax error cannot propagate out of this loop. Confirm this does not weaken ecpg's error detection (errors currently surface only via mmerror/mmfatal inside actions). If Lime exposes a parse-status accessor, it should be checked and reflected in the return value. Also note the parser object is leaked on any early/abnormal exit from the loop (e.g. an action calling exit) since the base_yyFree only runs on the normal fall-through path.
📄 src/interfaces/ecpg/preproc/parser.c (L338-L339)
These comments record development-thread narrative rather than describing current behavior: Lime v0.2.0's Parse_drain(), P0-NEW-8, and repeated Phase 3 final are versioned/aspirational churn notes that will not age well and are exactly the kind of history-in-comments rejected on -hackers. State only what the code does now (why push+drain emulates Bison's pull-mode timing) and drop the version/phase/ticket references.
📄 src/interfaces/ecpg/preproc/parser.c (L259-L259)
Stale historical note. /* renamed from Op in Phase 3 final */ describes past churn, not current behavior; comments should explain the code as it stands. Remove it (and drop the non-pgindent double-tab noise on the case labels below).
💡 Suggested change
Before:
case OP: /* renamed from Op in Phase 3 final */
After:
case OP:
📄 src/interfaces/ecpg/preproc/parser.c (L260-L261)
pgindent-breaking whitespace churn on lines whose semantics are unchanged: the case labels have gained a spurious extra tab (case\t\tCSTRING:), the base_yylloc = loc_strdup(...) body line was re-indented, and a gratuitous blank line was inserted before break. Only the Op->OP rename is needed here; this reformatting of otherwise-untouched lines will not survive pgindent and adds review noise. Revert the indentation to the original one-tab case labels and remove the blank line.
📄 src/interfaces/ecpg/test/expected/preproc-define.c (L77-L77)
This expected-output file is regenerated by running the ported ecpg preprocessor on define.pgc, so this diff is documenting a real behavior change in the new scanner (pgc.c), not just a cosmetic edit. In the source (define.pgc lines 36-39), #if 0 and #endif are C-passthrough directives that each sit on their own line at column 0. The old scanner echoed them on their own lines (blank line + #if 0); the new scanner now merges them onto the previous line's trailing whitespace ( #if 0 / #endif), i.e. it is dropping/collapsing a newline in echoed C passthrough. ecpg relies on faithfully preserving source line structure (the #line directives keep generated-C line numbers aligned with the .pgc source); silently swallowing newlines in passthrough desynchronizes compiler diagnostics against the original source. Confirm this is intended behavior of the rewrite rather than a newline-accounting regression in pgc.c's echo/newline handling that is merely being papered over here; the same concern applies to the whitespace-collapse change above (lines 58-63). Confidence: moderate.
📄 src/interfaces/ecpg/preproc/preproc_yytype.h (L11-L12)
This comment misidentifies the source of truth for the %union. The %union in the generated preproc.y comes from the hand-maintained ecpg.header file (which parse.pl passes through verbatim), NOT from gram.lime. The Makefile chain is: gram.lime -> derived_gram.y (via lime_to_bison_gram.py) -> preproc.y (via parse.pl, using ecpg.header for the %union). parse.pl does not synthesize the union from gram.lime. A future maintainer following this comment to gram.lime will not find the union at all. Point at ecpg.header instead.
Suggested: The struct definitions must be kept in sync by hand with the %union in ecpg.header (the input parse.pl copies verbatim into the generated preproc.y).
📄 src/interfaces/ecpg/preproc/preproc_yytype.h (L29-L33)
This is a hand-maintained duplicate of the %union in ecpg.header, with no build-time reference (not used by the Makefile, meson.build, or parse.pl) and no mechanism to detect drift. If a field is later added/reordered in ecpg.header's %union (the actual generation input for preproc.c), this copy will silently diverge, giving parser.c/pgc.c a different YYSTYPE layout than the generated preproc.c uses. Because base_yylval is a shared extern YYSTYPE, a size/layout mismatch is an ODR/ABI hazard that corrupts memory silently rather than failing to compile. Consider generating this header from the same source as preproc.c's union, or at minimum add a check (e.g. a _Static_assert on sizeof, or a comment cross-referencing ecpg.header's line) so drift is caught at build time.
📄 src/interfaces/ecpg/preproc/pgc.c (L961-L966)
Stack buffer overflow: strlcpy(inc_file, base_yytext, sizeof(inc_file)) truncates the filename to fit inc_file (MAXPGPATH), but the subsequent strcat(inc_file, ".h") appends 3 bytes (.h + NUL) with no capacity check. If the quoted include filename is within 2 bytes of MAXPGPATH, strlcpy fills the buffer and strcat writes past its end. Unlike the include_paths branch below (guarded by strlen(ip->path) + strlen(base_yytext) + 4 > MAXPGPATH), this branch has no length reservation for the .h suffix. Reachable from an EXEC SQL INCLUDE "..." directive.
💡 Suggested change
Before:
if (strlen(inc_file) <= 2 ||
strcmp(inc_file + strlen(inc_file) - 2, ".h") != 0)
{
strcat(inc_file, ".h");
f = fopen(inc_file, "r");
}
After:
if (strlen(inc_file) <= 2 ||
strcmp(inc_file + strlen(inc_file) - 2, ".h") != 0)
{
if (strlen(inc_file) + 3 <= sizeof(inc_file))
{
strcat(inc_file, ".h");
f = fopen(inc_file, "r");
}
}
📄 src/interfaces/ecpg/preproc/pgc.c (L586-L595)
Fragile public helper: strlen(base_yytext) - 1 underflows to SIZE_MAX/UINT_MAX when base_yytext is empty. Current lex rules guarantee non-empty input, so this is not reachable today, but the helper is publicly exported (pgc_internal.h) and carries no guard of its own. If the loop condition j > 0 skips (empty input), the subsequent unconditional base_yytext[j] read uses the wrapped index. Add an early-return guard on empty text to make the contract robust. Same pattern in pgc_handle_undef.
📄 src/pl/plpgsql/src/Makefile (L83-L84)
$(LIME) is undefined in the make build. src/Makefile.global.in provides BISON = @BISON@, FLEX, etc., but there is no LIME = @LIME@ substitution (LIME is only resolved on the meson side via find_program(get_option('LIME'))). As a result $(LIME) expands to empty and this recipe becomes -d. pl_gram.lime, which make tries to run as the command -d. and the build fails.
This is also inconsistent with every other grammar Makefile converted in this same change: src/backend/parser/Makefile, src/backend/replication/Makefile, and src/backend/bootstrap/Makefile all invoke the literal program lime -d. $<. Use the same form here (or add a proper LIME var to Makefile.global.in for all of them).
💡 Suggested change
Before:
pl_gram.c: pl_gram.lime
$(LIME) -d. $<
After:
pl_gram.c: pl_gram.lime
lime -d. $<
📄 src/pl/plpgsql/src/plpgsql.h (L1315-L1316)
These three prototypes are placed under the "Scanner functions in pl_scanner.c" section, but only plpgsql_yy_drain_lookahead is defined in pl_scanner.c. plpgsql_yy_get_lookahead and plpgsql_yy_clear_lookahead are emitted into the Lime-generated pl_gram.c (Lime's prefix-renamed Parse_get_lookahead/clear helpers). The section comment is therefore inaccurate for two of the three, which will mislead a reader grepping pl_scanner.c for their definitions. Confidence: high. Suggest a one-line note clarifying that get/clear_lookahead live in the generated parser, or moving them out of this section. (Low severity, documentation only.)
📄 src/pl/plpgsql/src/pl_gram_types.h (L30-L33)
This header includes postgres.h, which violates PostgreSQL's include convention that headers never pull in postgres.h (the translation unit includes it first). The sibling header added in this same series, repl_gram_yytype.h, solves the identical YYSTYPE-hoisting problem and correctly includes only the specific prerequisites it needs (access/xlogdefs.h, nodes/parsenodes.h, nodes/pg_list.h) -- not postgres.h. This file should do the same: drop postgres.h and include only what the union body actually references (e.g. common/keywords.h for the keyword typedefs and plpgsql.h for the PLpgSQL_* types; parser/scanner.h for core_YYSTYPE). Confidence: high.
💡 Suggested change
Before:
#include "postgres.h"
#include "common/keywords.h"
#include "parser/scanner.h"
#include "plpgsql.h"
After:
#include "common/keywords.h"
#include "parser/scanner.h"
#include "plpgsql.h"
📄 src/pl/plpgsql/src/pl_gram_types.h (L8-L12)
Stale/aspirational comment. pl_gram.y no longer exists in this tree and pl_gram.c/pl_gram.h are now generated by Lime from pl_gram.lime (see the Makefile rule pl_gram.c: pl_gram.lime; $(LIME) -d. $<). There is no %union in pl_gram.lime (Lime uses per-symbol %type declarations), so this union is the sole hand-maintained authoritative definition, and "stay in sync with the original %union from pl_gram.y" points at a source that was removed. Reword to describe the current reality: this header is the authoritative YYSTYPE for the Lime-generated parser and must be kept consistent with the per-symbol %type declarations in pl_gram.lime. Confidence: high.
📄 src/pl/plpgsql/src/pl_gram_types.h (L36-L42)
The YYSTYPE_IS_DECLARED guard and its comments describe a "legacy build path before Phase 2j flips" where a bison-generated pl_gram.h emits its own YYSTYPE typedef. That path no longer exists in this tree -- the Makefile already generates pl_gram.c via $(LIME), not bison. So the guard is guarding against a condition that cannot occur, and the comments reference an internal, future-tense project milestone ("Phase 2j") that is meaningless in the upstream tree. Comments must describe current behavior. Either remove the dead guard machinery and comments now that bison is gone, or, if the guard is retained defensively, drop the "Phase 2j"/"legacy" framing and state plainly why. Confidence: high.
📄 src/pl/plpgsql/src/pl_gram_types.h (L103-L107)
Same stale/aspirational framing here: "Once Phase 2j flips to Lime, pl_gram.h has no body of its own" describes a migration that has already happened in this tree (Lime already generates pl_gram.h). Drop the future-tense "Phase 2j" milestone language and describe what the code does now. Confidence: high.
📄 src/pl/plpgsql/src/pl_scanner.c (L372-L372)
This comparison is broken. Here token is the raw value returned by core_yylex() (core-scanner / bison numbering, where the operator token Op == 265, per the contract in parser/scanner.h: "any bison parser ... IDENT = 258 and so on"). But OP is the token macro from the Lime-generated pl_gram.h, and Lime does not use bison's numbering. The very existence of plpgsql_ascii_to_lime_token() / plpgsql_lime_to_ascii_token() in pl_gram.lime (mapping e.g. '(' -> LPAREN) proves Lime's terminal codes differ from the core/ASCII codes; if they matched, no translation layer would be needed. Therefore token == OP will (almost certainly) never be true, so <<, >> and # are never rewritten to LESS_LESS / GREATER_GREATER / '#'. That silently breaks block/loop labels (<<label>>) and the # diagnostics operator for valid PL/pgSQL. The diff's new comment block asserting the codes are "numbered identically in every grammar built atop parser/scanner.h" is false for Lime (scanner.h only guarantees this for bison parsers). The comparison must be made against the core scanner's Op value, not Lime's OP (e.g. keep comparing to the core token code, or translate token before the check). (high confidence)
📄 src/test/isolation/.gitignore (L8-L8)
Removing /specscanner.c is correct now that it is a committed hand-written driver. However, this .gitignore is now incomplete for the Lime migration: the new generated artifacts in this directory are not ignored. Per the sibling Makefile/meson.build, specscanner.lex is compiled to specscanner_lex.c and specscanner_lex.h (meson custom_target output; specscanner.c includes specscanner_lex.h), and Lime emits specparse.out (added to the Makefile clean rule). None of these are checked in, yet none are ignored, so they will show up as untracked after a build and can be accidentally committed. Add ignore entries for them. (high confidence)
💡 Suggested change
Before:
-/specscanner.c
After:
/specparse.out
/specscanner_lex.c
/specscanner_lex.h
📄 src/test/isolation/Makefile (L51-L52)
The Makefile build of isolationtester is broken: specscanner.c does #include "specscanner_lex.h" (see specscanner.c line 33), a header generated from specscanner.lex. The meson build generates it via the spec_scanner_lex custom target (specscanner.lex -> specscanner_lex.c/specscanner_lex.h, meson.build lines 23-28), but this Makefile only adds a rule for the grammar (specparse.c: specparse.lime) and has NO rule to generate specscanner_lex.c/.h from specscanner.lex. specscanner_lex.h is not committed anywhere in the tree, so specscanner.o will fail to compile under autoconf/Make. Add the equivalent Lime lexer-generation rule (produce specscanner_lex.c and specscanner_lex.h from specscanner.lex) and wire specscanner_lex.o into OBJS. Confidence: high.
📄 src/test/isolation/Makefile (L55-L55)
This dependency line is incomplete for the Makefile build. specscanner.c includes the generated specscanner_lex.h (specscanner.c line 33), so specscanner.o must also depend on specscanner_lex.h. As written (even once the missing lexer-generation rule is added), make may compile specscanner.o before the generated lexer header exists. Add specscanner_lex.h here. Confidence: high.
📄 src/test/isolation/Makefile (L61-L61)
The clean target removes the generated grammar (specparse.h/.c) and specparse.out, but does not remove the generated lexer artifacts specscanner_lex.c/specscanner_lex.h that the meson build produces from specscanner.lex. Once the missing Make lexer-generation rule is added, these must also be cleaned here to avoid stale generated files across VPATH/tarball builds. Confidence: moderate.
📄 src/test/isolation/spec_gram_yytype.h (L11-L12)
This comment references an internal migration label ("pre-Phase 2f specparse.y") that has no meaning in the committed tree once this patch lands. Per PostgreSQL comment discipline, comments should describe current behavior, not transient project-phase narrative that will read as stale to future readers. Consider dropping the "pre-Phase 2f" qualifier and just referring to the previous Bison grammar (specparse.y).
💡 Suggested change
Before:
* The union shape matches the Bison %union in the retired grammar
* (pre-Phase 2f specparse.y); it is kept identical so that the
After:
* The union shape matches the Bison %union in the previous Bison
* grammar (specparse.y); it is kept identical so that the
📄 src/test/isolation/specscanner.c (L54-L56)
Aspirational future-tense comment. This describes unshipped upstream work ("When Lime upstream P0-NEW-12 lands and threads the %lexer_extra_argument binding through ... this can collapse") and embeds an internal ticket ID. Per PostgreSQL comment discipline, comments must describe what the code does now, not future/aspirational behavior; internal tracker IDs are noise on a -hackers patch. Confidence: high. Suggest describing only the current mechanism (file-scope line counter reset per parse, incremented by the lexer's newline rules).
📄 src/test/isolation/specscanner.c (L243-L248)
Dead / unreachable code. spec_yyerror() unconditionally calls exit(1) (see its definition above), so these three cleanup calls and return 1 can never execute. The comment even concedes this ("spec_yyerror exits, but for analyzers"). Per the minimalism/YAGNI discipline this scaffolding should be removed; if a non-fatal error path is genuinely wanted, spec_yyerror must be changed to return rather than exit. As written it is misleading dead code. Confidence: high.
📄 src/test/isolation/specscanner.c (L193-L195)
Non-functional stub kept only to preserve a public symbol with no in-tree callers. I verified spec_yylex has no callers anywhere in src/ or contrib/ (only the declaration in isolationtester.h and this definition). A stub that always returns 0 is a silent footgun: if a caller ever appears, 0 reads as the EOF token and lexing terminates immediately with no error. Per YAGNI, prefer removing both the stub and its declaration rather than shipping a landmine. The accompanying file-scope spec_yylval exists solely to feed this dead stub and can go with it. Confidence: high.
📄 src/test/modules/dummy_grammar_ext/dummy_grammar_ext.c (L6-L11)
This file-header comment describes a design that no longer exists. It narrates a "subprocess pipeline" that "queues the extension for rebuild", a ".so" that "dlopens", a "$PGDATA/pg_parser_cache" directory, and dispatch through a "dlopen'd base_yyparse". The authoritative parser_extension.h (lines 9-12) states the subprocess (Track A) path was removed and the in-process compose (Track B) is the live implementation. Rewrite this header to describe current behavior: registration queues fragments, pg_grammar_ext_prewarm() composes them in-process at postmaster start, and there is no subprocess/.so/cache. Aspirational/stale comments block list submission.
📄 src/test/modules/dummy_grammar_ext/dummy_grammar_ext.c (L54-L58)
This doc-comment block directly contradicts the function body comment just below it. Here it says the callback is "wired but unreachable at runtime" and "the body is documentation"; the body comment (line 66) says "The trampoline now actually fires this callback." Both cannot be true. Per PostgreSQL comment discipline, remove the stale future-tense Track A/Track B narration and describe only what happens now: the composed snapshot's reduce dispatch invokes this callback, which logs a NOTICE and clears the LHS slot.
📄 src/test/modules/dummy_grammar_ext/dummy_grammar_ext.c (L91-L92)
Comment describes removed behavior. There is no "rebuild pipeline" and no "dlopen'd parser" in the live in-process compose implementation, and "full token-table integration is Track B's problem" is stale future-tense. Update to reflect the current in-process compose path.
📄 src/test/modules/dummy_grammar_ext/dummy_grammar_ext.c (L113-L115)
The NOTICE message states "rebuild will run on first parse", which no longer matches behavior: registered fragments are composed in-process at postmaster start via pg_grammar_ext_prewarm(), not lazily on first parse. Adjust the message to avoid misleading the test harness/reader.
📄 src/test/modules/dummy_grammar_ext/dummy_grammar_ext.c (L117-L119)
Embedded newline in the primary errmsg. PostgreSQL message style requires the primary message to be a single line with no embedded newlines; multi-line payloads belong in errdetail(). Move the fragment text into errdetail("%s", frag).
📄 src/test/modules/grammar_ext_compose/compose_ext_foxtrot.c (L6-L9)
This header comment contradicts the code and the actual, verified behavior. It claims foxtrot "should fail register() with a clear error" and that "expect_failure=true so the helper logs the failure as expected (NOTICE)". But the code sets .expect_failure = false (line 55), and the helper's register_compose_extension() in compose_ext_helpers.h emits a NOTICE on success when expect_failure is false (a WARNING only on unexpected failure) — so the mapping described here is inverted. Moreover, the sibling TAP test t/001_compose.pl Test 4 explicitly documents (in a TODO: block) that foxtrot's register() does NOT fail; the K_GRAMMAR_ALPHA collision is only detected later at in-process compile time. So the leading comment describes the opposite of both the code and the documented API behavior. This stale comment will mislead maintainers about whether the collision path fails at register() time — reconcile it with the .expect_failure = false reality (and fix the inline comment on lines 48-53, which also incorrectly states register() "should fail" in the alpha+foxtrot load and that a WARNING fires on standalone success). Confidence: high. Note also the ASCII/wrapping artifacts: the token expect_failure is split as expect_-\nfailure here, and line 51 has an alpha+ foxtrot spacing glitch.
📄 src/test/modules/grammar_ext_compose/compose_ext_golf.c (L34-L37)
Aspirational/conditional comment. "if we add the symbol-table check" describes a check that does not exist: the register() path (register_compose_extension -> pg_grammar_ext_add_rule -> pg_grammar_ext_register in parser_extension.c) validates only the RHS symbol count limit, not whether each RHS token is defined. Per PostgreSQL comment discipline, comments must describe current behavior, not a hypothetical future check. State that an unknown RHS token surfaces at lime-rebuild time, and drop the "(if we add ...)" clause.
Confidence: high.
💡 Suggested change
Before:
* Rule references K_GRAMMAR_ALPHA which alpha must have declared first.
* If alpha isn't loaded, this rule's reference to an unknown token will
* surface either at register() (if we add the symbol-table check) or at
* lime-rebuild time (lime errors on undefined RHS symbol).
After:
* Rule references K_GRAMMAR_ALPHA which alpha must have declared first.
* If alpha isn't loaded, this rule's reference to an unknown token
* surfaces at lime-rebuild time (lime errors on undefined RHS symbol);
* register() does not currently validate RHS token existence.
📄 src/test/modules/grammar_ext_compose/compose_ext_golf.c (L15-L15)
The header comment claims "reversed order should produce a clear error," but no test exercises the reversed load order (compose_ext_golf,compose_ext_alpha). Test 5 in t/001_compose.pl only covers the correct order (alpha,golf). Either add a reversed-order case asserting the error, or drop this claim so the comment does not describe untested/unverified behavior.
Confidence: moderate.
📄 src/test/modules/grammar_ext_compose/compose_ext_helpers.h (L65-L65)
pgindent artifact: ComposeType is not in the typedefs list, so pgindent misformats these lines with extra tabs (const\t\tComposeType here and }\t\t\tComposeType; at the typedef). The sibling structs (ComposeToken, ComposeRule, ComposePrec) are formatted normally, making this inconsistent within the same file and something git diff --check/pgindent reviewers will flag. Add ComposeType to typedefs.list (or the local pgindent invocation) and re-run pgindent so all four structs format identically.
📄 src/test/modules/grammar_ext_compose/compose_ext_hotel.c (L39-L44)
This comment describes a standalone-hotel scenario ("when loaded standalone the precedence names a symbol Lime doesn't know yet") and claims "the rebuild either resolves them later or errors at compile time -- either way the test asserts against the postmaster's log." This is inaccurate: per t/001_compose.pl, hotel is only ever loaded together with alpha (Test 6, alpha+hotel); there is no standalone-hotel test, so nothing asserts against the log for that case. The comment describes behavior that is neither exercised nor tested, and it hedges between two mutually exclusive outcomes. PostgreSQL comment discipline requires comments to describe what the code/test does now, not speculative or aspirational behavior. Trim this to the actual, tested scenario (hotel + alpha) or add a real standalone test that pins one deterministic outcome. (moderate confidence)
📄 src/test/modules/grammar_ext_compose/compose_ext_india.c (L18-L20)
Comment/code drift: the grammar sketch here writes the terminal as X, but the actual productions (lines 49-53) and the token table (line 40) use K_GRAMMAR_XX (lexeme grammar_xx). There is no X symbol in this extension. Per PostgreSQL comment discipline, comments must describe what the code does now; use the real symbol name so a maintainer reading the sketch can match it to the rules.
- stmt ::= india_if
- india_if ::= K_GRAMMAR_INDIA K_GRAMMAR_XX K_GRAMMAR_THEN india_if
- india_if ::= K_GRAMMAR_INDIA K_GRAMMAR_XX K_GRAMMAR_THEN india_if K_GRAMMAR_ELSE india_if
- india_if ::= K_GRAMMAR_XX
📄 src/test/modules/grammar_ext_overlap/ext_mongo_jsonb.c (L7-L9)
Comment-accuracy nit (low confidence): the header says "the value-shaping code lives in the reduce callback, not in the grammar", but the rules use the shared overlap_reduce callback (in overlap_helpers.h) which only emits a NOTICE and sets *lhs_out = NULL -- there is no value-shaping code anywhere. Comments should describe what the code does now; consider dropping the value-shaping claim to avoid implying behavior that doesn't exist.
📄 src/test/modules/grammar_ext_compose/t/001_compose.pl (L23-L26)
This header enumeration is stale and contradicts the implementation. Items 2/7/8 describe a .so cache, "SHA256 of the input fragment", and "cache hit avoids re-running lime+cc" -- but the live implementation (parser_extension.c: "no subprocess, no C compiler, no .so cache, no dlopen") removed Track A entirely, and the test body itself asserts unlike(... /pg_parser_cache/... /) and comments "Track B has no .so cache". The list also drifts from the executed blocks: the code has a 9th block that is the india conflict gate, not "base grammar invariance". Rewrite this header to describe what the file actually tests (in-process compose, no cache, conflict-gate FATAL), per the comment-accuracy rule.
📄 src/test/modules/grammar_ext_compose/t/001_compose.pl (L156-L159)
The parser_extension.h contract explicitly states a mismatched token redeclaration "is an error", and foxtrot is designed to "fail register() with a clear error". Wrapping this in a TODO block means the assertion can never fail the suite (failure is expected-and-ignored) and a pass is flagged as unexpected -- so it will never catch a regression in the token-conflict path and, worse, documents a live API-contract gap as acceptable. Either implement register()-time conflict detection so this becomes a real like(..., qr/register\(\) failed/) assertion, or assert the FATAL-at-prewarm behavior the code actually produces (as Test 9 does) instead of hiding it under TODO. A test that documents a defect but cannot enforce the contract is worthless.
📄 src/test/modules/grammar_ext_compose/t/001_compose.pl (L66-L75)
This bespoke slurp helper duplicates PostgreSQL::Test::Utils::slurp_file(), which is already used later in this same file (Test 9). Drop log_text() and use slurp_file($node->logfile) consistently (DRY); it also handles encoding/newline concerns uniformly.
📄 src/test/modules/grammar_ext_compose/t/001_compose.pl (L57-L57)
Stale/misleading comment. start_with_extensions() does not "defer logging to disk so we can grep AFTER stop()" -- most tests here grep the log while the node is still running (log_text() is called before $node->stop). The comment describes an intent the code does not implement; drop or correct it.
📄 src/test/modules/grammar_ext_compose/t/001_compose.pl (L51-L51)
Stale doc comment: start_with_extensions() returns only the node ($node), not the "(cluster, server_log)" tuple this comment claims. Update to match.
📄 src/test/modules/lime_in_process_smoke/lime_in_process_smoke.c (L77-L78)
The status string embeds a raw pointer via %p, and the TAP test (t/001_smoke.pl) asserts on it with qr/^ok: snapshot built \(snap=0x[0-9a-f]+\)/. %p output is implementation-defined: on MSVC/Windows pointers are printed uppercase and without a 0x prefix (e.g. 00000000005A1B30), so that regex will fail there. PostgreSQL must pass on Windows/MSVC. Beyond portability, leaking an internal heap address into user-visible SQL output is a footgun with no diagnostic value. Drop the pointer and return a stable string (e.g. "ok: snapshot built").
💡 Suggested change
Before:
appendStringInfo(&out, "ok: snapshot built (snap=%p)",
(void *) snap);
After:
appendStringInfoString(&out, "ok: snapshot built");
📄 src/test/modules/lime_in_process_smoke/lime_in_process_smoke.c (L69-L70)
len is taken from strlen(grammar) after text_to_cstring. A text datum may contain embedded NUL bytes; strlen truncates at the first NUL and understates the payload length passed to the compiler. Since the API takes an explicit length, use the true varlena length: VARSIZE_ANY_EXHDR(grammar_text). This also lets you drop the strlen call.
💡 Suggested change
Before:
grammar = text_to_cstring(grammar_text);
len = strlen(grammar);
After:
grammar = text_to_cstring(grammar_text);
len = VARSIZE_ANY_EXHDR(grammar_text);
📄 src/test/modules/grammar_ext_overlap/t/001_overlap.pl (L92-L95)
These two unlike assertions are vacuous: the guarded strings never exist anywhere in the implementation. I searched the whole tree (*.c, *.h, *.py) and there is no code that ever emits "running lime to rebuild parser", and no /pg_parser_cache/<sha256>.so path string is ever logged (the only pg_parser_cache reference is a comment in dummy_grammar_ext.c). Because the substrings can never appear in the log, both unlike checks pass unconditionally and would still pass even if the in-process compose path were completely broken or replaced by a subprocess/cache scheme that logged different text. A negative assertion against a string that no code emits cannot catch a regression. Either assert against the actual message the subprocess/cache path would emit (so the negative is meaningful), or drop these two checks and instead positively assert the in-process path (the like(... composing grammar in-process ...) above already does this). Confidence: high.
📄 src/test/modules/grammar_ext_overlap/t/001_overlap.pl (L197-L200)
qr/\b1\b/ (and the 2/3/4 variants) match any occurrence of the digit anywhere in psql's default output, not just the SELECT result value. Default psql output includes column headers, the (N row) footer, and other integers, so these boundary matches can be satisfied by unrelated text (e.g. a row count) and yield false passes -- the test would still pass even if statement interleaving regressed. Capture deterministic result values instead, e.g. run each SELECT via safe_psql (tuples-only) and compare the exact value, or run the script with -t -A and assert on exact lines. Confidence: medium.
📄 src/test/modules/grammar_ext_overlap/t/001_overlap.pl (L210-L213)
Stale/misleading comment. Every test here builds a fresh node (PostgreSQL::Test::Cluster->new + init), and the grammar compose is done in-process at postmaster start (parser_pushparse.c) with no on-disk .so/cache shared between nodes -- I confirmed no cross-node cache exists. So "already cached from test 1" and "Cache keys MAY differ ... part of the SHA256 input" describe a caching mechanism this code path does not use, and will mislead a future maintainer about actual behavior. Reword to reflect that each node composes independently at startup. Confidence: high.
📄 src/test/modules/grammar_ext_overlap/t/001_overlap.pl (L223-L224)
Test 4 claims order-independence but does not actually verify it. It only checks that each keyword produces some overlap:.*reduced NOTICE under reverse load order; it never compares the reverse-order accept/reduce set against the forward-order one. As written it would pass even if a reduce fired for the wrong label or if forward vs reverse diverged in which productions accept. To make the guarantee real, assert the specific expected label per lexeme (as Test 2 does) and/or diff the full accept set collected under both orders. Confidence: medium.
📄 src/test/modules/lime_in_process_smoke/t/001_smoke.pl (L68-L68)
Non-portable happy-path assertion. The C function formats the pointer with %p (appendStringInfo(&out, "ok: snapshot built (snap=%p)", ...) in lime_in_process_smoke.c), but %p output is implementation-defined. glibc/Linux prints 0x + lowercase hex, but other libc implementations (notably Windows/MSVC) print uppercase hex with no 0x prefix. This regex hard-codes 0x[0-9a-f]+ and will fail to match on those platforms. Either loosen the regex (e.g. snap=0x[0-9a-fA-F]+|snap=[0-9A-Fa-f]+) or, better, have the C side emit a fixed format instead of relying on %p. (Portability: high confidence.)
💡 Suggested change
Before:
+like($result, qr/^ok: snapshot built \(snap=0x[0-9a-f]+\)/,
After:
+like($result, qr/^ok: snapshot built \(snap=0x[0-9a-fA-F]+\)/i,
📄 src/test/modules/lime_in_process_smoke/t/001_smoke.pl (L82-L82)
Weak error-path assertion. qr/^error: rc=-?\d+ msg=/ matches ANY error output the function can emit, so it does not verify that the unmatched-brace input specifically triggered a parse/lex failure. If a regression caused valid grammars to also error out (or the compile path degraded to always returning an error), this test would still pass. Assert against the specific rc value and/or a substring of the actual failure message tied to the malformed input so the test can actually catch regressions. (Moderate confidence.)
📄 src/test/modules/lime_in_process_smoke/t/001_smoke.pl (L65-L65)
Dollar-quote interpolation footgun. The grammar text is spliced into a $$...$$ dollar-quoted literal. If a grammar ever contains the literal $$, the quote terminates early and the query breaks (or worse, silently changes meaning). The current fixed grammars are safe, but as this test grows use a tagged dollar-quote to make it robust, e.g. $grammar$...$grammar$. (Low confidence / hardening.)
📄 src/test/modules/lime_in_process_smoke/t/001_smoke.pl (L50-L51)
Aspirational/WIP prose and an out-of-tree reference do not belong in a committed test. The header uses future tense ("No changes to parser.c or parser_extension.c yet", "will be updated to reflect the actual in-process semantics") and references an internal path .agent/notes/track-b-phase2-design.md that is not part of the tree. For a patch destined for pgsql-hackers, comments should describe what the code does now; roadmap/WIP notes belong in the commit message. Trim this to a concise description of what the test verifies. (Low confidence / hygiene.)
📄 src/test/modules/parser_microbench/parser_microbench.c (L65-L67)
Error-path context leak: raw_parser() runs on fully user-controlled SQL, so any syntax error triggers ereport(ERROR) (a longjmp). Both the warm-up parse and the timed loop parse arbitrary text, so an invalid query reliably escapes this function with bench_ctx never deleted. There is no PG_TRY/PG_CATCH to tear down the context before re-throwing. Wrap the parse work in PG_TRY/PG_CATCH and MemoryContextDelete(bench_ctx) in the catch block (or make the context a short-lived child that is guaranteed reclaimed). Confidence: high.
📄 src/test/modules/parser_microbench/parser_microbench.c (L78-L78)
Portability gate: raw clock_gettime(CLOCK_MONOTONIC, ...) is not portable. No other code in the tree calls clock_gettime directly; MSVC/Windows has no clock_gettime, so this will fail to build there. Use the tree's portable timing wrapper instr_time (instr_time.h): INSTR_TIME_SET_CURRENT(t0/t1) and compute the delta with INSTR_TIME_SUBTRACT + INSTR_TIME_GET_NANOSEC. That also removes the need for <time.h> and the manual tv_sec/tv_nsec arithmetic below. Confidence: high.
📄 src/test/modules/parser_microbench/parser_microbench.c (L90-L91)
Manual timespec arithmetic goes away once instr_time is used (INSTR_TIME_GET_NANOSEC returns int64 nanoseconds portably). Keeping the raw computation ties this to the non-portable struct timespec fields. Confidence: high.
📄 src/test/modules/parser_microbench/parser_microbench.c (L43-L43)
No SQL binding for this SQL-exposed function. The file declares PG_FUNCTION_INFO_V1(parser_microbench) and the header comment states it "Exposes one SQL function", but there is no .control file and no SQL install script anywhere in the tree that maps a CREATE FUNCTION ... AS 'parser_microbench' to this C symbol. As shipped, the function is unreachable from SQL and cannot be invoked or tested. Add the extension control/SQL files (or document/register the intended invocation path). Confidence: high.
📄 src/test/modules/parser_microbench/parser_microbench.c (L41-L41)
Build wiring is inconsistent: this module is added to src/test/modules/meson.build (subdir) but is NOT added to src/test/modules/Makefile SUBDIRS. Under the make/Autoconf build it will not be built, and there is no per-module Makefile in the change. Either add it to both build systems or, if the make build is being retired in this series, do so explicitly. As-is the commit does not build consistently across the tree's build systems. Confidence: high.
📄 src/test/modules/parser_microbench/parser_microbench.c (L58-L61)
No tests accompany this module (no t/*.pl, no sql/expected). The function has clear error/edge paths that must be covered: iterations <= 0 (the ERROR path), NULL query, and invalid SQL that makes raw_parser() throw (which also exercises the leak concern above). Per project policy a behavioral addition without tests is WIP. Confidence: high.
📄 src/test/regress/pg_regress.c (L1246-L1246)
Switching execl to execlp here is an unjustified and undesirable change. shellprog is initialized once from SHELLPROG, which is always an absolute path (/bin/sh in meson, $(SHELL) in the Makefile) and is never reassigned. Because execlp only consults PATH when its argument contains no slash, this change is a functional no-op for the current value of shellprog.
Worse, it widens the semantics into a footgun: it introduces PATH-based lookup of the shell, so if SHELLPROG were ever set to a bare name, a caller-controlled PATH could select an arbitrary sh. This is an unrelated change with no stated purpose, no test, and no benefit; revert it to keep the diff minimal and avoid the PATH-resolution hazard. (high confidence)
💡 Suggested change
Before:
execlp(shellprog, shellprog, "-c", cmdline2, (char *) NULL);
After:
execl(shellprog, shellprog, "-c", cmdline2, (char *) NULL);
📄 src/tools/lime_format (L56-L57)
In-place clobber without validation is a data-loss footgun. Unlike the companion lime_format_check, which formats a copy in a temp dir and only diffs, this script runs lime -F directly on the source file and unconditionally moves the .formatted output over the original. If lime -F exits 0 but emits a truncated/corrupt .formatted (e.g. disk full, partial write), the committed source is silently destroyed with no backup. Consider validating the output (e.g. re-run lime -L/re-parse, or at least reject empty output) before the move, and/or format via a temp copy as the check script already does.
📄 src/tools/lime_format (L39-L39)
read_text() (and the implicit write via the moved file) relies on the platform default encoding. PostgreSQL tooling must run on Windows/MSVC and non-UTF-8 locales where this can raise UnicodeDecodeError or mangle non-ASCII content. Read explicitly as UTF-8, e.g. lime_path.read_text(encoding='utf-8').
📄 src/tools/lime_format (L37-L37)
part.startswith(p) over-skips: any path component merely beginning with these prefixes is silently excluded (e.g. a directory builder/installation, or a file build_grammar.lime). For a tree-wide formatter this silently drops in-scope files (POLA violation) and can let unformatted files pass lime_format_check, which shares this exact logic. Match exact component names instead, e.g. part in SKIP_PATTERNS.
📄 src/tools/lime_format (L40-L43)
subprocess.run has no timeout, so a hung lime invocation stalls the build indefinitely, and args.lime is not validated to exist before invocation (a bad path yields a raw FileNotFoundError traceback rather than a clean diagnostic). Consider adding a timeout= and an up-front existence/executable check on args.lime.
📄 src/tools/lime_format_check (L11-L14)
This header contradicts the meson.build wiring for this very test and cites a version that does not match the tree. meson.build (lines ~604-606) says: "Two passes are run because Lime's formatter is not idempotent on its first pass for %left/%right/%nonassoc symbol order (stabilizes after pass 2)", yet this script runs only a single lime -F pass. Additionally, flake.nix pins v1.11.0, not v0.6.0, and the rest of the change set describes "Lime v1.5.x". If the formatter really needs two passes, this single-pass check will emit spurious/false failures; if it is now idempotent, the meson.build comment is stale. Either way the version claim is wrong. Reconcile the comment with the actual pinned version and the number of passes actually performed.
📄 src/tools/lime_format_check (L47-L47)
Prefix matching (part.startswith(p)) is over-broad and silently drops files from the check. A directory such as buildinfo/, installers/, or any component merely beginning with one of these strings would be skipped, so .lime files there escape the canonical-format guarantee. Use exact component matching instead, e.g. part in SKIP_PATTERNS. (The companion src/tools/lime_format uses the same pattern, so keep them consistent.)
💡 Suggested change
Before:
if any(part.startswith(p) for part in rel.parts for p in SKIP_PATTERNS):
After:
if any(part in SKIP_PATTERNS for part in rel.parts):
📄 src/tools/lime_format_check (L72-L73)
read_text() uses the platform's default locale encoding. On Windows/MSVC buildfarm animals this can be a non-UTF-8 codepage, so a grammar file containing any non-ASCII byte can raise UnicodeDecodeError or yield a spurious diff, producing false format-check failures. Since this is fundamentally a byte-for-byte canonical comparison, compare bytes (read_bytes()) or pass an explicit encoding='utf-8'.
💡 Suggested change
Before:
src_text = lime_path.read_text()
fmt_text = formatted.read_text()
After:
src_text = lime_path.read_text(encoding='utf-8')
fmt_text = formatted.read_text(encoding='utf-8')
📄 src/tools/lime_convert_gram.py (L1303-L1304)
Label scheme mismatch for RHS index > 25. _label_for_index returns a multi-character label P{idx} (e.g. P26) here, but _labels_referenced only recognizes single uppercase letters via (?<![A-Za-z0-9_])([A-Z])(?![A-Za-z0-9_]) and @([A-Z]). A P26 reference in a rewritten action will never be matched (the regex requires the letter not be followed by an alnum, and P26 is P+digits), so the corresponding (P26) RHS decoration is dropped, or an emitted-but-undetected label triggers Lime's unused-label error. PostgreSQL's gram.y has rules with well over 25 RHS symbols, so this is a real correctness defect, not a 1%-edge case. The fallback and the reference-detection scheme must use a consistent, mutually-recognizable label format (confidence: high).
📄 src/tools/pgindent/pgindent (L1-L1)
This shebang change is unrelated to the stated purpose of the patch (grammar/lexer/parser tooling) and should be dropped to keep the diff minimal. It also breaks the tree-wide convention: every other Perl script in the repository uses #!/usr/bin/perl (e.g. src/tools/copyright.pl, src/backend/catalog/genbki.pl, and ~40 others), so this makes pgindent the sole outlier. Switching to /usr/bin/env perl is a tree-wide policy decision that belongs in its own dedicated patch discussed on -hackers, not slipped into an unrelated change. Confidence: high.
💡 Suggested change
Before:
#!/usr/bin/env perl
After:
#!/usr/bin/perl
📄 src/tools/lime_lint (L59-L59)
Non-ASCII character in source. The U+2713 checkmark ('✓') appears in both this comment and the matching expression below. The project mandates ASCII-only source and diffs; this is a hard gate for a pgsql-hackers patch and can cause encoding issues across editors/build environments. Since this is CI-gating detection logic, prefer keying off the machine-readable exit code rather than scraping localized/decorated output at all.
💡 Suggested change
Before:
# pre-v0.5.0: '✓ No errors or warnings'
After:
# pre-v0.5.0: 'No errors or warnings'
📄 src/tools/lime_lint (L65-L67)
Fragile pass/fail detection: '0 error(s)' in out is a naive substring test that also matches '10 error(s)', '20 error(s)', '100 error(s)', etc. If lime -L exits 0 while reporting e.g. '10 error(s)', this classifies the run as clean and a broken grammar silently passes the CI gate. Anchor the match (e.g. a regex on (?<!\d)0 error) or, better, rely solely on the tool's exit code for the pass/fail contract. Also remove the non-ASCII checkmark branch.
📄 src/tools/lime_lint (L63-L64)
Stale/aspirational comment: this references a --lint-strict flag, but argparse only defines --lime, --srcdir, and --quiet. No such option exists. Either remove this sentence or implement the flag; comments must describe current behavior.
📄 src/tools/lime_lint (L46-L46)
Prefix-based skip is overly broad: part.startswith(p) also skips legitimate path components like 'building', 'installer', or any dir merely prefixed with 'build'/'install'. That would silently omit valid .lime files from the lint gate with no warning, weakening the coverage guarantee. Use exact component matching instead.
💡 Suggested change
Before:
if any(part.startswith(p) for part in rel.parts for p in SKIP_PATTERNS):
After:
if any(part in SKIP_PATTERNS for part in rel.parts):
📄 src/tools/lime_to_bison_gram.py (L217-L217)
The local type hint for rules contradicts what is actually stored and what downstream consumers expect. Line 381 appends (rhs, prec) 2-tuples, and both this function's return annotation (line 204) and emit_bison's signature (line 388) declare OrderedDict[str, list[tuple[list[str], str | None]]]. The list[list[str]] here is stale/misleading. It is not a runtime bug (the sole mutation site always appends a 2-tuple), but the hint should match. Low/maintainability. Confidence: high.
💡 Suggested change
Before:
rules: "OrderedDict[str, list[list[str]]]" = OrderedDict()
After:
rules: "OrderedDict[str, list[tuple[list[str], str | None]]]" = OrderedDict()
📄 src/tools/pglime (L119-L121)
The --aot-output / --snapshot-output preconditions are validated too late: this check runs only after lime has already executed and after the primary .c/.h files were moved to their meson OUTPUT paths (lines 116-117). On the failure path, the target exits nonzero but leaves a partially-populated output set (.c/.h present, _aot.c/_snapshot.c missing), which ninja may treat inconsistently on a subsequent run. Since --aot always requires --aot-output (and --snapshot requires --snapshot-output), validate these mutual dependencies up front via argparse before building/running the lime command.
📄 src/tools/pglime (L86-L87)
--host-reduce is appended to the lime command unconditionally, but its own help text states it is only meaningful with --snapshot. Nothing enforces that --host-reduce implies --snapshot; if invoked alone it silently forwards a flag lime will either ignore or error on obscurely. Add an up-front guard (e.g. if args.host_reduce and not args.snapshot: sys.exit('--host-reduce requires --snapshot')) so misuse fails fast with an actionable message.
📄 src/tools/pglime (L116-L117)
These shutil.move calls are unguarded, so a failure (permission error, cross-device link, partial state) surfaces as a raw Python traceback rather than the clean sys.exit(msg) used by every other error path in this script (lines 101-104, 121, 124). For consistent, actionable build diagnostics, wrap the moves and report via sys.exit with the source/destination paths.
| repo: context.repo.repo, | ||
| pull_number: prNumber, | ||
| body, | ||
| commit_id: context.payload.pull_request.head.sha, |
There was a problem hiding this comment.
context.payload.pull_request.head.sha is dereferenced without a null-guard. The workflow (ai-code-review.yml) also triggers on workflow_dispatch, where context.payload.pull_request is undefined. In that path this throws TypeError: Cannot read properties of undefined (reading 'head'), and although it is caught by the surrounding try/catch (turning into a warning), inline commenting is completely non-functional for manual dispatch. Fetch the head SHA from the already-fetched PR object instead (e.g. pass pr.head.sha into this function).
| commit_id: context.payload.pull_request.head.sha, | |
| commit_id: pr.head.sha, |
| for (const issue of issues) { | ||
| try { | ||
| // Find the position in the diff for this line | ||
| const position = findDiffPosition(file, issue.line); |
There was a problem hiding this comment.
issue.line originates from free-text model output parsed by extractIssues (regex line[s]?\s+(\d+)), so it is an LLM-emitted number that need not correspond to any real position. findDiffPosition then maps it by counting every non-deleted change starting at 1, ignoring chunk.newStart. The result is that position almost never matches the intended source line, so inline comments land on the wrong lines or are silently dropped via the if (!position) guard. Additionally, createReviewComment's position parameter is deprecated by the GitHub API in favor of line/side against the file's diff. This makes inline commenting effectively unreliable as shipped.
| const promptPath = new URL(`./prompts/${fileType}.md`, import.meta.url); | ||
| return await readFile(promptPath, 'utf-8'); |
There was a problem hiding this comment.
getFileType maps every changed file to a type (e.g. c_code, sql, perl), and loadPrompt then reads ./prompts/${fileType}.md. No prompts/ directory exists in this PR (not present in the changed-files list and not found in the repo). readFile will therefore reject for every reviewable file, causing reviewFile to throw and each file to be skipped with only a core.error log. The feature is non-functional as shipped; the prompt files must be added.
| inputTokens = responseBody.usage.input_tokens; | ||
| outputTokens = responseBody.usage.output_tokens; | ||
| responseText = responseBody.content[0].text; |
There was a problem hiding this comment.
Bedrock/Anthropic responses are dereferenced without any shape checking: responseBody.usage.input_tokens, responseBody.content[0].text, and message.content[0].text. A refusal, an empty content array, or an error-shaped response body will throw a TypeError here, aborting the review with a confusing stack trace instead of a clear diagnostic. Guard for empty/malformed responses before indexing.
| async function callClaude(prompt, code, filename) { | ||
| const fullPrompt = `${prompt}\n\n${code}`; | ||
|
|
||
| // Estimate token count (rough approximation: 1 token ≈ 4 chars) |
There was a problem hiding this comment.
The comment contains a non-ASCII character (≈). The project mandates ASCII-only source. Replace with an ASCII approximation such as ~= or the word 'approximately'.
| // Estimate token count (rough approximation: 1 token ≈ 4 chars) | |
| // Estimate token count (rough approximation: 1 token ~= 4 chars) |
| id: cache | ||
| uses: actions/cache@v3 | ||
| with: | ||
| path: C:\zlib |
There was a problem hiding this comment.
actions/cache@v3 is deprecated; update to @v4 (see openssl job).
| id: cache | |
| uses: actions/cache@v3 | |
| with: | |
| path: C:\zlib | |
| id: cache | |
| uses: actions/cache@v4 | |
| with: | |
| path: C:\zlib |
| id: cache | ||
| uses: actions/cache@v3 | ||
| with: | ||
| path: C:\libxml2 |
There was a problem hiding this comment.
actions/cache@v3 is deprecated; update to @v4 (see openssl job).
| id: cache | |
| uses: actions/cache@v3 | |
| with: | |
| path: C:\libxml2 | |
| id: cache | |
| uses: actions/cache@v4 | |
| with: | |
| path: C:\libxml2 |
| build-openssl: | ||
| name: Build OpenSSL ${{ matrix.version }} | ||
| needs: build-matrix | ||
| if: contains(needs.build-matrix.outputs.matrix, 'openssl') |
There was a problem hiding this comment.
Silent functional gap: the build matrix (via manifest.json + optional_deps) advertises libxslt, icu, gettext, and libiconv, but only build-openssl, build-zlib, and build-libxml2 jobs exist. For the default all (used by pull_request/schedule) or a workflow_dispatch selection of any of these, the corresponding build never runs, yet create-bundle still produces and uploads postgresql-deps-bundle-win64. The bundle is silently incomplete, causing confusing downstream Windows build failures. Either add the missing build jobs or remove the unimplemented options from the matrix/manifest and the workflow_dispatch choice list.
| strategy: | ||
| matrix: | ||
| include: | ||
| - name: openssl | ||
| version: "3.0.13" |
There was a problem hiding this comment.
Version data is duplicated instead of consumed from build-matrix.outputs.matrix. Each build job hardcodes its version in strategy.matrix.include (openssl 3.0.13, zlib 1.3.1, libxml2 2.12.6), which also lives in manifest.json and drives the cache key/gating. Bumping manifest.json changes the cache key and gating but NOT the actually built version -- a drift hazard producing artifacts that mismatch the advertised version. Consume the matrix output instead of re-declaring versions.
| build-openssl: | ||
| name: Build OpenSSL ${{ matrix.version }} | ||
| needs: build-matrix | ||
| if: contains(needs.build-matrix.outputs.matrix, 'openssl') | ||
| runs-on: windows-2022 |
There was a problem hiding this comment.
Missing timeout-minutes. Windows nmake builds of OpenSSL/libxml2 are long-running; a hung download or build (only nmake test is bounded via continue-on-error) can consume a runner indefinitely. Add timeout-minutes to each build job.
| build-openssl: | |
| name: Build OpenSSL ${{ matrix.version }} | |
| needs: build-matrix | |
| if: contains(needs.build-matrix.outputs.matrix, 'openssl') | |
| runs-on: windows-2022 | |
| build-openssl: | |
| name: Build OpenSSL ${{ matrix.version }} | |
| needs: build-matrix | |
| if: contains(needs.build-matrix.outputs.matrix, 'openssl') | |
| runs-on: windows-2022 | |
| timeout-minutes: 60 |
While Flex/Bison have served us well, Lime (an evolution of SQLite's lemon parser generator) is faster than Flex/Bison and maintained and can enable runtime loading of additional grammars.