Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 1 addition & 6 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ require (
github.com/alibabacloud-go/tea v1.1.19
github.com/alibabacloud-go/tea-utils v1.4.3
github.com/baidubce/bce-sdk-go v0.9.151
github.com/beltran/gohive v1.6.0
github.com/bwmarrin/snowflake v0.3.0
github.com/clbanning/mxj/v2 v2.5.6 // indirect
github.com/cznic/mathutil v0.0.0-20181122101859-297441e03548
Expand Down Expand Up @@ -92,10 +91,7 @@ require (
github.com/alibabacloud-go/openapi-util v0.1.0 // indirect
github.com/alibabacloud-go/tea-xml v1.1.2 // indirect
github.com/antlr4-go/antlr/v4 v4.13.0 // indirect
github.com/apache/thrift v0.19.0 // indirect
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
github.com/beltran/gosasl v0.0.0-20231124144235-92b2e4f10bb6 // indirect
github.com/beltran/gssapi v0.0.0-20200324152954-d86554db4bab // indirect
github.com/cloudflare/circl v1.3.3 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
github.com/cyphar/filepath-securejoin v0.2.4 // indirect
Expand All @@ -116,7 +112,6 @@ require (
github.com/go-openapi/spec v0.20.4 // indirect
github.com/go-openapi/strfmt v0.21.7 // indirect
github.com/go-openapi/swag v0.19.15 // indirect
github.com/go-zookeeper/zk v1.0.3 // indirect
github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe // indirect
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b // indirect
Expand Down Expand Up @@ -191,7 +186,7 @@ replace (
cloud.google.com/go/compute/metadata => cloud.google.com/go/compute/metadata v0.1.0
github.com/labstack/echo/v4 => github.com/labstack/echo/v4 v4.6.1
github.com/pingcap/log => github.com/pingcap/log v0.0.0-20191012051959-b742a5d432e9
github.com/pingcap/parser => github.com/sjjian/parser v0.0.0-20260821091929-e152d91621bc
github.com/pingcap/parser => github.com/sjjian/parser v0.0.0-20260825094816-b6aefd90e347
github.com/swaggo/swag => github.com/swaggo/swag v1.6.7
google.golang.org/grpc => google.golang.org/grpc v1.29.0
)
826 changes: 408 additions & 418 deletions go.sum

Large diffs are not rendered by default.

53 changes: 53 additions & 0 deletions sqle/driver/mysql/cte_audit_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package mysql

import (
"context"
"strings"
"testing"

"github.com/stretchr/testify/assert"
)

// unsupportedSQLWarnSnippet is the Chinese body of UnsupportedSyntaxError /
// server unsupportedSQLWarnMessage. AC-005 forbids this when triggered by legal CTE.
const unsupportedSQLWarnSnippet = "语法错误或者解析器不支持,请人工确认SQL正确性"

// TestCTEAuditNoUnsupportedSQLWarn covers AC-005: legal CTE audit must not emit
// unsupportedSQLWarnMessage (UnparsedStmt → checkUnparsedStmt path).
// Empty rule set isolates the parser-unsupported warning from unrelated rule hits.
func TestCTEAuditNoUnsupportedSQLWarn(t *testing.T) {
cases := []struct {
name string
sql string
}{
{
name: "non_recursive_dql_with_optional_cols",
sql: `WITH cte (id) AS (SELECT 1) SELECT * FROM cte;`,
},
{
name: "with_recursive_dql",
sql: `WITH RECURSIVE t AS (SELECT 1 AS n UNION ALL SELECT n + 1 FROM t WHERE n < 3) SELECT * FROM t;`,
},
{
name: "cte_plus_dml_delete",
sql: `WITH cte AS (SELECT id FROM exist_db.exist_tb_1 WHERE id = 1) DELETE FROM exist_db.exist_tb_1 WHERE id IN (SELECT id FROM cte);`,
},
}

for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
runEmptyRuleInspectCase(t, c.name, DefaultMysqlInspectOffline(), c.sql, newTestResult())

i := DefaultMysqlInspectOffline()
i.rules = nil
actual, err := i.Audit(context.TODO(), []string{c.sql})
assert.NoError(t, err)
if !assert.Len(t, actual, 1) {
return
}
msg := actual[0].Message()
assert.False(t, strings.Contains(msg, unsupportedSQLWarnSnippet),
"legal CTE must not trigger unsupportedSQLWarnMessage; got %q", msg)
})
}
}
75 changes: 75 additions & 0 deletions sqle/driver/mysql/cte_non_regression_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package mysql

import (
"context"
"strings"
"testing"

rulepkg "github.com/actiontech/sqle/sqle/driver/mysql/rule"
"github.com/actiontech/sqle/sqle/driver/mysql/rule/ai"
"github.com/actiontech/sqle/sqle/driver/mysql/util"
driverV2 "github.com/actiontech/sqle/sqle/driver/v2"
"github.com/pingcap/parser/ast"
"github.com/stretchr/testify/assert"
)

// TestCTENonRegressionIllegalAndNonCTEWith covers AC-009:
// illegal SQL still fails / warns reasonably; GRANT … WITH GRANT OPTION must not be
// treated as CTE success path and must keep non-"parser unsupported" rule hits.
func TestCTENonRegressionIllegalAndNonCTEWith(t *testing.T) {
t.Run("illegal_sql_not_silent_legal_cte", func(t *testing.T) {
sql := `SELECT FROM WHERE;`

stmts, err := util.ParseSql(sql)
assert.NoError(t, err)
if !assert.Len(t, stmts, 1) {
return
}
_, isUnparsed := stmts[0].(*ast.UnparsedStmt)
assert.True(t, isUnparsed, "illegal SQL must be UnparsedStmt, got %T", stmts[0])
if sel, ok := stmts[0].(*ast.SelectStmt); ok {
assert.Nil(t, sel.With, "illegal SQL must not become SelectStmt with CTE With")
}

i := DefaultMysqlInspectOffline()
i.rules = nil
actual, err := i.Audit(context.TODO(), []string{sql})
assert.NoError(t, err)
if !assert.Len(t, actual, 1) {
return
}
msg := actual[0].Message()
assert.True(t, strings.Contains(msg, unsupportedSQLWarnSnippet),
"illegal SQL must keep reasonable unsupported/parse warn; got %q", msg)

assert.NotEqual(t, driverV2.SQLTypeDQL, (&MysqlDriverImpl{}).assertSQLType(stmts[0]),
"illegal UnparsedStmt must not be classified as CTE dql success path")
})

t.Run("grant_with_grant_option_not_cte", func(t *testing.T) {
sql := `GRANT SELECT ON database.* TO 'user'@'localhost' WITH GRANT OPTION;`

stmts, err := util.ParseSql(sql)
assert.NoError(t, err)
if !assert.Len(t, stmts, 1) {
return
}
grant, ok := stmts[0].(*ast.GrantStmt)
if !assert.True(t, ok, "expect *ast.GrantStmt, got %T", stmts[0]) {
return
}
assert.True(t, grant.WithGrant, "WITH GRANT OPTION must set GrantStmt.WithGrant")
_, isSelect := stmts[0].(*ast.SelectStmt)
assert.False(t, isSelect, "GRANT must not be misparsed as CTE SelectStmt")

sqlType := (&MysqlDriverImpl{}).assertSQLType(stmts[0])
assert.NotEqual(t, driverV2.SQLTypeDQL, sqlType,
"GRANT WITH GRANT OPTION must not follow CTE dql success path; got %s", sqlType)
assert.Equal(t, driverV2.SQLTypeDDL, sqlType)

// Non-"parser unsupported" reasonable alert must remain (SQLE00174).
rule := rulepkg.AIRuleHandlerMap[ai.SQLE00174].Rule
runAIRuleCase(rule, t, "grant_with_option_keeps_sqle00174", sql,
nil, nil, newTestResult().addResult(ai.SQLE00174))
})
}
47 changes: 47 additions & 0 deletions sqle/driver/mysql/cte_sql_type_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package mysql

import (
"testing"

"github.com/actiontech/sqle/sqle/driver/mysql/util"
driverV2 "github.com/actiontech/sqle/sqle/driver/v2"
"github.com/stretchr/testify/assert"
)

// TestCTESQLTypeThreeStates covers AC-006: sql_type follows outer CTE semantics.
// CTE+DML uses DELETE (UPDATE likewise); CTE+INSERT remains Unparsed in current parser and is out of scope.
func TestCTESQLTypeThreeStates(t *testing.T) {
cases := []struct {
name string
sql string
want string
}{
{
name: "cte_plus_select_dql",
sql: `WITH cte (id) AS (SELECT 1) SELECT * FROM cte`,
want: driverV2.SQLTypeDQL,
},
{
name: "cte_plus_delete_dml",
sql: `WITH cte AS (SELECT id FROM exist_tb_1 WHERE id = 1) DELETE FROM exist_tb_1 WHERE id IN (SELECT id FROM cte)`,
want: driverV2.SQLTypeDML,
},
{
name: "real_ddl",
sql: `CREATE TABLE t_ac006 (id INT)`,
want: driverV2.SQLTypeDDL,
},
}

i := &MysqlDriverImpl{}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
stmts, err := util.ParseSql(c.sql)
assert.NoError(t, err)
if !assert.NotEmpty(t, stmts) {
return
}
assert.Equal(t, c.want, i.assertSQLType(stmts[0]))
})
}
}
105 changes: 105 additions & 0 deletions sqle/driver/mysql/util/cte_ast_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package util

import (
"testing"

"github.com/pingcap/parser"
"github.com/pingcap/parser/ast"
"github.com/stretchr/testify/assert"
)

// TestCTEParseASTPositiveSupport covers AC-004: Parse succeeds and AST carries CTE
// definition fields (name / optional columns / AS subquery). Must not only assert
// "not Unparsed".
func TestCTEParseASTPositiveSupport(t *testing.T) {
p := parser.New()

t.Run("non_recursive_dql_with_optional_cols", func(t *testing.T) {
sql := "WITH cte (id) AS (SELECT 1) SELECT * FROM cte"
stmt, err := p.ParseOneStmt(sql, "", "")
assert.NoError(t, err)
assert.NotNil(t, stmt)
_, isUnparsed := stmt.(*ast.UnparsedStmt)
assert.False(t, isUnparsed, "expect parsed CTE SelectStmt, got UnparsedStmt")

sel, ok := stmt.(*ast.SelectStmt)
if !assert.True(t, ok, "expect *ast.SelectStmt, got %T", stmt) {
return
}
if !assert.NotNil(t, sel.With) {
return
}
assert.False(t, sel.With.IsRecursive)
if !assert.Len(t, sel.With.CTEs, 1) {
return
}

cte := sel.With.CTEs[0]
assert.Equal(t, "cte", cte.Name.L)
if assert.Len(t, cte.ColNameList, 1) {
assert.Equal(t, "id", cte.ColNameList[0].L)
}
if assert.NotNil(t, cte.Query, "AS subquery must be present") {
assert.NotNil(t, cte.Query.Query, "AS subquery Query node must be present")
}
})

t.Run("with_recursive_dql", func(t *testing.T) {
sql := `WITH RECURSIVE t AS (
SELECT 1 AS n UNION ALL SELECT n + 1 FROM t WHERE n < 3
) SELECT * FROM t`
stmt, err := p.ParseOneStmt(sql, "", "")
assert.NoError(t, err)
assert.NotNil(t, stmt)
_, isUnparsed := stmt.(*ast.UnparsedStmt)
assert.False(t, isUnparsed, "expect parsed CTE SelectStmt, got UnparsedStmt")

sel, ok := stmt.(*ast.SelectStmt)
if !assert.True(t, ok, "expect *ast.SelectStmt, got %T", stmt) {
return
}
if !assert.NotNil(t, sel.With) {
return
}
assert.True(t, sel.With.IsRecursive)
if !assert.Len(t, sel.With.CTEs, 1) {
return
}

cte := sel.With.CTEs[0]
assert.Equal(t, "t", cte.Name.L)
assert.Empty(t, cte.ColNameList)
if assert.NotNil(t, cte.Query, "AS subquery must be present") {
assert.NotNil(t, cte.Query.Query, "AS subquery Query node must be present")
}
})

t.Run("cte_plus_dml_delete", func(t *testing.T) {
sql := `WITH cte AS (SELECT id FROM exist_tb_1 WHERE id = 1)
DELETE FROM exist_tb_1 WHERE id IN (SELECT id FROM cte)`
stmt, err := p.ParseOneStmt(sql, "", "")
assert.NoError(t, err)
assert.NotNil(t, stmt)
_, isUnparsed := stmt.(*ast.UnparsedStmt)
assert.False(t, isUnparsed, "expect parsed CTE DeleteStmt, got UnparsedStmt")

del, ok := stmt.(*ast.DeleteStmt)
if !assert.True(t, ok, "expect *ast.DeleteStmt, got %T", stmt) {
return
}
if !assert.NotNil(t, del.With) {
return
}
assert.False(t, del.With.IsRecursive)
if !assert.Len(t, del.With.CTEs, 1) {
return
}

cte := del.With.CTEs[0]
assert.Equal(t, "cte", cte.Name.L)
assert.Empty(t, cte.ColNameList)
if assert.NotNil(t, cte.Query, "AS subquery must be present") {
assert.NotNil(t, cte.Query.Query, "AS subquery Query node must be present")
}
})
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading