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

sql: feat UUID #1796

Merged
merged 5 commits into from
Sep 20, 2023
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 2 additions & 0 deletions embedded/document/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,8 @@ func collectionFromTable(table *sql.Table) *protomodel.Collection {
colType = protomodel.FieldType_BOOLEAN
case sql.VarcharType:
colType = protomodel.FieldType_STRING
case sql.UUIDType:
colType = protomodel.FieldType_UUID
case sql.IntegerType:
colType = protomodel.FieldType_INTEGER
case sql.Float64Type:
Expand Down
17 changes: 17 additions & 0 deletions embedded/document/type_conversions.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"github.com/codenotary/immudb/embedded/sql"
"github.com/codenotary/immudb/embedded/store"
"github.com/codenotary/immudb/pkg/api/protomodel"
"github.com/google/uuid"

"google.golang.org/protobuf/types/known/structpb"
)
Expand All @@ -38,6 +39,18 @@ var structValueToSqlValue = func(value *structpb.Value, sqlType sql.SQLValueType
}

return sql.NewVarchar(value.GetStringValue()), nil
case sql.UUIDType:
_, ok := value.GetKind().(*structpb.Value_StringValue)
if !ok {
return nil, fmt.Errorf("%w: expecting value of type %s", ErrUnexpectedValue, sqlType)
}

u, err := uuid.Parse(value.GetStringValue())
if err != nil {
return nil, fmt.Errorf("%w: can not parse '%s' as an UUID", err, value.GetStringValue())
}

return sql.NewUUID(u), nil
case sql.IntegerType:
_, ok := value.GetKind().(*structpb.Value_NumberValue)
if !ok {
Expand Down Expand Up @@ -77,6 +90,8 @@ var protomodelValueTypeToSQLValueType = func(stype protomodel.FieldType) (sql.SQ
switch stype {
case protomodel.FieldType_STRING:
return sql.VarcharType, nil
case protomodel.FieldType_UUID:
return sql.UUIDType, nil
case protomodel.FieldType_INTEGER:
return sql.IntegerType, nil
case protomodel.FieldType_DOUBLE:
Expand All @@ -92,6 +107,8 @@ var sqlValueTypeDefaultLength = func(stype sql.SQLValueType) (int, error) {
switch stype {
case sql.VarcharType:
return sql.MaxKeyLen, nil
case sql.UUIDType:
return 0, nil
case sql.IntegerType:
return 0, nil
case sql.BLOBType:
Expand Down
53 changes: 51 additions & 2 deletions embedded/sql/catalog.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import (
"time"

"github.com/codenotary/immudb/embedded/store"
"github.com/google/uuid"
)

// Catalog represents a database catalog containing metadata for all tables in the database.
Expand Down Expand Up @@ -252,7 +253,7 @@ func indexName(tableName string, cols []*Column) string {

buf.WriteString(tableName)

buf.WriteString("[")
buf.WriteString("(")

for c, col := range cols {
buf.WriteString(col.colName)
Expand All @@ -262,7 +263,7 @@ func indexName(tableName string, cols []*Column) string {
}
}

buf.WriteString("]")
buf.WriteString(")")

return buf.String()
}
Expand Down Expand Up @@ -559,6 +560,8 @@ func (c *Column) MaxLen() int {
return 8
case Float64Type:
return 8
case UUIDType:
return 16
}

return c.maxLen
Expand All @@ -582,6 +585,8 @@ func validMaxLenForType(maxLen int, sqlType SQLValueType) bool {
return maxLen == 0 || maxLen == 8
case TimestampType:
return maxLen == 0 || maxLen == 8
case UUIDType:
return maxLen == 0 || maxLen == 16
}

return maxLen >= 0
Expand Down Expand Up @@ -875,6 +880,7 @@ func asType(t string) (SQLValueType, error) {
t == Float64Type ||
t == BooleanType ||
t == VarcharType ||
t == UUIDType ||
t == BLOBType ||
t == TimestampType {
return t, nil
Expand Down Expand Up @@ -1098,6 +1104,20 @@ func EncodeRawValueAsKey(val interface{}, colType SQLValueType, maxLen int) ([]b

return encv, len(blobVal), nil
}
case UUIDType:
{
uuidVal, ok := convVal.(uuid.UUID)
if !ok {
return nil, 0, fmt.Errorf("value is not an UUID: %w", ErrInvalidValue)
}

// notnull + value
encv := make([]byte, 17)
encv[0] = KeyValPrefixNotNull
copy(encv[1:], uuidVal[:])

return encv, 16, nil
}
case TimestampType:
{
if maxLen != 8 {
Expand Down Expand Up @@ -1240,6 +1260,20 @@ func EncodeRawValue(val interface{}, colType SQLValueType, maxLen int) ([]byte,
binary.BigEndian.PutUint32(encv[:], uint32(len(blobVal)))
copy(encv[EncLenLen:], blobVal)

return encv[:], nil
}
case UUIDType:
{
uuidVal, ok := convVal.(uuid.UUID)
if !ok {
return nil, fmt.Errorf("value is not an UUID: %w", ErrInvalidValue)
}

// len(v) + v
var encv [EncLenLen + 16]byte
binary.BigEndian.PutUint32(encv[:], uint32(16))
copy(encv[EncLenLen:], uuidVal[:])

return encv[:], nil
}
case TimestampType:
Expand Down Expand Up @@ -1333,6 +1367,21 @@ func DecodeValue(b []byte, colType SQLValueType) (TypedValue, int, error) {

return &Blob{val: v}, voff, nil
}
case UUIDType:
{
if vlen != 16 {
return nil, 0, ErrCorruptedData
}

u, err := uuid.FromBytes(b[voff : voff+16])
if err != nil {
return nil, 0, fmt.Errorf("%w: %s", ErrCorruptedData, err.Error())
}

voff += vlen

return &UUID{val: u}, voff, nil
}
case TimestampType:
{
if vlen != 8 {
Expand Down
4 changes: 2 additions & 2 deletions embedded/sql/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -193,9 +193,9 @@ func (e *Engine) NewTx(ctx context.Context, opts *TxOptions) (*SQLTx, error) {
rowEntryPrefix := MapKey(
e.prefix,
RowPrefix,
EncodeID(1),
EncodeID(DatabaseID),
EncodeID(table.id),
EncodeID(0),
EncodeID(primaryIndex.id),
)

mappedPKEntryPrefix := MapKey(
Expand Down
84 changes: 79 additions & 5 deletions embedded/sql/engine_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,80 @@ func TestTimestampCasts(t *testing.T) {
})
}

func TestUUIDAsPK(t *testing.T) {
engine := setupCommonTest(t)

_, _, err := engine.Exec(context.Background(), nil, "CREATE TABLE IF NOT EXISTS uuid_table(id UUID, PRIMARY KEY id)", nil)
require.NoError(t, err)

sel := EncodeSelector("", "uuid_table", "id")

t.Run("UUID as PK", func(t *testing.T) {
_, _, err = engine.Exec(context.Background(), nil, "INSERT INTO uuid_table(id) VALUES(RANDOM_UUID())", nil)
require.NoError(t, err)

_, err := engine.InferParameters(context.Background(), nil, "SELECT id FROM uuid_table WHERE id = NOW()")
require.ErrorIs(t, err, ErrInvalidTypes)

r, err := engine.Query(context.Background(), nil, "SELECT id FROM uuid_table", nil)
require.NoError(t, err)
defer r.Close()

row, err := r.Read(context.Background())
require.NoError(t, err)
require.Equal(t, UUIDType, row.ValuesBySelector[sel].Type())

require.Len(t, row.ValuesByPosition, 1)
require.Equal(t, row.ValuesByPosition[0], row.ValuesBySelector[sel])
})

t.Run("must accept RANDOM_UUID() as an UUID", func(t *testing.T) {
_, _, err = engine.Exec(context.Background(), nil, "INSERT INTO uuid_table(id) VALUES(RANDOM_UUID())", nil)
require.NoError(t, err)

_, err := engine.InferParameters(context.Background(), nil, "SELECT id FROM uuid_table WHERE id = NOW()")
require.ErrorIs(t, err, ErrInvalidTypes)

r, err := engine.Query(context.Background(), nil, "SELECT id FROM uuid_table", nil)
require.NoError(t, err)
defer r.Close()

row, err := r.Read(context.Background())
require.NoError(t, err)
require.Equal(t, UUIDType, row.ValuesBySelector[sel].Type())

require.Len(t, row.ValuesByPosition, 1)
require.Equal(t, row.ValuesByPosition[0], row.ValuesBySelector[sel])
})

}

func TestUUIDNonPK(t *testing.T) {
engine := setupCommonTest(t)

_, _, err := engine.Exec(context.Background(), nil, "CREATE TABLE uuid_table(id INTEGER, u UUID, t VARCHAR, PRIMARY KEY id)", nil)
require.NoError(t, err)

sel := EncodeSelector("", "uuid_table", "u")

t.Run("UUID as non PK", func(t *testing.T) {
_, _, err = engine.Exec(context.Background(), nil, "INSERT INTO uuid_table(id, u, t) VALUES(1, RANDOM_UUID(), 't')", nil)
require.NoError(t, err)

r, err := engine.Query(context.Background(), nil, "SELECT u FROM uuid_table", nil)
require.NoError(t, err)
defer r.Close()

row, err := r.Read(context.Background())
require.NoError(t, err)
require.Equal(t, UUIDType, row.ValuesBySelector[sel].Type())

require.Len(t, row.ValuesByPosition, 1)
require.Equal(t, row.ValuesByPosition[0], row.ValuesBySelector[sel])
})

}

func TestFloatType(t *testing.T) {
engine := setupCommonTest(t)

Expand Down Expand Up @@ -5980,14 +6054,14 @@ func TestSingleDBCatalogQueries(t *testing.T) {
row, err := r.Read(context.Background())
require.NoError(t, err)
require.Equal(t, "mytable1", row.ValuesBySelector["(indexes.table)"].RawValue())
require.Equal(t, "mytable1[id]", row.ValuesBySelector["(indexes.name)"].RawValue())
require.Equal(t, "mytable1(id)", row.ValuesBySelector["(indexes.name)"].RawValue())
require.True(t, row.ValuesBySelector["(indexes.unique)"].RawValue().(bool))
require.True(t, row.ValuesBySelector["(indexes.primary)"].RawValue().(bool))

row, err = r.Read(context.Background())
require.NoError(t, err)
require.Equal(t, "mytable1", row.ValuesBySelector["(indexes.table)"].RawValue())
require.Equal(t, "mytable1[title]", row.ValuesBySelector["(indexes.name)"].RawValue())
require.Equal(t, "mytable1(title)", row.ValuesBySelector["(indexes.name)"].RawValue())
require.False(t, row.ValuesBySelector["(indexes.unique)"].RawValue().(bool))
require.False(t, row.ValuesBySelector["(indexes.primary)"].RawValue().(bool))

Expand All @@ -6004,21 +6078,21 @@ func TestSingleDBCatalogQueries(t *testing.T) {
row, err := r.Read(context.Background())
require.NoError(t, err)
require.Equal(t, "mytable2", row.ValuesBySelector["(indexes.table)"].RawValue())
require.Equal(t, "mytable2[id]", row.ValuesBySelector["(indexes.name)"].RawValue())
require.Equal(t, "mytable2(id)", row.ValuesBySelector["(indexes.name)"].RawValue())
require.True(t, row.ValuesBySelector["(indexes.unique)"].RawValue().(bool))
require.True(t, row.ValuesBySelector["(indexes.primary)"].RawValue().(bool))

row, err = r.Read(context.Background())
require.NoError(t, err)
require.Equal(t, "mytable2", row.ValuesBySelector["(indexes.table)"].RawValue())
require.Equal(t, "mytable2[name]", row.ValuesBySelector["(indexes.name)"].RawValue())
require.Equal(t, "mytable2(name)", row.ValuesBySelector["(indexes.name)"].RawValue())
require.False(t, row.ValuesBySelector["(indexes.unique)"].RawValue().(bool))
require.False(t, row.ValuesBySelector["(indexes.primary)"].RawValue().(bool))

row, err = r.Read(context.Background())
require.NoError(t, err)
require.Equal(t, "mytable2", row.ValuesBySelector["(indexes.table)"].RawValue())
require.Equal(t, "mytable2[name,active]", row.ValuesBySelector["(indexes.name)"].RawValue())
require.Equal(t, "mytable2(name,active)", row.ValuesBySelector["(indexes.name)"].RawValue())
require.True(t, row.ValuesBySelector["(indexes.unique)"].RawValue().(bool))
require.False(t, row.ValuesBySelector["(indexes.primary)"].RawValue().(bool))

Expand Down
4 changes: 4 additions & 0 deletions embedded/sql/grouped_row_reader.go
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,10 @@ func zeroForType(t SQLValueType) TypedValue {
{
return &Varchar{}
}
case UUIDType:
{
return &UUID{}
}
case BLOBType:
{
return &Blob{}
Expand Down
1 change: 1 addition & 0 deletions embedded/sql/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ var types = map[string]SQLValueType{
"INTEGER": IntegerType,
"BOOLEAN": BooleanType,
"VARCHAR": VarcharType,
"UUID": UUIDType,
"BLOB": BLOBType,
"TIMESTAMP": TimestampType,
"FLOAT": Float64Type,
Expand Down
11 changes: 11 additions & 0 deletions embedded/sql/parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,17 @@ func TestCreateTableStmt(t *testing.T) {
}},
expectedError: nil,
},
{
input: "CREATE TABLE table1 (id UUID, PRIMARY KEY id)",
expectedOutput: []SQLStmt{
&CreateTableStmt{
table: "table1",
ifNotExists: false,
colsSpec: []*ColSpec{{colName: "id", colType: UUIDType}},
pkColNames: []string{"id"},
}},
expectedError: nil,
},
{
input: "CREATE TABLE table1 (id INTEGER AUTO_INCREMENT, PRIMARY KEY id)",
expectedOutput: []SQLStmt{
Expand Down
Loading