From ac98735cb1adc9384c5b2caeb9aac938db275bf7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Knut=20Olav=20L=C3=B8ite?= Date: Mon, 29 Nov 2021 05:58:19 +0100 Subject: [PATCH] feat(spanner/spannertest): support JSON_VALUE function (#5173) * 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 --- spanner/spannertest/db.go | 18 +++++++++++++----- spanner/spannertest/db_test.go | 4 ++-- spanner/spannertest/funcs.go | 19 +++++++++++++++++++ spanner/spannertest/integration_test.go | 4 ++-- spanner/spansql/keywords.go | 3 +++ spanner/spansql/parser_test.go | 15 +++++++++++++++ 6 files changed, 54 insertions(+), 9 deletions(-) diff --git a/spanner/spannertest/db.go b/spanner/spannertest/db.go index 920a44af4b2..f922e77af6d 100644 --- a/spanner/spannertest/db.go +++ b/spanner/spannertest/db.go @@ -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) + } } } diff --git a/spanner/spannertest/db_test.go b/spanner/spannertest/db_test.go index 9e8da729162..dbc90e12ce2 100644 --- a/spanner/spannertest/db_test.go +++ b/spanner/spannertest/db_test.go @@ -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) } } diff --git a/spanner/spannertest/funcs.go b/spanner/spannertest/funcs.go index a27eab5e7be..3c451a8b7cd 100644 --- a/spanner/spannertest/funcs.go +++ b/spanner/spannertest/funcs.go @@ -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) { diff --git a/spanner/spannertest/integration_test.go b/spanner/spannertest/integration_test.go index c5d5c81a328..4d962c5aaa0 100644 --- a/spanner/spannertest/integration_test.go +++ b/spanner/spannertest/integration_test.go @@ -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( diff --git a/spanner/spansql/keywords.go b/spanner/spansql/keywords.go index 61ac0657c49..0ddade0b1b3 100644 --- a/spanner/spansql/keywords.go +++ b/spanner/spansql/keywords.go @@ -231,4 +231,7 @@ var allFuncs = []string{ "UNIX_MILLIS", "UNIX_MICROS", "PENDING_COMMIT_TIMESTAMP", + + // JSON functions. + "JSON_VALUE", } diff --git a/spanner/spansql/parser_test.go b/spanner/spansql/parser_test.go index 966e4c5d1f0..429355dbec6 100644 --- a/spanner/spansql/parser_test.go +++ b/spanner/spansql/parser_test.go @@ -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)