diff --git a/csv_golden_test.go b/csv_golden_test.go index 45b56b4..a47196b 100644 --- a/csv_golden_test.go +++ b/csv_golden_test.go @@ -8,7 +8,7 @@ import ( "testing" ) -var updateGolden = flag.Bool("update-golden", false, "rewrite testdata/experimental_csv/*.golden files") +var updateGolden = flag.Bool("update-golden", false, "rewrite testdata/experimental_csv/*.golden, testdata/yaml_output/*.golden, and profile YAML goldens") func TestExperimentalCsvGolden(t *testing.T) { for name, rs := range csvGoldenFixtures() { diff --git a/params/fixtures_test.go b/params/fixtures_test.go new file mode 100644 index 0000000..f346f83 --- /dev/null +++ b/params/fixtures_test.go @@ -0,0 +1,109 @@ +package params + +import ( + "path/filepath" + "testing" + + "github.com/google/go-cmp/cmp" +) + +func TestLoadParamFileFixtures(t *testing.T) { + t.Parallel() + + tests := []struct { + file string + want map[string]string + }{ + { + file: "testdata/readme_example.yaml", + want: map[string]string{ + "arr": "ARRAY", + "names": `[STRUCT("John", "Doe"), ("Mary", "Sue")]`, + }, + }, + { + file: "testdata/spanner_literals.yml", + want: map[string]string{ + "b": "TRUE", + "bs": `b"foo"`, + "i64": "1", + "f64": "1.0", + "f32": "CAST(1.0 AS FLOAT32)", + "n": `NUMERIC "1"`, + "s": "'foo'", + "js": `JSON "{}"`, + "ts": `TIMESTAMP "2000-01-01T00:00:00Z"`, + "ival_single": "INTERVAL 3 DAY", + "n_b": "CAST(NULL AS BOOL)", + }, + }, + { + file: "testdata/multiline_literal.yaml", + want: map[string]string{ + "query": "SELECT SingerId, FirstName\nFROM Singers\nWHERE SingerId = @id\n", + "id": "42", + }, + }, + { + file: "testdata/large_int.yaml", + want: map[string]string{ + "id": "1234567890123456789", + }, + }, + { + file: "testdata/scalars_extra.yaml", + want: map[string]string{ + "id": "123", + "enabled": "FALSE", + "price": "1.0", + "ratio": "0.25", + "tag": "yes", + "label": "ARRAY", + "created_at": `TIMESTAMP "2023-01-01T00:00:00Z"`, + }, + }, + } + + for _, tc := range tests { + tc := tc + t.Run(filepath.Base(tc.file), func(t *testing.T) { + t.Parallel() + got, err := LoadParamFile(tc.file) + if err != nil { + t.Fatal(err) + } + if diff := cmp.Diff(tc.want, got); diff != "" { + t.Fatalf("(-want +got)\n%s", diff) + } + }) + } +} + +func TestLoadParamFileFixtureErrors(t *testing.T) { + t.Parallel() + + if _, err := LoadParamFile("testdata/nested_map.yaml"); err == nil { + t.Fatal("expected error for nested map") + } + if _, err := LoadParamFile("testdata/nested_array.yaml"); err == nil { + t.Fatal("expected error for nested array") + } +} + +func TestMergeParamsWithFixture(t *testing.T) { + t.Parallel() + + file, err := LoadParamFile("testdata/readme_example.yaml") + if err != nil { + t.Fatal(err) + } + got := MergeParams(file, map[string]string{"arr": "ARRAY", "extra": "1"}) + want := map[string]string{ + "arr": "ARRAY", + "names": `[STRUCT("John", "Doe"), ("Mary", "Sue")]`, + "extra": "1", + } + if diff := cmp.Diff(want, got); diff != "" { + t.Fatalf("(-want +got)\n%s", diff) + } +} diff --git a/params/load_test.go b/params/load_test.go index f20a673..3652998 100644 --- a/params/load_test.go +++ b/params/load_test.go @@ -63,24 +63,11 @@ func TestLoadParamFile(t *testing.T) { dir := t.TempDir() - yamlPath := filepath.Join(dir, "params.yaml") - if err := os.WriteFile(yamlPath, []byte("arr: ARRAY\nname: \"42\"\n"), 0o644); err != nil { - t.Fatal(err) - } - got, err := LoadParamFile(yamlPath) - if err != nil { - t.Fatal(err) - } - want := map[string]string{"arr": "ARRAY", "name": "42"} - if diff := cmp.Diff(want, got); diff != "" { - t.Fatalf("(-want +got)\n%s", diff) - } - jsonPath := filepath.Join(dir, "params.json") if err := os.WriteFile(jsonPath, []byte(`{"arr":"ARRAY"}`), 0o644); err != nil { t.Fatal(err) } - got, err = LoadParamFile(jsonPath) + got, err := LoadParamFile(jsonPath) if err != nil { t.Fatal(err) } @@ -88,24 +75,6 @@ func TestLoadParamFile(t *testing.T) { t.Fatalf("(-want +got)\n%s", diff) } - yamlScalarsPath := filepath.Join(dir, "params-scalars.yaml") - if err := os.WriteFile(yamlScalarsPath, []byte("id: 123\nenabled: true\nprice: 1.0\ncreated_at: 2023-01-01T00:00:00Z\n"), 0o644); err != nil { - t.Fatal(err) - } - got, err = LoadParamFile(yamlScalarsPath) - if err != nil { - t.Fatal(err) - } - wantScalars := map[string]string{ - "id": "123", - "enabled": "TRUE", - "price": "1.0", - "created_at": `TIMESTAMP "2023-01-01T00:00:00Z"`, - } - if diff := cmp.Diff(wantScalars, got); diff != "" { - t.Fatalf("(-want +got)\n%s", diff) - } - jsonLargeIntPath := filepath.Join(dir, "params-large-int.json") if err := os.WriteFile(jsonLargeIntPath, []byte(`{"id":1234567890123456789}`), 0o644); err != nil { t.Fatal(err) @@ -118,14 +87,6 @@ func TestLoadParamFile(t *testing.T) { t.Fatalf("got id=%q, want exact integer string", got["id"]) } - nestedPath := filepath.Join(dir, "params-nested.yaml") - if err := os.WriteFile(nestedPath, []byte("arr: [1, 2]\n"), 0o644); err != nil { - t.Fatal(err) - } - if _, err := LoadParamFile(nestedPath); err == nil { - t.Fatal("expected error for nested array in param file") - } - emptyPath := filepath.Join(dir, "params-empty.yaml") if err := os.WriteFile(emptyPath, []byte(" \n"), 0o644); err != nil { t.Fatal(err) diff --git a/params/testdata/large_int.yaml b/params/testdata/large_int.yaml new file mode 100644 index 0000000..f8556c8 --- /dev/null +++ b/params/testdata/large_int.yaml @@ -0,0 +1 @@ +id: 1234567890123456789 diff --git a/params/testdata/multiline_literal.yaml b/params/testdata/multiline_literal.yaml new file mode 100644 index 0000000..7216a5a --- /dev/null +++ b/params/testdata/multiline_literal.yaml @@ -0,0 +1,5 @@ +query: | + SELECT SingerId, FirstName + FROM Singers + WHERE SingerId = @id +id: "42" diff --git a/params/testdata/nested_array.yaml b/params/testdata/nested_array.yaml new file mode 100644 index 0000000..49f2612 --- /dev/null +++ b/params/testdata/nested_array.yaml @@ -0,0 +1 @@ +arr: [1, 2] diff --git a/params/testdata/nested_map.yaml b/params/testdata/nested_map.yaml new file mode 100644 index 0000000..dcb7a90 --- /dev/null +++ b/params/testdata/nested_map.yaml @@ -0,0 +1,2 @@ +opts: + key: value diff --git a/params/testdata/readme_example.yaml b/params/testdata/readme_example.yaml new file mode 100644 index 0000000..496c32e --- /dev/null +++ b/params/testdata/readme_example.yaml @@ -0,0 +1,3 @@ +# params.yaml from README (--param-file example) +arr: ARRAY +names: '[STRUCT("John", "Doe"), ("Mary", "Sue")]' diff --git a/params/testdata/scalars_extra.yaml b/params/testdata/scalars_extra.yaml new file mode 100644 index 0000000..59c8ff9 --- /dev/null +++ b/params/testdata/scalars_extra.yaml @@ -0,0 +1,7 @@ +id: 123 +enabled: false +price: 1.0 +ratio: 0.25 +tag: yes +label: "ARRAY" +created_at: 2023-01-01T00:00:00Z diff --git a/params/testdata/spanner_literals.yml b/params/testdata/spanner_literals.yml new file mode 100644 index 0000000..79c4d5b --- /dev/null +++ b/params/testdata/spanner_literals.yml @@ -0,0 +1,12 @@ +# Common Spanner parameter literal strings for --param-file +b: "TRUE" +bs: 'b"foo"' +i64: "1" +f64: "1.0" +f32: "CAST(1.0 AS FLOAT32)" +n: 'NUMERIC "1"' +s: "'foo'" +js: 'JSON "{}"' +ts: 'TIMESTAMP "2000-01-01T00:00:00Z"' +ival_single: "INTERVAL 3 DAY" +n_b: "CAST(NULL AS BOOL)" diff --git a/profile_yaml_test.go b/profile_yaml_test.go new file mode 100644 index 0000000..3861fb7 --- /dev/null +++ b/profile_yaml_test.go @@ -0,0 +1,109 @@ +package main + +import ( + "os" + "path/filepath" + "testing" + + sppb "cloud.google.com/go/spanner/apiv1/spannerpb" + "google.golang.org/protobuf/encoding/protojson" +) + +func loadProfileJSONFixture(path string) (*sppb.ResultSet, error) { + b, err := os.ReadFile(path) + if err != nil { + return nil, err + } + var rs sppb.ResultSet + if err := protojson.Unmarshal(b, &rs); err != nil { + return nil, err + } + return &rs, nil +} + +type profileYAMLGoldenCase struct { + jsonFile string + filter string + golden string +} + +func profileYAMLGoldenCases() []profileYAMLGoldenCase { + return []profileYAMLGoldenCase{ + { + jsonFile: "testdata/profile/param_scalar.json", + filter: ".", + golden: "profile_param_scalar", + }, + { + jsonFile: "testdata/profile/albums_by_singer.json", + filter: ".", + golden: "profile_albums_by_singer", + }, + { + jsonFile: "testdata/profile/singers_limit3.json", + filter: ".", + golden: "profile_singers_limit3", + }, + { + jsonFile: "testdata/profile/singers_limit3.json", + filter: `{rowType: .metadata.rowType, rows: .rows}`, + golden: "profile_singers_limit3_rowtype_rows", + }, + { + jsonFile: "testdata/profile/singers_limit3.json", + filter: `.rows[]`, + golden: "profile_singers_limit3_rows_stream", + }, + { + jsonFile: "testdata/profile/singers_limit3.json", + filter: `.metadata.rowType.fields | map(.name)`, + golden: "profile_singers_limit3_field_names", + }, + } +} + +func TestProfileJSONToYamlGolden(t *testing.T) { + for _, tc := range profileYAMLGoldenCases() { + tc := tc + t.Run(tc.golden, func(t *testing.T) { + rs, err := loadProfileJSONFixture(tc.jsonFile) + if err != nil { + t.Fatal(err) + } + if rs.GetMetadata() == nil || rs.GetMetadata().GetRowType() == nil { + t.Fatal("missing metadata.rowType") + } + if rs.GetStats() == nil { + t.Fatal("missing stats") + } + if rs.GetStats().GetQueryPlan() == nil && rs.GetStats().GetQueryStats() == nil { + t.Fatal("missing query plan or query stats") + } + + got, err := encodeResultSetYAML(tc.filter, rs) + if err != nil { + t.Fatalf("encodeResultSetYAML() error = %v", err) + } + + goldenPath := filepath.Join("testdata", "yaml_output", tc.golden+".golden") + if *updateGolden { + if err := os.MkdirAll(filepath.Dir(goldenPath), 0o755); err != nil { + t.Fatalf("MkdirAll() error = %v", err) + } + if err := os.WriteFile(goldenPath, got, 0o644); err != nil { + t.Fatalf("WriteFile() error = %v", err) + } + t.Logf("updated %s", goldenPath) + return + } + + want, err := os.ReadFile(goldenPath) + if err != nil { + t.Fatalf("ReadFile(%q) error = %v (run: go test -update-golden -run TestProfileJSONToYamlGolden)", goldenPath, err) + } + if string(got) != string(want) { + t.Fatalf("PROFILE JSON → YAML mismatch for %s\n\ngot:\n%s\n\nwant:\n%s", tc.golden, got, want) + } + }) + } +} diff --git a/testdata/profile/albums_by_singer.json b/testdata/profile/albums_by_singer.json new file mode 100644 index 0000000..7fe53c9 --- /dev/null +++ b/testdata/profile/albums_by_singer.json @@ -0,0 +1,447 @@ +{ + "metadata": { + "rowType": { + "fields": [ + { + "name": "SingerId", + "type": { + "code": "INT64" + } + }, + { + "name": "AlbumId", + "type": { + "code": "INT64" + } + }, + { + "name": "AlbumTitle", + "type": { + "code": "STRING" + } + } + ] + }, + "transaction": { + "readTimestamp": "2026-06-28T15:55:38.446224Z" + }, + "undeclaredParameters": {} + }, + "rows": [ + [ + "2", + "1", + "Green" + ], + [ + "2", + "2", + "Forever Hold Your Peace" + ] + ], + "stats": { + "queryPlan": { + "planNodes": [ + { + "childLinks": [ + { + "childIndex": 1 + }, + { + "childIndex": 17, + "type": "Split Range" + } + ], + "displayName": "Distributed Union", + "executionStats": { + "cpu_time": { + "total": "0.23", + "unit": "msecs" + }, + "execution_summary": { + "execution_end_timestamp": "1782662138.486515", + "execution_start_timestamp": "1782662138.485666", + "num_executions": "1" + }, + "latency": { + "total": "0.83", + "unit": "msecs" + }, + "remote_calls": { + "total": "0", + "unit": "calls" + }, + "rows": { + "total": "2", + "unit": "rows" + } + }, + "kind": "RELATIONAL", + "metadata": { + "distribution_table": "Singers", + "execution_method": "Row", + "split_ranges_aligned": "true", + "subquery_cluster_node": "1" + } + }, + { + "childLinks": [ + { + "childIndex": 2 + }, + { + "childIndex": 14 + }, + { + "childIndex": 15 + }, + { + "childIndex": 16 + } + ], + "displayName": "Serialize Result", + "executionStats": { + "cpu_time": { + "total": "0.21", + "unit": "msecs" + }, + "execution_summary": { + "execution_end_timestamp": "1782662138.486510", + "execution_start_timestamp": "1782662138.485691", + "num_executions": "1" + }, + "latency": { + "total": "0.81", + "unit": "msecs" + }, + "rows": { + "total": "2", + "unit": "rows" + } + }, + "index": 1, + "kind": "RELATIONAL", + "metadata": { + "execution_method": "Row" + } + }, + { + "childLinks": [ + { + "childIndex": 3 + }, + { + "childIndex": 13, + "type": "Limit" + } + ], + "displayName": "Limit", + "executionStats": { + "cpu_time": { + "total": "0.21", + "unit": "msecs" + }, + "execution_summary": { + "num_executions": "1" + }, + "latency": { + "total": "0.81", + "unit": "msecs" + }, + "rows": { + "total": "2", + "unit": "rows" + } + }, + "index": 2, + "kind": "RELATIONAL", + "metadata": { + "call_type": "Global", + "execution_method": "Row" + } + }, + { + "childLinks": [ + { + "childIndex": 4 + } + ], + "displayName": "Distributed Union", + "executionStats": { + "cpu_time": { + "total": "0.21", + "unit": "msecs" + }, + "execution_summary": { + "num_executions": "1" + }, + "latency": { + "total": "0.81", + "unit": "msecs" + }, + "remote_calls": { + "total": "0", + "unit": "calls" + }, + "rows": { + "total": "2", + "unit": "rows" + } + }, + "index": 3, + "kind": "RELATIONAL", + "metadata": { + "call_type": "Local", + "execution_method": "Row", + "subquery_cluster_node": "4" + } + }, + { + "childLinks": [ + { + "childIndex": 5 + } + ], + "displayName": "Filter Scan", + "index": 4, + "kind": "RELATIONAL", + "metadata": { + "execution_method": "Row", + "seekable_key_size": "0" + } + }, + { + "childLinks": [ + { + "childIndex": 6, + "variable": "SingerId" + }, + { + "childIndex": 7, + "variable": "AlbumId" + }, + { + "childIndex": 8, + "variable": "AlbumTitle" + }, + { + "childIndex": 12, + "type": "Seek Condition" + } + ], + "displayName": "Scan", + "executionStats": { + "cpu_time": { + "total": "0.2", + "unit": "msecs" + }, + "deleted_rows": { + "total": "0", + "unit": "rows" + }, + "execution_summary": { + "num_executions": "1" + }, + "filesystem_delay_seconds": { + "total": "0.57", + "unit": "msecs" + }, + "filtered_rows": { + "total": "0", + "unit": "rows" + }, + "latency": { + "total": "0.8", + "unit": "msecs" + }, + "rows": { + "total": "2", + "unit": "rows" + }, + "scanned_rows": { + "total": "2", + "unit": "rows" + } + }, + "index": 5, + "kind": "RELATIONAL", + "metadata": { + "execution_method": "Row", + "scan_method": "Row", + "scan_target": "Albums", + "scan_type": "TableScan" + } + }, + { + "displayName": "Reference", + "index": 6, + "kind": "SCALAR", + "shortRepresentation": { + "description": "SingerId" + } + }, + { + "displayName": "Reference", + "index": 7, + "kind": "SCALAR", + "shortRepresentation": { + "description": "AlbumId" + } + }, + { + "displayName": "Reference", + "index": 8, + "kind": "SCALAR", + "shortRepresentation": { + "description": "AlbumTitle" + } + }, + { + "childLinks": [ + { + "childIndex": 10 + }, + { + "childIndex": 11 + } + ], + "displayName": "Function", + "index": 9, + "kind": "SCALAR", + "shortRepresentation": { + "description": "($SingerId = @sid)" + } + }, + { + "displayName": "Reference", + "index": 10, + "kind": "SCALAR", + "shortRepresentation": { + "description": "$SingerId" + } + }, + { + "displayName": "Parameter", + "index": 11, + "kind": "SCALAR", + "metadata": { + "name": "sid", + "type": "scalar" + }, + "shortRepresentation": { + "description": "@sid" + } + }, + { + "childLinks": [ + { + "childIndex": 9 + } + ], + "displayName": "Function", + "index": 12, + "kind": "SCALAR", + "shortRepresentation": { + "description": "($SingerId = @sid)" + } + }, + { + "displayName": "Constant", + "index": 13, + "kind": "SCALAR", + "shortRepresentation": { + "description": "2" + } + }, + { + "displayName": "Parameter", + "index": 14, + "kind": "SCALAR", + "metadata": { + "name": "sid", + "type": "scalar" + }, + "shortRepresentation": { + "description": "@sid" + } + }, + { + "displayName": "Reference", + "index": 15, + "kind": "SCALAR", + "shortRepresentation": { + "description": "$AlbumId" + } + }, + { + "displayName": "Reference", + "index": 16, + "kind": "SCALAR", + "shortRepresentation": { + "description": "$AlbumTitle" + } + }, + { + "childLinks": [ + { + "childIndex": 18 + }, + { + "childIndex": 19 + } + ], + "displayName": "Function", + "index": 17, + "kind": "SCALAR", + "shortRepresentation": { + "description": "($SingerId = @sid)" + } + }, + { + "displayName": "Reference", + "index": 18, + "kind": "SCALAR", + "shortRepresentation": { + "description": "$SingerId" + } + }, + { + "displayName": "Parameter", + "index": 19, + "kind": "SCALAR", + "metadata": { + "name": "sid", + "type": "scalar" + }, + "shortRepresentation": { + "description": "@sid" + } + } + ] + }, + "queryStats": { + "bytes_returned": "48", + "cpu_time": "13.58 msecs", + "data_bytes_read": "0", + "deleted_rows_scanned": "0", + "elapsed_time": "40.25 msecs", + "filesystem_delay_seconds": "0.57 msecs", + "is_graph_query": "false", + "locking_delay": "0 msecs", + "memory_peak_usage_bytes": "52", + "memory_usage_percentage": "0.000", + "optimizer_statistics_package": "auto_20260627_06_11_04UTC", + "optimizer_version": "8", + "query_plan_creation_time": "8.33 msecs", + "query_text": "SELECT SingerId, AlbumId, AlbumTitle FROM Albums WHERE SingerId = @sid ORDER BY AlbumId LIMIT 2", + "remote_server_calls": "0/0", + "rows_returned": "2", + "rows_scanned": "2", + "runtime_creation_time": "0.32 msecs", + "server_queue_delay": "0.03 msecs", + "statistics_load_time": "0", + "time_to_first_row": "40.12 msecs", + "total_memory_peak_usage_byte": "52" + } + } +} diff --git a/testdata/profile/param_scalar.json b/testdata/profile/param_scalar.json new file mode 100644 index 0000000..0868304 --- /dev/null +++ b/testdata/profile/param_scalar.json @@ -0,0 +1,180 @@ +{ + "metadata": { + "rowType": { + "fields": [ + { + "name": "i64", + "type": { + "code": "INT64" + } + }, + { + "name": "s", + "type": { + "code": "STRING" + } + }, + { + "name": "bl", + "type": { + "code": "BOOL" + } + } + ] + }, + "transaction": { + "readTimestamp": "2026-06-28T15:55:36.515883Z" + }, + "undeclaredParameters": {} + }, + "rows": [ + [ + "1", + "hello", + true + ] + ], + "stats": { + "queryPlan": { + "planNodes": [ + { + "childLinks": [ + { + "childIndex": 1 + }, + { + "childIndex": 3 + }, + { + "childIndex": 4 + }, + { + "childIndex": 5 + } + ], + "displayName": "Serialize Result", + "executionStats": { + "cpu_time": { + "total": "0.01", + "unit": "msecs" + }, + "execution_summary": { + "execution_end_timestamp": "1782662136.516507", + "execution_start_timestamp": "1782662136.516479", + "num_executions": "1" + }, + "latency": { + "total": "0.01", + "unit": "msecs" + }, + "rows": { + "total": "1", + "unit": "rows" + } + }, + "kind": "RELATIONAL", + "metadata": { + "execution_method": "Row" + } + }, + { + "childLinks": [ + { + "childIndex": 2 + } + ], + "displayName": "Unit Relation", + "executionStats": { + "cpu_time": { + "total": "0", + "unit": "msecs" + }, + "execution_summary": { + "num_executions": "1" + }, + "latency": { + "total": "0", + "unit": "msecs" + }, + "rows": { + "total": "1", + "unit": "rows" + } + }, + "index": 1, + "kind": "RELATIONAL", + "metadata": { + "execution_method": "Row" + } + }, + { + "displayName": "Constant", + "index": 2, + "kind": "SCALAR", + "shortRepresentation": { + "description": "1" + } + }, + { + "displayName": "Parameter", + "index": 3, + "kind": "SCALAR", + "metadata": { + "name": "i64", + "type": "scalar" + }, + "shortRepresentation": { + "description": "@i64" + } + }, + { + "displayName": "Parameter", + "index": 4, + "kind": "SCALAR", + "metadata": { + "name": "s", + "type": "scalar" + }, + "shortRepresentation": { + "description": "@s" + } + }, + { + "displayName": "Parameter", + "index": 5, + "kind": "SCALAR", + "metadata": { + "name": "bl", + "type": "scalar" + }, + "shortRepresentation": { + "description": "@bl" + } + } + ] + }, + "queryStats": { + "bytes_returned": "17", + "cpu_time": "0.36 msecs", + "deleted_rows_scanned": "0", + "elapsed_time": "0.43 msecs", + "filesystem_delay_seconds": "0 msecs", + "is_graph_query": "false", + "locking_delay": "0 msecs", + "memory_peak_usage_bytes": "27", + "memory_usage_percentage": "0.000", + "optimizer_statistics_package": "auto_20260627_06_11_04UTC", + "optimizer_version": "8", + "query_plan_cached": "true", + "query_text": "SELECT @i64 AS i64, @s AS s, @bl AS bl", + "remote_server_calls": "0/0", + "rows_returned": "1", + "rows_scanned": "0", + "runtime_cached": "true", + "server_queue_delay": "0.01 msecs", + "statistics_load_time": "0", + "time_to_first_row": "0.39 msecs", + "total_memory_peak_usage_byte": "27" + } + } +} diff --git a/testdata/profile/singers_limit3.json b/testdata/profile/singers_limit3.json new file mode 100644 index 0000000..abed27d --- /dev/null +++ b/testdata/profile/singers_limit3.json @@ -0,0 +1,387 @@ +{ + "metadata": { + "rowType": { + "fields": [ + { + "name": "SingerId", + "type": { + "code": "INT64" + } + }, + { + "name": "FirstName", + "type": { + "code": "STRING" + } + } + ] + }, + "transaction": { + "readTimestamp": "2026-06-28T15:55:40.086987Z" + }, + "undeclaredParameters": {} + }, + "rows": [ + [ + "1", + "Marc" + ], + [ + "2", + "Catalina" + ], + [ + "3", + "Alice" + ] + ], + "stats": { + "queryPlan": { + "planNodes": [ + { + "childLinks": [ + { + "childIndex": 1 + }, + { + "childIndex": 12, + "type": "Limit" + } + ], + "displayName": "Limit", + "executionStats": { + "cpu_time": { + "total": "0.71", + "unit": "msecs" + }, + "execution_summary": { + "execution_end_timestamp": "1782662140.107635", + "execution_start_timestamp": "1782662140.100167", + "num_executions": "1" + }, + "latency": { + "total": "7.2", + "unit": "msecs" + }, + "rows": { + "total": "3", + "unit": "rows" + } + }, + "kind": "RELATIONAL", + "metadata": { + "call_type": "Global", + "execution_method": "Row" + } + }, + { + "childLinks": [ + { + "childIndex": 2 + }, + { + "childIndex": 11, + "type": "Split Range" + } + ], + "displayName": "Distributed Union", + "executionStats": { + "cpu_time": { + "total": "0.7", + "unit": "msecs" + }, + "execution_summary": { + "num_executions": "1" + }, + "latency": { + "total": "7.2", + "unit": "msecs" + }, + "remote_calls": { + "total": "3", + "unit": "calls" + }, + "rows": { + "total": "3", + "unit": "rows" + } + }, + "index": 1, + "kind": "RELATIONAL", + "metadata": { + "distribution_table": "Singers", + "execution_method": "Row", + "split_ranges_aligned": "false", + "subquery_cluster_node": "2" + } + }, + { + "childLinks": [ + { + "childIndex": 3 + }, + { + "childIndex": 9 + }, + { + "childIndex": 10 + } + ], + "displayName": "Serialize Result", + "executionStats": { + "cpu_time": { + "total": "0.11", + "unit": "msecs" + }, + "execution_summary": { + "execution_end_timestamp": "1782662140.105714", + "execution_start_timestamp": "1782662140.105587", + "num_executions": "1" + }, + "latency": { + "total": "0.11", + "unit": "msecs" + }, + "rows": { + "total": "3", + "unit": "rows" + } + }, + "index": 2, + "kind": "RELATIONAL", + "metadata": { + "execution_method": "Row" + } + }, + { + "childLinks": [ + { + "childIndex": 4 + }, + { + "childIndex": 8, + "type": "Limit" + } + ], + "displayName": "Limit", + "executionStats": { + "cpu_time": { + "total": "0.1", + "unit": "msecs" + }, + "execution_summary": { + "num_executions": "1" + }, + "latency": { + "total": "0.1", + "unit": "msecs" + }, + "rows": { + "total": "3", + "unit": "rows" + } + }, + "index": 3, + "kind": "RELATIONAL", + "metadata": { + "call_type": "Local", + "execution_method": "Row" + } + }, + { + "childLinks": [ + { + "childIndex": 5 + } + ], + "displayName": "Distributed Union", + "executionStats": { + "cpu_time": { + "total": "0.1", + "unit": "msecs" + }, + "execution_summary": { + "num_executions": "1" + }, + "latency": { + "total": "0.1", + "unit": "msecs" + }, + "remote_calls": { + "total": "0", + "unit": "calls" + }, + "rows": { + "total": "3", + "unit": "rows" + } + }, + "index": 4, + "kind": "RELATIONAL", + "metadata": { + "call_type": "Local", + "execution_method": "Row", + "subquery_cluster_node": "5" + } + }, + { + "childLinks": [ + { + "childIndex": 6, + "variable": "SingerId" + }, + { + "childIndex": 7, + "variable": "FirstName" + } + ], + "displayName": "Scan", + "executionStats": { + "cpu_time": { + "total": "0.09", + "unit": "msecs" + }, + "deleted_rows": { + "mean": "0", + "std_deviation": "0", + "total": "0", + "unit": "rows" + }, + "execution_summary": { + "num_executions": "1" + }, + "filesystem_delay_seconds": { + "mean": "0", + "std_deviation": "0", + "total": "0", + "unit": "msecs" + }, + "filtered_rows": { + "mean": "0", + "std_deviation": "0", + "total": "0", + "unit": "rows" + }, + "latency": { + "total": "0.09", + "unit": "msecs" + }, + "rows": { + "total": "3", + "unit": "rows" + }, + "scanned_rows": { + "histogram": [ + { + "count": "1", + "lower_bound": "0", + "percentage": "50", + "upper_bound": "1" + }, + { + "count": "1", + "lower_bound": "1", + "percentage": "50", + "upper_bound": "4" + } + ], + "mean": "1.5", + "std_deviation": "1.5", + "total": "3", + "unit": "rows" + } + }, + "index": 5, + "kind": "RELATIONAL", + "metadata": { + "Full scan": "true", + "execution_method": "Row", + "scan_method": "Row", + "scan_target": "Singers", + "scan_type": "TableScan" + } + }, + { + "displayName": "Reference", + "index": 6, + "kind": "SCALAR", + "shortRepresentation": { + "description": "SingerId" + } + }, + { + "displayName": "Reference", + "index": 7, + "kind": "SCALAR", + "shortRepresentation": { + "description": "FirstName" + } + }, + { + "displayName": "Constant", + "index": 8, + "kind": "SCALAR", + "shortRepresentation": { + "description": "3" + } + }, + { + "displayName": "Reference", + "index": 9, + "kind": "SCALAR", + "shortRepresentation": { + "description": "$SingerId" + } + }, + { + "displayName": "Reference", + "index": 10, + "kind": "SCALAR", + "shortRepresentation": { + "description": "$FirstName" + } + }, + { + "displayName": "Constant", + "index": 11, + "kind": "SCALAR", + "shortRepresentation": { + "description": "true" + } + }, + { + "displayName": "Constant", + "index": 12, + "kind": "SCALAR", + "shortRepresentation": { + "description": "3" + } + } + ] + }, + "queryStats": { + "bytes_returned": "39", + "cpu_time": "18.46 msecs", + "data_bytes_read": "31886", + "deleted_rows_scanned": "0", + "elapsed_time": "20.6 msecs", + "filesystem_delay_seconds": "0 msecs", + "is_graph_query": "false", + "locking_delay": "0 msecs", + "memory_peak_usage_bytes": "35", + "memory_usage_percentage": "0.000", + "optimizer_statistics_package": "auto_20260627_06_11_04UTC", + "optimizer_version": "8", + "query_plan_creation_time": "5.76 msecs", + "query_text": "SELECT SingerId, FirstName FROM Singers ORDER BY SingerId LIMIT 3", + "remote_server_calls": "1/3", + "rows_returned": "3", + "rows_scanned": "3", + "runtime_creation_time": "0.4 msecs", + "server_queue_delay": "0.04 msecs", + "statistics_load_time": "0", + "time_to_first_row": "20.25 msecs", + "total_memory_peak_usage_byte": "35" + } + } +} diff --git a/testdata/yaml_output/all_types_dot.golden b/testdata/yaml_output/all_types_dot.golden new file mode 100644 index 0000000..c864f36 --- /dev/null +++ b/testdata/yaml_output/all_types_dot.golden @@ -0,0 +1,20 @@ +metadata: + rowType: + fields: + - name: payload + type: + code: BYTES + - name: d + type: + code: DATE + - name: ts + type: + code: TIMESTAMP + - name: "n" + type: + code: NUMERIC +rows: + - - YWJj + - "2024-06-01" + - "2024-06-01T12:34:56Z" + - "99.5" diff --git a/testdata/yaml_output/dca_albums_rowtype_rows.golden b/testdata/yaml_output/dca_albums_rowtype_rows.golden new file mode 100644 index 0000000..493729d --- /dev/null +++ b/testdata/yaml_output/dca_albums_rowtype_rows.golden @@ -0,0 +1,27 @@ +rowType: + fields: + - name: SingerId + type: + code: INT64 + - name: AlbumId + type: + code: INT64 + - name: AlbumTitle + type: + code: STRING + - name: MarketingBudget + type: + code: INT64 +rows: + - - "2" + - "5" + - Trackfringe + - "916353" + - - "3" + - "17" + - Tigergeode + - "540557" + - - "4" + - "26" + - Thieftime + - "926938" diff --git a/testdata/yaml_output/multi_scalar_rows.golden b/testdata/yaml_output/multi_scalar_rows.golden new file mode 100644 index 0000000..d02c82d --- /dev/null +++ b/testdata/yaml_output/multi_scalar_rows.golden @@ -0,0 +1,6 @@ +- - null + - true + - "3.5" +- - ok + - false + - "0" diff --git a/testdata/yaml_output/profile_albums_by_singer.golden b/testdata/yaml_output/profile_albums_by_singer.golden new file mode 100644 index 0000000..4d2528e --- /dev/null +++ b/testdata/yaml_output/profile_albums_by_singer.golden @@ -0,0 +1,282 @@ +metadata: + rowType: + fields: + - name: SingerId + type: + code: INT64 + - name: AlbumId + type: + code: INT64 + - name: AlbumTitle + type: + code: STRING + transaction: + readTimestamp: "2026-06-28T15:55:38.446224Z" + undeclaredParameters: {} +rows: + - - "2" + - "1" + - Green + - - "2" + - "2" + - Forever Hold Your Peace +stats: + queryPlan: + planNodes: + - childLinks: + - childIndex: "1" + - childIndex: "17" + type: Split Range + displayName: Distributed Union + executionStats: + cpu_time: + total: "0.23" + unit: msecs + execution_summary: + execution_end_timestamp: "1782662138.486515" + execution_start_timestamp: "1782662138.485666" + num_executions: "1" + latency: + total: "0.83" + unit: msecs + remote_calls: + total: "0" + unit: calls + rows: + total: "2" + unit: rows + kind: RELATIONAL + metadata: + distribution_table: Singers + execution_method: Row + split_ranges_aligned: "true" + subquery_cluster_node: "1" + - childLinks: + - childIndex: "2" + - childIndex: "14" + - childIndex: "15" + - childIndex: "16" + displayName: Serialize Result + executionStats: + cpu_time: + total: "0.21" + unit: msecs + execution_summary: + execution_end_timestamp: "1782662138.486510" + execution_start_timestamp: "1782662138.485691" + num_executions: "1" + latency: + total: "0.81" + unit: msecs + rows: + total: "2" + unit: rows + index: "1" + kind: RELATIONAL + metadata: + execution_method: Row + - childLinks: + - childIndex: "3" + - childIndex: "13" + type: Limit + displayName: Limit + executionStats: + cpu_time: + total: "0.21" + unit: msecs + execution_summary: + num_executions: "1" + latency: + total: "0.81" + unit: msecs + rows: + total: "2" + unit: rows + index: "2" + kind: RELATIONAL + metadata: + call_type: Global + execution_method: Row + - childLinks: + - childIndex: "4" + displayName: Distributed Union + executionStats: + cpu_time: + total: "0.21" + unit: msecs + execution_summary: + num_executions: "1" + latency: + total: "0.81" + unit: msecs + remote_calls: + total: "0" + unit: calls + rows: + total: "2" + unit: rows + index: "3" + kind: RELATIONAL + metadata: + call_type: Local + execution_method: Row + subquery_cluster_node: "4" + - childLinks: + - childIndex: "5" + displayName: Filter Scan + index: "4" + kind: RELATIONAL + metadata: + execution_method: Row + seekable_key_size: "0" + - childLinks: + - childIndex: "6" + variable: SingerId + - childIndex: "7" + variable: AlbumId + - childIndex: "8" + variable: AlbumTitle + - childIndex: "12" + type: Seek Condition + displayName: Scan + executionStats: + cpu_time: + total: "0.2" + unit: msecs + deleted_rows: + total: "0" + unit: rows + execution_summary: + num_executions: "1" + filesystem_delay_seconds: + total: "0.57" + unit: msecs + filtered_rows: + total: "0" + unit: rows + latency: + total: "0.8" + unit: msecs + rows: + total: "2" + unit: rows + scanned_rows: + total: "2" + unit: rows + index: "5" + kind: RELATIONAL + metadata: + execution_method: Row + scan_method: Row + scan_target: Albums + scan_type: TableScan + - displayName: Reference + index: "6" + kind: SCALAR + shortRepresentation: + description: SingerId + - displayName: Reference + index: "7" + kind: SCALAR + shortRepresentation: + description: AlbumId + - displayName: Reference + index: "8" + kind: SCALAR + shortRepresentation: + description: AlbumTitle + - childLinks: + - childIndex: "10" + - childIndex: "11" + displayName: Function + index: "9" + kind: SCALAR + shortRepresentation: + description: ($SingerId = @sid) + - displayName: Reference + index: "10" + kind: SCALAR + shortRepresentation: + description: $SingerId + - displayName: Parameter + index: "11" + kind: SCALAR + metadata: + name: sid + type: scalar + shortRepresentation: + description: '@sid' + - childLinks: + - childIndex: "9" + displayName: Function + index: "12" + kind: SCALAR + shortRepresentation: + description: ($SingerId = @sid) + - displayName: Constant + index: "13" + kind: SCALAR + shortRepresentation: + description: "2" + - displayName: Parameter + index: "14" + kind: SCALAR + metadata: + name: sid + type: scalar + shortRepresentation: + description: '@sid' + - displayName: Reference + index: "15" + kind: SCALAR + shortRepresentation: + description: $AlbumId + - displayName: Reference + index: "16" + kind: SCALAR + shortRepresentation: + description: $AlbumTitle + - childLinks: + - childIndex: "18" + - childIndex: "19" + displayName: Function + index: "17" + kind: SCALAR + shortRepresentation: + description: ($SingerId = @sid) + - displayName: Reference + index: "18" + kind: SCALAR + shortRepresentation: + description: $SingerId + - displayName: Parameter + index: "19" + kind: SCALAR + metadata: + name: sid + type: scalar + shortRepresentation: + description: '@sid' + queryStats: + bytes_returned: "48" + cpu_time: 13.58 msecs + data_bytes_read: "0" + deleted_rows_scanned: "0" + elapsed_time: 40.25 msecs + filesystem_delay_seconds: 0.57 msecs + is_graph_query: "false" + locking_delay: 0 msecs + memory_peak_usage_bytes: "52" + memory_usage_percentage: "0.000" + optimizer_statistics_package: auto_20260627_06_11_04UTC + optimizer_version: "8" + query_plan_creation_time: 8.33 msecs + query_text: SELECT SingerId, AlbumId, AlbumTitle FROM Albums WHERE SingerId = @sid ORDER BY AlbumId LIMIT 2 + remote_server_calls: 0/0 + rows_returned: "2" + rows_scanned: "2" + runtime_creation_time: 0.32 msecs + server_queue_delay: 0.03 msecs + statistics_load_time: "0" + time_to_first_row: 40.12 msecs + total_memory_peak_usage_byte: "52" diff --git a/testdata/yaml_output/profile_param_scalar.golden b/testdata/yaml_output/profile_param_scalar.golden new file mode 100644 index 0000000..6e54869 --- /dev/null +++ b/testdata/yaml_output/profile_param_scalar.golden @@ -0,0 +1,115 @@ +metadata: + rowType: + fields: + - name: i64 + type: + code: INT64 + - name: s + type: + code: STRING + - name: bl + type: + code: BOOL + transaction: + readTimestamp: "2026-06-28T15:55:36.515883Z" + undeclaredParameters: {} +rows: + - - "1" + - hello + - true +stats: + queryPlan: + planNodes: + - childLinks: + - childIndex: "1" + - childIndex: "3" + - childIndex: "4" + - childIndex: "5" + displayName: Serialize Result + executionStats: + cpu_time: + total: "0.01" + unit: msecs + execution_summary: + execution_end_timestamp: "1782662136.516507" + execution_start_timestamp: "1782662136.516479" + num_executions: "1" + latency: + total: "0.01" + unit: msecs + rows: + total: "1" + unit: rows + kind: RELATIONAL + metadata: + execution_method: Row + - childLinks: + - childIndex: "2" + displayName: Unit Relation + executionStats: + cpu_time: + total: "0" + unit: msecs + execution_summary: + num_executions: "1" + latency: + total: "0" + unit: msecs + rows: + total: "1" + unit: rows + index: "1" + kind: RELATIONAL + metadata: + execution_method: Row + - displayName: Constant + index: "2" + kind: SCALAR + shortRepresentation: + description: "1" + - displayName: Parameter + index: "3" + kind: SCALAR + metadata: + name: i64 + type: scalar + shortRepresentation: + description: '@i64' + - displayName: Parameter + index: "4" + kind: SCALAR + metadata: + name: s + type: scalar + shortRepresentation: + description: '@s' + - displayName: Parameter + index: "5" + kind: SCALAR + metadata: + name: bl + type: scalar + shortRepresentation: + description: '@bl' + queryStats: + bytes_returned: "17" + cpu_time: 0.36 msecs + deleted_rows_scanned: "0" + elapsed_time: 0.43 msecs + filesystem_delay_seconds: 0 msecs + is_graph_query: "false" + locking_delay: 0 msecs + memory_peak_usage_bytes: "27" + memory_usage_percentage: "0.000" + optimizer_statistics_package: auto_20260627_06_11_04UTC + optimizer_version: "8" + query_plan_cached: "true" + query_text: SELECT @i64 AS i64, @s AS s, @bl AS bl + remote_server_calls: 0/0 + rows_returned: "1" + rows_scanned: "0" + runtime_cached: "true" + server_queue_delay: 0.01 msecs + statistics_load_time: "0" + time_to_first_row: 0.39 msecs + total_memory_peak_usage_byte: "27" diff --git a/testdata/yaml_output/profile_singers_limit3.golden b/testdata/yaml_output/profile_singers_limit3.golden new file mode 100644 index 0000000..16cbf0c --- /dev/null +++ b/testdata/yaml_output/profile_singers_limit3.golden @@ -0,0 +1,254 @@ +metadata: + rowType: + fields: + - name: SingerId + type: + code: INT64 + - name: FirstName + type: + code: STRING + transaction: + readTimestamp: "2026-06-28T15:55:40.086987Z" + undeclaredParameters: {} +rows: + - - "1" + - Marc + - - "2" + - Catalina + - - "3" + - Alice +stats: + queryPlan: + planNodes: + - childLinks: + - childIndex: "1" + - childIndex: "12" + type: Limit + displayName: Limit + executionStats: + cpu_time: + total: "0.71" + unit: msecs + execution_summary: + execution_end_timestamp: "1782662140.107635" + execution_start_timestamp: "1782662140.100167" + num_executions: "1" + latency: + total: "7.2" + unit: msecs + rows: + total: "3" + unit: rows + kind: RELATIONAL + metadata: + call_type: Global + execution_method: Row + - childLinks: + - childIndex: "2" + - childIndex: "11" + type: Split Range + displayName: Distributed Union + executionStats: + cpu_time: + total: "0.7" + unit: msecs + execution_summary: + num_executions: "1" + latency: + total: "7.2" + unit: msecs + remote_calls: + total: "3" + unit: calls + rows: + total: "3" + unit: rows + index: "1" + kind: RELATIONAL + metadata: + distribution_table: Singers + execution_method: Row + split_ranges_aligned: "false" + subquery_cluster_node: "2" + - childLinks: + - childIndex: "3" + - childIndex: "9" + - childIndex: "10" + displayName: Serialize Result + executionStats: + cpu_time: + total: "0.11" + unit: msecs + execution_summary: + execution_end_timestamp: "1782662140.105714" + execution_start_timestamp: "1782662140.105587" + num_executions: "1" + latency: + total: "0.11" + unit: msecs + rows: + total: "3" + unit: rows + index: "2" + kind: RELATIONAL + metadata: + execution_method: Row + - childLinks: + - childIndex: "4" + - childIndex: "8" + type: Limit + displayName: Limit + executionStats: + cpu_time: + total: "0.1" + unit: msecs + execution_summary: + num_executions: "1" + latency: + total: "0.1" + unit: msecs + rows: + total: "3" + unit: rows + index: "3" + kind: RELATIONAL + metadata: + call_type: Local + execution_method: Row + - childLinks: + - childIndex: "5" + displayName: Distributed Union + executionStats: + cpu_time: + total: "0.1" + unit: msecs + execution_summary: + num_executions: "1" + latency: + total: "0.1" + unit: msecs + remote_calls: + total: "0" + unit: calls + rows: + total: "3" + unit: rows + index: "4" + kind: RELATIONAL + metadata: + call_type: Local + execution_method: Row + subquery_cluster_node: "5" + - childLinks: + - childIndex: "6" + variable: SingerId + - childIndex: "7" + variable: FirstName + displayName: Scan + executionStats: + cpu_time: + total: "0.09" + unit: msecs + deleted_rows: + mean: "0" + std_deviation: "0" + total: "0" + unit: rows + execution_summary: + num_executions: "1" + filesystem_delay_seconds: + mean: "0" + std_deviation: "0" + total: "0" + unit: msecs + filtered_rows: + mean: "0" + std_deviation: "0" + total: "0" + unit: rows + latency: + total: "0.09" + unit: msecs + rows: + total: "3" + unit: rows + scanned_rows: + histogram: + - count: "1" + lower_bound: "0" + percentage: "50" + upper_bound: "1" + - count: "1" + lower_bound: "1" + percentage: "50" + upper_bound: "4" + mean: "1.5" + std_deviation: "1.5" + total: "3" + unit: rows + index: "5" + kind: RELATIONAL + metadata: + Full scan: "true" + execution_method: Row + scan_method: Row + scan_target: Singers + scan_type: TableScan + - displayName: Reference + index: "6" + kind: SCALAR + shortRepresentation: + description: SingerId + - displayName: Reference + index: "7" + kind: SCALAR + shortRepresentation: + description: FirstName + - displayName: Constant + index: "8" + kind: SCALAR + shortRepresentation: + description: "3" + - displayName: Reference + index: "9" + kind: SCALAR + shortRepresentation: + description: $SingerId + - displayName: Reference + index: "10" + kind: SCALAR + shortRepresentation: + description: $FirstName + - displayName: Constant + index: "11" + kind: SCALAR + shortRepresentation: + description: "true" + - displayName: Constant + index: "12" + kind: SCALAR + shortRepresentation: + description: "3" + queryStats: + bytes_returned: "39" + cpu_time: 18.46 msecs + data_bytes_read: "31886" + deleted_rows_scanned: "0" + elapsed_time: 20.6 msecs + filesystem_delay_seconds: 0 msecs + is_graph_query: "false" + locking_delay: 0 msecs + memory_peak_usage_bytes: "35" + memory_usage_percentage: "0.000" + optimizer_statistics_package: auto_20260627_06_11_04UTC + optimizer_version: "8" + query_plan_creation_time: 5.76 msecs + query_text: SELECT SingerId, FirstName FROM Singers ORDER BY SingerId LIMIT 3 + remote_server_calls: 1/3 + rows_returned: "3" + rows_scanned: "3" + runtime_creation_time: 0.4 msecs + server_queue_delay: 0.04 msecs + statistics_load_time: "0" + time_to_first_row: 20.25 msecs + total_memory_peak_usage_byte: "35" diff --git a/testdata/yaml_output/profile_singers_limit3_field_names.golden b/testdata/yaml_output/profile_singers_limit3_field_names.golden new file mode 100644 index 0000000..7306159 --- /dev/null +++ b/testdata/yaml_output/profile_singers_limit3_field_names.golden @@ -0,0 +1,2 @@ +- SingerId +- FirstName diff --git a/testdata/yaml_output/profile_singers_limit3_rows_stream.golden b/testdata/yaml_output/profile_singers_limit3_rows_stream.golden new file mode 100644 index 0000000..87f77b7 --- /dev/null +++ b/testdata/yaml_output/profile_singers_limit3_rows_stream.golden @@ -0,0 +1,8 @@ +- "1" +- Marc +--- +- "2" +- Catalina +--- +- "3" +- Alice diff --git a/testdata/yaml_output/profile_singers_limit3_rowtype_rows.golden b/testdata/yaml_output/profile_singers_limit3_rowtype_rows.golden new file mode 100644 index 0000000..a29172e --- /dev/null +++ b/testdata/yaml_output/profile_singers_limit3_rowtype_rows.golden @@ -0,0 +1,15 @@ +rowType: + fields: + - name: SingerId + type: + code: INT64 + - name: FirstName + type: + code: STRING +rows: + - - "1" + - Marc + - - "2" + - Catalina + - - "3" + - Alice diff --git a/yaml_fixtures_test.go b/yaml_fixtures_test.go new file mode 100644 index 0000000..e08c0af --- /dev/null +++ b/yaml_fixtures_test.go @@ -0,0 +1,58 @@ +package main + +import ( + sppb "cloud.google.com/go/spanner/apiv1/spannerpb" + "google.golang.org/protobuf/types/known/structpb" +) + +// yamlGoldenCases maps fixture name to jq filter and ResultSet input. +// Golden files live under testdata/yaml_output/.golden. +// PROFILE-shaped cases use testdata/profile/*.json instead (see profile_yaml_test.go). +func yamlGoldenCases() map[string]yamlGoldenCase { + dcaAlbums := resultSet( + []string{"SingerId", "AlbumId", "AlbumTitle", "MarketingBudget"}, + []*sppb.Type{ + {Code: sppb.TypeCode_INT64}, + {Code: sppb.TypeCode_INT64}, + {Code: sppb.TypeCode_STRING}, + {Code: sppb.TypeCode_INT64}, + }, + [][]*structpb.Value{ + { + structpb.NewStringValue("2"), structpb.NewStringValue("5"), + structpb.NewStringValue("Trackfringe"), structpb.NewStringValue("916353"), + }, + { + structpb.NewStringValue("3"), structpb.NewStringValue("17"), + structpb.NewStringValue("Tigergeode"), structpb.NewStringValue("540557"), + }, + { + structpb.NewStringValue("4"), structpb.NewStringValue("26"), + structpb.NewStringValue("Thieftime"), structpb.NewStringValue("926938"), + }, + }, + ) + + allTypes := csvFixtures()["bytes_date_timestamp_numeric"] + multiScalar := csvFixtures()["null_bool_float64"] + + return map[string]yamlGoldenCase{ + "dca_albums_rowtype_rows": { + filter: `{rowType: .metadata.rowType, rows: .rows}`, + rs: dcaAlbums, + }, + "all_types_dot": { + filter: `.`, + rs: allTypes, + }, + "multi_scalar_rows": { + filter: `.rows`, + rs: multiScalar, + }, + } +} + +type yamlGoldenCase struct { + filter string + rs *sppb.ResultSet +} diff --git a/yaml_golden_test.go b/yaml_golden_test.go new file mode 100644 index 0000000..de5d886 --- /dev/null +++ b/yaml_golden_test.go @@ -0,0 +1,80 @@ +package main + +import ( + "bytes" + "os" + "path/filepath" + "testing" + + sppb "cloud.google.com/go/spanner/apiv1/spannerpb" + + "github.com/apstndb/execspansql/jqresult" +) + +func encodeResultSetYAML(filter string, rs *sppb.ResultSet) ([]byte, error) { + code, err := jqresult.Compile(filter, jqresult.InputEager) + if err != nil { + return nil, err + } + iter, cleanup, err := jqresult.Execute(code, jqresult.InputEager, nil, rs, false) + if err != nil { + return nil, err + } + defer cleanup() + + var buf bytes.Buffer + enc, err := newEncoder(&buf, "yaml", false, false) + if err != nil { + return nil, err + } + if err := jqresult.Print(enc, iter); err != nil { + return nil, err + } + if closer, ok := enc.(interface{ Close() error }); ok { + if err := closer.Close(); err != nil { + return nil, err + } + } + return buf.Bytes(), nil +} + +func TestYamlOutputGolden(t *testing.T) { + for name, tc := range yamlGoldenCases() { + name, tc := name, tc + t.Run(name, func(t *testing.T) { + goldenPath := filepath.Join("testdata", "yaml_output", name+".golden") + + got, err := encodeResultSetYAML(tc.filter, tc.rs) + if err != nil { + t.Fatalf("encodeResultSetYAML() error = %v", err) + } + + if *updateGolden { + if err := os.MkdirAll(filepath.Dir(goldenPath), 0o755); err != nil { + t.Fatalf("MkdirAll() error = %v", err) + } + if err := os.WriteFile(goldenPath, got, 0o644); err != nil { + t.Fatalf("WriteFile() error = %v", err) + } + t.Logf("updated %s", goldenPath) + return + } + + want, err := os.ReadFile(goldenPath) + if err != nil { + t.Fatalf("ReadFile(%q) error = %v (run: go test -update-golden -run TestYamlOutputGolden)", goldenPath, err) + } + if string(got) != string(want) { + t.Fatalf("YAML output mismatch for %s\n\ngot:\n%s\n\nwant:\n%s", name, got, want) + } + }) + } +} + +func TestNewEncoderUnknownFormat(t *testing.T) { + t.Parallel() + + if _, err := newEncoder(&bytes.Buffer{}, "nope", false, false); err == nil { + t.Fatal("expected error for unknown format") + } +}