mirror of
https://github.com/featurebasedb/featurebase.git
synced 2026-08-28 10:54:59 +00:00
* create function, create/drop model; re-introduced limit; added COPY; var(); corr() * review feedback
180 lines
4.7 KiB
Go
180 lines
4.7 KiB
Go
// 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)
|
|
}
|
|
}
|