Skip to content

[2.1.0] Fix switch-without-default and strconv.Itoa lowering - #14

Open
OffgridwithJD wants to merge 6 commits into
commandprompt:masterfrom
OffgridwithJD:fix/audit-correctness
Open

OffgridwithJD wants to merge 6 commits into
commandprompt:masterfrom
OffgridwithJD:fix/audit-correctness

Conversation

@OffgridwithJD

Copy link
Copy Markdown

Depends on #12. This branch is stacked on it, so until #12 merges the diff also shows #12's two commits. Only the last commit, "Fix switch-without-default and strconv.Itoa lowering", belongs here. Merge #12 first.

Summary

Two lowering bugs found during the documentation audit:

  • switch / case / EVALUATE with no default arm (plxjs, plxts, plxphp, plxruby, plxcobol) became a plpgsql CASE statement without ELSE, which raises case not found when nothing matches. In each source language an unmatched switch is a no-op, so the lowering now emits ELSE NULL;. plxplsql is unchanged on purpose: an Oracle CASE statement with no match also raises CASE_NOT_FOUND. plxgo already lowers switch to an IF chain.
  • plxgo strconv.Itoa(x) became (x)::integer. The Atoi/Itoa check compared only the method name's length, which is 4 for both. It now compares the name, so Itoa produces text.

Testing

  • Regression tests in plxjs, plxruby, plxcobol and plxgo. The Itoa test (len(strconv.Itoa(n))) fails on the old code with function length(integer) does not exist.
  • make installcheck: all 13 suites pass on PG 18.4.
  • make differentialcheck passes.

🤖 Generated with Claude Code

jdatcmd and others added 2 commits November 9, 2026 18:46
Every plx language is trusted, so any role with USAGE can send arbitrary
input to the transpiler through CREATE FUNCTION or DO.

- plx_span_text: skip type-annotation tokens (which carry no source span)
  at the edges of a span. A span beginning on one computed its length from a
  NULL pointer and crashed the backend (plxjs/ts/php/ruby/python3).
- plxtsql, plxgo, plxcobol: guard the expression emitters with
  check_stack_depth() and the PLX_MAX_DEPTH counter. Deeply nested calls,
  conversions or subscripts overflowed the C stack (SIGSEGV).
- finally/ensure: cap finally-inside-finally nesting at 8 levels; each level
  doubled the generated body, so a few hundred bytes of source could demand
  gigabytes. The three copies of the try lowering are now one helper,
  plx_emit_try, which also runs the normal-path finally body after the
  protected block, so a finally that raises no longer runs twice.
- plxgo fmt.Sprintf: require a letter verb before lowering to a || chain;
  a '%' followed by a quote consumed half of an escaped '' pair and let
  Go string text escape into the generated SQL.

Regression tests cover each case.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…growth

Second round of the security audit (shared transpiler core):

- emit_string_value: escape every literal backslash and force E''. A
  string ending in a lone backslash inside an interpolation was emitted
  bare in an E'' literal, escaping its closing quote so the next literal's
  text ran as code. The synthetic-string scanners no longer step past the
  end of their input on a trailing backslash.
- Locals: index by lower-cased name in a hash table beside the ordered
  list; the linear lookup made many distinct variables quadratic.
- plxpython3 lexer: resume after the blank/comment lines the indentation
  scan already covered, instead of rescanning the run from every newline
  (O(K^2), and line numbers were counted twice).
- CHECK_FOR_INTERRUPTS in the lexer loop, the expression rewriter and the
  local table, so statement_timeout and cancel reach a long transpile.
- Ternary rewrite: free the condition and both arms after composing the
  CASE, so a chain of N ternaries no longer holds N copies of its tail.
- plxpython3 call(f"..."): reject an f-string procedure name, which was
  unquoted with its prefix into a broken identifier.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@jdatcmd jdatcmd left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adversarial review of the last commit only. The rest is #12, which has changes requested, so merge #12 first.

Tested on PostgreSQL 18.4: make installcheck passes 13/13. A no-default switch returns 0 for unmatched input in plxphp and plxts too, not just the dialects with tests. plxpython3 rejects match, so it cannot hit this bug. The Itoa change is correct.

I did not re-run the red step for the new tests.

Not caused by this PR, but worth an issue: the most common use of the fixed function still fails, because + is not lowered to || for strings in plxgo.

CREATE FUNCTION g_itoa(n int) RETURNS text LANGUAGE plxgo AS $$
	return strconv.Itoa(n) + "x"
$$;
SELECT g_itoa(5);
-- ERROR:  operator does not exist: text + unknown

CI has not run on this PR yet (fork run waiting for approval).

Approving, on condition that #12 merges first and CI passes.

jdatcmd and others added 2 commits November 9, 2026 20:01
…d tests

From the adversarial review of this PR:

- standard_conforming_strings: literals emitted by plxgo, plxtsql,
  plxplsql and plxcobol, and Ruby/PHP single-quoted strings, were plain
  '...' literals. plpgsql reads the body with the caller's setting, so
  with it off a trailing backslash swallowed the closing quote and the
  next literal ran as code. A final pass over the generated body (a small
  SQL lexer that skips comments, quoted identifiers, dollar quotes, E''
  and U&'' strings) now rewrites every ordinary literal containing a
  backslash as E'...' with the backslash doubled. One place covers every
  dialect and future emitters.
- plxpython3 lexer: a #:: annotation line ended the blank-line scan's
  resume point but not the scan, so runs of annotation lines were still
  quadratic. The scan now stops at an annotation line.
- Locals index: a name of 128+ bytes disabled the hash and brought back
  the linear scan. Keys are now downcase_truncate_identifier(), as the
  server reads an unquoted identifier, so every name is indexed and case
  folding matches pg_strncasecmp/the server for all encodings; the
  fallback is gone.
- Tests: standard_conforming_strings off across six dialects; timeouts
  around 60,000 locals after an over-long name, 400,000 blank lines and
  40,000 annotation lines; and a nesting depth under the 500-level counter
  with max_stack_depth = 100kB, which exercises check_stack_depth().
- CHANGELOG: the finally cap bounds depth, not output size.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- plxjs/plxts/plxphp switch, plxruby case and plxcobol EVALUATE with no
  default arm lowered to a plpgsql CASE without ELSE, which raises
  case_not_found when nothing matches. In each source language an
  unmatched switch is a no-op, so emit ELSE NULL. plxplsql is unchanged:
  an Oracle CASE statement with no match raises CASE_NOT_FOUND too.
- plxgo strconv.Itoa: the Atoi/Itoa test compared only the method name's
  length (both are 4), so Itoa cast to integer. Compare the name.

Found by the documentation audit; regression tests cover each case.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@OffgridwithJD OffgridwithJD changed the title Fix switch-without-default and strconv.Itoa lowering [2.1.0] Fix switch-without-default and strconv.Itoa lowering Nov 9, 2026
jdatcmd and others added 2 commits November 9, 2026 23:51
"let x /*:: int */ = 0;" drops the "= 0" (commandprompt#20 makes that form an error).
The test's result does not depend on it, but it should use the documented
form so it keeps passing once commandprompt#20 is merged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@OffgridwithJD

Copy link
Copy Markdown
Author

Thanks. On the plxgo + note: plxgo.md already lists + on strings under "Not supported", so it's a documented limitation rather than a bug.

Part of it can be fixed without type inference, though. Go has no implicit conversions, so + with an operand known to be a string, such as a string literal or strconv.Itoa(...), is always concatenation. I opened #23 to lower that case to || in 3.0. It changes what some stored bodies compute (n + "1" currently adds), so it shouldn't go into 2.1.0.

🤖 Generated with Claude Code

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants