vitess.io/vitess@v0.16.2/go/vt/vttablet/vexec/vexec.go (about)

     1  package vexec
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  
     8  	"vitess.io/vitess/go/vt/sqlparser"
     9  )
    10  
    11  const (
    12  	// TableQualifier is the standard schema used by VExec commands
    13  	TableQualifier = "_vt"
    14  )
    15  
    16  var (
    17  	// ErrColumNotFound is returned when we expect some column to exist and it does not
    18  	ErrColumNotFound = errors.New("Column not found")
    19  )
    20  
    21  // ValColumns map column name to Literal, for col=Val expressions in a WHERE clause
    22  type ValColumns map[string](*sqlparser.Literal)
    23  
    24  // TabletVExec is a utility structure, created when a VExec command is intercepted on the tablet.
    25  // This structure will parse and analyze the query, and make available some useful data.
    26  // VExec interceptors receive an instance of this struct so they can run more analysis/checks
    27  // on the given query, and potentially modify it.
    28  type TabletVExec struct {
    29  	Workflow   string
    30  	Keyspace   string
    31  	Query      string
    32  	Stmt       sqlparser.Statement
    33  	TableName  string
    34  	WhereCols  ValColumns
    35  	UpdateCols ValColumns
    36  	InsertCols ValColumns
    37  }
    38  
    39  // NewTabletVExec creates a new instance of TabletVExec
    40  func NewTabletVExec(workflow, keyspace string) *TabletVExec {
    41  	return &TabletVExec{
    42  		Workflow: workflow,
    43  		Keyspace: keyspace,
    44  	}
    45  }
    46  
    47  // ToStringVal converts a string to a string -typed Literal
    48  func (e *TabletVExec) ToStringVal(val string) *sqlparser.Literal {
    49  	return &sqlparser.Literal{
    50  		Type: sqlparser.StrVal,
    51  		Val:  val,
    52  	}
    53  }
    54  
    55  // ColumnStringVal returns a string value from a given column, or error if the column is not found
    56  func (e *TabletVExec) ColumnStringVal(columns ValColumns, colName string) (string, error) {
    57  	val, ok := columns[colName]
    58  	if !ok {
    59  		return "", fmt.Errorf("Could not find value for column %s", colName)
    60  	}
    61  	return string(val.Val), nil
    62  }
    63  
    64  // SetColumnStringVal modifies a column value into a given string
    65  func (e *TabletVExec) SetColumnStringVal(columns ValColumns, colName string, val string) {
    66  	columns[colName] = e.ToStringVal(val)
    67  }
    68  
    69  // analyzeWhereColumns identifies column names in a WHERE clause that have a comparison expression
    70  // e.g. will return `keyspace` in a "WHERE keyspace='abc'"
    71  // will not return `keyspace` in a "WHERE keyspace LIKE '%'"
    72  func (e *TabletVExec) analyzeWhereEqualsColumns(where *sqlparser.Where) ValColumns {
    73  	cols := ValColumns{}
    74  	if where == nil {
    75  		return cols
    76  	}
    77  	exprs := sqlparser.SplitAndExpression(nil, where.Expr)
    78  	for _, expr := range exprs {
    79  		switch expr := expr.(type) {
    80  		case *sqlparser.ComparisonExpr:
    81  			if expr.Operator != sqlparser.EqualOp {
    82  				continue
    83  			}
    84  			qualifiedName, ok := expr.Left.(*sqlparser.ColName)
    85  			if !ok {
    86  				continue
    87  			}
    88  			if val, ok := expr.Right.(*sqlparser.Literal); ok {
    89  				cols[qualifiedName.Name.String()] = val
    90  			}
    91  		}
    92  	}
    93  	return cols
    94  }
    95  
    96  // analyzeUpdateColumns analyses the columns modified by an UPDATE statement.
    97  // it returns the columns that are updated with a literal
    98  // e.g. in this statement: UPDATE tbl SET name='foo', val=3, status=other_column+2
    99  // the function returns name: 'foo' and val: 3, but does not return `status` column
   100  func (e *TabletVExec) analyzeUpdateColumns(update *sqlparser.Update) ValColumns {
   101  	cols := ValColumns{}
   102  	for _, col := range update.Exprs {
   103  		if val, ok := col.Expr.(*sqlparser.Literal); ok {
   104  			cols[col.Name.Name.Lowered()] = val
   105  		}
   106  	}
   107  	return cols
   108  }
   109  
   110  // analyzeInsertColumns analyses the columns and values given in an INSERT statement
   111  func (e *TabletVExec) analyzeInsertColumns(insert *sqlparser.Insert) ValColumns {
   112  	cols := ValColumns{}
   113  
   114  	rows, ok := insert.Rows.(sqlparser.Values)
   115  	if !ok {
   116  		return cols
   117  	}
   118  
   119  	if len(rows) != 1 {
   120  		return cols
   121  	}
   122  	for i, col := range insert.Columns {
   123  		expr := rows[0][i]
   124  		if val, ok := expr.(*sqlparser.Literal); ok {
   125  			cols[col.Lowered()] = val
   126  		}
   127  	}
   128  	return cols
   129  }
   130  
   131  // ReplaceInsertColumnVal manipulates the existing INSERT statement to replace a column value
   132  // into a given value
   133  func (e *TabletVExec) ReplaceInsertColumnVal(colName string, val *sqlparser.Literal) error {
   134  	insert, ok := e.Stmt.(*sqlparser.Insert)
   135  	if !ok {
   136  		return fmt.Errorf("Not an INSERT statement")
   137  	}
   138  	rows, ok := insert.Rows.(sqlparser.Values)
   139  	if !ok {
   140  		return fmt.Errorf("Not a Values type INSERT")
   141  	}
   142  	if len(rows) != 1 {
   143  		return fmt.Errorf("Not a single row INSERT")
   144  	}
   145  	for i, col := range insert.Columns {
   146  		if col.Lowered() == colName {
   147  			rows[0][i] = val
   148  			e.InsertCols[colName] = val
   149  			e.Query = sqlparser.String(e.Stmt)
   150  			return nil
   151  		}
   152  	}
   153  	return ErrColumNotFound
   154  }
   155  
   156  // AddOrReplaceInsertColumnVal manipulates the existing INSERT statement to replace a column value
   157  // into a given value
   158  func (e *TabletVExec) AddOrReplaceInsertColumnVal(colName string, val *sqlparser.Literal) error {
   159  	if err := e.ReplaceInsertColumnVal(colName, val); err != ErrColumNotFound {
   160  		return err
   161  	}
   162  	// We know the query is a valid single row INSERT
   163  	// We know column is not found. We need to add it.
   164  
   165  	insert, _ := e.Stmt.(*sqlparser.Insert)
   166  	rows, _ := insert.Rows.(sqlparser.Values)
   167  	rows[0] = append(rows[0], val)
   168  	insert.Columns = append(insert.Columns, sqlparser.NewIdentifierCI(colName))
   169  	e.InsertCols[colName] = val
   170  	e.Query = sqlparser.String(e.Stmt)
   171  
   172  	return nil
   173  }
   174  
   175  // analyzeStatement analyzes a given statement and produces the following ingredients, useful for
   176  // VExec interceptors:
   177  //   - table name
   178  //   - column names with values, for col=VAL in a WHERE expression
   179  //     e.g. in "UPDATE my_table SET ... WHERE keyspace='test' AND shard='-80' AND status > 2", the
   180  //     ValColumns are "keyspace" and "shard" with matching values. `status` is a range operator therefore
   181  //     not included.package vexec
   182  //     Equals operator is of special importance because it is known to filter results. An interceptor may
   183  //     require, for example, that a `DELETE` statement includes a WHERE with a UNIQUE KEY column with Equals operator
   184  //     to ensure we're not doing anything too risky.
   185  func (e *TabletVExec) analyzeStatement() error {
   186  	switch stmt := e.Stmt.(type) {
   187  	case *sqlparser.Update:
   188  		e.TableName = sqlparser.String(stmt.TableExprs)
   189  		e.WhereCols = e.analyzeWhereEqualsColumns(stmt.Where)
   190  		e.UpdateCols = e.analyzeUpdateColumns(stmt)
   191  	case *sqlparser.Delete:
   192  		e.TableName = sqlparser.String(stmt.TableExprs)
   193  		e.WhereCols = e.analyzeWhereEqualsColumns(stmt.Where)
   194  	case *sqlparser.Insert:
   195  		e.TableName = sqlparser.String(stmt.Table)
   196  		e.InsertCols = e.analyzeInsertColumns(stmt)
   197  	case *sqlparser.Select:
   198  		e.TableName = sqlparser.ToString(stmt.From)
   199  		e.WhereCols = e.analyzeWhereEqualsColumns(stmt.Where)
   200  	default:
   201  		return fmt.Errorf("query not supported by vexec: %+v", sqlparser.String(stmt))
   202  	}
   203  	return nil
   204  }
   205  
   206  // AnalyzeQuery analyzes a given statement and produces the following ingredients, useful for
   207  // VExec interceptors:
   208  //   - parsed statement
   209  //   - table name
   210  //   - column names with values, for col=VAL in a WHERE expression
   211  //     e.g. in "UPDATE my_table SET ... WHERE keyspace='test' AND shard='-80' AND status > 2", the
   212  //     ValColumns are "keyspace" and "shard" with matching values. `status` is a range operator therefore
   213  //     not included.package vexec
   214  //     Equals operator is of special importance because it is known to filter results. An interceptor may
   215  //     require, for example, that a `DELETE` statement includes a WHERE with a UNIQUE KEY column with Equals operator
   216  //     to ensure we're not doing anything too risky.
   217  func (e *TabletVExec) AnalyzeQuery(ctx context.Context, query string) (err error) {
   218  	if e.Stmt, err = sqlparser.Parse(query); err != nil {
   219  		return err
   220  	}
   221  	e.Query = query
   222  	if err := e.analyzeStatement(); err != nil {
   223  		return err
   224  	}
   225  	return nil
   226  }