pax_global_header00006660000000000000000000000064152674406050014523gustar00rootroot0000000000000052 comment=9a5c0ec4babebd5eb8e0db104c54f7ddb43fcc07 okbob-plpgsql_check-800db79/000077500000000000000000000000001526744060500157735ustar00rootroot00000000000000okbob-plpgsql_check-800db79/.editorconfig000066400000000000000000000002671526744060500204550ustar00rootroot00000000000000root = true [*.{c,h,l,y,pl,pm}] indent_style = tab indent_size = tab tab_width = 4 [*.{sgml,xml}] indent_style = space indent_size = 1 [*.xsl] indent_style = space indent_size = 2 okbob-plpgsql_check-800db79/.github/000077500000000000000000000000001526744060500173335ustar00rootroot00000000000000okbob-plpgsql_check-800db79/.github/workflows/000077500000000000000000000000001526744060500213705ustar00rootroot00000000000000okbob-plpgsql_check-800db79/.github/workflows/main.yml000066400000000000000000000005611526744060500230410ustar00rootroot00000000000000name: CI on: push: branches: - master - main pull_request: jobs: test: strategy: matrix: pg: [18, 17, 16, 15, 14] name: 🐘 PostgreSQL ${{ matrix.pg }} runs-on: ubuntu-latest container: pgxn/pgxn-tools steps: - run: pg-start ${{ matrix.pg }} - uses: actions/checkout@v2 - run: pg-build-test okbob-plpgsql_check-800db79/.github/workflows/release.yml000066400000000000000000000007001526744060500235300ustar00rootroot00000000000000name: Release on: push: tags: [v*] jobs: release: name: Release on PGXN runs-on: ubuntu-latest container: pgxn/pgxn-tools env: PGXN_USERNAME: ${{ secrets.PGXN_USERNAME }} PGXN_PASSWORD: ${{ secrets.PGXN_PASSWORD }} steps: - name: Check out the repo uses: actions/checkout@v4 - name: Bundle the Release id: bundle run: pgxn-bundle - name: Release on PGXN run: pgxn-release okbob-plpgsql_check-800db79/.gitignore000066400000000000000000000004561526744060500177700ustar00rootroot00000000000000# Global excludes across all subdirectories *.o *.so *.so.[0-9] *.so.[0-9].[0-9] *.sl *.sl.[0-9] *.sl.[0-9].[0-9] *.dylib *.dll *.a *.mo *.bc objfiles.txt .deps/ *.gcno *.gcda *.gcov *.gcov.out lcov.info *.vcproj *.vcxproj win32ver.rc *.exe lib*dll.def lib*.pc /results regression.diffs regression.out okbob-plpgsql_check-800db79/LICENSE000066400000000000000000000024651526744060500170070ustar00rootroot00000000000000# Licence Copyright (c) Pavel Stehule (pavel.stehule@gmail.com) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. # Note If you like it, send a postcard to address Pavel Stehule Skalice 12 256 01 Benesov u Prahy Czech Republic I invite any questions, comments, bug reports, patches on mail address pavel.stehule@gmail.com okbob-plpgsql_check-800db79/META.json000066400000000000000000000027101526744060500174140ustar00rootroot00000000000000{ "name": "plpgsql_check", "abstract": "Additional tools for plpgsql functions validation", "description": "The plpgsql_check is PostgreSQL extension with functionality for direct or indirect extra validation of functions in plpgsql language. It verifies a validity of SQL identifiers used in plpgsql code. It try to identify a performance issues. Modern versions has integrated profiler. The table and function dependencies can be displayed", "version": "2.10.16", "maintainer": "Pavel STEHULE ", "license": "bsd", "provides": { "plpgsql_check": { "abstract": "Additional tools for plpgsql functions validation", "file": "sql/plpgsql_check_active.sql", "docfile": "README.md", "version": "2.10.16" } }, "prereqs": { "runtime": { "requires": { "PostgreSQL": "14.0.0", "plpgsql": 0 } } }, "resources": { "bugtracker": { "web": "https://github.com/okbob/plpgsql_check/issues/" }, "repository": { "url": "git://github.com/okbob/plpgsql_check.git", "web": "https://github.com/okbob/plpgsql_check/", "type": "git" } }, "generated_by": "David E. Wheeler", "meta-spec": { "version": "1.0.0", "url": "http://pgxn.org/meta/spec.txt" }, "tags": [ "plpgsql", "analyzer", "validate", "development", "profiler" ] } okbob-plpgsql_check-800db79/Makefile000066400000000000000000000033111526744060500174310ustar00rootroot00000000000000# $PostgreSQL: pgsql/contrib/plpgsql_check/Makefile MODULE_big = plpgsql_check OBJS = $(patsubst %.c,%.o,$(wildcard src/*.c)) DATA = plpgsql_check--2.10.sql EXTENSION = plpgsql_check ifndef MAJORVERSION MAJORVERSION := $(basename $(VERSION)) endif REGRESS_OPTS = --dbname=$(PL_TESTDB) # Run plpgsql_check_nocrash with and without shared preloading. installcheck # does not restart its target server, so these configurations must be started # separately. REGRESS = plpgsql_check_passive\ plpgsql_check_active\ plpgsql_check_active-$(MAJORVERSION)\ plpgsql_check_passive-$(MAJORVERSION)\ plpgsql_check_profiler\ plpgsql_pragma_generator\ plpgsql_check_stmt_walker\ plpgsql_check_pragma\ plpgsql_check_comment_options\ plpgsql_check_parser\ plpgsql_check_profiler_local\ plpgsql_check_profiler_queryid\ plpgsql_check_profiler_ctrl\ plpgsql_check_tablefunc\ plpgsql_check_tracer\ plpgsql_check_report\ plpgsql_check_stmtwalk\ plpgsql_check_expr_walk\ plpgsql_check_assign\ plpgsql_check_catalog\ plpgsql_check_check_function\ plpgsql_check_pldbgapi3\ plpgsql_check_nocrash\ plpgsql_check_nocrash_unstable_01\ plpgsql_check_comment_options_legacystr ifdef NO_PGXS subdir = contrib/plpgsql_check top_builddir = ../.. include $(top_builddir)/src/Makefile.global include $(top_srcdir)/contrib/contrib-global.mk else PG_CONFIG ?= pg_config PGXS := $(shell $(PG_CONFIG) --pgxs) include $(PGXS) endif # temporary fix of compilation with gcc 15 override CFLAGS += -Wno-error=incompatible-pointer-types -I$(top_builddir)/src/pl/plpgsql/src -Wall -g plpgsql_check.typedefs: $(OBJS) ./typedefs_gen.py pgindent: plpgsql_check.typedefs pgindent --typedefs=plpgsql_check.typedefs src/*.c src/*.h okbob-plpgsql_check-800db79/README.md000066400000000000000000001567031526744060500172660ustar00rootroot00000000000000[![Stand With Ukraine](https://raw.githubusercontent.com/vshymanskyy/StandWithUkraine/main/banner2-direct.svg)](https://stand-with-ukraine.pp.ua) plpgsql_check ============= This extension is a full linter for plpgsql for PostgreSQL. It leverages only the internal PostgreSQL parser/evaluator so you see exactly the errors would occur at runtime. Furthermore, it parses the SQL inside your routines and finds errors not usually found during the "CREATE PROCEDURE/FUNCTION" command. You can control the levels of many warnings and hints. Finally, you can add PRAGMA type markers to turn off/on many aspects allowing you to hide messages you already know about, or to remind you to come back for deeper cleaning later. I founded this project, because I wanted to publish the code I wrote for the two years, when I tried to write enhanced checking for PostgreSQL upstream. It was not fully successful - integration into upstream requires some larger plpgsql refactoring. But the code is fully functional and can be used in production (and it is used in production). So, I created this extension to be available for all plpgsql developers. If you want to join our group to help the further development of this extension, register yourself at that [postgresql extension hacking](https://groups.google.com/forum/#!forum/postgresql-extensions-hacking) google group. # Features * checks fields of referenced database objects and types inside embedded SQL * validates you are using the correct types for function parameters * identifies unused variables and function arguments, unmodified OUT arguments * partial detection of dead code (code after an RETURN command) * detection of missing RETURN command in function (common after exception handlers, complex logic) * tries to identify unwanted hidden casts, which can be a performance issue like unused indexes * ability to collect relations and functions used by function * ability to check EXECUTE statements against SQL injection vulnerability I invite any ideas, patches, bugreports. PostgreSQL 14 - 20 are supported. The SQL statements inside PL/pgSQL functions are checked by the validator for semantic errors. These errors can be found by calling the plpgsql_check_function: # Active mode postgres=# CREATE EXTENSION plpgsql_check; CREATE EXTENSION postgres=# CREATE TABLE t1(a int, b int); CREATE TABLE postgres=# CREATE OR REPLACE FUNCTION public.f1() RETURNS void LANGUAGE plpgsql AS $function$ DECLARE r record; BEGIN FOR r IN SELECT * FROM t1 LOOP RAISE NOTICE '%', r.c; -- there is bug - table t1 missing "c" column END LOOP; END; $function$; CREATE FUNCTION postgres=# select f1(); -- execution doesn't find a bug due to empty table t1 f1 ──── (1 row) postgres=# \x Expanded display is on. postgres=# select * from plpgsql_check_function_tb('f1()'); ─[ RECORD 1 ]─────────────────────────── functionid β”‚ f1 lineno β”‚ 6 statement β”‚ RAISE sqlstate β”‚ 42703 message β”‚ record "r" has no field "c" detail β”‚ [null] hint β”‚ [null] level β”‚ error position β”‚ 0 query β”‚ [null] postgres=# \sf+ f1 CREATE OR REPLACE FUNCTION public.f1() RETURNS void LANGUAGE plpgsql 1 AS $function$ 2 DECLARE r record; 3 BEGIN 4 FOR r IN SELECT * FROM t1 5 LOOP 6 RAISE NOTICE '%', r.c; -- there is bug - table t1 missing "c" column 7 END LOOP; 8 END; 9 $function$ Function plpgsql_check_function() has three possible output formats: text, json or xml select * from plpgsql_check_function('f1()', fatal_errors := false); plpgsql_check_function ------------------------------------------------------------------------ error:42703:4:SQL statement:column "c" of relation "t1" does not exist Query: update t1 set c = 30 -- ^ error:42P01:7:RAISE:missing FROM-clause entry for table "r" Query: SELECT r.c -- ^ error:42601:7:RAISE:too few parameters specified for RAISE (7 rows) postgres=# select * from plpgsql_check_function('fx()', format:='xml'); plpgsql_check_function ──────────────────────────────────────────────────────────────── ↡ ↡ error ↡ 42P01 ↡ relation "foo111" does not exist ↡ RETURN ↡ SELECT (select a from foo111)↡ ↡ (1 row) ## Arguments You can set level of warnings via function's parameters: ### Mandatory argument * `funcoid regprocedure` (or `name text` for the text overload) - function name or function signature. Any function in PostgreSQL can be specified by Oid or by name or by signature. When you know oid or complete function's signature, you can use a regprocedure type parameter like `'fx()'::regprocedure` or `16799::regprocedure`. Possible alternative is using a name only, when function's name is unique - like `'fx'`. When the name is not unique or the function doesn't exists it raises a error. ### Optional arguments * `relid regclass DEFAULT 0` - relation on which a DML trigger operates. It is required for checking DML trigger functions, but must not be supplied for ordinary functions or procedures. It can also be supplied by an in-comment option. * `fatal_errors boolean DEFAULT true` - stop on first error (prevents massive error reports) * `other_warnings boolean DEFAULT true` - show warnings like different attributes number in assignment on left and right side, variable overlaps function's parameter, unused variables, unwanted casting, etc. * `extra_warnings boolean DEFAULT true` - show warnings like missing `RETURN`, shadowed variables, dead code, never read (unused) function's parameter, unmodified variables, modified auto variables, etc. * `performance_warnings boolean DEFAULT false` - performance related warnings like declared type with type modifier, casting, implicit casts in where clause (can be the reason why an index is not used), etc. * `security_warnings boolean DEFAULT false` - security related checks like SQL injection vulnerability detection * `compatibility_warnings boolean DEFAULT false` - compatibility related checks like obsolete explicit setting internal cursor names in refcursor's or cursor's variables. * `anyelememttype regtype DEFAULT 'int'` - an actual type to be used when testing `anyelement`. The SQL argument retains this historical misspelling; the corresponding in-comment option is spelled `anyelementtype`. * `anyenumtype regtype DEFAULT '-'` - an actual type to be used when testing the anyenum type * `anyrangetype regtype DEFAULT 'int4range'` - an actual type to be used when testing `anyrange`; its associated multirange type is used for `anymultirange` * `anycompatibletype DEFAULT 'int'` - an actual type to be used when testing the anycompatible type * `anycompatiblerangetype DEFAULT 'int4range'` - an actual range type to be used when testing `anycompatiblerange`; its associated multirange type is used for `anycompatiblemultirange` * `without_warnings DEFAULT false` - disable all warnings (Ignores all xxxx_warning parameters, a quick override) * `all_warnings DEFAULT false` - enable all warnings (Ignores other xxx_warning parameters, a quick positive) * `newtable DEFAULT NULL`, `oldtable DEFAULT NULL` - the names of NEW or OLD transition tables. These parameters are required when transition tables are used in trigger functions. * `use_incomment_options DEFAULT true` - when it is true, then in-comment options are active * `incomment_options_usage_warning DEFAULT false` - when it is true, then the warning is raised when in-comment option is used. * `constant_tracing boolean DEFAULT true` - when it is true, then the variable that holds some constant content, can be used like constant (it is work only in some simple cases, and the content of variable should not be ambiguous). * `pragmas text[] DEFAULT NULL` - list of pragmas that are applied before the check is started. Use scope-independent pragmas here, such as table and sequence definitions or warning switches. Pragmas that need a PL/pgSQL variable namespace (`type` and `assert-schema`, `assert-table`, `assert-column`) must instead be placed inside the function; they are ignored in this argument. The main usage is passing table pragmas generated by `plpgsql_make_pragma` (see the section "Table pragmas generator"). ## Triggers When checking a DML trigger function, supply the relation on which it operates, either through `relid` or an in-comment option. CREATE TABLE bar(a int, b int); postgres=# \sf+ foo_trg CREATE OR REPLACE FUNCTION public.foo_trg() RETURNS trigger LANGUAGE plpgsql 1 AS $function$ 2 BEGIN 3 NEW.c := NEW.a + NEW.b; 4 RETURN NEW; 5 END; 6 $function$ Missing relation specification postgres=# select * from plpgsql_check_function('foo_trg()'); ERROR: missing trigger relation HINT: Trigger relation oid must be valid Correct trigger checking (with specified relation) postgres=# select * from plpgsql_check_function('foo_trg()', 'bar'); plpgsql_check_function -------------------------------------------------------- error:42703:3:assignment:record "new" has no field "c" (1 row) For triggers with transition tables you can set the `oldtable` and `newtable` parameters: create or replace function footab_trig_func() returns trigger as $$ declare x int; begin if false then -- should be ok; select count(*) from newtab into x; -- should fail; select count(*) from newtab where d = 10 into x; end if; return null; end; $$ language plpgsql; select * from plpgsql_check_function('footab_trig_func','footab', newtable := 'newtab'); ## In-comment options plpgsql_check allows persistent setting written in comments. These options are taken from function's source code before checking. The syntax is: @plpgsql_check_options: optname [=] value [, optname [=] value ...] The settings from comment options has top high priority, but generally it can be disabled by option `use_incomment_options` to `false`. Comment overrides are applied before validating the final options. A trigger's required relation can therefore be provided by `@plpgsql_check_options: relid = schema.table`. An ordinary function cannot acquire trigger-only options through a comment. Example: create or replace function fx(anyelement) returns text as $$ begin /* * rewrite default polymorphic type to text * @plpgsql_check_options: anyelementtype = text */ return $1; end; $$ language plpgsql; ## Checking all of your code You can use the plpgsql_check_function for mass checking of functions/procedures and mass checking of triggers. Please, test following queries: -- check all nontrigger plpgsql functions SELECT p.oid, p.proname, plpgsql_check_function(p.oid) FROM pg_catalog.pg_namespace n JOIN pg_catalog.pg_proc p ON pronamespace = n.oid JOIN pg_catalog.pg_language l ON p.prolang = l.oid WHERE l.lanname = 'plpgsql' AND p.prorettype <> 2279; or -- check all trigger plpgsql functions SELECT p.proname, tgrelid::regclass, cf.* FROM pg_proc p JOIN pg_trigger t ON t.tgfoid = p.oid JOIN pg_language l ON p.prolang = l.oid JOIN pg_namespace n ON p.pronamespace = n.oid, LATERAL plpgsql_check_function(p.oid, t.tgrelid, oldtable=>t.tgoldtable, newtable=>t.tgnewtable) cf WHERE n.nspname = 'public' and l.lanname = 'plpgsql'; or -- check all plpgsql functions (functions or trigger functions with defined triggers) SELECT (pcf).functionid::regprocedure, (pcf).lineno, (pcf).statement, (pcf).sqlstate, (pcf).message, (pcf).detail, (pcf).hint, (pcf).level, (pcf)."position", (pcf).query, (pcf).context FROM ( SELECT plpgsql_check_function_tb(pg_proc.oid, COALESCE(pg_trigger.tgrelid, 0), oldtable=>pg_trigger.tgoldtable, newtable=>pg_trigger.tgnewtable) AS pcf FROM pg_proc LEFT JOIN pg_trigger ON (pg_trigger.tgfoid = pg_proc.oid) WHERE prolang = (SELECT lang.oid FROM pg_language lang WHERE lang.lanname = 'plpgsql') AND pronamespace <> (SELECT nsp.oid FROM pg_namespace nsp WHERE nsp.nspname = 'pg_catalog') AND -- ignore unused triggers (pg_proc.prorettype <> (SELECT typ.oid FROM pg_type typ WHERE typ.typname = 'trigger') OR pg_trigger.tgfoid IS NOT NULL) OFFSET 0 ) ss ORDER BY (pcf).functionid::regprocedure::text, (pcf).lineno; # Passive mode (only recommended for development or preproduction) Functions can be checked upon execution - plpgsql_check module must be loaded (via postgresql.conf). ## Configuration Settings plpgsql_check.mode = [ disabled | by_function | fresh_start | every_start ] plpgsql_check.fatal_errors = [ yes | no ] plpgsql_check.show_nonperformance_warnings = false plpgsql_check.show_performance_warnings = false Default mode is by_function, that means that the enhanced check is done only in active mode - by calling the plpgsql_check_function. `fresh_start` means cold start (first the function is called). You can enable passive mode by load 'plpgsql'; -- 1.1 and higher doesn't need it load 'plpgsql_check'; set plpgsql_check.mode = 'every_start'; -- This scans all code before it is executed SELECT fx(10); -- run functions - function is checked before runtime starts it # Compatibility warnings ## Assigning string to refcursor variable PostgreSQL cursor's and refcursor's variables are enhanced string variables that holds unique name of related portal (internal structure of Postgres that is used for cursor's implementation). Until PostgreSQL 16, the portal had same name like name of cursor variable. PostgreSQL 16 and higher change this mechanism and by default related portal will be named by some unique name. It solves some issues with cursors in nested blocks or when cursor is used in recursive called function. With mentioned change, the refcursor's variable should to take value from another refcursor variable or from some cursor variable (when cursor is opened). -- obsolete pattern DECLARE cur CURSOR FOR SELECT 1; rcur refcursor; BEGIN rcur := 'cur'; OPEN cur; ... -- new pattern DECLARE cur CURSOR FOR SELECT 1; rcur refcursor; BEGIN OPEN cur; rcur := cur; ... When `compatibility_warnings` flag is active, then `plpgsql_check` try to identify some fishy assigning to refcursor's variable or returning of refcursor's values: CREATE OR REPLACE FUNCTION public.foo() RETURNS refcursor AS $$ declare c cursor for select 1; r refcursor; begin open c; r := 'c'; return r; end; $$ LANGUAGE plpgsql; select * from plpgsql_check_function('foo', extra_warnings =>false, compatibility_warnings => true); β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ plpgsql_check_function β”‚ β•žβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•‘ β”‚ compatibility:00000:6:assignment:obsolete setting of refcursor or cursor variable β”‚ β”‚ Detail: Internal name of cursor should not be specified by users. β”‚ β”‚ Context: at assignment to variable "r" declared on line 3 β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ (3 rows) # Limits plpgsql_check should find almost all errors on really static code. When developers use PLpgSQL's dynamic features like dynamic SQL or record data type, then false positives are possible. These should be rare - in well written code - and then the affected function should be redesigned or plpgsql_check should be disabled for this function. CREATE OR REPLACE FUNCTION f1() RETURNS void AS $$ DECLARE r record; BEGIN FOR r IN EXECUTE 'SELECT * FROM t1' LOOP RAISE NOTICE '%', r.c; END LOOP; END; $$ LANGUAGE plpgsql SET plpgsql_check.mode TO 'disabled'; A usage of plpgsql_check adds a small overhead (when passive mode is enabled) and you should use that setting only in development or preproduction environments. ## Dynamic SQL The checker can analyze dynamically executed queries when constant tracing or known `format()` arguments determine their text, including field widths and padding. When the query text or a format width is unknown, it cannot reliably infer the result's record type or check expressions depending on its fields. When type of record's variable is not know, you can assign it explicitly with pragma `type`: DECLARE r record; BEGIN EXECUTE format('SELECT * FROM %I', _tablename) INTO r; PERFORM plpgsql_check_pragma('type: r (id int, processed bool)'); IF NOT r.processed THEN ... Attention: The SQL injection check can detect only some SQL injection vulnerabilities. This tool cannot be used for security audit! Some issues will not be detected. This check can raise false alarms too - probably when variable is sanitized by other command or when the value is of some composite type. ## Refcursors plpgsql_check cannot be used to detect structure of referenced cursors. A reference on cursor in PLpgSQL is implemented as name of global cursor. In check time, the name is not known (not in all possibilities), and global cursor doesn't exist. It is a significant issue for any static analysis. If the row structure is known, declare the target using a named composite type or `table_name%ROWTYPE`. Alternatively, describe a record target with a pragma: CREATE OR REPLACE FUNCTION foo(refcur_var refcursor) RETURNS void AS $$ DECLARE rec_var record; BEGIN FETCH refcur_var INTO rec_var; PERFORM plpgsql_check_pragma('type: rec_var (id integer, value text)'); RAISE NOTICE '%', rec_var.id; END; $$ LANGUAGE plpgsql; The pragma describes the expected row structure for checking; it does not change the cursor or its result type at runtime. ## Temporary tables Queries over temporary tables created at runtime need a check-time table definition. Use a table pragma (written manually or generated by `plpgsql_make_pragma`), create the temporary table before checking, or provide a matching permanent template. Disabling checking for the function is a fallback. Temporary tables are stored in a per-session schema, normally searched before schemas containing persistent tables. This permits the following template-table technique: CREATE OR REPLACE FUNCTION public.disable_dml() RETURNS trigger LANGUAGE plpgsql AS $function$ BEGIN RAISE EXCEPTION SQLSTATE '42P01' USING message = format('this instance of %I table doesn''t allow any DML operation', TG_TABLE_NAME), hint = format('you should use "CREATE TEMP TABLE %1$I(LIKE %1$I INCLUDING ALL);" statement', TG_TABLE_NAME); RETURN NULL; END; $function$; CREATE TABLE foo(a int, b int); -- doesn't hold data, ever CREATE TRIGGER foo_disable_dml BEFORE INSERT OR UPDATE OR DELETE ON foo EXECUTE PROCEDURE disable_dml(); postgres=# INSERT INTO foo VALUES(10,20); ERROR: this instance of foo table doesn't allow any DML operation HINT: you should to run "CREATE TEMP TABLE foo(LIKE foo INCLUDING ALL);" statement postgres=# CREATE TABLE postgres=# INSERT INTO foo VALUES(10,20); INSERT 0 1 This trick emulates GLOBAL TEMP tables partially and it allows a statical validation. Other possibility is using a [template foreign data wrapper] (https://github.com/okbob/template_fdw) You can use pragma `table` and create ephemeral table: BEGIN CREATE TEMP TABLE xxx(a int); PERFORM plpgsql_check_pragma('table: xxx(a int)'); INSERT INTO xxx VALUES(10); PERFORM plpgsql_check_pragma('table: pg_temp.zzz(like schemaname.table1)'); ... For temporary tables created inside the function's body, the table pragmas can be generated automatically by the function `plpgsql_make_pragma` (see the section "Table pragmas generator"). # Dependency list A function plpgsql_show_dependency_tb will show all functions, operators and relations used inside processed function: postgres=# select * from plpgsql_show_dependency_tb('testfunc(int,float)'); β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ type β”‚ oid β”‚ schema β”‚ name β”‚ params β”‚ β•žβ•β•β•β•β•β•β•β•β•β•β•ͺ═══════β•ͺ════════β•ͺ═════════β•ͺ════════════════════════════║ β”‚ FUNCTION β”‚ 36008 β”‚ public β”‚ myfunc1 β”‚ (integer,double precision) β”‚ β”‚ FUNCTION β”‚ 35999 β”‚ public β”‚ myfunc2 β”‚ (integer,double precision) β”‚ β”‚ OPERATOR β”‚ 36007 β”‚ public β”‚ ** β”‚ (integer,integer) β”‚ β”‚ RELATION β”‚ 36005 β”‚ public β”‚ myview β”‚ β”‚ β”‚ RELATION β”‚ 36002 β”‚ public β”‚ mytable β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ (4 rows) Optional arguments of plpgsql_show_dependency_tb are `relid`, `anyelememttype`, `anyenumtype`, `anyrangetype`, `anycompatibletype` and `anycompatiblerangetype`. # Profiler plpgsql_check includes a profiler for PL/pgSQL functions and procedures. It can store statistics either in session-local memory or in shared memory. Shared storage requires loading the library at server startup, for example: shared_preload_libraries = 'plpgsql_check' It is not necessary to list `plpgsql` before `plpgsql_check`. Without shared preloading, profiling works in the current session only. Load the extension before executing the PL/pgSQL routines you want to profile or trace. Calling `SELECT plpgsql_check_profiler(true)` loads the installed library and enables profiling without requiring a privileged `LOAD` command. Profiling is active while `plpgsql_check.profiler` is `on`; calling `plpgsql_check_profiler(false)` disables it. When shared storage is available, `plpgsql_check.use_shared_stats_when_it_possible` selects it by default (`on`). Set this option to `off` to use local statistics. Exhausting shared storage does not automatically switch to local storage. Shared per-statement statistics are buffered in a transaction-local cache by default: `plpgsql_check.use_lxcache` is `on`, and updates are merged at transaction end. This reduces lock contention when many sessions execute short functions. Set it to `off` to merge these statistics immediately after each function execution. `plpgsql_check.max_stats_size` limits storage for per-statement statistics, not all profiler overhead. Its default is 20 MB, minimum 64 kB, and maximum 200 MB. When capacity is exhausted, new statement profiles are skipped with a warning. `plpgsql_profiler_reset_all()` makes the allocated capacity reusable. Shared storage is allocated at server startup, so changing its size requires a restart. The local-storage limit can be changed without restarting the server. The profiler can also retrieve query identifiers from cached plans for expressions and static SQL statements. PostgreSQL's core query-ID computation can provide them when an administrator enables `compute_query_id = on`; `pg_stat_statements` is not required. An extension that enables or supplies query IDs can also be used. There are some limitations to the query identifier retrieval: * if a plpgsql expression contains underlying statements, only the top level query identifier will be retrieved * the profiler does not compute query identifiers itself. Availability depends on PostgreSQL or the query-ID provider; some statements, including DDL, may not have an identifier. * a query identifier is retrieved only for instructions containing expressions. This means that plpgsql_profiler_function_tb() function can report less query identifier than instructions on a single line. * query_id of dynamically executed queries are reported only when `plpgsql_check.profiler_show_dynquery_query_id` is on (default is off). Attention: in this case, the expression that produce query string is executed second by profiler. Attention: An update of shared profiles can decrease performance on servers under higher load. The profile can be displayed by function `plpgsql_profiler_function_tb`: postgres=# select lineno, avg_time, source from plpgsql_profiler_function_tb('fx(int)'); β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ lineno β”‚ avg_time β”‚ source β”‚ β•žβ•β•β•β•β•β•β•β•β•ͺ══════════β•ͺ═══════════════════════════════════════════════════════════════════║ β”‚ 1 β”‚ β”‚ β”‚ β”‚ 2 β”‚ β”‚ declare result int = 0; β”‚ β”‚ 3 β”‚ 0.075 β”‚ begin β”‚ β”‚ 4 β”‚ 0.202 β”‚ for i in 1..$1 loop β”‚ β”‚ 5 β”‚ 0.005 β”‚ select result + i into result; select result + i into result; β”‚ β”‚ 6 β”‚ β”‚ end loop; β”‚ β”‚ 7 β”‚ 0 β”‚ return result; β”‚ β”‚ 8 β”‚ β”‚ end; β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ (9 rows) The times in the result are in milliseconds. The profile per statements (not per line) can be displayed by function plpgsql_profiler_function_statements_tb: CREATE OR REPLACE FUNCTION public.fx1(a integer) RETURNS integer LANGUAGE plpgsql 1 AS $function$ 2 begin 3 if a > 10 then 4 raise notice 'ahoj'; 5 return -1; 6 else 7 raise notice 'nazdar'; 8 return 1; 9 end if; 10 end; 11 $function$ postgres=# select stmtid, parent_stmtid, block_num, lineno, exec_stmts, stmtname from plpgsql_profiler_function_statements_tb('fx1'); stmtid | parent_stmtid | block_num | lineno | exec_stmts | stmtname --------+---------------+-----------+--------+------------+----------------- 1 | | 1 | 2 | | statement block 2 | 1 | 1 | 3 | | IF 3 | 2 | 1 | 4 | | RAISE 4 | 2 | 2 | 5 | | RETURN 5 | 2 | 3 | 7 | | RAISE 6 | 2 | 4 | 8 | | RETURN (6 rows) Statement IDs start at 1. `block_num` is the statement's ordinal within its parent, not a branch label. This example has no collected profile yet, so execution counts are NULL (displayed as blanks). All stored profiles can be displayed by calling function `plpgsql_profiler_functions_all`: postgres=# select funcoid, exec_count, total_time, avg_time, stddev_time, min_time, max_time from plpgsql_profiler_functions_all(); β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ funcoid β”‚ exec_count β”‚ total_time β”‚ avg_time β”‚ stddev_time β”‚ min_time β”‚ max_time β”‚ β•žβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•ͺ════════════β•ͺ════════════β•ͺ══════════β•ͺ═════════════β•ͺ══════════β•ͺ══════════║ β”‚ fxx(double precision) β”‚ 1 β”‚ 0.01 β”‚ 0.01 β”‚ 0.00 β”‚ 0.01 β”‚ 0.01 β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ (1 row) `SELECT *` also includes the `exec_stmts_err` column. There are two functions for cleaning stored profiles: `plpgsql_profiler_reset_all()` and `plpgsql_profiler_reset(regprocedure)`. ## Coverage metrics plpgsql_check provides two functions: * `plpgsql_coverage_statements(name)` * `plpgsql_coverage_branches(name)` The coverage data are collected only when profiling is active. An exception while evaluating an `IF` or `ELSIF` condition does not cover a branch, including an implicit `ELSE`. An exception after entering a branch does count as reaching that branch. ## Note There is another very good PLpgSQL profiler - https://github.com/glynastill/plprofiler My extension is designed to be simple for use and practical. Nothing more or less. plprofiler is more complex. It builds call graphs and from this graph it can create flame graph of execution times. Both extensions can be used together with the builtin PostgreSQL's feature - tracking functions. set track_functions to 'pl'; ... select * from pg_stat_user_functions; # Tracer plpgsql_check provides a tracing possibility - in this mode you can see notices on start or end functions (terse and default verbosity) and start or end statements (verbose verbosity). For default and verbose verbosity the content of function arguments is displayed. The content of related variables are displayed when verbosity is verbose. postgres=# do $$ begin perform fx(10,null, 'now', e'stΔ›hule'); end; $$; NOTICE: #0 ->> start of inline_code_block (Oid=0) NOTICE: #2 ->> start of function fx(integer,integer,date,text) (Oid=16405) NOTICE: #2 call by inline_code_block line 1 at PERFORM NOTICE: #2 "a" => '10', "b" => null, "c" => '2020-08-03', "d" => 'stΔ›hule' NOTICE: #4 ->> start of function fx(integer) (Oid=16404) NOTICE: #4 call by fx(integer,integer,date,text) line 1 at PERFORM NOTICE: #4 "a" => '10' NOTICE: #4 <<- end of function fx (elapsed time=0.098 ms) NOTICE: #2 <<- end of function fx (elapsed time=0.399 ms) NOTICE: #0 <<- end of block (elapsed time=0.754 ms) The number after `#` is a execution frame counter (this number is related to depth of error context stack). It allows to pair start and end of function. Attention - the initial depth of error context stack can be different in dependency on environment (and used protocol). Tracing is enabled by setting `plpgsql_check.tracer` to `on`. Attention - enabling this behaviour has significant negative impact on performance (unlike the profiler). You can set a level for output used by tracer `plpgsql_check.tracer_errlevel` (default is `notice`). The output content is limited by length specified by `plpgsql_check.tracer_variable_max_length` configuration variable. The tracer can be activated by calling function `plpgsql_check_tracer(true)` and disabled by calling same function with `false` argument (or with literals `on`, `off`). First, the usage of tracer should be explicitly enabled by superuser by setting `set plpgsql_check.enable_tracer to on;` or `plpgsql_check.enable_tracer to on` in `postgresql.conf`. This is a security safeguard. The tracer shows content of plpgsql's variables, and then some security sensitive information can be displayed to an unprivileged user (when he runs security definer function). Second, the extension `plpgsql_check` should be loaded. It can be done by execution of some `plpgsql_check` function or explicitly by command `load 'plpgsql_check';`. You can use configuration's option `shared_preload_libraries`, `local_preload_libraries` or `session_preload_libraries`. In terse verbose mode the output is reduced: postgres=# set plpgsql_check.tracer_verbosity TO terse; SET postgres=# do $$ begin perform fx(10,null, 'now', e'stΔ›hule'); end; $$; NOTICE: #0 start of inline code block (oid=0) NOTICE: #2 start of fx (oid=16405) NOTICE: #4 start of fx (oid=16404) NOTICE: #4 end of fx NOTICE: #2 end of fx NOTICE: #0 end of inline code block In verbose mode the output is extended about statement details: postgres=# do $$ begin perform fx(10,null, 'now', e'stΔ›hule'); end; $$; NOTICE: #0 ->> start of block inline_code_block (oid=0) NOTICE: #0.1 1 --> start of PERFORM NOTICE: #2 ->> start of function fx(integer,integer,date,text) (oid=16405) NOTICE: #2 call by inline_code_block line 1 at PERFORM NOTICE: #2 "a" => '10', "b" => null, "c" => '2020-08-04', "d" => 'stΔ›hule' NOTICE: #2.1 1 --> start of PERFORM NOTICE: #2.1 "a" => '10' NOTICE: #4 ->> start of function fx(integer) (oid=16404) NOTICE: #4 call by fx(integer,integer,date,text) line 1 at PERFORM NOTICE: #4 "a" => '10' NOTICE: #4.1 6 --> start of assignment NOTICE: #4.1 "a" => '10', "b" => '20' NOTICE: #4.1 <-- end of assignment (elapsed time=0.076 ms) NOTICE: #4.1 "res" => '130' NOTICE: #4.2 7 --> start of RETURN NOTICE: #4.2 "res" => '130' NOTICE: #4.2 <-- end of RETURN (elapsed time=0.054 ms) NOTICE: #4 <<- end of function fx (elapsed time=0.373 ms) NOTICE: #2.1 <-- end of PERFORM (elapsed time=0.589 ms) NOTICE: #2 <<- end of function fx (elapsed time=0.727 ms) NOTICE: #0.1 <-- end of PERFORM (elapsed time=1.147 ms) NOTICE: #0 <<- end of block (elapsed time=1.286 ms) A special feature of the tracer is tracing `ASSERT` statements when `plpgsql_check.trace_assert` is `on`. A false assertion prints the current routine's variables. `plpgsql_check.trace_assert_verbosity = DEFAULT` also prints outer PL/pgSQL frame locations; `VERBOSE` additionally prints those frames' variables. This works independently of `plpgsql.check_asserts`, including when runtime assertions are disabled. postgres=# set plpgsql_check.tracer to off; postgres=# set plpgsql_check.trace_assert_verbosity TO verbose; postgres=# do $$ begin perform fx(10,null, 'now', e'stΔ›hule'); end; $$; NOTICE: #4 PLpgSQL assert expression (false) on line 12 of fx(integer) is false NOTICE: "a" => '10', "res" => null, "b" => '20' NOTICE: #2 PL/pgSQL function fx(integer,integer,date,text) line 1 at PERFORM NOTICE: "a" => '10', "b" => null, "c" => '2020-08-05', "d" => 'stΔ›hule' NOTICE: #0 PL/pgSQL function inline_code_block line 1 at PERFORM ERROR: assertion failed CONTEXT: PL/pgSQL function fx(integer) line 12 at ASSERT SQL statement "SELECT fx(a)" PL/pgSQL function fx(integer,integer,date,text) line 1 at PERFORM SQL statement "SELECT fx(10,null, 'now', e'stΔ›hule')" PL/pgSQL function inline_code_block line 1 at PERFORM postgres=# set plpgsql.check_asserts to off; SET postgres=# do $$ begin perform fx(10,null, 'now', e'stΔ›hule'); end; $$; NOTICE: #4 PLpgSQL assert expression (false) on line 12 of fx(integer) is false NOTICE: "a" => '10', "res" => null, "b" => '20' NOTICE: #2 PL/pgSQL function fx(integer,integer,date,text) line 1 at PERFORM NOTICE: "a" => '10', "b" => null, "c" => '2020-08-05', "d" => 'stΔ›hule' NOTICE: #0 PL/pgSQL function inline_code_block line 1 at PERFORM DO Attention: When plpgsql assertions is enabled, then assert expression is evaluated 2x times - first by plpgsql_check's tracer, second by plpgsql engine. Tracer can show usage of subtransaction buffer id (`nxids`). The displayed `tnl` number is transaction nesting level number (for plpgsql it depends on deep of blocks with exception's handlers). ## Detection of unclosed cursors PLpgSQL's cursors are just names of SQL cursors. The life cycle of SQL cursors is not joined with scope of related plpgsql's cursor variable. SQL cursors are closed by self at transaction end, but for long transaction and too much opened cursors it can be too late. It is better to close cursor explicitly when cursor is not necessary (by CLOSE statement). Without it the significant memory issues are possible. When OPEN statement try to use cursor that is not closed yet, the warning is raised. This feature can be disabled by setting `plpgsql_check.cursors_leaks to off`. This check is not active, when routine is called recursively. The unclosed cursors can be checked immediately when function is finished. This check is disabled by default, and should be enabled by `plpgsql_check.strict_cursors_leaks to on`. Any unclosed cursor is reported once. ## Using with plugin_debugger If you use `plugin_debugger` (plpgsql debugger) together with `plpgsql_check`, then `plpgsql_check` should be initialized after `plugin_debugger` (because `plugin_debugger` doesn't support the sharing of PL/pgSQL's debug API). For example (`postgresql.conf`): shared_preload_libraries = 'plugin_debugger,plpgsql,plpgsql_check' ## Attention - SECURITY Tracer prints content of variables or function arguments. For security definer function, this content can hold security sensitive data. This is reason why tracer is disabled by default and should be enabled only with super user rights `plpgsql_check.enable_tracer`. # Pragma You can configure plpgsql_check behaviour inside a checked function with "pragma" function. This is a analogy of PL/SQL or ADA language of PRAGMA feature. PLpgSQL doesn't support PRAGMA, but plpgsql_check detects function named `plpgsql_check_pragma` and takes options from the parameters of this function. These plpgsql_check options are valid to the end of this group of statements. CREATE OR REPLACE FUNCTION test() RETURNS void AS $$ BEGIN ... -- for following statements disable check PERFORM plpgsql_check_pragma('disable:check'); ... -- enable check again PERFORM plpgsql_check_pragma('enable:check'); ... END; $$ LANGUAGE plpgsql; The extension's `plpgsql_check_pragma` function is `VOLATILE` and returns integer 1. Its tracer directives can change runtime tracing state; other directives are interpreted by the checker. In a database without the extension, an application can provide a no-op compatibility stub: CREATE FUNCTION plpgsql_check_pragma(VARIADIC name text[]) RETURNS int AS $$ SELECT 1 $$ LANGUAGE sql IMMUTABLE; This stub ignores every directive, including tracer controls. Do not replace the extension-owned implementation with it. Using pragma function in declaration part of top block sets options on function level too. CREATE OR REPLACE FUNCTION test() RETURNS void AS $$ DECLARE aux int := plpgsql_check_pragma('disable:extra_warnings'); ... Shorter syntax for pragma is supported too: CREATE OR REPLACE FUNCTION test() RETURNS void AS $$ DECLARE r record; BEGIN PERFORM 'PRAGMA:TYPE:r (a int, b int)'; PERFORM 'PRAGMA:TABLE: x (like pg_class)'; ... ## Supported pragmas * `echo:str` - print string (for testing). Inside string, there can be used "variables": @@id, @@name, @@signature * `status:check`,`status:tracer`, `status:other_warnings`, `status:performance_warnings`, `status:extra_warnings`,`status:security_warnings`, `status:compatibility_warnings`, `status:constants_tracing` This outputs the current value (e.g. other_warnings enabled) * `enable:check`,`enable:tracer`, `enable:other_warnings`, `enable:performance_warnings`, `enable:extra_warnings`,`enable:security_warnings`, `enable:compatibility_warnings`, `enable:constants_tracing` * `disable:check`,`disable:tracer`, `disable:other_warnings`, `disable:performance_warnings`, `disable:extra_warnings`,`disable:security_warnings`, `disable:compatibility_warnings`, `disable:constants_tracing` This can be used to disable the Hint in returning from an anyelement function. Just put the pragma before the RETURN statement. * `type:varname typename` or `type:varname (fieldname type, ...)` - set type to variable of record type * `table: name (column_name type, ...)` or `table: name (like tablename)` - create ephemeral temporary table (if you want to specify schema, then only `pg_temp` schema is allowed). The column list supports only a column name and a type (with optional type modifiers and array dimensions) - column constraints (`PRIMARY KEY`, `NOT NULL`, `DEFAULT`, `CHECK`) and `COLLATE` clauses are not supported here. * `sequence: name` - create ephemeral temporary sequence * `assert-schema: varname` - check-time assertion - ensure so schema specified by variable is valid * `assert-table: [ varname_schema, ] , varname` - ensure so table name specified by variables (by constant tracing) is valid * `assert-column: [varname_schema, ], varname_table , varname` - ensure so column specified by variables is valid The historical spelling `status:constants_trancing` remains an alias for `status:constants_tracing`; prefer the correctly spelled form. # Table pragmas generator plpgsql_check cannot verify queries over temporary tables that are created at runtime. The pragma `table` solves this issue, but the column list must be written (and maintained) manually there - it gets out of sync easily when the definition is changed. These pragmas can be generated automatically by the function `plpgsql_make_pragma`. It scans the function's body, and for every statement there that creates a temporary table it returns one table pragma string. For `CREATE TEMP TABLE ... AS SELECT|VALUES|TABLE` statements the names and types of columns are derived from planning of the inner query (the query is never executed). For `CREATE TEMP TABLE` statements (the column definition list, the `LIKE` clause, the `OF type_name` clause, inheritance and partitioning) the statement is executed inside an always rolled back subtransaction, and the pragma is derived from the structure of the really created table - so serial or identity columns, inherited or LIKE-copied columns are expanded by PostgreSQL itself. Column constraints (`PRIMARY KEY`, `NOT NULL`, `DEFAULT`, `CHECK`) don't block the generation, but they are not carried into the generated pragma - the pragma holds only column names and types, which is enough for the static checks. In both cases no object survives the call, and repeated calls return the same result. Zero-column tables produce an empty column list, such as `table: target()`, which is accepted by the table pragma parser. create table gtp_src(a int, b text); create or replace function gtp_f1() returns void as $$ begin create temp table gtp_t1 as select a, b from gtp_src; insert into gtp_t1 values (10, 'hello'); end; $$ language plpgsql; Without the pragma the check fails on the missing temporary table: postgres=# select * from plpgsql_check_function('gtp_f1()'); plpgsql_check_function -------------------------------------------------------------- error:42P01:4:SQL statement:relation "gtp_t1" does not exist Query: insert into gtp_t1 values (10, 'hello') -- ^ (3 rows) postgres=# select * from plpgsql_make_pragma('gtp_f1()'); plpgsql_make_pragma -------------------------------------- table: gtp_t1(a integer, b text) (1 row) Generated pragmas (the returned text can be manually edited when it is necessary) can be passed to the option `pragmas` of the functions `plpgsql_check_function` and `plpgsql_check_function_tb`. These pragmas are applied before the check is started: postgres=# select * from plpgsql_check_function('gtp_f1()', pragmas => array(select plpgsql_make_pragma('gtp_f1()'))); plpgsql_check_function ------------------------ (0 rows) The inner query is not limited to a simple `FROM` clause - joins, subqueries, CTE and recursive CTE queries are supported: create or replace function gtp_f7() returns void as $$ begin create temp table gtp_rcte as with recursive r(n) as (values (1) union all select n + 1 from r where n < 10) select n from r; end; $$ language plpgsql; postgres=# select * from plpgsql_make_pragma('gtp_f7()'); plpgsql_make_pragma -------------------------------------- table: gtp_rcte(n integer) (1 row) More temporary tables can be processed. Every detected temporary table is registered immediately, so a temporary table can reference other temporary tables created earlier in the same function (in a simple `FROM` clause, in a join or in a subquery): create or replace function gtp_f15() returns void as $$ begin create temp table gtp_c1 as select a, b from gtp_src; create temp table gtp_c2 as select * from gtp_c1 where a > 0; create temp table gtp_c3 as select gtp_c1.a, gtp_c2.b from gtp_c1 join gtp_c2 on gtp_c1.a = gtp_c2.a; create temp table gtp_c4 as select a from gtp_src where a in (select a from gtp_c1); end; $$ language plpgsql; postgres=# select * from plpgsql_make_pragma('gtp_f15()'); plpgsql_make_pragma -------------------------------------- table: gtp_c1(a integer, b text) table: gtp_c2(a integer, b text) table: gtp_c3(a integer, b text) table: gtp_c4(a integer) (4 rows) The statements are processed in the order of appearance in the function's body (nested blocks, loops, IF branches and exception handlers are scanned too), and the pragmas are returned in the same order without deduplication. Explicitly written table pragmas inside the function's body are respected, so a temporary table created by dynamic SQL can be declared manually, and the following statements can reference it. The `CREATE TEMP TABLE` statement based forms are supported in all variants - with storage parameters, access method, `ON COMMIT` or `IF NOT EXISTS` clauses, or with the `pg_temp` schema qualification used without the `TEMP` keyword: create or replace function gtp_f20() returns void as $$ begin create temp table gtp_x (id serial, v varchar(10)); create temp table gtp_y (like gtp_x including all); insert into gtp_y(v) values ('hello'); end; $$ language plpgsql; postgres=# select * from plpgsql_make_pragma('gtp_f20()'); plpgsql_make_pragma --------------------------------------------------- table: gtp_x(id integer, v character varying(10)) table: gtp_y(id integer, v character varying(10)) (2 rows) Only statically written commands are processed - dynamic SQL (`EXECUTE`), not temporary tables and materialized views are ignored. A name collision (usually the pattern `CREATE`, `DROP`, `CREATE` with the same name - DROP statements are not executed by static scanning) is not an error. A warning is raised, pragmas are returned for both definitions, and the following statements of the function's body see the last definition like in runtime. `CREATE TEMP TABLE IF NOT EXISTS` over an existing table returns the structure of the existing table (the existing table wins like in runtime). The function raises an error when: * the query references a missing (and not temporary) relation - like `plpgsql_check_function`, the error (sqlstate `42P01`) is raised on the first failed statement, * a temporary table should be created in a non-temporary schema (e.g. `create temporary table public.t1 as select 1`) - the error `cannot create temporary relation in non-temporary schema` (sqlstate `42P16`) is raised. This check is done for all forms of temporary tables (for the column definition list, `LIKE` and `OF type_name` forms too), * more column names are explicitly specified than the query returns - the error `too many column names were specified`, * the execution of a `CREATE TEMP TABLE` statement fails - e.g. a temporary partition of a persistent table, a foreign key from a temporary table to a persistent table, or the `LIKE` clause over a missing table. The error is the same like in runtime. When the option `fatal_errors` is false, then failed statements are skipped (with a warning), and the scanning continues. Optional arguments of `plpgsql_make_pragma` are `relid` (like for `plpgsql_check_function`, it is necessary for checking of trigger functions) and `fatal_errors`. # Update This distribution ships the SQL installation script for extension version `2.10`, but no `ALTER EXTENSION UPDATE` migration scripts. Replacing the library with a compatible release using the same SQL extension version does not itself require dropping the extension. If the SQL extension version changes, plan its recreation and review dependent objects rather than blindly using `DROP ... CASCADE`. After replacing a loaded library, reconnect sessions using it. If it is in `shared_preload_libraries`, restart the postmaster before using the new library. # Compilation Use a C build toolchain and the server development headers/PGXS files for the target PostgreSQL installation. Select that installation consistently with `PG_CONFIG`: make PG_CONFIG=/path/to/postgresql/bin/pg_config make PG_CONFIG=/path/to/postgresql/bin/pg_config install The install step needs filesystem permissions for PostgreSQL's extension directories. Then run the regression suite against a running, disposable server: PGHOST=/path/to/socket PGPORT=5432 PGUSER=test_admin \ make PG_CONFIG=/path/to/postgresql/bin/pg_config installcheck The regression harness creates its test database and uses administrative SQL. Do not point it at a production server. If the selected PostgreSQL installation expects LLVM but its Clang toolchain is unavailable, add `with_llvm=no` to each `make` invocation to omit LLVM bitcode. Reconnect or restart as described under "Update" after installing a replacement library. `installcheck` does not restart the target server; test separately with and without `shared_preload_libraries = 'plpgsql_check'`. ## Compilation for PostgresPro `plpgsql_check` requires some unpublished patches to be successfully compiled and used with PostgresPro. Use `plpgsql_check` from PostgresPro repository. ## Compilation on Ubuntu Depending on how PostgreSQL was built, compilation may also require `libicu-dev` when that installation uses ICU: sudo apt install libicu-dev ## Compilation plpgsql_check on Windows You can check precompiled dll libraries http://okbob.blogspot.cz/2015/02/plpgsqlcheck-is-available-for-microsoft.html, http://okbob.blogspot.com/2023/10/compiled-dll-of-plpgsqlcheck-254-and.html or compile by self: 1. Install a supported PostgreSQL version for Windows from https://www.enterprisedb.com 2. Install a compatible Microsoft Visual C++ build toolchain. 3. Read tutorial http://blog.2ndquadrant.com/compiling-postgresql-extensions-visual-studio-windows 4. Build plpgsql_check.dll 5. Copy `plpgsql_check.dll` to `PostgreSQL\\lib`. 6. Copy `plpgsql_check.control` and `plpgsql_check--2.10.sql` to `PostgreSQL\\share\extension`. The DLL must match the PostgreSQL major version and architecture; use matching extension library and SQL files. ## Meson build 1. `meson setup build` 2. `cd build` 3. `ninja` 4. `ninja install` 5. optionally `ninja bindist` ## Testing prerequisites Use a supported PostgreSQL version (14 - 20). The `plpgsql_check_tablefunc` regression test exercises XML output with `xpath`, so PostgreSQL must be built with libxml support. The standalone ordinary-user review reproducers are documented separately in [reproducers/README.md](reproducers/README.md). # Licence Copyright (c) Pavel Stehule (pavel.stehule@gmail.com) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. # Note If you like it, send a postcard to address Pavel Stehule Skalice 12 256 01 Benesov u Prahy Czech Republic I invite any questions, comments, bug reports, patches on mail address pavel.stehule@gmail.com okbob-plpgsql_check-800db79/README.meson_msvc000066400000000000000000000014551526744060500210300ustar00rootroot00000000000000Build on Microsoft Windows with Microsoft Visual Studio and meson 1. Install - Microsft Visual Studio with C and C++ support - Meson - PostgreSQL 2. Run x64 Native Tools Command Prompt for VS 20xx (*** ensure x64 ***) 3. Set PATH set PATH=%PATH%;C:\Program Files\PostgreSQL\16\bin;C:\Program Files\Meson 4. execute in plpgsql_check source directory (buildtype should be release or plain, attention - the release buildtype inject dependency on vc_redist.x64.exe) meson setup --wipe --buildtype release build cd build ninja sudo ninja install (on linux) ninja test (on linux) # ninja bindist 5. tests on MSWIN requires installation DiffUtils from gnuwin32 set PATH=%PATH%;C:\Program Files (x86)\GnuWin32\bin set PGPORT= set PGUSER= set PGPASSWORD= ninja test okbob-plpgsql_check-800db79/SECURITY.md000066400000000000000000000000231526744060500175570ustar00rootroot00000000000000# Security Policy okbob-plpgsql_check-800db79/TODO.md000066400000000000000000000003611526744060500170620ustar00rootroot00000000000000- possibility to export profiles in format for pprof https://github.com/google/pprof - possibility to export call stack (short [parent, current], full [entry point, current]) - possibility to show critical path (function or statement level) okbob-plpgsql_check-800db79/_config.yml000066400000000000000000000000321526744060500201150ustar00rootroot00000000000000theme: jekyll-theme-caymanokbob-plpgsql_check-800db79/examples/000077500000000000000000000000001526744060500176115ustar00rootroot00000000000000okbob-plpgsql_check-800db79/examples/custom_scan_function.md000066400000000000000000000057721526744060500243710ustar00rootroot00000000000000custom_scan_function ==================== This creates a custom function, as an example, that changes the output to better fit my needs inside of my IDE (DataGrip) # Usage Through a macro that parses the current function, I call this with ONE routine: * `select * from plpgsql_check_custom('plpgsql_check_custom'::regproc);` Without that parameter, it runs for all routines! There are more parameters to easily control the intensity, or run for a set of schemas or filter messages! This is a starting point... Make it your own. # Why the timestamp FWIW, I return the timestamp ts with the message because I have 10+ windows open, each with it's own code+output. And after fixing a bunch of stuff in another window, that timestamp ALSO tells me how "dated" the scan is. It also confirms it refreshed (sometimes it runs so fast, and gives you the same output, you are not sure if it actually refreshed!). The great part is that once you have a RECORD() type setup for output, adding more columns is easy. # Why the procedure name a row and not a column Honestly, we have horribly long names: long_schema_name.Really_Long_package_name.Really_long_function_name()! While they are clear and make coding easier, it is quickly a waste of screen real-estate. I would rather have *one* long column in my output. It's a personal preference. And that is the beauty of PG and of this tool. # Motivation Finally, for output, the custom message that made me do this is given as an example below. The message from the base level was NOT enough. It only tells you what the code is trying to do. It does not make it clear WHICH parameter is likely wrong. So through a bit of introspection (Thanks to Pavel), I was able to add the full parameter details (including DEF it that parameter has a DEF value). As well as the expected TYPE... # Output
`
#### 
#### Error in: schema.pkg_name$update_user() at Line: 16       PARAMETER TYPING ISSUE?
#### 
#### Param Name           Flow/DEF  (your code)    Definition     
#### ==========           ========  ===========    ==========     
#### eid                  IN        bigint         bigint         
#### pid                  IN        bigint         bigint         
#### typ                  IN        character varyibigint         
#### val                  IN        bigint         text           
#### addrmv               IN        bigint         integer        
#### 
`
# Future Ideas Now that this actually exists, I have a few more ideas. I will actually see how to integrate this better with my DataGrip. I would love to make this work in psql, as a setting: `\set PLPGSQL_CHECK_ALTERED_ROUTINES ON` So, whenever I successfully compile (create or replace) a routine... Then this would run and output the issues it finds! There are a couple of additional things I would like to do. Some error messages give a line number, but it does not match up. I would love to do some code introspection, and extract the LINE in question (or better yet, the field in the case of FETCH INTO ... mismatch) okbob-plpgsql_check-800db79/examples/custom_scan_function.sql000066400000000000000000000205321526744060500245570ustar00rootroot00000000000000CREATE FUNCTION plpgsql_check_custom(funcoid oid DEFAULT NULL::oid, warns boolean DEFAULT false, stop_fatal boolean DEFAULT true, msg_like text DEFAULT '%'::text, proname_regex text DEFAULT '^(.*)$'::text, schema_regex text DEFAULT '^(.*)$'::text) RETURNS TABLE(ts character, check_msg text) LANGUAGE plpgsql AS $$ DECLARE -- This cursor drives the process (plpgsql_check_function()) does all the work! -- The filters are simple enough to filter down the messages, or the procedure name, and to control the INTENSITY of the LINTING -- You get the source... Make it your own! I wanted something I could use flexibly msgs CURSOR (func oid, warnings boolean , fatals boolean) FOR SELECT * FROM (SELECT p.oid, p.prokind, n.nspname || '.' || p.proname || '()' AS proname, public.plpgsql_check_function( funcoid => p.oid::regproc , fatal_errors := fatals , extra_warnings := warnings , performance_warnings := warnings /* set these 3 to false for initial pass */ , all_warnings := warnings)::text AS err FROM pg_catalog.pg_namespace n JOIN pg_catalog.pg_proc p ON pronamespace = n.oid JOIN pg_catalog.pg_language l ON p.prolang = l.oid WHERE l.lanname = 'plpgsql' AND p.prorettype <> 2279 /* not a trigger */ AND n.nspname <> 'public' AND p.prokind IN ('p', 'f') -- Only function and procedures AND p.oid = COALESCE(func, p.oid) AND p.proname OPERATOR (pg_catalog.~) proname_regex AND n.nspname OPERATOR (pg_catalog.~) schema_regex) q1 WHERE q1.err LIKE msg_like; thisproc text := ''; -- Used so we only waste ONE line outputting what function we are working on, as opposed to a COLUMN errmsg text; -- The error message: "error:42883:42:assignment:function schem.function(integer, unknown, unknown, unknown, unknown, unknown, unknown) does not exist" a_txt text[]; -- Used to pars errmsg fdecl text; -- Declaration after parsing fname text; -- Before the parens foid oid; -- Function OID to lookup the named parameters parm1 text; -- between the parens pos INT; -- Simple position of ( for parsing a_p1 text[]; -- Array of Params from the users code has_in boolean; -- is IN/OUT present in any parameters names text; -- Function Signature with Parameter Names a_name text[]; -- string_to_aarray( names, ', ' ) -- [IN/OUT/INOUT] FLDNAME type [DEFAULT ...] a_pname text[]; -- Name ONLY of the field name n_off INT; -- Offset into the array for stuff str_out text; -- Messages to send out, with Chr(10) separating them! flow_def text; -- Should we default to IN all the time for flow flow text; -- IN/INOUT/OUT + DEF BEGIN ts := TO_CHAR(NOW(), 'HH24:MI:SS'); -- this is constant (Maybe a waste of the column, but forces a TABLE() return in case you want to add more columns, etc! FOR msg IN msgs(funcoid, warns, stop_fatal) LOOP str_out := ''; -- Start Fresh, and add strings as we go, for one final RETURN NEXT! IF thisproc <> msg.proname THEN -- Return a header! IF thisproc <> '' THEN check_msg := ''; RETURN NEXT; -- Blank line between different functions! END IF; thisproc := msg.proname; check_msg := CONCAT('===========> PROCESSING: ', thisproc); -- While REDUNDANT on 42883 Errors, it separates ALL functions from each other! RETURN NEXT; END IF; check_msg := msg.err; RETURN NEXT; errmsg := msg.err; IF errmsg LIKE 'error:42883:%' THEN -- SELECT '{}','{}','{}','{}','{}','{}' INTO a_txt, a_p1, a_p2, a_name, a_pname, a_flow; -- Produces plpgsql_check() warnings! a_txt := '{}'; a_p1 := '{}'; a_name := '{}'; a_pname := '{}'; str_out := '#### '; -- RETURN NEXT; IF RIGHT(errmsg, 14) = 'does not exist' THEN errmsg := LEFT(errmsg, -15); END IF; a_txt := STRING_TO_ARRAY(errmsg, ':'); IF CARDINALITY(a_txt) <> 5 THEN check_msg := str_out || chr(10) || '######## ==> details unavailable, parsing error <=== #########'::TEXT; RETURN NEXT; CONTINUE; END IF; fdecl := a_txt[5]; pos := POSITION('(' IN fdecl); IF pos = 0 THEN check_msg := str_out || chr(10) || '######## ==> details unavailable, parsing error(2) <=== #########'::TEXT; RETURN NEXT; CONTINUE; END IF; fname := LEFT(fdecl, pos - 1); -- exclude the paren fname := SUBSTR(fname, POSITION(' ' IN fname) + 1); parm1 := TRIM(SUBSTR(fdecl, pos, POSITION(')' IN fdecl) - pos + 1)); -- RETURN NEXT (ts , concat('#### ', fdecl )); -- Really Just Debug! BEGIN foid := TO_REGPROC(fname)::oid; -- This function will not throw an exception, just returns NULL -- REPLACES the error block IF foid IS NULL THEN check_msg := '#### Either No Such function or No Paramters!'; RETURN NEXT; CONTINUE; END IF; str_out := str_out || chr(10) || CONCAT('#### ', 'Error in: ', thisproc, ' at Line: ', a_txt[3], ' PARAMETER TYPING ISSUE?') || chr(10) || '#### '; a_p1 := STRING_TO_ARRAY(SUBSTRING(parm1, 2, LENGTH(parm1) - 2), ', '); -- These are just the types SELECT (POSITION('IN ' IN args) + POSITION('OUT ' IN args) )> 0 as tagged, args into has_in, names FROM (SELECT pg_catalog.PG_GET_FUNCTION_ARGUMENTS(foid) as args) t; a_name := STRING_TO_ARRAY(names, ', '); -- Separate these out! has_in is set for us /* We have an array of [INOUT] varname type [DEFAULT xxx] | And an array of the users param types param1 We will OUTPUT: Parameter Name [35], INOUT+DEF[10], P1_TYPE[15], OUR_TYPE \n */ str_out := CONCAT(str_out, chr(10), '#### ', rpad('Param Name',20), ' ', rpad('Flow/DEF',10), rpad('(your code)',15), rpad('Definition',15) ); str_out := CONCAT(str_out, chr(10), '#### ', rpad('==========',20), ' ', rpad('========',10), rpad('===========',15), rpad('==========',15) ); IF has_in THEN n_off := 1; flow_def := NULL; ELSE n_off := 0; flow_def := 'IN '; -- We have to force the display of IN, just for consistency. END IF; FOR x IN 1 .. CARDINALITY(a_name) LOOP a_pname := STRING_TO_ARRAY(a_name[x], ' '); -- Parse into an array -- RAISE NOTICE 'a_pname 1 %, 2 %, 3 %', a_pname[1], a_pname[2], a_pname[3]; flow := COALESCE(flow_def, a_pname[1]) || CASE WHEN POSITION('DEFAULT' IN a_name[x])=0 THEN '' ELSE ' DEF' END; str_out := CONCAT(str_out, chr(10), '#### ', rpad(a_pname[1+n_off],20), ' ',rpad(flow,10), rpad(coalesce(a_p1[x],'???'),15), rpad(a_pname[2+n_off],15) ); END LOOP; EXCEPTION WHEN OTHERS THEN str_out := str_out || chr(10) || CONCAT('==== ERROR: ', SQLERRM, ' Unexpected Exception!'); END; str_out := str_out || chr(10) || '#### '; ELSE CONTINUE; -- Nothing to do, not our message END IF; check_msg := str_out; RETURN NEXT; END LOOP; IF thisproc='' AND funcoid is not null THEN check_msg := 'No Messages Returned for: ' || funcoid::regproc; RETURN NEXT; END IF; RETURN; END $$; okbob-plpgsql_check-800db79/expected/000077500000000000000000000000001526744060500175745ustar00rootroot00000000000000okbob-plpgsql_check-800db79/expected/README000066400000000000000000000007471526744060500204640ustar00rootroot00000000000000plpgsql_check_passive.out PostgreSQL 14, 15, 16 plpgsql_check_passive_2.out PostgreSQL 18, 19, 20 plpgsql_check_active_2.out PostgreSQL 14, 15, 16 plpgsql_check_active_3.out PostgreSQL 18 plpgsql_check_active_4.out PostgreSQL 19, 20 plpgsql_check_active.out PostgreSQL 17 plpgsql_pragma_generator.out PostgreSQL 14 and higher (all versions) plpgsql_check_parser_1.out PostgreSQL 14 plpgsql_check_stmtwalk_1.out PostgreSQL 14 plpgsql_check_nocrash_1.out PostgreSQL 14 okbob-plpgsql_check-800db79/expected/plpgsql_check_active-14.out000066400000000000000000000000001526744060500247070ustar00rootroot00000000000000okbob-plpgsql_check-800db79/expected/plpgsql_check_active-15.out000066400000000000000000000057541526744060500247350ustar00rootroot00000000000000LOAD 'plpgsql'; CREATE EXTENSION IF NOT EXISTS plpgsql_check; NOTICE: extension "plpgsql_check" already exists, skipping set client_min_messages to notice; create or replace function fxtest() returns void as $$ declare v_sqlstate text; v_message text; v_context text; begin get stacked diagnostics v_sqlstate = returned_sqlstate, v_message = message_text, v_context = pg_exception_context; end; $$ language plpgsql; select * from plpgsql_check_function('fxtest'); plpgsql_check_function ----------------------------------------------------------------------------------------------------------- error:0Z002:7:GET STACKED DIAGNOSTICS:GET STACKED DIAGNOSTICS cannot be used outside an exception handler (1 row) drop function fxtest(); create or replace procedure prtest() as $$ begin commit; end; $$ language plpgsql; select * from plpgsql_check_function('prtest'); --ok plpgsql_check_function ------------------------ (0 rows) create or replace procedure prtest() as $$ begin begin begin commit; end; end; exception when others then raise; end; $$ language plpgsql; select * from plpgsql_check_function('prtest'); --error plpgsql_check_function --------------------------------------------------------------------- error:2D000:5:COMMIT:cannot commit while a subtransaction is active (1 row) create or replace procedure prtest() as $$ begin raise exception 'error'; exception when others then begin begin commit; end; end; end; $$ language plpgsql; select * from plpgsql_check_function('prtest'); --ok plpgsql_check_function ------------------------ (0 rows) drop procedure prtest(); create function return_constant_refcursor() returns refcursor as $$ declare rc constant refcursor; begin open rc for select a from rc_test; return rc; end $$ language plpgsql; create table rc_test(a int); select * from plpgsql_check_function('return_constant_refcursor'); plpgsql_check_function ------------------------------------------------------- error:22005:5:OPEN:variable "rc" is declared CONSTANT (1 row) drop table rc_test; drop function return_constant_refcursor(); create procedure p1(a int, out b int) as $$ begin b := a + 10; end; $$ language plpgsql; create function f1() returns void as $$ declare b constant int; begin call p1(10, b); end; $$ language plpgsql; select * from plpgsql_check_function('f1'); plpgsql_check_function ------------------------------------------------------ error:22005:4:CALL:variable "b" is declared CONSTANT (1 row) drop function f1(); drop procedure p1(int, int); create or replace function f1() returns int as $$ declare c constant int default 100; begin return c; end; $$ language plpgsql; -- should be ok select * from plpgsql_check_function('f1'); plpgsql_check_function ------------------------ (0 rows) drop function f1(); okbob-plpgsql_check-800db79/expected/plpgsql_check_active-16.out000066400000000000000000000104711526744060500247260ustar00rootroot00000000000000LOAD 'plpgsql'; CREATE EXTENSION IF NOT EXISTS plpgsql_check; NOTICE: extension "plpgsql_check" already exists, skipping set client_min_messages to notice; create or replace function fxtest() returns void as $$ declare v_sqlstate text; v_message text; v_context text; begin get stacked diagnostics v_sqlstate = returned_sqlstate, v_message = message_text, v_context = pg_exception_context; end; $$ language plpgsql; select * from plpgsql_check_function('fxtest'); plpgsql_check_function ----------------------------------------------------------------------------------------------------------- error:0Z002:7:GET STACKED DIAGNOSTICS:GET STACKED DIAGNOSTICS cannot be used outside an exception handler (1 row) drop function fxtest(); create or replace procedure prtest() as $$ begin commit; end; $$ language plpgsql; select * from plpgsql_check_function('prtest'); --ok plpgsql_check_function ------------------------ (0 rows) create or replace procedure prtest() as $$ begin begin begin commit; end; end; exception when others then raise; end; $$ language plpgsql; select * from plpgsql_check_function('prtest'); --error plpgsql_check_function --------------------------------------------------------------------- error:2D000:5:COMMIT:cannot commit while a subtransaction is active (1 row) create or replace procedure prtest() as $$ begin raise exception 'error'; exception when others then begin begin commit; end; end; end; $$ language plpgsql; select * from plpgsql_check_function('prtest'); --ok plpgsql_check_function ------------------------ (0 rows) drop procedure prtest(); create function return_constant_refcursor() returns refcursor as $$ declare rc constant refcursor; begin open rc for select a from rc_test; return rc; end $$ language plpgsql; create table rc_test(a int); select * from plpgsql_check_function('return_constant_refcursor'); plpgsql_check_function ------------------------------------------------------- error:22005:5:OPEN:variable "rc" is declared CONSTANT (1 row) drop table rc_test; drop function return_constant_refcursor(); create procedure p1(a int, out b int) as $$ begin b := a + 10; end; $$ language plpgsql; create function f1() returns void as $$ declare b constant int; begin call p1(10, b); end; $$ language plpgsql; select * from plpgsql_check_function('f1'); plpgsql_check_function ------------------------------------------------------ error:22005:4:CALL:variable "b" is declared CONSTANT (1 row) drop function f1(); drop procedure p1(int, int); create or replace function f1() returns int as $$ declare c constant int default 100; begin return c; end; $$ language plpgsql; -- should be ok select * from plpgsql_check_function('f1'); plpgsql_check_function ------------------------ (0 rows) drop function f1(); -- do not raise false warning create or replace function test_function() returns text as $$ declare s text; begin get diagnostics s = PG_CONTEXT; return s; end; $$ language plpgsql; create or replace procedure test_procedure() as $$ begin null; end; $$ language plpgsql; -- should be without any warnings select * from plpgsql_check_function('test_function', performance_warnings=>true); plpgsql_check_function ------------------------ (0 rows) select * from plpgsql_check_function('test_procedure', performance_warnings=>true); plpgsql_check_function ------------------------ (0 rows) drop function test_function(); drop procedure test_procedure(); -- detect dependecy in CALL statement create or replace function fx1_dep(int) returns int as $$ begin return $1; end; $$ language plpgsql; create or replace procedure px1_dep(int) as $$ begin end; $$ language plpgsql; create or replace function test_function() returns void as $$ begin call px1_dep(fx1_dep(10)); end; $$ language plpgsql; select type, schema, name, params from plpgsql_show_dependency_tb('test_function'); type | schema | name | params -----------+--------+---------+----------- FUNCTION | public | fx1_dep | (integer) PROCEDURE | public | px1_dep | (integer) (2 rows) drop function test_function(); drop procedure px1_dep(int); drop function fx1_dep(int); okbob-plpgsql_check-800db79/expected/plpgsql_check_active-17.out000066400000000000000000000104711526744060500247270ustar00rootroot00000000000000LOAD 'plpgsql'; CREATE EXTENSION IF NOT EXISTS plpgsql_check; NOTICE: extension "plpgsql_check" already exists, skipping set client_min_messages to notice; create or replace function fxtest() returns void as $$ declare v_sqlstate text; v_message text; v_context text; begin get stacked diagnostics v_sqlstate = returned_sqlstate, v_message = message_text, v_context = pg_exception_context; end; $$ language plpgsql; select * from plpgsql_check_function('fxtest'); plpgsql_check_function ----------------------------------------------------------------------------------------------------------- error:0Z002:7:GET STACKED DIAGNOSTICS:GET STACKED DIAGNOSTICS cannot be used outside an exception handler (1 row) drop function fxtest(); create or replace procedure prtest() as $$ begin commit; end; $$ language plpgsql; select * from plpgsql_check_function('prtest'); --ok plpgsql_check_function ------------------------ (0 rows) create or replace procedure prtest() as $$ begin begin begin commit; end; end; exception when others then raise; end; $$ language plpgsql; select * from plpgsql_check_function('prtest'); --error plpgsql_check_function --------------------------------------------------------------------- error:2D000:5:COMMIT:cannot commit while a subtransaction is active (1 row) create or replace procedure prtest() as $$ begin raise exception 'error'; exception when others then begin begin commit; end; end; end; $$ language plpgsql; select * from plpgsql_check_function('prtest'); --ok plpgsql_check_function ------------------------ (0 rows) drop procedure prtest(); create function return_constant_refcursor() returns refcursor as $$ declare rc constant refcursor; begin open rc for select a from rc_test; return rc; end $$ language plpgsql; create table rc_test(a int); select * from plpgsql_check_function('return_constant_refcursor'); plpgsql_check_function ------------------------------------------------------- error:22005:5:OPEN:variable "rc" is declared CONSTANT (1 row) drop table rc_test; drop function return_constant_refcursor(); create procedure p1(a int, out b int) as $$ begin b := a + 10; end; $$ language plpgsql; create function f1() returns void as $$ declare b constant int; begin call p1(10, b); end; $$ language plpgsql; select * from plpgsql_check_function('f1'); plpgsql_check_function ------------------------------------------------------ error:22005:4:CALL:variable "b" is declared CONSTANT (1 row) drop function f1(); drop procedure p1(int, int); create or replace function f1() returns int as $$ declare c constant int default 100; begin return c; end; $$ language plpgsql; -- should be ok select * from plpgsql_check_function('f1'); plpgsql_check_function ------------------------ (0 rows) drop function f1(); -- do not raise false warning create or replace function test_function() returns text as $$ declare s text; begin get diagnostics s = PG_CONTEXT; return s; end; $$ language plpgsql; create or replace procedure test_procedure() as $$ begin null; end; $$ language plpgsql; -- should be without any warnings select * from plpgsql_check_function('test_function', performance_warnings=>true); plpgsql_check_function ------------------------ (0 rows) select * from plpgsql_check_function('test_procedure', performance_warnings=>true); plpgsql_check_function ------------------------ (0 rows) drop function test_function(); drop procedure test_procedure(); -- detect dependecy in CALL statement create or replace function fx1_dep(int) returns int as $$ begin return $1; end; $$ language plpgsql; create or replace procedure px1_dep(int) as $$ begin end; $$ language plpgsql; create or replace function test_function() returns void as $$ begin call px1_dep(fx1_dep(10)); end; $$ language plpgsql; select type, schema, name, params from plpgsql_show_dependency_tb('test_function'); type | schema | name | params -----------+--------+---------+----------- FUNCTION | public | fx1_dep | (integer) PROCEDURE | public | px1_dep | (integer) (2 rows) drop function test_function(); drop procedure px1_dep(int); drop function fx1_dep(int); okbob-plpgsql_check-800db79/expected/plpgsql_check_active-18.out000066400000000000000000000104711526744060500247300ustar00rootroot00000000000000LOAD 'plpgsql'; CREATE EXTENSION IF NOT EXISTS plpgsql_check; NOTICE: extension "plpgsql_check" already exists, skipping set client_min_messages to notice; create or replace function fxtest() returns void as $$ declare v_sqlstate text; v_message text; v_context text; begin get stacked diagnostics v_sqlstate = returned_sqlstate, v_message = message_text, v_context = pg_exception_context; end; $$ language plpgsql; select * from plpgsql_check_function('fxtest'); plpgsql_check_function ----------------------------------------------------------------------------------------------------------- error:0Z002:7:GET STACKED DIAGNOSTICS:GET STACKED DIAGNOSTICS cannot be used outside an exception handler (1 row) drop function fxtest(); create or replace procedure prtest() as $$ begin commit; end; $$ language plpgsql; select * from plpgsql_check_function('prtest'); --ok plpgsql_check_function ------------------------ (0 rows) create or replace procedure prtest() as $$ begin begin begin commit; end; end; exception when others then raise; end; $$ language plpgsql; select * from plpgsql_check_function('prtest'); --error plpgsql_check_function --------------------------------------------------------------------- error:2D000:5:COMMIT:cannot commit while a subtransaction is active (1 row) create or replace procedure prtest() as $$ begin raise exception 'error'; exception when others then begin begin commit; end; end; end; $$ language plpgsql; select * from plpgsql_check_function('prtest'); --ok plpgsql_check_function ------------------------ (0 rows) drop procedure prtest(); create function return_constant_refcursor() returns refcursor as $$ declare rc constant refcursor; begin open rc for select a from rc_test; return rc; end $$ language plpgsql; create table rc_test(a int); select * from plpgsql_check_function('return_constant_refcursor'); plpgsql_check_function ------------------------------------------------------- error:22005:5:OPEN:variable "rc" is declared CONSTANT (1 row) drop table rc_test; drop function return_constant_refcursor(); create procedure p1(a int, out b int) as $$ begin b := a + 10; end; $$ language plpgsql; create function f1() returns void as $$ declare b constant int; begin call p1(10, b); end; $$ language plpgsql; select * from plpgsql_check_function('f1'); plpgsql_check_function ------------------------------------------------------ error:22005:4:CALL:variable "b" is declared CONSTANT (1 row) drop function f1(); drop procedure p1(int, int); create or replace function f1() returns int as $$ declare c constant int default 100; begin return c; end; $$ language plpgsql; -- should be ok select * from plpgsql_check_function('f1'); plpgsql_check_function ------------------------ (0 rows) drop function f1(); -- do not raise false warning create or replace function test_function() returns text as $$ declare s text; begin get diagnostics s = PG_CONTEXT; return s; end; $$ language plpgsql; create or replace procedure test_procedure() as $$ begin null; end; $$ language plpgsql; -- should be without any warnings select * from plpgsql_check_function('test_function', performance_warnings=>true); plpgsql_check_function ------------------------ (0 rows) select * from plpgsql_check_function('test_procedure', performance_warnings=>true); plpgsql_check_function ------------------------ (0 rows) drop function test_function(); drop procedure test_procedure(); -- detect dependecy in CALL statement create or replace function fx1_dep(int) returns int as $$ begin return $1; end; $$ language plpgsql; create or replace procedure px1_dep(int) as $$ begin end; $$ language plpgsql; create or replace function test_function() returns void as $$ begin call px1_dep(fx1_dep(10)); end; $$ language plpgsql; select type, schema, name, params from plpgsql_show_dependency_tb('test_function'); type | schema | name | params -----------+--------+---------+----------- FUNCTION | public | fx1_dep | (integer) PROCEDURE | public | px1_dep | (integer) (2 rows) drop function test_function(); drop procedure px1_dep(int); drop function fx1_dep(int); okbob-plpgsql_check-800db79/expected/plpgsql_check_active-19.out000066400000000000000000000104711526744060500247310ustar00rootroot00000000000000LOAD 'plpgsql'; CREATE EXTENSION IF NOT EXISTS plpgsql_check; NOTICE: extension "plpgsql_check" already exists, skipping set client_min_messages to notice; create or replace function fxtest() returns void as $$ declare v_sqlstate text; v_message text; v_context text; begin get stacked diagnostics v_sqlstate = returned_sqlstate, v_message = message_text, v_context = pg_exception_context; end; $$ language plpgsql; select * from plpgsql_check_function('fxtest'); plpgsql_check_function ----------------------------------------------------------------------------------------------------------- error:0Z002:7:GET STACKED DIAGNOSTICS:GET STACKED DIAGNOSTICS cannot be used outside an exception handler (1 row) drop function fxtest(); create or replace procedure prtest() as $$ begin commit; end; $$ language plpgsql; select * from plpgsql_check_function('prtest'); --ok plpgsql_check_function ------------------------ (0 rows) create or replace procedure prtest() as $$ begin begin begin commit; end; end; exception when others then raise; end; $$ language plpgsql; select * from plpgsql_check_function('prtest'); --error plpgsql_check_function --------------------------------------------------------------------- error:2D000:5:COMMIT:cannot commit while a subtransaction is active (1 row) create or replace procedure prtest() as $$ begin raise exception 'error'; exception when others then begin begin commit; end; end; end; $$ language plpgsql; select * from plpgsql_check_function('prtest'); --ok plpgsql_check_function ------------------------ (0 rows) drop procedure prtest(); create function return_constant_refcursor() returns refcursor as $$ declare rc constant refcursor; begin open rc for select a from rc_test; return rc; end $$ language plpgsql; create table rc_test(a int); select * from plpgsql_check_function('return_constant_refcursor'); plpgsql_check_function ------------------------------------------------------- error:22005:5:OPEN:variable "rc" is declared CONSTANT (1 row) drop table rc_test; drop function return_constant_refcursor(); create procedure p1(a int, out b int) as $$ begin b := a + 10; end; $$ language plpgsql; create function f1() returns void as $$ declare b constant int; begin call p1(10, b); end; $$ language plpgsql; select * from plpgsql_check_function('f1'); plpgsql_check_function ------------------------------------------------------ error:22005:4:CALL:variable "b" is declared CONSTANT (1 row) drop function f1(); drop procedure p1(int, int); create or replace function f1() returns int as $$ declare c constant int default 100; begin return c; end; $$ language plpgsql; -- should be ok select * from plpgsql_check_function('f1'); plpgsql_check_function ------------------------ (0 rows) drop function f1(); -- do not raise false warning create or replace function test_function() returns text as $$ declare s text; begin get diagnostics s = PG_CONTEXT; return s; end; $$ language plpgsql; create or replace procedure test_procedure() as $$ begin null; end; $$ language plpgsql; -- should be without any warnings select * from plpgsql_check_function('test_function', performance_warnings=>true); plpgsql_check_function ------------------------ (0 rows) select * from plpgsql_check_function('test_procedure', performance_warnings=>true); plpgsql_check_function ------------------------ (0 rows) drop function test_function(); drop procedure test_procedure(); -- detect dependecy in CALL statement create or replace function fx1_dep(int) returns int as $$ begin return $1; end; $$ language plpgsql; create or replace procedure px1_dep(int) as $$ begin end; $$ language plpgsql; create or replace function test_function() returns void as $$ begin call px1_dep(fx1_dep(10)); end; $$ language plpgsql; select type, schema, name, params from plpgsql_show_dependency_tb('test_function'); type | schema | name | params -----------+--------+---------+----------- FUNCTION | public | fx1_dep | (integer) PROCEDURE | public | px1_dep | (integer) (2 rows) drop function test_function(); drop procedure px1_dep(int); drop function fx1_dep(int); okbob-plpgsql_check-800db79/expected/plpgsql_check_active-20.out000066400000000000000000000104711526744060500247210ustar00rootroot00000000000000LOAD 'plpgsql'; CREATE EXTENSION IF NOT EXISTS plpgsql_check; NOTICE: extension "plpgsql_check" already exists, skipping set client_min_messages to notice; create or replace function fxtest() returns void as $$ declare v_sqlstate text; v_message text; v_context text; begin get stacked diagnostics v_sqlstate = returned_sqlstate, v_message = message_text, v_context = pg_exception_context; end; $$ language plpgsql; select * from plpgsql_check_function('fxtest'); plpgsql_check_function ----------------------------------------------------------------------------------------------------------- error:0Z002:7:GET STACKED DIAGNOSTICS:GET STACKED DIAGNOSTICS cannot be used outside an exception handler (1 row) drop function fxtest(); create or replace procedure prtest() as $$ begin commit; end; $$ language plpgsql; select * from plpgsql_check_function('prtest'); --ok plpgsql_check_function ------------------------ (0 rows) create or replace procedure prtest() as $$ begin begin begin commit; end; end; exception when others then raise; end; $$ language plpgsql; select * from plpgsql_check_function('prtest'); --error plpgsql_check_function --------------------------------------------------------------------- error:2D000:5:COMMIT:cannot commit while a subtransaction is active (1 row) create or replace procedure prtest() as $$ begin raise exception 'error'; exception when others then begin begin commit; end; end; end; $$ language plpgsql; select * from plpgsql_check_function('prtest'); --ok plpgsql_check_function ------------------------ (0 rows) drop procedure prtest(); create function return_constant_refcursor() returns refcursor as $$ declare rc constant refcursor; begin open rc for select a from rc_test; return rc; end $$ language plpgsql; create table rc_test(a int); select * from plpgsql_check_function('return_constant_refcursor'); plpgsql_check_function ------------------------------------------------------- error:22005:5:OPEN:variable "rc" is declared CONSTANT (1 row) drop table rc_test; drop function return_constant_refcursor(); create procedure p1(a int, out b int) as $$ begin b := a + 10; end; $$ language plpgsql; create function f1() returns void as $$ declare b constant int; begin call p1(10, b); end; $$ language plpgsql; select * from plpgsql_check_function('f1'); plpgsql_check_function ------------------------------------------------------ error:22005:4:CALL:variable "b" is declared CONSTANT (1 row) drop function f1(); drop procedure p1(int, int); create or replace function f1() returns int as $$ declare c constant int default 100; begin return c; end; $$ language plpgsql; -- should be ok select * from plpgsql_check_function('f1'); plpgsql_check_function ------------------------ (0 rows) drop function f1(); -- do not raise false warning create or replace function test_function() returns text as $$ declare s text; begin get diagnostics s = PG_CONTEXT; return s; end; $$ language plpgsql; create or replace procedure test_procedure() as $$ begin null; end; $$ language plpgsql; -- should be without any warnings select * from plpgsql_check_function('test_function', performance_warnings=>true); plpgsql_check_function ------------------------ (0 rows) select * from plpgsql_check_function('test_procedure', performance_warnings=>true); plpgsql_check_function ------------------------ (0 rows) drop function test_function(); drop procedure test_procedure(); -- detect dependecy in CALL statement create or replace function fx1_dep(int) returns int as $$ begin return $1; end; $$ language plpgsql; create or replace procedure px1_dep(int) as $$ begin end; $$ language plpgsql; create or replace function test_function() returns void as $$ begin call px1_dep(fx1_dep(10)); end; $$ language plpgsql; select type, schema, name, params from plpgsql_show_dependency_tb('test_function'); type | schema | name | params -----------+--------+---------+----------- FUNCTION | public | fx1_dep | (integer) PROCEDURE | public | px1_dep | (integer) (2 rows) drop function test_function(); drop procedure px1_dep(int); drop function fx1_dep(int); okbob-plpgsql_check-800db79/expected/plpgsql_check_active.out000066400000000000000000006435511526744060500245150ustar00rootroot00000000000000load 'plpgsql'; create extension if not exists plpgsql_check; set client_min_messages to notice; set plpgsql_check.regress_test_mode = true; -- -- check function statement tests -- --should fail - is not plpgsql select * from plpgsql_check_function_tb('session_user()'); ERROR: "session_user"() is not a plpgsql function create table t1(a int, b int); create table pa (id int, pa_id character varying(32), status character varying(60)); create table ml(ml_id character varying(32), status_from character varying(60), pa_id character varying(32), xyz int); create function f1() returns void as $$ begin if false then update t1 set c = 30; end if; if false then raise notice '%', r.c; end if; end; $$ language plpgsql; select f1(); f1 ---- (1 row) select * from plpgsql_check_function_tb('f1()', fatal_errors := true); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+---------------+----------+--------------------------------------------+--------+------+-------+----------+----------------------+--------- f1 | 4 | SQL statement | 42703 | column "c" of relation "t1" does not exist | | | error | 15 | update t1 set c = 30 | (1 row) select * from plpgsql_check_function_tb('f1()', fatal_errors := false); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+---------------+----------+--------------------------------------------+--------+------+-------+----------+----------------------+--------- f1 | 4 | SQL statement | 42703 | column "c" of relation "t1" does not exist | | | error | 15 | update t1 set c = 30 | f1 | 7 | RAISE | 42P01 | missing FROM-clause entry for table "r" | | | error | 1 | r.c | (2 rows) select * from plpgsql_check_function_tb('f1()'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+---------------+----------+--------------------------------------------+--------+------+-------+----------+----------------------+--------- f1 | 4 | SQL statement | 42703 | column "c" of relation "t1" does not exist | | | error | 15 | update t1 set c = 30 | (1 row) drop function f1(); create function f1() returns void as $$ begin if false then insert into t1 values(10,20); update t1 set a = 10; delete from t1; end if; end; $$ language plpgsql stable; select f1(); f1 ---- (1 row) select * from plpgsql_check_function_tb('f1()', fatal_errors := false); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+---------------+----------+--------------------------------------------------+--------+------+-------+----------+------------------------------+--------- f1 | 4 | SQL statement | 0A000 | INSERT is not allowed in a non volatile function | | | error | 1 | insert into t1 values(10,20) | f1 | 5 | SQL statement | 0A000 | UPDATE is not allowed in a non volatile function | | | error | 1 | update t1 set a = 10 | f1 | 6 | SQL statement | 0A000 | DELETE is not allowed in a non volatile function | | | error | 1 | delete from t1 | (3 rows) drop function f1(); create function f1() returns void as $$ declare r record; begin if false then for r in update t1 set a = a + 1 returning * loop raise notice '%', r.a; end loop; end if; end; $$ language plpgsql; select f1(); f1 ---- (1 row) select * from plpgsql_check_function_tb('f1()', fatal_errors := false); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+---------+--------+------+-------+----------+-------+--------- (0 rows) drop function f1(); create function f1() returns void as $$ declare r record; begin if false then for r in update t1 set a = a + 1 returning * loop raise notice '%', r.a; end loop; end if; end; $$ language plpgsql stable; select f1(); f1 ---- (1 row) select * from plpgsql_check_function_tb('f1()', fatal_errors := false); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+----------------------+----------+--------------------------------------------------+--------+------+-------+----------+-------------------------------------+--------- f1 | 5 | FOR over SELECT rows | 0A000 | UPDATE is not allowed in a non volatile function | | | error | 1 | update t1 set a = a + 1 returning * | (1 row) drop function f1(); create function g1(out a int, out b int) as $$ select 10,20; $$ language sql; create function f1() returns void as $$ declare r record; begin r := g1(); if false then raise notice '%', r.c; end if; end; $$ language plpgsql; select f1(); f1 ---- (1 row) select * from plpgsql_check_function_tb('f1()'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+-----------------------------+--------+------+-------+----------+-------+---------------------- f1 | 6 | RAISE | 42703 | record "r" has no field "c" | | | error | | | SQL expression "r.c" (1 row) select f1(); f1 ---- (1 row) drop function f1(); drop function g1(); create function g1(out a int, out b int) returns setof record as $$ select * from t1; $$ language sql; create function f1() returns void as $$ declare r record; begin for r in select * from g1() loop raise notice '%', r.c; end loop; end; $$ language plpgsql; select f1(); f1 ---- (1 row) select * from plpgsql_check_function_tb('f1()'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+-----------------------------+--------+------+-------+----------+-------+---------------------- f1 | 6 | RAISE | 42703 | record "r" has no field "c" | | | error | | | SQL expression "r.c" (1 row) select f1(); f1 ---- (1 row) create or replace function f1() returns void as $$ declare r record; begin for r in select * from g1() loop r.c := 20; end loop; end; $$ language plpgsql; select f1(); f1 ---- (1 row) select * from plpgsql_check_function_tb('f1()'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+------------+----------+-----------------------------+--------+------+-------+----------+-------+--------------------------------------------------------------- f1 | 6 | assignment | 42703 | record "r" has no field "c" | | | error | | | at assignment to field "c" of variable "r" declared on line 2 (1 row) select f1(); f1 ---- (1 row) drop function f1(); drop function g1(); create function f1() returns int as $$ declare r int; begin if false then r := a + b; end if; return r; end; $$ language plpgsql; select f1(); f1 ---- (1 row) select * from plpgsql_check_function_tb('f1()'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+------------+----------+---------------------------+--------+------+-------+----------+------------+-------------------------------------------------- f1 | 5 | assignment | 42703 | column "a" does not exist | | | error | 6 | r := a + b | at assignment to variable "r" declared on line 2 (1 row) select f1(); f1 ---- (1 row) drop function f1(); create or replace function f1() returns void as $$ declare r int[]; begin if false then r[c+10] := 20; end if; end; $$ language plpgsql; select f1(); f1 ---- (1 row) select * from plpgsql_check_function_tb('f1()'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+------------+----------+---------------------------+--------+------+-------+----------+---------------+-------------------------------------------------- f1 | 5 | assignment | 42703 | column "c" does not exist | | | error | 3 | r[c+10] := 20 | at assignment to variable "r" declared on line 2 (1 row) select f1(); f1 ---- (1 row) drop function f1(); create or replace function f1() returns void as $$ declare r int; begin if false then r[10] := 20; end if; end; $$ language plpgsql set search_path = public; select f1(); f1 ---- (1 row) select * from plpgsql_check_function_tb('f1()'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+------------+----------+------------------------------------------------------------------------+--------+------+-------+----------+-------------+-------------------------------------------------- f1 | 5 | assignment | 42804 | cannot subscript type integer because it does not support subscripting | | | error | 1 | r[10] := 20 | at assignment to variable "r" declared on line 2 (1 row) select f1(); f1 ---- (1 row) drop function f1(); create or replace function f1_trg() returns trigger as $$ begin if new.a > 10 then raise notice '%', new.b; raise notice '%', new.c; end if; return new; end; $$ language plpgsql; create trigger t1_f1 before insert on t1 for each row execute procedure f1_trg(); insert into t1 values(6,30); select * from plpgsql_check_function_tb('f1_trg()','t1'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+-------------------------------+--------+------+-------+----------+-------+------------------------ f1_trg | 5 | RAISE | 42703 | record "new" has no field "c" | | | error | | | SQL expression "new.c" (1 row) insert into t1 values(6,30); create or replace function f1_trg() returns trigger as $$ begin new.a := new.a + 10; new.b := new.b + 10; new.c := 30; return new; end; $$ language plpgsql; -- should to fail select * from plpgsql_check_function_tb('f1_trg()','t1'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+------------+----------+-------------------------------+--------+------+-------+----------+-------+----------------------------------------------------------------- f1_trg | 5 | assignment | 42703 | record "new" has no field "c" | | | error | | | at assignment to field "c" of variable "new" declared on line 0 (1 row) -- should to fail but not crash insert into t1 values(6,30); ERROR: record "new" has no field "c" CONTEXT: PL/pgSQL assignment "new.c := 30" PL/pgSQL function f1_trg() line 5 at assignment create or replace function f1_trg() returns trigger as $$ begin new.a := new.a + 10; new.b := new.b + 10; return new; end; $$ language plpgsql; -- ok select * from plpgsql_check_function_tb('f1_trg()', 't1'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+---------+--------+------+-------+----------+-------+--------- (0 rows) -- ok insert into t1 values(6,30); create or replace function f1_trg() returns trigger as $$ begin new.a := new.a + 10; new.b := new.b + 10; return null; end; $$ language plpgsql; -- ok select * from plpgsql_check_function_tb('f1_trg()', 't1'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+---------+--------+------+-------+----------+-------+--------- (0 rows) insert into t1 values(60,300); select * from t1; a | b ----+---- 6 | 30 6 | 30 16 | 40 (3 rows) insert into t1 values(600,30); select * from t1; a | b ----+---- 6 | 30 6 | 30 16 | 40 (3 rows) drop trigger t1_f1 on t1; drop function f1_trg(); -- test of showing caret on correct place for multiline queries create or replace function f1() returns void as $$ begin select var from foo; end; $$ language plpgsql; select * from plpgsql_check_function_tb('f1()'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+---------------+----------+-------------------------------+--------+------+-------+----------+--------+--------- f1 | 3 | SQL statement | 42P01 | relation "foo" does not exist | | | error | 23 | select+| | | | | | | | | | var +| | | | | | | | | | from+| | | | | | | | | | foo | (1 row) drop function f1(); create or replace function f1() returns int as $$ begin return (select a from t1 where hh = 20); end; $$ language plpgsql; select * from plpgsql_check_function_tb('f1()'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+----------------------------+--------+------+-------+----------+----------------------------+--------- f1 | 3 | RETURN | 42703 | column "hh" does not exist | | | error | 50 | (select a +| | | | | | | | | | from t1 +| | | | | | | | | | where hh = 20) | (1 row) create or replace function f1() returns int as $$ begin return (select a from txxxxxxx where hh = 20); end; $$ language plpgsql; select * from plpgsql_check_function_tb('f1()'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+------------------------------------+--------+------+-------+----------+----------------------------+--------- f1 | 3 | RETURN | 42P01 | relation "txxxxxxx" does not exist | | | error | 29 | (select a +| | | | | | | | | | from txxxxxxx+| | | | | | | | | | where hh = 20) | (1 row) drop function f1(); drop table t1; -- raise warnings when target row has different number of attributies in -- SELECT INTO statement create or replace function f1() returns void as $$ declare a1 int; a2 int; begin select 10,20 into a1,a2; end; $$ language plpgsql; -- should be ok select * from plpgsql_check_function_tb('f1()'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+--------------------------+--------+------+---------------+----------+-------+--------- f1 | 2 | DECLARE | 00000 | never read variable "a1" | | | warning extra | | | f1 | 2 | DECLARE | 00000 | never read variable "a2" | | | warning extra | | | (2 rows) create or replace function f1() returns void as $$ declare a1 int; begin select 10,20 into a1; end; $$ language plpgsql; -- raise warning select * from plpgsql_check_function_tb('f1()'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+---------------+----------+------------------------------------------+---------------------------------------------------------------+-------------------------------------------------+---------------+----------+--------------+---------------------------------- f1 | 4 | SQL statement | 00000 | too many attributes for target variables | There are less target variables than output columns in query. | Check target variables in SELECT INTO statement | warning | 1 | select 10,20 | at SQL statement to a1 variables f1 | 2 | DECLARE | 00000 | never read variable "a1" | | | warning extra | | | (2 rows) create or replace function f1() returns void as $$ declare a1 int; a2 int; begin select 10 into a1,a2; end; $$ language plpgsql; -- raise warning select * from plpgsql_check_function_tb('f1()'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+---------------+----------+-----------------------------------------+---------------------------------------------------------------+--------------------------------------------------+---------------+----------+-----------+-------------------------------------- f1 | 4 | SQL statement | 00000 | too few attributes for target variables | There are more target variables than output columns in query. | Check target variables in SELECT INTO statement. | warning | 1 | select 10 | at SQL statement to a1, a2 variables f1 | 2 | DECLARE | 00000 | never read variable "a1" | | | warning extra | | | f1 | 2 | DECLARE | 00000 | never read variable "a2" | | | warning extra | | | (3 rows) -- bogus code set check_function_bodies to off; create or replace function f1() returns void as $$ adasdfsadf $$ language plpgsql; select * from plpgsql_check_function_tb('f1()'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+--------------------------------------+--------+------+-------+----------+------------+--------------------------------------------------- f1 | | | 42601 | syntax error at or near "adasdfsadf" | | | error | 2 | +| compilation of PL/pgSQL function "f1" near line 1 | | | | | | | | | adasdfsadf+| | | | | | | | | | | (1 row) drop function f1(); create table t1(a int, b int); create function g1(out a int, out b int) as $$ select 10,20; $$ language sql; create function f1() returns void as $$ declare r record; begin r := g1(); if false then raise notice '%', r.c; end if; end; $$ language plpgsql; select f1(); f1 ---- (1 row) select * from plpgsql_check_function('f1()'); plpgsql_check_function ------------------------------------------------- error:42703:6:RAISE:record "r" has no field "c" Context: SQL expression "r.c" (2 rows) select f1(); f1 ---- (1 row) drop function f1(); drop function g1(); create function g1(out a int, out b int) returns setof record as $$ select * from t1; $$ language sql; create function f1() returns void as $$ declare r record; begin for r in select * from g1() loop raise notice '%', r.c; end loop; end; $$ language plpgsql; select f1(); f1 ---- (1 row) select * from plpgsql_check_function('f1()'); plpgsql_check_function ------------------------------------------------- error:42703:6:RAISE:record "r" has no field "c" Context: SQL expression "r.c" (2 rows) select f1(); f1 ---- (1 row) create or replace function f1() returns void as $$ declare r record; begin for r in select * from g1() loop r.c := 20; end loop; end; $$ language plpgsql; select f1(); f1 ---- (1 row) select * from plpgsql_check_function('f1()'); plpgsql_check_function ------------------------------------------------------------------------ error:42703:6:assignment:record "r" has no field "c" Context: at assignment to field "c" of variable "r" declared on line 2 (2 rows) select f1(); f1 ---- (1 row) drop function f1(); drop function g1(); create function f1() returns int as $$ declare r int; begin if false then r := a + b; end if; return r; end; $$ language plpgsql; select f1(); f1 ---- (1 row) select * from plpgsql_check_function('f1()'); plpgsql_check_function ----------------------------------------------------------- error:42703:5:assignment:column "a" does not exist Query: r := a + b -- ^ Context: at assignment to variable "r" declared on line 2 (4 rows) select f1(); f1 ---- (1 row) drop function f1(); create or replace function f1() returns void as $$ declare r int[]; begin if false then r[c+10] := 20; end if; end; $$ language plpgsql; select f1(); f1 ---- (1 row) select * from plpgsql_check_function('f1()'); plpgsql_check_function ----------------------------------------------------------- error:42703:5:assignment:column "c" does not exist Query: r[c+10] := 20 -- ^ Context: at assignment to variable "r" declared on line 2 (4 rows) select f1(); f1 ---- (1 row) drop function f1(); create or replace function f1() returns void as $$ declare r int; begin if false then r[10] := 20; end if; end; $$ language plpgsql set search_path = public; select f1(); f1 ---- (1 row) select * from plpgsql_check_function('f1()'); plpgsql_check_function ------------------------------------------------------------------------------------------------- error:42804:5:assignment:cannot subscript type integer because it does not support subscripting Query: r[10] := 20 -- ^ Context: at assignment to variable "r" declared on line 2 (4 rows) select f1(); f1 ---- (1 row) drop function f1(); create or replace function f1_trg() returns trigger as $$ begin if new.a > 10 then raise notice '%', new.b; raise notice '%', new.c; end if; return new; end; $$ language plpgsql; create trigger t1_f1 before insert on t1 for each row execute procedure f1_trg(); insert into t1 values(6,30); select * from plpgsql_check_function('f1_trg()','t1'); plpgsql_check_function --------------------------------------------------- error:42703:5:RAISE:record "new" has no field "c" Context: SQL expression "new.c" (2 rows) insert into t1 values(6,30); create or replace function f1_trg() returns trigger as $$ begin new.a := new.a + 10; new.b := new.b + 10; new.c := 30; return new; end; $$ language plpgsql; -- should to fail select * from plpgsql_check_function('f1_trg()','t1'); plpgsql_check_function -------------------------------------------------------------------------- error:42703:5:assignment:record "new" has no field "c" Context: at assignment to field "c" of variable "new" declared on line 0 (2 rows) -- should to fail but not crash insert into t1 values(6,30); ERROR: record "new" has no field "c" CONTEXT: PL/pgSQL assignment "new.c := 30" PL/pgSQL function f1_trg() line 5 at assignment create or replace function f1_trg() returns trigger as $$ begin new.a := new.a + 10; new.b := new.b + 10; return new; end; $$ language plpgsql; -- ok select * from plpgsql_check_function('f1_trg()', 't1'); plpgsql_check_function ------------------------ (0 rows) -- ok insert into t1 values(6,30); select * from t1; a | b ----+---- 6 | 30 6 | 30 16 | 40 (3 rows) drop trigger t1_f1 on t1; drop function f1_trg(); -- test of showing caret on correct place for multiline queries create or replace function f1() returns void as $$ begin select var from foo; end; $$ language plpgsql; select * from plpgsql_check_function('f1()'); plpgsql_check_function ----------------------------------------------------------- error:42P01:3:SQL statement:relation "foo" does not exist Query: select var from foo -- ^ (6 rows) drop function f1(); create or replace function f1() returns int as $$ begin return (select a from t1 where hh = 20); end; $$ language plpgsql; select * from plpgsql_check_function('f1()'); plpgsql_check_function ------------------------------------------------- error:42703:3:RETURN:column "hh" does not exist Query: (select a from t1 where hh = 20) -- ^ (5 rows) create or replace function f1() returns int as $$ begin return (select a from txxxxxxx where hh = 20); end; $$ language plpgsql; select * from plpgsql_check_function('f1()'); plpgsql_check_function --------------------------------------------------------- error:42P01:3:RETURN:relation "txxxxxxx" does not exist Query: (select a from txxxxxxx -- ^ where hh = 20) (5 rows) drop function f1(); drop table t1; -- raise warnings when target row has different number of attributies in -- SELECT INTO statement create or replace function f1() returns void as $$ declare a1 int; a2 int; begin select 10,20 into a1,a2; end; $$ language plpgsql; -- should be ok select * from plpgsql_check_function('f1()'); plpgsql_check_function -------------------------------------------------------- warning extra:00000:2:DECLARE:never read variable "a1" warning extra:00000:2:DECLARE:never read variable "a2" (2 rows) create or replace function f1() returns void as $$ declare a1 int; begin select 10,20 into a1; end; $$ language plpgsql; -- raise warning select * from plpgsql_check_function('f1()'); plpgsql_check_function ------------------------------------------------------------------------ warning:00000:4:SQL statement:too many attributes for target variables Query: select 10,20 -- ^ Detail: There are less target variables than output columns in query. Hint: Check target variables in SELECT INTO statement Context: at SQL statement to a1 variables warning extra:00000:2:DECLARE:never read variable "a1" (7 rows) create or replace function f1() returns void as $$ declare a1 int; a2 int; begin select 10 into a1,a2; end; $$ language plpgsql; -- raise warning select * from plpgsql_check_function('f1()'); plpgsql_check_function ----------------------------------------------------------------------- warning:00000:4:SQL statement:too few attributes for target variables Query: select 10 -- ^ Detail: There are more target variables than output columns in query. Hint: Check target variables in SELECT INTO statement. Context: at SQL statement to a1, a2 variables warning extra:00000:2:DECLARE:never read variable "a1" warning extra:00000:2:DECLARE:never read variable "a2" (8 rows) -- bogus code set check_function_bodies to off; create or replace function f1() returns void as $$ adasdfsadf $$ language plpgsql; select * from plpgsql_check_function('f1()'); plpgsql_check_function ------------------------------------------------------------ error:42601:syntax error at or near "adasdfsadf" Query: adasdfsadf -- ^ Context: compilation of PL/pgSQL function "f1" near line 1 (6 rows) drop function f1(); create table f1tbl(a int, b int); -- unused variables create or replace function f1(_input1 int) returns table(_output1 int, _output2 int) as $$ declare _f1 int; _f2 int; _f3 int; _f4 int; _f5 int; _r record; _tbl f1tbl; begin if true then _f1 := 1; end if; select 1, 2 into _f3, _f4; perform 1 where _f5 is null; select 1 into _r; select 1, 2 into _tbl; -- check that SQLSTATE and SQLERRM don't raise false positives begin exception when raise_exception then end; end $$ language plpgsql; select * from plpgsql_check_function('f1(int)'); plpgsql_check_function ---------------------------------------------------------- warning:00000:4:DECLARE:unused variable "_f2" warning extra:00000:3:DECLARE:never read variable "_f1" warning extra:00000:5:DECLARE:never read variable "_f3" warning extra:00000:6:DECLARE:never read variable "_f4" warning extra:00000:8:DECLARE:never read variable "_r" warning extra:00000:9:DECLARE:never read variable "_tbl" warning extra:00000:unused parameter "_input1" warning extra:00000:unmodified OUT variable "_output1" warning extra:00000:unmodified OUT variable "_output2" (9 rows) drop function f1(int); drop table f1tbl; -- check that NEW and OLD are not reported unused create table f1tbl(); create or replace function f1() returns trigger as $$ begin return null; end $$ language plpgsql; select * from plpgsql_check_function('f1()', 'f1tbl'); plpgsql_check_function ------------------------ (0 rows) drop function f1(); drop table f1tbl; create table tabret(a int, b int); insert into tabret values(10,10); create or replace function f1() returns int as $$ begin return (select a from tabret); end; $$ language plpgsql; select * from plpgsql_check_function('f1()', performance_warnings := true); plpgsql_check_function ----------------------------------------------------------------------------------------- performance:00000:routine is marked as VOLATILE, should be STABLE Hint: When you fix this issue, please, recheck other functions that uses this function. (2 rows) create or replace function f1() returns int as $$ begin return (select a::numeric from tabret); end; $$ language plpgsql; select * from plpgsql_check_function('f1()', performance_warnings := true); plpgsql_check_function ----------------------------------------------------------------------------------------- performance:42804:3:RETURN:target type is different type than source type Detail: cast "numeric" value to "integer" type Hint: Hidden casting can be a performance issue. Context: at RETURN performance:00000:routine is marked as VOLATILE, should be STABLE Hint: When you fix this issue, please, recheck other functions that uses this function. (6 rows) create or replace function f1() returns int as $$ begin return (select a, b from tabret); end; $$ language plpgsql; select * from plpgsql_check_function('f1()', performance_warnings := true); plpgsql_check_function ----------------------------------------------------------- error:42601:3:RETURN:subquery must return only one column Query: (select a, b from tabret) -- ^ (3 rows) drop function f1(); create or replace function f1() returns table(ax int, bx int) as $$ begin return query select * from tabret; return; end; $$ language plpgsql; select * from plpgsql_check_function('f1()', performance_warnings := true); plpgsql_check_function ----------------------------------------------------------------------------------------- performance:00000:routine is marked as VOLATILE, should be STABLE Hint: When you fix this issue, please, recheck other functions that uses this function. (2 rows) drop function f1(); create or replace function f1() returns table(ax numeric, bx numeric) as $$ begin return query select * from tabret; return; end; $$ language plpgsql; select * from plpgsql_check_function('f1()', performance_warnings := true); plpgsql_check_function ----------------------------------------------------------------------------------- error:42804:3:RETURN QUERY:structure of query does not match function result type Detail: Returned type integer does not match expected type numeric in column 1. (2 rows) drop function f1(); create or replace function f1() returns setof tabret as $$ begin return query select * from tabret; return; end; $$ language plpgsql; select * from plpgsql_check_function('f1()', performance_warnings := true); plpgsql_check_function ----------------------------------------------------------------------------------------- performance:00000:routine is marked as VOLATILE, should be STABLE Hint: When you fix this issue, please, recheck other functions that uses this function. (2 rows) create or replace function f1() returns setof tabret as $$ begin return query select a from tabret; return; end; $$ language plpgsql; select * from plpgsql_check_function('f1()', performance_warnings := true); plpgsql_check_function ----------------------------------------------------------------------------------- error:42804:3:RETURN QUERY:structure of query does not match function result type Detail: Number of returned columns (1) does not match expected column count (2). (2 rows) create or replace function f1() returns setof tabret as $$ begin return query select a::numeric,b::numeric from tabret; return; end; $$ language plpgsql; select * from plpgsql_check_function('f1()', performance_warnings := true); plpgsql_check_function ----------------------------------------------------------------------------------- error:42804:3:RETURN QUERY:structure of query does not match function result type Detail: Returned type numeric does not match expected type integer in column 1. (2 rows) drop function f1(); create or replace function f1(a int) returns setof numeric as $$ begin return query select a; end $$ language plpgsql; select * from plpgsql_check_function('f1(int)', performance_warnings := true); plpgsql_check_function ----------------------------------------------------------------------------------- error:42804:2:RETURN QUERY:structure of query does not match function result type Detail: Returned type integer does not match expected type numeric in column 1. (2 rows) drop function f1(int); drop table tabret; create or replace function f1() returns void as $$ declare intval integer; begin intval := null; -- ok intval := 1; -- OK intval := '1'; -- OK intval := text '1'; -- not OK intval := current_date; -- not OK select 1 into intval; -- OK select '1' into intval; -- OK select text '1' into intval; -- not OK end $$ language plpgsql; select * from plpgsql_check_function('f1()', performance_warnings := true); plpgsql_check_function -------------------------------------------------------------------------------------- warning:42804:9:assignment:target type is different type than source type Query: intval := current_date -- ^ Detail: cast "date" value to "integer" type Hint: There are no possible explicit coercion between those types, possibly bug! Context: at assignment to variable "intval" declared on line 3 performance:00000:11:SQL statement:detected "SELECT expr INTO variable" Query: select 1 -- ^ Detail: This obsolete syntax of assigning can be slow. Hint: Use syntax "variable := expr" instead. performance:00000:12:SQL statement:detected "SELECT expr INTO variable" Query: select '1' -- ^ Detail: This obsolete syntax of assigning can be slow. Hint: Use syntax "variable := expr" instead. warning:42804:12:SQL statement:target type is different type than source type Query: select '1' -- ^ Detail: cast "text" value to "integer" type Hint: The input expression type does not have an assignment cast to the target type. Context: at SQL statement to variable "intval" declared on line 3 performance:00000:13:SQL statement:detected "SELECT expr INTO variable" Query: select text '1' -- ^ Detail: This obsolete syntax of assigning can be slow. Hint: Use syntax "variable := expr" instead. warning:42804:13:SQL statement:target type is different type than source type Query: select text '1' -- ^ Detail: cast "text" value to "integer" type Hint: The input expression type does not have an assignment cast to the target type. Context: at SQL statement to variable "intval" declared on line 3 warning extra:00000:3:DECLARE:never read variable "intval" (34 rows) drop function f1(); create or replace function f1() returns int as $$ begin return 1; end; $$ language plpgsql; select * from plpgsql_check_function('f1()', performance_warnings := true); plpgsql_check_function ----------------------------------------------------------------------------------------- performance:00000:routine is marked as VOLATILE, should be IMMUTABLE Hint: When you fix this issue, please, recheck other functions that uses this function. (2 rows) create or replace function f1() returns int as $$ begin return 1::numeric; end; $$ language plpgsql; select * from plpgsql_check_function('f1()', performance_warnings := true); plpgsql_check_function ----------------------------------------------------------------------------------------- performance:42804:3:RETURN:target type is different type than source type Detail: cast "numeric" value to "integer" type Hint: Hidden casting can be a performance issue. Context: at RETURN performance:00000:routine is marked as VOLATILE, should be IMMUTABLE Hint: When you fix this issue, please, recheck other functions that uses this function. (6 rows) create or replace function f1() returns int as $$ begin return null; end; $$ language plpgsql; select * from plpgsql_check_function('f1()', performance_warnings := true); plpgsql_check_function ----------------------------------------------------------------------------------------- performance:00000:routine is marked as VOLATILE, should be IMMUTABLE Hint: When you fix this issue, please, recheck other functions that uses this function. (2 rows) create or replace function f1() returns int as $$ begin return current_date; end; $$ language plpgsql; select * from plpgsql_check_function('f1()', performance_warnings := true); plpgsql_check_function ----------------------------------------------------------------------------------------- warning:42804:3:RETURN:target type is different type than source type Detail: cast "date" value to "integer" type Hint: There are no possible explicit coercion between those types, possibly bug! Context: at RETURN performance:00000:routine is marked as VOLATILE, should be STABLE Hint: When you fix this issue, please, recheck other functions that uses this function. (6 rows) create or replace function f1() returns int as $$ declare a int; begin return a; end; $$ language plpgsql; select * from plpgsql_check_function('f1()', performance_warnings := true); plpgsql_check_function ----------------------------------------------------------------------------------------- performance:00000:routine is marked as VOLATILE, should be IMMUTABLE Hint: When you fix this issue, please, recheck other functions that uses this function. (2 rows) create or replace function f1() returns int as $$ declare a numeric; begin return a; end; $$ language plpgsql; select * from plpgsql_check_function('f1()', performance_warnings := true); plpgsql_check_function ----------------------------------------------------------------------------------------- performance:42804:4:RETURN:target type is different type than source type Detail: cast "numeric" value to "integer" type Hint: Hidden casting can be a performance issue. Context: at RETURN of variable "a" declared on line 2 performance:00000:routine is marked as VOLATILE, should be IMMUTABLE Hint: When you fix this issue, please, recheck other functions that uses this function. (6 rows) drop function f1(); create or replace function f1() returns setof int as $$ begin return next 1; end; $$ language plpgsql; select * from plpgsql_check_function('f1()', performance_warnings := true); plpgsql_check_function ----------------------------------------------------------------------------------------- performance:00000:routine is marked as VOLATILE, should be IMMUTABLE Hint: When you fix this issue, please, recheck other functions that uses this function. (2 rows) create or replace function f1() returns setof int as $$ begin return next 1::numeric; -- tolerant, doesn't use tupmap end; $$ language plpgsql; select * from plpgsql_check_function('f1()', performance_warnings := true); plpgsql_check_function ----------------------------------------------------------------------------------------- performance:42804:3:RETURN NEXT:target type is different type than source type Detail: cast "numeric" value to "integer" type Hint: Hidden casting can be a performance issue. Context: at RETURN NEXT performance:00000:routine is marked as VOLATILE, should be IMMUTABLE Hint: When you fix this issue, please, recheck other functions that uses this function. (6 rows) drop function f1(); create type t1 as (a int, b int, c int); create type t2 as (a int, b numeric); create or replace function fx() returns t2 as $$ declare x t1; begin return x; end; $$ language plpgsql; select * from plpgsql_check_function('fx()', performance_warnings := true); plpgsql_check_function --------------------------------------------------------------------------------- error:42804:4:RETURN:returned record type does not match expected record type Detail: Returned type integer does not match expected type numeric in column 2. (2 rows) create or replace function fx() returns t2 as $$ declare x t2; begin return x; end; $$ language plpgsql; select * from plpgsql_check_function('fx()', performance_warnings := true); plpgsql_check_function ----------------------------------------------------------------------------------------- performance:00000:routine is marked as VOLATILE, should be IMMUTABLE Hint: When you fix this issue, please, recheck other functions that uses this function. (2 rows) drop function fx(); create or replace function fx() returns setof t2 as $$ declare x t1; begin return next x; return; end; $$ language plpgsql; select * from plpgsql_check_function('fx()', performance_warnings := true); plpgsql_check_function --------------------------------------------------------------------------------- error:42804:4:RETURN NEXT:wrong record type supplied in RETURN NEXT Detail: Returned type integer does not match expected type numeric in column 2. (2 rows) create or replace function fx() returns setof t2 as $$ declare x t2; begin return next x; return; end; $$ language plpgsql; select * from plpgsql_check_function('fx()', performance_warnings := true); plpgsql_check_function ----------------------------------------------------------------------------------------- performance:00000:routine is marked as VOLATILE, should be IMMUTABLE Hint: When you fix this issue, please, recheck other functions that uses this function. (2 rows) drop function fx(); create or replace function fx2(_id int, _pa_id varchar(32), _status varchar(60)) returns void as $$ declare begin insert into pa values(_id, _pa_id, _status); exception when OTHERS then raise notice '%', 'some message'; raise exception '%', sqlerrm; end $$ language plpgsql; select * from plpgsql_check_function('fx2(int, varchar, varchar)', performance_warnings := true); plpgsql_check_function ------------------------ (0 rows) create or replace function fx2(_id int, _pa_id varchar(32), _status varchar(60)) returns void as $$ declare begin insert into pa values(_id, _pa_id, _status) returning *; exception when OTHERS then raise notice '%', 'some message'; raise exception '%', sqlerrm; end $$ language plpgsql; select * from plpgsql_check_function('fx2(int, varchar, varchar)', performance_warnings := true); plpgsql_check_function ---------------------------------------------------------------------- error:42601:4:SQL statement:query has no destination for result data (1 row) create or replace function fx2(_id int, _pa_id varchar(32), _status varchar(60)) returns void as $$ declare begin SELECT * FROM pa LIMIT 1; exception when OTHERS then raise notice '%', 'some message'; raise exception '%', sqlerrm; end $$ language plpgsql; select * from plpgsql_check_function('fx2(int, varchar, varchar)', performance_warnings := true); plpgsql_check_function ---------------------------------------------------------------------- error:42601:4:SQL statement:query has no destination for result data (1 row) drop function fx2(int, varchar, varchar); create or replace function foreach_array_loop() returns void as $body$ declare arr text[]; el text; begin arr := array['1111','2222','3333']; foreach el in array arr loop raise notice '%', el; end loop; end; $body$ language 'plpgsql' stable; select * from plpgsql_check_function_tb('foreach_array_loop()', performance_warnings := true); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context --------------------+--------+-----------+----------+--------------------------------------------------+--------+-----------------------------------------------------------------------------------+-------------+----------+-------+--------- foreach_array_loop | | | 00000 | routine is marked as STABLE, should be IMMUTABLE | | When you fix this issue, please, recheck other functions that uses this function. | performance | | | (1 row) create or replace function foreach_array_loop() returns void as $body$ declare arr text[]; el int; begin arr := array['1111','2222','3333']; foreach el in array arr loop raise notice '%', el; end loop; end; $body$ language 'plpgsql' stable; select * from plpgsql_check_function_tb('foreach_array_loop()', performance_warnings := true); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context --------------------+--------+--------------------+----------+--------------------------------------------------+-------------------------------------+-----------------------------------------------------------------------------------+-------------+----------+-------+----------------------- foreach_array_loop | 7 | FOREACH over array | 42804 | target type is different type than source type | cast "text" value to "integer" type | The input expression type does not have an assignment cast to the target type. | warning | | | at FOREACH over array foreach_array_loop | | | 00000 | routine is marked as STABLE, should be IMMUTABLE | | When you fix this issue, please, recheck other functions that uses this function. | performance | | | (2 rows) create or replace function foreach_array_loop() returns void as $body$ declare arr date[]; el int; begin -- expression is not constant (else this check fails due tracking constant) arr := array['2014-01-01','2015-01-01','2016-01-01', current_date]::date[]; foreach el in array arr loop raise notice '%', el; end loop; end; $body$ language 'plpgsql' stable; select * from plpgsql_check_function_tb('foreach_array_loop()', performance_warnings := true); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context --------------------+--------+--------------------+----------+------------------------------------------------+-------------------------------------+----------------------------------------------------------------------------+---------+----------+-------+----------------------- foreach_array_loop | 8 | FOREACH over array | 42804 | target type is different type than source type | cast "date" value to "integer" type | There are no possible explicit coercion between those types, possibly bug! | warning | | | at FOREACH over array (1 row) create or replace function foreach_array_loop() returns void as $body$ declare arr date[]; el int; begin arr := array['2014-01-01','2015-01-01','2016-01-01']::date[]; foreach el in array arr loop raise notice '%', el; end loop; end; $body$ language 'plpgsql' stable; select * from plpgsql_check_function_tb('foreach_array_loop()', performance_warnings := true); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context --------------------+--------+--------------------+----------+-----------------------------------------------------+--------+------+-------+----------+-------+--------- foreach_array_loop | 7 | FOREACH over array | 22P02 | invalid input syntax for type integer: "01-01-2014" | | | error | | | (1 row) create or replace function foreach_array_loop() returns void as $body$ declare el text; begin foreach el in array array['1111','2222','3333'] loop raise notice '%', el; end loop; end; $body$ language 'plpgsql' stable; select * from plpgsql_check_function_tb('foreach_array_loop()', performance_warnings := true); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context --------------------+--------+-----------+----------+--------------------------------------------------+--------+-----------------------------------------------------------------------------------+-------------+----------+-------+--------- foreach_array_loop | | | 00000 | routine is marked as STABLE, should be IMMUTABLE | | When you fix this issue, please, recheck other functions that uses this function. | performance | | | (1 row) create or replace function foreach_array_loop() returns void as $body$ declare el int; begin foreach el in array array['1111','2222','3333'] loop raise notice '%', el; end loop; end; $body$ language 'plpgsql' stable; select * from plpgsql_check_function_tb('foreach_array_loop()', performance_warnings := true); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context --------------------+--------+--------------------+----------+--------------------------------------------------+-------------------------------------+-----------------------------------------------------------------------------------+-------------+----------+-------+----------------------- foreach_array_loop | 5 | FOREACH over array | 42804 | target type is different type than source type | cast "text" value to "integer" type | The input expression type does not have an assignment cast to the target type. | warning | | | at FOREACH over array foreach_array_loop | | | 00000 | routine is marked as STABLE, should be IMMUTABLE | | When you fix this issue, please, recheck other functions that uses this function. | performance | | | (2 rows) create or replace function foreach_array_loop() returns void as $body$ declare el int; begin foreach el in array array['2014-01-01','2015-01-01','2016-01-01']::date[] loop raise notice '%', el; end loop; end; $body$ language 'plpgsql' stable; select * from plpgsql_check_function_tb('foreach_array_loop()', performance_warnings := true); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context --------------------+--------+--------------------+----------+-----------------------------------------------------+--------+------+-------+----------+-------+--------- foreach_array_loop | 5 | FOREACH over array | 22P02 | invalid input syntax for type integer: "01-01-2014" | | | error | | | (1 row) drop function foreach_array_loop(); create or replace function scan_rows(int[]) returns void AS $$ declare x int[]; begin foreach x slice 1 in array $1 loop raise notice 'row = %', x; end loop; end; $$ language plpgsql; select * from plpgsql_check_function_tb('scan_rows(int[])', performance_warnings := true); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+----------------------------------------------------+--------+-----------------------------------------------------------------------------------+-------------+----------+-------+--------- scan_rows | | | 00000 | routine is marked as VOLATILE, should be IMMUTABLE | | When you fix this issue, please, recheck other functions that uses this function. | performance | | | (1 row) create or replace function scan_rows(int[]) returns void AS $$ declare x int[]; begin foreach x in array $1 loop raise notice 'row = %', x; end loop; end; $$ language plpgsql; select * from plpgsql_check_function_tb('scan_rows(int[])', performance_warnings := true); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+--------------------+----------+----------------------------------------------------+------------------------------------------+-----------------------------------------------------------------------------------+-------------+----------+-------+----------------------- scan_rows | 5 | FOREACH over array | 42804 | target type is different type than source type | cast "integer" value to "integer[]" type | There are no possible explicit coercion between those types, possibly bug! | warning | | | at FOREACH over array scan_rows | | | 00000 | routine is marked as VOLATILE, should be IMMUTABLE | | When you fix this issue, please, recheck other functions that uses this function. | performance | | | (2 rows) drop function scan_rows(int[]); drop type t1; drop type t2; create table t1(a int, b int); create table t2(a int, b int, c int); create table t3(a numeric, b int); insert into t1 values(10,20),(30,40); create or replace function fx() returns int as $$ declare s int default 0; r t1; begin foreach r in array (select array_agg(t1) from t1) loop s := r.a + r.b; end loop; return s; end; $$ language plpgsql; select * from plpgsql_check_function_tb('fx()', performance_warnings := true); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+-------------------------------------------------+--------+-----------------------------------------------------------------------------------+-------------+----------+-------+--------- fx | | | 00000 | routine is marked as VOLATILE, should be STABLE | | When you fix this issue, please, recheck other functions that uses this function. | performance | | | (1 row) create or replace function fx() returns int as $$ declare s int default 0; r t1; c t1[]; begin c := (select array_agg(t1) from t1); foreach r in array c loop s := r.a + r.b; end loop; return s; end; $$ language plpgsql; select * from plpgsql_check_function_tb('fx()', performance_warnings := true); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+-------------------------------------------------+--------+-----------------------------------------------------------------------------------+-------------+----------+-------+--------- fx | | | 00000 | routine is marked as VOLATILE, should be STABLE | | When you fix this issue, please, recheck other functions that uses this function. | performance | | | (1 row) create or replace function fx() returns int as $$ declare s int default 0; r t1; c t1[]; begin select array_agg(t1) into c from t1; foreach r in array c loop s := r.a + r.b; end loop; return s; end; $$ language plpgsql; select * from plpgsql_check_function_tb('fx()', performance_warnings := true); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+-------------------------------------------------+--------+-----------------------------------------------------------------------------------+-------------+----------+-------+--------- fx | | | 00000 | routine is marked as VOLATILE, should be STABLE | | When you fix this issue, please, recheck other functions that uses this function. | performance | | | (1 row) create or replace function fx() returns int as $$ declare s int default 0; r t1; c t1[]; begin select array_agg(t1) into c from t1; for i in array_lower(c, 1) .. array_upper(c, 1) loop r := c[i]; s := r.a + r.b; end loop; return s; end; $$ language plpgsql; select * from plpgsql_check_function_tb('fx()', performance_warnings := true); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+-------------------------------------------------+--------+-----------------------------------------------------------------------------------+-------------+----------+-------+--------- fx | | | 00000 | routine is marked as VOLATILE, should be STABLE | | When you fix this issue, please, recheck other functions that uses this function. | performance | | | (1 row) create or replace function fx() returns int as $$ declare s int default 0; c t1[]; begin select array_agg(t1) into c from t1; for i in array_lower(c, 1) .. array_upper(c, 1) loop s := (c[i]).a + (c[i]).b; end loop; return s; end; $$ language plpgsql; select * from plpgsql_check_function_tb('fx()', performance_warnings := true); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+-------------------------------------------------+--------+-----------------------------------------------------------------------------------+-------------+----------+-------+--------- fx | | | 00000 | routine is marked as VOLATILE, should be STABLE | | When you fix this issue, please, recheck other functions that uses this function. | performance | | | (1 row) create or replace function fx() returns int as $$ declare s int default 0; r record; c t1[]; begin select array_agg(t1) into c from t1; for i in array_lower(c, 1) .. array_upper(c, 1) loop r := c[i]; s := r.a + r.b; end loop; return s; end; $$ language plpgsql; select * from plpgsql_check_function_tb('fx()', performance_warnings := true); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+-------------------------------------------------+--------+-----------------------------------------------------------------------------------+-------------+----------+-------+--------- fx | | | 00000 | routine is marked as VOLATILE, should be STABLE | | When you fix this issue, please, recheck other functions that uses this function. | performance | | | (1 row) create or replace function fx() returns int as $$ declare s int default 0; r record; c t1[]; begin select array_agg(t1) into c from t1; for i in array_lower(c, 1) .. array_upper(c, 1) loop r := c[i]; s := r.a + r.b + r.c; end loop; return s; end; $$ language plpgsql; select * from plpgsql_check_function_tb('fx()', performance_warnings := true); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+------------+----------+-----------------------------+--------+------+-------+----------+-------+-------------------------------------------- fx | 11 | assignment | 42703 | record "r" has no field "c" | | | error | | | PL/pgSQL assignment "s := r.a + r.b + r.c" (1 row) create or replace function fx() returns int as $$ declare s int default 0; r t2; begin foreach r in array (select array_agg(t1) from t1) loop s := r.a + r.b; end loop; return s; end; $$ language plpgsql; select * from plpgsql_check_function_tb('fx()', performance_warnings := true); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+--------------------+----------+-------------------------------------------------+--------+-----------------------------------------------------------------------------------+-------------+----------+-------+----------------------- fx | 6 | FOREACH over array | 00000 | too few attributes for composite variable | | | warning | | | at FOREACH over array fx | | | 00000 | routine is marked as VOLATILE, should be STABLE | | When you fix this issue, please, recheck other functions that uses this function. | performance | | | (2 rows) create or replace function fx() returns int as $$ declare s int default 0; r t3; begin foreach r in array (select array_agg(t1) from t1) loop s := r.a + r.b; end loop; return s; end; $$ language plpgsql; select * from plpgsql_check_function_tb('fx()', performance_warnings := true); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+--------------------+----------+-------------------------------------------------+----------------------------------------+-----------------------------------------------------------------------------------+-------------+----------+----------------+-------------------------------------------------- fx | 6 | FOREACH over array | 42804 | target type is different type than source type | cast "integer" value to "numeric" type | Hidden casting can be a performance issue. | performance | | | at FOREACH over array fx | 8 | assignment | 42804 | target type is different type than source type | cast "numeric" value to "integer" type | Hidden casting can be a performance issue. | performance | 1 | s := r.a + r.b | at assignment to variable "s" declared on line 3 fx | | | 00000 | routine is marked as VOLATILE, should be STABLE | | When you fix this issue, please, recheck other functions that uses this function. | performance | | | (3 rows) drop function fx(); drop table t1; -- mscottie issue #13 create table test ( a text, b integer, c uuid ); create function before_insert_test() returns trigger language plpgsql as $$ begin select a into NEW.a from test where b = 1; select b into NEW.b from test where b = 1; select null::uuid into NEW.c from test where b = 1; return new; end; $$; select * from plpgsql_check_function_tb('before_insert_test()','test'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+---------+--------+------+-------+----------+-------+--------- (0 rows) create or replace function before_insert_test() returns trigger language plpgsql as $$ begin NEW.a := (select a from test where b = 1); NEW.b := (select b from test where b = 1); NEW.c := (select c from test where b = 1); return new; end; $$; select * from plpgsql_check_function_tb('before_insert_test()','test', fatal_errors := false); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+---------+--------+------+-------+----------+-------+--------- (0 rows) create or replace function before_insert_test() returns trigger language plpgsql as $$ begin NEW.a := 'Hello'::text; NEW.b := 10; NEW.c := null::uuid; return new; end; $$; select * from plpgsql_check_function_tb('before_insert_test()','test', fatal_errors := false); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+---------+--------+------+-------+----------+-------+--------- (0 rows) drop function before_insert_test(); create or replace function fx() returns void as $$ declare NEW test; OLD test; begin select null::uuid into NEW.c from test where b = 1; end; $$ language plpgsql; select * from plpgsql_check_function_tb('fx()', performance_warnings := true, fatal_errors := false); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+---------------------------+--------+------+---------------+----------+-------+--------- fx | 2 | DECLARE | 00000 | unused variable "old" | | | warning | | | fx | 2 | DECLARE | 00000 | never read variable "new" | | | warning extra | | | (2 rows) drop function fx(); create or replace function fx() returns void as $$ declare NEW test; begin NEW.a := 'Hello'::text; NEW.b := 10; NEW.c := null::uuid; end; $$ language plpgsql; select * from plpgsql_check_function_tb('fx()', performance_warnings := true, fatal_errors := false); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+----------------------------------------------------+--------+-----------------------------------------------------------------------------------+---------------+----------+-------+--------- fx | 2 | DECLARE | 00000 | never read variable "new" | | | warning extra | | | fx | | | 00000 | routine is marked as VOLATILE, should be IMMUTABLE | | When you fix this issue, please, recheck other functions that uses this function. | performance | | | (2 rows) drop function fx(); drop table test; create or replace function fx() returns void as $$ declare s int; sa int[]; sd date; bs int[]; begin sa[10] := s; sa[10] := sd; s := bs[10]; end; $$ language plpgsql; select * from plpgsql_check_function_tb('fx()', performance_warnings := true, fatal_errors := false); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+------------+----------+------------------------------------------------+-------------------------------------+----------------------------------------------------------------------------+---------------+----------+--------------+--------------------------------------------------- fx | 9 | assignment | 42804 | target type is different type than source type | cast "date" value to "integer" type | There are no possible explicit coercion between those types, possibly bug! | warning | 1 | sa[10] := sd | at assignment to variable "sa" declared on line 4 fx | 4 | DECLARE | 00000 | never read variable "sa" | | | warning extra | | | (2 rows) drop function fx(); create type t as (t text); create or replace function fx() returns void as $$ declare _t t; _tt t[]; _txt text; begin _t.t := 'ABC'; -- correct warning "unknown" _tt[1] := _t; _txt := _t; end; $$ language plpgsql; select * from plpgsql_check_function('fx()', performance_warnings := true); plpgsql_check_function ------------------------------------------------------------------------------- performance:42804:7:assignment:target type is different type than source type Query: _txt := _t -- ^ Detail: cast "t" value to "text" type Hint: Hidden casting can be a performance issue. Context: at assignment to variable "_txt" declared on line 3 warning extra:00000:2:DECLARE:never read variable "_tt" warning extra:00000:3:DECLARE:never read variable "_txt" (8 rows) drop function fx(); create or replace function fx() returns void as $$ declare _t1 t; _t2 t; begin _t1.t := 'ABC'::text; _t2 := _t1; raise notice '% %', _t2, _t2.t; end; $$ language plpgsql; select * from plpgsql_check_function('fx()', performance_warnings := true); plpgsql_check_function ----------------------------------------------------------------------------------------- performance:00000:routine is marked as VOLATILE, should be IMMUTABLE Hint: When you fix this issue, please, recheck other functions that uses this function. (2 rows) drop function fx(); create or replace function fx(out _tt t[]) as $$ declare _t t; begin _t.t := 'ABC'::text; _tt[1] := _t; end; $$ language plpgsql; select * from plpgsql_check_function('fx()', performance_warnings := true); plpgsql_check_function ----------------------------------------------------------------------------------------- performance:00000:routine is marked as VOLATILE, should be IMMUTABLE Hint: When you fix this issue, please, recheck other functions that uses this function. (2 rows) drop function fx(); drop type t; create or replace function fx() returns int as $$ declare x int; begin perform 1; return 10; end; $$ language plpgsql; select * from plpgsql_check_function('fx()', performance_warnings := true); plpgsql_check_function ----------------------------------------------------------------------------------------- warning:00000:2:DECLARE:unused variable "x" performance:00000:routine is marked as VOLATILE, should be IMMUTABLE Hint: When you fix this issue, please, recheck other functions that uses this function. (3 rows) drop function fx(); create table t(i int); create function test_t(OUT t) returns t AS $$ begin $1 := null; end; $$ language plpgsql; select test_t(); test_t -------- (1 row) select * from test_t(); i --- (1 row) select * from plpgsql_check_function('test_t()', performance_warnings := true); plpgsql_check_function ----------------------------------------------------------------------------------------- performance:00000:routine is marked as VOLATILE, should be IMMUTABLE Hint: When you fix this issue, please, recheck other functions that uses this function. (2 rows) create or replace function fx() returns void as $$ declare c cursor for select * from t; x varchar; begin open c; fetch c into x; close c; end; $$ language plpgsql; select test_t(); test_t -------- (1 row) select * from test_t(); i --- (1 row) select * from plpgsql_check_function('fx()', performance_warnings := true, fatal_errors := false); plpgsql_check_function -------------------------------------------------------------------------- performance:42804:7:FETCH:target type is different type than source type Detail: cast "integer" value to "character varying" type Hint: Hidden casting can be a performance issue. Context: at FETCH to variable "x" declared on line 4 warning extra:00000:4:DECLARE:never read variable "x" (5 rows) drop function fx(); create or replace function fx() returns void as $$ declare c cursor for select * from t; x int; begin open c; fetch c into x; close c; end; $$ language plpgsql; select test_t(); test_t -------- (1 row) select * from test_t(); i --- (1 row) select * from plpgsql_check_function('fx()', performance_warnings := true, fatal_errors := false); plpgsql_check_function ------------------------------------------------------- warning extra:00000:4:DECLARE:never read variable "x" (1 row) drop function fx(); create or replace function fx() returns void as $$ declare c cursor for select * from t; begin for r in c loop raise notice '%', r.a; end loop; end; $$ language plpgsql; select test_t(); test_t -------- (1 row) select * from test_t(); i --- (1 row) select * from plpgsql_check_function('fx()', performance_warnings := true, fatal_errors := false); plpgsql_check_function ------------------------------------------------------- error:42703:6:RAISE:record "r" has no field "a" Context: SQL expression "r.a" warning extra:00000:5:DECLARE:never read variable "r" (3 rows) drop function fx(); create or replace function fx() returns void as $$ declare c cursor for select * from t; begin for r in c loop raise notice '%', r.i; end loop; end; $$ language plpgsql; select test_t(); test_t -------- (1 row) select * from test_t(); i --- (1 row) select * from plpgsql_check_function('fx()', performance_warnings := true, fatal_errors := false); plpgsql_check_function ------------------------ (0 rows) drop function fx(); create table foo(a int, b int); create or replace function fx() returns void as $$ declare f1 int; f2 int; begin select 1, 2 into f1; select 1 into f1, f2; select a b into f1, f2 from foo; end; $$ language plpgsql; select fx(); fx ---- (1 row) select * from plpgsql_check_function('fx()', performance_warnings := true, fatal_errors := false); plpgsql_check_function ------------------------------------------------------------------------ warning:00000:4:SQL statement:too many attributes for target variables Query: select 1, 2 -- ^ Detail: There are less target variables than output columns in query. Hint: Check target variables in SELECT INTO statement Context: at SQL statement to f1 variables warning:00000:5:SQL statement:too few attributes for target variables Query: select 1 -- ^ Detail: There are more target variables than output columns in query. Hint: Check target variables in SELECT INTO statement. Context: at SQL statement to f1, f2 variables warning:00000:6:SQL statement:too few attributes for target variables Query: select a b from foo -- ^ Detail: There are more target variables than output columns in query. Hint: Check target variables in SELECT INTO statement. Context: at SQL statement to f1, f2 variables warning extra:00000:2:DECLARE:never read variable "f1" warning extra:00000:2:DECLARE:never read variable "f2" (20 rows) drop function fx(); drop table foo; create or replace function fx() returns void as $$ declare d date; begin d := (select 1 from pg_class limit 1); raise notice '%', d; end; $$ language plpgsql; select fx(); ERROR: invalid input syntax for type date: "1" CONTEXT: PL/pgSQL assignment "d := (select 1 from pg_class limit 1)" PL/pgSQL function fx() line 4 at assignment select * from plpgsql_check_function('fx()', performance_warnings := true, fatal_errors := false); plpgsql_check_function ---------------------------------------------------------------------------------- warning:42804:4:assignment:target type is different type than source type Query: d := (select 1 from pg_class limit 1) -- ^ Detail: cast "integer" value to "date" type Hint: There are no possible explicit coercion between those types, possibly bug! Context: at assignment to variable "d" declared on line 2 (6 rows) drop function fx(); create table tab_1(i int); create or replace function fx(a int) returns setof int as $$ declare c refcursor; r record; begin open c for select i from tab_1 where i = a; loop fetch c into r; if not found then exit; end if; return next r.i; end loop; end; $$ language plpgsql; select * from plpgsql_check_function('fx(int)', performance_warnings := true, fatal_errors := false); plpgsql_check_function ----------------------------------------------------------------------------------------- performance:00000:routine is marked as VOLATILE, should be STABLE Hint: When you fix this issue, please, recheck other functions that uses this function. (2 rows) create or replace function fx(a int) returns setof int as $$ declare c refcursor; r record; begin open c for select i from tab_1 where i = a; loop fetch c into r; if not found then exit; end if; return next r.x; end loop; end; $$ language plpgsql; select * from plpgsql_check_function('fx(int)', performance_warnings := true, fatal_errors := false); plpgsql_check_function ----------------------------------------------------------------------------------------- error:42703:12:RETURN NEXT:record "r" has no field "x" Context: SQL expression "r.x" warning extra:00000:4:DECLARE:never read variable "r" performance:00000:routine is marked as VOLATILE, should be STABLE Hint: When you fix this issue, please, recheck other functions that uses this function. (5 rows) drop function fx(int); drop table tab_1; create or replace function fxx() returns void as $$ begin rollback; end; $$ language plpgsql; select fxx(); ERROR: invalid transaction termination CONTEXT: PL/pgSQL function fxx() line 3 at ROLLBACK select * from plpgsql_check_function('fxx()'); plpgsql_check_function -------------------------------------------------------- error:2D000:3:ROLLBACK:invalid transaction termination (1 row) drop function fxx(); create or replace function fxx() returns void as $$ declare x int; begin declare x int; begin end; end; $$ language plpgsql; select * from plpgsql_check_function('fxx()'); plpgsql_check_function ------------------------------------------------------------------------------------------ warning extra:00000:5:statement block:variable "x" shadows a previously defined variable Hint: SET plpgsql.extra_warnings TO 'shadowed_variables' warning:00000:2:DECLARE:unused variable "x" warning:00000:4:DECLARE:unused variable "x" (4 rows) select * from plpgsql_check_function('fxx()', extra_warnings := false); plpgsql_check_function --------------------------------------------- warning:00000:2:DECLARE:unused variable "x" warning:00000:4:DECLARE:unused variable "x" (2 rows) drop function fxx(); create or replace function fxx(in a int, in b int, out c int, out d int) as $$ begin c := a; end; $$ language plpgsql; select * from plpgsql_check_function('fxx(int, int)'); plpgsql_check_function ------------------------------------------------- warning extra:00000:unused parameter "b" warning extra:00000:unmodified OUT variable "d" (2 rows) create or replace function fxx(in a int, in b int, out c int, out d int) as $$ begin c := d; end; $$ language plpgsql; select * from plpgsql_check_function('fxx(int, int)'); plpgsql_check_function ------------------------------------------------- warning extra:00000:unused parameter "a" warning extra:00000:unused parameter "b" warning extra:00000:unmodified OUT variable "d" (3 rows) create type ct as (a int, b int); create or replace function fxx(a ct, b ct, OUT c ct, OUT d ct) as $$ begin c.a := a.a; end; $$ language plpgsql; select * from plpgsql_check_function('fxx(ct, ct)'); plpgsql_check_function ----------------------------------------------------------------------- warning extra:00000:unused parameter "b" warning extra:00000:composite OUT variable "c" is not single argument warning extra:00000:composite OUT variable "d" is not single argument warning extra:00000:unmodified OUT variable "d" (4 rows) create or replace function fxx(a ct, b ct, OUT c ct, OUT d ct) as $$ begin c.a := d.a; end; $$ language plpgsql; select * from plpgsql_check_function('fxx(ct, ct)'); plpgsql_check_function ----------------------------------------------------------------------- warning extra:00000:unused parameter "a" warning extra:00000:unused parameter "b" warning extra:00000:composite OUT variable "c" is not single argument warning extra:00000:composite OUT variable "d" is not single argument warning extra:00000:unmodified OUT variable "d" (5 rows) create or replace function tx(a int) returns int as $$ declare a int; ax int; begin declare ax int; begin ax := 10; end; a := 10; return 20; end; $$ language plpgsql; select * from plpgsql_check_function('tx(int)'); plpgsql_check_function ------------------------------------------------------------------------------------------- warning:00000:3:statement block:parameter "a" is shadowed Detail: Local variable shadows function parameter. warning extra:00000:5:statement block:variable "ax" shadows a previously defined variable Hint: SET plpgsql.extra_warnings TO 'shadowed_variables' warning:00000:2:DECLARE:unused variable "ax" warning extra:00000:2:DECLARE:never read variable "a" warning extra:00000:4:DECLARE:never read variable "ax" warning extra:00000:unused parameter "a" (8 rows) create type xt as (a int, b int, c int); create or replace function fx_xt(out x xt) as $$ declare l xt; a int; begin return; end; $$ language plpgsql; select * from plpgsql_check_function('fx_xt()'); plpgsql_check_function ------------------------------------------------- warning:00000:2:DECLARE:unused variable "l" warning:00000:3:DECLARE:unused variable "a" warning extra:00000:unmodified OUT variable "x" (3 rows) drop function fx_xt(); create or replace function fx_xt(out x xt) as $$ declare l xt; a int; begin x.c := 1000; return; end; $$ language plpgsql; select * from plpgsql_check_function('fx_xt()'); plpgsql_check_function --------------------------------------------- warning:00000:2:DECLARE:unused variable "l" warning:00000:3:DECLARE:unused variable "a" (2 rows) drop function fx_xt(); create or replace function fx_xt(out x xt, out y xt) as $$ declare c1 xt; c2 xt; begin return; end; $$ language plpgsql; select * from plpgsql_check_function('fx_xt()'); plpgsql_check_function ----------------------------------------------------------------------- warning:00000:2:DECLARE:unused variable "c1" warning:00000:2:DECLARE:unused variable "c2" warning extra:00000:composite OUT variable "x" is not single argument warning extra:00000:unmodified OUT variable "x" warning extra:00000:composite OUT variable "y" is not single argument warning extra:00000:unmodified OUT variable "y" (6 rows) drop function fx_xt(); create or replace function fx_xt(out x xt, out y xt) as $$ declare c1 xt; c2 xt; begin x.a := 100; y := row(10,20,30); return; end; $$ language plpgsql; select * from plpgsql_check_function('fx_xt()'); plpgsql_check_function ----------------------------------------------------------------------- warning:00000:2:DECLARE:unused variable "c1" warning:00000:2:DECLARE:unused variable "c2" warning extra:00000:composite OUT variable "x" is not single argument warning extra:00000:composite OUT variable "y" is not single argument (4 rows) drop function fx_xt(); create or replace function fx_xt(out x xt, out z int) as $$ begin return; end; $$ language plpgsql; select * from plpgsql_check_function('fx_xt()'); plpgsql_check_function ----------------------------------------------------------------------- warning extra:00000:composite OUT variable "x" is not single argument warning extra:00000:unmodified OUT variable "x" warning extra:00000:unmodified OUT variable "z" (3 rows) drop function fx_xt(); drop type xt; -- missing RETURN create or replace function fx_flow() returns int as $$ begin raise notice 'kuku'; end; $$ language plpgsql; select fx_flow(); NOTICE: kuku ERROR: control reached end of function without RETURN CONTEXT: PL/pgSQL function fx_flow() select * from plpgsql_check_function('fx_flow()'); plpgsql_check_function ------------------------------------------------------------ error:2F005:control reached end of function without RETURN (1 row) -- ok create or replace function fx_flow() returns int as $$ declare a int; begin if a > 10 then return a; end if; return 10; end; $$ language plpgsql; select * from plpgsql_check_function('fx_flow()'); plpgsql_check_function ------------------------ (0 rows) -- dead code create or replace function fx_flow() returns int as $$ declare a int; begin if a > 10 then return a; else return a + 1; end if; return 10; end; $$ language plpgsql; select * from plpgsql_check_function('fx_flow()'); plpgsql_check_function ----------------------------------------------- warning extra:00000:9:RETURN:unreachable code (1 row) -- missing return create or replace function fx_flow() returns int as $$ declare a int; begin if a > 10 then return a; end if; end; $$ language plpgsql; select * from plpgsql_check_function('fx_flow()'); plpgsql_check_function -------------------------------------------------------------------- warning extra:2F005:control reached end of function without RETURN (1 row) drop function fx_flow(); create or replace function fx_flow(in p_param1 integer) returns text as $$ declare z1 text; begin if p_param1 is not null then z1 := '1111'; return z1; else z1 := '222222'; end if; return z1; end; $$ language plpgsql stable; select * from plpgsql_check_function_tb('fx_flow(integer)'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+---------+--------+------+-------+----------+-------+--------- (0 rows) create or replace function fx_flow(in p_param1 integer) returns text as $$ declare z1 text; begin if p_param1 is not null then z1 := '1111'; return z1; else z1 := '222222'; raise exception 'stop'; end if; return z1; end; $$ language plpgsql stable; select * from plpgsql_check_function_tb('fx_flow(integer)'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+------------------+--------+------+---------------+----------+-------+--------- fx_flow | 12 | RETURN | 00000 | unreachable code | | | warning extra | | | (1 row) drop function fx_flow(); ERROR: function fx_flow() does not exist drop function fx(int); ERROR: function fx(integer) does not exist create or replace function fx(x int) returns table(y int) as $$ begin return query select x union select x; end $$ language plpgsql; select * from fx(10); y ---- 10 (1 row) select * from plpgsql_check_function_tb('fx(int)'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+---------+--------+------+-------+----------+-------+--------- (0 rows) drop function fx(int); create or replace function fx(x int) returns table(y int, z int) as $$ begin return query select x,x+1 union select x, x+1; end $$ language plpgsql; select * from fx(10); y | z ----+---- 10 | 11 (1 row) select * from plpgsql_check_function_tb('fx(int)'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+---------+--------+------+-------+----------+-------+--------- (0 rows) drop function fx(int); create table xx(a int); create or replace function fx(x int) returns int as $$ declare _a int; begin begin select a from xx into strict _a where a = x; return _a; exception when others then null; end; return -1; end; $$ language plpgsql; select * from plpgsql_check_function_tb('fx(int)'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+---------+--------+------+-------+----------+-------+--------- (0 rows) drop table xx; create or replace function fx(x int) returns int as $$ begin begin if (x > 0) then raise exception 'xxx' using errcode = 'XX888'; else raise exception 'yyy' using errcode = 'YY888'; end if; return -1; -- dead code; end; return -1; end; $$ language plpgsql; select * from plpgsql_check_function_tb('fx(int)'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+------------------+--------+------+---------------+----------+-------+--------- fx | 9 | RETURN | 00000 | unreachable code | | | warning extra | | | fx | 11 | RETURN | 00000 | unreachable code | | | warning extra | | | (2 rows) create or replace function fx(x int) returns int as $$ begin begin if (x > 0) then raise exception 'xxx' using errcode = 'XX888'; else raise exception 'yyy' using errcode = 'YY888'; end if; exception when sqlstate 'XX888' then null; when sqlstate 'YY888' then null; end; end; -- missing return; $$ language plpgsql; select * from plpgsql_check_function_tb('fx(int)'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+------------------------------------------------+--------+------+-------+----------+-------+--------- fx | | | 2F005 | control reached end of function without RETURN | | | error | | | (1 row) create or replace function fx(x int) returns int as $$ begin begin if (x > 0) then raise exception 'xxx' using errcode = 'XX888'; else raise exception 'yyy' using errcode = 'YY888'; end if; exception when others then return 10; end; end; -- ok now $$ language plpgsql; select * from plpgsql_check_function_tb('fx(int)'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+---------+--------+------+-------+----------+-------+--------- (0 rows) --false alarm reported by Filip Zach create type testtype as (id integer); create or replace function fx() returns testtype as $$ begin return row(1); end; $$ language plpgsql; select * from fx(); id ---- 1 (1 row) select fx(); fx ----- (1) (1 row) select * from plpgsql_check_function('fx()'); plpgsql_check_function ------------------------ (0 rows) drop function fx(); create function out1(OUT f1 int, OUT f2 int) returns setof record as $$ begin for f1, f2 in execute $q$ select 1, 2 $q$ loop return next; end loop; end $$ language plpgsql; select * from plpgsql_check_function('out1()'); plpgsql_check_function ------------------------ (0 rows) drop function out1(); create function out1(OUT f1 int, OUT f2 int) returns setof record as $$ begin for f1, f2 in select 1, 2 loop return next; end loop; end $$ language plpgsql; select * from plpgsql_check_function('out1()'); plpgsql_check_function ------------------------ (0 rows) drop function out1(); -- never read variable detection create function a() returns int as $$ declare foo int; begin foo := 2; return 1; end; $$ language plpgsql; select * from plpgsql_check_function('a()'); plpgsql_check_function --------------------------------------------------------- warning extra:00000:2:DECLARE:never read variable "foo" (1 row) drop function a(); -- issue #29 false unused variable create or replace function f1(in p_cursor refcursor) returns void as $body$ declare z_offset integer; begin z_offset := 10; move absolute z_offset from p_cursor; end; $body$ language 'plpgsql' stable; select * from plpgsql_check_function_tb('f1(refcursor)'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+---------+--------+------+-------+----------+-------+--------- (0 rows) drop function f1(refcursor); -- issue #30 segfault due NULL refname create or replace function test(a varchar) returns void as $$ declare x cursor (_a varchar) for select _a; begin open x(a); end; $$ language plpgsql; select * from plpgsql_check_function_tb('test(varchar)'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+-------------------------+--------+------+---------------+----------+-------+--------- test | 2 | DECLARE | 00000 | never read variable "x" | | | warning extra | | | (1 row) drop function test(varchar); create or replace function test() returns void as $$ declare x numeric; begin x := NULL; end; $$ language plpgsql; select * from plpgsql_check_function('test()'); plpgsql_check_function ------------------------------------------------------- warning extra:00000:2:DECLARE:never read variable "x" (1 row) drop function test(); create table testtable(a int); create or replace function test() returns int as $$ declare r testtable; begin select * into r from testtable; return r.a; end; $$ language plpgsql; select * from plpgsql_check_function('test()'); plpgsql_check_function ------------------------ (0 rows) set check_function_bodies to on; drop table testtable; create table testtable(a int, b int); create or replace function test() returns int as $$ declare r testtable; begin select * into r from testtable; return r.a; end; $$ language plpgsql; alter table testtable drop column b; -- expected false alarm on PostgreSQL 10 and older -- there is not possibility to enforce recompilation -- before checking. select * from plpgsql_check_function('test()'); plpgsql_check_function ------------------------ (0 rows) drop function test(); -- issue #32 create table bigtable(id bigint, v varchar); create or replace function test() returns void as $$ declare r record; _id numeric; begin select * into r from bigtable where id = _id; for r in select * from bigtable where _id = id loop end loop; if (exists(select * from bigtable where id = _id)) then end if; end; $$ language plpgsql; select test(); test ------ (1 row) -- should to show performance warnings select * from plpgsql_check_function('test()', performance_warnings := true); plpgsql_check_function ------------------------------------------------------------------------------------------------------------------------------- performance:42804:6:SQL statement:implicit cast of attribute caused by different PLpgSQL variable type in WHERE clause Query: select * from bigtable where id = _id -- ^ Detail: An index of some attribute cannot be used, when variable, used in predicate, has not right type like a attribute Hint: Check a variable type - int versus numeric performance:42804:7:FOR over SELECT rows:implicit cast of attribute caused by different PLpgSQL variable type in WHERE clause Query: select * from bigtable where _id = id -- ^ Detail: An index of some attribute cannot be used, when variable, used in predicate, has not right type like a attribute Hint: Check a variable type - int versus numeric performance:42804:10:IF:implicit cast of attribute caused by different PLpgSQL variable type in WHERE clause Query: (exists(select * from bigtable where id = _id)) -- ^ Detail: An index of some attribute cannot be used, when variable, used in predicate, has not right type like a attribute Hint: Check a variable type - int versus numeric warning extra:00000:3:DECLARE:never read variable "r" (16 rows) create or replace function test() returns void as $$ declare r record; _id bigint; begin select * into r from bigtable where id = _id; for r in select * from bigtable where _id = id loop end loop; if (exists(select * from bigtable where id = _id)) then end if; end; $$ language plpgsql; -- there are not any performance issue now select * from plpgsql_check_function('test()', performance_warnings := true); plpgsql_check_function ------------------------------------------------------- warning extra:00000:3:DECLARE:never read variable "r" (1 row) -- nextval, currval and setval test create table test_table(); create or replace function testseq() returns void as $$ begin perform nextval('test_table'); perform currval('test_table'); perform setval('test_table', 10); perform setval('test_table', 10, true); end; $$ language plpgsql; -- should to fail select testseq(); ERROR: cannot open relation "test_table" DETAIL: This operation is not supported for tables. CONTEXT: SQL statement "SELECT nextval('test_table')" PL/pgSQL function testseq() line 3 at PERFORM select * from plpgsql_check_function('testseq()', fatal_errors := false); plpgsql_check_function ------------------------------------------------------ error:42809:3:PERFORM:"test_table" is not a sequence Query: SELECT nextval('test_table') -- ^ error:42809:4:PERFORM:"test_table" is not a sequence Query: SELECT currval('test_table') -- ^ error:42809:5:PERFORM:"test_table" is not a sequence Query: SELECT setval('test_table', 10) -- ^ error:42809:6:PERFORM:"test_table" is not a sequence Query: SELECT setval('test_table', 10, true) -- ^ (12 rows) drop function testseq(); drop table test_table; -- tests designed for PostgreSQL 9.2 set check_function_bodies to off; create table t1(a int, b int); create function f1() returns void as $$ begin if false then update t1 set c = 30; end if; if false then raise notice '%', r.c; end if; end; $$ language plpgsql; select f1(); f1 ---- (1 row) select * from plpgsql_check_function_tb('f1()', fatal_errors := true); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+---------------+----------+--------------------------------------------+--------+------+-------+----------+----------------------+--------- f1 | 4 | SQL statement | 42703 | column "c" of relation "t1" does not exist | | | error | 15 | update t1 set c = 30 | (1 row) select * from plpgsql_check_function_tb('f1()', fatal_errors := false); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+---------------+----------+--------------------------------------------+--------+------+-------+----------+----------------------+--------- f1 | 4 | SQL statement | 42703 | column "c" of relation "t1" does not exist | | | error | 15 | update t1 set c = 30 | f1 | 7 | RAISE | 42P01 | missing FROM-clause entry for table "r" | | | error | 1 | r.c | (2 rows) select * from plpgsql_check_function_tb('f1()'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+---------------+----------+--------------------------------------------+--------+------+-------+----------+----------------------+--------- f1 | 4 | SQL statement | 42703 | column "c" of relation "t1" does not exist | | | error | 15 | update t1 set c = 30 | (1 row) select f1(); f1 ---- (1 row) drop function f1(); create or replace function f1() returns void as $$ begin if false then raise notice '%', 1, 2; end if; end; $$ language plpgsql; select f1(); ERROR: too many parameters specified for RAISE CONTEXT: compilation of PL/pgSQL function "f1" near line 4 select * from plpgsql_check_function_tb('f1()'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+-----------------------------------------+--------+------+-------+----------+-------+--------------------------------------------------- f1 | | | 42601 | too many parameters specified for RAISE | | | error | | | compilation of PL/pgSQL function "f1" near line 4 (1 row) select f1(); ERROR: too many parameters specified for RAISE CONTEXT: compilation of PL/pgSQL function "f1" near line 4 drop function f1(); create or replace function f1() returns void as $$ begin if false then raise notice '% %'; end if; end; $$ language plpgsql; select f1(); ERROR: too few parameters specified for RAISE CONTEXT: compilation of PL/pgSQL function "f1" near line 4 select * from plpgsql_check_function_tb('f1()'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+----------------------------------------+--------+------+-------+----------+-------+--------------------------------------------------- f1 | | | 42601 | too few parameters specified for RAISE | | | error | | | compilation of PL/pgSQL function "f1" near line 4 (1 row) select f1(); ERROR: too few parameters specified for RAISE CONTEXT: compilation of PL/pgSQL function "f1" near line 4 drop function f1(); create or replace function ml_trg() returns trigger as $$ #option dump declare begin if TG_OP = 'INSERT' then if NEW.status_from IS NULL then begin -- performance issue only select status into NEW.status_from from pa where pa_id = NEW.pa_id; -- nonexist target value select status into NEW.status_from_xxx from pa where pa_id = NEW.pa_id; exception when DATA_EXCEPTION then new.status_from := 'DE'; end; end if; end if; if TG_OP = 'DELETE' then return OLD; else return NEW; end if; exception when OTHERS then NULL; if TG_OP = 'DELETE' then return OLD; else return NEW; end if; end; $$ language plpgsql; select * from plpgsql_check_function('ml_trg()', 'ml', performance_warnings := true); plpgsql_check_function -------------------------------------------------------------------------- error:42703:13:SQL statement:record "new" has no field "status_from_xxx" (1 row) create or replace function fx2() returns void as $$ declare _pa pa; begin select pa.id into _pa.id from pa limit 1; select pa.pa_id into _pa.pa_id from pa limit 1; end; $$ language plpgsql; select * from plpgsql_check_function('fx2()', performance_warnings := true); plpgsql_check_function --------------------------------------------------------- warning extra:00000:2:DECLARE:never read variable "_pa" (1 row) drop function fx2(); create or replace function fx2() returns void as $$ declare _pa pa; begin _pa.id := (select pa.id from pa limit 1); _pa.pa_id := (select pa.pa_id from pa limit 1); end; $$ language plpgsql; select * from plpgsql_check_function('fx2()', performance_warnings := true); plpgsql_check_function --------------------------------------------------------- warning extra:00000:2:DECLARE:never read variable "_pa" (1 row) drop function fx2(); create or replace function test_lab() returns void as $$ begin <> for a in 1..3 loop <> BEGIN <> for b in 8..9 loop if a=2 then continue sub; end if; raise notice '% %', a, b; end loop inner; END sub; end loop outer; end; $$ language plpgsql; select test_lab(); ERROR: block label "sub" cannot be used in CONTINUE LINE 10: continue sub; ^ QUERY: begin <> for a in 1..3 loop <> BEGIN <> for b in 8..9 loop if a=2 then continue sub; end if; raise notice '% %', a, b; end loop inner; END sub; end loop outer; end; CONTEXT: compilation of PL/pgSQL function "test_lab" near line 10 select * from plpgsql_check_function('test_lab()', performance_warnings := true); plpgsql_check_function ------------------------------------------------------------------- error:42601:block label "sub" cannot be used in CONTINUE Query: begin <> for a in 1..3 loop <> BEGIN <> for b in 8..9 loop if a=2 then continue sub; -- ^ end if; raise notice '% %', a, b; end loop inner; END sub; end loop outer; end; Context: compilation of PL/pgSQL function "test_lab" near line 10 (20 rows) create or replace function test_lab() returns void as $$ begin continue; end; $$ language plpgsql; select test_lab(); ERROR: CONTINUE cannot be used outside a loop LINE 3: continue; ^ QUERY: begin continue; end; CONTEXT: compilation of PL/pgSQL function "test_lab" near line 3 select * from plpgsql_check_function('test_lab()', performance_warnings := true); plpgsql_check_function ------------------------------------------------------------------ error:42601:CONTINUE cannot be used outside a loop Query: begin continue; -- ^ end; Context: compilation of PL/pgSQL function "test_lab" near line 3 (8 rows) create type _exception_type as ( state text, message text, detail text); create or replace function f1() returns void as $$ declare _exception record; begin _exception := NULL::_exception_type; exception when others then get stacked diagnostics _exception.state = RETURNED_SQLSTATE, _exception.message = MESSAGE_TEXT, _exception.detail = PG_EXCEPTION_DETAIL, _exception.hint = PG_EXCEPTION_HINT; end; $$ language plpgsql; select f1(); f1 ---- (1 row) select * from plpgsql_check_function('f1()'); plpgsql_check_function ------------------------------------------------------------------------------- error:42703:7:GET STACKED DIAGNOSTICS:record "_exception" has no field "hint" (1 row) drop function f1(); drop type _exception_type; drop table t1; create function myfunc1(a int, b float) returns integer as $$ begin end $$ language plpgsql; create function myfunc2(a int, b float) returns integer as $$ begin end $$ language plpgsql; create function myfunc3(a int, b float) returns integer as $$ begin end $$ language plpgsql; create function myfunc4(a int, b float) returns integer as $$ begin end $$ language plpgsql; create function opfunc1(a int, b float) returns integer as $$ begin end $$ language plpgsql; create operator *** (procedure = opfunc1, leftarg = int, rightarg = float); create table mytable(a int); create table myview as select * from mytable; create function testfunc(a int, b float) returns void as $$ declare x integer; begin raise notice '%', myfunc1(a, b); x := myfunc2(a, b) operator(public.***) 1; perform myfunc3(m.a, b) from myview m; insert into mytable select myfunc4(a, b); end; $$ language plpgsql; select * from plpgsql_check_function('testfunc(int,float)'); plpgsql_check_function ------------------------------------------------------- warning extra:00000:2:DECLARE:never read variable "x" (1 row) select type, schema, name, params from plpgsql_show_dependency_tb('testfunc(int,float)'); type | schema | name | params ----------+--------+---------+---------------------------- FUNCTION | public | myfunc1 | (integer,double precision) FUNCTION | public | myfunc2 | (integer,double precision) FUNCTION | public | myfunc3 | (integer,double precision) FUNCTION | public | myfunc4 | (integer,double precision) OPERATOR | public | *** | (integer,double precision) RELATION | public | mytable | RELATION | public | myview | (7 rows) drop function testfunc(int, float); drop function myfunc1(int, float); drop function myfunc2(int, float); drop function myfunc3(int, float); drop function myfunc4(int, float); drop table mytable; drop view myview; ERROR: "myview" is not a view HINT: Use DROP TABLE to remove a table. -- issue #221 create type foo_type as (a int); create table foo(a int); create function testfuncdep() returns void as $$ declare x foo_type; y foo; begin end; $$ language plpgsql; select type, schema, name from plpgsql_show_dependency_tb('testfuncdep'); type | schema | name ----------+--------+---------- RELATION | public | foo TYPE | public | foo_type (2 rows) drop function testfuncdep(); drop type foo_type; drop table foo; -- issue #221 - dependency report on trigger function should not crash -- (NEW and OLD record variables have no datatype) create table foo(a int, b text); create function foo_trg_func() returns trigger as $$ begin new.b := 'x'; return new; end; $$ language plpgsql; create trigger foo_trg before insert on foo for each row execute function foo_trg_func(); select type, schema, name from plpgsql_show_dependency_tb('foo_trg_func', 'foo'); type | schema | name ------+--------+------ (0 rows) drop trigger foo_trg on foo; drop function foo_trg_func(); drop table foo; -- issue #34 create or replace function testcase() returns bool as $$ declare x int; begin set local search_path to public, test; case x when 1 then return true; else return false; end case; end; $$ language plpgsql; -- should not to raise warning select * from plpgsql_check_function('testcase()'); plpgsql_check_function ------------------------ (0 rows) drop function testcase(); -- Adam's Bartoszewicz example create or replace function public.test12() returns refcursor language plpgsql as $body$ declare rc refcursor; begin open rc scroll for select pc.* from pg_cast pc; return rc; end; $body$; -- should not returns false alarm select * from plpgsql_check_function('test12()'); plpgsql_check_function ------------------------ (0 rows) drop function public.test12(); -- should to show performance warning on bad flag create or replace function flag_test1(int) returns int as $$ begin return $1 + 10; end; $$ language plpgsql stable; create table fufu(a int); create or replace function flag_test2(int) returns int as $$ begin return (select * from fufu limit 1); end; $$ language plpgsql volatile; select * from plpgsql_check_function('flag_test1(int)', performance_warnings := true); plpgsql_check_function ----------------------------------------------------------------------------------------- performance:00000:routine is marked as STABLE, should be IMMUTABLE Hint: When you fix this issue, please, recheck other functions that uses this function. (2 rows) select * from plpgsql_check_function('flag_test2(int)', performance_warnings := true); plpgsql_check_function ----------------------------------------------------------------------------------------- warning extra:00000:unused parameter "$1" performance:00000:routine is marked as VOLATILE, should be STABLE Hint: When you fix this issue, please, recheck other functions that uses this function. (3 rows) drop table fufu; drop function flag_test1(int); drop function flag_test2(int); create or replace function rrecord01() returns setof record as $$ begin return query select 1,2; end; $$ language plpgsql; create or replace function rrecord02() returns record as $$ begin return row(10,20,30); end; $$ language plpgsql; create type record03 as (a int, b int); create or replace function rrecord03() returns record03 as $$ declare r record; begin r := row(1); return r; end; $$ language plpgsql; -- should not to raise false alarms select * from plpgsql_check_function('rrecord01'); plpgsql_check_function ------------------------ (0 rows) select * from plpgsql_check_function('rrecord02'); plpgsql_check_function ------------------------ (0 rows) -- should detect different return but still detect return select * from plpgsql_check_function('rrecord03', fatal_errors => false); plpgsql_check_function ---------------------------------------------------------------------------------- error:42804:5:RETURN:returned record type does not match expected record type Detail: Number of returned columns (1) does not match expected column count (2). (2 rows) drop function rrecord01(); drop function rrecord02(); drop function rrecord03(); drop type record03; create or replace function bugfunc01() returns void as $$ declare cvar cursor(a int, b int) for select a + b from generate_series(1,b); begin for t in cvar(1,3) loop raise notice '%', t; end loop; end; $$ language plpgsql; select bugfunc01(); NOTICE: (4) NOTICE: (4) NOTICE: (4) bugfunc01 ----------- (1 row) select * from plpgsql_check_function('bugfunc01'); plpgsql_check_function ------------------------ (0 rows) create or replace function bugfunc02() returns void as $$ declare cvar cursor(a int, b int) for select a + b from generate_series(1,b); begin open cvar(10,20); close cvar; end; $$ language plpgsql; select bugfunc02(); bugfunc02 ----------- (1 row) select * from plpgsql_check_function('bugfunc02'); plpgsql_check_function ------------------------ (0 rows) create or replace function bugfunc03() returns void as $$ declare cvar cursor(a int, b int) for select a + b from not_exists_table; begin open cvar(10,20); close cvar; end; $$ language plpgsql; select bugfunc03(); ERROR: relation "not_exists_table" does not exist LINE 1: select a + b from not_exists_table ^ QUERY: select a + b from not_exists_table CONTEXT: PL/pgSQL function bugfunc03() line 5 at OPEN select * from plpgsql_check_function('bugfunc03'); plpgsql_check_function --------------------------------------------------------------- error:42P01:5:OPEN:relation "not_exists_table" does not exist Query: select a + b from not_exists_table -- ^ (3 rows) create or replace function f1(out cr refcursor) as $$ begin end; $$ language plpgsql; -- should to raise warning select * from plpgsql_check_function('f1()'); plpgsql_check_function -------------------------------------------------- warning extra:00000:unmodified OUT variable "cr" (1 row) create or replace function f1(out cr refcursor) as $$ begin open cr for select 1; end; $$ language plpgsql; -- should not to raise warning, see issue #43 select * from plpgsql_check_function('f1()'); plpgsql_check_function ------------------------ (0 rows) drop function f1(); create table testt(a int); create or replace function testt_trg_func() returns trigger as $$ begin return new; end; $$ language plpgsql; create trigger testt_trg before insert or update on testt for each row execute procedure testt_trg_func(); create or replace function maintaince_function() returns void as $$ begin alter table testt disable trigger testt_trg; alter table testt enable trigger testt_trg; end; $$ language plpgsql; -- should not to crash select * from plpgsql_check_function_tb('maintaince_function()', 0, true, true, true); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+---------+--------+------+-------+----------+-------+--------- (0 rows) drop function maintaince_function(); drop trigger testt_trg on testt; drop function testt_trg_func(); drop table testt; create or replace function test_crash() returns void as $$ declare ec int default buggyfunc(10); begin select * into ec from buggytab; end; $$ language plpgsql; -- should not to crash select * from plpgsql_check_function('test_crash', fatal_errors := false); plpgsql_check_function --------------------------------------------------------------------------------------------------------- error:42883:4:statement block:function buggyfunc(integer) does not exist Query: buggyfunc(10) -- ^ Hint: No function matches the given name and argument types. You might need to add explicit type casts. Context: during statement block local variable "ec" initialization on line 3 error:42P01:5:SQL statement:relation "buggytab" does not exist Query: select * from buggytab -- ^ warning extra:00000:3:DECLARE:never read variable "ec" (9 rows) select * from plpgsql_check_function('test_crash', fatal_errors := true); plpgsql_check_function --------------------------------------------------------------------------------------------------------- error:42883:4:statement block:function buggyfunc(integer) does not exist Query: buggyfunc(10) -- ^ Hint: No function matches the given name and argument types. You might need to add explicit type casts. Context: during statement block local variable "ec" initialization on line 3 (5 rows) drop function test_crash(); -- fix false alarm reported by Piotr Stepniewski create or replace function public.fx() returns void language plpgsql as $function$ begin raise exception 'xxx'; end; $function$; -- show raise nothing select * from plpgsql_check_function('fx()'); plpgsql_check_function ------------------------ (0 rows) create table errtab( message text, code character(5) ); create or replace function public.fx() returns void language plpgsql as $function$ declare var errtab%rowtype; begin raise exception using message = var.message, errcode = var.code; end; $function$; -- should not to crash select * from plpgsql_check_function('fx()'); plpgsql_check_function ------------------------ (0 rows) create or replace function public.fx() returns void language plpgsql as $function$ declare var errtab%rowtype; begin raise exception using message = var.message, errcode = var.code, hint = var.hint; end; $function$; -- should not to crash select * from plpgsql_check_function('fx()'); plpgsql_check_function ------------------------------------------------------ error:42703:5:RAISE:record "var" has no field "hint" Context: SQL expression "var.hint" (2 rows) drop function fx(); create or replace function foo_format(a text, b text) returns void as $$ declare s text; begin s := format('%s'); -- should to raise error s := format('%s %10s', a, b); -- should be ok s := format('%s %s', a, b, a); -- should to raise warning s := format('%s %d', a, b); -- should to raise error raise notice '%', s; end; $$ language plpgsql; select * from plpgsql_check_function('foo_format', fatal_errors := false); plpgsql_check_function ------------------------------------------------------------------- error:22023:4:assignment:too few arguments for format() Query: s := format('%s') -- ^ Context: at assignment to variable "s" declared on line 2 warning:00000:6:assignment:unused parameters of function "format" Query: s := format('%s %s', a, b, a) -- ^ Context: at assignment to variable "s" declared on line 2 error:22023:7:assignment:unrecognized format() type specifier "d" Query: s := format('%s %d', a, b) -- ^ Context: at assignment to variable "s" declared on line 2 (12 rows) drop function foo_format(text, text); create or replace function dyn_sql_1() returns void as $$ declare v varchar; n int; begin execute 'select ' || n; -- ok execute 'select ' || quote_literal(v); -- ok execute 'select ' || v; -- vulnerable execute format('select * from %I', v); -- ok execute format('select * from %s', v); -- vulnerable execute 'select $1' using v; -- ok execute 'select 1'; -- ok execute 'select 1' using v; -- warning execute 'select $1'; -- error end; $$ language plpgsql; select * from plpgsql_check_function('dyn_sql_1', security_warnings := true, fatal_errors := false); plpgsql_check_function ------------------------------------------------------------------------------------------ security:00000:8:EXECUTE:text type variable is not sanitized Query: 'select ' || v -- ^ Detail: The EXECUTE expression is SQL injection vulnerable. Hint: Use quote_ident, quote_literal or format function to secure variable. security:00000:10:EXECUTE:text type variable is not sanitized Query: format('select * from %s', v) -- ^ Detail: The EXECUTE expression is SQL injection vulnerable. Hint: Use quote_ident, quote_literal or format function to secure variable. warning:00000:13:EXECUTE:values passed to EXECUTE statement by USING clause was not used error:42P02:14:EXECUTE:there is no parameter $1 Query: select $1 -- ^ (14 rows) drop function dyn_sql_1(); create type tp as (a int, b int); create or replace function dyn_sql_2() returns void as $$ declare r tp; result int; begin select 10 a, 20 b into r; raise notice '%', r.a; execute 'select $1.a + $1.b' into result using r; execute 'select $1.c' into result using r; -- error raise notice '%', result; end; $$ language plpgsql; select * from plpgsql_check_function('dyn_sql_2', security_warnings := true); plpgsql_check_function ------------------------------------------------------------ error:42703:9:EXECUTE:column "c" not found in data type tp Query: select $1.c -- ^ (3 rows) drop function dyn_sql_2(); drop type tp; /* * Should not to work * * note: plpgsql doesn't support passing some necessary details for record * type. The parser setup for dynamic SQL column doesn't use ref hooks, and * then it cannot to pass TupleDesc info to query anyway. */ create or replace function dyn_sql_2() returns void as $$ declare r record; result int; begin select 10 a, 20 b into r; raise notice '%', r.a; execute 'select $1.a + $1.b' into result using r; raise notice '%', result; end; $$ language plpgsql; select dyn_sql_2(); --should to fail NOTICE: 10 ERROR: could not identify column "a" in record data type LINE 1: select $1.a + $1.b ^ QUERY: select $1.a + $1.b CONTEXT: PL/pgSQL function dyn_sql_2() line 8 at EXECUTE select * from plpgsql_check_function('dyn_sql_2', security_warnings := true); plpgsql_check_function ------------------------------------------------------------------------- error:42703:8:EXECUTE:could not identify column "a" in record data type Query: select $1.a + $1.b -- ^ (3 rows) drop function dyn_sql_2(); create or replace function dyn_sql_3() returns void as $$ declare r int; begin execute 'select $1' into r using 1; raise notice '%', r; end $$ language plpgsql; select dyn_sql_3(); NOTICE: 1 dyn_sql_3 ----------- (1 row) -- should be ok select * from plpgsql_check_function('dyn_sql_3'); plpgsql_check_function ------------------------ (0 rows) create or replace function dyn_sql_3() returns void as $$ declare r record; begin execute 'select $1 as a, $2 as b' into r using 1, 2; raise notice '% %', r.a, r.b; end $$ language plpgsql; select dyn_sql_3(); NOTICE: 1 2 dyn_sql_3 ----------- (1 row) -- should be ok select * from plpgsql_check_function('dyn_sql_3'); plpgsql_check_function ------------------------ (0 rows) create or replace function dyn_sql_3() returns void as $$ declare r record; begin execute 'create table foo(a int)' into r using 1, 2; raise notice '% %', r.a, r.b; end $$ language plpgsql; -- raise a error select * from plpgsql_check_function('dyn_sql_3'); plpgsql_check_function ----------------------------------------------------------------------------------------- warning:00000:4:EXECUTE:values passed to EXECUTE statement by USING clause was not used error:XX000:4:EXECUTE:expression does not return data (2 rows) create or replace function dyn_sql_3() returns void as $$ declare r1 int; r2 int; begin execute 'select 1' into r1, r2 using 1, 2; raise notice '% %', r1, r2; end $$ language plpgsql; -- raise a error select * from plpgsql_check_function('dyn_sql_3'); plpgsql_check_function ----------------------------------------------------------------------------------------- warning:00000:4:EXECUTE:values passed to EXECUTE statement by USING clause was not used warning:00000:4:EXECUTE:too few attributes for target variables Query: select 1 -- ^ Detail: There are more target variables than output columns in query. Hint: Check target variables in SELECT INTO statement. Context: at EXECUTE to r1, r2 variables (7 rows) drop function dyn_sql_3(); create or replace function dyn_sql_3() returns void as $$ declare r record; begin for r in execute 'select 1 as a, 2 as b' loop raise notice '%', r.a; end loop; end $$ language plpgsql; -- should be ok select * from plpgsql_check_function('dyn_sql_3'); plpgsql_check_function ------------------------ (0 rows) drop function dyn_sql_3(); create or replace function dyn_sql_3() returns void as $$ declare r record; begin for r in execute 'select 1 as a, 2 as b' loop raise notice '%', r.c; end loop; end $$ language plpgsql; -- should be error select * from plpgsql_check_function('dyn_sql_3'); plpgsql_check_function ------------------------------------------------- error:42703:6:RAISE:record "r" has no field "c" Context: SQL expression "r.c" (2 rows) drop function dyn_sql_3(); create or replace function dyn_sql_3() returns void as $$ declare r record; v text = 'select 10 a, 20 b't; begin for r in execute v loop raise notice '%', r.a; end loop; end $$ language plpgsql; -- should be ok select * from plpgsql_check_function('dyn_sql_3'); plpgsql_check_function ------------------------ (0 rows) drop function dyn_sql_3(); create or replace function dyn_sql_4() returns table(ax int, bx int) as $$ begin return query execute 'select 10, 20'; return; end; $$ language plpgsql; -- should be ok select * from plpgsql_check_function('dyn_sql_4()'); plpgsql_check_function ------------------------ (0 rows) create or replace function dyn_sql_4() returns table(ax int, bx int) as $$ begin return query execute 'select 10, 20, 30'; return; end; $$ language plpgsql; select * from dyn_sql_4(); ERROR: structure of query does not match function result type DETAIL: Number of returned columns (3) does not match expected column count (2). CONTEXT: SQL statement "select 10, 20, 30" PL/pgSQL function dyn_sql_4() line 3 at RETURN QUERY -- should be error select * from plpgsql_check_function('dyn_sql_4()'); plpgsql_check_function ----------------------------------------------------------------------------------- error:42804:3:RETURN QUERY:structure of query does not match function result type Detail: Number of returned columns (3) does not match expected column count (2). (2 rows) drop function dyn_sql_4(); create or replace function test_bug(text) returns regproc as $$ begin return $1::regproc; exception when undefined_function or invalid_name then raise; end; $$ language plpgsql; -- should not raise a exception select * from plpgsql_check_function('test_bug'); plpgsql_check_function ------------------------ (0 rows) create or replace function test_bug(text) returns regproc as $$ begin return $1::regproc; exception when undefined_function or invalid_name then raise notice '%', $1; -- bug end; $$ language plpgsql; select test_bug('kuku'); -- should to fail NOTICE: kuku ERROR: control reached end of function without RETURN CONTEXT: PL/pgSQL function test_bug(text) select * from plpgsql_check_function('test_bug'); plpgsql_check_function -------------------------------------------------------------------- warning extra:2F005:control reached end of function without RETURN (1 row) drop function test_bug(text); create or replace function test_bug(text) returns regproc as $$ begin return $1::regproc; exception when undefined_function or invalid_name then raise notice '%', $1; return NULL; end; $$ language plpgsql; select test_bug('kuku'); -- should be ok NOTICE: kuku test_bug ---------- (1 row) select * from plpgsql_check_function('test_bug'); plpgsql_check_function ------------------------ (0 rows) drop function test_bug(text); create or replace function foo(a text, b text) returns void as $$ begin -- unsecure execute 'select ' || a; a := quote_literal(a); -- is safe now execute 'select ' || a; a := a || b; -- it is unsecure again execute 'select ' || a; end; $$ language plpgsql; \sf+ foo(text, text) CREATE OR REPLACE FUNCTION public.foo(a text, b text) RETURNS void LANGUAGE plpgsql 1 AS $function$ 2 begin 3 -- unsecure 4 execute 'select ' || a; 5 a := quote_literal(a); -- is safe now 6 execute 'select ' || a; 7 a := a || b; -- it is unsecure again 8 execute 'select ' || a; 9 end; 10 $function$ -- should to raise two warnings select * from plpgsql_check_function('foo', security_warnings := true); plpgsql_check_function ----------------------------------------------------------------------------- security:00000:4:EXECUTE:text type variable is not sanitized Query: 'select ' || a -- ^ Detail: The EXECUTE expression is SQL injection vulnerable. Hint: Use quote_ident, quote_literal or format function to secure variable. security:00000:8:EXECUTE:text type variable is not sanitized Query: 'select ' || a -- ^ Detail: The EXECUTE expression is SQL injection vulnerable. Hint: Use quote_ident, quote_literal or format function to secure variable. (10 rows) drop function foo(text, text); create table testr(a int); create rule testr_rule as on insert to testr do nothing; create or replace function fx_testr() returns void as $$ begin insert into testr values(20); end; $$ language plpgsql; -- allow some rules on tables select fx_testr(); fx_testr ---------- (1 row) select * from plpgsql_check_function_tb('fx_testr'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+---------+--------+------+-------+----------+-------+--------- (0 rows) drop function fx_testr(); drop table testr; create or replace function f() returns void as $$ declare r1 record; r2 record; begin select 10 as a, 20 as b into r1; r2 := json_populate_record(r1, '{}'); raise notice '%', r2.a; end; $$ language plpgsql; select * from plpgsql_check_function('f'); plpgsql_check_function ------------------------ (0 rows) -- fix issue #63 create or replace function distinct_array(arr anyarray) returns anyarray as $$ begin return array(select distinct e from unnest(arr) as e); end; $$ language plpgsql immutable; select plpgsql_check_function('distinct_array(anyarray)'); plpgsql_check_function ------------------------ (0 rows) drop function distinct_array(anyarray); -- tracer test set plpgsql_check.enable_tracer to on; set plpgsql_check.tracer to on; set plpgsql_check.tracer_test_mode = true; -- should not crash do $$ begin raise exception 'raising an exception'; end; $$; NOTICE: #0 ->> start of block inline_code_block (oid=0, tnl=1) NOTICE: #0 <<- end of block (elapsed time=0.010 ms) aborted ERROR: raising an exception CONTEXT: PL/pgSQL function inline_code_block line 3 at RAISE do $$ begin do $_$ begin raise exception 'raising an exception'; end $_$; end; $$; NOTICE: #0 ->> start of block inline_code_block (oid=0, tnl=1) NOTICE: #2 ->> start of block inline_code_block (oid=0, tnl=1) NOTICE: #2 context: SQL statement "do $_$ begin raise exception 'raising an exception'; end $_$" NOTICE: #2 <<- end of block (elapsed time=0.010 ms) aborted NOTICE: #0 <<- end of block (elapsed time=0.010 ms) aborted ERROR: raising an exception CONTEXT: PL/pgSQL function inline_code_block line 3 at RAISE SQL statement "do $_$ begin raise exception 'raising an exception'; end $_$" PL/pgSQL function inline_code_block line 3 at DO create function fx_trace(int) returns int as $$ begin if $1 = 0 then raise exception 'arg is zero'; else return $1; end if; end; $$ language plpgsql; do $$ declare t int; begin t := fx_trace(1); end $$; NOTICE: #0 ->> start of block inline_code_block (oid=0, tnl=1) NOTICE: #1 ->> start of function fx_trace(integer) (oid=0, tnl=1) NOTICE: #1 context: PL/pgSQL function inline_code_block line 4 at assignment NOTICE: #1 "$1" => '1' NOTICE: #1 <<- end of function fx_trace (elapsed time=0.010 ms) NOTICE: #0 <<- end of block (elapsed time=0.010 ms) do $$ declare t int; begin t := fx_trace(0); end $$; NOTICE: #0 ->> start of block inline_code_block (oid=0, tnl=1) NOTICE: #1 ->> start of function fx_trace(integer) (oid=0, tnl=1) NOTICE: #1 context: PL/pgSQL function inline_code_block line 4 at assignment NOTICE: #1 "$1" => '0' NOTICE: #1 <<- end of function fx_trace (elapsed time=0.010 ms) aborted NOTICE: #0 <<- end of block (elapsed time=0.010 ms) aborted ERROR: arg is zero CONTEXT: PL/pgSQL function fx_trace(integer) line 4 at RAISE PL/pgSQL function inline_code_block line 4 at assignment drop function fx_trace; \set VERBOSITY terse create or replace function fxo(a int, b int, c date, d numeric) returns void as $$ begin insert into tracer_tab values(a,b,c,d); end; $$ language plpgsql; create table tracer_tab(a int, b int, c date, d numeric); create or replace function tracer_tab_trg_fx() returns trigger as $$ begin return new; end; $$ language plpgsql; create trigger tracer_tab_trg before insert on tracer_tab for each row execute procedure tracer_tab_trg_fx(); select fxo(10,20,'20200815', 3.14); NOTICE: #0 ->> start of function fxo(integer,integer,date,numeric) (oid=0, tnl=1) NOTICE: #0 "a" => '10', "b" => '20', "c" => '08-15-2020', "d" => '3.14' NOTICE: #2 ->> start of function tracer_tab_trg_fx() (oid=0, tnl=1) NOTICE: #2 context: SQL statement "insert into tracer_tab values(a,b,c,d)" NOTICE: #2 triggered by before row insert trigger NOTICE: #2 "new" => '(10,20,08-15-2020,3.14)' NOTICE: #2 <<- end of function tracer_tab_trg_fx (elapsed time=0.010 ms) NOTICE: #0 <<- end of function fxo (elapsed time=0.010 ms) fxo ----- (1 row) select fxo(11,21,'20200816', 6.28); NOTICE: #0 ->> start of function fxo(integer,integer,date,numeric) (oid=0, tnl=1) NOTICE: #0 "a" => '11', "b" => '21', "c" => '08-16-2020', "d" => '6.28' NOTICE: #2 ->> start of function tracer_tab_trg_fx() (oid=0, tnl=1) NOTICE: #2 context: SQL statement "insert into tracer_tab values(a,b,c,d)" NOTICE: #2 triggered by before row insert trigger NOTICE: #2 "new" => '(11,21,08-16-2020,6.28)' NOTICE: #2 <<- end of function tracer_tab_trg_fx (elapsed time=0.010 ms) NOTICE: #0 <<- end of function fxo (elapsed time=0.010 ms) fxo ----- (1 row) set plpgsql_check.enable_tracer to off; set plpgsql_check.tracer to off; drop table tracer_tab cascade; drop function tracer_tab_trg_fx(); drop function fxo(int, int, date, numeric); create or replace function foo_trg_func() returns trigger as $$ begin -- bad function, RETURN is missing end; $$ language plpgsql; create table foo(a int); create trigger foo_trg before insert for each row execute procedure foo_trg_func(); ERROR: syntax error at or near "for" at character 38 -- should to print error select * from plpgsql_check_function('foo_trg_func', 'foo'); plpgsql_check_function ------------------------------------------------------------ error:2F005:control reached end of function without RETURN (1 row) drop table foo; drop function foo_trg_func(); -- check event trigger function create or replace function f1() returns event_trigger as $$ BEGIN RAISE NOTICE 'test_event_trigger: % %', tg_event, tg_tag; END $$ language plpgsql; select * from plpgsql_check_function_tb('f1()'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+---------+--------+------+-------+----------+-------+--------- (0 rows) -- should fail create or replace function f1() returns event_trigger as $$ BEGIN RAISE NOTICE 'test_event_trigger: % %', tg_event, tg_tagX; END $$ language plpgsql; select * from plpgsql_check_function_tb('f1()'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+---------------------------------+--------+------+-------+----------+---------+--------- f1 | 3 | RAISE | 42703 | column "tg_tagx" does not exist | | | error | 1 | tg_tagX | (1 row) drop function f1(); -- check event trigger function create or replace function f1() returns event_trigger as $$ BEGIN RAISE NOTICE 'test_event_trigger: % %', tg_event, tg_tag; END $$ language plpgsql; select * from plpgsql_check_function('f1()'); plpgsql_check_function ------------------------ (0 rows) -- should fail create or replace function f1() returns event_trigger as $$ BEGIN RAISE NOTICE 'test_event_trigger: % %', tg_event, tg_tagX; END $$ language plpgsql; select * from plpgsql_check_function('f1()'); plpgsql_check_function ----------------------------------------------------- error:42703:3:RAISE:column "tg_tagx" does not exist Query: tg_tagX -- ^ (3 rows) drop function f1(); create table t1tab(a int, b int); create or replace function f1() returns setof t1tab as $$ begin return next (10,20); return; end; $$ language plpgsql; select * from plpgsql_check_function('f1()', performance_warnings => true); plpgsql_check_function ----------------------------------------------------------------------------------------- performance:00000:routine is marked as VOLATILE, should be IMMUTABLE Hint: When you fix this issue, please, recheck other functions that uses this function. (2 rows) create or replace function f1() returns setof t1tab as $$ begin return next (10::numeric,20); return; end; $$ language plpgsql; select * from plpgsql_check_function('f1()', performance_warnings => true); plpgsql_check_function ------------------------------------------------------------------------------------ error:42804:3:RETURN NEXT:returned record type does not match expected record type Detail: Returned type numeric does not match expected type integer in column 1. (2 rows) create or replace function f1() returns setof t1tab as $$ declare a int; b int; begin return next (a,b); return; end; $$ language plpgsql; select * from plpgsql_check_function('f1()', performance_warnings => true); plpgsql_check_function ----------------------------------------------------------------------------------------- performance:00000:routine is marked as VOLATILE, should be IMMUTABLE Hint: When you fix this issue, please, recheck other functions that uses this function. (2 rows) create or replace function f1() returns setof t1tab as $$ declare a numeric; b int; begin return next (a,b::numeric); return; end; $$ language plpgsql; select * from plpgsql_check_function('f1()', performance_warnings => true); plpgsql_check_function ------------------------------------------------------------------------------------ error:42804:4:RETURN NEXT:returned record type does not match expected record type Detail: Returned type numeric does not match expected type integer in column 1. (2 rows) drop function f1(); create table t1(a int, b int); create or replace function fx() returns t2 as $$ begin return (10,20,30)::t1; end; $$ language plpgsql; select * from plpgsql_check_function('fx()', performance_warnings => true); plpgsql_check_function ---------------------------------------------------- error:42846:3:RETURN:cannot cast type record to t1 Query: (10,20,30)::t1 -- ^ Detail: Input has too many columns. (4 rows) drop function fx(); drop table t1tab; drop table t1; create or replace function fx() returns void as $$ begin assert exists(select * from foo); assert false, (select boo from boo limit 1); end; $$ language plpgsql; select * from plpgsql_check_function('fx()', fatal_errors => false); plpgsql_check_function ---------------------------------------------------- error:42P01:3:ASSERT:relation "foo" does not exist Query: exists(select * from foo) -- ^ error:42P01:4:ASSERT:relation "boo" does not exist Query: (select boo from boo limit 1) -- ^ (6 rows) create or replace function ml_trg() returns trigger as $$ #option dump declare begin if TG_OP = 'INSERT' then if NEW.status_from IS NULL then begin -- performance issue only select status into NEW.status_from from pa where pa_id = NEW.pa_id; -- nonexist target value select status into NEW.status_from_xxx from pa where pa_id = NEW.pa_id; exception when DATA_EXCEPTION then new.status_from := 'DE'; end; end if; end if; if TG_OP = 'DELETE' then return OLD; else return NEW; end if; exception when OTHERS then NULL; if TG_OP = 'DELETE' then return OLD; else return NEW; end if; end; $$ language plpgsql; select * from plpgsql_check_function('ml_trg()', 'ml', performance_warnings := true); plpgsql_check_function -------------------------------------------------------------------------- error:42703:13:SQL statement:record "new" has no field "status_from_xxx" (1 row) create or replace function fx2() returns void as $$ declare _pa pa; begin select pa.id into _pa.id from pa limit 1; select pa.pa_id into _pa.pa_id from pa limit 1; end; $$ language plpgsql; select * from plpgsql_check_function('fx2()', performance_warnings := true); plpgsql_check_function --------------------------------------------------------- warning extra:00000:2:DECLARE:never read variable "_pa" (1 row) drop function fx2(); create or replace function fx2() returns void as $$ declare _pa pa; begin _pa.id := (select pa.id from pa limit 1); _pa.pa_id := (select pa.pa_id from pa limit 1); end; $$ language plpgsql; select * from plpgsql_check_function('fx2()', performance_warnings := true); plpgsql_check_function --------------------------------------------------------- warning extra:00000:2:DECLARE:never read variable "_pa" (1 row) drop function fx2(); create type _exception_type as ( state text, message text, detail text); create or replace function f1() returns void as $$ declare _exception record; begin _exception := NULL::_exception_type; exception when others then get stacked diagnostics _exception.state = RETURNED_SQLSTATE, _exception.message = MESSAGE_TEXT, _exception.detail = PG_EXCEPTION_DETAIL, _exception.hint = PG_EXCEPTION_HINT; end; $$ language plpgsql; select f1(); f1 ---- (1 row) select * from plpgsql_check_function_tb('f1()'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-------------------------+----------+-----------------------------------------+--------+------+-------+----------+-------+--------- f1 | 7 | GET STACKED DIAGNOSTICS | 42703 | record "_exception" has no field "hint" | | | error | | | (1 row) create or replace function f1() returns void as $$ declare _exception _exception_type; begin _exception := NULL::_exception_type; exception when others then get stacked diagnostics _exception.state = RETURNED_SQLSTATE, _exception.message = MESSAGE_TEXT, _exception.detail = PG_EXCEPTION_DETAIL; end; $$ language plpgsql; select f1(); f1 ---- (1 row) select * from plpgsql_check_function_tb('f1()'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+----------------------------------+--------+------+---------------+----------+-------+--------- f1 | 3 | DECLARE | 00000 | never read variable "_exception" | | | warning extra | | | (1 row) drop function f1(); drop type _exception_type; create type _exception_type as ( state text, message text, detail text); create or replace function f1() returns void as $$ declare _exception record; begin _exception := NULL::_exception_type; exception when others then get stacked diagnostics _exception.state = RETURNED_SQLSTATE, _exception.message = MESSAGE_TEXT, _exception.detail = PG_EXCEPTION_DETAIL, _exception.hint = PG_EXCEPTION_HINT; end; $$ language plpgsql; select f1(); f1 ---- (1 row) select * from plpgsql_check_function('f1()'); plpgsql_check_function ------------------------------------------------------------------------------- error:42703:7:GET STACKED DIAGNOSTICS:record "_exception" has no field "hint" (1 row) drop function f1(); drop type _exception_type; create table footab(a int, b int, c int); create or replace function footab_trig_func() returns trigger as $$ declare x int; begin if false then -- should be ok; select count(*) from newtab into x; -- should fail; select count(*) from newtab where d = 10 into x; end if; return null; end; $$ language plpgsql; select * from plpgsql_check_function('footab_trig_func','footab', newtable := 'newtab'); plpgsql_check_function ------------------------------------------------------- error:42703:9:SQL statement:column "d" does not exist Query: select count(*) from newtab where d = 10 -- ^ (3 rows) drop table footab; drop function footab_trig_func(); create or replace function df1(anyelement) returns anyelement as $$ begin return $1; end; $$ language plpgsql; create or replace function df2(anyelement, jsonb) returns anyelement as $$ begin return $1; end; $$ language plpgsql; create or replace function t1() returns void as $$ declare r record; begin r := df1(r); end; $$ language plpgsql; select * from plpgsql_check_function('t1()'); plpgsql_check_function ------------------------ (0 rows) create or replace function t1() returns void as $$ declare r record; begin r := df2(r, '{}'); end; $$ language plpgsql; select * from plpgsql_check_function('t1()'); plpgsql_check_function ------------------------ (0 rows) create or replace function t1() returns void as $$ declare r1 record; r2 record; begin select 10 as a, 20 as b into r1; r2 := df1(r1); raise notice '%', r2.a; end; $$ language plpgsql; select * from plpgsql_check_function('t1()'); plpgsql_check_function ------------------------ (0 rows) create or replace function t1() returns void as $$ declare r1 record; r2 record; begin select 10 as a, 20 as b into r1; r2 := df2(r1, '{}'); raise notice '%', r2.a; end; $$ language plpgsql; select * from plpgsql_check_function('t1()'); plpgsql_check_function ------------------------ (0 rows) create or replace function df1(anyelement) returns anyelement as $$ select $1 $$ language sql; create or replace function df22(jsonb, anyelement) returns anyelement as $$ select $2; $$ language sql; create or replace function t1() returns void as $$ declare r1 record; r2 record; begin select 10 as a, 20 as b into r1; r2 := df1(r1); raise notice '%', r2.a; end; $$ language plpgsql; select * from plpgsql_check_function('t1()'); plpgsql_check_function ------------------------ (0 rows) create or replace function t1() returns void as $$ declare r1 record; r2 record; begin select 10 as a, 20 as b into r1; r2 := df22('{}', r1); raise notice '%', r2.a; end; $$ language plpgsql; select * from plpgsql_check_function('t1()'); plpgsql_check_function ------------------------ (0 rows) drop function df1(anyelement); drop function df2(anyelement, jsonb); drop function df22(jsonb, anyelement); drop function t1(); create or replace function dyntest() returns void as $$ begin execute 'drop table if exists xxx; create table xxx(a int)'; end; $$ language plpgsql; -- should be ok select * from plpgsql_check_function('dyntest'); plpgsql_check_function ------------------------ (0 rows) create or replace function dyntest() returns void as $$ declare x int; begin execute 'drop table if exists xxx; create table xxx(a int)' into x; end; $$ language plpgsql; -- should to report error select * from plpgsql_check_function('dyntest'); plpgsql_check_function ------------------------------------------------------- error:XX000:4:EXECUTE:expression does not return data (1 row) drop function dyntest(); -- should to report error create type typ2 as (a int, b int); create or replace function broken_into() returns void as $$ declare v typ2; begin -- should to fail select (10,20)::typ2 into v; -- should be ok select ((10,20)::typ2).* into v; -- should to fail execute 'select (10,20)::typ2' into v; -- should be ok execute 'select ((10,20)::typ2).*' into v; end; $$ language plpgsql; select * from plpgsql_check_function('broken_into', fatal_errors => false); plpgsql_check_function ------------------------------------------------------------------------------------------------------------ error:42804:5:SQL statement:cannot cast composite value of "typ2" type to a scalar value of "integer" type Query: select (10,20)::typ2 -- ^ Context: at SQL statement to variable "v" declared on line 2 warning:00000:5:SQL statement:too few attributes for composite variable Query: select (10,20)::typ2 -- ^ Context: at SQL statement to variable "v" declared on line 2 error:42804:9:EXECUTE:cannot cast composite value of "typ2" type to a scalar value of "integer" type Context: at EXECUTE to variable "v" declared on line 2 warning:00000:9:EXECUTE:too few attributes for composite variable Context: at EXECUTE to variable "v" declared on line 2 warning extra:00000:2:DECLARE:never read variable "v" (13 rows) drop function broken_into(); drop type typ2; -- check output in xml or json formats CREATE OR REPLACE FUNCTION test_function() RETURNS void LANGUAGE plpgsql AS $function$ begin insert into non_existing_table values (1); end $function$; select * from plpgsql_check_function('test_function', format => 'xml'); plpgsql_check_function ---------------------------------------------------------------------------- + + error + 42P01 + relation "non_existing_table" does not exist + SQL statement + insert into non_existing_table values (1)+ + (1 row) select * from plpgsql_check_function('test_function', format => 'json'); plpgsql_check_function ------------------------------------------------------------------- { + "issues": [ + { + "level": "error", + "message":"relation \"non_existing_table\" does not exist",+ "statement": { + "lineNumber": "3", + "text": "SQL statement" + }, + "query": { + "position": "13", + "text": "insert into non_existing_table values (1)" + }, + "sqlState": "42P01" + } + ] + } (1 row) drop function test_function(); -- test settype pragma create or replace function test_function() returns void as $$ declare r record; begin raise notice '%', r.a; end; $$ language plpgsql; -- should to detect error select * from plpgsql_check_function('test_function'); plpgsql_check_function ---------------------------------------------------------------------------- error:55000:4:RAISE:record "r" is not assigned yet Detail: The tuple structure of a not-yet-assigned record is indeterminate. Context: SQL expression "r.a" (3 rows) create type ctype as (a int, b int); create or replace function test_function() returns void as $$ declare r record; begin perform plpgsql_check_pragma('type: r ctype'); raise notice '%', r.a; end; $$ language plpgsql; -- should to be ok select * from plpgsql_check_function('test_function'); plpgsql_check_function ------------------------ (0 rows) create or replace function test_function() returns void as $$ <>declare r record; begin perform plpgsql_check_pragma('type: x.r public."ctype"'); raise notice '%', r.a; end; $$ language plpgsql; -- should to be ok select * from plpgsql_check_function('test_function'); plpgsql_check_function ------------------------ (0 rows) create or replace function test_function() returns void as $$ <>declare r record; begin perform plpgsql_check_pragma('type: "x".r (a int, b int)'); raise notice '%', r.a; end; $$ language plpgsql; -- should to be ok select * from plpgsql_check_function('test_function'); plpgsql_check_function ------------------------ (0 rows) create or replace function test_function() returns void as $$ <>declare r record; begin perform plpgsql_check_pragma('type: "x".r (a int, b int'); raise notice '%', r.a; end; $$ language plpgsql; -- should to be ok select * from plpgsql_check_function('test_function'); WARNING: Pragma "type" on line 4 is not processed. plpgsql_check_function ---------------------------------------------------------------------------- error:55000:5:RAISE:record "r" is not assigned yet Detail: The tuple structure of a not-yet-assigned record is indeterminate. Context: SQL expression "r.a" (3 rows) create or replace function test_function() returns void as $$ <>declare r record; begin perform plpgsql_check_pragma('type: "x".r (a int, b int)x'); raise notice '%', r.a; end; $$ language plpgsql; -- should to be ok select * from plpgsql_check_function('test_function'); WARNING: Pragma "type" on line 4 is not processed. plpgsql_check_function ---------------------------------------------------------------------------- error:55000:5:RAISE:record "r" is not assigned yet Detail: The tuple structure of a not-yet-assigned record is indeterminate. Context: SQL expression "r.a" (3 rows) drop function test_function(); drop type ctype; create or replace function test_function() returns void as $$ declare r pg_class; begin create temp table foo(like pg_class); select * from foo into r; end; $$ language plpgsql; -- should to raise an error select * from plpgsql_check_function('test_function'); plpgsql_check_function ----------------------------------------------------------- error:42P01:5:SQL statement:relation "foo" does not exist Query: select * from foo -- ^ (3 rows) create or replace function test_function() returns void as $$ declare r record; begin create temp table foo(like pg_class); perform plpgsql_check_pragma('table: foo(like pg_class)'); select * from foo into r; raise notice '%', r.relname; end; $$ language plpgsql; -- should be ok select * from plpgsql_check_function('test_function'); plpgsql_check_function ------------------------ (0 rows) drop function test_function(); -- now plpgsql_check can do some other checks when statement EXECUTE -- contains only format function with constant fmt. create or replace function test_function() returns void as $$ begin execute format('create table zzz %I(a int, b int)', 'zzz'); end; $$ language plpgsql; -- should to detect bad expression select * from plpgsql_check_function('test_function'); plpgsql_check_function ----------------------------------------------------- error:42601:3:EXECUTE:syntax error at or near "zzz" Query: create table zzz zzz(a int, b int) (2 rows) -- should to correctly detect type create or replace function test_function() returns void as $$ declare r record; begin execute format('select %L::date + 1 as x', current_date) into r; raise notice '%', extract(dow from r.x); end; $$ language plpgsql; -- should be ok select * from plpgsql_check_function('test_function'); plpgsql_check_function ------------------------ (0 rows) -- should not to crash create or replace function test_function() returns void as $$ declare r record; begin r := null; end; $$ language plpgsql; select * from plpgsql_check_function('test_function'); plpgsql_check_function ------------------------------------------------------- warning extra:00000:2:DECLARE:never read variable "r" (1 row) drop function test_function(); -- ignores syntax errors when literals placehodlers are used create function test_function() returns void as $$ begin execute format('do %L', 'begin end'); end $$ language plpgsql; select * from plpgsql_check_function('test_function'); plpgsql_check_function ------------------------ (0 rows) drop function test_function(); load 'plpgsql_check'; drop type testtype cascade; create type testtype as (a int, b int); create function test_function() returns record as $$ declare r record; begin r := (10,20); if false then return r; end if; return null; end; $$ language plpgsql; create function test_function33() returns record as $$ declare r testtype; begin r := (10,20); if false then return r; end if; return null; end; $$ language plpgsql; -- should not to raise false alarm due check against fake result type select plpgsql_check_function('test_function'); plpgsql_check_function ------------------------ (0 rows) select plpgsql_check_function('test_function33'); plpgsql_check_function ------------------------ (0 rows) -- try to check in passive mode set plpgsql_check.mode = 'every_start'; select test_function(); test_function --------------- (1 row) select test_function33(); test_function33 ----------------- (1 row) select * from test_function() as (a int, b int); a | b ---+--- | (1 row) select * from test_function33() as (a int, b int); a | b ---+--- | (1 row) -- should to identify error select * from test_function() as (a int, b int, c int); ERROR: returned record type does not match expected record type select * from test_function33() as (a int, b int, c int); ERROR: returned record type does not match expected record type drop function test_function(); drop function test_function33(); drop type testtype; set plpgsql_check.mode to default; -- should not to raise false alarm create type c1 as ( a text ); create table t1 ( a c1, b c1 ); insert into t1 (values ('(abc)', '(def)')); alter table t1 drop column a; create or replace function test_function() returns t1 as $$ declare myrow t1%rowtype; begin select * into myrow from t1 limit 1; return myrow; end; $$ language plpgsql; select * from test_function(); b ------- (def) (1 row) select * from plpgsql_check_function('public.test_function()'); plpgsql_check_function ------------------------ (0 rows) drop function test_function(); drop table t1; drop type c1; -- compatibility warnings create or replace function foo01() returns refcursor as $$ declare c cursor for select 1; r refcursor; begin open c; r := c; return r; end; $$ language plpgsql; -- no warnings select * from plpgsql_check_function('foo01', compatibility_warnings => true); plpgsql_check_function ------------------------ (0 rows) create or replace function foo01() returns refcursor as $$ declare c cursor for select 1; r refcursor; begin open c; r := 'c'; return r; end; $$ language plpgsql; -- warning select * from plpgsql_check_function('foo01', extra_warnings => false, compatibility_warnings => true); plpgsql_check_function ----------------------------------------------------------------------------------- compatibility:00000:7:assignment:obsolete setting of refcursor or cursor variable Detail: Internal name of cursor should not be specified by users. Context: at assignment to variable "r" declared on line 4 (3 rows) create or replace function foo01() returns refcursor as $$ declare c cursor for select 1; r refcursor; begin open c; r := c; return 'c'; end; $$ language plpgsql; -- warning select * from plpgsql_check_function('foo01', extra_warnings => false, compatibility_warnings => true); plpgsql_check_function -------------------------------------------------------------------------------------- compatibility:00000:8:RETURN:obsolete setting of refcursor or cursor variable Detail: Internal name of cursor should not be specified by users. warning:42804:8:RETURN:target type is different type than source type Detail: cast "text" value to "refcursor" type Hint: The input expression type does not have an assignment cast to the target type. Context: at RETURN (6 rows) -- pragma sequence test create or replace function test_function() returns void as $$ begin perform plpgsql_check_pragma('sequence: xx'); perform nextval('pg_temp.xy'); perform nextval('pg_temp.xx'); end $$ language plpgsql; select * from plpgsql_check_function('test_function'); plpgsql_check_function ------------------------------------------------------------ error:42P01:4:PERFORM:relation "pg_temp.xy" does not exist Query: SELECT nextval('pg_temp.xy') -- ^ (3 rows) drop function test_function(); create table t1(x int); create or replace function f1_trg() returns trigger as $$ declare x int; begin return x; end; $$ language plpgsql; create trigger t1_f1 before insert on t1 for each row execute procedure f1_trg(); -- raise error select * from plpgsql_check_function('f1_trg', 't1'); plpgsql_check_function ----------------------------------------------------------------------------------------------- error:42804:4:RETURN:cannot return non-composite value from function returning composite type (1 row) drop trigger t1_f1 on t1; drop table t1; drop function f1_trg; create function test_function() returns void as $$ declare a int; b int; c int; d char; begin c := a + d; end; $$ language plpgsql; -- only b should be marked as unused variable select * from plpgsql_check_function('test_function', fatal_errors := false); plpgsql_check_function --------------------------------------------------------------------------------------------------------- error:42883:6:assignment:operator does not exist: integer + character Query: c := a + d -- ^ Hint: No operator matches the given name and argument types. You might need to add explicit type casts. Context: at assignment to variable "c" declared on line 4 warning:00000:3:DECLARE:unused variable "b" warning extra:00000:4:DECLARE:never read variable "c" (7 rows) drop function test_function(); -- from plpgsql_check_active-12 create or replace procedure proc(a int) as $$ begin end; $$ language plpgsql; call proc(10); select * from plpgsql_check_function('proc(int)'); plpgsql_check_function ------------------------------------------ warning extra:00000:unused parameter "a" (1 row) create or replace procedure testproc() as $$ begin call proc(10); end; $$ language plpgsql; call testproc(); select * from plpgsql_check_function('testproc()'); plpgsql_check_function ------------------------ (0 rows) -- should to fail create or replace procedure testproc() as $$ begin call proc((select count(*) from pg_class)); end; $$ language plpgsql; call testproc(); ERROR: cannot use subquery in CALL argument at character 11 select * from plpgsql_check_function('testproc()'); plpgsql_check_function --------------------------------------------------------- error:0A000:3:CALL:cannot use subquery in CALL argument Query: call proc((select count(*) from pg_class)) -- ^ (3 rows) drop procedure proc(int); create procedure proc(in a int, inout b int, in c int) as $$ begin end; $$ language plpgsql; select * from plpgsql_check_function('proc(int,int, int)'); plpgsql_check_function ------------------------------------------------- warning extra:00000:unused parameter "a" warning extra:00000:unused parameter "b" warning extra:00000:unused parameter "c" warning extra:00000:unmodified OUT variable "b" (4 rows) create or replace procedure proc(in a int, inout b int, in c int) as $$ begin b := a + c; end; $$ language plpgsql; select * from plpgsql_check_function('proc(int,int, int)'); plpgsql_check_function ------------------------ (0 rows) create or replace procedure testproc() as $$ declare r int; begin call proc(10, r, 20); end; $$ language plpgsql; call testproc(); select * from plpgsql_check_function('testproc()'); plpgsql_check_function ------------------------ (0 rows) -- should to fail create or replace procedure testproc() as $$ declare r int; begin call proc(10, r + 10, 20); end; $$ language plpgsql; call testproc(); ERROR: procedure parameter "b" is an output parameter but corresponding argument is not writable select * from plpgsql_check_function('testproc()'); plpgsql_check_function -------------------------------------------------------------------------------------------------------------- error:42601:4:CALL:procedure parameter "b" is an output parameter but corresponding argument is not writable (1 row) create or replace procedure testproc(inout r int) as $$ begin call proc(10, r, 20); end; $$ language plpgsql; call testproc(10); r ---- 30 (1 row) select * from plpgsql_check_function('testproc(int)'); plpgsql_check_function ------------------------ (0 rows) drop procedure testproc(int); -- should to raise warnings create or replace procedure testproc2(in p1 int, inout p2 int, in p3 int, inout p4 int) as $$ begin raise notice '% %', p1, p3; end; $$ language plpgsql; select * from plpgsql_check_function('testproc2'); plpgsql_check_function -------------------------------------------------- warning extra:00000:unused parameter "p2" warning extra:00000:unused parameter "p4" warning extra:00000:unmodified OUT variable "p2" warning extra:00000:unmodified OUT variable "p4" (4 rows) drop procedure testproc2; -- should be ok create or replace procedure testproc3(in p1 int, inout p2 int, in p3 int, inout p4 int) as $$ begin p2 := p1; p4 := p3; end; $$ language plpgsql; select * from plpgsql_check_function('testproc3'); plpgsql_check_function ------------------------ (0 rows) drop procedure testproc3; /* * Test pragma */ create or replace function test_pragma() returns void language plpgsql as $$ declare r record; begin perform plpgsql_check_pragma('disable:check'); raise notice '%', r.y; perform plpgsql_check_pragma('enable:check'); select 10 as a, 20 as b into r; raise notice '%', r.a; raise notice '%', r.x; end; $$; select * from plpgsql_check_function('test_pragma'); plpgsql_check_function ------------------------------------------------- error:42703:9:RAISE:record "r" has no field "x" Context: SQL expression "r.x" (2 rows) create or replace function test_pragma() returns void language plpgsql as $$ declare r record; begin if false then -- check is disabled just for if body perform plpgsql_check_pragma('disable:check'); raise notice '%', r.y; end if; select 10 as a, 20 as b into r; raise notice '%', r.a; raise notice '%', r.x; end; $$; select * from plpgsql_check_function('test_pragma'); plpgsql_check_function -------------------------------------------------- error:42703:11:RAISE:record "r" has no field "x" Context: SQL expression "r.x" (2 rows) drop function test_pragma(); create or replace function nested_trace_test(a int) returns int as $$ begin return a + 1; end; $$ language plpgsql; create or replace function trace_test(b int) returns int as $$ declare r int default 0; begin for i in 1..b loop r := nested_trace_test(r); end loop; return r; end; $$ language plpgsql; select trace_test(3); trace_test ------------ 3 (1 row) set plpgsql_check.enable_tracer to on; set plpgsql_check.tracer to on; set plpgsql_check.tracer_test_mode = true; select trace_test(3); NOTICE: #0 ->> start of function trace_test(integer) (oid=0, tnl=1) NOTICE: #0 "b" => '3' NOTICE: #1 ->> start of function nested_trace_test(integer) (oid=0, tnl=1) NOTICE: #1 context: PL/pgSQL function trace_test(integer) line 6 at assignment NOTICE: #1 "a" => '0' NOTICE: #1 <<- end of function nested_trace_test (elapsed time=0.010 ms) NOTICE: #1 ->> start of function nested_trace_test(integer) (oid=0, tnl=1) NOTICE: #1 context: PL/pgSQL function trace_test(integer) line 6 at assignment NOTICE: #1 "a" => '1' NOTICE: #1 <<- end of function nested_trace_test (elapsed time=0.010 ms) NOTICE: #1 ->> start of function nested_trace_test(integer) (oid=0, tnl=1) NOTICE: #1 context: PL/pgSQL function trace_test(integer) line 6 at assignment NOTICE: #1 "a" => '2' NOTICE: #1 <<- end of function nested_trace_test (elapsed time=0.010 ms) NOTICE: #0 <<- end of function trace_test (elapsed time=0.010 ms) trace_test ------------ 3 (1 row) set plpgsql_check.tracer_verbosity TO verbose; select trace_test(3); NOTICE: #0 ->> start of function trace_test(integer) (oid=0, tnl=1) NOTICE: #0 "b" => '3' NOTICE: #0.1 3 --> start of statement block (tnl=1) NOTICE: #0.2 4 --> start of FOR with integer loop variable (tnl=1) NOTICE: #0.3 6 --> start of assignment r := nested_trace_test(r) (tnl=1) NOTICE: #0.3 "r" => '0' NOTICE: #1 ->> start of function nested_trace_test(integer) (oid=0, tnl=1) NOTICE: #1 context: PL/pgSQL function trace_test(integer) line 6 at assignment NOTICE: #1 "a" => '0' NOTICE: #1.1 2 --> start of statement block (tnl=1) NOTICE: #1.2 3 --> start of RETURN (expr='a + 1') (tnl=1) NOTICE: #1.2 "a" => '0' NOTICE: #1.2 <-- end of RETURN (elapsed time=0.010 ms) NOTICE: #1.1 <-- end of statement block (elapsed time=0.010 ms) NOTICE: #1 <<- end of function nested_trace_test (elapsed time=0.010 ms) NOTICE: #0.3 <-- end of assignment (elapsed time=0.010 ms) NOTICE: #0.3 "r" => '1' NOTICE: #0.3 6 --> start of assignment r := nested_trace_test(r) (tnl=1) NOTICE: #0.3 "r" => '1' NOTICE: #1 ->> start of function nested_trace_test(integer) (oid=0, tnl=1) NOTICE: #1 context: PL/pgSQL function trace_test(integer) line 6 at assignment NOTICE: #1 "a" => '1' NOTICE: #1.1 2 --> start of statement block (tnl=1) NOTICE: #1.2 3 --> start of RETURN (expr='a + 1') (tnl=1) NOTICE: #1.2 "a" => '1' NOTICE: #1.2 <-- end of RETURN (elapsed time=0.010 ms) NOTICE: #1.1 <-- end of statement block (elapsed time=0.010 ms) NOTICE: #1 <<- end of function nested_trace_test (elapsed time=0.010 ms) NOTICE: #0.3 <-- end of assignment (elapsed time=0.010 ms) NOTICE: #0.3 "r" => '2' NOTICE: #0.3 6 --> start of assignment r := nested_trace_test(r) (tnl=1) NOTICE: #0.3 "r" => '2' NOTICE: #1 ->> start of function nested_trace_test(integer) (oid=0, tnl=1) NOTICE: #1 context: PL/pgSQL function trace_test(integer) line 6 at assignment NOTICE: #1 "a" => '2' NOTICE: #1.1 2 --> start of statement block (tnl=1) NOTICE: #1.2 3 --> start of RETURN (expr='a + 1') (tnl=1) NOTICE: #1.2 "a" => '2' NOTICE: #1.2 <-- end of RETURN (elapsed time=0.010 ms) NOTICE: #1.1 <-- end of statement block (elapsed time=0.010 ms) NOTICE: #1 <<- end of function nested_trace_test (elapsed time=0.010 ms) NOTICE: #0.3 <-- end of assignment (elapsed time=0.010 ms) NOTICE: #0.3 "r" => '3' NOTICE: #0.2 <-- end of FOR with integer loop variable (elapsed time=0.010 ms) NOTICE: #0.4 8 --> start of RETURN (tnl=1) NOTICE: #0.4 "r" => '3' NOTICE: #0.4 <-- end of RETURN (elapsed time=0.010 ms) NOTICE: #0.1 <-- end of statement block (elapsed time=0.010 ms) NOTICE: #0 <<- end of function trace_test (elapsed time=0.010 ms) trace_test ------------ 3 (1 row) create or replace function trace_test(b int) returns int as $$ declare r int default 0; begin for i in 1..b loop perform plpgsql_check_pragma('disable:tracer'); r := nested_trace_test(r); end loop; return r; end; $$ language plpgsql; select trace_test(3); NOTICE: #0 ->> start of function trace_test(integer) (oid=0, tnl=1) NOTICE: #0 "b" => '3' NOTICE: #0.1 3 --> start of statement block (tnl=1) NOTICE: #0.2 4 --> start of FOR with integer loop variable (tnl=1) NOTICE: #0.3 6 --> start of perform plpgsql_check_pragma('disable: .. (tnl=1) NOTICE: #0.3 <-- end of PERFORM (elapsed time=0.010 ms) NOTICE: #0.2 <-- end of FOR with integer loop variable (elapsed time=0.010 ms) NOTICE: #0.5 9 --> start of RETURN (tnl=1) NOTICE: #0.5 "r" => '3' NOTICE: #0.5 <-- end of RETURN (elapsed time=0.010 ms) NOTICE: #0.1 <-- end of statement block (elapsed time=0.010 ms) NOTICE: #0 <<- end of function trace_test (elapsed time=0.010 ms) trace_test ------------ 3 (1 row) create or replace function nested_trace_test(a int) returns int as $$ begin perform plpgsql_check_pragma('enable:tracer'); return a + 1; end; $$ language plpgsql; select trace_test(3); NOTICE: #0 ->> start of function trace_test(integer) (oid=0, tnl=1) NOTICE: #0 "b" => '3' NOTICE: #0.1 3 --> start of statement block (tnl=1) NOTICE: #0.2 4 --> start of FOR with integer loop variable (tnl=1) NOTICE: #0.3 6 --> start of perform plpgsql_check_pragma('disable: .. (tnl=1) NOTICE: #0.3 <-- end of PERFORM (elapsed time=0.010 ms) NOTICE: #1.3 4 --> start of RETURN (expr='a + 1') (tnl=1) NOTICE: #1.3 "a" => '0' NOTICE: #1.3 <-- end of RETURN (elapsed time=0.010 ms) NOTICE: #1.3 4 --> start of RETURN (expr='a + 1') (tnl=1) NOTICE: #1.3 "a" => '1' NOTICE: #1.3 <-- end of RETURN (elapsed time=0.010 ms) NOTICE: #1.3 4 --> start of RETURN (expr='a + 1') (tnl=1) NOTICE: #1.3 "a" => '2' NOTICE: #1.3 <-- end of RETURN (elapsed time=0.010 ms) NOTICE: #0.2 <-- end of FOR with integer loop variable (elapsed time=0.010 ms) NOTICE: #0.5 9 --> start of RETURN (tnl=1) NOTICE: #0.5 "r" => '3' NOTICE: #0.5 <-- end of RETURN (elapsed time=0.010 ms) NOTICE: #0.1 <-- end of statement block (elapsed time=0.010 ms) NOTICE: #0 <<- end of function trace_test (elapsed time=0.010 ms) trace_test ------------ 3 (1 row) drop function trace_test(int); drop function nested_trace_test(int); create or replace function trace_test(int) returns int as $$ declare r int default 0; begin for i in 1..$1 loop r := r + 1; end loop; r := r + 10; return r; end; $$ language plpgsql; select trace_test(4); NOTICE: #0 ->> start of function trace_test(integer) (oid=0, tnl=1) NOTICE: #0 "$1" => '4' NOTICE: #0.1 3 --> start of statement block (tnl=1) NOTICE: #0.2 4 --> start of FOR with integer loop variable (tnl=1) NOTICE: #0.3 5 --> start of assignment r := r + 1 (tnl=1) NOTICE: #0.3 "r" => '0' NOTICE: #0.3 <-- end of assignment (elapsed time=0.010 ms) NOTICE: #0.3 "r" => '1' NOTICE: #0.3 5 --> start of assignment r := r + 1 (tnl=1) NOTICE: #0.3 "r" => '1' NOTICE: #0.3 <-- end of assignment (elapsed time=0.010 ms) NOTICE: #0.3 "r" => '2' NOTICE: #0.3 5 --> start of assignment r := r + 1 (tnl=1) NOTICE: #0.3 "r" => '2' NOTICE: #0.3 <-- end of assignment (elapsed time=0.010 ms) NOTICE: #0.3 "r" => '3' NOTICE: #0.3 5 --> start of assignment r := r + 1 (tnl=1) NOTICE: #0.3 "r" => '3' NOTICE: #0.3 <-- end of assignment (elapsed time=0.010 ms) NOTICE: #0.3 "r" => '4' NOTICE: #0.2 <-- end of FOR with integer loop variable (elapsed time=0.010 ms) NOTICE: #0.4 7 --> start of assignment r := r + 10 (tnl=1) NOTICE: #0.4 "r" => '4' NOTICE: #0.4 <-- end of assignment (elapsed time=0.010 ms) NOTICE: #0.4 "r" => '14' NOTICE: #0.5 8 --> start of RETURN (tnl=1) NOTICE: #0.5 "r" => '14' NOTICE: #0.5 <-- end of RETURN (elapsed time=0.010 ms) NOTICE: #0.1 <-- end of statement block (elapsed time=0.010 ms) NOTICE: #0 <<- end of function trace_test (elapsed time=0.010 ms) trace_test ------------ 14 (1 row) create or replace function trace_test(int) returns int as $$ declare r int default 0; begin for i in 1..$1 loop perform plpgsql_check_pragma('disable:tracer'); r := r + 1; end loop; r := r + 10; return r; end; $$ language plpgsql; select trace_test(4); NOTICE: #0 ->> start of function trace_test(integer) (oid=0, tnl=1) NOTICE: #0 "$1" => '4' NOTICE: #0.1 3 --> start of statement block (tnl=1) NOTICE: #0.2 4 --> start of FOR with integer loop variable (tnl=1) NOTICE: #0.3 5 --> start of perform plpgsql_check_pragma('disable: .. (tnl=1) NOTICE: #0.3 <-- end of PERFORM (elapsed time=0.010 ms) NOTICE: #0.2 <-- end of FOR with integer loop variable (elapsed time=0.010 ms) NOTICE: #0.5 8 --> start of assignment r := r + 10 (tnl=1) NOTICE: #0.5 "r" => '4' NOTICE: #0.5 <-- end of assignment (elapsed time=0.010 ms) NOTICE: #0.5 "r" => '14' NOTICE: #0.6 9 --> start of RETURN (tnl=1) NOTICE: #0.6 "r" => '14' NOTICE: #0.6 <-- end of RETURN (elapsed time=0.010 ms) NOTICE: #0.1 <-- end of statement block (elapsed time=0.010 ms) NOTICE: #0 <<- end of function trace_test (elapsed time=0.010 ms) trace_test ------------ 14 (1 row) create or replace function trace_test(int) returns int as $$ declare r int default 0; begin perform plpgsql_check_pragma('disable:tracer'); for i in 1..$1 loop r := r + 1; end loop; perform plpgsql_check_pragma('enable:tracer'); r := r + 10; return r; end; $$ language plpgsql; select trace_test(4); NOTICE: #0 ->> start of function trace_test(integer) (oid=0, tnl=1) NOTICE: #0 "$1" => '4' NOTICE: #0.1 3 --> start of statement block (tnl=1) NOTICE: #0.2 4 --> start of perform plpgsql_check_pragma('disable: .. (tnl=1) NOTICE: #0.2 <-- end of PERFORM (elapsed time=0.010 ms) NOTICE: #0.6 12 --> start of assignment r := r + 10 (tnl=1) NOTICE: #0.6 "r" => '4' NOTICE: #0.6 <-- end of assignment (elapsed time=0.010 ms) NOTICE: #0.6 "r" => '14' NOTICE: #0.7 13 --> start of RETURN (tnl=1) NOTICE: #0.7 "r" => '14' NOTICE: #0.7 <-- end of RETURN (elapsed time=0.010 ms) NOTICE: #0.1 <-- end of statement block (elapsed time=0.010 ms) NOTICE: #0 <<- end of function trace_test (elapsed time=0.010 ms) trace_test ------------ 14 (1 row) drop function trace_test(int); create or replace function public.fx1() returns table(i integer, j integer, found boolean) as $$ begin for i in 1..10 loop for j in 1..10 loop return next; end loop; end loop; end; $$ language plpgsql immutable; select * from plpgsql_check_function('fx1'); plpgsql_check_function -------------------------------------------------------------------------- warning:00000:2:statement block:parameter "found" is shadowed Detail: Local auto variable shadows function parameter. warning:00000:3:FOR with integer loop variable:parameter "i" is shadowed Detail: Local auto variable shadows function parameter. warning:00000:5:FOR with integer loop variable:parameter "j" is shadowed Detail: Local auto variable shadows function parameter. warning extra:00000:unmodified OUT variable "i" warning extra:00000:unmodified OUT variable "j" warning extra:00000:unmodified OUT variable "found" (9 rows) drop function fx1; -- tracer test set plpgsql_check.enable_tracer to on; select plpgsql_check_tracer(true); NOTICE: tracer is active NOTICE: tracer verbosity is verbose plpgsql_check_tracer ---------------------- t (1 row) create role regress_plpgsql_check_test_role; DO $$ begin begin -- should to fail create role regress_plpgsql_check_test_role; exception when duplicate_object then -- Role already exists -- the exception handler is empty (#156) end; end; $$; NOTICE: #0 ->> start of block inline_code_block (oid=0, tnl=1) NOTICE: #0.1 2 --> start of statement block (tnl=1) NOTICE: #0.2 3 --> start of statement block (tnl=1) NOTICE: #0.3 5 --> start of SQL statement (query='create role regress_plpgsql_ch ..') (tnl=2) NOTICE: #0.3 <-- end of SQL statement (elapsed time=0.010 ms) aborted NOTICE: #0.2 <-- end of statement block (elapsed time=0.010 ms) NOTICE: #0.1 <-- end of statement block (elapsed time=0.010 ms) NOTICE: #0 <<- end of block (elapsed time=0.010 ms) drop role regress_plpgsql_check_test_role; set plpgsql_check.enable_tracer to off; select plpgsql_check_tracer(false); NOTICE: tracer is not active NOTICE: tracer verbosity is verbose plpgsql_check_tracer ---------------------- f (1 row) -- tracing constants -- issue #159 create or replace function tabret_dynamic() returns table (id integer, val text) as $$ begin return query execute 'select id, val from (values (1, ''a''), (2, ''b'')) as v(id, val)'; end; $$ language plpgsql immutable; -- should be ok select * from plpgsql_check_function('tabret_dynamic()'); plpgsql_check_function ------------------------ (0 rows) create or replace function tabret_dynamic() returns table (id integer, val text) as $$ declare z_query text; begin z_query := 'select id, val from (values (1, ''a''), (2, ''b'')) as v(id, val)'; execute z_query into id, val; return next; end; $$ language plpgsql immutable; -- should be ok select * from plpgsql_check_function('tabret_dynamic()'); plpgsql_check_function ------------------------ (0 rows) create or replace function tabret_dynamic() returns table (id integer, val text) as $$ declare z_query text; begin z_query := 'select id, val from (values (1, ''a''), (2, ''b'')) as v(id, val)'; for id, val in execute z_query loop return next; end loop; end; $$ language plpgsql immutable; -- should be ok select * from plpgsql_check_function('tabret_dynamic()'); plpgsql_check_function ------------------------ (0 rows) create or replace function tabret_dynamic() returns table (id integer, val text) as $$ declare z_query text; begin z_query := 'select id, val from (values (1, ''a''), (2, ''b'')) as v(id, val)'; return query execute z_query; end; $$ language plpgsql immutable; -- should be ok select * from plpgsql_check_function('tabret_dynamic()'); plpgsql_check_function ------------------------ (0 rows) drop function tabret_dynamic; -- should not to crash on empty string, or comment create or replace function dynamic_emptystr() returns void as $$ begin execute ''; end; $$ language plpgsql; -- should be ok select * from plpgsql_check_function('dynamic_emptystr()'); plpgsql_check_function ------------------------ (0 rows) create or replace function dynamic_emptystr() returns void as $$ declare x int; begin execute '--' into x; end; $$ language plpgsql; -- should not to crash, no result error select * from plpgsql_check_function('dynamic_emptystr()'); plpgsql_check_function ------------------------------------------------------- error:XX000:4:EXECUTE:expression does not return data (1 row) drop function dynamic_emptystr(); -- unclosed cursor detection create or replace function fx() returns void as $$ declare c refcursor; begin open c for select * from pg_class; end; $$ language plpgsql; do $$ begin perform fx(); -- should to show warning perform fx(); end; $$; WARNING: cursor "c" is not closed set plpgsql_check.strict_cursors_leaks to on; do $$ begin -- should to show warning perform fx(); -- should to show warning perform fx(); end; $$; WARNING: cursor is not closed WARNING: cursor is not closed create or replace function fx() returns void as $$ declare c refcursor; begin open c for select * from pg_class; close c; end; $$ language plpgsql; -- without warnings do $$ begin perform fx(); perform fx(); end; $$; set plpgsql_check.strict_cursors_leaks to off; drop function fx(); create table public.testt(a int, b int); create or replace function fx() returns void as $$ declare v1 varchar default 'public'; v2 varchar default 'testt'; v3 varchar default 'a'; begin raise notice '%', format('%I.%I.%I', v1, v2, v3); perform 'pragma:assert-schema: v1'; perform 'pragma:assert-table: v1, v2'; perform 'pragma:assert-column: v1, v2, v3'; perform 'pragma:assert-table: v2'; perform 'pragma:assert-column: v2, v3'; end; $$ language plpgsql; select * from plpgsql_check_function('fx()'); plpgsql_check_function ------------------------ (0 rows) create table public.testt(a int, b int); ERROR: relation "testt" already exists create or replace function fx() returns void as $$ declare v1 varchar default 'public'; v2 varchar default 'testt'; v3 varchar default 'x'; begin raise notice '%', format('%I.%I.%I', v1, v2, v3); perform 'pragma:assert-column: v1, v2, v3'; end; $$ language plpgsql; select * from plpgsql_check_function('fx()'); plpgsql_check_function ------------------------------------------------------------------------------ error:42703:8:PERFORM:column "x" of relation "public"."testt" does not exist (1 row) drop function fx(); drop table public.testt; create or replace function fx() returns void as $$ declare v_sql varchar default ''; i int; begin -- we don't support constant tracing from queries select 'bla bla bla' into v_sql; execute v_sql into i; raise notice '%', i; end; $$ language plpgsql; -- should be ok select * from plpgsql_check_function('fx()'); plpgsql_check_function ------------------------ (0 rows) drop function fx(); -- should not to raise warning do $$ declare c cursor for select * from generate_series(1,10); t int; begin for i in 1..2 loop open c; loop fetch c into t; exit when not found; raise notice '%', t; end loop; close c; end loop; end; $$; NOTICE: 1 NOTICE: 2 NOTICE: 3 NOTICE: 4 NOTICE: 5 NOTICE: 6 NOTICE: 7 NOTICE: 8 NOTICE: 9 NOTICE: 10 NOTICE: 1 NOTICE: 2 NOTICE: 3 NOTICE: 4 NOTICE: 5 NOTICE: 6 NOTICE: 7 NOTICE: 8 NOTICE: 9 NOTICE: 10 -- should not raise warning create or replace function fx(p text) returns void as $$ begin execute format($_$select '%1$I'$_$, p); end; $$ language plpgsql; -- should be ok select * from plpgsql_check_function('fx(text)'); plpgsql_check_function ------------------------ (0 rows) drop function fx(text); create or replace function fx() returns void as $$ declare p varchar; begin p := 'ahoj'; execute format($_$select '%1$I'$_$, p); end; $$ language plpgsql; -- should be ok select * from plpgsql_check_function('fx()'); plpgsql_check_function ------------------------ (0 rows) drop function fx(); -- should not crash create or replace procedure p1() as $$ begin commit; end; $$ language plpgsql; set plpgsql_check.cursors_leaks to on; do $$ declare c cursor for select 1; begin open c; call p1(); end $$; set plpgsql_check.cursors_leaks to default; drop procedure p1; -- should not crash create or replace function nested_trace_test(a int) returns int as $$ begin perform plpgsql_check_pragma('enable:tracer'); return a + 1; end; $$ language plpgsql; create or replace function trace_test(b int) returns int as $$ declare r int default 0; begin for i in 1..b loop r := nested_trace_test(r); end loop; return r; end; $$ language plpgsql; -- when plpgsql_check is not loaded yet, then plpgsql_check is -- load by perform plpgsql_check_pragma and this is another -- case, when fmgr hook is not called in expected order. \c -- should not to crash select trace_test(3); trace_test ------------ 3 (1 row) create table testtable_pure_expr(a int); -- detection of not pure expressions create or replace function pure_expr() returns void as $$ declare v int; begin v := 1 delete from testtable_pure_expr where a = 10; raise notice '%', v; end; $$ language plpgsql; -- raise warning select * from plpgsql_check_function('pure_expr()'); plpgsql_check_function -------------------------------------------------------------------- warning extra:42601:4:assignment:expression is not pure expression Query: v := 1 -- ^ delete from testtable_pure_expr where a = 10 Detail: there is a possibility of unwanted behave Hint: Maybe you forgot to use a semicolon. Context: at assignment to variable "v" declared on line 2 (7 rows) drop table testtable_pure_expr; -- should not to raise false alarm drop function fxx(ct, ct); drop type ct; create type ct as (x int, y numeric, v varchar); create procedure proc_composite(inout cv ct, x int, y numeric, v varchar) as $$ begin cv.x := x; cv.y := y; cv.v := v; end; $$ language plpgsql; call proc_composite(null, 10, 10.2, 'ahoj'); cv ---------------- (10,10.2,ahoj) (1 row) create or replace function test_proc_composite() returns void as $$ declare cv ct; begin call proc_composite(cv, 10, 10.2, 'ahoj'); end; $$ language plpgsql; select * from plpgsql_check_function('test_proc_composite'); plpgsql_check_function ------------------------ (0 rows) drop function test_proc_composite; drop procedure proc_composite; create procedure proc_composite(inout cv1 ct, cv2 ct) as $$ begin cv1.x := 10; cv2.v := 'Ahoj'; end; $$ language plpgsql; create or replace function test_proc_composite() returns void as $$ declare cv1 ct; cv2 ct; begin call proc_composite(cv1, cv2); end; $$ language plpgsql; select * from plpgsql_check_function('test_proc_composite'); plpgsql_check_function ------------------------ (0 rows) drop function test_proc_composite; drop procedure proc_composite; drop type ct; create or replace function double_usage_of_const_str() returns void as $$ declare p_id_arr integer[]; r_id integer; begin p_id_arr := '{1,2,3}'::integer[]; foreach r_id in array p_id_arr loop raise notice 'id: %', r_id; end loop; --the following loop causes an error foreach r_id in array p_id_arr loop raise notice 'id: %', r_id; end loop; end; $$ language plpgsql; -- should not to raise false alarm #195 select * from plpgsql_check_function('double_usage_of_const_str'); plpgsql_check_function ------------------------ (0 rows) -- using reserved world for names is bad create or replace function test_bad_label() returns int as $$ <> declare x int = 10; begin return x; end; $$ language plpgsql; select * from plpgsql_check_function('test_bad_label'); plpgsql_check_function --------------------------------------------------------------------------- warning:00000:4:statement block:name of label "outer" is reserved keyword Detail: The reserved keyword was used as label name. (2 rows) drop function test_bad_label; -- using slow legacy syntax for assignment create or replace function test_bad_assign() returns int as $$ declare x int; begin select 0 into x; return x; end; $$ language plpgsql immutable; select * from plpgsql_check_function('test_bad_assign()', all_warnings=>true); plpgsql_check_function ------------------------------------------------------------------------ performance:00000:4:SQL statement:detected "SELECT expr INTO variable" Query: select 0 -- ^ Detail: This obsolete syntax of assigning can be slow. Hint: Use syntax "variable := expr" instead. (5 rows) drop function test_bad_assign; create or replace function volatility_check() returns int as $$ begin return (select count(*) from pg_class); end; $$ language plpgsql volatile; -- should be ok select * from plpgsql_check_function('volatility_check()'); plpgsql_check_function ------------------------ (0 rows) create or replace function volatility_check() returns int as $$ begin return (select count(*) from pg_class); end; $$ language plpgsql stable; -- should be ok select * from plpgsql_check_function('volatility_check()'); plpgsql_check_function ------------------------ (0 rows) create or replace function volatility_check() returns int as $$ begin return (select count(*) from pg_class); end; $$ language plpgsql immutable; -- warning select * from plpgsql_check_function('volatility_check()'); plpgsql_check_function ----------------------------------------------------------------------------------------- warning:00000:3:RETURN:routine is marked as IMMUTABLE, but expression is STABLE Detail: there is a risk of unwanted behave Hint: When you fix this issue, please, recheck other functions that uses this function. (3 rows) create or replace function volatility_check() returns int as $$ begin return random()*1000; end; $$ language plpgsql volatile; -- should be ok select * from plpgsql_check_function('volatility_check()'); plpgsql_check_function ------------------------ (0 rows) create or replace function volatility_check() returns int as $$ begin return random()*1000; end; $$ language plpgsql stable; -- warning select * from plpgsql_check_function('volatility_check()'); plpgsql_check_function ----------------------------------------------------------------------------------------- warning:00000:3:RETURN:routine is marked as STABLE, but expression is VOLATILE Detail: there is a risk of unwanted behave Hint: When you fix this issue, please, recheck other functions that uses this function. (3 rows) drop function volatility_check(); -- issue #201 - should not to raise false error, when composite -- constant is used and assigned to composite with more fields create type t_retcode as (retcode varchar, errmsg varchar, id int); create or replace function test_retcode_bad() returns void as $$ declare rc t_retcode := row('MO_REG_ERROR'); begin raise notice '%', rc.retcode; end; $$ language plpgsql; select * from plpgsql_check_function('test_retcode_bad'); plpgsql_check_function ------------------------------------------------------------------------------ warning:00000:4:statement block:too few attributes for composite variable Context: during statement block local variable "rc" initialization on line 3 (2 rows) drop function test_retcode_bad; -- test usage domain of composite in output #202 create domain t_retcode_domain as t_retcode; create or replace function test_retcode_bad(INOUT x1 t_retcode_domain, INOUT x2 int) as $$ begin x1.retcode = concat(x1.retcode, 'xxx'); x2 := x2 + 10; end; $$ language plpgsql; select * from plpgsql_check_function('test_retcode_bad'); plpgsql_check_function ------------------------------------------------------------------------ warning extra:00000:composite OUT variable "x1" is not single argument (1 row) drop function test_retcode_bad; drop type t_retcode cascade; NOTICE: drop cascades to type t_retcode_domain create type demo_payload as (value integer); create function demo_produce_row(out status_code integer, out payload record) as $$ begin status_code := 0; payload := ROW(1)::demo_payload; end $$ language plpgsql; create function demo_consume_row() returns void as $$ declare fetched_row record; typed_payload demo_payload; observed_status integer; begin select * into strict fetched_row from demo_produce_row(); typed_payload := fetched_row.payload; observed_status := fetched_row.status_code; end; $$ language plpgsql; -- should not crash - issue #216 select * from plpgsql_check_function('demo_consume_row()'); plpgsql_check_function --------------------------------------------------------------------- warning extra:00000:4:DECLARE:never read variable "typed_payload" warning extra:00000:5:DECLARE:never read variable "observed_status" (2 rows) -- should not crash - issue #220 create function f_poly(anyelement) returns int as $$ begin return $1; end; $$ language plpgsql; select plpgsql_check_profiler(true); NOTICE: profiler is active plpgsql_check_profiler ------------------------ t (1 row) select f_poly(1); f_poly -------- 1 (1 row) select plpgsql_coverage_statements('f_poly(anyelement)'); plpgsql_coverage_statements ----------------------------- 1 (1 row) select plpgsql_coverage_statements('f_poly'); plpgsql_coverage_statements ----------------------------- 1 (1 row) select plpgsql_check_profiler(false); NOTICE: profiler is not active plpgsql_check_profiler ------------------------ f (1 row) drop function f_poly; create or replace function repro04() returns void as $$ begin execute 'select 1'; end $$ language plpgsql; select * from plpgsql_check_function('repro04()', performance_warnings => true); plpgsql_check_function ----------------------------------------------------------------------------------------- performance:00000:3:EXECUTE:immutable expression without parameters found Detail: the EXECUTE command is not necessary probably Hint: Don't use dynamic SQL when you can use static SQL. performance:00000:routine is marked as VOLATILE, should be IMMUTABLE Hint: When you fix this issue, please, recheck other functions that uses this function. (5 rows) create or replace function repro04_into() returns int as $$ declare r int; begin execute 'select $1' into r using 1; return r; end $$ language plpgsql; select * from plpgsql_check_function('repro04_into()', performance_warnings => true); plpgsql_check_function ----------------------------------------------------------------------------------------- performance:00000:routine is marked as VOLATILE, should be IMMUTABLE Hint: When you fix this issue, please, recheck other functions that uses this function. (2 rows) create or replace function repro04_real_dynsql(p text) returns void as $$ begin execute 'select ' || quote_literal(p); end $$ language plpgsql; select * from plpgsql_check_function('repro04_real_dynsql(text)', performance_warnings => true); plpgsql_check_function ----------------------------------------------------------------------------------------- performance:00000:routine is marked as VOLATILE, should be IMMUTABLE Detail: attention: cannot to determine volatility of used dynamic SQL Hint: When you fix this issue, please, recheck other functions that uses this function. (3 rows) -- a multi statement command is real dynamic sql too, and this case was -- reported correctly even before the fix. create or replace function repro04_multi() returns void as $$ begin execute 'select 1; select 2'; end $$ language plpgsql; select * from plpgsql_check_function('repro04_multi()', performance_warnings => true); plpgsql_check_function ----------------------------------------------------------------------------------------- performance:00000:routine is marked as VOLATILE, should be IMMUTABLE Hint: When you fix this issue, please, recheck other functions that uses this function. (2 rows) create table repro04_multi_target(a int); create or replace function repro04_multi() returns void as $$ begin execute 'select 1; insert into repro04_multi_target values(10)'; end $$ language plpgsql; select * from plpgsql_check_function('repro04_multi()', performance_warnings => true); plpgsql_check_function ------------------------ (0 rows) drop table repro04_multi_target; drop function repro04(); drop function repro04_into(); drop function repro04_real_dynsql(text); drop function repro04_multi(); create or replace function unpriv_issue4_rowexpr() returns void as $$ declare r record; begin select (row(1, 'hello'::text)).* into r; raise notice '%', r.f1; select row(1, 'hello') into r; raise notice '%', r.row; end; $$ language plpgsql; select unpriv_issue4_rowexpr(); NOTICE: 1 NOTICE: (1,hello) unpriv_issue4_rowexpr ----------------------- (1 row) -- there should not be any false errors select * from plpgsql_check_function_tb('unpriv_issue4_rowexpr()'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+---------+--------+------+-------+----------+-------+--------- (0 rows) drop function unpriv_issue4_rowexpr(); okbob-plpgsql_check-800db79/expected/plpgsql_check_active_2.out000066400000000000000000006434611526744060500247360ustar00rootroot00000000000000load 'plpgsql'; create extension if not exists plpgsql_check; set client_min_messages to notice; set plpgsql_check.regress_test_mode = true; -- -- check function statement tests -- --should fail - is not plpgsql select * from plpgsql_check_function_tb('session_user()'); ERROR: "session_user"() is not a plpgsql function create table t1(a int, b int); create table pa (id int, pa_id character varying(32), status character varying(60)); create table ml(ml_id character varying(32), status_from character varying(60), pa_id character varying(32), xyz int); create function f1() returns void as $$ begin if false then update t1 set c = 30; end if; if false then raise notice '%', r.c; end if; end; $$ language plpgsql; select f1(); f1 ---- (1 row) select * from plpgsql_check_function_tb('f1()', fatal_errors := true); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+---------------+----------+--------------------------------------------+--------+------+-------+----------+----------------------+--------- f1 | 4 | SQL statement | 42703 | column "c" of relation "t1" does not exist | | | error | 15 | update t1 set c = 30 | (1 row) select * from plpgsql_check_function_tb('f1()', fatal_errors := false); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+---------------+----------+--------------------------------------------+--------+------+-------+----------+----------------------+--------- f1 | 4 | SQL statement | 42703 | column "c" of relation "t1" does not exist | | | error | 15 | update t1 set c = 30 | f1 | 7 | RAISE | 42P01 | missing FROM-clause entry for table "r" | | | error | 1 | r.c | (2 rows) select * from plpgsql_check_function_tb('f1()'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+---------------+----------+--------------------------------------------+--------+------+-------+----------+----------------------+--------- f1 | 4 | SQL statement | 42703 | column "c" of relation "t1" does not exist | | | error | 15 | update t1 set c = 30 | (1 row) drop function f1(); create function f1() returns void as $$ begin if false then insert into t1 values(10,20); update t1 set a = 10; delete from t1; end if; end; $$ language plpgsql stable; select f1(); f1 ---- (1 row) select * from plpgsql_check_function_tb('f1()', fatal_errors := false); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+---------------+----------+--------------------------------------------------+--------+------+-------+----------+------------------------------+--------- f1 | 4 | SQL statement | 0A000 | INSERT is not allowed in a non volatile function | | | error | 1 | insert into t1 values(10,20) | f1 | 5 | SQL statement | 0A000 | UPDATE is not allowed in a non volatile function | | | error | 1 | update t1 set a = 10 | f1 | 6 | SQL statement | 0A000 | DELETE is not allowed in a non volatile function | | | error | 1 | delete from t1 | (3 rows) drop function f1(); create function f1() returns void as $$ declare r record; begin if false then for r in update t1 set a = a + 1 returning * loop raise notice '%', r.a; end loop; end if; end; $$ language plpgsql; select f1(); f1 ---- (1 row) select * from plpgsql_check_function_tb('f1()', fatal_errors := false); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+---------+--------+------+-------+----------+-------+--------- (0 rows) drop function f1(); create function f1() returns void as $$ declare r record; begin if false then for r in update t1 set a = a + 1 returning * loop raise notice '%', r.a; end loop; end if; end; $$ language plpgsql stable; select f1(); f1 ---- (1 row) select * from plpgsql_check_function_tb('f1()', fatal_errors := false); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+----------------------+----------+--------------------------------------------------+--------+------+-------+----------+-------------------------------------+--------- f1 | 5 | FOR over SELECT rows | 0A000 | UPDATE is not allowed in a non volatile function | | | error | 1 | update t1 set a = a + 1 returning * | (1 row) drop function f1(); create function g1(out a int, out b int) as $$ select 10,20; $$ language sql; create function f1() returns void as $$ declare r record; begin r := g1(); if false then raise notice '%', r.c; end if; end; $$ language plpgsql; select f1(); f1 ---- (1 row) select * from plpgsql_check_function_tb('f1()'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+-----------------------------+--------+------+-------+----------+-------+---------------------- f1 | 6 | RAISE | 42703 | record "r" has no field "c" | | | error | | | SQL expression "r.c" (1 row) select f1(); f1 ---- (1 row) drop function f1(); drop function g1(); create function g1(out a int, out b int) returns setof record as $$ select * from t1; $$ language sql; create function f1() returns void as $$ declare r record; begin for r in select * from g1() loop raise notice '%', r.c; end loop; end; $$ language plpgsql; select f1(); f1 ---- (1 row) select * from plpgsql_check_function_tb('f1()'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+-----------------------------+--------+------+-------+----------+-------+---------------------- f1 | 6 | RAISE | 42703 | record "r" has no field "c" | | | error | | | SQL expression "r.c" (1 row) select f1(); f1 ---- (1 row) create or replace function f1() returns void as $$ declare r record; begin for r in select * from g1() loop r.c := 20; end loop; end; $$ language plpgsql; select f1(); f1 ---- (1 row) select * from plpgsql_check_function_tb('f1()'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+------------+----------+-----------------------------+--------+------+-------+----------+-------+--------------------------------------------------------------- f1 | 6 | assignment | 42703 | record "r" has no field "c" | | | error | | | at assignment to field "c" of variable "r" declared on line 2 (1 row) select f1(); f1 ---- (1 row) drop function f1(); drop function g1(); create function f1() returns int as $$ declare r int; begin if false then r := a + b; end if; return r; end; $$ language plpgsql; select f1(); f1 ---- (1 row) select * from plpgsql_check_function_tb('f1()'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+------------+----------+---------------------------+--------+------+-------+----------+------------+-------------------------------------------------- f1 | 5 | assignment | 42703 | column "a" does not exist | | | error | 6 | r := a + b | at assignment to variable "r" declared on line 2 (1 row) select f1(); f1 ---- (1 row) drop function f1(); create or replace function f1() returns void as $$ declare r int[]; begin if false then r[c+10] := 20; end if; end; $$ language plpgsql; select f1(); f1 ---- (1 row) select * from plpgsql_check_function_tb('f1()'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+------------+----------+---------------------------+--------+------+-------+----------+---------------+-------------------------------------------------- f1 | 5 | assignment | 42703 | column "c" does not exist | | | error | 3 | r[c+10] := 20 | at assignment to variable "r" declared on line 2 (1 row) select f1(); f1 ---- (1 row) drop function f1(); create or replace function f1() returns void as $$ declare r int; begin if false then r[10] := 20; end if; end; $$ language plpgsql set search_path = public; select f1(); f1 ---- (1 row) select * from plpgsql_check_function_tb('f1()'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+------------+----------+------------------------------------------------------------------------+--------+------+-------+----------+-------------+-------------------------------------------------- f1 | 5 | assignment | 42804 | cannot subscript type integer because it does not support subscripting | | | error | 1 | r[10] := 20 | at assignment to variable "r" declared on line 2 (1 row) select f1(); f1 ---- (1 row) drop function f1(); create or replace function f1_trg() returns trigger as $$ begin if new.a > 10 then raise notice '%', new.b; raise notice '%', new.c; end if; return new; end; $$ language plpgsql; create trigger t1_f1 before insert on t1 for each row execute procedure f1_trg(); insert into t1 values(6,30); select * from plpgsql_check_function_tb('f1_trg()','t1'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+-------------------------------+--------+------+-------+----------+-------+------------------------ f1_trg | 5 | RAISE | 42703 | record "new" has no field "c" | | | error | | | SQL expression "new.c" (1 row) insert into t1 values(6,30); create or replace function f1_trg() returns trigger as $$ begin new.a := new.a + 10; new.b := new.b + 10; new.c := 30; return new; end; $$ language plpgsql; -- should to fail select * from plpgsql_check_function_tb('f1_trg()','t1'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+------------+----------+-------------------------------+--------+------+-------+----------+-------+----------------------------------------------------------------- f1_trg | 5 | assignment | 42703 | record "new" has no field "c" | | | error | | | at assignment to field "c" of variable "new" declared on line 0 (1 row) -- should to fail but not crash insert into t1 values(6,30); ERROR: record "new" has no field "c" CONTEXT: PL/pgSQL assignment "new.c := 30" PL/pgSQL function f1_trg() line 5 at assignment create or replace function f1_trg() returns trigger as $$ begin new.a := new.a + 10; new.b := new.b + 10; return new; end; $$ language plpgsql; -- ok select * from plpgsql_check_function_tb('f1_trg()', 't1'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+---------+--------+------+-------+----------+-------+--------- (0 rows) -- ok insert into t1 values(6,30); create or replace function f1_trg() returns trigger as $$ begin new.a := new.a + 10; new.b := new.b + 10; return null; end; $$ language plpgsql; -- ok select * from plpgsql_check_function_tb('f1_trg()', 't1'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+---------+--------+------+-------+----------+-------+--------- (0 rows) insert into t1 values(60,300); select * from t1; a | b ----+---- 6 | 30 6 | 30 16 | 40 (3 rows) insert into t1 values(600,30); select * from t1; a | b ----+---- 6 | 30 6 | 30 16 | 40 (3 rows) drop trigger t1_f1 on t1; drop function f1_trg(); -- test of showing caret on correct place for multiline queries create or replace function f1() returns void as $$ begin select var from foo; end; $$ language plpgsql; select * from plpgsql_check_function_tb('f1()'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+---------------+----------+-------------------------------+--------+------+-------+----------+--------+--------- f1 | 3 | SQL statement | 42P01 | relation "foo" does not exist | | | error | 23 | select+| | | | | | | | | | var +| | | | | | | | | | from+| | | | | | | | | | foo | (1 row) drop function f1(); create or replace function f1() returns int as $$ begin return (select a from t1 where hh = 20); end; $$ language plpgsql; select * from plpgsql_check_function_tb('f1()'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+----------------------------+--------+------+-------+----------+----------------------------+--------- f1 | 3 | RETURN | 42703 | column "hh" does not exist | | | error | 50 | (select a +| | | | | | | | | | from t1 +| | | | | | | | | | where hh = 20) | (1 row) create or replace function f1() returns int as $$ begin return (select a from txxxxxxx where hh = 20); end; $$ language plpgsql; select * from plpgsql_check_function_tb('f1()'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+------------------------------------+--------+------+-------+----------+----------------------------+--------- f1 | 3 | RETURN | 42P01 | relation "txxxxxxx" does not exist | | | error | 29 | (select a +| | | | | | | | | | from txxxxxxx+| | | | | | | | | | where hh = 20) | (1 row) drop function f1(); drop table t1; -- raise warnings when target row has different number of attributies in -- SELECT INTO statement create or replace function f1() returns void as $$ declare a1 int; a2 int; begin select 10,20 into a1,a2; end; $$ language plpgsql; -- should be ok select * from plpgsql_check_function_tb('f1()'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+--------------------------+--------+------+---------------+----------+-------+--------- f1 | 2 | DECLARE | 00000 | never read variable "a1" | | | warning extra | | | f1 | 2 | DECLARE | 00000 | never read variable "a2" | | | warning extra | | | (2 rows) create or replace function f1() returns void as $$ declare a1 int; begin select 10,20 into a1; end; $$ language plpgsql; -- raise warning select * from plpgsql_check_function_tb('f1()'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+---------------+----------+------------------------------------------+---------------------------------------------------------------+-------------------------------------------------+---------------+----------+--------------+---------------------------------- f1 | 4 | SQL statement | 00000 | too many attributes for target variables | There are less target variables than output columns in query. | Check target variables in SELECT INTO statement | warning | 1 | select 10,20 | at SQL statement to a1 variables f1 | 2 | DECLARE | 00000 | never read variable "a1" | | | warning extra | | | (2 rows) create or replace function f1() returns void as $$ declare a1 int; a2 int; begin select 10 into a1,a2; end; $$ language plpgsql; -- raise warning select * from plpgsql_check_function_tb('f1()'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+---------------+----------+-----------------------------------------+---------------------------------------------------------------+--------------------------------------------------+---------------+----------+-----------+-------------------------------------- f1 | 4 | SQL statement | 00000 | too few attributes for target variables | There are more target variables than output columns in query. | Check target variables in SELECT INTO statement. | warning | 1 | select 10 | at SQL statement to a1, a2 variables f1 | 2 | DECLARE | 00000 | never read variable "a1" | | | warning extra | | | f1 | 2 | DECLARE | 00000 | never read variable "a2" | | | warning extra | | | (3 rows) -- bogus code set check_function_bodies to off; create or replace function f1() returns void as $$ adasdfsadf $$ language plpgsql; select * from plpgsql_check_function_tb('f1()'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+--------------------------------------+--------+------+-------+----------+------------+--------------------------------------------------- f1 | | | 42601 | syntax error at or near "adasdfsadf" | | | error | 2 | +| compilation of PL/pgSQL function "f1" near line 1 | | | | | | | | | adasdfsadf+| | | | | | | | | | | (1 row) drop function f1(); create table t1(a int, b int); create function g1(out a int, out b int) as $$ select 10,20; $$ language sql; create function f1() returns void as $$ declare r record; begin r := g1(); if false then raise notice '%', r.c; end if; end; $$ language plpgsql; select f1(); f1 ---- (1 row) select * from plpgsql_check_function('f1()'); plpgsql_check_function ------------------------------------------------- error:42703:6:RAISE:record "r" has no field "c" Context: SQL expression "r.c" (2 rows) select f1(); f1 ---- (1 row) drop function f1(); drop function g1(); create function g1(out a int, out b int) returns setof record as $$ select * from t1; $$ language sql; create function f1() returns void as $$ declare r record; begin for r in select * from g1() loop raise notice '%', r.c; end loop; end; $$ language plpgsql; select f1(); f1 ---- (1 row) select * from plpgsql_check_function('f1()'); plpgsql_check_function ------------------------------------------------- error:42703:6:RAISE:record "r" has no field "c" Context: SQL expression "r.c" (2 rows) select f1(); f1 ---- (1 row) create or replace function f1() returns void as $$ declare r record; begin for r in select * from g1() loop r.c := 20; end loop; end; $$ language plpgsql; select f1(); f1 ---- (1 row) select * from plpgsql_check_function('f1()'); plpgsql_check_function ------------------------------------------------------------------------ error:42703:6:assignment:record "r" has no field "c" Context: at assignment to field "c" of variable "r" declared on line 2 (2 rows) select f1(); f1 ---- (1 row) drop function f1(); drop function g1(); create function f1() returns int as $$ declare r int; begin if false then r := a + b; end if; return r; end; $$ language plpgsql; select f1(); f1 ---- (1 row) select * from plpgsql_check_function('f1()'); plpgsql_check_function ----------------------------------------------------------- error:42703:5:assignment:column "a" does not exist Query: r := a + b -- ^ Context: at assignment to variable "r" declared on line 2 (4 rows) select f1(); f1 ---- (1 row) drop function f1(); create or replace function f1() returns void as $$ declare r int[]; begin if false then r[c+10] := 20; end if; end; $$ language plpgsql; select f1(); f1 ---- (1 row) select * from plpgsql_check_function('f1()'); plpgsql_check_function ----------------------------------------------------------- error:42703:5:assignment:column "c" does not exist Query: r[c+10] := 20 -- ^ Context: at assignment to variable "r" declared on line 2 (4 rows) select f1(); f1 ---- (1 row) drop function f1(); create or replace function f1() returns void as $$ declare r int; begin if false then r[10] := 20; end if; end; $$ language plpgsql set search_path = public; select f1(); f1 ---- (1 row) select * from plpgsql_check_function('f1()'); plpgsql_check_function ------------------------------------------------------------------------------------------------- error:42804:5:assignment:cannot subscript type integer because it does not support subscripting Query: r[10] := 20 -- ^ Context: at assignment to variable "r" declared on line 2 (4 rows) select f1(); f1 ---- (1 row) drop function f1(); create or replace function f1_trg() returns trigger as $$ begin if new.a > 10 then raise notice '%', new.b; raise notice '%', new.c; end if; return new; end; $$ language plpgsql; create trigger t1_f1 before insert on t1 for each row execute procedure f1_trg(); insert into t1 values(6,30); select * from plpgsql_check_function('f1_trg()','t1'); plpgsql_check_function --------------------------------------------------- error:42703:5:RAISE:record "new" has no field "c" Context: SQL expression "new.c" (2 rows) insert into t1 values(6,30); create or replace function f1_trg() returns trigger as $$ begin new.a := new.a + 10; new.b := new.b + 10; new.c := 30; return new; end; $$ language plpgsql; -- should to fail select * from plpgsql_check_function('f1_trg()','t1'); plpgsql_check_function -------------------------------------------------------------------------- error:42703:5:assignment:record "new" has no field "c" Context: at assignment to field "c" of variable "new" declared on line 0 (2 rows) -- should to fail but not crash insert into t1 values(6,30); ERROR: record "new" has no field "c" CONTEXT: PL/pgSQL assignment "new.c := 30" PL/pgSQL function f1_trg() line 5 at assignment create or replace function f1_trg() returns trigger as $$ begin new.a := new.a + 10; new.b := new.b + 10; return new; end; $$ language plpgsql; -- ok select * from plpgsql_check_function('f1_trg()', 't1'); plpgsql_check_function ------------------------ (0 rows) -- ok insert into t1 values(6,30); select * from t1; a | b ----+---- 6 | 30 6 | 30 16 | 40 (3 rows) drop trigger t1_f1 on t1; drop function f1_trg(); -- test of showing caret on correct place for multiline queries create or replace function f1() returns void as $$ begin select var from foo; end; $$ language plpgsql; select * from plpgsql_check_function('f1()'); plpgsql_check_function ----------------------------------------------------------- error:42P01:3:SQL statement:relation "foo" does not exist Query: select var from foo -- ^ (6 rows) drop function f1(); create or replace function f1() returns int as $$ begin return (select a from t1 where hh = 20); end; $$ language plpgsql; select * from plpgsql_check_function('f1()'); plpgsql_check_function ------------------------------------------------- error:42703:3:RETURN:column "hh" does not exist Query: (select a from t1 where hh = 20) -- ^ (5 rows) create or replace function f1() returns int as $$ begin return (select a from txxxxxxx where hh = 20); end; $$ language plpgsql; select * from plpgsql_check_function('f1()'); plpgsql_check_function --------------------------------------------------------- error:42P01:3:RETURN:relation "txxxxxxx" does not exist Query: (select a from txxxxxxx -- ^ where hh = 20) (5 rows) drop function f1(); drop table t1; -- raise warnings when target row has different number of attributies in -- SELECT INTO statement create or replace function f1() returns void as $$ declare a1 int; a2 int; begin select 10,20 into a1,a2; end; $$ language plpgsql; -- should be ok select * from plpgsql_check_function('f1()'); plpgsql_check_function -------------------------------------------------------- warning extra:00000:2:DECLARE:never read variable "a1" warning extra:00000:2:DECLARE:never read variable "a2" (2 rows) create or replace function f1() returns void as $$ declare a1 int; begin select 10,20 into a1; end; $$ language plpgsql; -- raise warning select * from plpgsql_check_function('f1()'); plpgsql_check_function ------------------------------------------------------------------------ warning:00000:4:SQL statement:too many attributes for target variables Query: select 10,20 -- ^ Detail: There are less target variables than output columns in query. Hint: Check target variables in SELECT INTO statement Context: at SQL statement to a1 variables warning extra:00000:2:DECLARE:never read variable "a1" (7 rows) create or replace function f1() returns void as $$ declare a1 int; a2 int; begin select 10 into a1,a2; end; $$ language plpgsql; -- raise warning select * from plpgsql_check_function('f1()'); plpgsql_check_function ----------------------------------------------------------------------- warning:00000:4:SQL statement:too few attributes for target variables Query: select 10 -- ^ Detail: There are more target variables than output columns in query. Hint: Check target variables in SELECT INTO statement. Context: at SQL statement to a1, a2 variables warning extra:00000:2:DECLARE:never read variable "a1" warning extra:00000:2:DECLARE:never read variable "a2" (8 rows) -- bogus code set check_function_bodies to off; create or replace function f1() returns void as $$ adasdfsadf $$ language plpgsql; select * from plpgsql_check_function('f1()'); plpgsql_check_function ------------------------------------------------------------ error:42601:syntax error at or near "adasdfsadf" Query: adasdfsadf -- ^ Context: compilation of PL/pgSQL function "f1" near line 1 (6 rows) drop function f1(); create table f1tbl(a int, b int); -- unused variables create or replace function f1(_input1 int) returns table(_output1 int, _output2 int) as $$ declare _f1 int; _f2 int; _f3 int; _f4 int; _f5 int; _r record; _tbl f1tbl; begin if true then _f1 := 1; end if; select 1, 2 into _f3, _f4; perform 1 where _f5 is null; select 1 into _r; select 1, 2 into _tbl; -- check that SQLSTATE and SQLERRM don't raise false positives begin exception when raise_exception then end; end $$ language plpgsql; select * from plpgsql_check_function('f1(int)'); plpgsql_check_function ---------------------------------------------------------- warning:00000:4:DECLARE:unused variable "_f2" warning extra:00000:3:DECLARE:never read variable "_f1" warning extra:00000:5:DECLARE:never read variable "_f3" warning extra:00000:6:DECLARE:never read variable "_f4" warning extra:00000:8:DECLARE:never read variable "_r" warning extra:00000:9:DECLARE:never read variable "_tbl" warning extra:00000:unused parameter "_input1" warning extra:00000:unmodified OUT variable "_output1" warning extra:00000:unmodified OUT variable "_output2" (9 rows) drop function f1(int); drop table f1tbl; -- check that NEW and OLD are not reported unused create table f1tbl(); create or replace function f1() returns trigger as $$ begin return null; end $$ language plpgsql; select * from plpgsql_check_function('f1()', 'f1tbl'); plpgsql_check_function ------------------------ (0 rows) drop function f1(); drop table f1tbl; create table tabret(a int, b int); insert into tabret values(10,10); create or replace function f1() returns int as $$ begin return (select a from tabret); end; $$ language plpgsql; select * from plpgsql_check_function('f1()', performance_warnings := true); plpgsql_check_function ----------------------------------------------------------------------------------------- performance:00000:routine is marked as VOLATILE, should be STABLE Hint: When you fix this issue, please, recheck other functions that uses this function. (2 rows) create or replace function f1() returns int as $$ begin return (select a::numeric from tabret); end; $$ language plpgsql; select * from plpgsql_check_function('f1()', performance_warnings := true); plpgsql_check_function ----------------------------------------------------------------------------------------- performance:42804:3:RETURN:target type is different type than source type Detail: cast "numeric" value to "integer" type Hint: Hidden casting can be a performance issue. Context: at RETURN performance:00000:routine is marked as VOLATILE, should be STABLE Hint: When you fix this issue, please, recheck other functions that uses this function. (6 rows) create or replace function f1() returns int as $$ begin return (select a, b from tabret); end; $$ language plpgsql; select * from plpgsql_check_function('f1()', performance_warnings := true); plpgsql_check_function ----------------------------------------------------------- error:42601:3:RETURN:subquery must return only one column Query: (select a, b from tabret) -- ^ (3 rows) drop function f1(); create or replace function f1() returns table(ax int, bx int) as $$ begin return query select * from tabret; return; end; $$ language plpgsql; select * from plpgsql_check_function('f1()', performance_warnings := true); plpgsql_check_function ----------------------------------------------------------------------------------------- performance:00000:routine is marked as VOLATILE, should be STABLE Hint: When you fix this issue, please, recheck other functions that uses this function. (2 rows) drop function f1(); create or replace function f1() returns table(ax numeric, bx numeric) as $$ begin return query select * from tabret; return; end; $$ language plpgsql; select * from plpgsql_check_function('f1()', performance_warnings := true); plpgsql_check_function ----------------------------------------------------------------------------------- error:42804:3:RETURN QUERY:structure of query does not match function result type Detail: Returned type integer does not match expected type numeric in column 1. (2 rows) drop function f1(); create or replace function f1() returns setof tabret as $$ begin return query select * from tabret; return; end; $$ language plpgsql; select * from plpgsql_check_function('f1()', performance_warnings := true); plpgsql_check_function ----------------------------------------------------------------------------------------- performance:00000:routine is marked as VOLATILE, should be STABLE Hint: When you fix this issue, please, recheck other functions that uses this function. (2 rows) create or replace function f1() returns setof tabret as $$ begin return query select a from tabret; return; end; $$ language plpgsql; select * from plpgsql_check_function('f1()', performance_warnings := true); plpgsql_check_function ----------------------------------------------------------------------------------- error:42804:3:RETURN QUERY:structure of query does not match function result type Detail: Number of returned columns (1) does not match expected column count (2). (2 rows) create or replace function f1() returns setof tabret as $$ begin return query select a::numeric,b::numeric from tabret; return; end; $$ language plpgsql; select * from plpgsql_check_function('f1()', performance_warnings := true); plpgsql_check_function ----------------------------------------------------------------------------------- error:42804:3:RETURN QUERY:structure of query does not match function result type Detail: Returned type numeric does not match expected type integer in column 1. (2 rows) drop function f1(); create or replace function f1(a int) returns setof numeric as $$ begin return query select a; end $$ language plpgsql; select * from plpgsql_check_function('f1(int)', performance_warnings := true); plpgsql_check_function ----------------------------------------------------------------------------------- error:42804:2:RETURN QUERY:structure of query does not match function result type Detail: Returned type integer does not match expected type numeric in column 1. (2 rows) drop function f1(int); drop table tabret; create or replace function f1() returns void as $$ declare intval integer; begin intval := null; -- ok intval := 1; -- OK intval := '1'; -- OK intval := text '1'; -- not OK intval := current_date; -- not OK select 1 into intval; -- OK select '1' into intval; -- OK select text '1' into intval; -- not OK end $$ language plpgsql; select * from plpgsql_check_function('f1()', performance_warnings := true); plpgsql_check_function -------------------------------------------------------------------------------------- warning:42804:9:assignment:target type is different type than source type Query: intval := current_date -- ^ Detail: cast "date" value to "integer" type Hint: There are no possible explicit coercion between those types, possibly bug! Context: at assignment to variable "intval" declared on line 3 performance:00000:11:SQL statement:detected "SELECT expr INTO variable" Query: select 1 -- ^ Detail: This obsolete syntax of assigning can be slow. Hint: Use syntax "variable := expr" instead. performance:00000:12:SQL statement:detected "SELECT expr INTO variable" Query: select '1' -- ^ Detail: This obsolete syntax of assigning can be slow. Hint: Use syntax "variable := expr" instead. warning:42804:12:SQL statement:target type is different type than source type Query: select '1' -- ^ Detail: cast "text" value to "integer" type Hint: The input expression type does not have an assignment cast to the target type. Context: at SQL statement to variable "intval" declared on line 3 performance:00000:13:SQL statement:detected "SELECT expr INTO variable" Query: select text '1' -- ^ Detail: This obsolete syntax of assigning can be slow. Hint: Use syntax "variable := expr" instead. warning:42804:13:SQL statement:target type is different type than source type Query: select text '1' -- ^ Detail: cast "text" value to "integer" type Hint: The input expression type does not have an assignment cast to the target type. Context: at SQL statement to variable "intval" declared on line 3 warning extra:00000:3:DECLARE:never read variable "intval" (34 rows) drop function f1(); create or replace function f1() returns int as $$ begin return 1; end; $$ language plpgsql; select * from plpgsql_check_function('f1()', performance_warnings := true); plpgsql_check_function ----------------------------------------------------------------------------------------- performance:00000:routine is marked as VOLATILE, should be IMMUTABLE Hint: When you fix this issue, please, recheck other functions that uses this function. (2 rows) create or replace function f1() returns int as $$ begin return 1::numeric; end; $$ language plpgsql; select * from plpgsql_check_function('f1()', performance_warnings := true); plpgsql_check_function ----------------------------------------------------------------------------------------- performance:42804:3:RETURN:target type is different type than source type Detail: cast "numeric" value to "integer" type Hint: Hidden casting can be a performance issue. Context: at RETURN performance:00000:routine is marked as VOLATILE, should be IMMUTABLE Hint: When you fix this issue, please, recheck other functions that uses this function. (6 rows) create or replace function f1() returns int as $$ begin return null; end; $$ language plpgsql; select * from plpgsql_check_function('f1()', performance_warnings := true); plpgsql_check_function ----------------------------------------------------------------------------------------- performance:00000:routine is marked as VOLATILE, should be IMMUTABLE Hint: When you fix this issue, please, recheck other functions that uses this function. (2 rows) create or replace function f1() returns int as $$ begin return current_date; end; $$ language plpgsql; select * from plpgsql_check_function('f1()', performance_warnings := true); plpgsql_check_function ----------------------------------------------------------------------------------------- warning:42804:3:RETURN:target type is different type than source type Detail: cast "date" value to "integer" type Hint: There are no possible explicit coercion between those types, possibly bug! Context: at RETURN performance:00000:routine is marked as VOLATILE, should be STABLE Hint: When you fix this issue, please, recheck other functions that uses this function. (6 rows) create or replace function f1() returns int as $$ declare a int; begin return a; end; $$ language plpgsql; select * from plpgsql_check_function('f1()', performance_warnings := true); plpgsql_check_function ----------------------------------------------------------------------------------------- performance:00000:routine is marked as VOLATILE, should be IMMUTABLE Hint: When you fix this issue, please, recheck other functions that uses this function. (2 rows) create or replace function f1() returns int as $$ declare a numeric; begin return a; end; $$ language plpgsql; select * from plpgsql_check_function('f1()', performance_warnings := true); plpgsql_check_function ----------------------------------------------------------------------------------------- performance:42804:4:RETURN:target type is different type than source type Detail: cast "numeric" value to "integer" type Hint: Hidden casting can be a performance issue. Context: at RETURN of variable "a" declared on line 2 performance:00000:routine is marked as VOLATILE, should be IMMUTABLE Hint: When you fix this issue, please, recheck other functions that uses this function. (6 rows) drop function f1(); create or replace function f1() returns setof int as $$ begin return next 1; end; $$ language plpgsql; select * from plpgsql_check_function('f1()', performance_warnings := true); plpgsql_check_function ----------------------------------------------------------------------------------------- performance:00000:routine is marked as VOLATILE, should be IMMUTABLE Hint: When you fix this issue, please, recheck other functions that uses this function. (2 rows) create or replace function f1() returns setof int as $$ begin return next 1::numeric; -- tolerant, doesn't use tupmap end; $$ language plpgsql; select * from plpgsql_check_function('f1()', performance_warnings := true); plpgsql_check_function ----------------------------------------------------------------------------------------- performance:42804:3:RETURN NEXT:target type is different type than source type Detail: cast "numeric" value to "integer" type Hint: Hidden casting can be a performance issue. Context: at RETURN NEXT performance:00000:routine is marked as VOLATILE, should be IMMUTABLE Hint: When you fix this issue, please, recheck other functions that uses this function. (6 rows) drop function f1(); create type t1 as (a int, b int, c int); create type t2 as (a int, b numeric); create or replace function fx() returns t2 as $$ declare x t1; begin return x; end; $$ language plpgsql; select * from plpgsql_check_function('fx()', performance_warnings := true); plpgsql_check_function --------------------------------------------------------------------------------- error:42804:4:RETURN:returned record type does not match expected record type Detail: Returned type integer does not match expected type numeric in column 2. (2 rows) create or replace function fx() returns t2 as $$ declare x t2; begin return x; end; $$ language plpgsql; select * from plpgsql_check_function('fx()', performance_warnings := true); plpgsql_check_function ----------------------------------------------------------------------------------------- performance:00000:routine is marked as VOLATILE, should be IMMUTABLE Hint: When you fix this issue, please, recheck other functions that uses this function. (2 rows) drop function fx(); create or replace function fx() returns setof t2 as $$ declare x t1; begin return next x; return; end; $$ language plpgsql; select * from plpgsql_check_function('fx()', performance_warnings := true); plpgsql_check_function --------------------------------------------------------------------------------- error:42804:4:RETURN NEXT:wrong record type supplied in RETURN NEXT Detail: Returned type integer does not match expected type numeric in column 2. (2 rows) create or replace function fx() returns setof t2 as $$ declare x t2; begin return next x; return; end; $$ language plpgsql; select * from plpgsql_check_function('fx()', performance_warnings := true); plpgsql_check_function ----------------------------------------------------------------------------------------- performance:00000:routine is marked as VOLATILE, should be IMMUTABLE Hint: When you fix this issue, please, recheck other functions that uses this function. (2 rows) drop function fx(); create or replace function fx2(_id int, _pa_id varchar(32), _status varchar(60)) returns void as $$ declare begin insert into pa values(_id, _pa_id, _status); exception when OTHERS then raise notice '%', 'some message'; raise exception '%', sqlerrm; end $$ language plpgsql; select * from plpgsql_check_function('fx2(int, varchar, varchar)', performance_warnings := true); plpgsql_check_function ------------------------ (0 rows) create or replace function fx2(_id int, _pa_id varchar(32), _status varchar(60)) returns void as $$ declare begin insert into pa values(_id, _pa_id, _status) returning *; exception when OTHERS then raise notice '%', 'some message'; raise exception '%', sqlerrm; end $$ language plpgsql; select * from plpgsql_check_function('fx2(int, varchar, varchar)', performance_warnings := true); plpgsql_check_function ---------------------------------------------------------------------- error:42601:4:SQL statement:query has no destination for result data (1 row) create or replace function fx2(_id int, _pa_id varchar(32), _status varchar(60)) returns void as $$ declare begin SELECT * FROM pa LIMIT 1; exception when OTHERS then raise notice '%', 'some message'; raise exception '%', sqlerrm; end $$ language plpgsql; select * from plpgsql_check_function('fx2(int, varchar, varchar)', performance_warnings := true); plpgsql_check_function ---------------------------------------------------------------------- error:42601:4:SQL statement:query has no destination for result data (1 row) drop function fx2(int, varchar, varchar); create or replace function foreach_array_loop() returns void as $body$ declare arr text[]; el text; begin arr := array['1111','2222','3333']; foreach el in array arr loop raise notice '%', el; end loop; end; $body$ language 'plpgsql' stable; select * from plpgsql_check_function_tb('foreach_array_loop()', performance_warnings := true); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context --------------------+--------+-----------+----------+--------------------------------------------------+--------+-----------------------------------------------------------------------------------+-------------+----------+-------+--------- foreach_array_loop | | | 00000 | routine is marked as STABLE, should be IMMUTABLE | | When you fix this issue, please, recheck other functions that uses this function. | performance | | | (1 row) create or replace function foreach_array_loop() returns void as $body$ declare arr text[]; el int; begin arr := array['1111','2222','3333']; foreach el in array arr loop raise notice '%', el; end loop; end; $body$ language 'plpgsql' stable; select * from plpgsql_check_function_tb('foreach_array_loop()', performance_warnings := true); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context --------------------+--------+--------------------+----------+--------------------------------------------------+-------------------------------------+-----------------------------------------------------------------------------------+-------------+----------+-------+----------------------- foreach_array_loop | 7 | FOREACH over array | 42804 | target type is different type than source type | cast "text" value to "integer" type | The input expression type does not have an assignment cast to the target type. | warning | | | at FOREACH over array foreach_array_loop | | | 00000 | routine is marked as STABLE, should be IMMUTABLE | | When you fix this issue, please, recheck other functions that uses this function. | performance | | | (2 rows) create or replace function foreach_array_loop() returns void as $body$ declare arr date[]; el int; begin -- expression is not constant (else this check fails due tracking constant) arr := array['2014-01-01','2015-01-01','2016-01-01', current_date]::date[]; foreach el in array arr loop raise notice '%', el; end loop; end; $body$ language 'plpgsql' stable; select * from plpgsql_check_function_tb('foreach_array_loop()', performance_warnings := true); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context --------------------+--------+--------------------+----------+------------------------------------------------+-------------------------------------+----------------------------------------------------------------------------+---------+----------+-------+----------------------- foreach_array_loop | 8 | FOREACH over array | 42804 | target type is different type than source type | cast "date" value to "integer" type | There are no possible explicit coercion between those types, possibly bug! | warning | | | at FOREACH over array (1 row) create or replace function foreach_array_loop() returns void as $body$ declare arr date[]; el int; begin arr := array['2014-01-01','2015-01-01','2016-01-01']::date[]; foreach el in array arr loop raise notice '%', el; end loop; end; $body$ language 'plpgsql' stable; select * from plpgsql_check_function_tb('foreach_array_loop()', performance_warnings := true); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context --------------------+--------+--------------------+----------+-----------------------------------------------------+--------+------+-------+----------+-------+--------- foreach_array_loop | 7 | FOREACH over array | 22P02 | invalid input syntax for type integer: "01-01-2014" | | | error | | | (1 row) create or replace function foreach_array_loop() returns void as $body$ declare el text; begin foreach el in array array['1111','2222','3333'] loop raise notice '%', el; end loop; end; $body$ language 'plpgsql' stable; select * from plpgsql_check_function_tb('foreach_array_loop()', performance_warnings := true); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context --------------------+--------+-----------+----------+--------------------------------------------------+--------+-----------------------------------------------------------------------------------+-------------+----------+-------+--------- foreach_array_loop | | | 00000 | routine is marked as STABLE, should be IMMUTABLE | | When you fix this issue, please, recheck other functions that uses this function. | performance | | | (1 row) create or replace function foreach_array_loop() returns void as $body$ declare el int; begin foreach el in array array['1111','2222','3333'] loop raise notice '%', el; end loop; end; $body$ language 'plpgsql' stable; select * from plpgsql_check_function_tb('foreach_array_loop()', performance_warnings := true); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context --------------------+--------+--------------------+----------+--------------------------------------------------+-------------------------------------+-----------------------------------------------------------------------------------+-------------+----------+-------+----------------------- foreach_array_loop | 5 | FOREACH over array | 42804 | target type is different type than source type | cast "text" value to "integer" type | The input expression type does not have an assignment cast to the target type. | warning | | | at FOREACH over array foreach_array_loop | | | 00000 | routine is marked as STABLE, should be IMMUTABLE | | When you fix this issue, please, recheck other functions that uses this function. | performance | | | (2 rows) create or replace function foreach_array_loop() returns void as $body$ declare el int; begin foreach el in array array['2014-01-01','2015-01-01','2016-01-01']::date[] loop raise notice '%', el; end loop; end; $body$ language 'plpgsql' stable; select * from plpgsql_check_function_tb('foreach_array_loop()', performance_warnings := true); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context --------------------+--------+--------------------+----------+-----------------------------------------------------+--------+------+-------+----------+-------+--------- foreach_array_loop | 5 | FOREACH over array | 22P02 | invalid input syntax for type integer: "01-01-2014" | | | error | | | (1 row) drop function foreach_array_loop(); create or replace function scan_rows(int[]) returns void AS $$ declare x int[]; begin foreach x slice 1 in array $1 loop raise notice 'row = %', x; end loop; end; $$ language plpgsql; select * from plpgsql_check_function_tb('scan_rows(int[])', performance_warnings := true); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+----------------------------------------------------+--------+-----------------------------------------------------------------------------------+-------------+----------+-------+--------- scan_rows | | | 00000 | routine is marked as VOLATILE, should be IMMUTABLE | | When you fix this issue, please, recheck other functions that uses this function. | performance | | | (1 row) create or replace function scan_rows(int[]) returns void AS $$ declare x int[]; begin foreach x in array $1 loop raise notice 'row = %', x; end loop; end; $$ language plpgsql; select * from plpgsql_check_function_tb('scan_rows(int[])', performance_warnings := true); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+--------------------+----------+----------------------------------------------------+------------------------------------------+-----------------------------------------------------------------------------------+-------------+----------+-------+----------------------- scan_rows | 5 | FOREACH over array | 42804 | target type is different type than source type | cast "integer" value to "integer[]" type | There are no possible explicit coercion between those types, possibly bug! | warning | | | at FOREACH over array scan_rows | | | 00000 | routine is marked as VOLATILE, should be IMMUTABLE | | When you fix this issue, please, recheck other functions that uses this function. | performance | | | (2 rows) drop function scan_rows(int[]); drop type t1; drop type t2; create table t1(a int, b int); create table t2(a int, b int, c int); create table t3(a numeric, b int); insert into t1 values(10,20),(30,40); create or replace function fx() returns int as $$ declare s int default 0; r t1; begin foreach r in array (select array_agg(t1) from t1) loop s := r.a + r.b; end loop; return s; end; $$ language plpgsql; select * from plpgsql_check_function_tb('fx()', performance_warnings := true); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+-------------------------------------------------+--------+-----------------------------------------------------------------------------------+-------------+----------+-------+--------- fx | | | 00000 | routine is marked as VOLATILE, should be STABLE | | When you fix this issue, please, recheck other functions that uses this function. | performance | | | (1 row) create or replace function fx() returns int as $$ declare s int default 0; r t1; c t1[]; begin c := (select array_agg(t1) from t1); foreach r in array c loop s := r.a + r.b; end loop; return s; end; $$ language plpgsql; select * from plpgsql_check_function_tb('fx()', performance_warnings := true); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+-------------------------------------------------+--------+-----------------------------------------------------------------------------------+-------------+----------+-------+--------- fx | | | 00000 | routine is marked as VOLATILE, should be STABLE | | When you fix this issue, please, recheck other functions that uses this function. | performance | | | (1 row) create or replace function fx() returns int as $$ declare s int default 0; r t1; c t1[]; begin select array_agg(t1) into c from t1; foreach r in array c loop s := r.a + r.b; end loop; return s; end; $$ language plpgsql; select * from plpgsql_check_function_tb('fx()', performance_warnings := true); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+-------------------------------------------------+--------+-----------------------------------------------------------------------------------+-------------+----------+-------+--------- fx | | | 00000 | routine is marked as VOLATILE, should be STABLE | | When you fix this issue, please, recheck other functions that uses this function. | performance | | | (1 row) create or replace function fx() returns int as $$ declare s int default 0; r t1; c t1[]; begin select array_agg(t1) into c from t1; for i in array_lower(c, 1) .. array_upper(c, 1) loop r := c[i]; s := r.a + r.b; end loop; return s; end; $$ language plpgsql; select * from plpgsql_check_function_tb('fx()', performance_warnings := true); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+-------------------------------------------------+--------+-----------------------------------------------------------------------------------+-------------+----------+-------+--------- fx | | | 00000 | routine is marked as VOLATILE, should be STABLE | | When you fix this issue, please, recheck other functions that uses this function. | performance | | | (1 row) create or replace function fx() returns int as $$ declare s int default 0; c t1[]; begin select array_agg(t1) into c from t1; for i in array_lower(c, 1) .. array_upper(c, 1) loop s := (c[i]).a + (c[i]).b; end loop; return s; end; $$ language plpgsql; select * from plpgsql_check_function_tb('fx()', performance_warnings := true); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+-------------------------------------------------+--------+-----------------------------------------------------------------------------------+-------------+----------+-------+--------- fx | | | 00000 | routine is marked as VOLATILE, should be STABLE | | When you fix this issue, please, recheck other functions that uses this function. | performance | | | (1 row) create or replace function fx() returns int as $$ declare s int default 0; r record; c t1[]; begin select array_agg(t1) into c from t1; for i in array_lower(c, 1) .. array_upper(c, 1) loop r := c[i]; s := r.a + r.b; end loop; return s; end; $$ language plpgsql; select * from plpgsql_check_function_tb('fx()', performance_warnings := true); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+-------------------------------------------------+--------+-----------------------------------------------------------------------------------+-------------+----------+-------+--------- fx | | | 00000 | routine is marked as VOLATILE, should be STABLE | | When you fix this issue, please, recheck other functions that uses this function. | performance | | | (1 row) create or replace function fx() returns int as $$ declare s int default 0; r record; c t1[]; begin select array_agg(t1) into c from t1; for i in array_lower(c, 1) .. array_upper(c, 1) loop r := c[i]; s := r.a + r.b + r.c; end loop; return s; end; $$ language plpgsql; select * from plpgsql_check_function_tb('fx()', performance_warnings := true); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+------------+----------+-----------------------------+--------+------+-------+----------+-------+-------------------------------------------- fx | 11 | assignment | 42703 | record "r" has no field "c" | | | error | | | PL/pgSQL assignment "s := r.a + r.b + r.c" (1 row) create or replace function fx() returns int as $$ declare s int default 0; r t2; begin foreach r in array (select array_agg(t1) from t1) loop s := r.a + r.b; end loop; return s; end; $$ language plpgsql; select * from plpgsql_check_function_tb('fx()', performance_warnings := true); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+--------------------+----------+-------------------------------------------------+--------+-----------------------------------------------------------------------------------+-------------+----------+-------+----------------------- fx | 6 | FOREACH over array | 00000 | too few attributes for composite variable | | | warning | | | at FOREACH over array fx | | | 00000 | routine is marked as VOLATILE, should be STABLE | | When you fix this issue, please, recheck other functions that uses this function. | performance | | | (2 rows) create or replace function fx() returns int as $$ declare s int default 0; r t3; begin foreach r in array (select array_agg(t1) from t1) loop s := r.a + r.b; end loop; return s; end; $$ language plpgsql; select * from plpgsql_check_function_tb('fx()', performance_warnings := true); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+--------------------+----------+-------------------------------------------------+----------------------------------------+-----------------------------------------------------------------------------------+-------------+----------+----------------+-------------------------------------------------- fx | 6 | FOREACH over array | 42804 | target type is different type than source type | cast "integer" value to "numeric" type | Hidden casting can be a performance issue. | performance | | | at FOREACH over array fx | 8 | assignment | 42804 | target type is different type than source type | cast "numeric" value to "integer" type | Hidden casting can be a performance issue. | performance | 1 | s := r.a + r.b | at assignment to variable "s" declared on line 3 fx | | | 00000 | routine is marked as VOLATILE, should be STABLE | | When you fix this issue, please, recheck other functions that uses this function. | performance | | | (3 rows) drop function fx(); drop table t1; -- mscottie issue #13 create table test ( a text, b integer, c uuid ); create function before_insert_test() returns trigger language plpgsql as $$ begin select a into NEW.a from test where b = 1; select b into NEW.b from test where b = 1; select null::uuid into NEW.c from test where b = 1; return new; end; $$; select * from plpgsql_check_function_tb('before_insert_test()','test'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+---------+--------+------+-------+----------+-------+--------- (0 rows) create or replace function before_insert_test() returns trigger language plpgsql as $$ begin NEW.a := (select a from test where b = 1); NEW.b := (select b from test where b = 1); NEW.c := (select c from test where b = 1); return new; end; $$; select * from plpgsql_check_function_tb('before_insert_test()','test', fatal_errors := false); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+---------+--------+------+-------+----------+-------+--------- (0 rows) create or replace function before_insert_test() returns trigger language plpgsql as $$ begin NEW.a := 'Hello'::text; NEW.b := 10; NEW.c := null::uuid; return new; end; $$; select * from plpgsql_check_function_tb('before_insert_test()','test', fatal_errors := false); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+---------+--------+------+-------+----------+-------+--------- (0 rows) drop function before_insert_test(); create or replace function fx() returns void as $$ declare NEW test; OLD test; begin select null::uuid into NEW.c from test where b = 1; end; $$ language plpgsql; select * from plpgsql_check_function_tb('fx()', performance_warnings := true, fatal_errors := false); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+---------------------------+--------+------+---------------+----------+-------+--------- fx | 2 | DECLARE | 00000 | unused variable "old" | | | warning | | | fx | 2 | DECLARE | 00000 | never read variable "new" | | | warning extra | | | (2 rows) drop function fx(); create or replace function fx() returns void as $$ declare NEW test; begin NEW.a := 'Hello'::text; NEW.b := 10; NEW.c := null::uuid; end; $$ language plpgsql; select * from plpgsql_check_function_tb('fx()', performance_warnings := true, fatal_errors := false); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+----------------------------------------------------+--------+-----------------------------------------------------------------------------------+---------------+----------+-------+--------- fx | 2 | DECLARE | 00000 | never read variable "new" | | | warning extra | | | fx | | | 00000 | routine is marked as VOLATILE, should be IMMUTABLE | | When you fix this issue, please, recheck other functions that uses this function. | performance | | | (2 rows) drop function fx(); drop table test; create or replace function fx() returns void as $$ declare s int; sa int[]; sd date; bs int[]; begin sa[10] := s; sa[10] := sd; s := bs[10]; end; $$ language plpgsql; select * from plpgsql_check_function_tb('fx()', performance_warnings := true, fatal_errors := false); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+------------+----------+------------------------------------------------+-------------------------------------+----------------------------------------------------------------------------+---------------+----------+--------------+--------------------------------------------------- fx | 9 | assignment | 42804 | target type is different type than source type | cast "date" value to "integer" type | There are no possible explicit coercion between those types, possibly bug! | warning | 1 | sa[10] := sd | at assignment to variable "sa" declared on line 4 fx | 4 | DECLARE | 00000 | never read variable "sa" | | | warning extra | | | (2 rows) drop function fx(); create type t as (t text); create or replace function fx() returns void as $$ declare _t t; _tt t[]; _txt text; begin _t.t := 'ABC'; -- correct warning "unknown" _tt[1] := _t; _txt := _t; end; $$ language plpgsql; select * from plpgsql_check_function('fx()', performance_warnings := true); plpgsql_check_function ------------------------------------------------------------------------------- performance:42804:7:assignment:target type is different type than source type Query: _txt := _t -- ^ Detail: cast "t" value to "text" type Hint: Hidden casting can be a performance issue. Context: at assignment to variable "_txt" declared on line 3 warning extra:00000:2:DECLARE:never read variable "_tt" warning extra:00000:3:DECLARE:never read variable "_txt" (8 rows) drop function fx(); create or replace function fx() returns void as $$ declare _t1 t; _t2 t; begin _t1.t := 'ABC'::text; _t2 := _t1; raise notice '% %', _t2, _t2.t; end; $$ language plpgsql; select * from plpgsql_check_function('fx()', performance_warnings := true); plpgsql_check_function ----------------------------------------------------------------------------------------- performance:00000:routine is marked as VOLATILE, should be IMMUTABLE Hint: When you fix this issue, please, recheck other functions that uses this function. (2 rows) drop function fx(); create or replace function fx(out _tt t[]) as $$ declare _t t; begin _t.t := 'ABC'::text; _tt[1] := _t; end; $$ language plpgsql; select * from plpgsql_check_function('fx()', performance_warnings := true); plpgsql_check_function ----------------------------------------------------------------------------------------- performance:00000:routine is marked as VOLATILE, should be IMMUTABLE Hint: When you fix this issue, please, recheck other functions that uses this function. (2 rows) drop function fx(); drop type t; create or replace function fx() returns int as $$ declare x int; begin perform 1; return 10; end; $$ language plpgsql; select * from plpgsql_check_function('fx()', performance_warnings := true); plpgsql_check_function ----------------------------------------------------------------------------------------- warning:00000:2:DECLARE:unused variable "x" performance:00000:routine is marked as VOLATILE, should be IMMUTABLE Hint: When you fix this issue, please, recheck other functions that uses this function. (3 rows) drop function fx(); create table t(i int); create function test_t(OUT t) returns t AS $$ begin $1 := null; end; $$ language plpgsql; select test_t(); test_t -------- (1 row) select * from test_t(); i --- (1 row) select * from plpgsql_check_function('test_t()', performance_warnings := true); plpgsql_check_function ----------------------------------------------------------------------------------------- performance:00000:routine is marked as VOLATILE, should be IMMUTABLE Hint: When you fix this issue, please, recheck other functions that uses this function. (2 rows) create or replace function fx() returns void as $$ declare c cursor for select * from t; x varchar; begin open c; fetch c into x; close c; end; $$ language plpgsql; select test_t(); test_t -------- (1 row) select * from test_t(); i --- (1 row) select * from plpgsql_check_function('fx()', performance_warnings := true, fatal_errors := false); plpgsql_check_function -------------------------------------------------------------------------- performance:42804:7:FETCH:target type is different type than source type Detail: cast "integer" value to "character varying" type Hint: Hidden casting can be a performance issue. Context: at FETCH to variable "x" declared on line 4 warning extra:00000:4:DECLARE:never read variable "x" (5 rows) drop function fx(); create or replace function fx() returns void as $$ declare c cursor for select * from t; x int; begin open c; fetch c into x; close c; end; $$ language plpgsql; select test_t(); test_t -------- (1 row) select * from test_t(); i --- (1 row) select * from plpgsql_check_function('fx()', performance_warnings := true, fatal_errors := false); plpgsql_check_function ------------------------------------------------------- warning extra:00000:4:DECLARE:never read variable "x" (1 row) drop function fx(); create or replace function fx() returns void as $$ declare c cursor for select * from t; begin for r in c loop raise notice '%', r.a; end loop; end; $$ language plpgsql; select test_t(); test_t -------- (1 row) select * from test_t(); i --- (1 row) select * from plpgsql_check_function('fx()', performance_warnings := true, fatal_errors := false); plpgsql_check_function ------------------------------------------------------- error:42703:6:RAISE:record "r" has no field "a" Context: SQL expression "r.a" warning extra:00000:5:DECLARE:never read variable "r" (3 rows) drop function fx(); create or replace function fx() returns void as $$ declare c cursor for select * from t; begin for r in c loop raise notice '%', r.i; end loop; end; $$ language plpgsql; select test_t(); test_t -------- (1 row) select * from test_t(); i --- (1 row) select * from plpgsql_check_function('fx()', performance_warnings := true, fatal_errors := false); plpgsql_check_function ------------------------ (0 rows) drop function fx(); create table foo(a int, b int); create or replace function fx() returns void as $$ declare f1 int; f2 int; begin select 1, 2 into f1; select 1 into f1, f2; select a b into f1, f2 from foo; end; $$ language plpgsql; select fx(); fx ---- (1 row) select * from plpgsql_check_function('fx()', performance_warnings := true, fatal_errors := false); plpgsql_check_function ------------------------------------------------------------------------ warning:00000:4:SQL statement:too many attributes for target variables Query: select 1, 2 -- ^ Detail: There are less target variables than output columns in query. Hint: Check target variables in SELECT INTO statement Context: at SQL statement to f1 variables warning:00000:5:SQL statement:too few attributes for target variables Query: select 1 -- ^ Detail: There are more target variables than output columns in query. Hint: Check target variables in SELECT INTO statement. Context: at SQL statement to f1, f2 variables warning:00000:6:SQL statement:too few attributes for target variables Query: select a b from foo -- ^ Detail: There are more target variables than output columns in query. Hint: Check target variables in SELECT INTO statement. Context: at SQL statement to f1, f2 variables warning extra:00000:2:DECLARE:never read variable "f1" warning extra:00000:2:DECLARE:never read variable "f2" (20 rows) drop function fx(); drop table foo; create or replace function fx() returns void as $$ declare d date; begin d := (select 1 from pg_class limit 1); raise notice '%', d; end; $$ language plpgsql; select fx(); ERROR: invalid input syntax for type date: "1" CONTEXT: PL/pgSQL assignment "d := (select 1 from pg_class limit 1)" PL/pgSQL function fx() line 4 at assignment select * from plpgsql_check_function('fx()', performance_warnings := true, fatal_errors := false); plpgsql_check_function ---------------------------------------------------------------------------------- warning:42804:4:assignment:target type is different type than source type Query: d := (select 1 from pg_class limit 1) -- ^ Detail: cast "integer" value to "date" type Hint: There are no possible explicit coercion between those types, possibly bug! Context: at assignment to variable "d" declared on line 2 (6 rows) drop function fx(); create table tab_1(i int); create or replace function fx(a int) returns setof int as $$ declare c refcursor; r record; begin open c for select i from tab_1 where i = a; loop fetch c into r; if not found then exit; end if; return next r.i; end loop; end; $$ language plpgsql; select * from plpgsql_check_function('fx(int)', performance_warnings := true, fatal_errors := false); plpgsql_check_function ----------------------------------------------------------------------------------------- performance:00000:routine is marked as VOLATILE, should be STABLE Hint: When you fix this issue, please, recheck other functions that uses this function. (2 rows) create or replace function fx(a int) returns setof int as $$ declare c refcursor; r record; begin open c for select i from tab_1 where i = a; loop fetch c into r; if not found then exit; end if; return next r.x; end loop; end; $$ language plpgsql; select * from plpgsql_check_function('fx(int)', performance_warnings := true, fatal_errors := false); plpgsql_check_function ----------------------------------------------------------------------------------------- error:42703:12:RETURN NEXT:record "r" has no field "x" Context: SQL expression "r.x" warning extra:00000:4:DECLARE:never read variable "r" performance:00000:routine is marked as VOLATILE, should be STABLE Hint: When you fix this issue, please, recheck other functions that uses this function. (5 rows) drop function fx(int); drop table tab_1; create or replace function fxx() returns void as $$ begin rollback; end; $$ language plpgsql; select fxx(); ERROR: invalid transaction termination CONTEXT: PL/pgSQL function fxx() line 3 at ROLLBACK select * from plpgsql_check_function('fxx()'); plpgsql_check_function -------------------------------------------------------- error:2D000:3:ROLLBACK:invalid transaction termination (1 row) drop function fxx(); create or replace function fxx() returns void as $$ declare x int; begin declare x int; begin end; end; $$ language plpgsql; select * from plpgsql_check_function('fxx()'); plpgsql_check_function ------------------------------------------------------------------------------------------ warning extra:00000:5:statement block:variable "x" shadows a previously defined variable Hint: SET plpgsql.extra_warnings TO 'shadowed_variables' warning:00000:2:DECLARE:unused variable "x" warning:00000:4:DECLARE:unused variable "x" (4 rows) select * from plpgsql_check_function('fxx()', extra_warnings := false); plpgsql_check_function --------------------------------------------- warning:00000:2:DECLARE:unused variable "x" warning:00000:4:DECLARE:unused variable "x" (2 rows) drop function fxx(); create or replace function fxx(in a int, in b int, out c int, out d int) as $$ begin c := a; end; $$ language plpgsql; select * from plpgsql_check_function('fxx(int, int)'); plpgsql_check_function ------------------------------------------------- warning extra:00000:unused parameter "b" warning extra:00000:unmodified OUT variable "d" (2 rows) create or replace function fxx(in a int, in b int, out c int, out d int) as $$ begin c := d; end; $$ language plpgsql; select * from plpgsql_check_function('fxx(int, int)'); plpgsql_check_function ------------------------------------------------- warning extra:00000:unused parameter "a" warning extra:00000:unused parameter "b" warning extra:00000:unmodified OUT variable "d" (3 rows) create type ct as (a int, b int); create or replace function fxx(a ct, b ct, OUT c ct, OUT d ct) as $$ begin c.a := a.a; end; $$ language plpgsql; select * from plpgsql_check_function('fxx(ct, ct)'); plpgsql_check_function ----------------------------------------------------------------------- warning extra:00000:unused parameter "b" warning extra:00000:composite OUT variable "c" is not single argument warning extra:00000:composite OUT variable "d" is not single argument warning extra:00000:unmodified OUT variable "d" (4 rows) create or replace function fxx(a ct, b ct, OUT c ct, OUT d ct) as $$ begin c.a := d.a; end; $$ language plpgsql; select * from plpgsql_check_function('fxx(ct, ct)'); plpgsql_check_function ----------------------------------------------------------------------- warning extra:00000:unused parameter "a" warning extra:00000:unused parameter "b" warning extra:00000:composite OUT variable "c" is not single argument warning extra:00000:composite OUT variable "d" is not single argument warning extra:00000:unmodified OUT variable "d" (5 rows) create or replace function tx(a int) returns int as $$ declare a int; ax int; begin declare ax int; begin ax := 10; end; a := 10; return 20; end; $$ language plpgsql; select * from plpgsql_check_function('tx(int)'); plpgsql_check_function ------------------------------------------------------------------------------------------- warning:00000:3:statement block:parameter "a" is shadowed Detail: Local variable shadows function parameter. warning extra:00000:5:statement block:variable "ax" shadows a previously defined variable Hint: SET plpgsql.extra_warnings TO 'shadowed_variables' warning:00000:2:DECLARE:unused variable "ax" warning extra:00000:2:DECLARE:never read variable "a" warning extra:00000:4:DECLARE:never read variable "ax" warning extra:00000:unused parameter "a" (8 rows) create type xt as (a int, b int, c int); create or replace function fx_xt(out x xt) as $$ declare l xt; a int; begin return; end; $$ language plpgsql; select * from plpgsql_check_function('fx_xt()'); plpgsql_check_function ------------------------------------------------- warning:00000:2:DECLARE:unused variable "l" warning:00000:3:DECLARE:unused variable "a" warning extra:00000:unmodified OUT variable "x" (3 rows) drop function fx_xt(); create or replace function fx_xt(out x xt) as $$ declare l xt; a int; begin x.c := 1000; return; end; $$ language plpgsql; select * from plpgsql_check_function('fx_xt()'); plpgsql_check_function --------------------------------------------- warning:00000:2:DECLARE:unused variable "l" warning:00000:3:DECLARE:unused variable "a" (2 rows) drop function fx_xt(); create or replace function fx_xt(out x xt, out y xt) as $$ declare c1 xt; c2 xt; begin return; end; $$ language plpgsql; select * from plpgsql_check_function('fx_xt()'); plpgsql_check_function ----------------------------------------------------------------------- warning:00000:2:DECLARE:unused variable "c1" warning:00000:2:DECLARE:unused variable "c2" warning extra:00000:composite OUT variable "x" is not single argument warning extra:00000:unmodified OUT variable "x" warning extra:00000:composite OUT variable "y" is not single argument warning extra:00000:unmodified OUT variable "y" (6 rows) drop function fx_xt(); create or replace function fx_xt(out x xt, out y xt) as $$ declare c1 xt; c2 xt; begin x.a := 100; y := row(10,20,30); return; end; $$ language plpgsql; select * from plpgsql_check_function('fx_xt()'); plpgsql_check_function ----------------------------------------------------------------------- warning:00000:2:DECLARE:unused variable "c1" warning:00000:2:DECLARE:unused variable "c2" warning extra:00000:composite OUT variable "x" is not single argument warning extra:00000:composite OUT variable "y" is not single argument (4 rows) drop function fx_xt(); create or replace function fx_xt(out x xt, out z int) as $$ begin return; end; $$ language plpgsql; select * from plpgsql_check_function('fx_xt()'); plpgsql_check_function ----------------------------------------------------------------------- warning extra:00000:composite OUT variable "x" is not single argument warning extra:00000:unmodified OUT variable "x" warning extra:00000:unmodified OUT variable "z" (3 rows) drop function fx_xt(); drop type xt; -- missing RETURN create or replace function fx_flow() returns int as $$ begin raise notice 'kuku'; end; $$ language plpgsql; select fx_flow(); NOTICE: kuku ERROR: control reached end of function without RETURN CONTEXT: PL/pgSQL function fx_flow() select * from plpgsql_check_function('fx_flow()'); plpgsql_check_function ------------------------------------------------------------ error:2F005:control reached end of function without RETURN (1 row) -- ok create or replace function fx_flow() returns int as $$ declare a int; begin if a > 10 then return a; end if; return 10; end; $$ language plpgsql; select * from plpgsql_check_function('fx_flow()'); plpgsql_check_function ------------------------ (0 rows) -- dead code create or replace function fx_flow() returns int as $$ declare a int; begin if a > 10 then return a; else return a + 1; end if; return 10; end; $$ language plpgsql; select * from plpgsql_check_function('fx_flow()'); plpgsql_check_function ----------------------------------------------- warning extra:00000:9:RETURN:unreachable code (1 row) -- missing return create or replace function fx_flow() returns int as $$ declare a int; begin if a > 10 then return a; end if; end; $$ language plpgsql; select * from plpgsql_check_function('fx_flow()'); plpgsql_check_function -------------------------------------------------------------------- warning extra:2F005:control reached end of function without RETURN (1 row) drop function fx_flow(); create or replace function fx_flow(in p_param1 integer) returns text as $$ declare z1 text; begin if p_param1 is not null then z1 := '1111'; return z1; else z1 := '222222'; end if; return z1; end; $$ language plpgsql stable; select * from plpgsql_check_function_tb('fx_flow(integer)'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+---------+--------+------+-------+----------+-------+--------- (0 rows) create or replace function fx_flow(in p_param1 integer) returns text as $$ declare z1 text; begin if p_param1 is not null then z1 := '1111'; return z1; else z1 := '222222'; raise exception 'stop'; end if; return z1; end; $$ language plpgsql stable; select * from plpgsql_check_function_tb('fx_flow(integer)'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+------------------+--------+------+---------------+----------+-------+--------- fx_flow | 12 | RETURN | 00000 | unreachable code | | | warning extra | | | (1 row) drop function fx_flow(); ERROR: function fx_flow() does not exist drop function fx(int); ERROR: function fx(integer) does not exist create or replace function fx(x int) returns table(y int) as $$ begin return query select x union select x; end $$ language plpgsql; select * from fx(10); y ---- 10 (1 row) select * from plpgsql_check_function_tb('fx(int)'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+---------+--------+------+-------+----------+-------+--------- (0 rows) drop function fx(int); create or replace function fx(x int) returns table(y int, z int) as $$ begin return query select x,x+1 union select x, x+1; end $$ language plpgsql; select * from fx(10); y | z ----+---- 10 | 11 (1 row) select * from plpgsql_check_function_tb('fx(int)'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+---------+--------+------+-------+----------+-------+--------- (0 rows) drop function fx(int); create table xx(a int); create or replace function fx(x int) returns int as $$ declare _a int; begin begin select a from xx into strict _a where a = x; return _a; exception when others then null; end; return -1; end; $$ language plpgsql; select * from plpgsql_check_function_tb('fx(int)'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+---------+--------+------+-------+----------+-------+--------- (0 rows) drop table xx; create or replace function fx(x int) returns int as $$ begin begin if (x > 0) then raise exception 'xxx' using errcode = 'XX888'; else raise exception 'yyy' using errcode = 'YY888'; end if; return -1; -- dead code; end; return -1; end; $$ language plpgsql; select * from plpgsql_check_function_tb('fx(int)'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+------------------+--------+------+---------------+----------+-------+--------- fx | 9 | RETURN | 00000 | unreachable code | | | warning extra | | | fx | 11 | RETURN | 00000 | unreachable code | | | warning extra | | | (2 rows) create or replace function fx(x int) returns int as $$ begin begin if (x > 0) then raise exception 'xxx' using errcode = 'XX888'; else raise exception 'yyy' using errcode = 'YY888'; end if; exception when sqlstate 'XX888' then null; when sqlstate 'YY888' then null; end; end; -- missing return; $$ language plpgsql; select * from plpgsql_check_function_tb('fx(int)'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+------------------------------------------------+--------+------+-------+----------+-------+--------- fx | | | 2F005 | control reached end of function without RETURN | | | error | | | (1 row) create or replace function fx(x int) returns int as $$ begin begin if (x > 0) then raise exception 'xxx' using errcode = 'XX888'; else raise exception 'yyy' using errcode = 'YY888'; end if; exception when others then return 10; end; end; -- ok now $$ language plpgsql; select * from plpgsql_check_function_tb('fx(int)'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+---------+--------+------+-------+----------+-------+--------- (0 rows) --false alarm reported by Filip Zach create type testtype as (id integer); create or replace function fx() returns testtype as $$ begin return row(1); end; $$ language plpgsql; select * from fx(); id ---- 1 (1 row) select fx(); fx ----- (1) (1 row) select * from plpgsql_check_function('fx()'); plpgsql_check_function ------------------------ (0 rows) drop function fx(); create function out1(OUT f1 int, OUT f2 int) returns setof record as $$ begin for f1, f2 in execute $q$ select 1, 2 $q$ loop return next; end loop; end $$ language plpgsql; select * from plpgsql_check_function('out1()'); plpgsql_check_function ------------------------ (0 rows) drop function out1(); create function out1(OUT f1 int, OUT f2 int) returns setof record as $$ begin for f1, f2 in select 1, 2 loop return next; end loop; end $$ language plpgsql; select * from plpgsql_check_function('out1()'); plpgsql_check_function ------------------------ (0 rows) drop function out1(); -- never read variable detection create function a() returns int as $$ declare foo int; begin foo := 2; return 1; end; $$ language plpgsql; select * from plpgsql_check_function('a()'); plpgsql_check_function --------------------------------------------------------- warning extra:00000:2:DECLARE:never read variable "foo" (1 row) drop function a(); -- issue #29 false unused variable create or replace function f1(in p_cursor refcursor) returns void as $body$ declare z_offset integer; begin z_offset := 10; move absolute z_offset from p_cursor; end; $body$ language 'plpgsql' stable; select * from plpgsql_check_function_tb('f1(refcursor)'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+---------+--------+------+-------+----------+-------+--------- (0 rows) drop function f1(refcursor); -- issue #30 segfault due NULL refname create or replace function test(a varchar) returns void as $$ declare x cursor (_a varchar) for select _a; begin open x(a); end; $$ language plpgsql; select * from plpgsql_check_function_tb('test(varchar)'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+-------------------------+--------+------+---------------+----------+-------+--------- test | 2 | DECLARE | 00000 | never read variable "x" | | | warning extra | | | (1 row) drop function test(varchar); create or replace function test() returns void as $$ declare x numeric; begin x := NULL; end; $$ language plpgsql; select * from plpgsql_check_function('test()'); plpgsql_check_function ------------------------------------------------------- warning extra:00000:2:DECLARE:never read variable "x" (1 row) drop function test(); create table testtable(a int); create or replace function test() returns int as $$ declare r testtable; begin select * into r from testtable; return r.a; end; $$ language plpgsql; select * from plpgsql_check_function('test()'); plpgsql_check_function ------------------------ (0 rows) set check_function_bodies to on; drop table testtable; create table testtable(a int, b int); create or replace function test() returns int as $$ declare r testtable; begin select * into r from testtable; return r.a; end; $$ language plpgsql; alter table testtable drop column b; -- expected false alarm on PostgreSQL 10 and older -- there is not possibility to enforce recompilation -- before checking. select * from plpgsql_check_function('test()'); plpgsql_check_function ------------------------ (0 rows) drop function test(); -- issue #32 create table bigtable(id bigint, v varchar); create or replace function test() returns void as $$ declare r record; _id numeric; begin select * into r from bigtable where id = _id; for r in select * from bigtable where _id = id loop end loop; if (exists(select * from bigtable where id = _id)) then end if; end; $$ language plpgsql; select test(); test ------ (1 row) -- should to show performance warnings select * from plpgsql_check_function('test()', performance_warnings := true); plpgsql_check_function ------------------------------------------------------------------------------------------------------------------------------- performance:42804:6:SQL statement:implicit cast of attribute caused by different PLpgSQL variable type in WHERE clause Query: select * from bigtable where id = _id -- ^ Detail: An index of some attribute cannot be used, when variable, used in predicate, has not right type like a attribute Hint: Check a variable type - int versus numeric performance:42804:7:FOR over SELECT rows:implicit cast of attribute caused by different PLpgSQL variable type in WHERE clause Query: select * from bigtable where _id = id -- ^ Detail: An index of some attribute cannot be used, when variable, used in predicate, has not right type like a attribute Hint: Check a variable type - int versus numeric performance:42804:10:IF:implicit cast of attribute caused by different PLpgSQL variable type in WHERE clause Query: (exists(select * from bigtable where id = _id)) -- ^ Detail: An index of some attribute cannot be used, when variable, used in predicate, has not right type like a attribute Hint: Check a variable type - int versus numeric warning extra:00000:3:DECLARE:never read variable "r" (16 rows) create or replace function test() returns void as $$ declare r record; _id bigint; begin select * into r from bigtable where id = _id; for r in select * from bigtable where _id = id loop end loop; if (exists(select * from bigtable where id = _id)) then end if; end; $$ language plpgsql; -- there are not any performance issue now select * from plpgsql_check_function('test()', performance_warnings := true); plpgsql_check_function ------------------------------------------------------- warning extra:00000:3:DECLARE:never read variable "r" (1 row) -- nextval, currval and setval test create table test_table(); create or replace function testseq() returns void as $$ begin perform nextval('test_table'); perform currval('test_table'); perform setval('test_table', 10); perform setval('test_table', 10, true); end; $$ language plpgsql; -- should to fail select testseq(); ERROR: "test_table" is not a sequence CONTEXT: SQL statement "SELECT nextval('test_table')" PL/pgSQL function testseq() line 3 at PERFORM select * from plpgsql_check_function('testseq()', fatal_errors := false); plpgsql_check_function ------------------------------------------------------ error:42809:3:PERFORM:"test_table" is not a sequence Query: SELECT nextval('test_table') -- ^ error:42809:4:PERFORM:"test_table" is not a sequence Query: SELECT currval('test_table') -- ^ error:42809:5:PERFORM:"test_table" is not a sequence Query: SELECT setval('test_table', 10) -- ^ error:42809:6:PERFORM:"test_table" is not a sequence Query: SELECT setval('test_table', 10, true) -- ^ (12 rows) drop function testseq(); drop table test_table; -- tests designed for PostgreSQL 9.2 set check_function_bodies to off; create table t1(a int, b int); create function f1() returns void as $$ begin if false then update t1 set c = 30; end if; if false then raise notice '%', r.c; end if; end; $$ language plpgsql; select f1(); f1 ---- (1 row) select * from plpgsql_check_function_tb('f1()', fatal_errors := true); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+---------------+----------+--------------------------------------------+--------+------+-------+----------+----------------------+--------- f1 | 4 | SQL statement | 42703 | column "c" of relation "t1" does not exist | | | error | 15 | update t1 set c = 30 | (1 row) select * from plpgsql_check_function_tb('f1()', fatal_errors := false); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+---------------+----------+--------------------------------------------+--------+------+-------+----------+----------------------+--------- f1 | 4 | SQL statement | 42703 | column "c" of relation "t1" does not exist | | | error | 15 | update t1 set c = 30 | f1 | 7 | RAISE | 42P01 | missing FROM-clause entry for table "r" | | | error | 1 | r.c | (2 rows) select * from plpgsql_check_function_tb('f1()'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+---------------+----------+--------------------------------------------+--------+------+-------+----------+----------------------+--------- f1 | 4 | SQL statement | 42703 | column "c" of relation "t1" does not exist | | | error | 15 | update t1 set c = 30 | (1 row) select f1(); f1 ---- (1 row) drop function f1(); create or replace function f1() returns void as $$ begin if false then raise notice '%', 1, 2; end if; end; $$ language plpgsql; select f1(); ERROR: too many parameters specified for RAISE CONTEXT: compilation of PL/pgSQL function "f1" near line 4 select * from plpgsql_check_function_tb('f1()'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+-----------------------------------------+--------+------+-------+----------+-------+--------------------------------------------------- f1 | | | 42601 | too many parameters specified for RAISE | | | error | | | compilation of PL/pgSQL function "f1" near line 4 (1 row) select f1(); ERROR: too many parameters specified for RAISE CONTEXT: compilation of PL/pgSQL function "f1" near line 4 drop function f1(); create or replace function f1() returns void as $$ begin if false then raise notice '% %'; end if; end; $$ language plpgsql; select f1(); ERROR: too few parameters specified for RAISE CONTEXT: compilation of PL/pgSQL function "f1" near line 4 select * from plpgsql_check_function_tb('f1()'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+----------------------------------------+--------+------+-------+----------+-------+--------------------------------------------------- f1 | | | 42601 | too few parameters specified for RAISE | | | error | | | compilation of PL/pgSQL function "f1" near line 4 (1 row) select f1(); ERROR: too few parameters specified for RAISE CONTEXT: compilation of PL/pgSQL function "f1" near line 4 drop function f1(); create or replace function ml_trg() returns trigger as $$ #option dump declare begin if TG_OP = 'INSERT' then if NEW.status_from IS NULL then begin -- performance issue only select status into NEW.status_from from pa where pa_id = NEW.pa_id; -- nonexist target value select status into NEW.status_from_xxx from pa where pa_id = NEW.pa_id; exception when DATA_EXCEPTION then new.status_from := 'DE'; end; end if; end if; if TG_OP = 'DELETE' then return OLD; else return NEW; end if; exception when OTHERS then NULL; if TG_OP = 'DELETE' then return OLD; else return NEW; end if; end; $$ language plpgsql; select * from plpgsql_check_function('ml_trg()', 'ml', performance_warnings := true); plpgsql_check_function -------------------------------------------------------------------------- error:42703:13:SQL statement:record "new" has no field "status_from_xxx" (1 row) create or replace function fx2() returns void as $$ declare _pa pa; begin select pa.id into _pa.id from pa limit 1; select pa.pa_id into _pa.pa_id from pa limit 1; end; $$ language plpgsql; select * from plpgsql_check_function('fx2()', performance_warnings := true); plpgsql_check_function --------------------------------------------------------- warning extra:00000:2:DECLARE:never read variable "_pa" (1 row) drop function fx2(); create or replace function fx2() returns void as $$ declare _pa pa; begin _pa.id := (select pa.id from pa limit 1); _pa.pa_id := (select pa.pa_id from pa limit 1); end; $$ language plpgsql; select * from plpgsql_check_function('fx2()', performance_warnings := true); plpgsql_check_function --------------------------------------------------------- warning extra:00000:2:DECLARE:never read variable "_pa" (1 row) drop function fx2(); create or replace function test_lab() returns void as $$ begin <> for a in 1..3 loop <> BEGIN <> for b in 8..9 loop if a=2 then continue sub; end if; raise notice '% %', a, b; end loop inner; END sub; end loop outer; end; $$ language plpgsql; select test_lab(); ERROR: block label "sub" cannot be used in CONTINUE LINE 10: continue sub; ^ QUERY: begin <> for a in 1..3 loop <> BEGIN <> for b in 8..9 loop if a=2 then continue sub; end if; raise notice '% %', a, b; end loop inner; END sub; end loop outer; end; CONTEXT: compilation of PL/pgSQL function "test_lab" near line 10 select * from plpgsql_check_function('test_lab()', performance_warnings := true); plpgsql_check_function ------------------------------------------------------------------- error:42601:block label "sub" cannot be used in CONTINUE Query: begin <> for a in 1..3 loop <> BEGIN <> for b in 8..9 loop if a=2 then continue sub; -- ^ end if; raise notice '% %', a, b; end loop inner; END sub; end loop outer; end; Context: compilation of PL/pgSQL function "test_lab" near line 10 (20 rows) create or replace function test_lab() returns void as $$ begin continue; end; $$ language plpgsql; select test_lab(); ERROR: CONTINUE cannot be used outside a loop LINE 3: continue; ^ QUERY: begin continue; end; CONTEXT: compilation of PL/pgSQL function "test_lab" near line 3 select * from plpgsql_check_function('test_lab()', performance_warnings := true); plpgsql_check_function ------------------------------------------------------------------ error:42601:CONTINUE cannot be used outside a loop Query: begin continue; -- ^ end; Context: compilation of PL/pgSQL function "test_lab" near line 3 (8 rows) create type _exception_type as ( state text, message text, detail text); create or replace function f1() returns void as $$ declare _exception record; begin _exception := NULL::_exception_type; exception when others then get stacked diagnostics _exception.state = RETURNED_SQLSTATE, _exception.message = MESSAGE_TEXT, _exception.detail = PG_EXCEPTION_DETAIL, _exception.hint = PG_EXCEPTION_HINT; end; $$ language plpgsql; select f1(); f1 ---- (1 row) select * from plpgsql_check_function('f1()'); plpgsql_check_function ------------------------------------------------------------------------------- error:42703:7:GET STACKED DIAGNOSTICS:record "_exception" has no field "hint" (1 row) drop function f1(); drop type _exception_type; drop table t1; create function myfunc1(a int, b float) returns integer as $$ begin end $$ language plpgsql; create function myfunc2(a int, b float) returns integer as $$ begin end $$ language plpgsql; create function myfunc3(a int, b float) returns integer as $$ begin end $$ language plpgsql; create function myfunc4(a int, b float) returns integer as $$ begin end $$ language plpgsql; create function opfunc1(a int, b float) returns integer as $$ begin end $$ language plpgsql; create operator *** (procedure = opfunc1, leftarg = int, rightarg = float); create table mytable(a int); create table myview as select * from mytable; create function testfunc(a int, b float) returns void as $$ declare x integer; begin raise notice '%', myfunc1(a, b); x := myfunc2(a, b) operator(public.***) 1; perform myfunc3(m.a, b) from myview m; insert into mytable select myfunc4(a, b); end; $$ language plpgsql; select * from plpgsql_check_function('testfunc(int,float)'); plpgsql_check_function ------------------------------------------------------- warning extra:00000:2:DECLARE:never read variable "x" (1 row) select type, schema, name, params from plpgsql_show_dependency_tb('testfunc(int,float)'); type | schema | name | params ----------+--------+---------+---------------------------- FUNCTION | public | myfunc1 | (integer,double precision) FUNCTION | public | myfunc2 | (integer,double precision) FUNCTION | public | myfunc3 | (integer,double precision) FUNCTION | public | myfunc4 | (integer,double precision) OPERATOR | public | *** | (integer,double precision) RELATION | public | mytable | RELATION | public | myview | (7 rows) drop function testfunc(int, float); drop function myfunc1(int, float); drop function myfunc2(int, float); drop function myfunc3(int, float); drop function myfunc4(int, float); drop table mytable; drop view myview; ERROR: "myview" is not a view HINT: Use DROP TABLE to remove a table. -- issue #221 create type foo_type as (a int); create table foo(a int); create function testfuncdep() returns void as $$ declare x foo_type; y foo; begin end; $$ language plpgsql; select type, schema, name from plpgsql_show_dependency_tb('testfuncdep'); type | schema | name ----------+--------+---------- RELATION | public | foo TYPE | public | foo_type (2 rows) drop function testfuncdep(); drop type foo_type; drop table foo; -- issue #221 - dependency report on trigger function should not crash -- (NEW and OLD record variables have no datatype) create table foo(a int, b text); create function foo_trg_func() returns trigger as $$ begin new.b := 'x'; return new; end; $$ language plpgsql; create trigger foo_trg before insert on foo for each row execute function foo_trg_func(); select type, schema, name from plpgsql_show_dependency_tb('foo_trg_func', 'foo'); type | schema | name ------+--------+------ (0 rows) drop trigger foo_trg on foo; drop function foo_trg_func(); drop table foo; -- issue #34 create or replace function testcase() returns bool as $$ declare x int; begin set local search_path to public, test; case x when 1 then return true; else return false; end case; end; $$ language plpgsql; -- should not to raise warning select * from plpgsql_check_function('testcase()'); plpgsql_check_function ------------------------ (0 rows) drop function testcase(); -- Adam's Bartoszewicz example create or replace function public.test12() returns refcursor language plpgsql as $body$ declare rc refcursor; begin open rc scroll for select pc.* from pg_cast pc; return rc; end; $body$; -- should not returns false alarm select * from plpgsql_check_function('test12()'); plpgsql_check_function ------------------------ (0 rows) drop function public.test12(); -- should to show performance warning on bad flag create or replace function flag_test1(int) returns int as $$ begin return $1 + 10; end; $$ language plpgsql stable; create table fufu(a int); create or replace function flag_test2(int) returns int as $$ begin return (select * from fufu limit 1); end; $$ language plpgsql volatile; select * from plpgsql_check_function('flag_test1(int)', performance_warnings := true); plpgsql_check_function ----------------------------------------------------------------------------------------- performance:00000:routine is marked as STABLE, should be IMMUTABLE Hint: When you fix this issue, please, recheck other functions that uses this function. (2 rows) select * from plpgsql_check_function('flag_test2(int)', performance_warnings := true); plpgsql_check_function ----------------------------------------------------------------------------------------- warning extra:00000:unused parameter "$1" performance:00000:routine is marked as VOLATILE, should be STABLE Hint: When you fix this issue, please, recheck other functions that uses this function. (3 rows) drop table fufu; drop function flag_test1(int); drop function flag_test2(int); create or replace function rrecord01() returns setof record as $$ begin return query select 1,2; end; $$ language plpgsql; create or replace function rrecord02() returns record as $$ begin return row(10,20,30); end; $$ language plpgsql; create type record03 as (a int, b int); create or replace function rrecord03() returns record03 as $$ declare r record; begin r := row(1); return r; end; $$ language plpgsql; -- should not to raise false alarms select * from plpgsql_check_function('rrecord01'); plpgsql_check_function ------------------------ (0 rows) select * from plpgsql_check_function('rrecord02'); plpgsql_check_function ------------------------ (0 rows) -- should detect different return but still detect return select * from plpgsql_check_function('rrecord03', fatal_errors => false); plpgsql_check_function ---------------------------------------------------------------------------------- error:42804:5:RETURN:returned record type does not match expected record type Detail: Number of returned columns (1) does not match expected column count (2). (2 rows) drop function rrecord01(); drop function rrecord02(); drop function rrecord03(); drop type record03; create or replace function bugfunc01() returns void as $$ declare cvar cursor(a int, b int) for select a + b from generate_series(1,b); begin for t in cvar(1,3) loop raise notice '%', t; end loop; end; $$ language plpgsql; select bugfunc01(); NOTICE: (4) NOTICE: (4) NOTICE: (4) bugfunc01 ----------- (1 row) select * from plpgsql_check_function('bugfunc01'); plpgsql_check_function ------------------------ (0 rows) create or replace function bugfunc02() returns void as $$ declare cvar cursor(a int, b int) for select a + b from generate_series(1,b); begin open cvar(10,20); close cvar; end; $$ language plpgsql; select bugfunc02(); bugfunc02 ----------- (1 row) select * from plpgsql_check_function('bugfunc02'); plpgsql_check_function ------------------------ (0 rows) create or replace function bugfunc03() returns void as $$ declare cvar cursor(a int, b int) for select a + b from not_exists_table; begin open cvar(10,20); close cvar; end; $$ language plpgsql; select bugfunc03(); ERROR: relation "not_exists_table" does not exist LINE 1: select a + b from not_exists_table ^ QUERY: select a + b from not_exists_table CONTEXT: PL/pgSQL function bugfunc03() line 5 at OPEN select * from plpgsql_check_function('bugfunc03'); plpgsql_check_function --------------------------------------------------------------- error:42P01:5:OPEN:relation "not_exists_table" does not exist Query: select a + b from not_exists_table -- ^ (3 rows) create or replace function f1(out cr refcursor) as $$ begin end; $$ language plpgsql; -- should to raise warning select * from plpgsql_check_function('f1()'); plpgsql_check_function -------------------------------------------------- warning extra:00000:unmodified OUT variable "cr" (1 row) create or replace function f1(out cr refcursor) as $$ begin open cr for select 1; end; $$ language plpgsql; -- should not to raise warning, see issue #43 select * from plpgsql_check_function('f1()'); plpgsql_check_function ------------------------ (0 rows) drop function f1(); create table testt(a int); create or replace function testt_trg_func() returns trigger as $$ begin return new; end; $$ language plpgsql; create trigger testt_trg before insert or update on testt for each row execute procedure testt_trg_func(); create or replace function maintaince_function() returns void as $$ begin alter table testt disable trigger testt_trg; alter table testt enable trigger testt_trg; end; $$ language plpgsql; -- should not to crash select * from plpgsql_check_function_tb('maintaince_function()', 0, true, true, true); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+---------+--------+------+-------+----------+-------+--------- (0 rows) drop function maintaince_function(); drop trigger testt_trg on testt; drop function testt_trg_func(); drop table testt; create or replace function test_crash() returns void as $$ declare ec int default buggyfunc(10); begin select * into ec from buggytab; end; $$ language plpgsql; -- should not to crash select * from plpgsql_check_function('test_crash', fatal_errors := false); plpgsql_check_function --------------------------------------------------------------------------------------------------------- error:42883:4:statement block:function buggyfunc(integer) does not exist Query: buggyfunc(10) -- ^ Hint: No function matches the given name and argument types. You might need to add explicit type casts. Context: during statement block local variable "ec" initialization on line 3 error:42P01:5:SQL statement:relation "buggytab" does not exist Query: select * from buggytab -- ^ warning extra:00000:3:DECLARE:never read variable "ec" (9 rows) select * from plpgsql_check_function('test_crash', fatal_errors := true); plpgsql_check_function --------------------------------------------------------------------------------------------------------- error:42883:4:statement block:function buggyfunc(integer) does not exist Query: buggyfunc(10) -- ^ Hint: No function matches the given name and argument types. You might need to add explicit type casts. Context: during statement block local variable "ec" initialization on line 3 (5 rows) drop function test_crash(); -- fix false alarm reported by Piotr Stepniewski create or replace function public.fx() returns void language plpgsql as $function$ begin raise exception 'xxx'; end; $function$; -- show raise nothing select * from plpgsql_check_function('fx()'); plpgsql_check_function ------------------------ (0 rows) create table errtab( message text, code character(5) ); create or replace function public.fx() returns void language plpgsql as $function$ declare var errtab%rowtype; begin raise exception using message = var.message, errcode = var.code; end; $function$; -- should not to crash select * from plpgsql_check_function('fx()'); plpgsql_check_function ------------------------ (0 rows) create or replace function public.fx() returns void language plpgsql as $function$ declare var errtab%rowtype; begin raise exception using message = var.message, errcode = var.code, hint = var.hint; end; $function$; -- should not to crash select * from plpgsql_check_function('fx()'); plpgsql_check_function ------------------------------------------------------ error:42703:5:RAISE:record "var" has no field "hint" Context: SQL expression "var.hint" (2 rows) drop function fx(); create or replace function foo_format(a text, b text) returns void as $$ declare s text; begin s := format('%s'); -- should to raise error s := format('%s %10s', a, b); -- should be ok s := format('%s %s', a, b, a); -- should to raise warning s := format('%s %d', a, b); -- should to raise error raise notice '%', s; end; $$ language plpgsql; select * from plpgsql_check_function('foo_format', fatal_errors := false); plpgsql_check_function ------------------------------------------------------------------- error:22023:4:assignment:too few arguments for format() Query: s := format('%s') -- ^ Context: at assignment to variable "s" declared on line 2 warning:00000:6:assignment:unused parameters of function "format" Query: s := format('%s %s', a, b, a) -- ^ Context: at assignment to variable "s" declared on line 2 error:22023:7:assignment:unrecognized format() type specifier "d" Query: s := format('%s %d', a, b) -- ^ Context: at assignment to variable "s" declared on line 2 (12 rows) drop function foo_format(text, text); create or replace function dyn_sql_1() returns void as $$ declare v varchar; n int; begin execute 'select ' || n; -- ok execute 'select ' || quote_literal(v); -- ok execute 'select ' || v; -- vulnerable execute format('select * from %I', v); -- ok execute format('select * from %s', v); -- vulnerable execute 'select $1' using v; -- ok execute 'select 1'; -- ok execute 'select 1' using v; -- warning execute 'select $1'; -- error end; $$ language plpgsql; select * from plpgsql_check_function('dyn_sql_1', security_warnings := true, fatal_errors := false); plpgsql_check_function ------------------------------------------------------------------------------------------ security:00000:8:EXECUTE:text type variable is not sanitized Query: 'select ' || v -- ^ Detail: The EXECUTE expression is SQL injection vulnerable. Hint: Use quote_ident, quote_literal or format function to secure variable. security:00000:10:EXECUTE:text type variable is not sanitized Query: format('select * from %s', v) -- ^ Detail: The EXECUTE expression is SQL injection vulnerable. Hint: Use quote_ident, quote_literal or format function to secure variable. warning:00000:13:EXECUTE:values passed to EXECUTE statement by USING clause was not used error:42P02:14:EXECUTE:there is no parameter $1 Query: select $1 -- ^ (14 rows) drop function dyn_sql_1(); create type tp as (a int, b int); create or replace function dyn_sql_2() returns void as $$ declare r tp; result int; begin select 10 a, 20 b into r; raise notice '%', r.a; execute 'select $1.a + $1.b' into result using r; execute 'select $1.c' into result using r; -- error raise notice '%', result; end; $$ language plpgsql; select * from plpgsql_check_function('dyn_sql_2', security_warnings := true); plpgsql_check_function ------------------------------------------------------------ error:42703:9:EXECUTE:column "c" not found in data type tp Query: select $1.c -- ^ (3 rows) drop function dyn_sql_2(); drop type tp; /* * Should not to work * * note: plpgsql doesn't support passing some necessary details for record * type. The parser setup for dynamic SQL column doesn't use ref hooks, and * then it cannot to pass TupleDesc info to query anyway. */ create or replace function dyn_sql_2() returns void as $$ declare r record; result int; begin select 10 a, 20 b into r; raise notice '%', r.a; execute 'select $1.a + $1.b' into result using r; raise notice '%', result; end; $$ language plpgsql; select dyn_sql_2(); --should to fail NOTICE: 10 ERROR: could not identify column "a" in record data type LINE 1: select $1.a + $1.b ^ QUERY: select $1.a + $1.b CONTEXT: PL/pgSQL function dyn_sql_2() line 8 at EXECUTE select * from plpgsql_check_function('dyn_sql_2', security_warnings := true); plpgsql_check_function ------------------------------------------------------------------------- error:42703:8:EXECUTE:could not identify column "a" in record data type Query: select $1.a + $1.b -- ^ (3 rows) drop function dyn_sql_2(); create or replace function dyn_sql_3() returns void as $$ declare r int; begin execute 'select $1' into r using 1; raise notice '%', r; end $$ language plpgsql; select dyn_sql_3(); NOTICE: 1 dyn_sql_3 ----------- (1 row) -- should be ok select * from plpgsql_check_function('dyn_sql_3'); plpgsql_check_function ------------------------ (0 rows) create or replace function dyn_sql_3() returns void as $$ declare r record; begin execute 'select $1 as a, $2 as b' into r using 1, 2; raise notice '% %', r.a, r.b; end $$ language plpgsql; select dyn_sql_3(); NOTICE: 1 2 dyn_sql_3 ----------- (1 row) -- should be ok select * from plpgsql_check_function('dyn_sql_3'); plpgsql_check_function ------------------------ (0 rows) create or replace function dyn_sql_3() returns void as $$ declare r record; begin execute 'create table foo(a int)' into r using 1, 2; raise notice '% %', r.a, r.b; end $$ language plpgsql; -- raise a error select * from plpgsql_check_function('dyn_sql_3'); plpgsql_check_function ----------------------------------------------------------------------------------------- warning:00000:4:EXECUTE:values passed to EXECUTE statement by USING clause was not used error:XX000:4:EXECUTE:expression does not return data (2 rows) create or replace function dyn_sql_3() returns void as $$ declare r1 int; r2 int; begin execute 'select 1' into r1, r2 using 1, 2; raise notice '% %', r1, r2; end $$ language plpgsql; -- raise a error select * from plpgsql_check_function('dyn_sql_3'); plpgsql_check_function ----------------------------------------------------------------------------------------- warning:00000:4:EXECUTE:values passed to EXECUTE statement by USING clause was not used warning:00000:4:EXECUTE:too few attributes for target variables Query: select 1 -- ^ Detail: There are more target variables than output columns in query. Hint: Check target variables in SELECT INTO statement. Context: at EXECUTE to r1, r2 variables (7 rows) drop function dyn_sql_3(); create or replace function dyn_sql_3() returns void as $$ declare r record; begin for r in execute 'select 1 as a, 2 as b' loop raise notice '%', r.a; end loop; end $$ language plpgsql; -- should be ok select * from plpgsql_check_function('dyn_sql_3'); plpgsql_check_function ------------------------ (0 rows) drop function dyn_sql_3(); create or replace function dyn_sql_3() returns void as $$ declare r record; begin for r in execute 'select 1 as a, 2 as b' loop raise notice '%', r.c; end loop; end $$ language plpgsql; -- should be error select * from plpgsql_check_function('dyn_sql_3'); plpgsql_check_function ------------------------------------------------- error:42703:6:RAISE:record "r" has no field "c" Context: SQL expression "r.c" (2 rows) drop function dyn_sql_3(); create or replace function dyn_sql_3() returns void as $$ declare r record; v text = 'select 10 a, 20 b't; begin for r in execute v loop raise notice '%', r.a; end loop; end $$ language plpgsql; -- should be ok select * from plpgsql_check_function('dyn_sql_3'); plpgsql_check_function ------------------------ (0 rows) drop function dyn_sql_3(); create or replace function dyn_sql_4() returns table(ax int, bx int) as $$ begin return query execute 'select 10, 20'; return; end; $$ language plpgsql; -- should be ok select * from plpgsql_check_function('dyn_sql_4()'); plpgsql_check_function ------------------------ (0 rows) create or replace function dyn_sql_4() returns table(ax int, bx int) as $$ begin return query execute 'select 10, 20, 30'; return; end; $$ language plpgsql; select * from dyn_sql_4(); ERROR: structure of query does not match function result type DETAIL: Number of returned columns (3) does not match expected column count (2). CONTEXT: SQL statement "select 10, 20, 30" PL/pgSQL function dyn_sql_4() line 3 at RETURN QUERY -- should be error select * from plpgsql_check_function('dyn_sql_4()'); plpgsql_check_function ----------------------------------------------------------------------------------- error:42804:3:RETURN QUERY:structure of query does not match function result type Detail: Number of returned columns (3) does not match expected column count (2). (2 rows) drop function dyn_sql_4(); create or replace function test_bug(text) returns regproc as $$ begin return $1::regproc; exception when undefined_function or invalid_name then raise; end; $$ language plpgsql; -- should not raise a exception select * from plpgsql_check_function('test_bug'); plpgsql_check_function ------------------------ (0 rows) create or replace function test_bug(text) returns regproc as $$ begin return $1::regproc; exception when undefined_function or invalid_name then raise notice '%', $1; -- bug end; $$ language plpgsql; select test_bug('kuku'); -- should to fail NOTICE: kuku ERROR: control reached end of function without RETURN CONTEXT: PL/pgSQL function test_bug(text) select * from plpgsql_check_function('test_bug'); plpgsql_check_function -------------------------------------------------------------------- warning extra:2F005:control reached end of function without RETURN (1 row) drop function test_bug(text); create or replace function test_bug(text) returns regproc as $$ begin return $1::regproc; exception when undefined_function or invalid_name then raise notice '%', $1; return NULL; end; $$ language plpgsql; select test_bug('kuku'); -- should be ok NOTICE: kuku test_bug ---------- (1 row) select * from plpgsql_check_function('test_bug'); plpgsql_check_function ------------------------ (0 rows) drop function test_bug(text); create or replace function foo(a text, b text) returns void as $$ begin -- unsecure execute 'select ' || a; a := quote_literal(a); -- is safe now execute 'select ' || a; a := a || b; -- it is unsecure again execute 'select ' || a; end; $$ language plpgsql; \sf+ foo(text, text) CREATE OR REPLACE FUNCTION public.foo(a text, b text) RETURNS void LANGUAGE plpgsql 1 AS $function$ 2 begin 3 -- unsecure 4 execute 'select ' || a; 5 a := quote_literal(a); -- is safe now 6 execute 'select ' || a; 7 a := a || b; -- it is unsecure again 8 execute 'select ' || a; 9 end; 10 $function$ -- should to raise two warnings select * from plpgsql_check_function('foo', security_warnings := true); plpgsql_check_function ----------------------------------------------------------------------------- security:00000:4:EXECUTE:text type variable is not sanitized Query: 'select ' || a -- ^ Detail: The EXECUTE expression is SQL injection vulnerable. Hint: Use quote_ident, quote_literal or format function to secure variable. security:00000:8:EXECUTE:text type variable is not sanitized Query: 'select ' || a -- ^ Detail: The EXECUTE expression is SQL injection vulnerable. Hint: Use quote_ident, quote_literal or format function to secure variable. (10 rows) drop function foo(text, text); create table testr(a int); create rule testr_rule as on insert to testr do nothing; create or replace function fx_testr() returns void as $$ begin insert into testr values(20); end; $$ language plpgsql; -- allow some rules on tables select fx_testr(); fx_testr ---------- (1 row) select * from plpgsql_check_function_tb('fx_testr'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+---------+--------+------+-------+----------+-------+--------- (0 rows) drop function fx_testr(); drop table testr; create or replace function f() returns void as $$ declare r1 record; r2 record; begin select 10 as a, 20 as b into r1; r2 := json_populate_record(r1, '{}'); raise notice '%', r2.a; end; $$ language plpgsql; select * from plpgsql_check_function('f'); plpgsql_check_function ------------------------ (0 rows) -- fix issue #63 create or replace function distinct_array(arr anyarray) returns anyarray as $$ begin return array(select distinct e from unnest(arr) as e); end; $$ language plpgsql immutable; select plpgsql_check_function('distinct_array(anyarray)'); plpgsql_check_function ------------------------ (0 rows) drop function distinct_array(anyarray); -- tracer test set plpgsql_check.enable_tracer to on; set plpgsql_check.tracer to on; set plpgsql_check.tracer_test_mode = true; -- should not crash do $$ begin raise exception 'raising an exception'; end; $$; NOTICE: #0 ->> start of block inline_code_block (oid=0, tnl=1) NOTICE: #0 <<- end of block (elapsed time=0.010 ms) aborted ERROR: raising an exception CONTEXT: PL/pgSQL function inline_code_block line 3 at RAISE do $$ begin do $_$ begin raise exception 'raising an exception'; end $_$; end; $$; NOTICE: #0 ->> start of block inline_code_block (oid=0, tnl=1) NOTICE: #2 ->> start of block inline_code_block (oid=0, tnl=1) NOTICE: #2 context: SQL statement "do $_$ begin raise exception 'raising an exception'; end $_$" NOTICE: #2 <<- end of block (elapsed time=0.010 ms) aborted NOTICE: #0 <<- end of block (elapsed time=0.010 ms) aborted ERROR: raising an exception CONTEXT: PL/pgSQL function inline_code_block line 3 at RAISE SQL statement "do $_$ begin raise exception 'raising an exception'; end $_$" PL/pgSQL function inline_code_block line 3 at DO create function fx_trace(int) returns int as $$ begin if $1 = 0 then raise exception 'arg is zero'; else return $1; end if; end; $$ language plpgsql; do $$ declare t int; begin t := fx_trace(1); end $$; NOTICE: #0 ->> start of block inline_code_block (oid=0, tnl=1) NOTICE: #1 ->> start of function fx_trace(integer) (oid=0, tnl=1) NOTICE: #1 context: PL/pgSQL function inline_code_block line 4 at assignment NOTICE: #1 "$1" => '1' NOTICE: #1 <<- end of function fx_trace (elapsed time=0.010 ms) NOTICE: #0 <<- end of block (elapsed time=0.010 ms) do $$ declare t int; begin t := fx_trace(0); end $$; NOTICE: #0 ->> start of block inline_code_block (oid=0, tnl=1) NOTICE: #1 ->> start of function fx_trace(integer) (oid=0, tnl=1) NOTICE: #1 context: PL/pgSQL function inline_code_block line 4 at assignment NOTICE: #1 "$1" => '0' NOTICE: #1 <<- end of function fx_trace (elapsed time=0.010 ms) aborted NOTICE: #0 <<- end of block (elapsed time=0.010 ms) aborted ERROR: arg is zero CONTEXT: PL/pgSQL function fx_trace(integer) line 4 at RAISE PL/pgSQL function inline_code_block line 4 at assignment drop function fx_trace; \set VERBOSITY terse create or replace function fxo(a int, b int, c date, d numeric) returns void as $$ begin insert into tracer_tab values(a,b,c,d); end; $$ language plpgsql; create table tracer_tab(a int, b int, c date, d numeric); create or replace function tracer_tab_trg_fx() returns trigger as $$ begin return new; end; $$ language plpgsql; create trigger tracer_tab_trg before insert on tracer_tab for each row execute procedure tracer_tab_trg_fx(); select fxo(10,20,'20200815', 3.14); NOTICE: #0 ->> start of function fxo(integer,integer,date,numeric) (oid=0, tnl=1) NOTICE: #0 "a" => '10', "b" => '20', "c" => '08-15-2020', "d" => '3.14' NOTICE: #2 ->> start of function tracer_tab_trg_fx() (oid=0, tnl=1) NOTICE: #2 context: SQL statement "insert into tracer_tab values(a,b,c,d)" NOTICE: #2 triggered by before row insert trigger NOTICE: #2 "new" => '(10,20,08-15-2020,3.14)' NOTICE: #2 <<- end of function tracer_tab_trg_fx (elapsed time=0.010 ms) NOTICE: #0 <<- end of function fxo (elapsed time=0.010 ms) fxo ----- (1 row) select fxo(11,21,'20200816', 6.28); NOTICE: #0 ->> start of function fxo(integer,integer,date,numeric) (oid=0, tnl=1) NOTICE: #0 "a" => '11', "b" => '21', "c" => '08-16-2020', "d" => '6.28' NOTICE: #2 ->> start of function tracer_tab_trg_fx() (oid=0, tnl=1) NOTICE: #2 context: SQL statement "insert into tracer_tab values(a,b,c,d)" NOTICE: #2 triggered by before row insert trigger NOTICE: #2 "new" => '(11,21,08-16-2020,6.28)' NOTICE: #2 <<- end of function tracer_tab_trg_fx (elapsed time=0.010 ms) NOTICE: #0 <<- end of function fxo (elapsed time=0.010 ms) fxo ----- (1 row) set plpgsql_check.enable_tracer to off; set plpgsql_check.tracer to off; drop table tracer_tab cascade; drop function tracer_tab_trg_fx(); drop function fxo(int, int, date, numeric); create or replace function foo_trg_func() returns trigger as $$ begin -- bad function, RETURN is missing end; $$ language plpgsql; create table foo(a int); create trigger foo_trg before insert for each row execute procedure foo_trg_func(); ERROR: syntax error at or near "for" at character 38 -- should to print error select * from plpgsql_check_function('foo_trg_func', 'foo'); plpgsql_check_function ------------------------------------------------------------ error:2F005:control reached end of function without RETURN (1 row) drop table foo; drop function foo_trg_func(); -- check event trigger function create or replace function f1() returns event_trigger as $$ BEGIN RAISE NOTICE 'test_event_trigger: % %', tg_event, tg_tag; END $$ language plpgsql; select * from plpgsql_check_function_tb('f1()'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+---------+--------+------+-------+----------+-------+--------- (0 rows) -- should fail create or replace function f1() returns event_trigger as $$ BEGIN RAISE NOTICE 'test_event_trigger: % %', tg_event, tg_tagX; END $$ language plpgsql; select * from plpgsql_check_function_tb('f1()'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+---------------------------------+--------+------+-------+----------+---------+--------- f1 | 3 | RAISE | 42703 | column "tg_tagx" does not exist | | | error | 1 | tg_tagX | (1 row) drop function f1(); -- check event trigger function create or replace function f1() returns event_trigger as $$ BEGIN RAISE NOTICE 'test_event_trigger: % %', tg_event, tg_tag; END $$ language plpgsql; select * from plpgsql_check_function('f1()'); plpgsql_check_function ------------------------ (0 rows) -- should fail create or replace function f1() returns event_trigger as $$ BEGIN RAISE NOTICE 'test_event_trigger: % %', tg_event, tg_tagX; END $$ language plpgsql; select * from plpgsql_check_function('f1()'); plpgsql_check_function ----------------------------------------------------- error:42703:3:RAISE:column "tg_tagx" does not exist Query: tg_tagX -- ^ (3 rows) drop function f1(); create table t1tab(a int, b int); create or replace function f1() returns setof t1tab as $$ begin return next (10,20); return; end; $$ language plpgsql; select * from plpgsql_check_function('f1()', performance_warnings => true); plpgsql_check_function ----------------------------------------------------------------------------------------- performance:00000:routine is marked as VOLATILE, should be IMMUTABLE Hint: When you fix this issue, please, recheck other functions that uses this function. (2 rows) create or replace function f1() returns setof t1tab as $$ begin return next (10::numeric,20); return; end; $$ language plpgsql; select * from plpgsql_check_function('f1()', performance_warnings => true); plpgsql_check_function ------------------------------------------------------------------------------------ error:42804:3:RETURN NEXT:returned record type does not match expected record type Detail: Returned type numeric does not match expected type integer in column 1. (2 rows) create or replace function f1() returns setof t1tab as $$ declare a int; b int; begin return next (a,b); return; end; $$ language plpgsql; select * from plpgsql_check_function('f1()', performance_warnings => true); plpgsql_check_function ----------------------------------------------------------------------------------------- performance:00000:routine is marked as VOLATILE, should be IMMUTABLE Hint: When you fix this issue, please, recheck other functions that uses this function. (2 rows) create or replace function f1() returns setof t1tab as $$ declare a numeric; b int; begin return next (a,b::numeric); return; end; $$ language plpgsql; select * from plpgsql_check_function('f1()', performance_warnings => true); plpgsql_check_function ------------------------------------------------------------------------------------ error:42804:4:RETURN NEXT:returned record type does not match expected record type Detail: Returned type numeric does not match expected type integer in column 1. (2 rows) drop function f1(); create table t1(a int, b int); create or replace function fx() returns t2 as $$ begin return (10,20,30)::t1; end; $$ language plpgsql; select * from plpgsql_check_function('fx()', performance_warnings => true); plpgsql_check_function ---------------------------------------------------- error:42846:3:RETURN:cannot cast type record to t1 Query: (10,20,30)::t1 -- ^ Detail: Input has too many columns. (4 rows) drop function fx(); drop table t1tab; drop table t1; create or replace function fx() returns void as $$ begin assert exists(select * from foo); assert false, (select boo from boo limit 1); end; $$ language plpgsql; select * from plpgsql_check_function('fx()', fatal_errors => false); plpgsql_check_function ---------------------------------------------------- error:42P01:3:ASSERT:relation "foo" does not exist Query: exists(select * from foo) -- ^ error:42P01:4:ASSERT:relation "boo" does not exist Query: (select boo from boo limit 1) -- ^ (6 rows) create or replace function ml_trg() returns trigger as $$ #option dump declare begin if TG_OP = 'INSERT' then if NEW.status_from IS NULL then begin -- performance issue only select status into NEW.status_from from pa where pa_id = NEW.pa_id; -- nonexist target value select status into NEW.status_from_xxx from pa where pa_id = NEW.pa_id; exception when DATA_EXCEPTION then new.status_from := 'DE'; end; end if; end if; if TG_OP = 'DELETE' then return OLD; else return NEW; end if; exception when OTHERS then NULL; if TG_OP = 'DELETE' then return OLD; else return NEW; end if; end; $$ language plpgsql; select * from plpgsql_check_function('ml_trg()', 'ml', performance_warnings := true); plpgsql_check_function -------------------------------------------------------------------------- error:42703:13:SQL statement:record "new" has no field "status_from_xxx" (1 row) create or replace function fx2() returns void as $$ declare _pa pa; begin select pa.id into _pa.id from pa limit 1; select pa.pa_id into _pa.pa_id from pa limit 1; end; $$ language plpgsql; select * from plpgsql_check_function('fx2()', performance_warnings := true); plpgsql_check_function --------------------------------------------------------- warning extra:00000:2:DECLARE:never read variable "_pa" (1 row) drop function fx2(); create or replace function fx2() returns void as $$ declare _pa pa; begin _pa.id := (select pa.id from pa limit 1); _pa.pa_id := (select pa.pa_id from pa limit 1); end; $$ language plpgsql; select * from plpgsql_check_function('fx2()', performance_warnings := true); plpgsql_check_function --------------------------------------------------------- warning extra:00000:2:DECLARE:never read variable "_pa" (1 row) drop function fx2(); create type _exception_type as ( state text, message text, detail text); create or replace function f1() returns void as $$ declare _exception record; begin _exception := NULL::_exception_type; exception when others then get stacked diagnostics _exception.state = RETURNED_SQLSTATE, _exception.message = MESSAGE_TEXT, _exception.detail = PG_EXCEPTION_DETAIL, _exception.hint = PG_EXCEPTION_HINT; end; $$ language plpgsql; select f1(); f1 ---- (1 row) select * from plpgsql_check_function_tb('f1()'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-------------------------+----------+-----------------------------------------+--------+------+-------+----------+-------+--------- f1 | 7 | GET STACKED DIAGNOSTICS | 42703 | record "_exception" has no field "hint" | | | error | | | (1 row) create or replace function f1() returns void as $$ declare _exception _exception_type; begin _exception := NULL::_exception_type; exception when others then get stacked diagnostics _exception.state = RETURNED_SQLSTATE, _exception.message = MESSAGE_TEXT, _exception.detail = PG_EXCEPTION_DETAIL; end; $$ language plpgsql; select f1(); f1 ---- (1 row) select * from plpgsql_check_function_tb('f1()'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+----------------------------------+--------+------+---------------+----------+-------+--------- f1 | 3 | DECLARE | 00000 | never read variable "_exception" | | | warning extra | | | (1 row) drop function f1(); drop type _exception_type; create type _exception_type as ( state text, message text, detail text); create or replace function f1() returns void as $$ declare _exception record; begin _exception := NULL::_exception_type; exception when others then get stacked diagnostics _exception.state = RETURNED_SQLSTATE, _exception.message = MESSAGE_TEXT, _exception.detail = PG_EXCEPTION_DETAIL, _exception.hint = PG_EXCEPTION_HINT; end; $$ language plpgsql; select f1(); f1 ---- (1 row) select * from plpgsql_check_function('f1()'); plpgsql_check_function ------------------------------------------------------------------------------- error:42703:7:GET STACKED DIAGNOSTICS:record "_exception" has no field "hint" (1 row) drop function f1(); drop type _exception_type; create table footab(a int, b int, c int); create or replace function footab_trig_func() returns trigger as $$ declare x int; begin if false then -- should be ok; select count(*) from newtab into x; -- should fail; select count(*) from newtab where d = 10 into x; end if; return null; end; $$ language plpgsql; select * from plpgsql_check_function('footab_trig_func','footab', newtable := 'newtab'); plpgsql_check_function ------------------------------------------------------- error:42703:9:SQL statement:column "d" does not exist Query: select count(*) from newtab where d = 10 -- ^ (3 rows) drop table footab; drop function footab_trig_func(); create or replace function df1(anyelement) returns anyelement as $$ begin return $1; end; $$ language plpgsql; create or replace function df2(anyelement, jsonb) returns anyelement as $$ begin return $1; end; $$ language plpgsql; create or replace function t1() returns void as $$ declare r record; begin r := df1(r); end; $$ language plpgsql; select * from plpgsql_check_function('t1()'); plpgsql_check_function ------------------------ (0 rows) create or replace function t1() returns void as $$ declare r record; begin r := df2(r, '{}'); end; $$ language plpgsql; select * from plpgsql_check_function('t1()'); plpgsql_check_function ------------------------ (0 rows) create or replace function t1() returns void as $$ declare r1 record; r2 record; begin select 10 as a, 20 as b into r1; r2 := df1(r1); raise notice '%', r2.a; end; $$ language plpgsql; select * from plpgsql_check_function('t1()'); plpgsql_check_function ------------------------ (0 rows) create or replace function t1() returns void as $$ declare r1 record; r2 record; begin select 10 as a, 20 as b into r1; r2 := df2(r1, '{}'); raise notice '%', r2.a; end; $$ language plpgsql; select * from plpgsql_check_function('t1()'); plpgsql_check_function ------------------------ (0 rows) create or replace function df1(anyelement) returns anyelement as $$ select $1 $$ language sql; create or replace function df22(jsonb, anyelement) returns anyelement as $$ select $2; $$ language sql; create or replace function t1() returns void as $$ declare r1 record; r2 record; begin select 10 as a, 20 as b into r1; r2 := df1(r1); raise notice '%', r2.a; end; $$ language plpgsql; select * from plpgsql_check_function('t1()'); plpgsql_check_function ------------------------ (0 rows) create or replace function t1() returns void as $$ declare r1 record; r2 record; begin select 10 as a, 20 as b into r1; r2 := df22('{}', r1); raise notice '%', r2.a; end; $$ language plpgsql; select * from plpgsql_check_function('t1()'); plpgsql_check_function ------------------------ (0 rows) drop function df1(anyelement); drop function df2(anyelement, jsonb); drop function df22(jsonb, anyelement); drop function t1(); create or replace function dyntest() returns void as $$ begin execute 'drop table if exists xxx; create table xxx(a int)'; end; $$ language plpgsql; -- should be ok select * from plpgsql_check_function('dyntest'); plpgsql_check_function ------------------------ (0 rows) create or replace function dyntest() returns void as $$ declare x int; begin execute 'drop table if exists xxx; create table xxx(a int)' into x; end; $$ language plpgsql; -- should to report error select * from plpgsql_check_function('dyntest'); plpgsql_check_function ------------------------------------------------------- error:XX000:4:EXECUTE:expression does not return data (1 row) drop function dyntest(); -- should to report error create type typ2 as (a int, b int); create or replace function broken_into() returns void as $$ declare v typ2; begin -- should to fail select (10,20)::typ2 into v; -- should be ok select ((10,20)::typ2).* into v; -- should to fail execute 'select (10,20)::typ2' into v; -- should be ok execute 'select ((10,20)::typ2).*' into v; end; $$ language plpgsql; select * from plpgsql_check_function('broken_into', fatal_errors => false); plpgsql_check_function ------------------------------------------------------------------------------------------------------------ error:42804:5:SQL statement:cannot cast composite value of "typ2" type to a scalar value of "integer" type Query: select (10,20)::typ2 -- ^ Context: at SQL statement to variable "v" declared on line 2 warning:00000:5:SQL statement:too few attributes for composite variable Query: select (10,20)::typ2 -- ^ Context: at SQL statement to variable "v" declared on line 2 error:42804:9:EXECUTE:cannot cast composite value of "typ2" type to a scalar value of "integer" type Context: at EXECUTE to variable "v" declared on line 2 warning:00000:9:EXECUTE:too few attributes for composite variable Context: at EXECUTE to variable "v" declared on line 2 warning extra:00000:2:DECLARE:never read variable "v" (13 rows) drop function broken_into(); drop type typ2; -- check output in xml or json formats CREATE OR REPLACE FUNCTION test_function() RETURNS void LANGUAGE plpgsql AS $function$ begin insert into non_existing_table values (1); end $function$; select * from plpgsql_check_function('test_function', format => 'xml'); plpgsql_check_function ---------------------------------------------------------------------------- + + error + 42P01 + relation "non_existing_table" does not exist + SQL statement + insert into non_existing_table values (1)+ + (1 row) select * from plpgsql_check_function('test_function', format => 'json'); plpgsql_check_function ------------------------------------------------------------------- { + "issues": [ + { + "level": "error", + "message":"relation \"non_existing_table\" does not exist",+ "statement": { + "lineNumber": "3", + "text": "SQL statement" + }, + "query": { + "position": "13", + "text": "insert into non_existing_table values (1)" + }, + "sqlState": "42P01" + } + ] + } (1 row) drop function test_function(); -- test settype pragma create or replace function test_function() returns void as $$ declare r record; begin raise notice '%', r.a; end; $$ language plpgsql; -- should to detect error select * from plpgsql_check_function('test_function'); plpgsql_check_function ---------------------------------------------------------------------------- error:55000:4:RAISE:record "r" is not assigned yet Detail: The tuple structure of a not-yet-assigned record is indeterminate. Context: SQL expression "r.a" (3 rows) create type ctype as (a int, b int); create or replace function test_function() returns void as $$ declare r record; begin perform plpgsql_check_pragma('type: r ctype'); raise notice '%', r.a; end; $$ language plpgsql; -- should to be ok select * from plpgsql_check_function('test_function'); plpgsql_check_function ------------------------ (0 rows) create or replace function test_function() returns void as $$ <>declare r record; begin perform plpgsql_check_pragma('type: x.r public."ctype"'); raise notice '%', r.a; end; $$ language plpgsql; -- should to be ok select * from plpgsql_check_function('test_function'); plpgsql_check_function ------------------------ (0 rows) create or replace function test_function() returns void as $$ <>declare r record; begin perform plpgsql_check_pragma('type: "x".r (a int, b int)'); raise notice '%', r.a; end; $$ language plpgsql; -- should to be ok select * from plpgsql_check_function('test_function'); plpgsql_check_function ------------------------ (0 rows) create or replace function test_function() returns void as $$ <>declare r record; begin perform plpgsql_check_pragma('type: "x".r (a int, b int'); raise notice '%', r.a; end; $$ language plpgsql; -- should to be ok select * from plpgsql_check_function('test_function'); WARNING: Pragma "type" on line 4 is not processed. plpgsql_check_function ---------------------------------------------------------------------------- error:55000:5:RAISE:record "r" is not assigned yet Detail: The tuple structure of a not-yet-assigned record is indeterminate. Context: SQL expression "r.a" (3 rows) create or replace function test_function() returns void as $$ <>declare r record; begin perform plpgsql_check_pragma('type: "x".r (a int, b int)x'); raise notice '%', r.a; end; $$ language plpgsql; -- should to be ok select * from plpgsql_check_function('test_function'); WARNING: Pragma "type" on line 4 is not processed. plpgsql_check_function ---------------------------------------------------------------------------- error:55000:5:RAISE:record "r" is not assigned yet Detail: The tuple structure of a not-yet-assigned record is indeterminate. Context: SQL expression "r.a" (3 rows) drop function test_function(); drop type ctype; create or replace function test_function() returns void as $$ declare r pg_class; begin create temp table foo(like pg_class); select * from foo into r; end; $$ language plpgsql; -- should to raise an error select * from plpgsql_check_function('test_function'); plpgsql_check_function ----------------------------------------------------------- error:42P01:5:SQL statement:relation "foo" does not exist Query: select * from foo -- ^ (3 rows) create or replace function test_function() returns void as $$ declare r record; begin create temp table foo(like pg_class); perform plpgsql_check_pragma('table: foo(like pg_class)'); select * from foo into r; raise notice '%', r.relname; end; $$ language plpgsql; -- should be ok select * from plpgsql_check_function('test_function'); plpgsql_check_function ------------------------ (0 rows) drop function test_function(); -- now plpgsql_check can do some other checks when statement EXECUTE -- contains only format function with constant fmt. create or replace function test_function() returns void as $$ begin execute format('create table zzz %I(a int, b int)', 'zzz'); end; $$ language plpgsql; -- should to detect bad expression select * from plpgsql_check_function('test_function'); plpgsql_check_function ----------------------------------------------------- error:42601:3:EXECUTE:syntax error at or near "zzz" Query: create table zzz zzz(a int, b int) (2 rows) -- should to correctly detect type create or replace function test_function() returns void as $$ declare r record; begin execute format('select %L::date + 1 as x', current_date) into r; raise notice '%', extract(dow from r.x); end; $$ language plpgsql; -- should be ok select * from plpgsql_check_function('test_function'); plpgsql_check_function ------------------------ (0 rows) -- should not to crash create or replace function test_function() returns void as $$ declare r record; begin r := null; end; $$ language plpgsql; select * from plpgsql_check_function('test_function'); plpgsql_check_function ------------------------------------------------------- warning extra:00000:2:DECLARE:never read variable "r" (1 row) drop function test_function(); -- ignores syntax errors when literals placehodlers are used create function test_function() returns void as $$ begin execute format('do %L', 'begin end'); end $$ language plpgsql; select * from plpgsql_check_function('test_function'); plpgsql_check_function ------------------------ (0 rows) drop function test_function(); load 'plpgsql_check'; drop type testtype cascade; create type testtype as (a int, b int); create function test_function() returns record as $$ declare r record; begin r := (10,20); if false then return r; end if; return null; end; $$ language plpgsql; create function test_function33() returns record as $$ declare r testtype; begin r := (10,20); if false then return r; end if; return null; end; $$ language plpgsql; -- should not to raise false alarm due check against fake result type select plpgsql_check_function('test_function'); plpgsql_check_function ------------------------ (0 rows) select plpgsql_check_function('test_function33'); plpgsql_check_function ------------------------ (0 rows) -- try to check in passive mode set plpgsql_check.mode = 'every_start'; select test_function(); test_function --------------- (1 row) select test_function33(); test_function33 ----------------- (1 row) select * from test_function() as (a int, b int); a | b ---+--- | (1 row) select * from test_function33() as (a int, b int); a | b ---+--- | (1 row) -- should to identify error select * from test_function() as (a int, b int, c int); ERROR: returned record type does not match expected record type select * from test_function33() as (a int, b int, c int); ERROR: returned record type does not match expected record type drop function test_function(); drop function test_function33(); drop type testtype; set plpgsql_check.mode to default; -- should not to raise false alarm create type c1 as ( a text ); create table t1 ( a c1, b c1 ); insert into t1 (values ('(abc)', '(def)')); alter table t1 drop column a; create or replace function test_function() returns t1 as $$ declare myrow t1%rowtype; begin select * into myrow from t1 limit 1; return myrow; end; $$ language plpgsql; select * from test_function(); b ------- (def) (1 row) select * from plpgsql_check_function('public.test_function()'); plpgsql_check_function ------------------------ (0 rows) drop function test_function(); drop table t1; drop type c1; -- compatibility warnings create or replace function foo01() returns refcursor as $$ declare c cursor for select 1; r refcursor; begin open c; r := c; return r; end; $$ language plpgsql; -- no warnings select * from plpgsql_check_function('foo01', compatibility_warnings => true); plpgsql_check_function ------------------------ (0 rows) create or replace function foo01() returns refcursor as $$ declare c cursor for select 1; r refcursor; begin open c; r := 'c'; return r; end; $$ language plpgsql; -- warning select * from plpgsql_check_function('foo01', extra_warnings => false, compatibility_warnings => true); plpgsql_check_function ----------------------------------------------------------------------------------- compatibility:00000:7:assignment:obsolete setting of refcursor or cursor variable Detail: Internal name of cursor should not be specified by users. Context: at assignment to variable "r" declared on line 4 (3 rows) create or replace function foo01() returns refcursor as $$ declare c cursor for select 1; r refcursor; begin open c; r := c; return 'c'; end; $$ language plpgsql; -- warning select * from plpgsql_check_function('foo01', extra_warnings => false, compatibility_warnings => true); plpgsql_check_function -------------------------------------------------------------------------------------- compatibility:00000:8:RETURN:obsolete setting of refcursor or cursor variable Detail: Internal name of cursor should not be specified by users. warning:42804:8:RETURN:target type is different type than source type Detail: cast "text" value to "refcursor" type Hint: The input expression type does not have an assignment cast to the target type. Context: at RETURN (6 rows) -- pragma sequence test create or replace function test_function() returns void as $$ begin perform plpgsql_check_pragma('sequence: xx'); perform nextval('pg_temp.xy'); perform nextval('pg_temp.xx'); end $$ language plpgsql; select * from plpgsql_check_function('test_function'); plpgsql_check_function ------------------------------------------------------------ error:42P01:4:PERFORM:relation "pg_temp.xy" does not exist Query: SELECT nextval('pg_temp.xy') -- ^ (3 rows) drop function test_function(); create table t1(x int); create or replace function f1_trg() returns trigger as $$ declare x int; begin return x; end; $$ language plpgsql; create trigger t1_f1 before insert on t1 for each row execute procedure f1_trg(); -- raise error select * from plpgsql_check_function('f1_trg', 't1'); plpgsql_check_function ----------------------------------------------------------------------------------------------- error:42804:4:RETURN:cannot return non-composite value from function returning composite type (1 row) drop trigger t1_f1 on t1; drop table t1; drop function f1_trg; create function test_function() returns void as $$ declare a int; b int; c int; d char; begin c := a + d; end; $$ language plpgsql; -- only b should be marked as unused variable select * from plpgsql_check_function('test_function', fatal_errors := false); plpgsql_check_function --------------------------------------------------------------------------------------------------------- error:42883:6:assignment:operator does not exist: integer + character Query: c := a + d -- ^ Hint: No operator matches the given name and argument types. You might need to add explicit type casts. Context: at assignment to variable "c" declared on line 4 warning:00000:3:DECLARE:unused variable "b" warning extra:00000:4:DECLARE:never read variable "c" (7 rows) drop function test_function(); -- from plpgsql_check_active-12 create or replace procedure proc(a int) as $$ begin end; $$ language plpgsql; call proc(10); select * from plpgsql_check_function('proc(int)'); plpgsql_check_function ------------------------------------------ warning extra:00000:unused parameter "a" (1 row) create or replace procedure testproc() as $$ begin call proc(10); end; $$ language plpgsql; call testproc(); select * from plpgsql_check_function('testproc()'); plpgsql_check_function ------------------------ (0 rows) -- should to fail create or replace procedure testproc() as $$ begin call proc((select count(*) from pg_class)); end; $$ language plpgsql; call testproc(); ERROR: cannot use subquery in CALL argument at character 11 select * from plpgsql_check_function('testproc()'); plpgsql_check_function --------------------------------------------------------- error:0A000:3:CALL:cannot use subquery in CALL argument Query: call proc((select count(*) from pg_class)) -- ^ (3 rows) drop procedure proc(int); create procedure proc(in a int, inout b int, in c int) as $$ begin end; $$ language plpgsql; select * from plpgsql_check_function('proc(int,int, int)'); plpgsql_check_function ------------------------------------------------- warning extra:00000:unused parameter "a" warning extra:00000:unused parameter "b" warning extra:00000:unused parameter "c" warning extra:00000:unmodified OUT variable "b" (4 rows) create or replace procedure proc(in a int, inout b int, in c int) as $$ begin b := a + c; end; $$ language plpgsql; select * from plpgsql_check_function('proc(int,int, int)'); plpgsql_check_function ------------------------ (0 rows) create or replace procedure testproc() as $$ declare r int; begin call proc(10, r, 20); end; $$ language plpgsql; call testproc(); select * from plpgsql_check_function('testproc()'); plpgsql_check_function ------------------------ (0 rows) -- should to fail create or replace procedure testproc() as $$ declare r int; begin call proc(10, r + 10, 20); end; $$ language plpgsql; call testproc(); ERROR: procedure parameter "b" is an output parameter but corresponding argument is not writable select * from plpgsql_check_function('testproc()'); plpgsql_check_function -------------------------------------------------------------------------------------------------------------- error:42601:4:CALL:procedure parameter "b" is an output parameter but corresponding argument is not writable (1 row) create or replace procedure testproc(inout r int) as $$ begin call proc(10, r, 20); end; $$ language plpgsql; call testproc(10); r ---- 30 (1 row) select * from plpgsql_check_function('testproc(int)'); plpgsql_check_function ------------------------ (0 rows) drop procedure testproc(int); -- should to raise warnings create or replace procedure testproc2(in p1 int, inout p2 int, in p3 int, inout p4 int) as $$ begin raise notice '% %', p1, p3; end; $$ language plpgsql; select * from plpgsql_check_function('testproc2'); plpgsql_check_function -------------------------------------------------- warning extra:00000:unused parameter "p2" warning extra:00000:unused parameter "p4" warning extra:00000:unmodified OUT variable "p2" warning extra:00000:unmodified OUT variable "p4" (4 rows) drop procedure testproc2; -- should be ok create or replace procedure testproc3(in p1 int, inout p2 int, in p3 int, inout p4 int) as $$ begin p2 := p1; p4 := p3; end; $$ language plpgsql; select * from plpgsql_check_function('testproc3'); plpgsql_check_function ------------------------ (0 rows) drop procedure testproc3; /* * Test pragma */ create or replace function test_pragma() returns void language plpgsql as $$ declare r record; begin perform plpgsql_check_pragma('disable:check'); raise notice '%', r.y; perform plpgsql_check_pragma('enable:check'); select 10 as a, 20 as b into r; raise notice '%', r.a; raise notice '%', r.x; end; $$; select * from plpgsql_check_function('test_pragma'); plpgsql_check_function ------------------------------------------------- error:42703:9:RAISE:record "r" has no field "x" Context: SQL expression "r.x" (2 rows) create or replace function test_pragma() returns void language plpgsql as $$ declare r record; begin if false then -- check is disabled just for if body perform plpgsql_check_pragma('disable:check'); raise notice '%', r.y; end if; select 10 as a, 20 as b into r; raise notice '%', r.a; raise notice '%', r.x; end; $$; select * from plpgsql_check_function('test_pragma'); plpgsql_check_function -------------------------------------------------- error:42703:11:RAISE:record "r" has no field "x" Context: SQL expression "r.x" (2 rows) drop function test_pragma(); create or replace function nested_trace_test(a int) returns int as $$ begin return a + 1; end; $$ language plpgsql; create or replace function trace_test(b int) returns int as $$ declare r int default 0; begin for i in 1..b loop r := nested_trace_test(r); end loop; return r; end; $$ language plpgsql; select trace_test(3); trace_test ------------ 3 (1 row) set plpgsql_check.enable_tracer to on; set plpgsql_check.tracer to on; set plpgsql_check.tracer_test_mode = true; select trace_test(3); NOTICE: #0 ->> start of function trace_test(integer) (oid=0, tnl=1) NOTICE: #0 "b" => '3' NOTICE: #1 ->> start of function nested_trace_test(integer) (oid=0, tnl=1) NOTICE: #1 context: PL/pgSQL function trace_test(integer) line 6 at assignment NOTICE: #1 "a" => '0' NOTICE: #1 <<- end of function nested_trace_test (elapsed time=0.010 ms) NOTICE: #1 ->> start of function nested_trace_test(integer) (oid=0, tnl=1) NOTICE: #1 context: PL/pgSQL function trace_test(integer) line 6 at assignment NOTICE: #1 "a" => '1' NOTICE: #1 <<- end of function nested_trace_test (elapsed time=0.010 ms) NOTICE: #1 ->> start of function nested_trace_test(integer) (oid=0, tnl=1) NOTICE: #1 context: PL/pgSQL function trace_test(integer) line 6 at assignment NOTICE: #1 "a" => '2' NOTICE: #1 <<- end of function nested_trace_test (elapsed time=0.010 ms) NOTICE: #0 <<- end of function trace_test (elapsed time=0.010 ms) trace_test ------------ 3 (1 row) set plpgsql_check.tracer_verbosity TO verbose; select trace_test(3); NOTICE: #0 ->> start of function trace_test(integer) (oid=0, tnl=1) NOTICE: #0 "b" => '3' NOTICE: #0.1 3 --> start of statement block (tnl=1) NOTICE: #0.2 4 --> start of FOR with integer loop variable (tnl=1) NOTICE: #0.3 6 --> start of assignment r := nested_trace_test(r) (tnl=1) NOTICE: #0.3 "r" => '0' NOTICE: #1 ->> start of function nested_trace_test(integer) (oid=0, tnl=1) NOTICE: #1 context: PL/pgSQL function trace_test(integer) line 6 at assignment NOTICE: #1 "a" => '0' NOTICE: #1.1 2 --> start of statement block (tnl=1) NOTICE: #1.2 3 --> start of RETURN (expr='a + 1') (tnl=1) NOTICE: #1.2 "a" => '0' NOTICE: #1.2 <-- end of RETURN (elapsed time=0.010 ms) NOTICE: #1.1 <-- end of statement block (elapsed time=0.010 ms) NOTICE: #1 <<- end of function nested_trace_test (elapsed time=0.010 ms) NOTICE: #0.3 <-- end of assignment (elapsed time=0.010 ms) NOTICE: #0.3 "r" => '1' NOTICE: #0.3 6 --> start of assignment r := nested_trace_test(r) (tnl=1) NOTICE: #0.3 "r" => '1' NOTICE: #1 ->> start of function nested_trace_test(integer) (oid=0, tnl=1) NOTICE: #1 context: PL/pgSQL function trace_test(integer) line 6 at assignment NOTICE: #1 "a" => '1' NOTICE: #1.1 2 --> start of statement block (tnl=1) NOTICE: #1.2 3 --> start of RETURN (expr='a + 1') (tnl=1) NOTICE: #1.2 "a" => '1' NOTICE: #1.2 <-- end of RETURN (elapsed time=0.010 ms) NOTICE: #1.1 <-- end of statement block (elapsed time=0.010 ms) NOTICE: #1 <<- end of function nested_trace_test (elapsed time=0.010 ms) NOTICE: #0.3 <-- end of assignment (elapsed time=0.010 ms) NOTICE: #0.3 "r" => '2' NOTICE: #0.3 6 --> start of assignment r := nested_trace_test(r) (tnl=1) NOTICE: #0.3 "r" => '2' NOTICE: #1 ->> start of function nested_trace_test(integer) (oid=0, tnl=1) NOTICE: #1 context: PL/pgSQL function trace_test(integer) line 6 at assignment NOTICE: #1 "a" => '2' NOTICE: #1.1 2 --> start of statement block (tnl=1) NOTICE: #1.2 3 --> start of RETURN (expr='a + 1') (tnl=1) NOTICE: #1.2 "a" => '2' NOTICE: #1.2 <-- end of RETURN (elapsed time=0.010 ms) NOTICE: #1.1 <-- end of statement block (elapsed time=0.010 ms) NOTICE: #1 <<- end of function nested_trace_test (elapsed time=0.010 ms) NOTICE: #0.3 <-- end of assignment (elapsed time=0.010 ms) NOTICE: #0.3 "r" => '3' NOTICE: #0.2 <-- end of FOR with integer loop variable (elapsed time=0.010 ms) NOTICE: #0.4 8 --> start of RETURN (tnl=1) NOTICE: #0.4 "r" => '3' NOTICE: #0.4 <-- end of RETURN (elapsed time=0.010 ms) NOTICE: #0.1 <-- end of statement block (elapsed time=0.010 ms) NOTICE: #0 <<- end of function trace_test (elapsed time=0.010 ms) trace_test ------------ 3 (1 row) create or replace function trace_test(b int) returns int as $$ declare r int default 0; begin for i in 1..b loop perform plpgsql_check_pragma('disable:tracer'); r := nested_trace_test(r); end loop; return r; end; $$ language plpgsql; select trace_test(3); NOTICE: #0 ->> start of function trace_test(integer) (oid=0, tnl=1) NOTICE: #0 "b" => '3' NOTICE: #0.1 3 --> start of statement block (tnl=1) NOTICE: #0.2 4 --> start of FOR with integer loop variable (tnl=1) NOTICE: #0.3 6 --> start of perform plpgsql_check_pragma('disable: .. (tnl=1) NOTICE: #0.3 <-- end of PERFORM (elapsed time=0.010 ms) NOTICE: #0.2 <-- end of FOR with integer loop variable (elapsed time=0.010 ms) NOTICE: #0.5 9 --> start of RETURN (tnl=1) NOTICE: #0.5 "r" => '3' NOTICE: #0.5 <-- end of RETURN (elapsed time=0.010 ms) NOTICE: #0.1 <-- end of statement block (elapsed time=0.010 ms) NOTICE: #0 <<- end of function trace_test (elapsed time=0.010 ms) trace_test ------------ 3 (1 row) create or replace function nested_trace_test(a int) returns int as $$ begin perform plpgsql_check_pragma('enable:tracer'); return a + 1; end; $$ language plpgsql; select trace_test(3); NOTICE: #0 ->> start of function trace_test(integer) (oid=0, tnl=1) NOTICE: #0 "b" => '3' NOTICE: #0.1 3 --> start of statement block (tnl=1) NOTICE: #0.2 4 --> start of FOR with integer loop variable (tnl=1) NOTICE: #0.3 6 --> start of perform plpgsql_check_pragma('disable: .. (tnl=1) NOTICE: #0.3 <-- end of PERFORM (elapsed time=0.010 ms) NOTICE: #1.3 4 --> start of RETURN (expr='a + 1') (tnl=1) NOTICE: #1.3 "a" => '0' NOTICE: #1.3 <-- end of RETURN (elapsed time=0.010 ms) NOTICE: #1.3 4 --> start of RETURN (expr='a + 1') (tnl=1) NOTICE: #1.3 "a" => '1' NOTICE: #1.3 <-- end of RETURN (elapsed time=0.010 ms) NOTICE: #1.3 4 --> start of RETURN (expr='a + 1') (tnl=1) NOTICE: #1.3 "a" => '2' NOTICE: #1.3 <-- end of RETURN (elapsed time=0.010 ms) NOTICE: #0.2 <-- end of FOR with integer loop variable (elapsed time=0.010 ms) NOTICE: #0.5 9 --> start of RETURN (tnl=1) NOTICE: #0.5 "r" => '3' NOTICE: #0.5 <-- end of RETURN (elapsed time=0.010 ms) NOTICE: #0.1 <-- end of statement block (elapsed time=0.010 ms) NOTICE: #0 <<- end of function trace_test (elapsed time=0.010 ms) trace_test ------------ 3 (1 row) drop function trace_test(int); drop function nested_trace_test(int); create or replace function trace_test(int) returns int as $$ declare r int default 0; begin for i in 1..$1 loop r := r + 1; end loop; r := r + 10; return r; end; $$ language plpgsql; select trace_test(4); NOTICE: #0 ->> start of function trace_test(integer) (oid=0, tnl=1) NOTICE: #0 "$1" => '4' NOTICE: #0.1 3 --> start of statement block (tnl=1) NOTICE: #0.2 4 --> start of FOR with integer loop variable (tnl=1) NOTICE: #0.3 5 --> start of assignment r := r + 1 (tnl=1) NOTICE: #0.3 "r" => '0' NOTICE: #0.3 <-- end of assignment (elapsed time=0.010 ms) NOTICE: #0.3 "r" => '1' NOTICE: #0.3 5 --> start of assignment r := r + 1 (tnl=1) NOTICE: #0.3 "r" => '1' NOTICE: #0.3 <-- end of assignment (elapsed time=0.010 ms) NOTICE: #0.3 "r" => '2' NOTICE: #0.3 5 --> start of assignment r := r + 1 (tnl=1) NOTICE: #0.3 "r" => '2' NOTICE: #0.3 <-- end of assignment (elapsed time=0.010 ms) NOTICE: #0.3 "r" => '3' NOTICE: #0.3 5 --> start of assignment r := r + 1 (tnl=1) NOTICE: #0.3 "r" => '3' NOTICE: #0.3 <-- end of assignment (elapsed time=0.010 ms) NOTICE: #0.3 "r" => '4' NOTICE: #0.2 <-- end of FOR with integer loop variable (elapsed time=0.010 ms) NOTICE: #0.4 7 --> start of assignment r := r + 10 (tnl=1) NOTICE: #0.4 "r" => '4' NOTICE: #0.4 <-- end of assignment (elapsed time=0.010 ms) NOTICE: #0.4 "r" => '14' NOTICE: #0.5 8 --> start of RETURN (tnl=1) NOTICE: #0.5 "r" => '14' NOTICE: #0.5 <-- end of RETURN (elapsed time=0.010 ms) NOTICE: #0.1 <-- end of statement block (elapsed time=0.010 ms) NOTICE: #0 <<- end of function trace_test (elapsed time=0.010 ms) trace_test ------------ 14 (1 row) create or replace function trace_test(int) returns int as $$ declare r int default 0; begin for i in 1..$1 loop perform plpgsql_check_pragma('disable:tracer'); r := r + 1; end loop; r := r + 10; return r; end; $$ language plpgsql; select trace_test(4); NOTICE: #0 ->> start of function trace_test(integer) (oid=0, tnl=1) NOTICE: #0 "$1" => '4' NOTICE: #0.1 3 --> start of statement block (tnl=1) NOTICE: #0.2 4 --> start of FOR with integer loop variable (tnl=1) NOTICE: #0.3 5 --> start of perform plpgsql_check_pragma('disable: .. (tnl=1) NOTICE: #0.3 <-- end of PERFORM (elapsed time=0.010 ms) NOTICE: #0.2 <-- end of FOR with integer loop variable (elapsed time=0.010 ms) NOTICE: #0.5 8 --> start of assignment r := r + 10 (tnl=1) NOTICE: #0.5 "r" => '4' NOTICE: #0.5 <-- end of assignment (elapsed time=0.010 ms) NOTICE: #0.5 "r" => '14' NOTICE: #0.6 9 --> start of RETURN (tnl=1) NOTICE: #0.6 "r" => '14' NOTICE: #0.6 <-- end of RETURN (elapsed time=0.010 ms) NOTICE: #0.1 <-- end of statement block (elapsed time=0.010 ms) NOTICE: #0 <<- end of function trace_test (elapsed time=0.010 ms) trace_test ------------ 14 (1 row) create or replace function trace_test(int) returns int as $$ declare r int default 0; begin perform plpgsql_check_pragma('disable:tracer'); for i in 1..$1 loop r := r + 1; end loop; perform plpgsql_check_pragma('enable:tracer'); r := r + 10; return r; end; $$ language plpgsql; select trace_test(4); NOTICE: #0 ->> start of function trace_test(integer) (oid=0, tnl=1) NOTICE: #0 "$1" => '4' NOTICE: #0.1 3 --> start of statement block (tnl=1) NOTICE: #0.2 4 --> start of perform plpgsql_check_pragma('disable: .. (tnl=1) NOTICE: #0.2 <-- end of PERFORM (elapsed time=0.010 ms) NOTICE: #0.6 12 --> start of assignment r := r + 10 (tnl=1) NOTICE: #0.6 "r" => '4' NOTICE: #0.6 <-- end of assignment (elapsed time=0.010 ms) NOTICE: #0.6 "r" => '14' NOTICE: #0.7 13 --> start of RETURN (tnl=1) NOTICE: #0.7 "r" => '14' NOTICE: #0.7 <-- end of RETURN (elapsed time=0.010 ms) NOTICE: #0.1 <-- end of statement block (elapsed time=0.010 ms) NOTICE: #0 <<- end of function trace_test (elapsed time=0.010 ms) trace_test ------------ 14 (1 row) drop function trace_test(int); create or replace function public.fx1() returns table(i integer, j integer, found boolean) as $$ begin for i in 1..10 loop for j in 1..10 loop return next; end loop; end loop; end; $$ language plpgsql immutable; select * from plpgsql_check_function('fx1'); plpgsql_check_function -------------------------------------------------------------------------- warning:00000:2:statement block:parameter "found" is shadowed Detail: Local auto variable shadows function parameter. warning:00000:3:FOR with integer loop variable:parameter "i" is shadowed Detail: Local auto variable shadows function parameter. warning:00000:5:FOR with integer loop variable:parameter "j" is shadowed Detail: Local auto variable shadows function parameter. warning extra:00000:unmodified OUT variable "i" warning extra:00000:unmodified OUT variable "j" warning extra:00000:unmodified OUT variable "found" (9 rows) drop function fx1; -- tracer test set plpgsql_check.enable_tracer to on; select plpgsql_check_tracer(true); NOTICE: tracer is active NOTICE: tracer verbosity is verbose plpgsql_check_tracer ---------------------- t (1 row) create role regress_plpgsql_check_test_role; DO $$ begin begin -- should to fail create role regress_plpgsql_check_test_role; exception when duplicate_object then -- Role already exists -- the exception handler is empty (#156) end; end; $$; NOTICE: #0 ->> start of block inline_code_block (oid=0, tnl=1) NOTICE: #0.1 2 --> start of statement block (tnl=1) NOTICE: #0.2 3 --> start of statement block (tnl=1) NOTICE: #0.3 5 --> start of SQL statement (query='create role regress_plpgsql_ch ..') (tnl=2) NOTICE: #0.3 <-- end of SQL statement (elapsed time=0.010 ms) aborted NOTICE: #0.2 <-- end of statement block (elapsed time=0.010 ms) NOTICE: #0.1 <-- end of statement block (elapsed time=0.010 ms) NOTICE: #0 <<- end of block (elapsed time=0.010 ms) drop role regress_plpgsql_check_test_role; set plpgsql_check.enable_tracer to off; select plpgsql_check_tracer(false); NOTICE: tracer is not active NOTICE: tracer verbosity is verbose plpgsql_check_tracer ---------------------- f (1 row) -- tracing constants -- issue #159 create or replace function tabret_dynamic() returns table (id integer, val text) as $$ begin return query execute 'select id, val from (values (1, ''a''), (2, ''b'')) as v(id, val)'; end; $$ language plpgsql immutable; -- should be ok select * from plpgsql_check_function('tabret_dynamic()'); plpgsql_check_function ------------------------ (0 rows) create or replace function tabret_dynamic() returns table (id integer, val text) as $$ declare z_query text; begin z_query := 'select id, val from (values (1, ''a''), (2, ''b'')) as v(id, val)'; execute z_query into id, val; return next; end; $$ language plpgsql immutable; -- should be ok select * from plpgsql_check_function('tabret_dynamic()'); plpgsql_check_function ------------------------ (0 rows) create or replace function tabret_dynamic() returns table (id integer, val text) as $$ declare z_query text; begin z_query := 'select id, val from (values (1, ''a''), (2, ''b'')) as v(id, val)'; for id, val in execute z_query loop return next; end loop; end; $$ language plpgsql immutable; -- should be ok select * from plpgsql_check_function('tabret_dynamic()'); plpgsql_check_function ------------------------ (0 rows) create or replace function tabret_dynamic() returns table (id integer, val text) as $$ declare z_query text; begin z_query := 'select id, val from (values (1, ''a''), (2, ''b'')) as v(id, val)'; return query execute z_query; end; $$ language plpgsql immutable; -- should be ok select * from plpgsql_check_function('tabret_dynamic()'); plpgsql_check_function ------------------------ (0 rows) drop function tabret_dynamic; -- should not to crash on empty string, or comment create or replace function dynamic_emptystr() returns void as $$ begin execute ''; end; $$ language plpgsql; -- should be ok select * from plpgsql_check_function('dynamic_emptystr()'); plpgsql_check_function ------------------------ (0 rows) create or replace function dynamic_emptystr() returns void as $$ declare x int; begin execute '--' into x; end; $$ language plpgsql; -- should not to crash, no result error select * from plpgsql_check_function('dynamic_emptystr()'); plpgsql_check_function ------------------------------------------------------- error:XX000:4:EXECUTE:expression does not return data (1 row) drop function dynamic_emptystr(); -- unclosed cursor detection create or replace function fx() returns void as $$ declare c refcursor; begin open c for select * from pg_class; end; $$ language plpgsql; do $$ begin perform fx(); -- should to show warning perform fx(); end; $$; WARNING: cursor "c" is not closed set plpgsql_check.strict_cursors_leaks to on; do $$ begin -- should to show warning perform fx(); -- should to show warning perform fx(); end; $$; WARNING: cursor is not closed WARNING: cursor is not closed create or replace function fx() returns void as $$ declare c refcursor; begin open c for select * from pg_class; close c; end; $$ language plpgsql; -- without warnings do $$ begin perform fx(); perform fx(); end; $$; set plpgsql_check.strict_cursors_leaks to off; drop function fx(); create table public.testt(a int, b int); create or replace function fx() returns void as $$ declare v1 varchar default 'public'; v2 varchar default 'testt'; v3 varchar default 'a'; begin raise notice '%', format('%I.%I.%I', v1, v2, v3); perform 'pragma:assert-schema: v1'; perform 'pragma:assert-table: v1, v2'; perform 'pragma:assert-column: v1, v2, v3'; perform 'pragma:assert-table: v2'; perform 'pragma:assert-column: v2, v3'; end; $$ language plpgsql; select * from plpgsql_check_function('fx()'); plpgsql_check_function ------------------------ (0 rows) create table public.testt(a int, b int); ERROR: relation "testt" already exists create or replace function fx() returns void as $$ declare v1 varchar default 'public'; v2 varchar default 'testt'; v3 varchar default 'x'; begin raise notice '%', format('%I.%I.%I', v1, v2, v3); perform 'pragma:assert-column: v1, v2, v3'; end; $$ language plpgsql; select * from plpgsql_check_function('fx()'); plpgsql_check_function ------------------------------------------------------------------------------ error:42703:8:PERFORM:column "x" of relation "public"."testt" does not exist (1 row) drop function fx(); drop table public.testt; create or replace function fx() returns void as $$ declare v_sql varchar default ''; i int; begin -- we don't support constant tracing from queries select 'bla bla bla' into v_sql; execute v_sql into i; raise notice '%', i; end; $$ language plpgsql; -- should be ok select * from plpgsql_check_function('fx()'); plpgsql_check_function ------------------------ (0 rows) drop function fx(); -- should not to raise warning do $$ declare c cursor for select * from generate_series(1,10); t int; begin for i in 1..2 loop open c; loop fetch c into t; exit when not found; raise notice '%', t; end loop; close c; end loop; end; $$; NOTICE: 1 NOTICE: 2 NOTICE: 3 NOTICE: 4 NOTICE: 5 NOTICE: 6 NOTICE: 7 NOTICE: 8 NOTICE: 9 NOTICE: 10 NOTICE: 1 NOTICE: 2 NOTICE: 3 NOTICE: 4 NOTICE: 5 NOTICE: 6 NOTICE: 7 NOTICE: 8 NOTICE: 9 NOTICE: 10 -- should not raise warning create or replace function fx(p text) returns void as $$ begin execute format($_$select '%1$I'$_$, p); end; $$ language plpgsql; -- should be ok select * from plpgsql_check_function('fx(text)'); plpgsql_check_function ------------------------ (0 rows) drop function fx(text); create or replace function fx() returns void as $$ declare p varchar; begin p := 'ahoj'; execute format($_$select '%1$I'$_$, p); end; $$ language plpgsql; -- should be ok select * from plpgsql_check_function('fx()'); plpgsql_check_function ------------------------ (0 rows) drop function fx(); -- should not crash create or replace procedure p1() as $$ begin commit; end; $$ language plpgsql; set plpgsql_check.cursors_leaks to on; do $$ declare c cursor for select 1; begin open c; call p1(); end $$; set plpgsql_check.cursors_leaks to default; drop procedure p1; -- should not crash create or replace function nested_trace_test(a int) returns int as $$ begin perform plpgsql_check_pragma('enable:tracer'); return a + 1; end; $$ language plpgsql; create or replace function trace_test(b int) returns int as $$ declare r int default 0; begin for i in 1..b loop r := nested_trace_test(r); end loop; return r; end; $$ language plpgsql; -- when plpgsql_check is not loaded yet, then plpgsql_check is -- load by perform plpgsql_check_pragma and this is another -- case, when fmgr hook is not called in expected order. \c -- should not to crash select trace_test(3); trace_test ------------ 3 (1 row) create table testtable_pure_expr(a int); -- detection of not pure expressions create or replace function pure_expr() returns void as $$ declare v int; begin v := 1 delete from testtable_pure_expr where a = 10; raise notice '%', v; end; $$ language plpgsql; -- raise warning select * from plpgsql_check_function('pure_expr()'); plpgsql_check_function -------------------------------------------------------------------- warning extra:42601:4:assignment:expression is not pure expression Query: v := 1 -- ^ delete from testtable_pure_expr where a = 10 Detail: there is a possibility of unwanted behave Hint: Maybe you forgot to use a semicolon. Context: at assignment to variable "v" declared on line 2 (7 rows) drop table testtable_pure_expr; -- should not to raise false alarm drop function fxx(ct, ct); drop type ct; create type ct as (x int, y numeric, v varchar); create procedure proc_composite(inout cv ct, x int, y numeric, v varchar) as $$ begin cv.x := x; cv.y := y; cv.v := v; end; $$ language plpgsql; call proc_composite(null, 10, 10.2, 'ahoj'); cv ---------------- (10,10.2,ahoj) (1 row) create or replace function test_proc_composite() returns void as $$ declare cv ct; begin call proc_composite(cv, 10, 10.2, 'ahoj'); end; $$ language plpgsql; select * from plpgsql_check_function('test_proc_composite'); plpgsql_check_function ------------------------ (0 rows) drop function test_proc_composite; drop procedure proc_composite; create procedure proc_composite(inout cv1 ct, cv2 ct) as $$ begin cv1.x := 10; cv2.v := 'Ahoj'; end; $$ language plpgsql; create or replace function test_proc_composite() returns void as $$ declare cv1 ct; cv2 ct; begin call proc_composite(cv1, cv2); end; $$ language plpgsql; select * from plpgsql_check_function('test_proc_composite'); plpgsql_check_function ------------------------ (0 rows) drop function test_proc_composite; drop procedure proc_composite; drop type ct; create or replace function double_usage_of_const_str() returns void as $$ declare p_id_arr integer[]; r_id integer; begin p_id_arr := '{1,2,3}'::integer[]; foreach r_id in array p_id_arr loop raise notice 'id: %', r_id; end loop; --the following loop causes an error foreach r_id in array p_id_arr loop raise notice 'id: %', r_id; end loop; end; $$ language plpgsql; -- should not to raise false alarm #195 select * from plpgsql_check_function('double_usage_of_const_str'); plpgsql_check_function ------------------------ (0 rows) -- using reserved world for names is bad create or replace function test_bad_label() returns int as $$ <> declare x int = 10; begin return x; end; $$ language plpgsql; select * from plpgsql_check_function('test_bad_label'); plpgsql_check_function --------------------------------------------------------------------------- warning:00000:4:statement block:name of label "outer" is reserved keyword Detail: The reserved keyword was used as label name. (2 rows) drop function test_bad_label; -- using slow legacy syntax for assignment create or replace function test_bad_assign() returns int as $$ declare x int; begin select 0 into x; return x; end; $$ language plpgsql immutable; select * from plpgsql_check_function('test_bad_assign()', all_warnings=>true); plpgsql_check_function ------------------------------------------------------------------------ performance:00000:4:SQL statement:detected "SELECT expr INTO variable" Query: select 0 -- ^ Detail: This obsolete syntax of assigning can be slow. Hint: Use syntax "variable := expr" instead. (5 rows) drop function test_bad_assign; create or replace function volatility_check() returns int as $$ begin return (select count(*) from pg_class); end; $$ language plpgsql volatile; -- should be ok select * from plpgsql_check_function('volatility_check()'); plpgsql_check_function ------------------------ (0 rows) create or replace function volatility_check() returns int as $$ begin return (select count(*) from pg_class); end; $$ language plpgsql stable; -- should be ok select * from plpgsql_check_function('volatility_check()'); plpgsql_check_function ------------------------ (0 rows) create or replace function volatility_check() returns int as $$ begin return (select count(*) from pg_class); end; $$ language plpgsql immutable; -- warning select * from plpgsql_check_function('volatility_check()'); plpgsql_check_function ----------------------------------------------------------------------------------------- warning:00000:3:RETURN:routine is marked as IMMUTABLE, but expression is STABLE Detail: there is a risk of unwanted behave Hint: When you fix this issue, please, recheck other functions that uses this function. (3 rows) create or replace function volatility_check() returns int as $$ begin return random()*1000; end; $$ language plpgsql volatile; -- should be ok select * from plpgsql_check_function('volatility_check()'); plpgsql_check_function ------------------------ (0 rows) create or replace function volatility_check() returns int as $$ begin return random()*1000; end; $$ language plpgsql stable; -- warning select * from plpgsql_check_function('volatility_check()'); plpgsql_check_function ----------------------------------------------------------------------------------------- warning:00000:3:RETURN:routine is marked as STABLE, but expression is VOLATILE Detail: there is a risk of unwanted behave Hint: When you fix this issue, please, recheck other functions that uses this function. (3 rows) drop function volatility_check(); -- issue #201 - should not to raise false error, when composite -- constant is used and assigned to composite with more fields create type t_retcode as (retcode varchar, errmsg varchar, id int); create or replace function test_retcode_bad() returns void as $$ declare rc t_retcode := row('MO_REG_ERROR'); begin raise notice '%', rc.retcode; end; $$ language plpgsql; select * from plpgsql_check_function('test_retcode_bad'); plpgsql_check_function ------------------------------------------------------------------------------ warning:00000:4:statement block:too few attributes for composite variable Context: during statement block local variable "rc" initialization on line 3 (2 rows) drop function test_retcode_bad; -- test usage domain of composite in output #202 create domain t_retcode_domain as t_retcode; create or replace function test_retcode_bad(INOUT x1 t_retcode_domain, INOUT x2 int) as $$ begin x1.retcode = concat(x1.retcode, 'xxx'); x2 := x2 + 10; end; $$ language plpgsql; select * from plpgsql_check_function('test_retcode_bad'); plpgsql_check_function ------------------------------------------------------------------------ warning extra:00000:composite OUT variable "x1" is not single argument (1 row) drop function test_retcode_bad; drop type t_retcode cascade; NOTICE: drop cascades to type t_retcode_domain create type demo_payload as (value integer); create function demo_produce_row(out status_code integer, out payload record) as $$ begin status_code := 0; payload := ROW(1)::demo_payload; end $$ language plpgsql; create function demo_consume_row() returns void as $$ declare fetched_row record; typed_payload demo_payload; observed_status integer; begin select * into strict fetched_row from demo_produce_row(); typed_payload := fetched_row.payload; observed_status := fetched_row.status_code; end; $$ language plpgsql; -- should not crash - issue #216 select * from plpgsql_check_function('demo_consume_row()'); plpgsql_check_function --------------------------------------------------------------------- warning extra:00000:4:DECLARE:never read variable "typed_payload" warning extra:00000:5:DECLARE:never read variable "observed_status" (2 rows) -- should not crash - issue #220 create function f_poly(anyelement) returns int as $$ begin return $1; end; $$ language plpgsql; select plpgsql_check_profiler(true); NOTICE: profiler is active plpgsql_check_profiler ------------------------ t (1 row) select f_poly(1); f_poly -------- 1 (1 row) select plpgsql_coverage_statements('f_poly(anyelement)'); plpgsql_coverage_statements ----------------------------- 1 (1 row) select plpgsql_coverage_statements('f_poly'); plpgsql_coverage_statements ----------------------------- 1 (1 row) select plpgsql_check_profiler(false); NOTICE: profiler is not active plpgsql_check_profiler ------------------------ f (1 row) drop function f_poly; create or replace function repro04() returns void as $$ begin execute 'select 1'; end $$ language plpgsql; select * from plpgsql_check_function('repro04()', performance_warnings => true); plpgsql_check_function ----------------------------------------------------------------------------------------- performance:00000:3:EXECUTE:immutable expression without parameters found Detail: the EXECUTE command is not necessary probably Hint: Don't use dynamic SQL when you can use static SQL. performance:00000:routine is marked as VOLATILE, should be IMMUTABLE Hint: When you fix this issue, please, recheck other functions that uses this function. (5 rows) create or replace function repro04_into() returns int as $$ declare r int; begin execute 'select $1' into r using 1; return r; end $$ language plpgsql; select * from plpgsql_check_function('repro04_into()', performance_warnings => true); plpgsql_check_function ----------------------------------------------------------------------------------------- performance:00000:routine is marked as VOLATILE, should be IMMUTABLE Hint: When you fix this issue, please, recheck other functions that uses this function. (2 rows) create or replace function repro04_real_dynsql(p text) returns void as $$ begin execute 'select ' || quote_literal(p); end $$ language plpgsql; select * from plpgsql_check_function('repro04_real_dynsql(text)', performance_warnings => true); plpgsql_check_function ----------------------------------------------------------------------------------------- performance:00000:routine is marked as VOLATILE, should be IMMUTABLE Detail: attention: cannot to determine volatility of used dynamic SQL Hint: When you fix this issue, please, recheck other functions that uses this function. (3 rows) -- a multi statement command is real dynamic sql too, and this case was -- reported correctly even before the fix. create or replace function repro04_multi() returns void as $$ begin execute 'select 1; select 2'; end $$ language plpgsql; select * from plpgsql_check_function('repro04_multi()', performance_warnings => true); plpgsql_check_function ----------------------------------------------------------------------------------------- performance:00000:routine is marked as VOLATILE, should be IMMUTABLE Hint: When you fix this issue, please, recheck other functions that uses this function. (2 rows) create table repro04_multi_target(a int); create or replace function repro04_multi() returns void as $$ begin execute 'select 1; insert into repro04_multi_target values(10)'; end $$ language plpgsql; select * from plpgsql_check_function('repro04_multi()', performance_warnings => true); plpgsql_check_function ------------------------ (0 rows) drop table repro04_multi_target; drop function repro04(); drop function repro04_into(); drop function repro04_real_dynsql(text); drop function repro04_multi(); create or replace function unpriv_issue4_rowexpr() returns void as $$ declare r record; begin select (row(1, 'hello'::text)).* into r; raise notice '%', r.f1; select row(1, 'hello') into r; raise notice '%', r.row; end; $$ language plpgsql; select unpriv_issue4_rowexpr(); NOTICE: 1 NOTICE: (1,hello) unpriv_issue4_rowexpr ----------------------- (1 row) -- there should not be any false errors select * from plpgsql_check_function_tb('unpriv_issue4_rowexpr()'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+---------+--------+------+-------+----------+-------+--------- (0 rows) drop function unpriv_issue4_rowexpr(); okbob-plpgsql_check-800db79/expected/plpgsql_check_active_3.out000066400000000000000000006444231526744060500247360ustar00rootroot00000000000000load 'plpgsql'; create extension if not exists plpgsql_check; set client_min_messages to notice; set plpgsql_check.regress_test_mode = true; -- -- check function statement tests -- --should fail - is not plpgsql select * from plpgsql_check_function_tb('session_user()'); ERROR: "session_user"() is not a plpgsql function create table t1(a int, b int); create table pa (id int, pa_id character varying(32), status character varying(60)); create table ml(ml_id character varying(32), status_from character varying(60), pa_id character varying(32), xyz int); create function f1() returns void as $$ begin if false then update t1 set c = 30; end if; if false then raise notice '%', r.c; end if; end; $$ language plpgsql; select f1(); f1 ---- (1 row) select * from plpgsql_check_function_tb('f1()', fatal_errors := true); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+---------------+----------+--------------------------------------------+--------+------+-------+----------+----------------------+--------- f1 | 4 | SQL statement | 42703 | column "c" of relation "t1" does not exist | | | error | 15 | update t1 set c = 30 | (1 row) select * from plpgsql_check_function_tb('f1()', fatal_errors := false); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+---------------+----------+--------------------------------------------+--------+------+-------+----------+----------------------+--------- f1 | 4 | SQL statement | 42703 | column "c" of relation "t1" does not exist | | | error | 15 | update t1 set c = 30 | f1 | 7 | RAISE | 42P01 | missing FROM-clause entry for table "r" | | | error | 1 | r.c | (2 rows) select * from plpgsql_check_function_tb('f1()'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+---------------+----------+--------------------------------------------+--------+------+-------+----------+----------------------+--------- f1 | 4 | SQL statement | 42703 | column "c" of relation "t1" does not exist | | | error | 15 | update t1 set c = 30 | (1 row) drop function f1(); create function f1() returns void as $$ begin if false then insert into t1 values(10,20); update t1 set a = 10; delete from t1; end if; end; $$ language plpgsql stable; select f1(); f1 ---- (1 row) select * from plpgsql_check_function_tb('f1()', fatal_errors := false); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+---------------+----------+--------------------------------------------------+--------+------+-------+----------+------------------------------+--------- f1 | 4 | SQL statement | 0A000 | INSERT is not allowed in a non volatile function | | | error | 1 | insert into t1 values(10,20) | f1 | 5 | SQL statement | 0A000 | UPDATE is not allowed in a non volatile function | | | error | 1 | update t1 set a = 10 | f1 | 6 | SQL statement | 0A000 | DELETE is not allowed in a non volatile function | | | error | 1 | delete from t1 | (3 rows) drop function f1(); create function f1() returns void as $$ declare r record; begin if false then for r in update t1 set a = a + 1 returning * loop raise notice '%', r.a; end loop; end if; end; $$ language plpgsql; select f1(); f1 ---- (1 row) select * from plpgsql_check_function_tb('f1()', fatal_errors := false); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+---------+--------+------+-------+----------+-------+--------- (0 rows) drop function f1(); create function f1() returns void as $$ declare r record; begin if false then for r in update t1 set a = a + 1 returning * loop raise notice '%', r.a; end loop; end if; end; $$ language plpgsql stable; select f1(); f1 ---- (1 row) select * from plpgsql_check_function_tb('f1()', fatal_errors := false); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+----------------------+----------+--------------------------------------------------+--------+------+-------+----------+-------------------------------------+--------- f1 | 5 | FOR over SELECT rows | 0A000 | UPDATE is not allowed in a non volatile function | | | error | 1 | update t1 set a = a + 1 returning * | (1 row) drop function f1(); create function g1(out a int, out b int) as $$ select 10,20; $$ language sql; create function f1() returns void as $$ declare r record; begin r := g1(); if false then raise notice '%', r.c; end if; end; $$ language plpgsql; select f1(); f1 ---- (1 row) select * from plpgsql_check_function_tb('f1()'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+-----------------------------+--------+------+-------+----------+-------+--------------------------- f1 | 6 | RAISE | 42703 | record "r" has no field "c" | | | error | | | PL/pgSQL expression "r.c" (1 row) select f1(); f1 ---- (1 row) drop function f1(); drop function g1(); create function g1(out a int, out b int) returns setof record as $$ select * from t1; $$ language sql; create function f1() returns void as $$ declare r record; begin for r in select * from g1() loop raise notice '%', r.c; end loop; end; $$ language plpgsql; select f1(); f1 ---- (1 row) select * from plpgsql_check_function_tb('f1()'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+-----------------------------+--------+------+-------+----------+-------+--------------------------- f1 | 6 | RAISE | 42703 | record "r" has no field "c" | | | error | | | PL/pgSQL expression "r.c" (1 row) select f1(); f1 ---- (1 row) create or replace function f1() returns void as $$ declare r record; begin for r in select * from g1() loop r.c := 20; end loop; end; $$ language plpgsql; select f1(); f1 ---- (1 row) select * from plpgsql_check_function_tb('f1()'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+------------+----------+-----------------------------+--------+------+-------+----------+-------+--------------------------------------------------------------- f1 | 6 | assignment | 42703 | record "r" has no field "c" | | | error | | | at assignment to field "c" of variable "r" declared on line 2 (1 row) select f1(); f1 ---- (1 row) drop function f1(); drop function g1(); create function f1() returns int as $$ declare r int; begin if false then r := a + b; end if; return r; end; $$ language plpgsql; select f1(); f1 ---- (1 row) select * from plpgsql_check_function_tb('f1()'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+------------+----------+---------------------------+--------+------+-------+----------+------------+-------------------------------------------------- f1 | 5 | assignment | 42703 | column "a" does not exist | | | error | 6 | r := a + b | at assignment to variable "r" declared on line 2 (1 row) select f1(); f1 ---- (1 row) drop function f1(); create or replace function f1() returns void as $$ declare r int[]; begin if false then r[c+10] := 20; end if; end; $$ language plpgsql; select f1(); f1 ---- (1 row) select * from plpgsql_check_function_tb('f1()'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+------------+----------+---------------------------+--------+------+-------+----------+---------------+-------------------------------------------------- f1 | 5 | assignment | 42703 | column "c" does not exist | | | error | 3 | r[c+10] := 20 | at assignment to variable "r" declared on line 2 (1 row) select f1(); f1 ---- (1 row) drop function f1(); create or replace function f1() returns void as $$ declare r int; begin if false then r[10] := 20; end if; end; $$ language plpgsql set search_path = public; select f1(); f1 ---- (1 row) select * from plpgsql_check_function_tb('f1()'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+------------+----------+------------------------------------------------------------------------+--------+------+-------+----------+-------------+-------------------------------------------------- f1 | 5 | assignment | 42804 | cannot subscript type integer because it does not support subscripting | | | error | 1 | r[10] := 20 | at assignment to variable "r" declared on line 2 (1 row) select f1(); f1 ---- (1 row) drop function f1(); create or replace function f1_trg() returns trigger as $$ begin if new.a > 10 then raise notice '%', new.b; raise notice '%', new.c; end if; return new; end; $$ language plpgsql; create trigger t1_f1 before insert on t1 for each row execute procedure f1_trg(); insert into t1 values(6,30); select * from plpgsql_check_function_tb('f1_trg()','t1'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+-------------------------------+--------+------+-------+----------+-------+----------------------------- f1_trg | 5 | RAISE | 42703 | record "new" has no field "c" | | | error | | | PL/pgSQL expression "new.c" (1 row) insert into t1 values(6,30); create or replace function f1_trg() returns trigger as $$ begin new.a := new.a + 10; new.b := new.b + 10; new.c := 30; return new; end; $$ language plpgsql; -- should to fail select * from plpgsql_check_function_tb('f1_trg()','t1'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+------------+----------+-------------------------------+--------+------+-------+----------+-------+----------------------------------------------------------------- f1_trg | 5 | assignment | 42703 | record "new" has no field "c" | | | error | | | at assignment to field "c" of variable "new" declared on line 0 (1 row) -- should to fail but not crash insert into t1 values(6,30); ERROR: record "new" has no field "c" CONTEXT: PL/pgSQL assignment "new.c := 30" PL/pgSQL function f1_trg() line 5 at assignment create or replace function f1_trg() returns trigger as $$ begin new.a := new.a + 10; new.b := new.b + 10; return new; end; $$ language plpgsql; -- ok select * from plpgsql_check_function_tb('f1_trg()', 't1'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+---------+--------+------+-------+----------+-------+--------- (0 rows) -- ok insert into t1 values(6,30); create or replace function f1_trg() returns trigger as $$ begin new.a := new.a + 10; new.b := new.b + 10; return null; end; $$ language plpgsql; -- ok select * from plpgsql_check_function_tb('f1_trg()', 't1'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+---------+--------+------+-------+----------+-------+--------- (0 rows) insert into t1 values(60,300); select * from t1; a | b ----+---- 6 | 30 6 | 30 16 | 40 (3 rows) insert into t1 values(600,30); select * from t1; a | b ----+---- 6 | 30 6 | 30 16 | 40 (3 rows) drop trigger t1_f1 on t1; drop function f1_trg(); -- test of showing caret on correct place for multiline queries create or replace function f1() returns void as $$ begin select var from foo; end; $$ language plpgsql; select * from plpgsql_check_function_tb('f1()'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+---------------+----------+-------------------------------+--------+------+-------+----------+--------+--------- f1 | 3 | SQL statement | 42P01 | relation "foo" does not exist | | | error | 23 | select+| | | | | | | | | | var +| | | | | | | | | | from+| | | | | | | | | | foo | (1 row) drop function f1(); create or replace function f1() returns int as $$ begin return (select a from t1 where hh = 20); end; $$ language plpgsql; select * from plpgsql_check_function_tb('f1()'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+----------------------------+--------+------+-------+----------+----------------------------+--------- f1 | 3 | RETURN | 42703 | column "hh" does not exist | | | error | 50 | (select a +| | | | | | | | | | from t1 +| | | | | | | | | | where hh = 20) | (1 row) create or replace function f1() returns int as $$ begin return (select a from txxxxxxx where hh = 20); end; $$ language plpgsql; select * from plpgsql_check_function_tb('f1()'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+------------------------------------+--------+------+-------+----------+----------------------------+--------- f1 | 3 | RETURN | 42P01 | relation "txxxxxxx" does not exist | | | error | 29 | (select a +| | | | | | | | | | from txxxxxxx+| | | | | | | | | | where hh = 20) | (1 row) drop function f1(); drop table t1; -- raise warnings when target row has different number of attributies in -- SELECT INTO statement create or replace function f1() returns void as $$ declare a1 int; a2 int; begin select 10,20 into a1,a2; end; $$ language plpgsql; -- should be ok select * from plpgsql_check_function_tb('f1()'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+--------------------------+--------+------+---------------+----------+-------+--------- f1 | 2 | DECLARE | 00000 | never read variable "a1" | | | warning extra | | | f1 | 2 | DECLARE | 00000 | never read variable "a2" | | | warning extra | | | (2 rows) create or replace function f1() returns void as $$ declare a1 int; begin select 10,20 into a1; end; $$ language plpgsql; -- raise warning select * from plpgsql_check_function_tb('f1()'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+---------------+----------+------------------------------------------+---------------------------------------------------------------+-------------------------------------------------+---------------+----------+--------------+---------------------------------- f1 | 4 | SQL statement | 00000 | too many attributes for target variables | There are less target variables than output columns in query. | Check target variables in SELECT INTO statement | warning | 1 | select 10,20 | at SQL statement to a1 variables f1 | 2 | DECLARE | 00000 | never read variable "a1" | | | warning extra | | | (2 rows) create or replace function f1() returns void as $$ declare a1 int; a2 int; begin select 10 into a1,a2; end; $$ language plpgsql; -- raise warning select * from plpgsql_check_function_tb('f1()'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+---------------+----------+-----------------------------------------+---------------------------------------------------------------+--------------------------------------------------+---------------+----------+-----------+-------------------------------------- f1 | 4 | SQL statement | 00000 | too few attributes for target variables | There are more target variables than output columns in query. | Check target variables in SELECT INTO statement. | warning | 1 | select 10 | at SQL statement to a1, a2 variables f1 | 2 | DECLARE | 00000 | never read variable "a1" | | | warning extra | | | f1 | 2 | DECLARE | 00000 | never read variable "a2" | | | warning extra | | | (3 rows) -- bogus code set check_function_bodies to off; create or replace function f1() returns void as $$ adasdfsadf $$ language plpgsql; select * from plpgsql_check_function_tb('f1()'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+--------------------------------------+--------+------+-------+----------+------------+--------------------------------------------------- f1 | | | 42601 | syntax error at or near "adasdfsadf" | | | error | 2 | +| compilation of PL/pgSQL function "f1" near line 1 | | | | | | | | | adasdfsadf+| | | | | | | | | | | (1 row) drop function f1(); create table t1(a int, b int); create function g1(out a int, out b int) as $$ select 10,20; $$ language sql; create function f1() returns void as $$ declare r record; begin r := g1(); if false then raise notice '%', r.c; end if; end; $$ language plpgsql; select f1(); f1 ---- (1 row) select * from plpgsql_check_function('f1()'); plpgsql_check_function ------------------------------------------------- error:42703:6:RAISE:record "r" has no field "c" Context: PL/pgSQL expression "r.c" (2 rows) select f1(); f1 ---- (1 row) drop function f1(); drop function g1(); create function g1(out a int, out b int) returns setof record as $$ select * from t1; $$ language sql; create function f1() returns void as $$ declare r record; begin for r in select * from g1() loop raise notice '%', r.c; end loop; end; $$ language plpgsql; select f1(); f1 ---- (1 row) select * from plpgsql_check_function('f1()'); plpgsql_check_function ------------------------------------------------- error:42703:6:RAISE:record "r" has no field "c" Context: PL/pgSQL expression "r.c" (2 rows) select f1(); f1 ---- (1 row) create or replace function f1() returns void as $$ declare r record; begin for r in select * from g1() loop r.c := 20; end loop; end; $$ language plpgsql; select f1(); f1 ---- (1 row) select * from plpgsql_check_function('f1()'); plpgsql_check_function ------------------------------------------------------------------------ error:42703:6:assignment:record "r" has no field "c" Context: at assignment to field "c" of variable "r" declared on line 2 (2 rows) select f1(); f1 ---- (1 row) drop function f1(); drop function g1(); create function f1() returns int as $$ declare r int; begin if false then r := a + b; end if; return r; end; $$ language plpgsql; select f1(); f1 ---- (1 row) select * from plpgsql_check_function('f1()'); plpgsql_check_function ----------------------------------------------------------- error:42703:5:assignment:column "a" does not exist Query: r := a + b -- ^ Context: at assignment to variable "r" declared on line 2 (4 rows) select f1(); f1 ---- (1 row) drop function f1(); create or replace function f1() returns void as $$ declare r int[]; begin if false then r[c+10] := 20; end if; end; $$ language plpgsql; select f1(); f1 ---- (1 row) select * from plpgsql_check_function('f1()'); plpgsql_check_function ----------------------------------------------------------- error:42703:5:assignment:column "c" does not exist Query: r[c+10] := 20 -- ^ Context: at assignment to variable "r" declared on line 2 (4 rows) select f1(); f1 ---- (1 row) drop function f1(); create or replace function f1() returns void as $$ declare r int; begin if false then r[10] := 20; end if; end; $$ language plpgsql set search_path = public; select f1(); f1 ---- (1 row) select * from plpgsql_check_function('f1()'); plpgsql_check_function ------------------------------------------------------------------------------------------------- error:42804:5:assignment:cannot subscript type integer because it does not support subscripting Query: r[10] := 20 -- ^ Context: at assignment to variable "r" declared on line 2 (4 rows) select f1(); f1 ---- (1 row) drop function f1(); create or replace function f1_trg() returns trigger as $$ begin if new.a > 10 then raise notice '%', new.b; raise notice '%', new.c; end if; return new; end; $$ language plpgsql; create trigger t1_f1 before insert on t1 for each row execute procedure f1_trg(); insert into t1 values(6,30); select * from plpgsql_check_function('f1_trg()','t1'); plpgsql_check_function --------------------------------------------------- error:42703:5:RAISE:record "new" has no field "c" Context: PL/pgSQL expression "new.c" (2 rows) insert into t1 values(6,30); create or replace function f1_trg() returns trigger as $$ begin new.a := new.a + 10; new.b := new.b + 10; new.c := 30; return new; end; $$ language plpgsql; -- should to fail select * from plpgsql_check_function('f1_trg()','t1'); plpgsql_check_function -------------------------------------------------------------------------- error:42703:5:assignment:record "new" has no field "c" Context: at assignment to field "c" of variable "new" declared on line 0 (2 rows) -- should to fail but not crash insert into t1 values(6,30); ERROR: record "new" has no field "c" CONTEXT: PL/pgSQL assignment "new.c := 30" PL/pgSQL function f1_trg() line 5 at assignment create or replace function f1_trg() returns trigger as $$ begin new.a := new.a + 10; new.b := new.b + 10; return new; end; $$ language plpgsql; -- ok select * from plpgsql_check_function('f1_trg()', 't1'); plpgsql_check_function ------------------------ (0 rows) -- ok insert into t1 values(6,30); select * from t1; a | b ----+---- 6 | 30 6 | 30 16 | 40 (3 rows) drop trigger t1_f1 on t1; drop function f1_trg(); -- test of showing caret on correct place for multiline queries create or replace function f1() returns void as $$ begin select var from foo; end; $$ language plpgsql; select * from plpgsql_check_function('f1()'); plpgsql_check_function ----------------------------------------------------------- error:42P01:3:SQL statement:relation "foo" does not exist Query: select var from foo -- ^ (6 rows) drop function f1(); create or replace function f1() returns int as $$ begin return (select a from t1 where hh = 20); end; $$ language plpgsql; select * from plpgsql_check_function('f1()'); plpgsql_check_function ------------------------------------------------- error:42703:3:RETURN:column "hh" does not exist Query: (select a from t1 where hh = 20) -- ^ (5 rows) create or replace function f1() returns int as $$ begin return (select a from txxxxxxx where hh = 20); end; $$ language plpgsql; select * from plpgsql_check_function('f1()'); plpgsql_check_function --------------------------------------------------------- error:42P01:3:RETURN:relation "txxxxxxx" does not exist Query: (select a from txxxxxxx -- ^ where hh = 20) (5 rows) drop function f1(); drop table t1; -- raise warnings when target row has different number of attributies in -- SELECT INTO statement create or replace function f1() returns void as $$ declare a1 int; a2 int; begin select 10,20 into a1,a2; end; $$ language plpgsql; -- should be ok select * from plpgsql_check_function('f1()'); plpgsql_check_function -------------------------------------------------------- warning extra:00000:2:DECLARE:never read variable "a1" warning extra:00000:2:DECLARE:never read variable "a2" (2 rows) create or replace function f1() returns void as $$ declare a1 int; begin select 10,20 into a1; end; $$ language plpgsql; -- raise warning select * from plpgsql_check_function('f1()'); plpgsql_check_function ------------------------------------------------------------------------ warning:00000:4:SQL statement:too many attributes for target variables Query: select 10,20 -- ^ Detail: There are less target variables than output columns in query. Hint: Check target variables in SELECT INTO statement Context: at SQL statement to a1 variables warning extra:00000:2:DECLARE:never read variable "a1" (7 rows) create or replace function f1() returns void as $$ declare a1 int; a2 int; begin select 10 into a1,a2; end; $$ language plpgsql; -- raise warning select * from plpgsql_check_function('f1()'); plpgsql_check_function ----------------------------------------------------------------------- warning:00000:4:SQL statement:too few attributes for target variables Query: select 10 -- ^ Detail: There are more target variables than output columns in query. Hint: Check target variables in SELECT INTO statement. Context: at SQL statement to a1, a2 variables warning extra:00000:2:DECLARE:never read variable "a1" warning extra:00000:2:DECLARE:never read variable "a2" (8 rows) -- bogus code set check_function_bodies to off; create or replace function f1() returns void as $$ adasdfsadf $$ language plpgsql; select * from plpgsql_check_function('f1()'); plpgsql_check_function ------------------------------------------------------------ error:42601:syntax error at or near "adasdfsadf" Query: adasdfsadf -- ^ Context: compilation of PL/pgSQL function "f1" near line 1 (6 rows) drop function f1(); create table f1tbl(a int, b int); -- unused variables create or replace function f1(_input1 int) returns table(_output1 int, _output2 int) as $$ declare _f1 int; _f2 int; _f3 int; _f4 int; _f5 int; _r record; _tbl f1tbl; begin if true then _f1 := 1; end if; select 1, 2 into _f3, _f4; perform 1 where _f5 is null; select 1 into _r; select 1, 2 into _tbl; -- check that SQLSTATE and SQLERRM don't raise false positives begin exception when raise_exception then end; end $$ language plpgsql; select * from plpgsql_check_function('f1(int)'); plpgsql_check_function ---------------------------------------------------------- warning:00000:4:DECLARE:unused variable "_f2" warning extra:00000:3:DECLARE:never read variable "_f1" warning extra:00000:5:DECLARE:never read variable "_f3" warning extra:00000:6:DECLARE:never read variable "_f4" warning extra:00000:8:DECLARE:never read variable "_r" warning extra:00000:9:DECLARE:never read variable "_tbl" warning extra:00000:unused parameter "_input1" warning extra:00000:unmodified OUT variable "_output1" warning extra:00000:unmodified OUT variable "_output2" (9 rows) drop function f1(int); drop table f1tbl; -- check that NEW and OLD are not reported unused create table f1tbl(); create or replace function f1() returns trigger as $$ begin return null; end $$ language plpgsql; select * from plpgsql_check_function('f1()', 'f1tbl'); plpgsql_check_function ------------------------ (0 rows) drop function f1(); drop table f1tbl; create table tabret(a int, b int); insert into tabret values(10,10); create or replace function f1() returns int as $$ begin return (select a from tabret); end; $$ language plpgsql; select * from plpgsql_check_function('f1()', performance_warnings := true); plpgsql_check_function ----------------------------------------------------------------------------------------- performance:00000:routine is marked as VOLATILE, should be STABLE Hint: When you fix this issue, please, recheck other functions that uses this function. (2 rows) create or replace function f1() returns int as $$ begin return (select a::numeric from tabret); end; $$ language plpgsql; select * from plpgsql_check_function('f1()', performance_warnings := true); plpgsql_check_function ----------------------------------------------------------------------------------------- performance:42804:3:RETURN:target type is different type than source type Detail: cast "numeric" value to "integer" type Hint: Hidden casting can be a performance issue. Context: at RETURN performance:00000:routine is marked as VOLATILE, should be STABLE Hint: When you fix this issue, please, recheck other functions that uses this function. (6 rows) create or replace function f1() returns int as $$ begin return (select a, b from tabret); end; $$ language plpgsql; select * from plpgsql_check_function('f1()', performance_warnings := true); plpgsql_check_function ----------------------------------------------------------- error:42601:3:RETURN:subquery must return only one column Query: (select a, b from tabret) -- ^ (3 rows) drop function f1(); create or replace function f1() returns table(ax int, bx int) as $$ begin return query select * from tabret; return; end; $$ language plpgsql; select * from plpgsql_check_function('f1()', performance_warnings := true); plpgsql_check_function ----------------------------------------------------------------------------------------- performance:00000:routine is marked as VOLATILE, should be STABLE Hint: When you fix this issue, please, recheck other functions that uses this function. (2 rows) drop function f1(); create or replace function f1() returns table(ax numeric, bx numeric) as $$ begin return query select * from tabret; return; end; $$ language plpgsql; select * from plpgsql_check_function('f1()', performance_warnings := true); plpgsql_check_function ------------------------------------------------------------------------------------------------- error:42804:3:RETURN QUERY:structure of query does not match function result type Detail: Returned type integer does not match expected type numeric in column "ax" (position 1). (2 rows) drop function f1(); create or replace function f1() returns setof tabret as $$ begin return query select * from tabret; return; end; $$ language plpgsql; select * from plpgsql_check_function('f1()', performance_warnings := true); plpgsql_check_function ----------------------------------------------------------------------------------------- performance:00000:routine is marked as VOLATILE, should be STABLE Hint: When you fix this issue, please, recheck other functions that uses this function. (2 rows) create or replace function f1() returns setof tabret as $$ begin return query select a from tabret; return; end; $$ language plpgsql; select * from plpgsql_check_function('f1()', performance_warnings := true); plpgsql_check_function ----------------------------------------------------------------------------------- error:42804:3:RETURN QUERY:structure of query does not match function result type Detail: Number of returned columns (1) does not match expected column count (2). (2 rows) create or replace function f1() returns setof tabret as $$ begin return query select a::numeric,b::numeric from tabret; return; end; $$ language plpgsql; select * from plpgsql_check_function('f1()', performance_warnings := true); plpgsql_check_function ------------------------------------------------------------------------------------------------ error:42804:3:RETURN QUERY:structure of query does not match function result type Detail: Returned type numeric does not match expected type integer in column "a" (position 1). (2 rows) drop function f1(); create or replace function f1(a int) returns setof numeric as $$ begin return query select a; end $$ language plpgsql; select * from plpgsql_check_function('f1(int)', performance_warnings := true); plpgsql_check_function --------------------------------------------------------------------------------------------------------- error:42804:2:RETURN QUERY:structure of query does not match function result type Detail: Returned type integer does not match expected type numeric in column "__result__" (position 1). (2 rows) drop function f1(int); drop table tabret; create or replace function f1() returns void as $$ declare intval integer; begin intval := null; -- ok intval := 1; -- OK intval := '1'; -- OK intval := text '1'; -- not OK intval := current_date; -- not OK select 1 into intval; -- OK select '1' into intval; -- OK select text '1' into intval; -- not OK end $$ language plpgsql; select * from plpgsql_check_function('f1()', performance_warnings := true); plpgsql_check_function -------------------------------------------------------------------------------------- warning:42804:9:assignment:target type is different type than source type Query: intval := current_date -- ^ Detail: cast "date" value to "integer" type Hint: There are no possible explicit coercion between those types, possibly bug! Context: at assignment to variable "intval" declared on line 3 performance:00000:11:SQL statement:detected "SELECT expr INTO variable" Query: select 1 -- ^ Detail: This obsolete syntax of assigning can be slow. Hint: Use syntax "variable := expr" instead. performance:00000:12:SQL statement:detected "SELECT expr INTO variable" Query: select '1' -- ^ Detail: This obsolete syntax of assigning can be slow. Hint: Use syntax "variable := expr" instead. warning:42804:12:SQL statement:target type is different type than source type Query: select '1' -- ^ Detail: cast "text" value to "integer" type Hint: The input expression type does not have an assignment cast to the target type. Context: at SQL statement to variable "intval" declared on line 3 performance:00000:13:SQL statement:detected "SELECT expr INTO variable" Query: select text '1' -- ^ Detail: This obsolete syntax of assigning can be slow. Hint: Use syntax "variable := expr" instead. warning:42804:13:SQL statement:target type is different type than source type Query: select text '1' -- ^ Detail: cast "text" value to "integer" type Hint: The input expression type does not have an assignment cast to the target type. Context: at SQL statement to variable "intval" declared on line 3 warning extra:00000:3:DECLARE:never read variable "intval" (34 rows) drop function f1(); create or replace function f1() returns int as $$ begin return 1; end; $$ language plpgsql; select * from plpgsql_check_function('f1()', performance_warnings := true); plpgsql_check_function ----------------------------------------------------------------------------------------- performance:00000:routine is marked as VOLATILE, should be IMMUTABLE Hint: When you fix this issue, please, recheck other functions that uses this function. (2 rows) create or replace function f1() returns int as $$ begin return 1::numeric; end; $$ language plpgsql; select * from plpgsql_check_function('f1()', performance_warnings := true); plpgsql_check_function ----------------------------------------------------------------------------------------- performance:42804:3:RETURN:target type is different type than source type Detail: cast "numeric" value to "integer" type Hint: Hidden casting can be a performance issue. Context: at RETURN performance:00000:routine is marked as VOLATILE, should be IMMUTABLE Hint: When you fix this issue, please, recheck other functions that uses this function. (6 rows) create or replace function f1() returns int as $$ begin return null; end; $$ language plpgsql; select * from plpgsql_check_function('f1()', performance_warnings := true); plpgsql_check_function ----------------------------------------------------------------------------------------- performance:00000:routine is marked as VOLATILE, should be IMMUTABLE Hint: When you fix this issue, please, recheck other functions that uses this function. (2 rows) create or replace function f1() returns int as $$ begin return current_date; end; $$ language plpgsql; select * from plpgsql_check_function('f1()', performance_warnings := true); plpgsql_check_function ----------------------------------------------------------------------------------------- warning:42804:3:RETURN:target type is different type than source type Detail: cast "date" value to "integer" type Hint: There are no possible explicit coercion between those types, possibly bug! Context: at RETURN performance:00000:routine is marked as VOLATILE, should be STABLE Hint: When you fix this issue, please, recheck other functions that uses this function. (6 rows) create or replace function f1() returns int as $$ declare a int; begin return a; end; $$ language plpgsql; select * from plpgsql_check_function('f1()', performance_warnings := true); plpgsql_check_function ----------------------------------------------------------------------------------------- performance:00000:routine is marked as VOLATILE, should be IMMUTABLE Hint: When you fix this issue, please, recheck other functions that uses this function. (2 rows) create or replace function f1() returns int as $$ declare a numeric; begin return a; end; $$ language plpgsql; select * from plpgsql_check_function('f1()', performance_warnings := true); plpgsql_check_function ----------------------------------------------------------------------------------------- performance:42804:4:RETURN:target type is different type than source type Detail: cast "numeric" value to "integer" type Hint: Hidden casting can be a performance issue. Context: at RETURN of variable "a" declared on line 2 performance:00000:routine is marked as VOLATILE, should be IMMUTABLE Hint: When you fix this issue, please, recheck other functions that uses this function. (6 rows) drop function f1(); create or replace function f1() returns setof int as $$ begin return next 1; end; $$ language plpgsql; select * from plpgsql_check_function('f1()', performance_warnings := true); plpgsql_check_function ----------------------------------------------------------------------------------------- performance:00000:routine is marked as VOLATILE, should be IMMUTABLE Hint: When you fix this issue, please, recheck other functions that uses this function. (2 rows) create or replace function f1() returns setof int as $$ begin return next 1::numeric; -- tolerant, doesn't use tupmap end; $$ language plpgsql; select * from plpgsql_check_function('f1()', performance_warnings := true); plpgsql_check_function ----------------------------------------------------------------------------------------- performance:42804:3:RETURN NEXT:target type is different type than source type Detail: cast "numeric" value to "integer" type Hint: Hidden casting can be a performance issue. Context: at RETURN NEXT performance:00000:routine is marked as VOLATILE, should be IMMUTABLE Hint: When you fix this issue, please, recheck other functions that uses this function. (6 rows) drop function f1(); create type t1 as (a int, b int, c int); create type t2 as (a int, b numeric); create or replace function fx() returns t2 as $$ declare x t1; begin return x; end; $$ language plpgsql; select * from plpgsql_check_function('fx()', performance_warnings := true); plpgsql_check_function ------------------------------------------------------------------------------------------------ error:42804:4:RETURN:returned record type does not match expected record type Detail: Returned type integer does not match expected type numeric in column "b" (position 2). (2 rows) create or replace function fx() returns t2 as $$ declare x t2; begin return x; end; $$ language plpgsql; select * from plpgsql_check_function('fx()', performance_warnings := true); plpgsql_check_function ----------------------------------------------------------------------------------------- performance:00000:routine is marked as VOLATILE, should be IMMUTABLE Hint: When you fix this issue, please, recheck other functions that uses this function. (2 rows) drop function fx(); create or replace function fx() returns setof t2 as $$ declare x t1; begin return next x; return; end; $$ language plpgsql; select * from plpgsql_check_function('fx()', performance_warnings := true); plpgsql_check_function ------------------------------------------------------------------------------------------------ error:42804:4:RETURN NEXT:wrong record type supplied in RETURN NEXT Detail: Returned type integer does not match expected type numeric in column "b" (position 2). (2 rows) create or replace function fx() returns setof t2 as $$ declare x t2; begin return next x; return; end; $$ language plpgsql; select * from plpgsql_check_function('fx()', performance_warnings := true); plpgsql_check_function ----------------------------------------------------------------------------------------- performance:00000:routine is marked as VOLATILE, should be IMMUTABLE Hint: When you fix this issue, please, recheck other functions that uses this function. (2 rows) drop function fx(); create or replace function fx2(_id int, _pa_id varchar(32), _status varchar(60)) returns void as $$ declare begin insert into pa values(_id, _pa_id, _status); exception when OTHERS then raise notice '%', 'some message'; raise exception '%', sqlerrm; end $$ language plpgsql; select * from plpgsql_check_function('fx2(int, varchar, varchar)', performance_warnings := true); plpgsql_check_function ------------------------ (0 rows) create or replace function fx2(_id int, _pa_id varchar(32), _status varchar(60)) returns void as $$ declare begin insert into pa values(_id, _pa_id, _status) returning *; exception when OTHERS then raise notice '%', 'some message'; raise exception '%', sqlerrm; end $$ language plpgsql; select * from plpgsql_check_function('fx2(int, varchar, varchar)', performance_warnings := true); plpgsql_check_function ---------------------------------------------------------------------- error:42601:4:SQL statement:query has no destination for result data (1 row) create or replace function fx2(_id int, _pa_id varchar(32), _status varchar(60)) returns void as $$ declare begin SELECT * FROM pa LIMIT 1; exception when OTHERS then raise notice '%', 'some message'; raise exception '%', sqlerrm; end $$ language plpgsql; select * from plpgsql_check_function('fx2(int, varchar, varchar)', performance_warnings := true); plpgsql_check_function ---------------------------------------------------------------------- error:42601:4:SQL statement:query has no destination for result data (1 row) drop function fx2(int, varchar, varchar); create or replace function foreach_array_loop() returns void as $body$ declare arr text[]; el text; begin arr := array['1111','2222','3333']; foreach el in array arr loop raise notice '%', el; end loop; end; $body$ language 'plpgsql' stable; select * from plpgsql_check_function_tb('foreach_array_loop()', performance_warnings := true); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context --------------------+--------+-----------+----------+--------------------------------------------------+--------+-----------------------------------------------------------------------------------+-------------+----------+-------+--------- foreach_array_loop | | | 00000 | routine is marked as STABLE, should be IMMUTABLE | | When you fix this issue, please, recheck other functions that uses this function. | performance | | | (1 row) create or replace function foreach_array_loop() returns void as $body$ declare arr text[]; el int; begin arr := array['1111','2222','3333']; foreach el in array arr loop raise notice '%', el; end loop; end; $body$ language 'plpgsql' stable; select * from plpgsql_check_function_tb('foreach_array_loop()', performance_warnings := true); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context --------------------+--------+--------------------+----------+--------------------------------------------------+-------------------------------------+-----------------------------------------------------------------------------------+-------------+----------+-------+----------------------- foreach_array_loop | 7 | FOREACH over array | 42804 | target type is different type than source type | cast "text" value to "integer" type | The input expression type does not have an assignment cast to the target type. | warning | | | at FOREACH over array foreach_array_loop | | | 00000 | routine is marked as STABLE, should be IMMUTABLE | | When you fix this issue, please, recheck other functions that uses this function. | performance | | | (2 rows) create or replace function foreach_array_loop() returns void as $body$ declare arr date[]; el int; begin -- expression is not constant (else this check fails due tracking constant) arr := array['2014-01-01','2015-01-01','2016-01-01', current_date]::date[]; foreach el in array arr loop raise notice '%', el; end loop; end; $body$ language 'plpgsql' stable; select * from plpgsql_check_function_tb('foreach_array_loop()', performance_warnings := true); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context --------------------+--------+--------------------+----------+------------------------------------------------+-------------------------------------+----------------------------------------------------------------------------+---------+----------+-------+----------------------- foreach_array_loop | 8 | FOREACH over array | 42804 | target type is different type than source type | cast "date" value to "integer" type | There are no possible explicit coercion between those types, possibly bug! | warning | | | at FOREACH over array (1 row) create or replace function foreach_array_loop() returns void as $body$ declare arr date[]; el int; begin arr := array['2014-01-01','2015-01-01','2016-01-01']::date[]; foreach el in array arr loop raise notice '%', el; end loop; end; $body$ language 'plpgsql' stable; select * from plpgsql_check_function_tb('foreach_array_loop()', performance_warnings := true); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context --------------------+--------+--------------------+----------+-----------------------------------------------------+--------+------+-------+----------+-------+--------- foreach_array_loop | 7 | FOREACH over array | 22P02 | invalid input syntax for type integer: "01-01-2014" | | | error | | | (1 row) create or replace function foreach_array_loop() returns void as $body$ declare el text; begin foreach el in array array['1111','2222','3333'] loop raise notice '%', el; end loop; end; $body$ language 'plpgsql' stable; select * from plpgsql_check_function_tb('foreach_array_loop()', performance_warnings := true); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context --------------------+--------+-----------+----------+--------------------------------------------------+--------+-----------------------------------------------------------------------------------+-------------+----------+-------+--------- foreach_array_loop | | | 00000 | routine is marked as STABLE, should be IMMUTABLE | | When you fix this issue, please, recheck other functions that uses this function. | performance | | | (1 row) create or replace function foreach_array_loop() returns void as $body$ declare el int; begin foreach el in array array['1111','2222','3333'] loop raise notice '%', el; end loop; end; $body$ language 'plpgsql' stable; select * from plpgsql_check_function_tb('foreach_array_loop()', performance_warnings := true); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context --------------------+--------+--------------------+----------+--------------------------------------------------+-------------------------------------+-----------------------------------------------------------------------------------+-------------+----------+-------+----------------------- foreach_array_loop | 5 | FOREACH over array | 42804 | target type is different type than source type | cast "text" value to "integer" type | The input expression type does not have an assignment cast to the target type. | warning | | | at FOREACH over array foreach_array_loop | | | 00000 | routine is marked as STABLE, should be IMMUTABLE | | When you fix this issue, please, recheck other functions that uses this function. | performance | | | (2 rows) create or replace function foreach_array_loop() returns void as $body$ declare el int; begin foreach el in array array['2014-01-01','2015-01-01','2016-01-01']::date[] loop raise notice '%', el; end loop; end; $body$ language 'plpgsql' stable; select * from plpgsql_check_function_tb('foreach_array_loop()', performance_warnings := true); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context --------------------+--------+--------------------+----------+-----------------------------------------------------+--------+------+-------+----------+-------+--------- foreach_array_loop | 5 | FOREACH over array | 22P02 | invalid input syntax for type integer: "01-01-2014" | | | error | | | (1 row) drop function foreach_array_loop(); create or replace function scan_rows(int[]) returns void AS $$ declare x int[]; begin foreach x slice 1 in array $1 loop raise notice 'row = %', x; end loop; end; $$ language plpgsql; select * from plpgsql_check_function_tb('scan_rows(int[])', performance_warnings := true); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+----------------------------------------------------+--------+-----------------------------------------------------------------------------------+-------------+----------+-------+--------- scan_rows | | | 00000 | routine is marked as VOLATILE, should be IMMUTABLE | | When you fix this issue, please, recheck other functions that uses this function. | performance | | | (1 row) create or replace function scan_rows(int[]) returns void AS $$ declare x int[]; begin foreach x in array $1 loop raise notice 'row = %', x; end loop; end; $$ language plpgsql; select * from plpgsql_check_function_tb('scan_rows(int[])', performance_warnings := true); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+--------------------+----------+----------------------------------------------------+------------------------------------------+-----------------------------------------------------------------------------------+-------------+----------+-------+----------------------- scan_rows | 5 | FOREACH over array | 42804 | target type is different type than source type | cast "integer" value to "integer[]" type | There are no possible explicit coercion between those types, possibly bug! | warning | | | at FOREACH over array scan_rows | | | 00000 | routine is marked as VOLATILE, should be IMMUTABLE | | When you fix this issue, please, recheck other functions that uses this function. | performance | | | (2 rows) drop function scan_rows(int[]); drop type t1; drop type t2; create table t1(a int, b int); create table t2(a int, b int, c int); create table t3(a numeric, b int); insert into t1 values(10,20),(30,40); create or replace function fx() returns int as $$ declare s int default 0; r t1; begin foreach r in array (select array_agg(t1) from t1) loop s := r.a + r.b; end loop; return s; end; $$ language plpgsql; select * from plpgsql_check_function_tb('fx()', performance_warnings := true); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+-------------------------------------------------+--------+-----------------------------------------------------------------------------------+-------------+----------+-------+--------- fx | | | 00000 | routine is marked as VOLATILE, should be STABLE | | When you fix this issue, please, recheck other functions that uses this function. | performance | | | (1 row) create or replace function fx() returns int as $$ declare s int default 0; r t1; c t1[]; begin c := (select array_agg(t1) from t1); foreach r in array c loop s := r.a + r.b; end loop; return s; end; $$ language plpgsql; select * from plpgsql_check_function_tb('fx()', performance_warnings := true); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+-------------------------------------------------+--------+-----------------------------------------------------------------------------------+-------------+----------+-------+--------- fx | | | 00000 | routine is marked as VOLATILE, should be STABLE | | When you fix this issue, please, recheck other functions that uses this function. | performance | | | (1 row) create or replace function fx() returns int as $$ declare s int default 0; r t1; c t1[]; begin select array_agg(t1) into c from t1; foreach r in array c loop s := r.a + r.b; end loop; return s; end; $$ language plpgsql; select * from plpgsql_check_function_tb('fx()', performance_warnings := true); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+-------------------------------------------------+--------+-----------------------------------------------------------------------------------+-------------+----------+-------+--------- fx | | | 00000 | routine is marked as VOLATILE, should be STABLE | | When you fix this issue, please, recheck other functions that uses this function. | performance | | | (1 row) create or replace function fx() returns int as $$ declare s int default 0; r t1; c t1[]; begin select array_agg(t1) into c from t1; for i in array_lower(c, 1) .. array_upper(c, 1) loop r := c[i]; s := r.a + r.b; end loop; return s; end; $$ language plpgsql; select * from plpgsql_check_function_tb('fx()', performance_warnings := true); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+-------------------------------------------------+--------+-----------------------------------------------------------------------------------+-------------+----------+-------+--------- fx | | | 00000 | routine is marked as VOLATILE, should be STABLE | | When you fix this issue, please, recheck other functions that uses this function. | performance | | | (1 row) create or replace function fx() returns int as $$ declare s int default 0; c t1[]; begin select array_agg(t1) into c from t1; for i in array_lower(c, 1) .. array_upper(c, 1) loop s := (c[i]).a + (c[i]).b; end loop; return s; end; $$ language plpgsql; select * from plpgsql_check_function_tb('fx()', performance_warnings := true); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+-------------------------------------------------+--------+-----------------------------------------------------------------------------------+-------------+----------+-------+--------- fx | | | 00000 | routine is marked as VOLATILE, should be STABLE | | When you fix this issue, please, recheck other functions that uses this function. | performance | | | (1 row) create or replace function fx() returns int as $$ declare s int default 0; r record; c t1[]; begin select array_agg(t1) into c from t1; for i in array_lower(c, 1) .. array_upper(c, 1) loop r := c[i]; s := r.a + r.b; end loop; return s; end; $$ language plpgsql; select * from plpgsql_check_function_tb('fx()', performance_warnings := true); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+-------------------------------------------------+--------+-----------------------------------------------------------------------------------+-------------+----------+-------+--------- fx | | | 00000 | routine is marked as VOLATILE, should be STABLE | | When you fix this issue, please, recheck other functions that uses this function. | performance | | | (1 row) create or replace function fx() returns int as $$ declare s int default 0; r record; c t1[]; begin select array_agg(t1) into c from t1; for i in array_lower(c, 1) .. array_upper(c, 1) loop r := c[i]; s := r.a + r.b + r.c; end loop; return s; end; $$ language plpgsql; select * from plpgsql_check_function_tb('fx()', performance_warnings := true); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+------------+----------+-----------------------------+--------+------+-------+----------+-------+-------------------------------------------- fx | 11 | assignment | 42703 | record "r" has no field "c" | | | error | | | PL/pgSQL assignment "s := r.a + r.b + r.c" (1 row) create or replace function fx() returns int as $$ declare s int default 0; r t2; begin foreach r in array (select array_agg(t1) from t1) loop s := r.a + r.b; end loop; return s; end; $$ language plpgsql; select * from plpgsql_check_function_tb('fx()', performance_warnings := true); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+--------------------+----------+-------------------------------------------------+--------+-----------------------------------------------------------------------------------+-------------+----------+-------+----------------------- fx | 6 | FOREACH over array | 00000 | too few attributes for composite variable | | | warning | | | at FOREACH over array fx | | | 00000 | routine is marked as VOLATILE, should be STABLE | | When you fix this issue, please, recheck other functions that uses this function. | performance | | | (2 rows) create or replace function fx() returns int as $$ declare s int default 0; r t3; begin foreach r in array (select array_agg(t1) from t1) loop s := r.a + r.b; end loop; return s; end; $$ language plpgsql; select * from plpgsql_check_function_tb('fx()', performance_warnings := true); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+--------------------+----------+-------------------------------------------------+----------------------------------------+-----------------------------------------------------------------------------------+-------------+----------+----------------+-------------------------------------------------- fx | 6 | FOREACH over array | 42804 | target type is different type than source type | cast "integer" value to "numeric" type | Hidden casting can be a performance issue. | performance | | | at FOREACH over array fx | 8 | assignment | 42804 | target type is different type than source type | cast "numeric" value to "integer" type | Hidden casting can be a performance issue. | performance | 1 | s := r.a + r.b | at assignment to variable "s" declared on line 3 fx | | | 00000 | routine is marked as VOLATILE, should be STABLE | | When you fix this issue, please, recheck other functions that uses this function. | performance | | | (3 rows) drop function fx(); drop table t1; -- mscottie issue #13 create table test ( a text, b integer, c uuid ); create function before_insert_test() returns trigger language plpgsql as $$ begin select a into NEW.a from test where b = 1; select b into NEW.b from test where b = 1; select null::uuid into NEW.c from test where b = 1; return new; end; $$; select * from plpgsql_check_function_tb('before_insert_test()','test'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+---------+--------+------+-------+----------+-------+--------- (0 rows) create or replace function before_insert_test() returns trigger language plpgsql as $$ begin NEW.a := (select a from test where b = 1); NEW.b := (select b from test where b = 1); NEW.c := (select c from test where b = 1); return new; end; $$; select * from plpgsql_check_function_tb('before_insert_test()','test', fatal_errors := false); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+---------+--------+------+-------+----------+-------+--------- (0 rows) create or replace function before_insert_test() returns trigger language plpgsql as $$ begin NEW.a := 'Hello'::text; NEW.b := 10; NEW.c := null::uuid; return new; end; $$; select * from plpgsql_check_function_tb('before_insert_test()','test', fatal_errors := false); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+---------+--------+------+-------+----------+-------+--------- (0 rows) drop function before_insert_test(); create or replace function fx() returns void as $$ declare NEW test; OLD test; begin select null::uuid into NEW.c from test where b = 1; end; $$ language plpgsql; select * from plpgsql_check_function_tb('fx()', performance_warnings := true, fatal_errors := false); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+---------------------------+--------+------+---------------+----------+-------+--------- fx | 2 | DECLARE | 00000 | unused variable "old" | | | warning | | | fx | 2 | DECLARE | 00000 | never read variable "new" | | | warning extra | | | (2 rows) drop function fx(); create or replace function fx() returns void as $$ declare NEW test; begin NEW.a := 'Hello'::text; NEW.b := 10; NEW.c := null::uuid; end; $$ language plpgsql; select * from plpgsql_check_function_tb('fx()', performance_warnings := true, fatal_errors := false); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+----------------------------------------------------+--------+-----------------------------------------------------------------------------------+---------------+----------+-------+--------- fx | 2 | DECLARE | 00000 | never read variable "new" | | | warning extra | | | fx | | | 00000 | routine is marked as VOLATILE, should be IMMUTABLE | | When you fix this issue, please, recheck other functions that uses this function. | performance | | | (2 rows) drop function fx(); drop table test; create or replace function fx() returns void as $$ declare s int; sa int[]; sd date; bs int[]; begin sa[10] := s; sa[10] := sd; s := bs[10]; end; $$ language plpgsql; select * from plpgsql_check_function_tb('fx()', performance_warnings := true, fatal_errors := false); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+------------+----------+------------------------------------------------+-------------------------------------+----------------------------------------------------------------------------+---------------+----------+--------------+--------------------------------------------------- fx | 9 | assignment | 42804 | target type is different type than source type | cast "date" value to "integer" type | There are no possible explicit coercion between those types, possibly bug! | warning | 1 | sa[10] := sd | at assignment to variable "sa" declared on line 4 fx | 4 | DECLARE | 00000 | never read variable "sa" | | | warning extra | | | (2 rows) drop function fx(); create type t as (t text); create or replace function fx() returns void as $$ declare _t t; _tt t[]; _txt text; begin _t.t := 'ABC'; -- correct warning "unknown" _tt[1] := _t; _txt := _t; end; $$ language plpgsql; select * from plpgsql_check_function('fx()', performance_warnings := true); plpgsql_check_function ------------------------------------------------------------------------------- performance:42804:7:assignment:target type is different type than source type Query: _txt := _t -- ^ Detail: cast "t" value to "text" type Hint: Hidden casting can be a performance issue. Context: at assignment to variable "_txt" declared on line 3 warning extra:00000:2:DECLARE:never read variable "_tt" warning extra:00000:3:DECLARE:never read variable "_txt" (8 rows) drop function fx(); create or replace function fx() returns void as $$ declare _t1 t; _t2 t; begin _t1.t := 'ABC'::text; _t2 := _t1; raise notice '% %', _t2, _t2.t; end; $$ language plpgsql; select * from plpgsql_check_function('fx()', performance_warnings := true); plpgsql_check_function ----------------------------------------------------------------------------------------- performance:00000:routine is marked as VOLATILE, should be IMMUTABLE Hint: When you fix this issue, please, recheck other functions that uses this function. (2 rows) drop function fx(); create or replace function fx(out _tt t[]) as $$ declare _t t; begin _t.t := 'ABC'::text; _tt[1] := _t; end; $$ language plpgsql; select * from plpgsql_check_function('fx()', performance_warnings := true); plpgsql_check_function ----------------------------------------------------------------------------------------- performance:00000:routine is marked as VOLATILE, should be IMMUTABLE Hint: When you fix this issue, please, recheck other functions that uses this function. (2 rows) drop function fx(); drop type t; create or replace function fx() returns int as $$ declare x int; begin perform 1; return 10; end; $$ language plpgsql; select * from plpgsql_check_function('fx()', performance_warnings := true); plpgsql_check_function ----------------------------------------------------------------------------------------- warning:00000:2:DECLARE:unused variable "x" performance:00000:routine is marked as VOLATILE, should be IMMUTABLE Hint: When you fix this issue, please, recheck other functions that uses this function. (3 rows) drop function fx(); create table t(i int); create function test_t(OUT t) returns t AS $$ begin $1 := null; end; $$ language plpgsql; select test_t(); test_t -------- (1 row) select * from test_t(); i --- (1 row) select * from plpgsql_check_function('test_t()', performance_warnings := true); plpgsql_check_function ----------------------------------------------------------------------------------------- performance:00000:routine is marked as VOLATILE, should be IMMUTABLE Hint: When you fix this issue, please, recheck other functions that uses this function. (2 rows) create or replace function fx() returns void as $$ declare c cursor for select * from t; x varchar; begin open c; fetch c into x; close c; end; $$ language plpgsql; select test_t(); test_t -------- (1 row) select * from test_t(); i --- (1 row) select * from plpgsql_check_function('fx()', performance_warnings := true, fatal_errors := false); plpgsql_check_function -------------------------------------------------------------------------- performance:42804:7:FETCH:target type is different type than source type Detail: cast "integer" value to "character varying" type Hint: Hidden casting can be a performance issue. Context: at FETCH to variable "x" declared on line 4 warning extra:00000:4:DECLARE:never read variable "x" (5 rows) drop function fx(); create or replace function fx() returns void as $$ declare c cursor for select * from t; x int; begin open c; fetch c into x; close c; end; $$ language plpgsql; select test_t(); test_t -------- (1 row) select * from test_t(); i --- (1 row) select * from plpgsql_check_function('fx()', performance_warnings := true, fatal_errors := false); plpgsql_check_function ------------------------------------------------------- warning extra:00000:4:DECLARE:never read variable "x" (1 row) drop function fx(); create or replace function fx() returns void as $$ declare c cursor for select * from t; begin for r in c loop raise notice '%', r.a; end loop; end; $$ language plpgsql; select test_t(); test_t -------- (1 row) select * from test_t(); i --- (1 row) select * from plpgsql_check_function('fx()', performance_warnings := true, fatal_errors := false); plpgsql_check_function ------------------------------------------------------- error:42703:6:RAISE:record "r" has no field "a" Context: PL/pgSQL expression "r.a" warning extra:00000:5:DECLARE:never read variable "r" (3 rows) drop function fx(); create or replace function fx() returns void as $$ declare c cursor for select * from t; begin for r in c loop raise notice '%', r.i; end loop; end; $$ language plpgsql; select test_t(); test_t -------- (1 row) select * from test_t(); i --- (1 row) select * from plpgsql_check_function('fx()', performance_warnings := true, fatal_errors := false); plpgsql_check_function ------------------------ (0 rows) drop function fx(); create table foo(a int, b int); create or replace function fx() returns void as $$ declare f1 int; f2 int; begin select 1, 2 into f1; select 1 into f1, f2; select a b into f1, f2 from foo; end; $$ language plpgsql; select fx(); fx ---- (1 row) select * from plpgsql_check_function('fx()', performance_warnings := true, fatal_errors := false); plpgsql_check_function ------------------------------------------------------------------------ warning:00000:4:SQL statement:too many attributes for target variables Query: select 1, 2 -- ^ Detail: There are less target variables than output columns in query. Hint: Check target variables in SELECT INTO statement Context: at SQL statement to f1 variables warning:00000:5:SQL statement:too few attributes for target variables Query: select 1 -- ^ Detail: There are more target variables than output columns in query. Hint: Check target variables in SELECT INTO statement. Context: at SQL statement to f1, f2 variables warning:00000:6:SQL statement:too few attributes for target variables Query: select a b from foo -- ^ Detail: There are more target variables than output columns in query. Hint: Check target variables in SELECT INTO statement. Context: at SQL statement to f1, f2 variables warning extra:00000:2:DECLARE:never read variable "f1" warning extra:00000:2:DECLARE:never read variable "f2" (20 rows) drop function fx(); drop table foo; create or replace function fx() returns void as $$ declare d date; begin d := (select 1 from pg_class limit 1); raise notice '%', d; end; $$ language plpgsql; select fx(); ERROR: invalid input syntax for type date: "1" CONTEXT: PL/pgSQL assignment "d := (select 1 from pg_class limit 1)" PL/pgSQL function fx() line 4 at assignment select * from plpgsql_check_function('fx()', performance_warnings := true, fatal_errors := false); plpgsql_check_function ---------------------------------------------------------------------------------- warning:42804:4:assignment:target type is different type than source type Query: d := (select 1 from pg_class limit 1) -- ^ Detail: cast "integer" value to "date" type Hint: There are no possible explicit coercion between those types, possibly bug! Context: at assignment to variable "d" declared on line 2 (6 rows) drop function fx(); create table tab_1(i int); create or replace function fx(a int) returns setof int as $$ declare c refcursor; r record; begin open c for select i from tab_1 where i = a; loop fetch c into r; if not found then exit; end if; return next r.i; end loop; end; $$ language plpgsql; select * from plpgsql_check_function('fx(int)', performance_warnings := true, fatal_errors := false); plpgsql_check_function ----------------------------------------------------------------------------------------- performance:00000:routine is marked as VOLATILE, should be STABLE Hint: When you fix this issue, please, recheck other functions that uses this function. (2 rows) create or replace function fx(a int) returns setof int as $$ declare c refcursor; r record; begin open c for select i from tab_1 where i = a; loop fetch c into r; if not found then exit; end if; return next r.x; end loop; end; $$ language plpgsql; select * from plpgsql_check_function('fx(int)', performance_warnings := true, fatal_errors := false); plpgsql_check_function ----------------------------------------------------------------------------------------- error:42703:12:RETURN NEXT:record "r" has no field "x" Context: PL/pgSQL expression "r.x" warning extra:00000:4:DECLARE:never read variable "r" performance:00000:routine is marked as VOLATILE, should be STABLE Hint: When you fix this issue, please, recheck other functions that uses this function. (5 rows) drop function fx(int); drop table tab_1; create or replace function fxx() returns void as $$ begin rollback; end; $$ language plpgsql; select fxx(); ERROR: invalid transaction termination CONTEXT: PL/pgSQL function fxx() line 3 at ROLLBACK select * from plpgsql_check_function('fxx()'); plpgsql_check_function -------------------------------------------------------- error:2D000:3:ROLLBACK:invalid transaction termination (1 row) drop function fxx(); create or replace function fxx() returns void as $$ declare x int; begin declare x int; begin end; end; $$ language plpgsql; select * from plpgsql_check_function('fxx()'); plpgsql_check_function ------------------------------------------------------------------------------------------ warning extra:00000:5:statement block:variable "x" shadows a previously defined variable Hint: SET plpgsql.extra_warnings TO 'shadowed_variables' warning:00000:2:DECLARE:unused variable "x" warning:00000:4:DECLARE:unused variable "x" (4 rows) select * from plpgsql_check_function('fxx()', extra_warnings := false); plpgsql_check_function --------------------------------------------- warning:00000:2:DECLARE:unused variable "x" warning:00000:4:DECLARE:unused variable "x" (2 rows) drop function fxx(); create or replace function fxx(in a int, in b int, out c int, out d int) as $$ begin c := a; end; $$ language plpgsql; select * from plpgsql_check_function('fxx(int, int)'); plpgsql_check_function ------------------------------------------------- warning extra:00000:unused parameter "b" warning extra:00000:unmodified OUT variable "d" (2 rows) create or replace function fxx(in a int, in b int, out c int, out d int) as $$ begin c := d; end; $$ language plpgsql; select * from plpgsql_check_function('fxx(int, int)'); plpgsql_check_function ------------------------------------------------- warning extra:00000:unused parameter "a" warning extra:00000:unused parameter "b" warning extra:00000:unmodified OUT variable "d" (3 rows) create type ct as (a int, b int); create or replace function fxx(a ct, b ct, OUT c ct, OUT d ct) as $$ begin c.a := a.a; end; $$ language plpgsql; select * from plpgsql_check_function('fxx(ct, ct)'); plpgsql_check_function ----------------------------------------------------------------------- warning extra:00000:unused parameter "b" warning extra:00000:composite OUT variable "c" is not single argument warning extra:00000:composite OUT variable "d" is not single argument warning extra:00000:unmodified OUT variable "d" (4 rows) create or replace function fxx(a ct, b ct, OUT c ct, OUT d ct) as $$ begin c.a := d.a; end; $$ language plpgsql; select * from plpgsql_check_function('fxx(ct, ct)'); plpgsql_check_function ----------------------------------------------------------------------- warning extra:00000:unused parameter "a" warning extra:00000:unused parameter "b" warning extra:00000:composite OUT variable "c" is not single argument warning extra:00000:composite OUT variable "d" is not single argument warning extra:00000:unmodified OUT variable "d" (5 rows) create or replace function tx(a int) returns int as $$ declare a int; ax int; begin declare ax int; begin ax := 10; end; a := 10; return 20; end; $$ language plpgsql; select * from plpgsql_check_function('tx(int)'); plpgsql_check_function ------------------------------------------------------------------------------------------- warning:00000:3:statement block:parameter "a" is shadowed Detail: Local variable shadows function parameter. warning extra:00000:5:statement block:variable "ax" shadows a previously defined variable Hint: SET plpgsql.extra_warnings TO 'shadowed_variables' warning:00000:2:DECLARE:unused variable "ax" warning extra:00000:2:DECLARE:never read variable "a" warning extra:00000:4:DECLARE:never read variable "ax" warning extra:00000:unused parameter "a" (8 rows) create type xt as (a int, b int, c int); create or replace function fx_xt(out x xt) as $$ declare l xt; a int; begin return; end; $$ language plpgsql; select * from plpgsql_check_function('fx_xt()'); plpgsql_check_function ------------------------------------------------- warning:00000:2:DECLARE:unused variable "l" warning:00000:3:DECLARE:unused variable "a" warning extra:00000:unmodified OUT variable "x" (3 rows) drop function fx_xt(); create or replace function fx_xt(out x xt) as $$ declare l xt; a int; begin x.c := 1000; return; end; $$ language plpgsql; select * from plpgsql_check_function('fx_xt()'); plpgsql_check_function --------------------------------------------- warning:00000:2:DECLARE:unused variable "l" warning:00000:3:DECLARE:unused variable "a" (2 rows) drop function fx_xt(); create or replace function fx_xt(out x xt, out y xt) as $$ declare c1 xt; c2 xt; begin return; end; $$ language plpgsql; select * from plpgsql_check_function('fx_xt()'); plpgsql_check_function ----------------------------------------------------------------------- warning:00000:2:DECLARE:unused variable "c1" warning:00000:2:DECLARE:unused variable "c2" warning extra:00000:composite OUT variable "x" is not single argument warning extra:00000:unmodified OUT variable "x" warning extra:00000:composite OUT variable "y" is not single argument warning extra:00000:unmodified OUT variable "y" (6 rows) drop function fx_xt(); create or replace function fx_xt(out x xt, out y xt) as $$ declare c1 xt; c2 xt; begin x.a := 100; y := row(10,20,30); return; end; $$ language plpgsql; select * from plpgsql_check_function('fx_xt()'); plpgsql_check_function ----------------------------------------------------------------------- warning:00000:2:DECLARE:unused variable "c1" warning:00000:2:DECLARE:unused variable "c2" warning extra:00000:composite OUT variable "x" is not single argument warning extra:00000:composite OUT variable "y" is not single argument (4 rows) drop function fx_xt(); create or replace function fx_xt(out x xt, out z int) as $$ begin return; end; $$ language plpgsql; select * from plpgsql_check_function('fx_xt()'); plpgsql_check_function ----------------------------------------------------------------------- warning extra:00000:composite OUT variable "x" is not single argument warning extra:00000:unmodified OUT variable "x" warning extra:00000:unmodified OUT variable "z" (3 rows) drop function fx_xt(); drop type xt; -- missing RETURN create or replace function fx_flow() returns int as $$ begin raise notice 'kuku'; end; $$ language plpgsql; select fx_flow(); NOTICE: kuku ERROR: control reached end of function without RETURN CONTEXT: PL/pgSQL function fx_flow() select * from plpgsql_check_function('fx_flow()'); plpgsql_check_function ------------------------------------------------------------ error:2F005:control reached end of function without RETURN (1 row) -- ok create or replace function fx_flow() returns int as $$ declare a int; begin if a > 10 then return a; end if; return 10; end; $$ language plpgsql; select * from plpgsql_check_function('fx_flow()'); plpgsql_check_function ------------------------ (0 rows) -- dead code create or replace function fx_flow() returns int as $$ declare a int; begin if a > 10 then return a; else return a + 1; end if; return 10; end; $$ language plpgsql; select * from plpgsql_check_function('fx_flow()'); plpgsql_check_function ----------------------------------------------- warning extra:00000:9:RETURN:unreachable code (1 row) -- missing return create or replace function fx_flow() returns int as $$ declare a int; begin if a > 10 then return a; end if; end; $$ language plpgsql; select * from plpgsql_check_function('fx_flow()'); plpgsql_check_function -------------------------------------------------------------------- warning extra:2F005:control reached end of function without RETURN (1 row) drop function fx_flow(); create or replace function fx_flow(in p_param1 integer) returns text as $$ declare z1 text; begin if p_param1 is not null then z1 := '1111'; return z1; else z1 := '222222'; end if; return z1; end; $$ language plpgsql stable; select * from plpgsql_check_function_tb('fx_flow(integer)'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+---------+--------+------+-------+----------+-------+--------- (0 rows) create or replace function fx_flow(in p_param1 integer) returns text as $$ declare z1 text; begin if p_param1 is not null then z1 := '1111'; return z1; else z1 := '222222'; raise exception 'stop'; end if; return z1; end; $$ language plpgsql stable; select * from plpgsql_check_function_tb('fx_flow(integer)'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+------------------+--------+------+---------------+----------+-------+--------- fx_flow | 12 | RETURN | 00000 | unreachable code | | | warning extra | | | (1 row) drop function fx_flow(); ERROR: function fx_flow() does not exist drop function fx(int); ERROR: function fx(integer) does not exist create or replace function fx(x int) returns table(y int) as $$ begin return query select x union select x; end $$ language plpgsql; select * from fx(10); y ---- 10 (1 row) select * from plpgsql_check_function_tb('fx(int)'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+---------+--------+------+-------+----------+-------+--------- (0 rows) drop function fx(int); create or replace function fx(x int) returns table(y int, z int) as $$ begin return query select x,x+1 union select x, x+1; end $$ language plpgsql; select * from fx(10); y | z ----+---- 10 | 11 (1 row) select * from plpgsql_check_function_tb('fx(int)'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+---------+--------+------+-------+----------+-------+--------- (0 rows) drop function fx(int); create table xx(a int); create or replace function fx(x int) returns int as $$ declare _a int; begin begin select a from xx into strict _a where a = x; return _a; exception when others then null; end; return -1; end; $$ language plpgsql; select * from plpgsql_check_function_tb('fx(int)'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+---------+--------+------+-------+----------+-------+--------- (0 rows) drop table xx; create or replace function fx(x int) returns int as $$ begin begin if (x > 0) then raise exception 'xxx' using errcode = 'XX888'; else raise exception 'yyy' using errcode = 'YY888'; end if; return -1; -- dead code; end; return -1; end; $$ language plpgsql; select * from plpgsql_check_function_tb('fx(int)'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+------------------+--------+------+---------------+----------+-------+--------- fx | 9 | RETURN | 00000 | unreachable code | | | warning extra | | | fx | 11 | RETURN | 00000 | unreachable code | | | warning extra | | | (2 rows) create or replace function fx(x int) returns int as $$ begin begin if (x > 0) then raise exception 'xxx' using errcode = 'XX888'; else raise exception 'yyy' using errcode = 'YY888'; end if; exception when sqlstate 'XX888' then null; when sqlstate 'YY888' then null; end; end; -- missing return; $$ language plpgsql; select * from plpgsql_check_function_tb('fx(int)'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+------------------------------------------------+--------+------+-------+----------+-------+--------- fx | | | 2F005 | control reached end of function without RETURN | | | error | | | (1 row) create or replace function fx(x int) returns int as $$ begin begin if (x > 0) then raise exception 'xxx' using errcode = 'XX888'; else raise exception 'yyy' using errcode = 'YY888'; end if; exception when others then return 10; end; end; -- ok now $$ language plpgsql; select * from plpgsql_check_function_tb('fx(int)'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+---------+--------+------+-------+----------+-------+--------- (0 rows) --false alarm reported by Filip Zach create type testtype as (id integer); create or replace function fx() returns testtype as $$ begin return row(1); end; $$ language plpgsql; select * from fx(); id ---- 1 (1 row) select fx(); fx ----- (1) (1 row) select * from plpgsql_check_function('fx()'); plpgsql_check_function ------------------------ (0 rows) drop function fx(); create function out1(OUT f1 int, OUT f2 int) returns setof record as $$ begin for f1, f2 in execute $q$ select 1, 2 $q$ loop return next; end loop; end $$ language plpgsql; select * from plpgsql_check_function('out1()'); plpgsql_check_function ------------------------ (0 rows) drop function out1(); create function out1(OUT f1 int, OUT f2 int) returns setof record as $$ begin for f1, f2 in select 1, 2 loop return next; end loop; end $$ language plpgsql; select * from plpgsql_check_function('out1()'); plpgsql_check_function ------------------------ (0 rows) drop function out1(); -- never read variable detection create function a() returns int as $$ declare foo int; begin foo := 2; return 1; end; $$ language plpgsql; select * from plpgsql_check_function('a()'); plpgsql_check_function --------------------------------------------------------- warning extra:00000:2:DECLARE:never read variable "foo" (1 row) drop function a(); -- issue #29 false unused variable create or replace function f1(in p_cursor refcursor) returns void as $body$ declare z_offset integer; begin z_offset := 10; move absolute z_offset from p_cursor; end; $body$ language 'plpgsql' stable; select * from plpgsql_check_function_tb('f1(refcursor)'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+---------+--------+------+-------+----------+-------+--------- (0 rows) drop function f1(refcursor); -- issue #30 segfault due NULL refname create or replace function test(a varchar) returns void as $$ declare x cursor (_a varchar) for select _a; begin open x(a); end; $$ language plpgsql; select * from plpgsql_check_function_tb('test(varchar)'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+-------------------------+--------+------+---------------+----------+-------+--------- test | 2 | DECLARE | 00000 | never read variable "x" | | | warning extra | | | (1 row) drop function test(varchar); create or replace function test() returns void as $$ declare x numeric; begin x := NULL; end; $$ language plpgsql; select * from plpgsql_check_function('test()'); plpgsql_check_function ------------------------------------------------------- warning extra:00000:2:DECLARE:never read variable "x" (1 row) drop function test(); create table testtable(a int); create or replace function test() returns int as $$ declare r testtable; begin select * into r from testtable; return r.a; end; $$ language plpgsql; select * from plpgsql_check_function('test()'); plpgsql_check_function ------------------------ (0 rows) set check_function_bodies to on; drop table testtable; create table testtable(a int, b int); create or replace function test() returns int as $$ declare r testtable; begin select * into r from testtable; return r.a; end; $$ language plpgsql; alter table testtable drop column b; -- expected false alarm on PostgreSQL 10 and older -- there is not possibility to enforce recompilation -- before checking. select * from plpgsql_check_function('test()'); plpgsql_check_function ------------------------ (0 rows) drop function test(); -- issue #32 create table bigtable(id bigint, v varchar); create or replace function test() returns void as $$ declare r record; _id numeric; begin select * into r from bigtable where id = _id; for r in select * from bigtable where _id = id loop end loop; if (exists(select * from bigtable where id = _id)) then end if; end; $$ language plpgsql; select test(); test ------ (1 row) -- should to show performance warnings select * from plpgsql_check_function('test()', performance_warnings := true); plpgsql_check_function ------------------------------------------------------------------------------------------------------------------------------- performance:42804:6:SQL statement:implicit cast of attribute caused by different PLpgSQL variable type in WHERE clause Query: select * from bigtable where id = _id -- ^ Detail: An index of some attribute cannot be used, when variable, used in predicate, has not right type like a attribute Hint: Check a variable type - int versus numeric performance:42804:7:FOR over SELECT rows:implicit cast of attribute caused by different PLpgSQL variable type in WHERE clause Query: select * from bigtable where _id = id -- ^ Detail: An index of some attribute cannot be used, when variable, used in predicate, has not right type like a attribute Hint: Check a variable type - int versus numeric performance:42804:10:IF:implicit cast of attribute caused by different PLpgSQL variable type in WHERE clause Query: (exists(select * from bigtable where id = _id)) -- ^ Detail: An index of some attribute cannot be used, when variable, used in predicate, has not right type like a attribute Hint: Check a variable type - int versus numeric warning extra:00000:3:DECLARE:never read variable "r" (16 rows) create or replace function test() returns void as $$ declare r record; _id bigint; begin select * into r from bigtable where id = _id; for r in select * from bigtable where _id = id loop end loop; if (exists(select * from bigtable where id = _id)) then end if; end; $$ language plpgsql; -- there are not any performance issue now select * from plpgsql_check_function('test()', performance_warnings := true); plpgsql_check_function ------------------------------------------------------- warning extra:00000:3:DECLARE:never read variable "r" (1 row) -- nextval, currval and setval test create table test_table(); create or replace function testseq() returns void as $$ begin perform nextval('test_table'); perform currval('test_table'); perform setval('test_table', 10); perform setval('test_table', 10, true); end; $$ language plpgsql; -- should to fail select testseq(); ERROR: cannot open relation "test_table" DETAIL: This operation is not supported for tables. CONTEXT: SQL statement "SELECT nextval('test_table')" PL/pgSQL function testseq() line 3 at PERFORM select * from plpgsql_check_function('testseq()', fatal_errors := false); plpgsql_check_function ------------------------------------------------------ error:42809:3:PERFORM:"test_table" is not a sequence Query: SELECT nextval('test_table') -- ^ error:42809:4:PERFORM:"test_table" is not a sequence Query: SELECT currval('test_table') -- ^ error:42809:5:PERFORM:"test_table" is not a sequence Query: SELECT setval('test_table', 10) -- ^ error:42809:6:PERFORM:"test_table" is not a sequence Query: SELECT setval('test_table', 10, true) -- ^ (12 rows) drop function testseq(); drop table test_table; -- tests designed for PostgreSQL 9.2 set check_function_bodies to off; create table t1(a int, b int); create function f1() returns void as $$ begin if false then update t1 set c = 30; end if; if false then raise notice '%', r.c; end if; end; $$ language plpgsql; select f1(); f1 ---- (1 row) select * from plpgsql_check_function_tb('f1()', fatal_errors := true); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+---------------+----------+--------------------------------------------+--------+------+-------+----------+----------------------+--------- f1 | 4 | SQL statement | 42703 | column "c" of relation "t1" does not exist | | | error | 15 | update t1 set c = 30 | (1 row) select * from plpgsql_check_function_tb('f1()', fatal_errors := false); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+---------------+----------+--------------------------------------------+--------+------+-------+----------+----------------------+--------- f1 | 4 | SQL statement | 42703 | column "c" of relation "t1" does not exist | | | error | 15 | update t1 set c = 30 | f1 | 7 | RAISE | 42P01 | missing FROM-clause entry for table "r" | | | error | 1 | r.c | (2 rows) select * from plpgsql_check_function_tb('f1()'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+---------------+----------+--------------------------------------------+--------+------+-------+----------+----------------------+--------- f1 | 4 | SQL statement | 42703 | column "c" of relation "t1" does not exist | | | error | 15 | update t1 set c = 30 | (1 row) select f1(); f1 ---- (1 row) drop function f1(); create or replace function f1() returns void as $$ begin if false then raise notice '%', 1, 2; end if; end; $$ language plpgsql; select f1(); ERROR: too many parameters specified for RAISE CONTEXT: compilation of PL/pgSQL function "f1" near line 4 select * from plpgsql_check_function_tb('f1()'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+-----------------------------------------+--------+------+-------+----------+-------+--------------------------------------------------- f1 | | | 42601 | too many parameters specified for RAISE | | | error | | | compilation of PL/pgSQL function "f1" near line 4 (1 row) select f1(); ERROR: too many parameters specified for RAISE CONTEXT: compilation of PL/pgSQL function "f1" near line 4 drop function f1(); create or replace function f1() returns void as $$ begin if false then raise notice '% %'; end if; end; $$ language plpgsql; select f1(); ERROR: too few parameters specified for RAISE CONTEXT: compilation of PL/pgSQL function "f1" near line 4 select * from plpgsql_check_function_tb('f1()'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+----------------------------------------+--------+------+-------+----------+-------+--------------------------------------------------- f1 | | | 42601 | too few parameters specified for RAISE | | | error | | | compilation of PL/pgSQL function "f1" near line 4 (1 row) select f1(); ERROR: too few parameters specified for RAISE CONTEXT: compilation of PL/pgSQL function "f1" near line 4 drop function f1(); create or replace function ml_trg() returns trigger as $$ #option dump declare begin if TG_OP = 'INSERT' then if NEW.status_from IS NULL then begin -- performance issue only select status into NEW.status_from from pa where pa_id = NEW.pa_id; -- nonexist target value select status into NEW.status_from_xxx from pa where pa_id = NEW.pa_id; exception when DATA_EXCEPTION then new.status_from := 'DE'; end; end if; end if; if TG_OP = 'DELETE' then return OLD; else return NEW; end if; exception when OTHERS then NULL; if TG_OP = 'DELETE' then return OLD; else return NEW; end if; end; $$ language plpgsql; select * from plpgsql_check_function('ml_trg()', 'ml', performance_warnings := true); plpgsql_check_function -------------------------------------------------------------------------- error:42703:13:SQL statement:record "new" has no field "status_from_xxx" (1 row) create or replace function fx2() returns void as $$ declare _pa pa; begin select pa.id into _pa.id from pa limit 1; select pa.pa_id into _pa.pa_id from pa limit 1; end; $$ language plpgsql; select * from plpgsql_check_function('fx2()', performance_warnings := true); plpgsql_check_function --------------------------------------------------------- warning extra:00000:2:DECLARE:never read variable "_pa" (1 row) drop function fx2(); create or replace function fx2() returns void as $$ declare _pa pa; begin _pa.id := (select pa.id from pa limit 1); _pa.pa_id := (select pa.pa_id from pa limit 1); end; $$ language plpgsql; select * from plpgsql_check_function('fx2()', performance_warnings := true); plpgsql_check_function --------------------------------------------------------- warning extra:00000:2:DECLARE:never read variable "_pa" (1 row) drop function fx2(); create or replace function test_lab() returns void as $$ begin <> for a in 1..3 loop <> BEGIN <> for b in 8..9 loop if a=2 then continue sub; end if; raise notice '% %', a, b; end loop inner; END sub; end loop outer; end; $$ language plpgsql; select test_lab(); ERROR: block label "sub" cannot be used in CONTINUE LINE 10: continue sub; ^ QUERY: begin <> for a in 1..3 loop <> BEGIN <> for b in 8..9 loop if a=2 then continue sub; end if; raise notice '% %', a, b; end loop inner; END sub; end loop outer; end; CONTEXT: compilation of PL/pgSQL function "test_lab" near line 10 select * from plpgsql_check_function('test_lab()', performance_warnings := true); plpgsql_check_function ------------------------------------------------------------------- error:42601:block label "sub" cannot be used in CONTINUE Query: begin <> for a in 1..3 loop <> BEGIN <> for b in 8..9 loop if a=2 then continue sub; -- ^ end if; raise notice '% %', a, b; end loop inner; END sub; end loop outer; end; Context: compilation of PL/pgSQL function "test_lab" near line 10 (20 rows) create or replace function test_lab() returns void as $$ begin continue; end; $$ language plpgsql; select test_lab(); ERROR: CONTINUE cannot be used outside a loop LINE 3: continue; ^ QUERY: begin continue; end; CONTEXT: compilation of PL/pgSQL function "test_lab" near line 3 select * from plpgsql_check_function('test_lab()', performance_warnings := true); plpgsql_check_function ------------------------------------------------------------------ error:42601:CONTINUE cannot be used outside a loop Query: begin continue; -- ^ end; Context: compilation of PL/pgSQL function "test_lab" near line 3 (8 rows) create type _exception_type as ( state text, message text, detail text); create or replace function f1() returns void as $$ declare _exception record; begin _exception := NULL::_exception_type; exception when others then get stacked diagnostics _exception.state = RETURNED_SQLSTATE, _exception.message = MESSAGE_TEXT, _exception.detail = PG_EXCEPTION_DETAIL, _exception.hint = PG_EXCEPTION_HINT; end; $$ language plpgsql; select f1(); f1 ---- (1 row) select * from plpgsql_check_function('f1()'); plpgsql_check_function ------------------------------------------------------------------------------- error:42703:7:GET STACKED DIAGNOSTICS:record "_exception" has no field "hint" (1 row) drop function f1(); drop type _exception_type; drop table t1; create function myfunc1(a int, b float) returns integer as $$ begin end $$ language plpgsql; create function myfunc2(a int, b float) returns integer as $$ begin end $$ language plpgsql; create function myfunc3(a int, b float) returns integer as $$ begin end $$ language plpgsql; create function myfunc4(a int, b float) returns integer as $$ begin end $$ language plpgsql; create function opfunc1(a int, b float) returns integer as $$ begin end $$ language plpgsql; create operator *** (procedure = opfunc1, leftarg = int, rightarg = float); create table mytable(a int); create table myview as select * from mytable; create function testfunc(a int, b float) returns void as $$ declare x integer; begin raise notice '%', myfunc1(a, b); x := myfunc2(a, b) operator(public.***) 1; perform myfunc3(m.a, b) from myview m; insert into mytable select myfunc4(a, b); end; $$ language plpgsql; select * from plpgsql_check_function('testfunc(int,float)'); plpgsql_check_function ------------------------------------------------------- warning extra:00000:2:DECLARE:never read variable "x" (1 row) select type, schema, name, params from plpgsql_show_dependency_tb('testfunc(int,float)'); type | schema | name | params ----------+--------+---------+---------------------------- FUNCTION | public | myfunc1 | (integer,double precision) FUNCTION | public | myfunc2 | (integer,double precision) FUNCTION | public | myfunc3 | (integer,double precision) FUNCTION | public | myfunc4 | (integer,double precision) OPERATOR | public | *** | (integer,double precision) RELATION | public | mytable | RELATION | public | myview | (7 rows) drop function testfunc(int, float); drop function myfunc1(int, float); drop function myfunc2(int, float); drop function myfunc3(int, float); drop function myfunc4(int, float); drop table mytable; drop view myview; ERROR: "myview" is not a view HINT: Use DROP TABLE to remove a table. -- issue #221 create type foo_type as (a int); create table foo(a int); create function testfuncdep() returns void as $$ declare x foo_type; y foo; begin end; $$ language plpgsql; select type, schema, name from plpgsql_show_dependency_tb('testfuncdep'); type | schema | name ----------+--------+---------- RELATION | public | foo TYPE | public | foo_type (2 rows) drop function testfuncdep(); drop type foo_type; drop table foo; -- issue #221 - dependency report on trigger function should not crash -- (NEW and OLD record variables have no datatype) create table foo(a int, b text); create function foo_trg_func() returns trigger as $$ begin new.b := 'x'; return new; end; $$ language plpgsql; create trigger foo_trg before insert on foo for each row execute function foo_trg_func(); select type, schema, name from plpgsql_show_dependency_tb('foo_trg_func', 'foo'); type | schema | name ------+--------+------ (0 rows) drop trigger foo_trg on foo; drop function foo_trg_func(); drop table foo; -- issue #34 create or replace function testcase() returns bool as $$ declare x int; begin set local search_path to public, test; case x when 1 then return true; else return false; end case; end; $$ language plpgsql; -- should not to raise warning select * from plpgsql_check_function('testcase()'); plpgsql_check_function ------------------------ (0 rows) drop function testcase(); -- Adam's Bartoszewicz example create or replace function public.test12() returns refcursor language plpgsql as $body$ declare rc refcursor; begin open rc scroll for select pc.* from pg_cast pc; return rc; end; $body$; -- should not returns false alarm select * from plpgsql_check_function('test12()'); plpgsql_check_function ------------------------ (0 rows) drop function public.test12(); -- should to show performance warning on bad flag create or replace function flag_test1(int) returns int as $$ begin return $1 + 10; end; $$ language plpgsql stable; create table fufu(a int); create or replace function flag_test2(int) returns int as $$ begin return (select * from fufu limit 1); end; $$ language plpgsql volatile; select * from plpgsql_check_function('flag_test1(int)', performance_warnings := true); plpgsql_check_function ----------------------------------------------------------------------------------------- performance:00000:routine is marked as STABLE, should be IMMUTABLE Hint: When you fix this issue, please, recheck other functions that uses this function. (2 rows) select * from plpgsql_check_function('flag_test2(int)', performance_warnings := true); plpgsql_check_function ----------------------------------------------------------------------------------------- warning extra:00000:unused parameter "$1" performance:00000:routine is marked as VOLATILE, should be STABLE Hint: When you fix this issue, please, recheck other functions that uses this function. (3 rows) drop table fufu; drop function flag_test1(int); drop function flag_test2(int); create or replace function rrecord01() returns setof record as $$ begin return query select 1,2; end; $$ language plpgsql; create or replace function rrecord02() returns record as $$ begin return row(10,20,30); end; $$ language plpgsql; create type record03 as (a int, b int); create or replace function rrecord03() returns record03 as $$ declare r record; begin r := row(1); return r; end; $$ language plpgsql; -- should not to raise false alarms select * from plpgsql_check_function('rrecord01'); plpgsql_check_function ------------------------ (0 rows) select * from plpgsql_check_function('rrecord02'); plpgsql_check_function ------------------------ (0 rows) -- should detect different return but still detect return select * from plpgsql_check_function('rrecord03', fatal_errors => false); plpgsql_check_function ---------------------------------------------------------------------------------- error:42804:5:RETURN:returned record type does not match expected record type Detail: Number of returned columns (1) does not match expected column count (2). (2 rows) drop function rrecord01(); drop function rrecord02(); drop function rrecord03(); drop type record03; create or replace function bugfunc01() returns void as $$ declare cvar cursor(a int, b int) for select a + b from generate_series(1,b); begin for t in cvar(1,3) loop raise notice '%', t; end loop; end; $$ language plpgsql; select bugfunc01(); NOTICE: (4) NOTICE: (4) NOTICE: (4) bugfunc01 ----------- (1 row) select * from plpgsql_check_function('bugfunc01'); plpgsql_check_function ------------------------ (0 rows) create or replace function bugfunc02() returns void as $$ declare cvar cursor(a int, b int) for select a + b from generate_series(1,b); begin open cvar(10,20); close cvar; end; $$ language plpgsql; select bugfunc02(); bugfunc02 ----------- (1 row) select * from plpgsql_check_function('bugfunc02'); plpgsql_check_function ------------------------ (0 rows) create or replace function bugfunc03() returns void as $$ declare cvar cursor(a int, b int) for select a + b from not_exists_table; begin open cvar(10,20); close cvar; end; $$ language plpgsql; select bugfunc03(); ERROR: relation "not_exists_table" does not exist LINE 1: select a + b from not_exists_table ^ QUERY: select a + b from not_exists_table CONTEXT: PL/pgSQL function bugfunc03() line 5 at OPEN select * from plpgsql_check_function('bugfunc03'); plpgsql_check_function --------------------------------------------------------------- error:42P01:5:OPEN:relation "not_exists_table" does not exist Query: select a + b from not_exists_table -- ^ (3 rows) create or replace function f1(out cr refcursor) as $$ begin end; $$ language plpgsql; -- should to raise warning select * from plpgsql_check_function('f1()'); plpgsql_check_function -------------------------------------------------- warning extra:00000:unmodified OUT variable "cr" (1 row) create or replace function f1(out cr refcursor) as $$ begin open cr for select 1; end; $$ language plpgsql; -- should not to raise warning, see issue #43 select * from plpgsql_check_function('f1()'); plpgsql_check_function ------------------------ (0 rows) drop function f1(); create table testt(a int); create or replace function testt_trg_func() returns trigger as $$ begin return new; end; $$ language plpgsql; create trigger testt_trg before insert or update on testt for each row execute procedure testt_trg_func(); create or replace function maintaince_function() returns void as $$ begin alter table testt disable trigger testt_trg; alter table testt enable trigger testt_trg; end; $$ language plpgsql; -- should not to crash select * from plpgsql_check_function_tb('maintaince_function()', 0, true, true, true); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+---------+--------+------+-------+----------+-------+--------- (0 rows) drop function maintaince_function(); drop trigger testt_trg on testt; drop function testt_trg_func(); drop table testt; create or replace function test_crash() returns void as $$ declare ec int default buggyfunc(10); begin select * into ec from buggytab; end; $$ language plpgsql; -- should not to crash select * from plpgsql_check_function('test_crash', fatal_errors := false); plpgsql_check_function --------------------------------------------------------------------------------------------------------- error:42883:4:statement block:function buggyfunc(integer) does not exist Query: buggyfunc(10) -- ^ Hint: No function matches the given name and argument types. You might need to add explicit type casts. Context: during statement block local variable "ec" initialization on line 3 error:42P01:5:SQL statement:relation "buggytab" does not exist Query: select * from buggytab -- ^ warning extra:00000:3:DECLARE:never read variable "ec" (9 rows) select * from plpgsql_check_function('test_crash', fatal_errors := true); plpgsql_check_function --------------------------------------------------------------------------------------------------------- error:42883:4:statement block:function buggyfunc(integer) does not exist Query: buggyfunc(10) -- ^ Hint: No function matches the given name and argument types. You might need to add explicit type casts. Context: during statement block local variable "ec" initialization on line 3 (5 rows) drop function test_crash(); -- fix false alarm reported by Piotr Stepniewski create or replace function public.fx() returns void language plpgsql as $function$ begin raise exception 'xxx'; end; $function$; -- show raise nothing select * from plpgsql_check_function('fx()'); plpgsql_check_function ------------------------ (0 rows) create table errtab( message text, code character(5) ); create or replace function public.fx() returns void language plpgsql as $function$ declare var errtab%rowtype; begin raise exception using message = var.message, errcode = var.code; end; $function$; -- should not to crash select * from plpgsql_check_function('fx()'); plpgsql_check_function ------------------------ (0 rows) create or replace function public.fx() returns void language plpgsql as $function$ declare var errtab%rowtype; begin raise exception using message = var.message, errcode = var.code, hint = var.hint; end; $function$; -- should not to crash select * from plpgsql_check_function('fx()'); plpgsql_check_function ------------------------------------------------------ error:42703:5:RAISE:record "var" has no field "hint" Context: PL/pgSQL expression "var.hint" (2 rows) drop function fx(); create or replace function foo_format(a text, b text) returns void as $$ declare s text; begin s := format('%s'); -- should to raise error s := format('%s %10s', a, b); -- should be ok s := format('%s %s', a, b, a); -- should to raise warning s := format('%s %d', a, b); -- should to raise error raise notice '%', s; end; $$ language plpgsql; select * from plpgsql_check_function('foo_format', fatal_errors := false); plpgsql_check_function ------------------------------------------------------------------- error:22023:4:assignment:too few arguments for format() Query: s := format('%s') -- ^ Context: at assignment to variable "s" declared on line 2 warning:00000:6:assignment:unused parameters of function "format" Query: s := format('%s %s', a, b, a) -- ^ Context: at assignment to variable "s" declared on line 2 error:22023:7:assignment:unrecognized format() type specifier "d" Query: s := format('%s %d', a, b) -- ^ Context: at assignment to variable "s" declared on line 2 (12 rows) drop function foo_format(text, text); create or replace function dyn_sql_1() returns void as $$ declare v varchar; n int; begin execute 'select ' || n; -- ok execute 'select ' || quote_literal(v); -- ok execute 'select ' || v; -- vulnerable execute format('select * from %I', v); -- ok execute format('select * from %s', v); -- vulnerable execute 'select $1' using v; -- ok execute 'select 1'; -- ok execute 'select 1' using v; -- warning execute 'select $1'; -- error end; $$ language plpgsql; select * from plpgsql_check_function('dyn_sql_1', security_warnings := true, fatal_errors := false); plpgsql_check_function ------------------------------------------------------------------------------------------ security:00000:8:EXECUTE:text type variable is not sanitized Query: 'select ' || v -- ^ Detail: The EXECUTE expression is SQL injection vulnerable. Hint: Use quote_ident, quote_literal or format function to secure variable. security:00000:10:EXECUTE:text type variable is not sanitized Query: format('select * from %s', v) -- ^ Detail: The EXECUTE expression is SQL injection vulnerable. Hint: Use quote_ident, quote_literal or format function to secure variable. warning:00000:13:EXECUTE:values passed to EXECUTE statement by USING clause was not used error:42P02:14:EXECUTE:there is no parameter $1 Query: select $1 -- ^ (14 rows) drop function dyn_sql_1(); create type tp as (a int, b int); create or replace function dyn_sql_2() returns void as $$ declare r tp; result int; begin select 10 a, 20 b into r; raise notice '%', r.a; execute 'select $1.a + $1.b' into result using r; execute 'select $1.c' into result using r; -- error raise notice '%', result; end; $$ language plpgsql; select * from plpgsql_check_function('dyn_sql_2', security_warnings := true); plpgsql_check_function ------------------------------------------------------------ error:42703:9:EXECUTE:column "c" not found in data type tp Query: select $1.c -- ^ (3 rows) drop function dyn_sql_2(); drop type tp; /* * Should not to work * * note: plpgsql doesn't support passing some necessary details for record * type. The parser setup for dynamic SQL column doesn't use ref hooks, and * then it cannot to pass TupleDesc info to query anyway. */ create or replace function dyn_sql_2() returns void as $$ declare r record; result int; begin select 10 a, 20 b into r; raise notice '%', r.a; execute 'select $1.a + $1.b' into result using r; raise notice '%', result; end; $$ language plpgsql; select dyn_sql_2(); --should to fail NOTICE: 10 ERROR: could not identify column "a" in record data type LINE 1: select $1.a + $1.b ^ QUERY: select $1.a + $1.b CONTEXT: PL/pgSQL function dyn_sql_2() line 8 at EXECUTE select * from plpgsql_check_function('dyn_sql_2', security_warnings := true); plpgsql_check_function ------------------------------------------------------------------------- error:42703:8:EXECUTE:could not identify column "a" in record data type Query: select $1.a + $1.b -- ^ (3 rows) drop function dyn_sql_2(); create or replace function dyn_sql_3() returns void as $$ declare r int; begin execute 'select $1' into r using 1; raise notice '%', r; end $$ language plpgsql; select dyn_sql_3(); NOTICE: 1 dyn_sql_3 ----------- (1 row) -- should be ok select * from plpgsql_check_function('dyn_sql_3'); plpgsql_check_function ------------------------ (0 rows) create or replace function dyn_sql_3() returns void as $$ declare r record; begin execute 'select $1 as a, $2 as b' into r using 1, 2; raise notice '% %', r.a, r.b; end $$ language plpgsql; select dyn_sql_3(); NOTICE: 1 2 dyn_sql_3 ----------- (1 row) -- should be ok select * from plpgsql_check_function('dyn_sql_3'); plpgsql_check_function ------------------------ (0 rows) create or replace function dyn_sql_3() returns void as $$ declare r record; begin execute 'create table foo(a int)' into r using 1, 2; raise notice '% %', r.a, r.b; end $$ language plpgsql; -- raise a error select * from plpgsql_check_function('dyn_sql_3'); plpgsql_check_function ----------------------------------------------------------------------------------------- warning:00000:4:EXECUTE:values passed to EXECUTE statement by USING clause was not used error:XX000:4:EXECUTE:expression does not return data (2 rows) create or replace function dyn_sql_3() returns void as $$ declare r1 int; r2 int; begin execute 'select 1' into r1, r2 using 1, 2; raise notice '% %', r1, r2; end $$ language plpgsql; -- raise a error select * from plpgsql_check_function('dyn_sql_3'); plpgsql_check_function ----------------------------------------------------------------------------------------- warning:00000:4:EXECUTE:values passed to EXECUTE statement by USING clause was not used warning:00000:4:EXECUTE:too few attributes for target variables Query: select 1 -- ^ Detail: There are more target variables than output columns in query. Hint: Check target variables in SELECT INTO statement. Context: at EXECUTE to r1, r2 variables (7 rows) drop function dyn_sql_3(); create or replace function dyn_sql_3() returns void as $$ declare r record; begin for r in execute 'select 1 as a, 2 as b' loop raise notice '%', r.a; end loop; end $$ language plpgsql; -- should be ok select * from plpgsql_check_function('dyn_sql_3'); plpgsql_check_function ------------------------ (0 rows) drop function dyn_sql_3(); create or replace function dyn_sql_3() returns void as $$ declare r record; begin for r in execute 'select 1 as a, 2 as b' loop raise notice '%', r.c; end loop; end $$ language plpgsql; -- should be error select * from plpgsql_check_function('dyn_sql_3'); plpgsql_check_function ------------------------------------------------- error:42703:6:RAISE:record "r" has no field "c" Context: PL/pgSQL expression "r.c" (2 rows) drop function dyn_sql_3(); create or replace function dyn_sql_3() returns void as $$ declare r record; v text = 'select 10 a, 20 b't; begin for r in execute v loop raise notice '%', r.a; end loop; end $$ language plpgsql; -- should be ok select * from plpgsql_check_function('dyn_sql_3'); plpgsql_check_function ------------------------ (0 rows) drop function dyn_sql_3(); create or replace function dyn_sql_4() returns table(ax int, bx int) as $$ begin return query execute 'select 10, 20'; return; end; $$ language plpgsql; -- should be ok select * from plpgsql_check_function('dyn_sql_4()'); plpgsql_check_function ------------------------ (0 rows) create or replace function dyn_sql_4() returns table(ax int, bx int) as $$ begin return query execute 'select 10, 20, 30'; return; end; $$ language plpgsql; select * from dyn_sql_4(); ERROR: structure of query does not match function result type DETAIL: Number of returned columns (3) does not match expected column count (2). CONTEXT: SQL statement "select 10, 20, 30" PL/pgSQL function dyn_sql_4() line 3 at RETURN QUERY -- should be error select * from plpgsql_check_function('dyn_sql_4()'); plpgsql_check_function ----------------------------------------------------------------------------------- error:42804:3:RETURN QUERY:structure of query does not match function result type Detail: Number of returned columns (3) does not match expected column count (2). (2 rows) drop function dyn_sql_4(); create or replace function test_bug(text) returns regproc as $$ begin return $1::regproc; exception when undefined_function or invalid_name then raise; end; $$ language plpgsql; -- should not raise a exception select * from plpgsql_check_function('test_bug'); plpgsql_check_function ------------------------ (0 rows) create or replace function test_bug(text) returns regproc as $$ begin return $1::regproc; exception when undefined_function or invalid_name then raise notice '%', $1; -- bug end; $$ language plpgsql; select test_bug('kuku'); -- should to fail NOTICE: kuku ERROR: control reached end of function without RETURN CONTEXT: PL/pgSQL function test_bug(text) select * from plpgsql_check_function('test_bug'); plpgsql_check_function -------------------------------------------------------------------- warning extra:2F005:control reached end of function without RETURN (1 row) drop function test_bug(text); create or replace function test_bug(text) returns regproc as $$ begin return $1::regproc; exception when undefined_function or invalid_name then raise notice '%', $1; return NULL; end; $$ language plpgsql; select test_bug('kuku'); -- should be ok NOTICE: kuku test_bug ---------- (1 row) select * from plpgsql_check_function('test_bug'); plpgsql_check_function ------------------------ (0 rows) drop function test_bug(text); create or replace function foo(a text, b text) returns void as $$ begin -- unsecure execute 'select ' || a; a := quote_literal(a); -- is safe now execute 'select ' || a; a := a || b; -- it is unsecure again execute 'select ' || a; end; $$ language plpgsql; \sf+ foo(text, text) CREATE OR REPLACE FUNCTION public.foo(a text, b text) RETURNS void LANGUAGE plpgsql 1 AS $function$ 2 begin 3 -- unsecure 4 execute 'select ' || a; 5 a := quote_literal(a); -- is safe now 6 execute 'select ' || a; 7 a := a || b; -- it is unsecure again 8 execute 'select ' || a; 9 end; 10 $function$ -- should to raise two warnings select * from plpgsql_check_function('foo', security_warnings := true); plpgsql_check_function ----------------------------------------------------------------------------- security:00000:4:EXECUTE:text type variable is not sanitized Query: 'select ' || a -- ^ Detail: The EXECUTE expression is SQL injection vulnerable. Hint: Use quote_ident, quote_literal or format function to secure variable. security:00000:8:EXECUTE:text type variable is not sanitized Query: 'select ' || a -- ^ Detail: The EXECUTE expression is SQL injection vulnerable. Hint: Use quote_ident, quote_literal or format function to secure variable. (10 rows) drop function foo(text, text); create table testr(a int); create rule testr_rule as on insert to testr do nothing; create or replace function fx_testr() returns void as $$ begin insert into testr values(20); end; $$ language plpgsql; -- allow some rules on tables select fx_testr(); fx_testr ---------- (1 row) select * from plpgsql_check_function_tb('fx_testr'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+---------+--------+------+-------+----------+-------+--------- (0 rows) drop function fx_testr(); drop table testr; create or replace function f() returns void as $$ declare r1 record; r2 record; begin select 10 as a, 20 as b into r1; r2 := json_populate_record(r1, '{}'); raise notice '%', r2.a; end; $$ language plpgsql; select * from plpgsql_check_function('f'); plpgsql_check_function ------------------------ (0 rows) -- fix issue #63 create or replace function distinct_array(arr anyarray) returns anyarray as $$ begin return array(select distinct e from unnest(arr) as e); end; $$ language plpgsql immutable; select plpgsql_check_function('distinct_array(anyarray)'); plpgsql_check_function ------------------------ (0 rows) drop function distinct_array(anyarray); -- tracer test set plpgsql_check.enable_tracer to on; set plpgsql_check.tracer to on; set plpgsql_check.tracer_test_mode = true; -- should not crash do $$ begin raise exception 'raising an exception'; end; $$; NOTICE: #0 ->> start of block inline_code_block (oid=0, tnl=1) NOTICE: #0 <<- end of block (elapsed time=0.010 ms) aborted ERROR: raising an exception CONTEXT: PL/pgSQL function inline_code_block line 3 at RAISE do $$ begin do $_$ begin raise exception 'raising an exception'; end $_$; end; $$; NOTICE: #0 ->> start of block inline_code_block (oid=0, tnl=1) NOTICE: #2 ->> start of block inline_code_block (oid=0, tnl=1) NOTICE: #2 context: SQL statement "do $_$ begin raise exception 'raising an exception'; end $_$" NOTICE: #2 <<- end of block (elapsed time=0.010 ms) aborted NOTICE: #0 <<- end of block (elapsed time=0.010 ms) aborted ERROR: raising an exception CONTEXT: PL/pgSQL function inline_code_block line 3 at RAISE SQL statement "do $_$ begin raise exception 'raising an exception'; end $_$" PL/pgSQL function inline_code_block line 3 at DO create function fx_trace(int) returns int as $$ begin if $1 = 0 then raise exception 'arg is zero'; else return $1; end if; end; $$ language plpgsql; do $$ declare t int; begin t := fx_trace(1); end $$; NOTICE: #0 ->> start of block inline_code_block (oid=0, tnl=1) NOTICE: #1 ->> start of function fx_trace(integer) (oid=0, tnl=1) NOTICE: #1 context: PL/pgSQL function inline_code_block line 4 at assignment NOTICE: #1 "$1" => '1' NOTICE: #1 <<- end of function fx_trace (elapsed time=0.010 ms) NOTICE: #0 <<- end of block (elapsed time=0.010 ms) do $$ declare t int; begin t := fx_trace(0); end $$; NOTICE: #0 ->> start of block inline_code_block (oid=0, tnl=1) NOTICE: #1 ->> start of function fx_trace(integer) (oid=0, tnl=1) NOTICE: #1 context: PL/pgSQL function inline_code_block line 4 at assignment NOTICE: #1 "$1" => '0' NOTICE: #1 <<- end of function fx_trace (elapsed time=0.010 ms) aborted NOTICE: #0 <<- end of block (elapsed time=0.010 ms) aborted ERROR: arg is zero CONTEXT: PL/pgSQL function fx_trace(integer) line 4 at RAISE PL/pgSQL function inline_code_block line 4 at assignment drop function fx_trace; \set VERBOSITY terse create or replace function fxo(a int, b int, c date, d numeric) returns void as $$ begin insert into tracer_tab values(a,b,c,d); end; $$ language plpgsql; create table tracer_tab(a int, b int, c date, d numeric); create or replace function tracer_tab_trg_fx() returns trigger as $$ begin return new; end; $$ language plpgsql; create trigger tracer_tab_trg before insert on tracer_tab for each row execute procedure tracer_tab_trg_fx(); select fxo(10,20,'20200815', 3.14); NOTICE: #0 ->> start of function fxo(integer,integer,date,numeric) (oid=0, tnl=1) NOTICE: #0 "a" => '10', "b" => '20', "c" => '08-15-2020', "d" => '3.14' NOTICE: #2 ->> start of function tracer_tab_trg_fx() (oid=0, tnl=1) NOTICE: #2 context: SQL statement "insert into tracer_tab values(a,b,c,d)" NOTICE: #2 triggered by before row insert trigger NOTICE: #2 "new" => '(10,20,08-15-2020,3.14)' NOTICE: #2 <<- end of function tracer_tab_trg_fx (elapsed time=0.010 ms) NOTICE: #0 <<- end of function fxo (elapsed time=0.010 ms) fxo ----- (1 row) select fxo(11,21,'20200816', 6.28); NOTICE: #0 ->> start of function fxo(integer,integer,date,numeric) (oid=0, tnl=1) NOTICE: #0 "a" => '11', "b" => '21', "c" => '08-16-2020', "d" => '6.28' NOTICE: #2 ->> start of function tracer_tab_trg_fx() (oid=0, tnl=1) NOTICE: #2 context: SQL statement "insert into tracer_tab values(a,b,c,d)" NOTICE: #2 triggered by before row insert trigger NOTICE: #2 "new" => '(11,21,08-16-2020,6.28)' NOTICE: #2 <<- end of function tracer_tab_trg_fx (elapsed time=0.010 ms) NOTICE: #0 <<- end of function fxo (elapsed time=0.010 ms) fxo ----- (1 row) set plpgsql_check.enable_tracer to off; set plpgsql_check.tracer to off; drop table tracer_tab cascade; drop function tracer_tab_trg_fx(); drop function fxo(int, int, date, numeric); create or replace function foo_trg_func() returns trigger as $$ begin -- bad function, RETURN is missing end; $$ language plpgsql; create table foo(a int); create trigger foo_trg before insert for each row execute procedure foo_trg_func(); ERROR: syntax error at or near "for" at character 38 -- should to print error select * from plpgsql_check_function('foo_trg_func', 'foo'); plpgsql_check_function ------------------------------------------------------------ error:2F005:control reached end of function without RETURN (1 row) drop table foo; drop function foo_trg_func(); -- check event trigger function create or replace function f1() returns event_trigger as $$ BEGIN RAISE NOTICE 'test_event_trigger: % %', tg_event, tg_tag; END $$ language plpgsql; select * from plpgsql_check_function_tb('f1()'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+---------+--------+------+-------+----------+-------+--------- (0 rows) -- should fail create or replace function f1() returns event_trigger as $$ BEGIN RAISE NOTICE 'test_event_trigger: % %', tg_event, tg_tagX; END $$ language plpgsql; select * from plpgsql_check_function_tb('f1()'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+---------------------------------+--------+------+-------+----------+---------+--------- f1 | 3 | RAISE | 42703 | column "tg_tagx" does not exist | | | error | 1 | tg_tagX | (1 row) drop function f1(); -- check event trigger function create or replace function f1() returns event_trigger as $$ BEGIN RAISE NOTICE 'test_event_trigger: % %', tg_event, tg_tag; END $$ language plpgsql; select * from plpgsql_check_function('f1()'); plpgsql_check_function ------------------------ (0 rows) -- should fail create or replace function f1() returns event_trigger as $$ BEGIN RAISE NOTICE 'test_event_trigger: % %', tg_event, tg_tagX; END $$ language plpgsql; select * from plpgsql_check_function('f1()'); plpgsql_check_function ----------------------------------------------------- error:42703:3:RAISE:column "tg_tagx" does not exist Query: tg_tagX -- ^ (3 rows) drop function f1(); create table t1tab(a int, b int); create or replace function f1() returns setof t1tab as $$ begin return next (10,20); return; end; $$ language plpgsql; select * from plpgsql_check_function('f1()', performance_warnings => true); plpgsql_check_function ----------------------------------------------------------------------------------------- performance:00000:routine is marked as VOLATILE, should be IMMUTABLE Hint: When you fix this issue, please, recheck other functions that uses this function. (2 rows) create or replace function f1() returns setof t1tab as $$ begin return next (10::numeric,20); return; end; $$ language plpgsql; select * from plpgsql_check_function('f1()', performance_warnings => true); plpgsql_check_function ------------------------------------------------------------------------------------------------ error:42804:3:RETURN NEXT:returned record type does not match expected record type Detail: Returned type numeric does not match expected type integer in column "a" (position 1). (2 rows) create or replace function f1() returns setof t1tab as $$ declare a int; b int; begin return next (a,b); return; end; $$ language plpgsql; select * from plpgsql_check_function('f1()', performance_warnings => true); plpgsql_check_function ----------------------------------------------------------------------------------------- performance:00000:routine is marked as VOLATILE, should be IMMUTABLE Hint: When you fix this issue, please, recheck other functions that uses this function. (2 rows) create or replace function f1() returns setof t1tab as $$ declare a numeric; b int; begin return next (a,b::numeric); return; end; $$ language plpgsql; select * from plpgsql_check_function('f1()', performance_warnings => true); plpgsql_check_function ------------------------------------------------------------------------------------------------ error:42804:4:RETURN NEXT:returned record type does not match expected record type Detail: Returned type numeric does not match expected type integer in column "a" (position 1). (2 rows) drop function f1(); create table t1(a int, b int); create or replace function fx() returns t2 as $$ begin return (10,20,30)::t1; end; $$ language plpgsql; select * from plpgsql_check_function('fx()', performance_warnings => true); plpgsql_check_function ---------------------------------------------------- error:42846:3:RETURN:cannot cast type record to t1 Query: (10,20,30)::t1 -- ^ Detail: Input has too many columns. (4 rows) drop function fx(); drop table t1tab; drop table t1; create or replace function fx() returns void as $$ begin assert exists(select * from foo); assert false, (select boo from boo limit 1); end; $$ language plpgsql; select * from plpgsql_check_function('fx()', fatal_errors => false); plpgsql_check_function ---------------------------------------------------- error:42P01:3:ASSERT:relation "foo" does not exist Query: exists(select * from foo) -- ^ error:42P01:4:ASSERT:relation "boo" does not exist Query: (select boo from boo limit 1) -- ^ (6 rows) create or replace function ml_trg() returns trigger as $$ #option dump declare begin if TG_OP = 'INSERT' then if NEW.status_from IS NULL then begin -- performance issue only select status into NEW.status_from from pa where pa_id = NEW.pa_id; -- nonexist target value select status into NEW.status_from_xxx from pa where pa_id = NEW.pa_id; exception when DATA_EXCEPTION then new.status_from := 'DE'; end; end if; end if; if TG_OP = 'DELETE' then return OLD; else return NEW; end if; exception when OTHERS then NULL; if TG_OP = 'DELETE' then return OLD; else return NEW; end if; end; $$ language plpgsql; select * from plpgsql_check_function('ml_trg()', 'ml', performance_warnings := true); plpgsql_check_function -------------------------------------------------------------------------- error:42703:13:SQL statement:record "new" has no field "status_from_xxx" (1 row) create or replace function fx2() returns void as $$ declare _pa pa; begin select pa.id into _pa.id from pa limit 1; select pa.pa_id into _pa.pa_id from pa limit 1; end; $$ language plpgsql; select * from plpgsql_check_function('fx2()', performance_warnings := true); plpgsql_check_function --------------------------------------------------------- warning extra:00000:2:DECLARE:never read variable "_pa" (1 row) drop function fx2(); create or replace function fx2() returns void as $$ declare _pa pa; begin _pa.id := (select pa.id from pa limit 1); _pa.pa_id := (select pa.pa_id from pa limit 1); end; $$ language plpgsql; select * from plpgsql_check_function('fx2()', performance_warnings := true); plpgsql_check_function --------------------------------------------------------- warning extra:00000:2:DECLARE:never read variable "_pa" (1 row) drop function fx2(); create type _exception_type as ( state text, message text, detail text); create or replace function f1() returns void as $$ declare _exception record; begin _exception := NULL::_exception_type; exception when others then get stacked diagnostics _exception.state = RETURNED_SQLSTATE, _exception.message = MESSAGE_TEXT, _exception.detail = PG_EXCEPTION_DETAIL, _exception.hint = PG_EXCEPTION_HINT; end; $$ language plpgsql; select f1(); f1 ---- (1 row) select * from plpgsql_check_function_tb('f1()'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-------------------------+----------+-----------------------------------------+--------+------+-------+----------+-------+--------- f1 | 7 | GET STACKED DIAGNOSTICS | 42703 | record "_exception" has no field "hint" | | | error | | | (1 row) create or replace function f1() returns void as $$ declare _exception _exception_type; begin _exception := NULL::_exception_type; exception when others then get stacked diagnostics _exception.state = RETURNED_SQLSTATE, _exception.message = MESSAGE_TEXT, _exception.detail = PG_EXCEPTION_DETAIL; end; $$ language plpgsql; select f1(); f1 ---- (1 row) select * from plpgsql_check_function_tb('f1()'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+----------------------------------+--------+------+---------------+----------+-------+--------- f1 | 3 | DECLARE | 00000 | never read variable "_exception" | | | warning extra | | | (1 row) drop function f1(); drop type _exception_type; create type _exception_type as ( state text, message text, detail text); create or replace function f1() returns void as $$ declare _exception record; begin _exception := NULL::_exception_type; exception when others then get stacked diagnostics _exception.state = RETURNED_SQLSTATE, _exception.message = MESSAGE_TEXT, _exception.detail = PG_EXCEPTION_DETAIL, _exception.hint = PG_EXCEPTION_HINT; end; $$ language plpgsql; select f1(); f1 ---- (1 row) select * from plpgsql_check_function('f1()'); plpgsql_check_function ------------------------------------------------------------------------------- error:42703:7:GET STACKED DIAGNOSTICS:record "_exception" has no field "hint" (1 row) drop function f1(); drop type _exception_type; create table footab(a int, b int, c int); create or replace function footab_trig_func() returns trigger as $$ declare x int; begin if false then -- should be ok; select count(*) from newtab into x; -- should fail; select count(*) from newtab where d = 10 into x; end if; return null; end; $$ language plpgsql; select * from plpgsql_check_function('footab_trig_func','footab', newtable := 'newtab'); plpgsql_check_function ------------------------------------------------------- error:42703:9:SQL statement:column "d" does not exist Query: select count(*) from newtab where d = 10 -- ^ (3 rows) drop table footab; drop function footab_trig_func(); create or replace function df1(anyelement) returns anyelement as $$ begin return $1; end; $$ language plpgsql; create or replace function df2(anyelement, jsonb) returns anyelement as $$ begin return $1; end; $$ language plpgsql; create or replace function t1() returns void as $$ declare r record; begin r := df1(r); end; $$ language plpgsql; select * from plpgsql_check_function('t1()'); plpgsql_check_function ------------------------ (0 rows) create or replace function t1() returns void as $$ declare r record; begin r := df2(r, '{}'); end; $$ language plpgsql; select * from plpgsql_check_function('t1()'); plpgsql_check_function ------------------------ (0 rows) create or replace function t1() returns void as $$ declare r1 record; r2 record; begin select 10 as a, 20 as b into r1; r2 := df1(r1); raise notice '%', r2.a; end; $$ language plpgsql; select * from plpgsql_check_function('t1()'); plpgsql_check_function ------------------------ (0 rows) create or replace function t1() returns void as $$ declare r1 record; r2 record; begin select 10 as a, 20 as b into r1; r2 := df2(r1, '{}'); raise notice '%', r2.a; end; $$ language plpgsql; select * from plpgsql_check_function('t1()'); plpgsql_check_function ------------------------ (0 rows) create or replace function df1(anyelement) returns anyelement as $$ select $1 $$ language sql; create or replace function df22(jsonb, anyelement) returns anyelement as $$ select $2; $$ language sql; create or replace function t1() returns void as $$ declare r1 record; r2 record; begin select 10 as a, 20 as b into r1; r2 := df1(r1); raise notice '%', r2.a; end; $$ language plpgsql; select * from plpgsql_check_function('t1()'); plpgsql_check_function ------------------------ (0 rows) create or replace function t1() returns void as $$ declare r1 record; r2 record; begin select 10 as a, 20 as b into r1; r2 := df22('{}', r1); raise notice '%', r2.a; end; $$ language plpgsql; select * from plpgsql_check_function('t1()'); plpgsql_check_function ------------------------ (0 rows) drop function df1(anyelement); drop function df2(anyelement, jsonb); drop function df22(jsonb, anyelement); drop function t1(); create or replace function dyntest() returns void as $$ begin execute 'drop table if exists xxx; create table xxx(a int)'; end; $$ language plpgsql; -- should be ok select * from plpgsql_check_function('dyntest'); plpgsql_check_function ------------------------ (0 rows) create or replace function dyntest() returns void as $$ declare x int; begin execute 'drop table if exists xxx; create table xxx(a int)' into x; end; $$ language plpgsql; -- should to report error select * from plpgsql_check_function('dyntest'); plpgsql_check_function ------------------------------------------------------- error:XX000:4:EXECUTE:expression does not return data (1 row) drop function dyntest(); -- should to report error create type typ2 as (a int, b int); create or replace function broken_into() returns void as $$ declare v typ2; begin -- should to fail select (10,20)::typ2 into v; -- should be ok select ((10,20)::typ2).* into v; -- should to fail execute 'select (10,20)::typ2' into v; -- should be ok execute 'select ((10,20)::typ2).*' into v; end; $$ language plpgsql; select * from plpgsql_check_function('broken_into', fatal_errors => false); plpgsql_check_function ------------------------------------------------------------------------------------------------------------ error:42804:5:SQL statement:cannot cast composite value of "typ2" type to a scalar value of "integer" type Query: select (10,20)::typ2 -- ^ Context: at SQL statement to variable "v" declared on line 2 warning:00000:5:SQL statement:too few attributes for composite variable Query: select (10,20)::typ2 -- ^ Context: at SQL statement to variable "v" declared on line 2 error:42804:9:EXECUTE:cannot cast composite value of "typ2" type to a scalar value of "integer" type Context: at EXECUTE to variable "v" declared on line 2 warning:00000:9:EXECUTE:too few attributes for composite variable Context: at EXECUTE to variable "v" declared on line 2 warning extra:00000:2:DECLARE:never read variable "v" (13 rows) drop function broken_into(); drop type typ2; -- check output in xml or json formats CREATE OR REPLACE FUNCTION test_function() RETURNS void LANGUAGE plpgsql AS $function$ begin insert into non_existing_table values (1); end $function$; select * from plpgsql_check_function('test_function', format => 'xml'); plpgsql_check_function ---------------------------------------------------------------------------- + + error + 42P01 + relation "non_existing_table" does not exist + SQL statement + insert into non_existing_table values (1)+ + (1 row) select * from plpgsql_check_function('test_function', format => 'json'); plpgsql_check_function ------------------------------------------------------------------- { + "issues": [ + { + "level": "error", + "message":"relation \"non_existing_table\" does not exist",+ "statement": { + "lineNumber": "3", + "text": "SQL statement" + }, + "query": { + "position": "13", + "text": "insert into non_existing_table values (1)" + }, + "sqlState": "42P01" + } + ] + } (1 row) drop function test_function(); -- test settype pragma create or replace function test_function() returns void as $$ declare r record; begin raise notice '%', r.a; end; $$ language plpgsql; -- should to detect error select * from plpgsql_check_function('test_function'); plpgsql_check_function ---------------------------------------------------------------------------- error:55000:4:RAISE:record "r" is not assigned yet Detail: The tuple structure of a not-yet-assigned record is indeterminate. Context: PL/pgSQL expression "r.a" (3 rows) create type ctype as (a int, b int); create or replace function test_function() returns void as $$ declare r record; begin perform plpgsql_check_pragma('type: r ctype'); raise notice '%', r.a; end; $$ language plpgsql; -- should to be ok select * from plpgsql_check_function('test_function'); plpgsql_check_function ------------------------ (0 rows) create or replace function test_function() returns void as $$ <>declare r record; begin perform plpgsql_check_pragma('type: x.r public."ctype"'); raise notice '%', r.a; end; $$ language plpgsql; -- should to be ok select * from plpgsql_check_function('test_function'); plpgsql_check_function ------------------------ (0 rows) create or replace function test_function() returns void as $$ <>declare r record; begin perform plpgsql_check_pragma('type: "x".r (a int, b int)'); raise notice '%', r.a; end; $$ language plpgsql; -- should to be ok select * from plpgsql_check_function('test_function'); plpgsql_check_function ------------------------ (0 rows) create or replace function test_function() returns void as $$ <>declare r record; begin perform plpgsql_check_pragma('type: "x".r (a int, b int'); raise notice '%', r.a; end; $$ language plpgsql; -- should to be ok select * from plpgsql_check_function('test_function'); WARNING: Pragma "type" on line 4 is not processed. plpgsql_check_function ---------------------------------------------------------------------------- error:55000:5:RAISE:record "r" is not assigned yet Detail: The tuple structure of a not-yet-assigned record is indeterminate. Context: PL/pgSQL expression "r.a" (3 rows) create or replace function test_function() returns void as $$ <>declare r record; begin perform plpgsql_check_pragma('type: "x".r (a int, b int)x'); raise notice '%', r.a; end; $$ language plpgsql; -- should to be ok select * from plpgsql_check_function('test_function'); WARNING: Pragma "type" on line 4 is not processed. plpgsql_check_function ---------------------------------------------------------------------------- error:55000:5:RAISE:record "r" is not assigned yet Detail: The tuple structure of a not-yet-assigned record is indeterminate. Context: PL/pgSQL expression "r.a" (3 rows) drop function test_function(); drop type ctype; create or replace function test_function() returns void as $$ declare r pg_class; begin create temp table foo(like pg_class); select * from foo into r; end; $$ language plpgsql; -- should to raise an error select * from plpgsql_check_function('test_function'); plpgsql_check_function ----------------------------------------------------------- error:42P01:5:SQL statement:relation "foo" does not exist Query: select * from foo -- ^ (3 rows) create or replace function test_function() returns void as $$ declare r record; begin create temp table foo(like pg_class); perform plpgsql_check_pragma('table: foo(like pg_class)'); select * from foo into r; raise notice '%', r.relname; end; $$ language plpgsql; -- should be ok select * from plpgsql_check_function('test_function'); plpgsql_check_function ------------------------ (0 rows) drop function test_function(); -- now plpgsql_check can do some other checks when statement EXECUTE -- contains only format function with constant fmt. create or replace function test_function() returns void as $$ begin execute format('create table zzz %I(a int, b int)', 'zzz'); end; $$ language plpgsql; -- should to detect bad expression select * from plpgsql_check_function('test_function'); plpgsql_check_function ----------------------------------------------------- error:42601:3:EXECUTE:syntax error at or near "zzz" Query: create table zzz zzz(a int, b int) (2 rows) -- should to correctly detect type create or replace function test_function() returns void as $$ declare r record; begin execute format('select %L::date + 1 as x', current_date) into r; raise notice '%', extract(dow from r.x); end; $$ language plpgsql; -- should be ok select * from plpgsql_check_function('test_function'); plpgsql_check_function ------------------------ (0 rows) -- should not to crash create or replace function test_function() returns void as $$ declare r record; begin r := null; end; $$ language plpgsql; select * from plpgsql_check_function('test_function'); plpgsql_check_function ------------------------------------------------------- warning extra:00000:2:DECLARE:never read variable "r" (1 row) drop function test_function(); -- ignores syntax errors when literals placehodlers are used create function test_function() returns void as $$ begin execute format('do %L', 'begin end'); end $$ language plpgsql; select * from plpgsql_check_function('test_function'); plpgsql_check_function ------------------------ (0 rows) drop function test_function(); load 'plpgsql_check'; drop type testtype cascade; create type testtype as (a int, b int); create function test_function() returns record as $$ declare r record; begin r := (10,20); if false then return r; end if; return null; end; $$ language plpgsql; create function test_function33() returns record as $$ declare r testtype; begin r := (10,20); if false then return r; end if; return null; end; $$ language plpgsql; -- should not to raise false alarm due check against fake result type select plpgsql_check_function('test_function'); plpgsql_check_function ------------------------ (0 rows) select plpgsql_check_function('test_function33'); plpgsql_check_function ------------------------ (0 rows) -- try to check in passive mode set plpgsql_check.mode = 'every_start'; select test_function(); test_function --------------- (1 row) select test_function33(); test_function33 ----------------- (1 row) select * from test_function() as (a int, b int); a | b ---+--- | (1 row) select * from test_function33() as (a int, b int); a | b ---+--- | (1 row) -- should to identify error select * from test_function() as (a int, b int, c int); ERROR: returned record type does not match expected record type select * from test_function33() as (a int, b int, c int); ERROR: returned record type does not match expected record type drop function test_function(); drop function test_function33(); drop type testtype; set plpgsql_check.mode to default; -- should not to raise false alarm create type c1 as ( a text ); create table t1 ( a c1, b c1 ); insert into t1 (values ('(abc)', '(def)')); alter table t1 drop column a; create or replace function test_function() returns t1 as $$ declare myrow t1%rowtype; begin select * into myrow from t1 limit 1; return myrow; end; $$ language plpgsql; select * from test_function(); b ------- (def) (1 row) select * from plpgsql_check_function('public.test_function()'); plpgsql_check_function ------------------------ (0 rows) drop function test_function(); drop table t1; drop type c1; -- compatibility warnings create or replace function foo01() returns refcursor as $$ declare c cursor for select 1; r refcursor; begin open c; r := c; return r; end; $$ language plpgsql; -- no warnings select * from plpgsql_check_function('foo01', compatibility_warnings => true); plpgsql_check_function ------------------------ (0 rows) create or replace function foo01() returns refcursor as $$ declare c cursor for select 1; r refcursor; begin open c; r := 'c'; return r; end; $$ language plpgsql; -- warning select * from plpgsql_check_function('foo01', extra_warnings => false, compatibility_warnings => true); plpgsql_check_function ----------------------------------------------------------------------------------- compatibility:00000:7:assignment:obsolete setting of refcursor or cursor variable Detail: Internal name of cursor should not be specified by users. Context: at assignment to variable "r" declared on line 4 (3 rows) create or replace function foo01() returns refcursor as $$ declare c cursor for select 1; r refcursor; begin open c; r := c; return 'c'; end; $$ language plpgsql; -- warning select * from plpgsql_check_function('foo01', extra_warnings => false, compatibility_warnings => true); plpgsql_check_function -------------------------------------------------------------------------------------- compatibility:00000:8:RETURN:obsolete setting of refcursor or cursor variable Detail: Internal name of cursor should not be specified by users. warning:42804:8:RETURN:target type is different type than source type Detail: cast "text" value to "refcursor" type Hint: The input expression type does not have an assignment cast to the target type. Context: at RETURN (6 rows) -- pragma sequence test create or replace function test_function() returns void as $$ begin perform plpgsql_check_pragma('sequence: xx'); perform nextval('pg_temp.xy'); perform nextval('pg_temp.xx'); end $$ language plpgsql; select * from plpgsql_check_function('test_function'); plpgsql_check_function ------------------------------------------------------------ error:42P01:4:PERFORM:relation "pg_temp.xy" does not exist Query: SELECT nextval('pg_temp.xy') -- ^ (3 rows) drop function test_function(); create table t1(x int); create or replace function f1_trg() returns trigger as $$ declare x int; begin return x; end; $$ language plpgsql; create trigger t1_f1 before insert on t1 for each row execute procedure f1_trg(); -- raise error select * from plpgsql_check_function('f1_trg', 't1'); plpgsql_check_function ----------------------------------------------------------------------------------------------- error:42804:4:RETURN:cannot return non-composite value from function returning composite type (1 row) drop trigger t1_f1 on t1; drop table t1; drop function f1_trg; create function test_function() returns void as $$ declare a int; b int; c int; d char; begin c := a + d; end; $$ language plpgsql; -- only b should be marked as unused variable select * from plpgsql_check_function('test_function', fatal_errors := false); plpgsql_check_function --------------------------------------------------------------------------------------------------------- error:42883:6:assignment:operator does not exist: integer + character Query: c := a + d -- ^ Hint: No operator matches the given name and argument types. You might need to add explicit type casts. Context: at assignment to variable "c" declared on line 4 warning:00000:3:DECLARE:unused variable "b" warning extra:00000:4:DECLARE:never read variable "c" (7 rows) drop function test_function(); -- from plpgsql_check_active-12 create or replace procedure proc(a int) as $$ begin end; $$ language plpgsql; call proc(10); select * from plpgsql_check_function('proc(int)'); plpgsql_check_function ------------------------------------------ warning extra:00000:unused parameter "a" (1 row) create or replace procedure testproc() as $$ begin call proc(10); end; $$ language plpgsql; call testproc(); select * from plpgsql_check_function('testproc()'); plpgsql_check_function ------------------------ (0 rows) -- should to fail create or replace procedure testproc() as $$ begin call proc((select count(*) from pg_class)); end; $$ language plpgsql; call testproc(); ERROR: cannot use subquery in CALL argument at character 11 select * from plpgsql_check_function('testproc()'); plpgsql_check_function --------------------------------------------------------- error:0A000:3:CALL:cannot use subquery in CALL argument Query: call proc((select count(*) from pg_class)) -- ^ (3 rows) drop procedure proc(int); create procedure proc(in a int, inout b int, in c int) as $$ begin end; $$ language plpgsql; select * from plpgsql_check_function('proc(int,int, int)'); plpgsql_check_function ------------------------------------------------- warning extra:00000:unused parameter "a" warning extra:00000:unused parameter "b" warning extra:00000:unused parameter "c" warning extra:00000:unmodified OUT variable "b" (4 rows) create or replace procedure proc(in a int, inout b int, in c int) as $$ begin b := a + c; end; $$ language plpgsql; select * from plpgsql_check_function('proc(int,int, int)'); plpgsql_check_function ------------------------ (0 rows) create or replace procedure testproc() as $$ declare r int; begin call proc(10, r, 20); end; $$ language plpgsql; call testproc(); select * from plpgsql_check_function('testproc()'); plpgsql_check_function ------------------------ (0 rows) -- should to fail create or replace procedure testproc() as $$ declare r int; begin call proc(10, r + 10, 20); end; $$ language plpgsql; call testproc(); ERROR: procedure parameter "b" is an output parameter but corresponding argument is not writable select * from plpgsql_check_function('testproc()'); plpgsql_check_function -------------------------------------------------------------------------------------------------------------- error:42601:4:CALL:procedure parameter "b" is an output parameter but corresponding argument is not writable (1 row) create or replace procedure testproc(inout r int) as $$ begin call proc(10, r, 20); end; $$ language plpgsql; call testproc(10); r ---- 30 (1 row) select * from plpgsql_check_function('testproc(int)'); plpgsql_check_function ------------------------ (0 rows) drop procedure testproc(int); -- should to raise warnings create or replace procedure testproc2(in p1 int, inout p2 int, in p3 int, inout p4 int) as $$ begin raise notice '% %', p1, p3; end; $$ language plpgsql; select * from plpgsql_check_function('testproc2'); plpgsql_check_function -------------------------------------------------- warning extra:00000:unused parameter "p2" warning extra:00000:unused parameter "p4" warning extra:00000:unmodified OUT variable "p2" warning extra:00000:unmodified OUT variable "p4" (4 rows) drop procedure testproc2; -- should be ok create or replace procedure testproc3(in p1 int, inout p2 int, in p3 int, inout p4 int) as $$ begin p2 := p1; p4 := p3; end; $$ language plpgsql; select * from plpgsql_check_function('testproc3'); plpgsql_check_function ------------------------ (0 rows) drop procedure testproc3; /* * Test pragma */ create or replace function test_pragma() returns void language plpgsql as $$ declare r record; begin perform plpgsql_check_pragma('disable:check'); raise notice '%', r.y; perform plpgsql_check_pragma('enable:check'); select 10 as a, 20 as b into r; raise notice '%', r.a; raise notice '%', r.x; end; $$; select * from plpgsql_check_function('test_pragma'); plpgsql_check_function ------------------------------------------------- error:42703:9:RAISE:record "r" has no field "x" Context: PL/pgSQL expression "r.x" (2 rows) create or replace function test_pragma() returns void language plpgsql as $$ declare r record; begin if false then -- check is disabled just for if body perform plpgsql_check_pragma('disable:check'); raise notice '%', r.y; end if; select 10 as a, 20 as b into r; raise notice '%', r.a; raise notice '%', r.x; end; $$; select * from plpgsql_check_function('test_pragma'); plpgsql_check_function -------------------------------------------------- error:42703:11:RAISE:record "r" has no field "x" Context: PL/pgSQL expression "r.x" (2 rows) drop function test_pragma(); create or replace function nested_trace_test(a int) returns int as $$ begin return a + 1; end; $$ language plpgsql; create or replace function trace_test(b int) returns int as $$ declare r int default 0; begin for i in 1..b loop r := nested_trace_test(r); end loop; return r; end; $$ language plpgsql; select trace_test(3); trace_test ------------ 3 (1 row) set plpgsql_check.enable_tracer to on; set plpgsql_check.tracer to on; set plpgsql_check.tracer_test_mode = true; select trace_test(3); NOTICE: #0 ->> start of function trace_test(integer) (oid=0, tnl=1) NOTICE: #0 "b" => '3' NOTICE: #1 ->> start of function nested_trace_test(integer) (oid=0, tnl=1) NOTICE: #1 context: PL/pgSQL function trace_test(integer) line 6 at assignment NOTICE: #1 "a" => '0' NOTICE: #1 <<- end of function nested_trace_test (elapsed time=0.010 ms) NOTICE: #1 ->> start of function nested_trace_test(integer) (oid=0, tnl=1) NOTICE: #1 context: PL/pgSQL function trace_test(integer) line 6 at assignment NOTICE: #1 "a" => '1' NOTICE: #1 <<- end of function nested_trace_test (elapsed time=0.010 ms) NOTICE: #1 ->> start of function nested_trace_test(integer) (oid=0, tnl=1) NOTICE: #1 context: PL/pgSQL function trace_test(integer) line 6 at assignment NOTICE: #1 "a" => '2' NOTICE: #1 <<- end of function nested_trace_test (elapsed time=0.010 ms) NOTICE: #0 <<- end of function trace_test (elapsed time=0.010 ms) trace_test ------------ 3 (1 row) set plpgsql_check.tracer_verbosity TO verbose; select trace_test(3); NOTICE: #0 ->> start of function trace_test(integer) (oid=0, tnl=1) NOTICE: #0 "b" => '3' NOTICE: #0.1 3 --> start of statement block (tnl=1) NOTICE: #0.2 4 --> start of FOR with integer loop variable (tnl=1) NOTICE: #0.3 6 --> start of assignment r := nested_trace_test(r) (tnl=1) NOTICE: #0.3 "r" => '0' NOTICE: #1 ->> start of function nested_trace_test(integer) (oid=0, tnl=1) NOTICE: #1 context: PL/pgSQL function trace_test(integer) line 6 at assignment NOTICE: #1 "a" => '0' NOTICE: #1.1 2 --> start of statement block (tnl=1) NOTICE: #1.2 3 --> start of RETURN (expr='a + 1') (tnl=1) NOTICE: #1.2 "a" => '0' NOTICE: #1.2 <-- end of RETURN (elapsed time=0.010 ms) NOTICE: #1.1 <-- end of statement block (elapsed time=0.010 ms) NOTICE: #1 <<- end of function nested_trace_test (elapsed time=0.010 ms) NOTICE: #0.3 <-- end of assignment (elapsed time=0.010 ms) NOTICE: #0.3 "r" => '1' NOTICE: #0.3 6 --> start of assignment r := nested_trace_test(r) (tnl=1) NOTICE: #0.3 "r" => '1' NOTICE: #1 ->> start of function nested_trace_test(integer) (oid=0, tnl=1) NOTICE: #1 context: PL/pgSQL function trace_test(integer) line 6 at assignment NOTICE: #1 "a" => '1' NOTICE: #1.1 2 --> start of statement block (tnl=1) NOTICE: #1.2 3 --> start of RETURN (expr='a + 1') (tnl=1) NOTICE: #1.2 "a" => '1' NOTICE: #1.2 <-- end of RETURN (elapsed time=0.010 ms) NOTICE: #1.1 <-- end of statement block (elapsed time=0.010 ms) NOTICE: #1 <<- end of function nested_trace_test (elapsed time=0.010 ms) NOTICE: #0.3 <-- end of assignment (elapsed time=0.010 ms) NOTICE: #0.3 "r" => '2' NOTICE: #0.3 6 --> start of assignment r := nested_trace_test(r) (tnl=1) NOTICE: #0.3 "r" => '2' NOTICE: #1 ->> start of function nested_trace_test(integer) (oid=0, tnl=1) NOTICE: #1 context: PL/pgSQL function trace_test(integer) line 6 at assignment NOTICE: #1 "a" => '2' NOTICE: #1.1 2 --> start of statement block (tnl=1) NOTICE: #1.2 3 --> start of RETURN (expr='a + 1') (tnl=1) NOTICE: #1.2 "a" => '2' NOTICE: #1.2 <-- end of RETURN (elapsed time=0.010 ms) NOTICE: #1.1 <-- end of statement block (elapsed time=0.010 ms) NOTICE: #1 <<- end of function nested_trace_test (elapsed time=0.010 ms) NOTICE: #0.3 <-- end of assignment (elapsed time=0.010 ms) NOTICE: #0.3 "r" => '3' NOTICE: #0.2 <-- end of FOR with integer loop variable (elapsed time=0.010 ms) NOTICE: #0.4 8 --> start of RETURN (tnl=1) NOTICE: #0.4 "r" => '3' NOTICE: #0.4 <-- end of RETURN (elapsed time=0.010 ms) NOTICE: #0.1 <-- end of statement block (elapsed time=0.010 ms) NOTICE: #0 <<- end of function trace_test (elapsed time=0.010 ms) trace_test ------------ 3 (1 row) create or replace function trace_test(b int) returns int as $$ declare r int default 0; begin for i in 1..b loop perform plpgsql_check_pragma('disable:tracer'); r := nested_trace_test(r); end loop; return r; end; $$ language plpgsql; select trace_test(3); NOTICE: #0 ->> start of function trace_test(integer) (oid=0, tnl=1) NOTICE: #0 "b" => '3' NOTICE: #0.1 3 --> start of statement block (tnl=1) NOTICE: #0.2 4 --> start of FOR with integer loop variable (tnl=1) NOTICE: #0.3 6 --> start of perform plpgsql_check_pragma('disable: .. (tnl=1) NOTICE: #0.3 <-- end of PERFORM (elapsed time=0.010 ms) NOTICE: #0.2 <-- end of FOR with integer loop variable (elapsed time=0.010 ms) NOTICE: #0.5 9 --> start of RETURN (tnl=1) NOTICE: #0.5 "r" => '3' NOTICE: #0.5 <-- end of RETURN (elapsed time=0.010 ms) NOTICE: #0.1 <-- end of statement block (elapsed time=0.010 ms) NOTICE: #0 <<- end of function trace_test (elapsed time=0.010 ms) trace_test ------------ 3 (1 row) create or replace function nested_trace_test(a int) returns int as $$ begin perform plpgsql_check_pragma('enable:tracer'); return a + 1; end; $$ language plpgsql; select trace_test(3); NOTICE: #0 ->> start of function trace_test(integer) (oid=0, tnl=1) NOTICE: #0 "b" => '3' NOTICE: #0.1 3 --> start of statement block (tnl=1) NOTICE: #0.2 4 --> start of FOR with integer loop variable (tnl=1) NOTICE: #0.3 6 --> start of perform plpgsql_check_pragma('disable: .. (tnl=1) NOTICE: #0.3 <-- end of PERFORM (elapsed time=0.010 ms) NOTICE: #1.3 4 --> start of RETURN (expr='a + 1') (tnl=1) NOTICE: #1.3 "a" => '0' NOTICE: #1.3 <-- end of RETURN (elapsed time=0.010 ms) NOTICE: #1.3 4 --> start of RETURN (expr='a + 1') (tnl=1) NOTICE: #1.3 "a" => '1' NOTICE: #1.3 <-- end of RETURN (elapsed time=0.010 ms) NOTICE: #1.3 4 --> start of RETURN (expr='a + 1') (tnl=1) NOTICE: #1.3 "a" => '2' NOTICE: #1.3 <-- end of RETURN (elapsed time=0.010 ms) NOTICE: #0.2 <-- end of FOR with integer loop variable (elapsed time=0.010 ms) NOTICE: #0.5 9 --> start of RETURN (tnl=1) NOTICE: #0.5 "r" => '3' NOTICE: #0.5 <-- end of RETURN (elapsed time=0.010 ms) NOTICE: #0.1 <-- end of statement block (elapsed time=0.010 ms) NOTICE: #0 <<- end of function trace_test (elapsed time=0.010 ms) trace_test ------------ 3 (1 row) drop function trace_test(int); drop function nested_trace_test(int); create or replace function trace_test(int) returns int as $$ declare r int default 0; begin for i in 1..$1 loop r := r + 1; end loop; r := r + 10; return r; end; $$ language plpgsql; select trace_test(4); NOTICE: #0 ->> start of function trace_test(integer) (oid=0, tnl=1) NOTICE: #0 "$1" => '4' NOTICE: #0.1 3 --> start of statement block (tnl=1) NOTICE: #0.2 4 --> start of FOR with integer loop variable (tnl=1) NOTICE: #0.3 5 --> start of assignment r := r + 1 (tnl=1) NOTICE: #0.3 "r" => '0' NOTICE: #0.3 <-- end of assignment (elapsed time=0.010 ms) NOTICE: #0.3 "r" => '1' NOTICE: #0.3 5 --> start of assignment r := r + 1 (tnl=1) NOTICE: #0.3 "r" => '1' NOTICE: #0.3 <-- end of assignment (elapsed time=0.010 ms) NOTICE: #0.3 "r" => '2' NOTICE: #0.3 5 --> start of assignment r := r + 1 (tnl=1) NOTICE: #0.3 "r" => '2' NOTICE: #0.3 <-- end of assignment (elapsed time=0.010 ms) NOTICE: #0.3 "r" => '3' NOTICE: #0.3 5 --> start of assignment r := r + 1 (tnl=1) NOTICE: #0.3 "r" => '3' NOTICE: #0.3 <-- end of assignment (elapsed time=0.010 ms) NOTICE: #0.3 "r" => '4' NOTICE: #0.2 <-- end of FOR with integer loop variable (elapsed time=0.010 ms) NOTICE: #0.4 7 --> start of assignment r := r + 10 (tnl=1) NOTICE: #0.4 "r" => '4' NOTICE: #0.4 <-- end of assignment (elapsed time=0.010 ms) NOTICE: #0.4 "r" => '14' NOTICE: #0.5 8 --> start of RETURN (tnl=1) NOTICE: #0.5 "r" => '14' NOTICE: #0.5 <-- end of RETURN (elapsed time=0.010 ms) NOTICE: #0.1 <-- end of statement block (elapsed time=0.010 ms) NOTICE: #0 <<- end of function trace_test (elapsed time=0.010 ms) trace_test ------------ 14 (1 row) create or replace function trace_test(int) returns int as $$ declare r int default 0; begin for i in 1..$1 loop perform plpgsql_check_pragma('disable:tracer'); r := r + 1; end loop; r := r + 10; return r; end; $$ language plpgsql; select trace_test(4); NOTICE: #0 ->> start of function trace_test(integer) (oid=0, tnl=1) NOTICE: #0 "$1" => '4' NOTICE: #0.1 3 --> start of statement block (tnl=1) NOTICE: #0.2 4 --> start of FOR with integer loop variable (tnl=1) NOTICE: #0.3 5 --> start of perform plpgsql_check_pragma('disable: .. (tnl=1) NOTICE: #0.3 <-- end of PERFORM (elapsed time=0.010 ms) NOTICE: #0.2 <-- end of FOR with integer loop variable (elapsed time=0.010 ms) NOTICE: #0.5 8 --> start of assignment r := r + 10 (tnl=1) NOTICE: #0.5 "r" => '4' NOTICE: #0.5 <-- end of assignment (elapsed time=0.010 ms) NOTICE: #0.5 "r" => '14' NOTICE: #0.6 9 --> start of RETURN (tnl=1) NOTICE: #0.6 "r" => '14' NOTICE: #0.6 <-- end of RETURN (elapsed time=0.010 ms) NOTICE: #0.1 <-- end of statement block (elapsed time=0.010 ms) NOTICE: #0 <<- end of function trace_test (elapsed time=0.010 ms) trace_test ------------ 14 (1 row) create or replace function trace_test(int) returns int as $$ declare r int default 0; begin perform plpgsql_check_pragma('disable:tracer'); for i in 1..$1 loop r := r + 1; end loop; perform plpgsql_check_pragma('enable:tracer'); r := r + 10; return r; end; $$ language plpgsql; select trace_test(4); NOTICE: #0 ->> start of function trace_test(integer) (oid=0, tnl=1) NOTICE: #0 "$1" => '4' NOTICE: #0.1 3 --> start of statement block (tnl=1) NOTICE: #0.2 4 --> start of perform plpgsql_check_pragma('disable: .. (tnl=1) NOTICE: #0.2 <-- end of PERFORM (elapsed time=0.010 ms) NOTICE: #0.6 12 --> start of assignment r := r + 10 (tnl=1) NOTICE: #0.6 "r" => '4' NOTICE: #0.6 <-- end of assignment (elapsed time=0.010 ms) NOTICE: #0.6 "r" => '14' NOTICE: #0.7 13 --> start of RETURN (tnl=1) NOTICE: #0.7 "r" => '14' NOTICE: #0.7 <-- end of RETURN (elapsed time=0.010 ms) NOTICE: #0.1 <-- end of statement block (elapsed time=0.010 ms) NOTICE: #0 <<- end of function trace_test (elapsed time=0.010 ms) trace_test ------------ 14 (1 row) drop function trace_test(int); create or replace function public.fx1() returns table(i integer, j integer, found boolean) as $$ begin for i in 1..10 loop for j in 1..10 loop return next; end loop; end loop; end; $$ language plpgsql immutable; select * from plpgsql_check_function('fx1'); plpgsql_check_function -------------------------------------------------------------------------- warning:00000:2:statement block:parameter "found" is shadowed Detail: Local auto variable shadows function parameter. warning:00000:3:FOR with integer loop variable:parameter "i" is shadowed Detail: Local auto variable shadows function parameter. warning:00000:5:FOR with integer loop variable:parameter "j" is shadowed Detail: Local auto variable shadows function parameter. warning extra:00000:unmodified OUT variable "i" warning extra:00000:unmodified OUT variable "j" warning extra:00000:unmodified OUT variable "found" (9 rows) drop function fx1; -- tracer test set plpgsql_check.enable_tracer to on; select plpgsql_check_tracer(true); NOTICE: tracer is active NOTICE: tracer verbosity is verbose plpgsql_check_tracer ---------------------- t (1 row) create role regress_plpgsql_check_test_role; DO $$ begin begin -- should to fail create role regress_plpgsql_check_test_role; exception when duplicate_object then -- Role already exists -- the exception handler is empty (#156) end; end; $$; NOTICE: #0 ->> start of block inline_code_block (oid=0, tnl=1) NOTICE: #0.1 2 --> start of statement block (tnl=1) NOTICE: #0.2 3 --> start of statement block (tnl=1) NOTICE: #0.3 5 --> start of SQL statement (query='create role regress_plpgsql_ch ..') (tnl=2) NOTICE: #0.3 <-- end of SQL statement (elapsed time=0.010 ms) aborted NOTICE: #0.2 <-- end of statement block (elapsed time=0.010 ms) NOTICE: #0.1 <-- end of statement block (elapsed time=0.010 ms) NOTICE: #0 <<- end of block (elapsed time=0.010 ms) drop role regress_plpgsql_check_test_role; set plpgsql_check.enable_tracer to off; select plpgsql_check_tracer(false); NOTICE: tracer is not active NOTICE: tracer verbosity is verbose plpgsql_check_tracer ---------------------- f (1 row) -- tracing constants -- issue #159 create or replace function tabret_dynamic() returns table (id integer, val text) as $$ begin return query execute 'select id, val from (values (1, ''a''), (2, ''b'')) as v(id, val)'; end; $$ language plpgsql immutable; -- should be ok select * from plpgsql_check_function('tabret_dynamic()'); plpgsql_check_function ------------------------ (0 rows) create or replace function tabret_dynamic() returns table (id integer, val text) as $$ declare z_query text; begin z_query := 'select id, val from (values (1, ''a''), (2, ''b'')) as v(id, val)'; execute z_query into id, val; return next; end; $$ language plpgsql immutable; -- should be ok select * from plpgsql_check_function('tabret_dynamic()'); plpgsql_check_function ------------------------ (0 rows) create or replace function tabret_dynamic() returns table (id integer, val text) as $$ declare z_query text; begin z_query := 'select id, val from (values (1, ''a''), (2, ''b'')) as v(id, val)'; for id, val in execute z_query loop return next; end loop; end; $$ language plpgsql immutable; -- should be ok select * from plpgsql_check_function('tabret_dynamic()'); plpgsql_check_function ------------------------ (0 rows) create or replace function tabret_dynamic() returns table (id integer, val text) as $$ declare z_query text; begin z_query := 'select id, val from (values (1, ''a''), (2, ''b'')) as v(id, val)'; return query execute z_query; end; $$ language plpgsql immutable; -- should be ok select * from plpgsql_check_function('tabret_dynamic()'); plpgsql_check_function ------------------------ (0 rows) drop function tabret_dynamic; -- should not to crash on empty string, or comment create or replace function dynamic_emptystr() returns void as $$ begin execute ''; end; $$ language plpgsql; -- should be ok select * from plpgsql_check_function('dynamic_emptystr()'); plpgsql_check_function ------------------------ (0 rows) create or replace function dynamic_emptystr() returns void as $$ declare x int; begin execute '--' into x; end; $$ language plpgsql; -- should not to crash, no result error select * from plpgsql_check_function('dynamic_emptystr()'); plpgsql_check_function ------------------------------------------------------- error:XX000:4:EXECUTE:expression does not return data (1 row) drop function dynamic_emptystr(); -- unclosed cursor detection create or replace function fx() returns void as $$ declare c refcursor; begin open c for select * from pg_class; end; $$ language plpgsql; do $$ begin perform fx(); -- should to show warning perform fx(); end; $$; WARNING: cursor "c" is not closed set plpgsql_check.strict_cursors_leaks to on; do $$ begin -- should to show warning perform fx(); -- should to show warning perform fx(); end; $$; WARNING: cursor is not closed WARNING: cursor is not closed create or replace function fx() returns void as $$ declare c refcursor; begin open c for select * from pg_class; close c; end; $$ language plpgsql; -- without warnings do $$ begin perform fx(); perform fx(); end; $$; set plpgsql_check.strict_cursors_leaks to off; drop function fx(); create table public.testt(a int, b int); create or replace function fx() returns void as $$ declare v1 varchar default 'public'; v2 varchar default 'testt'; v3 varchar default 'a'; begin raise notice '%', format('%I.%I.%I', v1, v2, v3); perform 'pragma:assert-schema: v1'; perform 'pragma:assert-table: v1, v2'; perform 'pragma:assert-column: v1, v2, v3'; perform 'pragma:assert-table: v2'; perform 'pragma:assert-column: v2, v3'; end; $$ language plpgsql; select * from plpgsql_check_function('fx()'); plpgsql_check_function ------------------------ (0 rows) create table public.testt(a int, b int); ERROR: relation "testt" already exists create or replace function fx() returns void as $$ declare v1 varchar default 'public'; v2 varchar default 'testt'; v3 varchar default 'x'; begin raise notice '%', format('%I.%I.%I', v1, v2, v3); perform 'pragma:assert-column: v1, v2, v3'; end; $$ language plpgsql; select * from plpgsql_check_function('fx()'); plpgsql_check_function ------------------------------------------------------------------------------ error:42703:8:PERFORM:column "x" of relation "public"."testt" does not exist (1 row) drop function fx(); drop table public.testt; create or replace function fx() returns void as $$ declare v_sql varchar default ''; i int; begin -- we don't support constant tracing from queries select 'bla bla bla' into v_sql; execute v_sql into i; raise notice '%', i; end; $$ language plpgsql; -- should be ok select * from plpgsql_check_function('fx()'); plpgsql_check_function ------------------------ (0 rows) drop function fx(); -- should not to raise warning do $$ declare c cursor for select * from generate_series(1,10); t int; begin for i in 1..2 loop open c; loop fetch c into t; exit when not found; raise notice '%', t; end loop; close c; end loop; end; $$; NOTICE: 1 NOTICE: 2 NOTICE: 3 NOTICE: 4 NOTICE: 5 NOTICE: 6 NOTICE: 7 NOTICE: 8 NOTICE: 9 NOTICE: 10 NOTICE: 1 NOTICE: 2 NOTICE: 3 NOTICE: 4 NOTICE: 5 NOTICE: 6 NOTICE: 7 NOTICE: 8 NOTICE: 9 NOTICE: 10 -- should not raise warning create or replace function fx(p text) returns void as $$ begin execute format($_$select '%1$I'$_$, p); end; $$ language plpgsql; -- should be ok select * from plpgsql_check_function('fx(text)'); plpgsql_check_function ------------------------ (0 rows) drop function fx(text); create or replace function fx() returns void as $$ declare p varchar; begin p := 'ahoj'; execute format($_$select '%1$I'$_$, p); end; $$ language plpgsql; -- should be ok select * from plpgsql_check_function('fx()'); plpgsql_check_function ------------------------ (0 rows) drop function fx(); -- should not crash create or replace procedure p1() as $$ begin commit; end; $$ language plpgsql; set plpgsql_check.cursors_leaks to on; do $$ declare c cursor for select 1; begin open c; call p1(); end $$; set plpgsql_check.cursors_leaks to default; drop procedure p1; -- should not crash create or replace function nested_trace_test(a int) returns int as $$ begin perform plpgsql_check_pragma('enable:tracer'); return a + 1; end; $$ language plpgsql; create or replace function trace_test(b int) returns int as $$ declare r int default 0; begin for i in 1..b loop r := nested_trace_test(r); end loop; return r; end; $$ language plpgsql; -- when plpgsql_check is not loaded yet, then plpgsql_check is -- load by perform plpgsql_check_pragma and this is another -- case, when fmgr hook is not called in expected order. \c -- should not to crash select trace_test(3); trace_test ------------ 3 (1 row) create table testtable_pure_expr(a int); -- detection of not pure expressions create or replace function pure_expr() returns void as $$ declare v int; begin v := 1 delete from testtable_pure_expr where a = 10; raise notice '%', v; end; $$ language plpgsql; -- raise warning select * from plpgsql_check_function('pure_expr()'); plpgsql_check_function -------------------------------------------------------------------- warning extra:42601:4:assignment:expression is not pure expression Query: v := 1 -- ^ delete from testtable_pure_expr where a = 10 Detail: there is a possibility of unwanted behave Hint: Maybe you forgot to use a semicolon. Context: at assignment to variable "v" declared on line 2 (7 rows) drop table testtable_pure_expr; -- should not to raise false alarm drop function fxx(ct, ct); drop type ct; create type ct as (x int, y numeric, v varchar); create procedure proc_composite(inout cv ct, x int, y numeric, v varchar) as $$ begin cv.x := x; cv.y := y; cv.v := v; end; $$ language plpgsql; call proc_composite(null, 10, 10.2, 'ahoj'); cv ---------------- (10,10.2,ahoj) (1 row) create or replace function test_proc_composite() returns void as $$ declare cv ct; begin call proc_composite(cv, 10, 10.2, 'ahoj'); end; $$ language plpgsql; select * from plpgsql_check_function('test_proc_composite'); plpgsql_check_function ------------------------ (0 rows) drop function test_proc_composite; drop procedure proc_composite; create procedure proc_composite(inout cv1 ct, cv2 ct) as $$ begin cv1.x := 10; cv2.v := 'Ahoj'; end; $$ language plpgsql; create or replace function test_proc_composite() returns void as $$ declare cv1 ct; cv2 ct; begin call proc_composite(cv1, cv2); end; $$ language plpgsql; select * from plpgsql_check_function('test_proc_composite'); plpgsql_check_function ------------------------ (0 rows) drop function test_proc_composite; drop procedure proc_composite; drop type ct; create or replace function double_usage_of_const_str() returns void as $$ declare p_id_arr integer[]; r_id integer; begin p_id_arr := '{1,2,3}'::integer[]; foreach r_id in array p_id_arr loop raise notice 'id: %', r_id; end loop; --the following loop causes an error foreach r_id in array p_id_arr loop raise notice 'id: %', r_id; end loop; end; $$ language plpgsql; -- should not to raise false alarm #195 select * from plpgsql_check_function('double_usage_of_const_str'); plpgsql_check_function ------------------------ (0 rows) -- using reserved world for names is bad create or replace function test_bad_label() returns int as $$ <> declare x int = 10; begin return x; end; $$ language plpgsql; select * from plpgsql_check_function('test_bad_label'); plpgsql_check_function --------------------------------------------------------------------------- warning:00000:4:statement block:name of label "outer" is reserved keyword Detail: The reserved keyword was used as label name. (2 rows) drop function test_bad_label; -- using slow legacy syntax for assignment create or replace function test_bad_assign() returns int as $$ declare x int; begin select 0 into x; return x; end; $$ language plpgsql immutable; select * from plpgsql_check_function('test_bad_assign()', all_warnings=>true); plpgsql_check_function ------------------------------------------------------------------------ performance:00000:4:SQL statement:detected "SELECT expr INTO variable" Query: select 0 -- ^ Detail: This obsolete syntax of assigning can be slow. Hint: Use syntax "variable := expr" instead. (5 rows) drop function test_bad_assign; create or replace function volatility_check() returns int as $$ begin return (select count(*) from pg_class); end; $$ language plpgsql volatile; -- should be ok select * from plpgsql_check_function('volatility_check()'); plpgsql_check_function ------------------------ (0 rows) create or replace function volatility_check() returns int as $$ begin return (select count(*) from pg_class); end; $$ language plpgsql stable; -- should be ok select * from plpgsql_check_function('volatility_check()'); plpgsql_check_function ------------------------ (0 rows) create or replace function volatility_check() returns int as $$ begin return (select count(*) from pg_class); end; $$ language plpgsql immutable; -- warning select * from plpgsql_check_function('volatility_check()'); plpgsql_check_function ----------------------------------------------------------------------------------------- warning:00000:3:RETURN:routine is marked as IMMUTABLE, but expression is STABLE Detail: there is a risk of unwanted behave Hint: When you fix this issue, please, recheck other functions that uses this function. (3 rows) create or replace function volatility_check() returns int as $$ begin return random()*1000; end; $$ language plpgsql volatile; -- should be ok select * from plpgsql_check_function('volatility_check()'); plpgsql_check_function ------------------------ (0 rows) create or replace function volatility_check() returns int as $$ begin return random()*1000; end; $$ language plpgsql stable; -- warning select * from plpgsql_check_function('volatility_check()'); plpgsql_check_function ----------------------------------------------------------------------------------------- warning:00000:3:RETURN:routine is marked as STABLE, but expression is VOLATILE Detail: there is a risk of unwanted behave Hint: When you fix this issue, please, recheck other functions that uses this function. (3 rows) drop function volatility_check(); -- issue #201 - should not to raise false error, when composite -- constant is used and assigned to composite with more fields create type t_retcode as (retcode varchar, errmsg varchar, id int); create or replace function test_retcode_bad() returns void as $$ declare rc t_retcode := row('MO_REG_ERROR'); begin raise notice '%', rc.retcode; end; $$ language plpgsql; select * from plpgsql_check_function('test_retcode_bad'); plpgsql_check_function ------------------------------------------------------------------------------ warning:00000:4:statement block:too few attributes for composite variable Context: during statement block local variable "rc" initialization on line 3 (2 rows) drop function test_retcode_bad; -- test usage domain of composite in output #202 create domain t_retcode_domain as t_retcode; create or replace function test_retcode_bad(INOUT x1 t_retcode_domain, INOUT x2 int) as $$ begin x1.retcode = concat(x1.retcode, 'xxx'); x2 := x2 + 10; end; $$ language plpgsql; select * from plpgsql_check_function('test_retcode_bad'); plpgsql_check_function ------------------------------------------------------------------------ warning extra:00000:composite OUT variable "x1" is not single argument (1 row) drop function test_retcode_bad; drop type t_retcode cascade; NOTICE: drop cascades to type t_retcode_domain create type demo_payload as (value integer); create function demo_produce_row(out status_code integer, out payload record) as $$ begin status_code := 0; payload := ROW(1)::demo_payload; end $$ language plpgsql; create function demo_consume_row() returns void as $$ declare fetched_row record; typed_payload demo_payload; observed_status integer; begin select * into strict fetched_row from demo_produce_row(); typed_payload := fetched_row.payload; observed_status := fetched_row.status_code; end; $$ language plpgsql; -- should not crash - issue #216 select * from plpgsql_check_function('demo_consume_row()'); plpgsql_check_function --------------------------------------------------------------------- warning extra:00000:4:DECLARE:never read variable "typed_payload" warning extra:00000:5:DECLARE:never read variable "observed_status" (2 rows) -- should not crash - issue #220 create function f_poly(anyelement) returns int as $$ begin return $1; end; $$ language plpgsql; select plpgsql_check_profiler(true); NOTICE: profiler is active plpgsql_check_profiler ------------------------ t (1 row) select f_poly(1); f_poly -------- 1 (1 row) select plpgsql_coverage_statements('f_poly(anyelement)'); plpgsql_coverage_statements ----------------------------- 1 (1 row) select plpgsql_coverage_statements('f_poly'); plpgsql_coverage_statements ----------------------------- 1 (1 row) select plpgsql_check_profiler(false); NOTICE: profiler is not active plpgsql_check_profiler ------------------------ f (1 row) drop function f_poly; create or replace function repro04() returns void as $$ begin execute 'select 1'; end $$ language plpgsql; select * from plpgsql_check_function('repro04()', performance_warnings => true); plpgsql_check_function ----------------------------------------------------------------------------------------- performance:00000:3:EXECUTE:immutable expression without parameters found Detail: the EXECUTE command is not necessary probably Hint: Don't use dynamic SQL when you can use static SQL. performance:00000:routine is marked as VOLATILE, should be IMMUTABLE Hint: When you fix this issue, please, recheck other functions that uses this function. (5 rows) create or replace function repro04_into() returns int as $$ declare r int; begin execute 'select $1' into r using 1; return r; end $$ language plpgsql; select * from plpgsql_check_function('repro04_into()', performance_warnings => true); plpgsql_check_function ----------------------------------------------------------------------------------------- performance:00000:routine is marked as VOLATILE, should be IMMUTABLE Hint: When you fix this issue, please, recheck other functions that uses this function. (2 rows) create or replace function repro04_real_dynsql(p text) returns void as $$ begin execute 'select ' || quote_literal(p); end $$ language plpgsql; select * from plpgsql_check_function('repro04_real_dynsql(text)', performance_warnings => true); plpgsql_check_function ----------------------------------------------------------------------------------------- performance:00000:routine is marked as VOLATILE, should be IMMUTABLE Detail: attention: cannot to determine volatility of used dynamic SQL Hint: When you fix this issue, please, recheck other functions that uses this function. (3 rows) -- a multi statement command is real dynamic sql too, and this case was -- reported correctly even before the fix. create or replace function repro04_multi() returns void as $$ begin execute 'select 1; select 2'; end $$ language plpgsql; select * from plpgsql_check_function('repro04_multi()', performance_warnings => true); plpgsql_check_function ----------------------------------------------------------------------------------------- performance:00000:routine is marked as VOLATILE, should be IMMUTABLE Hint: When you fix this issue, please, recheck other functions that uses this function. (2 rows) create table repro04_multi_target(a int); create or replace function repro04_multi() returns void as $$ begin execute 'select 1; insert into repro04_multi_target values(10)'; end $$ language plpgsql; select * from plpgsql_check_function('repro04_multi()', performance_warnings => true); plpgsql_check_function ------------------------ (0 rows) drop table repro04_multi_target; drop function repro04(); drop function repro04_into(); drop function repro04_real_dynsql(text); drop function repro04_multi(); create or replace function unpriv_issue4_rowexpr() returns void as $$ declare r record; begin select (row(1, 'hello'::text)).* into r; raise notice '%', r.f1; select row(1, 'hello') into r; raise notice '%', r.row; end; $$ language plpgsql; select unpriv_issue4_rowexpr(); NOTICE: 1 NOTICE: (1,hello) unpriv_issue4_rowexpr ----------------------- (1 row) -- there should not be any false errors select * from plpgsql_check_function_tb('unpriv_issue4_rowexpr()'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+---------+--------+------+-------+----------+-------+--------- (0 rows) drop function unpriv_issue4_rowexpr(); okbob-plpgsql_check-800db79/expected/plpgsql_check_active_4.out000066400000000000000000006436761526744060500247500ustar00rootroot00000000000000load 'plpgsql'; create extension if not exists plpgsql_check; set client_min_messages to notice; set plpgsql_check.regress_test_mode = true; -- -- check function statement tests -- --should fail - is not plpgsql select * from plpgsql_check_function_tb('session_user()'); ERROR: "session_user"() is not a plpgsql function create table t1(a int, b int); create table pa (id int, pa_id character varying(32), status character varying(60)); create table ml(ml_id character varying(32), status_from character varying(60), pa_id character varying(32), xyz int); create function f1() returns void as $$ begin if false then update t1 set c = 30; end if; if false then raise notice '%', r.c; end if; end; $$ language plpgsql; select f1(); f1 ---- (1 row) select * from plpgsql_check_function_tb('f1()', fatal_errors := true); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+---------------+----------+--------------------------------------------+--------+------+-------+----------+----------------------+--------- f1 | 4 | SQL statement | 42703 | column "c" of relation "t1" does not exist | | | error | 15 | update t1 set c = 30 | (1 row) select * from plpgsql_check_function_tb('f1()', fatal_errors := false); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+---------------+----------+--------------------------------------------+--------+------+-------+----------+----------------------+--------- f1 | 4 | SQL statement | 42703 | column "c" of relation "t1" does not exist | | | error | 15 | update t1 set c = 30 | f1 | 7 | RAISE | 42P01 | missing FROM-clause entry for table "r" | | | error | 1 | r.c | (2 rows) select * from plpgsql_check_function_tb('f1()'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+---------------+----------+--------------------------------------------+--------+------+-------+----------+----------------------+--------- f1 | 4 | SQL statement | 42703 | column "c" of relation "t1" does not exist | | | error | 15 | update t1 set c = 30 | (1 row) drop function f1(); create function f1() returns void as $$ begin if false then insert into t1 values(10,20); update t1 set a = 10; delete from t1; end if; end; $$ language plpgsql stable; select f1(); f1 ---- (1 row) select * from plpgsql_check_function_tb('f1()', fatal_errors := false); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+---------------+----------+--------------------------------------------------+--------+------+-------+----------+------------------------------+--------- f1 | 4 | SQL statement | 0A000 | INSERT is not allowed in a non volatile function | | | error | 1 | insert into t1 values(10,20) | f1 | 5 | SQL statement | 0A000 | UPDATE is not allowed in a non volatile function | | | error | 1 | update t1 set a = 10 | f1 | 6 | SQL statement | 0A000 | DELETE is not allowed in a non volatile function | | | error | 1 | delete from t1 | (3 rows) drop function f1(); create function f1() returns void as $$ declare r record; begin if false then for r in update t1 set a = a + 1 returning * loop raise notice '%', r.a; end loop; end if; end; $$ language plpgsql; select f1(); f1 ---- (1 row) select * from plpgsql_check_function_tb('f1()', fatal_errors := false); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+---------+--------+------+-------+----------+-------+--------- (0 rows) drop function f1(); create function f1() returns void as $$ declare r record; begin if false then for r in update t1 set a = a + 1 returning * loop raise notice '%', r.a; end loop; end if; end; $$ language plpgsql stable; select f1(); f1 ---- (1 row) select * from plpgsql_check_function_tb('f1()', fatal_errors := false); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+----------------------+----------+--------------------------------------------------+--------+------+-------+----------+-------------------------------------+--------- f1 | 5 | FOR over SELECT rows | 0A000 | UPDATE is not allowed in a non volatile function | | | error | 1 | update t1 set a = a + 1 returning * | (1 row) drop function f1(); create function g1(out a int, out b int) as $$ select 10,20; $$ language sql; create function f1() returns void as $$ declare r record; begin r := g1(); if false then raise notice '%', r.c; end if; end; $$ language plpgsql; select f1(); f1 ---- (1 row) select * from plpgsql_check_function_tb('f1()'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+-----------------------------+--------+------+-------+----------+-------+--------------------------- f1 | 6 | RAISE | 42703 | record "r" has no field "c" | | | error | | | PL/pgSQL expression "r.c" (1 row) select f1(); f1 ---- (1 row) drop function f1(); drop function g1(); create function g1(out a int, out b int) returns setof record as $$ select * from t1; $$ language sql; create function f1() returns void as $$ declare r record; begin for r in select * from g1() loop raise notice '%', r.c; end loop; end; $$ language plpgsql; select f1(); f1 ---- (1 row) select * from plpgsql_check_function_tb('f1()'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+-----------------------------+--------+------+-------+----------+-------+--------------------------- f1 | 6 | RAISE | 42703 | record "r" has no field "c" | | | error | | | PL/pgSQL expression "r.c" (1 row) select f1(); f1 ---- (1 row) create or replace function f1() returns void as $$ declare r record; begin for r in select * from g1() loop r.c := 20; end loop; end; $$ language plpgsql; select f1(); f1 ---- (1 row) select * from plpgsql_check_function_tb('f1()'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+------------+----------+-----------------------------+--------+------+-------+----------+-------+--------------------------------------------------------------- f1 | 6 | assignment | 42703 | record "r" has no field "c" | | | error | | | at assignment to field "c" of variable "r" declared on line 2 (1 row) select f1(); f1 ---- (1 row) drop function f1(); drop function g1(); create function f1() returns int as $$ declare r int; begin if false then r := a + b; end if; return r; end; $$ language plpgsql; select f1(); f1 ---- (1 row) select * from plpgsql_check_function_tb('f1()'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+------------+----------+---------------------------+--------+------+-------+----------+------------+-------------------------------------------------- f1 | 5 | assignment | 42703 | column "a" does not exist | | | error | 6 | r := a + b | at assignment to variable "r" declared on line 2 (1 row) select f1(); f1 ---- (1 row) drop function f1(); create or replace function f1() returns void as $$ declare r int[]; begin if false then r[c+10] := 20; end if; end; $$ language plpgsql; select f1(); f1 ---- (1 row) select * from plpgsql_check_function_tb('f1()'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+------------+----------+---------------------------+--------+------+-------+----------+---------------+-------------------------------------------------- f1 | 5 | assignment | 42703 | column "c" does not exist | | | error | 3 | r[c+10] := 20 | at assignment to variable "r" declared on line 2 (1 row) select f1(); f1 ---- (1 row) drop function f1(); create or replace function f1() returns void as $$ declare r int; begin if false then r[10] := 20; end if; end; $$ language plpgsql set search_path = public; select f1(); f1 ---- (1 row) select * from plpgsql_check_function_tb('f1()'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+------------+----------+------------------------------------------------------------------------+--------+------+-------+----------+-------------+-------------------------------------------------- f1 | 5 | assignment | 42804 | cannot subscript type integer because it does not support subscripting | | | error | 1 | r[10] := 20 | at assignment to variable "r" declared on line 2 (1 row) select f1(); f1 ---- (1 row) drop function f1(); create or replace function f1_trg() returns trigger as $$ begin if new.a > 10 then raise notice '%', new.b; raise notice '%', new.c; end if; return new; end; $$ language plpgsql; create trigger t1_f1 before insert on t1 for each row execute procedure f1_trg(); insert into t1 values(6,30); select * from plpgsql_check_function_tb('f1_trg()','t1'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+-------------------------------+--------+------+-------+----------+-------+----------------------------- f1_trg | 5 | RAISE | 42703 | record "new" has no field "c" | | | error | | | PL/pgSQL expression "new.c" (1 row) insert into t1 values(6,30); create or replace function f1_trg() returns trigger as $$ begin new.a := new.a + 10; new.b := new.b + 10; new.c := 30; return new; end; $$ language plpgsql; -- should to fail select * from plpgsql_check_function_tb('f1_trg()','t1'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+------------+----------+-------------------------------+--------+------+-------+----------+-------+----------------------------------------------------------------- f1_trg | 5 | assignment | 42703 | record "new" has no field "c" | | | error | | | at assignment to field "c" of variable "new" declared on line 0 (1 row) -- should to fail but not crash insert into t1 values(6,30); ERROR: record "new" has no field "c" CONTEXT: PL/pgSQL assignment "new.c := 30" PL/pgSQL function f1_trg() line 5 at assignment create or replace function f1_trg() returns trigger as $$ begin new.a := new.a + 10; new.b := new.b + 10; return new; end; $$ language plpgsql; -- ok select * from plpgsql_check_function_tb('f1_trg()', 't1'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+---------+--------+------+-------+----------+-------+--------- (0 rows) -- ok insert into t1 values(6,30); create or replace function f1_trg() returns trigger as $$ begin new.a := new.a + 10; new.b := new.b + 10; return null; end; $$ language plpgsql; -- ok select * from plpgsql_check_function_tb('f1_trg()', 't1'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+---------+--------+------+-------+----------+-------+--------- (0 rows) insert into t1 values(60,300); select * from t1; a | b ----+---- 6 | 30 6 | 30 16 | 40 (3 rows) insert into t1 values(600,30); select * from t1; a | b ----+---- 6 | 30 6 | 30 16 | 40 (3 rows) drop trigger t1_f1 on t1; drop function f1_trg(); -- test of showing caret on correct place for multiline queries create or replace function f1() returns void as $$ begin select var from foo; end; $$ language plpgsql; select * from plpgsql_check_function_tb('f1()'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+---------------+----------+-------------------------------+--------+------+-------+----------+--------+--------- f1 | 3 | SQL statement | 42P01 | relation "foo" does not exist | | | error | 23 | select+| | | | | | | | | | var +| | | | | | | | | | from+| | | | | | | | | | foo | (1 row) drop function f1(); create or replace function f1() returns int as $$ begin return (select a from t1 where hh = 20); end; $$ language plpgsql; select * from plpgsql_check_function_tb('f1()'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+----------------------------+--------+------+-------+----------+----------------------------+--------- f1 | 3 | RETURN | 42703 | column "hh" does not exist | | | error | 50 | (select a +| | | | | | | | | | from t1 +| | | | | | | | | | where hh = 20) | (1 row) create or replace function f1() returns int as $$ begin return (select a from txxxxxxx where hh = 20); end; $$ language plpgsql; select * from plpgsql_check_function_tb('f1()'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+------------------------------------+--------+------+-------+----------+----------------------------+--------- f1 | 3 | RETURN | 42P01 | relation "txxxxxxx" does not exist | | | error | 29 | (select a +| | | | | | | | | | from txxxxxxx+| | | | | | | | | | where hh = 20) | (1 row) drop function f1(); drop table t1; -- raise warnings when target row has different number of attributies in -- SELECT INTO statement create or replace function f1() returns void as $$ declare a1 int; a2 int; begin select 10,20 into a1,a2; end; $$ language plpgsql; -- should be ok select * from plpgsql_check_function_tb('f1()'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+--------------------------+--------+------+---------------+----------+-------+--------- f1 | 2 | DECLARE | 00000 | never read variable "a1" | | | warning extra | | | f1 | 2 | DECLARE | 00000 | never read variable "a2" | | | warning extra | | | (2 rows) create or replace function f1() returns void as $$ declare a1 int; begin select 10,20 into a1; end; $$ language plpgsql; -- raise warning select * from plpgsql_check_function_tb('f1()'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+---------------+----------+------------------------------------------+---------------------------------------------------------------+-------------------------------------------------+---------------+----------+--------------+---------------------------------- f1 | 4 | SQL statement | 00000 | too many attributes for target variables | There are less target variables than output columns in query. | Check target variables in SELECT INTO statement | warning | 1 | select 10,20 | at SQL statement to a1 variables f1 | 2 | DECLARE | 00000 | never read variable "a1" | | | warning extra | | | (2 rows) create or replace function f1() returns void as $$ declare a1 int; a2 int; begin select 10 into a1,a2; end; $$ language plpgsql; -- raise warning select * from plpgsql_check_function_tb('f1()'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+---------------+----------+-----------------------------------------+---------------------------------------------------------------+--------------------------------------------------+---------------+----------+-----------+-------------------------------------- f1 | 4 | SQL statement | 00000 | too few attributes for target variables | There are more target variables than output columns in query. | Check target variables in SELECT INTO statement. | warning | 1 | select 10 | at SQL statement to a1, a2 variables f1 | 2 | DECLARE | 00000 | never read variable "a1" | | | warning extra | | | f1 | 2 | DECLARE | 00000 | never read variable "a2" | | | warning extra | | | (3 rows) -- bogus code set check_function_bodies to off; create or replace function f1() returns void as $$ adasdfsadf $$ language plpgsql; select * from plpgsql_check_function_tb('f1()'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+--------------------------------------+--------+------+-------+----------+------------+--------------------------------------------------- f1 | | | 42601 | syntax error at or near "adasdfsadf" | | | error | 2 | +| compilation of PL/pgSQL function "f1" near line 1 | | | | | | | | | adasdfsadf+| | | | | | | | | | | (1 row) drop function f1(); create table t1(a int, b int); create function g1(out a int, out b int) as $$ select 10,20; $$ language sql; create function f1() returns void as $$ declare r record; begin r := g1(); if false then raise notice '%', r.c; end if; end; $$ language plpgsql; select f1(); f1 ---- (1 row) select * from plpgsql_check_function('f1()'); plpgsql_check_function ------------------------------------------------- error:42703:6:RAISE:record "r" has no field "c" Context: PL/pgSQL expression "r.c" (2 rows) select f1(); f1 ---- (1 row) drop function f1(); drop function g1(); create function g1(out a int, out b int) returns setof record as $$ select * from t1; $$ language sql; create function f1() returns void as $$ declare r record; begin for r in select * from g1() loop raise notice '%', r.c; end loop; end; $$ language plpgsql; select f1(); f1 ---- (1 row) select * from plpgsql_check_function('f1()'); plpgsql_check_function ------------------------------------------------- error:42703:6:RAISE:record "r" has no field "c" Context: PL/pgSQL expression "r.c" (2 rows) select f1(); f1 ---- (1 row) create or replace function f1() returns void as $$ declare r record; begin for r in select * from g1() loop r.c := 20; end loop; end; $$ language plpgsql; select f1(); f1 ---- (1 row) select * from plpgsql_check_function('f1()'); plpgsql_check_function ------------------------------------------------------------------------ error:42703:6:assignment:record "r" has no field "c" Context: at assignment to field "c" of variable "r" declared on line 2 (2 rows) select f1(); f1 ---- (1 row) drop function f1(); drop function g1(); create function f1() returns int as $$ declare r int; begin if false then r := a + b; end if; return r; end; $$ language plpgsql; select f1(); f1 ---- (1 row) select * from plpgsql_check_function('f1()'); plpgsql_check_function ----------------------------------------------------------- error:42703:5:assignment:column "a" does not exist Query: r := a + b -- ^ Context: at assignment to variable "r" declared on line 2 (4 rows) select f1(); f1 ---- (1 row) drop function f1(); create or replace function f1() returns void as $$ declare r int[]; begin if false then r[c+10] := 20; end if; end; $$ language plpgsql; select f1(); f1 ---- (1 row) select * from plpgsql_check_function('f1()'); plpgsql_check_function ----------------------------------------------------------- error:42703:5:assignment:column "c" does not exist Query: r[c+10] := 20 -- ^ Context: at assignment to variable "r" declared on line 2 (4 rows) select f1(); f1 ---- (1 row) drop function f1(); create or replace function f1() returns void as $$ declare r int; begin if false then r[10] := 20; end if; end; $$ language plpgsql set search_path = public; select f1(); f1 ---- (1 row) select * from plpgsql_check_function('f1()'); plpgsql_check_function ------------------------------------------------------------------------------------------------- error:42804:5:assignment:cannot subscript type integer because it does not support subscripting Query: r[10] := 20 -- ^ Context: at assignment to variable "r" declared on line 2 (4 rows) select f1(); f1 ---- (1 row) drop function f1(); create or replace function f1_trg() returns trigger as $$ begin if new.a > 10 then raise notice '%', new.b; raise notice '%', new.c; end if; return new; end; $$ language plpgsql; create trigger t1_f1 before insert on t1 for each row execute procedure f1_trg(); insert into t1 values(6,30); select * from plpgsql_check_function('f1_trg()','t1'); plpgsql_check_function --------------------------------------------------- error:42703:5:RAISE:record "new" has no field "c" Context: PL/pgSQL expression "new.c" (2 rows) insert into t1 values(6,30); create or replace function f1_trg() returns trigger as $$ begin new.a := new.a + 10; new.b := new.b + 10; new.c := 30; return new; end; $$ language plpgsql; -- should to fail select * from plpgsql_check_function('f1_trg()','t1'); plpgsql_check_function -------------------------------------------------------------------------- error:42703:5:assignment:record "new" has no field "c" Context: at assignment to field "c" of variable "new" declared on line 0 (2 rows) -- should to fail but not crash insert into t1 values(6,30); ERROR: record "new" has no field "c" CONTEXT: PL/pgSQL assignment "new.c := 30" PL/pgSQL function f1_trg() line 5 at assignment create or replace function f1_trg() returns trigger as $$ begin new.a := new.a + 10; new.b := new.b + 10; return new; end; $$ language plpgsql; -- ok select * from plpgsql_check_function('f1_trg()', 't1'); plpgsql_check_function ------------------------ (0 rows) -- ok insert into t1 values(6,30); select * from t1; a | b ----+---- 6 | 30 6 | 30 16 | 40 (3 rows) drop trigger t1_f1 on t1; drop function f1_trg(); -- test of showing caret on correct place for multiline queries create or replace function f1() returns void as $$ begin select var from foo; end; $$ language plpgsql; select * from plpgsql_check_function('f1()'); plpgsql_check_function ----------------------------------------------------------- error:42P01:3:SQL statement:relation "foo" does not exist Query: select var from foo -- ^ (6 rows) drop function f1(); create or replace function f1() returns int as $$ begin return (select a from t1 where hh = 20); end; $$ language plpgsql; select * from plpgsql_check_function('f1()'); plpgsql_check_function ------------------------------------------------- error:42703:3:RETURN:column "hh" does not exist Query: (select a from t1 where hh = 20) -- ^ (5 rows) create or replace function f1() returns int as $$ begin return (select a from txxxxxxx where hh = 20); end; $$ language plpgsql; select * from plpgsql_check_function('f1()'); plpgsql_check_function --------------------------------------------------------- error:42P01:3:RETURN:relation "txxxxxxx" does not exist Query: (select a from txxxxxxx -- ^ where hh = 20) (5 rows) drop function f1(); drop table t1; -- raise warnings when target row has different number of attributies in -- SELECT INTO statement create or replace function f1() returns void as $$ declare a1 int; a2 int; begin select 10,20 into a1,a2; end; $$ language plpgsql; -- should be ok select * from plpgsql_check_function('f1()'); plpgsql_check_function -------------------------------------------------------- warning extra:00000:2:DECLARE:never read variable "a1" warning extra:00000:2:DECLARE:never read variable "a2" (2 rows) create or replace function f1() returns void as $$ declare a1 int; begin select 10,20 into a1; end; $$ language plpgsql; -- raise warning select * from plpgsql_check_function('f1()'); plpgsql_check_function ------------------------------------------------------------------------ warning:00000:4:SQL statement:too many attributes for target variables Query: select 10,20 -- ^ Detail: There are less target variables than output columns in query. Hint: Check target variables in SELECT INTO statement Context: at SQL statement to a1 variables warning extra:00000:2:DECLARE:never read variable "a1" (7 rows) create or replace function f1() returns void as $$ declare a1 int; a2 int; begin select 10 into a1,a2; end; $$ language plpgsql; -- raise warning select * from plpgsql_check_function('f1()'); plpgsql_check_function ----------------------------------------------------------------------- warning:00000:4:SQL statement:too few attributes for target variables Query: select 10 -- ^ Detail: There are more target variables than output columns in query. Hint: Check target variables in SELECT INTO statement. Context: at SQL statement to a1, a2 variables warning extra:00000:2:DECLARE:never read variable "a1" warning extra:00000:2:DECLARE:never read variable "a2" (8 rows) -- bogus code set check_function_bodies to off; create or replace function f1() returns void as $$ adasdfsadf $$ language plpgsql; select * from plpgsql_check_function('f1()'); plpgsql_check_function ------------------------------------------------------------ error:42601:syntax error at or near "adasdfsadf" Query: adasdfsadf -- ^ Context: compilation of PL/pgSQL function "f1" near line 1 (6 rows) drop function f1(); create table f1tbl(a int, b int); -- unused variables create or replace function f1(_input1 int) returns table(_output1 int, _output2 int) as $$ declare _f1 int; _f2 int; _f3 int; _f4 int; _f5 int; _r record; _tbl f1tbl; begin if true then _f1 := 1; end if; select 1, 2 into _f3, _f4; perform 1 where _f5 is null; select 1 into _r; select 1, 2 into _tbl; -- check that SQLSTATE and SQLERRM don't raise false positives begin exception when raise_exception then end; end $$ language plpgsql; select * from plpgsql_check_function('f1(int)'); plpgsql_check_function ---------------------------------------------------------- warning:00000:4:DECLARE:unused variable "_f2" warning extra:00000:3:DECLARE:never read variable "_f1" warning extra:00000:5:DECLARE:never read variable "_f3" warning extra:00000:6:DECLARE:never read variable "_f4" warning extra:00000:8:DECLARE:never read variable "_r" warning extra:00000:9:DECLARE:never read variable "_tbl" warning extra:00000:unused parameter "_input1" warning extra:00000:unmodified OUT variable "_output1" warning extra:00000:unmodified OUT variable "_output2" (9 rows) drop function f1(int); drop table f1tbl; -- check that NEW and OLD are not reported unused create table f1tbl(); create or replace function f1() returns trigger as $$ begin return null; end $$ language plpgsql; select * from plpgsql_check_function('f1()', 'f1tbl'); plpgsql_check_function ------------------------ (0 rows) drop function f1(); drop table f1tbl; create table tabret(a int, b int); insert into tabret values(10,10); create or replace function f1() returns int as $$ begin return (select a from tabret); end; $$ language plpgsql; select * from plpgsql_check_function('f1()', performance_warnings := true); plpgsql_check_function ----------------------------------------------------------------------------------------- performance:00000:routine is marked as VOLATILE, should be STABLE Hint: When you fix this issue, please, recheck other functions that uses this function. (2 rows) create or replace function f1() returns int as $$ begin return (select a::numeric from tabret); end; $$ language plpgsql; select * from plpgsql_check_function('f1()', performance_warnings := true); plpgsql_check_function ----------------------------------------------------------------------------------------- performance:42804:3:RETURN:target type is different type than source type Detail: cast "numeric" value to "integer" type Hint: Hidden casting can be a performance issue. Context: at RETURN performance:00000:routine is marked as VOLATILE, should be STABLE Hint: When you fix this issue, please, recheck other functions that uses this function. (6 rows) create or replace function f1() returns int as $$ begin return (select a, b from tabret); end; $$ language plpgsql; select * from plpgsql_check_function('f1()', performance_warnings := true); plpgsql_check_function ----------------------------------------------------------- error:42601:3:RETURN:subquery must return only one column Query: (select a, b from tabret) -- ^ (3 rows) drop function f1(); create or replace function f1() returns table(ax int, bx int) as $$ begin return query select * from tabret; return; end; $$ language plpgsql; select * from plpgsql_check_function('f1()', performance_warnings := true); plpgsql_check_function ----------------------------------------------------------------------------------------- performance:00000:routine is marked as VOLATILE, should be STABLE Hint: When you fix this issue, please, recheck other functions that uses this function. (2 rows) drop function f1(); create or replace function f1() returns table(ax numeric, bx numeric) as $$ begin return query select * from tabret; return; end; $$ language plpgsql; select * from plpgsql_check_function('f1()', performance_warnings := true); plpgsql_check_function ------------------------------------------------------------------------------------------------- error:42804:3:RETURN QUERY:structure of query does not match function result type Detail: Returned type integer does not match expected type numeric in column "ax" (position 1). (2 rows) drop function f1(); create or replace function f1() returns setof tabret as $$ begin return query select * from tabret; return; end; $$ language plpgsql; select * from plpgsql_check_function('f1()', performance_warnings := true); plpgsql_check_function ----------------------------------------------------------------------------------------- performance:00000:routine is marked as VOLATILE, should be STABLE Hint: When you fix this issue, please, recheck other functions that uses this function. (2 rows) create or replace function f1() returns setof tabret as $$ begin return query select a from tabret; return; end; $$ language plpgsql; select * from plpgsql_check_function('f1()', performance_warnings := true); plpgsql_check_function ----------------------------------------------------------------------------------- error:42804:3:RETURN QUERY:structure of query does not match function result type Detail: Number of returned columns (1) does not match expected column count (2). (2 rows) create or replace function f1() returns setof tabret as $$ begin return query select a::numeric,b::numeric from tabret; return; end; $$ language plpgsql; select * from plpgsql_check_function('f1()', performance_warnings := true); plpgsql_check_function ------------------------------------------------------------------------------------------------ error:42804:3:RETURN QUERY:structure of query does not match function result type Detail: Returned type numeric does not match expected type integer in column "a" (position 1). (2 rows) drop function f1(); create or replace function f1(a int) returns setof numeric as $$ begin return query select a; end $$ language plpgsql; select * from plpgsql_check_function('f1(int)', performance_warnings := true); plpgsql_check_function --------------------------------------------------------------------------------------------------------- error:42804:2:RETURN QUERY:structure of query does not match function result type Detail: Returned type integer does not match expected type numeric in column "__result__" (position 1). (2 rows) drop function f1(int); drop table tabret; create or replace function f1() returns void as $$ declare intval integer; begin intval := null; -- ok intval := 1; -- OK intval := '1'; -- OK intval := text '1'; -- not OK intval := current_date; -- not OK select 1 into intval; -- OK select '1' into intval; -- OK select text '1' into intval; -- not OK end $$ language plpgsql; select * from plpgsql_check_function('f1()', performance_warnings := true); plpgsql_check_function -------------------------------------------------------------------------------------- warning:42804:9:assignment:target type is different type than source type Query: intval := current_date -- ^ Detail: cast "date" value to "integer" type Hint: There are no possible explicit coercion between those types, possibly bug! Context: at assignment to variable "intval" declared on line 3 performance:00000:11:SQL statement:detected "SELECT expr INTO variable" Query: select 1 -- ^ Detail: This obsolete syntax of assigning can be slow. Hint: Use syntax "variable := expr" instead. performance:00000:12:SQL statement:detected "SELECT expr INTO variable" Query: select '1' -- ^ Detail: This obsolete syntax of assigning can be slow. Hint: Use syntax "variable := expr" instead. warning:42804:12:SQL statement:target type is different type than source type Query: select '1' -- ^ Detail: cast "text" value to "integer" type Hint: The input expression type does not have an assignment cast to the target type. Context: at SQL statement to variable "intval" declared on line 3 performance:00000:13:SQL statement:detected "SELECT expr INTO variable" Query: select text '1' -- ^ Detail: This obsolete syntax of assigning can be slow. Hint: Use syntax "variable := expr" instead. warning:42804:13:SQL statement:target type is different type than source type Query: select text '1' -- ^ Detail: cast "text" value to "integer" type Hint: The input expression type does not have an assignment cast to the target type. Context: at SQL statement to variable "intval" declared on line 3 warning extra:00000:3:DECLARE:never read variable "intval" (34 rows) drop function f1(); create or replace function f1() returns int as $$ begin return 1; end; $$ language plpgsql; select * from plpgsql_check_function('f1()', performance_warnings := true); plpgsql_check_function ----------------------------------------------------------------------------------------- performance:00000:routine is marked as VOLATILE, should be IMMUTABLE Hint: When you fix this issue, please, recheck other functions that uses this function. (2 rows) create or replace function f1() returns int as $$ begin return 1::numeric; end; $$ language plpgsql; select * from plpgsql_check_function('f1()', performance_warnings := true); plpgsql_check_function ----------------------------------------------------------------------------------------- performance:42804:3:RETURN:target type is different type than source type Detail: cast "numeric" value to "integer" type Hint: Hidden casting can be a performance issue. Context: at RETURN performance:00000:routine is marked as VOLATILE, should be IMMUTABLE Hint: When you fix this issue, please, recheck other functions that uses this function. (6 rows) create or replace function f1() returns int as $$ begin return null; end; $$ language plpgsql; select * from plpgsql_check_function('f1()', performance_warnings := true); plpgsql_check_function ----------------------------------------------------------------------------------------- performance:00000:routine is marked as VOLATILE, should be IMMUTABLE Hint: When you fix this issue, please, recheck other functions that uses this function. (2 rows) create or replace function f1() returns int as $$ begin return current_date; end; $$ language plpgsql; select * from plpgsql_check_function('f1()', performance_warnings := true); plpgsql_check_function ----------------------------------------------------------------------------------------- warning:42804:3:RETURN:target type is different type than source type Detail: cast "date" value to "integer" type Hint: There are no possible explicit coercion between those types, possibly bug! Context: at RETURN performance:00000:routine is marked as VOLATILE, should be STABLE Hint: When you fix this issue, please, recheck other functions that uses this function. (6 rows) create or replace function f1() returns int as $$ declare a int; begin return a; end; $$ language plpgsql; select * from plpgsql_check_function('f1()', performance_warnings := true); plpgsql_check_function ----------------------------------------------------------------------------------------- performance:00000:routine is marked as VOLATILE, should be IMMUTABLE Hint: When you fix this issue, please, recheck other functions that uses this function. (2 rows) create or replace function f1() returns int as $$ declare a numeric; begin return a; end; $$ language plpgsql; select * from plpgsql_check_function('f1()', performance_warnings := true); plpgsql_check_function ----------------------------------------------------------------------------------------- performance:42804:4:RETURN:target type is different type than source type Detail: cast "numeric" value to "integer" type Hint: Hidden casting can be a performance issue. Context: at RETURN of variable "a" declared on line 2 performance:00000:routine is marked as VOLATILE, should be IMMUTABLE Hint: When you fix this issue, please, recheck other functions that uses this function. (6 rows) drop function f1(); create or replace function f1() returns setof int as $$ begin return next 1; end; $$ language plpgsql; select * from plpgsql_check_function('f1()', performance_warnings := true); plpgsql_check_function ----------------------------------------------------------------------------------------- performance:00000:routine is marked as VOLATILE, should be IMMUTABLE Hint: When you fix this issue, please, recheck other functions that uses this function. (2 rows) create or replace function f1() returns setof int as $$ begin return next 1::numeric; -- tolerant, doesn't use tupmap end; $$ language plpgsql; select * from plpgsql_check_function('f1()', performance_warnings := true); plpgsql_check_function ----------------------------------------------------------------------------------------- performance:42804:3:RETURN NEXT:target type is different type than source type Detail: cast "numeric" value to "integer" type Hint: Hidden casting can be a performance issue. Context: at RETURN NEXT performance:00000:routine is marked as VOLATILE, should be IMMUTABLE Hint: When you fix this issue, please, recheck other functions that uses this function. (6 rows) drop function f1(); create type t1 as (a int, b int, c int); create type t2 as (a int, b numeric); create or replace function fx() returns t2 as $$ declare x t1; begin return x; end; $$ language plpgsql; select * from plpgsql_check_function('fx()', performance_warnings := true); plpgsql_check_function ------------------------------------------------------------------------------------------------ error:42804:4:RETURN:returned record type does not match expected record type Detail: Returned type integer does not match expected type numeric in column "b" (position 2). (2 rows) create or replace function fx() returns t2 as $$ declare x t2; begin return x; end; $$ language plpgsql; select * from plpgsql_check_function('fx()', performance_warnings := true); plpgsql_check_function ----------------------------------------------------------------------------------------- performance:00000:routine is marked as VOLATILE, should be IMMUTABLE Hint: When you fix this issue, please, recheck other functions that uses this function. (2 rows) drop function fx(); create or replace function fx() returns setof t2 as $$ declare x t1; begin return next x; return; end; $$ language plpgsql; select * from plpgsql_check_function('fx()', performance_warnings := true); plpgsql_check_function ------------------------------------------------------------------------------------------------ error:42804:4:RETURN NEXT:wrong record type supplied in RETURN NEXT Detail: Returned type integer does not match expected type numeric in column "b" (position 2). (2 rows) create or replace function fx() returns setof t2 as $$ declare x t2; begin return next x; return; end; $$ language plpgsql; select * from plpgsql_check_function('fx()', performance_warnings := true); plpgsql_check_function ----------------------------------------------------------------------------------------- performance:00000:routine is marked as VOLATILE, should be IMMUTABLE Hint: When you fix this issue, please, recheck other functions that uses this function. (2 rows) drop function fx(); create or replace function fx2(_id int, _pa_id varchar(32), _status varchar(60)) returns void as $$ declare begin insert into pa values(_id, _pa_id, _status); exception when OTHERS then raise notice '%', 'some message'; raise exception '%', sqlerrm; end $$ language plpgsql; select * from plpgsql_check_function('fx2(int, varchar, varchar)', performance_warnings := true); plpgsql_check_function ------------------------ (0 rows) create or replace function fx2(_id int, _pa_id varchar(32), _status varchar(60)) returns void as $$ declare begin insert into pa values(_id, _pa_id, _status) returning *; exception when OTHERS then raise notice '%', 'some message'; raise exception '%', sqlerrm; end $$ language plpgsql; select * from plpgsql_check_function('fx2(int, varchar, varchar)', performance_warnings := true); plpgsql_check_function ---------------------------------------------------------------------- error:42601:4:SQL statement:query has no destination for result data (1 row) create or replace function fx2(_id int, _pa_id varchar(32), _status varchar(60)) returns void as $$ declare begin SELECT * FROM pa LIMIT 1; exception when OTHERS then raise notice '%', 'some message'; raise exception '%', sqlerrm; end $$ language plpgsql; select * from plpgsql_check_function('fx2(int, varchar, varchar)', performance_warnings := true); plpgsql_check_function ---------------------------------------------------------------------- error:42601:4:SQL statement:query has no destination for result data (1 row) drop function fx2(int, varchar, varchar); create or replace function foreach_array_loop() returns void as $body$ declare arr text[]; el text; begin arr := array['1111','2222','3333']; foreach el in array arr loop raise notice '%', el; end loop; end; $body$ language 'plpgsql' stable; select * from plpgsql_check_function_tb('foreach_array_loop()', performance_warnings := true); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context --------------------+--------+-----------+----------+--------------------------------------------------+--------+-----------------------------------------------------------------------------------+-------------+----------+-------+--------- foreach_array_loop | | | 00000 | routine is marked as STABLE, should be IMMUTABLE | | When you fix this issue, please, recheck other functions that uses this function. | performance | | | (1 row) create or replace function foreach_array_loop() returns void as $body$ declare arr text[]; el int; begin arr := array['1111','2222','3333']; foreach el in array arr loop raise notice '%', el; end loop; end; $body$ language 'plpgsql' stable; select * from plpgsql_check_function_tb('foreach_array_loop()', performance_warnings := true); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context --------------------+--------+--------------------+----------+--------------------------------------------------+-------------------------------------+-----------------------------------------------------------------------------------+-------------+----------+-------+----------------------- foreach_array_loop | 7 | FOREACH over array | 42804 | target type is different type than source type | cast "text" value to "integer" type | The input expression type does not have an assignment cast to the target type. | warning | | | at FOREACH over array foreach_array_loop | | | 00000 | routine is marked as STABLE, should be IMMUTABLE | | When you fix this issue, please, recheck other functions that uses this function. | performance | | | (2 rows) create or replace function foreach_array_loop() returns void as $body$ declare arr date[]; el int; begin -- expression is not constant (else this check fails due tracking constant) arr := array['2014-01-01','2015-01-01','2016-01-01', current_date]::date[]; foreach el in array arr loop raise notice '%', el; end loop; end; $body$ language 'plpgsql' stable; select * from plpgsql_check_function_tb('foreach_array_loop()', performance_warnings := true); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context --------------------+--------+--------------------+----------+------------------------------------------------+-------------------------------------+----------------------------------------------------------------------------+---------+----------+-------+----------------------- foreach_array_loop | 8 | FOREACH over array | 42804 | target type is different type than source type | cast "date" value to "integer" type | There are no possible explicit coercion between those types, possibly bug! | warning | | | at FOREACH over array (1 row) create or replace function foreach_array_loop() returns void as $body$ declare arr date[]; el int; begin arr := array['2014-01-01','2015-01-01','2016-01-01']::date[]; foreach el in array arr loop raise notice '%', el; end loop; end; $body$ language 'plpgsql' stable; select * from plpgsql_check_function_tb('foreach_array_loop()', performance_warnings := true); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context --------------------+--------+--------------------+----------+-----------------------------------------------------+--------+------+-------+----------+-------+--------- foreach_array_loop | 7 | FOREACH over array | 22P02 | invalid input syntax for type integer: "01-01-2014" | | | error | | | (1 row) create or replace function foreach_array_loop() returns void as $body$ declare el text; begin foreach el in array array['1111','2222','3333'] loop raise notice '%', el; end loop; end; $body$ language 'plpgsql' stable; select * from plpgsql_check_function_tb('foreach_array_loop()', performance_warnings := true); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context --------------------+--------+-----------+----------+--------------------------------------------------+--------+-----------------------------------------------------------------------------------+-------------+----------+-------+--------- foreach_array_loop | | | 00000 | routine is marked as STABLE, should be IMMUTABLE | | When you fix this issue, please, recheck other functions that uses this function. | performance | | | (1 row) create or replace function foreach_array_loop() returns void as $body$ declare el int; begin foreach el in array array['1111','2222','3333'] loop raise notice '%', el; end loop; end; $body$ language 'plpgsql' stable; select * from plpgsql_check_function_tb('foreach_array_loop()', performance_warnings := true); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context --------------------+--------+--------------------+----------+--------------------------------------------------+-------------------------------------+-----------------------------------------------------------------------------------+-------------+----------+-------+----------------------- foreach_array_loop | 5 | FOREACH over array | 42804 | target type is different type than source type | cast "text" value to "integer" type | The input expression type does not have an assignment cast to the target type. | warning | | | at FOREACH over array foreach_array_loop | | | 00000 | routine is marked as STABLE, should be IMMUTABLE | | When you fix this issue, please, recheck other functions that uses this function. | performance | | | (2 rows) create or replace function foreach_array_loop() returns void as $body$ declare el int; begin foreach el in array array['2014-01-01','2015-01-01','2016-01-01']::date[] loop raise notice '%', el; end loop; end; $body$ language 'plpgsql' stable; select * from plpgsql_check_function_tb('foreach_array_loop()', performance_warnings := true); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context --------------------+--------+--------------------+----------+-----------------------------------------------------+--------+------+-------+----------+-------+--------- foreach_array_loop | 5 | FOREACH over array | 22P02 | invalid input syntax for type integer: "01-01-2014" | | | error | | | (1 row) drop function foreach_array_loop(); create or replace function scan_rows(int[]) returns void AS $$ declare x int[]; begin foreach x slice 1 in array $1 loop raise notice 'row = %', x; end loop; end; $$ language plpgsql; select * from plpgsql_check_function_tb('scan_rows(int[])', performance_warnings := true); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+----------------------------------------------------+--------+-----------------------------------------------------------------------------------+-------------+----------+-------+--------- scan_rows | | | 00000 | routine is marked as VOLATILE, should be IMMUTABLE | | When you fix this issue, please, recheck other functions that uses this function. | performance | | | (1 row) create or replace function scan_rows(int[]) returns void AS $$ declare x int[]; begin foreach x in array $1 loop raise notice 'row = %', x; end loop; end; $$ language plpgsql; select * from plpgsql_check_function_tb('scan_rows(int[])', performance_warnings := true); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+--------------------+----------+----------------------------------------------------+------------------------------------------+-----------------------------------------------------------------------------------+-------------+----------+-------+----------------------- scan_rows | 5 | FOREACH over array | 42804 | target type is different type than source type | cast "integer" value to "integer[]" type | There are no possible explicit coercion between those types, possibly bug! | warning | | | at FOREACH over array scan_rows | | | 00000 | routine is marked as VOLATILE, should be IMMUTABLE | | When you fix this issue, please, recheck other functions that uses this function. | performance | | | (2 rows) drop function scan_rows(int[]); drop type t1; drop type t2; create table t1(a int, b int); create table t2(a int, b int, c int); create table t3(a numeric, b int); insert into t1 values(10,20),(30,40); create or replace function fx() returns int as $$ declare s int default 0; r t1; begin foreach r in array (select array_agg(t1) from t1) loop s := r.a + r.b; end loop; return s; end; $$ language plpgsql; select * from plpgsql_check_function_tb('fx()', performance_warnings := true); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+-------------------------------------------------+--------+-----------------------------------------------------------------------------------+-------------+----------+-------+--------- fx | | | 00000 | routine is marked as VOLATILE, should be STABLE | | When you fix this issue, please, recheck other functions that uses this function. | performance | | | (1 row) create or replace function fx() returns int as $$ declare s int default 0; r t1; c t1[]; begin c := (select array_agg(t1) from t1); foreach r in array c loop s := r.a + r.b; end loop; return s; end; $$ language plpgsql; select * from plpgsql_check_function_tb('fx()', performance_warnings := true); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+-------------------------------------------------+--------+-----------------------------------------------------------------------------------+-------------+----------+-------+--------- fx | | | 00000 | routine is marked as VOLATILE, should be STABLE | | When you fix this issue, please, recheck other functions that uses this function. | performance | | | (1 row) create or replace function fx() returns int as $$ declare s int default 0; r t1; c t1[]; begin select array_agg(t1) into c from t1; foreach r in array c loop s := r.a + r.b; end loop; return s; end; $$ language plpgsql; select * from plpgsql_check_function_tb('fx()', performance_warnings := true); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+-------------------------------------------------+--------+-----------------------------------------------------------------------------------+-------------+----------+-------+--------- fx | | | 00000 | routine is marked as VOLATILE, should be STABLE | | When you fix this issue, please, recheck other functions that uses this function. | performance | | | (1 row) create or replace function fx() returns int as $$ declare s int default 0; r t1; c t1[]; begin select array_agg(t1) into c from t1; for i in array_lower(c, 1) .. array_upper(c, 1) loop r := c[i]; s := r.a + r.b; end loop; return s; end; $$ language plpgsql; select * from plpgsql_check_function_tb('fx()', performance_warnings := true); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+-------------------------------------------------+--------+-----------------------------------------------------------------------------------+-------------+----------+-------+--------- fx | | | 00000 | routine is marked as VOLATILE, should be STABLE | | When you fix this issue, please, recheck other functions that uses this function. | performance | | | (1 row) create or replace function fx() returns int as $$ declare s int default 0; c t1[]; begin select array_agg(t1) into c from t1; for i in array_lower(c, 1) .. array_upper(c, 1) loop s := (c[i]).a + (c[i]).b; end loop; return s; end; $$ language plpgsql; select * from plpgsql_check_function_tb('fx()', performance_warnings := true); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+-------------------------------------------------+--------+-----------------------------------------------------------------------------------+-------------+----------+-------+--------- fx | | | 00000 | routine is marked as VOLATILE, should be STABLE | | When you fix this issue, please, recheck other functions that uses this function. | performance | | | (1 row) create or replace function fx() returns int as $$ declare s int default 0; r record; c t1[]; begin select array_agg(t1) into c from t1; for i in array_lower(c, 1) .. array_upper(c, 1) loop r := c[i]; s := r.a + r.b; end loop; return s; end; $$ language plpgsql; select * from plpgsql_check_function_tb('fx()', performance_warnings := true); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+-------------------------------------------------+--------+-----------------------------------------------------------------------------------+-------------+----------+-------+--------- fx | | | 00000 | routine is marked as VOLATILE, should be STABLE | | When you fix this issue, please, recheck other functions that uses this function. | performance | | | (1 row) create or replace function fx() returns int as $$ declare s int default 0; r record; c t1[]; begin select array_agg(t1) into c from t1; for i in array_lower(c, 1) .. array_upper(c, 1) loop r := c[i]; s := r.a + r.b + r.c; end loop; return s; end; $$ language plpgsql; select * from plpgsql_check_function_tb('fx()', performance_warnings := true); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+------------+----------+-----------------------------+--------+------+-------+----------+-------+-------------------------------------------- fx | 11 | assignment | 42703 | record "r" has no field "c" | | | error | | | PL/pgSQL assignment "s := r.a + r.b + r.c" (1 row) create or replace function fx() returns int as $$ declare s int default 0; r t2; begin foreach r in array (select array_agg(t1) from t1) loop s := r.a + r.b; end loop; return s; end; $$ language plpgsql; select * from plpgsql_check_function_tb('fx()', performance_warnings := true); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+--------------------+----------+-------------------------------------------------+--------+-----------------------------------------------------------------------------------+-------------+----------+-------+----------------------- fx | 6 | FOREACH over array | 00000 | too few attributes for composite variable | | | warning | | | at FOREACH over array fx | | | 00000 | routine is marked as VOLATILE, should be STABLE | | When you fix this issue, please, recheck other functions that uses this function. | performance | | | (2 rows) create or replace function fx() returns int as $$ declare s int default 0; r t3; begin foreach r in array (select array_agg(t1) from t1) loop s := r.a + r.b; end loop; return s; end; $$ language plpgsql; select * from plpgsql_check_function_tb('fx()', performance_warnings := true); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+--------------------+----------+-------------------------------------------------+----------------------------------------+-----------------------------------------------------------------------------------+-------------+----------+----------------+-------------------------------------------------- fx | 6 | FOREACH over array | 42804 | target type is different type than source type | cast "integer" value to "numeric" type | Hidden casting can be a performance issue. | performance | | | at FOREACH over array fx | 8 | assignment | 42804 | target type is different type than source type | cast "numeric" value to "integer" type | Hidden casting can be a performance issue. | performance | 1 | s := r.a + r.b | at assignment to variable "s" declared on line 3 fx | | | 00000 | routine is marked as VOLATILE, should be STABLE | | When you fix this issue, please, recheck other functions that uses this function. | performance | | | (3 rows) drop function fx(); drop table t1; -- mscottie issue #13 create table test ( a text, b integer, c uuid ); create function before_insert_test() returns trigger language plpgsql as $$ begin select a into NEW.a from test where b = 1; select b into NEW.b from test where b = 1; select null::uuid into NEW.c from test where b = 1; return new; end; $$; select * from plpgsql_check_function_tb('before_insert_test()','test'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+---------+--------+------+-------+----------+-------+--------- (0 rows) create or replace function before_insert_test() returns trigger language plpgsql as $$ begin NEW.a := (select a from test where b = 1); NEW.b := (select b from test where b = 1); NEW.c := (select c from test where b = 1); return new; end; $$; select * from plpgsql_check_function_tb('before_insert_test()','test', fatal_errors := false); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+---------+--------+------+-------+----------+-------+--------- (0 rows) create or replace function before_insert_test() returns trigger language plpgsql as $$ begin NEW.a := 'Hello'::text; NEW.b := 10; NEW.c := null::uuid; return new; end; $$; select * from plpgsql_check_function_tb('before_insert_test()','test', fatal_errors := false); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+---------+--------+------+-------+----------+-------+--------- (0 rows) drop function before_insert_test(); create or replace function fx() returns void as $$ declare NEW test; OLD test; begin select null::uuid into NEW.c from test where b = 1; end; $$ language plpgsql; select * from plpgsql_check_function_tb('fx()', performance_warnings := true, fatal_errors := false); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+---------------------------+--------+------+---------------+----------+-------+--------- fx | 2 | DECLARE | 00000 | unused variable "old" | | | warning | | | fx | 2 | DECLARE | 00000 | never read variable "new" | | | warning extra | | | (2 rows) drop function fx(); create or replace function fx() returns void as $$ declare NEW test; begin NEW.a := 'Hello'::text; NEW.b := 10; NEW.c := null::uuid; end; $$ language plpgsql; select * from plpgsql_check_function_tb('fx()', performance_warnings := true, fatal_errors := false); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+----------------------------------------------------+--------+-----------------------------------------------------------------------------------+---------------+----------+-------+--------- fx | 2 | DECLARE | 00000 | never read variable "new" | | | warning extra | | | fx | | | 00000 | routine is marked as VOLATILE, should be IMMUTABLE | | When you fix this issue, please, recheck other functions that uses this function. | performance | | | (2 rows) drop function fx(); drop table test; create or replace function fx() returns void as $$ declare s int; sa int[]; sd date; bs int[]; begin sa[10] := s; sa[10] := sd; s := bs[10]; end; $$ language plpgsql; select * from plpgsql_check_function_tb('fx()', performance_warnings := true, fatal_errors := false); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+------------+----------+------------------------------------------------+-------------------------------------+----------------------------------------------------------------------------+---------------+----------+--------------+--------------------------------------------------- fx | 9 | assignment | 42804 | target type is different type than source type | cast "date" value to "integer" type | There are no possible explicit coercion between those types, possibly bug! | warning | 1 | sa[10] := sd | at assignment to variable "sa" declared on line 4 fx | 4 | DECLARE | 00000 | never read variable "sa" | | | warning extra | | | (2 rows) drop function fx(); create type t as (t text); create or replace function fx() returns void as $$ declare _t t; _tt t[]; _txt text; begin _t.t := 'ABC'; -- correct warning "unknown" _tt[1] := _t; _txt := _t; end; $$ language plpgsql; select * from plpgsql_check_function('fx()', performance_warnings := true); plpgsql_check_function ------------------------------------------------------------------------------- performance:42804:7:assignment:target type is different type than source type Query: _txt := _t -- ^ Detail: cast "t" value to "text" type Hint: Hidden casting can be a performance issue. Context: at assignment to variable "_txt" declared on line 3 warning extra:00000:2:DECLARE:never read variable "_tt" warning extra:00000:3:DECLARE:never read variable "_txt" (8 rows) drop function fx(); create or replace function fx() returns void as $$ declare _t1 t; _t2 t; begin _t1.t := 'ABC'::text; _t2 := _t1; raise notice '% %', _t2, _t2.t; end; $$ language plpgsql; select * from plpgsql_check_function('fx()', performance_warnings := true); plpgsql_check_function ----------------------------------------------------------------------------------------- performance:00000:routine is marked as VOLATILE, should be IMMUTABLE Hint: When you fix this issue, please, recheck other functions that uses this function. (2 rows) drop function fx(); create or replace function fx(out _tt t[]) as $$ declare _t t; begin _t.t := 'ABC'::text; _tt[1] := _t; end; $$ language plpgsql; select * from plpgsql_check_function('fx()', performance_warnings := true); plpgsql_check_function ----------------------------------------------------------------------------------------- performance:00000:routine is marked as VOLATILE, should be IMMUTABLE Hint: When you fix this issue, please, recheck other functions that uses this function. (2 rows) drop function fx(); drop type t; create or replace function fx() returns int as $$ declare x int; begin perform 1; return 10; end; $$ language plpgsql; select * from plpgsql_check_function('fx()', performance_warnings := true); plpgsql_check_function ----------------------------------------------------------------------------------------- warning:00000:2:DECLARE:unused variable "x" performance:00000:routine is marked as VOLATILE, should be IMMUTABLE Hint: When you fix this issue, please, recheck other functions that uses this function. (3 rows) drop function fx(); create table t(i int); create function test_t(OUT t) returns t AS $$ begin $1 := null; end; $$ language plpgsql; select test_t(); test_t -------- (1 row) select * from test_t(); i --- (1 row) select * from plpgsql_check_function('test_t()', performance_warnings := true); plpgsql_check_function ----------------------------------------------------------------------------------------- performance:00000:routine is marked as VOLATILE, should be IMMUTABLE Hint: When you fix this issue, please, recheck other functions that uses this function. (2 rows) create or replace function fx() returns void as $$ declare c cursor for select * from t; x varchar; begin open c; fetch c into x; close c; end; $$ language plpgsql; select test_t(); test_t -------- (1 row) select * from test_t(); i --- (1 row) select * from plpgsql_check_function('fx()', performance_warnings := true, fatal_errors := false); plpgsql_check_function -------------------------------------------------------------------------- performance:42804:7:FETCH:target type is different type than source type Detail: cast "integer" value to "character varying" type Hint: Hidden casting can be a performance issue. Context: at FETCH to variable "x" declared on line 4 warning extra:00000:4:DECLARE:never read variable "x" (5 rows) drop function fx(); create or replace function fx() returns void as $$ declare c cursor for select * from t; x int; begin open c; fetch c into x; close c; end; $$ language plpgsql; select test_t(); test_t -------- (1 row) select * from test_t(); i --- (1 row) select * from plpgsql_check_function('fx()', performance_warnings := true, fatal_errors := false); plpgsql_check_function ------------------------------------------------------- warning extra:00000:4:DECLARE:never read variable "x" (1 row) drop function fx(); create or replace function fx() returns void as $$ declare c cursor for select * from t; begin for r in c loop raise notice '%', r.a; end loop; end; $$ language plpgsql; select test_t(); test_t -------- (1 row) select * from test_t(); i --- (1 row) select * from plpgsql_check_function('fx()', performance_warnings := true, fatal_errors := false); plpgsql_check_function ------------------------------------------------------- error:42703:6:RAISE:record "r" has no field "a" Context: PL/pgSQL expression "r.a" warning extra:00000:5:DECLARE:never read variable "r" (3 rows) drop function fx(); create or replace function fx() returns void as $$ declare c cursor for select * from t; begin for r in c loop raise notice '%', r.i; end loop; end; $$ language plpgsql; select test_t(); test_t -------- (1 row) select * from test_t(); i --- (1 row) select * from plpgsql_check_function('fx()', performance_warnings := true, fatal_errors := false); plpgsql_check_function ------------------------ (0 rows) drop function fx(); create table foo(a int, b int); create or replace function fx() returns void as $$ declare f1 int; f2 int; begin select 1, 2 into f1; select 1 into f1, f2; select a b into f1, f2 from foo; end; $$ language plpgsql; select fx(); fx ---- (1 row) select * from plpgsql_check_function('fx()', performance_warnings := true, fatal_errors := false); plpgsql_check_function ------------------------------------------------------------------------ warning:00000:4:SQL statement:too many attributes for target variables Query: select 1, 2 -- ^ Detail: There are less target variables than output columns in query. Hint: Check target variables in SELECT INTO statement Context: at SQL statement to f1 variables warning:00000:5:SQL statement:too few attributes for target variables Query: select 1 -- ^ Detail: There are more target variables than output columns in query. Hint: Check target variables in SELECT INTO statement. Context: at SQL statement to f1, f2 variables warning:00000:6:SQL statement:too few attributes for target variables Query: select a b from foo -- ^ Detail: There are more target variables than output columns in query. Hint: Check target variables in SELECT INTO statement. Context: at SQL statement to f1, f2 variables warning extra:00000:2:DECLARE:never read variable "f1" warning extra:00000:2:DECLARE:never read variable "f2" (20 rows) drop function fx(); drop table foo; create or replace function fx() returns void as $$ declare d date; begin d := (select 1 from pg_class limit 1); raise notice '%', d; end; $$ language plpgsql; select fx(); ERROR: invalid input syntax for type date: "1" CONTEXT: PL/pgSQL assignment "d := (select 1 from pg_class limit 1)" PL/pgSQL function fx() line 4 at assignment select * from plpgsql_check_function('fx()', performance_warnings := true, fatal_errors := false); plpgsql_check_function ---------------------------------------------------------------------------------- warning:42804:4:assignment:target type is different type than source type Query: d := (select 1 from pg_class limit 1) -- ^ Detail: cast "integer" value to "date" type Hint: There are no possible explicit coercion between those types, possibly bug! Context: at assignment to variable "d" declared on line 2 (6 rows) drop function fx(); create table tab_1(i int); create or replace function fx(a int) returns setof int as $$ declare c refcursor; r record; begin open c for select i from tab_1 where i = a; loop fetch c into r; if not found then exit; end if; return next r.i; end loop; end; $$ language plpgsql; select * from plpgsql_check_function('fx(int)', performance_warnings := true, fatal_errors := false); plpgsql_check_function ----------------------------------------------------------------------------------------- performance:00000:routine is marked as VOLATILE, should be STABLE Hint: When you fix this issue, please, recheck other functions that uses this function. (2 rows) create or replace function fx(a int) returns setof int as $$ declare c refcursor; r record; begin open c for select i from tab_1 where i = a; loop fetch c into r; if not found then exit; end if; return next r.x; end loop; end; $$ language plpgsql; select * from plpgsql_check_function('fx(int)', performance_warnings := true, fatal_errors := false); plpgsql_check_function ----------------------------------------------------------------------------------------- error:42703:12:RETURN NEXT:record "r" has no field "x" Context: PL/pgSQL expression "r.x" warning extra:00000:4:DECLARE:never read variable "r" performance:00000:routine is marked as VOLATILE, should be STABLE Hint: When you fix this issue, please, recheck other functions that uses this function. (5 rows) drop function fx(int); drop table tab_1; create or replace function fxx() returns void as $$ begin rollback; end; $$ language plpgsql; select fxx(); ERROR: invalid transaction termination CONTEXT: PL/pgSQL function fxx() line 3 at ROLLBACK select * from plpgsql_check_function('fxx()'); plpgsql_check_function -------------------------------------------------------- error:2D000:3:ROLLBACK:invalid transaction termination (1 row) drop function fxx(); create or replace function fxx() returns void as $$ declare x int; begin declare x int; begin end; end; $$ language plpgsql; select * from plpgsql_check_function('fxx()'); plpgsql_check_function ------------------------------------------------------------------------------------------ warning extra:00000:5:statement block:variable "x" shadows a previously defined variable Hint: SET plpgsql.extra_warnings TO 'shadowed_variables' warning:00000:2:DECLARE:unused variable "x" warning:00000:4:DECLARE:unused variable "x" (4 rows) select * from plpgsql_check_function('fxx()', extra_warnings := false); plpgsql_check_function --------------------------------------------- warning:00000:2:DECLARE:unused variable "x" warning:00000:4:DECLARE:unused variable "x" (2 rows) drop function fxx(); create or replace function fxx(in a int, in b int, out c int, out d int) as $$ begin c := a; end; $$ language plpgsql; select * from plpgsql_check_function('fxx(int, int)'); plpgsql_check_function ------------------------------------------------- warning extra:00000:unused parameter "b" warning extra:00000:unmodified OUT variable "d" (2 rows) create or replace function fxx(in a int, in b int, out c int, out d int) as $$ begin c := d; end; $$ language plpgsql; select * from plpgsql_check_function('fxx(int, int)'); plpgsql_check_function ------------------------------------------------- warning extra:00000:unused parameter "a" warning extra:00000:unused parameter "b" warning extra:00000:unmodified OUT variable "d" (3 rows) create type ct as (a int, b int); create or replace function fxx(a ct, b ct, OUT c ct, OUT d ct) as $$ begin c.a := a.a; end; $$ language plpgsql; select * from plpgsql_check_function('fxx(ct, ct)'); plpgsql_check_function ----------------------------------------------------------------------- warning extra:00000:unused parameter "b" warning extra:00000:composite OUT variable "c" is not single argument warning extra:00000:composite OUT variable "d" is not single argument warning extra:00000:unmodified OUT variable "d" (4 rows) create or replace function fxx(a ct, b ct, OUT c ct, OUT d ct) as $$ begin c.a := d.a; end; $$ language plpgsql; select * from plpgsql_check_function('fxx(ct, ct)'); plpgsql_check_function ----------------------------------------------------------------------- warning extra:00000:unused parameter "a" warning extra:00000:unused parameter "b" warning extra:00000:composite OUT variable "c" is not single argument warning extra:00000:composite OUT variable "d" is not single argument warning extra:00000:unmodified OUT variable "d" (5 rows) create or replace function tx(a int) returns int as $$ declare a int; ax int; begin declare ax int; begin ax := 10; end; a := 10; return 20; end; $$ language plpgsql; select * from plpgsql_check_function('tx(int)'); plpgsql_check_function ------------------------------------------------------------------------------------------- warning:00000:3:statement block:parameter "a" is shadowed Detail: Local variable shadows function parameter. warning extra:00000:5:statement block:variable "ax" shadows a previously defined variable Hint: SET plpgsql.extra_warnings TO 'shadowed_variables' warning:00000:2:DECLARE:unused variable "ax" warning extra:00000:2:DECLARE:never read variable "a" warning extra:00000:4:DECLARE:never read variable "ax" warning extra:00000:unused parameter "a" (8 rows) create type xt as (a int, b int, c int); create or replace function fx_xt(out x xt) as $$ declare l xt; a int; begin return; end; $$ language plpgsql; select * from plpgsql_check_function('fx_xt()'); plpgsql_check_function ------------------------------------------------- warning:00000:2:DECLARE:unused variable "l" warning:00000:3:DECLARE:unused variable "a" warning extra:00000:unmodified OUT variable "x" (3 rows) drop function fx_xt(); create or replace function fx_xt(out x xt) as $$ declare l xt; a int; begin x.c := 1000; return; end; $$ language plpgsql; select * from plpgsql_check_function('fx_xt()'); plpgsql_check_function --------------------------------------------- warning:00000:2:DECLARE:unused variable "l" warning:00000:3:DECLARE:unused variable "a" (2 rows) drop function fx_xt(); create or replace function fx_xt(out x xt, out y xt) as $$ declare c1 xt; c2 xt; begin return; end; $$ language plpgsql; select * from plpgsql_check_function('fx_xt()'); plpgsql_check_function ----------------------------------------------------------------------- warning:00000:2:DECLARE:unused variable "c1" warning:00000:2:DECLARE:unused variable "c2" warning extra:00000:composite OUT variable "x" is not single argument warning extra:00000:unmodified OUT variable "x" warning extra:00000:composite OUT variable "y" is not single argument warning extra:00000:unmodified OUT variable "y" (6 rows) drop function fx_xt(); create or replace function fx_xt(out x xt, out y xt) as $$ declare c1 xt; c2 xt; begin x.a := 100; y := row(10,20,30); return; end; $$ language plpgsql; select * from plpgsql_check_function('fx_xt()'); plpgsql_check_function ----------------------------------------------------------------------- warning:00000:2:DECLARE:unused variable "c1" warning:00000:2:DECLARE:unused variable "c2" warning extra:00000:composite OUT variable "x" is not single argument warning extra:00000:composite OUT variable "y" is not single argument (4 rows) drop function fx_xt(); create or replace function fx_xt(out x xt, out z int) as $$ begin return; end; $$ language plpgsql; select * from plpgsql_check_function('fx_xt()'); plpgsql_check_function ----------------------------------------------------------------------- warning extra:00000:composite OUT variable "x" is not single argument warning extra:00000:unmodified OUT variable "x" warning extra:00000:unmodified OUT variable "z" (3 rows) drop function fx_xt(); drop type xt; -- missing RETURN create or replace function fx_flow() returns int as $$ begin raise notice 'kuku'; end; $$ language plpgsql; select fx_flow(); NOTICE: kuku ERROR: control reached end of function without RETURN CONTEXT: PL/pgSQL function fx_flow() select * from plpgsql_check_function('fx_flow()'); plpgsql_check_function ------------------------------------------------------------ error:2F005:control reached end of function without RETURN (1 row) -- ok create or replace function fx_flow() returns int as $$ declare a int; begin if a > 10 then return a; end if; return 10; end; $$ language plpgsql; select * from plpgsql_check_function('fx_flow()'); plpgsql_check_function ------------------------ (0 rows) -- dead code create or replace function fx_flow() returns int as $$ declare a int; begin if a > 10 then return a; else return a + 1; end if; return 10; end; $$ language plpgsql; select * from plpgsql_check_function('fx_flow()'); plpgsql_check_function ----------------------------------------------- warning extra:00000:9:RETURN:unreachable code (1 row) -- missing return create or replace function fx_flow() returns int as $$ declare a int; begin if a > 10 then return a; end if; end; $$ language plpgsql; select * from plpgsql_check_function('fx_flow()'); plpgsql_check_function -------------------------------------------------------------------- warning extra:2F005:control reached end of function without RETURN (1 row) drop function fx_flow(); create or replace function fx_flow(in p_param1 integer) returns text as $$ declare z1 text; begin if p_param1 is not null then z1 := '1111'; return z1; else z1 := '222222'; end if; return z1; end; $$ language plpgsql stable; select * from plpgsql_check_function_tb('fx_flow(integer)'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+---------+--------+------+-------+----------+-------+--------- (0 rows) create or replace function fx_flow(in p_param1 integer) returns text as $$ declare z1 text; begin if p_param1 is not null then z1 := '1111'; return z1; else z1 := '222222'; raise exception 'stop'; end if; return z1; end; $$ language plpgsql stable; select * from plpgsql_check_function_tb('fx_flow(integer)'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+------------------+--------+------+---------------+----------+-------+--------- fx_flow | 12 | RETURN | 00000 | unreachable code | | | warning extra | | | (1 row) drop function fx_flow(); ERROR: function fx_flow() does not exist drop function fx(int); ERROR: function fx(integer) does not exist create or replace function fx(x int) returns table(y int) as $$ begin return query select x union select x; end $$ language plpgsql; select * from fx(10); y ---- 10 (1 row) select * from plpgsql_check_function_tb('fx(int)'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+---------+--------+------+-------+----------+-------+--------- (0 rows) drop function fx(int); create or replace function fx(x int) returns table(y int, z int) as $$ begin return query select x,x+1 union select x, x+1; end $$ language plpgsql; select * from fx(10); y | z ----+---- 10 | 11 (1 row) select * from plpgsql_check_function_tb('fx(int)'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+---------+--------+------+-------+----------+-------+--------- (0 rows) drop function fx(int); create table xx(a int); create or replace function fx(x int) returns int as $$ declare _a int; begin begin select a from xx into strict _a where a = x; return _a; exception when others then null; end; return -1; end; $$ language plpgsql; select * from plpgsql_check_function_tb('fx(int)'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+---------+--------+------+-------+----------+-------+--------- (0 rows) drop table xx; create or replace function fx(x int) returns int as $$ begin begin if (x > 0) then raise exception 'xxx' using errcode = 'XX888'; else raise exception 'yyy' using errcode = 'YY888'; end if; return -1; -- dead code; end; return -1; end; $$ language plpgsql; select * from plpgsql_check_function_tb('fx(int)'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+------------------+--------+------+---------------+----------+-------+--------- fx | 9 | RETURN | 00000 | unreachable code | | | warning extra | | | fx | 11 | RETURN | 00000 | unreachable code | | | warning extra | | | (2 rows) create or replace function fx(x int) returns int as $$ begin begin if (x > 0) then raise exception 'xxx' using errcode = 'XX888'; else raise exception 'yyy' using errcode = 'YY888'; end if; exception when sqlstate 'XX888' then null; when sqlstate 'YY888' then null; end; end; -- missing return; $$ language plpgsql; select * from plpgsql_check_function_tb('fx(int)'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+------------------------------------------------+--------+------+-------+----------+-------+--------- fx | | | 2F005 | control reached end of function without RETURN | | | error | | | (1 row) create or replace function fx(x int) returns int as $$ begin begin if (x > 0) then raise exception 'xxx' using errcode = 'XX888'; else raise exception 'yyy' using errcode = 'YY888'; end if; exception when others then return 10; end; end; -- ok now $$ language plpgsql; select * from plpgsql_check_function_tb('fx(int)'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+---------+--------+------+-------+----------+-------+--------- (0 rows) --false alarm reported by Filip Zach create type testtype as (id integer); create or replace function fx() returns testtype as $$ begin return row(1); end; $$ language plpgsql; select * from fx(); id ---- 1 (1 row) select fx(); fx ----- (1) (1 row) select * from plpgsql_check_function('fx()'); plpgsql_check_function ------------------------ (0 rows) drop function fx(); create function out1(OUT f1 int, OUT f2 int) returns setof record as $$ begin for f1, f2 in execute $q$ select 1, 2 $q$ loop return next; end loop; end $$ language plpgsql; select * from plpgsql_check_function('out1()'); plpgsql_check_function ------------------------ (0 rows) drop function out1(); create function out1(OUT f1 int, OUT f2 int) returns setof record as $$ begin for f1, f2 in select 1, 2 loop return next; end loop; end $$ language plpgsql; select * from plpgsql_check_function('out1()'); plpgsql_check_function ------------------------ (0 rows) drop function out1(); -- never read variable detection create function a() returns int as $$ declare foo int; begin foo := 2; return 1; end; $$ language plpgsql; select * from plpgsql_check_function('a()'); plpgsql_check_function --------------------------------------------------------- warning extra:00000:2:DECLARE:never read variable "foo" (1 row) drop function a(); -- issue #29 false unused variable create or replace function f1(in p_cursor refcursor) returns void as $body$ declare z_offset integer; begin z_offset := 10; move absolute z_offset from p_cursor; end; $body$ language 'plpgsql' stable; select * from plpgsql_check_function_tb('f1(refcursor)'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+---------+--------+------+-------+----------+-------+--------- (0 rows) drop function f1(refcursor); -- issue #30 segfault due NULL refname create or replace function test(a varchar) returns void as $$ declare x cursor (_a varchar) for select _a; begin open x(a); end; $$ language plpgsql; select * from plpgsql_check_function_tb('test(varchar)'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+-------------------------+--------+------+---------------+----------+-------+--------- test | 2 | DECLARE | 00000 | never read variable "x" | | | warning extra | | | (1 row) drop function test(varchar); create or replace function test() returns void as $$ declare x numeric; begin x := NULL; end; $$ language plpgsql; select * from plpgsql_check_function('test()'); plpgsql_check_function ------------------------------------------------------- warning extra:00000:2:DECLARE:never read variable "x" (1 row) drop function test(); create table testtable(a int); create or replace function test() returns int as $$ declare r testtable; begin select * into r from testtable; return r.a; end; $$ language plpgsql; select * from plpgsql_check_function('test()'); plpgsql_check_function ------------------------ (0 rows) set check_function_bodies to on; drop table testtable; create table testtable(a int, b int); create or replace function test() returns int as $$ declare r testtable; begin select * into r from testtable; return r.a; end; $$ language plpgsql; alter table testtable drop column b; -- expected false alarm on PostgreSQL 10 and older -- there is not possibility to enforce recompilation -- before checking. select * from plpgsql_check_function('test()'); plpgsql_check_function ------------------------ (0 rows) drop function test(); -- issue #32 create table bigtable(id bigint, v varchar); create or replace function test() returns void as $$ declare r record; _id numeric; begin select * into r from bigtable where id = _id; for r in select * from bigtable where _id = id loop end loop; if (exists(select * from bigtable where id = _id)) then end if; end; $$ language plpgsql; select test(); test ------ (1 row) -- should to show performance warnings select * from plpgsql_check_function('test()', performance_warnings := true); plpgsql_check_function ------------------------------------------------------------------------------------------------------------------------------- performance:42804:6:SQL statement:implicit cast of attribute caused by different PLpgSQL variable type in WHERE clause Query: select * from bigtable where id = _id -- ^ Detail: An index of some attribute cannot be used, when variable, used in predicate, has not right type like a attribute Hint: Check a variable type - int versus numeric performance:42804:7:FOR over SELECT rows:implicit cast of attribute caused by different PLpgSQL variable type in WHERE clause Query: select * from bigtable where _id = id -- ^ Detail: An index of some attribute cannot be used, when variable, used in predicate, has not right type like a attribute Hint: Check a variable type - int versus numeric performance:42804:10:IF:implicit cast of attribute caused by different PLpgSQL variable type in WHERE clause Query: (exists(select * from bigtable where id = _id)) -- ^ Detail: An index of some attribute cannot be used, when variable, used in predicate, has not right type like a attribute Hint: Check a variable type - int versus numeric warning extra:00000:3:DECLARE:never read variable "r" (16 rows) create or replace function test() returns void as $$ declare r record; _id bigint; begin select * into r from bigtable where id = _id; for r in select * from bigtable where _id = id loop end loop; if (exists(select * from bigtable where id = _id)) then end if; end; $$ language plpgsql; -- there are not any performance issue now select * from plpgsql_check_function('test()', performance_warnings := true); plpgsql_check_function ------------------------------------------------------- warning extra:00000:3:DECLARE:never read variable "r" (1 row) -- nextval, currval and setval test create table test_table(); create or replace function testseq() returns void as $$ begin perform nextval('test_table'); perform currval('test_table'); perform setval('test_table', 10); perform setval('test_table', 10, true); end; $$ language plpgsql; -- should to fail select testseq(); ERROR: "test_table" is not a sequence CONTEXT: SQL statement "SELECT nextval('test_table')" PL/pgSQL function testseq() line 3 at PERFORM select * from plpgsql_check_function('testseq()', fatal_errors := false); plpgsql_check_function ------------------------------------------------------ error:42809:3:PERFORM:"test_table" is not a sequence Query: SELECT nextval('test_table') -- ^ error:42809:4:PERFORM:"test_table" is not a sequence Query: SELECT currval('test_table') -- ^ error:42809:5:PERFORM:"test_table" is not a sequence Query: SELECT setval('test_table', 10) -- ^ error:42809:6:PERFORM:"test_table" is not a sequence Query: SELECT setval('test_table', 10, true) -- ^ (12 rows) drop function testseq(); drop table test_table; -- tests designed for PostgreSQL 9.2 set check_function_bodies to off; create table t1(a int, b int); create function f1() returns void as $$ begin if false then update t1 set c = 30; end if; if false then raise notice '%', r.c; end if; end; $$ language plpgsql; select f1(); f1 ---- (1 row) select * from plpgsql_check_function_tb('f1()', fatal_errors := true); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+---------------+----------+--------------------------------------------+--------+------+-------+----------+----------------------+--------- f1 | 4 | SQL statement | 42703 | column "c" of relation "t1" does not exist | | | error | 15 | update t1 set c = 30 | (1 row) select * from plpgsql_check_function_tb('f1()', fatal_errors := false); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+---------------+----------+--------------------------------------------+--------+------+-------+----------+----------------------+--------- f1 | 4 | SQL statement | 42703 | column "c" of relation "t1" does not exist | | | error | 15 | update t1 set c = 30 | f1 | 7 | RAISE | 42P01 | missing FROM-clause entry for table "r" | | | error | 1 | r.c | (2 rows) select * from plpgsql_check_function_tb('f1()'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+---------------+----------+--------------------------------------------+--------+------+-------+----------+----------------------+--------- f1 | 4 | SQL statement | 42703 | column "c" of relation "t1" does not exist | | | error | 15 | update t1 set c = 30 | (1 row) select f1(); f1 ---- (1 row) drop function f1(); create or replace function f1() returns void as $$ begin if false then raise notice '%', 1, 2; end if; end; $$ language plpgsql; select f1(); ERROR: too many parameters specified for RAISE CONTEXT: compilation of PL/pgSQL function "f1" near line 4 select * from plpgsql_check_function_tb('f1()'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+-----------------------------------------+--------+------+-------+----------+-------+--------------------------------------------------- f1 | | | 42601 | too many parameters specified for RAISE | | | error | | | compilation of PL/pgSQL function "f1" near line 4 (1 row) select f1(); ERROR: too many parameters specified for RAISE CONTEXT: compilation of PL/pgSQL function "f1" near line 4 drop function f1(); create or replace function f1() returns void as $$ begin if false then raise notice '% %'; end if; end; $$ language plpgsql; select f1(); ERROR: too few parameters specified for RAISE CONTEXT: compilation of PL/pgSQL function "f1" near line 4 select * from plpgsql_check_function_tb('f1()'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+----------------------------------------+--------+------+-------+----------+-------+--------------------------------------------------- f1 | | | 42601 | too few parameters specified for RAISE | | | error | | | compilation of PL/pgSQL function "f1" near line 4 (1 row) select f1(); ERROR: too few parameters specified for RAISE CONTEXT: compilation of PL/pgSQL function "f1" near line 4 drop function f1(); create or replace function ml_trg() returns trigger as $$ #option dump declare begin if TG_OP = 'INSERT' then if NEW.status_from IS NULL then begin -- performance issue only select status into NEW.status_from from pa where pa_id = NEW.pa_id; -- nonexist target value select status into NEW.status_from_xxx from pa where pa_id = NEW.pa_id; exception when DATA_EXCEPTION then new.status_from := 'DE'; end; end if; end if; if TG_OP = 'DELETE' then return OLD; else return NEW; end if; exception when OTHERS then NULL; if TG_OP = 'DELETE' then return OLD; else return NEW; end if; end; $$ language plpgsql; select * from plpgsql_check_function('ml_trg()', 'ml', performance_warnings := true); plpgsql_check_function -------------------------------------------------------------------------- error:42703:13:SQL statement:record "new" has no field "status_from_xxx" (1 row) create or replace function fx2() returns void as $$ declare _pa pa; begin select pa.id into _pa.id from pa limit 1; select pa.pa_id into _pa.pa_id from pa limit 1; end; $$ language plpgsql; select * from plpgsql_check_function('fx2()', performance_warnings := true); plpgsql_check_function --------------------------------------------------------- warning extra:00000:2:DECLARE:never read variable "_pa" (1 row) drop function fx2(); create or replace function fx2() returns void as $$ declare _pa pa; begin _pa.id := (select pa.id from pa limit 1); _pa.pa_id := (select pa.pa_id from pa limit 1); end; $$ language plpgsql; select * from plpgsql_check_function('fx2()', performance_warnings := true); plpgsql_check_function --------------------------------------------------------- warning extra:00000:2:DECLARE:never read variable "_pa" (1 row) drop function fx2(); create or replace function test_lab() returns void as $$ begin <> for a in 1..3 loop <> BEGIN <> for b in 8..9 loop if a=2 then continue sub; end if; raise notice '% %', a, b; end loop inner; END sub; end loop outer; end; $$ language plpgsql; select test_lab(); ERROR: block label "sub" cannot be used in CONTINUE LINE 10: continue sub; ^ QUERY: begin <> for a in 1..3 loop <> BEGIN <> for b in 8..9 loop if a=2 then continue sub; end if; raise notice '% %', a, b; end loop inner; END sub; end loop outer; end; CONTEXT: compilation of PL/pgSQL function "test_lab" near line 10 select * from plpgsql_check_function('test_lab()', performance_warnings := true); plpgsql_check_function ------------------------------------------------------------------- error:42601:block label "sub" cannot be used in CONTINUE Query: begin <> for a in 1..3 loop <> BEGIN <> for b in 8..9 loop if a=2 then continue sub; -- ^ end if; raise notice '% %', a, b; end loop inner; END sub; end loop outer; end; Context: compilation of PL/pgSQL function "test_lab" near line 10 (20 rows) create or replace function test_lab() returns void as $$ begin continue; end; $$ language plpgsql; select test_lab(); ERROR: CONTINUE cannot be used outside a loop LINE 3: continue; ^ QUERY: begin continue; end; CONTEXT: compilation of PL/pgSQL function "test_lab" near line 3 select * from plpgsql_check_function('test_lab()', performance_warnings := true); plpgsql_check_function ------------------------------------------------------------------ error:42601:CONTINUE cannot be used outside a loop Query: begin continue; -- ^ end; Context: compilation of PL/pgSQL function "test_lab" near line 3 (8 rows) create type _exception_type as ( state text, message text, detail text); create or replace function f1() returns void as $$ declare _exception record; begin _exception := NULL::_exception_type; exception when others then get stacked diagnostics _exception.state = RETURNED_SQLSTATE, _exception.message = MESSAGE_TEXT, _exception.detail = PG_EXCEPTION_DETAIL, _exception.hint = PG_EXCEPTION_HINT; end; $$ language plpgsql; select f1(); f1 ---- (1 row) select * from plpgsql_check_function('f1()'); plpgsql_check_function ------------------------------------------------------------------------------- error:42703:7:GET STACKED DIAGNOSTICS:record "_exception" has no field "hint" (1 row) drop function f1(); drop type _exception_type; drop table t1; create function myfunc1(a int, b float) returns integer as $$ begin end $$ language plpgsql; create function myfunc2(a int, b float) returns integer as $$ begin end $$ language plpgsql; create function myfunc3(a int, b float) returns integer as $$ begin end $$ language plpgsql; create function myfunc4(a int, b float) returns integer as $$ begin end $$ language plpgsql; create function opfunc1(a int, b float) returns integer as $$ begin end $$ language plpgsql; create operator *** (procedure = opfunc1, leftarg = int, rightarg = float); create table mytable(a int); create table myview as select * from mytable; create function testfunc(a int, b float) returns void as $$ declare x integer; begin raise notice '%', myfunc1(a, b); x := myfunc2(a, b) operator(public.***) 1; perform myfunc3(m.a, b) from myview m; insert into mytable select myfunc4(a, b); end; $$ language plpgsql; select * from plpgsql_check_function('testfunc(int,float)'); plpgsql_check_function ------------------------------------------------------- warning extra:00000:2:DECLARE:never read variable "x" (1 row) select type, schema, name, params from plpgsql_show_dependency_tb('testfunc(int,float)'); type | schema | name | params ----------+--------+---------+---------------------------- FUNCTION | public | myfunc1 | (integer,double precision) FUNCTION | public | myfunc2 | (integer,double precision) FUNCTION | public | myfunc3 | (integer,double precision) FUNCTION | public | myfunc4 | (integer,double precision) OPERATOR | public | *** | (integer,double precision) RELATION | public | mytable | RELATION | public | myview | (7 rows) drop function testfunc(int, float); drop function myfunc1(int, float); drop function myfunc2(int, float); drop function myfunc3(int, float); drop function myfunc4(int, float); drop table mytable; drop view myview; ERROR: "myview" is not a view HINT: Use DROP TABLE to remove a table. -- issue #221 create type foo_type as (a int); create table foo(a int); create function testfuncdep() returns void as $$ declare x foo_type; y foo; begin end; $$ language plpgsql; select type, schema, name from plpgsql_show_dependency_tb('testfuncdep'); type | schema | name ----------+--------+---------- RELATION | public | foo TYPE | public | foo_type (2 rows) drop function testfuncdep(); drop type foo_type; drop table foo; -- issue #221 - dependency report on trigger function should not crash -- (NEW and OLD record variables have no datatype) create table foo(a int, b text); create function foo_trg_func() returns trigger as $$ begin new.b := 'x'; return new; end; $$ language plpgsql; create trigger foo_trg before insert on foo for each row execute function foo_trg_func(); select type, schema, name from plpgsql_show_dependency_tb('foo_trg_func', 'foo'); type | schema | name ------+--------+------ (0 rows) drop trigger foo_trg on foo; drop function foo_trg_func(); drop table foo; -- issue #34 create or replace function testcase() returns bool as $$ declare x int; begin set local search_path to public, test; case x when 1 then return true; else return false; end case; end; $$ language plpgsql; -- should not to raise warning select * from plpgsql_check_function('testcase()'); plpgsql_check_function ------------------------ (0 rows) drop function testcase(); -- Adam's Bartoszewicz example create or replace function public.test12() returns refcursor language plpgsql as $body$ declare rc refcursor; begin open rc scroll for select pc.* from pg_cast pc; return rc; end; $body$; -- should not returns false alarm select * from plpgsql_check_function('test12()'); plpgsql_check_function ------------------------ (0 rows) drop function public.test12(); -- should to show performance warning on bad flag create or replace function flag_test1(int) returns int as $$ begin return $1 + 10; end; $$ language plpgsql stable; create table fufu(a int); create or replace function flag_test2(int) returns int as $$ begin return (select * from fufu limit 1); end; $$ language plpgsql volatile; select * from plpgsql_check_function('flag_test1(int)', performance_warnings := true); plpgsql_check_function ----------------------------------------------------------------------------------------- performance:00000:routine is marked as STABLE, should be IMMUTABLE Hint: When you fix this issue, please, recheck other functions that uses this function. (2 rows) select * from plpgsql_check_function('flag_test2(int)', performance_warnings := true); plpgsql_check_function ----------------------------------------------------------------------------------------- warning extra:00000:unused parameter "$1" performance:00000:routine is marked as VOLATILE, should be STABLE Hint: When you fix this issue, please, recheck other functions that uses this function. (3 rows) drop table fufu; drop function flag_test1(int); drop function flag_test2(int); create or replace function rrecord01() returns setof record as $$ begin return query select 1,2; end; $$ language plpgsql; create or replace function rrecord02() returns record as $$ begin return row(10,20,30); end; $$ language plpgsql; create type record03 as (a int, b int); create or replace function rrecord03() returns record03 as $$ declare r record; begin r := row(1); return r; end; $$ language plpgsql; -- should not to raise false alarms select * from plpgsql_check_function('rrecord01'); plpgsql_check_function ------------------------ (0 rows) select * from plpgsql_check_function('rrecord02'); plpgsql_check_function ------------------------ (0 rows) -- should detect different return but still detect return select * from plpgsql_check_function('rrecord03', fatal_errors => false); plpgsql_check_function ---------------------------------------------------------------------------------- error:42804:5:RETURN:returned record type does not match expected record type Detail: Number of returned columns (1) does not match expected column count (2). (2 rows) drop function rrecord01(); drop function rrecord02(); drop function rrecord03(); drop type record03; create or replace function bugfunc01() returns void as $$ declare cvar cursor(a int, b int) for select a + b from generate_series(1,b); begin for t in cvar(1,3) loop raise notice '%', t; end loop; end; $$ language plpgsql; select bugfunc01(); NOTICE: (4) NOTICE: (4) NOTICE: (4) bugfunc01 ----------- (1 row) select * from plpgsql_check_function('bugfunc01'); plpgsql_check_function ------------------------ (0 rows) create or replace function bugfunc02() returns void as $$ declare cvar cursor(a int, b int) for select a + b from generate_series(1,b); begin open cvar(10,20); close cvar; end; $$ language plpgsql; select bugfunc02(); bugfunc02 ----------- (1 row) select * from plpgsql_check_function('bugfunc02'); plpgsql_check_function ------------------------ (0 rows) create or replace function bugfunc03() returns void as $$ declare cvar cursor(a int, b int) for select a + b from not_exists_table; begin open cvar(10,20); close cvar; end; $$ language plpgsql; select bugfunc03(); ERROR: relation "not_exists_table" does not exist LINE 1: select a + b from not_exists_table ^ QUERY: select a + b from not_exists_table CONTEXT: PL/pgSQL function bugfunc03() line 5 at OPEN select * from plpgsql_check_function('bugfunc03'); plpgsql_check_function --------------------------------------------------------------- error:42P01:5:OPEN:relation "not_exists_table" does not exist Query: select a + b from not_exists_table -- ^ (3 rows) create or replace function f1(out cr refcursor) as $$ begin end; $$ language plpgsql; -- should to raise warning select * from plpgsql_check_function('f1()'); plpgsql_check_function -------------------------------------------------- warning extra:00000:unmodified OUT variable "cr" (1 row) create or replace function f1(out cr refcursor) as $$ begin open cr for select 1; end; $$ language plpgsql; -- should not to raise warning, see issue #43 select * from plpgsql_check_function('f1()'); plpgsql_check_function ------------------------ (0 rows) drop function f1(); create table testt(a int); create or replace function testt_trg_func() returns trigger as $$ begin return new; end; $$ language plpgsql; create trigger testt_trg before insert or update on testt for each row execute procedure testt_trg_func(); create or replace function maintaince_function() returns void as $$ begin alter table testt disable trigger testt_trg; alter table testt enable trigger testt_trg; end; $$ language plpgsql; -- should not to crash select * from plpgsql_check_function_tb('maintaince_function()', 0, true, true, true); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+---------+--------+------+-------+----------+-------+--------- (0 rows) drop function maintaince_function(); drop trigger testt_trg on testt; drop function testt_trg_func(); drop table testt; create or replace function test_crash() returns void as $$ declare ec int default buggyfunc(10); begin select * into ec from buggytab; end; $$ language plpgsql; -- should not to crash select * from plpgsql_check_function('test_crash', fatal_errors := false); plpgsql_check_function ------------------------------------------------------------------------------ error:42883:4:statement block:function buggyfunc(integer) does not exist Query: buggyfunc(10) -- ^ Detail: There is no function of that name. Context: during statement block local variable "ec" initialization on line 3 error:42P01:5:SQL statement:relation "buggytab" does not exist Query: select * from buggytab -- ^ warning extra:00000:3:DECLARE:never read variable "ec" (9 rows) select * from plpgsql_check_function('test_crash', fatal_errors := true); plpgsql_check_function ------------------------------------------------------------------------------ error:42883:4:statement block:function buggyfunc(integer) does not exist Query: buggyfunc(10) -- ^ Detail: There is no function of that name. Context: during statement block local variable "ec" initialization on line 3 (5 rows) drop function test_crash(); -- fix false alarm reported by Piotr Stepniewski create or replace function public.fx() returns void language plpgsql as $function$ begin raise exception 'xxx'; end; $function$; -- show raise nothing select * from plpgsql_check_function('fx()'); plpgsql_check_function ------------------------ (0 rows) create table errtab( message text, code character(5) ); create or replace function public.fx() returns void language plpgsql as $function$ declare var errtab%rowtype; begin raise exception using message = var.message, errcode = var.code; end; $function$; -- should not to crash select * from plpgsql_check_function('fx()'); plpgsql_check_function ------------------------ (0 rows) create or replace function public.fx() returns void language plpgsql as $function$ declare var errtab%rowtype; begin raise exception using message = var.message, errcode = var.code, hint = var.hint; end; $function$; -- should not to crash select * from plpgsql_check_function('fx()'); plpgsql_check_function ------------------------------------------------------ error:42703:5:RAISE:record "var" has no field "hint" Context: PL/pgSQL expression "var.hint" (2 rows) drop function fx(); create or replace function foo_format(a text, b text) returns void as $$ declare s text; begin s := format('%s'); -- should to raise error s := format('%s %10s', a, b); -- should be ok s := format('%s %s', a, b, a); -- should to raise warning s := format('%s %d', a, b); -- should to raise error raise notice '%', s; end; $$ language plpgsql; select * from plpgsql_check_function('foo_format', fatal_errors := false); plpgsql_check_function ------------------------------------------------------------------- error:22023:4:assignment:too few arguments for format() Query: s := format('%s') -- ^ Context: at assignment to variable "s" declared on line 2 warning:00000:6:assignment:unused parameters of function "format" Query: s := format('%s %s', a, b, a) -- ^ Context: at assignment to variable "s" declared on line 2 error:22023:7:assignment:unrecognized format() type specifier "d" Query: s := format('%s %d', a, b) -- ^ Context: at assignment to variable "s" declared on line 2 (12 rows) drop function foo_format(text, text); create or replace function dyn_sql_1() returns void as $$ declare v varchar; n int; begin execute 'select ' || n; -- ok execute 'select ' || quote_literal(v); -- ok execute 'select ' || v; -- vulnerable execute format('select * from %I', v); -- ok execute format('select * from %s', v); -- vulnerable execute 'select $1' using v; -- ok execute 'select 1'; -- ok execute 'select 1' using v; -- warning execute 'select $1'; -- error end; $$ language plpgsql; select * from plpgsql_check_function('dyn_sql_1', security_warnings := true, fatal_errors := false); plpgsql_check_function ------------------------------------------------------------------------------------------ security:00000:8:EXECUTE:text type variable is not sanitized Query: 'select ' || v -- ^ Detail: The EXECUTE expression is SQL injection vulnerable. Hint: Use quote_ident, quote_literal or format function to secure variable. security:00000:10:EXECUTE:text type variable is not sanitized Query: format('select * from %s', v) -- ^ Detail: The EXECUTE expression is SQL injection vulnerable. Hint: Use quote_ident, quote_literal or format function to secure variable. warning:00000:13:EXECUTE:values passed to EXECUTE statement by USING clause was not used error:42P02:14:EXECUTE:there is no parameter $1 Query: select $1 -- ^ (14 rows) drop function dyn_sql_1(); create type tp as (a int, b int); create or replace function dyn_sql_2() returns void as $$ declare r tp; result int; begin select 10 a, 20 b into r; raise notice '%', r.a; execute 'select $1.a + $1.b' into result using r; execute 'select $1.c' into result using r; -- error raise notice '%', result; end; $$ language plpgsql; select * from plpgsql_check_function('dyn_sql_2', security_warnings := true); plpgsql_check_function ------------------------------------------------------------ error:42703:9:EXECUTE:column "c" not found in data type tp Query: select $1.c -- ^ (3 rows) drop function dyn_sql_2(); drop type tp; /* * Should not to work * * note: plpgsql doesn't support passing some necessary details for record * type. The parser setup for dynamic SQL column doesn't use ref hooks, and * then it cannot to pass TupleDesc info to query anyway. */ create or replace function dyn_sql_2() returns void as $$ declare r record; result int; begin select 10 a, 20 b into r; raise notice '%', r.a; execute 'select $1.a + $1.b' into result using r; raise notice '%', result; end; $$ language plpgsql; select dyn_sql_2(); --should to fail NOTICE: 10 ERROR: could not identify column "a" in record data type LINE 1: select $1.a + $1.b ^ QUERY: select $1.a + $1.b CONTEXT: PL/pgSQL function dyn_sql_2() line 8 at EXECUTE select * from plpgsql_check_function('dyn_sql_2', security_warnings := true); plpgsql_check_function ------------------------------------------------------------------------- error:42703:8:EXECUTE:could not identify column "a" in record data type Query: select $1.a + $1.b -- ^ (3 rows) drop function dyn_sql_2(); create or replace function dyn_sql_3() returns void as $$ declare r int; begin execute 'select $1' into r using 1; raise notice '%', r; end $$ language plpgsql; select dyn_sql_3(); NOTICE: 1 dyn_sql_3 ----------- (1 row) -- should be ok select * from plpgsql_check_function('dyn_sql_3'); plpgsql_check_function ------------------------ (0 rows) create or replace function dyn_sql_3() returns void as $$ declare r record; begin execute 'select $1 as a, $2 as b' into r using 1, 2; raise notice '% %', r.a, r.b; end $$ language plpgsql; select dyn_sql_3(); NOTICE: 1 2 dyn_sql_3 ----------- (1 row) -- should be ok select * from plpgsql_check_function('dyn_sql_3'); plpgsql_check_function ------------------------ (0 rows) create or replace function dyn_sql_3() returns void as $$ declare r record; begin execute 'create table foo(a int)' into r using 1, 2; raise notice '% %', r.a, r.b; end $$ language plpgsql; -- raise a error select * from plpgsql_check_function('dyn_sql_3'); plpgsql_check_function ----------------------------------------------------------------------------------------- warning:00000:4:EXECUTE:values passed to EXECUTE statement by USING clause was not used error:XX000:4:EXECUTE:expression does not return data (2 rows) create or replace function dyn_sql_3() returns void as $$ declare r1 int; r2 int; begin execute 'select 1' into r1, r2 using 1, 2; raise notice '% %', r1, r2; end $$ language plpgsql; -- raise a error select * from plpgsql_check_function('dyn_sql_3'); plpgsql_check_function ----------------------------------------------------------------------------------------- warning:00000:4:EXECUTE:values passed to EXECUTE statement by USING clause was not used warning:00000:4:EXECUTE:too few attributes for target variables Query: select 1 -- ^ Detail: There are more target variables than output columns in query. Hint: Check target variables in SELECT INTO statement. Context: at EXECUTE to r1, r2 variables (7 rows) drop function dyn_sql_3(); create or replace function dyn_sql_3() returns void as $$ declare r record; begin for r in execute 'select 1 as a, 2 as b' loop raise notice '%', r.a; end loop; end $$ language plpgsql; -- should be ok select * from plpgsql_check_function('dyn_sql_3'); plpgsql_check_function ------------------------ (0 rows) drop function dyn_sql_3(); create or replace function dyn_sql_3() returns void as $$ declare r record; begin for r in execute 'select 1 as a, 2 as b' loop raise notice '%', r.c; end loop; end $$ language plpgsql; -- should be error select * from plpgsql_check_function('dyn_sql_3'); plpgsql_check_function ------------------------------------------------- error:42703:6:RAISE:record "r" has no field "c" Context: PL/pgSQL expression "r.c" (2 rows) drop function dyn_sql_3(); create or replace function dyn_sql_3() returns void as $$ declare r record; v text = 'select 10 a, 20 b't; begin for r in execute v loop raise notice '%', r.a; end loop; end $$ language plpgsql; -- should be ok select * from plpgsql_check_function('dyn_sql_3'); plpgsql_check_function ------------------------ (0 rows) drop function dyn_sql_3(); create or replace function dyn_sql_4() returns table(ax int, bx int) as $$ begin return query execute 'select 10, 20'; return; end; $$ language plpgsql; -- should be ok select * from plpgsql_check_function('dyn_sql_4()'); plpgsql_check_function ------------------------ (0 rows) create or replace function dyn_sql_4() returns table(ax int, bx int) as $$ begin return query execute 'select 10, 20, 30'; return; end; $$ language plpgsql; select * from dyn_sql_4(); ERROR: structure of query does not match function result type DETAIL: Number of returned columns (3) does not match expected column count (2). CONTEXT: SQL statement "select 10, 20, 30" PL/pgSQL function dyn_sql_4() line 3 at RETURN QUERY -- should be error select * from plpgsql_check_function('dyn_sql_4()'); plpgsql_check_function ----------------------------------------------------------------------------------- error:42804:3:RETURN QUERY:structure of query does not match function result type Detail: Number of returned columns (3) does not match expected column count (2). (2 rows) drop function dyn_sql_4(); create or replace function test_bug(text) returns regproc as $$ begin return $1::regproc; exception when undefined_function or invalid_name then raise; end; $$ language plpgsql; -- should not raise a exception select * from plpgsql_check_function('test_bug'); plpgsql_check_function ------------------------ (0 rows) create or replace function test_bug(text) returns regproc as $$ begin return $1::regproc; exception when undefined_function or invalid_name then raise notice '%', $1; -- bug end; $$ language plpgsql; select test_bug('kuku'); -- should to fail NOTICE: kuku ERROR: control reached end of function without RETURN CONTEXT: PL/pgSQL function test_bug(text) select * from plpgsql_check_function('test_bug'); plpgsql_check_function -------------------------------------------------------------------- warning extra:2F005:control reached end of function without RETURN (1 row) drop function test_bug(text); create or replace function test_bug(text) returns regproc as $$ begin return $1::regproc; exception when undefined_function or invalid_name then raise notice '%', $1; return NULL; end; $$ language plpgsql; select test_bug('kuku'); -- should be ok NOTICE: kuku test_bug ---------- (1 row) select * from plpgsql_check_function('test_bug'); plpgsql_check_function ------------------------ (0 rows) drop function test_bug(text); create or replace function foo(a text, b text) returns void as $$ begin -- unsecure execute 'select ' || a; a := quote_literal(a); -- is safe now execute 'select ' || a; a := a || b; -- it is unsecure again execute 'select ' || a; end; $$ language plpgsql; \sf+ foo(text, text) CREATE OR REPLACE FUNCTION public.foo(a text, b text) RETURNS void LANGUAGE plpgsql 1 AS $function$ 2 begin 3 -- unsecure 4 execute 'select ' || a; 5 a := quote_literal(a); -- is safe now 6 execute 'select ' || a; 7 a := a || b; -- it is unsecure again 8 execute 'select ' || a; 9 end; 10 $function$ -- should to raise two warnings select * from plpgsql_check_function('foo', security_warnings := true); plpgsql_check_function ----------------------------------------------------------------------------- security:00000:4:EXECUTE:text type variable is not sanitized Query: 'select ' || a -- ^ Detail: The EXECUTE expression is SQL injection vulnerable. Hint: Use quote_ident, quote_literal or format function to secure variable. security:00000:8:EXECUTE:text type variable is not sanitized Query: 'select ' || a -- ^ Detail: The EXECUTE expression is SQL injection vulnerable. Hint: Use quote_ident, quote_literal or format function to secure variable. (10 rows) drop function foo(text, text); create table testr(a int); create rule testr_rule as on insert to testr do nothing; create or replace function fx_testr() returns void as $$ begin insert into testr values(20); end; $$ language plpgsql; -- allow some rules on tables select fx_testr(); fx_testr ---------- (1 row) select * from plpgsql_check_function_tb('fx_testr'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+---------+--------+------+-------+----------+-------+--------- (0 rows) drop function fx_testr(); drop table testr; create or replace function f() returns void as $$ declare r1 record; r2 record; begin select 10 as a, 20 as b into r1; r2 := json_populate_record(r1, '{}'); raise notice '%', r2.a; end; $$ language plpgsql; select * from plpgsql_check_function('f'); plpgsql_check_function ------------------------ (0 rows) -- fix issue #63 create or replace function distinct_array(arr anyarray) returns anyarray as $$ begin return array(select distinct e from unnest(arr) as e); end; $$ language plpgsql immutable; select plpgsql_check_function('distinct_array(anyarray)'); plpgsql_check_function ------------------------ (0 rows) drop function distinct_array(anyarray); -- tracer test set plpgsql_check.enable_tracer to on; set plpgsql_check.tracer to on; set plpgsql_check.tracer_test_mode = true; -- should not crash do $$ begin raise exception 'raising an exception'; end; $$; NOTICE: #0 ->> start of block inline_code_block (oid=0, tnl=1) NOTICE: #0 <<- end of block (elapsed time=0.010 ms) aborted ERROR: raising an exception CONTEXT: PL/pgSQL function inline_code_block line 3 at RAISE do $$ begin do $_$ begin raise exception 'raising an exception'; end $_$; end; $$; NOTICE: #0 ->> start of block inline_code_block (oid=0, tnl=1) NOTICE: #2 ->> start of block inline_code_block (oid=0, tnl=1) NOTICE: #2 context: SQL statement "do $_$ begin raise exception 'raising an exception'; end $_$" NOTICE: #2 <<- end of block (elapsed time=0.010 ms) aborted NOTICE: #0 <<- end of block (elapsed time=0.010 ms) aborted ERROR: raising an exception CONTEXT: PL/pgSQL function inline_code_block line 3 at RAISE SQL statement "do $_$ begin raise exception 'raising an exception'; end $_$" PL/pgSQL function inline_code_block line 3 at DO create function fx_trace(int) returns int as $$ begin if $1 = 0 then raise exception 'arg is zero'; else return $1; end if; end; $$ language plpgsql; do $$ declare t int; begin t := fx_trace(1); end $$; NOTICE: #0 ->> start of block inline_code_block (oid=0, tnl=1) NOTICE: #1 ->> start of function fx_trace(integer) (oid=0, tnl=1) NOTICE: #1 context: PL/pgSQL function inline_code_block line 4 at assignment NOTICE: #1 "$1" => '1' NOTICE: #1 <<- end of function fx_trace (elapsed time=0.010 ms) NOTICE: #0 <<- end of block (elapsed time=0.010 ms) do $$ declare t int; begin t := fx_trace(0); end $$; NOTICE: #0 ->> start of block inline_code_block (oid=0, tnl=1) NOTICE: #1 ->> start of function fx_trace(integer) (oid=0, tnl=1) NOTICE: #1 context: PL/pgSQL function inline_code_block line 4 at assignment NOTICE: #1 "$1" => '0' NOTICE: #1 <<- end of function fx_trace (elapsed time=0.010 ms) aborted NOTICE: #0 <<- end of block (elapsed time=0.010 ms) aborted ERROR: arg is zero CONTEXT: PL/pgSQL function fx_trace(integer) line 4 at RAISE PL/pgSQL function inline_code_block line 4 at assignment drop function fx_trace; \set VERBOSITY terse create or replace function fxo(a int, b int, c date, d numeric) returns void as $$ begin insert into tracer_tab values(a,b,c,d); end; $$ language plpgsql; create table tracer_tab(a int, b int, c date, d numeric); create or replace function tracer_tab_trg_fx() returns trigger as $$ begin return new; end; $$ language plpgsql; create trigger tracer_tab_trg before insert on tracer_tab for each row execute procedure tracer_tab_trg_fx(); select fxo(10,20,'20200815', 3.14); NOTICE: #0 ->> start of function fxo(integer,integer,date,numeric) (oid=0, tnl=1) NOTICE: #0 "a" => '10', "b" => '20', "c" => '08-15-2020', "d" => '3.14' NOTICE: #2 ->> start of function tracer_tab_trg_fx() (oid=0, tnl=1) NOTICE: #2 context: SQL statement "insert into tracer_tab values(a,b,c,d)" NOTICE: #2 triggered by before row insert trigger NOTICE: #2 "new" => '(10,20,08-15-2020,3.14)' NOTICE: #2 <<- end of function tracer_tab_trg_fx (elapsed time=0.010 ms) NOTICE: #0 <<- end of function fxo (elapsed time=0.010 ms) fxo ----- (1 row) select fxo(11,21,'20200816', 6.28); NOTICE: #0 ->> start of function fxo(integer,integer,date,numeric) (oid=0, tnl=1) NOTICE: #0 "a" => '11', "b" => '21', "c" => '08-16-2020', "d" => '6.28' NOTICE: #2 ->> start of function tracer_tab_trg_fx() (oid=0, tnl=1) NOTICE: #2 context: SQL statement "insert into tracer_tab values(a,b,c,d)" NOTICE: #2 triggered by before row insert trigger NOTICE: #2 "new" => '(11,21,08-16-2020,6.28)' NOTICE: #2 <<- end of function tracer_tab_trg_fx (elapsed time=0.010 ms) NOTICE: #0 <<- end of function fxo (elapsed time=0.010 ms) fxo ----- (1 row) set plpgsql_check.enable_tracer to off; set plpgsql_check.tracer to off; drop table tracer_tab cascade; drop function tracer_tab_trg_fx(); drop function fxo(int, int, date, numeric); create or replace function foo_trg_func() returns trigger as $$ begin -- bad function, RETURN is missing end; $$ language plpgsql; create table foo(a int); create trigger foo_trg before insert for each row execute procedure foo_trg_func(); ERROR: syntax error at or near "for" at character 38 -- should to print error select * from plpgsql_check_function('foo_trg_func', 'foo'); plpgsql_check_function ------------------------------------------------------------ error:2F005:control reached end of function without RETURN (1 row) drop table foo; drop function foo_trg_func(); -- check event trigger function create or replace function f1() returns event_trigger as $$ BEGIN RAISE NOTICE 'test_event_trigger: % %', tg_event, tg_tag; END $$ language plpgsql; select * from plpgsql_check_function_tb('f1()'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+---------+--------+------+-------+----------+-------+--------- (0 rows) -- should fail create or replace function f1() returns event_trigger as $$ BEGIN RAISE NOTICE 'test_event_trigger: % %', tg_event, tg_tagX; END $$ language plpgsql; select * from plpgsql_check_function_tb('f1()'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+---------------------------------+--------+------+-------+----------+---------+--------- f1 | 3 | RAISE | 42703 | column "tg_tagx" does not exist | | | error | 1 | tg_tagX | (1 row) drop function f1(); -- check event trigger function create or replace function f1() returns event_trigger as $$ BEGIN RAISE NOTICE 'test_event_trigger: % %', tg_event, tg_tag; END $$ language plpgsql; select * from plpgsql_check_function('f1()'); plpgsql_check_function ------------------------ (0 rows) -- should fail create or replace function f1() returns event_trigger as $$ BEGIN RAISE NOTICE 'test_event_trigger: % %', tg_event, tg_tagX; END $$ language plpgsql; select * from plpgsql_check_function('f1()'); plpgsql_check_function ----------------------------------------------------- error:42703:3:RAISE:column "tg_tagx" does not exist Query: tg_tagX -- ^ (3 rows) drop function f1(); create table t1tab(a int, b int); create or replace function f1() returns setof t1tab as $$ begin return next (10,20); return; end; $$ language plpgsql; select * from plpgsql_check_function('f1()', performance_warnings => true); plpgsql_check_function ----------------------------------------------------------------------------------------- performance:00000:routine is marked as VOLATILE, should be IMMUTABLE Hint: When you fix this issue, please, recheck other functions that uses this function. (2 rows) create or replace function f1() returns setof t1tab as $$ begin return next (10::numeric,20); return; end; $$ language plpgsql; select * from plpgsql_check_function('f1()', performance_warnings => true); plpgsql_check_function ------------------------------------------------------------------------------------------------ error:42804:3:RETURN NEXT:returned record type does not match expected record type Detail: Returned type numeric does not match expected type integer in column "a" (position 1). (2 rows) create or replace function f1() returns setof t1tab as $$ declare a int; b int; begin return next (a,b); return; end; $$ language plpgsql; select * from plpgsql_check_function('f1()', performance_warnings => true); plpgsql_check_function ----------------------------------------------------------------------------------------- performance:00000:routine is marked as VOLATILE, should be IMMUTABLE Hint: When you fix this issue, please, recheck other functions that uses this function. (2 rows) create or replace function f1() returns setof t1tab as $$ declare a numeric; b int; begin return next (a,b::numeric); return; end; $$ language plpgsql; select * from plpgsql_check_function('f1()', performance_warnings => true); plpgsql_check_function ------------------------------------------------------------------------------------------------ error:42804:4:RETURN NEXT:returned record type does not match expected record type Detail: Returned type numeric does not match expected type integer in column "a" (position 1). (2 rows) drop function f1(); create table t1(a int, b int); create or replace function fx() returns t2 as $$ begin return (10,20,30)::t1; end; $$ language plpgsql; select * from plpgsql_check_function('fx()', performance_warnings => true); plpgsql_check_function ---------------------------------------------------- error:42846:3:RETURN:cannot cast type record to t1 Query: (10,20,30)::t1 -- ^ Detail: Input has too many columns. (4 rows) drop function fx(); drop table t1tab; drop table t1; create or replace function fx() returns void as $$ begin assert exists(select * from foo); assert false, (select boo from boo limit 1); end; $$ language plpgsql; select * from plpgsql_check_function('fx()', fatal_errors => false); plpgsql_check_function ---------------------------------------------------- error:42P01:3:ASSERT:relation "foo" does not exist Query: exists(select * from foo) -- ^ error:42P01:4:ASSERT:relation "boo" does not exist Query: (select boo from boo limit 1) -- ^ (6 rows) create or replace function ml_trg() returns trigger as $$ #option dump declare begin if TG_OP = 'INSERT' then if NEW.status_from IS NULL then begin -- performance issue only select status into NEW.status_from from pa where pa_id = NEW.pa_id; -- nonexist target value select status into NEW.status_from_xxx from pa where pa_id = NEW.pa_id; exception when DATA_EXCEPTION then new.status_from := 'DE'; end; end if; end if; if TG_OP = 'DELETE' then return OLD; else return NEW; end if; exception when OTHERS then NULL; if TG_OP = 'DELETE' then return OLD; else return NEW; end if; end; $$ language plpgsql; select * from plpgsql_check_function('ml_trg()', 'ml', performance_warnings := true); plpgsql_check_function -------------------------------------------------------------------------- error:42703:13:SQL statement:record "new" has no field "status_from_xxx" (1 row) create or replace function fx2() returns void as $$ declare _pa pa; begin select pa.id into _pa.id from pa limit 1; select pa.pa_id into _pa.pa_id from pa limit 1; end; $$ language plpgsql; select * from plpgsql_check_function('fx2()', performance_warnings := true); plpgsql_check_function --------------------------------------------------------- warning extra:00000:2:DECLARE:never read variable "_pa" (1 row) drop function fx2(); create or replace function fx2() returns void as $$ declare _pa pa; begin _pa.id := (select pa.id from pa limit 1); _pa.pa_id := (select pa.pa_id from pa limit 1); end; $$ language plpgsql; select * from plpgsql_check_function('fx2()', performance_warnings := true); plpgsql_check_function --------------------------------------------------------- warning extra:00000:2:DECLARE:never read variable "_pa" (1 row) drop function fx2(); create type _exception_type as ( state text, message text, detail text); create or replace function f1() returns void as $$ declare _exception record; begin _exception := NULL::_exception_type; exception when others then get stacked diagnostics _exception.state = RETURNED_SQLSTATE, _exception.message = MESSAGE_TEXT, _exception.detail = PG_EXCEPTION_DETAIL, _exception.hint = PG_EXCEPTION_HINT; end; $$ language plpgsql; select f1(); f1 ---- (1 row) select * from plpgsql_check_function_tb('f1()'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-------------------------+----------+-----------------------------------------+--------+------+-------+----------+-------+--------- f1 | 7 | GET STACKED DIAGNOSTICS | 42703 | record "_exception" has no field "hint" | | | error | | | (1 row) create or replace function f1() returns void as $$ declare _exception _exception_type; begin _exception := NULL::_exception_type; exception when others then get stacked diagnostics _exception.state = RETURNED_SQLSTATE, _exception.message = MESSAGE_TEXT, _exception.detail = PG_EXCEPTION_DETAIL; end; $$ language plpgsql; select f1(); f1 ---- (1 row) select * from plpgsql_check_function_tb('f1()'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+----------------------------------+--------+------+---------------+----------+-------+--------- f1 | 3 | DECLARE | 00000 | never read variable "_exception" | | | warning extra | | | (1 row) drop function f1(); drop type _exception_type; create type _exception_type as ( state text, message text, detail text); create or replace function f1() returns void as $$ declare _exception record; begin _exception := NULL::_exception_type; exception when others then get stacked diagnostics _exception.state = RETURNED_SQLSTATE, _exception.message = MESSAGE_TEXT, _exception.detail = PG_EXCEPTION_DETAIL, _exception.hint = PG_EXCEPTION_HINT; end; $$ language plpgsql; select f1(); f1 ---- (1 row) select * from plpgsql_check_function('f1()'); plpgsql_check_function ------------------------------------------------------------------------------- error:42703:7:GET STACKED DIAGNOSTICS:record "_exception" has no field "hint" (1 row) drop function f1(); drop type _exception_type; create table footab(a int, b int, c int); create or replace function footab_trig_func() returns trigger as $$ declare x int; begin if false then -- should be ok; select count(*) from newtab into x; -- should fail; select count(*) from newtab where d = 10 into x; end if; return null; end; $$ language plpgsql; select * from plpgsql_check_function('footab_trig_func','footab', newtable := 'newtab'); plpgsql_check_function ------------------------------------------------------- error:42703:9:SQL statement:column "d" does not exist Query: select count(*) from newtab where d = 10 -- ^ (3 rows) drop table footab; drop function footab_trig_func(); create or replace function df1(anyelement) returns anyelement as $$ begin return $1; end; $$ language plpgsql; create or replace function df2(anyelement, jsonb) returns anyelement as $$ begin return $1; end; $$ language plpgsql; create or replace function t1() returns void as $$ declare r record; begin r := df1(r); end; $$ language plpgsql; select * from plpgsql_check_function('t1()'); plpgsql_check_function ------------------------ (0 rows) create or replace function t1() returns void as $$ declare r record; begin r := df2(r, '{}'); end; $$ language plpgsql; select * from plpgsql_check_function('t1()'); plpgsql_check_function ------------------------ (0 rows) create or replace function t1() returns void as $$ declare r1 record; r2 record; begin select 10 as a, 20 as b into r1; r2 := df1(r1); raise notice '%', r2.a; end; $$ language plpgsql; select * from plpgsql_check_function('t1()'); plpgsql_check_function ------------------------ (0 rows) create or replace function t1() returns void as $$ declare r1 record; r2 record; begin select 10 as a, 20 as b into r1; r2 := df2(r1, '{}'); raise notice '%', r2.a; end; $$ language plpgsql; select * from plpgsql_check_function('t1()'); plpgsql_check_function ------------------------ (0 rows) create or replace function df1(anyelement) returns anyelement as $$ select $1 $$ language sql; create or replace function df22(jsonb, anyelement) returns anyelement as $$ select $2; $$ language sql; create or replace function t1() returns void as $$ declare r1 record; r2 record; begin select 10 as a, 20 as b into r1; r2 := df1(r1); raise notice '%', r2.a; end; $$ language plpgsql; select * from plpgsql_check_function('t1()'); plpgsql_check_function ------------------------ (0 rows) create or replace function t1() returns void as $$ declare r1 record; r2 record; begin select 10 as a, 20 as b into r1; r2 := df22('{}', r1); raise notice '%', r2.a; end; $$ language plpgsql; select * from plpgsql_check_function('t1()'); plpgsql_check_function ------------------------ (0 rows) drop function df1(anyelement); drop function df2(anyelement, jsonb); drop function df22(jsonb, anyelement); drop function t1(); create or replace function dyntest() returns void as $$ begin execute 'drop table if exists xxx; create table xxx(a int)'; end; $$ language plpgsql; -- should be ok select * from plpgsql_check_function('dyntest'); plpgsql_check_function ------------------------ (0 rows) create or replace function dyntest() returns void as $$ declare x int; begin execute 'drop table if exists xxx; create table xxx(a int)' into x; end; $$ language plpgsql; -- should to report error select * from plpgsql_check_function('dyntest'); plpgsql_check_function ------------------------------------------------------- error:XX000:4:EXECUTE:expression does not return data (1 row) drop function dyntest(); -- should to report error create type typ2 as (a int, b int); create or replace function broken_into() returns void as $$ declare v typ2; begin -- should to fail select (10,20)::typ2 into v; -- should be ok select ((10,20)::typ2).* into v; -- should to fail execute 'select (10,20)::typ2' into v; -- should be ok execute 'select ((10,20)::typ2).*' into v; end; $$ language plpgsql; select * from plpgsql_check_function('broken_into', fatal_errors => false); plpgsql_check_function ------------------------------------------------------------------------------------------------------------ error:42804:5:SQL statement:cannot cast composite value of "typ2" type to a scalar value of "integer" type Query: select (10,20)::typ2 -- ^ Context: at SQL statement to variable "v" declared on line 2 warning:00000:5:SQL statement:too few attributes for composite variable Query: select (10,20)::typ2 -- ^ Context: at SQL statement to variable "v" declared on line 2 error:42804:9:EXECUTE:cannot cast composite value of "typ2" type to a scalar value of "integer" type Context: at EXECUTE to variable "v" declared on line 2 warning:00000:9:EXECUTE:too few attributes for composite variable Context: at EXECUTE to variable "v" declared on line 2 warning extra:00000:2:DECLARE:never read variable "v" (13 rows) drop function broken_into(); drop type typ2; -- check output in xml or json formats CREATE OR REPLACE FUNCTION test_function() RETURNS void LANGUAGE plpgsql AS $function$ begin insert into non_existing_table values (1); end $function$; select * from plpgsql_check_function('test_function', format => 'xml'); plpgsql_check_function ---------------------------------------------------------------------------- + + error + 42P01 + relation "non_existing_table" does not exist + SQL statement + insert into non_existing_table values (1)+ + (1 row) select * from plpgsql_check_function('test_function', format => 'json'); plpgsql_check_function ------------------------------------------------------------------- { + "issues": [ + { + "level": "error", + "message":"relation \"non_existing_table\" does not exist",+ "statement": { + "lineNumber": "3", + "text": "SQL statement" + }, + "query": { + "position": "13", + "text": "insert into non_existing_table values (1)" + }, + "sqlState": "42P01" + } + ] + } (1 row) drop function test_function(); -- test settype pragma create or replace function test_function() returns void as $$ declare r record; begin raise notice '%', r.a; end; $$ language plpgsql; -- should to detect error select * from plpgsql_check_function('test_function'); plpgsql_check_function ---------------------------------------------------------------------------- error:55000:4:RAISE:record "r" is not assigned yet Detail: The tuple structure of a not-yet-assigned record is indeterminate. Context: PL/pgSQL expression "r.a" (3 rows) create type ctype as (a int, b int); create or replace function test_function() returns void as $$ declare r record; begin perform plpgsql_check_pragma('type: r ctype'); raise notice '%', r.a; end; $$ language plpgsql; -- should to be ok select * from plpgsql_check_function('test_function'); plpgsql_check_function ------------------------ (0 rows) create or replace function test_function() returns void as $$ <>declare r record; begin perform plpgsql_check_pragma('type: x.r public."ctype"'); raise notice '%', r.a; end; $$ language plpgsql; -- should to be ok select * from plpgsql_check_function('test_function'); plpgsql_check_function ------------------------ (0 rows) create or replace function test_function() returns void as $$ <>declare r record; begin perform plpgsql_check_pragma('type: "x".r (a int, b int)'); raise notice '%', r.a; end; $$ language plpgsql; -- should to be ok select * from plpgsql_check_function('test_function'); plpgsql_check_function ------------------------ (0 rows) create or replace function test_function() returns void as $$ <>declare r record; begin perform plpgsql_check_pragma('type: "x".r (a int, b int'); raise notice '%', r.a; end; $$ language plpgsql; -- should to be ok select * from plpgsql_check_function('test_function'); WARNING: Pragma "type" on line 4 is not processed. plpgsql_check_function ---------------------------------------------------------------------------- error:55000:5:RAISE:record "r" is not assigned yet Detail: The tuple structure of a not-yet-assigned record is indeterminate. Context: PL/pgSQL expression "r.a" (3 rows) create or replace function test_function() returns void as $$ <>declare r record; begin perform plpgsql_check_pragma('type: "x".r (a int, b int)x'); raise notice '%', r.a; end; $$ language plpgsql; -- should to be ok select * from plpgsql_check_function('test_function'); WARNING: Pragma "type" on line 4 is not processed. plpgsql_check_function ---------------------------------------------------------------------------- error:55000:5:RAISE:record "r" is not assigned yet Detail: The tuple structure of a not-yet-assigned record is indeterminate. Context: PL/pgSQL expression "r.a" (3 rows) drop function test_function(); drop type ctype; create or replace function test_function() returns void as $$ declare r pg_class; begin create temp table foo(like pg_class); select * from foo into r; end; $$ language plpgsql; -- should to raise an error select * from plpgsql_check_function('test_function'); plpgsql_check_function ----------------------------------------------------------- error:42P01:5:SQL statement:relation "foo" does not exist Query: select * from foo -- ^ (3 rows) create or replace function test_function() returns void as $$ declare r record; begin create temp table foo(like pg_class); perform plpgsql_check_pragma('table: foo(like pg_class)'); select * from foo into r; raise notice '%', r.relname; end; $$ language plpgsql; -- should be ok select * from plpgsql_check_function('test_function'); plpgsql_check_function ------------------------ (0 rows) drop function test_function(); -- now plpgsql_check can do some other checks when statement EXECUTE -- contains only format function with constant fmt. create or replace function test_function() returns void as $$ begin execute format('create table zzz %I(a int, b int)', 'zzz'); end; $$ language plpgsql; -- should to detect bad expression select * from plpgsql_check_function('test_function'); plpgsql_check_function ----------------------------------------------------- error:42601:3:EXECUTE:syntax error at or near "zzz" Query: create table zzz zzz(a int, b int) (2 rows) -- should to correctly detect type create or replace function test_function() returns void as $$ declare r record; begin execute format('select %L::date + 1 as x', current_date) into r; raise notice '%', extract(dow from r.x); end; $$ language plpgsql; -- should be ok select * from plpgsql_check_function('test_function'); plpgsql_check_function ------------------------ (0 rows) -- should not to crash create or replace function test_function() returns void as $$ declare r record; begin r := null; end; $$ language plpgsql; select * from plpgsql_check_function('test_function'); plpgsql_check_function ------------------------------------------------------- warning extra:00000:2:DECLARE:never read variable "r" (1 row) drop function test_function(); -- ignores syntax errors when literals placehodlers are used create function test_function() returns void as $$ begin execute format('do %L', 'begin end'); end $$ language plpgsql; select * from plpgsql_check_function('test_function'); plpgsql_check_function ------------------------ (0 rows) drop function test_function(); load 'plpgsql_check'; drop type testtype cascade; create type testtype as (a int, b int); create function test_function() returns record as $$ declare r record; begin r := (10,20); if false then return r; end if; return null; end; $$ language plpgsql; create function test_function33() returns record as $$ declare r testtype; begin r := (10,20); if false then return r; end if; return null; end; $$ language plpgsql; -- should not to raise false alarm due check against fake result type select plpgsql_check_function('test_function'); plpgsql_check_function ------------------------ (0 rows) select plpgsql_check_function('test_function33'); plpgsql_check_function ------------------------ (0 rows) -- try to check in passive mode set plpgsql_check.mode = 'every_start'; select test_function(); test_function --------------- (1 row) select test_function33(); test_function33 ----------------- (1 row) select * from test_function() as (a int, b int); a | b ---+--- | (1 row) select * from test_function33() as (a int, b int); a | b ---+--- | (1 row) -- should to identify error select * from test_function() as (a int, b int, c int); ERROR: returned record type does not match expected record type select * from test_function33() as (a int, b int, c int); ERROR: returned record type does not match expected record type drop function test_function(); drop function test_function33(); drop type testtype; set plpgsql_check.mode to default; -- should not to raise false alarm create type c1 as ( a text ); create table t1 ( a c1, b c1 ); insert into t1 (values ('(abc)', '(def)')); alter table t1 drop column a; create or replace function test_function() returns t1 as $$ declare myrow t1%rowtype; begin select * into myrow from t1 limit 1; return myrow; end; $$ language plpgsql; select * from test_function(); b ------- (def) (1 row) select * from plpgsql_check_function('public.test_function()'); plpgsql_check_function ------------------------ (0 rows) drop function test_function(); drop table t1; drop type c1; -- compatibility warnings create or replace function foo01() returns refcursor as $$ declare c cursor for select 1; r refcursor; begin open c; r := c; return r; end; $$ language plpgsql; -- no warnings select * from plpgsql_check_function('foo01', compatibility_warnings => true); plpgsql_check_function ------------------------ (0 rows) create or replace function foo01() returns refcursor as $$ declare c cursor for select 1; r refcursor; begin open c; r := 'c'; return r; end; $$ language plpgsql; -- warning select * from plpgsql_check_function('foo01', extra_warnings => false, compatibility_warnings => true); plpgsql_check_function ----------------------------------------------------------------------------------- compatibility:00000:7:assignment:obsolete setting of refcursor or cursor variable Detail: Internal name of cursor should not be specified by users. Context: at assignment to variable "r" declared on line 4 (3 rows) create or replace function foo01() returns refcursor as $$ declare c cursor for select 1; r refcursor; begin open c; r := c; return 'c'; end; $$ language plpgsql; -- warning select * from plpgsql_check_function('foo01', extra_warnings => false, compatibility_warnings => true); plpgsql_check_function -------------------------------------------------------------------------------------- compatibility:00000:8:RETURN:obsolete setting of refcursor or cursor variable Detail: Internal name of cursor should not be specified by users. warning:42804:8:RETURN:target type is different type than source type Detail: cast "text" value to "refcursor" type Hint: The input expression type does not have an assignment cast to the target type. Context: at RETURN (6 rows) -- pragma sequence test create or replace function test_function() returns void as $$ begin perform plpgsql_check_pragma('sequence: xx'); perform nextval('pg_temp.xy'); perform nextval('pg_temp.xx'); end $$ language plpgsql; select * from plpgsql_check_function('test_function'); plpgsql_check_function ------------------------------------------------------------ error:42P01:4:PERFORM:relation "pg_temp.xy" does not exist Query: SELECT nextval('pg_temp.xy') -- ^ (3 rows) drop function test_function(); create table t1(x int); create or replace function f1_trg() returns trigger as $$ declare x int; begin return x; end; $$ language plpgsql; create trigger t1_f1 before insert on t1 for each row execute procedure f1_trg(); -- raise error select * from plpgsql_check_function('f1_trg', 't1'); plpgsql_check_function ----------------------------------------------------------------------------------------------- error:42804:4:RETURN:cannot return non-composite value from function returning composite type (1 row) drop trigger t1_f1 on t1; drop table t1; drop function f1_trg; create function test_function() returns void as $$ declare a int; b int; c int; d char; begin c := a + d; end; $$ language plpgsql; -- only b should be marked as unused variable select * from plpgsql_check_function('test_function', fatal_errors := false); plpgsql_check_function ----------------------------------------------------------------------- error:42883:6:assignment:operator does not exist: integer + character Query: c := a + d -- ^ Detail: No operator of that name accepts the given argument types. Hint: You might need to add explicit type casts. Context: at assignment to variable "c" declared on line 4 warning:00000:3:DECLARE:unused variable "b" warning extra:00000:4:DECLARE:never read variable "c" (8 rows) drop function test_function(); -- from plpgsql_check_active-12 create or replace procedure proc(a int) as $$ begin end; $$ language plpgsql; call proc(10); select * from plpgsql_check_function('proc(int)'); plpgsql_check_function ------------------------------------------ warning extra:00000:unused parameter "a" (1 row) create or replace procedure testproc() as $$ begin call proc(10); end; $$ language plpgsql; call testproc(); select * from plpgsql_check_function('testproc()'); plpgsql_check_function ------------------------ (0 rows) -- should to fail create or replace procedure testproc() as $$ begin call proc((select count(*) from pg_class)); end; $$ language plpgsql; call testproc(); ERROR: cannot use subquery in CALL argument at character 11 select * from plpgsql_check_function('testproc()'); plpgsql_check_function --------------------------------------------------------- error:0A000:3:CALL:cannot use subquery in CALL argument Query: call proc((select count(*) from pg_class)) -- ^ (3 rows) drop procedure proc(int); create procedure proc(in a int, inout b int, in c int) as $$ begin end; $$ language plpgsql; select * from plpgsql_check_function('proc(int,int, int)'); plpgsql_check_function ------------------------------------------------- warning extra:00000:unused parameter "a" warning extra:00000:unused parameter "b" warning extra:00000:unused parameter "c" warning extra:00000:unmodified OUT variable "b" (4 rows) create or replace procedure proc(in a int, inout b int, in c int) as $$ begin b := a + c; end; $$ language plpgsql; select * from plpgsql_check_function('proc(int,int, int)'); plpgsql_check_function ------------------------ (0 rows) create or replace procedure testproc() as $$ declare r int; begin call proc(10, r, 20); end; $$ language plpgsql; call testproc(); select * from plpgsql_check_function('testproc()'); plpgsql_check_function ------------------------ (0 rows) -- should to fail create or replace procedure testproc() as $$ declare r int; begin call proc(10, r + 10, 20); end; $$ language plpgsql; call testproc(); ERROR: procedure parameter "b" is an output parameter but corresponding argument is not writable select * from plpgsql_check_function('testproc()'); plpgsql_check_function -------------------------------------------------------------------------------------------------------------- error:42601:4:CALL:procedure parameter "b" is an output parameter but corresponding argument is not writable (1 row) create or replace procedure testproc(inout r int) as $$ begin call proc(10, r, 20); end; $$ language plpgsql; call testproc(10); r ---- 30 (1 row) select * from plpgsql_check_function('testproc(int)'); plpgsql_check_function ------------------------ (0 rows) drop procedure testproc(int); -- should to raise warnings create or replace procedure testproc2(in p1 int, inout p2 int, in p3 int, inout p4 int) as $$ begin raise notice '% %', p1, p3; end; $$ language plpgsql; select * from plpgsql_check_function('testproc2'); plpgsql_check_function -------------------------------------------------- warning extra:00000:unused parameter "p2" warning extra:00000:unused parameter "p4" warning extra:00000:unmodified OUT variable "p2" warning extra:00000:unmodified OUT variable "p4" (4 rows) drop procedure testproc2; -- should be ok create or replace procedure testproc3(in p1 int, inout p2 int, in p3 int, inout p4 int) as $$ begin p2 := p1; p4 := p3; end; $$ language plpgsql; select * from plpgsql_check_function('testproc3'); plpgsql_check_function ------------------------ (0 rows) drop procedure testproc3; /* * Test pragma */ create or replace function test_pragma() returns void language plpgsql as $$ declare r record; begin perform plpgsql_check_pragma('disable:check'); raise notice '%', r.y; perform plpgsql_check_pragma('enable:check'); select 10 as a, 20 as b into r; raise notice '%', r.a; raise notice '%', r.x; end; $$; select * from plpgsql_check_function('test_pragma'); plpgsql_check_function ------------------------------------------------- error:42703:9:RAISE:record "r" has no field "x" Context: PL/pgSQL expression "r.x" (2 rows) create or replace function test_pragma() returns void language plpgsql as $$ declare r record; begin if false then -- check is disabled just for if body perform plpgsql_check_pragma('disable:check'); raise notice '%', r.y; end if; select 10 as a, 20 as b into r; raise notice '%', r.a; raise notice '%', r.x; end; $$; select * from plpgsql_check_function('test_pragma'); plpgsql_check_function -------------------------------------------------- error:42703:11:RAISE:record "r" has no field "x" Context: PL/pgSQL expression "r.x" (2 rows) drop function test_pragma(); create or replace function nested_trace_test(a int) returns int as $$ begin return a + 1; end; $$ language plpgsql; create or replace function trace_test(b int) returns int as $$ declare r int default 0; begin for i in 1..b loop r := nested_trace_test(r); end loop; return r; end; $$ language plpgsql; select trace_test(3); trace_test ------------ 3 (1 row) set plpgsql_check.enable_tracer to on; set plpgsql_check.tracer to on; set plpgsql_check.tracer_test_mode = true; select trace_test(3); NOTICE: #0 ->> start of function trace_test(integer) (oid=0, tnl=1) NOTICE: #0 "b" => '3' NOTICE: #1 ->> start of function nested_trace_test(integer) (oid=0, tnl=1) NOTICE: #1 context: PL/pgSQL function trace_test(integer) line 6 at assignment NOTICE: #1 "a" => '0' NOTICE: #1 <<- end of function nested_trace_test (elapsed time=0.010 ms) NOTICE: #1 ->> start of function nested_trace_test(integer) (oid=0, tnl=1) NOTICE: #1 context: PL/pgSQL function trace_test(integer) line 6 at assignment NOTICE: #1 "a" => '1' NOTICE: #1 <<- end of function nested_trace_test (elapsed time=0.010 ms) NOTICE: #1 ->> start of function nested_trace_test(integer) (oid=0, tnl=1) NOTICE: #1 context: PL/pgSQL function trace_test(integer) line 6 at assignment NOTICE: #1 "a" => '2' NOTICE: #1 <<- end of function nested_trace_test (elapsed time=0.010 ms) NOTICE: #0 <<- end of function trace_test (elapsed time=0.010 ms) trace_test ------------ 3 (1 row) set plpgsql_check.tracer_verbosity TO verbose; select trace_test(3); NOTICE: #0 ->> start of function trace_test(integer) (oid=0, tnl=1) NOTICE: #0 "b" => '3' NOTICE: #0.1 3 --> start of statement block (tnl=1) NOTICE: #0.2 4 --> start of FOR with integer loop variable (tnl=1) NOTICE: #0.3 6 --> start of assignment r := nested_trace_test(r) (tnl=1) NOTICE: #0.3 "r" => '0' NOTICE: #1 ->> start of function nested_trace_test(integer) (oid=0, tnl=1) NOTICE: #1 context: PL/pgSQL function trace_test(integer) line 6 at assignment NOTICE: #1 "a" => '0' NOTICE: #1.1 2 --> start of statement block (tnl=1) NOTICE: #1.2 3 --> start of RETURN (expr='a + 1') (tnl=1) NOTICE: #1.2 "a" => '0' NOTICE: #1.2 <-- end of RETURN (elapsed time=0.010 ms) NOTICE: #1.1 <-- end of statement block (elapsed time=0.010 ms) NOTICE: #1 <<- end of function nested_trace_test (elapsed time=0.010 ms) NOTICE: #0.3 <-- end of assignment (elapsed time=0.010 ms) NOTICE: #0.3 "r" => '1' NOTICE: #0.3 6 --> start of assignment r := nested_trace_test(r) (tnl=1) NOTICE: #0.3 "r" => '1' NOTICE: #1 ->> start of function nested_trace_test(integer) (oid=0, tnl=1) NOTICE: #1 context: PL/pgSQL function trace_test(integer) line 6 at assignment NOTICE: #1 "a" => '1' NOTICE: #1.1 2 --> start of statement block (tnl=1) NOTICE: #1.2 3 --> start of RETURN (expr='a + 1') (tnl=1) NOTICE: #1.2 "a" => '1' NOTICE: #1.2 <-- end of RETURN (elapsed time=0.010 ms) NOTICE: #1.1 <-- end of statement block (elapsed time=0.010 ms) NOTICE: #1 <<- end of function nested_trace_test (elapsed time=0.010 ms) NOTICE: #0.3 <-- end of assignment (elapsed time=0.010 ms) NOTICE: #0.3 "r" => '2' NOTICE: #0.3 6 --> start of assignment r := nested_trace_test(r) (tnl=1) NOTICE: #0.3 "r" => '2' NOTICE: #1 ->> start of function nested_trace_test(integer) (oid=0, tnl=1) NOTICE: #1 context: PL/pgSQL function trace_test(integer) line 6 at assignment NOTICE: #1 "a" => '2' NOTICE: #1.1 2 --> start of statement block (tnl=1) NOTICE: #1.2 3 --> start of RETURN (expr='a + 1') (tnl=1) NOTICE: #1.2 "a" => '2' NOTICE: #1.2 <-- end of RETURN (elapsed time=0.010 ms) NOTICE: #1.1 <-- end of statement block (elapsed time=0.010 ms) NOTICE: #1 <<- end of function nested_trace_test (elapsed time=0.010 ms) NOTICE: #0.3 <-- end of assignment (elapsed time=0.010 ms) NOTICE: #0.3 "r" => '3' NOTICE: #0.2 <-- end of FOR with integer loop variable (elapsed time=0.010 ms) NOTICE: #0.4 8 --> start of RETURN (tnl=1) NOTICE: #0.4 "r" => '3' NOTICE: #0.4 <-- end of RETURN (elapsed time=0.010 ms) NOTICE: #0.1 <-- end of statement block (elapsed time=0.010 ms) NOTICE: #0 <<- end of function trace_test (elapsed time=0.010 ms) trace_test ------------ 3 (1 row) create or replace function trace_test(b int) returns int as $$ declare r int default 0; begin for i in 1..b loop perform plpgsql_check_pragma('disable:tracer'); r := nested_trace_test(r); end loop; return r; end; $$ language plpgsql; select trace_test(3); NOTICE: #0 ->> start of function trace_test(integer) (oid=0, tnl=1) NOTICE: #0 "b" => '3' NOTICE: #0.1 3 --> start of statement block (tnl=1) NOTICE: #0.2 4 --> start of FOR with integer loop variable (tnl=1) NOTICE: #0.3 6 --> start of perform plpgsql_check_pragma('disable: .. (tnl=1) NOTICE: #0.3 <-- end of PERFORM (elapsed time=0.010 ms) NOTICE: #0.2 <-- end of FOR with integer loop variable (elapsed time=0.010 ms) NOTICE: #0.5 9 --> start of RETURN (tnl=1) NOTICE: #0.5 "r" => '3' NOTICE: #0.5 <-- end of RETURN (elapsed time=0.010 ms) NOTICE: #0.1 <-- end of statement block (elapsed time=0.010 ms) NOTICE: #0 <<- end of function trace_test (elapsed time=0.010 ms) trace_test ------------ 3 (1 row) create or replace function nested_trace_test(a int) returns int as $$ begin perform plpgsql_check_pragma('enable:tracer'); return a + 1; end; $$ language plpgsql; select trace_test(3); NOTICE: #0 ->> start of function trace_test(integer) (oid=0, tnl=1) NOTICE: #0 "b" => '3' NOTICE: #0.1 3 --> start of statement block (tnl=1) NOTICE: #0.2 4 --> start of FOR with integer loop variable (tnl=1) NOTICE: #0.3 6 --> start of perform plpgsql_check_pragma('disable: .. (tnl=1) NOTICE: #0.3 <-- end of PERFORM (elapsed time=0.010 ms) NOTICE: #1.3 4 --> start of RETURN (expr='a + 1') (tnl=1) NOTICE: #1.3 "a" => '0' NOTICE: #1.3 <-- end of RETURN (elapsed time=0.010 ms) NOTICE: #1.3 4 --> start of RETURN (expr='a + 1') (tnl=1) NOTICE: #1.3 "a" => '1' NOTICE: #1.3 <-- end of RETURN (elapsed time=0.010 ms) NOTICE: #1.3 4 --> start of RETURN (expr='a + 1') (tnl=1) NOTICE: #1.3 "a" => '2' NOTICE: #1.3 <-- end of RETURN (elapsed time=0.010 ms) NOTICE: #0.2 <-- end of FOR with integer loop variable (elapsed time=0.010 ms) NOTICE: #0.5 9 --> start of RETURN (tnl=1) NOTICE: #0.5 "r" => '3' NOTICE: #0.5 <-- end of RETURN (elapsed time=0.010 ms) NOTICE: #0.1 <-- end of statement block (elapsed time=0.010 ms) NOTICE: #0 <<- end of function trace_test (elapsed time=0.010 ms) trace_test ------------ 3 (1 row) drop function trace_test(int); drop function nested_trace_test(int); create or replace function trace_test(int) returns int as $$ declare r int default 0; begin for i in 1..$1 loop r := r + 1; end loop; r := r + 10; return r; end; $$ language plpgsql; select trace_test(4); NOTICE: #0 ->> start of function trace_test(integer) (oid=0, tnl=1) NOTICE: #0 "$1" => '4' NOTICE: #0.1 3 --> start of statement block (tnl=1) NOTICE: #0.2 4 --> start of FOR with integer loop variable (tnl=1) NOTICE: #0.3 5 --> start of assignment r := r + 1 (tnl=1) NOTICE: #0.3 "r" => '0' NOTICE: #0.3 <-- end of assignment (elapsed time=0.010 ms) NOTICE: #0.3 "r" => '1' NOTICE: #0.3 5 --> start of assignment r := r + 1 (tnl=1) NOTICE: #0.3 "r" => '1' NOTICE: #0.3 <-- end of assignment (elapsed time=0.010 ms) NOTICE: #0.3 "r" => '2' NOTICE: #0.3 5 --> start of assignment r := r + 1 (tnl=1) NOTICE: #0.3 "r" => '2' NOTICE: #0.3 <-- end of assignment (elapsed time=0.010 ms) NOTICE: #0.3 "r" => '3' NOTICE: #0.3 5 --> start of assignment r := r + 1 (tnl=1) NOTICE: #0.3 "r" => '3' NOTICE: #0.3 <-- end of assignment (elapsed time=0.010 ms) NOTICE: #0.3 "r" => '4' NOTICE: #0.2 <-- end of FOR with integer loop variable (elapsed time=0.010 ms) NOTICE: #0.4 7 --> start of assignment r := r + 10 (tnl=1) NOTICE: #0.4 "r" => '4' NOTICE: #0.4 <-- end of assignment (elapsed time=0.010 ms) NOTICE: #0.4 "r" => '14' NOTICE: #0.5 8 --> start of RETURN (tnl=1) NOTICE: #0.5 "r" => '14' NOTICE: #0.5 <-- end of RETURN (elapsed time=0.010 ms) NOTICE: #0.1 <-- end of statement block (elapsed time=0.010 ms) NOTICE: #0 <<- end of function trace_test (elapsed time=0.010 ms) trace_test ------------ 14 (1 row) create or replace function trace_test(int) returns int as $$ declare r int default 0; begin for i in 1..$1 loop perform plpgsql_check_pragma('disable:tracer'); r := r + 1; end loop; r := r + 10; return r; end; $$ language plpgsql; select trace_test(4); NOTICE: #0 ->> start of function trace_test(integer) (oid=0, tnl=1) NOTICE: #0 "$1" => '4' NOTICE: #0.1 3 --> start of statement block (tnl=1) NOTICE: #0.2 4 --> start of FOR with integer loop variable (tnl=1) NOTICE: #0.3 5 --> start of perform plpgsql_check_pragma('disable: .. (tnl=1) NOTICE: #0.3 <-- end of PERFORM (elapsed time=0.010 ms) NOTICE: #0.2 <-- end of FOR with integer loop variable (elapsed time=0.010 ms) NOTICE: #0.5 8 --> start of assignment r := r + 10 (tnl=1) NOTICE: #0.5 "r" => '4' NOTICE: #0.5 <-- end of assignment (elapsed time=0.010 ms) NOTICE: #0.5 "r" => '14' NOTICE: #0.6 9 --> start of RETURN (tnl=1) NOTICE: #0.6 "r" => '14' NOTICE: #0.6 <-- end of RETURN (elapsed time=0.010 ms) NOTICE: #0.1 <-- end of statement block (elapsed time=0.010 ms) NOTICE: #0 <<- end of function trace_test (elapsed time=0.010 ms) trace_test ------------ 14 (1 row) create or replace function trace_test(int) returns int as $$ declare r int default 0; begin perform plpgsql_check_pragma('disable:tracer'); for i in 1..$1 loop r := r + 1; end loop; perform plpgsql_check_pragma('enable:tracer'); r := r + 10; return r; end; $$ language plpgsql; select trace_test(4); NOTICE: #0 ->> start of function trace_test(integer) (oid=0, tnl=1) NOTICE: #0 "$1" => '4' NOTICE: #0.1 3 --> start of statement block (tnl=1) NOTICE: #0.2 4 --> start of perform plpgsql_check_pragma('disable: .. (tnl=1) NOTICE: #0.2 <-- end of PERFORM (elapsed time=0.010 ms) NOTICE: #0.6 12 --> start of assignment r := r + 10 (tnl=1) NOTICE: #0.6 "r" => '4' NOTICE: #0.6 <-- end of assignment (elapsed time=0.010 ms) NOTICE: #0.6 "r" => '14' NOTICE: #0.7 13 --> start of RETURN (tnl=1) NOTICE: #0.7 "r" => '14' NOTICE: #0.7 <-- end of RETURN (elapsed time=0.010 ms) NOTICE: #0.1 <-- end of statement block (elapsed time=0.010 ms) NOTICE: #0 <<- end of function trace_test (elapsed time=0.010 ms) trace_test ------------ 14 (1 row) drop function trace_test(int); create or replace function public.fx1() returns table(i integer, j integer, found boolean) as $$ begin for i in 1..10 loop for j in 1..10 loop return next; end loop; end loop; end; $$ language plpgsql immutable; select * from plpgsql_check_function('fx1'); plpgsql_check_function -------------------------------------------------------------------------- warning:00000:2:statement block:parameter "found" is shadowed Detail: Local auto variable shadows function parameter. warning:00000:3:FOR with integer loop variable:parameter "i" is shadowed Detail: Local auto variable shadows function parameter. warning:00000:5:FOR with integer loop variable:parameter "j" is shadowed Detail: Local auto variable shadows function parameter. warning extra:00000:unmodified OUT variable "i" warning extra:00000:unmodified OUT variable "j" warning extra:00000:unmodified OUT variable "found" (9 rows) drop function fx1; -- tracer test set plpgsql_check.enable_tracer to on; select plpgsql_check_tracer(true); NOTICE: tracer is active NOTICE: tracer verbosity is verbose plpgsql_check_tracer ---------------------- t (1 row) create role regress_plpgsql_check_test_role; DO $$ begin begin -- should to fail create role regress_plpgsql_check_test_role; exception when duplicate_object then -- Role already exists -- the exception handler is empty (#156) end; end; $$; NOTICE: #0 ->> start of block inline_code_block (oid=0, tnl=1) NOTICE: #0.1 2 --> start of statement block (tnl=1) NOTICE: #0.2 3 --> start of statement block (tnl=1) NOTICE: #0.3 5 --> start of SQL statement (query='create role regress_plpgsql_ch ..') (tnl=2) NOTICE: #0.3 <-- end of SQL statement (elapsed time=0.010 ms) aborted NOTICE: #0.2 <-- end of statement block (elapsed time=0.010 ms) NOTICE: #0.1 <-- end of statement block (elapsed time=0.010 ms) NOTICE: #0 <<- end of block (elapsed time=0.010 ms) drop role regress_plpgsql_check_test_role; set plpgsql_check.enable_tracer to off; select plpgsql_check_tracer(false); NOTICE: tracer is not active NOTICE: tracer verbosity is verbose plpgsql_check_tracer ---------------------- f (1 row) -- tracing constants -- issue #159 create or replace function tabret_dynamic() returns table (id integer, val text) as $$ begin return query execute 'select id, val from (values (1, ''a''), (2, ''b'')) as v(id, val)'; end; $$ language plpgsql immutable; -- should be ok select * from plpgsql_check_function('tabret_dynamic()'); plpgsql_check_function ------------------------ (0 rows) create or replace function tabret_dynamic() returns table (id integer, val text) as $$ declare z_query text; begin z_query := 'select id, val from (values (1, ''a''), (2, ''b'')) as v(id, val)'; execute z_query into id, val; return next; end; $$ language plpgsql immutable; -- should be ok select * from plpgsql_check_function('tabret_dynamic()'); plpgsql_check_function ------------------------ (0 rows) create or replace function tabret_dynamic() returns table (id integer, val text) as $$ declare z_query text; begin z_query := 'select id, val from (values (1, ''a''), (2, ''b'')) as v(id, val)'; for id, val in execute z_query loop return next; end loop; end; $$ language plpgsql immutable; -- should be ok select * from plpgsql_check_function('tabret_dynamic()'); plpgsql_check_function ------------------------ (0 rows) create or replace function tabret_dynamic() returns table (id integer, val text) as $$ declare z_query text; begin z_query := 'select id, val from (values (1, ''a''), (2, ''b'')) as v(id, val)'; return query execute z_query; end; $$ language plpgsql immutable; -- should be ok select * from plpgsql_check_function('tabret_dynamic()'); plpgsql_check_function ------------------------ (0 rows) drop function tabret_dynamic; -- should not to crash on empty string, or comment create or replace function dynamic_emptystr() returns void as $$ begin execute ''; end; $$ language plpgsql; -- should be ok select * from plpgsql_check_function('dynamic_emptystr()'); plpgsql_check_function ------------------------ (0 rows) create or replace function dynamic_emptystr() returns void as $$ declare x int; begin execute '--' into x; end; $$ language plpgsql; -- should not to crash, no result error select * from plpgsql_check_function('dynamic_emptystr()'); plpgsql_check_function ------------------------------------------------------- error:XX000:4:EXECUTE:expression does not return data (1 row) drop function dynamic_emptystr(); -- unclosed cursor detection create or replace function fx() returns void as $$ declare c refcursor; begin open c for select * from pg_class; end; $$ language plpgsql; do $$ begin perform fx(); -- should to show warning perform fx(); end; $$; WARNING: cursor "c" is not closed set plpgsql_check.strict_cursors_leaks to on; do $$ begin -- should to show warning perform fx(); -- should to show warning perform fx(); end; $$; WARNING: cursor is not closed WARNING: cursor is not closed create or replace function fx() returns void as $$ declare c refcursor; begin open c for select * from pg_class; close c; end; $$ language plpgsql; -- without warnings do $$ begin perform fx(); perform fx(); end; $$; set plpgsql_check.strict_cursors_leaks to off; drop function fx(); create table public.testt(a int, b int); create or replace function fx() returns void as $$ declare v1 varchar default 'public'; v2 varchar default 'testt'; v3 varchar default 'a'; begin raise notice '%', format('%I.%I.%I', v1, v2, v3); perform 'pragma:assert-schema: v1'; perform 'pragma:assert-table: v1, v2'; perform 'pragma:assert-column: v1, v2, v3'; perform 'pragma:assert-table: v2'; perform 'pragma:assert-column: v2, v3'; end; $$ language plpgsql; select * from plpgsql_check_function('fx()'); plpgsql_check_function ------------------------ (0 rows) create table public.testt(a int, b int); ERROR: relation "testt" already exists create or replace function fx() returns void as $$ declare v1 varchar default 'public'; v2 varchar default 'testt'; v3 varchar default 'x'; begin raise notice '%', format('%I.%I.%I', v1, v2, v3); perform 'pragma:assert-column: v1, v2, v3'; end; $$ language plpgsql; select * from plpgsql_check_function('fx()'); plpgsql_check_function ------------------------------------------------------------------------------ error:42703:8:PERFORM:column "x" of relation "public"."testt" does not exist (1 row) drop function fx(); drop table public.testt; create or replace function fx() returns void as $$ declare v_sql varchar default ''; i int; begin -- we don't support constant tracing from queries select 'bla bla bla' into v_sql; execute v_sql into i; raise notice '%', i; end; $$ language plpgsql; -- should be ok select * from plpgsql_check_function('fx()'); plpgsql_check_function ------------------------ (0 rows) drop function fx(); -- should not to raise warning do $$ declare c cursor for select * from generate_series(1,10); t int; begin for i in 1..2 loop open c; loop fetch c into t; exit when not found; raise notice '%', t; end loop; close c; end loop; end; $$; NOTICE: 1 NOTICE: 2 NOTICE: 3 NOTICE: 4 NOTICE: 5 NOTICE: 6 NOTICE: 7 NOTICE: 8 NOTICE: 9 NOTICE: 10 NOTICE: 1 NOTICE: 2 NOTICE: 3 NOTICE: 4 NOTICE: 5 NOTICE: 6 NOTICE: 7 NOTICE: 8 NOTICE: 9 NOTICE: 10 -- should not raise warning create or replace function fx(p text) returns void as $$ begin execute format($_$select '%1$I'$_$, p); end; $$ language plpgsql; -- should be ok select * from plpgsql_check_function('fx(text)'); plpgsql_check_function ------------------------ (0 rows) drop function fx(text); create or replace function fx() returns void as $$ declare p varchar; begin p := 'ahoj'; execute format($_$select '%1$I'$_$, p); end; $$ language plpgsql; -- should be ok select * from plpgsql_check_function('fx()'); plpgsql_check_function ------------------------ (0 rows) drop function fx(); -- should not crash create or replace procedure p1() as $$ begin commit; end; $$ language plpgsql; set plpgsql_check.cursors_leaks to on; do $$ declare c cursor for select 1; begin open c; call p1(); end $$; set plpgsql_check.cursors_leaks to default; drop procedure p1; -- should not crash create or replace function nested_trace_test(a int) returns int as $$ begin perform plpgsql_check_pragma('enable:tracer'); return a + 1; end; $$ language plpgsql; create or replace function trace_test(b int) returns int as $$ declare r int default 0; begin for i in 1..b loop r := nested_trace_test(r); end loop; return r; end; $$ language plpgsql; -- when plpgsql_check is not loaded yet, then plpgsql_check is -- load by perform plpgsql_check_pragma and this is another -- case, when fmgr hook is not called in expected order. \c -- should not to crash select trace_test(3); trace_test ------------ 3 (1 row) create table testtable_pure_expr(a int); -- detection of not pure expressions create or replace function pure_expr() returns void as $$ declare v int; begin v := 1 delete from testtable_pure_expr where a = 10; raise notice '%', v; end; $$ language plpgsql; -- raise warning select * from plpgsql_check_function('pure_expr()'); plpgsql_check_function -------------------------------------------------------------------- warning extra:42601:4:assignment:expression is not pure expression Query: v := 1 -- ^ delete from testtable_pure_expr where a = 10 Detail: there is a possibility of unwanted behave Hint: Maybe you forgot to use a semicolon. Context: at assignment to variable "v" declared on line 2 (7 rows) drop table testtable_pure_expr; -- should not to raise false alarm drop function fxx(ct, ct); drop type ct; create type ct as (x int, y numeric, v varchar); create procedure proc_composite(inout cv ct, x int, y numeric, v varchar) as $$ begin cv.x := x; cv.y := y; cv.v := v; end; $$ language plpgsql; call proc_composite(null, 10, 10.2, 'ahoj'); cv ---------------- (10,10.2,ahoj) (1 row) create or replace function test_proc_composite() returns void as $$ declare cv ct; begin call proc_composite(cv, 10, 10.2, 'ahoj'); end; $$ language plpgsql; select * from plpgsql_check_function('test_proc_composite'); plpgsql_check_function ------------------------ (0 rows) drop function test_proc_composite; drop procedure proc_composite; create procedure proc_composite(inout cv1 ct, cv2 ct) as $$ begin cv1.x := 10; cv2.v := 'Ahoj'; end; $$ language plpgsql; create or replace function test_proc_composite() returns void as $$ declare cv1 ct; cv2 ct; begin call proc_composite(cv1, cv2); end; $$ language plpgsql; select * from plpgsql_check_function('test_proc_composite'); plpgsql_check_function ------------------------ (0 rows) drop function test_proc_composite; drop procedure proc_composite; drop type ct; create or replace function double_usage_of_const_str() returns void as $$ declare p_id_arr integer[]; r_id integer; begin p_id_arr := '{1,2,3}'::integer[]; foreach r_id in array p_id_arr loop raise notice 'id: %', r_id; end loop; --the following loop causes an error foreach r_id in array p_id_arr loop raise notice 'id: %', r_id; end loop; end; $$ language plpgsql; -- should not to raise false alarm #195 select * from plpgsql_check_function('double_usage_of_const_str'); plpgsql_check_function ------------------------ (0 rows) -- using reserved world for names is bad create or replace function test_bad_label() returns int as $$ <> declare x int = 10; begin return x; end; $$ language plpgsql; select * from plpgsql_check_function('test_bad_label'); plpgsql_check_function --------------------------------------------------------------------------- warning:00000:4:statement block:name of label "outer" is reserved keyword Detail: The reserved keyword was used as label name. (2 rows) drop function test_bad_label; -- using slow legacy syntax for assignment create or replace function test_bad_assign() returns int as $$ declare x int; begin select 0 into x; return x; end; $$ language plpgsql immutable; select * from plpgsql_check_function('test_bad_assign()', all_warnings=>true); plpgsql_check_function ------------------------------------------------------------------------ performance:00000:4:SQL statement:detected "SELECT expr INTO variable" Query: select 0 -- ^ Detail: This obsolete syntax of assigning can be slow. Hint: Use syntax "variable := expr" instead. (5 rows) drop function test_bad_assign; create or replace function volatility_check() returns int as $$ begin return (select count(*) from pg_class); end; $$ language plpgsql volatile; -- should be ok select * from plpgsql_check_function('volatility_check()'); plpgsql_check_function ------------------------ (0 rows) create or replace function volatility_check() returns int as $$ begin return (select count(*) from pg_class); end; $$ language plpgsql stable; -- should be ok select * from plpgsql_check_function('volatility_check()'); plpgsql_check_function ------------------------ (0 rows) create or replace function volatility_check() returns int as $$ begin return (select count(*) from pg_class); end; $$ language plpgsql immutable; -- warning select * from plpgsql_check_function('volatility_check()'); plpgsql_check_function ----------------------------------------------------------------------------------------- warning:00000:3:RETURN:routine is marked as IMMUTABLE, but expression is STABLE Detail: there is a risk of unwanted behave Hint: When you fix this issue, please, recheck other functions that uses this function. (3 rows) create or replace function volatility_check() returns int as $$ begin return random()*1000; end; $$ language plpgsql volatile; -- should be ok select * from plpgsql_check_function('volatility_check()'); plpgsql_check_function ------------------------ (0 rows) create or replace function volatility_check() returns int as $$ begin return random()*1000; end; $$ language plpgsql stable; -- warning select * from plpgsql_check_function('volatility_check()'); plpgsql_check_function ----------------------------------------------------------------------------------------- warning:00000:3:RETURN:routine is marked as STABLE, but expression is VOLATILE Detail: there is a risk of unwanted behave Hint: When you fix this issue, please, recheck other functions that uses this function. (3 rows) drop function volatility_check(); -- issue #201 - should not to raise false error, when composite -- constant is used and assigned to composite with more fields create type t_retcode as (retcode varchar, errmsg varchar, id int); create or replace function test_retcode_bad() returns void as $$ declare rc t_retcode := row('MO_REG_ERROR'); begin raise notice '%', rc.retcode; end; $$ language plpgsql; select * from plpgsql_check_function('test_retcode_bad'); plpgsql_check_function ------------------------------------------------------------------------------ warning:00000:4:statement block:too few attributes for composite variable Context: during statement block local variable "rc" initialization on line 3 (2 rows) drop function test_retcode_bad; -- test usage domain of composite in output #202 create domain t_retcode_domain as t_retcode; create or replace function test_retcode_bad(INOUT x1 t_retcode_domain, INOUT x2 int) as $$ begin x1.retcode = concat(x1.retcode, 'xxx'); x2 := x2 + 10; end; $$ language plpgsql; select * from plpgsql_check_function('test_retcode_bad'); plpgsql_check_function ------------------------------------------------------------------------ warning extra:00000:composite OUT variable "x1" is not single argument (1 row) drop function test_retcode_bad; drop type t_retcode cascade; NOTICE: drop cascades to type t_retcode_domain create type demo_payload as (value integer); create function demo_produce_row(out status_code integer, out payload record) as $$ begin status_code := 0; payload := ROW(1)::demo_payload; end $$ language plpgsql; create function demo_consume_row() returns void as $$ declare fetched_row record; typed_payload demo_payload; observed_status integer; begin select * into strict fetched_row from demo_produce_row(); typed_payload := fetched_row.payload; observed_status := fetched_row.status_code; end; $$ language plpgsql; -- should not crash - issue #216 select * from plpgsql_check_function('demo_consume_row()'); plpgsql_check_function --------------------------------------------------------------------- warning extra:00000:4:DECLARE:never read variable "typed_payload" warning extra:00000:5:DECLARE:never read variable "observed_status" (2 rows) -- should not crash - issue #220 create function f_poly(anyelement) returns int as $$ begin return $1; end; $$ language plpgsql; select plpgsql_check_profiler(true); NOTICE: profiler is active plpgsql_check_profiler ------------------------ t (1 row) select f_poly(1); f_poly -------- 1 (1 row) select plpgsql_coverage_statements('f_poly(anyelement)'); plpgsql_coverage_statements ----------------------------- 1 (1 row) select plpgsql_coverage_statements('f_poly'); plpgsql_coverage_statements ----------------------------- 1 (1 row) select plpgsql_check_profiler(false); NOTICE: profiler is not active plpgsql_check_profiler ------------------------ f (1 row) drop function f_poly; create or replace function repro04() returns void as $$ begin execute 'select 1'; end $$ language plpgsql; select * from plpgsql_check_function('repro04()', performance_warnings => true); plpgsql_check_function ----------------------------------------------------------------------------------------- performance:00000:3:EXECUTE:immutable expression without parameters found Detail: the EXECUTE command is not necessary probably Hint: Don't use dynamic SQL when you can use static SQL. performance:00000:routine is marked as VOLATILE, should be IMMUTABLE Hint: When you fix this issue, please, recheck other functions that uses this function. (5 rows) create or replace function repro04_into() returns int as $$ declare r int; begin execute 'select $1' into r using 1; return r; end $$ language plpgsql; select * from plpgsql_check_function('repro04_into()', performance_warnings => true); plpgsql_check_function ----------------------------------------------------------------------------------------- performance:00000:routine is marked as VOLATILE, should be IMMUTABLE Hint: When you fix this issue, please, recheck other functions that uses this function. (2 rows) create or replace function repro04_real_dynsql(p text) returns void as $$ begin execute 'select ' || quote_literal(p); end $$ language plpgsql; select * from plpgsql_check_function('repro04_real_dynsql(text)', performance_warnings => true); plpgsql_check_function ----------------------------------------------------------------------------------------- performance:00000:routine is marked as VOLATILE, should be IMMUTABLE Detail: attention: cannot to determine volatility of used dynamic SQL Hint: When you fix this issue, please, recheck other functions that uses this function. (3 rows) -- a multi statement command is real dynamic sql too, and this case was -- reported correctly even before the fix. create or replace function repro04_multi() returns void as $$ begin execute 'select 1; select 2'; end $$ language plpgsql; select * from plpgsql_check_function('repro04_multi()', performance_warnings => true); plpgsql_check_function ----------------------------------------------------------------------------------------- performance:00000:routine is marked as VOLATILE, should be IMMUTABLE Hint: When you fix this issue, please, recheck other functions that uses this function. (2 rows) create table repro04_multi_target(a int); create or replace function repro04_multi() returns void as $$ begin execute 'select 1; insert into repro04_multi_target values(10)'; end $$ language plpgsql; select * from plpgsql_check_function('repro04_multi()', performance_warnings => true); plpgsql_check_function ------------------------ (0 rows) drop table repro04_multi_target; drop function repro04(); drop function repro04_into(); drop function repro04_real_dynsql(text); drop function repro04_multi(); create or replace function unpriv_issue4_rowexpr() returns void as $$ declare r record; begin select (row(1, 'hello'::text)).* into r; raise notice '%', r.f1; select row(1, 'hello') into r; raise notice '%', r.row; end; $$ language plpgsql; select unpriv_issue4_rowexpr(); NOTICE: 1 NOTICE: (1,hello) unpriv_issue4_rowexpr ----------------------- (1 row) -- there should not be any false errors select * from plpgsql_check_function_tb('unpriv_issue4_rowexpr()'); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+---------+--------+------+-------+----------+-------+--------- (0 rows) drop function unpriv_issue4_rowexpr(); okbob-plpgsql_check-800db79/expected/plpgsql_check_assign.out000066400000000000000000000525601526744060500245200ustar00rootroot00000000000000set client_min_messages to warning; create extension if not exists plpgsql_check; set client_min_messages to notice; -- -- assignment to the automatic variables -- -- the variable of a numeric FOR loop is maintained by the runtime, so it -- should not be modified by the body of the loop create function as_f1(n int) returns int as $$ declare s int := 0; begin for i in 1..n loop s := s + i; i := i + 1; end loop; return s; end; $$ language plpgsql; select * from plpgsql_check_function('as_f1', extra_warnings => false); plpgsql_check_function ------------------------ (0 rows) select * from plpgsql_check_function('as_f1', extra_warnings => true); plpgsql_check_function ---------------------------------------------------------------------------------- warning extra:00000:6:assignment:auto varible "i" should not be modified by user Context: at assignment (2 rows) -- the same for the FOUND variable create function as_f2() returns int as $$ begin found := true; return 1; end; $$ language plpgsql; select * from plpgsql_check_function('as_f2', extra_warnings => true); plpgsql_check_function -------------------------------------------------------------------------------------- warning extra:00000:3:assignment:auto varible "found" should not be modified by user Context: at assignment (2 rows) -- -- assignment to a constant -- -- the default value of a constant is assigned at the start of the block, -- and it is not reported create function as_f4() returns int as $$ declare c constant int := 1; begin return c; end; $$ language plpgsql; select * from plpgsql_check_function('as_f4'); plpgsql_check_function ------------------------ (0 rows) -- -- assignment to a field of a record -- -- the structure of a record is known after the first assignment only create function as_f5() returns int as $$ declare r record; begin r.a := 1; return r.a; end; $$ language plpgsql; select * from plpgsql_check_function('as_f5'); plpgsql_check_function ------------------------------------------------------------------------ error:55000:4:assignment:record "r" is not assigned to tuple structure Context: at assignment to field "a" of variable "r" declared on line 2 (2 rows) -- when the record has a structure, the name of the field is validated create function as_f6() returns int as $$ declare r record; begin select 1 as a, 2 as b into r; r.c := 1; return r.a; end; $$ language plpgsql; select * from plpgsql_check_function('as_f6'); plpgsql_check_function ------------------------------------------------------------------------ error:42703:5:assignment:record "r" has no field "c" Context: at assignment to field "c" of variable "r" declared on line 2 (2 rows) -- and the type of the field is used for the check of the assigned value create function as_f7() returns int as $$ declare r record; begin select 1 as a, 2 as b into r; r.a := 'x'; return r.a; end; $$ language plpgsql; select * from plpgsql_check_function('as_f7'); plpgsql_check_function ------------------------------------------------------------------------ error:22P02:5:assignment:invalid input syntax for type integer: "x" Query: r.a := 'x' -- ^ Context: at assignment to field "a" of variable "r" declared on line 2 (4 rows) -- -- checks of the assigned type -- create table as_tab1(a int, b int, c int); -- a composite value cannot be assigned to a scalar variable create function as_f8() returns int as $$ declare x int; begin select t into x from as_tab1 t; return x; end; $$ language plpgsql; select * from plpgsql_check_function('as_f8'); plpgsql_check_function --------------------------------------------------------------------------------------------------------------- error:42804:4:SQL statement:cannot cast composite value of "as_tab1" type to a scalar value of "integer" type Query: select t from as_tab1 t -- ^ Context: at SQL statement to variable "x" declared on line 2 (4 rows) -- a cast which is only explicit, a cast which is allowed on assignment -- and a cast which is done implicitly create function as_f9() returns void as $$ declare i int; t text; n numeric; begin select 'x'::text into i; select 1 into t; select 1.5::numeric into i; select 1 into n; end; $$ language plpgsql; select * from plpgsql_check_function('as_f9', fatal_errors => false); plpgsql_check_function -------------------------------------------------------------------------------------- warning:42804:7:SQL statement:target type is different type than source type Query: select 'x'::text -- ^ Detail: cast "text" value to "integer" type Hint: The input expression type does not have an assignment cast to the target type. Context: at SQL statement to variable "i" declared on line 3 warning extra:00000:3:DECLARE:never read variable "i" warning extra:00000:4:DECLARE:never read variable "t" warning extra:00000:5:DECLARE:never read variable "n" (9 rows) select * from plpgsql_check_function('as_f9', fatal_errors => false, performance_warnings => true); plpgsql_check_function ----------------------------------------------------------------------------------------- performance:00000:7:SQL statement:detected "SELECT expr INTO variable" Query: select 'x'::text -- ^ Detail: This obsolete syntax of assigning can be slow. Hint: Use syntax "variable := expr" instead. warning:42804:7:SQL statement:target type is different type than source type Query: select 'x'::text -- ^ Detail: cast "text" value to "integer" type Hint: The input expression type does not have an assignment cast to the target type. Context: at SQL statement to variable "i" declared on line 3 performance:00000:8:SQL statement:detected "SELECT expr INTO variable" Query: select 1 -- ^ Detail: This obsolete syntax of assigning can be slow. Hint: Use syntax "variable := expr" instead. performance:42804:8:SQL statement:target type is different type than source type Query: select 1 -- ^ Detail: cast "integer" value to "text" type Hint: Hidden casting can be a performance issue. Context: at SQL statement to variable "t" declared on line 4 performance:00000:9:SQL statement:detected "SELECT expr INTO variable" Query: select 1.5::numeric -- ^ Detail: This obsolete syntax of assigning can be slow. Hint: Use syntax "variable := expr" instead. performance:42804:9:SQL statement:target type is different type than source type Query: select 1.5::numeric -- ^ Detail: cast "numeric" value to "integer" type Hint: Hidden casting can be a performance issue. Context: at SQL statement to variable "i" declared on line 3 performance:00000:10:SQL statement:detected "SELECT expr INTO variable" Query: select 1 -- ^ Detail: This obsolete syntax of assigning can be slow. Hint: Use syntax "variable := expr" instead. performance:42804:10:SQL statement:target type is different type than source type Query: select 1 -- ^ Detail: cast "integer" value to "numeric" type Hint: Hidden casting can be a performance issue. Context: at SQL statement to variable "n" declared on line 5 warning extra:00000:3:DECLARE:never read variable "i" warning extra:00000:4:DECLARE:never read variable "t" warning extra:00000:5:DECLARE:never read variable "n" performance:00000:routine is marked as VOLATILE, should be IMMUTABLE Hint: When you fix this issue, please, recheck other functions that uses this function. (49 rows) -- -- assignment of a query result to a composite variable -- -- a variable of a table rowtype where the table has a dropped column - -- the descriptors are not equal, but the dropped column is skipped on -- both sides alter table as_tab1 drop column b; create function as_f10() returns int as $$ declare r as_tab1; begin select * into r from as_tab1; return r.a; end; $$ language plpgsql; select * from plpgsql_check_function('as_f10'); plpgsql_check_function ------------------------ (0 rows) -- the query returns less columns than the variable has create function as_f11() returns int as $$ declare r as_tab1; begin select a into r from as_tab1; return r.a; end; $$ language plpgsql; select * from plpgsql_check_function('as_f11'); plpgsql_check_function ------------------------------------------------------------------------- warning:00000:4:SQL statement:too few attributes for composite variable Query: select a from as_tab1 -- ^ Context: at SQL statement to variable "r" declared on line 2 (4 rows) -- and more columns than the variable has create function as_f12() returns int as $$ declare r as_tab1; begin select a, c, a from as_tab1 into r; return r.a; end; $$ language plpgsql; select * from plpgsql_check_function('as_f12'); plpgsql_check_function -------------------------------------------------------------------------- warning:00000:4:SQL statement:too many attributes for composite variable Query: select a, c, a from as_tab1 -- ^ Context: at SQL statement to variable "r" declared on line 2 (4 rows) -- the types of the columns are compared one by one create function as_f13() returns int as $$ declare r as_tab1; begin select 'x'::text, 1 into r; return r.a; end; $$ language plpgsql; select * from plpgsql_check_function('as_f13', fatal_errors => false); plpgsql_check_function -------------------------------------------------------------------------------------- warning:42804:4:SQL statement:target type is different type than source type Query: select 'x'::text, 1 -- ^ Detail: cast "text" value to "integer" type Hint: The input expression type does not have an assignment cast to the target type. Context: at SQL statement to variable "r" declared on line 2 (6 rows) -- a whole row value of a type with a dropped column is assigned to a -- record variable create function as_f14() returns int as $$ declare r record; begin select t.* into r from as_tab1 t; return r.a; end; $$ language plpgsql; select * from plpgsql_check_function('as_f14'); plpgsql_check_function ------------------------ (0 rows) -- the NEW and the OLD records of a trigger get the descriptor of the -- triggering relation, which contains the dropped column too create function as_trg1() returns trigger as $$ begin new.a := old.a; new.c := 1; return new; end; $$ language plpgsql; create trigger as_trg1 before update on as_tab1 for each row execute procedure as_trg1(); select * from plpgsql_check_function('as_trg1', relid => 'as_tab1'::regclass); plpgsql_check_function ------------------------ (0 rows) -- FOREACH applies assignment casts to elements and slices, not array input. create function as_f15() returns int as $$ declare item int; total int := 0; begin foreach item in array array[1.6::numeric, 2.4::numeric] loop total := total + item; end loop; return total; end; $$ language plpgsql; select * from plpgsql_check_function('as_f15'); plpgsql_check_function ------------------------ (0 rows) select as_f15(); as_f15 -------- 4 (1 row) create function as_f16() returns int[] as $$ declare item int[]; begin foreach item slice 1 in array array[[1.6::numeric, 2.4::numeric]] loop return item; end loop; return null; end; $$ language plpgsql; select * from plpgsql_check_function('as_f16'); plpgsql_check_function ------------------------ (0 rows) select as_f16(); as_f16 -------- {2,2} (1 row) -- Without an assignment cast, invalid textual input is still rejected. create function as_f17() returns void as $$ declare item int; begin foreach item in array array['not an integer'] loop raise notice '%', item; end loop; end; $$ language plpgsql; select * from plpgsql_check_function('as_f17'); plpgsql_check_function ------------------------------------------------------------------------------------------ error:22P02:4:FOREACH over array:invalid input syntax for type integer: "not an integer" (1 row) drop function as_f15(); drop function as_f16(); drop function as_f17(); drop function as_f1(int); drop function as_f2(); drop function as_f4(); drop function as_f5(); drop function as_f6(); drop function as_f7(); drop function as_f8(); drop function as_f9(); drop function as_f10(); drop function as_f11(); drop function as_f12(); drop function as_f13(); drop function as_f14(); drop trigger as_trg1 on as_tab1; drop function as_trg1(); -- A composite source may have a valid scalar assignment cast, or be NULL. create type as_pair as (a int, b int); create function as_f18() returns text as $$ declare value text; begin select row(1, 2) into value; return value; end; $$ language plpgsql; select * from plpgsql_check_function('as_f18'); plpgsql_check_function ------------------------ (0 rows) select as_f18(); as_f18 -------- (1,2) (1 row) create function as_f19() returns int as $$ declare value int; begin select null::as_tab1 into value; return value; end; $$ language plpgsql; select * from plpgsql_check_function('as_f19'); plpgsql_check_function ------------------------ (0 rows) select as_f19() is null; ?column? ---------- t (1 row) create function as_pair_to_int(p as_pair) returns int as $$ select p.a + p.b $$ language sql immutable; create cast (as_pair as int) with function as_pair_to_int(as_pair) as assignment; create function as_f20() returns int as $$ declare value int; begin select row(1, 2)::as_pair into value; return value; end; $$ language plpgsql; select * from plpgsql_check_function('as_f20'); plpgsql_check_function ------------------------ (0 rows) select as_f20(); as_f20 -------- 3 (1 row) drop function as_f18(); drop function as_f19(); drop function as_f20(); drop cast (as_pair as int); drop function as_pair_to_int(as_pair); drop type as_pair; -- RETURN QUERY retains composite-valued columns; RETURN expands a row value. create type as_leaf as (n int); create type as_wrapper as (payload as_leaf); create function as_f21() returns setof as_wrapper as $$ begin return query select row(7)::as_leaf; end; $$ language plpgsql; select * from plpgsql_check_function('as_f21'); plpgsql_check_function ------------------------ (0 rows) select (payload).n from as_f21(); n --- 7 (1 row) create function as_f22() returns setof as_wrapper as $$ begin return query execute 'select row(7)::as_leaf'; end; $$ language plpgsql; select * from plpgsql_check_function('as_f22'); plpgsql_check_function ------------------------ (0 rows) select (payload).n from as_f22(); n --- 7 (1 row) create function as_f23() returns setof as_leaf as $$ begin return query select row(7)::as_leaf; end; $$ language plpgsql; select * from plpgsql_check_function('as_f23'); plpgsql_check_function ------------------------------------------------------------------------------------------------ error:42804:3:RETURN QUERY:structure of query does not match function result type Detail: Returned type as_leaf does not match expected type integer in column "n" (position 1). (2 rows) select * from as_f23(); ERROR: structure of query does not match function result type DETAIL: Returned type as_leaf does not match expected type integer in column "n" (position 1). CONTEXT: SQL statement "select row(7)::as_leaf" PL/pgSQL function as_f23() line 3 at RETURN QUERY create function as_f24() returns as_leaf as $$ begin return row(7)::as_leaf; end; $$ language plpgsql; select * from plpgsql_check_function('as_f24'); plpgsql_check_function ------------------------ (0 rows) select (as_f24()).n; n --- 7 (1 row) create function as_f25() returns setof as_leaf as $$ begin return next row(7)::as_leaf; return; end; $$ language plpgsql; select * from plpgsql_check_function('as_f25'); plpgsql_check_function ------------------------ (0 rows) select * from as_f25(); n --- 7 (1 row) -- Bounds and the RHS read their variables; the implicit old array does not. create function as_f26() returns void as $$ declare element_array int[] := array[1, 2]; lower_array int[] := array[1, 2]; upper_array int[] := array[1, 2]; rhs_array int[] := array[1, 2]; fetch_array int[] := array[1, 2]; write_only int[] := array[1, 2]; begin element_array[element_array[1]] := 3; lower_array[lower_array[1]:2] := array[3, 4]; upper_array[1:upper_array[2]] := array[3, 4]; rhs_array[1] := rhs_array[2]; fetch_array := fetch_array[1:2]; write_only[1] := 3; end; $$ language plpgsql; select * from plpgsql_check_function('as_f26'); plpgsql_check_function ---------------------------------------------------------------- warning extra:00000:8:DECLARE:never read variable "write_only" (1 row) select as_f26(); as_f26 -------- (1 row) drop function as_f26(); drop function as_f21(); drop function as_f22(); drop function as_f23(); drop function as_f24(); drop function as_f25(); drop type as_wrapper; drop type as_leaf; drop table as_tab1; create type point2 as (x int, y int); create domain dpoint as point2 check ((value).x is not null); -- a) domain-over-composite as a parameter create function dom_param(p dpoint) returns int language plpgsql as $$ begin return p.x; end; $$; -- b) domain-over-composite as an initialized local variable create function dom_var() returns int language plpgsql as $$ declare r dpoint := row(1, 2)::dpoint; begin return r.x; end; $$; select count(*) = 0 from plpgsql_check_function_tb('dom_param(dpoint)', fatal_errors => false) where message like '%violates check constraint%'; ?column? ---------- t (1 row) select count(*) = 0 from plpgsql_check_function_tb('dom_var()', fatal_errors => false) where message like '%violates check constraint%'; ?column? ---------- t (1 row) drop function dom_var; drop function dom_param; drop type point2 cascade; NOTICE: drop cascades to type dpoint create function foreach_arrvar() returns int language plpgsql as $$ declare item int[]; total int := 0; begin foreach item in array '{"{1,2}","{3}"}'::text[] loop total := total + cardinality(item); end loop; return total; end; $$; select message from plpgsql_check_function_tb('foreach_arrvar()', fatal_errors => false); message ------------------------------------------------ target type is different type than source type (1 row) drop function foreach_arrvar(); create function stale_ctx() returns int language plpgsql as $$ declare r record; unused_v int; -- produces an "unused variable" warning begin r.a := 10; -- fails: record "r" is not assigned yet; sets err_text return 1; end; $$; select * from plpgsql_check_function('stale_ctx', fatal_errors => false); plpgsql_check_function ------------------------------------------------------------------------ error:55000:6:assignment:record "r" is not assigned to tuple structure Context: at assignment to field "a" of variable "r" declared on line 3 warning:00000:4:DECLARE:unused variable "unused_v" warning extra:00000:3:DECLARE:never read variable "r" (4 rows) drop function stale_ctx(); create table repro_foreach_tab(a int, b int); -- the target of FOREACH ARRAY holds an element of the array, so the array -- expression must not be traced as a constant value of the target variable create or replace function repro_foreach() returns void as $$ declare cols text[] := array['a', 'b']; c text; r int; begin foreach c in array cols loop execute format('select %I from repro_foreach_tab', c) into r; end loop; end $$ language plpgsql; select * from plpgsql_check_function('repro_foreach()'); plpgsql_check_function ------------------------------------------------------- warning extra:00000:5:DECLARE:never read variable "r" (1 row) -- with SLICE the target holds an array, and tracing of a composite value -- is not supported anyway create or replace function repro_foreach_slice() returns void as $$ declare cols text[] := array[['a'], ['b']]; c text[]; r int; begin foreach c slice 1 in array cols loop execute format('select %I from repro_foreach_tab', c[1]) into r; end loop; end $$ language plpgsql; select * from plpgsql_check_function('repro_foreach_slice()'); plpgsql_check_function ------------------------------------------------------- warning extra:00000:5:DECLARE:never read variable "r" (1 row) -- plain assignment is still traced create or replace function repro_foreach_const() returns void as $$ declare c text := 'unknown_column'; r int; begin execute format('select %I from repro_foreach_tab', c) into r; end $$ language plpgsql; select * from plpgsql_check_function('repro_foreach_const()'); plpgsql_check_function -------------------------------------------------------------- error:42703:6:EXECUTE:column "unknown_column" does not exist Query: select unknown_column from repro_foreach_tab -- ^ (3 rows) drop function repro_foreach(); drop function repro_foreach_slice(); drop function repro_foreach_const(); drop table repro_foreach_tab; okbob-plpgsql_check-800db79/expected/plpgsql_check_assign_1.out000066400000000000000000000524701526744060500247400ustar00rootroot00000000000000set client_min_messages to warning; create extension if not exists plpgsql_check; set client_min_messages to notice; -- -- assignment to the automatic variables -- -- the variable of a numeric FOR loop is maintained by the runtime, so it -- should not be modified by the body of the loop create function as_f1(n int) returns int as $$ declare s int := 0; begin for i in 1..n loop s := s + i; i := i + 1; end loop; return s; end; $$ language plpgsql; select * from plpgsql_check_function('as_f1', extra_warnings => false); plpgsql_check_function ------------------------ (0 rows) select * from plpgsql_check_function('as_f1', extra_warnings => true); plpgsql_check_function ---------------------------------------------------------------------------------- warning extra:00000:6:assignment:auto varible "i" should not be modified by user Context: at assignment (2 rows) -- the same for the FOUND variable create function as_f2() returns int as $$ begin found := true; return 1; end; $$ language plpgsql; select * from plpgsql_check_function('as_f2', extra_warnings => true); plpgsql_check_function -------------------------------------------------------------------------------------- warning extra:00000:3:assignment:auto varible "found" should not be modified by user Context: at assignment (2 rows) -- -- assignment to a constant -- -- the default value of a constant is assigned at the start of the block, -- and it is not reported create function as_f4() returns int as $$ declare c constant int := 1; begin return c; end; $$ language plpgsql; select * from plpgsql_check_function('as_f4'); plpgsql_check_function ------------------------ (0 rows) -- -- assignment to a field of a record -- -- the structure of a record is known after the first assignment only create function as_f5() returns int as $$ declare r record; begin r.a := 1; return r.a; end; $$ language plpgsql; select * from plpgsql_check_function('as_f5'); plpgsql_check_function ------------------------------------------------------------------------ error:55000:4:assignment:record "r" is not assigned to tuple structure Context: at assignment to field "a" of variable "r" declared on line 2 (2 rows) -- when the record has a structure, the name of the field is validated create function as_f6() returns int as $$ declare r record; begin select 1 as a, 2 as b into r; r.c := 1; return r.a; end; $$ language plpgsql; select * from plpgsql_check_function('as_f6'); plpgsql_check_function ------------------------------------------------------------------------ error:42703:5:assignment:record "r" has no field "c" Context: at assignment to field "c" of variable "r" declared on line 2 (2 rows) -- and the type of the field is used for the check of the assigned value create function as_f7() returns int as $$ declare r record; begin select 1 as a, 2 as b into r; r.a := 'x'; return r.a; end; $$ language plpgsql; select * from plpgsql_check_function('as_f7'); plpgsql_check_function ------------------------------------------------------------------------ error:22P02:5:assignment:invalid input syntax for type integer: "x" Query: r.a := 'x' -- ^ Context: at assignment to field "a" of variable "r" declared on line 2 (4 rows) -- -- checks of the assigned type -- create table as_tab1(a int, b int, c int); -- a composite value cannot be assigned to a scalar variable create function as_f8() returns int as $$ declare x int; begin select t into x from as_tab1 t; return x; end; $$ language plpgsql; select * from plpgsql_check_function('as_f8'); plpgsql_check_function --------------------------------------------------------------------------------------------------------------- error:42804:4:SQL statement:cannot cast composite value of "as_tab1" type to a scalar value of "integer" type Query: select t from as_tab1 t -- ^ Context: at SQL statement to variable "x" declared on line 2 (4 rows) -- a cast which is only explicit, a cast which is allowed on assignment -- and a cast which is done implicitly create function as_f9() returns void as $$ declare i int; t text; n numeric; begin select 'x'::text into i; select 1 into t; select 1.5::numeric into i; select 1 into n; end; $$ language plpgsql; select * from plpgsql_check_function('as_f9', fatal_errors => false); plpgsql_check_function -------------------------------------------------------------------------------------- warning:42804:7:SQL statement:target type is different type than source type Query: select 'x'::text -- ^ Detail: cast "text" value to "integer" type Hint: The input expression type does not have an assignment cast to the target type. Context: at SQL statement to variable "i" declared on line 3 warning extra:00000:3:DECLARE:never read variable "i" warning extra:00000:4:DECLARE:never read variable "t" warning extra:00000:5:DECLARE:never read variable "n" (9 rows) select * from plpgsql_check_function('as_f9', fatal_errors => false, performance_warnings => true); plpgsql_check_function ----------------------------------------------------------------------------------------- performance:00000:7:SQL statement:detected "SELECT expr INTO variable" Query: select 'x'::text -- ^ Detail: This obsolete syntax of assigning can be slow. Hint: Use syntax "variable := expr" instead. warning:42804:7:SQL statement:target type is different type than source type Query: select 'x'::text -- ^ Detail: cast "text" value to "integer" type Hint: The input expression type does not have an assignment cast to the target type. Context: at SQL statement to variable "i" declared on line 3 performance:00000:8:SQL statement:detected "SELECT expr INTO variable" Query: select 1 -- ^ Detail: This obsolete syntax of assigning can be slow. Hint: Use syntax "variable := expr" instead. performance:42804:8:SQL statement:target type is different type than source type Query: select 1 -- ^ Detail: cast "integer" value to "text" type Hint: Hidden casting can be a performance issue. Context: at SQL statement to variable "t" declared on line 4 performance:00000:9:SQL statement:detected "SELECT expr INTO variable" Query: select 1.5::numeric -- ^ Detail: This obsolete syntax of assigning can be slow. Hint: Use syntax "variable := expr" instead. performance:42804:9:SQL statement:target type is different type than source type Query: select 1.5::numeric -- ^ Detail: cast "numeric" value to "integer" type Hint: Hidden casting can be a performance issue. Context: at SQL statement to variable "i" declared on line 3 performance:00000:10:SQL statement:detected "SELECT expr INTO variable" Query: select 1 -- ^ Detail: This obsolete syntax of assigning can be slow. Hint: Use syntax "variable := expr" instead. performance:42804:10:SQL statement:target type is different type than source type Query: select 1 -- ^ Detail: cast "integer" value to "numeric" type Hint: Hidden casting can be a performance issue. Context: at SQL statement to variable "n" declared on line 5 warning extra:00000:3:DECLARE:never read variable "i" warning extra:00000:4:DECLARE:never read variable "t" warning extra:00000:5:DECLARE:never read variable "n" performance:00000:routine is marked as VOLATILE, should be IMMUTABLE Hint: When you fix this issue, please, recheck other functions that uses this function. (49 rows) -- -- assignment of a query result to a composite variable -- -- a variable of a table rowtype where the table has a dropped column - -- the descriptors are not equal, but the dropped column is skipped on -- both sides alter table as_tab1 drop column b; create function as_f10() returns int as $$ declare r as_tab1; begin select * into r from as_tab1; return r.a; end; $$ language plpgsql; select * from plpgsql_check_function('as_f10'); plpgsql_check_function ------------------------ (0 rows) -- the query returns less columns than the variable has create function as_f11() returns int as $$ declare r as_tab1; begin select a into r from as_tab1; return r.a; end; $$ language plpgsql; select * from plpgsql_check_function('as_f11'); plpgsql_check_function ------------------------------------------------------------------------- warning:00000:4:SQL statement:too few attributes for composite variable Query: select a from as_tab1 -- ^ Context: at SQL statement to variable "r" declared on line 2 (4 rows) -- and more columns than the variable has create function as_f12() returns int as $$ declare r as_tab1; begin select a, c, a from as_tab1 into r; return r.a; end; $$ language plpgsql; select * from plpgsql_check_function('as_f12'); plpgsql_check_function -------------------------------------------------------------------------- warning:00000:4:SQL statement:too many attributes for composite variable Query: select a, c, a from as_tab1 -- ^ Context: at SQL statement to variable "r" declared on line 2 (4 rows) -- the types of the columns are compared one by one create function as_f13() returns int as $$ declare r as_tab1; begin select 'x'::text, 1 into r; return r.a; end; $$ language plpgsql; select * from plpgsql_check_function('as_f13', fatal_errors => false); plpgsql_check_function -------------------------------------------------------------------------------------- warning:42804:4:SQL statement:target type is different type than source type Query: select 'x'::text, 1 -- ^ Detail: cast "text" value to "integer" type Hint: The input expression type does not have an assignment cast to the target type. Context: at SQL statement to variable "r" declared on line 2 (6 rows) -- a whole row value of a type with a dropped column is assigned to a -- record variable create function as_f14() returns int as $$ declare r record; begin select t.* into r from as_tab1 t; return r.a; end; $$ language plpgsql; select * from plpgsql_check_function('as_f14'); plpgsql_check_function ------------------------ (0 rows) -- the NEW and the OLD records of a trigger get the descriptor of the -- triggering relation, which contains the dropped column too create function as_trg1() returns trigger as $$ begin new.a := old.a; new.c := 1; return new; end; $$ language plpgsql; create trigger as_trg1 before update on as_tab1 for each row execute procedure as_trg1(); select * from plpgsql_check_function('as_trg1', relid => 'as_tab1'::regclass); plpgsql_check_function ------------------------ (0 rows) -- FOREACH applies assignment casts to elements and slices, not array input. create function as_f15() returns int as $$ declare item int; total int := 0; begin foreach item in array array[1.6::numeric, 2.4::numeric] loop total := total + item; end loop; return total; end; $$ language plpgsql; select * from plpgsql_check_function('as_f15'); plpgsql_check_function ------------------------ (0 rows) select as_f15(); as_f15 -------- 4 (1 row) create function as_f16() returns int[] as $$ declare item int[]; begin foreach item slice 1 in array array[[1.6::numeric, 2.4::numeric]] loop return item; end loop; return null; end; $$ language plpgsql; select * from plpgsql_check_function('as_f16'); plpgsql_check_function ------------------------ (0 rows) select as_f16(); as_f16 -------- {2,2} (1 row) -- Without an assignment cast, invalid textual input is still rejected. create function as_f17() returns void as $$ declare item int; begin foreach item in array array['not an integer'] loop raise notice '%', item; end loop; end; $$ language plpgsql; select * from plpgsql_check_function('as_f17'); plpgsql_check_function ------------------------------------------------------------------------------------------ error:22P02:4:FOREACH over array:invalid input syntax for type integer: "not an integer" (1 row) drop function as_f15(); drop function as_f16(); drop function as_f17(); drop function as_f1(int); drop function as_f2(); drop function as_f4(); drop function as_f5(); drop function as_f6(); drop function as_f7(); drop function as_f8(); drop function as_f9(); drop function as_f10(); drop function as_f11(); drop function as_f12(); drop function as_f13(); drop function as_f14(); drop trigger as_trg1 on as_tab1; drop function as_trg1(); -- A composite source may have a valid scalar assignment cast, or be NULL. create type as_pair as (a int, b int); create function as_f18() returns text as $$ declare value text; begin select row(1, 2) into value; return value; end; $$ language plpgsql; select * from plpgsql_check_function('as_f18'); plpgsql_check_function ------------------------ (0 rows) select as_f18(); as_f18 -------- (1,2) (1 row) create function as_f19() returns int as $$ declare value int; begin select null::as_tab1 into value; return value; end; $$ language plpgsql; select * from plpgsql_check_function('as_f19'); plpgsql_check_function ------------------------ (0 rows) select as_f19() is null; ?column? ---------- t (1 row) create function as_pair_to_int(p as_pair) returns int as $$ select p.a + p.b $$ language sql immutable; create cast (as_pair as int) with function as_pair_to_int(as_pair) as assignment; create function as_f20() returns int as $$ declare value int; begin select row(1, 2)::as_pair into value; return value; end; $$ language plpgsql; select * from plpgsql_check_function('as_f20'); plpgsql_check_function ------------------------ (0 rows) select as_f20(); as_f20 -------- 3 (1 row) drop function as_f18(); drop function as_f19(); drop function as_f20(); drop cast (as_pair as int); drop function as_pair_to_int(as_pair); drop type as_pair; -- RETURN QUERY retains composite-valued columns; RETURN expands a row value. create type as_leaf as (n int); create type as_wrapper as (payload as_leaf); create function as_f21() returns setof as_wrapper as $$ begin return query select row(7)::as_leaf; end; $$ language plpgsql; select * from plpgsql_check_function('as_f21'); plpgsql_check_function ------------------------ (0 rows) select (payload).n from as_f21(); n --- 7 (1 row) create function as_f22() returns setof as_wrapper as $$ begin return query execute 'select row(7)::as_leaf'; end; $$ language plpgsql; select * from plpgsql_check_function('as_f22'); plpgsql_check_function ------------------------ (0 rows) select (payload).n from as_f22(); n --- 7 (1 row) create function as_f23() returns setof as_leaf as $$ begin return query select row(7)::as_leaf; end; $$ language plpgsql; select * from plpgsql_check_function('as_f23'); plpgsql_check_function ----------------------------------------------------------------------------------- error:42804:3:RETURN QUERY:structure of query does not match function result type Detail: Returned type as_leaf does not match expected type integer in column 1. (2 rows) select * from as_f23(); ERROR: structure of query does not match function result type DETAIL: Returned type as_leaf does not match expected type integer in column 1. CONTEXT: SQL statement "select row(7)::as_leaf" PL/pgSQL function as_f23() line 3 at RETURN QUERY create function as_f24() returns as_leaf as $$ begin return row(7)::as_leaf; end; $$ language plpgsql; select * from plpgsql_check_function('as_f24'); plpgsql_check_function ------------------------ (0 rows) select (as_f24()).n; n --- 7 (1 row) create function as_f25() returns setof as_leaf as $$ begin return next row(7)::as_leaf; return; end; $$ language plpgsql; select * from plpgsql_check_function('as_f25'); plpgsql_check_function ------------------------ (0 rows) select * from as_f25(); n --- 7 (1 row) -- Bounds and the RHS read their variables; the implicit old array does not. create function as_f26() returns void as $$ declare element_array int[] := array[1, 2]; lower_array int[] := array[1, 2]; upper_array int[] := array[1, 2]; rhs_array int[] := array[1, 2]; fetch_array int[] := array[1, 2]; write_only int[] := array[1, 2]; begin element_array[element_array[1]] := 3; lower_array[lower_array[1]:2] := array[3, 4]; upper_array[1:upper_array[2]] := array[3, 4]; rhs_array[1] := rhs_array[2]; fetch_array := fetch_array[1:2]; write_only[1] := 3; end; $$ language plpgsql; select * from plpgsql_check_function('as_f26'); plpgsql_check_function ---------------------------------------------------------------- warning extra:00000:8:DECLARE:never read variable "write_only" (1 row) select as_f26(); as_f26 -------- (1 row) drop function as_f26(); drop function as_f21(); drop function as_f22(); drop function as_f23(); drop function as_f24(); drop function as_f25(); drop type as_wrapper; drop type as_leaf; drop table as_tab1; create type point2 as (x int, y int); create domain dpoint as point2 check ((value).x is not null); -- a) domain-over-composite as a parameter create function dom_param(p dpoint) returns int language plpgsql as $$ begin return p.x; end; $$; -- b) domain-over-composite as an initialized local variable create function dom_var() returns int language plpgsql as $$ declare r dpoint := row(1, 2)::dpoint; begin return r.x; end; $$; select count(*) = 0 from plpgsql_check_function_tb('dom_param(dpoint)', fatal_errors => false) where message like '%violates check constraint%'; ?column? ---------- t (1 row) select count(*) = 0 from plpgsql_check_function_tb('dom_var()', fatal_errors => false) where message like '%violates check constraint%'; ?column? ---------- t (1 row) drop function dom_var; drop function dom_param; drop type point2 cascade; NOTICE: drop cascades to type dpoint create function foreach_arrvar() returns int language plpgsql as $$ declare item int[]; total int := 0; begin foreach item in array '{"{1,2}","{3}"}'::text[] loop total := total + cardinality(item); end loop; return total; end; $$; select message from plpgsql_check_function_tb('foreach_arrvar()', fatal_errors => false); message ------------------------------------------------ target type is different type than source type (1 row) drop function foreach_arrvar(); create function stale_ctx() returns int language plpgsql as $$ declare r record; unused_v int; -- produces an "unused variable" warning begin r.a := 10; -- fails: record "r" is not assigned yet; sets err_text return 1; end; $$; select * from plpgsql_check_function('stale_ctx', fatal_errors => false); plpgsql_check_function ------------------------------------------------------------------------ error:55000:6:assignment:record "r" is not assigned to tuple structure Context: at assignment to field "a" of variable "r" declared on line 3 warning:00000:4:DECLARE:unused variable "unused_v" warning extra:00000:3:DECLARE:never read variable "r" (4 rows) drop function stale_ctx(); create table repro_foreach_tab(a int, b int); -- the target of FOREACH ARRAY holds an element of the array, so the array -- expression must not be traced as a constant value of the target variable create or replace function repro_foreach() returns void as $$ declare cols text[] := array['a', 'b']; c text; r int; begin foreach c in array cols loop execute format('select %I from repro_foreach_tab', c) into r; end loop; end $$ language plpgsql; select * from plpgsql_check_function('repro_foreach()'); plpgsql_check_function ------------------------------------------------------- warning extra:00000:5:DECLARE:never read variable "r" (1 row) -- with SLICE the target holds an array, and tracing of a composite value -- is not supported anyway create or replace function repro_foreach_slice() returns void as $$ declare cols text[] := array[['a'], ['b']]; c text[]; r int; begin foreach c slice 1 in array cols loop execute format('select %I from repro_foreach_tab', c[1]) into r; end loop; end $$ language plpgsql; select * from plpgsql_check_function('repro_foreach_slice()'); plpgsql_check_function ------------------------------------------------------- warning extra:00000:5:DECLARE:never read variable "r" (1 row) -- plain assignment is still traced create or replace function repro_foreach_const() returns void as $$ declare c text := 'unknown_column'; r int; begin execute format('select %I from repro_foreach_tab', c) into r; end $$ language plpgsql; select * from plpgsql_check_function('repro_foreach_const()'); plpgsql_check_function -------------------------------------------------------------- error:42703:6:EXECUTE:column "unknown_column" does not exist Query: select unknown_column from repro_foreach_tab -- ^ (3 rows) drop function repro_foreach(); drop function repro_foreach_slice(); drop function repro_foreach_const(); drop table repro_foreach_tab; okbob-plpgsql_check-800db79/expected/plpgsql_check_catalog.out000066400000000000000000000105531526744060500246420ustar00rootroot00000000000000set client_min_messages to warning; create extension if not exists plpgsql_check; set client_min_messages to notice; -- -- checks done before the function is checked -- -- the checked routine must be written in plpgsql select * from plpgsql_check_function('upper(text)'); ERROR: upper(text) is not a plpgsql function select * from plpgsql_check_function('sum(int)'); ERROR: sum(integer) is not a plpgsql function -- a routine with a pseudotype result is rejected sooner, the result type -- is read before the language of the routine is validated select * from plpgsql_check_function('textout(text)'); ERROR: PL/pgSQL functions cannot return type cstring -- record, void and the polymorphic types are pseudotypes too, but they -- are allowed create function cat_f1() returns void as $$ begin end; $$ language plpgsql; create function cat_f2(a anyelement) returns anyelement as $$ begin return a; end; $$ language plpgsql; create function cat_f3() returns record as $$ declare r record; begin select 1 as a, 2 as b into r; return r; end; $$ language plpgsql; select * from plpgsql_check_function('cat_f1'); plpgsql_check_function ------------------------ (0 rows) select * from plpgsql_check_function('cat_f2(anyelement)'); plpgsql_check_function ------------------------ (0 rows) select * from plpgsql_check_function('cat_f3'); plpgsql_check_function ------------------------ (0 rows) -- -- the relation of a trigger -- create table cat_tab1(a int, b int); create function cat_trg1() returns trigger as $$ begin return new; end; $$ language plpgsql; create function cat_evtrg1() returns event_trigger as $$ begin end; $$ language plpgsql; -- a dml trigger cannot be checked without the triggering relation select * from plpgsql_check_function('cat_trg1'); ERROR: missing trigger relation HINT: Trigger relation oid must be valid select * from plpgsql_check_function('cat_trg1', relid => 'cat_tab1'::regclass); plpgsql_check_function ------------------------ (0 rows) -- an event trigger has no triggering relation select * from plpgsql_check_function('cat_evtrg1', relid => 'cat_tab1'::regclass); ERROR: function is not trigger HINT: Trigger relation oid must not be valid for non dml trigger function. select * from plpgsql_check_function('cat_evtrg1'); plpgsql_check_function ------------------------ (0 rows) -- a plain function has no triggering relation either select * from plpgsql_check_function('cat_f1', relid => 'cat_tab1'::regclass); ERROR: function is not trigger HINT: Trigger relation oid must not be valid for non dml trigger function. -- the profiler does not check the triggering relation, it only reads the -- collected statistics select * from plpgsql_profiler_function_tb('cat_trg1'); lineno | stmt_lineno | queryids | cmds_on_row | exec_stmts | exec_stmts_err | total_time | avg_time | max_time | processed_rows | source --------+-------------+----------+-------------+------------+----------------+------------+----------+----------+----------------+--------------- 1 | | | | | | | | | | 2 | 2 | | 1 | | | | | | | begin 3 | 3 | | 1 | | | | | | | return new; 4 | | | | | | | | | | end; (4 rows) -- -- the pragma function is searched by name, and only the function from the -- schema of the extension is used -- create schema cat_ns; create function cat_ns.plpgsql_check_pragma(int) returns int as $$ select 1; $$ language sql; create function cat_f4() returns int as $$ declare r record; begin perform plpgsql_check_pragma('type: r (a int)'); return r.a; end; $$ language plpgsql; select * from plpgsql_check_function('cat_f4'); plpgsql_check_function ------------------------ (0 rows) drop function cat_ns.plpgsql_check_pragma(int); drop schema cat_ns; drop function cat_f1(); drop function cat_f2(anyelement); drop function cat_f3(); drop function cat_f4(); drop function cat_evtrg1(); drop trigger if exists cat_trg1 on cat_tab1; NOTICE: trigger "cat_trg1" for relation "cat_tab1" does not exist, skipping drop function cat_trg1(); drop table cat_tab1; okbob-plpgsql_check-800db79/expected/plpgsql_check_check_function.out000066400000000000000000000233721526744060500262150ustar00rootroot00000000000000set client_min_messages to warning; create extension if not exists plpgsql_check; set client_min_messages to notice; -- -- the polymorphic arguments are replaced by the types passed as the -- arguments of the check function -- create function cf_f1(a anynonarray) returns text as $$ begin return a::text; end; $$ language plpgsql; -- anynonarray uses the anyelememttype option (the name of the argument -- has a typo), which must not be an array select * from plpgsql_check_function('cf_f1(anynonarray)'); plpgsql_check_function ------------------------ (0 rows) select * from plpgsql_check_function('cf_f1(anynonarray)'::regprocedure, anyelememttype => 'date'); plpgsql_check_function ------------------------ (0 rows) select * from plpgsql_check_function('cf_f1(anynonarray)'::regprocedure, anyelememttype => 'int[]'); ERROR: anyelement type is a array (expected nonarray) create type cf_enum as enum ('a', 'b'); create function cf_f2(a anyenum) returns text as $$ begin return a::text; end; $$ language plpgsql; -- anyenum requires the anyenumtype option, and the passed type must be -- an enum select * from plpgsql_check_function('cf_f2(anyenum)'); ERROR: anyenumtype option should be specified (anyenum type is used) select * from plpgsql_check_function('cf_f2(anyenum)'::regprocedure, anyenumtype => 'int'); ERROR: type specified by anyenumtype option is not enum select * from plpgsql_check_function('cf_f2(anyenum)'::regprocedure, anyenumtype => 'cf_enum'); plpgsql_check_function ------------------------ (0 rows) create function cf_f3(a anycompatiblenonarray, b anycompatible) returns text as $$ begin return a::text || b::text; end; $$ language plpgsql; -- anycompatiblenonarray uses the anycompatibletype option, which must -- not be an array select * from plpgsql_check_function('cf_f3(anycompatiblenonarray,anycompatible)'); plpgsql_check_function ------------------------ (0 rows) select * from plpgsql_check_function('cf_f3(anycompatiblenonarray,anycompatible)'::regprocedure, anycompatibletype => 'int[]'); ERROR: anycompatible type is a array (expected nonarray) create function cf_f4(a anymultirange) returns int as $$ begin return 1; end; $$ language plpgsql; -- anymultirange is derived from the anyrangetype option select * from plpgsql_check_function('cf_f4(anymultirange)'); plpgsql_check_function ------------------------------------------ warning extra:00000:unused parameter "a" (1 row) create function cf_multirange(a anymultirange) returns anyrange as $$ begin return range_merge(a); end; $$ language plpgsql; select cf_multirange('{[1,3)}'::int4multirange); cf_multirange --------------- [1,3) (1 row) select * from plpgsql_check_function('cf_multirange'); plpgsql_check_function ------------------------ (0 rows) select * from plpgsql_check_function('cf_multirange', anyrangetype => 'numrange'); plpgsql_check_function ------------------------ (0 rows) select * from plpgsql_check_function('cf_multirange', anyrangetype => 'int'); ERROR: type specified by anyrangetype option is not range create function cf_multirange_result(a anyrange) returns anymultirange as $$ begin return multirange(a); end; $$ language plpgsql; select * from plpgsql_check_function('cf_multirange_result'); plpgsql_check_function ------------------------ (0 rows) select * from plpgsql_check_function('cf_multirange_result', anyrangetype => 'numrange'); plpgsql_check_function ------------------------ (0 rows) -- Some supported core PL/pgSQL validators reject anycompatiblemultirange. set check_function_bodies = off; create function cf_compatible_multirange(a anycompatiblemultirange) returns anycompatiblerange as $$ begin return range_merge(a); end; $$ language plpgsql; create function cf_compatible_multirange_result(a anycompatiblerange) returns anycompatiblemultirange as $$ begin return multirange(a); end; $$ language plpgsql; reset check_function_bodies; select cf_compatible_multirange('{[1,3)}'::int4multirange); cf_compatible_multirange -------------------------- [1,3) (1 row) select * from plpgsql_check_function('cf_compatible_multirange'); plpgsql_check_function ------------------------ (0 rows) select * from plpgsql_check_function('cf_compatible_multirange', anycompatiblerangetype => 'numrange'); plpgsql_check_function ------------------------ (0 rows) select * from plpgsql_check_function('cf_compatible_multirange', anycompatiblerangetype => 'int'); ERROR: type specified by anycompatiblerangetype option is not range select * from plpgsql_check_function('cf_compatible_multirange_result'); plpgsql_check_function ------------------------ (0 rows) select * from plpgsql_check_function('cf_compatible_multirange_result', anycompatiblerangetype => 'numrange'); plpgsql_check_function ------------------------ (0 rows) drop function cf_multirange(anymultirange); drop function cf_multirange_result(anyrange); drop function cf_compatible_multirange(anycompatiblemultirange); drop function cf_compatible_multirange_result(anycompatiblerange); -- when some argument is not an input argument, then the modes of all the -- arguments are read from the catalog create function cf_f5(a anyelement, out b anyelement, out c int) as $$ begin b := a; c := 1; end; $$ language plpgsql; -- the result descriptor is built from the output arguments, and it cannot -- be used when some of them is polymorphic select * from plpgsql_check_function('cf_f5(anyelement)'); plpgsql_check_function ------------------------ (0 rows) create function cf_f6(variadic a anyarray) returns int as $$ begin return array_length(a, 1); end; $$ language plpgsql; -- a variadic polymorphic argument is replaced by an array of the passed -- type select * from plpgsql_check_function('cf_f6(anyarray)'); plpgsql_check_function ------------------------ (0 rows) -- -- the dependencies on the rowtypes of the relations -- create table cf_tab1(a int, b int); create view cf_view1 as select * from cf_tab1; create materialized view cf_matview1 as select * from cf_tab1; create type cf_type1 as (a int, b int); create function cf_f7() returns int as $$ declare r1 cf_tab1%rowtype; r2 cf_view1%rowtype; r3 cf_matview1%rowtype; r4 cf_type1; begin r1.a := 1; r2.a := 1; r3.a := 1; r4.a := 1; return r1.a + r2.a + r3.a + r4.a; end; $$ language plpgsql; select type, schema, name from plpgsql_show_dependency_tb('cf_f7') order by 1, 2, 3; type | schema | name ----------+--------+------------- RELATION | public | cf_matview1 RELATION | public | cf_tab1 RELATION | public | cf_view1 TYPE | public | cf_type1 (4 rows) -- -- the name of an argument should not be a reserved keyword -- create function cf_f8("table" int, "left" int) returns int as $$ begin return 1; end; $$ language plpgsql; select * from plpgsql_check_function('cf_f8(int,int)'); plpgsql_check_function ------------------------------------------------------------- warning:00000:name of parameter "table" is reserved keyword Detail: The reserved keyword was used as parameter name. warning:00000:name of parameter "left" is reserved keyword Detail: The reserved keyword was used as parameter name. warning extra:00000:unused parameter "table" warning extra:00000:unused parameter "left" (6 rows) -- -- the configuration of a routine is set before the check and it is -- restored when the check is done -- set datestyle to 'ISO, DMY'; create function cf_f9() returns text as $$ begin return current_setting('datestyle'); end; $$ language plpgsql set datestyle to 'ISO, MDY'; select * from plpgsql_check_function('cf_f9'); plpgsql_check_function ------------------------ (0 rows) show datestyle; DateStyle ----------- ISO, DMY (1 row) reset datestyle; -- -- the check can be disabled -- set plpgsql_check.mode = 'disabled'; select * from plpgsql_check_function('cf_f7'); NOTICE: plpgsql_check is disabled plpgsql_check_function ------------------------ (0 rows) set plpgsql_check.mode = 'by_function'; -- -- the passive check is started when the checked routine is executed -- set plpgsql_check.mode = 'every_start'; -- a procedure and a routine which returns void are not required to be -- closed by a RETURN statement create procedure cf_p1() as $$ begin raise notice 'cf_p1'; end; $$ language plpgsql; create function cf_f10() returns void as $$ begin raise notice 'cf_f10'; end; $$ language plpgsql; call cf_p1(); NOTICE: cf_p1 select cf_f10(); NOTICE: cf_f10 cf_f10 -------- (1 row) -- but a routine which returns a value is, although the missing RETURN -- can be reported as an extra warning only create function cf_f11() returns int as $$ begin if random() < -1 then return 1; end if; end; $$ language plpgsql; create function cf_f12() returns int as $$ begin raise notice 'cf_f12'; end; $$ language plpgsql; select cf_f11(); ERROR: control reached end of function without RETURN CONTEXT: PL/pgSQL function cf_f11() select cf_f12(); ERROR: control reached end of function without RETURN CONTEXT: PL/pgSQL function cf_f12() -- in the fresh_start mode every routine is checked only once set plpgsql_check.mode = 'fresh_start'; select cf_f10(); NOTICE: cf_f10 cf_f10 -------- (1 row) select cf_f10(); NOTICE: cf_f10 cf_f10 -------- (1 row) set plpgsql_check.mode = 'by_function'; drop function cf_f1(anynonarray); drop function cf_f2(anyenum); drop function cf_f3(anycompatiblenonarray,anycompatible); drop function cf_f4(anymultirange); drop function cf_f5(anyelement); drop function cf_f6(anyarray); drop function cf_f7(); drop function cf_f8(int,int); drop function cf_f9(); drop function cf_f10(); drop function cf_f11(); drop function cf_f12(); drop procedure cf_p1(); drop type cf_enum; drop type cf_type1; drop materialized view cf_matview1; drop view cf_view1; drop table cf_tab1; okbob-plpgsql_check-800db79/expected/plpgsql_check_comment_options.out000066400000000000000000000716261526744060500264550ustar00rootroot00000000000000-- -- regress tests of the in-comment options parser (src/parser.c) -- -- plpgsql_check reads options from the source code of the checked -- function, from lines tagged by "@plpgsql_check_options:" inside line -- comments or block comments. Nothing of that parser was covered by the -- regress tests. -- load 'plpgsql_check'; set client_min_messages to warning; create extension if not exists plpgsql_check; set client_min_messages to notice; -- Some errors reported by the options parser contain the oid of the -- checked function. This wrapper replaces the oid, so the output is -- stable. create function co_check(fnname text, rel regclass default 0) returns setof text as $$ begin return query select * from plpgsql_check_function(fnname, rel); exception when others then return next 'ERROR: ' || regexp_replace(sqlerrm, 'fnoid: [0-9]+', 'fnoid: NNN'); end; $$ language plpgsql; -- -- boolean options -- -- an option with no value is true create function co_bool1() returns void as $$ -- @plpgsql_check_options: extra_warnings declare x int; begin x := 1; end; $$ language plpgsql; select * from co_check('co_bool1'); co_check ------------------------------------------------------- warning extra:00000:3:DECLARE:never read variable "x" (1 row) -- the value can be written with or without the equal sign, and all the -- accepted spellings of true and false are used here create function co_bool2() returns void as $$ -- @plpgsql_check_options: extra_warnings = true, other_warnings = yes -- @plpgsql_check_options: performance_warnings t, security_warnings on declare x int; begin x := 1; end; $$ language plpgsql; select * from co_check('co_bool2'); co_check ----------------------------------------------------------------------------------------- warning extra:00000:4:DECLARE:never read variable "x" performance:00000:routine is marked as VOLATILE, should be IMMUTABLE Hint: When you fix this issue, please, recheck other functions that uses this function. (3 rows) create function co_bool3() returns void as $$ -- @plpgsql_check_options: extra_warnings = false, other_warnings = no -- @plpgsql_check_options: performance_warnings f, security_warnings off -- @plpgsql_check_options: compatibility_warnings = off, fatal_errors = off declare x int; begin x := 1; end; $$ language plpgsql; select * from co_check('co_bool3'); co_check ---------- (0 rows) -- an option which is directly followed by a comma keeps its true value create function co_bool4() returns void as $$ -- @plpgsql_check_options: extra_warnings, other_warnings declare x int; begin x := 1; end; $$ language plpgsql; select * from co_check('co_bool4'); co_check ------------------------------------------------------- warning extra:00000:3:DECLARE:never read variable "x" (1 row) -- all_warnings and without_warnings are handled at the very end of the -- parsing, and they cannot be used together create function co_all_warnings() returns void as $$ -- @plpgsql_check_options: all_warnings declare x int; begin x := 1; end; $$ language plpgsql; select * from co_check('co_all_warnings'); co_check ----------------------------------------------------------------------------------------- warning extra:00000:3:DECLARE:never read variable "x" performance:00000:routine is marked as VOLATILE, should be IMMUTABLE Hint: When you fix this issue, please, recheck other functions that uses this function. (3 rows) create function co_without_warnings() returns void as $$ -- @plpgsql_check_options: without_warnings declare x int; begin x := 1; end; $$ language plpgsql; select * from co_check('co_without_warnings'); co_check ---------- (0 rows) create function co_warnings_conflict() returns void as $$ -- @plpgsql_check_options: all_warnings = on, without_warnings = on begin end; $$ language plpgsql; select * from co_check('co_warnings_conflict'); co_check ------------------------------------------------------------------------------- ERROR: all_warnings and without_warnings cannot be used together (fnoid: NNN) (1 row) -- the options can be disabled by the use_incomment_options argument select * from plpgsql_check_function('co_warnings_conflict', use_incomment_options := false); plpgsql_check_function ------------------------ (0 rows) -- -- comment forms -- -- a block comment, and more than one tagged line in a single block -- comment create function co_blockcomment() returns void as $$ /* * some description of the function * @plpgsql_check_options: extra_warnings = off * @plpgsql_check_options: other_warnings = off */ declare x int; begin x := 1; end; $$ language plpgsql; select * from co_check('co_blockcomment'); co_check ---------- (0 rows) -- tags before and after nested comments belong to the same outer comment create function co_nested_comment() returns void as $$ /* * @plpgsql_check_options: extra_warnings = off /* inner description /* deeper description */ */ * @plpgsql_check_options: other_warnings = off */ declare x int; y int; begin x := 1; end; $$ language plpgsql; select * from co_check('co_nested_comment'); co_check ---------- (0 rows) select count(*) = 2 as warnings_without_options from plpgsql_check_function_tb('co_nested_comment', use_incomment_options => false); warnings_without_options -------------------------- t (1 row) drop function co_nested_comment(); -- an inner terminator is not part of the tagged option on its line create function co_nested_inline_option() returns void as $$ /* /* @plpgsql_check_options: without_warnings */ */ declare x int; begin null; end; $$ language plpgsql; select * from co_check('co_nested_inline_option'); co_check ---------- (0 rows) select count(*) = 1 as warning_without_options from plpgsql_check_function_tb('co_nested_inline_option', use_incomment_options => false); warning_without_options ------------------------- t (1 row) drop function co_nested_inline_option(); -- The source code is scanned for the tag outside of the string -- literals, the quoted identifiers and the dollar quoted strings, so the -- tags written there are not options - if they were parsed, the check -- would fail on an unsupported option. The only real tag is the one on -- the last line, and it turns the extra warnings off; the same function -- checked with use_incomment_options = false reports them. create function co_skipping() returns void as $func$ declare s1 text = 'a string with -- @plpgsql_check_options: nonsense and '' inside'; s2 text = $tag$dollar quoted -- @plpgsql_check_options: nonsense$tag$; s3 text = $$another dollar quoted -- @plpgsql_check_options: nonsense$$; "quoted ""x"" -- @plpgsql_check_options: nonsense" int = 1; x int; begin x := 1; -- some note @plpgsql_check_options: extra_warnings = off raise notice '%', coalesce(s1, s2, s3); end; $func$ language plpgsql; select * from co_check('co_skipping'); co_check ---------- (0 rows) select * from plpgsql_check_function('co_skipping', use_incomment_options := false); plpgsql_check_function ---------------------------------------------------------------------------------------------------- warning extra:00000:6:DECLARE:never read variable "quoted "x" -- @plpgsql_check_options: nonsense" warning extra:00000:7:DECLARE:never read variable "x" (2 rows) -- escape strings honor backslashes, but literal contents are never options set standard_conforming_strings = on; create function co_escape_string() returns text as $func$ declare s1 text := E'can\'t -- @plpgsql_check_options: nonsense '; s2 text := e'won\'t'; x int; begin -- @plpgsql_check_options: without_warnings return s1 || s2; end; $func$ language plpgsql; select * from co_check('co_escape_string'); co_check ---------- (0 rows) select count(*) = 1 as unused_without_options from plpgsql_check_function_tb('co_escape_string', use_incomment_options => false) where message = 'unused variable "x"'; unused_without_options ------------------------ t (1 row) -- the last letter of a type name is not an escape-string prefix create function co_standard_string() returns text as $func$ declare s name := name'backslash\'; --' x int; begin -- @plpgsql_check_options: without_warnings return s::text; end; $func$ language plpgsql; select * from co_check('co_standard_string'); co_check ---------- (0 rows) drop function co_escape_string(); drop function co_standard_string(); drop function co_legacy_string(); ERROR: function co_legacy_string() does not exist -- positional parameters and identifiers do not open dollar-quoted strings create function co_dollar_parameters(int, int) returns int as $func$ declare x int; begin return $1+$2; -- @plpgsql_check_options: without_warnings end; $func$ language plpgsql; select * from co_check('co_dollar_parameters'); co_check ---------- (0 rows) create function co_dollar_identifier() returns int as $func$ declare v$x$part int := 1; x int; begin -- @plpgsql_check_options: without_warnings return v$x$part; end; $func$ language plpgsql; select * from co_check('co_dollar_identifier'); co_check ---------- (0 rows) -- genuine tags can contain digits, are case sensitive, and hide literal options create function co_dollar_literal() returns text as $func$ declare s text := $Tag_1$other delimiters $$ and $tag_1$ -- @plpgsql_check_options: nonsense $Tag_1$; x int; begin -- @plpgsql_check_options: without_warnings return s; end; $func$ language plpgsql; select * from co_check('co_dollar_literal'); co_check ---------- (0 rows) drop function co_dollar_parameters(int, int); drop function co_dollar_identifier(); drop function co_dollar_literal(); -- end of source terminates a line comment just like a newline create function co_final_comment() returns void as $func$declare x int; begin null; end; -- @plpgsql_check_options: without_warnings$func$ language plpgsql; select * from co_check('co_final_comment'); co_check ---------- (0 rows) select count(*) = 1 as unused_without_options from plpgsql_check_function_tb('co_final_comment', use_incomment_options => false) where message = 'unused variable "x"'; unused_without_options ------------------------ t (1 row) create function co_final_newline() returns void as $func$declare x int; begin null; end; -- @plpgsql_check_options: without_warnings $func$ language plpgsql; select * from co_check('co_final_newline'); co_check ---------- (0 rows) create function co_final_empty() returns void as $func$begin null; end; --$func$ language plpgsql; select * from co_check('co_final_empty'); co_check ---------- (0 rows) drop function co_final_comment(); drop function co_final_newline(); drop function co_final_empty(); -- -- name options -- -- the argument of a name option can be an identifier, a quoted -- identifier or a string. The names of the transition tables of a -- trigger function are usually passed as arguments of -- plpgsql_check_function(), the options replace them. create table co_trig_tab(a int, b int); create table co_trig_tab2(c int, d int); create function co_newtable() returns trigger as $$ -- @plpgsql_check_options: newtable = co_new begin perform count(*) from co_new; return null; end; $$ language plpgsql; select * from co_check('co_newtable', 'co_trig_tab'); co_check ---------- (0 rows) create function co_oldtable() returns trigger as $$ -- @plpgsql_check_options: oldtable "co_old", newtable = 'co_new' begin perform count(*) from co_old; perform count(*) from co_new; return null; end; $$ language plpgsql; select * from co_check('co_oldtable', 'co_trig_tab'); co_check ---------- (0 rows) -- -- table option -- -- relid is used for the resolution of the NEW and OLD records of a -- trigger function. The relation given by the option replaces the -- relation passed as an argument, so the check passes although the -- fields used by the function do not exist in co_trig_tab. create function co_relid() returns trigger as $$ -- @plpgsql_check_options: relid = co_trig_tab2 begin if new.c > 10 then new.d := new.c; end if; return new; end; $$ language plpgsql; select * from co_check('co_relid', 'co_trig_tab'); co_check ---------- (0 rows) -- Validate the final options, so a comment can supply a required relid. select * from co_check('co_relid'); co_check ---------- (0 rows) select * from plpgsql_check_function_tb('co_relid()'::regprocedure); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+---------+--------+------+-------+----------+-------+--------- (0 rows) select * from plpgsql_check_function('co_relid', newtable => 'co_transition'); plpgsql_check_function ------------------------ (0 rows) select * from plpgsql_make_pragma('co_relid()'); plpgsql_make_pragma --------------------- (0 rows) -- Conversely, a comment cannot attach trigger metadata to a nontrigger. create function co_relid_invalid() returns int as $$ -- @plpgsql_check_options: relid = co_trig_tab begin return 1; end; $$ language plpgsql; select * from plpgsql_check_function('co_relid_invalid'); ERROR: function is not trigger HINT: Trigger relation oid must not be valid for non dml trigger function. select * from plpgsql_check_function('co_relid_invalid()'::regprocedure); ERROR: function is not trigger HINT: Trigger relation oid must not be valid for non dml trigger function. select * from plpgsql_check_function_tb('co_relid_invalid'); ERROR: function is not trigger HINT: Trigger relation oid must not be valid for non dml trigger function. select * from plpgsql_check_function_tb('co_relid_invalid()'::regprocedure); ERROR: function is not trigger HINT: Trigger relation oid must not be valid for non dml trigger function. select * from plpgsql_make_pragma('co_relid_invalid()'); ERROR: function is not trigger HINT: Trigger relation oid must not be valid for non dml trigger function. select * from plpgsql_check_function('co_relid_invalid', use_incomment_options => false); plpgsql_check_function ------------------------ (0 rows) select * from plpgsql_check_function_tb('co_relid_invalid', use_incomment_options => false); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+---------+--------+------+-------+----------+-------+--------- (0 rows) drop function co_relid_invalid(); create function co_transition_invalid() returns int as $$ -- @plpgsql_check_options: newtable = co_transition begin return 1; end; $$ language plpgsql; select * from plpgsql_check_function('co_transition_invalid'); ERROR: missing description of oldtable or newtable HINT: Parameter relid is a empty. select * from plpgsql_check_function_tb('co_transition_invalid'); ERROR: missing description of oldtable or newtable HINT: Parameter relid is a empty. select * from plpgsql_make_pragma('co_transition_invalid()'); ERROR: missing description of oldtable or newtable HINT: Parameter relid is a empty. drop function co_transition_invalid(); -- the value can be a qualified name, with quoted parts create function co_relid_qualified() returns trigger as $$ -- @plpgsql_check_options: relid = public."co_trig_tab2" begin new.d := new.c; return new; end; $$ language plpgsql; select * from co_check('co_relid_qualified', 'co_trig_tab'); co_check ---------- (0 rows) -- -- type options -- -- the polymorphic types are substituted by the types given by the -- options, and a qualified type name is used too create function co_anyelement(anyelement) returns text as $$ -- @plpgsql_check_options: anyelementtype = text begin return $1; end; $$ language plpgsql; select * from co_check('co_anyelement'); co_check ---------- (0 rows) create function co_anyelement2(anyelement) returns text as $$ -- @plpgsql_check_options: anyelementtype pg_catalog.text begin return $1; end; $$ language plpgsql; select * from co_check('co_anyelement2'); co_check ---------- (0 rows) create type co_enum as enum ('a', 'b'); create function co_anyenum(anyenum) returns text as $$ -- @plpgsql_check_options: anyenumtype = co_enum begin return $1::text; end; $$ language plpgsql; select * from co_check('co_anyenum'); co_check ---------- (0 rows) create function co_anyrange(anyrange) returns text as $$ -- @plpgsql_check_options: anyrangetype = numrange begin return lower($1)::text; end; $$ language plpgsql; select * from co_check('co_anyrange'); co_check ---------- (0 rows) create function co_anycompatible(anycompatible) returns text as $$ -- @plpgsql_check_options: anycompatibletype = text begin return $1; end; $$ language plpgsql; select * from co_check('co_anycompatible'); co_check ---------- (0 rows) create function co_anycompatiblerange(anycompatiblerange) returns text as $$ -- @plpgsql_check_options: anycompatiblerangetype = numrange begin return lower($1)::text; end; $$ language plpgsql; select * from co_check('co_anycompatiblerange'); co_check ---------- (0 rows) -- complete type syntax, including separators inside modifiers and between options create function co_type_array(anyelement) returns text as $$ -- @plpgsql_check_options: anyelementtype = "pg_catalog"."int4"[][], extra_warnings = off begin return $1::text; end; $$ language plpgsql; select * from co_check('co_type_array'); co_check ---------- (0 rows) create function co_type_datetime(anyelement) returns text as $$ -- @plpgsql_check_options: anyelementtype = timestamp(3) without time zone begin return $1::text; end; $$ language plpgsql; select * from co_check('co_type_datetime'); co_check ---------- (0 rows) create function co_type_multiword(anycompatible) returns text as $$ -- @plpgsql_check_options: anycompatibletype = double precision, extra_warnings = off begin return $1::text; end; $$ language plpgsql; select * from co_check('co_type_multiword'); co_check ---------- (0 rows) create function co_type_numeric(anyelement) returns text as $$ -- @plpgsql_check_options: anyelementtype = numeric(8,2), extra_warnings = off begin return $1::text; end; $$ language plpgsql; select * from co_check('co_type_numeric'); co_check ---------- (0 rows) -- -- echo option -- -- the argument of echo can be of any token type, and the @@name and -- @@signature placeholders are substituted create function co_echo(a int) returns void as $$ -- @plpgsql_check_options: echo = 'a string with @@name' -- @plpgsql_check_options: echo = 'a signature @@signature' -- @plpgsql_check_options: echo = '@@unknown is not a placeholder' -- @plpgsql_check_options: echo an_identifier -- @plpgsql_check_options: echo = "a quoted identifier" -- @plpgsql_check_options: echo = 10.1 -- @plpgsql_check_options: echo = * begin end; $$ language plpgsql; select * from co_check('co_echo'); NOTICE: comment option "echo" is 'a string with co_echo' NOTICE: comment option "echo" is 'a signature co_echo(integer)' NOTICE: comment option "echo" is '@@unknown is not a placeholder' NOTICE: comment option "echo" is an_identifier NOTICE: comment option "echo" is "a quoted identifier" NOTICE: comment option "echo" is 10.1 NOTICE: comment option "echo" is '*' co_check ------------------------------------------ warning extra:00000:unused parameter "a" (1 row) -- -- syntax errors -- create function co_err_unknown() returns void as $$ -- @plpgsql_check_options: unknown_option = 1 begin end; $$ language plpgsql; select * from co_check('co_err_unknown'); co_check ----------------------------------------------------------------------------------------------- ERROR: unsupported option "unknown_option" specified by "@plpgsql_check_options" (fnoid: NNN) (1 row) create function co_err_nooption() returns void as $$ -- @plpgsql_check_options: = 1 begin end; $$ language plpgsql; select * from co_check('co_err_nooption'); co_check --------------------------------------------------------- ERROR: Syntax error (fnoid: NNN) (expected option name) (1 row) create function co_err_nocomma() returns void as $$ -- @plpgsql_check_options: extra_warnings = on other_warnings = on begin end; $$ language plpgsql; select * from co_check('co_err_nocomma'); co_check ----------------------------------------------------------------------------------------------- ERROR: expected "," or end of line on line with "@plpgsql_check_options" options (fnoid: NNN) (1 row) create function co_err_badbool() returns void as $$ -- @plpgsql_check_options: extra_warnings = maybe begin end; $$ language plpgsql; select * from co_check('co_err_badbool'); co_check ---------------------------------------------------------------------------------------------- ERROR: syntax error in comment option "extra_warnings" (fnoid: NNN) (expected boolean value) (1 row) create function co_err_nobool() returns void as $$ -- @plpgsql_check_options: extra_warnings = begin end; $$ language plpgsql; select * from co_check('co_err_nobool'); co_check -------------------------------------------------------------------------------------------------------- ERROR: syntax error in comment option "extra_warnings" (fnoid: NNN) (expected boolean value after "=") (1 row) create function co_err_notrange(anyrange) returns void as $$ -- @plpgsql_check_options: anyrangetype = int begin end; $$ language plpgsql; select * from co_check('co_err_notrange'); co_check -------------------------------------------------------------------------------------- ERROR: the type specified by "anyrangetype" comment option is not range (fnoid: NNN) (1 row) create function co_err_notrange2(anycompatiblerange) returns void as $$ -- @plpgsql_check_options: anycompatiblerangetype = int begin end; $$ language plpgsql; select * from co_check('co_err_notrange2'); co_check ------------------------------------------------------------------------------------------------ ERROR: the type specified by "anycompatiblerangetype" comment option is not range (fnoid: NNN) (1 row) create function co_err_badtype(anyelement) returns void as $$ -- @plpgsql_check_options: anyelementtype = 'text' begin end; $$ language plpgsql; select * from co_check('co_err_badtype'); co_check ------------------------------------------------------------------------------------------------ ERROR: syntax error in comment option "anyelementtype" (fnoid: NNN) (expected type identifier) (1 row) create function co_err_type_suffix(anyelement) returns text as $$ -- @plpgsql_check_options: anyelementtype = integer[] trailing begin return $1::text; end; $$ language plpgsql; select * from co_check('co_err_type_suffix'); co_check ------------------------------------------- ERROR: syntax error at or near "trailing" (1 row) create function co_err_notype(anyelement) returns void as $$ -- @plpgsql_check_options: anyelementtype begin end; $$ language plpgsql; select * from co_check('co_err_notype'); co_check --------------------------------------------------------------------------------------------------------------- ERROR: syntax error in comment option "anyelementtype" (fnoid: NNN) (expected option's argument of type name) (1 row) create function co_err_badname() returns trigger as $$ -- @plpgsql_check_options: newtable = 10 begin return null; end; $$ language plpgsql; select * from co_check('co_err_badname', 'co_trig_tab'); co_check ----------------------------------------------------------------------------------------------------- ERROR: syntax error in comment option "newtable" (fnoid: NNN) (expected SQL identifier as argument) (1 row) create function co_err_noname() returns trigger as $$ -- @plpgsql_check_options: newtable begin return null; end; $$ language plpgsql; select * from co_check('co_err_noname', 'co_trig_tab'); co_check --------------------------------------------------------------------------------------------------------- ERROR: syntax error in comment option "newtable" (fnoid: NNN) (expected option's argument of name type) (1 row) create function co_err_badtable() returns trigger as $$ -- @plpgsql_check_options: relid = 10 begin return null; end; $$ language plpgsql; select * from co_check('co_err_badtable', 'co_trig_tab'); co_check ---------------------------------------------------------------------------------------- ERROR: syntax error in comment option "relid" (fnoid: NNN) (expected table identifier) (1 row) create function co_err_notable() returns trigger as $$ -- @plpgsql_check_options: relid begin return null; end; $$ language plpgsql; select * from co_check('co_err_notable', 'co_trig_tab'); co_check ------------------------------------------------------------------------------------------------------- ERROR: syntax error in comment option "relid" (fnoid: NNN) (expected option's argument of table name) (1 row) create function co_err_noecho() returns void as $$ -- @plpgsql_check_options: echo begin end; $$ language plpgsql; select * from co_check('co_err_noecho'); co_check ------------------------------------------ ERROR: missing argument of option "echo" (1 row) create function co_err_noechovalue() returns void as $$ -- @plpgsql_check_options: echo = begin end; $$ language plpgsql; select * from co_check('co_err_noechovalue'); co_check --------------------------------- ERROR: expected value after "=" (1 row) drop function co_err_noechovalue(); drop function co_err_noecho(); drop function co_err_notable(); drop function co_err_badtable(); drop function co_err_noname(); drop function co_err_badname(); drop function co_err_notype(anyelement); drop function co_err_badtype(anyelement); drop function co_err_type_suffix(anyelement); drop function co_err_notrange2(anycompatiblerange); drop function co_err_notrange(anyrange); drop function co_err_nobool(); drop function co_err_badbool(); drop function co_err_nocomma(); drop function co_err_nooption(); drop function co_err_unknown(); drop function co_echo(int); drop function co_anycompatiblerange(anycompatiblerange); drop function co_type_array(anyelement); drop function co_type_datetime(anyelement); drop function co_type_multiword(anycompatible); drop function co_type_numeric(anyelement); drop function co_anycompatible(anycompatible); drop function co_anyrange(anyrange); drop function co_anyenum(anyenum); drop type co_enum; drop function co_anyelement2(anyelement); drop function co_anyelement(anyelement); drop function co_relid_qualified(); drop function co_relid(); drop function co_oldtable(); drop function co_newtable(); drop table co_trig_tab2; drop table co_trig_tab; drop function co_skipping(); drop function co_blockcomment(); drop function co_warnings_conflict(); drop function co_without_warnings(); drop function co_all_warnings(); drop function co_bool4(); drop function co_bool3(); drop function co_bool2(); drop function co_bool1(); drop function co_check(text, regclass); -- disable body check so postgresql allows storing function with unclosed dollar tag set check_function_bodies = off; create or replace function unpriv_issue1_dollar_overread() returns void as $body$ $test_custom_tag_1234567890$ abc $test_custom_tag_123456789$body$ language plpgsql; -- trigger plpgsql_check_search_comment_options on the function's prosrc select * from plpgsql_check_function('unpriv_issue1_dollar_overread()'); plpgsql_check_function ---------------------------------------------------------------------------------------- error:42601:unterminated dollar-quoted string at or near "$test_custom_tag_1234567890$+ abc + $test_custom_tag_123456789" Context: compilation of PL/pgSQL function "unpriv_issue1_dollar_overread" near line 1 (2 rows) drop function unpriv_issue1_dollar_overread(); set check_function_bodies = on; okbob-plpgsql_check-800db79/expected/plpgsql_check_comment_options_legacystr.out000066400000000000000000000031311526744060500305140ustar00rootroot00000000000000load 'plpgsql_check'; set client_min_messages to warning; create extension if not exists plpgsql_check; set client_min_messages to notice; -- Some errors reported by the options parser contain the oid of the -- checked function. This wrapper replaces the oid, so the output is -- stable. create function co_check(fnname text, rel regclass default 0) returns setof text as $$ begin return query select * from plpgsql_check_function(fnname, rel); exception when others then return next 'ERROR: ' || regexp_replace(sqlerrm, 'fnoid: [0-9]+', 'fnoid: NNN'); end; $$ language plpgsql; set standard_conforming_strings = off; ERROR: non-standard string literals are not supported set escape_string_warning = off; ERROR: unrecognized configuration parameter "escape_string_warning" create function co_legacy_string() returns text as $func$ declare s text := 'can\'t'; x int; begin -- @plpgsql_check_options: without_warnings return s; end; $func$ language plpgsql; ERROR: unterminated quoted string at or near "'; x int; begin -- @plpgsql_check_options: without_warnings return s; end; " LINE 4: s text := 'can\'t'; ^ select * from co_check('co_legacy_string'); co_check --------------------------------------------------- ERROR: function "co_legacy_string" does not exist (1 row) reset escape_string_warning; ERROR: unrecognized configuration parameter "escape_string_warning" reset standard_conforming_strings; drop function co_legacy_string(); ERROR: function co_legacy_string() does not exist drop function co_check(text, regclass); okbob-plpgsql_check-800db79/expected/plpgsql_check_comment_options_legacystr_1.out000066400000000000000000000020441526744060500307360ustar00rootroot00000000000000load 'plpgsql_check'; set client_min_messages to warning; create extension if not exists plpgsql_check; set client_min_messages to notice; -- Some errors reported by the options parser contain the oid of the -- checked function. This wrapper replaces the oid, so the output is -- stable. create function co_check(fnname text, rel regclass default 0) returns setof text as $$ begin return query select * from plpgsql_check_function(fnname, rel); exception when others then return next 'ERROR: ' || regexp_replace(sqlerrm, 'fnoid: [0-9]+', 'fnoid: NNN'); end; $$ language plpgsql; set standard_conforming_strings = off; set escape_string_warning = off; create function co_legacy_string() returns text as $func$ declare s text := 'can\'t'; x int; begin -- @plpgsql_check_options: without_warnings return s; end; $func$ language plpgsql; select * from co_check('co_legacy_string'); co_check ---------- (0 rows) reset escape_string_warning; reset standard_conforming_strings; drop function co_legacy_string(); drop function co_check(text, regclass); okbob-plpgsql_check-800db79/expected/plpgsql_check_expr_walk.out000066400000000000000000000777451526744060500252440ustar00rootroot00000000000000set client_min_messages to warning; create extension if not exists plpgsql_check; set client_min_messages to notice; -- -- checks of the format() function -- -- the format string is parsed when it is a constant, the number of the -- placeholders is compared against the number of the arguments and the -- syntax of the format specifiers is validated -- create table ew_tab1(a int, b text); -- correct format strings - a simple placeholder, a doubled percent, -- an explicit argument position, a direct width, the minus flag, -- the identifier and the literal placeholders create function ew_f1(p text) returns void as $$ begin raise notice '%', format('%s %s', p, p); raise notice '%', format('100%% %s', p); raise notice '%', format('%1$s %1$s %2$s', p, p); raise notice '%', format('[%10s][%-10s]', p, p); raise notice '%', format('%I = %L', p, p); end; $$ language plpgsql; select * from plpgsql_check_function('ew_f1', fatal_errors => false); plpgsql_check_function ------------------------ (0 rows) -- an indirect width - the width is taken from the next argument or from -- the argument on the given position create function ew_f2(p text, w int) returns void as $$ begin raise notice '%', format('[%*s]', w, p); raise notice '%', format('[%2$*1$s]', w, p); end; $$ language plpgsql; select * from plpgsql_check_function('ew_f2', fatal_errors => false); plpgsql_check_function ------------------------ (0 rows) -- the arguments are numbered from one create function ew_f3(p text) returns void as $$ begin raise notice '%', format('%0$s', p); end; $$ language plpgsql; select * from plpgsql_check_function('ew_f3', fatal_errors => false); plpgsql_check_function ------------------------------------------------------------------------------------ error:22023:3:RAISE:format specifies argument 0, but arguments are numbered from 1 Query: format('%0$s', p) -- ^ (3 rows) -- the same for the position of the width argument create function ew_f4(p text, w int) returns void as $$ begin raise notice '%', format('%0$*s', w, p); end; $$ language plpgsql; select * from plpgsql_check_function('ew_f4', fatal_errors => false); plpgsql_check_function ------------------------------------------------------------------------------------ error:22023:3:RAISE:format specifies argument 0, but arguments are numbered from 1 Query: format('%0$*s', w, p) -- ^ (3 rows) -- the position of the width argument has to be closed by a dollar create function ew_f5(p text, w int) returns void as $$ begin raise notice '%', format('%*1s', w, p); end; $$ language plpgsql; select * from plpgsql_check_function('ew_f5', fatal_errors => false); plpgsql_check_function ------------------------------------------------------------------ error:22023:3:RAISE:width argument position must be ended by "$" Query: format('%*1s', w, p) -- ^ (3 rows) -- the parsed number has to fit into an integer create function ew_f6(p text) returns void as $$ begin raise notice '%', format('%99999999999999$s', p); end; $$ language plpgsql; select * from plpgsql_check_function('ew_f6', fatal_errors => false); plpgsql_check_function -------------------------------------------- error:22003:3:RAISE:number is out of range Query: format('%99999999999999$s', p) -- ^ (3 rows) -- Adding the format-string argument must not overflow an INT_MAX position. create function ew_max_position(p text, args text[]) returns void as $$ begin execute format('SELECT %2147483647$s', p); execute format('SELECT %1$*2147483647$s', p); perform format('%2147483647$s', p); perform format('%1$*2147483647$s', p); execute format('SELECT %2147483647$s', variadic args); execute format('SELECT %*2147483647$s %s', variadic args); end; $$ language plpgsql; select sqlstate, message from plpgsql_check_function_tb('ew_max_position', fatal_errors => false, security_warnings => true) where level = 'error'; sqlstate | message ----------+-------------------------------- 22023 | too few arguments for format() 22023 | too few arguments for format() 22023 | too few arguments for format() 22023 | too few arguments for format() (4 rows) drop function ew_max_position(text, text[]); -- only s, I and L are known specifiers create function ew_f7(p text) returns void as $$ begin raise notice '%', format('%d', p); end; $$ language plpgsql; select * from plpgsql_check_function('ew_f7', fatal_errors => false); plpgsql_check_function -------------------------------------------------------------- error:22023:3:RAISE:unrecognized format() type specifier "d" Query: format('%d', p) -- ^ (3 rows) -- the format string cannot end by an unfinished specifier - the parser -- can stop at any part of the specifier create function ew_f8(p text, w int) returns void as $$ begin raise notice '%', format('value %', p); end; $$ language plpgsql; select * from plpgsql_check_function('ew_f8', fatal_errors => false); plpgsql_check_function ---------------------------------------------------------- error:22023:3:RAISE:unterminated format() type specifier Query: format('value %', p) -- ^ Hint: For a single "%%" use "%%%%". warning extra:00000:unused parameter "w" (5 rows) create function ew_f8a(p text) returns void as $$ begin raise notice '%', format('value %1', p); end; $$ language plpgsql; select * from plpgsql_check_function('ew_f8a', fatal_errors => false); plpgsql_check_function ---------------------------------------------------------- error:22023:3:RAISE:unterminated format() type specifier Query: format('value %1', p) -- ^ Hint: For a single "%%" use "%%%%". (4 rows) create function ew_f8b(p text) returns void as $$ begin raise notice '%', format('value %1$', p); end; $$ language plpgsql; select * from plpgsql_check_function('ew_f8b', fatal_errors => false); plpgsql_check_function ---------------------------------------------------------- error:22023:3:RAISE:unterminated format() type specifier Query: format('value %1$', p) -- ^ Hint: For a single "%%" use "%%%%". (4 rows) create function ew_f8c(p text) returns void as $$ begin raise notice '%', format('value %-', p); end; $$ language plpgsql; select * from plpgsql_check_function('ew_f8c', fatal_errors => false); plpgsql_check_function ---------------------------------------------------------- error:22023:3:RAISE:unterminated format() type specifier Query: format('value %-', p) -- ^ Hint: For a single "%%" use "%%%%". (4 rows) create function ew_f8d(p text) returns void as $$ begin raise notice '%', format('value %*', p); end; $$ language plpgsql; select * from plpgsql_check_function('ew_f8d', fatal_errors => false); plpgsql_check_function ---------------------------------------------------------- error:22023:3:RAISE:unterminated format() type specifier Query: format('value %*', p) -- ^ Hint: For a single "%%" use "%%%%". (4 rows) create function ew_f8e(p text, w int) returns void as $$ begin raise notice '%', format('value %*1$', w, p); end; $$ language plpgsql; select * from plpgsql_check_function('ew_f8e', fatal_errors => false); plpgsql_check_function ---------------------------------------------------------- error:22023:3:RAISE:unterminated format() type specifier Query: format('value %*1$', w, p) -- ^ Hint: For a single "%%" use "%%%%". (4 rows) -- the position of the width argument is numbered from one too, and the -- number of the direct width has to fit into an integer create function ew_f8f(p text, w int) returns void as $$ begin raise notice '%', format('value %*0$s', w, p); end; $$ language plpgsql; select * from plpgsql_check_function('ew_f8f', fatal_errors => false); plpgsql_check_function ------------------------------------------------------------------------------------ error:22023:3:RAISE:format specifies argument 0, but arguments are numbered from 1 Query: format('value %*0$s', w, p) -- ^ (3 rows) create function ew_f8g(p text, w int) returns void as $$ begin raise notice '%', format('value %*99999999999999$s', w, p); end; $$ language plpgsql; select * from plpgsql_check_function('ew_f8g', fatal_errors => false); plpgsql_check_function ------------------------------------------------- error:22003:3:RAISE:number is out of range Query: format('value %*99999999999999$s', w, p) -- ^ (3 rows) create function ew_f8h(p text) returns void as $$ begin raise notice '%', format('value %99999999999999s', p); end; $$ language plpgsql; select * from plpgsql_check_function('ew_f8h', fatal_errors => false); plpgsql_check_function -------------------------------------------- error:22003:3:RAISE:number is out of range Query: format('value %99999999999999s', p) -- ^ (3 rows) create function ew_f8i(p text) returns void as $$ begin raise notice '%', format('value %1$99999999999999s', p); end; $$ language plpgsql; select * from plpgsql_check_function('ew_f8i', fatal_errors => false); plpgsql_check_function ---------------------------------------------- error:22003:3:RAISE:number is out of range Query: format('value %1$99999999999999s', p) -- ^ (3 rows) -- there have to be enough arguments for all the placeholders create function ew_f9(p text) returns void as $$ begin raise notice '%', format('%s %s', p); end; $$ language plpgsql; select * from plpgsql_check_function('ew_f9', fatal_errors => false); plpgsql_check_function ---------------------------------------------------- error:22023:3:RAISE:too few arguments for format() Query: format('%s %s', p) -- ^ (3 rows) -- the same for the indirect width create function ew_f10(w int) returns void as $$ begin raise notice '%', format('[%*s]', w); end; $$ language plpgsql; select * from plpgsql_check_function('ew_f10', fatal_errors => false); plpgsql_check_function ---------------------------------------------------- error:22023:3:RAISE:too few arguments for format() Query: format('[%*s]', w) -- ^ (3 rows) create function ew_f11(w int) returns void as $$ begin raise notice '%', format('[%2$*3$s]', w, w); end; $$ language plpgsql; select * from plpgsql_check_function('ew_f11', fatal_errors => false); plpgsql_check_function ---------------------------------------------------- error:22023:3:RAISE:too few arguments for format() Query: format('[%2$*3$s]', w, w) -- ^ (3 rows) -- the arguments that are not used by the format string are reported create function ew_f12(p text) returns void as $$ begin raise notice '%', format('%s', p, p, p); end; $$ language plpgsql; select * from plpgsql_check_function('ew_f12', fatal_errors => false); plpgsql_check_function -------------------------------------------------------------- warning:00000:3:RAISE:unused parameters of function "format" Query: format('%s', p, p, p) -- ^ (3 rows) -- -- the format() function as a source of a dynamic query -- -- when the format string and all the arguments are constants, the result -- is known and it is checked like a static query -- create function ew_f13() returns void as $$ begin execute format('select %I from %I', 'a', 'ew_tab1'); end; $$ language plpgsql; select * from plpgsql_check_function('ew_f13', fatal_errors => false); plpgsql_check_function ------------------------ (0 rows) -- an identifier placeholder with an unknown value is replaced by a -- constant name, so the query cannot be executed, but its syntax is -- still checked create function ew_f14(p text) returns void as $$ begin execute format('select a from %I', p); end; $$ language plpgsql; select * from plpgsql_check_function('ew_f14', fatal_errors => false); plpgsql_check_function ------------------------ (0 rows) -- a literal placeholder with an unknown value is replaced by NULL create function ew_f15(p text) returns void as $$ begin execute format('select a from ew_tab1 where b = %L', p); end; $$ language plpgsql; select * from plpgsql_check_function('ew_f15', fatal_errors => false); plpgsql_check_function ------------------------ (0 rows) -- a value placeholder with an unknown value makes the result unknown create function ew_f16(p text) returns void as $$ begin execute format('select a from ew_tab1 where b = %s', p); end; $$ language plpgsql; select * from plpgsql_check_function('ew_f16', fatal_errors => false, security_warnings => true); plpgsql_check_function ----------------------------------------------------------------------------- security:00000:3:EXECUTE:text type variable is not sanitized Query: format('select a from ew_tab1 where b = %s', p) -- ^ Detail: The EXECUTE expression is SQL injection vulnerable. Hint: Use quote_ident, quote_literal or format function to secure variable. (5 rows) -- the same with an explicit argument position and with an indirect width create function ew_f17(p text, w int) returns void as $$ begin execute format('select %1$I from ew_tab1', 'a'); execute format('select %*s from ew_tab1', w, 'a'); end; $$ language plpgsql; select * from plpgsql_check_function('ew_f17', fatal_errors => false, security_warnings => true); plpgsql_check_function ------------------------------------------ warning extra:00000:unused parameter "p" (1 row) -- Padding can be required SQL syntax, including after multibyte characters. create table ew_width_source(U&"\00e9" int, "select" int); insert into ew_width_source values (1, 1); create function ew_widths() returns int as $$ declare n int; w text := '1'; begin execute format('SELECT%1s1', '') into n; execute format('SELECT%-1s1', '') into n; execute format('SELECT%*s1', 1, '') into n; execute format('SELECT%2$*1$s1', -1, '') into n; execute format('SELECT%1$1s%1$1s1', '') into n; execute format('SELECT%*s1', w, '') into n; execute format('SELECT %-2sFROM ew_width_source', U&'\00e9') into n; execute format('SELECT %-9IFROM ew_width_source', 'select') into n; execute format('SELECT %5L::int', '1') into n; execute format('SELECT%*s 1', null::int, '') into n; return n; end; $$ language plpgsql; select ew_widths(); ew_widths ----------- 1 (1 row) select * from plpgsql_check_function('ew_widths'); plpgsql_check_function ------------------------ (0 rows) drop function ew_widths(); drop table ew_width_source; -- An unknown width makes the reconstructed query unknown, not malformed. create function ew_unknown_width(w int) returns int as $$ declare n int; begin execute format('SELECT%*s1', w, '') into n; return n; end; $$ language plpgsql; select ew_unknown_width(1); ew_unknown_width ------------------ 1 (1 row) select level, message from plpgsql_check_function_tb('ew_unknown_width') where level = 'error'; level | message -------+--------- (0 rows) drop function ew_unknown_width(int); create function ew_invalid_width() returns void as $$ begin execute format('SELECT%*s1', '-2147483648'::int, ''); end; $$ language plpgsql; select sqlstate, message from plpgsql_check_function_tb('ew_invalid_width') where level = 'error'; sqlstate | message ----------+------------------------ 22003 | number is out of range (1 row) drop function ew_invalid_width(); -- a format string that cannot be evaluated makes the dynamic query -- unknown - the string ends by an unfinished specifier, it contains an -- unknown specifier, it needs more arguments than it gets, or it refers -- to an argument or to a width argument behind the last one create function ew_f18(p text) returns void as $$ begin execute format('select %s %s', p); end; $$ language plpgsql; select * from plpgsql_check_function('ew_f18', fatal_errors => false, security_warnings => true); plpgsql_check_function ----------------------------------------------------------------------------- error:22023:3:EXECUTE:too few arguments for format() Query: format('select %s %s', p) -- ^ security:00000:3:EXECUTE:text type variable is not sanitized Query: format('select %s %s', p) -- ^ Detail: The EXECUTE expression is SQL injection vulnerable. Hint: Use quote_ident, quote_literal or format function to secure variable. (8 rows) create function ew_f18a() returns void as $$ begin execute format('select 1 %'); end; $$ language plpgsql; select * from plpgsql_check_function('ew_f18a', fatal_errors => false); plpgsql_check_function ------------------------------------------------------------ error:22023:3:EXECUTE:unterminated format() type specifier Query: format('select 1 %') -- ^ Hint: For a single "%%" use "%%%%". (4 rows) create function ew_f18b() returns void as $$ begin execute format('select 1 -- 100%%'); end; $$ language plpgsql; select * from plpgsql_check_function('ew_f18b', fatal_errors => false); plpgsql_check_function ------------------------ (0 rows) create function ew_f18c() returns void as $$ begin execute format('select %d', 1); end; $$ language plpgsql; select * from plpgsql_check_function('ew_f18c', fatal_errors => false); plpgsql_check_function ---------------------------------------------------------------- error:22023:3:EXECUTE:unrecognized format() type specifier "d" Query: format('select %d', 1) -- ^ (3 rows) create function ew_f18d() returns void as $$ begin execute format('select %2$*5$s', 'a', 'b'); end; $$ language plpgsql; select * from plpgsql_check_function('ew_f18d', fatal_errors => false); plpgsql_check_function ------------------------------------------------------ error:22023:3:EXECUTE:too few arguments for format() Query: format('select %2$*5$s', 'a', 'b') -- ^ (3 rows) create function ew_f18e() returns void as $$ begin execute format('select %*s'); end; $$ language plpgsql; select * from plpgsql_check_function('ew_f18e', fatal_errors => false); plpgsql_check_function ------------------------------------------------------ error:22023:3:EXECUTE:too few arguments for format() Query: format('select %*s') -- ^ (3 rows) create function ew_f18f() returns void as $$ begin execute format('select %3$s', 'a'); end; $$ language plpgsql; select * from plpgsql_check_function('ew_f18f', fatal_errors => false); plpgsql_check_function ------------------------------------------------------ error:22023:3:EXECUTE:too few arguments for format() Query: format('select %3$s', 'a') -- ^ (3 rows) create function ew_f18g() returns void as $$ begin execute format('select %s %s', 'a'); end; $$ language plpgsql; select * from plpgsql_check_function('ew_f18g', fatal_errors => false); plpgsql_check_function ------------------------------------------------------ error:22023:3:EXECUTE:too few arguments for format() Query: format('select %s %s', 'a') -- ^ (3 rows) -- an argument of the format string which is not a string is not a source -- of a SQL injection create function ew_f18h(n int) returns void as $$ begin execute format('select %s', n); end; $$ language plpgsql; select * from plpgsql_check_function('ew_f18h', fatal_errors => false, security_warnings => true); plpgsql_check_function ------------------------ (0 rows) -- -- detection of the SQL injection in the dynamic queries -- -- the format string itself is a variable, so nothing can be checked create function ew_f19(p text, q text) returns void as $$ begin execute format(p, q); end; $$ language plpgsql; select * from plpgsql_check_function('ew_f19', security_warnings => true); plpgsql_check_function ----------------------------------------------------------------------------- security:00000:3:EXECUTE:text type variable is not sanitized Query: format(p, q) -- ^ Detail: The EXECUTE expression is SQL injection vulnerable. Hint: Use quote_ident, quote_literal or format function to secure variable. (5 rows) -- the arguments of a function are not sanitized create function ew_f20(p text) returns void as $$ begin execute upper(p); end; $$ language plpgsql; select * from plpgsql_check_function('ew_f20', security_warnings => true); plpgsql_check_function ----------------------------------------------------------------------------- security:00000:3:EXECUTE:text type variable is not sanitized Query: upper(p) -- ^ Detail: The EXECUTE expression is SQL injection vulnerable. Hint: Use quote_ident, quote_literal or format function to secure variable. (5 rows) -- a named argument is checked like a positional one create function ew_id(val text) returns text as $$ begin return val; end $$ language plpgsql; create function ew_f21(p text) returns void as $$ begin execute ew_id(val => p); end; $$ language plpgsql; select * from plpgsql_check_function('ew_f21', security_warnings => true); plpgsql_check_function ----------------------------------------------------------------------------- security:00000:3:EXECUTE:text type variable is not sanitized Query: ew_id(val => p) -- ^ Detail: The EXECUTE expression is SQL injection vulnerable. Hint: Use quote_ident, quote_literal or format function to secure variable. (5 rows) -- a function whose arguments are not strings is safe create function ew_f22(n int) returns void as $$ begin execute ew_id(n::text); end; $$ language plpgsql; select * from plpgsql_check_function('ew_f22', security_warnings => true); plpgsql_check_function ------------------------ (0 rows) -- an expression that the walker does not know is considered safe create function ew_f23(p text) returns void as $$ begin execute case when p is null then 'select 1' else 'select 2' end; end; $$ language plpgsql; select * from plpgsql_check_function('ew_f23', security_warnings => true); plpgsql_check_function ------------------------ (0 rows) -- a sanitized value is safe create function ew_f24(p text) returns void as $$ begin execute 'select * from ' || quote_ident(p); execute 'select ' || quote_literal(p); execute 'select ' || quote_nullable(p); end; $$ language plpgsql; select * from plpgsql_check_function('ew_f24', security_warnings => true); plpgsql_check_function ------------------------ (0 rows) -- -- detection of an implicit cast of a table attribute -- -- the type of the variable is wider than the type of the attribute, so -- the attribute is casted and the index on it cannot be used create function ew_f25(p bigint) returns int as $$ declare r int; begin select a into r from ew_tab1 where a = p; return r; end; $$ language plpgsql; select * from plpgsql_check_function('ew_f25', performance_warnings => true); plpgsql_check_function ----------------------------------------------------------------------------------------- performance:00000:routine is marked as VOLATILE, should be STABLE Hint: When you fix this issue, please, recheck other functions that uses this function. (2 rows) -- an explicit cast written by the user is not reported create function ew_f26(p bigint) returns int as $$ declare r int; begin select a into r from ew_tab1 where a::bigint = p; return r; end; $$ language plpgsql; select * from plpgsql_check_function('ew_f26', performance_warnings => true); plpgsql_check_function ----------------------------------------------------------------------------------------- performance:00000:routine is marked as VOLATILE, should be STABLE Hint: When you fix this issue, please, recheck other functions that uses this function. (2 rows) -- the casted expression is not a plain attribute create function ew_f27(p bigint) returns int as $$ declare r int; begin select a into r from ew_tab1 where abs(a) = p; return r; end; $$ language plpgsql; select * from plpgsql_check_function('ew_f27', performance_warnings => true); plpgsql_check_function ----------------------------------------------------------------------------------------- performance:00000:routine is marked as VOLATILE, should be STABLE Hint: When you fix this issue, please, recheck other functions that uses this function. (2 rows) -- -- collecting of the dependencies -- -- an operator that is not in pg_catalog is a dependency of the routine, -- a prefix operator has no type on its left side create function ew_neg(int) returns int as $$ select -$1 $$ language sql immutable; create operator @# (function = ew_neg, rightarg = int); create function ew_f29(n int) returns int as $$ begin return @# n; end; $$ language plpgsql; select type, schema, name, params from plpgsql_show_dependency_tb('ew_f29'); type | schema | name | params ----------+--------+------+------------- OPERATOR | public | @# | (-,integer) (1 row) -- Explicit VARIADIC arrays supply their elements, not one array argument. create function ew_f30(p text) returns void as $$ begin raise notice '%', format('%s %s', variadic array['a', p]); raise notice '%', format('%s %s', variadic '{a,b}'::text[]); raise notice '%', format('%s %s', variadic '[0:1]={a,b}'::text[]); raise notice '%', format('%s %s', variadic array[1, 2]); raise notice '%', format('%s %s %s %s', variadic array[['a', p], ['b', p]]); raise notice '%', format('plain', variadic array[]::text[]); raise notice '%', format('plain', variadic null::text[]); raise notice '%', format('%s%L', variadic array[null, null]::text[]); end; $$ language plpgsql; select * from plpgsql_check_function('ew_f30', fatal_errors => false); plpgsql_check_function ------------------------ (0 rows) -- Unknown array lengths do not disable format syntax validation. create function ew_f31(args text[]) returns text as $$ begin return format('%s %s', variadic args); end; $$ language plpgsql; select * from plpgsql_check_function('ew_f31'); plpgsql_check_function ------------------------ (0 rows) create function ew_f32() returns text as $$ begin return format('%s %s', variadic array['a']); end; $$ language plpgsql; select * from plpgsql_check_function('ew_f32'); plpgsql_check_function ----------------------------------------------------- error:22023:3:RETURN:too few arguments for format() Query: format('%s %s', variadic array['a']) -- ^ (3 rows) create function ew_f33(args text[]) returns text as $$ begin return format('%s %Q', variadic args); end; $$ language plpgsql; select * from plpgsql_check_function('ew_f33'); plpgsql_check_function --------------------------------------------------------------- error:22023:3:RETURN:unrecognized format() type specifier "Q" Query: format('%s %Q', variadic args) -- ^ (3 rows) create function ew_f34() returns text as $$ begin return format('%s', variadic null::text[]); end; $$ language plpgsql; select * from plpgsql_check_function('ew_f34'); plpgsql_check_function ----------------------------------------------------- error:22023:3:RETURN:too few arguments for format() Query: format('%s', variadic null::text[]) -- ^ (3 rows) -- Constant synthesis uses the same expanded arguments, including NULLs. create function ew_f35() returns int as $$ declare r record; begin execute format('select %s%L::int as n', variadic array[null, '7']) into r; return r.n; end; $$ language plpgsql; select * from plpgsql_check_function('ew_f35'); plpgsql_check_function ------------------------ (0 rows) select ew_f35(); ew_f35 -------- 7 (1 row) create function ew_f36() returns int as $$ declare r record; begin execute format('select %L::int as n', variadic array[null]::text[]) into r; return r.n; end; $$ language plpgsql; select * from plpgsql_check_function('ew_f36'); plpgsql_check_function ------------------------ (0 rows) select ew_f36() is null; ?column? ---------- t (1 row) -- An explicit position resets subsequent implicit argument consumption. create function ew_f37(a text, b text) returns text as $$ begin return format('%s %s %1$s %s', a, b); end; $$ language plpgsql; select * from plpgsql_check_function('ew_f37'); plpgsql_check_function ------------------------ (0 rows) select ew_f37('a', 'b'); ew_f37 --------- a b a b (1 row) create function ew_f38() returns text as $$ begin return format('%s %s %*1$s', 3, 'a'); end; $$ language plpgsql; select * from plpgsql_check_function('ew_f38'); plpgsql_check_function ------------------------ (0 rows) select ew_f38(); ew_f38 --------- 3 a a (1 row) create function ew_f39(a text, b text) returns text as $$ begin return format('%2$s %s', a, b); end; $$ language plpgsql; select * from plpgsql_check_function('ew_f39'); plpgsql_check_function ----------------------------------------------------- error:22023:3:RETURN:too few arguments for format() Query: format('%2$s %s', a, b) -- ^ (3 rows) -- Synthesis must follow the same cursor for ordinary and VARIADIC calls. create function ew_f40() returns int as $$ declare a int; b int; begin execute format('select %s + %s + %1$s + %s', '1', '2') into a; execute format('select %s + %s + %1$s + %s', variadic array['1', '2']) into b; return a + b; end; $$ language plpgsql; select * from plpgsql_check_function('ew_f40'); plpgsql_check_function ------------------------ (0 rows) select ew_f40(); ew_f40 -------- 12 (1 row) create function ew_f41() returns int as $$ declare a int; begin execute format('select %s + %s + %*1$s', 1, 2) into a; return a; end; $$ language plpgsql; select * from plpgsql_check_function('ew_f41'); plpgsql_check_function ------------------------ (0 rows) select ew_f41(); ew_f41 -------- 5 (1 row) drop function ew_f37(text, text); drop function ew_f38(); drop function ew_f39(text, text); drop function ew_f40(); drop function ew_f41(); drop function ew_f30(text); drop function ew_f31(text[]); drop function ew_f32(); drop function ew_f33(text[]); drop function ew_f34(); drop function ew_f35(); drop function ew_f36(); drop function ew_f1(text); drop function ew_f2(text, int); drop function ew_f3(text); drop function ew_f4(text, int); drop function ew_f5(text, int); drop function ew_f6(text); drop function ew_f7(text); drop function ew_f8(text, int); drop function ew_f8a(text); drop function ew_f8b(text); drop function ew_f8c(text); drop function ew_f8d(text); drop function ew_f8e(text, int); drop function ew_f8f(text, int); drop function ew_f8g(text, int); drop function ew_f8h(text); drop function ew_f8i(text); drop function ew_f9(text); drop function ew_f10(int); drop function ew_f11(int); drop function ew_f12(text); drop function ew_f13(); drop function ew_f14(text); drop function ew_f15(text); drop function ew_f16(text); drop function ew_f17(text, int); drop function ew_f18(text); drop function ew_f18a(); drop function ew_f18b(); drop function ew_f18c(); drop function ew_f18d(); drop function ew_f18e(); drop function ew_f18f(); drop function ew_f18g(); drop function ew_f18h(int); drop function ew_f19(text, text); drop function ew_f20(text); drop function ew_f21(text); drop function ew_f22(int); drop function ew_f23(text); drop function ew_f24(text); drop function ew_f25(bigint); drop function ew_f26(bigint); drop function ew_f27(bigint); drop function ew_f29(int); drop function ew_id(text); drop operator @# (none, int); drop function ew_neg(int); drop table ew_tab1; okbob-plpgsql_check-800db79/expected/plpgsql_check_nocrash.out000066400000000000000000001044321526744060500246650ustar00rootroot00000000000000load 'plpgsql'; create extension if not exists plpgsql_check; NOTICE: extension "plpgsql_check" already exists, skipping set client_min_messages to notice; set plpgsql_check.regress_test_mode = true; create table zero_columns(); create function assign_empty_tupdesc() returns int as $$ declare v int; begin select * from zero_columns into v; execute 'select * from zero_columns' into v; return v; end; $$ language plpgsql; select * from plpgsql_check_function('assign_empty_tupdesc()'); plpgsql_check_function ----------------------------------------------------------------------- warning:00000:5:SQL statement:too few attributes for target variables Query: select * from zero_columns -- ^ Detail: There are more target variables than output columns in query. Hint: Check target variables in SELECT INTO statement. Context: at SQL statement to v variables warning:00000:6:EXECUTE:too few attributes for target variables Query: select * from zero_columns -- ^ Detail: There are more target variables than output columns in query. Hint: Check target variables in SELECT INTO statement. Context: at EXECUTE to v variables (12 rows) drop function assign_empty_tupdesc(); drop table zero_columns; create function public.int_eq(int, int) returns bool as $$ select $1 = $2; $$ language sql; create operator public.=== ( leftarg = int, rightarg = int, procedure = public.int_eq ); create function use_custom_operator() returns bool as $$ begin return 1 operator(public.===) 2; end; $$ language plpgsql; select type, schema, name, params from plpgsql_show_dependency_tb('use_custom_operator()'); type | schema | name | params ----------+--------+------+------------------- OPERATOR | public | === | (integer,integer) (1 row) drop function use_custom_operator(); drop function public.int_eq cascade; NOTICE: drop cascades to operator ===(integer,integer) create function tracked_const_oob() returns void as $$ declare str text; res int; begin -- assign a string constant, so the strconstvars array is allocated str := 'constant'; -- the dynamic query has much more params than the function has datums execute 'select $40' into res using 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 'x'::text; raise notice '%, %', str, res; end; $$ language plpgsql; select * from plpgsql_check_function('tracked_const_oob()'); plpgsql_check_function -------------------------------------------------------------------------------------- warning:42804:10:EXECUTE:target type is different type than source type Detail: cast "text" value to "integer" type Hint: The input expression type does not have an assignment cast to the target type. Context: at EXECUTE to variable "res" declared on line 4 (4 rows) drop function tracked_const_oob(); create table returned_expr_tab(a int); create function returned_expr_null_node() returns setof refcursor as $$ begin return query select a::text::refcursor from returned_expr_tab; end; $$ language plpgsql; select * from plpgsql_check_function('returned_expr_null_node()', compatibility_warnings => true); plpgsql_check_function ------------------------------------------------------------------------------------- compatibility:00000:3:RETURN QUERY:obsolete setting of refcursor or cursor variable Detail: Internal name of cursor should not be specified by users. (2 rows) drop function returned_expr_null_node(); drop table returned_expr_tab; create table rvalue_tab(a int); create function rvalue_null_node() returns void as $$ declare c refcursor; begin c := (select a::text::refcursor from rvalue_tab limit 1); raise notice '%', c; end; $$ language plpgsql; select * from plpgsql_check_function('rvalue_null_node()', compatibility_warnings => true); plpgsql_check_function ------------------------ (0 rows) drop function rvalue_null_node(); drop table rvalue_tab; create table assert_column_tab(a int); create function pragma_assert_column() returns void as $$ declare tname text default 'assert_column_tab'; begin -- ASSERT-COLUMN expects two variables, only one is passed perform plpgsql_check_pragma('assert-column: tname'); raise notice '%', tname; end; $$ language plpgsql; select * from plpgsql_check_function('pragma_assert_column()'); WARNING: "assert-column" on line 6 is not processed. DETAIL: too few variables for "assert-column" pragma plpgsql_check_function ------------------------ (0 rows) drop function pragma_assert_column(); drop table assert_column_tab; create function pragma_sequence_prefix() returns void as $$ begin -- 6 chars long pragma name, the sequence handler reads 3 chars behind -- the end of the string perform plpgsql_check_pragma('SEQUEN'); end; $$ language plpgsql; select * from plpgsql_check_function('pragma_sequence_prefix()'); WARNING: unsupported pragma: SEQUEN plpgsql_check_function ------------------------ (0 rows) drop function pragma_sequence_prefix(); create function pragma_sequence_misdispatch() returns void as $$ begin perform plpgsql_check_pragma('SEQUENCE_FOO'); end; $$ language plpgsql; select * from plpgsql_check_function('pragma_sequence_misdispatch()'); WARNING: unsupported pragma: SEQUENCE_FOO plpgsql_check_function ------------------------ (0 rows) drop function pragma_sequence_misdispatch(); create function profiled_function() returns int as $$ begin return 1; end; $$ language plpgsql; set plpgsql_check.profiler to on; begin; select profiled_function(); profiled_function ------------------- 1 (1 row) select plpgsql_profiler_reset_all(); plpgsql_profiler_reset_all ---------------------------- (1 row) commit; set plpgsql_check.profiler to off; drop function profiled_function(); do $$ declare src text := ''; begin for i in 1..3000 loop src := src || 'perform 1;'; end loop; execute format('create or replace function too_many_statements() returns void as $q$ begin if false then %s end if; end $q$ language plpgsql', src); end; $$; set plpgsql_check.profiler to off; drop function too_many_statements(); create function dynamic_query_with_params(p int) returns void as $$ begin execute 'select 1 where $1 > 0' using p; end; $$ language plpgsql; set plpgsql_check.profiler to on; select dynamic_query_with_params(1); dynamic_query_with_params --------------------------- (1 row) set plpgsql_check.profiler to off; drop function dynamic_query_with_params(int); create function case_over_record() returns int as $$ declare r record; begin case r when null then return 1; else return 2; end case; end; $$ language plpgsql; select * from plpgsql_check_function('case_over_record()'); plpgsql_check_function ------------------------ (0 rows) drop function case_over_record(); create table dynsql_tab(a int); create function dynamic_sql_forms(tname text) returns void as $$ declare query text; begin execute 'select * from dynsql_tab'; execute format('select * from %I', tname); query := 'select * from ' || quote_ident(tname); execute query; end; $$ language plpgsql; select * from plpgsql_check_function('dynamic_sql_forms(text)', all_warnings => true); plpgsql_check_function --------------------------------------------------------------------------- performance:00000:5:EXECUTE:immutable expression without parameters found Detail: the EXECUTE command is not necessary probably Hint: Don't use dynamic SQL when you can use static SQL. (3 rows) drop function dynamic_sql_forms(text); drop table dynsql_tab; create function uninitialized_is_mp(v text) returns void as $$ begin -- the %L placeholder makes the query non constant, so it is checked in a -- subtransaction, and the check fails because the table does not exist execute format('select * from missing_table where a = %L', v); end; $$ language plpgsql; select * from plpgsql_check_function('uninitialized_is_mp(text)', all_warnings => true); plpgsql_check_function ------------------------ (0 rows) drop function uninitialized_is_mp(text); create table traced_tab(a int); insert into traced_tab values(1); create or replace function traced_trg_func() returns trigger as $$ begin return old; end; $$ language plpgsql; create trigger traced_trg before delete on traced_tab for each row execute procedure traced_trg_func(); set plpgsql_check.enable_tracer to on; set plpgsql_check.tracer to on; set plpgsql_check.tracer_test_mode to true; delete from traced_tab; NOTICE: #0 ->> start of function traced_trg_func() (oid=0, tnl=1) NOTICE: #0 triggered by before row delete trigger NOTICE: #0 "old" => '(1)' NOTICE: #0 <<- end of function traced_trg_func (elapsed time=0.010 ms) set plpgsql_check.tracer to off; set plpgsql_check.enable_tracer to off; drop table traced_tab cascade; drop function traced_trg_func(); create function failing_assert() returns void as $$ declare r record; begin select 1 as x into r; -- the reference to a record field creates a RECFIELD datum assert r.x = 1; end; $$ language plpgsql; set plpgsql_check.enable_tracer to on; set plpgsql_check.tracer to on; set plpgsql_check.trace_assert to on; set plpgsql_check.tracer_test_mode to true; select failing_assert(); NOTICE: #0 ->> start of function failing_assert() (oid=0, tnl=1) NOTICE: PLpgSQL assert expression (r.x = 1) on line 7 of failing_assert() is true NOTICE: "r"."x" => '1' NOTICE: #0 <<- end of function failing_assert (elapsed time=0.010 ms) failing_assert ---------------- (1 row) set plpgsql_check.trace_assert to off; set plpgsql_check.tracer to off; set plpgsql_check.enable_tracer to off; drop function failing_assert(); create function dynamic_record_param() returns setof record as $$ begin -- the 12th USING argument is a record, so the tuple descriptor of the -- returned unpinned record is deduced from the param return query execute 'select $12' using 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, row(1, 2); end; $$ language plpgsql; select * from plpgsql_check_function('dynamic_record_param()'); plpgsql_check_function ------------------------ (0 rows) drop function dynamic_record_param(); create function polymorphic_out_first(out b anyelement, a anyelement) as $$ begin b := a; end; $$ language plpgsql; create function call_polymorphic_out_first() returns void as $$ declare arg record; res record; begin select 1 as x, 2 as y into arg; res := polymorphic_out_first(arg); raise notice '%', res; end; $$ language plpgsql; select * from plpgsql_check_function('call_polymorphic_out_first()'); plpgsql_check_function ------------------------ (0 rows) drop function call_polymorphic_out_first(); drop function polymorphic_out_first(anyelement); create function memsafety_profiled_fx() returns int as $$ begin return 1; end; $$ language plpgsql; set plpgsql_check.profiler to on; begin; select memsafety_profiled_fx(); memsafety_profiled_fx ----------------------- 1 (1 row) select plpgsql_profiler_reset_all(); plpgsql_profiler_reset_all ---------------------------- (1 row) select memsafety_profiled_fx(); memsafety_profiled_fx ----------------------- 1 (1 row) select plpgsql_profiler_reset_all(); plpgsql_profiler_reset_all ---------------------------- (1 row) commit; begin; select memsafety_profiled_fx(); memsafety_profiled_fx ----------------------- 1 (1 row) select plpgsql_profiler_reset_all(); plpgsql_profiler_reset_all ---------------------------- (1 row) rollback; set plpgsql_check.profiler to off; drop function memsafety_profiled_fx(); create table memsafety_pt(a int); create function memsafety_pragma_tokens() returns void as $$ declare r record; begin -- "(", ")", ",", "[" and "]" are one character tokens perform plpgsql_check_pragma('table: memsafety_pt1(a int, b numeric(10,2), c int[])'); perform plpgsql_check_pragma('table: memsafety_pt2(like memsafety_pt)'); for r in execute 'select * from memsafety_pt1' loop raise notice '%', r.b; end loop; for r in execute 'select * from memsafety_pt2' loop raise notice '%', r.a; end loop; end; $$ language plpgsql; select * from plpgsql_check_function('memsafety_pragma_tokens()'); plpgsql_check_function ------------------------ (0 rows) drop function memsafety_pragma_tokens(); drop table memsafety_pt; create function memsafety_format_pct() returns void as $$ begin raise notice '%', format('abc%'); end; $$ language plpgsql; select * from plpgsql_check_function('memsafety_format_pct()'); plpgsql_check_function ---------------------------------------------------------- error:22023:3:RAISE:unterminated format() type specifier Query: format('abc%') -- ^ Hint: For a single "%%" use "%%%%". (4 rows) drop function memsafety_format_pct(); create function memsafety_format_pct2() returns void as $$ begin raise notice '%', format('abc%', 1); end; $$ language plpgsql; select * from plpgsql_check_function('memsafety_format_pct2()'); plpgsql_check_function ---------------------------------------------------------- error:22023:3:RAISE:unterminated format() type specifier Query: format('abc%', 1) -- ^ Hint: For a single "%%" use "%%%%". (4 rows) drop function memsafety_format_pct2(); create function memsafety_nextval_missing_rel() returns void as $$ begin perform nextval('4294967000'::regclass); end; $$ language plpgsql; select * from plpgsql_check_function('memsafety_nextval_missing_rel()'); plpgsql_check_function ------------------------------------------------------ error:42809:3:PERFORM:"4294967000" is not a sequence Query: SELECT nextval('4294967000'::regclass) -- ^ (3 rows) drop function memsafety_nextval_missing_rel(); create type memsafety_ct as (a int, b int); create function memsafety_case_composite() returns void as $$ declare v memsafety_ct; begin v := row(1,2)::memsafety_ct; case v when row(1,2)::memsafety_ct then raise notice 'one'; else raise notice 'other'; end case; end; $$ language plpgsql; select * from plpgsql_check_function('memsafety_case_composite()'); plpgsql_check_function ------------------------ (0 rows) select memsafety_case_composite(); NOTICE: one memsafety_case_composite -------------------------- (1 row) drop function memsafety_case_composite(); drop type memsafety_ct; -- the constant is assigned in a branch, so it must not be used afterwards create or replace function repro02_stale_const(flag bool) returns void as $$ declare s text; begin if flag then s := 'select * from repro02_missing'; end if; execute s; end; $$ language plpgsql; select * from plpgsql_check_function('repro02_stale_const(bool)'); plpgsql_check_function ------------------------ (0 rows) -- control: an unconditional assignment really is a constant and must be used create or replace function repro02_live_const() returns void as $$ declare s text; begin s := 'select * from repro02_missing'; execute s; end; $$ language plpgsql; select * from plpgsql_check_function('repro02_live_const()'); plpgsql_check_function ----------------------------------------------------------------- error:42P01:7:EXECUTE:relation "repro02_missing" does not exist Query: select * from repro02_missing -- ^ (3 rows) drop function repro02_stale_const(bool); drop function repro02_live_const(); set plpgsql_check.enable_tracer to on; set plpgsql_check.tracer to on; set plpgsql_check.tracer_test_mode to true; set plpgsql_check.tracer_verbosity to verbose; set client_min_messages to notice; -- the OUT parameters of a function are collected into an (unnamed) row datum, -- and RETURN traces that datum create or replace function repro03_out_params(out a int, out b text) as $$ begin a := 1; b := 'x'; return; end; $$ language plpgsql; select * from repro03_out_params(); NOTICE: #0 ->> start of function repro03_out_params() (oid=0, tnl=1) NOTICE: #0.1 2 --> start of statement block (tnl=1) NOTICE: #0.2 3 --> start of assignment a := 1 (tnl=1) NOTICE: #0.2 "a" => null NOTICE: #0.2 <-- end of assignment (elapsed time=0.010 ms) NOTICE: #0.2 "a" => '1' NOTICE: #0.3 4 --> start of assignment b := 'x' (tnl=1) NOTICE: #0.3 "b" => null NOTICE: #0.3 <-- end of assignment (elapsed time=0.010 ms) NOTICE: #0.3 "b" => 'x' NOTICE: #0.4 5 --> start of RETURN (tnl=1) NOTICE: #0.4 "(unnamed row)" => '(1,x)' NOTICE: #0.4 <-- end of RETURN (elapsed time=0.010 ms) NOTICE: #0.1 <-- end of statement block (elapsed time=0.010 ms) NOTICE: #0 <<- end of function repro03_out_params (elapsed time=0.010 ms) a | b ---+--- 1 | x (1 row) set plpgsql_check.tracer to off; set plpgsql_check.enable_tracer to off; drop function repro03_out_params(); create or replace function repro04_tracked_const() returns void as $$ declare str text; res int; begin str := 'constant'; execute 'select $40' into res using 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40; raise notice '%, %', str, res; end; $$ language plpgsql; select * from plpgsql_check_function('repro04_tracked_const()'); plpgsql_check_function ------------------------ (0 rows) create or replace function repro04_tracked_const() returns void as $$ declare str text; res int; begin str := 'constant'; execute 'select $41' into res using 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40; raise notice '%, %', str, res; end; $$ language plpgsql; select * from plpgsql_check_function('repro04_tracked_const()'); plpgsql_check_function ------------------------------------------------- error:42P02:8:EXECUTE:there is no parameter $41 Query: select $41 -- ^ (3 rows) -- (b) param_get_desc(): the tuple descriptor of the unpinned record returned by -- the dynamic query is deduced from a high numbered parameter create or replace function repro04_param_desc() returns setof record as $$ begin return query execute 'select $40' using 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, row(1, 2); end; $$ language plpgsql; select * from plpgsql_check_function('repro04_param_desc()'); plpgsql_check_function ------------------------ (0 rows) -- (c) plpgsql_check_assign_tupdesc_dno(): a query with no output column at all create table repro04_zero_columns(); create or replace function repro04_empty_tupdesc() returns int as $$ declare v int; begin select * from repro04_zero_columns into v; execute 'select * from repro04_zero_columns' into v; return v; end; $$ language plpgsql; select * from plpgsql_check_function('repro04_empty_tupdesc()'); plpgsql_check_function ----------------------------------------------------------------------- warning:00000:5:SQL statement:too few attributes for target variables Query: select * from repro04_zero_columns -- ^ Detail: There are more target variables than output columns in query. Hint: Check target variables in SELECT INTO statement. Context: at SQL statement to v variables warning:00000:6:EXECUTE:too few attributes for target variables Query: select * from repro04_zero_columns -- ^ Detail: There are more target variables than output columns in query. Hint: Check target variables in SELECT INTO statement. Context: at EXECUTE to v variables (12 rows) drop function repro04_tracked_const(); drop function repro04_param_desc(); drop function repro04_empty_tupdesc(); drop table repro04_zero_columns; set plpgsql_check.profiler to on; set plpgsql_check.use_lxcache to on; create or replace function repro07_ok() returns int as $$ begin return 1; end; $$ language plpgsql; create or replace function repro07_err() returns int as $$ begin raise exception 'boom'; end; $$ language plpgsql; -- control: an aborted function with no lxcache from an earlier call is fine begin; select repro07_err(); ERROR: boom CONTEXT: PL/pgSQL function repro07_err() line 3 at RAISE commit; -- the successful call creates the lxcache, the failing one leaves an execution -- context behind, and the cleanup of that context re-enters get_lxcache() begin; select repro07_ok(); repro07_ok ------------ 1 (1 row) select repro07_err(); ERROR: boom CONTEXT: PL/pgSQL function repro07_err() line 3 at RAISE commit; select 'survived' as result; result ---------- survived (1 row) -- the same happens on an explicit ROLLBACK begin; select repro07_ok(); repro07_ok ------------ 1 (1 row) select repro07_err(); ERROR: boom CONTEXT: PL/pgSQL function repro07_err() line 3 at RAISE rollback; select 'survived' as result; result ---------- survived (1 row) set plpgsql_check.profiler to off; drop function if exists repro07_ok(); drop function if exists repro07_err(); CREATE SCHEMA IF NOT EXISTS repro01; SET search_path = repro01, public; CREATE TABLE t_b (a int); -- The EXECUTE records t_b into cstate_dynsql.rel_oids, which is then thrown -- away. The static PERFORM records the very same OID again, this time into -- the caller's cstate->rel_oids. CREATE FUNCTION f_dup() RETURNS void AS $$ BEGIN EXECUTE 'select * from repro01.t_b'; PERFORM * FROM repro01.t_b; END $$ LANGUAGE plpgsql; \echo '### Finding 1: duplicate RELATION dependency (expect 1 row, bug yields 2)' ### Finding 1: duplicate RELATION dependency (expect 1 row, bug yields 2) SELECT type, schema, name FROM plpgsql_show_dependency_tb('repro01.f_dup()') ORDER BY type, name; type | schema | name ----------+---------+------ RELATION | repro01 | t_b (1 row) \echo '### Same query, counted:' ### Same query, counted: SELECT count(*) AS relation_rows_for_t_b FROM plpgsql_show_dependency_tb('repro01.f_dup()') WHERE type = 'RELATION' AND name = 't_b'; relation_rows_for_t_b ----------------------- 1 (1 row) -- Control: the purely static equivalent reports the relation exactly once. CREATE FUNCTION f_static() RETURNS void AS $$ BEGIN PERFORM * FROM repro01.t_b; PERFORM * FROM repro01.t_b; END $$ LANGUAGE plpgsql; \echo '### Control: static-only function reports t_b once' ### Control: static-only function reports t_b once SELECT count(*) AS relation_rows_for_t_b FROM plpgsql_show_dependency_tb('repro01.f_static()') WHERE type = 'RELATION' AND name = 't_b'; relation_rows_for_t_b ----------------------- 1 (1 row) DROP SCHEMA repro01 CASCADE; NOTICE: drop cascades to 3 other objects DETAIL: drop cascades to table t_b drop cascades to function f_dup() drop cascades to function f_static() CREATE SCHEMA IF NOT EXISTS repro04; SET search_path = repro04, public; SET plpgsql_check.tracer_test_mode = true; SET plpgsql_check.tracer = on; SET plpgsql_check.tracer_verbosity = verbose; SET client_min_messages = error; -- silence the trace output itself -- (1) exception at maximum depth, EMPTY handler: nothing inside the handler -- calls stmt_beg, so the stack is only unwound by stmt_end of the block. CREATE FUNCTION f_empty_handler() RETURNS int AS $$ DECLARE x int := 0; BEGIN FOR i IN 1..3 LOOP BEGIN RAISE EXCEPTION 'boom'; EXCEPTION WHEN others THEN END; END LOOP; RETURN x; END $$ LANGUAGE plpgsql; -- (2) nested handlers, each re-raising: unwinds several levels per stmt_beg. CREATE FUNCTION f_nested_reraise() RETURNS int AS $$ BEGIN BEGIN BEGIN RAISE EXCEPTION 'inner'; EXCEPTION WHEN others THEN RAISE EXCEPTION 'middle'; END; EXCEPTION WHEN others THEN PERFORM 1; END; RETURN 1; END $$ LANGUAGE plpgsql; -- (3) exception thrown from deep inside loops and caught at the very top. CREATE FUNCTION f_deep_escape() RETURNS int AS $$ DECLARE j int; BEGIN BEGIN FOR i IN 1..2 LOOP FOREACH j IN ARRAY ARRAY[1,2] LOOP WHILE true LOOP IF j = 2 THEN RAISE EXCEPTION 'deep'; END IF; EXIT; END LOOP; END LOOP; END LOOP; EXCEPTION WHEN others THEN RETURN -1; END; RETURN 0; END $$ LANGUAGE plpgsql; -- (4) handler whose FIRST statement is itself a block (repair path must match -- the block's parent, not the failing statement's parent). CREATE FUNCTION f_handler_block() RETURNS int AS $$ BEGIN BEGIN BEGIN RAISE EXCEPTION 'x'; EXCEPTION WHEN others THEN BEGIN PERFORM 1; END; END; END; RETURN 1; END $$ LANGUAGE plpgsql; -- (5) repeated entry/exit so any leaked stack slot accumulates across calls. CREATE FUNCTION f_repeat() RETURNS int AS $$ DECLARE n int := 0; BEGIN FOR i IN 1..50 LOOP BEGIN BEGIN RAISE EXCEPTION 'e%', i; EXCEPTION WHEN others THEN END; n := n + 1; EXCEPTION WHEN others THEN n := n - 1; END; END LOOP; RETURN n; END $$ LANGUAGE plpgsql; -- (6) exception crossing a function boundary (separate plugin_info per estate). CREATE FUNCTION f_callee() RETURNS int AS $$ BEGIN BEGIN RAISE EXCEPTION 'from callee'; END; END $$ LANGUAGE plpgsql; CREATE FUNCTION f_caller() RETURNS int AS $$ BEGIN BEGIN FOR i IN 1..2 LOOP PERFORM f_callee(); END LOOP; EXCEPTION WHEN others THEN RETURN -1; END; RETURN 0; END $$ LANGUAGE plpgsql; RESET client_min_messages; \echo '### Driving stmt_beg/stmt_end imbalance patterns under the tracer' ### Driving stmt_beg/stmt_end imbalance patterns under the tracer SET client_min_messages = error; SELECT f_empty_handler(); f_empty_handler ----------------- 0 (1 row) SELECT f_nested_reraise(); f_nested_reraise ------------------ 1 (1 row) SELECT f_deep_escape(); f_deep_escape --------------- -1 (1 row) SELECT f_handler_block(); f_handler_block ----------------- 1 (1 row) SELECT f_repeat(); f_repeat ---------- 50 (1 row) SELECT f_caller(); f_caller ---------- -1 (1 row) -- run them all again, now with the profiler also attached (two active plugins, -- which additionally exercises the duplicated palloc in func_setup) SET plpgsql_check.profiler = on; SELECT f_empty_handler(); f_empty_handler ----------------- 0 (1 row) SELECT f_nested_reraise(); f_nested_reraise ------------------ 1 (1 row) SELECT f_deep_escape(); f_deep_escape --------------- -1 (1 row) SELECT f_handler_block(); f_handler_block ----------------- 1 (1 row) SELECT f_repeat(); f_repeat ---------- 50 (1 row) SELECT f_caller(); f_caller ---------- -1 (1 row) RESET client_min_messages; \echo '### Survived: no assertion failure and the backend is still alive' ### Survived: no assertion failure and the backend is still alive SELECT 'still alive' AS status; status ------------- still alive (1 row) DROP SCHEMA repro04 CASCADE; NOTICE: drop cascades to 7 other objects DETAIL: drop cascades to function f_empty_handler() drop cascades to function f_nested_reraise() drop cascades to function f_deep_escape() drop cascades to function f_handler_block() drop cascades to function f_repeat() drop cascades to function f_callee() drop cascades to function f_caller() SELECT format('%2$s', 'a'); ERROR: too few arguments for format() CREATE OR REPLACE FUNCTION repro05_a() RETURNS void AS $$ BEGIN RAISE NOTICE '%', format('%2$s', 'a'); END $$ LANGUAGE plpgsql; -- buggy: reports nothing -- correct: error:22023:...:RAISE:too few arguments for format() SELECT * FROM plpgsql_check_function('repro05_a()'); plpgsql_check_function ---------------------------------------------------- error:22023:3:RAISE:too few arguments for format() Query: format('%2$s', 'a') -- ^ (3 rows) -- for comparison, the same mistake without an explicit position IS reported CREATE OR REPLACE FUNCTION repro05_a2() RETURNS void AS $$ BEGIN RAISE NOTICE '%', format('%s %s', 'a'); END $$ LANGUAGE plpgsql; SELECT * FROM plpgsql_check_function('repro05_a2()'); plpgsql_check_function ---------------------------------------------------- error:22023:3:RAISE:too few arguments for format() Query: format('%s %s', 'a') -- ^ (3 rows) ---------------------------------------------------------------------- -- part B: missed SQL-injection warning ---------------------------------------------------------------------- CREATE OR REPLACE FUNCTION repro05_b(p text) RETURNS void AS $$ BEGIN EXECUTE format('SELECT %1$s', p); END $$ LANGUAGE plpgsql; -- buggy: reports nothing -- correct: security:00000:...:EXECUTE:text type variable is not sanitized SELECT * FROM plpgsql_check_function('repro05_b(text)', security_warnings => true); plpgsql_check_function ----------------------------------------------------------------------------- security:00000:3:EXECUTE:text type variable is not sanitized Query: format('SELECT %1$s', p) -- ^ Detail: The EXECUTE expression is SQL injection vulnerable. Hint: Use quote_ident, quote_literal or format function to secure variable. (5 rows) -- for comparison, the same code without an explicit position IS reported CREATE OR REPLACE FUNCTION repro05_b2(p text) RETURNS void AS $$ BEGIN EXECUTE format('SELECT %s', p); END $$ LANGUAGE plpgsql; SELECT * FROM plpgsql_check_function('repro05_b2(text)', security_warnings => true); plpgsql_check_function ----------------------------------------------------------------------------- security:00000:3:EXECUTE:text type variable is not sanitized Query: format('SELECT %s', p) -- ^ Detail: The EXECUTE expression is SQL injection vulnerable. Hint: Use quote_ident, quote_literal or format function to secure variable. (5 rows) DROP SEQUENCE IF EXISTS repro02_seq; NOTICE: sequence "repro02_seq" does not exist, skipping CREATE SEQUENCE repro02_seq; -- 1st call returns a valid query, every later call returns NULL CREATE OR REPLACE FUNCTION repro02_gen() RETURNS text AS $$ BEGIN IF nextval('repro02_seq') = 1 THEN RETURN 'SELECT 1'; ELSE RETURN NULL; END IF; END $$ LANGUAGE plpgsql; CREATE OR REPLACE FUNCTION repro02_run() RETURNS void AS $$ BEGIN EXECUTE repro02_gen(); END $$ LANGUAGE plpgsql; -- PGC_USERSET SET plpgsql_check.profiler = on; -- boom SELECT repro02_run(); repro02_run ------------- (1 row) SET plpgsql_check.profiler = off; DROP SEQUENCE IF EXISTS repro03_seq; NOTICE: sequence "repro03_seq" does not exist, skipping CREATE SEQUENCE repro03_seq; ---------------------------------------------------------------------- -- part A: the side effect of the query-text expression happens twice ---------------------------------------------------------------------- CREATE OR REPLACE FUNCTION repro03_gen() RETURNS text AS $$ BEGIN PERFORM nextval('repro03_seq'); RETURN 'SELECT 1'; END $$ LANGUAGE plpgsql; CREATE OR REPLACE FUNCTION repro03_run() RETURNS void AS $$ BEGIN EXECUTE repro03_gen(); END $$ LANGUAGE plpgsql; SET plpgsql_check.profiler = off; SELECT setval('repro03_seq', 1, false); setval -------- 1 (1 row) SELECT repro03_run(); repro03_run ------------- (1 row) -- correct: 1 SELECT currval('repro03_seq') AS "nextval calls with profiler off"; nextval calls with profiler off --------------------------------- 1 (1 row) SET plpgsql_check.profiler = on; SELECT setval('repro03_seq', 1, false); setval -------- 1 (1 row) SELECT repro03_run(); repro03_run ------------- (1 row) -- buggy: 2 - the profiler evaluated repro03_gen() a second time SELECT currval('repro03_seq') AS "nextval calls with profiler on"; nextval calls with profiler on -------------------------------- 1 (1 row) -- Use-after-free and NULL-pointer dereference in prepare_plan()'s retry loop. CREATE TABLE uaf_t (a int); CREATE FUNCTION uaf_f() RETURNS int LANGUAGE plpgsql AS $$ DECLARE v int; BEGIN SELECT count(*) INTO v FROM uaf_t; RETURN v; END; $$; -- Execute the function so PL/pgSQL caches its run-time plan in expr->plan. SELECT uaf_f(); uaf_f ------- 0 (1 row) -- Invalidate that cached plan with ordinary DDL. ALTER TABLE uaf_t ADD COLUMN b int; -- should not crash SELECT count(*) >= 0 FROM plpgsql_check_function('uaf_f()'); ?column? ---------- t (1 row) DROP TABLE uaf_t; DROP FUNCTION uaf_f; CREATE TABLE tt_tab (a int); CREATE FUNCTION tt_trg() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN RETURN NULL; END; $$; DO $$ BEGIN PERFORM * FROM plpgsql_check_function('tt_trg()', 'tt_tab', oldtable => 'dup', newtable => 'dup'); EXCEPTION WHEN OTHERS THEN -- A clean error is the fixed behaviour; we only require that the backend -- did not crash. NULL; END; $$; DROP TABLE IF EXISTS tt_tab; DROP FUNCTION tt_trg(); CREATE TYPE empty_comp AS (); CREATE FUNCTION oob_if() RETURNS int LANGUAGE plpgsql AS $$ DECLARE r empty_comp; BEGIN IF r THEN RETURN 1; END IF; RETURN 0; END; $$; CREATE FUNCTION oob_ret() RETURNS int LANGUAGE plpgsql AS $$ DECLARE r empty_comp; BEGIN RETURN (r); END; $$; SELECT * FROM plpgsql_check_function('oob_if()', fatal_errors => false); plpgsql_check_function ---------------------------------------------- error:42804:5:IF:unexpected non-scalar value (1 row) SELECT * FROM plpgsql_check_function('oob_ret()', fatal_errors => false); plpgsql_check_function ----------------------------------------------------------------------------------- error:42804:5:RETURN:cannot return composite value from function returning number (1 row) DROP FUNCTION oob_if; DROP FUNCTION oob_ret; DROP TYPE empty_comp; okbob-plpgsql_check-800db79/expected/plpgsql_check_nocrash_1.out000066400000000000000000001044011526744060500251010ustar00rootroot00000000000000load 'plpgsql'; create extension if not exists plpgsql_check; NOTICE: extension "plpgsql_check" already exists, skipping set client_min_messages to notice; set plpgsql_check.regress_test_mode = true; create table zero_columns(); create function assign_empty_tupdesc() returns int as $$ declare v int; begin select * from zero_columns into v; execute 'select * from zero_columns' into v; return v; end; $$ language plpgsql; select * from plpgsql_check_function('assign_empty_tupdesc()'); plpgsql_check_function ----------------------------------------------------------------------- warning:00000:5:SQL statement:too few attributes for target variables Query: select * from zero_columns -- ^ Detail: There are more target variables than output columns in query. Hint: Check target variables in SELECT INTO statement. Context: at SQL statement to v variables warning:00000:6:EXECUTE:too few attributes for target variables Query: select * from zero_columns -- ^ Detail: There are more target variables than output columns in query. Hint: Check target variables in SELECT INTO statement. Context: at EXECUTE to v variables (12 rows) drop function assign_empty_tupdesc(); drop table zero_columns; create function public.int_eq(int, int) returns bool as $$ select $1 = $2; $$ language sql; create operator public.=== ( leftarg = int, rightarg = int, procedure = public.int_eq ); create function use_custom_operator() returns bool as $$ begin return 1 operator(public.===) 2; end; $$ language plpgsql; select type, schema, name, params from plpgsql_show_dependency_tb('use_custom_operator()'); type | schema | name | params ----------+--------+------+------------------- OPERATOR | public | === | (integer,integer) (1 row) drop function use_custom_operator(); drop function public.int_eq cascade; NOTICE: drop cascades to operator ===(integer,integer) create function tracked_const_oob() returns void as $$ declare str text; res int; begin -- assign a string constant, so the strconstvars array is allocated str := 'constant'; -- the dynamic query has much more params than the function has datums execute 'select $40' into res using 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 'x'::text; raise notice '%, %', str, res; end; $$ language plpgsql; select * from plpgsql_check_function('tracked_const_oob()'); plpgsql_check_function -------------------------------------------------------------------------------------- warning:42804:10:EXECUTE:target type is different type than source type Detail: cast "text" value to "integer" type Hint: The input expression type does not have an assignment cast to the target type. Context: at EXECUTE to variable "res" declared on line 4 (4 rows) drop function tracked_const_oob(); create table returned_expr_tab(a int); create function returned_expr_null_node() returns setof refcursor as $$ begin return query select a::text::refcursor from returned_expr_tab; end; $$ language plpgsql; select * from plpgsql_check_function('returned_expr_null_node()', compatibility_warnings => true); plpgsql_check_function ------------------------------------------------------------------------------------- compatibility:00000:3:RETURN QUERY:obsolete setting of refcursor or cursor variable Detail: Internal name of cursor should not be specified by users. (2 rows) drop function returned_expr_null_node(); drop table returned_expr_tab; create table rvalue_tab(a int); create function rvalue_null_node() returns void as $$ declare c refcursor; begin c := (select a::text::refcursor from rvalue_tab limit 1); raise notice '%', c; end; $$ language plpgsql; select * from plpgsql_check_function('rvalue_null_node()', compatibility_warnings => true); plpgsql_check_function ------------------------ (0 rows) drop function rvalue_null_node(); drop table rvalue_tab; create table assert_column_tab(a int); create function pragma_assert_column() returns void as $$ declare tname text default 'assert_column_tab'; begin -- ASSERT-COLUMN expects two variables, only one is passed perform plpgsql_check_pragma('assert-column: tname'); raise notice '%', tname; end; $$ language plpgsql; select * from plpgsql_check_function('pragma_assert_column()'); WARNING: "assert-column" on line 6 is not processed. DETAIL: too few variables for "assert-column" pragma plpgsql_check_function ------------------------ (0 rows) drop function pragma_assert_column(); drop table assert_column_tab; create function pragma_sequence_prefix() returns void as $$ begin -- 6 chars long pragma name, the sequence handler reads 3 chars behind -- the end of the string perform plpgsql_check_pragma('SEQUEN'); end; $$ language plpgsql; select * from plpgsql_check_function('pragma_sequence_prefix()'); WARNING: unsupported pragma: SEQUEN plpgsql_check_function ------------------------ (0 rows) drop function pragma_sequence_prefix(); create function pragma_sequence_misdispatch() returns void as $$ begin perform plpgsql_check_pragma('SEQUENCE_FOO'); end; $$ language plpgsql; select * from plpgsql_check_function('pragma_sequence_misdispatch()'); WARNING: unsupported pragma: SEQUENCE_FOO plpgsql_check_function ------------------------ (0 rows) drop function pragma_sequence_misdispatch(); create function profiled_function() returns int as $$ begin return 1; end; $$ language plpgsql; set plpgsql_check.profiler to on; begin; select profiled_function(); profiled_function ------------------- 1 (1 row) select plpgsql_profiler_reset_all(); plpgsql_profiler_reset_all ---------------------------- (1 row) commit; set plpgsql_check.profiler to off; drop function profiled_function(); do $$ declare src text := ''; begin for i in 1..3000 loop src := src || 'perform 1;'; end loop; execute format('create or replace function too_many_statements() returns void as $q$ begin if false then %s end if; end $q$ language plpgsql', src); end; $$; set plpgsql_check.profiler to off; drop function too_many_statements(); create function dynamic_query_with_params(p int) returns void as $$ begin execute 'select 1 where $1 > 0' using p; end; $$ language plpgsql; set plpgsql_check.profiler to on; select dynamic_query_with_params(1); dynamic_query_with_params --------------------------- (1 row) set plpgsql_check.profiler to off; drop function dynamic_query_with_params(int); create function case_over_record() returns int as $$ declare r record; begin case r when null then return 1; else return 2; end case; end; $$ language plpgsql; select * from plpgsql_check_function('case_over_record()'); plpgsql_check_function ------------------------ (0 rows) drop function case_over_record(); create table dynsql_tab(a int); create function dynamic_sql_forms(tname text) returns void as $$ declare query text; begin execute 'select * from dynsql_tab'; execute format('select * from %I', tname); query := 'select * from ' || quote_ident(tname); execute query; end; $$ language plpgsql; select * from plpgsql_check_function('dynamic_sql_forms(text)', all_warnings => true); plpgsql_check_function --------------------------------------------------------------------------- performance:00000:5:EXECUTE:immutable expression without parameters found Detail: the EXECUTE command is not necessary probably Hint: Don't use dynamic SQL when you can use static SQL. (3 rows) drop function dynamic_sql_forms(text); drop table dynsql_tab; create function uninitialized_is_mp(v text) returns void as $$ begin -- the %L placeholder makes the query non constant, so it is checked in a -- subtransaction, and the check fails because the table does not exist execute format('select * from missing_table where a = %L', v); end; $$ language plpgsql; select * from plpgsql_check_function('uninitialized_is_mp(text)', all_warnings => true); plpgsql_check_function ------------------------ (0 rows) drop function uninitialized_is_mp(text); create table traced_tab(a int); insert into traced_tab values(1); create or replace function traced_trg_func() returns trigger as $$ begin return old; end; $$ language plpgsql; create trigger traced_trg before delete on traced_tab for each row execute procedure traced_trg_func(); set plpgsql_check.enable_tracer to on; set plpgsql_check.tracer to on; set plpgsql_check.tracer_test_mode to true; delete from traced_tab; NOTICE: #0 ->> start of function traced_trg_func() (oid=0, tnl=1) NOTICE: #0 triggered by before row delete trigger NOTICE: #0 "old" => '(1)' NOTICE: #0 <<- end of function traced_trg_func (elapsed time=0.010 ms) set plpgsql_check.tracer to off; set plpgsql_check.enable_tracer to off; drop table traced_tab cascade; drop function traced_trg_func(); create function failing_assert() returns void as $$ declare r record; begin select 1 as x into r; -- the reference to a record field creates a RECFIELD datum assert r.x = 1; end; $$ language plpgsql; set plpgsql_check.enable_tracer to on; set plpgsql_check.tracer to on; set plpgsql_check.trace_assert to on; set plpgsql_check.tracer_test_mode to true; select failing_assert(); NOTICE: #0 ->> start of function failing_assert() (oid=0, tnl=1) NOTICE: PLpgSQL assert expression (r.x = 1) on line 7 of failing_assert() is true NOTICE: #0 <<- end of function failing_assert (elapsed time=0.010 ms) failing_assert ---------------- (1 row) set plpgsql_check.trace_assert to off; set plpgsql_check.tracer to off; set plpgsql_check.enable_tracer to off; drop function failing_assert(); create function dynamic_record_param() returns setof record as $$ begin -- the 12th USING argument is a record, so the tuple descriptor of the -- returned unpinned record is deduced from the param return query execute 'select $12' using 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, row(1, 2); end; $$ language plpgsql; select * from plpgsql_check_function('dynamic_record_param()'); plpgsql_check_function ------------------------ (0 rows) drop function dynamic_record_param(); create function polymorphic_out_first(out b anyelement, a anyelement) as $$ begin b := a; end; $$ language plpgsql; create function call_polymorphic_out_first() returns void as $$ declare arg record; res record; begin select 1 as x, 2 as y into arg; res := polymorphic_out_first(arg); raise notice '%', res; end; $$ language plpgsql; select * from plpgsql_check_function('call_polymorphic_out_first()'); plpgsql_check_function ------------------------ (0 rows) drop function call_polymorphic_out_first(); drop function polymorphic_out_first(anyelement); create function memsafety_profiled_fx() returns int as $$ begin return 1; end; $$ language plpgsql; set plpgsql_check.profiler to on; begin; select memsafety_profiled_fx(); memsafety_profiled_fx ----------------------- 1 (1 row) select plpgsql_profiler_reset_all(); plpgsql_profiler_reset_all ---------------------------- (1 row) select memsafety_profiled_fx(); memsafety_profiled_fx ----------------------- 1 (1 row) select plpgsql_profiler_reset_all(); plpgsql_profiler_reset_all ---------------------------- (1 row) commit; begin; select memsafety_profiled_fx(); memsafety_profiled_fx ----------------------- 1 (1 row) select plpgsql_profiler_reset_all(); plpgsql_profiler_reset_all ---------------------------- (1 row) rollback; set plpgsql_check.profiler to off; drop function memsafety_profiled_fx(); create table memsafety_pt(a int); create function memsafety_pragma_tokens() returns void as $$ declare r record; begin -- "(", ")", ",", "[" and "]" are one character tokens perform plpgsql_check_pragma('table: memsafety_pt1(a int, b numeric(10,2), c int[])'); perform plpgsql_check_pragma('table: memsafety_pt2(like memsafety_pt)'); for r in execute 'select * from memsafety_pt1' loop raise notice '%', r.b; end loop; for r in execute 'select * from memsafety_pt2' loop raise notice '%', r.a; end loop; end; $$ language plpgsql; select * from plpgsql_check_function('memsafety_pragma_tokens()'); plpgsql_check_function ------------------------ (0 rows) drop function memsafety_pragma_tokens(); drop table memsafety_pt; create function memsafety_format_pct() returns void as $$ begin raise notice '%', format('abc%'); end; $$ language plpgsql; select * from plpgsql_check_function('memsafety_format_pct()'); plpgsql_check_function ---------------------------------------------------------- error:22023:3:RAISE:unterminated format() type specifier Query: format('abc%') -- ^ Hint: For a single "%%" use "%%%%". (4 rows) drop function memsafety_format_pct(); create function memsafety_format_pct2() returns void as $$ begin raise notice '%', format('abc%', 1); end; $$ language plpgsql; select * from plpgsql_check_function('memsafety_format_pct2()'); plpgsql_check_function ---------------------------------------------------------- error:22023:3:RAISE:unterminated format() type specifier Query: format('abc%', 1) -- ^ Hint: For a single "%%" use "%%%%". (4 rows) drop function memsafety_format_pct2(); create function memsafety_nextval_missing_rel() returns void as $$ begin perform nextval('4294967000'::regclass); end; $$ language plpgsql; select * from plpgsql_check_function('memsafety_nextval_missing_rel()'); plpgsql_check_function ------------------------------------------------------ error:42809:3:PERFORM:"4294967000" is not a sequence Query: SELECT nextval('4294967000'::regclass) -- ^ (3 rows) drop function memsafety_nextval_missing_rel(); create type memsafety_ct as (a int, b int); create function memsafety_case_composite() returns void as $$ declare v memsafety_ct; begin v := row(1,2)::memsafety_ct; case v when row(1,2)::memsafety_ct then raise notice 'one'; else raise notice 'other'; end case; end; $$ language plpgsql; select * from plpgsql_check_function('memsafety_case_composite()'); plpgsql_check_function ------------------------ (0 rows) select memsafety_case_composite(); NOTICE: one memsafety_case_composite -------------------------- (1 row) drop function memsafety_case_composite(); drop type memsafety_ct; -- the constant is assigned in a branch, so it must not be used afterwards create or replace function repro02_stale_const(flag bool) returns void as $$ declare s text; begin if flag then s := 'select * from repro02_missing'; end if; execute s; end; $$ language plpgsql; select * from plpgsql_check_function('repro02_stale_const(bool)'); plpgsql_check_function ------------------------ (0 rows) -- control: an unconditional assignment really is a constant and must be used create or replace function repro02_live_const() returns void as $$ declare s text; begin s := 'select * from repro02_missing'; execute s; end; $$ language plpgsql; select * from plpgsql_check_function('repro02_live_const()'); plpgsql_check_function ----------------------------------------------------------------- error:42P01:7:EXECUTE:relation "repro02_missing" does not exist Query: select * from repro02_missing -- ^ (3 rows) drop function repro02_stale_const(bool); drop function repro02_live_const(); set plpgsql_check.enable_tracer to on; set plpgsql_check.tracer to on; set plpgsql_check.tracer_test_mode to true; set plpgsql_check.tracer_verbosity to verbose; set client_min_messages to notice; -- the OUT parameters of a function are collected into an (unnamed) row datum, -- and RETURN traces that datum create or replace function repro03_out_params(out a int, out b text) as $$ begin a := 1; b := 'x'; return; end; $$ language plpgsql; select * from repro03_out_params(); NOTICE: #0 ->> start of function repro03_out_params() (oid=0, tnl=1) NOTICE: #0.1 2 --> start of statement block (tnl=1) NOTICE: #0.2 3 --> start of assignment a := 1 (tnl=1) NOTICE: #0.2 "a" => null NOTICE: #0.2 <-- end of assignment (elapsed time=0.010 ms) NOTICE: #0.2 "a" => '1' NOTICE: #0.3 4 --> start of assignment b := 'x' (tnl=1) NOTICE: #0.3 "b" => null NOTICE: #0.3 <-- end of assignment (elapsed time=0.010 ms) NOTICE: #0.3 "b" => 'x' NOTICE: #0.4 5 --> start of RETURN (tnl=1) NOTICE: #0.4 "(unnamed row)" => '(1,x)' NOTICE: #0.4 <-- end of RETURN (elapsed time=0.010 ms) NOTICE: #0.1 <-- end of statement block (elapsed time=0.010 ms) NOTICE: #0 <<- end of function repro03_out_params (elapsed time=0.010 ms) a | b ---+--- 1 | x (1 row) set plpgsql_check.tracer to off; set plpgsql_check.enable_tracer to off; drop function repro03_out_params(); create or replace function repro04_tracked_const() returns void as $$ declare str text; res int; begin str := 'constant'; execute 'select $40' into res using 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40; raise notice '%, %', str, res; end; $$ language plpgsql; select * from plpgsql_check_function('repro04_tracked_const()'); plpgsql_check_function ------------------------ (0 rows) create or replace function repro04_tracked_const() returns void as $$ declare str text; res int; begin str := 'constant'; execute 'select $41' into res using 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40; raise notice '%, %', str, res; end; $$ language plpgsql; select * from plpgsql_check_function('repro04_tracked_const()'); plpgsql_check_function ------------------------------------------------- error:42P02:8:EXECUTE:there is no parameter $41 Query: select $41 -- ^ (3 rows) -- (b) param_get_desc(): the tuple descriptor of the unpinned record returned by -- the dynamic query is deduced from a high numbered parameter create or replace function repro04_param_desc() returns setof record as $$ begin return query execute 'select $40' using 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, row(1, 2); end; $$ language plpgsql; select * from plpgsql_check_function('repro04_param_desc()'); plpgsql_check_function ------------------------ (0 rows) -- (c) plpgsql_check_assign_tupdesc_dno(): a query with no output column at all create table repro04_zero_columns(); create or replace function repro04_empty_tupdesc() returns int as $$ declare v int; begin select * from repro04_zero_columns into v; execute 'select * from repro04_zero_columns' into v; return v; end; $$ language plpgsql; select * from plpgsql_check_function('repro04_empty_tupdesc()'); plpgsql_check_function ----------------------------------------------------------------------- warning:00000:5:SQL statement:too few attributes for target variables Query: select * from repro04_zero_columns -- ^ Detail: There are more target variables than output columns in query. Hint: Check target variables in SELECT INTO statement. Context: at SQL statement to v variables warning:00000:6:EXECUTE:too few attributes for target variables Query: select * from repro04_zero_columns -- ^ Detail: There are more target variables than output columns in query. Hint: Check target variables in SELECT INTO statement. Context: at EXECUTE to v variables (12 rows) drop function repro04_tracked_const(); drop function repro04_param_desc(); drop function repro04_empty_tupdesc(); drop table repro04_zero_columns; set plpgsql_check.profiler to on; set plpgsql_check.use_lxcache to on; create or replace function repro07_ok() returns int as $$ begin return 1; end; $$ language plpgsql; create or replace function repro07_err() returns int as $$ begin raise exception 'boom'; end; $$ language plpgsql; -- control: an aborted function with no lxcache from an earlier call is fine begin; select repro07_err(); ERROR: boom CONTEXT: PL/pgSQL function repro07_err() line 3 at RAISE commit; -- the successful call creates the lxcache, the failing one leaves an execution -- context behind, and the cleanup of that context re-enters get_lxcache() begin; select repro07_ok(); repro07_ok ------------ 1 (1 row) select repro07_err(); ERROR: boom CONTEXT: PL/pgSQL function repro07_err() line 3 at RAISE commit; select 'survived' as result; result ---------- survived (1 row) -- the same happens on an explicit ROLLBACK begin; select repro07_ok(); repro07_ok ------------ 1 (1 row) select repro07_err(); ERROR: boom CONTEXT: PL/pgSQL function repro07_err() line 3 at RAISE rollback; select 'survived' as result; result ---------- survived (1 row) set plpgsql_check.profiler to off; drop function if exists repro07_ok(); drop function if exists repro07_err(); CREATE SCHEMA IF NOT EXISTS repro01; SET search_path = repro01, public; CREATE TABLE t_b (a int); -- The EXECUTE records t_b into cstate_dynsql.rel_oids, which is then thrown -- away. The static PERFORM records the very same OID again, this time into -- the caller's cstate->rel_oids. CREATE FUNCTION f_dup() RETURNS void AS $$ BEGIN EXECUTE 'select * from repro01.t_b'; PERFORM * FROM repro01.t_b; END $$ LANGUAGE plpgsql; \echo '### Finding 1: duplicate RELATION dependency (expect 1 row, bug yields 2)' ### Finding 1: duplicate RELATION dependency (expect 1 row, bug yields 2) SELECT type, schema, name FROM plpgsql_show_dependency_tb('repro01.f_dup()') ORDER BY type, name; type | schema | name ----------+---------+------ RELATION | repro01 | t_b (1 row) \echo '### Same query, counted:' ### Same query, counted: SELECT count(*) AS relation_rows_for_t_b FROM plpgsql_show_dependency_tb('repro01.f_dup()') WHERE type = 'RELATION' AND name = 't_b'; relation_rows_for_t_b ----------------------- 1 (1 row) -- Control: the purely static equivalent reports the relation exactly once. CREATE FUNCTION f_static() RETURNS void AS $$ BEGIN PERFORM * FROM repro01.t_b; PERFORM * FROM repro01.t_b; END $$ LANGUAGE plpgsql; \echo '### Control: static-only function reports t_b once' ### Control: static-only function reports t_b once SELECT count(*) AS relation_rows_for_t_b FROM plpgsql_show_dependency_tb('repro01.f_static()') WHERE type = 'RELATION' AND name = 't_b'; relation_rows_for_t_b ----------------------- 1 (1 row) DROP SCHEMA repro01 CASCADE; NOTICE: drop cascades to 3 other objects DETAIL: drop cascades to table t_b drop cascades to function f_dup() drop cascades to function f_static() CREATE SCHEMA IF NOT EXISTS repro04; SET search_path = repro04, public; SET plpgsql_check.tracer_test_mode = true; SET plpgsql_check.tracer = on; SET plpgsql_check.tracer_verbosity = verbose; SET client_min_messages = error; -- silence the trace output itself -- (1) exception at maximum depth, EMPTY handler: nothing inside the handler -- calls stmt_beg, so the stack is only unwound by stmt_end of the block. CREATE FUNCTION f_empty_handler() RETURNS int AS $$ DECLARE x int := 0; BEGIN FOR i IN 1..3 LOOP BEGIN RAISE EXCEPTION 'boom'; EXCEPTION WHEN others THEN END; END LOOP; RETURN x; END $$ LANGUAGE plpgsql; -- (2) nested handlers, each re-raising: unwinds several levels per stmt_beg. CREATE FUNCTION f_nested_reraise() RETURNS int AS $$ BEGIN BEGIN BEGIN RAISE EXCEPTION 'inner'; EXCEPTION WHEN others THEN RAISE EXCEPTION 'middle'; END; EXCEPTION WHEN others THEN PERFORM 1; END; RETURN 1; END $$ LANGUAGE plpgsql; -- (3) exception thrown from deep inside loops and caught at the very top. CREATE FUNCTION f_deep_escape() RETURNS int AS $$ DECLARE j int; BEGIN BEGIN FOR i IN 1..2 LOOP FOREACH j IN ARRAY ARRAY[1,2] LOOP WHILE true LOOP IF j = 2 THEN RAISE EXCEPTION 'deep'; END IF; EXIT; END LOOP; END LOOP; END LOOP; EXCEPTION WHEN others THEN RETURN -1; END; RETURN 0; END $$ LANGUAGE plpgsql; -- (4) handler whose FIRST statement is itself a block (repair path must match -- the block's parent, not the failing statement's parent). CREATE FUNCTION f_handler_block() RETURNS int AS $$ BEGIN BEGIN BEGIN RAISE EXCEPTION 'x'; EXCEPTION WHEN others THEN BEGIN PERFORM 1; END; END; END; RETURN 1; END $$ LANGUAGE plpgsql; -- (5) repeated entry/exit so any leaked stack slot accumulates across calls. CREATE FUNCTION f_repeat() RETURNS int AS $$ DECLARE n int := 0; BEGIN FOR i IN 1..50 LOOP BEGIN BEGIN RAISE EXCEPTION 'e%', i; EXCEPTION WHEN others THEN END; n := n + 1; EXCEPTION WHEN others THEN n := n - 1; END; END LOOP; RETURN n; END $$ LANGUAGE plpgsql; -- (6) exception crossing a function boundary (separate plugin_info per estate). CREATE FUNCTION f_callee() RETURNS int AS $$ BEGIN BEGIN RAISE EXCEPTION 'from callee'; END; END $$ LANGUAGE plpgsql; CREATE FUNCTION f_caller() RETURNS int AS $$ BEGIN BEGIN FOR i IN 1..2 LOOP PERFORM f_callee(); END LOOP; EXCEPTION WHEN others THEN RETURN -1; END; RETURN 0; END $$ LANGUAGE plpgsql; RESET client_min_messages; \echo '### Driving stmt_beg/stmt_end imbalance patterns under the tracer' ### Driving stmt_beg/stmt_end imbalance patterns under the tracer SET client_min_messages = error; SELECT f_empty_handler(); f_empty_handler ----------------- 0 (1 row) SELECT f_nested_reraise(); f_nested_reraise ------------------ 1 (1 row) SELECT f_deep_escape(); f_deep_escape --------------- -1 (1 row) SELECT f_handler_block(); f_handler_block ----------------- 1 (1 row) SELECT f_repeat(); f_repeat ---------- 50 (1 row) SELECT f_caller(); f_caller ---------- -1 (1 row) -- run them all again, now with the profiler also attached (two active plugins, -- which additionally exercises the duplicated palloc in func_setup) SET plpgsql_check.profiler = on; SELECT f_empty_handler(); f_empty_handler ----------------- 0 (1 row) SELECT f_nested_reraise(); f_nested_reraise ------------------ 1 (1 row) SELECT f_deep_escape(); f_deep_escape --------------- -1 (1 row) SELECT f_handler_block(); f_handler_block ----------------- 1 (1 row) SELECT f_repeat(); f_repeat ---------- 50 (1 row) SELECT f_caller(); f_caller ---------- -1 (1 row) RESET client_min_messages; \echo '### Survived: no assertion failure and the backend is still alive' ### Survived: no assertion failure and the backend is still alive SELECT 'still alive' AS status; status ------------- still alive (1 row) DROP SCHEMA repro04 CASCADE; NOTICE: drop cascades to 7 other objects DETAIL: drop cascades to function f_empty_handler() drop cascades to function f_nested_reraise() drop cascades to function f_deep_escape() drop cascades to function f_handler_block() drop cascades to function f_repeat() drop cascades to function f_callee() drop cascades to function f_caller() SELECT format('%2$s', 'a'); ERROR: too few arguments for format() CREATE OR REPLACE FUNCTION repro05_a() RETURNS void AS $$ BEGIN RAISE NOTICE '%', format('%2$s', 'a'); END $$ LANGUAGE plpgsql; -- buggy: reports nothing -- correct: error:22023:...:RAISE:too few arguments for format() SELECT * FROM plpgsql_check_function('repro05_a()'); plpgsql_check_function ---------------------------------------------------- error:22023:3:RAISE:too few arguments for format() Query: format('%2$s', 'a') -- ^ (3 rows) -- for comparison, the same mistake without an explicit position IS reported CREATE OR REPLACE FUNCTION repro05_a2() RETURNS void AS $$ BEGIN RAISE NOTICE '%', format('%s %s', 'a'); END $$ LANGUAGE plpgsql; SELECT * FROM plpgsql_check_function('repro05_a2()'); plpgsql_check_function ---------------------------------------------------- error:22023:3:RAISE:too few arguments for format() Query: format('%s %s', 'a') -- ^ (3 rows) ---------------------------------------------------------------------- -- part B: missed SQL-injection warning ---------------------------------------------------------------------- CREATE OR REPLACE FUNCTION repro05_b(p text) RETURNS void AS $$ BEGIN EXECUTE format('SELECT %1$s', p); END $$ LANGUAGE plpgsql; -- buggy: reports nothing -- correct: security:00000:...:EXECUTE:text type variable is not sanitized SELECT * FROM plpgsql_check_function('repro05_b(text)', security_warnings => true); plpgsql_check_function ----------------------------------------------------------------------------- security:00000:3:EXECUTE:text type variable is not sanitized Query: format('SELECT %1$s', p) -- ^ Detail: The EXECUTE expression is SQL injection vulnerable. Hint: Use quote_ident, quote_literal or format function to secure variable. (5 rows) -- for comparison, the same code without an explicit position IS reported CREATE OR REPLACE FUNCTION repro05_b2(p text) RETURNS void AS $$ BEGIN EXECUTE format('SELECT %s', p); END $$ LANGUAGE plpgsql; SELECT * FROM plpgsql_check_function('repro05_b2(text)', security_warnings => true); plpgsql_check_function ----------------------------------------------------------------------------- security:00000:3:EXECUTE:text type variable is not sanitized Query: format('SELECT %s', p) -- ^ Detail: The EXECUTE expression is SQL injection vulnerable. Hint: Use quote_ident, quote_literal or format function to secure variable. (5 rows) DROP SEQUENCE IF EXISTS repro02_seq; NOTICE: sequence "repro02_seq" does not exist, skipping CREATE SEQUENCE repro02_seq; -- 1st call returns a valid query, every later call returns NULL CREATE OR REPLACE FUNCTION repro02_gen() RETURNS text AS $$ BEGIN IF nextval('repro02_seq') = 1 THEN RETURN 'SELECT 1'; ELSE RETURN NULL; END IF; END $$ LANGUAGE plpgsql; CREATE OR REPLACE FUNCTION repro02_run() RETURNS void AS $$ BEGIN EXECUTE repro02_gen(); END $$ LANGUAGE plpgsql; -- PGC_USERSET SET plpgsql_check.profiler = on; -- boom SELECT repro02_run(); repro02_run ------------- (1 row) SET plpgsql_check.profiler = off; DROP SEQUENCE IF EXISTS repro03_seq; NOTICE: sequence "repro03_seq" does not exist, skipping CREATE SEQUENCE repro03_seq; ---------------------------------------------------------------------- -- part A: the side effect of the query-text expression happens twice ---------------------------------------------------------------------- CREATE OR REPLACE FUNCTION repro03_gen() RETURNS text AS $$ BEGIN PERFORM nextval('repro03_seq'); RETURN 'SELECT 1'; END $$ LANGUAGE plpgsql; CREATE OR REPLACE FUNCTION repro03_run() RETURNS void AS $$ BEGIN EXECUTE repro03_gen(); END $$ LANGUAGE plpgsql; SET plpgsql_check.profiler = off; SELECT setval('repro03_seq', 1, false); setval -------- 1 (1 row) SELECT repro03_run(); repro03_run ------------- (1 row) -- correct: 1 SELECT currval('repro03_seq') AS "nextval calls with profiler off"; nextval calls with profiler off --------------------------------- 1 (1 row) SET plpgsql_check.profiler = on; SELECT setval('repro03_seq', 1, false); setval -------- 1 (1 row) SELECT repro03_run(); repro03_run ------------- (1 row) -- buggy: 2 - the profiler evaluated repro03_gen() a second time SELECT currval('repro03_seq') AS "nextval calls with profiler on"; nextval calls with profiler on -------------------------------- 1 (1 row) -- Use-after-free and NULL-pointer dereference in prepare_plan()'s retry loop. CREATE TABLE uaf_t (a int); CREATE FUNCTION uaf_f() RETURNS int LANGUAGE plpgsql AS $$ DECLARE v int; BEGIN SELECT count(*) INTO v FROM uaf_t; RETURN v; END; $$; -- Execute the function so PL/pgSQL caches its run-time plan in expr->plan. SELECT uaf_f(); uaf_f ------- 0 (1 row) -- Invalidate that cached plan with ordinary DDL. ALTER TABLE uaf_t ADD COLUMN b int; -- should not crash SELECT count(*) >= 0 FROM plpgsql_check_function('uaf_f()'); ?column? ---------- t (1 row) DROP TABLE uaf_t; DROP FUNCTION uaf_f; CREATE TABLE tt_tab (a int); CREATE FUNCTION tt_trg() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN RETURN NULL; END; $$; DO $$ BEGIN PERFORM * FROM plpgsql_check_function('tt_trg()', 'tt_tab', oldtable => 'dup', newtable => 'dup'); EXCEPTION WHEN OTHERS THEN -- A clean error is the fixed behaviour; we only require that the backend -- did not crash. NULL; END; $$; DROP TABLE IF EXISTS tt_tab; DROP FUNCTION tt_trg(); CREATE TYPE empty_comp AS (); CREATE FUNCTION oob_if() RETURNS int LANGUAGE plpgsql AS $$ DECLARE r empty_comp; BEGIN IF r THEN RETURN 1; END IF; RETURN 0; END; $$; CREATE FUNCTION oob_ret() RETURNS int LANGUAGE plpgsql AS $$ DECLARE r empty_comp; BEGIN RETURN (r); END; $$; SELECT * FROM plpgsql_check_function('oob_if()', fatal_errors => false); plpgsql_check_function ---------------------------------------------- error:42804:5:IF:unexpected non-scalar value (1 row) SELECT * FROM plpgsql_check_function('oob_ret()', fatal_errors => false); plpgsql_check_function ----------------------------------------------------------------------------------- error:42804:5:RETURN:cannot return composite value from function returning number (1 row) DROP FUNCTION oob_if; DROP FUNCTION oob_ret; DROP TYPE empty_comp; okbob-plpgsql_check-800db79/expected/plpgsql_check_nocrash_unstable_01.out000066400000000000000000000035211526744060500270570ustar00rootroot00000000000000load 'plpgsql'; create extension if not exists plpgsql_check; NOTICE: extension "plpgsql_check" already exists, skipping load 'plpgsql_check'; set client_min_messages to notice; set plpgsql_check.regress_test_mode = true; set plpgsql_check.enable_tracer to on; set plpgsql_check.tracer to on; set plpgsql_check.trace_assert to on; set plpgsql_check.tracer_test_mode to true; create or replace function repro06_inner() returns void as $$ begin assert 1 = 2, 'boom'; end; $$ language plpgsql; create or replace function repro06_outer() returns void as $$ begin perform repro06_inner(); end; $$ language plpgsql; -- no outer error context frame - the loop is not entered, so this is safe select repro06_inner(); NOTICE: #0 ->> start of function repro06_inner() (oid=0, tnl=1) NOTICE: #0 PLpgSQL assert expression (1 = 2) on line 3 of repro06_inner() is false NOTICE: #0 <<- end of function repro06_inner (elapsed time=0.010 ms) aborted ERROR: boom CONTEXT: PL/pgSQL function repro06_inner() line 3 at ASSERT -- called from another function, so error_context_stack->previous is set select repro06_outer(); NOTICE: #0 ->> start of function repro06_outer() (oid=0, tnl=1) NOTICE: #2 ->> start of function repro06_inner() (oid=0, tnl=1) NOTICE: #2 context: SQL statement "SELECT repro06_inner()" NOTICE: #2 PLpgSQL assert expression (1 = 2) on line 3 of repro06_inner() is false NOTICE: #0 PL/pgSQL function repro06_outer() line 3 at PERFORM NOTICE: #2 <<- end of function repro06_inner (elapsed time=0.010 ms) aborted NOTICE: #0 <<- end of function repro06_outer (elapsed time=0.010 ms) aborted ERROR: boom CONTEXT: PL/pgSQL function repro06_inner() line 3 at ASSERT SQL statement "SELECT repro06_inner()" PL/pgSQL function repro06_outer() line 3 at PERFORM set plpgsql_check.tracer to off; set plpgsql_check.trace_assert to off; okbob-plpgsql_check-800db79/expected/plpgsql_check_nocrash_unstable_01_1.out000066400000000000000000000035211526744060500272770ustar00rootroot00000000000000load 'plpgsql'; create extension if not exists plpgsql_check; NOTICE: extension "plpgsql_check" already exists, skipping load 'plpgsql_check'; set client_min_messages to notice; set plpgsql_check.regress_test_mode = true; set plpgsql_check.enable_tracer to on; set plpgsql_check.tracer to on; set plpgsql_check.trace_assert to on; set plpgsql_check.tracer_test_mode to true; create or replace function repro06_inner() returns void as $$ begin assert 1 = 2, 'boom'; end; $$ language plpgsql; create or replace function repro06_outer() returns void as $$ begin perform repro06_inner(); end; $$ language plpgsql; -- no outer error context frame - the loop is not entered, so this is safe select repro06_inner(); NOTICE: #0 ->> start of function repro06_inner() (oid=0, tnl=1) NOTICE: #0 PLpgSQL assert expression (1 = 2) on line 3 of repro06_inner() is false ERROR: boom CONTEXT: PL/pgSQL function repro06_inner() line 3 at ASSERT NOTICE: #0 <<- end of function repro06_inner (elapsed time=0.010 ms) aborted -- called from another function, so error_context_stack->previous is set select repro06_outer(); NOTICE: #0 ->> start of function repro06_outer() (oid=0, tnl=1) NOTICE: #2 ->> start of function repro06_inner() (oid=0, tnl=1) NOTICE: #2 context: SQL statement "SELECT repro06_inner()" NOTICE: #2 PLpgSQL assert expression (1 = 2) on line 3 of repro06_inner() is false NOTICE: #0 PL/pgSQL function repro06_outer() line 3 at PERFORM ERROR: boom CONTEXT: PL/pgSQL function repro06_inner() line 3 at ASSERT SQL statement "SELECT repro06_inner()" PL/pgSQL function repro06_outer() line 3 at PERFORM NOTICE: #2 <<- end of function repro06_inner (elapsed time=0.010 ms) aborted NOTICE: #0 <<- end of function repro06_outer (elapsed time=0.010 ms) aborted set plpgsql_check.tracer to off; set plpgsql_check.trace_assert to off; okbob-plpgsql_check-800db79/expected/plpgsql_check_nocrash_unstable_01_2.out000066400000000000000000000035211526744060500273000ustar00rootroot00000000000000load 'plpgsql'; create extension if not exists plpgsql_check; NOTICE: extension "plpgsql_check" already exists, skipping load 'plpgsql_check'; set client_min_messages to notice; set plpgsql_check.regress_test_mode = true; set plpgsql_check.enable_tracer to on; set plpgsql_check.tracer to on; set plpgsql_check.trace_assert to on; set plpgsql_check.tracer_test_mode to true; create or replace function repro06_inner() returns void as $$ begin assert 1 = 2, 'boom'; end; $$ language plpgsql; create or replace function repro06_outer() returns void as $$ begin perform repro06_inner(); end; $$ language plpgsql; -- no outer error context frame - the loop is not entered, so this is safe select repro06_inner(); NOTICE: #0 ->> start of function repro06_inner() (oid=0, tnl=1) NOTICE: #0 PLpgSQL assert expression (1 = 2) on line 3 of repro06_inner() is false ERROR: boom CONTEXT: PL/pgSQL function repro06_inner() line 3 at ASSERT NOTICE: #0 <<- end of function repro06_inner (elapsed time=0.010 ms) aborted -- called from another function, so error_context_stack->previous is set select repro06_outer(); NOTICE: #0 ->> start of function repro06_outer() (oid=0, tnl=1) NOTICE: #2 ->> start of function repro06_inner() (oid=0, tnl=1) NOTICE: #2 context: SQL statement "SELECT repro06_inner()" NOTICE: #2 PLpgSQL assert expression (1 = 2) on line 3 of repro06_inner() is false NOTICE: #0 PL/pgSQL function repro06_outer() line 3 at PERFORM NOTICE: #2 <<- end of function repro06_inner (elapsed time=0.010 ms) aborted NOTICE: #0 <<- end of function repro06_outer (elapsed time=0.010 ms) aborted ERROR: boom CONTEXT: PL/pgSQL function repro06_inner() line 3 at ASSERT SQL statement "SELECT repro06_inner()" PL/pgSQL function repro06_outer() line 3 at PERFORM set plpgsql_check.tracer to off; set plpgsql_check.trace_assert to off; okbob-plpgsql_check-800db79/expected/plpgsql_check_nocrash_unstable_01_3.out000066400000000000000000000035211526744060500273010ustar00rootroot00000000000000load 'plpgsql'; create extension if not exists plpgsql_check; NOTICE: extension "plpgsql_check" already exists, skipping load 'plpgsql_check'; set client_min_messages to notice; set plpgsql_check.regress_test_mode = true; set plpgsql_check.enable_tracer to on; set plpgsql_check.tracer to on; set plpgsql_check.trace_assert to on; set plpgsql_check.tracer_test_mode to true; create or replace function repro06_inner() returns void as $$ begin assert 1 = 2, 'boom'; end; $$ language plpgsql; create or replace function repro06_outer() returns void as $$ begin perform repro06_inner(); end; $$ language plpgsql; -- no outer error context frame - the loop is not entered, so this is safe select repro06_inner(); NOTICE: #0 ->> start of function repro06_inner() (oid=0, tnl=1) NOTICE: #0 PLpgSQL assert expression (1 = 2) on line 3 of repro06_inner() is false NOTICE: #0 <<- end of function repro06_inner (elapsed time=0.010 ms) aborted ERROR: boom CONTEXT: PL/pgSQL function repro06_inner() line 3 at ASSERT -- called from another function, so error_context_stack->previous is set select repro06_outer(); NOTICE: #0 ->> start of function repro06_outer() (oid=0, tnl=1) NOTICE: #2 ->> start of function repro06_inner() (oid=0, tnl=1) NOTICE: #2 context: SQL statement "SELECT repro06_inner()" NOTICE: #2 PLpgSQL assert expression (1 = 2) on line 3 of repro06_inner() is false NOTICE: #0 PL/pgSQL function repro06_outer() line 3 at PERFORM NOTICE: #2 <<- end of function repro06_inner (elapsed time=0.010 ms) aborted ERROR: boom CONTEXT: PL/pgSQL function repro06_inner() line 3 at ASSERT SQL statement "SELECT repro06_inner()" PL/pgSQL function repro06_outer() line 3 at PERFORM NOTICE: #0 <<- end of function repro06_outer (elapsed time=0.010 ms) aborted set plpgsql_check.tracer to off; set plpgsql_check.trace_assert to off; okbob-plpgsql_check-800db79/expected/plpgsql_check_nocrash_unstable_01_4.out000066400000000000000000000035211526744060500273020ustar00rootroot00000000000000load 'plpgsql'; create extension if not exists plpgsql_check; NOTICE: extension "plpgsql_check" already exists, skipping load 'plpgsql_check'; set client_min_messages to notice; set plpgsql_check.regress_test_mode = true; set plpgsql_check.enable_tracer to on; set plpgsql_check.tracer to on; set plpgsql_check.trace_assert to on; set plpgsql_check.tracer_test_mode to true; create or replace function repro06_inner() returns void as $$ begin assert 1 = 2, 'boom'; end; $$ language plpgsql; create or replace function repro06_outer() returns void as $$ begin perform repro06_inner(); end; $$ language plpgsql; -- no outer error context frame - the loop is not entered, so this is safe select repro06_inner(); NOTICE: #0 ->> start of function repro06_inner() (oid=0, tnl=1) NOTICE: #0 PLpgSQL assert expression (1 = 2) on line 3 of repro06_inner() is false NOTICE: #0 <<- end of function repro06_inner (elapsed time=0.010 ms) aborted ERROR: boom CONTEXT: PL/pgSQL function repro06_inner() line 3 at ASSERT -- called from another function, so error_context_stack->previous is set select repro06_outer(); NOTICE: #0 ->> start of function repro06_outer() (oid=0, tnl=1) NOTICE: #2 ->> start of function repro06_inner() (oid=0, tnl=1) NOTICE: #2 context: SQL statement "SELECT repro06_inner()" NOTICE: #2 PLpgSQL assert expression (1 = 2) on line 3 of repro06_inner() is false NOTICE: #0 PL/pgSQL function repro06_outer() line 3 at PERFORM ERROR: boom CONTEXT: PL/pgSQL function repro06_inner() line 3 at ASSERT SQL statement "SELECT repro06_inner()" PL/pgSQL function repro06_outer() line 3 at PERFORM NOTICE: #2 <<- end of function repro06_inner (elapsed time=0.010 ms) aborted NOTICE: #0 <<- end of function repro06_outer (elapsed time=0.010 ms) aborted set plpgsql_check.tracer to off; set plpgsql_check.trace_assert to off; okbob-plpgsql_check-800db79/expected/plpgsql_check_nocrash_unstable_01_5.out000066400000000000000000000035211526744060500273030ustar00rootroot00000000000000load 'plpgsql'; create extension if not exists plpgsql_check; NOTICE: extension "plpgsql_check" already exists, skipping load 'plpgsql_check'; set client_min_messages to notice; set plpgsql_check.regress_test_mode = true; set plpgsql_check.enable_tracer to on; set plpgsql_check.tracer to on; set plpgsql_check.trace_assert to on; set plpgsql_check.tracer_test_mode to true; create or replace function repro06_inner() returns void as $$ begin assert 1 = 2, 'boom'; end; $$ language plpgsql; create or replace function repro06_outer() returns void as $$ begin perform repro06_inner(); end; $$ language plpgsql; -- no outer error context frame - the loop is not entered, so this is safe select repro06_inner(); NOTICE: #0 ->> start of function repro06_inner() (oid=0, tnl=1) NOTICE: #0 PLpgSQL assert expression (1 = 2) on line 3 of repro06_inner() is false ERROR: boom CONTEXT: PL/pgSQL function repro06_inner() line 3 at ASSERT NOTICE: #0 <<- end of function repro06_inner (elapsed time=0.010 ms) aborted -- called from another function, so error_context_stack->previous is set select repro06_outer(); NOTICE: #0 ->> start of function repro06_outer() (oid=0, tnl=1) NOTICE: #2 ->> start of function repro06_inner() (oid=0, tnl=1) NOTICE: #2 context: SQL statement "SELECT repro06_inner()" NOTICE: #2 PLpgSQL assert expression (1 = 2) on line 3 of repro06_inner() is false NOTICE: #0 PL/pgSQL function repro06_outer() line 3 at PERFORM NOTICE: #2 <<- end of function repro06_inner (elapsed time=0.010 ms) aborted ERROR: boom CONTEXT: PL/pgSQL function repro06_inner() line 3 at ASSERT SQL statement "SELECT repro06_inner()" PL/pgSQL function repro06_outer() line 3 at PERFORM NOTICE: #0 <<- end of function repro06_outer (elapsed time=0.010 ms) aborted set plpgsql_check.tracer to off; set plpgsql_check.trace_assert to off; okbob-plpgsql_check-800db79/expected/plpgsql_check_parser.out000066400000000000000000000476471526744060500245420ustar00rootroot00000000000000set client_min_messages to warning; create extension if not exists plpgsql_check; set client_min_messages to notice; -- -- Tests of the parser used for the function names passed to the -- plpgsql_check functions, and of the tokenizer and the parsers of -- the arguments of the pragmas. -- -- -- function name or signature -- create function pr_fx(a int) returns int as $$ begin return a; end; $$ language plpgsql; create function "pr FX"(a int) returns int as $$ begin return a; end; $$ language plpgsql; create function "pr""q"(a int) returns int as $$ begin return a; end; $$ language plpgsql; -- an overloaded function cannot be addressed by a name create function pr_over(a int) returns int as $$ begin return a; end; $$ language plpgsql; create function pr_over(a text) returns text as $$ begin return a; end; $$ language plpgsql; -- the errors are raised by the parser of the name, so they are catched -- and printed as a value create function pr_name(fnname text) returns text as $$ begin perform plpgsql_check_function(fnname); return 'ok'; exception when others then return sqlstate || ' ' || sqlerrm; end; $$ language plpgsql; -- plain name, with and without the leading and trailing spaces select pr_name('pr_fx'); pr_name --------- ok (1 row) select pr_name(' pr_fx '); pr_name --------- ok (1 row) -- quoted name, a doubled quote inside a quoted name select pr_name('"pr FX"'); pr_name --------- ok (1 row) select pr_name('"pr""q"'); pr_name --------- ok (1 row) -- qualified names select pr_name('public.pr_fx'); pr_name --------- ok (1 row) select pr_name('public."pr FX"'); pr_name --------- ok (1 row) select pr_name('"public" . "pr FX"'); pr_name --------- ok (1 row) -- signatures; the parser stops on the left parenthesis and the rest -- is processed by the regprocedure input function select pr_name('pr_fx(int)'); pr_name --------- ok (1 row) select pr_name('public.pr_fx(integer)'); pr_name --------- ok (1 row) select pr_name('"pr FX"(int)'); pr_name --------- ok (1 row) -- the name is not an identifier select pr_name('"pr FX'); pr_name -------------------------------------------------- 22023 string is not a valid identifier: ""pr FX" (1 row) select pr_name('""'); pr_name ---------------------------------------------- 22023 string is not a valid identifier: """" (1 row) select pr_name('.pr_fx'); pr_name -------------------------------------------------- 22023 string is not a valid identifier: ".pr_fx" (1 row) select pr_name('public.'); pr_name --------------------------------------------------- 22023 string is not a valid identifier: "public." (1 row) select pr_name('public..pr_fx'); pr_name --------------------------------------------------------- 22023 string is not a valid identifier: "public..pr_fx" (1 row) select pr_name('1234'); pr_name ------------------------------------------------ 22023 string is not a valid identifier: "1234" (1 row) select pr_name('pr_fx pr_fx'); pr_name ------------------------------------------------------- 22023 string is not a valid identifier: "pr_fx pr_fx" (1 row) -- the name is an identifier, but the function is not there or the -- name is not unique select pr_name('pr_missing'); pr_name -------------------------------------------- 42883 function "pr_missing" does not exist (1 row) select pr_name('pr_over'); pr_name ---------------------------------------------- 42725 more than one function named "pr_over" (1 row) drop function pr_name(text); drop function pr_over(text); drop function pr_over(int); drop function "pr""q"(int); drop function "pr FX"(int); drop function pr_fx(int); -- -- tokenizer -- -- The tokenizer is shared by all the pragmas and by the in-comment -- options. The echo option is used here, because it is the only place -- where a token of any type is accepted and printed back, so the result -- of the tokenization is visible. create function pr_tokens() returns void as $func$ -- @plpgsql_check_options: echo = 'a string with a '' quote' -- @plpgsql_check_options: echo = "a quoted identifier with a "" quote" -- @plpgsql_check_options: echo = AnIdentifier -- @plpgsql_check_options: echo = 3.14 begin end; $func$ language plpgsql; select * from plpgsql_check_function('pr_tokens'); NOTICE: comment option "echo" is 'a string with a ' quote' NOTICE: comment option "echo" is "a quoted identifier with a " quote" NOTICE: comment option "echo" is anidentifier NOTICE: comment option "echo" is 3.14 plpgsql_check_function ------------------------ (0 rows) drop function pr_tokens(); -- -- pragma "settype" -- create type pr_ctype as (a int, b int); create table pr_tab(a int, b int); create function pr_settype() returns void as $$ <> declare r record; n int; begin -- a plain, a qualified and a quoted type name perform plpgsql_check_pragma('type: r pr_ctype'); perform plpgsql_check_pragma('type: lbl.r public.pr_ctype'); perform plpgsql_check_pragma('type: "lbl"."r" public."pr_ctype"'); -- a table can be used as a type too perform plpgsql_check_pragma('type: r pr_tab'); raise notice '%', r.a; raise notice '%', n; end; $$ language plpgsql; select * from plpgsql_check_function('pr_settype'); plpgsql_check_function ------------------------ (0 rows) -- errors of the "settype" pragma create or replace function pr_settype() returns void as $$ declare r record; n int; begin perform plpgsql_check_pragma('type: missing_var pr_ctype'); perform plpgsql_check_pragma('type: n pr_ctype'); perform plpgsql_check_pragma('type: r'); perform plpgsql_check_pragma('type: 1 pr_ctype'); perform plpgsql_check_pragma('type: r pr_ctype x'); raise notice '%', r.a; raise notice '%', n; end; $$ language plpgsql; select * from plpgsql_check_function('pr_settype'); WARNING: Pragma "type" on line 6 is not processed. DETAIL: Cannot find variable "missing_var" used in type pragma WARNING: Pragma "type" on line 7 is not processed. DETAIL: Pragma "type" can be applied only on variable of record type WARNING: Pragma "type" on line 8 is not processed. DETAIL: Syntax error (expected identifier) WARNING: Pragma "type" on line 9 is not processed. DETAIL: Syntax error (expected identifier) WARNING: Pragma "type" on line 10 is not processed. DETAIL: syntax error at or near "x" plpgsql_check_function ---------------------------------------------------------------------------- error:55000:11:RAISE:record "r" is not assigned yet Detail: The tuple structure of a not-yet-assigned record is indeterminate. Context: PL/pgSQL expression "r.a" (3 rows) drop function pr_settype(); -- -- the type parser -- -- The type used by the "settype" and "table" pragmas is parsed by -- plpgsql_check itself, so all the accepted forms are checked here. create function pr_type() returns void as $$ declare r record; begin -- a multiword type name, a type modifier, a list of type modifiers perform plpgsql_check_pragma('type: r (a double precision, b varchar(10), c numeric(10,2))'); -- an array, with and without a dimension perform plpgsql_check_pragma('type: r (a int[], b int[3], c pg_catalog.varchar[])'); -- datetime modifiers can precede more words, and arrays can have many dimensions perform plpgsql_check_pragma('type: r (a timestamp(3) without time zone, b time(2) with time zone, c interval day to second(2))'); perform plpgsql_check_pragma('type: r (a integer[][], b integer[2][3], c (d numeric(8,2), e timestamp(1) with time zone))'); -- a composite type copied from a table perform plpgsql_check_pragma('type: r (like pr_tab)'); raise notice '%', r.a; end; $$ language plpgsql; select * from plpgsql_check_function('pr_type'); plpgsql_check_function ------------------------ (0 rows) create function pr_negative_scale() returns void as $$ declare r record; begin perform plpgsql_check_pragma('type: r (a numeric(5,-2), b int)'); raise notice '%', r.a; end; $$ language plpgsql; select case when current_setting('server_version_num')::int >= 150000 then not exists (select from plpgsql_check_function_tb('pr_negative_scale') where level = 'error') else true end as negative_scale; negative_scale ---------------- t (1 row) drop function pr_negative_scale(); -- errors of the type parser create or replace function pr_type() returns void as $$ declare r record; begin perform plpgsql_check_pragma('type: r (like int)'); perform plpgsql_check_pragma('type: r (like pr_tab'); perform plpgsql_check_pragma('type: r (1 int)'); perform plpgsql_check_pragma('type: r (a int b int)'); perform plpgsql_check_pragma('type: r (a numeric(x))'); perform plpgsql_check_pragma('type: r (a numeric(10'); perform plpgsql_check_pragma('type: r (a numeric(10 2))'); perform plpgsql_check_pragma('type: r (a int[)'); perform plpgsql_check_pragma('type: r (a int['); perform plpgsql_check_pragma('type: r (a "unclosed)'); raise notice '%', r.a; end; $$ language plpgsql; select * from plpgsql_check_function('pr_type'); WARNING: Pragma "type" on line 4 is not processed. DETAIL: "integer" is not composite type WARNING: Pragma "type" on line 5 is not processed. DETAIL: Syntax error (expected ")") WARNING: Pragma "type" on line 6 is not processed. DETAIL: Syntax error (expected identifier) WARNING: Pragma "type" on line 7 is not processed. DETAIL: syntax error at or near "b" WARNING: Pragma "type" on line 8 is not processed. DETAIL: invalid input syntax for type integer: "x" WARNING: Pragma "type" on line 9 is not processed. DETAIL: syntax error at end of input WARNING: Pragma "type" on line 10 is not processed. DETAIL: syntax error at or near "2" WARNING: Pragma "type" on line 11 is not processed. DETAIL: syntax error at or near ")" WARNING: Pragma "type" on line 12 is not processed. DETAIL: syntax error at end of input WARNING: Pragma "type" on line 13 is not processed. DETAIL: Syntax error (unclosed quoted identifier) plpgsql_check_function ---------------------------------------------------------------------------- error:55000:14:RAISE:record "r" is not assigned yet Detail: The tuple structure of a not-yet-assigned record is indeterminate. Context: PL/pgSQL expression "r.a" (3 rows) drop function pr_type(); -- -- pragma "table" -- create function pr_table() returns void as $$ declare r record; begin perform plpgsql_check_pragma('table: pr_tt1(a int, b int)'); perform plpgsql_check_pragma('table: pg_temp.pr_tt2(a int)'); perform plpgsql_check_pragma('table: pg_temp."pr""tt3"(a int)'); select * from pr_tt1 into r; select * from pr_tt2 into r; raise notice '%', r.a; end; $$ language plpgsql; select * from plpgsql_check_function('pr_table'); plpgsql_check_function ------------------------ (0 rows) -- errors of the "table" pragma create or replace function pr_table() returns void as $$ begin perform plpgsql_check_pragma('table: public.pr_tt4(a int)'); perform plpgsql_check_pragma('table: 1(a int)'); perform plpgsql_check_pragma('table: pg_temp.1(a int)'); perform plpgsql_check_pragma('table: pr_tt5'); perform plpgsql_check_pragma('table: pr_tt6(a int) x'); perform plpgsql_check_pragma('table: "pr_tt7(a int)'); -- a composite type is not allowed as a type of a column of a table -- created by this pragma perform plpgsql_check_pragma('table: pr_tt8(a (b int))'); -- constraints and collations are still outside the table pragma grammar perform plpgsql_check_pragma('table: pr_tt9(a int primary key)'); perform plpgsql_check_pragma('table: pr_tt9(a int not null)'); perform plpgsql_check_pragma('table: pr_tt9(a int default 1)'); perform plpgsql_check_pragma('table: pr_tt9(a int check (a > 0))'); perform plpgsql_check_pragma('table: pr_tt9(a text collate "C")'); end; $$ language plpgsql; select * from plpgsql_check_function('pr_table'); WARNING: Pragma "table" on line 3 is not processed. DETAIL: schema "public" cannot be used in pragma "table" (only "pg_temp" schema is allowed) WARNING: Pragma "table" on line 4 is not processed. DETAIL: Syntax error (expected identifier) WARNING: Pragma "table" on line 5 is not processed. DETAIL: Syntax error (expected identifier) WARNING: Pragma "table" on line 6 is not processed. DETAIL: Syntax error (expected table specification) WARNING: Pragma "table" on line 7 is not processed. DETAIL: Syntax error (unexpected chars after table specification) WARNING: Pragma "table" on line 8 is not processed. DETAIL: Syntax error (unclosed quoted identifier) WARNING: Pragma "table" on line 11 is not processed. DETAIL: Cannot to create table with pseudo-type record. WARNING: Pragma "table" on line 13 is not processed. DETAIL: syntax error at or near "primary" WARNING: Pragma "table" on line 14 is not processed. DETAIL: syntax error at or near "not" WARNING: Pragma "table" on line 15 is not processed. DETAIL: syntax error at or near "default" WARNING: Pragma "table" on line 16 is not processed. DETAIL: syntax error at or near "check" WARNING: Pragma "table" on line 17 is not processed. DETAIL: syntax error at or near "collate" plpgsql_check_function ------------------------ (0 rows) drop function pr_table(); -- -- pragma "sequence" -- create function pr_sequence() returns void as $$ begin perform plpgsql_check_pragma('sequence: pr_sq1'); perform plpgsql_check_pragma('sequence: pg_temp.pr_sq2'); perform plpgsql_check_pragma('sequence: pg_temp."pr""sq3"'); perform nextval('pg_temp.pr_sq1'); perform nextval('pg_temp.pr_sq2'); end; $$ language plpgsql; select * from plpgsql_check_function('pr_sequence'); plpgsql_check_function ------------------------ (0 rows) -- errors of the "sequence" pragma create or replace function pr_sequence() returns void as $$ begin perform plpgsql_check_pragma('sequence: public.pr_sq4'); perform plpgsql_check_pragma('sequence: 1'); perform plpgsql_check_pragma('sequence: pg_temp.1'); perform plpgsql_check_pragma('sequence: pr_sq5 x y'); perform plpgsql_check_pragma('sequence: "pr_sq6'); end; $$ language plpgsql; select * from plpgsql_check_function('pr_sequence'); WARNING: Pragma "sequence" on line 3 is not processed. DETAIL: schema "public" cannot be used in pragma "sequence" (only "pg_temp" schema is allowed) WARNING: Pragma "sequence" on line 4 is not processed. DETAIL: Syntax error (expected identifier) WARNING: Pragma "sequence" on line 5 is not processed. DETAIL: Syntax error (expected identifier) WARNING: Pragma "sequence" on line 6 is not processed. DETAIL: Syntax error (unexpected chars after sequence name) WARNING: Pragma "sequence" on line 7 is not processed. DETAIL: Syntax error (unclosed quoted identifier) plpgsql_check_function ------------------------ (0 rows) drop function pr_sequence(); -- -- pragmas "assert-schema", "assert-table" and "assert-column" -- create function pr_assert() returns void as $$ <> declare v_schema varchar default 'public'; v_table varchar default 'pr_tab'; v_column varchar default 'a'; begin raise notice '%', format('%I.%I.%I', v_schema, v_table, v_column); perform 'pragma:assert-schema: v_schema'; perform 'pragma:assert-table: v_schema, v_table'; perform 'pragma:assert-table: v_table'; perform 'pragma:assert-column: v_schema, v_table, v_column'; perform 'pragma:assert-column: v_table, v_column'; -- a variable can be addressed by a qualified name too perform 'pragma:assert-schema: lbl.v_schema'; end; $$ language plpgsql; select * from plpgsql_check_function('pr_assert'); plpgsql_check_function ------------------------ (0 rows) -- the asserted objects do not exist create or replace function pr_assert() returns void as $$ declare v_schema varchar default 'pr_missing_schema'; v_table varchar default 'pr_tab'; v_column varchar default 'a'; begin perform 'pragma:assert-schema: v_schema'; perform 'pragma:assert-table: v_schema, v_table'; perform 'pragma:assert-column: v_schema, v_table, v_column'; end; $$ language plpgsql; select count(*) = 3 as missing_schemas from plpgsql_check_function_tb('pr_assert', fatal_errors => false) where sqlstate = '3F000'; missing_schemas ----------------- t (1 row) create or replace function pr_assert() returns void as $$ declare v_schema varchar default 'pr_missing_schema'; v_table varchar default 'pr_missing_table'; v_column varchar default 'pr_missing_column'; begin perform 'pragma:assert-table: v_table'; end; $$ language plpgsql; select * from plpgsql_check_function('pr_assert'); plpgsql_check_function --------------------------------------------------------------- error:42P01:7:PERFORM:table "pr_missing_table" does not exist (1 row) create or replace function pr_assert() returns void as $$ declare v_schema varchar default 'public'; v_table varchar default 'pr_tab'; v_column varchar default 'pr_missing_column'; begin perform 'pragma:assert-column: v_schema, v_table, v_column'; end; $$ language plpgsql; select * from plpgsql_check_function('pr_assert'); plpgsql_check_function ----------------------------------------------------------------------------------------------- error:42703:7:PERFORM:column "pr_missing_column" of relation "public"."pr_tab" does not exist (1 row) -- errors of the "assert" pragmas; they are reported as warnings and the -- check continues create or replace function pr_assert() returns void as $$ <> declare v_schema varchar default 'public'; v_table varchar default 'pr_tab'; v_column varchar default 'a'; v_var varchar; begin -- an unknown variable, a name with too many parts perform 'pragma:assert-schema: v_missing'; perform 'pragma:assert-schema: a.b.c.d'; -- a variable without an assigned constant perform 'pragma:assert-schema: v_var'; -- a missing comma between the variables perform 'pragma:assert-table: v_schema v_table'; -- too many and too few variables; the parser reads three names at -- most, so a fourth one is reported as an unexpected text perform 'pragma:assert-schema: v_schema, v_table'; perform 'pragma:assert-table: v_schema, v_table, v_column'; perform 'pragma:assert-column: v_schema, v_table, v_column, v_column'; perform 'pragma:assert-column: v_table'; -- a syntax error in the name of the variable perform 'pragma:assert-schema: 1'; end; $$ language plpgsql; select * from plpgsql_check_function('pr_assert'); WARNING: "assert-schema" on line 10 is not processed. DETAIL: Cannot to find variable "v_missing" used in "assert-schema" pragma WARNING: "assert-schema" on line 11 is not processed. DETAIL: Cannot to find variable "a"."b"."c"."d" used in "assert-schema" pragma WARNING: "assert-schema" on line 13 is not processed. DETAIL: Variable "v_var" has not assigned constant WARNING: "assert-table" on line 15 is not processed. DETAIL: Syntax error (expected ",") WARNING: "assert-schema" on line 18 is not processed. DETAIL: too much variables for "assert-schema" pragma WARNING: "assert-table" on line 19 is not processed. DETAIL: too much variables for "assert-table" pragma WARNING: "assert-column" on line 20 is not processed. DETAIL: Syntax error (unexpected chars after variable) WARNING: "assert-column" on line 21 is not processed. DETAIL: too few variables for "assert-column" pragma WARNING: "assert-schema" on line 23 is not processed. DETAIL: Syntax error (expected identifier) plpgsql_check_function -------------------------------------------------------------- warning:00000:7:DECLARE:unused variable "v_var" warning extra:00000:4:DECLARE:never read variable "v_schema" warning extra:00000:5:DECLARE:never read variable "v_table" warning extra:00000:6:DECLARE:never read variable "v_column" (4 rows) drop function pr_assert(); drop table pr_tab; drop type pr_ctype; okbob-plpgsql_check-800db79/expected/plpgsql_check_parser_1.out000066400000000000000000000476351526744060500247570ustar00rootroot00000000000000set client_min_messages to warning; create extension if not exists plpgsql_check; set client_min_messages to notice; -- -- Tests of the parser used for the function names passed to the -- plpgsql_check functions, and of the tokenizer and the parsers of -- the arguments of the pragmas. -- -- -- function name or signature -- create function pr_fx(a int) returns int as $$ begin return a; end; $$ language plpgsql; create function "pr FX"(a int) returns int as $$ begin return a; end; $$ language plpgsql; create function "pr""q"(a int) returns int as $$ begin return a; end; $$ language plpgsql; -- an overloaded function cannot be addressed by a name create function pr_over(a int) returns int as $$ begin return a; end; $$ language plpgsql; create function pr_over(a text) returns text as $$ begin return a; end; $$ language plpgsql; -- the errors are raised by the parser of the name, so they are catched -- and printed as a value create function pr_name(fnname text) returns text as $$ begin perform plpgsql_check_function(fnname); return 'ok'; exception when others then return sqlstate || ' ' || sqlerrm; end; $$ language plpgsql; -- plain name, with and without the leading and trailing spaces select pr_name('pr_fx'); pr_name --------- ok (1 row) select pr_name(' pr_fx '); pr_name --------- ok (1 row) -- quoted name, a doubled quote inside a quoted name select pr_name('"pr FX"'); pr_name --------- ok (1 row) select pr_name('"pr""q"'); pr_name --------- ok (1 row) -- qualified names select pr_name('public.pr_fx'); pr_name --------- ok (1 row) select pr_name('public."pr FX"'); pr_name --------- ok (1 row) select pr_name('"public" . "pr FX"'); pr_name --------- ok (1 row) -- signatures; the parser stops on the left parenthesis and the rest -- is processed by the regprocedure input function select pr_name('pr_fx(int)'); pr_name --------- ok (1 row) select pr_name('public.pr_fx(integer)'); pr_name --------- ok (1 row) select pr_name('"pr FX"(int)'); pr_name --------- ok (1 row) -- the name is not an identifier select pr_name('"pr FX'); pr_name -------------------------------------------------- 22023 string is not a valid identifier: ""pr FX" (1 row) select pr_name('""'); pr_name ---------------------------------------------- 22023 string is not a valid identifier: """" (1 row) select pr_name('.pr_fx'); pr_name -------------------------------------------------- 22023 string is not a valid identifier: ".pr_fx" (1 row) select pr_name('public.'); pr_name --------------------------------------------------- 22023 string is not a valid identifier: "public." (1 row) select pr_name('public..pr_fx'); pr_name --------------------------------------------------------- 22023 string is not a valid identifier: "public..pr_fx" (1 row) select pr_name('1234'); pr_name ------------------------------------------------ 22023 string is not a valid identifier: "1234" (1 row) select pr_name('pr_fx pr_fx'); pr_name ------------------------------------------------------- 22023 string is not a valid identifier: "pr_fx pr_fx" (1 row) -- the name is an identifier, but the function is not there or the -- name is not unique select pr_name('pr_missing'); pr_name -------------------------------------------- 42883 function "pr_missing" does not exist (1 row) select pr_name('pr_over'); pr_name ---------------------------------------------- 42725 more than one function named "pr_over" (1 row) drop function pr_name(text); drop function pr_over(text); drop function pr_over(int); drop function "pr""q"(int); drop function "pr FX"(int); drop function pr_fx(int); -- -- tokenizer -- -- The tokenizer is shared by all the pragmas and by the in-comment -- options. The echo option is used here, because it is the only place -- where a token of any type is accepted and printed back, so the result -- of the tokenization is visible. create function pr_tokens() returns void as $func$ -- @plpgsql_check_options: echo = 'a string with a '' quote' -- @plpgsql_check_options: echo = "a quoted identifier with a "" quote" -- @plpgsql_check_options: echo = AnIdentifier -- @plpgsql_check_options: echo = 3.14 begin end; $func$ language plpgsql; select * from plpgsql_check_function('pr_tokens'); NOTICE: comment option "echo" is 'a string with a ' quote' NOTICE: comment option "echo" is "a quoted identifier with a " quote" NOTICE: comment option "echo" is anidentifier NOTICE: comment option "echo" is 3.14 plpgsql_check_function ------------------------ (0 rows) drop function pr_tokens(); -- -- pragma "settype" -- create type pr_ctype as (a int, b int); create table pr_tab(a int, b int); create function pr_settype() returns void as $$ <> declare r record; n int; begin -- a plain, a qualified and a quoted type name perform plpgsql_check_pragma('type: r pr_ctype'); perform plpgsql_check_pragma('type: lbl.r public.pr_ctype'); perform plpgsql_check_pragma('type: "lbl"."r" public."pr_ctype"'); -- a table can be used as a type too perform plpgsql_check_pragma('type: r pr_tab'); raise notice '%', r.a; raise notice '%', n; end; $$ language plpgsql; select * from plpgsql_check_function('pr_settype'); plpgsql_check_function ------------------------ (0 rows) -- errors of the "settype" pragma create or replace function pr_settype() returns void as $$ declare r record; n int; begin perform plpgsql_check_pragma('type: missing_var pr_ctype'); perform plpgsql_check_pragma('type: n pr_ctype'); perform plpgsql_check_pragma('type: r'); perform plpgsql_check_pragma('type: 1 pr_ctype'); perform plpgsql_check_pragma('type: r pr_ctype x'); raise notice '%', r.a; raise notice '%', n; end; $$ language plpgsql; select * from plpgsql_check_function('pr_settype'); WARNING: Pragma "type" on line 6 is not processed. DETAIL: Cannot find variable "missing_var" used in type pragma WARNING: Pragma "type" on line 7 is not processed. DETAIL: Pragma "type" can be applied only on variable of record type WARNING: Pragma "type" on line 8 is not processed. DETAIL: Syntax error (expected identifier) WARNING: Pragma "type" on line 9 is not processed. DETAIL: Syntax error (expected identifier) WARNING: Pragma "type" on line 10 is not processed. DETAIL: syntax error at or near "x" plpgsql_check_function ---------------------------------------------------------------------------- error:55000:11:RAISE:record "r" is not assigned yet Detail: The tuple structure of a not-yet-assigned record is indeterminate. Context: SQL expression "r.a" (3 rows) drop function pr_settype(); -- -- the type parser -- -- The type used by the "settype" and "table" pragmas is parsed by -- plpgsql_check itself, so all the accepted forms are checked here. create function pr_type() returns void as $$ declare r record; begin -- a multiword type name, a type modifier, a list of type modifiers perform plpgsql_check_pragma('type: r (a double precision, b varchar(10), c numeric(10,2))'); -- an array, with and without a dimension perform plpgsql_check_pragma('type: r (a int[], b int[3], c pg_catalog.varchar[])'); -- datetime modifiers can precede more words, and arrays can have many dimensions perform plpgsql_check_pragma('type: r (a timestamp(3) without time zone, b time(2) with time zone, c interval day to second(2))'); perform plpgsql_check_pragma('type: r (a integer[][], b integer[2][3], c (d numeric(8,2), e timestamp(1) with time zone))'); -- a composite type copied from a table perform plpgsql_check_pragma('type: r (like pr_tab)'); raise notice '%', r.a; end; $$ language plpgsql; select * from plpgsql_check_function('pr_type'); plpgsql_check_function ------------------------ (0 rows) create function pr_negative_scale() returns void as $$ declare r record; begin perform plpgsql_check_pragma('type: r (a numeric(5,-2), b int)'); raise notice '%', r.a; end; $$ language plpgsql; select case when current_setting('server_version_num')::int >= 150000 then not exists (select from plpgsql_check_function_tb('pr_negative_scale') where level = 'error') else true end as negative_scale; negative_scale ---------------- t (1 row) drop function pr_negative_scale(); -- errors of the type parser create or replace function pr_type() returns void as $$ declare r record; begin perform plpgsql_check_pragma('type: r (like int)'); perform plpgsql_check_pragma('type: r (like pr_tab'); perform plpgsql_check_pragma('type: r (1 int)'); perform plpgsql_check_pragma('type: r (a int b int)'); perform plpgsql_check_pragma('type: r (a numeric(x))'); perform plpgsql_check_pragma('type: r (a numeric(10'); perform plpgsql_check_pragma('type: r (a numeric(10 2))'); perform plpgsql_check_pragma('type: r (a int[)'); perform plpgsql_check_pragma('type: r (a int['); perform plpgsql_check_pragma('type: r (a "unclosed)'); raise notice '%', r.a; end; $$ language plpgsql; select * from plpgsql_check_function('pr_type'); WARNING: Pragma "type" on line 4 is not processed. DETAIL: "integer" is not composite type WARNING: Pragma "type" on line 5 is not processed. DETAIL: Syntax error (expected ")") WARNING: Pragma "type" on line 6 is not processed. DETAIL: Syntax error (expected identifier) WARNING: Pragma "type" on line 7 is not processed. DETAIL: syntax error at or near "b" WARNING: Pragma "type" on line 8 is not processed. DETAIL: invalid input syntax for type integer: "x" WARNING: Pragma "type" on line 9 is not processed. DETAIL: syntax error at end of input WARNING: Pragma "type" on line 10 is not processed. DETAIL: syntax error at or near "2" WARNING: Pragma "type" on line 11 is not processed. DETAIL: syntax error at or near ")" WARNING: Pragma "type" on line 12 is not processed. DETAIL: syntax error at end of input WARNING: Pragma "type" on line 13 is not processed. DETAIL: Syntax error (unclosed quoted identifier) plpgsql_check_function ---------------------------------------------------------------------------- error:55000:14:RAISE:record "r" is not assigned yet Detail: The tuple structure of a not-yet-assigned record is indeterminate. Context: SQL expression "r.a" (3 rows) drop function pr_type(); -- -- pragma "table" -- create function pr_table() returns void as $$ declare r record; begin perform plpgsql_check_pragma('table: pr_tt1(a int, b int)'); perform plpgsql_check_pragma('table: pg_temp.pr_tt2(a int)'); perform plpgsql_check_pragma('table: pg_temp."pr""tt3"(a int)'); select * from pr_tt1 into r; select * from pr_tt2 into r; raise notice '%', r.a; end; $$ language plpgsql; select * from plpgsql_check_function('pr_table'); plpgsql_check_function ------------------------ (0 rows) -- errors of the "table" pragma create or replace function pr_table() returns void as $$ begin perform plpgsql_check_pragma('table: public.pr_tt4(a int)'); perform plpgsql_check_pragma('table: 1(a int)'); perform plpgsql_check_pragma('table: pg_temp.1(a int)'); perform plpgsql_check_pragma('table: pr_tt5'); perform plpgsql_check_pragma('table: pr_tt6(a int) x'); perform plpgsql_check_pragma('table: "pr_tt7(a int)'); -- a composite type is not allowed as a type of a column of a table -- created by this pragma perform plpgsql_check_pragma('table: pr_tt8(a (b int))'); -- constraints and collations are still outside the table pragma grammar perform plpgsql_check_pragma('table: pr_tt9(a int primary key)'); perform plpgsql_check_pragma('table: pr_tt9(a int not null)'); perform plpgsql_check_pragma('table: pr_tt9(a int default 1)'); perform plpgsql_check_pragma('table: pr_tt9(a int check (a > 0))'); perform plpgsql_check_pragma('table: pr_tt9(a text collate "C")'); end; $$ language plpgsql; select * from plpgsql_check_function('pr_table'); WARNING: Pragma "table" on line 3 is not processed. DETAIL: schema "public" cannot be used in pragma "table" (only "pg_temp" schema is allowed) WARNING: Pragma "table" on line 4 is not processed. DETAIL: Syntax error (expected identifier) WARNING: Pragma "table" on line 5 is not processed. DETAIL: Syntax error (expected identifier) WARNING: Pragma "table" on line 6 is not processed. DETAIL: Syntax error (expected table specification) WARNING: Pragma "table" on line 7 is not processed. DETAIL: Syntax error (unexpected chars after table specification) WARNING: Pragma "table" on line 8 is not processed. DETAIL: Syntax error (unclosed quoted identifier) WARNING: Pragma "table" on line 11 is not processed. DETAIL: Cannot to create table with pseudo-type record. WARNING: Pragma "table" on line 13 is not processed. DETAIL: syntax error at or near "primary" WARNING: Pragma "table" on line 14 is not processed. DETAIL: syntax error at or near "not" WARNING: Pragma "table" on line 15 is not processed. DETAIL: syntax error at or near "default" WARNING: Pragma "table" on line 16 is not processed. DETAIL: syntax error at or near "check" WARNING: Pragma "table" on line 17 is not processed. DETAIL: syntax error at or near "collate" plpgsql_check_function ------------------------ (0 rows) drop function pr_table(); -- -- pragma "sequence" -- create function pr_sequence() returns void as $$ begin perform plpgsql_check_pragma('sequence: pr_sq1'); perform plpgsql_check_pragma('sequence: pg_temp.pr_sq2'); perform plpgsql_check_pragma('sequence: pg_temp."pr""sq3"'); perform nextval('pg_temp.pr_sq1'); perform nextval('pg_temp.pr_sq2'); end; $$ language plpgsql; select * from plpgsql_check_function('pr_sequence'); plpgsql_check_function ------------------------ (0 rows) -- errors of the "sequence" pragma create or replace function pr_sequence() returns void as $$ begin perform plpgsql_check_pragma('sequence: public.pr_sq4'); perform plpgsql_check_pragma('sequence: 1'); perform plpgsql_check_pragma('sequence: pg_temp.1'); perform plpgsql_check_pragma('sequence: pr_sq5 x y'); perform plpgsql_check_pragma('sequence: "pr_sq6'); end; $$ language plpgsql; select * from plpgsql_check_function('pr_sequence'); WARNING: Pragma "sequence" on line 3 is not processed. DETAIL: schema "public" cannot be used in pragma "sequence" (only "pg_temp" schema is allowed) WARNING: Pragma "sequence" on line 4 is not processed. DETAIL: Syntax error (expected identifier) WARNING: Pragma "sequence" on line 5 is not processed. DETAIL: Syntax error (expected identifier) WARNING: Pragma "sequence" on line 6 is not processed. DETAIL: Syntax error (unexpected chars after sequence name) WARNING: Pragma "sequence" on line 7 is not processed. DETAIL: Syntax error (unclosed quoted identifier) plpgsql_check_function ------------------------ (0 rows) drop function pr_sequence(); -- -- pragmas "assert-schema", "assert-table" and "assert-column" -- create function pr_assert() returns void as $$ <> declare v_schema varchar default 'public'; v_table varchar default 'pr_tab'; v_column varchar default 'a'; begin raise notice '%', format('%I.%I.%I', v_schema, v_table, v_column); perform 'pragma:assert-schema: v_schema'; perform 'pragma:assert-table: v_schema, v_table'; perform 'pragma:assert-table: v_table'; perform 'pragma:assert-column: v_schema, v_table, v_column'; perform 'pragma:assert-column: v_table, v_column'; -- a variable can be addressed by a qualified name too perform 'pragma:assert-schema: lbl.v_schema'; end; $$ language plpgsql; select * from plpgsql_check_function('pr_assert'); plpgsql_check_function ------------------------ (0 rows) -- the asserted objects do not exist create or replace function pr_assert() returns void as $$ declare v_schema varchar default 'pr_missing_schema'; v_table varchar default 'pr_tab'; v_column varchar default 'a'; begin perform 'pragma:assert-schema: v_schema'; perform 'pragma:assert-table: v_schema, v_table'; perform 'pragma:assert-column: v_schema, v_table, v_column'; end; $$ language plpgsql; select count(*) = 3 as missing_schemas from plpgsql_check_function_tb('pr_assert', fatal_errors => false) where sqlstate = '3F000'; missing_schemas ----------------- t (1 row) create or replace function pr_assert() returns void as $$ declare v_schema varchar default 'pr_missing_schema'; v_table varchar default 'pr_missing_table'; v_column varchar default 'pr_missing_column'; begin perform 'pragma:assert-table: v_table'; end; $$ language plpgsql; select * from plpgsql_check_function('pr_assert'); plpgsql_check_function --------------------------------------------------------------- error:42P01:7:PERFORM:table "pr_missing_table" does not exist (1 row) create or replace function pr_assert() returns void as $$ declare v_schema varchar default 'public'; v_table varchar default 'pr_tab'; v_column varchar default 'pr_missing_column'; begin perform 'pragma:assert-column: v_schema, v_table, v_column'; end; $$ language plpgsql; select * from plpgsql_check_function('pr_assert'); plpgsql_check_function ----------------------------------------------------------------------------------------------- error:42703:7:PERFORM:column "pr_missing_column" of relation "public"."pr_tab" does not exist (1 row) -- errors of the "assert" pragmas; they are reported as warnings and the -- check continues create or replace function pr_assert() returns void as $$ <> declare v_schema varchar default 'public'; v_table varchar default 'pr_tab'; v_column varchar default 'a'; v_var varchar; begin -- an unknown variable, a name with too many parts perform 'pragma:assert-schema: v_missing'; perform 'pragma:assert-schema: a.b.c.d'; -- a variable without an assigned constant perform 'pragma:assert-schema: v_var'; -- a missing comma between the variables perform 'pragma:assert-table: v_schema v_table'; -- too many and too few variables; the parser reads three names at -- most, so a fourth one is reported as an unexpected text perform 'pragma:assert-schema: v_schema, v_table'; perform 'pragma:assert-table: v_schema, v_table, v_column'; perform 'pragma:assert-column: v_schema, v_table, v_column, v_column'; perform 'pragma:assert-column: v_table'; -- a syntax error in the name of the variable perform 'pragma:assert-schema: 1'; end; $$ language plpgsql; select * from plpgsql_check_function('pr_assert'); WARNING: "assert-schema" on line 10 is not processed. DETAIL: Cannot to find variable "v_missing" used in "assert-schema" pragma WARNING: "assert-schema" on line 11 is not processed. DETAIL: Cannot to find variable "a"."b"."c"."d" used in "assert-schema" pragma WARNING: "assert-schema" on line 13 is not processed. DETAIL: Variable "v_var" has not assigned constant WARNING: "assert-table" on line 15 is not processed. DETAIL: Syntax error (expected ",") WARNING: "assert-schema" on line 18 is not processed. DETAIL: too much variables for "assert-schema" pragma WARNING: "assert-table" on line 19 is not processed. DETAIL: too much variables for "assert-table" pragma WARNING: "assert-column" on line 20 is not processed. DETAIL: Syntax error (unexpected chars after variable) WARNING: "assert-column" on line 21 is not processed. DETAIL: too few variables for "assert-column" pragma WARNING: "assert-schema" on line 23 is not processed. DETAIL: Syntax error (expected identifier) plpgsql_check_function -------------------------------------------------------------- warning:00000:7:DECLARE:unused variable "v_var" warning extra:00000:4:DECLARE:never read variable "v_schema" warning extra:00000:5:DECLARE:never read variable "v_table" warning extra:00000:6:DECLARE:never read variable "v_column" (4 rows) drop function pr_assert(); drop table pr_tab; drop type pr_ctype; okbob-plpgsql_check-800db79/expected/plpgsql_check_passive-14.out000066400000000000000000000000001526744060500251060ustar00rootroot00000000000000okbob-plpgsql_check-800db79/expected/plpgsql_check_passive-15.out000066400000000000000000000000001526744060500251070ustar00rootroot00000000000000okbob-plpgsql_check-800db79/expected/plpgsql_check_passive-16.out000066400000000000000000000000001526744060500251100ustar00rootroot00000000000000okbob-plpgsql_check-800db79/expected/plpgsql_check_passive-17.out000066400000000000000000000000001526744060500251110ustar00rootroot00000000000000okbob-plpgsql_check-800db79/expected/plpgsql_check_passive-18.out000066400000000000000000000000001526744060500251120ustar00rootroot00000000000000okbob-plpgsql_check-800db79/expected/plpgsql_check_passive-19.out000066400000000000000000000000001526744060500251130ustar00rootroot00000000000000okbob-plpgsql_check-800db79/expected/plpgsql_check_passive-20.out000066400000000000000000000000001526744060500251030ustar00rootroot00000000000000okbob-plpgsql_check-800db79/expected/plpgsql_check_passive.out000066400000000000000000000242361526744060500247050ustar00rootroot00000000000000load 'plpgsql'; load 'plpgsql_check'; set client_min_messages to notice; -- enforce context's displaying -- emulate pre 9.6 behave \set SHOW_CONTEXT always set plpgsql_check.mode = 'every_start'; create table t1(a int, b int); create function f1() returns void as $$ begin if false then update t1 set c = 30; end if; end; $$ language plpgsql; select f1(); ERROR: column "c" of relation "t1" does not exist LINE 1: update t1 set c = 30 ^ QUERY: update t1 set c = 30 CONTEXT: PL/pgSQL function f1() line 4 at SQL statement drop function f1(); create function f1() returns void as $$ begin if false then insert into t1 values(10,20); update t1 set a = 10; delete from t1; end if; end; $$ language plpgsql stable; select f1(); ERROR: INSERT is not allowed in a non volatile function LINE 1: insert into t1 values(10,20) ^ QUERY: insert into t1 values(10,20) CONTEXT: PL/pgSQL function f1() line 4 at SQL statement drop function f1(); create function g1(out a int, out b int) as $$ select 10,20; $$ language sql; create function f1() returns void as $$ declare r record; begin r := g1(); if false then raise notice '%', r.c; end if; end; $$ language plpgsql; select f1(); ERROR: record "r" has no field "c" CONTEXT: SQL expression "r.c" PL/pgSQL function f1() line 6 at RAISE drop function f1(); drop function g1(); create function g1(out a int, out b int) returns setof record as $$ select * from t1; $$ language sql; create function f1() returns void as $$ declare r record; begin for r in select * from g1() loop raise notice '%', r.c; end loop; end; $$ language plpgsql; select f1(); ERROR: record "r" has no field "c" CONTEXT: SQL expression "r.c" PL/pgSQL function f1() line 6 at RAISE create or replace function f1() returns void as $$ declare r record; begin for r in select * from g1() loop r.c := 20; end loop; end; $$ language plpgsql; select f1(); ERROR: record "r" has no field "c" CONTEXT: PL/pgSQL function f1() line 6 at assignment to field "c" of variable "r" declared on line 2 drop function f1(); drop function g1(); create function f1() returns int as $$ declare r int; begin if false then r := a + b; end if; return r; end; $$ language plpgsql; select f1(); ERROR: column "a" does not exist LINE 1: r := a + b ^ QUERY: r := a + b CONTEXT: PL/pgSQL function f1() line 5 at assignment to variable "r" declared on line 2 drop function f1(); create or replace function f1() returns void as $$ declare r int[]; begin if false then r[c+10] := 20; end if; end; $$ language plpgsql; select f1(); ERROR: column "c" does not exist LINE 1: r[c+10] := 20 ^ QUERY: r[c+10] := 20 CONTEXT: PL/pgSQL function f1() line 5 at assignment to variable "r" declared on line 2 drop function f1(); create or replace function f1() returns void as $$ declare r int; begin if false then r[10] := 20; end if; end; $$ language plpgsql; select f1(); ERROR: cannot subscript type integer because it does not support subscripting LINE 1: r[10] := 20 ^ QUERY: r[10] := 20 CONTEXT: PL/pgSQL function f1() line 5 at assignment to variable "r" declared on line 2 drop function f1(); create or replace function f1() returns void as $$ begin if false then insert into badbadtable values(10,20); end if; return; end; $$ language plpgsql; set plpgsql_check.mode = 'fresh_start'; select f1(); ERROR: relation "badbadtable" does not exist LINE 1: insert into badbadtable values(10,20) ^ QUERY: insert into badbadtable values(10,20) CONTEXT: PL/pgSQL function f1() line 4 at SQL statement -- should not raise exception there select f1(); f1 ---- (1 row) create or replace function f1() returns void as $$ begin if false then insert into badbadtable values(10,20); end if; return; end; $$ language plpgsql; -- after refreshing it should to raise exception again select f1(); ERROR: relation "badbadtable" does not exist LINE 1: insert into badbadtable values(10,20) ^ QUERY: insert into badbadtable values(10,20) CONTEXT: PL/pgSQL function f1() line 4 at SQL statement set plpgsql_check.mode = 'every_start'; -- should to raise warning only set plpgsql_check.fatal_errors = false; select f1(); WARNING: relation "badbadtable" does not exist LINE 1: insert into badbadtable values(10,20) ^ QUERY: insert into badbadtable values(10,20) CONTEXT: PL/pgSQL function f1() line 4 at SQL statement PL/pgSQL function f1() line 4 at SQL statement f1 ---- (1 row) drop function f1(); create function f1() returns setof t1 as $$ begin if false then return query select a,a,a from t1; return; end if; end; $$ language plpgsql; select * from f1(); WARNING: structure of query does not match function result type DETAIL: Number of returned columns (3) does not match expected column count (2). CONTEXT: PL/pgSQL function f1() line 4 at RETURN QUERY PL/pgSQL function f1() line 4 at RETURN QUERY a | b ---+--- (0 rows) drop function f1(); create function f1() returns setof t1 as $$ begin if false then return query select a, b::numeric from t1; return; end if; end; $$ language plpgsql; select * from f1(); WARNING: structure of query does not match function result type DETAIL: Returned type numeric does not match expected type integer in column 2. CONTEXT: PL/pgSQL function f1() line 4 at RETURN QUERY PL/pgSQL function f1() line 4 at RETURN QUERY a | b ---+--- (0 rows) drop function f1(); drop table t1; do $$ declare begin if false then for i in 1,3..(2) loop raise notice 'foo %', i; end loop; end if; end; $$; WARNING: query "1,3" returned 2 columns CONTEXT: PL/pgSQL function inline_code_block line 5 at FOR with integer loop variable PL/pgSQL function inline_code_block line 5 at FOR with integer loop variable -- tests designed for 9.2 set check_function_bodies to off; create or replace function f1() returns void as $$ begin if false then raise notice '%', 1, 2; end if; end; $$ language plpgsql; select f1(); ERROR: too many parameters specified for RAISE CONTEXT: compilation of PL/pgSQL function "f1" near line 4 drop function f1(); create or replace function f1() returns void as $$ begin if false then raise notice '% %'; end if; end; $$ language plpgsql; select f1(); ERROR: too few parameters specified for RAISE CONTEXT: compilation of PL/pgSQL function "f1" near line 4 drop function f1(); create type _exception_type as ( state text, message text, detail text); create or replace function f1() returns void as $$ declare _exception record; begin _exception := NULL::_exception_type; exception when others then get stacked diagnostics _exception.state = RETURNED_SQLSTATE, _exception.message = MESSAGE_TEXT, _exception.detail = PG_EXCEPTION_DETAIL, _exception.hint = PG_EXCEPTION_HINT; end; $$ language plpgsql; select f1(); WARNING: record "_exception" has no field "hint" CONTEXT: PL/pgSQL function f1() line 7 at GET STACKED DIAGNOSTICS PL/pgSQL function f1() line 7 at GET STACKED DIAGNOSTICS f1 ---- (1 row) drop function f1(); drop type _exception_type; create table footab(a int, b int, c int); create or replace function footab_trig_func() returns trigger as $$ declare x int; begin if false then -- should be ok; select count(*) from newtab into x; -- should fail; select count(*) from newtab where d = 10 into x; end if; return null; end; $$ language plpgsql; create trigger footab_trigger after insert on footab referencing new table as newtab for each statement execute procedure footab_trig_func(); -- should to fail insert into footab values(1,2,3); WARNING: column "d" does not exist LINE 1: select count(*) from newtab where d = 10 ^ QUERY: select count(*) from newtab where d = 10 CONTEXT: PL/pgSQL function footab_trig_func() line 9 at SQL statement PL/pgSQL function footab_trig_func() line 9 at SQL statement create or replace function footab_trig_func() returns trigger as $$ declare x int; begin if false then -- should be ok; select count(*) from newtab into x; end if; return null; end; $$ language plpgsql; -- should be ok insert into footab values(1,2,3); drop table footab; drop function footab_trig_func(); set plpgsql_check.mode = 'every_start'; create or replace procedure proc_test() as $$ begin commit; end; $$ language plpgsql; call proc_test(); drop procedure proc_test(); -- should not to crash set plpgsql_check.mode = 'fresh_start'; do $$ begin end; $$; create table t(a int, b text); create type ct as (p int, q text); create or replace function trg_fn() returns trigger as $$ begin new.a := new.a + 1; return new; end $$ language plpgsql; create trigger t_trg before insert on t for each row execute function trg_fn(); create or replace function local_rowvar() returns text as $$ declare r t; r2 t%rowtype; c ct; begin r.b := 'x'; r2.b := 'y'; c.q := 'z'; return r.b || r2.b || c.q; end $$ language plpgsql; create or replace function composite_arg(p ct) returns text as $$ begin p.q := 'w'; return p.q; end $$ language plpgsql; -- and everything works without passive checking set plpgsql_check.mode = 'by_function'; insert into t values (1, 'a') returning *; a | b ---+--- 2 | a (1 row) select local_rowvar(); local_rowvar -------------- xyz (1 row) select composite_arg(row(1, 'a')); composite_arg --------------- w (1 row) -- should work: passive mode must not break valid functions -- (fresh_start first: it checks a function only once per compiled version, -- so it has to run before every_start marks the functions as checked) set plpgsql_check.mode = 'fresh_start'; insert into t values (1, 'a') returning *; a | b ---+--- 2 | a (1 row) select local_rowvar(); local_rowvar -------------- xyz (1 row) select composite_arg(row(1, 'a')); composite_arg --------------- w (1 row) set plpgsql_check.mode = 'every_start'; insert into t values (1, 'a') returning *; a | b ---+--- 2 | a (1 row) select local_rowvar(); local_rowvar -------------- xyz (1 row) select composite_arg(row(1, 'a')); composite_arg --------------- w (1 row) drop function composite_arg; drop function local_rowvar; drop table t; drop function trg_fn; drop type ct; okbob-plpgsql_check-800db79/expected/plpgsql_check_passive_2.out000066400000000000000000000242671526744060500251320ustar00rootroot00000000000000load 'plpgsql'; load 'plpgsql_check'; set client_min_messages to notice; -- enforce context's displaying -- emulate pre 9.6 behave \set SHOW_CONTEXT always set plpgsql_check.mode = 'every_start'; create table t1(a int, b int); create function f1() returns void as $$ begin if false then update t1 set c = 30; end if; end; $$ language plpgsql; select f1(); ERROR: column "c" of relation "t1" does not exist LINE 1: update t1 set c = 30 ^ QUERY: update t1 set c = 30 CONTEXT: PL/pgSQL function f1() line 4 at SQL statement drop function f1(); create function f1() returns void as $$ begin if false then insert into t1 values(10,20); update t1 set a = 10; delete from t1; end if; end; $$ language plpgsql stable; select f1(); ERROR: INSERT is not allowed in a non volatile function LINE 1: insert into t1 values(10,20) ^ QUERY: insert into t1 values(10,20) CONTEXT: PL/pgSQL function f1() line 4 at SQL statement drop function f1(); create function g1(out a int, out b int) as $$ select 10,20; $$ language sql; create function f1() returns void as $$ declare r record; begin r := g1(); if false then raise notice '%', r.c; end if; end; $$ language plpgsql; select f1(); ERROR: record "r" has no field "c" CONTEXT: PL/pgSQL expression "r.c" PL/pgSQL function f1() line 6 at RAISE drop function f1(); drop function g1(); create function g1(out a int, out b int) returns setof record as $$ select * from t1; $$ language sql; create function f1() returns void as $$ declare r record; begin for r in select * from g1() loop raise notice '%', r.c; end loop; end; $$ language plpgsql; select f1(); ERROR: record "r" has no field "c" CONTEXT: PL/pgSQL expression "r.c" PL/pgSQL function f1() line 6 at RAISE create or replace function f1() returns void as $$ declare r record; begin for r in select * from g1() loop r.c := 20; end loop; end; $$ language plpgsql; select f1(); ERROR: record "r" has no field "c" CONTEXT: PL/pgSQL function f1() line 6 at assignment to field "c" of variable "r" declared on line 2 drop function f1(); drop function g1(); create function f1() returns int as $$ declare r int; begin if false then r := a + b; end if; return r; end; $$ language plpgsql; select f1(); ERROR: column "a" does not exist LINE 1: r := a + b ^ QUERY: r := a + b CONTEXT: PL/pgSQL function f1() line 5 at assignment to variable "r" declared on line 2 drop function f1(); create or replace function f1() returns void as $$ declare r int[]; begin if false then r[c+10] := 20; end if; end; $$ language plpgsql; select f1(); ERROR: column "c" does not exist LINE 1: r[c+10] := 20 ^ QUERY: r[c+10] := 20 CONTEXT: PL/pgSQL function f1() line 5 at assignment to variable "r" declared on line 2 drop function f1(); create or replace function f1() returns void as $$ declare r int; begin if false then r[10] := 20; end if; end; $$ language plpgsql; select f1(); ERROR: cannot subscript type integer because it does not support subscripting LINE 1: r[10] := 20 ^ QUERY: r[10] := 20 CONTEXT: PL/pgSQL function f1() line 5 at assignment to variable "r" declared on line 2 drop function f1(); create or replace function f1() returns void as $$ begin if false then insert into badbadtable values(10,20); end if; return; end; $$ language plpgsql; set plpgsql_check.mode = 'fresh_start'; select f1(); ERROR: relation "badbadtable" does not exist LINE 1: insert into badbadtable values(10,20) ^ QUERY: insert into badbadtable values(10,20) CONTEXT: PL/pgSQL function f1() line 4 at SQL statement -- should not raise exception there select f1(); f1 ---- (1 row) create or replace function f1() returns void as $$ begin if false then insert into badbadtable values(10,20); end if; return; end; $$ language plpgsql; -- after refreshing it should to raise exception again select f1(); ERROR: relation "badbadtable" does not exist LINE 1: insert into badbadtable values(10,20) ^ QUERY: insert into badbadtable values(10,20) CONTEXT: PL/pgSQL function f1() line 4 at SQL statement set plpgsql_check.mode = 'every_start'; -- should to raise warning only set plpgsql_check.fatal_errors = false; select f1(); WARNING: relation "badbadtable" does not exist LINE 1: insert into badbadtable values(10,20) ^ QUERY: insert into badbadtable values(10,20) CONTEXT: PL/pgSQL function f1() line 4 at SQL statement PL/pgSQL function f1() line 4 at SQL statement f1 ---- (1 row) drop function f1(); create function f1() returns setof t1 as $$ begin if false then return query select a,a,a from t1; return; end if; end; $$ language plpgsql; select * from f1(); WARNING: structure of query does not match function result type DETAIL: Number of returned columns (3) does not match expected column count (2). CONTEXT: PL/pgSQL function f1() line 4 at RETURN QUERY PL/pgSQL function f1() line 4 at RETURN QUERY a | b ---+--- (0 rows) drop function f1(); create function f1() returns setof t1 as $$ begin if false then return query select a, b::numeric from t1; return; end if; end; $$ language plpgsql; select * from f1(); WARNING: structure of query does not match function result type DETAIL: Returned type numeric does not match expected type integer in column "b" (position 2). CONTEXT: PL/pgSQL function f1() line 4 at RETURN QUERY PL/pgSQL function f1() line 4 at RETURN QUERY a | b ---+--- (0 rows) drop function f1(); drop table t1; do $$ declare begin if false then for i in 1,3..(2) loop raise notice 'foo %', i; end loop; end if; end; $$; WARNING: query "1,3" returned 2 columns CONTEXT: PL/pgSQL function inline_code_block line 5 at FOR with integer loop variable PL/pgSQL function inline_code_block line 5 at FOR with integer loop variable -- tests designed for 9.2 set check_function_bodies to off; create or replace function f1() returns void as $$ begin if false then raise notice '%', 1, 2; end if; end; $$ language plpgsql; select f1(); ERROR: too many parameters specified for RAISE CONTEXT: compilation of PL/pgSQL function "f1" near line 4 drop function f1(); create or replace function f1() returns void as $$ begin if false then raise notice '% %'; end if; end; $$ language plpgsql; select f1(); ERROR: too few parameters specified for RAISE CONTEXT: compilation of PL/pgSQL function "f1" near line 4 drop function f1(); create type _exception_type as ( state text, message text, detail text); create or replace function f1() returns void as $$ declare _exception record; begin _exception := NULL::_exception_type; exception when others then get stacked diagnostics _exception.state = RETURNED_SQLSTATE, _exception.message = MESSAGE_TEXT, _exception.detail = PG_EXCEPTION_DETAIL, _exception.hint = PG_EXCEPTION_HINT; end; $$ language plpgsql; select f1(); WARNING: record "_exception" has no field "hint" CONTEXT: PL/pgSQL function f1() line 7 at GET STACKED DIAGNOSTICS PL/pgSQL function f1() line 7 at GET STACKED DIAGNOSTICS f1 ---- (1 row) drop function f1(); drop type _exception_type; create table footab(a int, b int, c int); create or replace function footab_trig_func() returns trigger as $$ declare x int; begin if false then -- should be ok; select count(*) from newtab into x; -- should fail; select count(*) from newtab where d = 10 into x; end if; return null; end; $$ language plpgsql; create trigger footab_trigger after insert on footab referencing new table as newtab for each statement execute procedure footab_trig_func(); -- should to fail insert into footab values(1,2,3); WARNING: column "d" does not exist LINE 1: select count(*) from newtab where d = 10 ^ QUERY: select count(*) from newtab where d = 10 CONTEXT: PL/pgSQL function footab_trig_func() line 9 at SQL statement PL/pgSQL function footab_trig_func() line 9 at SQL statement create or replace function footab_trig_func() returns trigger as $$ declare x int; begin if false then -- should be ok; select count(*) from newtab into x; end if; return null; end; $$ language plpgsql; -- should be ok insert into footab values(1,2,3); drop table footab; drop function footab_trig_func(); set plpgsql_check.mode = 'every_start'; create or replace procedure proc_test() as $$ begin commit; end; $$ language plpgsql; call proc_test(); drop procedure proc_test(); -- should not to crash set plpgsql_check.mode = 'fresh_start'; do $$ begin end; $$; create table t(a int, b text); create type ct as (p int, q text); create or replace function trg_fn() returns trigger as $$ begin new.a := new.a + 1; return new; end $$ language plpgsql; create trigger t_trg before insert on t for each row execute function trg_fn(); create or replace function local_rowvar() returns text as $$ declare r t; r2 t%rowtype; c ct; begin r.b := 'x'; r2.b := 'y'; c.q := 'z'; return r.b || r2.b || c.q; end $$ language plpgsql; create or replace function composite_arg(p ct) returns text as $$ begin p.q := 'w'; return p.q; end $$ language plpgsql; -- and everything works without passive checking set plpgsql_check.mode = 'by_function'; insert into t values (1, 'a') returning *; a | b ---+--- 2 | a (1 row) select local_rowvar(); local_rowvar -------------- xyz (1 row) select composite_arg(row(1, 'a')); composite_arg --------------- w (1 row) -- should work: passive mode must not break valid functions -- (fresh_start first: it checks a function only once per compiled version, -- so it has to run before every_start marks the functions as checked) set plpgsql_check.mode = 'fresh_start'; insert into t values (1, 'a') returning *; a | b ---+--- 2 | a (1 row) select local_rowvar(); local_rowvar -------------- xyz (1 row) select composite_arg(row(1, 'a')); composite_arg --------------- w (1 row) set plpgsql_check.mode = 'every_start'; insert into t values (1, 'a') returning *; a | b ---+--- 2 | a (1 row) select local_rowvar(); local_rowvar -------------- xyz (1 row) select composite_arg(row(1, 'a')); composite_arg --------------- w (1 row) drop function composite_arg; drop function local_rowvar; drop table t; drop function trg_fn; drop type ct; okbob-plpgsql_check-800db79/expected/plpgsql_check_pldbgapi3.out000066400000000000000000000230301526744060500250670ustar00rootroot00000000000000set client_min_messages to warning; create extension if not exists plpgsql_check; set client_min_messages to notice; -- -- Tests of the plugin multiplexer. -- -- The plugin of plpgsql allows one client only, so plpgsql_check -- registers a multiplexer, which dispatches the events to the tracer, -- to the profiler and to the passive check. The multiplexer keeps a -- stack of the entered statements per an executed routine, and it has -- to unwind the stack when an exception aborts the execution. -- -- the multiplexer is installed when the library is loaded, so the -- routines which are already running have no state allocated for them, -- and the events of their statements have to be ignored (this is a no -- operation when the library is preloaded) do $$ begin execute 'load ''plpgsql_check'''; perform 1; end; $$; select plpgsql_check_tracer(true, 'terse'); NOTICE: tracer is active NOTICE: tracer verbosity is terse NOTICE: tracer is still blocked DETAIL: The tracer should be enabled by the superuser for security reasons. HINT: Execute "set plpgsql_check.enable_tracer to on" (superuser only). plpgsql_check_tracer ---------------------- t (1 row) set plpgsql_check.enable_tracer to on; set plpgsql_check.tracer_test_mode to true; set plpgsql_check.profiler to on; create function pd_f1(a int) returns int as $$ begin if a = 0 then raise exception 'pd_f1 failed'; end if; return a; end; $$ language plpgsql; create function pd_f2(a int) returns int as $$ declare res int; begin begin res := pd_f1(a); exception when others then res := -1; end; return res; end; $$ language plpgsql; -- no exception, the statements are closed in the expected order select pd_f2(1); NOTICE: #0 start of pd_f2 (oid=0, tnl=1) NOTICE: #1 start of pd_f1 (oid=0, tnl=2) NOTICE: #1 end of pd_f1 NOTICE: #0 end of pd_f2 pd_f2 ------- 1 (1 row) -- the called routine is aborted, so its part of the stack is unwound -- when the outer routine continues select pd_f2(0); NOTICE: #0 start of pd_f2 (oid=0, tnl=1) NOTICE: #1 start of pd_f1 (oid=0, tnl=2) NOTICE: #1 end of pd_f1 aborted NOTICE: #0 end of pd_f2 pd_f2 ------- -1 (1 row) -- the same, but the exception is raised in a deeper nested block, so -- more statements have to be closed at once create function pd_f3(a int) returns int as $$ declare res int; begin begin for i in 1..3 loop if i = 2 then res := pd_f1(a); end if; end loop; exception when others then res := -1; end; return res; end; $$ language plpgsql; select pd_f3(0); NOTICE: #0 start of pd_f3 (oid=0, tnl=1) NOTICE: #1 start of pd_f1 (oid=0, tnl=2) NOTICE: #1 end of pd_f1 aborted NOTICE: #0 end of pd_f3 pd_f3 ------- -1 (1 row) -- an exception which is not handled by the executed routine leaves the -- whole stack open do $$ begin perform pd_f3(1); perform pd_f1(0); exception when others then raise notice 'handled by the block'; end; $$; NOTICE: #0 start of inline code block (oid=0, tnl=1) NOTICE: #2 start of pd_f3 (oid=0, tnl=2) NOTICE: #3 start of pd_f1 (oid=0, tnl=3) NOTICE: #3 end of pd_f1 NOTICE: #2 end of pd_f3 NOTICE: #2 start of pd_f1 (oid=0, tnl=2) NOTICE: #2 end of pd_f1 aborted NOTICE: handled by the block NOTICE: #0 end of inline code block -- an exception raised by an inline block, which is not handled at all do $$ begin perform pd_f1(0); end; $$; NOTICE: #0 start of inline code block (oid=0, tnl=1) NOTICE: #2 start of pd_f1 (oid=0, tnl=1) NOTICE: #2 end of pd_f1 aborted NOTICE: #0 end of inline code block aborted ERROR: pd_f1 failed CONTEXT: PL/pgSQL function pd_f1(integer) line 4 at RAISE SQL statement "SELECT pd_f1(0)" PL/pgSQL function inline_code_block line 3 at PERFORM -- the memory of an aborted inline block is released later than the -- memory of an aborted routine, so its entry has to be removed from the -- stack of the multiplexer when the outer routine continues create function pd_f4() returns int as $$ declare res int; begin begin execute 'do $x$ begin perform pd_f1(0); end $x$'; exception when others then res := -1; end; res := coalesce(res, 0); return res; end; $$ language plpgsql; select pd_f4(); NOTICE: #0 start of pd_f4 (oid=0, tnl=1) NOTICE: #2 start of inline code block (oid=0, tnl=2) NOTICE: #4 start of pd_f1 (oid=0, tnl=2) NOTICE: #4 end of pd_f1 aborted NOTICE: #2 end of inline code block aborted NOTICE: #0 end of pd_f4 pd_f4 ------- -1 (1 row) -- when a plugin raises an exception, the multiplexer has to restore the -- plugin info of the executed routine before the exception is reraised set plpgsql_check.cursors_leaks to on; set plpgsql_check.cursors_leaks_errlevel to 'error'; create function pd_f5() returns int as $$ declare c refcursor; begin open c for select 1; return 1; end; $$ language plpgsql; -- the leaked cursor is detected when the routine is left set plpgsql_check.strict_cursors_leaks to on; do $$ begin perform pd_f5(); exception when others then raise notice 'leak reported: %', sqlerrm; end; $$; NOTICE: #0 start of inline code block (oid=0, tnl=1) NOTICE: #2 start of pd_f5 (oid=0, tnl=2) NOTICE: #2 end of pd_f5 NOTICE: leak reported: cursor is not closed NOTICE: #0 end of inline code block -- and when the statement which opened it is executed again set plpgsql_check.strict_cursors_leaks to off; create function pd_f6() returns int as $$ declare c refcursor; begin for i in 1..2 loop c := 'pd_cursor_' || i; open c for select 1; end loop; return 1; end; $$ language plpgsql; do $$ begin perform pd_f6(); exception when others then raise notice 'leak reported: %', sqlerrm; end; $$; NOTICE: #0 start of inline code block (oid=0, tnl=1) NOTICE: #2 start of pd_f6 (oid=0, tnl=2) NOTICE: #2 end of pd_f6 aborted NOTICE: leak reported: cursor "c" is not closed NOTICE: #0 end of inline code block set plpgsql_check.strict_cursors_leaks to default; set plpgsql_check.cursors_leaks_errlevel to default; set plpgsql_check.cursors_leaks to default; -- the multiplexer is not used when no plugin is active set plpgsql_check.enable_tracer to off; set plpgsql_check.profiler to off; select pd_f2(0); pd_f2 ------- -1 (1 row) -- the profiler alone set plpgsql_check.profiler to on; select pd_f2(0); pd_f2 ------- -1 (1 row) set plpgsql_check.profiler to off; set plpgsql_check.enable_tracer to off; select plpgsql_check_tracer(false, 'default'); NOTICE: tracer is not active NOTICE: tracer verbosity is default plpgsql_check_tracer ---------------------- f (1 row) -- Recursive callers keep their cursors open, but each invocation closes its own set plpgsql_check.cursors_leaks to on; set plpgsql_check.strict_cursors_leaks to off; set plpgsql_check.cursors_leaks_errlevel to 'error'; create function pd_cursor_recursive(depth int) returns void as $$ declare c refcursor; begin open c for select 1; if depth > 0 then perform pd_cursor_recursive(depth - 1); end if; close c; end; $$ language plpgsql; do $$ begin perform pd_cursor_recursive(1); perform pd_cursor_recursive(1); end; $$; -- Genuine leaks between sequential calls must still be detected do $$ begin perform pd_f5(); perform pd_f5(); exception when invalid_cursor_state then raise notice 'sequential cursor leak detected'; end; $$; NOTICE: sequential cursor leak detected set plpgsql_check.strict_cursors_leaks to default; set plpgsql_check.cursors_leaks_errlevel to default; set plpgsql_check.cursors_leaks to default; drop function pd_cursor_recursive(int); drop function pd_f6(); drop function pd_f5(); drop function pd_f4(); drop function pd_f3(int); drop function pd_f2(int); drop function pd_f1(int); -- when no plugin is active for an aborted routine (the tracer and the -- profiler are disabled now), its entry has to be removed from the stack -- of the multiplexer too, else the stack would point to released memory -- when the outer routine continues create function pd_f7() returns void as $$ begin raise exception 'pd_f7 failed'; end; $$ language plpgsql set plpgsql_check.cursors_leaks = off; create function pd_f8() returns text as $$ begin begin perform pd_f7(); exception when others then return 'handled: ' || sqlerrm; end; return 'not reached'; end; $$ language plpgsql; select pd_f8(); pd_f8 ----------------------- handled: pd_f7 failed (1 row) -- the same when the error is not handled at all: during the cleanup of the -- aborted transaction, the reset callback of the outer routine (with an -- active plugin) must not find the released entry of the inner routine -- on the stack create function pd_f10() returns text as $$ begin perform pd_f7(); return 'not reached'; end; $$ language plpgsql; select pd_f10(); ERROR: pd_f7 failed CONTEXT: PL/pgSQL function pd_f7() line 3 at RAISE SQL statement "SELECT pd_f7()" PL/pgSQL function pd_f10() line 3 at PERFORM -- an inline block without any active plugin, aborted by a routine with -- an active plugin, has to unwind the entry of that routine too create function pd_f9() returns void as $$ begin raise exception 'pd_f9 failed'; end; $$ language plpgsql set plpgsql_check.cursors_leaks = on; set plpgsql_check.cursors_leaks to off; do $$ begin perform pd_f9(); end; $$; ERROR: pd_f9 failed CONTEXT: PL/pgSQL function pd_f9() line 3 at RAISE SQL statement "SELECT pd_f9()" PL/pgSQL function inline_code_block line 3 at PERFORM set plpgsql_check.cursors_leaks to default; select pd_f8(); pd_f8 ----------------------- handled: pd_f7 failed (1 row) drop function pd_f10(); drop function pd_f9(); drop function pd_f8(); drop function pd_f7(); okbob-plpgsql_check-800db79/expected/plpgsql_check_pragma.out000066400000000000000000000265041526744060500245020ustar00rootroot00000000000000-- -- regress tests of the pragma function (src/pragma.c) -- -- The plpgsql_check_pragma() function has two completely separate -- implementations. When plpgsql_check walks the AST, the arguments are -- taken from the parse tree and applied to the check state. When the -- function is really executed, the arguments are processed as ordinary -- variadic text arguments, and only the tracer can be controlled. -- load 'plpgsql_check'; set client_min_messages to warning; create extension if not exists plpgsql_check; set client_min_messages to notice; -- -- compile time pragmas -- -- STATUS reports the current value of every switch. create function prg_status() returns void as $$ begin perform plpgsql_check_pragma('status:check'); perform plpgsql_check_pragma('status:tracer'); perform plpgsql_check_pragma('status:other_warnings'); perform plpgsql_check_pragma('status:performance_warnings'); perform plpgsql_check_pragma('status:extra_warnings'); perform plpgsql_check_pragma('status:security_warnings'); perform plpgsql_check_pragma('status:compatibility_warnings'); perform plpgsql_check_pragma('status:constants_tracing'); -- spaces around the pragma name and its argument are ignored perform plpgsql_check_pragma(' status: check'); end; $$ language plpgsql; select * from plpgsql_check_function('prg_status'); NOTICE: check is enabled NOTICE: tracer is enabled NOTICE: other_warnings is enabled NOTICE: performance_warnings is enabled NOTICE: extra_warnings is enabled NOTICE: security_warnings is enabled NOTICE: compatibility_warnings is enabled NOTICE: constants_tracing is enabled NOTICE: check is enabled plpgsql_check_function ------------------------ (0 rows) -- DISABLE turns every switch off, the following STATUS shows it. create function prg_disable() returns void as $$ begin perform plpgsql_check_pragma(' disable: check'); perform plpgsql_check_pragma('disable:tracer'); perform plpgsql_check_pragma('disable:other_warnings'); perform plpgsql_check_pragma('disable:performance_warnings'); perform plpgsql_check_pragma('disable:extra_warnings'); perform plpgsql_check_pragma('disable:security_warnings'); perform plpgsql_check_pragma('disable:compatibility_warnings'); perform plpgsql_check_pragma('disable:constants_tracing'); perform plpgsql_check_pragma('status:check'); perform plpgsql_check_pragma('status:tracer'); perform plpgsql_check_pragma('status:other_warnings'); perform plpgsql_check_pragma('status:performance_warnings'); perform plpgsql_check_pragma('status:extra_warnings'); perform plpgsql_check_pragma('status:security_warnings'); perform plpgsql_check_pragma('status:compatibility_warnings'); perform plpgsql_check_pragma('status:constants_tracing'); end; $$ language plpgsql; select * from plpgsql_check_function('prg_disable'); NOTICE: check is disabled NOTICE: tracer is disabled NOTICE: other_warnings is disabled NOTICE: performance_warnings is disabled NOTICE: extra_warnings is disabled NOTICE: security_warnings is disabled NOTICE: compatibility_warnings is disabled NOTICE: constants_tracing is disabled plpgsql_check_function ------------------------ (0 rows) -- ENABLE turns them on again create function prg_enable() returns void as $$ begin perform plpgsql_check_pragma('disable:check'); perform plpgsql_check_pragma('disable:tracer'); perform plpgsql_check_pragma('disable:other_warnings'); perform plpgsql_check_pragma('disable:performance_warnings'); perform plpgsql_check_pragma('disable:extra_warnings'); perform plpgsql_check_pragma('disable:security_warnings'); perform plpgsql_check_pragma('disable:compatibility_warnings'); perform plpgsql_check_pragma('disable:constants_tracing'); perform plpgsql_check_pragma(' enable: check'); perform plpgsql_check_pragma('enable:tracer'); perform plpgsql_check_pragma('enable:other_warnings'); perform plpgsql_check_pragma('enable:performance_warnings'); perform plpgsql_check_pragma('enable:extra_warnings'); perform plpgsql_check_pragma('enable:security_warnings'); perform plpgsql_check_pragma('enable:compatibility_warnings'); perform plpgsql_check_pragma('enable:constants_tracing'); perform plpgsql_check_pragma('status:check'); perform plpgsql_check_pragma('status:tracer'); perform plpgsql_check_pragma('status:other_warnings'); perform plpgsql_check_pragma('status:performance_warnings'); perform plpgsql_check_pragma('status:extra_warnings'); perform plpgsql_check_pragma('status:security_warnings'); perform plpgsql_check_pragma('status:compatibility_warnings'); perform plpgsql_check_pragma('status:constants_tracing'); end; $$ language plpgsql; select * from plpgsql_check_function('prg_enable'); NOTICE: check is enabled NOTICE: tracer is enabled NOTICE: other_warnings is enabled NOTICE: performance_warnings is enabled NOTICE: extra_warnings is enabled NOTICE: security_warnings is enabled NOTICE: compatibility_warnings is enabled NOTICE: constants_tracing is enabled plpgsql_check_function ------------------------ (0 rows) create function prg_constants_scope(p boolean) returns void as $$ begin if p then perform plpgsql_check_pragma('disable:constants_tracing'); perform plpgsql_check_pragma('status:constants_tracing'); else perform plpgsql_check_pragma('status:constants_tracing'); end if; perform plpgsql_check_pragma('status:constants_tracing'); end; $$ language plpgsql; select * from plpgsql_check_function('prg_constants_scope'); NOTICE: constants_tracing is disabled NOTICE: constants_tracing is enabled NOTICE: constants_tracing is enabled plpgsql_check_function ------------------------ (0 rows) drop function prg_constants_scope(boolean); -- an unknown switch is reported for every pragma that takes a switch -- name, and an entirely unknown pragma is reported too create function prg_unsupported() returns void as $$ begin perform plpgsql_check_pragma('status:unknown_switch'); perform plpgsql_check_pragma('enable:unknown_switch'); perform plpgsql_check_pragma('disable:unknown_switch'); perform plpgsql_check_pragma('unknown_pragma: something'); end; $$ language plpgsql; select * from plpgsql_check_function('prg_unsupported'); WARNING: unsuported pragma: unknown_switch WARNING: unsuported pragma: unknown_switch WARNING: unsuported pragma: unknown_switch WARNING: unsupported pragma: unknown_pragma: something plpgsql_check_function ------------------------ (0 rows) -- ECHO prints its argument. The @@name and @@signature placeholders are -- replaced by the name and the signature of the checked function, an -- unknown placeholder is copied to the output. create function prg_echo(a int, b text) returns void as $$ begin perform plpgsql_check_pragma('echo: nothing to substitute here'); perform plpgsql_check_pragma('echo: the name is @@name'); perform plpgsql_check_pragma('echo: the signature is @@signature'); perform plpgsql_check_pragma('echo: @@unknown is not a placeholder'); perform plpgsql_check_pragma('echo:'); end; $$ language plpgsql; select * from plpgsql_check_function('prg_echo'); NOTICE: nothing to substitute here NOTICE: the name is prg_echo NOTICE: the signature is prg_echo(integer,text) NOTICE: @@unknown is not a placeholder NOTICE: plpgsql_check_function ------------------------------------------ warning extra:00000:unused parameter "a" warning extra:00000:unused parameter "b" (2 rows) -- more pragmas can be passed in one call create function prg_multi() returns void as $$ begin perform plpgsql_check_pragma('disable:extra_warnings', 'status:extra_warnings', 'enable:extra_warnings', 'status:extra_warnings'); end; $$ language plpgsql; select * from plpgsql_check_function('prg_multi'); NOTICE: extra_warnings is disabled NOTICE: extra_warnings is enabled plpgsql_check_function ------------------------ (0 rows) drop function prg_multi(); drop function prg_echo(int, text); drop function prg_unsupported(); drop function prg_enable(); drop function prg_disable(); drop function prg_status(); -- -- runtime pragmas -- -- When the pragma function is really executed, only the tracer pragmas -- are recognized, everything else is silently ignored. The tracer is -- only armed here, no tracing is started, because that additionally -- requires plpgsql_check.enable_tracer. -- select plpgsql_check_pragma('status:tracer'); NOTICE: tracer is disabled plpgsql_check_pragma ---------------------- 1 (1 row) select plpgsql_check_pragma('enable:tracer'); plpgsql_check_pragma ---------------------- 1 (1 row) select plpgsql_check_pragma('status:tracer'); NOTICE: tracer is enabled plpgsql_check_pragma ---------------------- 1 (1 row) select plpgsql_check_pragma('disable:tracer'); plpgsql_check_pragma ---------------------- 1 (1 row) select plpgsql_check_pragma('status:tracer'); NOTICE: tracer is disabled plpgsql_check_pragma ---------------------- 1 (1 row) -- leading spaces and spaces after the colon are ignored select plpgsql_check_pragma(' enable: tracer'); plpgsql_check_pragma ---------------------- 1 (1 row) select plpgsql_check_pragma(' status: tracer'); NOTICE: tracer is enabled plpgsql_check_pragma ---------------------- 1 (1 row) select plpgsql_check_pragma(' disable: tracer'); plpgsql_check_pragma ---------------------- 1 (1 row) -- pragmas which are compile time only, and unknown pragmas, are ignored select plpgsql_check_pragma('status:check'); plpgsql_check_pragma ---------------------- 1 (1 row) select plpgsql_check_pragma('enable:check'); plpgsql_check_pragma ---------------------- 1 (1 row) select plpgsql_check_pragma('disable:check'); plpgsql_check_pragma ---------------------- 1 (1 row) select plpgsql_check_pragma('echo: not printed in runtime'); plpgsql_check_pragma ---------------------- 1 (1 row) select plpgsql_check_pragma('this is not a pragma'); plpgsql_check_pragma ---------------------- 1 (1 row) select plpgsql_check_pragma(''); plpgsql_check_pragma ---------------------- 1 (1 row) -- NULL argument is ignored, NULL elements of the argument array are -- skipped select plpgsql_check_pragma(VARIADIC NULL::text[]); plpgsql_check_pragma ---------------------- 0 (1 row) select plpgsql_check_pragma(VARIADIC array[NULL, 'enable:tracer', NULL]::text[]); plpgsql_check_pragma ---------------------- 1 (1 row) select plpgsql_check_pragma('status:tracer'); NOTICE: tracer is enabled plpgsql_check_pragma ---------------------- 1 (1 row) select plpgsql_check_pragma('disable:tracer'); plpgsql_check_pragma ---------------------- 1 (1 row) -- the pragma function called from a really executed function create function prg_runtime() returns void as $$ begin perform plpgsql_check_pragma('enable:tracer'); perform plpgsql_check_pragma('status:tracer'); perform plpgsql_check_pragma('disable:tracer'); perform plpgsql_check_pragma('status:tracer'); end; $$ language plpgsql; select prg_runtime(); NOTICE: tracer is enabled NOTICE: tracer is disabled prg_runtime ------------- (1 row) drop function prg_runtime(); okbob-plpgsql_check-800db79/expected/plpgsql_check_profiler.out000066400000000000000000004751161526744060500250640ustar00rootroot00000000000000create table t1(a int, b int); -- profiler check set plpgsql_check.profiler to on; create function f1() returns void as $$ begin if false then insert into t1 values(10,20); update t1 set a = 10; delete from t1; end if; end; $$ language plpgsql; select lineno, stmt_lineno, exec_stmts, source from plpgsql_profiler_function_tb('f1()'); lineno | stmt_lineno | exec_stmts | source --------+-------------+------------+----------------------------------- 1 | | | 2 | 2 | | begin 3 | 3 | | if false then 4 | 4 | | insert into t1 values(10,20); 5 | 5 | | update t1 set a = 10; 6 | 6 | | delete from t1; 7 | | | end if; 8 | | | end; (8 rows) select f1(); f1 ---- (1 row) select lineno, stmt_lineno, exec_stmts, source from plpgsql_profiler_function_tb('f1()'); lineno | stmt_lineno | exec_stmts | source --------+-------------+------------+----------------------------------- 1 | | | 2 | 2 | {1} | begin 3 | 3 | {1} | if false then 4 | 4 | {0} | insert into t1 values(10,20); 5 | 5 | {0} | update t1 set a = 10; 6 | 6 | {0} | delete from t1; 7 | | | end if; 8 | | | end; (8 rows) select plpgsql_profiler_reset('f1()'); plpgsql_profiler_reset ------------------------ (1 row) select lineno, stmt_lineno, exec_stmts, source from plpgsql_profiler_function_tb('f1()'); lineno | stmt_lineno | exec_stmts | source --------+-------------+------------+----------------------------------- 1 | | | 2 | 2 | | begin 3 | 3 | | if false then 4 | 4 | | insert into t1 values(10,20); 5 | 5 | | update t1 set a = 10; 6 | 6 | | delete from t1; 7 | | | end if; 8 | | | end; (8 rows) select f1(); f1 ---- (1 row) select lineno, stmt_lineno, exec_stmts, source from plpgsql_profiler_function_tb('f1()'); lineno | stmt_lineno | exec_stmts | source --------+-------------+------------+----------------------------------- 1 | | | 2 | 2 | {1} | begin 3 | 3 | {1} | if false then 4 | 4 | {0} | insert into t1 values(10,20); 5 | 5 | {0} | update t1 set a = 10; 6 | 6 | {0} | delete from t1; 7 | | | end if; 8 | | | end; (8 rows) select plpgsql_profiler_reset_all(); plpgsql_profiler_reset_all ---------------------------- (1 row) select lineno, stmt_lineno, exec_stmts, source from plpgsql_profiler_function_tb('f1()'); lineno | stmt_lineno | exec_stmts | source --------+-------------+------------+----------------------------------- 1 | | | 2 | 2 | | begin 3 | 3 | | if false then 4 | 4 | | insert into t1 values(10,20); 5 | 5 | | update t1 set a = 10; 6 | 6 | | delete from t1; 7 | | | end if; 8 | | | end; (8 rows) drop function f1(); create or replace function f1() returns void as $$ begin raise notice '1'; exception when others then raise notice '2'; end; $$ language plpgsql; select f1(); NOTICE: 1 f1 ---- (1 row) select lineno, stmt_lineno, exec_stmts, source from plpgsql_profiler_function_tb('f1()'); lineno | stmt_lineno | exec_stmts | source --------+-------------+------------+---------------------------- 1 | | | 2 | 2 | {1} | begin 3 | 3 | {1} | raise notice '1'; 4 | | | exception when others then 5 | 5 | {0} | raise notice '2'; 6 | | | end; (6 rows) drop function f1(); -- test queryid retrieval create function f1() returns void as $$ declare t1 text = 't1'; begin insert into t1 values(10,20); EXECUTE 'update ' || 't1' || ' set a = 10'; EXECUTE 'delete from ' || t1; end; $$ language plpgsql; select plpgsql_profiler_reset_all(); plpgsql_profiler_reset_all ---------------------------- (1 row) select plpgsql_profiler_install_fake_queryid_hook(); plpgsql_profiler_install_fake_queryid_hook -------------------------------------------- (1 row) select f1(); f1 ---- (1 row) select queryids, lineno, stmt_lineno, exec_stmts, source from plpgsql_profiler_function_tb('f1()'); queryids | lineno | stmt_lineno | exec_stmts | source ----------+--------+-------------+------------+------------------------------------------------ | 1 | | | | 2 | | | declare | 3 | | | t1 text = 't1'; | 4 | 4 | {1} | begin {3} | 5 | 5 | {1} | insert into t1 values(10,20); {NULL} | 6 | 6 | {1} | EXECUTE 'update ' || 't1' || ' set a = 10'; {NULL} | 7 | 7 | {1} | EXECUTE 'delete from ' || t1; | 8 | | | end; (8 rows) select plpgsql_profiler_remove_fake_queryid_hook(); plpgsql_profiler_remove_fake_queryid_hook ------------------------------------------- (1 row) drop function f1(); -- don't crash on empty dynamic query create or replace function f1() returns void as $$ begin execute ''; end; $$ language plpgsql; select f1(); f1 ---- (1 row) drop function f1(); create or replace function covtest(int) returns int as $$ declare a int = $1; begin a := a + 1; if a < 10 then a := a + 1; drop function test_function(); drop function test_function(); drop function test_function(); drop function test_function(); drop function test_function(); drop function test_function(); drop function test_function(); drop function test_function(); drop function test_function(); drop function test_function(); drop function test_function(); drop function test_function; drop function test_function(); drop function test_function(); drop function test_function(); drop function test_function(); drop function test_function(); drop function test_function(); drop function test_function(); drop function test_function(); drop function test_function(); drop function test_function(); drop function test_function(); drop function test_function(); drop function test_function; drop function test_function(); end if; a := a + 1; return a; end; $$ language plpgsql; select covtest(10); covtest --------- 12 (1 row) -- should not crash select stmtid, exec_stmts, stmtname from plpgsql_profiler_function_statements_tb('covtest'); stmtid | exec_stmts | stmtname --------+------------+----------------- 1 | 1 | statement block 2 | 1 | assignment 3 | 1 | IF 4 | 0 | assignment 5 | 0 | SQL statement 6 | 0 | SQL statement 7 | 0 | SQL statement 8 | 0 | SQL statement 9 | 0 | SQL statement 10 | 0 | SQL statement 11 | 0 | SQL statement 12 | 0 | SQL statement 13 | 0 | SQL statement 14 | 0 | SQL statement 15 | 0 | SQL statement 16 | 0 | SQL statement 17 | 0 | SQL statement 18 | 0 | SQL statement 19 | 0 | SQL statement 20 | 0 | SQL statement 21 | 0 | SQL statement 22 | 0 | SQL statement 23 | 0 | SQL statement 24 | 0 | SQL statement 25 | 0 | SQL statement 26 | 0 | SQL statement 27 | 0 | SQL statement 28 | 0 | SQL statement 29 | 0 | SQL statement 30 | 0 | SQL statement 31 | 1 | assignment 32 | 1 | RETURN (32 rows) drop function covtest; set plpgsql_check.profiler to off; -- test of very long function inside profiler create or replace function longfx(int) returns int as $$ declare s int default 0; j int default 0; r record; begin begin while j < 10 loop for i in 1..1 loop for r in select * from generate_series(1,1) loop s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; end loop; end loop; j := j + 1; end loop; exception when others then raise 'reraised exception %', sqlerrm; end; return $1; end; $$ language plpgsql; select lineno, stmt_lineno, exec_stmts, source from plpgsql_profiler_function_tb('longfx'); lineno | stmt_lineno | exec_stmts | source --------+-------------+------------+----------------------------------------------------- 1 | | | 2 | | | declare 3 | | | s int default 0; 4 | | | j int default 0; 5 | | | r record; 6 | 6 | | begin 7 | 7 | | begin 8 | 8 | | while j < 10 9 | | | loop 10 | 10 | | for i in 1..1 11 | | | loop 12 | 12 | | for r in select * from generate_series(1,1) 13 | | | loop 14 | 14 | | s := s + 1; 15 | 15 | | s := s + 1; 16 | 16 | | s := s + 1; 17 | 17 | | s := s + 1; 18 | 18 | | s := s + 1; 19 | 19 | | s := s + 1; 20 | 20 | | s := s + 1; 21 | 21 | | s := s + 1; 22 | 22 | | s := s + 1; 23 | 23 | | s := s + 1; 24 | 24 | | s := s + 1; 25 | 25 | | s := s + 1; 26 | 26 | | s := s + 1; 27 | 27 | | s := s + 1; 28 | 28 | | s := s + 1; 29 | 29 | | s := s + 1; 30 | 30 | | s := s + 1; 31 | 31 | | s := s + 1; 32 | 32 | | s := s + 1; 33 | 33 | | s := s + 1; 34 | 34 | | s := s + 1; 35 | 35 | | s := s + 1; 36 | 36 | | s := s + 1; 37 | 37 | | s := s + 1; 38 | 38 | | s := s + 1; 39 | 39 | | s := s + 1; 40 | 40 | | s := s + 1; 41 | 41 | | s := s + 1; 42 | 42 | | s := s + 1; 43 | 43 | | s := s + 1; 44 | 44 | | s := s + 1; 45 | 45 | | s := s + 1; 46 | 46 | | s := s + 1; 47 | 47 | | s := s + 1; 48 | 48 | | s := s + 1; 49 | 49 | | s := s + 1; 50 | 50 | | s := s + 1; 51 | 51 | | s := s + 1; 52 | 52 | | s := s + 1; 53 | 53 | | s := s + 1; 54 | 54 | | s := s + 1; 55 | 55 | | s := s + 1; 56 | 56 | | s := s + 1; 57 | 57 | | s := s + 1; 58 | 58 | | s := s + 1; 59 | 59 | | s := s + 1; 60 | 60 | | s := s + 1; 61 | 61 | | s := s + 1; 62 | 62 | | s := s + 1; 63 | 63 | | s := s + 1; 64 | 64 | | s := s + 1; 65 | 65 | | s := s + 1; 66 | 66 | | s := s + 1; 67 | 67 | | s := s + 1; 68 | 68 | | s := s + 1; 69 | 69 | | s := s + 1; 70 | 70 | | s := s + 1; 71 | 71 | | s := s + 1; 72 | 72 | | s := s + 1; 73 | 73 | | s := s + 1; 74 | 74 | | s := s + 1; 75 | 75 | | s := s + 1; 76 | 76 | | s := s + 1; 77 | 77 | | s := s + 1; 78 | 78 | | s := s + 1; 79 | 79 | | s := s + 1; 80 | 80 | | s := s + 1; 81 | 81 | | s := s + 1; 82 | 82 | | s := s + 1; 83 | 83 | | s := s + 1; 84 | 84 | | s := s + 1; 85 | 85 | | s := s + 1; 86 | 86 | | s := s + 1; 87 | 87 | | s := s + 1; 88 | 88 | | s := s + 1; 89 | 89 | | s := s + 1; 90 | 90 | | s := s + 1; 91 | 91 | | s := s + 1; 92 | 92 | | s := s + 1; 93 | 93 | | s := s + 1; 94 | 94 | | s := s + 1; 95 | 95 | | s := s + 1; 96 | 96 | | s := s + 1; 97 | 97 | | s := s + 1; 98 | 98 | | s := s + 1; 99 | 99 | | s := s + 1; 100 | 100 | | s := s + 1; 101 | 101 | | s := s + 1; 102 | 102 | | s := s + 1; 103 | 103 | | s := s + 1; 104 | 104 | | s := s + 1; 105 | 105 | | s := s + 1; 106 | 106 | | s := s + 1; 107 | 107 | | s := s + 1; 108 | 108 | | s := s + 1; 109 | 109 | | s := s + 1; 110 | 110 | | s := s + 1; 111 | 111 | | s := s + 1; 112 | 112 | | s := s + 1; 113 | 113 | | s := s + 1; 114 | 114 | | s := s + 1; 115 | 115 | | s := s + 1; 116 | 116 | | s := s + 1; 117 | 117 | | s := s + 1; 118 | 118 | | s := s + 1; 119 | 119 | | s := s + 1; 120 | 120 | | s := s + 1; 121 | 121 | | s := s + 1; 122 | 122 | | s := s + 1; 123 | 123 | | s := s + 1; 124 | 124 | | s := s + 1; 125 | 125 | | s := s + 1; 126 | 126 | | s := s + 1; 127 | 127 | | s := s + 1; 128 | 128 | | s := s + 1; 129 | 129 | | s := s + 1; 130 | 130 | | s := s + 1; 131 | 131 | | s := s + 1; 132 | 132 | | s := s + 1; 133 | 133 | | s := s + 1; 134 | 134 | | s := s + 1; 135 | 135 | | s := s + 1; 136 | 136 | | s := s + 1; 137 | 137 | | s := s + 1; 138 | 138 | | s := s + 1; 139 | 139 | | s := s + 1; 140 | 140 | | s := s + 1; 141 | 141 | | s := s + 1; 142 | 142 | | s := s + 1; 143 | 143 | | s := s + 1; 144 | 144 | | s := s + 1; 145 | 145 | | s := s + 1; 146 | 146 | | s := s + 1; 147 | 147 | | s := s + 1; 148 | 148 | | s := s + 1; 149 | 149 | | s := s + 1; 150 | 150 | | s := s + 1; 151 | 151 | | s := s + 1; 152 | 152 | | s := s + 1; 153 | 153 | | s := s + 1; 154 | 154 | | s := s + 1; 155 | 155 | | s := s + 1; 156 | 156 | | s := s + 1; 157 | 157 | | s := s + 1; 158 | 158 | | s := s + 1; 159 | 159 | | s := s + 1; 160 | 160 | | s := s + 1; 161 | 161 | | s := s + 1; 162 | 162 | | s := s + 1; 163 | 163 | | s := s + 1; 164 | 164 | | s := s + 1; 165 | 165 | | s := s + 1; 166 | 166 | | s := s + 1; 167 | 167 | | s := s + 1; 168 | 168 | | s := s + 1; 169 | 169 | | s := s + 1; 170 | 170 | | s := s + 1; 171 | 171 | | s := s + 1; 172 | 172 | | s := s + 1; 173 | 173 | | s := s + 1; 174 | 174 | | s := s + 1; 175 | 175 | | s := s + 1; 176 | 176 | | s := s + 1; 177 | 177 | | s := s + 1; 178 | 178 | | s := s + 1; 179 | 179 | | s := s + 1; 180 | 180 | | s := s + 1; 181 | 181 | | s := s + 1; 182 | 182 | | s := s + 1; 183 | 183 | | s := s + 1; 184 | 184 | | s := s + 1; 185 | 185 | | s := s + 1; 186 | 186 | | s := s + 1; 187 | 187 | | s := s + 1; 188 | 188 | | s := s + 1; 189 | 189 | | s := s + 1; 190 | 190 | | s := s + 1; 191 | 191 | | s := s + 1; 192 | 192 | | s := s + 1; 193 | 193 | | s := s + 1; 194 | 194 | | s := s + 1; 195 | 195 | | s := s + 1; 196 | 196 | | s := s + 1; 197 | 197 | | s := s + 1; 198 | 198 | | s := s + 1; 199 | 199 | | s := s + 1; 200 | 200 | | s := s + 1; 201 | 201 | | s := s + 1; 202 | 202 | | s := s + 1; 203 | 203 | | s := s + 1; 204 | 204 | | s := s + 1; 205 | 205 | | s := s + 1; 206 | 206 | | s := s + 1; 207 | 207 | | s := s + 1; 208 | 208 | | s := s + 1; 209 | 209 | | s := s + 1; 210 | 210 | | s := s + 1; 211 | 211 | | s := s + 1; 212 | 212 | | s := s + 1; 213 | 213 | | s := s + 1; 214 | 214 | | s := s + 1; 215 | 215 | | s := s + 1; 216 | 216 | | s := s + 1; 217 | 217 | | s := s + 1; 218 | 218 | | s := s + 1; 219 | 219 | | s := s + 1; 220 | 220 | | s := s + 1; 221 | 221 | | s := s + 1; 222 | 222 | | s := s + 1; 223 | 223 | | s := s + 1; 224 | 224 | | s := s + 1; 225 | 225 | | s := s + 1; 226 | 226 | | s := s + 1; 227 | 227 | | s := s + 1; 228 | 228 | | s := s + 1; 229 | 229 | | s := s + 1; 230 | 230 | | s := s + 1; 231 | 231 | | s := s + 1; 232 | 232 | | s := s + 1; 233 | 233 | | s := s + 1; 234 | 234 | | s := s + 1; 235 | 235 | | s := s + 1; 236 | 236 | | s := s + 1; 237 | 237 | | s := s + 1; 238 | 238 | | s := s + 1; 239 | 239 | | s := s + 1; 240 | 240 | | s := s + 1; 241 | 241 | | s := s + 1; 242 | 242 | | s := s + 1; 243 | 243 | | s := s + 1; 244 | 244 | | s := s + 1; 245 | 245 | | s := s + 1; 246 | 246 | | s := s + 1; 247 | 247 | | s := s + 1; 248 | 248 | | s := s + 1; 249 | 249 | | s := s + 1; 250 | 250 | | s := s + 1; 251 | 251 | | s := s + 1; 252 | 252 | | s := s + 1; 253 | 253 | | s := s + 1; 254 | 254 | | s := s + 1; 255 | 255 | | s := s + 1; 256 | 256 | | s := s + 1; 257 | 257 | | s := s + 1; 258 | 258 | | s := s + 1; 259 | 259 | | s := s + 1; 260 | 260 | | s := s + 1; 261 | 261 | | s := s + 1; 262 | 262 | | s := s + 1; 263 | 263 | | s := s + 1; 264 | 264 | | s := s + 1; 265 | 265 | | s := s + 1; 266 | 266 | | s := s + 1; 267 | 267 | | s := s + 1; 268 | 268 | | s := s + 1; 269 | 269 | | s := s + 1; 270 | 270 | | s := s + 1; 271 | 271 | | s := s + 1; 272 | 272 | | s := s + 1; 273 | 273 | | s := s + 1; 274 | 274 | | s := s + 1; 275 | 275 | | s := s + 1; 276 | 276 | | s := s + 1; 277 | 277 | | s := s + 1; 278 | 278 | | s := s + 1; 279 | 279 | | s := s + 1; 280 | 280 | | s := s + 1; 281 | 281 | | s := s + 1; 282 | 282 | | s := s + 1; 283 | 283 | | s := s + 1; 284 | 284 | | s := s + 1; 285 | 285 | | s := s + 1; 286 | 286 | | s := s + 1; 287 | 287 | | s := s + 1; 288 | 288 | | s := s + 1; 289 | 289 | | s := s + 1; 290 | 290 | | s := s + 1; 291 | 291 | | s := s + 1; 292 | 292 | | s := s + 1; 293 | 293 | | s := s + 1; 294 | 294 | | s := s + 1; 295 | 295 | | s := s + 1; 296 | 296 | | s := s + 1; 297 | 297 | | s := s + 1; 298 | 298 | | s := s + 1; 299 | 299 | | s := s + 1; 300 | 300 | | s := s + 1; 301 | 301 | | s := s + 1; 302 | 302 | | s := s + 1; 303 | 303 | | s := s + 1; 304 | 304 | | s := s + 1; 305 | 305 | | s := s + 1; 306 | 306 | | s := s + 1; 307 | 307 | | s := s + 1; 308 | 308 | | s := s + 1; 309 | 309 | | s := s + 1; 310 | 310 | | s := s + 1; 311 | 311 | | s := s + 1; 312 | 312 | | s := s + 1; 313 | 313 | | s := s + 1; 314 | 314 | | s := s + 1; 315 | 315 | | s := s + 1; 316 | 316 | | s := s + 1; 317 | 317 | | s := s + 1; 318 | 318 | | s := s + 1; 319 | 319 | | s := s + 1; 320 | 320 | | s := s + 1; 321 | 321 | | s := s + 1; 322 | 322 | | s := s + 1; 323 | 323 | | s := s + 1; 324 | 324 | | s := s + 1; 325 | 325 | | s := s + 1; 326 | 326 | | s := s + 1; 327 | 327 | | s := s + 1; 328 | 328 | | s := s + 1; 329 | 329 | | s := s + 1; 330 | 330 | | s := s + 1; 331 | 331 | | s := s + 1; 332 | 332 | | s := s + 1; 333 | 333 | | s := s + 1; 334 | 334 | | s := s + 1; 335 | 335 | | s := s + 1; 336 | 336 | | s := s + 1; 337 | 337 | | s := s + 1; 338 | 338 | | s := s + 1; 339 | 339 | | s := s + 1; 340 | 340 | | s := s + 1; 341 | 341 | | s := s + 1; 342 | 342 | | s := s + 1; 343 | 343 | | s := s + 1; 344 | 344 | | s := s + 1; 345 | 345 | | s := s + 1; 346 | 346 | | s := s + 1; 347 | 347 | | s := s + 1; 348 | 348 | | s := s + 1; 349 | 349 | | s := s + 1; 350 | 350 | | s := s + 1; 351 | 351 | | s := s + 1; 352 | 352 | | s := s + 1; 353 | 353 | | s := s + 1; 354 | 354 | | s := s + 1; 355 | 355 | | s := s + 1; 356 | 356 | | s := s + 1; 357 | 357 | | s := s + 1; 358 | 358 | | s := s + 1; 359 | 359 | | s := s + 1; 360 | 360 | | s := s + 1; 361 | 361 | | s := s + 1; 362 | 362 | | s := s + 1; 363 | 363 | | s := s + 1; 364 | 364 | | s := s + 1; 365 | 365 | | s := s + 1; 366 | 366 | | s := s + 1; 367 | 367 | | s := s + 1; 368 | 368 | | s := s + 1; 369 | 369 | | s := s + 1; 370 | 370 | | s := s + 1; 371 | 371 | | s := s + 1; 372 | 372 | | s := s + 1; 373 | 373 | | s := s + 1; 374 | 374 | | s := s + 1; 375 | 375 | | s := s + 1; 376 | 376 | | s := s + 1; 377 | 377 | | s := s + 1; 378 | 378 | | s := s + 1; 379 | 379 | | s := s + 1; 380 | 380 | | s := s + 1; 381 | 381 | | s := s + 1; 382 | 382 | | s := s + 1; 383 | 383 | | s := s + 1; 384 | 384 | | s := s + 1; 385 | 385 | | s := s + 1; 386 | 386 | | s := s + 1; 387 | 387 | | s := s + 1; 388 | 388 | | s := s + 1; 389 | 389 | | s := s + 1; 390 | 390 | | s := s + 1; 391 | 391 | | s := s + 1; 392 | 392 | | s := s + 1; 393 | 393 | | s := s + 1; 394 | 394 | | s := s + 1; 395 | 395 | | s := s + 1; 396 | 396 | | s := s + 1; 397 | 397 | | s := s + 1; 398 | 398 | | s := s + 1; 399 | 399 | | s := s + 1; 400 | 400 | | s := s + 1; 401 | 401 | | s := s + 1; 402 | 402 | | s := s + 1; 403 | 403 | | s := s + 1; 404 | 404 | | s := s + 1; 405 | 405 | | s := s + 1; 406 | 406 | | s := s + 1; 407 | 407 | | s := s + 1; 408 | 408 | | s := s + 1; 409 | 409 | | s := s + 1; 410 | 410 | | s := s + 1; 411 | 411 | | s := s + 1; 412 | 412 | | s := s + 1; 413 | 413 | | s := s + 1; 414 | 414 | | s := s + 1; 415 | 415 | | s := s + 1; 416 | 416 | | s := s + 1; 417 | 417 | | s := s + 1; 418 | 418 | | s := s + 1; 419 | 419 | | s := s + 1; 420 | 420 | | s := s + 1; 421 | 421 | | s := s + 1; 422 | 422 | | s := s + 1; 423 | 423 | | s := s + 1; 424 | 424 | | s := s + 1; 425 | 425 | | s := s + 1; 426 | 426 | | s := s + 1; 427 | 427 | | s := s + 1; 428 | 428 | | s := s + 1; 429 | 429 | | s := s + 1; 430 | 430 | | s := s + 1; 431 | 431 | | s := s + 1; 432 | 432 | | s := s + 1; 433 | 433 | | s := s + 1; 434 | 434 | | s := s + 1; 435 | 435 | | s := s + 1; 436 | 436 | | s := s + 1; 437 | 437 | | s := s + 1; 438 | 438 | | s := s + 1; 439 | 439 | | s := s + 1; 440 | 440 | | s := s + 1; 441 | 441 | | s := s + 1; 442 | 442 | | s := s + 1; 443 | 443 | | s := s + 1; 444 | 444 | | s := s + 1; 445 | 445 | | s := s + 1; 446 | 446 | | s := s + 1; 447 | 447 | | s := s + 1; 448 | 448 | | s := s + 1; 449 | 449 | | s := s + 1; 450 | 450 | | s := s + 1; 451 | 451 | | s := s + 1; 452 | 452 | | s := s + 1; 453 | 453 | | s := s + 1; 454 | 454 | | s := s + 1; 455 | 455 | | s := s + 1; 456 | 456 | | s := s + 1; 457 | 457 | | s := s + 1; 458 | 458 | | s := s + 1; 459 | 459 | | s := s + 1; 460 | 460 | | s := s + 1; 461 | 461 | | s := s + 1; 462 | 462 | | s := s + 1; 463 | 463 | | s := s + 1; 464 | 464 | | s := s + 1; 465 | 465 | | s := s + 1; 466 | 466 | | s := s + 1; 467 | 467 | | s := s + 1; 468 | 468 | | s := s + 1; 469 | 469 | | s := s + 1; 470 | 470 | | s := s + 1; 471 | 471 | | s := s + 1; 472 | 472 | | s := s + 1; 473 | 473 | | s := s + 1; 474 | 474 | | s := s + 1; 475 | 475 | | s := s + 1; 476 | 476 | | s := s + 1; 477 | 477 | | s := s + 1; 478 | 478 | | s := s + 1; 479 | 479 | | s := s + 1; 480 | 480 | | s := s + 1; 481 | 481 | | s := s + 1; 482 | 482 | | s := s + 1; 483 | 483 | | s := s + 1; 484 | 484 | | s := s + 1; 485 | 485 | | s := s + 1; 486 | 486 | | s := s + 1; 487 | 487 | | s := s + 1; 488 | 488 | | s := s + 1; 489 | 489 | | s := s + 1; 490 | 490 | | s := s + 1; 491 | 491 | | s := s + 1; 492 | 492 | | s := s + 1; 493 | 493 | | s := s + 1; 494 | 494 | | s := s + 1; 495 | 495 | | s := s + 1; 496 | 496 | | s := s + 1; 497 | 497 | | s := s + 1; 498 | 498 | | s := s + 1; 499 | 499 | | s := s + 1; 500 | 500 | | s := s + 1; 501 | 501 | | s := s + 1; 502 | 502 | | s := s + 1; 503 | 503 | | s := s + 1; 504 | 504 | | s := s + 1; 505 | 505 | | s := s + 1; 506 | 506 | | s := s + 1; 507 | 507 | | s := s + 1; 508 | 508 | | s := s + 1; 509 | 509 | | s := s + 1; 510 | 510 | | s := s + 1; 511 | 511 | | s := s + 1; 512 | 512 | | s := s + 1; 513 | 513 | | s := s + 1; 514 | 514 | | s := s + 1; 515 | 515 | | s := s + 1; 516 | 516 | | s := s + 1; 517 | 517 | | s := s + 1; 518 | 518 | | s := s + 1; 519 | 519 | | s := s + 1; 520 | 520 | | s := s + 1; 521 | 521 | | s := s + 1; 522 | 522 | | s := s + 1; 523 | 523 | | s := s + 1; 524 | 524 | | s := s + 1; 525 | 525 | | s := s + 1; 526 | 526 | | s := s + 1; 527 | 527 | | s := s + 1; 528 | 528 | | s := s + 1; 529 | 529 | | s := s + 1; 530 | 530 | | s := s + 1; 531 | 531 | | s := s + 1; 532 | 532 | | s := s + 1; 533 | 533 | | s := s + 1; 534 | 534 | | s := s + 1; 535 | 535 | | s := s + 1; 536 | 536 | | s := s + 1; 537 | 537 | | s := s + 1; 538 | 538 | | s := s + 1; 539 | 539 | | s := s + 1; 540 | 540 | | s := s + 1; 541 | 541 | | s := s + 1; 542 | 542 | | s := s + 1; 543 | 543 | | s := s + 1; 544 | 544 | | s := s + 1; 545 | 545 | | s := s + 1; 546 | 546 | | s := s + 1; 547 | 547 | | s := s + 1; 548 | 548 | | s := s + 1; 549 | 549 | | s := s + 1; 550 | 550 | | s := s + 1; 551 | 551 | | s := s + 1; 552 | 552 | | s := s + 1; 553 | 553 | | s := s + 1; 554 | 554 | | s := s + 1; 555 | 555 | | s := s + 1; 556 | 556 | | s := s + 1; 557 | 557 | | s := s + 1; 558 | 558 | | s := s + 1; 559 | 559 | | s := s + 1; 560 | 560 | | s := s + 1; 561 | 561 | | s := s + 1; 562 | 562 | | s := s + 1; 563 | 563 | | s := s + 1; 564 | 564 | | s := s + 1; 565 | 565 | | s := s + 1; 566 | 566 | | s := s + 1; 567 | 567 | | s := s + 1; 568 | 568 | | s := s + 1; 569 | 569 | | s := s + 1; 570 | 570 | | s := s + 1; 571 | 571 | | s := s + 1; 572 | 572 | | s := s + 1; 573 | 573 | | s := s + 1; 574 | 574 | | s := s + 1; 575 | 575 | | s := s + 1; 576 | 576 | | s := s + 1; 577 | 577 | | s := s + 1; 578 | 578 | | s := s + 1; 579 | 579 | | s := s + 1; 580 | 580 | | s := s + 1; 581 | 581 | | s := s + 1; 582 | 582 | | s := s + 1; 583 | 583 | | s := s + 1; 584 | 584 | | s := s + 1; 585 | 585 | | s := s + 1; 586 | 586 | | s := s + 1; 587 | 587 | | s := s + 1; 588 | 588 | | s := s + 1; 589 | 589 | | s := s + 1; 590 | 590 | | s := s + 1; 591 | 591 | | s := s + 1; 592 | 592 | | s := s + 1; 593 | 593 | | s := s + 1; 594 | 594 | | s := s + 1; 595 | 595 | | s := s + 1; 596 | 596 | | s := s + 1; 597 | 597 | | s := s + 1; 598 | 598 | | s := s + 1; 599 | 599 | | s := s + 1; 600 | 600 | | s := s + 1; 601 | 601 | | s := s + 1; 602 | 602 | | s := s + 1; 603 | 603 | | s := s + 1; 604 | 604 | | s := s + 1; 605 | 605 | | s := s + 1; 606 | 606 | | s := s + 1; 607 | 607 | | s := s + 1; 608 | 608 | | s := s + 1; 609 | 609 | | s := s + 1; 610 | 610 | | s := s + 1; 611 | 611 | | s := s + 1; 612 | 612 | | s := s + 1; 613 | 613 | | s := s + 1; 614 | 614 | | s := s + 1; 615 | 615 | | s := s + 1; 616 | 616 | | s := s + 1; 617 | 617 | | s := s + 1; 618 | 618 | | s := s + 1; 619 | 619 | | s := s + 1; 620 | 620 | | s := s + 1; 621 | 621 | | s := s + 1; 622 | 622 | | s := s + 1; 623 | 623 | | s := s + 1; 624 | 624 | | s := s + 1; 625 | 625 | | s := s + 1; 626 | 626 | | s := s + 1; 627 | 627 | | s := s + 1; 628 | 628 | | s := s + 1; 629 | 629 | | s := s + 1; 630 | 630 | | s := s + 1; 631 | 631 | | s := s + 1; 632 | 632 | | s := s + 1; 633 | 633 | | s := s + 1; 634 | 634 | | s := s + 1; 635 | 635 | | s := s + 1; 636 | 636 | | s := s + 1; 637 | 637 | | s := s + 1; 638 | 638 | | s := s + 1; 639 | 639 | | s := s + 1; 640 | 640 | | s := s + 1; 641 | 641 | | s := s + 1; 642 | 642 | | s := s + 1; 643 | 643 | | s := s + 1; 644 | 644 | | s := s + 1; 645 | 645 | | s := s + 1; 646 | 646 | | s := s + 1; 647 | 647 | | s := s + 1; 648 | 648 | | s := s + 1; 649 | 649 | | s := s + 1; 650 | 650 | | s := s + 1; 651 | 651 | | s := s + 1; 652 | 652 | | s := s + 1; 653 | 653 | | s := s + 1; 654 | 654 | | s := s + 1; 655 | 655 | | s := s + 1; 656 | 656 | | s := s + 1; 657 | 657 | | s := s + 1; 658 | 658 | | s := s + 1; 659 | 659 | | s := s + 1; 660 | 660 | | s := s + 1; 661 | 661 | | s := s + 1; 662 | 662 | | s := s + 1; 663 | 663 | | s := s + 1; 664 | 664 | | s := s + 1; 665 | 665 | | s := s + 1; 666 | 666 | | s := s + 1; 667 | 667 | | s := s + 1; 668 | 668 | | s := s + 1; 669 | 669 | | s := s + 1; 670 | 670 | | s := s + 1; 671 | 671 | | s := s + 1; 672 | 672 | | s := s + 1; 673 | 673 | | s := s + 1; 674 | 674 | | s := s + 1; 675 | 675 | | s := s + 1; 676 | 676 | | s := s + 1; 677 | 677 | | s := s + 1; 678 | 678 | | s := s + 1; 679 | 679 | | s := s + 1; 680 | 680 | | s := s + 1; 681 | 681 | | s := s + 1; 682 | 682 | | s := s + 1; 683 | 683 | | s := s + 1; 684 | 684 | | s := s + 1; 685 | 685 | | s := s + 1; 686 | 686 | | s := s + 1; 687 | 687 | | s := s + 1; 688 | 688 | | s := s + 1; 689 | 689 | | s := s + 1; 690 | 690 | | s := s + 1; 691 | 691 | | s := s + 1; 692 | 692 | | s := s + 1; 693 | 693 | | s := s + 1; 694 | 694 | | s := s + 1; 695 | 695 | | s := s + 1; 696 | 696 | | s := s + 1; 697 | 697 | | s := s + 1; 698 | 698 | | s := s + 1; 699 | 699 | | s := s + 1; 700 | 700 | | s := s + 1; 701 | 701 | | s := s + 1; 702 | 702 | | s := s + 1; 703 | 703 | | s := s + 1; 704 | 704 | | s := s + 1; 705 | 705 | | s := s + 1; 706 | 706 | | s := s + 1; 707 | 707 | | s := s + 1; 708 | 708 | | s := s + 1; 709 | 709 | | s := s + 1; 710 | 710 | | s := s + 1; 711 | 711 | | s := s + 1; 712 | 712 | | s := s + 1; 713 | 713 | | s := s + 1; 714 | 714 | | s := s + 1; 715 | 715 | | s := s + 1; 716 | 716 | | s := s + 1; 717 | 717 | | s := s + 1; 718 | 718 | | s := s + 1; 719 | 719 | | s := s + 1; 720 | 720 | | s := s + 1; 721 | 721 | | s := s + 1; 722 | 722 | | s := s + 1; 723 | 723 | | s := s + 1; 724 | 724 | | s := s + 1; 725 | 725 | | s := s + 1; 726 | 726 | | s := s + 1; 727 | 727 | | s := s + 1; 728 | 728 | | s := s + 1; 729 | 729 | | s := s + 1; 730 | 730 | | s := s + 1; 731 | 731 | | s := s + 1; 732 | 732 | | s := s + 1; 733 | 733 | | s := s + 1; 734 | 734 | | s := s + 1; 735 | 735 | | s := s + 1; 736 | 736 | | s := s + 1; 737 | 737 | | s := s + 1; 738 | 738 | | s := s + 1; 739 | 739 | | s := s + 1; 740 | 740 | | s := s + 1; 741 | 741 | | s := s + 1; 742 | 742 | | s := s + 1; 743 | 743 | | s := s + 1; 744 | 744 | | s := s + 1; 745 | 745 | | s := s + 1; 746 | 746 | | s := s + 1; 747 | 747 | | s := s + 1; 748 | 748 | | s := s + 1; 749 | 749 | | s := s + 1; 750 | 750 | | s := s + 1; 751 | 751 | | s := s + 1; 752 | 752 | | s := s + 1; 753 | 753 | | s := s + 1; 754 | 754 | | s := s + 1; 755 | 755 | | s := s + 1; 756 | 756 | | s := s + 1; 757 | 757 | | s := s + 1; 758 | 758 | | s := s + 1; 759 | 759 | | s := s + 1; 760 | 760 | | s := s + 1; 761 | 761 | | s := s + 1; 762 | 762 | | s := s + 1; 763 | 763 | | s := s + 1; 764 | 764 | | s := s + 1; 765 | 765 | | s := s + 1; 766 | 766 | | s := s + 1; 767 | 767 | | s := s + 1; 768 | 768 | | s := s + 1; 769 | 769 | | s := s + 1; 770 | 770 | | s := s + 1; 771 | 771 | | s := s + 1; 772 | 772 | | s := s + 1; 773 | 773 | | s := s + 1; 774 | 774 | | s := s + 1; 775 | 775 | | s := s + 1; 776 | 776 | | s := s + 1; 777 | 777 | | s := s + 1; 778 | 778 | | s := s + 1; 779 | 779 | | s := s + 1; 780 | 780 | | s := s + 1; 781 | 781 | | s := s + 1; 782 | 782 | | s := s + 1; 783 | 783 | | s := s + 1; 784 | 784 | | s := s + 1; 785 | 785 | | s := s + 1; 786 | 786 | | s := s + 1; 787 | 787 | | s := s + 1; 788 | 788 | | s := s + 1; 789 | 789 | | s := s + 1; 790 | 790 | | s := s + 1; 791 | 791 | | s := s + 1; 792 | 792 | | s := s + 1; 793 | 793 | | s := s + 1; 794 | 794 | | s := s + 1; 795 | 795 | | s := s + 1; 796 | 796 | | s := s + 1; 797 | 797 | | s := s + 1; 798 | 798 | | s := s + 1; 799 | 799 | | s := s + 1; 800 | 800 | | s := s + 1; 801 | 801 | | s := s + 1; 802 | 802 | | s := s + 1; 803 | 803 | | s := s + 1; 804 | 804 | | s := s + 1; 805 | 805 | | s := s + 1; 806 | 806 | | s := s + 1; 807 | 807 | | s := s + 1; 808 | 808 | | s := s + 1; 809 | 809 | | s := s + 1; 810 | 810 | | s := s + 1; 811 | 811 | | s := s + 1; 812 | 812 | | s := s + 1; 813 | 813 | | s := s + 1; 814 | 814 | | s := s + 1; 815 | 815 | | s := s + 1; 816 | 816 | | s := s + 1; 817 | 817 | | s := s + 1; 818 | 818 | | s := s + 1; 819 | 819 | | s := s + 1; 820 | 820 | | s := s + 1; 821 | 821 | | s := s + 1; 822 | 822 | | s := s + 1; 823 | 823 | | s := s + 1; 824 | 824 | | s := s + 1; 825 | 825 | | s := s + 1; 826 | 826 | | s := s + 1; 827 | 827 | | s := s + 1; 828 | 828 | | s := s + 1; 829 | 829 | | s := s + 1; 830 | 830 | | s := s + 1; 831 | 831 | | s := s + 1; 832 | 832 | | s := s + 1; 833 | 833 | | s := s + 1; 834 | 834 | | s := s + 1; 835 | 835 | | s := s + 1; 836 | 836 | | s := s + 1; 837 | 837 | | s := s + 1; 838 | 838 | | s := s + 1; 839 | 839 | | s := s + 1; 840 | 840 | | s := s + 1; 841 | 841 | | s := s + 1; 842 | 842 | | s := s + 1; 843 | 843 | | s := s + 1; 844 | 844 | | s := s + 1; 845 | 845 | | s := s + 1; 846 | 846 | | s := s + 1; 847 | 847 | | s := s + 1; 848 | 848 | | s := s + 1; 849 | 849 | | s := s + 1; 850 | 850 | | s := s + 1; 851 | 851 | | s := s + 1; 852 | 852 | | s := s + 1; 853 | 853 | | s := s + 1; 854 | 854 | | s := s + 1; 855 | 855 | | s := s + 1; 856 | 856 | | s := s + 1; 857 | 857 | | s := s + 1; 858 | 858 | | s := s + 1; 859 | 859 | | s := s + 1; 860 | 860 | | s := s + 1; 861 | 861 | | s := s + 1; 862 | 862 | | s := s + 1; 863 | 863 | | s := s + 1; 864 | 864 | | s := s + 1; 865 | 865 | | s := s + 1; 866 | 866 | | s := s + 1; 867 | 867 | | s := s + 1; 868 | 868 | | s := s + 1; 869 | 869 | | s := s + 1; 870 | 870 | | s := s + 1; 871 | 871 | | s := s + 1; 872 | 872 | | s := s + 1; 873 | 873 | | s := s + 1; 874 | 874 | | s := s + 1; 875 | 875 | | s := s + 1; 876 | 876 | | s := s + 1; 877 | 877 | | s := s + 1; 878 | 878 | | s := s + 1; 879 | 879 | | s := s + 1; 880 | 880 | | s := s + 1; 881 | 881 | | s := s + 1; 882 | 882 | | s := s + 1; 883 | 883 | | s := s + 1; 884 | 884 | | s := s + 1; 885 | 885 | | s := s + 1; 886 | 886 | | s := s + 1; 887 | 887 | | s := s + 1; 888 | 888 | | s := s + 1; 889 | 889 | | s := s + 1; 890 | 890 | | s := s + 1; 891 | 891 | | s := s + 1; 892 | 892 | | s := s + 1; 893 | 893 | | s := s + 1; 894 | 894 | | s := s + 1; 895 | 895 | | s := s + 1; 896 | 896 | | s := s + 1; 897 | 897 | | s := s + 1; 898 | 898 | | s := s + 1; 899 | 899 | | s := s + 1; 900 | 900 | | s := s + 1; 901 | 901 | | s := s + 1; 902 | 902 | | s := s + 1; 903 | 903 | | s := s + 1; 904 | 904 | | s := s + 1; 905 | 905 | | s := s + 1; 906 | 906 | | s := s + 1; 907 | 907 | | s := s + 1; 908 | 908 | | s := s + 1; 909 | 909 | | s := s + 1; 910 | 910 | | s := s + 1; 911 | 911 | | s := s + 1; 912 | 912 | | s := s + 1; 913 | 913 | | s := s + 1; 914 | 914 | | s := s + 1; 915 | 915 | | s := s + 1; 916 | 916 | | s := s + 1; 917 | 917 | | s := s + 1; 918 | 918 | | s := s + 1; 919 | 919 | | s := s + 1; 920 | 920 | | s := s + 1; 921 | 921 | | s := s + 1; 922 | 922 | | s := s + 1; 923 | 923 | | s := s + 1; 924 | 924 | | s := s + 1; 925 | 925 | | s := s + 1; 926 | 926 | | s := s + 1; 927 | 927 | | s := s + 1; 928 | 928 | | s := s + 1; 929 | 929 | | s := s + 1; 930 | 930 | | s := s + 1; 931 | 931 | | s := s + 1; 932 | 932 | | s := s + 1; 933 | 933 | | s := s + 1; 934 | 934 | | s := s + 1; 935 | 935 | | s := s + 1; 936 | 936 | | s := s + 1; 937 | 937 | | s := s + 1; 938 | 938 | | s := s + 1; 939 | 939 | | s := s + 1; 940 | 940 | | s := s + 1; 941 | 941 | | s := s + 1; 942 | 942 | | s := s + 1; 943 | 943 | | s := s + 1; 944 | 944 | | s := s + 1; 945 | 945 | | s := s + 1; 946 | 946 | | s := s + 1; 947 | 947 | | s := s + 1; 948 | 948 | | s := s + 1; 949 | 949 | | s := s + 1; 950 | 950 | | s := s + 1; 951 | 951 | | s := s + 1; 952 | 952 | | s := s + 1; 953 | 953 | | s := s + 1; 954 | 954 | | s := s + 1; 955 | 955 | | s := s + 1; 956 | 956 | | s := s + 1; 957 | 957 | | s := s + 1; 958 | 958 | | s := s + 1; 959 | 959 | | s := s + 1; 960 | 960 | | s := s + 1; 961 | 961 | | s := s + 1; 962 | 962 | | s := s + 1; 963 | 963 | | s := s + 1; 964 | 964 | | s := s + 1; 965 | 965 | | s := s + 1; 966 | 966 | | s := s + 1; 967 | 967 | | s := s + 1; 968 | 968 | | s := s + 1; 969 | 969 | | s := s + 1; 970 | 970 | | s := s + 1; 971 | 971 | | s := s + 1; 972 | 972 | | s := s + 1; 973 | 973 | | s := s + 1; 974 | 974 | | s := s + 1; 975 | 975 | | s := s + 1; 976 | 976 | | s := s + 1; 977 | 977 | | s := s + 1; 978 | 978 | | s := s + 1; 979 | 979 | | s := s + 1; 980 | 980 | | s := s + 1; 981 | 981 | | s := s + 1; 982 | 982 | | s := s + 1; 983 | 983 | | s := s + 1; 984 | 984 | | s := s + 1; 985 | 985 | | s := s + 1; 986 | 986 | | s := s + 1; 987 | 987 | | s := s + 1; 988 | 988 | | s := s + 1; 989 | 989 | | s := s + 1; 990 | 990 | | s := s + 1; 991 | 991 | | s := s + 1; 992 | 992 | | s := s + 1; 993 | 993 | | s := s + 1; 994 | 994 | | s := s + 1; 995 | 995 | | s := s + 1; 996 | 996 | | s := s + 1; 997 | 997 | | s := s + 1; 998 | 998 | | s := s + 1; 999 | 999 | | s := s + 1; 1000 | 1000 | | s := s + 1; 1001 | 1001 | | s := s + 1; 1002 | 1002 | | s := s + 1; 1003 | 1003 | | s := s + 1; 1004 | 1004 | | s := s + 1; 1005 | 1005 | | s := s + 1; 1006 | 1006 | | s := s + 1; 1007 | 1007 | | s := s + 1; 1008 | 1008 | | s := s + 1; 1009 | 1009 | | s := s + 1; 1010 | 1010 | | s := s + 1; 1011 | 1011 | | s := s + 1; 1012 | 1012 | | s := s + 1; 1013 | 1013 | | s := s + 1; 1014 | 1014 | | s := s + 1; 1015 | 1015 | | s := s + 1; 1016 | 1016 | | s := s + 1; 1017 | 1017 | | s := s + 1; 1018 | 1018 | | s := s + 1; 1019 | 1019 | | s := s + 1; 1020 | 1020 | | s := s + 1; 1021 | 1021 | | s := s + 1; 1022 | 1022 | | s := s + 1; 1023 | 1023 | | s := s + 1; 1024 | 1024 | | s := s + 1; 1025 | 1025 | | s := s + 1; 1026 | 1026 | | s := s + 1; 1027 | 1027 | | s := s + 1; 1028 | 1028 | | s := s + 1; 1029 | 1029 | | s := s + 1; 1030 | 1030 | | s := s + 1; 1031 | 1031 | | s := s + 1; 1032 | 1032 | | s := s + 1; 1033 | 1033 | | s := s + 1; 1034 | 1034 | | s := s + 1; 1035 | 1035 | | s := s + 1; 1036 | 1036 | | s := s + 1; 1037 | 1037 | | s := s + 1; 1038 | | | end loop; 1039 | | | end loop; 1040 | 1040 | | j := j + 1; 1041 | | | end loop; 1042 | | | exception when others then 1043 | 1043 | | raise 'reraised exception %', sqlerrm; 1044 | | | end; 1045 | 1045 | | return $1; 1046 | | | end; (1046 rows) set plpgsql_check.profiler = on; select longfx(10); longfx -------- 10 (1 row) select longfx(10); longfx -------- 10 (1 row) set plpgsql_check.profiler = off; select longfx(10); longfx -------- 10 (1 row) select lineno, stmt_lineno, exec_stmts, source from plpgsql_profiler_function_tb('longfx'); lineno | stmt_lineno | exec_stmts | source --------+-------------+------------+----------------------------------------------------- 1 | | | 2 | | | declare 3 | | | s int default 0; 4 | | | j int default 0; 5 | | | r record; 6 | 6 | {2} | begin 7 | 7 | {2} | begin 8 | 8 | {2} | while j < 10 9 | | | loop 10 | 10 | {20} | for i in 1..1 11 | | | loop 12 | 12 | {20} | for r in select * from generate_series(1,1) 13 | | | loop 14 | 14 | {20} | s := s + 1; 15 | 15 | {20} | s := s + 1; 16 | 16 | {20} | s := s + 1; 17 | 17 | {20} | s := s + 1; 18 | 18 | {20} | s := s + 1; 19 | 19 | {20} | s := s + 1; 20 | 20 | {20} | s := s + 1; 21 | 21 | {20} | s := s + 1; 22 | 22 | {20} | s := s + 1; 23 | 23 | {20} | s := s + 1; 24 | 24 | {20} | s := s + 1; 25 | 25 | {20} | s := s + 1; 26 | 26 | {20} | s := s + 1; 27 | 27 | {20} | s := s + 1; 28 | 28 | {20} | s := s + 1; 29 | 29 | {20} | s := s + 1; 30 | 30 | {20} | s := s + 1; 31 | 31 | {20} | s := s + 1; 32 | 32 | {20} | s := s + 1; 33 | 33 | {20} | s := s + 1; 34 | 34 | {20} | s := s + 1; 35 | 35 | {20} | s := s + 1; 36 | 36 | {20} | s := s + 1; 37 | 37 | {20} | s := s + 1; 38 | 38 | {20} | s := s + 1; 39 | 39 | {20} | s := s + 1; 40 | 40 | {20} | s := s + 1; 41 | 41 | {20} | s := s + 1; 42 | 42 | {20} | s := s + 1; 43 | 43 | {20} | s := s + 1; 44 | 44 | {20} | s := s + 1; 45 | 45 | {20} | s := s + 1; 46 | 46 | {20} | s := s + 1; 47 | 47 | {20} | s := s + 1; 48 | 48 | {20} | s := s + 1; 49 | 49 | {20} | s := s + 1; 50 | 50 | {20} | s := s + 1; 51 | 51 | {20} | s := s + 1; 52 | 52 | {20} | s := s + 1; 53 | 53 | {20} | s := s + 1; 54 | 54 | {20} | s := s + 1; 55 | 55 | {20} | s := s + 1; 56 | 56 | {20} | s := s + 1; 57 | 57 | {20} | s := s + 1; 58 | 58 | {20} | s := s + 1; 59 | 59 | {20} | s := s + 1; 60 | 60 | {20} | s := s + 1; 61 | 61 | {20} | s := s + 1; 62 | 62 | {20} | s := s + 1; 63 | 63 | {20} | s := s + 1; 64 | 64 | {20} | s := s + 1; 65 | 65 | {20} | s := s + 1; 66 | 66 | {20} | s := s + 1; 67 | 67 | {20} | s := s + 1; 68 | 68 | {20} | s := s + 1; 69 | 69 | {20} | s := s + 1; 70 | 70 | {20} | s := s + 1; 71 | 71 | {20} | s := s + 1; 72 | 72 | {20} | s := s + 1; 73 | 73 | {20} | s := s + 1; 74 | 74 | {20} | s := s + 1; 75 | 75 | {20} | s := s + 1; 76 | 76 | {20} | s := s + 1; 77 | 77 | {20} | s := s + 1; 78 | 78 | {20} | s := s + 1; 79 | 79 | {20} | s := s + 1; 80 | 80 | {20} | s := s + 1; 81 | 81 | {20} | s := s + 1; 82 | 82 | {20} | s := s + 1; 83 | 83 | {20} | s := s + 1; 84 | 84 | {20} | s := s + 1; 85 | 85 | {20} | s := s + 1; 86 | 86 | {20} | s := s + 1; 87 | 87 | {20} | s := s + 1; 88 | 88 | {20} | s := s + 1; 89 | 89 | {20} | s := s + 1; 90 | 90 | {20} | s := s + 1; 91 | 91 | {20} | s := s + 1; 92 | 92 | {20} | s := s + 1; 93 | 93 | {20} | s := s + 1; 94 | 94 | {20} | s := s + 1; 95 | 95 | {20} | s := s + 1; 96 | 96 | {20} | s := s + 1; 97 | 97 | {20} | s := s + 1; 98 | 98 | {20} | s := s + 1; 99 | 99 | {20} | s := s + 1; 100 | 100 | {20} | s := s + 1; 101 | 101 | {20} | s := s + 1; 102 | 102 | {20} | s := s + 1; 103 | 103 | {20} | s := s + 1; 104 | 104 | {20} | s := s + 1; 105 | 105 | {20} | s := s + 1; 106 | 106 | {20} | s := s + 1; 107 | 107 | {20} | s := s + 1; 108 | 108 | {20} | s := s + 1; 109 | 109 | {20} | s := s + 1; 110 | 110 | {20} | s := s + 1; 111 | 111 | {20} | s := s + 1; 112 | 112 | {20} | s := s + 1; 113 | 113 | {20} | s := s + 1; 114 | 114 | {20} | s := s + 1; 115 | 115 | {20} | s := s + 1; 116 | 116 | {20} | s := s + 1; 117 | 117 | {20} | s := s + 1; 118 | 118 | {20} | s := s + 1; 119 | 119 | {20} | s := s + 1; 120 | 120 | {20} | s := s + 1; 121 | 121 | {20} | s := s + 1; 122 | 122 | {20} | s := s + 1; 123 | 123 | {20} | s := s + 1; 124 | 124 | {20} | s := s + 1; 125 | 125 | {20} | s := s + 1; 126 | 126 | {20} | s := s + 1; 127 | 127 | {20} | s := s + 1; 128 | 128 | {20} | s := s + 1; 129 | 129 | {20} | s := s + 1; 130 | 130 | {20} | s := s + 1; 131 | 131 | {20} | s := s + 1; 132 | 132 | {20} | s := s + 1; 133 | 133 | {20} | s := s + 1; 134 | 134 | {20} | s := s + 1; 135 | 135 | {20} | s := s + 1; 136 | 136 | {20} | s := s + 1; 137 | 137 | {20} | s := s + 1; 138 | 138 | {20} | s := s + 1; 139 | 139 | {20} | s := s + 1; 140 | 140 | {20} | s := s + 1; 141 | 141 | {20} | s := s + 1; 142 | 142 | {20} | s := s + 1; 143 | 143 | {20} | s := s + 1; 144 | 144 | {20} | s := s + 1; 145 | 145 | {20} | s := s + 1; 146 | 146 | {20} | s := s + 1; 147 | 147 | {20} | s := s + 1; 148 | 148 | {20} | s := s + 1; 149 | 149 | {20} | s := s + 1; 150 | 150 | {20} | s := s + 1; 151 | 151 | {20} | s := s + 1; 152 | 152 | {20} | s := s + 1; 153 | 153 | {20} | s := s + 1; 154 | 154 | {20} | s := s + 1; 155 | 155 | {20} | s := s + 1; 156 | 156 | {20} | s := s + 1; 157 | 157 | {20} | s := s + 1; 158 | 158 | {20} | s := s + 1; 159 | 159 | {20} | s := s + 1; 160 | 160 | {20} | s := s + 1; 161 | 161 | {20} | s := s + 1; 162 | 162 | {20} | s := s + 1; 163 | 163 | {20} | s := s + 1; 164 | 164 | {20} | s := s + 1; 165 | 165 | {20} | s := s + 1; 166 | 166 | {20} | s := s + 1; 167 | 167 | {20} | s := s + 1; 168 | 168 | {20} | s := s + 1; 169 | 169 | {20} | s := s + 1; 170 | 170 | {20} | s := s + 1; 171 | 171 | {20} | s := s + 1; 172 | 172 | {20} | s := s + 1; 173 | 173 | {20} | s := s + 1; 174 | 174 | {20} | s := s + 1; 175 | 175 | {20} | s := s + 1; 176 | 176 | {20} | s := s + 1; 177 | 177 | {20} | s := s + 1; 178 | 178 | {20} | s := s + 1; 179 | 179 | {20} | s := s + 1; 180 | 180 | {20} | s := s + 1; 181 | 181 | {20} | s := s + 1; 182 | 182 | {20} | s := s + 1; 183 | 183 | {20} | s := s + 1; 184 | 184 | {20} | s := s + 1; 185 | 185 | {20} | s := s + 1; 186 | 186 | {20} | s := s + 1; 187 | 187 | {20} | s := s + 1; 188 | 188 | {20} | s := s + 1; 189 | 189 | {20} | s := s + 1; 190 | 190 | {20} | s := s + 1; 191 | 191 | {20} | s := s + 1; 192 | 192 | {20} | s := s + 1; 193 | 193 | {20} | s := s + 1; 194 | 194 | {20} | s := s + 1; 195 | 195 | {20} | s := s + 1; 196 | 196 | {20} | s := s + 1; 197 | 197 | {20} | s := s + 1; 198 | 198 | {20} | s := s + 1; 199 | 199 | {20} | s := s + 1; 200 | 200 | {20} | s := s + 1; 201 | 201 | {20} | s := s + 1; 202 | 202 | {20} | s := s + 1; 203 | 203 | {20} | s := s + 1; 204 | 204 | {20} | s := s + 1; 205 | 205 | {20} | s := s + 1; 206 | 206 | {20} | s := s + 1; 207 | 207 | {20} | s := s + 1; 208 | 208 | {20} | s := s + 1; 209 | 209 | {20} | s := s + 1; 210 | 210 | {20} | s := s + 1; 211 | 211 | {20} | s := s + 1; 212 | 212 | {20} | s := s + 1; 213 | 213 | {20} | s := s + 1; 214 | 214 | {20} | s := s + 1; 215 | 215 | {20} | s := s + 1; 216 | 216 | {20} | s := s + 1; 217 | 217 | {20} | s := s + 1; 218 | 218 | {20} | s := s + 1; 219 | 219 | {20} | s := s + 1; 220 | 220 | {20} | s := s + 1; 221 | 221 | {20} | s := s + 1; 222 | 222 | {20} | s := s + 1; 223 | 223 | {20} | s := s + 1; 224 | 224 | {20} | s := s + 1; 225 | 225 | {20} | s := s + 1; 226 | 226 | {20} | s := s + 1; 227 | 227 | {20} | s := s + 1; 228 | 228 | {20} | s := s + 1; 229 | 229 | {20} | s := s + 1; 230 | 230 | {20} | s := s + 1; 231 | 231 | {20} | s := s + 1; 232 | 232 | {20} | s := s + 1; 233 | 233 | {20} | s := s + 1; 234 | 234 | {20} | s := s + 1; 235 | 235 | {20} | s := s + 1; 236 | 236 | {20} | s := s + 1; 237 | 237 | {20} | s := s + 1; 238 | 238 | {20} | s := s + 1; 239 | 239 | {20} | s := s + 1; 240 | 240 | {20} | s := s + 1; 241 | 241 | {20} | s := s + 1; 242 | 242 | {20} | s := s + 1; 243 | 243 | {20} | s := s + 1; 244 | 244 | {20} | s := s + 1; 245 | 245 | {20} | s := s + 1; 246 | 246 | {20} | s := s + 1; 247 | 247 | {20} | s := s + 1; 248 | 248 | {20} | s := s + 1; 249 | 249 | {20} | s := s + 1; 250 | 250 | {20} | s := s + 1; 251 | 251 | {20} | s := s + 1; 252 | 252 | {20} | s := s + 1; 253 | 253 | {20} | s := s + 1; 254 | 254 | {20} | s := s + 1; 255 | 255 | {20} | s := s + 1; 256 | 256 | {20} | s := s + 1; 257 | 257 | {20} | s := s + 1; 258 | 258 | {20} | s := s + 1; 259 | 259 | {20} | s := s + 1; 260 | 260 | {20} | s := s + 1; 261 | 261 | {20} | s := s + 1; 262 | 262 | {20} | s := s + 1; 263 | 263 | {20} | s := s + 1; 264 | 264 | {20} | s := s + 1; 265 | 265 | {20} | s := s + 1; 266 | 266 | {20} | s := s + 1; 267 | 267 | {20} | s := s + 1; 268 | 268 | {20} | s := s + 1; 269 | 269 | {20} | s := s + 1; 270 | 270 | {20} | s := s + 1; 271 | 271 | {20} | s := s + 1; 272 | 272 | {20} | s := s + 1; 273 | 273 | {20} | s := s + 1; 274 | 274 | {20} | s := s + 1; 275 | 275 | {20} | s := s + 1; 276 | 276 | {20} | s := s + 1; 277 | 277 | {20} | s := s + 1; 278 | 278 | {20} | s := s + 1; 279 | 279 | {20} | s := s + 1; 280 | 280 | {20} | s := s + 1; 281 | 281 | {20} | s := s + 1; 282 | 282 | {20} | s := s + 1; 283 | 283 | {20} | s := s + 1; 284 | 284 | {20} | s := s + 1; 285 | 285 | {20} | s := s + 1; 286 | 286 | {20} | s := s + 1; 287 | 287 | {20} | s := s + 1; 288 | 288 | {20} | s := s + 1; 289 | 289 | {20} | s := s + 1; 290 | 290 | {20} | s := s + 1; 291 | 291 | {20} | s := s + 1; 292 | 292 | {20} | s := s + 1; 293 | 293 | {20} | s := s + 1; 294 | 294 | {20} | s := s + 1; 295 | 295 | {20} | s := s + 1; 296 | 296 | {20} | s := s + 1; 297 | 297 | {20} | s := s + 1; 298 | 298 | {20} | s := s + 1; 299 | 299 | {20} | s := s + 1; 300 | 300 | {20} | s := s + 1; 301 | 301 | {20} | s := s + 1; 302 | 302 | {20} | s := s + 1; 303 | 303 | {20} | s := s + 1; 304 | 304 | {20} | s := s + 1; 305 | 305 | {20} | s := s + 1; 306 | 306 | {20} | s := s + 1; 307 | 307 | {20} | s := s + 1; 308 | 308 | {20} | s := s + 1; 309 | 309 | {20} | s := s + 1; 310 | 310 | {20} | s := s + 1; 311 | 311 | {20} | s := s + 1; 312 | 312 | {20} | s := s + 1; 313 | 313 | {20} | s := s + 1; 314 | 314 | {20} | s := s + 1; 315 | 315 | {20} | s := s + 1; 316 | 316 | {20} | s := s + 1; 317 | 317 | {20} | s := s + 1; 318 | 318 | {20} | s := s + 1; 319 | 319 | {20} | s := s + 1; 320 | 320 | {20} | s := s + 1; 321 | 321 | {20} | s := s + 1; 322 | 322 | {20} | s := s + 1; 323 | 323 | {20} | s := s + 1; 324 | 324 | {20} | s := s + 1; 325 | 325 | {20} | s := s + 1; 326 | 326 | {20} | s := s + 1; 327 | 327 | {20} | s := s + 1; 328 | 328 | {20} | s := s + 1; 329 | 329 | {20} | s := s + 1; 330 | 330 | {20} | s := s + 1; 331 | 331 | {20} | s := s + 1; 332 | 332 | {20} | s := s + 1; 333 | 333 | {20} | s := s + 1; 334 | 334 | {20} | s := s + 1; 335 | 335 | {20} | s := s + 1; 336 | 336 | {20} | s := s + 1; 337 | 337 | {20} | s := s + 1; 338 | 338 | {20} | s := s + 1; 339 | 339 | {20} | s := s + 1; 340 | 340 | {20} | s := s + 1; 341 | 341 | {20} | s := s + 1; 342 | 342 | {20} | s := s + 1; 343 | 343 | {20} | s := s + 1; 344 | 344 | {20} | s := s + 1; 345 | 345 | {20} | s := s + 1; 346 | 346 | {20} | s := s + 1; 347 | 347 | {20} | s := s + 1; 348 | 348 | {20} | s := s + 1; 349 | 349 | {20} | s := s + 1; 350 | 350 | {20} | s := s + 1; 351 | 351 | {20} | s := s + 1; 352 | 352 | {20} | s := s + 1; 353 | 353 | {20} | s := s + 1; 354 | 354 | {20} | s := s + 1; 355 | 355 | {20} | s := s + 1; 356 | 356 | {20} | s := s + 1; 357 | 357 | {20} | s := s + 1; 358 | 358 | {20} | s := s + 1; 359 | 359 | {20} | s := s + 1; 360 | 360 | {20} | s := s + 1; 361 | 361 | {20} | s := s + 1; 362 | 362 | {20} | s := s + 1; 363 | 363 | {20} | s := s + 1; 364 | 364 | {20} | s := s + 1; 365 | 365 | {20} | s := s + 1; 366 | 366 | {20} | s := s + 1; 367 | 367 | {20} | s := s + 1; 368 | 368 | {20} | s := s + 1; 369 | 369 | {20} | s := s + 1; 370 | 370 | {20} | s := s + 1; 371 | 371 | {20} | s := s + 1; 372 | 372 | {20} | s := s + 1; 373 | 373 | {20} | s := s + 1; 374 | 374 | {20} | s := s + 1; 375 | 375 | {20} | s := s + 1; 376 | 376 | {20} | s := s + 1; 377 | 377 | {20} | s := s + 1; 378 | 378 | {20} | s := s + 1; 379 | 379 | {20} | s := s + 1; 380 | 380 | {20} | s := s + 1; 381 | 381 | {20} | s := s + 1; 382 | 382 | {20} | s := s + 1; 383 | 383 | {20} | s := s + 1; 384 | 384 | {20} | s := s + 1; 385 | 385 | {20} | s := s + 1; 386 | 386 | {20} | s := s + 1; 387 | 387 | {20} | s := s + 1; 388 | 388 | {20} | s := s + 1; 389 | 389 | {20} | s := s + 1; 390 | 390 | {20} | s := s + 1; 391 | 391 | {20} | s := s + 1; 392 | 392 | {20} | s := s + 1; 393 | 393 | {20} | s := s + 1; 394 | 394 | {20} | s := s + 1; 395 | 395 | {20} | s := s + 1; 396 | 396 | {20} | s := s + 1; 397 | 397 | {20} | s := s + 1; 398 | 398 | {20} | s := s + 1; 399 | 399 | {20} | s := s + 1; 400 | 400 | {20} | s := s + 1; 401 | 401 | {20} | s := s + 1; 402 | 402 | {20} | s := s + 1; 403 | 403 | {20} | s := s + 1; 404 | 404 | {20} | s := s + 1; 405 | 405 | {20} | s := s + 1; 406 | 406 | {20} | s := s + 1; 407 | 407 | {20} | s := s + 1; 408 | 408 | {20} | s := s + 1; 409 | 409 | {20} | s := s + 1; 410 | 410 | {20} | s := s + 1; 411 | 411 | {20} | s := s + 1; 412 | 412 | {20} | s := s + 1; 413 | 413 | {20} | s := s + 1; 414 | 414 | {20} | s := s + 1; 415 | 415 | {20} | s := s + 1; 416 | 416 | {20} | s := s + 1; 417 | 417 | {20} | s := s + 1; 418 | 418 | {20} | s := s + 1; 419 | 419 | {20} | s := s + 1; 420 | 420 | {20} | s := s + 1; 421 | 421 | {20} | s := s + 1; 422 | 422 | {20} | s := s + 1; 423 | 423 | {20} | s := s + 1; 424 | 424 | {20} | s := s + 1; 425 | 425 | {20} | s := s + 1; 426 | 426 | {20} | s := s + 1; 427 | 427 | {20} | s := s + 1; 428 | 428 | {20} | s := s + 1; 429 | 429 | {20} | s := s + 1; 430 | 430 | {20} | s := s + 1; 431 | 431 | {20} | s := s + 1; 432 | 432 | {20} | s := s + 1; 433 | 433 | {20} | s := s + 1; 434 | 434 | {20} | s := s + 1; 435 | 435 | {20} | s := s + 1; 436 | 436 | {20} | s := s + 1; 437 | 437 | {20} | s := s + 1; 438 | 438 | {20} | s := s + 1; 439 | 439 | {20} | s := s + 1; 440 | 440 | {20} | s := s + 1; 441 | 441 | {20} | s := s + 1; 442 | 442 | {20} | s := s + 1; 443 | 443 | {20} | s := s + 1; 444 | 444 | {20} | s := s + 1; 445 | 445 | {20} | s := s + 1; 446 | 446 | {20} | s := s + 1; 447 | 447 | {20} | s := s + 1; 448 | 448 | {20} | s := s + 1; 449 | 449 | {20} | s := s + 1; 450 | 450 | {20} | s := s + 1; 451 | 451 | {20} | s := s + 1; 452 | 452 | {20} | s := s + 1; 453 | 453 | {20} | s := s + 1; 454 | 454 | {20} | s := s + 1; 455 | 455 | {20} | s := s + 1; 456 | 456 | {20} | s := s + 1; 457 | 457 | {20} | s := s + 1; 458 | 458 | {20} | s := s + 1; 459 | 459 | {20} | s := s + 1; 460 | 460 | {20} | s := s + 1; 461 | 461 | {20} | s := s + 1; 462 | 462 | {20} | s := s + 1; 463 | 463 | {20} | s := s + 1; 464 | 464 | {20} | s := s + 1; 465 | 465 | {20} | s := s + 1; 466 | 466 | {20} | s := s + 1; 467 | 467 | {20} | s := s + 1; 468 | 468 | {20} | s := s + 1; 469 | 469 | {20} | s := s + 1; 470 | 470 | {20} | s := s + 1; 471 | 471 | {20} | s := s + 1; 472 | 472 | {20} | s := s + 1; 473 | 473 | {20} | s := s + 1; 474 | 474 | {20} | s := s + 1; 475 | 475 | {20} | s := s + 1; 476 | 476 | {20} | s := s + 1; 477 | 477 | {20} | s := s + 1; 478 | 478 | {20} | s := s + 1; 479 | 479 | {20} | s := s + 1; 480 | 480 | {20} | s := s + 1; 481 | 481 | {20} | s := s + 1; 482 | 482 | {20} | s := s + 1; 483 | 483 | {20} | s := s + 1; 484 | 484 | {20} | s := s + 1; 485 | 485 | {20} | s := s + 1; 486 | 486 | {20} | s := s + 1; 487 | 487 | {20} | s := s + 1; 488 | 488 | {20} | s := s + 1; 489 | 489 | {20} | s := s + 1; 490 | 490 | {20} | s := s + 1; 491 | 491 | {20} | s := s + 1; 492 | 492 | {20} | s := s + 1; 493 | 493 | {20} | s := s + 1; 494 | 494 | {20} | s := s + 1; 495 | 495 | {20} | s := s + 1; 496 | 496 | {20} | s := s + 1; 497 | 497 | {20} | s := s + 1; 498 | 498 | {20} | s := s + 1; 499 | 499 | {20} | s := s + 1; 500 | 500 | {20} | s := s + 1; 501 | 501 | {20} | s := s + 1; 502 | 502 | {20} | s := s + 1; 503 | 503 | {20} | s := s + 1; 504 | 504 | {20} | s := s + 1; 505 | 505 | {20} | s := s + 1; 506 | 506 | {20} | s := s + 1; 507 | 507 | {20} | s := s + 1; 508 | 508 | {20} | s := s + 1; 509 | 509 | {20} | s := s + 1; 510 | 510 | {20} | s := s + 1; 511 | 511 | {20} | s := s + 1; 512 | 512 | {20} | s := s + 1; 513 | 513 | {20} | s := s + 1; 514 | 514 | {20} | s := s + 1; 515 | 515 | {20} | s := s + 1; 516 | 516 | {20} | s := s + 1; 517 | 517 | {20} | s := s + 1; 518 | 518 | {20} | s := s + 1; 519 | 519 | {20} | s := s + 1; 520 | 520 | {20} | s := s + 1; 521 | 521 | {20} | s := s + 1; 522 | 522 | {20} | s := s + 1; 523 | 523 | {20} | s := s + 1; 524 | 524 | {20} | s := s + 1; 525 | 525 | {20} | s := s + 1; 526 | 526 | {20} | s := s + 1; 527 | 527 | {20} | s := s + 1; 528 | 528 | {20} | s := s + 1; 529 | 529 | {20} | s := s + 1; 530 | 530 | {20} | s := s + 1; 531 | 531 | {20} | s := s + 1; 532 | 532 | {20} | s := s + 1; 533 | 533 | {20} | s := s + 1; 534 | 534 | {20} | s := s + 1; 535 | 535 | {20} | s := s + 1; 536 | 536 | {20} | s := s + 1; 537 | 537 | {20} | s := s + 1; 538 | 538 | {20} | s := s + 1; 539 | 539 | {20} | s := s + 1; 540 | 540 | {20} | s := s + 1; 541 | 541 | {20} | s := s + 1; 542 | 542 | {20} | s := s + 1; 543 | 543 | {20} | s := s + 1; 544 | 544 | {20} | s := s + 1; 545 | 545 | {20} | s := s + 1; 546 | 546 | {20} | s := s + 1; 547 | 547 | {20} | s := s + 1; 548 | 548 | {20} | s := s + 1; 549 | 549 | {20} | s := s + 1; 550 | 550 | {20} | s := s + 1; 551 | 551 | {20} | s := s + 1; 552 | 552 | {20} | s := s + 1; 553 | 553 | {20} | s := s + 1; 554 | 554 | {20} | s := s + 1; 555 | 555 | {20} | s := s + 1; 556 | 556 | {20} | s := s + 1; 557 | 557 | {20} | s := s + 1; 558 | 558 | {20} | s := s + 1; 559 | 559 | {20} | s := s + 1; 560 | 560 | {20} | s := s + 1; 561 | 561 | {20} | s := s + 1; 562 | 562 | {20} | s := s + 1; 563 | 563 | {20} | s := s + 1; 564 | 564 | {20} | s := s + 1; 565 | 565 | {20} | s := s + 1; 566 | 566 | {20} | s := s + 1; 567 | 567 | {20} | s := s + 1; 568 | 568 | {20} | s := s + 1; 569 | 569 | {20} | s := s + 1; 570 | 570 | {20} | s := s + 1; 571 | 571 | {20} | s := s + 1; 572 | 572 | {20} | s := s + 1; 573 | 573 | {20} | s := s + 1; 574 | 574 | {20} | s := s + 1; 575 | 575 | {20} | s := s + 1; 576 | 576 | {20} | s := s + 1; 577 | 577 | {20} | s := s + 1; 578 | 578 | {20} | s := s + 1; 579 | 579 | {20} | s := s + 1; 580 | 580 | {20} | s := s + 1; 581 | 581 | {20} | s := s + 1; 582 | 582 | {20} | s := s + 1; 583 | 583 | {20} | s := s + 1; 584 | 584 | {20} | s := s + 1; 585 | 585 | {20} | s := s + 1; 586 | 586 | {20} | s := s + 1; 587 | 587 | {20} | s := s + 1; 588 | 588 | {20} | s := s + 1; 589 | 589 | {20} | s := s + 1; 590 | 590 | {20} | s := s + 1; 591 | 591 | {20} | s := s + 1; 592 | 592 | {20} | s := s + 1; 593 | 593 | {20} | s := s + 1; 594 | 594 | {20} | s := s + 1; 595 | 595 | {20} | s := s + 1; 596 | 596 | {20} | s := s + 1; 597 | 597 | {20} | s := s + 1; 598 | 598 | {20} | s := s + 1; 599 | 599 | {20} | s := s + 1; 600 | 600 | {20} | s := s + 1; 601 | 601 | {20} | s := s + 1; 602 | 602 | {20} | s := s + 1; 603 | 603 | {20} | s := s + 1; 604 | 604 | {20} | s := s + 1; 605 | 605 | {20} | s := s + 1; 606 | 606 | {20} | s := s + 1; 607 | 607 | {20} | s := s + 1; 608 | 608 | {20} | s := s + 1; 609 | 609 | {20} | s := s + 1; 610 | 610 | {20} | s := s + 1; 611 | 611 | {20} | s := s + 1; 612 | 612 | {20} | s := s + 1; 613 | 613 | {20} | s := s + 1; 614 | 614 | {20} | s := s + 1; 615 | 615 | {20} | s := s + 1; 616 | 616 | {20} | s := s + 1; 617 | 617 | {20} | s := s + 1; 618 | 618 | {20} | s := s + 1; 619 | 619 | {20} | s := s + 1; 620 | 620 | {20} | s := s + 1; 621 | 621 | {20} | s := s + 1; 622 | 622 | {20} | s := s + 1; 623 | 623 | {20} | s := s + 1; 624 | 624 | {20} | s := s + 1; 625 | 625 | {20} | s := s + 1; 626 | 626 | {20} | s := s + 1; 627 | 627 | {20} | s := s + 1; 628 | 628 | {20} | s := s + 1; 629 | 629 | {20} | s := s + 1; 630 | 630 | {20} | s := s + 1; 631 | 631 | {20} | s := s + 1; 632 | 632 | {20} | s := s + 1; 633 | 633 | {20} | s := s + 1; 634 | 634 | {20} | s := s + 1; 635 | 635 | {20} | s := s + 1; 636 | 636 | {20} | s := s + 1; 637 | 637 | {20} | s := s + 1; 638 | 638 | {20} | s := s + 1; 639 | 639 | {20} | s := s + 1; 640 | 640 | {20} | s := s + 1; 641 | 641 | {20} | s := s + 1; 642 | 642 | {20} | s := s + 1; 643 | 643 | {20} | s := s + 1; 644 | 644 | {20} | s := s + 1; 645 | 645 | {20} | s := s + 1; 646 | 646 | {20} | s := s + 1; 647 | 647 | {20} | s := s + 1; 648 | 648 | {20} | s := s + 1; 649 | 649 | {20} | s := s + 1; 650 | 650 | {20} | s := s + 1; 651 | 651 | {20} | s := s + 1; 652 | 652 | {20} | s := s + 1; 653 | 653 | {20} | s := s + 1; 654 | 654 | {20} | s := s + 1; 655 | 655 | {20} | s := s + 1; 656 | 656 | {20} | s := s + 1; 657 | 657 | {20} | s := s + 1; 658 | 658 | {20} | s := s + 1; 659 | 659 | {20} | s := s + 1; 660 | 660 | {20} | s := s + 1; 661 | 661 | {20} | s := s + 1; 662 | 662 | {20} | s := s + 1; 663 | 663 | {20} | s := s + 1; 664 | 664 | {20} | s := s + 1; 665 | 665 | {20} | s := s + 1; 666 | 666 | {20} | s := s + 1; 667 | 667 | {20} | s := s + 1; 668 | 668 | {20} | s := s + 1; 669 | 669 | {20} | s := s + 1; 670 | 670 | {20} | s := s + 1; 671 | 671 | {20} | s := s + 1; 672 | 672 | {20} | s := s + 1; 673 | 673 | {20} | s := s + 1; 674 | 674 | {20} | s := s + 1; 675 | 675 | {20} | s := s + 1; 676 | 676 | {20} | s := s + 1; 677 | 677 | {20} | s := s + 1; 678 | 678 | {20} | s := s + 1; 679 | 679 | {20} | s := s + 1; 680 | 680 | {20} | s := s + 1; 681 | 681 | {20} | s := s + 1; 682 | 682 | {20} | s := s + 1; 683 | 683 | {20} | s := s + 1; 684 | 684 | {20} | s := s + 1; 685 | 685 | {20} | s := s + 1; 686 | 686 | {20} | s := s + 1; 687 | 687 | {20} | s := s + 1; 688 | 688 | {20} | s := s + 1; 689 | 689 | {20} | s := s + 1; 690 | 690 | {20} | s := s + 1; 691 | 691 | {20} | s := s + 1; 692 | 692 | {20} | s := s + 1; 693 | 693 | {20} | s := s + 1; 694 | 694 | {20} | s := s + 1; 695 | 695 | {20} | s := s + 1; 696 | 696 | {20} | s := s + 1; 697 | 697 | {20} | s := s + 1; 698 | 698 | {20} | s := s + 1; 699 | 699 | {20} | s := s + 1; 700 | 700 | {20} | s := s + 1; 701 | 701 | {20} | s := s + 1; 702 | 702 | {20} | s := s + 1; 703 | 703 | {20} | s := s + 1; 704 | 704 | {20} | s := s + 1; 705 | 705 | {20} | s := s + 1; 706 | 706 | {20} | s := s + 1; 707 | 707 | {20} | s := s + 1; 708 | 708 | {20} | s := s + 1; 709 | 709 | {20} | s := s + 1; 710 | 710 | {20} | s := s + 1; 711 | 711 | {20} | s := s + 1; 712 | 712 | {20} | s := s + 1; 713 | 713 | {20} | s := s + 1; 714 | 714 | {20} | s := s + 1; 715 | 715 | {20} | s := s + 1; 716 | 716 | {20} | s := s + 1; 717 | 717 | {20} | s := s + 1; 718 | 718 | {20} | s := s + 1; 719 | 719 | {20} | s := s + 1; 720 | 720 | {20} | s := s + 1; 721 | 721 | {20} | s := s + 1; 722 | 722 | {20} | s := s + 1; 723 | 723 | {20} | s := s + 1; 724 | 724 | {20} | s := s + 1; 725 | 725 | {20} | s := s + 1; 726 | 726 | {20} | s := s + 1; 727 | 727 | {20} | s := s + 1; 728 | 728 | {20} | s := s + 1; 729 | 729 | {20} | s := s + 1; 730 | 730 | {20} | s := s + 1; 731 | 731 | {20} | s := s + 1; 732 | 732 | {20} | s := s + 1; 733 | 733 | {20} | s := s + 1; 734 | 734 | {20} | s := s + 1; 735 | 735 | {20} | s := s + 1; 736 | 736 | {20} | s := s + 1; 737 | 737 | {20} | s := s + 1; 738 | 738 | {20} | s := s + 1; 739 | 739 | {20} | s := s + 1; 740 | 740 | {20} | s := s + 1; 741 | 741 | {20} | s := s + 1; 742 | 742 | {20} | s := s + 1; 743 | 743 | {20} | s := s + 1; 744 | 744 | {20} | s := s + 1; 745 | 745 | {20} | s := s + 1; 746 | 746 | {20} | s := s + 1; 747 | 747 | {20} | s := s + 1; 748 | 748 | {20} | s := s + 1; 749 | 749 | {20} | s := s + 1; 750 | 750 | {20} | s := s + 1; 751 | 751 | {20} | s := s + 1; 752 | 752 | {20} | s := s + 1; 753 | 753 | {20} | s := s + 1; 754 | 754 | {20} | s := s + 1; 755 | 755 | {20} | s := s + 1; 756 | 756 | {20} | s := s + 1; 757 | 757 | {20} | s := s + 1; 758 | 758 | {20} | s := s + 1; 759 | 759 | {20} | s := s + 1; 760 | 760 | {20} | s := s + 1; 761 | 761 | {20} | s := s + 1; 762 | 762 | {20} | s := s + 1; 763 | 763 | {20} | s := s + 1; 764 | 764 | {20} | s := s + 1; 765 | 765 | {20} | s := s + 1; 766 | 766 | {20} | s := s + 1; 767 | 767 | {20} | s := s + 1; 768 | 768 | {20} | s := s + 1; 769 | 769 | {20} | s := s + 1; 770 | 770 | {20} | s := s + 1; 771 | 771 | {20} | s := s + 1; 772 | 772 | {20} | s := s + 1; 773 | 773 | {20} | s := s + 1; 774 | 774 | {20} | s := s + 1; 775 | 775 | {20} | s := s + 1; 776 | 776 | {20} | s := s + 1; 777 | 777 | {20} | s := s + 1; 778 | 778 | {20} | s := s + 1; 779 | 779 | {20} | s := s + 1; 780 | 780 | {20} | s := s + 1; 781 | 781 | {20} | s := s + 1; 782 | 782 | {20} | s := s + 1; 783 | 783 | {20} | s := s + 1; 784 | 784 | {20} | s := s + 1; 785 | 785 | {20} | s := s + 1; 786 | 786 | {20} | s := s + 1; 787 | 787 | {20} | s := s + 1; 788 | 788 | {20} | s := s + 1; 789 | 789 | {20} | s := s + 1; 790 | 790 | {20} | s := s + 1; 791 | 791 | {20} | s := s + 1; 792 | 792 | {20} | s := s + 1; 793 | 793 | {20} | s := s + 1; 794 | 794 | {20} | s := s + 1; 795 | 795 | {20} | s := s + 1; 796 | 796 | {20} | s := s + 1; 797 | 797 | {20} | s := s + 1; 798 | 798 | {20} | s := s + 1; 799 | 799 | {20} | s := s + 1; 800 | 800 | {20} | s := s + 1; 801 | 801 | {20} | s := s + 1; 802 | 802 | {20} | s := s + 1; 803 | 803 | {20} | s := s + 1; 804 | 804 | {20} | s := s + 1; 805 | 805 | {20} | s := s + 1; 806 | 806 | {20} | s := s + 1; 807 | 807 | {20} | s := s + 1; 808 | 808 | {20} | s := s + 1; 809 | 809 | {20} | s := s + 1; 810 | 810 | {20} | s := s + 1; 811 | 811 | {20} | s := s + 1; 812 | 812 | {20} | s := s + 1; 813 | 813 | {20} | s := s + 1; 814 | 814 | {20} | s := s + 1; 815 | 815 | {20} | s := s + 1; 816 | 816 | {20} | s := s + 1; 817 | 817 | {20} | s := s + 1; 818 | 818 | {20} | s := s + 1; 819 | 819 | {20} | s := s + 1; 820 | 820 | {20} | s := s + 1; 821 | 821 | {20} | s := s + 1; 822 | 822 | {20} | s := s + 1; 823 | 823 | {20} | s := s + 1; 824 | 824 | {20} | s := s + 1; 825 | 825 | {20} | s := s + 1; 826 | 826 | {20} | s := s + 1; 827 | 827 | {20} | s := s + 1; 828 | 828 | {20} | s := s + 1; 829 | 829 | {20} | s := s + 1; 830 | 830 | {20} | s := s + 1; 831 | 831 | {20} | s := s + 1; 832 | 832 | {20} | s := s + 1; 833 | 833 | {20} | s := s + 1; 834 | 834 | {20} | s := s + 1; 835 | 835 | {20} | s := s + 1; 836 | 836 | {20} | s := s + 1; 837 | 837 | {20} | s := s + 1; 838 | 838 | {20} | s := s + 1; 839 | 839 | {20} | s := s + 1; 840 | 840 | {20} | s := s + 1; 841 | 841 | {20} | s := s + 1; 842 | 842 | {20} | s := s + 1; 843 | 843 | {20} | s := s + 1; 844 | 844 | {20} | s := s + 1; 845 | 845 | {20} | s := s + 1; 846 | 846 | {20} | s := s + 1; 847 | 847 | {20} | s := s + 1; 848 | 848 | {20} | s := s + 1; 849 | 849 | {20} | s := s + 1; 850 | 850 | {20} | s := s + 1; 851 | 851 | {20} | s := s + 1; 852 | 852 | {20} | s := s + 1; 853 | 853 | {20} | s := s + 1; 854 | 854 | {20} | s := s + 1; 855 | 855 | {20} | s := s + 1; 856 | 856 | {20} | s := s + 1; 857 | 857 | {20} | s := s + 1; 858 | 858 | {20} | s := s + 1; 859 | 859 | {20} | s := s + 1; 860 | 860 | {20} | s := s + 1; 861 | 861 | {20} | s := s + 1; 862 | 862 | {20} | s := s + 1; 863 | 863 | {20} | s := s + 1; 864 | 864 | {20} | s := s + 1; 865 | 865 | {20} | s := s + 1; 866 | 866 | {20} | s := s + 1; 867 | 867 | {20} | s := s + 1; 868 | 868 | {20} | s := s + 1; 869 | 869 | {20} | s := s + 1; 870 | 870 | {20} | s := s + 1; 871 | 871 | {20} | s := s + 1; 872 | 872 | {20} | s := s + 1; 873 | 873 | {20} | s := s + 1; 874 | 874 | {20} | s := s + 1; 875 | 875 | {20} | s := s + 1; 876 | 876 | {20} | s := s + 1; 877 | 877 | {20} | s := s + 1; 878 | 878 | {20} | s := s + 1; 879 | 879 | {20} | s := s + 1; 880 | 880 | {20} | s := s + 1; 881 | 881 | {20} | s := s + 1; 882 | 882 | {20} | s := s + 1; 883 | 883 | {20} | s := s + 1; 884 | 884 | {20} | s := s + 1; 885 | 885 | {20} | s := s + 1; 886 | 886 | {20} | s := s + 1; 887 | 887 | {20} | s := s + 1; 888 | 888 | {20} | s := s + 1; 889 | 889 | {20} | s := s + 1; 890 | 890 | {20} | s := s + 1; 891 | 891 | {20} | s := s + 1; 892 | 892 | {20} | s := s + 1; 893 | 893 | {20} | s := s + 1; 894 | 894 | {20} | s := s + 1; 895 | 895 | {20} | s := s + 1; 896 | 896 | {20} | s := s + 1; 897 | 897 | {20} | s := s + 1; 898 | 898 | {20} | s := s + 1; 899 | 899 | {20} | s := s + 1; 900 | 900 | {20} | s := s + 1; 901 | 901 | {20} | s := s + 1; 902 | 902 | {20} | s := s + 1; 903 | 903 | {20} | s := s + 1; 904 | 904 | {20} | s := s + 1; 905 | 905 | {20} | s := s + 1; 906 | 906 | {20} | s := s + 1; 907 | 907 | {20} | s := s + 1; 908 | 908 | {20} | s := s + 1; 909 | 909 | {20} | s := s + 1; 910 | 910 | {20} | s := s + 1; 911 | 911 | {20} | s := s + 1; 912 | 912 | {20} | s := s + 1; 913 | 913 | {20} | s := s + 1; 914 | 914 | {20} | s := s + 1; 915 | 915 | {20} | s := s + 1; 916 | 916 | {20} | s := s + 1; 917 | 917 | {20} | s := s + 1; 918 | 918 | {20} | s := s + 1; 919 | 919 | {20} | s := s + 1; 920 | 920 | {20} | s := s + 1; 921 | 921 | {20} | s := s + 1; 922 | 922 | {20} | s := s + 1; 923 | 923 | {20} | s := s + 1; 924 | 924 | {20} | s := s + 1; 925 | 925 | {20} | s := s + 1; 926 | 926 | {20} | s := s + 1; 927 | 927 | {20} | s := s + 1; 928 | 928 | {20} | s := s + 1; 929 | 929 | {20} | s := s + 1; 930 | 930 | {20} | s := s + 1; 931 | 931 | {20} | s := s + 1; 932 | 932 | {20} | s := s + 1; 933 | 933 | {20} | s := s + 1; 934 | 934 | {20} | s := s + 1; 935 | 935 | {20} | s := s + 1; 936 | 936 | {20} | s := s + 1; 937 | 937 | {20} | s := s + 1; 938 | 938 | {20} | s := s + 1; 939 | 939 | {20} | s := s + 1; 940 | 940 | {20} | s := s + 1; 941 | 941 | {20} | s := s + 1; 942 | 942 | {20} | s := s + 1; 943 | 943 | {20} | s := s + 1; 944 | 944 | {20} | s := s + 1; 945 | 945 | {20} | s := s + 1; 946 | 946 | {20} | s := s + 1; 947 | 947 | {20} | s := s + 1; 948 | 948 | {20} | s := s + 1; 949 | 949 | {20} | s := s + 1; 950 | 950 | {20} | s := s + 1; 951 | 951 | {20} | s := s + 1; 952 | 952 | {20} | s := s + 1; 953 | 953 | {20} | s := s + 1; 954 | 954 | {20} | s := s + 1; 955 | 955 | {20} | s := s + 1; 956 | 956 | {20} | s := s + 1; 957 | 957 | {20} | s := s + 1; 958 | 958 | {20} | s := s + 1; 959 | 959 | {20} | s := s + 1; 960 | 960 | {20} | s := s + 1; 961 | 961 | {20} | s := s + 1; 962 | 962 | {20} | s := s + 1; 963 | 963 | {20} | s := s + 1; 964 | 964 | {20} | s := s + 1; 965 | 965 | {20} | s := s + 1; 966 | 966 | {20} | s := s + 1; 967 | 967 | {20} | s := s + 1; 968 | 968 | {20} | s := s + 1; 969 | 969 | {20} | s := s + 1; 970 | 970 | {20} | s := s + 1; 971 | 971 | {20} | s := s + 1; 972 | 972 | {20} | s := s + 1; 973 | 973 | {20} | s := s + 1; 974 | 974 | {20} | s := s + 1; 975 | 975 | {20} | s := s + 1; 976 | 976 | {20} | s := s + 1; 977 | 977 | {20} | s := s + 1; 978 | 978 | {20} | s := s + 1; 979 | 979 | {20} | s := s + 1; 980 | 980 | {20} | s := s + 1; 981 | 981 | {20} | s := s + 1; 982 | 982 | {20} | s := s + 1; 983 | 983 | {20} | s := s + 1; 984 | 984 | {20} | s := s + 1; 985 | 985 | {20} | s := s + 1; 986 | 986 | {20} | s := s + 1; 987 | 987 | {20} | s := s + 1; 988 | 988 | {20} | s := s + 1; 989 | 989 | {20} | s := s + 1; 990 | 990 | {20} | s := s + 1; 991 | 991 | {20} | s := s + 1; 992 | 992 | {20} | s := s + 1; 993 | 993 | {20} | s := s + 1; 994 | 994 | {20} | s := s + 1; 995 | 995 | {20} | s := s + 1; 996 | 996 | {20} | s := s + 1; 997 | 997 | {20} | s := s + 1; 998 | 998 | {20} | s := s + 1; 999 | 999 | {20} | s := s + 1; 1000 | 1000 | {20} | s := s + 1; 1001 | 1001 | {20} | s := s + 1; 1002 | 1002 | {20} | s := s + 1; 1003 | 1003 | {20} | s := s + 1; 1004 | 1004 | {20} | s := s + 1; 1005 | 1005 | {20} | s := s + 1; 1006 | 1006 | {20} | s := s + 1; 1007 | 1007 | {20} | s := s + 1; 1008 | 1008 | {20} | s := s + 1; 1009 | 1009 | {20} | s := s + 1; 1010 | 1010 | {20} | s := s + 1; 1011 | 1011 | {20} | s := s + 1; 1012 | 1012 | {20} | s := s + 1; 1013 | 1013 | {20} | s := s + 1; 1014 | 1014 | {20} | s := s + 1; 1015 | 1015 | {20} | s := s + 1; 1016 | 1016 | {20} | s := s + 1; 1017 | 1017 | {20} | s := s + 1; 1018 | 1018 | {20} | s := s + 1; 1019 | 1019 | {20} | s := s + 1; 1020 | 1020 | {20} | s := s + 1; 1021 | 1021 | {20} | s := s + 1; 1022 | 1022 | {20} | s := s + 1; 1023 | 1023 | {20} | s := s + 1; 1024 | 1024 | {20} | s := s + 1; 1025 | 1025 | {20} | s := s + 1; 1026 | 1026 | {20} | s := s + 1; 1027 | 1027 | {20} | s := s + 1; 1028 | 1028 | {20} | s := s + 1; 1029 | 1029 | {20} | s := s + 1; 1030 | 1030 | {20} | s := s + 1; 1031 | 1031 | {20} | s := s + 1; 1032 | 1032 | {20} | s := s + 1; 1033 | 1033 | {20} | s := s + 1; 1034 | 1034 | {20} | s := s + 1; 1035 | 1035 | {20} | s := s + 1; 1036 | 1036 | {20} | s := s + 1; 1037 | 1037 | {20} | s := s + 1; 1038 | | | end loop; 1039 | | | end loop; 1040 | 1040 | {20} | j := j + 1; 1041 | | | end loop; 1042 | | | exception when others then 1043 | 1043 | {0} | raise 'reraised exception %', sqlerrm; 1044 | | | end; 1045 | 1045 | {2} | return $1; 1046 | | | end; (1046 rows) select funcoid, exec_count from plpgsql_profiler_functions_all(); funcoid | exec_count -----------------+------------ longfx(integer) | 2 (1 row) -- coverage tests set plpgsql_check.profiler to on; create or replace function covtest(int) returns int as $$ declare a int = $1; begin a := a + 1; if a < 10 then a := a + 1; end if; a := a + 1; return a; end; $$ language plpgsql; set plpgsql_check.profiler to on; select covtest(10); covtest --------- 12 (1 row) select stmtid, exec_stmts, stmtname from plpgsql_profiler_function_statements_tb('covtest'); stmtid | exec_stmts | stmtname --------+------------+----------------- 1 | 1 | statement block 2 | 1 | assignment 3 | 1 | IF 4 | 0 | assignment 5 | 1 | assignment 6 | 1 | RETURN (6 rows) select plpgsql_coverage_statements('covtest'); plpgsql_coverage_statements ----------------------------- 0.8333333333333334 (1 row) select plpgsql_coverage_branches('covtest'); plpgsql_coverage_branches --------------------------- 0.5 (1 row) select covtest(1); covtest --------- 4 (1 row) select stmtid, exec_stmts, stmtname from plpgsql_profiler_function_statements_tb('covtest'); stmtid | exec_stmts | stmtname --------+------------+----------------- 1 | 2 | statement block 2 | 2 | assignment 3 | 2 | IF 4 | 1 | assignment 5 | 2 | assignment 6 | 2 | RETURN (6 rows) select plpgsql_coverage_statements('covtest'); plpgsql_coverage_statements ----------------------------- 1 (1 row) select plpgsql_coverage_branches('covtest'); plpgsql_coverage_branches --------------------------- 1 (1 row) set plpgsql_check.profiler to off; -- aborted function has profile too create or replace function test_function(a int) returns int as $$ begin if (a > 5) then a := a + 10; return a; else raise exception 'a < 5'; end if; end; $$ language plpgsql; set plpgsql_check.profiler to on; select test_function(1); ERROR: a < 5 CONTEXT: PL/pgSQL function test_function(integer) line 7 at RAISE select test_function(10); test_function --------------- 20 (1 row) select lineno, exec_stmts, exec_stmts_err, source from plpgsql_profiler_function_tb('test_function'); lineno | exec_stmts | exec_stmts_err | source --------+------------+----------------+------------------------------ 1 | | | 2 | {2} | {1} | begin 3 | {2} | {1} | if (a > 5) then 4 | {1} | {0} | a := a + 10; 5 | {1} | {0} | return a; 6 | | | else 7 | {1} | {1} | raise exception 'a < 5'; 8 | | | end if; 9 | | | end; (9 rows) create or replace function test_function1(a int) returns int as $$ begin if (a > 5) then a := a + 10; return a; else raise exception 'a < 5'; end if; exception when others then raise notice 'do warning'; return -1; end; $$ language plpgsql; select test_function1(1); NOTICE: do warning test_function1 ---------------- -1 (1 row) select test_function1(10); test_function1 ---------------- 20 (1 row) select lineno, exec_stmts, exec_stmts_err, source from plpgsql_profiler_function_tb('test_function1'); lineno | exec_stmts | exec_stmts_err | source --------+------------+----------------+-------------------------------- 1 | | | 2 | {2} | {0} | begin 3 | {2} | {1} | if (a > 5) then 4 | {1} | {0} | a := a + 10; 5 | {1} | {0} | return a; 6 | | | else 7 | {1} | {1} | raise exception 'a < 5'; 8 | | | end if; 9 | | | exception when others then 10 | {1} | {0} | raise notice 'do warning'; 11 | {1} | {0} | return -1; 12 | | | end; (12 rows) drop function test_function(int); drop function test_function1(int); drop table t1; create or replace function test_trg() returns trigger as $$ begin if new.a > 10 then new.a := 10; end if; return new; end; $$ language plpgsql; create table t1(a int); create trigger t1_insert before insert on t1 for each row execute function test_trg(); insert into t1 values(0); select * from plpgsql_coverage_branches('test_trg'); plpgsql_coverage_branches --------------------------- 0.5 (1 row) select * from plpgsql_coverage_statements('test_trg'); plpgsql_coverage_statements ----------------------------- 0.75 (1 row) insert into t1 values(22); select * from plpgsql_coverage_branches('test_trg'); plpgsql_coverage_branches --------------------------- 1 (1 row) select * from plpgsql_coverage_statements('test_trg'); plpgsql_coverage_statements ----------------------------- 1 (1 row) select stmtid, lineno, exec_stmts, exec_stmts_err from plpgsql_profiler_function_statements_tb('test_trg'); stmtid | lineno | exec_stmts | exec_stmts_err --------+--------+------------+---------------- 1 | 2 | 2 | 0 2 | 3 | 2 | 0 3 | 4 | 1 | 0 4 | 6 | 2 | 0 (4 rows) select stmt_lineno, exec_stmts, exec_stmts_err, source from plpgsql_profiler_function_tb('test_trg'); stmt_lineno | exec_stmts | exec_stmts_err | source -------------+------------+----------------+---------------------- | | | 2 | {2} | {0} | begin 3 | {2} | {0} | if new.a > 10 then 4 | {1} | {0} | new.a := 10; | | | end if; 6 | {2} | {0} | return new; | | | end; (7 rows) drop trigger t1_insert on t1; drop function test_trg; drop table t1; set plpgsql_check.profiler to off; okbob-plpgsql_check-800db79/expected/plpgsql_check_profiler_ctrl.out000066400000000000000000000112131526744060500260700ustar00rootroot00000000000000set client_min_messages to warning; create extension if not exists plpgsql_check; set client_min_messages to notice; -- -- Tests of the control functions of the profiler and of the checks of -- the arguments of the profiler functions. -- -- the profiler is enabled and disabled by a function, which reports the -- new state and returns it select plpgsql_check_profiler(true); NOTICE: profiler is active plpgsql_check_profiler ------------------------ t (1 row) select plpgsql_check_profiler(false); NOTICE: profiler is not active plpgsql_check_profiler ------------------------ f (1 row) -- when the argument is omitted or null, the state is only reported. The -- reported notices describe the configuration of the shared memory, -- which depends on the preloading of the library and on the size of the -- allocated segment, so they are suppressed here and only the returned -- value is checked. set client_min_messages to warning; select plpgsql_check_profiler(); plpgsql_check_profiler ------------------------ f (1 row) select plpgsql_check_profiler(true); plpgsql_check_profiler ------------------------ t (1 row) select plpgsql_check_profiler(); plpgsql_check_profiler ------------------------ t (1 row) set plpgsql_check.use_shared_stats_when_it_possible to off; select plpgsql_check_profiler(null); plpgsql_check_profiler ------------------------ t (1 row) set plpgsql_check.use_shared_stats_when_it_possible to default; select plpgsql_check_profiler(null); plpgsql_check_profiler ------------------------ t (1 row) select plpgsql_check_profiler(false); plpgsql_check_profiler ------------------------ f (1 row) set client_min_messages to notice; -- the fake queryid hook is installed and removed only once, the -- repeated calls do nothing select plpgsql_profiler_install_fake_queryid_hook(); plpgsql_profiler_install_fake_queryid_hook -------------------------------------------- (1 row) select plpgsql_profiler_install_fake_queryid_hook(); plpgsql_profiler_install_fake_queryid_hook -------------------------------------------- (1 row) select plpgsql_profiler_remove_fake_queryid_hook(); plpgsql_profiler_remove_fake_queryid_hook ------------------------------------------- (1 row) select plpgsql_profiler_remove_fake_queryid_hook(); plpgsql_profiler_remove_fake_queryid_hook ------------------------------------------- (1 row) -- the coverage functions are not strict, so they check the argument -- themselves select plpgsql_coverage_statements(null::text); ERROR: the first argument should not be null select plpgsql_coverage_branches(null::text); ERROR: the first argument should not be null select plpgsql_coverage_statements(null::regprocedure); ERROR: the first argument should not be null select plpgsql_coverage_branches(null::regprocedure); ERROR: the first argument should not be null -- the profile of a function which was never executed while the profiler -- was active is empty, and the function is not listed create function pc_f1() returns void as $$ begin raise exception 'pc_f1 failed'; end; $$ language plpgsql; do $$ begin perform pc_f1(); exception when others then raise notice 'catched: %', sqlerrm; end; $$; NOTICE: catched: pc_f1 failed select stmtid, exec_stmts, stmtname from plpgsql_profiler_function_statements_tb('pc_f1'); stmtid | exec_stmts | stmtname --------+------------+----------------- 1 | | statement block 2 | | RAISE (2 rows) -- a function whose whole body is written on a single line has all the -- statements on one row of the profile set plpgsql_check.profiler to on; create function pc_f2(a int) returns int as $$ declare b int; begin b := a; if b > 0 then b := -b; end if; return b; end $$ language plpgsql; select pc_f2(1); pc_f2 ------- -1 (1 row) select lineno, stmt_lineno, exec_stmts, source from plpgsql_profiler_function_tb('pc_f2'); lineno | stmt_lineno | exec_stmts | source --------+-------------+-------------+----------------------------------------------------------------------------- 1 | 1 | {1,1,1,1,1} | declare b int; begin b := a; if b > 0 then b := -b; end if; return b; end (1 row) select plpgsql_coverage_statements('pc_f2(int)'); plpgsql_coverage_statements ----------------------------- 1 (1 row) select plpgsql_coverage_branches('pc_f2(int)'); plpgsql_coverage_branches --------------------------- 0.5 (1 row) set plpgsql_check.profiler to off; select plpgsql_profiler_reset_all(); plpgsql_profiler_reset_all ---------------------------- (1 row) drop function pc_f2(int); drop function pc_f1(); okbob-plpgsql_check-800db79/expected/plpgsql_check_profiler_local.out000066400000000000000000000505071526744060500262270ustar00rootroot00000000000000set client_min_messages to warning; create extension if not exists plpgsql_check; set client_min_messages to notice; -- -- Tests of the profiler with the statistics stored in the local memory -- of the session. -- -- When the shared memory was preallocated, the profiler stores the -- statistics there, so the local variant of the storage is used only -- when the shared memory is not available or when it is disabled by -- the following option. The option is used here, so the same code is -- tested in both configurations. -- set plpgsql_check.use_shared_stats_when_it_possible to off; -- the profiler can be enabled by a function too select plpgsql_check_profiler(true); NOTICE: profiler is active plpgsql_check_profiler ------------------------ t (1 row) create table pl_t1(a int, b int); create function pl_f1(a int) returns int as $$ declare b int = 0; begin if a > 10 then b := a; else b := -a; end if; while b > 0 loop b := b - 1; end loop; return b; end; $$ language plpgsql; -- there are no statistics before the first execution of the function, -- but the statements are listed anyway select stmtid, parent_stmtid, exec_stmts, stmtname from plpgsql_profiler_function_statements_tb('pl_f1'); stmtid | parent_stmtid | exec_stmts | stmtname --------+---------------+------------+----------------- 1 | | | statement block 2 | 1 | | IF 3 | 2 | | assignment 4 | 2 | | assignment 5 | 1 | | WHILE 6 | 5 | | assignment 7 | 1 | | RETURN (7 rows) select lineno, stmt_lineno, exec_stmts, source from plpgsql_profiler_function_tb('pl_f1'); lineno | stmt_lineno | exec_stmts | source --------+-------------+------------+-------------------- 1 | | | 2 | | | declare b int = 0; 3 | 3 | | begin 4 | 4 | | if a > 10 then 5 | 5 | | b := a; 6 | | | else 7 | 7 | | b := -a; 8 | | | end if; 9 | 9 | | while b > 0 loop 10 | 10 | | b := b - 1; 11 | | | end loop; 12 | 12 | | return b; 13 | | | end; (13 rows) select plpgsql_coverage_statements('pl_f1'); plpgsql_coverage_statements ----------------------------- 0 (1 row) select plpgsql_coverage_branches('pl_f1'); plpgsql_coverage_branches --------------------------- 0 (1 row) -- only a part of the function is executed, so the coverage is partial select pl_f1(20); pl_f1 ------- 0 (1 row) select stmtid, parent_stmtid, exec_stmts, stmtname from plpgsql_profiler_function_statements_tb('pl_f1'); stmtid | parent_stmtid | exec_stmts | stmtname --------+---------------+------------+----------------- 1 | | 1 | statement block 2 | 1 | 1 | IF 3 | 2 | 1 | assignment 4 | 2 | 0 | assignment 5 | 1 | 1 | WHILE 6 | 5 | 20 | assignment 7 | 1 | 1 | RETURN (7 rows) select lineno, stmt_lineno, exec_stmts, source from plpgsql_profiler_function_tb('pl_f1'); lineno | stmt_lineno | exec_stmts | source --------+-------------+------------+-------------------- 1 | | | 2 | | | declare b int = 0; 3 | 3 | {1} | begin 4 | 4 | {1} | if a > 10 then 5 | 5 | {1} | b := a; 6 | | | else 7 | 7 | {0} | b := -a; 8 | | | end if; 9 | 9 | {1} | while b > 0 loop 10 | 10 | {20} | b := b - 1; 11 | | | end loop; 12 | 12 | {1} | return b; 13 | | | end; (13 rows) select plpgsql_coverage_statements('pl_f1'); plpgsql_coverage_statements ----------------------------- 0.8571428571428571 (1 row) select plpgsql_coverage_branches('pl_f1'); plpgsql_coverage_branches --------------------------- 0.6666666666666666 (1 row) -- the coverage functions accept the oid of the function too select plpgsql_coverage_statements('pl_f1(int)'::regprocedure); plpgsql_coverage_statements ----------------------------- 0.8571428571428571 (1 row) select plpgsql_coverage_branches('pl_f1(int)'::regprocedure); plpgsql_coverage_branches --------------------------- 0.6666666666666666 (1 row) -- the statistics of the second execution are merged with the first ones -- and both branches of the IF statement are executed now select pl_f1(5); pl_f1 ------- -5 (1 row) select stmtid, parent_stmtid, exec_stmts, stmtname from plpgsql_profiler_function_statements_tb('pl_f1'); stmtid | parent_stmtid | exec_stmts | stmtname --------+---------------+------------+----------------- 1 | | 2 | statement block 2 | 1 | 2 | IF 3 | 2 | 1 | assignment 4 | 2 | 1 | assignment 5 | 1 | 2 | WHILE 6 | 5 | 20 | assignment 7 | 1 | 2 | RETURN (7 rows) select plpgsql_coverage_statements('pl_f1'); plpgsql_coverage_statements ----------------------------- 1 (1 row) select plpgsql_coverage_branches('pl_f1'); plpgsql_coverage_branches --------------------------- 1 (1 row) -- a function without any conditional statement has no branches, and the -- branch coverage of such function is 1 create function pl_f2() returns void as $$ begin insert into pl_t1 values(1, 2); end; $$ language plpgsql; select pl_f2(); pl_f2 ------- (1 row) select plpgsql_coverage_statements('pl_f2'); plpgsql_coverage_statements ----------------------------- 1 (1 row) select plpgsql_coverage_branches('pl_f2'); plpgsql_coverage_branches --------------------------- 1 (1 row) -- the statistics of an aborted execution are collected too; the -- anonymous block is profiled as well, but it is not listed by -- plpgsql_profiler_functions_all(), because it has no entry in pg_proc create function pl_f3() returns void as $$ begin raise exception 'pl_f3 failed'; end; $$ language plpgsql; do $$ begin perform pl_f3(); exception when others then raise notice 'catched: %', sqlerrm; end; $$; NOTICE: catched: pl_f3 failed select funcoid, exec_count, exec_stmts_err from plpgsql_profiler_functions_all() order by funcoid::text; funcoid | exec_count | exec_stmts_err ----------------+------------+---------------- pl_f1(integer) | 2 | 0 pl_f2() | 1 | 0 pl_f3() | 1 | 1 (3 rows) -- the statistics of one function can be removed select plpgsql_profiler_reset('pl_f1(int)'); plpgsql_profiler_reset ------------------------ (1 row) select stmtid, parent_stmtid, exec_stmts, stmtname from plpgsql_profiler_function_statements_tb('pl_f1'); stmtid | parent_stmtid | exec_stmts | stmtname --------+---------------+------------+----------------- 1 | | | statement block 2 | 1 | | IF 3 | 2 | | assignment 4 | 2 | | assignment 5 | 1 | | WHILE 6 | 5 | | assignment 7 | 1 | | RETURN (7 rows) select funcoid, exec_count, exec_stmts_err from plpgsql_profiler_functions_all() order by funcoid::text; funcoid | exec_count | exec_stmts_err ---------+------------+---------------- pl_f2() | 1 | 0 pl_f3() | 1 | 1 (2 rows) -- ... and the statistics of all functions too select plpgsql_profiler_reset_all(); plpgsql_profiler_reset_all ---------------------------- (1 row) select funcoid, exec_count, exec_stmts_err from plpgsql_profiler_functions_all() order by funcoid::text; funcoid | exec_count | exec_stmts_err ---------+------------+---------------- (0 rows) -- the size of the statistics is limited by a configuration option; when -- the limit is reached, the statistics of the function are not stored -- and a warning is raised set plpgsql_check.max_stats_size to '64kB'; do $$ begin execute 'create function pl_big() returns void as $x$ begin ' || repeat('perform 1;', 2000) || 'end; $x$ language plpgsql'; end; $$; select pl_big(); WARNING: cannot allocate local memory for profiler statistics DETAIL: Statistics can be cleaned by calling function "plpgsql_profiler_reset_all()". pl_big -------- (1 row) -- the function level statistics are collected even in this case select funcoid, exec_count from plpgsql_profiler_functions_all() where funcoid = 'pl_big()'::regprocedure; funcoid | exec_count ----------+------------ pl_big() | 1 (1 row) select stmtid, exec_stmts from plpgsql_profiler_function_statements_tb('pl_big') where stmtid = 1; stmtid | exec_stmts --------+------------ 1 | (1 row) drop function pl_big(); drop function pl_f3(); drop function pl_f2(); drop function pl_f1(int); drop table pl_t1; -- CASE does not have the hypothetical ELSE branch used for IF. create function pl_case(n int) returns int as $$ begin case n when 1 then return 10; when 2 then return 20; end case; end; $$ language plpgsql; create function pl_case_else(n int) returns int as $$ begin case n when 1 then return 10; when 2 then return 20; else return 0; end case; end; $$ language plpgsql; select pl_case(1), pl_case(2); pl_case | pl_case ---------+--------- 10 | 20 (1 row) select pl_case_else(1), pl_case_else(2), pl_case_else(0); pl_case_else | pl_case_else | pl_case_else --------------+--------------+-------------- 10 | 20 | 0 (1 row) select plpgsql_coverage_branches('pl_case'), plpgsql_coverage_branches('pl_case_else'); plpgsql_coverage_branches | plpgsql_coverage_branches ---------------------------+--------------------------- 1 | 1 (1 row) drop function pl_case(int); drop function pl_case_else(int); -- Row counts belong to the SQL/fetch/return statements, not to subsequent -- assignments or enclosing control statements. create table pl_row_data(a int); create function pl_rows() returns setof int as $$ declare n int; c refcursor; begin insert into pl_row_data values (1), (2), (3); n := 99; if n > 0 then perform * from pl_row_data; end if; update pl_row_data set a = a + 10 where a < 3; delete from pl_row_data where a = 3; execute 'select * from pl_row_data'; select count(*) into n from pl_row_data; open c for select a from pl_row_data order by a; fetch c into n; move forward 1 from c; fetch c into n; close c; return next n; return query select a from pl_row_data order by a; return query execute 'select 7'; return; end; $$ language plpgsql; select count(*) from pl_rows(); count ------- 4 (1 row) select stmtid, stmtname, processed_rows from plpgsql_profiler_function_statements_tb('pl_rows'); stmtid | stmtname | processed_rows --------+-----------------+---------------- 1 | statement block | 0 2 | SQL statement | 3 3 | assignment | 0 4 | IF | 0 5 | PERFORM | 3 6 | SQL statement | 2 7 | SQL statement | 1 8 | EXECUTE | 2 9 | SQL statement | 1 10 | OPEN | 0 11 | FETCH | 1 12 | MOVE | 1 13 | FETCH | 0 14 | CLOSE | 0 15 | RETURN NEXT | 1 16 | RETURN QUERY | 2 17 | RETURN QUERY | 1 18 | RETURN | 0 (18 rows) select sum(processed_rows[1]) as processed_rows from plpgsql_profiler_function_tb('pl_rows'); processed_rows ---------------- 18 (1 row) drop function pl_rows(); drop table pl_row_data; -- Error counts are a subset of execution counts, not additional executions. create function pl_timing_error() returns int as $$ begin perform pg_sleep(0.01); raise exception 'controlled timing error'; end; $$ language plpgsql; create function pl_timing_catch() returns void as $$ begin perform pl_timing_error(); exception when raise_exception then null; end; $$ language plpgsql; select pl_timing_catch(); pl_timing_catch ----------------- (1 row) select count(*) = 1 and bool_and(exec_stmts = 1 and exec_stmts_err = 1 and total_time > 0 and avg_time = total_time) as correct_average from plpgsql_profiler_function_statements_tb('pl_timing_catch') where stmtname = 'PERFORM'; correct_average ----------------- t (1 row) select count(*) = 1 and bool_and(exec_stmts[1] = 1 and exec_stmts_err[1] = 1 and total_time[1] > 0 and avg_time[1] = total_time[1]) as correct_average from plpgsql_profiler_function_tb('pl_timing_catch') where source like '%perform pl_timing_error%'; correct_average ----------------- t (1 row) drop function pl_timing_catch(); drop function pl_timing_error(); -- The types of the parameters of EXECUTE ... USING, which are cached for -- the computation of the query identifier, have to stay valid when all -- statistics are reset while the function is running. Else the types -- cached for the first EXECUTE would be overwritten by the types of the -- second one, and the first query would be analyzed with wrong types. -- The cached types are used again only while the query identifier is -- unknown, so this needs a server that does not compute query identifiers -- (the default). The next test does not depend on it. create function pl_reset_using() returns text as $$ declare r int; t text; begin perform plpgsql_profiler_reset_all(); for i in 1..2 loop execute 'select $1 + 1' into r using i; perform plpgsql_profiler_reset_all(); execute 'select $1 || $2' into t using 'a'::text, 'b'::text; end loop; return t || r; end; $$ language plpgsql; select pl_reset_using(); pl_reset_using ---------------- ab3 (1 row) drop function pl_reset_using(); -- The statistics can be reset even while the profiler computes the query -- identifier, because it evaluates the expression of the query string, -- after the types of the parameters were cached. create function pl_reset_in_expr() returns text as $$ begin perform plpgsql_profiler_reset_all(); return '1'; end; $$ language plpgsql stable; create function pl_reset_using_expr() returns int as $$ declare r int; begin execute 'select $1 + ' || pl_reset_in_expr() into r using 41; return r; end; $$ language plpgsql; select pl_reset_using_expr(); pl_reset_using_expr --------------------- 42 (1 row) drop function pl_reset_using_expr(); drop function pl_reset_in_expr(); select plpgsql_check_profiler(false); NOTICE: profiler is not active plpgsql_check_profiler ------------------------ f (1 row) select plpgsql_profiler_reset_all(); plpgsql_profiler_reset_all ---------------------------- (1 row) set plpgsql_check.max_stats_size to default; -- Condition errors enter neither branch; errors in a body do enter it. set plpgsql_check.profiler = on; create function pl_condition(n int) returns int as $$ begin if 1 / n > 0 then return 1; end if; return 0; end; $$ language plpgsql; create function pl_body_error(n int) returns int as $$ begin if n > 0 then perform 1 / (n - 1); return 1; end if; return 0; end; $$ language plpgsql; create function pl_elsif_error(n int) returns int as $$ begin if n = 2 then return 2; elsif 1 / n > 0 then return 1; end if; return 0; end; $$ language plpgsql; create function pl_caught_condition(n int) returns int as $$ begin if 1 / n > 0 then return 1; end if; return 0; exception when division_by_zero then return -1; end; $$ language plpgsql; create function pl_coverage_calls() returns void as $$ begin begin perform pl_condition(0); exception when division_by_zero then null; end; begin perform pl_body_error(1); exception when division_by_zero then null; end; perform pl_body_error(0); begin perform pl_elsif_error(0); exception when division_by_zero then null; end; perform pl_caught_condition(0); end; $$ language plpgsql; select pl_coverage_calls(); pl_coverage_calls ------------------- (1 row) select plpgsql_coverage_branches('pl_condition') = 0 as condition_ok, plpgsql_coverage_branches('pl_body_error') = 1 as body_ok, plpgsql_coverage_branches('pl_elsif_error') = 0 as elsif_ok, plpgsql_coverage_branches('pl_caught_condition') = 0.5 as caught_ok; condition_ok | body_ok | elsif_ok | caught_ok --------------+---------+----------+----------- t | t | t | t (1 row) select pl_condition(1), pl_condition(-1), pl_elsif_error(2), pl_elsif_error(1), pl_elsif_error(-1); pl_condition | pl_condition | pl_elsif_error | pl_elsif_error | pl_elsif_error --------------+--------------+----------------+----------------+---------------- 1 | 0 | 2 | 1 | 0 (1 row) select plpgsql_coverage_branches('pl_condition') = 1 as complete_if, plpgsql_coverage_branches('pl_elsif_error') = 1 as complete_elsif; complete_if | complete_elsif -------------+---------------- t | t (1 row) select plpgsql_profiler_reset_all(); plpgsql_profiler_reset_all ---------------------------- (1 row) -- Exercise both shared merge paths when the library is preloaded. set plpgsql_check.use_shared_stats_when_it_possible = on; set plpgsql_check.use_lxcache = off; select pl_coverage_calls(); pl_coverage_calls ------------------- (1 row) select plpgsql_coverage_branches('pl_condition') = 0 as condition_ok, plpgsql_coverage_branches('pl_body_error') = 1 as body_ok, plpgsql_coverage_branches('pl_elsif_error') = 0 as elsif_ok, plpgsql_coverage_branches('pl_caught_condition') = 0.5 as caught_ok; condition_ok | body_ok | elsif_ok | caught_ok --------------+---------+----------+----------- t | t | t | t (1 row) select plpgsql_profiler_reset_all(); plpgsql_profiler_reset_all ---------------------------- (1 row) set plpgsql_check.use_lxcache = on; select pl_coverage_calls(); pl_coverage_calls ------------------- (1 row) select plpgsql_coverage_branches('pl_condition') = 0 as condition_ok, plpgsql_coverage_branches('pl_body_error') = 1 as body_ok, plpgsql_coverage_branches('pl_elsif_error') = 0 as elsif_ok, plpgsql_coverage_branches('pl_caught_condition') = 0.5 as caught_ok; condition_ok | body_ok | elsif_ok | caught_ok --------------+---------+----------+----------- t | t | t | t (1 row) select pl_condition(1), pl_condition(-1), pl_elsif_error(2), pl_elsif_error(1), pl_elsif_error(-1); pl_condition | pl_condition | pl_elsif_error | pl_elsif_error | pl_elsif_error --------------+--------------+----------------+----------------+---------------- 1 | 0 | 2 | 1 | 0 (1 row) select plpgsql_coverage_branches('pl_condition') = 1 as complete_if, plpgsql_coverage_branches('pl_elsif_error') = 1 as complete_elsif; complete_if | complete_elsif -------------+---------------- t | t (1 row) set plpgsql_check.profiler = off; select plpgsql_profiler_reset_all(); plpgsql_profiler_reset_all ---------------------------- (1 row) drop function pl_condition(int); drop function pl_body_error(int); drop function pl_elsif_error(int); drop function pl_caught_condition(int); drop function pl_coverage_calls(); set plpgsql_check.use_lxcache = default; set plpgsql_check.use_shared_stats_when_it_possible to default; okbob-plpgsql_check-800db79/expected/plpgsql_check_profiler_queryid.out000066400000000000000000000231761526744060500266210ustar00rootroot00000000000000set client_min_messages to warning; create extension if not exists plpgsql_check; set client_min_messages to notice; -- -- Tests of the collection of the query identifiers by the profiler. -- -- The profiler remembers the identifier of the query executed by a -- statement. A fake hook assigns the command type as its identifier so -- the expected output does not depend on PostgreSQL's query jumbling. -- set plpgsql_check.use_shared_stats_when_it_possible to off; set plpgsql_check.profiler to on; select plpgsql_profiler_install_fake_queryid_hook(); plpgsql_profiler_install_fake_queryid_hook -------------------------------------------- (1 row) create table pq_t1(a int, b int); -- the queryid of a static query is taken from the cached plan of the -- expression. Dynamic SQL has no retained plan, so its identifier is -- unavailable rather than reconstructed by evaluating application code. create function pq_f1() returns void as $$ declare n int = 1; c int; begin insert into pq_t1 values(1, 2); update pq_t1 set a = 10; select count(*) into c from pq_t1; delete from pq_t1; execute 'insert into pq_t1 values($1, $2)' using n, n + 1; execute 'update pq_t1 set a = $1' using n; execute 'delete from pq_t1'; -- an empty dynamic query and a dynamic query with more than one -- statement have no identifier execute ''; execute 'select 1; select 2'; end; $$ language plpgsql; select pq_f1(); pq_f1 ------- (1 row) select queryids, lineno, stmt_lineno, exec_stmts, source from plpgsql_profiler_function_tb('pq_f1'); queryids | lineno | stmt_lineno | exec_stmts | source ----------+--------+-------------+------------+-------------------------------------------------------------------- | 1 | | | | 2 | | | declare | 3 | | | n int = 1; | 4 | | | c int; | 5 | 5 | {1} | begin {3} | 6 | 6 | {1} | insert into pq_t1 values(1, 2); {2} | 7 | 7 | {1} | update pq_t1 set a = 10; {1} | 8 | 8 | {1} | select count(*) into c from pq_t1; {4} | 9 | 9 | {1} | delete from pq_t1; {NULL} | 10 | 10 | {1} | execute 'insert into pq_t1 values($1, $2)' using n, n + 1; {NULL} | 11 | 11 | {1} | execute 'update pq_t1 set a = $1' using n; {NULL} | 12 | 12 | {1} | execute 'delete from pq_t1'; | 13 | | | -- an empty dynamic query and a dynamic query with more than one | 14 | | | -- statement have no identifier {NULL} | 15 | 15 | {1} | execute ''; {NULL} | 16 | 16 | {1} | execute 'select 1; select 2'; | 17 | | | end; (17 rows) select plpgsql_profiler_reset_all(); plpgsql_profiler_reset_all ---------------------------- (1 row) set plpgsql_check.profiler_show_dynquery_query_id to on; select pq_f1(); pq_f1 ------- (1 row) select queryids, lineno, stmt_lineno, exec_stmts, source from plpgsql_profiler_function_tb('pq_f1'); queryids | lineno | stmt_lineno | exec_stmts | source ----------+--------+-------------+------------+-------------------------------------------------------------------- | 1 | | | | 2 | | | declare | 3 | | | n int = 1; | 4 | | | c int; | 5 | 5 | {1} | begin {3} | 6 | 6 | {1} | insert into pq_t1 values(1, 2); {2} | 7 | 7 | {1} | update pq_t1 set a = 10; {1} | 8 | 8 | {1} | select count(*) into c from pq_t1; {4} | 9 | 9 | {1} | delete from pq_t1; {3} | 10 | 10 | {1} | execute 'insert into pq_t1 values($1, $2)' using n, n + 1; {2} | 11 | 11 | {1} | execute 'update pq_t1 set a = $1' using n; {4} | 12 | 12 | {1} | execute 'delete from pq_t1'; | 13 | | | -- an empty dynamic query and a dynamic query with more than one | 14 | | | -- statement have no identifier {NULL} | 15 | 15 | {1} | execute ''; {NULL} | 16 | 16 | {1} | execute 'select 1; select 2'; | 17 | | | end; (17 rows) select plpgsql_profiler_reset_all(); plpgsql_profiler_reset_all ---------------------------- (1 row) set plpgsql_check.profiler_show_dynquery_query_id to off; -- Profiling must not re-evaluate SQL text or USING expressions. create function pq_f2() returns void as $$ declare r pq_t1; begin r := (1, 2); execute 'select $1, $2' using r.*; end; $$ language plpgsql; do $$ begin perform pq_f2(); exception when others then raise notice 'catched: %', sqlerrm; end; $$; NOTICE: catched: query returned 2 columns select queryids, lineno, stmt_lineno, exec_stmts, source from plpgsql_profiler_function_tb('pq_f2'); queryids | lineno | stmt_lineno | exec_stmts | source ----------+--------+-------------+------------+-------------------------------------- | 1 | | | | 2 | | | declare r pq_t1; | 3 | 3 | {1} | begin {1} | 4 | 4 | {1} | r := (1, 2); | 5 | 5 | {1} | execute 'select $1, $2' using r.*; | 6 | | | end; (6 rows) create function pq_into() returns text as $$ declare sql_text text := 'SELECT 42'; begin execute sql_text into sql_text; return sql_text; end; $$ language plpgsql; create function pq_loop() returns int as $$ declare sql_text text := 'SELECT 42'; n int; begin for n in execute sql_text loop sql_text := 'not sql'; end loop; return n; end; $$ language plpgsql; create function pq_open() returns int as $$ declare c refcursor; n int; begin open c for execute coalesce(c::text, 'SELECT 42'); fetch c into n; close c; return n; end; $$ language plpgsql; create function pq_return() returns setof int as $$ begin return query execute case when found then 'not sql' else 'SELECT 42' end; end; $$ language plpgsql; select pq_into(), pq_loop(), pq_open(); pq_into | pq_loop | pq_open ---------+---------+--------- 42 | 42 | 42 (1 row) select * from pq_return(); pq_return ----------- 42 (1 row) select stmtname, queryid, exec_stmts from plpgsql_profiler_function_statements_tb('pq_into') where stmtname = 'EXECUTE'; stmtname | queryid | exec_stmts ----------+---------+------------ EXECUTE | | 1 (1 row) drop function pq_into(); drop function pq_loop(); drop function pq_open(); drop function pq_return(); select plpgsql_profiler_remove_fake_queryid_hook(); plpgsql_profiler_remove_fake_queryid_hook ------------------------------------------- (1 row) -- without a provider of the identifiers no statement has one create function pq_f3() returns void as $$ begin insert into pq_t1 values(1, 2); execute 'delete from pq_t1'; end; $$ language plpgsql; select pq_f3(); pq_f3 ------- (1 row) select queryids, lineno, stmt_lineno, exec_stmts, source from plpgsql_profiler_function_tb('pq_f3'); queryids | lineno | stmt_lineno | exec_stmts | source ----------+--------+-------------+------------+----------------------------------- | 1 | | | | 2 | 2 | {1} | begin {NULL} | 3 | 3 | {1} | insert into pq_t1 values(1, 2); {NULL} | 4 | 4 | {1} | execute 'delete from pq_t1'; | 5 | | | end; (5 rows) select plpgsql_profiler_install_fake_queryid_hook(); plpgsql_profiler_install_fake_queryid_hook -------------------------------------------- (1 row) -- Skipping a statement must retain its last observed query identifier. create function pq_branch(take_branch boolean) returns void as $$ begin if take_branch then perform 42; end if; end; $$ language plpgsql; select pq_branch(true); pq_branch ----------- (1 row) select queryid, exec_stmts from plpgsql_profiler_function_statements_tb('pq_branch') where stmtname = 'PERFORM'; queryid | exec_stmts ---------+------------ 1 | 1 (1 row) select pq_branch(false); pq_branch ----------- (1 row) select queryid, exec_stmts from plpgsql_profiler_function_statements_tb('pq_branch') where stmtname = 'PERFORM'; queryid | exec_stmts ---------+------------ 1 | 1 (1 row) select queryids, exec_stmts from plpgsql_profiler_function_tb('pq_branch') where source like '%perform 42%'; queryids | exec_stmts ----------+------------ {1} | {1} (1 row) select plpgsql_profiler_reset_all(); plpgsql_profiler_reset_all ---------------------------- (1 row) set plpgsql_check.profiler to off; select plpgsql_profiler_remove_fake_queryid_hook(); plpgsql_profiler_remove_fake_queryid_hook ------------------------------------------- (1 row) drop function pq_f3(); drop function pq_f2(); drop function pq_f1(); drop table pq_t1; drop function pq_branch(boolean); set plpgsql_check.use_shared_stats_when_it_possible to default; okbob-plpgsql_check-800db79/expected/plpgsql_check_report.out000066400000000000000000000222221526744060500245370ustar00rootroot00000000000000set client_min_messages to warning; create extension if not exists plpgsql_check; set client_min_messages to notice; -- -- reports about unused, never read and unmodified variables and parameters -- -- a declared variable that is not used at all, a variable that is only -- written and a variable that is only read create function rp_f1() returns int as $$ declare unused_var int; write_only int; read_only int := 10; begin write_only := 1; return read_only; end; $$ language plpgsql; -- the "never read" report belongs to the extra warnings, the "unused" -- report is printed always select * from plpgsql_check_function('rp_f1', extra_warnings => false); plpgsql_check_function ------------------------------------------------------ warning:00000:3:DECLARE:unused variable "unused_var" (1 row) select * from plpgsql_check_function('rp_f1', extra_warnings => true); plpgsql_check_function ---------------------------------------------------------------- warning:00000:3:DECLARE:unused variable "unused_var" warning extra:00000:4:DECLARE:never read variable "write_only" (2 rows) -- the same for the parameters of a function - "a" is not used at all, -- "b" is only written and "c" is read create function rp_f2(a int, b int, c int) returns int as $$ begin b := 1; return c; end; $$ language plpgsql; select * from plpgsql_check_function('rp_f2', extra_warnings => true); plpgsql_check_function ------------------------------------------------- warning extra:00000:unused parameter "a" warning extra:00000:parameter "b" is never read (2 rows) -- INOUT parameter of a procedure that is only written is expected, because -- a procedure cannot have a plain OUT parameter in older releases create procedure rp_p1(inout a int, inout b int) as $$ begin a := 1; b := b + 1; end; $$ language plpgsql; select * from plpgsql_check_function('rp_p1', extra_warnings => true); plpgsql_check_function ------------------------ (0 rows) -- single OUT variable that is not modified create function rp_f4(out a int) as $$ begin raise notice 'nothing'; end; $$ language plpgsql; select * from plpgsql_check_function('rp_f4', extra_warnings => true); plpgsql_check_function ------------------------------------------------- warning extra:00000:unmodified OUT variable "a" (1 row) -- more OUT variables, one of them is composite, so the result of the -- function cannot be assigned by a simple expression create type rp_t1 as (x int, y int); create function rp_f5(out a int, out b rp_t1, out c rp_t1) as $$ begin b := (1,2)::rp_t1; end; $$ language plpgsql; select * from plpgsql_check_function('rp_f5', extra_warnings => true); plpgsql_check_function ----------------------------------------------------------------------- warning extra:00000:unmodified OUT variable "a" warning extra:00000:composite OUT variable "b" is not single argument warning extra:00000:composite OUT variable "c" is not single argument warning extra:00000:unmodified OUT variable "c" (4 rows) -- when the result is returned by a dynamic query whose text is not known, -- the OUT variables are reported as "maybe unmodified" only create function rp_f6(p text, out a int, out b int) returns setof record as $$ begin return query execute p; end; $$ language plpgsql; select * from plpgsql_check_function('rp_f6', extra_warnings => true); plpgsql_check_function ---------------------------------------------------------- warning extra:00000:OUT variable "a" is maybe unmodified Detail: cannot to determine result of dynamic SQL warning extra:00000:OUT variable "b" is maybe unmodified Detail: cannot to determine result of dynamic SQL (4 rows) -- the same with a single OUT variable create function rp_f6b(p text, out a int) returns setof int as $$ begin return query execute p; end; $$ language plpgsql; select * from plpgsql_check_function('rp_f6b', extra_warnings => true); plpgsql_check_function ---------------------------------------------------------- warning extra:00000:OUT variable "a" is maybe unmodified Detail: cannot to determine result of dynamic SQL (2 rows) -- a dynamic query with a known text is checked like a static query create function rp_f6c(out a int, out b int) returns setof record as $$ begin return query execute 'select 1, 2'; end; $$ language plpgsql; select * from plpgsql_check_function('rp_f6c', extra_warnings => true); plpgsql_check_function ------------------------ (0 rows) -- RETURN QUERY (without EXECUTE) sets the OUT variables, so nothing is -- reported create function rp_f7(out a int, out b int) returns setof record as $$ begin return query select 1, 2; end; $$ language plpgsql; select * from plpgsql_check_function('rp_f7', extra_warnings => true); plpgsql_check_function ------------------------ (0 rows) -- a variable used only as a field of a record or of a row is used create function rp_f8() returns int as $$ declare r record; x int; y int; begin select 1 as a, 2 as b into r; select 1, 2 into x, y; return r.a + x + y; end; $$ language plpgsql; select * from plpgsql_check_function('rp_f8', extra_warnings => true); plpgsql_check_function ------------------------ (0 rows) -- -- reports about too high volatility -- -- the body is immutable, but the function is declared as volatile create function rp_f9(a int) returns int as $$ begin return a + 1; end; $$ language plpgsql volatile; select * from plpgsql_check_function('rp_f9', performance_warnings => true); plpgsql_check_function ----------------------------------------------------------------------------------------- performance:00000:routine is marked as VOLATILE, should be IMMUTABLE Hint: When you fix this issue, please, recheck other functions that uses this function. (2 rows) -- the same function declared as stable create function rp_f10(a int) returns int as $$ begin return a + 1; end; $$ language plpgsql stable; select * from plpgsql_check_function('rp_f10', performance_warnings => true); plpgsql_check_function ----------------------------------------------------------------------------------------- performance:00000:routine is marked as STABLE, should be IMMUTABLE Hint: When you fix this issue, please, recheck other functions that uses this function. (2 rows) -- reading a table makes the function stable create table rp_tab1(a int); create function rp_f11() returns int as $$ begin return (select count(*) from rp_tab1); end; $$ language plpgsql volatile; select * from plpgsql_check_function('rp_f11', performance_warnings => true); plpgsql_check_function ----------------------------------------------------------------------------------------- performance:42804:3:RETURN:target type is different type than source type Detail: cast "bigint" value to "integer" type Hint: Hidden casting can be a performance issue. Context: at RETURN performance:00000:routine is marked as VOLATILE, should be STABLE Hint: When you fix this issue, please, recheck other functions that uses this function. (6 rows) -- a procedure returns void, so the stable volatility is not suggested create procedure rp_p2() as $$ begin perform count(*) from rp_tab1; end; $$ language plpgsql; select * from plpgsql_check_function('rp_p2', performance_warnings => true); plpgsql_check_function ------------------------ (0 rows) -- when the function uses a dynamic SQL, the volatility cannot be safely -- determined, so the report has an additional detail create function rp_f12(a int) returns int as $$ declare r int; begin execute 'select 1' into r; return a + r; end; $$ language plpgsql volatile; select * from plpgsql_check_function('rp_f12', performance_warnings => true); plpgsql_check_function ----------------------------------------------------------------------------------------- performance:00000:4:EXECUTE:immutable expression without parameters found Detail: the EXECUTE command is not necessary probably Hint: Don't use dynamic SQL when you can use static SQL. performance:00000:routine is marked as VOLATILE, should be IMMUTABLE Hint: When you fix this issue, please, recheck other functions that uses this function. (5 rows) -- an immutable function is never reported create function rp_f13(a int) returns int as $$ begin return a + 1; end; $$ language plpgsql immutable; select * from plpgsql_check_function('rp_f13', performance_warnings => true); plpgsql_check_function ------------------------ (0 rows) drop function rp_f1(); drop function rp_f2(int, int, int); drop procedure rp_p1(int, int); drop function rp_f4(); drop function rp_f5(); drop function rp_f6(text); drop function rp_f6b(text); drop function rp_f6c(); drop function rp_f7(); drop function rp_f8(); drop function rp_f9(int); drop function rp_f10(int); drop function rp_f11(); drop procedure rp_p2(); drop function rp_f12(int); drop function rp_f13(int); drop table rp_tab1; drop type rp_t1; okbob-plpgsql_check-800db79/expected/plpgsql_check_stmt_walker.out000066400000000000000000000406641526744060500255720ustar00rootroot00000000000000-- -- regress tests of generic PL/pgSQL statement walker (src/generic_stmt_walker.c) -- -- The walker itself has no SQL interface. It is used by the profiler -- (statement and branch coverage, per statement statistics, queryid -- retrieval), so the profiler API is used here as a probe. -- load 'plpgsql_check'; set client_min_messages to warning; create extension if not exists plpgsql_check; set client_min_messages to notice; create table swt_tab(a int, b int); insert into swt_tab values(1,1),(2,2),(3,3); create procedure swt_proc(a int) as $$ begin end; $$ language plpgsql; -- -- A function using every statement type known to the walker. The -- statements are executed, so the profiler collects statistics for all -- of them. -- create function swt_all_stmts(par int) returns int as $$ declare i int; j int; r record; arr int[] = array[1, 2, 3]; c refcursor; cc cursor(p int) for select a from swt_tab where a = p; res int = 0; begin -- IF with two ELSIF branches and an ELSE branch if par = 1 then res := res + 1; elsif par = 2 then res := res + 2; elsif par = 3 then res := res + 3; else res := res + 4; end if; -- IF without ELSE branch (hypothetical else branch of coverage) if par > 100 then res := res + 1; end if; -- simple CASE - the tested expression is the statement's expression case par when 1 then res := res + 1; when 2 then res := res + 2; else res := res + 3; end case; -- searched CASE - has no tested expression case when par > 0 then res := res + 1; else res := res + 2; end case; -- unconditional LOOP terminated by conditional EXIT i := 0; loop i := i + 1; exit when i >= 2; end loop; -- WHILE loop while i < 4 loop i := i + 1; end loop; -- FOR over integer range, with an explicit BY step for j in reverse 10 .. 1 by 3 loop res := res + 1; end loop; -- FOR over a static query for r in select a, b from swt_tab order by a loop res := res + r.a; end loop; -- FOR over a bound cursor (the argument list is the statement's query) for r in cc(1) loop res := res + r.a; end loop; -- FOR over a dynamic query with USING parameters for r in execute 'select a from swt_tab where a > $1' using 0 loop res := res + r.a; end loop; -- FOREACH over an array foreach i in array arr loop res := res + i; end loop; -- OPEN of a bound cursor, FETCH, CLOSE open cc(2); fetch cc into r; close cc; -- OPEN of an unbound cursor for a static query open c for select a from swt_tab; fetch c into i; close c; -- OPEN of an unbound cursor for a dynamic query with USING parameters open c for execute 'select a from swt_tab where a > $1' using 0; fetch c into i; close c; -- PERFORM perform count(*) from swt_tab; -- CALL call swt_proc(1); -- plain SQL statements insert into swt_tab values(10, 10); update swt_tab set b = 11 where a = 10; delete from swt_tab where a = 10; -- dynamic SQL with USING parameters execute 'delete from swt_tab where a = $1' using -1; -- GET DIAGNOSTICS has no interesting substructure get diagnostics i = row_count; -- ASSERT with a message expression assert res is not null, 'res is null for ' || par; -- RAISE with format parameters and with options, handled by a nested -- block with an exception handler begin raise exception 'raised for %', par using errcode = 'division_by_zero', hint = 'no hint ' || par; exception when division_by_zero then res := res + 1; end; return res; end; $$ language plpgsql; -- -- RETURN NEXT and both flavours of RETURN QUERY live in a set returning -- function. -- create function swt_setof() returns setof int as $$ declare i int; begin for i in 1 .. 2 loop return next i; end loop; return query select a from swt_tab order by a limit 1; return query execute 'select b from swt_tab where b = $1' using 2; return; end; $$ language plpgsql; -- -- COMMIT and ROLLBACK are allowed in procedures only. -- create procedure swt_txn() as $$ begin commit; rollback; end; $$ language plpgsql; set plpgsql_check.profiler to on; select swt_all_stmts(1); swt_all_stmts --------------- 27 (1 row) select swt_all_stmts(2); swt_all_stmts --------------- 29 (1 row) select swt_all_stmts(10); swt_all_stmts --------------- 32 (1 row) select * from swt_setof(); swt_setof ----------- 1 2 1 2 (4 rows) call swt_txn(); set plpgsql_check.profiler to off; -- the walker is used to assign the statement statistics to the AST select stmtid, parent_stmtid, block_num, lineno, exec_stmts, stmtname from plpgsql_profiler_function_statements_tb('swt_all_stmts'); stmtid | parent_stmtid | block_num | lineno | exec_stmts | stmtname --------+---------------+-----------+--------+------------+-------------------------------- 1 | | 1 | 10 | 3 | statement block 2 | 1 | 1 | 12 | 3 | IF 3 | 2 | 1 | 13 | 1 | assignment 4 | 2 | 2 | 15 | 1 | assignment 5 | 2 | 3 | 17 | 0 | assignment 6 | 2 | 4 | 19 | 1 | assignment 7 | 1 | 2 | 23 | 3 | IF 8 | 7 | 1 | 24 | 0 | assignment 9 | 1 | 3 | 28 | 3 | CASE 10 | 9 | 1 | 29 | 1 | assignment 11 | 9 | 2 | 30 | 1 | assignment 12 | 9 | 3 | 31 | 1 | assignment 13 | 1 | 4 | 35 | 3 | CASE 14 | 13 | 1 | 36 | 3 | assignment 15 | 13 | 2 | 37 | 0 | assignment 16 | 1 | 5 | 41 | 3 | assignment 17 | 1 | 6 | 42 | 3 | LOOP 18 | 17 | 1 | 43 | 6 | assignment 19 | 17 | 2 | 44 | 6 | EXIT 20 | 1 | 7 | 48 | 3 | WHILE 21 | 20 | 1 | 49 | 6 | assignment 22 | 1 | 8 | 53 | 3 | FOR with integer loop variable 23 | 22 | 1 | 54 | 12 | assignment 24 | 1 | 9 | 58 | 3 | FOR over SELECT rows 25 | 24 | 1 | 59 | 9 | assignment 26 | 1 | 10 | 63 | 3 | FOR over cursor 27 | 26 | 1 | 64 | 3 | assignment 28 | 1 | 11 | 68 | 3 | FOR over EXECUTE statement 29 | 28 | 1 | 69 | 9 | assignment 30 | 1 | 12 | 73 | 3 | FOREACH over array 31 | 30 | 1 | 74 | 9 | assignment 32 | 1 | 13 | 78 | 3 | OPEN 33 | 1 | 14 | 79 | 3 | FETCH 34 | 1 | 15 | 80 | 3 | CLOSE 35 | 1 | 16 | 83 | 3 | OPEN 36 | 1 | 17 | 84 | 3 | FETCH 37 | 1 | 18 | 85 | 3 | CLOSE 38 | 1 | 19 | 88 | 3 | OPEN 39 | 1 | 20 | 89 | 3 | FETCH 40 | 1 | 21 | 90 | 3 | CLOSE 41 | 1 | 22 | 93 | 3 | PERFORM 42 | 1 | 23 | 96 | 3 | CALL 43 | 1 | 24 | 99 | 3 | SQL statement 44 | 1 | 25 | 100 | 3 | SQL statement 45 | 1 | 26 | 101 | 3 | SQL statement 46 | 1 | 27 | 104 | 3 | EXECUTE 47 | 1 | 28 | 107 | 3 | GET DIAGNOSTICS 48 | 1 | 29 | 110 | 3 | ASSERT 49 | 1 | 30 | 114 | 3 | statement block 50 | 49 | 1 | 115 | 3 | RAISE 51 | 49 | 2 | 118 | 3 | assignment 52 | 1 | 31 | 121 | 3 | RETURN (52 rows) select stmtid, parent_stmtid, block_num, lineno, exec_stmts, stmtname from plpgsql_profiler_function_statements_tb('swt_setof'); stmtid | parent_stmtid | block_num | lineno | exec_stmts | stmtname --------+---------------+-----------+--------+------------+-------------------------------- 1 | | 1 | 4 | 1 | statement block 2 | 1 | 1 | 5 | 1 | FOR with integer loop variable 3 | 2 | 1 | 6 | 2 | RETURN NEXT 4 | 1 | 2 | 9 | 1 | RETURN QUERY 5 | 1 | 3 | 10 | 1 | RETURN QUERY 6 | 1 | 4 | 12 | 1 | RETURN (6 rows) select stmtid, parent_stmtid, block_num, lineno, exec_stmts, stmtname from plpgsql_profiler_function_statements_tb('swt_txn'); stmtid | parent_stmtid | block_num | lineno | exec_stmts | stmtname --------+---------------+-----------+--------+------------+----------------- 1 | | 1 | 2 | 1 | statement block 2 | 1 | 1 | 3 | 1 | COMMIT 3 | 1 | 2 | 4 | 1 | ROLLBACK (3 rows) -- every loop kind has to be recognized as a branch, and the walker has -- to be able to return the body of every loop kind select plpgsql_coverage_statements('swt_all_stmts'); plpgsql_coverage_statements ----------------------------- 0.9423076923076923 (1 row) select plpgsql_coverage_branches('swt_all_stmts'); plpgsql_coverage_branches --------------------------- 0.85 (1 row) select plpgsql_coverage_statements('swt_setof'); plpgsql_coverage_statements ----------------------------- 1 (1 row) select plpgsql_coverage_branches('swt_setof'); plpgsql_coverage_branches --------------------------- 1 (1 row) select plpgsql_coverage_statements('swt_txn'); plpgsql_coverage_statements ----------------------------- 1 (1 row) select plpgsql_coverage_branches('swt_txn'); plpgsql_coverage_branches --------------------------- 1 (1 row) -- -- The expression returned by the walker for a statement is used for the -- queryid retrieval. The fake queryid hook stores the command type of the -- query, so the result shows which statements carry a query. -- create function swt_queryid() returns void as $$ declare r record; c refcursor; cc cursor(p int) for select a from swt_tab where a = p; tabname text = 'swt_tab'; i int; begin insert into swt_tab values(20, 20); update swt_tab set b = 21 where a = 20; delete from swt_tab where a = 20; select count(*) into i from swt_tab; perform count(*) from swt_tab; for r in select a from swt_tab order by a loop null; end loop; for r in cc(1) loop null; end loop; for r in execute 'select a from ' || tabname loop null; end loop; open c for select a from swt_tab; fetch c into i; close c; open c for execute 'select a from ' || tabname; fetch c into i; close c; execute 'delete from ' || tabname || ' where a = $1' using -1; end; $$ language plpgsql; create function swt_queryid_setof() returns setof int as $$ declare tabname text = 'swt_tab'; begin return query select a from swt_tab order by a; return query execute 'select b from ' || tabname || ' order by b'; end; $$ language plpgsql; set plpgsql_check.profiler to on; select plpgsql_profiler_reset_all(); plpgsql_profiler_reset_all ---------------------------- (1 row) select plpgsql_profiler_install_fake_queryid_hook(); plpgsql_profiler_install_fake_queryid_hook -------------------------------------------- (1 row) select swt_queryid(); swt_queryid ------------- (1 row) select * from swt_queryid_setof(); swt_queryid_setof ------------------- 1 2 3 1 2 3 (6 rows) select plpgsql_profiler_remove_fake_queryid_hook(); plpgsql_profiler_remove_fake_queryid_hook ------------------------------------------- (1 row) select lineno, stmt_lineno, queryids, exec_stmts, source from plpgsql_profiler_function_tb('swt_queryid'); lineno | stmt_lineno | queryids | exec_stmts | source --------+-------------+----------+------------+------------------------------------------------------------------ 1 | | | | 2 | | | | declare 3 | | | | r record; 4 | | | | c refcursor; 5 | | | | cc cursor(p int) for select a from swt_tab where a = p; 6 | | | | tabname text = 'swt_tab'; 7 | | | | i int; 8 | 8 | | {1} | begin 9 | 9 | {3} | {1} | insert into swt_tab values(20, 20); 10 | 10 | {2} | {1} | update swt_tab set b = 21 where a = 20; 11 | 11 | {4} | {1} | delete from swt_tab where a = 20; 12 | | | | 13 | 13 | {1} | {1} | select count(*) into i from swt_tab; 14 | 14 | {1} | {1} | perform count(*) from swt_tab; 15 | | | | 16 | 16 | {1} | {1} | for r in select a from swt_tab order by a loop 17 | | | | null; 18 | | | | end loop; 19 | | | | 20 | 20 | {1} | {1} | for r in cc(1) loop 21 | | | | null; 22 | | | | end loop; 23 | | | | 24 | 24 | {NULL} | {1} | for r in execute 'select a from ' || tabname loop 25 | | | | null; 26 | | | | end loop; 27 | | | | 28 | 28 | {1} | {1} | open c for select a from swt_tab; 29 | 29 | | {1} | fetch c into i; 30 | 30 | | {1} | close c; 31 | | | | 32 | 32 | {NULL} | {1} | open c for execute 'select a from ' || tabname; 33 | 33 | | {1} | fetch c into i; 34 | 34 | | {1} | close c; 35 | | | | 36 | 36 | {NULL} | {1} | execute 'delete from ' || tabname || ' where a = $1' using -1; 37 | | | | end; (37 rows) select lineno, stmt_lineno, queryids, exec_stmts, source from plpgsql_profiler_function_tb('swt_queryid_setof'); lineno | stmt_lineno | queryids | exec_stmts | source --------+-------------+----------+------------+---------------------------------------------------------------------- 1 | | | | 2 | | | | declare 3 | | | | tabname text = 'swt_tab'; 4 | 4 | | {1} | begin 5 | 5 | {1} | {1} | return query select a from swt_tab order by a; 6 | 6 | {NULL} | {1} | return query execute 'select b from ' || tabname || ' order by b'; 7 | | | | end; (7 rows) set plpgsql_check.profiler to off; drop function swt_queryid_setof(); drop function swt_queryid(); drop procedure swt_txn(); drop function swt_setof(); drop function swt_all_stmts(int); drop procedure swt_proc(int); drop table swt_tab; okbob-plpgsql_check-800db79/expected/plpgsql_check_stmtwalk.out000066400000000000000000000655311526744060500251040ustar00rootroot00000000000000set client_min_messages to warning; create extension if not exists plpgsql_check; set client_min_messages to notice; -- -- WHILE statement -- -- the condition is checked like a scalar boolean expression create function sw_f1(n int) returns int as $$ declare i int := 0; begin while i < n loop i := i + 1; end loop; return i; end; $$ language plpgsql; select * from plpgsql_check_function('sw_f1'); plpgsql_check_function ------------------------ (0 rows) -- a non boolean condition is reported create function sw_f2(n int) returns int as $$ declare i int := 0; begin while i loop i := i + 1; end loop; return i; end; $$ language plpgsql; select * from plpgsql_check_function('sw_f2'); plpgsql_check_function -------------------------------------------------------------------------------------- warning:42804:4:WHILE:target type is different type than source type Detail: cast "integer" value to "boolean" type Hint: The input expression type does not have an assignment cast to the target type. Context: at WHILE warning extra:00000:unused parameter "n" (5 rows) -- the body of a loop is not necessarily executed, so a RETURN inside -- the loop does not close the execution path create function sw_f3(n int) returns int as $$ begin while n > 0 loop return n; end loop; end; $$ language plpgsql; select * from plpgsql_check_function('sw_f3'); plpgsql_check_function -------------------------------------------------------------------- warning extra:2F005:control reached end of function without RETURN (1 row) -- an unconditional loop is closed by the RETURN inside its body create function sw_f4(n int) returns int as $$ begin loop return n; end loop; end; $$ language plpgsql; select * from plpgsql_check_function('sw_f4'); plpgsql_check_function ------------------------ (0 rows) -- the STEP expression of a numeric FOR loop is checked too create function sw_f4b(n int) returns int as $$ declare i int := 0; begin for j in 1..n by 2 loop i := i + j; end loop; return i; end; $$ language plpgsql; select * from plpgsql_check_function('sw_f4b'); plpgsql_check_function ------------------------ (0 rows) -- -- names of the local variables -- -- a reserved keyword used as a variable name, a local variable shadowing -- a parameter and a local variable shadowing an outer local variable create function sw_f4c(shadowed_par int) returns int as $$ declare "table" int := 1; shadowed_var int := 2; begin declare shadowed_par int := 3; shadowed_var int := 4; begin return "table" + shadowed_par + shadowed_var; end; end; $$ language plpgsql; select * from plpgsql_check_function('sw_f4c'); plpgsql_check_function ----------------------------------------------------------------------------------------------------- warning:00000:5:statement block:name of variable "table" is reserved keyword Detail: The reserved keyword was used as variable name. warning:00000:9:statement block:parameter "shadowed_par" is shadowed Detail: Local variable shadows function parameter. warning extra:00000:9:statement block:variable "shadowed_var" shadows a previously defined variable Hint: SET plpgsql.extra_warnings TO 'shadowed_variables' warning extra:00000:4:DECLARE:never read variable "shadowed_var" warning extra:00000:unused parameter "shadowed_par" (8 rows) -- -- CASE statement -- -- when the CASE has no ELSE part, the execution path is closed only -- possibly, although every WHEN part returns create function sw_f5(n int) returns int as $$ begin case n when 1 then return 10; when 2 then return 20; end case; end; $$ language plpgsql; select * from plpgsql_check_function('sw_f5'); plpgsql_check_function -------------------------------------------------------------------- warning extra:2F005:control reached end of function without RETURN (1 row) -- with the ELSE part the path is closed create function sw_f6(n int) returns int as $$ begin case n when 1 then return 10; else return 20; end case; end; $$ language plpgsql; select * from plpgsql_check_function('sw_f6'); plpgsql_check_function ------------------------ (0 rows) -- -- IF statement with ELSIF parts -- -- every ELSIF condition is checked separately create function sw_f7(n int) returns int as $$ begin if n = 1 then return 10; elsif n then return 20; elsif n = 3 then return 30; else return 40; end if; end; $$ language plpgsql; select * from plpgsql_check_function('sw_f7'); plpgsql_check_function -------------------------------------------------------------------------------------- warning:42804:4:RETURN:target type is different type than source type Detail: cast "integer" value to "boolean" type Hint: The input expression type does not have an assignment cast to the target type. Context: at RETURN (4 rows) -- -- EXIT and CONTINUE statements -- -- EXIT can leave a labeled statement block, CONTINUE needs a labeled loop create function sw_f8(n int) returns int as $$ declare i int := 0; begin <> begin <> for j in 1..n loop if j = 2 then continue outer_loop; end if; if j = 3 then exit outer_block; end if; i := i + j; end loop; end; return i; end; $$ language plpgsql; select * from plpgsql_check_function('sw_f8'); plpgsql_check_function ------------------------ (0 rows) -- -- RAISE statement -- -- a condition name closes the execution path by the related exception create function sw_f9() returns int as $$ begin raise division_by_zero; end; $$ language plpgsql; select * from plpgsql_check_function('sw_f9'); plpgsql_check_function ------------------------ (0 rows) -- the errcode option is preferred against the condition name create function sw_f10() returns int as $$ begin raise division_by_zero using errcode = 'unique_violation'; end; $$ language plpgsql; select * from plpgsql_check_function('sw_f10'); plpgsql_check_function ------------------------ (0 rows) -- an errcode that cannot be evaluated at the check time create function sw_f11(c text) returns int as $$ begin raise exception 'broken' using errcode = c; end; $$ language plpgsql; select * from plpgsql_check_function('sw_f11'); plpgsql_check_function ------------------------ (0 rows) -- the doubled percent is not a placeholder, so the message needs no -- parameter for it create function sw_f12() returns int as $$ begin raise notice '100%% done, value %', 1; return 1; end; $$ language plpgsql; select * from plpgsql_check_function('sw_f12'); plpgsql_check_function ------------------------ (0 rows) -- -- exception handlers -- -- the raised exception is matched by the name of its category create function sw_f13() returns int as $$ begin begin raise division_by_zero; exception when data_exception then return -1; end; return 0; end; $$ language plpgsql; select * from plpgsql_check_function('sw_f13', extra_warnings => true); plpgsql_check_function ----------------------------------------------- warning extra:00000:8:RETURN:unreachable code (1 row) -- an exception that cannot be raised by the protected statements is -- reported as an unused handler create function sw_f14() returns int as $$ begin begin raise division_by_zero; exception when unique_violation then return -1; end; return 0; end; $$ language plpgsql; select * from plpgsql_check_function('sw_f14', extra_warnings => true); plpgsql_check_function ----------------------------------------------- warning extra:00000:8:RETURN:unreachable code (1 row) -- a block whose body is closed by an exception and whose handler raises -- another exception is closed by the exception of the handler create function sw_f14b() returns int as $$ begin begin raise division_by_zero; exception when division_by_zero then raise unique_violation; end; end; $$ language plpgsql; select * from plpgsql_check_function('sw_f14b', extra_warnings => true); plpgsql_check_function ------------------------ (0 rows) -- when the handlers only re-raise the caught exception, the block is -- closed by the exceptions raised by the protected body create function sw_f14c(n int) returns int as $$ begin begin if n = 1 then raise division_by_zero; else raise unique_violation; end if; exception when division_by_zero then raise; when unique_violation then raise; end; end; $$ language plpgsql; select * from plpgsql_check_function('sw_f14c', extra_warnings => true); plpgsql_check_function ------------------------ (0 rows) -- -- RETURN statement -- -- a function returning refcursor should return a refcursor variable create function sw_f14d() returns refcursor as $$ declare c text := 'some_cursor'; begin return c; end; $$ language plpgsql; select * from plpgsql_check_function('sw_f14d', compatibility_warnings => true); plpgsql_check_function -------------------------------------------------------------------------------------- warning:42804:4:RETURN:target type is different type than source type Detail: cast "text" value to "refcursor" type Hint: The input expression type does not have an assignment cast to the target type. Context: at RETURN of variable "c" declared on line 2 compatibility:00000:4:RETURN:obsolete setting of refcursor or cursor variable Detail: Internal name of cursor should not be specified by users. (6 rows) create function sw_f14e() returns refcursor as $$ declare c refcursor; begin open c for select 1; return c; end; $$ language plpgsql; select * from plpgsql_check_function('sw_f14e', compatibility_warnings => true); plpgsql_check_function ------------------------ (0 rows) -- -- RETURN NEXT statement -- -- the returned value is a single OUT variable create function sw_f15(out a int) returns setof int as $$ begin a := 1; return next; end; $$ language plpgsql; select * from plpgsql_check_function('sw_f15'); plpgsql_check_function ------------------------ (0 rows) -- more OUT variables are collected into a row create function sw_f16(out a int, out b int) returns setof record as $$ begin a := 1; b := 2; return next; end; $$ language plpgsql; select * from plpgsql_check_function('sw_f16'); plpgsql_check_function ------------------------ (0 rows) -- the types of the row fields have to match the result type create table sw_tab1(a int, b int); create function sw_f17(out a int, out b text) returns setof record as $$ begin a := 1; b := 'x'; return next; end; $$ language plpgsql; select * from plpgsql_check_function('sw_f17'); plpgsql_check_function ------------------------ (0 rows) -- a record variable is returned create function sw_f18() returns setof sw_tab1 as $$ declare r sw_tab1; begin r := (1,2); return next r; end; $$ language plpgsql; select * from plpgsql_check_function('sw_f18'); plpgsql_check_function ------------------------ (0 rows) -- -- transaction control statements -- -- COMMIT and ROLLBACK are allowed in a procedure only create function sw_f19() returns void as $$ begin commit; end; $$ language plpgsql; select * from plpgsql_check_function('sw_f19'); plpgsql_check_function ------------------------------------------------------ error:2D000:3:COMMIT:invalid transaction termination (1 row) create procedure sw_p1() as $$ begin commit; rollback; end; $$ language plpgsql; select * from plpgsql_check_function('sw_p1'); plpgsql_check_function ------------------------ (0 rows) -- inside a block with an exception handler a subtransaction is active, -- so neither COMMIT nor ROLLBACK can be used create procedure sw_p2() as $$ begin begin commit; exception when others then null; end; end; $$ language plpgsql; select * from plpgsql_check_function('sw_p2'); plpgsql_check_function --------------------------------------------------------------------- error:2D000:4:COMMIT:cannot commit while a subtransaction is active (1 row) create procedure sw_p3() as $$ begin begin rollback; exception when others then null; end; end; $$ language plpgsql; select * from plpgsql_check_function('sw_p3'); plpgsql_check_function -------------------------------------------------------------------------- error:2D000:4:ROLLBACK:cannot roll back while a subtransaction is active (1 row) -- -- dynamic SQL -- -- OPEN FOR EXECUTE is checked like the other dynamic statements create function sw_f20(p text) returns int as $$ declare c refcursor; r int; begin open c for execute p using 1; fetch c into r; close c; return r; end; $$ language plpgsql; select * from plpgsql_check_function('sw_f20'); plpgsql_check_function ------------------------ (0 rows) -- when the query text is not known, the result of the dynamic SQL cannot -- be determined, and the record variable stays without a descriptor create function sw_f21(p text) returns int as $$ declare r record; begin execute p into r; return r.x; end; $$ language plpgsql; select * from plpgsql_check_function('sw_f21'); plpgsql_check_function -------------------------------------------------------------------------------------- warning:00000:4:EXECUTE:cannot determinate a result of dynamic SQL Detail: There is a risk of related false alarms. Hint: Don't use dynamic SQL and record type together, when you would check function. error:55000:5:RETURN:record "r" is not assigned yet Detail: The tuple structure of a not-yet-assigned record is indeterminate. Context: PL/pgSQL expression "r.x" (6 rows) -- the expression of EXECUTE is built by a function which does not -- sanitize its argument, and the position of the unsafe value is unknown create function sw_f21b(p text) returns void as $$ begin execute upper(p); end; $$ language plpgsql; select * from plpgsql_check_function('sw_f21b', security_warnings => true); plpgsql_check_function ----------------------------------------------------------------------------- security:00000:3:EXECUTE:text type variable is not sanitized Query: upper(p) -- ^ Detail: The EXECUTE expression is SQL injection vulnerable. Hint: Use quote_ident, quote_literal or format function to secure variable. (5 rows) -- a record variable with a declared type is known, so no warning is raised create function sw_f22(p text) returns int as $$ declare r sw_tab1; begin execute p into r; return r.a; end; $$ language plpgsql; select * from plpgsql_check_function('sw_f22'); plpgsql_check_function -------------------------------------------------------------------------------------- warning:00000:4:EXECUTE:cannot determinate a result of dynamic SQL Detail: There is a risk of related false alarms. Hint: Don't use dynamic SQL and record type together, when you would check function. (3 rows) drop function sw_f1(int); drop function sw_f2(int); drop function sw_f3(int); drop function sw_f4(int); drop function sw_f4b(int); drop function sw_f4c(int); drop function sw_f5(int); drop function sw_f6(int); drop function sw_f7(int); drop function sw_f8(int); drop function sw_f9(); drop function sw_f10(); drop function sw_f11(text); drop function sw_f12(); drop function sw_f13(); drop function sw_f14(); drop function sw_f14b(); drop function sw_f14c(int); drop function sw_f14d(); drop function sw_f14e(); drop function sw_f15(); drop function sw_f16(); drop function sw_f17(); drop function sw_f18(); drop function sw_f19(); drop procedure sw_p1(); drop procedure sw_p2(); drop procedure sw_p3(); drop function sw_f20(text); drop function sw_f21(text); drop function sw_f21b(text); drop function sw_f22(text); drop table sw_tab1; -- Bare RAISE needs an active handler, including through nested blocks create function sw_bare_raise() returns int as $$ begin raise; end; $$ language plpgsql; select * from plpgsql_check_function('sw_bare_raise'); plpgsql_check_function ------------------------------------------------------------------------------------------ error:0Z002:3:RAISE:RAISE without parameters cannot be used outside an exception handler (1 row) create function sw_handler_raise() returns void as $$ begin raise division_by_zero; exception when division_by_zero then begin raise; end; end; $$ language plpgsql; select * from plpgsql_check_function('sw_handler_raise'); plpgsql_check_function ------------------------ (0 rows) drop function sw_bare_raise(); drop function sw_handler_raise(); -- MOVE has no result target, even when the cursor query has several columns create function sw_move(distance int) returns void as $$ declare c cursor for select 1 as a, 2 as b; begin open c; move forward distance from c; close c; end; $$ language plpgsql; select * from plpgsql_check_function('sw_move(int)'); plpgsql_check_function ------------------------ (0 rows) drop function sw_move(int); -- Check before execution initializes the hidden CASE variable's type create type sw_single_field as (a int); create function sw_case_single(v sw_single_field) returns int as $$ begin case v when row(1)::sw_single_field then return 1; else return 2; end case; end; $$ language plpgsql; select * from plpgsql_check_function('sw_case_single(sw_single_field)'); plpgsql_check_function ------------------------ (0 rows) select sw_case_single(row(1)::sw_single_field) = 1 and sw_case_single(row(2)::sw_single_field) = 2 as valid; valid ------- t (1 row) drop function sw_case_single(sw_single_field); drop type sw_single_field; -- Earlier exception paths must remain visible when later paths are merged create function sw_exception_union(p boolean) returns int as $$ begin begin if p then raise division_by_zero; else raise unique_violation; end if; exception when division_by_zero then null; when unique_violation then return 2; end; return 1; end; $$ language plpgsql; select * from plpgsql_check_function('sw_exception_union(boolean)'); plpgsql_check_function ------------------------ (0 rows) drop function sw_exception_union(boolean); -- A first-handler rethrow keeps the caught SQLSTATE for outer handlers create function sw_nested_rethrow() returns int as $$ begin begin begin raise division_by_zero; exception when division_by_zero then raise; end; exception when division_by_zero then null; end; return 1; end; $$ language plpgsql; select * from plpgsql_check_function('sw_nested_rethrow()'); plpgsql_check_function ------------------------ (0 rows) drop function sw_nested_rethrow(); -- Dead exits must not change which exception handler is reachable create function sw_dead_flow() returns int as $$ begin begin raise division_by_zero; raise unique_violation; exception when division_by_zero then return 1; when unique_violation then null; end; end; $$ language plpgsql; select sqlstate, message from plpgsql_check_function_tb('sw_dead_flow()'); sqlstate | message ----------+------------------ 00000 | unreachable code (1 row) -- Expressions in dead statements still need to be checked create function sw_dead_expression() returns int as $$ begin return 1; perform sw_missing_dead_column; end; $$ language plpgsql; select exists ( select from plpgsql_check_function_tb('sw_dead_expression()') where sqlstate = '42703') as checked; checked --------- t (1 row) drop function sw_dead_flow(); drop function sw_dead_expression(); -- Pragmas in one branch or handler must not affect its siblings create function sw_pragma_siblings(p boolean) returns void as $$ begin if p then perform plpgsql_check_pragma('disable:check'); else perform sw_missing_else_column; end if; case when p then perform plpgsql_check_pragma('disable:check'); else perform sw_missing_case_column; end case; begin raise division_by_zero; exception when unique_violation then perform plpgsql_check_pragma('disable:check'); when division_by_zero then perform sw_missing_handler_column; end; end; $$ language plpgsql; select count(*) = 3 as sibling_checks from plpgsql_check_function_tb('sw_pragma_siblings(boolean)', fatal_errors => false) where sqlstate = '42703'; sibling_checks ---------------- t (1 row) drop function sw_pragma_siblings(boolean); -- Check dynamic cursors before execution can prime record-field metadata create function sw_dynamic_cursor() returns int as $$ declare c refcursor; r record; begin open c for execute 'select 1 as a'; fetch c into r; close c; return r.a; end; $$ language plpgsql; create function sw_parameter_cursor(v int) returns int as $$ declare c refcursor; r record; begin open c for execute 'select $1::int as a' using v; fetch c into r; close c; return r.a; end; $$ language plpgsql; -- A later static OPEN supersedes the earlier dynamic query create function sw_cursor_reopen(v int) returns int as $$ declare c refcursor; r record; begin open c for execute 'select $1::int as a' using v; fetch c into r; close c; open c for select v as b; fetch c into r; close c; return r.b; end; $$ language plpgsql; -- FETCH into scalar targets also invalidates their previous traced constants create function sw_dynamic_scalar() returns void as $$ declare c refcursor; s text := 'select sw_missing_dynamic_column'; begin open c for execute $q$select 'select 1'::text$q$; fetch c into s; close c; execute s; end; $$ language plpgsql; select * from plpgsql_check_function('sw_dynamic_cursor()'); plpgsql_check_function ------------------------ (0 rows) select * from plpgsql_check_function('sw_parameter_cursor(int)'); plpgsql_check_function ------------------------ (0 rows) select * from plpgsql_check_function('sw_cursor_reopen(int)'); plpgsql_check_function ------------------------ (0 rows) select * from plpgsql_check_function('sw_dynamic_scalar()'); plpgsql_check_function ------------------------ (0 rows) select sw_dynamic_cursor() = 1 and sw_parameter_cursor(3) = 3 and sw_cursor_reopen(4) = 4 as valid; valid ------- t (1 row) do $$ begin perform sw_dynamic_scalar(); end; $$; drop function sw_dynamic_cursor(); drop function sw_parameter_cursor(int); drop function sw_cursor_reopen(int); drop function sw_dynamic_scalar(); -- FETCH from a cursor that is not opened in the function (so its query -- is unknown) has to be checked without crash, also when there is no -- OPEN FOR EXECUTE in the function. The passive mode does the same check. create function sw_fetch_unbound(c refcursor) returns int as $$ declare v int; begin fetch c into v; return v; end; $$ language plpgsql; select * from plpgsql_check_function('sw_fetch_unbound(refcursor)'); plpgsql_check_function ------------------------ (0 rows) set plpgsql_check.mode = 'every_start'; begin; declare sw_fetch_cur cursor for select 1; select sw_fetch_unbound('sw_fetch_cur'); sw_fetch_unbound ------------------ 1 (1 row) commit; reset plpgsql_check.mode; drop function sw_fetch_unbound(refcursor); create function sw_exit_loop(p boolean) returns int as $$ begin loop if p then exit; end if; return 1; end loop; return 2; end; $$ language plpgsql; -- is ok select * from plpgsql_check_function('sw_exit_loop(boolean)'); plpgsql_check_function ------------------------ (0 rows) drop function sw_exit_loop(boolean); create function sw_exit_loop(p boolean) returns int as $$ begin loop exit when p; return 1; end loop; return 2; end; $$ language plpgsql; -- is ok select * from plpgsql_check_function('sw_exit_loop'); plpgsql_check_function ------------------------ (0 rows) drop function sw_exit_loop(boolean); create function sw_exit_nested(p boolean) returns int as $$ begin <> loop loop exit outer_loop when p; return 1; end loop; end loop; return 2; end; $$ language plpgsql; -- is ok select * from plpgsql_check_function('sw_exit_nested'); plpgsql_check_function ------------------------ (0 rows) drop function sw_exit_nested(boolean); create function sw_exit_block(p boolean) returns int as $$ begin <> begin exit target when p; return 1; end; return 2; end; $$ language plpgsql; -- is ok select * from plpgsql_check_function('sw_exit_block'); plpgsql_check_function ------------------------ (0 rows) drop function sw_exit_block(boolean); create function sw_exit_missing(p boolean) returns int as $$ begin loop exit when p; return 1; end loop; return 1; end; $$ language plpgsql; -- is ok select * from plpgsql_check_function('sw_exit_missing'); plpgsql_check_function ------------------------ (0 rows) drop function sw_exit_missing(boolean); create function sw_exit_inner() returns int as $$ begin loop loop exit; end loop; return 1; end loop; return 2; end; $$ language plpgsql; -- should be warning select * from plpgsql_check_function('sw_exit_inner'); plpgsql_check_function ----------------------------------------------- warning extra:00000:9:RETURN:unreachable code (1 row) drop function sw_exit_inner(); create function sw_continue_path() returns int as $$ declare i int := 0; begin loop i := i + 1; continue when i = 1; return i; end loop; return 3; end; $$ language plpgsql; -- is ok select * from plpgsql_check_function('sw_continue_path'); plpgsql_check_function ----------------------------------------------- warning extra:00000:9:RETURN:unreachable code (1 row) drop function sw_continue_path(); create function sw_continue_outer() returns int as $$ begin <> for i in 1..2 loop loop continue outer_loop; end loop; end loop; return 1; end; $$ language plpgsql; -- is ok select * from plpgsql_check_function('sw_continue_outer'); plpgsql_check_function ------------------------ (0 rows) drop function sw_continue_outer(); create function sw_dead_exit() returns int as $$ begin loop return 1; exit; end loop; return 2; end; $$ language plpgsql; -- should be warning select * from plpgsql_check_function('sw_dead_exit'); plpgsql_check_function ----------------------------------------------- warning extra:00000:5:EXIT:unreachable code warning extra:00000:7:RETURN:unreachable code (2 rows) drop function sw_dead_exit(); create function sw_handler_exit() returns int as $$ begin loop begin raise division_by_zero; exception when unique_violation then exit; when division_by_zero then return 1; end; end loop; return 2; end; $$ language plpgsql; -- is ok select * from plpgsql_check_function('sw_handler_exit'); plpgsql_check_function ------------------------ (0 rows) drop function sw_handler_exit(); okbob-plpgsql_check-800db79/expected/plpgsql_check_stmtwalk_1.out000066400000000000000000000655241526744060500253260ustar00rootroot00000000000000set client_min_messages to warning; create extension if not exists plpgsql_check; set client_min_messages to notice; -- -- WHILE statement -- -- the condition is checked like a scalar boolean expression create function sw_f1(n int) returns int as $$ declare i int := 0; begin while i < n loop i := i + 1; end loop; return i; end; $$ language plpgsql; select * from plpgsql_check_function('sw_f1'); plpgsql_check_function ------------------------ (0 rows) -- a non boolean condition is reported create function sw_f2(n int) returns int as $$ declare i int := 0; begin while i loop i := i + 1; end loop; return i; end; $$ language plpgsql; select * from plpgsql_check_function('sw_f2'); plpgsql_check_function -------------------------------------------------------------------------------------- warning:42804:4:WHILE:target type is different type than source type Detail: cast "integer" value to "boolean" type Hint: The input expression type does not have an assignment cast to the target type. Context: at WHILE warning extra:00000:unused parameter "n" (5 rows) -- the body of a loop is not necessarily executed, so a RETURN inside -- the loop does not close the execution path create function sw_f3(n int) returns int as $$ begin while n > 0 loop return n; end loop; end; $$ language plpgsql; select * from plpgsql_check_function('sw_f3'); plpgsql_check_function -------------------------------------------------------------------- warning extra:2F005:control reached end of function without RETURN (1 row) -- an unconditional loop is closed by the RETURN inside its body create function sw_f4(n int) returns int as $$ begin loop return n; end loop; end; $$ language plpgsql; select * from plpgsql_check_function('sw_f4'); plpgsql_check_function ------------------------ (0 rows) -- the STEP expression of a numeric FOR loop is checked too create function sw_f4b(n int) returns int as $$ declare i int := 0; begin for j in 1..n by 2 loop i := i + j; end loop; return i; end; $$ language plpgsql; select * from plpgsql_check_function('sw_f4b'); plpgsql_check_function ------------------------ (0 rows) -- -- names of the local variables -- -- a reserved keyword used as a variable name, a local variable shadowing -- a parameter and a local variable shadowing an outer local variable create function sw_f4c(shadowed_par int) returns int as $$ declare "table" int := 1; shadowed_var int := 2; begin declare shadowed_par int := 3; shadowed_var int := 4; begin return "table" + shadowed_par + shadowed_var; end; end; $$ language plpgsql; select * from plpgsql_check_function('sw_f4c'); plpgsql_check_function ----------------------------------------------------------------------------------------------------- warning:00000:5:statement block:name of variable "table" is reserved keyword Detail: The reserved keyword was used as variable name. warning:00000:9:statement block:parameter "shadowed_par" is shadowed Detail: Local variable shadows function parameter. warning extra:00000:9:statement block:variable "shadowed_var" shadows a previously defined variable Hint: SET plpgsql.extra_warnings TO 'shadowed_variables' warning extra:00000:4:DECLARE:never read variable "shadowed_var" warning extra:00000:unused parameter "shadowed_par" (8 rows) -- -- CASE statement -- -- when the CASE has no ELSE part, the execution path is closed only -- possibly, although every WHEN part returns create function sw_f5(n int) returns int as $$ begin case n when 1 then return 10; when 2 then return 20; end case; end; $$ language plpgsql; select * from plpgsql_check_function('sw_f5'); plpgsql_check_function -------------------------------------------------------------------- warning extra:2F005:control reached end of function without RETURN (1 row) -- with the ELSE part the path is closed create function sw_f6(n int) returns int as $$ begin case n when 1 then return 10; else return 20; end case; end; $$ language plpgsql; select * from plpgsql_check_function('sw_f6'); plpgsql_check_function ------------------------ (0 rows) -- -- IF statement with ELSIF parts -- -- every ELSIF condition is checked separately create function sw_f7(n int) returns int as $$ begin if n = 1 then return 10; elsif n then return 20; elsif n = 3 then return 30; else return 40; end if; end; $$ language plpgsql; select * from plpgsql_check_function('sw_f7'); plpgsql_check_function -------------------------------------------------------------------------------------- warning:42804:4:RETURN:target type is different type than source type Detail: cast "integer" value to "boolean" type Hint: The input expression type does not have an assignment cast to the target type. Context: at RETURN (4 rows) -- -- EXIT and CONTINUE statements -- -- EXIT can leave a labeled statement block, CONTINUE needs a labeled loop create function sw_f8(n int) returns int as $$ declare i int := 0; begin <> begin <> for j in 1..n loop if j = 2 then continue outer_loop; end if; if j = 3 then exit outer_block; end if; i := i + j; end loop; end; return i; end; $$ language plpgsql; select * from plpgsql_check_function('sw_f8'); plpgsql_check_function ------------------------ (0 rows) -- -- RAISE statement -- -- a condition name closes the execution path by the related exception create function sw_f9() returns int as $$ begin raise division_by_zero; end; $$ language plpgsql; select * from plpgsql_check_function('sw_f9'); plpgsql_check_function ------------------------ (0 rows) -- the errcode option is preferred against the condition name create function sw_f10() returns int as $$ begin raise division_by_zero using errcode = 'unique_violation'; end; $$ language plpgsql; select * from plpgsql_check_function('sw_f10'); plpgsql_check_function ------------------------ (0 rows) -- an errcode that cannot be evaluated at the check time create function sw_f11(c text) returns int as $$ begin raise exception 'broken' using errcode = c; end; $$ language plpgsql; select * from plpgsql_check_function('sw_f11'); plpgsql_check_function ------------------------ (0 rows) -- the doubled percent is not a placeholder, so the message needs no -- parameter for it create function sw_f12() returns int as $$ begin raise notice '100%% done, value %', 1; return 1; end; $$ language plpgsql; select * from plpgsql_check_function('sw_f12'); plpgsql_check_function ------------------------ (0 rows) -- -- exception handlers -- -- the raised exception is matched by the name of its category create function sw_f13() returns int as $$ begin begin raise division_by_zero; exception when data_exception then return -1; end; return 0; end; $$ language plpgsql; select * from plpgsql_check_function('sw_f13', extra_warnings => true); plpgsql_check_function ----------------------------------------------- warning extra:00000:8:RETURN:unreachable code (1 row) -- an exception that cannot be raised by the protected statements is -- reported as an unused handler create function sw_f14() returns int as $$ begin begin raise division_by_zero; exception when unique_violation then return -1; end; return 0; end; $$ language plpgsql; select * from plpgsql_check_function('sw_f14', extra_warnings => true); plpgsql_check_function ----------------------------------------------- warning extra:00000:8:RETURN:unreachable code (1 row) -- a block whose body is closed by an exception and whose handler raises -- another exception is closed by the exception of the handler create function sw_f14b() returns int as $$ begin begin raise division_by_zero; exception when division_by_zero then raise unique_violation; end; end; $$ language plpgsql; select * from plpgsql_check_function('sw_f14b', extra_warnings => true); plpgsql_check_function ------------------------ (0 rows) -- when the handlers only re-raise the caught exception, the block is -- closed by the exceptions raised by the protected body create function sw_f14c(n int) returns int as $$ begin begin if n = 1 then raise division_by_zero; else raise unique_violation; end if; exception when division_by_zero then raise; when unique_violation then raise; end; end; $$ language plpgsql; select * from plpgsql_check_function('sw_f14c', extra_warnings => true); plpgsql_check_function ------------------------ (0 rows) -- -- RETURN statement -- -- a function returning refcursor should return a refcursor variable create function sw_f14d() returns refcursor as $$ declare c text := 'some_cursor'; begin return c; end; $$ language plpgsql; select * from plpgsql_check_function('sw_f14d', compatibility_warnings => true); plpgsql_check_function -------------------------------------------------------------------------------------- warning:42804:4:RETURN:target type is different type than source type Detail: cast "text" value to "refcursor" type Hint: The input expression type does not have an assignment cast to the target type. Context: at RETURN of variable "c" declared on line 2 compatibility:00000:4:RETURN:obsolete setting of refcursor or cursor variable Detail: Internal name of cursor should not be specified by users. (6 rows) create function sw_f14e() returns refcursor as $$ declare c refcursor; begin open c for select 1; return c; end; $$ language plpgsql; select * from plpgsql_check_function('sw_f14e', compatibility_warnings => true); plpgsql_check_function ------------------------ (0 rows) -- -- RETURN NEXT statement -- -- the returned value is a single OUT variable create function sw_f15(out a int) returns setof int as $$ begin a := 1; return next; end; $$ language plpgsql; select * from plpgsql_check_function('sw_f15'); plpgsql_check_function ------------------------ (0 rows) -- more OUT variables are collected into a row create function sw_f16(out a int, out b int) returns setof record as $$ begin a := 1; b := 2; return next; end; $$ language plpgsql; select * from plpgsql_check_function('sw_f16'); plpgsql_check_function ------------------------ (0 rows) -- the types of the row fields have to match the result type create table sw_tab1(a int, b int); create function sw_f17(out a int, out b text) returns setof record as $$ begin a := 1; b := 'x'; return next; end; $$ language plpgsql; select * from plpgsql_check_function('sw_f17'); plpgsql_check_function ------------------------ (0 rows) -- a record variable is returned create function sw_f18() returns setof sw_tab1 as $$ declare r sw_tab1; begin r := (1,2); return next r; end; $$ language plpgsql; select * from plpgsql_check_function('sw_f18'); plpgsql_check_function ------------------------ (0 rows) -- -- transaction control statements -- -- COMMIT and ROLLBACK are allowed in a procedure only create function sw_f19() returns void as $$ begin commit; end; $$ language plpgsql; select * from plpgsql_check_function('sw_f19'); plpgsql_check_function ------------------------------------------------------ error:2D000:3:COMMIT:invalid transaction termination (1 row) create procedure sw_p1() as $$ begin commit; rollback; end; $$ language plpgsql; select * from plpgsql_check_function('sw_p1'); plpgsql_check_function ------------------------ (0 rows) -- inside a block with an exception handler a subtransaction is active, -- so neither COMMIT nor ROLLBACK can be used create procedure sw_p2() as $$ begin begin commit; exception when others then null; end; end; $$ language plpgsql; select * from plpgsql_check_function('sw_p2'); plpgsql_check_function --------------------------------------------------------------------- error:2D000:4:COMMIT:cannot commit while a subtransaction is active (1 row) create procedure sw_p3() as $$ begin begin rollback; exception when others then null; end; end; $$ language plpgsql; select * from plpgsql_check_function('sw_p3'); plpgsql_check_function -------------------------------------------------------------------------- error:2D000:4:ROLLBACK:cannot roll back while a subtransaction is active (1 row) -- -- dynamic SQL -- -- OPEN FOR EXECUTE is checked like the other dynamic statements create function sw_f20(p text) returns int as $$ declare c refcursor; r int; begin open c for execute p using 1; fetch c into r; close c; return r; end; $$ language plpgsql; select * from plpgsql_check_function('sw_f20'); plpgsql_check_function ------------------------ (0 rows) -- when the query text is not known, the result of the dynamic SQL cannot -- be determined, and the record variable stays without a descriptor create function sw_f21(p text) returns int as $$ declare r record; begin execute p into r; return r.x; end; $$ language plpgsql; select * from plpgsql_check_function('sw_f21'); plpgsql_check_function -------------------------------------------------------------------------------------- warning:00000:4:EXECUTE:cannot determinate a result of dynamic SQL Detail: There is a risk of related false alarms. Hint: Don't use dynamic SQL and record type together, when you would check function. error:55000:5:RETURN:record "r" is not assigned yet Detail: The tuple structure of a not-yet-assigned record is indeterminate. Context: SQL expression "r.x" (6 rows) -- the expression of EXECUTE is built by a function which does not -- sanitize its argument, and the position of the unsafe value is unknown create function sw_f21b(p text) returns void as $$ begin execute upper(p); end; $$ language plpgsql; select * from plpgsql_check_function('sw_f21b', security_warnings => true); plpgsql_check_function ----------------------------------------------------------------------------- security:00000:3:EXECUTE:text type variable is not sanitized Query: upper(p) -- ^ Detail: The EXECUTE expression is SQL injection vulnerable. Hint: Use quote_ident, quote_literal or format function to secure variable. (5 rows) -- a record variable with a declared type is known, so no warning is raised create function sw_f22(p text) returns int as $$ declare r sw_tab1; begin execute p into r; return r.a; end; $$ language plpgsql; select * from plpgsql_check_function('sw_f22'); plpgsql_check_function -------------------------------------------------------------------------------------- warning:00000:4:EXECUTE:cannot determinate a result of dynamic SQL Detail: There is a risk of related false alarms. Hint: Don't use dynamic SQL and record type together, when you would check function. (3 rows) drop function sw_f1(int); drop function sw_f2(int); drop function sw_f3(int); drop function sw_f4(int); drop function sw_f4b(int); drop function sw_f4c(int); drop function sw_f5(int); drop function sw_f6(int); drop function sw_f7(int); drop function sw_f8(int); drop function sw_f9(); drop function sw_f10(); drop function sw_f11(text); drop function sw_f12(); drop function sw_f13(); drop function sw_f14(); drop function sw_f14b(); drop function sw_f14c(int); drop function sw_f14d(); drop function sw_f14e(); drop function sw_f15(); drop function sw_f16(); drop function sw_f17(); drop function sw_f18(); drop function sw_f19(); drop procedure sw_p1(); drop procedure sw_p2(); drop procedure sw_p3(); drop function sw_f20(text); drop function sw_f21(text); drop function sw_f21b(text); drop function sw_f22(text); drop table sw_tab1; -- Bare RAISE needs an active handler, including through nested blocks create function sw_bare_raise() returns int as $$ begin raise; end; $$ language plpgsql; select * from plpgsql_check_function('sw_bare_raise'); plpgsql_check_function ------------------------------------------------------------------------------------------ error:0Z002:3:RAISE:RAISE without parameters cannot be used outside an exception handler (1 row) create function sw_handler_raise() returns void as $$ begin raise division_by_zero; exception when division_by_zero then begin raise; end; end; $$ language plpgsql; select * from plpgsql_check_function('sw_handler_raise'); plpgsql_check_function ------------------------ (0 rows) drop function sw_bare_raise(); drop function sw_handler_raise(); -- MOVE has no result target, even when the cursor query has several columns create function sw_move(distance int) returns void as $$ declare c cursor for select 1 as a, 2 as b; begin open c; move forward distance from c; close c; end; $$ language plpgsql; select * from plpgsql_check_function('sw_move(int)'); plpgsql_check_function ------------------------ (0 rows) drop function sw_move(int); -- Check before execution initializes the hidden CASE variable's type create type sw_single_field as (a int); create function sw_case_single(v sw_single_field) returns int as $$ begin case v when row(1)::sw_single_field then return 1; else return 2; end case; end; $$ language plpgsql; select * from plpgsql_check_function('sw_case_single(sw_single_field)'); plpgsql_check_function ------------------------ (0 rows) select sw_case_single(row(1)::sw_single_field) = 1 and sw_case_single(row(2)::sw_single_field) = 2 as valid; valid ------- t (1 row) drop function sw_case_single(sw_single_field); drop type sw_single_field; -- Earlier exception paths must remain visible when later paths are merged create function sw_exception_union(p boolean) returns int as $$ begin begin if p then raise division_by_zero; else raise unique_violation; end if; exception when division_by_zero then null; when unique_violation then return 2; end; return 1; end; $$ language plpgsql; select * from plpgsql_check_function('sw_exception_union(boolean)'); plpgsql_check_function ------------------------ (0 rows) drop function sw_exception_union(boolean); -- A first-handler rethrow keeps the caught SQLSTATE for outer handlers create function sw_nested_rethrow() returns int as $$ begin begin begin raise division_by_zero; exception when division_by_zero then raise; end; exception when division_by_zero then null; end; return 1; end; $$ language plpgsql; select * from plpgsql_check_function('sw_nested_rethrow()'); plpgsql_check_function ------------------------ (0 rows) drop function sw_nested_rethrow(); -- Dead exits must not change which exception handler is reachable create function sw_dead_flow() returns int as $$ begin begin raise division_by_zero; raise unique_violation; exception when division_by_zero then return 1; when unique_violation then null; end; end; $$ language plpgsql; select sqlstate, message from plpgsql_check_function_tb('sw_dead_flow()'); sqlstate | message ----------+------------------ 00000 | unreachable code (1 row) -- Expressions in dead statements still need to be checked create function sw_dead_expression() returns int as $$ begin return 1; perform sw_missing_dead_column; end; $$ language plpgsql; select exists ( select from plpgsql_check_function_tb('sw_dead_expression()') where sqlstate = '42703') as checked; checked --------- t (1 row) drop function sw_dead_flow(); drop function sw_dead_expression(); -- Pragmas in one branch or handler must not affect its siblings create function sw_pragma_siblings(p boolean) returns void as $$ begin if p then perform plpgsql_check_pragma('disable:check'); else perform sw_missing_else_column; end if; case when p then perform plpgsql_check_pragma('disable:check'); else perform sw_missing_case_column; end case; begin raise division_by_zero; exception when unique_violation then perform plpgsql_check_pragma('disable:check'); when division_by_zero then perform sw_missing_handler_column; end; end; $$ language plpgsql; select count(*) = 3 as sibling_checks from plpgsql_check_function_tb('sw_pragma_siblings(boolean)', fatal_errors => false) where sqlstate = '42703'; sibling_checks ---------------- t (1 row) drop function sw_pragma_siblings(boolean); -- Check dynamic cursors before execution can prime record-field metadata create function sw_dynamic_cursor() returns int as $$ declare c refcursor; r record; begin open c for execute 'select 1 as a'; fetch c into r; close c; return r.a; end; $$ language plpgsql; create function sw_parameter_cursor(v int) returns int as $$ declare c refcursor; r record; begin open c for execute 'select $1::int as a' using v; fetch c into r; close c; return r.a; end; $$ language plpgsql; -- A later static OPEN supersedes the earlier dynamic query create function sw_cursor_reopen(v int) returns int as $$ declare c refcursor; r record; begin open c for execute 'select $1::int as a' using v; fetch c into r; close c; open c for select v as b; fetch c into r; close c; return r.b; end; $$ language plpgsql; -- FETCH into scalar targets also invalidates their previous traced constants create function sw_dynamic_scalar() returns void as $$ declare c refcursor; s text := 'select sw_missing_dynamic_column'; begin open c for execute $q$select 'select 1'::text$q$; fetch c into s; close c; execute s; end; $$ language plpgsql; select * from plpgsql_check_function('sw_dynamic_cursor()'); plpgsql_check_function ------------------------ (0 rows) select * from plpgsql_check_function('sw_parameter_cursor(int)'); plpgsql_check_function ------------------------ (0 rows) select * from plpgsql_check_function('sw_cursor_reopen(int)'); plpgsql_check_function ------------------------ (0 rows) select * from plpgsql_check_function('sw_dynamic_scalar()'); plpgsql_check_function ------------------------ (0 rows) select sw_dynamic_cursor() = 1 and sw_parameter_cursor(3) = 3 and sw_cursor_reopen(4) = 4 as valid; valid ------- t (1 row) do $$ begin perform sw_dynamic_scalar(); end; $$; drop function sw_dynamic_cursor(); drop function sw_parameter_cursor(int); drop function sw_cursor_reopen(int); drop function sw_dynamic_scalar(); -- FETCH from a cursor that is not opened in the function (so its query -- is unknown) has to be checked without crash, also when there is no -- OPEN FOR EXECUTE in the function. The passive mode does the same check. create function sw_fetch_unbound(c refcursor) returns int as $$ declare v int; begin fetch c into v; return v; end; $$ language plpgsql; select * from plpgsql_check_function('sw_fetch_unbound(refcursor)'); plpgsql_check_function ------------------------ (0 rows) set plpgsql_check.mode = 'every_start'; begin; declare sw_fetch_cur cursor for select 1; select sw_fetch_unbound('sw_fetch_cur'); sw_fetch_unbound ------------------ 1 (1 row) commit; reset plpgsql_check.mode; drop function sw_fetch_unbound(refcursor); create function sw_exit_loop(p boolean) returns int as $$ begin loop if p then exit; end if; return 1; end loop; return 2; end; $$ language plpgsql; -- is ok select * from plpgsql_check_function('sw_exit_loop(boolean)'); plpgsql_check_function ------------------------ (0 rows) drop function sw_exit_loop(boolean); create function sw_exit_loop(p boolean) returns int as $$ begin loop exit when p; return 1; end loop; return 2; end; $$ language plpgsql; -- is ok select * from plpgsql_check_function('sw_exit_loop'); plpgsql_check_function ------------------------ (0 rows) drop function sw_exit_loop(boolean); create function sw_exit_nested(p boolean) returns int as $$ begin <> loop loop exit outer_loop when p; return 1; end loop; end loop; return 2; end; $$ language plpgsql; -- is ok select * from plpgsql_check_function('sw_exit_nested'); plpgsql_check_function ------------------------ (0 rows) drop function sw_exit_nested(boolean); create function sw_exit_block(p boolean) returns int as $$ begin <> begin exit target when p; return 1; end; return 2; end; $$ language plpgsql; -- is ok select * from plpgsql_check_function('sw_exit_block'); plpgsql_check_function ------------------------ (0 rows) drop function sw_exit_block(boolean); create function sw_exit_missing(p boolean) returns int as $$ begin loop exit when p; return 1; end loop; return 1; end; $$ language plpgsql; -- is ok select * from plpgsql_check_function('sw_exit_missing'); plpgsql_check_function ------------------------ (0 rows) drop function sw_exit_missing(boolean); create function sw_exit_inner() returns int as $$ begin loop loop exit; end loop; return 1; end loop; return 2; end; $$ language plpgsql; -- should be warning select * from plpgsql_check_function('sw_exit_inner'); plpgsql_check_function ----------------------------------------------- warning extra:00000:9:RETURN:unreachable code (1 row) drop function sw_exit_inner(); create function sw_continue_path() returns int as $$ declare i int := 0; begin loop i := i + 1; continue when i = 1; return i; end loop; return 3; end; $$ language plpgsql; -- is ok select * from plpgsql_check_function('sw_continue_path'); plpgsql_check_function ----------------------------------------------- warning extra:00000:9:RETURN:unreachable code (1 row) drop function sw_continue_path(); create function sw_continue_outer() returns int as $$ begin <> for i in 1..2 loop loop continue outer_loop; end loop; end loop; return 1; end; $$ language plpgsql; -- is ok select * from plpgsql_check_function('sw_continue_outer'); plpgsql_check_function ------------------------ (0 rows) drop function sw_continue_outer(); create function sw_dead_exit() returns int as $$ begin loop return 1; exit; end loop; return 2; end; $$ language plpgsql; -- should be warning select * from plpgsql_check_function('sw_dead_exit'); plpgsql_check_function ----------------------------------------------- warning extra:00000:5:EXIT:unreachable code warning extra:00000:7:RETURN:unreachable code (2 rows) drop function sw_dead_exit(); create function sw_handler_exit() returns int as $$ begin loop begin raise division_by_zero; exception when unique_violation then exit; when division_by_zero then return 1; end; end loop; return 2; end; $$ language plpgsql; -- is ok select * from plpgsql_check_function('sw_handler_exit'); plpgsql_check_function ------------------------ (0 rows) drop function sw_handler_exit(); okbob-plpgsql_check-800db79/expected/plpgsql_check_tablefunc.out000066400000000000000000000333521526744060500251750ustar00rootroot00000000000000set client_min_messages to warning; create extension if not exists plpgsql_check; set client_min_messages to notice; -- -- Tests of the SQL callable entry points of plpgsql_check and of the -- validation of their arguments. -- -- Almost every option of the check functions is mandatory - it has a -- default value, but an explicit NULL is rejected. Every such check is -- executed here. -- create function tf_f1(a int) returns int as $$ begin return a; end; $$ language plpgsql; create table tf_t1(a int, b int); -- -- plpgsql_check_function -- select * from plpgsql_check_function(null::regprocedure); ERROR: the option "funcoid" is NULL HINT: this option should not be NULL select * from plpgsql_check_function(null::text); ERROR: the option "name" is NULL HINT: this option should not be NULL select * from plpgsql_check_function('tf_f1', relid := null); ERROR: the option "relid" is NULL HINT: this option should not be NULL select * from plpgsql_check_function('tf_f1', format := null); ERROR: the option "format" is NULL HINT: this option should not be NULL select * from plpgsql_check_function('tf_f1', fatal_errors := null); ERROR: the option "fatal_errors" is NULL HINT: this option should not be NULL select * from plpgsql_check_function('tf_f1', other_warnings := null); ERROR: the option "other_warnings" is NULL HINT: this option should not be NULL select * from plpgsql_check_function('tf_f1', performance_warnings := null); ERROR: the option "performance_warnings" is NULL HINT: this option should not be NULL select * from plpgsql_check_function('tf_f1', extra_warnings := null); ERROR: the option "extra_warnings" is NULL HINT: this option should not be NULL select * from plpgsql_check_function('tf_f1', security_warnings := null); ERROR: the option "security_warnings" is NULL HINT: this option should not be NULL select * from plpgsql_check_function('tf_f1', compatibility_warnings := null); ERROR: the option "compatibility_warnings" is NULL HINT: this option should not be NULL select * from plpgsql_check_function('tf_f1', anyelememttype := null); ERROR: the option "anyelementtype" is NULL HINT: this option should not be NULL select * from plpgsql_check_function('tf_f1', anyenumtype := null); ERROR: the option "anyenumtype" is NULL HINT: this option should not be NULL select * from plpgsql_check_function('tf_f1', anyrangetype := null); ERROR: the option "anyrangetype" is NULL HINT: this option should not be NULL select * from plpgsql_check_function('tf_f1', anycompatibletype := null); ERROR: the option "anycompatibletype" is NULL HINT: this option should not be NULL select * from plpgsql_check_function('tf_f1', anycompatiblerangetype := null); ERROR: the option "anycompatiblerangetype" is NULL HINT: this option should not be NULL select * from plpgsql_check_function('tf_f1', without_warnings := null); ERROR: the option "without_warnings" is NULL HINT: this option should not be NULL select * from plpgsql_check_function('tf_f1', all_warnings := null); ERROR: the option "all_warnings" is NULL HINT: this option should not be NULL select * from plpgsql_check_function('tf_f1', use_incomment_options := null); ERROR: the option "use_incomment_options" is NULL HINT: this option should not be NULL select * from plpgsql_check_function('tf_f1', incomment_options_usage_warning := null); ERROR: the option "incomment_options_usage_warning" is NULL HINT: this option should not be NULL select * from plpgsql_check_function('tf_f1', constant_tracing := null); ERROR: the option "constants_tracing" is NULL HINT: this option should not be NULL -- the two shortcuts of the set of enabled warnings are exclusive select * from plpgsql_check_function('tf_f1', without_warnings := true, all_warnings := true); ERROR: without_warnings and all_warnings cannot be true same time -- the names of the transition tables can be used only for a trigger -- function, which requires the relation select * from plpgsql_check_function('tf_f1', oldtable := 'o'); ERROR: missing description of oldtable or newtable HINT: Parameter relid is a empty. select * from plpgsql_check_function('tf_f1', newtable := 'n'); ERROR: missing description of oldtable or newtable HINT: Parameter relid is a empty. -- the list of the pragmas can contain a NULL, such entry is ignored select * from plpgsql_check_function('tf_f1', pragmas := array[null, 'echo:from a pragma']::text[]); NOTICE: from a pragma plpgsql_check_function ------------------------ (0 rows) -- -- plpgsql_check_function_tb -- select * from plpgsql_check_function_tb(null::regprocedure); ERROR: the option "funcoid" is NULL HINT: this option should not be NULL select * from plpgsql_check_function_tb(null::text); ERROR: the option "name" is NULL HINT: this option should not be NULL select * from plpgsql_check_function_tb('tf_f1', relid := null); ERROR: the option "relid" is NULL HINT: this option should not be NULL select * from plpgsql_check_function_tb('tf_f1', fatal_errors := null); ERROR: the option "fatal_errors" is NULL HINT: this option should not be NULL select * from plpgsql_check_function_tb('tf_f1', other_warnings := null); ERROR: the option "other_warnings" is NULL HINT: this option should not be NULL select * from plpgsql_check_function_tb('tf_f1', performance_warnings := null); ERROR: the option "performance_warnings" is NULL HINT: this option should not be NULL select * from plpgsql_check_function_tb('tf_f1', extra_warnings := null); ERROR: the option "extra_warnings" is NULL HINT: this option should not be NULL select * from plpgsql_check_function_tb('tf_f1', security_warnings := null); ERROR: the option "security_warnings" is NULL HINT: this option should not be NULL select * from plpgsql_check_function_tb('tf_f1', compatibility_warnings := null); ERROR: the option "compatibility_warnings" is NULL HINT: this option should not be NULL select * from plpgsql_check_function_tb('tf_f1', anyelememttype := null); ERROR: the option "anyelementtype" is NULL HINT: this option should not be NULL select * from plpgsql_check_function_tb('tf_f1', anyenumtype := null); ERROR: the option "anyenumtype" is NULL HINT: this option should not be NULL select * from plpgsql_check_function_tb('tf_f1', anyrangetype := null); ERROR: the option "anyrangetype" is NULL HINT: this option should not be NULL select * from plpgsql_check_function_tb('tf_f1', anycompatibletype := null); ERROR: the option "anycompatibletype" is NULL HINT: this option should not be NULL select * from plpgsql_check_function_tb('tf_f1', anycompatiblerangetype := null); ERROR: the option "anycompatiblerangetype" is NULL HINT: this option should not be NULL select * from plpgsql_check_function_tb('tf_f1', without_warnings := null); ERROR: the option "without_warnings" is NULL HINT: this option should not be NULL select * from plpgsql_check_function_tb('tf_f1', all_warnings := null); ERROR: the option "all_warnings" is NULL HINT: this option should not be NULL select * from plpgsql_check_function_tb('tf_f1', use_incomment_options := null); ERROR: the option "use_incomment_options" is NULL HINT: this option should not be NULL select * from plpgsql_check_function_tb('tf_f1', incomment_options_usage_warning := null); ERROR: the option "incomment_options_usage_warning" is NULL HINT: this option should not be NULL select * from plpgsql_check_function_tb('tf_f1', constant_tracing := null); ERROR: the option "constants_tracing" is NULL HINT: this option should not be NULL select * from plpgsql_check_function_tb('tf_f1', without_warnings := true, all_warnings := true); ERROR: without_warnings and all_warnings cannot be true same time select * from plpgsql_check_function_tb('tf_f1', oldtable := 'o'); ERROR: missing description of oldtable or newtable HINT: Parameter relid is a empty. select * from plpgsql_check_function_tb('tf_f1', newtable := 'n'); ERROR: missing description of oldtable or newtable HINT: Parameter relid is a empty. -- all_warnings enables every group of the warnings, without_warnings -- disables them; the difference is visible on a function with an unused -- variable create function tf_f2() returns void as $$ declare v int; begin v := 1; end; $$ language plpgsql; select * from plpgsql_check_function('tf_f2', all_warnings := true); plpgsql_check_function ----------------------------------------------------------------------------------------- warning extra:00000:3:DECLARE:never read variable "v" performance:00000:routine is marked as VOLATILE, should be IMMUTABLE Hint: When you fix this issue, please, recheck other functions that uses this function. (3 rows) select * from plpgsql_check_function('tf_f2', without_warnings := true); plpgsql_check_function ------------------------ (0 rows) select level, message from plpgsql_check_function_tb('tf_f2', all_warnings := true); level | message ---------------+---------------------------------------------------- warning extra | never read variable "v" performance | routine is marked as VOLATILE, should be IMMUTABLE (2 rows) select level, message from plpgsql_check_function_tb('tf_f2', without_warnings := true); level | message -------+--------- (0 rows) -- declaration locations must agree in tabular, JSON and XML output create function tf_declarations() returns void as $$ declare unused_value int; written_value int; begin written_value := 1; end; $$ language plpgsql; select lineno, statement, message from plpgsql_check_function_tb('tf_declarations'); lineno | statement | message --------+-----------+------------------------------------- 3 | DECLARE | unused variable "unused_value" 4 | DECLARE | never read variable "written_value" (2 rows) select issue->'statement'->>'lineNumber' as lineno, issue->'statement'->>'text' as statement, issue->>'message' as message from plpgsql_check_function('tf_declarations', format => 'json') as r(doc), lateral jsonb_array_elements(doc::jsonb->'issues') as i(issue); lineno | statement | message --------+-----------+------------------------------------- 3 | DECLARE | unused variable "unused_value" 4 | DECLARE | never read variable "written_value" (2 rows) select xpath('/Function/Issue/Stmt/@lineno', doc::xml) as linenos, xpath('/Function/Issue/Stmt/text()', doc::xml) as statements from plpgsql_check_function('tf_declarations', format => 'xml') as r(doc); linenos | statements ---------+------------------- {3,4} | {DECLARE,DECLARE} (1 row) drop function tf_declarations(); -- -- plpgsql_show_dependency_tb -- select * from plpgsql_show_dependency_tb(null::regprocedure); ERROR: the option "funcoid" is NULL HINT: this option should not be NULL CONTEXT: SQL function "plpgsql_show_dependency_tb" statement 1 select * from plpgsql_show_dependency_tb(null::text); ERROR: the option "name" is NULL HINT: this option should not be NULL CONTEXT: SQL function "plpgsql_show_dependency_tb" statement 1 select * from plpgsql_show_dependency_tb('tf_f1', relid := null); ERROR: the option "relid" is NULL HINT: this option should not be NULL CONTEXT: SQL function "plpgsql_show_dependency_tb" statement 1 select * from plpgsql_show_dependency_tb('tf_f1', anyelememttype := null); ERROR: the option "anyelementtype" is NULL HINT: this option should not be NULL CONTEXT: SQL function "plpgsql_show_dependency_tb" statement 1 select * from plpgsql_show_dependency_tb('tf_f1', anyenumtype := null); ERROR: the option "anyenumtype" is NULL HINT: this option should not be NULL CONTEXT: SQL function "plpgsql_show_dependency_tb" statement 1 select * from plpgsql_show_dependency_tb('tf_f1', anyrangetype := null); ERROR: the option "anyrangetype" is NULL HINT: this option should not be NULL CONTEXT: SQL function "plpgsql_show_dependency_tb" statement 1 select * from plpgsql_show_dependency_tb('tf_f1', anycompatibletype := null); ERROR: the option "anycompatibletype" is NULL HINT: this option should not be NULL CONTEXT: SQL function "plpgsql_show_dependency_tb" statement 1 select * from plpgsql_show_dependency_tb('tf_f1', anycompatiblerangetype := null); ERROR: the option "anycompatiblerangetype" is NULL HINT: this option should not be NULL CONTEXT: SQL function "plpgsql_show_dependency_tb" statement 1 create function tf_f3() returns void as $$ begin insert into tf_t1 values(1, 2); perform tf_f1(1); end; $$ language plpgsql; select type, schema, name, params from plpgsql_show_dependency_tb('tf_f3'); type | schema | name | params ----------+--------+-------+----------- FUNCTION | public | tf_f1 | (integer) RELATION | public | tf_t1 | (2 rows) select type, schema, name, params from plpgsql_show_dependency_tb('tf_f3()'::regprocedure); type | schema | name | params ----------+--------+-------+----------- FUNCTION | public | tf_f1 | (integer) RELATION | public | tf_t1 | (2 rows) -- -- plpgsql_make_pragma -- select * from plpgsql_make_pragma(null::regprocedure); ERROR: the option "funcoid" is NULL HINT: this option should not be NULL select * from plpgsql_make_pragma('tf_f1(int)', relid := null); ERROR: the option "relid" is NULL HINT: this option should not be NULL select * from plpgsql_make_pragma('tf_f1(int)', fatal_errors := null); ERROR: the option "fatal_errors" is NULL HINT: this option should not be NULL create function tf_f4() returns void as $$ declare r record; begin create temp table tf_tmp as select 1 as a, 2 as b; select * from tf_tmp into r; raise notice '%', r.a; end; $$ language plpgsql; select * from plpgsql_make_pragma('tf_f4()'); plpgsql_make_pragma ------------------------------------- table: tf_tmp(a integer, b integer) (1 row) drop function tf_f4(); drop function tf_f3(); drop function tf_f2(); drop function tf_f1(int); drop table tf_t1; okbob-plpgsql_check-800db79/expected/plpgsql_check_tracer.out000066400000000000000000000350231526744060500245070ustar00rootroot00000000000000set client_min_messages to warning; create extension if not exists plpgsql_check; set client_min_messages to notice; -- -- Tests of the tracer. -- -- The tracer prints the entered and left functions and statements, the -- arguments of the expressions and the content of the variables. The -- test mode replaces the measured time and the oid of the function by -- constants, so the output is stable. -- -- the tracer can be activated by anybody, but it does nothing until a -- superuser enables it select plpgsql_check_tracer(true); NOTICE: tracer is active NOTICE: tracer verbosity is default NOTICE: tracer is still blocked DETAIL: The tracer should be enabled by the superuser for security reasons. HINT: Execute "set plpgsql_check.enable_tracer to on" (superuser only). plpgsql_check_tracer ---------------------- t (1 row) set plpgsql_check.enable_tracer to on; set plpgsql_check.tracer_test_mode to true; -- the control function sets the state and the verbosity and reports both select plpgsql_check_tracer(false); NOTICE: tracer is not active NOTICE: tracer verbosity is default plpgsql_check_tracer ---------------------- f (1 row) select plpgsql_check_tracer(); NOTICE: tracer is not active NOTICE: tracer verbosity is default plpgsql_check_tracer ---------------------- f (1 row) select plpgsql_check_tracer(null, 'terse'); NOTICE: tracer is not active NOTICE: tracer verbosity is terse plpgsql_check_tracer ---------------------- f (1 row) create table tr_t1(a int, b int); create function tr_f1(a int, t text) returns int as $$ declare b int; begin b := a * 2; if b > 100 then b := 100; end if; raise notice 'tr_f1: % %', b, t; return b; end; $$ language plpgsql; -- the terse verbosity prints one short line per function and statement select plpgsql_check_tracer(true, 'terse'); NOTICE: tracer is active NOTICE: tracer verbosity is terse plpgsql_check_tracer ---------------------- t (1 row) select tr_f1(10, 'terse'); NOTICE: #0 start of tr_f1 (oid=0, tnl=1) NOTICE: tr_f1: 20 terse NOTICE: #0 end of tr_f1 tr_f1 ------- 20 (1 row) -- the default verbosity prints the frame numbers and the context select plpgsql_check_tracer(true, 'default'); NOTICE: tracer is active NOTICE: tracer verbosity is default plpgsql_check_tracer ---------------------- t (1 row) select tr_f1(10, 'default'); NOTICE: #0 ->> start of function tr_f1(integer,text) (oid=0, tnl=1) NOTICE: #0 "a" => '10', "t" => 'default' NOTICE: tr_f1: 20 default NOTICE: #0 <<- end of function tr_f1 (elapsed time=0.010 ms) tr_f1 ------- 20 (1 row) -- the verbose verbosity prints the arguments of the function and the -- arguments of every traced expression select plpgsql_check_tracer(true, 'verbose'); NOTICE: tracer is active NOTICE: tracer verbosity is verbose plpgsql_check_tracer ---------------------- t (1 row) select tr_f1(10, 'verbose'); NOTICE: #0 ->> start of function tr_f1(integer,text) (oid=0, tnl=1) NOTICE: #0 "a" => '10', "t" => 'verbose' NOTICE: #0.1 3 --> start of statement block (tnl=1) NOTICE: #0.2 4 --> start of assignment b := a * 2 (tnl=1) NOTICE: #0.2 "a" => '10', "b" => null NOTICE: #0.2 <-- end of assignment (elapsed time=0.010 ms) NOTICE: #0.2 "b" => '20' NOTICE: #0.3 5 --> start of IF (cond='b > 100') (tnl=1) NOTICE: #0.3 "b" => '20' NOTICE: #0.3 <-- end of IF (elapsed time=0.010 ms) NOTICE: #0.5 8 --> start of RAISE (tnl=1) NOTICE: tr_f1: 20 verbose NOTICE: #0.5 <-- end of RAISE (elapsed time=0.010 ms) NOTICE: #0.6 9 --> start of RETURN (tnl=1) NOTICE: #0.6 "b" => '20' NOTICE: #0.6 <-- end of RETURN (elapsed time=0.010 ms) NOTICE: #0.1 <-- end of statement block (elapsed time=0.010 ms) NOTICE: #0 <<- end of function tr_f1 (elapsed time=0.010 ms) tr_f1 ------- 20 (1 row) -- the number of the subtransactions can be printed too set plpgsql_check.tracer_show_nsubxids to on; select tr_f1(1, 'nsubxids'); NOTICE: #0 ->> start of function tr_f1(integer,text) (oid=0, tnl=1, nxids=0) NOTICE: #0 "a" => '1', "t" => 'nsubxids' NOTICE: #0.1 3 --> start of statement block (tnl=1, nxids=0) NOTICE: #0.2 4 --> start of assignment b := a * 2 (tnl=1, nxids=0) NOTICE: #0.2 "a" => '1', "b" => null NOTICE: #0.2 <-- end of assignment (elapsed time=0.010 ms) NOTICE: #0.2 "b" => '2' NOTICE: #0.3 5 --> start of IF (cond='b > 100') (tnl=1, nxids=0) NOTICE: #0.3 "b" => '2' NOTICE: #0.3 <-- end of IF (elapsed time=0.010 ms) NOTICE: #0.5 8 --> start of RAISE (tnl=1, nxids=0) NOTICE: tr_f1: 2 nsubxids NOTICE: #0.5 <-- end of RAISE (elapsed time=0.010 ms) NOTICE: #0.6 9 --> start of RETURN (tnl=1, nxids=0) NOTICE: #0.6 "b" => '2' NOTICE: #0.6 <-- end of RETURN (elapsed time=0.010 ms) NOTICE: #0.1 <-- end of statement block (elapsed time=0.010 ms) NOTICE: #0 <<- end of function tr_f1 (elapsed time=0.010 ms) tr_f1 ------- 2 (1 row) set plpgsql_check.tracer_show_nsubxids to off; -- an IF statement with the ELSIF parts prints the condition of every part create function tr_f2(a int) returns text as $$ begin if a < 0 then return 'negative'; elsif a = 0 then return 'zero'; elsif a < 10 then return 'small'; else return 'big'; end if; end; $$ language plpgsql; select tr_f2(5); NOTICE: #0 ->> start of function tr_f2(integer) (oid=0, tnl=1) NOTICE: #0 "a" => '5' NOTICE: #0.1 2 --> start of statement block (tnl=1) NOTICE: #0.2 3 --> start of IF (cond='a < 0') (tnl=1) NOTICE: #0.2 "a" => '5' NOTICE: #0.2 5 ELSEIF (expr='a = 0') NOTICE: #0.2 "a" => '5' NOTICE: #0.2 7 ELSEIF (expr='a < 10') NOTICE: #0.2 "a" => '5' NOTICE: #0.5 8 --> start of RETURN (expr=''small'') (tnl=1) NOTICE: #0.5 <-- end of RETURN (elapsed time=0.010 ms) NOTICE: #0.2 <-- end of IF (elapsed time=0.010 ms) NOTICE: #0.1 <-- end of statement block (elapsed time=0.010 ms) NOTICE: #0 <<- end of function tr_f2 (elapsed time=0.010 ms) tr_f2 ------- small (1 row) -- When the assert fails, the tracer prints the content of all the -- variables of the function. The variables of the types which cannot be -- converted to a string are skipped, a value containing a newline is -- printed on a separate row, and a long value is trimmed. create function tr_f3() returns void as $$ declare r record; v tr_t1; a int[]; n int; t text; m text; c refcursor := 'tr_cursor'; begin a := array[1, 2, 3]; t := 'a longer text used for the trimming'; m := 'first line' || chr(10) || 'second line'; select 1 as x, 2 as y into r; v := (1, 2); open c for select * from tr_t1; assert n is not null, 'n should not be null'; close c; end; $$ language plpgsql; -- The ASSERT statement is traced separately and has its own verbosity. -- With the verbose verbosity it prints all the variables of the -- function. The tracer itself is switched to the terse verbosity here, -- so only the output of the traced assert is interesting. set plpgsql_check.trace_assert to on; set plpgsql_check.trace_assert_verbosity to verbose; select plpgsql_check_tracer(true, 'terse'); NOTICE: tracer is active NOTICE: tracer verbosity is terse plpgsql_check_tracer ---------------------- t (1 row) -- the exception raised by the failed assert is caught, so the output of -- the tracer and the message of the exception are not mixed do $$ begin perform tr_f3(); exception when others then raise notice 'catched: %', sqlerrm; end; $$; NOTICE: #0 start of inline code block (oid=0, tnl=1) NOTICE: #2 start of tr_f3 (oid=0, tnl=2) NOTICE: #2 PLpgSQL assert expression (n is not null) on line 17 of tr_f3() is false NOTICE: "r" => '(1,2)', "v" => '(1,2)', "a" => '{1,2,3}', "n" => null, "t" => 'a longer text used for the trimming' NOTICE: "m" => 'first line second line' NOTICE: "c" => 'tr_cursor' NOTICE: #0 PL/pgSQL function inline_code_block line 3 at PERFORM NOTICE: "sqlstate" => null, "sqlerrm" => null NOTICE: #2 end of tr_f3 aborted NOTICE: #0 end of inline code block aborted ERROR: n should not be null CONTEXT: PL/pgSQL function tr_f3() line 17 at ASSERT SQL statement "SELECT tr_f3()" PL/pgSQL function inline_code_block line 3 at PERFORM -- the content of a long variable is trimmed set plpgsql_check.tracer_variable_max_length to 10; do $$ begin perform tr_f3(); exception when others then raise notice 'catched: %', sqlerrm; end; $$; NOTICE: #0 start of inline code block (oid=0, tnl=1) NOTICE: #2 start of tr_f3 (oid=0, tnl=2) NOTICE: #2 PLpgSQL assert expression (n is not null) on line 17 of tr_f3() is false NOTICE: "r" => '(1,2)' NOTICE: "v" => '(1,2)' NOTICE: "a" => '{1,2,3}' NOTICE: "n" => null NOTICE: "t" => 'a longer t' NOTICE: "m" => 'first line' NOTICE: "c" => 'tr_cursor' NOTICE: #0 PL/pgSQL function inline_code_block line 3 at PERFORM NOTICE: "sqlstate" => null NOTICE: "sqlerrm" => null NOTICE: #2 end of tr_f3 aborted NOTICE: #0 end of inline code block aborted ERROR: n should not be null CONTEXT: PL/pgSQL function tr_f3() line 17 at ASSERT SQL statement "SELECT tr_f3()" PL/pgSQL function inline_code_block line 3 at PERFORM set plpgsql_check.tracer_variable_max_length to default; -- a failed assert is traced too create function tr_f4(a int) returns void as $$ begin assert a > 0, 'a should be positive'; end; $$ language plpgsql; select tr_f4(1); NOTICE: #0 start of tr_f4 (oid=0, tnl=1) NOTICE: PLpgSQL assert expression (a > 0) on line 3 of tr_f4(integer) is true NOTICE: "a" => '1' NOTICE: #0 end of tr_f4 tr_f4 ------- (1 row) do $$ begin perform tr_f4(-1); exception when others then raise notice 'catched: %', sqlerrm; end; $$; NOTICE: #0 start of inline code block (oid=0, tnl=1) NOTICE: #2 start of tr_f4 (oid=0, tnl=2) NOTICE: #2 PLpgSQL assert expression (a > 0) on line 3 of tr_f4(integer) is false NOTICE: "a" => '-1' NOTICE: #0 PL/pgSQL function inline_code_block line 3 at PERFORM NOTICE: "sqlstate" => null, "sqlerrm" => null NOTICE: #2 end of tr_f4 aborted NOTICE: #0 end of inline code block aborted ERROR: a should be positive CONTEXT: PL/pgSQL function tr_f4(integer) line 3 at ASSERT SQL statement "SELECT tr_f4(-1)" PL/pgSQL function inline_code_block line 3 at PERFORM set plpgsql_check.trace_assert_verbosity to terse; do $$ begin perform tr_f4(-1); exception when others then raise notice 'catched: %', sqlerrm; end; $$; NOTICE: #0 start of inline code block (oid=0, tnl=1) NOTICE: #2 start of tr_f4 (oid=0, tnl=2) NOTICE: #2 PLpgSQL assert expression (a > 0) on line 3 of tr_f4(integer) is false NOTICE: "a" => '-1' NOTICE: #2 end of tr_f4 aborted NOTICE: #0 end of inline code block aborted ERROR: a should be positive CONTEXT: PL/pgSQL function tr_f4(integer) line 3 at ASSERT SQL statement "SELECT tr_f4(-1)" PL/pgSQL function inline_code_block line 3 at PERFORM set plpgsql_check.trace_assert to off; set plpgsql_check.trace_assert_verbosity to default; -- the tracer prints the type of the fired trigger and the transition -- records create function tr_trg_row() returns trigger as $$ begin if TG_OP = 'DELETE' then return old; end if; return new; end; $$ language plpgsql; create function tr_trg_stmt() returns trigger as $$ begin return null; end; $$ language plpgsql; create trigger tr_t1_row before insert or update or delete on tr_t1 for each row execute procedure tr_trg_row(); create trigger tr_t1_trunc before truncate on tr_t1 for each statement execute procedure tr_trg_stmt(); select plpgsql_check_tracer(true, 'default'); NOTICE: tracer is active NOTICE: tracer verbosity is default plpgsql_check_tracer ---------------------- t (1 row) insert into tr_t1 values(1, 2); NOTICE: #0 ->> start of function tr_trg_row() (oid=0, tnl=1) NOTICE: #0 triggered by before row insert trigger NOTICE: #0 "new" => '(1,2)' NOTICE: #0 <<- end of function tr_trg_row (elapsed time=0.010 ms) update tr_t1 set b = 3; NOTICE: #0 ->> start of function tr_trg_row() (oid=0, tnl=1) NOTICE: #0 triggered by before row update trigger NOTICE: #0 "new" => '(1,3)' NOTICE: #0 "old" => '(1,2)' NOTICE: #0 <<- end of function tr_trg_row (elapsed time=0.010 ms) delete from tr_t1; NOTICE: #0 ->> start of function tr_trg_row() (oid=0, tnl=1) NOTICE: #0 triggered by before row delete trigger NOTICE: #0 "old" => '(1,3)' NOTICE: #0 <<- end of function tr_trg_row (elapsed time=0.010 ms) insert into tr_t1 values(1, 2); NOTICE: #0 ->> start of function tr_trg_row() (oid=0, tnl=1) NOTICE: #0 triggered by before row insert trigger NOTICE: #0 "new" => '(1,2)' NOTICE: #0 <<- end of function tr_trg_row (elapsed time=0.010 ms) truncate tr_t1; NOTICE: #0 ->> start of function tr_trg_stmt() (oid=0, tnl=1) NOTICE: #0 triggered by before statement trigger NOTICE: #0 "new" => null NOTICE: #0 "old" => null NOTICE: #0 <<- end of function tr_trg_stmt (elapsed time=0.010 ms) select plpgsql_check_tracer(false); NOTICE: tracer is not active NOTICE: tracer verbosity is default plpgsql_check_tracer ---------------------- f (1 row) -- ASSERT tracing is independent of both general tracing and runtime asserts. create sequence tr_assert_calls; create function tr_assert_only() returns void as $$ begin assert nextval('tr_assert_calls') > 0; end; $$ language plpgsql; set plpgsql.check_asserts to off; set plpgsql_check.trace_assert to on; select tr_assert_only(); NOTICE: PLpgSQL assert expression (nextval('tr_assert_calls') > 0) on line 3 of tr_assert_only() is true tr_assert_only ---------------- (1 row) select last_value, is_called from tr_assert_calls; last_value | is_called ------------+----------- 1 | t (1 row) set plpgsql_check.trace_assert to off; set plpgsql.check_asserts to default; drop function tr_assert_only(); drop sequence tr_assert_calls; set plpgsql_check.enable_tracer to off; set plpgsql_check.tracer_test_mode to false; set plpgsql_check.tracer_verbosity to default; drop trigger tr_t1_trunc on tr_t1; drop trigger tr_t1_row on tr_t1; drop function tr_trg_stmt(); drop function tr_trg_row(); drop function tr_f4(int); drop function tr_f3(); drop function tr_f2(int); drop function tr_f1(int, text); drop table tr_t1; okbob-plpgsql_check-800db79/expected/plpgsql_pragma_generator.out000066400000000000000000001070571526744060500254160ustar00rootroot00000000000000load 'plpgsql'; set client_min_messages to warning; create extension if not exists plpgsql_check; set client_min_messages to notice; set plpgsql_check.regress_test_mode = true; -- -- tests of plpgsql_make_pragma and "pragmas" option -- of check functions -- create table gtp_src(a int, b text); create type gtp_ct as (x int, y text); create domain gtp_dom as int check (value > 0); -- happy path - CREATE TEMP TABLE AS SELECT create function gtp_f1() returns void as $$ begin create temp table gtp_t1 as select a, b from gtp_src; insert into gtp_t1 values (10, 'hello'); end; $$ language plpgsql; select * from plpgsql_make_pragma('gtp_f1()'); plpgsql_make_pragma ---------------------------------- table: gtp_t1(a integer, b text) (1 row) -- without pragmas the check should detect missing relation select * from plpgsql_check_function('gtp_f1()'); plpgsql_check_function -------------------------------------------------------------- error:42P01:4:SQL statement:relation "gtp_t1" does not exist Query: insert into gtp_t1 values (10, 'hello') -- ^ (3 rows) -- with generated pragmas the check should be ok select * from plpgsql_check_function('gtp_f1()', pragmas => array(select plpgsql_make_pragma('gtp_f1()'))); plpgsql_check_function ------------------------ (0 rows) -- tabular form select * from plpgsql_check_function_tb('gtp_f1()', pragmas => array(select plpgsql_make_pragma('gtp_f1()'))); functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context ------------+--------+-----------+----------+---------+--------+------+-------+----------+-------+--------- (0 rows) -- generator is idempotent - repeated call returns same result, -- and doesn't leave any relation in system catalogue select * from plpgsql_make_pragma('gtp_f1()'); plpgsql_make_pragma ---------------------------------- table: gtp_t1(a integer, b text) (1 row) select count(*) from pg_class where relname in ('gtp_t1'); count ------- 0 (1 row) -- happy path - one and two temporary tables created by AS VALUES and AS TABLE create function gtp_f2() returns void as $$ begin create temp table gtp_v1 as values (1, 'x'), (2, 'y'); end; $$ language plpgsql; select * from plpgsql_make_pragma('gtp_f2()'); plpgsql_make_pragma ---------------------------------------------- table: gtp_v1(column1 integer, column2 text) (1 row) create function gtp_f3() returns void as $$ begin create temp table gtp_tt1 as table gtp_src; end; $$ language plpgsql; select * from plpgsql_make_pragma('gtp_f3()'); plpgsql_make_pragma ----------------------------------- table: gtp_tt1(a integer, b text) (1 row) create function gtp_f4() returns void as $$ begin create temp table gtp_v1 as values (1, 'x'), (2, 'y'); create temp table gtp_tt1 as table gtp_src; end; $$ language plpgsql; select * from plpgsql_make_pragma('gtp_f4()'); plpgsql_make_pragma ---------------------------------------------- table: gtp_v1(column1 integer, column2 text) table: gtp_tt1(a integer, b text) (2 rows) drop function gtp_f2(); drop function gtp_f3(); drop function gtp_f4(); -- WITH NO DATA, IF NOT EXISTS and ON COMMIT variants create function gtp_f5() returns void as $$ begin create temp table gtp_nd as select a from gtp_src with no data; create temp table if not exists gtp_ine as select b from gtp_src; create temp table gtp_oc1 on commit preserve rows as select a from gtp_src; create temp table gtp_oc2 on commit delete rows as select a from gtp_src; create temp table gtp_oc3 on commit drop as select a from gtp_src; end; $$ language plpgsql; select * from plpgsql_make_pragma('gtp_f5()'); plpgsql_make_pragma --------------------------- table: gtp_nd(a integer) table: gtp_ine(b text) table: gtp_oc1(a integer) table: gtp_oc2(a integer) table: gtp_oc3(a integer) (5 rows) drop function gtp_f5(); -- explicitly entered column names create function gtp_f6() returns void as $$ begin create temp table gtp_cn(c1, c2) as select a, b from gtp_src; end; $$ language plpgsql; select * from plpgsql_make_pragma('gtp_f6()'); plpgsql_make_pragma ------------------------------------ table: gtp_cn(c1 integer, c2 text) (1 row) drop function gtp_f6(); -- star, join, subquery, CTE and recursive CTE create function gtp_f7() returns void as $$ begin create temp table gtp_star as select * from gtp_src; create temp table gtp_join as select s1.a, s2.b from gtp_src s1 join gtp_src s2 on s1.a = s2.a; create temp table gtp_sub as select a from gtp_src where a in (select a from gtp_src); create temp table gtp_cte as with x as (select a, b from gtp_src) select * from x; create temp table gtp_rcte as with recursive r(n) as (values (1) union all select n + 1 from r where n < 10) select n from r; end; $$ language plpgsql; select * from plpgsql_make_pragma('gtp_f7()'); plpgsql_make_pragma ------------------------------------ table: gtp_star(a integer, b text) table: gtp_join(a integer, b text) table: gtp_sub(a integer) table: gtp_cte(a integer, b text) table: gtp_rcte(n integer) (5 rows) drop function gtp_f7(); -- computed columns without aliases have name "?column?". Generated -- pragma returns these names without any change (the pragma is not -- applicable, and self registration fails with warning like CREATE -- TABLE AS would fail). create function gtp_f8() returns void as $$ begin create temp table gtp_comp as select a + 1, a + 1 from gtp_src; end; $$ language plpgsql; select * from plpgsql_make_pragma('gtp_f8()'); WARNING: Pragma "table" on line 3 is not processed. DETAIL: column "?column?" specified more than once plpgsql_make_pragma --------------------------------------------------------- table: gtp_comp("?column?" integer, "?column?" integer) (1 row) drop function gtp_f8(); -- unknown type is replaced by text like CREATE TABLE AS does create function gtp_f9() returns void as $$ begin create temp table gtp_unk as select null as x; end; $$ language plpgsql; select * from plpgsql_make_pragma('gtp_f9()'); plpgsql_make_pragma ------------------------ table: gtp_unk(x text) (1 row) drop function gtp_f9(); -- array, composite, domain types and typmods create function gtp_f10() returns void as $$ begin create temp table gtp_types as select array[1,2] as arr, (1, 'x')::gtp_ct as comp, 10::gtp_dom as dom, 'abc'::varchar(10) as vc, 3.14::numeric(12,2) as num; end; $$ language plpgsql; select * from plpgsql_make_pragma('gtp_f10()'); plpgsql_make_pragma -------------------------------------------------------------------------------------------------------- table: gtp_types(arr integer[], comp gtp_ct, dom gtp_dom, vc character varying(10), num numeric(12,2)) (1 row) drop function gtp_f10(); -- generated datetime and interval type spellings must be usable as pragmas create function gtp_datetime() returns void as $$ begin create temp table gtp_datetime1 as select timestamp(3) '2026-01-01 12:34:56.789' as ts, time(2) with time zone '12:34:56.78+00' as tm; create temp table gtp_datetime2 (elapsed interval day to second(2), items int[2][3]); perform ts, tm, elapsed, items from gtp_datetime1, gtp_datetime2; end; $$ language plpgsql; select * from plpgsql_make_pragma('gtp_datetime()'); plpgsql_make_pragma ------------------------------------------------------------------------------------ table: gtp_datetime1(ts timestamp(3) without time zone, tm time(2) with time zone) table: gtp_datetime2(elapsed interval day to second(2), items integer[]) (2 rows) select * from plpgsql_check_function('gtp_datetime()', pragmas => array(select plpgsql_make_pragma('gtp_datetime()'))); plpgsql_check_function ------------------------ (0 rows) drop function gtp_datetime(); -- PostgreSQL tables and anonymous record types can have no attributes. create function gtp_empty() returns void as $$ begin create temp table gtp_empty1(); create temp table gtp_empty2 as select from gtp_src; insert into gtp_empty1 default values; insert into gtp_empty2 default values; end; $$ language plpgsql; select * from plpgsql_make_pragma('gtp_empty()'); plpgsql_make_pragma --------------------- table: gtp_empty1() table: gtp_empty2() (2 rows) select * from plpgsql_check_function('gtp_empty()', pragmas => array(select plpgsql_make_pragma('gtp_empty()'))); plpgsql_check_function ------------------------ (0 rows) drop function gtp_empty(); create function gtp_empty_record() returns void as $$ declare r record; begin perform plpgsql_check_pragma('type: r ()'); perform r; end; $$ language plpgsql; select * from plpgsql_check_function('gtp_empty_record()'); plpgsql_check_function ------------------------ (0 rows) drop function gtp_empty_record(); -- Empty lists must not make missing columns or trailing commas legal. create function gtp_empty_invalid() returns void as $$ begin perform plpgsql_check_pragma('table: gtp_bad1(,)'); perform plpgsql_check_pragma('table: gtp_bad2(a int,)'); perform plpgsql_check_pragma('table: gtp_bad3('); end; $$ language plpgsql; select * from plpgsql_check_function('gtp_empty_invalid()'); WARNING: Pragma "table" on line 3 is not processed. DETAIL: Syntax error (expected identifier) WARNING: Pragma "table" on line 4 is not processed. DETAIL: Syntax error (expected identifier) WARNING: Pragma "table" on line 5 is not processed. DETAIL: Syntax error (expected identifier) plpgsql_check_function ------------------------ (0 rows) drop function gtp_empty_invalid(); -- identifiers that require quoting create function gtp_f11() returns void as $$ begin create temp table "MyT" as select 1 as "select", 2 as normal; end; $$ language plpgsql; select * from plpgsql_make_pragma('gtp_f11()'); plpgsql_make_pragma ------------------------------------------------ table: "MyT"("select" integer, normal integer) (1 row) drop function gtp_f11(); -- resjunk columns (ORDER BY) should not leak to generated pragma create function gtp_f12() returns void as $$ begin create temp table gtp_junk as select a from gtp_src order by b; end; $$ language plpgsql; select * from plpgsql_make_pragma('gtp_f12()'); plpgsql_make_pragma ---------------------------- table: gtp_junk(a integer) (1 row) drop function gtp_f12(); -- traversing of nested statements - IF/ELSE branches, loops, nested -- blocks, exception handlers and unreachable code create function gtp_f13() returns void as $$ declare i int; begin if random() > 0.5 then create temp table gtp_n1 as select 1 as x; else create temp table gtp_n2 as select 2 as y; end if; for i in 1..2 loop create temp table gtp_n3 as select 3 as z; end loop; declare v int; begin create temp table gtp_n4 as select 4 as w; exception when others then create temp table gtp_n5 as select 5 as e; end; return; create temp table gtp_n6 as select 6 as u; end; $$ language plpgsql; select * from plpgsql_make_pragma('gtp_f13()'); plpgsql_make_pragma -------------------------- table: gtp_n1(x integer) table: gtp_n2(y integer) table: gtp_n3(z integer) table: gtp_n4(w integer) table: gtp_n5(e integer) table: gtp_n6(u integer) (6 rows) drop function gtp_f13(); -- positional processing - same table name twice (no deduplication), -- the following statements see the last definition create function gtp_f14() returns void as $$ begin create temp table gtp_dup as select 1 as x; drop table gtp_dup; create temp table gtp_dup as select 'a'::text as y; create temp table gtp_dup2 as select * from gtp_dup; end; $$ language plpgsql; select * from plpgsql_make_pragma('gtp_f14()'); WARNING: relation "gtp_dup" already exists DETAIL: The temporary table is replaced by the new definition. plpgsql_make_pragma --------------------------- table: gtp_dup(x integer) table: gtp_dup(y text) table: gtp_dup2(y text) (3 rows) drop function gtp_f14(); -- one temporary table references another one create function gtp_f15() returns void as $$ begin create temp table gtp_c1 as select a, b from gtp_src; create temp table gtp_c2 as select * from gtp_c1 where a > 0; create temp table gtp_c3 as select gtp_c1.a, gtp_c2.b from gtp_c1 join gtp_c2 on gtp_c1.a = gtp_c2.a; create temp table gtp_c4 as select a from gtp_src where a in (select a from gtp_c1); end; $$ language plpgsql; select * from plpgsql_make_pragma('gtp_f15()'); plpgsql_make_pragma ---------------------------------- table: gtp_c1(a integer, b text) table: gtp_c2(a integer, b text) table: gtp_c3(a integer, b text) table: gtp_c4(a integer) (4 rows) drop function gtp_f15(); -- temporary table shadows an existing table with same name create table gtp_shadow(t text); create function gtp_f16() returns void as $$ begin create temp table gtp_shadow as select 1 as id; create temp table gtp_from_shadow as select * from gtp_shadow; end; $$ language plpgsql; select * from plpgsql_make_pragma('gtp_f16()'); plpgsql_make_pragma ------------------------------------ table: gtp_shadow(id integer) table: gtp_from_shadow(id integer) (2 rows) drop function gtp_f16(); drop table gtp_shadow; -- explicitly used pg_temp schema create function gtp_f17() returns void as $$ begin create temp table pg_temp.gtp_q1 as select 1 as z; create table pg_temp.gtp_q2 as select 2 as w; end; $$ language plpgsql; select * from plpgsql_make_pragma('gtp_f17()'); plpgsql_make_pragma -------------------------- table: gtp_q1(z integer) table: gtp_q2(w integer) (2 rows) drop function gtp_f17(); -- these forms are ignored - dynamic SQL, not temporary tables and -- materialized views create function gtp_f18() returns void as $$ begin execute 'create temp table gtp_s4 as select 1 as x'; create table gtp_s5 as select a from gtp_src; create unlogged table gtp_s6 as select a from gtp_src; create table gtp_s8 (id int); create unlogged table gtp_s9 (id int); create materialized view gtp_s7 as select a from gtp_src; drop table gtp_s4, gtp_s5, gtp_s6, gtp_s8, gtp_s9; drop materialized view gtp_s7; end; $$ language plpgsql; select * from plpgsql_make_pragma('gtp_f18()'); plpgsql_make_pragma --------------------- (0 rows) drop function gtp_f18(); -- function without temporary tables create function gtp_f19() returns int as $$ begin return (select count(*) from gtp_src); end; $$ language plpgsql; select * from plpgsql_make_pragma('gtp_f19()'); plpgsql_make_pragma --------------------- (0 rows) drop function gtp_f19(); -- A zero-column table produces a usable empty column list. create function gtp_f20() returns void as $$ begin create temp table gtp_zero as select from gtp_src; end; $$ language plpgsql; select * from plpgsql_make_pragma('gtp_f20()'); plpgsql_make_pragma --------------------- table: gtp_zero() (1 row) drop function gtp_f20(); -- CREATE TABLE AS EXECUTE is ignored create function gtp_f21() returns void as $$ begin create temp table gtp_pe as execute some_prepared_stmt; end; $$ language plpgsql; select * from plpgsql_make_pragma('gtp_f21()'); plpgsql_make_pragma --------------------- (0 rows) drop function gtp_f21(); -- inner query can use plpgsql variables create function gtp_f22() returns void as $$ declare v_id int default 10; v_txt text default 'x'; begin create temp table gtp_vars as select v_id as id, v_txt as txt, a from gtp_src where a > v_id; end; $$ language plpgsql; select * from plpgsql_make_pragma('gtp_f22()'); plpgsql_make_pragma -------------------------------------------------- table: gtp_vars(id integer, txt text, a integer) (1 row) drop function gtp_f22(); -- regprocedure argument selects one function from overloaded set, -- function can use OUT arguments create function gtp_over(p int) returns void as $$ begin create temp table gtp_o1 as select p as x; end; $$ language plpgsql; create function gtp_over(p text) returns void as $$ begin create temp table gtp_o2 as select p as y; end; $$ language plpgsql; select * from plpgsql_make_pragma('gtp_over(int)'); plpgsql_make_pragma -------------------------- table: gtp_o1(x integer) (1 row) select * from plpgsql_make_pragma('gtp_over(text)'); plpgsql_make_pragma ----------------------- table: gtp_o2(y text) (1 row) drop function gtp_over(int); drop function gtp_over(text); create function gtp_out(in p int, out o1 int, out o2 text) as $$ begin create temp table gtp_ot as select p + 1 as q; o1 := p; o2 := 'x'; end; $$ language plpgsql; select * from plpgsql_make_pragma('gtp_out(int)'); plpgsql_make_pragma -------------------------- table: gtp_ot(q integer) (1 row) drop function gtp_out(int); -- trigger function requires relid argument create table gtp_trg_tbl(id int, val text); create function gtp_trg() returns trigger as $$ begin create temp table gtp_trg_snapshot as select new.id, new.val; return new; end; $$ language plpgsql; select * from plpgsql_make_pragma('gtp_trg()', 'gtp_trg_tbl'); plpgsql_make_pragma ----------------------------------------------- table: gtp_trg_snapshot(id integer, val text) (1 row) drop function gtp_trg(); drop table gtp_trg_tbl; -- explicitly written pragmas are respected by generator create function gtp_f23() returns void as $$ begin execute 'create temp table gtp_dyn(a int, b text)'; perform plpgsql_check_pragma('table: gtp_dyn(a int, b text)'); create temp table gtp_from_dyn as select * from gtp_dyn; end; $$ language plpgsql; select * from plpgsql_make_pragma('gtp_f23()'); plpgsql_make_pragma ---------------------------------------- table: gtp_from_dyn(a integer, b text) (1 row) drop function gtp_f23(); -- CREATE TABLE statement based forms. The temporary table is created -- inside the check's subtransaction (and removed by its rollback), and -- the pragma is derived from the structure of really created table. -- Column definition list form - typmods, quoting, serial, identity and -- generated columns, constraints, collation (not carried to pragma), -- zero columns create function gtp_cs1() returns void as $$ begin create temp table gtp_cl1 (id int, name text); create temp table gtp_cl2 (v varchar(10), n numeric(12,2), t text collate "C"); create temp table "GtpQ" ("select" int, normal text); create temp table gtp_cl3 (id serial, iid int generated always as identity, g int generated always as (id + 1) stored); create temp table gtp_cl4 (id int primary key, v text not null default 'x', check (id > 0)); create temp table gtp_cl5 (); end; $$ language plpgsql; select * from plpgsql_make_pragma('gtp_cs1()'); plpgsql_make_pragma ------------------------------------------------------------------ table: gtp_cl1(id integer, name text) table: gtp_cl2(v character varying(10), n numeric(12,2), t text) table: "GtpQ"("select" integer, normal text) table: gtp_cl3(id integer, iid integer, g integer) table: gtp_cl4(id integer, v text) table: gtp_cl5() (6 rows) drop function gtp_cs1(); -- LIKE clause and OF type clause create function gtp_cs2() returns void as $$ begin create temp table gtp_lk1 (like gtp_src); create temp table gtp_lk2 (like gtp_src including all); create temp table gtp_lk3 (id int, like gtp_src); create temp table gtp_of1 of gtp_ct; create temp table gtp_of2 of gtp_ct (x with options not null); end; $$ language plpgsql; select * from plpgsql_make_pragma('gtp_cs2()'); plpgsql_make_pragma ----------------------------------------------- table: gtp_lk1(a integer, b text) table: gtp_lk2(a integer, b text) table: gtp_lk3(id integer, a integer, b text) table: gtp_of1(x integer, y text) table: gtp_of2(x integer, y text) (5 rows) drop function gtp_cs2(); -- inheritance - from a persistent table and from a temporary table -- created earlier in same function create table gtp_parent(pid int, pval text); create function gtp_cs3() returns void as $$ begin create temp table gtp_inh1 () inherits (gtp_parent); create temp table gtp_inh2 (own text) inherits (gtp_parent); create temp table gtp_tp (a int, b text); create temp table gtp_inh3 () inherits (gtp_tp); end; $$ language plpgsql; select * from plpgsql_make_pragma('gtp_cs3()'); plpgsql_make_pragma --------------------------------------------------- table: gtp_inh1(pid integer, pval text) table: gtp_inh2(pid integer, pval text, own text) table: gtp_tp(a integer, b text) table: gtp_inh3(a integer, b text) (4 rows) drop function gtp_cs3(); drop table gtp_parent; -- partitioning - temporary parent and temporary partitions create function gtp_cs4() returns void as $$ begin create temp table gtp_pp (id int, v text) partition by range (id); create temp table gtp_p1 partition of gtp_pp for values from (1) to (100); create temp table gtp_p2 partition of gtp_pp default; end; $$ language plpgsql; select * from plpgsql_make_pragma('gtp_cs4()'); plpgsql_make_pragma ----------------------------------- table: gtp_pp(id integer, v text) table: gtp_p1(id integer, v text) table: gtp_p2(id integer, v text) (3 rows) drop function gtp_cs4(); -- storage options, access method, ON COMMIT variants, IF NOT EXISTS -- (the existing table wins - like runtime does), pg_temp qualification -- without TEMP keyword create function gtp_cs5() returns void as $$ begin create temp table gtp_o1 (a int) with (fillfactor = 70); create temp table gtp_o2 (a int) using heap; create temp table gtp_o3 (a int) on commit preserve rows; create temp table gtp_o4 (a int) on commit delete rows; create temp table gtp_o5 (a int) on commit drop; create temp table if not exists gtp_o6 (a int); create temp table if not exists gtp_o6 (b text); create table pg_temp.gtp_o7 (a int); create table pg_temp.gtp_o8 (like gtp_src); create table pg_temp.gtp_o9 of gtp_ct; end; $$ language plpgsql; select * from plpgsql_make_pragma('gtp_cs5()'); NOTICE: relation "gtp_o6" already exists, skipping plpgsql_make_pragma ---------------------------------- table: gtp_o1(a integer) table: gtp_o2(a integer) table: gtp_o3(a integer) table: gtp_o4(a integer) table: gtp_o5(a integer) table: gtp_o6(a integer) table: gtp_o6(a integer) table: gtp_o7(a integer) table: gtp_o8(a integer, b text) table: gtp_o9(x integer, y text) (10 rows) drop function gtp_cs5(); -- mixed CREATE TABLE AS and CREATE TABLE statements create function gtp_mx1() returns void as $$ begin create temp table gtp_mm1 as select a, b from gtp_src; create temp table gtp_mm2 (id int, v text); end; $$ language plpgsql; select * from plpgsql_make_pragma('gtp_mx1()'); plpgsql_make_pragma ------------------------------------ table: gtp_mm1(a integer, b text) table: gtp_mm2(id integer, v text) (2 rows) drop function gtp_mx1(); -- the table created by CREATE TABLE statement is visible for the -- following CREATE TABLE AS statements (join, subquery) create function gtp_mx2() returns void as $$ begin create temp table gtp_mn1 (a int, b text); create temp table gtp_mn2 as select gtp_mn1.a, s.b from gtp_mn1 join gtp_src s on s.a = gtp_mn1.a where gtp_mn1.a in (select a from gtp_mn1); end; $$ language plpgsql; select * from plpgsql_make_pragma('gtp_mx2()'); plpgsql_make_pragma ----------------------------------- table: gtp_mn1(a integer, b text) table: gtp_mn2(a integer, b text) (2 rows) drop function gtp_mx2(); -- and the reverse order - the table created by CREATE TABLE AS -- statement is visible for the following CREATE TABLE statement create function gtp_mx3() returns void as $$ begin create temp table gtp_mo1 as select a, b from gtp_src; create temp table gtp_mo2 (like gtp_mo1 including all); end; $$ language plpgsql; select * from plpgsql_make_pragma('gtp_mx3()'); plpgsql_make_pragma ----------------------------------- table: gtp_mo1(a integer, b text) table: gtp_mo2(a integer, b text) (2 rows) drop function gtp_mx3(); -- generator of CREATE TABLE statement based pragmas is idempotent too create function gtp_cs6() returns void as $$ begin create temp table gtp_ii (id int, v text); insert into gtp_ii values (1, 'x'); end; $$ language plpgsql; select * from plpgsql_make_pragma('gtp_cs6()'); plpgsql_make_pragma ----------------------------------- table: gtp_ii(id integer, v text) (1 row) select * from plpgsql_make_pragma('gtp_cs6()'); plpgsql_make_pragma ----------------------------------- table: gtp_ii(id integer, v text) (1 row) select count(*) from pg_class where relname in ('gtp_ii'); count ------- 0 (1 row) -- generated pragma is usable by "pragmas" option select * from plpgsql_check_function('gtp_cs6()', pragmas => array(select plpgsql_make_pragma('gtp_cs6()'))); plpgsql_check_function ------------------------ (0 rows) drop function gtp_cs6(); -- error cases create function gtp_err1() returns void as $$ begin create temp table gtp_e1 as select * from gtp_nonexistent; end; $$ language plpgsql; -- generator reports same error like plpgsql_check_function select * from plpgsql_check_function('gtp_err1()'); plpgsql_check_function ----------------------------------------------------------------------- error:42P01:3:SQL statement:relation "gtp_nonexistent" does not exist Query: create temp table gtp_e1 as select * from gtp_nonexistent -- ^ (3 rows) \set VERBOSITY terse select * from plpgsql_make_pragma('gtp_err1()'); ERROR: relation "gtp_nonexistent" does not exist at character 43 \set VERBOSITY default do $$ declare s text; m text; begin perform plpgsql_make_pragma('gtp_err1()'); exception when others then get stacked diagnostics s = returned_sqlstate, m = message_text; raise notice 'sqlstate: %, message: %', s, m; end; $$; NOTICE: sqlstate: 42P01, message: relation "gtp_nonexistent" does not exist drop function gtp_err1(); -- error is raised on first failed statement only, but when -- fatal_errors is false, then the scanning continues create function gtp_err2() returns void as $$ begin create temp table gtp_e21 as select * from gtp_missing1; create temp table gtp_e22 as select * from gtp_missing2; create temp table gtp_e23 as select a from gtp_src; end; $$ language plpgsql; do $$ declare s text; m text; begin perform plpgsql_make_pragma('gtp_err2()'); exception when others then get stacked diagnostics s = returned_sqlstate, m = message_text; raise notice 'sqlstate: %, message: %', s, m; end; $$; NOTICE: sqlstate: 42P01, message: relation "gtp_missing1" does not exist select * from plpgsql_make_pragma('gtp_err2()', fatal_errors => false); WARNING: pragma cannot be generated for statement on line 3 DETAIL: relation "gtp_missing1" does not exist WARNING: pragma cannot be generated for statement on line 4 DETAIL: relation "gtp_missing2" does not exist plpgsql_make_pragma --------------------------- table: gtp_e23(a integer) (1 row) drop function gtp_err2(); -- temporary table cannot be created in non-temporary schema create function gtp_err3() returns void as $$ begin create temporary table public.gtp_e3 as select 1::int, 'JOHN'::text; end; $$ language plpgsql; do $$ declare s text; m text; begin perform plpgsql_make_pragma('gtp_err3()'); exception when others then get stacked diagnostics s = returned_sqlstate, m = message_text; raise notice 'sqlstate: %, message: %', s, m; end; $$; NOTICE: sqlstate: 42P16, message: cannot create temporary relation in non-temporary schema drop function gtp_err3(); -- the check of target schema is done before execution for CREATE TABLE -- statement forms too (column definition list, LIKE clause, OF type -- clause), and it respects fatal_errors option create function gtp_err4() returns void as $$ begin create temp table public.gtp_e41 (id int); create temp table public.gtp_e42 (like gtp_src); create temp table public.gtp_e43 of gtp_ct; create temp table gtp_e44 as select a from gtp_src; end; $$ language plpgsql; do $$ declare s text; m text; begin perform plpgsql_make_pragma('gtp_err4()'); exception when others then get stacked diagnostics s = returned_sqlstate, m = message_text; raise notice 'sqlstate: %, message: %', s, m; end; $$; NOTICE: sqlstate: 42P16, message: cannot create temporary relation in non-temporary schema select * from plpgsql_make_pragma('gtp_err4()', fatal_errors => false); WARNING: pragma cannot be generated for statement on line 3 DETAIL: cannot create temporary relation in non-temporary schema WARNING: pragma cannot be generated for statement on line 4 DETAIL: cannot create temporary relation in non-temporary schema WARNING: pragma cannot be generated for statement on line 5 DETAIL: cannot create temporary relation in non-temporary schema plpgsql_make_pragma --------------------------- table: gtp_e44(a integer) (1 row) drop function gtp_err4(); -- too many column names create function gtp_err5() returns void as $$ begin create temp table gtp_e5(c1, c2, c3) as select a, b from gtp_src; end; $$ language plpgsql; do $$ declare s text; m text; begin perform plpgsql_make_pragma('gtp_err5()'); exception when others then get stacked diagnostics s = returned_sqlstate, m = message_text; raise notice 'sqlstate: %, message: %', s, m; end; $$; NOTICE: sqlstate: 42601, message: too many column names were specified drop function gtp_err5(); -- duplicate table name (usually the pattern CREATE, DROP, CREATE with -- same name) is reported as warning only - DROP statements are not -- executed by static scanning, so this is not a real error. Pragmas -- are returned for both definitions, and the following statements see -- the last definition. create function gtp_err6() returns void as $$ begin create temp table gtp_dd (x int); drop table gtp_dd; create temp table gtp_dd (y text); create temp table gtp_dd2 (z int); create temp table gtp_dd3 as select * from gtp_dd; end; $$ language plpgsql; select * from plpgsql_make_pragma('gtp_err6()'); WARNING: relation "gtp_dd" already exists DETAIL: The temporary table is replaced by the new definition. plpgsql_make_pragma --------------------------- table: gtp_dd(x integer) table: gtp_dd(y text) table: gtp_dd2(z integer) table: gtp_dd3(y text) (4 rows) drop function gtp_err6(); -- mixed collisions - CREATE TABLE statement redefined by CREATE TABLE -- AS statement and vice versa create function gtp_err9() returns void as $$ begin create temp table gtp_md (a int); drop table gtp_md; create temp table gtp_md as select 'x'::text as t; create temp table gtp_md2 as select 1 as b; drop table gtp_md2; create temp table gtp_md2 (c date); create temp table gtp_md3 as select * from gtp_md, gtp_md2; end; $$ language plpgsql; select * from plpgsql_make_pragma('gtp_err9()'); WARNING: relation "gtp_md" already exists DETAIL: The temporary table is replaced by the new definition. WARNING: relation "gtp_md2" already exists DETAIL: The temporary table is replaced by the new definition. plpgsql_make_pragma -------------------------------- table: gtp_md(a integer) table: gtp_md(t text) table: gtp_md2(b integer) table: gtp_md2(c date) table: gtp_md3(t text, c date) (5 rows) drop function gtp_err9(); -- CREATE TABLE AS IF NOT EXISTS - the existing table wins like in -- runtime create function gtp_err10() returns void as $$ begin create temp table gtp_ine2 as select 1 as a; create temp table if not exists gtp_ine2 as select 'x'::text as b; end; $$ language plpgsql; select * from plpgsql_make_pragma('gtp_err10()'); plpgsql_make_pragma ---------------------------- table: gtp_ine2(a integer) table: gtp_ine2(a integer) (2 rows) drop function gtp_err10(); -- runtime errors of CREATE TABLE statement execution respect -- fatal_errors option - a temporary partition of persistent table create table gtp_ppart (id int) partition by range (id); create function gtp_err7() returns void as $$ begin create temp table gtp_bp partition of gtp_ppart for values from (1) to (100); create temp table gtp_bp2 (id int); end; $$ language plpgsql; do $$ declare s text; m text; begin perform plpgsql_make_pragma('gtp_err7()'); exception when others then get stacked diagnostics s = returned_sqlstate, m = message_text; raise notice 'sqlstate: %, message: %', s, m; end; $$; NOTICE: sqlstate: 42809, message: cannot create a temporary relation as partition of permanent relation "gtp_ppart" select * from plpgsql_make_pragma('gtp_err7()', fatal_errors => false); WARNING: pragma cannot be generated for statement on line 3 DETAIL: cannot create a temporary relation as partition of permanent relation "gtp_ppart" plpgsql_make_pragma ---------------------------- table: gtp_bp2(id integer) (1 row) drop function gtp_err7(); drop table gtp_ppart; -- more not executable CREATE TABLE statements - foreign key from -- temporary table to persistent table, LIKE of missing table, OF -- missing type create function gtp_err8() returns void as $$ begin create temp table gtp_fk (id int references gtp_src(a)); create temp table gtp_bl (like gtp_not_exists); create temp table gtp_bo of gtp_type_not_exists; end; $$ language plpgsql; select * from plpgsql_make_pragma('gtp_err8()', fatal_errors => false); WARNING: pragma cannot be generated for statement on line 3 DETAIL: constraints on temporary tables may reference only temporary tables WARNING: pragma cannot be generated for statement on line 4 DETAIL: relation "gtp_not_exists" does not exist WARNING: pragma cannot be generated for statement on line 5 DETAIL: type "gtp_type_not_exists" does not exist plpgsql_make_pragma --------------------- (0 rows) drop function gtp_err8(); -- "pragmas" option of check functions create function gtp_f24() returns void as $$ begin insert into gtp_manual values (1, 'x'); end; $$ language plpgsql; -- NULL and empty array have no effect select * from plpgsql_check_function('gtp_f24()', pragmas => null); plpgsql_check_function ------------------------------------------------------------------ error:42P01:3:SQL statement:relation "gtp_manual" does not exist Query: insert into gtp_manual values (1, 'x') -- ^ (3 rows) select * from plpgsql_check_function('gtp_f24()', pragmas => '{}'::text[]); plpgsql_check_function ------------------------------------------------------------------ error:42P01:3:SQL statement:relation "gtp_manual" does not exist Query: insert into gtp_manual values (1, 'x') -- ^ (3 rows) -- manually written (or fixed) pragma can be used, NULL elements -- are ignored select * from plpgsql_check_function('gtp_f24()', pragmas => array[null, 'table: gtp_manual(a int, b text)']); plpgsql_check_function ------------------------ (0 rows) -- broken pragma raises warning only, and check continues select * from plpgsql_check_function('gtp_f24()', pragmas => array['table: gtp_manual(int int int)']); WARNING: Pragma "table" on line 0 is not processed. DETAIL: syntax error at or near "int" plpgsql_check_function ------------------------------------------------------------------ error:42P01:3:SQL statement:relation "gtp_manual" does not exist Query: insert into gtp_manual values (1, 'x') -- ^ (3 rows) -- any pragma type is allowed select * from plpgsql_check_function('gtp_f24()', pragmas => array['disable: check']); plpgsql_check_function ------------------------ (0 rows) drop function gtp_f24(); drop table gtp_src; drop type gtp_ct; drop domain gtp_dom; okbob-plpgsql_check-800db79/install_bindist.py000066400000000000000000000003551526744060500215320ustar00rootroot00000000000000import os import sys import shutil os.makedirs(sys.argv[2], exist_ok=True) shutil.copy(sys.argv[3], os.path.join(sys.argv[2], sys.argv[4])) for f in sys.argv[5:]: shutil.copy(os.path.join(sys.argv[1], f), os.path.join(sys.argv[2], f))okbob-plpgsql_check-800db79/meson.build000066400000000000000000000117241526744060500201420ustar00rootroot00000000000000project('plpgsql_check', ['c'], version: '2.10.16') pg_config = find_program('pg_config') bindir = run_command(pg_config, '--bindir', check: true).stdout().strip() includedir_server = run_command(pg_config, '--includedir-server', check: true).stdout().strip() includedir = run_command(pg_config, '--includedir', check: true).stdout().strip() pkglibdir = run_command(pg_config, '--pkglibdir', check: true).stdout().strip() sharedir = run_command(pg_config, '--sharedir', check: true).stdout().strip() libdir = run_command(pg_config, '--libdir', check: true).stdout().strip() versionstring = run_command(pg_config, '--version', check: true).stdout().strip() pg_version = versionstring.split(' ')[1] if pg_version.endswith('devel') pg_version_arr = [pg_version.split('devel')[0], '0'] elif pg_version.contains('beta') pg_version_arr = [pg_version.split('beta')[0], '0'] elif pg_version.contains('rc') pg_version_arr = [pg_version.split('rc')[0], '0'] else pg_version_arr = pg_version.split('.') endif pg_version_major = pg_version_arr[0].to_int() pg_version_minor = pg_version_arr[1].to_int() pg_version_num = (pg_version_major * 10000) + pg_version_minor module_version_minor = meson.project_version() module_version_arr = module_version_minor.split('.') module_version = module_version_arr[0] + '.' + module_version_arr[1] module_name = meson.project_name() sources = files( 'src/assign.c', 'src/cursors_leaks.c', 'src/format.c', 'src/check_function.c', 'src/pldbgapi3.c', 'src/pragma.c', 'src/pragma_generator.c', 'src/report.c', 'src/tablefunc.c', 'src/typdesc.c', 'src/catalog.c', 'src/expr_walk.c', 'src/check_expr.c', 'src/parser.c', 'src/plpgsql_check.c', 'src/profiler.c', 'src/stmtwalk.c', 'src/tracer.c', 'src/fextra_cache.c', 'src/generic_stmt_walker.c', ) data = [ module_name + '.control', module_name + '--' + module_version + '.sql' ] tests = [ 'plpgsql_check_active', 'plpgsql_check_passive', 'plpgsql_check_active-' + pg_version_major.to_string(), 'plpgsql_check_passive-' + pg_version_major.to_string(), 'plpgsql_check_profiler', 'plpgsql_pragma_generator', 'plpgsql_check_stmt_walker', 'plpgsql_check_pragma', 'plpgsql_check_comment_options', 'plpgsql_check_parser', 'plpgsql_check_profiler_local', 'plpgsql_check_profiler_queryid', 'plpgsql_check_profiler_ctrl', 'plpgsql_check_tablefunc', 'plpgsql_check_tracer', 'plpgsql_check_report', 'plpgsql_check_stmtwalk', 'plpgsql_check_expr_walk', 'plpgsql_check_assign', 'plpgsql_check_catalog', 'plpgsql_check_check_function', 'plpgsql_check_pldbgapi3', 'plpgsql_check_nocrash', 'plpgsql_check_nocrash_unstable_01', 'plpgsql_check_comment_options_legacystr' ] compiler = meson.get_compiler('c') compiler_name = compiler.get_id() compiler_version = compiler.version() coptions = [] if pg_version_major >= 19 if meson.get_compiler('c').get_id() == 'msvc' coptions = ['c_std=c11'] endif endif if compiler_name == 'msvc' incdir = [includedir_server / 'port/win32_msvc', includedir_server / 'port/win32', includedir_server, includedir] postgres_lib = compiler.find_library( 'postgres', dirs: libdir, static: true, required: true ) else incdir = [ includedir_server ] postgres_lib = [] endif if compiler_name == 'gcc' and pg_version_major <= 15 # see issue 207 cargs = ['-Wno-error=incompatible-pointer-types', '--no-warnings' ] else cargs = [] endif module_lib = shared_module( module_name, sources, c_args: cargs, include_directories: incdir, install: true, install_dir: pkglibdir, name_prefix: '', dependencies: postgres_lib, override_options: coptions, ) install_data(data, install_dir: sharedir / 'extension') fs = import('fs') bindistdir = fs.expanduser('~' / module_name / module_name + '-' + '-'.join( [ module_version_minor, target_machine.cpu(), target_machine.system() ] )) lib_name = fs.name(module_lib.full_path()) lib_suffix = lib_name.split('.')[1] python_exe = find_program('python3', 'python') custom_target('bindist', depends: module_lib, input: module_lib, output: 'bindist', command: [ python_exe, meson.current_source_dir() / 'install_bindist.py', meson.current_source_dir(), bindistdir, module_lib.full_path(), fs.stem(lib_name) + '_' + pg_version_major.to_string() + '.' + lib_suffix, data ], install: false, build_by_default: false) pg_regress = find_program( 'pg_regress', dirs: [pkglibdir / 'pgxs/src/test/regress'] ) test('regress', pg_regress, args: [ '--bindir', bindir, '--inputdir', meson.current_source_dir(), ] + tests, ) okbob-plpgsql_check-800db79/msvc/000077500000000000000000000000001526744060500167435ustar00rootroot00000000000000okbob-plpgsql_check-800db79/msvc/plpgsql_check.sln000066400000000000000000000040351526744060500223020ustar00rootroot00000000000000ο»Ώ Microsoft Visual Studio Solution File, Format Version 11.00 # Visual C++ Express 2010 Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "plpgsql_check", "plpgsql_check\plpgsql_check.vcxproj", "{7ED1E443-C8B4-4CF1-93AE-580AE10F5F95}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution 9.2|Win32 = 9.2|Win32 9.2|x64 = 9.2|x64 9.3|Win32 = 9.3|Win32 9.3|x64 = 9.3|x64 9.4|Win32 = 9.4|Win32 9.4|x64 = 9.4|x64 Debug|Win32 = Debug|Win32 Debug|x64 = Debug|x64 Release|Win32 = Release|Win32 Release|x64 = Release|x64 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {7ED1E443-C8B4-4CF1-93AE-580AE10F5F95}.9.2|Win32.ActiveCfg = 9.2|Win32 {7ED1E443-C8B4-4CF1-93AE-580AE10F5F95}.9.2|Win32.Build.0 = 9.2|Win32 {7ED1E443-C8B4-4CF1-93AE-580AE10F5F95}.9.2|x64.ActiveCfg = 9.2|x64 {7ED1E443-C8B4-4CF1-93AE-580AE10F5F95}.9.2|x64.Build.0 = 9.2|x64 {7ED1E443-C8B4-4CF1-93AE-580AE10F5F95}.9.3|Win32.ActiveCfg = 9.3|Win32 {7ED1E443-C8B4-4CF1-93AE-580AE10F5F95}.9.3|Win32.Build.0 = 9.3|Win32 {7ED1E443-C8B4-4CF1-93AE-580AE10F5F95}.9.3|x64.ActiveCfg = 9.3|x64 {7ED1E443-C8B4-4CF1-93AE-580AE10F5F95}.9.3|x64.Build.0 = 9.3|x64 {7ED1E443-C8B4-4CF1-93AE-580AE10F5F95}.9.4|Win32.ActiveCfg = 9.4|Win32 {7ED1E443-C8B4-4CF1-93AE-580AE10F5F95}.9.4|Win32.Build.0 = 9.4|Win32 {7ED1E443-C8B4-4CF1-93AE-580AE10F5F95}.9.4|x64.ActiveCfg = 9.4|x64 {7ED1E443-C8B4-4CF1-93AE-580AE10F5F95}.9.4|x64.Build.0 = 9.4|x64 {7ED1E443-C8B4-4CF1-93AE-580AE10F5F95}.Debug|Win32.ActiveCfg = 9.4|x64 {7ED1E443-C8B4-4CF1-93AE-580AE10F5F95}.Debug|x64.ActiveCfg = 9.4|x64 {7ED1E443-C8B4-4CF1-93AE-580AE10F5F95}.Debug|x64.Build.0 = 9.4|x64 {7ED1E443-C8B4-4CF1-93AE-580AE10F5F95}.Release|Win32.ActiveCfg = 9.4|x64 {7ED1E443-C8B4-4CF1-93AE-580AE10F5F95}.Release|x64.ActiveCfg = 9.4|x64 {7ED1E443-C8B4-4CF1-93AE-580AE10F5F95}.Release|x64.Build.0 = 9.4|x64 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection EndGlobal okbob-plpgsql_check-800db79/msvc/plpgsql_check/000077500000000000000000000000001526744060500215625ustar00rootroot00000000000000okbob-plpgsql_check-800db79/msvc/plpgsql_check/plpgsql_check.vcxproj000066400000000000000000000356241526744060500260300ustar00rootroot00000000000000ο»Ώ 9.2 Win32 9.2 x64 9.3 Win32 9.3 x64 9.4 Win32 9.4 x64 {7ED1E443-C8B4-4CF1-93AE-580AE10F5F95} plpgsql_check DynamicLibrary false true MultiByte Windows7.1SDK DynamicLibrary false true MultiByte Windows7.1SDK DynamicLibrary false true MultiByte Windows7.1SDK DynamicLibrary false true MultiByte Windows7.1SDK DynamicLibrary false true MultiByte Windows7.1SDK DynamicLibrary false true MultiByte Windows7.1SDK false false false false false false Level3 MaxSpeed true true WIN32;%(PreprocessorDefinitions) false CompileAsC C:\Program Files %28x86%29\PostgreSQL\9.4\include\server\port\win32_msvc;C:\Program Files %28x86%29\PostgreSQL\9.4\include\server\port\win32;C:\Program Files %28x86%29\PostgreSQL\9.4\include\server;C:\Program Files %28x86%29\PostgreSQL\9.4\include;%(AdditionalIncludeDirectories) true true true postgres.lib;plpgsql.lib;%(AdditionalDependencies) C:\Program Files %28x86%29\PostgreSQL\9.4\lib;%(AdditionalLibraryDirectories) false Level3 MaxSpeed true true WIN32;%(PreprocessorDefinitions) false CompileAsC C:\Program Files %28x86%29\PostgreSQL\9.2\include\server\port\win32_msvc;C:\Program Files %28x86%29\PostgreSQL\9.2\include\server\port\win32;C:\Program Files %28x86%29\PostgreSQL\9.2\include\server;C:\Program Files %28x86%29\PostgreSQL\9.2\include;%(AdditionalIncludeDirectories) true true true postgres.lib;plpgsql.lib;%(AdditionalDependencies) C:\Program Files %28x86%29\PostgreSQL\9.2\lib;%(AdditionalLibraryDirectories) false Level3 MaxSpeed true true WIN32;%(PreprocessorDefinitions) false CompileAsC C:\Program Files\PostgreSQL\9.4\include\server\port\win32_msvc;C:\Program Files\PostgreSQL\9.4\include\server\port\win32;C:\Program Files\PostgreSQL\9.4\include\server;C:\Program Files\PostgreSQL\9.4\include;%(AdditionalIncludeDirectories) true true true postgres.lib;plpgsql.lib;%(AdditionalDependencies) C:\Program Files\PostgreSQL\9.4\lib;%(AdditionalLibraryDirectories) false Level3 MaxSpeed true true WIN32;%(PreprocessorDefinitions) false CompileAsC C:\Program Files\PostgreSQL\9.2\include\server\port\win32_msvc;C:\Program Files\PostgreSQL\9.2\include\server\port\win32;C:\Program Files\PostgreSQL\9.2\include\server;C:\Program Files\PostgreSQL\9.2\include;%(AdditionalIncludeDirectories) true true true postgres.lib;plpgsql.lib;%(AdditionalDependencies) C:\Program Files\PostgreSQL\9.2\lib;%(AdditionalLibraryDirectories) false Level3 MaxSpeed true true WIN32;%(PreprocessorDefinitions) false CompileAsC C:\Program Files %28x86%29\PostgreSQL\9.3\include\server\port\win32_msvc;C:\Program Files %28x86%29\PostgreSQL\9.3\include\server\port\win32;C:\Program Files %28x86%29\PostgreSQL\9.3\include\server;C:\Program Files %28x86%29\PostgreSQL\9.3\include;%(AdditionalIncludeDirectories) true true true postgres.lib;plpgsql.lib;%(AdditionalDependencies) C:\Program Files %28x86%29\PostgreSQL\9.3\lib;%(AdditionalLibraryDirectories) false Level3 MaxSpeed true true WIN32;%(PreprocessorDefinitions) false CompileAsC C:\Program Files\PostgreSQL\9.3\include\server\port\win32_msvc;C:\Program Files\PostgreSQL\9.3\include\server\port\win32;C:\Program Files\PostgreSQL\9.3\include\server;C:\Program Files\PostgreSQL\9.3\include;%(AdditionalIncludeDirectories) true true true postgres.lib;plpgsql.lib;%(AdditionalDependencies) C:\Program Files\PostgreSQL\9.3\lib;%(AdditionalLibraryDirectories) false okbob-plpgsql_check-800db79/msvc/plpgsql_check/plpgsql_check.vcxproj.filters000066400000000000000000000021561526744060500274710ustar00rootroot00000000000000ο»Ώ {4FC737F1-C7A5-4376-A066-2A32D752A2FF} cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx {93995380-89BD-4b04-88EB-625FBE52EBFB} h;hpp;hxx;hm;inl;inc;xsd {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms Source Files Header Files okbob-plpgsql_check-800db79/msvc/plpgsql_check/plpgsql_check.vcxproj.user000066400000000000000000000002171526744060500267730ustar00rootroot00000000000000ο»Ώ okbob-plpgsql_check-800db79/plpgsql_check--2.10.sql000066400000000000000000000360061526744060500217730ustar00rootroot00000000000000-- complain if script is sourced in psql, rather than via CREATE EXTENSION \echo Use "CREATE EXTENSION plpgsql_check" to load this file. \quit CREATE FUNCTION plpgsql_check_function_tb(funcoid regprocedure, relid regclass DEFAULT 0, fatal_errors boolean DEFAULT true, other_warnings boolean DEFAULT true, performance_warnings boolean DEFAULT false, extra_warnings boolean DEFAULT true, security_warnings boolean DEFAULT false, compatibility_warnings boolean DEFAULT false, oldtable name DEFAULT null, newtable name DEFAULT null, anyelememttype regtype DEFAULT 'int', anyenumtype regtype DEFAULT '-', anyrangetype regtype DEFAULT 'int4range', anycompatibletype regtype DEFAULT 'int', anycompatiblerangetype regtype DEFAULT 'int4range', without_warnings boolean DEFAULT false, all_warnings boolean DEFAULT false, use_incomment_options boolean DEFAULT true, incomment_options_usage_warning boolean DEFAULT false, constant_tracing boolean DEFAULT true, pragmas text[] DEFAULT NULL) RETURNS TABLE(functionid regproc, lineno int, statement text, sqlstate text, message text, detail text, hint text, level text, "position" int, query text, context text) AS 'MODULE_PATHNAME','plpgsql_check_function_tb' LANGUAGE C; CREATE FUNCTION plpgsql_check_function(funcoid regprocedure, relid regclass DEFAULT 0, format text DEFAULT 'text', fatal_errors boolean DEFAULT true, other_warnings boolean DEFAULT true, performance_warnings boolean DEFAULT false, extra_warnings boolean DEFAULT true, security_warnings boolean DEFAULT false, compatibility_warnings boolean DEFAULT false, oldtable name DEFAULT null, newtable name DEFAULT null, anyelememttype regtype DEFAULT 'int', anyenumtype regtype DEFAULT '-', anyrangetype regtype DEFAULT 'int4range', anycompatibletype regtype DEFAULT 'int', anycompatiblerangetype regtype DEFAULT 'int4range', without_warnings boolean DEFAULT false, all_warnings boolean DEFAULT false, use_incomment_options boolean DEFAULT true, incomment_options_usage_warning boolean DEFAULT false, constant_tracing boolean DEFAULT true, pragmas text[] DEFAULT NULL) RETURNS SETOF text AS 'MODULE_PATHNAME','plpgsql_check_function' LANGUAGE C; CREATE FUNCTION plpgsql_check_function_tb(name text, relid regclass DEFAULT 0, fatal_errors boolean DEFAULT true, other_warnings boolean DEFAULT true, performance_warnings boolean DEFAULT false, extra_warnings boolean DEFAULT true, security_warnings boolean DEFAULT false, compatibility_warnings boolean DEFAULT false, oldtable name DEFAULT null, newtable name DEFAULT null, anyelememttype regtype DEFAULT 'int', anyenumtype regtype DEFAULT '-', anyrangetype regtype DEFAULT 'int4range', anycompatibletype regtype DEFAULT 'int', anycompatiblerangetype regtype DEFAULT 'int4range', without_warnings boolean DEFAULT false, all_warnings boolean DEFAULT false, use_incomment_options boolean DEFAULT true, incomment_options_usage_warning boolean DEFAULT false, constant_tracing boolean DEFAULT true, pragmas text[] DEFAULT NULL) RETURNS TABLE(functionid regproc, lineno int, statement text, sqlstate text, message text, detail text, hint text, level text, "position" int, query text, context text) AS 'MODULE_PATHNAME','plpgsql_check_function_tb_name' LANGUAGE C; CREATE FUNCTION plpgsql_check_function(name text, relid regclass DEFAULT 0, format text DEFAULT 'text', fatal_errors boolean DEFAULT true, other_warnings boolean DEFAULT true, performance_warnings boolean DEFAULT false, extra_warnings boolean DEFAULT true, security_warnings boolean DEFAULT false, compatibility_warnings boolean DEFAULT false, oldtable name DEFAULT null, newtable name DEFAULT null, anyelememttype regtype DEFAULT 'int', anyenumtype regtype DEFAULT '-', anyrangetype regtype DEFAULT 'int4range', anycompatibletype regtype DEFAULT 'int', anycompatiblerangetype regtype DEFAULT 'int4range', without_warnings boolean DEFAULT false, all_warnings boolean DEFAULT false, use_incomment_options boolean DEFAULT true, incomment_options_usage_warning boolean DEFAULT false, constant_tracing boolean DEFAULT true, pragmas text[] DEFAULT NULL) RETURNS SETOF text AS 'MODULE_PATHNAME','plpgsql_check_function_name' LANGUAGE C; CREATE FUNCTION plpgsql_make_pragma(funcoid regprocedure, relid regclass DEFAULT 0, fatal_errors boolean DEFAULT true) RETURNS SETOF text AS 'MODULE_PATHNAME','plpgsql_make_pragma' LANGUAGE C; CREATE FUNCTION __plpgsql_show_dependency_tb(funcoid regprocedure, relid regclass DEFAULT 0, anyelememttype regtype DEFAULT 'int', anyenumtype regtype DEFAULT '-', anyrangetype regtype DEFAULT 'int4range', anycompatibletype regtype DEFAULT 'int', anycompatiblerangetype regtype DEFAULT 'int4range') RETURNS TABLE(type text, oid oid, schema text, name text, params text) AS 'MODULE_PATHNAME','plpgsql_show_dependency_tb' LANGUAGE C; CREATE FUNCTION __plpgsql_show_dependency_tb(name text, relid regclass DEFAULT 0, anyelememttype regtype DEFAULT 'int', anyenumtype regtype DEFAULT '-', anyrangetype regtype DEFAULT 'int4range', anycompatibletype regtype DEFAULT 'int', anycompatiblerangetype regtype DEFAULT 'int4range') RETURNS TABLE(type text, oid oid, schema text, name text, params text) AS 'MODULE_PATHNAME','plpgsql_show_dependency_tb_name' LANGUAGE C; CREATE FUNCTION plpgsql_show_dependency_tb(funcoid regprocedure, relid regclass DEFAULT 0, anyelememttype regtype DEFAULT 'int', anyenumtype regtype DEFAULT '-', anyrangetype regtype DEFAULT 'int4range', anycompatibletype regtype DEFAULT 'int', anycompatiblerangetype regtype DEFAULT 'int4range') RETURNS TABLE(type text, oid oid, schema text, name text, params text) AS $$ SELECT * FROM @extschema@.__plpgsql_show_dependency_tb($1, $2, $3, $4, $5, $6, $7) ORDER BY 1, 3, 4; $$ LANGUAGE sql; CREATE FUNCTION plpgsql_show_dependency_tb(fnname text, relid regclass DEFAULT 0, anyelememttype regtype DEFAULT 'int', anyenumtype regtype DEFAULT '-', anyrangetype regtype DEFAULT 'int4range', anycompatibletype regtype DEFAULT 'int', anycompatiblerangetype regtype DEFAULT 'int4range') RETURNS TABLE(type text, oid oid, schema text, name text, params text) AS $$ SELECT * FROM @extschema@.__plpgsql_show_dependency_tb($1, $2, $3, $4, $5, $6, $7) ORDER BY 1, 3, 4; $$ LANGUAGE sql; CREATE FUNCTION plpgsql_profiler_function_tb(funcoid regprocedure) RETURNS TABLE(lineno int, stmt_lineno int, queryids int8[], cmds_on_row int, exec_stmts int8[], exec_stmts_err int8[], total_time double precision[], avg_time double precision[], max_time double precision[], processed_rows int8[], source text) AS 'MODULE_PATHNAME','plpgsql_profiler_function_tb' LANGUAGE C STRICT; CREATE FUNCTION plpgsql_profiler_function_tb(name text) RETURNS TABLE(lineno int, stmt_lineno int, queryids int8[], cmds_on_row int, exec_stmts int8[], exec_stmts_err int8[], total_time double precision[], avg_time double precision[], max_time double precision[], processed_rows int8[], source text) AS 'MODULE_PATHNAME','plpgsql_profiler_function_tb_name' LANGUAGE C STRICT; CREATE FUNCTION plpgsql_profiler_function_statements_tb(funcoid regprocedure) RETURNS TABLE(stmtid int, parent_stmtid int, block_num int, lineno int, queryid int8, exec_stmts int8, exec_stmts_err int8, total_time double precision, avg_time double precision, max_time double precision, processed_rows int8, stmtname text) AS 'MODULE_PATHNAME','plpgsql_profiler_function_statements_tb' LANGUAGE C STRICT; CREATE FUNCTION plpgsql_profiler_function_statements_tb(name text) RETURNS TABLE(stmtid int, parent_stmtid int, block_num int, lineno int, queryid int8, exec_stmts int8, exec_stmts_err int8, total_time double precision, avg_time double precision, max_time double precision, processed_rows int8, stmtname text) AS 'MODULE_PATHNAME','plpgsql_profiler_function_statements_tb_name' LANGUAGE C STRICT; CREATE FUNCTION plpgsql_profiler_install_fake_queryid_hook() RETURNS void AS 'MODULE_PATHNAME','plpgsql_profiler_install_fake_queryid_hook' LANGUAGE C STRICT; CREATE FUNCTION plpgsql_profiler_remove_fake_queryid_hook() RETURNS void AS 'MODULE_PATHNAME','plpgsql_profiler_remove_fake_queryid_hook' LANGUAGE C STRICT; CREATE FUNCTION plpgsql_profiler_reset_all() RETURNS void AS 'MODULE_PATHNAME','plpgsql_profiler_reset_all' LANGUAGE C STRICT; CREATE FUNCTION plpgsql_profiler_reset(funcoid regprocedure) RETURNS void AS 'MODULE_PATHNAME','plpgsql_profiler_reset' LANGUAGE C STRICT; CREATE OR REPLACE FUNCTION plpgsql_coverage_statements(funcoid regprocedure) RETURNS double precision AS 'MODULE_PATHNAME', 'plpgsql_coverage_statements' LANGUAGE C; CREATE OR REPLACE FUNCTION plpgsql_coverage_statements(name text) RETURNS double precision AS 'MODULE_PATHNAME', 'plpgsql_coverage_statements_name' LANGUAGE C; CREATE OR REPLACE FUNCTION plpgsql_coverage_branches(funcoid regprocedure) RETURNS double precision AS 'MODULE_PATHNAME', 'plpgsql_coverage_branches' LANGUAGE C; CREATE OR REPLACE FUNCTION plpgsql_coverage_branches(name text) RETURNS double precision AS 'MODULE_PATHNAME', 'plpgsql_coverage_branches_name' LANGUAGE C; CREATE OR REPLACE FUNCTION plpgsql_check_pragma(VARIADIC name text[]) RETURNS integer AS 'MODULE_PATHNAME', 'plpgsql_check_pragma' LANGUAGE C VOLATILE; CREATE OR REPLACE FUNCTION plpgsql_profiler_functions_all() RETURNS TABLE(funcoid regprocedure, exec_count int8, exec_stmts_err int8, total_time double precision, avg_time double precision, stddev_time double precision, min_time double precision, max_time double precision) AS 'MODULE_PATHNAME','plpgsql_profiler_functions_all_tb' LANGUAGE C STRICT; CREATE OR REPLACE FUNCTION plpgsql_check_profiler(enable boolean DEFAULT NULL) RETURNS boolean AS 'MODULE_PATHNAME', 'plpgsql_check_profiler_ctrl' LANGUAGE C VOLATILE; CREATE OR REPLACE FUNCTION plpgsql_check_tracer(enable boolean DEFAULT NULL, verbosity text DEFAULT NULL) RETURNS boolean AS 'MODULE_PATHNAME', 'plpgsql_check_tracer_ctrl' LANGUAGE C VOLATILE;okbob-plpgsql_check-800db79/plpgsql_check.control000066400000000000000000000002671526744060500222210ustar00rootroot00000000000000# plpgsql_check extension comment = 'extended check for plpgsql functions' default_version = '2.10' module_pathname = '$libdir/plpgsql_check' relocatable = false requires = 'plpgsql' okbob-plpgsql_check-800db79/plpgsql_check.typedefs000066400000000000000000000156061526744060500223670ustar00rootroot00000000000000A_Const AclMode Alias ArrayBuildState ArrayExpr ArrayIterator ArrayMetaState ArrayType AttrDefault AttrMap AttrNumber BackendType BitString Bitmapset BlockIdData BlockNumber Boolean BufferAccessStrategy BulkInsertStateData CCFastEqualFN CCHashFN CTECycleClause CTEMaterialize CTESearchClause CachedFunction CachedFunctionDeleteCallback CachedFunctionHashKey CachedPlan CachedPlanSource CallStmt Cardinality CatCList CatCTup CatCache CmdType CoerceParamHook CoerceViaIO CoercionContext CoercionForm ColumnRef CommandDest CommandId CommandTag CommonTableExpr CompactAttribute Const ConstrCheck Cost CreateStmt CreateTableAsStmt CursorLeaksPlugin2Info CursorTrace Datum DatumTupleFields DestReceiver DomainConstraintCache DynSQLParams EOM_flatten_into_method EOM_get_flat_size_method EState EphemeralNameRelationType EphemeralNamedRelation EphemeralNamedRelationData EphemeralNamedRelationMetadataData ErrorContextCallback ErrorData ErrorSaveContext EventTriggerData ExecAuxRowMark ExecProcNodeMtd ExecRowMark ExpandedObjectHeader ExpandedObjectMethods ExpandedRecordFieldInfo ExpandedRecordHeader Expr ExprContext ExprContextCallbackFunction ExprContext_CB ExprDoneCond ExprState ExprStateEvalFunc FetchDirection Float FmgrInfo ForBothState ForEachState ForkNumber FormData_pg_attribute FormData_pg_class FormData_pg_index FormData_pg_operator FormData_pg_proc Form_pg_attribute Form_pg_class Form_pg_index Form_pg_operator Form_pg_proc FromExpr FuncCall FuncCandidateList FuncExpr FuncStats FuncStmtsStats FunctionCallInfo FunctionCallInfoBaseData FunctionTrace GucAction GucBoolAssignHook GucBoolCheckHook GucContext GucEnumAssignHook GucEnumCheckHook GucIntAssignHook GucIntCheckHook GucShowHook GucSource HASHACTION HASHCTL HASHELEMENT HASHHDR HASH_SEQ_STATUS HTAB HashAllocFunc HashCompareFunc HashCopyFunc HashValueFunc HeapTuple HeapTupleData HeapTupleFields HeapTupleHeader HeapTupleHeaderData IOStats Index IndexBuildCallback IndexInfo IndexScanDesc IndexTuple IndexTupleData Integer IntoClause ItemPointer ItemPointerData JoinType JumbleState JunkFilter LOCK LOCKMASK LOCKMODE LOCKTAG LWLock LWLockMode LWLockPadded LXCache LXCache_info Latch LimitOption List ListCell LocalTransactionId LocationLen LockClauseStrength LockInfoData LockRelId LockTupleMode LockWaitPolicy MemoryContext MemoryContextCallback MemoryContextCallbackFunction MemoryContextCounters MemoryContextMethods MemoryStatsPrintFunc MinimalTuple MinimalTupleData MultiXactId Name NameData NamedArgExpr Node NodeInstrumentation NodeTag NullableDatum ObjectType OffsetNumber Oid Oid8 OnCommitAction OnConflictAction OnConflictActionState OnConflictExpr OpExpr OverridingKind PGErrorVerbosity PGFunction PGPROC PGSemaphore PLpgSQL_case_when PLpgSQL_checkstate PLpgSQL_condition PLpgSQL_datum PLpgSQL_datum_type PLpgSQL_diag_item PLpgSQL_exception PLpgSQL_exception_block PLpgSQL_execstate PLpgSQL_expr PLpgSQL_function PLpgSQL_getdiag_kind PLpgSQL_if_elsif PLpgSQL_nsitem PLpgSQL_nsitem_type PLpgSQL_plugin PLpgSQL_promise_type PLpgSQL_raise_option PLpgSQL_raise_option_type PLpgSQL_rec PLpgSQL_recfield PLpgSQL_resolve_option PLpgSQL_row PLpgSQL_rwopt PLpgSQL_statements PLpgSQL_stmt PLpgSQL_stmt_assert PLpgSQL_stmt_assign PLpgSQL_stmt_block PLpgSQL_stmt_call PLpgSQL_stmt_case PLpgSQL_stmt_close PLpgSQL_stmt_dynexecute PLpgSQL_stmt_dynfors PLpgSQL_stmt_execsql PLpgSQL_stmt_exit PLpgSQL_stmt_fetch PLpgSQL_stmt_forc PLpgSQL_stmt_foreach_a PLpgSQL_stmt_fori PLpgSQL_stmt_fors PLpgSQL_stmt_getdiag PLpgSQL_stmt_if PLpgSQL_stmt_loop PLpgSQL_stmt_open PLpgSQL_stmt_perform PLpgSQL_stmt_raise PLpgSQL_stmt_return PLpgSQL_stmt_return_next PLpgSQL_stmt_return_query PLpgSQL_stmt_stack_item PLpgSQL_stmt_type PLpgSQL_stmt_while PLpgSQL_trigtype PLpgSQL_type PLpgSQL_type_type PLpgSQL_var PLpgSQL_variable PROCLOCK PROCLOCKTAG ParallelTableScanDesc Param ParamCompileHook ParamExecData ParamExternData ParamFetchHook ParamKind ParamListInfo ParamRef ParseExprKind ParseLoc ParseNamespaceColumn ParseNamespaceItem ParseParamRefHook ParseState ParserSetupHook PartitionBoundSpec PartitionDesc PartitionDirectory PartitionKey PartitionSpec PartitionStrategy Pg_abi_values Pg_finfo_record Pg_magic_struct Plan PlanState PlannedStmt PlannedStmtOrigin Pointer Portal PortalStatus PortalStrategy PostParseColumnRefHook PostRewriteHook PragmaAssertType PragmaTokenType PreParseColumnRefHook ProcNumber ProcWaitStatus ProfilerSharedState ProjectionInfo PublicationActions PublicationDesc QParams Query QueryCompletion QueryDesc QueryEnvironment QuerySource RTEKind RTEPermissionInfo RangeTblEntry RangeVar RangeVarGetRelidCallback RawParseMode RawStmt ReadStream RegProcedure RelFileLocator RelFileLocatorBackend RelFileNumber RelabelType Relation RelationPtr ResTarget ResourceOwner ResultRelInfo ReturnSetInfo RewriteRule RowExpr RowMarkType RuleLock SMgrRelation SPIPlanPtr SPIPrepareOptions SPITupleTable SampleScanState ScanDirection ScanKeyData ScanKeywordHashFunc ScanKeywordList ScanState SelectStmt SetFunctionReturnMode SetOperation Size Snapshot SnapshotType StmtInstr StmtStats StrategyNumber String StringInfo StringInfoData SubLinkType SubPlan SubTransactionId SubscriptingRef SysCacheIdentifier SyscacheCallbackFunction TBMIterator TBMPrivateIterator TBMSharedIterator TM_FailureData TM_IndexDelete TM_IndexDeleteOp TM_IndexStatus TM_Result TU_UpdateIndexes TableFunc TableFuncType TableScanDesc TargetEntry TimestampTz TokenizerState TransactionId Trigger TriggerData TriggerDesc TriggerEvent TriggerInstrumentation TupleConstr TupleConversionMap TupleDesc TupleTableSlot TupleTableSlotOps Tuplestorestate TypeCacheEntry TypeFuncClass TypeName VacuumParams ValidateIndexState VarReturningType WithClause WorkerNodeInstrumentation XLogRecPtr XidCacheStatus XidStatus __clockid_t __int16_t __int32_t __int64_t __jmp_buf __sig_atomic_t __sigset_t __syscall_slong_t __time_t __uint16_t __uint32_t __uint64_t __uint8_t bitmapword bytea check_funcexpr_walker_params check_function_callback clockid_t count_stmt_exec_time_context coverage_branches_context coverage_statements_context dclist_head dlist_head dlist_node fextra_init_context float4 float8 func_hk instr_time int16 int16_t int2vector int32 int32_t int64 int64_t oidvector pairingheap_node pc_queryid pg_atomic_uint32 pg_atomic_uint64 plch_expr_walker_callback plch_fextra plch_fidentity_hk plch_plugin plch_stmt_walker_callback plpgsql_check_HashEnt plpgsql_check__build_datatype_t plpgsql_check__compile_t plpgsql_check__exec_get_datum_type_t plpgsql_check__ns_lookup_t plpgsql_check__parser_setup_t plpgsql_check__recognize_err_condition_t plpgsql_check__stmt_typename_t plpgsql_check_info plpgsql_check_pragma_vector plpgsql_check_result_info plpgsql_plugin_info post_parse_analyze_hook_type proclist_head proclist_node profiler_info profiler_report_context regproc shmem_request_hook_type shmem_startup_hook_type sig_atomic_t sigjmp_buf size_t slist_node slock_t statement_stats_report_context text tracer_info tree_walker_callback uint16 uint16_t uint32 uint32_t uint64 uint64_t uint8 uint8_t uintptr_t varlena okbob-plpgsql_check-800db79/postgresql14-plpgsql_check.spec000066400000000000000000000147241526744060500240440ustar00rootroot00000000000000%global pgmajorversion 14 %global pginstdir /usr/pgsql-14 %global sname plpgsql_check Name: %{sname}_%{pgmajorversion} Version: 2.10.16 Release: 1%{?dist} Summary: Additional tools for plpgsql functions validation Group: Applications/Databases License: BSD URL: https://github.com/okbob/plpgsql_check/archive/v%{version}.zip Source0: plpgsql_check-%{version}.zip BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root-%(%{__id_u} -n) BuildRequires: postgresql%{pgmajorversion}-devel Requires: postgresql%{pgmajorversion} %description The plpgsql_check is PostgreSQL extension with functionality for direct or indirect extra validation of functions in plpgsql language. It verifies a validity of SQL identifiers used in plpgsql code. It try to identify a performance issues. %prep %setup -q -n %{sname}-%{version} %build PATH="%{pginstdir}/bin;$PATH" ; export PATH CFLAGS="${CFLAGS:-%optflags}" ; export CFLAGS make USE_PGXS=1 PG_CONFIG=%{pginstdir}/bin/pg_config %{?_smp_mflags} %install rm -rf %{buildroot} make install DESTDIR=%{buildroot} PG_CONFIG=%{pginstdir}/bin/pg_config %{?_smp_mflags} %clean rm -rf %{buildroot} %files %defattr(644,root,root,755) %doc README.md %{pginstdir}/lib/plpgsql_check.so %{pginstdir}/share/extension/plpgsql_check--2.10.sql %{pginstdir}/share/extension/plpgsql_check.control %{pginstdir}/lib/bitcode/*.bc %{pginstdir}/lib/bitcode/plpgsql_check/src/*.bc %{pginstdir}/share/extension/*.control %changelog * Wed Jul 15 2026 - Pavel Stehule 2.10.0 - possibility to pass pragma as an argument - possibility to generate pragma from code * Thu May 21 2026 - Pavel Stehule 2.9.0 - significant profiler refactoring * Tue Mar 18 2025 - Pavel Stehule 2.8.0 - remove support for PostgreSQL 12 and 13 * Wed Dec 6 2023 - Pavel Stehule 2.7.0 - unclosed cursors detection * Tue Oct 31 2023 - Pavel Stehule 2.6.0 - simple constant tracing support * Sat Apr 29 2023 - Pavel Stehule 2.4.0 - remove support for PostgreSQL 10 and 11 * Wed Jan 11 2023 - Pavel Stehule 2.3.0 - possibility to detect compatibility issues (obsolete setting of refcursor) * Tue Sep 20 2022 - Pavel Stehule 2.2.0 - possibility to use in comment options * Wed Dec 29 2021 - Pavel Stehule 2.1.0 - possibility to count statement's aborted execution - possibility to count "unfinished" statements due exception * Mon Sep 27 2021 - Pavel Stehule 2.0.0 - pragma type for setting type to record variable - pragma table for creating ephemeral table * Mon Jun 21 2021 - Pavel Stehule 1.17.0 - remove support for PostgreSQL 9.5 and 9.6 * Sat Mar 6 2021 - Pavel Stehule 1.16.0 - plpgsql_profiler_functions_all * Mon Nov 16 2020 - Pavel Stehule 1.14.0 - queryid can be displayed in profiler's reports (Julien Rouhaud) - new profiler's GUC plpgsql_check.profiler_max_shared_chunks (Julien Rouhaud) - few minor bugfixes * Fri Aug 14 2020 - Pavel Stehule 1.13.0 - tracer - pragma support to control checks, warnings and tracing * Thu Jul 2 2020 - Pavel STEHULE 1.11.0 - possibility to check functions with arguments of polymorphic type - possibility to specify type used as real type instead polymorphic type * Fri Jun 05 2020 - Pavel STEHULE 1.10.0 - deduction record type structure from result of polymorphic function * Mon Apr 27 2020 - Pavel STEHULE 1.9.1 - minor bugfixes * Mon Mar 30 2020 - Pavel STEHULE 1.9.0 - statement and branch coverage metrics - remove support for Postgres 9.4 * Mon Jan 06 2020 - Pavel STEHULE 1.8.2 - fix of compilation issue * Sun Jan 05 2020 - Pavel STEHULE 1.8.1 - cleaner detection function oid from name or signature * Sun Dec 29 2019 - Pavel STEHULE 1.8.0 - use Postgres tool for calling functions from plpgsql library instead dynamic linking - it solve issues related to dependency plpgsq_check on plpgsql * Mon Sep 23 2019 - Pavel STEHULE 1.7.6 - fix false alarm - multiple plans in EXECUTE statement, and possible crash * Tue Sep 10 2019 - Pavel STEHULE 1.7.5 - allow some work on tables with rules * Wed Jul 24 2019 - Pavel STEHULE 1.7.3 - profiler bugfixes * Tue May 21 2019 - Pavel STEHULE 1.7.2 - profiler bugfixes * Fri Apr 26 2019 - Pavel STEHULE 1.7.1 - bugfixes * Wed Apr 17 2019 - Pavel STEHULE 1.7.0 - check of format of fmt string of "format" function - better check of dynamic SQL when it is const string - check of SQL injection vulnerability of stmt expression at EXECUTE stmt * Sun Dec 23 2018 - Pavel STEHULE 1.4.2-1 - metada fix * Fri Dec 21 2018 - Pavel STEHULE 1.4.1-1 - minor bugfixes * Sun Dec 2 2018 - Pavel STEHULE 1.4.0-1 - possible to show function's dependency on functions and tables - integrated profiler - bug fixes (almost false alarms) * Wed Jun 6 2018 - Pavel STEHULE 1.2.3-1 - PostgreSQL 11 support - detect hidden casts in expressions * Thu Oct 26 2017 - Pavel STEHULE 1.2.2-1 - never read variables detection - fix false alarm on MOVE command * Fri Sep 15 2017 - Pavel STEHULE 1.2.1-1 - missing RETURN detection - fix some bugs and false alarms - PostgreSQL 11 support * Fri Nov 11 2016 - Pavel STEHULE 1.2.0-1 - support extra warnings - shadowed variables * Thu Aug 25 2016 - Pavel STEHULE 1.0.5-1 - minor fixes, support for PostgreSQL 10 * Fri Apr 15 2016 - Pavel STEHULE 1.0.4-1 - support for PostgreSQL 9.6 * Mon Oct 12 2015 - Pavel STEHULE 1.0.3-1 - fix false alarms of unused cursor variables - fix regress tests * Thu Jul 09 2015 - Pavel STEHULE 1.0.2-2 - bugfix release * Fri Dec 19 2014 - Pavel STEHULE 0.9.3-1 - fix a broken record field type checking - add check for assign to array field * Mon Aug 25 2014 - Pavel STEHULE 0.9.1-1 - Initial packaging okbob-plpgsql_check-800db79/postgresql15-plpgsql_check.spec000066400000000000000000000147241526744060500240450ustar00rootroot00000000000000%global pgmajorversion 15 %global pginstdir /usr/pgsql-15 %global sname plpgsql_check Name: %{sname}_%{pgmajorversion} Version: 2.10.16 Release: 1%{?dist} Summary: Additional tools for plpgsql functions validation Group: Applications/Databases License: BSD URL: https://github.com/okbob/plpgsql_check/archive/v%{version}.zip Source0: plpgsql_check-%{version}.zip BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root-%(%{__id_u} -n) BuildRequires: postgresql%{pgmajorversion}-devel Requires: postgresql%{pgmajorversion} %description The plpgsql_check is PostgreSQL extension with functionality for direct or indirect extra validation of functions in plpgsql language. It verifies a validity of SQL identifiers used in plpgsql code. It try to identify a performance issues. %prep %setup -q -n %{sname}-%{version} %build PATH="%{pginstdir}/bin;$PATH" ; export PATH CFLAGS="${CFLAGS:-%optflags}" ; export CFLAGS make USE_PGXS=1 PG_CONFIG=%{pginstdir}/bin/pg_config %{?_smp_mflags} %install rm -rf %{buildroot} make install DESTDIR=%{buildroot} PG_CONFIG=%{pginstdir}/bin/pg_config %{?_smp_mflags} %clean rm -rf %{buildroot} %files %defattr(644,root,root,755) %doc README.md %{pginstdir}/lib/plpgsql_check.so %{pginstdir}/share/extension/plpgsql_check--2.10.sql %{pginstdir}/share/extension/plpgsql_check.control %{pginstdir}/lib/bitcode/*.bc %{pginstdir}/lib/bitcode/plpgsql_check/src/*.bc %{pginstdir}/share/extension/*.control %changelog * Wed Jul 15 2026 - Pavel Stehule 2.10.0 - possibility to pass pragma as an argument - possibility to generate pragma from code * Thu May 21 2026 - Pavel Stehule 2.9.0 - significant profiler refactoring * Tue Mar 18 2025 - Pavel Stehule 2.8.0 - remove support for PostgreSQL 12 and 13 * Wed Dec 6 2023 - Pavel Stehule 2.7.0 - unclosed cursors detection * Tue Oct 31 2023 - Pavel Stehule 2.6.0 - simple constant tracing support * Sat Apr 29 2023 - Pavel Stehule 2.4.0 - remove support for PostgreSQL 10 and 11 * Wed Jan 11 2023 - Pavel Stehule 2.3.0 - possibility to detect compatibility issues (obsolete setting of refcursor) * Tue Sep 20 2022 - Pavel Stehule 2.2.0 - possibility to use in comment options * Wed Dec 29 2021 - Pavel Stehule 2.1.0 - possibility to count statement's aborted execution - possibility to count "unfinished" statements due exception * Mon Sep 27 2021 - Pavel Stehule 2.0.0 - pragma type for setting type to record variable - pragma table for creating ephemeral table * Mon Jun 21 2021 - Pavel Stehule 1.17.0 - remove support for PostgreSQL 9.5 and 9.6 * Sat Mar 6 2021 - Pavel Stehule 1.16.0 - plpgsql_profiler_functions_all * Mon Nov 16 2020 - Pavel Stehule 1.14.0 - queryid can be displayed in profiler's reports (Julien Rouhaud) - new profiler's GUC plpgsql_check.profiler_max_shared_chunks (Julien Rouhaud) - few minor bugfixes * Fri Aug 14 2020 - Pavel Stehule 1.13.0 - tracer - pragma support to control checks, warnings and tracing * Thu Jul 2 2020 - Pavel STEHULE 1.11.0 - possibility to check functions with arguments of polymorphic type - possibility to specify type used as real type instead polymorphic type * Fri Jun 05 2020 - Pavel STEHULE 1.10.0 - deduction record type structure from result of polymorphic function * Mon Apr 27 2020 - Pavel STEHULE 1.9.1 - minor bugfixes * Mon Mar 30 2020 - Pavel STEHULE 1.9.0 - statement and branch coverage metrics - remove support for Postgres 9.4 * Mon Jan 06 2020 - Pavel STEHULE 1.8.2 - fix of compilation issue * Sun Jan 05 2020 - Pavel STEHULE 1.8.1 - cleaner detection function oid from name or signature * Sun Dec 29 2019 - Pavel STEHULE 1.8.0 - use Postgres tool for calling functions from plpgsql library instead dynamic linking - it solve issues related to dependency plpgsq_check on plpgsql * Mon Sep 23 2019 - Pavel STEHULE 1.7.6 - fix false alarm - multiple plans in EXECUTE statement, and possible crash * Tue Sep 10 2019 - Pavel STEHULE 1.7.5 - allow some work on tables with rules * Wed Jul 24 2019 - Pavel STEHULE 1.7.3 - profiler bugfixes * Tue May 21 2019 - Pavel STEHULE 1.7.2 - profiler bugfixes * Fri Apr 26 2019 - Pavel STEHULE 1.7.1 - bugfixes * Wed Apr 17 2019 - Pavel STEHULE 1.7.0 - check of format of fmt string of "format" function - better check of dynamic SQL when it is const string - check of SQL injection vulnerability of stmt expression at EXECUTE stmt * Sun Dec 23 2018 - Pavel STEHULE 1.4.2-1 - metada fix * Fri Dec 21 2018 - Pavel STEHULE 1.4.1-1 - minor bugfixes * Sun Dec 2 2018 - Pavel STEHULE 1.4.0-1 - possible to show function's dependency on functions and tables - integrated profiler - bug fixes (almost false alarms) * Wed Jun 6 2018 - Pavel STEHULE 1.2.3-1 - PostgreSQL 11 support - detect hidden casts in expressions * Thu Oct 26 2017 - Pavel STEHULE 1.2.2-1 - never read variables detection - fix false alarm on MOVE command * Fri Sep 15 2017 - Pavel STEHULE 1.2.1-1 - missing RETURN detection - fix some bugs and false alarms - PostgreSQL 11 support * Fri Nov 11 2016 - Pavel STEHULE 1.2.0-1 - support extra warnings - shadowed variables * Thu Aug 25 2016 - Pavel STEHULE 1.0.5-1 - minor fixes, support for PostgreSQL 10 * Fri Apr 15 2016 - Pavel STEHULE 1.0.4-1 - support for PostgreSQL 9.6 * Mon Oct 12 2015 - Pavel STEHULE 1.0.3-1 - fix false alarms of unused cursor variables - fix regress tests * Thu Jul 09 2015 - Pavel STEHULE 1.0.2-2 - bugfix release * Fri Dec 19 2014 - Pavel STEHULE 0.9.3-1 - fix a broken record field type checking - add check for assign to array field * Mon Aug 25 2014 - Pavel STEHULE 0.9.1-1 - Initial packaging okbob-plpgsql_check-800db79/postgresql16-plpgsql_check.spec000066400000000000000000000147241526744060500240460ustar00rootroot00000000000000%global pgmajorversion 16 %global pginstdir /usr/pgsql-16 %global sname plpgsql_check Name: %{sname}_%{pgmajorversion} Version: 2.10.16 Release: 1%{?dist} Summary: Additional tools for plpgsql functions validation Group: Applications/Databases License: BSD URL: https://github.com/okbob/plpgsql_check/archive/v%{version}.zip Source0: plpgsql_check-%{version}.zip BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root-%(%{__id_u} -n) BuildRequires: postgresql%{pgmajorversion}-devel Requires: postgresql%{pgmajorversion} %description The plpgsql_check is PostgreSQL extension with functionality for direct or indirect extra validation of functions in plpgsql language. It verifies a validity of SQL identifiers used in plpgsql code. It try to identify a performance issues. %prep %setup -q -n %{sname}-%{version} %build PATH="%{pginstdir}/bin;$PATH" ; export PATH CFLAGS="${CFLAGS:-%optflags}" ; export CFLAGS make USE_PGXS=1 PG_CONFIG=%{pginstdir}/bin/pg_config %{?_smp_mflags} %install rm -rf %{buildroot} make install DESTDIR=%{buildroot} PG_CONFIG=%{pginstdir}/bin/pg_config %{?_smp_mflags} %clean rm -rf %{buildroot} %files %defattr(644,root,root,755) %doc README.md %{pginstdir}/lib/plpgsql_check.so %{pginstdir}/share/extension/plpgsql_check--2.10.sql %{pginstdir}/share/extension/plpgsql_check.control %{pginstdir}/lib/bitcode/*.bc %{pginstdir}/lib/bitcode/plpgsql_check/src/*.bc %{pginstdir}/share/extension/*.control %changelog * Wed Jul 15 2026 - Pavel Stehule 2.10.0 - possibility to pass pragma as an argument - possibility to generate pragma from code * Thu May 21 2026 - Pavel Stehule 2.9.0 - significant profiler refactoring * Tue Mar 18 2025 - Pavel Stehule 2.8.0 - remove support for PostgreSQL 12 and 13 * Wed Dec 6 2023 - Pavel Stehule 2.7.0 - unclosed cursors detection * Tue Oct 31 2023 - Pavel Stehule 2.6.0 - simple constant tracing support * Sat Apr 29 2023 - Pavel Stehule 2.4.0 - remove support for PostgreSQL 10 and 11 * Wed Jan 11 2023 - Pavel Stehule 2.3.0 - possibility to detect compatibility issues (obsolete setting of refcursor) * Tue Sep 20 2022 - Pavel Stehule 2.2.0 - possibility to use in comment options * Wed Dec 29 2021 - Pavel Stehule 2.1.0 - possibility to count statement's aborted execution - possibility to count "unfinished" statements due exception * Mon Sep 27 2021 - Pavel Stehule 2.0.0 - pragma type for setting type to record variable - pragma table for creating ephemeral table * Mon Jun 21 2021 - Pavel Stehule 1.17.0 - remove support for PostgreSQL 9.5 and 9.6 * Sat Mar 6 2021 - Pavel Stehule 1.16.0 - plpgsql_profiler_functions_all * Mon Nov 16 2020 - Pavel Stehule 1.14.0 - queryid can be displayed in profiler's reports (Julien Rouhaud) - new profiler's GUC plpgsql_check.profiler_max_shared_chunks (Julien Rouhaud) - few minor bugfixes * Fri Aug 14 2020 - Pavel Stehule 1.13.0 - tracer - pragma support to control checks, warnings and tracing * Thu Jul 2 2020 - Pavel STEHULE 1.11.0 - possibility to check functions with arguments of polymorphic type - possibility to specify type used as real type instead polymorphic type * Fri Jun 05 2020 - Pavel STEHULE 1.10.0 - deduction record type structure from result of polymorphic function * Mon Apr 27 2020 - Pavel STEHULE 1.9.1 - minor bugfixes * Mon Mar 30 2020 - Pavel STEHULE 1.9.0 - statement and branch coverage metrics - remove support for Postgres 9.4 * Mon Jan 06 2020 - Pavel STEHULE 1.8.2 - fix of compilation issue * Sun Jan 05 2020 - Pavel STEHULE 1.8.1 - cleaner detection function oid from name or signature * Sun Dec 29 2019 - Pavel STEHULE 1.8.0 - use Postgres tool for calling functions from plpgsql library instead dynamic linking - it solve issues related to dependency plpgsq_check on plpgsql * Mon Sep 23 2019 - Pavel STEHULE 1.7.6 - fix false alarm - multiple plans in EXECUTE statement, and possible crash * Tue Sep 10 2019 - Pavel STEHULE 1.7.5 - allow some work on tables with rules * Wed Jul 24 2019 - Pavel STEHULE 1.7.3 - profiler bugfixes * Tue May 21 2019 - Pavel STEHULE 1.7.2 - profiler bugfixes * Fri Apr 26 2019 - Pavel STEHULE 1.7.1 - bugfixes * Wed Apr 17 2019 - Pavel STEHULE 1.7.0 - check of format of fmt string of "format" function - better check of dynamic SQL when it is const string - check of SQL injection vulnerability of stmt expression at EXECUTE stmt * Sun Dec 23 2018 - Pavel STEHULE 1.4.2-1 - metada fix * Fri Dec 21 2018 - Pavel STEHULE 1.4.1-1 - minor bugfixes * Sun Dec 2 2018 - Pavel STEHULE 1.4.0-1 - possible to show function's dependency on functions and tables - integrated profiler - bug fixes (almost false alarms) * Wed Jun 6 2018 - Pavel STEHULE 1.2.3-1 - PostgreSQL 11 support - detect hidden casts in expressions * Thu Oct 26 2017 - Pavel STEHULE 1.2.2-1 - never read variables detection - fix false alarm on MOVE command * Fri Sep 15 2017 - Pavel STEHULE 1.2.1-1 - missing RETURN detection - fix some bugs and false alarms - PostgreSQL 11 support * Fri Nov 11 2016 - Pavel STEHULE 1.2.0-1 - support extra warnings - shadowed variables * Thu Aug 25 2016 - Pavel STEHULE 1.0.5-1 - minor fixes, support for PostgreSQL 10 * Fri Apr 15 2016 - Pavel STEHULE 1.0.4-1 - support for PostgreSQL 9.6 * Mon Oct 12 2015 - Pavel STEHULE 1.0.3-1 - fix false alarms of unused cursor variables - fix regress tests * Thu Jul 09 2015 - Pavel STEHULE 1.0.2-2 - bugfix release * Fri Dec 19 2014 - Pavel STEHULE 0.9.3-1 - fix a broken record field type checking - add check for assign to array field * Mon Aug 25 2014 - Pavel STEHULE 0.9.1-1 - Initial packaging okbob-plpgsql_check-800db79/postgresql17-plpgsql_check.spec000066400000000000000000000147241526744060500240470ustar00rootroot00000000000000%global pgmajorversion 17 %global pginstdir /usr/pgsql-17 %global sname plpgsql_check Name: %{sname}_%{pgmajorversion} Version: 2.10.16 Release: 1%{?dist} Summary: Additional tools for plpgsql functions validation Group: Applications/Databases License: BSD URL: https://github.com/okbob/plpgsql_check/archive/v%{version}.zip Source0: plpgsql_check-%{version}.zip BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root-%(%{__id_u} -n) BuildRequires: postgresql%{pgmajorversion}-devel Requires: postgresql%{pgmajorversion} %description The plpgsql_check is PostgreSQL extension with functionality for direct or indirect extra validation of functions in plpgsql language. It verifies a validity of SQL identifiers used in plpgsql code. It try to identify a performance issues. %prep %setup -q -n %{sname}-%{version} %build PATH="%{pginstdir}/bin;$PATH" ; export PATH CFLAGS="${CFLAGS:-%optflags}" ; export CFLAGS make USE_PGXS=1 PG_CONFIG=%{pginstdir}/bin/pg_config %{?_smp_mflags} %install rm -rf %{buildroot} make install DESTDIR=%{buildroot} PG_CONFIG=%{pginstdir}/bin/pg_config %{?_smp_mflags} %clean rm -rf %{buildroot} %files %defattr(644,root,root,755) %doc README.md %{pginstdir}/lib/plpgsql_check.so %{pginstdir}/share/extension/plpgsql_check--2.10.sql %{pginstdir}/share/extension/plpgsql_check.control %{pginstdir}/lib/bitcode/*.bc %{pginstdir}/lib/bitcode/plpgsql_check/src/*.bc %{pginstdir}/share/extension/*.control %changelog * Wed Jul 15 2026 - Pavel Stehule 2.10.0 - possibility to pass pragma as an argument - possibility to generate pragma from code * Thu May 21 2026 - Pavel Stehule 2.9.0 - significant profiler refactoring * Tue Mar 18 2025 - Pavel Stehule 2.8.0 - remove support for PostgreSQL 12 and 13 * Wed Dec 6 2023 - Pavel Stehule 2.7.0 - unclosed cursors detection * Tue Oct 31 2023 - Pavel Stehule 2.6.0 - simple constant tracing support * Sat Apr 29 2023 - Pavel Stehule 2.4.0 - remove support for PostgreSQL 10 and 11 * Wed Jan 11 2023 - Pavel Stehule 2.3.0 - possibility to detect compatibility issues (obsolete setting of refcursor) * Tue Sep 20 2022 - Pavel Stehule 2.2.0 - possibility to use in comment options * Wed Dec 29 2021 - Pavel Stehule 2.1.0 - possibility to count statement's aborted execution - possibility to count "unfinished" statements due exception * Mon Sep 27 2021 - Pavel Stehule 2.0.0 - pragma type for setting type to record variable - pragma table for creating ephemeral table * Mon Jun 21 2021 - Pavel Stehule 1.17.0 - remove support for PostgreSQL 9.5 and 9.6 * Sat Mar 6 2021 - Pavel Stehule 1.16.0 - plpgsql_profiler_functions_all * Mon Nov 16 2020 - Pavel Stehule 1.14.0 - queryid can be displayed in profiler's reports (Julien Rouhaud) - new profiler's GUC plpgsql_check.profiler_max_shared_chunks (Julien Rouhaud) - few minor bugfixes * Fri Aug 14 2020 - Pavel Stehule 1.13.0 - tracer - pragma support to control checks, warnings and tracing * Thu Jul 2 2020 - Pavel STEHULE 1.11.0 - possibility to check functions with arguments of polymorphic type - possibility to specify type used as real type instead polymorphic type * Fri Jun 05 2020 - Pavel STEHULE 1.10.0 - deduction record type structure from result of polymorphic function * Mon Apr 27 2020 - Pavel STEHULE 1.9.1 - minor bugfixes * Mon Mar 30 2020 - Pavel STEHULE 1.9.0 - statement and branch coverage metrics - remove support for Postgres 9.4 * Mon Jan 06 2020 - Pavel STEHULE 1.8.2 - fix of compilation issue * Sun Jan 05 2020 - Pavel STEHULE 1.8.1 - cleaner detection function oid from name or signature * Sun Dec 29 2019 - Pavel STEHULE 1.8.0 - use Postgres tool for calling functions from plpgsql library instead dynamic linking - it solve issues related to dependency plpgsq_check on plpgsql * Mon Sep 23 2019 - Pavel STEHULE 1.7.6 - fix false alarm - multiple plans in EXECUTE statement, and possible crash * Tue Sep 10 2019 - Pavel STEHULE 1.7.5 - allow some work on tables with rules * Wed Jul 24 2019 - Pavel STEHULE 1.7.3 - profiler bugfixes * Tue May 21 2019 - Pavel STEHULE 1.7.2 - profiler bugfixes * Fri Apr 26 2019 - Pavel STEHULE 1.7.1 - bugfixes * Wed Apr 17 2019 - Pavel STEHULE 1.7.0 - check of format of fmt string of "format" function - better check of dynamic SQL when it is const string - check of SQL injection vulnerability of stmt expression at EXECUTE stmt * Sun Dec 23 2018 - Pavel STEHULE 1.4.2-1 - metada fix * Fri Dec 21 2018 - Pavel STEHULE 1.4.1-1 - minor bugfixes * Sun Dec 2 2018 - Pavel STEHULE 1.4.0-1 - possible to show function's dependency on functions and tables - integrated profiler - bug fixes (almost false alarms) * Wed Jun 6 2018 - Pavel STEHULE 1.2.3-1 - PostgreSQL 11 support - detect hidden casts in expressions * Thu Oct 26 2017 - Pavel STEHULE 1.2.2-1 - never read variables detection - fix false alarm on MOVE command * Fri Sep 15 2017 - Pavel STEHULE 1.2.1-1 - missing RETURN detection - fix some bugs and false alarms - PostgreSQL 11 support * Fri Nov 11 2016 - Pavel STEHULE 1.2.0-1 - support extra warnings - shadowed variables * Thu Aug 25 2016 - Pavel STEHULE 1.0.5-1 - minor fixes, support for PostgreSQL 10 * Fri Apr 15 2016 - Pavel STEHULE 1.0.4-1 - support for PostgreSQL 9.6 * Mon Oct 12 2015 - Pavel STEHULE 1.0.3-1 - fix false alarms of unused cursor variables - fix regress tests * Thu Jul 09 2015 - Pavel STEHULE 1.0.2-2 - bugfix release * Fri Dec 19 2014 - Pavel STEHULE 0.9.3-1 - fix a broken record field type checking - add check for assign to array field * Mon Aug 25 2014 - Pavel STEHULE 0.9.1-1 - Initial packaging okbob-plpgsql_check-800db79/postgresql18-plpgsql_check.spec000066400000000000000000000147241526744060500240500ustar00rootroot00000000000000%global pgmajorversion 18 %global pginstdir /usr/pgsql-18 %global sname plpgsql_check Name: %{sname}_%{pgmajorversion} Version: 2.10.16 Release: 1%{?dist} Summary: Additional tools for plpgsql functions validation Group: Applications/Databases License: BSD URL: https://github.com/okbob/plpgsql_check/archive/v%{version}.zip Source0: plpgsql_check-%{version}.zip BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root-%(%{__id_u} -n) BuildRequires: postgresql%{pgmajorversion}-devel Requires: postgresql%{pgmajorversion} %description The plpgsql_check is PostgreSQL extension with functionality for direct or indirect extra validation of functions in plpgsql language. It verifies a validity of SQL identifiers used in plpgsql code. It try to identify a performance issues. %prep %setup -q -n %{sname}-%{version} %build PATH="%{pginstdir}/bin;$PATH" ; export PATH CFLAGS="${CFLAGS:-%optflags}" ; export CFLAGS make USE_PGXS=1 PG_CONFIG=%{pginstdir}/bin/pg_config %{?_smp_mflags} %install rm -rf %{buildroot} make install DESTDIR=%{buildroot} PG_CONFIG=%{pginstdir}/bin/pg_config %{?_smp_mflags} %clean rm -rf %{buildroot} %files %defattr(644,root,root,755) %doc README.md %{pginstdir}/lib/plpgsql_check.so %{pginstdir}/share/extension/plpgsql_check--2.10.sql %{pginstdir}/share/extension/plpgsql_check.control %{pginstdir}/lib/bitcode/*.bc %{pginstdir}/lib/bitcode/plpgsql_check/src/*.bc %{pginstdir}/share/extension/*.control %changelog * Wed Jul 15 2026 - Pavel Stehule 2.10.0 - possibility to pass pragma as an argument - possibility to generate pragma from code * Thu May 21 2026 - Pavel Stehule 2.9.0 - significant profiler refactoring * Tue Mar 18 2025 - Pavel Stehule 2.8.0 - remove support for PostgreSQL 12 and 13 * Wed Dec 6 2023 - Pavel Stehule 2.7.0 - unclosed cursors detection * Tue Oct 31 2023 - Pavel Stehule 2.6.0 - simple constant tracing support * Sat Apr 29 2023 - Pavel Stehule 2.4.0 - remove support for PostgreSQL 10 and 11 * Wed Jan 11 2023 - Pavel Stehule 2.3.0 - possibility to detect compatibility issues (obsolete setting of refcursor) * Tue Sep 20 2022 - Pavel Stehule 2.2.0 - possibility to use in comment options * Wed Dec 29 2021 - Pavel Stehule 2.1.0 - possibility to count statement's aborted execution - possibility to count "unfinished" statements due exception * Mon Sep 27 2021 - Pavel Stehule 2.0.0 - pragma type for setting type to record variable - pragma table for creating ephemeral table * Mon Jun 21 2021 - Pavel Stehule 1.17.0 - remove support for PostgreSQL 9.5 and 9.6 * Sat Mar 6 2021 - Pavel Stehule 1.16.0 - plpgsql_profiler_functions_all * Mon Nov 16 2020 - Pavel Stehule 1.14.0 - queryid can be displayed in profiler's reports (Julien Rouhaud) - new profiler's GUC plpgsql_check.profiler_max_shared_chunks (Julien Rouhaud) - few minor bugfixes * Fri Aug 14 2020 - Pavel Stehule 1.13.0 - tracer - pragma support to control checks, warnings and tracing * Thu Jul 2 2020 - Pavel STEHULE 1.11.0 - possibility to check functions with arguments of polymorphic type - possibility to specify type used as real type instead polymorphic type * Fri Jun 05 2020 - Pavel STEHULE 1.10.0 - deduction record type structure from result of polymorphic function * Mon Apr 27 2020 - Pavel STEHULE 1.9.1 - minor bugfixes * Mon Mar 30 2020 - Pavel STEHULE 1.9.0 - statement and branch coverage metrics - remove support for Postgres 9.4 * Mon Jan 06 2020 - Pavel STEHULE 1.8.2 - fix of compilation issue * Sun Jan 05 2020 - Pavel STEHULE 1.8.1 - cleaner detection function oid from name or signature * Sun Dec 29 2019 - Pavel STEHULE 1.8.0 - use Postgres tool for calling functions from plpgsql library instead dynamic linking - it solve issues related to dependency plpgsq_check on plpgsql * Mon Sep 23 2019 - Pavel STEHULE 1.7.6 - fix false alarm - multiple plans in EXECUTE statement, and possible crash * Tue Sep 10 2019 - Pavel STEHULE 1.7.5 - allow some work on tables with rules * Wed Jul 24 2019 - Pavel STEHULE 1.7.3 - profiler bugfixes * Tue May 21 2019 - Pavel STEHULE 1.7.2 - profiler bugfixes * Fri Apr 26 2019 - Pavel STEHULE 1.7.1 - bugfixes * Wed Apr 17 2019 - Pavel STEHULE 1.7.0 - check of format of fmt string of "format" function - better check of dynamic SQL when it is const string - check of SQL injection vulnerability of stmt expression at EXECUTE stmt * Sun Dec 23 2018 - Pavel STEHULE 1.4.2-1 - metada fix * Fri Dec 21 2018 - Pavel STEHULE 1.4.1-1 - minor bugfixes * Sun Dec 2 2018 - Pavel STEHULE 1.4.0-1 - possible to show function's dependency on functions and tables - integrated profiler - bug fixes (almost false alarms) * Wed Jun 6 2018 - Pavel STEHULE 1.2.3-1 - PostgreSQL 11 support - detect hidden casts in expressions * Thu Oct 26 2017 - Pavel STEHULE 1.2.2-1 - never read variables detection - fix false alarm on MOVE command * Fri Sep 15 2017 - Pavel STEHULE 1.2.1-1 - missing RETURN detection - fix some bugs and false alarms - PostgreSQL 11 support * Fri Nov 11 2016 - Pavel STEHULE 1.2.0-1 - support extra warnings - shadowed variables * Thu Aug 25 2016 - Pavel STEHULE 1.0.5-1 - minor fixes, support for PostgreSQL 10 * Fri Apr 15 2016 - Pavel STEHULE 1.0.4-1 - support for PostgreSQL 9.6 * Mon Oct 12 2015 - Pavel STEHULE 1.0.3-1 - fix false alarms of unused cursor variables - fix regress tests * Thu Jul 09 2015 - Pavel STEHULE 1.0.2-2 - bugfix release * Fri Dec 19 2014 - Pavel STEHULE 0.9.3-1 - fix a broken record field type checking - add check for assign to array field * Mon Aug 25 2014 - Pavel STEHULE 0.9.1-1 - Initial packaging okbob-plpgsql_check-800db79/postgresql19-plpgsql_check.spec000066400000000000000000000147241526744060500240510ustar00rootroot00000000000000%global pgmajorversion 19 %global pginstdir /usr/pgsql-19 %global sname plpgsql_check Name: %{sname}_%{pgmajorversion} Version: 2.10.16 Release: 1%{?dist} Summary: Additional tools for plpgsql functions validation Group: Applications/Databases License: BSD URL: https://github.com/okbob/plpgsql_check/archive/v%{version}.zip Source0: plpgsql_check-%{version}.zip BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root-%(%{__id_u} -n) BuildRequires: postgresql%{pgmajorversion}-devel Requires: postgresql%{pgmajorversion} %description The plpgsql_check is PostgreSQL extension with functionality for direct or indirect extra validation of functions in plpgsql language. It verifies a validity of SQL identifiers used in plpgsql code. It try to identify a performance issues. %prep %setup -q -n %{sname}-%{version} %build PATH="%{pginstdir}/bin;$PATH" ; export PATH CFLAGS="${CFLAGS:-%optflags}" ; export CFLAGS make USE_PGXS=1 PG_CONFIG=%{pginstdir}/bin/pg_config %{?_smp_mflags} %install rm -rf %{buildroot} make install DESTDIR=%{buildroot} PG_CONFIG=%{pginstdir}/bin/pg_config %{?_smp_mflags} %clean rm -rf %{buildroot} %files %defattr(644,root,root,755) %doc README.md %{pginstdir}/lib/plpgsql_check.so %{pginstdir}/share/extension/plpgsql_check--2.10.sql %{pginstdir}/share/extension/plpgsql_check.control %{pginstdir}/lib/bitcode/*.bc %{pginstdir}/lib/bitcode/plpgsql_check/src/*.bc %{pginstdir}/share/extension/*.control %changelog * Wed Jul 15 2026 - Pavel Stehule 2.10.0 - possibility to pass pragma as an argument - possibility to generate pragma from code * Thu May 21 2026 - Pavel Stehule 2.9.0 - significant profiler refactoring * Tue Mar 18 2025 - Pavel Stehule 2.8.0 - remove support for PostgreSQL 12 and 13 * Wed Dec 6 2023 - Pavel Stehule 2.7.0 - unclosed cursors detection * Tue Oct 31 2023 - Pavel Stehule 2.6.0 - simple constant tracing support * Sat Apr 29 2023 - Pavel Stehule 2.4.0 - remove support for PostgreSQL 10 and 11 * Wed Jan 11 2023 - Pavel Stehule 2.3.0 - possibility to detect compatibility issues (obsolete setting of refcursor) * Tue Sep 20 2022 - Pavel Stehule 2.2.0 - possibility to use in comment options * Wed Dec 29 2021 - Pavel Stehule 2.1.0 - possibility to count statement's aborted execution - possibility to count "unfinished" statements due exception * Mon Sep 27 2021 - Pavel Stehule 2.0.0 - pragma type for setting type to record variable - pragma table for creating ephemeral table * Mon Jun 21 2021 - Pavel Stehule 1.17.0 - remove support for PostgreSQL 9.5 and 9.6 * Sat Mar 6 2021 - Pavel Stehule 1.16.0 - plpgsql_profiler_functions_all * Mon Nov 16 2020 - Pavel Stehule 1.14.0 - queryid can be displayed in profiler's reports (Julien Rouhaud) - new profiler's GUC plpgsql_check.profiler_max_shared_chunks (Julien Rouhaud) - few minor bugfixes * Fri Aug 14 2020 - Pavel Stehule 1.13.0 - tracer - pragma support to control checks, warnings and tracing * Thu Jul 2 2020 - Pavel STEHULE 1.11.0 - possibility to check functions with arguments of polymorphic type - possibility to specify type used as real type instead polymorphic type * Fri Jun 05 2020 - Pavel STEHULE 1.10.0 - deduction record type structure from result of polymorphic function * Mon Apr 27 2020 - Pavel STEHULE 1.9.1 - minor bugfixes * Mon Mar 30 2020 - Pavel STEHULE 1.9.0 - statement and branch coverage metrics - remove support for Postgres 9.4 * Mon Jan 06 2020 - Pavel STEHULE 1.8.2 - fix of compilation issue * Sun Jan 05 2020 - Pavel STEHULE 1.8.1 - cleaner detection function oid from name or signature * Sun Dec 29 2019 - Pavel STEHULE 1.8.0 - use Postgres tool for calling functions from plpgsql library instead dynamic linking - it solve issues related to dependency plpgsq_check on plpgsql * Mon Sep 23 2019 - Pavel STEHULE 1.7.6 - fix false alarm - multiple plans in EXECUTE statement, and possible crash * Tue Sep 10 2019 - Pavel STEHULE 1.7.5 - allow some work on tables with rules * Wed Jul 24 2019 - Pavel STEHULE 1.7.3 - profiler bugfixes * Tue May 21 2019 - Pavel STEHULE 1.7.2 - profiler bugfixes * Fri Apr 26 2019 - Pavel STEHULE 1.7.1 - bugfixes * Wed Apr 17 2019 - Pavel STEHULE 1.7.0 - check of format of fmt string of "format" function - better check of dynamic SQL when it is const string - check of SQL injection vulnerability of stmt expression at EXECUTE stmt * Sun Dec 23 2018 - Pavel STEHULE 1.4.2-1 - metada fix * Fri Dec 21 2018 - Pavel STEHULE 1.4.1-1 - minor bugfixes * Sun Dec 2 2018 - Pavel STEHULE 1.4.0-1 - possible to show function's dependency on functions and tables - integrated profiler - bug fixes (almost false alarms) * Wed Jun 6 2018 - Pavel STEHULE 1.2.3-1 - PostgreSQL 11 support - detect hidden casts in expressions * Thu Oct 26 2017 - Pavel STEHULE 1.2.2-1 - never read variables detection - fix false alarm on MOVE command * Fri Sep 15 2017 - Pavel STEHULE 1.2.1-1 - missing RETURN detection - fix some bugs and false alarms - PostgreSQL 11 support * Fri Nov 11 2016 - Pavel STEHULE 1.2.0-1 - support extra warnings - shadowed variables * Thu Aug 25 2016 - Pavel STEHULE 1.0.5-1 - minor fixes, support for PostgreSQL 10 * Fri Apr 15 2016 - Pavel STEHULE 1.0.4-1 - support for PostgreSQL 9.6 * Mon Oct 12 2015 - Pavel STEHULE 1.0.3-1 - fix false alarms of unused cursor variables - fix regress tests * Thu Jul 09 2015 - Pavel STEHULE 1.0.2-2 - bugfix release * Fri Dec 19 2014 - Pavel STEHULE 0.9.3-1 - fix a broken record field type checking - add check for assign to array field * Mon Aug 25 2014 - Pavel STEHULE 0.9.1-1 - Initial packaging okbob-plpgsql_check-800db79/postgresql20-plpgsql_check.spec000066400000000000000000000147241526744060500240410ustar00rootroot00000000000000%global pgmajorversion 20 %global pginstdir /usr/pgsql-20 %global sname plpgsql_check Name: %{sname}_%{pgmajorversion} Version: 2.10.16 Release: 1%{?dist} Summary: Additional tools for plpgsql functions validation Group: Applications/Databases License: BSD URL: https://github.com/okbob/plpgsql_check/archive/v%{version}.zip Source0: plpgsql_check-%{version}.zip BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root-%(%{__id_u} -n) BuildRequires: postgresql%{pgmajorversion}-devel Requires: postgresql%{pgmajorversion} %description The plpgsql_check is PostgreSQL extension with functionality for direct or indirect extra validation of functions in plpgsql language. It verifies a validity of SQL identifiers used in plpgsql code. It try to identify a performance issues. %prep %setup -q -n %{sname}-%{version} %build PATH="%{pginstdir}/bin;$PATH" ; export PATH CFLAGS="${CFLAGS:-%optflags}" ; export CFLAGS make USE_PGXS=1 PG_CONFIG=%{pginstdir}/bin/pg_config %{?_smp_mflags} %install rm -rf %{buildroot} make install DESTDIR=%{buildroot} PG_CONFIG=%{pginstdir}/bin/pg_config %{?_smp_mflags} %clean rm -rf %{buildroot} %files %defattr(644,root,root,755) %doc README.md %{pginstdir}/lib/plpgsql_check.so %{pginstdir}/share/extension/plpgsql_check--2.10.sql %{pginstdir}/share/extension/plpgsql_check.control %{pginstdir}/lib/bitcode/*.bc %{pginstdir}/lib/bitcode/plpgsql_check/src/*.bc %{pginstdir}/share/extension/*.control %changelog * Wed Jul 15 2026 - Pavel Stehule 2.10.0 - possibility to pass pragma as an argument - possibility to generate pragma from code * Thu May 21 2026 - Pavel Stehule 2.9.0 - significant profiler refactoring * Tue Mar 18 2025 - Pavel Stehule 2.8.0 - remove support for PostgreSQL 12 and 13 * Wed Dec 6 2023 - Pavel Stehule 2.7.0 - unclosed cursors detection * Tue Oct 31 2023 - Pavel Stehule 2.6.0 - simple constant tracing support * Sat Apr 29 2023 - Pavel Stehule 2.4.0 - remove support for PostgreSQL 10 and 11 * Wed Jan 11 2023 - Pavel Stehule 2.3.0 - possibility to detect compatibility issues (obsolete setting of refcursor) * Tue Sep 20 2022 - Pavel Stehule 2.2.0 - possibility to use in comment options * Wed Dec 29 2021 - Pavel Stehule 2.1.0 - possibility to count statement's aborted execution - possibility to count "unfinished" statements due exception * Mon Sep 27 2021 - Pavel Stehule 2.0.0 - pragma type for setting type to record variable - pragma table for creating ephemeral table * Mon Jun 21 2021 - Pavel Stehule 1.17.0 - remove support for PostgreSQL 9.5 and 9.6 * Sat Mar 6 2021 - Pavel Stehule 1.16.0 - plpgsql_profiler_functions_all * Mon Nov 16 2020 - Pavel Stehule 1.14.0 - queryid can be displayed in profiler's reports (Julien Rouhaud) - new profiler's GUC plpgsql_check.profiler_max_shared_chunks (Julien Rouhaud) - few minor bugfixes * Fri Aug 14 2020 - Pavel Stehule 1.13.0 - tracer - pragma support to control checks, warnings and tracing * Thu Jul 2 2020 - Pavel STEHULE 1.11.0 - possibility to check functions with arguments of polymorphic type - possibility to specify type used as real type instead polymorphic type * Fri Jun 05 2020 - Pavel STEHULE 1.10.0 - deduction record type structure from result of polymorphic function * Mon Apr 27 2020 - Pavel STEHULE 1.9.1 - minor bugfixes * Mon Mar 30 2020 - Pavel STEHULE 1.9.0 - statement and branch coverage metrics - remove support for Postgres 9.4 * Mon Jan 06 2020 - Pavel STEHULE 1.8.2 - fix of compilation issue * Sun Jan 05 2020 - Pavel STEHULE 1.8.1 - cleaner detection function oid from name or signature * Sun Dec 29 2019 - Pavel STEHULE 1.8.0 - use Postgres tool for calling functions from plpgsql library instead dynamic linking - it solve issues related to dependency plpgsq_check on plpgsql * Mon Sep 23 2019 - Pavel STEHULE 1.7.6 - fix false alarm - multiple plans in EXECUTE statement, and possible crash * Tue Sep 10 2019 - Pavel STEHULE 1.7.5 - allow some work on tables with rules * Wed Jul 24 2019 - Pavel STEHULE 1.7.3 - profiler bugfixes * Tue May 21 2019 - Pavel STEHULE 1.7.2 - profiler bugfixes * Fri Apr 26 2019 - Pavel STEHULE 1.7.1 - bugfixes * Wed Apr 17 2019 - Pavel STEHULE 1.7.0 - check of format of fmt string of "format" function - better check of dynamic SQL when it is const string - check of SQL injection vulnerability of stmt expression at EXECUTE stmt * Sun Dec 23 2018 - Pavel STEHULE 1.4.2-1 - metada fix * Fri Dec 21 2018 - Pavel STEHULE 1.4.1-1 - minor bugfixes * Sun Dec 2 2018 - Pavel STEHULE 1.4.0-1 - possible to show function's dependency on functions and tables - integrated profiler - bug fixes (almost false alarms) * Wed Jun 6 2018 - Pavel STEHULE 1.2.3-1 - PostgreSQL 11 support - detect hidden casts in expressions * Thu Oct 26 2017 - Pavel STEHULE 1.2.2-1 - never read variables detection - fix false alarm on MOVE command * Fri Sep 15 2017 - Pavel STEHULE 1.2.1-1 - missing RETURN detection - fix some bugs and false alarms - PostgreSQL 11 support * Fri Nov 11 2016 - Pavel STEHULE 1.2.0-1 - support extra warnings - shadowed variables * Thu Aug 25 2016 - Pavel STEHULE 1.0.5-1 - minor fixes, support for PostgreSQL 10 * Fri Apr 15 2016 - Pavel STEHULE 1.0.4-1 - support for PostgreSQL 9.6 * Mon Oct 12 2015 - Pavel STEHULE 1.0.3-1 - fix false alarms of unused cursor variables - fix regress tests * Thu Jul 09 2015 - Pavel STEHULE 1.0.2-2 - bugfix release * Fri Dec 19 2014 - Pavel STEHULE 0.9.3-1 - fix a broken record field type checking - add check for assign to array field * Mon Aug 25 2014 - Pavel STEHULE 0.9.1-1 - Initial packaging okbob-plpgsql_check-800db79/sql/000077500000000000000000000000001526744060500165725ustar00rootroot00000000000000okbob-plpgsql_check-800db79/sql/plpgsql_check_active-14.sql000066400000000000000000000000001526744060500236750ustar00rootroot00000000000000okbob-plpgsql_check-800db79/sql/plpgsql_check_active-15.sql000066400000000000000000000036161526744060500237160ustar00rootroot00000000000000LOAD 'plpgsql'; CREATE EXTENSION IF NOT EXISTS plpgsql_check; set client_min_messages to notice; create or replace function fxtest() returns void as $$ declare v_sqlstate text; v_message text; v_context text; begin get stacked diagnostics v_sqlstate = returned_sqlstate, v_message = message_text, v_context = pg_exception_context; end; $$ language plpgsql; select * from plpgsql_check_function('fxtest'); drop function fxtest(); create or replace procedure prtest() as $$ begin commit; end; $$ language plpgsql; select * from plpgsql_check_function('prtest'); --ok create or replace procedure prtest() as $$ begin begin begin commit; end; end; exception when others then raise; end; $$ language plpgsql; select * from plpgsql_check_function('prtest'); --error create or replace procedure prtest() as $$ begin raise exception 'error'; exception when others then begin begin commit; end; end; end; $$ language plpgsql; select * from plpgsql_check_function('prtest'); --ok drop procedure prtest(); create function return_constant_refcursor() returns refcursor as $$ declare rc constant refcursor; begin open rc for select a from rc_test; return rc; end $$ language plpgsql; create table rc_test(a int); select * from plpgsql_check_function('return_constant_refcursor'); drop table rc_test; drop function return_constant_refcursor(); create procedure p1(a int, out b int) as $$ begin b := a + 10; end; $$ language plpgsql; create function f1() returns void as $$ declare b constant int; begin call p1(10, b); end; $$ language plpgsql; select * from plpgsql_check_function('f1'); drop function f1(); drop procedure p1(int, int); create or replace function f1() returns int as $$ declare c constant int default 100; begin return c; end; $$ language plpgsql; -- should be ok select * from plpgsql_check_function('f1'); drop function f1(); okbob-plpgsql_check-800db79/sql/plpgsql_check_active-16.sql000066400000000000000000000056671526744060500237270ustar00rootroot00000000000000LOAD 'plpgsql'; CREATE EXTENSION IF NOT EXISTS plpgsql_check; set client_min_messages to notice; create or replace function fxtest() returns void as $$ declare v_sqlstate text; v_message text; v_context text; begin get stacked diagnostics v_sqlstate = returned_sqlstate, v_message = message_text, v_context = pg_exception_context; end; $$ language plpgsql; select * from plpgsql_check_function('fxtest'); drop function fxtest(); create or replace procedure prtest() as $$ begin commit; end; $$ language plpgsql; select * from plpgsql_check_function('prtest'); --ok create or replace procedure prtest() as $$ begin begin begin commit; end; end; exception when others then raise; end; $$ language plpgsql; select * from plpgsql_check_function('prtest'); --error create or replace procedure prtest() as $$ begin raise exception 'error'; exception when others then begin begin commit; end; end; end; $$ language plpgsql; select * from plpgsql_check_function('prtest'); --ok drop procedure prtest(); create function return_constant_refcursor() returns refcursor as $$ declare rc constant refcursor; begin open rc for select a from rc_test; return rc; end $$ language plpgsql; create table rc_test(a int); select * from plpgsql_check_function('return_constant_refcursor'); drop table rc_test; drop function return_constant_refcursor(); create procedure p1(a int, out b int) as $$ begin b := a + 10; end; $$ language plpgsql; create function f1() returns void as $$ declare b constant int; begin call p1(10, b); end; $$ language plpgsql; select * from plpgsql_check_function('f1'); drop function f1(); drop procedure p1(int, int); create or replace function f1() returns int as $$ declare c constant int default 100; begin return c; end; $$ language plpgsql; -- should be ok select * from plpgsql_check_function('f1'); drop function f1(); -- do not raise false warning create or replace function test_function() returns text as $$ declare s text; begin get diagnostics s = PG_CONTEXT; return s; end; $$ language plpgsql; create or replace procedure test_procedure() as $$ begin null; end; $$ language plpgsql; -- should be without any warnings select * from plpgsql_check_function('test_function', performance_warnings=>true); select * from plpgsql_check_function('test_procedure', performance_warnings=>true); drop function test_function(); drop procedure test_procedure(); -- detect dependecy in CALL statement create or replace function fx1_dep(int) returns int as $$ begin return $1; end; $$ language plpgsql; create or replace procedure px1_dep(int) as $$ begin end; $$ language plpgsql; create or replace function test_function() returns void as $$ begin call px1_dep(fx1_dep(10)); end; $$ language plpgsql; select type, schema, name, params from plpgsql_show_dependency_tb('test_function'); drop function test_function(); drop procedure px1_dep(int); drop function fx1_dep(int);okbob-plpgsql_check-800db79/sql/plpgsql_check_active-17.sql000066400000000000000000000056671526744060500237300ustar00rootroot00000000000000LOAD 'plpgsql'; CREATE EXTENSION IF NOT EXISTS plpgsql_check; set client_min_messages to notice; create or replace function fxtest() returns void as $$ declare v_sqlstate text; v_message text; v_context text; begin get stacked diagnostics v_sqlstate = returned_sqlstate, v_message = message_text, v_context = pg_exception_context; end; $$ language plpgsql; select * from plpgsql_check_function('fxtest'); drop function fxtest(); create or replace procedure prtest() as $$ begin commit; end; $$ language plpgsql; select * from plpgsql_check_function('prtest'); --ok create or replace procedure prtest() as $$ begin begin begin commit; end; end; exception when others then raise; end; $$ language plpgsql; select * from plpgsql_check_function('prtest'); --error create or replace procedure prtest() as $$ begin raise exception 'error'; exception when others then begin begin commit; end; end; end; $$ language plpgsql; select * from plpgsql_check_function('prtest'); --ok drop procedure prtest(); create function return_constant_refcursor() returns refcursor as $$ declare rc constant refcursor; begin open rc for select a from rc_test; return rc; end $$ language plpgsql; create table rc_test(a int); select * from plpgsql_check_function('return_constant_refcursor'); drop table rc_test; drop function return_constant_refcursor(); create procedure p1(a int, out b int) as $$ begin b := a + 10; end; $$ language plpgsql; create function f1() returns void as $$ declare b constant int; begin call p1(10, b); end; $$ language plpgsql; select * from plpgsql_check_function('f1'); drop function f1(); drop procedure p1(int, int); create or replace function f1() returns int as $$ declare c constant int default 100; begin return c; end; $$ language plpgsql; -- should be ok select * from plpgsql_check_function('f1'); drop function f1(); -- do not raise false warning create or replace function test_function() returns text as $$ declare s text; begin get diagnostics s = PG_CONTEXT; return s; end; $$ language plpgsql; create or replace procedure test_procedure() as $$ begin null; end; $$ language plpgsql; -- should be without any warnings select * from plpgsql_check_function('test_function', performance_warnings=>true); select * from plpgsql_check_function('test_procedure', performance_warnings=>true); drop function test_function(); drop procedure test_procedure(); -- detect dependecy in CALL statement create or replace function fx1_dep(int) returns int as $$ begin return $1; end; $$ language plpgsql; create or replace procedure px1_dep(int) as $$ begin end; $$ language plpgsql; create or replace function test_function() returns void as $$ begin call px1_dep(fx1_dep(10)); end; $$ language plpgsql; select type, schema, name, params from plpgsql_show_dependency_tb('test_function'); drop function test_function(); drop procedure px1_dep(int); drop function fx1_dep(int);okbob-plpgsql_check-800db79/sql/plpgsql_check_active-18.sql000066400000000000000000000056671526744060500237310ustar00rootroot00000000000000LOAD 'plpgsql'; CREATE EXTENSION IF NOT EXISTS plpgsql_check; set client_min_messages to notice; create or replace function fxtest() returns void as $$ declare v_sqlstate text; v_message text; v_context text; begin get stacked diagnostics v_sqlstate = returned_sqlstate, v_message = message_text, v_context = pg_exception_context; end; $$ language plpgsql; select * from plpgsql_check_function('fxtest'); drop function fxtest(); create or replace procedure prtest() as $$ begin commit; end; $$ language plpgsql; select * from plpgsql_check_function('prtest'); --ok create or replace procedure prtest() as $$ begin begin begin commit; end; end; exception when others then raise; end; $$ language plpgsql; select * from plpgsql_check_function('prtest'); --error create or replace procedure prtest() as $$ begin raise exception 'error'; exception when others then begin begin commit; end; end; end; $$ language plpgsql; select * from plpgsql_check_function('prtest'); --ok drop procedure prtest(); create function return_constant_refcursor() returns refcursor as $$ declare rc constant refcursor; begin open rc for select a from rc_test; return rc; end $$ language plpgsql; create table rc_test(a int); select * from plpgsql_check_function('return_constant_refcursor'); drop table rc_test; drop function return_constant_refcursor(); create procedure p1(a int, out b int) as $$ begin b := a + 10; end; $$ language plpgsql; create function f1() returns void as $$ declare b constant int; begin call p1(10, b); end; $$ language plpgsql; select * from plpgsql_check_function('f1'); drop function f1(); drop procedure p1(int, int); create or replace function f1() returns int as $$ declare c constant int default 100; begin return c; end; $$ language plpgsql; -- should be ok select * from plpgsql_check_function('f1'); drop function f1(); -- do not raise false warning create or replace function test_function() returns text as $$ declare s text; begin get diagnostics s = PG_CONTEXT; return s; end; $$ language plpgsql; create or replace procedure test_procedure() as $$ begin null; end; $$ language plpgsql; -- should be without any warnings select * from plpgsql_check_function('test_function', performance_warnings=>true); select * from plpgsql_check_function('test_procedure', performance_warnings=>true); drop function test_function(); drop procedure test_procedure(); -- detect dependecy in CALL statement create or replace function fx1_dep(int) returns int as $$ begin return $1; end; $$ language plpgsql; create or replace procedure px1_dep(int) as $$ begin end; $$ language plpgsql; create or replace function test_function() returns void as $$ begin call px1_dep(fx1_dep(10)); end; $$ language plpgsql; select type, schema, name, params from plpgsql_show_dependency_tb('test_function'); drop function test_function(); drop procedure px1_dep(int); drop function fx1_dep(int);okbob-plpgsql_check-800db79/sql/plpgsql_check_active-19.sql000066400000000000000000000056671526744060500237320ustar00rootroot00000000000000LOAD 'plpgsql'; CREATE EXTENSION IF NOT EXISTS plpgsql_check; set client_min_messages to notice; create or replace function fxtest() returns void as $$ declare v_sqlstate text; v_message text; v_context text; begin get stacked diagnostics v_sqlstate = returned_sqlstate, v_message = message_text, v_context = pg_exception_context; end; $$ language plpgsql; select * from plpgsql_check_function('fxtest'); drop function fxtest(); create or replace procedure prtest() as $$ begin commit; end; $$ language plpgsql; select * from plpgsql_check_function('prtest'); --ok create or replace procedure prtest() as $$ begin begin begin commit; end; end; exception when others then raise; end; $$ language plpgsql; select * from plpgsql_check_function('prtest'); --error create or replace procedure prtest() as $$ begin raise exception 'error'; exception when others then begin begin commit; end; end; end; $$ language plpgsql; select * from plpgsql_check_function('prtest'); --ok drop procedure prtest(); create function return_constant_refcursor() returns refcursor as $$ declare rc constant refcursor; begin open rc for select a from rc_test; return rc; end $$ language plpgsql; create table rc_test(a int); select * from plpgsql_check_function('return_constant_refcursor'); drop table rc_test; drop function return_constant_refcursor(); create procedure p1(a int, out b int) as $$ begin b := a + 10; end; $$ language plpgsql; create function f1() returns void as $$ declare b constant int; begin call p1(10, b); end; $$ language plpgsql; select * from plpgsql_check_function('f1'); drop function f1(); drop procedure p1(int, int); create or replace function f1() returns int as $$ declare c constant int default 100; begin return c; end; $$ language plpgsql; -- should be ok select * from plpgsql_check_function('f1'); drop function f1(); -- do not raise false warning create or replace function test_function() returns text as $$ declare s text; begin get diagnostics s = PG_CONTEXT; return s; end; $$ language plpgsql; create or replace procedure test_procedure() as $$ begin null; end; $$ language plpgsql; -- should be without any warnings select * from plpgsql_check_function('test_function', performance_warnings=>true); select * from plpgsql_check_function('test_procedure', performance_warnings=>true); drop function test_function(); drop procedure test_procedure(); -- detect dependecy in CALL statement create or replace function fx1_dep(int) returns int as $$ begin return $1; end; $$ language plpgsql; create or replace procedure px1_dep(int) as $$ begin end; $$ language plpgsql; create or replace function test_function() returns void as $$ begin call px1_dep(fx1_dep(10)); end; $$ language plpgsql; select type, schema, name, params from plpgsql_show_dependency_tb('test_function'); drop function test_function(); drop procedure px1_dep(int); drop function fx1_dep(int);okbob-plpgsql_check-800db79/sql/plpgsql_check_active-20.sql000066400000000000000000000056671526744060500237220ustar00rootroot00000000000000LOAD 'plpgsql'; CREATE EXTENSION IF NOT EXISTS plpgsql_check; set client_min_messages to notice; create or replace function fxtest() returns void as $$ declare v_sqlstate text; v_message text; v_context text; begin get stacked diagnostics v_sqlstate = returned_sqlstate, v_message = message_text, v_context = pg_exception_context; end; $$ language plpgsql; select * from plpgsql_check_function('fxtest'); drop function fxtest(); create or replace procedure prtest() as $$ begin commit; end; $$ language plpgsql; select * from plpgsql_check_function('prtest'); --ok create or replace procedure prtest() as $$ begin begin begin commit; end; end; exception when others then raise; end; $$ language plpgsql; select * from plpgsql_check_function('prtest'); --error create or replace procedure prtest() as $$ begin raise exception 'error'; exception when others then begin begin commit; end; end; end; $$ language plpgsql; select * from plpgsql_check_function('prtest'); --ok drop procedure prtest(); create function return_constant_refcursor() returns refcursor as $$ declare rc constant refcursor; begin open rc for select a from rc_test; return rc; end $$ language plpgsql; create table rc_test(a int); select * from plpgsql_check_function('return_constant_refcursor'); drop table rc_test; drop function return_constant_refcursor(); create procedure p1(a int, out b int) as $$ begin b := a + 10; end; $$ language plpgsql; create function f1() returns void as $$ declare b constant int; begin call p1(10, b); end; $$ language plpgsql; select * from plpgsql_check_function('f1'); drop function f1(); drop procedure p1(int, int); create or replace function f1() returns int as $$ declare c constant int default 100; begin return c; end; $$ language plpgsql; -- should be ok select * from plpgsql_check_function('f1'); drop function f1(); -- do not raise false warning create or replace function test_function() returns text as $$ declare s text; begin get diagnostics s = PG_CONTEXT; return s; end; $$ language plpgsql; create or replace procedure test_procedure() as $$ begin null; end; $$ language plpgsql; -- should be without any warnings select * from plpgsql_check_function('test_function', performance_warnings=>true); select * from plpgsql_check_function('test_procedure', performance_warnings=>true); drop function test_function(); drop procedure test_procedure(); -- detect dependecy in CALL statement create or replace function fx1_dep(int) returns int as $$ begin return $1; end; $$ language plpgsql; create or replace procedure px1_dep(int) as $$ begin end; $$ language plpgsql; create or replace function test_function() returns void as $$ begin call px1_dep(fx1_dep(10)); end; $$ language plpgsql; select type, schema, name, params from plpgsql_show_dependency_tb('test_function'); drop function test_function(); drop procedure px1_dep(int); drop function fx1_dep(int);okbob-plpgsql_check-800db79/sql/plpgsql_check_active.sql000066400000000000000000002712171526744060500234770ustar00rootroot00000000000000load 'plpgsql'; create extension if not exists plpgsql_check; set client_min_messages to notice; set plpgsql_check.regress_test_mode = true; -- -- check function statement tests -- --should fail - is not plpgsql select * from plpgsql_check_function_tb('session_user()'); create table t1(a int, b int); create table pa (id int, pa_id character varying(32), status character varying(60)); create table ml(ml_id character varying(32), status_from character varying(60), pa_id character varying(32), xyz int); create function f1() returns void as $$ begin if false then update t1 set c = 30; end if; if false then raise notice '%', r.c; end if; end; $$ language plpgsql; select f1(); select * from plpgsql_check_function_tb('f1()', fatal_errors := true); select * from plpgsql_check_function_tb('f1()', fatal_errors := false); select * from plpgsql_check_function_tb('f1()'); drop function f1(); create function f1() returns void as $$ begin if false then insert into t1 values(10,20); update t1 set a = 10; delete from t1; end if; end; $$ language plpgsql stable; select f1(); select * from plpgsql_check_function_tb('f1()', fatal_errors := false); drop function f1(); create function f1() returns void as $$ declare r record; begin if false then for r in update t1 set a = a + 1 returning * loop raise notice '%', r.a; end loop; end if; end; $$ language plpgsql; select f1(); select * from plpgsql_check_function_tb('f1()', fatal_errors := false); drop function f1(); create function f1() returns void as $$ declare r record; begin if false then for r in update t1 set a = a + 1 returning * loop raise notice '%', r.a; end loop; end if; end; $$ language plpgsql stable; select f1(); select * from plpgsql_check_function_tb('f1()', fatal_errors := false); drop function f1(); create function g1(out a int, out b int) as $$ select 10,20; $$ language sql; create function f1() returns void as $$ declare r record; begin r := g1(); if false then raise notice '%', r.c; end if; end; $$ language plpgsql; select f1(); select * from plpgsql_check_function_tb('f1()'); select f1(); drop function f1(); drop function g1(); create function g1(out a int, out b int) returns setof record as $$ select * from t1; $$ language sql; create function f1() returns void as $$ declare r record; begin for r in select * from g1() loop raise notice '%', r.c; end loop; end; $$ language plpgsql; select f1(); select * from plpgsql_check_function_tb('f1()'); select f1(); create or replace function f1() returns void as $$ declare r record; begin for r in select * from g1() loop r.c := 20; end loop; end; $$ language plpgsql; select f1(); select * from plpgsql_check_function_tb('f1()'); select f1(); drop function f1(); drop function g1(); create function f1() returns int as $$ declare r int; begin if false then r := a + b; end if; return r; end; $$ language plpgsql; select f1(); select * from plpgsql_check_function_tb('f1()'); select f1(); drop function f1(); create or replace function f1() returns void as $$ declare r int[]; begin if false then r[c+10] := 20; end if; end; $$ language plpgsql; select f1(); select * from plpgsql_check_function_tb('f1()'); select f1(); drop function f1(); create or replace function f1() returns void as $$ declare r int; begin if false then r[10] := 20; end if; end; $$ language plpgsql set search_path = public; select f1(); select * from plpgsql_check_function_tb('f1()'); select f1(); drop function f1(); create or replace function f1_trg() returns trigger as $$ begin if new.a > 10 then raise notice '%', new.b; raise notice '%', new.c; end if; return new; end; $$ language plpgsql; create trigger t1_f1 before insert on t1 for each row execute procedure f1_trg(); insert into t1 values(6,30); select * from plpgsql_check_function_tb('f1_trg()','t1'); insert into t1 values(6,30); create or replace function f1_trg() returns trigger as $$ begin new.a := new.a + 10; new.b := new.b + 10; new.c := 30; return new; end; $$ language plpgsql; -- should to fail select * from plpgsql_check_function_tb('f1_trg()','t1'); -- should to fail but not crash insert into t1 values(6,30); create or replace function f1_trg() returns trigger as $$ begin new.a := new.a + 10; new.b := new.b + 10; return new; end; $$ language plpgsql; -- ok select * from plpgsql_check_function_tb('f1_trg()', 't1'); -- ok insert into t1 values(6,30); create or replace function f1_trg() returns trigger as $$ begin new.a := new.a + 10; new.b := new.b + 10; return null; end; $$ language plpgsql; -- ok select * from plpgsql_check_function_tb('f1_trg()', 't1'); insert into t1 values(60,300); select * from t1; insert into t1 values(600,30); select * from t1; drop trigger t1_f1 on t1; drop function f1_trg(); -- test of showing caret on correct place for multiline queries create or replace function f1() returns void as $$ begin select var from foo; end; $$ language plpgsql; select * from plpgsql_check_function_tb('f1()'); drop function f1(); create or replace function f1() returns int as $$ begin return (select a from t1 where hh = 20); end; $$ language plpgsql; select * from plpgsql_check_function_tb('f1()'); create or replace function f1() returns int as $$ begin return (select a from txxxxxxx where hh = 20); end; $$ language plpgsql; select * from plpgsql_check_function_tb('f1()'); drop function f1(); drop table t1; -- raise warnings when target row has different number of attributies in -- SELECT INTO statement create or replace function f1() returns void as $$ declare a1 int; a2 int; begin select 10,20 into a1,a2; end; $$ language plpgsql; -- should be ok select * from plpgsql_check_function_tb('f1()'); create or replace function f1() returns void as $$ declare a1 int; begin select 10,20 into a1; end; $$ language plpgsql; -- raise warning select * from plpgsql_check_function_tb('f1()'); create or replace function f1() returns void as $$ declare a1 int; a2 int; begin select 10 into a1,a2; end; $$ language plpgsql; -- raise warning select * from plpgsql_check_function_tb('f1()'); -- bogus code set check_function_bodies to off; create or replace function f1() returns void as $$ adasdfsadf $$ language plpgsql; select * from plpgsql_check_function_tb('f1()'); drop function f1(); create table t1(a int, b int); create function g1(out a int, out b int) as $$ select 10,20; $$ language sql; create function f1() returns void as $$ declare r record; begin r := g1(); if false then raise notice '%', r.c; end if; end; $$ language plpgsql; select f1(); select * from plpgsql_check_function('f1()'); select f1(); drop function f1(); drop function g1(); create function g1(out a int, out b int) returns setof record as $$ select * from t1; $$ language sql; create function f1() returns void as $$ declare r record; begin for r in select * from g1() loop raise notice '%', r.c; end loop; end; $$ language plpgsql; select f1(); select * from plpgsql_check_function('f1()'); select f1(); create or replace function f1() returns void as $$ declare r record; begin for r in select * from g1() loop r.c := 20; end loop; end; $$ language plpgsql; select f1(); select * from plpgsql_check_function('f1()'); select f1(); drop function f1(); drop function g1(); create function f1() returns int as $$ declare r int; begin if false then r := a + b; end if; return r; end; $$ language plpgsql; select f1(); select * from plpgsql_check_function('f1()'); select f1(); drop function f1(); create or replace function f1() returns void as $$ declare r int[]; begin if false then r[c+10] := 20; end if; end; $$ language plpgsql; select f1(); select * from plpgsql_check_function('f1()'); select f1(); drop function f1(); create or replace function f1() returns void as $$ declare r int; begin if false then r[10] := 20; end if; end; $$ language plpgsql set search_path = public; select f1(); select * from plpgsql_check_function('f1()'); select f1(); drop function f1(); create or replace function f1_trg() returns trigger as $$ begin if new.a > 10 then raise notice '%', new.b; raise notice '%', new.c; end if; return new; end; $$ language plpgsql; create trigger t1_f1 before insert on t1 for each row execute procedure f1_trg(); insert into t1 values(6,30); select * from plpgsql_check_function('f1_trg()','t1'); insert into t1 values(6,30); create or replace function f1_trg() returns trigger as $$ begin new.a := new.a + 10; new.b := new.b + 10; new.c := 30; return new; end; $$ language plpgsql; -- should to fail select * from plpgsql_check_function('f1_trg()','t1'); -- should to fail but not crash insert into t1 values(6,30); create or replace function f1_trg() returns trigger as $$ begin new.a := new.a + 10; new.b := new.b + 10; return new; end; $$ language plpgsql; -- ok select * from plpgsql_check_function('f1_trg()', 't1'); -- ok insert into t1 values(6,30); select * from t1; drop trigger t1_f1 on t1; drop function f1_trg(); -- test of showing caret on correct place for multiline queries create or replace function f1() returns void as $$ begin select var from foo; end; $$ language plpgsql; select * from plpgsql_check_function('f1()'); drop function f1(); create or replace function f1() returns int as $$ begin return (select a from t1 where hh = 20); end; $$ language plpgsql; select * from plpgsql_check_function('f1()'); create or replace function f1() returns int as $$ begin return (select a from txxxxxxx where hh = 20); end; $$ language plpgsql; select * from plpgsql_check_function('f1()'); drop function f1(); drop table t1; -- raise warnings when target row has different number of attributies in -- SELECT INTO statement create or replace function f1() returns void as $$ declare a1 int; a2 int; begin select 10,20 into a1,a2; end; $$ language plpgsql; -- should be ok select * from plpgsql_check_function('f1()'); create or replace function f1() returns void as $$ declare a1 int; begin select 10,20 into a1; end; $$ language plpgsql; -- raise warning select * from plpgsql_check_function('f1()'); create or replace function f1() returns void as $$ declare a1 int; a2 int; begin select 10 into a1,a2; end; $$ language plpgsql; -- raise warning select * from plpgsql_check_function('f1()'); -- bogus code set check_function_bodies to off; create or replace function f1() returns void as $$ adasdfsadf $$ language plpgsql; select * from plpgsql_check_function('f1()'); drop function f1(); create table f1tbl(a int, b int); -- unused variables create or replace function f1(_input1 int) returns table(_output1 int, _output2 int) as $$ declare _f1 int; _f2 int; _f3 int; _f4 int; _f5 int; _r record; _tbl f1tbl; begin if true then _f1 := 1; end if; select 1, 2 into _f3, _f4; perform 1 where _f5 is null; select 1 into _r; select 1, 2 into _tbl; -- check that SQLSTATE and SQLERRM don't raise false positives begin exception when raise_exception then end; end $$ language plpgsql; select * from plpgsql_check_function('f1(int)'); drop function f1(int); drop table f1tbl; -- check that NEW and OLD are not reported unused create table f1tbl(); create or replace function f1() returns trigger as $$ begin return null; end $$ language plpgsql; select * from plpgsql_check_function('f1()', 'f1tbl'); drop function f1(); drop table f1tbl; create table tabret(a int, b int); insert into tabret values(10,10); create or replace function f1() returns int as $$ begin return (select a from tabret); end; $$ language plpgsql; select * from plpgsql_check_function('f1()', performance_warnings := true); create or replace function f1() returns int as $$ begin return (select a::numeric from tabret); end; $$ language plpgsql; select * from plpgsql_check_function('f1()', performance_warnings := true); create or replace function f1() returns int as $$ begin return (select a, b from tabret); end; $$ language plpgsql; select * from plpgsql_check_function('f1()', performance_warnings := true); drop function f1(); create or replace function f1() returns table(ax int, bx int) as $$ begin return query select * from tabret; return; end; $$ language plpgsql; select * from plpgsql_check_function('f1()', performance_warnings := true); drop function f1(); create or replace function f1() returns table(ax numeric, bx numeric) as $$ begin return query select * from tabret; return; end; $$ language plpgsql; select * from plpgsql_check_function('f1()', performance_warnings := true); drop function f1(); create or replace function f1() returns setof tabret as $$ begin return query select * from tabret; return; end; $$ language plpgsql; select * from plpgsql_check_function('f1()', performance_warnings := true); create or replace function f1() returns setof tabret as $$ begin return query select a from tabret; return; end; $$ language plpgsql; select * from plpgsql_check_function('f1()', performance_warnings := true); create or replace function f1() returns setof tabret as $$ begin return query select a::numeric,b::numeric from tabret; return; end; $$ language plpgsql; select * from plpgsql_check_function('f1()', performance_warnings := true); drop function f1(); create or replace function f1(a int) returns setof numeric as $$ begin return query select a; end $$ language plpgsql; select * from plpgsql_check_function('f1(int)', performance_warnings := true); drop function f1(int); drop table tabret; create or replace function f1() returns void as $$ declare intval integer; begin intval := null; -- ok intval := 1; -- OK intval := '1'; -- OK intval := text '1'; -- not OK intval := current_date; -- not OK select 1 into intval; -- OK select '1' into intval; -- OK select text '1' into intval; -- not OK end $$ language plpgsql; select * from plpgsql_check_function('f1()', performance_warnings := true); drop function f1(); create or replace function f1() returns int as $$ begin return 1; end; $$ language plpgsql; select * from plpgsql_check_function('f1()', performance_warnings := true); create or replace function f1() returns int as $$ begin return 1::numeric; end; $$ language plpgsql; select * from plpgsql_check_function('f1()', performance_warnings := true); create or replace function f1() returns int as $$ begin return null; end; $$ language plpgsql; select * from plpgsql_check_function('f1()', performance_warnings := true); create or replace function f1() returns int as $$ begin return current_date; end; $$ language plpgsql; select * from plpgsql_check_function('f1()', performance_warnings := true); create or replace function f1() returns int as $$ declare a int; begin return a; end; $$ language plpgsql; select * from plpgsql_check_function('f1()', performance_warnings := true); create or replace function f1() returns int as $$ declare a numeric; begin return a; end; $$ language plpgsql; select * from plpgsql_check_function('f1()', performance_warnings := true); drop function f1(); create or replace function f1() returns setof int as $$ begin return next 1; end; $$ language plpgsql; select * from plpgsql_check_function('f1()', performance_warnings := true); create or replace function f1() returns setof int as $$ begin return next 1::numeric; -- tolerant, doesn't use tupmap end; $$ language plpgsql; select * from plpgsql_check_function('f1()', performance_warnings := true); drop function f1(); create type t1 as (a int, b int, c int); create type t2 as (a int, b numeric); create or replace function fx() returns t2 as $$ declare x t1; begin return x; end; $$ language plpgsql; select * from plpgsql_check_function('fx()', performance_warnings := true); create or replace function fx() returns t2 as $$ declare x t2; begin return x; end; $$ language plpgsql; select * from plpgsql_check_function('fx()', performance_warnings := true); drop function fx(); create or replace function fx() returns setof t2 as $$ declare x t1; begin return next x; return; end; $$ language plpgsql; select * from plpgsql_check_function('fx()', performance_warnings := true); create or replace function fx() returns setof t2 as $$ declare x t2; begin return next x; return; end; $$ language plpgsql; select * from plpgsql_check_function('fx()', performance_warnings := true); drop function fx(); create or replace function fx2(_id int, _pa_id varchar(32), _status varchar(60)) returns void as $$ declare begin insert into pa values(_id, _pa_id, _status); exception when OTHERS then raise notice '%', 'some message'; raise exception '%', sqlerrm; end $$ language plpgsql; select * from plpgsql_check_function('fx2(int, varchar, varchar)', performance_warnings := true); create or replace function fx2(_id int, _pa_id varchar(32), _status varchar(60)) returns void as $$ declare begin insert into pa values(_id, _pa_id, _status) returning *; exception when OTHERS then raise notice '%', 'some message'; raise exception '%', sqlerrm; end $$ language plpgsql; select * from plpgsql_check_function('fx2(int, varchar, varchar)', performance_warnings := true); create or replace function fx2(_id int, _pa_id varchar(32), _status varchar(60)) returns void as $$ declare begin SELECT * FROM pa LIMIT 1; exception when OTHERS then raise notice '%', 'some message'; raise exception '%', sqlerrm; end $$ language plpgsql; select * from plpgsql_check_function('fx2(int, varchar, varchar)', performance_warnings := true); drop function fx2(int, varchar, varchar); create or replace function foreach_array_loop() returns void as $body$ declare arr text[]; el text; begin arr := array['1111','2222','3333']; foreach el in array arr loop raise notice '%', el; end loop; end; $body$ language 'plpgsql' stable; select * from plpgsql_check_function_tb('foreach_array_loop()', performance_warnings := true); create or replace function foreach_array_loop() returns void as $body$ declare arr text[]; el int; begin arr := array['1111','2222','3333']; foreach el in array arr loop raise notice '%', el; end loop; end; $body$ language 'plpgsql' stable; select * from plpgsql_check_function_tb('foreach_array_loop()', performance_warnings := true); create or replace function foreach_array_loop() returns void as $body$ declare arr date[]; el int; begin -- expression is not constant (else this check fails due tracking constant) arr := array['2014-01-01','2015-01-01','2016-01-01', current_date]::date[]; foreach el in array arr loop raise notice '%', el; end loop; end; $body$ language 'plpgsql' stable; select * from plpgsql_check_function_tb('foreach_array_loop()', performance_warnings := true); create or replace function foreach_array_loop() returns void as $body$ declare arr date[]; el int; begin arr := array['2014-01-01','2015-01-01','2016-01-01']::date[]; foreach el in array arr loop raise notice '%', el; end loop; end; $body$ language 'plpgsql' stable; select * from plpgsql_check_function_tb('foreach_array_loop()', performance_warnings := true); create or replace function foreach_array_loop() returns void as $body$ declare el text; begin foreach el in array array['1111','2222','3333'] loop raise notice '%', el; end loop; end; $body$ language 'plpgsql' stable; select * from plpgsql_check_function_tb('foreach_array_loop()', performance_warnings := true); create or replace function foreach_array_loop() returns void as $body$ declare el int; begin foreach el in array array['1111','2222','3333'] loop raise notice '%', el; end loop; end; $body$ language 'plpgsql' stable; select * from plpgsql_check_function_tb('foreach_array_loop()', performance_warnings := true); create or replace function foreach_array_loop() returns void as $body$ declare el int; begin foreach el in array array['2014-01-01','2015-01-01','2016-01-01']::date[] loop raise notice '%', el; end loop; end; $body$ language 'plpgsql' stable; select * from plpgsql_check_function_tb('foreach_array_loop()', performance_warnings := true); drop function foreach_array_loop(); create or replace function scan_rows(int[]) returns void AS $$ declare x int[]; begin foreach x slice 1 in array $1 loop raise notice 'row = %', x; end loop; end; $$ language plpgsql; select * from plpgsql_check_function_tb('scan_rows(int[])', performance_warnings := true); create or replace function scan_rows(int[]) returns void AS $$ declare x int[]; begin foreach x in array $1 loop raise notice 'row = %', x; end loop; end; $$ language plpgsql; select * from plpgsql_check_function_tb('scan_rows(int[])', performance_warnings := true); drop function scan_rows(int[]); drop type t1; drop type t2; create table t1(a int, b int); create table t2(a int, b int, c int); create table t3(a numeric, b int); insert into t1 values(10,20),(30,40); create or replace function fx() returns int as $$ declare s int default 0; r t1; begin foreach r in array (select array_agg(t1) from t1) loop s := r.a + r.b; end loop; return s; end; $$ language plpgsql; select * from plpgsql_check_function_tb('fx()', performance_warnings := true); create or replace function fx() returns int as $$ declare s int default 0; r t1; c t1[]; begin c := (select array_agg(t1) from t1); foreach r in array c loop s := r.a + r.b; end loop; return s; end; $$ language plpgsql; select * from plpgsql_check_function_tb('fx()', performance_warnings := true); create or replace function fx() returns int as $$ declare s int default 0; r t1; c t1[]; begin select array_agg(t1) into c from t1; foreach r in array c loop s := r.a + r.b; end loop; return s; end; $$ language plpgsql; select * from plpgsql_check_function_tb('fx()', performance_warnings := true); create or replace function fx() returns int as $$ declare s int default 0; r t1; c t1[]; begin select array_agg(t1) into c from t1; for i in array_lower(c, 1) .. array_upper(c, 1) loop r := c[i]; s := r.a + r.b; end loop; return s; end; $$ language plpgsql; select * from plpgsql_check_function_tb('fx()', performance_warnings := true); create or replace function fx() returns int as $$ declare s int default 0; c t1[]; begin select array_agg(t1) into c from t1; for i in array_lower(c, 1) .. array_upper(c, 1) loop s := (c[i]).a + (c[i]).b; end loop; return s; end; $$ language plpgsql; select * from plpgsql_check_function_tb('fx()', performance_warnings := true); create or replace function fx() returns int as $$ declare s int default 0; r record; c t1[]; begin select array_agg(t1) into c from t1; for i in array_lower(c, 1) .. array_upper(c, 1) loop r := c[i]; s := r.a + r.b; end loop; return s; end; $$ language plpgsql; select * from plpgsql_check_function_tb('fx()', performance_warnings := true); create or replace function fx() returns int as $$ declare s int default 0; r record; c t1[]; begin select array_agg(t1) into c from t1; for i in array_lower(c, 1) .. array_upper(c, 1) loop r := c[i]; s := r.a + r.b + r.c; end loop; return s; end; $$ language plpgsql; select * from plpgsql_check_function_tb('fx()', performance_warnings := true); create or replace function fx() returns int as $$ declare s int default 0; r t2; begin foreach r in array (select array_agg(t1) from t1) loop s := r.a + r.b; end loop; return s; end; $$ language plpgsql; select * from plpgsql_check_function_tb('fx()', performance_warnings := true); create or replace function fx() returns int as $$ declare s int default 0; r t3; begin foreach r in array (select array_agg(t1) from t1) loop s := r.a + r.b; end loop; return s; end; $$ language plpgsql; select * from plpgsql_check_function_tb('fx()', performance_warnings := true); drop function fx(); drop table t1; -- mscottie issue #13 create table test ( a text, b integer, c uuid ); create function before_insert_test() returns trigger language plpgsql as $$ begin select a into NEW.a from test where b = 1; select b into NEW.b from test where b = 1; select null::uuid into NEW.c from test where b = 1; return new; end; $$; select * from plpgsql_check_function_tb('before_insert_test()','test'); create or replace function before_insert_test() returns trigger language plpgsql as $$ begin NEW.a := (select a from test where b = 1); NEW.b := (select b from test where b = 1); NEW.c := (select c from test where b = 1); return new; end; $$; select * from plpgsql_check_function_tb('before_insert_test()','test', fatal_errors := false); create or replace function before_insert_test() returns trigger language plpgsql as $$ begin NEW.a := 'Hello'::text; NEW.b := 10; NEW.c := null::uuid; return new; end; $$; select * from plpgsql_check_function_tb('before_insert_test()','test', fatal_errors := false); drop function before_insert_test(); create or replace function fx() returns void as $$ declare NEW test; OLD test; begin select null::uuid into NEW.c from test where b = 1; end; $$ language plpgsql; select * from plpgsql_check_function_tb('fx()', performance_warnings := true, fatal_errors := false); drop function fx(); create or replace function fx() returns void as $$ declare NEW test; begin NEW.a := 'Hello'::text; NEW.b := 10; NEW.c := null::uuid; end; $$ language plpgsql; select * from plpgsql_check_function_tb('fx()', performance_warnings := true, fatal_errors := false); drop function fx(); drop table test; create or replace function fx() returns void as $$ declare s int; sa int[]; sd date; bs int[]; begin sa[10] := s; sa[10] := sd; s := bs[10]; end; $$ language plpgsql; select * from plpgsql_check_function_tb('fx()', performance_warnings := true, fatal_errors := false); drop function fx(); create type t as (t text); create or replace function fx() returns void as $$ declare _t t; _tt t[]; _txt text; begin _t.t := 'ABC'; -- correct warning "unknown" _tt[1] := _t; _txt := _t; end; $$ language plpgsql; select * from plpgsql_check_function('fx()', performance_warnings := true); drop function fx(); create or replace function fx() returns void as $$ declare _t1 t; _t2 t; begin _t1.t := 'ABC'::text; _t2 := _t1; raise notice '% %', _t2, _t2.t; end; $$ language plpgsql; select * from plpgsql_check_function('fx()', performance_warnings := true); drop function fx(); create or replace function fx(out _tt t[]) as $$ declare _t t; begin _t.t := 'ABC'::text; _tt[1] := _t; end; $$ language plpgsql; select * from plpgsql_check_function('fx()', performance_warnings := true); drop function fx(); drop type t; create or replace function fx() returns int as $$ declare x int; begin perform 1; return 10; end; $$ language plpgsql; select * from plpgsql_check_function('fx()', performance_warnings := true); drop function fx(); create table t(i int); create function test_t(OUT t) returns t AS $$ begin $1 := null; end; $$ language plpgsql; select test_t(); select * from test_t(); select * from plpgsql_check_function('test_t()', performance_warnings := true); create or replace function fx() returns void as $$ declare c cursor for select * from t; x varchar; begin open c; fetch c into x; close c; end; $$ language plpgsql; select test_t(); select * from test_t(); select * from plpgsql_check_function('fx()', performance_warnings := true, fatal_errors := false); drop function fx(); create or replace function fx() returns void as $$ declare c cursor for select * from t; x int; begin open c; fetch c into x; close c; end; $$ language plpgsql; select test_t(); select * from test_t(); select * from plpgsql_check_function('fx()', performance_warnings := true, fatal_errors := false); drop function fx(); create or replace function fx() returns void as $$ declare c cursor for select * from t; begin for r in c loop raise notice '%', r.a; end loop; end; $$ language plpgsql; select test_t(); select * from test_t(); select * from plpgsql_check_function('fx()', performance_warnings := true, fatal_errors := false); drop function fx(); create or replace function fx() returns void as $$ declare c cursor for select * from t; begin for r in c loop raise notice '%', r.i; end loop; end; $$ language plpgsql; select test_t(); select * from test_t(); select * from plpgsql_check_function('fx()', performance_warnings := true, fatal_errors := false); drop function fx(); create table foo(a int, b int); create or replace function fx() returns void as $$ declare f1 int; f2 int; begin select 1, 2 into f1; select 1 into f1, f2; select a b into f1, f2 from foo; end; $$ language plpgsql; select fx(); select * from plpgsql_check_function('fx()', performance_warnings := true, fatal_errors := false); drop function fx(); drop table foo; create or replace function fx() returns void as $$ declare d date; begin d := (select 1 from pg_class limit 1); raise notice '%', d; end; $$ language plpgsql; select fx(); select * from plpgsql_check_function('fx()', performance_warnings := true, fatal_errors := false); drop function fx(); create table tab_1(i int); create or replace function fx(a int) returns setof int as $$ declare c refcursor; r record; begin open c for select i from tab_1 where i = a; loop fetch c into r; if not found then exit; end if; return next r.i; end loop; end; $$ language plpgsql; select * from plpgsql_check_function('fx(int)', performance_warnings := true, fatal_errors := false); create or replace function fx(a int) returns setof int as $$ declare c refcursor; r record; begin open c for select i from tab_1 where i = a; loop fetch c into r; if not found then exit; end if; return next r.x; end loop; end; $$ language plpgsql; select * from plpgsql_check_function('fx(int)', performance_warnings := true, fatal_errors := false); drop function fx(int); drop table tab_1; create or replace function fxx() returns void as $$ begin rollback; end; $$ language plpgsql; select fxx(); select * from plpgsql_check_function('fxx()'); drop function fxx(); create or replace function fxx() returns void as $$ declare x int; begin declare x int; begin end; end; $$ language plpgsql; select * from plpgsql_check_function('fxx()'); select * from plpgsql_check_function('fxx()', extra_warnings := false); drop function fxx(); create or replace function fxx(in a int, in b int, out c int, out d int) as $$ begin c := a; end; $$ language plpgsql; select * from plpgsql_check_function('fxx(int, int)'); create or replace function fxx(in a int, in b int, out c int, out d int) as $$ begin c := d; end; $$ language plpgsql; select * from plpgsql_check_function('fxx(int, int)'); create type ct as (a int, b int); create or replace function fxx(a ct, b ct, OUT c ct, OUT d ct) as $$ begin c.a := a.a; end; $$ language plpgsql; select * from plpgsql_check_function('fxx(ct, ct)'); create or replace function fxx(a ct, b ct, OUT c ct, OUT d ct) as $$ begin c.a := d.a; end; $$ language plpgsql; select * from plpgsql_check_function('fxx(ct, ct)'); create or replace function tx(a int) returns int as $$ declare a int; ax int; begin declare ax int; begin ax := 10; end; a := 10; return 20; end; $$ language plpgsql; select * from plpgsql_check_function('tx(int)'); create type xt as (a int, b int, c int); create or replace function fx_xt(out x xt) as $$ declare l xt; a int; begin return; end; $$ language plpgsql; select * from plpgsql_check_function('fx_xt()'); drop function fx_xt(); create or replace function fx_xt(out x xt) as $$ declare l xt; a int; begin x.c := 1000; return; end; $$ language plpgsql; select * from plpgsql_check_function('fx_xt()'); drop function fx_xt(); create or replace function fx_xt(out x xt, out y xt) as $$ declare c1 xt; c2 xt; begin return; end; $$ language plpgsql; select * from plpgsql_check_function('fx_xt()'); drop function fx_xt(); create or replace function fx_xt(out x xt, out y xt) as $$ declare c1 xt; c2 xt; begin x.a := 100; y := row(10,20,30); return; end; $$ language plpgsql; select * from plpgsql_check_function('fx_xt()'); drop function fx_xt(); create or replace function fx_xt(out x xt, out z int) as $$ begin return; end; $$ language plpgsql; select * from plpgsql_check_function('fx_xt()'); drop function fx_xt(); drop type xt; -- missing RETURN create or replace function fx_flow() returns int as $$ begin raise notice 'kuku'; end; $$ language plpgsql; select fx_flow(); select * from plpgsql_check_function('fx_flow()'); -- ok create or replace function fx_flow() returns int as $$ declare a int; begin if a > 10 then return a; end if; return 10; end; $$ language plpgsql; select * from plpgsql_check_function('fx_flow()'); -- dead code create or replace function fx_flow() returns int as $$ declare a int; begin if a > 10 then return a; else return a + 1; end if; return 10; end; $$ language plpgsql; select * from plpgsql_check_function('fx_flow()'); -- missing return create or replace function fx_flow() returns int as $$ declare a int; begin if a > 10 then return a; end if; end; $$ language plpgsql; select * from plpgsql_check_function('fx_flow()'); drop function fx_flow(); create or replace function fx_flow(in p_param1 integer) returns text as $$ declare z1 text; begin if p_param1 is not null then z1 := '1111'; return z1; else z1 := '222222'; end if; return z1; end; $$ language plpgsql stable; select * from plpgsql_check_function_tb('fx_flow(integer)'); create or replace function fx_flow(in p_param1 integer) returns text as $$ declare z1 text; begin if p_param1 is not null then z1 := '1111'; return z1; else z1 := '222222'; raise exception 'stop'; end if; return z1; end; $$ language plpgsql stable; select * from plpgsql_check_function_tb('fx_flow(integer)'); drop function fx_flow(); drop function fx(int); create or replace function fx(x int) returns table(y int) as $$ begin return query select x union select x; end $$ language plpgsql; select * from fx(10); select * from plpgsql_check_function_tb('fx(int)'); drop function fx(int); create or replace function fx(x int) returns table(y int, z int) as $$ begin return query select x,x+1 union select x, x+1; end $$ language plpgsql; select * from fx(10); select * from plpgsql_check_function_tb('fx(int)'); drop function fx(int); create table xx(a int); create or replace function fx(x int) returns int as $$ declare _a int; begin begin select a from xx into strict _a where a = x; return _a; exception when others then null; end; return -1; end; $$ language plpgsql; select * from plpgsql_check_function_tb('fx(int)'); drop table xx; create or replace function fx(x int) returns int as $$ begin begin if (x > 0) then raise exception 'xxx' using errcode = 'XX888'; else raise exception 'yyy' using errcode = 'YY888'; end if; return -1; -- dead code; end; return -1; end; $$ language plpgsql; select * from plpgsql_check_function_tb('fx(int)'); create or replace function fx(x int) returns int as $$ begin begin if (x > 0) then raise exception 'xxx' using errcode = 'XX888'; else raise exception 'yyy' using errcode = 'YY888'; end if; exception when sqlstate 'XX888' then null; when sqlstate 'YY888' then null; end; end; -- missing return; $$ language plpgsql; select * from plpgsql_check_function_tb('fx(int)'); create or replace function fx(x int) returns int as $$ begin begin if (x > 0) then raise exception 'xxx' using errcode = 'XX888'; else raise exception 'yyy' using errcode = 'YY888'; end if; exception when others then return 10; end; end; -- ok now $$ language plpgsql; select * from plpgsql_check_function_tb('fx(int)'); --false alarm reported by Filip Zach create type testtype as (id integer); create or replace function fx() returns testtype as $$ begin return row(1); end; $$ language plpgsql; select * from fx(); select fx(); select * from plpgsql_check_function('fx()'); drop function fx(); create function out1(OUT f1 int, OUT f2 int) returns setof record as $$ begin for f1, f2 in execute $q$ select 1, 2 $q$ loop return next; end loop; end $$ language plpgsql; select * from plpgsql_check_function('out1()'); drop function out1(); create function out1(OUT f1 int, OUT f2 int) returns setof record as $$ begin for f1, f2 in select 1, 2 loop return next; end loop; end $$ language plpgsql; select * from plpgsql_check_function('out1()'); drop function out1(); -- never read variable detection create function a() returns int as $$ declare foo int; begin foo := 2; return 1; end; $$ language plpgsql; select * from plpgsql_check_function('a()'); drop function a(); -- issue #29 false unused variable create or replace function f1(in p_cursor refcursor) returns void as $body$ declare z_offset integer; begin z_offset := 10; move absolute z_offset from p_cursor; end; $body$ language 'plpgsql' stable; select * from plpgsql_check_function_tb('f1(refcursor)'); drop function f1(refcursor); -- issue #30 segfault due NULL refname create or replace function test(a varchar) returns void as $$ declare x cursor (_a varchar) for select _a; begin open x(a); end; $$ language plpgsql; select * from plpgsql_check_function_tb('test(varchar)'); drop function test(varchar); create or replace function test() returns void as $$ declare x numeric; begin x := NULL; end; $$ language plpgsql; select * from plpgsql_check_function('test()'); drop function test(); create table testtable(a int); create or replace function test() returns int as $$ declare r testtable; begin select * into r from testtable; return r.a; end; $$ language plpgsql; select * from plpgsql_check_function('test()'); set check_function_bodies to on; drop table testtable; create table testtable(a int, b int); create or replace function test() returns int as $$ declare r testtable; begin select * into r from testtable; return r.a; end; $$ language plpgsql; alter table testtable drop column b; -- expected false alarm on PostgreSQL 10 and older -- there is not possibility to enforce recompilation -- before checking. select * from plpgsql_check_function('test()'); drop function test(); -- issue #32 create table bigtable(id bigint, v varchar); create or replace function test() returns void as $$ declare r record; _id numeric; begin select * into r from bigtable where id = _id; for r in select * from bigtable where _id = id loop end loop; if (exists(select * from bigtable where id = _id)) then end if; end; $$ language plpgsql; select test(); -- should to show performance warnings select * from plpgsql_check_function('test()', performance_warnings := true); create or replace function test() returns void as $$ declare r record; _id bigint; begin select * into r from bigtable where id = _id; for r in select * from bigtable where _id = id loop end loop; if (exists(select * from bigtable where id = _id)) then end if; end; $$ language plpgsql; -- there are not any performance issue now select * from plpgsql_check_function('test()', performance_warnings := true); -- nextval, currval and setval test create table test_table(); create or replace function testseq() returns void as $$ begin perform nextval('test_table'); perform currval('test_table'); perform setval('test_table', 10); perform setval('test_table', 10, true); end; $$ language plpgsql; -- should to fail select testseq(); select * from plpgsql_check_function('testseq()', fatal_errors := false); drop function testseq(); drop table test_table; -- tests designed for PostgreSQL 9.2 set check_function_bodies to off; create table t1(a int, b int); create function f1() returns void as $$ begin if false then update t1 set c = 30; end if; if false then raise notice '%', r.c; end if; end; $$ language plpgsql; select f1(); select * from plpgsql_check_function_tb('f1()', fatal_errors := true); select * from plpgsql_check_function_tb('f1()', fatal_errors := false); select * from plpgsql_check_function_tb('f1()'); select f1(); drop function f1(); create or replace function f1() returns void as $$ begin if false then raise notice '%', 1, 2; end if; end; $$ language plpgsql; select f1(); select * from plpgsql_check_function_tb('f1()'); select f1(); drop function f1(); create or replace function f1() returns void as $$ begin if false then raise notice '% %'; end if; end; $$ language plpgsql; select f1(); select * from plpgsql_check_function_tb('f1()'); select f1(); drop function f1(); create or replace function ml_trg() returns trigger as $$ #option dump declare begin if TG_OP = 'INSERT' then if NEW.status_from IS NULL then begin -- performance issue only select status into NEW.status_from from pa where pa_id = NEW.pa_id; -- nonexist target value select status into NEW.status_from_xxx from pa where pa_id = NEW.pa_id; exception when DATA_EXCEPTION then new.status_from := 'DE'; end; end if; end if; if TG_OP = 'DELETE' then return OLD; else return NEW; end if; exception when OTHERS then NULL; if TG_OP = 'DELETE' then return OLD; else return NEW; end if; end; $$ language plpgsql; select * from plpgsql_check_function('ml_trg()', 'ml', performance_warnings := true); create or replace function fx2() returns void as $$ declare _pa pa; begin select pa.id into _pa.id from pa limit 1; select pa.pa_id into _pa.pa_id from pa limit 1; end; $$ language plpgsql; select * from plpgsql_check_function('fx2()', performance_warnings := true); drop function fx2(); create or replace function fx2() returns void as $$ declare _pa pa; begin _pa.id := (select pa.id from pa limit 1); _pa.pa_id := (select pa.pa_id from pa limit 1); end; $$ language plpgsql; select * from plpgsql_check_function('fx2()', performance_warnings := true); drop function fx2(); create or replace function test_lab() returns void as $$ begin <> for a in 1..3 loop <> BEGIN <> for b in 8..9 loop if a=2 then continue sub; end if; raise notice '% %', a, b; end loop inner; END sub; end loop outer; end; $$ language plpgsql; select test_lab(); select * from plpgsql_check_function('test_lab()', performance_warnings := true); create or replace function test_lab() returns void as $$ begin continue; end; $$ language plpgsql; select test_lab(); select * from plpgsql_check_function('test_lab()', performance_warnings := true); create type _exception_type as ( state text, message text, detail text); create or replace function f1() returns void as $$ declare _exception record; begin _exception := NULL::_exception_type; exception when others then get stacked diagnostics _exception.state = RETURNED_SQLSTATE, _exception.message = MESSAGE_TEXT, _exception.detail = PG_EXCEPTION_DETAIL, _exception.hint = PG_EXCEPTION_HINT; end; $$ language plpgsql; select f1(); select * from plpgsql_check_function('f1()'); drop function f1(); drop type _exception_type; drop table t1; create function myfunc1(a int, b float) returns integer as $$ begin end $$ language plpgsql; create function myfunc2(a int, b float) returns integer as $$ begin end $$ language plpgsql; create function myfunc3(a int, b float) returns integer as $$ begin end $$ language plpgsql; create function myfunc4(a int, b float) returns integer as $$ begin end $$ language plpgsql; create function opfunc1(a int, b float) returns integer as $$ begin end $$ language plpgsql; create operator *** (procedure = opfunc1, leftarg = int, rightarg = float); create table mytable(a int); create table myview as select * from mytable; create function testfunc(a int, b float) returns void as $$ declare x integer; begin raise notice '%', myfunc1(a, b); x := myfunc2(a, b) operator(public.***) 1; perform myfunc3(m.a, b) from myview m; insert into mytable select myfunc4(a, b); end; $$ language plpgsql; select * from plpgsql_check_function('testfunc(int,float)'); select type, schema, name, params from plpgsql_show_dependency_tb('testfunc(int,float)'); drop function testfunc(int, float); drop function myfunc1(int, float); drop function myfunc2(int, float); drop function myfunc3(int, float); drop function myfunc4(int, float); drop table mytable; drop view myview; -- issue #221 create type foo_type as (a int); create table foo(a int); create function testfuncdep() returns void as $$ declare x foo_type; y foo; begin end; $$ language plpgsql; select type, schema, name from plpgsql_show_dependency_tb('testfuncdep'); drop function testfuncdep(); drop type foo_type; drop table foo; -- issue #221 - dependency report on trigger function should not crash -- (NEW and OLD record variables have no datatype) create table foo(a int, b text); create function foo_trg_func() returns trigger as $$ begin new.b := 'x'; return new; end; $$ language plpgsql; create trigger foo_trg before insert on foo for each row execute function foo_trg_func(); select type, schema, name from plpgsql_show_dependency_tb('foo_trg_func', 'foo'); drop trigger foo_trg on foo; drop function foo_trg_func(); drop table foo; -- issue #34 create or replace function testcase() returns bool as $$ declare x int; begin set local search_path to public, test; case x when 1 then return true; else return false; end case; end; $$ language plpgsql; -- should not to raise warning select * from plpgsql_check_function('testcase()'); drop function testcase(); -- Adam's Bartoszewicz example create or replace function public.test12() returns refcursor language plpgsql as $body$ declare rc refcursor; begin open rc scroll for select pc.* from pg_cast pc; return rc; end; $body$; -- should not returns false alarm select * from plpgsql_check_function('test12()'); drop function public.test12(); -- should to show performance warning on bad flag create or replace function flag_test1(int) returns int as $$ begin return $1 + 10; end; $$ language plpgsql stable; create table fufu(a int); create or replace function flag_test2(int) returns int as $$ begin return (select * from fufu limit 1); end; $$ language plpgsql volatile; select * from plpgsql_check_function('flag_test1(int)', performance_warnings := true); select * from plpgsql_check_function('flag_test2(int)', performance_warnings := true); drop table fufu; drop function flag_test1(int); drop function flag_test2(int); create or replace function rrecord01() returns setof record as $$ begin return query select 1,2; end; $$ language plpgsql; create or replace function rrecord02() returns record as $$ begin return row(10,20,30); end; $$ language plpgsql; create type record03 as (a int, b int); create or replace function rrecord03() returns record03 as $$ declare r record; begin r := row(1); return r; end; $$ language plpgsql; -- should not to raise false alarms select * from plpgsql_check_function('rrecord01'); select * from plpgsql_check_function('rrecord02'); -- should detect different return but still detect return select * from plpgsql_check_function('rrecord03', fatal_errors => false); drop function rrecord01(); drop function rrecord02(); drop function rrecord03(); drop type record03; create or replace function bugfunc01() returns void as $$ declare cvar cursor(a int, b int) for select a + b from generate_series(1,b); begin for t in cvar(1,3) loop raise notice '%', t; end loop; end; $$ language plpgsql; select bugfunc01(); select * from plpgsql_check_function('bugfunc01'); create or replace function bugfunc02() returns void as $$ declare cvar cursor(a int, b int) for select a + b from generate_series(1,b); begin open cvar(10,20); close cvar; end; $$ language plpgsql; select bugfunc02(); select * from plpgsql_check_function('bugfunc02'); create or replace function bugfunc03() returns void as $$ declare cvar cursor(a int, b int) for select a + b from not_exists_table; begin open cvar(10,20); close cvar; end; $$ language plpgsql; select bugfunc03(); select * from plpgsql_check_function('bugfunc03'); create or replace function f1(out cr refcursor) as $$ begin end; $$ language plpgsql; -- should to raise warning select * from plpgsql_check_function('f1()'); create or replace function f1(out cr refcursor) as $$ begin open cr for select 1; end; $$ language plpgsql; -- should not to raise warning, see issue #43 select * from plpgsql_check_function('f1()'); drop function f1(); create table testt(a int); create or replace function testt_trg_func() returns trigger as $$ begin return new; end; $$ language plpgsql; create trigger testt_trg before insert or update on testt for each row execute procedure testt_trg_func(); create or replace function maintaince_function() returns void as $$ begin alter table testt disable trigger testt_trg; alter table testt enable trigger testt_trg; end; $$ language plpgsql; -- should not to crash select * from plpgsql_check_function_tb('maintaince_function()', 0, true, true, true); drop function maintaince_function(); drop trigger testt_trg on testt; drop function testt_trg_func(); drop table testt; create or replace function test_crash() returns void as $$ declare ec int default buggyfunc(10); begin select * into ec from buggytab; end; $$ language plpgsql; -- should not to crash select * from plpgsql_check_function('test_crash', fatal_errors := false); select * from plpgsql_check_function('test_crash', fatal_errors := true); drop function test_crash(); -- fix false alarm reported by Piotr Stepniewski create or replace function public.fx() returns void language plpgsql as $function$ begin raise exception 'xxx'; end; $function$; -- show raise nothing select * from plpgsql_check_function('fx()'); create table errtab( message text, code character(5) ); create or replace function public.fx() returns void language plpgsql as $function$ declare var errtab%rowtype; begin raise exception using message = var.message, errcode = var.code; end; $function$; -- should not to crash select * from plpgsql_check_function('fx()'); create or replace function public.fx() returns void language plpgsql as $function$ declare var errtab%rowtype; begin raise exception using message = var.message, errcode = var.code, hint = var.hint; end; $function$; -- should not to crash select * from plpgsql_check_function('fx()'); drop function fx(); create or replace function foo_format(a text, b text) returns void as $$ declare s text; begin s := format('%s'); -- should to raise error s := format('%s %10s', a, b); -- should be ok s := format('%s %s', a, b, a); -- should to raise warning s := format('%s %d', a, b); -- should to raise error raise notice '%', s; end; $$ language plpgsql; select * from plpgsql_check_function('foo_format', fatal_errors := false); drop function foo_format(text, text); create or replace function dyn_sql_1() returns void as $$ declare v varchar; n int; begin execute 'select ' || n; -- ok execute 'select ' || quote_literal(v); -- ok execute 'select ' || v; -- vulnerable execute format('select * from %I', v); -- ok execute format('select * from %s', v); -- vulnerable execute 'select $1' using v; -- ok execute 'select 1'; -- ok execute 'select 1' using v; -- warning execute 'select $1'; -- error end; $$ language plpgsql; select * from plpgsql_check_function('dyn_sql_1', security_warnings := true, fatal_errors := false); drop function dyn_sql_1(); create type tp as (a int, b int); create or replace function dyn_sql_2() returns void as $$ declare r tp; result int; begin select 10 a, 20 b into r; raise notice '%', r.a; execute 'select $1.a + $1.b' into result using r; execute 'select $1.c' into result using r; -- error raise notice '%', result; end; $$ language plpgsql; select * from plpgsql_check_function('dyn_sql_2', security_warnings := true); drop function dyn_sql_2(); drop type tp; /* * Should not to work * * note: plpgsql doesn't support passing some necessary details for record * type. The parser setup for dynamic SQL column doesn't use ref hooks, and * then it cannot to pass TupleDesc info to query anyway. */ create or replace function dyn_sql_2() returns void as $$ declare r record; result int; begin select 10 a, 20 b into r; raise notice '%', r.a; execute 'select $1.a + $1.b' into result using r; raise notice '%', result; end; $$ language plpgsql; select dyn_sql_2(); --should to fail select * from plpgsql_check_function('dyn_sql_2', security_warnings := true); drop function dyn_sql_2(); create or replace function dyn_sql_3() returns void as $$ declare r int; begin execute 'select $1' into r using 1; raise notice '%', r; end $$ language plpgsql; select dyn_sql_3(); -- should be ok select * from plpgsql_check_function('dyn_sql_3'); create or replace function dyn_sql_3() returns void as $$ declare r record; begin execute 'select $1 as a, $2 as b' into r using 1, 2; raise notice '% %', r.a, r.b; end $$ language plpgsql; select dyn_sql_3(); -- should be ok select * from plpgsql_check_function('dyn_sql_3'); create or replace function dyn_sql_3() returns void as $$ declare r record; begin execute 'create table foo(a int)' into r using 1, 2; raise notice '% %', r.a, r.b; end $$ language plpgsql; -- raise a error select * from plpgsql_check_function('dyn_sql_3'); create or replace function dyn_sql_3() returns void as $$ declare r1 int; r2 int; begin execute 'select 1' into r1, r2 using 1, 2; raise notice '% %', r1, r2; end $$ language plpgsql; -- raise a error select * from plpgsql_check_function('dyn_sql_3'); drop function dyn_sql_3(); create or replace function dyn_sql_3() returns void as $$ declare r record; begin for r in execute 'select 1 as a, 2 as b' loop raise notice '%', r.a; end loop; end $$ language plpgsql; -- should be ok select * from plpgsql_check_function('dyn_sql_3'); drop function dyn_sql_3(); create or replace function dyn_sql_3() returns void as $$ declare r record; begin for r in execute 'select 1 as a, 2 as b' loop raise notice '%', r.c; end loop; end $$ language plpgsql; -- should be error select * from plpgsql_check_function('dyn_sql_3'); drop function dyn_sql_3(); create or replace function dyn_sql_3() returns void as $$ declare r record; v text = 'select 10 a, 20 b't; begin for r in execute v loop raise notice '%', r.a; end loop; end $$ language plpgsql; -- should be ok select * from plpgsql_check_function('dyn_sql_3'); drop function dyn_sql_3(); create or replace function dyn_sql_4() returns table(ax int, bx int) as $$ begin return query execute 'select 10, 20'; return; end; $$ language plpgsql; -- should be ok select * from plpgsql_check_function('dyn_sql_4()'); create or replace function dyn_sql_4() returns table(ax int, bx int) as $$ begin return query execute 'select 10, 20, 30'; return; end; $$ language plpgsql; select * from dyn_sql_4(); -- should be error select * from plpgsql_check_function('dyn_sql_4()'); drop function dyn_sql_4(); create or replace function test_bug(text) returns regproc as $$ begin return $1::regproc; exception when undefined_function or invalid_name then raise; end; $$ language plpgsql; -- should not raise a exception select * from plpgsql_check_function('test_bug'); create or replace function test_bug(text) returns regproc as $$ begin return $1::regproc; exception when undefined_function or invalid_name then raise notice '%', $1; -- bug end; $$ language plpgsql; select test_bug('kuku'); -- should to fail select * from plpgsql_check_function('test_bug'); drop function test_bug(text); create or replace function test_bug(text) returns regproc as $$ begin return $1::regproc; exception when undefined_function or invalid_name then raise notice '%', $1; return NULL; end; $$ language plpgsql; select test_bug('kuku'); -- should be ok select * from plpgsql_check_function('test_bug'); drop function test_bug(text); create or replace function foo(a text, b text) returns void as $$ begin -- unsecure execute 'select ' || a; a := quote_literal(a); -- is safe now execute 'select ' || a; a := a || b; -- it is unsecure again execute 'select ' || a; end; $$ language plpgsql; \sf+ foo(text, text) -- should to raise two warnings select * from plpgsql_check_function('foo', security_warnings := true); drop function foo(text, text); create table testr(a int); create rule testr_rule as on insert to testr do nothing; create or replace function fx_testr() returns void as $$ begin insert into testr values(20); end; $$ language plpgsql; -- allow some rules on tables select fx_testr(); select * from plpgsql_check_function_tb('fx_testr'); drop function fx_testr(); drop table testr; create or replace function f() returns void as $$ declare r1 record; r2 record; begin select 10 as a, 20 as b into r1; r2 := json_populate_record(r1, '{}'); raise notice '%', r2.a; end; $$ language plpgsql; select * from plpgsql_check_function('f'); -- fix issue #63 create or replace function distinct_array(arr anyarray) returns anyarray as $$ begin return array(select distinct e from unnest(arr) as e); end; $$ language plpgsql immutable; select plpgsql_check_function('distinct_array(anyarray)'); drop function distinct_array(anyarray); -- tracer test set plpgsql_check.enable_tracer to on; set plpgsql_check.tracer to on; set plpgsql_check.tracer_test_mode = true; -- should not crash do $$ begin raise exception 'raising an exception'; end; $$; do $$ begin do $_$ begin raise exception 'raising an exception'; end $_$; end; $$; create function fx_trace(int) returns int as $$ begin if $1 = 0 then raise exception 'arg is zero'; else return $1; end if; end; $$ language plpgsql; do $$ declare t int; begin t := fx_trace(1); end $$; do $$ declare t int; begin t := fx_trace(0); end $$; drop function fx_trace; \set VERBOSITY terse create or replace function fxo(a int, b int, c date, d numeric) returns void as $$ begin insert into tracer_tab values(a,b,c,d); end; $$ language plpgsql; create table tracer_tab(a int, b int, c date, d numeric); create or replace function tracer_tab_trg_fx() returns trigger as $$ begin return new; end; $$ language plpgsql; create trigger tracer_tab_trg before insert on tracer_tab for each row execute procedure tracer_tab_trg_fx(); select fxo(10,20,'20200815', 3.14); select fxo(11,21,'20200816', 6.28); set plpgsql_check.enable_tracer to off; set plpgsql_check.tracer to off; drop table tracer_tab cascade; drop function tracer_tab_trg_fx(); drop function fxo(int, int, date, numeric); create or replace function foo_trg_func() returns trigger as $$ begin -- bad function, RETURN is missing end; $$ language plpgsql; create table foo(a int); create trigger foo_trg before insert for each row execute procedure foo_trg_func(); -- should to print error select * from plpgsql_check_function('foo_trg_func', 'foo'); drop table foo; drop function foo_trg_func(); -- check event trigger function create or replace function f1() returns event_trigger as $$ BEGIN RAISE NOTICE 'test_event_trigger: % %', tg_event, tg_tag; END $$ language plpgsql; select * from plpgsql_check_function_tb('f1()'); -- should fail create or replace function f1() returns event_trigger as $$ BEGIN RAISE NOTICE 'test_event_trigger: % %', tg_event, tg_tagX; END $$ language plpgsql; select * from plpgsql_check_function_tb('f1()'); drop function f1(); -- check event trigger function create or replace function f1() returns event_trigger as $$ BEGIN RAISE NOTICE 'test_event_trigger: % %', tg_event, tg_tag; END $$ language plpgsql; select * from plpgsql_check_function('f1()'); -- should fail create or replace function f1() returns event_trigger as $$ BEGIN RAISE NOTICE 'test_event_trigger: % %', tg_event, tg_tagX; END $$ language plpgsql; select * from plpgsql_check_function('f1()'); drop function f1(); create table t1tab(a int, b int); create or replace function f1() returns setof t1tab as $$ begin return next (10,20); return; end; $$ language plpgsql; select * from plpgsql_check_function('f1()', performance_warnings => true); create or replace function f1() returns setof t1tab as $$ begin return next (10::numeric,20); return; end; $$ language plpgsql; select * from plpgsql_check_function('f1()', performance_warnings => true); create or replace function f1() returns setof t1tab as $$ declare a int; b int; begin return next (a,b); return; end; $$ language plpgsql; select * from plpgsql_check_function('f1()', performance_warnings => true); create or replace function f1() returns setof t1tab as $$ declare a numeric; b int; begin return next (a,b::numeric); return; end; $$ language plpgsql; select * from plpgsql_check_function('f1()', performance_warnings => true); drop function f1(); create table t1(a int, b int); create or replace function fx() returns t2 as $$ begin return (10,20,30)::t1; end; $$ language plpgsql; select * from plpgsql_check_function('fx()', performance_warnings => true); drop function fx(); drop table t1tab; drop table t1; create or replace function fx() returns void as $$ begin assert exists(select * from foo); assert false, (select boo from boo limit 1); end; $$ language plpgsql; select * from plpgsql_check_function('fx()', fatal_errors => false); create or replace function ml_trg() returns trigger as $$ #option dump declare begin if TG_OP = 'INSERT' then if NEW.status_from IS NULL then begin -- performance issue only select status into NEW.status_from from pa where pa_id = NEW.pa_id; -- nonexist target value select status into NEW.status_from_xxx from pa where pa_id = NEW.pa_id; exception when DATA_EXCEPTION then new.status_from := 'DE'; end; end if; end if; if TG_OP = 'DELETE' then return OLD; else return NEW; end if; exception when OTHERS then NULL; if TG_OP = 'DELETE' then return OLD; else return NEW; end if; end; $$ language plpgsql; select * from plpgsql_check_function('ml_trg()', 'ml', performance_warnings := true); create or replace function fx2() returns void as $$ declare _pa pa; begin select pa.id into _pa.id from pa limit 1; select pa.pa_id into _pa.pa_id from pa limit 1; end; $$ language plpgsql; select * from plpgsql_check_function('fx2()', performance_warnings := true); drop function fx2(); create or replace function fx2() returns void as $$ declare _pa pa; begin _pa.id := (select pa.id from pa limit 1); _pa.pa_id := (select pa.pa_id from pa limit 1); end; $$ language plpgsql; select * from plpgsql_check_function('fx2()', performance_warnings := true); drop function fx2(); create type _exception_type as ( state text, message text, detail text); create or replace function f1() returns void as $$ declare _exception record; begin _exception := NULL::_exception_type; exception when others then get stacked diagnostics _exception.state = RETURNED_SQLSTATE, _exception.message = MESSAGE_TEXT, _exception.detail = PG_EXCEPTION_DETAIL, _exception.hint = PG_EXCEPTION_HINT; end; $$ language plpgsql; select f1(); select * from plpgsql_check_function_tb('f1()'); create or replace function f1() returns void as $$ declare _exception _exception_type; begin _exception := NULL::_exception_type; exception when others then get stacked diagnostics _exception.state = RETURNED_SQLSTATE, _exception.message = MESSAGE_TEXT, _exception.detail = PG_EXCEPTION_DETAIL; end; $$ language plpgsql; select f1(); select * from plpgsql_check_function_tb('f1()'); drop function f1(); drop type _exception_type; create type _exception_type as ( state text, message text, detail text); create or replace function f1() returns void as $$ declare _exception record; begin _exception := NULL::_exception_type; exception when others then get stacked diagnostics _exception.state = RETURNED_SQLSTATE, _exception.message = MESSAGE_TEXT, _exception.detail = PG_EXCEPTION_DETAIL, _exception.hint = PG_EXCEPTION_HINT; end; $$ language plpgsql; select f1(); select * from plpgsql_check_function('f1()'); drop function f1(); drop type _exception_type; create table footab(a int, b int, c int); create or replace function footab_trig_func() returns trigger as $$ declare x int; begin if false then -- should be ok; select count(*) from newtab into x; -- should fail; select count(*) from newtab where d = 10 into x; end if; return null; end; $$ language plpgsql; select * from plpgsql_check_function('footab_trig_func','footab', newtable := 'newtab'); drop table footab; drop function footab_trig_func(); create or replace function df1(anyelement) returns anyelement as $$ begin return $1; end; $$ language plpgsql; create or replace function df2(anyelement, jsonb) returns anyelement as $$ begin return $1; end; $$ language plpgsql; create or replace function t1() returns void as $$ declare r record; begin r := df1(r); end; $$ language plpgsql; select * from plpgsql_check_function('t1()'); create or replace function t1() returns void as $$ declare r record; begin r := df2(r, '{}'); end; $$ language plpgsql; select * from plpgsql_check_function('t1()'); create or replace function t1() returns void as $$ declare r1 record; r2 record; begin select 10 as a, 20 as b into r1; r2 := df1(r1); raise notice '%', r2.a; end; $$ language plpgsql; select * from plpgsql_check_function('t1()'); create or replace function t1() returns void as $$ declare r1 record; r2 record; begin select 10 as a, 20 as b into r1; r2 := df2(r1, '{}'); raise notice '%', r2.a; end; $$ language plpgsql; select * from plpgsql_check_function('t1()'); create or replace function df1(anyelement) returns anyelement as $$ select $1 $$ language sql; create or replace function df22(jsonb, anyelement) returns anyelement as $$ select $2; $$ language sql; create or replace function t1() returns void as $$ declare r1 record; r2 record; begin select 10 as a, 20 as b into r1; r2 := df1(r1); raise notice '%', r2.a; end; $$ language plpgsql; select * from plpgsql_check_function('t1()'); create or replace function t1() returns void as $$ declare r1 record; r2 record; begin select 10 as a, 20 as b into r1; r2 := df22('{}', r1); raise notice '%', r2.a; end; $$ language plpgsql; select * from plpgsql_check_function('t1()'); drop function df1(anyelement); drop function df2(anyelement, jsonb); drop function df22(jsonb, anyelement); drop function t1(); create or replace function dyntest() returns void as $$ begin execute 'drop table if exists xxx; create table xxx(a int)'; end; $$ language plpgsql; -- should be ok select * from plpgsql_check_function('dyntest'); create or replace function dyntest() returns void as $$ declare x int; begin execute 'drop table if exists xxx; create table xxx(a int)' into x; end; $$ language plpgsql; -- should to report error select * from plpgsql_check_function('dyntest'); drop function dyntest(); -- should to report error create type typ2 as (a int, b int); create or replace function broken_into() returns void as $$ declare v typ2; begin -- should to fail select (10,20)::typ2 into v; -- should be ok select ((10,20)::typ2).* into v; -- should to fail execute 'select (10,20)::typ2' into v; -- should be ok execute 'select ((10,20)::typ2).*' into v; end; $$ language plpgsql; select * from plpgsql_check_function('broken_into', fatal_errors => false); drop function broken_into(); drop type typ2; -- check output in xml or json formats CREATE OR REPLACE FUNCTION test_function() RETURNS void LANGUAGE plpgsql AS $function$ begin insert into non_existing_table values (1); end $function$; select * from plpgsql_check_function('test_function', format => 'xml'); select * from plpgsql_check_function('test_function', format => 'json'); drop function test_function(); -- test settype pragma create or replace function test_function() returns void as $$ declare r record; begin raise notice '%', r.a; end; $$ language plpgsql; -- should to detect error select * from plpgsql_check_function('test_function'); create type ctype as (a int, b int); create or replace function test_function() returns void as $$ declare r record; begin perform plpgsql_check_pragma('type: r ctype'); raise notice '%', r.a; end; $$ language plpgsql; -- should to be ok select * from plpgsql_check_function('test_function'); create or replace function test_function() returns void as $$ <>declare r record; begin perform plpgsql_check_pragma('type: x.r public."ctype"'); raise notice '%', r.a; end; $$ language plpgsql; -- should to be ok select * from plpgsql_check_function('test_function'); create or replace function test_function() returns void as $$ <>declare r record; begin perform plpgsql_check_pragma('type: "x".r (a int, b int)'); raise notice '%', r.a; end; $$ language plpgsql; -- should to be ok select * from plpgsql_check_function('test_function'); create or replace function test_function() returns void as $$ <>declare r record; begin perform plpgsql_check_pragma('type: "x".r (a int, b int'); raise notice '%', r.a; end; $$ language plpgsql; -- should to be ok select * from plpgsql_check_function('test_function'); create or replace function test_function() returns void as $$ <>declare r record; begin perform plpgsql_check_pragma('type: "x".r (a int, b int)x'); raise notice '%', r.a; end; $$ language plpgsql; -- should to be ok select * from plpgsql_check_function('test_function'); drop function test_function(); drop type ctype; create or replace function test_function() returns void as $$ declare r pg_class; begin create temp table foo(like pg_class); select * from foo into r; end; $$ language plpgsql; -- should to raise an error select * from plpgsql_check_function('test_function'); create or replace function test_function() returns void as $$ declare r record; begin create temp table foo(like pg_class); perform plpgsql_check_pragma('table: foo(like pg_class)'); select * from foo into r; raise notice '%', r.relname; end; $$ language plpgsql; -- should be ok select * from plpgsql_check_function('test_function'); drop function test_function(); -- now plpgsql_check can do some other checks when statement EXECUTE -- contains only format function with constant fmt. create or replace function test_function() returns void as $$ begin execute format('create table zzz %I(a int, b int)', 'zzz'); end; $$ language plpgsql; -- should to detect bad expression select * from plpgsql_check_function('test_function'); -- should to correctly detect type create or replace function test_function() returns void as $$ declare r record; begin execute format('select %L::date + 1 as x', current_date) into r; raise notice '%', extract(dow from r.x); end; $$ language plpgsql; -- should be ok select * from plpgsql_check_function('test_function'); -- should not to crash create or replace function test_function() returns void as $$ declare r record; begin r := null; end; $$ language plpgsql; select * from plpgsql_check_function('test_function'); drop function test_function(); -- ignores syntax errors when literals placehodlers are used create function test_function() returns void as $$ begin execute format('do %L', 'begin end'); end $$ language plpgsql; select * from plpgsql_check_function('test_function'); drop function test_function(); load 'plpgsql_check'; drop type testtype cascade; create type testtype as (a int, b int); create function test_function() returns record as $$ declare r record; begin r := (10,20); if false then return r; end if; return null; end; $$ language plpgsql; create function test_function33() returns record as $$ declare r testtype; begin r := (10,20); if false then return r; end if; return null; end; $$ language plpgsql; -- should not to raise false alarm due check against fake result type select plpgsql_check_function('test_function'); select plpgsql_check_function('test_function33'); -- try to check in passive mode set plpgsql_check.mode = 'every_start'; select test_function(); select test_function33(); select * from test_function() as (a int, b int); select * from test_function33() as (a int, b int); -- should to identify error select * from test_function() as (a int, b int, c int); select * from test_function33() as (a int, b int, c int); drop function test_function(); drop function test_function33(); drop type testtype; set plpgsql_check.mode to default; -- should not to raise false alarm create type c1 as ( a text ); create table t1 ( a c1, b c1 ); insert into t1 (values ('(abc)', '(def)')); alter table t1 drop column a; create or replace function test_function() returns t1 as $$ declare myrow t1%rowtype; begin select * into myrow from t1 limit 1; return myrow; end; $$ language plpgsql; select * from test_function(); select * from plpgsql_check_function('public.test_function()'); drop function test_function(); drop table t1; drop type c1; -- compatibility warnings create or replace function foo01() returns refcursor as $$ declare c cursor for select 1; r refcursor; begin open c; r := c; return r; end; $$ language plpgsql; -- no warnings select * from plpgsql_check_function('foo01', compatibility_warnings => true); create or replace function foo01() returns refcursor as $$ declare c cursor for select 1; r refcursor; begin open c; r := 'c'; return r; end; $$ language plpgsql; -- warning select * from plpgsql_check_function('foo01', extra_warnings => false, compatibility_warnings => true); create or replace function foo01() returns refcursor as $$ declare c cursor for select 1; r refcursor; begin open c; r := c; return 'c'; end; $$ language plpgsql; -- warning select * from plpgsql_check_function('foo01', extra_warnings => false, compatibility_warnings => true); -- pragma sequence test create or replace function test_function() returns void as $$ begin perform plpgsql_check_pragma('sequence: xx'); perform nextval('pg_temp.xy'); perform nextval('pg_temp.xx'); end $$ language plpgsql; select * from plpgsql_check_function('test_function'); drop function test_function(); create table t1(x int); create or replace function f1_trg() returns trigger as $$ declare x int; begin return x; end; $$ language plpgsql; create trigger t1_f1 before insert on t1 for each row execute procedure f1_trg(); -- raise error select * from plpgsql_check_function('f1_trg', 't1'); drop trigger t1_f1 on t1; drop table t1; drop function f1_trg; create function test_function() returns void as $$ declare a int; b int; c int; d char; begin c := a + d; end; $$ language plpgsql; -- only b should be marked as unused variable select * from plpgsql_check_function('test_function', fatal_errors := false); drop function test_function(); -- from plpgsql_check_active-12 create or replace procedure proc(a int) as $$ begin end; $$ language plpgsql; call proc(10); select * from plpgsql_check_function('proc(int)'); create or replace procedure testproc() as $$ begin call proc(10); end; $$ language plpgsql; call testproc(); select * from plpgsql_check_function('testproc()'); -- should to fail create or replace procedure testproc() as $$ begin call proc((select count(*) from pg_class)); end; $$ language plpgsql; call testproc(); select * from plpgsql_check_function('testproc()'); drop procedure proc(int); create procedure proc(in a int, inout b int, in c int) as $$ begin end; $$ language plpgsql; select * from plpgsql_check_function('proc(int,int, int)'); create or replace procedure proc(in a int, inout b int, in c int) as $$ begin b := a + c; end; $$ language plpgsql; select * from plpgsql_check_function('proc(int,int, int)'); create or replace procedure testproc() as $$ declare r int; begin call proc(10, r, 20); end; $$ language plpgsql; call testproc(); select * from plpgsql_check_function('testproc()'); -- should to fail create or replace procedure testproc() as $$ declare r int; begin call proc(10, r + 10, 20); end; $$ language plpgsql; call testproc(); select * from plpgsql_check_function('testproc()'); create or replace procedure testproc(inout r int) as $$ begin call proc(10, r, 20); end; $$ language plpgsql; call testproc(10); select * from plpgsql_check_function('testproc(int)'); drop procedure testproc(int); -- should to raise warnings create or replace procedure testproc2(in p1 int, inout p2 int, in p3 int, inout p4 int) as $$ begin raise notice '% %', p1, p3; end; $$ language plpgsql; select * from plpgsql_check_function('testproc2'); drop procedure testproc2; -- should be ok create or replace procedure testproc3(in p1 int, inout p2 int, in p3 int, inout p4 int) as $$ begin p2 := p1; p4 := p3; end; $$ language plpgsql; select * from plpgsql_check_function('testproc3'); drop procedure testproc3; /* * Test pragma */ create or replace function test_pragma() returns void language plpgsql as $$ declare r record; begin perform plpgsql_check_pragma('disable:check'); raise notice '%', r.y; perform plpgsql_check_pragma('enable:check'); select 10 as a, 20 as b into r; raise notice '%', r.a; raise notice '%', r.x; end; $$; select * from plpgsql_check_function('test_pragma'); create or replace function test_pragma() returns void language plpgsql as $$ declare r record; begin if false then -- check is disabled just for if body perform plpgsql_check_pragma('disable:check'); raise notice '%', r.y; end if; select 10 as a, 20 as b into r; raise notice '%', r.a; raise notice '%', r.x; end; $$; select * from plpgsql_check_function('test_pragma'); drop function test_pragma(); create or replace function nested_trace_test(a int) returns int as $$ begin return a + 1; end; $$ language plpgsql; create or replace function trace_test(b int) returns int as $$ declare r int default 0; begin for i in 1..b loop r := nested_trace_test(r); end loop; return r; end; $$ language plpgsql; select trace_test(3); set plpgsql_check.enable_tracer to on; set plpgsql_check.tracer to on; set plpgsql_check.tracer_test_mode = true; select trace_test(3); set plpgsql_check.tracer_verbosity TO verbose; select trace_test(3); create or replace function trace_test(b int) returns int as $$ declare r int default 0; begin for i in 1..b loop perform plpgsql_check_pragma('disable:tracer'); r := nested_trace_test(r); end loop; return r; end; $$ language plpgsql; select trace_test(3); create or replace function nested_trace_test(a int) returns int as $$ begin perform plpgsql_check_pragma('enable:tracer'); return a + 1; end; $$ language plpgsql; select trace_test(3); drop function trace_test(int); drop function nested_trace_test(int); create or replace function trace_test(int) returns int as $$ declare r int default 0; begin for i in 1..$1 loop r := r + 1; end loop; r := r + 10; return r; end; $$ language plpgsql; select trace_test(4); create or replace function trace_test(int) returns int as $$ declare r int default 0; begin for i in 1..$1 loop perform plpgsql_check_pragma('disable:tracer'); r := r + 1; end loop; r := r + 10; return r; end; $$ language plpgsql; select trace_test(4); create or replace function trace_test(int) returns int as $$ declare r int default 0; begin perform plpgsql_check_pragma('disable:tracer'); for i in 1..$1 loop r := r + 1; end loop; perform plpgsql_check_pragma('enable:tracer'); r := r + 10; return r; end; $$ language plpgsql; select trace_test(4); drop function trace_test(int); create or replace function public.fx1() returns table(i integer, j integer, found boolean) as $$ begin for i in 1..10 loop for j in 1..10 loop return next; end loop; end loop; end; $$ language plpgsql immutable; select * from plpgsql_check_function('fx1'); drop function fx1; -- tracer test set plpgsql_check.enable_tracer to on; select plpgsql_check_tracer(true); create role regress_plpgsql_check_test_role; DO $$ begin begin -- should to fail create role regress_plpgsql_check_test_role; exception when duplicate_object then -- Role already exists -- the exception handler is empty (#156) end; end; $$; drop role regress_plpgsql_check_test_role; set plpgsql_check.enable_tracer to off; select plpgsql_check_tracer(false); -- tracing constants -- issue #159 create or replace function tabret_dynamic() returns table (id integer, val text) as $$ begin return query execute 'select id, val from (values (1, ''a''), (2, ''b'')) as v(id, val)'; end; $$ language plpgsql immutable; -- should be ok select * from plpgsql_check_function('tabret_dynamic()'); create or replace function tabret_dynamic() returns table (id integer, val text) as $$ declare z_query text; begin z_query := 'select id, val from (values (1, ''a''), (2, ''b'')) as v(id, val)'; execute z_query into id, val; return next; end; $$ language plpgsql immutable; -- should be ok select * from plpgsql_check_function('tabret_dynamic()'); create or replace function tabret_dynamic() returns table (id integer, val text) as $$ declare z_query text; begin z_query := 'select id, val from (values (1, ''a''), (2, ''b'')) as v(id, val)'; for id, val in execute z_query loop return next; end loop; end; $$ language plpgsql immutable; -- should be ok select * from plpgsql_check_function('tabret_dynamic()'); create or replace function tabret_dynamic() returns table (id integer, val text) as $$ declare z_query text; begin z_query := 'select id, val from (values (1, ''a''), (2, ''b'')) as v(id, val)'; return query execute z_query; end; $$ language plpgsql immutable; -- should be ok select * from plpgsql_check_function('tabret_dynamic()'); drop function tabret_dynamic; -- should not to crash on empty string, or comment create or replace function dynamic_emptystr() returns void as $$ begin execute ''; end; $$ language plpgsql; -- should be ok select * from plpgsql_check_function('dynamic_emptystr()'); create or replace function dynamic_emptystr() returns void as $$ declare x int; begin execute '--' into x; end; $$ language plpgsql; -- should not to crash, no result error select * from plpgsql_check_function('dynamic_emptystr()'); drop function dynamic_emptystr(); -- unclosed cursor detection create or replace function fx() returns void as $$ declare c refcursor; begin open c for select * from pg_class; end; $$ language plpgsql; do $$ begin perform fx(); -- should to show warning perform fx(); end; $$; set plpgsql_check.strict_cursors_leaks to on; do $$ begin -- should to show warning perform fx(); -- should to show warning perform fx(); end; $$; create or replace function fx() returns void as $$ declare c refcursor; begin open c for select * from pg_class; close c; end; $$ language plpgsql; -- without warnings do $$ begin perform fx(); perform fx(); end; $$; set plpgsql_check.strict_cursors_leaks to off; drop function fx(); create table public.testt(a int, b int); create or replace function fx() returns void as $$ declare v1 varchar default 'public'; v2 varchar default 'testt'; v3 varchar default 'a'; begin raise notice '%', format('%I.%I.%I', v1, v2, v3); perform 'pragma:assert-schema: v1'; perform 'pragma:assert-table: v1, v2'; perform 'pragma:assert-column: v1, v2, v3'; perform 'pragma:assert-table: v2'; perform 'pragma:assert-column: v2, v3'; end; $$ language plpgsql; select * from plpgsql_check_function('fx()'); create table public.testt(a int, b int); create or replace function fx() returns void as $$ declare v1 varchar default 'public'; v2 varchar default 'testt'; v3 varchar default 'x'; begin raise notice '%', format('%I.%I.%I', v1, v2, v3); perform 'pragma:assert-column: v1, v2, v3'; end; $$ language plpgsql; select * from plpgsql_check_function('fx()'); drop function fx(); drop table public.testt; create or replace function fx() returns void as $$ declare v_sql varchar default ''; i int; begin -- we don't support constant tracing from queries select 'bla bla bla' into v_sql; execute v_sql into i; raise notice '%', i; end; $$ language plpgsql; -- should be ok select * from plpgsql_check_function('fx()'); drop function fx(); -- should not to raise warning do $$ declare c cursor for select * from generate_series(1,10); t int; begin for i in 1..2 loop open c; loop fetch c into t; exit when not found; raise notice '%', t; end loop; close c; end loop; end; $$; -- should not raise warning create or replace function fx(p text) returns void as $$ begin execute format($_$select '%1$I'$_$, p); end; $$ language plpgsql; -- should be ok select * from plpgsql_check_function('fx(text)'); drop function fx(text); create or replace function fx() returns void as $$ declare p varchar; begin p := 'ahoj'; execute format($_$select '%1$I'$_$, p); end; $$ language plpgsql; -- should be ok select * from plpgsql_check_function('fx()'); drop function fx(); -- should not crash create or replace procedure p1() as $$ begin commit; end; $$ language plpgsql; set plpgsql_check.cursors_leaks to on; do $$ declare c cursor for select 1; begin open c; call p1(); end $$; set plpgsql_check.cursors_leaks to default; drop procedure p1; -- should not crash create or replace function nested_trace_test(a int) returns int as $$ begin perform plpgsql_check_pragma('enable:tracer'); return a + 1; end; $$ language plpgsql; create or replace function trace_test(b int) returns int as $$ declare r int default 0; begin for i in 1..b loop r := nested_trace_test(r); end loop; return r; end; $$ language plpgsql; -- when plpgsql_check is not loaded yet, then plpgsql_check is -- load by perform plpgsql_check_pragma and this is another -- case, when fmgr hook is not called in expected order. \c -- should not to crash select trace_test(3); create table testtable_pure_expr(a int); -- detection of not pure expressions create or replace function pure_expr() returns void as $$ declare v int; begin v := 1 delete from testtable_pure_expr where a = 10; raise notice '%', v; end; $$ language plpgsql; -- raise warning select * from plpgsql_check_function('pure_expr()'); drop table testtable_pure_expr; -- should not to raise false alarm drop function fxx(ct, ct); drop type ct; create type ct as (x int, y numeric, v varchar); create procedure proc_composite(inout cv ct, x int, y numeric, v varchar) as $$ begin cv.x := x; cv.y := y; cv.v := v; end; $$ language plpgsql; call proc_composite(null, 10, 10.2, 'ahoj'); create or replace function test_proc_composite() returns void as $$ declare cv ct; begin call proc_composite(cv, 10, 10.2, 'ahoj'); end; $$ language plpgsql; select * from plpgsql_check_function('test_proc_composite'); drop function test_proc_composite; drop procedure proc_composite; create procedure proc_composite(inout cv1 ct, cv2 ct) as $$ begin cv1.x := 10; cv2.v := 'Ahoj'; end; $$ language plpgsql; create or replace function test_proc_composite() returns void as $$ declare cv1 ct; cv2 ct; begin call proc_composite(cv1, cv2); end; $$ language plpgsql; select * from plpgsql_check_function('test_proc_composite'); drop function test_proc_composite; drop procedure proc_composite; drop type ct; create or replace function double_usage_of_const_str() returns void as $$ declare p_id_arr integer[]; r_id integer; begin p_id_arr := '{1,2,3}'::integer[]; foreach r_id in array p_id_arr loop raise notice 'id: %', r_id; end loop; --the following loop causes an error foreach r_id in array p_id_arr loop raise notice 'id: %', r_id; end loop; end; $$ language plpgsql; -- should not to raise false alarm #195 select * from plpgsql_check_function('double_usage_of_const_str'); -- using reserved world for names is bad create or replace function test_bad_label() returns int as $$ <> declare x int = 10; begin return x; end; $$ language plpgsql; select * from plpgsql_check_function('test_bad_label'); drop function test_bad_label; -- using slow legacy syntax for assignment create or replace function test_bad_assign() returns int as $$ declare x int; begin select 0 into x; return x; end; $$ language plpgsql immutable; select * from plpgsql_check_function('test_bad_assign()', all_warnings=>true); drop function test_bad_assign; create or replace function volatility_check() returns int as $$ begin return (select count(*) from pg_class); end; $$ language plpgsql volatile; -- should be ok select * from plpgsql_check_function('volatility_check()'); create or replace function volatility_check() returns int as $$ begin return (select count(*) from pg_class); end; $$ language plpgsql stable; -- should be ok select * from plpgsql_check_function('volatility_check()'); create or replace function volatility_check() returns int as $$ begin return (select count(*) from pg_class); end; $$ language plpgsql immutable; -- warning select * from plpgsql_check_function('volatility_check()'); create or replace function volatility_check() returns int as $$ begin return random()*1000; end; $$ language plpgsql volatile; -- should be ok select * from plpgsql_check_function('volatility_check()'); create or replace function volatility_check() returns int as $$ begin return random()*1000; end; $$ language plpgsql stable; -- warning select * from plpgsql_check_function('volatility_check()'); drop function volatility_check(); -- issue #201 - should not to raise false error, when composite -- constant is used and assigned to composite with more fields create type t_retcode as (retcode varchar, errmsg varchar, id int); create or replace function test_retcode_bad() returns void as $$ declare rc t_retcode := row('MO_REG_ERROR'); begin raise notice '%', rc.retcode; end; $$ language plpgsql; select * from plpgsql_check_function('test_retcode_bad'); drop function test_retcode_bad; -- test usage domain of composite in output #202 create domain t_retcode_domain as t_retcode; create or replace function test_retcode_bad(INOUT x1 t_retcode_domain, INOUT x2 int) as $$ begin x1.retcode = concat(x1.retcode, 'xxx'); x2 := x2 + 10; end; $$ language plpgsql; select * from plpgsql_check_function('test_retcode_bad'); drop function test_retcode_bad; drop type t_retcode cascade; create type demo_payload as (value integer); create function demo_produce_row(out status_code integer, out payload record) as $$ begin status_code := 0; payload := ROW(1)::demo_payload; end $$ language plpgsql; create function demo_consume_row() returns void as $$ declare fetched_row record; typed_payload demo_payload; observed_status integer; begin select * into strict fetched_row from demo_produce_row(); typed_payload := fetched_row.payload; observed_status := fetched_row.status_code; end; $$ language plpgsql; -- should not crash - issue #216 select * from plpgsql_check_function('demo_consume_row()'); -- should not crash - issue #220 create function f_poly(anyelement) returns int as $$ begin return $1; end; $$ language plpgsql; select plpgsql_check_profiler(true); select f_poly(1); select plpgsql_coverage_statements('f_poly(anyelement)'); select plpgsql_coverage_statements('f_poly'); select plpgsql_check_profiler(false); drop function f_poly; create or replace function repro04() returns void as $$ begin execute 'select 1'; end $$ language plpgsql; select * from plpgsql_check_function('repro04()', performance_warnings => true); create or replace function repro04_into() returns int as $$ declare r int; begin execute 'select $1' into r using 1; return r; end $$ language plpgsql; select * from plpgsql_check_function('repro04_into()', performance_warnings => true); create or replace function repro04_real_dynsql(p text) returns void as $$ begin execute 'select ' || quote_literal(p); end $$ language plpgsql; select * from plpgsql_check_function('repro04_real_dynsql(text)', performance_warnings => true); -- a multi statement command is real dynamic sql too, and this case was -- reported correctly even before the fix. create or replace function repro04_multi() returns void as $$ begin execute 'select 1; select 2'; end $$ language plpgsql; select * from plpgsql_check_function('repro04_multi()', performance_warnings => true); create table repro04_multi_target(a int); create or replace function repro04_multi() returns void as $$ begin execute 'select 1; insert into repro04_multi_target values(10)'; end $$ language plpgsql; select * from plpgsql_check_function('repro04_multi()', performance_warnings => true); drop table repro04_multi_target; drop function repro04(); drop function repro04_into(); drop function repro04_real_dynsql(text); drop function repro04_multi(); create or replace function unpriv_issue4_rowexpr() returns void as $$ declare r record; begin select (row(1, 'hello'::text)).* into r; raise notice '%', r.f1; select row(1, 'hello') into r; raise notice '%', r.row; end; $$ language plpgsql; select unpriv_issue4_rowexpr(); -- there should not be any false errors select * from plpgsql_check_function_tb('unpriv_issue4_rowexpr()'); drop function unpriv_issue4_rowexpr(); okbob-plpgsql_check-800db79/sql/plpgsql_check_assign.sql000066400000000000000000000276361526744060500235140ustar00rootroot00000000000000set client_min_messages to warning; create extension if not exists plpgsql_check; set client_min_messages to notice; -- -- assignment to the automatic variables -- -- the variable of a numeric FOR loop is maintained by the runtime, so it -- should not be modified by the body of the loop create function as_f1(n int) returns int as $$ declare s int := 0; begin for i in 1..n loop s := s + i; i := i + 1; end loop; return s; end; $$ language plpgsql; select * from plpgsql_check_function('as_f1', extra_warnings => false); select * from plpgsql_check_function('as_f1', extra_warnings => true); -- the same for the FOUND variable create function as_f2() returns int as $$ begin found := true; return 1; end; $$ language plpgsql; select * from plpgsql_check_function('as_f2', extra_warnings => true); -- -- assignment to a constant -- -- the default value of a constant is assigned at the start of the block, -- and it is not reported create function as_f4() returns int as $$ declare c constant int := 1; begin return c; end; $$ language plpgsql; select * from plpgsql_check_function('as_f4'); -- -- assignment to a field of a record -- -- the structure of a record is known after the first assignment only create function as_f5() returns int as $$ declare r record; begin r.a := 1; return r.a; end; $$ language plpgsql; select * from plpgsql_check_function('as_f5'); -- when the record has a structure, the name of the field is validated create function as_f6() returns int as $$ declare r record; begin select 1 as a, 2 as b into r; r.c := 1; return r.a; end; $$ language plpgsql; select * from plpgsql_check_function('as_f6'); -- and the type of the field is used for the check of the assigned value create function as_f7() returns int as $$ declare r record; begin select 1 as a, 2 as b into r; r.a := 'x'; return r.a; end; $$ language plpgsql; select * from plpgsql_check_function('as_f7'); -- -- checks of the assigned type -- create table as_tab1(a int, b int, c int); -- a composite value cannot be assigned to a scalar variable create function as_f8() returns int as $$ declare x int; begin select t into x from as_tab1 t; return x; end; $$ language plpgsql; select * from plpgsql_check_function('as_f8'); -- a cast which is only explicit, a cast which is allowed on assignment -- and a cast which is done implicitly create function as_f9() returns void as $$ declare i int; t text; n numeric; begin select 'x'::text into i; select 1 into t; select 1.5::numeric into i; select 1 into n; end; $$ language plpgsql; select * from plpgsql_check_function('as_f9', fatal_errors => false); select * from plpgsql_check_function('as_f9', fatal_errors => false, performance_warnings => true); -- -- assignment of a query result to a composite variable -- -- a variable of a table rowtype where the table has a dropped column - -- the descriptors are not equal, but the dropped column is skipped on -- both sides alter table as_tab1 drop column b; create function as_f10() returns int as $$ declare r as_tab1; begin select * into r from as_tab1; return r.a; end; $$ language plpgsql; select * from plpgsql_check_function('as_f10'); -- the query returns less columns than the variable has create function as_f11() returns int as $$ declare r as_tab1; begin select a into r from as_tab1; return r.a; end; $$ language plpgsql; select * from plpgsql_check_function('as_f11'); -- and more columns than the variable has create function as_f12() returns int as $$ declare r as_tab1; begin select a, c, a from as_tab1 into r; return r.a; end; $$ language plpgsql; select * from plpgsql_check_function('as_f12'); -- the types of the columns are compared one by one create function as_f13() returns int as $$ declare r as_tab1; begin select 'x'::text, 1 into r; return r.a; end; $$ language plpgsql; select * from plpgsql_check_function('as_f13', fatal_errors => false); -- a whole row value of a type with a dropped column is assigned to a -- record variable create function as_f14() returns int as $$ declare r record; begin select t.* into r from as_tab1 t; return r.a; end; $$ language plpgsql; select * from plpgsql_check_function('as_f14'); -- the NEW and the OLD records of a trigger get the descriptor of the -- triggering relation, which contains the dropped column too create function as_trg1() returns trigger as $$ begin new.a := old.a; new.c := 1; return new; end; $$ language plpgsql; create trigger as_trg1 before update on as_tab1 for each row execute procedure as_trg1(); select * from plpgsql_check_function('as_trg1', relid => 'as_tab1'::regclass); -- FOREACH applies assignment casts to elements and slices, not array input. create function as_f15() returns int as $$ declare item int; total int := 0; begin foreach item in array array[1.6::numeric, 2.4::numeric] loop total := total + item; end loop; return total; end; $$ language plpgsql; select * from plpgsql_check_function('as_f15'); select as_f15(); create function as_f16() returns int[] as $$ declare item int[]; begin foreach item slice 1 in array array[[1.6::numeric, 2.4::numeric]] loop return item; end loop; return null; end; $$ language plpgsql; select * from plpgsql_check_function('as_f16'); select as_f16(); -- Without an assignment cast, invalid textual input is still rejected. create function as_f17() returns void as $$ declare item int; begin foreach item in array array['not an integer'] loop raise notice '%', item; end loop; end; $$ language plpgsql; select * from plpgsql_check_function('as_f17'); drop function as_f15(); drop function as_f16(); drop function as_f17(); drop function as_f1(int); drop function as_f2(); drop function as_f4(); drop function as_f5(); drop function as_f6(); drop function as_f7(); drop function as_f8(); drop function as_f9(); drop function as_f10(); drop function as_f11(); drop function as_f12(); drop function as_f13(); drop function as_f14(); drop trigger as_trg1 on as_tab1; drop function as_trg1(); -- A composite source may have a valid scalar assignment cast, or be NULL. create type as_pair as (a int, b int); create function as_f18() returns text as $$ declare value text; begin select row(1, 2) into value; return value; end; $$ language plpgsql; select * from plpgsql_check_function('as_f18'); select as_f18(); create function as_f19() returns int as $$ declare value int; begin select null::as_tab1 into value; return value; end; $$ language plpgsql; select * from plpgsql_check_function('as_f19'); select as_f19() is null; create function as_pair_to_int(p as_pair) returns int as $$ select p.a + p.b $$ language sql immutable; create cast (as_pair as int) with function as_pair_to_int(as_pair) as assignment; create function as_f20() returns int as $$ declare value int; begin select row(1, 2)::as_pair into value; return value; end; $$ language plpgsql; select * from plpgsql_check_function('as_f20'); select as_f20(); drop function as_f18(); drop function as_f19(); drop function as_f20(); drop cast (as_pair as int); drop function as_pair_to_int(as_pair); drop type as_pair; -- RETURN QUERY retains composite-valued columns; RETURN expands a row value. create type as_leaf as (n int); create type as_wrapper as (payload as_leaf); create function as_f21() returns setof as_wrapper as $$ begin return query select row(7)::as_leaf; end; $$ language plpgsql; select * from plpgsql_check_function('as_f21'); select (payload).n from as_f21(); create function as_f22() returns setof as_wrapper as $$ begin return query execute 'select row(7)::as_leaf'; end; $$ language plpgsql; select * from plpgsql_check_function('as_f22'); select (payload).n from as_f22(); create function as_f23() returns setof as_leaf as $$ begin return query select row(7)::as_leaf; end; $$ language plpgsql; select * from plpgsql_check_function('as_f23'); select * from as_f23(); create function as_f24() returns as_leaf as $$ begin return row(7)::as_leaf; end; $$ language plpgsql; select * from plpgsql_check_function('as_f24'); select (as_f24()).n; create function as_f25() returns setof as_leaf as $$ begin return next row(7)::as_leaf; return; end; $$ language plpgsql; select * from plpgsql_check_function('as_f25'); select * from as_f25(); -- Bounds and the RHS read their variables; the implicit old array does not. create function as_f26() returns void as $$ declare element_array int[] := array[1, 2]; lower_array int[] := array[1, 2]; upper_array int[] := array[1, 2]; rhs_array int[] := array[1, 2]; fetch_array int[] := array[1, 2]; write_only int[] := array[1, 2]; begin element_array[element_array[1]] := 3; lower_array[lower_array[1]:2] := array[3, 4]; upper_array[1:upper_array[2]] := array[3, 4]; rhs_array[1] := rhs_array[2]; fetch_array := fetch_array[1:2]; write_only[1] := 3; end; $$ language plpgsql; select * from plpgsql_check_function('as_f26'); select as_f26(); drop function as_f26(); drop function as_f21(); drop function as_f22(); drop function as_f23(); drop function as_f24(); drop function as_f25(); drop type as_wrapper; drop type as_leaf; drop table as_tab1; create type point2 as (x int, y int); create domain dpoint as point2 check ((value).x is not null); -- a) domain-over-composite as a parameter create function dom_param(p dpoint) returns int language plpgsql as $$ begin return p.x; end; $$; -- b) domain-over-composite as an initialized local variable create function dom_var() returns int language plpgsql as $$ declare r dpoint := row(1, 2)::dpoint; begin return r.x; end; $$; select count(*) = 0 from plpgsql_check_function_tb('dom_param(dpoint)', fatal_errors => false) where message like '%violates check constraint%'; select count(*) = 0 from plpgsql_check_function_tb('dom_var()', fatal_errors => false) where message like '%violates check constraint%'; drop function dom_var; drop function dom_param; drop type point2 cascade; create function foreach_arrvar() returns int language plpgsql as $$ declare item int[]; total int := 0; begin foreach item in array '{"{1,2}","{3}"}'::text[] loop total := total + cardinality(item); end loop; return total; end; $$; select message from plpgsql_check_function_tb('foreach_arrvar()', fatal_errors => false); drop function foreach_arrvar(); create function stale_ctx() returns int language plpgsql as $$ declare r record; unused_v int; -- produces an "unused variable" warning begin r.a := 10; -- fails: record "r" is not assigned yet; sets err_text return 1; end; $$; select * from plpgsql_check_function('stale_ctx', fatal_errors => false); drop function stale_ctx(); create table repro_foreach_tab(a int, b int); -- the target of FOREACH ARRAY holds an element of the array, so the array -- expression must not be traced as a constant value of the target variable create or replace function repro_foreach() returns void as $$ declare cols text[] := array['a', 'b']; c text; r int; begin foreach c in array cols loop execute format('select %I from repro_foreach_tab', c) into r; end loop; end $$ language plpgsql; select * from plpgsql_check_function('repro_foreach()'); -- with SLICE the target holds an array, and tracing of a composite value -- is not supported anyway create or replace function repro_foreach_slice() returns void as $$ declare cols text[] := array[['a'], ['b']]; c text[]; r int; begin foreach c slice 1 in array cols loop execute format('select %I from repro_foreach_tab', c[1]) into r; end loop; end $$ language plpgsql; select * from plpgsql_check_function('repro_foreach_slice()'); -- plain assignment is still traced create or replace function repro_foreach_const() returns void as $$ declare c text := 'unknown_column'; r int; begin execute format('select %I from repro_foreach_tab', c) into r; end $$ language plpgsql; select * from plpgsql_check_function('repro_foreach_const()'); drop function repro_foreach(); drop function repro_foreach_slice(); drop function repro_foreach_const(); drop table repro_foreach_tab; okbob-plpgsql_check-800db79/sql/plpgsql_check_catalog.sql000066400000000000000000000053021526744060500236240ustar00rootroot00000000000000set client_min_messages to warning; create extension if not exists plpgsql_check; set client_min_messages to notice; -- -- checks done before the function is checked -- -- the checked routine must be written in plpgsql select * from plpgsql_check_function('upper(text)'); select * from plpgsql_check_function('sum(int)'); -- a routine with a pseudotype result is rejected sooner, the result type -- is read before the language of the routine is validated select * from plpgsql_check_function('textout(text)'); -- record, void and the polymorphic types are pseudotypes too, but they -- are allowed create function cat_f1() returns void as $$ begin end; $$ language plpgsql; create function cat_f2(a anyelement) returns anyelement as $$ begin return a; end; $$ language plpgsql; create function cat_f3() returns record as $$ declare r record; begin select 1 as a, 2 as b into r; return r; end; $$ language plpgsql; select * from plpgsql_check_function('cat_f1'); select * from plpgsql_check_function('cat_f2(anyelement)'); select * from plpgsql_check_function('cat_f3'); -- -- the relation of a trigger -- create table cat_tab1(a int, b int); create function cat_trg1() returns trigger as $$ begin return new; end; $$ language plpgsql; create function cat_evtrg1() returns event_trigger as $$ begin end; $$ language plpgsql; -- a dml trigger cannot be checked without the triggering relation select * from plpgsql_check_function('cat_trg1'); select * from plpgsql_check_function('cat_trg1', relid => 'cat_tab1'::regclass); -- an event trigger has no triggering relation select * from plpgsql_check_function('cat_evtrg1', relid => 'cat_tab1'::regclass); select * from plpgsql_check_function('cat_evtrg1'); -- a plain function has no triggering relation either select * from plpgsql_check_function('cat_f1', relid => 'cat_tab1'::regclass); -- the profiler does not check the triggering relation, it only reads the -- collected statistics select * from plpgsql_profiler_function_tb('cat_trg1'); -- -- the pragma function is searched by name, and only the function from the -- schema of the extension is used -- create schema cat_ns; create function cat_ns.plpgsql_check_pragma(int) returns int as $$ select 1; $$ language sql; create function cat_f4() returns int as $$ declare r record; begin perform plpgsql_check_pragma('type: r (a int)'); return r.a; end; $$ language plpgsql; select * from plpgsql_check_function('cat_f4'); drop function cat_ns.plpgsql_check_pragma(int); drop schema cat_ns; drop function cat_f1(); drop function cat_f2(anyelement); drop function cat_f3(); drop function cat_f4(); drop function cat_evtrg1(); drop trigger if exists cat_trg1 on cat_tab1; drop function cat_trg1(); drop table cat_tab1; okbob-plpgsql_check-800db79/sql/plpgsql_check_check_function.sql000066400000000000000000000163041526744060500252000ustar00rootroot00000000000000set client_min_messages to warning; create extension if not exists plpgsql_check; set client_min_messages to notice; -- -- the polymorphic arguments are replaced by the types passed as the -- arguments of the check function -- create function cf_f1(a anynonarray) returns text as $$ begin return a::text; end; $$ language plpgsql; -- anynonarray uses the anyelememttype option (the name of the argument -- has a typo), which must not be an array select * from plpgsql_check_function('cf_f1(anynonarray)'); select * from plpgsql_check_function('cf_f1(anynonarray)'::regprocedure, anyelememttype => 'date'); select * from plpgsql_check_function('cf_f1(anynonarray)'::regprocedure, anyelememttype => 'int[]'); create type cf_enum as enum ('a', 'b'); create function cf_f2(a anyenum) returns text as $$ begin return a::text; end; $$ language plpgsql; -- anyenum requires the anyenumtype option, and the passed type must be -- an enum select * from plpgsql_check_function('cf_f2(anyenum)'); select * from plpgsql_check_function('cf_f2(anyenum)'::regprocedure, anyenumtype => 'int'); select * from plpgsql_check_function('cf_f2(anyenum)'::regprocedure, anyenumtype => 'cf_enum'); create function cf_f3(a anycompatiblenonarray, b anycompatible) returns text as $$ begin return a::text || b::text; end; $$ language plpgsql; -- anycompatiblenonarray uses the anycompatibletype option, which must -- not be an array select * from plpgsql_check_function('cf_f3(anycompatiblenonarray,anycompatible)'); select * from plpgsql_check_function('cf_f3(anycompatiblenonarray,anycompatible)'::regprocedure, anycompatibletype => 'int[]'); create function cf_f4(a anymultirange) returns int as $$ begin return 1; end; $$ language plpgsql; -- anymultirange is derived from the anyrangetype option select * from plpgsql_check_function('cf_f4(anymultirange)'); create function cf_multirange(a anymultirange) returns anyrange as $$ begin return range_merge(a); end; $$ language plpgsql; select cf_multirange('{[1,3)}'::int4multirange); select * from plpgsql_check_function('cf_multirange'); select * from plpgsql_check_function('cf_multirange', anyrangetype => 'numrange'); select * from plpgsql_check_function('cf_multirange', anyrangetype => 'int'); create function cf_multirange_result(a anyrange) returns anymultirange as $$ begin return multirange(a); end; $$ language plpgsql; select * from plpgsql_check_function('cf_multirange_result'); select * from plpgsql_check_function('cf_multirange_result', anyrangetype => 'numrange'); -- Some supported core PL/pgSQL validators reject anycompatiblemultirange. set check_function_bodies = off; create function cf_compatible_multirange(a anycompatiblemultirange) returns anycompatiblerange as $$ begin return range_merge(a); end; $$ language plpgsql; create function cf_compatible_multirange_result(a anycompatiblerange) returns anycompatiblemultirange as $$ begin return multirange(a); end; $$ language plpgsql; reset check_function_bodies; select cf_compatible_multirange('{[1,3)}'::int4multirange); select * from plpgsql_check_function('cf_compatible_multirange'); select * from plpgsql_check_function('cf_compatible_multirange', anycompatiblerangetype => 'numrange'); select * from plpgsql_check_function('cf_compatible_multirange', anycompatiblerangetype => 'int'); select * from plpgsql_check_function('cf_compatible_multirange_result'); select * from plpgsql_check_function('cf_compatible_multirange_result', anycompatiblerangetype => 'numrange'); drop function cf_multirange(anymultirange); drop function cf_multirange_result(anyrange); drop function cf_compatible_multirange(anycompatiblemultirange); drop function cf_compatible_multirange_result(anycompatiblerange); -- when some argument is not an input argument, then the modes of all the -- arguments are read from the catalog create function cf_f5(a anyelement, out b anyelement, out c int) as $$ begin b := a; c := 1; end; $$ language plpgsql; -- the result descriptor is built from the output arguments, and it cannot -- be used when some of them is polymorphic select * from plpgsql_check_function('cf_f5(anyelement)'); create function cf_f6(variadic a anyarray) returns int as $$ begin return array_length(a, 1); end; $$ language plpgsql; -- a variadic polymorphic argument is replaced by an array of the passed -- type select * from plpgsql_check_function('cf_f6(anyarray)'); -- -- the dependencies on the rowtypes of the relations -- create table cf_tab1(a int, b int); create view cf_view1 as select * from cf_tab1; create materialized view cf_matview1 as select * from cf_tab1; create type cf_type1 as (a int, b int); create function cf_f7() returns int as $$ declare r1 cf_tab1%rowtype; r2 cf_view1%rowtype; r3 cf_matview1%rowtype; r4 cf_type1; begin r1.a := 1; r2.a := 1; r3.a := 1; r4.a := 1; return r1.a + r2.a + r3.a + r4.a; end; $$ language plpgsql; select type, schema, name from plpgsql_show_dependency_tb('cf_f7') order by 1, 2, 3; -- -- the name of an argument should not be a reserved keyword -- create function cf_f8("table" int, "left" int) returns int as $$ begin return 1; end; $$ language plpgsql; select * from plpgsql_check_function('cf_f8(int,int)'); -- -- the configuration of a routine is set before the check and it is -- restored when the check is done -- set datestyle to 'ISO, DMY'; create function cf_f9() returns text as $$ begin return current_setting('datestyle'); end; $$ language plpgsql set datestyle to 'ISO, MDY'; select * from plpgsql_check_function('cf_f9'); show datestyle; reset datestyle; -- -- the check can be disabled -- set plpgsql_check.mode = 'disabled'; select * from plpgsql_check_function('cf_f7'); set plpgsql_check.mode = 'by_function'; -- -- the passive check is started when the checked routine is executed -- set plpgsql_check.mode = 'every_start'; -- a procedure and a routine which returns void are not required to be -- closed by a RETURN statement create procedure cf_p1() as $$ begin raise notice 'cf_p1'; end; $$ language plpgsql; create function cf_f10() returns void as $$ begin raise notice 'cf_f10'; end; $$ language plpgsql; call cf_p1(); select cf_f10(); -- but a routine which returns a value is, although the missing RETURN -- can be reported as an extra warning only create function cf_f11() returns int as $$ begin if random() < -1 then return 1; end if; end; $$ language plpgsql; create function cf_f12() returns int as $$ begin raise notice 'cf_f12'; end; $$ language plpgsql; select cf_f11(); select cf_f12(); -- in the fresh_start mode every routine is checked only once set plpgsql_check.mode = 'fresh_start'; select cf_f10(); select cf_f10(); set plpgsql_check.mode = 'by_function'; drop function cf_f1(anynonarray); drop function cf_f2(anyenum); drop function cf_f3(anycompatiblenonarray,anycompatible); drop function cf_f4(anymultirange); drop function cf_f5(anyelement); drop function cf_f6(anyarray); drop function cf_f7(); drop function cf_f8(int,int); drop function cf_f9(); drop function cf_f10(); drop function cf_f11(); drop function cf_f12(); drop procedure cf_p1(); drop type cf_enum; drop type cf_type1; drop materialized view cf_matview1; drop view cf_view1; drop table cf_tab1; okbob-plpgsql_check-800db79/sql/plpgsql_check_comment_options.sql000066400000000000000000000470521526744060500254370ustar00rootroot00000000000000-- -- regress tests of the in-comment options parser (src/parser.c) -- -- plpgsql_check reads options from the source code of the checked -- function, from lines tagged by "@plpgsql_check_options:" inside line -- comments or block comments. Nothing of that parser was covered by the -- regress tests. -- load 'plpgsql_check'; set client_min_messages to warning; create extension if not exists plpgsql_check; set client_min_messages to notice; -- Some errors reported by the options parser contain the oid of the -- checked function. This wrapper replaces the oid, so the output is -- stable. create function co_check(fnname text, rel regclass default 0) returns setof text as $$ begin return query select * from plpgsql_check_function(fnname, rel); exception when others then return next 'ERROR: ' || regexp_replace(sqlerrm, 'fnoid: [0-9]+', 'fnoid: NNN'); end; $$ language plpgsql; -- -- boolean options -- -- an option with no value is true create function co_bool1() returns void as $$ -- @plpgsql_check_options: extra_warnings declare x int; begin x := 1; end; $$ language plpgsql; select * from co_check('co_bool1'); -- the value can be written with or without the equal sign, and all the -- accepted spellings of true and false are used here create function co_bool2() returns void as $$ -- @plpgsql_check_options: extra_warnings = true, other_warnings = yes -- @plpgsql_check_options: performance_warnings t, security_warnings on declare x int; begin x := 1; end; $$ language plpgsql; select * from co_check('co_bool2'); create function co_bool3() returns void as $$ -- @plpgsql_check_options: extra_warnings = false, other_warnings = no -- @plpgsql_check_options: performance_warnings f, security_warnings off -- @plpgsql_check_options: compatibility_warnings = off, fatal_errors = off declare x int; begin x := 1; end; $$ language plpgsql; select * from co_check('co_bool3'); -- an option which is directly followed by a comma keeps its true value create function co_bool4() returns void as $$ -- @plpgsql_check_options: extra_warnings, other_warnings declare x int; begin x := 1; end; $$ language plpgsql; select * from co_check('co_bool4'); -- all_warnings and without_warnings are handled at the very end of the -- parsing, and they cannot be used together create function co_all_warnings() returns void as $$ -- @plpgsql_check_options: all_warnings declare x int; begin x := 1; end; $$ language plpgsql; select * from co_check('co_all_warnings'); create function co_without_warnings() returns void as $$ -- @plpgsql_check_options: without_warnings declare x int; begin x := 1; end; $$ language plpgsql; select * from co_check('co_without_warnings'); create function co_warnings_conflict() returns void as $$ -- @plpgsql_check_options: all_warnings = on, without_warnings = on begin end; $$ language plpgsql; select * from co_check('co_warnings_conflict'); -- the options can be disabled by the use_incomment_options argument select * from plpgsql_check_function('co_warnings_conflict', use_incomment_options := false); -- -- comment forms -- -- a block comment, and more than one tagged line in a single block -- comment create function co_blockcomment() returns void as $$ /* * some description of the function * @plpgsql_check_options: extra_warnings = off * @plpgsql_check_options: other_warnings = off */ declare x int; begin x := 1; end; $$ language plpgsql; select * from co_check('co_blockcomment'); -- tags before and after nested comments belong to the same outer comment create function co_nested_comment() returns void as $$ /* * @plpgsql_check_options: extra_warnings = off /* inner description /* deeper description */ */ * @plpgsql_check_options: other_warnings = off */ declare x int; y int; begin x := 1; end; $$ language plpgsql; select * from co_check('co_nested_comment'); select count(*) = 2 as warnings_without_options from plpgsql_check_function_tb('co_nested_comment', use_incomment_options => false); drop function co_nested_comment(); -- an inner terminator is not part of the tagged option on its line create function co_nested_inline_option() returns void as $$ /* /* @plpgsql_check_options: without_warnings */ */ declare x int; begin null; end; $$ language plpgsql; select * from co_check('co_nested_inline_option'); select count(*) = 1 as warning_without_options from plpgsql_check_function_tb('co_nested_inline_option', use_incomment_options => false); drop function co_nested_inline_option(); -- The source code is scanned for the tag outside of the string -- literals, the quoted identifiers and the dollar quoted strings, so the -- tags written there are not options - if they were parsed, the check -- would fail on an unsupported option. The only real tag is the one on -- the last line, and it turns the extra warnings off; the same function -- checked with use_incomment_options = false reports them. create function co_skipping() returns void as $func$ declare s1 text = 'a string with -- @plpgsql_check_options: nonsense and '' inside'; s2 text = $tag$dollar quoted -- @plpgsql_check_options: nonsense$tag$; s3 text = $$another dollar quoted -- @plpgsql_check_options: nonsense$$; "quoted ""x"" -- @plpgsql_check_options: nonsense" int = 1; x int; begin x := 1; -- some note @plpgsql_check_options: extra_warnings = off raise notice '%', coalesce(s1, s2, s3); end; $func$ language plpgsql; select * from co_check('co_skipping'); select * from plpgsql_check_function('co_skipping', use_incomment_options := false); -- escape strings honor backslashes, but literal contents are never options set standard_conforming_strings = on; create function co_escape_string() returns text as $func$ declare s1 text := E'can\'t -- @plpgsql_check_options: nonsense '; s2 text := e'won\'t'; x int; begin -- @plpgsql_check_options: without_warnings return s1 || s2; end; $func$ language plpgsql; select * from co_check('co_escape_string'); select count(*) = 1 as unused_without_options from plpgsql_check_function_tb('co_escape_string', use_incomment_options => false) where message = 'unused variable "x"'; -- the last letter of a type name is not an escape-string prefix create function co_standard_string() returns text as $func$ declare s name := name'backslash\'; --' x int; begin -- @plpgsql_check_options: without_warnings return s::text; end; $func$ language plpgsql; select * from co_check('co_standard_string'); drop function co_escape_string(); drop function co_standard_string(); drop function co_legacy_string(); -- positional parameters and identifiers do not open dollar-quoted strings create function co_dollar_parameters(int, int) returns int as $func$ declare x int; begin return $1+$2; -- @plpgsql_check_options: without_warnings end; $func$ language plpgsql; select * from co_check('co_dollar_parameters'); create function co_dollar_identifier() returns int as $func$ declare v$x$part int := 1; x int; begin -- @plpgsql_check_options: without_warnings return v$x$part; end; $func$ language plpgsql; select * from co_check('co_dollar_identifier'); -- genuine tags can contain digits, are case sensitive, and hide literal options create function co_dollar_literal() returns text as $func$ declare s text := $Tag_1$other delimiters $$ and $tag_1$ -- @plpgsql_check_options: nonsense $Tag_1$; x int; begin -- @plpgsql_check_options: without_warnings return s; end; $func$ language plpgsql; select * from co_check('co_dollar_literal'); drop function co_dollar_parameters(int, int); drop function co_dollar_identifier(); drop function co_dollar_literal(); -- end of source terminates a line comment just like a newline create function co_final_comment() returns void as $func$declare x int; begin null; end; -- @plpgsql_check_options: without_warnings$func$ language plpgsql; select * from co_check('co_final_comment'); select count(*) = 1 as unused_without_options from plpgsql_check_function_tb('co_final_comment', use_incomment_options => false) where message = 'unused variable "x"'; create function co_final_newline() returns void as $func$declare x int; begin null; end; -- @plpgsql_check_options: without_warnings $func$ language plpgsql; select * from co_check('co_final_newline'); create function co_final_empty() returns void as $func$begin null; end; --$func$ language plpgsql; select * from co_check('co_final_empty'); drop function co_final_comment(); drop function co_final_newline(); drop function co_final_empty(); -- -- name options -- -- the argument of a name option can be an identifier, a quoted -- identifier or a string. The names of the transition tables of a -- trigger function are usually passed as arguments of -- plpgsql_check_function(), the options replace them. create table co_trig_tab(a int, b int); create table co_trig_tab2(c int, d int); create function co_newtable() returns trigger as $$ -- @plpgsql_check_options: newtable = co_new begin perform count(*) from co_new; return null; end; $$ language plpgsql; select * from co_check('co_newtable', 'co_trig_tab'); create function co_oldtable() returns trigger as $$ -- @plpgsql_check_options: oldtable "co_old", newtable = 'co_new' begin perform count(*) from co_old; perform count(*) from co_new; return null; end; $$ language plpgsql; select * from co_check('co_oldtable', 'co_trig_tab'); -- -- table option -- -- relid is used for the resolution of the NEW and OLD records of a -- trigger function. The relation given by the option replaces the -- relation passed as an argument, so the check passes although the -- fields used by the function do not exist in co_trig_tab. create function co_relid() returns trigger as $$ -- @plpgsql_check_options: relid = co_trig_tab2 begin if new.c > 10 then new.d := new.c; end if; return new; end; $$ language plpgsql; select * from co_check('co_relid', 'co_trig_tab'); -- Validate the final options, so a comment can supply a required relid. select * from co_check('co_relid'); select * from plpgsql_check_function_tb('co_relid()'::regprocedure); select * from plpgsql_check_function('co_relid', newtable => 'co_transition'); select * from plpgsql_make_pragma('co_relid()'); -- Conversely, a comment cannot attach trigger metadata to a nontrigger. create function co_relid_invalid() returns int as $$ -- @plpgsql_check_options: relid = co_trig_tab begin return 1; end; $$ language plpgsql; select * from plpgsql_check_function('co_relid_invalid'); select * from plpgsql_check_function('co_relid_invalid()'::regprocedure); select * from plpgsql_check_function_tb('co_relid_invalid'); select * from plpgsql_check_function_tb('co_relid_invalid()'::regprocedure); select * from plpgsql_make_pragma('co_relid_invalid()'); select * from plpgsql_check_function('co_relid_invalid', use_incomment_options => false); select * from plpgsql_check_function_tb('co_relid_invalid', use_incomment_options => false); drop function co_relid_invalid(); create function co_transition_invalid() returns int as $$ -- @plpgsql_check_options: newtable = co_transition begin return 1; end; $$ language plpgsql; select * from plpgsql_check_function('co_transition_invalid'); select * from plpgsql_check_function_tb('co_transition_invalid'); select * from plpgsql_make_pragma('co_transition_invalid()'); drop function co_transition_invalid(); -- the value can be a qualified name, with quoted parts create function co_relid_qualified() returns trigger as $$ -- @plpgsql_check_options: relid = public."co_trig_tab2" begin new.d := new.c; return new; end; $$ language plpgsql; select * from co_check('co_relid_qualified', 'co_trig_tab'); -- -- type options -- -- the polymorphic types are substituted by the types given by the -- options, and a qualified type name is used too create function co_anyelement(anyelement) returns text as $$ -- @plpgsql_check_options: anyelementtype = text begin return $1; end; $$ language plpgsql; select * from co_check('co_anyelement'); create function co_anyelement2(anyelement) returns text as $$ -- @plpgsql_check_options: anyelementtype pg_catalog.text begin return $1; end; $$ language plpgsql; select * from co_check('co_anyelement2'); create type co_enum as enum ('a', 'b'); create function co_anyenum(anyenum) returns text as $$ -- @plpgsql_check_options: anyenumtype = co_enum begin return $1::text; end; $$ language plpgsql; select * from co_check('co_anyenum'); create function co_anyrange(anyrange) returns text as $$ -- @plpgsql_check_options: anyrangetype = numrange begin return lower($1)::text; end; $$ language plpgsql; select * from co_check('co_anyrange'); create function co_anycompatible(anycompatible) returns text as $$ -- @plpgsql_check_options: anycompatibletype = text begin return $1; end; $$ language plpgsql; select * from co_check('co_anycompatible'); create function co_anycompatiblerange(anycompatiblerange) returns text as $$ -- @plpgsql_check_options: anycompatiblerangetype = numrange begin return lower($1)::text; end; $$ language plpgsql; select * from co_check('co_anycompatiblerange'); -- complete type syntax, including separators inside modifiers and between options create function co_type_array(anyelement) returns text as $$ -- @plpgsql_check_options: anyelementtype = "pg_catalog"."int4"[][], extra_warnings = off begin return $1::text; end; $$ language plpgsql; select * from co_check('co_type_array'); create function co_type_datetime(anyelement) returns text as $$ -- @plpgsql_check_options: anyelementtype = timestamp(3) without time zone begin return $1::text; end; $$ language plpgsql; select * from co_check('co_type_datetime'); create function co_type_multiword(anycompatible) returns text as $$ -- @plpgsql_check_options: anycompatibletype = double precision, extra_warnings = off begin return $1::text; end; $$ language plpgsql; select * from co_check('co_type_multiword'); create function co_type_numeric(anyelement) returns text as $$ -- @plpgsql_check_options: anyelementtype = numeric(8,2), extra_warnings = off begin return $1::text; end; $$ language plpgsql; select * from co_check('co_type_numeric'); -- -- echo option -- -- the argument of echo can be of any token type, and the @@name and -- @@signature placeholders are substituted create function co_echo(a int) returns void as $$ -- @plpgsql_check_options: echo = 'a string with @@name' -- @plpgsql_check_options: echo = 'a signature @@signature' -- @plpgsql_check_options: echo = '@@unknown is not a placeholder' -- @plpgsql_check_options: echo an_identifier -- @plpgsql_check_options: echo = "a quoted identifier" -- @plpgsql_check_options: echo = 10.1 -- @plpgsql_check_options: echo = * begin end; $$ language plpgsql; select * from co_check('co_echo'); -- -- syntax errors -- create function co_err_unknown() returns void as $$ -- @plpgsql_check_options: unknown_option = 1 begin end; $$ language plpgsql; select * from co_check('co_err_unknown'); create function co_err_nooption() returns void as $$ -- @plpgsql_check_options: = 1 begin end; $$ language plpgsql; select * from co_check('co_err_nooption'); create function co_err_nocomma() returns void as $$ -- @plpgsql_check_options: extra_warnings = on other_warnings = on begin end; $$ language plpgsql; select * from co_check('co_err_nocomma'); create function co_err_badbool() returns void as $$ -- @plpgsql_check_options: extra_warnings = maybe begin end; $$ language plpgsql; select * from co_check('co_err_badbool'); create function co_err_nobool() returns void as $$ -- @plpgsql_check_options: extra_warnings = begin end; $$ language plpgsql; select * from co_check('co_err_nobool'); create function co_err_notrange(anyrange) returns void as $$ -- @plpgsql_check_options: anyrangetype = int begin end; $$ language plpgsql; select * from co_check('co_err_notrange'); create function co_err_notrange2(anycompatiblerange) returns void as $$ -- @plpgsql_check_options: anycompatiblerangetype = int begin end; $$ language plpgsql; select * from co_check('co_err_notrange2'); create function co_err_badtype(anyelement) returns void as $$ -- @plpgsql_check_options: anyelementtype = 'text' begin end; $$ language plpgsql; select * from co_check('co_err_badtype'); create function co_err_type_suffix(anyelement) returns text as $$ -- @plpgsql_check_options: anyelementtype = integer[] trailing begin return $1::text; end; $$ language plpgsql; select * from co_check('co_err_type_suffix'); create function co_err_notype(anyelement) returns void as $$ -- @plpgsql_check_options: anyelementtype begin end; $$ language plpgsql; select * from co_check('co_err_notype'); create function co_err_badname() returns trigger as $$ -- @plpgsql_check_options: newtable = 10 begin return null; end; $$ language plpgsql; select * from co_check('co_err_badname', 'co_trig_tab'); create function co_err_noname() returns trigger as $$ -- @plpgsql_check_options: newtable begin return null; end; $$ language plpgsql; select * from co_check('co_err_noname', 'co_trig_tab'); create function co_err_badtable() returns trigger as $$ -- @plpgsql_check_options: relid = 10 begin return null; end; $$ language plpgsql; select * from co_check('co_err_badtable', 'co_trig_tab'); create function co_err_notable() returns trigger as $$ -- @plpgsql_check_options: relid begin return null; end; $$ language plpgsql; select * from co_check('co_err_notable', 'co_trig_tab'); create function co_err_noecho() returns void as $$ -- @plpgsql_check_options: echo begin end; $$ language plpgsql; select * from co_check('co_err_noecho'); create function co_err_noechovalue() returns void as $$ -- @plpgsql_check_options: echo = begin end; $$ language plpgsql; select * from co_check('co_err_noechovalue'); drop function co_err_noechovalue(); drop function co_err_noecho(); drop function co_err_notable(); drop function co_err_badtable(); drop function co_err_noname(); drop function co_err_badname(); drop function co_err_notype(anyelement); drop function co_err_badtype(anyelement); drop function co_err_type_suffix(anyelement); drop function co_err_notrange2(anycompatiblerange); drop function co_err_notrange(anyrange); drop function co_err_nobool(); drop function co_err_badbool(); drop function co_err_nocomma(); drop function co_err_nooption(); drop function co_err_unknown(); drop function co_echo(int); drop function co_anycompatiblerange(anycompatiblerange); drop function co_type_array(anyelement); drop function co_type_datetime(anyelement); drop function co_type_multiword(anycompatible); drop function co_type_numeric(anyelement); drop function co_anycompatible(anycompatible); drop function co_anyrange(anyrange); drop function co_anyenum(anyenum); drop type co_enum; drop function co_anyelement2(anyelement); drop function co_anyelement(anyelement); drop function co_relid_qualified(); drop function co_relid(); drop function co_oldtable(); drop function co_newtable(); drop table co_trig_tab2; drop table co_trig_tab; drop function co_skipping(); drop function co_blockcomment(); drop function co_warnings_conflict(); drop function co_without_warnings(); drop function co_all_warnings(); drop function co_bool4(); drop function co_bool3(); drop function co_bool2(); drop function co_bool1(); drop function co_check(text, regclass); -- disable body check so postgresql allows storing function with unclosed dollar tag set check_function_bodies = off; create or replace function unpriv_issue1_dollar_overread() returns void as $body$ $test_custom_tag_1234567890$ abc $test_custom_tag_123456789$body$ language plpgsql; -- trigger plpgsql_check_search_comment_options on the function's prosrc select * from plpgsql_check_function('unpriv_issue1_dollar_overread()'); drop function unpriv_issue1_dollar_overread(); set check_function_bodies = on; okbob-plpgsql_check-800db79/sql/plpgsql_check_comment_options_legacystr.sql000066400000000000000000000020131526744060500275000ustar00rootroot00000000000000load 'plpgsql_check'; set client_min_messages to warning; create extension if not exists plpgsql_check; set client_min_messages to notice; -- Some errors reported by the options parser contain the oid of the -- checked function. This wrapper replaces the oid, so the output is -- stable. create function co_check(fnname text, rel regclass default 0) returns setof text as $$ begin return query select * from plpgsql_check_function(fnname, rel); exception when others then return next 'ERROR: ' || regexp_replace(sqlerrm, 'fnoid: [0-9]+', 'fnoid: NNN'); end; $$ language plpgsql; set standard_conforming_strings = off; set escape_string_warning = off; create function co_legacy_string() returns text as $func$ declare s text := 'can\'t'; x int; begin -- @plpgsql_check_options: without_warnings return s; end; $func$ language plpgsql; select * from co_check('co_legacy_string'); reset escape_string_warning; reset standard_conforming_strings; drop function co_legacy_string(); drop function co_check(text, regclass); okbob-plpgsql_check-800db79/sql/plpgsql_check_expr_walk.sql000066400000000000000000000474221526744060500242170ustar00rootroot00000000000000set client_min_messages to warning; create extension if not exists plpgsql_check; set client_min_messages to notice; -- -- checks of the format() function -- -- the format string is parsed when it is a constant, the number of the -- placeholders is compared against the number of the arguments and the -- syntax of the format specifiers is validated -- create table ew_tab1(a int, b text); -- correct format strings - a simple placeholder, a doubled percent, -- an explicit argument position, a direct width, the minus flag, -- the identifier and the literal placeholders create function ew_f1(p text) returns void as $$ begin raise notice '%', format('%s %s', p, p); raise notice '%', format('100%% %s', p); raise notice '%', format('%1$s %1$s %2$s', p, p); raise notice '%', format('[%10s][%-10s]', p, p); raise notice '%', format('%I = %L', p, p); end; $$ language plpgsql; select * from plpgsql_check_function('ew_f1', fatal_errors => false); -- an indirect width - the width is taken from the next argument or from -- the argument on the given position create function ew_f2(p text, w int) returns void as $$ begin raise notice '%', format('[%*s]', w, p); raise notice '%', format('[%2$*1$s]', w, p); end; $$ language plpgsql; select * from plpgsql_check_function('ew_f2', fatal_errors => false); -- the arguments are numbered from one create function ew_f3(p text) returns void as $$ begin raise notice '%', format('%0$s', p); end; $$ language plpgsql; select * from plpgsql_check_function('ew_f3', fatal_errors => false); -- the same for the position of the width argument create function ew_f4(p text, w int) returns void as $$ begin raise notice '%', format('%0$*s', w, p); end; $$ language plpgsql; select * from plpgsql_check_function('ew_f4', fatal_errors => false); -- the position of the width argument has to be closed by a dollar create function ew_f5(p text, w int) returns void as $$ begin raise notice '%', format('%*1s', w, p); end; $$ language plpgsql; select * from plpgsql_check_function('ew_f5', fatal_errors => false); -- the parsed number has to fit into an integer create function ew_f6(p text) returns void as $$ begin raise notice '%', format('%99999999999999$s', p); end; $$ language plpgsql; select * from plpgsql_check_function('ew_f6', fatal_errors => false); -- Adding the format-string argument must not overflow an INT_MAX position. create function ew_max_position(p text, args text[]) returns void as $$ begin execute format('SELECT %2147483647$s', p); execute format('SELECT %1$*2147483647$s', p); perform format('%2147483647$s', p); perform format('%1$*2147483647$s', p); execute format('SELECT %2147483647$s', variadic args); execute format('SELECT %*2147483647$s %s', variadic args); end; $$ language plpgsql; select sqlstate, message from plpgsql_check_function_tb('ew_max_position', fatal_errors => false, security_warnings => true) where level = 'error'; drop function ew_max_position(text, text[]); -- only s, I and L are known specifiers create function ew_f7(p text) returns void as $$ begin raise notice '%', format('%d', p); end; $$ language plpgsql; select * from plpgsql_check_function('ew_f7', fatal_errors => false); -- the format string cannot end by an unfinished specifier - the parser -- can stop at any part of the specifier create function ew_f8(p text, w int) returns void as $$ begin raise notice '%', format('value %', p); end; $$ language plpgsql; select * from plpgsql_check_function('ew_f8', fatal_errors => false); create function ew_f8a(p text) returns void as $$ begin raise notice '%', format('value %1', p); end; $$ language plpgsql; select * from plpgsql_check_function('ew_f8a', fatal_errors => false); create function ew_f8b(p text) returns void as $$ begin raise notice '%', format('value %1$', p); end; $$ language plpgsql; select * from plpgsql_check_function('ew_f8b', fatal_errors => false); create function ew_f8c(p text) returns void as $$ begin raise notice '%', format('value %-', p); end; $$ language plpgsql; select * from plpgsql_check_function('ew_f8c', fatal_errors => false); create function ew_f8d(p text) returns void as $$ begin raise notice '%', format('value %*', p); end; $$ language plpgsql; select * from plpgsql_check_function('ew_f8d', fatal_errors => false); create function ew_f8e(p text, w int) returns void as $$ begin raise notice '%', format('value %*1$', w, p); end; $$ language plpgsql; select * from plpgsql_check_function('ew_f8e', fatal_errors => false); -- the position of the width argument is numbered from one too, and the -- number of the direct width has to fit into an integer create function ew_f8f(p text, w int) returns void as $$ begin raise notice '%', format('value %*0$s', w, p); end; $$ language plpgsql; select * from plpgsql_check_function('ew_f8f', fatal_errors => false); create function ew_f8g(p text, w int) returns void as $$ begin raise notice '%', format('value %*99999999999999$s', w, p); end; $$ language plpgsql; select * from plpgsql_check_function('ew_f8g', fatal_errors => false); create function ew_f8h(p text) returns void as $$ begin raise notice '%', format('value %99999999999999s', p); end; $$ language plpgsql; select * from plpgsql_check_function('ew_f8h', fatal_errors => false); create function ew_f8i(p text) returns void as $$ begin raise notice '%', format('value %1$99999999999999s', p); end; $$ language plpgsql; select * from plpgsql_check_function('ew_f8i', fatal_errors => false); -- there have to be enough arguments for all the placeholders create function ew_f9(p text) returns void as $$ begin raise notice '%', format('%s %s', p); end; $$ language plpgsql; select * from plpgsql_check_function('ew_f9', fatal_errors => false); -- the same for the indirect width create function ew_f10(w int) returns void as $$ begin raise notice '%', format('[%*s]', w); end; $$ language plpgsql; select * from plpgsql_check_function('ew_f10', fatal_errors => false); create function ew_f11(w int) returns void as $$ begin raise notice '%', format('[%2$*3$s]', w, w); end; $$ language plpgsql; select * from plpgsql_check_function('ew_f11', fatal_errors => false); -- the arguments that are not used by the format string are reported create function ew_f12(p text) returns void as $$ begin raise notice '%', format('%s', p, p, p); end; $$ language plpgsql; select * from plpgsql_check_function('ew_f12', fatal_errors => false); -- -- the format() function as a source of a dynamic query -- -- when the format string and all the arguments are constants, the result -- is known and it is checked like a static query -- create function ew_f13() returns void as $$ begin execute format('select %I from %I', 'a', 'ew_tab1'); end; $$ language plpgsql; select * from plpgsql_check_function('ew_f13', fatal_errors => false); -- an identifier placeholder with an unknown value is replaced by a -- constant name, so the query cannot be executed, but its syntax is -- still checked create function ew_f14(p text) returns void as $$ begin execute format('select a from %I', p); end; $$ language plpgsql; select * from plpgsql_check_function('ew_f14', fatal_errors => false); -- a literal placeholder with an unknown value is replaced by NULL create function ew_f15(p text) returns void as $$ begin execute format('select a from ew_tab1 where b = %L', p); end; $$ language plpgsql; select * from plpgsql_check_function('ew_f15', fatal_errors => false); -- a value placeholder with an unknown value makes the result unknown create function ew_f16(p text) returns void as $$ begin execute format('select a from ew_tab1 where b = %s', p); end; $$ language plpgsql; select * from plpgsql_check_function('ew_f16', fatal_errors => false, security_warnings => true); -- the same with an explicit argument position and with an indirect width create function ew_f17(p text, w int) returns void as $$ begin execute format('select %1$I from ew_tab1', 'a'); execute format('select %*s from ew_tab1', w, 'a'); end; $$ language plpgsql; select * from plpgsql_check_function('ew_f17', fatal_errors => false, security_warnings => true); -- Padding can be required SQL syntax, including after multibyte characters. create table ew_width_source(U&"\00e9" int, "select" int); insert into ew_width_source values (1, 1); create function ew_widths() returns int as $$ declare n int; w text := '1'; begin execute format('SELECT%1s1', '') into n; execute format('SELECT%-1s1', '') into n; execute format('SELECT%*s1', 1, '') into n; execute format('SELECT%2$*1$s1', -1, '') into n; execute format('SELECT%1$1s%1$1s1', '') into n; execute format('SELECT%*s1', w, '') into n; execute format('SELECT %-2sFROM ew_width_source', U&'\00e9') into n; execute format('SELECT %-9IFROM ew_width_source', 'select') into n; execute format('SELECT %5L::int', '1') into n; execute format('SELECT%*s 1', null::int, '') into n; return n; end; $$ language plpgsql; select ew_widths(); select * from plpgsql_check_function('ew_widths'); drop function ew_widths(); drop table ew_width_source; -- An unknown width makes the reconstructed query unknown, not malformed. create function ew_unknown_width(w int) returns int as $$ declare n int; begin execute format('SELECT%*s1', w, '') into n; return n; end; $$ language plpgsql; select ew_unknown_width(1); select level, message from plpgsql_check_function_tb('ew_unknown_width') where level = 'error'; drop function ew_unknown_width(int); create function ew_invalid_width() returns void as $$ begin execute format('SELECT%*s1', '-2147483648'::int, ''); end; $$ language plpgsql; select sqlstate, message from plpgsql_check_function_tb('ew_invalid_width') where level = 'error'; drop function ew_invalid_width(); -- a format string that cannot be evaluated makes the dynamic query -- unknown - the string ends by an unfinished specifier, it contains an -- unknown specifier, it needs more arguments than it gets, or it refers -- to an argument or to a width argument behind the last one create function ew_f18(p text) returns void as $$ begin execute format('select %s %s', p); end; $$ language plpgsql; select * from plpgsql_check_function('ew_f18', fatal_errors => false, security_warnings => true); create function ew_f18a() returns void as $$ begin execute format('select 1 %'); end; $$ language plpgsql; select * from plpgsql_check_function('ew_f18a', fatal_errors => false); create function ew_f18b() returns void as $$ begin execute format('select 1 -- 100%%'); end; $$ language plpgsql; select * from plpgsql_check_function('ew_f18b', fatal_errors => false); create function ew_f18c() returns void as $$ begin execute format('select %d', 1); end; $$ language plpgsql; select * from plpgsql_check_function('ew_f18c', fatal_errors => false); create function ew_f18d() returns void as $$ begin execute format('select %2$*5$s', 'a', 'b'); end; $$ language plpgsql; select * from plpgsql_check_function('ew_f18d', fatal_errors => false); create function ew_f18e() returns void as $$ begin execute format('select %*s'); end; $$ language plpgsql; select * from plpgsql_check_function('ew_f18e', fatal_errors => false); create function ew_f18f() returns void as $$ begin execute format('select %3$s', 'a'); end; $$ language plpgsql; select * from plpgsql_check_function('ew_f18f', fatal_errors => false); create function ew_f18g() returns void as $$ begin execute format('select %s %s', 'a'); end; $$ language plpgsql; select * from plpgsql_check_function('ew_f18g', fatal_errors => false); -- an argument of the format string which is not a string is not a source -- of a SQL injection create function ew_f18h(n int) returns void as $$ begin execute format('select %s', n); end; $$ language plpgsql; select * from plpgsql_check_function('ew_f18h', fatal_errors => false, security_warnings => true); -- -- detection of the SQL injection in the dynamic queries -- -- the format string itself is a variable, so nothing can be checked create function ew_f19(p text, q text) returns void as $$ begin execute format(p, q); end; $$ language plpgsql; select * from plpgsql_check_function('ew_f19', security_warnings => true); -- the arguments of a function are not sanitized create function ew_f20(p text) returns void as $$ begin execute upper(p); end; $$ language plpgsql; select * from plpgsql_check_function('ew_f20', security_warnings => true); -- a named argument is checked like a positional one create function ew_id(val text) returns text as $$ begin return val; end $$ language plpgsql; create function ew_f21(p text) returns void as $$ begin execute ew_id(val => p); end; $$ language plpgsql; select * from plpgsql_check_function('ew_f21', security_warnings => true); -- a function whose arguments are not strings is safe create function ew_f22(n int) returns void as $$ begin execute ew_id(n::text); end; $$ language plpgsql; select * from plpgsql_check_function('ew_f22', security_warnings => true); -- an expression that the walker does not know is considered safe create function ew_f23(p text) returns void as $$ begin execute case when p is null then 'select 1' else 'select 2' end; end; $$ language plpgsql; select * from plpgsql_check_function('ew_f23', security_warnings => true); -- a sanitized value is safe create function ew_f24(p text) returns void as $$ begin execute 'select * from ' || quote_ident(p); execute 'select ' || quote_literal(p); execute 'select ' || quote_nullable(p); end; $$ language plpgsql; select * from plpgsql_check_function('ew_f24', security_warnings => true); -- -- detection of an implicit cast of a table attribute -- -- the type of the variable is wider than the type of the attribute, so -- the attribute is casted and the index on it cannot be used create function ew_f25(p bigint) returns int as $$ declare r int; begin select a into r from ew_tab1 where a = p; return r; end; $$ language plpgsql; select * from plpgsql_check_function('ew_f25', performance_warnings => true); -- an explicit cast written by the user is not reported create function ew_f26(p bigint) returns int as $$ declare r int; begin select a into r from ew_tab1 where a::bigint = p; return r; end; $$ language plpgsql; select * from plpgsql_check_function('ew_f26', performance_warnings => true); -- the casted expression is not a plain attribute create function ew_f27(p bigint) returns int as $$ declare r int; begin select a into r from ew_tab1 where abs(a) = p; return r; end; $$ language plpgsql; select * from plpgsql_check_function('ew_f27', performance_warnings => true); -- -- collecting of the dependencies -- -- an operator that is not in pg_catalog is a dependency of the routine, -- a prefix operator has no type on its left side create function ew_neg(int) returns int as $$ select -$1 $$ language sql immutable; create operator @# (function = ew_neg, rightarg = int); create function ew_f29(n int) returns int as $$ begin return @# n; end; $$ language plpgsql; select type, schema, name, params from plpgsql_show_dependency_tb('ew_f29'); -- Explicit VARIADIC arrays supply their elements, not one array argument. create function ew_f30(p text) returns void as $$ begin raise notice '%', format('%s %s', variadic array['a', p]); raise notice '%', format('%s %s', variadic '{a,b}'::text[]); raise notice '%', format('%s %s', variadic '[0:1]={a,b}'::text[]); raise notice '%', format('%s %s', variadic array[1, 2]); raise notice '%', format('%s %s %s %s', variadic array[['a', p], ['b', p]]); raise notice '%', format('plain', variadic array[]::text[]); raise notice '%', format('plain', variadic null::text[]); raise notice '%', format('%s%L', variadic array[null, null]::text[]); end; $$ language plpgsql; select * from plpgsql_check_function('ew_f30', fatal_errors => false); -- Unknown array lengths do not disable format syntax validation. create function ew_f31(args text[]) returns text as $$ begin return format('%s %s', variadic args); end; $$ language plpgsql; select * from plpgsql_check_function('ew_f31'); create function ew_f32() returns text as $$ begin return format('%s %s', variadic array['a']); end; $$ language plpgsql; select * from plpgsql_check_function('ew_f32'); create function ew_f33(args text[]) returns text as $$ begin return format('%s %Q', variadic args); end; $$ language plpgsql; select * from plpgsql_check_function('ew_f33'); create function ew_f34() returns text as $$ begin return format('%s', variadic null::text[]); end; $$ language plpgsql; select * from plpgsql_check_function('ew_f34'); -- Constant synthesis uses the same expanded arguments, including NULLs. create function ew_f35() returns int as $$ declare r record; begin execute format('select %s%L::int as n', variadic array[null, '7']) into r; return r.n; end; $$ language plpgsql; select * from plpgsql_check_function('ew_f35'); select ew_f35(); create function ew_f36() returns int as $$ declare r record; begin execute format('select %L::int as n', variadic array[null]::text[]) into r; return r.n; end; $$ language plpgsql; select * from plpgsql_check_function('ew_f36'); select ew_f36() is null; -- An explicit position resets subsequent implicit argument consumption. create function ew_f37(a text, b text) returns text as $$ begin return format('%s %s %1$s %s', a, b); end; $$ language plpgsql; select * from plpgsql_check_function('ew_f37'); select ew_f37('a', 'b'); create function ew_f38() returns text as $$ begin return format('%s %s %*1$s', 3, 'a'); end; $$ language plpgsql; select * from plpgsql_check_function('ew_f38'); select ew_f38(); create function ew_f39(a text, b text) returns text as $$ begin return format('%2$s %s', a, b); end; $$ language plpgsql; select * from plpgsql_check_function('ew_f39'); -- Synthesis must follow the same cursor for ordinary and VARIADIC calls. create function ew_f40() returns int as $$ declare a int; b int; begin execute format('select %s + %s + %1$s + %s', '1', '2') into a; execute format('select %s + %s + %1$s + %s', variadic array['1', '2']) into b; return a + b; end; $$ language plpgsql; select * from plpgsql_check_function('ew_f40'); select ew_f40(); create function ew_f41() returns int as $$ declare a int; begin execute format('select %s + %s + %*1$s', 1, 2) into a; return a; end; $$ language plpgsql; select * from plpgsql_check_function('ew_f41'); select ew_f41(); drop function ew_f37(text, text); drop function ew_f38(); drop function ew_f39(text, text); drop function ew_f40(); drop function ew_f41(); drop function ew_f30(text); drop function ew_f31(text[]); drop function ew_f32(); drop function ew_f33(text[]); drop function ew_f34(); drop function ew_f35(); drop function ew_f36(); drop function ew_f1(text); drop function ew_f2(text, int); drop function ew_f3(text); drop function ew_f4(text, int); drop function ew_f5(text, int); drop function ew_f6(text); drop function ew_f7(text); drop function ew_f8(text, int); drop function ew_f8a(text); drop function ew_f8b(text); drop function ew_f8c(text); drop function ew_f8d(text); drop function ew_f8e(text, int); drop function ew_f8f(text, int); drop function ew_f8g(text, int); drop function ew_f8h(text); drop function ew_f8i(text); drop function ew_f9(text); drop function ew_f10(int); drop function ew_f11(int); drop function ew_f12(text); drop function ew_f13(); drop function ew_f14(text); drop function ew_f15(text); drop function ew_f16(text); drop function ew_f17(text, int); drop function ew_f18(text); drop function ew_f18a(); drop function ew_f18b(); drop function ew_f18c(); drop function ew_f18d(); drop function ew_f18e(); drop function ew_f18f(); drop function ew_f18g(); drop function ew_f18h(int); drop function ew_f19(text, text); drop function ew_f20(text); drop function ew_f21(text); drop function ew_f22(int); drop function ew_f23(text); drop function ew_f24(text); drop function ew_f25(bigint); drop function ew_f26(bigint); drop function ew_f27(bigint); drop function ew_f29(int); drop function ew_id(text); drop operator @# (none, int); drop function ew_neg(int); drop table ew_tab1; okbob-plpgsql_check-800db79/sql/plpgsql_check_nocrash.sql000066400000000000000000000561001526744060500236510ustar00rootroot00000000000000load 'plpgsql'; create extension if not exists plpgsql_check; set client_min_messages to notice; set plpgsql_check.regress_test_mode = true; create table zero_columns(); create function assign_empty_tupdesc() returns int as $$ declare v int; begin select * from zero_columns into v; execute 'select * from zero_columns' into v; return v; end; $$ language plpgsql; select * from plpgsql_check_function('assign_empty_tupdesc()'); drop function assign_empty_tupdesc(); drop table zero_columns; create function public.int_eq(int, int) returns bool as $$ select $1 = $2; $$ language sql; create operator public.=== ( leftarg = int, rightarg = int, procedure = public.int_eq ); create function use_custom_operator() returns bool as $$ begin return 1 operator(public.===) 2; end; $$ language plpgsql; select type, schema, name, params from plpgsql_show_dependency_tb('use_custom_operator()'); drop function use_custom_operator(); drop function public.int_eq cascade; create function tracked_const_oob() returns void as $$ declare str text; res int; begin -- assign a string constant, so the strconstvars array is allocated str := 'constant'; -- the dynamic query has much more params than the function has datums execute 'select $40' into res using 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 'x'::text; raise notice '%, %', str, res; end; $$ language plpgsql; select * from plpgsql_check_function('tracked_const_oob()'); drop function tracked_const_oob(); create table returned_expr_tab(a int); create function returned_expr_null_node() returns setof refcursor as $$ begin return query select a::text::refcursor from returned_expr_tab; end; $$ language plpgsql; select * from plpgsql_check_function('returned_expr_null_node()', compatibility_warnings => true); drop function returned_expr_null_node(); drop table returned_expr_tab; create table rvalue_tab(a int); create function rvalue_null_node() returns void as $$ declare c refcursor; begin c := (select a::text::refcursor from rvalue_tab limit 1); raise notice '%', c; end; $$ language plpgsql; select * from plpgsql_check_function('rvalue_null_node()', compatibility_warnings => true); drop function rvalue_null_node(); drop table rvalue_tab; create table assert_column_tab(a int); create function pragma_assert_column() returns void as $$ declare tname text default 'assert_column_tab'; begin -- ASSERT-COLUMN expects two variables, only one is passed perform plpgsql_check_pragma('assert-column: tname'); raise notice '%', tname; end; $$ language plpgsql; select * from plpgsql_check_function('pragma_assert_column()'); drop function pragma_assert_column(); drop table assert_column_tab; create function pragma_sequence_prefix() returns void as $$ begin -- 6 chars long pragma name, the sequence handler reads 3 chars behind -- the end of the string perform plpgsql_check_pragma('SEQUEN'); end; $$ language plpgsql; select * from plpgsql_check_function('pragma_sequence_prefix()'); drop function pragma_sequence_prefix(); create function pragma_sequence_misdispatch() returns void as $$ begin perform plpgsql_check_pragma('SEQUENCE_FOO'); end; $$ language plpgsql; select * from plpgsql_check_function('pragma_sequence_misdispatch()'); drop function pragma_sequence_misdispatch(); create function profiled_function() returns int as $$ begin return 1; end; $$ language plpgsql; set plpgsql_check.profiler to on; begin; select profiled_function(); select plpgsql_profiler_reset_all(); commit; set plpgsql_check.profiler to off; drop function profiled_function(); do $$ declare src text := ''; begin for i in 1..3000 loop src := src || 'perform 1;'; end loop; execute format('create or replace function too_many_statements() returns void as $q$ begin if false then %s end if; end $q$ language plpgsql', src); end; $$; set plpgsql_check.profiler to off; drop function too_many_statements(); create function dynamic_query_with_params(p int) returns void as $$ begin execute 'select 1 where $1 > 0' using p; end; $$ language plpgsql; set plpgsql_check.profiler to on; select dynamic_query_with_params(1); set plpgsql_check.profiler to off; drop function dynamic_query_with_params(int); create function case_over_record() returns int as $$ declare r record; begin case r when null then return 1; else return 2; end case; end; $$ language plpgsql; select * from plpgsql_check_function('case_over_record()'); drop function case_over_record(); create table dynsql_tab(a int); create function dynamic_sql_forms(tname text) returns void as $$ declare query text; begin execute 'select * from dynsql_tab'; execute format('select * from %I', tname); query := 'select * from ' || quote_ident(tname); execute query; end; $$ language plpgsql; select * from plpgsql_check_function('dynamic_sql_forms(text)', all_warnings => true); drop function dynamic_sql_forms(text); drop table dynsql_tab; create function uninitialized_is_mp(v text) returns void as $$ begin -- the %L placeholder makes the query non constant, so it is checked in a -- subtransaction, and the check fails because the table does not exist execute format('select * from missing_table where a = %L', v); end; $$ language plpgsql; select * from plpgsql_check_function('uninitialized_is_mp(text)', all_warnings => true); drop function uninitialized_is_mp(text); create table traced_tab(a int); insert into traced_tab values(1); create or replace function traced_trg_func() returns trigger as $$ begin return old; end; $$ language plpgsql; create trigger traced_trg before delete on traced_tab for each row execute procedure traced_trg_func(); set plpgsql_check.enable_tracer to on; set plpgsql_check.tracer to on; set plpgsql_check.tracer_test_mode to true; delete from traced_tab; set plpgsql_check.tracer to off; set plpgsql_check.enable_tracer to off; drop table traced_tab cascade; drop function traced_trg_func(); create function failing_assert() returns void as $$ declare r record; begin select 1 as x into r; -- the reference to a record field creates a RECFIELD datum assert r.x = 1; end; $$ language plpgsql; set plpgsql_check.enable_tracer to on; set plpgsql_check.tracer to on; set plpgsql_check.trace_assert to on; set plpgsql_check.tracer_test_mode to true; select failing_assert(); set plpgsql_check.trace_assert to off; set plpgsql_check.tracer to off; set plpgsql_check.enable_tracer to off; drop function failing_assert(); create function dynamic_record_param() returns setof record as $$ begin -- the 12th USING argument is a record, so the tuple descriptor of the -- returned unpinned record is deduced from the param return query execute 'select $12' using 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, row(1, 2); end; $$ language plpgsql; select * from plpgsql_check_function('dynamic_record_param()'); drop function dynamic_record_param(); create function polymorphic_out_first(out b anyelement, a anyelement) as $$ begin b := a; end; $$ language plpgsql; create function call_polymorphic_out_first() returns void as $$ declare arg record; res record; begin select 1 as x, 2 as y into arg; res := polymorphic_out_first(arg); raise notice '%', res; end; $$ language plpgsql; select * from plpgsql_check_function('call_polymorphic_out_first()'); drop function call_polymorphic_out_first(); drop function polymorphic_out_first(anyelement); create function memsafety_profiled_fx() returns int as $$ begin return 1; end; $$ language plpgsql; set plpgsql_check.profiler to on; begin; select memsafety_profiled_fx(); select plpgsql_profiler_reset_all(); select memsafety_profiled_fx(); select plpgsql_profiler_reset_all(); commit; begin; select memsafety_profiled_fx(); select plpgsql_profiler_reset_all(); rollback; set plpgsql_check.profiler to off; drop function memsafety_profiled_fx(); create table memsafety_pt(a int); create function memsafety_pragma_tokens() returns void as $$ declare r record; begin -- "(", ")", ",", "[" and "]" are one character tokens perform plpgsql_check_pragma('table: memsafety_pt1(a int, b numeric(10,2), c int[])'); perform plpgsql_check_pragma('table: memsafety_pt2(like memsafety_pt)'); for r in execute 'select * from memsafety_pt1' loop raise notice '%', r.b; end loop; for r in execute 'select * from memsafety_pt2' loop raise notice '%', r.a; end loop; end; $$ language plpgsql; select * from plpgsql_check_function('memsafety_pragma_tokens()'); drop function memsafety_pragma_tokens(); drop table memsafety_pt; create function memsafety_format_pct() returns void as $$ begin raise notice '%', format('abc%'); end; $$ language plpgsql; select * from plpgsql_check_function('memsafety_format_pct()'); drop function memsafety_format_pct(); create function memsafety_format_pct2() returns void as $$ begin raise notice '%', format('abc%', 1); end; $$ language plpgsql; select * from plpgsql_check_function('memsafety_format_pct2()'); drop function memsafety_format_pct2(); create function memsafety_nextval_missing_rel() returns void as $$ begin perform nextval('4294967000'::regclass); end; $$ language plpgsql; select * from plpgsql_check_function('memsafety_nextval_missing_rel()'); drop function memsafety_nextval_missing_rel(); create type memsafety_ct as (a int, b int); create function memsafety_case_composite() returns void as $$ declare v memsafety_ct; begin v := row(1,2)::memsafety_ct; case v when row(1,2)::memsafety_ct then raise notice 'one'; else raise notice 'other'; end case; end; $$ language plpgsql; select * from plpgsql_check_function('memsafety_case_composite()'); select memsafety_case_composite(); drop function memsafety_case_composite(); drop type memsafety_ct; -- the constant is assigned in a branch, so it must not be used afterwards create or replace function repro02_stale_const(flag bool) returns void as $$ declare s text; begin if flag then s := 'select * from repro02_missing'; end if; execute s; end; $$ language plpgsql; select * from plpgsql_check_function('repro02_stale_const(bool)'); -- control: an unconditional assignment really is a constant and must be used create or replace function repro02_live_const() returns void as $$ declare s text; begin s := 'select * from repro02_missing'; execute s; end; $$ language plpgsql; select * from plpgsql_check_function('repro02_live_const()'); drop function repro02_stale_const(bool); drop function repro02_live_const(); set plpgsql_check.enable_tracer to on; set plpgsql_check.tracer to on; set plpgsql_check.tracer_test_mode to true; set plpgsql_check.tracer_verbosity to verbose; set client_min_messages to notice; -- the OUT parameters of a function are collected into an (unnamed) row datum, -- and RETURN traces that datum create or replace function repro03_out_params(out a int, out b text) as $$ begin a := 1; b := 'x'; return; end; $$ language plpgsql; select * from repro03_out_params(); set plpgsql_check.tracer to off; set plpgsql_check.enable_tracer to off; drop function repro03_out_params(); create or replace function repro04_tracked_const() returns void as $$ declare str text; res int; begin str := 'constant'; execute 'select $40' into res using 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40; raise notice '%, %', str, res; end; $$ language plpgsql; select * from plpgsql_check_function('repro04_tracked_const()'); create or replace function repro04_tracked_const() returns void as $$ declare str text; res int; begin str := 'constant'; execute 'select $41' into res using 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40; raise notice '%, %', str, res; end; $$ language plpgsql; select * from plpgsql_check_function('repro04_tracked_const()'); -- (b) param_get_desc(): the tuple descriptor of the unpinned record returned by -- the dynamic query is deduced from a high numbered parameter create or replace function repro04_param_desc() returns setof record as $$ begin return query execute 'select $40' using 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, row(1, 2); end; $$ language plpgsql; select * from plpgsql_check_function('repro04_param_desc()'); -- (c) plpgsql_check_assign_tupdesc_dno(): a query with no output column at all create table repro04_zero_columns(); create or replace function repro04_empty_tupdesc() returns int as $$ declare v int; begin select * from repro04_zero_columns into v; execute 'select * from repro04_zero_columns' into v; return v; end; $$ language plpgsql; select * from plpgsql_check_function('repro04_empty_tupdesc()'); drop function repro04_tracked_const(); drop function repro04_param_desc(); drop function repro04_empty_tupdesc(); drop table repro04_zero_columns; set plpgsql_check.profiler to on; set plpgsql_check.use_lxcache to on; create or replace function repro07_ok() returns int as $$ begin return 1; end; $$ language plpgsql; create or replace function repro07_err() returns int as $$ begin raise exception 'boom'; end; $$ language plpgsql; -- control: an aborted function with no lxcache from an earlier call is fine begin; select repro07_err(); commit; -- the successful call creates the lxcache, the failing one leaves an execution -- context behind, and the cleanup of that context re-enters get_lxcache() begin; select repro07_ok(); select repro07_err(); commit; select 'survived' as result; -- the same happens on an explicit ROLLBACK begin; select repro07_ok(); select repro07_err(); rollback; select 'survived' as result; set plpgsql_check.profiler to off; drop function if exists repro07_ok(); drop function if exists repro07_err(); CREATE SCHEMA IF NOT EXISTS repro01; SET search_path = repro01, public; CREATE TABLE t_b (a int); -- The EXECUTE records t_b into cstate_dynsql.rel_oids, which is then thrown -- away. The static PERFORM records the very same OID again, this time into -- the caller's cstate->rel_oids. CREATE FUNCTION f_dup() RETURNS void AS $$ BEGIN EXECUTE 'select * from repro01.t_b'; PERFORM * FROM repro01.t_b; END $$ LANGUAGE plpgsql; \echo '### Finding 1: duplicate RELATION dependency (expect 1 row, bug yields 2)' SELECT type, schema, name FROM plpgsql_show_dependency_tb('repro01.f_dup()') ORDER BY type, name; \echo '### Same query, counted:' SELECT count(*) AS relation_rows_for_t_b FROM plpgsql_show_dependency_tb('repro01.f_dup()') WHERE type = 'RELATION' AND name = 't_b'; -- Control: the purely static equivalent reports the relation exactly once. CREATE FUNCTION f_static() RETURNS void AS $$ BEGIN PERFORM * FROM repro01.t_b; PERFORM * FROM repro01.t_b; END $$ LANGUAGE plpgsql; \echo '### Control: static-only function reports t_b once' SELECT count(*) AS relation_rows_for_t_b FROM plpgsql_show_dependency_tb('repro01.f_static()') WHERE type = 'RELATION' AND name = 't_b'; DROP SCHEMA repro01 CASCADE; CREATE SCHEMA IF NOT EXISTS repro04; SET search_path = repro04, public; SET plpgsql_check.tracer_test_mode = true; SET plpgsql_check.tracer = on; SET plpgsql_check.tracer_verbosity = verbose; SET client_min_messages = error; -- silence the trace output itself -- (1) exception at maximum depth, EMPTY handler: nothing inside the handler -- calls stmt_beg, so the stack is only unwound by stmt_end of the block. CREATE FUNCTION f_empty_handler() RETURNS int AS $$ DECLARE x int := 0; BEGIN FOR i IN 1..3 LOOP BEGIN RAISE EXCEPTION 'boom'; EXCEPTION WHEN others THEN END; END LOOP; RETURN x; END $$ LANGUAGE plpgsql; -- (2) nested handlers, each re-raising: unwinds several levels per stmt_beg. CREATE FUNCTION f_nested_reraise() RETURNS int AS $$ BEGIN BEGIN BEGIN RAISE EXCEPTION 'inner'; EXCEPTION WHEN others THEN RAISE EXCEPTION 'middle'; END; EXCEPTION WHEN others THEN PERFORM 1; END; RETURN 1; END $$ LANGUAGE plpgsql; -- (3) exception thrown from deep inside loops and caught at the very top. CREATE FUNCTION f_deep_escape() RETURNS int AS $$ DECLARE j int; BEGIN BEGIN FOR i IN 1..2 LOOP FOREACH j IN ARRAY ARRAY[1,2] LOOP WHILE true LOOP IF j = 2 THEN RAISE EXCEPTION 'deep'; END IF; EXIT; END LOOP; END LOOP; END LOOP; EXCEPTION WHEN others THEN RETURN -1; END; RETURN 0; END $$ LANGUAGE plpgsql; -- (4) handler whose FIRST statement is itself a block (repair path must match -- the block's parent, not the failing statement's parent). CREATE FUNCTION f_handler_block() RETURNS int AS $$ BEGIN BEGIN BEGIN RAISE EXCEPTION 'x'; EXCEPTION WHEN others THEN BEGIN PERFORM 1; END; END; END; RETURN 1; END $$ LANGUAGE plpgsql; -- (5) repeated entry/exit so any leaked stack slot accumulates across calls. CREATE FUNCTION f_repeat() RETURNS int AS $$ DECLARE n int := 0; BEGIN FOR i IN 1..50 LOOP BEGIN BEGIN RAISE EXCEPTION 'e%', i; EXCEPTION WHEN others THEN END; n := n + 1; EXCEPTION WHEN others THEN n := n - 1; END; END LOOP; RETURN n; END $$ LANGUAGE plpgsql; -- (6) exception crossing a function boundary (separate plugin_info per estate). CREATE FUNCTION f_callee() RETURNS int AS $$ BEGIN BEGIN RAISE EXCEPTION 'from callee'; END; END $$ LANGUAGE plpgsql; CREATE FUNCTION f_caller() RETURNS int AS $$ BEGIN BEGIN FOR i IN 1..2 LOOP PERFORM f_callee(); END LOOP; EXCEPTION WHEN others THEN RETURN -1; END; RETURN 0; END $$ LANGUAGE plpgsql; RESET client_min_messages; \echo '### Driving stmt_beg/stmt_end imbalance patterns under the tracer' SET client_min_messages = error; SELECT f_empty_handler(); SELECT f_nested_reraise(); SELECT f_deep_escape(); SELECT f_handler_block(); SELECT f_repeat(); SELECT f_caller(); -- run them all again, now with the profiler also attached (two active plugins, -- which additionally exercises the duplicated palloc in func_setup) SET plpgsql_check.profiler = on; SELECT f_empty_handler(); SELECT f_nested_reraise(); SELECT f_deep_escape(); SELECT f_handler_block(); SELECT f_repeat(); SELECT f_caller(); RESET client_min_messages; \echo '### Survived: no assertion failure and the backend is still alive' SELECT 'still alive' AS status; DROP SCHEMA repro04 CASCADE; SELECT format('%2$s', 'a'); CREATE OR REPLACE FUNCTION repro05_a() RETURNS void AS $$ BEGIN RAISE NOTICE '%', format('%2$s', 'a'); END $$ LANGUAGE plpgsql; -- buggy: reports nothing -- correct: error:22023:...:RAISE:too few arguments for format() SELECT * FROM plpgsql_check_function('repro05_a()'); -- for comparison, the same mistake without an explicit position IS reported CREATE OR REPLACE FUNCTION repro05_a2() RETURNS void AS $$ BEGIN RAISE NOTICE '%', format('%s %s', 'a'); END $$ LANGUAGE plpgsql; SELECT * FROM plpgsql_check_function('repro05_a2()'); ---------------------------------------------------------------------- -- part B: missed SQL-injection warning ---------------------------------------------------------------------- CREATE OR REPLACE FUNCTION repro05_b(p text) RETURNS void AS $$ BEGIN EXECUTE format('SELECT %1$s', p); END $$ LANGUAGE plpgsql; -- buggy: reports nothing -- correct: security:00000:...:EXECUTE:text type variable is not sanitized SELECT * FROM plpgsql_check_function('repro05_b(text)', security_warnings => true); -- for comparison, the same code without an explicit position IS reported CREATE OR REPLACE FUNCTION repro05_b2(p text) RETURNS void AS $$ BEGIN EXECUTE format('SELECT %s', p); END $$ LANGUAGE plpgsql; SELECT * FROM plpgsql_check_function('repro05_b2(text)', security_warnings => true); DROP SEQUENCE IF EXISTS repro02_seq; CREATE SEQUENCE repro02_seq; -- 1st call returns a valid query, every later call returns NULL CREATE OR REPLACE FUNCTION repro02_gen() RETURNS text AS $$ BEGIN IF nextval('repro02_seq') = 1 THEN RETURN 'SELECT 1'; ELSE RETURN NULL; END IF; END $$ LANGUAGE plpgsql; CREATE OR REPLACE FUNCTION repro02_run() RETURNS void AS $$ BEGIN EXECUTE repro02_gen(); END $$ LANGUAGE plpgsql; -- PGC_USERSET SET plpgsql_check.profiler = on; -- boom SELECT repro02_run(); SET plpgsql_check.profiler = off; DROP SEQUENCE IF EXISTS repro03_seq; CREATE SEQUENCE repro03_seq; ---------------------------------------------------------------------- -- part A: the side effect of the query-text expression happens twice ---------------------------------------------------------------------- CREATE OR REPLACE FUNCTION repro03_gen() RETURNS text AS $$ BEGIN PERFORM nextval('repro03_seq'); RETURN 'SELECT 1'; END $$ LANGUAGE plpgsql; CREATE OR REPLACE FUNCTION repro03_run() RETURNS void AS $$ BEGIN EXECUTE repro03_gen(); END $$ LANGUAGE plpgsql; SET plpgsql_check.profiler = off; SELECT setval('repro03_seq', 1, false); SELECT repro03_run(); -- correct: 1 SELECT currval('repro03_seq') AS "nextval calls with profiler off"; SET plpgsql_check.profiler = on; SELECT setval('repro03_seq', 1, false); SELECT repro03_run(); -- buggy: 2 - the profiler evaluated repro03_gen() a second time SELECT currval('repro03_seq') AS "nextval calls with profiler on"; -- Use-after-free and NULL-pointer dereference in prepare_plan()'s retry loop. CREATE TABLE uaf_t (a int); CREATE FUNCTION uaf_f() RETURNS int LANGUAGE plpgsql AS $$ DECLARE v int; BEGIN SELECT count(*) INTO v FROM uaf_t; RETURN v; END; $$; -- Execute the function so PL/pgSQL caches its run-time plan in expr->plan. SELECT uaf_f(); -- Invalidate that cached plan with ordinary DDL. ALTER TABLE uaf_t ADD COLUMN b int; -- should not crash SELECT count(*) >= 0 FROM plpgsql_check_function('uaf_f()'); DROP TABLE uaf_t; DROP FUNCTION uaf_f; CREATE TABLE tt_tab (a int); CREATE FUNCTION tt_trg() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN RETURN NULL; END; $$; DO $$ BEGIN PERFORM * FROM plpgsql_check_function('tt_trg()', 'tt_tab', oldtable => 'dup', newtable => 'dup'); EXCEPTION WHEN OTHERS THEN -- A clean error is the fixed behaviour; we only require that the backend -- did not crash. NULL; END; $$; DROP TABLE IF EXISTS tt_tab; DROP FUNCTION tt_trg(); CREATE TYPE empty_comp AS (); CREATE FUNCTION oob_if() RETURNS int LANGUAGE plpgsql AS $$ DECLARE r empty_comp; BEGIN IF r THEN RETURN 1; END IF; RETURN 0; END; $$; CREATE FUNCTION oob_ret() RETURNS int LANGUAGE plpgsql AS $$ DECLARE r empty_comp; BEGIN RETURN (r); END; $$; SELECT * FROM plpgsql_check_function('oob_if()', fatal_errors => false); SELECT * FROM plpgsql_check_function('oob_ret()', fatal_errors => false); DROP FUNCTION oob_if; DROP FUNCTION oob_ret; DROP TYPE empty_comp; okbob-plpgsql_check-800db79/sql/plpgsql_check_nocrash_unstable_01.sql000066400000000000000000000014761526744060500260540ustar00rootroot00000000000000load 'plpgsql'; create extension if not exists plpgsql_check; load 'plpgsql_check'; set client_min_messages to notice; set plpgsql_check.regress_test_mode = true; set plpgsql_check.enable_tracer to on; set plpgsql_check.tracer to on; set plpgsql_check.trace_assert to on; set plpgsql_check.tracer_test_mode to true; create or replace function repro06_inner() returns void as $$ begin assert 1 = 2, 'boom'; end; $$ language plpgsql; create or replace function repro06_outer() returns void as $$ begin perform repro06_inner(); end; $$ language plpgsql; -- no outer error context frame - the loop is not entered, so this is safe select repro06_inner(); -- called from another function, so error_context_stack->previous is set select repro06_outer(); set plpgsql_check.tracer to off; set plpgsql_check.trace_assert to off; okbob-plpgsql_check-800db79/sql/plpgsql_check_parser.sql000066400000000000000000000274431526744060500235200ustar00rootroot00000000000000set client_min_messages to warning; create extension if not exists plpgsql_check; set client_min_messages to notice; -- -- Tests of the parser used for the function names passed to the -- plpgsql_check functions, and of the tokenizer and the parsers of -- the arguments of the pragmas. -- -- -- function name or signature -- create function pr_fx(a int) returns int as $$ begin return a; end; $$ language plpgsql; create function "pr FX"(a int) returns int as $$ begin return a; end; $$ language plpgsql; create function "pr""q"(a int) returns int as $$ begin return a; end; $$ language plpgsql; -- an overloaded function cannot be addressed by a name create function pr_over(a int) returns int as $$ begin return a; end; $$ language plpgsql; create function pr_over(a text) returns text as $$ begin return a; end; $$ language plpgsql; -- the errors are raised by the parser of the name, so they are catched -- and printed as a value create function pr_name(fnname text) returns text as $$ begin perform plpgsql_check_function(fnname); return 'ok'; exception when others then return sqlstate || ' ' || sqlerrm; end; $$ language plpgsql; -- plain name, with and without the leading and trailing spaces select pr_name('pr_fx'); select pr_name(' pr_fx '); -- quoted name, a doubled quote inside a quoted name select pr_name('"pr FX"'); select pr_name('"pr""q"'); -- qualified names select pr_name('public.pr_fx'); select pr_name('public."pr FX"'); select pr_name('"public" . "pr FX"'); -- signatures; the parser stops on the left parenthesis and the rest -- is processed by the regprocedure input function select pr_name('pr_fx(int)'); select pr_name('public.pr_fx(integer)'); select pr_name('"pr FX"(int)'); -- the name is not an identifier select pr_name('"pr FX'); select pr_name('""'); select pr_name('.pr_fx'); select pr_name('public.'); select pr_name('public..pr_fx'); select pr_name('1234'); select pr_name('pr_fx pr_fx'); -- the name is an identifier, but the function is not there or the -- name is not unique select pr_name('pr_missing'); select pr_name('pr_over'); drop function pr_name(text); drop function pr_over(text); drop function pr_over(int); drop function "pr""q"(int); drop function "pr FX"(int); drop function pr_fx(int); -- -- tokenizer -- -- The tokenizer is shared by all the pragmas and by the in-comment -- options. The echo option is used here, because it is the only place -- where a token of any type is accepted and printed back, so the result -- of the tokenization is visible. create function pr_tokens() returns void as $func$ -- @plpgsql_check_options: echo = 'a string with a '' quote' -- @plpgsql_check_options: echo = "a quoted identifier with a "" quote" -- @plpgsql_check_options: echo = AnIdentifier -- @plpgsql_check_options: echo = 3.14 begin end; $func$ language plpgsql; select * from plpgsql_check_function('pr_tokens'); drop function pr_tokens(); -- -- pragma "settype" -- create type pr_ctype as (a int, b int); create table pr_tab(a int, b int); create function pr_settype() returns void as $$ <> declare r record; n int; begin -- a plain, a qualified and a quoted type name perform plpgsql_check_pragma('type: r pr_ctype'); perform plpgsql_check_pragma('type: lbl.r public.pr_ctype'); perform plpgsql_check_pragma('type: "lbl"."r" public."pr_ctype"'); -- a table can be used as a type too perform plpgsql_check_pragma('type: r pr_tab'); raise notice '%', r.a; raise notice '%', n; end; $$ language plpgsql; select * from plpgsql_check_function('pr_settype'); -- errors of the "settype" pragma create or replace function pr_settype() returns void as $$ declare r record; n int; begin perform plpgsql_check_pragma('type: missing_var pr_ctype'); perform plpgsql_check_pragma('type: n pr_ctype'); perform plpgsql_check_pragma('type: r'); perform plpgsql_check_pragma('type: 1 pr_ctype'); perform plpgsql_check_pragma('type: r pr_ctype x'); raise notice '%', r.a; raise notice '%', n; end; $$ language plpgsql; select * from plpgsql_check_function('pr_settype'); drop function pr_settype(); -- -- the type parser -- -- The type used by the "settype" and "table" pragmas is parsed by -- plpgsql_check itself, so all the accepted forms are checked here. create function pr_type() returns void as $$ declare r record; begin -- a multiword type name, a type modifier, a list of type modifiers perform plpgsql_check_pragma('type: r (a double precision, b varchar(10), c numeric(10,2))'); -- an array, with and without a dimension perform plpgsql_check_pragma('type: r (a int[], b int[3], c pg_catalog.varchar[])'); -- datetime modifiers can precede more words, and arrays can have many dimensions perform plpgsql_check_pragma('type: r (a timestamp(3) without time zone, b time(2) with time zone, c interval day to second(2))'); perform plpgsql_check_pragma('type: r (a integer[][], b integer[2][3], c (d numeric(8,2), e timestamp(1) with time zone))'); -- a composite type copied from a table perform plpgsql_check_pragma('type: r (like pr_tab)'); raise notice '%', r.a; end; $$ language plpgsql; select * from plpgsql_check_function('pr_type'); create function pr_negative_scale() returns void as $$ declare r record; begin perform plpgsql_check_pragma('type: r (a numeric(5,-2), b int)'); raise notice '%', r.a; end; $$ language plpgsql; select case when current_setting('server_version_num')::int >= 150000 then not exists (select from plpgsql_check_function_tb('pr_negative_scale') where level = 'error') else true end as negative_scale; drop function pr_negative_scale(); -- errors of the type parser create or replace function pr_type() returns void as $$ declare r record; begin perform plpgsql_check_pragma('type: r (like int)'); perform plpgsql_check_pragma('type: r (like pr_tab'); perform plpgsql_check_pragma('type: r (1 int)'); perform plpgsql_check_pragma('type: r (a int b int)'); perform plpgsql_check_pragma('type: r (a numeric(x))'); perform plpgsql_check_pragma('type: r (a numeric(10'); perform plpgsql_check_pragma('type: r (a numeric(10 2))'); perform plpgsql_check_pragma('type: r (a int[)'); perform plpgsql_check_pragma('type: r (a int['); perform plpgsql_check_pragma('type: r (a "unclosed)'); raise notice '%', r.a; end; $$ language plpgsql; select * from plpgsql_check_function('pr_type'); drop function pr_type(); -- -- pragma "table" -- create function pr_table() returns void as $$ declare r record; begin perform plpgsql_check_pragma('table: pr_tt1(a int, b int)'); perform plpgsql_check_pragma('table: pg_temp.pr_tt2(a int)'); perform plpgsql_check_pragma('table: pg_temp."pr""tt3"(a int)'); select * from pr_tt1 into r; select * from pr_tt2 into r; raise notice '%', r.a; end; $$ language plpgsql; select * from plpgsql_check_function('pr_table'); -- errors of the "table" pragma create or replace function pr_table() returns void as $$ begin perform plpgsql_check_pragma('table: public.pr_tt4(a int)'); perform plpgsql_check_pragma('table: 1(a int)'); perform plpgsql_check_pragma('table: pg_temp.1(a int)'); perform plpgsql_check_pragma('table: pr_tt5'); perform plpgsql_check_pragma('table: pr_tt6(a int) x'); perform plpgsql_check_pragma('table: "pr_tt7(a int)'); -- a composite type is not allowed as a type of a column of a table -- created by this pragma perform plpgsql_check_pragma('table: pr_tt8(a (b int))'); -- constraints and collations are still outside the table pragma grammar perform plpgsql_check_pragma('table: pr_tt9(a int primary key)'); perform plpgsql_check_pragma('table: pr_tt9(a int not null)'); perform plpgsql_check_pragma('table: pr_tt9(a int default 1)'); perform plpgsql_check_pragma('table: pr_tt9(a int check (a > 0))'); perform plpgsql_check_pragma('table: pr_tt9(a text collate "C")'); end; $$ language plpgsql; select * from plpgsql_check_function('pr_table'); drop function pr_table(); -- -- pragma "sequence" -- create function pr_sequence() returns void as $$ begin perform plpgsql_check_pragma('sequence: pr_sq1'); perform plpgsql_check_pragma('sequence: pg_temp.pr_sq2'); perform plpgsql_check_pragma('sequence: pg_temp."pr""sq3"'); perform nextval('pg_temp.pr_sq1'); perform nextval('pg_temp.pr_sq2'); end; $$ language plpgsql; select * from plpgsql_check_function('pr_sequence'); -- errors of the "sequence" pragma create or replace function pr_sequence() returns void as $$ begin perform plpgsql_check_pragma('sequence: public.pr_sq4'); perform plpgsql_check_pragma('sequence: 1'); perform plpgsql_check_pragma('sequence: pg_temp.1'); perform plpgsql_check_pragma('sequence: pr_sq5 x y'); perform plpgsql_check_pragma('sequence: "pr_sq6'); end; $$ language plpgsql; select * from plpgsql_check_function('pr_sequence'); drop function pr_sequence(); -- -- pragmas "assert-schema", "assert-table" and "assert-column" -- create function pr_assert() returns void as $$ <> declare v_schema varchar default 'public'; v_table varchar default 'pr_tab'; v_column varchar default 'a'; begin raise notice '%', format('%I.%I.%I', v_schema, v_table, v_column); perform 'pragma:assert-schema: v_schema'; perform 'pragma:assert-table: v_schema, v_table'; perform 'pragma:assert-table: v_table'; perform 'pragma:assert-column: v_schema, v_table, v_column'; perform 'pragma:assert-column: v_table, v_column'; -- a variable can be addressed by a qualified name too perform 'pragma:assert-schema: lbl.v_schema'; end; $$ language plpgsql; select * from plpgsql_check_function('pr_assert'); -- the asserted objects do not exist create or replace function pr_assert() returns void as $$ declare v_schema varchar default 'pr_missing_schema'; v_table varchar default 'pr_tab'; v_column varchar default 'a'; begin perform 'pragma:assert-schema: v_schema'; perform 'pragma:assert-table: v_schema, v_table'; perform 'pragma:assert-column: v_schema, v_table, v_column'; end; $$ language plpgsql; select count(*) = 3 as missing_schemas from plpgsql_check_function_tb('pr_assert', fatal_errors => false) where sqlstate = '3F000'; create or replace function pr_assert() returns void as $$ declare v_schema varchar default 'pr_missing_schema'; v_table varchar default 'pr_missing_table'; v_column varchar default 'pr_missing_column'; begin perform 'pragma:assert-table: v_table'; end; $$ language plpgsql; select * from plpgsql_check_function('pr_assert'); create or replace function pr_assert() returns void as $$ declare v_schema varchar default 'public'; v_table varchar default 'pr_tab'; v_column varchar default 'pr_missing_column'; begin perform 'pragma:assert-column: v_schema, v_table, v_column'; end; $$ language plpgsql; select * from plpgsql_check_function('pr_assert'); -- errors of the "assert" pragmas; they are reported as warnings and the -- check continues create or replace function pr_assert() returns void as $$ <> declare v_schema varchar default 'public'; v_table varchar default 'pr_tab'; v_column varchar default 'a'; v_var varchar; begin -- an unknown variable, a name with too many parts perform 'pragma:assert-schema: v_missing'; perform 'pragma:assert-schema: a.b.c.d'; -- a variable without an assigned constant perform 'pragma:assert-schema: v_var'; -- a missing comma between the variables perform 'pragma:assert-table: v_schema v_table'; -- too many and too few variables; the parser reads three names at -- most, so a fourth one is reported as an unexpected text perform 'pragma:assert-schema: v_schema, v_table'; perform 'pragma:assert-table: v_schema, v_table, v_column'; perform 'pragma:assert-column: v_schema, v_table, v_column, v_column'; perform 'pragma:assert-column: v_table'; -- a syntax error in the name of the variable perform 'pragma:assert-schema: 1'; end; $$ language plpgsql; select * from plpgsql_check_function('pr_assert'); drop function pr_assert(); drop table pr_tab; drop type pr_ctype; okbob-plpgsql_check-800db79/sql/plpgsql_check_passive-14.sql000066400000000000000000000000001526744060500240740ustar00rootroot00000000000000okbob-plpgsql_check-800db79/sql/plpgsql_check_passive-15.sql000066400000000000000000000000001526744060500240750ustar00rootroot00000000000000okbob-plpgsql_check-800db79/sql/plpgsql_check_passive-16.sql000066400000000000000000000000001526744060500240760ustar00rootroot00000000000000okbob-plpgsql_check-800db79/sql/plpgsql_check_passive-17.sql000066400000000000000000000000001526744060500240770ustar00rootroot00000000000000okbob-plpgsql_check-800db79/sql/plpgsql_check_passive-18.sql000066400000000000000000000000001526744060500241000ustar00rootroot00000000000000okbob-plpgsql_check-800db79/sql/plpgsql_check_passive-19.sql000066400000000000000000000000001526744060500241010ustar00rootroot00000000000000okbob-plpgsql_check-800db79/sql/plpgsql_check_passive-20.sql000066400000000000000000000000001526744060500240710ustar00rootroot00000000000000okbob-plpgsql_check-800db79/sql/plpgsql_check_passive.sql000066400000000000000000000146751526744060500237010ustar00rootroot00000000000000load 'plpgsql'; load 'plpgsql_check'; set client_min_messages to notice; -- enforce context's displaying -- emulate pre 9.6 behave \set SHOW_CONTEXT always set plpgsql_check.mode = 'every_start'; create table t1(a int, b int); create function f1() returns void as $$ begin if false then update t1 set c = 30; end if; end; $$ language plpgsql; select f1(); drop function f1(); create function f1() returns void as $$ begin if false then insert into t1 values(10,20); update t1 set a = 10; delete from t1; end if; end; $$ language plpgsql stable; select f1(); drop function f1(); create function g1(out a int, out b int) as $$ select 10,20; $$ language sql; create function f1() returns void as $$ declare r record; begin r := g1(); if false then raise notice '%', r.c; end if; end; $$ language plpgsql; select f1(); drop function f1(); drop function g1(); create function g1(out a int, out b int) returns setof record as $$ select * from t1; $$ language sql; create function f1() returns void as $$ declare r record; begin for r in select * from g1() loop raise notice '%', r.c; end loop; end; $$ language plpgsql; select f1(); create or replace function f1() returns void as $$ declare r record; begin for r in select * from g1() loop r.c := 20; end loop; end; $$ language plpgsql; select f1(); drop function f1(); drop function g1(); create function f1() returns int as $$ declare r int; begin if false then r := a + b; end if; return r; end; $$ language plpgsql; select f1(); drop function f1(); create or replace function f1() returns void as $$ declare r int[]; begin if false then r[c+10] := 20; end if; end; $$ language plpgsql; select f1(); drop function f1(); create or replace function f1() returns void as $$ declare r int; begin if false then r[10] := 20; end if; end; $$ language plpgsql; select f1(); drop function f1(); create or replace function f1() returns void as $$ begin if false then insert into badbadtable values(10,20); end if; return; end; $$ language plpgsql; set plpgsql_check.mode = 'fresh_start'; select f1(); -- should not raise exception there select f1(); create or replace function f1() returns void as $$ begin if false then insert into badbadtable values(10,20); end if; return; end; $$ language plpgsql; -- after refreshing it should to raise exception again select f1(); set plpgsql_check.mode = 'every_start'; -- should to raise warning only set plpgsql_check.fatal_errors = false; select f1(); drop function f1(); create function f1() returns setof t1 as $$ begin if false then return query select a,a,a from t1; return; end if; end; $$ language plpgsql; select * from f1(); drop function f1(); create function f1() returns setof t1 as $$ begin if false then return query select a, b::numeric from t1; return; end if; end; $$ language plpgsql; select * from f1(); drop function f1(); drop table t1; do $$ declare begin if false then for i in 1,3..(2) loop raise notice 'foo %', i; end loop; end if; end; $$; -- tests designed for 9.2 set check_function_bodies to off; create or replace function f1() returns void as $$ begin if false then raise notice '%', 1, 2; end if; end; $$ language plpgsql; select f1(); drop function f1(); create or replace function f1() returns void as $$ begin if false then raise notice '% %'; end if; end; $$ language plpgsql; select f1(); drop function f1(); create type _exception_type as ( state text, message text, detail text); create or replace function f1() returns void as $$ declare _exception record; begin _exception := NULL::_exception_type; exception when others then get stacked diagnostics _exception.state = RETURNED_SQLSTATE, _exception.message = MESSAGE_TEXT, _exception.detail = PG_EXCEPTION_DETAIL, _exception.hint = PG_EXCEPTION_HINT; end; $$ language plpgsql; select f1(); drop function f1(); drop type _exception_type; create table footab(a int, b int, c int); create or replace function footab_trig_func() returns trigger as $$ declare x int; begin if false then -- should be ok; select count(*) from newtab into x; -- should fail; select count(*) from newtab where d = 10 into x; end if; return null; end; $$ language plpgsql; create trigger footab_trigger after insert on footab referencing new table as newtab for each statement execute procedure footab_trig_func(); -- should to fail insert into footab values(1,2,3); create or replace function footab_trig_func() returns trigger as $$ declare x int; begin if false then -- should be ok; select count(*) from newtab into x; end if; return null; end; $$ language plpgsql; -- should be ok insert into footab values(1,2,3); drop table footab; drop function footab_trig_func(); set plpgsql_check.mode = 'every_start'; create or replace procedure proc_test() as $$ begin commit; end; $$ language plpgsql; call proc_test(); drop procedure proc_test(); -- should not to crash set plpgsql_check.mode = 'fresh_start'; do $$ begin end; $$; create table t(a int, b text); create type ct as (p int, q text); create or replace function trg_fn() returns trigger as $$ begin new.a := new.a + 1; return new; end $$ language plpgsql; create trigger t_trg before insert on t for each row execute function trg_fn(); create or replace function local_rowvar() returns text as $$ declare r t; r2 t%rowtype; c ct; begin r.b := 'x'; r2.b := 'y'; c.q := 'z'; return r.b || r2.b || c.q; end $$ language plpgsql; create or replace function composite_arg(p ct) returns text as $$ begin p.q := 'w'; return p.q; end $$ language plpgsql; -- and everything works without passive checking set plpgsql_check.mode = 'by_function'; insert into t values (1, 'a') returning *; select local_rowvar(); select composite_arg(row(1, 'a')); -- should work: passive mode must not break valid functions -- (fresh_start first: it checks a function only once per compiled version, -- so it has to run before every_start marks the functions as checked) set plpgsql_check.mode = 'fresh_start'; insert into t values (1, 'a') returning *; select local_rowvar(); select composite_arg(row(1, 'a')); set plpgsql_check.mode = 'every_start'; insert into t values (1, 'a') returning *; select local_rowvar(); select composite_arg(row(1, 'a')); drop function composite_arg; drop function local_rowvar; drop table t; drop function trg_fn; drop type ct; okbob-plpgsql_check-800db79/sql/plpgsql_check_pldbgapi3.sql000066400000000000000000000152751526744060500240710ustar00rootroot00000000000000set client_min_messages to warning; create extension if not exists plpgsql_check; set client_min_messages to notice; -- -- Tests of the plugin multiplexer. -- -- The plugin of plpgsql allows one client only, so plpgsql_check -- registers a multiplexer, which dispatches the events to the tracer, -- to the profiler and to the passive check. The multiplexer keeps a -- stack of the entered statements per an executed routine, and it has -- to unwind the stack when an exception aborts the execution. -- -- the multiplexer is installed when the library is loaded, so the -- routines which are already running have no state allocated for them, -- and the events of their statements have to be ignored (this is a no -- operation when the library is preloaded) do $$ begin execute 'load ''plpgsql_check'''; perform 1; end; $$; select plpgsql_check_tracer(true, 'terse'); set plpgsql_check.enable_tracer to on; set plpgsql_check.tracer_test_mode to true; set plpgsql_check.profiler to on; create function pd_f1(a int) returns int as $$ begin if a = 0 then raise exception 'pd_f1 failed'; end if; return a; end; $$ language plpgsql; create function pd_f2(a int) returns int as $$ declare res int; begin begin res := pd_f1(a); exception when others then res := -1; end; return res; end; $$ language plpgsql; -- no exception, the statements are closed in the expected order select pd_f2(1); -- the called routine is aborted, so its part of the stack is unwound -- when the outer routine continues select pd_f2(0); -- the same, but the exception is raised in a deeper nested block, so -- more statements have to be closed at once create function pd_f3(a int) returns int as $$ declare res int; begin begin for i in 1..3 loop if i = 2 then res := pd_f1(a); end if; end loop; exception when others then res := -1; end; return res; end; $$ language plpgsql; select pd_f3(0); -- an exception which is not handled by the executed routine leaves the -- whole stack open do $$ begin perform pd_f3(1); perform pd_f1(0); exception when others then raise notice 'handled by the block'; end; $$; -- an exception raised by an inline block, which is not handled at all do $$ begin perform pd_f1(0); end; $$; -- the memory of an aborted inline block is released later than the -- memory of an aborted routine, so its entry has to be removed from the -- stack of the multiplexer when the outer routine continues create function pd_f4() returns int as $$ declare res int; begin begin execute 'do $x$ begin perform pd_f1(0); end $x$'; exception when others then res := -1; end; res := coalesce(res, 0); return res; end; $$ language plpgsql; select pd_f4(); -- when a plugin raises an exception, the multiplexer has to restore the -- plugin info of the executed routine before the exception is reraised set plpgsql_check.cursors_leaks to on; set plpgsql_check.cursors_leaks_errlevel to 'error'; create function pd_f5() returns int as $$ declare c refcursor; begin open c for select 1; return 1; end; $$ language plpgsql; -- the leaked cursor is detected when the routine is left set plpgsql_check.strict_cursors_leaks to on; do $$ begin perform pd_f5(); exception when others then raise notice 'leak reported: %', sqlerrm; end; $$; -- and when the statement which opened it is executed again set plpgsql_check.strict_cursors_leaks to off; create function pd_f6() returns int as $$ declare c refcursor; begin for i in 1..2 loop c := 'pd_cursor_' || i; open c for select 1; end loop; return 1; end; $$ language plpgsql; do $$ begin perform pd_f6(); exception when others then raise notice 'leak reported: %', sqlerrm; end; $$; set plpgsql_check.strict_cursors_leaks to default; set plpgsql_check.cursors_leaks_errlevel to default; set plpgsql_check.cursors_leaks to default; -- the multiplexer is not used when no plugin is active set plpgsql_check.enable_tracer to off; set plpgsql_check.profiler to off; select pd_f2(0); -- the profiler alone set plpgsql_check.profiler to on; select pd_f2(0); set plpgsql_check.profiler to off; set plpgsql_check.enable_tracer to off; select plpgsql_check_tracer(false, 'default'); -- Recursive callers keep their cursors open, but each invocation closes its own set plpgsql_check.cursors_leaks to on; set plpgsql_check.strict_cursors_leaks to off; set plpgsql_check.cursors_leaks_errlevel to 'error'; create function pd_cursor_recursive(depth int) returns void as $$ declare c refcursor; begin open c for select 1; if depth > 0 then perform pd_cursor_recursive(depth - 1); end if; close c; end; $$ language plpgsql; do $$ begin perform pd_cursor_recursive(1); perform pd_cursor_recursive(1); end; $$; -- Genuine leaks between sequential calls must still be detected do $$ begin perform pd_f5(); perform pd_f5(); exception when invalid_cursor_state then raise notice 'sequential cursor leak detected'; end; $$; set plpgsql_check.strict_cursors_leaks to default; set plpgsql_check.cursors_leaks_errlevel to default; set plpgsql_check.cursors_leaks to default; drop function pd_cursor_recursive(int); drop function pd_f6(); drop function pd_f5(); drop function pd_f4(); drop function pd_f3(int); drop function pd_f2(int); drop function pd_f1(int); -- when no plugin is active for an aborted routine (the tracer and the -- profiler are disabled now), its entry has to be removed from the stack -- of the multiplexer too, else the stack would point to released memory -- when the outer routine continues create function pd_f7() returns void as $$ begin raise exception 'pd_f7 failed'; end; $$ language plpgsql set plpgsql_check.cursors_leaks = off; create function pd_f8() returns text as $$ begin begin perform pd_f7(); exception when others then return 'handled: ' || sqlerrm; end; return 'not reached'; end; $$ language plpgsql; select pd_f8(); -- the same when the error is not handled at all: during the cleanup of the -- aborted transaction, the reset callback of the outer routine (with an -- active plugin) must not find the released entry of the inner routine -- on the stack create function pd_f10() returns text as $$ begin perform pd_f7(); return 'not reached'; end; $$ language plpgsql; select pd_f10(); -- an inline block without any active plugin, aborted by a routine with -- an active plugin, has to unwind the entry of that routine too create function pd_f9() returns void as $$ begin raise exception 'pd_f9 failed'; end; $$ language plpgsql set plpgsql_check.cursors_leaks = on; set plpgsql_check.cursors_leaks to off; do $$ begin perform pd_f9(); end; $$; set plpgsql_check.cursors_leaks to default; select pd_f8(); drop function pd_f10(); drop function pd_f9(); drop function pd_f8(); drop function pd_f7(); okbob-plpgsql_check-800db79/sql/plpgsql_check_pragma.sql000066400000000000000000000175351526744060500234740ustar00rootroot00000000000000-- -- regress tests of the pragma function (src/pragma.c) -- -- The plpgsql_check_pragma() function has two completely separate -- implementations. When plpgsql_check walks the AST, the arguments are -- taken from the parse tree and applied to the check state. When the -- function is really executed, the arguments are processed as ordinary -- variadic text arguments, and only the tracer can be controlled. -- load 'plpgsql_check'; set client_min_messages to warning; create extension if not exists plpgsql_check; set client_min_messages to notice; -- -- compile time pragmas -- -- STATUS reports the current value of every switch. create function prg_status() returns void as $$ begin perform plpgsql_check_pragma('status:check'); perform plpgsql_check_pragma('status:tracer'); perform plpgsql_check_pragma('status:other_warnings'); perform plpgsql_check_pragma('status:performance_warnings'); perform plpgsql_check_pragma('status:extra_warnings'); perform plpgsql_check_pragma('status:security_warnings'); perform plpgsql_check_pragma('status:compatibility_warnings'); perform plpgsql_check_pragma('status:constants_tracing'); -- spaces around the pragma name and its argument are ignored perform plpgsql_check_pragma(' status: check'); end; $$ language plpgsql; select * from plpgsql_check_function('prg_status'); -- DISABLE turns every switch off, the following STATUS shows it. create function prg_disable() returns void as $$ begin perform plpgsql_check_pragma(' disable: check'); perform plpgsql_check_pragma('disable:tracer'); perform plpgsql_check_pragma('disable:other_warnings'); perform plpgsql_check_pragma('disable:performance_warnings'); perform plpgsql_check_pragma('disable:extra_warnings'); perform plpgsql_check_pragma('disable:security_warnings'); perform plpgsql_check_pragma('disable:compatibility_warnings'); perform plpgsql_check_pragma('disable:constants_tracing'); perform plpgsql_check_pragma('status:check'); perform plpgsql_check_pragma('status:tracer'); perform plpgsql_check_pragma('status:other_warnings'); perform plpgsql_check_pragma('status:performance_warnings'); perform plpgsql_check_pragma('status:extra_warnings'); perform plpgsql_check_pragma('status:security_warnings'); perform plpgsql_check_pragma('status:compatibility_warnings'); perform plpgsql_check_pragma('status:constants_tracing'); end; $$ language plpgsql; select * from plpgsql_check_function('prg_disable'); -- ENABLE turns them on again create function prg_enable() returns void as $$ begin perform plpgsql_check_pragma('disable:check'); perform plpgsql_check_pragma('disable:tracer'); perform plpgsql_check_pragma('disable:other_warnings'); perform plpgsql_check_pragma('disable:performance_warnings'); perform plpgsql_check_pragma('disable:extra_warnings'); perform plpgsql_check_pragma('disable:security_warnings'); perform plpgsql_check_pragma('disable:compatibility_warnings'); perform plpgsql_check_pragma('disable:constants_tracing'); perform plpgsql_check_pragma(' enable: check'); perform plpgsql_check_pragma('enable:tracer'); perform plpgsql_check_pragma('enable:other_warnings'); perform plpgsql_check_pragma('enable:performance_warnings'); perform plpgsql_check_pragma('enable:extra_warnings'); perform plpgsql_check_pragma('enable:security_warnings'); perform plpgsql_check_pragma('enable:compatibility_warnings'); perform plpgsql_check_pragma('enable:constants_tracing'); perform plpgsql_check_pragma('status:check'); perform plpgsql_check_pragma('status:tracer'); perform plpgsql_check_pragma('status:other_warnings'); perform plpgsql_check_pragma('status:performance_warnings'); perform plpgsql_check_pragma('status:extra_warnings'); perform plpgsql_check_pragma('status:security_warnings'); perform plpgsql_check_pragma('status:compatibility_warnings'); perform plpgsql_check_pragma('status:constants_tracing'); end; $$ language plpgsql; select * from plpgsql_check_function('prg_enable'); create function prg_constants_scope(p boolean) returns void as $$ begin if p then perform plpgsql_check_pragma('disable:constants_tracing'); perform plpgsql_check_pragma('status:constants_tracing'); else perform plpgsql_check_pragma('status:constants_tracing'); end if; perform plpgsql_check_pragma('status:constants_tracing'); end; $$ language plpgsql; select * from plpgsql_check_function('prg_constants_scope'); drop function prg_constants_scope(boolean); -- an unknown switch is reported for every pragma that takes a switch -- name, and an entirely unknown pragma is reported too create function prg_unsupported() returns void as $$ begin perform plpgsql_check_pragma('status:unknown_switch'); perform plpgsql_check_pragma('enable:unknown_switch'); perform plpgsql_check_pragma('disable:unknown_switch'); perform plpgsql_check_pragma('unknown_pragma: something'); end; $$ language plpgsql; select * from plpgsql_check_function('prg_unsupported'); -- ECHO prints its argument. The @@name and @@signature placeholders are -- replaced by the name and the signature of the checked function, an -- unknown placeholder is copied to the output. create function prg_echo(a int, b text) returns void as $$ begin perform plpgsql_check_pragma('echo: nothing to substitute here'); perform plpgsql_check_pragma('echo: the name is @@name'); perform plpgsql_check_pragma('echo: the signature is @@signature'); perform plpgsql_check_pragma('echo: @@unknown is not a placeholder'); perform plpgsql_check_pragma('echo:'); end; $$ language plpgsql; select * from plpgsql_check_function('prg_echo'); -- more pragmas can be passed in one call create function prg_multi() returns void as $$ begin perform plpgsql_check_pragma('disable:extra_warnings', 'status:extra_warnings', 'enable:extra_warnings', 'status:extra_warnings'); end; $$ language plpgsql; select * from plpgsql_check_function('prg_multi'); drop function prg_multi(); drop function prg_echo(int, text); drop function prg_unsupported(); drop function prg_enable(); drop function prg_disable(); drop function prg_status(); -- -- runtime pragmas -- -- When the pragma function is really executed, only the tracer pragmas -- are recognized, everything else is silently ignored. The tracer is -- only armed here, no tracing is started, because that additionally -- requires plpgsql_check.enable_tracer. -- select plpgsql_check_pragma('status:tracer'); select plpgsql_check_pragma('enable:tracer'); select plpgsql_check_pragma('status:tracer'); select plpgsql_check_pragma('disable:tracer'); select plpgsql_check_pragma('status:tracer'); -- leading spaces and spaces after the colon are ignored select plpgsql_check_pragma(' enable: tracer'); select plpgsql_check_pragma(' status: tracer'); select plpgsql_check_pragma(' disable: tracer'); -- pragmas which are compile time only, and unknown pragmas, are ignored select plpgsql_check_pragma('status:check'); select plpgsql_check_pragma('enable:check'); select plpgsql_check_pragma('disable:check'); select plpgsql_check_pragma('echo: not printed in runtime'); select plpgsql_check_pragma('this is not a pragma'); select plpgsql_check_pragma(''); -- NULL argument is ignored, NULL elements of the argument array are -- skipped select plpgsql_check_pragma(VARIADIC NULL::text[]); select plpgsql_check_pragma(VARIADIC array[NULL, 'enable:tracer', NULL]::text[]); select plpgsql_check_pragma('status:tracer'); select plpgsql_check_pragma('disable:tracer'); -- the pragma function called from a really executed function create function prg_runtime() returns void as $$ begin perform plpgsql_check_pragma('enable:tracer'); perform plpgsql_check_pragma('status:tracer'); perform plpgsql_check_pragma('disable:tracer'); perform plpgsql_check_pragma('status:tracer'); end; $$ language plpgsql; select prg_runtime(); drop function prg_runtime(); okbob-plpgsql_check-800db79/sql/plpgsql_check_profiler.sql000066400000000000000000000703651526744060500240470ustar00rootroot00000000000000create table t1(a int, b int); -- profiler check set plpgsql_check.profiler to on; create function f1() returns void as $$ begin if false then insert into t1 values(10,20); update t1 set a = 10; delete from t1; end if; end; $$ language plpgsql; select lineno, stmt_lineno, exec_stmts, source from plpgsql_profiler_function_tb('f1()'); select f1(); select lineno, stmt_lineno, exec_stmts, source from plpgsql_profiler_function_tb('f1()'); select plpgsql_profiler_reset('f1()'); select lineno, stmt_lineno, exec_stmts, source from plpgsql_profiler_function_tb('f1()'); select f1(); select lineno, stmt_lineno, exec_stmts, source from plpgsql_profiler_function_tb('f1()'); select plpgsql_profiler_reset_all(); select lineno, stmt_lineno, exec_stmts, source from plpgsql_profiler_function_tb('f1()'); drop function f1(); create or replace function f1() returns void as $$ begin raise notice '1'; exception when others then raise notice '2'; end; $$ language plpgsql; select f1(); select lineno, stmt_lineno, exec_stmts, source from plpgsql_profiler_function_tb('f1()'); drop function f1(); -- test queryid retrieval create function f1() returns void as $$ declare t1 text = 't1'; begin insert into t1 values(10,20); EXECUTE 'update ' || 't1' || ' set a = 10'; EXECUTE 'delete from ' || t1; end; $$ language plpgsql; select plpgsql_profiler_reset_all(); select plpgsql_profiler_install_fake_queryid_hook(); select f1(); select queryids, lineno, stmt_lineno, exec_stmts, source from plpgsql_profiler_function_tb('f1()'); select plpgsql_profiler_remove_fake_queryid_hook(); drop function f1(); -- don't crash on empty dynamic query create or replace function f1() returns void as $$ begin execute ''; end; $$ language plpgsql; select f1(); drop function f1(); create or replace function covtest(int) returns int as $$ declare a int = $1; begin a := a + 1; if a < 10 then a := a + 1; drop function test_function(); drop function test_function(); drop function test_function(); drop function test_function(); drop function test_function(); drop function test_function(); drop function test_function(); drop function test_function(); drop function test_function(); drop function test_function(); drop function test_function(); drop function test_function; drop function test_function(); drop function test_function(); drop function test_function(); drop function test_function(); drop function test_function(); drop function test_function(); drop function test_function(); drop function test_function(); drop function test_function(); drop function test_function(); drop function test_function(); drop function test_function(); drop function test_function; drop function test_function(); end if; a := a + 1; return a; end; $$ language plpgsql; select covtest(10); -- should not crash select stmtid, exec_stmts, stmtname from plpgsql_profiler_function_statements_tb('covtest'); drop function covtest; set plpgsql_check.profiler to off; -- test of very long function inside profiler create or replace function longfx(int) returns int as $$ declare s int default 0; j int default 0; r record; begin begin while j < 10 loop for i in 1..1 loop for r in select * from generate_series(1,1) loop s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; s := s + 1; end loop; end loop; j := j + 1; end loop; exception when others then raise 'reraised exception %', sqlerrm; end; return $1; end; $$ language plpgsql; select lineno, stmt_lineno, exec_stmts, source from plpgsql_profiler_function_tb('longfx'); set plpgsql_check.profiler = on; select longfx(10); select longfx(10); set plpgsql_check.profiler = off; select longfx(10); select lineno, stmt_lineno, exec_stmts, source from plpgsql_profiler_function_tb('longfx'); select funcoid, exec_count from plpgsql_profiler_functions_all(); -- coverage tests set plpgsql_check.profiler to on; create or replace function covtest(int) returns int as $$ declare a int = $1; begin a := a + 1; if a < 10 then a := a + 1; end if; a := a + 1; return a; end; $$ language plpgsql; set plpgsql_check.profiler to on; select covtest(10); select stmtid, exec_stmts, stmtname from plpgsql_profiler_function_statements_tb('covtest'); select plpgsql_coverage_statements('covtest'); select plpgsql_coverage_branches('covtest'); select covtest(1); select stmtid, exec_stmts, stmtname from plpgsql_profiler_function_statements_tb('covtest'); select plpgsql_coverage_statements('covtest'); select plpgsql_coverage_branches('covtest'); set plpgsql_check.profiler to off; -- aborted function has profile too create or replace function test_function(a int) returns int as $$ begin if (a > 5) then a := a + 10; return a; else raise exception 'a < 5'; end if; end; $$ language plpgsql; set plpgsql_check.profiler to on; select test_function(1); select test_function(10); select lineno, exec_stmts, exec_stmts_err, source from plpgsql_profiler_function_tb('test_function'); create or replace function test_function1(a int) returns int as $$ begin if (a > 5) then a := a + 10; return a; else raise exception 'a < 5'; end if; exception when others then raise notice 'do warning'; return -1; end; $$ language plpgsql; select test_function1(1); select test_function1(10); select lineno, exec_stmts, exec_stmts_err, source from plpgsql_profiler_function_tb('test_function1'); drop function test_function(int); drop function test_function1(int); drop table t1; create or replace function test_trg() returns trigger as $$ begin if new.a > 10 then new.a := 10; end if; return new; end; $$ language plpgsql; create table t1(a int); create trigger t1_insert before insert on t1 for each row execute function test_trg(); insert into t1 values(0); select * from plpgsql_coverage_branches('test_trg'); select * from plpgsql_coverage_statements('test_trg'); insert into t1 values(22); select * from plpgsql_coverage_branches('test_trg'); select * from plpgsql_coverage_statements('test_trg'); select stmtid, lineno, exec_stmts, exec_stmts_err from plpgsql_profiler_function_statements_tb('test_trg'); select stmt_lineno, exec_stmts, exec_stmts_err, source from plpgsql_profiler_function_tb('test_trg'); drop trigger t1_insert on t1; drop function test_trg; drop table t1; set plpgsql_check.profiler to off; okbob-plpgsql_check-800db79/sql/plpgsql_check_profiler_ctrl.sql000066400000000000000000000053561526744060500250710ustar00rootroot00000000000000set client_min_messages to warning; create extension if not exists plpgsql_check; set client_min_messages to notice; -- -- Tests of the control functions of the profiler and of the checks of -- the arguments of the profiler functions. -- -- the profiler is enabled and disabled by a function, which reports the -- new state and returns it select plpgsql_check_profiler(true); select plpgsql_check_profiler(false); -- when the argument is omitted or null, the state is only reported. The -- reported notices describe the configuration of the shared memory, -- which depends on the preloading of the library and on the size of the -- allocated segment, so they are suppressed here and only the returned -- value is checked. set client_min_messages to warning; select plpgsql_check_profiler(); select plpgsql_check_profiler(true); select plpgsql_check_profiler(); set plpgsql_check.use_shared_stats_when_it_possible to off; select plpgsql_check_profiler(null); set plpgsql_check.use_shared_stats_when_it_possible to default; select plpgsql_check_profiler(null); select plpgsql_check_profiler(false); set client_min_messages to notice; -- the fake queryid hook is installed and removed only once, the -- repeated calls do nothing select plpgsql_profiler_install_fake_queryid_hook(); select plpgsql_profiler_install_fake_queryid_hook(); select plpgsql_profiler_remove_fake_queryid_hook(); select plpgsql_profiler_remove_fake_queryid_hook(); -- the coverage functions are not strict, so they check the argument -- themselves select plpgsql_coverage_statements(null::text); select plpgsql_coverage_branches(null::text); select plpgsql_coverage_statements(null::regprocedure); select plpgsql_coverage_branches(null::regprocedure); -- the profile of a function which was never executed while the profiler -- was active is empty, and the function is not listed create function pc_f1() returns void as $$ begin raise exception 'pc_f1 failed'; end; $$ language plpgsql; do $$ begin perform pc_f1(); exception when others then raise notice 'catched: %', sqlerrm; end; $$; select stmtid, exec_stmts, stmtname from plpgsql_profiler_function_statements_tb('pc_f1'); -- a function whose whole body is written on a single line has all the -- statements on one row of the profile set plpgsql_check.profiler to on; create function pc_f2(a int) returns int as $$ declare b int; begin b := a; if b > 0 then b := -b; end if; return b; end $$ language plpgsql; select pc_f2(1); select lineno, stmt_lineno, exec_stmts, source from plpgsql_profiler_function_tb('pc_f2'); select plpgsql_coverage_statements('pc_f2(int)'); select plpgsql_coverage_branches('pc_f2(int)'); set plpgsql_check.profiler to off; select plpgsql_profiler_reset_all(); drop function pc_f2(int); drop function pc_f1(); okbob-plpgsql_check-800db79/sql/plpgsql_check_profiler_local.sql000066400000000000000000000275531526744060500252220ustar00rootroot00000000000000set client_min_messages to warning; create extension if not exists plpgsql_check; set client_min_messages to notice; -- -- Tests of the profiler with the statistics stored in the local memory -- of the session. -- -- When the shared memory was preallocated, the profiler stores the -- statistics there, so the local variant of the storage is used only -- when the shared memory is not available or when it is disabled by -- the following option. The option is used here, so the same code is -- tested in both configurations. -- set plpgsql_check.use_shared_stats_when_it_possible to off; -- the profiler can be enabled by a function too select plpgsql_check_profiler(true); create table pl_t1(a int, b int); create function pl_f1(a int) returns int as $$ declare b int = 0; begin if a > 10 then b := a; else b := -a; end if; while b > 0 loop b := b - 1; end loop; return b; end; $$ language plpgsql; -- there are no statistics before the first execution of the function, -- but the statements are listed anyway select stmtid, parent_stmtid, exec_stmts, stmtname from plpgsql_profiler_function_statements_tb('pl_f1'); select lineno, stmt_lineno, exec_stmts, source from plpgsql_profiler_function_tb('pl_f1'); select plpgsql_coverage_statements('pl_f1'); select plpgsql_coverage_branches('pl_f1'); -- only a part of the function is executed, so the coverage is partial select pl_f1(20); select stmtid, parent_stmtid, exec_stmts, stmtname from plpgsql_profiler_function_statements_tb('pl_f1'); select lineno, stmt_lineno, exec_stmts, source from plpgsql_profiler_function_tb('pl_f1'); select plpgsql_coverage_statements('pl_f1'); select plpgsql_coverage_branches('pl_f1'); -- the coverage functions accept the oid of the function too select plpgsql_coverage_statements('pl_f1(int)'::regprocedure); select plpgsql_coverage_branches('pl_f1(int)'::regprocedure); -- the statistics of the second execution are merged with the first ones -- and both branches of the IF statement are executed now select pl_f1(5); select stmtid, parent_stmtid, exec_stmts, stmtname from plpgsql_profiler_function_statements_tb('pl_f1'); select plpgsql_coverage_statements('pl_f1'); select plpgsql_coverage_branches('pl_f1'); -- a function without any conditional statement has no branches, and the -- branch coverage of such function is 1 create function pl_f2() returns void as $$ begin insert into pl_t1 values(1, 2); end; $$ language plpgsql; select pl_f2(); select plpgsql_coverage_statements('pl_f2'); select plpgsql_coverage_branches('pl_f2'); -- the statistics of an aborted execution are collected too; the -- anonymous block is profiled as well, but it is not listed by -- plpgsql_profiler_functions_all(), because it has no entry in pg_proc create function pl_f3() returns void as $$ begin raise exception 'pl_f3 failed'; end; $$ language plpgsql; do $$ begin perform pl_f3(); exception when others then raise notice 'catched: %', sqlerrm; end; $$; select funcoid, exec_count, exec_stmts_err from plpgsql_profiler_functions_all() order by funcoid::text; -- the statistics of one function can be removed select plpgsql_profiler_reset('pl_f1(int)'); select stmtid, parent_stmtid, exec_stmts, stmtname from plpgsql_profiler_function_statements_tb('pl_f1'); select funcoid, exec_count, exec_stmts_err from plpgsql_profiler_functions_all() order by funcoid::text; -- ... and the statistics of all functions too select plpgsql_profiler_reset_all(); select funcoid, exec_count, exec_stmts_err from plpgsql_profiler_functions_all() order by funcoid::text; -- the size of the statistics is limited by a configuration option; when -- the limit is reached, the statistics of the function are not stored -- and a warning is raised set plpgsql_check.max_stats_size to '64kB'; do $$ begin execute 'create function pl_big() returns void as $x$ begin ' || repeat('perform 1;', 2000) || 'end; $x$ language plpgsql'; end; $$; select pl_big(); -- the function level statistics are collected even in this case select funcoid, exec_count from plpgsql_profiler_functions_all() where funcoid = 'pl_big()'::regprocedure; select stmtid, exec_stmts from plpgsql_profiler_function_statements_tb('pl_big') where stmtid = 1; drop function pl_big(); drop function pl_f3(); drop function pl_f2(); drop function pl_f1(int); drop table pl_t1; -- CASE does not have the hypothetical ELSE branch used for IF. create function pl_case(n int) returns int as $$ begin case n when 1 then return 10; when 2 then return 20; end case; end; $$ language plpgsql; create function pl_case_else(n int) returns int as $$ begin case n when 1 then return 10; when 2 then return 20; else return 0; end case; end; $$ language plpgsql; select pl_case(1), pl_case(2); select pl_case_else(1), pl_case_else(2), pl_case_else(0); select plpgsql_coverage_branches('pl_case'), plpgsql_coverage_branches('pl_case_else'); drop function pl_case(int); drop function pl_case_else(int); -- Row counts belong to the SQL/fetch/return statements, not to subsequent -- assignments or enclosing control statements. create table pl_row_data(a int); create function pl_rows() returns setof int as $$ declare n int; c refcursor; begin insert into pl_row_data values (1), (2), (3); n := 99; if n > 0 then perform * from pl_row_data; end if; update pl_row_data set a = a + 10 where a < 3; delete from pl_row_data where a = 3; execute 'select * from pl_row_data'; select count(*) into n from pl_row_data; open c for select a from pl_row_data order by a; fetch c into n; move forward 1 from c; fetch c into n; close c; return next n; return query select a from pl_row_data order by a; return query execute 'select 7'; return; end; $$ language plpgsql; select count(*) from pl_rows(); select stmtid, stmtname, processed_rows from plpgsql_profiler_function_statements_tb('pl_rows'); select sum(processed_rows[1]) as processed_rows from plpgsql_profiler_function_tb('pl_rows'); drop function pl_rows(); drop table pl_row_data; -- Error counts are a subset of execution counts, not additional executions. create function pl_timing_error() returns int as $$ begin perform pg_sleep(0.01); raise exception 'controlled timing error'; end; $$ language plpgsql; create function pl_timing_catch() returns void as $$ begin perform pl_timing_error(); exception when raise_exception then null; end; $$ language plpgsql; select pl_timing_catch(); select count(*) = 1 and bool_and(exec_stmts = 1 and exec_stmts_err = 1 and total_time > 0 and avg_time = total_time) as correct_average from plpgsql_profiler_function_statements_tb('pl_timing_catch') where stmtname = 'PERFORM'; select count(*) = 1 and bool_and(exec_stmts[1] = 1 and exec_stmts_err[1] = 1 and total_time[1] > 0 and avg_time[1] = total_time[1]) as correct_average from plpgsql_profiler_function_tb('pl_timing_catch') where source like '%perform pl_timing_error%'; drop function pl_timing_catch(); drop function pl_timing_error(); -- The types of the parameters of EXECUTE ... USING, which are cached for -- the computation of the query identifier, have to stay valid when all -- statistics are reset while the function is running. Else the types -- cached for the first EXECUTE would be overwritten by the types of the -- second one, and the first query would be analyzed with wrong types. -- The cached types are used again only while the query identifier is -- unknown, so this needs a server that does not compute query identifiers -- (the default). The next test does not depend on it. create function pl_reset_using() returns text as $$ declare r int; t text; begin perform plpgsql_profiler_reset_all(); for i in 1..2 loop execute 'select $1 + 1' into r using i; perform plpgsql_profiler_reset_all(); execute 'select $1 || $2' into t using 'a'::text, 'b'::text; end loop; return t || r; end; $$ language plpgsql; select pl_reset_using(); drop function pl_reset_using(); -- The statistics can be reset even while the profiler computes the query -- identifier, because it evaluates the expression of the query string, -- after the types of the parameters were cached. create function pl_reset_in_expr() returns text as $$ begin perform plpgsql_profiler_reset_all(); return '1'; end; $$ language plpgsql stable; create function pl_reset_using_expr() returns int as $$ declare r int; begin execute 'select $1 + ' || pl_reset_in_expr() into r using 41; return r; end; $$ language plpgsql; select pl_reset_using_expr(); drop function pl_reset_using_expr(); drop function pl_reset_in_expr(); select plpgsql_check_profiler(false); select plpgsql_profiler_reset_all(); set plpgsql_check.max_stats_size to default; -- Condition errors enter neither branch; errors in a body do enter it. set plpgsql_check.profiler = on; create function pl_condition(n int) returns int as $$ begin if 1 / n > 0 then return 1; end if; return 0; end; $$ language plpgsql; create function pl_body_error(n int) returns int as $$ begin if n > 0 then perform 1 / (n - 1); return 1; end if; return 0; end; $$ language plpgsql; create function pl_elsif_error(n int) returns int as $$ begin if n = 2 then return 2; elsif 1 / n > 0 then return 1; end if; return 0; end; $$ language plpgsql; create function pl_caught_condition(n int) returns int as $$ begin if 1 / n > 0 then return 1; end if; return 0; exception when division_by_zero then return -1; end; $$ language plpgsql; create function pl_coverage_calls() returns void as $$ begin begin perform pl_condition(0); exception when division_by_zero then null; end; begin perform pl_body_error(1); exception when division_by_zero then null; end; perform pl_body_error(0); begin perform pl_elsif_error(0); exception when division_by_zero then null; end; perform pl_caught_condition(0); end; $$ language plpgsql; select pl_coverage_calls(); select plpgsql_coverage_branches('pl_condition') = 0 as condition_ok, plpgsql_coverage_branches('pl_body_error') = 1 as body_ok, plpgsql_coverage_branches('pl_elsif_error') = 0 as elsif_ok, plpgsql_coverage_branches('pl_caught_condition') = 0.5 as caught_ok; select pl_condition(1), pl_condition(-1), pl_elsif_error(2), pl_elsif_error(1), pl_elsif_error(-1); select plpgsql_coverage_branches('pl_condition') = 1 as complete_if, plpgsql_coverage_branches('pl_elsif_error') = 1 as complete_elsif; select plpgsql_profiler_reset_all(); -- Exercise both shared merge paths when the library is preloaded. set plpgsql_check.use_shared_stats_when_it_possible = on; set plpgsql_check.use_lxcache = off; select pl_coverage_calls(); select plpgsql_coverage_branches('pl_condition') = 0 as condition_ok, plpgsql_coverage_branches('pl_body_error') = 1 as body_ok, plpgsql_coverage_branches('pl_elsif_error') = 0 as elsif_ok, plpgsql_coverage_branches('pl_caught_condition') = 0.5 as caught_ok; select plpgsql_profiler_reset_all(); set plpgsql_check.use_lxcache = on; select pl_coverage_calls(); select plpgsql_coverage_branches('pl_condition') = 0 as condition_ok, plpgsql_coverage_branches('pl_body_error') = 1 as body_ok, plpgsql_coverage_branches('pl_elsif_error') = 0 as elsif_ok, plpgsql_coverage_branches('pl_caught_condition') = 0.5 as caught_ok; select pl_condition(1), pl_condition(-1), pl_elsif_error(2), pl_elsif_error(1), pl_elsif_error(-1); select plpgsql_coverage_branches('pl_condition') = 1 as complete_if, plpgsql_coverage_branches('pl_elsif_error') = 1 as complete_elsif; set plpgsql_check.profiler = off; select plpgsql_profiler_reset_all(); drop function pl_condition(int); drop function pl_body_error(int); drop function pl_elsif_error(int); drop function pl_caught_condition(int); drop function pl_coverage_calls(); set plpgsql_check.use_lxcache = default; set plpgsql_check.use_shared_stats_when_it_possible to default; okbob-plpgsql_check-800db79/sql/plpgsql_check_profiler_queryid.sql000066400000000000000000000107071526744060500256030ustar00rootroot00000000000000set client_min_messages to warning; create extension if not exists plpgsql_check; set client_min_messages to notice; -- -- Tests of the collection of the query identifiers by the profiler. -- -- The profiler remembers the identifier of the query executed by a -- statement. A fake hook assigns the command type as its identifier so -- the expected output does not depend on PostgreSQL's query jumbling. -- set plpgsql_check.use_shared_stats_when_it_possible to off; set plpgsql_check.profiler to on; select plpgsql_profiler_install_fake_queryid_hook(); create table pq_t1(a int, b int); -- the queryid of a static query is taken from the cached plan of the -- expression. Dynamic SQL has no retained plan, so its identifier is -- unavailable rather than reconstructed by evaluating application code. create function pq_f1() returns void as $$ declare n int = 1; c int; begin insert into pq_t1 values(1, 2); update pq_t1 set a = 10; select count(*) into c from pq_t1; delete from pq_t1; execute 'insert into pq_t1 values($1, $2)' using n, n + 1; execute 'update pq_t1 set a = $1' using n; execute 'delete from pq_t1'; -- an empty dynamic query and a dynamic query with more than one -- statement have no identifier execute ''; execute 'select 1; select 2'; end; $$ language plpgsql; select pq_f1(); select queryids, lineno, stmt_lineno, exec_stmts, source from plpgsql_profiler_function_tb('pq_f1'); select plpgsql_profiler_reset_all(); set plpgsql_check.profiler_show_dynquery_query_id to on; select pq_f1(); select queryids, lineno, stmt_lineno, exec_stmts, source from plpgsql_profiler_function_tb('pq_f1'); select plpgsql_profiler_reset_all(); set plpgsql_check.profiler_show_dynquery_query_id to off; -- Profiling must not re-evaluate SQL text or USING expressions. create function pq_f2() returns void as $$ declare r pq_t1; begin r := (1, 2); execute 'select $1, $2' using r.*; end; $$ language plpgsql; do $$ begin perform pq_f2(); exception when others then raise notice 'catched: %', sqlerrm; end; $$; select queryids, lineno, stmt_lineno, exec_stmts, source from plpgsql_profiler_function_tb('pq_f2'); create function pq_into() returns text as $$ declare sql_text text := 'SELECT 42'; begin execute sql_text into sql_text; return sql_text; end; $$ language plpgsql; create function pq_loop() returns int as $$ declare sql_text text := 'SELECT 42'; n int; begin for n in execute sql_text loop sql_text := 'not sql'; end loop; return n; end; $$ language plpgsql; create function pq_open() returns int as $$ declare c refcursor; n int; begin open c for execute coalesce(c::text, 'SELECT 42'); fetch c into n; close c; return n; end; $$ language plpgsql; create function pq_return() returns setof int as $$ begin return query execute case when found then 'not sql' else 'SELECT 42' end; end; $$ language plpgsql; select pq_into(), pq_loop(), pq_open(); select * from pq_return(); select stmtname, queryid, exec_stmts from plpgsql_profiler_function_statements_tb('pq_into') where stmtname = 'EXECUTE'; drop function pq_into(); drop function pq_loop(); drop function pq_open(); drop function pq_return(); select plpgsql_profiler_remove_fake_queryid_hook(); -- without a provider of the identifiers no statement has one create function pq_f3() returns void as $$ begin insert into pq_t1 values(1, 2); execute 'delete from pq_t1'; end; $$ language plpgsql; select pq_f3(); select queryids, lineno, stmt_lineno, exec_stmts, source from plpgsql_profiler_function_tb('pq_f3'); select plpgsql_profiler_install_fake_queryid_hook(); -- Skipping a statement must retain its last observed query identifier. create function pq_branch(take_branch boolean) returns void as $$ begin if take_branch then perform 42; end if; end; $$ language plpgsql; select pq_branch(true); select queryid, exec_stmts from plpgsql_profiler_function_statements_tb('pq_branch') where stmtname = 'PERFORM'; select pq_branch(false); select queryid, exec_stmts from plpgsql_profiler_function_statements_tb('pq_branch') where stmtname = 'PERFORM'; select queryids, exec_stmts from plpgsql_profiler_function_tb('pq_branch') where source like '%perform 42%'; select plpgsql_profiler_reset_all(); set plpgsql_check.profiler to off; select plpgsql_profiler_remove_fake_queryid_hook(); drop function pq_f3(); drop function pq_f2(); drop function pq_f1(); drop table pq_t1; drop function pq_branch(boolean); set plpgsql_check.use_shared_stats_when_it_possible to default; okbob-plpgsql_check-800db79/sql/plpgsql_check_report.sql000066400000000000000000000125771526744060500235410ustar00rootroot00000000000000set client_min_messages to warning; create extension if not exists plpgsql_check; set client_min_messages to notice; -- -- reports about unused, never read and unmodified variables and parameters -- -- a declared variable that is not used at all, a variable that is only -- written and a variable that is only read create function rp_f1() returns int as $$ declare unused_var int; write_only int; read_only int := 10; begin write_only := 1; return read_only; end; $$ language plpgsql; -- the "never read" report belongs to the extra warnings, the "unused" -- report is printed always select * from plpgsql_check_function('rp_f1', extra_warnings => false); select * from plpgsql_check_function('rp_f1', extra_warnings => true); -- the same for the parameters of a function - "a" is not used at all, -- "b" is only written and "c" is read create function rp_f2(a int, b int, c int) returns int as $$ begin b := 1; return c; end; $$ language plpgsql; select * from plpgsql_check_function('rp_f2', extra_warnings => true); -- INOUT parameter of a procedure that is only written is expected, because -- a procedure cannot have a plain OUT parameter in older releases create procedure rp_p1(inout a int, inout b int) as $$ begin a := 1; b := b + 1; end; $$ language plpgsql; select * from plpgsql_check_function('rp_p1', extra_warnings => true); -- single OUT variable that is not modified create function rp_f4(out a int) as $$ begin raise notice 'nothing'; end; $$ language plpgsql; select * from plpgsql_check_function('rp_f4', extra_warnings => true); -- more OUT variables, one of them is composite, so the result of the -- function cannot be assigned by a simple expression create type rp_t1 as (x int, y int); create function rp_f5(out a int, out b rp_t1, out c rp_t1) as $$ begin b := (1,2)::rp_t1; end; $$ language plpgsql; select * from plpgsql_check_function('rp_f5', extra_warnings => true); -- when the result is returned by a dynamic query whose text is not known, -- the OUT variables are reported as "maybe unmodified" only create function rp_f6(p text, out a int, out b int) returns setof record as $$ begin return query execute p; end; $$ language plpgsql; select * from plpgsql_check_function('rp_f6', extra_warnings => true); -- the same with a single OUT variable create function rp_f6b(p text, out a int) returns setof int as $$ begin return query execute p; end; $$ language plpgsql; select * from plpgsql_check_function('rp_f6b', extra_warnings => true); -- a dynamic query with a known text is checked like a static query create function rp_f6c(out a int, out b int) returns setof record as $$ begin return query execute 'select 1, 2'; end; $$ language plpgsql; select * from plpgsql_check_function('rp_f6c', extra_warnings => true); -- RETURN QUERY (without EXECUTE) sets the OUT variables, so nothing is -- reported create function rp_f7(out a int, out b int) returns setof record as $$ begin return query select 1, 2; end; $$ language plpgsql; select * from plpgsql_check_function('rp_f7', extra_warnings => true); -- a variable used only as a field of a record or of a row is used create function rp_f8() returns int as $$ declare r record; x int; y int; begin select 1 as a, 2 as b into r; select 1, 2 into x, y; return r.a + x + y; end; $$ language plpgsql; select * from plpgsql_check_function('rp_f8', extra_warnings => true); -- -- reports about too high volatility -- -- the body is immutable, but the function is declared as volatile create function rp_f9(a int) returns int as $$ begin return a + 1; end; $$ language plpgsql volatile; select * from plpgsql_check_function('rp_f9', performance_warnings => true); -- the same function declared as stable create function rp_f10(a int) returns int as $$ begin return a + 1; end; $$ language plpgsql stable; select * from plpgsql_check_function('rp_f10', performance_warnings => true); -- reading a table makes the function stable create table rp_tab1(a int); create function rp_f11() returns int as $$ begin return (select count(*) from rp_tab1); end; $$ language plpgsql volatile; select * from plpgsql_check_function('rp_f11', performance_warnings => true); -- a procedure returns void, so the stable volatility is not suggested create procedure rp_p2() as $$ begin perform count(*) from rp_tab1; end; $$ language plpgsql; select * from plpgsql_check_function('rp_p2', performance_warnings => true); -- when the function uses a dynamic SQL, the volatility cannot be safely -- determined, so the report has an additional detail create function rp_f12(a int) returns int as $$ declare r int; begin execute 'select 1' into r; return a + r; end; $$ language plpgsql volatile; select * from plpgsql_check_function('rp_f12', performance_warnings => true); -- an immutable function is never reported create function rp_f13(a int) returns int as $$ begin return a + 1; end; $$ language plpgsql immutable; select * from plpgsql_check_function('rp_f13', performance_warnings => true); drop function rp_f1(); drop function rp_f2(int, int, int); drop procedure rp_p1(int, int); drop function rp_f4(); drop function rp_f5(); drop function rp_f6(text); drop function rp_f6b(text); drop function rp_f6c(); drop function rp_f7(); drop function rp_f8(); drop function rp_f9(int); drop function rp_f10(int); drop function rp_f11(); drop procedure rp_p2(); drop function rp_f12(int); drop function rp_f13(int); drop table rp_tab1; drop type rp_t1; okbob-plpgsql_check-800db79/sql/plpgsql_check_stmt_walker.sql000066400000000000000000000160231526744060500245500ustar00rootroot00000000000000-- -- regress tests of generic PL/pgSQL statement walker (src/generic_stmt_walker.c) -- -- The walker itself has no SQL interface. It is used by the profiler -- (statement and branch coverage, per statement statistics, queryid -- retrieval), so the profiler API is used here as a probe. -- load 'plpgsql_check'; set client_min_messages to warning; create extension if not exists plpgsql_check; set client_min_messages to notice; create table swt_tab(a int, b int); insert into swt_tab values(1,1),(2,2),(3,3); create procedure swt_proc(a int) as $$ begin end; $$ language plpgsql; -- -- A function using every statement type known to the walker. The -- statements are executed, so the profiler collects statistics for all -- of them. -- create function swt_all_stmts(par int) returns int as $$ declare i int; j int; r record; arr int[] = array[1, 2, 3]; c refcursor; cc cursor(p int) for select a from swt_tab where a = p; res int = 0; begin -- IF with two ELSIF branches and an ELSE branch if par = 1 then res := res + 1; elsif par = 2 then res := res + 2; elsif par = 3 then res := res + 3; else res := res + 4; end if; -- IF without ELSE branch (hypothetical else branch of coverage) if par > 100 then res := res + 1; end if; -- simple CASE - the tested expression is the statement's expression case par when 1 then res := res + 1; when 2 then res := res + 2; else res := res + 3; end case; -- searched CASE - has no tested expression case when par > 0 then res := res + 1; else res := res + 2; end case; -- unconditional LOOP terminated by conditional EXIT i := 0; loop i := i + 1; exit when i >= 2; end loop; -- WHILE loop while i < 4 loop i := i + 1; end loop; -- FOR over integer range, with an explicit BY step for j in reverse 10 .. 1 by 3 loop res := res + 1; end loop; -- FOR over a static query for r in select a, b from swt_tab order by a loop res := res + r.a; end loop; -- FOR over a bound cursor (the argument list is the statement's query) for r in cc(1) loop res := res + r.a; end loop; -- FOR over a dynamic query with USING parameters for r in execute 'select a from swt_tab where a > $1' using 0 loop res := res + r.a; end loop; -- FOREACH over an array foreach i in array arr loop res := res + i; end loop; -- OPEN of a bound cursor, FETCH, CLOSE open cc(2); fetch cc into r; close cc; -- OPEN of an unbound cursor for a static query open c for select a from swt_tab; fetch c into i; close c; -- OPEN of an unbound cursor for a dynamic query with USING parameters open c for execute 'select a from swt_tab where a > $1' using 0; fetch c into i; close c; -- PERFORM perform count(*) from swt_tab; -- CALL call swt_proc(1); -- plain SQL statements insert into swt_tab values(10, 10); update swt_tab set b = 11 where a = 10; delete from swt_tab where a = 10; -- dynamic SQL with USING parameters execute 'delete from swt_tab where a = $1' using -1; -- GET DIAGNOSTICS has no interesting substructure get diagnostics i = row_count; -- ASSERT with a message expression assert res is not null, 'res is null for ' || par; -- RAISE with format parameters and with options, handled by a nested -- block with an exception handler begin raise exception 'raised for %', par using errcode = 'division_by_zero', hint = 'no hint ' || par; exception when division_by_zero then res := res + 1; end; return res; end; $$ language plpgsql; -- -- RETURN NEXT and both flavours of RETURN QUERY live in a set returning -- function. -- create function swt_setof() returns setof int as $$ declare i int; begin for i in 1 .. 2 loop return next i; end loop; return query select a from swt_tab order by a limit 1; return query execute 'select b from swt_tab where b = $1' using 2; return; end; $$ language plpgsql; -- -- COMMIT and ROLLBACK are allowed in procedures only. -- create procedure swt_txn() as $$ begin commit; rollback; end; $$ language plpgsql; set plpgsql_check.profiler to on; select swt_all_stmts(1); select swt_all_stmts(2); select swt_all_stmts(10); select * from swt_setof(); call swt_txn(); set plpgsql_check.profiler to off; -- the walker is used to assign the statement statistics to the AST select stmtid, parent_stmtid, block_num, lineno, exec_stmts, stmtname from plpgsql_profiler_function_statements_tb('swt_all_stmts'); select stmtid, parent_stmtid, block_num, lineno, exec_stmts, stmtname from plpgsql_profiler_function_statements_tb('swt_setof'); select stmtid, parent_stmtid, block_num, lineno, exec_stmts, stmtname from plpgsql_profiler_function_statements_tb('swt_txn'); -- every loop kind has to be recognized as a branch, and the walker has -- to be able to return the body of every loop kind select plpgsql_coverage_statements('swt_all_stmts'); select plpgsql_coverage_branches('swt_all_stmts'); select plpgsql_coverage_statements('swt_setof'); select plpgsql_coverage_branches('swt_setof'); select plpgsql_coverage_statements('swt_txn'); select plpgsql_coverage_branches('swt_txn'); -- -- The expression returned by the walker for a statement is used for the -- queryid retrieval. The fake queryid hook stores the command type of the -- query, so the result shows which statements carry a query. -- create function swt_queryid() returns void as $$ declare r record; c refcursor; cc cursor(p int) for select a from swt_tab where a = p; tabname text = 'swt_tab'; i int; begin insert into swt_tab values(20, 20); update swt_tab set b = 21 where a = 20; delete from swt_tab where a = 20; select count(*) into i from swt_tab; perform count(*) from swt_tab; for r in select a from swt_tab order by a loop null; end loop; for r in cc(1) loop null; end loop; for r in execute 'select a from ' || tabname loop null; end loop; open c for select a from swt_tab; fetch c into i; close c; open c for execute 'select a from ' || tabname; fetch c into i; close c; execute 'delete from ' || tabname || ' where a = $1' using -1; end; $$ language plpgsql; create function swt_queryid_setof() returns setof int as $$ declare tabname text = 'swt_tab'; begin return query select a from swt_tab order by a; return query execute 'select b from ' || tabname || ' order by b'; end; $$ language plpgsql; set plpgsql_check.profiler to on; select plpgsql_profiler_reset_all(); select plpgsql_profiler_install_fake_queryid_hook(); select swt_queryid(); select * from swt_queryid_setof(); select plpgsql_profiler_remove_fake_queryid_hook(); select lineno, stmt_lineno, queryids, exec_stmts, source from plpgsql_profiler_function_tb('swt_queryid'); select lineno, stmt_lineno, queryids, exec_stmts, source from plpgsql_profiler_function_tb('swt_queryid_setof'); set plpgsql_check.profiler to off; drop function swt_queryid_setof(); drop function swt_queryid(); drop procedure swt_txn(); drop function swt_setof(); drop function swt_all_stmts(int); drop procedure swt_proc(int); drop table swt_tab; okbob-plpgsql_check-800db79/sql/plpgsql_check_stmtwalk.sql000066400000000000000000000461401526744060500240650ustar00rootroot00000000000000set client_min_messages to warning; create extension if not exists plpgsql_check; set client_min_messages to notice; -- -- WHILE statement -- -- the condition is checked like a scalar boolean expression create function sw_f1(n int) returns int as $$ declare i int := 0; begin while i < n loop i := i + 1; end loop; return i; end; $$ language plpgsql; select * from plpgsql_check_function('sw_f1'); -- a non boolean condition is reported create function sw_f2(n int) returns int as $$ declare i int := 0; begin while i loop i := i + 1; end loop; return i; end; $$ language plpgsql; select * from plpgsql_check_function('sw_f2'); -- the body of a loop is not necessarily executed, so a RETURN inside -- the loop does not close the execution path create function sw_f3(n int) returns int as $$ begin while n > 0 loop return n; end loop; end; $$ language plpgsql; select * from plpgsql_check_function('sw_f3'); -- an unconditional loop is closed by the RETURN inside its body create function sw_f4(n int) returns int as $$ begin loop return n; end loop; end; $$ language plpgsql; select * from plpgsql_check_function('sw_f4'); -- the STEP expression of a numeric FOR loop is checked too create function sw_f4b(n int) returns int as $$ declare i int := 0; begin for j in 1..n by 2 loop i := i + j; end loop; return i; end; $$ language plpgsql; select * from plpgsql_check_function('sw_f4b'); -- -- names of the local variables -- -- a reserved keyword used as a variable name, a local variable shadowing -- a parameter and a local variable shadowing an outer local variable create function sw_f4c(shadowed_par int) returns int as $$ declare "table" int := 1; shadowed_var int := 2; begin declare shadowed_par int := 3; shadowed_var int := 4; begin return "table" + shadowed_par + shadowed_var; end; end; $$ language plpgsql; select * from plpgsql_check_function('sw_f4c'); -- -- CASE statement -- -- when the CASE has no ELSE part, the execution path is closed only -- possibly, although every WHEN part returns create function sw_f5(n int) returns int as $$ begin case n when 1 then return 10; when 2 then return 20; end case; end; $$ language plpgsql; select * from plpgsql_check_function('sw_f5'); -- with the ELSE part the path is closed create function sw_f6(n int) returns int as $$ begin case n when 1 then return 10; else return 20; end case; end; $$ language plpgsql; select * from plpgsql_check_function('sw_f6'); -- -- IF statement with ELSIF parts -- -- every ELSIF condition is checked separately create function sw_f7(n int) returns int as $$ begin if n = 1 then return 10; elsif n then return 20; elsif n = 3 then return 30; else return 40; end if; end; $$ language plpgsql; select * from plpgsql_check_function('sw_f7'); -- -- EXIT and CONTINUE statements -- -- EXIT can leave a labeled statement block, CONTINUE needs a labeled loop create function sw_f8(n int) returns int as $$ declare i int := 0; begin <> begin <> for j in 1..n loop if j = 2 then continue outer_loop; end if; if j = 3 then exit outer_block; end if; i := i + j; end loop; end; return i; end; $$ language plpgsql; select * from plpgsql_check_function('sw_f8'); -- -- RAISE statement -- -- a condition name closes the execution path by the related exception create function sw_f9() returns int as $$ begin raise division_by_zero; end; $$ language plpgsql; select * from plpgsql_check_function('sw_f9'); -- the errcode option is preferred against the condition name create function sw_f10() returns int as $$ begin raise division_by_zero using errcode = 'unique_violation'; end; $$ language plpgsql; select * from plpgsql_check_function('sw_f10'); -- an errcode that cannot be evaluated at the check time create function sw_f11(c text) returns int as $$ begin raise exception 'broken' using errcode = c; end; $$ language plpgsql; select * from plpgsql_check_function('sw_f11'); -- the doubled percent is not a placeholder, so the message needs no -- parameter for it create function sw_f12() returns int as $$ begin raise notice '100%% done, value %', 1; return 1; end; $$ language plpgsql; select * from plpgsql_check_function('sw_f12'); -- -- exception handlers -- -- the raised exception is matched by the name of its category create function sw_f13() returns int as $$ begin begin raise division_by_zero; exception when data_exception then return -1; end; return 0; end; $$ language plpgsql; select * from plpgsql_check_function('sw_f13', extra_warnings => true); -- an exception that cannot be raised by the protected statements is -- reported as an unused handler create function sw_f14() returns int as $$ begin begin raise division_by_zero; exception when unique_violation then return -1; end; return 0; end; $$ language plpgsql; select * from plpgsql_check_function('sw_f14', extra_warnings => true); -- a block whose body is closed by an exception and whose handler raises -- another exception is closed by the exception of the handler create function sw_f14b() returns int as $$ begin begin raise division_by_zero; exception when division_by_zero then raise unique_violation; end; end; $$ language plpgsql; select * from plpgsql_check_function('sw_f14b', extra_warnings => true); -- when the handlers only re-raise the caught exception, the block is -- closed by the exceptions raised by the protected body create function sw_f14c(n int) returns int as $$ begin begin if n = 1 then raise division_by_zero; else raise unique_violation; end if; exception when division_by_zero then raise; when unique_violation then raise; end; end; $$ language plpgsql; select * from plpgsql_check_function('sw_f14c', extra_warnings => true); -- -- RETURN statement -- -- a function returning refcursor should return a refcursor variable create function sw_f14d() returns refcursor as $$ declare c text := 'some_cursor'; begin return c; end; $$ language plpgsql; select * from plpgsql_check_function('sw_f14d', compatibility_warnings => true); create function sw_f14e() returns refcursor as $$ declare c refcursor; begin open c for select 1; return c; end; $$ language plpgsql; select * from plpgsql_check_function('sw_f14e', compatibility_warnings => true); -- -- RETURN NEXT statement -- -- the returned value is a single OUT variable create function sw_f15(out a int) returns setof int as $$ begin a := 1; return next; end; $$ language plpgsql; select * from plpgsql_check_function('sw_f15'); -- more OUT variables are collected into a row create function sw_f16(out a int, out b int) returns setof record as $$ begin a := 1; b := 2; return next; end; $$ language plpgsql; select * from plpgsql_check_function('sw_f16'); -- the types of the row fields have to match the result type create table sw_tab1(a int, b int); create function sw_f17(out a int, out b text) returns setof record as $$ begin a := 1; b := 'x'; return next; end; $$ language plpgsql; select * from plpgsql_check_function('sw_f17'); -- a record variable is returned create function sw_f18() returns setof sw_tab1 as $$ declare r sw_tab1; begin r := (1,2); return next r; end; $$ language plpgsql; select * from plpgsql_check_function('sw_f18'); -- -- transaction control statements -- -- COMMIT and ROLLBACK are allowed in a procedure only create function sw_f19() returns void as $$ begin commit; end; $$ language plpgsql; select * from plpgsql_check_function('sw_f19'); create procedure sw_p1() as $$ begin commit; rollback; end; $$ language plpgsql; select * from plpgsql_check_function('sw_p1'); -- inside a block with an exception handler a subtransaction is active, -- so neither COMMIT nor ROLLBACK can be used create procedure sw_p2() as $$ begin begin commit; exception when others then null; end; end; $$ language plpgsql; select * from plpgsql_check_function('sw_p2'); create procedure sw_p3() as $$ begin begin rollback; exception when others then null; end; end; $$ language plpgsql; select * from plpgsql_check_function('sw_p3'); -- -- dynamic SQL -- -- OPEN FOR EXECUTE is checked like the other dynamic statements create function sw_f20(p text) returns int as $$ declare c refcursor; r int; begin open c for execute p using 1; fetch c into r; close c; return r; end; $$ language plpgsql; select * from plpgsql_check_function('sw_f20'); -- when the query text is not known, the result of the dynamic SQL cannot -- be determined, and the record variable stays without a descriptor create function sw_f21(p text) returns int as $$ declare r record; begin execute p into r; return r.x; end; $$ language plpgsql; select * from plpgsql_check_function('sw_f21'); -- the expression of EXECUTE is built by a function which does not -- sanitize its argument, and the position of the unsafe value is unknown create function sw_f21b(p text) returns void as $$ begin execute upper(p); end; $$ language plpgsql; select * from plpgsql_check_function('sw_f21b', security_warnings => true); -- a record variable with a declared type is known, so no warning is raised create function sw_f22(p text) returns int as $$ declare r sw_tab1; begin execute p into r; return r.a; end; $$ language plpgsql; select * from plpgsql_check_function('sw_f22'); drop function sw_f1(int); drop function sw_f2(int); drop function sw_f3(int); drop function sw_f4(int); drop function sw_f4b(int); drop function sw_f4c(int); drop function sw_f5(int); drop function sw_f6(int); drop function sw_f7(int); drop function sw_f8(int); drop function sw_f9(); drop function sw_f10(); drop function sw_f11(text); drop function sw_f12(); drop function sw_f13(); drop function sw_f14(); drop function sw_f14b(); drop function sw_f14c(int); drop function sw_f14d(); drop function sw_f14e(); drop function sw_f15(); drop function sw_f16(); drop function sw_f17(); drop function sw_f18(); drop function sw_f19(); drop procedure sw_p1(); drop procedure sw_p2(); drop procedure sw_p3(); drop function sw_f20(text); drop function sw_f21(text); drop function sw_f21b(text); drop function sw_f22(text); drop table sw_tab1; -- Bare RAISE needs an active handler, including through nested blocks create function sw_bare_raise() returns int as $$ begin raise; end; $$ language plpgsql; select * from plpgsql_check_function('sw_bare_raise'); create function sw_handler_raise() returns void as $$ begin raise division_by_zero; exception when division_by_zero then begin raise; end; end; $$ language plpgsql; select * from plpgsql_check_function('sw_handler_raise'); drop function sw_bare_raise(); drop function sw_handler_raise(); -- MOVE has no result target, even when the cursor query has several columns create function sw_move(distance int) returns void as $$ declare c cursor for select 1 as a, 2 as b; begin open c; move forward distance from c; close c; end; $$ language plpgsql; select * from plpgsql_check_function('sw_move(int)'); drop function sw_move(int); -- Check before execution initializes the hidden CASE variable's type create type sw_single_field as (a int); create function sw_case_single(v sw_single_field) returns int as $$ begin case v when row(1)::sw_single_field then return 1; else return 2; end case; end; $$ language plpgsql; select * from plpgsql_check_function('sw_case_single(sw_single_field)'); select sw_case_single(row(1)::sw_single_field) = 1 and sw_case_single(row(2)::sw_single_field) = 2 as valid; drop function sw_case_single(sw_single_field); drop type sw_single_field; -- Earlier exception paths must remain visible when later paths are merged create function sw_exception_union(p boolean) returns int as $$ begin begin if p then raise division_by_zero; else raise unique_violation; end if; exception when division_by_zero then null; when unique_violation then return 2; end; return 1; end; $$ language plpgsql; select * from plpgsql_check_function('sw_exception_union(boolean)'); drop function sw_exception_union(boolean); -- A first-handler rethrow keeps the caught SQLSTATE for outer handlers create function sw_nested_rethrow() returns int as $$ begin begin begin raise division_by_zero; exception when division_by_zero then raise; end; exception when division_by_zero then null; end; return 1; end; $$ language plpgsql; select * from plpgsql_check_function('sw_nested_rethrow()'); drop function sw_nested_rethrow(); -- Dead exits must not change which exception handler is reachable create function sw_dead_flow() returns int as $$ begin begin raise division_by_zero; raise unique_violation; exception when division_by_zero then return 1; when unique_violation then null; end; end; $$ language plpgsql; select sqlstate, message from plpgsql_check_function_tb('sw_dead_flow()'); -- Expressions in dead statements still need to be checked create function sw_dead_expression() returns int as $$ begin return 1; perform sw_missing_dead_column; end; $$ language plpgsql; select exists ( select from plpgsql_check_function_tb('sw_dead_expression()') where sqlstate = '42703') as checked; drop function sw_dead_flow(); drop function sw_dead_expression(); -- Pragmas in one branch or handler must not affect its siblings create function sw_pragma_siblings(p boolean) returns void as $$ begin if p then perform plpgsql_check_pragma('disable:check'); else perform sw_missing_else_column; end if; case when p then perform plpgsql_check_pragma('disable:check'); else perform sw_missing_case_column; end case; begin raise division_by_zero; exception when unique_violation then perform plpgsql_check_pragma('disable:check'); when division_by_zero then perform sw_missing_handler_column; end; end; $$ language plpgsql; select count(*) = 3 as sibling_checks from plpgsql_check_function_tb('sw_pragma_siblings(boolean)', fatal_errors => false) where sqlstate = '42703'; drop function sw_pragma_siblings(boolean); -- Check dynamic cursors before execution can prime record-field metadata create function sw_dynamic_cursor() returns int as $$ declare c refcursor; r record; begin open c for execute 'select 1 as a'; fetch c into r; close c; return r.a; end; $$ language plpgsql; create function sw_parameter_cursor(v int) returns int as $$ declare c refcursor; r record; begin open c for execute 'select $1::int as a' using v; fetch c into r; close c; return r.a; end; $$ language plpgsql; -- A later static OPEN supersedes the earlier dynamic query create function sw_cursor_reopen(v int) returns int as $$ declare c refcursor; r record; begin open c for execute 'select $1::int as a' using v; fetch c into r; close c; open c for select v as b; fetch c into r; close c; return r.b; end; $$ language plpgsql; -- FETCH into scalar targets also invalidates their previous traced constants create function sw_dynamic_scalar() returns void as $$ declare c refcursor; s text := 'select sw_missing_dynamic_column'; begin open c for execute $q$select 'select 1'::text$q$; fetch c into s; close c; execute s; end; $$ language plpgsql; select * from plpgsql_check_function('sw_dynamic_cursor()'); select * from plpgsql_check_function('sw_parameter_cursor(int)'); select * from plpgsql_check_function('sw_cursor_reopen(int)'); select * from plpgsql_check_function('sw_dynamic_scalar()'); select sw_dynamic_cursor() = 1 and sw_parameter_cursor(3) = 3 and sw_cursor_reopen(4) = 4 as valid; do $$ begin perform sw_dynamic_scalar(); end; $$; drop function sw_dynamic_cursor(); drop function sw_parameter_cursor(int); drop function sw_cursor_reopen(int); drop function sw_dynamic_scalar(); -- FETCH from a cursor that is not opened in the function (so its query -- is unknown) has to be checked without crash, also when there is no -- OPEN FOR EXECUTE in the function. The passive mode does the same check. create function sw_fetch_unbound(c refcursor) returns int as $$ declare v int; begin fetch c into v; return v; end; $$ language plpgsql; select * from plpgsql_check_function('sw_fetch_unbound(refcursor)'); set plpgsql_check.mode = 'every_start'; begin; declare sw_fetch_cur cursor for select 1; select sw_fetch_unbound('sw_fetch_cur'); commit; reset plpgsql_check.mode; drop function sw_fetch_unbound(refcursor); create function sw_exit_loop(p boolean) returns int as $$ begin loop if p then exit; end if; return 1; end loop; return 2; end; $$ language plpgsql; -- is ok select * from plpgsql_check_function('sw_exit_loop(boolean)'); drop function sw_exit_loop(boolean); create function sw_exit_loop(p boolean) returns int as $$ begin loop exit when p; return 1; end loop; return 2; end; $$ language plpgsql; -- is ok select * from plpgsql_check_function('sw_exit_loop'); drop function sw_exit_loop(boolean); create function sw_exit_nested(p boolean) returns int as $$ begin <> loop loop exit outer_loop when p; return 1; end loop; end loop; return 2; end; $$ language plpgsql; -- is ok select * from plpgsql_check_function('sw_exit_nested'); drop function sw_exit_nested(boolean); create function sw_exit_block(p boolean) returns int as $$ begin <> begin exit target when p; return 1; end; return 2; end; $$ language plpgsql; -- is ok select * from plpgsql_check_function('sw_exit_block'); drop function sw_exit_block(boolean); create function sw_exit_missing(p boolean) returns int as $$ begin loop exit when p; return 1; end loop; return 1; end; $$ language plpgsql; -- is ok select * from plpgsql_check_function('sw_exit_missing'); drop function sw_exit_missing(boolean); create function sw_exit_inner() returns int as $$ begin loop loop exit; end loop; return 1; end loop; return 2; end; $$ language plpgsql; -- should be warning select * from plpgsql_check_function('sw_exit_inner'); drop function sw_exit_inner(); create function sw_continue_path() returns int as $$ declare i int := 0; begin loop i := i + 1; continue when i = 1; return i; end loop; return 3; end; $$ language plpgsql; -- is ok select * from plpgsql_check_function('sw_continue_path'); drop function sw_continue_path(); create function sw_continue_outer() returns int as $$ begin <> for i in 1..2 loop loop continue outer_loop; end loop; end loop; return 1; end; $$ language plpgsql; -- is ok select * from plpgsql_check_function('sw_continue_outer'); drop function sw_continue_outer(); create function sw_dead_exit() returns int as $$ begin loop return 1; exit; end loop; return 2; end; $$ language plpgsql; -- should be warning select * from plpgsql_check_function('sw_dead_exit'); drop function sw_dead_exit(); create function sw_handler_exit() returns int as $$ begin loop begin raise division_by_zero; exception when unique_violation then exit; when division_by_zero then return 1; end; end loop; return 2; end; $$ language plpgsql; -- is ok select * from plpgsql_check_function('sw_handler_exit'); drop function sw_handler_exit(); okbob-plpgsql_check-800db79/sql/plpgsql_check_tablefunc.sql000066400000000000000000000157311526744060500241640ustar00rootroot00000000000000set client_min_messages to warning; create extension if not exists plpgsql_check; set client_min_messages to notice; -- -- Tests of the SQL callable entry points of plpgsql_check and of the -- validation of their arguments. -- -- Almost every option of the check functions is mandatory - it has a -- default value, but an explicit NULL is rejected. Every such check is -- executed here. -- create function tf_f1(a int) returns int as $$ begin return a; end; $$ language plpgsql; create table tf_t1(a int, b int); -- -- plpgsql_check_function -- select * from plpgsql_check_function(null::regprocedure); select * from plpgsql_check_function(null::text); select * from plpgsql_check_function('tf_f1', relid := null); select * from plpgsql_check_function('tf_f1', format := null); select * from plpgsql_check_function('tf_f1', fatal_errors := null); select * from plpgsql_check_function('tf_f1', other_warnings := null); select * from plpgsql_check_function('tf_f1', performance_warnings := null); select * from plpgsql_check_function('tf_f1', extra_warnings := null); select * from plpgsql_check_function('tf_f1', security_warnings := null); select * from plpgsql_check_function('tf_f1', compatibility_warnings := null); select * from plpgsql_check_function('tf_f1', anyelememttype := null); select * from plpgsql_check_function('tf_f1', anyenumtype := null); select * from plpgsql_check_function('tf_f1', anyrangetype := null); select * from plpgsql_check_function('tf_f1', anycompatibletype := null); select * from plpgsql_check_function('tf_f1', anycompatiblerangetype := null); select * from plpgsql_check_function('tf_f1', without_warnings := null); select * from plpgsql_check_function('tf_f1', all_warnings := null); select * from plpgsql_check_function('tf_f1', use_incomment_options := null); select * from plpgsql_check_function('tf_f1', incomment_options_usage_warning := null); select * from plpgsql_check_function('tf_f1', constant_tracing := null); -- the two shortcuts of the set of enabled warnings are exclusive select * from plpgsql_check_function('tf_f1', without_warnings := true, all_warnings := true); -- the names of the transition tables can be used only for a trigger -- function, which requires the relation select * from plpgsql_check_function('tf_f1', oldtable := 'o'); select * from plpgsql_check_function('tf_f1', newtable := 'n'); -- the list of the pragmas can contain a NULL, such entry is ignored select * from plpgsql_check_function('tf_f1', pragmas := array[null, 'echo:from a pragma']::text[]); -- -- plpgsql_check_function_tb -- select * from plpgsql_check_function_tb(null::regprocedure); select * from plpgsql_check_function_tb(null::text); select * from plpgsql_check_function_tb('tf_f1', relid := null); select * from plpgsql_check_function_tb('tf_f1', fatal_errors := null); select * from plpgsql_check_function_tb('tf_f1', other_warnings := null); select * from plpgsql_check_function_tb('tf_f1', performance_warnings := null); select * from plpgsql_check_function_tb('tf_f1', extra_warnings := null); select * from plpgsql_check_function_tb('tf_f1', security_warnings := null); select * from plpgsql_check_function_tb('tf_f1', compatibility_warnings := null); select * from plpgsql_check_function_tb('tf_f1', anyelememttype := null); select * from plpgsql_check_function_tb('tf_f1', anyenumtype := null); select * from plpgsql_check_function_tb('tf_f1', anyrangetype := null); select * from plpgsql_check_function_tb('tf_f1', anycompatibletype := null); select * from plpgsql_check_function_tb('tf_f1', anycompatiblerangetype := null); select * from plpgsql_check_function_tb('tf_f1', without_warnings := null); select * from plpgsql_check_function_tb('tf_f1', all_warnings := null); select * from plpgsql_check_function_tb('tf_f1', use_incomment_options := null); select * from plpgsql_check_function_tb('tf_f1', incomment_options_usage_warning := null); select * from plpgsql_check_function_tb('tf_f1', constant_tracing := null); select * from plpgsql_check_function_tb('tf_f1', without_warnings := true, all_warnings := true); select * from plpgsql_check_function_tb('tf_f1', oldtable := 'o'); select * from plpgsql_check_function_tb('tf_f1', newtable := 'n'); -- all_warnings enables every group of the warnings, without_warnings -- disables them; the difference is visible on a function with an unused -- variable create function tf_f2() returns void as $$ declare v int; begin v := 1; end; $$ language plpgsql; select * from plpgsql_check_function('tf_f2', all_warnings := true); select * from plpgsql_check_function('tf_f2', without_warnings := true); select level, message from plpgsql_check_function_tb('tf_f2', all_warnings := true); select level, message from plpgsql_check_function_tb('tf_f2', without_warnings := true); -- declaration locations must agree in tabular, JSON and XML output create function tf_declarations() returns void as $$ declare unused_value int; written_value int; begin written_value := 1; end; $$ language plpgsql; select lineno, statement, message from plpgsql_check_function_tb('tf_declarations'); select issue->'statement'->>'lineNumber' as lineno, issue->'statement'->>'text' as statement, issue->>'message' as message from plpgsql_check_function('tf_declarations', format => 'json') as r(doc), lateral jsonb_array_elements(doc::jsonb->'issues') as i(issue); select xpath('/Function/Issue/Stmt/@lineno', doc::xml) as linenos, xpath('/Function/Issue/Stmt/text()', doc::xml) as statements from plpgsql_check_function('tf_declarations', format => 'xml') as r(doc); drop function tf_declarations(); -- -- plpgsql_show_dependency_tb -- select * from plpgsql_show_dependency_tb(null::regprocedure); select * from plpgsql_show_dependency_tb(null::text); select * from plpgsql_show_dependency_tb('tf_f1', relid := null); select * from plpgsql_show_dependency_tb('tf_f1', anyelememttype := null); select * from plpgsql_show_dependency_tb('tf_f1', anyenumtype := null); select * from plpgsql_show_dependency_tb('tf_f1', anyrangetype := null); select * from plpgsql_show_dependency_tb('tf_f1', anycompatibletype := null); select * from plpgsql_show_dependency_tb('tf_f1', anycompatiblerangetype := null); create function tf_f3() returns void as $$ begin insert into tf_t1 values(1, 2); perform tf_f1(1); end; $$ language plpgsql; select type, schema, name, params from plpgsql_show_dependency_tb('tf_f3'); select type, schema, name, params from plpgsql_show_dependency_tb('tf_f3()'::regprocedure); -- -- plpgsql_make_pragma -- select * from plpgsql_make_pragma(null::regprocedure); select * from plpgsql_make_pragma('tf_f1(int)', relid := null); select * from plpgsql_make_pragma('tf_f1(int)', fatal_errors := null); create function tf_f4() returns void as $$ declare r record; begin create temp table tf_tmp as select 1 as a, 2 as b; select * from tf_tmp into r; raise notice '%', r.a; end; $$ language plpgsql; select * from plpgsql_make_pragma('tf_f4()'); drop function tf_f4(); drop function tf_f3(); drop function tf_f2(); drop function tf_f1(int); drop table tf_t1; okbob-plpgsql_check-800db79/sql/plpgsql_check_tracer.sql000066400000000000000000000133301526744060500234720ustar00rootroot00000000000000set client_min_messages to warning; create extension if not exists plpgsql_check; set client_min_messages to notice; -- -- Tests of the tracer. -- -- The tracer prints the entered and left functions and statements, the -- arguments of the expressions and the content of the variables. The -- test mode replaces the measured time and the oid of the function by -- constants, so the output is stable. -- -- the tracer can be activated by anybody, but it does nothing until a -- superuser enables it select plpgsql_check_tracer(true); set plpgsql_check.enable_tracer to on; set plpgsql_check.tracer_test_mode to true; -- the control function sets the state and the verbosity and reports both select plpgsql_check_tracer(false); select plpgsql_check_tracer(); select plpgsql_check_tracer(null, 'terse'); create table tr_t1(a int, b int); create function tr_f1(a int, t text) returns int as $$ declare b int; begin b := a * 2; if b > 100 then b := 100; end if; raise notice 'tr_f1: % %', b, t; return b; end; $$ language plpgsql; -- the terse verbosity prints one short line per function and statement select plpgsql_check_tracer(true, 'terse'); select tr_f1(10, 'terse'); -- the default verbosity prints the frame numbers and the context select plpgsql_check_tracer(true, 'default'); select tr_f1(10, 'default'); -- the verbose verbosity prints the arguments of the function and the -- arguments of every traced expression select plpgsql_check_tracer(true, 'verbose'); select tr_f1(10, 'verbose'); -- the number of the subtransactions can be printed too set plpgsql_check.tracer_show_nsubxids to on; select tr_f1(1, 'nsubxids'); set plpgsql_check.tracer_show_nsubxids to off; -- an IF statement with the ELSIF parts prints the condition of every part create function tr_f2(a int) returns text as $$ begin if a < 0 then return 'negative'; elsif a = 0 then return 'zero'; elsif a < 10 then return 'small'; else return 'big'; end if; end; $$ language plpgsql; select tr_f2(5); -- When the assert fails, the tracer prints the content of all the -- variables of the function. The variables of the types which cannot be -- converted to a string are skipped, a value containing a newline is -- printed on a separate row, and a long value is trimmed. create function tr_f3() returns void as $$ declare r record; v tr_t1; a int[]; n int; t text; m text; c refcursor := 'tr_cursor'; begin a := array[1, 2, 3]; t := 'a longer text used for the trimming'; m := 'first line' || chr(10) || 'second line'; select 1 as x, 2 as y into r; v := (1, 2); open c for select * from tr_t1; assert n is not null, 'n should not be null'; close c; end; $$ language plpgsql; -- The ASSERT statement is traced separately and has its own verbosity. -- With the verbose verbosity it prints all the variables of the -- function. The tracer itself is switched to the terse verbosity here, -- so only the output of the traced assert is interesting. set plpgsql_check.trace_assert to on; set plpgsql_check.trace_assert_verbosity to verbose; select plpgsql_check_tracer(true, 'terse'); -- the exception raised by the failed assert is caught, so the output of -- the tracer and the message of the exception are not mixed do $$ begin perform tr_f3(); exception when others then raise notice 'catched: %', sqlerrm; end; $$; -- the content of a long variable is trimmed set plpgsql_check.tracer_variable_max_length to 10; do $$ begin perform tr_f3(); exception when others then raise notice 'catched: %', sqlerrm; end; $$; set plpgsql_check.tracer_variable_max_length to default; -- a failed assert is traced too create function tr_f4(a int) returns void as $$ begin assert a > 0, 'a should be positive'; end; $$ language plpgsql; select tr_f4(1); do $$ begin perform tr_f4(-1); exception when others then raise notice 'catched: %', sqlerrm; end; $$; set plpgsql_check.trace_assert_verbosity to terse; do $$ begin perform tr_f4(-1); exception when others then raise notice 'catched: %', sqlerrm; end; $$; set plpgsql_check.trace_assert to off; set plpgsql_check.trace_assert_verbosity to default; -- the tracer prints the type of the fired trigger and the transition -- records create function tr_trg_row() returns trigger as $$ begin if TG_OP = 'DELETE' then return old; end if; return new; end; $$ language plpgsql; create function tr_trg_stmt() returns trigger as $$ begin return null; end; $$ language plpgsql; create trigger tr_t1_row before insert or update or delete on tr_t1 for each row execute procedure tr_trg_row(); create trigger tr_t1_trunc before truncate on tr_t1 for each statement execute procedure tr_trg_stmt(); select plpgsql_check_tracer(true, 'default'); insert into tr_t1 values(1, 2); update tr_t1 set b = 3; delete from tr_t1; insert into tr_t1 values(1, 2); truncate tr_t1; select plpgsql_check_tracer(false); -- ASSERT tracing is independent of both general tracing and runtime asserts. create sequence tr_assert_calls; create function tr_assert_only() returns void as $$ begin assert nextval('tr_assert_calls') > 0; end; $$ language plpgsql; set plpgsql.check_asserts to off; set plpgsql_check.trace_assert to on; select tr_assert_only(); select last_value, is_called from tr_assert_calls; set plpgsql_check.trace_assert to off; set plpgsql.check_asserts to default; drop function tr_assert_only(); drop sequence tr_assert_calls; set plpgsql_check.enable_tracer to off; set plpgsql_check.tracer_test_mode to false; set plpgsql_check.tracer_verbosity to default; drop trigger tr_t1_trunc on tr_t1; drop trigger tr_t1_row on tr_t1; drop function tr_trg_stmt(); drop function tr_trg_row(); drop function tr_f4(int); drop function tr_f3(); drop function tr_f2(int); drop function tr_f1(int, text); drop table tr_t1; okbob-plpgsql_check-800db79/sql/plpgsql_pragma_generator.sql000066400000000000000000000574451526744060500244110ustar00rootroot00000000000000load 'plpgsql'; set client_min_messages to warning; create extension if not exists plpgsql_check; set client_min_messages to notice; set plpgsql_check.regress_test_mode = true; -- -- tests of plpgsql_make_pragma and "pragmas" option -- of check functions -- create table gtp_src(a int, b text); create type gtp_ct as (x int, y text); create domain gtp_dom as int check (value > 0); -- happy path - CREATE TEMP TABLE AS SELECT create function gtp_f1() returns void as $$ begin create temp table gtp_t1 as select a, b from gtp_src; insert into gtp_t1 values (10, 'hello'); end; $$ language plpgsql; select * from plpgsql_make_pragma('gtp_f1()'); -- without pragmas the check should detect missing relation select * from plpgsql_check_function('gtp_f1()'); -- with generated pragmas the check should be ok select * from plpgsql_check_function('gtp_f1()', pragmas => array(select plpgsql_make_pragma('gtp_f1()'))); -- tabular form select * from plpgsql_check_function_tb('gtp_f1()', pragmas => array(select plpgsql_make_pragma('gtp_f1()'))); -- generator is idempotent - repeated call returns same result, -- and doesn't leave any relation in system catalogue select * from plpgsql_make_pragma('gtp_f1()'); select count(*) from pg_class where relname in ('gtp_t1'); -- happy path - one and two temporary tables created by AS VALUES and AS TABLE create function gtp_f2() returns void as $$ begin create temp table gtp_v1 as values (1, 'x'), (2, 'y'); end; $$ language plpgsql; select * from plpgsql_make_pragma('gtp_f2()'); create function gtp_f3() returns void as $$ begin create temp table gtp_tt1 as table gtp_src; end; $$ language plpgsql; select * from plpgsql_make_pragma('gtp_f3()'); create function gtp_f4() returns void as $$ begin create temp table gtp_v1 as values (1, 'x'), (2, 'y'); create temp table gtp_tt1 as table gtp_src; end; $$ language plpgsql; select * from plpgsql_make_pragma('gtp_f4()'); drop function gtp_f2(); drop function gtp_f3(); drop function gtp_f4(); -- WITH NO DATA, IF NOT EXISTS and ON COMMIT variants create function gtp_f5() returns void as $$ begin create temp table gtp_nd as select a from gtp_src with no data; create temp table if not exists gtp_ine as select b from gtp_src; create temp table gtp_oc1 on commit preserve rows as select a from gtp_src; create temp table gtp_oc2 on commit delete rows as select a from gtp_src; create temp table gtp_oc3 on commit drop as select a from gtp_src; end; $$ language plpgsql; select * from plpgsql_make_pragma('gtp_f5()'); drop function gtp_f5(); -- explicitly entered column names create function gtp_f6() returns void as $$ begin create temp table gtp_cn(c1, c2) as select a, b from gtp_src; end; $$ language plpgsql; select * from plpgsql_make_pragma('gtp_f6()'); drop function gtp_f6(); -- star, join, subquery, CTE and recursive CTE create function gtp_f7() returns void as $$ begin create temp table gtp_star as select * from gtp_src; create temp table gtp_join as select s1.a, s2.b from gtp_src s1 join gtp_src s2 on s1.a = s2.a; create temp table gtp_sub as select a from gtp_src where a in (select a from gtp_src); create temp table gtp_cte as with x as (select a, b from gtp_src) select * from x; create temp table gtp_rcte as with recursive r(n) as (values (1) union all select n + 1 from r where n < 10) select n from r; end; $$ language plpgsql; select * from plpgsql_make_pragma('gtp_f7()'); drop function gtp_f7(); -- computed columns without aliases have name "?column?". Generated -- pragma returns these names without any change (the pragma is not -- applicable, and self registration fails with warning like CREATE -- TABLE AS would fail). create function gtp_f8() returns void as $$ begin create temp table gtp_comp as select a + 1, a + 1 from gtp_src; end; $$ language plpgsql; select * from plpgsql_make_pragma('gtp_f8()'); drop function gtp_f8(); -- unknown type is replaced by text like CREATE TABLE AS does create function gtp_f9() returns void as $$ begin create temp table gtp_unk as select null as x; end; $$ language plpgsql; select * from plpgsql_make_pragma('gtp_f9()'); drop function gtp_f9(); -- array, composite, domain types and typmods create function gtp_f10() returns void as $$ begin create temp table gtp_types as select array[1,2] as arr, (1, 'x')::gtp_ct as comp, 10::gtp_dom as dom, 'abc'::varchar(10) as vc, 3.14::numeric(12,2) as num; end; $$ language plpgsql; select * from plpgsql_make_pragma('gtp_f10()'); drop function gtp_f10(); -- generated datetime and interval type spellings must be usable as pragmas create function gtp_datetime() returns void as $$ begin create temp table gtp_datetime1 as select timestamp(3) '2026-01-01 12:34:56.789' as ts, time(2) with time zone '12:34:56.78+00' as tm; create temp table gtp_datetime2 (elapsed interval day to second(2), items int[2][3]); perform ts, tm, elapsed, items from gtp_datetime1, gtp_datetime2; end; $$ language plpgsql; select * from plpgsql_make_pragma('gtp_datetime()'); select * from plpgsql_check_function('gtp_datetime()', pragmas => array(select plpgsql_make_pragma('gtp_datetime()'))); drop function gtp_datetime(); -- PostgreSQL tables and anonymous record types can have no attributes. create function gtp_empty() returns void as $$ begin create temp table gtp_empty1(); create temp table gtp_empty2 as select from gtp_src; insert into gtp_empty1 default values; insert into gtp_empty2 default values; end; $$ language plpgsql; select * from plpgsql_make_pragma('gtp_empty()'); select * from plpgsql_check_function('gtp_empty()', pragmas => array(select plpgsql_make_pragma('gtp_empty()'))); drop function gtp_empty(); create function gtp_empty_record() returns void as $$ declare r record; begin perform plpgsql_check_pragma('type: r ()'); perform r; end; $$ language plpgsql; select * from plpgsql_check_function('gtp_empty_record()'); drop function gtp_empty_record(); -- Empty lists must not make missing columns or trailing commas legal. create function gtp_empty_invalid() returns void as $$ begin perform plpgsql_check_pragma('table: gtp_bad1(,)'); perform plpgsql_check_pragma('table: gtp_bad2(a int,)'); perform plpgsql_check_pragma('table: gtp_bad3('); end; $$ language plpgsql; select * from plpgsql_check_function('gtp_empty_invalid()'); drop function gtp_empty_invalid(); -- identifiers that require quoting create function gtp_f11() returns void as $$ begin create temp table "MyT" as select 1 as "select", 2 as normal; end; $$ language plpgsql; select * from plpgsql_make_pragma('gtp_f11()'); drop function gtp_f11(); -- resjunk columns (ORDER BY) should not leak to generated pragma create function gtp_f12() returns void as $$ begin create temp table gtp_junk as select a from gtp_src order by b; end; $$ language plpgsql; select * from plpgsql_make_pragma('gtp_f12()'); drop function gtp_f12(); -- traversing of nested statements - IF/ELSE branches, loops, nested -- blocks, exception handlers and unreachable code create function gtp_f13() returns void as $$ declare i int; begin if random() > 0.5 then create temp table gtp_n1 as select 1 as x; else create temp table gtp_n2 as select 2 as y; end if; for i in 1..2 loop create temp table gtp_n3 as select 3 as z; end loop; declare v int; begin create temp table gtp_n4 as select 4 as w; exception when others then create temp table gtp_n5 as select 5 as e; end; return; create temp table gtp_n6 as select 6 as u; end; $$ language plpgsql; select * from plpgsql_make_pragma('gtp_f13()'); drop function gtp_f13(); -- positional processing - same table name twice (no deduplication), -- the following statements see the last definition create function gtp_f14() returns void as $$ begin create temp table gtp_dup as select 1 as x; drop table gtp_dup; create temp table gtp_dup as select 'a'::text as y; create temp table gtp_dup2 as select * from gtp_dup; end; $$ language plpgsql; select * from plpgsql_make_pragma('gtp_f14()'); drop function gtp_f14(); -- one temporary table references another one create function gtp_f15() returns void as $$ begin create temp table gtp_c1 as select a, b from gtp_src; create temp table gtp_c2 as select * from gtp_c1 where a > 0; create temp table gtp_c3 as select gtp_c1.a, gtp_c2.b from gtp_c1 join gtp_c2 on gtp_c1.a = gtp_c2.a; create temp table gtp_c4 as select a from gtp_src where a in (select a from gtp_c1); end; $$ language plpgsql; select * from plpgsql_make_pragma('gtp_f15()'); drop function gtp_f15(); -- temporary table shadows an existing table with same name create table gtp_shadow(t text); create function gtp_f16() returns void as $$ begin create temp table gtp_shadow as select 1 as id; create temp table gtp_from_shadow as select * from gtp_shadow; end; $$ language plpgsql; select * from plpgsql_make_pragma('gtp_f16()'); drop function gtp_f16(); drop table gtp_shadow; -- explicitly used pg_temp schema create function gtp_f17() returns void as $$ begin create temp table pg_temp.gtp_q1 as select 1 as z; create table pg_temp.gtp_q2 as select 2 as w; end; $$ language plpgsql; select * from plpgsql_make_pragma('gtp_f17()'); drop function gtp_f17(); -- these forms are ignored - dynamic SQL, not temporary tables and -- materialized views create function gtp_f18() returns void as $$ begin execute 'create temp table gtp_s4 as select 1 as x'; create table gtp_s5 as select a from gtp_src; create unlogged table gtp_s6 as select a from gtp_src; create table gtp_s8 (id int); create unlogged table gtp_s9 (id int); create materialized view gtp_s7 as select a from gtp_src; drop table gtp_s4, gtp_s5, gtp_s6, gtp_s8, gtp_s9; drop materialized view gtp_s7; end; $$ language plpgsql; select * from plpgsql_make_pragma('gtp_f18()'); drop function gtp_f18(); -- function without temporary tables create function gtp_f19() returns int as $$ begin return (select count(*) from gtp_src); end; $$ language plpgsql; select * from plpgsql_make_pragma('gtp_f19()'); drop function gtp_f19(); -- A zero-column table produces a usable empty column list. create function gtp_f20() returns void as $$ begin create temp table gtp_zero as select from gtp_src; end; $$ language plpgsql; select * from plpgsql_make_pragma('gtp_f20()'); drop function gtp_f20(); -- CREATE TABLE AS EXECUTE is ignored create function gtp_f21() returns void as $$ begin create temp table gtp_pe as execute some_prepared_stmt; end; $$ language plpgsql; select * from plpgsql_make_pragma('gtp_f21()'); drop function gtp_f21(); -- inner query can use plpgsql variables create function gtp_f22() returns void as $$ declare v_id int default 10; v_txt text default 'x'; begin create temp table gtp_vars as select v_id as id, v_txt as txt, a from gtp_src where a > v_id; end; $$ language plpgsql; select * from plpgsql_make_pragma('gtp_f22()'); drop function gtp_f22(); -- regprocedure argument selects one function from overloaded set, -- function can use OUT arguments create function gtp_over(p int) returns void as $$ begin create temp table gtp_o1 as select p as x; end; $$ language plpgsql; create function gtp_over(p text) returns void as $$ begin create temp table gtp_o2 as select p as y; end; $$ language plpgsql; select * from plpgsql_make_pragma('gtp_over(int)'); select * from plpgsql_make_pragma('gtp_over(text)'); drop function gtp_over(int); drop function gtp_over(text); create function gtp_out(in p int, out o1 int, out o2 text) as $$ begin create temp table gtp_ot as select p + 1 as q; o1 := p; o2 := 'x'; end; $$ language plpgsql; select * from plpgsql_make_pragma('gtp_out(int)'); drop function gtp_out(int); -- trigger function requires relid argument create table gtp_trg_tbl(id int, val text); create function gtp_trg() returns trigger as $$ begin create temp table gtp_trg_snapshot as select new.id, new.val; return new; end; $$ language plpgsql; select * from plpgsql_make_pragma('gtp_trg()', 'gtp_trg_tbl'); drop function gtp_trg(); drop table gtp_trg_tbl; -- explicitly written pragmas are respected by generator create function gtp_f23() returns void as $$ begin execute 'create temp table gtp_dyn(a int, b text)'; perform plpgsql_check_pragma('table: gtp_dyn(a int, b text)'); create temp table gtp_from_dyn as select * from gtp_dyn; end; $$ language plpgsql; select * from plpgsql_make_pragma('gtp_f23()'); drop function gtp_f23(); -- CREATE TABLE statement based forms. The temporary table is created -- inside the check's subtransaction (and removed by its rollback), and -- the pragma is derived from the structure of really created table. -- Column definition list form - typmods, quoting, serial, identity and -- generated columns, constraints, collation (not carried to pragma), -- zero columns create function gtp_cs1() returns void as $$ begin create temp table gtp_cl1 (id int, name text); create temp table gtp_cl2 (v varchar(10), n numeric(12,2), t text collate "C"); create temp table "GtpQ" ("select" int, normal text); create temp table gtp_cl3 (id serial, iid int generated always as identity, g int generated always as (id + 1) stored); create temp table gtp_cl4 (id int primary key, v text not null default 'x', check (id > 0)); create temp table gtp_cl5 (); end; $$ language plpgsql; select * from plpgsql_make_pragma('gtp_cs1()'); drop function gtp_cs1(); -- LIKE clause and OF type clause create function gtp_cs2() returns void as $$ begin create temp table gtp_lk1 (like gtp_src); create temp table gtp_lk2 (like gtp_src including all); create temp table gtp_lk3 (id int, like gtp_src); create temp table gtp_of1 of gtp_ct; create temp table gtp_of2 of gtp_ct (x with options not null); end; $$ language plpgsql; select * from plpgsql_make_pragma('gtp_cs2()'); drop function gtp_cs2(); -- inheritance - from a persistent table and from a temporary table -- created earlier in same function create table gtp_parent(pid int, pval text); create function gtp_cs3() returns void as $$ begin create temp table gtp_inh1 () inherits (gtp_parent); create temp table gtp_inh2 (own text) inherits (gtp_parent); create temp table gtp_tp (a int, b text); create temp table gtp_inh3 () inherits (gtp_tp); end; $$ language plpgsql; select * from plpgsql_make_pragma('gtp_cs3()'); drop function gtp_cs3(); drop table gtp_parent; -- partitioning - temporary parent and temporary partitions create function gtp_cs4() returns void as $$ begin create temp table gtp_pp (id int, v text) partition by range (id); create temp table gtp_p1 partition of gtp_pp for values from (1) to (100); create temp table gtp_p2 partition of gtp_pp default; end; $$ language plpgsql; select * from plpgsql_make_pragma('gtp_cs4()'); drop function gtp_cs4(); -- storage options, access method, ON COMMIT variants, IF NOT EXISTS -- (the existing table wins - like runtime does), pg_temp qualification -- without TEMP keyword create function gtp_cs5() returns void as $$ begin create temp table gtp_o1 (a int) with (fillfactor = 70); create temp table gtp_o2 (a int) using heap; create temp table gtp_o3 (a int) on commit preserve rows; create temp table gtp_o4 (a int) on commit delete rows; create temp table gtp_o5 (a int) on commit drop; create temp table if not exists gtp_o6 (a int); create temp table if not exists gtp_o6 (b text); create table pg_temp.gtp_o7 (a int); create table pg_temp.gtp_o8 (like gtp_src); create table pg_temp.gtp_o9 of gtp_ct; end; $$ language plpgsql; select * from plpgsql_make_pragma('gtp_cs5()'); drop function gtp_cs5(); -- mixed CREATE TABLE AS and CREATE TABLE statements create function gtp_mx1() returns void as $$ begin create temp table gtp_mm1 as select a, b from gtp_src; create temp table gtp_mm2 (id int, v text); end; $$ language plpgsql; select * from plpgsql_make_pragma('gtp_mx1()'); drop function gtp_mx1(); -- the table created by CREATE TABLE statement is visible for the -- following CREATE TABLE AS statements (join, subquery) create function gtp_mx2() returns void as $$ begin create temp table gtp_mn1 (a int, b text); create temp table gtp_mn2 as select gtp_mn1.a, s.b from gtp_mn1 join gtp_src s on s.a = gtp_mn1.a where gtp_mn1.a in (select a from gtp_mn1); end; $$ language plpgsql; select * from plpgsql_make_pragma('gtp_mx2()'); drop function gtp_mx2(); -- and the reverse order - the table created by CREATE TABLE AS -- statement is visible for the following CREATE TABLE statement create function gtp_mx3() returns void as $$ begin create temp table gtp_mo1 as select a, b from gtp_src; create temp table gtp_mo2 (like gtp_mo1 including all); end; $$ language plpgsql; select * from plpgsql_make_pragma('gtp_mx3()'); drop function gtp_mx3(); -- generator of CREATE TABLE statement based pragmas is idempotent too create function gtp_cs6() returns void as $$ begin create temp table gtp_ii (id int, v text); insert into gtp_ii values (1, 'x'); end; $$ language plpgsql; select * from plpgsql_make_pragma('gtp_cs6()'); select * from plpgsql_make_pragma('gtp_cs6()'); select count(*) from pg_class where relname in ('gtp_ii'); -- generated pragma is usable by "pragmas" option select * from plpgsql_check_function('gtp_cs6()', pragmas => array(select plpgsql_make_pragma('gtp_cs6()'))); drop function gtp_cs6(); -- error cases create function gtp_err1() returns void as $$ begin create temp table gtp_e1 as select * from gtp_nonexistent; end; $$ language plpgsql; -- generator reports same error like plpgsql_check_function select * from plpgsql_check_function('gtp_err1()'); \set VERBOSITY terse select * from plpgsql_make_pragma('gtp_err1()'); \set VERBOSITY default do $$ declare s text; m text; begin perform plpgsql_make_pragma('gtp_err1()'); exception when others then get stacked diagnostics s = returned_sqlstate, m = message_text; raise notice 'sqlstate: %, message: %', s, m; end; $$; drop function gtp_err1(); -- error is raised on first failed statement only, but when -- fatal_errors is false, then the scanning continues create function gtp_err2() returns void as $$ begin create temp table gtp_e21 as select * from gtp_missing1; create temp table gtp_e22 as select * from gtp_missing2; create temp table gtp_e23 as select a from gtp_src; end; $$ language plpgsql; do $$ declare s text; m text; begin perform plpgsql_make_pragma('gtp_err2()'); exception when others then get stacked diagnostics s = returned_sqlstate, m = message_text; raise notice 'sqlstate: %, message: %', s, m; end; $$; select * from plpgsql_make_pragma('gtp_err2()', fatal_errors => false); drop function gtp_err2(); -- temporary table cannot be created in non-temporary schema create function gtp_err3() returns void as $$ begin create temporary table public.gtp_e3 as select 1::int, 'JOHN'::text; end; $$ language plpgsql; do $$ declare s text; m text; begin perform plpgsql_make_pragma('gtp_err3()'); exception when others then get stacked diagnostics s = returned_sqlstate, m = message_text; raise notice 'sqlstate: %, message: %', s, m; end; $$; drop function gtp_err3(); -- the check of target schema is done before execution for CREATE TABLE -- statement forms too (column definition list, LIKE clause, OF type -- clause), and it respects fatal_errors option create function gtp_err4() returns void as $$ begin create temp table public.gtp_e41 (id int); create temp table public.gtp_e42 (like gtp_src); create temp table public.gtp_e43 of gtp_ct; create temp table gtp_e44 as select a from gtp_src; end; $$ language plpgsql; do $$ declare s text; m text; begin perform plpgsql_make_pragma('gtp_err4()'); exception when others then get stacked diagnostics s = returned_sqlstate, m = message_text; raise notice 'sqlstate: %, message: %', s, m; end; $$; select * from plpgsql_make_pragma('gtp_err4()', fatal_errors => false); drop function gtp_err4(); -- too many column names create function gtp_err5() returns void as $$ begin create temp table gtp_e5(c1, c2, c3) as select a, b from gtp_src; end; $$ language plpgsql; do $$ declare s text; m text; begin perform plpgsql_make_pragma('gtp_err5()'); exception when others then get stacked diagnostics s = returned_sqlstate, m = message_text; raise notice 'sqlstate: %, message: %', s, m; end; $$; drop function gtp_err5(); -- duplicate table name (usually the pattern CREATE, DROP, CREATE with -- same name) is reported as warning only - DROP statements are not -- executed by static scanning, so this is not a real error. Pragmas -- are returned for both definitions, and the following statements see -- the last definition. create function gtp_err6() returns void as $$ begin create temp table gtp_dd (x int); drop table gtp_dd; create temp table gtp_dd (y text); create temp table gtp_dd2 (z int); create temp table gtp_dd3 as select * from gtp_dd; end; $$ language plpgsql; select * from plpgsql_make_pragma('gtp_err6()'); drop function gtp_err6(); -- mixed collisions - CREATE TABLE statement redefined by CREATE TABLE -- AS statement and vice versa create function gtp_err9() returns void as $$ begin create temp table gtp_md (a int); drop table gtp_md; create temp table gtp_md as select 'x'::text as t; create temp table gtp_md2 as select 1 as b; drop table gtp_md2; create temp table gtp_md2 (c date); create temp table gtp_md3 as select * from gtp_md, gtp_md2; end; $$ language plpgsql; select * from plpgsql_make_pragma('gtp_err9()'); drop function gtp_err9(); -- CREATE TABLE AS IF NOT EXISTS - the existing table wins like in -- runtime create function gtp_err10() returns void as $$ begin create temp table gtp_ine2 as select 1 as a; create temp table if not exists gtp_ine2 as select 'x'::text as b; end; $$ language plpgsql; select * from plpgsql_make_pragma('gtp_err10()'); drop function gtp_err10(); -- runtime errors of CREATE TABLE statement execution respect -- fatal_errors option - a temporary partition of persistent table create table gtp_ppart (id int) partition by range (id); create function gtp_err7() returns void as $$ begin create temp table gtp_bp partition of gtp_ppart for values from (1) to (100); create temp table gtp_bp2 (id int); end; $$ language plpgsql; do $$ declare s text; m text; begin perform plpgsql_make_pragma('gtp_err7()'); exception when others then get stacked diagnostics s = returned_sqlstate, m = message_text; raise notice 'sqlstate: %, message: %', s, m; end; $$; select * from plpgsql_make_pragma('gtp_err7()', fatal_errors => false); drop function gtp_err7(); drop table gtp_ppart; -- more not executable CREATE TABLE statements - foreign key from -- temporary table to persistent table, LIKE of missing table, OF -- missing type create function gtp_err8() returns void as $$ begin create temp table gtp_fk (id int references gtp_src(a)); create temp table gtp_bl (like gtp_not_exists); create temp table gtp_bo of gtp_type_not_exists; end; $$ language plpgsql; select * from plpgsql_make_pragma('gtp_err8()', fatal_errors => false); drop function gtp_err8(); -- "pragmas" option of check functions create function gtp_f24() returns void as $$ begin insert into gtp_manual values (1, 'x'); end; $$ language plpgsql; -- NULL and empty array have no effect select * from plpgsql_check_function('gtp_f24()', pragmas => null); select * from plpgsql_check_function('gtp_f24()', pragmas => '{}'::text[]); -- manually written (or fixed) pragma can be used, NULL elements -- are ignored select * from plpgsql_check_function('gtp_f24()', pragmas => array[null, 'table: gtp_manual(a int, b text)']); -- broken pragma raises warning only, and check continues select * from plpgsql_check_function('gtp_f24()', pragmas => array['table: gtp_manual(int int int)']); -- any pragma type is allowed select * from plpgsql_check_function('gtp_f24()', pragmas => array['disable: check']); drop function gtp_f24(); drop table gtp_src; drop type gtp_ct; drop domain gtp_dom; okbob-plpgsql_check-800db79/src/000077500000000000000000000000001526744060500165625ustar00rootroot00000000000000okbob-plpgsql_check-800db79/src/assign.c000066400000000000000000000453621526744060500202240ustar00rootroot00000000000000/*------------------------------------------------------------------------- * * assign.c * * assign types to record variables * * by Pavel Stehule 2013-2026 * *------------------------------------------------------------------------- */ #include "plpgsql_check.h" #include "access/htup_details.h" #include "catalog/pg_type.h" #include "parser/parse_coerce.h" #include "utils/builtins.h" #include "utils/lsyscache.h" #include "utils/typcache.h" #define get_eval_mcontext(estate) \ ((estate)->eval_econtext->ecxt_per_tuple_memory) #define eval_mcontext_alloc(estate, sz) \ MemoryContextAlloc(get_eval_mcontext(estate), sz) #define eval_mcontext_alloc0(estate, sz) \ MemoryContextAllocZero(get_eval_mcontext(estate), sz) static bool compatible_tupdescs(TupleDesc src_tupdesc, TupleDesc dst_tupdesc); /* * Mark variable as used */ void plpgsql_check_record_variable_usage(PLpgSQL_checkstate *cstate, int dno, bool write) { if (dno >= 0) { if (!write) cstate->used_variables = bms_add_member(cstate->used_variables, dno); else { cstate->modif_variables = bms_add_member(cstate->modif_variables, dno); /* raise extra warning when protected variable is modified */ if (bms_is_member(dno, cstate->protected_variables)) { PLpgSQL_variable *var = (PLpgSQL_variable *) cstate->estate->datums[dno]; StringInfoData message; initStringInfo(&message); appendStringInfo(&message, "auto varible \"%s\" should not be modified by user", var->refname); plpgsql_check_put_error(cstate, 0, var->lineno, message.data, NULL, NULL, PLPGSQL_CHECK_WARNING_EXTRA, 0, NULL, NULL); pfree(message.data); } } } } void plpgsql_check_row_or_rec(PLpgSQL_checkstate *cstate, PLpgSQL_row *row, PLpgSQL_rec *rec) { if (row != NULL) { int fnum; for (fnum = 0; fnum < row->nfields; fnum++) { /* skip dropped columns */ if (row->varnos[fnum] < 0) continue; plpgsql_check_target(cstate, row->varnos[fnum], NULL, NULL); } plpgsql_check_record_variable_usage(cstate, row->dno, true); } else if (rec != NULL) { /* * There are no checks done on records currently; just record that the * variable is not unused. */ plpgsql_check_record_variable_usage(cstate, rec->dno, true); } } void plpgsql_check_is_assignable(PLpgSQL_execstate *estate, int dno) { PLpgSQL_datum *datum; Assert(dno >= 0 && dno < estate->ndatums); datum = estate->datums[dno]; switch (datum->dtype) { case PLPGSQL_DTYPE_VAR: case PLPGSQL_DTYPE_PROMISE: case PLPGSQL_DTYPE_REC: if (((PLpgSQL_variable *) datum)->isconst) ereport(ERROR, (errcode(ERRCODE_ERROR_IN_ASSIGNMENT), errmsg("variable \"%s\" is declared CONSTANT", ((PLpgSQL_variable *) datum)->refname))); break; case PLPGSQL_DTYPE_ROW: /* always assignable; member vars were checked at compile time */ break; case PLPGSQL_DTYPE_RECFIELD: /* assignable if parent record is */ plpgsql_check_is_assignable(estate, ((PLpgSQL_recfield *) datum)->recparentno); break; default: elog(ERROR, "unrecognized dtype: %d", datum->dtype); break; } } /* * Verify lvalue It doesn't repeat a checks that are done. Checks a subscript * expressions, verify a validity of record's fields. */ void plpgsql_check_target(PLpgSQL_checkstate *cstate, int varno, Oid *expected_typoid, int *expected_typmod) { PLpgSQL_datum *target = cstate->estate->datums[varno]; /* * The target should be not constant, but we can allow assignment to * constant variable at block statement - it is using default value. */ if (cstate->estate->err_stmt->cmd_type != PLPGSQL_STMT_BLOCK) plpgsql_check_is_assignable(cstate->estate, varno); plpgsql_check_record_variable_usage(cstate, varno, true); switch (target->dtype) { case PLPGSQL_DTYPE_VAR: { PLpgSQL_var *var = (PLpgSQL_var *) target; PLpgSQL_type *tp = var->datatype; if (expected_typoid != NULL) *expected_typoid = tp->typoid; if (expected_typmod != NULL) *expected_typmod = tp->atttypmod; } break; case PLPGSQL_DTYPE_REC: { PLpgSQL_rec *rec = (PLpgSQL_rec *) target; plpgsql_check_recvar_info(rec, expected_typoid, expected_typmod); } break; case PLPGSQL_DTYPE_ROW: { PLpgSQL_row *row = (PLpgSQL_row *) target; if (row->rowtupdesc != NULL) { if (expected_typoid != NULL) *expected_typoid = row->rowtupdesc->tdtypeid; if (expected_typmod != NULL) *expected_typmod = row->rowtupdesc->tdtypmod; } else { if (expected_typoid != NULL) *expected_typoid = RECORDOID; if (expected_typmod != NULL) *expected_typmod = -1; } plpgsql_check_row_or_rec(cstate, row, NULL); } break; case PLPGSQL_DTYPE_RECFIELD: { PLpgSQL_recfield *recfield = (PLpgSQL_recfield *) target; PLpgSQL_rec *rec; int fno; rec = (PLpgSQL_rec *) (cstate->estate->datums[recfield->recparentno]); /* * Check that there is already a tuple in the record. We need * that because records don't have any predefined field * structure. */ if (!recvar_tupdesc(rec)) ereport(ERROR, (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), errmsg("record \"%s\" is not assigned to tuple structure", rec->refname))); /* * Get the number of the records field to change and the * number of attributes in the tuple. Note: disallow system * column names because the code below won't cope. */ fno = SPI_fnumber(recvar_tupdesc(rec), recfield->fieldname); if (fno <= 0) ereport(ERROR, (errcode(ERRCODE_UNDEFINED_COLUMN), errmsg("record \"%s\" has no field \"%s\"", rec->refname, recfield->fieldname))); if (expected_typoid) *expected_typoid = SPI_gettypeid(recvar_tupdesc(rec), fno); if (expected_typmod) *expected_typmod = TupleDescAttr(recvar_tupdesc(rec), fno - 1)->atttypmod; } break; default: ; /* nope */ } } /* * Check so target can accept typoid value * */ void plpgsql_check_assign_to_target_type(PLpgSQL_checkstate *cstate, Oid target_typoid, int32 target_typmod, Oid value_typoid, bool isnull, int target_dno) { char *local_err_text = NULL; char *local_query = NULL; if (!cstate->estate->err_text && cstate->estate->err_stmt) local_err_text = plpgsql_check_prepare_err_text_with_target_vardecl(cstate, cstate->estate->err_stmt, target_dno); if (cstate->estate->err_stmt && cstate->estate->err_stmt->cmd_type == PLPGSQL_STMT_EXECSQL) local_query = ((PLpgSQL_stmt_execsql *) cstate->estate->err_stmt)->sqlstmt->query; /* not used yet */ (void) target_typmod; /* the overhead UNKONWNOID --> TEXT is low */ if ((target_typoid == value_typoid) || (target_typoid == TEXTOID && value_typoid == UNKNOWNOID)) return; if (!isnull && type_is_rowtype(value_typoid) && !type_is_rowtype(target_typoid) && !can_coerce_type(1, &value_typoid, &target_typoid, COERCION_ASSIGNMENT)) { StringInfoData str; initStringInfo(&str); appendStringInfo(&str, "cannot cast composite value of \"%s\" type to a scalar value of \"%s\" type", format_type_be(value_typoid), format_type_be(target_typoid)); plpgsql_check_put_error(cstate, ERRCODE_DATATYPE_MISMATCH, 0, str.data, NULL, NULL, PLPGSQL_CHECK_ERROR, 0, local_query, local_err_text); } else if (!isnull) { StringInfoData str; initStringInfo(&str); appendStringInfo(&str, "cast \"%s\" value to \"%s\" type", format_type_be(value_typoid), format_type_be(target_typoid)); /* accent warning when cast is without supported explicit casting */ if (!can_coerce_type(1, &value_typoid, &target_typoid, COERCION_EXPLICIT)) plpgsql_check_put_error(cstate, ERRCODE_DATATYPE_MISMATCH, 0, "target type is different type than source type", str.data, "There are no possible explicit coercion between those types, possibly bug!", PLPGSQL_CHECK_WARNING_OTHERS, 0, local_query, local_err_text); else if (!can_coerce_type(1, &value_typoid, &target_typoid, COERCION_ASSIGNMENT)) plpgsql_check_put_error(cstate, ERRCODE_DATATYPE_MISMATCH, 0, "target type is different type than source type", str.data, "The input expression type does not have an assignment cast to the target type.", PLPGSQL_CHECK_WARNING_OTHERS, 0, local_query, local_err_text); else { /* highly probably only performance issue */ plpgsql_check_put_error(cstate, ERRCODE_DATATYPE_MISMATCH, 0, "target type is different type than source type", str.data, "Hidden casting can be a performance issue.", PLPGSQL_CHECK_WARNING_PERFORMANCE, 0, local_query, local_err_text); } pfree(str.data); } /* release local err_text */ if (local_err_text) pfree(local_err_text); } /* * Assign a tuple descriptor to variable specified by dno */ void plpgsql_check_assign_tupdesc_dno(PLpgSQL_checkstate *cstate, int varno, TupleDesc tupdesc, bool isnull) { PLpgSQL_datum *target = cstate->estate->datums[varno]; switch (target->dtype) { case PLPGSQL_DTYPE_VAR: { PLpgSQL_var *var = (PLpgSQL_var *) target; /* * plpgsql_check_assign_tupdesc_dno is used when the expr * result is assigned to scalar variable. And then the tupdesc * should be not null */ if (!tupdesc || tupdesc->natts == 0) return; plpgsql_check_assign_to_target_type(cstate, var->datatype->typoid, var->datatype->atttypmod, TupleDescAttr(tupdesc, 0)->atttypid, isnull, target->dno); } break; case PLPGSQL_DTYPE_ROW: plpgsql_check_assign_tupdesc_row_or_rec(cstate, (PLpgSQL_row *) target, NULL, tupdesc, isnull); break; case PLPGSQL_DTYPE_REC: plpgsql_check_assign_tupdesc_row_or_rec(cstate, NULL, (PLpgSQL_rec *) target, tupdesc, isnull); break; case PLPGSQL_DTYPE_RECFIELD: { Oid typoid; int typmod; plpgsql_check_target(cstate, varno, &typoid, &typmod); if (!tupdesc || tupdesc->natts == 0) return; plpgsql_check_assign_to_target_type(cstate, typoid, typmod, TupleDescAttr(tupdesc, 0)->atttypid, isnull, target->dno); } break; default: ; /* nope */ } } /* * We have to assign TupleDesc to all used record variables step by step. We * would to use a exec routines for query preprocessing, so we must to create * a typed NULL value, and this value is assigned to record variable. */ void plpgsql_check_assign_tupdesc_row_or_rec(PLpgSQL_checkstate *cstate, PLpgSQL_row *row, PLpgSQL_rec *rec, TupleDesc tupdesc, bool isnull) { if (tupdesc == NULL) { char *local_err_text = NULL; if (!cstate->estate->err_text && cstate->estate->err_stmt) { int target_dno; if (rec) target_dno = rec->dno; else if (row) target_dno = row->dno; else target_dno = -1; local_err_text = plpgsql_check_prepare_err_text_with_target_vardecl(cstate, cstate->estate->err_stmt, target_dno); } plpgsql_check_put_error(cstate, 0, 0, "tuple descriptor is empty", NULL, NULL, PLPGSQL_CHECK_WARNING_OTHERS, 0, NULL, local_err_text); /* release local err_text */ if (local_err_text) pfree(local_err_text); return; } /* * row variable has assigned TupleDesc already, so don't be processed here */ if (rec != NULL) { PLpgSQL_rec *target = (PLpgSQL_rec *) (cstate->estate->datums[rec->dno]); plpgsql_check_recval_release(target); plpgsql_check_recval_assign_tupdesc(cstate, target, tupdesc, isnull); } else if (row != NULL) { int td_natts = tupdesc->natts; int fnum; int anum; anum = 0; for (fnum = 0; fnum < row->nfields; fnum++) { if (row->varnos[fnum] < 0) continue; /* skip dropped column in row struct */ while (anum < td_natts && TupleDescAttr(tupdesc, anum)->attisdropped) anum++; /* skip dropped column in tuple */ if (anum < td_natts) { Oid valtype = SPI_gettypeid(tupdesc, anum + 1); PLpgSQL_datum *target = cstate->estate->datums[row->varnos[fnum]]; switch (target->dtype) { case PLPGSQL_DTYPE_VAR: { PLpgSQL_var *var = (PLpgSQL_var *) target; plpgsql_check_assign_to_target_type(cstate, var->datatype->typoid, var->datatype->atttypmod, valtype, isnull, target->dno); } break; case PLPGSQL_DTYPE_RECFIELD: { Oid expected_typoid; int expected_typmod; plpgsql_check_target(cstate, target->dno, &expected_typoid, &expected_typmod); plpgsql_check_assign_to_target_type(cstate, expected_typoid, expected_typmod, valtype, isnull, target->dno); } break; default: ; /* nope */ } anum++; } } } } /* * recval_init, recval_release, recval_assign_tupdesc * * Initialize, clear, and assign expanded-record values during checking. */ void plpgsql_check_recval_init(PLpgSQL_rec *rec) { Assert(rec->dtype == PLPGSQL_DTYPE_REC); rec->erh = NULL; } void plpgsql_check_recval_release(PLpgSQL_rec *rec) { Assert(rec->dtype == PLPGSQL_DTYPE_REC); if (rec->erh) DeleteExpandedObject(ExpandedRecordGetDatum(rec->erh)); rec->erh = NULL; } /* * is_null is true, when we assign NULL expression and type should not be checked. */ void plpgsql_check_recval_assign_tupdesc(PLpgSQL_checkstate *cstate, PLpgSQL_rec *rec, TupleDesc tupdesc, bool is_null) { PLpgSQL_execstate *estate = cstate->estate; ExpandedRecordHeader *newerh; MemoryContext mcontext; TupleDesc var_tupdesc; Datum *newvalues; bool *newnulls; char *chunk; int vtd_natts; int i; char *local_err_text = NULL; char *local_query = NULL; if (!cstate->estate->err_text && cstate->estate->err_stmt) local_err_text = plpgsql_check_prepare_err_text_with_target_vardecl(cstate, cstate->estate->err_stmt, rec->dno); if (cstate->estate->err_stmt && cstate->estate->err_stmt->cmd_type == PLPGSQL_STMT_EXECSQL) local_query = ((PLpgSQL_stmt_execsql *) cstate->estate->err_stmt)->sqlstmt->query; mcontext = get_eval_mcontext(estate); plpgsql_check_recval_release(rec); /* * code is reduced version of make_expanded_record_for_rec */ if (rec->rectypeid != RECORDOID) { Oid rectypeid = rec->rectypeid; /* * rec->rectypeid can be a domain over a composite type. Building an * expanded record for the domain and then setting its fields with * constraint checking enabled (below) would evaluate the domain's * CHECK constraints against the freshly made, all-NULL dummy row and * report a bogus "violates check constraint" error while merely * analysing the function. The checker only needs the row structure, * so look through the domain to its base composite type. */ if (get_typtype(rectypeid) == TYPTYPE_DOMAIN) rectypeid = getBaseType(rectypeid); newerh = make_expanded_record_from_typeid(rectypeid, -1, mcontext); } else { if (!tupdesc) return; newerh = make_expanded_record_from_tupdesc(tupdesc, mcontext); } /* * code is reduced version of exec_move_row_from_field */ var_tupdesc = expanded_record_get_tupdesc(newerh); vtd_natts = var_tupdesc->natts; if (!is_null && tupdesc != NULL && !compatible_tupdescs(var_tupdesc, tupdesc)) { int attn1 = 0; int attn2 = 0; int target_nfields = 0; int src_nfields = 0; bool src_field_is_valid = false; bool target_field_is_valid = false; Form_pg_attribute sattr = NULL; Form_pg_attribute tattr = NULL; while (attn1 < var_tupdesc->natts || attn2 < tupdesc->natts) { if (!target_field_is_valid && attn1 < var_tupdesc->natts) { tattr = TupleDescAttr(var_tupdesc, attn1); if (tattr->attisdropped) { attn1 += 1; continue; } target_field_is_valid = true; target_nfields += 1; } if (!src_field_is_valid && attn2 < tupdesc->natts) { sattr = TupleDescAttr(tupdesc, attn2); if (sattr->attisdropped) { attn2 += 1; continue; } src_field_is_valid = true; src_nfields += 1; } if (src_field_is_valid && target_field_is_valid) { plpgsql_check_assign_to_target_type(cstate, tattr->atttypid, tattr->atttypmod, sattr->atttypid, false, rec->dno); /* try to search next tuple of fields */ src_field_is_valid = false; target_field_is_valid = false; attn1 += 1; attn2 += 1; } else break; } if (src_nfields < target_nfields) plpgsql_check_put_error(cstate, 0, 0, "too few attributes for composite variable", NULL, NULL, PLPGSQL_CHECK_WARNING_OTHERS, 0, local_query, local_err_text); else if (src_nfields > target_nfields) plpgsql_check_put_error(cstate, 0, 0, "too many attributes for composite variable", NULL, NULL, PLPGSQL_CHECK_WARNING_OTHERS, 0, local_query, local_err_text); } chunk = eval_mcontext_alloc(estate, vtd_natts * (sizeof(Datum) + sizeof(bool))); newvalues = (Datum *) chunk; newnulls = (bool *) (chunk + vtd_natts * sizeof(Datum)); for (i = 0; i < vtd_natts; i++) { newvalues[i] = (Datum) 0; newnulls[i] = true; } expanded_record_set_fields(newerh, newvalues, newnulls, true); TransferExpandedRecord(newerh, estate->datum_context); rec->erh = newerh; /* release local err_text */ if (local_err_text) pfree(local_err_text); } /* * compatible_tupdescs: detect whether two tupdescs are physically compatible * * TRUE indicates that a tuple satisfying src_tupdesc can be used directly as * a value for a composite variable using dst_tupdesc. */ static bool compatible_tupdescs(TupleDesc src_tupdesc, TupleDesc dst_tupdesc) { int i; /* Possibly we could allow src_tupdesc to have extra columns? */ if (dst_tupdesc->natts != src_tupdesc->natts) return false; for (i = 0; i < dst_tupdesc->natts; i++) { Form_pg_attribute dattr = TupleDescAttr(dst_tupdesc, i); Form_pg_attribute sattr = TupleDescAttr(src_tupdesc, i); if (dattr->attisdropped != sattr->attisdropped) return false; if (!dattr->attisdropped) { /* Normal columns must match by type and typmod */ if (dattr->atttypid != sattr->atttypid || (dattr->atttypmod >= 0 && dattr->atttypmod != sattr->atttypmod)) return false; } else { /* Dropped columns are OK as long as length/alignment match */ if (dattr->attlen != sattr->attlen || dattr->attalign != sattr->attalign) return false; } } return true; } okbob-plpgsql_check-800db79/src/catalog.c000066400000000000000000000221031526744060500203360ustar00rootroot00000000000000/*------------------------------------------------------------------------- * * catalog.c * * routines for working with Postgres's catalog and caches * * by Pavel Stehule 2013-2026 * *------------------------------------------------------------------------- */ #include "plpgsql_check.h" #include "catalog/pg_extension.h" #include "catalog/pg_language.h" #include "catalog/pg_operator.h" #include "catalog/pg_proc.h" #include "catalog/pg_type.h" #include "commands/extension.h" #include "commands/proclang.h" #include "utils/builtins.h" #include "utils/catcache.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/regproc.h" #include "catalog/pg_proc.h" #include "utils/syscache.h" /* * Pre pg 18 doesn't support system cache for pg_extension, * we need to manipulate with table on low level. */ #if PG_VERSION_NUM < 180000 #include "access/genam.h" #include "access/htup_details.h" #include "access/table.h" #include "catalog/indexing.h" #include "utils/rel.h" #endif static Oid plpgsql_check_PLpgSQLlanguageId = InvalidOid; /* * Prepare metadata necessary for plpgsql_check */ void plpgsql_check_get_function_info(plpgsql_check_info *cinfo) { Form_pg_proc proc; char functyptype; proc = (Form_pg_proc) GETSTRUCT(cinfo->proctuple); functyptype = get_typtype(proc->prorettype); cinfo->trigtype = PLPGSQL_NOT_TRIGGER; cinfo->is_procedure = proc->prokind == PROKIND_PROCEDURE; /* * Disallow pseudotype results except TRIGGER, EVENT_TRIGGER, RECORD, * VOID, or polymorphic types. */ if (functyptype == TYPTYPE_PSEUDO) { if (proc->prorettype == TRIGGEROID) cinfo->trigtype = PLPGSQL_DML_TRIGGER; else if (proc->prorettype == EVENT_TRIGGEROID) cinfo->trigtype = PLPGSQL_EVENT_TRIGGER; else if (proc->prorettype != RECORDOID && proc->prorettype != VOIDOID && !IsPolymorphicType(proc->prorettype)) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("PL/pgSQL functions cannot return type %s", format_type_be(proc->prorettype)))); } cinfo->volatility = proc->provolatile; cinfo->rettype = proc->prorettype; } char * plpgsql_check_get_src(HeapTuple procTuple) { Datum prosrcdatum; bool isnull; prosrcdatum = SysCacheGetAttr(PROCOID, procTuple, Anum_pg_proc_prosrc, &isnull); if (isnull) elog(ERROR, "null prosrc"); return TextDatumGetCString(prosrcdatum); } /* * Process necessary checking before code checking * a) disallow other than plpgsql check function, * b) when function is trigger function, then reloid must be defined */ void plpgsql_check_precheck_conditions(plpgsql_check_info *cinfo) { Form_pg_proc proc; char *funcname; proc = (Form_pg_proc) GETSTRUCT(cinfo->proctuple); funcname = format_procedure(cinfo->fn_oid); /* * The plpgsql_check can be loaded by shared_proload_libraries. That means * so in init time the access to system catalog can be impossible. So * plpgsql_check_PLpgSQLlanguageId should be initialized here. */ if (!OidIsValid(plpgsql_check_PLpgSQLlanguageId)) plpgsql_check_PLpgSQLlanguageId = get_language_oid("plpgsql", false); /* used language must be plpgsql */ if (proc->prolang != plpgsql_check_PLpgSQLlanguageId) ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("%s is not a plpgsql function", funcname))); /* profiler doesn't require trigger data check */ if (!cinfo->show_profile) { if ((cinfo->oldtable || cinfo->newtable) && !OidIsValid(cinfo->relid)) ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("missing description of oldtable or newtable"), errhint("Parameter relid is a empty."))); if (cinfo->oldtable && cinfo->newtable && strcmp(cinfo->oldtable, cinfo->newtable) == 0) ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("OLD TABLE name and NEW TABLE name cannot be the same"))); /* dml trigger needs valid relid, others not */ if (cinfo->trigtype == PLPGSQL_DML_TRIGGER) { if (!OidIsValid(cinfo->relid)) ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("missing trigger relation"), errhint("Trigger relation oid must be valid"))); } else { if (OidIsValid(cinfo->relid)) ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("function is not trigger"), errhint("Trigger relation oid must not be valid for non dml trigger function."))); } } pfree(funcname); } #if PG_VERSION_NUM < 160000 /* * plpgsql_check_get_extension_schema - given an extension OID, fetch its extnamespace * * Returns InvalidOid if no such extension. */ static Oid get_extension_schema(Oid ext_oid) { Oid result; Relation rel; SysScanDesc scandesc; HeapTuple tuple; ScanKeyData entry[1]; rel = table_open(ExtensionRelationId, AccessShareLock); ScanKeyInit(&entry[0], Anum_pg_extension_oid, BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(ext_oid)); scandesc = systable_beginscan(rel, ExtensionOidIndexId, true, NULL, 1, entry); tuple = systable_getnext(scandesc); /* We assume that there can be at most one matching tuple */ if (HeapTupleIsValid(tuple)) result = ((Form_pg_extension) GETSTRUCT(tuple))->extnamespace; else result = InvalidOid; systable_endscan(scandesc); table_close(rel, AccessShareLock); return result; } #endif #if PG_VERSION_NUM < 180000 /* * get_extension_version - given an extension OID, look up the version * * Returns a palloc'd string, or NULL if no such extension. */ char * get_extension_version(Oid ext_oid) { char *result; Relation rel; SysScanDesc scandesc; HeapTuple tuple; ScanKeyData entry[1]; rel = table_open(ExtensionRelationId, AccessShareLock); ScanKeyInit(&entry[0], Anum_pg_extension_oid, BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(ext_oid)); scandesc = systable_beginscan(rel, ExtensionOidIndexId, true, NULL, 1, entry); tuple = systable_getnext(scandesc); /* We assume that there can be at most one matching tuple */ if (HeapTupleIsValid(tuple)) { Datum datum; bool isnull; datum = heap_getattr(tuple, Anum_pg_extension_extversion, RelationGetDescr(rel), &isnull); if (isnull) elog(ERROR, "extversion is null"); result = text_to_cstring(DatumGetTextPP(datum)); } else result = NULL; systable_endscan(scandesc); table_close(rel, AccessShareLock); return result; } #else /* * Returns a palloc'd string, or NULL if no such extension. * Use extension syscache from PostgreSQL 18+ */ char * get_extension_version2(Oid ext_oid) { HeapTuple extTuple; Datum extversiondatum; char *result; bool isnull; extTuple = SearchSysCache1(EXTENSIONOID, ObjectIdGetDatum(ext_oid)); if (!HeapTupleIsValid(extTuple)) elog(ERROR, "cache lookup failed for extension %u", ext_oid); extversiondatum = SysCacheGetAttr(EXTENSIONOID, extTuple, Anum_pg_extension_extversion, &isnull); if (isnull) elog(ERROR, "extversion is null"); result = TextDatumGetCString(extversiondatum); ReleaseSysCache(extTuple); return result; } #endif /* * Returns oid of pragma function. It is used for elimination * pragma function from volatility tests. */ Oid plpgsql_check_pragma_func_oid(void) { Oid result = InvalidOid; Oid extoid; extoid = get_extension_oid("plpgsql_check", true); if (OidIsValid(extoid)) { CatCList *catlist; Oid schemaoid; int i; schemaoid = get_extension_schema(extoid); /* Search syscache by name only */ catlist = SearchSysCacheList1(PROCNAMEARGSNSP, CStringGetDatum("plpgsql_check_pragma")); for (i = 0; i < catlist->n_members; i++) { HeapTuple proctup = &catlist->members[i]->tuple; Form_pg_proc procform = (Form_pg_proc) GETSTRUCT(proctup); /* Consider only procs in specified namespace */ if (procform->pronamespace != schemaoid) continue; result = procform->oid; break; } ReleaseSysCacheList(catlist); } return result; } /* * Returns true, if function specified by oid is plpgsql function. */ bool plpgsql_check_is_plpgsql_function(Oid foid) { HeapTuple procTuple; Form_pg_proc procStruct; bool result; procTuple = SearchSysCache1(PROCOID, ObjectIdGetDatum(foid)); if (!HeapTupleIsValid(procTuple)) return false; procStruct = (Form_pg_proc) GETSTRUCT(procTuple); /* * The plpgsql_check can be loaded by shared_proload_libraries. That means * so in init time the access to system catalog can be impossible. So * plpgsql_check_PLpgSQLlanguageId should be initialized here. */ if (!OidIsValid(plpgsql_check_PLpgSQLlanguageId)) plpgsql_check_PLpgSQLlanguageId = get_language_oid("plpgsql", false); result = procStruct->prolang == plpgsql_check_PLpgSQLlanguageId; ReleaseSysCache(procTuple); return result; } /* * plpgsql_check_get_op_namespace * returns the name space of the operator with the given opno */ Oid plpgsql_check_get_op_namespace(Oid opno) { HeapTuple tp; Oid result = InvalidOid; tp = SearchSysCache1(OPEROID, ObjectIdGetDatum(opno)); if (HeapTupleIsValid(tp)) { Form_pg_operator optup = (Form_pg_operator) GETSTRUCT(tp); result = optup->oprnamespace; ReleaseSysCache(tp); } return result; } okbob-plpgsql_check-800db79/src/check_expr.c000066400000000000000000001575301526744060500210540ustar00rootroot00000000000000/*------------------------------------------------------------------------- * * check_expr.c * * routines for enforce plans for every expr/query and * related checks over these plans. * * by Pavel Stehule 2013-2026 * *------------------------------------------------------------------------- */ #include "plpgsql_check.h" #include "access/tupconvert.h" #include "catalog/namespace.h" #include "catalog/pg_proc.h" #include "catalog/pg_type.h" #include "executor/spi_priv.h" #include "nodes/nodeFuncs.h" #include "optimizer/clauses.h" #include "optimizer/optimizer.h" #include "parser/parse_node.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "utils/builtins.h" #include "tcop/utility.h" #include "utils/lsyscache.h" static void collect_volatility(PLpgSQL_checkstate *cstate, Query *query); static Query *ExprGetQuery(PLpgSQL_checkstate *cstate, PLpgSQL_expr *expr, CachedPlanSource *plansource); static void check_pure_expr(PLpgSQL_checkstate *cstate, Query *query, char *query_str); static CachedPlan *get_cached_plan(PLpgSQL_checkstate *cstate, PLpgSQL_expr *expr, bool *has_result_desc); static void plan_checks(PLpgSQL_checkstate *cstate, CachedPlan *cplan, char *query_str); static void prohibit_write_plan(PLpgSQL_checkstate *cstate, CachedPlan *cplan, char *query_str); static void prohibit_transaction_stmt(PLpgSQL_checkstate *cstate, CachedPlan *cplan, char *query_str); static void check_fishy_qual(PLpgSQL_checkstate *cstate, CachedPlan *cplan, char *query_str); static Const *expr_get_const(PLpgSQL_checkstate *cstate, PLpgSQL_expr *expr); static bool is_const_null_expr(PLpgSQL_checkstate *cstate, PLpgSQL_expr *expr); static void force_plan_checks(PLpgSQL_checkstate *cstate, PLpgSQL_expr *expr); static bool is_pure_expr(PLpgSQL_checkstate *cstate, Query *query); static int RowGetValidFields(PLpgSQL_row *row); static int TupleDescNVatts(TupleDesc tupdesc); static PreParseColumnRefHook p_pre_columnref_hook = NULL; static PostParseColumnRefHook p_post_columnref_hook = NULL; static ParseParamRefHook p_paramref_hook = NULL; /* * Following routines wraps plpgsql sql parser's hooks. These wrappers * are used for identification of not used variables. Identification based * on plan postprocessing can raise false alarms when plan is not created * due some errors. */ static void parserhook_wrapper_update_used_variables(ParseState *pstate, Node *node) { if (node && IsA(node, Param)) { Param *p = (Param *) node; if (p->paramkind == PARAM_EXTERN) { PLpgSQL_checkstate *cstate; PLpgSQL_expr *expr; int dno = p->paramid - 1; expr = (PLpgSQL_expr *) pstate->p_ref_hook_state; Assert(expr); cstate = expr->func->cur_estate->plugin_info; if (!cstate || cstate->ci_magic != CI_MAGIC) return; if (bms_is_member(dno, expr->paramnos)) { /* * PostgreSQL 14 and higher uses SQL parser for parsing left * side of assign statement too. We should to eliminate it * from used_variables (read) if we want to get warning NEVER * READ. */ if (dno != expr->target_param) { MemoryContext oldcxt = MemoryContextSwitchTo(cstate->check_cxt); cstate->used_variables = bms_add_member(cstate->used_variables, dno); MemoryContextSwitchTo(oldcxt); } } } } } static Node * pre_column_ref(ParseState *pstate, ColumnRef *cref) { Node *result; result = p_pre_columnref_hook(pstate, cref); parserhook_wrapper_update_used_variables(pstate, result); return result; } static Node * post_column_ref(ParseState *pstate, ColumnRef *cref, Node *var) { Node *result; result = p_post_columnref_hook(pstate, cref, var); parserhook_wrapper_update_used_variables(pstate, result); return result; } static Node * param_ref(ParseState *pstate, ParamRef *pref) { Node *result; result = p_paramref_hook(pstate, pref); parserhook_wrapper_update_used_variables(pstate, result); return result; } static void plpgsql_parser_setup_wrapper(struct ParseState *pstate, PLpgSQL_expr *expr) { plpgsql_check__parser_setup_p(pstate, expr); /* * save previous hooks. * * The prepare_plan routine can be called recursively in passive mode. the * planner can try to execute any immutable functions with constant * parameters. Then saving old hooks to global variables is not 100% * correct, but the addresses should be same always. */ p_pre_columnref_hook = pstate->p_pre_columnref_hook; p_post_columnref_hook = pstate->p_post_columnref_hook; p_paramref_hook = pstate->p_paramref_hook; /* set new hooks */ pstate->p_pre_columnref_hook = pre_column_ref; pstate->p_post_columnref_hook = post_column_ref; pstate->p_paramref_hook = param_ref; } static void _prepare_plan(PLpgSQL_checkstate *cstate, PLpgSQL_expr *expr, int cursorOptions, ParserSetupHook parser_setup, void *arg) { SPIPlanPtr plan; if (expr->plan == NULL) { MemoryContext old_cxt; void *old_plugin_info; SPIPrepareOptions options; memset(&options, 0, sizeof(options)); options.parserSetup = parser_setup ? parser_setup : (ParserSetupHook) plpgsql_parser_setup_wrapper; options.parserSetupArg = arg ? arg : (void *) expr; options.parseMode = expr->parseMode; options.cursorOptions = cursorOptions; /* * The grammar can't conveniently set expr->func while building the * parse tree, so make sure it's set before parser hooks need it. */ expr->func = cstate->estate->func; old_plugin_info = expr->func->cur_estate->plugin_info; expr->func->cur_estate->plugin_info = cstate; PG_TRY(); { /* * Generate and save the plan */ plan = SPI_prepare_extended(expr->query, &options); expr->func->cur_estate->plugin_info = old_plugin_info; } PG_CATCH(); { expr->func->cur_estate->plugin_info = old_plugin_info; PG_RE_THROW(); } PG_END_TRY(); if (plan == NULL) { /* Some SPI errors deserve specific error messages */ switch (SPI_result) { case SPI_ERROR_COPY: ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("cannot COPY to/from client in PL/pgSQL"))); break; case SPI_ERROR_TRANSACTION: ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("cannot begin/end transactions in PL/pgSQL"), errhint("Use a BEGIN block with an EXCEPTION clause instead."))); break; default: elog(ERROR, "SPI_prepare_params failed for \"%s\": %s", expr->query, SPI_result_code_string(SPI_result)); } } /* * Save prepared plan to plpgsql_check state context. It will be * released on end of check, and it should be valid to this time. */ old_cxt = MemoryContextSwitchTo(cstate->check_cxt); expr->plan = SPI_saveplan(plan); /* This plan should be released later */ cstate->exprs = lappend(cstate->exprs, expr); MemoryContextSwitchTo(old_cxt); SPI_freeplan(plan); } } /* * Generate a prepared plan - this is simplified copy from pl_exec.c Is not * necessary to check simple plan. The planning is forced when plancache is * not valid. */ static void prepare_plan(PLpgSQL_checkstate *cstate, PLpgSQL_expr *expr, int cursorOptions, ParserSetupHook parser_setup, void *arg, bool is_expression) { Query *query; CachedPlanSource *plansource = NULL; bool is_valid; do { _prepare_plan(cstate, expr, cursorOptions, parser_setup, arg); plansource = plpgsql_check_get_plan_source(cstate, expr->plan); if (!plansource) return; is_valid = plansource->is_valid; if (!is_valid) { /* replan plan because it is invalid */ cstate->exprs = list_delete(cstate->exprs, expr->plan); SPI_freeplan(expr->plan); expr->plan = NULL; } } while (!is_valid); if (list_length(expr->plan->plancache_list) > 1) { ListCell *lc; /* * executed as EXECUTE ' ...; ...' * * in this case plpgsql_check_get_plan_source returns last subplan. * But we can collect volatility for all previous subplans. */ Assert(cstate->allow_mp && cstate->is_dynsql); foreach(lc, expr->plan->plancache_list) { CachedPlanSource *plansourcesub = (CachedPlanSource *) lfirst(lc); if (plansourcesub != (CachedPlanSource *) llast(expr->plan->plancache_list)) { CachedPlan *cplan; query = ExprGetQuery(cstate, expr, plansourcesub); if (query) { plpgsql_check_funcexpr(cstate, query, expr->query); collect_volatility(cstate, query); plpgsql_check_detect_dependency(cstate, query); } cplan = GetCachedPlan(plansourcesub, NULL, CurrentResourceOwner, NULL); prohibit_write_plan(cstate, cplan, expr->query); /* disallow BEGIN TRANS, COMMIT, ROLLBACK, .. */ prohibit_transaction_stmt(cstate, cplan, expr->query); ReleaseCachedPlan(cplan, CurrentResourceOwner); } } } query = ExprGetQuery(cstate, expr, plansource); if (!query) return; /* there checks are common on every expr/query */ plpgsql_check_funcexpr(cstate, query, expr->query); collect_volatility(cstate, query); plpgsql_check_detect_dependency(cstate, query); if (is_expression) check_pure_expr(cstate, query, expr->query); else { if (cstate->estate && cstate->cinfo->performance_warnings) { PLpgSQL_stmt *stmt = cstate->estate->err_stmt; if (stmt) { bool have_into = false; PLpgSQL_variable *target; if (stmt->cmd_type == PLPGSQL_STMT_EXECSQL) { have_into = ((PLpgSQL_stmt_execsql *) stmt)->into; target = ((PLpgSQL_stmt_execsql *) stmt)->target; if (have_into && target->dtype == PLPGSQL_DTYPE_ROW) { PLpgSQL_row *row = (PLpgSQL_row *) target; if (row->nfields == 1 && plpgsql_check_is_internal_variable(cstate, target)) { if (is_pure_expr(cstate, query)) { plpgsql_check_put_error(cstate, 0, 0, "detected \"SELECT expr INTO variable\"", "This obsolete syntax of assigning can be slow.", "Use syntax \"variable := expr\" instead.", PLPGSQL_CHECK_WARNING_PERFORMANCE, 0, expr->query, NULL); } } } } } } } } /* * Prepare a plan for expression without any additional checks. Used by * the table pragmas generator, where only successful planning of the * statement is interesting. */ void plch_expr_prepare_plan(PLpgSQL_checkstate *cstate, PLpgSQL_expr *expr) { prepare_plan(cstate, expr, 0, NULL, NULL, false); } static const char * volatility_to_string(const char volatility) { switch (volatility) { case PROVOLATILE_VOLATILE: return "VOLATILE"; case PROVOLATILE_STABLE: return "STABLE"; case PROVOLATILE_IMMUTABLE: return "IMMUTABLE"; default: elog(ERROR, "unknown volatility '%c'", volatility); } } /* * Update function's volatility flag by query */ static void collect_volatility(PLpgSQL_checkstate *cstate, Query *query) { bool nowarning = false; if (cstate->skip_volatility_check) return; if ((cstate->cinfo->performance_warnings && cstate->volatility != PROVOLATILE_VOLATILE) || (cstate->cinfo->other_warnings)) { char curr_volatility; if (query->commandType == CMD_SELECT) { if (!query->hasModifyingCTE && !query->hasForUpdate) { /* there is chance so query will be immutable */ if (plpgsql_check_contain_volatile_functions((Node *) query, cstate)) curr_volatility = PROVOLATILE_VOLATILE; else if (!plpgsql_check_contain_mutable_functions((Node *) query, cstate)) { if (plpgsql_check_has_rtable(query)) curr_volatility = PROVOLATILE_STABLE; else curr_volatility = PROVOLATILE_IMMUTABLE; } else curr_volatility = PROVOLATILE_STABLE; } else { curr_volatility = PROVOLATILE_VOLATILE; } } else { /* not read only statements requare VOLATILE flag */ curr_volatility = PROVOLATILE_VOLATILE; /* * In this case the error will be raised when function is not * volatile, so we don't want to raise redundant warning. */ nowarning = true; } if (cstate->cinfo->other_warnings && !nowarning) { const char *volatility_name = NULL; const char *func_volatility_name = NULL; if (curr_volatility != PROVOLATILE_IMMUTABLE && cstate->decl_volatility == PROVOLATILE_IMMUTABLE) { volatility_name = volatility_to_string(curr_volatility); func_volatility_name = "IMMUTABLE"; } else if (curr_volatility == PROVOLATILE_VOLATILE && cstate->decl_volatility == PROVOLATILE_STABLE) { volatility_name = "VOLATILE"; func_volatility_name = "STABLE"; } if (volatility_name) { StringInfoData message; initStringInfo(&message); appendStringInfo(&message, "routine is marked as %s, but expression is %s", func_volatility_name, volatility_name); plpgsql_check_put_error(cstate, 0, -1, message.data, "there is a risk of unwanted behave", "When you fix this issue, please, recheck other functions that uses this function.", PLPGSQL_CHECK_WARNING_OTHERS, 0, NULL, NULL); pfree(message.data); message.data = NULL; } } if (cstate->volatility == PROVOLATILE_IMMUTABLE) cstate->volatility = curr_volatility; else if (cstate->volatility == PROVOLATILE_STABLE && curr_volatility == PROVOLATILE_VOLATILE) cstate->volatility = curr_volatility; } } /* * Validate plan and returns related node. */ CachedPlanSource * plpgsql_check_get_plan_source(PLpgSQL_checkstate *cstate, SPIPlanPtr plan) { CachedPlanSource *plansource = NULL; int nplans; if (plan == NULL || plan->magic != _SPI_PLAN_MAGIC) elog(ERROR, "cached plan is not valid plan"); nplans = list_length(plan->plancache_list); if (nplans > 1) { /* * We can allow multiple plans for commands executed by EXECUTE * command. Result of last plan is result. But it can be allowed only * in main query - not in parameters. */ if (cstate->allow_mp) { /* take last */ plansource = (CachedPlanSource *) llast(plan->plancache_list); cstate->found_mp = true; } else elog(ERROR, "plan is not single execution plany"); } else if (nplans == 1) plansource = (CachedPlanSource *) linitial(plan->plancache_list); return plansource; } /* * Check if query holds just an expression * */ static bool is_pure_expr(PLpgSQL_checkstate *cstate, Query *query) { Node *n; /* only restarget can be assigned in pure expression */ if (query->rtable) return false; if (query->distinctClause) return false; if (query->groupClause) return false; if (query->havingQual) return false; if (!query->targetList) return false; if (list_length(query->targetList) > 1) return false; n = (Node *) linitial(query->targetList); if (!IsA(n, TargetEntry)) return false; /* * unfortunately, the resname should not be checked, postgres uses * ?column?, varname, or type names, ... */ return true; } static void check_pure_expr(PLpgSQL_checkstate *cstate, Query *query, char *query_str) { if (!cstate->cinfo->extra_warnings) return; if (!is_pure_expr(cstate, query)) { plpgsql_check_put_error(cstate, ERRCODE_SYNTAX_ERROR, 0, "expression is not pure expression", "there is a possibility of unwanted behave", "Maybe you forgot to use a semicolon.", PLPGSQL_CHECK_WARNING_EXTRA, exprLocation((Node *) query->targetList), query_str, NULL); } } /* * Process plpgsql_check pragmas written inside the checked function's * body. The pragma can be entered as a plpgsql_check_pragma function * call with constant string arguments, or (only in PERFORM statements) * as a string constant with the "PRAGMA:" prefix. Returns true, when * the statement was recognized as a pragma. */ bool plch_apply_inline_pragmas(PLpgSQL_checkstate *cstate, SelectStmt *selectStmt, PLpgSQL_nsitem *ns, int lineno, bool is_perform_stmt) { bool was_pragma = false; if (selectStmt->targetList && IsA(linitial(selectStmt->targetList), ResTarget)) { ResTarget *rt = (ResTarget *) linitial(selectStmt->targetList); if (rt->val && IsA(rt->val, A_Const)) { A_Const *ac = (A_Const *) rt->val; char *str = NULL; #if PG_VERSION_NUM < 150000 if (ac->val.type == T_String) str = strVal(&(ac->val)); #else if (!ac->isnull && IsA(&ac->val, String)) str = strVal(&(ac->val)); #endif if (str && is_perform_stmt) { while (*str == ' ') str++; if (strncasecmp(str, "pragma:", 7) == 0) { was_pragma = true; plpgsql_check_pragma_apply(cstate, str + 7, ns, lineno); } } } else if (rt->val && IsA(rt->val, FuncCall)) { char *funcname; char *schemaname; FuncCall *fc = (FuncCall *) rt->val; DeconstructQualifiedName(fc->funcname, &schemaname, &funcname); if (strcmp(funcname, "plpgsql_check_pragma") == 0) { ListCell *lc; was_pragma = true; foreach(lc, fc->args) { Node *arg = (Node *) lfirst(lc); if (IsA(arg, A_Const)) { A_Const *ac = (A_Const *) arg; #if PG_VERSION_NUM < 150000 if (ac->val.type == T_String) plpgsql_check_pragma_apply(cstate, strVal(&(ac->val)), ns, lineno); #else if (!ac->isnull && IsA(&ac->val, String)) plpgsql_check_pragma_apply(cstate, strVal(&(ac->val)), ns, lineno); #endif } } } } } return was_pragma; } /* * Returns Query node for expression * */ static Query * ExprGetQuery(PLpgSQL_checkstate *cstate, PLpgSQL_expr *expr, CachedPlanSource *plansource) { Query *result = NULL; Assert(plansource); Assert(plansource->is_valid); if (!plansource->query_list) elog(ERROR, "missing plan in plancache source"); /* * query_list has more fields, when rules are used. There can be * combination INSERT; NOTIFY */ if (list_length(plansource->query_list) > 1) { ListCell *lc; CmdType first_ctype = CMD_UNKNOWN; bool first = true; foreach(lc, plansource->query_list) { Query *query = (Query *) lfirst(lc); if (first) { first = false; first_ctype = query->commandType; result = query; } else { /* * When current command is SELECT, then first command should * be SELECT too */ if (query->commandType == CMD_SELECT) { if (first_ctype != CMD_SELECT) ereport(ERROR, (errmsg("there is not single query"), errdetail("plpgsql_check cannot detect result type"), errhint("Probably there are some unsupported (by plpgsql_check) rules on related tables"))); result = query; } } } } else result = linitial(plansource->query_list); cstate->was_pragma = false; /* the test of PRAGMA function call */ if (result->commandType == CMD_SELECT) { if (plansource->raw_parse_tree && plansource->raw_parse_tree->stmt && IsA(plansource->raw_parse_tree->stmt, SelectStmt)) { bool is_perform_stmt; int lineno = 0; is_perform_stmt = (cstate->estate && cstate->estate->err_stmt && cstate->estate->err_stmt->cmd_type == PLPGSQL_STMT_PERFORM); if (cstate->estate && cstate->estate->err_stmt) lineno = cstate->estate->err_stmt->lineno; cstate->was_pragma = plch_apply_inline_pragmas(cstate, (SelectStmt *) plansource->raw_parse_tree->stmt, expr->ns, lineno, is_perform_stmt); } } return result; } /* * Operations that requires cached plan * */ /* * Returns cached plan from plan cache. * */ static CachedPlan * get_cached_plan(PLpgSQL_checkstate *cstate, PLpgSQL_expr *expr, bool *has_result_desc) { CachedPlanSource *plansource = NULL; CachedPlan *cplan; plansource = plpgsql_check_get_plan_source(cstate, expr->plan); if (!plansource) { *has_result_desc = false; return NULL; } *has_result_desc = plansource->resultDesc ? true : false; cplan = GetCachedPlan(plansource, NULL, CurrentResourceOwner, NULL); return cplan; } /* * Process common checks on cached plan * */ static void plan_checks(PLpgSQL_checkstate *cstate, CachedPlan *cplan, char *query_str) { /* disallow write op in read only function */ prohibit_write_plan(cstate, cplan, query_str); /* detect bad casts in quals */ check_fishy_qual(cstate, cplan, query_str); /* disallow BEGIN TRANS, COMMIT, ROLLBACK, .. */ prohibit_transaction_stmt(cstate, cplan, query_str); } /* * Raise a error when plan is not read only */ static void prohibit_write_plan(PLpgSQL_checkstate *cstate, CachedPlan *cplan, char *query_str) { ListCell *lc; if (!cstate->estate->readonly_func) return; foreach(lc, cplan->stmt_list) { PlannedStmt *pstmt = (PlannedStmt *) lfirst(lc); if (!CommandIsReadOnly(pstmt)) { StringInfoData message; initStringInfo(&message); appendStringInfo(&message, "%s is not allowed in a non volatile function", GetCommandTagName(CreateCommandTag((Node *) pstmt))); plpgsql_check_put_error(cstate, ERRCODE_FEATURE_NOT_SUPPORTED, 0, message.data, NULL, NULL, PLPGSQL_CHECK_ERROR, 0, query_str, NULL); pfree(message.data); message.data = NULL; } } } /* * Raise a error when plan is a transactional statement */ static void prohibit_transaction_stmt(PLpgSQL_checkstate *cstate, CachedPlan *cplan, char *query_str) { ListCell *lc; foreach(lc, cplan->stmt_list) { Node *pstmt = (Node *) lfirst(lc); /* PostgtreSQL 10 can have one level of nesting more */ if (IsA(pstmt, PlannedStmt)) { PlannedStmt *planstmt = (PlannedStmt *) pstmt; if (planstmt->commandType == CMD_UTILITY) pstmt = (Node *) planstmt->utilityStmt; } if (IsA(pstmt, TransactionStmt)) { plpgsql_check_put_error(cstate, ERRCODE_FEATURE_NOT_SUPPORTED, 0, "cannot begin/end transactions in PL/pgSQL", NULL, "Use a BEGIN block with an EXCEPTION clause instead.", PLPGSQL_CHECK_ERROR, 0, query_str, NULL); } } } /* * Raise a performance warning when plan hash fishy qual */ static void check_fishy_qual(PLpgSQL_checkstate *cstate, CachedPlan *cplan, char *query_str) { ListCell *lc; if (!cstate->cinfo->performance_warnings) return; foreach(lc, cplan->stmt_list) { Param *param; PlannedStmt *pstmt = (PlannedStmt *) lfirst(lc); Plan *plan = NULL; /* Only plans can contains fishy quals */ if (!IsA(pstmt, PlannedStmt)) continue; plan = pstmt->planTree; if (plpgsql_check_qual_has_fishy_cast(pstmt, plan, ¶m)) { plpgsql_check_put_error(cstate, ERRCODE_DATATYPE_MISMATCH, 0, "implicit cast of attribute caused by different PLpgSQL variable type in WHERE clause", "An index of some attribute cannot be used, when variable, used in predicate, has not right type like a attribute", "Check a variable type - int versus numeric", PLPGSQL_CHECK_WARNING_PERFORMANCE, param->location, query_str, NULL); } } } Node * plpgsql_check_expr_get_node(PLpgSQL_checkstate *cstate, PLpgSQL_expr *expr, bool force_plan_checks) { PlannedStmt *_stmt; CachedPlan *cplan; Node *result = NULL; bool has_result_desc; cplan = get_cached_plan(cstate, expr, &has_result_desc); if (!has_result_desc) elog(ERROR, "expression does not return data"); /* do all checks for this plan, reduce a access to plan cache */ if (force_plan_checks) plan_checks(cstate, cplan, expr->query); _stmt = (PlannedStmt *) linitial(cplan->stmt_list); if (has_result_desc && IsA(_stmt, PlannedStmt) && _stmt->commandType == CMD_SELECT) { Plan *_plan; _plan = _stmt->planTree; if ((IsA(_plan, Result) || IsA(_plan, ProjectSet)) && list_length(_plan->targetlist) == 1) { TargetEntry *tle; tle = (TargetEntry *) linitial(_plan->targetlist); /* * plpgsql_check in active mode doesn't generate one shot plans, * so there should not be a risk of memory releasing plan context * by ReleaseCachedPlan, and poisoning memory referenced by result * pointer. On second hand, return direct pointer is dirty (when * plan is "released" immediately. Using copyObject is probabably * not necessary, but it is more robust, safe and reduces possible * bugs in future. */ result = copyObject((Node *) tle->expr); } } ReleaseCachedPlan(cplan, CurrentResourceOwner); return result; } /* * Returns Const Value from expression if it is possible. * * Ensure all plan related checks on expression. * */ static Const * expr_get_const(PLpgSQL_checkstate *cstate, PLpgSQL_expr *expr) { Node *node = plpgsql_check_expr_get_node(cstate, expr, true); if (node && node->type == T_Const) return (Const *) node; return NULL; } /* * Returns true, when expr is constant NULL * */ static bool is_const_null_expr(PLpgSQL_checkstate *cstate, PLpgSQL_expr *expr) { Const *c; c = expr_get_const(cstate, expr); return c && c->constisnull ? true : false; } char * plpgsql_check_const_to_string(Node *node, int *location) { if (IsA(node, Const)) { Const *c = (Const *) node; if (location) *location = c->location; if (!c->constisnull) { Oid typoutput; bool typisvarlena; getTypeOutputInfo(c->consttype, &typoutput, &typisvarlena); return OidOutputFunctionCall(typoutput, c->constvalue); } } return NULL; } static char * plpgsql_check_get_tracked_const(PLpgSQL_checkstate *cstate, Node *node) { if (!cstate->strconstvars) return NULL; if (cstate->pragma_vector.disable_constants_tracing) return NULL; if (!cstate->is_dynsql && IsA(node, Param)) { Param *p = (Param *) node; if (p->paramkind == PARAM_EXTERN && p->paramid > 0 && p->location != -1) { int dno = p->paramid - 1; /* * The paramid is calculated by PLpgSQL compiler. And then should * should be correct. User has not possibility to change paramid * in static queries (dynamic queries are different case, but its * are prohibited in this path). */ if (cstate->strconstvars[dno]) return cstate->strconstvars[dno]; } } else if (IsA(node, CoerceViaIO)) { CoerceViaIO *coerce = (CoerceViaIO *) node; bool typispreferred; char typcategory; get_type_category_preferred(coerce->resulttype, &typcategory, &typispreferred); if (typcategory == 'S') return plpgsql_check_get_tracked_const(cstate, (Node *) coerce->arg); } return NULL; } char * plpgsql_check_get_const_string(PLpgSQL_checkstate *cstate, Node *node, int *location) { char *str; if (location) *location = -1; str = plpgsql_check_const_to_string(node, location); if (!str) str = plpgsql_check_get_tracked_const(cstate, node); return str; } /* * Returns string for any not null constant. isnull is true, * when constant is null. * */ char * plpgsql_check_expr_get_string(PLpgSQL_checkstate *cstate, PLpgSQL_expr *expr, int *location) { Node *node = plpgsql_check_expr_get_node(cstate, expr, true); if (!node) return NULL; return plpgsql_check_get_const_string(cstate, node, location); } static void force_plan_checks(PLpgSQL_checkstate *cstate, PLpgSQL_expr *expr) { CachedPlan *cplan; bool has_result_desc; cplan = get_cached_plan(cstate, expr, &has_result_desc); if (!cplan) return; /* do all checks for this plan, reduce a access to plan cache */ plan_checks(cstate, cplan, expr->query); ReleaseCachedPlan(cplan, CurrentResourceOwner); } /* * No casts, no other checks * */ void plpgsql_check_expr_generic(PLpgSQL_checkstate *cstate, PLpgSQL_expr *expr) { prepare_plan(cstate, expr, 0, NULL, NULL, true); force_plan_checks(cstate, expr); } void plpgsql_check_expr_generic_with_parser_setup(PLpgSQL_checkstate *cstate, PLpgSQL_expr *expr, ParserSetupHook parser_setup, void *arg) { prepare_plan(cstate, expr, 0, parser_setup, arg, false); force_plan_checks(cstate, expr); } /* * Top level checks - forces prepare_plan, protected by subtransaction. * */ /* * Verify to possible cast to bool, integer, .. * */ void plpgsql_check_expr_with_scalar_type(PLpgSQL_checkstate *cstate, PLpgSQL_expr *expr, Oid expected_typoid, bool required) { ResourceOwner oldowner; MemoryContext oldCxt = CurrentMemoryContext; if (!expr) { if (required) ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR), errmsg("required expression is empty"))); return; } oldowner = CurrentResourceOwner; BeginInternalSubTransaction(NULL); MemoryContextSwitchTo(oldCxt); PG_TRY(); { TupleDesc tupdesc; bool is_immutable_null; prepare_plan(cstate, expr, 0, NULL, NULL, true); /* record all variables used by the query */ cstate->used_variables = bms_add_members(cstate->used_variables, expr->paramnos); tupdesc = plpgsql_check_expr_get_desc(cstate, expr, false, true, true, NULL); is_immutable_null = is_const_null_expr(cstate, expr); if (tupdesc) { /* when we know value or type */ if (!is_immutable_null) { if (tupdesc->natts != 1) ereport(ERROR, (errcode(ERRCODE_DATATYPE_MISMATCH), errmsg("unexpected non-scalar value"))); plpgsql_check_assign_to_target_type(cstate, expected_typoid, -1, TupleDescAttr(tupdesc, 0)->atttypid, is_immutable_null, -1); } ReleaseTupleDesc(tupdesc); } ReleaseCurrentSubTransaction(); MemoryContextSwitchTo(oldCxt); CurrentResourceOwner = oldowner; } PG_CATCH(); { ErrorData *edata; MemoryContextSwitchTo(oldCxt); edata = CopyErrorData(); FlushErrorState(); RollbackAndReleaseCurrentSubTransaction(); MemoryContextSwitchTo(oldCxt); CurrentResourceOwner = oldowner; /* * If fatal_errors is true, we just propagate the error up to the * highest level. Otherwise the error is appended to our current list * of errors, and we continue checking. */ if (cstate->cinfo->fatal_errors) ReThrowError(edata); plpgsql_check_put_error_edata(cstate, edata); FreeErrorData(edata); MemoryContextSwitchTo(oldCxt); } PG_END_TRY(); } /* * Checks used for RETURN QUERY * */ void plpgsql_check_returned_expr_with_parser_setup(PLpgSQL_checkstate *cstate, PLpgSQL_expr *expr, bool is_expression, ParserSetupHook parser_setup, void *arg) { PLpgSQL_execstate *estate = cstate->estate; PLpgSQL_function *func = estate->func; bool is_return_query = !is_expression; ResourceOwner oldowner; MemoryContext oldCxt = CurrentMemoryContext; oldowner = CurrentResourceOwner; BeginInternalSubTransaction(NULL); MemoryContextSwitchTo(oldCxt); PG_TRY(); { TupleDesc tupdesc; bool is_immutable_null; Oid first_level_typ = InvalidOid; prepare_plan(cstate, expr, 0, parser_setup, arg, is_expression); /* * record all variables used by the query, should be after * prepare_plan. Note: expr->paramnos should be empty for dynamic * queries. */ cstate->used_variables = bms_add_members(cstate->used_variables, expr->paramnos); tupdesc = plpgsql_check_expr_get_desc(cstate, expr, false, is_expression, is_expression, &first_level_typ); is_immutable_null = is_const_null_expr(cstate, expr); /* try to identify obsolete return refcursor's value */ if (cstate->estate->func->fn_rettype == REFCURSOROID && cstate->cinfo->compatibility_warnings) { Node *node = plpgsql_check_expr_get_node(cstate, expr, false); bool is_ok = true; if (node && IsA((Node *) node, Const)) { /* only NULL constant argument is ok */ if (!((Const *) node)->constisnull) is_ok = false; } else if (node && IsA((Node *) node, Param)) { /* only variable of refcursor is ok */ if (((Param *) node)->paramtype != REFCURSOROID) is_ok = false; } else is_ok = false; if (!is_ok) plpgsql_check_put_error(cstate, 0, 0, "obsolete setting of refcursor or cursor variable", "Internal name of cursor should not be specified by users.", NULL, PLPGSQL_CHECK_WARNING_COMPATIBILITY, 0, NULL, NULL); } if (!func->fn_retistuple && is_expression && type_is_rowtype(first_level_typ)) { /* * PL/pgSQL try to use IO conversion, so we are not sure, how this * conversion is strict. But We can be sure so any composite * cannot be converted to any numeric. */ if (!is_immutable_null) { bool typispreferred; char typcategory; Oid base_rettype; base_rettype = getBaseType(func->fn_rettype); get_type_category_preferred(base_rettype, &typcategory, &typispreferred); /* * when source type is composite, and target is numeric, then * there is a high probability of some error. */ if (typcategory == 'N') plpgsql_check_put_error(cstate, ERRCODE_DATATYPE_MISMATCH, 0, "cannot return composite value from function returning number", NULL, NULL, PLPGSQL_CHECK_ERROR, 0, NULL, NULL); } } if (tupdesc && tupdesc->natts > 0) { /* enforce check for trigger function - result must be composit */ if (func->fn_retistuple && is_expression && !(type_is_rowtype(TupleDescAttr(tupdesc, 0)->atttypid) || type_is_rowtype(first_level_typ) || tupdesc->natts > 1)) { /* but we should to allow NULL */ if (!is_immutable_null) plpgsql_check_put_error(cstate, ERRCODE_DATATYPE_MISMATCH, 0, "cannot return non-composite value from function returning composite type", NULL, NULL, PLPGSQL_CHECK_ERROR, 0, NULL, NULL); } /* * tupmap is used when function returns tuple or RETURN QUERY was * used */ else if (func->fn_retistuple || is_return_query) { /* should to know expected result */ if (!cstate->fake_rtd && estate->rsi && IsA(estate->rsi, ReturnSetInfo)) { TupleDesc rettupdesc = estate->rsi->expectedDesc; TupleConversionMap *tupmap; tupmap = convert_tuples_by_position(tupdesc, rettupdesc, !is_expression ? gettext_noop("structure of query does not match function result type") : gettext_noop("returned record type does not match expected record type")); if (tupmap) free_conversion_map(tupmap); } } else { /* returns scalar */ if (!IsPolymorphicType(func->fn_rettype)) { plpgsql_check_assign_to_target_type(cstate, func->fn_rettype, -1, TupleDescAttr(tupdesc, 0)->atttypid, is_immutable_null, -1); } } ReleaseTupleDesc(tupdesc); } ReleaseCurrentSubTransaction(); MemoryContextSwitchTo(oldCxt); CurrentResourceOwner = oldowner; } PG_CATCH(); { ErrorData *edata; MemoryContextSwitchTo(oldCxt); edata = CopyErrorData(); FlushErrorState(); RollbackAndReleaseCurrentSubTransaction(); MemoryContextSwitchTo(oldCxt); CurrentResourceOwner = oldowner; /* * If fatal_errors is true, we just propagate the error up to the * highest level. Otherwise the error is appended to our current list * of errors, and we continue checking. */ if (cstate->cinfo->fatal_errors) ReThrowError(edata); plpgsql_check_put_error_edata(cstate, edata); FreeErrorData(edata); MemoryContextSwitchTo(oldCxt); } PG_END_TRY(); } void plpgsql_check_returned_expr(PLpgSQL_checkstate *cstate, PLpgSQL_expr *expr, bool is_expression) { plpgsql_check_returned_expr_with_parser_setup(cstate, expr, is_expression, NULL, NULL); } /* * Release all string constants related to variables from row variable */ static void free_string_constant(PLpgSQL_checkstate *cstate, PLpgSQL_row *row) { int fnum; for (fnum = 0; fnum < row->nfields; fnum++) { PLpgSQL_datum *field; int varno = row->varnos[fnum]; /* skip dropped fields */ if (varno < 0) continue; /* the assigned value is not constant, reset current */ if (cstate->strconstvars && cstate->strconstvars[varno]) { pfree(cstate->strconstvars[varno]); cstate->strconstvars[varno] = NULL; } field = cstate->estate->datums[varno]; if (field->dtype == PLPGSQL_DTYPE_ROW) free_string_constant(cstate, (PLpgSQL_row *) field); } } /* * Check expression as rvalue - on right in assign statement. It is used for * only expression check too - when target is unknown. * */ void plpgsql_check_expr_as_rvalue_with_parser_setup(PLpgSQL_checkstate *cstate, PLpgSQL_expr *expr, PLpgSQL_rec *targetrec, PLpgSQL_row *targetrow, int targetdno, bool use_element_type, bool is_expression, ParserSetupHook parser_setup, void *arg) { ResourceOwner oldowner; MemoryContext oldCxt = CurrentMemoryContext; TupleDesc tupdesc; bool is_immutable_null; volatile bool expand = true; Oid first_level_typoid; Oid expected_typoid = InvalidOid; int expected_typmod = InvalidOid; if (targetdno != -1) { plpgsql_check_target(cstate, targetdno, &expected_typoid, &expected_typmod); /* * When target variable is not composite, then we should not expand * result tupdesc. */ if (!type_is_rowtype(expected_typoid)) expand = false; expr->target_param = targetdno; } else expr->target_param = -1; /* * SELECT INTO for composite target type doesn't do expand. */ if (targetrec || targetrow) { if (cstate->estate) { PLpgSQL_stmt *stmt = cstate->estate->err_stmt; if (stmt && (stmt->cmd_type == PLPGSQL_STMT_EXECSQL || stmt->cmd_type == PLPGSQL_STMT_DYNEXECUTE || stmt->cmd_type == PLPGSQL_STMT_CALL)) { expand = false; } } } oldowner = CurrentResourceOwner; BeginInternalSubTransaction(NULL); MemoryContextSwitchTo(oldCxt); PG_TRY(); { char *local_err_text; bool free_local_err_text; if (cstate->estate->err_text) { local_err_text = (char *) cstate->estate->err_text; free_local_err_text = false; } else if (cstate->estate->err_stmt) { local_err_text = plpgsql_check_prepare_err_text_with_target_vardecl(cstate, cstate->estate->err_stmt, -1); free_local_err_text = local_err_text != NULL; } else { local_err_text = NULL; free_local_err_text = false; } prepare_plan(cstate, expr, 0, parser_setup, arg, is_expression); /* record all variables used by the query */ if (expr->target_param != -1) { int target_dno = expr->target_param; Node *node; Oid target_typoid = InvalidOid; Oid value_typoid = InvalidOid; node = plpgsql_check_expr_get_node(cstate, expr, false); if (node && exprType((Node *) node) != expected_typoid) { char *str; /* When expr is constant string, try it cast to target type */ str = plpgsql_check_get_const_string(cstate, node, NULL); if (str) { Oid expr_typoid = exprType((Node *) node); if (!type_is_rowtype(expr_typoid) && !type_is_rowtype(expected_typoid)) { Oid infunc; Oid intypeioparam; Oid typeid; typeid = use_element_type ? get_array_type(expected_typoid) : expected_typoid; /* * Assignment casts, including element casts in an * array, need not agree with textual input. * * get_array_type() returns InvalidOid when no array * type exists for expected_typoid - for instance when * the loop variable of a FOREACH ... IN ARRAY is * itself an array type. There is then no target type * to probe, and getTypeInputInfo(InvalidOid) would * fail with an internal "cache lookup failed for type * 0", so skip the probe. */ if (OidIsValid(typeid) && !can_coerce_type(1, &expr_typoid, &typeid, COERCION_ASSIGNMENT)) { getTypeInputInfo(typeid, &infunc, &intypeioparam); (void) OidInputFunctionCall(infunc, str, intypeioparam, -1); } } } } if (bms_is_member(target_dno, expr->paramnos)) { /* * recheck if target_dno is really used on right side of * assignment */ if (!plpgsql_check_vardno_is_used_for_reading(node, target_dno)) { Bitmapset *paramnos; /* create set without target_param */ paramnos = bms_copy(expr->paramnos); paramnos = bms_del_member(paramnos, expr->target_param); cstate->used_variables = bms_add_members(cstate->used_variables, paramnos); bms_free(paramnos); } else cstate->used_variables = bms_add_members(cstate->used_variables, expr->paramnos); } else cstate->used_variables = bms_add_members(cstate->used_variables, expr->paramnos); if (node && IsA(node, SubscriptingRef)) node = (Node *) ((SubscriptingRef *) node)->refassgnexpr; /* check implicit coercion */ if (node && IsA(node, FuncExpr)) { FuncExpr *fexpr = (FuncExpr *) node; if (fexpr->funcformat == COERCE_IMPLICIT_CAST) { target_typoid = fexpr->funcresulttype; value_typoid = exprType(linitial(fexpr->args)); } } else if (node && IsA(node, CoerceViaIO)) { CoerceViaIO *cexpr = (CoerceViaIO *) node; if (cexpr->coerceformat == COERCE_IMPLICIT_CAST) { target_typoid = cexpr->resulttype; value_typoid = exprType((Node *) cexpr->arg); } } if (target_typoid != value_typoid) { StringInfoData str; initStringInfo(&str); appendStringInfo(&str, "cast \"%s\" value to \"%s\" type", format_type_be(value_typoid), format_type_be(target_typoid)); /* * accent warning when cast is without supported explicit * casting */ if (!can_coerce_type(1, &value_typoid, &target_typoid, COERCION_EXPLICIT)) plpgsql_check_put_error(cstate, ERRCODE_DATATYPE_MISMATCH, 0, "target type is different type than source type", str.data, "There are no possible explicit coercion between those types, possibly bug!", PLPGSQL_CHECK_WARNING_OTHERS, 0, expr->query, local_err_text); else if (!can_coerce_type(1, &value_typoid, &target_typoid, COERCION_ASSIGNMENT)) plpgsql_check_put_error(cstate, ERRCODE_DATATYPE_MISMATCH, 0, "target type is different type than source type", str.data, "The input expression type does not have an assignment cast to the target type.", PLPGSQL_CHECK_WARNING_OTHERS, 0, expr->query, local_err_text); else plpgsql_check_put_error(cstate, ERRCODE_DATATYPE_MISMATCH, 0, "target type is different type than source type", str.data, "Hidden casting can be a performance issue.", PLPGSQL_CHECK_WARNING_PERFORMANCE, 0, expr->query, local_err_text); pfree(str.data); } } else cstate->used_variables = bms_add_members(cstate->used_variables, expr->paramnos); /* * there is a possibility to call a plpgsql_pragma like default for * some aux variable. When we detect this case, then we mark target * variable as used variable. */ if (cstate->was_pragma && targetdno != -1) cstate->used_variables = bms_add_member(cstate->used_variables, targetdno); tupdesc = plpgsql_check_expr_get_desc(cstate, expr, use_element_type, expand, is_expression, &first_level_typoid); is_immutable_null = is_const_null_expr(cstate, expr); /* try to identify obsolete setting of refcursor's variables */ if (cstate->cinfo->compatibility_warnings && targetdno != -1) { PLpgSQL_var *var = (PLpgSQL_var *) cstate->estate->datums[targetdno]; if (var->dtype == PLPGSQL_DTYPE_VAR && var->datatype->typoid == REFCURSOROID) { Node *node = plpgsql_check_expr_get_node(cstate, expr, false); bool is_declare_cursor; bool is_ok = true; is_declare_cursor = cstate->estate->err_stmt && cstate->estate->err_stmt->cmd_type == PLPGSQL_STMT_BLOCK && var->cursor_explicit_expr; if (node && IsA((Node *) node, Const)) { /* only NULL constant argument is ok */ if (!((Const *) node)->constisnull) is_ok = false; } else if (node && IsA((Node *) node, Param)) { /* only variable of refcursor is ok */ if (((Param *) node)->paramtype != REFCURSOROID) is_ok = false; } else is_ok = false; /* * when the assignment is still ok, check an immutability of * bound cursor */ if (is_ok && var->cursor_explicit_expr) { if (!is_immutable_null) is_ok = false; } /* Don't raise warnings for old DECLARE CURSOR AS stmts */ if (!is_ok && !is_declare_cursor) plpgsql_check_put_error(cstate, 0, 0, "obsolete setting of refcursor or cursor variable", "Internal name of cursor should not be specified by users.", NULL, PLPGSQL_CHECK_WARNING_COMPATIBILITY, 0, NULL, NULL); } } /* try to detect safe variables */ if (cstate->cinfo->security_warnings && targetdno != -1) { PLpgSQL_var *var = (PLpgSQL_var *) cstate->estate->datums[targetdno]; if (var->dtype == PLPGSQL_DTYPE_VAR) { bool typispreferred; char typcategory; get_type_category_preferred(var->datatype->typoid, &typcategory, &typispreferred); if (typcategory == 'S') { Node *node = plpgsql_check_expr_get_node(cstate, expr, false); int location; if (plpgsql_check_is_sql_injection_vulnerable(cstate, expr, node, &location)) cstate->safe_variables = bms_del_member(cstate->safe_variables, targetdno); else cstate->safe_variables = bms_add_member(cstate->safe_variables, targetdno); } } } if (cstate->cinfo->constants_tracing && targetrow) { /* * We cannot to cut constants with results now, but we have to * reset all possible string constants saved in variables from row * variable. */ free_string_constant(cstate, targetrow); } else if (cstate->cinfo->constants_tracing && targetdno != -1) { PLpgSQL_var *var = (PLpgSQL_var *) cstate->estate->datums[targetdno]; /* * Trace constants only when target is really scalar. The scalar * types allows simple IO casting, but composites with different * number of fields doesn't allow it. There are difference between * PL/pgSQL assignment, that is not sensitive on different numbers * of fields between composite target and composite source, and * common composite casts (that doesn't allow to cast composites * when source and target has different natts. */ if (var->dtype == PLPGSQL_DTYPE_VAR && var->datatype->ttype == PLPGSQL_TTYPE_SCALAR) { char *str; str = plpgsql_check_expr_get_string(cstate, expr, NULL); /* * We are not able take an alement from an array string. * We don't trace typid, so we are not able to do correct * deserialization. It is necessary for correct casting * to target element type, because simple IO casting can * fail for common case numeric[] -> integer. So don't * try to open this box now, and just invalidate str. */ if (use_element_type) str = NULL; if (str) { PLpgSQL_statements *top_stmts = cstate->top_stmts; MemoryContext oldcxt = MemoryContextSwitchTo(cstate->check_cxt); char *prev_val; if (!cstate->strconstvars) cstate->strconstvars = palloc0(sizeof(char *) * cstate->estate->ndatums); /* * We need to do copy string first. There is an * possibility to self reference, and then we need to * first copy, and after that free. */ prev_val = cstate->strconstvars[targetdno]; cstate->strconstvars[targetdno] = pstrdup(str); if (prev_val) pfree(prev_val); if (top_stmts) top_stmts->invalidate_strconstvars = bms_add_member(top_stmts->invalidate_strconstvars, targetdno); MemoryContextSwitchTo(oldcxt); } else { /* the assigned value is not constant, reset current */ if (cstate->strconstvars && cstate->strconstvars[targetdno]) { pfree(cstate->strconstvars[targetdno]); cstate->strconstvars[targetdno] = NULL; } } } } if (expected_typoid != InvalidOid && type_is_rowtype(expected_typoid) && first_level_typoid != InvalidOid) { /* simple error, scalar source to composite target */ if (!type_is_rowtype(first_level_typoid) && !is_immutable_null) { plpgsql_check_put_error(cstate, ERRCODE_DATATYPE_MISMATCH, 0, "cannot assign scalar variable to composite target", NULL, NULL, PLPGSQL_CHECK_ERROR, 0, NULL, local_err_text); goto no_other_check; } /* simple ok, target and source composite types are same */ if (type_is_rowtype(first_level_typoid) && first_level_typoid != RECORDOID && first_level_typoid == expected_typoid) goto no_other_check; } if (tupdesc) { if (targetrow != NULL || targetrec != NULL) plpgsql_check_assign_tupdesc_row_or_rec(cstate, targetrow, targetrec, tupdesc, is_immutable_null); if (targetdno != -1) plpgsql_check_assign_tupdesc_dno(cstate, targetdno, tupdesc, is_immutable_null); if (targetrow) { if (RowGetValidFields(targetrow) > TupleDescNVatts(tupdesc)) plpgsql_check_put_error(cstate, 0, 0, "too few attributes for target variables", "There are more target variables than output columns in query.", "Check target variables in SELECT INTO statement.", PLPGSQL_CHECK_WARNING_OTHERS, 0, expr->query, local_err_text); else if (RowGetValidFields(targetrow) < TupleDescNVatts(tupdesc)) plpgsql_check_put_error(cstate, 0, 0, "too many attributes for target variables", "There are less target variables than output columns in query.", "Check target variables in SELECT INTO statement", PLPGSQL_CHECK_WARNING_OTHERS, 0, expr->query, local_err_text); } } no_other_check: if (tupdesc) ReleaseTupleDesc(tupdesc); if (free_local_err_text) pfree(local_err_text); ReleaseCurrentSubTransaction(); MemoryContextSwitchTo(oldCxt); CurrentResourceOwner = oldowner; } PG_CATCH(); { ErrorData *edata; MemoryContextSwitchTo(oldCxt); edata = CopyErrorData(); FlushErrorState(); RollbackAndReleaseCurrentSubTransaction(); MemoryContextSwitchTo(oldCxt); CurrentResourceOwner = oldowner; /* * If fatal_errors is true, we just propagate the error up to the * highest level. Otherwise the error is appended to our current list * of errors, and we continue checking. */ if (cstate->cinfo->fatal_errors) ReThrowError(edata); plpgsql_check_put_error_edata(cstate, edata); FreeErrorData(edata); MemoryContextSwitchTo(oldCxt); } PG_END_TRY(); } void plpgsql_check_expr_as_rvalue(PLpgSQL_checkstate *cstate, PLpgSQL_expr *expr, PLpgSQL_rec *targetrec, PLpgSQL_row *targetrow, int targetdno, bool use_element_type, bool is_expression) { plpgsql_check_expr_as_rvalue_with_parser_setup(cstate, expr, targetrec, targetrow, targetdno, use_element_type, is_expression, NULL, NULL); } /* * Check a SQL statement, should not to return data * */ void plpgsql_check_expr_as_sqlstmt_nodata(PLpgSQL_checkstate *cstate, PLpgSQL_expr *expr) { if (expr && plpgsql_check_expr_as_sqlstmt(cstate, expr)) ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR), errmsg("query has no destination for result data"))); } /* * Check a SQL statement, should to return data * */ void plpgsql_check_expr_as_sqlstmt_data(PLpgSQL_checkstate *cstate, PLpgSQL_expr *expr) { if (expr && !plpgsql_check_expr_as_sqlstmt(cstate, expr)) ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR), errmsg("query does not return data"))); } /* * Check a SQL statement, can (not) return data. Returns true * when statement returns data - we are able to get tuple descriptor. */ bool plpgsql_check_expr_as_sqlstmt(PLpgSQL_checkstate *cstate, PLpgSQL_expr *expr) { ResourceOwner oldowner; MemoryContext oldCxt = CurrentMemoryContext; volatile bool result = false; if (!expr) return true; oldowner = CurrentResourceOwner; BeginInternalSubTransaction(NULL); MemoryContextSwitchTo(oldCxt); PG_TRY(); { TupleDesc tupdesc; prepare_plan(cstate, expr, 0, NULL, NULL, false); /* record all variables used by the query */ cstate->used_variables = bms_add_members(cstate->used_variables, expr->paramnos); force_plan_checks(cstate, expr); tupdesc = plpgsql_check_expr_get_desc(cstate, expr, false, false, false, NULL); if (tupdesc) { result = true; ReleaseTupleDesc(tupdesc); } ReleaseCurrentSubTransaction(); MemoryContextSwitchTo(oldCxt); CurrentResourceOwner = oldowner; } PG_CATCH(); { ErrorData *edata; MemoryContextSwitchTo(oldCxt); edata = CopyErrorData(); FlushErrorState(); RollbackAndReleaseCurrentSubTransaction(); MemoryContextSwitchTo(oldCxt); CurrentResourceOwner = oldowner; /* * If fatal_errors is true, we just propagate the error up to the * highest level. Otherwise the error is appended to our current list * of errors, and we continue checking. */ if (cstate->cinfo->fatal_errors) ReThrowError(edata); plpgsql_check_put_error_edata(cstate, edata); FreeErrorData(edata); MemoryContextSwitchTo(oldCxt); } PG_END_TRY(); return result; } /* * Verify an assignment of 'expr' to 'target' with possible slices * * it is used in FOREACH ARRAY where SLICE change a target type * */ void plpgsql_check_assignment_with_possible_slices(PLpgSQL_checkstate *cstate, PLpgSQL_expr *expr, PLpgSQL_rec *targetrec, PLpgSQL_row *targetrow, int targetdno, bool use_element_type) { bool is_expression = (targetrec == NULL && targetrow == NULL); if (expr) plpgsql_check_expr_as_rvalue(cstate, expr, targetrec, targetrow, targetdno, use_element_type, is_expression); } /* * Verify a expression * */ void plpgsql_check_expr(PLpgSQL_checkstate *cstate, PLpgSQL_expr *expr) { if (expr) plpgsql_check_expr_as_rvalue(cstate, expr, NULL, NULL, -1, false, true); } /* * Verify an assignment of 'expr' to 'target' * */ void plpgsql_check_assignment(PLpgSQL_checkstate *cstate, PLpgSQL_expr *expr, PLpgSQL_rec *targetrec, PLpgSQL_row *targetrow, int targetdno) { bool is_expression = (targetrec == NULL && targetrow == NULL); plpgsql_check_expr_as_rvalue(cstate, expr, targetrec, targetrow, targetdno, false, is_expression); } void plpgsql_check_assignment_to_variable_with_parser_setup(PLpgSQL_checkstate *cstate, PLpgSQL_expr *expr, PLpgSQL_variable *targetvar, int targetdno, ParserSetupHook parser_setup, void *arg) { if (targetvar != NULL) { if (targetvar->dtype == PLPGSQL_DTYPE_ROW) plpgsql_check_expr_as_rvalue_with_parser_setup(cstate, expr, NULL, (PLpgSQL_row *) targetvar, targetdno, false, false, parser_setup, arg); else if (targetvar->dtype == PLPGSQL_DTYPE_REC) plpgsql_check_expr_as_rvalue_with_parser_setup(cstate, expr, (PLpgSQL_rec *) targetvar, NULL, targetdno, false, false, parser_setup, arg); else elog(ERROR, "unsupported target variable type"); } else plpgsql_check_expr_as_rvalue_with_parser_setup(cstate, expr, NULL, NULL, targetdno, false, true, parser_setup, arg); } void plpgsql_check_assignment_to_variable(PLpgSQL_checkstate *cstate, PLpgSQL_expr *expr, PLpgSQL_variable *targetvar, int targetdno) { plpgsql_check_assignment_to_variable_with_parser_setup(cstate, expr, targetvar, targetdno, NULL, NULL); } /* * row->nfields can cound dropped columns. When this behave can raise * false alarms, we should to count fields more precisely. */ static int RowGetValidFields(PLpgSQL_row *row) { int i; int result = 0; for (i = 0; i < row->nfields; i++) { if (row->varnos[i] != -1) result += 1; } return result; } /* * returns number of valid fields */ static int TupleDescNVatts(TupleDesc tupdesc) { int natts = 0; int i; for (i = 0; i < tupdesc->natts; i++) { if (!TupleDescAttr(tupdesc, i)->attisdropped) natts += 1; } return natts; } okbob-plpgsql_check-800db79/src/check_function.c000066400000000000000000001221131526744060500217100ustar00rootroot00000000000000/*------------------------------------------------------------------------- * * check_function.c * * workhorse functionality of this extension - expression * and query validator * * by Pavel Stehule 2013-2026 * *------------------------------------------------------------------------- */ #include "plpgsql_check.h" #include "catalog/pg_proc.h" #include "catalog/pg_type.h" #if PG_VERSION_NUM >= 180000 #include "common/hashfn.h" #endif #include "nodes/makefuncs.h" #include "utils/guc.h" #include "utils/memutils.h" #include "utils/lsyscache.h" #include "utils/rel.h" #include "utils/syscache.h" #include "utils/typcache.h" #include "access/heapam.h" static HTAB *plpgsql_check_HashTable = NULL; bool plpgsql_check_other_warnings = false; bool plpgsql_check_extra_warnings = false; bool plpgsql_check_performance_warnings = false; bool plpgsql_check_compatibility_warnings = false; bool plpgsql_check_fatal_errors = true; bool plpgsql_check_constants_tracing = true; int plpgsql_check_mode = PLPGSQL_CHECK_MODE_BY_FUNCTION; /* ---------- * Hash table for checked functions * ---------- */ typedef struct plpgsql_hashent { #if PG_VERSION_NUM >= 180000 CachedFunctionHashKey hk; #else PLpgSQL_func_hashkey hk; #endif TransactionId fn_xmin; ItemPointerData fn_tid; bool is_checked; } plpgsql_check_HashEnt; static void function_check(PLpgSQL_function *func, PLpgSQL_checkstate *cstate); static void trigger_check(PLpgSQL_function *func, Node *tdata, PLpgSQL_checkstate *cstate); static void release_exprs(List *exprs); static int load_configuration(HeapTuple procTuple, bool *reload_config); static void init_datum_dno(PLpgSQL_checkstate *cstate, int dno, bool is_auto, bool is_protected); static PLpgSQL_datum *copy_plpgsql_datum(PLpgSQL_checkstate *cstate, PLpgSQL_datum *datum); static void setup_estate(PLpgSQL_execstate *estate, PLpgSQL_function *func, ReturnSetInfo *rsi, plpgsql_check_info *cinfo); static void setup_cstate(PLpgSQL_checkstate *cstate, plpgsql_check_result_info *result_info, plpgsql_check_info *cinfo, bool is_active_mode, bool fake_rtd); static void passive_check_func_beg(PLpgSQL_execstate *estate, PLpgSQL_function *func, plch_fextra *fextra); static bool is_active(PLpgSQL_execstate *estate, PLpgSQL_function *func); static plch_plugin check_plugin = { is_active, NULL, passive_check_func_beg, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL }; /* * Prepare list of OUT variables for later report */ static void collect_out_variables(PLpgSQL_function *func, PLpgSQL_checkstate *cstate) { cstate->out_variables = NULL; if (func->out_param_varno != -1) { int varno = func->out_param_varno; PLpgSQL_variable *var = (PLpgSQL_variable *) func->datums[varno]; if (var->dtype == PLPGSQL_DTYPE_ROW && plpgsql_check_is_internal_variable(cstate, var)) { /* this function has more OUT parameters */ PLpgSQL_row *row = (PLpgSQL_row *) var; int fnum; for (fnum = 0; fnum < row->nfields; fnum++) cstate->out_variables = bms_add_member(cstate->out_variables, row->varnos[fnum]); } else cstate->out_variables = bms_add_member(cstate->out_variables, varno); } } /* * reports dependency of composite variables that uses referenced types */ static void reports_used_rowtypes_dependency(PLpgSQL_function *func, PLpgSQL_checkstate *cstate) { plpgsql_check_result_info *ri = cstate->result_info; int i; if (cstate->result_info->format != PLPGSQL_SHOW_DEPENDENCY_FORMAT_TABULAR) return; for (i = 0; i < func->ndatums; i++) { PLpgSQL_var *var = (PLpgSQL_var *) func->datums[i]; if (var->dtype == PLPGSQL_DTYPE_REC) { PLpgSQL_rec *rec = (PLpgSQL_rec *) var; Oid typrelid; char relkind; /* * datatype can be NULL, when rectypeid is RECORDOID (trigger * NEW/OLD) */ if (!rec->datatype) continue; typrelid = get_typ_typrelid(rec->datatype->typoid); relkind = get_rel_relkind(typrelid); if (!bms_is_member(typrelid, cstate->rel_oids)) { if (relkind == RELKIND_COMPOSITE_TYPE) { plpgsql_check_put_dependency(ri, "TYPE", typrelid, get_namespace_name(get_rel_namespace(typrelid)), get_rel_name(typrelid), NULL); cstate->rel_oids = bms_add_member(cstate->rel_oids, typrelid); } else if (relkind == RELKIND_RELATION || relkind == RELKIND_PARTITIONED_TABLE || relkind == RELKIND_FOREIGN_TABLE || relkind == RELKIND_VIEW || relkind == RELKIND_MATVIEW) { plpgsql_check_put_dependency(ri, "RELATION", typrelid, get_namespace_name(get_rel_namespace(typrelid)), get_rel_name(typrelid), NULL); cstate->rel_oids = bms_add_member(cstate->rel_oids, typrelid); } } } } } /* * Returns true, when routine should be closed by RETURN statement * */ static bool return_is_required(plpgsql_check_info *cinfo) { if (cinfo->is_procedure) return false; if (cinfo->rettype == VOIDOID) return false; return true; } /* * own implementation - active mode * */ void plpgsql_check_function_internal(plpgsql_check_result_info *ri, plpgsql_check_info *cinfo) { PLpgSQL_checkstate cstate; PLpgSQL_function *volatile function = NULL; volatile bool function_is_pinned = false; bool reload_config; LOCAL_FCINFO(fake_fcinfo, 0); FmgrInfo flinfo; TriggerData trigdata; EventTriggerData etrigdata; Trigger tg_trigger; int rc; ResourceOwner oldowner; PLpgSQL_execstate *volatile cur_estate = NULL; MemoryContext old_cxt; PLpgSQL_execstate estate; ReturnSetInfo rsinfo; bool fake_rtd; /* * Connect to SPI manager */ if ((rc = SPI_connect()) != SPI_OK_CONNECT) elog(ERROR, "SPI_connect failed: %s", SPI_result_code_string(rc)); plpgsql_check_setup_fcinfo(cinfo, &flinfo, fake_fcinfo, &rsinfo, &trigdata, &etrigdata, &tg_trigger, &fake_rtd); setup_cstate(&cstate, ri, cinfo, true, fake_rtd); old_cxt = MemoryContextSwitchTo(cstate.check_cxt); /* * Copy argument names for later check, only when other warnings are * required. Argument names are used for check parameter versus local * variable collision. */ if (cinfo->other_warnings) { int numargs; Oid *argtypes; char **argnames; char *argmodes; numargs = get_func_arg_info(cinfo->proctuple, &argtypes, &argnames, &argmodes); if (argnames != NULL) { int i; for (i = 0; i < numargs; i++) { if (argnames[i][0] != '\0') cstate.argnames = lappend(cstate.argnames, argnames[i]); } } } oldowner = CurrentResourceOwner; PG_TRY(); { int save_nestlevel = 0; BeginInternalSubTransaction(NULL); MemoryContextSwitchTo(cstate.check_cxt); save_nestlevel = load_configuration(cinfo->proctuple, &reload_config); /* have to wait for this decision to loaded configuration */ if (plpgsql_check_mode != PLPGSQL_CHECK_MODE_DISABLED) { /* Get a compiled function */ function = plpgsql_check__compile_p(fake_fcinfo, false); /* Acquire the pin before any preparation step can fail. */ cur_estate = function->cur_estate; plch_use_count(function)++; function_is_pinned = true; collect_out_variables(function, &cstate); reports_used_rowtypes_dependency(function, &cstate); /* recheck trigtype */ Assert(function->fn_is_trigger == cinfo->trigtype); setup_estate(&estate, function, (ReturnSetInfo *) fake_fcinfo->resultinfo, cinfo); cstate.estate = &estate; /* * Apply pragmas entered as an argument of the check function. * They are applied before the start of static analysis, so the * table pragmas can create temporary objects referenced by the * checked function. */ if (cinfo->pragmas) { ListCell *lc; foreach(lc, cinfo->pragmas) { char *pragma_str = (char *) lfirst(lc); plpgsql_check_pragma_apply(&cstate, pragma_str, NULL, 0); } } /* Create a fake runtime environment and process check */ switch (cinfo->trigtype) { case PLPGSQL_DML_TRIGGER: trigger_check(function, (Node *) &trigdata, &cstate); break; case PLPGSQL_EVENT_TRIGGER: trigger_check(function, (Node *) &etrigdata, &cstate); break; case PLPGSQL_NOT_TRIGGER: function_check(function, &cstate); break; } function->cur_estate = cur_estate; plch_use_count(function)--; function_is_pinned = false; } else elog(NOTICE, "plpgsql_check is disabled"); /* * reload back a GUC. XXX: isn't this done automatically by subxact * rollback? */ if (reload_config) AtEOXact_GUC(true, save_nestlevel); RollbackAndReleaseCurrentSubTransaction(); MemoryContextSwitchTo(cstate.check_cxt); CurrentResourceOwner = oldowner; if (OidIsValid(cinfo->relid)) relation_close(trigdata.tg_relation, AccessShareLock); release_exprs(cstate.exprs); } PG_CATCH(); { ErrorData *edata; MemoryContextSwitchTo(cstate.check_cxt); edata = CopyErrorData(); FlushErrorState(); RollbackAndReleaseCurrentSubTransaction(); MemoryContextSwitchTo(cstate.check_cxt); CurrentResourceOwner = oldowner; if (OidIsValid(cinfo->relid)) relation_close(trigdata.tg_relation, AccessShareLock); if (function) { if (function_is_pinned) { function->cur_estate = cur_estate; plch_use_count(function)--; function_is_pinned = false; } release_exprs(cstate.exprs); } /* * In pragma generation mode, errors are not converted to check result * rows - the original error is rethrown. */ if (cinfo->make_pragma) ReThrowError(edata); plpgsql_check_put_error_edata(&cstate, edata); } PG_END_TRY(); MemoryContextSwitchTo(old_cxt); MemoryContextDelete(cstate.check_cxt); /* * Disconnect from SPI manager */ if ((rc = SPI_finish()) != SPI_OK_FINISH) elog(ERROR, "SPI_finish failed: %s", SPI_result_code_string(rc)); } static bool is_active(PLpgSQL_execstate *estate, PLpgSQL_function *func) { return plpgsql_check_mode == PLPGSQL_CHECK_MODE_FRESH_START || plpgsql_check_mode == PLPGSQL_CHECK_MODE_EVERY_START; } /* * plpgsql_check_on_func_beg - passive mode * * callback function - called by plgsql executor, when function is started * and local variables are initialized. * */ static void passive_check_func_beg(PLpgSQL_execstate *estate, PLpgSQL_function *func, plch_fextra *fextra) { const char *err_text = estate->err_text; int closing; List *exceptions; if (plpgsql_check_mode == PLPGSQL_CHECK_MODE_FRESH_START || plpgsql_check_mode == PLPGSQL_CHECK_MODE_EVERY_START) { int i; PLpgSQL_rec *saved_records; PLpgSQL_var *saved_vars; MemoryContext oldcontext, old_cxt; ResourceOwner oldowner; plpgsql_check_result_info ri; plpgsql_check_info cinfo; PLpgSQL_checkstate cstate; /* * don't allow repeated execution on checked function when it is not * requsted. */ if (plpgsql_check_mode == PLPGSQL_CHECK_MODE_FRESH_START && plpgsql_check_is_checked(func)) return; plpgsql_check_mark_as_checked(func); memset(&ri, 0, sizeof(ri)); plpgsql_check_info_init(&cinfo, func->fn_oid); if (OidIsValid(func->fn_oid)) { cinfo.proctuple = SearchSysCache1(PROCOID, ObjectIdGetDatum(func->fn_oid)); if (!HeapTupleIsValid(cinfo.proctuple)) elog(ERROR, "cache lookup failed for function %u", func->fn_oid); plpgsql_check_get_function_info(&cinfo); ReleaseSysCache(cinfo.proctuple); cinfo.proctuple = NULL; cinfo.fn_oid = func->fn_oid; } else cinfo.volatility = PROVOLATILE_VOLATILE; cinfo.fatal_errors = plpgsql_check_fatal_errors, cinfo.other_warnings = plpgsql_check_other_warnings, cinfo.performance_warnings = plpgsql_check_performance_warnings, cinfo.extra_warnings = plpgsql_check_extra_warnings, cinfo.compatibility_warnings = plpgsql_check_compatibility_warnings; cinfo.constants_tracing = plpgsql_check_constants_tracing; ri.format = PLPGSQL_CHECK_FORMAT_ELOG; setup_cstate(&cstate, &ri, &cinfo, false, false); collect_out_variables(func, &cstate); /* use real estate */ cstate.estate = estate; old_cxt = MemoryContextSwitchTo(cstate.check_cxt); /* * During the check stage a rec and vars variables are modified, so we * should to save their content */ saved_records = palloc(sizeof(PLpgSQL_rec) * estate->ndatums); saved_vars = palloc(sizeof(PLpgSQL_var) * estate->ndatums); for (i = 0; i < estate->ndatums; i++) { if (estate->datums[i]->dtype == PLPGSQL_DTYPE_REC) { PLpgSQL_rec *rec = (PLpgSQL_rec *) estate->datums[i]; memcpy(&saved_records[i], rec, sizeof(PLpgSQL_rec)); if (rec->erh) { /* work with dummy copy */ rec->erh = make_expanded_record_from_exprecord(rec->erh, cstate.check_cxt); } else { plpgsql_check_recval_assign_tupdesc(&cstate, rec, NULL, false); } } else if (estate->datums[i]->dtype == PLPGSQL_DTYPE_VAR) { PLpgSQL_var *var = (PLpgSQL_var *) estate->datums[i]; memcpy(&saved_vars[i], var, sizeof(PLpgSQL_var)); var->freeval = false; } } estate->err_text = NULL; /* * Raised exception should be trapped in outer functtion. Protection * against outer trap is QUERY_CANCELED exception. */ oldcontext = CurrentMemoryContext; oldowner = CurrentResourceOwner; PG_TRY(); { /* * Now check the toplevel block of statements */ plpgsql_check_stmt(&cstate, (PLpgSQL_stmt *) func->action, &closing, &exceptions); estate->err_stmt = NULL; if (!cstate.stop_check) { if (closing != PLPGSQL_CHECK_CLOSED && closing != PLPGSQL_CHECK_CLOSED_BY_EXCEPTIONS && return_is_required(cstate.cinfo)) plpgsql_check_put_error(&cstate, ERRCODE_S_R_E_FUNCTION_EXECUTED_NO_RETURN_STATEMENT, 0, "control reached end of function without RETURN", NULL, NULL, closing == PLPGSQL_CHECK_UNCLOSED ? PLPGSQL_CHECK_ERROR : PLPGSQL_CHECK_WARNING_EXTRA, 0, NULL, NULL); plpgsql_check_report_unused_variables(&cstate); plpgsql_check_report_too_high_volatility(&cstate); } release_exprs(cstate.exprs); } PG_CATCH(); { ErrorData *edata; /* Save error info */ MemoryContextSwitchTo(oldcontext); edata = CopyErrorData(); FlushErrorState(); CurrentResourceOwner = oldowner; release_exprs(cstate.exprs); edata->sqlerrcode = ERRCODE_QUERY_CANCELED; ReThrowError(edata); } PG_END_TRY(); estate->err_text = err_text; estate->err_stmt = NULL; /* return back a original rec variables */ for (i = 0; i < estate->ndatums; i++) { if (estate->datums[i]->dtype == PLPGSQL_DTYPE_REC) { PLpgSQL_rec *rec = (PLpgSQL_rec *) estate->datums[i]; memcpy(rec, &saved_records[i], sizeof(PLpgSQL_rec)); } else if (estate->datums[i]->dtype == PLPGSQL_DTYPE_VAR) { PLpgSQL_var *var = (PLpgSQL_var *) estate->datums[i]; memcpy(var, &saved_vars[i], sizeof(PLpgSQL_var)); } } MemoryContextSwitchTo(old_cxt); MemoryContextDelete(cstate.check_cxt); } } void plpgsql_check_passive_check_init(void) { plch_register_plugin(&check_plugin); } /* * Check function - it prepare variables and starts a prepare plan walker * */ static void function_check(PLpgSQL_function *func, PLpgSQL_checkstate *cstate) { int i; int closing = PLPGSQL_CHECK_UNCLOSED; List *exceptions; ListCell *lc; /* * Make local execution copies of all the datums */ for (i = 0; i < cstate->estate->ndatums; i++) cstate->estate->datums[i] = copy_plpgsql_datum(cstate, func->datums[i]); init_datum_dno(cstate, cstate->estate->found_varno, true, true); /* * check function's parameters to not be reserved keywords */ foreach(lc, cstate->argnames) { char *argname = (char *) lfirst(lc); if (plpgsql_check_is_reserved_keyword(argname)) { StringInfoData str; initStringInfo(&str); appendStringInfo(&str, "name of parameter \"%s\" is reserved keyword", argname); plpgsql_check_put_error(cstate, 0, 0, str.data, "The reserved keyword was used as parameter name.", NULL, PLPGSQL_CHECK_WARNING_OTHERS, 0, NULL, NULL); pfree(str.data); } } /* * Store the actual call argument values (fake) into the appropriate * variables */ for (i = 0; i < func->fn_nargs; i++) { init_datum_dno(cstate, func->fn_argvarnos[i], false, false); } /* * In pragma generation mode, only scan the function's body for CREATE * TEMP TABLE statements, including AS. Ordinary checks are skipped. */ if (cstate->cinfo->make_pragma) { plch_make_pragma(cstate, func); /* clean state values - next errors are not related to any command */ cstate->estate->err_stmt = NULL; return; } /* * Now check the toplevel block of statements */ plpgsql_check_stmt(cstate, (PLpgSQL_stmt *) func->action, &closing, &exceptions); /* clean state values - next errors are not related to any command */ cstate->estate->err_stmt = NULL; if (!cstate->stop_check) { if (closing != PLPGSQL_CHECK_CLOSED && closing != PLPGSQL_CHECK_CLOSED_BY_EXCEPTIONS && return_is_required(cstate->cinfo)) plpgsql_check_put_error(cstate, ERRCODE_S_R_E_FUNCTION_EXECUTED_NO_RETURN_STATEMENT, 0, "control reached end of function without RETURN", NULL, NULL, closing == PLPGSQL_CHECK_UNCLOSED ? PLPGSQL_CHECK_ERROR : PLPGSQL_CHECK_WARNING_EXTRA, 0, NULL, NULL); plpgsql_check_report_unused_variables(cstate); plpgsql_check_report_too_high_volatility(cstate); } } /* * Check trigger - prepare fake environments for testing trigger * */ static void trigger_check(PLpgSQL_function *func, Node *tdata, PLpgSQL_checkstate *cstate) { int i; int closing = PLPGSQL_CHECK_UNCLOSED; List *exceptions; /* * Make local execution copies of all the datums */ for (i = 0; i < cstate->estate->ndatums; i++) cstate->estate->datums[i] = copy_plpgsql_datum(cstate, func->datums[i]); init_datum_dno(cstate, cstate->estate->found_varno, true, true); if (IsA(tdata, TriggerData)) { PLpgSQL_rec *rec_new, *rec_old; TriggerData *trigdata = (TriggerData *) tdata; /* * Put the OLD and NEW tuples into record variables * * We make the tupdescs available in both records even though only one * may have a value. This allows parsing of record references to * succeed in functions that are used for multiple trigger types. For * example, we might have a test like "if (TG_OP = 'INSERT' and * NEW.foo = 'xyz')", which should parse regardless of the current * trigger type. */ /* * find all PROMISE VARIABLES and initit their */ for (i = 0; i < func->ndatums; i++) { PLpgSQL_datum *datum = func->datums[i]; if (datum->dtype == PLPGSQL_DTYPE_PROMISE) init_datum_dno(cstate, datum->dno, true, datum->dno != func->new_varno && datum->dno != func->old_varno); } rec_new = (PLpgSQL_rec *) (cstate->estate->datums[func->new_varno]); plpgsql_check_recval_assign_tupdesc(cstate, rec_new, trigdata->tg_relation->rd_att, false); rec_old = (PLpgSQL_rec *) (cstate->estate->datums[func->old_varno]); plpgsql_check_recval_assign_tupdesc(cstate, rec_old, trigdata->tg_relation->rd_att, false); } else if (IsA(tdata, EventTriggerData)) { /* do nothing */ } else elog(ERROR, "unexpected environment"); /* * In pragma generation mode, only scan the function's body for CREATE * TEMP TABLE statements, including AS. Ordinary checks are skipped. */ if (cstate->cinfo->make_pragma) { plch_make_pragma(cstate, func); /* clean state values - next errors are not related to any command */ cstate->estate->err_stmt = NULL; return; } /* * Now check the toplevel block of statements */ plpgsql_check_stmt(cstate, (PLpgSQL_stmt *) func->action, &closing, &exceptions); /* clean state values - next errors are not related to any command */ cstate->estate->err_stmt = NULL; if (!cstate->stop_check) { if (closing != PLPGSQL_CHECK_CLOSED && closing != PLPGSQL_CHECK_CLOSED_BY_EXCEPTIONS && return_is_required(cstate->cinfo)) plpgsql_check_put_error(cstate, ERRCODE_S_R_E_FUNCTION_EXECUTED_NO_RETURN_STATEMENT, 0, "control reached end of function without RETURN", NULL, NULL, closing == PLPGSQL_CHECK_UNCLOSED ? PLPGSQL_CHECK_ERROR : PLPGSQL_CHECK_WARNING_EXTRA, 0, NULL, NULL); plpgsql_check_report_unused_variables(cstate); plpgsql_check_report_too_high_volatility(cstate); } } /* * Returns true when some fields is polymorphics */ static bool is_polymorphic_tupdesc(TupleDesc tupdesc) { int i; for (i = 0; i < tupdesc->natts; i++) if (IsPolymorphicType(TupleDescAttr(tupdesc, i)->atttypid)) return true; return false; } /* * Replaces polymorphic types by real type */ static Oid replace_polymorphic_type(plpgsql_check_info *cinfo, Oid typ, Oid anyelement_array_oid, bool is_array_anyelement, Oid anycompatible_array_oid, bool is_array_anycompatible, bool is_variadic) { /* quite compiler warnings */ (void) anycompatible_array_oid; (void) is_array_anycompatible; if (OidIsValid(typ) && IsPolymorphicType(typ)) { switch (typ) { case ANYELEMENTOID: typ = is_variadic ? anyelement_array_oid : cinfo->anyelementoid; break; case ANYNONARRAYOID: if (is_array_anyelement) elog(ERROR, "anyelement type is a array (expected nonarray)"); typ = is_variadic ? anyelement_array_oid : cinfo->anyelementoid; break; case ANYENUMOID: /* XXX dubious */ if (!OidIsValid(cinfo->anyenumoid)) elog(ERROR, "anyenumtype option should be specified (anyenum type is used)"); if (!type_is_enum(cinfo->anyenumoid)) elog(ERROR, "type specified by anyenumtype option is not enum"); typ = cinfo->anyenumoid; break; case ANYARRAYOID: typ = anyelement_array_oid; break; case ANYRANGEOID: typ = is_variadic ? get_array_type(cinfo->anyrangeoid) : cinfo->anyrangeoid; break; case ANYMULTIRANGEOID: typ = get_range_multirange(cinfo->anyrangeoid); if (!OidIsValid(typ)) elog(ERROR, "type specified by anyrangetype option is not range"); if (is_variadic) typ = get_array_type(typ); break; case ANYCOMPATIBLEOID: typ = is_variadic ? anycompatible_array_oid : cinfo->anycompatibleoid; break; case ANYCOMPATIBLENONARRAYOID: if (is_array_anycompatible) elog(ERROR, "anycompatible type is a array (expected nonarray)"); typ = is_variadic ? anycompatible_array_oid : cinfo->anycompatibleoid; break; case ANYCOMPATIBLEARRAYOID: typ = anycompatible_array_oid; break; case ANYCOMPATIBLERANGEOID: typ = is_variadic ? get_array_type(cinfo->anycompatiblerangeoid) : cinfo->anycompatiblerangeoid; break; case ANYCOMPATIBLEMULTIRANGEOID: typ = get_range_multirange(cinfo->anycompatiblerangeoid); if (!OidIsValid(typ)) elog(ERROR, "type specified by anycompatiblerangetype option is not range"); if (is_variadic) typ = get_array_type(typ); break; default: /* fallback */ typ = is_variadic ? INT4ARRAYOID : INT4OID; } } return typ; } /* * Set up a fake fcinfo with just enough info to satisfy plpgsql_compile(). * * There should be a different real argtypes for polymorphic params. * * When output fake_rtd is true, then we should to not compare result fields, * because we know nothing about expected result. */ void plpgsql_check_setup_fcinfo(plpgsql_check_info *cinfo, FmgrInfo *flinfo, FunctionCallInfo fcinfo, ReturnSetInfo *rsinfo, TriggerData *trigdata, EventTriggerData *etrigdata, Trigger *tg_trigger, bool *fake_rtd) { TupleDesc resultTupleDesc; int nargs; Oid *argtypes; char **argnames; char *argmodes; Oid rettype; bool found_polymorphic = false; *fake_rtd = false; /* clean structures */ memset(fcinfo, 0, SizeForFunctionCallInfo(0)); memset(flinfo, 0, sizeof(FmgrInfo)); memset(rsinfo, 0, sizeof(ReturnSetInfo)); fcinfo->flinfo = flinfo; flinfo->fn_oid = cinfo->fn_oid; flinfo->fn_mcxt = CurrentMemoryContext; rettype = cinfo->rettype; if (cinfo->trigtype == PLPGSQL_DML_TRIGGER) { Assert(trigdata != NULL); memset(trigdata, 0, sizeof(TriggerData)); memset(tg_trigger, 0, sizeof(Trigger)); trigdata->type = T_TriggerData; trigdata->tg_trigger = tg_trigger; fcinfo->context = (Node *) trigdata; if (OidIsValid(cinfo->relid)) trigdata->tg_relation = relation_open(cinfo->relid, AccessShareLock); } else if (cinfo->trigtype == PLPGSQL_EVENT_TRIGGER) { memset(etrigdata, 0, sizeof(EventTriggerData)); etrigdata->type = T_EventTriggerData; fcinfo->context = (Node *) etrigdata; } /* prepare call expression - used for polymorphic arguments */ nargs = get_func_arg_info(cinfo->proctuple, &argtypes, &argnames, &argmodes); if (nargs > 0) { int i; for (i = 0; i < nargs; i++) { Oid argtype = InvalidOid; if (argmodes) { if (argmodes[i] == FUNC_PARAM_IN || argmodes[i] == FUNC_PARAM_INOUT || argmodes[i] == FUNC_PARAM_VARIADIC) argtype = argtypes[i]; } else argtype = argtypes[i]; if (OidIsValid(argtype) && IsPolymorphicType(argtype)) { found_polymorphic = true; break; } } if (found_polymorphic) { List *args = NIL; Oid anyelement_array_oid; Oid anyelement_base_oid; bool is_array_anyelement; Oid anycompatible_array_oid; Oid anycompatible_base_oid; bool is_array_anycompatible; anyelement_array_oid = get_array_type(cinfo->anyelementoid); anyelement_base_oid = getBaseType(cinfo->anyelementoid); is_array_anyelement = OidIsValid(get_element_type(anyelement_base_oid)); anycompatible_array_oid = get_array_type(cinfo->anycompatibleoid); anycompatible_base_oid = getBaseType(cinfo->anycompatibleoid); is_array_anycompatible = OidIsValid(get_element_type(anycompatible_base_oid)); /* * when polymorphic types are used, then we need to build fake * fn_expr, to be in plpgsql_resolve_polymorphic_argtypes happy. */ for (i = 0; i < nargs; i++) { bool is_variadic = false; Oid argtype = InvalidOid; if (argmodes) { if (argmodes[i] == FUNC_PARAM_IN || argmodes[i] == FUNC_PARAM_INOUT || argmodes[i] == FUNC_PARAM_VARIADIC) { argtype = argtypes[i]; if (argmodes[i] == FUNC_PARAM_VARIADIC) is_variadic = true; } } else argtype = argtypes[i]; if (OidIsValid(argtype)) { argtype = replace_polymorphic_type(cinfo, argtype, anyelement_array_oid, is_array_anyelement, anycompatible_array_oid, is_array_anycompatible, is_variadic); args = lappend(args, makeNullConst(argtype, -1, InvalidOid)); } } rettype = replace_polymorphic_type(cinfo, rettype, anyelement_array_oid, is_array_anyelement, anycompatible_array_oid, is_array_anycompatible, false); fcinfo->flinfo->fn_expr = (Node *) makeFuncExpr(cinfo->fn_oid, rettype, args, InvalidOid, InvalidOid, COERCE_EXPLICIT_CALL); } } if (argtypes) pfree(argtypes); if (argnames) pfree(argnames); if (argmodes) pfree(argmodes); /* * prepare ReturnSetInfo * * necessary for RETURN NEXT and RETURN QUERY * */ resultTupleDesc = build_function_result_tupdesc_t(cinfo->proctuple); if (resultTupleDesc) { /* we cannot to solve polymorphic params now */ if (is_polymorphic_tupdesc(resultTupleDesc)) { FreeTupleDesc(resultTupleDesc); resultTupleDesc = NULL; } } else if (cinfo->rettype == TRIGGEROID) { /* trigger - return value should be ROW or RECORD based on relid */ if (trigdata->tg_relation) resultTupleDesc = CreateTupleDescCopy(trigdata->tg_relation->rd_att); } else if (!IsPolymorphicType(cinfo->rettype)) { Oid result_rettype = getBaseType(cinfo->rettype); if (get_typtype(result_rettype) == TYPTYPE_COMPOSITE) resultTupleDesc = lookup_rowtype_tupdesc_copy(result_rettype, -1); else { *fake_rtd = result_rettype == RECORDOID; resultTupleDesc = CreateTemplateTupleDesc(1); TupleDescInitEntry(resultTupleDesc, (AttrNumber) 1, "__result__", result_rettype, -1, 0); #if PG_VERSION_NUM >= 190000 TupleDescFinalize(resultTupleDesc); #endif resultTupleDesc = BlessTupleDesc(resultTupleDesc); } } else { if (IsPolymorphicType(cinfo->rettype)) { /* * ensure replacament of polymorphic rettype, but this error is * checked in validation stage, so this case should not be * possible. */ if (IsPolymorphicType(rettype)) elog(ERROR, "return type is still polymorphic"); } } if (resultTupleDesc) { fcinfo->resultinfo = (Node *) rsinfo; rsinfo->type = T_ReturnSetInfo; rsinfo->expectedDesc = resultTupleDesc; rsinfo->allowedModes = (int) (SFRM_ValuePerCall | SFRM_Materialize); rsinfo->returnMode = SFRM_ValuePerCall; /* * ExprContext is created inside CurrentMemoryContext, without any * additional source allocation. It is released on end of transaction. */ rsinfo->econtext = CreateStandaloneExprContext(); } } /* ---------- * Initialize a plpgsql fake execution state * ---------- */ static void setup_estate(PLpgSQL_execstate *estate, PLpgSQL_function *func, ReturnSetInfo *rsi, plpgsql_check_info *cinfo) { memset(estate, 0, sizeof(*estate)); /* this link will be restored at exit from plpgsql_call_handler */ func->cur_estate = estate; estate->func = func; estate->retval = (Datum) 0; estate->retisnull = true; estate->rettype = InvalidOid; estate->fn_rettype = func->fn_rettype; estate->retistuple = func->fn_retistuple; estate->retisset = func->fn_retset; estate->readonly_func = func->fn_readonly; estate->eval_econtext = makeNode(ExprContext); estate->eval_econtext->ecxt_per_tuple_memory = AllocSetContextCreate(CurrentMemoryContext, "ExprContext", ALLOCSET_DEFAULT_SIZES); estate->datum_context = CurrentMemoryContext; estate->exitlabel = NULL; estate->cur_error = NULL; estate->tuple_store = NULL; if (rsi) { estate->tuple_store_cxt = rsi->econtext->ecxt_per_query_memory; estate->tuple_store_owner = CurrentResourceOwner; estate->tuple_store_desc = rsi->expectedDesc; } else { estate->tuple_store_cxt = NULL; estate->tuple_store_owner = NULL; } estate->rsi = rsi; estate->found_varno = func->found_varno; estate->ndatums = func->ndatums; estate->datums = palloc(sizeof(PLpgSQL_datum *) * estate->ndatums); /* caller is expected to fill the datums array */ estate->eval_tuptable = NULL; estate->eval_processed = 0; if (cinfo->oldtable) { EphemeralNamedRelation enr = palloc(sizeof(EphemeralNamedRelationData)); int rc; enr->md.name = cinfo->oldtable; enr->md.reliddesc = cinfo->relid; enr->md.tupdesc = NULL; enr->md.enrtype = ENR_NAMED_TUPLESTORE; enr->md.enrtuples = 0; enr->reldata = NULL; rc = SPI_register_relation(enr); if (rc < 0) elog(ERROR, "SPI_register_relation failed for \"%s\": %s", cinfo->oldtable, SPI_result_code_string(rc)); } if (cinfo->newtable) { EphemeralNamedRelation enr = palloc(sizeof(EphemeralNamedRelationData)); int rc; enr->md.name = cinfo->newtable; enr->md.reliddesc = cinfo->relid; enr->md.tupdesc = NULL; enr->md.enrtype = ENR_NAMED_TUPLESTORE; enr->md.enrtuples = 0; enr->reldata = NULL; rc = SPI_register_relation(enr); if (rc < 0) elog(ERROR, "SPI_register_relation failed for \"%s\": %s", cinfo->newtable, SPI_result_code_string(rc)); } estate->err_stmt = NULL; estate->err_text = NULL; estate->plugin_info = NULL; } /* * prepare PLpgSQL_checkstate structure * */ static void setup_cstate(PLpgSQL_checkstate *cstate, plpgsql_check_result_info *result_info, plpgsql_check_info *cinfo, bool is_active_mode, bool fake_rtd) { cstate->ci_magic = CI_MAGIC; cstate->decl_volatility = cinfo->volatility; cstate->found_unknown_query = false; cstate->volatility = PROVOLATILE_IMMUTABLE; cstate->skip_volatility_check = (cinfo->rettype == TRIGGEROID || cinfo->rettype == EVENT_TRIGGEROID || cinfo->is_procedure); cstate->estate = NULL; cstate->result_info = result_info; cstate->cinfo = cinfo; cstate->argnames = NIL; cstate->exprs = NIL; cstate->used_variables = NULL; cstate->modif_variables = NULL; cstate->out_variables = NULL; cstate->top_stmt_stack = NULL; cstate->is_active_mode = is_active_mode; cstate->func_oids = NULL; cstate->rel_oids = NULL; cstate->check_cxt = AllocSetContextCreate(CurrentMemoryContext, "plpgsql_check temporary cxt", ALLOCSET_DEFAULT_SIZES); cstate->found_return_query = false; cstate->found_return_dyn_query = false; cstate->fake_rtd = fake_rtd; cstate->safe_variables = NULL; cstate->protected_variables = NULL; cstate->auto_variables = NULL; cstate->typed_variables = NULL; cstate->stop_check = false; cstate->allow_mp = false; cstate->is_dynsql = false; cstate->dyn_stmt_open = NULL; cstate->found_mp = false; cstate->top_stmts = NULL; cstate->pragma_vector.disable_check = false; cstate->pragma_vector.disable_other_warnings = false; cstate->pragma_vector.disable_performance_warnings = false; cstate->pragma_vector.disable_extra_warnings = false; cstate->pragma_vector.disable_security_warnings = false; cstate->pragma_vector.disable_compatibility_warnings = false; cstate->pragma_vector.disable_constants_tracing = false; cstate->pragma_vector.disable_tracer = false; cstate->was_pragma = false; /* try to find oid of plpgsql_check pragma function */ cstate->pragma_foid = plpgsql_check_pragma_func_oid(); /* for simple string constants tracing */ cstate->strconstvars = NULL; } /* * Loads function's configuration * * Before checking function we have to load configuration related to * function. This is function manager job, but we don't use it for checking. */ static int load_configuration(HeapTuple procTuple, bool *reload_config) { Datum datum; bool isnull; int new_nest_level; *reload_config = false; new_nest_level = 0; datum = SysCacheGetAttr(PROCOID, procTuple, Anum_pg_proc_proconfig, &isnull); if (!isnull) { ArrayType *set_items; /* Set per-function configuration parameters */ set_items = DatumGetArrayTypeP(datum); if (set_items != NULL) { /* Need a new GUC nesting level */ new_nest_level = NewGUCNestLevel(); *reload_config = true; ProcessGUCArray(set_items, (superuser() ? PGC_SUSET : PGC_USERSET), PGC_S_SESSION, GUC_ACTION_SAVE); } } return new_nest_level; } /* * Release all plans created in check time * */ static void release_exprs(List *exprs) { ListCell *l; foreach(l, exprs) { PLpgSQL_expr *expr = (PLpgSQL_expr *) lfirst(l); SPI_freeplan(expr->plan); expr->plan = NULL; } } /* * Initialize plpgsql datum to NULL. This routine is used only for function * and trigger parameters so it should not support all dtypes. * */ static void init_datum_dno(PLpgSQL_checkstate *cstate, int dno, bool is_auto, bool is_protected) { switch (cstate->estate->datums[dno]->dtype) { case PLPGSQL_DTYPE_PROMISE: case PLPGSQL_DTYPE_VAR: { PLpgSQL_var *var = (PLpgSQL_var *) cstate->estate->datums[dno]; var->value = (Datum) 0; var->isnull = true; var->freeval = false; } break; case PLPGSQL_DTYPE_REC: { PLpgSQL_rec *rec = (PLpgSQL_rec *) cstate->estate->datums[dno]; plpgsql_check_recval_init(rec); plpgsql_check_recval_assign_tupdesc(cstate, rec, NULL, false); } break; case PLPGSQL_DTYPE_ROW: { PLpgSQL_row *row = (PLpgSQL_row *) cstate->estate->datums[dno]; int fnum; for (fnum = 0; fnum < row->nfields; fnum++) { if (row->varnos[fnum] < 0) continue; /* skip dropped column in row struct */ init_datum_dno(cstate, row->varnos[fnum], is_auto, is_protected); } } break; default: elog(ERROR, "unexpected dtype: %d", cstate->estate->datums[dno]->dtype); } if (is_protected) cstate->protected_variables = bms_add_member(cstate->protected_variables, dno); if (is_auto) cstate->auto_variables = bms_add_member(cstate->auto_variables, dno); } /* * initializing local execution variables * */ static PLpgSQL_datum * copy_plpgsql_datum(PLpgSQL_checkstate *cstate, PLpgSQL_datum *datum) { PLpgSQL_datum *result; switch (datum->dtype) { case PLPGSQL_DTYPE_VAR: case PLPGSQL_DTYPE_PROMISE: { PLpgSQL_var *new = palloc(sizeof(PLpgSQL_var)); memcpy(new, datum, sizeof(PLpgSQL_var)); /* Ensure the value is null (possibly not needed?) */ new->value = 0; new->isnull = true; new->freeval = false; result = (PLpgSQL_datum *) new; } break; case PLPGSQL_DTYPE_REC: { PLpgSQL_rec *new = palloc(sizeof(PLpgSQL_rec)); memcpy(new, datum, sizeof(PLpgSQL_rec)); /* Ensure the value is well initialized with correct type */ plpgsql_check_recval_init(new); plpgsql_check_recval_assign_tupdesc(cstate, new, NULL, false); result = (PLpgSQL_datum *) new; } break; case PLPGSQL_DTYPE_ROW: case PLPGSQL_DTYPE_RECFIELD: /* * Share row definitions and record-field metadata, including * cached field type information, with the compiled function. */ result = datum; break; default: elog(ERROR, "unrecognized dtype: %d", datum->dtype); result = NULL; /* keep compiler quiet */ break; } return result; } #if PG_VERSION_NUM >= 180000 /* * cfunc_hash: hash function for cfunc hash table (copy from funccache.c) * * We need special hash and match functions to deal with the optional * presence of a TupleDesc in the hash keys. As long as we have to do * that, we might as well also be smart about not comparing unused * elements of the argtypes arrays. */ static uint32 cfunc_hash(const void *key, Size keysize) { const CachedFunctionHashKey *k = (const CachedFunctionHashKey *) key; uint32 h; Assert(keysize == sizeof(CachedFunctionHashKey)); /* Hash all the fixed fields except callResultType */ h = DatumGetUInt32(hash_any((const unsigned char *) k, offsetof(CachedFunctionHashKey, callResultType))); /* Incorporate input argument types */ if (k->nargs > 0) h = hash_combine(h, DatumGetUInt32(hash_any((const unsigned char *) k->argtypes, k->nargs * sizeof(Oid)))); /* Incorporate callResultType if present */ if (k->callResultType) h = hash_combine(h, hashRowType(k->callResultType)); return h; } /* * cfunc_match: match function to use with cfunc_hash */ static int cfunc_match(const void *key1, const void *key2, Size keysize) { const CachedFunctionHashKey *k1 = (const CachedFunctionHashKey *) key1; const CachedFunctionHashKey *k2 = (const CachedFunctionHashKey *) key2; Assert(keysize == sizeof(CachedFunctionHashKey)); /* Compare all the fixed fields except callResultType */ if (memcmp(k1, k2, offsetof(CachedFunctionHashKey, callResultType)) != 0) return 1; /* not equal */ /* Compare input argument types (we just verified that nargs matches) */ if (k1->nargs > 0 && memcmp(k1->argtypes, k2->argtypes, k1->nargs * sizeof(Oid)) != 0) return 1; /* not equal */ /* Compare callResultType */ if (k1->callResultType) { if (k2->callResultType) { if (!equalRowTypes(k1->callResultType, k2->callResultType)) return 1; /* not equal */ } else return 1; /* not equal */ } else { if (k2->callResultType) return 1; /* not equal */ } return 0; /* equal */ } #endif void plpgsql_check_HashTableInit(void) { HASHCTL ctl; /* don't allow double-initialization */ Assert(plpgsql_check_HashTable == NULL); memset(&ctl, 0, sizeof(ctl)); #if PG_VERSION_NUM >= 180000 ctl.keysize = sizeof(CachedFunctionHashKey); ctl.entrysize = sizeof(plpgsql_check_HashEnt); ctl.hash = cfunc_hash; ctl.match = cfunc_match; plpgsql_check_HashTable = hash_create("plpgsql_check function cache", FUNCS_PER_USER, &ctl, HASH_ELEM | HASH_FUNCTION | HASH_COMPARE); #else ctl.keysize = sizeof(PLpgSQL_func_hashkey); ctl.entrysize = sizeof(plpgsql_check_HashEnt); plpgsql_check_HashTable = hash_create("plpgsql_check function cache", FUNCS_PER_USER, &ctl, HASH_ELEM | HASH_BLOBS); #endif } /* * Returns true, when function is marked as checked already * */ bool plpgsql_check_is_checked(PLpgSQL_function *func) { plpgsql_check_HashEnt *hentry; if (!plch_func_hashkey(func)) return false; hentry = (plpgsql_check_HashEnt *) hash_search(plpgsql_check_HashTable, (void *) plch_func_hashkey(func), HASH_FIND, NULL); if (hentry != NULL && hentry->fn_xmin == plch_func_xmin(func) && ItemPointerEquals(&hentry->fn_tid, &plch_func_tid(func))) return hentry->is_checked; return false; } /* * Protect function agains repeated checking * */ void plpgsql_check_mark_as_checked(PLpgSQL_function *func) { /* don't try to mark anonymous code blocks */ if (func->fn_oid != InvalidOid) { plpgsql_check_HashEnt *hentry; bool found; hentry = (plpgsql_check_HashEnt *) hash_search(plpgsql_check_HashTable, (void *) plch_func_hashkey(func), HASH_ENTER, &found); hentry->fn_xmin = plch_func_xmin(func); hentry->fn_tid = plch_func_tid(func); hentry->is_checked = true; } } okbob-plpgsql_check-800db79/src/cursors_leaks.c000066400000000000000000000163431526744060500216140ustar00rootroot00000000000000/*------------------------------------------------------------------------- * * cursors_leak.c * * detection unclosed cursors code * * by Pavel Stehule 2013-2026 * *------------------------------------------------------------------------- */ #include "plpgsql_check.h" #include "plpgsql_check_builtins.h" #include "storage/proc.h" #include "utils/builtins.h" #include "utils/guc.h" #include "utils/memutils.h" #if PG_VERSION_NUM >= 180000 #include "utils/funccache.h" #endif bool plpgsql_check_cursors_leaks = true; bool plpgsql_check_cursors_leaks_strict = false; int plpgsql_check_cursors_leaks_level = WARNING; #define MAX_NAMES_PER_STATEMENT 20 typedef struct CursorTrace { int stmtid; void *plugin_info; char *curname; } CursorTrace; typedef struct FunctionTrace { plch_fidentity_hk hk; int ncursors; int cursors_size; CursorTrace *cursors_traces; } FunctionTrace; typedef struct CursorLeaksPlugin2Info { FunctionTrace *ftrace; LocalTransactionId lxid; } CursorLeaksPlugin2Info; static LocalTransactionId traces_lxid = InvalidLocalTransactionId; static HTAB *traces = NULL; static MemoryContext traces_mcxt = NULL; static bool is_active(PLpgSQL_execstate *estate, PLpgSQL_function *func); static void func_setup(PLpgSQL_execstate *estate, PLpgSQL_function *func, plch_fextra *fextra); static void func_end(PLpgSQL_execstate *estate, PLpgSQL_function *func, plch_fextra *fextra); static void stmt_end(PLpgSQL_execstate *estate, PLpgSQL_stmt *stmt, plch_fextra *fextra); static plch_plugin cursors_leaks_plugin = { .is_active = is_active, .func_setup = func_setup, .func_beg = NULL, .func_end = func_end, .stmt_beg = NULL, .stmt_end = stmt_end, .stmt_abort = NULL, .stmt_error = NULL, }; static FunctionTrace * get_function_trace(PLpgSQL_function *func) { bool found; FunctionTrace *ftrace; plch_fidentity_hk hk; LocalTransactionId thislxid = CURRENT_LXID; if (traces == NULL || traces_lxid != thislxid) { HASHCTL ctl; traces_mcxt = AllocSetContextCreate(TopTransactionContext, "plpgsql_check - trace cursors", ALLOCSET_DEFAULT_SIZES); memset(&ctl, 0, sizeof(ctl)); ctl.keysize = sizeof(plch_fidentity_hk); ctl.entrysize = sizeof(FunctionTrace); ctl.hcxt = traces_mcxt; traces = hash_create("plpgsql_check - cursors leaks detection", FUNCS_PER_USER, &ctl, HASH_ELEM | HASH_BLOBS | HASH_CONTEXT); traces_lxid = thislxid; } plch_init_fidentity_hk(&hk, func); ftrace = (FunctionTrace *) hash_search(traces, (void *) &hk, HASH_ENTER, &found); if (!found) { ftrace->ncursors = 0; ftrace->cursors_size = 0; ftrace->cursors_traces = NULL; } return ftrace; } static bool is_active(PLpgSQL_execstate *estate, PLpgSQL_function *func) { return plpgsql_check_cursors_leaks; } static void func_setup(PLpgSQL_execstate *estate, PLpgSQL_function *func, plch_fextra *fextra) { estate->plugin_info = NULL; /* * An active caller can legitimately keep its cursor open during * recursion. */ if (plpgsql_check_cursors_leaks && fextra->use_count == 1) { CursorLeaksPlugin2Info *pinfo; pinfo = palloc(sizeof(CursorLeaksPlugin2Info)); pinfo->ftrace = get_function_trace(func); pinfo->lxid = CURRENT_LXID; estate->plugin_info = pinfo; } } static void func_end(PLpgSQL_execstate *estate, PLpgSQL_function *func, plch_fextra *fextra) { CursorLeaksPlugin2Info *pinfo = (CursorLeaksPlugin2Info *) estate->plugin_info; FunctionTrace *ftrace; int i; if (!pinfo || pinfo->lxid != CURRENT_LXID) return; ftrace = pinfo->ftrace; for (i = 0; i < ftrace->ncursors; i++) { CursorTrace *ct = &ftrace->cursors_traces[i]; /* * Iterate over traced cursors. Remove slots for tracing immediately, * when traced cursor is closed already. */ if (ct->curname && ct->plugin_info == estate->plugin_info) { if (SPI_cursor_find(ct->curname)) { if (plpgsql_check_cursors_leaks_strict) { char *context; context = GetErrorContextStack(); ereport(plpgsql_check_cursors_leaks_level, errcode(ERRCODE_INVALID_CURSOR_STATE), errmsg("cursor is not closed"), errdetail("%s", context)); pfree(context); pfree(ct->curname); ct->stmtid = -1; ct->curname = NULL; } } else { pfree(ct->curname); ct->stmtid = -1; ct->curname = NULL; } } } } static void stmt_end(PLpgSQL_execstate *estate, PLpgSQL_stmt *stmt, plch_fextra *fextra) { CursorLeaksPlugin2Info *pinfo = (CursorLeaksPlugin2Info *) estate->plugin_info; FunctionTrace *ftrace; if (!pinfo) return; if (traces_lxid != CURRENT_LXID || pinfo->lxid != CURRENT_LXID) { pinfo->ftrace = get_function_trace(estate->func); pinfo->lxid = CURRENT_LXID; } ftrace = pinfo->ftrace; if (stmt->cmd_type == PLPGSQL_STMT_OPEN) { int i; int cursors_for_current_stmt = 0; int free_slot = -1; PLpgSQL_var *curvar; char *curname; curvar = (PLpgSQL_var *) (estate->datums[((PLpgSQL_stmt_open *) stmt)->curvar]); Assert(!curvar->isnull); curname = TextDatumGetCString(curvar->value); for (i = 0; i < ftrace->ncursors; i++) { CursorTrace *ct = &ftrace->cursors_traces[i]; if (ct->curname && ct->stmtid == stmt->stmtid) { /* * PLpgSQL open statements reuses portal name and does check * already used portal with already used portal name. So when * the traced name and name in cursor variable is same, we * should not to do this check. This eliminate false alarms. */ if (strcmp(curname, ct->curname) == 0) { pfree(curname); return; } if (SPI_cursor_find(ct->curname)) { if (!plpgsql_check_cursors_leaks_strict) { char *context; context = GetErrorContextStack(); ereport(plpgsql_check_cursors_leaks_level, errcode(ERRCODE_INVALID_CURSOR_STATE), errmsg("cursor \"%s\" is not closed", curvar->refname), errdetail("%s", context)); pfree(context); pfree(ct->curname); ct->stmtid = -1; ct->curname = NULL; } else { cursors_for_current_stmt += 1; } } else { pfree(ct->curname); ct->stmtid = -1; ct->curname = NULL; } } if (ct->stmtid == -1 && free_slot == -1) free_slot = i; } if (cursors_for_current_stmt < MAX_NAMES_PER_STATEMENT) { MemoryContext oldcxt; CursorTrace *ct = NULL; oldcxt = MemoryContextSwitchTo(traces_mcxt); if (free_slot != -1) ct = &ftrace->cursors_traces[free_slot]; else { if (ftrace->ncursors == ftrace->cursors_size) { if (ftrace->cursors_size > 0) { ftrace->cursors_size += 10; ftrace->cursors_traces = repalloc_array(ftrace->cursors_traces, CursorTrace, ftrace->cursors_size); } else { ftrace->cursors_size = 10; ftrace->cursors_traces = palloc_array(CursorTrace, ftrace->cursors_size); } } ct = &ftrace->cursors_traces[ftrace->ncursors++]; } ct->stmtid = stmt->stmtid; ct->plugin_info = estate->plugin_info; ct->curname = pstrdup(curname); MemoryContextSwitchTo(oldcxt); } pfree(curname); } } void plpgsql_check_cursors_leaks_init(void) { plch_register_plugin(&cursors_leaks_plugin); } okbob-plpgsql_check-800db79/src/expr_walk.c000066400000000000000000001005451526744060500207270ustar00rootroot00000000000000/*------------------------------------------------------------------------- * * expr_walk.c * * set of Query/Expr walkers * * by Pavel Stehule 2013-2026 * *------------------------------------------------------------------------- */ #include "plpgsql_check.h" #include "nodes/makefuncs.h" #include "nodes/nodeFuncs.h" #include "catalog/pg_class.h" #include "catalog/pg_namespace.h" #include "catalog/pg_proc.h" #include "catalog/pg_type.h" #include "mb/pg_wchar.h" #include "nodes/nodeFuncs.h" #include "utils/array.h" #include "utils/builtins.h" #include "utils/lsyscache.h" typedef struct check_funcexpr_walker_params { PLpgSQL_checkstate *cstate; PLpgSQL_expr *expr; char *query_str; } check_funcexpr_walker_params; static int check_fmt_string(const char *fmt, List *args, int location, check_funcexpr_walker_params *wp, bool *is_error, int *unsafe_expr_location, bool no_error); static bool append_format_array_args(Node *node, List **args) { if (IsA(node, ArrayExpr)) { ArrayExpr *array = (ArrayExpr *) node; if (array->multidims) { ListCell *lc; foreach(lc, array->elements) { if (!append_format_array_args(lfirst(lc), args)) return false; } } else *args = list_concat(*args, list_copy(array->elements)); return true; } else if (IsA(node, Const)) { Const *c = (Const *) node; ArrayType *array; Oid elemtype; int16 typlen; bool typbyval; char typalign; Datum *values; bool *nulls; int nelems; int i; /* format() treats a NULL variadic array as an empty array. */ if (c->constisnull) return true; if (!OidIsValid(get_base_element_type(c->consttype))) return false; array = DatumGetArrayTypeP(c->constvalue); elemtype = ARR_ELEMTYPE(array); get_typlenbyvalalign(elemtype, &typlen, &typbyval, &typalign); deconstruct_array(array, elemtype, typlen, typbyval, typalign, &values, &nulls, &nelems); for (i = 0; i < nelems; i++) { Const *element; element = makeConst(elemtype, -1, get_typcollation(elemtype), typlen, values[i], nulls[i], typbyval); element->location = c->location; *args = lappend(*args, element); } pfree(values); pfree(nulls); return true; } return false; } /* * Include the format string at index zero, as in an ordinary call. * NIL means that the number and values of variadic arguments are unknown. */ static List * format_get_args(FuncExpr *fexpr) { List *args; if (!fexpr->funcvariadic) return fexpr->args; Assert(list_length(fexpr->args) == 2); args = list_make1(linitial(fexpr->args)); if (!append_format_array_args(lsecond(fexpr->args), &args)) return NIL; return args; } /* * Send to ouput all not yet displayed relations, operators and functions. */ static bool detect_dependency_walker(Node *node, void *context) { PLpgSQL_checkstate *cstate = (PLpgSQL_checkstate *) context; plpgsql_check_result_info *ri = cstate->result_info; if (node == NULL) return false; if (IsA(node, Query)) { Query *query = (Query *) node; ListCell *lc; foreach(lc, query->rtable) { RangeTblEntry *rt = (RangeTblEntry *) lfirst(lc); if (rt->rtekind == RTE_RELATION) { if (!bms_is_member(rt->relid, cstate->rel_oids)) { plpgsql_check_put_dependency(ri, "RELATION", rt->relid, get_namespace_name(get_rel_namespace(rt->relid)), get_rel_name(rt->relid), NULL); cstate->rel_oids = bms_add_member(cstate->rel_oids, rt->relid); } } } if (query->utilityStmt && IsA(query->utilityStmt, CallStmt)) { CallStmt *callstmt = (CallStmt *) query->utilityStmt; detect_dependency_walker((Node *) callstmt->funcexpr, context); } return query_tree_walker((Query *) node, detect_dependency_walker, context, 0); } if (IsA(node, FuncExpr)) { FuncExpr *fexpr = (FuncExpr *) node; if (get_func_namespace(fexpr->funcid) != PG_CATALOG_NAMESPACE) { if (!bms_is_member(fexpr->funcid, cstate->func_oids)) { StringInfoData str; ListCell *lc; bool is_first = true; char prokind = get_func_prokind(fexpr->funcid); initStringInfo(&str); appendStringInfoChar(&str, '('); foreach(lc, fexpr->args) { Node *expr = (Node *) lfirst(lc); if (!is_first) appendStringInfoChar(&str, ','); else is_first = false; appendStringInfoString(&str, format_type_be(exprType(expr))); } appendStringInfoChar(&str, ')'); plpgsql_check_put_dependency(ri, prokind == PROKIND_PROCEDURE ? "PROCEDURE" : "FUNCTION", fexpr->funcid, get_namespace_name(get_func_namespace(fexpr->funcid)), get_func_name(fexpr->funcid), str.data); pfree(str.data); cstate->func_oids = bms_add_member(cstate->func_oids, fexpr->funcid); } } } if (IsA(node, OpExpr)) { OpExpr *opexpr = (OpExpr *) node; if (plpgsql_check_get_op_namespace(opexpr->opno) != PG_CATALOG_NAMESPACE) { StringInfoData str; Oid lefttype; Oid righttype; op_input_types(opexpr->opno, &lefttype, &righttype); initStringInfo(&str); appendStringInfoChar(&str, '('); if (lefttype != InvalidOid) appendStringInfoString(&str, format_type_be(lefttype)); else appendStringInfoChar(&str, '-'); appendStringInfoChar(&str, ','); if (righttype != InvalidOid) appendStringInfoString(&str, format_type_be(righttype)); else appendStringInfoChar(&str, '-'); appendStringInfoChar(&str, ')'); plpgsql_check_put_dependency(ri, "OPERATOR", opexpr->opno, get_namespace_name(plpgsql_check_get_op_namespace(opexpr->opno)), get_opname(opexpr->opno), str.data); pfree(str.data); } } return expression_tree_walker(node, detect_dependency_walker, context); } void plpgsql_check_detect_dependency(PLpgSQL_checkstate *cstate, Query *query) { if (cstate->result_info->format != PLPGSQL_SHOW_DEPENDENCY_FORMAT_TABULAR) return; detect_dependency_walker((Node *) query, cstate); } /* * When sequence related functions has constant oid parameter, then ensure, so * this oid is related to some sequnce object. * */ static bool check_funcexpr_walker(Node *node, void *context) { if (node == NULL) return false; if (IsA(node, Query)) { return query_tree_walker((Query *) node, check_funcexpr_walker, context, 0); } if (IsA(node, FuncExpr)) { FuncExpr *fexpr = (FuncExpr *) node; switch (fexpr->funcid) { case NEXTVAL_OID: case CURRVAL_OID: case SETVAL_OID: case SETVAL2_OID: { Node *first_arg = linitial(fexpr->args); int location = fexpr->location; if (first_arg && IsA(first_arg, Const)) { Const *c = (Const *) first_arg; if (c->consttype == REGCLASSOID && !c->constisnull) { Oid classid; if (c->location != -1) location = c->location; classid = DatumGetObjectId(c->constvalue); if (get_rel_relkind(classid) != RELKIND_SEQUENCE) { char message[1024]; check_funcexpr_walker_params *wp; char *relname; wp = (check_funcexpr_walker_params *) context; relname = get_rel_name(classid); if (relname) snprintf(message, sizeof(message), "\"%s\" is not a sequence", relname); else snprintf(message, sizeof(message), "\"%u\" is not a sequence", classid); plpgsql_check_put_error(wp->cstate, ERRCODE_WRONG_OBJECT_TYPE, 0, message, NULL, NULL, PLPGSQL_CHECK_ERROR, location, wp->query_str, NULL); } } } } break; case FORMAT_0PARAM_OID: case FORMAT_NPARAM_OID: { /* We can do check only when first argument is constant */ Node *first_arg = linitial(fexpr->args); if (first_arg && IsA(first_arg, Const)) { Const *c = (Const *) first_arg; if (c->consttype == TEXTOID && !c->constisnull) { char *fmt = TextDatumGetCString(c->constvalue); List *args = format_get_args(fexpr); check_funcexpr_walker_params *wp; int required_nargs; bool is_error; wp = (check_funcexpr_walker_params *) context; required_nargs = check_fmt_string(fmt, args, c->location, wp, &is_error, NULL, false); if (!is_error && required_nargs != -1) { if (required_nargs + 1 != list_length(args)) plpgsql_check_put_error(wp->cstate, 0, 0, "unused parameters of function \"format\"", NULL, NULL, PLPGSQL_CHECK_WARNING_OTHERS, c->location, wp->query_str, NULL); } } } } } } return expression_tree_walker(node, check_funcexpr_walker, context); } void plpgsql_check_funcexpr(PLpgSQL_checkstate *cstate, Query *query, char *query_str) { check_funcexpr_walker_params wp; wp.cstate = cstate; wp.query_str = query_str; wp.expr = NULL; check_funcexpr_walker((Node *) query, &wp); } /* * Aux functions for checking format string of function format */ /* * Support macros for text_format() */ #define ADVANCE_PARSE_POINTER(ptr,end_ptr) \ do { \ if (++(ptr) >= (end_ptr)) \ { \ if (wp) \ plpgsql_check_put_error(wp->cstate, \ ERRCODE_INVALID_PARAMETER_VALUE, 0, \ "unterminated format() type specifier", \ NULL, \ "For a single \"%%\" use \"%%%%\".", \ PLPGSQL_CHECK_ERROR, \ location, \ wp->query_str, NULL); \ *is_error = true; \ } \ } while (0) /* * Parse contiguous digits as a decimal number. * * Returns true if some digits could be parsed. * The value is returned into *value, and *ptr is advanced to the next * character to be parsed. */ static bool text_format_parse_digits(const char **ptr, const char *end_ptr, int *value, int location, check_funcexpr_walker_params *wp, bool *is_error) { bool found = false; const char *cp = *ptr; int val = 0; *is_error = false; while (*cp >= '0' && *cp <= '9') { int newval = val * 10 + (*cp - '0'); if (newval / 10 != val) /* overflow? */ { if (wp) plpgsql_check_put_error(wp->cstate, ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE, 0, "number is out of range", NULL, NULL, PLPGSQL_CHECK_ERROR, location, wp->query_str, NULL); *is_error = true; return false; } val = newval; ADVANCE_PARSE_POINTER(cp, end_ptr); if (*is_error) return false; found = true; } *ptr = cp; *value = val; return found; } /* * reduced code from varlena.c */ static const char * text_format_parse_format(const char *start_ptr, const char *end_ptr, int *argpos, int *widthpos, int *width, bool *left_justify, int location, check_funcexpr_walker_params *wp, bool *is_error) { const char *cp = start_ptr; int n; bool found; /* set defaults for output parameters */ *argpos = -1; *widthpos = -1; *width = 0; *left_justify = false; *is_error = false; /* try to identify first number */ found = text_format_parse_digits(&cp, end_ptr, &n, location, wp, is_error); if (*is_error) return NULL; if (found) { if (*cp != '$') { /* Must be just a width and a type, so we're done */ *width = n; return cp; } /* The number was argument position */ *argpos = n; /* Explicit 0 for argument index is immediately refused */ if (n == 0) { if (wp) plpgsql_check_put_error(wp->cstate, ERRCODE_INVALID_PARAMETER_VALUE, 0, "format specifies argument 0, but arguments are numbered from 1", NULL, NULL, PLPGSQL_CHECK_ERROR, location, wp->query_str, NULL); *is_error = true; return NULL; } ADVANCE_PARSE_POINTER(cp, end_ptr); if (*is_error) return NULL; } /* Handle flags (only minus is supported now) */ while (*cp == '-') { *left_justify = true; ADVANCE_PARSE_POINTER(cp, end_ptr); if (*is_error) return NULL; } if (*cp == '*') { /* Handle indirect width */ ADVANCE_PARSE_POINTER(cp, end_ptr); if (*is_error) return NULL; found = text_format_parse_digits(&cp, end_ptr, &n, location, wp, is_error); if (*is_error) return NULL; if (found) { /* number in this position must be closed by $ */ if (*cp != '$') { if (wp) plpgsql_check_put_error(wp->cstate, ERRCODE_INVALID_PARAMETER_VALUE, 0, "width argument position must be ended by \"$\"", NULL, NULL, PLPGSQL_CHECK_ERROR, location, wp->query_str, NULL); *is_error = true; return NULL; } /* The number was width argument position */ *widthpos = n; /* Explicit 0 for argument index is immediately refused */ if (n == 0) { if (wp) plpgsql_check_put_error(wp->cstate, ERRCODE_INVALID_PARAMETER_VALUE, 0, "format specifies argument 0, but arguments are numbered from 1", NULL, NULL, PLPGSQL_CHECK_ERROR, location, wp->query_str, NULL); *is_error = true; return NULL; } ADVANCE_PARSE_POINTER(cp, end_ptr); if (*is_error) return NULL; } else *widthpos = 0; /* width's argument position is unspecified */ } else { /* Check for direct width specification */ (void) text_format_parse_digits(&cp, end_ptr, width, location, wp, is_error); if (*is_error) return NULL; } /* cp should now be pointing at type character */ return cp; } static void append_format_string(StringInfo buf, const char *str, int width, bool left_justify) { int padding = width > 0 ? Max(width - pg_mbstrlen(str), 0) : 0; if (!left_justify) appendStringInfoSpaces(buf, padding); appendStringInfoString(buf, str); if (left_justify) appendStringInfoSpaces(buf, padding); } #define TOO_FEW_ARGUMENTS_CHECK(arg, nargs) \ if ((nargs) >= 0 && (arg) > (nargs)) \ { \ if (wp) \ plpgsql_check_put_error(wp->cstate, \ ERRCODE_INVALID_PARAMETER_VALUE, 0, \ "too few arguments for format()", \ NULL, \ NULL, \ PLPGSQL_CHECK_ERROR, \ location, \ wp->query_str, NULL); \ *is_error = true; \ return -1; \ } /* * Returns string based on expected result of "format" function. * Try to use traced constant, when are available. */ char * plpgsql_check_get_formatted_string(PLpgSQL_checkstate *cstate, const char *fmt, FuncExpr *fexpr, bool *found_ident_placeholder, bool *found_literal_placeholder, bool *expr_is_const) { StringInfoData sinfo; const char *cp; const char *end_ptr = fmt + strlen(fmt); List *args = format_get_args(fexpr); int nargs = list_length(args); int64 arg = 1; int64 _arg; bool is_error; *found_ident_placeholder = false; *found_literal_placeholder = false; *expr_is_const = true; if (!args) { *expr_is_const = false; return NULL; } initStringInfo(&sinfo); /* Scan format string, looking for conversion specifiers. */ for (cp = fmt; cp < end_ptr; cp++) { int argpos; int widthpos; int width; bool left_justify; if (*cp != '%') { appendStringInfoChar(&sinfo, *cp); continue; } if (++cp >= end_ptr) { pfree(sinfo.data); return NULL; } if (*cp == '%') { appendStringInfoChar(&sinfo, '%'); continue; } /* Parse the optional portions of the format specifier */ cp = text_format_parse_format(cp, end_ptr, &argpos, &widthpos, &width, &left_justify, -1, NULL, &is_error); if (is_error || strchr("sIL", *cp) == NULL) { pfree(sinfo.data); return NULL; } if (widthpos >= 0) { Node *widthnode; char *widthstr; if (widthpos > 0) { if ((int64) widthpos + 1 > nargs) { pfree(sinfo.data); return NULL; } arg = (int64) widthpos + 1; } else { if (++arg > nargs) { pfree(sinfo.data); return NULL; } } widthnode = list_nth(args, arg - 1); if (IsA(widthnode, Const) && ((Const *) widthnode)->constisnull) width = 0; else { widthstr = plpgsql_check_get_const_string(cstate, widthnode, NULL); if (!widthstr) { pfree(sinfo.data); *expr_is_const = false; return NULL; } width = pg_strtoint32(widthstr); } } if (width < 0) { if (width == PG_INT32_MIN) ereport(ERROR, (errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE), errmsg("number is out of range"))); width = -width; left_justify = true; } _arg = argpos >= 1 ? (int64) argpos + 1 : arg + 1; if (_arg <= nargs) { Node *argnode = list_nth(args, _arg - 1); char *str; bool isnull = IsA(argnode, Const) && ((Const *) argnode)->constisnull; str = plpgsql_check_get_const_string(cstate, argnode, NULL); if (*cp == 'I') { if (isnull) { pfree(sinfo.data); return NULL; } if (!str) { append_format_string(&sinfo, "\"%I\"", width, left_justify); *found_ident_placeholder = true; *expr_is_const = false; } else append_format_string(&sinfo, quote_identifier(str), width, left_justify); } else if (*cp == 'L') { if (isnull) append_format_string(&sinfo, "NULL", width, left_justify); else if (!str) { /* * Original idea was used external parameter, but external * parameters requires known type, so most safe value is * NULL instead. */ append_format_string(&sinfo, " null ", width, left_justify); *found_literal_placeholder = true; *expr_is_const = false; } else { char *qstr = quote_literal_cstr(str); append_format_string(&sinfo, qstr, width, left_justify); pfree(qstr); } } else { if (isnull) str = ""; if (!str) { pfree(sinfo.data); *expr_is_const = false; return NULL; } else append_format_string(&sinfo, str, width, left_justify); } } if (argpos >= 1) { if ((int64) argpos + 1 > nargs) { pfree(sinfo.data); return NULL; } arg = (int64) argpos + 1; } else { if (++arg > nargs) { pfree(sinfo.data); return NULL; } } } return sinfo.data; } /* * Returns number of rquired arguments or -1 when we cannot detect this number. * When no_error is true, then this function doesn't raise a error or warning. */ static int check_fmt_string(const char *fmt, List *args, int location, check_funcexpr_walker_params *wp, bool *is_error, int *unsafe_expr_location, bool no_error) { const char *cp; const char *end_ptr = fmt + strlen(fmt); int nargs = args ? list_length(args) : -1; int required_nargs = args ? 0 : -1; /* Parsed positions can be INT_MAX; leave room for the format argument. */ int64 arg = 1; *is_error = false; /* Scan format string, looking for conversion specifiers. */ for (cp = fmt; cp < end_ptr; cp++) { int argpos; int widthpos; int width; bool left_justify; if (*cp != '%') continue; ADVANCE_PARSE_POINTER(cp, end_ptr); if (*is_error) return -1; if (*cp == '%') continue; /* Parse the optional portions of the format specifier */ cp = text_format_parse_format(cp, end_ptr, &argpos, &widthpos, &width, &left_justify, location, wp, is_error); if (*is_error) return -1; /* * Next we should see the main conversion specifier. Whether or not * an argument position was present, it's known that at least one * character remains in the string at this point. Experience suggests * that it's worth checking that that character is one of the expected * ones before we try to fetch arguments, so as to produce the least * confusing response to a mis-formatted specifier. */ if (strchr("sIL", *cp) == NULL) { StringInfoData sinfo; initStringInfo(&sinfo); appendStringInfo(&sinfo, "unrecognized format() type specifier \"%c\"", *cp); if (!no_error) plpgsql_check_put_error(wp->cstate, ERRCODE_INVALID_PARAMETER_VALUE, 0, sinfo.data, NULL, NULL, PLPGSQL_CHECK_ERROR, location, wp->query_str, NULL); pfree(sinfo.data); *is_error = true; return -1; } if (widthpos >= 0) { if (widthpos > 0) { TOO_FEW_ARGUMENTS_CHECK((int64) widthpos + 1, nargs); arg = (int64) widthpos + 1; required_nargs = -1; } else { TOO_FEW_ARGUMENTS_CHECK(++arg, nargs); if (required_nargs != -1) required_nargs += 1; } } /* Check safety of argument againt SQL injection when it is required */ if (unsafe_expr_location) { if (*cp == 's') { int64 argn = argpos >= 1 ? (int64) argpos + 1 : arg + 1; /* * this is usually called after format check, but better be * safe */ if (argn <= nargs) { if (plpgsql_check_is_sql_injection_vulnerable(wp->cstate, wp->expr, list_nth(args, argn - 1), unsafe_expr_location)) { /* found vulnerability, stop */ *is_error = false; return -1; } } } } if (argpos >= 1) { TOO_FEW_ARGUMENTS_CHECK((int64) argpos + 1, nargs); arg = (int64) argpos + 1; required_nargs = -1; } else { TOO_FEW_ARGUMENTS_CHECK(++arg, nargs); if (required_nargs != -1) required_nargs += 1; } } return required_nargs; } /* * Try to detect relations in query */ static bool has_rtable_walker(Node *node, void *context) { if (node == NULL) return false; if (IsA(node, Query)) { Query *query = (Query *) node; bool has_relation = false; ListCell *lc; foreach(lc, query->rtable) { RangeTblEntry *rte = (RangeTblEntry *) lfirst(lc); if (rte->rtekind == RTE_RELATION) { has_relation = true; break; } } if (has_relation) { return true; } else return query_tree_walker(query, has_rtable_walker, context, 0); } return expression_tree_walker(node, has_rtable_walker, context); } /* * Returns true, if query use any relation */ bool plpgsql_check_has_rtable(Query *query) { return has_rtable_walker((Node *) query, NULL); } /* * Try to identify constraint where variable from one side is implicitly casted to * parameter type of second side. It can symptom of parameter wrong type. * */ static bool contain_fishy_cast_walker(Node *node, void *context) { if (node == NULL) return false; if (IsA(node, OpExpr)) { OpExpr *opexpr = (OpExpr *) node; if (!opexpr->opretset && opexpr->opresulttype == BOOLOID && list_length(opexpr->args) == 2) { Node *l1 = linitial(opexpr->args); Node *l2 = lsecond(opexpr->args); Param *param = NULL; FuncExpr *fexpr = NULL; if (IsA(l1, Param)) param = (Param *) l1; else if (IsA(l1, FuncExpr)) fexpr = (FuncExpr *) l1; if (IsA(l2, Param)) param = (Param *) l2; else if (IsA(l2, FuncExpr)) fexpr = (FuncExpr *) l2; if (param != NULL && fexpr != NULL) { if (param->paramkind != PARAM_EXTERN) return false; if (fexpr->funcformat != COERCE_IMPLICIT_CAST || fexpr->funcretset || list_length(fexpr->args) != 1 || param->paramtype != fexpr->funcresulttype) return false; if (!IsA(linitial(fexpr->args), Var)) return false; *((Param **) context) = param; return true; } } } return expression_tree_walker(node, contain_fishy_cast_walker, context); } bool plpgsql_check_qual_has_fishy_cast(PlannedStmt *plannedstmt, Plan *plan, Param **param) { ListCell *lc; if (plan == NULL) return false; if (contain_fishy_cast_walker((Node *) plan->qual, param)) return true; if (plpgsql_check_qual_has_fishy_cast(plannedstmt, innerPlan(plan), param)) return true; if (plpgsql_check_qual_has_fishy_cast(plannedstmt, outerPlan(plan), param)) return true; foreach(lc, plan->initPlan) { SubPlan *subplan = (SubPlan *) lfirst(lc); Plan *s_plan = exec_subplan_get_plan(plannedstmt, subplan); if (plpgsql_check_qual_has_fishy_cast(plannedstmt, s_plan, param)) return true; } return false; } #define QUOTE_IDENT_OID 1282 #define QUOTE_LITERAL_OID 1283 #define QUOTE_NULLABLE_OID 1289 /* * Recursive iterate to deep and search extern params with typcategory "S", and check * if this value is sanitized. Flag is_safe is true, when result is safe. */ bool plpgsql_check_is_sql_injection_vulnerable(PLpgSQL_checkstate *cstate, PLpgSQL_expr *expr, Node *node, int *location) { if (IsA(node, FuncExpr)) { FuncExpr *fexpr = (FuncExpr *) node; bool is_vulnerable = false; ListCell *lc; foreach(lc, fexpr->args) { Node *arg = lfirst(lc); if (plpgsql_check_is_sql_injection_vulnerable(cstate, expr, arg, location)) { is_vulnerable = true; break; } } if (is_vulnerable) { bool typispreferred; char typcategory; get_type_category_preferred(fexpr->funcresulttype, &typcategory, &typispreferred); if (typcategory == 'S') { switch (fexpr->funcid) { case QUOTE_IDENT_OID: case QUOTE_LITERAL_OID: case QUOTE_NULLABLE_OID: return false; case FORMAT_0PARAM_OID: case FORMAT_NPARAM_OID: { /* * We can do check only when first argument is * constant */ char *fmt; List *args = format_get_args(fexpr); int loc; fmt = plpgsql_check_get_const_string(cstate, linitial(fexpr->args), &loc); if (fmt && args) { check_funcexpr_walker_params wp; bool is_error; wp.cstate = cstate; wp.expr = expr; wp.query_str = expr->query; *location = -1; check_fmt_string(fmt, args, loc, &wp, &is_error, location, true); /* * only in this case, "format" function * obviously sanitize parameters */ if (!is_error) return *location != -1; } } break; default: /* do nothing */ ; } return true; } } return false; } else if (IsA(node, OpExpr)) { OpExpr *op = (OpExpr *) node; bool is_vulnerable = false; ListCell *lc; foreach(lc, op->args) { Node *arg = lfirst(lc); if (plpgsql_check_is_sql_injection_vulnerable(cstate, expr, arg, location)) { is_vulnerable = true; break; } } if (is_vulnerable) { bool typispreferred; char typcategory; get_type_category_preferred(op->opresulttype, &typcategory, &typispreferred); if (typcategory == 'S') { char *opname = get_opname(op->opno); bool result = false; if (opname) { result = strcmp(opname, "||") == 0; pfree(opname); } return result; } } return false; } else if (IsA(node, NamedArgExpr)) { return plpgsql_check_is_sql_injection_vulnerable(cstate, expr, (Node *) ((NamedArgExpr *) node)->arg, location); } else if (IsA(node, RelabelType)) { return plpgsql_check_is_sql_injection_vulnerable(cstate, expr, (Node *) ((RelabelType *) node)->arg, location); } else if (IsA(node, Param)) { Param *p = (Param *) node; if (p->paramkind == PARAM_EXTERN || p->paramkind == PARAM_EXEC) { bool typispreferred; char typcategory; get_type_category_preferred(p->paramtype, &typcategory, &typispreferred); if (typcategory == 'S') { if (p->paramkind == PARAM_EXTERN && p->paramid > 0 && p->location != -1) { int dno = p->paramid - 1; /* * When paramid looks well and related datum is variable * with same type, then we can check, if this variable has * sanitized content already. */ if (expr && bms_is_member(dno, expr->paramnos)) { PLpgSQL_var *var = (PLpgSQL_var *) cstate->estate->datums[dno]; if (var->dtype == PLPGSQL_DTYPE_VAR) { if (var->datatype->typoid == p->paramtype) { if (bms_is_member(dno, cstate->safe_variables)) return false; } } } } *location = p->location; return true; } } return false; } else return false; } /* * These checker function returns volatility or immutability of any function. * Special case is plpgsql_check_pragma function. This function is vollatile, * but we should to fake flags to be immutable - becase we would not to change * result of analyzes and we know so this function has not any negative side * effect. */ static bool contain_volatile_functions_checker(Oid func_id, void *context) { PLpgSQL_checkstate *cstate = (PLpgSQL_checkstate *) context; if (func_id == cstate->pragma_foid) return false; return (func_volatile(func_id) == PROVOLATILE_VOLATILE); } static bool contain_mutable_functions_checker(Oid func_id, void *context) { PLpgSQL_checkstate *cstate = (PLpgSQL_checkstate *) context; if (func_id == cstate->pragma_foid) return false; return (func_volatile(func_id) != PROVOLATILE_IMMUTABLE); } static bool contain_volatile_functions_walker(Node *node, void *context) { if (node == NULL) return false; /* Check for volatile functions in node itself */ if (check_functions_in_node(node, contain_volatile_functions_checker, context)) return true; if (IsA(node, NextValueExpr)) { /* NextValueExpr is volatile */ return true; } /* * See notes in contain_mutable_functions_walker about why we treat * MinMaxExpr, XmlExpr, and CoerceToDomain as immutable, while * SQLValueFunction is stable. Hence, none of them are of interest here. */ /* Recurse to check arguments */ if (IsA(node, Query)) { /* Recurse into subselects */ return query_tree_walker((Query *) node, contain_volatile_functions_walker, context, 0); } return expression_tree_walker(node, contain_volatile_functions_walker, context); } static bool contain_mutable_functions_walker(Node *node, void *context) { if (node == NULL) return false; /* Check for mutable functions in node itself */ if (check_functions_in_node(node, contain_mutable_functions_checker, context)) return true; if (IsA(node, SQLValueFunction)) { /* all variants of SQLValueFunction are stable */ return true; } if (IsA(node, NextValueExpr)) { /* NextValueExpr is volatile */ return true; } /* * It should be safe to treat MinMaxExpr as immutable, because it will * depend on a non-cross-type btree comparison function, and those should * always be immutable. Treating XmlExpr as immutable is more dubious, * and treating CoerceToDomain as immutable is outright dangerous. But we * have done so historically, and changing this would probably cause more * problems than it would fix. In practice, if you have a non-immutable * domain constraint you are in for pain anyhow. */ /* Recurse to check arguments */ if (IsA(node, Query)) { /* Recurse into subselects */ return query_tree_walker((Query *) node, contain_mutable_functions_walker, context, 0); } return expression_tree_walker(node, contain_mutable_functions_walker, context); } /* * This is same like Postgres buildin function, but it ignores used * plpgsql_check pragma function */ bool plpgsql_check_contain_volatile_functions(Node *clause, PLpgSQL_checkstate *cstate) { return contain_volatile_functions_walker(clause, cstate); } bool plpgsql_check_contain_mutable_functions(Node *clause, PLpgSQL_checkstate *cstate) { return contain_mutable_functions_walker(clause, cstate); } static bool has_external_param_with_paramid(Node *node, void *context) { if (node == NULL) return false; if (IsA(node, Param)) { Param *p = (Param *) node; if (p->paramkind == PARAM_EXTERN && p->paramid > 0 && p->location != -1) { int dno = p->paramid - 1; if (dno == *((int *) context)) return true; } } return expression_tree_walker(node, has_external_param_with_paramid, context); } /* * Ignore the implicit old-container fetch in a subscript assignment, but * count reads in its bounds and assigned value. */ bool plpgsql_check_vardno_is_used_for_reading(Node *node, int dno) { if (node == NULL) return false; if (IsA(node, SubscriptingRef)) { SubscriptingRef *sbsref = (SubscriptingRef *) node; if (sbsref->refassgnexpr != NULL) return has_external_param_with_paramid((Node *) sbsref->refupperindexpr, &dno) || has_external_param_with_paramid((Node *) sbsref->reflowerindexpr, &dno) || has_external_param_with_paramid((Node *) sbsref->refassgnexpr, &dno); } return has_external_param_with_paramid(node, (void *) &dno); } okbob-plpgsql_check-800db79/src/fextra_cache.c000066400000000000000000000166621526744060500213550ustar00rootroot00000000000000/*------------------------------------------------------------------------- * * fextra_cache * * cache with function extra informations - mostly AST based * * by Pavel Stehule 2013-2026 * *------------------------------------------------------------------------- */ #include "postgres.h" #include "plpgsql.h" #include "plpgsql_check.h" #if PG_VERSION_NUM >= 190000 #include "utils/hsearch.h" #endif #include "utils/inval.h" #include "utils/lsyscache.h" #include "utils/syscache.h" static HTAB *fextra_ht = NULL; static MemoryContext fextra_mcxt = NULL; typedef struct { plch_fextra *fextra; int parentid; int *naturalid; int current_deep; /* starts from zero */ } fextra_init_context; static void init_fextra_stmt_walker(PLpgSQL_stmt *stmt, fextra_init_context *context) { fextra_init_context loccontext; plch_fextra *fextra = context->fextra; int stmtid = stmt->stmtid; /* * statement ids are starts by one. For simplicity don't change base to * zero. */ fextra->parentids[stmtid] = context->parentid; fextra->naturalids[stmtid] = ++(*context->naturalid); fextra->levels[stmtid] = context->current_deep + 1; if (context->current_deep > fextra->max_deep) fextra->max_deep = context->current_deep; loccontext.fextra = fextra; loccontext.parentid = stmtid; loccontext.naturalid = context->naturalid; loccontext.current_deep = context->current_deep + 1; plch_statement_tree_walker(stmt, init_fextra_stmt_walker, NULL, &loccontext); } void plch_init_fidentity_hk(plch_fidentity_hk *hk, PLpgSQL_function *func) { memset(hk, 0, sizeof(plch_fidentity_hk)); hk->fn_oid = func->fn_oid; hk->db_oid = MyDatabaseId; hk->fn_xmin = plch_func_xmin(func); hk->fn_tid = plch_func_tid(func); } static void pin_func(PLpgSQL_function *func) { /* * We cannot pin inline block due to two reasons: * * 1. After error, the Assert(func->cfunc.use_count == 0); is executed * before exec memory is released and our abort callback is raised. * * 2. There is not any reason, why to do it. inline block uses short life * contexts for everything - including fextra, and then fextra cannot be * corrupted. Unfortunately inside plpgsql_free_function_memory is * free_stmt called before MemoryContextDelete(func->fn_cxt) */ Assert(OidIsValid(func->fn_oid)); plch_use_count(func)++; } static void unpin_func(PLpgSQL_function *func) { Assert(OidIsValid(func->fn_oid)); plch_use_count(func)--; } static void fextra_CacheObjectCallback(Datum arg, int cacheid, uint32 hashValue) { HASH_SEQ_STATUS status; plch_fextra *fextra; if (!fextra_ht) return; /* Currently we just flush all entries; hard to be smarter ... */ hash_seq_init(&status, fextra_ht); while ((fextra = (plch_fextra *) hash_seq_search(&status)) != NULL) { if (hashValue == 0 || fextra->hashValue == hashValue) fextra->is_valid = false; if (!fextra->is_valid && fextra->use_count == 0) { MemoryContextDelete(fextra->mcxt); if (hash_search(fextra_ht, &fextra->hk, HASH_REMOVE, NULL) == NULL) elog(ERROR, "hash table corrupted"); } } } plch_fextra * plch_get_fextra(PLpgSQL_function *func) { plch_fextra *fextra; if (OidIsValid(func->fn_oid)) { HASHCTL ctl; plch_fidentity_hk hk; bool found; /* * Prepare persistent cache - don't do this for anonymous block */ if (!fextra_mcxt) { fextra_mcxt = AllocSetContextCreate(TopMemoryContext, "plpgsql_check - fextra cache context", ALLOCSET_DEFAULT_MINSIZE, ALLOCSET_DEFAULT_INITSIZE, ALLOCSET_DEFAULT_MAXSIZE); Assert(fextra_ht == NULL); memset(&ctl, 0, sizeof(ctl)); ctl.keysize = sizeof(plch_fidentity_hk); ctl.entrysize = sizeof(plch_fextra); ctl.hcxt = fextra_mcxt; fextra_ht = hash_create("plpgsql_check function fextra cache", FUNCS_PER_USER, &ctl, HASH_ELEM | HASH_BLOBS | HASH_CONTEXT); CacheRegisterSyscacheCallback(PROCOID, fextra_CacheObjectCallback, (Datum) 0); } plch_init_fidentity_hk(&hk, func); fextra = (plch_fextra *) hash_search(fextra_ht, (void *) &hk, HASH_ENTER, &found); /* * Although, there are not threads, there can be "semi parallel" * access to fextra_ht due fextra_CacheObjectCallback, that can be * emitted by any catalog operations. So immediately set fields, that * can be checked used by fextra_CacheObjectCallback. */ if (!found) { fextra->hashValue = 0; fextra->mcxt = NULL; fextra->use_count = 0; fextra->is_valid = false; } } else { /* * one shot fextra for anonymous blocks. These structures are not * stored in fextra_ht, so there is not risk with * fextra_CacheObjectCallback */ fextra = palloc0(sizeof(plch_fextra)); } /* * pin entry first as protection against dropping in * fextra_CacheObjectCallback */ fextra->use_count++; PG_TRY(); { if (!fextra->is_valid && fextra->use_count == 1) { MemoryContext oldcxt; char *fn_name = NULL; char *fn_namespacename = NULL; int naturalid = 0; fextra_init_context context; /* initialization of refreshing of fextra entry */ if (!fextra->mcxt) { if (OidIsValid(func->fn_oid)) { fextra->mcxt = AllocSetContextCreate(fextra_mcxt, "PLpgSQL fextra entry context", ALLOCSET_DEFAULT_SIZES); fextra->hashValue = GetSysCacheHashValue1(PROCOID, ObjectIdGetDatum(func->fn_oid)); } else fextra->mcxt = CurrentMemoryContext; } else MemoryContextReset(fextra->mcxt); if (OidIsValid(func->fn_oid)) { fn_name = get_func_name(func->fn_oid); if (!fn_name) fn_name = func->fn_signature; fn_namespacename = get_namespace_name_or_temp(get_func_namespace(func->fn_oid)); } oldcxt = MemoryContextSwitchTo(fextra->mcxt); fextra->fn_oid = func->fn_oid; fextra->fn_name = fn_name ? pstrdup(fn_name) : NULL; fextra->fn_namespacename = fn_namespacename ? pstrdup(fn_namespacename) : NULL; fextra->fn_signature = func->fn_signature ? pstrdup(func->fn_signature) : NULL; fextra->nstatements = func->nstatements; fextra->parentids = palloc0(sizeof(int) * (func->nstatements + 1)); fextra->naturalids = palloc0(sizeof(int) * (func->nstatements + 1)); fextra->levels = palloc0(sizeof(int) * (func->nstatements + 1)); MemoryContextSwitchTo(oldcxt); fextra->max_deep = 0; context.fextra = fextra; context.parentid = 0; context.current_deep = 0; context.naturalid = &naturalid; init_fextra_stmt_walker((PLpgSQL_stmt *) func->action, &context); fextra->func = func; fextra->is_valid = true; } } PG_CATCH(); { /* don't leave a half built entry pinned forever */ fextra->use_count--; PG_RE_THROW(); } PG_END_TRY(); if (OidIsValid(func->fn_oid)) pin_func(fextra->func); return fextra; } void plch_release_fextra(plch_fextra *fextra) { Assert(fextra->use_count > 0); /* until now, referenced PLpgSQL_function should be still valid */ Assert(fextra->hk.fn_oid == fextra->func->fn_oid); if (OidIsValid(fextra->fn_oid)) { /* * until now, referenced PLpgSQL_function should be still valid. * Attention - it is not true, for inline block. Pointer fextra->func * when func is inline block is already invalid. */ Assert(fextra->hk.fn_oid == fextra->func->fn_oid); unpin_func(fextra->func); } fextra->use_count--; } #if PG_VERSION_NUM < 150000 void plch_fextra_deinit() { if (fextra_mcxt) { MemoryContextDelete(fextra_mcxt); fextra_mcxt = NULL; fextra_ht = NULL; } } #endif okbob-plpgsql_check-800db79/src/format.c000066400000000000000000001063011526744060500202170ustar00rootroot00000000000000/*------------------------------------------------------------------------- * * format.c * * error/warning message formatting * * by Pavel Stehule 2013-2026 * *------------------------------------------------------------------------- */ #include #include "plpgsql_check.h" #include "access/htup_details.h" #include "catalog/pg_collation.h" #include "mb/pg_wchar.h" #include "utils/builtins.h" #include "utils/formatting.h" #include "utils/json.h" #include "utils/xml.h" #if PG_VERSION_NUM >= 190000 #include "utils/tuplestore.h" #endif static void put_text_line(plpgsql_check_result_info *ri, const char *message, int len); static const char *error_level_str(int level); static void init_tag(plpgsql_check_result_info *ri, Oid fn_oid); static void close_and_save(plpgsql_check_result_info *ri); static void put_error_text(plpgsql_check_result_info *ri, PLpgSQL_execstate *estate, int sqlerrcode, int lineno, const char *message, const char *detail, const char *hint, int level, int position, const char *query, const char *context); static void format_error_xml(StringInfo str, PLpgSQL_execstate *estate, int sqlerrcode, int lineno, const char *message, const char *detail, const char *hint, int level, int position, const char *query, const char *context); static void format_error_json(StringInfo str, PLpgSQL_execstate *estate, int sqlerrcode, int lineno, const char *message, const char *detail, const char *hint, int level, int position, const char *query, const char *context, bool is_first_item); static void put_error_tabular(plpgsql_check_result_info *ri, PLpgSQL_execstate *estate, Oid fn_oid, int sqlerrcode, int lineno, const char *message, const char *detail, const char *hint, int level, int position, const char *query, const char *context); /* * columns of plpgsql_check_function_table result * */ #define Natts_result 11 #define Anum_result_functionid 0 #define Anum_result_lineno 1 #define Anum_result_statement 2 #define Anum_result_sqlstate 3 #define Anum_result_message 4 #define Anum_result_detail 5 #define Anum_result_hint 6 #define Anum_result_level 7 #define Anum_result_position 8 #define Anum_result_query 9 #define Anum_result_context 10 /* * columns of plpgsql_show_dependency_tb result * */ #define Natts_dependency 5 #define Anum_dependency_type 0 #define Anum_dependency_oid 1 #define Anum_dependency_schema 2 #define Anum_dependency_name 3 #define Anum_dependency_params 4 /* * columns of plpgsql_profiler_function_tb result * */ #define Natts_profiler 11 #define Anum_profiler_lineno 0 #define Anum_profiler_stmt_lineno 1 #define Anum_profiler_queryid 2 #define Anum_profiler_cmds_on_row 3 #define Anum_profiler_exec_count 4 #define Anum_profiler_exec_count_err 5 #define Anum_profiler_total_time 6 #define Anum_profiler_avg_time 7 #define Anum_profiler_max_time 8 #define Anum_profiler_processed_rows 9 #define Anum_profiler_source 10 /* * columns of plpgsql_profiler_function_statements_tb result * */ #define Natts_profiler_statements 12 #define Anum_profiler_statements_stmtid 0 #define Anum_profiler_statements_parent_stmtid 1 #define Anum_profiler_statements_block_num 2 #define Anum_profiler_statements_lineno 3 #define Anum_profiler_statements_queryid 4 #define Anum_profiler_statements_exec_stmts 5 #define Anum_profiler_statements_exec_stmts_err 6 #define Anum_profiler_statements_total_time 7 #define Anum_profiler_statements_avg_time 8 #define Anum_profiler_statements_max_time 9 #define Anum_profiler_statements_processed_rows 10 #define Anum_profiler_statements_stmtname 11 /* * columns of plpgsql_profiler_functions_all_tb result * */ #define Natts_profiler_functions_all_tb 8 #define Anum_profiler_functions_all_funcoid 0 #define Anum_profiler_functions_all_exec_count 1 #define Anum_profiler_functions_all_exec_count_err 2 #define Anum_profiler_functions_all_total_time 3 #define Anum_profiler_functions_all_avg_time 4 #define Anum_profiler_functions_all_stddev_time 5 #define Anum_profiler_functions_all_min_time 6 #define Anum_profiler_functions_all_max_time 7 #define SET_RESULT_NULL(anum) \ do { \ values[(anum)] = (Datum) 0; \ nulls[(anum)] = true; \ } while (0) #define SET_RESULT(anum, value) \ do { \ values[(anum)] = (value); \ nulls[(anum)] = false; \ } while(0) #define SET_RESULT_NULLABLE_CSTR_TO_TEXT(anum, str) \ do { \ if (str != NULL) \ { \ SET_RESULT((anum), CStringGetTextDatum((str))); \ } \ else \ { \ SET_RESULT_NULL(anum); \ } \ } while (0) #define SET_RESULT_NULLABLE(anum, ptr) \ do { \ if (ptr != (Datum) 0) \ { \ SET_RESULT((anum), (ptr)); \ } \ else \ { \ SET_RESULT_NULL((anum)); \ } \ } while (0) #define SET_RESULT_INT32(anum, ival) SET_RESULT((anum), Int32GetDatum((ival))) #define SET_RESULT_INT64(anum, ival) SET_RESULT((anum), Int64GetDatum((ival))) #define SET_RESULT_QUERYID(anum, ival) SET_RESULT((anum), UInt64GetDatum((ival))) #define SET_RESULT_OID(anum, oid) SET_RESULT((anum), ObjectIdGetDatum((oid))) #define SET_RESULT_FLOAT8(anum, fval) SET_RESULT((anum), Float8GetDatum(fval)) /* * Translate name of format to number of format * */ int plpgsql_check_format_num(char *format_str) { int result = PLPGSQL_CHECK_FORMAT_INVALID; char *format_lower_str = str_tolower(format_str, strlen(format_str), DEFAULT_COLLATION_OID); if (strcmp(format_lower_str, "text") == 0) result = PLPGSQL_CHECK_FORMAT_TEXT; else if (strcmp(format_lower_str, "xml") == 0) result = PLPGSQL_CHECK_FORMAT_XML; else if (strcmp(format_lower_str, "json") == 0) result = PLPGSQL_CHECK_FORMAT_JSON; else ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("unrecognize format: \"%s\"", format_str), errhint("Only \"text\", \"xml\" and \"json\" formats are supported."))); return result; } /* * Prepare storage and formats for result * */ void plpgsql_check_init_ri(plpgsql_check_result_info *ri, int format, ReturnSetInfo *rsinfo) { int natts; MemoryContext per_query_ctx; MemoryContext oldctx; ri->format = format; ri->sinfo = NULL; switch (format) { case PLPGSQL_CHECK_FORMAT_TEXT: case PLPGSQL_CHECK_FORMAT_XML: case PLPGSQL_CHECK_FORMAT_JSON: natts = 1; break; case PLPGSQL_CHECK_FORMAT_TABULAR: natts = Natts_result; break; case PLPGSQL_SHOW_DEPENDENCY_FORMAT_TABULAR: natts = Natts_dependency; break; case PLPGSQL_SHOW_PROFILE_TABULAR: natts = Natts_profiler; break; case PLPGSQL_SHOW_PROFILE_STATEMENTS_TABULAR: natts = Natts_profiler_statements; break; case PLPGSQL_SHOW_PROFILE_FUNCTIONS_ALL_TABULAR: natts = Natts_profiler_functions_all_tb; break; default: elog(ERROR, "unknown format %d", format); } ri->init_tag = format == PLPGSQL_CHECK_FORMAT_XML || format == PLPGSQL_CHECK_FORMAT_JSON; /* need to build tuplestore in query context */ per_query_ctx = rsinfo->econtext->ecxt_per_query_memory; oldctx = MemoryContextSwitchTo(per_query_ctx); ri->tupdesc = CreateTupleDescCopy(rsinfo->expectedDesc); ri->tuple_store = tuplestore_begin_heap(false, false, work_mem); ri->query_ctx = per_query_ctx; MemoryContextSwitchTo(oldctx); /* simple check of target */ if (ri->tupdesc->natts != natts) elog(ERROR, "unexpected returning columns (%d instead %d)", ri->tupdesc->natts, natts); /* prepare rsinfo for this tuplestore result */ rsinfo->returnMode = SFRM_Materialize; rsinfo->setResult = ri->tuple_store; rsinfo->setDesc = ri->tupdesc; ri->is_first_item = true; } /* * When result is not empty, finalize result and close tuplestore. * */ void plpgsql_check_finalize_ri(plpgsql_check_result_info *ri) { if (ri->sinfo) { close_and_save(ri); pfree(ri->sinfo->data); pfree(ri->sinfo); ri->sinfo = NULL; } } /* * returns string used like errcontext for warnings. When target variable is defined, * then the string contains target variable name with the row of declaration. */ char * plpgsql_check_prepare_err_text_with_target_vardecl(PLpgSQL_checkstate *cstate, PLpgSQL_stmt *stmt, int varno) { StringInfoData str; int target_varno = -1; const char *direction = NULL; initStringInfo(&str); appendStringInfo(&str, "at %s", plpgsql_check__stmt_typename_p(stmt)); switch (stmt->cmd_type) { case PLPGSQL_STMT_ASSIGN: target_varno = ((PLpgSQL_stmt_assign *) stmt)->varno; direction = "to"; break; case PLPGSQL_STMT_RETURN: target_varno = ((PLpgSQL_stmt_return *) stmt)->retvarno; direction = "of"; break; case PLPGSQL_STMT_RETURN_NEXT: target_varno = ((PLpgSQL_stmt_return_next *) stmt)->retvarno; direction = "of"; break; case PLPGSQL_STMT_FETCH: { PLpgSQL_stmt_fetch *stmt_fetch = (PLpgSQL_stmt_fetch *) stmt; if (stmt_fetch->target) { target_varno = stmt_fetch->target->dno; direction = "to"; } break; } case PLPGSQL_STMT_EXECSQL: { PLpgSQL_stmt_execsql *stmt_execsql = (PLpgSQL_stmt_execsql *) stmt; if (stmt_execsql->target) { target_varno = stmt_execsql->target->dno; direction = "to"; } break; } case PLPGSQL_STMT_DYNEXECUTE: { PLpgSQL_stmt_dynexecute *stmt_dynexec = (PLpgSQL_stmt_dynexecute *) stmt; if (stmt_dynexec->target) { target_varno = stmt_dynexec->target->dno; direction = "to"; } break; } default: varno = -1; } /* * SELECT INTO makes internal row variable for target. Showing this * variable as target is useless. We have to wait, when we know real * target variable (passed as fieldno). */ if (varno != -1) target_varno = varno; if (target_varno != -1) { PLpgSQL_datum *d = (PLpgSQL_datum *) cstate->estate->datums[target_varno]; if (d->dtype == PLPGSQL_DTYPE_VAR || d->dtype == PLPGSQL_DTYPE_ROW || d->dtype == PLPGSQL_DTYPE_REC) { PLpgSQL_variable *var = (PLpgSQL_variable *) d; if (var->lineno != -1) { if (!plpgsql_check_is_internal_variable(cstate, var)) { appendStringInfo(&str, " %s variable \"%s\" declared on line %d", direction, var->refname, var->lineno); } else if (d->dtype == PLPGSQL_DTYPE_ROW) { /* try to enumerate fields */ PLpgSQL_row *row = (PLpgSQL_row *) var; int fnum; bool is_first = true; for (fnum = 0; fnum < row->nfields; fnum++) { PLpgSQL_datum *d2; /* skip dropped columns */ if (row->varnos[fnum] < 0) continue; if (is_first) { appendStringInfo(&str, " %s ", direction); is_first = false; } else appendStringInfoString(&str, ", "); d2 = (PLpgSQL_datum *) cstate->estate->datums[row->varnos[fnum]]; if (d2->dtype == PLPGSQL_DTYPE_VAR || d2->dtype == PLPGSQL_DTYPE_ROW || d2->dtype == PLPGSQL_DTYPE_REC) { appendStringInfo(&str, "%s", ((PLpgSQL_variable *) d2)->refname); } else if (d2->dtype == PLPGSQL_DTYPE_RECFIELD) { PLpgSQL_recfield *recfield = (PLpgSQL_recfield *) d2; PLpgSQL_variable *var2 = (PLpgSQL_variable *) cstate->estate->datums[recfield->recparentno]; appendStringInfo(&str, "%s.%s", var2->refname, recfield->fieldname); } } if (!is_first) appendStringInfoString(&str, " variables"); } } } else if (d->dtype == PLPGSQL_DTYPE_RECFIELD) { PLpgSQL_recfield *recfield = (PLpgSQL_recfield *) d; PLpgSQL_variable *var = (PLpgSQL_variable *) cstate->estate->datums[recfield->recparentno]; if (var->lineno != -1) { appendStringInfo(&str, " %s field \"%s\" of variable \"%s\" declared on line %d", direction, recfield->fieldname, var->refname, var->lineno); } } } return str.data; } /* * error message processing router */ static void plpgsql_check_put_error_internal(PLpgSQL_checkstate *cstate, int sqlerrcode, int lineno, const char *message, const char *detail, const char *hint, int level, int position, const char *query, const char *context) { plpgsql_check_result_info *ri = cstate->result_info; PLpgSQL_execstate *estate = cstate->estate; if (context == NULL && estate && estate->err_text) context = estate->err_text; /* ignore warnings when is not requested */ if ((level == PLPGSQL_CHECK_WARNING_PERFORMANCE && !cstate->cinfo->performance_warnings) || (level == PLPGSQL_CHECK_WARNING_OTHERS && !cstate->cinfo->other_warnings) || (level == PLPGSQL_CHECK_WARNING_EXTRA && !cstate->cinfo->extra_warnings) || (level == PLPGSQL_CHECK_WARNING_SECURITY && !cstate->cinfo->security_warnings) || (level == PLPGSQL_CHECK_WARNING_COMPATIBILITY && !cstate->cinfo->compatibility_warnings)) return; if ((level == PLPGSQL_CHECK_WARNING_PERFORMANCE && cstate->pragma_vector.disable_performance_warnings) || (level == PLPGSQL_CHECK_WARNING_OTHERS && cstate->pragma_vector.disable_other_warnings) || (level == PLPGSQL_CHECK_WARNING_EXTRA && cstate->pragma_vector.disable_extra_warnings) || (level == PLPGSQL_CHECK_WARNING_SECURITY && cstate->pragma_vector.disable_security_warnings) || (level == PLPGSQL_CHECK_WARNING_COMPATIBILITY && cstate->pragma_vector.disable_compatibility_warnings)) return; if (cstate->pragma_vector.disable_check) return; if (ri->init_tag) { init_tag(ri, cstate->cinfo->fn_oid); ri->init_tag = false; } if (ri->tuple_store) { switch (ri->format) { case PLPGSQL_CHECK_FORMAT_TABULAR: put_error_tabular(ri, estate, cstate->cinfo->fn_oid, sqlerrcode, lineno, message, detail, hint, level, position, query, context); break; case PLPGSQL_CHECK_FORMAT_TEXT: put_error_text(ri, estate, sqlerrcode, lineno, message, detail, hint, level, position, query, context); break; case PLPGSQL_CHECK_FORMAT_XML: format_error_xml(ri->sinfo, estate, sqlerrcode, lineno, message, detail, hint, level, position, query, context); break; case PLPGSQL_CHECK_FORMAT_JSON: format_error_json(ri->sinfo, estate, sqlerrcode, lineno, message, detail, hint, level, position, query, context, ri->is_first_item); break; } /* stop checking if it is necessary */ if (level == PLPGSQL_CHECK_ERROR && cstate->cinfo->fatal_errors) cstate->stop_check = true; } else { int elevel; /* * when a passive mode is active and fatal_errors is false, then raise * warning everytime. */ if (!cstate->is_active_mode && !cstate->cinfo->fatal_errors) elevel = WARNING; else elevel = level == PLPGSQL_CHECK_ERROR ? ERROR : WARNING; /* use error fields as parameters of postgres exception */ ereport(elevel, (sqlerrcode ? errcode(sqlerrcode) : 0, errmsg_internal("%s", message), (detail != NULL) ? errdetail_internal("%s", detail) : 0, (hint != NULL) ? errhint("%s", hint) : 0, (query != NULL) ? internalerrquery(query) : 0, (position != 0) ? internalerrposition(position) : 0, (context != NULL) ? errcontext("%s", context) : 0)); } ri->is_first_item = false; } /* * store edata */ void plpgsql_check_put_error_edata(PLpgSQL_checkstate *cstate, ErrorData *edata) { plpgsql_check_put_error_internal(cstate, edata->sqlerrcode, edata->lineno, edata->message, edata->detail, edata->hint, PLPGSQL_CHECK_ERROR, edata->internalpos, edata->internalquery, edata->context); } void plpgsql_check_put_error(PLpgSQL_checkstate *cstate, int sqlerrcode, int lineno, const char *message, const char *detail, const char *hint, int level, int position, const char *query, const char *context) { /* * Trapped internal errors has transformed position. The plpgsql_check * errors (and warnings) have to have same transformation for position to * correct display caret (for trapped and reraised, and raised errors) */ if (position < 0) position = 0; else if (query) position = pg_mbstrlen_with_len(query, position) + 1; plpgsql_check_put_error_internal(cstate, sqlerrcode, lineno, message, detail, hint, level, position, query, context); } /* * Append text line (StringInfo) to one column tuple store * */ static void put_text_line(plpgsql_check_result_info *ri, const char *message, int len) { Datum value; bool isnull = false; HeapTuple tuple; if (len >= 0) value = PointerGetDatum(cstring_to_text_with_len(message, len)); else value = PointerGetDatum(cstring_to_text(message)); tuple = heap_form_tuple(ri->tupdesc, &value, &isnull); tuplestore_puttuple(ri->tuple_store, tuple); } /* * Public wrapper of put_text_line. Used by the table pragmas generator * for returning generated pragma lines. */ void plch_put_text_line(plpgsql_check_result_info *ri, const char *message, int len) { put_text_line(ri, message, len); } static const char * error_level_str(int level) { switch (level) { case PLPGSQL_CHECK_ERROR: return "error"; case PLPGSQL_CHECK_WARNING_OTHERS: return "warning"; case PLPGSQL_CHECK_WARNING_EXTRA: return "warning extra"; case PLPGSQL_CHECK_WARNING_PERFORMANCE: return "performance"; case PLPGSQL_CHECK_WARNING_SECURITY: return "security"; case PLPGSQL_CHECK_WARNING_COMPATIBILITY: return "compatibility"; default: return "???"; } } /* * collects errors and warnings in plain text format */ static void put_error_text(plpgsql_check_result_info *ri, PLpgSQL_execstate *estate, int sqlerrcode, int lineno, const char *message, const char *detail, const char *hint, int level, int position, const char *query, const char *context) { StringInfoData sinfo; const char *level_str = error_level_str(level); Assert(message != NULL); initStringInfo(&sinfo); /* lineno should be valid for actual statements */ if (estate != NULL && estate->err_stmt != NULL && estate->err_stmt->lineno > 0) appendStringInfo(&sinfo, "%s:%s:%d:%s:%s", level_str, unpack_sql_state(sqlerrcode), estate->err_stmt->lineno, plpgsql_check__stmt_typename_p(estate->err_stmt), message); else if (strncmp(message, UNUSED_VARIABLE_TEXT, UNUSED_VARIABLE_TEXT_CHECK_LENGTH) == 0) { appendStringInfo(&sinfo, "%s:%s:%d:%s:%s", level_str, unpack_sql_state(sqlerrcode), lineno, "DECLARE", message); } else if (strncmp(message, NEVER_READ_VARIABLE_TEXT, NEVER_READ_VARIABLE_TEXT_CHECK_LENGTH) == 0) { appendStringInfo(&sinfo, "%s:%s:%d:%s:%s", level_str, unpack_sql_state(sqlerrcode), lineno, "DECLARE", message); } else { appendStringInfo(&sinfo, "%s:%s:%s", level_str, unpack_sql_state(sqlerrcode), message); } put_text_line(ri, sinfo.data, sinfo.len); resetStringInfo(&sinfo); if (query != NULL) { char *query_line; /* pointer to beginning of current line */ int line_caret_pos; bool is_first_line = true; char *_query = pstrdup(query); char *ptr; ptr = _query; query_line = ptr; line_caret_pos = position; while (*ptr != '\0') { /* search end of lines and replace '\n' by '\0' */ if (*ptr == '\n') { *ptr = '\0'; if (is_first_line) { appendStringInfo(&sinfo, "Query: %s", query_line); is_first_line = false; } else appendStringInfo(&sinfo, " %s", query_line); put_text_line(ri, sinfo.data, sinfo.len); resetStringInfo(&sinfo); if (line_caret_pos > 0 && position == 0) { appendStringInfo(&sinfo, "-- %*s", line_caret_pos, "^"); put_text_line(ri, sinfo.data, sinfo.len); resetStringInfo(&sinfo); line_caret_pos = 0; } /* store caret position offset for next line */ if (position > 1) line_caret_pos = position - 1; /* go to next line */ query_line = ptr + 1; } ptr += pg_mblen_cstr(ptr); if (position > 0) position--; } /* flush last line */ if (query_line != NULL) { if (is_first_line) appendStringInfo(&sinfo, "Query: %s", query_line); else appendStringInfo(&sinfo, " %s", query_line); put_text_line(ri, sinfo.data, sinfo.len); resetStringInfo(&sinfo); if (line_caret_pos > 0 && position == 0) { appendStringInfo(&sinfo, "-- %*s", line_caret_pos, "^"); put_text_line(ri, sinfo.data, sinfo.len); resetStringInfo(&sinfo); } } pfree(_query); } if (detail != NULL) { appendStringInfo(&sinfo, "Detail: %s", detail); put_text_line(ri, sinfo.data, sinfo.len); resetStringInfo(&sinfo); } if (hint != NULL) { appendStringInfo(&sinfo, "Hint: %s", hint); put_text_line(ri, sinfo.data, sinfo.len); resetStringInfo(&sinfo); } if (context != NULL) { appendStringInfo(&sinfo, "Context: %s", context); put_text_line(ri, sinfo.data, sinfo.len); resetStringInfo(&sinfo); } pfree(sinfo.data); } /* * Initialize StringInfo buffer with top tag * */ static void init_tag(plpgsql_check_result_info *ri, Oid fn_oid) { /* XML format requires StringInfo buffer */ if (ri->format == PLPGSQL_CHECK_FORMAT_XML || ri->format == PLPGSQL_CHECK_FORMAT_JSON) { if (ri->sinfo != NULL) resetStringInfo(ri->sinfo); else { MemoryContext oldcxt; /* StringInfo should be created in some longer life context */ oldcxt = MemoryContextSwitchTo(ri->query_ctx); ri->sinfo = makeStringInfo(); MemoryContextSwitchTo(oldcxt); } if (ri->format == PLPGSQL_CHECK_FORMAT_XML) { /* create a initial tag */ if (plpgsql_check_regress_test_mode) appendStringInfo(ri->sinfo, "\n"); else appendStringInfo(ri->sinfo, "\n", fn_oid); } else if (ri->format == PLPGSQL_CHECK_FORMAT_JSON) { /* create a initial tag */ if (plpgsql_check_regress_test_mode) appendStringInfo(ri->sinfo, "{\n \"issues\": [\n"); else appendStringInfo(ri->sinfo, "{\n \"function\": \"%d\",\n \"issues\": [\n", fn_oid); } } } /* * Append close tag and store document * */ static void close_and_save(plpgsql_check_result_info *ri) { if (ri->format == PLPGSQL_CHECK_FORMAT_XML) { appendStringInfoString(ri->sinfo, ""); put_text_line(ri, ri->sinfo->data, ri->sinfo->len); } else if (ri->format == PLPGSQL_CHECK_FORMAT_JSON) { appendStringInfoString(ri->sinfo, "\n ]\n}"); put_text_line(ri, ri->sinfo->data, ri->sinfo->len); } } /* * format_error_xml formats and collects a identifided issues */ static void format_error_xml(StringInfo str, PLpgSQL_execstate *estate, int sqlerrcode, int lineno, const char *message, const char *detail, const char *hint, int level, int position, const char *query, const char *context) { const char *level_str = error_level_str(level); Assert(message != NULL); /* flush tag */ appendStringInfoString(str, " \n"); appendStringInfo(str, " %s\n", level_str); appendStringInfo(str, " %s\n", unpack_sql_state(sqlerrcode)); appendStringInfo(str, " %s\n", escape_xml(message)); if (estate != NULL && estate->err_stmt != NULL && estate->err_stmt->lineno > 0) appendStringInfo(str, " %s\n", estate->err_stmt->lineno, plpgsql_check__stmt_typename_p(estate->err_stmt)); else if (strncmp(message, UNUSED_VARIABLE_TEXT, UNUSED_VARIABLE_TEXT_CHECK_LENGTH) == 0 || strncmp(message, NEVER_READ_VARIABLE_TEXT, NEVER_READ_VARIABLE_TEXT_CHECK_LENGTH) == 0) appendStringInfo(str, " DECLARE\n", lineno); if (hint != NULL) appendStringInfo(str, " %s\n", escape_xml(hint)); if (detail != NULL) appendStringInfo(str, " %s\n", escape_xml(detail)); if (query != NULL) appendStringInfo(str, " %s\n", position, escape_xml(query)); if (context != NULL) appendStringInfo(str, " %s\n", escape_xml(context)); /* flush closing tag */ appendStringInfoString(str, " \n"); } /* * format_error_json formats and collects a identifided issues */ static void format_error_json(StringInfo str, PLpgSQL_execstate *estate, int sqlerrcode, int lineno, const char *message, const char *detail, const char *hint, int level, int position, const char *query, const char *context, bool is_first_item) { const char *level_str = error_level_str(level); StringInfoData sinfo; /* Holds escaped json */ Assert(message != NULL); initStringInfo(&sinfo); /* flush tag */ if (is_first_item) appendStringInfoString(str, " {\n"); else appendStringInfoString(str, ",\n {\n"); appendStringInfo(str, " \"level\": \"%s\",\n", level_str); escape_json(&sinfo, message); appendStringInfo(str, " \"message\":%s,\n", sinfo.data); if (estate != NULL && estate->err_stmt != NULL && estate->err_stmt->lineno > 0) appendStringInfo(str, " \"statement\": {\n \"lineNumber\": \"%d\",\n \"text\": \"%s\"\n },\n", estate->err_stmt->lineno, plpgsql_check__stmt_typename_p(estate->err_stmt)); else if (strncmp(message, UNUSED_VARIABLE_TEXT, UNUSED_VARIABLE_TEXT_CHECK_LENGTH) == 0 || strncmp(message, NEVER_READ_VARIABLE_TEXT, NEVER_READ_VARIABLE_TEXT_CHECK_LENGTH) == 0) appendStringInfo(str, " \"statement\": {\n \"lineNumber\": \"%d\",\n \"text\": \"DECLARE\"\n },\n", lineno); if (hint != NULL) { resetStringInfo(&sinfo); escape_json(&sinfo, hint); appendStringInfo(str, " \"hint\": %s,\n", sinfo.data); } if (detail != NULL) { resetStringInfo(&sinfo); escape_json(&sinfo, detail); appendStringInfo(str, " \"detail\": %s,\n", sinfo.data); } if (query != NULL) { resetStringInfo(&sinfo); escape_json(&sinfo, query); appendStringInfo(str, " \"query\": {\n \"position\": \"%d\",\n \"text\": %s\n },\n", position, sinfo.data); } if (context != NULL) { resetStringInfo(&sinfo); escape_json(&sinfo, context); appendStringInfo(str, " \"context\": %s,\n", sinfo.data); } /* placing this property last as to avoid a trailing comma */ appendStringInfo(str, " \"sqlState\": \"%s\"\n", unpack_sql_state(sqlerrcode)); /* * flush closing tag. Needs comman jus in case there is more than one * issue. Comma removed in epilog */ appendStringInfoString(str, " }"); } /* * store error fields to result tuplestore * */ static void put_error_tabular(plpgsql_check_result_info *ri, PLpgSQL_execstate *estate, Oid fn_oid, int sqlerrcode, int lineno, const char *message, const char *detail, const char *hint, int level, int position, const char *query, const char *context) { Datum values[Natts_result]; bool nulls[Natts_result]; Assert(ri->tuple_store); Assert(ri->tupdesc); Assert(message != NULL); SET_RESULT_OID(Anum_result_functionid, fn_oid); /* lineno should be valid */ if (estate != NULL && estate->err_stmt != NULL && estate->err_stmt->lineno > 0) { /* use lineno based on err_stmt */ SET_RESULT_INT32(Anum_result_lineno, estate->err_stmt->lineno); SET_RESULT_NULLABLE_CSTR_TO_TEXT(Anum_result_statement, plpgsql_check__stmt_typename_p(estate->err_stmt)); } else if (strncmp(message, UNUSED_VARIABLE_TEXT, UNUSED_VARIABLE_TEXT_CHECK_LENGTH) == 0) { SET_RESULT_INT32(Anum_result_lineno, lineno); SET_RESULT(Anum_result_statement, CStringGetTextDatum("DECLARE")); } else if (strncmp(message, NEVER_READ_VARIABLE_TEXT, NEVER_READ_VARIABLE_TEXT_CHECK_LENGTH) == 0) { SET_RESULT_INT32(Anum_result_lineno, lineno); SET_RESULT(Anum_result_statement, CStringGetTextDatum("DECLARE")); } else { SET_RESULT_NULL(Anum_result_lineno); SET_RESULT_NULL(Anum_result_statement); } SET_RESULT_NULLABLE_CSTR_TO_TEXT(Anum_result_sqlstate, unpack_sql_state(sqlerrcode)); SET_RESULT_NULLABLE_CSTR_TO_TEXT(Anum_result_message, message); SET_RESULT_NULLABLE_CSTR_TO_TEXT(Anum_result_detail, detail); SET_RESULT_NULLABLE_CSTR_TO_TEXT(Anum_result_hint, hint); SET_RESULT_NULLABLE_CSTR_TO_TEXT(Anum_result_level, error_level_str(level)); if (position != 0) SET_RESULT_INT32(Anum_result_position, position); else SET_RESULT_NULL(Anum_result_position); SET_RESULT_NULLABLE_CSTR_TO_TEXT(Anum_result_query, query); SET_RESULT_NULLABLE_CSTR_TO_TEXT(Anum_result_context, context); tuplestore_putvalues(ri->tuple_store, ri->tupdesc, values, nulls); } /* * Store one output row of dependency view to result tuplestore * */ void plpgsql_check_put_dependency(plpgsql_check_result_info *ri, char *type, Oid oid, char *schema, char *name, char *params) { Datum values[Natts_dependency]; bool nulls[Natts_dependency]; Assert(ri->tuple_store); Assert(ri->tupdesc); SET_RESULT_NULLABLE_CSTR_TO_TEXT(Anum_dependency_type, type); SET_RESULT_OID(Anum_dependency_oid, oid); SET_RESULT_NULLABLE_CSTR_TO_TEXT(Anum_dependency_schema, schema); SET_RESULT_NULLABLE_CSTR_TO_TEXT(Anum_dependency_name, name); SET_RESULT_NULLABLE_CSTR_TO_TEXT(Anum_dependency_params, params); tuplestore_putvalues(ri->tuple_store, ri->tupdesc, values, nulls); } /* * Store one output row of profiler to result tuplestore * */ void plpgsql_check_put_profile(plpgsql_check_result_info *ri, Datum queryids_array, int lineno, int stmt_lineno, int cmds_on_row, Datum exec_count_array, Datum exec_count_err_array, Datum total_time_array, Datum avg_time_array, Datum max_time_array, Datum processed_rows_array, char *source_row) { Datum values[Natts_profiler]; bool nulls[Natts_profiler]; Assert(ri->tuple_store); Assert(ri->tupdesc); if (stmt_lineno > 0) { SET_RESULT_INT32(Anum_profiler_stmt_lineno, stmt_lineno); SET_RESULT_INT32(Anum_profiler_cmds_on_row, cmds_on_row); } else { SET_RESULT_NULL(Anum_profiler_stmt_lineno); SET_RESULT_NULL(Anum_profiler_cmds_on_row); } SET_RESULT_INT32(Anum_profiler_lineno, lineno); SET_RESULT_NULLABLE(Anum_profiler_queryid, queryids_array); SET_RESULT_NULLABLE(Anum_profiler_exec_count, exec_count_array); SET_RESULT_NULLABLE(Anum_profiler_exec_count_err, exec_count_err_array); SET_RESULT_NULLABLE(Anum_profiler_total_time, total_time_array); SET_RESULT_NULLABLE(Anum_profiler_avg_time, avg_time_array); SET_RESULT_NULLABLE(Anum_profiler_max_time, max_time_array); SET_RESULT_NULLABLE(Anum_profiler_processed_rows, processed_rows_array); SET_RESULT_NULLABLE_CSTR_TO_TEXT(Anum_profiler_source, source_row); tuplestore_putvalues(ri->tuple_store, ri->tupdesc, values, nulls); } /* * Store one output row of profiler to result tuplestore in statement * oriented format * */ void plpgsql_check_put_profile_statement(plpgsql_check_result_info *ri, pc_queryid queryid, int stmtid, int parent_stmtid, int block_num, int lineno, int64 exec_stmts, int64 exec_stmts_err, double total_time, double max_time, int64 processed_rows, const char *stmtname) { Datum values[Natts_profiler_statements]; bool nulls[Natts_profiler_statements]; Assert(ri->tuple_store); Assert(ri->tupdesc); /* ignore invisible statements */ if (lineno <= 0) return; SET_RESULT_INT32(Anum_profiler_statements_stmtid, stmtid); SET_RESULT_INT32(Anum_profiler_statements_block_num, block_num); SET_RESULT_INT32(Anum_profiler_statements_lineno, lineno); if (queryid == NOQUERYID) SET_RESULT_NULL(Anum_profiler_statements_queryid); else SET_RESULT_QUERYID(Anum_profiler_statements_queryid, queryid); SET_RESULT_INT64(Anum_profiler_statements_exec_stmts, exec_stmts); SET_RESULT_INT64(Anum_profiler_statements_exec_stmts_err, exec_stmts_err); SET_RESULT_INT64(Anum_profiler_statements_processed_rows, processed_rows); SET_RESULT_FLOAT8(Anum_profiler_statements_total_time, total_time / 1000.0); SET_RESULT_FLOAT8(Anum_profiler_statements_total_time, total_time / 1000.0); SET_RESULT_FLOAT8(Anum_profiler_statements_max_time, max_time / 1000.0); SET_RESULT_NULLABLE_CSTR_TO_TEXT(Anum_profiler_statements_stmtname, stmtname); /* set nullable field */ if (parent_stmtid == -1) SET_RESULT_NULL(Anum_profiler_statements_parent_stmtid); else SET_RESULT_INT32(Anum_profiler_statements_parent_stmtid, parent_stmtid); if ((exec_stmts) > 0) SET_RESULT_FLOAT8(Anum_profiler_statements_avg_time, ceil(((float8) total_time) / (exec_stmts)) / 1000.0); else SET_RESULT_NULL(Anum_profiler_statements_avg_time); tuplestore_putvalues(ri->tuple_store, ri->tupdesc, values, nulls); } /* * Store one output row of profiler to result tuplestore in statement * oriented format - this variant is used when function was not executed * */ void plpgsql_check_put_profile_statement_no_stats(plpgsql_check_result_info *ri, int stmtid, int parent_stmtid, int block_num, int lineno, const char *stmtname) { Datum values[Natts_profiler_statements]; bool nulls[Natts_profiler_statements]; Assert(ri->tuple_store); Assert(ri->tupdesc); /* ignore invisible statements */ if (lineno <= 0) return; SET_RESULT_INT32(Anum_profiler_statements_stmtid, stmtid); SET_RESULT_INT32(Anum_profiler_statements_block_num, block_num); SET_RESULT_INT32(Anum_profiler_statements_lineno, lineno); SET_RESULT_NULL(Anum_profiler_statements_queryid); SET_RESULT_NULL(Anum_profiler_statements_exec_stmts); SET_RESULT_NULL(Anum_profiler_statements_exec_stmts_err); SET_RESULT_NULL(Anum_profiler_statements_processed_rows); SET_RESULT_NULL(Anum_profiler_statements_total_time); SET_RESULT_NULL(Anum_profiler_statements_total_time); SET_RESULT_NULL(Anum_profiler_statements_max_time); SET_RESULT_NULLABLE_CSTR_TO_TEXT(Anum_profiler_statements_stmtname, stmtname); /* set nullable field */ if (parent_stmtid == -1) SET_RESULT_NULL(Anum_profiler_statements_parent_stmtid); else SET_RESULT_INT32(Anum_profiler_statements_parent_stmtid, parent_stmtid); SET_RESULT_NULL(Anum_profiler_statements_avg_time); tuplestore_putvalues(ri->tuple_store, ri->tupdesc, values, nulls); } void plpgsql_check_put_profiler_functions_all_tb(plpgsql_check_result_info *ri, Oid funcoid, int64 exec_count, int64 exec_count_err, double total_time, double avg_time, double stddev_time, double min_time, double max_time) { Datum values[Natts_profiler_functions_all_tb]; bool nulls[Natts_profiler_functions_all_tb]; Assert(ri->tuple_store); Assert(ri->tupdesc); SET_RESULT_OID(Anum_profiler_functions_all_funcoid, funcoid); SET_RESULT_INT64(Anum_profiler_functions_all_exec_count, exec_count); SET_RESULT_INT64(Anum_profiler_functions_all_exec_count_err, exec_count_err); SET_RESULT_FLOAT8(Anum_profiler_functions_all_total_time, total_time / 1000.0); SET_RESULT_FLOAT8(Anum_profiler_functions_all_avg_time, avg_time / 1000.0); if (isnan(stddev_time)) SET_RESULT_NULL(Anum_profiler_functions_all_stddev_time); else SET_RESULT_FLOAT8(Anum_profiler_functions_all_stddev_time, stddev_time / 1000.0); SET_RESULT_FLOAT8(Anum_profiler_functions_all_min_time, min_time / 1000.0); SET_RESULT_FLOAT8(Anum_profiler_functions_all_max_time, max_time / 1000.0); tuplestore_putvalues(ri->tuple_store, ri->tupdesc, values, nulls); } okbob-plpgsql_check-800db79/src/generic_stmt_walker.c000066400000000000000000000271321526744060500227630ustar00rootroot00000000000000/*------------------------------------------------------------------------- * * generic_stmt_walker.c * * allows to execute defined routines over PL/pgSQL AST * * by Pavel Stehule 2013-2026 * *------------------------------------------------------------------------- * * This code is based on iterator over PL/pgSQL statement tree from * pl_funcs.c, but modified for plpgsql_check purposes. */ #include "postgres.h" #include "plpgsql.h" #include "plpgsql_check.h" #ifndef foreach_ptr /* * cut from pg_list.h * * remove when foreach_ptr will be supported on all versions * */ #define foreach_ptr(type, var, lst) foreach_internal(type, *, var, lst, lfirst) #define foreach_int(var, lst) foreach_internal(int, , var, lst, lfirst_int) #define foreach_oid(var, lst) foreach_internal(Oid, , var, lst, lfirst_oid) #define foreach_xid(var, lst) foreach_internal(TransactionId, , var, lst, lfirst_xid) /* * The internal implementation of the above macros. Do not use directly. * * This macro actually generates two loops in order to declare two variables of * different types. The outer loop only iterates once, so we expect optimizing * compilers will unroll it, thereby optimizing it away. */ #define foreach_internal(type, pointer, var, lst, func) \ for (type pointer var = 0, pointer var##__outerloop = (type pointer) 1; \ var##__outerloop; \ var##__outerloop = 0) \ for (ForEachState var##__state = {(lst), 0}; \ (var##__state.l != NIL && \ var##__state.i < var##__state.l->length && \ (var = (type pointer) func(&var##__state.l->elements[var##__state.i]), true)); \ var##__state.i++) #endif void plch_statement_tree_walker_impl(PLpgSQL_stmt *stmt, plch_stmt_walker_callback stmt_callback, plch_expr_walker_callback expr_callback, void *context) { #define S_WALK(st) do { if (stmt_callback) stmt_callback(st, context); } while(0) #define E_WALK(st, ex) do { if (expr_callback) expr_callback(st, ex, context); } while(0) #define S_LIST_WALK(lst) foreach_ptr(PLpgSQL_stmt, st, lst) S_WALK(st) #define E_LIST_WALK(lst) foreach_ptr(PLpgSQL_expr, ex, lst) E_WALK(stmt, ex) switch (stmt->cmd_type) { case PLPGSQL_STMT_BLOCK: { PLpgSQL_stmt_block *bstmt = (PLpgSQL_stmt_block *) stmt; S_LIST_WALK(bstmt->body); if (bstmt->exceptions) { foreach_ptr(PLpgSQL_exception, exc, bstmt->exceptions->exc_list) { /* conditions list has no interesting sub-structure */ S_LIST_WALK(exc->action); } } break; } case PLPGSQL_STMT_ASSIGN: { PLpgSQL_stmt_assign *astmt = (PLpgSQL_stmt_assign *) stmt; E_WALK(stmt, astmt->expr); break; } case PLPGSQL_STMT_IF: { PLpgSQL_stmt_if *ifstmt = (PLpgSQL_stmt_if *) stmt; E_WALK(stmt, ifstmt->cond); S_LIST_WALK(ifstmt->then_body); foreach_ptr(PLpgSQL_if_elsif, elif, ifstmt->elsif_list) { E_WALK(stmt, elif->cond); S_LIST_WALK(elif->stmts); } S_LIST_WALK(ifstmt->else_body); break; } case PLPGSQL_STMT_CASE: { PLpgSQL_stmt_case *cstmt = (PLpgSQL_stmt_case *) stmt; E_WALK(stmt, cstmt->t_expr); foreach_ptr(PLpgSQL_case_when, cwt, cstmt->case_when_list) { E_WALK(stmt, cwt->expr); S_LIST_WALK(cwt->stmts); } S_LIST_WALK(cstmt->else_stmts); break; } case PLPGSQL_STMT_LOOP: { PLpgSQL_stmt_loop *lstmt = (PLpgSQL_stmt_loop *) stmt; S_LIST_WALK(lstmt->body); break; } case PLPGSQL_STMT_WHILE: { PLpgSQL_stmt_while *wstmt = (PLpgSQL_stmt_while *) stmt; E_WALK(stmt, wstmt->cond); S_LIST_WALK(wstmt->body); break; } case PLPGSQL_STMT_FORI: { PLpgSQL_stmt_fori *fori = (PLpgSQL_stmt_fori *) stmt; E_WALK(stmt, fori->lower); E_WALK(stmt, fori->upper); E_WALK(stmt, fori->step); S_LIST_WALK(fori->body); break; } case PLPGSQL_STMT_FORS: { PLpgSQL_stmt_fors *fors = (PLpgSQL_stmt_fors *) stmt; S_LIST_WALK(fors->body); E_WALK(stmt, fors->query); break; } case PLPGSQL_STMT_FORC: { PLpgSQL_stmt_forc *forc = (PLpgSQL_stmt_forc *) stmt; S_LIST_WALK(forc->body); E_WALK(stmt, forc->argquery); break; } case PLPGSQL_STMT_FOREACH_A: { PLpgSQL_stmt_foreach_a *fstmt = (PLpgSQL_stmt_foreach_a *) stmt; E_WALK(stmt, fstmt->expr); S_LIST_WALK(fstmt->body); break; } case PLPGSQL_STMT_EXIT: { PLpgSQL_stmt_exit *estmt = (PLpgSQL_stmt_exit *) stmt; E_WALK(stmt, estmt->cond); break; } case PLPGSQL_STMT_RETURN: { PLpgSQL_stmt_return *rstmt = (PLpgSQL_stmt_return *) stmt; E_WALK(stmt, rstmt->expr); break; } case PLPGSQL_STMT_RETURN_NEXT: { PLpgSQL_stmt_return_next *rstmt = (PLpgSQL_stmt_return_next *) stmt; E_WALK(stmt, rstmt->expr); break; } case PLPGSQL_STMT_RETURN_QUERY: { PLpgSQL_stmt_return_query *rstmt = (PLpgSQL_stmt_return_query *) stmt; E_WALK(stmt, rstmt->query); E_WALK(stmt, rstmt->dynquery); E_LIST_WALK(rstmt->params); break; } case PLPGSQL_STMT_RAISE: { PLpgSQL_stmt_raise *rstmt = (PLpgSQL_stmt_raise *) stmt; E_LIST_WALK(rstmt->params); foreach_ptr(PLpgSQL_raise_option, opt, rstmt->options) { E_WALK(stmt, opt->expr); } break; } case PLPGSQL_STMT_ASSERT: { PLpgSQL_stmt_assert *astmt = (PLpgSQL_stmt_assert *) stmt; E_WALK(stmt, astmt->cond); E_WALK(stmt, astmt->message); break; } case PLPGSQL_STMT_EXECSQL: { PLpgSQL_stmt_execsql *xstmt = (PLpgSQL_stmt_execsql *) stmt; E_WALK(stmt, xstmt->sqlstmt); break; } case PLPGSQL_STMT_DYNEXECUTE: { PLpgSQL_stmt_dynexecute *dstmt = (PLpgSQL_stmt_dynexecute *) stmt; E_WALK(stmt, dstmt->query); E_LIST_WALK(dstmt->params); break; } case PLPGSQL_STMT_DYNFORS: { PLpgSQL_stmt_dynfors *dstmt = (PLpgSQL_stmt_dynfors *) stmt; S_LIST_WALK(dstmt->body); E_WALK(stmt, dstmt->query); E_LIST_WALK(dstmt->params); break; } case PLPGSQL_STMT_GETDIAG: { /* no interesting sub-structure */ break; } case PLPGSQL_STMT_OPEN: { PLpgSQL_stmt_open *ostmt = (PLpgSQL_stmt_open *) stmt; E_WALK(stmt, ostmt->argquery); E_WALK(stmt, ostmt->query); E_WALK(stmt, ostmt->dynquery); E_LIST_WALK(ostmt->params); break; } case PLPGSQL_STMT_FETCH: { PLpgSQL_stmt_fetch *fstmt = (PLpgSQL_stmt_fetch *) stmt; E_WALK(stmt, fstmt->expr); break; } case PLPGSQL_STMT_CLOSE: { /* no interesting sub-structure */ break; } case PLPGSQL_STMT_PERFORM: { PLpgSQL_stmt_perform *pstmt = (PLpgSQL_stmt_perform *) stmt; E_WALK(stmt, pstmt->expr); break; } case PLPGSQL_STMT_CALL: { PLpgSQL_stmt_call *cstmt = (PLpgSQL_stmt_call *) stmt; E_WALK(stmt, cstmt->expr); break; } case PLPGSQL_STMT_COMMIT: case PLPGSQL_STMT_ROLLBACK: { /* no interesting sub-structure */ break; } } } /* * Returns true, when statement is any form of loop */ bool plch_statement_is_loop(PLpgSQL_stmt *stmt) { switch (stmt->cmd_type) { case PLPGSQL_STMT_LOOP: case PLPGSQL_STMT_FORI: case PLPGSQL_STMT_FORS: case PLPGSQL_STMT_FORC: case PLPGSQL_STMT_DYNFORS: case PLPGSQL_STMT_FOREACH_A: case PLPGSQL_STMT_WHILE: return true; default: return false; } } /* * Returns statements assigned to cycle's body */ List * plch_statement_get_loop_body(PLpgSQL_stmt *stmt) { List *stmts; Assert(plch_statement_is_loop(stmt)); switch (stmt->cmd_type) { case PLPGSQL_STMT_WHILE: stmts = ((PLpgSQL_stmt_while *) stmt)->body; break; case PLPGSQL_STMT_LOOP: stmts = ((PLpgSQL_stmt_loop *) stmt)->body; break; case PLPGSQL_STMT_FORI: stmts = ((PLpgSQL_stmt_fori *) stmt)->body; break; case PLPGSQL_STMT_FORS: stmts = ((PLpgSQL_stmt_fors *) stmt)->body; break; case PLPGSQL_STMT_FORC: stmts = ((PLpgSQL_stmt_forc *) stmt)->body; break; case PLPGSQL_STMT_DYNFORS: stmts = ((PLpgSQL_stmt_dynfors *) stmt)->body; break; case PLPGSQL_STMT_FOREACH_A: stmts = ((PLpgSQL_stmt_foreach_a *) stmt)->body; break; default: stmts = NIL; break; } return stmts; } /* * Returns true when statement can contains another statements */ bool plch_statement_is_container(PLpgSQL_stmt *stmt) { switch (stmt->cmd_type) { case PLPGSQL_STMT_BLOCK: case PLPGSQL_STMT_IF: case PLPGSQL_STMT_CASE: case PLPGSQL_STMT_LOOP: case PLPGSQL_STMT_FORI: case PLPGSQL_STMT_FORS: case PLPGSQL_STMT_FORC: case PLPGSQL_STMT_DYNFORS: case PLPGSQL_STMT_FOREACH_A: case PLPGSQL_STMT_WHILE: return true; default: return false; } } /* * Given a PLpgSQL_stmt, return the underlying PLpgSQL_expr that may contain a * queryid. */ PLpgSQL_expr * plch_statement_get_expr(PLpgSQL_stmt *stmt, bool *is_dynamic, List **params, const char **exprname) { PLpgSQL_expr *expr = NULL; List *_params = NIL; bool _is_dynamic = false; const char *_exprname = "expr"; switch (stmt->cmd_type) { case PLPGSQL_STMT_ASSIGN: expr = ((PLpgSQL_stmt_assign *) stmt)->expr; break; case PLPGSQL_STMT_PERFORM: expr = ((PLpgSQL_stmt_perform *) stmt)->expr; _exprname = "perform"; break; case PLPGSQL_STMT_CALL: expr = ((PLpgSQL_stmt_call *) stmt)->expr; break; case PLPGSQL_STMT_IF: expr = ((PLpgSQL_stmt_if *) stmt)->cond; _exprname = "cond"; break; case PLPGSQL_STMT_CASE: expr = ((PLpgSQL_stmt_case *) stmt)->t_expr; _exprname = "cond"; break; case PLPGSQL_STMT_WHILE: expr = ((PLpgSQL_stmt_while *) stmt)->cond; break; case PLPGSQL_STMT_FORC: expr = ((PLpgSQL_stmt_forc *) stmt)->argquery; _exprname = "query"; break; case PLPGSQL_STMT_FORS: expr = ((PLpgSQL_stmt_fors *) stmt)->query; _exprname = "query"; break; case PLPGSQL_STMT_DYNFORS: expr = ((PLpgSQL_stmt_dynfors *) stmt)->query; _exprname = "query"; _params = ((PLpgSQL_stmt_dynfors *) stmt)->params; _is_dynamic = true; break; case PLPGSQL_STMT_FOREACH_A: expr = ((PLpgSQL_stmt_foreach_a *) stmt)->expr; break; case PLPGSQL_STMT_FETCH: expr = ((PLpgSQL_stmt_fetch *) stmt)->expr; break; case PLPGSQL_STMT_EXIT: expr = ((PLpgSQL_stmt_exit *) stmt)->cond; break; case PLPGSQL_STMT_RETURN: expr = ((PLpgSQL_stmt_return *) stmt)->expr; break; case PLPGSQL_STMT_RETURN_NEXT: expr = ((PLpgSQL_stmt_return_next *) stmt)->expr; break; case PLPGSQL_STMT_RETURN_QUERY: { PLpgSQL_stmt_return_query *q; q = (PLpgSQL_stmt_return_query *) stmt; if (q->query) { expr = q->query; _exprname = "query"; } else { expr = q->dynquery; _params = q->params; _is_dynamic = true; } } break; case PLPGSQL_STMT_ASSERT: expr = ((PLpgSQL_stmt_assert *) stmt)->cond; break; case PLPGSQL_STMT_EXECSQL: expr = ((PLpgSQL_stmt_execsql *) stmt)->sqlstmt; _exprname = "query"; break; case PLPGSQL_STMT_DYNEXECUTE: expr = ((PLpgSQL_stmt_dynexecute *) stmt)->query; _params = ((PLpgSQL_stmt_dynexecute *) stmt)->params; _is_dynamic = true; break; case PLPGSQL_STMT_OPEN: { PLpgSQL_stmt_open *o; o = (PLpgSQL_stmt_open *) stmt; if (o->query) { expr = o->query; _exprname = "query"; } else if (o->dynquery) { expr = o->dynquery; _params = o->params; _is_dynamic = true; } else expr = o->argquery; } break; case PLPGSQL_STMT_BLOCK: case PLPGSQL_STMT_COMMIT: case PLPGSQL_STMT_ROLLBACK: case PLPGSQL_STMT_GETDIAG: case PLPGSQL_STMT_LOOP: case PLPGSQL_STMT_FORI: case PLPGSQL_STMT_RAISE: case PLPGSQL_STMT_CLOSE: break; } if (is_dynamic) *is_dynamic = _is_dynamic; if (params) *params = _params; if (exprname) *exprname = _exprname; return expr; } okbob-plpgsql_check-800db79/src/parser.c000066400000000000000000001251321526744060500202260ustar00rootroot00000000000000/*------------------------------------------------------------------------- * * parse_name.c * * parse function signature * parse identifier, and type name * * by Pavel Stehule 2013-2026 * *------------------------------------------------------------------------- */ #include "plpgsql_check.h" #include #include "catalog/namespace.h" #include "parser/parser.h" #include "parser/scansup.h" #include "parser/parse_type.h" #include "utils/builtins.h" #include "utils/lsyscache.h" #include "utils/regproc.h" /* * Originaly this structure was named TokenType, but this is in collision * with name from NT SDK. So it is renamed to PragmaTokenType. */ typedef struct PragmaTokenType { int value; const char *substr; size_t size; } PragmaTokenType; typedef struct TokenizerState { const char *str; PragmaTokenType saved_token; bool saved_token_is_valid; } TokenizerState; static char *make_string(PragmaTokenType *token); static const char *tagstr = "@plpgsql_check_options:"; #define PRAGMA_TOKEN_IDENTIF 128 #define PRAGMA_TOKEN_QIDENTIF 129 #define PRAGMA_TOKEN_NUMBER 130 #define PRAGMA_TOKEN_STRING 131 #ifdef _MSC_VER static void * memmem(const void *haystack, size_t haystack_len, const void *const needle, const size_t needle_len) { if (haystack == NULL) return NULL; /* or assert(haystack != NULL); */ if (haystack_len == 0) return NULL; if (needle == NULL) return NULL; /* or assert(needle != NULL); */ if (needle_len == 0) return NULL; for (const char *h = haystack; haystack_len >= needle_len; ++h, --haystack_len) { if (!memcmp(h, needle, needle_len)) { return (void *) h; } } return NULL; } #endif /* * Is character a valid identifier start? * Must match scan.l's {ident_start} character class. */ static bool is_ident_start(unsigned char c) { /* Underscores and ASCII letters are OK */ if (c == '_') return true; if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) return true; /* Any high-bit-set character is OK (might be part of a multibyte char) */ if (IS_HIGHBIT_SET(c)) return true; return false; } /* * Is character a valid identifier continuation? * Must match scan.l's {ident_cont} character class. */ static bool is_ident_cont(unsigned char c) { /* Can be digit or dollar sign ... */ if ((c >= '0' && c <= '9') || c == '$') return true; /* ... or an identifier start character */ return is_ident_start(c); } /* * parse_ident - returns list of Strings when input is valid name. * Returns NIL, when input string is signature. Can raise a error, * when input is not valid identifier. */ static List * parse_name_or_signature(char *qualname, bool *is_signature) { char *nextp; char *rawname; bool after_dot = false; List *result = NIL; /* We need a modifiable copy of the input string. */ rawname = pstrdup(qualname); /* * The code below scribbles on qualname_str in some cases, so we should * reconvert qualname if we need to show the original string in error * messages. */ nextp = rawname; /* skip leading whitespace */ while (scanner_isspace(*nextp)) nextp++; for (;;) { char *curname; bool missing_ident = true; if (*nextp == '"') { char *endp; curname = nextp + 1; for (;;) { endp = strchr(nextp + 1, '"'); if (!endp) ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("string is not a valid identifier: \"%s\"", qualname), errdetail("String has unclosed double quotes."))); if (endp[1] != '"') break; memmove(endp, endp + 1, strlen(endp)); nextp = endp; } nextp = endp + 1; *endp = '\0'; if (endp - curname == 0) ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("string is not a valid identifier: \"%s\"", qualname), errdetail("Quoted identifier must not be empty."))); truncate_identifier(curname, strlen(curname), true); result = lappend(result, makeString(curname)); missing_ident = false; } else if (is_ident_start((unsigned char) *nextp)) { char *downname; size_t len; curname = nextp++; while (is_ident_cont((unsigned char) *nextp)) nextp++; len = nextp - curname; downname = downcase_truncate_identifier(curname, (int) len, false); result = lappend(result, makeString(downname)); missing_ident = false; } if (missing_ident) { /* Different error messages based on where we failed. */ if (*nextp == '.') ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("string is not a valid identifier: \"%s\"", qualname), errdetail("No valid identifier before \".\"."))); else if (after_dot) ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("string is not a valid identifier: \"%s\"", qualname), errdetail("No valid identifier after \".\"."))); else ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("string is not a valid identifier: \"%s\"", qualname))); } while (scanner_isspace(*nextp)) nextp++; if (*nextp == '.') { after_dot = true; nextp++; while (scanner_isspace(*nextp)) nextp++; } else if (*nextp == '\0') { break; } else if (*nextp == '(') { *is_signature = true; return NIL; } else ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("string is not a valid identifier: \"%s\"", qualname))); } *is_signature = false; return result; } /* * Returns Oid of function specified by name or by signature * */ Oid plpgsql_check_parse_name_or_signature(char *name_or_signature) { List *names; bool is_signature; names = parse_name_or_signature(name_or_signature, &is_signature); if (!is_signature) { FuncCandidateList clist; #if PG_VERSION_NUM < 190000 clist = FuncnameGetCandidates(names, -1, NIL, false, false, false, true); #else int fgc_flags; clist = FuncnameGetCandidates(names, -1, NIL, false, false, false, true, &fgc_flags); (void) fgc_flags; #endif if (clist == NULL) ereport(ERROR, (errcode(ERRCODE_UNDEFINED_FUNCTION), errmsg("function \"%s\" does not exist", name_or_signature))); else if (clist->next != NULL) ereport(ERROR, (errcode(ERRCODE_AMBIGUOUS_FUNCTION), errmsg("more than one function named \"%s\"", name_or_signature))); return clist->oid; } return DatumGetObjectId(DirectFunctionCall1(regprocedurein, CStringGetDatum(name_or_signature))); } /* * Tokenize text. Only one error is possible here - unclosed double quotes. * Returns NULL, when found EOL or on error. */ static PragmaTokenType * get_token(TokenizerState *state, PragmaTokenType *token) { if (state->saved_token_is_valid) { state->saved_token_is_valid = false; return &state->saved_token; } /* skip inital spaces */ while (scanner_isspace(*state->str)) state->str++; if (!*state->str) return NULL; if (isdigit(*state->str)) { bool have_dot = false; token->value = PRAGMA_TOKEN_NUMBER; token->substr = state->str++; while (isdigit(*state->str) || (*state->str == '.')) { if (*state->str == '.') { if (!have_dot) have_dot = true; else break; } state->str += 1; } } else if (*state->str == '"') { bool is_error = true; token->value = PRAGMA_TOKEN_QIDENTIF; token->substr = state->str++; is_error = true; while (*state->str) { if (*state->str == '"') { state->str += 1; if (*state->str != '"') { is_error = false; break; } } state->str += 1; } if (is_error) elog(ERROR, "Syntax error (unclosed quoted identifier)"); } else if (*state->str == '\'') { bool is_error = true; token->value = PRAGMA_TOKEN_STRING; token->substr = state->str++; is_error = true; while (*state->str) { if (*state->str == '\'') { state->str += 1; if (*state->str != '\'') { is_error = false; break; } } state->str += 1; } if (is_error) elog(ERROR, "Syntax error (unclosed quoted identifier)"); } else if (is_ident_start(*state->str)) { token->value = PRAGMA_TOKEN_IDENTIF; token->substr = state->str++; while (is_ident_cont(*state->str)) state->str += 1; } else { token->substr = state->str; token->value = *state->str++; } token->size = state->str - token->substr; return token; } static void unget_token(TokenizerState *state, PragmaTokenType *token) { if (token) { state->saved_token.value = token->value; state->saved_token.substr = token->substr; state->saved_token.size = token->size; state->saved_token_is_valid = true; } else state->saved_token_is_valid = false; } static bool token_is_keyword(PragmaTokenType *token, const char *str) { if (!token) return false; if (token->value == PRAGMA_TOKEN_IDENTIF && token->size == strlen(str) && strncasecmp(token->substr, str, token->size) == 0) return true; return false; } /* * Returns true if all tokens was read */ static bool tokenizer_eol(TokenizerState *state) { if (state->saved_token_is_valid) return false; while (*state->str) { if (!isspace(*state->str)) return false; state->str += 1; } return true; } static void initialize_tokenizer(TokenizerState *state, const char *str) { state->str = str; state->saved_token_is_valid = false; } static char * make_ident(PragmaTokenType *token) { if (token->value == PRAGMA_TOKEN_IDENTIF) { return downcase_truncate_identifier(token->substr, (int) token->size, false); } else if (token->value == PRAGMA_TOKEN_QIDENTIF) { char *result = palloc(token->size); const char *ptr = token->substr + 1; char *write_ptr; size_t n = token->size - 2; write_ptr = result; while (n-- > 0) { *write_ptr++ = *ptr; if (*ptr++ == '"') { ptr += 1; n -= 1; } } *write_ptr = '\0'; truncate_identifier(result, (int) (write_ptr - result), false); return result; } else if (token->value == PRAGMA_TOKEN_STRING) { char *str = make_string(token); /* does same conversion like varchar->name */ truncate_identifier(str, (int) strlen(str), false); return str; } return NULL; } static char * make_string(PragmaTokenType *token) { if (token->value == PRAGMA_TOKEN_IDENTIF || token->value == PRAGMA_TOKEN_QIDENTIF) return make_ident(token); else if (token->value == PRAGMA_TOKEN_NUMBER) return pnstrdup(token->substr, token->size); else if (token->value == PRAGMA_TOKEN_STRING) { char *result = palloc(token->size); const char *ptr = token->substr + 1; char *write_ptr; size_t n = token->size - 2; write_ptr = result; while (n-- > 0) { *write_ptr++ = *ptr; if (*ptr++ == '\'') { ptr += 1; n -= 1; } } *write_ptr = '\0'; return result; } return NULL; } /* * Returns list of strings used in qualified identifiers */ static List * get_qualified_identifier(TokenizerState *state, List *result) { bool read_atleast_one = false; while (1) { PragmaTokenType token, *_token; _token = get_token(state, &token); if (!_token) break; if (_token->value != PRAGMA_TOKEN_IDENTIF && _token->value != PRAGMA_TOKEN_QIDENTIF) elog(ERROR, "Syntax error (expected identifier)"); result = lappend(result, make_ident(_token)); read_atleast_one = true; _token = get_token(state, &token); if (!_token) break; if (_token->value != '.') { unget_token(state, _token); break; } } if (!read_atleast_one) elog(ERROR, "Syntax error (expected identifier)"); return result; } /* * Set start position and length of qualified identifier. Returns true, * if parsed identifier is valid. */ static void parse_qualified_identifier(TokenizerState *state, const char **startptr, size_t *size) { bool read_atleast_one = false; const char *_startptr = *startptr; size_t _size = 0; while (1) { PragmaTokenType token, *_token; _token = get_token(state, &token); if (!_token) break; if (_token->value != PRAGMA_TOKEN_IDENTIF && _token->value != PRAGMA_TOKEN_QIDENTIF) elog(ERROR, "Syntax error (expected identifier)"); if (!_startptr) { _startptr = _token->substr; _size = _token->size; } else _size = _token->substr - _startptr + _token->size; read_atleast_one = true; _token = get_token(state, &token); if (!_token) break; if (_token->value != '.') { unget_token(state, _token); break; } } if (!read_atleast_one) elog(ERROR, "Syntax error (expected identifier)"); *startptr = _startptr; *size = _size; } static Oid get_scalar_type(TokenizerState *state, int32 *typmod) { PragmaTokenType token, *_token; const char *typename_start; const char *typename_end; const char *typestr; TypeName *typeName; Oid typtype; int paren_depth = 0; int bracket_depth = 0; _token = get_token(state, &token); if (!_token || (_token->value != PRAGMA_TOKEN_IDENTIF && _token->value != PRAGMA_TOKEN_QIDENTIF)) elog(ERROR, "Syntax error (expected identifier)"); typename_start = _token->substr; typename_end = typename_start; /* * Leave the enclosing field list or option separator unread. PostgreSQL * validates the complete type spelling, including modifiers and arrays. */ do { if (paren_depth == 0 && bracket_depth == 0 && (_token->value == ',' || _token->value == ')')) { unget_token(state, _token); break; } if (_token->value == '(') paren_depth++; else if (_token->value == ')' && paren_depth > 0) paren_depth--; else if (_token->value == '[') bracket_depth++; else if (_token->value == ']' && bracket_depth > 0) bracket_depth--; typename_end = _token->substr + _token->size; _token = get_token(state, &token); } while (_token); typestr = pnstrdup(typename_start, typename_end - typename_start); #if PG_VERSION_NUM >= 160000 typeName = typeStringToTypeName(typestr, NULL); #else typeName = typeStringToTypeName(typestr); #endif typenameTypeIdAndMod(NULL, typeName, &typtype, typmod); return typtype; } /* * When rectype is not allowed, then composite type is allowed only * on top level. */ static Oid get_type_internal(TokenizerState *state, int32 *typmod, bool allow_rectype, bool istop) { PragmaTokenType token, *_token; Oid typtype; check_stack_depth(); _token = get_token(state, &token); if (!_token) elog(ERROR, "Syntax error (expected identifier)"); if (_token->value == '(') { TupleDesc resultTupleDesc; List *names = NIL; List *types = NIL; List *typmods = NIL; List *collations = NIL; if (!allow_rectype && !istop) elog(ERROR, "Cannot to create table with pseudo-type record."); _token = get_token(state, &token); if (token_is_keyword(_token, "like")) { typtype = get_type_internal(state, typmod, allow_rectype, false); if (!type_is_rowtype(typtype)) elog(ERROR, "\"%s\" is not composite type", format_type_be(typtype)); _token = get_token(state, &token); if (!_token || _token->value != ')') elog(ERROR, "Syntax error (expected \")\")"); return typtype; } else unget_token(state, _token); while (1) { Oid _typtype; int32 _typmod; _token = get_token(state, &token); if (_token && _token->value == ')' && names == NIL) break; if (!_token || (_token->value != PRAGMA_TOKEN_IDENTIF && _token->value != PRAGMA_TOKEN_QIDENTIF)) elog(ERROR, "Syntax error (expected identifier)"); names = lappend(names, makeString(make_ident(_token))); _typtype = get_type_internal(state, &_typmod, allow_rectype, false); types = lappend_oid(types, _typtype); typmods = lappend_int(typmods, _typmod); collations = lappend_oid(collations, InvalidOid); _token = get_token(state, &token); if (!_token) elog(ERROR, "Syntax error (unclosed composite type definition - expected \")\")"); if (_token->value == ')') { break; } else if (_token->value != ',') elog(ERROR, "Syntax error (expected \",\")"); } resultTupleDesc = BuildDescFromLists(names, types, typmods, collations); resultTupleDesc = BlessTupleDesc(resultTupleDesc); *typmod = resultTupleDesc->tdtypmod; return resultTupleDesc->tdtypeid; } unget_token(state, _token); return get_scalar_type(state, typmod); } static Oid get_type(TokenizerState *state, int32 *typmod, bool allow_rectype) { return get_type_internal(state, typmod, allow_rectype, true); } static int get_varno(PLpgSQL_nsitem *cur_ns, List *names) { char *name1 = NULL; char *name2 = NULL; char *name3 = NULL; int names_used; PLpgSQL_nsitem *nsitem; switch (list_length(names)) { case 1: { name1 = (char *) linitial(names); break; } case 2: { name1 = (char *) linitial(names); name2 = (char *) lsecond(names); break; } case 3: { name1 = (char *) linitial(names); name2 = (char *) lsecond(names); name3 = (char *) lthird(names); break; } default: return -1; } nsitem = plpgsql_check__ns_lookup_p(cur_ns, false, name1, name2, name3, &names_used); return nsitem ? nsitem->itemno : -1; } static char * get_name(List *names) { bool is_first = true; ListCell *lc; StringInfoData sinfo; initStringInfo(&sinfo); foreach(lc, names) { if (is_first) is_first = false; else appendStringInfoChar(&sinfo, '.'); appendStringInfo(&sinfo, "\"%s\"", (char *) lfirst(lc)); } return sinfo.data; } static const char * pragma_assert_name(PragmaAssertType pat) { switch (pat) { case PLPGSQL_CHECK_PRAGMA_ASSERT_SCHEMA: return "assert-schema"; case PLPGSQL_CHECK_PRAGMA_ASSERT_TABLE: return "assert-table"; case PLPGSQL_CHECK_PRAGMA_ASSERT_COLUMN: return "assert-column"; } return NULL; } static Oid check_var_schema(PLpgSQL_checkstate *cstate, int dno) { return get_namespace_oid(cstate->strconstvars[dno], false); } static Oid check_var_table(PLpgSQL_checkstate *cstate, int dno1, int dno2) { char *relname = cstate->strconstvars[dno2]; Oid relid = InvalidOid; if (dno1 != -1) relid = get_relname_relid(relname, check_var_schema(cstate, dno1)); else relid = RelnameGetRelid(relname); if (!OidIsValid(relid)) ereport(ERROR, (errcode(ERRCODE_UNDEFINED_TABLE), errmsg("table \"%s\" does not exist", relname))); return relid; } static AttrNumber check_var_column(PLpgSQL_checkstate *cstate, int dno1, int dno2, int dno3) { char *attname = cstate->strconstvars[dno3]; Oid relid = check_var_table(cstate, dno1, dno2); AttrNumber attnum; attnum = get_attnum(relid, attname); if (attnum == InvalidAttrNumber) ereport(ERROR, (errcode(ERRCODE_UNDEFINED_COLUMN), errmsg("column \"%s\" of relation \"%s\".\"%s\" does not exist", attname, get_namespace_name(get_rel_namespace(relid)), get_rel_name(relid)))); return attnum; } bool plpgsql_check_pragma_assert(PLpgSQL_checkstate *cstate, PragmaAssertType pat, const char *str, PLpgSQL_nsitem *ns, int lineno) { MemoryContext oldCxt; ResourceOwner oldowner; volatile int dno[3]; volatile int nvars = 0; volatile bool result = true; /* * namespace is available only in compile check mode, and only in this * mode this pragma can be used. */ if (!ns || !cstate) return true; oldCxt = CurrentMemoryContext; oldowner = CurrentResourceOwner; BeginInternalSubTransaction(NULL); MemoryContextSwitchTo(cstate->check_cxt); PG_TRY(); { TokenizerState tstate; int i; List *names; initialize_tokenizer(&tstate, str); for (i = 0; i < 3; i++) { if (i > 0) { PragmaTokenType token, *_token; _token = get_token(&tstate, &token); if (_token->value != ',') elog(ERROR, "Syntax error (expected \",\")"); } names = get_qualified_identifier(&tstate, NULL); if ((dno[i] = get_varno(ns, names)) == -1) elog(ERROR, "Cannot to find variable %s used in \"%s\" pragma", get_name(names), pragma_assert_name(pat)); if (!cstate->strconstvars || !cstate->strconstvars[dno[i]]) elog(ERROR, "Variable %s has not assigned constant", get_name(names)); nvars += 1; if (tokenizer_eol(&tstate)) break; } if (!tokenizer_eol(&tstate)) elog(ERROR, "Syntax error (unexpected chars after variable)"); if ((pat == PLPGSQL_CHECK_PRAGMA_ASSERT_SCHEMA && nvars > 1) || (pat == PLPGSQL_CHECK_PRAGMA_ASSERT_TABLE && nvars > 2) || (pat == PLPGSQL_CHECK_PRAGMA_ASSERT_COLUMN && nvars > 3)) elog(ERROR, "too much variables for \"%s\" pragma", pragma_assert_name(pat)); if (pat == PLPGSQL_CHECK_PRAGMA_ASSERT_COLUMN && nvars < 2) elog(ERROR, "too few variables for \"%s\" pragma", pragma_assert_name(pat)); RollbackAndReleaseCurrentSubTransaction(); MemoryContextSwitchTo(oldCxt); CurrentResourceOwner = oldowner; } PG_CATCH(); { ErrorData *edata; MemoryContextSwitchTo(oldCxt); edata = CopyErrorData(); FlushErrorState(); RollbackAndReleaseCurrentSubTransaction(); MemoryContextSwitchTo(oldCxt); CurrentResourceOwner = oldowner; /* raise warning (errors in pragma can be ignored instead */ ereport(WARNING, (errmsg("\"%s\" on line %d is not processed.", pragma_assert_name(pat), lineno), errdetail("%s", edata->message))); FreeErrorData(edata); result = false; } PG_END_TRY(); if (!result) return false; if (pat == PLPGSQL_CHECK_PRAGMA_ASSERT_SCHEMA) { Assert(nvars == 1); (void) check_var_schema(cstate, dno[0]); } else if (pat == PLPGSQL_CHECK_PRAGMA_ASSERT_TABLE) { if (nvars == 1) (void) check_var_table(cstate, -1, dno[0]); else { Assert(nvars == 2); (void) check_var_table(cstate, dno[0], dno[1]); } } else if (pat == PLPGSQL_CHECK_PRAGMA_ASSERT_COLUMN) { if (nvars == 2) (void) check_var_column(cstate, -1, dno[0], dno[1]); else { Assert(nvars == 3); (void) check_var_column(cstate, dno[0], dno[1], dno[2]); } } return result; } bool plpgsql_check_pragma_type(PLpgSQL_checkstate *cstate, const char *str, PLpgSQL_nsitem *ns, int lineno) { MemoryContext oldCxt; ResourceOwner oldowner; volatile bool result = true; /* * namespace is available only in compile check mode, and only in this * mode this pragma can be used. */ if (!ns || !cstate) return true; oldCxt = CurrentMemoryContext; oldowner = CurrentResourceOwner; BeginInternalSubTransaction(NULL); MemoryContextSwitchTo(cstate->check_cxt); PG_TRY(); { TokenizerState tstate; int target_dno; PLpgSQL_datum *target; List *names; Oid typtype; int32 typmod; TupleDesc typtupdesc; initialize_tokenizer(&tstate, str); names = get_qualified_identifier(&tstate, NULL); if ((target_dno = get_varno(ns, names)) == -1) elog(ERROR, "Cannot find variable %s used in type pragma", get_name(names)); target = cstate->estate->datums[target_dno]; if (target->dtype != PLPGSQL_DTYPE_REC) elog(ERROR, "Pragma \"type\" can be applied only on variable of record type"); typtype = get_type(&tstate, &typmod, true); if (!tokenizer_eol(&tstate)) elog(ERROR, "Syntax error (unexpected chars after type specification)"); typtupdesc = lookup_rowtype_tupdesc_copy(typtype, typmod); plpgsql_check_assign_tupdesc_dno(cstate, target_dno, typtupdesc, false); cstate->typed_variables = bms_add_member(cstate->typed_variables, target_dno); RollbackAndReleaseCurrentSubTransaction(); MemoryContextSwitchTo(oldCxt); CurrentResourceOwner = oldowner; } PG_CATCH(); { ErrorData *edata; MemoryContextSwitchTo(oldCxt); edata = CopyErrorData(); FlushErrorState(); RollbackAndReleaseCurrentSubTransaction(); MemoryContextSwitchTo(oldCxt); CurrentResourceOwner = oldowner; /* raise warning (errors in pragma can be ignored instead */ ereport(WARNING, (errmsg("Pragma \"type\" on line %d is not processed.", lineno), errdetail("%s", edata->message))); FreeErrorData(edata); result = false; } PG_END_TRY(); return result; } /* * Unfortunately the ephemeral tables introduced in PostgreSQL 10 cannot be * used for this purpose, because any DML operations are prohibited, and others * DML catalogue operations doesn't calculate with Ephemeral space. */ bool plpgsql_check_pragma_table(PLpgSQL_checkstate *cstate, const char *str, int lineno) { MemoryContext oldCxt; ResourceOwner oldowner; volatile bool result = true; if (!cstate) return true; oldCxt = CurrentMemoryContext; oldowner = CurrentResourceOwner; BeginInternalSubTransaction(NULL); MemoryContextSwitchTo(cstate->check_cxt); PG_TRY(); { TokenizerState tstate; PragmaTokenType token, *_token; PragmaTokenType token2, *_token2; StringInfoData query; int32 typmod; initialize_tokenizer(&tstate, str); _token = get_token(&tstate, &token); if (!_token || (_token->value != PRAGMA_TOKEN_IDENTIF && _token->value != PRAGMA_TOKEN_QIDENTIF)) elog(ERROR, "Syntax error (expected identifier)"); _token2 = get_token(&tstate, &token2); if (_token2 && _token2->value == '.') { char *nsname = make_ident(_token); if (strcmp(nsname, "pg_temp") != 0) elog(ERROR, "schema \"%s\" cannot be used in pragma \"table\" (only \"pg_temp\" schema is allowed)", nsname); _token = get_token(&tstate, &token); if (!_token || (_token->value != PRAGMA_TOKEN_IDENTIF && _token->value != PRAGMA_TOKEN_QIDENTIF)) elog(ERROR, "Syntax error (expected identifier)"); _token2 = get_token(&tstate, &token2); } if (!_token2 || _token2->value != '(') elog(ERROR, "Syntax error (expected table specification)"); unget_token(&tstate, _token2); (void) get_type(&tstate, &typmod, false); if (!tokenizer_eol(&tstate)) elog(ERROR, "Syntax error (unexpected chars after table specification)"); /* In this case we use parser just for syntax check and security check */ initStringInfo(&query); appendStringInfoString(&query, "CREATE TEMP TABLE "); appendStringInfoString(&query, str); if (SPI_execute(query.data, false, 0) != SPI_OK_UTILITY) elog(NOTICE, "Cannot to create temporary table"); ReleaseCurrentSubTransaction(); MemoryContextSwitchTo(oldCxt); CurrentResourceOwner = oldowner; } PG_CATCH(); { ErrorData *edata; MemoryContextSwitchTo(oldCxt); edata = CopyErrorData(); FlushErrorState(); RollbackAndReleaseCurrentSubTransaction(); MemoryContextSwitchTo(oldCxt); CurrentResourceOwner = oldowner; /* raise warning (errors in pragma can be ignored instead */ ereport(WARNING, (errmsg("Pragma \"table\" on line %d is not processed.", lineno), errdetail("%s", edata->message))); FreeErrorData(edata); result = false; } PG_END_TRY(); return result; } /* * An sequence can be temporary too, so there should be related PRAGMA */ bool plpgsql_check_pragma_sequence(PLpgSQL_checkstate *cstate, const char *str, int lineno) { MemoryContext oldCxt; ResourceOwner oldowner; volatile bool result = true; if (!cstate) return true; oldCxt = CurrentMemoryContext; oldowner = CurrentResourceOwner; BeginInternalSubTransaction(NULL); MemoryContextSwitchTo(cstate->check_cxt); PG_TRY(); { TokenizerState tstate; PragmaTokenType token, *_token; PragmaTokenType token2, *_token2; StringInfoData query; initialize_tokenizer(&tstate, str); _token = get_token(&tstate, &token); if (!_token || (_token->value != PRAGMA_TOKEN_IDENTIF && _token->value != PRAGMA_TOKEN_QIDENTIF)) elog(ERROR, "Syntax error (expected identifier)"); _token2 = get_token(&tstate, &token2); if (_token2 && _token2->value == '.') { char *nsname = make_ident(_token); if (strcmp(nsname, "pg_temp") != 0) elog(ERROR, "schema \"%s\" cannot be used in pragma \"sequence\" (only \"pg_temp\" schema is allowed)", nsname); _token = get_token(&tstate, &token); if (!_token || (_token->value != PRAGMA_TOKEN_IDENTIF && _token->value != PRAGMA_TOKEN_QIDENTIF)) elog(ERROR, "Syntax error (expected identifier)"); (void) get_token(&tstate, &token2); } if (!tokenizer_eol(&tstate)) elog(ERROR, "Syntax error (unexpected chars after sequence name)"); /* In this case we use parser just for syntax check and security check */ initStringInfo(&query); appendStringInfoString(&query, "CREATE TEMP SEQUENCE "); appendStringInfoString(&query, str); if (SPI_execute(query.data, false, 0) != SPI_OK_UTILITY) elog(NOTICE, "Cannot to create temporary sequence"); ReleaseCurrentSubTransaction(); MemoryContextSwitchTo(oldCxt); CurrentResourceOwner = oldowner; } PG_CATCH(); { ErrorData *edata; MemoryContextSwitchTo(oldCxt); edata = CopyErrorData(); FlushErrorState(); RollbackAndReleaseCurrentSubTransaction(); MemoryContextSwitchTo(oldCxt); CurrentResourceOwner = oldowner; /* raise warning (errors in pragma can be ignored instead */ ereport(WARNING, (errmsg("Pragma \"sequence\" on line %d is not processed.", lineno), errdetail("%s", edata->message))); FreeErrorData(edata); result = false; } PG_END_TRY(); return result; } static bool get_boolean_comment_option(TokenizerState *tstate, const char *name, plpgsql_check_info *cinfo) { PragmaTokenType token, *_token; _token = get_token(tstate, &token); if (!_token) return true; if (_token->value == ',') { unget_token(tstate, _token); return true; } if (_token->value == '=') { _token = get_token(tstate, &token); if (!_token) elog(ERROR, "syntax error in comment option \"%s\" (fnoid: %u) (expected boolean value after \"=\")", name, cinfo->fn_oid); } if (token_is_keyword(_token, "true") || token_is_keyword(_token, "yes") || token_is_keyword(_token, "t") || token_is_keyword(_token, "on")) return true; else if (token_is_keyword(_token, "false") || token_is_keyword(_token, "no") || token_is_keyword(_token, "f") || token_is_keyword(_token, "off")) return false; else elog(ERROR, "syntax error in comment option \"%s\" (fnoid: %u) (expected boolean value)", name, cinfo->fn_oid); /* fix warning C4715 on msvc */ Assert(0); return false; } static char * get_name_comment_option(TokenizerState *tstate, const char *name, plpgsql_check_info *cinfo) { PragmaTokenType token, *_token; _token = get_token(tstate, &token); if (!_token) elog(ERROR, "syntax error in comment option \"%s\" (fnoid: %u) (expected option's argument of name type)", name, cinfo->fn_oid); if (_token->value == '=') { _token = get_token(tstate, &token); if (!_token) elog(ERROR, "syntax error in comment option \"%s\" (fnoid: %u) (expected name value after \"=\")", name, cinfo->fn_oid); } if (_token->value == PRAGMA_TOKEN_IDENTIF || _token->value == PRAGMA_TOKEN_QIDENTIF || _token->value == PRAGMA_TOKEN_STRING) { return pstrdup(make_ident(_token)); } else elog(ERROR, "syntax error in comment option \"%s\" (fnoid: %u) (expected SQL identifier as argument)", name, cinfo->fn_oid); /* fix warning C4715 on msvc */ Assert(0); return NULL; } static Oid get_type_comment_option(TokenizerState *tstate, const char *name, plpgsql_check_info *cinfo) { PragmaTokenType token, *_token; _token = get_token(tstate, &token); if (!_token) elog(ERROR, "syntax error in comment option \"%s\" (fnoid: %u) (expected option's argument of type name)", name, cinfo->fn_oid); if (_token->value == '=') { _token = get_token(tstate, &token); if (!_token) elog(ERROR, "syntax error in comment option \"%s\" (fnoid: %u) (expected type name after \"=\")", name, cinfo->fn_oid); } if (_token->value == PRAGMA_TOKEN_IDENTIF || _token->value == PRAGMA_TOKEN_QIDENTIF) { int32 typmod; unget_token(tstate, _token); return get_scalar_type(tstate, &typmod); } else elog(ERROR, "syntax error in comment option \"%s\" (fnoid: %u) (expected type identifier)", name, cinfo->fn_oid); /* fix warning C4715 on msvc */ Assert(0); return InvalidOid; } static Oid get_table_comment_option(TokenizerState *tstate, const char *name, plpgsql_check_info *cinfo) { PragmaTokenType token, *_token; _token = get_token(tstate, &token); if (!_token) elog(ERROR, "syntax error in comment option \"%s\" (fnoid: %u) (expected option's argument of table name)", name, cinfo->fn_oid); if (_token->value == '=') { _token = get_token(tstate, &token); if (!_token) elog(ERROR, "syntax error in comment option \"%s\" (fnoid: %u) (expected table name after \"=\")", name, cinfo->fn_oid); } if (_token->value == PRAGMA_TOKEN_IDENTIF || _token->value == PRAGMA_TOKEN_QIDENTIF) { List *names; const char *tablename_start = NULL; size_t tablename_length; char *tablenamestr; Oid result; unget_token(tstate, _token); parse_qualified_identifier(tstate, &tablename_start, &tablename_length); tablenamestr = pnstrdup(tablename_start, tablename_length); #if PG_VERSION_NUM >= 160000 names = stringToQualifiedNameList(tablenamestr, NULL); #else names = stringToQualifiedNameList(tablenamestr); #endif /* We might not even have permissions on this relation; don't lock it. */ result = RangeVarGetRelid(makeRangeVarFromNameList(names), NoLock, false); return result; } else elog(ERROR, "syntax error in comment option \"%s\" (fnoid: %u) (expected table identifier)", name, cinfo->fn_oid); /* fix warning C4715 on msvc */ Assert(0); return InvalidOid; } static bool is_keyword(char *str, size_t bytes, const char *keyword) { if (bytes != strlen(keyword)) return false; if (strncasecmp(str, keyword, bytes) != 0) return false; return true; } char * plpgsql_check_process_echo_string(char *str, plpgsql_check_info *cinfo) { StringInfoData sinfo; initStringInfo(&sinfo); while (*str) { if (*str == '@' && str[1] == '@') { char *start; size_t bytes; str += 2; start = str; while (*str && isalpha(*str)) { str += 1; } bytes = str - start; if (is_keyword(start, bytes, "id")) { appendStringInfo(&sinfo, "%u", cinfo->fn_oid); } else if (is_keyword(start, bytes, "name")) { if (OidIsValid(cinfo->fn_oid)) appendStringInfoString(&sinfo, get_func_name(cinfo->fn_oid)); else appendStringInfoString(&sinfo, "inline code block"); } else if (is_keyword(start, bytes, "signature")) { if (OidIsValid(cinfo->fn_oid)) appendStringInfoString(&sinfo, format_procedure(cinfo->fn_oid)); else appendStringInfoString(&sinfo, "inline code block"); } else appendStringInfo(&sinfo, "@@%.*s", (int) bytes, start); } else appendStringInfoChar(&sinfo, *str++); } return sinfo.data; } static void comment_options_parser(char *str, plpgsql_check_info *cinfo) { TokenizerState tstate; PragmaTokenType token, *_token; initialize_tokenizer(&tstate, str); do { _token = get_token(&tstate, &token); if (!_token || (_token->value != PRAGMA_TOKEN_IDENTIF)) elog(ERROR, "Syntax error (fnoid: %u) (expected option name)", cinfo->fn_oid); if (cinfo->incomment_options_usage_warning) elog(WARNING, "comment option \"%s\" is used (oid: %u)", make_ident(_token), cinfo->fn_oid); if (token_is_keyword(_token, "relid")) { cinfo->relid = get_table_comment_option(&tstate, "relid", cinfo); } else if (token_is_keyword(_token, "fatal_errors")) { cinfo->fatal_errors = get_boolean_comment_option(&tstate, "fatal_errors", cinfo); } else if (token_is_keyword(_token, "other_warnings")) { cinfo->other_warnings = get_boolean_comment_option(&tstate, "other_warnings", cinfo); } else if (token_is_keyword(_token, "extra_warnings")) { cinfo->extra_warnings = get_boolean_comment_option(&tstate, "extra_warnings", cinfo); } else if (token_is_keyword(_token, "performance_warnings")) { cinfo->performance_warnings = get_boolean_comment_option(&tstate, "performance_warnings", cinfo); } else if (token_is_keyword(_token, "security_warnings")) { cinfo->security_warnings = get_boolean_comment_option(&tstate, "security_warnings", cinfo); } else if (token_is_keyword(_token, "compatibility_warnings")) { cinfo->compatibility_warnings = get_boolean_comment_option(&tstate, "compatibility_warnings", cinfo); } else if (token_is_keyword(_token, "anyelementtype")) { cinfo->anyelementoid = get_type_comment_option(&tstate, "anyelementtype", cinfo); } else if (token_is_keyword(_token, "anyenumtype")) { cinfo->anyenumoid = get_type_comment_option(&tstate, "anyenumtype", cinfo); } else if (token_is_keyword(_token, "anyrangetype")) { cinfo->anyrangeoid = get_type_comment_option(&tstate, "anyrangetype", cinfo); if (!type_is_range(cinfo->anyrangeoid)) elog(ERROR, "the type specified by \"anyrangetype\" comment option is not range (fnoid: %u)", cinfo->fn_oid); } else if (token_is_keyword(_token, "anycompatibletype")) { cinfo->anycompatibleoid = get_type_comment_option(&tstate, "anycompatibletype", cinfo); } else if (token_is_keyword(_token, "anycompatiblerangetype")) { cinfo->anycompatiblerangeoid = get_type_comment_option(&tstate, "anycompatiblerangetype", cinfo); if (!type_is_range(cinfo->anycompatiblerangeoid)) elog(ERROR, "the type specified by \"anycompatiblerangetype\" comment option is not range (fnoid: %u)", cinfo->fn_oid); } else if (token_is_keyword(_token, "without_warnings")) { cinfo->without_warnings = get_boolean_comment_option(&tstate, "without_warnings", cinfo); } else if (token_is_keyword(_token, "all_warnings")) { cinfo->all_warnings = get_boolean_comment_option(&tstate, "all_warnings", cinfo); } else if (token_is_keyword(_token, "newtable")) { cinfo->newtable = get_name_comment_option(&tstate, "newtable", cinfo); } else if (token_is_keyword(_token, "oldtable")) { cinfo->oldtable = get_name_comment_option(&tstate, "oldtable", cinfo); } else if (token_is_keyword(_token, "echo")) { _token = get_token(&tstate, &token); if (!_token) elog(ERROR, "missing argument of option \"echo\""); if (_token->value == '=') { _token = get_token(&tstate, &token); if (!_token) elog(ERROR, "expected value after \"=\""); } if (_token->value == PRAGMA_TOKEN_IDENTIF) elog(NOTICE, "comment option \"echo\" is %s", plpgsql_check_process_echo_string(make_string(_token), cinfo)); else if (_token->value == PRAGMA_TOKEN_QIDENTIF) elog(NOTICE, "comment option \"echo\" is \"%s\"", plpgsql_check_process_echo_string(make_string(_token), cinfo)); else if (_token->value == PRAGMA_TOKEN_NUMBER) elog(NOTICE, "comment option \"echo\" is %s", plpgsql_check_process_echo_string(make_string(_token), cinfo)); else if (_token->value == PRAGMA_TOKEN_STRING) elog(NOTICE, "comment option \"echo\" is '%s'", plpgsql_check_process_echo_string(make_string(_token), cinfo)); else elog(NOTICE, "comment option \"echo\" is '%c'", _token->value); } else elog(ERROR, "unsupported option \"%.*s\" specified by \"@plpgsql_check_options\" (fnoid: %u)", (int) _token->size, _token->substr, cinfo->fn_oid); _token = get_token(&tstate, &token); if (!_token) break; if (_token->value != ',') elog(ERROR, "expected \",\" or end of line on line with \"@plpgsql_check_options\" options (fnoid: %u)", cinfo->fn_oid); } while (_token); } static void comment_options_parsecontent(char *str, size_t bytes, plpgsql_check_info *cinfo) { char *endchar = str + bytes; do { char *ptr, *optsline; bool found_eol; str += strlen(tagstr); Assert(str <= endchar); /* find end of line */ ptr = str; found_eol = false; while (ptr < endchar && *ptr) { if (*ptr == '\n') { found_eol = true; break; } ptr += 1; } optsline = pnstrdup(str, found_eol ? ptr - str : endchar - str); comment_options_parser(optsline, cinfo); pfree(optsline); if (!found_eol || ptr >= endchar) break; str = memmem(ptr + 1, endchar - (ptr + 1), tagstr, strlen(tagstr)); } while (str); } static char * search_comment_options_linecomment(char *src, plpgsql_check_info *cinfo) { char *start = src; char *tag; while (*src && *src != '\n') src += 1; tag = memmem(start, src - start, tagstr, strlen(tagstr)); if (tag) comment_options_parsecontent(tag, src - tag, cinfo); return *src ? src + 1 : src; } static char * search_comment_options_multilinecomment(char *src, plpgsql_check_info *cinfo) { char *start = src; int depth = 1; while (*src) { if (*src == '/' && src[1] == '*') { depth++; src += 2; } else if (*src == '*' && src[1] == '/') { char *tag; /* An inner terminator must not become part of its option line. */ tag = memmem(start, src - start, tagstr, strlen(tagstr)); if (tag) comment_options_parsecontent(tag, src - tag, cinfo); src += 2; if (--depth == 0) return src; start = src; } else src += 1; } return src; } /* * Try to read plpgsql_check options. * */ void plpgsql_check_search_comment_options(plpgsql_check_info *cinfo) { char *src = plpgsql_check_get_src(cinfo->proctuple); char *srcstart = src; cinfo->all_warnings = false; cinfo->without_warnings = false; while (*src) { if (*src == '-' && src[1] == '-') src = search_comment_options_linecomment(src + 2, cinfo); else if (*src == '/' && src[1] == '*') src = search_comment_options_multilinecomment(src + 2, cinfo); else if (*src == '\'') { #if PG_VERSION_NUM < 190000 bool backslash_escapes = !standard_conforming_strings || (src > srcstart && (src[-1] == 'e' || src[-1] == 'E') && (src == srcstart + 1 || !is_ident_cont((unsigned char) src[-2]))); #else bool backslash_escapes = (src > srcstart && (src[-1] == 'e' || src[-1] == 'E') && (src == srcstart + 1 || !is_ident_cont((unsigned char) src[-2]))); #endif src++; while (*src) { if (*src == '\\' && backslash_escapes && src[1]) src += 2; else if (*src++ == '\'') { if (*src == '\'') src += 1; else break; } } } else if (*src == '"') { src++; while (*src) { if (*src++ == '"') { if (*src == '"') src += 1; else break; } } } else if (*src == '$' && (src == srcstart || !is_ident_cont((unsigned char) src[-1]))) { char *start = src++; if (is_ident_start((unsigned char) *src)) { do { src += 1; } while (*src != '$' && is_ident_cont((unsigned char) *src)); } /* found ending $ of starting tag */ if (*src == '$') { /* size of custom string separator (include dolars) */ size_t cust_str_length = src - start + 1; while (*src != '\0') { if (strncmp(src, start, cust_str_length) == 0) { /* found complete custom string separator */ src += cust_str_length; break; } src += 1; } } else src = start + 1; } else src += 1; } if (cinfo->all_warnings && cinfo->without_warnings) elog(ERROR, "all_warnings and without_warnings cannot be used together (fnoid: %u)", cinfo->fn_oid); if (cinfo->all_warnings) plpgsql_check_set_all_warnings(cinfo); else if (cinfo->without_warnings) plpgsql_check_set_without_warnings(cinfo); } okbob-plpgsql_check-800db79/src/pldbgapi3.c000066400000000000000000000532751526744060500206070ustar00rootroot00000000000000/*------------------------------------------------------------------------- * * pldbgapi3 * * enhanced debug API for plpgsql * * by Pavel Stehule 2013-2026 * *------------------------------------------------------------------------- * * Notes: * PL debug API has few issues related to plpgsql profiler and tracer: * * 1. Only one extension that use this API can be active * * 2. Doesn't catch an application's exceptions, and cannot to handle an * exceptions in applications. * * pldbgapi3 enhancing pl debug API and implementation allows to register * more plugins. Against previous implementation (pldbgapi2), it doesn't * use fmgr API. Cleaning statement stack after an exception is implemented * by usage memory context reset callback. The main advantage of this design * is possibility to access function's runtime data, that are accesible at this * time. * * */ #include "postgres.h" #include "plpgsql.h" #include "utils/palloc.h" #include "plpgsql_check.h" #define MAX_PLUGINS 10 typedef struct plpgsql_plugin_info { int magic; void *prev_plugin_info; /* for assertations */ Oid fn_oid; PLpgSQL_execstate *estate; /* * Raising of any exception by errfinish collects stacktrace. In own error * context callback we can persist error data. * * Note: we don't need to clean error_context_stack it is done by * plpgsql_handler. */ ErrorContextCallback plerrcontext; MemoryContext mcxt; MemoryContext ErrorContext; ErrorData *cur_error; PLpgSQL_stmt *err_stmt; plch_fextra *fextra; void *plugin_info[MAX_PLUGINS]; bool is_active[MAX_PLUGINS]; /* * This array holds a stack of opened statements. It is the base for * calling stmt_abort callback. */ PLpgSQL_stmt **stmts_stack; int stmts_stack_size; /* * This array holds a statements, that should be removed from stmts_stack. * It can be used inside a exception, and then can be dangerous to * allocate memory. So this array is preallocated. */ PLpgSQL_stmt **stmts_buf; MemoryContextCallback mcb; /* * Unfortunately, memory of calling inline block is released before memory * of called function (after an exception). So we need to force execution * plugin_info_reset in correct order, and then we need to hold sequence * of pldbgapi plugins info. */ struct plpgsql_plugin_info *prev_pldbgapi_plugin_info; } plpgsql_plugin_info; static plpgsql_plugin_info *top_pldbgapi_plugin_info = NULL; #define PLUGIN_INFO_MAGIC 2026010118 static plch_plugin *plugins[MAX_PLUGINS]; static int nplugins = 0; static void func_setup(PLpgSQL_execstate *estate, PLpgSQL_function *func); static void func_beg(PLpgSQL_execstate *estate, PLpgSQL_function *func); static void func_end(PLpgSQL_execstate *estate, PLpgSQL_function *func); static void stmt_beg(PLpgSQL_execstate *estate, PLpgSQL_stmt *stmt); static void stmt_end(PLpgSQL_execstate *estate, PLpgSQL_stmt *stmt); static void plpgsql_check_error_callback(void *arg); static PLpgSQL_plugin plpgsql_plugin = { .func_setup = func_setup, .func_beg = func_beg, .func_end = func_end, .stmt_beg = stmt_beg, .stmt_end = stmt_end }; static PLpgSQL_plugin *prev_plpgsql_plugin = NULL; /* * For all statements from buffer for all plugins calls * stmt_abort callback. This can be used on stmts_stack * and then it is processed from top, or can be used stmts_buf, * and then it is processed from bottom. In this case, the * statement order is inverted when statements are copied from * stmts_stack to stmts_buf. * */ static void abort_statements(PLpgSQL_stmt **stmts, int nstmts, plpgsql_plugin_info *plugin_info, bool from_top) { MemoryContext exec_mcxt = CurrentMemoryContext; int i; int j; PLpgSQL_stmt *err_stmt = plugin_info->estate->err_stmt; /* * overwrite estate->err_stmt - this field can be overwritten already by * exec_stmts - abort_statements can be called from stmt_beg when an * exception is handled, and first statement of exception handler is * executed. */ plugin_info->estate->err_stmt = plugin_info->err_stmt; PG_TRY(); { for (i = 0; i < nplugins; i++) { if (plugin_info->is_active[i] && plugins[i]->stmt_abort) { plugin_info->estate->plugin_info = plugin_info->plugin_info[i]; if (from_top) { for (j = nstmts - 1; j >= 0; j--) { MemoryContextSwitchTo(exec_mcxt); plugins[i]->stmt_abort(plugin_info->estate, stmts[j], plugin_info->fextra); } } else { for (j = 0; j < nstmts; j++) { MemoryContextSwitchTo(exec_mcxt); plugins[i]->stmt_abort(plugin_info->estate, stmts[j], plugin_info->fextra); } } } } } PG_FINALLY(); { /* return back err_stmt */ plugin_info->estate->err_stmt = err_stmt; } PG_END_TRY(); } /* * This callback function is called when MemoryContext used for * function execution state is released. It is called after any * type of function ending - end or abort. We don't want to call * func_abort after correct ending. The flag is plugin_info->fextra. * When is released already, then function was correctly ended. */ static void plugin_info_reset(void *arg) { plpgsql_plugin_info *plugin_info = (plpgsql_plugin_info *) arg; PLpgSQL_execstate loc_estate; PLpgSQL_execstate *old_cur_estate; MemoryContext exec_mcxt = CurrentMemoryContext; int stmts_stack_size; int i; /* The memory should not be corrupted! */ Assert(plugin_info->magic == PLUGIN_INFO_MAGIC); /* * PostgreSQL 19 can remove this callback. But we need to support previous * releases, so when fextra is already released, then do just nothing * here. */ if (!plugin_info->fextra) { if (top_pldbgapi_plugin_info == plugin_info) top_pldbgapi_plugin_info = plugin_info->prev_pldbgapi_plugin_info; return; } /* * When current plugin_info is not not top plugin_info, then memory is * released in unexpected order. But we can fix it, by explicit call of * plugin_info_reset, so call all plugin_info_reset for all nested * executed functions. */ while (top_pldbgapi_plugin_info && top_pldbgapi_plugin_info != plugin_info) { plugin_info_reset(top_pldbgapi_plugin_info); } /* * In this moment, the estate content can be corrupted, because estate is * from stack of already ended function. * * Inside abort methods, the estate fields should not be referenced! */ memset(&loc_estate, 0, sizeof(PLpgSQL_execstate)); Assert(plugin_info->fextra->use_count > 0); Assert(plch_use_count(plugin_info->fextra->func) > 0); loc_estate.func = plugin_info->fextra->func; loc_estate.cur_error = plugin_info->cur_error; old_cur_estate = loc_estate.func->cur_estate; loc_estate.func->cur_estate = &loc_estate; plugin_info->estate = &loc_estate; stmts_stack_size = plugin_info->stmts_stack_size; plugin_info->stmts_stack_size = 0; PG_TRY(); { abort_statements(plugin_info->stmts_stack, stmts_stack_size, plugin_info, true); for (i = 0; i < nplugins; i++) { if (plugin_info->is_active[i] && plugins[i]->func_abort) { plugin_info->estate->plugin_info = plugin_info->plugin_info[i]; MemoryContextSwitchTo(exec_mcxt); plugins[i]->func_abort(plugin_info->estate, plugin_info->estate->func, plugin_info->fextra); } } } PG_FINALLY(); { plugin_info->fextra->func->cur_estate = old_cur_estate; plch_release_fextra(plugin_info->fextra); plugin_info->fextra = NULL; plugin_info->estate = NULL; top_pldbgapi_plugin_info = plugin_info->prev_pldbgapi_plugin_info; } PG_END_TRY(); } /* * for all active plugins and prev_plpgsql_plugins calls func_setup, * and when some plugin is active, prepares fextra. */ static void func_setup(PLpgSQL_execstate *estate, PLpgSQL_function *func) { plpgsql_plugin_info *plugin_info = palloc0(sizeof(plpgsql_plugin_info)); MemoryContext setup_mcxt = CurrentMemoryContext; int i; plugin_info->magic = PLUGIN_INFO_MAGIC; plugin_info->fn_oid = func->fn_oid; plugin_info->estate = estate; plugin_info->mcxt = CurrentMemoryContext; plugin_info->ErrorContext = AllocSetContextCreate(plugin_info->mcxt, "ErrorContext", 8 * 1024, 8 * 1024, 8 * 1024); plugin_info->prev_pldbgapi_plugin_info = top_pldbgapi_plugin_info; top_pldbgapi_plugin_info = plugin_info; for (i = 0; i < nplugins; i++) { if (plugins[i]->is_active(estate, func)) { plugin_info->is_active[i] = true; if (!plugin_info->fextra) plugin_info->fextra = plch_get_fextra(func); /* * The size of stmt_stack is constant. We know max deep of AST of * plpgsql function. The one stack is per one function call. When * function call self recursively, then new stack is created. * Because AST is tree always - and the PLpgSQL is based on * iteration over AST, then there is not the possibility how the * execution can across max_deep size. * * After an exception pldbgapi reduces stack how it is necessary, * so under pldbgapi - the number of calls of stmt_beg should be * same like number of calls of stmt_end (because stmt_end is * called after any abort before an execution of any statement * from same function). */ plugin_info->stmts_stack = palloc((plugin_info->fextra->max_deep + 1) * sizeof(PLpgSQL_stmt *)); plugin_info->stmts_buf = palloc((plugin_info->fextra->max_deep + 1) * sizeof(PLpgSQL_stmt *)); } else plugin_info->is_active[i] = false; plugins[i]->error_callback = plpgsql_plugin.error_callback; plugins[i]->assign_expr = plpgsql_plugin.assign_expr; #if PG_VERSION_NUM >= 150000 plugins[i]->assign_value = plpgsql_plugin.assign_value; plugins[i]->eval_datum = plpgsql_plugin.eval_datum; plugins[i]->cast_value = plpgsql_plugin.cast_value; #else plugins[i]->assign_value = NULL; plugins[i]->eval_datum = NULL; plugins[i]->cast_value = NULL; #endif plugins[i]->plch_error_callback = plpgsql_check_error_callback; } plugin_info->mcb.func = plugin_info_reset; plugin_info->mcb.arg = plugin_info; /* * Unfortunately, the caller context of inline block is released in * unstable order (after an exception). The other memory is cleaned * explicitly in plpgsql_inline_handler. * * The workaround - using context from simple_eval_estate is not nice, * because it enforce plugin_reset before final raising an exception. * inline block hash not use shared simple eval estate, and every inline * block has unique simple eval estate. * * The advantage of using es_query_cxt context instead caller context is a * fact, so this context is destroyed before related PLpgSQL_function is * released (including AST). Caller context is released too late, and * access to AST is broken due access to already released memory. */ if (!OidIsValid(func->fn_oid)) { MemoryContextRegisterResetCallback(estate->simple_eval_estate->es_query_cxt, &plugin_info->mcb); } else { MemoryContextRegisterResetCallback(CurrentMemoryContext, &plugin_info->mcb); } if (prev_plpgsql_plugin) { prev_plpgsql_plugin->error_callback = plpgsql_plugin.error_callback; prev_plpgsql_plugin->assign_expr = plpgsql_plugin.assign_expr; #if PG_VERSION_NUM >= 150000 prev_plpgsql_plugin->assign_value = plpgsql_plugin.assign_value; prev_plpgsql_plugin->eval_datum = plpgsql_plugin.eval_datum; prev_plpgsql_plugin->cast_value = plpgsql_plugin.cast_value; #endif } /* try to call setup function for all plugins */ PG_TRY(); { for (i = 0; i < nplugins; i++) { if (plugin_info->is_active[i] && plugins[i]->func_setup) { estate->plugin_info = NULL; MemoryContextSwitchTo(setup_mcxt); plugins[i]->func_setup(estate, func, plugin_info->fextra); plugin_info->plugin_info[i] = estate->plugin_info; } } if (prev_plpgsql_plugin && prev_plpgsql_plugin->func_setup) { estate->plugin_info = NULL; MemoryContextSwitchTo(setup_mcxt); (prev_plpgsql_plugin->func_setup) (estate, func); plugin_info->prev_plugin_info = estate->plugin_info; } } PG_FINALLY(); { estate->plugin_info = plugin_info; } PG_END_TRY(); } /* * Every errfinish collects stacktrace. So this routine * should be executed immediately before raising a error. * * Do not raise any error, notifications or warnings in this * routine, and nested routines!!! */ static void plpgsql_check_error_callback(void *arg) { plpgsql_plugin_info *plugin_info = (plpgsql_plugin_info *) arg; /* * Attention - the callback can be raised from pl plugins, and then * estate->plugin_info should not be a reference to pldbapi3 plugin. */ if (top_pldbgapi_plugin_info == plugin_info && geterrcode() != 0) { /* * Use preallocated 8kB private error context. Then CopyErrorData() * should be safe operation. When some plugin needs more memory, then * should to use own preallocated own ErrorContext. */ MemoryContext oldcxt = MemoryContextSwitchTo(plugin_info->ErrorContext); if (plugin_info->cur_error) FreeErrorData(plugin_info->cur_error); plugin_info->cur_error = CopyErrorData(); plugin_info->err_stmt = plugin_info->estate->err_stmt; MemoryContextSwitchTo(oldcxt); Assert(plugin_info->magic == PLUGIN_INFO_MAGIC); if (plugin_info->fextra) { PLpgSQL_execstate *estate = plugin_info->estate; PLpgSQL_stmt *err_stmt = estate->err_stmt; void *old_plugin_info = estate->plugin_info; int i; PG_TRY(); { for (i = 0; i < nplugins; i++) { if (plugin_info->is_active[i] && plugins[i]->stmt_error) { estate->plugin_info = plugin_info->plugin_info[i]; MemoryContextSwitchTo(plugin_info->ErrorContext); plugins[i]->stmt_error(estate, err_stmt, plugin_info->fextra); plugin_info->plugin_info[i] = estate->plugin_info; } } } PG_FINALLY(); { estate->plugin_info = old_plugin_info; MemoryContextSwitchTo(oldcxt); } PG_END_TRY(); } MemoryContextSwitchTo(oldcxt); } } /* * for all active plugins and prev_plpgsql_plugins calls func_beg */ static void func_beg(PLpgSQL_execstate *estate, PLpgSQL_function *func) { plpgsql_plugin_info *plugin_info = estate->plugin_info; MemoryContext exec_mcxt = CurrentMemoryContext; int i; if (!plugin_info || plugin_info->magic != PLUGIN_INFO_MAGIC) return; Assert(plugin_info->estate == estate); Assert(plugin_info->fn_oid == func->fn_oid); plugin_info->plerrcontext.callback = plpgsql_check_error_callback; plugin_info->plerrcontext.arg = plugin_info; plugin_info->plerrcontext.previous = error_context_stack; error_context_stack = &plugin_info->plerrcontext; PG_TRY(); { if (plugin_info->fextra) { Assert(plugin_info->fextra->fn_oid == plugin_info->fn_oid); for (i = 0; i < nplugins; i++) { if (plugin_info->is_active[i] && plugins[i]->func_beg) { estate->plugin_info = plugin_info->plugin_info[i]; plugins[i]->func_beg(estate, func, plugin_info->fextra); plugin_info->plugin_info[i] = estate->plugin_info; /* force original memory context */ MemoryContextSwitchTo(exec_mcxt); } } } if (prev_plpgsql_plugin && prev_plpgsql_plugin->func_beg) { estate->plugin_info = plugin_info->prev_plugin_info; (prev_plpgsql_plugin->func_beg) (estate, func); plugin_info->prev_plugin_info = estate->plugin_info; } } PG_FINALLY(); { estate->plugin_info = plugin_info; } PG_END_TRY(); } /* * for all active plugins and prev_plpgsql_plugins calls func_end */ static void func_end(PLpgSQL_execstate *estate, PLpgSQL_function *func) { plpgsql_plugin_info *plugin_info = estate->plugin_info; MemoryContext exec_mcxt = CurrentMemoryContext; int i; if (!plugin_info || plugin_info->magic != PLUGIN_INFO_MAGIC) return; Assert(plugin_info->estate == estate); Assert(plugin_info->fn_oid == func->fn_oid); PG_TRY(); { if (plugin_info->fextra) { int naborted_stmts = plugin_info->stmts_stack_size; Assert(plugin_info->fextra->fn_oid == plugin_info->fn_oid); plugin_info->stmts_stack_size = 0; abort_statements(plugin_info->stmts_stack, naborted_stmts, plugin_info, true); for (i = 0; i < nplugins; i++) { if (plugin_info->is_active[i] && plugins[i]->func_end) { estate->plugin_info = plugin_info->plugin_info[i]; MemoryContextSwitchTo(exec_mcxt); plugins[i]->func_end(estate, func, plugin_info->fextra); plugin_info->plugin_info[i] = estate->plugin_info; } } } if (prev_plpgsql_plugin && prev_plpgsql_plugin->func_end) { estate->plugin_info = plugin_info->prev_plugin_info; MemoryContextSwitchTo(exec_mcxt); (prev_plpgsql_plugin->func_end) (estate, func); plugin_info->prev_plugin_info = estate->plugin_info; } } PG_FINALLY(); { estate->plugin_info = plugin_info; if (plugin_info->fextra) { plch_release_fextra(plugin_info->fextra); plugin_info->fextra = NULL; } top_pldbgapi_plugin_info = plugin_info->prev_pldbgapi_plugin_info; } PG_END_TRY(); } /* * for all active plugins and prev_plpgsql_plugins calls stmt_beg */ static void stmt_beg(PLpgSQL_execstate *estate, PLpgSQL_stmt *stmt) { plpgsql_plugin_info *plugin_info = estate->plugin_info; MemoryContext exec_mcxt = CurrentMemoryContext; int naborted_stmts = 0; int i; if (!plugin_info || plugin_info->magic != PLUGIN_INFO_MAGIC) return; Assert(plugin_info->estate == estate); Assert(plugin_info->fn_oid == estate->func->fn_oid); /* * When current plugin_info is not not top plugin_info, then memory is * released in unexpected order. But we can fix it, by explicit call of * plugin_info_reset, so call all plugin_info_reset for all nested * executed functions. */ while (top_pldbgapi_plugin_info && top_pldbgapi_plugin_info != plugin_info) { plugin_info_reset(top_pldbgapi_plugin_info); } if (plugin_info->fextra) { if (estate->cur_error) { /* * Only inside error handler we need reduce statements from * stacks, because stmt_end was skipped due some exception. All * statements until parent of current statements should be closed. */ int cur_parentid = plugin_info->fextra->parentids[stmt->stmtid]; while (plugin_info->stmts_stack_size > 0 && plugin_info->stmts_stack[plugin_info->stmts_stack_size - 1]->stmtid != cur_parentid) { plugin_info->stmts_buf[naborted_stmts++] = plugin_info->stmts_stack[plugin_info->stmts_stack_size - 1]; plugin_info->stmts_stack_size--; } } plugin_info->stmts_stack[plugin_info->stmts_stack_size++] = stmt; } PG_TRY(); { if (plugin_info->fextra) { Assert(plugin_info->fextra->fn_oid == plugin_info->fn_oid); abort_statements(plugin_info->stmts_buf, naborted_stmts, plugin_info, false); for (i = 0; i < nplugins; i++) { if (plugin_info->is_active[i] && plugins[i]->stmt_beg) { estate->plugin_info = plugin_info->plugin_info[i]; MemoryContextSwitchTo(exec_mcxt); plugins[i]->stmt_beg(estate, stmt, plugin_info->fextra); plugin_info->plugin_info[i] = estate->plugin_info; } } } if (prev_plpgsql_plugin && prev_plpgsql_plugin->stmt_beg) { estate->plugin_info = plugin_info->prev_plugin_info; MemoryContextSwitchTo(exec_mcxt); (prev_plpgsql_plugin->stmt_beg) (estate, stmt); plugin_info->prev_plugin_info = estate->plugin_info; } } PG_FINALLY(); { estate->plugin_info = plugin_info; } PG_END_TRY(); } /* * for all active plugins and prev_plpgsql_plugins calls stmt_end. */ static void stmt_end(PLpgSQL_execstate *estate, PLpgSQL_stmt *stmt) { plpgsql_plugin_info *plugin_info = estate->plugin_info; MemoryContext exec_mcxt = CurrentMemoryContext; int naborted_stmts = 0; int i; if (!plugin_info || plugin_info->magic != PLUGIN_INFO_MAGIC) return; Assert(plugin_info->estate == estate); Assert(plugin_info->fn_oid == estate->func->fn_oid); if (plugin_info->fextra) { Assert(plugin_info->stmts_stack_size > 0); /* * After NULL exception handler, we need to close statements in * stmt_end */ while (plugin_info->stmts_stack_size > 0 && stmt->stmtid != plugin_info->stmts_stack[plugin_info->stmts_stack_size - 1]->stmtid) { plugin_info->stmts_buf[naborted_stmts++] = plugin_info->stmts_stack[plugin_info->stmts_stack_size - 1]; plugin_info->stmts_stack_size--; } Assert(plugin_info->stmts_stack_size > 0); Assert(plugin_info->stmts_stack[plugin_info->stmts_stack_size - 1]->stmtid == stmt->stmtid); plugin_info->stmts_stack_size--; } PG_TRY(); { if (plugin_info->fextra) { Assert(plugin_info->fextra->fn_oid == plugin_info->fn_oid); abort_statements(plugin_info->stmts_buf, naborted_stmts, plugin_info, false); for (i = 0; i < nplugins; i++) { if (plugin_info->is_active[i] && plugins[i]->stmt_end) { estate->plugin_info = plugin_info->plugin_info[i]; MemoryContextSwitchTo(exec_mcxt); plugins[i]->stmt_end(estate, stmt, plugin_info->fextra); plugin_info->plugin_info[i] = estate->plugin_info; } } } if (prev_plpgsql_plugin && prev_plpgsql_plugin->stmt_end) { estate->plugin_info = plugin_info->prev_plugin_info; MemoryContextSwitchTo(exec_mcxt); (prev_plpgsql_plugin->stmt_end) (estate, stmt); plugin_info->prev_plugin_info = estate->plugin_info; } } PG_FINALLY(); { estate->plugin_info = plugin_info; } PG_END_TRY(); } void plch_register_plugin(plch_plugin *plugin) { if (nplugins < MAX_PLUGINS) plugins[nplugins++] = plugin; else elog(ERROR, "too much plpgsql_check pl debug API plugins"); } void plch_init_plugin(void) { PLpgSQL_plugin **plugin_ptr; static bool inited = false; if (inited) return; plugin_ptr = (PLpgSQL_plugin **) find_rendezvous_variable("PLpgSQL_plugin"); prev_plpgsql_plugin = *plugin_ptr; *plugin_ptr = &plpgsql_plugin; inited = true; } #if PG_VERSION_NUM < 150000 void plch_finish_plugin(void) { PLpgSQL_plugin **plugin_ptr; plugin_ptr = (PLpgSQL_plugin **) find_rendezvous_variable("PLpgSQL_plugin"); *plugin_ptr = prev_plpgsql_plugin; } #endif okbob-plpgsql_check-800db79/src/plpgsql_check.c000066400000000000000000000371601526744060500215540ustar00rootroot00000000000000/*------------------------------------------------------------------------- * * plpgsql_check.c * * enhanced checks for plpgsql functions * * by Pavel Stehule 2013-2026 * *------------------------------------------------------------------------- * * Notes: * * 1) Secondary hash table for function signature is necessary due holding is_checked * attribute - this protection against unwanted repeated check. * * 2) Reusing some plpgsql_xxx functions requires full run-time environment. It is * emulated by fake expression context and fake fceinfo (these are created when * active checking is used) - see: setup_fake_fcinfo, setup_cstate. * * 3) The environment is referenced by stored execution plans. The actual plan should * not be linked with fake environment. All expressions created in checking time * should be relased by release_exprs(cstate.exprs) function. * */ #include "plpgsql_check.h" #include "plpgsql_check_builtins.h" #include "catalog/dependency.h" #include "catalog/pg_proc.h" #include "commands/extension.h" #include "storage/lwlock.h" #include "storage/shmem.h" #include "utils/guc.h" #include "utils/memutils.h" #if PG_VERSION_NUM >= 180000 #include "utils/inval.h" #include "utils/syscache.h" #endif #ifdef PG_MODULE_MAGIC_EXT PG_MODULE_MAGIC_EXT( .name = "plpgsql_check", .version = "2.10.16" ); #else PG_MODULE_MAGIC; #endif static const struct config_enum_entry plpgsql_check_mode_options[] = { {"disabled", PLPGSQL_CHECK_MODE_DISABLED, false}, {"by_function", PLPGSQL_CHECK_MODE_BY_FUNCTION, false}, {"fresh_start", PLPGSQL_CHECK_MODE_FRESH_START, false}, {"every_start", PLPGSQL_CHECK_MODE_EVERY_START, false}, {NULL, 0, false} }; static const struct config_enum_entry tracer_verbosity_options[] = { {"terse", PGERROR_TERSE, false}, {"default", PGERROR_DEFAULT, false}, {"verbose", PGERROR_VERBOSE, false}, {NULL, 0, false} }; static const struct config_enum_entry tracer_level_options[] = { {"debug5", DEBUG5, false}, {"debug4", DEBUG4, false}, {"debug3", DEBUG3, false}, {"debug2", DEBUG2, false}, {"debug1", DEBUG1, false}, {"debug", DEBUG2, true}, {"info", INFO, false}, {"notice", NOTICE, false}, {"log", LOG, false}, {NULL, 0, false} }; static const struct config_enum_entry cursors_leaks_level_options[] = { {"notice", NOTICE, false}, {"WARNING", WARNING, false}, {"ERROR", ERROR, false}, {NULL, 0, false} }; void _PG_init(void); #if PG_VERSION_NUM < 150000 void _PG_fini(void); #endif #if PG_VERSION_NUM >= 150000 shmem_request_hook_type plpgsql_check_prev_shmem_request_hook = NULL; #endif shmem_startup_hook_type plpgsql_check_prev_shmem_startup_hook = NULL; bool plpgsql_check_regress_test_mode; /* * Links to function in plpgsql module */ plpgsql_check__build_datatype_t plpgsql_check__build_datatype_p; plpgsql_check__compile_t plpgsql_check__compile_p; plpgsql_check__parser_setup_t plpgsql_check__parser_setup_p; plpgsql_check__stmt_typename_t plpgsql_check__stmt_typename_p; plpgsql_check__exec_get_datum_type_t plpgsql_check__exec_get_datum_type_p; plpgsql_check__recognize_err_condition_t plpgsql_check__recognize_err_condition_p; plpgsql_check__ns_lookup_t plpgsql_check__ns_lookup_p; static bool is_expected_extversion = false; #if PG_VERSION_NUM >= 180000 static void pg_extension_cache_callback(Datum arg, int cacheid, uint32 hashvalue) { is_expected_extversion = false; } #endif /* * load_external_function retursn PGFunctions - we need generic function, so * it is not 100% correct, but in used context it is not a problem. */ #define LOAD_EXTERNAL_FUNCTION(file, funcname) ((void *) (load_external_function(file, funcname, true, NULL))) #define EXPECTED_EXTVERSION "2.10" void plpgsql_check_check_ext_version(Oid fn_oid) { if (!is_expected_extversion) { Oid extoid; char *extver; extoid = getExtensionOfObject(ProcedureRelationId, fn_oid); Assert(OidIsValid(extoid)); #if PG_VERSION_NUM >= 180000 extver = get_extension_version2(extoid); #else extver = get_extension_version(extoid); #endif Assert(extver); if (strcmp(EXPECTED_EXTVERSION, extver) != 0) { char *extname = get_extension_name(extoid); ereport(ERROR, (errmsg("extension \"%s\" is not updated in system catalog", extname), errdetail("version \"%s\" is required, version \"%s\" is installed", EXPECTED_EXTVERSION, extver), errhint("execute \"ALTER EXTENSION %s UPDATE TO '%s'\"", extname, EXPECTED_EXTVERSION))); } else { pfree(extver); is_expected_extversion = true; } } } #if PG_VERSION_NUM >= 190000 StaticAssertVariableIsOfType(&plpgsql_build_datatype, plpgsql_check__build_datatype_t); StaticAssertVariableIsOfType(&plpgsql_compile, plpgsql_check__compile_t); StaticAssertVariableIsOfType(&plpgsql_parser_setup, plpgsql_check__parser_setup_t); StaticAssertVariableIsOfType(&plpgsql_stmt_typename, plpgsql_check__stmt_typename_t); StaticAssertVariableIsOfType(&plpgsql_exec_get_datum_type, plpgsql_check__exec_get_datum_type_t); StaticAssertVariableIsOfType(&plpgsql_recognize_err_condition, plpgsql_check__recognize_err_condition_t); StaticAssertVariableIsOfType(&plpgsql_ns_lookup, plpgsql_check__ns_lookup_t); #endif /* * Module initialization * * join to PLpgSQL executor * */ void _PG_init(void) { /* Be sure we do initialization only once (should be redundant now) */ static bool inited = false; if (inited) return; pg_bindtextdomain(TEXTDOMAIN); #if PG_VERSION_NUM >= 190000 plpgsql_check__build_datatype_p = (plpgsql_check__build_datatype_t) LOAD_EXTERNAL_FUNCTION("$libdir/plpgsql", "plpgsql_build_datatype"); plpgsql_check__compile_p = (plpgsql_check__compile_t) LOAD_EXTERNAL_FUNCTION("$libdir/plpgsql", "plpgsql_compile"); plpgsql_check__parser_setup_p = (plpgsql_check__parser_setup_t) LOAD_EXTERNAL_FUNCTION("$libdir/plpgsql", "plpgsql_parser_setup"); plpgsql_check__stmt_typename_p = (plpgsql_check__stmt_typename_t) LOAD_EXTERNAL_FUNCTION("$libdir/plpgsql", "plpgsql_stmt_typename"); plpgsql_check__exec_get_datum_type_p = (plpgsql_check__exec_get_datum_type_t) LOAD_EXTERNAL_FUNCTION("$libdir/plpgsql", "plpgsql_exec_get_datum_type"); plpgsql_check__recognize_err_condition_p = (plpgsql_check__recognize_err_condition_t) LOAD_EXTERNAL_FUNCTION("$libdir/plpgsql", "plpgsql_recognize_err_condition"); plpgsql_check__ns_lookup_p = (plpgsql_check__ns_lookup_t) LOAD_EXTERNAL_FUNCTION("$libdir/plpgsql", "plpgsql_ns_lookup"); #else AssertVariableIsOfType(&plpgsql_build_datatype, plpgsql_check__build_datatype_t); plpgsql_check__build_datatype_p = (plpgsql_check__build_datatype_t) LOAD_EXTERNAL_FUNCTION("$libdir/plpgsql", "plpgsql_build_datatype"); AssertVariableIsOfType(&plpgsql_compile, plpgsql_check__compile_t); plpgsql_check__compile_p = (plpgsql_check__compile_t) LOAD_EXTERNAL_FUNCTION("$libdir/plpgsql", "plpgsql_compile"); AssertVariableIsOfType(&plpgsql_parser_setup, plpgsql_check__parser_setup_t); plpgsql_check__parser_setup_p = (plpgsql_check__parser_setup_t) LOAD_EXTERNAL_FUNCTION("$libdir/plpgsql", "plpgsql_parser_setup"); AssertVariableIsOfType(&plpgsql_stmt_typename, plpgsql_check__stmt_typename_t); plpgsql_check__stmt_typename_p = (plpgsql_check__stmt_typename_t) LOAD_EXTERNAL_FUNCTION("$libdir/plpgsql", "plpgsql_stmt_typename"); AssertVariableIsOfType(&plpgsql_exec_get_datum_type, plpgsql_check__exec_get_datum_type_t); plpgsql_check__exec_get_datum_type_p = (plpgsql_check__exec_get_datum_type_t) LOAD_EXTERNAL_FUNCTION("$libdir/plpgsql", "plpgsql_exec_get_datum_type"); AssertVariableIsOfType(&plpgsql_recognize_err_condition, plpgsql_check__recognize_err_condition_t); plpgsql_check__recognize_err_condition_p = (plpgsql_check__recognize_err_condition_t) LOAD_EXTERNAL_FUNCTION("$libdir/plpgsql", "plpgsql_recognize_err_condition"); AssertVariableIsOfType(&plpgsql_ns_lookup, plpgsql_check__ns_lookup_t); plpgsql_check__ns_lookup_p = (plpgsql_check__ns_lookup_t) LOAD_EXTERNAL_FUNCTION("$libdir/plpgsql", "plpgsql_ns_lookup"); #endif DefineCustomBoolVariable("plpgsql_check.regress_test_mode", "reduces volatile output", NULL, &plpgsql_check_regress_test_mode, false, PGC_USERSET, 0, NULL, NULL, NULL); DefineCustomEnumVariable("plpgsql_check.mode", "choose a mode for enhanced checking", NULL, &plpgsql_check_mode, PLPGSQL_CHECK_MODE_BY_FUNCTION, plpgsql_check_mode_options, PGC_USERSET, 0, NULL, NULL, NULL); DefineCustomBoolVariable("plpgsql_check.show_nonperformance_extra_warnings", "when is true, then extra warning (except performance warnings) are showed", NULL, &plpgsql_check_extra_warnings, false, PGC_USERSET, 0, NULL, NULL, NULL); DefineCustomBoolVariable("plpgsql_check.show_nonperformance_warnings", "when is true, then warning (except performance warnings) are showed", NULL, &plpgsql_check_other_warnings, false, PGC_USERSET, 0, NULL, NULL, NULL); DefineCustomBoolVariable("plpgsql_check.show_performance_warnings", "when is true, then performance warnings are showed", NULL, &plpgsql_check_performance_warnings, false, PGC_USERSET, 0, NULL, NULL, NULL); DefineCustomBoolVariable("plpgsql_check.compatibility_warnings", "when is true, then compatibility warnings are showed", NULL, &plpgsql_check_compatibility_warnings, false, PGC_USERSET, 0, NULL, NULL, NULL); DefineCustomBoolVariable("plpgsql_check.constants_tracing", "when is true, the variables with constant value can be used like constant", NULL, &plpgsql_check_constants_tracing, true, PGC_USERSET, 0, NULL, NULL, NULL); DefineCustomBoolVariable("plpgsql_check.fatal_errors", "when is true, then plpgsql check stops execution on detected error", NULL, &plpgsql_check_fatal_errors, true, PGC_USERSET, 0, NULL, NULL, NULL); DefineCustomBoolVariable("plpgsql_check.profiler", "when is true, then function execution profile is updated", NULL, &plpgsql_check_profiler, false, PGC_USERSET, 0, NULL, NULL, NULL); DefineCustomBoolVariable("plpgsql_check.profiler_show_dynquery_query_id", "when is true, then queryid of dynamic queries are displayed", NULL, &plch_report_dynquery_queryid, false, PGC_USERSET, 0, NULL, NULL, NULL); DefineCustomBoolVariable("plpgsql_check.enable_tracer", "when is true, then tracer's functionality is enabled", NULL, &plpgsql_check_enable_tracer, false, PGC_SUSET, 0, NULL, NULL, NULL); DefineCustomBoolVariable("plpgsql_check.tracer", "when is true, then function is traced", NULL, &plpgsql_check_tracer, false, PGC_USERSET, 0, NULL, NULL, NULL); DefineCustomBoolVariable("plpgsql_check.trace_assert", "when is true, then statement ASSERT is traced", NULL, &plpgsql_check_trace_assert, false, PGC_USERSET, 0, NULL, NULL, NULL); DefineCustomBoolVariable("plpgsql_check.tracer_test_mode", "when is true, then output of tracer is in regress test possible format", NULL, &plpgsql_check_tracer_test_mode, false, PGC_USERSET, 0, NULL, NULL, NULL); DefineCustomBoolVariable("plpgsql_check.tracer_show_nsubxids", "when is true, then the tracer shows number of current subxids", NULL, &plpgsql_check_tracer_show_nsubxids, false, PGC_USERSET, 0, NULL, NULL, NULL); DefineCustomEnumVariable("plpgsql_check.tracer_verbosity", "sets the verbosity of tracer", NULL, (int *) &plpgsql_check_tracer_verbosity, PGERROR_DEFAULT, tracer_verbosity_options, PGC_USERSET, 0, NULL, NULL, NULL); DefineCustomEnumVariable("plpgsql_check.trace_assert_verbosity", "sets the verbosity of trace ASSERT statement", NULL, (int *) &plpgsql_check_trace_assert_verbosity, PGERROR_DEFAULT, tracer_verbosity_options, PGC_USERSET, 0, NULL, NULL, NULL); DefineCustomEnumVariable("plpgsql_check.tracer_errlevel", "sets an error level of tracer's messages", NULL, (int *) &plpgsql_check_tracer_errlevel, NOTICE, tracer_level_options, PGC_USERSET, 0, NULL, NULL, NULL); DefineCustomIntVariable("plpgsql_check.tracer_variable_max_length", "Maximum output length of content of variables in bytes", NULL, &plpgsql_check_tracer_variable_max_length, 1024, 10, 2048, PGC_USERSET, 0, NULL, NULL, NULL); DefineCustomEnumVariable("plpgsql_check.cursors_leaks_errlevel", "sets an error level of detection of unclosed cursors", NULL, (int *) &plpgsql_check_cursors_leaks_level, WARNING, cursors_leaks_level_options, PGC_USERSET, 0, NULL, NULL, NULL); DefineCustomBoolVariable("plpgsql_check.cursors_leaks", "when is true, then detection of unclosed cursors is active", NULL, &plpgsql_check_cursors_leaks, true, PGC_USERSET, 0, NULL, NULL, NULL); DefineCustomBoolVariable("plpgsql_check.strict_cursors_leaks", "when is true, then detection of unclosed cursors is executed immediately when function is finished", NULL, &plpgsql_check_cursors_leaks_strict, false, PGC_USERSET, 0, NULL, NULL, NULL); DefineCustomIntVariable("plpgsql_check.max_stats_size", "Maximum used memory for profile statistics", NULL, &plch_max_stat_size, 20480, 64, 204800, PGC_USERSET, GUC_UNIT_KB, NULL, NULL, NULL); DefineCustomBoolVariable("plpgsql_check.use_shared_stats_when_it_possible", "when is true, then profiler statistics are in shared memory (only when shared memory was preallocated)", NULL, &plch_use_shared_stats_when_it_possible, true, PGC_USERSET, 0, NULL, NULL, NULL); DefineCustomBoolVariable("plpgsql_check.use_lxcache", "when is true, then profiler shared statistics are updated at transaction end", NULL, &plch_use_lxcache, true, PGC_USERSET, 0, NULL, NULL, NULL); EmitWarningsOnPlaceholders("plpgsql_check"); plpgsql_check_HashTableInit(); plch_profiler_init_local_hash_tables(); /* Use shared memory when we can register more for self */ if (process_shared_preload_libraries_in_progress) { #if PG_VERSION_NUM < 150000 /* * If you change code here, don't forget to also report the * modifications in plpgsql_check_profiler_shmem_request() for pg15 * and later. */ RequestAddinShmemSpace(plpgsql_check_shmem_size()); RequestNamedLWLockTranche("plpgsql_check profiler funcs stats", 1); RequestNamedLWLockTranche("plpgsql_check profiler func stmts stats", 1); #endif /* * Install hooks. */ #if PG_VERSION_NUM >= 150000 plpgsql_check_prev_shmem_request_hook = shmem_request_hook; shmem_request_hook = plpgsql_check_profiler_shmem_request; #endif plpgsql_check_prev_shmem_startup_hook = shmem_startup_hook; shmem_startup_hook = plpgsql_check_profiler_shmem_startup; } plch_init_plugin(); plpgsql_check_passive_check_init(); plpgsql_check_profiler_init(); plpgsql_check_tracer_init(); plpgsql_check_cursors_leaks_init(); #if PG_VERSION_NUM >= 180000 /* used for checking expected version of SQL interface (extension) */ CacheRegisterSyscacheCallback(EXTENSIONOID, pg_extension_cache_callback, (Datum) 0); #endif inited = true; } okbob-plpgsql_check-800db79/src/plpgsql_check.h000066400000000000000000000667651526744060500215760ustar00rootroot00000000000000#include "postgres.h" #include "plpgsql.h" #include "funcapi.h" #include "miscadmin.h" #include "access/tupdesc.h" #include "storage/ipc.h" #if PG_VERSION_NUM >= 190000 #include "portability/instr_time.h" #endif typedef uint64 pc_queryid; #define NOQUERYID (UINT64CONST(0)) #if PG_VERSION_NUM < 150000 #define parse_analyze_fixedparams parse_analyze #endif enum { PLPGSQL_CHECK_ERROR, PLPGSQL_CHECK_WARNING_OTHERS, PLPGSQL_CHECK_WARNING_EXTRA, /* check shadowed variables */ PLPGSQL_CHECK_WARNING_PERFORMANCE, /* invisible cast check */ PLPGSQL_CHECK_WARNING_SECURITY, /* sql injection check */ PLPGSQL_CHECK_WARNING_COMPATIBILITY /* obsolete setting of cursor's or * refcursor's variable */ }; enum { PLPGSQL_CHECK_FORMAT_INVALID = 0, PLPGSQL_CHECK_FORMAT_ELOG, PLPGSQL_CHECK_FORMAT_TEXT, PLPGSQL_CHECK_FORMAT_TABULAR, PLPGSQL_CHECK_FORMAT_XML, PLPGSQL_CHECK_FORMAT_JSON, PLPGSQL_SHOW_DEPENDENCY_FORMAT_TABULAR, PLPGSQL_SHOW_PROFILE_TABULAR, PLPGSQL_SHOW_PROFILE_STATEMENTS_TABULAR, PLPGSQL_SHOW_PROFILE_FUNCTIONS_ALL_TABULAR }; enum { PLPGSQL_CHECK_MODE_DISABLED, /* all functionality is disabled */ PLPGSQL_CHECK_MODE_BY_FUNCTION, /* checking is allowed via CHECK function * only (default) */ PLPGSQL_CHECK_MODE_FRESH_START, /* check only when function is called * first time */ PLPGSQL_CHECK_MODE_EVERY_START /* check on every start */ }; enum { PLPGSQL_CHECK_CLOSED, PLPGSQL_CHECK_CLOSED_BY_EXCEPTIONS, PLPGSQL_CHECK_POSSIBLY_CLOSED, PLPGSQL_CHECK_UNCLOSED, PLPGSQL_CHECK_UNKNOWN }; typedef enum PragmaAssertType { PLPGSQL_CHECK_PRAGMA_ASSERT_SCHEMA, PLPGSQL_CHECK_PRAGMA_ASSERT_TABLE, PLPGSQL_CHECK_PRAGMA_ASSERT_COLUMN, } PragmaAssertType; typedef struct PLpgSQL_stmt_stack_item { PLpgSQL_stmt *stmt; char *label; struct PLpgSQL_stmt_stack_item *outer; bool is_exception_handler; bool has_exit; } PLpgSQL_stmt_stack_item; typedef struct PLpgSQL_statements { struct PLpgSQL_statements *outer; Bitmapset *invalidate_strconstvars; } PLpgSQL_statements; typedef struct plpgsql_check_result_info { int format; /* produced / expected format */ Tuplestorestate *tuple_store; /* target tuple store */ TupleDesc tupdesc; /* target tuple store tuple descriptor */ MemoryContext query_ctx; /* memory context for string operations */ StringInfo sinfo; /* buffer for multi line one value output * formats */ bool init_tag; /* true, when init tag should be created */ bool is_first_item; /* true, when first error is printed */ } plpgsql_check_result_info; typedef struct plpgsql_check_info { HeapTuple proctuple; bool is_procedure; Oid fn_oid; Oid rettype; char volatility; Oid relid; Oid anyelementoid; Oid anyenumoid; Oid anyrangeoid; Oid anycompatibleoid; Oid anycompatiblerangeoid; PLpgSQL_trigtype trigtype; char *src; bool fatal_errors; bool other_warnings; bool performance_warnings; bool extra_warnings; bool security_warnings; bool compatibility_warnings; bool constants_tracing; bool show_profile; bool all_warnings; bool without_warnings; char *oldtable; char *newtable; bool incomment_options_usage_warning; List *pragmas; /* list of pragma strings applied before check */ bool make_pragma; /* true, when table pragmas should be * generated instead of function check */ } plpgsql_check_info; typedef struct plpgsql_check_pragma_vector { unsigned int disable_check:1; unsigned int disable_tracer:1; /* has not any effect - it's runtime */ unsigned int disable_other_warnings:1; unsigned int disable_performance_warnings:1; unsigned int disable_extra_warnings:1; unsigned int disable_security_warnings:1; unsigned int disable_compatibility_warnings:1; unsigned int disable_constants_tracing:1; } plpgsql_check_pragma_vector; #define CI_MAGIC 2023042922 typedef struct PLpgSQL_checkstate { int ci_magic; List *argnames; /* function arg names */ char decl_volatility; /* declared function volatility */ char volatility; /* detected function volatility */ bool found_unknown_query; /* detected real dynamic SQL, disable * volatility check */ bool skip_volatility_check; /* don't do this test for trigger */ PLpgSQL_execstate *estate; /* check state is estate extension */ MemoryContext check_cxt; List *exprs; /* list of all expression created by checker */ bool is_active_mode; /* true, when checking is started by * plpgsql_check_function */ Bitmapset *used_variables; /* track which variables have been used; bit * per varno */ Bitmapset *modif_variables; /* track which variables had been changed; * bit per varno */ PLpgSQL_stmt_stack_item *top_stmt_stack; /* list of known labels + * related command */ bool found_return_query; /* true, when code contains RETURN query */ bool found_return_dyn_query; /* true, when code contains RETURN * QUERY EXECUTE */ Bitmapset *func_oids; /* list of used (and displayed) functions */ Bitmapset *rel_oids; /* list of used (and displayed) relations */ bool fake_rtd; /* true when functions returns record */ plpgsql_check_result_info *result_info; plpgsql_check_info *cinfo; Bitmapset *safe_variables; /* track which variables are safe against sql * injection */ Bitmapset *out_variables; /* what variables are used as OUT variables */ Bitmapset *protected_variables; /* what variables should be assigned * internal only */ Bitmapset *auto_variables; /* variables initialized, used by runtime */ Bitmapset *typed_variables; /* record variables with assigned type by * pragma TYPE */ bool stop_check; /* true after error when fatal_errors option * is active */ bool allow_mp; /* true, when multiple plans in plancache are * allowed */ bool found_mp; /* true, when multiple plan was used, disable * useless dynsql query warning */ bool was_pragma; /* true, when last expression was a * plpgsql_check pragma */ plpgsql_check_pragma_vector pragma_vector; Oid pragma_foid; /* oid of plpgsql_check pragma function */ char **strconstvars; /* the values of string variables where the * value is constant */ PLpgSQL_statements *top_stmts; /* pointer to current statement group */ /* * is_dynsql is true if currently processed query is executed by EXECUTE * command. In this case, we cannot to derive estate variable from Param * nodes. In this case, the Param node reference is a number of expr in * USING list. */ bool is_dynsql; /* * Holds a pointer to OPEN statement, that was used for specific dynamic * cursor. This allow to assign result type to target variable in FETCH * statement when referenced cursor is dynamic. When referenced cursor is * static, then all necessary info are in cursor variable. */ PLpgSQL_stmt_open **dyn_stmt_open; } PLpgSQL_checkstate; /* * function from assign.c */ extern void plpgsql_check_record_variable_usage(PLpgSQL_checkstate *cstate, int dno, bool write); extern void plpgsql_check_row_or_rec(PLpgSQL_checkstate *cstate, PLpgSQL_row *row, PLpgSQL_rec *rec); extern void plpgsql_check_target(PLpgSQL_checkstate *cstate, int varno, Oid *expected_typoid, int *expected_typmod); extern void plpgsql_check_assign_to_target_type(PLpgSQL_checkstate *cstate, Oid target_typoid, int32 target_typmod, Oid value_typoid, bool isnull, int target_dno); extern void plpgsql_check_assign_tupdesc_dno(PLpgSQL_checkstate *cstate, int varno, TupleDesc tupdesc, bool isnull); extern void plpgsql_check_assign_tupdesc_row_or_rec(PLpgSQL_checkstate *cstate, PLpgSQL_row *row, PLpgSQL_rec *rec, TupleDesc tupdesc, bool isnull); extern void plpgsql_check_recval_assign_tupdesc(PLpgSQL_checkstate *cstate, PLpgSQL_rec *rec, TupleDesc tupdesc, bool is_null); extern void plpgsql_check_recval_init(PLpgSQL_rec *rec); extern void plpgsql_check_recval_release(PLpgSQL_rec *rec); extern void plpgsql_check_is_assignable(PLpgSQL_execstate *estate, int dno); /* * functions from format.c */ extern int plpgsql_check_format_num(char *format_str); extern void plpgsql_check_init_ri(plpgsql_check_result_info *ri, int format, ReturnSetInfo *rsinfo); extern void plpgsql_check_finalize_ri(plpgsql_check_result_info *ri); extern void plpgsql_check_put_error(PLpgSQL_checkstate *cstate, int sqlerrcode, int lineno, const char *message, const char *detail, const char *hint, int level, int position, const char *query, const char *context); extern void plpgsql_check_put_error_edata(PLpgSQL_checkstate *cstate, ErrorData *edata); extern void plch_put_text_line(plpgsql_check_result_info *ri, const char *message, int len); extern void plpgsql_check_put_dependency(plpgsql_check_result_info *ri, char *type, Oid oid, char *schema, char *name, char *params); extern void plpgsql_check_put_profile(plpgsql_check_result_info *ri, Datum queryids_array, int lineno, int stmt_lineno, int cmds_on_row, Datum exec_count_array, Datum exec_count_err_array, Datum total_time_array, Datum avg_time_array, Datum max_time_array, Datum processed_rows_array, char *source_row); extern void plpgsql_check_put_profile_statement(plpgsql_check_result_info *ri, pc_queryid queryid, int stmtid, int parent_stmtid, int block_num, int lineno, int64 exec_stmts, int64 exec_count_err, double total_time, double max_time, int64 processed_rows, const char *stmtname); extern void plpgsql_check_put_profile_statement_no_stats(plpgsql_check_result_info *ri, int stmtid, int parent_stmtid, int block_num, int lineno, const char *stmtname); extern void plpgsql_check_put_profiler_functions_all_tb(plpgsql_check_result_info *ri, Oid funcoid, int64 exec_count, int64 exec_count_err, double total_time, double avg_time, double stddev_time, double min_time, double max_time); extern char *plpgsql_check_prepare_err_text_with_target_vardecl(PLpgSQL_checkstate *cstate, PLpgSQL_stmt *stmt, int varno); /* * function from catalog.c */ extern bool plpgsql_check_is_eventtriggeroid(Oid typoid); extern void plpgsql_check_get_function_info(plpgsql_check_info *cinfo); extern void plpgsql_check_precheck_conditions(plpgsql_check_info *cinfo); extern char *plpgsql_check_get_src(HeapTuple procTuple); extern Oid plpgsql_check_pragma_func_oid(void); extern bool plpgsql_check_is_plpgsql_function(Oid foid); extern Oid plpgsql_check_get_op_namespace(Oid opno); #if PG_VERSION_NUM < 180000 extern char *get_extension_version(Oid ext_oid); #else extern char *get_extension_version2(Oid ext_oid); #endif /* * functions from tablefunc.c */ extern void plpgsql_check_info_init(plpgsql_check_info *cinfo, Oid fn_oid); extern void plpgsql_check_set_all_warnings(plpgsql_check_info *cinfo); extern void plpgsql_check_set_without_warnings(plpgsql_check_info *cinfo); /* * functions from check_function.c */ extern void plpgsql_check_function_internal(plpgsql_check_result_info *ri, plpgsql_check_info *cinfo); extern void plpgsql_check_on_func_beg(PLpgSQL_execstate *estate, PLpgSQL_function *func); extern void plpgsql_check_HashTableInit(void); extern bool plpgsql_check_is_checked(PLpgSQL_function *func); extern void plpgsql_check_mark_as_checked(PLpgSQL_function *func); extern void plpgsql_check_setup_fcinfo(plpgsql_check_info *cinfo, FmgrInfo *flinfo, FunctionCallInfo fcinfo, ReturnSetInfo *rsinfo, TriggerData *trigdata, EventTriggerData *etrigdata, Trigger *tg_trigger, bool *fake_rtd); extern bool plpgsql_check_other_warnings; extern bool plpgsql_check_extra_warnings; extern bool plpgsql_check_performance_warnings; extern bool plpgsql_check_compatibility_warnings; extern bool plpgsql_check_fatal_errors; extern bool plpgsql_check_constants_tracing; extern int plpgsql_check_mode; /* * functions from expr_walk.c */ extern void plpgsql_check_detect_dependency(PLpgSQL_checkstate *cstate, Query *query); extern bool plpgsql_check_has_rtable(Query *query); extern bool plpgsql_check_qual_has_fishy_cast(PlannedStmt *plannedstmt, Plan *plan, Param **param); extern void plpgsql_check_funcexpr(PLpgSQL_checkstate *cstate, Query *query, char *query_str); extern bool plpgsql_check_is_sql_injection_vulnerable(PLpgSQL_checkstate *cstate, PLpgSQL_expr *expr, Node *node, int *location); extern bool plpgsql_check_contain_volatile_functions(Node *clause, PLpgSQL_checkstate *cstate); extern bool plpgsql_check_contain_mutable_functions(Node *clause, PLpgSQL_checkstate *cstate); extern bool plpgsql_check_vardno_is_used_for_reading(Node *node, int dno); extern char *plpgsql_check_get_formatted_string(PLpgSQL_checkstate *cstate, const char *fmt, FuncExpr *fexpr, bool *found_ident_placeholder, bool *found_literal_placeholder, bool *expr_is_const); /* * functions from check_expr.c */ extern char *plpgsql_check_expr_get_string(PLpgSQL_checkstate *cstate, PLpgSQL_expr *expr, int *location); extern char *plpgsql_check_get_const_string(PLpgSQL_checkstate *cstate, Node *node, int *location); extern void plpgsql_check_expr_with_scalar_type(PLpgSQL_checkstate *cstate, PLpgSQL_expr *expr, Oid expected_typoid, bool required); extern void plpgsql_check_returned_expr(PLpgSQL_checkstate *cstate, PLpgSQL_expr *expr, bool is_expression); extern void plpgsql_check_expr_as_rvalue(PLpgSQL_checkstate *cstate, PLpgSQL_expr *expr, PLpgSQL_rec *targetrec, PLpgSQL_row *targetrow, int targetdno, bool use_element_type, bool is_expression); extern void plpgsql_check_expr(PLpgSQL_checkstate *cstate, PLpgSQL_expr *expr); extern void plpgsql_check_assignment_with_possible_slices(PLpgSQL_checkstate *cstate, PLpgSQL_expr *expr, PLpgSQL_rec *targetrec, PLpgSQL_row *targetrow, int targetdno, bool use_element_type); extern void plpgsql_check_expr_as_sqlstmt_nodata(PLpgSQL_checkstate *cstate, PLpgSQL_expr *expr); extern void plpgsql_check_expr_as_sqlstmt_data(PLpgSQL_checkstate *cstate, PLpgSQL_expr *expr); extern bool plpgsql_check_expr_as_sqlstmt(PLpgSQL_checkstate *cstate, PLpgSQL_expr *expr); extern void plpgsql_check_assignment(PLpgSQL_checkstate *cstate, PLpgSQL_expr *expr, PLpgSQL_rec *targetrec, PLpgSQL_row *targetrow, int targetdno); extern void plpgsql_check_expr_generic(PLpgSQL_checkstate *cstate, PLpgSQL_expr *expr); extern void plpgsql_check_expr_generic_with_parser_setup(PLpgSQL_checkstate *cstate, PLpgSQL_expr *expr, ParserSetupHook parser_setup, void *arg); extern void plpgsql_check_returned_expr_with_parser_setup(PLpgSQL_checkstate *cstate, PLpgSQL_expr *expr, bool is_expression, ParserSetupHook parser_setup, void *arg); extern void plpgsql_check_expr_as_rvalue_with_parser_setup(PLpgSQL_checkstate *cstate, PLpgSQL_expr *expr, PLpgSQL_rec *targetrec, PLpgSQL_row *targetrow, int targetdno, bool use_element_type, bool is_expression, ParserSetupHook parser_setup, void *arg); extern Node *plpgsql_check_expr_get_node(PLpgSQL_checkstate *cstate, PLpgSQL_expr *expr, bool force_plan_checks); extern char *plpgsql_check_const_to_string(Node *node, int *location); extern CachedPlanSource *plpgsql_check_get_plan_source(PLpgSQL_checkstate *cstate, SPIPlanPtr plan); extern void plch_expr_prepare_plan(PLpgSQL_checkstate *cstate, PLpgSQL_expr *expr); extern bool plch_apply_inline_pragmas(PLpgSQL_checkstate *cstate, SelectStmt *selectStmt, PLpgSQL_nsitem *ns, int lineno, bool is_perform_stmt); extern void plpgsql_check_assignment_to_variable_with_parser_setup(PLpgSQL_checkstate *cstate, PLpgSQL_expr *expr, PLpgSQL_variable *targetvar, int targetdno, ParserSetupHook parser_setup, void *arg); extern void plpgsql_check_assignment_to_variable(PLpgSQL_checkstate *cstate, PLpgSQL_expr *expr, PLpgSQL_variable *targetvar, int targetdno); /* * functions from report.c */ extern char *plpgsql_check_datum_get_refname(PLpgSQL_checkstate *cstate, PLpgSQL_datum *d); extern void plpgsql_check_report_unused_variables(PLpgSQL_checkstate *cstate); extern void plpgsql_check_report_too_high_volatility(PLpgSQL_checkstate *cstate); extern bool plpgsql_check_is_internal_variable(PLpgSQL_checkstate *cstate, PLpgSQL_variable *var); /* * functions from stmtwalk.c */ extern bool plpgsql_check_is_reserved_keyword(char *name); extern void plpgsql_check_stmt(PLpgSQL_checkstate *cstate, PLpgSQL_stmt *stmt, int *closing, List **exceptions); /* * functions from typdesc.c */ extern TupleDesc plpgsql_check_expr_get_desc(PLpgSQL_checkstate *cstate, PLpgSQL_expr *query, bool use_element_type, bool expand_record, bool is_expression, Oid *first_level_typoid); extern void plpgsql_check_recvar_info(PLpgSQL_rec *rec, Oid *typoid, int32 *typmod); extern PLpgSQL_row *plpgsql_check_CallExprGetRowTarget(PLpgSQL_checkstate *cstate, PLpgSQL_expr *CallExpr); /* * functions from parser.c */ extern Oid plpgsql_check_parse_name_or_signature(char *name_or_signature); extern bool plpgsql_check_pragma_type(PLpgSQL_checkstate *cstate, const char *str, PLpgSQL_nsitem *ns, int lineno); extern bool plpgsql_check_pragma_table(PLpgSQL_checkstate *cstate, const char *str, int lineno); extern bool plpgsql_check_pragma_sequence(PLpgSQL_checkstate *cstate, const char *str, int lineno); extern bool plpgsql_check_pragma_assert(PLpgSQL_checkstate *cstate, PragmaAssertType pat, const char *str, PLpgSQL_nsitem *ns, int lineno); extern void plpgsql_check_search_comment_options(plpgsql_check_info *cinfo); extern char *plpgsql_check_process_echo_string(char *str, plpgsql_check_info *cinfo); /* * functions from profiler.c */ extern bool plpgsql_check_profiler; extern int plpgsql_check_profiler_max_shared_chunks; extern int plch_max_stat_size; extern bool plch_use_shared_stats_when_it_possible; extern bool plch_use_lxcache; extern bool plch_report_dynquery_queryid; extern needs_fmgr_hook_type plpgsql_check_next_needs_fmgr_hook; extern fmgr_hook_type plpgsql_check_next_fmgr_hook; #if PG_VERSION_NUM >= 150000 extern void plpgsql_check_profiler_shmem_request(void); #endif extern void plpgsql_check_profiler_shmem_startup(void); extern Size plpgsql_check_shmem_size(void); extern void plch_profiler_init_local_hash_tables(void); extern void plpgsql_check_profiler_show_profile(plpgsql_check_result_info *ri, plpgsql_check_info *cinfo); extern void plpgsql_check_profiler_iterate_over_all_profiles(plpgsql_check_result_info *ri); extern void plpgsql_check_init_trace_info(PLpgSQL_execstate *estate); extern bool plpgsql_check_get_trace_info(PLpgSQL_execstate *estate, PLpgSQL_stmt *stmt, PLpgSQL_execstate **outer_estate, int *frame_num, int *level, instr_time *start_time); extern void plpgsql_check_get_trace_stmt_info(PLpgSQL_execstate *estate, int stmt_id, instr_time **start_time); extern bool *plpgsql_check_get_disable_tracer_on_stack(PLpgSQL_execstate *estate, PLpgSQL_stmt *stmt); extern void plpgsql_check_profiler_init(void); extern void plch_statements_stats_report(plpgsql_check_info *cinfo, plpgsql_check_result_info *ri); /* * functions and variables from tracer.c */ extern bool plpgsql_check_enable_tracer; extern bool plpgsql_check_tracer; extern bool plpgsql_check_trace_assert; extern bool plpgsql_check_tracer_test_mode; extern int plpgsql_check_tracer_variable_max_length; extern int plpgsql_check_tracer_errlevel; extern PGErrorVerbosity plpgsql_check_tracer_verbosity; extern PGErrorVerbosity plpgsql_check_trace_assert_verbosity; extern bool plpgsql_check_regress_test_mode; extern bool plpgsql_check_tracer_show_nsubxids; extern void plpgsql_check_trace_assert_on_stmt_beg(PLpgSQL_execstate *estate, PLpgSQL_stmt *stmt); extern void plpgsql_check_set_stmt_group_number(PLpgSQL_stmt *stmt, int *group_numbers, int *parent_group_numbers, int sgn, int *cgn, int psgn); extern void plpgsql_check_tracer_init(void); /* * variables from pragma.c */ extern void plpgsql_check_pragma_apply(PLpgSQL_checkstate *cstate, char *pragma_str, PLpgSQL_nsitem *ns, int lineno); /* * functions from pragma_generator.c */ extern void plch_make_pragma(PLpgSQL_checkstate *cstate, PLpgSQL_function *func); /* * functions and structures from fextra_cache.c */ /* * It is unique for any version of function with same oid. * It ensure strong relation between runtime data and source code * (pg_proc tuple). Because it contains db_oid, it can be used as * hash key of shared hash tabs. */ typedef struct { Oid fn_oid; Oid db_oid; TransactionId fn_xmin; ItemPointerData fn_tid; } plch_fidentity_hk; extern void plch_init_fidentity_hk(plch_fidentity_hk *hk, PLpgSQL_function *func); typedef struct { plch_fidentity_hk hk; uint32 hashValue; PLpgSQL_function *func; Oid fn_oid; char *fn_signature; char *fn_name; char *fn_namespacename; MemoryContext mcxt; int *parentids; int *naturalids; int *levels; int max_deep; int nstatements; int use_count; bool is_valid; } plch_fextra; plch_fextra *plch_get_fextra(PLpgSQL_function *func); void plch_release_fextra(plch_fextra *fextra); #if PG_VERSION_NUM < 150000 extern void plch_fextra_deinit(void); #endif /* * functions from pldbgapi3 */ typedef struct plch_plugin { /* Function pointers set up by the plugin */ bool (*is_active) (PLpgSQL_execstate *estate, PLpgSQL_function *func); void (*func_setup) (PLpgSQL_execstate *estate, PLpgSQL_function *func, plch_fextra *fextra); void (*func_beg) (PLpgSQL_execstate *estate, PLpgSQL_function *func, plch_fextra *fextra); void (*func_end) (PLpgSQL_execstate *estate, PLpgSQL_function *func, plch_fextra *fextra); void (*func_abort) (PLpgSQL_execstate *estate, PLpgSQL_function *func, plch_fextra *fextra); void (*stmt_beg) (PLpgSQL_execstate *estate, PLpgSQL_stmt *stmt, plch_fextra *fextra); void (*stmt_end) (PLpgSQL_execstate *estate, PLpgSQL_stmt *stmt, plch_fextra *fextra); void (*stmt_abort) (PLpgSQL_execstate *estate, PLpgSQL_stmt *stmt, plch_fextra *fextra); void (*stmt_error) (PLpgSQL_execstate *estate, PLpgSQL_stmt *stmt, plch_fextra *fextra); /* Function pointers set by PL/pgSQL itself */ void (*error_callback) (void *arg); void (*assign_expr) (PLpgSQL_execstate *estate, PLpgSQL_datum *target, PLpgSQL_expr *expr); void (*assign_value) (PLpgSQL_execstate *estate, PLpgSQL_datum *target, Datum value, bool isNull, Oid valtype, int32 valtypmod); void (*eval_datum) (PLpgSQL_execstate *estate, PLpgSQL_datum *datum, Oid *typeId, int32 *typetypmod, Datum *value, bool *isnull); Datum (*cast_value) (PLpgSQL_execstate *estate, Datum value, bool *isnull, Oid valtype, int32 valtypmod, Oid reqtype, int32 reqtypmod); /* Functions pointers set by plpgsql_check */ void (*plch_error_callback) (void *arg); } plch_plugin; extern void plch_init_plugin(void); extern void plch_register_plugin(plch_plugin *plugin); #if PG_VERSION_NUM < 150000 extern void plch_finish_plugin(void); #endif extern void simple_plugin_init(void); /* * functions from cursors_leaks.c */ extern bool plpgsql_check_cursors_leaks; extern bool plpgsql_check_cursors_leaks_strict; extern int plpgsql_check_cursors_leaks_level; extern void plpgsql_check_cursors_leaks_init(void); /* * functions and definitions from generic_stmt_walker.c */ typedef void (*plch_stmt_walker_callback) (PLpgSQL_stmt *stmt, void *context); typedef void (*plch_expr_walker_callback) (PLpgSQL_stmt *stmt, PLpgSQL_expr *expr, void *context); /* * As in nodeFuncs.h, we respectfully decline to support the C standard's * position that a pointer to struct is incompatible with "void *". Instead, * silence related compiler warnings using casts in this macro wrapper. */ #define plch_statement_tree_walker(s, sw, ew, c) \ plch_statement_tree_walker_impl(s, (plch_stmt_walker_callback) (sw), \ (plch_expr_walker_callback) (ew), c) extern void plch_statement_tree_walker_impl(PLpgSQL_stmt *stmt, plch_stmt_walker_callback stmt_callback, plch_expr_walker_callback expr_callback, void *context); extern bool plch_statement_is_loop(PLpgSQL_stmt *stmt); extern List *plch_statement_get_loop_body(PLpgSQL_stmt *stmt); extern bool plch_statement_is_container(PLpgSQL_stmt *stmt); extern PLpgSQL_expr *plch_statement_get_expr(PLpgSQL_stmt *stmt, bool *is_dynamic, List **params, const char **exprname); /* * functions from plpgsql_check.c */ #if PG_VERSION_NUM >= 150000 extern shmem_request_hook_type plpgsql_check_prev_shmem_request_hook; #endif extern shmem_startup_hook_type plpgsql_check_prev_shmem_startup_hook; extern void plpgsql_check_check_ext_version(Oid fn_oid); extern void plpgsql_check_passive_check_init(void); /* * Links to function in plpgsql module */ typedef PLpgSQL_type *(*plpgsql_check__build_datatype_t) (Oid typeOid, int32 typmod, Oid collation, TypeName *origtypname); extern plpgsql_check__build_datatype_t plpgsql_check__build_datatype_p; typedef PLpgSQL_function *(*plpgsql_check__compile_t) (FunctionCallInfo fcinfo, bool forValidator); extern plpgsql_check__compile_t plpgsql_check__compile_p; typedef void (*plpgsql_check__parser_setup_t) (struct ParseState *pstate, PLpgSQL_expr *expr); extern plpgsql_check__parser_setup_t plpgsql_check__parser_setup_p; typedef const char *(*plpgsql_check__stmt_typename_t) (PLpgSQL_stmt *stmt); extern plpgsql_check__stmt_typename_t plpgsql_check__stmt_typename_p; typedef Oid (*plpgsql_check__exec_get_datum_type_t) (PLpgSQL_execstate *estate, PLpgSQL_datum *datum); extern plpgsql_check__exec_get_datum_type_t plpgsql_check__exec_get_datum_type_p; typedef int (*plpgsql_check__recognize_err_condition_t) (const char *condname, bool allow_sqlstate); extern plpgsql_check__recognize_err_condition_t plpgsql_check__recognize_err_condition_p; typedef PLpgSQL_nsitem *(*plpgsql_check__ns_lookup_t) (PLpgSQL_nsitem *ns_cur, bool localmode, const char *name1, const char *name2, const char *name3, int *names_used); extern plpgsql_check__ns_lookup_t plpgsql_check__ns_lookup_p; #define NEVER_READ_VARIABLE_TEXT "never read variable \"%s\"" #define NEVER_READ_VARIABLE_TEXT_CHECK_LENGTH 19 #define UNUSED_PARAMETER_TEXT "unused parameter \"%s\"" #define NEVER_READ_PARAMETER_TEXT "parameter \"%s\" is never read" #define UNMODIFIED_VARIABLE_TEXT "unmodified OUT variable \"%s\"" #define OUT_COMPOSITE_IS_NOT_SINGLE_TEXT "composite OUT variable \"%s\" is not single argument" #define UNUSED_VARIABLE_TEXT "unused variable \"%s\"" #define UNUSED_VARIABLE_TEXT_CHECK_LENGTH 15 #define MAYBE_UNMODIFIED_VARIABLE_TEXT "OUT variable \"%s\" is maybe unmodified" /* * Expecting persistent oid of nextval, currval and setval functions. * Ensured by regress tests. */ #define NEXTVAL_OID 1574 #define CURRVAL_OID 1575 #define SETVAL_OID 1576 #define SETVAL2_OID 1765 #define FORMAT_0PARAM_OID 3540 #define FORMAT_NPARAM_OID 3539 #if PG_VERSION_NUM < 180000 #ifndef TupleDescAttr #define TupleDescAttr(tupdesc, i) ((tupdesc)->attrs[(i)]) #endif #endif #if PG_VERSION_NUM >= 180000 #define plch_use_count(func) ((func)->cfunc.use_count) #define plch_func_xmin(func) ((func)->cfunc.fn_xmin) #define plch_func_tid(func) ((func)->cfunc.fn_tid) #define plch_func_hashkey(func) ((func)->cfunc.fn_hashkey) #else #define plch_use_count(func) ((func)->use_count) #define plch_func_xmin(func) ((func)->fn_xmin) #define plch_func_tid(func) ((func)->fn_tid) #define plch_func_hashkey(func) ((func)->fn_hashkey) #endif #if PG_VERSION_NUM >= 170000 #define CURRENT_LXID (MyProc->vxid.lxid) #else #define CURRENT_LXID (MyProc->lxid) #endif #define recvar_tuple(rec) (rec->erh ? expanded_record_get_tuple(rec->erh) : NULL) #define recvar_tupdesc(rec) (rec->erh ? expanded_record_fetch_tupdesc(rec->erh) : NULL) #define FUNCS_PER_USER 128 /* initial table size */ #ifdef _MSC_VER #define strcasecmp _stricmp #define strncasecmp _strnicmp #endif okbob-plpgsql_check-800db79/src/plpgsql_check_builtins.h000066400000000000000000000055341526744060500234720ustar00rootroot00000000000000 #ifndef PLPGSQL_CHECK_BUILTINS #define PLPGSQL_CHECK_BUILTINS #ifdef _MSC_VER /* * _PG_init should be exported, but PGDLLEXPORT cannot be used due * collision with _PG_init from plpgsql.h */ #ifdef _M_X64 #pragma comment( linker, "/export:_PG_init" ) #else #pragma comment( linker, "/export:_PG_init=__PG_init" ) #endif #endif #ifndef PGDLLEXPORT #ifdef _MSC_VER #define PGDLLEXPORT __declspec(dllexport) /* * PG_MODULE_MAGIC and PG_FUNCTION_INFO_V1 macros are broken for MSVC. * So, we redefine them. */ #undef PG_MODULE_MAGIC #define PG_MODULE_MAGIC \ extern PGDLLEXPORT const Pg_magic_struct *PG_MAGIC_FUNCTION_NAME(void); \ const Pg_magic_struct * \ PG_MAGIC_FUNCTION_NAME(void) \ { \ static const Pg_magic_struct Pg_magic_data = PG_MODULE_MAGIC_DATA; \ return &Pg_magic_data; \ } \ extern int no_such_variable #undef PG_FUNCTION_INFO_V1 #define PG_FUNCTION_INFO_V1(funcname) \ extern PGDLLEXPORT const Pg_finfo_record * CppConcat(pg_finfo_,funcname)(void); \ const Pg_finfo_record * \ CppConcat(pg_finfo_,funcname) (void) \ { \ static const Pg_finfo_record my_finfo = { 1 }; \ return &my_finfo; \ } \ extern int no_such_variable #else #define PGDLLEXPORT PGDLLIMPORT #endif #endif /* * Interface * */ extern void _PG_init(void); extern PGDLLEXPORT Datum plpgsql_check_function_tb(PG_FUNCTION_ARGS); extern PGDLLEXPORT Datum plpgsql_check_function(PG_FUNCTION_ARGS); extern PGDLLEXPORT Datum plpgsql_show_dependency_tb(PG_FUNCTION_ARGS); extern PGDLLEXPORT Datum plpgsql_check_function_tb_name(PG_FUNCTION_ARGS); extern PGDLLEXPORT Datum plpgsql_check_function_name(PG_FUNCTION_ARGS); extern PGDLLEXPORT Datum plpgsql_show_dependency_tb_name(PG_FUNCTION_ARGS); extern PGDLLEXPORT Datum plpgsql_profiler_reset(PG_FUNCTION_ARGS); extern PGDLLEXPORT Datum plpgsql_profiler_reset_all(PG_FUNCTION_ARGS); extern PGDLLEXPORT Datum plpgsql_profiler_function_tb(PG_FUNCTION_ARGS); extern PGDLLEXPORT Datum plpgsql_profiler_function_statements_tb(PG_FUNCTION_ARGS); extern PGDLLEXPORT Datum plpgsql_profiler_function_tb_name(PG_FUNCTION_ARGS); extern PGDLLEXPORT Datum plpgsql_profiler_function_statements_tb_name(PG_FUNCTION_ARGS); extern PGDLLEXPORT Datum plpgsql_profiler_functions_all_tb(PG_FUNCTION_ARGS); extern PGDLLEXPORT Datum plpgsql_coverage_statements(PG_FUNCTION_ARGS); extern PGDLLEXPORT Datum plpgsql_coverage_branches(PG_FUNCTION_ARGS); extern PGDLLEXPORT Datum plpgsql_coverage_statements_name(PG_FUNCTION_ARGS); extern PGDLLEXPORT Datum plpgsql_coverage_branches_name(PG_FUNCTION_ARGS); extern PGDLLEXPORT Datum plpgsql_make_pragma(PG_FUNCTION_ARGS); extern PGDLLEXPORT Datum plpgsql_profiler_install_fake_queryid_hook(PG_FUNCTION_ARGS); extern PGDLLEXPORT Datum plpgsql_profiler_remove_fake_queryid_hook(PG_FUNCTION_ARGS); extern PGDLLEXPORT Datum plpgsql_check_profiler_ctrl(PG_FUNCTION_ARGS); extern PGDLLEXPORT Datum plpgsql_check_tracer_ctrl(PG_FUNCTION_ARGS); #endif okbob-plpgsql_check-800db79/src/pragma.c000066400000000000000000000164071526744060500202050ustar00rootroot00000000000000/*------------------------------------------------------------------------- * * pragma.c * * pragma related code * * by Pavel Stehule 2013-2026 * *------------------------------------------------------------------------- */ #include "plpgsql_check.h" #include "plpgsql_check_builtins.h" #include "utils/builtins.h" #include "utils/array.h" #include "parser/scansup.h" #include "tcop/tcopprot.h" #include "tcop/utility.h" #ifdef _MSC_VER #define strcasecmp _stricmp #define strncasecmp _strnicmp #endif PG_FUNCTION_INFO_V1(plpgsql_check_pragma); static void runtime_pragma_apply(char *pragma_str) { while (scanner_isspace(*pragma_str)) pragma_str++; if (strncasecmp(pragma_str, "STATUS:", 7) == 0) { pragma_str += 7; while (scanner_isspace(*pragma_str)) pragma_str++; if (strcasecmp(pragma_str, "TRACER") == 0) elog(NOTICE, "tracer is %s", plpgsql_check_tracer ? "enabled" : "disabled"); } else if (strncasecmp(pragma_str, "ENABLE:", 7) == 0) { pragma_str += 7; while (scanner_isspace(*pragma_str)) pragma_str++; if (strcasecmp(pragma_str, "TRACER") == 0) plpgsql_check_tracer = true; } else if (strncasecmp(pragma_str, "DISABLE:", 8) == 0) { pragma_str += 8; while (scanner_isspace(*pragma_str)) pragma_str++; if (strcasecmp(pragma_str, "TRACER") == 0) plpgsql_check_tracer = false; } } static bool pragma_apply(PLpgSQL_checkstate *cstate, plpgsql_check_pragma_vector *pv, char *pragma_str, PLpgSQL_nsitem *ns, int lineno) { bool is_valid = true; Assert(cstate); while (scanner_isspace(*pragma_str)) pragma_str++; if (strncasecmp(pragma_str, "ECHO:", 5) == 0) { elog(NOTICE, "%s", plpgsql_check_process_echo_string(pragma_str + 5, cstate->cinfo)); } else if (strncasecmp(pragma_str, "STATUS:", 7) == 0) { pragma_str += 7; while (scanner_isspace(*pragma_str)) pragma_str++; if (strcasecmp(pragma_str, "CHECK") == 0) elog(NOTICE, "check is %s", pv->disable_check ? "disabled" : "enabled"); else if (strcasecmp(pragma_str, "TRACER") == 0) elog(NOTICE, "tracer is %s", pv->disable_tracer ? "disabled" : "enabled"); else if (strcasecmp(pragma_str, "OTHER_WARNINGS") == 0) elog(NOTICE, "other_warnings is %s", pv->disable_other_warnings ? "disabled" : "enabled"); else if (strcasecmp(pragma_str, "PERFORMANCE_WARNINGS") == 0) elog(NOTICE, "performance_warnings is %s", pv->disable_performance_warnings ? "disabled" : "enabled"); else if (strcasecmp(pragma_str, "EXTRA_WARNINGS") == 0) elog(NOTICE, "extra_warnings is %s", pv->disable_extra_warnings ? "disabled" : "enabled"); else if (strcasecmp(pragma_str, "SECURITY_WARNINGS") == 0) elog(NOTICE, "security_warnings is %s", pv->disable_security_warnings ? "disabled" : "enabled"); else if (strcasecmp(pragma_str, "COMPATIBILITY_WARNINGS") == 0) elog(NOTICE, "compatibility_warnings is %s", pv->disable_compatibility_warnings ? "disabled" : "enabled"); else if (strcasecmp(pragma_str, "CONSTANTS_TRACING") == 0) elog(NOTICE, "constants_tracing is %s", pv->disable_constants_tracing ? "disabled" : "enabled"); else { elog(WARNING, "unsuported pragma: %s", pragma_str); is_valid = false; } } else if (strncasecmp(pragma_str, "ENABLE:", 7) == 0) { pragma_str += 7; while (scanner_isspace(*pragma_str)) pragma_str++; if (strcasecmp(pragma_str, "CHECK") == 0) pv->disable_check = false; else if (strcasecmp(pragma_str, "TRACER") == 0) pv->disable_tracer = false; else if (strcasecmp(pragma_str, "OTHER_WARNINGS") == 0) pv->disable_other_warnings = false; else if (strcasecmp(pragma_str, "PERFORMANCE_WARNINGS") == 0) pv->disable_performance_warnings = false; else if (strcasecmp(pragma_str, "EXTRA_WARNINGS") == 0) pv->disable_extra_warnings = false; else if (strcasecmp(pragma_str, "SECURITY_WARNINGS") == 0) pv->disable_security_warnings = false; else if (strcasecmp(pragma_str, "COMPATIBILITY_WARNINGS") == 0) pv->disable_compatibility_warnings = false; else if (strcasecmp(pragma_str, "CONSTANTS_TRACING") == 0) pv->disable_constants_tracing = false; else { elog(WARNING, "unsuported pragma: %s", pragma_str); is_valid = false; } } else if (strncasecmp(pragma_str, "DISABLE:", 8) == 0) { pragma_str += 8; while (scanner_isspace(*pragma_str)) pragma_str++; if (strcasecmp(pragma_str, "CHECK") == 0) pv->disable_check = true; else if (strcasecmp(pragma_str, "TRACER") == 0) pv->disable_tracer = true; else if (strcasecmp(pragma_str, "OTHER_WARNINGS") == 0) pv->disable_other_warnings = true; else if (strcasecmp(pragma_str, "PERFORMANCE_WARNINGS") == 0) pv->disable_performance_warnings = true; else if (strcasecmp(pragma_str, "EXTRA_WARNINGS") == 0) pv->disable_extra_warnings = true; else if (strcasecmp(pragma_str, "SECURITY_WARNINGS") == 0) pv->disable_security_warnings = true; else if (strcasecmp(pragma_str, "COMPATIBILITY_WARNINGS") == 0) pv->disable_compatibility_warnings = true; else if (strcasecmp(pragma_str, "CONSTANTS_TRACING") == 0) pv->disable_constants_tracing = true; else { elog(WARNING, "unsuported pragma: %s", pragma_str); is_valid = false; } } else if (strncasecmp(pragma_str, "TYPE:", 5) == 0) { is_valid = plpgsql_check_pragma_type(cstate, pragma_str + 5, ns, lineno); } else if (strncasecmp(pragma_str, "TABLE:", 6) == 0) { is_valid = plpgsql_check_pragma_table(cstate, pragma_str + 6, lineno); } else if (strncasecmp(pragma_str, "SEQUENCE:", 9) == 0) { is_valid = plpgsql_check_pragma_sequence(cstate, pragma_str + 9, lineno); } else if (strncasecmp(pragma_str, "ASSERT-SCHEMA:", 14) == 0) { is_valid = plpgsql_check_pragma_assert(cstate, PLPGSQL_CHECK_PRAGMA_ASSERT_SCHEMA, pragma_str + 14, ns, lineno); } else if (strncasecmp(pragma_str, "ASSERT-TABLE:", 13) == 0) { is_valid = plpgsql_check_pragma_assert(cstate, PLPGSQL_CHECK_PRAGMA_ASSERT_TABLE, pragma_str + 13, ns, lineno); } else if (strncasecmp(pragma_str, "ASSERT-COLUMN:", 14) == 0) { is_valid = plpgsql_check_pragma_assert(cstate, PLPGSQL_CHECK_PRAGMA_ASSERT_COLUMN, pragma_str + 14, ns, lineno); } else { elog(WARNING, "unsupported pragma: %s", pragma_str); is_valid = false; } return is_valid; } /* * Implementation of pragma function. There are two different * use cases - 1) it is used for static analyze by plpgsql_check, * where arguments are read from parse tree. * 2) it is used for controlling of code tracing in runtime. * arguments, are processed as usual for variadic text function. */ Datum plpgsql_check_pragma(PG_FUNCTION_ARGS) { ArrayType *array; ArrayIterator iter; bool isnull; Datum value; if (PG_ARGISNULL(0)) PG_RETURN_INT32(0); array = PG_GETARG_ARRAYTYPE_P(0); iter = array_create_iterator(array, 0, NULL); while (array_iterate(iter, &value, &isnull)) { char *pragma_str; if (isnull) continue; pragma_str = TextDatumGetCString(value); runtime_pragma_apply(pragma_str); pfree(pragma_str); } array_free_iterator(iter); PG_RETURN_INT32(1); } void plpgsql_check_pragma_apply(PLpgSQL_checkstate *cstate, char *pragma_str, PLpgSQL_nsitem *ns, int lineno) { if (pragma_apply(cstate, &(cstate->pragma_vector), pragma_str, ns, lineno)) cstate->was_pragma = true; } okbob-plpgsql_check-800db79/src/pragma_generator.c000066400000000000000000000305671526744060500222560ustar00rootroot00000000000000/*------------------------------------------------------------------------- * * pragma_generator.c * * generator of table pragmas for CREATE TEMP TABLE * statements used in function's body * * by Pavel Stehule 2013-2026 * *------------------------------------------------------------------------- */ #include "plpgsql_check.h" #include "access/table.h" #include "catalog/namespace.h" #include "catalog/pg_class.h" #include "catalog/pg_type.h" #include "nodes/nodeFuncs.h" #include "parser/parser.h" #include "utils/builtins.h" #include "utils/rel.h" static void make_pragma_stmt_walker(PLpgSQL_stmt *stmt, void *context); static void process_stmt_query(PLpgSQL_checkstate *cstate, PLpgSQL_stmt *stmt, PLpgSQL_expr *expr, bool is_perform_stmt); static void process_create_table_as(PLpgSQL_checkstate *cstate, PLpgSQL_stmt *stmt, PLpgSQL_expr *expr, CreateTableAsStmt *ctas); static void process_create_table(PLpgSQL_checkstate *cstate, PLpgSQL_expr *expr, CreateStmt *cstmt); static char *build_table_pragma_def(const char *relname, List *colNames, List *targetList); static char *build_table_pragma_def_from_tupdesc(const char *relname, TupleDesc tupdesc); static void emit_pragma_from_relid(PLpgSQL_checkstate *cstate, Oid relid); static void drop_temp_table(const char *relname); static bool is_temp_range_var(RangeVar *rel); static void check_temp_schema(RangeVar *rel); /* * Returns true, when a range var describes a temporary table. The * persistence can be entered explicitly (TEMP or TEMPORARY keywords), * or implicitly by using of "pg_temp" schema. */ static bool is_temp_range_var(RangeVar *rel) { if (rel->relpersistence == RELPERSISTENCE_TEMP) return true; return rel->schemaname && strcmp(rel->schemaname, "pg_temp") == 0; } /* * Emulates the check of target schema of a temporary table done by * RangeVarAdjustRelationPersistence routine. We cannot to use this * routine directly, because it requires an existing schema, and we * don't want to create the temporary schema as a side effect. */ static void check_temp_schema(RangeVar *rel) { if (rel->relpersistence == RELPERSISTENCE_TEMP && rel->schemaname && strcmp(rel->schemaname, "pg_temp") != 0) ereport(ERROR, (errcode(ERRCODE_INVALID_TABLE_DEFINITION), errmsg("cannot create temporary relation in non-temporary schema"))); } /* * Returns definition of a table (usable by table pragma) derived from * target list of an analyzed query. The names of columns can be * overwritten by explicitly entered column names from CREATE TABLE AS * statement. */ static char * build_table_pragma_def(const char *relname, List *colNames, List *targetList) { StringInfoData def; ListCell *lc; ListCell *colname_item; bool is_first = true; initStringInfo(&def); appendStringInfo(&def, "%s(", quote_identifier(relname)); colname_item = list_head(colNames); foreach(lc, targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); const char *colname; Oid typid; int32 typmod; if (tle->resjunk) continue; if (colname_item) { colname = strVal(lfirst(colname_item)); colname_item = lnext(colNames, colname_item); } else colname = tle->resname ? tle->resname : "?column?"; typid = exprType((Node *) tle->expr); typmod = exprTypmod((Node *) tle->expr); /* like CREATE TABLE AS does, replace unknown type by text */ if (typid == UNKNOWNOID) { typid = TEXTOID; typmod = -1; } if (!is_first) appendStringInfoString(&def, ", "); appendStringInfo(&def, "%s %s", quote_identifier(colname), format_type_with_typemod(typid, typmod)); is_first = false; } /* be consistent with CREATE TABLE AS behaviour */ if (colname_item) ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR), errmsg("too many column names were specified"))); appendStringInfoChar(&def, ')'); return def.data; } /* * Returns definition of a table (usable by table pragma) derived from * a tuple descriptor of an existing relation. */ static char * build_table_pragma_def_from_tupdesc(const char *relname, TupleDesc tupdesc) { StringInfoData def; bool is_first = true; int i; initStringInfo(&def); appendStringInfo(&def, "%s(", quote_identifier(relname)); for (i = 0; i < tupdesc->natts; i++) { Form_pg_attribute att = TupleDescAttr(tupdesc, i); if (att->attisdropped) continue; if (!is_first) appendStringInfoString(&def, ", "); appendStringInfo(&def, "%s %s", quote_identifier(NameStr(att->attname)), format_type_with_typemod(att->atttypid, att->atttypmod)); is_first = false; } appendStringInfoChar(&def, ')'); return def.data; } /* * Emits the table pragma derived from the structure of an existing * relation. */ static void emit_pragma_from_relid(PLpgSQL_checkstate *cstate, Oid relid) { Relation rel; char *def; rel = table_open(relid, AccessShareLock); def = build_table_pragma_def_from_tupdesc(RelationGetRelationName(rel), RelationGetDescr(rel)); table_close(rel, AccessShareLock); plch_put_text_line(cstate->result_info, psprintf("table: %s", def), -1); } /* * A name collision (usually the pattern CREATE, DROP, CREATE with same * name) is an artifact of static scanning - DROP statements are not * executed. We want to return pragmas for both definitions, so the * previous table is dropped (the drop is reverted by rollback of the * check's subtransaction), and the following statements will see the * last definition like in runtime. */ static void drop_temp_table(const char *relname) { ereport(WARNING, (errcode(ERRCODE_DUPLICATE_TABLE), errmsg("relation \"%s\" already exists", relname), errdetail("The temporary table is replaced by the new definition."))); if (SPI_execute(psprintf("DROP TABLE pg_temp.%s", quote_identifier(relname)), false, 0) != SPI_OK_UTILITY) elog(ERROR, "unexpected SPI result on DROP TABLE execution"); } /* * Generates (and returns as a result row) the table pragma for one * CREATE TEMP TABLE AS statement. The pragma is applied immediately * too, so the temporary table is visible for planning of the following * statements. */ static void process_create_table_as(PLpgSQL_checkstate *cstate, PLpgSQL_stmt *stmt, PLpgSQL_expr *expr, CreateTableAsStmt *ctas) { CachedPlanSource *plansource; CreateTableAsStmt *ctas_analyzed; Query *query; Query *inner_query; Oid relid; char *def; /* materialized views cannot be temporary */ if (ctas->objtype != OBJECT_TABLE) return; if (!ctas->into || !ctas->into->rel) return; if (!is_temp_range_var(ctas->into->rel)) return; check_temp_schema(ctas->into->rel); /* CREATE TABLE AS EXECUTE is not supported */ if (ctas->query && IsA(ctas->query, ExecuteStmt)) return; /* * Plan the statement. The parse analysis transforms an inner query of * CREATE TABLE AS statement, so names and types of columns can be read * from the target list without execution. */ plch_expr_prepare_plan(cstate, expr); plansource = plpgsql_check_get_plan_source(cstate, expr->plan); if (!plansource || !plansource->query_list) return; query = linitial_node(Query, plansource->query_list); if (query->commandType != CMD_UTILITY || !query->utilityStmt || !IsA(query->utilityStmt, CreateTableAsStmt)) return; ctas_analyzed = (CreateTableAsStmt *) query->utilityStmt; if (!ctas_analyzed->query || !IsA(ctas_analyzed->query, Query)) return; inner_query = (Query *) ctas_analyzed->query; if (inner_query->commandType != CMD_SELECT) return; def = build_table_pragma_def(ctas->into->rel->relname, ctas->into->colNames, inner_query->targetList); relid = RangeVarGetRelid(ctas->into->rel, NoLock, true); if (OidIsValid(relid)) { /* like runtime does, the existing table wins */ if (ctas->if_not_exists) { emit_pragma_from_relid(cstate, relid); return; } drop_temp_table(ctas->into->rel->relname); } plch_put_text_line(cstate->result_info, psprintf("table: %s", def), -1); /* make the temporary table visible for following statements */ plpgsql_check_pragma_table(cstate, def, stmt->lineno); } /* * Generates (and returns as a result row) the table pragma for one * CREATE TEMP TABLE statement - the column definition list form, the * LIKE clause, the OF type clause, inheritance or partitioning. The * original statement is executed - the temporary table is created * inside the check's subtransaction, that is always rolled back, so * no object survives the check. The execution is the only reliable * way how to get the table's structure - the LIKE clause, the OF type * clause, inheritance, partitioning or serial columns are expanded by * PostgreSQL itself. Unlike the CREATE TABLE AS branch, the pragma is * not applied here - the table already exists, and it is visible for * planning of the following statements. */ static void process_create_table(PLpgSQL_checkstate *cstate, PLpgSQL_expr *expr, CreateStmt *cstmt) { Oid relid; if (!is_temp_range_var(cstmt->relation)) return; check_temp_schema(cstmt->relation); relid = RangeVarGetRelid(cstmt->relation, NoLock, true); if (OidIsValid(relid) && !cstmt->if_not_exists) drop_temp_table(cstmt->relation->relname); if (SPI_execute(expr->query, false, 0) != SPI_OK_UTILITY) elog(ERROR, "unexpected SPI result on CREATE TABLE execution"); relid = RangeVarGetRelid(cstmt->relation, NoLock, true); if (!OidIsValid(relid)) return; emit_pragma_from_relid(cstate, relid); } /* * Processes one SQL statement of function's body. When the statement * creates a temporary table, then the table pragma is generated for it * (and applied). Explicitly written pragmas are applied too, so a * manually declared temporary table is visible for planning of the * following statements. */ static void process_stmt_query(PLpgSQL_checkstate *cstate, PLpgSQL_stmt *stmt, PLpgSQL_expr *expr, bool is_perform_stmt) { MemoryContext oldCxt; ResourceOwner oldowner; if (!expr || !expr->query) return; cstate->estate->err_stmt = stmt; oldCxt = CurrentMemoryContext; oldowner = CurrentResourceOwner; BeginInternalSubTransaction(NULL); MemoryContextSwitchTo(cstate->check_cxt); PG_TRY(); { List *parsetree; parsetree = raw_parser(expr->query, expr->parseMode); if (list_length(parsetree) == 1) { Node *node = linitial_node(RawStmt, parsetree)->stmt; if (IsA(node, CreateTableAsStmt)) { process_create_table_as(cstate, stmt, expr, (CreateTableAsStmt *) node); } else if (IsA(node, CreateStmt)) { process_create_table(cstate, expr, (CreateStmt *) node); } else if (IsA(node, SelectStmt)) { (void) plch_apply_inline_pragmas(cstate, (SelectStmt *) node, expr->ns, stmt->lineno, is_perform_stmt); } } ReleaseCurrentSubTransaction(); MemoryContextSwitchTo(oldCxt); CurrentResourceOwner = oldowner; } PG_CATCH(); { ErrorData *edata; MemoryContextSwitchTo(cstate->check_cxt); edata = CopyErrorData(); FlushErrorState(); RollbackAndReleaseCurrentSubTransaction(); MemoryContextSwitchTo(oldCxt); CurrentResourceOwner = oldowner; if (cstate->cinfo->fatal_errors) ReThrowError(edata); /* the statement is ignored, and warning is raised instead */ ereport(WARNING, (errcode(edata->sqlerrcode), errmsg("pragma cannot be generated for statement on line %d", stmt->lineno), errdetail("%s", edata->message))); FreeErrorData(edata); } PG_END_TRY(); } /* * The walker of statement tree. The statements are visited in textual * order (pre-order). Only static SQL statements are processed - the * dynamic (EXECUTE) statements are ignored. */ static void make_pragma_stmt_walker(PLpgSQL_stmt *stmt, void *context) { PLpgSQL_checkstate *cstate = (PLpgSQL_checkstate *) context; switch (stmt->cmd_type) { case PLPGSQL_STMT_EXECSQL: process_stmt_query(cstate, stmt, ((PLpgSQL_stmt_execsql *) stmt)->sqlstmt, false); break; case PLPGSQL_STMT_PERFORM: process_stmt_query(cstate, stmt, ((PLpgSQL_stmt_perform *) stmt)->expr, true); break; default: break; } plch_statement_tree_walker(stmt, make_pragma_stmt_walker, NULL, context); } /* * Scans the function's body and generates (and returns as result rows) * table pragmas for all statements there that create temporary tables. */ void plch_make_pragma(PLpgSQL_checkstate *cstate, PLpgSQL_function *func) { make_pragma_stmt_walker((PLpgSQL_stmt *) func->action, cstate); } okbob-plpgsql_check-800db79/src/profiler.c000066400000000000000000002135371526744060500205630ustar00rootroot00000000000000/*------------------------------------------------------------------------- * * profiler.c * * profiler accessories code * * by Pavel Stehule 2013-2026 * *------------------------------------------------------------------------- */ #include "plpgsql_check.h" #include "plpgsql_check_builtins.h" #include "access/htup_details.h" #include "catalog/pg_proc.h" #include "catalog/pg_type.h" #include "commands/dbcommands.h" #include "nodes/pg_list.h" #include "optimizer/optimizer.h" #include "parser/analyze.h" #include "storage/lwlock.h" #include "storage/shmem.h" #include "storage/lock.h" #include "storage/proc.h" #include "tcop/tcopprot.h" #include "utils/array.h" #include "utils/builtins.h" #include "utils/memutils.h" #include "utils/lsyscache.h" #include "utils/snapmgr.h" #include "utils/syscache.h" #include "utils/float.h" #include /* * It is unique for function with same oid. For function statistic * We don't want to have multiple stats per every update of any * function. */ typedef struct func_hk { Oid fn_oid; Oid db_oid; } func_hk; typedef struct FuncStats { func_hk hk; slock_t mutex; uint64 exec_count; uint64 exec_count_err; uint64 total_time; double total_time_mean; double total_time_m2; uint64 min_time; uint64 max_time; } FuncStats; /* * This is used as cache for types of expressions of USING clause * (EXECUTE like statements). */ typedef struct QParams { int nparams; Oid paramtypes[FLEXIBLE_ARRAY_MEMBER]; } QParams; /* * PLpgSQL statement instrumentation * Attention - the commands that can contains nestested commands * has attached own time and nested statements time too. */ typedef struct StmtInstr { bool has_queryid; pc_queryid queryid; uint64 us_max; uint64 us_total; uint64 rows; uint64 exec_count; uint64 exec_count_err; uint64 exec_count_cond_err; instr_time start_time; instr_time total; QParams *qparams; } StmtInstr; typedef struct StmtStats { bool has_queryid; pc_queryid queryid; uint64 us_max; uint64 us_total; uint64 rows; uint64 exec_count; uint64 exec_count_err; uint64 exec_count_cond_err; } StmtStats; #define FUNC_STATS_COUNT 20000 typedef struct FuncStmtsStats { plch_fidentity_hk hk; int nstatements; slock_t mutex; StmtStats *sstats; int shared_sstats_offset; } FuncStmtsStats; typedef struct ProfilerSharedState { LWLock *func_stats_lock; LWLock *func_stmts_stats_lock; int stmt_stats_count; int used_stmt_stats_count; } ProfilerSharedState; /* * local transactional cache - instead update shared stats * after every execution, stats are updated at end of transaction. * Attention - the transactions can be ended inside procedures, so * sometimes transactions are shorter than functions, sometimes are * longer. */ typedef struct LXCache { plch_fidentity_hk hk; FuncStats fstats; StmtStats *sstats; int nstatements; } LXCache; /* * This structure is used as plpgsql extension parameter */ typedef struct profiler_info { StmtInstr *sinstrs; StmtStats *sstats; int nstatements; instr_time start_time; PLpgSQL_function *func; MemoryContext mcxt; /* reduce second searching lxcache when se update fstats */ LXCache *lxcache; LocalTransactionId lxcache_lxid; MemoryContextCallback er_mcb; } profiler_info; enum { COVERAGE_STATEMENTS, COVERAGE_BRANCHES }; /* * lxcache_ht is created in top memory context, but the content * is transactional. This is necessary, because we want to use * lxcache_ht from lxcache mcx reset callback. Sometimes can be * problem who is responsible for destroing this table. We can * do this at lxcace reset callback. Then this table cannot be * orphaned. This structure will be used as a arg of MemoryContext * reset callback; */ typedef struct LXCache_info { MemoryContext lxcache_mcxt; HTAB *lxcache_ht; MemoryContextCallback lxcache_mcb; } LXCache_info; PG_FUNCTION_INFO_V1(plpgsql_check_profiler_ctrl); PG_FUNCTION_INFO_V1(plpgsql_profiler_reset_all); PG_FUNCTION_INFO_V1(plpgsql_profiler_reset); PG_FUNCTION_INFO_V1(plpgsql_coverage_statements); PG_FUNCTION_INFO_V1(plpgsql_coverage_branches); PG_FUNCTION_INFO_V1(plpgsql_coverage_statements_name); PG_FUNCTION_INFO_V1(plpgsql_coverage_branches_name); PG_FUNCTION_INFO_V1(plpgsql_profiler_install_fake_queryid_hook); PG_FUNCTION_INFO_V1(plpgsql_profiler_remove_fake_queryid_hook); static void update_persistent_stmts_stats(profiler_info *pinfo); #if PG_VERSION_NUM >= 190000 static void profiler_fake_queryid_hook(ParseState *pstate, Query *query, const JumbleState *jstate); #else static void profiler_fake_queryid_hook(ParseState *pstate, Query *query, JumbleState *jstate); #endif static bool profiler_is_active(PLpgSQL_execstate *estate, PLpgSQL_function *func); static void profiler_func_setup(PLpgSQL_execstate *estate, PLpgSQL_function *func, plch_fextra *fextra); static void profiler_func_end(PLpgSQL_execstate *estate, PLpgSQL_function *func, plch_fextra *fextra); static void profiler_func_abort(PLpgSQL_execstate *estate, PLpgSQL_function *func, plch_fextra *fextra); static void profiler_stmt_beg(PLpgSQL_execstate *estate, PLpgSQL_stmt *stmt, plch_fextra *fextra); static void profiler_stmt_end(PLpgSQL_execstate *estate, PLpgSQL_stmt *stmt, plch_fextra *fextra); static void profiler_stmt_abort(PLpgSQL_execstate *estate, PLpgSQL_stmt *stmt, plch_fextra *fextra); static plch_plugin profiler_plugin = { .is_active = profiler_is_active, .func_setup = profiler_func_setup, .func_beg = NULL, .func_end = profiler_func_end, .func_abort = profiler_func_abort, .stmt_beg = profiler_stmt_beg, .stmt_end = profiler_stmt_end, .stmt_abort = profiler_stmt_abort, .stmt_error = NULL, }; static void init_func_hk(func_hk *hk, Oid fn_oid); static void proctuple_init_function_identity_hashkey(plch_fidentity_hk *hk, HeapTuple procTuple); static void init_fstats(FuncStats *fs); static pc_queryid profiler_get_dyn_queryid(PLpgSQL_execstate *estate, PLpgSQL_expr *expr, QParams *qparams); static pc_queryid profiler_get_queryid(PLpgSQL_execstate *estate, PLpgSQL_stmt *stmt, bool *has_queryid, QParams **qparams, MemoryContext mcxt); static void WelfordVarianceAdd(uint64 *count, float8 *mean, float8 *m2, float8 x); static void WelfordVarianceMerge(uint64 *count, float8 *mean, float8 *m2, uint64 a_count, float8 a_mean, float8 a_m2, uint64 b_count, float8 b_mean, float8 b_m2); static PLpgSQL_function *cinfo_get_function(plpgsql_check_info *cinfo, FunctionCallInfo fcinfo); static bool get_plpgsql_expr_type(PLpgSQL_expr *expr, Oid *result_type); static char *cutline(char *str, char **line); static HTAB *assign_shared_htab(const char *tabname, Size keysize, Size Entrysize, int nelems); static void lxcache_reset_callback(void *arg); static void merge_lxcached_shared_stmts_stats(LXCache *lxcache, bool *raise_warning); static void merge_lxcached_shared_fstats(LXCache *lxcache); static void merge_fstats(FuncStats *a, FuncStats *b); static HTAB *func_stats_ht = NULL; static HTAB *shared_func_stats_ht = NULL; static HTAB *func_stmts_stats_ht = NULL; static HTAB *shared_func_stmts_stats_ht = NULL; static HTAB *lxcache_ht = NULL; static MemoryContext profiler_mcxt = NULL; static MemoryContext profiler_queryid_mcxt = NULL; static MemoryContext lxcache_mcxt = NULL; static LocalTransactionId lxcache_lxid = InvalidLocalTransactionId; static post_parse_analyze_hook_type prev_post_parse_analyze_hook = NULL; bool plpgsql_check_profiler = false; bool plch_use_shared_stats_when_it_possible = true; bool plch_use_lxcache = true; int plch_max_stat_size = 20480; bool plch_report_dynquery_queryid = false; static int used_stmt_stats_count = 0; static int estimated_stmt_stats_count = 0; /* * plpgsql_profiler_reset_all destroy lxcache_mcxt. It emits * execution of lxcache_reset_callback. But in this case, we * don't want to persistenting the content of lxcache. * A flag skip_cirrent_lxcache signalize this case. */ static bool skip_current_lxcache = false; static ProfilerSharedState *profiler_ss = NULL; static StmtStats *SharedStmtStatsArray = NULL; #define USE_SHARED_FUNC_STATS (shared_func_stats_ht && plch_use_shared_stats_when_it_possible) #define USE_SHARED_FUNC_STMTS_STATS (shared_func_stmts_stats_ht && plch_use_shared_stats_when_it_possible) /*************************************** * * Profiler initialization * *************************************** */ /* * Calculate shared memory for profiler state, hash tables, and statement stats. * */ Size plpgsql_check_shmem_size(void) { Size num_bytes = 0; num_bytes = MAXALIGN(sizeof(ProfilerSharedState)); estimated_stmt_stats_count = (((Size) plch_max_stat_size) * 1024) / sizeof(StmtStats); num_bytes = add_size(num_bytes, mul_size(estimated_stmt_stats_count, sizeof(StmtStats))); num_bytes = add_size(num_bytes, hash_estimate_size(20000, sizeof(FuncStmtsStats))); num_bytes = add_size(num_bytes, hash_estimate_size(20000, sizeof(FuncStats))); return num_bytes; } /* * Request additional shared memory resources. * * If you change code here, don't forget to also report the modifications in * _PG_init() for pg14 and below. */ #if PG_VERSION_NUM >= 150000 void plpgsql_check_profiler_shmem_request(void) { if (plpgsql_check_prev_shmem_request_hook) plpgsql_check_prev_shmem_request_hook(); RequestAddinShmemSpace(plpgsql_check_shmem_size()); RequestNamedLWLockTranche("plpgsql_check profiler funcs stats", 1); RequestNamedLWLockTranche("plpgsql_check profiler func stmts stats", 1); } #endif /* * Initialize shared memory used like permanent profile storage. * No other parts use shared memory, so this code is completly here. * */ void plpgsql_check_profiler_shmem_startup(void) { bool found; shared_func_stats_ht = NULL; if (plpgsql_check_prev_shmem_startup_hook) plpgsql_check_prev_shmem_startup_hook(); /* * Create or attach to the shared memory state, including hash table */ LWLockAcquire(AddinShmemInitLock, LW_EXCLUSIVE); profiler_ss = ShmemInitStruct("plpgsql_check profiler state", sizeof(ProfilerSharedState), &found); if (!found) { profiler_ss->func_stats_lock = &(GetNamedLWLockTranche("plpgsql_check profiler funcs stats"))->lock; profiler_ss->func_stmts_stats_lock = &(GetNamedLWLockTranche("plpgsql_check profiler func stmts stats"))->lock; profiler_ss->stmt_stats_count = estimated_stmt_stats_count; profiler_ss->used_stmt_stats_count = 0; } SharedStmtStatsArray = ShmemInitStruct("plpgsql_check profiler shared statements stats", mul_size(sizeof(StmtStats), estimated_stmt_stats_count), &found); shared_func_stats_ht = assign_shared_htab("plpgsql_check profiler func stats", sizeof(func_hk), sizeof(FuncStats), FUNC_STATS_COUNT); shared_func_stmts_stats_ht = assign_shared_htab("plpgsql_check profiler func stmts stats", sizeof(plch_fidentity_hk), sizeof(FuncStmtsStats), FUNC_STATS_COUNT); LWLockRelease(AddinShmemInitLock); } /* * Register plpgsql plugin2 for profiler */ void plpgsql_check_profiler_init(void) { plch_register_plugin(&profiler_plugin); } /*************************************** * * Profiler controlling * *************************************** */ /* * Enable, disable, show state profiler */ Datum plpgsql_check_profiler_ctrl(PG_FUNCTION_ARGS) { char *optstr; bool result; #define OPTNAME "plpgsql_check.profiler" if (!PG_ARGISNULL(0)) { bool optval = PG_GETARG_BOOL(0); (void) set_config_option(OPTNAME, optval ? "on" : "off", (superuser() ? PGC_SUSET : PGC_USERSET), PGC_S_SESSION, GUC_ACTION_SET, true, 0, false); } optstr = GetConfigOptionByName(OPTNAME, NULL, false); if (strcmp(optstr, "on") == 0) { elog(NOTICE, "profiler is active"); result = true; } else { elog(NOTICE, "profiler is not active"); result = false; }; if (PG_ARGISNULL(0)) { if (result) { if (shared_func_stats_ht) { if (plch_use_shared_stats_when_it_possible) elog(NOTICE, "shared memory is preallocated and used now"); else elog(NOTICE, "shared memory is preallocated and not used now"); } else elog(NOTICE, "shared memory is not preallocated"); } else { if (shared_func_stats_ht) elog(NOTICE, "shared memory is preallocated"); else elog(NOTICE, "shared memory is not preallocated"); } if (profiler_ss) elog(NOTICE, "allocated stats: %d (%ldkB), used stats: %d", profiler_ss->stmt_stats_count, (mul_size(sizeof(StmtStats), estimated_stmt_stats_count) / 1024), profiler_ss->used_stmt_stats_count); } PG_RETURN_BOOL(result); } /* * Reset all local and shared profiler statistics. */ Datum plpgsql_profiler_reset_all(PG_FUNCTION_ARGS) { /* be compiler quite */ (void) fcinfo; if (shared_func_stats_ht) { HASH_SEQ_STATUS hash_seq; FuncStats *fs; LWLockAcquire(profiler_ss->func_stats_lock, LW_EXCLUSIVE); hash_seq_init(&hash_seq, shared_func_stats_ht); while ((fs = hash_seq_search(&hash_seq)) != NULL) { hash_search(shared_func_stats_ht, &(fs->hk), HASH_REMOVE, NULL); } LWLockRelease(profiler_ss->func_stats_lock); } if (shared_func_stmts_stats_ht) { HASH_SEQ_STATUS hash_seq; FuncStmtsStats *fss; LWLockAcquire(profiler_ss->func_stmts_stats_lock, LW_EXCLUSIVE); hash_seq_init(&hash_seq, shared_func_stmts_stats_ht); while ((fss = hash_seq_search(&hash_seq)) != NULL) { hash_search(shared_func_stmts_stats_ht, &(fss->hk), HASH_REMOVE, NULL); } profiler_ss->used_stmt_stats_count = 0; LWLockRelease(profiler_ss->func_stmts_stats_lock); } if (lxcache_mcxt) { /* * More robust solution can be remove MemoryContextResetCallback and * destroy context locally. But this possibility is not on available * on all supported releases. */ skip_current_lxcache = true; /* * In this case only hash_destroy -> MemoryContextDelete are executed. * No exception can be throw, so we don't need to wrap this call to * PG_TRY */ MemoryContextDelete(lxcache_mcxt); skip_current_lxcache = false; Assert(lxcache_mcxt == NULL); Assert(lxcache_lxid == InvalidLocalTransactionId); } plch_profiler_init_local_hash_tables(); PG_RETURN_VOID(); } /* * Remove a function's aggregate statistics and current statement profile. */ Datum plpgsql_profiler_reset(PG_FUNCTION_ARGS) { Oid funcoid = PG_GETARG_OID(0); func_hk fhk; plch_fidentity_hk hk; HeapTuple procTuple; FuncStmtsStats *fss; bool found; init_func_hk(&fhk, funcoid); hash_search(func_stats_ht, (void *) &fhk, HASH_REMOVE, NULL); if (shared_func_stats_ht) { LWLockAcquire(profiler_ss->func_stats_lock, LW_EXCLUSIVE); hash_search(shared_func_stats_ht, (void *) &fhk, HASH_REMOVE, NULL); LWLockRelease(profiler_ss->func_stats_lock); } procTuple = SearchSysCache1(PROCOID, ObjectIdGetDatum(funcoid)); if (!HeapTupleIsValid(procTuple)) elog(ERROR, "cache lookup failed for function %u", funcoid); proctuple_init_function_identity_hashkey(&hk, procTuple); ReleaseSysCache(procTuple); fss = (FuncStmtsStats *) hash_search(func_stmts_stats_ht, (void *) &hk, HASH_FIND, &found); if (found) { used_stmt_stats_count -= fss->nstatements; pfree(fss->sstats); hash_search(func_stmts_stats_ht, (void *) &hk, HASH_REMOVE, NULL); } if (shared_func_stmts_stats_ht) { /* * In this case we doesn't release memory from SharedStmtStatsArray. * Probably it is possible if we use DSA. But user friendly API for * DSA is in Ph 18+. So DSA is not supported now. */ LWLockAcquire(profiler_ss->func_stmts_stats_lock, LW_EXCLUSIVE); hash_search(shared_func_stmts_stats_ht, (void *) &hk, HASH_REMOVE, NULL); LWLockRelease(profiler_ss->func_stmts_stats_lock); } if (lxcache_ht) { LXCache *lxcache; lxcache = (LXCache *) hash_search(lxcache_ht, (void *) &hk, HASH_FIND, &found); if (found) { pfree(lxcache->sstats); hash_search(lxcache_ht, (void *) &hk, HASH_REMOVE, NULL); } } PG_RETURN_VOID(); } Datum plpgsql_profiler_install_fake_queryid_hook(PG_FUNCTION_ARGS) { (void) fcinfo; if (post_parse_analyze_hook == profiler_fake_queryid_hook) PG_RETURN_VOID(); prev_post_parse_analyze_hook = post_parse_analyze_hook; post_parse_analyze_hook = profiler_fake_queryid_hook; PG_RETURN_VOID(); } Datum plpgsql_profiler_remove_fake_queryid_hook(PG_FUNCTION_ARGS) { (void) fcinfo; if (post_parse_analyze_hook == profiler_fake_queryid_hook) { post_parse_analyze_hook = prev_post_parse_analyze_hook; prev_post_parse_analyze_hook = NULL; } PG_RETURN_VOID(); } /* * Generate simple queryid for testing purpose. * DO NOT USE IN PRODUCTION. */ #if PG_VERSION_NUM >= 190000 static void profiler_fake_queryid_hook(ParseState *pstate, Query *query, const JumbleState *jstate) #else static void profiler_fake_queryid_hook(ParseState *pstate, Query *query, JumbleState *jstate) #endif { if (prev_post_parse_analyze_hook) prev_post_parse_analyze_hook(pstate, query, jstate); /* * force fake queryid * * profiler_fake_queryid_hook is active only for one plpgsql_check regress * test. For this case, we can force fake queryid, althought is possible, * so real queryid (computed by pg_stat_statements) is already computed. * * Because this fake queryid hook is designed only for one test, I don't * check this possibility, and I don't raise any warning (because it * breaks stability of plpgsql_check regress tests - in dependency on * active (or non active) pg_stat_statements). */ query->queryId = query->commandType; } /*************************************** * * Profiler hash tables * *************************************** */ static void init_func_hk(func_hk *hk, Oid fn_oid) { memset(hk, 0, sizeof(func_hk)); hk->db_oid = MyDatabaseId; hk->fn_oid = fn_oid; } static void func_stats_ht_init(void) { HASHCTL ctl; Assert(func_stats_ht == NULL); memset(&ctl, 0, sizeof(ctl)); ctl.keysize = sizeof(func_hk); ctl.entrysize = sizeof(FuncStats); ctl.hcxt = profiler_mcxt; func_stats_ht = hash_create("plpgsql_check function execution statistics", FUNCS_PER_USER, &ctl, HASH_ELEM | HASH_BLOBS | HASH_CONTEXT); } static void proctuple_init_function_identity_hashkey(plch_fidentity_hk *hk, HeapTuple procTuple) { Form_pg_proc proc = (Form_pg_proc) GETSTRUCT(procTuple); memset(hk, 0, sizeof(plch_fidentity_hk)); hk->db_oid = MyDatabaseId; hk->fn_oid = proc->oid; hk->fn_xmin = HeapTupleHeaderGetRawXmin(procTuple->t_data); hk->fn_tid = procTuple->t_self; } static void func_stmts_stats_ht_init(void) { HASHCTL ctl; Assert(func_stmts_stats_ht == NULL); memset(&ctl, 0, sizeof(ctl)); ctl.keysize = sizeof(plch_fidentity_hk); ctl.entrysize = sizeof(FuncStmtsStats); ctl.hcxt = profiler_mcxt; func_stmts_stats_ht = hash_create("plpgsql_check function execution statistics", FUNCS_PER_USER, &ctl, HASH_ELEM | HASH_BLOBS | HASH_CONTEXT); } void plch_profiler_init_local_hash_tables(void) { if (profiler_mcxt) { MemoryContextReset(profiler_mcxt); func_stats_ht = NULL; func_stmts_stats_ht = NULL; } else { profiler_mcxt = AllocSetContextCreate(TopMemoryContext, "plpgsql_check - profiler context", ALLOCSET_DEFAULT_MINSIZE, ALLOCSET_DEFAULT_INITSIZE, ALLOCSET_DEFAULT_MAXSIZE); } func_stats_ht_init(); func_stmts_stats_ht_init(); used_stmt_stats_count = 0; } static void lxcache_ht_init(void) { HASHCTL ctl; LocalTransactionId thislxid = CURRENT_LXID; LXCache_info *lxcinfo; Assert(thislxid != InvalidLocalTransactionId); memset(&ctl, 0, sizeof(ctl)); ctl.keysize = sizeof(plch_fidentity_hk); ctl.entrysize = sizeof(LXCache); /* * Originally I used lxcache_mcxt for hashtable. Unfortunaly it doesn't * work, because hashtable creates and uses always own child context, that * is destroyed before lxcache_mcxt. Becase I need to access lxcache_ht at * transaction end, then I need to use TopMemoryContext, and then destroy * hashtab explicitly. */ ctl.hcxt = TopMemoryContext; lxcache_ht = hash_create("plpgsql_check function lxcache", FUNCS_PER_USER, &ctl, HASH_ELEM | HASH_BLOBS | HASH_CONTEXT); lxcache_mcxt = AllocSetContextCreate(TopTransactionContext, "plpgsql_check - lxcache context", ALLOCSET_DEFAULT_MINSIZE, ALLOCSET_DEFAULT_INITSIZE, ALLOCSET_DEFAULT_MAXSIZE); lxcinfo = MemoryContextAlloc(lxcache_mcxt, sizeof(LXCache_info)); lxcinfo->lxcache_mcxt = lxcache_mcxt; lxcinfo->lxcache_ht = lxcache_ht; lxcinfo->lxcache_mcb.func = lxcache_reset_callback; lxcinfo->lxcache_mcb.arg = lxcinfo; MemoryContextRegisterResetCallback(lxcache_mcxt, &lxcinfo->lxcache_mcb); lxcache_lxid = thislxid; } /* * can return NULL, when it is called transaction cleanup. */ static LXCache * get_lxcache(profiler_info *pinfo) { bool found; plch_fidentity_hk hk; LXCache *lxcache; LocalTransactionId thislxid = CURRENT_LXID; if (pinfo->lxcache && pinfo->lxcache_lxid == thislxid) return pinfo->lxcache; /* * When we lost current lxid (due aborted state), we it is better don't * create new lxcache - in this moment nothing new can be executed. */ if (thislxid == InvalidLocalTransactionId) return NULL; if (!lxcache_ht || lxcache_lxid != thislxid) lxcache_ht_init(); plch_init_fidentity_hk(&hk, pinfo->func); lxcache = (LXCache *) hash_search(lxcache_ht, (void *) &hk, HASH_ENTER, &found); if (!found) { init_fstats(&lxcache->fstats); lxcache->nstatements = pinfo->func->nstatements; lxcache->sstats = MemoryContextAlloc(lxcache_mcxt, lxcache->nstatements * sizeof(StmtStats)); memset(lxcache->sstats, 0, lxcache->nstatements * sizeof(StmtStats)); } pinfo->lxcache = lxcache; pinfo->lxcache_lxid = thislxid; return pinfo->lxcache; } /* * This callback is called at transaction end, when Top transaction context * is released. In this time we save lxcache to shared memory. */ static void lxcache_reset_callback(void *arg) { HASH_SEQ_STATUS seqstatus; LXCache *lxcache; bool raise_warning = true; LXCache_info *lxcinfo = (LXCache_info *) arg; if (skip_current_lxcache && lxcache_mcxt == lxcinfo->lxcache_mcxt) { /* * when the callback is from plpgsql_profiler_reset_all, we just clean * memory. */ hash_destroy(lxcinfo->lxcache_ht); lxcache_mcxt = NULL; lxcache_ht = NULL; lxcache_lxid = InvalidLocalTransactionId; return; } hash_seq_init(&seqstatus, lxcinfo->lxcache_ht); LWLockAcquire(profiler_ss->func_stmts_stats_lock, LW_EXCLUSIVE); LWLockAcquire(profiler_ss->func_stats_lock, LW_EXCLUSIVE); PG_TRY(); { while ((lxcache = (LXCache *) hash_seq_search(&seqstatus)) != NULL) { /* don't repeat warning for every lxcached stats */ merge_lxcached_shared_stmts_stats(lxcache, &raise_warning); merge_lxcached_shared_fstats(lxcache); } } PG_FINALLY(); { LWLockRelease(profiler_ss->func_stmts_stats_lock); LWLockRelease(profiler_ss->func_stats_lock); hash_destroy(lxcinfo->lxcache_ht); /* * Invalidate static variables if referenced to currently resetted * (released) lxcache. */ if (lxcache_mcxt == lxcinfo->lxcache_mcxt) { /* * Invalidate static variables if referenced to currently resetted * (released) lxcache. */ lxcache_mcxt = NULL; lxcache_ht = NULL; lxcache_lxid = InvalidLocalTransactionId; } } PG_END_TRY(); } /*************************************** * * Merging statistics to persistent memory * *************************************** */ static void init_fstats(FuncStats *fs) { SpinLockInit(&fs->mutex); fs->exec_count = 0; fs->exec_count_err = 0; fs->total_time = 0; fs->total_time_mean = 0.0; fs->total_time_m2 = 0.0; fs->min_time = 0; fs->max_time = 0; } static void update_fstats(FuncStats *fs, uint64 elapsed, bool aborted) { fs->total_time += elapsed; if (fs->min_time > 0) fs->min_time = fs->min_time < elapsed ? fs->min_time : elapsed; else fs->min_time = elapsed; fs->max_time = fs->max_time > elapsed ? fs->max_time : elapsed; if (aborted) fs->exec_count_err += 1; WelfordVarianceAdd(&fs->exec_count, &fs->total_time_mean, &fs->total_time_m2, ((float8) elapsed)); } static void merge_fstats(FuncStats *a, FuncStats *b) { uint64 ab_exec_count; float8 ab_total_time_mean; float8 ab_total_time_m2; WelfordVarianceMerge(&ab_exec_count, &ab_total_time_mean, &ab_total_time_m2, a->exec_count, a->total_time_mean, a->total_time_m2, b->exec_count, b->total_time_mean, b->total_time_m2); a->exec_count = ab_exec_count; a->exec_count_err += b->exec_count_err; a->total_time += b->total_time; a->total_time_mean = ab_total_time_mean; a->total_time_m2 = ab_total_time_m2; if (a->min_time > 0) a->min_time = a->min_time < b->min_time ? a->min_time : b->min_time; else a->min_time = b->min_time; a->max_time = a->max_time > b->max_time ? a->max_time : b->max_time; } static void update_local_persistent_fstats(profiler_info *pinfo, uint64 elapsed, bool aborted) { func_hk hk; FuncStats *fs; bool found; init_func_hk(&hk, pinfo->func->fn_oid); fs = (FuncStats *) hash_search(func_stats_ht, (void *) &hk, HASH_ENTER, &found); if (!found) init_fstats(fs); update_fstats(fs, elapsed, aborted); } static void update_shared_persistent_fstats(profiler_info *pinfo, uint64 elapsed, bool aborted) { func_hk hk; FuncStats *fs; bool found; bool unlock_mutex = false; /* * current statistics are buffered inside * update_shared_persistent_stmts_stats */ if (plch_use_lxcache) { LXCache *lxcache = get_lxcache(pinfo); if (lxcache) { update_fstats(&lxcache->fstats, elapsed, aborted); return; } } init_func_hk(&hk, pinfo->func->fn_oid); LWLockAcquire(profiler_ss->func_stats_lock, LW_SHARED); fs = (FuncStats *) hash_search(shared_func_stats_ht, (void *) &hk, HASH_FIND, &found); if (!found) { LWLockRelease(profiler_ss->func_stats_lock); LWLockAcquire(profiler_ss->func_stats_lock, LW_EXCLUSIVE); fs = (FuncStats *) hash_search(shared_func_stats_ht, (void *) &hk, HASH_ENTER, &found); } if (found) { SpinLockAcquire(&fs->mutex); unlock_mutex = true; } else { init_fstats(fs); SpinLockInit(&fs->mutex); } update_fstats(fs, elapsed, aborted); if (unlock_mutex) SpinLockRelease(&fs->mutex); LWLockRelease(profiler_ss->func_stats_lock); } static void update_persistent_fstats(profiler_info *pinfo, uint64 elapsed, bool aborted) { if (USE_SHARED_FUNC_STATS) update_shared_persistent_fstats(pinfo, elapsed, aborted); else update_local_persistent_fstats(pinfo, elapsed, aborted); } /* It is executed under exclusive lock */ static void merge_lxcached_shared_fstats(LXCache *lxcache) { func_hk hk; FuncStats *fs; bool found; init_func_hk(&hk, lxcache->hk.fn_oid); fs = (FuncStats *) hash_search(shared_func_stats_ht, (void *) &hk, HASH_ENTER, &found); if (!found) init_fstats(fs); merge_fstats(fs, &lxcache->fstats); } /* * Helper function - reduce repeated code */ static void merge_stmts_sstats(StmtStats *persist_ss, StmtStats *sstats, int nstatements) { int i; /* merge new statistics with persistent statistics */ for (i = 0; i < nstatements; i++) { StmtStats *_t, *_s; _t = &persist_ss[i]; _s = &sstats[i]; if (_s->has_queryid) { _t->queryid = _s->queryid; _t->has_queryid = _s->has_queryid; } if (_t->us_max < _s->us_max) _t->us_max = _s->us_max; _t->us_total += _s->us_total; _t->rows += _s->rows; _t->exec_count += _s->exec_count; _t->exec_count_err += _s->exec_count_err; _t->exec_count_cond_err += _s->exec_count_cond_err; } } static void init_stmts_sstats(StmtStats *sstats, int nstatements) { int i; /* merge new statistics with persistent statistics */ for (i = 0; i < nstatements; i++) { StmtStats *_s; _s = &sstats[i]; _s->queryid = NOQUERYID; _s->has_queryid = false; _s->us_max = 0; _s->us_total = 0; _s->rows = 0; _s->exec_count = 0; _s->exec_count_err = 0; _s->exec_count_cond_err = 0; } } static void update_local_persistent_stmts_stats(profiler_info *pinfo) { plch_fidentity_hk hk; FuncStmtsStats *fss; bool found; plch_init_fidentity_hk(&hk, pinfo->func); /* don't need too strong lock for reading shared memory */ fss = (FuncStmtsStats *) hash_search(func_stmts_stats_ht, (void *) &hk, HASH_ENTER, &found); if (!found) { fss->nstatements = pinfo->func->nstatements; if (((used_stmt_stats_count + fss->nstatements) * sizeof(StmtStats)) > (plch_max_stat_size * 1024)) { /* remove invalid current func_stmts_stats */ hash_search(func_stmts_stats_ht, &(fss->hk), HASH_REMOVE, NULL); ereport(WARNING, (errcode(ERRCODE_CONFIGURATION_LIMIT_EXCEEDED), errmsg("cannot allocate local memory for profiler statistics"), errdetail("the used memory exceeds \"plpgsql_check.max_stats_size\" (%dkB)", plch_max_stat_size), errdetail("Statistics can be cleaned by calling function \"plpgsql_profiler_reset_all()\"."))); return; } fss->sstats = MemoryContextAllocExtended(profiler_mcxt, pinfo->func->nstatements * sizeof(StmtStats), MCXT_ALLOC_NO_OOM); if (unlikely(fss->sstats == NULL)) { /* remove invalid current func_stmts_stats */ hash_search(func_stmts_stats_ht, &(fss->hk), HASH_REMOVE, NULL); ereport(ERROR, (errcode(ERRCODE_OUT_OF_MEMORY), errmsg("out of memory"))); } memcpy(fss->sstats, pinfo->sstats, pinfo->func->nstatements * sizeof(StmtStats)); fss->shared_sstats_offset = -1; used_stmt_stats_count += fss->nstatements; return; } merge_stmts_sstats(fss->sstats, pinfo->sstats, fss->nstatements); } static void update_shared_persistent_stmts_stats(profiler_info *pinfo) { plch_fidentity_hk hk; FuncStmtsStats *fss; bool found; if (plch_use_lxcache) { LXCache *lxcache = get_lxcache(pinfo); if (lxcache) { merge_stmts_sstats(lxcache->sstats, pinfo->sstats, lxcache->nstatements); return; } } plch_init_fidentity_hk(&hk, pinfo->func); LWLockAcquire(profiler_ss->func_stmts_stats_lock, LW_SHARED); /* don't need too strong lock for reading shared memory */ fss = (FuncStmtsStats *) hash_search(shared_func_stmts_stats_ht, (void *) &hk, HASH_FIND, &found); if (!found) { LWLockRelease(profiler_ss->func_stmts_stats_lock); LWLockAcquire(profiler_ss->func_stmts_stats_lock, LW_EXCLUSIVE); fss = (FuncStmtsStats *) hash_search(shared_func_stmts_stats_ht, (void *) &hk, HASH_ENTER, &found); if (!fss) elog(ERROR, "cannot to insert new entry to profiler's function statements statistics"); } if (!found) { if (profiler_ss->used_stmt_stats_count + pinfo->func->nstatements > profiler_ss->stmt_stats_count) { /* remove invalid func stmts stats entry */ hash_search(shared_func_stmts_stats_ht, (void *) &(fss->hk), HASH_REMOVE, NULL); ereport(WARNING, (errcode(ERRCODE_CONFIGURATION_LIMIT_EXCEEDED), errmsg("cannot use share memory for profiler statistics"), errdetail("the used memory exceeds \"plpgsql_check.max_stats_size\" (%dkB)", plch_max_stat_size), errhint("Statistics can be cleaned by calling function \"plpgsql_profiler_reset_all()\"."))); LWLockRelease(profiler_ss->func_stmts_stats_lock); return; } SpinLockInit(&fss->mutex); fss->sstats = NULL; fss->nstatements = pinfo->func->nstatements; fss->shared_sstats_offset = profiler_ss->used_stmt_stats_count; profiler_ss->used_stmt_stats_count += fss->nstatements; init_stmts_sstats(&SharedStmtStatsArray[fss->shared_sstats_offset], fss->nstatements); merge_stmts_sstats(&SharedStmtStatsArray[fss->shared_sstats_offset], pinfo->sstats, fss->nstatements); } else { SpinLockAcquire(&fss->mutex); merge_stmts_sstats(&SharedStmtStatsArray[fss->shared_sstats_offset], pinfo->sstats, fss->nstatements); SpinLockRelease(&fss->mutex); } LWLockRelease(profiler_ss->func_stmts_stats_lock); } static void update_persistent_stmts_stats(profiler_info *pinfo) { if (USE_SHARED_FUNC_STMTS_STATS) update_shared_persistent_stmts_stats(pinfo); else update_local_persistent_stmts_stats(pinfo); } /* * It is executed under EXCLUSIVE lock */ static void merge_lxcached_shared_stmts_stats(LXCache *lxcache, bool *raise_warning) { FuncStmtsStats *fss; bool found; fss = (FuncStmtsStats *) hash_search(shared_func_stmts_stats_ht, (void *) &lxcache->hk, HASH_ENTER, &found); if (!found) { if (profiler_ss->used_stmt_stats_count + lxcache->nstatements > profiler_ss->stmt_stats_count) { /* remove invalid func stmts stats entry */ hash_search(shared_func_stmts_stats_ht, (void *) &(fss->hk), HASH_REMOVE, NULL); if (*raise_warning) { ereport(WARNING, (errcode(ERRCODE_CONFIGURATION_LIMIT_EXCEEDED), errmsg("cannot use share memory for profiler statistics"), errdetail("the used memory exceeds \"plpgsql_check.max_stats_size\" (%dkB)", plch_max_stat_size), errhint("Statistics can be cleaned by calling function \"plpgsql_profiler_reset_all()\"."))); *raise_warning = false; } return; } SpinLockInit(&fss->mutex); fss->sstats = NULL; fss->nstatements = lxcache->nstatements; fss->shared_sstats_offset = profiler_ss->used_stmt_stats_count; init_stmts_sstats(&SharedStmtStatsArray[fss->shared_sstats_offset], fss->nstatements); profiler_ss->used_stmt_stats_count += fss->nstatements; } merge_stmts_sstats(&SharedStmtStatsArray[fss->shared_sstats_offset], lxcache->sstats, fss->nstatements); } /* * Calculate statement statistics from statement instrumentation. * statement exec time is calculated as statement total exec time * minus total exec time of nestated statements. */ typedef struct count_stmt_exec_time_context { StmtInstr *sinstrs; StmtStats *sstats; int64 nested_us_time; } count_stmt_exec_time_context; static void count_stmt_exec_time_walker(PLpgSQL_stmt *stmt, count_stmt_exec_time_context *context) { count_stmt_exec_time_context loccontext; int stmtid_0 = stmt->stmtid - 1; StmtInstr *sinstr = &context->sinstrs[stmtid_0]; StmtStats *sstats = &context->sstats[stmtid_0]; loccontext.sinstrs = context->sinstrs; loccontext.sstats = context->sstats; loccontext.nested_us_time = 0; plch_statement_tree_walker(stmt, count_stmt_exec_time_walker, NULL, &loccontext); context->nested_us_time += sinstr->us_total; sstats->has_queryid = sinstr->has_queryid; sstats->queryid = sinstr->queryid; sstats->us_max = sinstr->us_max; sstats->us_total = sinstr->us_total - loccontext.nested_us_time; sstats->rows = sinstr->rows; sstats->exec_count = sinstr->exec_count; sstats->exec_count_err = sinstr->exec_count_err; sstats->exec_count_cond_err = sinstr->exec_count_cond_err; } /*************************************** * * Profiler pldbg3 api plugin * *************************************** */ /* * pldebug3 methods * */ static bool profiler_is_active(PLpgSQL_execstate *estate, PLpgSQL_function *func) { return plpgsql_check_profiler; } static void profiler_func_setup(PLpgSQL_execstate *estate, PLpgSQL_function *func, plch_fextra *fextra) { if (OidIsValid(func->fn_oid)) { profiler_info *pinfo; pinfo = palloc0(sizeof(profiler_info)); pinfo->mcxt = CurrentMemoryContext; pinfo->nstatements = func->nstatements; pinfo->sinstrs = palloc0(func->nstatements * sizeof(StmtInstr)); pinfo->sstats = palloc0(func->nstatements * sizeof(StmtStats)); INSTR_TIME_SET_CURRENT(pinfo->start_time); pinfo->func = func; estate->plugin_info = pinfo; } } static void _profiler_func_end(profiler_info *pinfo, Oid fn_oid, bool is_aborted, plch_fextra *fextra) { instr_time end_time; uint64 elapsed; count_stmt_exec_time_context loccontext; Assert(pinfo); Assert(pinfo->func); Assert(pinfo->func->fn_oid == fn_oid); INSTR_TIME_SET_CURRENT(end_time); INSTR_TIME_SUBTRACT(end_time, pinfo->start_time); elapsed = INSTR_TIME_GET_MICROSEC(end_time); /* finalize profile - get result profile */ loccontext.sinstrs = pinfo->sinstrs; loccontext.sstats = pinfo->sstats; loccontext.nested_us_time = 0; count_stmt_exec_time_walker((PLpgSQL_stmt *) pinfo->func->action, &loccontext); update_persistent_stmts_stats(pinfo); update_persistent_fstats(pinfo, elapsed, is_aborted); } static void profiler_func_end(PLpgSQL_execstate *estate, PLpgSQL_function *func, plch_fextra *fextra) { profiler_info *pinfo = (profiler_info *) estate->plugin_info; if (!pinfo) return; _profiler_func_end(pinfo, func->fn_oid, false, fextra); } static void profiler_func_abort(PLpgSQL_execstate *estate, PLpgSQL_function *func, plch_fextra *fextra) { profiler_info *pinfo = (profiler_info *) estate->plugin_info; if (!pinfo) return; _profiler_func_end(pinfo, func->fn_oid, true, fextra); } static void profiler_stmt_beg(PLpgSQL_execstate *estate, PLpgSQL_stmt *stmt, plch_fextra *fextra) { profiler_info *pinfo = (profiler_info *) estate->plugin_info; if (pinfo) { StmtInstr *sinstr = &pinfo->sinstrs[stmt->stmtid - 1]; INSTR_TIME_SET_CURRENT(sinstr->start_time); } } static void _profiler_stmt_end(StmtInstr *sinstr, bool is_aborted) { instr_time end_time; uint64 elapsed; instr_time end_time2; INSTR_TIME_SET_CURRENT(end_time); end_time2 = end_time; INSTR_TIME_ACCUM_DIFF(sinstr->total, end_time, sinstr->start_time); INSTR_TIME_SUBTRACT(end_time2, sinstr->start_time); elapsed = INSTR_TIME_GET_MICROSEC(end_time2); if (elapsed > sinstr->us_max) sinstr->us_max = elapsed; sinstr->us_total = INSTR_TIME_GET_MICROSEC(sinstr->total); sinstr->exec_count_err += is_aborted ? 1 : 0; sinstr->exec_count++; } /* * Cleaning mode is used for closing unfinished statements after an exception. */ static void profiler_stmt_end(PLpgSQL_execstate *estate, PLpgSQL_stmt *stmt, plch_fextra *fextra) { profiler_info *pinfo = (profiler_info *) estate->plugin_info; if (pinfo) { StmtInstr *sinstr = &pinfo->sinstrs[stmt->stmtid - 1]; /* * We can get query id only if stmt_end is not executed in cleaning * mode, because we need to execute expression */ if (sinstr->queryid == NOQUERYID) sinstr->queryid = profiler_get_queryid(estate, stmt, &sinstr->has_queryid, &sinstr->qparams, pinfo->mcxt); /* Only these statements set a fresh processed-row count. */ switch (stmt->cmd_type) { case PLPGSQL_STMT_EXECSQL: case PLPGSQL_STMT_DYNEXECUTE: case PLPGSQL_STMT_PERFORM: case PLPGSQL_STMT_RETURN_QUERY: case PLPGSQL_STMT_FETCH: sinstr->rows += estate->eval_processed; break; case PLPGSQL_STMT_RETURN_NEXT: sinstr->rows++; break; default: break; } _profiler_stmt_end(sinstr, false); } } static void profiler_stmt_abort(PLpgSQL_execstate *estate, PLpgSQL_stmt *stmt, plch_fextra *fextra) { profiler_info *pinfo = (profiler_info *) estate->plugin_info; if (pinfo) { StmtInstr *sinstr = &pinfo->sinstrs[stmt->stmtid - 1]; /* A failing child statement means that a real branch was entered. */ if (stmt->cmd_type == PLPGSQL_STMT_IF && estate->err_stmt == stmt) sinstr->exec_count_cond_err++; _profiler_stmt_end(sinstr, true); } } /* * Returns true, when the expression can have side effects, or when its value * can differ between two evaluations. Returns true also when the related query * is not available, because then we cannot prove the opposite. */ static bool expr_is_volatile(PLpgSQL_expr *expr) { List *plan_sources; CachedPlanSource *plan_source; plan_sources = SPI_plan_get_plan_sources(expr->plan); if (!plan_sources) return true; plan_source = (CachedPlanSource *) linitial(plan_sources); if (!plan_source->query_list) return true; return contain_volatile_functions((Node *) linitial_node(Query, plan_source->query_list)); } /*************************************** * * queryid and fake queryid functions * *************************************** */ /* Return the first queryid found in the given PLpgSQL_stmt, if any. */ static pc_queryid profiler_get_queryid(PLpgSQL_execstate *estate, PLpgSQL_stmt *stmt, bool *has_queryid, QParams **qparams, MemoryContext mcxt) { PLpgSQL_expr *expr; bool dynamic; List *params; List *plan_sources; expr = plch_statement_get_expr(stmt, &dynamic, ¶ms, NULL); *has_queryid = (expr != NULL); /* fast leaving, when expression has not assigned plan */ if (!expr || !expr->plan) return NOQUERYID; if (dynamic) { Assert(expr); if (!plch_report_dynquery_queryid) return NOQUERYID; /* * Attention - the expression used in EXECUTE commands is executed 2x. * Unfortunatelly there is not any other way how to get queryid of * query maked by the expression. Usually this is not a problem, but * when volatile function is executed, then some unwanted side effect * can be detected. In this case the computed queryid can be false, * and looks so can be better in this case don't compute queryid. */ if (expr_is_volatile(expr)) return NOQUERYID; if (params && !*qparams) { QParams *qps = NULL; int nparams = list_length(params); int paramno = 0; ListCell *lc; /* build array of Oid used like dynamic query parameters */ qps = (QParams *) MemoryContextAlloc(mcxt, sizeof(Oid) * nparams + sizeof(int)); foreach(lc, params) { PLpgSQL_expr *param_expr = (PLpgSQL_expr *) lfirst(lc); if (!get_plpgsql_expr_type(param_expr, &qps->paramtypes[paramno++])) { pfree(qps); return NOQUERYID; } } qps->nparams = nparams; *qparams = qps; } return profiler_get_dyn_queryid(estate, expr, *qparams); } plan_sources = SPI_plan_get_plan_sources(expr->plan); if (plan_sources) { CachedPlanSource *plan_source = (CachedPlanSource *) linitial(plan_sources); if (plan_source->query_list) { Query *q = linitial_node(Query, plan_source->query_list); return q->queryId; } } return NOQUERYID; } static pc_queryid profiler_get_dyn_queryid(PLpgSQL_execstate *estate, PLpgSQL_expr *expr, QParams *qparams) { MemoryContext oldcxt; Query *query; RawStmt *parsetree; bool snapshot_set; List *parsetree_list; PLpgSQL_var result; PLpgSQL_type typ; char *query_string = NULL; Oid *paramtypes = NULL; int nparams = 0; pc_queryid result_qid; if (qparams) { paramtypes = qparams->paramtypes; nparams = qparams->nparams; } memset(&result, 0, sizeof(result)); memset(&typ, 0, sizeof(typ)); result.dtype = PLPGSQL_DTYPE_VAR; result.refname = "*auxstorage*"; result.datatype = &typ; typ.typoid = TEXTOID; typ.ttype = PLPGSQL_TTYPE_SCALAR; typ.typlen = -1; typ.typbyval = false; typ.typtype = 'b'; if (profiler_queryid_mcxt == NULL) profiler_queryid_mcxt = AllocSetContextCreate(TopMemoryContext, "plpgsql_check - profiler queryid context", ALLOCSET_DEFAULT_MINSIZE, ALLOCSET_DEFAULT_INITSIZE, ALLOCSET_DEFAULT_MAXSIZE); oldcxt = MemoryContextSwitchTo(profiler_queryid_mcxt); profiler_plugin.assign_expr(estate, (PLpgSQL_datum *) &result, expr); if (result.isnull) { MemoryContextSwitchTo(oldcxt); MemoryContextReset(profiler_queryid_mcxt); return NOQUERYID; } query_string = TextDatumGetCString(result.value); /* * Do basic parsing of the query or queries (this should be safe even if * we are in aborted transaction state!) */ parsetree_list = pg_parse_query(query_string); /* The query can be empty. Returns NOQUERYID is this case. */ if (!parsetree_list) { MemoryContextSwitchTo(oldcxt); MemoryContextReset(profiler_queryid_mcxt); return NOQUERYID; } /* * There should not be more than one query, silently ignore rather than * error out in that case. */ if (list_length(parsetree_list) > 1) { MemoryContextSwitchTo(oldcxt); MemoryContextReset(profiler_queryid_mcxt); return NOQUERYID; } /* Run through the raw parsetree and process it. */ parsetree = (RawStmt *) linitial(parsetree_list); snapshot_set = false; /* * Set up a snapshot if parse analysis/planning will need one. */ if (analyze_requires_snapshot(parsetree)) { PushActiveSnapshot(GetTransactionSnapshot()); snapshot_set = true; } query = parse_analyze_fixedparams(parsetree, query_string, paramtypes, nparams, NULL); if (snapshot_set) PopActiveSnapshot(); result_qid = query->queryId; MemoryContextSwitchTo(oldcxt); MemoryContextReset(profiler_queryid_mcxt); return result_qid; } /*************************************** * * Reporting * *************************************** */ static double fstats_stddev(FuncStats *fs) { /* Use the Welford population variance */ if (fs->exec_count == 0) return get_float8_nan(); return sqrt(fs->total_time_m2 / fs->exec_count); } static void local_iterate_over_all_profiles(plpgsql_check_result_info *ri) { HASH_SEQ_STATUS seqstatus; FuncStats *fs; hash_seq_init(&seqstatus, func_stats_ht); while ((fs = (FuncStats *) hash_seq_search(&seqstatus)) != NULL) { HeapTuple tp; /* skip dropped functions */ tp = SearchSysCache1(PROCOID, ObjectIdGetDatum(fs->hk.fn_oid)); if (!HeapTupleIsValid(tp)) continue; ReleaseSysCache(tp); plpgsql_check_put_profiler_functions_all_tb(ri, fs->hk.fn_oid, fs->exec_count, fs->exec_count_err, (double) fs->total_time, ceil(fs->total_time_mean), ceil(fstats_stddev(fs)), (double) fs->min_time, (double) fs->max_time); } } static void shared_iterate_over_all_profiles(plpgsql_check_result_info *ri) { HASH_SEQ_STATUS seqstatus; FuncStats *fs; FuncStats locfs; LWLockAcquire(profiler_ss->func_stats_lock, LW_SHARED); hash_seq_init(&seqstatus, shared_func_stats_ht); while ((fs = (FuncStats *) hash_seq_search(&seqstatus)) != NULL) { HeapTuple tp; /* * only function's statistics for current database can be displayed * here, Oid of functions from other databases has unassigned oids to * current system catalogue. */ if (fs->hk.db_oid != MyDatabaseId) continue; /* Don't hold spin lock long time */ SpinLockAcquire(&fs->mutex); memcpy(&locfs, fs, sizeof(FuncStats)); SpinLockRelease(&fs->mutex); fs = &locfs; /* check if function still exists */ tp = SearchSysCache1(PROCOID, ObjectIdGetDatum(fs->hk.fn_oid)); if (HeapTupleIsValid(tp)) { plpgsql_check_put_profiler_functions_all_tb(ri, fs->hk.fn_oid, fs->exec_count, fs->exec_count_err, (double) fs->total_time, ceil(fs->total_time_mean), ceil(fstats_stddev(fs)), (double) fs->min_time, (double) fs->max_time); ReleaseSysCache(tp); } } LWLockRelease(profiler_ss->func_stats_lock); } /* * Reports all persistent function's statistics */ void plpgsql_check_profiler_iterate_over_all_profiles(plpgsql_check_result_info *ri) { if (USE_SHARED_FUNC_STATS) shared_iterate_over_all_profiles(ri); else local_iterate_over_all_profiles(ri); } /* * Working horse for plch_statements_stats_report. * Wrapped by SQL plpgsql_profiler_function_statements_tb function * * Iterate over statement tree and fill tuplestore * * Use the cached natural statement IDs from fextra. */ typedef struct statement_stats_report_context { plpgsql_check_result_info *ri; plch_fextra *fextra; StmtStats *sstats; PLpgSQL_stmt *parent_stmt; int stmt_counter; /* used for block_num */ } statement_stats_report_context; static void statement_stats_report_walker(PLpgSQL_stmt *stmt, statement_stats_report_context *context) { statement_stats_report_context loccontext; int *naturalids = context->fextra->naturalids; if (stmt->lineno > 0) { int parent_natural_stmtid = -1; if (context->parent_stmt && context->parent_stmt->lineno > 0) parent_natural_stmtid = naturalids[context->parent_stmt->stmtid]; else parent_natural_stmtid = -1; if (context->sstats) { StmtStats *sstats = &context->sstats[stmt->stmtid - 1]; plpgsql_check_put_profile_statement(context->ri, sstats->queryid, naturalids[stmt->stmtid], parent_natural_stmtid, context->stmt_counter++, stmt->lineno, sstats->exec_count, sstats->exec_count_err, sstats->us_total, sstats->us_max, sstats->rows, plpgsql_check__stmt_typename_p(stmt)); } else { plpgsql_check_put_profile_statement_no_stats(context->ri, naturalids[stmt->stmtid], parent_natural_stmtid, context->stmt_counter++, stmt->lineno, plpgsql_check__stmt_typename_p(stmt)); } } loccontext.ri = context->ri; loccontext.fextra = context->fextra; loccontext.sstats = context->sstats; loccontext.parent_stmt = stmt; loccontext.stmt_counter = 1; plch_statement_tree_walker(stmt, statement_stats_report_walker, NULL, &loccontext); } /* * the work with local statistics are primitive, we don't need to play with locks */ static void local_statements_stats_report(plch_fidentity_hk *hk, PLpgSQL_stmt *stmt, statement_stats_report_context *context) { FuncStmtsStats *fss; bool found; fss = (FuncStmtsStats *) hash_search(func_stmts_stats_ht, (void *) hk, HASH_FIND, &found); if (found) context->sstats = fss->sstats; statement_stats_report_walker(stmt, context); } /* * do local copy of shared statement statistics */ static StmtStats * statement_stats_local_copy(FuncStmtsStats *fss) { size_t ss_size = fss->nstatements * sizeof(StmtStats); StmtStats *result; result = palloc(ss_size); SpinLockAcquire(&fss->mutex); memcpy(result, &SharedStmtStatsArray[fss->shared_sstats_offset], ss_size); SpinLockRelease(&fss->mutex); return result; } /* * shared variant is more complex, we need to protect against race condition */ static void shared_statements_stats_report(plch_fidentity_hk *hk, PLpgSQL_stmt *stmt, statement_stats_report_context *context) { FuncStmtsStats *fss; bool found; StmtStats *loc_sstats = NULL; LWLockAcquire(profiler_ss->func_stmts_stats_lock, LW_SHARED); fss = (FuncStmtsStats *) hash_search(shared_func_stmts_stats_ht, (void *) hk, HASH_FIND, &found); if (found) { loc_sstats = statement_stats_local_copy(fss); context->sstats = loc_sstats; } statement_stats_report_walker(stmt, context); if (loc_sstats) pfree(loc_sstats); LWLockRelease(profiler_ss->func_stmts_stats_lock); } /* * Build simple statement statistics report (statement profile) * * This is simple report - one statement, one line (and then there is not any * cummulation per lines). */ void plch_statements_stats_report(plpgsql_check_info *cinfo, plpgsql_check_result_info *ri) { LOCAL_FCINFO(fake_fcinfo, 0); statement_stats_report_context loccontext; plch_fidentity_hk hk; PLpgSQL_function *func; PLpgSQL_stmt *stmt; func = cinfo_get_function(cinfo, fake_fcinfo); plch_init_fidentity_hk(&hk, func); loccontext.ri = ri; loccontext.fextra = plch_get_fextra(func); loccontext.parent_stmt = NULL; loccontext.stmt_counter = 1; loccontext.sstats = NULL; stmt = (PLpgSQL_stmt *) func->action; PG_TRY(); { if (USE_SHARED_FUNC_STMTS_STATS) shared_statements_stats_report(&hk, stmt, &loccontext); else local_statements_stats_report(&hk, stmt, &loccontext); } PG_FINALLY(); { plch_release_fextra(loccontext.fextra); } PG_END_TRY(); } /* * Profiler report is more terrible becase maps statements to lines, and because * more statements can be placed on one line, then some stats are merged in an arrays. * Next complication is synchronization reported stats with function source code. */ typedef struct profiler_report_context { plpgsql_check_result_info *ri; StmtStats *sstats; MemoryContext tmp_cxt; ArrayBuildState *queryids_abs; ArrayBuildState *exec_count_abs; ArrayBuildState *exec_count_err_abs; ArrayBuildState *total_time_abs; ArrayBuildState *avg_time_abs; ArrayBuildState *max_time_abs; ArrayBuildState *processed_rows_abs; bool queryid_on_row; int buffered_stmt_lineno; int lineno; int cmds_on_row; char *src; } profiler_report_context; /* * Makes arrays from builders, push row and cleans context */ static void put_profiler_report_row(profiler_report_context *context) { char *srcrow = NULL; if (context->buffered_stmt_lineno < 0) return; while (context->lineno < context->buffered_stmt_lineno) { context->src = cutline(context->src, &srcrow); context->lineno++; if (context->lineno < context->buffered_stmt_lineno) { plpgsql_check_put_profile(context->ri, (Datum) 0, context->lineno, -1, -1, (Datum) 0, (Datum) 0, (Datum) 0, (Datum) 0, (Datum) 0, (Datum) 0, srcrow); } } #define NullableArray(context, abs) \ (((context)->abs) ? makeArrayResult((context)->abs, (context)->tmp_cxt) : (Datum) 0) if (context->cmds_on_row > 0) { plpgsql_check_put_profile(context->ri, NullableArray(context, queryids_abs), context->lineno, context->buffered_stmt_lineno, context->cmds_on_row, NullableArray(context, exec_count_abs), NullableArray(context, exec_count_err_abs), NullableArray(context, total_time_abs), NullableArray(context, avg_time_abs), NullableArray(context, max_time_abs), NullableArray(context, processed_rows_abs), srcrow); MemoryContextReset(context->tmp_cxt); context->queryids_abs = NULL; context->exec_count_abs = NULL; context->exec_count_err_abs = NULL; context->total_time_abs = NULL; context->avg_time_abs = NULL; context->max_time_abs = NULL; context->processed_rows_abs = NULL; context->buffered_stmt_lineno = -1; context->cmds_on_row = 0; } else { plpgsql_check_put_profile(context->ri, (Datum) 0, context->lineno, -1, -1, (Datum) 0, (Datum) 0, (Datum) 0, (Datum) 0, (Datum) 0, (Datum) 0, srcrow); } } static void profiler_report_walker(PLpgSQL_stmt *stmt, profiler_report_context *context) { if (stmt->lineno > 0 && stmt->lineno != context->buffered_stmt_lineno) { put_profiler_report_row(context); context->buffered_stmt_lineno = stmt->lineno; } #define accum_int64(context, abs, value) accumArrayResult(context->abs, \ Int64GetDatum((int64) (value)), \ false, \ INT8OID, \ context->tmp_cxt) #define accum_float8(context, abs, value) accumArrayResult(context->abs, \ Float8GetDatum((value)), \ false, \ FLOAT8OID, \ context->tmp_cxt) if (stmt->lineno > 0) { context->cmds_on_row += 1; if (context->sstats) { StmtStats *sstats = &context->sstats[stmt->stmtid - 1]; uint64 total_exec_count; float8 avg_time_us; if (sstats->has_queryid) context->queryids_abs = accumArrayResult(context->queryids_abs, Int64GetDatum((int64) sstats->queryid), sstats->queryid == NOQUERYID, INT8OID, context->tmp_cxt); context->exec_count_abs = accum_int64(context, exec_count_abs, sstats->exec_count); context->exec_count_err_abs = accum_int64(context, exec_count_err_abs, sstats->exec_count_err); context->total_time_abs = accum_float8(context, total_time_abs, sstats->us_total / 1000.0); total_exec_count = sstats->exec_count; if (total_exec_count > 0) avg_time_us = ceil(((float8) sstats->us_total) / total_exec_count); else avg_time_us = 0.0; context->avg_time_abs = accum_float8(context, avg_time_abs, avg_time_us / 1000.0); context->max_time_abs = accum_float8(context, max_time_abs, sstats->us_max / 1000.0); context->processed_rows_abs = accum_int64(context, processed_rows_abs, sstats->rows); } } plch_statement_tree_walker(stmt, profiler_report_walker, NULL, context); } /* * Common part for reporting local or shared profile */ static void show_profile(PLpgSQL_function *func, profiler_report_context *context) { profiler_report_walker((PLpgSQL_stmt *) func->action, context); /* send unsent buffered statistics */ put_profiler_report_row(context); /* send unprocessed source code rows */ if (context->src) { char *srcrow; while (context->src) { context->src = cutline(context->src, &srcrow); context->lineno++; plpgsql_check_put_profile(context->ri, (Datum) 0, context->lineno, -1, -1, (Datum) 0, (Datum) 0, (Datum) 0, (Datum) 0, (Datum) 0, (Datum) 0, srcrow); } } } /* * simple - no locks for access to local stats */ static void show_local_profile(PLpgSQL_function *func, plch_fidentity_hk *hk, profiler_report_context *context) { FuncStmtsStats *fss; bool found; fss = (FuncStmtsStats *) hash_search(func_stmts_stats_ht, (void *) hk, HASH_FIND, &found); if (found) context->sstats = fss->sstats; show_profile(func, context); } static void show_shared_profile(PLpgSQL_function *func, plch_fidentity_hk *hk, profiler_report_context *context) { FuncStmtsStats *fss; bool found; LWLockAcquire(profiler_ss->func_stmts_stats_lock, LW_SHARED); fss = (FuncStmtsStats *) hash_search(shared_func_stmts_stats_ht, (void *) hk, HASH_FIND, &found); if (found) { SpinLockAcquire(&fss->mutex); context->sstats = &SharedStmtStatsArray[fss->shared_sstats_offset]; } PG_TRY(); { show_profile(func, context); } PG_CATCH(); { if (found) SpinLockRelease(&fss->mutex); PG_RE_THROW(); } PG_END_TRY(); if (found) SpinLockRelease(&fss->mutex); LWLockRelease(profiler_ss->func_stmts_stats_lock); } /* * Prepare tuplestore with function profile * */ void plpgsql_check_profiler_show_profile(plpgsql_check_result_info *ri, plpgsql_check_info *cinfo) { LOCAL_FCINFO(fake_fcinfo, 0); profiler_report_context loccontext; plch_fidentity_hk hk; PLpgSQL_function *func; memset(&loccontext, 0, sizeof(profiler_report_context)); loccontext.ri = ri; loccontext.tmp_cxt = AllocSetContextCreate(CurrentMemoryContext, "profiler report temporary cxt", ALLOCSET_DEFAULT_SIZES); loccontext.buffered_stmt_lineno = -1; /* attention cinfo->src will be modified */ loccontext.src = cinfo->src; func = cinfo_get_function(cinfo, fake_fcinfo); plch_init_fidentity_hk(&hk, func); if (USE_SHARED_FUNC_STMTS_STATS) show_shared_profile(func, &hk, &loccontext); else show_local_profile(func, &hk, &loccontext); MemoryContextDelete(loccontext.tmp_cxt); } /*************************************** * Coverage calculation * *************************************** */ typedef struct coverage_statements_context { int nstatements; int nexecuted_statements; StmtStats *sstats; } coverage_statements_context; /* * Coverage statement metric is simple, just count statements and executed * statements. */ static void coverage_statements_walker(PLpgSQL_stmt *stmt, coverage_statements_context *context) { /* calculate only visible statements */ if (stmt->lineno > 0) { context->nstatements += 1; if (context->sstats && context->sstats[stmt->stmtid - 1].exec_count > 0) context->nexecuted_statements += 1; } plch_statement_tree_walker(stmt, coverage_statements_walker, NULL, context); } /* * Coverage branches is more difficult, becase we have not branch walker. * Any branch is a list of statements, and we can check of number of execution * of first statement in the list. */ typedef struct coverage_branches_context { int nbranches; int nexecuted_branches; StmtStats *sstats; } coverage_branches_context; static void increment_branch_counters(coverage_branches_context *context, List *stmts, int64 *sum_exec_count) { context->nbranches += 1; if (stmts && context->sstats) { PLpgSQL_stmt *stmt = (PLpgSQL_stmt *) linitial(stmts); int64 exec_count = context->sstats[stmt->stmtid - 1].exec_count; if (exec_count > 0) context->nexecuted_branches += 1; if (sum_exec_count) *sum_exec_count += exec_count; } } static void coverage_branches_walker(PLpgSQL_stmt *stmt, coverage_branches_context *context) { List *stmts; ListCell *lc; #define IS_PLPGSQL_STMT(stmt, typ) (stmt->cmd_type == typ) if (plch_statement_is_loop(stmt)) { stmts = plch_statement_get_loop_body(stmt); increment_branch_counters(context, stmts, NULL); } else if (IS_PLPGSQL_STMT(stmt, PLPGSQL_STMT_IF)) { PLpgSQL_stmt_if *stmt_if = (PLpgSQL_stmt_if *) stmt; int64 sum_exec_count = 0; increment_branch_counters(context, stmt_if->then_body, &sum_exec_count); foreach(lc, stmt_if->elsif_list) { stmts = ((PLpgSQL_if_elsif *) lfirst(lc))->stmts; increment_branch_counters(context, stmts, &sum_exec_count); } if (stmt_if->else_body) { increment_branch_counters(context, stmt_if->else_body, NULL); } else { context->nbranches += 1; /* * When IF has not ELSE branch, we have to calculate it with * hypothetical else branch. In this case we have a little problem * how to detect if this branch was executed. We can derived it * from IF statements execution and all real branch execution, * excluding attempts that failed while evaluating a condition. */ if (context->sstats) { StmtStats *sstats = &context->sstats[stmt->stmtid - 1]; int64 hyp_exec_count; hyp_exec_count = sstats->exec_count - sstats->exec_count_cond_err - sum_exec_count; if (hyp_exec_count > 0) context->nexecuted_branches += 1; } } } else if (IS_PLPGSQL_STMT(stmt, PLPGSQL_STMT_CASE)) { PLpgSQL_stmt_case *stmt_case = (PLpgSQL_stmt_case *) stmt; foreach(lc, stmt_case->case_when_list) { stmts = ((PLpgSQL_case_when *) lfirst(lc))->stmts; increment_branch_counters(context, stmts, NULL); } /* * The CASE in PL/pgSQL statement has not implicit ELSE path, because * when no one path is used, then the exception is raised. Example, if * we used a enum type, and if we write paths for all values in enums, * then all paths are covered - and we no able to to write legal code, * that can execute implicit else paths. */ if (stmt_case->have_else) increment_branch_counters(context, stmt_case->else_stmts, NULL); /* * CASE has not hypothetical else branch. In this case an exception is * raised. */ } else if (IS_PLPGSQL_STMT(stmt, PLPGSQL_STMT_BLOCK)) { PLpgSQL_stmt_block *stmt_block = (PLpgSQL_stmt_block *) stmt; if (stmt_block->exceptions) { /* * block without exception handling doesn't make branches, in this * case the check of execution of first statement from the list is * maybe not accurate - but it is consistent with the variant * without the exception handling. We check only first statement. */ increment_branch_counters(context, stmt_block->body, NULL); foreach(lc, stmt_block->exceptions->exc_list) { stmts = ((PLpgSQL_exception *) lfirst(lc))->action; increment_branch_counters(context, stmts, NULL); } } } plch_statement_tree_walker(stmt, coverage_branches_walker, NULL, context); } static double coverage_compute(PLpgSQL_stmt *stmt, StmtStats *sstats, int ct) { if (ct == COVERAGE_STATEMENTS) { coverage_statements_context loccontext; loccontext.nstatements = 0; loccontext.nexecuted_statements = 0; loccontext.sstats = sstats; coverage_statements_walker(stmt, &loccontext); if (loccontext.nstatements > 0) return (double) loccontext.nexecuted_statements / (double) loccontext.nstatements; else return (double) 1.0; } else { coverage_branches_context loccontext; loccontext.nbranches = 0; loccontext.nexecuted_branches = 0; loccontext.sstats = sstats; coverage_branches_walker(stmt, &loccontext); if (loccontext.nbranches > 0) return (double) loccontext.nexecuted_branches / (double) loccontext.nbranches; else return (double) 1.0; } } static double local_coverage_compute(plch_fidentity_hk *hk, PLpgSQL_stmt *stmt, int ct) { FuncStmtsStats *fss; bool found; fss = (FuncStmtsStats *) hash_search(func_stmts_stats_ht, (void *) hk, HASH_FIND, &found); return coverage_compute(stmt, found ? fss->sstats : NULL, ct); } static double shared_coverage_compute(plch_fidentity_hk *hk, PLpgSQL_stmt *stmt, int ct) { FuncStmtsStats *fss; StmtStats *sstats = NULL; bool found; double result; LWLockAcquire(profiler_ss->func_stmts_stats_lock, LW_SHARED); fss = (FuncStmtsStats *) hash_search(shared_func_stmts_stats_ht, (void *) hk, HASH_FIND, &found); if (found) sstats = statement_stats_local_copy(fss); result = coverage_compute(stmt, sstats, ct); if (sstats) pfree(sstats); LWLockRelease(profiler_ss->func_stmts_stats_lock); return result; } /* * Prepare environment for reading profile and calculation of coverage metric */ static double coverage_internal(Oid fnoid, int ct) { LOCAL_FCINFO(fake_fcinfo, 0); plpgsql_check_info cinfo; PLpgSQL_function *func; plch_fidentity_hk hk; plpgsql_check_info_init(&cinfo, fnoid); cinfo.proctuple = SearchSysCache1(PROCOID, ObjectIdGetDatum(cinfo.fn_oid)); if (!HeapTupleIsValid(cinfo.proctuple)) elog(ERROR, "cache lookup failed for function %u", cinfo.fn_oid); plpgsql_check_get_function_info(&cinfo); /* * show_profile on reduces checks - we don't need to specify relations for * triggers, because we use statements profile without type deduction * (from expressions). */ cinfo.show_profile = true; plpgsql_check_precheck_conditions(&cinfo); func = cinfo_get_function(&cinfo, fake_fcinfo); ReleaseSysCache(cinfo.proctuple); plch_init_fidentity_hk(&hk, func); if (USE_SHARED_FUNC_STMTS_STATS) return shared_coverage_compute(&hk, (PLpgSQL_stmt *) func->action, ct); else return local_coverage_compute(&hk, (PLpgSQL_stmt *) func->action, ct); } Datum plpgsql_coverage_statements_name(PG_FUNCTION_ARGS) { Oid fnoid; char *name_or_signature; if (PG_ARGISNULL(0)) elog(ERROR, "the first argument should not be null"); name_or_signature = text_to_cstring(PG_GETARG_TEXT_PP(0)); fnoid = plpgsql_check_parse_name_or_signature(name_or_signature); PG_RETURN_FLOAT8(coverage_internal(fnoid, COVERAGE_STATEMENTS)); } Datum plpgsql_coverage_branches_name(PG_FUNCTION_ARGS) { Oid fnoid; char *name_or_signature; if (PG_ARGISNULL(0)) elog(ERROR, "the first argument should not be null"); name_or_signature = text_to_cstring(PG_GETARG_TEXT_PP(0)); fnoid = plpgsql_check_parse_name_or_signature(name_or_signature); PG_RETURN_FLOAT8(coverage_internal(fnoid, COVERAGE_BRANCHES)); } Datum plpgsql_coverage_statements(PG_FUNCTION_ARGS) { Oid fnoid; if (PG_ARGISNULL(0)) elog(ERROR, "the first argument should not be null"); fnoid = PG_GETARG_OID(0); PG_RETURN_FLOAT8(coverage_internal(fnoid, COVERAGE_STATEMENTS)); } Datum plpgsql_coverage_branches(PG_FUNCTION_ARGS) { Oid fnoid; if (PG_ARGISNULL(0)) elog(ERROR, "the first argument should not be null"); fnoid = PG_GETARG_OID(0); PG_RETURN_FLOAT8(coverage_internal(fnoid, COVERAGE_BRANCHES)); } /*************************************** * * Helper functionality * *************************************** */ static void WelfordVarianceAdd(uint64 *count, float8 *mean, float8 *m2, float8 x) { float8 old_mean = *mean; *count += 1; *mean += (x - *mean) / ((float8) *count); *m2 += (x - old_mean) * (x - *mean); if (isinf(*m2)) *m2 = get_float8_nan(); } static void WelfordVarianceMerge(uint64 *count, float8 *mean, float8 *m2, uint64 a_count, float8 a_mean, float8 a_m2, uint64 b_count, float8 b_mean, float8 b_m2) { float8 delta = b_mean - a_mean; *count = a_count + b_count; *mean = (((float8) a_count) * a_mean + ((float8) b_count) * b_mean) / ((float8) *count); *m2 = a_m2 + b_m2 + delta * delta * ((float8) a_count) * ((float8) b_count) / ((float8) *count); } /* * helper function that returns compiled statement tree */ static PLpgSQL_function * cinfo_get_function(plpgsql_check_info *cinfo, FunctionCallInfo fcinfo) { FmgrInfo flinfo; TriggerData trigdata; EventTriggerData etrigdata; Trigger tg_trigger; ReturnSetInfo rsinfo; bool fake_rtd; plpgsql_check_setup_fcinfo(cinfo, &flinfo, fcinfo, &rsinfo, &trigdata, &etrigdata, &tg_trigger, &fake_rtd); return plpgsql_check__compile_p(fcinfo, false); } /* * Assigned result type of already executed (has assigned plan) expression. * * Returns false when expression was not executed or when plpgsql expression * is not an expression (has more plans or returns more columns). */ static bool get_plpgsql_expr_type(PLpgSQL_expr *expr, Oid *result_type) { if (expr) { SPIPlanPtr ptr = expr->plan; if (ptr) { List *plan_sources = SPI_plan_get_plan_sources(ptr); if (plan_sources && list_length(plan_sources) == 1) { CachedPlanSource *plan_source; TupleDesc tupdesc; plan_source = (CachedPlanSource *) linitial(plan_sources); tupdesc = plan_source->resultDesc; if (tupdesc->natts == 1) { *result_type = TupleDescAttr(tupdesc, 0)->atttypid; return true; } } } } return false; } /* * returns pointer in to next line in text or null, when there is not next line. * It modify input text. */ static char * cutline(char *str, char **line) { if (!str) { *line = NULL; return NULL; } *line = str; if (*str) { char *ptr = str; while (*ptr != '\0' && *ptr != '\n') ptr++; if (*ptr == '\n') { *ptr++ = '\0'; /* * ignore last empty rows, reduce garbage row. We cannot to skip * first usuall empty row, because we don't want to break relation * between lineno and statement's lineno. Last empty line we can * throw without any impact. */ if (*ptr == '\0') return NULL; return ptr; } } return NULL; } static HTAB * assign_shared_htab(const char *tabname, Size keysize, Size entrysize, int nelems) { HASHCTL info; memset(&info, 0, sizeof(info)); info.keysize = keysize; info.entrysize = entrysize; #if PG_VERSION_NUM >= 190000 return ShmemInitHash(tabname, nelems, &info, HASH_ELEM | HASH_BLOBS); #else return ShmemInitHash(tabname, nelems, nelems, &info, HASH_ELEM | HASH_BLOBS); #endif } okbob-plpgsql_check-800db79/src/report.c000066400000000000000000000267401526744060500202520ustar00rootroot00000000000000/*------------------------------------------------------------------------- * * report.c * * last stage checks * * by Pavel Stehule 2013-2026 * *------------------------------------------------------------------------- */ #include "plpgsql_check.h" #include "catalog/pg_proc.h" #include "catalog/pg_type.h" static bool datum_is_used(PLpgSQL_checkstate *cstate, int dno, bool write); static bool datum_is_explicit(PLpgSQL_checkstate *cstate, int dno); /* * Returns true, when variable is internal (automatic) * */ static bool is_internal(char *refname, int lineno) { if (lineno <= 0) return true; if (refname == NULL) return true; if (strcmp(refname, "*internal*") == 0) return true; if (strcmp(refname, "(unnamed row)") == 0) return true; return false; } bool plpgsql_check_is_internal_variable(PLpgSQL_checkstate *cstate, PLpgSQL_variable *var) { if (bms_is_member(var->dno, cstate->auto_variables)) return true; return is_internal(var->refname, var->lineno); } /* * returns refname of PLpgSQL_datum. When refname is generated, * then return null too, although refname is not null. */ char * plpgsql_check_datum_get_refname(PLpgSQL_checkstate *cstate, PLpgSQL_datum *d) { char *refname; int lineno; switch (d->dtype) { case PLPGSQL_DTYPE_VAR: refname = ((PLpgSQL_var *) d)->refname; lineno = ((PLpgSQL_var *) d)->lineno; break; case PLPGSQL_DTYPE_ROW: refname = ((PLpgSQL_row *) d)->refname; lineno = ((PLpgSQL_row *) d)->lineno; break; case PLPGSQL_DTYPE_REC: refname = ((PLpgSQL_rec *) d)->refname; lineno = ((PLpgSQL_rec *) d)->lineno; break; default: refname = NULL; lineno = -1; } /* * This routine is used for shadowing check. We would to check auto * variables too */ if (bms_is_member(d->dno, cstate->auto_variables)) return refname; /* * PostgreSQL 12 started use "(unnamed row)" name for internal variables. * Hide this name too (lineno is -1). */ if (is_internal(refname, lineno)) return NULL; return refname; } /* * Returns true if dno is explicitly declared. It should not be used * for arguments. */ static bool datum_is_explicit(PLpgSQL_checkstate *cstate, int dno) { PLpgSQL_execstate *estate = cstate->estate; if (bms_is_member(dno, cstate->auto_variables)) return false; switch (estate->datums[dno]->dtype) { case PLPGSQL_DTYPE_VAR: { PLpgSQL_variable *var = (PLpgSQL_variable *) estate->datums[dno]; return !is_internal(var->refname, var->lineno); } case PLPGSQL_DTYPE_ROW: { PLpgSQL_row *row = (PLpgSQL_row *) estate->datums[dno]; return !is_internal(row->refname, row->lineno); } case PLPGSQL_DTYPE_REC: { PLpgSQL_rec *rec = (PLpgSQL_rec *) estate->datums[dno]; return !is_internal(rec->refname, rec->lineno); } default: return false; } } /* * returns true, when datum or some child is used */ static bool datum_is_used(PLpgSQL_checkstate *cstate, int dno, bool write) { PLpgSQL_execstate *estate = cstate->estate; switch (estate->datums[dno]->dtype) { case PLPGSQL_DTYPE_VAR: { return bms_is_member(dno, write ? cstate->modif_variables : cstate->used_variables); } break; case PLPGSQL_DTYPE_ROW: { PLpgSQL_row *row = (PLpgSQL_row *) estate->datums[dno]; int i; if (bms_is_member(dno, write ? cstate->modif_variables : cstate->used_variables)) return true; for (i = 0; i < row->nfields; i++) { if (row->varnos[i] < 0) continue; if (datum_is_used(cstate, row->varnos[i], write)) return true; } return false; } break; case PLPGSQL_DTYPE_REC: { PLpgSQL_rec *rec = (PLpgSQL_rec *) estate->datums[dno]; int i; if (bms_is_member(dno, write ? cstate->modif_variables : cstate->used_variables)) return true; /* search any used recfield with related recparentno */ for (i = 0; i < estate->ndatums; i++) { if (estate->datums[i]->dtype == PLPGSQL_DTYPE_RECFIELD) { PLpgSQL_recfield *recfield = (PLpgSQL_recfield *) estate->datums[i]; if (recfield->recparentno == rec->dno && datum_is_used(cstate, i, write)) return true; } } } break; case PLPGSQL_DTYPE_RECFIELD: return bms_is_member(dno, write ? cstate->modif_variables : cstate->used_variables); default: return false; } return false; } /* * Reports all unused variables explicitly DECLAREd by the user. Ignores * special variables created by PL/PgSQL. */ void plpgsql_check_report_unused_variables(PLpgSQL_checkstate *cstate) { int i; PLpgSQL_execstate *estate = cstate->estate; /* now, there are no active plpgsql statement */ estate->err_stmt = NULL; for (i = 0; i < estate->ndatums; i++) if (datum_is_explicit(cstate, i) && !(datum_is_used(cstate, i, false) || datum_is_used(cstate, i, true))) { PLpgSQL_variable *var = (PLpgSQL_variable *) estate->datums[i]; StringInfoData message; initStringInfo(&message); appendStringInfo(&message, UNUSED_VARIABLE_TEXT, var->refname); plpgsql_check_put_error(cstate, 0, var->lineno, message.data, NULL, NULL, PLPGSQL_CHECK_WARNING_OTHERS, 0, NULL, NULL); pfree(message.data); message.data = NULL; } if (cstate->cinfo->extra_warnings) { PLpgSQL_function *func = estate->func; /* check never read variables */ for (i = 0; i < estate->ndatums; i++) { if (datum_is_explicit(cstate, i) && !datum_is_used(cstate, i, false) && datum_is_used(cstate, i, true)) { PLpgSQL_variable *var = (PLpgSQL_variable *) estate->datums[i]; StringInfoData message; initStringInfo(&message); appendStringInfo(&message, NEVER_READ_VARIABLE_TEXT, var->refname); plpgsql_check_put_error(cstate, 0, var->lineno, message.data, NULL, NULL, PLPGSQL_CHECK_WARNING_EXTRA, 0, NULL, NULL); pfree(message.data); message.data = NULL; } } /* check IN parameters */ for (i = 0; i < func->fn_nargs; i++) { int varno = func->fn_argvarnos[i]; bool is_read = datum_is_used(cstate, varno, false); bool is_write = datum_is_used(cstate, varno, true); if (!is_read && !is_write) { PLpgSQL_variable *var = (PLpgSQL_variable *) estate->datums[varno]; StringInfoData message; initStringInfo(&message); appendStringInfo(&message, UNUSED_PARAMETER_TEXT, var->refname); plpgsql_check_put_error(cstate, 0, 0, message.data, NULL, NULL, PLPGSQL_CHECK_WARNING_EXTRA, 0, NULL, NULL); pfree(message.data); message.data = NULL; } else if (!is_read) { bool is_inout_procedure_param = false; /* * An INOUT procedure parameter may be used only as output, so * do not warn merely because it is written but not read. */ is_inout_procedure_param = cstate->cinfo->is_procedure && bms_is_member(varno, cstate->out_variables); if (!is_inout_procedure_param) { PLpgSQL_variable *var = (PLpgSQL_variable *) estate->datums[varno]; StringInfoData message; initStringInfo(&message); appendStringInfo(&message, NEVER_READ_PARAMETER_TEXT, var->refname); plpgsql_check_put_error(cstate, 0, 0, message.data, NULL, NULL, PLPGSQL_CHECK_WARNING_EXTRA, 0, NULL, NULL); pfree(message.data); message.data = NULL; } } } /* are there some OUT parameters (expect modification)? */ if (func->out_param_varno != -1 && !cstate->found_return_query) { int varno = func->out_param_varno; PLpgSQL_variable *var = (PLpgSQL_variable *) estate->datums[varno]; if (var->dtype == PLPGSQL_DTYPE_ROW && plpgsql_check_is_internal_variable(cstate, var)) { /* this function has more OUT parameters */ PLpgSQL_row *row = (PLpgSQL_row *) var; int fnum; for (fnum = 0; fnum < row->nfields; fnum++) { int varno2 = row->varnos[fnum]; PLpgSQL_variable *var2 = (PLpgSQL_variable *) estate->datums[varno2]; StringInfoData message; if (var2->dtype == PLPGSQL_DTYPE_ROW || var2->dtype == PLPGSQL_DTYPE_REC) { /* * The result of function with more OUT variables (and * one should be an composite), is not possible simply * assign to outer variables. The related expression * cannot be "simple" expression, and then an * evaluation is 10x slower. So there is warning */ initStringInfo(&message); appendStringInfo(&message, OUT_COMPOSITE_IS_NOT_SINGLE_TEXT, var2->refname); plpgsql_check_put_error(cstate, 0, 0, message.data, NULL, NULL, PLPGSQL_CHECK_WARNING_EXTRA, 0, NULL, NULL); pfree(message.data); message.data = NULL; } if (!datum_is_used(cstate, varno2, true)) { const char *fmt = cstate->found_return_dyn_query ? MAYBE_UNMODIFIED_VARIABLE_TEXT : UNMODIFIED_VARIABLE_TEXT; const char *detail = cstate->found_return_dyn_query ? "cannot to determine result of dynamic SQL" : NULL; initStringInfo(&message); appendStringInfo(&message, fmt, var2->refname); plpgsql_check_put_error(cstate, 0, 0, message.data, detail, NULL, PLPGSQL_CHECK_WARNING_EXTRA, 0, NULL, NULL); pfree(message.data); message.data = NULL; } } } else { if (!datum_is_used(cstate, varno, true)) { StringInfoData message; const char *fmt = cstate->found_return_dyn_query ? MAYBE_UNMODIFIED_VARIABLE_TEXT : UNMODIFIED_VARIABLE_TEXT; const char *detail = cstate->found_return_dyn_query ? "cannot to determine result of dynamic SQL" : NULL; initStringInfo(&message); appendStringInfo(&message, fmt, var->refname); plpgsql_check_put_error(cstate, 0, 0, message.data, detail, NULL, PLPGSQL_CHECK_WARNING_EXTRA, 0, NULL, NULL); pfree(message.data); message.data = NULL; } } } } } /* * Report too high volatility * */ void plpgsql_check_report_too_high_volatility(PLpgSQL_checkstate *cstate) { if (cstate->cinfo->performance_warnings && !cstate->skip_volatility_check) { char *current = NULL; char *should_be = NULL; bool raise_warning = false; if (cstate->volatility == PROVOLATILE_IMMUTABLE && (cstate->decl_volatility == PROVOLATILE_VOLATILE || cstate->decl_volatility == PROVOLATILE_STABLE)) { should_be = "IMMUTABLE"; current = cstate->decl_volatility == PROVOLATILE_VOLATILE ? "VOLATILE" : "STABLE"; raise_warning = true; } else if (cstate->volatility == PROVOLATILE_STABLE && (cstate->decl_volatility == PROVOLATILE_VOLATILE)) { if (cstate->cinfo->rettype != VOIDOID) { should_be = "STABLE"; current = "VOLATILE"; raise_warning = true; } } if (raise_warning) { StringInfoData message; initStringInfo(&message); appendStringInfo(&message, "routine is marked as %s, should be %s", current, should_be); plpgsql_check_put_error(cstate, 0, -1, message.data, cstate->found_unknown_query ? "attention: cannot to determine volatility of used dynamic SQL" : NULL, "When you fix this issue, please, recheck other functions that uses this function.", PLPGSQL_CHECK_WARNING_PERFORMANCE, 0, NULL, NULL); pfree(message.data); message.data = NULL; } } } okbob-plpgsql_check-800db79/src/stmtwalk.c000066400000000000000000001663451526744060500206130ustar00rootroot00000000000000/*------------------------------------------------------------------------- * * stmtwalk.c * * iteration over plpgsql statements loop * * by Pavel Stehule 2013-2026 * *------------------------------------------------------------------------- */ #include "plpgsql_check.h" #include "access/tupconvert.h" #include "catalog/pg_collation.h" #include "catalog/pg_proc.h" #include "catalog/pg_type.h" #include "nodes/nodeFuncs.h" #include "nodes/value.h" #include "parser/parse_node.h" #include "parser/parser.h" #include "common/keywords.h" static void check_stmts(PLpgSQL_checkstate *cstate, List *stmts, int *closing, List **exceptions); static PLpgSQL_stmt_stack_item *push_stmt_to_stmt_stack(PLpgSQL_checkstate *cstate); static void pop_stmt_from_stmt_stack(PLpgSQL_checkstate *cstate, int *closing, List **exceptions); static bool is_any_loop_stmt(PLpgSQL_stmt *stmt); static bool is_inside_exception_handler(PLpgSQL_stmt_stack_item *current); static PLpgSQL_stmt *find_nearest_loop(PLpgSQL_stmt_stack_item *current); static PLpgSQL_stmt *find_stmt_with_label(char *label, PLpgSQL_stmt_stack_item *current); static int possibly_closed(int c); static int merge_closing(int c, int c_local, List **exceptions, List *exceptions_local, int err_code); static bool exception_matches_conditions(int sqlerrstate, PLpgSQL_condition *cond); static bool found_shadowed_variable(char *varname, PLpgSQL_stmt_stack_item *current, PLpgSQL_checkstate *cstate); static void check_dynamic_sql(PLpgSQL_checkstate *cstate, PLpgSQL_stmt *stmt, PLpgSQL_expr *query, bool into, PLpgSQL_variable *target, List *params); static bool is_inside_protected_block(PLpgSQL_stmt_stack_item *current); typedef struct DynSQLParams { List *args; PLpgSQL_checkstate *cstate; /* * When dynamic query uses params, then this flag will be set to true by * dynsql_param_ref callback */ bool use_params; } DynSQLParams; static void check_variable(PLpgSQL_checkstate *cstate, PLpgSQL_variable *var) { /* leave quickly when var is not defined */ if (var == NULL) return; if (var->dtype == PLPGSQL_DTYPE_ROW) { PLpgSQL_row *row = (PLpgSQL_row *) var; int fnum; for (fnum = 0; fnum < row->nfields; fnum++) { /* skip dropped columns */ if (row->varnos[fnum] < 0) continue; plpgsql_check_target(cstate, row->varnos[fnum], NULL, NULL); } plpgsql_check_record_variable_usage(cstate, row->dno, true); return; } if (var->dtype == PLPGSQL_DTYPE_REC) { PLpgSQL_rec *rec = (PLpgSQL_rec *) var; /* * There are no checks done on records currently; just record that the * variable is not unused. */ plpgsql_check_record_variable_usage(cstate, rec->dno, true); return; } elog(ERROR, "unsupported dtype %d", var->dtype); } bool plpgsql_check_is_reserved_keyword(char *name) { int i; for (i = 0; i < ScanKeywords.num_keywords; i++) { if (ScanKeywordCategories[i] == RESERVED_KEYWORD || ScanKeywordCategories[i] == TYPE_FUNC_NAME_KEYWORD) { char *value; value = unconstify(char *, GetScanKeyword(i, &ScanKeywords)); if (strcmp(name, value) == 0) return true; } } return false; } /* * does warning checks - variable shadowing, function's argument shadowing and * using keywords as variable's name */ static void check_variable_name(PLpgSQL_checkstate *cstate, PLpgSQL_stmt_stack_item *outer_stmt_stack, int dno) { char *refname; PLpgSQL_datum *d = cstate->estate->func->datums[dno]; refname = plpgsql_check_datum_get_refname(cstate, d); if (refname != NULL) { ListCell *l; bool is_auto = bms_is_member(d->dno, cstate->auto_variables); if (plpgsql_check_is_reserved_keyword(refname)) { StringInfoData str; initStringInfo(&str); appendStringInfo(&str, "name of variable \"%s\" is reserved keyword", refname); plpgsql_check_put_error(cstate, 0, 0, str.data, "The reserved keyword was used as variable name.", NULL, PLPGSQL_CHECK_WARNING_OTHERS, 0, NULL, NULL); pfree(str.data); } foreach(l, cstate->argnames) { char *argname = (char *) lfirst(l); if (strcmp(argname, refname) == 0) { StringInfoData str; initStringInfo(&str); appendStringInfo(&str, "parameter \"%s\" is shadowed", refname); plpgsql_check_put_error(cstate, 0, 0, str.data, is_auto ? "Local auto variable shadows function parameter." : "Local variable shadows function parameter.", NULL, PLPGSQL_CHECK_WARNING_OTHERS, 0, NULL, NULL); pfree(str.data); } } if (found_shadowed_variable(refname, outer_stmt_stack, cstate)) { StringInfoData str; initStringInfo(&str); appendStringInfo(&str, "%svariable \"%s\" shadows a previously defined variable", is_auto ? "auto " : "", refname); plpgsql_check_put_error(cstate, 0, 0, str.data, NULL, "SET plpgsql.extra_warnings TO 'shadowed_variables'", PLPGSQL_CHECK_WARNING_EXTRA, 0, NULL, NULL); pfree(str.data); } } } /* * walk over all plpgsql statements - search and check expressions * */ void plpgsql_check_stmt(PLpgSQL_checkstate *cstate, PLpgSQL_stmt *stmt, int *closing, List **exceptions) { TupleDesc tupdesc = NULL; PLpgSQL_function *func; ResourceOwner oldowner; MemoryContext oldCxt = CurrentMemoryContext; PLpgSQL_stmt_stack_item *outer_stmt_stack; plpgsql_check_pragma_vector pragma_vector; const char *save_err_text; if (stmt == NULL) return; if (cstate->stop_check) return; cstate->estate->err_stmt = stmt; cstate->was_pragma = false; func = cstate->estate->func; pragma_vector = cstate->pragma_vector; save_err_text = cstate->estate->err_text; /* * Attention - returns NULL, when there are not any outer level */ outer_stmt_stack = push_stmt_to_stmt_stack(cstate); oldowner = CurrentResourceOwner; BeginInternalSubTransaction(NULL); MemoryContextSwitchTo(oldCxt); PG_TRY(); { switch (stmt->cmd_type) { case PLPGSQL_STMT_BLOCK: { PLpgSQL_stmt_block *stmt_block = (PLpgSQL_stmt_block *) stmt; int i; for (i = 0; i < stmt_block->n_initvars; i++) { PLpgSQL_datum *d = func->datums[stmt_block->initvarnos[i]]; if (d->dtype == PLPGSQL_DTYPE_VAR || d->dtype == PLPGSQL_DTYPE_ROW || d->dtype == PLPGSQL_DTYPE_REC) { PLpgSQL_variable *var = (PLpgSQL_variable *) d; StringInfoData str; initStringInfo(&str); appendStringInfo(&str, "during statement block local variable \"%s\" initialization on line %d", var->refname, var->lineno); cstate->estate->err_text = str.data; if (var->default_val) plpgsql_check_assignment(cstate, var->default_val, NULL, NULL, var->dno); cstate->estate->err_text = NULL; pfree(str.data); } check_variable_name(cstate, outer_stmt_stack, d->dno); } check_variable_name(cstate, outer_stmt_stack, cstate->estate->found_varno); check_stmts(cstate, stmt_block->body, closing, exceptions); if (stmt_block->exceptions) { int closing_local; List *exceptions_local = NIL; int closing_handlers = PLPGSQL_CHECK_UNKNOWN; List *exceptions_transformed = NIL; cstate->top_stmt_stack->is_exception_handler = true; if (*closing == PLPGSQL_CHECK_CLOSED_BY_EXCEPTIONS) { ListCell *lc; ListCell *l; int errn = 0; int *err_codes = NULL; int nerr_codes = 0; /* copy errcodes to a array */ nerr_codes = list_length(*exceptions); err_codes = palloc(sizeof(int) * nerr_codes); foreach(lc, *exceptions) { err_codes[errn++] = lfirst_int(lc); } foreach(l, stmt_block->exceptions->exc_list) { PLpgSQL_exception *exception = (PLpgSQL_exception *) lfirst(l); /* * RETURN in exception handler ~ is possible * closing */ check_stmts(cstate, exception->action, &closing_local, &exceptions_local); if (*exceptions != NIL) { int idx; for (idx = 0; idx < nerr_codes; idx++) { int err_code = err_codes[idx]; if (err_code != -1 && exception_matches_conditions(err_code, exception->conditions)) { closing_handlers = merge_closing(closing_handlers, closing_local, &exceptions_transformed, exceptions_local, err_code); *exceptions = list_delete_int(*exceptions, err_code); err_codes[idx] = -1; } } } } Assert(err_codes != NULL); pfree(err_codes); if (closing_handlers != PLPGSQL_CHECK_UNKNOWN) { *closing = closing_handlers; if (closing_handlers == PLPGSQL_CHECK_CLOSED_BY_EXCEPTIONS) *exceptions = list_concat_unique_int(*exceptions, exceptions_transformed); else *exceptions = NIL; } } else { ListCell *l; closing_handlers = *closing; foreach(l, stmt_block->exceptions->exc_list) { PLpgSQL_exception *exception = (PLpgSQL_exception *) lfirst(l); /* * RETURN in exception handler ~ it is * possible closing only */ check_stmts(cstate, exception->action, &closing_local, &exceptions_local); closing_handlers = merge_closing(closing_handlers, closing_local, &exceptions_transformed, exceptions_local, -1); } *closing = closing_handlers; if (closing_handlers == PLPGSQL_CHECK_CLOSED_BY_EXCEPTIONS) *exceptions = exceptions_transformed; else *exceptions = NIL; } /* * Mark the hidden variables SQLSTATE and SQLERRM used * even if they actually weren't. Not using them * should practically never be a sign of a problem, so * there's no point in annoying the user. */ plpgsql_check_record_variable_usage(cstate, stmt_block->exceptions->sqlstate_varno, false); plpgsql_check_record_variable_usage(cstate, stmt_block->exceptions->sqlerrm_varno, false); } } break; case PLPGSQL_STMT_ASSERT: { PLpgSQL_stmt_assert *stmt_assert = (PLpgSQL_stmt_assert *) stmt; /* * Should or should not to depends on * plpgsql_check_asserts? I am thinking, so any code * (active or inactive) should be valid, so I ignore * plpgsql_check_asserts option. */ plpgsql_check_expr_with_scalar_type(cstate, stmt_assert->cond, BOOLOID, true); if (stmt_assert->message != NULL) plpgsql_check_expr(cstate, stmt_assert->message); } break; case PLPGSQL_STMT_ASSIGN: { char *local_err_text = plpgsql_check_prepare_err_text_with_target_vardecl(cstate, stmt, -1); PLpgSQL_stmt_assign *stmt_assign = (PLpgSQL_stmt_assign *) stmt; cstate->estate->err_text = local_err_text; plpgsql_check_assignment(cstate, stmt_assign->expr, NULL, NULL, stmt_assign->varno); pfree(local_err_text); cstate->estate->err_text = NULL; } break; case PLPGSQL_STMT_IF: { PLpgSQL_stmt_if *stmt_if = (PLpgSQL_stmt_if *) stmt; ListCell *l; int closing_local; int closing_all_paths = PLPGSQL_CHECK_UNKNOWN; List *exceptions_local; plpgsql_check_expr_with_scalar_type(cstate, stmt_if->cond, BOOLOID, true); check_stmts(cstate, stmt_if->then_body, &closing_local, &exceptions_local); closing_all_paths = merge_closing(closing_all_paths, closing_local, exceptions, exceptions_local, -1); foreach(l, stmt_if->elsif_list) { PLpgSQL_if_elsif *elif = (PLpgSQL_if_elsif *) lfirst(l); plpgsql_check_expr_with_scalar_type(cstate, elif->cond, BOOLOID, true); check_stmts(cstate, elif->stmts, &closing_local, &exceptions_local); closing_all_paths = merge_closing(closing_all_paths, closing_local, exceptions, exceptions_local, -1); } check_stmts(cstate, stmt_if->else_body, &closing_local, &exceptions_local); closing_all_paths = merge_closing(closing_all_paths, closing_local, exceptions, exceptions_local, -1); if (stmt_if->else_body != NULL) *closing = closing_all_paths; else if (closing_all_paths == PLPGSQL_CHECK_UNCLOSED) *closing = PLPGSQL_CHECK_UNCLOSED; else *closing = PLPGSQL_CHECK_POSSIBLY_CLOSED; } break; case PLPGSQL_STMT_CASE: { PLpgSQL_stmt_case *stmt_case = (PLpgSQL_stmt_case *) stmt; int closing_local; List *exceptions_local; ListCell *l; int closing_all_paths = PLPGSQL_CHECK_UNKNOWN; if (stmt_case->t_expr != NULL) { PLpgSQL_var *t_var = (PLpgSQL_var *) cstate->estate->datums[stmt_case->t_varno]; Oid result_oid; /* * we need to set hidden variable type */ plpgsql_check_expr_generic(cstate, stmt_case->t_expr); /* record all variables used by the query */ cstate->used_variables = bms_add_members(cstate->used_variables, stmt_case->t_expr->paramnos); tupdesc = plpgsql_check_expr_get_desc(cstate, stmt_case->t_expr, false, /* no element type */ true, /* expand record */ true, /* is expression */ &result_oid); if (tupdesc) { /* * When expected datatype is different from real, * change it. Note that what we're modifying here * is an execution copy of the datum, so this * doesn't affect the originally stored function * parse tree. */ if (t_var->datatype->typoid != result_oid) t_var->datatype = plpgsql_check__build_datatype_p(result_oid, -1, cstate->estate->func->fn_input_collation, t_var->datatype->origtypname); ReleaseTupleDesc(tupdesc); } } foreach(l, stmt_case->case_when_list) { PLpgSQL_case_when *cwt = (PLpgSQL_case_when *) lfirst(l); plpgsql_check_expr(cstate, cwt->expr); check_stmts(cstate, cwt->stmts, &closing_local, &exceptions_local); closing_all_paths = merge_closing(closing_all_paths, closing_local, exceptions, exceptions_local, -1); } if (stmt_case->else_stmts) { check_stmts(cstate, stmt_case->else_stmts, &closing_local, &exceptions_local); *closing = merge_closing(closing_all_paths, closing_local, exceptions, exceptions_local, -1); } else /* is not ensured all path evaluation */ *closing = possibly_closed(closing_all_paths); } break; case PLPGSQL_STMT_LOOP: check_stmts(cstate, ((PLpgSQL_stmt_loop *) stmt)->body, closing, exceptions); break; case PLPGSQL_STMT_WHILE: { PLpgSQL_stmt_while *stmt_while = (PLpgSQL_stmt_while *) stmt; int closing_local; List *exceptions_local; plpgsql_check_expr_with_scalar_type(cstate, stmt_while->cond, BOOLOID, true); /* * When is not guaranteed execution (possible zero loops), * then ignore closing info from body. */ check_stmts(cstate, stmt_while->body, &closing_local, &exceptions_local); *closing = possibly_closed(closing_local); } break; case PLPGSQL_STMT_FORI: { PLpgSQL_stmt_fori *stmt_fori = (PLpgSQL_stmt_fori *) stmt; int dno = stmt_fori->var->dno; int closing_local; List *exceptions_local; /* prepare plan if desn't exist yet */ plpgsql_check_assignment(cstate, stmt_fori->lower, NULL, NULL, dno); plpgsql_check_assignment(cstate, stmt_fori->upper, NULL, NULL, dno); if (stmt_fori->step) plpgsql_check_assignment(cstate, stmt_fori->step, NULL, NULL, dno); /* this variable should not be updated */ cstate->protected_variables = bms_add_member(cstate->protected_variables, dno); cstate->auto_variables = bms_add_member(cstate->auto_variables, dno); check_variable_name(cstate, outer_stmt_stack, dno); check_stmts(cstate, stmt_fori->body, &closing_local, &exceptions_local); *closing = possibly_closed(closing_local); } break; case PLPGSQL_STMT_FORS: { PLpgSQL_stmt_fors *stmt_fors = (PLpgSQL_stmt_fors *) stmt; int closing_local; List *exceptions_local; check_variable(cstate, stmt_fors->var); /* we need to set hidden variable type */ plpgsql_check_assignment_to_variable(cstate, stmt_fors->query, stmt_fors->var, -1); check_stmts(cstate, stmt_fors->body, &closing_local, &exceptions_local); *closing = possibly_closed(closing_local); } break; case PLPGSQL_STMT_FORC: { PLpgSQL_stmt_forc *stmt_forc = (PLpgSQL_stmt_forc *) stmt; PLpgSQL_var *var = (PLpgSQL_var *) func->datums[stmt_forc->curvar]; int closing_local; List *exceptions_local; check_variable(cstate, stmt_forc->var); plpgsql_check_expr_as_sqlstmt_data(cstate, stmt_forc->argquery); if (var->cursor_explicit_expr != NULL) plpgsql_check_assignment_to_variable(cstate, var->cursor_explicit_expr, stmt_forc->var, -1); check_stmts(cstate, stmt_forc->body, &closing_local, &exceptions_local); *closing = possibly_closed(closing_local); cstate->used_variables = bms_add_member(cstate->used_variables, stmt_forc->curvar); } break; case PLPGSQL_STMT_DYNFORS: { PLpgSQL_stmt_dynfors *stmt_dynfors = (PLpgSQL_stmt_dynfors *) stmt; int closing_local; List *exceptions_local; check_dynamic_sql(cstate, stmt, stmt_dynfors->query, true, stmt_dynfors->var, stmt_dynfors->params); check_stmts(cstate, stmt_dynfors->body, &closing_local, &exceptions_local); *closing = possibly_closed(closing_local); } break; case PLPGSQL_STMT_FOREACH_A: { PLpgSQL_stmt_foreach_a *stmt_foreach_a = (PLpgSQL_stmt_foreach_a *) stmt; bool use_element_type; int closing_local; List *exceptions_local; plpgsql_check_target(cstate, stmt_foreach_a->varno, NULL, NULL); /* * When slice > 0, then result and target are a array. We * shoudl to disable a array element refencing. */ use_element_type = stmt_foreach_a->slice == 0; plpgsql_check_assignment_with_possible_slices(cstate, stmt_foreach_a->expr, NULL, NULL, stmt_foreach_a->varno, use_element_type); check_stmts(cstate, stmt_foreach_a->body, &closing_local, &exceptions_local); *closing = possibly_closed(closing_local); } break; case PLPGSQL_STMT_EXIT: { PLpgSQL_stmt_exit *stmt_exit = (PLpgSQL_stmt_exit *) stmt; plpgsql_check_expr_with_scalar_type(cstate, stmt_exit->cond, BOOLOID, false); if (stmt_exit->label != NULL) { PLpgSQL_stmt *labeled_stmt = find_stmt_with_label(stmt_exit->label, outer_stmt_stack); if (labeled_stmt == NULL) ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR), errmsg("label \"%s\" does not exist", stmt_exit->label))); /* CONTINUE only allows loop labels */ if (!is_any_loop_stmt(labeled_stmt) && !stmt_exit->is_exit) ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR), errmsg("block label \"%s\" cannot be used in CONTINUE", stmt_exit->label))); } else { if (find_nearest_loop(outer_stmt_stack) == NULL) ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR), errmsg("%s cannot be used outside a loop", plpgsql_check__stmt_typename_p((PLpgSQL_stmt *) stmt_exit)))); } } break; case PLPGSQL_STMT_PERFORM: plpgsql_check_expr_as_sqlstmt(cstate, ((PLpgSQL_stmt_perform *) stmt)->expr); /* * Note: if you want to raise warning when used expressions * returns some value (other than VOID type), change previous * command plpgsql_check_expr to following * check_expr_with_expected_scalar_type. This should be not * enabled by default, because PERFORM can be used with reason * to ignore result. * * check_expr_with_expected_scalar_type(cstate, * ((PLpgSQL_stmt_perform *) stmt)->expr, VOIDOID, true); */ break; case PLPGSQL_STMT_RETURN: { PLpgSQL_stmt_return *stmt_rt = (PLpgSQL_stmt_return *) stmt; *closing = PLPGSQL_CHECK_CLOSED; if (stmt_rt->retvarno >= 0) { PLpgSQL_datum *retvar = cstate->estate->datums[stmt_rt->retvarno]; PLpgSQL_execstate *estate = cstate->estate; cstate->used_variables = bms_add_member(cstate->used_variables, stmt_rt->retvarno); switch (retvar->dtype) { case PLPGSQL_DTYPE_VAR: { PLpgSQL_var *var = (PLpgSQL_var *) retvar; Oid rettype = cstate->estate->func->fn_rettype; if (cstate->estate->retistuple) ereport(ERROR, (errcode(ERRCODE_DATATYPE_MISMATCH), errmsg("cannot return non-composite value from function returning composite type"))); plpgsql_check_assign_to_target_type(cstate, rettype, -1, var->datatype->typoid, false, retvar->dno); } break; case PLPGSQL_DTYPE_REC: { PLpgSQL_rec *rec = (PLpgSQL_rec *) retvar; /* * don't do next check, when result tuple * desc is fake */ if (recvar_tupdesc(rec) && !cstate->fake_rtd && estate->rsi && IsA(estate->rsi, ReturnSetInfo)) { TupleDesc rettupdesc = estate->rsi->expectedDesc; TupleConversionMap *tupmap; tupmap = convert_tuples_by_position(recvar_tupdesc(rec), rettupdesc, gettext_noop("returned record type does not match expected record type")); if (tupmap) free_conversion_map(tupmap); } } break; case PLPGSQL_DTYPE_ROW: { PLpgSQL_row *row = (PLpgSQL_row *) retvar; if (row->rowtupdesc && !cstate->fake_rtd && estate->rsi && IsA(estate->rsi, ReturnSetInfo)) { TupleDesc rettupdesc = estate->rsi->expectedDesc; TupleConversionMap *tupmap; tupmap = convert_tuples_by_position(row->rowtupdesc, rettupdesc, gettext_noop("returned record type does not match expected record type")); if (tupmap) free_conversion_map(tupmap); } } break; default: ; /* nope */ } if (cstate->estate->func->fn_rettype == REFCURSOROID && cstate->cinfo->compatibility_warnings) { if (!(retvar->dtype == PLPGSQL_DTYPE_VAR && ((PLpgSQL_var *) retvar)->datatype->typoid == REFCURSOROID)) { plpgsql_check_put_error(cstate, 0, 0, "obsolete setting of refcursor or cursor variable", "Internal name of cursor should not be specified by users.", NULL, PLPGSQL_CHECK_WARNING_COMPATIBILITY, 0, NULL, NULL); } } } if (stmt_rt->expr) plpgsql_check_returned_expr(cstate, stmt_rt->expr, true); } break; case PLPGSQL_STMT_RETURN_NEXT: { PLpgSQL_stmt_return_next *stmt_rn = (PLpgSQL_stmt_return_next *) stmt; if (stmt_rn->retvarno >= 0) { PLpgSQL_datum *retvar = cstate->estate->datums[stmt_rn->retvarno]; PLpgSQL_execstate *estate = cstate->estate; int natts; cstate->used_variables = bms_add_member(cstate->used_variables, stmt_rn->retvarno); if (!estate->retisset) ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR), errmsg("cannot use RETURN NEXT in a non-SETOF function"))); tupdesc = estate->tuple_store_desc; natts = tupdesc ? tupdesc->natts : 0; switch (retvar->dtype) { case PLPGSQL_DTYPE_VAR: { PLpgSQL_var *var = (PLpgSQL_var *) retvar; if (natts > 1) ereport(ERROR, (errcode(ERRCODE_DATATYPE_MISMATCH), errmsg("wrong result type supplied in RETURN NEXT"))); plpgsql_check_assign_to_target_type(cstate, cstate->estate->func->fn_rettype, -1, var->datatype->typoid, false, var->dno); } break; case PLPGSQL_DTYPE_REC: { PLpgSQL_rec *rec = (PLpgSQL_rec *) retvar; if (!HeapTupleIsValid(recvar_tuple(rec))) ereport(ERROR, (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), errmsg("record \"%s\" is not assigned yet", rec->refname), errdetail("The tuple structure of a not-yet-assigned" " record is indeterminate."))); if (tupdesc) { TupleConversionMap *tupmap; tupmap = convert_tuples_by_position(recvar_tupdesc(rec), tupdesc, gettext_noop("wrong record type supplied in RETURN NEXT")); if (tupmap) free_conversion_map(tupmap); } } break; case PLPGSQL_DTYPE_ROW: { if (tupdesc) { bool row_is_valid_result = true; PLpgSQL_row *row = (PLpgSQL_row *) retvar; if (row->nfields == natts) { int i; for (i = 0; i < natts; i++) { PLpgSQL_var *var; if (TupleDescAttr(tupdesc, i)->attisdropped) continue; if (row->varnos[i] < 0) elog(ERROR, "dropped rowtype entry for non-dropped column"); var = (PLpgSQL_var *) (cstate->estate->datums[row->varnos[i]]); if (var->datatype->typoid != TupleDescAttr(tupdesc, i)->atttypid) { row_is_valid_result = false; break; } } } else row_is_valid_result = false; if (!row_is_valid_result) ereport(ERROR, (errcode(ERRCODE_DATATYPE_MISMATCH), errmsg("wrong record type supplied in RETURN NEXT"))); } } break; default: ; /* nope */ } } if (stmt_rn->expr) plpgsql_check_returned_expr(cstate, stmt_rn->expr, true); } break; case PLPGSQL_STMT_RETURN_QUERY: { PLpgSQL_stmt_return_query *stmt_rq = (PLpgSQL_stmt_return_query *) stmt; if (stmt_rq->query) { plpgsql_check_returned_expr(cstate, stmt_rq->query, false); cstate->found_return_query = true; } if (stmt_rq->dynquery) { check_dynamic_sql(cstate, stmt, stmt_rq->dynquery, false, NULL, stmt_rq->params); } } break; case PLPGSQL_STMT_RAISE: { PLpgSQL_stmt_raise *stmt_raise = (PLpgSQL_stmt_raise *) stmt; ListCell *current_param; ListCell *l; char *cp; int err_code = 0; if (stmt_raise->condname != NULL) err_code = plpgsql_check__recognize_err_condition_p(stmt_raise->condname, true); foreach(l, stmt_raise->params) { plpgsql_check_expr(cstate, (PLpgSQL_expr *) lfirst(l)); } foreach(l, stmt_raise->options) { PLpgSQL_raise_option *opt = (PLpgSQL_raise_option *) lfirst(l); plpgsql_check_expr(cstate, opt->expr); if (opt->opt_type == PLPGSQL_RAISEOPTION_ERRCODE) { char *value; value = plpgsql_check_expr_get_string(cstate, opt->expr, NULL); if (value != NULL) err_code = plpgsql_check__recognize_err_condition_p(value, true); else err_code = -1; /* cannot be calculated now */ } } current_param = list_head(stmt_raise->params); /* ensure any single % has a own parameter */ if (stmt_raise->message != NULL) { for (cp = stmt_raise->message; *cp; cp++) { if (cp[0] == '%') { if (cp[1] == '%') { cp++; continue; } if (current_param == NULL) ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR), errmsg("too few parameters specified for RAISE"))); current_param = lnext(stmt_raise->params, current_param); } } } if (current_param != NULL) ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR), errmsg("too many parameters specified for RAISE"))); if (stmt_raise->elog_level >= ERROR) { *closing = PLPGSQL_CHECK_CLOSED_BY_EXCEPTIONS; if (err_code == 0) err_code = ERRCODE_RAISE_EXCEPTION; else if (err_code == -1) err_code = 0; /* cannot be calculated */ *exceptions = list_make1_int(err_code); } /* without any parameters it is reRAISE */ if (stmt_raise->condname == NULL && stmt_raise->message == NULL && stmt_raise->options == NIL) { if (!is_inside_exception_handler(outer_stmt_stack)) ereport(ERROR, (errcode(ERRCODE_STACKED_DIAGNOSTICS_ACCESSED_WITHOUT_ACTIVE_HANDLER), errmsg("RAISE without parameters cannot be used outside an exception handler"))); *closing = PLPGSQL_CHECK_CLOSED_BY_EXCEPTIONS; /* should be enhanced in future */ *exceptions = list_make1_int(-2); /* reRAISE */ } } break; case PLPGSQL_STMT_EXECSQL: { PLpgSQL_stmt_execsql *stmt_execsql = (PLpgSQL_stmt_execsql *) stmt; if (stmt_execsql->into) { check_variable(cstate, stmt_execsql->target); plpgsql_check_assignment_to_variable(cstate, stmt_execsql->sqlstmt, stmt_execsql->target, -1); } else /* only statement */ plpgsql_check_expr_as_sqlstmt_nodata(cstate, stmt_execsql->sqlstmt); } break; case PLPGSQL_STMT_DYNEXECUTE: { PLpgSQL_stmt_dynexecute *stmt_dynexecute = (PLpgSQL_stmt_dynexecute *) stmt; check_dynamic_sql(cstate, stmt, stmt_dynexecute->query, stmt_dynexecute->into, stmt_dynexecute->target, stmt_dynexecute->params); } break; case PLPGSQL_STMT_OPEN: { PLpgSQL_stmt_open *stmt_open = (PLpgSQL_stmt_open *) stmt; PLpgSQL_var *var = (PLpgSQL_var *) (cstate->estate->datums[stmt_open->curvar]); plpgsql_check_expr_as_sqlstmt_data(cstate, var->cursor_explicit_expr); plpgsql_check_expr_as_sqlstmt_data(cstate, stmt_open->query); if (stmt_open->query != NULL) var->cursor_explicit_expr = stmt_open->query; plpgsql_check_expr_as_sqlstmt_data(cstate, stmt_open->argquery); if (stmt_open->dynquery) { check_dynamic_sql(cstate, stmt, stmt_open->dynquery, false, NULL, stmt_open->params); if (!cstate->dyn_stmt_open) { MemoryContext oldcxt = MemoryContextSwitchTo(cstate->check_cxt); cstate->dyn_stmt_open = palloc0(cstate->estate->ndatums * sizeof(PLpgSQL_stmt_open *)); MemoryContextSwitchTo(oldcxt); } cstate->dyn_stmt_open[stmt_open->curvar] = stmt_open; } plpgsql_check_target(cstate, stmt_open->curvar, NULL, NULL); cstate->modif_variables = bms_add_member(cstate->modif_variables, stmt_open->curvar); } break; case PLPGSQL_STMT_GETDIAG: { PLpgSQL_stmt_getdiag *stmt_getdiag = (PLpgSQL_stmt_getdiag *) stmt; ListCell *lc; foreach(lc, stmt_getdiag->diag_items) { PLpgSQL_diag_item *diag_item = (PLpgSQL_diag_item *) lfirst(lc); plpgsql_check_target(cstate, diag_item->target, NULL, NULL); /* * Using GET DIAGNOSTICS stack = PG_CONTEXT is like * using other VOLATILE function. */ if (!cstate->skip_volatility_check && cstate->cinfo->performance_warnings && !stmt_getdiag->is_stacked) { if (diag_item->kind == PLPGSQL_GETDIAG_CONTEXT) cstate->volatility = PROVOLATILE_VOLATILE; } } if (stmt_getdiag->is_stacked && !is_inside_exception_handler(outer_stmt_stack)) { ereport(ERROR, (errcode(ERRCODE_STACKED_DIAGNOSTICS_ACCESSED_WITHOUT_ACTIVE_HANDLER), errmsg("GET STACKED DIAGNOSTICS cannot be used outside an exception handler"))); } } break; case PLPGSQL_STMT_FETCH: { PLpgSQL_stmt_fetch *stmt_fetch = (PLpgSQL_stmt_fetch *) stmt; PLpgSQL_var *var = (PLpgSQL_var *) (cstate->estate->datums[stmt_fetch->curvar]); check_variable(cstate, stmt_fetch->target); if (!stmt_fetch->is_move && var) { if (var->cursor_explicit_expr != NULL) { plpgsql_check_assignment_to_variable(cstate, var->cursor_explicit_expr, stmt_fetch->target, -1); } else if (cstate->dyn_stmt_open) { /* maybe, the cursor is dynamic */ PLpgSQL_stmt_open *stmt_open = cstate->dyn_stmt_open[var->dno]; if (stmt_open) { check_dynamic_sql(cstate, stmt, stmt_open->dynquery, true, stmt_fetch->target, stmt_open->params); } } } plpgsql_check_expr(cstate, stmt_fetch->expr); cstate->used_variables = bms_add_member(cstate->used_variables, stmt_fetch->curvar); } break; case PLPGSQL_STMT_CLOSE: cstate->used_variables = bms_add_member(cstate->used_variables, ((PLpgSQL_stmt_close *) stmt)->curvar); break; case PLPGSQL_STMT_COMMIT: case PLPGSQL_STMT_ROLLBACK: /* These commands are allowed only in procedures */ if (!cstate->cinfo->is_procedure) ereport(ERROR, (errcode(ERRCODE_INVALID_TRANSACTION_TERMINATION), errmsg("invalid transaction termination"))); if (is_inside_protected_block(outer_stmt_stack)) { if (stmt->cmd_type == PLPGSQL_STMT_COMMIT) ereport(ERROR, (errcode(ERRCODE_INVALID_TRANSACTION_TERMINATION), errmsg("cannot commit while a subtransaction is active"))); else ereport(ERROR, (errcode(ERRCODE_INVALID_TRANSACTION_TERMINATION), errmsg("cannot roll back while a subtransaction is active"))); } break; case PLPGSQL_STMT_CALL: { PLpgSQL_stmt_call *stmt_call = (PLpgSQL_stmt_call *) stmt; PLpgSQL_row *target; bool has_data; has_data = plpgsql_check_expr_as_sqlstmt(cstate, stmt_call->expr); /* * any check_expr_xxx should be called before * CallExprGetRowTarget */ target = plpgsql_check_CallExprGetRowTarget(cstate, stmt_call->expr); if (has_data != (target != NULL)) elog(ERROR, "plpgsql internal error, broken CALL statement"); if (target != NULL) { check_variable(cstate, (PLpgSQL_variable *) target); plpgsql_check_assignment_to_variable(cstate, stmt_call->expr, (PLpgSQL_variable *) target, -1); pfree(target->varnos); pfree(target); } } break; default: elog(ERROR, "unrecognized cmd_type: %d", stmt->cmd_type); } pop_stmt_from_stmt_stack(cstate, closing, exceptions); ReleaseCurrentSubTransaction(); MemoryContextSwitchTo(oldCxt); CurrentResourceOwner = oldowner; } PG_CATCH(); { ErrorData *edata; MemoryContextSwitchTo(oldCxt); edata = CopyErrorData(); FlushErrorState(); RollbackAndReleaseCurrentSubTransaction(); MemoryContextSwitchTo(oldCxt); CurrentResourceOwner = oldowner; pop_stmt_from_stmt_stack(cstate, closing, exceptions); if (!cstate->pragma_vector.disable_check) { /* * If fatal_errors is true, we just propagate the error up to the * highest level. Otherwise the error is appended to our current * list of errors, and we continue checking. */ if (cstate->cinfo->fatal_errors) ReThrowError(edata); else plpgsql_check_put_error_edata(cstate, edata); } /* * Restore err_text saved on entry. A statement handler may set * estate->err_text (for instance the assignment target context) and * clear it again when it returns normally. When analysing the * statement threw and we caught it here, that reset was skipped, so * the stale context would otherwise be attached to every later, * unrelated diagnostic (e.g. an unused-variable warning). */ cstate->estate->err_text = save_err_text; MemoryContextSwitchTo(oldCxt); } PG_END_TRY(); if (!cstate->was_pragma) cstate->pragma_vector = pragma_vector; else cstate->was_pragma = false; } static void invalidate_strconstvars(PLpgSQL_checkstate *cstate) { /* * We cannot safely use string constant when we leave related path (maybe * we can, but it needs deeper analyze ensure so we will process all * possible variants). */ if (cstate->top_stmts->invalidate_strconstvars) { int dno = -1; Assert(cstate->strconstvars); while ((dno = bms_next_member(cstate->top_stmts->invalidate_strconstvars, dno)) >= 0) { /* * there is an possibility so string was invalided in some nested * node. If still it is valid, invalidate it. */ if (cstate->strconstvars[dno]) { pfree(cstate->strconstvars[dno]); cstate->strconstvars[dno] = NULL; } } bms_free(cstate->top_stmts->invalidate_strconstvars); cstate->top_stmts->invalidate_strconstvars = NULL; } } /* * Ensure check for all statements in list * */ static void check_stmts(PLpgSQL_checkstate *cstate, List *stmts, int *closing, List **exceptions) { int closing_local; List *exceptions_local; plpgsql_check_pragma_vector prev_pragma_vector = cstate->pragma_vector; PLpgSQL_statements current_stmts; *closing = PLPGSQL_CHECK_UNCLOSED; *exceptions = NIL; current_stmts.outer = cstate->top_stmts; current_stmts.invalidate_strconstvars = NULL; cstate->top_stmts = ¤t_stmts; PG_TRY(); { ListCell *lc; bool dead_code_alert = false; foreach(lc, stmts) { PLpgSQL_stmt *stmt = (PLpgSQL_stmt *) lfirst(lc); closing_local = PLPGSQL_CHECK_UNCLOSED; exceptions_local = NIL; plpgsql_check_stmt(cstate, stmt, &closing_local, &exceptions_local); /* raise dead_code_alert only for visible statements */ if (dead_code_alert && stmt->lineno > 0) { plpgsql_check_put_error(cstate, 0, stmt->lineno, "unreachable code", NULL, NULL, PLPGSQL_CHECK_WARNING_EXTRA, 0, NULL, NULL); /* don't raise this warning every line */ dead_code_alert = false; } /* Diagnose dead statements without changing the reachable exits. */ if (*closing == PLPGSQL_CHECK_CLOSED || *closing == PLPGSQL_CHECK_CLOSED_BY_EXCEPTIONS) continue; if (closing_local == PLPGSQL_CHECK_CLOSED) { dead_code_alert = true; *closing = PLPGSQL_CHECK_CLOSED; *exceptions = NIL; } else if (closing_local == PLPGSQL_CHECK_CLOSED_BY_EXCEPTIONS) { dead_code_alert = true; if (*closing == PLPGSQL_CHECK_UNCLOSED || *closing == PLPGSQL_CHECK_POSSIBLY_CLOSED || *closing == PLPGSQL_CHECK_CLOSED_BY_EXCEPTIONS) { *closing = PLPGSQL_CHECK_CLOSED_BY_EXCEPTIONS; *exceptions = exceptions_local; } } else if (closing_local == PLPGSQL_CHECK_POSSIBLY_CLOSED) { if (*closing == PLPGSQL_CHECK_UNCLOSED) { *closing = PLPGSQL_CHECK_POSSIBLY_CLOSED; *exceptions = NIL; } } if (stmt->cmd_type == PLPGSQL_STMT_EXIT && !dead_code_alert) { PLpgSQL_stmt_exit *stmt_exit = (PLpgSQL_stmt_exit *) stmt; PLpgSQL_stmt_stack_item *outer_target_stmt; /* * the EXIT and the CONTINUE statements does jump out of body, * and this jump can overwrite exit status. * * In this moment, the outer_target_stmt should be correct, * because the stmt is already succesfully checked. */ outer_target_stmt = cstate->top_stmt_stack; while (outer_target_stmt) { /* set has_exit for all inner bodies */ if (stmt_exit->label) { if (outer_target_stmt->label && strcmp(stmt_exit->label, outer_target_stmt->label) == 0) { break; } } else if (is_any_loop_stmt(outer_target_stmt->stmt)) { break; } outer_target_stmt->has_exit = true; outer_target_stmt = outer_target_stmt->outer; } if (outer_target_stmt) { /* CONTINUE for LOOP cycle doesn't force cycle leaving */ if (stmt_exit->is_exit || outer_target_stmt->stmt->cmd_type != PLPGSQL_STMT_LOOP) { outer_target_stmt->has_exit = true; } } } } } PG_FINALLY(); { cstate->pragma_vector = prev_pragma_vector; cstate->was_pragma = false; invalidate_strconstvars(cstate); cstate->top_stmts = current_stmts.outer; } PG_END_TRY(); } /* * Using reserved world as label is bad practice */ static void check_label(PLpgSQL_checkstate *cstate, char *label) { if (plpgsql_check_is_reserved_keyword(label)) { StringInfoData str; initStringInfo(&str); appendStringInfo(&str, "name of label \"%s\" is reserved keyword", label); plpgsql_check_put_error(cstate, 0, 0, str.data, "The reserved keyword was used as label name.", NULL, PLPGSQL_CHECK_WARNING_OTHERS, 0, NULL, NULL); pfree(str.data); } } /* * Add label to stack of labels */ static PLpgSQL_stmt_stack_item * push_stmt_to_stmt_stack(PLpgSQL_checkstate *cstate) { PLpgSQL_stmt *stmt = cstate->estate->err_stmt; PLpgSQL_stmt_stack_item *stmt_stack_item; PLpgSQL_stmt_stack_item *current = cstate->top_stmt_stack; stmt_stack_item = (PLpgSQL_stmt_stack_item *) palloc0(sizeof(PLpgSQL_stmt_stack_item)); stmt_stack_item->stmt = stmt; switch (stmt->cmd_type) { case PLPGSQL_STMT_BLOCK: stmt_stack_item->label = ((PLpgSQL_stmt_block *) stmt)->label; break; case PLPGSQL_STMT_EXIT: stmt_stack_item->label = ((PLpgSQL_stmt_exit *) stmt)->label; break; case PLPGSQL_STMT_LOOP: stmt_stack_item->label = ((PLpgSQL_stmt_loop *) stmt)->label; break; case PLPGSQL_STMT_WHILE: stmt_stack_item->label = ((PLpgSQL_stmt_while *) stmt)->label; break; case PLPGSQL_STMT_FORI: stmt_stack_item->label = ((PLpgSQL_stmt_fori *) stmt)->label; break; case PLPGSQL_STMT_FORS: stmt_stack_item->label = ((PLpgSQL_stmt_fors *) stmt)->label; break; case PLPGSQL_STMT_FORC: stmt_stack_item->label = ((PLpgSQL_stmt_forc *) stmt)->label; break; case PLPGSQL_STMT_DYNFORS: stmt_stack_item->label = ((PLpgSQL_stmt_dynfors *) stmt)->label; break; case PLPGSQL_STMT_FOREACH_A: stmt_stack_item->label = ((PLpgSQL_stmt_foreach_a *) stmt)->label; break; default: stmt_stack_item->label = NULL; } if (stmt_stack_item->label) check_label(cstate, stmt_stack_item->label); stmt_stack_item->outer = current; cstate->top_stmt_stack = stmt_stack_item; return current; } static void pop_stmt_from_stmt_stack(PLpgSQL_checkstate *cstate, int *closing, List **exceptions) { PLpgSQL_stmt_stack_item *current = cstate->top_stmt_stack; Assert(cstate->top_stmt_stack != NULL); /* * Overwrite exit status, when statement was ended by EXIT statement. In * this case, the most valid exit state is UNKNOWN. */ if (cstate->top_stmt_stack->has_exit) { *closing = PLPGSQL_CHECK_UNKNOWN; *exceptions = NIL; } cstate->top_stmt_stack = current->outer; pfree(current); } /* * Returns true, when stmt is any loop statement */ static bool is_any_loop_stmt(PLpgSQL_stmt *stmt) { switch (stmt->cmd_type) { case PLPGSQL_STMT_LOOP: case PLPGSQL_STMT_WHILE: case PLPGSQL_STMT_FORI: case PLPGSQL_STMT_FORS: case PLPGSQL_STMT_FORC: case PLPGSQL_STMT_DYNFORS: case PLPGSQL_STMT_FOREACH_A: return true; default: return false; } } /* * Searching a any statement related to CONTINUE/EXIT statement. * label cannot be NULL. */ static PLpgSQL_stmt * find_stmt_with_label(char *label, PLpgSQL_stmt_stack_item *current) { while (current != NULL) { if (current->label != NULL && strcmp(current->label, label) == 0) return current->stmt; current = current->outer; } return NULL; } static PLpgSQL_stmt * find_nearest_loop(PLpgSQL_stmt_stack_item *current) { while (current != NULL) { if (is_any_loop_stmt(current->stmt)) return current->stmt; current = current->outer; } return NULL; } /* * Returns true, when some outer block handles exceptions. * It is used for check of correct usage of COMMIT or ROLLBACK. */ static bool is_inside_protected_block(PLpgSQL_stmt_stack_item *current) { while (current != NULL) { if (current->stmt->cmd_type == PLPGSQL_STMT_BLOCK) { PLpgSQL_stmt_block *stmt_block = (PLpgSQL_stmt_block *) current->stmt; if (stmt_block->exceptions && !current->is_exception_handler) return true; } current = current->outer; } return false; } /* * This is used for check of correct usage GET STACKED DIAGNOSTICS */ static bool is_inside_exception_handler(PLpgSQL_stmt_stack_item *current) { while (current != NULL) { if (current->stmt->cmd_type == PLPGSQL_STMT_BLOCK) { PLpgSQL_stmt_block *stmt_block = (PLpgSQL_stmt_block *) current->stmt; if (stmt_block->exceptions && current->is_exception_handler) return true; } current = current->outer; } return false; } /* * returns false, when a variable doesn't shadows any other variable */ static bool found_shadowed_variable(char *varname, PLpgSQL_stmt_stack_item *current, PLpgSQL_checkstate *cstate) { while (current != NULL) { if (current->stmt->cmd_type == PLPGSQL_STMT_BLOCK) { PLpgSQL_stmt_block *stmt_block = (PLpgSQL_stmt_block *) current->stmt; int i; for (i = 0; i < stmt_block->n_initvars; i++) { char *refname; PLpgSQL_datum *d; d = cstate->estate->func->datums[stmt_block->initvarnos[i]]; refname = plpgsql_check_datum_get_refname(cstate, d); if (refname != NULL && strcmp(refname, varname) == 0) return true; } } current = current->outer; } return false; } /* * Reduce ending states of execution paths. * */ static int possibly_closed(int c) { switch (c) { case PLPGSQL_CHECK_CLOSED: case PLPGSQL_CHECK_CLOSED_BY_EXCEPTIONS: case PLPGSQL_CHECK_POSSIBLY_CLOSED: return PLPGSQL_CHECK_POSSIBLY_CLOSED; default: return PLPGSQL_CHECK_UNCLOSED; } } /* * Deduce ending state of execution paths. * */ static int merge_closing(int c, int c_local, List **exceptions, List *exceptions_local, int err_code) { if (c_local == PLPGSQL_CHECK_CLOSED_BY_EXCEPTIONS && err_code != -1) { List *normalized = NIL; ListCell *lc; /* replace reRAISE symbol (-2) by real err_code */ foreach(lc, exceptions_local) { int t_err_code = lfirst_int(lc); normalized = list_append_unique_int(normalized, t_err_code != -2 ? t_err_code : err_code); } exceptions_local = normalized; } /* * initial state - result state is copy of local state */ if (c == PLPGSQL_CHECK_UNKNOWN) { if (c_local == PLPGSQL_CHECK_CLOSED_BY_EXCEPTIONS) *exceptions = exceptions_local; else *exceptions = NIL; return c_local; } /* * Do nothing when new state is UNKNOWN */ if (c_local == PLPGSQL_CHECK_UNKNOWN) return c; /* * When both first and second path has same closing state, the result is * same. Concat exceptions when closing state is an exception. */ if (c == c_local) { if (c == PLPGSQL_CHECK_CLOSED_BY_EXCEPTIONS) { *exceptions = list_concat_unique_int(*exceptions, exceptions_local); } return c_local; } /* * When one path ending by RETURN and second by an exception, then we * know, so the result is closed paths. It is more practical to return * PLPGSQL_CHECK_CLOSED_BY_EXCEPTIONS, because we don't lost a list of * exceptions. */ if (c == PLPGSQL_CHECK_CLOSED || c_local == PLPGSQL_CHECK_CLOSED) { if (c_local == PLPGSQL_CHECK_CLOSED_BY_EXCEPTIONS) { *exceptions = exceptions_local; return PLPGSQL_CHECK_CLOSED_BY_EXCEPTIONS; } else if (c == PLPGSQL_CHECK_CLOSED_BY_EXCEPTIONS) { return PLPGSQL_CHECK_CLOSED_BY_EXCEPTIONS; } } *exceptions = NIL; /* * common combination is CLOSED and UNCLOSED, maybe there is a possibility * to be more accurate, but the result can be some between. */ return PLPGSQL_CHECK_POSSIBLY_CLOSED; } /* * Returns true, if exception with sqlerrstate is handled. * */ static bool exception_matches_conditions(int sqlerrstate, PLpgSQL_condition *cond) { for (; cond != NULL; cond = cond->next) { int _sqlerrstate = cond->sqlerrstate; /* * OTHERS matches everything *except* query-canceled and * assert-failure. If you're foolish enough, you can match those * explicitly. */ if (_sqlerrstate == 0) { if (sqlerrstate != ERRCODE_QUERY_CANCELED && sqlerrstate != ERRCODE_ASSERT_FAILURE) return true; } /* Exact match? */ else if (sqlerrstate == _sqlerrstate) return true; /* Category match? */ else if (ERRCODE_IS_CATEGORY(_sqlerrstate) && ERRCODE_TO_CATEGORY(sqlerrstate) == _sqlerrstate) return true; } return false; } /* * Dynamic SQL processing. * * When dynamic query is constant, we can do same work like with * static SQL. */ static Node * dynsql_param_ref(ParseState *pstate, ParamRef *pref) { DynSQLParams *params = (DynSQLParams *) pstate->p_ref_hook_state; List *args = params->args; int nargs = list_length(args); Param *param = NULL; PLpgSQL_expr *expr; TupleDesc tupdesc; if (pref->number < 1 || pref->number > nargs) ereport(ERROR, (errcode(ERRCODE_UNDEFINED_PARAMETER), errmsg("there is no parameter $%d", pref->number), parser_errposition(pstate, pref->location))); expr = (PLpgSQL_expr *) list_nth(args, pref->number - 1); tupdesc = plpgsql_check_expr_get_desc(params->cstate, expr, false, false, true, NULL); if (tupdesc && tupdesc->natts > 0) { param = makeNode(Param); param->paramkind = PARAM_EXTERN; param->paramid = pref->number; param->paramtype = TupleDescAttr(tupdesc, 0)->atttypid; param->location = pref->location; /* * SPI_execute_with_args doesn't allow pass typmod. */ param->paramtypmod = -1; param->paramcollid = InvalidOid; ReleaseTupleDesc(tupdesc); } else elog(ERROR, "cannot to detect type of $%d parameter", pref->number); params->use_params = true; return (Node *) param; } /* * Dynamic query requires own setup. In reality it is executed by * different SPI, here we need to emulate different environment. * Parameters are not mapped to function parameters, but to USING * clause expressions. */ static void dynsql_parser_setup(struct ParseState *pstate, void *params) { pstate->p_pre_columnref_hook = NULL; pstate->p_post_columnref_hook = NULL; pstate->p_paramref_hook = dynsql_param_ref; pstate->p_ref_hook_state = (void *) params; } /* * Returns true if record variable has assigned some type */ static bool has_assigned_tupdesc(PLpgSQL_checkstate *cstate, PLpgSQL_rec *rec) { PLpgSQL_rec *target; Assert(rec); target = (PLpgSQL_rec *) (cstate->estate->datums[rec->dno]); Assert(rec->dtype == PLPGSQL_DTYPE_REC); if (recvar_tupdesc(target)) return true; return false; } static int plpgsql_check_expr_generic_with_parser_setup_safe(PLpgSQL_checkstate *cstate, PLpgSQL_expr *expr, ParserSetupHook parser_setup, void *arg) { MemoryContext oldCxt; ResourceOwner oldowner; volatile bool is_ok = true; oldCxt = CurrentMemoryContext; oldowner = CurrentResourceOwner; BeginInternalSubTransaction(NULL); MemoryContextSwitchTo(cstate->check_cxt); PG_TRY(); { plpgsql_check_expr_generic_with_parser_setup(cstate, expr, parser_setup, arg); RollbackAndReleaseCurrentSubTransaction(); MemoryContextSwitchTo(oldCxt); CurrentResourceOwner = oldowner; } PG_CATCH(); { is_ok = false; MemoryContextSwitchTo(oldCxt); FlushErrorState(); RollbackAndReleaseCurrentSubTransaction(); MemoryContextSwitchTo(oldCxt); CurrentResourceOwner = oldowner; } PG_END_TRY(); return is_ok; } static void check_dynamic_sql(PLpgSQL_checkstate *cstate, PLpgSQL_stmt *stmt, PLpgSQL_expr *query, bool into, PLpgSQL_variable *target, List *params) { Node *expr_node; ListCell *l; int loc = -1; char *dynquery = NULL; volatile bool expr_is_const = false; volatile bool raise_unknown_rec_warning = false; volatile bool known_type_of_dynexpr = false; /* * possible checks: * * 1. When expression is string literal, then we can check this query * similary like cursor query with parameters. When this query has not a * parameters, and it is not DDL, DML, then we can raise a performance * warning'. * * 2. When expression is real expression, then we should to check any * string kind parameters if are sanitized by functions quote_ident, * qoute_literal, or format. * * 3. When expression is based on calling format function, and there are * used only placeholders %I and %L, then we can try to check syntax of * embeded query. */ foreach(l, params) { plpgsql_check_expr(cstate, (PLpgSQL_expr *) lfirst(l)); } plpgsql_check_expr(cstate, query); expr_node = plpgsql_check_expr_get_node(cstate, query, false); /* * The query is checked before by PL/pgSQL parser. And then result of * plpgsql_check_expr_get_node should be not NULL every time. */ Assert(expr_node); if (IsA(expr_node, FuncExpr)) { FuncExpr *fexpr = (FuncExpr *) expr_node; if (fexpr->funcid == FORMAT_0PARAM_OID || fexpr->funcid == FORMAT_NPARAM_OID) { char *fmt = NULL; bool found_ident_placeholder = false; bool found_literal_placeholder = false; bool _expr_is_const; if (fexpr->args) fmt = plpgsql_check_get_const_string(cstate, linitial(fexpr->args), NULL); if (fmt) { char *fstr; MemoryContext oldcxt; fstr = plpgsql_check_get_formatted_string(cstate, fmt, fexpr, &found_ident_placeholder, &found_literal_placeholder, &_expr_is_const); /* * fix passing 'volatile bool *' to parameter of type 'bool *' * discards qualifiers */ expr_is_const = _expr_is_const; if (fstr) { if (!found_literal_placeholder) { oldcxt = CurrentMemoryContext; PG_TRY(); { raw_parser(fstr, RAW_PARSE_DEFAULT); } PG_CATCH(); { ErrorData *errdata; MemoryContextSwitchTo(oldcxt); errdata = CopyErrorData(); FlushErrorState(); errdata->internalquery = fstr; ThrowErrorData(errdata); } PG_END_TRY(); } if (!found_ident_placeholder) dynquery = fstr; } } } } else { dynquery = plpgsql_check_get_const_string(cstate, expr_node, NULL); expr_is_const = (dynquery != NULL); } if (dynquery) { PLpgSQL_expr *dynexpr = NULL; DynSQLParams dsp; volatile bool is_ok = true; dynexpr = palloc0(sizeof(PLpgSQL_expr)); dynexpr->expr_rw_param = NULL; dynexpr->query = dynquery; dsp.args = params; dsp.cstate = cstate; dsp.use_params = false; PG_TRY(); { cstate->allow_mp = true; cstate->is_dynsql = true; cstate->found_mp = false; if (expr_is_const) { plpgsql_check_expr_generic_with_parser_setup(cstate, dynexpr, (ParserSetupHook) dynsql_parser_setup, &dsp); } else { /* * When dynquery is not really constant, then there are * possible false alarms because we try to replace string * literal by parameter, so we can use it just for type * detection when check is ok. ` */ is_ok = plpgsql_check_expr_generic_with_parser_setup_safe(cstate, dynexpr, (ParserSetupHook) dynsql_parser_setup, &dsp); if (!is_ok) cstate->found_unknown_query = true; } if (is_ok && expr_is_const && !cstate->found_mp && (!params || !dsp.use_params)) { /* probably useless dynamic command */ plpgsql_check_put_error(cstate, 0, 0, "immutable expression without parameters found", "the EXECUTE command is not necessary probably", "Don't use dynamic SQL when you can use static SQL.", PLPGSQL_CHECK_WARNING_PERFORMANCE, 0, NULL, NULL); } if (is_ok && params && !dsp.use_params) { plpgsql_check_put_error(cstate, 0, 0, "values passed to EXECUTE statement by USING clause was not used", NULL, NULL, PLPGSQL_CHECK_WARNING_OTHERS, 0, NULL, NULL); } if (is_ok && dynexpr->plan) { known_type_of_dynexpr = true; if (stmt->cmd_type == PLPGSQL_STMT_RETURN_QUERY) { plpgsql_check_returned_expr_with_parser_setup(cstate, dynexpr, false, (ParserSetupHook) dynsql_parser_setup, &dsp); cstate->found_return_query = true; } else if (into) { check_variable(cstate, target); plpgsql_check_assignment_to_variable_with_parser_setup(cstate, dynexpr, target, -1, (ParserSetupHook) dynsql_parser_setup, &dsp); } } } PG_FINALLY(); { cstate->allow_mp = false; cstate->is_dynsql = false; cstate->found_mp = false; } PG_END_TRY(); } else /* when we have not dynquery, we cannot to do volatility check */ cstate->found_unknown_query = true; if (!expr_is_const) { /* * execute string is not constant (is not safe), but we can check * sanitize parameters. */ if (cstate->cinfo->security_warnings && plpgsql_check_is_sql_injection_vulnerable(cstate, query, expr_node, &loc)) { if (loc != -1) plpgsql_check_put_error(cstate, 0, 0, "text type variable is not sanitized", "The EXECUTE expression is SQL injection vulnerable.", "Use quote_ident, quote_literal or format function to secure variable.", PLPGSQL_CHECK_WARNING_SECURITY, loc, query->query, NULL); else plpgsql_check_put_error(cstate, 0, 0, "the expression is not SQL injection safe", "Cannot ensure so dynamic EXECUTE statement is SQL injection secure.", "Use quote_ident, quote_literal or format function to secure variable.", PLPGSQL_CHECK_WARNING_SECURITY, -1, query->query, NULL); } /* in this case we don't know number of output columns */ if (stmt->cmd_type == PLPGSQL_STMT_RETURN_QUERY && !known_type_of_dynexpr) { cstate->found_return_dyn_query = true; } /* * In this case, we don't know a result type, and we should to raise * warning about this situation. */ if (into && !known_type_of_dynexpr) { if (target->dtype == PLPGSQL_DTYPE_REC) raise_unknown_rec_warning = true; } } /* recheck if target rec var has assigned tupdesc */ if (into) { Assert(target); check_variable(cstate, target); if (raise_unknown_rec_warning || (target->dtype == PLPGSQL_DTYPE_REC && !has_assigned_tupdesc(cstate, (PLpgSQL_rec *) target))) { if (!bms_is_member(target->dno, cstate->typed_variables)) plpgsql_check_put_error(cstate, 0, 0, "cannot determinate a result of dynamic SQL", "There is a risk of related false alarms.", "Don't use dynamic SQL and record type together, when you would check function.", PLPGSQL_CHECK_WARNING_OTHERS, 0, NULL, NULL); } } } okbob-plpgsql_check-800db79/src/tablefunc.c000066400000000000000000000474511526744060500207040ustar00rootroot00000000000000/*------------------------------------------------------------------------- * * tablefunc.c * * top functions - display results in table format * * by Pavel Stehule 2013-2026 * *------------------------------------------------------------------------- */ #include "plpgsql_check.h" #include "plpgsql_check_builtins.h" #include "catalog/pg_type.h" #include "utils/array.h" #include "utils/builtins.h" #include "utils/syscache.h" static void SetReturningFunctionCheck(ReturnSetInfo *rsinfo); PG_FUNCTION_INFO_V1(plpgsql_check_function); PG_FUNCTION_INFO_V1(plpgsql_make_pragma); PG_FUNCTION_INFO_V1(plpgsql_check_function_tb); PG_FUNCTION_INFO_V1(plpgsql_show_dependency_tb); PG_FUNCTION_INFO_V1(plpgsql_profiler_function_tb); PG_FUNCTION_INFO_V1(plpgsql_profiler_function_statements_tb); PG_FUNCTION_INFO_V1(plpgsql_check_function_name); PG_FUNCTION_INFO_V1(plpgsql_check_function_tb_name); PG_FUNCTION_INFO_V1(plpgsql_show_dependency_tb_name); PG_FUNCTION_INFO_V1(plpgsql_profiler_function_tb_name); PG_FUNCTION_INFO_V1(plpgsql_profiler_function_statements_tb_name); PG_FUNCTION_INFO_V1(plpgsql_profiler_functions_all_tb); #define ERR_NULL_OPTION(option) ereport(ERROR, \ (errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED), \ errmsg("the option \"" option "\" is NULL"), \ errhint("this option should not be NULL"))) /* * Validate function result description * */ static void SetReturningFunctionCheck(ReturnSetInfo *rsinfo) { /* check to see if caller supports us returning a tuplestore */ if (rsinfo == NULL || !IsA(rsinfo, ReturnSetInfo)) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("set-valued function called in context that cannot accept a set"))); if (!(rsinfo->allowedModes & SFRM_Materialize)) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("materialize mode required, but it is not allowed in this context"))); } void plpgsql_check_info_init(plpgsql_check_info *cinfo, Oid fn_oid) { memset(cinfo, 0, sizeof(*cinfo)); cinfo->fn_oid = fn_oid; cinfo->anyelementoid = INT4OID; cinfo->anyrangeoid = INT4RANGEOID; cinfo->anycompatibleoid = INT4OID; cinfo->anycompatiblerangeoid = INT4RANGEOID; } void plpgsql_check_set_all_warnings(plpgsql_check_info *cinfo) { cinfo->other_warnings = true; cinfo->performance_warnings = true; cinfo->extra_warnings = true; cinfo->security_warnings = true; cinfo->compatibility_warnings = true; } void plpgsql_check_set_without_warnings(plpgsql_check_info *cinfo) { cinfo->other_warnings = false; cinfo->performance_warnings = false; cinfo->extra_warnings = false; cinfo->security_warnings = false; cinfo->compatibility_warnings = false; } /* * Transform the value of "pragmas" option (text array) to list of * pragma strings. NULL elements are ignored. */ static List * get_pragmas_list(ArrayType *pragmas_array) { List *result = NIL; Datum *elems; bool *nulls; int nelems; int i; deconstruct_array(pragmas_array, TEXTOID, -1, false, TYPALIGN_INT, &elems, &nulls, &nelems); for (i = 0; i < nelems; i++) { if (!nulls[i]) result = lappend(result, TextDatumGetCString(elems[i])); } return result; } /* * plpgsql_check_function * * Extended check with formatted text output * */ static Datum check_function_internal(Oid fnoid, FunctionCallInfo fcinfo) { plpgsql_check_info cinfo; plpgsql_check_result_info ri; ReturnSetInfo *rsinfo; ErrorContextCallback *prev_errorcontext; int format; plpgsql_check_check_ext_version(fcinfo->flinfo->fn_oid); Assert(PG_NARGS() == 22); /* check to see if caller supports us returning a tuplestore */ rsinfo = (ReturnSetInfo *) fcinfo->resultinfo; SetReturningFunctionCheck(rsinfo); if (PG_ARGISNULL(1)) ERR_NULL_OPTION("relid"); if (PG_ARGISNULL(2)) ERR_NULL_OPTION("format"); if (PG_ARGISNULL(3)) ERR_NULL_OPTION("fatal_errors"); if (PG_ARGISNULL(4)) ERR_NULL_OPTION("other_warnings"); if (PG_ARGISNULL(5)) ERR_NULL_OPTION("performance_warnings"); if (PG_ARGISNULL(6)) ERR_NULL_OPTION("extra_warnings"); if (PG_ARGISNULL(7)) ERR_NULL_OPTION("security_warnings"); if (PG_ARGISNULL(8)) ERR_NULL_OPTION("compatibility_warnings"); if (PG_ARGISNULL(11)) ERR_NULL_OPTION("anyelementtype"); if (PG_ARGISNULL(12)) ERR_NULL_OPTION("anyenumtype"); if (PG_ARGISNULL(13)) ERR_NULL_OPTION("anyrangetype"); if (PG_ARGISNULL(14)) ERR_NULL_OPTION("anycompatibletype"); if (PG_ARGISNULL(15)) ERR_NULL_OPTION("anycompatiblerangetype"); if (PG_ARGISNULL(16)) ERR_NULL_OPTION("without_warnings"); if (PG_ARGISNULL(17)) ERR_NULL_OPTION("all_warnings"); if (PG_ARGISNULL(18)) ERR_NULL_OPTION("use_incomment_options"); if (PG_ARGISNULL(19)) ERR_NULL_OPTION("incomment_options_usage_warning"); if (PG_ARGISNULL(20)) ERR_NULL_OPTION("constants_tracing"); format = plpgsql_check_format_num(text_to_cstring(PG_GETARG_TEXT_PP(2))); plpgsql_check_info_init(&cinfo, fnoid); cinfo.relid = PG_GETARG_OID(1); cinfo.fatal_errors = PG_GETARG_BOOL(3); cinfo.other_warnings = PG_GETARG_BOOL(4); cinfo.performance_warnings = PG_GETARG_BOOL(5); cinfo.extra_warnings = PG_GETARG_BOOL(6); cinfo.security_warnings = PG_GETARG_BOOL(7); cinfo.compatibility_warnings = PG_GETARG_BOOL(8); cinfo.incomment_options_usage_warning = PG_GETARG_BOOL(19); cinfo.constants_tracing = PG_GETARG_BOOL(20); if (!PG_ARGISNULL(21)) cinfo.pragmas = get_pragmas_list(PG_GETARG_ARRAYTYPE_P(21)); /* without_warnings */ if (PG_GETARG_BOOL(16)) { if (PG_GETARG_BOOL(17)) ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("without_warnings and all_warnings cannot be true same time"))); plpgsql_check_set_without_warnings(&cinfo); } /* all warnings */ else if (PG_GETARG_BOOL(17)) { if (PG_GETARG_BOOL(16)) ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("without_warnings and all_warnings cannot be true same time"))); plpgsql_check_set_all_warnings(&cinfo); } if (PG_ARGISNULL(9)) cinfo.oldtable = NULL; else cinfo.oldtable = NameStr(*(PG_GETARG_NAME(9))); if (PG_ARGISNULL(10)) cinfo.newtable = NULL; else cinfo.newtable = NameStr(*(PG_GETARG_NAME(10))); cinfo.anyelementoid = PG_GETARG_OID(11); cinfo.anyenumoid = PG_GETARG_OID(12); cinfo.anyrangeoid = PG_GETARG_OID(13); cinfo.anycompatibleoid = PG_GETARG_OID(14); cinfo.anycompatiblerangeoid = PG_GETARG_OID(15); cinfo.proctuple = SearchSysCache1(PROCOID, ObjectIdGetDatum(cinfo.fn_oid)); if (!HeapTupleIsValid(cinfo.proctuple)) elog(ERROR, "cache lookup failed for function %u", cinfo.fn_oid); plpgsql_check_get_function_info(&cinfo); if (PG_GETARG_BOOL(18)) plpgsql_check_search_comment_options(&cinfo); plpgsql_check_precheck_conditions(&cinfo); plpgsql_check_init_ri(&ri, format, rsinfo); /* Envelope outer plpgsql function is not interesting */ prev_errorcontext = error_context_stack; error_context_stack = NULL; PG_TRY(); { plpgsql_check_function_internal(&ri, &cinfo); } PG_FINALLY(); { error_context_stack = prev_errorcontext; } PG_END_TRY(); plpgsql_check_finalize_ri(&ri); ReleaseSysCache(cinfo.proctuple); return (Datum) 0; } /* * plpgsql_check_function_tb * * It ensure a detailed validation and returns result as multicolumn table * */ static Datum check_function_tb_internal(Oid fnoid, FunctionCallInfo fcinfo) { plpgsql_check_info cinfo; plpgsql_check_result_info ri; ReturnSetInfo *rsinfo; ErrorContextCallback *prev_errorcontext; plpgsql_check_check_ext_version(fcinfo->flinfo->fn_oid); Assert(PG_NARGS() == 21); /* check to see if caller supports us returning a tuplestore */ rsinfo = (ReturnSetInfo *) fcinfo->resultinfo; SetReturningFunctionCheck(rsinfo); if (PG_ARGISNULL(1)) ERR_NULL_OPTION("relid"); if (PG_ARGISNULL(2)) ERR_NULL_OPTION("fatal_errors"); if (PG_ARGISNULL(3)) ERR_NULL_OPTION("other_warnings"); if (PG_ARGISNULL(4)) ERR_NULL_OPTION("performance_warnings"); if (PG_ARGISNULL(5)) ERR_NULL_OPTION("extra_warnings"); if (PG_ARGISNULL(6)) ERR_NULL_OPTION("security_warnings"); if (PG_ARGISNULL(7)) ERR_NULL_OPTION("compatibility_warnings"); if (PG_ARGISNULL(10)) ERR_NULL_OPTION("anyelementtype"); if (PG_ARGISNULL(11)) ERR_NULL_OPTION("anyenumtype"); if (PG_ARGISNULL(12)) ERR_NULL_OPTION("anyrangetype"); if (PG_ARGISNULL(13)) ERR_NULL_OPTION("anycompatibletype"); if (PG_ARGISNULL(14)) ERR_NULL_OPTION("anycompatiblerangetype"); if (PG_ARGISNULL(15)) ERR_NULL_OPTION("without_warnings"); if (PG_ARGISNULL(16)) ERR_NULL_OPTION("all_warnings"); if (PG_ARGISNULL(17)) ERR_NULL_OPTION("use_incomment_options"); if (PG_ARGISNULL(18)) ERR_NULL_OPTION("incomment_options_usage_warning"); if (PG_ARGISNULL(19)) ERR_NULL_OPTION("constants_tracing"); plpgsql_check_info_init(&cinfo, fnoid); cinfo.relid = PG_GETARG_OID(1); cinfo.fatal_errors = PG_GETARG_BOOL(2); cinfo.other_warnings = PG_GETARG_BOOL(3); cinfo.performance_warnings = PG_GETARG_BOOL(4); cinfo.extra_warnings = PG_GETARG_BOOL(5); cinfo.security_warnings = PG_GETARG_BOOL(6); cinfo.compatibility_warnings = PG_GETARG_BOOL(7); cinfo.incomment_options_usage_warning = PG_GETARG_BOOL(18); cinfo.constants_tracing = PG_GETARG_BOOL(19); if (!PG_ARGISNULL(20)) cinfo.pragmas = get_pragmas_list(PG_GETARG_ARRAYTYPE_P(20)); /* without_warnings */ if (PG_GETARG_BOOL(15)) { if (PG_GETARG_BOOL(16)) ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("without_warnings and all_warnings cannot be true same time"))); plpgsql_check_set_without_warnings(&cinfo); } /* all warnings */ else if (PG_GETARG_BOOL(16)) { if (PG_GETARG_BOOL(15)) ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("without_warnings and all_warnings cannot be true same time"))); plpgsql_check_set_all_warnings(&cinfo); } cinfo.anyelementoid = PG_GETARG_OID(10); cinfo.anyenumoid = PG_GETARG_OID(11); cinfo.anyrangeoid = PG_GETARG_OID(12); cinfo.anycompatibleoid = PG_GETARG_OID(13); cinfo.anycompatiblerangeoid = PG_GETARG_OID(14); if (PG_ARGISNULL(8)) cinfo.oldtable = NULL; else cinfo.oldtable = NameStr(*(PG_GETARG_NAME(8))); if (PG_ARGISNULL(9)) cinfo.newtable = NULL; else cinfo.newtable = NameStr(*(PG_GETARG_NAME(9))); cinfo.proctuple = SearchSysCache1(PROCOID, ObjectIdGetDatum(cinfo.fn_oid)); if (!HeapTupleIsValid(cinfo.proctuple)) elog(ERROR, "cache lookup failed for function %u", cinfo.fn_oid); plpgsql_check_get_function_info(&cinfo); if (PG_GETARG_BOOL(17)) plpgsql_check_search_comment_options(&cinfo); plpgsql_check_precheck_conditions(&cinfo); plpgsql_check_init_ri(&ri, PLPGSQL_CHECK_FORMAT_TABULAR, rsinfo); /* Envelope outer plpgsql function is not interesting */ prev_errorcontext = error_context_stack; error_context_stack = NULL; PG_TRY(); { plpgsql_check_function_internal(&ri, &cinfo); } PG_FINALLY(); { error_context_stack = prev_errorcontext; } PG_END_TRY(); plpgsql_check_finalize_ri(&ri); ReleaseSysCache(cinfo.proctuple); return (Datum) 0; } /* * plpgsql_show_dependency_tb * * Prepare tuplestore and start check function in mode dependency detection * */ static Datum show_dependency_tb_internal(Oid fnoid, FunctionCallInfo fcinfo) { plpgsql_check_info cinfo; plpgsql_check_result_info ri; ReturnSetInfo *rsinfo; plpgsql_check_check_ext_version(fcinfo->flinfo->fn_oid); Assert(PG_NARGS() == 7); /* check to see if caller supports us returning a tuplestore */ rsinfo = (ReturnSetInfo *) fcinfo->resultinfo; SetReturningFunctionCheck(rsinfo); if (PG_ARGISNULL(1)) ERR_NULL_OPTION("relid"); if (PG_ARGISNULL(2)) ERR_NULL_OPTION("anyelementtype"); if (PG_ARGISNULL(3)) ERR_NULL_OPTION("anyenumtype"); if (PG_ARGISNULL(4)) ERR_NULL_OPTION("anyrangetype"); if (PG_ARGISNULL(5)) ERR_NULL_OPTION("anycompatibletype"); if (PG_ARGISNULL(6)) ERR_NULL_OPTION("anycompatiblerangetype"); plpgsql_check_info_init(&cinfo, fnoid); cinfo.relid = PG_GETARG_OID(1); cinfo.fatal_errors = false; cinfo.other_warnings = false; cinfo.performance_warnings = false; cinfo.extra_warnings = false; cinfo.compatibility_warnings = false; cinfo.anyelementoid = PG_GETARG_OID(2); cinfo.anyenumoid = PG_GETARG_OID(3); cinfo.anyrangeoid = PG_GETARG_OID(4); cinfo.anycompatibleoid = PG_GETARG_OID(5); cinfo.anycompatiblerangeoid = PG_GETARG_OID(6); cinfo.proctuple = SearchSysCache1(PROCOID, ObjectIdGetDatum(cinfo.fn_oid)); if (!HeapTupleIsValid(cinfo.proctuple)) elog(ERROR, "cache lookup failed for function %u", cinfo.fn_oid); plpgsql_check_get_function_info(&cinfo); plpgsql_check_precheck_conditions(&cinfo); plpgsql_check_init_ri(&ri, PLPGSQL_SHOW_DEPENDENCY_FORMAT_TABULAR, rsinfo); plpgsql_check_function_internal(&ri, &cinfo); plpgsql_check_finalize_ri(&ri); ReleaseSysCache(cinfo.proctuple); return (Datum) 0; } /* * Displaying a function profile */ static Datum profiler_function_tb_internal(Oid fnoid, FunctionCallInfo fcinfo) { plpgsql_check_info cinfo; plpgsql_check_result_info ri; ReturnSetInfo *rsinfo; plpgsql_check_check_ext_version(fcinfo->flinfo->fn_oid); Assert(PG_NARGS() == 1); /* check to see if caller supports us returning a tuplestore */ rsinfo = (ReturnSetInfo *) fcinfo->resultinfo; SetReturningFunctionCheck(rsinfo); plpgsql_check_info_init(&cinfo, fnoid); cinfo.show_profile = true; cinfo.proctuple = SearchSysCache1(PROCOID, ObjectIdGetDatum(cinfo.fn_oid)); if (!HeapTupleIsValid(cinfo.proctuple)) elog(ERROR, "cache lookup failed for function %u", cinfo.fn_oid); plpgsql_check_get_function_info(&cinfo); plpgsql_check_precheck_conditions(&cinfo); cinfo.src = plpgsql_check_get_src(cinfo.proctuple); plpgsql_check_init_ri(&ri, PLPGSQL_SHOW_PROFILE_TABULAR, rsinfo); plpgsql_check_profiler_show_profile(&ri, &cinfo); plpgsql_check_finalize_ri(&ri); pfree(cinfo.src); ReleaseSysCache(cinfo.proctuple); return (Datum) 0; } /* * Displaying a function profile */ static Datum profiler_function_statements_tb_internal(Oid fnoid, FunctionCallInfo fcinfo) { plpgsql_check_info cinfo; plpgsql_check_result_info ri; ReturnSetInfo *rsinfo; plpgsql_check_check_ext_version(fcinfo->flinfo->fn_oid); Assert(PG_NARGS() == 1); /* check to see if caller supports us returning a tuplestore */ rsinfo = (ReturnSetInfo *) fcinfo->resultinfo; SetReturningFunctionCheck(rsinfo); plpgsql_check_info_init(&cinfo, fnoid); cinfo.show_profile = true; cinfo.proctuple = SearchSysCache1(PROCOID, ObjectIdGetDatum(cinfo.fn_oid)); if (!HeapTupleIsValid(cinfo.proctuple)) elog(ERROR, "cache lookup failed for function %u", cinfo.fn_oid); plpgsql_check_get_function_info(&cinfo); plpgsql_check_precheck_conditions(&cinfo); plpgsql_check_init_ri(&ri, PLPGSQL_SHOW_PROFILE_STATEMENTS_TABULAR, rsinfo); plch_statements_stats_report(&cinfo, &ri); plpgsql_check_finalize_ri(&ri); ReleaseSysCache(cinfo.proctuple); return (Datum) 0; } /* * Public functions */ Datum plpgsql_check_function(PG_FUNCTION_ARGS) { Oid fnoid; if (PG_ARGISNULL(0)) ERR_NULL_OPTION("funcoid"); fnoid = PG_GETARG_OID(0); return check_function_internal(fnoid, fcinfo); } Datum plpgsql_check_function_tb(PG_FUNCTION_ARGS) { Oid fnoid; if (PG_ARGISNULL(0)) ERR_NULL_OPTION("funcoid"); fnoid = PG_GETARG_OID(0); return check_function_tb_internal(fnoid, fcinfo); } /* * plpgsql_make_pragma * * Returns table pragmas generated for CREATE TEMP TABLE statements, including AS, * used inside the function's body. * */ Datum plpgsql_make_pragma(PG_FUNCTION_ARGS) { plpgsql_check_info cinfo; plpgsql_check_result_info ri; ReturnSetInfo *rsinfo; ErrorContextCallback *prev_errorcontext; Oid fnoid; plpgsql_check_check_ext_version(fcinfo->flinfo->fn_oid); Assert(PG_NARGS() == 3); /* check to see if caller supports us returning a tuplestore */ rsinfo = (ReturnSetInfo *) fcinfo->resultinfo; SetReturningFunctionCheck(rsinfo); if (PG_ARGISNULL(0)) ERR_NULL_OPTION("funcoid"); if (PG_ARGISNULL(1)) ERR_NULL_OPTION("relid"); if (PG_ARGISNULL(2)) ERR_NULL_OPTION("fatal_errors"); fnoid = PG_GETARG_OID(0); plpgsql_check_info_init(&cinfo, fnoid); cinfo.relid = PG_GETARG_OID(1); cinfo.fatal_errors = PG_GETARG_BOOL(2); cinfo.make_pragma = true; /* use the same defaults as plpgsql_check_function */ cinfo.anyelementoid = INT4OID; cinfo.anyenumoid = InvalidOid; cinfo.anyrangeoid = INT4RANGEOID; cinfo.anycompatibleoid = INT4OID; cinfo.anycompatiblerangeoid = INT4RANGEOID; cinfo.proctuple = SearchSysCache1(PROCOID, ObjectIdGetDatum(cinfo.fn_oid)); if (!HeapTupleIsValid(cinfo.proctuple)) elog(ERROR, "cache lookup failed for function %u", cinfo.fn_oid); plpgsql_check_get_function_info(&cinfo); plpgsql_check_search_comment_options(&cinfo); plpgsql_check_precheck_conditions(&cinfo); /* no warnings should be raised in pragma generation mode */ plpgsql_check_set_without_warnings(&cinfo); cinfo.all_warnings = false; cinfo.without_warnings = false; plpgsql_check_init_ri(&ri, PLPGSQL_CHECK_FORMAT_TEXT, rsinfo); /* Envelope outer plpgsql function is not interesting */ prev_errorcontext = error_context_stack; error_context_stack = NULL; PG_TRY(); { plpgsql_check_function_internal(&ri, &cinfo); } PG_FINALLY(); { error_context_stack = prev_errorcontext; } PG_END_TRY(); plpgsql_check_finalize_ri(&ri); ReleaseSysCache(cinfo.proctuple); return (Datum) 0; } Datum plpgsql_show_dependency_tb(PG_FUNCTION_ARGS) { Oid fnoid; if (PG_ARGISNULL(0)) ERR_NULL_OPTION("funcoid"); fnoid = PG_GETARG_OID(0); return show_dependency_tb_internal(fnoid, fcinfo); } Datum plpgsql_profiler_function_tb(PG_FUNCTION_ARGS) { Oid fnoid; if (PG_ARGISNULL(0)) ERR_NULL_OPTION("funcoid"); fnoid = PG_GETARG_OID(0); return profiler_function_tb_internal(fnoid, fcinfo); } Datum plpgsql_profiler_function_statements_tb(PG_FUNCTION_ARGS) { Oid fnoid; if (PG_ARGISNULL(0)) ERR_NULL_OPTION("funcoid"); fnoid = PG_GETARG_OID(0); return profiler_function_statements_tb_internal(fnoid, fcinfo); } Datum plpgsql_check_function_name(PG_FUNCTION_ARGS) { Oid fnoid; char *name_or_signature; if (PG_ARGISNULL(0)) ERR_NULL_OPTION("name"); name_or_signature = text_to_cstring(PG_GETARG_TEXT_PP(0)); fnoid = plpgsql_check_parse_name_or_signature(name_or_signature); return check_function_internal(fnoid, fcinfo); } Datum plpgsql_check_function_tb_name(PG_FUNCTION_ARGS) { Oid fnoid; char *name_or_signature; if (PG_ARGISNULL(0)) ERR_NULL_OPTION("name"); name_or_signature = text_to_cstring(PG_GETARG_TEXT_PP(0)); fnoid = plpgsql_check_parse_name_or_signature(name_or_signature); return check_function_tb_internal(fnoid, fcinfo); } Datum plpgsql_show_dependency_tb_name(PG_FUNCTION_ARGS) { Oid fnoid; char *name_or_signature; if (PG_ARGISNULL(0)) ERR_NULL_OPTION("name"); name_or_signature = text_to_cstring(PG_GETARG_TEXT_PP(0)); fnoid = plpgsql_check_parse_name_or_signature(name_or_signature); return show_dependency_tb_internal(fnoid, fcinfo); } Datum plpgsql_profiler_function_tb_name(PG_FUNCTION_ARGS) { Oid fnoid; char *name_or_signature; if (PG_ARGISNULL(0)) ERR_NULL_OPTION("name"); name_or_signature = text_to_cstring(PG_GETARG_TEXT_PP(0)); fnoid = plpgsql_check_parse_name_or_signature(name_or_signature); return profiler_function_tb_internal(fnoid, fcinfo); } Datum plpgsql_profiler_function_statements_tb_name(PG_FUNCTION_ARGS) { Oid fnoid; char *name_or_signature; if (PG_ARGISNULL(0)) ERR_NULL_OPTION("name"); name_or_signature = text_to_cstring(PG_GETARG_TEXT_PP(0)); fnoid = plpgsql_check_parse_name_or_signature(name_or_signature); return profiler_function_statements_tb_internal(fnoid, fcinfo); } Datum plpgsql_profiler_functions_all_tb(PG_FUNCTION_ARGS) { plpgsql_check_result_info ri; ReturnSetInfo *rsinfo; /* check to see if caller supports us returning a tuplestore */ rsinfo = (ReturnSetInfo *) fcinfo->resultinfo; SetReturningFunctionCheck(rsinfo); plpgsql_check_init_ri(&ri, PLPGSQL_SHOW_PROFILE_FUNCTIONS_ALL_TABULAR, rsinfo); plpgsql_check_profiler_iterate_over_all_profiles(&ri); return (Datum) 0; } okbob-plpgsql_check-800db79/src/tracer.c000066400000000000000000001055461526744060500202210ustar00rootroot00000000000000/*------------------------------------------------------------------------- * * tracer.c * * tracer related code * * by Pavel Stehule 2013-2026 * *------------------------------------------------------------------------- */ #include "plpgsql_check.h" #include "plpgsql_check_builtins.h" #include "catalog/pg_type.h" #include "mb/pg_wchar.h" #include "storage/proc.h" #include "utils/builtins.h" #include "utils/guc.h" #include "utils/lsyscache.h" #include "utils/memutils.h" #include "utils/syscache.h" bool plpgsql_check_enable_tracer = false; bool plpgsql_check_tracer = false; bool plpgsql_check_trace_assert = false; /* the output is modified for regress tests */ bool plpgsql_check_tracer_test_mode = false; bool plpgsql_check_tracer_show_nsubxids = false; PGErrorVerbosity plpgsql_check_tracer_verbosity = PGERROR_DEFAULT; PGErrorVerbosity plpgsql_check_trace_assert_verbosity = PGERROR_DEFAULT; int plpgsql_check_tracer_errlevel = NOTICE; int plpgsql_check_tracer_variable_max_length = 1024; static void print_datum(PLpgSQL_execstate *estate, PLpgSQL_datum *dtm, char *frame, int level); static char *convert_plpgsql_datum_to_string(PLpgSQL_execstate *estate, PLpgSQL_datum *dtm, bool *isnull, char **refname); PG_FUNCTION_INFO_V1(plpgsql_check_tracer_ctrl); #define STREXPR_START 0 /* * This structure is used as a pldbgapi3 extension parameter. */ typedef struct tracer_info { Oid fn_oid; int frame_num; char *fn_name; char *fn_signature; instr_time start_time; instr_time *stmts_start_time; bool *stmts_tracer_state; /* true when function is traced from func_beg */ bool is_traced; } tracer_info; static void trace_assert(PLpgSQL_execstate *estate, PLpgSQL_stmt *stmt, tracer_info *tinfo); static bool tracer_is_active(PLpgSQL_execstate *estate, PLpgSQL_function *func); static void tracer_func_setup(PLpgSQL_execstate *estate, PLpgSQL_function *func, plch_fextra *fextra); static void tracer_func_beg(PLpgSQL_execstate *estate, PLpgSQL_function *func, plch_fextra *fextra); static void tracer_func_end(PLpgSQL_execstate *estate, PLpgSQL_function *func, plch_fextra *fextra); static void tracer_func_abort(PLpgSQL_execstate *estate, PLpgSQL_function *func, plch_fextra *fextra); static void tracer_stmt_beg(PLpgSQL_execstate *estate, PLpgSQL_stmt *stmt, plch_fextra *fextra); static void tracer_stmt_end(PLpgSQL_execstate *estate, PLpgSQL_stmt *stmt, plch_fextra *fextra); static void tracer_stmt_abort(PLpgSQL_execstate *estate, PLpgSQL_stmt *stmt, plch_fextra *fextra); static plch_plugin tracer_plugin = { .is_active = tracer_is_active, .func_setup = tracer_func_setup, .func_beg = tracer_func_beg, .func_end = tracer_func_end, .func_abort = tracer_func_abort, .stmt_beg = tracer_stmt_beg, .stmt_end = tracer_stmt_end, .stmt_abort = tracer_stmt_abort, .stmt_error = NULL, }; /* * Convert binary value to text */ static char * convert_value_to_string(PLpgSQL_execstate *estate, Datum value, Oid valtype) { char *result; MemoryContext oldcontext; Oid typoutput; bool typIsVarlena; oldcontext = MemoryContextSwitchTo(estate->eval_econtext->ecxt_per_tuple_memory); getTypeOutputInfo(valtype, &typoutput, &typIsVarlena); result = OidOutputFunctionCall(typoutput, value); MemoryContextSwitchTo(oldcontext); return result; } static void StringInfoPrintRow(StringInfo ds, PLpgSQL_execstate *estate, PLpgSQL_row *row) { bool isfirst = true; int i; bool isnull; char *refname; appendStringInfoChar(ds, '('); for (i = 0; i < row->nfields; i++) { char *str; str = convert_plpgsql_datum_to_string(estate, estate->datums[row->varnos[i]], &isnull, &refname); if (!isfirst) appendStringInfoChar(ds, ','); else isfirst = false; if (!isnull) { if (*str) appendStringInfoString(ds, str); else appendStringInfoString(ds, "\"\""); pfree(str); } else appendStringInfoString(ds, ""); } appendStringInfoChar(ds, ')'); } static char * convert_plpgsql_datum_to_string(PLpgSQL_execstate *estate, PLpgSQL_datum *dtm, bool *isnull, char **refname) { *isnull = true; *refname = NULL; switch (dtm->dtype) { case PLPGSQL_DTYPE_VAR: { PLpgSQL_var *var = (PLpgSQL_var *) dtm; *refname = var->refname; if (!var->isnull) { *isnull = false; return convert_value_to_string(estate, var->value, var->datatype->typoid); } else return NULL; } break; case PLPGSQL_DTYPE_REC: { PLpgSQL_rec *rec = (PLpgSQL_rec *) dtm; *refname = rec->refname; if (rec->erh && !ExpandedRecordIsEmpty(rec->erh)) { *isnull = false; return convert_value_to_string(estate, ExpandedRecordGetDatum(rec->erh), rec->rectypeid); } else return NULL; } break; case PLPGSQL_DTYPE_ROW: { PLpgSQL_row *row = (PLpgSQL_row *) dtm; StringInfoData ds; *refname = row->refname; *isnull = false; initStringInfo(&ds); StringInfoPrintRow(&ds, estate, row); return ds.data; } case PLPGSQL_DTYPE_RECFIELD: { if (tracer_plugin.eval_datum) { Datum value; Oid typid; int32 typmod; StringInfoData ds; PLpgSQL_variable *variable; PLpgSQL_recfield *recfield = (PLpgSQL_recfield *) dtm; variable = (PLpgSQL_variable *) estate->datums[recfield->recparentno]; Assert(variable->dtype == PLPGSQL_DTYPE_VAR || variable->dtype == PLPGSQL_DTYPE_ROW || variable->dtype == PLPGSQL_DTYPE_REC); initStringInfo(&ds); /* refnames are printed in double quotes outside */ appendStringInfo(&ds, "%s\".\"%s", variable->refname, recfield->fieldname); *refname = ds.data; /* for PostgreSQL 15+ */ tracer_plugin.eval_datum(estate, dtm, &typid, &typmod, &value, isnull); if (*isnull) return NULL; return convert_value_to_string(estate, value, typid); } else return NULL; } default: ; } return NULL; } /* * returns size of string trimmed to plpgsql_check_tracer_variable_max_length. * This function does not break multibyte characters. */ static int trimmed_str_size(char *str) { int len = strlen(str); if (len > plpgsql_check_tracer_variable_max_length) { if (pg_database_encoding_max_length() > 1) { len = pg_mbcliplen(str, len, plpgsql_check_tracer_variable_max_length); } else len = plpgsql_check_tracer_variable_max_length; } return len; } /* * Print function's arguments */ static void print_func_args(PLpgSQL_execstate *estate, PLpgSQL_function *func, int frame_num, int level) { int i; int indent = level * 2 + (plpgsql_check_tracer_verbosity == PGERROR_VERBOSE ? 6 : 0); int frame_width = plpgsql_check_tracer_verbosity == PGERROR_VERBOSE ? 6 : 3; StringInfoData ds; initStringInfo(&ds); if (func->fn_is_trigger == PLPGSQL_DML_TRIGGER) { TriggerData *td = estate->trigdata; const char *trgtyp; const char *trgtime; const char *trgcmd; int rec_new_varno = func->new_varno; int rec_old_varno = func->old_varno; char buffer[20]; Assert(estate->trigdata); trgtyp = TRIGGER_FIRED_FOR_ROW(td->tg_event) ? "row" : "statement"; trgtime = TRIGGER_FIRED_BEFORE(td->tg_event) ? "before" : "after"; if (TRIGGER_FIRED_BY_INSERT(td->tg_event)) { trgcmd = " insert"; rec_old_varno = -1; } else if (TRIGGER_FIRED_BY_UPDATE(td->tg_event)) { trgcmd = " update"; } else if (TRIGGER_FIRED_BY_DELETE(td->tg_event)) { trgcmd = " delete"; rec_new_varno = -1; } else { trgcmd = ""; } elog(plpgsql_check_tracer_errlevel, "#%-*d%*s triggered by %s %s%s trigger", frame_width, frame_num, indent + 4, "", trgtime, trgtyp, trgcmd); sprintf(buffer, "%d", frame_num); if (rec_new_varno != -1) print_datum(estate, estate->datums[rec_new_varno], buffer, level); if (rec_old_varno != -1) print_datum(estate, estate->datums[rec_old_varno], buffer, level); } if (func->fn_is_trigger == PLPGSQL_EVENT_TRIGGER) { Assert(estate->evtrigdata); elog(plpgsql_check_tracer_errlevel, "#%-*d%*s triggered by event trigger", frame_width, frame_num, indent + 4, ""); } /* print value of arguments */ for (i = 0; i < func->fn_nargs; i++) { int n = func->fn_argvarnos[i]; bool isnull; char *refname; char *str; str = convert_plpgsql_datum_to_string(estate, estate->datums[n], &isnull, &refname); if (!isnull) { int tlen; Assert(str); tlen = trimmed_str_size(str); str[tlen] = '\0'; } if (refname) { if (!isnull) { /* * contains new line, print it separately */ if (strchr(str, '\n') != NULL) { if (*ds.data) { elog(plpgsql_check_tracer_errlevel, "#%-*d%*s %s", frame_width, frame_num, indent + 4, "", ds.data); resetStringInfo(&ds); } elog(plpgsql_check_tracer_errlevel, "#%-*d%*s \"%s\" => '%s'", frame_width, frame_num, indent + 4, "", refname, str); } else { if (*ds.data) appendStringInfoString(&ds, ", "); appendStringInfo(&ds, "\"%s\" => '%s'", refname, str); } } else { if (*ds.data) appendStringInfoString(&ds, ", "); appendStringInfo(&ds, "\"%s\" => null", refname); } } if (str) pfree(str); /* print too long lines immediately */ if (ds.len > plpgsql_check_tracer_variable_max_length) { elog(plpgsql_check_tracer_errlevel, "#%-*d%*s %s", frame_width, frame_num, indent + 4, "", ds.data); resetStringInfo(&ds); } } if (*ds.data) elog(plpgsql_check_tracer_errlevel, "#%-*d%*s %s", frame_width, frame_num, indent + 4, "", ds.data); pfree(ds.data); } /* * Print expression's arguments */ static void print_expr_args(PLpgSQL_execstate *estate, PLpgSQL_expr *expr, char *frame, int level) { int dno; int indent = level * 2 + (plpgsql_check_tracer_verbosity == PGERROR_VERBOSE ? 6 : 0); int frame_width = plpgsql_check_tracer_verbosity == PGERROR_VERBOSE ? 6 : 3; StringInfoData ds; initStringInfo(&ds); /* * When expression hasn't assigned plan, then we cannot to show a related * variables. So we can enforce creating plan. */ if (!expr->plan) { SPIPlanPtr plan; SPIPrepareOptions options; memset(&options, 0, sizeof(options)); options.parserSetup = (ParserSetupHook) plpgsql_check__parser_setup_p; options.parserSetupArg = (void *) expr; options.parseMode = expr->parseMode; options.cursorOptions = 0; expr->func = estate->func; /* * Generate and save the plan (enforce query parsing) and throw plan */ plan = SPI_prepare_extended(expr->query, &options); SPI_freeplan(plan); } /* print value of arguments */ dno = -1; while ((dno = bms_next_member(expr->paramnos, dno)) >= 0) { bool isnull; char *refname; char *str; str = convert_plpgsql_datum_to_string(estate, estate->datums[dno], &isnull, &refname); if (!isnull) { int tlen; Assert(str); tlen = trimmed_str_size(str); str[tlen] = '\0'; } if (refname) { if (!isnull) { /* * when this output is too long or contains new line, print it * separately */ if (strchr(str, '\n') != NULL) { if (*ds.data) { elog(plpgsql_check_tracer_errlevel, "#%-*s%*s %s", frame_width, frame, indent + 4, "", ds.data); resetStringInfo(&ds); } elog(plpgsql_check_tracer_errlevel, "#%-*s%*s \"%s\" => '%s'", frame_width, frame, indent + 4, "", refname, str); } else { if (*ds.data) appendStringInfoString(&ds, ", "); appendStringInfo(&ds, "\"%s\" => '%s'", refname, str); } } else { if (*ds.data) appendStringInfoString(&ds, ", "); appendStringInfo(&ds, "\"%s\" => null", refname); } } if (str) pfree(str); /* print too long lines immediately */ if (ds.len > plpgsql_check_tracer_variable_max_length) { elog(plpgsql_check_tracer_errlevel, "#%-*s%*s %s", frame_width, frame, indent + 4, "", ds.data); resetStringInfo(&ds); } } if (*ds.data) elog(plpgsql_check_tracer_errlevel, "#%-*s%*s %s", frame_width, frame, indent + 4, "", ds.data); pfree(ds.data); } /* * Print expression's arguments */ static void print_assert_args(PLpgSQL_execstate *estate, PLpgSQL_stmt_assert *stmt) { int dno; StringInfoData ds; initStringInfo(&ds); /* * When expression hasn't assigned plan, then we cannot to show a related * variables. So we can enforce creating plan. */ if (!stmt->cond->plan) { SPIPlanPtr plan; SPIPrepareOptions options; memset(&options, 0, sizeof(options)); options.parserSetup = (ParserSetupHook) plpgsql_check__parser_setup_p; options.parserSetupArg = (void *) stmt->cond; options.parseMode = stmt->cond->parseMode; options.cursorOptions = 0; stmt->cond->func = estate->func; /* * force parsing - generate and throw plan */ plan = SPI_prepare_extended((void *) stmt->cond->query, &options); SPI_freeplan(plan); } /* print value of arguments */ dno = -1; while ((dno = bms_next_member(stmt->cond->paramnos, dno)) >= 0) { bool isnull; char *refname; char *str; str = convert_plpgsql_datum_to_string(estate, estate->datums[dno], &isnull, &refname); if (!isnull) { int tlen; Assert(str); tlen = trimmed_str_size(str); str[tlen] = '\0'; } if (refname) { if (!isnull) { /* * when this output is too long or contains new line, print it * separately */ if (strchr(str, '\n') != NULL) { if (*ds.data) { elog(plpgsql_check_tracer_errlevel, " %s", ds.data); resetStringInfo(&ds); } elog(plpgsql_check_tracer_errlevel, " \"%s\" => '%s'", refname, str); } else { if (*ds.data) appendStringInfoString(&ds, ", "); appendStringInfo(&ds, "\"%s\" => '%s'", refname, str); } } else { if (*ds.data) appendStringInfoString(&ds, ", "); appendStringInfo(&ds, "\"%s\" => null", refname); } } if (str) pfree(str); /* print too long lines immediately */ if (ds.len > plpgsql_check_tracer_variable_max_length) { elog(plpgsql_check_tracer_errlevel, " %s", ds.data); resetStringInfo(&ds); } } if (*ds.data) elog(plpgsql_check_tracer_errlevel, " %s", ds.data); pfree(ds.data); } /* * Print all frame variables */ static void print_all_variables(PLpgSQL_execstate *estate) { int dno; StringInfoData ds; int indent = 1; initStringInfo(&ds); for (dno = 0; dno < estate->ndatums; dno++) { bool isnull; char *refname; char *str; if (dno == estate->found_varno) continue; str = convert_plpgsql_datum_to_string(estate, estate->datums[dno], &isnull, &refname); if (!isnull) { int tlen; Assert(str); tlen = trimmed_str_size(str); str[tlen] = '\0'; } if (refname && ( ((strcmp(refname, "*internal*") == 0) || (strcmp(refname, "(unnamed row)") == 0)))) refname = NULL; if (refname) { if (!isnull) { /* * when this output is too long or contains new line, print it * separately */ if (strchr(str, '\n') != NULL) { if (*ds.data) { elog(plpgsql_check_tracer_errlevel, "%*s%s", indent, "", ds.data); indent = 2; resetStringInfo(&ds); } elog(plpgsql_check_tracer_errlevel, "%*s \"%s\" => '%s'", indent, "", refname, str); indent = 2; } else { if (*ds.data) appendStringInfoString(&ds, ", "); appendStringInfo(&ds, "\"%s\" => '%s'", refname, str); } } else { if (*ds.data) appendStringInfoString(&ds, ", "); appendStringInfo(&ds, "\"%s\" => null", refname); } } if (str) pfree(str); /* print too long lines immediately */ if (ds.len > plpgsql_check_tracer_variable_max_length) { elog(plpgsql_check_tracer_errlevel, "%*s%s", indent, "", ds.data); indent = 2; resetStringInfo(&ds); } } if (*ds.data) elog(plpgsql_check_tracer_errlevel, "%*s%s", indent, "", ds.data); pfree(ds.data); } /* * Print plpgsql datum */ static void print_datum(PLpgSQL_execstate *estate, PLpgSQL_datum *dtm, char *frame, int level) { int indent = level * 2 + (plpgsql_check_tracer_verbosity == PGERROR_VERBOSE ? 6 : 0); int frame_width = plpgsql_check_tracer_verbosity == PGERROR_VERBOSE ? 6 : 3; bool isnull; char *refname; char *str; str = convert_plpgsql_datum_to_string(estate, dtm, &isnull, &refname); if (!isnull) { int tlen; Assert(str); tlen = trimmed_str_size(str); str[tlen] = '\0'; } if (refname) { if (!isnull) { elog(plpgsql_check_tracer_errlevel, "#%-*s%*s \"%s\" => '%s'", frame_width, frame, indent + 4, "", refname, str); } else elog(plpgsql_check_tracer_errlevel, "#%-*s%*s \"%s\" => null", frame_width, frame, indent + 4, "", refname); } if (str) pfree(str); } static bool tracer_is_active(PLpgSQL_execstate *estate, PLpgSQL_function *func) { return plpgsql_check_enable_tracer; } static void tracer_func_setup(PLpgSQL_execstate *estate, PLpgSQL_function *func, plch_fextra *fextra) { tracer_info *tinfo = NULL; if (plpgsql_check_enable_tracer) { tinfo = palloc0(sizeof(tracer_info)); tinfo->stmts_start_time = palloc0(sizeof(instr_time) * func->nstatements); tinfo->stmts_tracer_state = palloc(sizeof(bool) * func->nstatements); tinfo->fn_oid = func->fn_oid; tinfo->fn_name = fextra->fn_name; tinfo->fn_signature = fextra->fn_signature; INSTR_TIME_SET_CURRENT(tinfo->start_time); } estate->plugin_info = tinfo; } /* * Skip pldbgapi3 aux error contexts */ static ErrorContextCallback * skip_plch_errcontext(ErrorContextCallback *econtext) { while (econtext) { if (econtext->callback != tracer_plugin.plch_error_callback) return econtext; econtext = econtext->previous; } return econtext; } /* * get_caller_estate - try to returns near outer estate */ static void get_outer_info(char **errcontextstr, int *frame_num) { ErrorContextCallback *ecxt; ErrorContextCallback *top_ecxt; MemoryContext oldcxt = CurrentMemoryContext; *errcontextstr = NULL; *frame_num = 0; top_ecxt = skip_plch_errcontext(error_context_stack); for (ecxt = top_ecxt->previous; ecxt != NULL; ecxt = ecxt->previous) { /* skip pldbgapi3 error context */ if (ecxt->callback != tracer_plugin.plch_error_callback) *frame_num += 1; } if (plpgsql_check_tracer_verbosity >= PGERROR_DEFAULT && top_ecxt->previous) { ErrorData *edata; ecxt = skip_plch_errcontext(top_ecxt->previous); errstart(ERROR, TEXTDOMAIN); MemoryContextSwitchTo(oldcxt); (*ecxt->callback) (ecxt->arg); edata = CopyErrorData(); FlushErrorState(); if (edata->context) *errcontextstr = edata->context; MemoryContextSwitchTo(oldcxt); } } /* * Tracer event routines */ static void tracer_func_beg(PLpgSQL_execstate *estate, PLpgSQL_function *func, plch_fextra *fextra) { tracer_info *tinfo = (tracer_info *) estate->plugin_info; char *caller_errcontext = NULL; Oid fn_oid; int indent; int frame_width; char buffer[30]; if (!tinfo) return; fn_oid = plpgsql_check_tracer_test_mode ? 0 : func->fn_oid; get_outer_info(&caller_errcontext, &tinfo->frame_num); if (!plpgsql_check_tracer) return; indent = tinfo->frame_num * 2 + (plpgsql_check_tracer_verbosity == PGERROR_VERBOSE ? 6 : 0); frame_width = plpgsql_check_tracer_verbosity == PGERROR_VERBOSE ? 6 : 3; if (plpgsql_check_tracer_show_nsubxids) { if (MyProc->subxidStatus.overflowed) snprintf(buffer, 30, ", nxids=OF"); else snprintf(buffer, 30, ", nxids=%d", MyProc->subxidStatus.count); } else buffer[0] = '\0'; if (plpgsql_check_tracer_verbosity >= PGERROR_DEFAULT) elog(plpgsql_check_tracer_errlevel, "#%-*d%*s ->> start of %s%s (oid=%u, tnl=%d%s)", frame_width, tinfo->frame_num, indent, "", func->fn_oid ? "function " : "block ", func->fn_signature, fn_oid, GetCurrentTransactionNestLevel(), buffer); else elog(plpgsql_check_tracer_errlevel, "#%-*d start of %s (oid=%u, tnl=%d%s)", frame_width, tinfo->frame_num, func->fn_oid ? get_func_name(func->fn_oid) : "inline code block", fn_oid, GetCurrentTransactionNestLevel(), buffer); if (plpgsql_check_tracer_verbosity >= PGERROR_DEFAULT) { if (caller_errcontext) { elog(plpgsql_check_tracer_errlevel, "#%-*d%*s context: %s", frame_width, tinfo->frame_num, indent + 4, " ", caller_errcontext); pfree(caller_errcontext); } print_func_args(estate, func, tinfo->frame_num, tinfo->frame_num); } tinfo->is_traced = true; } /* * workhorse for tracer_func_end and tracer_func_end_aborted */ static void _tracer_func_end(tracer_info *tinfo, bool is_aborted) { instr_time end_time; uint64 elapsed; int indent; int frame_width; const char *aborted; Assert(tinfo); if (!tinfo->is_traced || !plpgsql_check_tracer) return; indent = tinfo->frame_num * 2 + (plpgsql_check_tracer_verbosity == PGERROR_VERBOSE ? 6 : 0); frame_width = plpgsql_check_tracer_verbosity == PGERROR_VERBOSE ? 6 : 3; aborted = is_aborted ? " aborted" : ""; INSTR_TIME_SET_CURRENT(end_time); INSTR_TIME_SUBTRACT(end_time, tinfo->start_time); elapsed = INSTR_TIME_GET_MICROSEC(end_time); /* For output in regress tests use immutable time 0.010 ms */ if (plpgsql_check_tracer_test_mode) elapsed = 10; if (plpgsql_check_tracer_verbosity >= PGERROR_DEFAULT) { if (OidIsValid(tinfo->fn_oid)) elog(plpgsql_check_tracer_errlevel, "#%-*d%*s <<- end of function %s (elapsed time=%.3f ms)%s", frame_width, tinfo->frame_num, indent, "", tinfo->fn_name, elapsed / 1000.0, aborted); else elog(plpgsql_check_tracer_errlevel, "#%-*d%*s <<- end of block (elapsed time=%.3f ms)%s", frame_width, tinfo->frame_num, indent, "", elapsed / 1000.0, aborted); } else elog(plpgsql_check_tracer_errlevel, "#%-3d end of %s%s", tinfo->frame_num, tinfo->fn_oid ? tinfo->fn_name : "inline code block", aborted); } static void tracer_func_end(PLpgSQL_execstate *estate, PLpgSQL_function *func, plch_fextra *fextra) { tracer_info *tinfo = (tracer_info *) estate->plugin_info; if (!tinfo) return; Assert(tinfo->fn_oid == func->fn_oid); _tracer_func_end(tinfo, false); } static void tracer_func_abort(PLpgSQL_execstate *estate, PLpgSQL_function *func, plch_fextra *fextra) { tracer_info *tinfo = (tracer_info *) estate->plugin_info; if (!tinfo) return; Assert(tinfo->fn_oid == func->fn_oid); _tracer_func_end(tinfo, true); } /* * returns trimmed string */ static char * sntrimstr(char *dest, size_t dest_size, char *src, int n) { char *result = dest; size_t available_bytes = dest_size - 4; while (*src && n > 0) { int mbl = pg_mblen_cstr(src); if (mbl > available_bytes) break; memcpy(dest, src, mbl); src += mbl; dest += mbl; n -= mbl; available_bytes -= mbl; } if (*src) { memcpy(dest, " ...", 3); dest += 3; } *dest = '\0'; return result; } static void tracer_stmt_beg(PLpgSQL_execstate *estate, PLpgSQL_stmt *stmt, plch_fextra *fextra) { tracer_info *tinfo = (tracer_info *) estate->plugin_info; int total_level; bool invisible = stmt->lineno < 1; char buffer[20]; if (!tinfo) return; /* save current tracer state (enabled | disabled) */ tinfo->stmts_tracer_state[stmt->stmtid - 1] = plpgsql_check_tracer; /* don't trace invisible statements */ if (invisible) return; if (stmt->cmd_type == PLPGSQL_STMT_ASSERT && plpgsql_check_trace_assert) trace_assert(estate, stmt, tinfo); if (!plpgsql_check_tracer) return; total_level = tinfo->frame_num + fextra->levels[stmt->stmtid]; if (plpgsql_check_tracer_show_nsubxids) { if (MyProc->subxidStatus.overflowed) snprintf(buffer, 20, " (tnl=%d, nxids=OF)", GetCurrentTransactionNestLevel()); else snprintf(buffer, 20, " (tnl=%d, nxids=%d)", GetCurrentTransactionNestLevel(), MyProc->subxidStatus.count); } else snprintf(buffer, 20, " (tnl=%d)", GetCurrentTransactionNestLevel()); if (plpgsql_check_tracer_verbosity == PGERROR_VERBOSE) { int indent = (tinfo->frame_num + fextra->levels[stmt->stmtid]) * 2; int frame_width = 6; char printbuf[20]; char exprbuf[200]; PLpgSQL_expr *expr = NULL; char *exprname = NULL; int retvarno = -1; bool is_assignment = false; bool is_perform = false; switch (stmt->cmd_type) { case PLPGSQL_STMT_PERFORM: expr = ((PLpgSQL_stmt_perform *) stmt)->expr; exprname = "perform"; is_perform = true; break; case PLPGSQL_STMT_ASSIGN: { PLpgSQL_stmt_assign *stmt_assign = (PLpgSQL_stmt_assign *) stmt; PLpgSQL_datum *target = estate->datums[stmt_assign->varno]; expr = stmt_assign->expr; if (target->dtype == PLPGSQL_DTYPE_VAR) expr->target_param = target->dno; else expr->target_param = -1; exprname = "expr"; is_assignment = true; } break; case PLPGSQL_STMT_RETURN: expr = ((PLpgSQL_stmt_return *) stmt)->expr; retvarno = ((PLpgSQL_stmt_return *) stmt)->retvarno; exprname = "expr"; break; case PLPGSQL_STMT_ASSERT: expr = ((PLpgSQL_stmt_assert *) stmt)->cond; exprname = "expr"; break; case PLPGSQL_STMT_CALL: expr = ((PLpgSQL_stmt_call *) stmt)->expr; exprname = "expr"; break; case PLPGSQL_STMT_EXECSQL: expr = ((PLpgSQL_stmt_execsql *) stmt)->sqlstmt; exprname = "query"; break; case PLPGSQL_STMT_IF: expr = ((PLpgSQL_stmt_if *) stmt)->cond; exprname = "cond"; default: ; } INSTR_TIME_SET_CURRENT(tinfo->stmts_start_time[stmt->stmtid - 1]); snprintf(printbuf, 20, "%d.%d", tinfo->frame_num, fextra->naturalids[stmt->stmtid]); if (expr) { int startpos; if (strcmp(exprname, "perform") == 0) { startpos = 7; exprname = "expr"; } else startpos = 0; if (is_assignment) { elog(plpgsql_check_tracer_errlevel, "#%-*s %4d %*s --> start of assignment %s%s", frame_width, printbuf, stmt->lineno, indent, "", sntrimstr(exprbuf, sizeof(exprbuf), expr->query + startpos, 30), buffer); } else if (is_perform) { elog(plpgsql_check_tracer_errlevel, "#%-*s %4d %*s --> start of perform %s%s", frame_width, printbuf, stmt->lineno, indent, "", sntrimstr(exprbuf, sizeof(exprbuf), expr->query + startpos, 30), buffer); } else { elog(plpgsql_check_tracer_errlevel, "#%-*s %4d %*s --> start of %s (%s='%s')%s", frame_width, printbuf, stmt->lineno, indent, "", plpgsql_check__stmt_typename_p(stmt), exprname, sntrimstr(exprbuf, sizeof(exprbuf), expr->query + startpos, 30), buffer); } } else elog(plpgsql_check_tracer_errlevel, "#%-*s %4d %*s --> start of %s%s", frame_width, printbuf, stmt->lineno, indent, "", plpgsql_check__stmt_typename_p(stmt), buffer); if (expr) print_expr_args(estate, expr, printbuf, total_level); if (retvarno >= 0) print_datum(estate, estate->datums[retvarno], printbuf, total_level); switch (stmt->cmd_type) { case PLPGSQL_STMT_IF: { PLpgSQL_stmt_if *stmt_if = (PLpgSQL_stmt_if *) stmt; ListCell *lc; foreach(lc, stmt_if->elsif_list) { PLpgSQL_if_elsif *ifelseif = (PLpgSQL_if_elsif *) lfirst(lc); elog(plpgsql_check_tracer_errlevel, "#%-*s %4d %*s ELSEIF (expr='%s')", frame_width, printbuf, ifelseif->lineno, indent, "", sntrimstr(exprbuf, sizeof(exprbuf), ifelseif->cond->query + STREXPR_START, 30)); print_expr_args(estate, ifelseif->cond, printbuf, total_level); } break; } default: ; } } } static void _tracer_stmt_end(tracer_info *tinfo, plch_fextra *fextra, PLpgSQL_stmt *stmt, bool is_aborted) { const char *aborted = is_aborted ? " aborted" : ""; int stmtid = stmt->stmtid; bool is_container = plch_statement_is_container(stmt); bool invisible = stmt->lineno < 1; Assert(tinfo); /* don't trace invisible statements */ if (invisible) { if (is_container) { /* restore tracer state (enabled | disabled) */ plpgsql_check_tracer = tinfo->stmts_tracer_state[stmtid - 1]; } return; } if (tinfo->stmts_tracer_state[stmtid - 1] && plpgsql_check_tracer_verbosity == PGERROR_VERBOSE) { int indent = (tinfo->frame_num + fextra->levels[stmtid]) * 2; int frame_width = 6; char printbuf[20]; uint64 elapsed = 0; if (!INSTR_TIME_IS_ZERO(tinfo->stmts_start_time[stmtid - 1])) { instr_time end_time; INSTR_TIME_SET_CURRENT(end_time); INSTR_TIME_SUBTRACT(end_time, tinfo->stmts_start_time[stmtid - 1]); elapsed = INSTR_TIME_GET_MICROSEC(end_time); if (plpgsql_check_tracer_test_mode) elapsed = 10; } snprintf(printbuf, 20, "%d.%d", tinfo->frame_num, fextra->naturalids[stmtid]); elog(plpgsql_check_tracer_errlevel, "#%-*s %*s <-- end of %s (elapsed time=%.3f ms)%s", frame_width, printbuf, indent, "", plpgsql_check__stmt_typename_p(stmt), elapsed / 1000.0, aborted); } if (is_container) { /* restore tracer state (enabled | disabled) */ plpgsql_check_tracer = tinfo->stmts_tracer_state[stmtid - 1]; } } static void tracer_stmt_end(PLpgSQL_execstate *estate, PLpgSQL_stmt *stmt, plch_fextra *fextra) { tracer_info *tinfo = (tracer_info *) estate->plugin_info; bool invisible = stmt->lineno < 1; if (!tinfo) return; _tracer_stmt_end(tinfo, fextra, stmt, false); if (!plpgsql_check_tracer) return; if (plpgsql_check_tracer_verbosity == PGERROR_VERBOSE && stmt->cmd_type == PLPGSQL_STMT_ASSIGN && !invisible) { char printbuf[20]; snprintf(printbuf, 20, "%d.%d", tinfo->frame_num, fextra->naturalids[stmt->stmtid]); print_datum(estate, estate->datums[((PLpgSQL_stmt_assign *) stmt)->varno], printbuf, tinfo->frame_num + fextra->levels[stmt->stmtid]); } } static void tracer_stmt_abort(PLpgSQL_execstate *estate, PLpgSQL_stmt *stmt, plch_fextra *fextra) { tracer_info *tinfo = (tracer_info *) estate->plugin_info; if (!tinfo) return; _tracer_stmt_end(tinfo, fextra, stmt, true); } static void trace_assert(PLpgSQL_execstate *estate, PLpgSQL_stmt *stmt, tracer_info *tinfo) { PLpgSQL_var result; PLpgSQL_type typ; char exprbuf[200]; PLpgSQL_stmt_assert *stmt_assert = (PLpgSQL_stmt_assert *) stmt; memset(&result, 0, sizeof(result)); memset(&typ, 0, sizeof(typ)); result.dtype = PLPGSQL_DTYPE_VAR; result.refname = "*auxstorage*"; result.datatype = &typ; result.value = (Datum) 5; typ.typoid = BOOLOID; typ.ttype = PLPGSQL_TTYPE_SCALAR; typ.typlen = 1; typ.typbyval = true; typ.typtype = 'b'; /* * This expression can be evaluated more two times (when assertions are * enabled in plpgsql). Theoretically we can catch ERRCODE_ASSERT_FAILURE * by overwriting error_context_stack, and then print variables content, * from error_context_stack. Unfortunately, the print_all_variables, can * build large strings and there is a risk of exhausing of ErrorContext. * So multiple evaluation of assert condition looks more acceptable. * Usually, there should not be any really volatile expressions, because * then the assertions are volatile too. */ tracer_plugin.assign_expr(estate, (PLpgSQL_datum *) &result, stmt_assert->cond); if ((bool) result.value) { if (plpgsql_check_trace_assert_verbosity >= PGERROR_DEFAULT) { elog(plpgsql_check_tracer_errlevel, "PLpgSQL assert expression (%s) on line %d of %s is true", sntrimstr(exprbuf, sizeof(exprbuf), stmt_assert->cond->query + STREXPR_START, 30), stmt->lineno, estate->func->fn_signature); print_assert_args(estate, stmt_assert); } } else { ErrorContextCallback *econtext; int frame_num = tinfo->frame_num; elog(plpgsql_check_tracer_errlevel, "#%d PLpgSQL assert expression (%s) on line %d of %s is false", frame_num, sntrimstr(exprbuf, sizeof(exprbuf), stmt_assert->cond->query + STREXPR_START, 30), stmt->lineno, estate->func->fn_signature); print_all_variables(estate); /* * Show outer frames by default, adding their variables in verbose * mode. */ if (plpgsql_check_trace_assert_verbosity >= PGERROR_DEFAULT) { for (econtext = (skip_plch_errcontext(error_context_stack))->previous; econtext != NULL; econtext = econtext->previous) { if (econtext->callback == tracer_plugin.plch_error_callback) continue; frame_num -= 1; /* * We detect PLpgSQL related estate by known error callback * function. This is inspirated by PLDebugger. */ if (econtext->callback == tracer_plugin.error_callback) { PLpgSQL_execstate *oestate = (PLpgSQL_execstate *) econtext->arg; if (oestate->err_stmt) elog(plpgsql_check_tracer_errlevel, "#%d PL/pgSQL function %s line %d at %s", frame_num, oestate->func->fn_signature, oestate->err_stmt->lineno, plpgsql_check__stmt_typename_p(oestate->err_stmt)); else elog(plpgsql_check_tracer_errlevel, "#%d PLpgSQL function %s", frame_num, oestate->func->fn_signature); if (plpgsql_check_trace_assert_verbosity == PGERROR_VERBOSE) print_all_variables(oestate); } } } } } void plpgsql_check_tracer_init(void) { plch_register_plugin(&tracer_plugin); } /* * Enable, disable, show state tracer */ Datum plpgsql_check_tracer_ctrl(PG_FUNCTION_ARGS) { char *optstr; bool result; #define OPTNAME_1 "plpgsql_check.tracer" #define OPTNAME_2 "plpgsql_check.tracer_verbosity" if (!PG_ARGISNULL(0)) { bool optval = PG_GETARG_BOOL(0); (void) set_config_option(OPTNAME_1, optval ? "on" : "off", (superuser() ? PGC_SUSET : PGC_USERSET), PGC_S_SESSION, GUC_ACTION_SET, true, 0, false); } if (!PG_ARGISNULL(1)) { char *optval = TextDatumGetCString(PG_GETARG_DATUM(1)); (void) set_config_option(OPTNAME_2, optval, (superuser() ? PGC_SUSET : PGC_USERSET), PGC_S_SESSION, GUC_ACTION_SET, true, 0, false); } optstr = GetConfigOptionByName(OPTNAME_1, NULL, false); if (strcmp(optstr, "on") == 0) { elog(NOTICE, "tracer is active"); result = true; } else { elog(NOTICE, "tracer is not active"); result = false; } optstr = GetConfigOptionByName(OPTNAME_2, NULL, false); elog(NOTICE, "tracer verbosity is %s", optstr); if (result && !plpgsql_check_enable_tracer) ereport(NOTICE, (errmsg("tracer is still blocked"), errdetail("The tracer should be enabled by the superuser for security reasons."), errhint("Execute \"set plpgsql_check.enable_tracer to on\" (superuser only)."))); PG_RETURN_BOOL(result); } okbob-plpgsql_check-800db79/src/typdesc.c000066400000000000000000000410731526744060500204060ustar00rootroot00000000000000/*------------------------------------------------------------------------- * * typdesc.c * * deduction result tupdesc from expression * * by Pavel Stehule 2013-2026 * *------------------------------------------------------------------------- */ #include "plpgsql_check.h" #include "access/htup_details.h" #include "catalog/pg_proc.h" #include "catalog/pg_type.h" #include "executor/spi_priv.h" #include "nodes/nodeFuncs.h" #include "optimizer/clauses.h" #include "optimizer/optimizer.h" #include "utils/builtins.h" #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/typcache.h" /* * Try to calculate procedure row target from used INOUT variables * */ PLpgSQL_row * plpgsql_check_CallExprGetRowTarget(PLpgSQL_checkstate *cstate, PLpgSQL_expr *CallExpr) { Node *node; FuncExpr *funcexpr; PLpgSQL_row *result = NULL; if (CallExpr->plan != NULL) { PLpgSQL_row *row; CachedPlanSource *plansource; CallStmt *stmt; HeapTuple tuple; Oid *argtypes; char **argnames; char *argmodes; int i; int nfields = 0; int numargs; plansource = plpgsql_check_get_plan_source(cstate, CallExpr->plan); /* * Get the original CallStmt */ node = linitial_node(Query, plansource->query_list)->utilityStmt; if (!IsA(node, CallStmt)) elog(ERROR, "returned row from not a CallStmt"); stmt = castNode(CallStmt, node); funcexpr = stmt->funcexpr; /* * Get the argument modes */ tuple = SearchSysCache1(PROCOID, ObjectIdGetDatum(funcexpr->funcid)); if (!HeapTupleIsValid(tuple)) elog(ERROR, "cache lookup failed for function %u", funcexpr->funcid); /* * Get the argument names and modes, so that we can deliver on-point * error messages when something is wrong. */ numargs = get_func_arg_info(tuple, &argtypes, &argnames, &argmodes); ReleaseSysCache(tuple); row = (PLpgSQL_row *) palloc0(sizeof(PLpgSQL_row)); row->dtype = PLPGSQL_DTYPE_ROW; row->refname = NULL; row->dno = -1; row->lineno = -1; row->varnos = (int *) palloc(numargs * sizeof(int)); /* * Examine procedure's argument list. Each output arg position should * be an unadorned plpgsql variable (Datum), which we can insert into * the row Datum. */ nfields = 0; for (i = 0; i < numargs; i++) { if (argmodes && (argmodes[i] == PROARGMODE_INOUT || argmodes[i] == PROARGMODE_OUT)) { Node *n = list_nth(stmt->outargs, nfields); if (IsA(n, Param)) { Param *param = (Param *) n; int dno; /* paramid is offset by 1 (see make_datum_param()) */ dno = param->paramid - 1; /* must check assignability now, because grammar can't */ plpgsql_check_is_assignable(cstate->estate, dno); row->varnos[nfields++] = dno; } else { /* report error using parameter name, if available */ if (argnames && argnames[i] && argnames[i][0]) ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR), errmsg("procedure parameter \"%s\" is an output parameter but corresponding argument is not writable", argnames[i]))); else ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR), errmsg("procedure parameter %d is an output parameter but corresponding argument is not writable", i + 1))); } } } Assert(nfields == list_length(stmt->outargs)); row->nfields = nfields; /* Don't return empty row variable */ if (nfields > 0) { result = row; } else { pfree(row->varnos); pfree(row); } } else elog(ERROR, "there are no plan for query: \"%s\"", CallExpr->query); return result; } /* * Returns typoid, typmod associated with record variable */ void plpgsql_check_recvar_info(PLpgSQL_rec *rec, Oid *typoid, int32 *typmod) { if (rec->dtype != PLPGSQL_DTYPE_REC) elog(ERROR, "variable is not record type"); if (rec->rectypeid != RECORDOID) { if (typoid != NULL) *typoid = rec->rectypeid; if (typmod != NULL) *typmod = -1; } else if (recvar_tupdesc(rec) != NULL) { TupleDesc tdesc = recvar_tupdesc(rec); BlessTupleDesc(tdesc); if (typoid != NULL) *typoid = tdesc->tdtypeid; if (typmod != NULL) *typmod = tdesc->tdtypmod; } else { if (typoid != NULL) *typoid = RECORDOID; if (typmod != NULL) *typmod = -1; } } static TupleDesc param_get_desc(PLpgSQL_checkstate *cstate, Param *p) { TupleDesc rettupdesc = NULL; if (!type_is_rowtype(p->paramtype)) ereport(ERROR, (errcode(ERRCODE_DATATYPE_MISMATCH), errmsg("function does not return composite type, is not possible to identify composite type"))); /* * paramid inside dynamic query (executed by EXECUTE command) is not * related to datum number */ if (cstate->is_dynsql) { TupleDesc rectupdesc; rectupdesc = lookup_rowtype_tupdesc_noerror(p->paramtype, p->paramtypmod, true); if (rectupdesc != NULL) { rettupdesc = CreateTupleDescCopy(rectupdesc); ReleaseTupleDesc(rectupdesc); } } else if (p->paramkind == PARAM_EXTERN && p->paramid > 0 && p->location != -1) { int dno; PLpgSQL_var *var; /* * When paramid looks well and related datum is variable with same * type, then we can check, if this variable has sanitized content * already. */ dno = p->paramid - 1; Assert(dno < cstate->estate->ndatums); var = (PLpgSQL_var *) cstate->estate->datums[dno]; if (var->dtype == PLPGSQL_DTYPE_ROW || var->dtype == PLPGSQL_DTYPE_RECFIELD) { TupleDesc rectupdesc; rectupdesc = lookup_rowtype_tupdesc_noerror(p->paramtype, p->paramtypmod, true); if (rectupdesc != NULL) { rettupdesc = CreateTupleDescCopy(rectupdesc); ReleaseTupleDesc(rectupdesc); } } else if (var->dtype == PLPGSQL_DTYPE_REC) { PLpgSQL_rec *rec = (PLpgSQL_rec *) var; if (!rec->datatype || !OidIsValid(rec->datatype->typoid) || rec->datatype->typoid == 0xFFFFFFFF || rec->datatype->typoid == p->paramtype) { TupleDesc rectupdesc; Oid typoid; int32 typmod; plpgsql_check_recvar_info(rec, &typoid, &typmod); rectupdesc = lookup_rowtype_tupdesc_noerror(typoid, typmod, true); if (rectupdesc) { rettupdesc = CreateTupleDescCopy(rectupdesc); ReleaseTupleDesc(rectupdesc); } } } else elog(ERROR, "unexpected dtype"); } return rettupdesc; } /* * Try to deduce result tuple descriptor from polymorphic function * like fce(.., anyelement, ..) returns anyelement */ static TupleDesc pofce_get_desc(PLpgSQL_checkstate *cstate, PLpgSQL_expr *expr, FuncExpr *fn) { HeapTuple func_tuple; Form_pg_proc procStruct; Oid fnoid = fn->funcid; TupleDesc result = NULL; func_tuple = SearchSysCache1(PROCOID, ObjectIdGetDatum(fnoid)); if (!HeapTupleIsValid(func_tuple)) elog(ERROR, "cache lookup failed for function %u", fnoid); procStruct = (Form_pg_proc) GETSTRUCT(func_tuple); if (procStruct->prorettype == ANYELEMENTOID) { Oid *argtypes; char *argmodes; char **argnames; int pronallargs; int i; int inargno; pronallargs = get_func_arg_info(func_tuple, &argtypes, &argnames, &argmodes); inargno = 0; Assert(procStruct->pronargs == list_length(fn->args)); for (i = 0; i < pronallargs; i++) { if (argmodes && (argmodes[i] != PROARGMODE_IN && argmodes[i] != PROARGMODE_INOUT)) continue; if (argtypes[i] == ANYELEMENTOID) { Node *arg; Assert(inargno < procStruct->pronargs); /* * fn->args are transformated already, so items in this list * must match fields in proargtypes. This matching is * fundamental prerequsity of this routine. */ arg = list_nth(fn->args, inargno); if (IsA(arg, Param)) { Param *p = (Param *) arg; if (!cstate->is_dynsql && p->paramkind == PARAM_EXTERN && p->paramid > 0 && p->location != -1) { int dno = p->paramid - 1; /* * When paramid looks well and related datum is * variable with same type, then we can check, if this * variable has sanitized content already. */ if (expr && bms_is_member(dno, expr->paramnos)) { PLpgSQL_var *var = (PLpgSQL_var *) cstate->estate->datums[dno]; /* * When we know a datatype, then we expect eq with * param type. But sometimes a Oid of datatype is * not valid - record type for some older * releases. What is worse - sometimes Oid is 0 or * FFFFFFFF. */ if (var->dtype == PLPGSQL_DTYPE_REC && (!var->datatype || !OidIsValid(var->datatype->typoid) || var->datatype->typoid == 0xFFFFFFFF || var->datatype->typoid == p->paramtype)) { PLpgSQL_rec *rec = (PLpgSQL_rec *) var; Oid typoid; int32 typmod; TupleDesc rectupdesc; plpgsql_check_recvar_info(rec, &typoid, &typmod); rectupdesc = lookup_rowtype_tupdesc_noerror(typoid, typmod, true); if (rectupdesc) { result = CreateTupleDescCopy(rectupdesc); ReleaseTupleDesc(rectupdesc); break; } } } } } } inargno++; } if (argtypes) pfree(argtypes); if (argnames) pfree(argnames); if (argmodes) pfree(argmodes); } ReleaseSysCache(func_tuple); return result; } /* * Returns a tuple descriptor based on existing plan, When error is detected * returns null. Does hardwork when result is based on record type. * */ TupleDesc plpgsql_check_expr_get_desc(PLpgSQL_checkstate *cstate, PLpgSQL_expr *query, bool use_element_type, bool expand_record, bool is_expression, Oid *first_level_typoid) { TupleDesc tupdesc = NULL; CachedPlanSource *plansource = NULL; if (query->plan != NULL) { plansource = plpgsql_check_get_plan_source(cstate, query->plan); /* * The EXECUTE command can sucessfully execute empty string. Then the * plansource is empty - NULL. */ if (!plansource) return NULL; if (!plansource->resultDesc) { if (is_expression) elog(ERROR, "query returns no result"); else return NULL; } tupdesc = CreateTupleDescCopy(plansource->resultDesc); } else elog(ERROR, "there are no plan for query: \"%s\"", query->query); if (is_expression && tupdesc->natts != 1) ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR), errmsg("query \"%s\" returned %d columns", query->query, tupdesc->natts))); /* * try to get a element type, when result is a array (used with FOREACH * ARRAY stmt) */ if (use_element_type) { Oid elemtype; /* result should be a array */ if (is_expression && tupdesc->natts != 1) ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR), errmsg("query \"%s\" returned %d columns", query->query, tupdesc->natts))); /* check the type of the expression - must be an array */ if (tupdesc->natts > 0) elemtype = get_element_type(TupleDescAttr(tupdesc, 0)->atttypid); else elemtype = InvalidOid; if (!OidIsValid(elemtype)) { ereport(ERROR, (errcode(ERRCODE_DATATYPE_MISMATCH), errmsg("FOREACH expression must yield an array, not type %s", format_type_be(TupleDescAttr(tupdesc, 0)->atttypid)))); FreeTupleDesc(tupdesc); } if (is_expression && first_level_typoid != NULL) *first_level_typoid = elemtype; /* when elemtype is not composite, prepare single field tupdesc */ if (!type_is_rowtype(elemtype)) { TupleDesc rettupdesc; rettupdesc = CreateTemplateTupleDesc(1); TupleDescInitEntry(rettupdesc, 1, "__array_element__", elemtype, -1, 0); #if PG_VERSION_NUM >= 190000 TupleDescFinalize(rettupdesc); #endif FreeTupleDesc(tupdesc); BlessTupleDesc(rettupdesc); tupdesc = rettupdesc; } else { TupleDesc elemtupdesc; elemtupdesc = lookup_rowtype_tupdesc_noerror(elemtype, -1, true); if (elemtupdesc != NULL) { FreeTupleDesc(tupdesc); tupdesc = CreateTupleDescCopy(elemtupdesc); ReleaseTupleDesc(elemtupdesc); } } } else { if (is_expression && first_level_typoid != NULL) *first_level_typoid = TupleDescAttr(tupdesc, 0)->atttypid; } /* * One special case is when record is assigned to composite type, then we * should unpack composite type. */ if (tupdesc->tdtypeid == RECORDOID && tupdesc->tdtypmod == -1 && tupdesc->natts == 1 && expand_record) { TupleDesc unpack_tupdesc; unpack_tupdesc = lookup_rowtype_tupdesc_noerror(TupleDescAttr(tupdesc, 0)->atttypid, TupleDescAttr(tupdesc, 0)->atttypmod, true); if (unpack_tupdesc != NULL) { FreeTupleDesc(tupdesc); tupdesc = CreateTupleDescCopy(unpack_tupdesc); ReleaseTupleDesc(unpack_tupdesc); } } /* * There is special case, when returned tupdesc contains only unpinned * record: rec := func_with_out_parameters(). In this case we must dig * deeper - we have to find oid of function and get their parameters, * * This is support for assign statement recvar := * func_with_out_parameters(..) * * XXX: Why don't we always do that? */ if (tupdesc->tdtypeid == RECORDOID && tupdesc->tdtypmod == -1 && tupdesc->natts == 1 && TupleDescAttr(tupdesc, 0)->atttypid == RECORDOID && TupleDescAttr(tupdesc, 0)->atttypmod == -1 && expand_record) { PlannedStmt *_stmt; Plan *_plan; TargetEntry *tle; CachedPlan *cplan; /* * When tupdesc is related to unpined record, we will try to check * plan if it is just function call and if it is then we can try to * derive a tupledes from function's description. */ cplan = GetCachedPlan(plansource, NULL, CurrentResourceOwner, NULL); _stmt = (PlannedStmt *) linitial(cplan->stmt_list); if (IsA(_stmt, PlannedStmt) && _stmt->commandType == CMD_SELECT) { _plan = _stmt->planTree; if (IsA(_plan, Result) && list_length(_plan->targetlist) == 1) { tle = (TargetEntry *) linitial(_plan->targetlist); switch (((Node *) tle->expr)->type) { case T_FuncExpr: { FuncExpr *fn = (FuncExpr *) tle->expr; FmgrInfo flinfo; LOCAL_FCINFO(fcinfo, 0); TupleDesc rd; Oid rt; TypeFuncClass tfc; fmgr_info(fn->funcid, &flinfo); flinfo.fn_expr = (Node *) fn; fcinfo->flinfo = &flinfo; fcinfo->resultinfo = NULL; tfc = get_call_result_type(fcinfo, &rt, &rd); if (tfc == TYPEFUNC_SCALAR || tfc == TYPEFUNC_OTHER) ereport(ERROR, (errcode(ERRCODE_DATATYPE_MISMATCH), errmsg("function does not return composite type, is not possible to identify composite type"))); FreeTupleDesc(tupdesc); if (rd) { BlessTupleDesc(rd); tupdesc = rd; } else { /* * for polymorphic function we can determine * record typmod (and tupdesc) from arguments. */ tupdesc = pofce_get_desc(cstate, query, fn); } } break; case T_RowExpr: { RowExpr *row = (RowExpr *) tle->expr; ListCell *lc_colname; ListCell *lc_arg; TupleDesc rettupdesc; int i = 1; rettupdesc = CreateTemplateTupleDesc(list_length(row->args)); forboth(lc_colname, row->colnames, lc_arg, row->args) { Node *arg = lfirst(lc_arg); char *name = strVal(lfirst(lc_colname)); TupleDescInitEntry(rettupdesc, i, name, exprType(arg), exprTypmod(arg), 0); i++; } #if PG_VERSION_NUM >= 190000 TupleDescFinalize(rettupdesc); #endif FreeTupleDesc(tupdesc); BlessTupleDesc(rettupdesc); tupdesc = rettupdesc; } break; case T_Const: { Const *c = (Const *) tle->expr; FreeTupleDesc(tupdesc); if (c->consttype == RECORDOID && c->consttypmod == -1 && !c->constisnull) { Oid tupType; int32 tupTypmod; HeapTupleHeader rec = DatumGetHeapTupleHeader(c->constvalue); tupType = HeapTupleHeaderGetTypeId(rec); tupTypmod = HeapTupleHeaderGetTypMod(rec); tupdesc = lookup_rowtype_tupdesc(tupType, tupTypmod); } else tupdesc = NULL; } break; case T_Param: { Param *p = (Param *) tle->expr; if (!type_is_rowtype(p->paramtype)) ereport(ERROR, (errcode(ERRCODE_DATATYPE_MISMATCH), errmsg("function does not return composite type, is not possible to identify composite type"))); FreeTupleDesc(tupdesc); tupdesc = param_get_desc(cstate, p); } break; default: /* cannot to take tupdesc */ FreeTupleDesc(tupdesc); tupdesc = NULL; } } } ReleaseCachedPlan(cplan, CurrentResourceOwner); } /* attention: returned tupdesc can have zero columns */ return tupdesc; } okbob-plpgsql_check-800db79/typedefs_gen.py000077500000000000000000000060501526744060500210250ustar00rootroot00000000000000#!/usr/bin/env python3 # coding: utf-8 import glob import os import platform import subprocess def is_objdump(execname): try: output = subprocess.run([execname, '-v'], stdout=subprocess.PIPE, stderr=subprocess.PIPE).stdout.decode('utf-8') except: return False return output.startswith('GNU objdump') def find_objdump(): execname = os.getenv('OBJDUMP') if execname and is_objdump(execname): return execname if is_objdump('objdump'): return 'objdump' if is_objdump('gobjdump'): return 'gobjdump' raise Exception('objdump not found') def has_dwarfdump(): try: result = subprocess.run(['dwarfdump', '--version'], stdout=subprocess.PIPE, stderr=subprocess.PIPE) return result.returncode == 0 except (FileNotFoundError, OSError): return False def extract_typedefs_dwarfdump(): """Extract typedefs using dwarfdump (macOS)""" result = subprocess.run(['dwarfdump', '--debug-info'] + glob.glob('src/*/*.o') + glob.glob('src/*.o'), stdout=subprocess.PIPE, stderr=subprocess.PIPE) if result.returncode != 0: raise Exception( f"dwarfdump failed with code {result.returncode}: {result.stderr.decode('utf-8')}" ) output = result.stdout.decode('utf-8') typenames = [] in_typedef = False for line in output.splitlines(): if 'DW_TAG_typedef' in line: # Start tracking a new typedef block in_typedef = True continue if in_typedef: if 'DW_AT_name' in line: # Extract the name from lines like: # DW_AT_name ("TypeName") if '"' in line: parts = line.split('"') if len(parts) >= 2: typenames.append(parts[1]) in_typedef = False elif 'DW_TAG_' in line: # Reset if we encounter another tag without finding DW_AT_name in_typedef = False return typenames def extract_typedefs_objdump(): """Extract typedefs using objdump (Linux/Ubuntu)""" result = subprocess.run([find_objdump(), '-W'] + glob.glob('src/*/*.o') + glob.glob('src/*.o'), stdout=subprocess.PIPE, stderr=subprocess.PIPE) if result.returncode != 0: raise Exception( f"objdump failed with code {result.returncode}: {result.stderr.decode('utf-8')}" ) output = result.stdout.decode('utf-8') typenames = [] i = 3 for line in output.splitlines(): if line.find('DW_TAG_typedef') >= 0: i = 0 continue i = i + 1 if i > 3: continue fields = line.split() if len(fields) < 2: continue if fields[0] != 'DW_AT_name' and fields[1] != 'DW_AT_name': continue if fields[-1].startswith('DW_FORM_str'): continue typenames.append(fields[-1]) return typenames if platform.system() == 'Darwin' and has_dwarfdump(): print("Using dwarfdump (macOS)") typenames = extract_typedefs_dwarfdump() else: print("Using objdump") typenames = extract_typedefs_objdump() file = open('plpgsql_check.typedefs', 'w') file.write("\n".join(sorted(set(typenames))) + "\n") file.close()