From 871c7fd5edb315f3d2055dd46dca2b89723e12ae Mon Sep 17 00:00:00 2001 From: wangyue Date: Wed, 31 Jan 2024 16:25:16 +0800 Subject: [PATCH] Feat actiontech/dms-ee/issues/125: plugin interface mysql impl --- sqle/driver/mysql/mysql.go | 4 - sqle/driver/mysql/sql_ops.go | 632 ++++++++++ sqle/driver/mysql/sql_ops_test.go | 1826 +++++++++++++++++++++++++++++ 3 files changed, 2458 insertions(+), 4 deletions(-) create mode 100644 sqle/driver/mysql/sql_ops.go create mode 100644 sqle/driver/mysql/sql_ops_test.go diff --git a/sqle/driver/mysql/mysql.go b/sqle/driver/mysql/mysql.go index a013e6da6a..e7057d1ed8 100644 --- a/sqle/driver/mysql/mysql.go +++ b/sqle/driver/mysql/mysql.go @@ -577,10 +577,6 @@ func (i *MysqlDriverImpl) getPrimaryKey(stmt *ast.CreateTableStmt) (map[string]s return pkColumnsName, hasPk, nil } -func (i *MysqlDriverImpl) GetSQLOp(ctx context.Context, sqls string) ([]*driverV2.SQLObjectOps, error) { - return nil, fmt.Errorf("not implement yet") -} - type PluginProcessor struct{} func (p *PluginProcessor) GetDriverMetas() (*driverV2.DriverMetas, error) { diff --git a/sqle/driver/mysql/sql_ops.go b/sqle/driver/mysql/sql_ops.go new file mode 100644 index 0000000000..7b0fab004e --- /dev/null +++ b/sqle/driver/mysql/sql_ops.go @@ -0,0 +1,632 @@ +package mysql + +import ( + "context" + "fmt" + "strings" + + "github.com/actiontech/sqle/sqle/driver/mysql/util" + driverV2 "github.com/actiontech/sqle/sqle/driver/v2" + "github.com/pingcap/parser" + "github.com/pingcap/parser/ast" +) + +// GetSQLOp 获取sql中涉及的对象操作,sql可以是单条语句,也可以是多条语句 +// GetSQLOp 目前实现的对象的最小粒度只到表级别,考虑识别列对象的成本较高,暂时没有识别列对象 +func (i *MysqlDriverImpl) GetSQLOp(ctx context.Context, sqls string) ([]*driverV2.SQLObjectOps, error) { + p := parser.New() + stmts, _, err := p.PerfectParse(sqls, "", "") + if err != nil { + i.Logger().Errorf("parse sql failed, error: %v, sql: %s", err, sqls) + return nil, err + } + + ret := make([]*driverV2.SQLObjectOps, 0, len(stmts)) + for _, stmt := range stmts { + objectOps := driverV2.NewSQLObjectOps(stmt.Text()) + err := parseStmtOpInfos(p, stmt, objectOps) + if err != nil { + i.Logger().Errorf("parse sql op failed, error: %v, sql: %s", err, stmt.Text()) + return nil, err + } + ret = append(ret, objectOps) + } + + for i := range ret { + ret[i].ObjectOps = SQLObjectOpsDuplicateRemoval(ret[i].ObjectOps) + } + + return ret, nil +} + +func parseStmtOpInfos(p *parser.Parser, stmt ast.StmtNode, ops *driverV2.SQLObjectOps) error { + + switch s := stmt.(type) { + case *ast.CreateTableStmt: + // https://dev.mysql.com/doc/refman/8.0/en/create-table.html + // You must have the CREATE privilege for the table. + ops.AddObjectOp(newTableObjectFromTableName(driverV2.SQLOpAddOrUpdate, s.Table)) + if s.ReferTable != nil { + ops.AddObjectOp(newTableObjectFromTableName(driverV2.SQLOpRead, s.ReferTable)) + } + + case *ast.DropTableStmt: + // https://dev.mysql.com/doc/refman/8.0/en/drop-table.html + // You must have the DROP privilege for each table. + for _, table := range s.Tables { + ops.AddObjectOp(newTableObjectFromTableName(driverV2.SQLOpDelete, table)) + } + + case *ast.AlterTableStmt: + // https://dev.mysql.com/doc/refman/8.0/en/alter-table.html + // To use ALTER TABLE, you need ALTER, CREATE, and INSERT privileges for the table. + ops.AddObjectOp(newTableObjectFromTableName(driverV2.SQLOpAddOrUpdate, s.Table)) + + // Renaming a table requires ALTER and DROP on the old table, ALTER, CREATE, and INSERT on the new table. + for _, spec := range s.Specs { + if spec.Tp == ast.AlterTableRenameTable { + ops.AddObjectOp(newTableObjectFromTableName(driverV2.SQLOpAddOrUpdate, s.Table)) + ops.AddObjectOp(newTableObjectFromTableName(driverV2.SQLOpDelete, s.Table)) + ops.AddObjectOp(newTableObjectFromTableName(driverV2.SQLOpAddOrUpdate, spec.NewTable)) + } + } + + case *ast.TruncateTableStmt: + // https://dev.mysql.com/doc/refman/8.0/en/truncate-table.html + // It requires the DROP privilege. + ops.AddObjectOp(newTableObjectFromTableName(driverV2.SQLOpDelete, s.Table)) + + case *ast.RepairTableStmt: + // https://dev.mysql.com/doc/refman/8.0/en/repair-table.html + // https://docs.pingcap.com/tidb/v5.4/sql-statement-admin/ + // TODO: TiDB的解析器(ADMIN REPAIR TABLE)不支持解析MySQL的REPAIR TABLE语句 + return fmt.Errorf("repair table not supported") + + case *ast.CreateDatabaseStmt: + // https://dev.mysql.com/doc/refman/8.0/en/create-database.html + // To use this statement, you need the CREATE privilege for the database. + ops.AddObjectOp(newSchemaObject(driverV2.SQLOpAddOrUpdate, s.Name)) + + case *ast.AlterDatabaseStmt: + // https://dev.mysql.com/doc/refman/8.0/en/alter-database.html + // This statement requires the ALTER privilege on the database. + ops.AddObjectOp(newSchemaObject(driverV2.SQLOpAddOrUpdate, s.Name)) + + case *ast.DropDatabaseStmt: + // https://dev.mysql.com/doc/refman/8.0/en/drop-database.html + // To use DROP DATABASE, you need the DROP privilege on the database. + ops.AddObjectOp(newSchemaObject(driverV2.SQLOpDelete, s.Name)) + + case *ast.RenameTableStmt: + // https://dev.mysql.com/doc/refman/8.0/en/rename-table.html + // You must have ALTER and DROP privileges for the original table, and CREATE and INSERT privileges for the new table + ops.AddObjectOp(newTableObjectFromTableName(driverV2.SQLOpAddOrUpdate, s.OldTable)) + ops.AddObjectOp(newTableObjectFromTableName(driverV2.SQLOpDelete, s.OldTable)) + ops.AddObjectOp(newTableObjectFromTableName(driverV2.SQLOpAddOrUpdate, s.NewTable)) + + case *ast.CreateViewStmt: + // https://dev.mysql.com/doc/refman/8.0/en/create-view.html + // The CREATE VIEW statement requires the CREATE VIEW privilege for the view, + // and some privilege for each column selected by the SELECT statement. + // For columns used elsewhere in the SELECT statement, you must have the SELECT privilege. + // If the OR REPLACE clause is present, you must also have the DROP privilege for the view. + ops.AddObjectOp(newTableObjectFromTableName(driverV2.SQLOpAddOrUpdate, s.ViewName)) + if s.OrReplace { + ops.AddObjectOp(newTableObjectFromTableName(driverV2.SQLOpDelete, s.ViewName)) + } + + switch s := s.Select.(type) { + case *ast.SelectStmt: + o, err := getSelectStmtObjectOps(s) + if nil != err { + return fmt.Errorf("failed to parse create view, %v", err) + } + ops.AddObjectOp(o...) + case *ast.UnionStmt: + for _, selectStmt := range s.SelectList.Selects { + o, err := getSelectStmtObjectOps(selectStmt) + if nil != err { + return fmt.Errorf("failed to parse create view, %v", err) + } + ops.AddObjectOp(o...) + } + default: + return fmt.Errorf("failed to parse create view, not support select type: %T", s) + } + + case *ast.CreateIndexStmt: + // https://dev.mysql.com/doc/refman/8.0/en/create-index.html + // CREATE INDEX is mapped to an ALTER TABLE statement to create indexes. + ops.AddObjectOp(newTableObjectFromTableName(driverV2.SQLOpAddOrUpdate, s.Table)) + + case *ast.DropIndexStmt: + // https://dev.mysql.com/doc/refman/8.0/en/drop-index.html + // This statement is mapped to an ALTER TABLE statement to drop the index. + ops.AddObjectOp(newTableObjectFromTableName(driverV2.SQLOpAddOrUpdate, s.Table)) + + case *ast.LockTablesStmt: + // https://dev.mysql.com/doc/refman/8.0/en/lock-tables.html + // You must have the LOCK TABLES privilege, and the SELECT privilege for each object to be locked. + for _, t := range s.TableLocks { + ops.AddObjectOp(newTableObjectFromTableName(driverV2.SQLOpAdmin, t.Table)) + ops.AddObjectOp(newTableObjectFromTableName(driverV2.SQLOpRead, t.Table)) + } + + // TODO: 视图处理 + // For view locking, LOCK TABLES adds all base tables used in the view to the set of tables to be locked and locks them automatically. + // For tables underlying any view being locked, LOCK TABLES checks that the view definer (for SQL SECURITY DEFINER views) or invoker (for all views) has the proper privileges on the tables. + + case *ast.UnlockTablesStmt: + // https://dev.mysql.com/doc/refman/8.0/en/lock-tables.html + // UNLOCK TABLES explicitly releases any table locks held by the current session. + // TODO: Unlock table的操作针对的当前session,无法从sql语句中确定具体的表,暂且不处理 + case *ast.SelectStmt: + + o, err := getSelectStmtObjectOps(s) + if nil != err { + return fmt.Errorf("failed to parse select, %v", err) + } + ops.AddObjectOp(o...) + + case *ast.UnionStmt: + // https://dev.mysql.com/doc/refman/8.0/en/union.html + + for _, selectStmt := range s.SelectList.Selects { + o, err := getSelectStmtObjectOps(selectStmt) + if nil != err { + return fmt.Errorf("failed to parse union, %v", err) + } + ops.AddObjectOp(o...) + } + + // TiDB的解析器尚不支持MySQL8.0的一些新union语法,暂且不处理 + // 如(SELECT 1 UNION SELECT 1) UNION SELECT 1; + + case *ast.LoadDataStmt: + // https://dev.mysql.com/doc/refman/8.0/en/load-data.html + // You must have the FILE privilege + // For a LOCAL load operation, the client program reads a text file located on the client host. + // Because the file contents are sent over the connection by the client to the server, + // using LOCAL is a bit slower than when the server accesses the file directly. + // On the other hand, you do not need the FILE privilege, + // and the file can be located in any directory the client program can access. + if !s.IsLocal { + ops.AddObjectOp(newServerObject(driverV2.SQLOpAdmin)) + } + ops.AddObjectOp(newTableObjectFromTableName(driverV2.SQLOpAddOrUpdate, s.Table)) + + case *ast.InsertStmt: + // https://dev.mysql.com/doc/refman/8.0/en/insert.html + // Inserting into a table requires the INSERT privilege for the table. + // If the ON DUPLICATE KEY UPDATE clause is used and a duplicate key causes an UPDATE to be performed instead, + // the statement requires the UPDATE privilege for the columns to be updated. + // For columns that are read but not modified you need only the SELECT privilege + // (such as for a column referenced only on the right hand side of an col_name=expr assignment in an ON DUPLICATE KEY UPDATE clause). + tableNames := util.GetTables(s.Table.TableRefs) + for _, tableName := range tableNames { + ops.AddObjectOp(newTableObjectFromTableName(driverV2.SQLOpAddOrUpdate, tableName)) + } + + if s.Select != nil { + if selectStmt, ok := s.Select.(*ast.SelectStmt); ok { + o, err := getSelectStmtObjectOps(selectStmt) + if nil != err { + return fmt.Errorf("failed to parse insert, %v", err) + } + ops.AddObjectOp(o...) + } else { + return fmt.Errorf("failed to parse insert, not support select type: %T", s.Select) + } + } + + case *ast.DeleteStmt: + // https://dev.mysql.com/doc/refman/8.0/en/delete.html + // You need the DELETE privilege on a table to delete rows from it. + // You need only the SELECT privilege for any columns that are only read, + // such as those named in the WHERE clause. + + var tableAlias []*tableAliasInfo + if s.IsMultiTable { + for _, table := range s.Tables.Tables { + ops.AddObjectOp(newTableObjectFromTableName(driverV2.SQLOpDelete, table)) + } + tableAlias = getTableAliasInfoFromTableNames(s.Tables.Tables) + } else { + tableNames := util.GetTables(s.TableRefs.TableRefs) + for _, tableName := range tableNames { + ops.AddObjectOp(newTableObjectFromTableName(driverV2.SQLOpDelete, tableName)) + } + + tableAlias = getTableAliasInfoFromJoin(s.TableRefs.TableRefs) + } + + if s.Where != nil { + ops.AddObjectOp(getTableObjectOpsInWhere(tableAlias, s.Where)...) + } + + case *ast.UpdateStmt: + // https://dev.mysql.com/doc/refman/8.0/en/update.html + // You need the UPDATE privilege only for columns referenced in an UPDATE that are actually updated. + // You need only the SELECT privilege for any columns that are read but not modified. + + tableAlias := getTableAliasInfoFromJoin(s.TableRefs.TableRefs) + // UPDATE items,month SET items.price=month.price + // WHERE items.id=month.id; + // 对 items 表有 更新和读取 ,对 month 表只有读取 + for _, list := range s.List { + if list.Column != nil { + ops.AddObjectOp(newTableObjectFromColumn(driverV2.SQLOpAddOrUpdate, list.Column, tableAlias)) + } + if c, ok := list.Expr.(*ast.ColumnNameExpr); ok { + ops.AddObjectOp(newTableObjectFromColumn(driverV2.SQLOpRead, c.Name, tableAlias)) + } + } + + if s.Where != nil { + ops.AddObjectOp(getTableObjectOpsInWhere(tableAlias, s.Where)...) + } + + case *ast.ShowStmt: + // https://dev.mysql.com/doc/refman/8.0/en/show.html + switch s.Tp { + case ast.ShowEngines: + case ast.ShowDatabases: + // You see only those databases for which you have some kind of privilege, + // unless you have the global SHOW DATABASES privilege. + case ast.ShowTables, ast.ShowTableStatus: + case ast.ShowColumns: + // https://dev.mysql.com/doc/refman/8.0/en/show-columns.html + schemaName := s.Table.Schema.L + if s.DBName != "" { + schemaName = s.DBName + } + ops.AddObjectOp(newTableObject(driverV2.SQLOpRead, s.Table.Name.L, schemaName)) + case ast.ShowWarnings, ast.ShowErrors: + case ast.ShowCharset, ast.ShowCollation: + case ast.ShowVariables, ast.ShowStatus: + case ast.ShowCreateTable, ast.ShowCreateView: + // https://dev.mysql.com/doc/refman/8.0/en/show-create-table.html + ops.AddObjectOp(newTableObjectFromTableName(driverV2.SQLOpRead, s.Table)) + case ast.ShowCreateUser: + // https://dev.mysql.com/doc/refman/8.0/en/show-create-user.html + // The statement requires the SELECT privilege for the mysql system schema, + // except to see information for the current user. + ops.AddObjectOp(newMysqlSchemaObject(driverV2.SQLOpRead)) + case ast.ShowGrants: + // https://dev.mysql.com/doc/refman/8.0/en/show-grants.html + ops.AddObjectOp(newMysqlSchemaObject(driverV2.SQLOpRead)) + case ast.ShowTriggers: + case ast.ShowProcedureStatus: + // https://dev.mysql.com/doc/refman/8.0/en/show-procedure-status.html + ops.AddObjectOp(newInstanceObject(driverV2.SQLOpRead)) + case ast.ShowIndex: + ops.AddObjectOp(newTableObjectFromTableName(driverV2.SQLOpRead, s.Table)) + case ast.ShowProcessList: + case ast.ShowCreateDatabase: + ops.AddObjectOp(newSchemaObject(driverV2.SQLOpRead, s.DBName)) + case ast.ShowEvents: + // https://dev.mysql.com/doc/refman/5.7/en/show-events.html + // It requires the EVENT privilege for the database from which the events are to be shown. + ops.AddObjectOp(newSchemaObject(driverV2.SQLOpRead, s.DBName)) + case ast.ShowPlugins: + case ast.ShowProfile, ast.ShowProfiles: + case ast.ShowMasterStatus: + // https://dev.mysql.com/doc/refman/5.7/en/show-master-status.html + // It requires either the SUPER or REPLICATION CLIENT privilege. + ops.AddObjectOp(newInstanceObject(driverV2.SQLOpAdmin)) + case ast.ShowPrivileges: + default: + return fmt.Errorf("failed to parse show, not support show type: %v", s.Tp) + } + case *ast.ExplainForStmt: + // https://dev.mysql.com/doc/refman/8.0/en/explain.html + // EXPLAIN ... FOR CONNECTION also requires the PROCESS privilege if the specified connection belongs to a different user. + // 由于无法判断FOR CONNECTION的连接是否属于当前用户,所以这里定义为整个实例的读 + ops.AddObjectOp(newInstanceObject(driverV2.SQLOpRead)) + case *ast.ExplainStmt: + // https://dev.mysql.com/doc/refman/8.0/en/explain.html + // EXPLAIN requires the same privileges required to execute the explained statement. + if err := parseStmtOpInfos(p, s.Stmt, ops); err != nil { + return err + } + // TODO: + // Additionally, EXPLAIN also requires the SHOW VIEW privilege for any explained view. + case *ast.PrepareStmt: + // https://dev.mysql.com/doc/refman/8.0/en/sql-prepared-statements.html + stmt, err := p.ParseOneStmt(s.SQLText, "", "") + if nil != err { + return err + } + if err := parseStmtOpInfos(p, stmt, ops); err != nil { + return err + } + + // TODO: prepare 可能使用上下文来定义语句,如下: + // SET @s = 'SELECT SQRT(POW(?,2) + POW(?,2)) AS hypotenuse'; + // PREPARE stmt2 FROM @s; + // 这种情况需要结合上下文来解析 + case *ast.ExecuteStmt: + case *ast.DeallocateStmt: + case *ast.BeginStmt: + case *ast.CommitStmt: + case *ast.RollbackStmt: + case *ast.BinlogStmt: + // https://dev.mysql.com/doc/refman/8.0/en/binlog.html + // To execute BINLOG statements when applying mysqlbinlog output, + // a user account requires the BINLOG_ADMIN privilege (or the deprecated SUPER privilege), + // or the REPLICATION_APPLIER privilege plus the appropriate privileges to execute each log event. + ops.AddObjectOp(newInstanceObject(driverV2.SQLOpAdmin)) + case *ast.UseStmt: + // https://dev.mysql.com/doc/refman/8.0/en/use.html + // This statement requires some privilege for the database or some object within it. + case *ast.FlushStmt: + // https://dev.mysql.com/doc/refman/8.0/en/flush.html + ops.AddObjectOp(newInstanceObject(driverV2.SQLOpAdmin)) + case *ast.KillStmt: + // https://dev.mysql.com/doc/refman/8.0/en/kill.html + ops.AddObjectOp(newInstanceObject(driverV2.SQLOpAdmin)) + case *ast.SetStmt: + case *ast.SetPwdStmt: + // https://dev.mysql.com/doc/refman/8.0/en/set-password.html + if !s.User.CurrentUser { + ops.AddObjectOp(newInstanceObject(driverV2.SQLOpAdmin)) + } + case *ast.CreateUserStmt: + // https://dev.mysql.com/doc/refman/8.0/en/create-user.html + ops.AddObjectOp(newInstanceObject(driverV2.SQLOpAdmin)) + case *ast.AlterUserStmt: + // https://dev.mysql.com/doc/refman/8.0/en/alter-user.html + ops.AddObjectOp(newInstanceObject(driverV2.SQLOpAdmin)) + case *ast.AlterInstanceStmt: + // https://dev.mysql.com/doc/refman/8.0/en/alter-instance.html + // TiDB的解析器仅支持ALTER INSTANCE RELOAD TLS语句 + ops.AddObjectOp(newInstanceObject(driverV2.SQLOpAdmin)) + case *ast.DropUserStmt: + // https://dev.mysql.com/doc/refman/8.0/en/drop-user.html + ops.AddObjectOp(newInstanceObject(driverV2.SQLOpAdmin)) + case *ast.RevokeStmt: + // https://dev.mysql.com/doc/refman/8.0/en/revoke.html + switch s.Level.Level { + case ast.GrantLevelGlobal: + ops.AddObjectOp(newInstanceObject(driverV2.SQLOpGrant)) + case ast.GrantLevelDB: + ops.AddObjectOp(newSchemaObject(driverV2.SQLOpGrant, s.Level.DBName)) + case ast.GrantLevelTable: + ops.AddObjectOp(newTableObject(driverV2.SQLOpGrant, s.Level.TableName, s.Level.DBName)) + default: + return fmt.Errorf("not support grant level: %v", s.Level.Level) + } + case *ast.GrantStmt: + // https://dev.mysql.com/doc/refman/8.0/en/grant.html + switch s.Level.Level { + case ast.GrantLevelGlobal: + ops.AddObjectOp(newInstanceObject(driverV2.SQLOpGrant)) + case ast.GrantLevelDB: + ops.AddObjectOp(newSchemaObject(driverV2.SQLOpGrant, s.Level.DBName)) + case ast.GrantLevelTable: + ops.AddObjectOp(newTableObject(driverV2.SQLOpGrant, s.Level.TableName, s.Level.DBName)) + default: + return fmt.Errorf("not support grant level: %v", s.Level.Level) + } + case *ast.ShutdownStmt: + // https://dev.mysql.com/doc/refman/8.0/en/shutdown.html + ops.AddObjectOp(newInstanceObject(driverV2.SQLOpAdmin)) + case *ast.UnparsedStmt: + return fmt.Errorf("there is unparsed stmt: %s", stmt.Text()) + default: + return fmt.Errorf("not support stmt type: %T", stmt) + } + return nil +} + +func getSelectStmtObjectOps(selectStmt *ast.SelectStmt) ([]*driverV2.SQLObjectOp, error) { + ret := make([]*driverV2.SQLObjectOp, 0) + if selectStmt.From != nil { + tableNames := util.GetTables(selectStmt.From.TableRefs) + for _, tableName := range tableNames { + ret = append(ret, newTableObjectFromTableName(driverV2.SQLOpRead, tableName)) + } + } + if selectStmt.SelectIntoOpt != nil { + // https://dev.mysql.com/doc/refman/8.0/en/select-into.html + // The SELECT ... INTO OUTFILE 'file_name' form of SELECT writes the selected rows to a file. + // The file is created on the server host, + // so you must have the FILE privilege to use this syntax. + if selectStmt.SelectIntoOpt.Tp == ast.SelectIntoOutfile || selectStmt.SelectIntoOpt.Tp == ast.SelectIntoDumpfile { + ret = append(ret, newServerObject(driverV2.SQLOpAdmin)) + } + } + return ret, nil +} + +func newTableObjectFromTableName(op driverV2.SQLOp, table *ast.TableName) *driverV2.SQLObjectOp { + return &driverV2.SQLObjectOp{ + Op: op, + Object: newTable(table.Name.L, table.Schema.L), + } +} + +func newTableObject(op driverV2.SQLOp, tableName, schemaName string) *driverV2.SQLObjectOp { + return &driverV2.SQLObjectOp{ + Op: op, + Object: newTable(tableName, schemaName), + } +} + +type tableAliasInfo struct { + tableName string + schemaName string + tableAliasName string +} + +func newTableObjectFromColumn(op driverV2.SQLOp, column *ast.ColumnName, tableAliasInfo []*tableAliasInfo) *driverV2.SQLObjectOp { + if column.Table.String() == "" && len(tableAliasInfo) == 1 { + t := tableAliasInfo[0] + return newTableObject(op, t.tableName, t.schemaName) + } + for _, t := range tableAliasInfo { + if t.tableAliasName == column.Table.String() { + return newTableObject(op, t.tableName, t.schemaName) + } + } + return &driverV2.SQLObjectOp{ + Op: op, + Object: newTable(column.Table.L, column.Schema.L), + } +} + +func newSchemaObject(op driverV2.SQLOp, schemaName string) *driverV2.SQLObjectOp { + return &driverV2.SQLObjectOp{ + Op: op, + Object: newSchema(schemaName), + } +} + +func newMysqlSchemaObject(op driverV2.SQLOp) *driverV2.SQLObjectOp { + return newSchemaObject(op, "mysql") +} + +func newInstanceObject(op driverV2.SQLOp) *driverV2.SQLObjectOp { + return &driverV2.SQLObjectOp{ + Op: op, + Object: newInstance(), + } +} + +func newServerObject(op driverV2.SQLOp) *driverV2.SQLObjectOp { + return &driverV2.SQLObjectOp{ + Op: op, + Object: newServer(), + } +} + +func newTable(tableName, schemaName string) *driverV2.SQLObject { + return &driverV2.SQLObject{ + Type: driverV2.SQLObjectTypeTable, + SchemaName: schemaName, + TableName: tableName, + } +} + +func newSchema(schemaName string) *driverV2.SQLObject { + return &driverV2.SQLObject{ + Type: driverV2.SQLObjectTypeSchema, + SchemaName: schemaName, + TableName: "", + } +} + +func newInstance() *driverV2.SQLObject { + return &driverV2.SQLObject{ + Type: driverV2.SQLObjectTypeInstance, + SchemaName: "", + TableName: "", + } +} + +func newServer() *driverV2.SQLObject { + return &driverV2.SQLObject{ + Type: driverV2.SQLObjectTypeServer, + SchemaName: "", + TableName: "", + } +} + +func getTableAliasInfoFromTableNames(tableNames []*ast.TableName) []*tableAliasInfo { + tableAlias := make([]*tableAliasInfo, 0) + for _, tableName := range tableNames { + tableAlias = append(tableAlias, &tableAliasInfo{ + tableAliasName: "", + tableName: tableName.Name.L, + schemaName: tableName.Schema.L, + }) + } + return tableAlias +} + +func getTableAliasInfoFromJoin(stmt *ast.Join) []*tableAliasInfo { + tableAlias := make([]*tableAliasInfo, 0) + tableSources := util.GetTableSources(stmt) + for _, tableSource := range tableSources { + switch source := tableSource.Source.(type) { + case *ast.TableName: + tableAlias = append(tableAlias, &tableAliasInfo{ + tableAliasName: tableSource.AsName.String(), + tableName: source.Name.L, + schemaName: source.Schema.L, + }) + default: + } + } + return tableAlias +} + +func getTableObjectOpsInWhere(tableAliasInfo []*tableAliasInfo, where ast.ExprNode) []*driverV2.SQLObjectOp { + c := &tableObjectsInWhere{ + tableAliasInfo: tableAliasInfo, + } + where.Accept(c) + return c.tables +} + +type tableObjectsInWhere struct { + tables []*driverV2.SQLObjectOp + tableAliasInfo []*tableAliasInfo +} + +func (c *tableObjectsInWhere) Enter(in ast.Node) (ast.Node, bool) { + if cn, ok := in.(*ast.ColumnName); ok { + c.tables = append(c.tables, newTableObjectFromColumn(driverV2.SQLOpRead, cn, c.tableAliasInfo)) + } + return in, false +} + +func (c *tableObjectsInWhere) Leave(in ast.Node) (ast.Node, bool) { + return in, true +} + +func SQLObjectOpsDuplicateRemoval(ops []*driverV2.SQLObjectOp) []*driverV2.SQLObjectOp { + m := make(map[string]*driverV2.SQLObjectOp) + for _, o := range ops { + m[SQLObjectOpFingerPrint(o)] = o + } + ret := make([]*driverV2.SQLObjectOp, 0) + for _, o := range m { + ret = append(ret, o) + } + return ret +} + +func SQLObjectOpsFingerPrint(ops []*driverV2.SQLObjectOps) string { + s := make([]string, len(ops)) + for i := range ops { + s[i] = SQLObjectOpFingerPrints(ops[i].ObjectOps) + s[i] = fmt.Sprintf("%s %s", s[i], ops[i].Sql.Sql) + } + return strings.Join(s, "\n") +} + +func SQLObjectOpFingerPrints(ops []*driverV2.SQLObjectOp) string { + s := make([]string, len(ops)) + for i := range ops { + s[i] = SQLObjectOpFingerPrint(ops[i]) + } + return strings.Join(s, ";") +} + +func SQLObjectOpFingerPrint(op *driverV2.SQLObjectOp) string { + return fmt.Sprintf("%s %s", SQLObjectFingerPrint(op.Object), op.Op) +} + +func SQLObjectFingerPrint(obj *driverV2.SQLObject) string { + switch obj.Type { + case driverV2.SQLObjectTypeInstance: + return "*.*" + case driverV2.SQLObjectTypeSchema: + return fmt.Sprintf("%s.*", obj.SchemaName) + case driverV2.SQLObjectTypeTable: + return fmt.Sprintf("%s.%s", obj.SchemaName, obj.TableName) + default: + return "unknown" + } +} diff --git a/sqle/driver/mysql/sql_ops_test.go b/sqle/driver/mysql/sql_ops_test.go new file mode 100644 index 0000000000..42be961be6 --- /dev/null +++ b/sqle/driver/mysql/sql_ops_test.go @@ -0,0 +1,1826 @@ +package mysql + +import ( + "context" + "fmt" + "reflect" + "sort" + "testing" + + driverV2 "github.com/actiontech/sqle/sqle/driver/v2" + "github.com/actiontech/sqle/sqle/log" +) + +var ( + testSQLObjectOpT1Read = &driverV2.SQLObjectOp{ + Op: driverV2.SQLOpRead, + Object: &driverV2.SQLObject{ + Type: driverV2.SQLObjectTypeTable, + SchemaName: "s1", + TableName: "t1", + }, + } + testSQLObjectOpT1AddOrUpdate = &driverV2.SQLObjectOp{ + Op: driverV2.SQLOpAddOrUpdate, + Object: &driverV2.SQLObject{ + Type: driverV2.SQLObjectTypeTable, + SchemaName: "s1", + TableName: "t1", + }, + } +) + +func TestSQLObjectOpsDuplicateRemoval(t *testing.T) { + type args struct { + ops []*driverV2.SQLObjectOp + } + tests := []struct { + name string + args args + want []*driverV2.SQLObjectOp + }{ + { + name: "test1", + args: args{ops: []*driverV2.SQLObjectOp{testSQLObjectOpT1Read, testSQLObjectOpT1Read}}, + want: []*driverV2.SQLObjectOp{testSQLObjectOpT1Read}, + }, + { + name: "test1", + args: args{ops: []*driverV2.SQLObjectOp{testSQLObjectOpT1Read, testSQLObjectOpT1Read, testSQLObjectOpT1AddOrUpdate}}, + want: []*driverV2.SQLObjectOp{testSQLObjectOpT1Read, testSQLObjectOpT1AddOrUpdate}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := SQLObjectOpsDuplicateRemoval(tt.args.ops); !reflect.DeepEqual(got, tt.want) { + t.Errorf("SQLObjectOpsDuplicateRemoval() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestMysqlDriverImpl_GetSQLOp(t *testing.T) { + type args struct { + ctx context.Context + sqls string + } + tests := []struct { + name string + args args + want []*driverV2.SQLObjectOps + wantErr error + }{ + { + name: "multi sql", + args: args{ctx: context.Background(), sqls: "select * from s1.t1;select * from s1.t2"}, + want: []*driverV2.SQLObjectOps{ + { + ObjectOps: []*driverV2.SQLObjectOp{ + { + Op: driverV2.SQLOpRead, + Object: &driverV2.SQLObject{ + Type: driverV2.SQLObjectTypeTable, + SchemaName: "s1", + TableName: "t1", + }, + }, + }, + Sql: driverV2.SQLInfo{ + Sql: "select * from s1.t1;", + }, + }, + { + ObjectOps: []*driverV2.SQLObjectOp{ + { + Op: driverV2.SQLOpRead, + Object: &driverV2.SQLObject{ + Type: driverV2.SQLObjectTypeTable, + SchemaName: "s1", + TableName: "t2", + }, + }, + }, + Sql: driverV2.SQLInfo{ + Sql: "select * from s1.t2", + }, + }, + }, + wantErr: nil, + }, + { + name: "select basic", + args: args{ctx: context.Background(), sqls: "select * from s1.t1"}, + want: []*driverV2.SQLObjectOps{ + { + ObjectOps: []*driverV2.SQLObjectOp{ + { + Op: driverV2.SQLOpRead, + Object: &driverV2.SQLObject{ + Type: driverV2.SQLObjectTypeTable, + SchemaName: "s1", + TableName: "t1", + }, + }, + }, + Sql: driverV2.SQLInfo{ + Sql: "select * from s1.t1", + }, + }, + }, + wantErr: nil, + }, + { + name: "select no schema", + args: args{ctx: context.Background(), sqls: "select * from t1"}, + want: []*driverV2.SQLObjectOps{ + { + ObjectOps: []*driverV2.SQLObjectOp{ + { + Op: driverV2.SQLOpRead, + Object: &driverV2.SQLObject{ + Type: driverV2.SQLObjectTypeTable, + SchemaName: "", + TableName: "t1", + }, + }, + }, + Sql: driverV2.SQLInfo{ + Sql: "select * from t1", + }, + }, + }, + wantErr: nil, + }, + { + name: "select 1", + args: args{ctx: context.Background(), sqls: "select 1"}, + want: []*driverV2.SQLObjectOps{ + { + ObjectOps: []*driverV2.SQLObjectOp{}, + Sql: driverV2.SQLInfo{ + Sql: "select 1", + }, + }, + }, + wantErr: nil, + }, + { + name: "select multi table", + args: args{ctx: context.Background(), sqls: "select * from t1,t2"}, + want: []*driverV2.SQLObjectOps{ + { + ObjectOps: []*driverV2.SQLObjectOp{ + { + Op: driverV2.SQLOpRead, + Object: &driverV2.SQLObject{ + Type: driverV2.SQLObjectTypeTable, + SchemaName: "", + TableName: "t1", + }, + }, + { + Op: driverV2.SQLOpRead, + Object: &driverV2.SQLObject{ + Type: driverV2.SQLObjectTypeTable, + SchemaName: "", + TableName: "t2", + }, + }, + }, + Sql: driverV2.SQLInfo{ + Sql: "select * from t1,t2", + }, + }, + }, + wantErr: nil, + }, + { + name: "select join table", + args: args{ctx: context.Background(), sqls: "SELECT * FROM t1 INNER JOIN t2"}, + want: []*driverV2.SQLObjectOps{ + { + ObjectOps: []*driverV2.SQLObjectOp{ + { + Op: driverV2.SQLOpRead, + Object: &driverV2.SQLObject{ + Type: driverV2.SQLObjectTypeTable, + SchemaName: "", + TableName: "t1", + }, + }, + { + Op: driverV2.SQLOpRead, + Object: &driverV2.SQLObject{ + Type: driverV2.SQLObjectTypeTable, + SchemaName: "", + TableName: "t2", + }, + }, + }, + Sql: driverV2.SQLInfo{ + Sql: "SELECT * FROM t1 INNER JOIN t2", + }, + }, + }, + wantErr: nil, + }, + { + name: "select into outfile", + args: args{ctx: context.Background(), + sqls: "SELECT * FROM t1 INTO OUTFILE '/tmp/select-values.txt'"}, + want: []*driverV2.SQLObjectOps{ + { + ObjectOps: []*driverV2.SQLObjectOp{ + { + Op: driverV2.SQLOpAdmin, + Object: &driverV2.SQLObject{ + Type: driverV2.SQLObjectTypeServer, + SchemaName: "", + TableName: "", + }, + }, + { + Op: driverV2.SQLOpRead, + Object: &driverV2.SQLObject{ + Type: driverV2.SQLObjectTypeTable, + SchemaName: "", + TableName: "t1", + }, + }, + }, + Sql: driverV2.SQLInfo{ + Sql: "SELECT * FROM t1 INTO OUTFILE '/tmp/select-values.txt'", + }, + }, + }, + wantErr: nil, + }, + { + name: "create table like", + args: args{ctx: context.Background(), sqls: "CREATE TABLE new_tbl LIKE orig_tbl"}, + want: []*driverV2.SQLObjectOps{ + { + ObjectOps: []*driverV2.SQLObjectOp{ + { + Op: driverV2.SQLOpAddOrUpdate, + Object: &driverV2.SQLObject{ + Type: driverV2.SQLObjectTypeTable, + SchemaName: "", + TableName: "new_tbl", + }, + }, + { + Op: driverV2.SQLOpRead, + Object: &driverV2.SQLObject{ + Type: driverV2.SQLObjectTypeTable, + SchemaName: "", + TableName: "orig_tbl", + }, + }, + }, + Sql: driverV2.SQLInfo{ + Sql: "CREATE TABLE new_tbl LIKE orig_tbl", + }, + }, + }, + wantErr: nil, + }, + { + name: "create table basic", + args: args{ctx: context.Background(), sqls: "CREATE TABLE t (c CHAR(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin);"}, + want: []*driverV2.SQLObjectOps{ + { + ObjectOps: []*driverV2.SQLObjectOp{ + { + Op: driverV2.SQLOpAddOrUpdate, + Object: &driverV2.SQLObject{ + Type: driverV2.SQLObjectTypeTable, + SchemaName: "", + TableName: "t", + }, + }, + }, + Sql: driverV2.SQLInfo{ + Sql: "CREATE TABLE t (c CHAR(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin);", + }, + }, + }, + wantErr: nil, + }, + { + name: "drop table basic", + args: args{ctx: context.Background(), sqls: "DROP TABLE t1;"}, + want: []*driverV2.SQLObjectOps{ + { + ObjectOps: []*driverV2.SQLObjectOp{ + { + Op: driverV2.SQLOpDelete, + Object: &driverV2.SQLObject{ + Type: driverV2.SQLObjectTypeTable, + SchemaName: "", + TableName: "t1", + }, + }, + }, + Sql: driverV2.SQLInfo{ + Sql: "DROP TABLE t1;", + }, + }, + }, + wantErr: nil, + }, + { + name: "alter table basic", + args: args{ctx: context.Background(), sqls: "ALTER TABLE t2 DROP COLUMN c, DROP COLUMN d;"}, + want: []*driverV2.SQLObjectOps{ + { + ObjectOps: []*driverV2.SQLObjectOp{ + { + Op: driverV2.SQLOpAddOrUpdate, + Object: &driverV2.SQLObject{ + Type: driverV2.SQLObjectTypeTable, + SchemaName: "", + TableName: "t2", + }, + }, + }, + Sql: driverV2.SQLInfo{ + Sql: "ALTER TABLE t2 DROP COLUMN c, DROP COLUMN d;", + }, + }, + }, + wantErr: nil, + }, + { + name: "alter table rename", + args: args{ctx: context.Background(), sqls: "ALTER TABLE old_table RENAME new_table;"}, + want: []*driverV2.SQLObjectOps{ + { + ObjectOps: []*driverV2.SQLObjectOp{ + { + Op: driverV2.SQLOpAddOrUpdate, + Object: &driverV2.SQLObject{ + Type: driverV2.SQLObjectTypeTable, + SchemaName: "", + TableName: "new_table", + }, + }, + { + Op: driverV2.SQLOpAddOrUpdate, + Object: &driverV2.SQLObject{ + Type: driverV2.SQLObjectTypeTable, + SchemaName: "", + TableName: "old_table", + }, + }, + { + Op: driverV2.SQLOpDelete, + Object: &driverV2.SQLObject{ + Type: driverV2.SQLObjectTypeTable, + SchemaName: "", + TableName: "old_table", + }, + }, + }, + Sql: driverV2.SQLInfo{ + Sql: "ALTER TABLE old_table RENAME new_table;", + }, + }, + }, + wantErr: nil, + }, + { + name: "rename table", + args: args{ctx: context.Background(), sqls: "RENAME TABLE old_table TO new_table;"}, + want: []*driverV2.SQLObjectOps{ + { + ObjectOps: []*driverV2.SQLObjectOp{ + { + Op: driverV2.SQLOpAddOrUpdate, + Object: &driverV2.SQLObject{ + Type: driverV2.SQLObjectTypeTable, + SchemaName: "", + TableName: "new_table", + }, + }, + { + Op: driverV2.SQLOpAddOrUpdate, + Object: &driverV2.SQLObject{ + Type: driverV2.SQLObjectTypeTable, + SchemaName: "", + TableName: "old_table", + }, + }, + { + Op: driverV2.SQLOpDelete, + Object: &driverV2.SQLObject{ + Type: driverV2.SQLObjectTypeTable, + SchemaName: "", + TableName: "old_table", + }, + }, + }, + Sql: driverV2.SQLInfo{ + Sql: "RENAME TABLE old_table TO new_table;", + }, + }, + }, + wantErr: nil, + }, + { + name: "truncate table", + args: args{ctx: context.Background(), sqls: "TRUNCATE TABLE t1"}, + want: []*driverV2.SQLObjectOps{ + { + ObjectOps: []*driverV2.SQLObjectOp{ + { + Op: driverV2.SQLOpDelete, + Object: &driverV2.SQLObject{ + Type: driverV2.SQLObjectTypeTable, + SchemaName: "", + TableName: "t1", + }, + }, + }, + Sql: driverV2.SQLInfo{ + Sql: "TRUNCATE TABLE t1", + }, + }, + }, + wantErr: nil, + }, + { + name: "repair table", + args: args{ctx: context.Background(), sqls: "REPAIR TABLE t1"}, + wantErr: fmt.Errorf("there is unparsed stmt: REPAIR TABLE t1"), + }, + { + name: "alter databases", + args: args{ctx: context.Background(), sqls: "ALTER DATABASE myDatabase CHARACTER SET= ascii;"}, + want: []*driverV2.SQLObjectOps{ + { + ObjectOps: []*driverV2.SQLObjectOp{ + { + Op: driverV2.SQLOpAddOrUpdate, + Object: &driverV2.SQLObject{ + Type: driverV2.SQLObjectTypeSchema, + SchemaName: "myDatabase", + TableName: "", + }, + }, + }, + Sql: driverV2.SQLInfo{ + Sql: "ALTER DATABASE myDatabase CHARACTER SET= ascii;", + }, + }, + }, + wantErr: nil, + }, + { + name: "drop databases", + args: args{ctx: context.Background(), sqls: "DROP DATABASE mydb"}, + want: []*driverV2.SQLObjectOps{ + { + ObjectOps: []*driverV2.SQLObjectOp{ + { + Op: driverV2.SQLOpDelete, + Object: &driverV2.SQLObject{ + Type: driverV2.SQLObjectTypeSchema, + SchemaName: "mydb", + TableName: "", + }, + }, + }, + Sql: driverV2.SQLInfo{ + Sql: "DROP DATABASE mydb", + }, + }, + }, + wantErr: nil, + }, + { + name: "create view", + args: args{ctx: context.Background(), sqls: "CREATE VIEW test.v AS SELECT * FROM t;"}, + want: []*driverV2.SQLObjectOps{ + { + ObjectOps: []*driverV2.SQLObjectOp{ + { + Op: driverV2.SQLOpAddOrUpdate, + Object: &driverV2.SQLObject{ + Type: driverV2.SQLObjectTypeTable, + SchemaName: "test", + TableName: "v", + }, + }, + { + Op: driverV2.SQLOpRead, + Object: &driverV2.SQLObject{ + Type: driverV2.SQLObjectTypeTable, + SchemaName: "", + TableName: "t", + }, + }, + }, + Sql: driverV2.SQLInfo{ + Sql: "CREATE VIEW test.v AS SELECT * FROM t;", + }, + }, + }, + wantErr: nil, + }, + { + name: "create view select CURRENT_DATE", + args: args{ctx: context.Background(), sqls: "CREATE VIEW v_today (today) AS SELECT CURRENT_DATE;"}, + want: []*driverV2.SQLObjectOps{ + { + ObjectOps: []*driverV2.SQLObjectOp{ + { + Op: driverV2.SQLOpAddOrUpdate, + Object: &driverV2.SQLObject{ + Type: driverV2.SQLObjectTypeTable, + SchemaName: "", + TableName: "v_today", + }, + }, + }, + Sql: driverV2.SQLInfo{ + Sql: "CREATE VIEW v_today (today) AS SELECT CURRENT_DATE;", + }, + }, + }, + wantErr: nil, + }, + { + name: "create or replace view", + args: args{ctx: context.Background(), sqls: `CREATE OR REPLACE VIEW view_name AS +SELECT column_name(s) +FROM table_name +WHERE condition`}, + want: []*driverV2.SQLObjectOps{ + { + ObjectOps: []*driverV2.SQLObjectOp{ + { + Op: driverV2.SQLOpAddOrUpdate, + Object: &driverV2.SQLObject{ + Type: driverV2.SQLObjectTypeTable, + SchemaName: "", + TableName: "view_name", + }, + }, + { + Op: driverV2.SQLOpDelete, + Object: &driverV2.SQLObject{ + Type: driverV2.SQLObjectTypeTable, + SchemaName: "", + TableName: "view_name", + }, + }, + { + Op: driverV2.SQLOpRead, + Object: &driverV2.SQLObject{ + Type: driverV2.SQLObjectTypeTable, + SchemaName: "", + TableName: "table_name", + }, + }, + }, + Sql: driverV2.SQLInfo{ + Sql: `CREATE OR REPLACE VIEW view_name AS +SELECT column_name(s) +FROM table_name +WHERE condition`, + }, + }, + }, + wantErr: nil, + }, + { + name: "create view union", + args: args{ctx: context.Background(), sqls: `CREATE VIEW netcheck.cpu_mp AS +(SELECT + cpu.ID AS id, + cpu.chanel_name AS chanel_name, + cpu.first_channel AS first_channel, + cpu.IMG_Url AS IMG_Url, + cpu.lastModifyTime AS lastModifyTime, + cpu.second_channel AS second_channel, + cpu.SHOW_TIME AS SHOW_TIME, + cpu.TASK_Id AS TASK_Id, + cpu.TITLE AS TITLE, + cpu.URL AS URL, + cpu.checkSysTaskId AS checkSysTaskId, + cpu.innerUUID AS innerUUID, + cpu.isReject AS isReject, + cpu.scanTime AS scanTime +FROM channel_page_update_result cpu +) + UNION ALL +(SELECT + mp.id AS id, + '' AS chanel_name, + '' AS first_channel, + '' AS second_channel, + '' AS TITLE, + mp.imgUrl AS IMG_Url + ,mp.lastModifyTime AS lastModifyTime, + mp.showTime AS SHOW_TIME, + mp.taskId AS TASK_Id, + mp.url AS URL, + mp.checkSysTaskId AS checkSysTaskId, + mp.innerUUID AS innerUUID, + mp.isReject AS isReject, + mp.scanTime AS scanTime +FROM mainpageupdateresult mp +);`}, + want: []*driverV2.SQLObjectOps{ + { + ObjectOps: []*driverV2.SQLObjectOp{ + { + Op: driverV2.SQLOpAddOrUpdate, + Object: &driverV2.SQLObject{ + Type: driverV2.SQLObjectTypeTable, + SchemaName: "netcheck", + TableName: "cpu_mp", + }, + }, + { + Op: driverV2.SQLOpRead, + Object: &driverV2.SQLObject{ + Type: driverV2.SQLObjectTypeTable, + SchemaName: "", + TableName: "channel_page_update_result", + }, + }, + { + Op: driverV2.SQLOpRead, + Object: &driverV2.SQLObject{ + Type: driverV2.SQLObjectTypeTable, + SchemaName: "", + TableName: "mainpageupdateresult", + }, + }, + }, + Sql: driverV2.SQLInfo{ + Sql: `CREATE VIEW netcheck.cpu_mp AS +(SELECT + cpu.ID AS id, + cpu.chanel_name AS chanel_name, + cpu.first_channel AS first_channel, + cpu.IMG_Url AS IMG_Url, + cpu.lastModifyTime AS lastModifyTime, + cpu.second_channel AS second_channel, + cpu.SHOW_TIME AS SHOW_TIME, + cpu.TASK_Id AS TASK_Id, + cpu.TITLE AS TITLE, + cpu.URL AS URL, + cpu.checkSysTaskId AS checkSysTaskId, + cpu.innerUUID AS innerUUID, + cpu.isReject AS isReject, + cpu.scanTime AS scanTime +FROM channel_page_update_result cpu +) + UNION ALL +(SELECT + mp.id AS id, + '' AS chanel_name, + '' AS first_channel, + '' AS second_channel, + '' AS TITLE, + mp.imgUrl AS IMG_Url + ,mp.lastModifyTime AS lastModifyTime, + mp.showTime AS SHOW_TIME, + mp.taskId AS TASK_Id, + mp.url AS URL, + mp.checkSysTaskId AS checkSysTaskId, + mp.innerUUID AS innerUUID, + mp.isReject AS isReject, + mp.scanTime AS scanTime +FROM mainpageupdateresult mp +);`, + }, + }, + }, + wantErr: nil, + }, + { + name: "create index", + args: args{ctx: context.Background(), sqls: `CREATE INDEX part_of_name ON customer (name(10));`}, + want: []*driverV2.SQLObjectOps{ + { + ObjectOps: []*driverV2.SQLObjectOp{ + { + Op: driverV2.SQLOpAddOrUpdate, + Object: &driverV2.SQLObject{ + Type: driverV2.SQLObjectTypeTable, + SchemaName: "", + TableName: "customer", + }, + }, + }, + Sql: driverV2.SQLInfo{ + Sql: `CREATE INDEX part_of_name ON customer (name(10));`, + }, + }, + }, + wantErr: nil, + }, + { + name: "drop index", + args: args{ctx: context.Background(), sqls: `DROP INDEX i1 ON t;`}, + want: []*driverV2.SQLObjectOps{ + { + ObjectOps: []*driverV2.SQLObjectOp{ + { + Op: driverV2.SQLOpAddOrUpdate, + Object: &driverV2.SQLObject{ + Type: driverV2.SQLObjectTypeTable, + SchemaName: "", + TableName: "t", + }, + }, + }, + Sql: driverV2.SQLInfo{ + Sql: `DROP INDEX i1 ON t;`, + }, + }, + }, + wantErr: nil, + }, + { + name: "lock table", + args: args{ctx: context.Background(), sqls: `LOCK TABLES t1 READ`}, + want: []*driverV2.SQLObjectOps{ + { + ObjectOps: []*driverV2.SQLObjectOp{ + { + Op: driverV2.SQLOpAdmin, + Object: &driverV2.SQLObject{ + Type: driverV2.SQLObjectTypeTable, + SchemaName: "", + TableName: "t1", + }, + }, + { + Op: driverV2.SQLOpRead, + Object: &driverV2.SQLObject{ + Type: driverV2.SQLObjectTypeTable, + SchemaName: "", + TableName: "t1", + }, + }, + }, + Sql: driverV2.SQLInfo{ + Sql: `LOCK TABLES t1 READ`, + }, + }, + }, + wantErr: nil, + }, + { + name: "unlock table", + args: args{ctx: context.Background(), sqls: `UNLOCK TABLES;`}, + want: []*driverV2.SQLObjectOps{ + { + ObjectOps: []*driverV2.SQLObjectOp{}, + Sql: driverV2.SQLInfo{ + Sql: `UNLOCK TABLES;`, + }, + }, + }, + wantErr: nil, + }, + { + name: "union", + args: args{ctx: context.Background(), sqls: `SELECT 1, 2 UNION SELECT 'a', 'b';`}, + want: []*driverV2.SQLObjectOps{ + { + ObjectOps: []*driverV2.SQLObjectOp{}, + Sql: driverV2.SQLInfo{ + Sql: `SELECT 1, 2 UNION SELECT 'a', 'b';`, + }, + }, + }, + wantErr: nil, + }, + { + name: "union table", + args: args{ctx: context.Background(), sqls: `SELECT city FROM customers +UNION +SELECT city FROM suppliers +ORDER BY city;`}, + want: []*driverV2.SQLObjectOps{ + { + ObjectOps: []*driverV2.SQLObjectOp{ + { + Op: driverV2.SQLOpRead, + Object: &driverV2.SQLObject{ + Type: driverV2.SQLObjectTypeTable, + SchemaName: "", + TableName: "customers", + }, + }, + { + Op: driverV2.SQLOpRead, + Object: &driverV2.SQLObject{ + Type: driverV2.SQLObjectTypeTable, + SchemaName: "", + TableName: "suppliers", + }, + }, + }, + Sql: driverV2.SQLInfo{ + Sql: `SELECT city FROM customers +UNION +SELECT city FROM suppliers +ORDER BY city;`, + }, + }, + }, + wantErr: nil, + }, + { + name: "load data", + args: args{ctx: context.Background(), sqls: `LOAD DATA INFILE 'data.txt' INTO TABLE db2.my_table;`}, + want: []*driverV2.SQLObjectOps{ + { + ObjectOps: []*driverV2.SQLObjectOp{ + { + Op: driverV2.SQLOpAdmin, + Object: &driverV2.SQLObject{ + Type: driverV2.SQLObjectTypeServer, + SchemaName: "", + TableName: "", + }, + }, + { + Op: driverV2.SQLOpAddOrUpdate, + Object: &driverV2.SQLObject{ + Type: driverV2.SQLObjectTypeTable, + SchemaName: "db2", + TableName: "my_table", + }, + }, + }, + Sql: driverV2.SQLInfo{ + Sql: `LOAD DATA INFILE 'data.txt' INTO TABLE db2.my_table;`, + }, + }, + }, + wantErr: nil, + }, + { + name: "load data local", + args: args{ctx: context.Background(), sqls: `LOAD DATA LOCAL INFILE 'data.txt' INTO TABLE db2.my_table;`}, + want: []*driverV2.SQLObjectOps{ + { + ObjectOps: []*driverV2.SQLObjectOp{ + { + Op: driverV2.SQLOpAddOrUpdate, + Object: &driverV2.SQLObject{ + Type: driverV2.SQLObjectTypeTable, + SchemaName: "db2", + TableName: "my_table", + }, + }, + }, + Sql: driverV2.SQLInfo{ + Sql: `LOAD DATA LOCAL INFILE 'data.txt' INTO TABLE db2.my_table;`, + }, + }, + }, + wantErr: nil, + }, + { + name: "insert", + args: args{ctx: context.Background(), sqls: `INSERT INTO tbl_name () VALUES();`}, + want: []*driverV2.SQLObjectOps{ + { + ObjectOps: []*driverV2.SQLObjectOp{ + { + Op: driverV2.SQLOpAddOrUpdate, + Object: &driverV2.SQLObject{ + Type: driverV2.SQLObjectTypeTable, + SchemaName: "", + TableName: "tbl_name", + }, + }, + }, + Sql: driverV2.SQLInfo{ + Sql: `INSERT INTO tbl_name () VALUES();`, + }, + }, + }, + wantErr: nil, + }, + { + name: "insert select from", + args: args{ctx: context.Background(), sqls: `INSERT INTO tbl_temp2 (fld_id) +SELECT tbl_temp1.fld_order_id FROM tbl_temp1 WHERE tbl_temp1.fld_order_id > 100;`}, + want: []*driverV2.SQLObjectOps{ + { + ObjectOps: []*driverV2.SQLObjectOp{ + { + Op: driverV2.SQLOpAddOrUpdate, + Object: &driverV2.SQLObject{ + Type: driverV2.SQLObjectTypeTable, + SchemaName: "", + TableName: "tbl_temp2", + }, + }, + { + Op: driverV2.SQLOpRead, + Object: &driverV2.SQLObject{ + Type: driverV2.SQLObjectTypeTable, + SchemaName: "", + TableName: "tbl_temp1", + }, + }, + }, + Sql: driverV2.SQLInfo{ + Sql: `INSERT INTO tbl_temp2 (fld_id) +SELECT tbl_temp1.fld_order_id FROM tbl_temp1 WHERE tbl_temp1.fld_order_id > 100;`, + }, + }, + }, + wantErr: nil, + }, + { + name: "delete where", + args: args{ctx: context.Background(), sqls: `DELETE FROM somelog WHERE user = 'jcole' ORDER BY timestamp_column LIMIT 1;`}, + want: []*driverV2.SQLObjectOps{ + { + ObjectOps: []*driverV2.SQLObjectOp{ + { + Op: driverV2.SQLOpDelete, + Object: &driverV2.SQLObject{ + Type: driverV2.SQLObjectTypeTable, + SchemaName: "", + TableName: "somelog", + }, + }, + { + Op: driverV2.SQLOpRead, + Object: &driverV2.SQLObject{ + Type: driverV2.SQLObjectTypeTable, + SchemaName: "", + TableName: "somelog", + }, + }, + }, + Sql: driverV2.SQLInfo{ + Sql: `DELETE FROM somelog WHERE user = 'jcole' ORDER BY timestamp_column LIMIT 1;`, + }, + }, + }, + wantErr: nil, + }, + { + name: "delete where use alias", + args: args{ctx: context.Background(), sqls: `DELETE FROM somelog AS s WHERE s.user = 'jcole' ORDER BY timestamp_column LIMIT 1;`}, + want: []*driverV2.SQLObjectOps{ + { + ObjectOps: []*driverV2.SQLObjectOp{ + { + Op: driverV2.SQLOpDelete, + Object: &driverV2.SQLObject{ + Type: driverV2.SQLObjectTypeTable, + SchemaName: "", + TableName: "somelog", + }, + }, + { + Op: driverV2.SQLOpRead, + Object: &driverV2.SQLObject{ + Type: driverV2.SQLObjectTypeTable, + SchemaName: "", + TableName: "somelog", + }, + }, + }, + Sql: driverV2.SQLInfo{ + Sql: `DELETE FROM somelog AS s WHERE s.user = 'jcole' ORDER BY timestamp_column LIMIT 1;`, + }, + }, + }, + wantErr: nil, + }, + { + name: "delete table", + args: args{ctx: context.Background(), sqls: `DROP TABLE t_old;`}, + want: []*driverV2.SQLObjectOps{ + { + ObjectOps: []*driverV2.SQLObjectOp{ + { + Op: driverV2.SQLOpDelete, + Object: &driverV2.SQLObject{ + Type: driverV2.SQLObjectTypeTable, + SchemaName: "", + TableName: "t_old", + }, + }, + }, + Sql: driverV2.SQLInfo{ + Sql: `DROP TABLE t_old;`, + }, + }, + }, + wantErr: nil, + }, + { + name: "delete multi table", + args: args{ctx: context.Background(), sqls: `DELETE t1, t2 FROM t1 INNER JOIN t2 INNER JOIN t3 WHERE t1.id=t2.id AND t2.id=t3.id;`}, + want: []*driverV2.SQLObjectOps{ + { + ObjectOps: []*driverV2.SQLObjectOp{ + { + Op: driverV2.SQLOpDelete, + Object: &driverV2.SQLObject{ + Type: driverV2.SQLObjectTypeTable, + SchemaName: "", + TableName: "t1", + }, + }, + { + Op: driverV2.SQLOpDelete, + Object: &driverV2.SQLObject{ + Type: driverV2.SQLObjectTypeTable, + SchemaName: "", + TableName: "t2", + }, + }, + { + Op: driverV2.SQLOpRead, + Object: &driverV2.SQLObject{ + Type: driverV2.SQLObjectTypeTable, + SchemaName: "", + TableName: "t1", + }, + }, + { + Op: driverV2.SQLOpRead, + Object: &driverV2.SQLObject{ + Type: driverV2.SQLObjectTypeTable, + SchemaName: "", + TableName: "t2", + }, + }, + { + Op: driverV2.SQLOpRead, + Object: &driverV2.SQLObject{ + Type: driverV2.SQLObjectTypeTable, + SchemaName: "", + TableName: "t3", + }, + }, + }, + Sql: driverV2.SQLInfo{ + Sql: `DELETE t1, t2 FROM t1 INNER JOIN t2 INNER JOIN t3 WHERE t1.id=t2.id AND t2.id=t3.id;`, + }, + }, + }, + wantErr: nil, + }, + { + name: "update table basic", + args: args{ctx: context.Background(), sqls: `UPDATE t1 SET col1 = col1 + 1;`}, + want: []*driverV2.SQLObjectOps{ + { + ObjectOps: []*driverV2.SQLObjectOp{ + { + Op: driverV2.SQLOpAddOrUpdate, + Object: &driverV2.SQLObject{ + Type: driverV2.SQLObjectTypeTable, + SchemaName: "", + TableName: "t1", + }, + }, + }, + Sql: driverV2.SQLInfo{ + Sql: `UPDATE t1 SET col1 = col1 + 1;`, + }, + }, + }, + wantErr: nil, + }, + { + name: "update table with two table", + args: args{ctx: context.Background(), sqls: `UPDATE items,month SET items.price=month.price WHERE items.id=month.id;`}, + want: []*driverV2.SQLObjectOps{ + { + ObjectOps: []*driverV2.SQLObjectOp{ + { + Op: driverV2.SQLOpAddOrUpdate, + Object: &driverV2.SQLObject{ + Type: driverV2.SQLObjectTypeTable, + SchemaName: "", + TableName: "items", + }, + }, + { + Op: driverV2.SQLOpRead, + Object: &driverV2.SQLObject{ + Type: driverV2.SQLObjectTypeTable, + SchemaName: "", + TableName: "items", + }, + }, + { + Op: driverV2.SQLOpRead, + Object: &driverV2.SQLObject{ + Type: driverV2.SQLObjectTypeTable, + SchemaName: "", + TableName: "month", + }, + }, + }, + Sql: driverV2.SQLInfo{ + Sql: `UPDATE items,month SET items.price=month.price WHERE items.id=month.id;`, + }, + }, + }, + wantErr: nil, + }, + { + name: "show columns", + args: args{ctx: context.Background(), sqls: `SHOW COLUMNS FROM mytable FROM mydb;`}, + want: []*driverV2.SQLObjectOps{ + { + ObjectOps: []*driverV2.SQLObjectOp{ + { + Op: driverV2.SQLOpRead, + Object: &driverV2.SQLObject{ + Type: driverV2.SQLObjectTypeTable, + SchemaName: "mydb", + TableName: "mytable", + }, + }, + }, + Sql: driverV2.SQLInfo{ + Sql: `SHOW COLUMNS FROM mytable FROM mydb;`, + }, + }, + }, + wantErr: nil, + }, + { + name: "show create table", + args: args{ctx: context.Background(), sqls: `SHOW CREATE TABLE t`}, + want: []*driverV2.SQLObjectOps{ + { + ObjectOps: []*driverV2.SQLObjectOp{ + { + Op: driverV2.SQLOpRead, + Object: &driverV2.SQLObject{ + Type: driverV2.SQLObjectTypeTable, + SchemaName: "", + TableName: "t", + }, + }, + }, + Sql: driverV2.SQLInfo{ + Sql: `SHOW CREATE TABLE t`, + }, + }, + }, + wantErr: nil, + }, + { + name: "show create user", + args: args{ctx: context.Background(), sqls: `SHOW CREATE USER 'u1'@'localhost'`}, + want: []*driverV2.SQLObjectOps{ + { + ObjectOps: []*driverV2.SQLObjectOp{ + { + Op: driverV2.SQLOpRead, + Object: &driverV2.SQLObject{ + Type: driverV2.SQLObjectTypeSchema, + SchemaName: "mysql", + TableName: "", + }, + }, + }, + Sql: driverV2.SQLInfo{ + Sql: `SHOW CREATE USER 'u1'@'localhost'`, + }, + }, + }, + wantErr: nil, + }, + { + name: "show grants", + args: args{ctx: context.Background(), sqls: `SHOW GRANTS FOR 'jeffrey'@'localhost';`}, + want: []*driverV2.SQLObjectOps{ + { + ObjectOps: []*driverV2.SQLObjectOp{ + { + Op: driverV2.SQLOpRead, + Object: &driverV2.SQLObject{ + Type: driverV2.SQLObjectTypeSchema, + SchemaName: "mysql", + TableName: "", + }, + }, + }, + Sql: driverV2.SQLInfo{ + Sql: `SHOW GRANTS FOR 'jeffrey'@'localhost';`, + }, + }, + }, + wantErr: nil, + }, + { + name: "show procedure status", + args: args{ctx: context.Background(), sqls: `SHOW PROCEDURE STATUS LIKE 'sp1'`}, + want: []*driverV2.SQLObjectOps{ + { + ObjectOps: []*driverV2.SQLObjectOp{ + { + Op: driverV2.SQLOpRead, + Object: &driverV2.SQLObject{ + Type: driverV2.SQLObjectTypeInstance, + SchemaName: "", + TableName: "", + }, + }, + }, + Sql: driverV2.SQLInfo{ + Sql: `SHOW PROCEDURE STATUS LIKE 'sp1'`, + }, + }, + }, + wantErr: nil, + }, + { + name: "show index", + args: args{ctx: context.Background(), sqls: `SHOW INDEX FROM City`}, + want: []*driverV2.SQLObjectOps{ + { + ObjectOps: []*driverV2.SQLObjectOp{ + { + Op: driverV2.SQLOpRead, + Object: &driverV2.SQLObject{ + Type: driverV2.SQLObjectTypeTable, + SchemaName: "", + TableName: "city", + }, + }, + }, + Sql: driverV2.SQLInfo{ + Sql: `SHOW INDEX FROM City`, + }, + }, + }, + wantErr: nil, + }, + { + name: "show create databases", + args: args{ctx: context.Background(), sqls: `SHOW CREATE DATABASE test`}, + want: []*driverV2.SQLObjectOps{ + { + ObjectOps: []*driverV2.SQLObjectOp{ + { + Op: driverV2.SQLOpRead, + Object: &driverV2.SQLObject{ + Type: driverV2.SQLObjectTypeSchema, + SchemaName: "test", + TableName: "", + }, + }, + }, + Sql: driverV2.SQLInfo{ + Sql: `SHOW CREATE DATABASE test`, + }, + }, + }, + wantErr: nil, + }, + { + name: "show events", + args: args{ctx: context.Background(), sqls: `SHOW EVENTS FROM test`}, + want: []*driverV2.SQLObjectOps{ + { + ObjectOps: []*driverV2.SQLObjectOp{ + { + Op: driverV2.SQLOpRead, + Object: &driverV2.SQLObject{ + Type: driverV2.SQLObjectTypeSchema, + SchemaName: "test", + TableName: "", + }, + }, + }, + Sql: driverV2.SQLInfo{ + Sql: `SHOW EVENTS FROM test`, + }, + }, + }, + wantErr: nil, + }, + { + name: "show master status", + args: args{ctx: context.Background(), sqls: `SHOW MASTER STATUS`}, + want: []*driverV2.SQLObjectOps{ + { + ObjectOps: []*driverV2.SQLObjectOp{ + { + Op: driverV2.SQLOpAdmin, + Object: &driverV2.SQLObject{ + Type: driverV2.SQLObjectTypeInstance, + SchemaName: "", + TableName: "", + }, + }, + }, + Sql: driverV2.SQLInfo{ + Sql: `SHOW MASTER STATUS`, + }, + }, + }, + wantErr: nil, + }, + { + name: "explain for connection", + args: args{ctx: context.Background(), sqls: `EXPLAIN FOR CONNECTION 4`}, + want: []*driverV2.SQLObjectOps{ + { + ObjectOps: []*driverV2.SQLObjectOp{ + { + Op: driverV2.SQLOpRead, + Object: &driverV2.SQLObject{ + Type: driverV2.SQLObjectTypeInstance, + SchemaName: "", + TableName: "", + }, + }, + }, + Sql: driverV2.SQLInfo{ + Sql: `EXPLAIN FOR CONNECTION 4`, + }, + }, + }, + wantErr: nil, + }, + { + name: "explain", + args: args{ctx: context.Background(), sqls: `EXPLAIN ANALYZE SELECT * FROM t1 JOIN t2 ON (t1.c1 = t2.c2)`}, + want: []*driverV2.SQLObjectOps{ + { + ObjectOps: []*driverV2.SQLObjectOp{ + { + Op: driverV2.SQLOpRead, + Object: &driverV2.SQLObject{ + Type: driverV2.SQLObjectTypeTable, + SchemaName: "", + TableName: "t1", + }, + }, + { + Op: driverV2.SQLOpRead, + Object: &driverV2.SQLObject{ + Type: driverV2.SQLObjectTypeTable, + SchemaName: "", + TableName: "t2", + }, + }, + }, + Sql: driverV2.SQLInfo{ + Sql: `EXPLAIN ANALYZE SELECT * FROM t1 JOIN t2 ON (t1.c1 = t2.c2)`, + }, + }, + }, + wantErr: nil, + }, + { + name: "prepare", + args: args{ctx: context.Background(), sqls: `PREPARE stmt1 FROM 'SELECT productCode, productName FROM products WHERE productCode = ?'`}, + want: []*driverV2.SQLObjectOps{ + { + ObjectOps: []*driverV2.SQLObjectOp{ + { + Op: driverV2.SQLOpRead, + Object: &driverV2.SQLObject{ + Type: driverV2.SQLObjectTypeTable, + SchemaName: "", + TableName: "products", + }, + }, + }, + Sql: driverV2.SQLInfo{ + Sql: `PREPARE stmt1 FROM 'SELECT productCode, productName FROM products WHERE productCode = ?'`, + }, + }, + }, + wantErr: nil, + }, + { + name: "binlog", + args: args{ctx: context.Background(), sqls: `BINLOG 'str'`}, + want: []*driverV2.SQLObjectOps{ + { + ObjectOps: []*driverV2.SQLObjectOp{ + { + Op: driverV2.SQLOpAdmin, + Object: &driverV2.SQLObject{ + Type: driverV2.SQLObjectTypeInstance, + SchemaName: "", + TableName: "", + }, + }, + }, + Sql: driverV2.SQLInfo{ + Sql: `BINLOG 'str'`, + }, + }, + }, + wantErr: nil, + }, + { + name: "flush", + args: args{ctx: context.Background(), sqls: `FLUSH BINARY LOGS`}, + want: []*driverV2.SQLObjectOps{ + { + ObjectOps: []*driverV2.SQLObjectOp{ + { + Op: driverV2.SQLOpAdmin, + Object: &driverV2.SQLObject{ + Type: driverV2.SQLObjectTypeInstance, + SchemaName: "", + TableName: "", + }, + }, + }, + Sql: driverV2.SQLInfo{ + Sql: `FLUSH BINARY LOGS`, + }, + }, + }, + wantErr: nil, + }, + { + name: "kill", + args: args{ctx: context.Background(), sqls: `KILL 10`}, + want: []*driverV2.SQLObjectOps{ + { + ObjectOps: []*driverV2.SQLObjectOp{ + { + Op: driverV2.SQLOpAdmin, + Object: &driverV2.SQLObject{ + Type: driverV2.SQLObjectTypeInstance, + SchemaName: "", + TableName: "", + }, + }, + }, + Sql: driverV2.SQLInfo{ + Sql: `KILL 10`, + }, + }, + }, + wantErr: nil, + }, + { + name: "set password", + args: args{ctx: context.Background(), sqls: `SET PASSWORD FOR 'jeffrey'@'localhost' = 'auth_string'`}, + want: []*driverV2.SQLObjectOps{ + { + ObjectOps: []*driverV2.SQLObjectOp{ + { + Op: driverV2.SQLOpAdmin, + Object: &driverV2.SQLObject{ + Type: driverV2.SQLObjectTypeInstance, + SchemaName: "", + TableName: "", + }, + }, + }, + Sql: driverV2.SQLInfo{ + Sql: `SET PASSWORD FOR 'jeffrey'@'localhost' = 'auth_string'`, + }, + }, + }, + wantErr: nil, + }, + { + name: "create user", + args: args{ctx: context.Background(), sqls: `CREATE USER 'jeffrey'@'localhost' IDENTIFIED BY 'password';`}, + want: []*driverV2.SQLObjectOps{ + { + ObjectOps: []*driverV2.SQLObjectOp{ + { + Op: driverV2.SQLOpAdmin, + Object: &driverV2.SQLObject{ + Type: driverV2.SQLObjectTypeInstance, + SchemaName: "", + TableName: "", + }, + }, + }, + Sql: driverV2.SQLInfo{ + Sql: `CREATE USER 'jeffrey'@'localhost' IDENTIFIED BY 'password';`, + }, + }, + }, + wantErr: nil, + }, + { + name: "alter user", + args: args{ctx: context.Background(), sqls: `ALTER USER 'jeffrey'@'localhost' ACCOUNT LOCK`}, + want: []*driverV2.SQLObjectOps{ + { + ObjectOps: []*driverV2.SQLObjectOp{ + { + Op: driverV2.SQLOpAdmin, + Object: &driverV2.SQLObject{ + Type: driverV2.SQLObjectTypeInstance, + SchemaName: "", + TableName: "", + }, + }, + }, + Sql: driverV2.SQLInfo{ + Sql: `ALTER USER 'jeffrey'@'localhost' ACCOUNT LOCK`, + }, + }, + }, + wantErr: nil, + }, + { + name: "alter instance", + args: args{ctx: context.Background(), sqls: `ALTER INSTANCE RELOAD TLS;`}, + want: []*driverV2.SQLObjectOps{ + { + ObjectOps: []*driverV2.SQLObjectOp{ + { + Op: driverV2.SQLOpAdmin, + Object: &driverV2.SQLObject{ + Type: driverV2.SQLObjectTypeInstance, + SchemaName: "", + TableName: "", + }, + }, + }, + Sql: driverV2.SQLInfo{ + Sql: `ALTER INSTANCE RELOAD TLS;`, + }, + }, + }, + wantErr: nil, + }, + { + name: "drop user", + args: args{ctx: context.Background(), sqls: `DROP USER 'jeffrey'@'localhost';`}, + want: []*driverV2.SQLObjectOps{ + { + ObjectOps: []*driverV2.SQLObjectOp{ + { + Op: driverV2.SQLOpAdmin, + Object: &driverV2.SQLObject{ + Type: driverV2.SQLObjectTypeInstance, + SchemaName: "", + TableName: "", + }, + }, + }, + Sql: driverV2.SQLInfo{ + Sql: `DROP USER 'jeffrey'@'localhost';`, + }, + }, + }, + wantErr: nil, + }, + { + name: "revoke instance level", + args: args{ctx: context.Background(), sqls: `REVOKE INSERT ON *.* FROM 'jeffrey'@'localhost'`}, + want: []*driverV2.SQLObjectOps{ + { + ObjectOps: []*driverV2.SQLObjectOp{ + { + Op: driverV2.SQLOpGrant, + Object: &driverV2.SQLObject{ + Type: driverV2.SQLObjectTypeInstance, + SchemaName: "", + TableName: "", + }, + }, + }, + Sql: driverV2.SQLInfo{ + Sql: `REVOKE INSERT ON *.* FROM 'jeffrey'@'localhost'`, + }, + }, + }, + wantErr: nil, + }, + { + name: "revoke schema level", + args: args{ctx: context.Background(), sqls: `REVOKE INSERT ON db1.* FROM 'jeffrey'@'localhost'`}, + want: []*driverV2.SQLObjectOps{ + { + ObjectOps: []*driverV2.SQLObjectOp{ + { + Op: driverV2.SQLOpGrant, + Object: &driverV2.SQLObject{ + Type: driverV2.SQLObjectTypeSchema, + SchemaName: "db1", + TableName: "", + }, + }, + }, + Sql: driverV2.SQLInfo{ + Sql: `REVOKE INSERT ON db1.* FROM 'jeffrey'@'localhost'`, + }, + }, + }, + wantErr: nil, + }, + { + name: "revoke table level", + args: args{ctx: context.Background(), sqls: `REVOKE INSERT ON db1.t1 FROM 'jeffrey'@'localhost'`}, + want: []*driverV2.SQLObjectOps{ + { + ObjectOps: []*driverV2.SQLObjectOp{ + { + Op: driverV2.SQLOpGrant, + Object: &driverV2.SQLObject{ + Type: driverV2.SQLObjectTypeTable, + SchemaName: "db1", + TableName: "t1", + }, + }, + }, + Sql: driverV2.SQLInfo{ + Sql: `REVOKE INSERT ON db1.t1 FROM 'jeffrey'@'localhost'`, + }, + }, + }, + wantErr: nil, + }, + { + name: "grant instance level", + args: args{ctx: context.Background(), sqls: `GRANT ALL ON *.* TO 'jeffrey'@'localhost'`}, + want: []*driverV2.SQLObjectOps{ + { + ObjectOps: []*driverV2.SQLObjectOp{ + { + Op: driverV2.SQLOpGrant, + Object: &driverV2.SQLObject{ + Type: driverV2.SQLObjectTypeInstance, + SchemaName: "", + TableName: "", + }, + }, + }, + Sql: driverV2.SQLInfo{ + Sql: `GRANT ALL ON *.* TO 'jeffrey'@'localhost'`, + }, + }, + }, + wantErr: nil, + }, + { + name: "grant schema level", + args: args{ctx: context.Background(), sqls: `GRANT ALL ON db1.* TO 'jeffrey'@'localhost'`}, + want: []*driverV2.SQLObjectOps{ + { + ObjectOps: []*driverV2.SQLObjectOp{ + { + Op: driverV2.SQLOpGrant, + Object: &driverV2.SQLObject{ + Type: driverV2.SQLObjectTypeSchema, + SchemaName: "db1", + TableName: "", + }, + }, + }, + Sql: driverV2.SQLInfo{ + Sql: `GRANT ALL ON db1.* TO 'jeffrey'@'localhost'`, + }, + }, + }, + wantErr: nil, + }, + { + name: "grant table level", + args: args{ctx: context.Background(), sqls: `GRANT ALL ON db1.t1 TO 'jeffrey'@'localhost'`}, + want: []*driverV2.SQLObjectOps{ + { + ObjectOps: []*driverV2.SQLObjectOp{ + { + Op: driverV2.SQLOpGrant, + Object: &driverV2.SQLObject{ + Type: driverV2.SQLObjectTypeTable, + SchemaName: "db1", + TableName: "t1", + }, + }, + }, + Sql: driverV2.SQLInfo{ + Sql: `GRANT ALL ON db1.t1 TO 'jeffrey'@'localhost'`, + }, + }, + }, + wantErr: nil, + }, + { + name: "shutdown", + args: args{ctx: context.Background(), sqls: `SHUTDOWN`}, + want: []*driverV2.SQLObjectOps{ + { + ObjectOps: []*driverV2.SQLObjectOp{ + { + Op: driverV2.SQLOpAdmin, + Object: &driverV2.SQLObject{ + Type: driverV2.SQLObjectTypeInstance, + SchemaName: "", + TableName: "", + }, + }, + }, + Sql: driverV2.SQLInfo{ + Sql: `SHUTDOWN`, + }, + }, + }, + wantErr: nil, + }, + { + name: "unparsed sql", + args: args{ctx: context.Background(), sqls: `SELECT * FROMa t1`}, + wantErr: fmt.Errorf("there is unparsed stmt: SELECT * FROMa t1"), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + i := &MysqlDriverImpl{log: log.NewEntry()} + got, err := i.GetSQLOp(tt.args.ctx, tt.args.sqls) + if nil == err && nil == tt.wantErr { + if !isResultEqual(got, tt.want) { + t.Errorf("MysqlDriverImpl.GetSQLOp() = %v, want %v", SQLObjectOpsFingerPrint(got), SQLObjectOpsFingerPrint(tt.want)) + } + return + } + if fmt.Sprintf("%v", err) != fmt.Sprintf("%v", tt.wantErr) { + t.Errorf("MysqlDriverImpl.GetSQLOp() error = %v, wantErr %v", err, tt.wantErr) + return + } + }) + } +} + +func isResultEqual(a, b []*driverV2.SQLObjectOps) bool { + if len(a) != len(b) { + return false + } + for i := range a { + if !isSQLObjectOpsEqual(a[i], b[i]) { + return false + } + } + return true +} + +func isSQLObjectOpsEqual(a, b *driverV2.SQLObjectOps) bool { + if len(a.ObjectOps) != len(b.ObjectOps) { + return false + } + if a.Sql.Sql != b.Sql.Sql { + return false + } + sort.Slice(a.ObjectOps, func(i, j int) bool { + s1 := SQLObjectOpFingerPrint(a.ObjectOps[i]) + s2 := SQLObjectOpFingerPrint(a.ObjectOps[j]) + return s1 < s2 + }) + + sort.Slice(b.ObjectOps, func(i, j int) bool { + s1 := SQLObjectOpFingerPrint(b.ObjectOps[i]) + s2 := SQLObjectOpFingerPrint(b.ObjectOps[j]) + return s1 < s2 + }) + + for i := range a.ObjectOps { + if !isSQLObjectOpEqual(a.ObjectOps[i], b.ObjectOps[i]) { + return false + } + } + return true +} + +func isSQLObjectOpEqual(a, b *driverV2.SQLObjectOp) bool { + if a.Op != b.Op { + return false + } + return isSQLObjectEqual(a.Object, b.Object) +} + +func isSQLObjectEqual(a, b *driverV2.SQLObject) bool { + if a.Type != b.Type { + return false + } + if a.SchemaName != b.SchemaName { + return false + } + if a.TableName != b.TableName { + return false + } + return true +}