mirror of
https://github.com/chaisql/chai.git
synced 2025-09-27 12:02:09 +08:00
424 lines
16 KiB
Go
424 lines
16 KiB
Go
package statement_test
|
|
|
|
import (
|
|
"bytes"
|
|
"testing"
|
|
|
|
"github.com/genjidb/genji/document"
|
|
"github.com/genjidb/genji/internal/database"
|
|
"github.com/genjidb/genji/internal/expr"
|
|
"github.com/genjidb/genji/internal/sql/parser"
|
|
"github.com/genjidb/genji/internal/testutil"
|
|
"github.com/genjidb/genji/internal/testutil/assert"
|
|
"github.com/genjidb/genji/types"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func parsePath(t testing.TB, str string) document.Path {
|
|
vp, err := parser.ParsePath(str)
|
|
assert.NoError(t, err)
|
|
return vp
|
|
}
|
|
|
|
func TestCreateTable(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
query string
|
|
fails bool
|
|
}{
|
|
{"Basic", `CREATE TABLE test`, false},
|
|
{"Exists", "CREATE TABLE test;CREATE TABLE test", true},
|
|
{"If not exists", "CREATE TABLE IF NOT EXISTS test", false},
|
|
{"If not exists, twice", "CREATE TABLE IF NOT EXISTS test;CREATE TABLE IF NOT EXISTS test", false},
|
|
{"With primary key", "CREATE TABLE test(foo TEXT PRIMARY KEY)", false},
|
|
{"With field constraints", "CREATE TABLE test(foo.a[1][2] TEXT primary key, bar[4][0].bat INTEGER not null, baz not null)", false},
|
|
{"With no constraints", "CREATE TABLE test(a, b)", true},
|
|
{"With coherent constraint(common)", "CREATE TABLE test(a DOCUMENT, a.b ARRAY, a.b[0] TEXT);", false},
|
|
{"With coherent constraint(document)", "CREATE TABLE test(a DOCUMENT, a.b TEXT);", false},
|
|
{"With coherent constraint(array)", "CREATE TABLE test(a ARRAY, a[0] TEXT);", false},
|
|
{"With incoherent constraint(any)", "CREATE TABLE test(a, a.b[0] TEXT);", true},
|
|
{"With incoherent constraint(common)", "CREATE TABLE test(a INTEGER, a.b[0] TEXT);", true},
|
|
{"With incoherent constraint(common)", "CREATE TABLE test(a DOCUMENT, a.b[0] TEXT, a.b.c TEXT);", true},
|
|
{"With incoherent constraint(common)", "CREATE TABLE test(a DOCUMENT, a.b.c TEXT, a.b[0] TEXT);", true},
|
|
{"With incoherent constraint(document)", "CREATE TABLE test(a INTEGER, a.b TEXT);", true},
|
|
{"With incoherent constraint(array)", "CREATE TABLE test(a INTEGER, a[0] TEXT);", true},
|
|
{"With duplicate constraints", "CREATE TABLE test(a INTEGER, a TEXT);", true},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
db, tx, cleanup := testutil.NewTestTx(t)
|
|
defer cleanup()
|
|
|
|
err := testutil.Exec(db, tx, test.query)
|
|
if test.fails {
|
|
assert.Error(t, err)
|
|
return
|
|
}
|
|
assert.NoError(t, err)
|
|
|
|
_, err = db.Catalog.GetTable(tx, "test")
|
|
assert.NoError(t, err)
|
|
})
|
|
}
|
|
|
|
t.Run("constraints", func(t *testing.T) {
|
|
t.Run("with fixed size data types", func(t *testing.T) {
|
|
db, tx, cleanup := testutil.NewTestTx(t)
|
|
defer cleanup()
|
|
|
|
testutil.MustExec(t, db, tx, `CREATE TABLE test(d double, b bool)`)
|
|
|
|
tb, err := db.Catalog.GetTable(tx, "test")
|
|
assert.NoError(t, err)
|
|
|
|
require.Equal(t, database.FieldConstraints{
|
|
{Path: parsePath(t, "d"), Type: types.DoubleValue},
|
|
{Path: parsePath(t, "b"), Type: types.BoolValue},
|
|
}, tb.Info.FieldConstraints)
|
|
assert.NoError(t, err)
|
|
})
|
|
|
|
t.Run("with variable size data types", func(t *testing.T) {
|
|
db, tx, cleanup := testutil.NewTestTx(t)
|
|
defer cleanup()
|
|
|
|
testutil.MustExec(t, db, tx, `
|
|
CREATE TABLE test1(
|
|
foo.bar[1].hello bytes PRIMARY KEY, foo.a[1][2] TEXT NOT NULL, bar[4][0].bat integer, b blob, t text, a array, d document
|
|
)
|
|
`)
|
|
|
|
tb, err := db.Catalog.GetTable(tx, "test1")
|
|
assert.NoError(t, err)
|
|
|
|
require.Equal(t, database.FieldConstraints{
|
|
{Path: parsePath(t, "foo"), Type: types.DocumentValue, IsInferred: true,
|
|
InferredBy: []document.Path{
|
|
parsePath(t, "foo.bar[1].hello"),
|
|
parsePath(t, "foo.a[1][2]"),
|
|
}},
|
|
{Path: parsePath(t, "foo.bar"), Type: types.ArrayValue, IsInferred: true,
|
|
InferredBy: []document.Path{
|
|
parsePath(t, "foo.bar[1].hello"),
|
|
}},
|
|
{Path: parsePath(t, "foo.bar[1]"), Type: types.DocumentValue, IsInferred: true,
|
|
InferredBy: []document.Path{
|
|
parsePath(t, "foo.bar[1].hello"),
|
|
}},
|
|
{Path: parsePath(t, "foo.bar[1].hello"), Type: types.BlobValue},
|
|
{Path: parsePath(t, "foo.a"), Type: types.ArrayValue, IsInferred: true,
|
|
InferredBy: []document.Path{
|
|
parsePath(t, "foo.a[1][2]"),
|
|
}},
|
|
{Path: parsePath(t, "foo.a[1]"), Type: types.ArrayValue, IsInferred: true,
|
|
InferredBy: []document.Path{
|
|
parsePath(t, "foo.a[1][2]"),
|
|
}},
|
|
{Path: parsePath(t, "foo.a[1][2]"), Type: types.TextValue, IsNotNull: true},
|
|
{Path: parsePath(t, "bar"), Type: types.ArrayValue, IsInferred: true,
|
|
InferredBy: []document.Path{
|
|
parsePath(t, "bar[4][0].bat"),
|
|
}},
|
|
{Path: parsePath(t, "bar[4]"), Type: types.ArrayValue, IsInferred: true,
|
|
InferredBy: []document.Path{
|
|
parsePath(t, "bar[4][0].bat"),
|
|
}},
|
|
{Path: parsePath(t, "bar[4][0]"), Type: types.DocumentValue, IsInferred: true,
|
|
InferredBy: []document.Path{
|
|
parsePath(t, "bar[4][0].bat"),
|
|
}},
|
|
{Path: parsePath(t, "bar[4][0].bat"), Type: types.IntegerValue},
|
|
{Path: parsePath(t, "b"), Type: types.BlobValue},
|
|
{Path: parsePath(t, "t"), Type: types.TextValue},
|
|
{Path: parsePath(t, "a"), Type: types.ArrayValue},
|
|
{Path: parsePath(t, "d"), Type: types.DocumentValue},
|
|
}, tb.Info.FieldConstraints)
|
|
assert.NoError(t, err)
|
|
})
|
|
|
|
t.Run("with variable aliases data types", func(t *testing.T) {
|
|
db, tx, cleanup := testutil.NewTestTx(t)
|
|
defer cleanup()
|
|
|
|
testutil.MustExec(t, db, tx, `
|
|
CREATE TABLE test2(
|
|
foo.bar[1].hello bytes PRIMARY KEY, foo.a[1][2] VARCHAR(255) NOT NULL, bar[4][0].bat tinyint,
|
|
dp double precision, r real, b bigint, m mediumint, eight int8, ii int2, c character(64)
|
|
)
|
|
`)
|
|
|
|
tb, err := db.Catalog.GetTable(tx, "test2")
|
|
assert.NoError(t, err)
|
|
|
|
require.Equal(t, database.FieldConstraints{{Path: parsePath(t, "foo"), Type: types.DocumentValue, IsInferred: true,
|
|
InferredBy: []document.Path{
|
|
parsePath(t, "foo.bar[1].hello"),
|
|
parsePath(t, "foo.a[1][2]"),
|
|
}},
|
|
{Path: parsePath(t, "foo.bar"), Type: types.ArrayValue, IsInferred: true,
|
|
InferredBy: []document.Path{
|
|
parsePath(t, "foo.bar[1].hello"),
|
|
}},
|
|
{Path: parsePath(t, "foo.bar[1]"), Type: types.DocumentValue, IsInferred: true,
|
|
InferredBy: []document.Path{
|
|
parsePath(t, "foo.bar[1].hello"),
|
|
}},
|
|
{Path: parsePath(t, "foo.bar[1].hello"), Type: types.BlobValue},
|
|
{Path: parsePath(t, "foo.a"), Type: types.ArrayValue, IsInferred: true,
|
|
InferredBy: []document.Path{
|
|
parsePath(t, "foo.a[1][2]"),
|
|
}},
|
|
{Path: parsePath(t, "foo.a[1]"), Type: types.ArrayValue, IsInferred: true,
|
|
InferredBy: []document.Path{
|
|
parsePath(t, "foo.a[1][2]"),
|
|
}},
|
|
{Path: parsePath(t, "foo.a[1][2]"), Type: types.TextValue, IsNotNull: true},
|
|
{Path: parsePath(t, "bar"), Type: types.ArrayValue, IsInferred: true,
|
|
InferredBy: []document.Path{
|
|
parsePath(t, "bar[4][0].bat"),
|
|
}},
|
|
{Path: parsePath(t, "bar[4]"), Type: types.ArrayValue, IsInferred: true,
|
|
InferredBy: []document.Path{
|
|
parsePath(t, "bar[4][0].bat"),
|
|
}},
|
|
{Path: parsePath(t, "bar[4][0]"), Type: types.DocumentValue, IsInferred: true,
|
|
InferredBy: []document.Path{
|
|
parsePath(t, "bar[4][0].bat"),
|
|
}},
|
|
{Path: parsePath(t, "bar[4][0].bat"), Type: types.IntegerValue},
|
|
{Path: parsePath(t, "dp"), Type: types.DoubleValue},
|
|
{Path: parsePath(t, "r"), Type: types.DoubleValue},
|
|
{Path: parsePath(t, "b"), Type: types.IntegerValue},
|
|
{Path: parsePath(t, "m"), Type: types.IntegerValue},
|
|
{Path: parsePath(t, "eight"), Type: types.IntegerValue},
|
|
{Path: parsePath(t, "ii"), Type: types.IntegerValue},
|
|
{Path: parsePath(t, "c"), Type: types.TextValue},
|
|
}, tb.Info.FieldConstraints)
|
|
assert.NoError(t, err)
|
|
})
|
|
|
|
t.Run("default values", func(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
query string
|
|
constraints database.FieldConstraints
|
|
fails bool
|
|
}{
|
|
{"With default, no type and integer default", "CREATE TABLE test(foo DEFAULT 10)", database.FieldConstraints{{Path: parsePath(t, "foo"), DefaultValue: expr.Constraint(testutil.IntegerValue(10))}}, false},
|
|
{"With default, double type and integer default", "CREATE TABLE test(foo DOUBLE DEFAULT 10)", database.FieldConstraints{{Path: parsePath(t, "foo"), Type: types.DoubleValue, DefaultValue: expr.Constraint(testutil.IntegerValue(10))}}, false},
|
|
{"With default, some type and compatible default", "CREATE TABLE test(foo BOOL DEFAULT 10)", database.FieldConstraints{{Path: parsePath(t, "foo"), Type: types.BoolValue, DefaultValue: expr.Constraint(testutil.IntegerValue(10))}}, false},
|
|
{"With default, some type and incompatible default", "CREATE TABLE test(foo BOOL DEFAULT 10.5)", nil, true},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
db, tx, cleanup := testutil.NewTestTx(t)
|
|
defer cleanup()
|
|
|
|
err := testutil.Exec(db, tx, test.query)
|
|
if test.fails {
|
|
assert.Error(t, err)
|
|
return
|
|
}
|
|
assert.NoError(t, err)
|
|
|
|
tb, err := db.Catalog.GetTable(tx, "test")
|
|
assert.NoError(t, err)
|
|
|
|
for _, fc := range test.constraints {
|
|
if fc.DefaultValue != nil {
|
|
fc.DefaultValue.(*expr.ConstraintExpr).Catalog = db.Catalog
|
|
}
|
|
}
|
|
require.Equal(t, test.constraints, tb.Info.FieldConstraints)
|
|
})
|
|
}
|
|
})
|
|
|
|
t.Run("unique", func(t *testing.T) {
|
|
db, tx, cleanup := testutil.NewTestTx(t)
|
|
defer cleanup()
|
|
|
|
testutil.MustExec(t, db, tx, "CREATE TABLE test (a INT UNIQUE, b DOUBLE UNIQUE, c UNIQUE)")
|
|
|
|
tb, err := db.Catalog.GetTable(tx, "test")
|
|
assert.NoError(t, err)
|
|
require.Len(t, tb.Info.FieldConstraints, 2)
|
|
require.Len(t, tb.Info.TableConstraints, 3)
|
|
|
|
require.Equal(t, &database.FieldConstraint{
|
|
Path: parsePath(t, "a"),
|
|
Type: types.IntegerValue,
|
|
}, tb.Info.FieldConstraints[0])
|
|
|
|
require.Equal(t, &database.FieldConstraint{
|
|
Path: parsePath(t, "b"),
|
|
Type: types.DoubleValue,
|
|
}, tb.Info.FieldConstraints[1])
|
|
|
|
require.Equal(t, &database.TableConstraint{
|
|
Path: parsePath(t, "a"),
|
|
Unique: true,
|
|
}, 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")
|
|
assert.NoError(t, err)
|
|
require.Equal(t, types.IntegerValue, idx.Info.Types[0])
|
|
require.True(t, idx.Info.Unique)
|
|
|
|
idx, err = db.Catalog.GetIndex(tx, "test_b_idx")
|
|
assert.NoError(t, err)
|
|
require.Equal(t, types.DoubleValue, idx.Info.Types[0])
|
|
require.True(t, idx.Info.Unique)
|
|
|
|
idx, err = db.Catalog.GetIndex(tx, "test_c_idx")
|
|
assert.NoError(t, err)
|
|
require.Zero(t, idx.Info.Types[0])
|
|
require.True(t, idx.Info.Unique)
|
|
assert.NoError(t, err)
|
|
})
|
|
|
|
t.Run("default with nested doc", func(t *testing.T) {
|
|
db, tx, cleanup := testutil.NewTestTx(t)
|
|
defer cleanup()
|
|
|
|
err := testutil.Exec(db, tx, "CREATE TABLE test (a.b.c TEXT DEFAULT 1 + 1)")
|
|
assert.NoError(t, err)
|
|
|
|
var buf bytes.Buffer
|
|
err = testutil.IteratorToJSONArray(&buf, testutil.MustQuery(t, db, tx, "INSERT INTO test VALUES {}; SELECT * FROM test"))
|
|
assert.NoError(t, err)
|
|
require.JSONEq(t, `[{"a": {"b": {"c": "2"}}}]`, buf.String())
|
|
})
|
|
})
|
|
}
|
|
|
|
func TestCreateIndex(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
query string
|
|
fails bool
|
|
}{
|
|
{"Basic", "CREATE INDEX idx ON test (foo)", false},
|
|
{"If not exists", "CREATE INDEX IF NOT EXISTS idx ON test (foo.bar)", false},
|
|
{"Duplicate", "CREATE INDEX idx ON test (foo.bar);CREATE INDEX idx ON test (foo.bar)", true},
|
|
{"Unique", "CREATE UNIQUE INDEX IF NOT EXISTS idx ON test (foo[1])", false},
|
|
{"No name", "CREATE UNIQUE INDEX ON test (foo[1])", false},
|
|
{"No name if not exists", "CREATE UNIQUE INDEX IF NOT EXISTS ON test (foo[1])", true},
|
|
{"No fields", "CREATE INDEX idx ON test", true},
|
|
{"Composite (2)", "CREATE INDEX idx ON test (foo, bar)", false},
|
|
{"Composite (4)", "CREATE INDEX idx ON test (foo, bar, baz, baf)", false},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
db, tx, cleanup := testutil.NewTestTx(t)
|
|
defer cleanup()
|
|
|
|
testutil.MustExec(t, db, tx, "CREATE TABLE test")
|
|
|
|
err := testutil.Exec(db, tx, test.query)
|
|
if test.fails {
|
|
assert.Error(t, err)
|
|
return
|
|
}
|
|
assert.NoError(t, err)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCreateSequence(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
query string
|
|
fails bool
|
|
}{
|
|
{"Basic", "CREATE SEQUENCE seq", false},
|
|
{"If not exists", "CREATE SEQUENCE IF NOT EXISTS seq", false},
|
|
{"AS integer", "CREATE SEQUENCE seq AS TINYINT", false},
|
|
{"AS double", "CREATE SEQUENCE seq AS DOUBLE", true},
|
|
{"INCREMENT", "CREATE SEQUENCE seq INCREMENT 10", false},
|
|
{"INCREMENT BY", "CREATE SEQUENCE seq INCREMENT BY 10", false},
|
|
{"INCREMENT BY 0", "CREATE SEQUENCE seq INCREMENT BY 0", true},
|
|
{"NO MINVALUE", "CREATE SEQUENCE seq NO MINVALUE", false},
|
|
{"NO MAXVALUE", "CREATE SEQUENCE seq NO MAXVALUE", false},
|
|
{"NO CYCLE", "CREATE SEQUENCE seq NO CYCLE", false},
|
|
{"NO SUGAR", "CREATE SEQUENCE seq NO SUGAR", true},
|
|
{"MINVALUE 10", "CREATE SEQUENCE seq MINVALUE 10", false},
|
|
{"MINVALUE 'hello'", "CREATE SEQUENCE seq MINVALUE 'hello'", true},
|
|
{"MAXVALUE 10", "CREATE SEQUENCE seq MAXVALUE 10", false},
|
|
{"MAXVALUE 'hello'", "CREATE SEQUENCE seq MAXVALUE 'hello'", true},
|
|
{"START WITH 10", "CREATE SEQUENCE seq START WITH 10", false},
|
|
{"START WITH 'hello'", "CREATE SEQUENCE seq START WITH 'hello'", true},
|
|
{"START 10", "CREATE SEQUENCE seq START 10", false},
|
|
{"CACHE 10", "CREATE SEQUENCE seq CACHE 10", false},
|
|
{"CACHE 'hello'", "CREATE SEQUENCE seq CACHE 'hello'", true},
|
|
{"CACHE -10", "CREATE SEQUENCE seq CACHE -10", true},
|
|
{"CYCLE", "CREATE SEQUENCE seq CYCLE", false},
|
|
{"Order 1", `
|
|
CREATE SEQUENCE IF NOT EXISTS seq
|
|
AS INTEGER
|
|
INCREMENT BY 2
|
|
NO MINVALUE
|
|
MAXVALUE 10
|
|
START WITH 5
|
|
CACHE 5
|
|
CYCLE
|
|
`, false},
|
|
{"Order 2", `
|
|
CREATE SEQUENCE IF NOT EXISTS seq
|
|
CYCLE
|
|
MAXVALUE 10
|
|
INCREMENT BY 2
|
|
START WITH 5
|
|
AS INTEGER
|
|
NO MINVALUE
|
|
CACHE 5
|
|
`, false},
|
|
{"NO MINVALUE with MINVALUE 10", "CREATE SEQUENCE seq NO MINVALUE MINVALUE 10", true},
|
|
{"NO MAXVALUE with MAXVALUE 10", "CREATE SEQUENCE seq NO MAXVALUE MAXVALUE 10", true},
|
|
{"NO CYCLE with CYCLE", "CREATE SEQUENCE seq NO MAXVALUE MAXVALUE 10", true},
|
|
{"duplicate AS INT", "CREATE SEQUENCE seq AS INT AS INT", true},
|
|
{"duplicate INCREMENT BY", "CREATE SEQUENCE seq INCREMENT BY 10 INCREMENT BY 10", true},
|
|
{"duplicate NO MINVALUE", "CREATE SEQUENCE seq NO MINVALUE NO MINVALUE", true},
|
|
{"duplicate NO MAXVALUE", "CREATE SEQUENCE seq NO MAXVALUE NO MAXVALUE", true},
|
|
{"duplicate NO CYCLE", "CREATE SEQUENCE seq NO CYCLE NO CYCLE", true},
|
|
{"duplicate MINVALUE", "CREATE SEQUENCE seq MINVALUE 10 MINVALUE 10", true},
|
|
{"duplicate MAXVALUE", "CREATE SEQUENCE seq MAXVALUE 10 MAXVALUE 10", true},
|
|
{"duplicate START WITH", "CREATE SEQUENCE seq START WITH 10 START WITH 10", true},
|
|
{"duplicate CACHE", "CREATE SEQUENCE seq CACHE 10 CACHE 10", true},
|
|
{"duplicate CYCLE", "CREATE SEQUENCE seq CYCLE CYCLE", true},
|
|
{"BAD MINVALUE MAXVALUE", "CREATE SEQUENCE seq MINVALUE 10 MAXVALUE 5", true},
|
|
{"BAD START", "CREATE SEQUENCE seq MINVALUE 5 MAXVALUE 10 START 100", true},
|
|
{"BAD START", "CREATE SEQUENCE seq MINVALUE 5 MAXVALUE 10 START -100", true},
|
|
{"MINVALUE 10 DESC", "CREATE SEQUENCE seq MINVALUE 10 MAXVALUE 100 INCREMENT BY -1", false},
|
|
{"NO MINVALUE DESC", "CREATE SEQUENCE seq NO MINVALUE MAXVALUE 100 INCREMENT BY -1", false},
|
|
{"NO MAXVALUE DESC", "CREATE SEQUENCE seq NO MINVALUE NO MAXVALUE INCREMENT BY -1", false},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
db, tx, cleanup := testutil.NewTestTx(t)
|
|
defer cleanup()
|
|
|
|
err := testutil.Exec(db, tx, test.query)
|
|
if test.fails {
|
|
assert.Error(t, err)
|
|
return
|
|
}
|
|
assert.NoError(t, err)
|
|
})
|
|
}
|
|
}
|