Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion internal/mycli/dump_cyclic_mutations.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ func dumpMutationFormatConfig() *spanvalue.FormatConfig {
// decoding them. Validate scalars (also inside arrays) before falling
// through to the shared literal policy, so malformed values fail before
// any dump output. Do not format the decoded wrapper: retain wire fidelity.
return sqlLiteralFormatConfig().WithComplexPlugin(spanvalue.PluginFromNullable(
return spanvalue.LiteralFormatConfig().WithComplexPlugin(spanvalue.PluginFromNullable(
func(spanvalue.NullableValue) (string, error) { return "", spanvalue.ErrFallthrough },
)).WithComplexPlugin(func(_ spanvalue.Formatter, value spanner.GenericColumnValue, _ bool) (string, error) {
if value.Value == nil || value.Value.Kind == nil {
Expand Down
23 changes: 1 addition & 22 deletions internal/mycli/execute_sql.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ func prepareFormatConfig(sql string, sysVars *systemVariables, render queryRende

switch vfm {
case format.SQLLiteralValues:
render.Spanvalue = sqlLiteralFormatConfig()
render.Spanvalue = spanvalue.LiteralFormatConfig()
if render.Export.SQLTableName == "" {
detectedTableName, detectionErr := extractTableNameFromQuery(sql)
if detectedTableName != "" {
Expand Down Expand Up @@ -143,27 +143,6 @@ func prepareFormatConfig(sql string, sysVars *systemVariables, render queryRende
}
}

// sqlLiteralFormatConfig keeps SQL export and typed replay on the same policy.
func sqlLiteralFormatConfig() *spanvalue.FormatConfig {
// spanvalue v0.8.4 emits CAST(-0 AS FLOAT32), whose integer operand loses
// the sign on replay. Remove this bridge after adopting an upstream version
// that preserves FLOAT32 negative zero. A typed plugin also covers nested
// values without rewriting matching text inside STRING or JSON literals.
return spanvalue.LiteralFormatConfig().WithComplexPlugin(spanvalue.PluginForTypeCode(
sppb.TypeCode_FLOAT32,
func(_ spanvalue.Formatter, value spanner.GenericColumnValue, _ bool) (string, error) {
var f spanner.NullFloat32
if err := value.Decode(&f); err != nil {
return "", err
}
if f.Valid && f.Float32 == 0 && math.Signbit(float64(f.Float32)) {
return "CAST(-0.0 AS FLOAT32)", nil
}
return "", spanvalue.ErrFallthrough
},
))
}

// newMetrics creates and initializes execution metrics from system variables.
func newMetrics(sysVars *systemVariables) *metrics.ExecutionMetrics {
m := &metrics.ExecutionMetrics{
Expand Down
56 changes: 19 additions & 37 deletions internal/mycli/execute_sql_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,36 +35,23 @@ import (
"google.golang.org/protobuf/types/known/structpb"
)

// TestSQLLiteralFormatConfigFloat32 checks mycli SQL-literal export and
// typed-replay formatter selection. The scalar/array/struct CAST(-0.0 AS
// FLOAT32) matrix is owned by spanvalue v0.9.0:
// - literal_test.go TestLiteralFloat32NegativeZero
// - writer/writer_test.go TestSQLInsertWriterFloat32NegativeZero
func TestSQLLiteralFormatConfigFloat32(t *testing.T) {
t.Parallel()
negativeZero := float32(math.Copysign(0, -1))
structValue := spanner.GenericColumnValue{
Type: &sppb.Type{Code: sppb.TypeCode_STRUCT, StructType: &sppb.StructType{
Fields: []*sppb.StructType_Field{{Name: "F", Type: &sppb.Type{Code: sppb.TypeCode_FLOAT32}}},
}},
Value: structpb.NewListValue(&structpb.ListValue{Values: []*structpb.Value{structpb.NewNumberValue(math.Copysign(0, -1))}}),
}
for _, tc := range []struct {
name string
value any
want string // Empty means preserve the upstream preset's existing bytes.
want string
}{
{"negative zero", negativeZero, "CAST(-0.0 AS FLOAT32)"},
{"nullable negative zero", spanner.NullFloat32{Float32: negativeZero, Valid: true}, "CAST(-0.0 AS FLOAT32)"},
{"positive zero", float32(0), ""},
{"integer", float32(3), ""},
{"finite", float32(1.5), ""},
{"NaN", float32(math.NaN()), ""},
{"positive infinity", float32(math.Inf(1)), ""},
{"negative infinity", float32(math.Inf(-1)), ""},
{"NULL", spanner.NullFloat32{}, ""},
{"FLOAT64 negative zero", math.Copysign(0, -1), ""},
{"STRING with cast text", "CAST(-0 AS FLOAT32)", ""},
{"JSON with cast text", spanner.NullJSON{Value: map[string]any{"text": "CAST(-0 AS FLOAT32)"}, Valid: true}, ""},
{"array", []spanner.NullFloat32{{Float32: negativeZero, Valid: true}, {Valid: true}, {}}, "[CAST(-0.0 AS FLOAT32), CAST(0 AS FLOAT32), NULL]"},
{"empty array", []spanner.NullFloat32{}, ""},
{"NULL array", []spanner.NullFloat32(nil), ""},
{"struct field", structValue, "STRUCT<F FLOAT32>(CAST(-0.0 AS FLOAT32))"},
{"negative zero", float32(math.Copysign(0, -1)), "CAST(-0.0 AS FLOAT32)"},
{"STRING with cast text", "CAST(-0 AS FLOAT32)", `"CAST(-0 AS FLOAT32)"`},
{"JSON with cast text", spanner.NullJSON{Value: map[string]any{"text": "CAST(-0 AS FLOAT32)"}, Valid: true}, `JSON '{"text":"CAST(-0 AS FLOAT32)"}'`},
{"empty array", []spanner.NullFloat32{}, "[]"},
{"NULL array", []spanner.NullFloat32(nil), "NULL"},
} {
t.Run(tc.name, func(t *testing.T) {
row, err := spanner.NewRow([]string{"V"}, []any{tc.value})
Expand All @@ -75,13 +62,6 @@ func TestSQLLiteralFormatConfigFloat32(t *testing.T) {
if err := row.Column(0, &value); err != nil {
t.Fatal(err)
}
want := tc.want
if want == "" {
want, err = spanvalue.LiteralFormatConfig().FormatToplevelColumn(value)
if err != nil {
t.Fatal(err)
}
}
for _, mode := range []enums.DisplayMode{enums.DisplayModeSQLInsert, enums.DisplayModeSQLInsertOrIgnore, enums.DisplayModeSQLInsertOrUpdate} {
sv := newSystemVariablesWithDefaults()
sv.Display.CLIFormat = mode
Expand All @@ -100,8 +80,8 @@ func TestSQLLiteralFormatConfigFloat32(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if got != want {
t.Errorf("%s/%s got %s, want %s", mode, name, got, want)
if got != tc.want {
t.Errorf("%s/%s got %s, want %s", mode, name, got, tc.want)
}
}
}
Expand All @@ -120,8 +100,9 @@ func TestSQLLiteralFormatConfigInvalidFloat32(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if _, err := config.FormatToplevelColumn(value); err == nil {
t.Fatal("expected invalid FLOAT32 error")
_, err = config.FormatToplevelColumn(value)
if !errors.Is(err, spanvalue.ErrMalformedWire) || !strings.Contains(err.Error(), `"not a float"`) {
t.Fatalf("invalid FLOAT32 error = %v, want ErrMalformedWire mentioning the payload", err)
}
}

Expand Down Expand Up @@ -199,8 +180,9 @@ func TestSQLLiteralFormatConfigInvalidFloat32Streaming(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if _, err := render.Spanvalue.FormatToplevelColumn(value); err == nil {
t.Fatal("expected invalid FLOAT32 error from sqlLiteralFormatConfig")
_, err = render.Spanvalue.FormatToplevelColumn(value)
if !errors.Is(err, spanvalue.ErrMalformedWire) || !strings.Contains(err.Error(), `"not a float"`) {
t.Fatalf("invalid FLOAT32 streaming error = %v, want ErrMalformedWire mentioning the payload", err)
}
}

Expand Down
16 changes: 8 additions & 8 deletions internal/mycli/spanvalue_streaming_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ func TestNewSpanvalueRowIteratorWriterForContract(t *testing.T) {
t.Run("SQL export missing table name is handled with exact error text", func(t *testing.T) {
t.Parallel()
var buf bytes.Buffer
w, handled, err := newSpanvalueRowIteratorWriterFor(&buf, exportWriterOptions{CLIFormat: enums.DisplayModeSQLInsert}, sqlLiteralFormatConfig())
w, handled, err := newSpanvalueRowIteratorWriterFor(&buf, exportWriterOptions{CLIFormat: enums.DisplayModeSQLInsert}, spanvalue.LiteralFormatConfig())
if w != nil || !handled {
t.Fatalf("got writer=%v handled=%v, want nil, true", w, handled)
}
Expand All @@ -87,7 +87,7 @@ func TestNewSpanvalueRowIteratorWriterForContract(t *testing.T) {
CLIFormat: enums.DisplayModeSQLInsert,
SQLTableName: "Items",
SQLBatchSize: -1,
}, sqlLiteralFormatConfig())
}, spanvalue.LiteralFormatConfig())
if !handled {
t.Fatal("negative batch size should be handled")
}
Expand All @@ -104,7 +104,7 @@ func TestNewSpanvalueRowIteratorWriterForContract(t *testing.T) {
CLIFormat: enums.DisplayModeSQLInsert,
SQLTableName: "Items",
SQLBatchSize: 10001,
}, sqlLiteralFormatConfig())
}, spanvalue.LiteralFormatConfig())
if !handled {
t.Fatal("oversized batch size should be handled")
}
Expand Down Expand Up @@ -148,25 +148,25 @@ func TestNewSpanvalueRowIteratorWriterForOutput(t *testing.T) {
{
name: "SQL INSERT uses table name and GoogleSQL string quotes",
opts: exportWriterOptions{CLIFormat: enums.DisplayModeSQLInsert, SQLTableName: "Items"},
fc: sqlLiteralFormatConfig(),
fc: spanvalue.LiteralFormatConfig(),
want: "INSERT INTO `Items` (`id`, `name`) VALUES (1, \"Alice\");\nINSERT INTO `Items` (`id`, `name`) VALUES (2, \"Bob\");\nINSERT INTO `Items` (`id`, `name`) VALUES (3, \"Carol\");\n",
},
{
name: "SQL INSERT OR IGNORE",
opts: exportWriterOptions{CLIFormat: enums.DisplayModeSQLInsertOrIgnore, SQLTableName: "Items"},
fc: sqlLiteralFormatConfig(),
fc: spanvalue.LiteralFormatConfig(),
want: "INSERT OR IGNORE INTO `Items` (`id`, `name`) VALUES (1, \"Alice\");\nINSERT OR IGNORE INTO `Items` (`id`, `name`) VALUES (2, \"Bob\");\nINSERT OR IGNORE INTO `Items` (`id`, `name`) VALUES (3, \"Carol\");\n",
},
{
name: "SQL INSERT OR UPDATE",
opts: exportWriterOptions{CLIFormat: enums.DisplayModeSQLInsertOrUpdate, SQLTableName: "Items"},
fc: sqlLiteralFormatConfig(),
fc: spanvalue.LiteralFormatConfig(),
want: "INSERT OR UPDATE INTO `Items` (`id`, `name`) VALUES (1, \"Alice\");\nINSERT OR UPDATE INTO `Items` (`id`, `name`) VALUES (2, \"Bob\");\nINSERT OR UPDATE INTO `Items` (`id`, `name`) VALUES (3, \"Carol\");\n",
},
{
name: "SQL batch size groups VALUES lists",
opts: exportWriterOptions{CLIFormat: enums.DisplayModeSQLInsert, SQLTableName: "Items", SQLBatchSize: 2},
fc: sqlLiteralFormatConfig(),
fc: spanvalue.LiteralFormatConfig(),
want: "INSERT INTO `Items` (`id`, `name`) VALUES\n (1, \"Alice\"),\n (2, \"Bob\");\nINSERT INTO `Items` (`id`, `name`) VALUES\n (3, \"Carol\");\n",
},
{
Expand All @@ -176,7 +176,7 @@ func TestNewSpanvalueRowIteratorWriterForOutput(t *testing.T) {
SQLTableName: "Items",
DatabaseDialect: databasepb.DatabaseDialect_POSTGRESQL,
},
fc: sqlLiteralFormatConfig(),
fc: spanvalue.LiteralFormatConfig(),
want: "INSERT INTO \"Items\" (\"id\", \"name\") VALUES (1, \"Alice\");\nINSERT INTO \"Items\" (\"id\", \"name\") VALUES (2, \"Bob\");\nINSERT INTO \"Items\" (\"id\", \"name\") VALUES (3, \"Carol\");\n",
},
}
Expand Down
2 changes: 1 addition & 1 deletion internal/mycli/typed_rows.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ func typedReplayFormatConfig(sysVars *systemVariables) (*spanvalue.FormatConfig,
vfm := format.ValueFormatModeFor(format.Mode(sysVars.Display.CLIFormat.String()))
switch vfm {
case format.SQLLiteralValues:
return sqlLiteralFormatConfig(), vfm, nil
return spanvalue.LiteralFormatConfig(), vfm, nil
case format.JSONValues:
return decoder.JSONFormatConfig(), vfm, nil
default:
Expand Down