summaryrefslogtreecommitdiffstats
path: root/src/pl/tcl/sql
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-13 13:44:03 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-13 13:44:03 +0000
commit293913568e6a7a86fd1479e1cff8e2ecb58d6568 (patch)
treefc3b469a3ec5ab71b36ea97cc7aaddb838423a0c /src/pl/tcl/sql
parentInitial commit. (diff)
downloadpostgresql-16-293913568e6a7a86fd1479e1cff8e2ecb58d6568.tar.xz
postgresql-16-293913568e6a7a86fd1479e1cff8e2ecb58d6568.zip
Adding upstream version 16.2.upstream/16.2
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/pl/tcl/sql')
-rw-r--r--src/pl/tcl/sql/pltcl_call.sql78
-rw-r--r--src/pl/tcl/sql/pltcl_queries.sql166
-rw-r--r--src/pl/tcl/sql/pltcl_setup.sql278
-rw-r--r--src/pl/tcl/sql/pltcl_start_proc.sql21
-rw-r--r--src/pl/tcl/sql/pltcl_subxact.sql95
-rw-r--r--src/pl/tcl/sql/pltcl_transaction.sql135
-rw-r--r--src/pl/tcl/sql/pltcl_trigger.sql603
-rw-r--r--src/pl/tcl/sql/pltcl_unicode.sql38
8 files changed, 1414 insertions, 0 deletions
diff --git a/src/pl/tcl/sql/pltcl_call.sql b/src/pl/tcl/sql/pltcl_call.sql
new file mode 100644
index 0000000..37efbde
--- /dev/null
+++ b/src/pl/tcl/sql/pltcl_call.sql
@@ -0,0 +1,78 @@
+CREATE PROCEDURE test_proc1()
+LANGUAGE pltcl
+AS $$
+unset
+$$;
+
+CALL test_proc1();
+
+
+CREATE PROCEDURE test_proc2()
+LANGUAGE pltcl
+AS $$
+return 5
+$$;
+
+CALL test_proc2();
+
+
+CREATE TABLE test1 (a int);
+
+CREATE PROCEDURE test_proc3(x int)
+LANGUAGE pltcl
+AS $$
+spi_exec "INSERT INTO test1 VALUES ($1)"
+$$;
+
+CALL test_proc3(55);
+
+SELECT * FROM test1;
+
+
+-- output arguments
+
+CREATE PROCEDURE test_proc5(INOUT a text)
+LANGUAGE pltcl
+AS $$
+set aa [concat $1 "+" $1]
+return [list a $aa]
+$$;
+
+CALL test_proc5('abc');
+
+
+CREATE PROCEDURE test_proc6(a int, INOUT b int, INOUT c int)
+LANGUAGE pltcl
+AS $$
+set bb [expr $2 * $1]
+set cc [expr $3 * $1]
+return [list b $bb c $cc]
+$$;
+
+CALL test_proc6(2, 3, 4);
+
+
+-- OUT parameters
+
+CREATE PROCEDURE test_proc9(IN a int, OUT b int)
+LANGUAGE pltcl
+AS $$
+elog NOTICE "a: $1"
+return [list b [expr {$1 * 2}]]
+$$;
+
+DO $$
+DECLARE _a int; _b int;
+BEGIN
+ _a := 10; _b := 30;
+ CALL test_proc9(_a, _b);
+ RAISE NOTICE '_a: %, _b: %', _a, _b;
+END
+$$;
+
+
+DROP PROCEDURE test_proc1;
+DROP PROCEDURE test_proc2;
+DROP PROCEDURE test_proc3;
+
+DROP TABLE test1;
diff --git a/src/pl/tcl/sql/pltcl_queries.sql b/src/pl/tcl/sql/pltcl_queries.sql
new file mode 100644
index 0000000..bbd2d97
--- /dev/null
+++ b/src/pl/tcl/sql/pltcl_queries.sql
@@ -0,0 +1,166 @@
+-- suppress CONTEXT so that function OIDs aren't in output
+\set VERBOSITY terse
+
+-- Test composite-type arguments
+select tcl_composite_arg_ref1(row('tkey', 42, 'ref2'));
+select tcl_composite_arg_ref2(row('tkey', 42, 'ref2'));
+
+-- More tests for composite argument/result types
+
+create domain d_comp1 as T_comp1 check ((value).ref1 > 0);
+
+create function tcl_record_arg(record, fldname text) returns int as '
+ return $1($2)
+' language pltcl;
+
+select tcl_record_arg(row('tkey', 42, 'ref2')::T_comp1, 'ref1');
+select tcl_record_arg(row('tkey', 42, 'ref2')::d_comp1, 'ref1');
+select tcl_record_arg(row(2,4), 'f2');
+
+create function tcl_cdomain_arg(d_comp1) returns int as '
+ return $1(ref1)
+' language pltcl;
+
+select tcl_cdomain_arg(row('tkey', 42, 'ref2'));
+select tcl_cdomain_arg(row('tkey', 42, 'ref2')::T_comp1);
+select tcl_cdomain_arg(row('tkey', -1, 'ref2')); -- fail
+
+-- Test argisnull primitive
+select tcl_argisnull('foo');
+select tcl_argisnull('');
+select tcl_argisnull(null);
+
+-- test some error cases
+create function tcl_error(out a int, out b int) as $$return {$$ language pltcl;
+select tcl_error();
+
+create function bad_record(out a text, out b text) as $$return [list a]$$ language pltcl;
+select bad_record();
+
+create function bad_field(out a text, out b text) as $$return [list a 1 b 2 cow 3]$$ language pltcl;
+select bad_field();
+
+-- test compound return
+select * from tcl_test_cube_squared(5);
+
+-- test SRF
+select * from tcl_test_squared_rows(0,5);
+
+select * from tcl_test_sequence(0,5) as a;
+
+select 1, tcl_test_sequence(0,5);
+
+create function non_srf() returns int as $$return_next 1$$ language pltcl;
+select non_srf();
+
+create function bad_record_srf(out a text, out b text) returns setof record as $$
+return_next [list a]
+$$ language pltcl;
+select bad_record_srf();
+
+create function bad_field_srf(out a text, out b text) returns setof record as $$
+return_next [list a 1 b 2 cow 3]
+$$ language pltcl;
+select bad_field_srf();
+
+-- test composite and domain-over-composite results
+create function tcl_composite_result(int) returns T_comp1 as $$
+return [list tkey tkey1 ref1 $1 ref2 ref22]
+$$ language pltcl;
+select tcl_composite_result(1001);
+select * from tcl_composite_result(1002);
+
+create function tcl_dcomposite_result(int) returns d_comp1 as $$
+return [list tkey tkey2 ref1 $1 ref2 ref42]
+$$ language pltcl;
+select tcl_dcomposite_result(1001);
+select * from tcl_dcomposite_result(1002);
+select * from tcl_dcomposite_result(-1); -- fail
+
+create function tcl_record_result(int) returns record as $$
+return [list q1 sometext q2 $1 q3 moretext]
+$$ language pltcl;
+select tcl_record_result(42); -- fail
+select * from tcl_record_result(42); -- fail
+select * from tcl_record_result(42) as (q1 text, q2 int, q3 text);
+select * from tcl_record_result(42) as (q1 text, q2 int, q3 text, q4 int);
+select * from tcl_record_result(42) as (q1 text, q2 int, q4 int); -- fail
+
+-- test quote
+select tcl_eval('quote foo bar');
+select tcl_eval('quote [format %c 39]');
+select tcl_eval('quote [format %c 92]');
+
+-- Test argisnull
+select tcl_eval('argisnull');
+select tcl_eval('argisnull 14');
+select tcl_eval('argisnull abc');
+
+-- Test return_null
+select tcl_eval('return_null 14');
+
+-- Test spi_exec
+select tcl_eval('spi_exec');
+select tcl_eval('spi_exec -count');
+select tcl_eval('spi_exec -array');
+select tcl_eval('spi_exec -count abc');
+select tcl_eval('spi_exec query loop body toomuch');
+select tcl_eval('spi_exec "begin; rollback;"');
+
+-- Test spi_execp
+select tcl_eval('spi_execp');
+select tcl_eval('spi_execp -count');
+select tcl_eval('spi_execp -array');
+select tcl_eval('spi_execp -count abc');
+select tcl_eval('spi_execp -nulls');
+select tcl_eval('spi_execp ""');
+
+-- test spi_prepare
+select tcl_eval('spi_prepare');
+select tcl_eval('spi_prepare a b');
+select tcl_eval('spi_prepare a "b {"');
+select tcl_error_handling_test($tcl$spi_prepare "select moo" []$tcl$);
+
+-- test full error text
+select tcl_error_handling_test($tcl$
+spi_exec "DO $$
+BEGIN
+RAISE 'my message'
+ USING HINT = 'my hint'
+ , DETAIL = 'my detail'
+ , SCHEMA = 'my schema'
+ , TABLE = 'my table'
+ , COLUMN = 'my column'
+ , CONSTRAINT = 'my constraint'
+ , DATATYPE = 'my datatype'
+;
+END$$;"
+$tcl$);
+
+-- verify tcl_error_handling_test() properly reports non-postgres errors
+select tcl_error_handling_test('moo');
+
+-- test elog
+select tcl_eval('elog');
+select tcl_eval('elog foo bar');
+
+-- test forced error
+select tcl_eval('error "forced error"');
+
+-- test loop control in spi_exec[p]
+select tcl_spi_exec(true, 'break');
+select tcl_spi_exec(true, 'continue');
+select tcl_spi_exec(true, 'error');
+select tcl_spi_exec(true, 'return');
+select tcl_spi_exec(false, 'break');
+select tcl_spi_exec(false, 'continue');
+select tcl_spi_exec(false, 'error');
+select tcl_spi_exec(false, 'return');
+
+-- forcibly run the Tcl event loop for awhile, to check that we have not
+-- messed things up too badly by disabling the Tcl notifier subsystem
+select tcl_eval($$
+ unset -nocomplain ::tcl_vwait
+ after 100 {set ::tcl_vwait 1}
+ vwait ::tcl_vwait
+ unset -nocomplain ::tcl_vwait$$);
diff --git a/src/pl/tcl/sql/pltcl_setup.sql b/src/pl/tcl/sql/pltcl_setup.sql
new file mode 100644
index 0000000..b9892ea
--- /dev/null
+++ b/src/pl/tcl/sql/pltcl_setup.sql
@@ -0,0 +1,278 @@
+create table T_comp1 (
+ tkey char(10),
+ ref1 int4,
+ ref2 char(20)
+);
+
+create function tcl_composite_arg_ref1(T_comp1) returns int as '
+ return $1(ref1)
+' language pltcl;
+
+create function tcl_composite_arg_ref2(T_comp1) returns text as '
+ return $1(ref2)
+' language pltcl;
+
+create function tcl_argisnull(text) returns bool as '
+ argisnull 1
+' language pltcl;
+
+
+create function tcl_int4add(int4,int4) returns int4 as '
+ return [expr $1 + $2]
+' language pltcl;
+
+-- We use split(n) as a quick-and-dirty way of parsing the input array
+-- value, which comes in as a string like '{1,2}'. There are better ways...
+
+create function tcl_int4_accum(int4[], int4) returns int4[] as '
+ set state [split $1 "{,}"]
+ set newsum [expr {[lindex $state 1] + $2}]
+ set newcnt [expr {[lindex $state 2] + 1}]
+ return "{$newsum,$newcnt}"
+' language pltcl;
+
+create function tcl_int4_avg(int4[]) returns int4 as '
+ set state [split $1 "{,}"]
+ if {[lindex $state 2] == 0} { return_null }
+ return [expr {[lindex $state 1] / [lindex $state 2]}]
+' language pltcl;
+
+create aggregate tcl_avg (
+ sfunc = tcl_int4_accum,
+ basetype = int4,
+ stype = int4[],
+ finalfunc = tcl_int4_avg,
+ initcond = '{0,0}'
+ );
+
+create aggregate tcl_sum (
+ sfunc = tcl_int4add,
+ basetype = int4,
+ stype = int4,
+ initcond1 = 0
+ );
+
+create function tcl_int4lt(int4,int4) returns bool as '
+ if {$1 < $2} {
+ return t
+ }
+ return f
+' language pltcl;
+
+create function tcl_int4le(int4,int4) returns bool as '
+ if {$1 <= $2} {
+ return t
+ }
+ return f
+' language pltcl;
+
+create function tcl_int4eq(int4,int4) returns bool as '
+ if {$1 == $2} {
+ return t
+ }
+ return f
+' language pltcl;
+
+create function tcl_int4ge(int4,int4) returns bool as '
+ if {$1 >= $2} {
+ return t
+ }
+ return f
+' language pltcl;
+
+create function tcl_int4gt(int4,int4) returns bool as '
+ if {$1 > $2} {
+ return t
+ }
+ return f
+' language pltcl;
+
+create operator @< (
+ leftarg = int4,
+ rightarg = int4,
+ procedure = tcl_int4lt
+ );
+
+create operator @<= (
+ leftarg = int4,
+ rightarg = int4,
+ procedure = tcl_int4le
+ );
+
+create operator @= (
+ leftarg = int4,
+ rightarg = int4,
+ procedure = tcl_int4eq
+ );
+
+create operator @>= (
+ leftarg = int4,
+ rightarg = int4,
+ procedure = tcl_int4ge
+ );
+
+create operator @> (
+ leftarg = int4,
+ rightarg = int4,
+ procedure = tcl_int4gt
+ );
+
+create function tcl_int4cmp(int4,int4) returns int4 as '
+ if {$1 < $2} {
+ return -1
+ }
+ if {$1 > $2} {
+ return 1
+ }
+ return 0
+' language pltcl;
+
+CREATE OPERATOR CLASS tcl_int4_ops
+ FOR TYPE int4 USING btree AS
+ OPERATOR 1 @<,
+ OPERATOR 2 @<=,
+ OPERATOR 3 @=,
+ OPERATOR 4 @>=,
+ OPERATOR 5 @>,
+ FUNCTION 1 tcl_int4cmp(int4,int4) ;
+
+--
+-- Test usage of Tcl's "clock" command. In recent Tcl versions this
+-- command fails without working "unknown" support, so it's a good canary
+-- for initialization problems.
+--
+create function tcl_date_week(int4,int4,int4) returns text as $$
+ return [clock format [clock scan "$2/$3/$1" -gmt 1] -format "%U" -gmt 1]
+$$ language pltcl immutable;
+
+select tcl_date_week(2010,1,26);
+select tcl_date_week(2001,10,24);
+
+-- test pltcl event triggers
+create function tclsnitch() returns event_trigger language pltcl as $$
+ elog NOTICE "tclsnitch: $TG_event $TG_tag"
+$$;
+
+create event trigger tcl_a_snitch on ddl_command_start execute procedure tclsnitch();
+create event trigger tcl_b_snitch on ddl_command_end execute procedure tclsnitch();
+
+create function foobar() returns int language sql as $$select 1;$$;
+alter function foobar() cost 77;
+drop function foobar();
+
+create table foo();
+drop table foo;
+
+drop event trigger tcl_a_snitch;
+drop event trigger tcl_b_snitch;
+
+create function tcl_test_cube_squared(in int, out squared int, out cubed int) as $$
+ return [list squared [expr {$1 * $1}] cubed [expr {$1 * $1 * $1}]]
+$$ language pltcl;
+
+create function tcl_test_squared_rows(int,int) returns table (x int, y int) as $$
+ for {set i $1} {$i < $2} {incr i} {
+ return_next [list y [expr {$i * $i}] x $i]
+ }
+$$ language pltcl;
+
+create function tcl_test_sequence(int,int) returns setof int as $$
+ for {set i $1} {$i < $2} {incr i} {
+ return_next $i
+ }
+$$ language pltcl;
+
+create function tcl_eval(string text) returns text as $$
+ eval $1
+$$ language pltcl;
+
+-- test use of errorCode in error handling
+create function tcl_error_handling_test(text) returns text
+language pltcl
+as $function$
+ if {[catch $1 err]} {
+ # If not a Postgres error, just return the basic error message
+ if {[lindex $::errorCode 0] != "POSTGRES"} {
+ return $err
+ }
+
+ # Get rid of keys that can't be expected to remain constant
+ array set myArray $::errorCode
+ unset myArray(POSTGRES)
+ unset -nocomplain myArray(funcname)
+ unset -nocomplain myArray(filename)
+ unset -nocomplain myArray(lineno)
+
+ # Format into something nicer
+ set vals []
+ foreach {key} [lsort [array names myArray]] {
+ set value [string map {"\n" "\n\t"} $myArray($key)]
+ lappend vals "$key: $value"
+ }
+ return [join $vals "\n"]
+ } else {
+ return "no error"
+ }
+$function$;
+
+-- test spi_exec and spi_execp with -array
+create function tcl_spi_exec(
+ prepare boolean,
+ action text
+)
+returns void language pltcl AS $function$
+set query "select * from (values (1,'foo'),(2,'bar'),(3,'baz')) v(col1,col2)"
+if {$1 == "t"} {
+ set prep [spi_prepare $query {}]
+ spi_execp -array A $prep {
+ elog NOTICE "col1 $A(col1), col2 $A(col2)"
+
+ switch $A(col1) {
+ 2 {
+ elog NOTICE "action: $2"
+ switch $2 {
+ break {
+ break
+ }
+ continue {
+ continue
+ }
+ return {
+ return
+ }
+ error {
+ error "error message"
+ }
+ }
+ error "should not get here"
+ }
+ }
+ }
+} else {
+ spi_exec -array A $query {
+ elog NOTICE "col1 $A(col1), col2 $A(col2)"
+
+ switch $A(col1) {
+ 2 {
+ elog NOTICE "action: $2"
+ switch $2 {
+ break {
+ break
+ }
+ continue {
+ continue
+ }
+ return {
+ return
+ }
+ error {
+ error "error message"
+ }
+ }
+ error "should not get here"
+ }
+ }
+ }
+}
+elog NOTICE "end of function"
+$function$;
diff --git a/src/pl/tcl/sql/pltcl_start_proc.sql b/src/pl/tcl/sql/pltcl_start_proc.sql
new file mode 100644
index 0000000..7a8e68e
--- /dev/null
+++ b/src/pl/tcl/sql/pltcl_start_proc.sql
@@ -0,0 +1,21 @@
+--
+-- Test start_proc execution
+--
+
+SET pltcl.start_proc = 'no_such_function';
+
+select tcl_int4add(1, 2);
+select tcl_int4add(1, 2);
+
+create function tcl_initialize() returns void as
+$$ elog NOTICE "in tcl_initialize" $$ language pltcl SECURITY DEFINER;
+
+SET pltcl.start_proc = 'public.tcl_initialize';
+
+select tcl_int4add(1, 2); -- fail
+
+create or replace function tcl_initialize() returns void as
+$$ elog NOTICE "in tcl_initialize" $$ language pltcl;
+
+select tcl_int4add(1, 2);
+select tcl_int4add(1, 2);
diff --git a/src/pl/tcl/sql/pltcl_subxact.sql b/src/pl/tcl/sql/pltcl_subxact.sql
new file mode 100644
index 0000000..0625736
--- /dev/null
+++ b/src/pl/tcl/sql/pltcl_subxact.sql
@@ -0,0 +1,95 @@
+--
+-- Test explicit subtransactions
+--
+
+CREATE TABLE subtransaction_tbl (
+ i integer
+);
+
+--
+-- We use this wrapper to catch errors and return errormsg only,
+-- because values of $::errorinfo variable contain procedure name which
+-- includes OID, so it's not stable
+--
+CREATE FUNCTION pltcl_wrapper(statement text) RETURNS text
+AS $$
+ if [catch {spi_exec $1} msg] {
+ return "ERROR: $msg"
+ } else {
+ return "SUCCESS: $msg"
+ }
+$$ LANGUAGE pltcl;
+
+-- Test subtransaction successfully committed
+
+CREATE FUNCTION subtransaction_ctx_success() RETURNS void
+AS $$
+ spi_exec "INSERT INTO subtransaction_tbl VALUES(1)"
+ subtransaction {
+ spi_exec "INSERT INTO subtransaction_tbl VALUES(2)"
+ }
+$$ LANGUAGE pltcl;
+
+BEGIN;
+INSERT INTO subtransaction_tbl VALUES(0);
+SELECT subtransaction_ctx_success();
+COMMIT;
+SELECT * FROM subtransaction_tbl;
+TRUNCATE subtransaction_tbl;
+
+-- Test subtransaction rollback
+
+CREATE FUNCTION subtransaction_ctx_test(what_error text = NULL) RETURNS void
+AS $$
+ spi_exec "INSERT INTO subtransaction_tbl VALUES (1)"
+ subtransaction {
+ spi_exec "INSERT INTO subtransaction_tbl VALUES (2)"
+ if {$1 == "SPI"} {
+ spi_exec "INSERT INTO subtransaction_tbl VALUES ('oops')"
+ } elseif { $1 == "Tcl"} {
+ elog ERROR "Tcl error"
+ }
+ }
+$$ LANGUAGE pltcl;
+
+SELECT pltcl_wrapper('SELECT subtransaction_ctx_test()');
+SELECT * FROM subtransaction_tbl;
+TRUNCATE subtransaction_tbl;
+
+SELECT pltcl_wrapper('SELECT subtransaction_ctx_test(''SPI'')');
+SELECT * FROM subtransaction_tbl;
+TRUNCATE subtransaction_tbl;
+
+SELECT pltcl_wrapper('SELECT subtransaction_ctx_test(''Tcl'')');
+SELECT * FROM subtransaction_tbl;
+TRUNCATE subtransaction_tbl;
+
+-- Nested subtransactions
+
+CREATE FUNCTION subtransaction_nested_test(swallow boolean = 'f') RETURNS text
+AS $$
+spi_exec "INSERT INTO subtransaction_tbl VALUES (1)"
+subtransaction {
+ spi_exec "INSERT INTO subtransaction_tbl VALUES (2)"
+ if [catch {
+ subtransaction {
+ spi_exec "INSERT INTO subtransaction_tbl VALUES (3)"
+ spi_exec "error"
+ }
+ } errormsg] {
+ if {$1 != "t"} {
+ error $errormsg $::errorInfo $::errorCode
+ }
+ elog NOTICE "Swallowed $errormsg"
+ }
+}
+return "ok"
+$$ LANGUAGE pltcl;
+
+SELECT pltcl_wrapper('SELECT subtransaction_nested_test()');
+SELECT * FROM subtransaction_tbl;
+TRUNCATE subtransaction_tbl;
+
+SELECT pltcl_wrapper('SELECT subtransaction_nested_test(''t'')');
+SELECT * FROM subtransaction_tbl;
+TRUNCATE subtransaction_tbl;
diff --git a/src/pl/tcl/sql/pltcl_transaction.sql b/src/pl/tcl/sql/pltcl_transaction.sql
new file mode 100644
index 0000000..bd75985
--- /dev/null
+++ b/src/pl/tcl/sql/pltcl_transaction.sql
@@ -0,0 +1,135 @@
+-- suppress CONTEXT so that function OIDs aren't in output
+\set VERBOSITY terse
+
+CREATE TABLE test1 (a int, b text);
+
+
+CREATE PROCEDURE transaction_test1()
+LANGUAGE pltcl
+AS $$
+for {set i 0} {$i < 10} {incr i} {
+ spi_exec "INSERT INTO test1 (a) VALUES ($i)"
+ if {$i % 2 == 0} {
+ commit
+ } else {
+ rollback
+ }
+}
+$$;
+
+CALL transaction_test1();
+
+SELECT * FROM test1;
+
+
+TRUNCATE test1;
+
+-- not allowed in a function
+CREATE FUNCTION transaction_test2() RETURNS int
+LANGUAGE pltcl
+AS $$
+for {set i 0} {$i < 10} {incr i} {
+ spi_exec "INSERT INTO test1 (a) VALUES ($i)"
+ if {$i % 2 == 0} {
+ commit
+ } else {
+ rollback
+ }
+}
+return 1
+$$;
+
+SELECT transaction_test2();
+
+SELECT * FROM test1;
+
+
+-- also not allowed if procedure is called from a function
+CREATE FUNCTION transaction_test3() RETURNS int
+LANGUAGE pltcl
+AS $$
+spi_exec "CALL transaction_test1()"
+return 1
+$$;
+
+SELECT transaction_test3();
+
+SELECT * FROM test1;
+
+
+-- commit inside cursor loop
+CREATE TABLE test2 (x int);
+INSERT INTO test2 VALUES (0), (1), (2), (3), (4);
+
+TRUNCATE test1;
+
+CREATE PROCEDURE transaction_test4a()
+LANGUAGE pltcl
+AS $$
+spi_exec -array row "SELECT * FROM test2 ORDER BY x" {
+ spi_exec "INSERT INTO test1 (a) VALUES ($row(x))"
+ commit
+}
+$$;
+
+CALL transaction_test4a();
+
+SELECT * FROM test1;
+
+
+-- rollback inside cursor loop
+TRUNCATE test1;
+
+CREATE PROCEDURE transaction_test4b()
+LANGUAGE pltcl
+AS $$
+spi_exec -array row "SELECT * FROM test2 ORDER BY x" {
+ spi_exec "INSERT INTO test1 (a) VALUES ($row(x))"
+ rollback
+}
+$$;
+
+CALL transaction_test4b();
+
+SELECT * FROM test1;
+
+
+-- check handling of an error during COMMIT
+CREATE TABLE testpk (id int PRIMARY KEY);
+CREATE TABLE testfk(f1 int REFERENCES testpk DEFERRABLE INITIALLY DEFERRED);
+
+CREATE PROCEDURE transaction_testfk()
+LANGUAGE pltcl
+AS $$
+# this insert will fail during commit:
+spi_exec "INSERT INTO testfk VALUES (0)"
+commit
+elog WARNING "should not get here"
+$$;
+
+CALL transaction_testfk();
+
+SELECT * FROM testpk;
+SELECT * FROM testfk;
+
+CREATE OR REPLACE PROCEDURE transaction_testfk()
+LANGUAGE pltcl
+AS $$
+# this insert will fail during commit:
+spi_exec "INSERT INTO testfk VALUES (0)"
+if [catch {commit} msg] {
+ elog INFO $msg
+}
+# these inserts should work:
+spi_exec "INSERT INTO testpk VALUES (1)"
+spi_exec "INSERT INTO testfk VALUES (1)"
+$$;
+
+CALL transaction_testfk();
+
+SELECT * FROM testpk;
+SELECT * FROM testfk;
+
+
+DROP TABLE test1;
+DROP TABLE test2;
diff --git a/src/pl/tcl/sql/pltcl_trigger.sql b/src/pl/tcl/sql/pltcl_trigger.sql
new file mode 100644
index 0000000..2db75a3
--- /dev/null
+++ b/src/pl/tcl/sql/pltcl_trigger.sql
@@ -0,0 +1,603 @@
+-- suppress CONTEXT so that function OIDs aren't in output
+\set VERBOSITY terse
+
+--
+-- Create the tables used in the test queries
+--
+-- T_pkey1 is the primary key table for T_dta1. Entries from T_pkey1
+-- Cannot be changed or deleted if they are referenced from T_dta1.
+--
+-- T_pkey2 is the primary key table for T_dta2. If the key values in
+-- T_pkey2 are changed, the references in T_dta2 follow. If entries
+-- are deleted, the referencing entries from T_dta2 are deleted too.
+-- The values for field key2 in T_pkey2 are silently converted to
+-- upper case on insert/update.
+--
+create table T_pkey1 (
+ key1 int4,
+ key2 char(20),
+ txt char(40)
+);
+
+create table T_pkey2 (
+ key1 int4,
+ key2 char(20),
+ txt char(40)
+);
+
+create table T_dta1 (
+ tkey char(10),
+ ref1 int4,
+ ref2 char(20)
+);
+
+create table T_dta2 (
+ tkey char(10),
+ ref1 int4,
+ ref2 char(20)
+);
+
+
+--
+-- Function to check key existence in T_pkey1
+--
+create function check_pkey1_exists(int4, bpchar) returns bool as E'
+ if {![info exists GD]} {
+ set GD(plan) [spi_prepare \\
+ "select 1 from T_pkey1 \\
+ where key1 = \\$1 and key2 = \\$2" \\
+ {int4 bpchar}]
+ }
+
+ set n [spi_execp -count 1 $GD(plan) [list $1 $2]]
+
+ if {$n > 0} {
+ return "t"
+ }
+ return "f"
+' language pltcl;
+
+
+-- dump trigger data
+
+CREATE TABLE trigger_test (
+ i int,
+ v text,
+ dropme text,
+ test_skip boolean DEFAULT false,
+ test_return_null boolean DEFAULT false,
+ test_argisnull boolean DEFAULT false
+);
+-- Make certain dropped attributes are handled correctly
+ALTER TABLE trigger_test DROP dropme;
+
+CREATE TABLE trigger_test_generated (
+ i int,
+ j int GENERATED ALWAYS AS (i * 2) STORED
+);
+
+CREATE VIEW trigger_test_view AS SELECT i, v FROM trigger_test;
+
+CREATE FUNCTION trigger_data() returns trigger language pltcl as $_$
+ if {$TG_table_name eq "trigger_test" && $TG_level eq "ROW" && $TG_op ne "DELETE"} {
+ # Special case tests
+ if {$NEW(test_return_null) eq "t" } {
+ return_null
+ }
+ if {$NEW(test_argisnull) eq "t" } {
+ set should_error [argisnull 1]
+ }
+ if {$NEW(test_skip) eq "t" } {
+ elog NOTICE "SKIPPING OPERATION $TG_op"
+ return SKIP
+ }
+ }
+
+ if { [info exists TG_relid] } {
+ set TG_relid "bogus:12345"
+ }
+
+ set dnames [info locals {[a-zA-Z]*} ]
+
+ foreach key [lsort $dnames] {
+
+ if { [array exists $key] } {
+ set str "{"
+ foreach akey [lsort [ array names $key ] ] {
+ if {[string length $str] > 1} { set str "$str, " }
+ set cmd "($akey)"
+ set cmd "set val \$$key$cmd"
+ eval $cmd
+ set str "$str$akey: $val"
+ }
+ set str "$str}"
+ elog NOTICE "$key: $str"
+ } else {
+ set val [eval list "\$$key" ]
+ elog NOTICE "$key: $val"
+ }
+ }
+
+
+ return OK
+
+$_$;
+
+CREATE TRIGGER show_trigger_data_trig
+BEFORE INSERT OR UPDATE OR DELETE ON trigger_test
+FOR EACH ROW EXECUTE PROCEDURE trigger_data(23,'skidoo');
+CREATE TRIGGER statement_trigger
+BEFORE INSERT OR UPDATE OR DELETE OR TRUNCATE ON trigger_test
+FOR EACH STATEMENT EXECUTE PROCEDURE trigger_data(42,'statement trigger');
+
+CREATE TRIGGER show_trigger_data_trig_before
+BEFORE INSERT OR UPDATE OR DELETE ON trigger_test_generated
+FOR EACH ROW EXECUTE PROCEDURE trigger_data();
+CREATE TRIGGER show_trigger_data_trig_after
+AFTER INSERT OR UPDATE OR DELETE ON trigger_test_generated
+FOR EACH ROW EXECUTE PROCEDURE trigger_data();
+
+CREATE TRIGGER show_trigger_data_view_trig
+INSTEAD OF INSERT OR UPDATE OR DELETE ON trigger_test_view
+FOR EACH ROW EXECUTE PROCEDURE trigger_data(24,'skidoo view');
+
+--
+-- Trigger function on every change to T_pkey1
+--
+create function trig_pkey1_before() returns trigger as E'
+ #
+ # Create prepared plans on the first call
+ #
+ if {![info exists GD]} {
+ #
+ # Plan to check for duplicate key in T_pkey1
+ #
+ set GD(plan_pkey1) [spi_prepare \\
+ "select check_pkey1_exists(\\$1, \\$2) as ret" \\
+ {int4 bpchar}]
+ #
+ # Plan to check for references from T_dta1
+ #
+ set GD(plan_dta1) [spi_prepare \\
+ "select 1 from T_dta1 \\
+ where ref1 = \\$1 and ref2 = \\$2" \\
+ {int4 bpchar}]
+ }
+
+ #
+ # Initialize flags
+ #
+ set check_old_ref 0
+ set check_new_dup 0
+
+ switch $TG_op {
+ INSERT {
+ #
+ # Must check for duplicate key on INSERT
+ #
+ set check_new_dup 1
+ }
+ UPDATE {
+ #
+ # Must check for duplicate key on UPDATE only if
+ # the key changes. In that case we must check for
+ # references to OLD values too.
+ #
+ if {[string compare $NEW(key1) $OLD(key1)] != 0} {
+ set check_old_ref 1
+ set check_new_dup 1
+ }
+ if {[string compare $NEW(key2) $OLD(key2)] != 0} {
+ set check_old_ref 1
+ set check_new_dup 1
+ }
+ }
+ DELETE {
+ #
+ # Must only check for references to OLD on DELETE
+ #
+ set check_old_ref 1
+ }
+ }
+
+ if {$check_new_dup} {
+ #
+ # Check for duplicate key
+ #
+ spi_execp -count 1 $GD(plan_pkey1) [list $NEW(key1) $NEW(key2)]
+ if {$ret == "t"} {
+ elog ERROR \\
+ "duplicate key ''$NEW(key1)'', ''$NEW(key2)'' for T_pkey1"
+ }
+ }
+
+ if {$check_old_ref} {
+ #
+ # Check for references to OLD
+ #
+ set n [spi_execp -count 1 $GD(plan_dta1) [list $OLD(key1) $OLD(key2)]]
+ if {$n > 0} {
+ elog ERROR \\
+ "key ''$OLD(key1)'', ''$OLD(key2)'' referenced by T_dta1"
+ }
+ }
+
+ #
+ # Anything is fine - let operation pass through
+ #
+ return OK
+' language pltcl;
+
+
+create trigger pkey1_before before insert or update or delete on T_pkey1
+ for each row execute procedure
+ trig_pkey1_before();
+
+
+--
+-- Trigger function to check for duplicate keys in T_pkey2
+-- and to force key2 to be upper case only without leading whitespaces
+--
+create function trig_pkey2_before() returns trigger as E'
+ #
+ # Prepare plan on first call
+ #
+ if {![info exists GD]} {
+ set GD(plan_pkey2) [spi_prepare \\
+ "select 1 from T_pkey2 \\
+ where key1 = \\$1 and key2 = \\$2" \\
+ {int4 bpchar}]
+ }
+
+ #
+ # Convert key2 value
+ #
+ set NEW(key2) [string toupper [string trim $NEW(key2)]]
+
+ #
+ # Check for duplicate key
+ #
+ set n [spi_execp -count 1 $GD(plan_pkey2) [list $NEW(key1) $NEW(key2)]]
+ if {$n > 0} {
+ elog ERROR \\
+ "duplicate key ''$NEW(key1)'', ''$NEW(key2)'' for T_pkey2"
+ }
+
+ #
+ # Return modified tuple in NEW
+ #
+ return [array get NEW]
+' language pltcl;
+
+
+create trigger pkey2_before before insert or update on T_pkey2
+ for each row execute procedure
+ trig_pkey2_before();
+
+
+--
+-- Trigger function to force references from T_dta2 follow changes
+-- in T_pkey2 or be deleted too. This must be done AFTER the changes
+-- in T_pkey2 are done so the trigger for primkey check on T_dta2
+-- fired on our updates will see the new key values in T_pkey2.
+--
+create function trig_pkey2_after() returns trigger as E'
+ #
+ # Prepare plans on first call
+ #
+ if {![info exists GD]} {
+ #
+ # Plan to update references from T_dta2
+ #
+ set GD(plan_dta2_upd) [spi_prepare \\
+ "update T_dta2 set ref1 = \\$3, ref2 = \\$4 \\
+ where ref1 = \\$1 and ref2 = \\$2" \\
+ {int4 bpchar int4 bpchar}]
+ #
+ # Plan to delete references from T_dta2
+ #
+ set GD(plan_dta2_del) [spi_prepare \\
+ "delete from T_dta2 \\
+ where ref1 = \\$1 and ref2 = \\$2" \\
+ {int4 bpchar}]
+ }
+
+ #
+ # Initialize flags
+ #
+ set old_ref_follow 0
+ set old_ref_delete 0
+
+ switch $TG_op {
+ UPDATE {
+ #
+ # On update we must let old references follow
+ #
+ set NEW(key2) [string toupper $NEW(key2)]
+
+ if {[string compare $NEW(key1) $OLD(key1)] != 0} {
+ set old_ref_follow 1
+ }
+ if {[string compare $NEW(key2) $OLD(key2)] != 0} {
+ set old_ref_follow 1
+ }
+ }
+ DELETE {
+ #
+ # On delete we must delete references too
+ #
+ set old_ref_delete 1
+ }
+ }
+
+ if {$old_ref_follow} {
+ #
+ # Let old references follow and fire NOTICE message if
+ # there where some
+ #
+ set n [spi_execp $GD(plan_dta2_upd) \\
+ [list $OLD(key1) $OLD(key2) $NEW(key1) $NEW(key2)]]
+ if {$n > 0} {
+ elog NOTICE \\
+ "updated $n entries in T_dta2 for new key in T_pkey2"
+ }
+ }
+
+ if {$old_ref_delete} {
+ #
+ # delete references and fire NOTICE message if
+ # there where some
+ #
+ set n [spi_execp $GD(plan_dta2_del) \\
+ [list $OLD(key1) $OLD(key2)]]
+ if {$n > 0} {
+ elog NOTICE \\
+ "deleted $n entries from T_dta2"
+ }
+ }
+
+ return OK
+' language pltcl;
+
+
+create trigger pkey2_after after update or delete on T_pkey2
+ for each row execute procedure
+ trig_pkey2_after();
+
+
+--
+-- Generic trigger function to check references in T_dta1 and T_dta2
+--
+create function check_primkey() returns trigger as E'
+ #
+ # For every trigger/relation pair we create
+ # a saved plan and hold them in GD
+ #
+ set plankey [list "plan" $TG_name $TG_relid]
+ set planrel [list "relname" $TG_relid]
+
+ #
+ # Extract the pkey relation name
+ #
+ set keyidx [expr [llength $args] / 2]
+ set keyrel [string tolower [lindex $args $keyidx]]
+
+ if {![info exists GD($plankey)]} {
+ #
+ # We must prepare a new plan. Build up a query string
+ # for the primary key check.
+ #
+ set keylist [lrange $args [expr $keyidx + 1] end]
+
+ set query "select 1 from $keyrel"
+ set qual " where"
+ set typlist ""
+ set idx 1
+ foreach key $keylist {
+ set key [string tolower $key]
+ #
+ # Add the qual part to the query string
+ #
+ append query "$qual $key = \\$$idx"
+ set qual " and"
+
+ #
+ # Lookup the fields type in pg_attribute
+ #
+ set n [spi_exec "select T.typname \\
+ from pg_catalog.pg_type T, pg_catalog.pg_attribute A, pg_catalog.pg_class C \\
+ where C.relname = ''[quote $keyrel]'' \\
+ and C.oid = A.attrelid \\
+ and A.attname = ''[quote $key]'' \\
+ and A.atttypid = T.oid"]
+ if {$n != 1} {
+ elog ERROR "table $keyrel doesn''t have a field named $key"
+ }
+
+ #
+ # Append the fields type to the argument type list
+ #
+ lappend typlist $typname
+ incr idx
+ }
+
+ #
+ # Prepare the plan
+ #
+ set GD($plankey) [spi_prepare $query $typlist]
+
+ #
+ # Lookup and remember the table name for later error messages
+ #
+ spi_exec "select relname from pg_catalog.pg_class \\
+ where oid = ''$TG_relid''::oid"
+ set GD($planrel) $relname
+ }
+
+ #
+ # Build the argument list from the NEW row
+ #
+ incr keyidx -1
+ set arglist ""
+ foreach arg [lrange $args 0 $keyidx] {
+ lappend arglist $NEW($arg)
+ }
+
+ #
+ # Check for the primary key
+ #
+ set n [spi_execp -count 1 $GD($plankey) $arglist]
+ if {$n <= 0} {
+ elog ERROR "key for $GD($planrel) not in $keyrel"
+ }
+
+ #
+ # Anything is fine
+ #
+ return OK
+' language pltcl;
+
+
+create trigger dta1_before before insert or update on T_dta1
+ for each row execute procedure
+ check_primkey('ref1', 'ref2', 'T_pkey1', 'key1', 'key2');
+
+
+create trigger dta2_before before insert or update on T_dta2
+ for each row execute procedure
+ check_primkey('ref1', 'ref2', 'T_pkey2', 'key1', 'key2');
+
+
+insert into T_pkey1 values (1, 'key1-1', 'test key');
+insert into T_pkey1 values (1, 'key1-2', 'test key');
+insert into T_pkey1 values (1, 'key1-3', 'test key');
+insert into T_pkey1 values (2, 'key2-1', 'test key');
+insert into T_pkey1 values (2, 'key2-2', 'test key');
+insert into T_pkey1 values (2, 'key2-3', 'test key');
+
+insert into T_pkey2 values (1, 'key1-1', 'test key');
+insert into T_pkey2 values (1, 'key1-2', 'test key');
+insert into T_pkey2 values (1, 'key1-3', 'test key');
+insert into T_pkey2 values (2, 'key2-1', 'test key');
+insert into T_pkey2 values (2, 'key2-2', 'test key');
+insert into T_pkey2 values (2, 'key2-3', 'test key');
+
+select * from T_pkey1;
+
+-- key2 in T_pkey2 should have upper case only
+select * from T_pkey2;
+
+insert into T_pkey1 values (1, 'KEY1-3', 'should work');
+
+-- Due to the upper case translation in trigger this must fail
+insert into T_pkey2 values (1, 'KEY1-3', 'should fail');
+
+insert into T_dta1 values ('trec 1', 1, 'key1-1');
+insert into T_dta1 values ('trec 2', 1, 'key1-2');
+insert into T_dta1 values ('trec 3', 1, 'key1-3');
+
+-- Must fail due to unknown key in T_pkey1
+insert into T_dta1 values ('trec 4', 1, 'key1-4');
+
+insert into T_dta2 values ('trec 1', 1, 'KEY1-1');
+insert into T_dta2 values ('trec 2', 1, 'KEY1-2');
+insert into T_dta2 values ('trec 3', 1, 'KEY1-3');
+
+-- Must fail due to unknown key in T_pkey2
+insert into T_dta2 values ('trec 4', 1, 'KEY1-4');
+
+select * from T_dta1;
+
+select * from T_dta2;
+
+update T_pkey1 set key2 = 'key2-9' where key1 = 2 and key2 = 'key2-1';
+update T_pkey1 set key2 = 'key1-9' where key1 = 1 and key2 = 'key1-1';
+delete from T_pkey1 where key1 = 2 and key2 = 'key2-2';
+delete from T_pkey1 where key1 = 1 and key2 = 'key1-2';
+
+update T_pkey2 set key2 = 'KEY2-9' where key1 = 2 and key2 = 'KEY2-1';
+update T_pkey2 set key2 = 'KEY1-9' where key1 = 1 and key2 = 'KEY1-1';
+delete from T_pkey2 where key1 = 2 and key2 = 'KEY2-2';
+delete from T_pkey2 where key1 = 1 and key2 = 'KEY1-2';
+
+select * from T_pkey1;
+select * from T_pkey2;
+select * from T_dta1;
+select * from T_dta2;
+
+select tcl_avg(key1) from T_pkey1;
+select tcl_sum(key1) from T_pkey1;
+select tcl_avg(key1) from T_pkey2;
+select tcl_sum(key1) from T_pkey2;
+
+-- The following should return NULL instead of 0
+select tcl_avg(key1) from T_pkey1 where key1 = 99;
+select tcl_sum(key1) from T_pkey1 where key1 = 99;
+
+select 1 @< 2;
+select 100 @< 4;
+
+select * from T_pkey1 order by key1 using @<, key2 collate "C";
+select * from T_pkey2 order by key1 using @<, key2 collate "C";
+
+-- show dump of trigger data
+insert into trigger_test values(1,'insert');
+
+insert into trigger_test_generated (i) values (1);
+update trigger_test_generated set i = 11 where i = 1;
+delete from trigger_test_generated;
+
+insert into trigger_test_view values(2,'insert');
+update trigger_test_view set v = 'update' where i=1;
+delete from trigger_test_view;
+
+update trigger_test set v = 'update', test_skip=true where i = 1;
+update trigger_test set v = 'update' where i = 1;
+delete from trigger_test;
+truncate trigger_test;
+
+DROP TRIGGER show_trigger_data_trig_before ON trigger_test_generated;
+DROP TRIGGER show_trigger_data_trig_after ON trigger_test_generated;
+
+-- should error
+insert into trigger_test(test_argisnull) values(true);
+
+-- should error
+insert into trigger_test(test_return_null) values(true);
+
+-- test transition table visibility
+create table transition_table_test (id int, name text);
+insert into transition_table_test values (1, 'a');
+create function transition_table_test_f() returns trigger language pltcl as
+$$
+ spi_exec -array C "SELECT id, name FROM old_table" {
+ elog INFO "old: $C(id) -> $C(name)"
+ }
+ spi_exec -array C "SELECT id, name FROM new_table" {
+ elog INFO "new: $C(id) -> $C(name)"
+ }
+ return OK
+$$;
+CREATE TRIGGER a_t AFTER UPDATE ON transition_table_test
+ REFERENCING OLD TABLE AS old_table NEW TABLE AS new_table
+ FOR EACH STATEMENT EXECUTE PROCEDURE transition_table_test_f();
+update transition_table_test set name = 'b';
+drop table transition_table_test;
+drop function transition_table_test_f();
+
+-- dealing with generated columns
+
+CREATE FUNCTION generated_test_func1() RETURNS trigger
+LANGUAGE pltcl
+AS $$
+# not allowed
+set NEW(j) 5
+return [array get NEW]
+$$;
+
+CREATE TRIGGER generated_test_trigger1 BEFORE INSERT ON trigger_test_generated
+FOR EACH ROW EXECUTE PROCEDURE generated_test_func1();
+
+TRUNCATE trigger_test_generated;
+INSERT INTO trigger_test_generated (i) VALUES (1);
+SELECT * FROM trigger_test_generated;
diff --git a/src/pl/tcl/sql/pltcl_unicode.sql b/src/pl/tcl/sql/pltcl_unicode.sql
new file mode 100644
index 0000000..f000604
--- /dev/null
+++ b/src/pl/tcl/sql/pltcl_unicode.sql
@@ -0,0 +1,38 @@
+--
+-- Unicode handling
+--
+-- Note: this test case is known to fail if the database encoding is
+-- EUC_CN, EUC_JP, EUC_KR, or EUC_TW, for lack of any equivalent to
+-- U+00A0 (no-break space) in those encodings. However, testing with
+-- plain ASCII data would be rather useless, so we must live with that.
+--
+
+SET client_encoding TO UTF8;
+
+CREATE TABLE unicode_test (
+ testvalue text NOT NULL
+);
+
+CREATE FUNCTION unicode_return() RETURNS text AS $$
+ return "\xA0"
+$$ LANGUAGE pltcl;
+
+CREATE FUNCTION unicode_trigger() RETURNS trigger AS $$
+ set NEW(testvalue) "\xA0"
+ return [array get NEW]
+$$ LANGUAGE pltcl;
+
+CREATE TRIGGER unicode_test_bi BEFORE INSERT ON unicode_test
+ FOR EACH ROW EXECUTE PROCEDURE unicode_trigger();
+
+CREATE FUNCTION unicode_plan1() RETURNS text AS $$
+ set plan [ spi_prepare {SELECT $1 AS testvalue} [ list "text" ] ]
+ spi_execp $plan [ list "\xA0" ]
+ return $testvalue
+$$ LANGUAGE pltcl;
+
+
+SELECT unicode_return();
+INSERT INTO unicode_test (testvalue) VALUES ('test');
+SELECT * FROM unicode_test;
+SELECT unicode_plan1();