// Copyright 2022 Molecula Corp. All rights reserved. package planner import ( "context" "fmt" "os" "strconv" "strings" pilosa "github.com/featurebasedb/featurebase/v3" "github.com/featurebasedb/featurebase/v3/dax" "github.com/featurebasedb/featurebase/v3/sql3" "github.com/featurebasedb/featurebase/v3/sql3/parser" "github.com/featurebasedb/featurebase/v3/sql3/planner/types" goerrors "github.com/pkg/errors" ) // compileBulkInsertStatement compiles a BULK INSERT statement into a // PlanOperator. func (p *ExecutionPlanner) compileBulkInsertStatement(ctx context.Context, stmt *parser.BulkInsertStatement) (_ types.PlanOperator, err error) { tableName := strings.ToLower(parser.IdentName(stmt.Table)) tname := dax.TableName(tableName) tbl, err := p.schemaAPI.TableByName(ctx, tname) if err != nil { if isTableNotFoundError(err) { return nil, sql3.NewErrTableNotFound(stmt.Table.NamePos.Line, stmt.Table.NamePos.Column, tableName) } return nil, err } err = p.checkAccess(ctx, tableName, accessTypeWriteData) if err != nil { return nil, err } // create an options options := &bulkInsertOptions{} // data source sliteral, sok := stmt.DataSource.(*parser.StringLit) if !sok { return nil, sql3.NewErrStringLiteral(stmt.DataSource.Pos().Line, stmt.DataSource.Pos().Column) } options.sourceData = sliteral.Value // format specifier sliteral, sok = stmt.Format.(*parser.StringLit) if !sok { return nil, sql3.NewErrStringLiteral(stmt.Format.Pos().Line, stmt.Format.Pos().Column) } options.format = sliteral.Value // input specifier sliteral, sok = stmt.Input.(*parser.StringLit) if !sok { return nil, sql3.NewErrStringLiteral(stmt.Input.Pos().Line, stmt.Input.Pos().Column) } options.input = sliteral.Value switch strings.ToUpper(options.input) { case "FILE": // file should exist if _, err := os.Stat(options.sourceData); goerrors.Is(err, os.ErrNotExist) { return nil, sql3.NewErrReadingDatasource(stmt.DataSource.Pos().Line, stmt.DataSource.Pos().Column, options.sourceData, fmt.Sprintf("file '%s' does not exist", options.sourceData)) } case "URL", "STREAM": // nothing to do here break default: return nil, sql3.NewErrInvalidInputSpecifier(stmt.Input.Pos().Line, stmt.Input.Pos().Column, options.input) } // HEADER_ROW bliteral, sok := stmt.HeaderRow.(*parser.BoolLit) if !sok { return nil, sql3.NewErrBoolLiteral(stmt.HeaderRow.Pos().Line, stmt.HeaderRow.Pos().Column) } options.hasHeaderRow = bliteral.Value // ALLOW_MISSING_VALUES bliteral, sok = stmt.AllowMissingValues.(*parser.BoolLit) if !sok { return nil, sql3.NewErrBoolLiteral(stmt.AllowMissingValues.Pos().Line, stmt.AllowMissingValues.Pos().Column) } options.allowMissingValues = bliteral.Value // batchsize literal, ok := stmt.BatchSize.(*parser.IntegerLit) if !ok { return nil, sql3.NewErrIntegerLiteral(stmt.BatchSize.Pos().Line, stmt.BatchSize.Pos().Column) } i, err := strconv.ParseInt(literal.Value, 10, 64) if err != nil { return nil, err } options.batchSize = int(i) // rows limit literal, ok = stmt.RowsLimit.(*parser.IntegerLit) if !ok { return nil, sql3.NewErrIntegerLiteral(stmt.RowsLimit.Pos().Line, stmt.RowsLimit.Pos().Column) } i, err = strconv.ParseInt(literal.Value, 10, 64) if err != nil { return nil, err } options.rowsLimit = int(i) // build the target columns options.targetColumns = make([]*qualifiedRefPlanExpression, 0) for _, m := range stmt.Columns { for idx, fld := range tbl.Fields { if strings.EqualFold(string(fld.Name), m.Name) { options.targetColumns = append(options.targetColumns, newQualifiedRefPlanExpression(tableName, strings.ToLower(m.Name), idx, fieldSQLDataType(pilosa.FieldToFieldInfo(fld)))) break } } } // build the map expressions options.mapExpressions = make([]*bulkInsertMapColumn, 0) for _, m := range stmt.MapList { expr, err := p.compileExpr(m.MapExpr) if err != nil { return nil, err } mapType, err := dataTypeFromParserType(m.Type) if err != nil { return nil, err } options.mapExpressions = append(options.mapExpressions, &bulkInsertMapColumn{ name: m.Name.String(), expr: expr, colType: mapType, }) } // build the transforms options.transformExpressions = make([]types.PlanExpression, 0) if stmt.TransformList != nil { for _, t := range stmt.TransformList { expr, err := p.compileExpr(t) if err != nil { return nil, err } options.transformExpressions = append(options.transformExpressions, expr) } } return NewPlanOpQuery(p, NewPlanOpBulkInsert(p, tableName, options), p.sql), nil } // analyzeBulkInsertStatement analyzes a BULK INSERT statement and returns an // error if anything is invalid. func (p *ExecutionPlanner) analyzeBulkInsertStatement(ctx context.Context, stmt *parser.BulkInsertStatement) error { // check referred to table exists tableName := strings.ToLower(parser.IdentName(stmt.Table)) tname := dax.TableName(tableName) tbl, err := p.schemaAPI.TableByName(ctx, tname) if err != nil { if isTableNotFoundError(err) { return sql3.NewErrTableNotFound(stmt.Table.NamePos.Line, stmt.Table.NamePos.Column, tableName) } return err } // check source // source should be literal and a string if !(stmt.DataSource.IsLiteral() && typeIsString(stmt.DataSource.DataType())) { return sql3.NewErrStringLiteral(stmt.DataSource.Pos().Line, stmt.DataSource.Pos().Column) } // check options // check we have format specifier if stmt.Format == nil { return sql3.NewErrFormatSpecifierExpected(stmt.With.Line, stmt.With.Column) } // format should be literal and a string if !(stmt.Format.IsLiteral() && typeIsString(stmt.Format.DataType())) { return sql3.NewErrStringLiteral(stmt.Format.Pos().Line, stmt.Format.Pos().Column) } format, ok := stmt.Format.(*parser.StringLit) if !ok { return sql3.NewErrStringLiteral(stmt.Format.Pos().Line, stmt.Format.Pos().Column) } // check map and other correctness per format switch strings.ToUpper(format.Value) { case "CSV": // for csv the map expressions need to be integer values // that represent the offsets in the source file for _, im := range stmt.MapList { if !(im.MapExpr.IsLiteral() && typeIsInteger(im.MapExpr.DataType())) { return sql3.NewErrIntegerLiteral(im.MapExpr.Pos().Line, im.MapExpr.Pos().Column) } } case "PARQUET": // for parquet the map expressions need to be string values // that represent the offsets in the source file for _, im := range stmt.MapList { if !(im.MapExpr.IsLiteral() && typeIsString(im.MapExpr.DataType())) { return sql3.NewErrStringLiteral(im.MapExpr.Pos().Line, im.MapExpr.Pos().Column) } } case "NDJSON": // for ndjson the map expressions need to be string values // that represent json path expressions for _, im := range stmt.MapList { if !(im.MapExpr.IsLiteral() && typeIsString(im.MapExpr.DataType())) { return sql3.NewErrStringLiteral(im.MapExpr.Pos().Line, im.MapExpr.Pos().Column) } } default: return sql3.NewErrInvalidFormatSpecifier(stmt.Format.Pos().Line, stmt.Format.Pos().Column, format.Value) } // check we have input specifier if stmt.Input == nil { return sql3.NewErrInputSpecifierExpected(stmt.With.Line, stmt.With.Column) } // input should be literal and a string if !(stmt.Input.IsLiteral() && typeIsString(stmt.Input.DataType())) { return sql3.NewErrStringLiteral(stmt.Input.Pos().Line, stmt.Input.Pos().Column) } // input specifier either FILE or URL input, ok := stmt.Input.(*parser.StringLit) if !ok { return sql3.NewErrStringLiteral(stmt.Input.Pos().Line, stmt.Input.Pos().Column) } if !(strings.EqualFold(input.Value, "FILE") || strings.EqualFold(input.Value, "URL") || strings.EqualFold(input.Value, "STREAM")) { return sql3.NewErrInvalidInputSpecifier(stmt.Input.Pos().Line, stmt.Input.Pos().Column, input.Value) } // batch size should default to 1000 if stmt.BatchSize == nil { stmt.BatchSize = &parser.IntegerLit{ Value: "1000", } } // batch size should be literal and an int if !(stmt.BatchSize.IsLiteral() && typeIsInteger(stmt.BatchSize.DataType())) { return sql3.NewErrIntegerLiteral(stmt.BatchSize.Pos().Line, stmt.BatchSize.Pos().Column) } // check batch size > 0 literal, ok := stmt.BatchSize.(*parser.IntegerLit) if !ok { return sql3.NewErrIntegerLiteral(stmt.BatchSize.Pos().Line, stmt.BatchSize.Pos().Column) } i, err := strconv.ParseInt(literal.Value, 10, 64) if err != nil { return err } if i == 0 { return sql3.NewErrInvalidBatchSize(stmt.BatchSize.Pos().Line, stmt.BatchSize.Pos().Column, int(i)) } // rowslimit should default to 0 if stmt.RowsLimit == nil { stmt.RowsLimit = &parser.IntegerLit{ Value: "0", } } // rowslimit should be literal and an int if !(stmt.RowsLimit.IsLiteral() && typeIsInteger(stmt.RowsLimit.DataType())) { return sql3.NewErrIntegerLiteral(stmt.RowsLimit.Pos().Line, stmt.RowsLimit.Pos().Column) } // header row is true if specified, false if not stmt.HeaderRow = &parser.BoolLit{ Value: stmt.HeaderRow != nil, } // allow missing values is true if specified, false if not stmt.AllowMissingValues = &parser.BoolLit{ Value: stmt.AllowMissingValues != nil, } // analyze map expressions for i, m := range stmt.MapList { typeName := parser.IdentName(m.Type.Name) if !parser.IsValidTypeName(typeName) { return sql3.NewErrUnknownType(m.Type.Name.NamePos.Line, m.Type.Name.NamePos.Column, typeName) } ex, err := p.analyzeExpression(ctx, m.MapExpr, stmt) if err != nil { return err } stmt.MapList[i].MapExpr = ex } // check columns if stmt.Columns == nil { // we didn't get any columns so the column list is implictly // the column list of the table referenced stmt.Columns = []*parser.Ident{} for _, fld := range tbl.Fields { stmt.Columns = append(stmt.Columns, &parser.Ident{ NamePos: parser.Pos{Line: 0, Column: 0}, Name: string(fld.Name), }) } } // check map count is the same as target column count if there are no transforms if stmt.TransformList == nil { if len(stmt.Columns) != len(stmt.MapList) { return sql3.NewErrInsertExprTargetCountMismatch(stmt.MapRparen.Line, stmt.MapRparen.Column) } } // analyze transform expressions if stmt.TransformList != nil { // check transform count is the same as target column count if there are transforms if len(stmt.Columns) != len(stmt.TransformList) { return sql3.NewErrInsertExprTargetCountMismatch(stmt.TransformRparen.Line, stmt.TransformRparen.Column) } for i, t := range stmt.TransformList { ex, err := p.analyzeExpression(ctx, t, stmt) if err != nil { return err } stmt.TransformList[i] = ex } } // check columns being inserted to are actual columns and that one of them is the _id column // also do type checking, and check there are no dupes columnNameMap := make(map[string]struct{}) foundID := false for idx, cm := range stmt.Columns { found := false for _, fld := range tbl.Fields { if strings.EqualFold(cm.Name, string(fld.Name)) { found = true colDataType := fieldSQLDataType(pilosa.FieldToFieldInfo(fld)) // if we have transforms check that type and target colum ref are assignment compatible // else check that the map expressions type and target column ref are assignment compatible if stmt.TransformList != nil { t := stmt.TransformList[idx] if !typesAreAssignmentCompatible(colDataType, t.DataType()) { return sql3.NewErrTypeAssignmentIncompatible(t.Pos().Line, t.Pos().Column, t.DataType().TypeDescription(), colDataType.TypeDescription()) } } else { // this assumes that map and col list have already been checked for length me := stmt.MapList[idx] t, err := dataTypeFromParserType(me.Type) if err != nil { return err } if !typesAreAssignmentCompatible(colDataType, t) { return sql3.NewErrTypeAssignmentIncompatible(me.MapExpr.Pos().Line, me.MapExpr.Pos().Column, t.TypeDescription(), colDataType.TypeDescription()) } } break } } if !found { return sql3.NewErrColumnNotFound(cm.NamePos.Line, cm.NamePos.Line, cm.Name) } // Ensure the column name hasn't already appeared in the list of // columns. colName := strings.ToLower(cm.Name) if _, found := columnNameMap[colName]; found { return sql3.NewErrDuplicateColumn(cm.NamePos.Line, cm.NamePos.Column, colName) } columnNameMap[colName] = struct{}{} if strings.EqualFold(cm.Name, string(dax.PrimaryKeyFieldName)) { foundID = true } } if !foundID { return sql3.NewErrInsertMustHaveIDColumn(stmt.ColumnsRparen.Line, stmt.ColumnsRparen.Column) } // check we have columns other than just _id if len(stmt.Columns) < 2 { return sql3.NewErrInsertMustAtLeastOneNonIDColumn(stmt.ColumnsLparen.Line, stmt.ColumnsLparen.Column) } return nil }