mirror of
https://github.com/chaisql/chai.git
synced 2025-10-05 15:46:55 +08:00
Treat all NULL values differently in UNIQUE indexes
SQL standard can be interpreted differently: either NULL values are all unique (SQLite, PostgreSQL, ... and now Genji) or they are considered equal (SQL Server, ...).
This commit is contained in:
@@ -55,7 +55,6 @@ func ArrayContains(a types.Array, v types.Value) (bool, error) {
|
|||||||
// ValueBuffer is an array that holds values in memory.
|
// ValueBuffer is an array that holds values in memory.
|
||||||
type ValueBuffer struct {
|
type ValueBuffer struct {
|
||||||
Values []types.Value
|
Values []types.Value
|
||||||
err error
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewValueBuffer creates a buffer of values.
|
// NewValueBuffer creates a buffer of values.
|
||||||
|
@@ -134,7 +134,7 @@ func testDocumentGetByField(t *testing.T, codecBuilder func() encoding.Codec) {
|
|||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
require.Equal(t, types.NewTextValue("john"), v)
|
require.Equal(t, types.NewTextValue("john"), v)
|
||||||
|
|
||||||
v, err = d.GetByField("d")
|
_, err = d.GetByField("d")
|
||||||
assert.ErrorIs(t, err, document.ErrFieldNotFound)
|
assert.ErrorIs(t, err, document.ErrFieldNotFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -23,29 +23,8 @@ var doc types.Document = func() types.Document {
|
|||||||
|
|
||||||
var envWithDoc = environment.New(doc)
|
var envWithDoc = environment.New(doc)
|
||||||
|
|
||||||
var envWithDocAndKey *environment.Environment = func() *environment.Environment {
|
|
||||||
env := environment.New(doc)
|
|
||||||
env.Set(environment.TableKey, types.NewTextValue("string"))
|
|
||||||
env.Set(environment.DocPKKey, types.NewBlobValue([]byte("foo")))
|
|
||||||
return env
|
|
||||||
}()
|
|
||||||
|
|
||||||
var nullLiteral = types.NewNullValue()
|
var nullLiteral = types.NewNullValue()
|
||||||
|
|
||||||
func testExpr(t testing.TB, exprStr string, env *environment.Environment, want types.Value, fails bool) {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
e, err := parser.NewParser(strings.NewReader(exprStr)).ParseExpr()
|
|
||||||
assert.NoError(t, err)
|
|
||||||
res, err := e.Eval(env)
|
|
||||||
if fails {
|
|
||||||
assert.Error(t, err)
|
|
||||||
} else {
|
|
||||||
assert.NoError(t, err)
|
|
||||||
require.Equal(t, want, res)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestString(t *testing.T) {
|
func TestString(t *testing.T) {
|
||||||
var operands = []string{
|
var operands = []string{
|
||||||
`10.4`,
|
`10.4`,
|
||||||
|
@@ -636,7 +636,7 @@ func scanBareIdent(r io.RuneScanner) string {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
break
|
break
|
||||||
} else if !isIdentChar(ch) {
|
} else if !isIdentChar(ch) {
|
||||||
r.UnreadRune()
|
_ = r.UnreadRune()
|
||||||
break
|
break
|
||||||
} else {
|
} else {
|
||||||
_, _ = buf.WriteRune(ch)
|
_, _ = buf.WriteRune(ch)
|
||||||
|
@@ -150,11 +150,7 @@ func (op *IndexValidateOperator) Iterate(in *environment.Environment, fn func(ou
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
var newEnv environment.Environment
|
|
||||||
|
|
||||||
return op.Prev.Iterate(in, func(out *environment.Environment) error {
|
return op.Prev.Iterate(in, func(out *environment.Environment) error {
|
||||||
newEnv.SetOuter(out)
|
|
||||||
|
|
||||||
doc, ok := out.GetDocument()
|
doc, ok := out.GetDocument()
|
||||||
if !ok {
|
if !ok {
|
||||||
return errors.New("missing document")
|
return errors.New("missing document")
|
||||||
@@ -162,28 +158,37 @@ func (op *IndexValidateOperator) Iterate(in *environment.Environment, fn func(ou
|
|||||||
|
|
||||||
vs := make([]types.Value, 0, len(info.Paths))
|
vs := make([]types.Value, 0, len(info.Paths))
|
||||||
|
|
||||||
|
// if the indexes values contain NULL somewhere,
|
||||||
|
// we don't check for unicity.
|
||||||
|
// cf: https://sqlite.org/lang_createindex.html#unique_indexes
|
||||||
|
var hasNull bool
|
||||||
for _, path := range info.Paths {
|
for _, path := range info.Paths {
|
||||||
v, err := path.GetValueFromDocument(doc)
|
v, err := path.GetValueFromDocument(doc)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
hasNull = true
|
||||||
v = types.NewNullValue()
|
v = types.NewNullValue()
|
||||||
|
} else if v.Type() == types.NullValue {
|
||||||
|
hasNull = true
|
||||||
}
|
}
|
||||||
|
|
||||||
vs = append(vs, v)
|
vs = append(vs, v)
|
||||||
}
|
}
|
||||||
|
|
||||||
duplicate, key, err := idx.Exists(vs)
|
if !hasNull {
|
||||||
if err != nil {
|
duplicate, key, err := idx.Exists(vs)
|
||||||
return err
|
if err != nil {
|
||||||
}
|
return err
|
||||||
if duplicate {
|
}
|
||||||
return &errs.ConstraintViolationError{
|
if duplicate {
|
||||||
Constraint: "UNIQUE",
|
return &errs.ConstraintViolationError{
|
||||||
Paths: info.Paths,
|
Constraint: "UNIQUE",
|
||||||
Key: key,
|
Paths: info.Paths,
|
||||||
|
Key: key,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return fn(&newEnv)
|
return fn(out)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
28
sqltests/INSERT/unique.sql
Normal file
28
sqltests/INSERT/unique.sql
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
-- setup:
|
||||||
|
CREATE TABLE test (a int unique, b int);
|
||||||
|
|
||||||
|
-- test: same value
|
||||||
|
INSERT INTO test (a, b) VALUES (1, 1);
|
||||||
|
INSERT INTO test (a, b) VALUES (1, 1);
|
||||||
|
-- error:
|
||||||
|
|
||||||
|
-- test: same value, same statement
|
||||||
|
INSERT INTO test (a, b) VALUES (1, 1), (1, 1);
|
||||||
|
-- error:
|
||||||
|
|
||||||
|
-- test: different values
|
||||||
|
INSERT INTO test (a, b) VALUES (1, 1), (2, 2);
|
||||||
|
/* result:
|
||||||
|
{a: 1, b: 1}
|
||||||
|
{a: 2, b: 2}
|
||||||
|
*/
|
||||||
|
|
||||||
|
-- test: NULL
|
||||||
|
INSERT INTO test (b) VALUES (1), (2);
|
||||||
|
INSERT INTO test (a, b) VALUES (NULL, 3);
|
||||||
|
SELECT a, b FROM test;
|
||||||
|
/* result:
|
||||||
|
{a: NULL, b: 1}
|
||||||
|
{a: NULL, b: 2}
|
||||||
|
{a: NULL, b: 3}
|
||||||
|
*/
|
38
sqltests/INSERT/unique_composite.sql
Normal file
38
sqltests/INSERT/unique_composite.sql
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
-- setup:
|
||||||
|
CREATE TABLE test (a int, b int, c int, d int, UNIQUE (a, b, c));
|
||||||
|
|
||||||
|
-- test: same value
|
||||||
|
INSERT INTO test (a, b, c, d) VALUES (1, 1, 1, 1);
|
||||||
|
INSERT INTO test (a, b, c, d) VALUES (1, 1, 1, 1);
|
||||||
|
-- error:
|
||||||
|
|
||||||
|
-- test: same value, same statement
|
||||||
|
INSERT INTO test (a, b, c, d) VALUES (1, 1, 1, 1), (1, 1, 1, 1);
|
||||||
|
-- error:
|
||||||
|
|
||||||
|
-- test: different values
|
||||||
|
INSERT INTO test (a, b, c, d) VALUES (1, 1, 1, 1), (1, 2, 1, 1);
|
||||||
|
/* result:
|
||||||
|
{a: 1, b: 1, c: 1, d: 1}
|
||||||
|
{a: 1, b: 2, c: 1, d: 1}
|
||||||
|
*/
|
||||||
|
|
||||||
|
-- test: NULL
|
||||||
|
INSERT INTO test (d) VALUES (1), (2);
|
||||||
|
INSERT INTO test (c, d) VALUES (3, 3);
|
||||||
|
INSERT INTO test (c, d) VALUES (3, 3);
|
||||||
|
INSERT INTO test (b, c, d) VALUES (4, 4, 4);
|
||||||
|
INSERT INTO test (b, c, d) VALUES (4, 4, 4);
|
||||||
|
INSERT INTO test (a, b, c, d) VALUES (5, null, 5, 5);
|
||||||
|
INSERT INTO test (a, c, d) VALUES (5, 5, 5);
|
||||||
|
SELECT a, b, c, d FROM test;
|
||||||
|
/* result:
|
||||||
|
{a: NULL, b: NULL, c: NULL, d: 1}
|
||||||
|
{a: NULL, b: NULL, c: NULL, d: 2}
|
||||||
|
{a: NULL, b: NULL, c: 3, d: 3}
|
||||||
|
{a: NULL, b: NULL, c: 3, d: 3}
|
||||||
|
{a: NULL, b: 4, c: 4, d: 4}
|
||||||
|
{a: NULL, b: 4, c: 4, d: 4}
|
||||||
|
{a: 5, b: NULL, c: 5, d: 5}
|
||||||
|
{a: 5, b: NULL, c: 5, d: 5}
|
||||||
|
*/
|
Reference in New Issue
Block a user