fix(xugu): preserve programmable object DDL execution
This commit is contained in:
1 parent
f56d3a86c1
commit
3836142f72
5 files changed
+660
-36
No files matched your search
@@ -3363,7 +3363,74 @@ func errorResponse(id json.RawMessage, err error) response {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func trimStatementSQL(sqlText string) string {
|
func trimStatementSQL(sqlText string) string {
|
||||||
return strings.TrimRight(strings.TrimSpace(sqlText), "; \t\r\n")
|
trimmed := strings.TrimSpace(sqlText)
|
||||||
|
if isXuguProgrammableObjectDDL(trimmed) {
|
||||||
|
// Xugu's compiler requires the terminator after END. The desktop
|
||||||
|
// statement splitter already removes only client-side delimiters, while
|
||||||
|
// retaining this one for Oracle-style procedural objects.
|
||||||
|
return trimmed
|
||||||
|
}
|
||||||
|
return strings.TrimRight(trimmed, "; \t\r\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func isXuguProgrammableObjectDDL(sqlText string) bool {
|
||||||
|
fields := strings.Fields(strings.ToUpper(stripLeadingSQLComments(sqlText)))
|
||||||
|
if len(fields) < 2 || fields[0] != "CREATE" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip CREATE modifiers used by Xugu/Oracle-style programmable DDL:
|
||||||
|
// OR REPLACE, FORCE/NOFORCE, and EDITIONABLE/NONEDITIONABLE (any order).
|
||||||
|
index := 1
|
||||||
|
for index < len(fields) {
|
||||||
|
if index+1 < len(fields) && fields[index] == "OR" && fields[index+1] == "REPLACE" {
|
||||||
|
index += 2
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
switch fields[index] {
|
||||||
|
case "FORCE", "NOFORCE", "EDITIONABLE", "NONEDITIONABLE":
|
||||||
|
index++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if index >= len(fields) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
switch fields[index] {
|
||||||
|
case "PROCEDURE", "FUNCTION", "TRIGGER", "PACKAGE":
|
||||||
|
// PACKAGE also covers PACKAGE BODY (next token is BODY).
|
||||||
|
return true
|
||||||
|
case "TYPE":
|
||||||
|
// Only TYPE BODY needs the trailing END; terminator.
|
||||||
|
// Plain CREATE TYPE ... AS OBJECT (...); is ordinary SQL.
|
||||||
|
return index+1 < len(fields) && fields[index+1] == "BODY"
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func stripLeadingSQLComments(sqlText string) string {
|
||||||
|
remaining := strings.TrimLeft(sqlText, " \t\r\n")
|
||||||
|
for {
|
||||||
|
switch {
|
||||||
|
case strings.HasPrefix(remaining, "--"):
|
||||||
|
lineEnd := strings.IndexByte(remaining, '\n')
|
||||||
|
if lineEnd < 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
remaining = strings.TrimLeft(remaining[lineEnd+1:], " \t\r\n")
|
||||||
|
case strings.HasPrefix(remaining, "/*"):
|
||||||
|
commentEnd := strings.Index(remaining[2:], "*/")
|
||||||
|
if commentEnd < 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
remaining = strings.TrimLeft(remaining[commentEnd+4:], " \t\r\n")
|
||||||
|
default:
|
||||||
|
return remaining
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func isQuerySQL(sqlText string) bool {
|
func isQuerySQL(sqlText string) bool {
|
||||||
|
|||||||
@@ -1037,6 +1037,69 @@ func TestNormalizeValuePreservesDriverNumericTypes(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestTrimStatementSQLKeepsXuguProgrammableObjectTerminators(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
name string
|
||||||
|
sql string
|
||||||
|
}{
|
||||||
|
{"procedure", "CREATE OR REPLACE PROCEDURE p AS BEGIN NULL; END;"},
|
||||||
|
{"procedure without or replace", "CREATE PROCEDURE p AS BEGIN NULL; END;"},
|
||||||
|
{"function", "CREATE OR REPLACE FUNCTION f RETURN INTEGER AS BEGIN RETURN 1; END;"},
|
||||||
|
{"function without or replace", "CREATE FUNCTION f RETURN INTEGER AS BEGIN RETURN 1; END;"},
|
||||||
|
{"trigger", "CREATE OR REPLACE TRIGGER t BEFORE INSERT ON events FOR EACH ROW BEGIN NULL; END;"},
|
||||||
|
{"trigger without or replace", "CREATE TRIGGER t BEFORE INSERT ON events FOR EACH ROW BEGIN NULL; END;"},
|
||||||
|
{"package", "CREATE OR REPLACE PACKAGE pkg AS PROCEDURE ping; END pkg;"},
|
||||||
|
{"package without or replace", "CREATE PACKAGE pkg AS PROCEDURE ping; END pkg;"},
|
||||||
|
{"package body", "CREATE OR REPLACE PACKAGE BODY pkg AS PROCEDURE ping AS BEGIN NULL; END ping; END pkg;"},
|
||||||
|
{"force package", "CREATE OR REPLACE FORCE PACKAGE pkg AS PROCEDURE ping; END pkg;"},
|
||||||
|
{"noforce package", "CREATE OR REPLACE NOFORCE PACKAGE pkg AS PROCEDURE ping; END pkg;"},
|
||||||
|
{"force package body", "CREATE OR REPLACE FORCE PACKAGE BODY pkg AS PROCEDURE ping AS BEGIN NULL; END ping; END pkg;"},
|
||||||
|
{"noforce package body", "CREATE OR REPLACE NOFORCE PACKAGE BODY pkg AS PROCEDURE ping AS BEGIN NULL; END ping; END pkg;"},
|
||||||
|
{"type body", "CREATE OR REPLACE TYPE BODY obj AS MEMBER PROCEDURE ping IS BEGIN NULL; END; END;"},
|
||||||
|
{"type body without or replace", "CREATE TYPE BODY obj AS MEMBER PROCEDURE ping IS BEGIN NULL; END; END;"},
|
||||||
|
{"force type body", "CREATE OR REPLACE FORCE TYPE BODY obj AS MEMBER PROCEDURE ping IS BEGIN NULL; END; END;"},
|
||||||
|
{"leading comments", "-- generated source\n/* object DDL */\nCREATE OR REPLACE PROCEDURE p AS BEGIN NULL; END;"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range cases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
if got := trimStatementSQL(tc.sql); got != tc.sql {
|
||||||
|
t.Fatalf("trimStatementSQL() = %q, want %q", got, tc.sql)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if got := trimStatementSQL("CREATE TABLE items (id INTEGER);"); got != "CREATE TABLE items (id INTEGER)" {
|
||||||
|
t.Fatalf("regular SQL terminator should be removed, got %q", got)
|
||||||
|
}
|
||||||
|
// Plain CREATE TYPE ends with ");" and is ordinary SQL — strip the client terminator.
|
||||||
|
if got := trimStatementSQL("CREATE OR REPLACE TYPE address_t AS OBJECT (id INT);"); got != "CREATE OR REPLACE TYPE address_t AS OBJECT (id INT)" {
|
||||||
|
t.Fatalf("plain TYPE should strip trailing semicolon, got %q", got)
|
||||||
|
}
|
||||||
|
if got := trimStatementSQL("CREATE TYPE address_t AS OBJECT (id INT);"); got != "CREATE TYPE address_t AS OBJECT (id INT)" {
|
||||||
|
t.Fatalf("plain TYPE without OR REPLACE should strip trailing semicolon, got %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExecuteQueryPreservesXuguTypeBodyTerminator(t *testing.T) {
|
||||||
|
resetXuguRecordingDriver()
|
||||||
|
db, err := sql.Open("xugu-test-recording", "")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
s := newServer()
|
||||||
|
s.db = db
|
||||||
|
sqlText := "CREATE OR REPLACE TYPE BODY obj_t AS MEMBER PROCEDURE ping IS BEGIN NULL; END; END;"
|
||||||
|
if _, err := s.executeQuery(queryOptions{SQL: sqlText}); err != nil {
|
||||||
|
t.Fatalf("executeQuery() error: %v", err)
|
||||||
|
}
|
||||||
|
if got := recordedXuguSQL(); got != sqlText {
|
||||||
|
t.Fatalf("Agent executed %q, want %q", got, sqlText)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func contains(values []string, target string) bool {
|
func contains(values []string, target string) bool {
|
||||||
for _, value := range values {
|
for _, value := range values {
|
||||||
if value == target {
|
if value == target {
|
||||||
@@ -1051,10 +1114,48 @@ func contains(values []string, target string) bool {
|
|||||||
func init() {
|
func init() {
|
||||||
sql.Register("xugu-test-blocking", &xuguBlockingDriver{})
|
sql.Register("xugu-test-blocking", &xuguBlockingDriver{})
|
||||||
sql.Register("xugu-test-fast", &xuguFastDriver{})
|
sql.Register("xugu-test-fast", &xuguFastDriver{})
|
||||||
|
sql.Register("xugu-test-recording", &xuguRecordingDriver{})
|
||||||
sql.Register("xugu-test-legacy-columns", &xuguLegacyColumnsDriver{})
|
sql.Register("xugu-test-legacy-columns", &xuguLegacyColumnsDriver{})
|
||||||
sql.Register("xugu-test-table-objects", &xuguTableObjectsDriver{})
|
sql.Register("xugu-test-table-objects", &xuguTableObjectsDriver{})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type xuguRecordingDriver struct{}
|
||||||
|
|
||||||
|
var xuguRecordingState struct {
|
||||||
|
sync.Mutex
|
||||||
|
sql string
|
||||||
|
}
|
||||||
|
|
||||||
|
func resetXuguRecordingDriver() {
|
||||||
|
xuguRecordingState.Lock()
|
||||||
|
xuguRecordingState.sql = ""
|
||||||
|
xuguRecordingState.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func recordedXuguSQL() string {
|
||||||
|
xuguRecordingState.Lock()
|
||||||
|
defer xuguRecordingState.Unlock()
|
||||||
|
return xuguRecordingState.sql
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *xuguRecordingDriver) Open(name string) (driver.Conn, error) {
|
||||||
|
return &xuguRecordingConn{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type xuguRecordingConn struct{}
|
||||||
|
|
||||||
|
func (c *xuguRecordingConn) Prepare(query string) (driver.Stmt, error) {
|
||||||
|
return nil, errors.New("not supported")
|
||||||
|
}
|
||||||
|
func (c *xuguRecordingConn) Close() error { return nil }
|
||||||
|
func (c *xuguRecordingConn) Begin() (driver.Tx, error) { return nil, errors.New("not supported") }
|
||||||
|
func (c *xuguRecordingConn) ExecContext(_ context.Context, query string, _ []driver.NamedValue) (driver.Result, error) {
|
||||||
|
xuguRecordingState.Lock()
|
||||||
|
xuguRecordingState.sql = query
|
||||||
|
xuguRecordingState.Unlock()
|
||||||
|
return driver.ResultNoRows, nil
|
||||||
|
}
|
||||||
|
|
||||||
type xuguTableObjectsDriver struct{}
|
type xuguTableObjectsDriver struct{}
|
||||||
|
|
||||||
func (d *xuguTableObjectsDriver) Open(name string) (driver.Conn, error) {
|
func (d *xuguTableObjectsDriver) Open(name string) (driver.Conn, error) {
|
||||||
|
|||||||
@@ -80,6 +80,75 @@ BEGIN
|
|||||||
NULL;
|
NULL;
|
||||||
END;`;
|
END;`;
|
||||||
|
|
||||||
|
const xuguProgrammableObjectFixtures = [
|
||||||
|
`CREATE OR REPLACE PROCEDURE dbx_xugu_procedure AS
|
||||||
|
v_value INTEGER;
|
||||||
|
BEGIN
|
||||||
|
v_value := 1;
|
||||||
|
END;`,
|
||||||
|
`CREATE PROCEDURE dbx_xugu_procedure_without_replace AS
|
||||||
|
v_value INTEGER;
|
||||||
|
BEGIN
|
||||||
|
v_value := 1;
|
||||||
|
END;`,
|
||||||
|
`CREATE OR REPLACE FUNCTION dbx_xugu_function RETURN INTEGER AS
|
||||||
|
BEGIN
|
||||||
|
RETURN 1;
|
||||||
|
END;`,
|
||||||
|
`CREATE FUNCTION dbx_xugu_function_without_replace RETURN INTEGER AS
|
||||||
|
BEGIN
|
||||||
|
RETURN 1;
|
||||||
|
END;`,
|
||||||
|
`CREATE OR REPLACE TRIGGER dbx_xugu_trigger
|
||||||
|
BEFORE INSERT ON dbx_xugu_events
|
||||||
|
FOR EACH ROW
|
||||||
|
BEGIN
|
||||||
|
NULL;
|
||||||
|
END;`,
|
||||||
|
`CREATE TRIGGER dbx_xugu_trigger_without_replace
|
||||||
|
BEFORE INSERT ON dbx_xugu_events
|
||||||
|
FOR EACH ROW
|
||||||
|
BEGIN
|
||||||
|
NULL;
|
||||||
|
END;`,
|
||||||
|
`CREATE OR REPLACE PACKAGE BODY dbx_xugu_package AS
|
||||||
|
PROCEDURE ping AS
|
||||||
|
BEGIN
|
||||||
|
NULL;
|
||||||
|
END ping;
|
||||||
|
END dbx_xugu_package;`,
|
||||||
|
`CREATE PACKAGE BODY dbx_xugu_package_without_replace AS
|
||||||
|
PROCEDURE ping AS
|
||||||
|
BEGIN
|
||||||
|
NULL;
|
||||||
|
END ping;
|
||||||
|
END dbx_xugu_package_without_replace;`,
|
||||||
|
`CREATE OR REPLACE FORCE PACKAGE BODY dbx_xugu_force_package AS
|
||||||
|
PROCEDURE ping AS
|
||||||
|
BEGIN
|
||||||
|
NULL;
|
||||||
|
END ping;
|
||||||
|
END dbx_xugu_force_package;`,
|
||||||
|
`CREATE OR REPLACE NOFORCE PACKAGE BODY dbx_xugu_noforce_package AS
|
||||||
|
PROCEDURE ping AS
|
||||||
|
BEGIN
|
||||||
|
NULL;
|
||||||
|
END ping;
|
||||||
|
END dbx_xugu_noforce_package;`,
|
||||||
|
`CREATE OR REPLACE TYPE BODY dbx_xugu_type AS
|
||||||
|
MEMBER PROCEDURE ping IS
|
||||||
|
BEGIN
|
||||||
|
NULL;
|
||||||
|
END;
|
||||||
|
END;`,
|
||||||
|
`CREATE TYPE BODY dbx_xugu_type_without_replace AS
|
||||||
|
MEMBER PROCEDURE ping IS
|
||||||
|
BEGIN
|
||||||
|
NULL;
|
||||||
|
END;
|
||||||
|
END;`,
|
||||||
|
];
|
||||||
|
|
||||||
const mysqlRoutineFixture = `CREATE PROCEDURE p()
|
const mysqlRoutineFixture = `CREATE PROCEDURE p()
|
||||||
BEGIN
|
BEGIN
|
||||||
SELECT 1;
|
SELECT 1;
|
||||||
@@ -223,6 +292,54 @@ describe("splitSqlStatementRanges", () => {
|
|||||||
expect(rangeSqlTexts(splitSqlStatementRanges(gaussDbNestedProcedure, "gaussdb"))).toEqual([gaussDbNestedProcedure]);
|
expect(rangeSqlTexts(splitSqlStatementRanges(gaussDbNestedProcedure, "gaussdb"))).toEqual([gaussDbNestedProcedure]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("keeps Xugu programmable object DDL together and retains its terminator", () => {
|
||||||
|
for (const sql of xuguProgrammableObjectFixtures) {
|
||||||
|
const ranges = splitSqlStatementRanges(`${sql}\nSELECT 1;`, "xugu");
|
||||||
|
expect(rangeSqlTexts(ranges)).toEqual([sql, "SELECT 1"]);
|
||||||
|
expect(ranges[0].sql.trimEnd()).toMatch(/END(?:\s+\w+)?;$/);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("splits Xugu package specification without a slash before following SQL", () => {
|
||||||
|
const packageSpec = `CREATE OR REPLACE PACKAGE pkg_utils AS
|
||||||
|
FUNCTION get_version RETURN VARCHAR2;
|
||||||
|
PROCEDURE log_message(msg VARCHAR2);
|
||||||
|
END pkg_utils;`;
|
||||||
|
const forcePackageSpec = `CREATE OR REPLACE FORCE PACKAGE pkg_utils AS
|
||||||
|
PROCEDURE ping;
|
||||||
|
END pkg_utils;`;
|
||||||
|
const packageSpecWithoutReplace = `CREATE PACKAGE pkg_utils_without_replace AS
|
||||||
|
PROCEDURE ping;
|
||||||
|
END pkg_utils_without_replace;`;
|
||||||
|
|
||||||
|
expect(rangeSqlTexts(splitSqlStatementRanges(`${packageSpec}\nSELECT 1;`, "xugu"))).toEqual([packageSpec, "SELECT 1"]);
|
||||||
|
expect(rangeSqlTexts(splitSqlStatementRanges(`${packageSpec}\n/\nSELECT 1;`, "xugu"))).toEqual([packageSpec, "SELECT 1"]);
|
||||||
|
expect(rangeSqlTexts(splitSqlStatementRanges(`${forcePackageSpec}\nSELECT 1;`, "xugu"))).toEqual([forcePackageSpec, "SELECT 1"]);
|
||||||
|
expect(rangeSqlTexts(splitSqlStatementRanges(`${packageSpecWithoutReplace}\nSELECT 1;`, "xugu"))).toEqual([packageSpecWithoutReplace, "SELECT 1"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("splits plain CREATE TYPE AS OBJECT on semicolon without waiting for END", () => {
|
||||||
|
const sql = "CREATE OR REPLACE TYPE address_t AS OBJECT (id INT);\nSELECT 1;";
|
||||||
|
expect(rangeSqlTexts(splitSqlStatementRanges(sql, "xugu"))).toEqual(["CREATE OR REPLACE TYPE address_t AS OBJECT (id INT)", "SELECT 1"]);
|
||||||
|
expect(rangeSqlTexts(splitSqlStatementRanges("CREATE TYPE address_t_without_replace AS OBJECT (id INT);\nSELECT 1;", "xugu"))).toEqual(["CREATE TYPE address_t_without_replace AS OBJECT (id INT)", "SELECT 1"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps Oracle-style CASE expressions inside Xugu and Oracle routines", () => {
|
||||||
|
const routine = `CREATE OR REPLACE FUNCTION dbx_case_expr RETURN NUMBER AS
|
||||||
|
BEGIN
|
||||||
|
RETURN CASE WHEN 1 = 1 THEN CASE WHEN 2 = 2 THEN 1 ELSE 2 END ELSE 0 END;
|
||||||
|
END;`;
|
||||||
|
const caseStatementRoutine = `CREATE OR REPLACE PROCEDURE dbx_case_statement AS
|
||||||
|
BEGIN
|
||||||
|
CASE WHEN 1 = 1 THEN NULL; ELSE NULL; END CASE;
|
||||||
|
END;`;
|
||||||
|
|
||||||
|
for (const database of ["xugu", "oracle"] as const) {
|
||||||
|
expect(rangeSqlTexts(splitSqlStatementRanges(`${routine}\nSELECT 1;`, database))).toEqual([routine, "SELECT 1"]);
|
||||||
|
expect(rangeSqlTexts(splitSqlStatementRanges(`${caseStatementRoutine}\nSELECT 1;`, database))).toEqual([caseStatementRoutine, "SELECT 1"]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it("keeps SAP HANA DO blocks together", () => {
|
it("keeps SAP HANA DO blocks together", () => {
|
||||||
const ranges = splitSqlStatementRanges(sapHanaDoBlockFixture, "saphana");
|
const ranges = splitSqlStatementRanges(sapHanaDoBlockFixture, "saphana");
|
||||||
|
|
||||||
|
|||||||
@@ -249,14 +249,16 @@ const ALTER_BODY_KEYWORDS = new Set(["ADD", "ALTER", "COMMENT", "DROP", "MODIFY"
|
|||||||
const CLICKHOUSE_ALTER_TABLE_HEADER = /^ALTER\s+TABLE\s+(?:(?:[A-Za-z_][\w$]*|`(?:``|[^`])+`|"(?:""|[^"])+")\s*\.\s*)?(?:[A-Za-z_][\w$]*|`(?:``|[^`])+`|"(?:""|[^"])+")(?:\s+ON\s+CLUSTER\s+(?:[A-Za-z_][\w$]*|`(?:``|[^`])+`|"(?:""|[^"])+"|'(?:''|[^'])+'))?\s*$/i;
|
const CLICKHOUSE_ALTER_TABLE_HEADER = /^ALTER\s+TABLE\s+(?:(?:[A-Za-z_][\w$]*|`(?:``|[^`])+`|"(?:""|[^"])+")\s*\.\s*)?(?:[A-Za-z_][\w$]*|`(?:``|[^`])+`|"(?:""|[^"])+")(?:\s+ON\s+CLUSTER\s+(?:[A-Za-z_][\w$]*|`(?:``|[^`])+`|"(?:""|[^"])+"|'(?:''|[^'])+'))?\s*$/i;
|
||||||
const SET_OPERATION_KEYWORDS = new Set(["UNION", "INTERSECT", "EXCEPT", "MINUS"]);
|
const SET_OPERATION_KEYWORDS = new Set(["UNION", "INTERSECT", "EXCEPT", "MINUS"]);
|
||||||
const SET_OPERATION_MODIFIER_KEYWORDS = new Set(["ALL", "DISTINCT"]);
|
const SET_OPERATION_MODIFIER_KEYWORDS = new Set(["ALL", "DISTINCT"]);
|
||||||
const ORACLE_LIKE_PL_SQL_DATABASES: ReadonlySet<DatabaseType> = new Set(["oracle", "dameng", "gaussdb", "yashandb", "oscar", "oceanbase-oracle"]);
|
const ORACLE_LIKE_PL_SQL_DATABASES: ReadonlySet<DatabaseType> = new Set(["oracle", "dameng", "gaussdb", "yashandb", "oscar", "oceanbase-oracle", "xugu"]);
|
||||||
const MYSQL_ROUTINE_BLOCK_DATABASES: ReadonlySet<DatabaseType> = new Set(["mysql", "doris", "starrocks", "manticoresearch", "goldendb"]);
|
const MYSQL_ROUTINE_BLOCK_DATABASES: ReadonlySet<DatabaseType> = new Set(["mysql", "doris", "starrocks", "manticoresearch", "goldendb"]);
|
||||||
const MYSQL_CREATE_TABLE_OPTION_DATABASES: ReadonlySet<DatabaseType> = new Set(["mysql", "doris", "starrocks", "manticoresearch", "goldendb", "gbase"]);
|
const MYSQL_CREATE_TABLE_OPTION_DATABASES: ReadonlySet<DatabaseType> = new Set(["mysql", "doris", "starrocks", "manticoresearch", "goldendb", "gbase"]);
|
||||||
const MYSQL_ROUTINE_OBJECT_TYPES = new Set(["PROCEDURE", "FUNCTION", "TRIGGER", "EVENT"]);
|
const MYSQL_ROUTINE_OBJECT_TYPES = new Set(["PROCEDURE", "FUNCTION", "TRIGGER", "EVENT"]);
|
||||||
const MYSQL_NON_ROUTINE_CREATE_TYPES = new Set(["DATABASE", "INDEX", "LOGFILE", "ROLE", "SCHEMA", "SERVER", "SPATIAL", "TABLE", "TEMPORARY", "UNIQUE", "USER", "VIEW"]);
|
const MYSQL_NON_ROUTINE_CREATE_TYPES = new Set(["DATABASE", "INDEX", "LOGFILE", "ROLE", "SCHEMA", "SERVER", "SPATIAL", "TABLE", "TEMPORARY", "UNIQUE", "USER", "VIEW"]);
|
||||||
const MYSQL_CONTROL_BLOCK_SUFFIXES = new Set(["IF", "LOOP", "CASE", "REPEAT", "WHILE"]);
|
const MYSQL_CONTROL_BLOCK_SUFFIXES = new Set(["IF", "LOOP", "CASE", "REPEAT", "WHILE"]);
|
||||||
const ORACLE_PL_SQL_BLOCK_STARTERS = new Set(["DECLARE", "BEGIN"]);
|
const ORACLE_PL_SQL_BLOCK_STARTERS = new Set(["DECLARE", "BEGIN"]);
|
||||||
const ORACLE_PL_SQL_CREATE_OBJECT_TYPES = new Set(["FUNCTION", "PROCEDURE", "TRIGGER", "PACKAGE", "PACKAGE BODY", "TYPE", "TYPE BODY"]);
|
// Plain CREATE TYPE ... AS OBJECT (...); ends with ");" and is not a PL/SQL block.
|
||||||
|
// Only PACKAGE (spec), PACKAGE/TYPE BODY, and routine/trigger objects are PL/SQL blocks.
|
||||||
|
const ORACLE_PL_SQL_CREATE_OBJECT_TYPES = new Set(["FUNCTION", "PROCEDURE", "TRIGGER", "PACKAGE"]);
|
||||||
const ORACLE_PL_SQL_TERMINATORS = new Set(["IF", "LOOP", "CASE"]);
|
const ORACLE_PL_SQL_TERMINATORS = new Set(["IF", "LOOP", "CASE"]);
|
||||||
const SAP_HANA_SCRIPT_BLOCK_TERMINATORS = new Set(["IF", "FOR", "WHILE"]);
|
const SAP_HANA_SCRIPT_BLOCK_TERMINATORS = new Set(["IF", "FOR", "WHILE"]);
|
||||||
|
|
||||||
@@ -1614,15 +1616,32 @@ function startsWithOraclePlSqlBlock(sql: string): boolean {
|
|||||||
if (ORACLE_PL_SQL_BLOCK_STARTERS.has(first)) return first !== "BEGIN" || words[1] !== "TRANSACTION";
|
if (ORACLE_PL_SQL_BLOCK_STARTERS.has(first)) return first !== "BEGIN" || words[1] !== "TRANSACTION";
|
||||||
if (first !== "CREATE") return false;
|
if (first !== "CREATE") return false;
|
||||||
|
|
||||||
let index = 1;
|
const index = skipOraclePlSqlCreateModifiers(words, 1);
|
||||||
while (["OR", "REPLACE", "EDITIONABLE", "NONEDITIONABLE"].includes(words[index] ?? "")) {
|
// PACKAGE BODY / TYPE BODY are programmable blocks with an outer END.
|
||||||
index += 1;
|
|
||||||
}
|
|
||||||
if (words[index] === "PACKAGE" && words[index + 1] === "BODY") return true;
|
if (words[index] === "PACKAGE" && words[index + 1] === "BODY") return true;
|
||||||
if (words[index] === "TYPE" && words[index + 1] === "BODY") return true;
|
if (words[index] === "TYPE" && words[index + 1] === "BODY") return true;
|
||||||
|
// Plain CREATE TYPE ... AS OBJECT (...); is ordinary SQL terminated by ';'.
|
||||||
|
if (words[index] === "TYPE") return false;
|
||||||
return ORACLE_PL_SQL_CREATE_OBJECT_TYPES.has(words[index] ?? "");
|
return ORACLE_PL_SQL_CREATE_OBJECT_TYPES.has(words[index] ?? "");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Skip OR REPLACE / FORCE / NOFORCE / EDITIONABLE modifiers after CREATE. */
|
||||||
|
function skipOraclePlSqlCreateModifiers(words: readonly string[], startIndex: number): number {
|
||||||
|
let index = startIndex;
|
||||||
|
while (index < words.length) {
|
||||||
|
if (words[index] === "OR" && words[index + 1] === "REPLACE") {
|
||||||
|
index += 2;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (["FORCE", "NOFORCE", "EDITIONABLE", "NONEDITIONABLE"].includes(words[index] ?? "")) {
|
||||||
|
index += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
|
||||||
function startsWithSapHanaScriptBlock(sql: string): boolean {
|
function startsWithSapHanaScriptBlock(sql: string): boolean {
|
||||||
return oraclePlSqlWords(sql)[0] === "DO";
|
return oraclePlSqlWords(sql)[0] === "DO";
|
||||||
}
|
}
|
||||||
@@ -1663,7 +1682,11 @@ function oraclePlSqlBlockIsComplete(sql: string): boolean {
|
|||||||
const tokens = oraclePlSqlTokens(sql);
|
const tokens = oraclePlSqlTokens(sql);
|
||||||
if (!startsWithOraclePlSqlBlock(sql)) return false;
|
if (!startsWithOraclePlSqlBlock(sql)) return false;
|
||||||
|
|
||||||
const stack: string[] = [];
|
// Package/type specifications have no BEGIN — only declarations closed by
|
||||||
|
// END [name];. Bodies also own an outer END beyond nested routine END pairs.
|
||||||
|
const objectKind = oraclePlSqlCreateObjectKind(sql);
|
||||||
|
const stack: string[] = objectKind === "body" ? ["OBJECT_BODY"] : objectKind === "spec" ? ["OBJECT_SPEC"] : [];
|
||||||
|
let sawBegin = false;
|
||||||
for (let index = 0; index < tokens.length; index += 1) {
|
for (let index = 0; index < tokens.length; index += 1) {
|
||||||
const token = tokens[index];
|
const token = tokens[index];
|
||||||
if (token.kind !== "word") continue;
|
if (token.kind !== "word") continue;
|
||||||
@@ -1676,6 +1699,7 @@ function oraclePlSqlBlockIsComplete(sql: string): boolean {
|
|||||||
if (tokens[index - 1]?.kind === "word" && tokens[index - 1]?.value === "TRANSACTION") continue;
|
if (tokens[index - 1]?.kind === "word" && tokens[index - 1]?.value === "TRANSACTION") continue;
|
||||||
const previous = previousWordToken(tokens, index);
|
const previous = previousWordToken(tokens, index);
|
||||||
if (previous === "END") continue;
|
if (previous === "END") continue;
|
||||||
|
sawBegin = true;
|
||||||
if (stack[stack.length - 1] === "DECLARATION") stack[stack.length - 1] = "BLOCK";
|
if (stack[stack.length - 1] === "DECLARATION") stack[stack.length - 1] = "BLOCK";
|
||||||
else stack.push("BLOCK");
|
else stack.push("BLOCK");
|
||||||
continue;
|
continue;
|
||||||
@@ -1690,19 +1714,53 @@ function oraclePlSqlBlockIsComplete(sql: string): boolean {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (token.value === "CASE") {
|
if (token.value === "CASE") {
|
||||||
|
// Both CASE statements and CASE expressions own an END. The CASE token
|
||||||
|
// following END CASE is ignored below, so it cannot start a new scope.
|
||||||
if (previousWordToken(tokens, index) !== "END") stack.push("CASE");
|
if (previousWordToken(tokens, index) !== "END") stack.push("CASE");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (token.value === "END") {
|
if (token.value === "END") {
|
||||||
const next = nextWordToken(tokens, index);
|
|
||||||
const target = ORACLE_PL_SQL_TERMINATORS.has(next ?? "") ? next : "BLOCK";
|
|
||||||
const top = stack[stack.length - 1];
|
const top = stack[stack.length - 1];
|
||||||
|
// CASE expressions close as END; while CASE statements close as END CASE;.
|
||||||
|
// In either form, this END belongs to CASE rather than the surrounding block.
|
||||||
|
if (top === "CASE") {
|
||||||
|
stack.pop();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const next = nextWordToken(tokens, index);
|
||||||
|
const target = ORACLE_PL_SQL_TERMINATORS.has(next ?? "") ? next : top === "OBJECT_BODY" || top === "OBJECT_SPEC" ? top : "BLOCK";
|
||||||
if (top === target || (target === "BLOCK" && top === "BLOCK")) stack.pop();
|
if (top === target || (target === "BLOCK" && top === "BLOCK")) stack.pop();
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return stack.length === 0 && tokens[tokens.length - 1]?.kind === "semicolon";
|
const endsWithSemicolon = tokens[tokens.length - 1]?.kind === "semicolon";
|
||||||
|
if (objectKind === "spec") {
|
||||||
|
// Specs complete on outer END [name]; without requiring a BEGIN block.
|
||||||
|
return stack.length === 0 && endsWithSemicolon;
|
||||||
|
}
|
||||||
|
return sawBegin && stack.length === 0 && endsWithSemicolon;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Classify CREATE programmable objects:
|
||||||
|
* - body: PACKAGE BODY / TYPE BODY (outer END beyond nested routines)
|
||||||
|
* - spec: PACKAGE specification only (declarations + END, no BEGIN)
|
||||||
|
* - null: ordinary SQL / other objects (including plain CREATE TYPE ... AS OBJECT)
|
||||||
|
*/
|
||||||
|
function oraclePlSqlCreateObjectKind(sql: string): "body" | "spec" | null {
|
||||||
|
const words = oraclePlSqlWords(sql);
|
||||||
|
if (words[0] !== "CREATE") return null;
|
||||||
|
|
||||||
|
const index = skipOraclePlSqlCreateModifiers(words, 1);
|
||||||
|
if ((words[index] === "PACKAGE" || words[index] === "TYPE") && words[index + 1] === "BODY") {
|
||||||
|
return "body";
|
||||||
|
}
|
||||||
|
// Only PACKAGE specs lack BEGIN; plain TYPE objects end with ");".
|
||||||
|
if (words[index] === "PACKAGE") {
|
||||||
|
return "spec";
|
||||||
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function oraclePlSqlWords(sql: string): string[] {
|
function oraclePlSqlWords(sql: string): string[] {
|
||||||
|
|||||||
+306
-25
@@ -191,6 +191,7 @@ impl SqlDialectProfile {
|
|||||||
| DatabaseType::Yashandb
|
| DatabaseType::Yashandb
|
||||||
| DatabaseType::Oscar
|
| DatabaseType::Oscar
|
||||||
| DatabaseType::OceanbaseOracle
|
| DatabaseType::OceanbaseOracle
|
||||||
|
| DatabaseType::Xugu
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2084,51 +2085,99 @@ impl OraclePlSqlBlock {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut depth = 0usize;
|
// Package/type specifications have declarations and an outer END with
|
||||||
|
// no BEGIN. Bodies also own an outer END beyond nested routine END
|
||||||
|
// pairs so an inner END cannot finish the object.
|
||||||
|
let object_kind = self.create_object_kind();
|
||||||
|
let mut scopes = match object_kind {
|
||||||
|
Some(OraclePlSqlCreateObjectKind::Body | OraclePlSqlCreateObjectKind::Spec) => {
|
||||||
|
vec![OraclePlSqlScope::Object]
|
||||||
|
}
|
||||||
|
None => Vec::new(),
|
||||||
|
};
|
||||||
let mut saw_begin = false;
|
let mut saw_begin = false;
|
||||||
let mut complete = false;
|
let mut complete = false;
|
||||||
let mut pending_end: Option<bool> = None;
|
|
||||||
|
|
||||||
for token in &self.tokens {
|
for (index, token) in self.tokens.iter().enumerate() {
|
||||||
if token.is_semicolon() {
|
if token.is_semicolon() {
|
||||||
if let Some(is_block_end) = pending_end.take() {
|
|
||||||
if is_block_end && depth > 0 {
|
|
||||||
depth -= 1;
|
|
||||||
complete = depth == 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(is_block_end) = pending_end.as_mut() {
|
|
||||||
if token.is_any_word(&["IF", "LOOP", "CASE"]) {
|
|
||||||
*is_block_end = false;
|
|
||||||
}
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if token.is_word("BEGIN") {
|
if token.is_word("BEGIN") {
|
||||||
depth += 1;
|
scopes.push(OraclePlSqlScope::Block);
|
||||||
saw_begin = true;
|
saw_begin = true;
|
||||||
complete = false;
|
complete = false;
|
||||||
|
} else if token.is_word("CASE") {
|
||||||
|
// Both CASE expressions and CASE statements own an END.
|
||||||
|
// The CASE token that follows END CASE is a suffix, not a scope start.
|
||||||
|
if previous_word_token(&self.tokens, index) != Some("END") {
|
||||||
|
scopes.push(OraclePlSqlScope::Case);
|
||||||
|
}
|
||||||
} else if token.is_word("END") {
|
} else if token.is_word("END") {
|
||||||
pending_end = Some(true);
|
let next = self.tokens.get(index + 1).and_then(OraclePlSqlToken::as_word);
|
||||||
|
if matches!(next, Some("IF" | "LOOP")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if matches!(next, Some("CASE")) && !matches!(scopes.last(), Some(OraclePlSqlScope::Case)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if scopes.pop().is_some() {
|
||||||
|
complete = scopes.is_empty();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
saw_begin && complete
|
match object_kind {
|
||||||
|
// Specs complete on outer END [name]; without requiring BEGIN.
|
||||||
|
Some(OraclePlSqlCreateObjectKind::Spec) => complete,
|
||||||
|
_ => saw_begin && complete,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn starts_create_plsql_object(tokens: &[OraclePlSqlToken]) -> bool {
|
fn starts_create_plsql_object(tokens: &[OraclePlSqlToken]) -> bool {
|
||||||
let tokens = Self::skip_or_replace(tokens);
|
let tokens = Self::skip_create_modifiers(tokens);
|
||||||
tokens.first().is_some_and(|token| token.is_any_word(&["FUNCTION", "PROCEDURE", "TRIGGER", "PACKAGE", "TYPE"]))
|
match tokens {
|
||||||
|
// PACKAGE BODY / TYPE BODY are programmable blocks with an outer END.
|
||||||
|
[object, body, ..] if object.is_any_word(&["PACKAGE", "TYPE"]) && body.is_word("BODY") => true,
|
||||||
|
// Plain CREATE TYPE ... AS OBJECT (...); ends with ");" — not a PL/SQL block.
|
||||||
|
[object, ..] if object.is_word("TYPE") => false,
|
||||||
|
[object, ..] if object.is_any_word(&["FUNCTION", "PROCEDURE", "TRIGGER", "PACKAGE"]) => true,
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn skip_or_replace(tokens: &[OraclePlSqlToken]) -> &[OraclePlSqlToken] {
|
fn create_object_kind(&self) -> Option<OraclePlSqlCreateObjectKind> {
|
||||||
match tokens {
|
if self.tokens.first().is_none_or(|token| !token.is_word("CREATE")) {
|
||||||
[or, replace, rest @ ..] if or.is_word("OR") && replace.is_word("REPLACE") => rest,
|
return None;
|
||||||
_ => tokens,
|
|
||||||
}
|
}
|
||||||
|
let tokens = Self::skip_create_modifiers(&self.tokens[1..]);
|
||||||
|
match tokens {
|
||||||
|
[object, body, ..] if object.is_any_word(&["PACKAGE", "TYPE"]) && body.is_word("BODY") => {
|
||||||
|
Some(OraclePlSqlCreateObjectKind::Body)
|
||||||
|
}
|
||||||
|
// Only PACKAGE specs lack BEGIN; plain TYPE objects are ordinary SQL.
|
||||||
|
[object, ..] if object.is_word("PACKAGE") => Some(OraclePlSqlCreateObjectKind::Spec),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Skip OR REPLACE / FORCE / NOFORCE / EDITIONABLE modifiers after CREATE.
|
||||||
|
fn skip_create_modifiers(tokens: &[OraclePlSqlToken]) -> &[OraclePlSqlToken] {
|
||||||
|
let mut rest = tokens;
|
||||||
|
loop {
|
||||||
|
match rest {
|
||||||
|
[or, replace, tail @ ..] if or.is_word("OR") && replace.is_word("REPLACE") => {
|
||||||
|
rest = tail;
|
||||||
|
}
|
||||||
|
[modifier, tail @ ..]
|
||||||
|
if modifier.is_any_word(&["FORCE", "NOFORCE", "EDITIONABLE", "NONEDITIONABLE"]) =>
|
||||||
|
{
|
||||||
|
rest = tail;
|
||||||
|
}
|
||||||
|
_ => break,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rest
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_transaction_begin_tail(token: &OraclePlSqlToken) -> bool {
|
fn is_transaction_begin_tail(token: &OraclePlSqlToken) -> bool {
|
||||||
@@ -2136,6 +2185,19 @@ impl OraclePlSqlBlock {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
enum OraclePlSqlCreateObjectKind {
|
||||||
|
Spec,
|
||||||
|
Body,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
enum OraclePlSqlScope {
|
||||||
|
Object,
|
||||||
|
Block,
|
||||||
|
Case,
|
||||||
|
}
|
||||||
|
|
||||||
impl OraclePlSqlToken {
|
impl OraclePlSqlToken {
|
||||||
fn word(value: String) -> Self {
|
fn word(value: String) -> Self {
|
||||||
Self::Word(value)
|
Self::Word(value)
|
||||||
@@ -3083,6 +3145,7 @@ END;";
|
|||||||
assert_eq!(split_sql_statements_for_database(sql, DatabaseType::Oracle), vec![sql.to_string()]);
|
assert_eq!(split_sql_statements_for_database(sql, DatabaseType::Oracle), vec![sql.to_string()]);
|
||||||
assert_eq!(split_sql_statements_for_database(sql, DatabaseType::Dameng), vec![sql.to_string()]);
|
assert_eq!(split_sql_statements_for_database(sql, DatabaseType::Dameng), vec![sql.to_string()]);
|
||||||
assert_eq!(split_sql_statements_for_database(sql, DatabaseType::Gaussdb), vec![sql.to_string()]);
|
assert_eq!(split_sql_statements_for_database(sql, DatabaseType::Gaussdb), vec![sql.to_string()]);
|
||||||
|
assert_eq!(split_sql_statements_for_database(sql, DatabaseType::Xugu), vec![sql.to_string()]);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -3229,6 +3292,10 @@ SELECT 1;";
|
|||||||
split_sql_statements_for_database(sql, DatabaseType::Oracle),
|
split_sql_statements_for_database(sql, DatabaseType::Oracle),
|
||||||
vec!["CREATE OR REPLACE FUNCTION number_tochar(nums VARCHAR(20))\nRETURN VARCHAR(20)\nAS\n res VARCHAR(20);\nBEGIN\n RETURN '一';\nEND;", "SELECT 1"]
|
vec!["CREATE OR REPLACE FUNCTION number_tochar(nums VARCHAR(20))\nRETURN VARCHAR(20)\nAS\n res VARCHAR(20);\nBEGIN\n RETURN '一';\nEND;", "SELECT 1"]
|
||||||
);
|
);
|
||||||
|
assert_eq!(
|
||||||
|
split_sql_statements_for_database(sql, DatabaseType::Xugu),
|
||||||
|
vec!["CREATE OR REPLACE FUNCTION number_tochar(nums VARCHAR(20))\nRETURN VARCHAR(20)\nAS\n res VARCHAR(20);\nBEGIN\n RETURN '一';\nEND;", "SELECT 1"]
|
||||||
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
split_sql_statements_for_database(sql, DatabaseType::Dameng),
|
split_sql_statements_for_database(sql, DatabaseType::Dameng),
|
||||||
vec!["CREATE OR REPLACE FUNCTION number_tochar(nums VARCHAR(20))\nRETURN VARCHAR(20)\nAS\n res VARCHAR(20);\nBEGIN\n RETURN '一';\nEND;", "SELECT 1"]
|
vec!["CREATE OR REPLACE FUNCTION number_tochar(nums VARCHAR(20))\nRETURN VARCHAR(20)\nAS\n res VARCHAR(20);\nBEGIN\n RETURN '一';\nEND;", "SELECT 1"]
|
||||||
@@ -3254,6 +3321,13 @@ SELECT 1;";
|
|||||||
"SELECT 1"
|
"SELECT 1"
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
assert_eq!(
|
||||||
|
split_sql_statements_for_database(sql, DatabaseType::Xugu),
|
||||||
|
vec![
|
||||||
|
"CREATE OR REPLACE PROCEDURE update_salary(p_id NUMBER, p_amount NUMBER)\nAS\nBEGIN\n UPDATE employees SET salary = salary + p_amount WHERE id = p_id;\n COMMIT;\nEND;",
|
||||||
|
"SELECT 1"
|
||||||
|
]
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -3275,6 +3349,13 @@ SELECT 1;";
|
|||||||
"SELECT 1"
|
"SELECT 1"
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
assert_eq!(
|
||||||
|
split_sql_statements_for_database(sql, DatabaseType::Xugu),
|
||||||
|
vec![
|
||||||
|
"CREATE TRIGGER trg_audit\nBEFORE INSERT ON employees\nFOR EACH ROW\nBEGIN\n INSERT INTO audit_log VALUES (:NEW.id, 'INSERT');\nEND;",
|
||||||
|
"SELECT 1"
|
||||||
|
]
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -3317,6 +3398,206 @@ SELECT 1;";
|
|||||||
"SELECT 1"
|
"SELECT 1"
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
assert_eq!(
|
||||||
|
split_sql_statements_for_database(sql, DatabaseType::Xugu),
|
||||||
|
vec![
|
||||||
|
"CREATE OR REPLACE PACKAGE pkg_utils AS\n FUNCTION get_version RETURN VARCHAR2;\n PROCEDURE log_message(msg VARCHAR2);\nEND pkg_utils;",
|
||||||
|
"SELECT 1"
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn xugu_split_keeps_create_package_body_together() {
|
||||||
|
let sql = "\
|
||||||
|
CREATE OR REPLACE PACKAGE BODY dbx_pkg AS
|
||||||
|
PROCEDURE ping AS
|
||||||
|
BEGIN
|
||||||
|
NULL;
|
||||||
|
END ping;
|
||||||
|
END dbx_pkg;
|
||||||
|
/
|
||||||
|
SELECT 1;";
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
split_sql_statements_for_database(sql, DatabaseType::Xugu),
|
||||||
|
vec![
|
||||||
|
"CREATE OR REPLACE PACKAGE BODY dbx_pkg AS\n PROCEDURE ping AS\n BEGIN\n NULL;\n END ping;\nEND dbx_pkg;",
|
||||||
|
"SELECT 1"
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn xugu_split_keeps_routines_without_or_replace_together() {
|
||||||
|
let cases = [
|
||||||
|
"CREATE PROCEDURE dbx_proc_without_replace AS BEGIN NULL; END;",
|
||||||
|
"CREATE FUNCTION dbx_func_without_replace RETURN INTEGER AS BEGIN RETURN 1; END;",
|
||||||
|
"CREATE TRIGGER dbx_trigger_without_replace BEFORE INSERT ON dbx_events FOR EACH ROW BEGIN NULL; END;",
|
||||||
|
];
|
||||||
|
|
||||||
|
for statement in cases {
|
||||||
|
assert_eq!(
|
||||||
|
split_sql_statements_for_database(&format!("{statement}\nSELECT 1;"), DatabaseType::Xugu),
|
||||||
|
vec![statement.to_owned(), "SELECT 1".to_owned()],
|
||||||
|
"failed for {statement}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn xugu_split_keeps_force_package_body_together() {
|
||||||
|
let sql = "\
|
||||||
|
CREATE OR REPLACE FORCE PACKAGE BODY dbx_pkg AS
|
||||||
|
PROCEDURE ping AS
|
||||||
|
BEGIN
|
||||||
|
NULL;
|
||||||
|
END ping;
|
||||||
|
END dbx_pkg;
|
||||||
|
SELECT 1;";
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
split_sql_statements_for_database(sql, DatabaseType::Xugu),
|
||||||
|
vec![
|
||||||
|
"CREATE OR REPLACE FORCE PACKAGE BODY dbx_pkg AS\n PROCEDURE ping AS\n BEGIN\n NULL;\n END ping;\nEND dbx_pkg;",
|
||||||
|
"SELECT 1"
|
||||||
|
]
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
split_sql_statements_for_database(
|
||||||
|
"CREATE OR REPLACE NOFORCE PACKAGE BODY dbx_pkg AS\n PROCEDURE ping AS\n BEGIN\n NULL;\n END ping;\nEND dbx_pkg;\nSELECT 1;",
|
||||||
|
DatabaseType::Xugu
|
||||||
|
),
|
||||||
|
vec![
|
||||||
|
"CREATE OR REPLACE NOFORCE PACKAGE BODY dbx_pkg AS\n PROCEDURE ping AS\n BEGIN\n NULL;\n END ping;\nEND dbx_pkg;",
|
||||||
|
"SELECT 1"
|
||||||
|
]
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
split_sql_statements_for_database(
|
||||||
|
"CREATE PACKAGE BODY dbx_pkg_without_replace AS\n PROCEDURE ping AS\n BEGIN\n NULL;\n END ping;\nEND dbx_pkg_without_replace;\nSELECT 1;",
|
||||||
|
DatabaseType::Xugu
|
||||||
|
),
|
||||||
|
vec![
|
||||||
|
"CREATE PACKAGE BODY dbx_pkg_without_replace AS\n PROCEDURE ping AS\n BEGIN\n NULL;\n END ping;\nEND dbx_pkg_without_replace;",
|
||||||
|
"SELECT 1"
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn xugu_split_package_spec_without_slash_does_not_consume_following_sql() {
|
||||||
|
let sql = "\
|
||||||
|
CREATE OR REPLACE PACKAGE pkg_utils AS
|
||||||
|
FUNCTION get_version RETURN VARCHAR2;
|
||||||
|
PROCEDURE log_message(msg VARCHAR2);
|
||||||
|
END pkg_utils;
|
||||||
|
SELECT 1;";
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
split_sql_statements_for_database(sql, DatabaseType::Xugu),
|
||||||
|
vec![
|
||||||
|
"CREATE OR REPLACE PACKAGE pkg_utils AS\n FUNCTION get_version RETURN VARCHAR2;\n PROCEDURE log_message(msg VARCHAR2);\nEND pkg_utils;",
|
||||||
|
"SELECT 1"
|
||||||
|
]
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
split_sql_statements_for_database(
|
||||||
|
"CREATE OR REPLACE PACKAGE pkg_utils AS\n FUNCTION get_version RETURN VARCHAR2;\n PROCEDURE log_message(msg VARCHAR2);\nEND pkg_utils;\n/\nSELECT 1;",
|
||||||
|
DatabaseType::Xugu
|
||||||
|
),
|
||||||
|
vec![
|
||||||
|
"CREATE OR REPLACE PACKAGE pkg_utils AS\n FUNCTION get_version RETURN VARCHAR2;\n PROCEDURE log_message(msg VARCHAR2);\nEND pkg_utils;",
|
||||||
|
"SELECT 1"
|
||||||
|
]
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
split_sql_statements_for_database(
|
||||||
|
"CREATE OR REPLACE FORCE PACKAGE pkg_utils AS\n PROCEDURE ping;\nEND pkg_utils;\nSELECT 1;",
|
||||||
|
DatabaseType::Xugu
|
||||||
|
),
|
||||||
|
vec!["CREATE OR REPLACE FORCE PACKAGE pkg_utils AS\n PROCEDURE ping;\nEND pkg_utils;", "SELECT 1"]
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
split_sql_statements_for_database(
|
||||||
|
"CREATE PACKAGE pkg_utils_without_replace AS\n PROCEDURE ping;\nEND pkg_utils_without_replace;\nSELECT 1;",
|
||||||
|
DatabaseType::Xugu
|
||||||
|
),
|
||||||
|
vec![
|
||||||
|
"CREATE PACKAGE pkg_utils_without_replace AS\n PROCEDURE ping;\nEND pkg_utils_without_replace;",
|
||||||
|
"SELECT 1"
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn xugu_split_keeps_create_type_body_together() {
|
||||||
|
let sql = "\
|
||||||
|
CREATE OR REPLACE TYPE BODY obj_t AS
|
||||||
|
MEMBER PROCEDURE ping IS
|
||||||
|
BEGIN
|
||||||
|
NULL;
|
||||||
|
END;
|
||||||
|
END;
|
||||||
|
SELECT 1;";
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
split_sql_statements_for_database(sql, DatabaseType::Xugu),
|
||||||
|
vec![
|
||||||
|
"CREATE OR REPLACE TYPE BODY obj_t AS\n MEMBER PROCEDURE ping IS\n BEGIN\n NULL;\n END;\nEND;",
|
||||||
|
"SELECT 1"
|
||||||
|
]
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
split_sql_statements_for_database(
|
||||||
|
"CREATE TYPE BODY obj_t_without_replace AS\n MEMBER PROCEDURE ping IS\n BEGIN\n NULL;\n END;\nEND;\nSELECT 1;",
|
||||||
|
DatabaseType::Xugu
|
||||||
|
),
|
||||||
|
vec![
|
||||||
|
"CREATE TYPE BODY obj_t_without_replace AS\n MEMBER PROCEDURE ping IS\n BEGIN\n NULL;\n END;\nEND;",
|
||||||
|
"SELECT 1"
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn xugu_split_plain_create_type_object_on_semicolon() {
|
||||||
|
// Plain CREATE TYPE ends with ");" and must not wait for a nonexistent outer END.
|
||||||
|
let sql = "CREATE OR REPLACE TYPE address_t AS OBJECT (id INT);\nSELECT 1;";
|
||||||
|
assert_eq!(
|
||||||
|
split_sql_statements_for_database(sql, DatabaseType::Xugu),
|
||||||
|
vec!["CREATE OR REPLACE TYPE address_t AS OBJECT (id INT)", "SELECT 1"]
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
split_sql_statements_for_database(
|
||||||
|
"CREATE TYPE address_t_without_replace AS OBJECT (id INT);\nSELECT 1;",
|
||||||
|
DatabaseType::Xugu
|
||||||
|
),
|
||||||
|
vec!["CREATE TYPE address_t_without_replace AS OBJECT (id INT)", "SELECT 1"]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn oracle_like_split_keeps_case_expressions_inside_routines() {
|
||||||
|
let function = "CREATE OR REPLACE FUNCTION dbx_case_expr RETURN NUMBER AS\nBEGIN\n RETURN CASE WHEN 1 = 1 THEN CASE WHEN 2 = 2 THEN 1 ELSE 2 END ELSE 0 END;\nEND;\nSELECT 1;";
|
||||||
|
let procedure = "CREATE OR REPLACE PROCEDURE dbx_case_statement AS\nBEGIN\n CASE WHEN 1 = 1 THEN NULL; ELSE NULL; END CASE;\nEND;\nSELECT 1;";
|
||||||
|
|
||||||
|
for database in [DatabaseType::Xugu, DatabaseType::Oracle] {
|
||||||
|
assert_eq!(
|
||||||
|
split_sql_statements_for_database(function, database),
|
||||||
|
vec![
|
||||||
|
"CREATE OR REPLACE FUNCTION dbx_case_expr RETURN NUMBER AS\nBEGIN\n RETURN CASE WHEN 1 = 1 THEN CASE WHEN 2 = 2 THEN 1 ELSE 2 END ELSE 0 END;\nEND;",
|
||||||
|
"SELECT 1"
|
||||||
|
]
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
split_sql_statements_for_database(procedure, database),
|
||||||
|
vec![
|
||||||
|
"CREATE OR REPLACE PROCEDURE dbx_case_statement AS\nBEGIN\n CASE WHEN 1 = 1 THEN NULL; ELSE NULL; END CASE;\nEND;",
|
||||||
|
"SELECT 1"
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
Reference in new issue
Block a user