Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Max/check constraint #317

Merged
merged 19 commits into from
Mar 17, 2021
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ func (e *Engine) QueryWithBindings(
case *plan.CreateIndex:
typ = sql.CreateIndexProcess
perm = auth.ReadPerm | auth.WritePerm
case *plan.CreateForeignKey, *plan.DropForeignKey, *plan.AlterIndex, *plan.CreateView,
case *plan.CreateForeignKey, *plan.CreateCheck, *plan.DropForeignKey, *plan.AlterIndex, *plan.CreateView,
*plan.DeleteFrom, *plan.DropIndex, *plan.DropView,
*plan.InsertInto, *plan.LockTables, *plan.UnlockTables,
*plan.Update:
Expand Down Expand Up @@ -242,7 +242,7 @@ func ResolveDefaults(tableName string, schema []*ColumnWithRawDefault) (sql.Sche
return unresolvedSchema, nil
}
// *plan.CreateTable properly handles resolving default values, so we hijack it
createTable := plan.NewCreateTable(db, tableName, unresolvedSchema, false, nil, nil)
createTable := plan.NewCreateTable(db, tableName, false, &plan.TableSpec{Schema: unresolvedSchema})
analyzed, err := e.Analyzer.Analyze(ctx, createTable, nil)
if err != nil {
return nil, err
Expand Down
217 changes: 217 additions & 0 deletions enginetest/enginetests.go
Original file line number Diff line number Diff line change
Expand Up @@ -2187,6 +2187,223 @@ func TestDropForeignKeys(t *testing.T, harness Harness) {
assert.True(t, sql.ErrTableNotFound.Is(err))
}

func TestCreateCheckConstraints(t *testing.T, harness Harness) {
require := require.New(t)

e := NewEngine(t, harness)

TestQuery(t, harness, e,
max-hoffman marked this conversation as resolved.
Show resolved Hide resolved
"CREATE TABLE t1 (a INTEGER PRIMARY KEY, b INTEGER)",
[]sql.Row(nil),
nil,
)
TestQuery(t, harness, e,
"ALTER TABLE t1 ADD CONSTRAINT chk1 CHECK (b > 0)",
[]sql.Row(nil),
nil,
)
TestQuery(t, harness, e,
"ALTER TABLE t1 ADD CONSTRAINT chk2 CHECK (b > 0) NOT ENFORCED",
[]sql.Row(nil),
nil,
)

db, err := e.Catalog.Database("mydb")
require.NoError(err)

ctx := NewContext(harness)
table, ok, err := db.GetTableInsensitive(ctx, "t1")
require.NoError(err)
require.True(ok)

cht, ok := table.(sql.CheckTable)
require.True(ok)

checks, err := cht.GetChecks(NewContext(harness))
require.NoError(err)

con := []sql.CheckConstraint{
max-hoffman marked this conversation as resolved.
Show resolved Hide resolved
{
Name: "chk1",
Expr: expression.NewGreaterThan(
expression.NewUnresolvedColumn("t1.b"),
expression.NewLiteral(int8(0), sql.Int8),
),
Enforced: true,
},
{
Name: "chk2",
Expr: expression.NewGreaterThan(
expression.NewUnresolvedColumn("t1.b"),
expression.NewLiteral(int8(0), sql.Int8),
),
Enforced: false,
},
}
cmp1, _ := plan.NewCheckDefinition(&con[0])
cmp2, _ := plan.NewCheckDefinition(&con[1])
expected := []sql.CheckDefinition{*cmp1, *cmp2}

assert.Equal(t, expected, checks)

// Some faulty create statements
_, _, err = e.Query(NewContext(harness), "ALTER TABLE t2 ADD CONSTRAINT chk2 CHECK (c > 0)")
require.Error(err)
assert.True(t, sql.ErrTableNotFound.Is(err))

_, _, err = e.Query(NewContext(harness), "ALTER TABLE t1 ADD CONSTRAINT chk3 CHECK (c > 0)")
require.Error(err)
assert.True(t, sql.ErrTableColumnNotFound.Is(err))
}

func TestChecksOnInsert(t *testing.T, harness Harness) {

e := NewEngine(t, harness)

TestQuery(t, harness, e,
"CREATE TABLE t1 (a INTEGER PRIMARY KEY, b INTEGER)",
[]sql.Row(nil),
nil,
)
TestQuery(t, harness, e,
"ALTER TABLE t1 ADD CONSTRAINT chk1 CHECK (b > 1) NOT ENFORCED",
[]sql.Row(nil),
nil,
)
TestQuery(t, harness, e,
"ALTER TABLE t1 ADD CONSTRAINT chk2 CHECK (b > 0)",
[]sql.Row(nil),
nil,
)
TestQuery(t, harness, e,
"ALTER TABLE t1 ADD CONSTRAINT chk3 CHECK (b > -1) ENFORCED",
[]sql.Row(nil),
nil,
)
RunQuery(t, e, harness, "INSERT INTO t1 VALUES (1,1)")
TestQuery(t, harness, e, `SELECT * FROM t1`,
[]sql.Row{
{1, 1},
},
nil,
)
RunQuery(t, e, harness, "INSERT INTO t1 VALUES (0,0)")
TestQuery(t, harness, e, `SELECT * FROM t1`,
[]sql.Row{
{1, 1},
},
nil,
)

ctx := NewContext(harness)
require.True(t, len(ctx.Warnings()) > 0)

expectedCode := 3819
condition := false
for _, warning := range ctx.Warnings() {
if warning.Code == expectedCode {
condition = true
break
}
}

require.True(t, condition)

}

func TestDisallowedCheckConstraints(t *testing.T, harness Harness) {
require := require.New(t)
e := NewEngine(t, harness)
var err error

TestQuery(t, harness, e,
"CREATE TABLE t1 (a INTEGER PRIMARY KEY, b INTEGER)",
[]sql.Row(nil),
nil,
)

// functions, UDFs, procedures
_, _, err = e.Query(NewContext(harness), "ALTER TABLE t1 ADD CONSTRAINT chk2 CHECK (current_user = \"root@\")")
require.Error(err)
assert.True(t, sql.ErrInvalidConstraintFunctionsNotSupported.Is(err))

_, _, err = e.Query(NewContext(harness), "ALTER TABLE t1 ADD CONSTRAINT chk2 CHECK ((select count(*) from t1) = 0)")
require.Error(err)
assert.True(t, sql.ErrInvalidConstraintSubqueryNotSupported.Is(err))
}

func TestDropCheckConstraints(t *testing.T, harness Harness) {
require := require.New(t)

e := NewEngine(t, harness)

TestQuery(t, harness, e,
"CREATE TABLE t1 (a INTEGER PRIMARY KEY, b INTEGER, c integer)",
[]sql.Row(nil),
nil,
)
TestQuery(t, harness, e,
"ALTER TABLE t1 ADD CONSTRAINT chk1 CHECK (a > 0)",
[]sql.Row(nil),
nil,
)
TestQuery(t, harness, e,
"ALTER TABLE t1 ADD CONSTRAINT chk2 CHECK (b > 0) NOT ENFORCED",
[]sql.Row(nil),
nil,
)
TestQuery(t, harness, e,
"ALTER TABLE t1 ADD CONSTRAINT chk3 CHECK (c > 0)",
[]sql.Row(nil),
nil,
)
TestQuery(t, harness, e,
"ALTER TABLE t1 DROP CONSTRAINT chk2",
[]sql.Row(nil),
nil,
)
TestQuery(t, harness, e,
"ALTER TABLE t1 DROP CHECK chk1",
[]sql.Row(nil),
nil,
)

db, err := e.Catalog.Database("mydb")
require.NoError(err)

ctx := NewContext(harness)
table, ok, err := db.GetTableInsensitive(ctx, "t1")
require.NoError(err)
require.True(ok)

cht, ok := table.(sql.CheckTable)
require.True(ok)

checks, err := cht.GetChecks(NewContext(harness))
require.NoError(err)

con := sql.CheckConstraint{
Name: "chk3",
Expr: expression.NewGreaterThan(
expression.NewUnresolvedColumn("t1.c"),
expression.NewLiteral(int8(0), sql.Int8),
),
Enforced: true,
}
cmp, _ := plan.NewCheckDefinition(&con)
expected := []sql.CheckDefinition{*cmp}

assert.Equal(t, expected, checks)

// Some faulty create statements
_, _, err = e.Query(NewContext(harness), "ALTER TABLE t2 DROP CONSTRAINT chk2")
require.Error(err)
assert.True(t, sql.ErrTableNotFound.Is(err))

_, _, err = e.Query(NewContext(harness), "ALTER TABLE t1 DROP CHECK chk3")
require.NoError(err)
}

func TestNaturalJoin(t *testing.T, harness Harness) {
require := require.New(t)

Expand Down
9 changes: 9 additions & 0 deletions enginetest/harness.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,15 @@ type ForeignKeyHarness interface {
SupportsForeignKeys() bool
}

// CheckConstraintHarness is an extension to Harness that lets an integrator test their implementation with check constraints.
// Integrator tables must implement sql.CheckAlterableTable and sql.CheckTable.
type CheckConstraintHarness interface {
max-hoffman marked this conversation as resolved.
Show resolved Hide resolved
Harness
// SupportsCheckConstraint returns whether this harness should accept CREATE CHECK statements as part of test
// setup.
SupportsCheckConstraint() bool
}

// VersionedDBHarness is an extension to Harness that lets an integrator test their implementation of versioned (AS OF)
// queries. Integrators must implement sql.VersionedDatabase. For each table version being created, there will be a
// call to NewTableAsOf, some number of Delete and Insert operations, and then a call to SnapshotTable.
Expand Down
16 changes: 16 additions & 0 deletions enginetest/memory_engine_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,22 @@ func TestDropForeignKeys(t *testing.T) {
enginetest.TestDropForeignKeys(t, enginetest.NewDefaultMemoryHarness())
}

func TestCreateCheckConstraints(t *testing.T) {
enginetest.TestCreateCheckConstraints(t, enginetest.NewDefaultMemoryHarness())
}

func TestChecksOnInsert(t *testing.T) {
enginetest.TestChecksOnInsert(t, enginetest.NewDefaultMemoryHarness())
}

func TestTestDisallowedCheckConstraints(t *testing.T) {
enginetest.TestDisallowedCheckConstraints(t, enginetest.NewDefaultMemoryHarness())
}

func TestDropCheckConstraints(t *testing.T) {
enginetest.TestDropCheckConstraints(t, enginetest.NewDefaultMemoryHarness())
}

func TestExplode(t *testing.T) {
enginetest.TestExplode(t, enginetest.NewDefaultMemoryHarness())
}
Expand Down
50 changes: 49 additions & 1 deletion memory/table.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ type Table struct {
columns []int
indexes map[string]sql.Index
foreignKeys []sql.ForeignKeyConstraint
checks []sql.CheckDefinition
pkIndexesEnabled bool

// Data storage
Expand Down Expand Up @@ -66,6 +67,8 @@ var _ sql.IndexAlterableTable = (*Table)(nil)
var _ sql.IndexedTable = (*Table)(nil)
var _ sql.ForeignKeyAlterableTable = (*Table)(nil)
var _ sql.ForeignKeyTable = (*Table)(nil)
var _ sql.CheckAlterableTable = (*Table)(nil)
var _ sql.CheckTable = (*Table)(nil)
var _ sql.AutoIncrementTable = (*Table)(nil)
var _ sql.StatisticsTable = (*Table)(nil)

Expand Down Expand Up @@ -1103,6 +1106,12 @@ func (t *Table) CreateForeignKey(_ *sql.Context, fkName string, columns []string
}
}

for _, key := range t.checks {
if key.Name == fkName {
return fmt.Errorf("constraint %s already exists", fkName)
}
}

t.foreignKeys = append(t.foreignKeys, sql.ForeignKeyConstraint{
Name: fkName,
Columns: columns,
Expand All @@ -1117,15 +1126,54 @@ func (t *Table) CreateForeignKey(_ *sql.Context, fkName string, columns []string

// DropForeignKey implements sql.ForeignKeyAlterableTable.
func (t *Table) DropForeignKey(ctx *sql.Context, fkName string) error {
return t.dropConstraint(ctx, fkName)
}

func (t *Table) dropConstraint(ctx *sql.Context, name string) error {
for i, key := range t.foreignKeys {
if key.Name == fkName {
if key.Name == name {
t.foreignKeys = append(t.foreignKeys[:i], t.foreignKeys[i+1:]...)
return nil
}
}
for i, key := range t.checks {
if key.Name == name {
t.checks = append(t.checks[:i], t.checks[i+1:]...)
return nil
}
}
return nil
}

// GetChecks implements sql.CheckTable
func (t *Table) GetChecks(_ *sql.Context) ([]sql.CheckDefinition, error) {
return t.checks, nil
}

// CreateCheck implements sql.CheckAlterableTable
func (t *Table) CreateCheck(_ *sql.Context, check *sql.CheckDefinition) error {
for _, key := range t.checks {
if key.Name == check.Name {
return fmt.Errorf("constraint %s already exists", check.Name)
}
}

for _, key := range t.foreignKeys {
if key.Name == check.Name {
return fmt.Errorf("constraint %s already exists", check.Name)
}
}

t.checks = append(t.checks, *check)

return nil
}

// func (t *Table) DropCheck(ctx *sql.Context, chName string) error {} implements sql.CheckAlterableTable.
func (t *Table) DropCheck(ctx *sql.Context, chName string) error {
return t.dropConstraint(ctx, chName)
}

func (t *Table) createIndex(name string, columns []sql.IndexColumn, constraint sql.IndexConstraint, comment string) (sql.Index, error) {
if t.indexes[name] != nil {
// TODO: extract a standard error type for this
Expand Down
Loading