Skip to content

Commit

Permalink
Parse blob literal (#423)
Browse files Browse the repository at this point in the history
We now have a literal representation for BLOBs. Any string literal starting
with '\x' is parsed as an hex encoded blob. This mimics PostgreSQL's behavior.

```sql
SELECT '\xAAFF';
```
  • Loading branch information
asdine committed Jul 23, 2021
1 parent 1659ae2 commit 43a9b3e
Show file tree
Hide file tree
Showing 17 changed files with 305 additions and 74 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ build: $(NAME)
$(NAME):
cd ./cmd/$@ && go install

gen: $(NAME)
gen:
go generate ./...

test:
Expand Down
18 changes: 8 additions & 10 deletions document/cast.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,8 +121,11 @@ func CastAsDouble(v types.Value) (types.Value, error) {
// CastAsText returns a JSON representation of v.
// If the representation is a string, it gets unquoted.
func CastAsText(v types.Value) (types.Value, error) {
if v.Type() == types.TextValue {
switch v.Type() {
case types.TextValue:
return v, nil
case types.BlobValue:
return types.NewTextValue(base64.StdEncoding.EncodeToString(v.V().([]byte))), nil
}

d, err := ValueToJSON(v)
Expand All @@ -132,13 +135,6 @@ func CastAsText(v types.Value) (types.Value, error) {

s := string(d)

if v.Type() == types.BlobValue {
s, err = strconv.Unquote(s)
if err != nil {
return nil, err
}
}

return types.NewTextValue(s), nil
}

Expand All @@ -151,9 +147,11 @@ func CastAsBlob(v types.Value) (types.Value, error) {
}

if v.Type() == types.TextValue {
b, err := base64.StdEncoding.DecodeString(v.V().(string))
// if the string starts with \x, read it as hex
s := v.V().(string)
b, err := base64.StdEncoding.DecodeString(s)
if err != nil {
return nil, stringutil.Errorf(`cannot cast %q as blob: %w`, v.V(), err)
return nil, err
}

return types.NewBlobValue(b), nil
Expand Down
12 changes: 8 additions & 4 deletions document/cast_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ func TestCastAs(t *testing.T) {
integerV := types.NewIntegerValue(10)
doubleV := types.NewDoubleValue(10.5)
textV := types.NewTextValue("foo")
blobV := types.NewBlobValue([]byte("abc"))
blobV := types.NewBlobValue([]byte("asdine"))
arrayV := types.NewArrayValue(NewValueBuffer().
Append(types.NewTextValue("bar")).
Append(integerV))
Expand All @@ -26,8 +26,12 @@ func TestCastAs(t *testing.T) {
Add("b", textV))

check := func(t *testing.T, targetType types.ValueType, tests []test) {
t.Helper()

for _, test := range tests {
t.Run(ValueToString(test.v), func(t *testing.T) {
t.Helper()

got, err := CastAs(test.v, targetType)
if test.fails {
require.Error(t, err)
Expand Down Expand Up @@ -89,7 +93,7 @@ func TestCastAs(t *testing.T) {
{integerV, types.NewTextValue("10"), false},
{doubleV, types.NewTextValue("10.5"), false},
{textV, textV, false},
{blobV, types.NewTextValue("YWJj"), false},
{blobV, types.NewTextValue(`YXNkaW5l`), false},
{arrayV, types.NewTextValue(`["bar", 10]`), false},
{docV,
types.NewTextValue(`{"a": 10, "b": "foo"}`),
Expand All @@ -102,8 +106,8 @@ func TestCastAs(t *testing.T) {
{boolV, nil, true},
{integerV, nil, true},
{doubleV, nil, true},
{types.NewTextValue("YWJj"), blobV, false},
{types.NewTextValue(" dww "), nil, true},
{types.NewTextValue("YXNkaW5l"), types.NewBlobValue([]byte{0x61, 0x73, 0x64, 0x69, 0x6e, 0x65}), false},
{types.NewTextValue("not base64"), nil, true},
{blobV, blobV, false},
{arrayV, nil, true},
{docV, nil, true},
Expand Down
2 changes: 0 additions & 2 deletions document/json.go
Original file line number Diff line number Diff line change
Expand Up @@ -175,8 +175,6 @@ func ValueToString(v types.Value) string {
return "NULL"
case types.TextValue:
return strconv.Quote(v.V().(string))
case types.BlobValue:
return stringutil.Sprintf("%v", v.V())
}

d, _ := ValueToJSON(v)
Expand Down
4 changes: 2 additions & 2 deletions document/json_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ func TestValueString(t *testing.T) {
expected string
}{
{"null", types.NewNullValue(), "NULL"},
{"bytes", types.NewBlobValue([]byte("bar")), "[98 97 114]"},
{"string", types.NewTextValue("bar"), "\"bar\""},
{"blob", types.NewBlobValue([]byte("bar")), `"YmFy"`},
{"string", types.NewTextValue("bar"), `"bar"`},
{"bool", types.NewBoolValue(true), "true"},
{"int", types.NewIntegerValue(10), "10"},
{"double", types.NewDoubleValue(10.1), "10.1"},
Expand Down
7 changes: 1 addition & 6 deletions internal/database/constraint.go
Original file line number Diff line number Diff line change
Expand Up @@ -361,12 +361,7 @@ type ConversionFunc func(v types.Value, path document.Path, targetType types.Val

// CastConversion is a ConversionFunc that casts the value to the target type.
func CastConversion(v types.Value, path document.Path, targetType types.ValueType) (types.Value, error) {
newV, err := document.CastAs(v, targetType)
if err != nil {
return v, stringutil.Errorf("field %q must be of type %q, got %q", path, targetType, v.Type())
}

return newV, nil
return document.CastAs(v, targetType)
}

// ConvertValueAtPath converts the value using the field constraints that are applicable
Expand Down
45 changes: 0 additions & 45 deletions internal/expr/functions/builtins.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,51 +93,6 @@ func (k *PK) String() string {
return "pk()"
}

// Cast represents the CAST expression.
type Cast struct {
Expr expr.Expr
CastAs types.ValueType
}

// Eval returns the primary key of the current document.
func (c Cast) Eval(env *environment.Environment) (types.Value, error) {
v, err := c.Expr.Eval(env)
if err != nil {
return v, err
}

return document.CastAs(v, c.CastAs)
}

// IsEqual compares this expression with the other expression and returns
// true if they are equal.
func (c Cast) IsEqual(other expr.Expr) bool {
if other == nil {
return false
}

o, ok := other.(Cast)
if !ok {
return false
}

if c.CastAs != o.CastAs {
return false
}

if c.Expr != nil {
return expr.Equal(c.Expr, o.Expr)
}

return o.Expr != nil
}

func (c Cast) Params() []expr.Expr { return []expr.Expr{c.Expr} }

func (c Cast) String() string {
return stringutil.Sprintf("CAST(%v AS %v)", c.Expr, c.CastAs)
}

var _ expr.AggregatorBuilder = (*Count)(nil)

// Count is the COUNT aggregator function. It counts the number of documents
Expand Down
3 changes: 3 additions & 0 deletions internal/expr/literal.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,9 @@ func (l LiteralExprList) String() string {
// Eval evaluates all the expressions and returns a literalValueList. It implements the Expr interface.
func (l LiteralExprList) Eval(env *environment.Environment) (types.Value, error) {
var err error
if len(l) == 0 {
return types.NewArrayValue(document.NewValueBuffer()), nil
}
values := make([]types.Value, len(l))
for i, e := range l {
values[i], err = e.Eval(env)
Expand Down
46 changes: 46 additions & 0 deletions internal/expr/operator.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package expr
import (
"errors"

"github.com/genjidb/genji/document"
"github.com/genjidb/genji/internal/environment"
"github.com/genjidb/genji/internal/sql/scanner"
"github.com/genjidb/genji/internal/stringutil"
Expand Down Expand Up @@ -119,3 +120,48 @@ func (op *ConcatOperator) Eval(env *environment.Environment) (types.Value, error
return types.NewTextValue(a.V().(string) + b.V().(string)), nil
})
}

// Cast represents the CAST expression.
type Cast struct {
Expr Expr
CastAs types.ValueType
}

// Eval returns the primary key of the current document.
func (c Cast) Eval(env *environment.Environment) (types.Value, error) {
v, err := c.Expr.Eval(env)
if err != nil {
return v, err
}

return document.CastAs(v, c.CastAs)
}

// IsEqual compares this expression with the other expression and returns
// true if they are equal.
func (c Cast) IsEqual(other Expr) bool {
if other == nil {
return false
}

o, ok := other.(Cast)
if !ok {
return false
}

if c.CastAs != o.CastAs {
return false
}

if c.Expr != nil {
return Equal(c.Expr, o.Expr)
}

return o.Expr != nil
}

func (c Cast) Params() []Expr { return []Expr{c.Expr} }

func (c Cast) String() string {
return stringutil.Sprintf("CAST(%v AS %v)", c.Expr, c.CastAs)
}
5 changes: 5 additions & 0 deletions internal/expr/operator_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package expr_test

import (
"path/filepath"
"testing"

"github.com/genjidb/genji/internal/testutil"
Expand All @@ -25,3 +26,7 @@ func TestConcatExpr(t *testing.T) {
})
}
}

func TestCast(t *testing.T) {
testutil.ExprRunner(t, filepath.Join("testdata", "cast.sql"))
}
38 changes: 38 additions & 0 deletions internal/expr/testdata/arithmetic.sql
Original file line number Diff line number Diff line change
Expand Up @@ -75,3 +75,41 @@ NULL

! 1 ^ a
'field not found'

-- test: division
> 1 / 2
0

> 1 + 1 * 2 / 4
1

-- test: arithmetic with different types
> 1 + 1.5
2.5

> 1 + '2'
NULL

> 1 + true
NULL

> 1 + [1]
NULL

> 1 + {a: 1}
NULL

> [1] + [1]
NULL

> {a: 1} + {a: 1}
NULL

> 4.5 + 4.5
9.0

> 1000000000 * 1000000000
1000000000000000000

> 1000000000000000000 * 1000000000000000000 * 1000000000000000000
1000000000000000000000000000000000000000000000000000000
Loading

0 comments on commit 43a9b3e

Please sign in to comment.