Files
dolthub--doltgresql/testing/go/create_function_plpgsql_test.go
2026-07-13 12:32:25 +08:00

1711 lines
41 KiB
Go

// 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
<<cont_label>>
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_label>>
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_label>>
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}},
},
},
},
})
}