// Copyright 2024 Dolthub, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package _go import ( "testing" "github.com/dolthub/go-mysql-server/sql" ) func TestCreateFunctionLanguagePlpgsql(t *testing.T) { RunScripts(t, []ScriptTest{ { Name: "ALIAS", // TODO: Implement OpCode conversion for parsed ALIAS statements. Skip: true, SetUpScript: []string{ `CREATE FUNCTION interpreted_alias(input TEXT) RETURNS TEXT AS $$ DECLARE var1 TEXT; var2 TEXT; BEGIN DECLARE alias1 ALIAS FOR var1; alias2 ALIAS FOR alias1; alias3 ALIAS FOR input; BEGIN alias2 := alias3; END; RETURN var1; END; $$ LANGUAGE plpgsql; `, }, Assertions: []ScriptTestAssertion{ { Query: "SELECT interpreted_alias('123');", Expected: []sql.Row{{"123"}}, }, }, }, { Name: "Assignment", SetUpScript: []string{`CREATE FUNCTION interpreted_assignment(input TEXT) RETURNS TEXT AS $$ DECLARE var1 TEXT; BEGIN var1 := 'Initial: ' || input; IF input = 'Hello' THEN var1 := var1 || ' - Greeting'; ELSIF input = 'Bye' THEN var1 := var1 || ' - Farewell'; ELSIF length(input) > 5 THEN var1 := var1 || ' - Over 5'; ELSE var1 := var1 || ' - Else'; END IF; RETURN var1; END; $$ LANGUAGE plpgsql;`}, Assertions: []ScriptTestAssertion{ { Query: "SELECT interpreted_assignment('Hello');", Expected: []sql.Row{ {"Initial: Hello - Greeting"}, }, }, { Query: "SELECT interpreted_assignment('Bye');", Expected: []sql.Row{ {"Initial: Bye - Farewell"}, }, }, { Query: "SELECT interpreted_assignment('abc');", Expected: []sql.Row{ {"Initial: abc - Else"}, }, }, { Query: "SELECT interpreted_assignment('something');", Expected: []sql.Row{ {"Initial: something - Over 5"}, }, }, }, }, { Name: "CASE, with ELSE", SetUpScript: []string{` CREATE FUNCTION interpreted_case(x INT) RETURNS TEXT AS $$ DECLARE msg TEXT; BEGIN CASE x WHEN 1, 2 THEN msg := 'one'; msg := msg || ' or two'; ELSE msg := 'other'; msg := msg || ' value than one or two'; END CASE; RETURN msg; END; $$ LANGUAGE plpgsql;`}, Assertions: []ScriptTestAssertion{ { Query: "SELECT interpreted_case(1);", Expected: []sql.Row{{"one or two"}}, }, { Query: "SELECT interpreted_case(2);", Expected: []sql.Row{{"one or two"}}, }, { Query: "SELECT interpreted_case(0);", Expected: []sql.Row{{"other value than one or two"}}, }, }, }, { // TODO: When no CASE statements match, and there is no ELSE block, // Postgres raises an exception. Unskip this test after we // add support for raising exceptions from functions. Skip: true, Name: "CASE, without ELSE", SetUpScript: []string{` CREATE FUNCTION interpreted_case(x INT) RETURNS TEXT AS $$ DECLARE msg TEXT; BEGIN CASE x WHEN 1, 2 THEN msg := 'one or two'; END CASE; RETURN msg; END; $$ LANGUAGE plpgsql;`}, Assertions: []ScriptTestAssertion{ { Query: "SELECT interpreted_case(1);", Expected: []sql.Row{{"one or two"}}, }, { Query: "SELECT interpreted_case(2);", Expected: []sql.Row{{"one or two"}}, }, { Query: "SELECT interpreted_case(0);", ExpectedErr: "case not found", }, }, }, { Name: "Searched CASE, with ELSE", SetUpScript: []string{` CREATE FUNCTION interpreted_case(x INT) RETURNS TEXT AS $$ DECLARE msg TEXT; BEGIN CASE WHEN x BETWEEN 0 AND 10 THEN msg := 'value is between zero'; msg := msg || ' and ten'; WHEN x BETWEEN 11 AND 20 THEN msg := 'value is between eleven and twenty'; ELSE msg := 'value'; msg := msg || ' is'; msg := msg || ' out of'; msg := msg || ' bounds'; END CASE; RETURN msg; END; $$ LANGUAGE plpgsql;`}, Assertions: []ScriptTestAssertion{ { Query: "SELECT interpreted_case(0);", Expected: []sql.Row{{"value is between zero and ten"}}, }, { Query: "SELECT interpreted_case(1);", Expected: []sql.Row{{"value is between zero and ten"}}, }, { Query: "SELECT interpreted_case(10);", Expected: []sql.Row{{"value is between zero and ten"}}, }, { Query: "SELECT interpreted_case(11);", Expected: []sql.Row{{"value is between eleven and twenty"}}, }, { Query: "SELECT interpreted_case(21);", Expected: []sql.Row{{"value is out of bounds"}}, }, }, }, { // TODO: When no CASE statements match, and there is no ELSE block, // Postgres raises an exception. Unskip this test after we // add support for raising exceptions from functions. Skip: true, Name: "Searched CASE, without ELSE", SetUpScript: []string{` CREATE FUNCTION interpreted_case(x INT) RETURNS TEXT AS $$ DECLARE msg TEXT; BEGIN CASE WHEN x BETWEEN 0 AND 10 THEN msg := 'value is between zero and ten'; WHEN x BETWEEN 11 AND 20 THEN msg := 'value'; msg := msg || ' is between'; msg := msg || ' eleven and'; msg := msg || ' twenty'; END CASE; RETURN msg; END; $$ LANGUAGE plpgsql;`}, Assertions: []ScriptTestAssertion{ { Query: "SELECT interpreted_case(0);", Expected: []sql.Row{{"value is between zero and ten"}}, }, { Query: "SELECT interpreted_case(1);", Expected: []sql.Row{{"value is between zero and ten"}}, }, { Query: "SELECT interpreted_case(10);", Expected: []sql.Row{{"value is between zero and ten"}}, }, { Query: "SELECT interpreted_case(11);", Expected: []sql.Row{{"value is between eleven and twenty"}}, }, { Query: "SELECT interpreted_case(21);", ExpectedErr: "case not found", }, }, }, { Name: "CONTINUE", SetUpScript: []string{`CREATE FUNCTION interpreted_continue() RETURNS INT4 AS $$ DECLARE var1 INT4; BEGIN LOOP var1 := var1 + 1; IF var1 < 4 THEN CONTINUE; END IF; RETURN var1; END LOOP; END; $$ LANGUAGE plpgsql;`}, Assertions: []ScriptTestAssertion{ { Query: "SELECT interpreted_continue();", Expected: []sql.Row{{4}}, }, }, }, { Name: "CONTINUE Label", SetUpScript: []string{`CREATE FUNCTION interpreted_continue_label() RETURNS INT4 AS $$ DECLARE var1 INT4; BEGIN <> LOOP var1 := var1 + 1; IF var1 < 6 THEN CONTINUE cont_label; END IF; RETURN var1; END LOOP; END; $$ LANGUAGE plpgsql;`}, Assertions: []ScriptTestAssertion{ { Query: "SELECT interpreted_continue_label();", Expected: []sql.Row{{6}}, }, }, }, { Name: "EXIT", SetUpScript: []string{`CREATE FUNCTION interpreted_exit() RETURNS INT4 AS $$ DECLARE var1 INT4; BEGIN LOOP var1 := var1 + 1; IF var1 >= 8 THEN EXIT; END IF; END LOOP; RETURN var1; END; $$ LANGUAGE plpgsql;`}, Assertions: []ScriptTestAssertion{ { Query: "SELECT interpreted_exit();", Expected: []sql.Row{{8}}, }, }, }, { Name: "EXIT WHEN", SetUpScript: []string{`CREATE FUNCTION interpreted_exit_when() RETURNS INT4 AS $$ DECLARE var1 INT4; BEGIN LOOP var1 := var1 + 1; EXIT WHEN var1 >= 9; END LOOP; RETURN var1; END; $$ LANGUAGE plpgsql;`}, Assertions: []ScriptTestAssertion{ { Query: "SELECT interpreted_exit_when();", Expected: []sql.Row{{9}}, }, }, }, { Name: "LOOP", SetUpScript: []string{`CREATE FUNCTION interpreted_loop() RETURNS INT4 AS $$ DECLARE var1 INT4; BEGIN LOOP var1 := var1 + 1; IF var1 >= 10 THEN RETURN var1; END IF; END LOOP; END; $$ LANGUAGE plpgsql;`}, Assertions: []ScriptTestAssertion{ { Query: "SELECT interpreted_loop();", Expected: []sql.Row{{10}}, }, }, }, { Name: "LOOP Label", SetUpScript: []string{`CREATE FUNCTION interpreted_loop_label() RETURNS INT4 AS $$ DECLARE var1 INT4; BEGIN <> LOOP var1 := var1 + 1; IF var1 >= 12 THEN EXIT loop_label; END IF; END LOOP; RETURN var1; END; $$ LANGUAGE plpgsql;`}, Assertions: []ScriptTestAssertion{ { Query: "SELECT interpreted_loop_label();", Expected: []sql.Row{{12}}, }, }, }, { Name: "PERFORM", SetUpScript: []string{ `CREATE SEQUENCE test_sequence;`, `CREATE FUNCTION interpreted_perform() RETURNS VOID AS $$ BEGIN PERFORM nextval('test_sequence'); END; $$ LANGUAGE plpgsql;`}, Assertions: []ScriptTestAssertion{ { Query: "SELECT nextval('test_sequence');", Expected: []sql.Row{{1}}, }, { Query: "SELECT interpreted_perform();", Expected: []sql.Row{{nil}}, // TODO: Postgres returns a value that's not null, but also not a value? }, { Query: "SELECT nextval('test_sequence');", Expected: []sql.Row{{3}}, }, }, }, { Name: "RETURNS SETOF", SetUpScript: []string{ `CREATE TYPE user_summary AS ( user_id integer, username text, is_active boolean);`, `CREATE OR REPLACE FUNCTION func2() RETURNS SETOF user_summary LANGUAGE plpgsql AS $$ BEGIN RETURN QUERY SELECT 1, 'username', true; RETURN QUERY SELECT 2, 'another', false; END; $$;`, }, Assertions: []ScriptTestAssertion{ { Query: "SELECT func2();", Expected: []sql.Row{ {"(1,username,t)"}, {"(2,another,f)"}, }, }, { Query: "SELECT func2(), func2();", Expected: []sql.Row{ {"(1,username,t)", "(1,username,t)"}, {"(2,another,f)", "(2,another,f)"}, }, }, }, }, { Name: "RETURNS SETOF with no results", SetUpScript: []string{ `CREATE TABLE user_summary (user_id integer, username text, is_active boolean);`, `CREATE OR REPLACE FUNCTION func2() RETURNS SETOF user_summary LANGUAGE plpgsql AS $$ BEGIN RETURN QUERY SELECT * from user_summary; RETURN QUERY SELECT * from user_summary; END; $$;`, }, Assertions: []ScriptTestAssertion{ { Query: "SELECT func2();", Expected: []sql.Row{}, }, { Query: "SELECT func2(), func2();", Expected: []sql.Row{}, }, }, }, { Name: "RETURNS SETOF with type from other schema", SetUpScript: []string{ `CREATE SCHEMA sch1;`, `CREATE TYPE sch1.user_summary AS ( user_id integer, username text, is_active boolean);`, `CREATE OR REPLACE FUNCTION func2() RETURNS SETOF sch1.user_summary LANGUAGE plpgsql AS $$ BEGIN RETURN QUERY SELECT 1, 'username', true; RETURN QUERY SELECT 2, 'another', false; END; $$;`, }, Assertions: []ScriptTestAssertion{ { Query: "SELECT func2();", Expected: []sql.Row{ {"(1,username,t)"}, {"(2,another,f)"}, }, }, { Query: "SELECT func2(), func2();", Expected: []sql.Row{ {"(1,username,t)", "(1,username,t)"}, {"(2,another,f)", "(2,another,f)"}, }, }, }, }, { Name: "RETURNS SETOF with param", SetUpScript: []string{ `CREATE TYPE user_summary AS ( user_id integer, username text, is_active boolean);`, `CREATE OR REPLACE FUNCTION func3(user_id integer) RETURNS SETOF user_summary LANGUAGE plpgsql AS $$ BEGIN RETURN QUERY SELECT user_id, 'username', true; RETURN QUERY SELECT user_id, 'another', false; END; $$;`, }, Assertions: []ScriptTestAssertion{ { Query: "SELECT func3(111);", Expected: []sql.Row{ {"(111,username,t)"}, {"(111,another,f)"}, }, }, { Query: "SELECT func3(111), func3(222);", Expected: []sql.Row{ {"(111,username,t)", "(222,username,t)"}, {"(111,another,f)", "(222,another,f)"}, }, }, }, }, { Name: "RETURNS TABLE", SetUpScript: []string{ `CREATE FUNCTION func2() RETURNS TABLE(user_id integer, username text, is_active boolean) LANGUAGE plpgsql AS $$ BEGIN RETURN QUERY SELECT 1, 'username', true; RETURN QUERY SELECT 2, 'another', false; END; $$;`, }, Assertions: []ScriptTestAssertion{ { Query: "SELECT func2();", Expected: []sql.Row{ {"(1,username,t)"}, {"(2,another,f)"}, }, }, { Query: "SELECT func2(), func2();", Expected: []sql.Row{ {"(1,username,t)", "(1,username,t)"}, {"(2,another,f)", "(2,another,f)"}, }, }, }, }, { Name: "RETURNS TABLE with single field", SetUpScript: []string{ `CREATE FUNCTION func2() RETURNS TABLE(username text) LANGUAGE plpgsql AS $$ BEGIN RETURN QUERY SELECT 'username1'; RETURN QUERY SELECT 'username2'; END; $$;`, }, Assertions: []ScriptTestAssertion{ { Query: "SELECT func2();", Expected: []sql.Row{ {"(username1)"}, {"(username2)"}, }, }, { Query: "SELECT func2(), func2();", Expected: []sql.Row{ {"(username1)", "(username1)"}, {"(username2)", "(username2)"}, }, }, }, }, { Name: "RETURNS TABLE with types from other schema", SetUpScript: []string{ `CREATE SCHEMA sch1;`, `CREATE TYPE sch1.mytype AS ( user_id integer, username text);`, `CREATE FUNCTION func2() RETURNS TABLE(foo sch1.mytype) LANGUAGE plpgsql AS $$ BEGIN RETURN QUERY SELECT 1, 'username1'; RETURN QUERY SELECT 2, 'username2'; END; $$;`, }, Assertions: []ScriptTestAssertion{ { Query: "SELECT func2();", Expected: []sql.Row{ {"(1,username1)"}, {"(2,username2)"}, }, }, { Query: "SELECT func2(), func2();", Expected: []sql.Row{ {"(1,username1)", "(1,username1)"}, {"(2,username2)", "(2,username2)"}, }, }, }, }, { Name: "RETURNS TABLE with param", SetUpScript: []string{ `CREATE OR REPLACE FUNCTION func3(user_id integer) RETURNS TABLE(user_id integer, username text, is_active boolean) LANGUAGE plpgsql AS $$ BEGIN RETURN QUERY SELECT user_id, 'username', true; RETURN QUERY SELECT user_id, 'another', false; END; $$;`, }, Assertions: []ScriptTestAssertion{ { Query: "SELECT func3(111);", Expected: []sql.Row{ {"(111,username,t)"}, {"(111,another,f)"}, }, }, { Query: "SELECT func3(111), func3(222);", Expected: []sql.Row{ {"(111,username,t)", "(222,username,t)"}, {"(111,another,f)", "(222,another,f)"}, }, }, }, }, { Name: "RETURNS TABLE with join query", SetUpScript: []string{ `CREATE TABLE customers ( id INT PRIMARY KEY, name TEXT );`, `CREATE TABLE orders ( id SERIAL PRIMARY KEY, customer_id INT, amount INT );`, `INSERT INTO customers VALUES (1, 'John'), (2, 'Jane');`, `INSERT INTO orders VALUES (1, 1, 100), (2, 2, 10);`, `CREATE OR REPLACE FUNCTION func2(n INT) RETURNS TABLE (c_id INT, c_name TEXT, c_total_spent INT) LANGUAGE plpgsql AS $$ BEGIN RETURN QUERY SELECT c.id, c.name, SUM(o.amount) AS total_spent FROM customers c JOIN orders o ON o.customer_id = c.id GROUP BY c.id, c.name HAVING SUM(o.amount) >= n ; END; $$;`, }, Assertions: []ScriptTestAssertion{ { Query: "SELECT func2(1);", Expected: []sql.Row{ {"(1,John,100)"}, {"(2,Jane,10)"}, }, }, { Query: "SELECT func2(11);", Expected: []sql.Row{ {"(1,John,100)"}, }, }, { Query: "SELECT func2(111);", Expected: []sql.Row{}, }, }, }, { Name: "RETURNS TABLE with conflicting variable names", SetUpScript: []string{ `CREATE TABLE customers2 ( id INT PRIMARY KEY, name TEXT );`, `INSERT INTO customers2 VALUES (1, 'John'), (2, 'Jane');`, `CREATE OR REPLACE FUNCTION func_conflict(n INT) RETURNS TABLE (id INT, name TEXT) LANGUAGE plpgsql AS $$ BEGIN RETURN QUERY SELECT c.id, c.name FROM customers2 c WHERE c.id = n; END; $$;`, }, Assertions: []ScriptTestAssertion{ { Query: "SELECT func_conflict(1);", Expected: []sql.Row{ {"(1,John)"}, }, }, { Query: "SELECT func_conflict(2);", Expected: []sql.Row{ {"(2,Jane)"}, }, }, }, }, { Name: "RETURNS SETOF with composite param", SetUpScript: []string{ `CREATE TYPE user_summary AS ( user_id integer, username text, is_active boolean);`, `CREATE OR REPLACE FUNCTION func3(u user_summary) RETURNS SETOF user_summary LANGUAGE plpgsql AS $$ BEGIN RETURN QUERY SELECT u.user_id, u.username, u.is_active; END; $$;`, }, Assertions: []ScriptTestAssertion{ { Query: "SELECT func3((222,'passedin',false)::user_summary);", Expected: []sql.Row{ {"(222,passedin,f)"}, }, }, }, }, { Name: "RAISE", SetUpScript: []string{ `CREATE FUNCTION interpreted_raise1(input TEXT) RETURNS TEXT AS $$ DECLARE var1 TEXT; BEGIN RAISE WARNING 'MyMessage'; RAISE NOTICE USING MESSAGE = 'MyNoticeMessage'; RAISE DEBUG 'DebugTest1' USING MESSAGE = 'DebugMessage'; var1 := input; RETURN var1; END; $$ LANGUAGE plpgsql;`, // TODO: this is incorrect, cannot raise both NOTICE USING MESSAGE and DEBUG USING MESSAGE, need to fix `CREATE FUNCTION interpreted_raise2(input TEXT) RETURNS TEXT AS $$ DECLARE var1 TEXT; BEGIN RAISE EXCEPTION '% %% bar %', 'foo', 1+1; var1 := input; RETURN var1; END; $$ LANGUAGE plpgsql;`, }, Assertions: []ScriptTestAssertion{ { Query: "SELECT interpreted_raise1('123');", Expected: []sql.Row{{"123"}}, ExpectedNotices: []ExpectedNotice{ { Severity: "WARNING", Message: "MyMessage", }, { Severity: "NOTICE", Message: "'MyNoticeMessage'", }, { Severity: "DEBUG", Message: "'DebugMessage'", }, }, }, { Query: "SELECT interpreted_raise2('123');", ExpectedErr: "foo % bar 2", }, }, }, { Name: "SELECT INTO", SetUpScript: []string{`CREATE FUNCTION interpreted_select_into(input INT4) RETURNS TEXT AS $$ DECLARE ret TEXT; count INT4; BEGIN DROP TABLE IF EXISTS temp_table; CREATE TABLE temp_table (pk SERIAL PRIMARY KEY, v1 TEXT NOT NULL); INSERT INTO temp_table (v1) VALUES ('abc'), ('def'), ('ghi'); SELECT COUNT(*) INTO count FROM temp_table; IF input > 0 AND input <= count THEN SELECT v1 INTO ret FROM temp_table WHERE pk = input; ELSE ret := 'out of bounds'; END IF; RETURN ret; END; $$ LANGUAGE plpgsql;`}, Assertions: []ScriptTestAssertion{ { Query: "SELECT interpreted_select_into(1);", Expected: []sql.Row{ {"abc"}, }, }, { Query: "SELECT interpreted_select_into(2);", Expected: []sql.Row{ {"def"}, }, }, { Query: "SELECT interpreted_select_into(3);", Expected: []sql.Row{ {"ghi"}, }, }, { Query: "SELECT interpreted_select_into(4);", Expected: []sql.Row{ {"out of bounds"}, }, }, }, }, { Name: "WHILE", SetUpScript: []string{ `CREATE FUNCTION interpreted_while(input INT4) RETURNS INT AS $$ DECLARE counter INT4; BEGIN WHILE counter + input < 100 LOOP -- Include more than one statement in the loop so it's not too simple counter = counter + 1; counter = counter - 1; counter = counter + 1; END LOOP; RETURN counter; END; $$ LANGUAGE plpgsql;`}, Assertions: []ScriptTestAssertion{ { Query: "SELECT interpreted_while(42);", Expected: []sql.Row{{58}}, }, }, }, { Name: "WHILE Label", SetUpScript: []string{ `CREATE FUNCTION interpreted_while_label(input INT4) RETURNS INT AS $$ DECLARE counter INT4; BEGIN <> WHILE input < 1000 LOOP input := input + 1; counter := counter + 1; IF counter >= 10 THEN EXIT while_label; END IF; END LOOP; RETURN input; END; $$ LANGUAGE plpgsql;`}, Assertions: []ScriptTestAssertion{ { Query: "SELECT interpreted_while_label(42);", Expected: []sql.Row{{52}}, }, }, }, { Name: "NULL", SetUpScript: []string{ `CREATE FUNCTION interpreted_null(input INT) RETURNS TEXT AS $$ BEGIN IF input = 42 THEN NULL; NULL; ELSE RETURN 'No'; END IF; NULL; RETURN 'Yes'; END; $$ LANGUAGE plpgsql;`}, Assertions: []ScriptTestAssertion{ { Query: "SELECT interpreted_null(42);", Expected: []sql.Row{{"Yes"}}, }, { Query: "SELECT interpreted_null(43);", Expected: []sql.Row{{"No"}}, }, }, }, { // Tests that variable names are correctly substituted with references // to the variables when the function is parsed. Name: "Variable reference substitution", SetUpScript: []string{` CREATE FUNCTION test1(input TEXT) RETURNS TEXT AS $$ DECLARE var1 TEXT; BEGIN var1 := 'input' || input; IF var1 = 'input' || input THEN RETURN var1 || 'var1'; ELSE RETURN '!!!'; END IF; END; $$ LANGUAGE plpgsql;`, }, Assertions: []ScriptTestAssertion{ { Query: "SELECT test1('Hello');", Expected: []sql.Row{{"inputHellovar1"}}, }, }, }, { Name: "Overloading", SetUpScript: []string{`CREATE FUNCTION interpreted_overload(input TEXT) RETURNS TEXT AS $$ DECLARE var1 TEXT; BEGIN IF length(input) > 3 THEN var1 := input || '_long'; ELSE var1 := input; END IF; RETURN var1; END; $$ LANGUAGE plpgsql;`, `CREATE FUNCTION interpreted_overload(input INT4) RETURNS INT4 AS $$ DECLARE var1 INT4; BEGIN IF input > 3 THEN var1 := -input; ELSE var1 := input; END IF; RETURN var1; END; $$ LANGUAGE plpgsql;`}, Assertions: []ScriptTestAssertion{ { Query: "SELECT interpreted_overload('abc');", Expected: []sql.Row{ {"abc"}, }, }, { Query: "SELECT interpreted_overload('abcd');", Expected: []sql.Row{ {"abcd_long"}, }, }, { Query: "SELECT interpreted_overload(3);", Expected: []sql.Row{ {3}, }, }, { Query: "SELECT interpreted_overload(4);", Expected: []sql.Row{ {-4}, }, }, }, }, { Name: "Branching", Assertions: []ScriptTestAssertion{ { Query: `CREATE FUNCTION interpreted_as_of(input TEXT) RETURNS TEXT AS $$ BEGIN RETURN input || '_extra'; END; $$ LANGUAGE plpgsql;`, Expected: []sql.Row{}, }, { Query: "SELECT interpreted_as_of('abcd');", Expected: []sql.Row{{"abcd_extra"}}, }, { Query: `SELECT dolt_add('.');`, Expected: []sql.Row{{"{0}"}}, }, { Query: "SELECT length(dolt_commit('-m', 'initial')::text) = 34;", Expected: []sql.Row{{"t"}}, }, { Query: `SELECT dolt_checkout('-b', 'other')`, Expected: []sql.Row{{`{0,"Switched to branch 'other'"}`}}, }, { Query: `CREATE OR REPLACE FUNCTION interpreted_as_of(input TEXT) RETURNS TEXT AS $$ BEGIN RETURN input; END; $$ LANGUAGE plpgsql;`, Expected: []sql.Row{}, }, { Query: `SELECT dolt_add('.');`, Expected: []sql.Row{{"{0}"}}, }, { Query: "SELECT length(dolt_commit('-m', 'updated func')::text) = 34;", Expected: []sql.Row{{"t"}}, }, { Query: "SELECT interpreted_as_of('abc');", Expected: []sql.Row{{"abc"}}, }, { Query: "SELECT dolt_checkout('main')", Expected: []sql.Row{{`{0,"Switched to branch 'main'"}`}}, }, { Query: "SELECT interpreted_as_of('abcd');", Expected: []sql.Row{{"abcd_extra"}}, }, }, }, { Name: "Merging No Conflict", SetUpScript: []string{ `CREATE TABLE test(pk INT4);`, `INSERT INTO test VALUES (77);`, }, Assertions: []ScriptTestAssertion{ { Query: `CREATE FUNCTION interpreted_merging(input TEXT) RETURNS TEXT AS $$ BEGIN RETURN input || '_extra'; END; $$ LANGUAGE plpgsql;`, Expected: []sql.Row{}, }, { Query: "SELECT interpreted_merging('abcd');", Expected: []sql.Row{{"abcd_extra"}}, }, { Query: "SELECT interpreted_merging(55);", ExpectedErr: "does not exist", }, { Query: `SELECT dolt_add('.');`, Expected: []sql.Row{{"{0}"}}, }, { Query: "SELECT length(dolt_commit('-m', 'initial')::text) = 34;", Expected: []sql.Row{{"t"}}, }, { Query: `SELECT dolt_checkout('-b', 'other')`, Expected: []sql.Row{{`{0,"Switched to branch 'other'"}`}}, }, { Query: `CREATE FUNCTION interpreted_merging(input INT4) RETURNS INT4 AS $$ BEGIN RETURN input + 11; END; $$ LANGUAGE plpgsql;`, Expected: []sql.Row{}, }, { Query: `SELECT dolt_add('.');`, Expected: []sql.Row{{"{0}"}}, }, { Query: "SELECT length(dolt_commit('-m', 'another func')::text) = 34;", Expected: []sql.Row{{"t"}}, }, { Query: "SELECT interpreted_merging(55);", Expected: []sql.Row{{66}}, }, { Query: "SELECT dolt_checkout('main')", Expected: []sql.Row{{`{0,"Switched to branch 'main'"}`}}, }, { Query: "INSERT INTO test VALUES (80);", Expected: []sql.Row{}, }, { Query: `SELECT dolt_add('.');`, Expected: []sql.Row{{"{0}"}}, }, { Query: "SELECT length(dolt_commit('-m', 'updated table')::text) = 34;", Expected: []sql.Row{{"t"}}, }, { Query: "SELECT interpreted_merging('abcde');", Expected: []sql.Row{{"abcde_extra"}}, }, { Query: "SELECT interpreted_merging(67);", ExpectedErr: "does not exist", }, { Query: "SELECT * FROM test;", Expected: []sql.Row{{77}, {80}}, }, { Query: "SELECT length(dolt_merge('other')::text) = 57;", Expected: []sql.Row{{"t"}}, }, { Query: "SELECT interpreted_merging('abcdef');", Expected: []sql.Row{{"abcdef_extra"}}, }, { Query: "SELECT interpreted_merging(58);", Expected: []sql.Row{{69}}, }, { Query: "SELECT * FROM test;", Expected: []sql.Row{{77}, {80}}, }, }, }, { Name: "INSERT values from function", SetUpScript: []string{ "CREATE TABLE test (v1 TEXT);", `CREATE FUNCTION insertion_text() RETURNS TEXT AS $$ DECLARE var1 TEXT; BEGIN var1 := 'example'; RETURN var1; END; $$ LANGUAGE plpgsql;`, }, Assertions: []ScriptTestAssertion{ { Query: "INSERT INTO test VALUES (insertion_text()), (insertion_text());", Expected: []sql.Row{}, }, { Query: "SELECT * FROM test;", Expected: []sql.Row{ {"example"}, {"example"}, }, }, }, }, { Name: "Create function on different branch", Skip: true, // several issues prevent this from working yet SetUpScript: []string{ `CREATE FUNCTION f1(input TEXT) RETURNS TEXT AS $$ BEGIN RETURN input || '_extra'; END; $$ LANGUAGE plpgsql;`, `call dolt_branch('b1');`, `CREATE FUNCTION "postgres/b1".public.f1(input INT4) RETURNS INT4 AS $$ BEGIN RETURN input + 11; END; $$ LANGUAGE plpgsql;`, }, Assertions: []ScriptTestAssertion{ { Query: `SELECT f1('abcd');`, Expected: []sql.Row{{"abcd_extra"}}, }, { Query: `SELECT "postgres/b1".public.f1(55);`, Expected: []sql.Row{{66}}, }, { Query: `call dolt_checkout('b1');`, }, { Query: `SELECT f1(55);`, Expected: []sql.Row{{66}}, }, }, }, { Name: "Nested IF statements with exceptions", SetUpScript: []string{ `CREATE TABLE public.table_name (start_date DATE NOT NULL, end_date DATE);`, }, Assertions: []ScriptTestAssertion{ { Query: `CREATE FUNCTION public.fn_name() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN IF NEW.start_date IS NOT NULL AND NEW.end_date IS NULL THEN NEW.end_date := NEW.start_date + INTERVAL '31 day'; END IF; IF NEW.start_date IS NOT NULL AND NEW.end_date IS NOT NULL THEN IF NEW.end_date < NEW.start_date THEN RAISE EXCEPTION 'end_date (%) start_date (%)', NEW.end_date, NEW.start_date; END IF; IF NEW.end_date > (NEW.start_date + INTERVAL '31 day') THEN RAISE EXCEPTION 'Too far (start_date=%, end_date=%)', NEW.start_date, NEW.end_date; END IF; END IF; RETURN NEW; END; $$;`, Expected: []sql.Row{}, }, { Query: `CREATE TRIGGER trig_name BEFORE INSERT OR UPDATE ON public.table_name FOR EACH ROW EXECUTE FUNCTION public.fn_name();`, Expected: []sql.Row{}, }, { Query: "INSERT INTO public.table_name VALUES ('2025-01-02', '2025-02-02');", Expected: []sql.Row{}, }, { Query: "INSERT INTO public.table_name VALUES ('2025-04-05', NULL);", Expected: []sql.Row{}, }, { Query: "INSERT INTO public.table_name VALUES ('2025-09-10', '2025-07-08');", ExpectedErr: "end_date (2025-07-08) start_date (2025-09-10)", }, { Query: "INSERT INTO public.table_name VALUES ('2025-11-11', '2025-12-31');", ExpectedErr: "Too far (start_date=2025-11-11, end_date=2025-12-31)", }, }, }, { Name: "Table as type for functions", SetUpScript: []string{ // TODO: test case sensitivity of parameter names `CREATE TABLE test (id INT4 PRIMARY KEY, name TEXT NOT NULL, qty INT4 NOT NULL, price REAL NOT NULL);`, `INSERT INTO test VALUES (1, 'apple', 3, 2.5), (2, 'banana', 5, 1.2);`, `CREATE FUNCTION total(t test) RETURNS REAL AS $$ BEGIN RETURN t.qty * t.price; END; $$ LANGUAGE plpgsql;`, `CREATE FUNCTION priceHike(t test, pricehike REAL) RETURNS test AS $$ BEGIN RETURN (t.id, t.name, t.qty, t.price + pricehike)::test; END; $$ LANGUAGE plpgsql;`, `CREATE FUNCTION singleReturn() RETURNS test AS $$ DECLARE result test; BEGIN SELECT * INTO result FROM test WHERE id = 1; RETURN result; END; $$ LANGUAGE plpgsql;`, }, Assertions: []ScriptTestAssertion{ { Query: `SELECT total(t) FROM test AS t;`, Expected: []sql.Row{ {7.5}, {6.0}, }, }, { Query: `SELECT priceHike(t, 10.0) FROM test AS t;`, Expected: []sql.Row{ {"(1,apple,3,12.5)"}, {"(2,banana,5,11.2)"}, }, }, { Query: `SELECT priceHike(ROW(3, 'orange', 1, 1.8)::test, 100.0);`, Expected: []sql.Row{ {"(3,orange,1,101.8)"}, }, }, { Query: `SELECT singleReturn();`, Skip: true, // TODO: better PL/pgSQL internal support for non-trigger composite types Expected: []sql.Row{ {"(1,apple,3,2.5)"}, }, }, }, }, { Name: "Table as type for columns", SetUpScript: []string{ `CREATE TABLE t1 (v1 INT4 PRIMARY KEY, v2 TEXT NOT NULL);`, }, Assertions: []ScriptTestAssertion{ { Query: `CREATE TABLE t2 (v1 INT4 PRIMARY KEY, v2 t1 NOT NULL);`, Expected: []sql.Row{}, }, { Query: `INSERT INTO t2 VALUES (1, ROW(0, 'hello')::t1), (2, ROW(10, 'world')::t1);`, Expected: []sql.Row{}, }, { Query: `SELECT * FROM t2 ORDER BY v1;`, Expected: []sql.Row{ {1, "(0,hello)"}, {2, "(10,world)"}, }, }, }, }, { Name: "AlexTransit_venderctl import dump", SetUpScript: []string{ `CREATE TYPE public.tax_job_state AS ENUM ( 'sched', 'busy', 'final', 'help' );`, `CREATE TABLE public.catalog ( vmid integer NOT NULL, code text NOT NULL, name text NOT NULL );`, `CREATE SEQUENCE public.tax_job_id_seq START WITH 1 INCREMENT BY 1 NO MINVALUE NO MAXVALUE CACHE 1;`, `CREATE TABLE public.tax_job ( id bigint NOT NULL, state public.tax_job_state NOT NULL, created timestamp with time zone NOT NULL, modified timestamp with time zone NOT NULL, scheduled timestamp with time zone, worker text, processor text, ext_id text, data jsonb, gross integer, notes text[], ops jsonb );`, `CREATE TABLE public.trans ( vmid integer NOT NULL, vmtime timestamp with time zone, received timestamp with time zone NOT NULL, menu_code text NOT NULL, options integer[], price integer NOT NULL, method integer NOT NULL, tax_job_id bigint, executer bigint, exeputer_type integer, executer_str text );`, `ALTER TABLE ONLY public.tax_job ALTER COLUMN id SET DEFAULT nextval('public.tax_job_id_seq'::regclass);`, `INSERT INTO public.trans VALUES (1, '2023-04-05 06:07:08', '2023-05-06 07:08:09', 'test', ARRAY[5,7], 44, 1, NULL, 1, 1, '');`, }, Assertions: []ScriptTestAssertion{ { Query: `CREATE FUNCTION public.tax_job_trans(t public.trans) RETURNS public.tax_job LANGUAGE plpgsql AS ' # print_strict_params ON DECLARE tjd jsonb; ops jsonb; tj tax_job; name text; BEGIN -- lock trans row PERFORM 1 FROM trans WHERE (vmid, vmtime) = (t.vmid, t.vmtime) LIMIT 1 FOR UPDATE; -- if trans already has tax_job assigned, just return it IF t.tax_job_id IS NOT NULL THEN SELECT * INTO STRICT tj FROM tax_job WHERE id = t.tax_job_id; RETURN tj; END IF; -- op code to human friendly name via catalog SELECT catalog.name INTO name FROM catalog WHERE (vmid, code) = (t.vmid, t.menu_code); IF NOT found THEN name := ''#'' || t.menu_code; END IF; ops := jsonb_build_array (jsonb_build_object(''vmid'', t.vmid, ''time'', t.vmtime, ''name'', name, ''code'', t.menu_code, ''amount'', 1, ''price'', t.price, ''method'', t.method)); INSERT INTO tax_job (state, created, modified, scheduled, processor, ops, gross) VALUES (''sched'', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, ''ru2019'', ops, t.price) RETURNING * INTO STRICT tj; UPDATE trans SET tax_job_id = tj.id WHERE (vmid, vmtime) = (t.vmid, t.vmtime); RETURN tj; END; ';`, Expected: []sql.Row{}, }, { Query: `SELECT public.tax_job_trans(trans.*) FROM public.trans;`, Skip: true, // TODO: implement table.* syntax Expected: []sql.Row{{`(1,sched,"2026-01-23 14:06:32.794817+00","2026-01-23 14:06:32.794817+00","2026-01-23 14:06:32.794817+00",,ru2019,,,44,,"[{""code"": ""test"", ""name"": ""#test"", ""time"": ""2023-04-05T06:07:08+00:00"", ""vmid"": 1, ""price"": 44, ""amount"": 1, ""method"": 1}]")`}}, }, }, }, { Name: "resolve type with empty search path", SetUpScript: []string{ "set search_path to ''", `CREATE TABLE public.ambienttempdetail (tempdetailid integer NOT NULL, panelprojectid integer, threshold_value numeric(10,2), readingintervalinmin integer);`, `insert into public.ambienttempdetail values (1, 101, 25.5, 15);`, }, Assertions: []ScriptTestAssertion{ { Query: `CREATE FUNCTION public.ambienttempdetail_insertupdate(p_panel_project_id integer, p_threshold_value numeric, p_reading_interval_in_min integer) RETURNS integer LANGUAGE plpgsql AS $$ DECLARE v_rtn_value INTEGER; BEGIN IF NOT EXISTS (SELECT * FROM AmbientTempDetail WHERE PanelProjectId = p_panel_project_id) THEN INSERT INTO AmbientTempDetail (PanelProjectId, Threshold_Value, ReadingIntervalInMin) VALUES (p_panel_project_id, p_threshold_value, p_reading_interval_in_min) RETURNING TempDetailId INTO v_rtn_value; ELSE UPDATE AmbientTempDetail SET PanelProjectId = p_panel_project_id, Threshold_Value = p_threshold_value, ReadingIntervalInMin = p_reading_interval_in_min WHERE PanelProjectId = p_panel_project_id; v_rtn_value := p_panel_project_id; END IF; RETURN v_rtn_value; END; $$;`, Expected: []sql.Row{}, }, { Query: "set search_path to 'public'", }, { Query: "SELECT public.ambienttempdetail_insertupdate(101, 25.5, 15);", Expected: []sql.Row{{101}}, }, }, }, { Name: "create function on non-existent table that does not exist yet with 'check_function_bodies'", Assertions: []ScriptTestAssertion{ { Query: `SHOW check_function_bodies;`, Expected: []sql.Row{{1}}, }, { Query: `CREATE FUNCTION public.film_in_stock(p_film_id integer, p_store_id integer, OUT p_film_count integer) RETURNS SETOF integer LANGUAGE sql AS $_$ SELECT inventory_id FROM inventory WHERE film_id = $1 AND store_id = $2 AND inventory_in_stock(inventory_id); $_$;`, ExpectedErr: `table not found`, }, { Query: "SET check_function_bodies = false;", Expected: []sql.Row{}, }, { Query: `CREATE FUNCTION public.film_in_stock(p_film_id integer, p_store_id integer, OUT p_film_count integer) RETURNS SETOF integer LANGUAGE sql AS $_$ SELECT inventory_id FROM inventory WHERE film_id = $1 AND store_id = $2 AND inventory_in_stock(inventory_id); $_$;`, Expected: []sql.Row{}, }, { Query: `CREATE TABLE public.inventory ( inventory_id integer DEFAULT nextval('public.inventory_inventory_id_seq'::regclass) NOT NULL, film_id smallint NOT NULL, store_id smallint NOT NULL, last_update timestamp without time zone DEFAULT now() NOT NULL ); `, Expected: []sql.Row{}, }, }, }, { Name: "DECLARE variable with default value of literal value or parameter reference", Assertions: []ScriptTestAssertion{ { Query: `CREATE OR REPLACE FUNCTION d() RETURNS TEXT[] AS $$ DECLARE chars TEXT[] := '{A,B,C,D,E,F,G,H}'; BEGIN RETURN chars; END; $$ LANGUAGE plpgsql;`, Expected: []sql.Row{}, }, { Query: "SELECT d();", Expected: []sql.Row{{"{A,B,C,D,E,F,G,H}"}}, }, { Query: `CREATE OR REPLACE FUNCTION mylt2 (x text, y text, e int) RETURNS boolean LANGUAGE plpgsql AS $$ declare xx text COLLATE "POSIX" := x; yy text := y; zz int := e; begin return xx < yy; end $$;`, }, { Query: "SELECT mylt2('a', 'B', 1) as f;", Expected: []sql.Row{{"f"}}, }, }, }, { Name: "FOR I LOOP statement", Assertions: []ScriptTestAssertion{ { Query: `CREATE OR REPLACE FUNCTION code() RETURNS VARCHAR AS $$ DECLARE chars TEXT[] := '{A,B,C,D,E,F,G,H}'; result TEXT := ''; i INTEGER; BEGIN FOR i IN 1..3 LOOP result := result || chars[1+i]; END LOOP; RETURN result; END; $$ LANGUAGE plpgsql;`, Expected: []sql.Row{}, }, { Query: "SELECT code();", Expected: []sql.Row{{"BCD"}}, }, }, }, { Name: "FOR S LOOP statement", SetUpScript: []string{ `CREATE TABLE decks ( id bigint NOT NULL, name text DEFAULT NULL::character varying, parent bigint );`, `INSERT INTO decks VALUES (1, 'name1', 2), (2, 'name2', 4), (5, 'name3', 1), (7, 'name4', 9);`, }, Assertions: []ScriptTestAssertion{ { Query: `CREATE FUNCTION delete_deck_tree(p_id bigint) RETURNS void LANGUAGE plpgsql AS $$ DECLARE r record; BEGIN DELETE FROM decks WHERE parent = p_id; FOR r IN SELECT id FROM decks WHERE parent = p_id LOOP PERFORM delete_deck_tree(r.id); END LOOP; DELETE FROM decks WHERE id = p_id; END; $$;`, Expected: []sql.Row{}, }, { Query: "SELECT * from decks;", Expected: []sql.Row{ {1, "name1", 2}, {2, "name2", 4}, {5, "name3", 1}, {7, "name4", 9}, }, }, { Query: "SELECT delete_deck_tree(1);", Expected: []sql.Row{{nil}}, }, { Query: "SELECT * from decks;", Expected: []sql.Row{ {2, "name2", 4}, {7, "name4", 9}, }, }, }, }, { Name: "use multiple types returning in block statement", SetUpScript: []string{ `CREATE TABLE test (id int, v text);`, `INSERT INTO test VALUES (1, 'r'), (2, 'g');`, }, Assertions: []ScriptTestAssertion{ { Query: `CREATE FUNCTION return_table() RETURNS int AS $$ DECLARE ti int; tt text; BEGIN INSERT INTO test VALUES (3, 'w') returning * INTO ti, tt; RETURN ti; END; $$ LANGUAGE plpgsql;`, Expected: []sql.Row{}, }, { Query: "select return_table();", Expected: []sql.Row{{3}}, }, }, }, { Name: "distinct expression over function", SetUpScript: []string{ `CREATE TABLE test (pk SERIAL PRIMARY KEY);`, }, Assertions: []ScriptTestAssertion{ { Query: `CREATE FUNCTION func1() RETURNS INT4 AS $$ DECLARE ret INT4; BEGIN INSERT INTO test VALUES (DEFAULT) RETURNING pk INTO ret; IF ret % 2 <> 0 THEN RETURN NULL; END IF; RETURN ret; END; $$ LANGUAGE plpgsql;`, Expected: []sql.Row{}, }, { Query: "SELECT func1() IS DISTINCT FROM func1() as DISTINCT_RESULT;", Expected: []sql.Row{{"t"}}, }, { Query: "SELECT * FROM test;", Expected: []sql.Row{{1}, {2}}, }, }, }, }) }