Repository: genjidb/genji
Pull request #424 (2021-07-24 14:05:45)

Fix field name when using alias
This fixes a bug where using AS wouldn't rename the field

codecov-commenter: # [Codecov](https://codecov.io/gh/genjidb/genji/pull/424?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=genjidb) Report
> Merging [#424](https://codecov.io/gh/genjidb/genji/pull/424?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=genjidb) (314f9e3) into [main](https://codecov.io/gh/genjidb/genji/commit/43a9b3e5603fe8309fbf53b144251105164b0083?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=genjidb) (43a9b3e) will **decrease** coverage by `0.01%`.
> The diff coverage is `100.00%`.

[![Impacted file tree graph](https://codecov.io/gh/genjidb/genji/pull/424/graphs/tree.svg?width=650&height=150&src=pr&token=nwR1GpvtyH&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=genjidb)](https://codecov.io/gh/genjidb/genji/pull/424?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=genjidb)

```diff
@@            Coverage Diff             @@
##             main     #424      +/-   ##
==========================================
- Coverage   68.90%   68.88%   -0.02%     
==========================================
  Files          93       93              
  Lines        9639     9633       -6     
==========================================
- Hits         6642     6636       -6     
  Misses       2281     2281              
  Partials      716      716              
```


| [Impacted Files](https://codecov.io/gh/genjidb/genji/pull/424?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=genjidb) | Coverage Δ | |
|---|---|---|
| [document/json.go](https://codecov.io/gh/genjidb/genji/pull/424/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=genjidb#diff-ZG9jdW1lbnQvanNvbi5nbw==) | `80.35% <100.00%> (ø)` | |
| [internal/query/statement/select.go](https://codecov.io/gh/genjidb/genji/pull/424/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=genjidb#diff-aW50ZXJuYWwvcXVlcnkvc3RhdGVtZW50L3NlbGVjdC5nbw==) | `89.13% <100.00%> (-0.77%)` | :arrow_down: |
| [internal/sql/parser/select.go](https://codecov.io/gh/genjidb/genji/pull/424/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=genjidb#diff-aW50ZXJuYWwvc3FsL3BhcnNlci9zZWxlY3QuZ28=) | `61.95% <100.00%> (+0.41%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/genjidb/genji/pull/424?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=genjidb).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=genjidb)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/genjidb/genji/pull/424?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=genjidb). Last update [43a9b3e...314f9e3](https://codecov.io/gh/genjidb/genji/pull/424?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=genjidb). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=genjidb).


Patch:
diff --git a/document/json.go b/document/json.go
index 3c1eaf26..0c60747a 100644
--- a/document/json.go
+++ b/document/json.go
@@ -5,6 +5,7 @@ import (
 	"encoding/base64"
 	"math"
 	"strconv"
+	"strings"
 
 	"github.com/buger/jsonparser"
 	"github.com/genjidb/genji/internal/stringutil"
@@ -174,7 +175,7 @@ func ValueToString(v types.Value) string {
 	case types.NullValue:
 		return "NULL"
 	case types.TextValue:
-		return strconv.Quote(v.V().(string))
+		return "'" + strings.ReplaceAll(v.V().(string), "'", "\\'") + "'"
 	}
 
 	d, _ := ValueToJSON(v)
diff --git a/internal/query/statement/select.go b/internal/query/statement/select.go
index 62932c9c..8c5a5ef5 100644
--- a/internal/query/statement/select.go
+++ b/internal/query/statement/select.go
@@ -121,22 +121,8 @@ func (stmt *SelectStmt) ToStream() (*StreamStmt, error) {
 				return nil, err
 			}
 		}
-
-		// build a document expression with the
-		// projected exprs
-		d := expr.KVPairs{
-			Pairs: make([]expr.KVPair, len(stmt.ProjectionExprs)),
-		}
-
-		for i := range stmt.ProjectionExprs {
-			d.Pairs[i].K = stmt.ProjectionExprs[i].String()
-			d.Pairs[i].V = stmt.ProjectionExprs[i]
-		}
-
-		s = s.Pipe(stream.Expressions(&d))
-	} else {
-		s = s.Pipe(stream.Project(stmt.ProjectionExprs...))
 	}
+	s = s.Pipe(stream.Project(stmt.ProjectionExprs...))
 
 	if stmt.Distinct {
 		s = s.Pipe(stream.Distinct())
diff --git a/internal/sql/parser/select.go b/internal/sql/parser/select.go
index bc325296..e5c1923c 100644
--- a/internal/sql/parser/select.go
+++ b/internal/sql/parser/select.go
@@ -104,25 +104,27 @@ func (p *Parser) parseProjectedExpr() (expr.Expr, error) {
 	}
 	p.Unscan()
 
-	e, err := p.ParseExpr()
+	pe, err := p.ParseExpr()
 	if err != nil {
 		return nil, err
 	}
 
-	rf := &expr.NamedExpr{Expr: e, ExprName: e.String()}
+	ne := &expr.NamedExpr{Expr: pe}
 
 	// Check if the AS token exists.
 	if tok, _, _ := p.ScanIgnoreWhitespace(); tok == scanner.AS {
-		rf.ExprName, err = p.parseIdent()
+		ne.ExprName, err = p.parseIdent()
 		if err != nil {
 			return nil, err
 		}
 
-		return rf, nil
+		return ne, nil
 	}
 	p.Unscan()
 
-	return rf, nil
+	ne.ExprName = pe.String()
+
+	return ne, nil
 }
 
 func (p *Parser) parseDistinct() (bool, error) {


Test patch:
diff --git a/document/json_test.go b/document/json_test.go
index f9bce485..8b19060f 100644
--- a/document/json_test.go
+++ b/document/json_test.go
@@ -16,7 +16,7 @@ func TestValueString(t *testing.T) {
 	}{
 		{"null", types.NewNullValue(), "NULL"},
 		{"blob", types.NewBlobValue([]byte("bar")), `"YmFy"`},
-		{"string", types.NewTextValue("bar"), `"bar"`},
+		{"string", types.NewTextValue("bar"), `'bar'`},
 		{"bool", types.NewBoolValue(true), "true"},
 		{"int", types.NewIntegerValue(10), "10"},
 		{"double", types.NewDoubleValue(10.1), "10.1"},
diff --git a/internal/expr/expr_test.go b/internal/expr/expr_test.go
index a59025f8..326f23c6 100644
--- a/internal/expr/expr_test.go
+++ b/internal/expr/expr_test.go
@@ -65,9 +65,9 @@ func TestString(t *testing.T) {
 		"true",
 		"500",
 		`foo.bar[1]`,
-		`"hello"`,
-		`[1, 2, "foo"]`,
-		`{a: "foo", b: 10}`,
+		`'hello'`,
+		`[1, 2, 'foo']`,
+		`{a: 'foo', b: 10}`,
 		"pk()",
 		"CAST(10 AS integer)",
 	}
diff --git a/internal/query/statement/explain_test.go b/internal/query/statement/explain_test.go
index 5f920122..b7bba731 100644
--- a/internal/query/statement/explain_test.go
+++ b/internal/query/statement/explain_test.go
@@ -14,7 +14,7 @@ func TestExplainStmt(t *testing.T) {
 		fails    bool
 		expected string
 	}{
-		{"EXPLAIN SELECT 1 + 1", false, `"exprs({\"1 + 1\": 1 + 1})"`},
+		{"EXPLAIN SELECT 1 + 1", false, `"project(1 + 1)"`},
 		{"EXPLAIN SELECT * FROM noexist", true, ``},
 		{"EXPLAIN SELECT * FROM test", false, `"seqScan(test)"`},
 		{"EXPLAIN SELECT *, a FROM test", false, `"seqScan(test) | project(*, a)"`},
@@ -64,7 +64,10 @@ func TestExplainStmt(t *testing.T) {
 			v, err := d.GetByField("plan")
 			require.NoError(t, err)
 
-			require.JSONEq(t, test.expected, document.ValueToString(v))
+			got, err := document.ValueToJSON(v)
+			require.NoError(t, err)
+
+			require.JSONEq(t, test.expected, string(got))
 		})
 	}
 }
diff --git a/internal/query/statement/gen_select_test.go b/internal/query/statement/gen_select_test.go
new file mode 100644
index 00000000..739d070b
--- /dev/null
+++ b/internal/query/statement/gen_select_test.go
@@ -0,0 +1,187 @@
+/*
+* CODE GENERATED AUTOMATICALLY WITH github.com/genjidb/genji/dev/gensqltest
+* THIS FILE SHOULD NOT BE EDITED BY HAND
+ */
+package statement_test
+
+import (
+	"testing"
+
+	"github.com/genjidb/genji"
+	"github.com/genjidb/genji/internal/testutil"
+	"github.com/stretchr/testify/require"
+)
+
+func TestGenSelect(t *testing.T) {
+	setup := func(t *testing.T, db *genji.DB) {
+		t.Helper()
+
+		q := `
+CREATE TABLE foo;
+`
+		err := db.Exec(q)
+		require.NoError(t, err)
+	}
+
+	// --------------------------------------------------------------------------
+	t.Run("simple projection", func(t *testing.T) {
+		db, err := genji.Open(":memory:")
+		require.NoError(t, err)
+		defer db.Close()
+
+		setup(t, db)
+
+		t.Run(`SELECT 1;`, func(t *testing.T) {
+			q := `
+SELECT 1;
+`
+			res, err := db.Query(q)
+			require.NoError(t, err)
+			defer res.Close()
+			raw := `
+{"1": 1}
+`
+			testutil.RequireStreamEq(t, raw, res)
+		})
+
+	})
+
+	// --------------------------------------------------------------------------
+	t.Run("complex expression", func(t *testing.T) {
+		db, err := genji.Open(":memory:")
+		require.NoError(t, err)
+		defer db.Close()
+
+		setup(t, db)
+
+		t.Run(`SELECT 1 + 1 * 2 / 4;`, func(t *testing.T) {
+			q := `
+SELECT 1 + 1 * 2 / 4;
+`
+			res, err := db.Query(q)
+			require.NoError(t, err)
+			defer res.Close()
+			raw := `
+{"1 + 1 * 2 / 4": 1}
+`
+			testutil.RequireStreamEq(t, raw, res)
+		})
+
+	})
+
+	// --------------------------------------------------------------------------
+	t.Run("with spaces", func(t *testing.T) {
+		db, err := genji.Open(":memory:")
+		require.NoError(t, err)
+		defer db.Close()
+
+		setup(t, db)
+
+		t.Run(`SELECT     1  + 1 *      2 /                    4;`, func(t *testing.T) {
+			q := `
+SELECT     1  + 1 *      2 /                    4;
+`
+			res, err := db.Query(q)
+			require.NoError(t, err)
+			defer res.Close()
+			raw := `
+{"1 + 1 * 2 / 4": 1}
+`
+			testutil.RequireStreamEq(t, raw, res)
+		})
+
+	})
+
+	// --------------------------------------------------------------------------
+	t.Run("escaping, double quotes", func(t *testing.T) {
+		db, err := genji.Open(":memory:")
+		require.NoError(t, err)
+		defer db.Close()
+
+		setup(t, db)
+
+		t.Run(`SELECT '"A"';`, func(t *testing.T) {
+			q := `
+SELECT '"A"';
+`
+			res, err := db.Query(q)
+			require.NoError(t, err)
+			defer res.Close()
+			raw := `
+{"'\"A\"'": "\"A\""}
+`
+			testutil.RequireStreamEq(t, raw, res)
+		})
+
+	})
+
+	// --------------------------------------------------------------------------
+	t.Run("escaping, single quotes", func(t *testing.T) {
+		db, err := genji.Open(":memory:")
+		require.NoError(t, err)
+		defer db.Close()
+
+		setup(t, db)
+
+		t.Run(`SELECT "'A'";`, func(t *testing.T) {
+			q := `
+SELECT "'A'";
+`
+			res, err := db.Query(q)
+			require.NoError(t, err)
+			defer res.Close()
+			raw := `
+{"'\\'A\\''": "'A'"}
+`
+			testutil.RequireStreamEq(t, raw, res)
+		})
+
+	})
+
+	// --------------------------------------------------------------------------
+	t.Run("aliases", func(t *testing.T) {
+		db, err := genji.Open(":memory:")
+		require.NoError(t, err)
+		defer db.Close()
+
+		setup(t, db)
+
+		t.Run(`SELECT 1 AS A;`, func(t *testing.T) {
+			q := `
+SELECT 1 AS A;
+`
+			res, err := db.Query(q)
+			require.NoError(t, err)
+			defer res.Close()
+			raw := `
+{"A": 1}
+`
+			testutil.RequireStreamEq(t, raw, res)
+		})
+
+	})
+
+	// --------------------------------------------------------------------------
+	t.Run("aliases with cast", func(t *testing.T) {
+		db, err := genji.Open(":memory:")
+		require.NoError(t, err)
+		defer db.Close()
+
+		setup(t, db)
+
+		t.Run(`SELECT CAST(1 AS DOUBLE) AS A;`, func(t *testing.T) {
+			q := `
+SELECT CAST(1 AS DOUBLE) AS A;
+`
+			res, err := db.Query(q)
+			require.NoError(t, err)
+			defer res.Close()
+			raw := `
+{"A": 1.0}
+`
+			testutil.RequireStreamEq(t, raw, res)
+		})
+
+	})
+
+}
diff --git a/internal/query/statement/gen_select_test.sql b/internal/query/statement/gen_select_test.sql
new file mode 100644
index 00000000..01849782
--- /dev/null
+++ b/internal/query/statement/gen_select_test.sql
@@ -0,0 +1,45 @@
+-- setup:
+CREATE TABLE foo;
+
+-- test: simple projection
+SELECT 1;
+/* result:
+{"1": 1}
+*/
+
+-- test: complex expression
+SELECT 1 + 1 * 2 / 4;
+/* result:
+{"1 + 1 * 2 / 4": 1}
+*/
+
+-- test: with spaces
+SELECT     1  + 1 *      2 /                    4;
+/* result:
+{"1 + 1 * 2 / 4": 1}
+*/
+
+-- test: escaping, double quotes
+SELECT '"A"';
+/* result:
+{"'\"A\"'": "\"A\""}
+*/
+
+-- test: escaping, single quotes
+SELECT "'A'";
+/* result:
+{"'\\'A\\''": "'A'"}
+*/
+
+-- test: aliases
+SELECT 1 AS A;
+/* result:
+{"A": 1}
+*/
+
+-- test: aliases with cast
+SELECT CAST(1 AS DOUBLE) AS A;
+/* result:
+{"A": 1.0}
+*/
+
diff --git a/internal/query/statement/select_test.go b/internal/query/statement/select_test.go
index bde12ba3..e94d8a23 100644
--- a/internal/query/statement/select_test.go
+++ b/internal/query/statement/select_test.go
@@ -38,7 +38,7 @@ func TestSelectStmt(t *testing.T) {
 		{"With fields", "SELECT color, shape FROM test", false, `[{"color":"red","shape":"square"},{"color":"blue","shape":null},{"color":null,"shape":null}]`, nil},
 		{"No cond, wildcard and other field", "SELECT *, color FROM test", false, `[{"color": "red", "k": 1, "color": "red", "size": 10, "shape": "square"}, {"color": "blue", "k": 2, "color": "blue", "size": 10, "weight": 100}, {"color": null, "k": 3, "height": 100, "weight": 200}]`, nil},
 		{"With DISTINCT", "SELECT DISTINCT * FROM test", false, `[{"k":1,"color":"red","size":10,"shape":"square"},{"k":2,"color":"blue","size":10,"weight":100},{"k":3,"height":100,"weight":200}]`, nil},
-		{"With DISTINCT and expr", "SELECT DISTINCT 'a' FROM test", false, `[{"\"a\"":"a"}]`, nil},
+		{"With DISTINCT and expr", "SELECT DISTINCT 'a' FROM test", false, `[{"'a'":"a"}]`, nil},
 		{"With expr fields", "SELECT color, color != 'red' AS notred FROM test", false, `[{"color":"red","notred":false},{"color":"blue","notred":true},{"color":null,"notred":null}]`, nil},
 		{"With eq op", "SELECT * FROM test WHERE size = 10", false, `[{"k":1,"color":"red","size":10,"shape":"square"},{"k":2,"color":"blue","size":10,"weight":100}]`, nil},
 		{"With neq op", "SELECT * FROM test WHERE color != 'red'", false, `[{"k":2,"color":"blue","size":10,"weight":100}]`, nil},
diff --git a/internal/sql/parser/select_test.go b/internal/sql/parser/select_test.go
index 6c4cb8f8..8383596a 100644
--- a/internal/sql/parser/select_test.go
+++ b/internal/sql/parser/select_test.go
@@ -20,43 +20,21 @@ func TestParserSelect(t *testing.T) {
 		mustFail bool
 	}{
 		{"NoTable", "SELECT 1",
-			stream.New(stream.Expressions(
-				&expr.KVPairs{
-					Pairs: []expr.KVPair{
-						{K: "1", V: testutil.ParseNamedExpr(t, "1")},
-					},
-				},
-			)),
+			stream.New(stream.Project(testutil.ParseNamedExpr(t, "1"))),
 			false,
 		},
 		{"NoTableWithTuple", "SELECT (1, 2)",
-			stream.New(stream.Expressions(
-				&expr.KVPairs{
-					Pairs: []expr.KVPair{
-						{K: "[1, 2]", V: testutil.ParseNamedExpr(t, "[1, 2]")},
-					},
-				},
-			)),
+			stream.New(stream.Project(testutil.ParseNamedExpr(t, "[1, 2]"))),
 			false,
 		},
 		{"NoTableWithBrackets", "SELECT [1, 2]",
-			stream.New(stream.Expressions(
-				&expr.KVPairs{
-					Pairs: []expr.KVPair{
-						{K: "[1, 2]", V: testutil.ParseNamedExpr(t, "[1, 2]")},
-					},
-				},
-			)),
+			stream.New(stream.Project(testutil.ParseNamedExpr(t, "[1, 2]"))),
 			false,
 		},
 		{"NoTableWithINOperator", "SELECT 1 in (1, 2), 3",
-			stream.New(stream.Expressions(
-				&expr.KVPairs{
-					Pairs: []expr.KVPair{
-						{K: "1 IN [1, 2]", V: testutil.ParseNamedExpr(t, "1 IN [1, 2]")},
-						{K: "3", V: testutil.ParseNamedExpr(t, "3")},
-					},
-				},
+			stream.New(stream.Project(
+				testutil.ParseNamedExpr(t, "1 IN [1, 2]"),
+				testutil.ParseNamedExpr(t, "3"),
 			)),
 			false,
 		},
@@ -81,7 +59,7 @@ func TestParserSelect(t *testing.T) {
 			false,
 		},
 		{"WithExpr", "SELECT a    > 1 FROM test",
-			stream.New(stream.SeqScan("test")).Pipe(stream.Project(testutil.ParseNamedExpr(t, "a > 1", "a > 1"))),
+			stream.New(stream.SeqScan("test")).Pipe(stream.Project(testutil.ParseNamedExpr(t, "a > 1"))),
 			false,
 		},
 		{"WithCond", "SELECT * FROM test WHERE age = 10",
diff --git a/internal/testutil/expr.go b/internal/testutil/expr.go
index 28b8e2dd..6a552e05 100644
--- a/internal/testutil/expr.go
+++ b/internal/testutil/expr.go
@@ -83,11 +83,8 @@ func ParseDocumentPath(t testing.TB, p string) document.Path {
 func ParseNamedExpr(t testing.TB, s string, name ...string) expr.Expr {
 	t.Helper()
 
-	e, err := parser.ParseExpr(s)
-	require.NoError(t, err)
-
 	ne := expr.NamedExpr{
-		Expr:     e,
+		Expr:     ParseExpr(t, s),
 		ExprName: s,
 	}
 
@@ -98,6 +95,15 @@ func ParseNamedExpr(t testing.TB, s string, name ...string) expr.Expr {
 	return &ne
 }
 
+func ParseExpr(t testing.TB, s string) expr.Expr {
+	t.Helper()
+
+	e, err := parser.ParseExpr(s)
+	require.NoError(t, err)
+
+	return e
+}
+
 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()
diff --git a/open_test.go b/open_test.go
index b33d16b9..b5f64c4d 100644
--- a/open_test.go
+++ b/open_test.go
@@ -74,7 +74,7 @@ func TestOpen(t *testing.T) {
 		}
 
 		if count == 6 {
-			testutil.RequireDocJSONEq(t, d, `{"name":"tableB", "sql":"CREATE TABLE tableB (a TEXT NOT NULL PRIMARY KEY DEFAULT \"hello\")", "store_name":"Aw==", "type":"table"}`)
+			testutil.RequireDocJSONEq(t, d, `{"name":"tableB", "sql":"CREATE TABLE tableB (a TEXT NOT NULL PRIMARY KEY DEFAULT 'hello')", "store_name":"Aw==", "type":"table"}`)
 			return nil
 		}