Skip to content

Commit

Permalink
feat(spanner/spannertest): support JSON_VALUE function (#5173)
Browse files Browse the repository at this point in the history
* feat: support JSON_VALUE function

Adds support for the JSON_VALUE function. The function always returns an empty string,
as there is no XPath query engine available in the Spanner client library. This does
however enable the usage of computed columns that use the function.

This change also fixes the TODO that generated columns cannot be added to non-empty
tables.

Fixes #5167

* fix: linting
  • Loading branch information
olavloite committed Nov 29, 2021
1 parent c103ff6 commit ac98735
Show file tree
Hide file tree
Showing 6 changed files with 54 additions and 9 deletions.
18 changes: 13 additions & 5 deletions spanner/spannertest/db.go
Expand Up @@ -700,12 +700,20 @@ func (t *table) addColumn(cd spansql.ColumnDef, newTable bool) *status.Status {
// TODO: what happens in this case?
return status.Newf(codes.Unimplemented, "can't add NOT NULL columns to non-empty tables yet")
}
if cd.Generated != nil {
// TODO: should backfill the data to maintain behaviour with real spanner
return status.Newf(codes.Unimplemented, "can't add generated columns to non-empty tables yet")
}
for i := range t.rows {
t.rows[i] = append(t.rows[i], nil)
if cd.Generated != nil {
ec := evalContext{
cols: t.cols,
row: t.rows[i],
}
val, err := ec.evalExpr(cd.Generated)
if err != nil {
return status.Newf(codes.InvalidArgument, "could not backfill values for generated column: %v", err)
}
t.rows[i] = append(t.rows[i], val)
} else {
t.rows[i] = append(t.rows[i], nil)
}
}
}

Expand Down
4 changes: 2 additions & 2 deletions spanner/spannertest/db_test.go
Expand Up @@ -441,8 +441,8 @@ func TestGeneratedColumn(t *testing.T) {
if err != nil {
t.Fatalf("%s: Bad DDL", err)
}
if st := db.ApplyDDL(ddl.List[0]); st.Code() == codes.OK {
t.Fatalf("Should have failed to add a generated column to non-empty table\n status: %v", st)
if st := db.ApplyDDL(ddl.List[0]); st.Code() != codes.OK {
t.Fatalf("Failed to add a generated column to non-empty table\n status: %v", st)
}

}
Expand Down
19 changes: 19 additions & 0 deletions spanner/spannertest/funcs.go
Expand Up @@ -88,6 +88,25 @@ var functions = map[string]function{
return cast(values, types, true)
},
},
"JSON_VALUE": {
Eval: func(values []interface{}, types []spansql.Type) (interface{}, spansql.Type, error) {
if len(values) != 2 {
return nil, spansql.Type{}, status.Error(codes.InvalidArgument, "No matching signature for function JSON_VALUE for the given argument types")
}
if values[0] == nil || values[1] == nil {
return nil, spansql.Type{Base: spansql.String}, nil
}
_, okArg1 := values[0].(string)
_, okArg2 := values[1].(string)
if !(okArg1 && okArg2) {
return nil, spansql.Type{}, status.Error(codes.InvalidArgument, "No matching signature for function JSON_VALUE for the given argument types")
}
// This function currently has no implementation and always returns
// an empty string, as it would otherwise require an XPath query
// engine.
return "", spansql.Type{Base: spansql.String}, nil
},
},
}

func cast(values []interface{}, types []spansql.Type, safe bool) (interface{}, spansql.Type, error) {
Expand Down
4 changes: 2 additions & 2 deletions spanner/spannertest/integration_test.go
Expand Up @@ -1312,8 +1312,8 @@ func TestIntegration_GeneratedColumns(t *testing.T) {

err = updateDDL(t, adminClient,
`ALTER TABLE `+tableName+` ADD COLUMN TotalSales2 INT64 AS (NumSongs * EstimatedSales) STORED`)
if err == nil {
t.Fatalf("Should have failed to add a generated column to non empty table")
if err != nil {
t.Fatalf("Failed to add a generated column to a non-empty table: %v", err)
}

ri := client.Single().Query(ctx, spanner.NewStatement(
Expand Down
3 changes: 3 additions & 0 deletions spanner/spansql/keywords.go
Expand Up @@ -231,4 +231,7 @@ var allFuncs = []string{
"UNIX_MILLIS",
"UNIX_MICROS",
"PENDING_COMMIT_TIMESTAMP",

// JSON functions.
"JSON_VALUE",
}
15 changes: 15 additions & 0 deletions spanner/spansql/parser_test.go
Expand Up @@ -862,6 +862,21 @@ func TestParseDDL(t *testing.T) {
},
},
}},
{`ALTER TABLE products ADD COLUMN item STRING(MAX) AS (JSON_VALUE(itemDetails, '$.itemDetails')) STORED`, &DDL{Filename: "filename", List: []DDLStmt{
&AlterTable{
Name: "products",
Alteration: AddColumn{Def: ColumnDef{
Name: "item",
Type: Type{Base: String, Len: MaxLen},
Position: line(1),
Generated: Func{
Name: "JSON_VALUE",
Args: []Expr{ID("itemDetails"), StringLiteral("$.itemDetails")},
},
}},
Position: line(1),
},
}}},
}
for _, test := range tests {
got, err := ParseDDL("filename", test.in)
Expand Down

0 comments on commit ac98735

Please sign in to comment.