mirror of
https://github.com/chaisql/chai.git
synced 2025-11-03 01:33:31 +08:00
Add support for CHECK (#436)
This commit is contained in:
@@ -137,7 +137,7 @@ func TestDumpSchema(t *testing.T) {
|
|||||||
writeToBuf("\n")
|
writeToBuf("\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
q := fmt.Sprintf("CREATE TABLE %s (a INTEGER UNIQUE);", table)
|
q := fmt.Sprintf("CREATE TABLE %s (a INTEGER, UNIQUE (a));", table)
|
||||||
err = db.Exec(q)
|
err = db.Exec(q)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
writeToBuf(q + "\n")
|
writeToBuf(q + "\n")
|
||||||
|
|||||||
@@ -338,7 +338,7 @@ func (c *Catalog) dropIndex(tx *Transaction, name string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// AddFieldConstraint adds a field constraint to a table.
|
// AddFieldConstraint adds a field constraint to a table.
|
||||||
func (c *Catalog) AddFieldConstraint(tx *Transaction, tableName string, fc FieldConstraint) error {
|
func (c *Catalog) AddFieldConstraint(tx *Transaction, tableName string, fc *FieldConstraint, tcs TableConstraints) error {
|
||||||
r, err := c.Cache.Get(RelationTableType, tableName)
|
r, err := c.Cache.Get(RelationTableType, tableName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -346,7 +346,14 @@ func (c *Catalog) AddFieldConstraint(tx *Transaction, tableName string, fc Field
|
|||||||
ti := r.(*TableInfo)
|
ti := r.(*TableInfo)
|
||||||
|
|
||||||
clone := ti.Clone()
|
clone := ti.Clone()
|
||||||
err = clone.FieldConstraints.Add(&fc)
|
if fc != nil {
|
||||||
|
err = clone.FieldConstraints.Add(fc)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = clone.TableConstraints.Merge(tcs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -761,6 +768,16 @@ func newCatalogStore() *CatalogStore {
|
|||||||
info: &TableInfo{
|
info: &TableInfo{
|
||||||
TableName: TableName,
|
TableName: TableName,
|
||||||
StoreName: []byte(TableName),
|
StoreName: []byte(TableName),
|
||||||
|
TableConstraints: []*TableConstraint{
|
||||||
|
{
|
||||||
|
PrimaryKey: true,
|
||||||
|
Path: document.Path{
|
||||||
|
document.PathFragment{
|
||||||
|
FieldName: "name",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
FieldConstraints: []*FieldConstraint{
|
FieldConstraints: []*FieldConstraint{
|
||||||
{
|
{
|
||||||
Path: document.Path{
|
Path: document.Path{
|
||||||
@@ -768,8 +785,7 @@ func newCatalogStore() *CatalogStore {
|
|||||||
FieldName: "name",
|
FieldName: "name",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Type: types.TextValue,
|
Type: types.TextValue,
|
||||||
IsPrimaryKey: true,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Path: document.Path{
|
Path: document.Path{
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
errs "github.com/genjidb/genji/errors"
|
errs "github.com/genjidb/genji/errors"
|
||||||
"github.com/genjidb/genji/internal/database"
|
"github.com/genjidb/genji/internal/database"
|
||||||
"github.com/genjidb/genji/internal/errors"
|
"github.com/genjidb/genji/internal/errors"
|
||||||
|
"github.com/genjidb/genji/internal/expr"
|
||||||
"github.com/genjidb/genji/internal/testutil"
|
"github.com/genjidb/genji/internal/testutil"
|
||||||
"github.com/genjidb/genji/internal/testutil/assert"
|
"github.com/genjidb/genji/internal/testutil/assert"
|
||||||
"github.com/genjidb/genji/types"
|
"github.com/genjidb/genji/types"
|
||||||
@@ -112,12 +113,15 @@ func TestCatalogTable(t *testing.T) {
|
|||||||
db, cleanup := testutil.NewTestDB(t)
|
db, cleanup := testutil.NewTestDB(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
ti := &database.TableInfo{FieldConstraints: []*database.FieldConstraint{
|
ti := &database.TableInfo{
|
||||||
{Path: testutil.ParseDocumentPath(t, "name"), Type: types.TextValue, IsNotNull: true},
|
FieldConstraints: []*database.FieldConstraint{
|
||||||
{Path: testutil.ParseDocumentPath(t, "age"), Type: types.IntegerValue, IsPrimaryKey: true},
|
{Path: testutil.ParseDocumentPath(t, "name"), Type: types.TextValue, IsNotNull: true},
|
||||||
{Path: testutil.ParseDocumentPath(t, "gender"), Type: types.TextValue},
|
{Path: testutil.ParseDocumentPath(t, "age"), Type: types.IntegerValue},
|
||||||
{Path: testutil.ParseDocumentPath(t, "city"), Type: types.TextValue},
|
{Path: testutil.ParseDocumentPath(t, "gender"), Type: types.TextValue},
|
||||||
}}
|
{Path: testutil.ParseDocumentPath(t, "city"), Type: types.TextValue},
|
||||||
|
}, TableConstraints: []*database.TableConstraint{
|
||||||
|
{Path: testutil.ParseDocumentPath(t, "age"), PrimaryKey: true},
|
||||||
|
}}
|
||||||
|
|
||||||
updateCatalog(t, db, func(tx *database.Transaction, catalog *database.Catalog) error {
|
updateCatalog(t, db, func(tx *database.Transaction, catalog *database.Catalog) error {
|
||||||
err := catalog.CreateTable(tx, "foo", ti)
|
err := catalog.CreateTable(tx, "foo", ti)
|
||||||
@@ -194,9 +198,11 @@ func TestCatalogTable(t *testing.T) {
|
|||||||
|
|
||||||
ti := &database.TableInfo{FieldConstraints: []*database.FieldConstraint{
|
ti := &database.TableInfo{FieldConstraints: []*database.FieldConstraint{
|
||||||
{Path: testutil.ParseDocumentPath(t, "name"), Type: types.TextValue, IsNotNull: true},
|
{Path: testutil.ParseDocumentPath(t, "name"), Type: types.TextValue, IsNotNull: true},
|
||||||
{Path: testutil.ParseDocumentPath(t, "age"), Type: types.IntegerValue, IsPrimaryKey: true},
|
{Path: testutil.ParseDocumentPath(t, "age"), Type: types.IntegerValue},
|
||||||
{Path: testutil.ParseDocumentPath(t, "gender"), Type: types.TextValue},
|
{Path: testutil.ParseDocumentPath(t, "gender"), Type: types.TextValue},
|
||||||
{Path: testutil.ParseDocumentPath(t, "city"), Type: types.TextValue},
|
{Path: testutil.ParseDocumentPath(t, "city"), Type: types.TextValue},
|
||||||
|
}, TableConstraints: []*database.TableConstraint{
|
||||||
|
{Path: testutil.ParseDocumentPath(t, "age"), PrimaryKey: true},
|
||||||
}}
|
}}
|
||||||
|
|
||||||
updateCatalog(t, db, func(tx *database.Transaction, catalog *database.Catalog) error {
|
updateCatalog(t, db, func(tx *database.Transaction, catalog *database.Catalog) error {
|
||||||
@@ -211,7 +217,10 @@ func TestCatalogTable(t *testing.T) {
|
|||||||
fieldToAdd := database.FieldConstraint{
|
fieldToAdd := database.FieldConstraint{
|
||||||
Path: testutil.ParseDocumentPath(t, "last_name"), Type: types.TextValue,
|
Path: testutil.ParseDocumentPath(t, "last_name"), Type: types.TextValue,
|
||||||
}
|
}
|
||||||
err := catalog.AddFieldConstraint(tx, "foo", fieldToAdd)
|
// Add table constraint
|
||||||
|
var tcs database.TableConstraints
|
||||||
|
tcs.AddCheck("foo", expr.Constraint(testutil.ParseExpr(t, "last_name > first_name")))
|
||||||
|
err := catalog.AddFieldConstraint(tx, "foo", &fieldToAdd, tcs)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
tb, err := catalog.GetTable(tx, "foo")
|
tb, err := catalog.GetTable(tx, "foo")
|
||||||
@@ -220,22 +229,22 @@ func TestCatalogTable(t *testing.T) {
|
|||||||
// The field constraints should not be the same.
|
// The field constraints should not be the same.
|
||||||
|
|
||||||
require.Contains(t, tb.Info.FieldConstraints, &fieldToAdd)
|
require.Contains(t, tb.Info.FieldConstraints, &fieldToAdd)
|
||||||
|
require.Equal(t, expr.Constraint(testutil.ParseExpr(t, "last_name > first_name")), tb.Info.TableConstraints[1].Check)
|
||||||
|
|
||||||
// Renaming a non existing table should return an error
|
// Renaming a non existing table should return an error
|
||||||
err = catalog.AddFieldConstraint(tx, "bar", fieldToAdd)
|
err = catalog.AddFieldConstraint(tx, "bar", &fieldToAdd, nil)
|
||||||
if !errors.Is(err, errs.NotFoundError{}) {
|
if !errors.Is(err, errs.NotFoundError{}) {
|
||||||
assert.ErrorIs(t, err, errs.NotFoundError{Name: "bar"})
|
assert.ErrorIs(t, err, errs.NotFoundError{Name: "bar"})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Adding a existing field should return an error
|
// Adding a existing field should return an error
|
||||||
err = catalog.AddFieldConstraint(tx, "foo", *ti.FieldConstraints[0])
|
err = catalog.AddFieldConstraint(tx, "foo", ti.FieldConstraints[0], nil)
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
|
|
||||||
// Adding a second primary key should return an error
|
// Adding a second primary key should return an error
|
||||||
fieldToAdd = database.FieldConstraint{
|
tcs = nil
|
||||||
Path: testutil.ParseDocumentPath(t, "foobar"), Type: types.IntegerValue, IsPrimaryKey: true,
|
tcs.AddPrimaryKey("foo", testutil.ParseDocumentPath(t, "address"))
|
||||||
}
|
err = catalog.AddFieldConstraint(tx, "foo", nil, tcs)
|
||||||
err = catalog.AddFieldConstraint(tx, "foo", fieldToAdd)
|
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
|
|
||||||
return errDontCommit
|
return errDontCommit
|
||||||
@@ -311,8 +320,8 @@ func TestCatalogCreateIndex(t *testing.T) {
|
|||||||
|
|
||||||
updateCatalog(t, db, func(tx *database.Transaction, catalog *database.Catalog) error {
|
updateCatalog(t, db, func(tx *database.Transaction, catalog *database.Catalog) error {
|
||||||
err := catalog.CreateTable(tx, "test", &database.TableInfo{
|
err := catalog.CreateTable(tx, "test", &database.TableInfo{
|
||||||
FieldConstraints: database.FieldConstraints{
|
TableConstraints: []*database.TableConstraint{
|
||||||
{Path: testutil.ParseDocumentPath(t, "a"), IsPrimaryKey: true},
|
{Path: testutil.ParseDocumentPath(t, "a"), PrimaryKey: true},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -493,8 +502,8 @@ func TestCatalogReIndex(t *testing.T) {
|
|||||||
|
|
||||||
updateCatalog(t, db, func(tx *database.Transaction, catalog *database.Catalog) error {
|
updateCatalog(t, db, func(tx *database.Transaction, catalog *database.Catalog) error {
|
||||||
err := catalog.CreateTable(tx, "test", &database.TableInfo{
|
err := catalog.CreateTable(tx, "test", &database.TableInfo{
|
||||||
FieldConstraints: database.FieldConstraints{
|
TableConstraints: []*database.TableConstraint{
|
||||||
{Path: testutil.ParseDocumentPath(t, "a"), IsPrimaryKey: true},
|
{Path: testutil.ParseDocumentPath(t, "a"), PrimaryKey: true},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
@@ -545,8 +554,8 @@ func TestCatalogReIndex(t *testing.T) {
|
|||||||
|
|
||||||
updateCatalog(t, db, func(tx *database.Transaction, catalog *database.Catalog) error {
|
updateCatalog(t, db, func(tx *database.Transaction, catalog *database.Catalog) error {
|
||||||
err := catalog.CreateTable(tx, "test", &database.TableInfo{
|
err := catalog.CreateTable(tx, "test", &database.TableInfo{
|
||||||
FieldConstraints: database.FieldConstraints{
|
TableConstraints: []*database.TableConstraint{
|
||||||
{Path: testutil.ParseDocumentPath(t, "a"), IsPrimaryKey: true},
|
{Path: testutil.ParseDocumentPath(t, "a"), PrimaryKey: true},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
@@ -640,8 +649,8 @@ func TestReIndexAll(t *testing.T) {
|
|||||||
|
|
||||||
updateCatalog(t, db, func(tx *database.Transaction, catalog *database.Catalog) error {
|
updateCatalog(t, db, func(tx *database.Transaction, catalog *database.Catalog) error {
|
||||||
err := catalog.CreateTable(tx, "test1", &database.TableInfo{
|
err := catalog.CreateTable(tx, "test1", &database.TableInfo{
|
||||||
FieldConstraints: database.FieldConstraints{
|
TableConstraints: []*database.TableConstraint{
|
||||||
{Path: testutil.ParseDocumentPath(t, "a"), IsPrimaryKey: true},
|
{Path: testutil.ParseDocumentPath(t, "a"), PrimaryKey: true},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
@@ -649,8 +658,8 @@ func TestReIndexAll(t *testing.T) {
|
|||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
err = catalog.CreateTable(tx, "test2", &database.TableInfo{
|
err = catalog.CreateTable(tx, "test2", &database.TableInfo{
|
||||||
FieldConstraints: database.FieldConstraints{
|
TableConstraints: []*database.TableConstraint{
|
||||||
{Path: testutil.ParseDocumentPath(t, "a"), IsPrimaryKey: true},
|
{Path: testutil.ParseDocumentPath(t, "a"), PrimaryKey: true},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
@@ -759,11 +768,11 @@ func TestReadOnlyTables(t *testing.T) {
|
|||||||
err = res.Iterate(func(d types.Document) error {
|
err = res.Iterate(func(d types.Document) error {
|
||||||
switch i {
|
switch i {
|
||||||
case 0:
|
case 0:
|
||||||
testutil.RequireDocJSONEq(t, d, `{"name":"__genji_sequence", "sql":"CREATE TABLE __genji_sequence (name TEXT PRIMARY KEY, seq INTEGER)", "store_name":"X19nZW5qaV9zZXF1ZW5jZQ==", "type":"table"}`)
|
testutil.RequireDocJSONEq(t, d, `{"name":"__genji_sequence", "sql":"CREATE TABLE __genji_sequence (name TEXT, seq INTEGER, PRIMARY KEY (name))", "store_name":"X19nZW5qaV9zZXF1ZW5jZQ==", "type":"table"}`)
|
||||||
case 1:
|
case 1:
|
||||||
testutil.RequireDocJSONEq(t, d, `{"name":"__genji_store_seq", "owner":{"table_name":"__genji_catalog"}, "sql":"CREATE SEQUENCE __genji_store_seq CACHE 16", "type":"sequence"}`)
|
testutil.RequireDocJSONEq(t, d, `{"name":"__genji_store_seq", "owner":{"table_name":"__genji_catalog"}, "sql":"CREATE SEQUENCE __genji_store_seq CACHE 16", "type":"sequence"}`)
|
||||||
case 2:
|
case 2:
|
||||||
testutil.RequireDocJSONEq(t, d, `{"name":"foo", "docid_sequence_name":"foo_seq", "sql":"CREATE TABLE foo (a INTEGER, b[3].c DOUBLE UNIQUE)", "store_name":"AQ==", "type":"table"}`)
|
testutil.RequireDocJSONEq(t, d, `{"name":"foo", "docid_sequence_name":"foo_seq", "sql":"CREATE TABLE foo (a INTEGER, b[3].c DOUBLE, UNIQUE (b[3].c))", "store_name":"AQ==", "type":"table"}`)
|
||||||
case 3:
|
case 3:
|
||||||
testutil.RequireDocJSONEq(t, d, `{"name":"foo_b[3].c_idx", "owner":{"table_name":"foo", "path":"b[3].c"}, "sql":"CREATE UNIQUE INDEX `+"`foo_b[3].c_idx`"+` ON foo (b[3].c)", "store_name":"Ag==", "table_name":"foo", "type":"index"}`)
|
testutil.RequireDocJSONEq(t, d, `{"name":"foo_b[3].c_idx", "owner":{"table_name":"foo", "path":"b[3].c"}, "sql":"CREATE UNIQUE INDEX `+"`foo_b[3].c_idx`"+` ON foo (b[3].c)", "store_name":"Ag==", "table_name":"foo", "type":"index"}`)
|
||||||
case 4:
|
case 4:
|
||||||
|
|||||||
@@ -26,6 +26,6 @@ func OnInsertConflictDoReplace(t *Table, key []byte, d types.Document, err error
|
|||||||
return documentWithKey{
|
return documentWithKey{
|
||||||
Document: d,
|
Document: d,
|
||||||
key: key,
|
key: key,
|
||||||
pk: t.Info.FieldConstraints.GetPrimaryKey(),
|
pk: t.Info.GetPrimaryKey(),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package database
|
package database
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/genjidb/genji/document"
|
"github.com/genjidb/genji/document"
|
||||||
@@ -22,11 +23,8 @@ func (c *ConstraintViolationError) Error() string {
|
|||||||
type FieldConstraint struct {
|
type FieldConstraint struct {
|
||||||
Path document.Path
|
Path document.Path
|
||||||
Type types.ValueType
|
Type types.ValueType
|
||||||
IsPrimaryKey bool
|
|
||||||
IsNotNull bool
|
IsNotNull bool
|
||||||
IsUnique bool
|
|
||||||
DefaultValue TableExpression
|
DefaultValue TableExpression
|
||||||
Identity *FieldConstraintIdentity
|
|
||||||
IsInferred bool
|
IsInferred bool
|
||||||
InferredBy []document.Path
|
InferredBy []document.Path
|
||||||
}
|
}
|
||||||
@@ -42,10 +40,6 @@ func (f *FieldConstraint) IsEqual(other *FieldConstraint) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if f.IsPrimaryKey != other.IsPrimaryKey {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if f.IsNotNull != other.IsNotNull {
|
if f.IsNotNull != other.IsNotNull {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -60,13 +54,13 @@ func (f *FieldConstraint) IsEqual(other *FieldConstraint) bool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !f.Identity.IsEqual(other.Identity) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *FieldConstraint) IsEmpty() bool {
|
||||||
|
return f.Type.IsAny() && !f.IsNotNull && f.DefaultValue == nil
|
||||||
|
}
|
||||||
|
|
||||||
func (f *FieldConstraint) String() string {
|
func (f *FieldConstraint) String() string {
|
||||||
var s strings.Builder
|
var s strings.Builder
|
||||||
|
|
||||||
@@ -78,14 +72,6 @@ func (f *FieldConstraint) String() string {
|
|||||||
s.WriteString(" NOT NULL")
|
s.WriteString(" NOT NULL")
|
||||||
}
|
}
|
||||||
|
|
||||||
if f.IsPrimaryKey {
|
|
||||||
s.WriteString(" PRIMARY KEY")
|
|
||||||
}
|
|
||||||
|
|
||||||
if f.IsUnique {
|
|
||||||
s.WriteString(" UNIQUE")
|
|
||||||
}
|
|
||||||
|
|
||||||
if f.HasDefaultValue() {
|
if f.HasDefaultValue() {
|
||||||
s.WriteString(" DEFAULT ")
|
s.WriteString(" DEFAULT ")
|
||||||
s.WriteString(f.DefaultValue.String())
|
s.WriteString(f.DefaultValue.String())
|
||||||
@@ -136,18 +122,6 @@ func (f FieldConstraints) Get(path document.Path) *FieldConstraint {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetPrimaryKey returns the field constraint of the primary key.
|
|
||||||
// Returns nil if there is no primary key.
|
|
||||||
func (f FieldConstraints) GetPrimaryKey() *FieldConstraint {
|
|
||||||
for _, fc := range f {
|
|
||||||
if fc.IsPrimaryKey {
|
|
||||||
return fc
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Infer additional constraints based on user defined ones.
|
// Infer additional constraints based on user defined ones.
|
||||||
// For example, given the following table:
|
// For example, given the following table:
|
||||||
// CREATE TABLE foo (
|
// CREATE TABLE foo (
|
||||||
@@ -231,7 +205,7 @@ func (f *FieldConstraints) Add(newFc *FieldConstraint) error {
|
|||||||
// the inferred one may has less constraints than the user-defined one
|
// the inferred one may has less constraints than the user-defined one
|
||||||
inferredFc.DefaultValue = nonInferredFc.DefaultValue
|
inferredFc.DefaultValue = nonInferredFc.DefaultValue
|
||||||
inferredFc.IsNotNull = nonInferredFc.IsNotNull
|
inferredFc.IsNotNull = nonInferredFc.IsNotNull
|
||||||
inferredFc.IsPrimaryKey = nonInferredFc.IsPrimaryKey
|
// inferredFc.IsPrimaryKey = nonInferredFc.IsPrimaryKey
|
||||||
|
|
||||||
// detect if constraints are different
|
// detect if constraints are different
|
||||||
if !c.IsEqual(newFc) {
|
if !c.IsEqual(newFc) {
|
||||||
@@ -260,14 +234,6 @@ func (f *FieldConstraints) Add(newFc *FieldConstraint) error {
|
|||||||
(*f)[i] = newFc
|
(*f)[i] = newFc
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ensure we don't have duplicate primary keys
|
|
||||||
if c.IsPrimaryKey && newFc.IsPrimaryKey {
|
|
||||||
return stringutil.Errorf(
|
|
||||||
"multiple primary keys are not allowed (%q is primary key)",
|
|
||||||
c.Path.String(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
err := f.validateDefaultValue(newFc)
|
err := f.validateDefaultValue(newFc)
|
||||||
@@ -293,7 +259,7 @@ func (f *FieldConstraints) validateDefaultValue(newFc *FieldConstraint) error {
|
|||||||
// ensure default value type is compatible
|
// ensure default value type is compatible
|
||||||
if newFc.DefaultValue != nil && !newFc.Type.IsAny() {
|
if newFc.DefaultValue != nil && !newFc.Type.IsAny() {
|
||||||
// first, try to evaluate the default value
|
// first, try to evaluate the default value
|
||||||
v, err := newFc.DefaultValue.Eval(nil)
|
v, err := newFc.DefaultValue.Eval(nil, nil)
|
||||||
// if there is no error, check if the default value can be converted to the type of the constraint
|
// if there is no error, check if the default value can be converted to the type of the constraint
|
||||||
if err == nil {
|
if err == nil {
|
||||||
_, err = document.CastAs(v, newFc.Type)
|
_, err = document.CastAs(v, newFc.Type)
|
||||||
@@ -316,13 +282,7 @@ func (f *FieldConstraints) validateDefaultValue(newFc *FieldConstraint) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ValidateDocument calls Convert then ensures the document validates against the field constraints.
|
// ValidateDocument calls Convert then ensures the document validates against the field constraints.
|
||||||
func (f FieldConstraints) ValidateDocument(tx *Transaction, d types.Document) (*document.FieldBuffer, error) {
|
func (f FieldConstraints) ValidateDocument(tx *Transaction, fb *document.FieldBuffer) (*document.FieldBuffer, error) {
|
||||||
fb := document.NewFieldBuffer()
|
|
||||||
err := fb.Copy(d)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// generate default values for all fields
|
// generate default values for all fields
|
||||||
for _, fc := range f {
|
for _, fc := range f {
|
||||||
if fc.DefaultValue == nil {
|
if fc.DefaultValue == nil {
|
||||||
@@ -338,7 +298,7 @@ func (f FieldConstraints) ValidateDocument(tx *Transaction, d types.Document) (*
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
v, err := fc.DefaultValue.Eval(tx)
|
v, err := fc.DefaultValue.Eval(tx, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -348,7 +308,7 @@ func (f FieldConstraints) ValidateDocument(tx *Transaction, d types.Document) (*
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fb, err = f.ConvertDocument(fb)
|
fb, err := f.ConvertDocument(fb)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -475,26 +435,9 @@ func (f FieldConstraints) convertArrayAtPath(path document.Path, a types.Array,
|
|||||||
return vb, err
|
return vb, err
|
||||||
}
|
}
|
||||||
|
|
||||||
type FieldConstraintIdentity struct {
|
|
||||||
SequenceName string
|
|
||||||
Always bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *FieldConstraintIdentity) IsEqual(other *FieldConstraintIdentity) bool {
|
|
||||||
if f == nil {
|
|
||||||
return other == nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if other == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return f.SequenceName == other.SequenceName && f.Always == other.Always
|
|
||||||
}
|
|
||||||
|
|
||||||
type TableExpression interface {
|
type TableExpression interface {
|
||||||
Bind(catalog *Catalog)
|
Bind(catalog *Catalog)
|
||||||
Eval(tx *Transaction) (types.Value, error)
|
Eval(tx *Transaction, d types.Document) (types.Value, error)
|
||||||
IsEqual(other TableExpression) bool
|
IsEqual(other TableExpression) bool
|
||||||
String() string
|
String() string
|
||||||
}
|
}
|
||||||
@@ -503,7 +446,7 @@ type inferredTableExpression struct {
|
|||||||
v types.Value
|
v types.Value
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *inferredTableExpression) Eval(tx *Transaction) (types.Value, error) {
|
func (t *inferredTableExpression) Eval(tx *Transaction, d types.Document) (types.Value, error) {
|
||||||
return t.v, nil
|
return t.v, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -527,3 +470,139 @@ func (t *inferredTableExpression) IsEqual(other TableExpression) bool {
|
|||||||
func (t *inferredTableExpression) String() string {
|
func (t *inferredTableExpression) String() string {
|
||||||
return stringutil.Sprintf("%s", t.v)
|
return stringutil.Sprintf("%s", t.v)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A TableConstraint represent a constraint specific to a table
|
||||||
|
// and not necessarily to a single field path.
|
||||||
|
type TableConstraint struct {
|
||||||
|
Name string
|
||||||
|
Path document.Path
|
||||||
|
Check TableExpression
|
||||||
|
Unique bool
|
||||||
|
PrimaryKey bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsEqual compares t with other member by member.
|
||||||
|
func (t *TableConstraint) IsEqual(other *TableConstraint) bool {
|
||||||
|
if t == nil {
|
||||||
|
return other == nil
|
||||||
|
}
|
||||||
|
if other == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return t.Name == other.Name && t.Path.IsEqual(other.Path) && t.Check.IsEqual(other.Check) && t.Unique == other.Unique && t.PrimaryKey == other.PrimaryKey
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TableConstraint) String() string {
|
||||||
|
if t.Check != nil {
|
||||||
|
return stringutil.Sprintf("CHECK (%s)", t.Check)
|
||||||
|
}
|
||||||
|
|
||||||
|
if t.PrimaryKey {
|
||||||
|
return stringutil.Sprintf("PRIMARY KEY (%s)", t.Path)
|
||||||
|
}
|
||||||
|
|
||||||
|
if t.Unique {
|
||||||
|
return stringutil.Sprintf("UNIQUE (%s)", t.Path)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// TableConstraints holds the list of CHECK constraints.
|
||||||
|
type TableConstraints []*TableConstraint
|
||||||
|
|
||||||
|
// ValidateDocument checks all the table constraint for the given document.
|
||||||
|
func (t *TableConstraints) ValidateDocument(tx *Transaction, fb *document.FieldBuffer) error {
|
||||||
|
for _, tc := range *t {
|
||||||
|
if tc.Check == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
v, err := tc.Check.Eval(tx, fb)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var ok bool
|
||||||
|
switch v.Type() {
|
||||||
|
case types.BoolValue:
|
||||||
|
ok = v.V().(bool)
|
||||||
|
case types.IntegerValue:
|
||||||
|
ok = v.V().(int64) != 0
|
||||||
|
case types.DoubleValue:
|
||||||
|
ok = v.V().(float64) != 0
|
||||||
|
case types.NullValue:
|
||||||
|
ok = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
return stringutil.Errorf("document violates check constraint %q", tc.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TableConstraints) AddCheck(tableName string, e TableExpression) {
|
||||||
|
var i int
|
||||||
|
for _, tc := range *t {
|
||||||
|
if tc.Check != nil {
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
name := tableName + "_" + "check"
|
||||||
|
if i > 0 {
|
||||||
|
name += strconv.Itoa(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
*t = append(*t, &TableConstraint{
|
||||||
|
Name: name,
|
||||||
|
Check: e,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TableConstraints) AddPrimaryKey(tableName string, p document.Path) error {
|
||||||
|
for _, tc := range *t {
|
||||||
|
if tc.PrimaryKey {
|
||||||
|
return stringutil.Errorf("multiple primary keys for table %q are not allowed", tableName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
*t = append(*t, &TableConstraint{
|
||||||
|
Path: p,
|
||||||
|
PrimaryKey: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddUnique adds a unique constraint to the table.
|
||||||
|
// If the constraint is already present, it is ignored.
|
||||||
|
func (t *TableConstraints) AddUnique(p document.Path) {
|
||||||
|
for _, tc := range *t {
|
||||||
|
if tc.Unique && tc.Path.IsEqual(p) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
*t = append(*t, &TableConstraint{
|
||||||
|
Path: p,
|
||||||
|
Unique: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TableConstraints) Merge(other TableConstraints) error {
|
||||||
|
for _, tc := range other {
|
||||||
|
if tc.PrimaryKey {
|
||||||
|
if err := t.AddPrimaryKey(tc.Name, tc.Path); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else if tc.Unique {
|
||||||
|
t.AddUnique(tc.Path)
|
||||||
|
} else if tc.Check != nil {
|
||||||
|
t.AddCheck(tc.Name, tc.Check)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -43,18 +43,6 @@ func TestFieldConstraintsInfer(t *testing.T) {
|
|||||||
},
|
},
|
||||||
false,
|
false,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"Primary key",
|
|
||||||
[]*database.FieldConstraint{
|
|
||||||
{Path: document.NewPath("a"), Type: types.ArrayValue, IsPrimaryKey: true},
|
|
||||||
{Path: document.NewPath("a", "0"), Type: types.IntegerValue},
|
|
||||||
},
|
|
||||||
[]*database.FieldConstraint{
|
|
||||||
{Path: document.NewPath("a"), Type: types.ArrayValue, IsPrimaryKey: true},
|
|
||||||
{Path: document.NewPath("a", "0"), Type: types.IntegerValue},
|
|
||||||
},
|
|
||||||
false,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"Complex path",
|
"Complex path",
|
||||||
[]*database.FieldConstraint{{Path: document.NewPath("a", "b", "3", "1", "c"), Type: types.IntegerValue}},
|
[]*database.FieldConstraint{{Path: document.NewPath("a", "b", "3", "1", "c"), Type: types.IntegerValue}},
|
||||||
@@ -183,13 +171,6 @@ func TestFieldConstraintsAdd(t *testing.T) {
|
|||||||
nil,
|
nil,
|
||||||
true,
|
true,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"Duplicate primary key",
|
|
||||||
[]*database.FieldConstraint{{Path: document.NewPath("a"), IsPrimaryKey: true, Type: types.IntegerValue}},
|
|
||||||
database.FieldConstraint{Path: document.NewPath("b"), IsPrimaryKey: true, Type: types.IntegerValue},
|
|
||||||
nil,
|
|
||||||
true,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"Different path",
|
"Different path",
|
||||||
[]*database.FieldConstraint{{Path: document.NewPath("a"), Type: types.IntegerValue}},
|
[]*database.FieldConstraint{{Path: document.NewPath("a"), Type: types.IntegerValue}},
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ type TableInfo struct {
|
|||||||
ReadOnly bool
|
ReadOnly bool
|
||||||
|
|
||||||
FieldConstraints FieldConstraints
|
FieldConstraints FieldConstraints
|
||||||
|
TableConstraints TableConstraints
|
||||||
|
|
||||||
// Name of the docid sequence if any.
|
// Name of the docid sequence if any.
|
||||||
DocidSequenceName string
|
DocidSequenceName string
|
||||||
@@ -39,15 +40,66 @@ func (ti *TableInfo) GenerateBaseName() string {
|
|||||||
return ti.TableName
|
return ti.TableName
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ValidateDocument calls Convert then ensures the document validates against the field constraints.
|
||||||
|
func (ti *TableInfo) ValidateDocument(tx *Transaction, d types.Document) (*document.FieldBuffer, error) {
|
||||||
|
fb := document.NewFieldBuffer()
|
||||||
|
err := fb.Copy(d)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
fb, err = ti.FieldConstraints.ValidateDocument(tx, fb)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = ti.TableConstraints.ValidateDocument(tx, fb)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return fb, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ti *TableInfo) GetPrimaryKey() *FieldConstraint {
|
||||||
|
for _, tc := range ti.TableConstraints {
|
||||||
|
if tc.PrimaryKey == false {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
fc := ti.GetFieldConstraintForPath(tc.Path)
|
||||||
|
if fc == nil {
|
||||||
|
return &FieldConstraint{
|
||||||
|
Path: tc.Path,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fc
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ti *TableInfo) GetFieldConstraintForPath(p document.Path) *FieldConstraint {
|
||||||
|
for _, fc := range ti.FieldConstraints {
|
||||||
|
if fc.Path.IsEqual(p) {
|
||||||
|
return fc
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// String returns a SQL representation.
|
// String returns a SQL representation.
|
||||||
func (ti *TableInfo) String() string {
|
func (ti *TableInfo) String() string {
|
||||||
var s strings.Builder
|
var s strings.Builder
|
||||||
|
|
||||||
stringutil.Fprintf(&s, "CREATE TABLE %s", stringutil.NormalizeIdentifier(ti.TableName, '`'))
|
stringutil.Fprintf(&s, "CREATE TABLE %s", stringutil.NormalizeIdentifier(ti.TableName, '`'))
|
||||||
if len(ti.FieldConstraints) > 0 {
|
if len(ti.FieldConstraints) > 0 || len(ti.TableConstraints) > 0 {
|
||||||
s.WriteString(" (")
|
s.WriteString(" (")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var hasFieldConstraints bool
|
||||||
for i, fc := range ti.FieldConstraints {
|
for i, fc := range ti.FieldConstraints {
|
||||||
if fc.IsInferred {
|
if fc.IsInferred {
|
||||||
continue
|
continue
|
||||||
@@ -58,9 +110,19 @@ func (ti *TableInfo) String() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
s.WriteString(fc.String())
|
s.WriteString(fc.String())
|
||||||
|
|
||||||
|
hasFieldConstraints = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(ti.FieldConstraints) > 0 {
|
for i, tc := range ti.TableConstraints {
|
||||||
|
if i > 0 || hasFieldConstraints {
|
||||||
|
s.WriteString(", ")
|
||||||
|
}
|
||||||
|
|
||||||
|
s.WriteString(tc.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(ti.FieldConstraints) > 0 || len(ti.TableConstraints) > 0 {
|
||||||
s.WriteString(")")
|
s.WriteString(")")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,7 +133,9 @@ func (ti *TableInfo) String() string {
|
|||||||
func (ti *TableInfo) Clone() *TableInfo {
|
func (ti *TableInfo) Clone() *TableInfo {
|
||||||
cp := *ti
|
cp := *ti
|
||||||
cp.FieldConstraints = nil
|
cp.FieldConstraints = nil
|
||||||
|
cp.TableConstraints = nil
|
||||||
cp.FieldConstraints = append(cp.FieldConstraints, ti.FieldConstraints...)
|
cp.FieldConstraints = append(cp.FieldConstraints, ti.FieldConstraints...)
|
||||||
|
cp.TableConstraints = append(cp.TableConstraints, ti.TableConstraints...)
|
||||||
return &cp
|
return &cp
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,8 +24,7 @@ var sequenceTableInfo = &TableInfo{
|
|||||||
FieldName: "name",
|
FieldName: "name",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Type: types.TextValue,
|
Type: types.TextValue,
|
||||||
IsPrimaryKey: true,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Path: document.Path{
|
Path: document.Path{
|
||||||
@@ -36,6 +35,16 @@ var sequenceTableInfo = &TableInfo{
|
|||||||
Type: types.IntegerValue,
|
Type: types.IntegerValue,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
TableConstraints: []*TableConstraint{
|
||||||
|
{
|
||||||
|
Path: document.Path{
|
||||||
|
document.PathFragment{
|
||||||
|
FieldName: "name",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
PrimaryKey: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// A Sequence manages a sequence of numbers.
|
// A Sequence manages a sequence of numbers.
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ func (t *Table) InsertWithConflictResolution(d types.Document, onConflict OnInse
|
|||||||
return nil, errors.New("cannot write to read-only table")
|
return nil, errors.New("cannot write to read-only table")
|
||||||
}
|
}
|
||||||
|
|
||||||
fb, err := t.Info.FieldConstraints.ValidateDocument(t.Tx, d)
|
fb, err := t.Info.ValidateDocument(t.Tx, d)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if onConflict != nil {
|
if onConflict != nil {
|
||||||
if ce, ok := err.(*ConstraintViolationError); ok && ce.Constraint == "NOT NULL" {
|
if ce, ok := err.(*ConstraintViolationError); ok && ce.Constraint == "NOT NULL" {
|
||||||
@@ -156,7 +156,7 @@ func (t *Table) InsertWithConflictResolution(d types.Document, onConflict OnInse
|
|||||||
return documentWithKey{
|
return documentWithKey{
|
||||||
Document: fb,
|
Document: fb,
|
||||||
key: key,
|
key: key,
|
||||||
pk: t.Info.FieldConstraints.GetPrimaryKey(),
|
pk: t.Info.GetPrimaryKey(),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -229,7 +229,7 @@ func (t *Table) Replace(key []byte, d types.Document) (types.Document, error) {
|
|||||||
return nil, errors.New("cannot write to read-only table")
|
return nil, errors.New("cannot write to read-only table")
|
||||||
}
|
}
|
||||||
|
|
||||||
d, err := t.Info.FieldConstraints.ValidateDocument(t.Tx, d)
|
d, err := t.Info.ValidateDocument(t.Tx, d)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -428,7 +428,7 @@ func (t *Table) EncodeValue(v types.Value) ([]byte, error) {
|
|||||||
func (t *Table) encodeValueToKey(info *TableInfo, v types.Value) ([]byte, error) {
|
func (t *Table) encodeValueToKey(info *TableInfo, v types.Value) ([]byte, error) {
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
pk := t.Info.FieldConstraints.GetPrimaryKey()
|
pk := t.Info.GetPrimaryKey()
|
||||||
if pk == nil {
|
if pk == nil {
|
||||||
// if no primary key was defined, convert the pivot to an integer then to an unsigned integer
|
// if no primary key was defined, convert the pivot to an integer then to an unsigned integer
|
||||||
// and encode it as a varint
|
// and encode it as a varint
|
||||||
@@ -502,7 +502,7 @@ func (t *Table) iterate(pivot types.Value, reverse bool, fn func(d types.Documen
|
|||||||
codec: t.Codec,
|
codec: t.Codec,
|
||||||
}
|
}
|
||||||
|
|
||||||
d.pk = t.Info.FieldConstraints.GetPrimaryKey()
|
d.pk = t.Info.GetPrimaryKey()
|
||||||
|
|
||||||
it := t.Store.Iterator(engine.IteratorOptions{Reverse: reverse})
|
it := t.Store.Iterator(engine.IteratorOptions{Reverse: reverse})
|
||||||
defer it.Close()
|
defer it.Close()
|
||||||
@@ -538,7 +538,7 @@ func (t *Table) GetDocument(key []byte) (types.Document, error) {
|
|||||||
var d documentWithKey
|
var d documentWithKey
|
||||||
d.Document = t.Codec.NewDecoder(v)
|
d.Document = t.Codec.NewDecoder(v)
|
||||||
d.key = key
|
d.key = key
|
||||||
d.pk = t.Info.FieldConstraints.GetPrimaryKey()
|
d.pk = t.Info.GetPrimaryKey()
|
||||||
return &d, err
|
return &d, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -549,7 +549,7 @@ func (t *Table) GetDocument(key []byte) (types.Document, error) {
|
|||||||
// if there are no primary key in the table, a default
|
// if there are no primary key in the table, a default
|
||||||
// key is generated, called the docid.
|
// key is generated, called the docid.
|
||||||
func (t *Table) generateKey(info *TableInfo, d types.Document) ([]byte, error) {
|
func (t *Table) generateKey(info *TableInfo, d types.Document) ([]byte, error) {
|
||||||
if pk := t.Info.FieldConstraints.GetPrimaryKey(); pk != nil {
|
if pk := t.Info.GetPrimaryKey(); pk != nil {
|
||||||
v, err := pk.Path.GetValueFromDocument(d)
|
v, err := pk.Path.GetValueFromDocument(d)
|
||||||
if errors.Is(err, document.ErrFieldNotFound) {
|
if errors.Is(err, document.ErrFieldNotFound) {
|
||||||
return nil, stringutil.Errorf("missing primary key at path %q", pk.Path)
|
return nil, stringutil.Errorf("missing primary key at path %q", pk.Path)
|
||||||
|
|||||||
@@ -244,7 +244,10 @@ func TestTableInsert(t *testing.T) {
|
|||||||
|
|
||||||
err := db.Catalog.CreateTable(tx, "test", &database.TableInfo{
|
err := db.Catalog.CreateTable(tx, "test", &database.TableInfo{
|
||||||
FieldConstraints: []*database.FieldConstraint{
|
FieldConstraints: []*database.FieldConstraint{
|
||||||
{Path: testutil.ParseDocumentPath(t, "foo.a[1]"), Type: types.IntegerValue, IsPrimaryKey: true},
|
{Path: testutil.ParseDocumentPath(t, "foo.a[1]"), Type: types.IntegerValue},
|
||||||
|
},
|
||||||
|
TableConstraints: []*database.TableConstraint{
|
||||||
|
{Path: testutil.ParseDocumentPath(t, "foo.a[1]"), PrimaryKey: true},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
@@ -306,7 +309,10 @@ func TestTableInsert(t *testing.T) {
|
|||||||
|
|
||||||
err := db.Catalog.CreateTable(tx, "test", &database.TableInfo{
|
err := db.Catalog.CreateTable(tx, "test", &database.TableInfo{
|
||||||
FieldConstraints: []*database.FieldConstraint{
|
FieldConstraints: []*database.FieldConstraint{
|
||||||
{Path: testutil.ParseDocumentPath(t, "foo"), Type: types.IntegerValue, IsPrimaryKey: true},
|
{Path: testutil.ParseDocumentPath(t, "foo"), Type: types.IntegerValue},
|
||||||
|
},
|
||||||
|
TableConstraints: []*database.TableConstraint{
|
||||||
|
{Path: testutil.ParseDocumentPath(t, "foo"), PrimaryKey: true},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
@@ -582,8 +588,12 @@ func TestTableInsert(t *testing.T) {
|
|||||||
tb := createTable(t, tx, db.Catalog, database.TableInfo{
|
tb := createTable(t, tx, db.Catalog, database.TableInfo{
|
||||||
TableName: "test",
|
TableName: "test",
|
||||||
FieldConstraints: []*database.FieldConstraint{
|
FieldConstraints: []*database.FieldConstraint{
|
||||||
{Path: testutil.ParseDocumentPath(t, "foo"), IsPrimaryKey: true, IsNotNull: true},
|
{Path: testutil.ParseDocumentPath(t, "foo"), IsNotNull: true},
|
||||||
}})
|
},
|
||||||
|
TableConstraints: []*database.TableConstraint{
|
||||||
|
{Path: testutil.ParseDocumentPath(t, "foo"), PrimaryKey: true},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
doc := document.NewFieldBuffer().
|
doc := document.NewFieldBuffer().
|
||||||
Add("foo", types.NewIntegerValue(10))
|
Add("foo", types.NewIntegerValue(10))
|
||||||
@@ -604,8 +614,12 @@ func TestTableInsert(t *testing.T) {
|
|||||||
tb := createTable(t, tx, db.Catalog, database.TableInfo{
|
tb := createTable(t, tx, db.Catalog, database.TableInfo{
|
||||||
TableName: "test",
|
TableName: "test",
|
||||||
FieldConstraints: []*database.FieldConstraint{
|
FieldConstraints: []*database.FieldConstraint{
|
||||||
{Path: testutil.ParseDocumentPath(t, "foo"), IsPrimaryKey: true, IsNotNull: true},
|
{Path: testutil.ParseDocumentPath(t, "foo"), IsNotNull: true},
|
||||||
}})
|
},
|
||||||
|
TableConstraints: []*database.TableConstraint{
|
||||||
|
{Path: testutil.ParseDocumentPath(t, "foo"), PrimaryKey: true},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
doc := document.NewFieldBuffer().
|
doc := document.NewFieldBuffer().
|
||||||
Add("foo", types.NewIntegerValue(10))
|
Add("foo", types.NewIntegerValue(10))
|
||||||
@@ -723,8 +737,12 @@ func TestTableInsert(t *testing.T) {
|
|||||||
|
|
||||||
err := db.Catalog.CreateTable(tx, "test", &database.TableInfo{
|
err := db.Catalog.CreateTable(tx, "test", &database.TableInfo{
|
||||||
FieldConstraints: []*database.FieldConstraint{
|
FieldConstraints: []*database.FieldConstraint{
|
||||||
{Path: testutil.ParseDocumentPath(t, "foo"), IsPrimaryKey: true, IsNotNull: true},
|
{Path: testutil.ParseDocumentPath(t, "foo"), IsNotNull: true},
|
||||||
}})
|
},
|
||||||
|
TableConstraints: []*database.TableConstraint{
|
||||||
|
{Path: testutil.ParseDocumentPath(t, "foo"), PrimaryKey: true},
|
||||||
|
},
|
||||||
|
})
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
tb, err := db.Catalog.GetTable(tx, "test")
|
tb, err := db.Catalog.GetTable(tx, "test")
|
||||||
|
|||||||
@@ -18,10 +18,11 @@ func Constraint(e Expr) *ConstraintExpr {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *ConstraintExpr) Eval(tx *database.Transaction) (types.Value, error) {
|
func (t *ConstraintExpr) Eval(tx *database.Transaction, d types.Document) (types.Value, error) {
|
||||||
var env environment.Environment
|
var env environment.Environment
|
||||||
env.Catalog = t.Catalog
|
env.Catalog = t.Catalog
|
||||||
env.Tx = tx
|
env.Tx = tx
|
||||||
|
env.SetDocument(d)
|
||||||
|
|
||||||
if t.Expr == nil {
|
if t.Expr == nil {
|
||||||
return NullLiteral, errors.New("missing expression")
|
return NullLiteral, errors.New("missing expression")
|
||||||
|
|||||||
@@ -219,7 +219,7 @@ func (i *indexSelector) isFilterIndexable(f *stream.FilterOperator) *filterNode
|
|||||||
|
|
||||||
func (i *indexSelector) associatePkWithNodes(tb *database.TableInfo, nodes filterNodes) *candidate {
|
func (i *indexSelector) associatePkWithNodes(tb *database.TableInfo, nodes filterNodes) *candidate {
|
||||||
// TODO: add support for the pk() function
|
// TODO: add support for the pk() function
|
||||||
pk := tb.FieldConstraints.GetPrimaryKey()
|
pk := tb.GetPrimaryKey()
|
||||||
|
|
||||||
if pk == nil {
|
if pk == nil {
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -39,8 +39,7 @@ func (stmt AlterStmt) Run(ctx *Context) (Result, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type AlterTableAddField struct {
|
type AlterTableAddField struct {
|
||||||
TableName string
|
Info database.TableInfo
|
||||||
Constraint database.FieldConstraint
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsReadOnly always returns false. It implements the Statement interface.
|
// IsReadOnly always returns false. It implements the Statement interface.
|
||||||
@@ -53,14 +52,11 @@ func (stmt AlterTableAddField) IsReadOnly() bool {
|
|||||||
func (stmt AlterTableAddField) Run(ctx *Context) (Result, error) {
|
func (stmt AlterTableAddField) Run(ctx *Context) (Result, error) {
|
||||||
var res Result
|
var res Result
|
||||||
|
|
||||||
if stmt.TableName == "" {
|
var fc *database.FieldConstraint
|
||||||
return res, errors.New("missing table name")
|
if stmt.Info.FieldConstraints != nil {
|
||||||
|
fc = stmt.Info.FieldConstraints[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
if stmt.Constraint.Path == nil {
|
err := ctx.Catalog.AddFieldConstraint(ctx.Tx, stmt.Info.TableName, fc, stmt.Info.TableConstraints)
|
||||||
return res, errors.New("missing field name")
|
|
||||||
}
|
|
||||||
|
|
||||||
err := ctx.Catalog.AddFieldConstraint(ctx.Tx, stmt.TableName, stmt.Constraint)
|
|
||||||
return res, err
|
return res, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ func (stmt *CreateTableStmt) Run(ctx *Context) (Result, error) {
|
|||||||
var res Result
|
var res Result
|
||||||
|
|
||||||
// if there is no primary key, create a docid sequence
|
// if there is no primary key, create a docid sequence
|
||||||
if stmt.Info.FieldConstraints.GetPrimaryKey() == nil {
|
if stmt.Info.GetPrimaryKey() == nil {
|
||||||
seq := database.SequenceInfo{
|
seq := database.SequenceInfo{
|
||||||
IncrementBy: 1,
|
IncrementBy: 1,
|
||||||
Min: 1, Max: math.MaxInt64,
|
Min: 1, Max: math.MaxInt64,
|
||||||
@@ -52,16 +52,21 @@ func (stmt *CreateTableStmt) Run(ctx *Context) (Result, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// create a unique index for every unique constraint
|
// create a unique index for every unique constraint
|
||||||
for _, fc := range stmt.Info.FieldConstraints {
|
for _, tc := range stmt.Info.TableConstraints {
|
||||||
if fc.IsUnique {
|
if tc.Unique {
|
||||||
|
fc := stmt.Info.GetFieldConstraintForPath(tc.Path)
|
||||||
|
var tp types.ValueType
|
||||||
|
if fc != nil {
|
||||||
|
tp = fc.Type
|
||||||
|
}
|
||||||
err = ctx.Catalog.CreateIndex(ctx.Tx, &database.IndexInfo{
|
err = ctx.Catalog.CreateIndex(ctx.Tx, &database.IndexInfo{
|
||||||
TableName: stmt.Info.TableName,
|
TableName: stmt.Info.TableName,
|
||||||
Paths: []document.Path{fc.Path},
|
Paths: []document.Path{tc.Path},
|
||||||
Unique: true,
|
Unique: true,
|
||||||
Types: []types.ValueType{fc.Type},
|
Types: []types.ValueType{tp},
|
||||||
Owner: database.Owner{
|
Owner: database.Owner{
|
||||||
TableName: stmt.Info.TableName,
|
TableName: stmt.Info.TableName,
|
||||||
Path: fc.Path,
|
Path: tc.Path,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ func TestCreateTable(t *testing.T) {
|
|||||||
InferredBy: []document.Path{
|
InferredBy: []document.Path{
|
||||||
parsePath(t, "foo.bar[1].hello"),
|
parsePath(t, "foo.bar[1].hello"),
|
||||||
}},
|
}},
|
||||||
{Path: parsePath(t, "foo.bar[1].hello"), Type: types.BlobValue, IsPrimaryKey: true},
|
{Path: parsePath(t, "foo.bar[1].hello"), Type: types.BlobValue},
|
||||||
{Path: parsePath(t, "foo.a"), Type: types.ArrayValue, IsInferred: true,
|
{Path: parsePath(t, "foo.a"), Type: types.ArrayValue, IsInferred: true,
|
||||||
InferredBy: []document.Path{
|
InferredBy: []document.Path{
|
||||||
parsePath(t, "foo.a[1][2]"),
|
parsePath(t, "foo.a[1][2]"),
|
||||||
@@ -164,7 +164,7 @@ func TestCreateTable(t *testing.T) {
|
|||||||
InferredBy: []document.Path{
|
InferredBy: []document.Path{
|
||||||
parsePath(t, "foo.bar[1].hello"),
|
parsePath(t, "foo.bar[1].hello"),
|
||||||
}},
|
}},
|
||||||
{Path: parsePath(t, "foo.bar[1].hello"), Type: types.BlobValue, IsPrimaryKey: true},
|
{Path: parsePath(t, "foo.bar[1].hello"), Type: types.BlobValue},
|
||||||
{Path: parsePath(t, "foo.a"), Type: types.ArrayValue, IsInferred: true,
|
{Path: parsePath(t, "foo.a"), Type: types.ArrayValue, IsInferred: true,
|
||||||
InferredBy: []document.Path{
|
InferredBy: []document.Path{
|
||||||
parsePath(t, "foo.a[1][2]"),
|
parsePath(t, "foo.a[1][2]"),
|
||||||
@@ -244,24 +244,33 @@ func TestCreateTable(t *testing.T) {
|
|||||||
|
|
||||||
tb, err := db.Catalog.GetTable(tx, "test")
|
tb, err := db.Catalog.GetTable(tx, "test")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
require.Len(t, tb.Info.FieldConstraints, 3)
|
require.Len(t, tb.Info.FieldConstraints, 2)
|
||||||
|
require.Len(t, tb.Info.TableConstraints, 3)
|
||||||
|
|
||||||
require.Equal(t, &database.FieldConstraint{
|
require.Equal(t, &database.FieldConstraint{
|
||||||
Path: parsePath(t, "a"),
|
Path: parsePath(t, "a"),
|
||||||
Type: types.IntegerValue,
|
Type: types.IntegerValue,
|
||||||
IsUnique: true,
|
|
||||||
}, tb.Info.FieldConstraints[0])
|
}, tb.Info.FieldConstraints[0])
|
||||||
|
|
||||||
require.Equal(t, &database.FieldConstraint{
|
require.Equal(t, &database.FieldConstraint{
|
||||||
Path: parsePath(t, "b"),
|
Path: parsePath(t, "b"),
|
||||||
Type: types.DoubleValue,
|
Type: types.DoubleValue,
|
||||||
IsUnique: true,
|
|
||||||
}, tb.Info.FieldConstraints[1])
|
}, tb.Info.FieldConstraints[1])
|
||||||
|
|
||||||
require.Equal(t, &database.FieldConstraint{
|
require.Equal(t, &database.TableConstraint{
|
||||||
Path: parsePath(t, "c"),
|
Path: parsePath(t, "a"),
|
||||||
IsUnique: true,
|
Unique: true,
|
||||||
}, tb.Info.FieldConstraints[2])
|
}, tb.Info.TableConstraints[0])
|
||||||
|
|
||||||
|
require.Equal(t, &database.TableConstraint{
|
||||||
|
Path: parsePath(t, "b"),
|
||||||
|
Unique: true,
|
||||||
|
}, tb.Info.TableConstraints[1])
|
||||||
|
|
||||||
|
require.Equal(t, &database.TableConstraint{
|
||||||
|
Path: parsePath(t, "c"),
|
||||||
|
Unique: true,
|
||||||
|
}, tb.Info.TableConstraints[2])
|
||||||
|
|
||||||
idx, err := db.Catalog.GetIndex(tx, "test_a_idx")
|
idx, err := db.Catalog.GetIndex(tx, "test_a_idx")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ func (stmt DropTableStmt) Run(ctx *Context) (Result, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// if there is no primary key, drop the docid sequence
|
// if there is no primary key, drop the docid sequence
|
||||||
if tb.Info.FieldConstraints.GetPrimaryKey() == nil {
|
if tb.Info.GetPrimaryKey() == nil {
|
||||||
err = ctx.Catalog.DropSequence(ctx.Tx, tb.Info.DocidSequenceName)
|
err = ctx.Catalog.DropSequence(ctx.Tx, tb.Info.DocidSequenceName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return res, err
|
return res, err
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package parser
|
package parser
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/genjidb/genji/internal/database"
|
||||||
"github.com/genjidb/genji/internal/query/statement"
|
"github.com/genjidb/genji/internal/query/statement"
|
||||||
"github.com/genjidb/genji/internal/sql/scanner"
|
"github.com/genjidb/genji/internal/sql/scanner"
|
||||||
)
|
)
|
||||||
@@ -25,23 +26,31 @@ func (p *Parser) parseAlterTableRenameStatement(tableName string) (_ statement.A
|
|||||||
|
|
||||||
func (p *Parser) parseAlterTableAddFieldStatement(tableName string) (_ statement.AlterTableAddField, err error) {
|
func (p *Parser) parseAlterTableAddFieldStatement(tableName string) (_ statement.AlterTableAddField, err error) {
|
||||||
var stmt statement.AlterTableAddField
|
var stmt statement.AlterTableAddField
|
||||||
stmt.TableName = tableName
|
stmt.Info.TableName = tableName
|
||||||
|
|
||||||
// Parse "FIELD".
|
// Parse "FIELD".
|
||||||
if err := p.parseTokens(scanner.FIELD); err != nil {
|
if err := p.parseTokens(scanner.FIELD); err != nil {
|
||||||
return stmt, err
|
return stmt, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var fc database.FieldConstraint
|
||||||
// Parse new field definition.
|
// Parse new field definition.
|
||||||
err = p.parseFieldDefinition(&stmt.Constraint)
|
err = p.parseFieldDefinition(&fc, &stmt.Info)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return stmt, err
|
return stmt, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if stmt.Constraint.IsPrimaryKey {
|
if stmt.Info.GetPrimaryKey() != nil {
|
||||||
return stmt, &ParseError{Message: "cannot add a PRIMARY KEY constraint"}
|
return stmt, &ParseError{Message: "cannot add a PRIMARY KEY constraint"}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !fc.IsEmpty() {
|
||||||
|
err = stmt.Info.FieldConstraints.Add(&fc)
|
||||||
|
if err != nil {
|
||||||
|
return stmt, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return stmt, nil
|
return stmt, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -48,28 +48,41 @@ func TestParserAlterTableAddField(t *testing.T) {
|
|||||||
expected statement.Statement
|
expected statement.Statement
|
||||||
errored bool
|
errored bool
|
||||||
}{
|
}{
|
||||||
{"Basic", "ALTER TABLE foo ADD FIELD bar", statement.AlterTableAddField{TableName: "foo",
|
{"Basic", "ALTER TABLE foo ADD FIELD bar", nil, true},
|
||||||
Constraint: database.FieldConstraint{},
|
{"With type", "ALTER TABLE foo ADD FIELD bar integer", statement.AlterTableAddField{
|
||||||
}, true},
|
Info: database.TableInfo{
|
||||||
{"With type", "ALTER TABLE foo ADD FIELD bar integer", statement.AlterTableAddField{TableName: "foo",
|
TableName: "foo",
|
||||||
Constraint: database.FieldConstraint{
|
FieldConstraints: []*database.FieldConstraint{
|
||||||
Path: document.Path(testutil.ParsePath(t, "bar")),
|
{
|
||||||
Type: types.IntegerValue,
|
Path: document.Path(testutil.ParsePath(t, "bar")),
|
||||||
|
Type: types.IntegerValue,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}, false},
|
}, false},
|
||||||
{"With not null", "ALTER TABLE foo ADD FIELD bar NOT NULL", statement.AlterTableAddField{TableName: "foo",
|
{"With not null", "ALTER TABLE foo ADD FIELD bar NOT NULL", statement.AlterTableAddField{
|
||||||
Constraint: database.FieldConstraint{
|
Info: database.TableInfo{
|
||||||
Path: document.Path(testutil.ParsePath(t, "bar")),
|
TableName: "foo",
|
||||||
IsNotNull: true,
|
FieldConstraints: []*database.FieldConstraint{
|
||||||
|
{
|
||||||
|
Path: document.Path(testutil.ParsePath(t, "bar")),
|
||||||
|
IsNotNull: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}, false},
|
}, false},
|
||||||
{"With primary key", "ALTER TABLE foo ADD FIELD bar PRIMARY KEY", statement.AlterTableAddField{}, true},
|
{"With primary key", "ALTER TABLE foo ADD FIELD bar PRIMARY KEY", nil, true},
|
||||||
{"With multiple constraints", "ALTER TABLE foo ADD FIELD bar integer NOT NULL DEFAULT 0", statement.AlterTableAddField{TableName: "foo",
|
{"With multiple constraints", "ALTER TABLE foo ADD FIELD bar integer NOT NULL DEFAULT 0", statement.AlterTableAddField{
|
||||||
Constraint: database.FieldConstraint{
|
Info: database.TableInfo{
|
||||||
Path: document.Path(testutil.ParsePath(t, "bar")),
|
TableName: "foo",
|
||||||
Type: types.IntegerValue,
|
FieldConstraints: []*database.FieldConstraint{
|
||||||
IsNotNull: true,
|
{
|
||||||
DefaultValue: expr.Constraint(expr.LiteralValue{Value: types.NewIntegerValue(0)}),
|
Path: document.Path(testutil.ParsePath(t, "bar")),
|
||||||
|
Type: types.IntegerValue,
|
||||||
|
IsNotNull: true,
|
||||||
|
DefaultValue: expr.Constraint(expr.LiteralValue{Value: types.NewIntegerValue(0)}),
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}, false},
|
}, false},
|
||||||
{"With error / missing FIELD keyword", "ALTER TABLE foo ADD bar", nil, true},
|
{"With error / missing FIELD keyword", "ALTER TABLE foo ADD bar", nil, true},
|
||||||
|
|||||||
@@ -59,30 +59,6 @@ func (p *Parser) parseCreateTableStatement() (*statement.CreateTableStmt, error)
|
|||||||
return &stmt, err
|
return &stmt, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Parser) parseFieldDefinition(fc *database.FieldConstraint) (err error) {
|
|
||||||
fc.Path, err = p.parsePath()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
fc.Type, err = p.parseType()
|
|
||||||
if err != nil {
|
|
||||||
p.Unscan()
|
|
||||||
}
|
|
||||||
|
|
||||||
err = p.parseFieldConstraint(fc)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if fc.Type.IsAny() && fc.DefaultValue == nil && !fc.IsNotNull && !fc.IsPrimaryKey && !fc.IsUnique {
|
|
||||||
tok, pos, lit := p.ScanIgnoreWhitespace()
|
|
||||||
return newParseError(scanner.Tokstr(tok, lit), []string{"CONSTRAINT", "TYPE"}, pos)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Parser) parseConstraints(stmt *statement.CreateTableStmt) error {
|
func (p *Parser) parseConstraints(stmt *statement.CreateTableStmt) error {
|
||||||
// Parse ( token.
|
// Parse ( token.
|
||||||
if ok, err := p.parseOptional(scanner.LPAREN); !ok || err != nil {
|
if ok, err := p.parseOptional(scanner.LPAREN); !ok || err != nil {
|
||||||
@@ -116,15 +92,17 @@ func (p *Parser) parseConstraints(stmt *statement.CreateTableStmt) error {
|
|||||||
// if set to false, we are still parsing field definitions
|
// if set to false, we are still parsing field definitions
|
||||||
if !parsingTableConstraints {
|
if !parsingTableConstraints {
|
||||||
var fc database.FieldConstraint
|
var fc database.FieldConstraint
|
||||||
|
err := p.parseFieldDefinition(&fc, &stmt.Info)
|
||||||
err = p.parseFieldDefinition(&fc)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = stmt.Info.FieldConstraints.Add(&fc)
|
// if the field definition is empty, we ignore it
|
||||||
if err != nil {
|
if !fc.IsEmpty() {
|
||||||
return err
|
err = stmt.Info.FieldConstraints.Add(&fc)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,22 +117,25 @@ func (p *Parser) parseConstraints(stmt *statement.CreateTableStmt) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// ensure only one primary key
|
|
||||||
var pkFound bool
|
|
||||||
for _, fc := range stmt.Info.FieldConstraints {
|
|
||||||
if fc.IsPrimaryKey {
|
|
||||||
if pkFound {
|
|
||||||
return stringutil.Errorf("table %q has more than one primary key", stmt.Info.TableName)
|
|
||||||
}
|
|
||||||
|
|
||||||
pkFound = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Parser) parseFieldConstraint(fc *database.FieldConstraint) error {
|
func (p *Parser) parseFieldDefinition(fc *database.FieldConstraint, info *database.TableInfo) error {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
fc.Path, err = p.parsePath()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fc.Type, err = p.parseType()
|
||||||
|
if err != nil {
|
||||||
|
p.Unscan()
|
||||||
|
}
|
||||||
|
|
||||||
|
var addedTc int
|
||||||
|
|
||||||
|
LOOP:
|
||||||
for {
|
for {
|
||||||
tok, pos, lit := p.ScanIgnoreWhitespace()
|
tok, pos, lit := p.ScanIgnoreWhitespace()
|
||||||
switch tok {
|
switch tok {
|
||||||
@@ -164,12 +145,11 @@ func (p *Parser) parseFieldConstraint(fc *database.FieldConstraint) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// if it's already a primary key we return an error
|
err = info.TableConstraints.AddPrimaryKey(info.TableName, fc.Path)
|
||||||
if fc.IsPrimaryKey {
|
if err != nil {
|
||||||
return newParseError(scanner.Tokstr(tok, lit), []string{"CONSTRAINT", ")"}, pos)
|
return err
|
||||||
}
|
}
|
||||||
|
addedTc++
|
||||||
fc.IsPrimaryKey = true
|
|
||||||
case scanner.NOT:
|
case scanner.NOT:
|
||||||
// Parse "NULL"
|
// Parse "NULL"
|
||||||
if err := p.parseTokens(scanner.NULL); err != nil {
|
if err := p.parseTokens(scanner.NULL); err != nil {
|
||||||
@@ -234,17 +214,42 @@ func (p *Parser) parseFieldConstraint(fc *database.FieldConstraint) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
case scanner.UNIQUE:
|
case scanner.UNIQUE:
|
||||||
// if it's already unique we return an error
|
info.TableConstraints.AddUnique(fc.Path)
|
||||||
if fc.IsUnique {
|
addedTc++
|
||||||
return newParseError(scanner.Tokstr(tok, lit), []string{"CONSTRAINT", ")"}, pos)
|
case scanner.CHECK:
|
||||||
|
// Parse "("
|
||||||
|
err := p.parseTokens(scanner.LPAREN)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
fc.IsUnique = true
|
e, err := p.ParseExpr()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse ")"
|
||||||
|
err = p.parseTokens(scanner.RPAREN)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
info.TableConstraints.AddCheck(info.TableName, expr.Constraint(e))
|
||||||
|
addedTc++
|
||||||
default:
|
default:
|
||||||
p.Unscan()
|
p.Unscan()
|
||||||
return nil
|
break LOOP
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// if no constraint was added we return an error. i.e:
|
||||||
|
// CREATE TABLE t (a)
|
||||||
|
if fc.IsEmpty() && addedTc == 0 {
|
||||||
|
tok, pos, lit := p.ScanIgnoreWhitespace()
|
||||||
|
return newParseError(scanner.Tokstr(tok, lit), []string{"CONSTRAINT", "TYPE"}, pos)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Parser) parseTableConstraint(stmt *statement.CreateTableStmt) (bool, error) {
|
func (p *Parser) parseTableConstraint(stmt *statement.CreateTableStmt) (bool, error) {
|
||||||
@@ -270,20 +275,8 @@ func (p *Parser) parseTableConstraint(stmt *statement.CreateTableStmt) (bool, er
|
|||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if pk := stmt.Info.FieldConstraints.GetPrimaryKey(); pk != nil {
|
if err := stmt.Info.TableConstraints.AddPrimaryKey(stmt.Info.TableName, primaryKeyPath); err != nil {
|
||||||
return false, stringutil.Errorf("table %q has more than one primary key", stmt.Info.TableName)
|
return false, err
|
||||||
}
|
|
||||||
fc := stmt.Info.FieldConstraints.Get(primaryKeyPath)
|
|
||||||
if fc == nil {
|
|
||||||
err = stmt.Info.FieldConstraints.Add(&database.FieldConstraint{
|
|
||||||
Path: primaryKeyPath,
|
|
||||||
IsPrimaryKey: true,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
fc.IsPrimaryKey = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return true, nil
|
return true, nil
|
||||||
@@ -305,19 +298,28 @@ func (p *Parser) parseTableConstraint(stmt *statement.CreateTableStmt) (bool, er
|
|||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
fc := stmt.Info.FieldConstraints.Get(uniquePath)
|
stmt.Info.TableConstraints.AddUnique(uniquePath)
|
||||||
if fc == nil {
|
return true, nil
|
||||||
err = stmt.Info.FieldConstraints.Add(&database.FieldConstraint{
|
case scanner.CHECK:
|
||||||
Path: uniquePath,
|
// Parse "("
|
||||||
IsUnique: true,
|
err = p.parseTokens(scanner.LPAREN)
|
||||||
})
|
if err != nil {
|
||||||
if err != nil {
|
return false, err
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
fc.IsUnique = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
e, err := p.ParseExpr()
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse ")"
|
||||||
|
err = p.parseTokens(scanner.RPAREN)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
stmt.Info.TableConstraints.AddCheck(stmt.Info.TableName, expr.Constraint(e))
|
||||||
|
|
||||||
return true, nil
|
return true, nil
|
||||||
default:
|
default:
|
||||||
p.Unscan()
|
p.Unscan()
|
||||||
|
|||||||
@@ -30,7 +30,10 @@ func TestParserCreateTable(t *testing.T) {
|
|||||||
Info: database.TableInfo{
|
Info: database.TableInfo{
|
||||||
TableName: "test",
|
TableName: "test",
|
||||||
FieldConstraints: []*database.FieldConstraint{
|
FieldConstraints: []*database.FieldConstraint{
|
||||||
{Path: document.Path(testutil.ParsePath(t, "foo")), Type: types.IntegerValue, IsPrimaryKey: true},
|
{Path: document.Path(testutil.ParsePath(t, "foo")), Type: types.IntegerValue},
|
||||||
|
},
|
||||||
|
TableConstraints: []*database.TableConstraint{
|
||||||
|
{Path: document.Path(testutil.ParsePath(t, "foo")), PrimaryKey: true},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}, false},
|
}, false},
|
||||||
@@ -79,14 +82,37 @@ func TestParserCreateTable(t *testing.T) {
|
|||||||
&statement.CreateTableStmt{
|
&statement.CreateTableStmt{
|
||||||
Info: database.TableInfo{
|
Info: database.TableInfo{
|
||||||
TableName: "test",
|
TableName: "test",
|
||||||
FieldConstraints: []*database.FieldConstraint{
|
TableConstraints: []*database.TableConstraint{
|
||||||
{Path: document.Path(testutil.ParsePath(t, "foo")), IsUnique: true},
|
{Path: document.Path(testutil.ParsePath(t, "foo")), Unique: true},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}, false},
|
}, false},
|
||||||
|
|
||||||
{"With not null twice", "CREATE TABLE test(foo NOT NULL NOT NULL)", nil, true},
|
{"With not null twice", "CREATE TABLE test(foo NOT NULL NOT NULL)", nil, true},
|
||||||
{"With unique twice", "CREATE TABLE test(foo UNIQUE UNIQUE)", nil, true},
|
{"With unique twice", "CREATE TABLE test(foo UNIQUE UNIQUE)", &statement.CreateTableStmt{
|
||||||
|
Info: database.TableInfo{
|
||||||
|
TableName: "test",
|
||||||
|
TableConstraints: []*database.TableConstraint{
|
||||||
|
{Path: document.Path(testutil.ParsePath(t, "foo")), Unique: true},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, false},
|
||||||
|
{"With check", "CREATE TABLE test(a CHECK(a > 10), b int CHECK(a > 20) CHECK(b > 10), CHECK(a > 30))",
|
||||||
|
&statement.CreateTableStmt{
|
||||||
|
Info: database.TableInfo{
|
||||||
|
TableName: "test",
|
||||||
|
FieldConstraints: []*database.FieldConstraint{
|
||||||
|
{Path: document.Path(testutil.ParsePath(t, "b")), Type: types.IntegerValue},
|
||||||
|
},
|
||||||
|
TableConstraints: []*database.TableConstraint{
|
||||||
|
{Name: "test_check", Check: expr.Constraint(testutil.ParseExpr(t, "a > 10"))},
|
||||||
|
{Name: "test_check1", Check: expr.Constraint(testutil.ParseExpr(t, "a > 20"))},
|
||||||
|
{Name: "test_check2", Check: expr.Constraint(testutil.ParseExpr(t, "b > 10"))},
|
||||||
|
{Name: "test_check3", Check: expr.Constraint(testutil.ParseExpr(t, "a > 30"))},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
false},
|
||||||
{"With type and not null", "CREATE TABLE test(foo INTEGER NOT NULL)",
|
{"With type and not null", "CREATE TABLE test(foo INTEGER NOT NULL)",
|
||||||
&statement.CreateTableStmt{
|
&statement.CreateTableStmt{
|
||||||
Info: database.TableInfo{
|
Info: database.TableInfo{
|
||||||
@@ -101,7 +127,10 @@ func TestParserCreateTable(t *testing.T) {
|
|||||||
Info: database.TableInfo{
|
Info: database.TableInfo{
|
||||||
TableName: "test",
|
TableName: "test",
|
||||||
FieldConstraints: []*database.FieldConstraint{
|
FieldConstraints: []*database.FieldConstraint{
|
||||||
{Path: document.Path(testutil.ParsePath(t, "foo")), Type: types.IntegerValue, IsPrimaryKey: true, IsNotNull: true},
|
{Path: document.Path(testutil.ParsePath(t, "foo")), Type: types.IntegerValue, IsNotNull: true},
|
||||||
|
},
|
||||||
|
TableConstraints: []*database.TableConstraint{
|
||||||
|
{Path: document.Path(testutil.ParsePath(t, "foo")), PrimaryKey: true},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}, false},
|
}, false},
|
||||||
@@ -110,7 +139,10 @@ func TestParserCreateTable(t *testing.T) {
|
|||||||
Info: database.TableInfo{
|
Info: database.TableInfo{
|
||||||
TableName: "test",
|
TableName: "test",
|
||||||
FieldConstraints: []*database.FieldConstraint{
|
FieldConstraints: []*database.FieldConstraint{
|
||||||
{Path: document.Path(testutil.ParsePath(t, "foo")), Type: types.IntegerValue, IsPrimaryKey: true, IsNotNull: true},
|
{Path: document.Path(testutil.ParsePath(t, "foo")), Type: types.IntegerValue, IsNotNull: true},
|
||||||
|
},
|
||||||
|
TableConstraints: []*database.TableConstraint{
|
||||||
|
{Path: document.Path(testutil.ParsePath(t, "foo")), PrimaryKey: true},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}, false},
|
}, false},
|
||||||
@@ -119,10 +151,13 @@ func TestParserCreateTable(t *testing.T) {
|
|||||||
Info: database.TableInfo{
|
Info: database.TableInfo{
|
||||||
TableName: "test",
|
TableName: "test",
|
||||||
FieldConstraints: []*database.FieldConstraint{
|
FieldConstraints: []*database.FieldConstraint{
|
||||||
{Path: document.Path(testutil.ParsePath(t, "foo")), Type: types.IntegerValue, IsPrimaryKey: true},
|
{Path: document.Path(testutil.ParsePath(t, "foo")), Type: types.IntegerValue},
|
||||||
{Path: document.Path(testutil.ParsePath(t, "bar")), Type: types.IntegerValue, IsNotNull: true},
|
{Path: document.Path(testutil.ParsePath(t, "bar")), Type: types.IntegerValue, IsNotNull: true},
|
||||||
{Path: document.Path(testutil.ParsePath(t, "baz[4][1].bat")), Type: types.TextValue},
|
{Path: document.Path(testutil.ParsePath(t, "baz[4][1].bat")), Type: types.TextValue},
|
||||||
},
|
},
|
||||||
|
TableConstraints: []*database.TableConstraint{
|
||||||
|
{Path: document.Path(testutil.ParsePath(t, "foo")), PrimaryKey: true},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}, false},
|
}, false},
|
||||||
{"With table constraints / PK on defined field", "CREATE TABLE test(foo INTEGER, bar NOT NULL, PRIMARY KEY (foo))",
|
{"With table constraints / PK on defined field", "CREATE TABLE test(foo INTEGER, bar NOT NULL, PRIMARY KEY (foo))",
|
||||||
@@ -130,9 +165,12 @@ func TestParserCreateTable(t *testing.T) {
|
|||||||
Info: database.TableInfo{
|
Info: database.TableInfo{
|
||||||
TableName: "test",
|
TableName: "test",
|
||||||
FieldConstraints: []*database.FieldConstraint{
|
FieldConstraints: []*database.FieldConstraint{
|
||||||
{Path: document.Path(testutil.ParsePath(t, "foo")), Type: types.IntegerValue, IsPrimaryKey: true},
|
{Path: document.Path(testutil.ParsePath(t, "foo")), Type: types.IntegerValue},
|
||||||
{Path: document.Path(testutil.ParsePath(t, "bar")), IsNotNull: true},
|
{Path: document.Path(testutil.ParsePath(t, "bar")), IsNotNull: true},
|
||||||
},
|
},
|
||||||
|
TableConstraints: []*database.TableConstraint{
|
||||||
|
{Path: document.Path(testutil.ParsePath(t, "foo")), PrimaryKey: true},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}, false},
|
}, false},
|
||||||
{"With table constraints / PK on undefined field", "CREATE TABLE test(foo INTEGER, PRIMARY KEY (bar))",
|
{"With table constraints / PK on undefined field", "CREATE TABLE test(foo INTEGER, PRIMARY KEY (bar))",
|
||||||
@@ -141,7 +179,9 @@ func TestParserCreateTable(t *testing.T) {
|
|||||||
TableName: "test",
|
TableName: "test",
|
||||||
FieldConstraints: []*database.FieldConstraint{
|
FieldConstraints: []*database.FieldConstraint{
|
||||||
{Path: document.Path(testutil.ParsePath(t, "foo")), Type: types.IntegerValue},
|
{Path: document.Path(testutil.ParsePath(t, "foo")), Type: types.IntegerValue},
|
||||||
{Path: document.Path(testutil.ParsePath(t, "bar")), IsPrimaryKey: true},
|
},
|
||||||
|
TableConstraints: []*database.TableConstraint{
|
||||||
|
{Path: document.Path(testutil.ParsePath(t, "bar")), PrimaryKey: true},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}, false},
|
}, false},
|
||||||
@@ -153,9 +193,12 @@ func TestParserCreateTable(t *testing.T) {
|
|||||||
Info: database.TableInfo{
|
Info: database.TableInfo{
|
||||||
TableName: "test",
|
TableName: "test",
|
||||||
FieldConstraints: []*database.FieldConstraint{
|
FieldConstraints: []*database.FieldConstraint{
|
||||||
{Path: document.Path(testutil.ParsePath(t, "foo")), Type: types.IntegerValue, IsUnique: true},
|
{Path: document.Path(testutil.ParsePath(t, "foo")), Type: types.IntegerValue},
|
||||||
{Path: document.Path(testutil.ParsePath(t, "bar")), IsNotNull: true},
|
{Path: document.Path(testutil.ParsePath(t, "bar")), IsNotNull: true},
|
||||||
},
|
},
|
||||||
|
TableConstraints: []*database.TableConstraint{
|
||||||
|
{Path: document.Path(testutil.ParsePath(t, "foo")), Unique: true},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}, false},
|
}, false},
|
||||||
{"With table constraints / UNIQUE on undefined field", "CREATE TABLE test(foo INTEGER, UNIQUE (bar))",
|
{"With table constraints / UNIQUE on undefined field", "CREATE TABLE test(foo INTEGER, UNIQUE (bar))",
|
||||||
@@ -164,7 +207,9 @@ func TestParserCreateTable(t *testing.T) {
|
|||||||
TableName: "test",
|
TableName: "test",
|
||||||
FieldConstraints: []*database.FieldConstraint{
|
FieldConstraints: []*database.FieldConstraint{
|
||||||
{Path: document.Path(testutil.ParsePath(t, "foo")), Type: types.IntegerValue},
|
{Path: document.Path(testutil.ParsePath(t, "foo")), Type: types.IntegerValue},
|
||||||
{Path: document.Path(testutil.ParsePath(t, "bar")), IsUnique: true},
|
},
|
||||||
|
TableConstraints: []*database.TableConstraint{
|
||||||
|
{Path: document.Path(testutil.ParsePath(t, "bar")), Unique: true},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}, false},
|
}, false},
|
||||||
@@ -173,7 +218,10 @@ func TestParserCreateTable(t *testing.T) {
|
|||||||
Info: database.TableInfo{
|
Info: database.TableInfo{
|
||||||
TableName: "test",
|
TableName: "test",
|
||||||
FieldConstraints: []*database.FieldConstraint{
|
FieldConstraints: []*database.FieldConstraint{
|
||||||
{Path: document.Path(testutil.ParsePath(t, "foo")), Type: types.IntegerValue, IsUnique: true},
|
{Path: document.Path(testutil.ParsePath(t, "foo")), Type: types.IntegerValue},
|
||||||
|
},
|
||||||
|
TableConstraints: []*database.TableConstraint{
|
||||||
|
{Path: document.Path(testutil.ParsePath(t, "foo")), Unique: true},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}, false},
|
}, false},
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ func (p *Parser) parseSelectCore() (*statement.StreamStmt, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
stmt.Distinct, err = p.parseDistinct()
|
stmt.Distinct, err = p.parseOptional(scanner.DISTINCT)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -202,15 +202,6 @@ func (p *Parser) parseProjectedExpr() (expr.Expr, error) {
|
|||||||
return ne, nil
|
return ne, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Parser) parseDistinct() (bool, error) {
|
|
||||||
if tok, _, _ := p.ScanIgnoreWhitespace(); tok != scanner.DISTINCT {
|
|
||||||
p.Unscan()
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Parser) parseFrom() (string, error) {
|
func (p *Parser) parseFrom() (string, error) {
|
||||||
if ok, err := p.parseOptional(scanner.FROM); !ok || err != nil {
|
if ok, err := p.parseOptional(scanner.FROM); !ok || err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
|
|||||||
@@ -128,6 +128,7 @@ func TestScanner_Scan(t *testing.T) {
|
|||||||
{s: `BETWEEN`, tok: BETWEEN},
|
{s: `BETWEEN`, tok: BETWEEN},
|
||||||
{s: `CACHE`, tok: CACHE},
|
{s: `CACHE`, tok: CACHE},
|
||||||
{s: `CAST`, tok: CAST},
|
{s: `CAST`, tok: CAST},
|
||||||
|
{s: `CHECK`, tok: CHECK},
|
||||||
{s: `COMMIT`, tok: COMMIT},
|
{s: `COMMIT`, tok: COMMIT},
|
||||||
{s: `CONFLICT`, tok: CONFLICT},
|
{s: `CONFLICT`, tok: CONFLICT},
|
||||||
{s: `CREATE`, tok: CREATE},
|
{s: `CREATE`, tok: CREATE},
|
||||||
|
|||||||
@@ -85,6 +85,7 @@ const (
|
|||||||
BY
|
BY
|
||||||
CACHE
|
CACHE
|
||||||
CAST
|
CAST
|
||||||
|
CHECK
|
||||||
COMMIT
|
COMMIT
|
||||||
CONFLICT
|
CONFLICT
|
||||||
CREATE
|
CREATE
|
||||||
@@ -229,6 +230,7 @@ var tokens = [...]string{
|
|||||||
BY: "BY",
|
BY: "BY",
|
||||||
CACHE: "CACHE",
|
CACHE: "CACHE",
|
||||||
CAST: "CAST",
|
CAST: "CAST",
|
||||||
|
CHECK: "CHECK",
|
||||||
COMMIT: "COMMIT",
|
COMMIT: "COMMIT",
|
||||||
CONFLICT: "CONFLICT",
|
CONFLICT: "CONFLICT",
|
||||||
CREATE: "CREATE",
|
CREATE: "CREATE",
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ type ValueRange struct {
|
|||||||
func (r *ValueRange) evalRange(table *database.Table, env *environment.Environment) (*encodedValueRange, bool, error) {
|
func (r *ValueRange) evalRange(table *database.Table, env *environment.Environment) (*encodedValueRange, bool, error) {
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
pk := table.Info.FieldConstraints.GetPrimaryKey()
|
pk := table.Info.GetPrimaryKey()
|
||||||
|
|
||||||
rng := encodedValueRange{
|
rng := encodedValueRange{
|
||||||
pkType: pk.Type,
|
pkType: pk.Type,
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ func TestOpen(t *testing.T) {
|
|||||||
err = res1.Iterate(func(d types.Document) error {
|
err = res1.Iterate(func(d types.Document) error {
|
||||||
count++
|
count++
|
||||||
if count == 1 {
|
if count == 1 {
|
||||||
testutil.RequireDocJSONEq(t, d, `{"name":"__genji_sequence", "sql":"CREATE TABLE __genji_sequence (name TEXT PRIMARY KEY, seq INTEGER)", "store_name":"X19nZW5qaV9zZXF1ZW5jZQ==", "type":"table"}`)
|
testutil.RequireDocJSONEq(t, d, `{"name":"__genji_sequence", "sql":"CREATE TABLE __genji_sequence (name TEXT, seq INTEGER, PRIMARY KEY (name))", "store_name":"X19nZW5qaV9zZXF1ZW5jZQ==", "type":"table"}`)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,7 +64,7 @@ func TestOpen(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if count == 4 {
|
if count == 4 {
|
||||||
testutil.RequireDocJSONEq(t, d, `{"name":"tableA", "sql":"CREATE TABLE tableA (a INTEGER NOT NULL UNIQUE, b.c[0].d DOUBLE PRIMARY KEY)", "store_name":"AQ==", "type":"table"}`)
|
testutil.RequireDocJSONEq(t, d, `{"name":"tableA", "sql":"CREATE TABLE tableA (a INTEGER NOT NULL, b.c[0].d DOUBLE, UNIQUE (a), PRIMARY KEY (b.c[0].d))", "store_name":"AQ==", "type":"table"}`)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,7 +74,7 @@ func TestOpen(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if count == 6 {
|
if count == 6 {
|
||||||
testutil.RequireDocJSONEq(t, d, `{"name":"tableB", "sql":"CREATE TABLE tableB (a TEXT NOT NULL PRIMARY KEY DEFAULT \"hello\")", "store_name":"Aw==", "type":"table"}`)
|
testutil.RequireDocJSONEq(t, d, `{"name":"tableB", "sql":"CREATE TABLE tableB (a TEXT NOT NULL DEFAULT \"hello\", PRIMARY KEY (a))", "store_name":"Aw==", "type":"table"}`)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ SELECT name, sql FROM __genji_catalog WHERE type = "table" AND (name = "test2" O
|
|||||||
/* result:
|
/* result:
|
||||||
{
|
{
|
||||||
"name": "test2",
|
"name": "test2",
|
||||||
"sql": "CREATE TABLE test2 (a INTEGER PRIMARY KEY)"
|
"sql": "CREATE TABLE test2 (a INTEGER, PRIMARY KEY (a))"
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
73
sqltests/CREATE_TABLE/check.sql
Normal file
73
sqltests/CREATE_TABLE/check.sql
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
-- test: as field constraint
|
||||||
|
CREATE TABLE test (
|
||||||
|
a CHECK(a > 10) CHECK(b < 10)
|
||||||
|
);
|
||||||
|
SELECT name, type, sql FROM __genji_catalog WHERE name = "test";
|
||||||
|
/* result:
|
||||||
|
{
|
||||||
|
name: "test",
|
||||||
|
type: "table",
|
||||||
|
sql: "CREATE TABLE test (CHECK (a > 10), CHECK (b < 10))"
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
-- test: as field constraint, with other constraints
|
||||||
|
CREATE TABLE test (
|
||||||
|
a INT CHECK (a > 10) DEFAULT 100 NOT NULL PRIMARY KEY
|
||||||
|
);
|
||||||
|
SELECT name, type, sql FROM __genji_catalog WHERE name = "test";
|
||||||
|
/* result:
|
||||||
|
{
|
||||||
|
name: "test",
|
||||||
|
type: "table",
|
||||||
|
sql: "CREATE TABLE test (a INTEGER NOT NULL DEFAULT 100, CHECK (a > 10), PRIMARY KEY (a))"
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
-- test: as field constraint, no parentheses
|
||||||
|
CREATE TABLE test (
|
||||||
|
a INT CHECK a > 10
|
||||||
|
);
|
||||||
|
-- error:
|
||||||
|
|
||||||
|
-- test: as field constraint, incompatible default value
|
||||||
|
CREATE TABLE test (
|
||||||
|
a INT CHECK (a > 10) DEFAULT 0
|
||||||
|
);
|
||||||
|
SELECT name, type, sql FROM __genji_catalog WHERE name = "test";
|
||||||
|
/* result:
|
||||||
|
{
|
||||||
|
name: "test",
|
||||||
|
type: "table",
|
||||||
|
sql: "CREATE TABLE test (a INTEGER DEFAULT 0, CHECK (a > 10))"
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
-- test: as field constraint, reference other fields
|
||||||
|
CREATE TABLE test (
|
||||||
|
a INT CHECK (a > 10 AND b < 10),
|
||||||
|
b INT
|
||||||
|
);
|
||||||
|
SELECT name, type, sql FROM __genji_catalog WHERE name = "test";
|
||||||
|
/* result:
|
||||||
|
{
|
||||||
|
name: "test",
|
||||||
|
type: "table",
|
||||||
|
sql: "CREATE TABLE test (a INTEGER, b INTEGER, CHECK (a > 10 AND b < 10))"
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
-- test: as table constraint
|
||||||
|
CREATE TABLE test (
|
||||||
|
a INT,
|
||||||
|
CHECK (a > 10),
|
||||||
|
CHECK (a > 20)
|
||||||
|
);
|
||||||
|
SELECT name, type, sql FROM __genji_catalog WHERE name = "test";
|
||||||
|
/* result:
|
||||||
|
{
|
||||||
|
name: "test",
|
||||||
|
type: "table",
|
||||||
|
sql: "CREATE TABLE test (a INTEGER, CHECK (a > 10), CHECK (a > 20))"
|
||||||
|
}
|
||||||
|
*/
|
||||||
87
sqltests/INSERT/check.sql
Normal file
87
sqltests/INSERT/check.sql
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
/*
|
||||||
|
Check behavior: These tests check the behavior of the check constraint depending
|
||||||
|
on the result of the evaluation of the expression.
|
||||||
|
*/
|
||||||
|
|
||||||
|
-- test: boolean check constraint
|
||||||
|
CREATE TABLE test (a text CHECK(true));
|
||||||
|
INSERT INTO test (a) VALUES ("hello");
|
||||||
|
/* result:
|
||||||
|
{
|
||||||
|
a: "hello"
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
-- test: non-boolean check constraint, numeric result
|
||||||
|
CREATE TABLE test (a text CHECK(1 + 1));
|
||||||
|
INSERT INTO test (a) VALUES ("hello");
|
||||||
|
/* result:
|
||||||
|
{
|
||||||
|
a: "hello"
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
-- test: non-boolean check constraint, non-numeric result
|
||||||
|
CREATE TABLE test (a text CHECK("hello"));
|
||||||
|
INSERT INTO test (a) VALUES ("hello");
|
||||||
|
-- error:
|
||||||
|
|
||||||
|
-- test: non-boolean check constraint, NULL
|
||||||
|
CREATE TABLE test (a text CHECK(NULL));
|
||||||
|
INSERT INTO test (a) VALUES ("hello");
|
||||||
|
/* result:
|
||||||
|
{
|
||||||
|
a: "hello"
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
Field types: These tests check the behavior of the check constraint depending
|
||||||
|
on the type of the field
|
||||||
|
*/
|
||||||
|
|
||||||
|
-- test: no type constraint, valid double
|
||||||
|
CREATE TABLE test (a CHECK(a > 10));
|
||||||
|
INSERT INTO test (a) VALUES (11);
|
||||||
|
SELECT * FROM test;
|
||||||
|
/* result:
|
||||||
|
{
|
||||||
|
a: 11.0
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
-- test: no type constraint, invalid double
|
||||||
|
CREATE TABLE test (a CHECK(a > 10));
|
||||||
|
INSERT INTO test (a) VALUES (1);
|
||||||
|
-- error: document violates check constraint "test_check"
|
||||||
|
|
||||||
|
-- test: no type constraint, multiple checks, invalid double
|
||||||
|
CREATE TABLE test (a CHECK(a > 10), CHECK(a < 20));
|
||||||
|
INSERT INTO test (a) VALUES (40);
|
||||||
|
-- error: document violates check constraint "test_check1"
|
||||||
|
|
||||||
|
-- test: no type constraint, text
|
||||||
|
CREATE TABLE test (a CHECK(a > 10));
|
||||||
|
INSERT INTO test (a) VALUES ("hello");
|
||||||
|
-- error: document violates check constraint "test_check"
|
||||||
|
|
||||||
|
-- test: no type constraint, null
|
||||||
|
CREATE TABLE test (a CHECK(a > 10));
|
||||||
|
INSERT INTO test (b) VALUES (10);
|
||||||
|
SELECT * FROM test;
|
||||||
|
/* result:
|
||||||
|
{
|
||||||
|
b: 10.0
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
-- test: int type constraint, double
|
||||||
|
CREATE TABLE test (a int CHECK(a > 10));
|
||||||
|
INSERT INTO test (a) VALUES (15.2);
|
||||||
|
SELECT * FROM test;
|
||||||
|
/* result:
|
||||||
|
{
|
||||||
|
a: 15
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
42
sqltests/UPDATE/check.sql
Normal file
42
sqltests/UPDATE/check.sql
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
-- test: no type constraint, valid double
|
||||||
|
CREATE TABLE test (a CHECK(a > 10));
|
||||||
|
INSERT INTO test (a) VALUES (11);
|
||||||
|
UPDATE test SET a = 12;
|
||||||
|
SELECT * FROM test;
|
||||||
|
/* result:
|
||||||
|
{
|
||||||
|
a: 12.0
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
-- test: no type constraint, invalid double
|
||||||
|
CREATE TABLE test (a CHECK(a > 10));
|
||||||
|
INSERT INTO test (a) VALUES (11);
|
||||||
|
UPDATE test SET a = 1;
|
||||||
|
-- error: document violates check constraint "test_check"
|
||||||
|
|
||||||
|
-- test: no type constraint, text
|
||||||
|
CREATE TABLE test (a CHECK(a > 10));
|
||||||
|
INSERT INTO test (a) VALUES (11);
|
||||||
|
UPDATE test SET a = "hello";
|
||||||
|
-- error: document violates check constraint "test_check"
|
||||||
|
|
||||||
|
-- test: no type constraint, null
|
||||||
|
CREATE TABLE test (a CHECK(a > 10));
|
||||||
|
INSERT INTO test (a) VALUES (11);
|
||||||
|
UPDATE test UNSET a;
|
||||||
|
SELECT * FROM test;
|
||||||
|
/* result:
|
||||||
|
{}
|
||||||
|
*/
|
||||||
|
|
||||||
|
-- test: int type constraint, double
|
||||||
|
CREATE TABLE test (a int CHECK(a > 10));
|
||||||
|
INSERT INTO test (a) VALUES (11);
|
||||||
|
UPDATE test SET a = 15.2;
|
||||||
|
SELECT * FROM test;
|
||||||
|
/* result:
|
||||||
|
{
|
||||||
|
a: 15
|
||||||
|
}
|
||||||
|
*/
|
||||||
@@ -133,6 +133,7 @@ func parse(r io.Reader, filename string) *testSuite {
|
|||||||
var readingResult bool
|
var readingResult bool
|
||||||
var readingSetup bool
|
var readingSetup bool
|
||||||
var readingSuite bool
|
var readingSuite bool
|
||||||
|
var readingCommentBlock bool
|
||||||
var suiteIndex int = -1
|
var suiteIndex int = -1
|
||||||
|
|
||||||
var lineCount = 0
|
var lineCount = 0
|
||||||
@@ -147,7 +148,11 @@ func parse(r io.Reader, filename string) *testSuite {
|
|||||||
|
|
||||||
switch {
|
switch {
|
||||||
case line == "":
|
case line == "":
|
||||||
// ignore blank lines
|
// ignore blank lines
|
||||||
|
case readingCommentBlock && strings.TrimSpace(line) == "*/":
|
||||||
|
readingCommentBlock = false
|
||||||
|
case readingCommentBlock:
|
||||||
|
// ignore comment blocks
|
||||||
case strings.HasPrefix(line, "-- setup:"):
|
case strings.HasPrefix(line, "-- setup:"):
|
||||||
readingSetup = true
|
readingSetup = true
|
||||||
case strings.HasPrefix(line, "-- suite:"):
|
case strings.HasPrefix(line, "-- suite:"):
|
||||||
@@ -191,9 +196,11 @@ func parse(r io.Reader, filename string) *testSuite {
|
|||||||
curTest.Fails = true
|
curTest.Fails = true
|
||||||
}
|
}
|
||||||
curTest = nil
|
curTest = nil
|
||||||
|
case strings.HasPrefix(line, "/*"): // ignore block comments
|
||||||
case strings.HasPrefix(line, "--"): // ignore normal comments
|
readingCommentBlock = true
|
||||||
|
case strings.HasPrefix(line, "--"):
|
||||||
|
// ignore line comments
|
||||||
|
case !readingResult && strings.TrimSpace(line) == "*/":
|
||||||
default:
|
default:
|
||||||
if readingSuite {
|
if readingSuite {
|
||||||
ts.Suites[suiteIndex].PostSetup += line + "\n"
|
ts.Suites[suiteIndex].PostSetup += line + "\n"
|
||||||
|
|||||||
Reference in New Issue
Block a user