435 lines
12 KiB
Go
435 lines
12 KiB
Go
// Copyright 2025 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 TestCreateFunctionsLanguageSQL(t *testing.T) {
|
|
RunScripts(t, []ScriptTest{
|
|
{
|
|
Name: "unnamed parameter",
|
|
SetUpScript: []string{},
|
|
Assertions: []ScriptTestAssertion{
|
|
{
|
|
Query: `CREATE FUNCTION alt_func1(int) RETURNS int LANGUAGE sql AS 'SELECT $1 + 1';`,
|
|
Expected: []sql.Row{},
|
|
},
|
|
{
|
|
Query: `SELECT alt_func1(3);`,
|
|
Expected: []sql.Row{{4}},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Name: "named parameter",
|
|
SetUpScript: []string{},
|
|
Assertions: []ScriptTestAssertion{
|
|
{
|
|
Query: `CREATE FUNCTION alt_func1(x int) RETURNS int LANGUAGE sql AS 'SELECT x + 1';`,
|
|
Expected: []sql.Row{},
|
|
},
|
|
{
|
|
Query: `SELECT alt_func1(3);`,
|
|
Expected: []sql.Row{{4}},
|
|
},
|
|
{
|
|
Query: `CREATE FUNCTION sub_numbers(x int, y int) RETURNS int LANGUAGE sql AS 'SELECT y - x';`,
|
|
Expected: []sql.Row{},
|
|
},
|
|
{
|
|
Query: `SELECT sub_numbers(1, 2);`,
|
|
Expected: []sql.Row{{1}},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Name: "unknown functions",
|
|
SetUpScript: []string{},
|
|
Assertions: []ScriptTestAssertion{
|
|
{
|
|
Query: `CREATE FUNCTION get_grade_description(score INT)
|
|
RETURNS TEXT
|
|
LANGUAGE SQL
|
|
AS $$
|
|
SELECT
|
|
CASE
|
|
WHEN score >= 90 THEN 'Excellent'
|
|
WHEN score >= 75 THEN 'Good'
|
|
WHEN score >= 50 THEN 'Average'
|
|
ELSE 'Fail'
|
|
END;
|
|
$$;`,
|
|
Expected: []sql.Row{},
|
|
},
|
|
{
|
|
Query: `SELECT get_grade_description(92);`,
|
|
Expected: []sql.Row{{"Excellent"}},
|
|
},
|
|
{
|
|
Query: `SELECT get_grade_description(65);`,
|
|
Expected: []sql.Row{{"Average"}},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Name: "nested functions",
|
|
SetUpScript: []string{},
|
|
Assertions: []ScriptTestAssertion{
|
|
{
|
|
Query: `CREATE FUNCTION calculate_double_sum(x INT, y INT)
|
|
RETURNS INT
|
|
LANGUAGE SQL
|
|
AS $$
|
|
SELECT add_numbers(x, y) * 2;
|
|
$$;`,
|
|
// TODO: error message should be: function add_numbers(integer, integer) does not exist
|
|
ExpectedErr: "function: 'add_numbers' not found",
|
|
},
|
|
{
|
|
Query: `CREATE FUNCTION add_numbers(int, int) RETURNS int LANGUAGE sql AS 'SELECT $1 + $2';`,
|
|
Expected: []sql.Row{},
|
|
},
|
|
{
|
|
Query: `CREATE FUNCTION calculate_double_sum(x INT, y INT)
|
|
RETURNS INT
|
|
LANGUAGE SQL
|
|
AS $$
|
|
SELECT add_numbers(x, y) * 2;
|
|
$$;`,
|
|
Expected: []sql.Row{},
|
|
},
|
|
{
|
|
Query: `SELECT calculate_double_sum(1, 2);`,
|
|
Expected: []sql.Row{{6}},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Name: "function returning multiple rows",
|
|
Assertions: []ScriptTestAssertion{
|
|
{
|
|
Query: `CREATE FUNCTION gen(a int) RETURNS SETOF INT LANGUAGE SQL AS $$ SELECT generate_series(1, a) $$ STABLE;`,
|
|
Expected: []sql.Row{},
|
|
},
|
|
{
|
|
Query: `SELECT * FROM gen(3);`,
|
|
Expected: []sql.Row{{1}, {2}, {3}},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Name: "function with create or replace view",
|
|
Assertions: []ScriptTestAssertion{
|
|
{
|
|
Query: `CREATE FUNCTION public.sp_build_view_bathymetry_layer() RETURNS void
|
|
LANGUAGE sql
|
|
AS $$
|
|
CREATE OR REPLACE VIEW public.view_bathymetry_layer AS
|
|
SELECT 1;
|
|
$$;`,
|
|
Expected: []sql.Row{},
|
|
},
|
|
{
|
|
Query: `SELECT public.sp_build_view_bathymetry_layer()`,
|
|
Expected: []sql.Row{},
|
|
},
|
|
{
|
|
Query: `SELECT * from view_bathymetry_layer`,
|
|
Expected: []sql.Row{{1}},
|
|
},
|
|
{
|
|
Query: `SELECT public.sp_build_view_bathymetry_layer()`,
|
|
Expected: []sql.Row{},
|
|
},
|
|
{
|
|
Query: `SELECT * from view_bathymetry_layer`,
|
|
Expected: []sql.Row{{1}},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Name: "function with update ... returning",
|
|
SetUpScript: []string{
|
|
`CREATE TYPE public.tax_job_state AS ENUM (
|
|
'sched',
|
|
'busy',
|
|
'final',
|
|
'help'
|
|
);`,
|
|
`CREATE TABLE public.tax_job (
|
|
id bigint NOT NULL,
|
|
state public.tax_job_state NOT NULL,
|
|
created timestamp NOT NULL,
|
|
modified timestamp NOT NULL,
|
|
scheduled timestamp,
|
|
worker text,
|
|
processor text,
|
|
ext_id text,
|
|
data jsonb,
|
|
gross integer,
|
|
notes text[],
|
|
ops jsonb,
|
|
CONSTRAINT tax_job_check CHECK ((NOT ((state = 'sched'::public.tax_job_state) AND (scheduled IS NULL)))),
|
|
CONSTRAINT tax_job_check1 CHECK ((NOT ((state = 'busy'::public.tax_job_state) AND (worker IS NULL))))
|
|
);`,
|
|
`INSERT INTO tax_job (id, state, created, modified, scheduled, worker, processor, ext_id, data) VALUES (1, 'sched', '2025-05-05 05:05:05', '2025-05-05 05:05:05', '2025-05-05 05:05:05', 'worker', 'processor', 'ext_id', NULL)`,
|
|
},
|
|
Assertions: []ScriptTestAssertion{
|
|
{
|
|
Query: `CREATE FUNCTION public.tax_job_take(arg_worker text) RETURNS SETOF public.tax_job
|
|
LANGUAGE sql
|
|
AS '
|
|
UPDATE
|
|
tax_job
|
|
SET
|
|
state = ''busy'',
|
|
worker = arg_worker
|
|
WHERE
|
|
state = ''sched''
|
|
AND scheduled <= CURRENT_TIMESTAMP
|
|
RETURNING
|
|
*;
|
|
';`,
|
|
Expected: []sql.Row{},
|
|
},
|
|
{
|
|
Query: `SELECT public.tax_job_take('worker')`,
|
|
Expected: []sql.Row{{`(1,busy,"2025-05-05 05:05:05","2025-05-05 05:05:05","2025-05-05 05:05:05",worker,processor,ext_id,,,,)`}},
|
|
},
|
|
{
|
|
Query: `INSERT INTO tax_job (id, state, created, modified, scheduled, worker, processor, ext_id, data) VALUES (2, 'sched', '2025-05-05 05:05:06', '2025-05-05 05:05:06', '2025-05-05 05:05:06', 'worker', 'processor', 'ext_id', NULL), (3, 'sched', '2025-05-05 05:05:07', '2025-05-05 05:05:07', '2025-05-05 05:05:07', 'worker', 'processor', 'ext_id', NULL)`,
|
|
},
|
|
{
|
|
Query: `SELECT public.tax_job_take('worker')`,
|
|
Expected: []sql.Row{
|
|
{`(2,busy,"2025-05-05 05:05:06","2025-05-05 05:05:06","2025-05-05 05:05:06",worker,processor,ext_id,,,,)`},
|
|
{`(3,busy,"2025-05-05 05:05:07","2025-05-05 05:05:07","2025-05-05 05:05:07",worker,processor,ext_id,,,,)`},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Name: "function with delete",
|
|
SetUpScript: []string{
|
|
`CREATE TABLE test (id bigint NOT NULL, state text NOT NULL);`,
|
|
`INSERT INTO test VALUES (1, 'sched'), (2, 'busy');`,
|
|
},
|
|
Assertions: []ScriptTestAssertion{
|
|
{
|
|
Query: `CREATE FUNCTION d(w text) RETURNS bigint
|
|
LANGUAGE sql
|
|
AS '
|
|
DELETE FROM test
|
|
WHERE
|
|
state = w
|
|
RETURNING
|
|
id;
|
|
';`,
|
|
Expected: []sql.Row{},
|
|
},
|
|
{
|
|
Query: `SELECT * FROM test;`,
|
|
Expected: []sql.Row{{1, "sched"}, {2, "busy"}},
|
|
},
|
|
{
|
|
Query: `SELECT d('sched');`,
|
|
Expected: []sql.Row{{1}},
|
|
},
|
|
{
|
|
Query: `SELECT * FROM test;`,
|
|
Expected: []sql.Row{{2, "busy"}},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Name: "multiple statements in function",
|
|
SetUpScript: []string{
|
|
`CREATE TABLE test (id int);`,
|
|
`INSERT INTO test VALUES (1), (2), (3);`,
|
|
`CREATE VIEW test1 AS SELECT * FROM test WHERE id = 1;`,
|
|
`CREATE VIEW test2 AS SELECT * FROM test WHERE id = 2;`,
|
|
},
|
|
Assertions: []ScriptTestAssertion{
|
|
{
|
|
Query: `CREATE FUNCTION drop_views() RETURNS void
|
|
LANGUAGE sql
|
|
AS $$
|
|
DROP VIEW test1;
|
|
DROP VIEW test2;
|
|
$$;`,
|
|
Expected: []sql.Row{},
|
|
},
|
|
{
|
|
Query: `SELECT * FROM test1`,
|
|
Expected: []sql.Row{{1}},
|
|
},
|
|
{
|
|
Query: `SELECT * FROM test2`,
|
|
Expected: []sql.Row{{2}},
|
|
},
|
|
{
|
|
Query: `SELECT drop_views();`,
|
|
Expected: []sql.Row{{nil}},
|
|
},
|
|
{
|
|
Query: `SELECT * FROM test1`,
|
|
ExpectedErr: `not found`,
|
|
},
|
|
{
|
|
Query: `SELECT * FROM test2`,
|
|
ExpectedErr: `not found`,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Name: "function with default expression in parameter",
|
|
SetUpScript: []string{
|
|
`CREATE TABLE cp_test (a int, b text);`,
|
|
},
|
|
Assertions: []ScriptTestAssertion{
|
|
{
|
|
Query: `CREATE OR REPLACE FUNCTION dfunc(e int, d text, f int default 100)
|
|
RETURNS int LANGUAGE SQL
|
|
AS $$
|
|
INSERT INTO cp_test VALUES(e+f, d);
|
|
SELECT a FROM cp_test WHERE b = d;
|
|
$$;`,
|
|
Expected: []sql.Row{},
|
|
},
|
|
{
|
|
Query: `CREATE OR REPLACE FUNCTION dfunc(e int, f int default 100)
|
|
RETURNS int LANGUAGE SQL
|
|
AS $$
|
|
INSERT INTO cp_test VALUES(e+f, 'seconddfunc');
|
|
SELECT a FROM cp_test WHERE b = 'seconddfunc';
|
|
$$;`,
|
|
Expected: []sql.Row{},
|
|
},
|
|
{
|
|
Query: `SELECT * FROM dfunc(10, 'Hello', 20);`,
|
|
Expected: []sql.Row{{30}},
|
|
},
|
|
{
|
|
Query: `SELECT * FROM cp_test`,
|
|
Expected: []sql.Row{{30, "Hello"}},
|
|
},
|
|
{
|
|
Query: `SELECT * FROM dfunc(50, 'Bye');`,
|
|
Expected: []sql.Row{{150}},
|
|
},
|
|
{
|
|
Query: `SELECT * FROM cp_test`,
|
|
Expected: []sql.Row{{30, "Hello"}, {150, "Bye"}},
|
|
},
|
|
{
|
|
Query: `SELECT dfunc(2, 'After');`,
|
|
Expected: []sql.Row{{102}},
|
|
},
|
|
{
|
|
Query: `SELECT * FROM cp_test`,
|
|
Expected: []sql.Row{{30, "Hello"}, {150, "Bye"}, {102, "After"}},
|
|
},
|
|
{
|
|
Query: `CREATE OR REPLACE FUNCTION dfunc(e int, f text default '100')
|
|
RETURNS int LANGUAGE SQL
|
|
AS $$
|
|
INSERT INTO cp_test VALUES(e, f);
|
|
SELECT a FROM cp_test WHERE b = f;
|
|
$$;`,
|
|
Expected: []sql.Row{},
|
|
},
|
|
{
|
|
// TODO: the error message should be "function dfunc(integer) is not unique"
|
|
Query: `SELECT dfunc(50);`,
|
|
ExpectedErr: `does not exist`,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Name: "use sql statements in BEGIN ATOMIC ... END in sql_body",
|
|
SetUpScript: []string{},
|
|
Assertions: []ScriptTestAssertion{
|
|
{
|
|
Query: `CREATE FUNCTION match_default() RETURNS jsonb
|
|
LANGUAGE sql
|
|
BEGIN ATOMIC
|
|
SELECT jsonb_build_object('k', 6, 'm', 2048, 'include_original', true, 'tokenizer', json_build_object('kind', 'ngram', 'token_length', 3), 'token_filters', json_build_array(json_build_object('kind', 'downcase'))) AS jsonb_build_object;
|
|
END;`,
|
|
Expected: []sql.Row{},
|
|
},
|
|
{
|
|
Skip: true, // TODO support json_build_object() function
|
|
Query: `SELECT public.match_default();`,
|
|
Expected: []sql.Row{{`{"k": 6, "m": 2048, "tokenizer": {"kind": "ngram", "token_length": 3}, "token_filters": [{"kind": "downcase"}], "include_original": true}`}},
|
|
},
|
|
{
|
|
Query: `CREATE FUNCTION select1() RETURNS int
|
|
LANGUAGE sql
|
|
BEGIN ATOMIC
|
|
SELECT 1;
|
|
END;`,
|
|
Expected: []sql.Row{},
|
|
},
|
|
{
|
|
Query: `SELECT select1();`,
|
|
Expected: []sql.Row{{1}},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Name: "use RETURN in BEGIN ATOMIC ... END in sql_body",
|
|
SetUpScript: []string{},
|
|
Assertions: []ScriptTestAssertion{
|
|
{
|
|
Query: `CREATE FUNCTION return1() RETURNS text
|
|
LANGUAGE sql
|
|
BEGIN ATOMIC
|
|
RETURN 1::text || 'one';
|
|
END;`,
|
|
Expected: []sql.Row{},
|
|
},
|
|
{
|
|
Query: `SELECT return1();`,
|
|
Expected: []sql.Row{{"1one"}},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Name: "user-defined composite array parameter and return type",
|
|
SetUpScript: []string{
|
|
`CREATE TYPE aggtype AS (a integer, b integer, c text)`,
|
|
},
|
|
Assertions: []ScriptTestAssertion{
|
|
{
|
|
Query: `CREATE FUNCTION aggf_trans(aggtype[],integer,integer,text) RETURNS aggtype[] AS 'select array_append($1,ROW($2,$3,$4)::aggtype)' LANGUAGE sql STRICT IMMUTABLE`,
|
|
Expected: []sql.Row{},
|
|
},
|
|
{
|
|
Query: `SELECT aggf_trans(ARRAY[]::aggtype[], 1, 2, 'hello')`,
|
|
Expected: []sql.Row{{`{"(1,2,hello)"}`}},
|
|
},
|
|
{
|
|
Query: `SELECT aggf_trans(ARRAY[ROW(1,2,'hello')::aggtype], 3, 4, 'world')`,
|
|
Expected: []sql.Row{{`{"(1,2,hello)","(3,4,world)"}`}},
|
|
},
|
|
},
|
|
},
|
|
})
|
|
}
|