// Copyright 2022 Molecula Corp. All rights reserved. package planner import ( "context" "strings" "github.com/featurebasedb/featurebase/v3/sql3" "github.com/featurebasedb/featurebase/v3/sql3/parser" "github.com/featurebasedb/featurebase/v3/sql3/planner/types" ) // TODO (pok) what does 'if not exists' do? // compileCreateModelStatement compiles a parser.CreateModelStatement AST into a PlanOperator func (p *ExecutionPlanner) compileCreateModelStatement(stmt *parser.CreateModelStatement) (types.PlanOperator, error) { modelName := parser.IdentName(stmt.Name) // does the model exist obj, err := p.getModelByName(modelName) if err != nil { return nil, err } if obj != nil { return nil, sql3.NewErrInternalf("model '%s' already exists", modelName) } // if we got to here model does not exist model := &modelSystemObject{ name: modelName, } for _, o := range stmt.Options { optName := parser.IdentName(o.Name) switch strings.ToLower(optName) { case "modeltype": lit, ok := o.OptionExpr.(*parser.StringLit) if !ok { return nil, sql3.NewErrInternalf("unexpected type '%T'", o.OptionExpr) } model.modelType = lit.Value case "labels": lit, ok := o.OptionExpr.(*parser.SetLiteralExpr) if !ok { return nil, sql3.NewErrInternalf("unexpected type '%T'", o.OptionExpr) } model.labels = make([]string, len(lit.Members)) for i, m := range lit.Members { mlit, ok := m.(*parser.StringLit) if !ok { return nil, sql3.NewErrInternalf("unexpected type '%T'", m) } model.labels[i] = mlit.Value } default: return nil, sql3.NewErrInternalf("unexpected model option '%s'", optName) } } selOp, err := p.compileSelectStatement(stmt.ModelQuery, true) if err != nil { return nil, err } // build a list of input columns for the model from the select query schema := selOp.Schema() model.inputColumns = make([]string, 0) for _, p := range schema { // if we have no column name, we have an error if len(p.ColumnName) == 0 { return nil, sql3.NewErrInternalf("query output columns used as inputs to models must be named") } // exclude any that are in the labels isLabel := false for _, l := range model.labels { if strings.EqualFold(p.ColumnName, l) { isLabel = true break } } if !isLabel { model.inputColumns = append(model.inputColumns, p.ColumnName) } } createModel := NewPlanOpCreateModel(p, model, selOp) createModel.AddWarning("🦖 here there be dragons! CREATE MODEL statement is experimental.") query := NewPlanOpQuery(p, createModel, p.sql) return query, nil } func (p *ExecutionPlanner) analyzeCreateModelStatement(ctx context.Context, stmt *parser.CreateModelStatement) error { // iterate the options for _, opt := range stmt.Options { optName := parser.IdentName(opt.Name) if !isValidModelOption(optName) { return sql3.NewErrInternalf("invalid model option '%s'", optName) } e, err := p.analyzeModelOptionExpr(ctx, optName, opt.OptionExpr, stmt) if err != nil { return err } opt.OptionExpr = e } // analyze the select _, err := p.analyzeSelectStatement(ctx, stmt.ModelQuery) if err != nil { return err } return nil } func isValidModelOption(name string) bool { switch strings.ToLower(name) { case "modeltype": return true case "labels": return true default: return false } } func (p *ExecutionPlanner) analyzeModelOptionExpr(ctx context.Context, optName string, expr parser.Expr, scope parser.Statement) (parser.Expr, error) { if expr == nil { return nil, nil } e, err := p.analyzeExpression(ctx, expr, scope) if err != nil { return nil, err } switch strings.ToLower(optName) { case "modeltype": // model type needs to be a string literal if !(e.IsLiteral() && typeIsString(e.DataType())) { return nil, sql3.NewErrStringLiteral(e.Pos().Line, e.Pos().Column) } ty, ok := e.(*parser.StringLit) if !ok { return nil, sql3.NewErrInternalf("unexpected type '%T'", e) } // these are the model types supported switch strings.ToLower(ty.Value) { case "linear_regresssion": break default: return nil, sql3.NewErrInternalf("unexpected model tyoe '%s'", ty.Value) } return e, nil case "labels": // labels needs to be a string array literal // TODO (pok) revist 'set' literals (should be array literal; type checking could be robustified etc.) if !e.IsLiteral() { return nil, sql3.NewErrInternalf("string array literal expected") } ok, baseType := typeIsSet(e.DataType()) if !ok { return nil, sql3.NewErrInternalf("array expression expected") } if !typeIsString(baseType) { return nil, sql3.NewErrInternalf("string array expected") } return e, nil default: return nil, sql3.NewErrInternalf("unexpected option name '%s'", optName) } }