diff --git a/CHANGELOG.md b/CHANGELOG.md index 1930387..2a7f971 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,94 @@ All notable changes to plx are recorded here. The format follows [Keep a Changelog](https://keepachangelog.com/), and plx uses the extension version in `plx.control` (currently `1.0`). +## [Unreleased] + +Security fixes from a code audit. Code-only: no SQL objects change, so a new +build and a server restart (or new sessions) pick them up without +`ALTER EXTENSION`. Every fix concerns input to `CREATE FUNCTION` or `DO`, which +any role with USAGE on a plx language can send (the languages are trusted, so +PUBLIC has USAGE by default). + +### Security + +- **Backend crash on a type annotation at the start of an expression.** A + `/*:: T */` or `#:: T` annotation as the first token of an `if`/`while` + condition, a call argument, or a statement made the transpiler copy a span + from a NULL pointer, killing the backend with SIGSEGV (and resetting every + session on the server). Affected plxjs, plxts, plxphp, plxruby, plxpython3, + including DO blocks. Annotations are now skipped at span edges. +- **Stack-overflow crash on deeply nested expressions in plxtsql, plxgo and + plxcobol.** Their expression emitters recursed once per nested call, + subscript or literal without `check_stack_depth()`, so a body like + `ISNULL(ISNULL(...))` or `int(int(...))` some 50,000 levels deep crashed the + backend. They now raise `expression nested too deeply`, like the shared + rewriter already did. +- **Exponential output from nested `finally`/`ensure`.** Each finally body is + emitted once per exit path, so a finally nested inside a finally doubled the + output per level: a 668-byte function stored a 94 MB body, and a slightly + deeper one drove a backend to gigabytes of memory. The finally body is now + emitted once (see Changed), so output grows linearly with nesting. +- **String literal escaping its quotes.** A dialect string that ended in a lone + backslash, inside an interpolation (plxjs/plxts `${'...\'}`, plxpython3 + f-strings), was emitted with that backslash bare inside an `E'...'` literal, + where it escaped the closing quote. The next string literal in the body then + ran as code, so a function whose source showed only inert string assignments + could execute arbitrary SQL. +- **String literals that depend on `standard_conforming_strings`.** plpgsql + reads a function body with the caller's setting, which any role may change. + Literals that plxgo, plxtsql, plxplsql and plxcobol emit, and Ruby and PHP + single-quoted strings, were written as plain `'...'`, so with the setting off + a literal ending in a backslash swallowed its closing quote and the following + text ran as code; the caller could change what a `SECURITY DEFINER` function + did. The finished body is now rewritten so that every ordinary literal + containing a backslash is an `E'...'` literal with the backslash doubled, + which reads the same under either setting and, with it on, has the value the + original had. This covers every dialect, and SQL passed through verbatim. +- **Quadratic, uninterruptible transpile time.** Local-variable lookup was a + linear scan, so a body with 40,000 distinct variables took 47 seconds; the + plxpython3 lexer rescanned every run of blank lines from each newline, so + 400,000 blank lines took 43 seconds. Neither honoured `statement_timeout`. + Locals are now hashed (keyed as PostgreSQL reads an identifier, down-cased + and truncated to 63 bytes, so no name defeats the index), blank and + annotation lines are scanned once, and the lexer, rewriter and local table + check for interrupts. (The same fix stops plxpython3 from + counting blank lines twice in error line numbers.) +- **Memory amplification in chained ternaries.** Each `?:` level kept its own + copy of the rest of the chain, so peak memory was depth times source length + (568 MB for a 1 MB body). Intermediate results are now freed. +- **plxgo `fmt.Sprintf` directive without a letter verb.** A `%` followed by a + quote consumed half of the escaped `''` pair, so text inside a Go string + literal could end up as SQL in the generated body. Such directives now fall + back to the `format()` lowering, which keeps the text literal. + +### Changed + +- **`finally`/`ensure` now runs on `return`, `break` and `continue`** (plxjs, + plxts, plxphp, plxruby, plxpython3), as in the source languages. Before, a + `return` inside `try` left the function without running `finally`. The + finally body is now emitted once: an exit from the try (or from a catch arm) + records which exit is pending, leaves the block, runs the finally body, then + completes the exit; an error is caught, the finally body runs, and the error + is re-raised with its SQLSTATE, message, detail, hint and object names + (schema, table, column, constraint, type). A `return` or an error in the + finally body replaces the pending exit or error, as in the source languages. + Behaviour differences to be aware of: an error re-raised after `finally` + carries the location of the re-raise in its `CONTEXT`, not the original + statement's; when it names any object, the object names it does not have + are sent as empty fields rather than left out (psql shows them in verbose + mode). As before, the finally body does not run when the statement is + cancelled (a query cancel or `statement_timeout`) or an `assert` fails: + `WHEN OTHERS` does not catch those two errors. Only functions created or + replaced after upgrading are affected; stored functions keep the plpgsql + they were created with. + +### Fixed + +- A `finally`/`ensure` body that raised an error on the normal path ran a + second time from the error path. It now runs once. +- plxpython3 `call(f"...")` emitted an unterminated quoted identifier. An + f-string procedure name is now rejected. + ## [2.0.1] - 2026-08-25 Packaging only. **The extension is unchanged at 2.0.0**, so there is no diff --git a/Makefile b/Makefile index f01d495..ea06457 100644 --- a/Makefile +++ b/Makefile @@ -28,6 +28,8 @@ PLX_PYTHON ?= python3 .PHONY: differentialcheck differentialcheck: PLX_PSQL="$(shell $(PG_CONFIG) --bindir)/psql" $(PLX_PYTHON) test/differential.py + PLX_PSQL="$(shell $(PG_CONFIG) --bindir)/psql" PGDATABASE=contrib_regression \ + $(PLX_PYTHON) test/finally_diff.py 300 1 # Check META.json against the PGXN Meta Spec. PGXN validates at upload time, # which is after a release is tagged and published, so check it before then. diff --git a/doc/plxjs.md b/doc/plxjs.md index daf5b7e..c44076d 100644 --- a/doc/plxjs.md +++ b/doc/plxjs.md @@ -205,6 +205,15 @@ try { `e.table`, `e.schema`, `e.datatype` to the matching `GET STACKED DIAGNOSTICS` fields. +The `finally` body runs once, whether the `try` block completes, returns, +breaks, continues or raises an error. After an error it runs and then raises +the error again, with its SQLSTATE, message, detail, hint and the names of +the objects it is about (schema, table, column, constraint, type); the +`CONTEXT` of the raised error points at the re-raise, not the original +statement. It does not run when the statement is cancelled (a query cancel +or `statement_timeout`) or an `assert` fails, because `WHEN OTHERS` does not +catch those two errors; for the same reason `catch (e)` does not catch them. + ### Assertions ```sql diff --git a/doc/plxphp.md b/doc/plxphp.md index 8bbd8be..138550e 100644 --- a/doc/plxphp.md +++ b/doc/plxphp.md @@ -208,6 +208,16 @@ Accessors: `$e->message` to `SQLERRM`, `$e->sqlstate` to `SQLSTATE`, and `$e->detail`, `$e->hint`, `$e->constraint`, `$e->column`, `$e->table`, `$e->schema`, `$e->datatype` to the matching `GET STACKED DIAGNOSTICS` fields. +The `finally` body runs once, whether the `try` block completes, returns, +breaks, continues or raises an error. After an error it runs and then raises +the error again, with its SQLSTATE, message, detail, hint and the names of +the objects it is about (schema, table, column, constraint, type); the +`CONTEXT` of the raised error points at the re-raise, not the original +statement. It does not run when the statement is cancelled (a query cancel +or `statement_timeout`) or an `assert` fails, because `WHEN OTHERS` does not +catch those two errors; for the same reason `catch (\Exception $e)` does not +catch them. + ### Assertions ```sql diff --git a/doc/plxpython3.md b/doc/plxpython3.md index 71bcb48..4b5c741 100644 --- a/doc/plxpython3.md +++ b/doc/plxpython3.md @@ -192,6 +192,15 @@ to `unique_violation`. Accessors: `e.message` to `SQLERRM`, `e.sqlstate` to `SQLSTATE`, and `e.detail`, `e.hint`, `e.constraint`, `e.column`, `e.table`, `e.schema`, `e.datatype` to the matching `GET STACKED DIAGNOSTICS` fields. +The `finally` body runs once, whether the `try` block completes, returns, +breaks, continues or raises an error. After an error it runs and then raises +the error again, with its SQLSTATE, message, detail, hint and the names of +the objects it is about (schema, table, column, constraint, type); the +`CONTEXT` of the raised error points at the re-raise, not the original +statement. It does not run when the statement is cancelled (a query cancel +or `statement_timeout`) or an `assert` fails, because `WHEN OTHERS` does not +catch those two errors; for the same reason `except Exception` does not catch them. + ### Assertions ```sql diff --git a/doc/plxruby.md b/doc/plxruby.md index 22a2c79..c433d51 100644 --- a/doc/plxruby.md +++ b/doc/plxruby.md @@ -233,6 +233,15 @@ Accessors: `e.message` to `SQLERRM`, `e.sqlstate` to `SQLSTATE`, and `e.detail`, `e.hint`, `e.constraint`, `e.column`, `e.table`, `e.schema`, `e.datatype` to the matching `GET STACKED DIAGNOSTICS` fields. +The `ensure` body runs once, whether the `begin` block completes, returns, +breaks, continues or raises an error. After an error it runs and then raises +the error again, with its SQLSTATE, message, detail, hint and the names of +the objects it is about (schema, table, column, constraint, type); the +`CONTEXT` of the raised error points at the re-raise, not the original +statement. It does not run when the statement is cancelled (a query cancel +or `statement_timeout`) or an `assert` fails, because `WHEN OTHERS` does not +catch those two errors; for the same reason `rescue` does not catch them. + ### Assertions ```sql diff --git a/src/plx_dialect_cobol.c b/src/plx_dialect_cobol.c index 33ffa49..0cf6554 100644 --- a/src/plx_dialect_cobol.c +++ b/src/plx_dialect_cobol.c @@ -442,8 +442,10 @@ cob_is_array(Cb *cb, const char *name) * noise word, figurative constants, ** -> ^, data-name mapping, and array * subscripts (WS-ARR(I) -> ws_arr[i]). */ +static void cob_emit_range(Cb *cb, int a, int b, StringInfo out); + static void -cob_emit_range(Cb *cb, int a, int b, StringInfo out) +cob_emit_range_inner(Cb *cb, int a, int b, StringInfo out) { CbTok *t = cb->t; int i = a; @@ -621,6 +623,21 @@ cob_emit_range(Cb *cb, int a, int b, StringInfo out) } } +/* + * Recursion guard for the expression emitter. Nested calls, subscripts and + * literals recurse through here once per level; without check_stack_depth() + * hostile nesting overflows the C stack and crashes the backend (SIGSEGV). + */ +static void +cob_emit_range(Cb *cb, int a, int b, StringInfo out) +{ + check_stack_depth(); + if (++cb->cx->depth > PLX_MAX_DEPTH) + cob_err(cb, "expression nested too deeply"); + cob_emit_range_inner(cb, a, b, out); + cb->cx->depth--; +} + /* true if the current token stops a value scan */ static bool cob_value_stop(Cb *cb, const char *const *stops, int nstops) diff --git a/src/plx_dialect_go.c b/src/plx_dialect_go.c index be53a05..08383fc 100644 --- a/src/plx_dialect_go.c +++ b/src/plx_dialect_go.c @@ -489,6 +489,36 @@ go_emit_format_str(GoTok *tk, StringInfo out) pfree(raw.data); } +/* + * Index of the verb of the fmt directive whose '%' is at raw->data[pct], after + * skipping flags, width and precision; -1 when that character is not an ASCII + * letter (or the directive runs into the closing quote). + */ +static int +go_sprintf_verb(StringInfo raw, int pct) +{ + int j = pct + 1; + + while (j + 1 < raw->len && (raw->data[j] == '-' || raw->data[j] == '+' || + raw->data[j] == ' ' || raw->data[j] == '#' || + raw->data[j] == '0')) + j++; + while (j + 1 < raw->len && raw->data[j] >= '0' && raw->data[j] <= '9') + j++; + if (j + 1 < raw->len && raw->data[j] == '.') + { + j++; + while (j + 1 < raw->len && raw->data[j] >= '0' && raw->data[j] <= '9') + j++; + } + if (j + 1 >= raw->len) + return -1; + if (!((raw->data[j] >= 'a' && raw->data[j] <= 'z') || + (raw->data[j] >= 'A' && raw->data[j] <= 'Z'))) + return -1; + return j; +} + /* fmt.Sprintf with a literal format string, lowered to a || chain. * format() renders a NULL operand as the empty string and cannot be made to do @@ -565,6 +595,17 @@ go_emit_sprintf_concat(Go *g, int lp, int close, StringInfo out) i++; continue; } + /* + * The verb must be a letter. raw is already a SQL literal (' + * doubled), so a quote here would otherwise be consumed as the verb + * and leave half of a '' pair unescaped in the output; fall back to + * the format() path, which validates verbs, instead. + */ + if (go_sprintf_verb(&raw, i) < 0) + { + pfree(raw.data); + return false; + } ndir++; } if (ndir != nargs) @@ -961,7 +1002,7 @@ go_emit_call(Go *g, int i, int b, StringInfo out, int *ni) /* emit tokens [a,b) as a plpgsql expression */ static void -go_emit_range(Go *g, int a, int b, StringInfo out) +go_emit_range_inner(Go *g, int a, int b, StringInfo out) { int i = a; @@ -1117,6 +1158,21 @@ go_emit_range(Go *g, int a, int b, StringInfo out) } } +/* + * Recursion guard for the expression emitter. Nested calls, subscripts and + * literals recurse through here once per level; without check_stack_depth() + * hostile nesting overflows the C stack and crashes the backend (SIGSEGV). + */ +static void +go_emit_range(Go *g, int a, int b, StringInfo out) +{ + check_stack_depth(); + if (++g->cx->depth > PLX_MAX_DEPTH) + go_err(g, "expression nested too deeply"); + go_emit_range_inner(g, a, b, out); + g->cx->depth--; +} + /* end of a simple statement: the next top-level ; (brackets/braces nest) */ static int go_stmt_end(Go *g, int from) diff --git a/src/plx_dialect_python.c b/src/plx_dialect_python.c index 0daba37..b4b3527 100644 --- a/src/plx_dialect_python.c +++ b/src/plx_dialect_python.c @@ -274,11 +274,13 @@ parse_py_try(Ctx *cx, int ind) arms; int nrescue = 0; char *ensure_body = NULL; + PlxFin *fin; colon = py_header_colon(cx, cx->pos + 1); if (colon < 0) plx_err(cx, hd, "try requires ':'"); cx->pos = colon + 1; + fin = plx_fin_push(cx); saved = cx->out; initStringInfo(&cx->out); parse_py_block(cx, ind + 1); @@ -343,12 +345,14 @@ parse_py_try(Ctx *cx, int ind) cx->exc_var = save_ev; cx->exc_varlen = save_evl; - appendStringInfoSpaces(&arms, (ind + 1) * 2); + plx_indent(&arms, ind + 1); appendStringInfo(&arms, "WHEN %s THEN\n", cond); appendStringInfoString(&arms, pfx); appendStringInfoString(&arms, hb); + pfree(hb); nrescue++; } + plx_fin_pop(cx, fin); if (cx->t[cx->pos].kind == T_KW && cx->t[cx->pos].kw == KW_ENSURE) /* finally */ { colon = py_header_colon(cx, cx->pos + 1); @@ -362,44 +366,7 @@ parse_py_try(Ctx *cx, int ind) cx->out = saved; } - if (!ensure_body) - { - plx_indent(o, ind); - appendStringInfoString(o, "BEGIN\n"); - appendStringInfoString(o, body); - if (nrescue) - { - plx_indent(o, ind); - appendStringInfoString(o, "EXCEPTION\n"); - appendStringInfoString(o, arms.data); - } - plx_indent(o, ind); - appendStringInfoString(o, "END;\n"); - } - else - { - plx_indent(o, ind); - appendStringInfoString(o, "BEGIN\n"); - plx_indent(o, ind + 1); - appendStringInfoString(o, "BEGIN\n"); - appendStringInfoString(o, body); - if (nrescue) - { - plx_indent(o, ind + 1); - appendStringInfoString(o, "EXCEPTION\n"); - appendStringInfoString(o, arms.data); - } - plx_indent(o, ind + 1); - appendStringInfoString(o, "END;\n"); - appendStringInfoString(o, ensure_body); - plx_indent(o, ind); - appendStringInfoString(o, "EXCEPTION WHEN OTHERS THEN\n"); - appendStringInfoString(o, ensure_body); - plx_indent(o, ind + 1); - appendStringInfoString(o, "RAISE;\n"); - plx_indent(o, ind); - appendStringInfoString(o, "END;\n"); - } + plx_emit_try(cx, o, ind, body, nrescue, arms.data, ensure_body, fin); } /* raise / raise Class("msg") */ @@ -463,10 +430,17 @@ parse_py_stmt_inner(Ctx *cx, int ind, bool toplevel) cx->t[cx->pos + 2].kind == T_KW && (cx->t[cx->pos + 2].kw == KW_FOR || cx->t[cx->pos + 2].kw == KW_WHILE)) { + PlxLabel *mark = cx->labels; + plx_indent(&cx->out, ind); appendStringInfo(&cx->out, "<<%.*s>>\n", tk->len, tk->s); + plx_label_note(cx, tk->s, tk->len); cx->pos += 2; - tk = &cx->t[cx->pos]; + /* the label is in scope only inside its loop: a later loop may + * reuse the name, and a break must not resolve to a closed one */ + parse_py_stmt_inner(cx, ind, toplevel); + cx->labels = mark; + return; } if (tk->kind == T_KW) { diff --git a/src/plx_dialect_ruby.c b/src/plx_dialect_ruby.c index 918a950..e8eaf40 100644 --- a/src/plx_dialect_ruby.c +++ b/src/plx_dialect_ruby.c @@ -419,8 +419,10 @@ parse_begin(Ctx *cx, int ind) StringInfoData arms; int nrescue = 0; char *ensure_body = NULL; + PlxFin *fin; cx->pos++; /* consume 'begin' */ + fin = plx_fin_push(cx); body = capture_block(cx, ind + 1); initStringInfo(&arms); @@ -474,14 +476,16 @@ parse_begin(Ctx *cx, int ind) cx->exc_var = save_ev; cx->exc_varlen = save_evl; - appendStringInfoSpaces(&arms, (ind + 1) * 2); + plx_indent(&arms, ind + 1); appendStringInfo(&arms, "WHEN %s THEN\n", cond); appendStringInfoString(&arms, pfx); appendStringInfoString(&arms, hb); + pfree(hb); } nrescue++; } + plx_fin_pop(cx, fin); if (cx->t[cx->pos].kind == T_KW && cx->t[cx->pos].kw == KW_ENSURE) { cx->pos++; @@ -493,44 +497,7 @@ parse_begin(Ctx *cx, int ind) else plx_err(cx, bl, "unterminated begin block"); - if (!ensure_body) - { - plx_indent(o, ind); - appendStringInfoString(o, "BEGIN\n"); - appendStringInfoString(o, body); - if (nrescue) - { - plx_indent(o, ind); - appendStringInfoString(o, "EXCEPTION\n"); - appendStringInfoString(o, arms.data); - } - plx_indent(o, ind); - appendStringInfoString(o, "END;\n"); - } - else - { - plx_indent(o, ind); - appendStringInfoString(o, "BEGIN\n"); - plx_indent(o, ind + 1); - appendStringInfoString(o, "BEGIN\n"); - appendStringInfoString(o, body); - if (nrescue) - { - plx_indent(o, ind + 1); - appendStringInfoString(o, "EXCEPTION\n"); - appendStringInfoString(o, arms.data); - } - plx_indent(o, ind + 1); - appendStringInfoString(o, "END;\n"); - appendStringInfoString(o, ensure_body); - plx_indent(o, ind); - appendStringInfoString(o, "EXCEPTION WHEN OTHERS THEN\n"); - appendStringInfoString(o, ensure_body); - plx_indent(o, ind + 1); - appendStringInfoString(o, "RAISE;\n"); - plx_indent(o, ind); - appendStringInfoString(o, "END;\n"); - } + plx_emit_try(cx, o, ind, body, nrescue, arms.data, ensure_body, fin); } /* rewrite a comma-separated value list (for CASE WHEN v1, v2) */ @@ -667,10 +634,17 @@ parse_stmt_inner(Ctx *cx, int ind, bool toplevel) (cx->t[cx->pos + 2].kw == KW_FOR || cx->t[cx->pos + 2].kw == KW_WHILE || cx->t[cx->pos + 2].kw == KW_UNTIL || cx->t[cx->pos + 2].kw == KW_LOOP)) { + PlxLabel *mark = cx->labels; + plx_indent(&cx->out, ind); appendStringInfo(&cx->out, "<<%.*s>>\n", tk->len, tk->s); + plx_label_note(cx, tk->s, tk->len); cx->pos += 2; - tk = &cx->t[cx->pos]; + /* the label is in scope only inside its loop: a later loop may + * reuse the name, and a break must not resolve to a closed one */ + parse_stmt_inner(cx, ind, toplevel); + cx->labels = mark; + return; } if (tk->kind == T_KW) { diff --git a/src/plx_dialect_tsql.c b/src/plx_dialect_tsql.c index fd03afd..1ca2534 100644 --- a/src/plx_dialect_tsql.c +++ b/src/plx_dialect_tsql.c @@ -679,7 +679,7 @@ tq_emit_call(Tq *tq, int i, int b, StringInfo out, int *ni) /* emit tokens [a,b) as a plpgsql expression */ static void -tq_emit_range(Tq *tq, int a, int b, StringInfo out) +tq_emit_range_inner(Tq *tq, int a, int b, StringInfo out) { int i = a; @@ -782,6 +782,21 @@ tq_emit_range(Tq *tq, int a, int b, StringInfo out) } } +/* + * Recursion guard for the expression emitter. Nested calls, subscripts and + * literals recurse through here once per level; without check_stack_depth() + * hostile nesting overflows the C stack and crashes the backend (SIGSEGV). + */ +static void +tq_emit_range(Tq *tq, int a, int b, StringInfo out) +{ + check_stack_depth(); + if (++tq->cx->depth > PLX_MAX_DEPTH) + tq_err(tq, "expression nested too deeply"); + tq_emit_range_inner(tq, a, b, out); + tq->cx->depth--; +} + /* end of an expression/raw statement: first depth-0 ; EOF END or ELSE */ static int tq_value_end(Tq *tq, int from) @@ -1519,7 +1534,7 @@ plx_tsql_parse_body(Ctx *cx) tq.pos = save; cx->out.len = 0; cx->out.data[0] = '\0'; - cx->locals = cx->ltail = NULL; + plx_locals_reset(cx); } while (tq_cur(&tq)->kind != TQ_EOF) { diff --git a/src/plx_engine.h b/src/plx_engine.h index 434be02..69e1733 100644 --- a/src/plx_engine.h +++ b/src/plx_engine.h @@ -73,6 +73,44 @@ typedef struct PlxLocal2 struct PlxLocal2 *next; } PlxLocal2; +/* ------------------------------------------------------- try/finally frames */ + +/* + * A frame is pushed while a try body and its catch arms are parsed. A return, + * break or continue that would leave the frame is emitted as a placeholder + * (PlxFinExit) because the parser does not yet know whether a finally follows; + * plx_emit_try resolves the placeholders once it does. See plx_emit_try. + */ +typedef enum +{ + PLX_EXIT_RETURN, PLX_EXIT_BREAK, PLX_EXIT_CONTINUE +} PlxExitKind; + +typedef struct PlxFinExit +{ + PlxExitKind kind; + char *label; /* loop label, or NULL */ + char *value; /* rewritten return value, or NULL */ + int code; /* dispatch code once routed through finally */ +} PlxFinExit; + +typedef struct PlxFin +{ + int id; + int loopdepth; /* cx->loopdepth when the try began */ + struct PlxFin *outer; + PlxFinExit *exits; + int nexits, + cap; +} PlxFin; + +typedef struct PlxLabel +{ + char *name; + int fin_id; /* frame the labeled loop is in (0 = none) */ + struct PlxLabel *next; +} PlxLabel; + /* ---------------------------------------------------------------- context */ /* @@ -89,9 +127,14 @@ typedef struct PlxCtx const PlxSurface *surf; StringInfoData out; /* emitted BEGIN..END body */ PlxLocal2 *locals, *ltail; + struct HTAB *lhash; /* lower-cased name -> local (see plx_local_find) */ int loopdepth; int depth; /* recursion depth guard */ int handlerdepth; /* inside a rescue handler body */ + struct PlxFin *fin; /* innermost enclosing try-with-finally frame */ + int fin_seq; /* frame id counter */ + struct PlxLabel *labels; /* loop labels seen so far, most recent first */ + bool ret_declared; /* __plx_ret hoisted (see plx_fin_ret_var) */ int subq; /* __plx_fo_N / __plx_p_N counter */ const char *exc_var; /* current rescue exception var name */ int exc_varlen; @@ -133,8 +176,16 @@ bool plx_tok_is(Tok *tk, const char *op); bool plx_name_eq(Tok *tk, const char *w); int plx_stmt_end(Ctx *cx, int from); void plx_indent(StringInfo o, int n); +PlxFin *plx_fin_push(Ctx *cx); +void plx_fin_pop(Ctx *cx, PlxFin *f); +void plx_label_note(Ctx *cx, const char *name, int len); +void plx_emit_exit(Ctx *cx, StringInfo o, int ind, PlxExitKind kind, + const char *label, const char *value, const char *cond); +void plx_emit_try(Ctx *cx, StringInfo o, int ind, char *body, int nrescue, + char *arms, char *ensure_body, PlxFin *f); /* frees its strings */ PlxLocal2 *plx_local_add(Ctx *cx, const char *name, int len); PlxLocal2 *plx_local_find(Ctx *cx, const char *name, int len); +void plx_locals_reset(Ctx *cx); bool plx_is_param(Ctx *cx, const char *name, int len); /* ---------------------------------------------- dialect front ends (vtable) */ diff --git a/src/plx_parse_brace.c b/src/plx_parse_brace.c index 49b8ee3..c0405e9 100644 --- a/src/plx_parse_brace.c +++ b/src/plx_parse_brace.c @@ -350,8 +350,10 @@ parse_try_brace(Ctx *cx, int ind) StringInfoData arms; int nrescue = 0; char *ensure_body = NULL; + PlxFin *fin; cx->pos++; /* try */ + fin = plx_fin_push(cx); { StringInfoData saved = cx->out; @@ -415,14 +417,16 @@ parse_try_brace(Ctx *cx, int ind) cx->exc_var = save_ev; cx->exc_varlen = save_evl; - appendStringInfoSpaces(&arms, (ind + 1) * 2); + plx_indent(&arms, ind + 1); appendStringInfo(&arms, "WHEN %s THEN\n", cond); appendStringInfoString(&arms, pfx); appendStringInfoString(&arms, hb); + pfree(hb); } nrescue++; plx_skip_seps(cx); } + plx_fin_pop(cx, fin); if (cx->t[cx->pos].kind == T_KW && cx->t[cx->pos].kw == KW_ENSURE) /* finally */ { StringInfoData saved = cx->out; @@ -434,44 +438,7 @@ parse_try_brace(Ctx *cx, int ind) cx->out = saved; } - if (!ensure_body) - { - plx_indent(o, ind); - appendStringInfoString(o, "BEGIN\n"); - appendStringInfoString(o, body); - if (nrescue) - { - plx_indent(o, ind); - appendStringInfoString(o, "EXCEPTION\n"); - appendStringInfoString(o, arms.data); - } - plx_indent(o, ind); - appendStringInfoString(o, "END;\n"); - } - else - { - plx_indent(o, ind); - appendStringInfoString(o, "BEGIN\n"); - plx_indent(o, ind + 1); - appendStringInfoString(o, "BEGIN\n"); - appendStringInfoString(o, body); - if (nrescue) - { - plx_indent(o, ind + 1); - appendStringInfoString(o, "EXCEPTION\n"); - appendStringInfoString(o, arms.data); - } - plx_indent(o, ind + 1); - appendStringInfoString(o, "END;\n"); - appendStringInfoString(o, ensure_body); - plx_indent(o, ind); - appendStringInfoString(o, "EXCEPTION WHEN OTHERS THEN\n"); - appendStringInfoString(o, ensure_body); - plx_indent(o, ind + 1); - appendStringInfoString(o, "RAISE;\n"); - plx_indent(o, ind); - appendStringInfoString(o, "END;\n"); - } + plx_emit_try(cx, o, ind, body, nrescue, arms.data, ensure_body, fin); (void) line; } @@ -656,10 +623,17 @@ parse_brace_stmt_inner(Ctx *cx, int ind, bool toplevel) cx->t[cx->pos + 2].kind == T_KW && (cx->t[cx->pos + 2].kw == KW_FOR || cx->t[cx->pos + 2].kw == KW_WHILE)) { + PlxLabel *mark = cx->labels; + plx_indent(&cx->out, ind); appendStringInfo(&cx->out, "<<%.*s>>\n", tk->len, tk->s); + plx_label_note(cx, tk->s, tk->len); cx->pos += 2; /* consume name and ':' */ - tk = &cx->t[cx->pos]; + /* the label is in scope only inside its loop: a later loop may + * reuse the name, and a break must not resolve to a closed one */ + parse_brace_stmt_inner(cx, ind, toplevel); + cx->labels = mark; + return; } if (tk->kind == T_KW) { diff --git a/src/plx_transpile.c b/src/plx_transpile.c index 0a23018..88fead1 100644 --- a/src/plx_transpile.c +++ b/src/plx_transpile.c @@ -25,7 +25,10 @@ #include "lib/stringinfo.h" #include "mb/pg_wchar.h" #include "miscadmin.h" +#include "catalog/pg_type.h" +#include "parser/scansup.h" #include "utils/builtins.h" +#include "utils/hsearch.h" #include "plx.h" #include "plx_int.h" @@ -69,6 +72,7 @@ plx_rewrite_expr(Ctx *cx, const char *s, int len, bool boolctx) char *r; check_stack_depth(); + CHECK_FOR_INTERRUPTS(); if (++cx->depth > PLX_MAX_DEPTH) plx_err(cx, 0, "expression nested too deeply"); r = rewrite_expr_inner(cx, s, len, boolctx); @@ -188,6 +192,7 @@ plx_lex(Ctx *cx) { const char *tokstart = p; + CHECK_FOR_INTERRUPTS(); if (*p == ' ' || *p == '\t' || *p == '\r') { p++; @@ -204,12 +209,25 @@ plx_lex(Ctx *cx) PUSH0(T_NEWLINE); if (indent_mode) { - /* find the next real line's indentation, skipping blanks/comments */ + /* + * Find the next real line's indentation, skipping blanks and + * comments, and resume lexing after the skipped lines rather + * than at the next newline: rescanning the run from every + * newline made K blank lines cost O(K^2) and counted their + * line numbers twice. A type-annotation line ends the scan + * (it has to be lexed); its newline scans on from there, so + * a run of them is linear too. + */ const char *q = p; + const char *resume = p; + int resume_line = line; + bool ann_line = false; int col = 0; for (;;) { + const char *ls = q; + col = 0; while (*q == ' ' || *q == '\t') { @@ -219,19 +237,30 @@ plx_lex(Ctx *cx) if (q < end && *q == '\n') { q++; - line++; + resume = q; + resume_line++; continue; } if (q < end && *q == '#') { + if (surf->type_ann && + strncmp(q, surf->type_ann, strlen(surf->type_ann)) == 0) + { + resume = ls; + ann_line = true; + break; + } while (q < end && *q != '\n') q++; continue; } break; /* real line, or EOF */ } - if (q >= end) - continue; /* trailing dedents handled at EOF */ + p = resume; + line = resume_line; + if (ann_line || q >= end) + continue; /* indentation is taken at the next real line; + * trailing dedents are handled at EOF */ if (col > indent_stack[indent_sp]) { /* @@ -564,21 +593,63 @@ plx_is_param(Ctx *cx, const char *name, int len) return false; } +/* + * Locals are kept in declaration order (cx->locals, which DECLARE emission + * walks) and indexed in cx->lhash. A linear scan made a body with many + * distinct variables quadratic: 40,000 assignments took most of a minute. + * + * The key is the name as PostgreSQL itself reads an unquoted identifier: + * down-cased and truncated to NAMEDATALEN - 1 bytes (downcase_truncate_ + * identifier). Two names that plpgsql would treat as one variable therefore + * share an entry, any name can be indexed (an over-long one no longer forces a + * fallback scan), and case folding matches the server's for every encoding. + */ +typedef struct +{ + char key[NAMEDATALEN]; + PlxLocal2 *l; +} PlxLocalEnt; + +static void +plx_local_key(const char *name, int len, char *key) +{ + char *k = downcase_truncate_identifier(name, len, false); + + memset(key, 0, NAMEDATALEN); + strlcpy(key, k, NAMEDATALEN); + pfree(k); +} + +void +plx_locals_reset(Ctx *cx) +{ + cx->locals = cx->ltail = NULL; + if (cx->lhash) + hash_destroy(cx->lhash); + cx->lhash = NULL; +} + PlxLocal2 * plx_local_find(Ctx *cx, const char *name, int len) { - PlxLocal2 *l; + char key[NAMEDATALEN]; + PlxLocalEnt *e; - for (l = cx->locals; l; l = l->next) - if ((int) strlen(l->name) == len && pg_strncasecmp(l->name, name, len) == 0) - return l; - return NULL; + CHECK_FOR_INTERRUPTS(); + if (!cx->lhash) + return NULL; + plx_local_key(name, len, key); + e = hash_search(cx->lhash, key, HASH_FIND, NULL); + return e ? e->l : NULL; } PlxLocal2 * plx_local_add(Ctx *cx, const char *name, int len) { PlxLocal2 *l = palloc0(sizeof(PlxLocal2)); + char key[NAMEDATALEN]; + bool found; + PlxLocalEnt *e; l->name = pnstrdup(name, len); if (cx->ltail) @@ -586,6 +657,26 @@ plx_local_add(Ctx *cx, const char *name, int len) else cx->locals = l; cx->ltail = l; + + if (!cx->lhash) + { + HASHCTL ctl; + + ctl.keysize = NAMEDATALEN; + ctl.entrysize = sizeof(PlxLocalEnt); + ctl.hcxt = CurrentMemoryContext; +#if PG_VERSION_NUM >= 140000 + cx->lhash = hash_create("plx locals", 64, &ctl, + HASH_ELEM | HASH_STRINGS | HASH_CONTEXT); +#else + cx->lhash = hash_create("plx locals", 64, &ctl, + HASH_ELEM | HASH_CONTEXT); /* string keys */ +#endif + } + plx_local_key(name, len, key); + e = hash_search(cx->lhash, key, HASH_ENTER, &found); + if (!found) + e->l = l; /* keep the first declaration */ return l; } @@ -843,6 +934,19 @@ emit_string_value(Ctx *cx, Tok *tk, StringInfo out) s++; continue; } + /* + * A backslash that reaches here is literal content (a raw string, or + * a lone trailing backslash). Escape it and force the E'' form: left + * bare inside an E'' literal it would escape the closing quote and + * let the next literal's text run on as code. + */ + if (s[0] == '\\') + { + appendStringInfoString(&lit, "\\\\"); + need_e = true; + s++; + continue; + } appendStringInfoChar(&lit, *s); s++; } @@ -992,6 +1096,12 @@ rewrite_expr_inner(Ctx *cx, const char *s, int len, bool boolctx) a = plx_rewrite_expr(cx, s + q + 1, c - q - 1, false); b = plx_rewrite_expr(cx, s + c + 1, len - c - 1, false); r = psprintf("CASE WHEN %s THEN %s ELSE %s END", cond, a, b); + /* free the parts: a chain of ternaries nests one level per + * '?', and keeping every level's copy of the rest of the chain + * made peak memory depth * length */ + pfree(cond); + pfree(a); + pfree(b); return r; } } @@ -1064,7 +1174,7 @@ rewrite_expr_inner(Ctx *cx, const char *s, int len, bool boolctx) while (j < len && s[j] != c) { - if (s[j] == '\\') + if (s[j] == '\\' && j + 1 < len) j++; j++; } @@ -1090,7 +1200,7 @@ rewrite_expr_inner(Ctx *cx, const char *s, int len, bool boolctx) while (j < len && s[j] != q) { - if (s[j] == '\\') + if (s[j] == '\\' && j + 1 < len) j++; j++; } @@ -1471,6 +1581,15 @@ plx_span_text(Ctx *cx, int a, int b) { const char *st, *en; + /* + * Type-annotation tokens carry no source span (s == NULL), so trim them + * off both ends; a span that began on one used to compute its length + * from a NULL pointer and crash the backend in pnstrdup. + */ + while (a < b && cx->t[a].s == NULL) + a++; + while (b > a && cx->t[b - 1].s == NULL) + b--; if (b <= a) return pstrdup(""); st = cx->t[a].s; @@ -1503,13 +1622,408 @@ const_literal_type(Ctx *cx, int a, int b) /* ---------------------------------------------------------------- indent */ +/* + * Indentation is cosmetic in plpgsql, so it stops growing past + * PLX_MAX_INDENT levels. Without the cap, generated code grows with the + * square of the nesting depth: 490 nested try blocks in a 15 KB body produced + * 5.8 MB of plpgsql, nearly all of it leading spaces. + */ +#define PLX_MAX_INDENT 32 + void plx_indent(StringInfo o, int n) { - int i; + if (n > PLX_MAX_INDENT) + n = PLX_MAX_INDENT; + if (n > 0) + appendStringInfoSpaces(o, n * 2); +} + +/* ------------------------------------------------------- try/finally frames */ + +#define PLX_PH '\x01' /* placeholder delimiter; rejected in source */ + +PlxFin * +plx_fin_push(Ctx *cx) +{ + PlxFin *f = palloc0(sizeof(PlxFin)); + + f->id = ++cx->fin_seq; + f->loopdepth = cx->loopdepth; + f->outer = cx->fin; + cx->fin = f; + return f; +} + +void +plx_fin_pop(Ctx *cx, PlxFin *f) +{ + Assert(cx->fin == f); + cx->fin = f->outer; +} + +/* Record a loop label, so a labeled break/continue knows which frame its + * target loop is in. */ +void +plx_label_note(Ctx *cx, const char *name, int len) +{ + PlxLabel *l = palloc(sizeof(PlxLabel)); + + l->name = pnstrdup(name, len); + l->fin_id = cx->fin ? cx->fin->id : 0; + l->next = cx->labels; + cx->labels = l; +} + +/* + * Does this exit leave frame f? A return always does. An unlabeled + * break/continue does when its loop (the innermost loop at `loopdepth`) is + * outside f; a labeled one when its label was recorded outside f. + */ +static bool +exit_crosses(Ctx *cx, PlxFin *f, PlxExitKind kind, const char *label, + int loopdepth) +{ + PlxLabel *l; + + if (!f) + return false; + if (kind == PLX_EXIT_RETURN) + return true; + if (!label) + return loopdepth == f->loopdepth; + for (l = cx->labels; l; l = l->next) + if (pg_strcasecmp(l->name, label) == 0) + return l->fin_id != f->id; + return false; /* unknown label: let plpgsql report it */ +} + +/* Register an exit with f and return its placeholder text. */ +static char * +fin_placeholder(PlxFin *f, PlxExitKind kind, const char *label, const char *value) +{ + PlxFinExit *e; + + if (f->nexits >= f->cap) + { + f->cap = f->cap ? f->cap * 2 : 8; + f->exits = f->exits ? repalloc(f->exits, f->cap * sizeof(PlxFinExit)) + : palloc(f->cap * sizeof(PlxFinExit)); + } + e = &f->exits[f->nexits]; + e->kind = kind; + e->label = label ? pstrdup(label) : NULL; + e->value = value ? pstrdup(value) : NULL; + e->code = 0; + return psprintf("%c%d.%d%c", PLX_PH, f->id, f->nexits++, PLX_PH); +} + +/* The exit as a statement in the context of frame g (NULL = none): either + * the plain plpgsql statement, or a placeholder for g to resolve. */ +static char * +render_exit(Ctx *cx, PlxFin *g, PlxExitKind kind, const char *label, + const char *value, int loopdepth) +{ + if (exit_crosses(cx, g, kind, label, loopdepth)) + return fin_placeholder(g, kind, label, value); + if (kind == PLX_EXIT_RETURN) + return value ? psprintf("RETURN %s;", value) : pstrdup("RETURN;"); + return psprintf("%s%s%s;", kind == PLX_EXIT_BREAK ? "EXIT" : "CONTINUE", + label ? " " : "", label ? label : ""); +} + +void +plx_emit_exit(Ctx *cx, StringInfo o, int ind, PlxExitKind kind, + const char *label, const char *value, const char *cond) +{ + plx_indent(o, ind); + if (!exit_crosses(cx, cx->fin, kind, label, cx->loopdepth)) + { + /* unchanged from the pre-frame lowering */ + if (kind == PLX_EXIT_RETURN) + appendStringInfo(o, value ? "RETURN %s;\n" : "RETURN;\n", value); + else + appendStringInfo(o, "%s%s%s%s%s;\n", + kind == PLX_EXIT_BREAK ? "EXIT" : "CONTINUE", + label ? " " : "", label ? label : "", + cond ? " WHEN " : "", cond ? cond : ""); + return; + } + if (cond) + appendStringInfo(o, "IF %s THEN %s END IF;\n", cond, + fin_placeholder(cx->fin, kind, label, value)); + else + appendStringInfo(o, "%s\n", fin_placeholder(cx->fin, kind, label, value)); +} + +/* The hoisted return-value variable, typed from the function's result. */ +static const char * +plx_fin_ret_var(Ctx *cx) +{ + if (!cx->ret_declared) + { + PlxLocal2 *l = plx_local_add(cx, "__plx_ret", 9); + Oid rt = cx->meta ? cx->meta->rettype : InvalidOid; + + if (!OidIsValid(rt) || rt == VOIDOID) + l->typ = "text"; /* plpgsql rejects the RETURN value anyway */ + else if (IsPolymorphicType(rt)) + l->typ = "ALIAS FOR $0"; + else if (rt == RECORDOID || rt == TRIGGEROID) + l->is_record = true; + else + l->typ = format_type_be(rt); + cx->ret_declared = true; + } + return "__plx_ret"; +} + +/* + * Replace frame f's placeholders in s. With a finally, an exit becomes + * "save the value, note which exit, leave the block"; the dispatch after the + * finally body completes it. Without one, the exit is re-rendered for the + * enclosing frame (plain, or a placeholder of its own). + */ +static char * +fin_resolve(Ctx *cx, PlxFin *f, const char *s, bool routed, int *ncodes) +{ + StringInfoData r; + + if (!strchr(s, PLX_PH)) + return (char *) s; + initStringInfo(&r); + while (*s) + { + const char *q; + int fid, + idx; + PlxFinExit *e; + + if (*s != PLX_PH) + { + appendStringInfoChar(&r, *s++); + continue; + } + if (sscanf(s + 1, "%d.%d", &fid, &idx) != 2 || fid != f->id || + idx < 0 || idx >= f->nexits || !(q = strchr(s + 1, PLX_PH))) + elog(ERROR, "plx: malformed try/finally placeholder"); + e = &f->exits[idx]; + if (!routed) + appendStringInfoString(&r, render_exit(cx, f->outer, e->kind, e->label, + e->value, f->loopdepth)); + else + { + int i; + + /* one dispatch code per distinct target */ + for (i = 0; i < idx && !e->code; i++) + if (f->exits[i].code && f->exits[i].kind == e->kind && + (f->exits[i].value != NULL) == (e->value != NULL) && + ((!f->exits[i].label && !e->label) || + (f->exits[i].label && e->label && + pg_strcasecmp(f->exits[i].label, e->label) == 0))) + e->code = f->exits[i].code; + if (!e->code) + e->code = ++*ncodes; + if (e->value) + appendStringInfo(&r, "%s := %s; ", plx_fin_ret_var(cx), e->value); + appendStringInfo(&r, "__plx_pend_%d := %d; EXIT __plx_try_%d;", + f->id, e->code, f->id); + } + s = q + 1; + } + return r.data; +} + +/* + * Emit a try/rescue[/finally] statement (shared by every front end). Without + * a finally it is BEGIN body EXCEPTION arms END. With one, the finally body + * is emitted once: + * + * __plx_pend_N := 0; __plx_es_N := NULL; + * <<__plx_try_N>> + * BEGIN + * BEGIN body EXCEPTION arms END; -- exits: note + EXIT __plx_try_N + * EXCEPTION WHEN OTHERS THEN + * GET STACKED DIAGNOSTICS __plx_es_N = RETURNED_SQLSTATE, ...; + * END; + * finally body + * IF __plx_es_N IS NOT NULL THEN RAISE (the saved error) END IF; + * IF __plx_pend_N = 1 THEN RETURN __plx_ret; ELSIF ... END IF; + * + * So the finally body runs on normal completion, on an error (which is then + * re-raised with its SQLSTATE, message, detail, hint and object names), and + * on a return, break or continue out of the try. An error raised by the + * finally body, or a return from it, replaces the pending one, as in the + * source languages. It does not run on a query cancel or a failed ASSERT, + * which WHEN OTHERS does not catch. + * + * body, arms and ensure_body (NULL when there is no finally) must be palloc'd; + * this function frees them. Each nesting level holds the text of every level + * inside it, so keeping those copies until the end of the transpile made + * memory grow with the cube of the depth (3.6 GB for 490 levels). + */ +void +plx_emit_try(Ctx *cx, StringInfo o, int ind, char *body, int nrescue, + char *arms, char *ensure_body, PlxFin *f) +{ + bool fin = (ensure_body != NULL); + int in = fin ? ind + 1 : ind; + int ncodes = 0; + int code; + char *rbody = fin_resolve(cx, f, body, fin, &ncodes); + char *rarms = fin_resolve(cx, f, arms, fin, &ncodes); + + if (fin) + { + PlxLocal2 *l; + static const char *const errv[] = {"es", "em", "ed", "eh", "ecol", + "econ", "etyp", "etab", "esch"}; + int i; + + for (i = 0; i < lengthof(errv); i++) + { + char *nm = psprintf("__plx_%s_%d", errv[i], f->id); + + l = plx_local_add(cx, nm, (int) strlen(nm)); + l->typ = "text"; + } + if (ncodes) + { + char *nm = psprintf("__plx_pend_%d", f->id); + + l = plx_local_add(cx, nm, (int) strlen(nm)); + l->typ = "int"; + plx_indent(o, ind); + appendStringInfo(o, "__plx_pend_%d := 0;\n", f->id); + } + plx_indent(o, ind); + appendStringInfo(o, "__plx_es_%d := NULL;\n", f->id); + plx_indent(o, ind); + appendStringInfo(o, "<<__plx_try_%d>>\n", f->id); + plx_indent(o, ind); + appendStringInfoString(o, "BEGIN\n"); + } + plx_indent(o, in); + appendStringInfoString(o, "BEGIN\n"); + appendStringInfoString(o, rbody); + if (nrescue) + { + plx_indent(o, in); + appendStringInfoString(o, "EXCEPTION\n"); + appendStringInfoString(o, rarms); + } + plx_indent(o, in); + appendStringInfoString(o, "END;\n"); + if (rbody != body) + pfree(rbody); + if (rarms != arms) + pfree(rarms); + pfree(body); + pfree(arms); + if (!fin) + return; + + plx_indent(o, ind); + appendStringInfoString(o, "EXCEPTION WHEN OTHERS THEN\n"); + plx_indent(o, ind + 1); + appendStringInfo(o, "GET STACKED DIAGNOSTICS __plx_es_%1$d = RETURNED_SQLSTATE, " + "__plx_em_%1$d = MESSAGE_TEXT, __plx_ed_%1$d = PG_EXCEPTION_DETAIL, " + "__plx_eh_%1$d = PG_EXCEPTION_HINT, __plx_ecol_%1$d = COLUMN_NAME, " + "__plx_econ_%1$d = CONSTRAINT_NAME, __plx_etyp_%1$d = PG_DATATYPE_NAME, " + "__plx_etab_%1$d = TABLE_NAME, __plx_esch_%1$d = SCHEMA_NAME;\n", + f->id); + plx_indent(o, ind); + appendStringInfoString(o, "END;\n"); + appendStringInfoString(o, ensure_body); + pfree(ensure_body); + + /* + * Re-raise. A RAISE option may not be NULL, and an empty one is not the + * same as an absent one: an empty DETAIL or HINT prints an empty line, + * and an empty object name is sent to the client as a present field (psql + * prints it in verbose mode). Absent fields come back from GET STACKED + * DIAGNOSTICS as '', so pass each group only when it is non-empty: DETAIL + * and HINT each, and the five object names as one group, which keeps this + * to eight RAISEs. The cost: an error that names some objects (a unique + * violation names the schema, table and constraint) is re-raised with the + * others present but empty. + */ + plx_indent(o, ind); + appendStringInfo(o, "IF __plx_es_%d IS NOT NULL THEN\n", f->id); + plx_indent(o, ind + 1); + appendStringInfo(o, "IF __plx_ecol_%1$d || __plx_econ_%1$d || __plx_etyp_%1$d || " + "__plx_etab_%1$d || __plx_esch_%1$d <> '' THEN\n", f->id); + { + int k; + + for (k = 7; k >= 0; k--) + { + bool obj = (k & 4) != 0, + det = (k & 2) != 0, + hnt = (k & 1) != 0; + + if (k == 3) + { + plx_indent(o, ind + 1); + appendStringInfoString(o, "ELSE\n"); + } + plx_indent(o, ind + 2); + if ((k & 3) == 0) + appendStringInfoString(o, "ELSE "); + else + { + appendStringInfoString(o, (k & 3) == 3 ? "IF " : "ELSIF "); + if (det) + appendStringInfo(o, "__plx_ed_%d <> ''%s", f->id, hnt ? " AND " : ""); + if (hnt) + appendStringInfo(o, "__plx_eh_%d <> ''", f->id); + appendStringInfoString(o, " THEN "); + } + appendStringInfo(o, "RAISE EXCEPTION USING ERRCODE = __plx_es_%1$d, " + "MESSAGE = __plx_em_%1$d", f->id); + if (det) + appendStringInfo(o, ", DETAIL = __plx_ed_%d", f->id); + if (hnt) + appendStringInfo(o, ", HINT = __plx_eh_%d", f->id); + if (obj) + appendStringInfo(o, ", COLUMN = __plx_ecol_%1$d, CONSTRAINT = __plx_econ_%1$d, " + "DATATYPE = __plx_etyp_%1$d, TABLE = __plx_etab_%1$d, " + "SCHEMA = __plx_esch_%1$d", f->id); + appendStringInfoString(o, ";\n"); + if ((k & 3) == 0) + { + plx_indent(o, ind + 2); + appendStringInfoString(o, "END IF;\n"); + } + } + } + plx_indent(o, ind + 1); + appendStringInfoString(o, "END IF;\n"); + plx_indent(o, ind); + appendStringInfoString(o, "END IF;\n"); - for (i = 0; i < n; i++) - appendStringInfoString(o, " "); + /* complete the pending exit, in the enclosing frame's context */ + for (code = 1; code <= ncodes; code++) + { + int i; + + for (i = 0; i < f->nexits; i++) + if (f->exits[i].code == code) + break; + Assert(i < f->nexits); + plx_indent(o, ind); + appendStringInfo(o, "%s __plx_pend_%d = %d THEN %s\n", + code == 1 ? "IF" : "ELSIF", f->id, code, + render_exit(cx, f->outer, f->exits[i].kind, f->exits[i].label, + f->exits[i].value ? plx_fin_ret_var(cx) : NULL, + f->loopdepth)); + } + if (ncodes) + { + plx_indent(o, ind); + appendStringInfoString(o, "END IF;\n"); + } } /* ---------------------------------------------------------------- leaves */ @@ -1622,7 +2136,7 @@ plx_diag_prefix(int mask, int ind) for (i = 0; i < (int) (sizeof(t) / sizeof(t[0])); i++) if (mask & t[i].bit) { - appendStringInfoSpaces(&p, ind * 2); + plx_indent(&p, ind); appendStringInfo(&p, "GET STACKED DIAGNOSTICS __plx_%s = %s;\n", t[i].fld, t[i].item); } return p.data; @@ -2143,6 +2657,9 @@ plx_emit_core(Ctx *cx, int a, int b, int ind, bool toplevel) { Tok *pn = &cx->t[as[0]]; + /* an f-string's prefix would be unquoted as part of the name */ + if (pn->fstr) + plx_err(cx, t0->line, "call needs a plain string procedure name, not an f-string"); appendBinaryStringInfo(o, pn->s + 1, pn->len - 2); /* unquote */ } else @@ -2191,10 +2708,7 @@ plx_emit_core(Ctx *cx, int a, int b, int ind, bool toplevel) if (t0->kind == T_KW && t0->kw == KW_RETURN) { if (a + 1 >= b) - { - plx_indent(o, ind); - appendStringInfoString(o, "RETURN;\n"); - } + plx_emit_exit(cx, o, ind, PLX_EXIT_RETURN, NULL, NULL, NULL); else { char *e = plx_rewrite_expr(cx, plx_span_text(cx, a + 1, b), @@ -2202,8 +2716,7 @@ plx_emit_core(Ctx *cx, int a, int b, int ind, bool toplevel) if (cx->retset) plx_err(cx, t0->line, "use emit/return_next in a set-returning function, not 'return '"); - plx_indent(o, ind); - appendStringInfo(o, "RETURN %s;\n", e); + plx_emit_exit(cx, o, ind, PLX_EXIT_RETURN, NULL, e, NULL); } return; } @@ -2233,15 +2746,14 @@ plx_emit_core(Ctx *cx, int a, int b, int ind, bool toplevel) /* next / break, optionally with a loop label */ if (t0->kind == T_KW && (t0->kw == KW_NEXT || t0->kw == KW_BREAK)) { - const char *kw = (t0->kw == KW_NEXT) ? "CONTINUE" : "EXIT"; + PlxExitKind kind = (t0->kw == KW_NEXT) ? PLX_EXIT_CONTINUE : PLX_EXIT_BREAK; if (cx->loopdepth == 0) plx_err(cx, t0->line, "%s outside a loop", t0->kw == KW_NEXT ? "next" : "break"); - plx_indent(o, ind); - if (a + 1 < b && cx->t[a + 1].kind == T_IDENT) - appendStringInfo(o, "%s %.*s;\n", kw, cx->t[a + 1].len, cx->t[a + 1].s); - else - appendStringInfo(o, "%s;\n", kw); + plx_emit_exit(cx, o, ind, kind, + (a + 1 < b && cx->t[a + 1].kind == T_IDENT) + ? pnstrdup(cx->t[a + 1].s, cx->t[a + 1].len) : NULL, + NULL, NULL); return; } /* raise */ @@ -2533,16 +3045,13 @@ plx_emit_leaf(Ctx *cx, int a, int b, int ind, bool toplevel) /* next/break modifiers -> WHEN (optionally with a loop label) */ if (t0->kind == T_KW && (t0->kw == KW_NEXT || t0->kw == KW_BREAK)) { - char lbl[NAMEDATALEN + 1] = ""; - if (cx->loopdepth == 0) plx_err(cx, t0->line, "%s outside a loop", t0->kw == KW_NEXT ? "next" : "break"); - if (a + 1 < m && cx->t[a + 1].kind == T_IDENT) - snprintf(lbl, sizeof(lbl), " %.*s", cx->t[a + 1].len, cx->t[a + 1].s); - plx_indent(o, ind); - appendStringInfo(o, "%s%s WHEN %s%s%s;\n", - t0->kw == KW_NEXT ? "CONTINUE" : "EXIT", lbl, - neg ? "NOT (" : "", cond, neg ? ")" : ""); + plx_emit_exit(cx, o, ind, + t0->kw == KW_NEXT ? PLX_EXIT_CONTINUE : PLX_EXIT_BREAK, + (a + 1 < m && cx->t[a + 1].kind == T_IDENT) + ? pnstrdup(cx->t[a + 1].s, cx->t[a + 1].len) : NULL, + NULL, neg ? psprintf("NOT (%s)", cond) : cond); return; } if (mk == KW_IF || mk == KW_UNLESS) @@ -2633,6 +3142,162 @@ djb2(const char *s) return h; } +static bool +sql_ident_char(char c) +{ + return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || + (c >= '0' && c <= '9') || c == '_' || c == '$' || IS_HIGHBIT_SET(c); +} + +/* + * Rewrite every standard string literal in the generated plpgsql that contains + * a backslash as an escape string: 'C:\' becomes E'C:\\'. + * + * plpgsql reads a function body with the caller's standard_conforming_strings, + * which any role may set. With it off, a backslash in an ordinary '...' + * literal is an escape, so a literal ending in one swallows its closing quote + * and the following text runs as code; a SECURITY DEFINER function's + * behaviour would then depend on its caller. An E'...' literal reads the same + * either way, and with the setting on (the default) the rewritten literal has + * exactly the value of the original. Doing this once over the finished body + * covers every dialect's literal emitters, and SQL the author passed through. + * + * A small SQL lexer: skips comments (nested block comments too), quoted + * identifiers, dollar-quoted strings, E'' strings (already safe) and U&'' + * strings (where a backslash introduces a Unicode escape). + */ +static char * +escape_backslash_literals(const char *s) +{ + StringInfoData o; + const char *p = s; + + if (!strchr(s, '\\')) + return (char *) s; + initStringInfo(&o); + while (*p) + { + const char *q; + + if (p[0] == '-' && p[1] == '-') + { + q = strchr(p, '\n'); + q = q ? q + 1 : p + strlen(p); + appendBinaryStringInfo(&o, p, q - p); + p = q; + continue; + } + if (p[0] == '/' && p[1] == '*') + { + int depth = 1; + + q = p + 2; + while (*q && depth > 0) + { + if (q[0] == '/' && q[1] == '*') + { + depth++; + q += 2; + } + else if (q[0] == '*' && q[1] == '/') + { + depth--; + q += 2; + } + else + q++; + } + appendBinaryStringInfo(&o, p, q - p); + p = q; + continue; + } + if (p[0] == '"') + { + q = p + 1; + while (*q && !(q[0] == '"' && q[1] != '"')) + q += (q[0] == '"') ? 2 : 1; + if (*q) + q++; + appendBinaryStringInfo(&o, p, q - p); + p = q; + continue; + } + if (p[0] == '$' && (p == s || !sql_ident_char(p[-1]))) + { + /* $tag$ ... $tag$; the tag cannot start with a digit ($1 is a param) */ + const char *t = p + 1; + + if (*t != '$' && ((*t >= 'a' && *t <= 'z') || (*t >= 'A' && *t <= 'Z') || + *t == '_' || IS_HIGHBIT_SET(*t))) + while (sql_ident_char(*t) && *t != '$') + t++; + if (*t == '$') + { + int taglen = (int) (t - p) + 1; + char *tag = pnstrdup(p, taglen); + const char *end = strstr(t + 1, tag); + + q = end ? end + taglen : p + strlen(p); + appendBinaryStringInfo(&o, p, q - p); + p = q; + pfree(tag); + continue; + } + } + if (p[0] == '\'') + { + bool estr = p > s && (p[-1] == 'E' || p[-1] == 'e') && + (p - 1 == s || !sql_ident_char(p[-2])); + bool ustr = p - 1 > s && p[-1] == '&' && + (p[-2] == 'U' || p[-2] == 'u') && + (p - 2 == s || !sql_ident_char(p[-3])); + /* B'..', X'..', N'..' and the like: leave other prefixes alone */ + bool prefixed = !estr && !ustr && p > s && sql_ident_char(p[-1]); + bool has_bs = false; + + q = p + 1; + while (*q) + { + if (estr && q[0] == '\\' && q[1]) + q += 2; + else if (q[0] == '\'' && q[1] == '\'') + q += 2; + else if (q[0] == '\'') + break; + else + { + if (q[0] == '\\') + has_bs = true; + q++; + } + } + if (has_bs && !estr && !ustr && !prefixed) + { + const char *c; + + appendStringInfoString(&o, "E'"); + for (c = p + 1; c < q; c++) + { + if (*c == '\\') + appendStringInfoChar(&o, '\\'); + appendStringInfoChar(&o, *c); + } + } + else + appendBinaryStringInfo(&o, p, q - p); + if (*q) + { + appendStringInfoChar(&o, '\''); + q++; + } + p = q; + continue; + } + appendStringInfoChar(&o, *p++); + } + return o.data; +} + char * plx_transpile(const char *body, const PlxFuncMeta *meta, const PlxSurface *surf, MemoryContext scratch) @@ -2649,6 +3314,13 @@ plx_transpile(const char *body, const PlxFuncMeta *meta, const PlxSurface *surf, cx.mcx = scratch; initStringInfo(&cx.out); + /* the try/finally lowering uses \x01 to delimit placeholders */ + if (strchr(body, PLX_PH)) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("%s: function body contains the control character U+0001", + surf->lanname))); + /* Run the dialect front end (the vtable method): fills cx.out. */ surf->parse_body(&cx); @@ -2692,5 +3364,5 @@ plx_transpile(const char *body, const PlxFuncMeta *meta, const PlxSurface *surf, } appendStringInfo(&asm_, "/*plx-orig:b64$%s$plx-orig*/\n", b64_encode_body(body)); - return asm_.data; + return escape_backslash_literals(asm_.data); } diff --git a/test/expected/plx_features.out b/test/expected/plx_features.out index 859077e..b0f561f 100644 --- a/test/expected/plx_features.out +++ b/test/expected/plx_features.out @@ -233,3 +233,52 @@ SELECT f_strict() AS strict_no_row; -1 (1 row) +-- a literal with a backslash reads the same whatever standard_conforming_strings +-- the caller uses: generated code writes it as E'...', so with the setting off +-- the backslash cannot swallow the closing quote and run the next literal as code +CREATE FUNCTION lit_go() RETURNS text LANGUAGE plxgo AS $P$ +a := "C:\\" +b := "; RAISE EXCEPTION $$go: injected$$; --" +return a +$P$; +CREATE FUNCTION lit_rb() RETURNS text LANGUAGE plxruby AS $P$ +a = 'C:\\' +b = "; RAISE EXCEPTION $$rb: injected$$; --" +return a +$P$; +CREATE FUNCTION lit_php() RETURNS text LANGUAGE plxphp AS $P$ +$a = 'C:\\'; +$b = '; RAISE EXCEPTION $$php: injected$$; --'; +return $a; +$P$; +CREATE FUNCTION lit_tsql() RETURNS varchar LANGUAGE plxtsql AS $P$ +DECLARE @a varchar(10) = 'C:\'; +DECLARE @b varchar(80) = '; RAISE EXCEPTION $$tsql: injected$$; --'; +RETURN @a; +$P$; +CREATE FUNCTION lit_plsql() RETURNS text LANGUAGE plxplsql AS $P$ +DECLARE a text := 'C:\'; b text := '; RAISE EXCEPTION $$plsql: injected$$; --'; +BEGIN RETURN a; END; +$P$; +CREATE FUNCTION lit_cob() RETURNS text LANGUAGE plxcobol AS $P$ +DATA DIVISION. +WORKING-STORAGE SECTION. +01 A PIC X(10) VALUE "C:\". +01 B PIC X(60) VALUE "; RAISE EXCEPTION $$cob: injected$$; --". +PROCEDURE DIVISION. + RETURN A. +$P$; +SET standard_conforming_strings = off; +SELECT lit_go(), lit_rb(), lit_php(), lit_tsql(), lit_plsql(), lit_cob(); + lit_go | lit_rb | lit_php | lit_tsql | lit_plsql | lit_cob +--------+--------+---------+----------+-----------+--------- + C:\ | C:\ | C:\ | C:\ | C:\ | C:\ +(1 row) + +RESET standard_conforming_strings; +SELECT lit_go(), lit_rb(), lit_php(), lit_tsql(), lit_plsql(), lit_cob(); + lit_go | lit_rb | lit_php | lit_tsql | lit_plsql | lit_cob +--------+--------+---------+----------+-----------+--------- + C:\ | C:\ | C:\ | C:\ | C:\ | C:\ +(1 row) + diff --git a/test/expected/plxcobol.out b/test/expected/plxcobol.out index cf488c0..5b7a6cb 100644 --- a/test/expected/plxcobol.out +++ b/test/expected/plxcobol.out @@ -490,3 +490,16 @@ SELECT msg FROM cob_log; logged (1 row) +-- hostile subscript nesting raises a clean error instead of overflowing the stack +DO $d$ BEGIN + EXECUTE 'CREATE FUNCTION cob_deep() RETURNS int LANGUAGE plxcobol AS $b$ +DATA DIVISION. +WORKING-STORAGE SECTION. +01 WS-ARR PIC 9(9) OCCURS 5 TIMES. +01 WS-X PIC 9(9). +PROCEDURE DIVISION. + COMPUTE WS-X = ' || repeat('WS-ARR(', 2000) || '1' || repeat(')', 2000) || ' + GOBACK. +$b$'; +EXCEPTION WHEN OTHERS THEN RAISE WARNING 'caught: %', SQLERRM; END $d$; +WARNING: caught: plxcobol: expression nested too deeply diff --git a/test/expected/plxgo.out b/test/expected/plxgo.out index 549dea0..b15be36 100644 --- a/test/expected/plxgo.out +++ b/test/expected/plxgo.out @@ -352,3 +352,19 @@ CREATE FUNCTION g_panic_msg(who text) RETURNS int LANGUAGE plxgo AS $$ $$; DO $d$ BEGIN PERFORM g_panic_msg(NULL); EXCEPTION WHEN OTHERS THEN RAISE NOTICE 'caught: [%]', SQLERRM; END $d$; +-- hostile nesting raises a clean error instead of overflowing the C stack +DO $d$ BEGIN + EXECUTE 'CREATE FUNCTION g_deep() RETURNS int LANGUAGE plxgo AS $b$return ' + || repeat('int(', 2000) || '1' || repeat(')', 2000) || E'\n$b$'; +EXCEPTION WHEN OTHERS THEN RAISE WARNING 'caught: %', SQLERRM; END $d$; +WARNING: caught: plxgo: expression nested too deeply +-- a Sprintf directive without a letter verb stays inside the literal +CREATE FUNCTION g_badverb(x text) RETURNS text LANGUAGE plxgo AS $$ + return fmt.Sprintf("%' || version() || '", x) +$$; +SELECT g_badverb('v'); + g_badverb +---------------------- + %' || version() || ' +(1 row) + diff --git a/test/expected/plxjs.out b/test/expected/plxjs.out index baf4fdb..4aed34d 100644 --- a/test/expected/plxjs.out +++ b/test/expected/plxjs.out @@ -241,3 +241,230 @@ SELECT js_flt() AS flt, js_exp() AS exp; 3.14 | 1500 (1 row) +-- a type annotation at the start of a condition is ignored (it used to give a +-- span with no source text and crash the backend) +CREATE FUNCTION js_ann_cond(x int) RETURNS int LANGUAGE plxjs AS $$ + if (/*:: int */ x > 0) { return 1; } + return 0; +$$; +SELECT js_ann_cond(3), js_ann_cond(-3); + js_ann_cond | js_ann_cond +-------------+------------- + 1 | 0 +(1 row) + +-- finally inside finally is emitted once per level, so deep nesting stays +-- linear in size (each level used to double the generated body) +DO $d$ DECLARE s text := 'x = 1;'; BEGIN + FOR i IN 1..40 LOOP s := 'try { x = x + 1; } finally { ' || s || ' }'; END LOOP; + EXECUTE 'CREATE FUNCTION js_fin_deep() RETURNS int LANGUAGE plxjs AS $b$ let x = 0 /*:: int */; ' + || s || ' return x; $b$'; +END $d$; +SELECT js_fin_deep(), length(prosrc) < 200000 AS linear FROM pg_proc WHERE proname = 'js_fin_deep'; + js_fin_deep | linear +-------------+-------- + 1 | t +(1 row) + +-- a return at every level of deeply nested try/finally: the generated code +-- stays linear in the source (indentation is capped), and each level's text is +-- freed once emitted (it used to be kept, which took 3.6 GB at 490 levels) +DO $d$ DECLARE s text := 'x = 1;'; BEGIN + FOR i IN 1..300 LOOP + s := 'try { if (x > 5) { return 1; } ' || s || ' } finally { x = x + 1; }'; + IF i IN (150, 300) THEN + EXECUTE 'CREATE FUNCTION js_fin_ret_deep' || i || '() RETURNS int LANGUAGE plxjs AS $b$ let x = 0 /*:: int */; ' + || s || ' return x; $b$'; + END IF; + END LOOP; +END $d$; +-- twice the depth gives about twice the code (2.06 times: the first levels are +-- less indented); growing indentation made it over three times +SELECT js_fin_ret_deep300(), + (SELECT length(prosrc) FROM pg_proc WHERE proname = 'js_fin_ret_deep300') < + 2.5 * (SELECT length(prosrc) FROM pg_proc WHERE proname = 'js_fin_ret_deep150') AS linear; + js_fin_ret_deep300 | linear +--------------------+-------- + 301 | t +(1 row) + +-- a finally body that throws runs once, not again from the error path +DO LANGUAGE plxjs $p$ + try { raise("warning", "body"); } + finally { raise("warning", "finally ran"); throw new Exception("boom"); } +$p$; +WARNING: body +WARNING: finally ran +ERROR: boom +CONTEXT: PL/pgSQL function inline_code_block line 23 at RAISE +-- a string ending in a lone backslash cannot escape its quotes: the second +-- assignment stays an inert string instead of running set_config +CREATE FUNCTION js_bs_escape() RETURNS text LANGUAGE plxjs AS $q$ +let a = `${'\n\'}`; +let b = " || set_config($x$plx.test_pwned$x$, $x$yes$x$, false)); --"; +return b; +$q$; +SELECT js_bs_escape() LIKE '%set_config%' AS literal_kept, + current_setting('plx.test_pwned', true) IS NULL AS not_run; + literal_kept | not_run +--------------+--------- + t | t +(1 row) + +-- finally runs on return, break and continue, and after an error, once each +CREATE FUNCTION js_fin_return() RETURNS int LANGUAGE plxjs AS $$ + let x = 1; + try { return x; } finally { x = 99; raise("warning", "finally after return"); } +$$; +SELECT js_fin_return(); -- 1: the value is taken before finally runs +WARNING: finally after return + js_fin_return +--------------- + 1 +(1 row) + +CREATE FUNCTION js_fin_loop() RETURNS int LANGUAGE plxjs AS $$ + let n = 0; let i = 0; + while (i < 10) { + i = i + 1; + try { if (i == 2) { continue; } if (i == 5) { break; } n = n + i; } + finally { n = n + 100; } + } + return n; +$$; +SELECT js_fin_loop(); -- 508 + js_fin_loop +------------- + 508 +(1 row) + +CREATE FUNCTION js_fin_nested() RETURNS text LANGUAGE plxjs AS $$ + try { try { return "r"; } finally { raise("warning", "inner"); } } + finally { raise("warning", "outer"); } +$$; +SELECT js_fin_nested(); +WARNING: inner +WARNING: outer + js_fin_nested +--------------- + r +(1 row) + +CREATE FUNCTION js_fin_label() RETURNS int LANGUAGE plxjs AS $$ + let n = 0; + outer: for (let i = 0; i < 3; i++) { + try { + for (let j = 0; j < 3; j++) { if (j == 1) { break; } n = n + 1; } + if (i == 1) { break outer; } + } finally { n = n + 10; } + } + return n; +$$; +SELECT js_fin_label(); -- 22 + js_fin_label +-------------- + 22 +(1 row) + +-- a loop label reused inside the try still crosses the finally: a break or +-- continue resolves to the enclosing loop once the inner one has closed +CREATE FUNCTION js_fin_label_reuse(c bool) RETURNS int LANGUAGE plxjs AS $$ + let r = 0; + outer: for (let i = 0; i < 2; i++) { + try { + outer: for (let j = 0; j < 1; j++) { r = r * 10 + 1; } + if (c) { continue outer; } + break outer; + } finally { r = r * 10 + 9; } + } + return r; +$$; +SELECT js_fin_label_reuse(false), js_fin_label_reuse(true); -- 19, 1919 + js_fin_label_reuse | js_fin_label_reuse +--------------------+-------------------- + 19 | 1919 +(1 row) + +-- an error is re-raised after finally with its SQLSTATE, detail and hint +CREATE FUNCTION js_fin_fail() RETURNS int LANGUAGE plpgsql AS $$ +BEGIN RAISE EXCEPTION USING ERRCODE = '22012', MESSAGE = 'boom', DETAIL = 'd1', HINT = 'h1'; END $$; +CREATE FUNCTION js_fin_err() RETURNS int LANGUAGE plxjs AS $$ + try { return js_fin_fail(); } finally { raise("warning", "cleanup"); } +$$; +DO $d$ DECLARE st text; dt text; ht text; BEGIN PERFORM js_fin_err(); +EXCEPTION WHEN OTHERS THEN + GET STACKED DIAGNOSTICS st = RETURNED_SQLSTATE, dt = PG_EXCEPTION_DETAIL, ht = PG_EXCEPTION_HINT; + RAISE WARNING 'caught: % % % %', st, SQLERRM, dt, ht; +END $d$; +WARNING: cleanup +WARNING: caught: 22012 boom d1 h1 +-- ... and with the names of the objects it is about (table, constraint, type) +CREATE TABLE js_fin_objt (id int CONSTRAINT js_fin_objt_pk PRIMARY KEY); +INSERT INTO js_fin_objt VALUES (1); +CREATE DOMAIN js_fin_pos AS int CONSTRAINT js_fin_pos_ck CHECK (VALUE > 0); +CREATE FUNCTION js_fin_objs(k int) RETURNS void LANGUAGE plxjs AS $$ + try { + if (k == 1) { execute("INSERT INTO js_fin_objt VALUES (1)"); } + else { execute("SELECT (-1)::js_fin_pos"); } + } finally { let z = 1; } +$$; +DO $d$ DECLARE st text; sc text; tb text; co text; ty text; cl text; BEGIN + FOR k IN 1..2 LOOP + BEGIN PERFORM js_fin_objs(k); + EXCEPTION WHEN OTHERS THEN + GET STACKED DIAGNOSTICS st = RETURNED_SQLSTATE, sc = SCHEMA_NAME, tb = TABLE_NAME, + co = CONSTRAINT_NAME, ty = PG_DATATYPE_NAME, cl = COLUMN_NAME; + RAISE WARNING 'caught %: schema=% table=% constraint=% type=% column=%', st, sc, tb, co, ty, cl; + END; + END LOOP; +END $d$; +WARNING: caught 23505: schema=public table=js_fin_objt constraint=js_fin_objt_pk type= column= +WARNING: caught 23514: schema=public table= constraint=js_fin_pos_ck type=js_fin_pos column= +-- a return in finally replaces the error; a return from catch runs finally +CREATE FUNCTION js_fin_override() RETURNS int LANGUAGE plxjs AS $$ + try { return js_fin_fail(); } finally { return 7; } +$$; +SELECT js_fin_override(); + js_fin_override +----------------- + 7 +(1 row) + +CREATE FUNCTION js_fin_catch() RETURNS int LANGUAGE plxjs AS $$ + try { return js_fin_fail(); } catch (e) { return -1; } finally { raise("warning", "after catch"); } +$$; +SELECT js_fin_catch(); +WARNING: after catch + js_fin_catch +-------------- + -1 +(1 row) + +-- a trigger returning NEW, and a set-returning function's bare return +CREATE TABLE js_fin_t(a int); +CREATE FUNCTION js_fin_trg() RETURNS trigger LANGUAGE plxjs AS $$ + try { NEW.a = NEW.a * 2; return NEW; } finally { raise("warning", "trigger finally"); } +$$; +CREATE TRIGGER js_fin_t1 BEFORE INSERT ON js_fin_t FOR EACH ROW EXECUTE FUNCTION js_fin_trg(); +INSERT INTO js_fin_t VALUES (21); +WARNING: trigger finally +SELECT * FROM js_fin_t; + a +---- + 42 +(1 row) + +DROP TABLE js_fin_t CASCADE; +CREATE FUNCTION js_fin_srf() RETURNS SETOF int LANGUAGE plxjs AS $$ + emit 1; + try { emit 2; return; } finally { emit 3; } + emit 4; +$$; +SELECT * FROM js_fin_srf(); + js_fin_srf +------------ + 1 + 2 + 3 +(3 rows) + diff --git a/test/expected/plxpython3.out b/test/expected/plxpython3.out index 1e0359f..d3f777f 100644 --- a/test/expected/plxpython3.out +++ b/test/expected/plxpython3.out @@ -201,3 +201,77 @@ SELECT py_isnull(NULL), py_isnull(5); null | notnull (1 row) +-- a type annotation inside a call argument list is ignored (it used to give a +-- span with no source text and crash the backend) +DO LANGUAGE plxpython3 $p$ +assert(#:: int + 1 > 0) +$p$; +-- call() takes a plain string name; an f-string used to emit a broken identifier +DO $d$ BEGIN + EXECUTE E'DO LANGUAGE plxpython3 $b$\ncall(f"noop")\n$b$'; +EXCEPTION WHEN OTHERS THEN RAISE WARNING 'caught: %', SQLERRM; END $d$; +WARNING: caught: plxpython3: call needs a plain string procedure name, not an f-string +-- blank lines and comments are counted once in error line numbers +DO $d$ DECLARE d text; BEGIN + EXECUTE E'DO LANGUAGE plxpython3 $b$\nx = 1\n\n\n# c\n\nclass Foo:\n pass\n$b$'; +EXCEPTION WHEN OTHERS THEN + GET STACKED DIAGNOSTICS d = PG_EXCEPTION_DETAIL; + RAISE WARNING 'caught: % (%)', SQLERRM, d; +END $d$; +WARNING: caught: plxpython3: def is not supported (at plxpython3 source line 7) +-- runs of blank or annotation lines lex in linear time (a regression takes tens +-- of seconds and trips the timeout, which the handlers below do not catch) +SET statement_timeout = '20s'; +DO $d$ BEGIN + EXECUTE E'CREATE FUNCTION py_blank_run() RETURNS int LANGUAGE plxpython3 AS $b$\nx = 1\n' + || repeat(E'\n', 400000) || E'return x\n$b$'; +END $d$; +SELECT py_blank_run(); + py_blank_run +-------------- + 1 +(1 row) + +DO $d$ BEGIN + EXECUTE E'DO LANGUAGE plxpython3 $b$\nx = 1\n' || repeat(E'#:: int\n', 40000) || E'$b$'; +EXCEPTION WHEN syntax_error OR feature_not_supported THEN NULL; END $d$; +RESET statement_timeout; +-- finally runs on return and continue +CREATE FUNCTION py_fin_return(x int) RETURNS int LANGUAGE plxpython3 AS $$ +for i in range(1, 10): + try: + if i == 3: + return i * x + if i == 1: + continue + finally: + raise("warning", f"py finally {i}") +return 0 +$$; +SELECT py_fin_return(5); +WARNING: py finally 1 +WARNING: py finally 2 +WARNING: py finally 3 + py_fin_return +--------------- + 15 +(1 row) + +CREATE FUNCTION py_fin_label_reuse() RETURNS int LANGUAGE plxpython3 AS $$ +r = 0 +outer: for i in range(0, 2): + try: + outer: for j in range(0, 1): + r = r * 10 + 1 + break outer + finally: + r = r * 10 + 9 +return r +$$; +SELECT py_fin_label_reuse(); -- 19 + py_fin_label_reuse +-------------------- + 19 +(1 row) + diff --git a/test/expected/plxruby.out b/test/expected/plxruby.out index c18c4ed..6bb90c8 100644 --- a/test/expected/plxruby.out +++ b/test/expected/plxruby.out @@ -398,3 +398,55 @@ SELECT rb_hex(); 255 (1 row) +-- many distinct locals transpile in linear time, even after an over-long name +-- (which used to disable the index); a regression takes tens of seconds +SET statement_timeout = '20s'; +DO $d$ BEGIN + EXECUTE 'CREATE FUNCTION rb_many_locals() RETURNS int LANGUAGE plxruby AS $b$' + || 'v' || repeat('a', 130) || E' = 0\n' + || (SELECT string_agg(format('v%s = %s', i, i), E'\n') FROM generate_series(1, 60000) AS i) + || E'\nreturn 1\n$b$'; +END $d$; +-- (not called: plpgsql's own compile of 60,000 variables is slow, and is not plx's) +SELECT count(*) FROM pg_proc WHERE proname = 'rb_many_locals'; + count +------- + 1 +(1 row) + +RESET statement_timeout; +-- ensure runs on return +CREATE FUNCTION rb_ensure_return(x int) RETURNS int LANGUAGE plxruby AS $$ + begin + return x * 2 + ensure + raise warning: "ruby ensure" + end +$$; +SELECT rb_ensure_return(4); +WARNING: ruby ensure + rb_ensure_return +------------------ + 8 +(1 row) + +CREATE FUNCTION rb_ensure_label_reuse() RETURNS int LANGUAGE plxruby AS $$ + r = 0 + outer: for i in 0..1 + begin + outer: for j in 0..0 + r = r * 10 + 1 + end + break outer + ensure + r = r * 10 + 9 + end + end + return r +$$; +SELECT rb_ensure_label_reuse(); -- 19 + rb_ensure_label_reuse +----------------------- + 19 +(1 row) + diff --git a/test/expected/plxtsql.out b/test/expected/plxtsql.out index daa328c..b2699ef 100644 --- a/test/expected/plxtsql.out +++ b/test/expected/plxtsql.out @@ -205,3 +205,17 @@ SELECT id, total, tag FROM tq_trg; (1 row) DROP TABLE tq_trg CASCADE; +-- hostile nesting raises a clean error instead of overflowing the C stack +DO $d$ BEGIN + EXECUTE 'DO LANGUAGE plxtsql $b$RETURN ' + || repeat('ISNULL(', 2000) || '1' || repeat(', 0)', 2000) || ';$b$'; +EXCEPTION WHEN OTHERS THEN RAISE WARNING 'caught: %', SQLERRM; END $d$; +WARNING: caught: plxtsql: expression nested too deeply +-- below the 500-level counter, a small stack is guarded by check_stack_depth() +SET max_stack_depth = '100kB'; +DO $d$ BEGIN + EXECUTE 'DO LANGUAGE plxtsql $b$RETURN ' + || repeat('ISNULL(', 450) || '1' || repeat(', 0)', 450) || ';$b$'; +EXCEPTION WHEN OTHERS THEN RAISE WARNING 'caught: %', SQLERRM; END $d$; +WARNING: caught: stack depth limit exceeded +RESET max_stack_depth; diff --git a/test/finally_diff.py b/test/finally_diff.py new file mode 100755 index 0000000..d081711 --- /dev/null +++ b/test/finally_diff.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python3 +"""Differential check of try/except/finally control flow. + +Generates random Python programs that mix nested try/except/finally, for +loops, return/break/continue (also inside finally) and raised errors, runs +each in real Python and as a plxpython3 function, and requires the results to +agree. `acc` folds in every executed statement, so a result encodes the order +of execution, not just the last value. + +Needs the extension installed and a running server, like installcheck: + PGDATABASE=contrib_regression python3 test/finally_diff.py [cases] [seed] +""" +import os, random, subprocess, sys, warnings + +warnings.simplefilter("ignore") # Python warns about return in finally +random.seed(int(sys.argv[2]) if len(sys.argv) > 2 else 1) +N = int(sys.argv[1]) if len(sys.argv) > 1 else 200 +K = [0] +def k(): + K[0] += 1; return K[0] % 7 + 1 +def block(d, inloop, ind): + out = [] + for _ in range(random.randint(1, 3)): + out += stmt(d, inloop, ind) + return out +def stmt(d, inloop, ind): + p = " " * ind + r = random.random() + if d >= 4 or r < 0.30: + return [f"{p}acc = (acc * 3 + {k()}) % 1000003"] + if r < 0.40: + return [f"{p}if acc % 2 == 0:"] + [f"{p} return acc"] + if r < 0.48 and inloop: + return [f"{p}if acc % 3 == 0:", f"{p} break"] + if r < 0.56 and inloop: + return [f"{p}if acc % 5 == 1:", f"{p} continue"] + if r < 0.62: + return [f"{p}if acc % 4 == 1:", f"{p} raise ValueError(\"x\")"] + if r < 0.75: + v = f"i{d}" + return [f"{p}for {v} in range(0, 3):"] + block(d + 1, True, ind + 4) + # try + lines = [f"{p}try:"] + block(d + 1, inloop, ind + 4) + form = random.choice(["exc", "fin", "both"]) + if form in ("exc", "both"): + lines += [f"{p}except Exception as e:"] + block(d + 1, inloop, ind + 4) + if form in ("fin", "both"): + lines += [f"{p}finally:"] + block(d + 1, inloop, ind + 4) + return lines +def prog(): + return ["acc = 1"] + block(0, False, 0) + ["return acc"] +def run_py(body): + src = "def f():\n" + "\n".join(" " + l for l in body) + "\n" + ns = {} + exec(src, ns) + try: + return str(ns["f"]()) + except ValueError as e: + # plx raises it as P0001 with the same message + return f"ERROR P0001:{e}" +cases = [] +for n in range(N): + body = prog() + cases.append((n, body, run_py(body))) +sql = ["SET client_min_messages = warning;"] +for n, body, _ in cases: + b = "\n".join(body) + sql.append(f"CREATE OR REPLACE FUNCTION fd{n}() RETURNS bigint LANGUAGE plxpython3 AS $plx$\nacc #:: bigint\n{b}\n$plx$;") + sql.append(f"DO $d$ BEGIN RAISE WARNING 'R{n}=%', fd{n}(); EXCEPTION WHEN OTHERS THEN RAISE WARNING 'R{n}=ERROR %:%', SQLSTATE, SQLERRM; END $d$;") +r = subprocess.run([os.environ.get("PLX_PSQL", "psql"), "-q", "-X"], input="\n".join(sql), + capture_output=True, text=True) +got = {} +for line in r.stderr.splitlines(): + if "R" in line and "=" in line and "WARNING" in line: + tag = line.split("WARNING:")[1].strip() + key, val = tag.split("=", 1) + got[int(key[1:])] = val +other = [l for l in r.stderr.splitlines() if "WARNING" not in l] +bad = 0 +for n, body, exp in cases: + g = got.get(n, "MISSING") + if g != exp: + bad += 1 + if bad <= 3: + print(f"--- case {n}: python={exp} plx={g}\n" + "\n".join(body)) +print(f"{N} cases, {bad} mismatches; other stderr lines: {len(other)}") +for l in other[:5]: + print(" ", l) +sys.exit(1 if bad or other else 0) diff --git a/test/sql/plx_features.sql b/test/sql/plx_features.sql index 6e2699d..a5ea987 100644 --- a/test/sql/plx_features.sql +++ b/test/sql/plx_features.sql @@ -143,3 +143,43 @@ rescue => e end $$; SELECT f_strict() AS strict_no_row; + +-- a literal with a backslash reads the same whatever standard_conforming_strings +-- the caller uses: generated code writes it as E'...', so with the setting off +-- the backslash cannot swallow the closing quote and run the next literal as code +CREATE FUNCTION lit_go() RETURNS text LANGUAGE plxgo AS $P$ +a := "C:\\" +b := "; RAISE EXCEPTION $$go: injected$$; --" +return a +$P$; +CREATE FUNCTION lit_rb() RETURNS text LANGUAGE plxruby AS $P$ +a = 'C:\\' +b = "; RAISE EXCEPTION $$rb: injected$$; --" +return a +$P$; +CREATE FUNCTION lit_php() RETURNS text LANGUAGE plxphp AS $P$ +$a = 'C:\\'; +$b = '; RAISE EXCEPTION $$php: injected$$; --'; +return $a; +$P$; +CREATE FUNCTION lit_tsql() RETURNS varchar LANGUAGE plxtsql AS $P$ +DECLARE @a varchar(10) = 'C:\'; +DECLARE @b varchar(80) = '; RAISE EXCEPTION $$tsql: injected$$; --'; +RETURN @a; +$P$; +CREATE FUNCTION lit_plsql() RETURNS text LANGUAGE plxplsql AS $P$ +DECLARE a text := 'C:\'; b text := '; RAISE EXCEPTION $$plsql: injected$$; --'; +BEGIN RETURN a; END; +$P$; +CREATE FUNCTION lit_cob() RETURNS text LANGUAGE plxcobol AS $P$ +DATA DIVISION. +WORKING-STORAGE SECTION. +01 A PIC X(10) VALUE "C:\". +01 B PIC X(60) VALUE "; RAISE EXCEPTION $$cob: injected$$; --". +PROCEDURE DIVISION. + RETURN A. +$P$; +SET standard_conforming_strings = off; +SELECT lit_go(), lit_rb(), lit_php(), lit_tsql(), lit_plsql(), lit_cob(); +RESET standard_conforming_strings; +SELECT lit_go(), lit_rb(), lit_php(), lit_tsql(), lit_plsql(), lit_cob(); diff --git a/test/sql/plxcobol.sql b/test/sql/plxcobol.sql index 0bc2f3e..44474e4 100644 --- a/test/sql/plxcobol.sql +++ b/test/sql/plxcobol.sql @@ -376,3 +376,16 @@ PROCEDURE DIVISION. $$; SELECT cob_donote(); SELECT msg FROM cob_log; + +-- hostile subscript nesting raises a clean error instead of overflowing the stack +DO $d$ BEGIN + EXECUTE 'CREATE FUNCTION cob_deep() RETURNS int LANGUAGE plxcobol AS $b$ +DATA DIVISION. +WORKING-STORAGE SECTION. +01 WS-ARR PIC 9(9) OCCURS 5 TIMES. +01 WS-X PIC 9(9). +PROCEDURE DIVISION. + COMPUTE WS-X = ' || repeat('WS-ARR(', 2000) || '1' || repeat(')', 2000) || ' + GOBACK. +$b$'; +EXCEPTION WHEN OTHERS THEN RAISE WARNING 'caught: %', SQLERRM; END $d$; diff --git a/test/sql/plxgo.sql b/test/sql/plxgo.sql index c3565c3..c11e9c2 100644 --- a/test/sql/plxgo.sql +++ b/test/sql/plxgo.sql @@ -246,3 +246,15 @@ CREATE FUNCTION g_panic_msg(who text) RETURNS int LANGUAGE plxgo AS $$ $$; DO $d$ BEGIN PERFORM g_panic_msg(NULL); EXCEPTION WHEN OTHERS THEN RAISE NOTICE 'caught: [%]', SQLERRM; END $d$; + +-- hostile nesting raises a clean error instead of overflowing the C stack +DO $d$ BEGIN + EXECUTE 'CREATE FUNCTION g_deep() RETURNS int LANGUAGE plxgo AS $b$return ' + || repeat('int(', 2000) || '1' || repeat(')', 2000) || E'\n$b$'; +EXCEPTION WHEN OTHERS THEN RAISE WARNING 'caught: %', SQLERRM; END $d$; + +-- a Sprintf directive without a letter verb stays inside the literal +CREATE FUNCTION g_badverb(x text) RETURNS text LANGUAGE plxgo AS $$ + return fmt.Sprintf("%' || version() || '", x) +$$; +SELECT g_badverb('v'); diff --git a/test/sql/plxjs.sql b/test/sql/plxjs.sql index 17218fa..2f6856a 100644 --- a/test/sql/plxjs.sql +++ b/test/sql/plxjs.sql @@ -165,3 +165,156 @@ SELECT js_hex() AS hex, js_bin() AS bin, js_oct() AS oct, js_hex_us() AS hex_us; CREATE FUNCTION js_flt() RETURNS numeric LANGUAGE plxjs AS $$ return 3.14; $$; CREATE FUNCTION js_exp() RETURNS numeric LANGUAGE plxjs AS $$ return 1.5e3; $$; SELECT js_flt() AS flt, js_exp() AS exp; + +-- a type annotation at the start of a condition is ignored (it used to give a +-- span with no source text and crash the backend) +CREATE FUNCTION js_ann_cond(x int) RETURNS int LANGUAGE plxjs AS $$ + if (/*:: int */ x > 0) { return 1; } + return 0; +$$; +SELECT js_ann_cond(3), js_ann_cond(-3); + +-- finally inside finally is emitted once per level, so deep nesting stays +-- linear in size (each level used to double the generated body) +DO $d$ DECLARE s text := 'x = 1;'; BEGIN + FOR i IN 1..40 LOOP s := 'try { x = x + 1; } finally { ' || s || ' }'; END LOOP; + EXECUTE 'CREATE FUNCTION js_fin_deep() RETURNS int LANGUAGE plxjs AS $b$ let x = 0 /*:: int */; ' + || s || ' return x; $b$'; +END $d$; +SELECT js_fin_deep(), length(prosrc) < 200000 AS linear FROM pg_proc WHERE proname = 'js_fin_deep'; + +-- a return at every level of deeply nested try/finally: the generated code +-- stays linear in the source (indentation is capped), and each level's text is +-- freed once emitted (it used to be kept, which took 3.6 GB at 490 levels) +DO $d$ DECLARE s text := 'x = 1;'; BEGIN + FOR i IN 1..300 LOOP + s := 'try { if (x > 5) { return 1; } ' || s || ' } finally { x = x + 1; }'; + IF i IN (150, 300) THEN + EXECUTE 'CREATE FUNCTION js_fin_ret_deep' || i || '() RETURNS int LANGUAGE plxjs AS $b$ let x = 0 /*:: int */; ' + || s || ' return x; $b$'; + END IF; + END LOOP; +END $d$; +-- twice the depth gives about twice the code (2.06 times: the first levels are +-- less indented); growing indentation made it over three times +SELECT js_fin_ret_deep300(), + (SELECT length(prosrc) FROM pg_proc WHERE proname = 'js_fin_ret_deep300') < + 2.5 * (SELECT length(prosrc) FROM pg_proc WHERE proname = 'js_fin_ret_deep150') AS linear; + +-- a finally body that throws runs once, not again from the error path +DO LANGUAGE plxjs $p$ + try { raise("warning", "body"); } + finally { raise("warning", "finally ran"); throw new Exception("boom"); } +$p$; + +-- a string ending in a lone backslash cannot escape its quotes: the second +-- assignment stays an inert string instead of running set_config +CREATE FUNCTION js_bs_escape() RETURNS text LANGUAGE plxjs AS $q$ +let a = `${'\n\'}`; +let b = " || set_config($x$plx.test_pwned$x$, $x$yes$x$, false)); --"; +return b; +$q$; +SELECT js_bs_escape() LIKE '%set_config%' AS literal_kept, + current_setting('plx.test_pwned', true) IS NULL AS not_run; + +-- finally runs on return, break and continue, and after an error, once each +CREATE FUNCTION js_fin_return() RETURNS int LANGUAGE plxjs AS $$ + let x = 1; + try { return x; } finally { x = 99; raise("warning", "finally after return"); } +$$; +SELECT js_fin_return(); -- 1: the value is taken before finally runs +CREATE FUNCTION js_fin_loop() RETURNS int LANGUAGE plxjs AS $$ + let n = 0; let i = 0; + while (i < 10) { + i = i + 1; + try { if (i == 2) { continue; } if (i == 5) { break; } n = n + i; } + finally { n = n + 100; } + } + return n; +$$; +SELECT js_fin_loop(); -- 508 +CREATE FUNCTION js_fin_nested() RETURNS text LANGUAGE plxjs AS $$ + try { try { return "r"; } finally { raise("warning", "inner"); } } + finally { raise("warning", "outer"); } +$$; +SELECT js_fin_nested(); +CREATE FUNCTION js_fin_label() RETURNS int LANGUAGE plxjs AS $$ + let n = 0; + outer: for (let i = 0; i < 3; i++) { + try { + for (let j = 0; j < 3; j++) { if (j == 1) { break; } n = n + 1; } + if (i == 1) { break outer; } + } finally { n = n + 10; } + } + return n; +$$; +SELECT js_fin_label(); -- 22 +-- a loop label reused inside the try still crosses the finally: a break or +-- continue resolves to the enclosing loop once the inner one has closed +CREATE FUNCTION js_fin_label_reuse(c bool) RETURNS int LANGUAGE plxjs AS $$ + let r = 0; + outer: for (let i = 0; i < 2; i++) { + try { + outer: for (let j = 0; j < 1; j++) { r = r * 10 + 1; } + if (c) { continue outer; } + break outer; + } finally { r = r * 10 + 9; } + } + return r; +$$; +SELECT js_fin_label_reuse(false), js_fin_label_reuse(true); -- 19, 1919 +-- an error is re-raised after finally with its SQLSTATE, detail and hint +CREATE FUNCTION js_fin_fail() RETURNS int LANGUAGE plpgsql AS $$ +BEGIN RAISE EXCEPTION USING ERRCODE = '22012', MESSAGE = 'boom', DETAIL = 'd1', HINT = 'h1'; END $$; +CREATE FUNCTION js_fin_err() RETURNS int LANGUAGE plxjs AS $$ + try { return js_fin_fail(); } finally { raise("warning", "cleanup"); } +$$; +DO $d$ DECLARE st text; dt text; ht text; BEGIN PERFORM js_fin_err(); +EXCEPTION WHEN OTHERS THEN + GET STACKED DIAGNOSTICS st = RETURNED_SQLSTATE, dt = PG_EXCEPTION_DETAIL, ht = PG_EXCEPTION_HINT; + RAISE WARNING 'caught: % % % %', st, SQLERRM, dt, ht; +END $d$; +-- ... and with the names of the objects it is about (table, constraint, type) +CREATE TABLE js_fin_objt (id int CONSTRAINT js_fin_objt_pk PRIMARY KEY); +INSERT INTO js_fin_objt VALUES (1); +CREATE DOMAIN js_fin_pos AS int CONSTRAINT js_fin_pos_ck CHECK (VALUE > 0); +CREATE FUNCTION js_fin_objs(k int) RETURNS void LANGUAGE plxjs AS $$ + try { + if (k == 1) { execute("INSERT INTO js_fin_objt VALUES (1)"); } + else { execute("SELECT (-1)::js_fin_pos"); } + } finally { let z = 1; } +$$; +DO $d$ DECLARE st text; sc text; tb text; co text; ty text; cl text; BEGIN + FOR k IN 1..2 LOOP + BEGIN PERFORM js_fin_objs(k); + EXCEPTION WHEN OTHERS THEN + GET STACKED DIAGNOSTICS st = RETURNED_SQLSTATE, sc = SCHEMA_NAME, tb = TABLE_NAME, + co = CONSTRAINT_NAME, ty = PG_DATATYPE_NAME, cl = COLUMN_NAME; + RAISE WARNING 'caught %: schema=% table=% constraint=% type=% column=%', st, sc, tb, co, ty, cl; + END; + END LOOP; +END $d$; +-- a return in finally replaces the error; a return from catch runs finally +CREATE FUNCTION js_fin_override() RETURNS int LANGUAGE plxjs AS $$ + try { return js_fin_fail(); } finally { return 7; } +$$; +SELECT js_fin_override(); +CREATE FUNCTION js_fin_catch() RETURNS int LANGUAGE plxjs AS $$ + try { return js_fin_fail(); } catch (e) { return -1; } finally { raise("warning", "after catch"); } +$$; +SELECT js_fin_catch(); +-- a trigger returning NEW, and a set-returning function's bare return +CREATE TABLE js_fin_t(a int); +CREATE FUNCTION js_fin_trg() RETURNS trigger LANGUAGE plxjs AS $$ + try { NEW.a = NEW.a * 2; return NEW; } finally { raise("warning", "trigger finally"); } +$$; +CREATE TRIGGER js_fin_t1 BEFORE INSERT ON js_fin_t FOR EACH ROW EXECUTE FUNCTION js_fin_trg(); +INSERT INTO js_fin_t VALUES (21); +SELECT * FROM js_fin_t; +DROP TABLE js_fin_t CASCADE; +CREATE FUNCTION js_fin_srf() RETURNS SETOF int LANGUAGE plxjs AS $$ + emit 1; + try { emit 2; return; } finally { emit 3; } + emit 4; +$$; +SELECT * FROM js_fin_srf(); diff --git a/test/sql/plxpython3.sql b/test/sql/plxpython3.sql index d87b56a..26a45a8 100644 --- a/test/sql/plxpython3.sql +++ b/test/sql/plxpython3.sql @@ -138,3 +138,61 @@ if x is not None: return "?" $$; SELECT py_isnull(NULL), py_isnull(5); + +-- a type annotation inside a call argument list is ignored (it used to give a +-- span with no source text and crash the backend) +DO LANGUAGE plxpython3 $p$ +assert(#:: int + 1 > 0) +$p$; + +-- call() takes a plain string name; an f-string used to emit a broken identifier +DO $d$ BEGIN + EXECUTE E'DO LANGUAGE plxpython3 $b$\ncall(f"noop")\n$b$'; +EXCEPTION WHEN OTHERS THEN RAISE WARNING 'caught: %', SQLERRM; END $d$; + +-- blank lines and comments are counted once in error line numbers +DO $d$ DECLARE d text; BEGIN + EXECUTE E'DO LANGUAGE plxpython3 $b$\nx = 1\n\n\n# c\n\nclass Foo:\n pass\n$b$'; +EXCEPTION WHEN OTHERS THEN + GET STACKED DIAGNOSTICS d = PG_EXCEPTION_DETAIL; + RAISE WARNING 'caught: % (%)', SQLERRM, d; +END $d$; + +-- runs of blank or annotation lines lex in linear time (a regression takes tens +-- of seconds and trips the timeout, which the handlers below do not catch) +SET statement_timeout = '20s'; +DO $d$ BEGIN + EXECUTE E'CREATE FUNCTION py_blank_run() RETURNS int LANGUAGE plxpython3 AS $b$\nx = 1\n' + || repeat(E'\n', 400000) || E'return x\n$b$'; +END $d$; +SELECT py_blank_run(); +DO $d$ BEGIN + EXECUTE E'DO LANGUAGE plxpython3 $b$\nx = 1\n' || repeat(E'#:: int\n', 40000) || E'$b$'; +EXCEPTION WHEN syntax_error OR feature_not_supported THEN NULL; END $d$; +RESET statement_timeout; +-- finally runs on return and continue +CREATE FUNCTION py_fin_return(x int) RETURNS int LANGUAGE plxpython3 AS $$ +for i in range(1, 10): + try: + if i == 3: + return i * x + if i == 1: + continue + finally: + raise("warning", f"py finally {i}") +return 0 +$$; +SELECT py_fin_return(5); +CREATE FUNCTION py_fin_label_reuse() RETURNS int LANGUAGE plxpython3 AS $$ +r = 0 +outer: for i in range(0, 2): + try: + outer: for j in range(0, 1): + r = r * 10 + 1 + break outer + finally: + r = r * 10 + 9 +return r +$$; +SELECT py_fin_label_reuse(); -- 19 diff --git a/test/sql/plxruby.sql b/test/sql/plxruby.sql index 37f5d2c..9b9a7d7 100644 --- a/test/sql/plxruby.sql +++ b/test/sql/plxruby.sql @@ -282,3 +282,40 @@ CREATE FUNCTION rb_hex() RETURNS int LANGUAGE plxruby AS $$ return 0xff $$; SELECT rb_hex(); + +-- many distinct locals transpile in linear time, even after an over-long name +-- (which used to disable the index); a regression takes tens of seconds +SET statement_timeout = '20s'; +DO $d$ BEGIN + EXECUTE 'CREATE FUNCTION rb_many_locals() RETURNS int LANGUAGE plxruby AS $b$' + || 'v' || repeat('a', 130) || E' = 0\n' + || (SELECT string_agg(format('v%s = %s', i, i), E'\n') FROM generate_series(1, 60000) AS i) + || E'\nreturn 1\n$b$'; +END $d$; +-- (not called: plpgsql's own compile of 60,000 variables is slow, and is not plx's) +SELECT count(*) FROM pg_proc WHERE proname = 'rb_many_locals'; +RESET statement_timeout; +-- ensure runs on return +CREATE FUNCTION rb_ensure_return(x int) RETURNS int LANGUAGE plxruby AS $$ + begin + return x * 2 + ensure + raise warning: "ruby ensure" + end +$$; +SELECT rb_ensure_return(4); +CREATE FUNCTION rb_ensure_label_reuse() RETURNS int LANGUAGE plxruby AS $$ + r = 0 + outer: for i in 0..1 + begin + outer: for j in 0..0 + r = r * 10 + 1 + end + break outer + ensure + r = r * 10 + 9 + end + end + return r +$$; +SELECT rb_ensure_label_reuse(); -- 19 diff --git a/test/sql/plxtsql.sql b/test/sql/plxtsql.sql index 1d9f354..e95cd2c 100644 --- a/test/sql/plxtsql.sql +++ b/test/sql/plxtsql.sql @@ -147,3 +147,17 @@ CREATE TRIGGER tq_trg_ins BEFORE INSERT ON tq_trg INSERT INTO tq_trg(id, qty, price) VALUES (7, 3, 10); SELECT id, total, tag FROM tq_trg; DROP TABLE tq_trg CASCADE; + +-- hostile nesting raises a clean error instead of overflowing the C stack +DO $d$ BEGIN + EXECUTE 'DO LANGUAGE plxtsql $b$RETURN ' + || repeat('ISNULL(', 2000) || '1' || repeat(', 0)', 2000) || ';$b$'; +EXCEPTION WHEN OTHERS THEN RAISE WARNING 'caught: %', SQLERRM; END $d$; + +-- below the 500-level counter, a small stack is guarded by check_stack_depth() +SET max_stack_depth = '100kB'; +DO $d$ BEGIN + EXECUTE 'DO LANGUAGE plxtsql $b$RETURN ' + || repeat('ISNULL(', 450) || '1' || repeat(', 0)', 450) || ';$b$'; +EXCEPTION WHEN OTHERS THEN RAISE WARNING 'caught: %', SQLERRM; END $d$; +RESET max_stack_depth;