From cec73a6c35008ad99512c982dcba16d2e8f642b5 Mon Sep 17 00:00:00 2001 From: Asdine El Hrychy Date: Sun, 1 Mar 2020 21:52:11 +0100 Subject: [PATCH] Parse not null and primary key --- database/config.go | 20 +++++++++--- database/config_test.go | 5 ++- database/table.go | 24 ++++++++------ database/table_test.go | 20 ++++++------ sql/parser/create.go | 67 ++++++++++++++++++++++++++++++--------- sql/parser/create_test.go | 61 ++++++++++++++++++++++++++++++++--- sql/parser/expr.go | 35 ++++++++++---------- sql/query/create_test.go | 8 ++--- sql/query/expr.go | 5 +-- sql/query/plan.go | 13 +++++--- 10 files changed, 183 insertions(+), 75 deletions(-) diff --git a/database/config.go b/database/config.go index a53fb0f40..6e8058e27 100644 --- a/database/config.go +++ b/database/config.go @@ -9,17 +9,29 @@ import ( // TableConfig holds the configuration of a table type TableConfig struct { - PrimaryKey FieldConstraint FieldConstraints []FieldConstraint LastKey int64 } +// GetPrimaryKey returns the field constraint of the primary key. +// Returns nil if there is no primary key. +func (t TableConfig) GetPrimaryKey() *FieldConstraint { + for _, f := range t.FieldConstraints { + if f.IsPrimaryKey { + return &f + } + } + + return nil +} + // FieldConstraint describes constraints on a particular field. type FieldConstraint struct { - Path document.ValuePath - Type document.ValueType - NotNull bool + Path document.ValuePath + Type document.ValueType + IsPrimaryKey bool + IsNotNull bool } type tableConfigStore struct { diff --git a/database/config_test.go b/database/config_test.go index bf615602b..9962ff1ac 100644 --- a/database/config_test.go +++ b/database/config_test.go @@ -24,9 +24,8 @@ func TestTableConfigStore(t *testing.T) { tcs := tableConfigStore{st} cfg := TableConfig{ - PrimaryKey: FieldConstraint{ - Path: []string{"k"}, - Type: document.Float64Value, + FieldConstraints: []FieldConstraint{ + {Path: []string{"k"}, Type: document.Float64Value, IsPrimaryKey: true}, }, LastKey: 100, } diff --git a/database/table.go b/database/table.go index 8663c6474..f924c7887 100644 --- a/database/table.go +++ b/database/table.go @@ -77,10 +77,10 @@ func (t *Table) generateKey(d document.Document) ([]byte, error) { } var key []byte - if len(cfg.PrimaryKey.Path) != 0 { - v, err := cfg.PrimaryKey.Path.GetValue(d) + if pk := cfg.GetPrimaryKey(); pk != nil { + v, err := pk.Path.GetValue(d) if err == document.ErrFieldNotFound { - return nil, fmt.Errorf("missing primary key at path %q", cfg.PrimaryKey.Path) + return nil, fmt.Errorf("missing primary key at path %q", pk.Path) } if err != nil { return nil, err @@ -129,7 +129,9 @@ func (t *Table) validateConstraints(d document.Document) (document.Document, err return nil, err } - if len(cfg.FieldConstraints) == 0 && len(cfg.PrimaryKey.Path) == 0 { + pk := cfg.GetPrimaryKey() + + if len(cfg.FieldConstraints) == 0 && pk == nil { return d, nil } @@ -142,15 +144,15 @@ func (t *Table) validateConstraints(d document.Document) (document.Document, err return nil, err } - if len(cfg.PrimaryKey.Path) != 0 { - err = validateConstraint(&fb, cfg.PrimaryKey) + if pk != nil { + err = validateConstraint(&fb, pk) if err != nil { return nil, err } } for _, fc := range cfg.FieldConstraints { - err := validateConstraint(&fb, fc) + err := validateConstraint(&fb, &fc) if err != nil { return nil, err } @@ -159,7 +161,7 @@ func (t *Table) validateConstraints(d document.Document) (document.Document, err return &fb, err } -func validateConstraint(d document.Document, c FieldConstraint) error { +func validateConstraint(d document.Document, c *FieldConstraint) error { // get the parent buffer parent, err := getParentValue(d, c.Path) if err != nil { @@ -178,7 +180,7 @@ func validateConstraint(d document.Document, c FieldConstraint) error { // if the field is not found we make sure it is not required, if err != nil { if err == document.ErrFieldNotFound { - if c.NotNull { + if c.IsNotNull { return fmt.Errorf("field %q is required and must be not null", c.Path) } @@ -189,6 +191,8 @@ func validateConstraint(d document.Document, c FieldConstraint) error { } // if not we convert it and replace it in the buffer + + // if no type was provided, no need to convert though if c.Type == 0 { return nil } @@ -218,7 +222,7 @@ func validateConstraint(d document.Document, c FieldConstraint) error { // if the value is not found we make sure it is not required, if err != nil { if err == document.ErrValueNotFound { - if c.NotNull { + if c.IsNotNull { return fmt.Errorf("value %q is required and must be not null", c.Path) } diff --git a/database/table_test.go b/database/table_test.go index eb6535cc7..05612e19d 100644 --- a/database/table_test.go +++ b/database/table_test.go @@ -127,9 +127,8 @@ func TestTableInsert(t *testing.T) { defer cleanup() err := tx.CreateTable("test", &database.TableConfig{ - PrimaryKey: database.FieldConstraint{ - Path: []string{"foo", "a", "1"}, - Type: document.Int32Value, + FieldConstraints: []database.FieldConstraint{ + {Path: []string{"foo", "a", "1"}, Type: document.Int32Value, IsPrimaryKey: true}, }, }) require.NoError(t, err) @@ -189,9 +188,8 @@ func TestTableInsert(t *testing.T) { defer cleanup() err := tx.CreateTable("test", &database.TableConfig{ - PrimaryKey: database.FieldConstraint{ - Path: []string{"foo"}, - Type: document.Int32Value, + FieldConstraints: []database.FieldConstraint{ + {Path: []string{"foo"}, Type: document.Int32Value, IsPrimaryKey: true}, }, }) require.NoError(t, err) @@ -268,8 +266,8 @@ func TestTableInsert(t *testing.T) { err := tx.CreateTable("test", &database.TableConfig{ FieldConstraints: []database.FieldConstraint{ - {[]string{"foo"}, document.Int32Value, false}, - {[]string{"bar"}, document.Int8Value, false}, + {[]string{"foo"}, document.Int32Value, false, false}, + {[]string{"bar"}, document.Int8Value, false, false}, }, }) require.NoError(t, err) @@ -305,7 +303,7 @@ func TestTableInsert(t *testing.T) { err := tx.CreateTable("test1", &database.TableConfig{ FieldConstraints: []database.FieldConstraint{ - {[]string{"foo"}, 0, true}, + {[]string{"foo"}, 0, false, true}, }, }) require.NoError(t, err) @@ -314,7 +312,7 @@ func TestTableInsert(t *testing.T) { err = tx.CreateTable("test2", &database.TableConfig{ FieldConstraints: []database.FieldConstraint{ - {[]string{"foo"}, document.Int32Value, true}, + {[]string{"foo"}, document.Int32Value, false, true}, }, }) require.NoError(t, err) @@ -343,7 +341,7 @@ func TestTableInsert(t *testing.T) { err := tx.CreateTable("test1", &database.TableConfig{ FieldConstraints: []database.FieldConstraint{ - {[]string{"foo", "1"}, 0, true}, + {[]string{"foo", "1"}, 0, false, true}, }, }) require.NoError(t, err) diff --git a/sql/parser/create.go b/sql/parser/create.go index f753588fe..961321160 100644 --- a/sql/parser/create.go +++ b/sql/parser/create.go @@ -1,6 +1,8 @@ package parser import ( + "fmt" + "github.com/asdine/genji/database" "github.com/asdine/genji/sql/query" "github.com/asdine/genji/sql/scanner" @@ -92,25 +94,14 @@ func (p *Parser) parseTableConfig(cfg *database.TableConfig) error { break } - fc.Type, err = p.parseType() + fc.Type = p.parseType() + + err = p.parseFieldConstraint(&fc) if err != nil { return err } - // Parse "PRIMARY" - if tok, _, _ := p.ScanIgnoreWhitespace(); tok == scanner.PRIMARY { - // Parse "KEY" - if tok, pos, lit := p.ScanIgnoreWhitespace(); tok != scanner.KEY { - return newParseError(scanner.Tokstr(tok, lit), []string{"KEY"}, pos) - } - if len(cfg.PrimaryKey.Path) != 0 { - return &ParseError{Message: "only one primary key is allowed"} - } - cfg.PrimaryKey = fc - } else { - p.Unscan() - cfg.FieldConstraints = append(cfg.FieldConstraints, fc) - } + cfg.FieldConstraints = append(cfg.FieldConstraints, fc) if tok, _, _ := p.ScanIgnoreWhitespace(); tok != scanner.COMMA { p.Unscan() @@ -123,9 +114,55 @@ func (p *Parser) parseTableConfig(cfg *database.TableConfig) error { return newParseError(scanner.Tokstr(tok, lit), []string{")"}, pos) } + // ensure only one primary key + var pkCount int + for _, fc := range cfg.FieldConstraints { + if fc.IsPrimaryKey { + pkCount++ + } + } + if pkCount > 1 { + return &ParseError{Message: fmt.Sprintf("only one primary key is allowed, got %d", pkCount)} + } + return nil } +func (p *Parser) parseFieldConstraint(fc *database.FieldConstraint) error { + for { + tok, pos, lit := p.ScanIgnoreWhitespace() + switch tok { + case scanner.PRIMARY: + // Parse "KEY" + if tok, pos, lit := p.ScanIgnoreWhitespace(); tok != scanner.KEY { + return newParseError(scanner.Tokstr(tok, lit), []string{"KEY"}, pos) + } + + // if it's already a primary key we return an error + if fc.IsPrimaryKey { + return newParseError(scanner.Tokstr(tok, lit), []string{"CONSTRAINT", ")"}, pos) + } + + fc.IsPrimaryKey = true + case scanner.NOT: + // Parse "NULL" + if tok, pos, lit := p.ScanIgnoreWhitespace(); tok != scanner.NULL { + return newParseError(scanner.Tokstr(tok, lit), []string{"NULL"}, pos) + } + + // if it's already not null we return an error + if fc.IsNotNull { + return newParseError(scanner.Tokstr(tok, lit), []string{"CONSTRAINT", ")"}, pos) + } + + fc.IsNotNull = true + default: + p.Unscan() + return nil + } + } +} + // parseCreateIndexStatement parses a create index string and returns a Statement AST object. // This function assumes the CREATE INDEX or CREATE UNIQUE INDEX tokens have already been consumed. func (p *Parser) parseCreateIndexStatement(unique bool) (query.CreateIndexStmt, error) { diff --git a/sql/parser/create_test.go b/sql/parser/create_test.go index cc5132cb0..f652e2402 100644 --- a/sql/parser/create_test.go +++ b/sql/parser/create_test.go @@ -22,20 +22,73 @@ func TestParserCreateTable(t *testing.T) { query.CreateTableStmt{ TableName: "test", Config: database.TableConfig{ - PrimaryKey: database.FieldConstraint{Path: []string{"foo"}, Type: document.Int64Value}, + FieldConstraints: []database.FieldConstraint{ + {Path: []string{"foo"}, Type: document.Int64Value, IsPrimaryKey: true}, + }, + }, + }, false}, + {"With primary key twice", "CREATE TABLE test(foo PRIMARY KEY PRIMARY KEY)", + query.CreateTableStmt{}, true}, + {"With type", "CREATE TABLE test(foo INT)", + query.CreateTableStmt{ + TableName: "test", + Config: database.TableConfig{ + FieldConstraints: []database.FieldConstraint{ + {Path: []string{"foo"}, Type: document.Int64Value}, + }, + }, + }, false}, + {"With not null", "CREATE TABLE test(foo NOT NULL)", + query.CreateTableStmt{ + TableName: "test", + Config: database.TableConfig{ + FieldConstraints: []database.FieldConstraint{ + {Path: []string{"foo"}, IsNotNull: true}, + }, + }, + }, false}, + {"With not null twice", "CREATE TABLE test(foo NOT NULL NOT NULL)", + query.CreateTableStmt{}, true}, + {"With type and not null", "CREATE TABLE test(foo INT NOT NULL)", + query.CreateTableStmt{ + TableName: "test", + Config: database.TableConfig{ + FieldConstraints: []database.FieldConstraint{ + {Path: []string{"foo"}, Type: document.Int64Value, IsNotNull: true}, + }, + }, + }, false}, + {"With not null and primary key", "CREATE TABLE test(foo INT NOT NULL PRIMARY KEY)", + query.CreateTableStmt{ + TableName: "test", + Config: database.TableConfig{ + FieldConstraints: []database.FieldConstraint{ + {Path: []string{"foo"}, Type: document.Int64Value, IsPrimaryKey: true, IsNotNull: true}, + }, + }, + }, false}, + {"With primary key and not null", "CREATE TABLE test(foo INT PRIMARY KEY NOT NULL)", + query.CreateTableStmt{ + TableName: "test", + Config: database.TableConfig{ + FieldConstraints: []database.FieldConstraint{ + {Path: []string{"foo"}, Type: document.Int64Value, IsPrimaryKey: true, IsNotNull: true}, + }, }, }, false}, - {"With multiple constraints", "CREATE TABLE test(foo INT PRIMARY KEY, bar INT16, baz.4.1.bat STRING)", + {"With multiple constraints", "CREATE TABLE test(foo INT PRIMARY KEY, bar INT16 NOT NULL, baz.4.1.bat STRING)", query.CreateTableStmt{ TableName: "test", Config: database.TableConfig{ - PrimaryKey: database.FieldConstraint{Path: []string{"foo"}, Type: document.Int64Value}, FieldConstraints: []database.FieldConstraint{ - {Path: []string{"bar"}, Type: document.Int16Value}, + {Path: []string{"foo"}, Type: document.Int64Value, IsPrimaryKey: true}, + {Path: []string{"bar"}, Type: document.Int16Value, IsNotNull: true}, {Path: []string{"baz", "4", "1", "bat"}, Type: document.TextValue}, }, }, }, false}, + {"With multiple primary keys", "CREATE TABLE test(foo PRIMARY KEY, bar PRIMARY KEY)", + query.CreateTableStmt{}, true}, } for _, test := range tests { diff --git a/sql/parser/expr.go b/sql/parser/expr.go index 097278d95..8e47056e8 100644 --- a/sql/parser/expr.go +++ b/sql/parser/expr.go @@ -249,32 +249,33 @@ func (p *Parser) parseParam() (query.Expr, error) { } } -func (p *Parser) parseType() (document.ValueType, error) { - tok, pos, lit := p.ScanIgnoreWhitespace() +func (p *Parser) parseType() document.ValueType { + tok, _, _ := p.ScanIgnoreWhitespace() switch tok { case scanner.TYPEBYTES: - return document.BlobValue, nil + return document.BlobValue case scanner.TYPESTRING: - return document.TextValue, nil + return document.TextValue case scanner.TYPEBOOL: - return document.BoolValue, nil + return document.BoolValue case scanner.TYPEINT8: - return document.Int8Value, nil + return document.Int8Value case scanner.TYPEINT16: - return document.Int16Value, nil + return document.Int16Value case scanner.TYPEINT32: - return document.Int32Value, nil + return document.Int32Value case scanner.TYPEINT64, scanner.TYPEINT, scanner.TYPEINTEGER: - return document.Int64Value, nil + return document.Int64Value case scanner.TYPEFLOAT64, scanner.TYPENUMERIC: - return document.Float64Value, nil + return document.Float64Value case scanner.TYPETEXT: - return document.TextValue, nil + return document.TextValue case scanner.TYPEDURATION: - return document.DurationValue, nil + return document.DurationValue } - return 0, newParseError(scanner.Tokstr(tok, lit), []string{"type"}, pos) + p.Unscan() + return 0 } // parseDocument parses a document @@ -479,9 +480,11 @@ func (p *Parser) parseCastExpression() (query.Expr, error) { } // Parse require typename. - tp, err := p.parseType() - if err != nil { - return nil, err + tp := p.parseType() + if tp == 0 { + tok, pos, lit := p.ScanIgnoreWhitespace() + p.Unscan() + return nil, newParseError(scanner.Tokstr(tok, lit), []string{"type"}, pos) } // Parse required ) token. diff --git a/sql/query/create_test.go b/sql/query/create_test.go index 78b70e9c2..f32b3a47a 100644 --- a/sql/query/create_test.go +++ b/sql/query/create_test.go @@ -20,7 +20,8 @@ func TestCreateTable(t *testing.T) { {"If not exists", "CREATE TABLE IF NOT EXISTS test", false}, {"If not exists, twice", "CREATE TABLE IF NOT EXISTS test;CREATE TABLE IF NOT EXISTS test", false}, {"With primary key", "CREATE TABLE test(foo STRING PRIMARY KEY)", false}, - {"With field constraints key", "CREATE TABLE test(foo.a.1.2 STRING, bar.4.0.bat int8)", false}, + {"With field constraints", "CREATE TABLE test(foo.a.1.2 STRING primary key, bar.4.0.bat int8 not null, baz not null)", false}, + {"With no constraints", "CREATE TABLE test(a, b)", false}, } for _, test := range tests { @@ -58,11 +59,8 @@ func TestCreateTable(t *testing.T) { } require.Equal(t, &database.TableConfig{ - PrimaryKey: database.FieldConstraint{ - Path: []string{"foo", "bar", "1", "hello"}, - Type: document.BlobValue, - }, FieldConstraints: []database.FieldConstraint{ + {Path: []string{"foo", "bar", "1", "hello"}, Type: document.BlobValue, IsPrimaryKey: true}, {Path: []string{"foo", "a", "1", "2"}, Type: document.TextValue}, {Path: []string{"bar", "4", "0", "bat"}, Type: document.Int8Value}, }, diff --git a/sql/query/expr.go b/sql/query/expr.go index 496c9f7c3..76111a61d 100644 --- a/sql/query/expr.go +++ b/sql/query/expr.go @@ -593,8 +593,9 @@ func (k PKFunc) Eval(ctx EvalStack) (document.Value, error) { return document.Value{}, errors.New("no table specified") } - if len(ctx.Cfg.PrimaryKey.Path) != 0 { - return ctx.Cfg.PrimaryKey.Path.GetValue(ctx.Document) + pk := ctx.Cfg.GetPrimaryKey() + if pk != nil { + return pk.Path.GetValue(ctx.Document) } return encoding.DecodeValue(document.Int64Value, ctx.Document.(document.Keyer).Key()) diff --git a/sql/query/plan.go b/sql/query/plan.go index 07c0cce3d..9b6bf42be 100644 --- a/sql/query/plan.go +++ b/sql/query/plan.go @@ -97,7 +97,7 @@ func (qo *queryOptimizer) optimizeQuery() (st document.Stream, err error) { return } - v, err := v.ConvertTo(qo.cfg.PrimaryKey.Type) + v, err := v.ConvertTo(qo.cfg.GetPrimaryKey().Type) if err != nil { st = document.NewStream(qo.t) break @@ -144,10 +144,11 @@ func (qo *queryOptimizer) buildQueryPlan() queryPlan { if qp.field == nil { if len(qo.orderBy) != 0 { _, ok := qo.indexes[qo.orderBy.Name()] - if ok || qo.cfg.PrimaryKey.Path.String() == qo.orderBy.Name() { + pk := qo.cfg.GetPrimaryKey() + if ok || pk.Path.String() == qo.orderBy.Name() { qp.field = &queryPlanField{ indexedField: qo.orderBy, - isPrimaryKey: qo.cfg.PrimaryKey.Path.String() == qo.orderBy.Name(), + isPrimaryKey: pk.Path.String() == qo.orderBy.Name(), } qp.sorted = true @@ -183,7 +184,7 @@ func (qo *queryOptimizer) analyseExpr(e Expr) *queryPlanField { } } - if qo.cfg.PrimaryKey.Path.String() == fs.Name() { + if qo.cfg.GetPrimaryKey().Path.String() == fs.Name() { return &queryPlanField{ indexedField: fs, op: t.Token, @@ -588,4 +589,6 @@ type maxHeap struct { minHeap } -func (h maxHeap) Less(i, j int) bool { return bytes.Compare(h.minHeap[i].value, h.minHeap[j].value) > 0 } +func (h maxHeap) Less(i, j int) bool { + return bytes.Compare(h.minHeap[i].value, h.minHeap[j].value) > 0 +}