diff --git a/internal/mycli/app.go b/internal/mycli/app.go index 422a1d98..4dff9f01 100644 --- a/internal/mycli/app.go +++ b/internal/mycli/app.go @@ -41,6 +41,7 @@ import ( tcspanner "github.com/testcontainers/testcontainers-go/modules/gcloud/spanner" "github.com/apstndb/spanner-mycli/enums" + "github.com/apstndb/spanner-mycli/internal/mycli/streamio" "github.com/cloudspannerecosystem/memefish" @@ -573,7 +574,7 @@ func run(ctx context.Context, opts *spannerOptions) error { var originalOut io.Writer = os.Stdout // Create StreamManager for managing all input/output streams - streamManager := NewStreamManager(os.Stdin, originalOut, errStream) + streamManager := streamio.NewStreamManager(os.Stdin, originalOut, errStream) // StreamManager will automatically detect if os.Stdout is a TTY if term.IsTerminal(int(os.Stdout.Fd())) { streamManager.SetTtyStream(os.Stdout) diff --git a/internal/mycli/cli_current_width_test.go b/internal/mycli/cli_current_width_test.go index 83a1f4ef..4f0b817e 100644 --- a/internal/mycli/cli_current_width_test.go +++ b/internal/mycli/cli_current_width_test.go @@ -6,6 +6,8 @@ import ( "os" "strconv" "testing" + + "github.com/apstndb/spanner-mycli/internal/mycli/streamio" ) func TestCliCurrentWidthWithTee(t *testing.T) { @@ -15,7 +17,7 @@ func TestCliCurrentWidthWithTee(t *testing.T) { t.Run("with TtyStream in StreamManager", func(t *testing.T) { // Setup StreamManager with a buffer for tee output - teeManager := NewStreamManager(os.Stdin, os.Stdout, os.Stderr) + teeManager := streamio.NewStreamManager(os.Stdin, os.Stdout, os.Stderr) teeManager.SetTtyStream(os.Stdout) sysVars := &systemVariables{ @@ -42,7 +44,7 @@ func TestCliCurrentWidthWithTee(t *testing.T) { t.Run("without TtyStream and non-file stream", func(t *testing.T) { // Setup StreamManager with non-TTY output consoleBuf := &bytes.Buffer{} - teeManager := NewStreamManager(io.NopCloser(bytes.NewReader(nil)), consoleBuf, consoleBuf) + teeManager := streamio.NewStreamManager(io.NopCloser(bytes.NewReader(nil)), consoleBuf, consoleBuf) // Do not set TTY stream sysVars := &systemVariables{ diff --git a/internal/mycli/cli_output.go b/internal/mycli/cli_output.go index efb2f853..9a0442e8 100644 --- a/internal/mycli/cli_output.go +++ b/internal/mycli/cli_output.go @@ -12,6 +12,7 @@ import ( sppb "cloud.google.com/go/spanner/apiv1/spannerpb" "github.com/apstndb/lox" "github.com/apstndb/spanner-mycli/enums" + "github.com/apstndb/spanner-mycli/internal/mycli/decoder" "github.com/apstndb/spanner-mycli/internal/mycli/format" "github.com/apstndb/spanner-mycli/internal/mycli/metrics" "github.com/go-sprout/sprout" @@ -280,5 +281,5 @@ func resultLine(outputTemplate *template.Template, result *Result, verbose bool) } func formatTypedHeaderColumn(field *sppb.StructType_Field) string { - return field.GetName() + "\n" + formatTypeSimple(field.GetType()) + return field.GetName() + "\n" + decoder.FormatTypeSimple(field.GetType()) } diff --git a/internal/mycli/cli_test.go b/internal/mycli/cli_test.go index c85d3906..daabfca4 100644 --- a/internal/mycli/cli_test.go +++ b/internal/mycli/cli_test.go @@ -40,6 +40,7 @@ import ( "google.golang.org/protobuf/encoding/protojson" "google.golang.org/protobuf/proto" + "github.com/apstndb/spanner-mycli/internal/mycli/streamio" "github.com/apstndb/spanner-mycli/internal/protostruct" ) @@ -1122,7 +1123,7 @@ func TestCli_handleExit(t *testing.T) { t.Parallel() outBuf := &bytes.Buffer{} sysVars := &systemVariables{ - StreamManager: NewStreamManager(io.NopCloser(bytes.NewReader(nil)), outBuf, outBuf), + StreamManager: streamio.NewStreamManager(io.NopCloser(bytes.NewReader(nil)), outBuf, outBuf), } cli := &Cli{ SessionHandler: NewSessionHandler(&Session{}), // Dummy session, Close() is now safe with nil client @@ -1163,7 +1164,7 @@ func TestCli_ExitOnError(t *testing.T) { t.Run(tt.desc, func(t *testing.T) { errBuf := &bytes.Buffer{} sysVars := &systemVariables{ - StreamManager: NewStreamManager(io.NopCloser(bytes.NewReader(nil)), errBuf, errBuf), + StreamManager: streamio.NewStreamManager(io.NopCloser(bytes.NewReader(nil)), errBuf, errBuf), } cli := &Cli{ SessionHandler: NewSessionHandler(&Session{}), // Dummy session, Close() is now safe with nil client @@ -1266,7 +1267,7 @@ func TestCli_handleSpecialStatements(t *testing.T) { } // Create StreamManager with the test streams - sysVars.StreamManager = NewStreamManager( + sysVars.StreamManager = streamio.NewStreamManager( io.NopCloser(strings.NewReader(tt.confirmInput)), // InStream for confirm outBuf, errBuf, @@ -1323,7 +1324,7 @@ func TestCli_PrintResult(t *testing.T) { sysVars := &systemVariables{ UsePager: tt.usePager, CLIFormat: enums.DisplayModeTab, // Use TAB format for predictable output - StreamManager: NewStreamManager(io.NopCloser(bytes.NewReader(nil)), outBuf, outBuf), + StreamManager: streamio.NewStreamManager(io.NopCloser(bytes.NewReader(nil)), outBuf, outBuf), } cli := &Cli{ SystemVariables: sysVars, @@ -1367,7 +1368,7 @@ func TestCli_PrintBatchError(t *testing.T) { t.Run(tt.desc, func(t *testing.T) { errBuf := &bytes.Buffer{} sysVars := &systemVariables{ - StreamManager: NewStreamManager(io.NopCloser(bytes.NewReader(nil)), errBuf, errBuf), + StreamManager: streamio.NewStreamManager(io.NopCloser(bytes.NewReader(nil)), errBuf, errBuf), } cli := &Cli{ SystemVariables: sysVars, @@ -1553,7 +1554,7 @@ func TestCli_executeSourceFile(t *testing.T) { sysVars := &systemVariables{ BuildStatementMode: enums.ParseModeFallback, CLIFormat: enums.DisplayModeTab, - StreamManager: NewStreamManager(io.NopCloser(bytes.NewReader(nil)), outBuf, outBuf), + StreamManager: streamio.NewStreamManager(io.NopCloser(bytes.NewReader(nil)), outBuf, outBuf), } session := &Session{systemVariables: sysVars} @@ -1639,7 +1640,7 @@ func TestCli_executeSourceFile_FileTooLarge(t *testing.T) { outBuf := &bytes.Buffer{} errBuf := &bytes.Buffer{} sysVars := &systemVariables{ - StreamManager: NewStreamManager(io.NopCloser(bytes.NewReader(nil)), outBuf, errBuf), + StreamManager: streamio.NewStreamManager(io.NopCloser(bytes.NewReader(nil)), outBuf, errBuf), } cli := &Cli{ SessionHandler: NewSessionHandler(&Session{}), diff --git a/internal/mycli/decoder.go b/internal/mycli/decoder/decoder.go similarity index 87% rename from internal/mycli/decoder.go rename to internal/mycli/decoder/decoder.go index 1984f7c0..0ff797ae 100644 --- a/internal/mycli/decoder.go +++ b/internal/mycli/decoder/decoder.go @@ -14,7 +14,7 @@ // limitations under the License. // -package mycli +package decoder import ( "cmp" @@ -40,7 +40,11 @@ func DecodeRow(row *spanner.Row) ([]string, error) { return spanvalue.FormatRowSpannerCLICompatible(row) } -func formatConfigWithProto(fds *descriptorpb.FileDescriptorSet, multiline bool) (*spanvalue.FormatConfig, error) { +// FormatConfigWithProto creates a format configuration for decoding Spanner values, +// with special handling for PROTO and ENUM types based on the provided protobuf file descriptor set. +// If fds is nil, it returns a config without custom proto/enum support. +// The multiline parameter controls the formatting of protobuf messages. +func FormatConfigWithProto(fds *descriptorpb.FileDescriptorSet, multiline bool) (*spanvalue.FormatConfig, error) { types, err := dynamicTypesByFDS(fds) if err != nil { return nil, err @@ -134,8 +138,8 @@ func formatEnum(types protoEnumResolver) func(formatter spanvalue.Formatter, val } } -// formatTypeSimple is format type for headers. -func formatTypeSimple(typ *sppb.Type) string { +// FormatTypeSimple is format type for headers. +func FormatTypeSimple(typ *sppb.Type) string { return spantype.FormatType(typ, spantype.FormatOption{ Struct: spantype.StructModeBase, Proto: spantype.ProtoEnumModeLeafWithKind, @@ -144,7 +148,7 @@ func formatTypeSimple(typ *sppb.Type) string { }) } -// formatTypeVerbose is format type for DESCRIBE. -func formatTypeVerbose(typ *sppb.Type) string { +// FormatTypeVerbose is format type for DESCRIBE. +func FormatTypeVerbose(typ *sppb.Type) string { return spantype.FormatTypeMoreVerbose(typ) } diff --git a/internal/mycli/decoder_test.go b/internal/mycli/decoder/decoder_test.go similarity index 96% rename from internal/mycli/decoder_test.go rename to internal/mycli/decoder/decoder_test.go index ed9cd277..31b9cd2c 100644 --- a/internal/mycli/decoder_test.go +++ b/internal/mycli/decoder/decoder_test.go @@ -14,7 +14,7 @@ // limitations under the License. // -package mycli +package decoder import ( "math/big" @@ -399,7 +399,7 @@ func TestDecodeColumn(t *testing.T) { for _, test := range tests { t.Run(test.desc, func(t *testing.T) { - got, err := lo.Must(formatConfigWithProto(test.fds, test.multiline)).FormatToplevelColumn(createColumnValue(t, test.value)) + got, err := lo.Must(FormatConfigWithProto(test.fds, test.multiline)).FormatToplevelColumn(createColumnValue(t, test.value)) if err != nil { t.Error(err) } @@ -410,7 +410,7 @@ func TestDecodeColumn(t *testing.T) { t.Error(err) } if diff := cmp.Diff(nm.Interface(), test.wantMessage, protocmp.Transform()); diff != "" { - t.Errorf("formatConfigWithProto(%v) mismatch (-got +want):\n%s", test.value, diff) + t.Errorf("FormatConfigWithProto(%v) mismatch (-got +want):\n%s", test.value, diff) } } else { if got != test.want { @@ -442,7 +442,7 @@ func TestDecodeColumnRoundTripEnum(t *testing.T) { for _, test := range tests { t.Run(test.desc, func(t *testing.T) { - got, err := lo.Must(formatConfigWithProto(test.fds, false)).FormatToplevelColumn(createColumnValue(t, test.value)) + got, err := lo.Must(FormatConfigWithProto(test.fds, false)).FormatToplevelColumn(createColumnValue(t, test.value)) if err != nil { t.Error(err) } @@ -450,7 +450,7 @@ func TestDecodeColumnRoundTripEnum(t *testing.T) { gotEnumValue := test.want.Type().Descriptor().Values().ByName(protoreflect.Name(got)) gotNumber := gotEnumValue.Number() if gotNumber != test.want.Number() { - t.Errorf("formatConfigWithProto(%v): %v(%v), want: %v(%v)", test.value, gotEnumValue.Name(), gotNumber, test.want, test.want.Number()) + t.Errorf("FormatConfigWithProto(%v): %v(%v), want: %v(%v)", test.value, gotEnumValue.Name(), gotNumber, test.want, test.want.Number()) } }) } @@ -482,7 +482,7 @@ func TestDecodeColumnRoundTripProto(t *testing.T) { for _, test := range tests { t.Run(test.desc, func(t *testing.T) { - got, err := lo.Must(formatConfigWithProto(test.fds, test.multiline)).FormatToplevelColumn(createColumnValue(t, test.value)) + got, err := lo.Must(FormatConfigWithProto(test.fds, test.multiline)).FormatToplevelColumn(createColumnValue(t, test.value)) if err != nil { t.Error(err) } @@ -492,7 +492,7 @@ func TestDecodeColumnRoundTripProto(t *testing.T) { t.Error(err) } if diff := cmp.Diff(nm.Interface(), test.want, protocmp.Transform()); diff != "" { - t.Errorf("formatConfigWithProto(%v) mismatch (-got +want):\n%s", test.value, diff) + t.Errorf("FormatConfigWithProto(%v) mismatch (-got +want):\n%s", test.value, diff) } }) } @@ -549,9 +549,9 @@ func TestFormatTypeVerbose(t *testing.T) { } for _, test := range tests { t.Run(test.desc, func(t *testing.T) { - got := formatTypeVerbose(test.sppbType) + got := FormatTypeVerbose(test.sppbType) if diff := cmp.Diff(got, test.want); diff != "" { - t.Errorf("formatTypeVerbose(%v) mismatch (-got +want):\n%s", test.sppbType, diff) + t.Errorf("FormatTypeVerbose(%v) mismatch (-got +want):\n%s", test.sppbType, diff) } }) } diff --git a/internal/mycli/execute_sql.go b/internal/mycli/execute_sql.go index 46b893a6..3cb1a867 100644 --- a/internal/mycli/execute_sql.go +++ b/internal/mycli/execute_sql.go @@ -16,6 +16,7 @@ import ( "github.com/apstndb/gsqlutils" "github.com/apstndb/spanner-mycli/enums" + "github.com/apstndb/spanner-mycli/internal/mycli/decoder" "github.com/apstndb/spanner-mycli/internal/mycli/format" "github.com/apstndb/spanner-mycli/internal/mycli/metrics" "github.com/apstndb/spanvalue" @@ -104,7 +105,7 @@ func prepareFormatConfig(sql string, sysVars *systemVariables) (*spanvalue.Forma default: // Use regular display formatting for other modes // formatConfigWithProto handles custom proto descriptors if set - fc, err = formatConfigWithProto(sysVars.ProtoDescriptor, sysVars.MultilineProtoText) + fc, err = decoder.FormatConfigWithProto(sysVars.ProtoDescriptor, sysVars.MultilineProtoText) usingSQLLiterals = false } @@ -726,7 +727,7 @@ func spannerRowToRow(fc *spanvalue.FormatConfig) func(row *spanner.Row) (Row, er } func runPartitionedQuery(ctx context.Context, session *Session, sql string) (*Result, error) { - fc, err := formatConfigWithProto(session.systemVariables.ProtoDescriptor, session.systemVariables.MultilineProtoText) + fc, err := decoder.FormatConfigWithProto(session.systemVariables.ProtoDescriptor, session.systemVariables.MultilineProtoText) if err != nil { return nil, err } diff --git a/internal/mycli/integration_dump_test.go b/internal/mycli/integration_dump_test.go index f192c0a9..35235c77 100644 --- a/internal/mycli/integration_dump_test.go +++ b/internal/mycli/integration_dump_test.go @@ -9,6 +9,7 @@ import ( "testing" "github.com/apstndb/spanner-mycli/enums" + "github.com/apstndb/spanner-mycli/internal/mycli/streamio" "github.com/google/go-cmp/cmp" ) @@ -267,7 +268,7 @@ func TestDumpWithStreaming(t *testing.T) { // Replace the session's output stream with our buffer // This simulates streaming mode with captured output originalStream := session.systemVariables.StreamManager - session.systemVariables.StreamManager = NewStreamManager( + session.systemVariables.StreamManager = streamio.NewStreamManager( originalStream.GetInStream(), &buf, // Use our buffer as output originalStream.GetErrStream(), diff --git a/internal/mycli/integration_mcp_test.go b/internal/mycli/integration_mcp_test.go index 19863256..399dbbd6 100644 --- a/internal/mycli/integration_mcp_test.go +++ b/internal/mycli/integration_mcp_test.go @@ -10,6 +10,7 @@ import ( sppb "cloud.google.com/go/spanner/apiv1/spannerpb" "github.com/apstndb/spanemuboost" + "github.com/apstndb/spanner-mycli/internal/mycli/streamio" "github.com/cloudspannerecosystem/memefish/ast" "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/samber/lo" @@ -26,7 +27,7 @@ func setupMCPClientServer(t *testing.T, ctx context.Context, session *Session) ( session.systemVariables.Verbose = true // Set Verbose to true to ensure result line is printed // Update the session's StreamManager to use the output buffer - session.systemVariables.StreamManager = NewStreamManager(io.NopCloser(strings.NewReader("")), &outputBuf, &outputBuf) + session.systemVariables.StreamManager = streamio.NewStreamManager(io.NopCloser(strings.NewReader("")), &outputBuf, &outputBuf) cli := &Cli{ SessionHandler: NewSessionHandler(session), @@ -252,7 +253,7 @@ func testRunMCPWithNonExistentDatabase(t *testing.T) { defer func() { _ = pipeWriter.Close() }() // Create StreamManager with the pipe for input - sysVarsNonExistent.StreamManager = NewStreamManager(pipeReader, &outputBuf, &outputBuf) + sysVarsNonExistent.StreamManager = streamio.NewStreamManager(pipeReader, &outputBuf, &outputBuf) cli, err := NewCli(ctx, nil, &sysVarsNonExistent) if err != nil { @@ -444,7 +445,7 @@ func TestRunMCP(t *testing.T) { StatementTimeout: lo.ToPtr(1 * time.Hour), // Long timeout for integration tests AutoWrap: true, // Set a different value EnableHighlight: true, // Set a different value - StreamManager: NewStreamManager(io.NopCloser(strings.NewReader("")), &outputBuf, &outputBuf), + StreamManager: streamio.NewStreamManager(io.NopCloser(strings.NewReader("")), &outputBuf, &outputBuf), } cli := &Cli{ SessionHandler: NewSessionHandler(session), diff --git a/internal/mycli/integration_meta_command_test.go b/internal/mycli/integration_meta_command_test.go index f18b0231..0f3410dc 100644 --- a/internal/mycli/integration_meta_command_test.go +++ b/internal/mycli/integration_meta_command_test.go @@ -10,12 +10,14 @@ import ( "path/filepath" "strings" "testing" + + "github.com/apstndb/spanner-mycli/internal/mycli/streamio" ) // createTestCli creates a test CLI with the given input, output, and error streams func createTestCli(t *testing.T, ctx context.Context, input io.ReadCloser, output, errOutput io.Writer, sysVars *systemVariables) *Cli { // Create StreamManager with the provided streams - sysVars.StreamManager = NewStreamManager(input, output, errOutput) + sysVars.StreamManager = streamio.NewStreamManager(input, output, errOutput) cli, err := NewCli(ctx, nil, sysVars) if err != nil { @@ -178,7 +180,7 @@ SELECT "foo" AS s;` output := &bytes.Buffer{} // Create StreamManager with the test streams - sysVars.StreamManager = NewStreamManager(io.NopCloser(input), output, output) + sysVars.StreamManager = streamio.NewStreamManager(io.NopCloser(input), output, output) cli := &Cli{ SessionHandler: sessionHandler, @@ -224,7 +226,7 @@ SELECT "foo" AS s;` output := &bytes.Buffer{} // Create StreamManager with the test streams - sysVars.StreamManager = NewStreamManager(io.NopCloser(input), output, output) + sysVars.StreamManager = streamio.NewStreamManager(io.NopCloser(input), output, output) cli := &Cli{ SessionHandler: sessionHandler, @@ -291,7 +293,7 @@ SELECT "foo" AS s;` output := &bytes.Buffer{} // Create StreamManager with the test streams - sysVars.StreamManager = NewStreamManager(io.NopCloser(input), output, output) + sysVars.StreamManager = streamio.NewStreamManager(io.NopCloser(input), output, output) cli := &Cli{ SessionHandler: sessionHandler, @@ -342,7 +344,7 @@ SELECT "foo" AS s;` output := &bytes.Buffer{} // Create StreamManager with the test streams - sysVars.StreamManager = NewStreamManager(io.NopCloser(input), output, output) + sysVars.StreamManager = streamio.NewStreamManager(io.NopCloser(input), output, output) cli := &Cli{ SessionHandler: sessionHandler, @@ -493,7 +495,7 @@ SELECT "foo" AS s;` input := strings.NewReader(commands + "\nexit;\n") // Create StreamManager with the test streams - sysVars.StreamManager = NewStreamManager(io.NopCloser(input), consoleBuf, consoleBuf) + sysVars.StreamManager = streamio.NewStreamManager(io.NopCloser(input), consoleBuf, consoleBuf) cli := &Cli{ SessionHandler: sessionHandler, diff --git a/internal/mycli/integration_source_echo_test.go b/internal/mycli/integration_source_echo_test.go index bbc1feb7..40b5e4b6 100644 --- a/internal/mycli/integration_source_echo_test.go +++ b/internal/mycli/integration_source_echo_test.go @@ -10,6 +10,8 @@ import ( "path/filepath" "strings" "testing" + + "github.com/apstndb/spanner-mycli/internal/mycli/streamio" ) func TestSourceCommandWithEcho(t *testing.T) { @@ -49,7 +51,7 @@ DESCRIBE SELECT * FROM users;` output := &bytes.Buffer{} // Create StreamManager with the test streams - sysVars.StreamManager = NewStreamManager(io.NopCloser(input), output, output) + sysVars.StreamManager = streamio.NewStreamManager(io.NopCloser(input), output, output) cli := &Cli{ SessionHandler: sessionHandler, @@ -117,7 +119,7 @@ DESCRIBE SELECT * FROM users;` output := &bytes.Buffer{} // Create StreamManager with the test streams - sysVars.StreamManager = NewStreamManager(io.NopCloser(input), output, output) + sysVars.StreamManager = streamio.NewStreamManager(io.NopCloser(input), output, output) cli := &Cli{ SessionHandler: sessionHandler, diff --git a/internal/mycli/integration_sql_export_test.go b/internal/mycli/integration_sql_export_test.go index 0e550902..1dc3e54f 100644 --- a/internal/mycli/integration_sql_export_test.go +++ b/internal/mycli/integration_sql_export_test.go @@ -12,6 +12,7 @@ import ( sppb "cloud.google.com/go/spanner/apiv1/spannerpb" "github.com/apstndb/gsqlutils" "github.com/apstndb/spanner-mycli/enums" + "github.com/apstndb/spanner-mycli/internal/mycli/streamio" "github.com/apstndb/spantype/typector" "github.com/apstndb/spanvalue/gcvctor" "github.com/google/go-cmp/cmp" @@ -458,7 +459,7 @@ CREATE TABLE TestTable ( // Create StreamManager and assign it to system variables // The streaming execution will use GetOutputStream() to get the writer - session.systemVariables.StreamManager = NewStreamManager(nil, &buf, &buf) + session.systemVariables.StreamManager = streamio.NewStreamManager(nil, &buf, &buf) // Execute the query - with streaming mode, output goes directly to buffer stmt, err := BuildStatement(tt.query) @@ -669,7 +670,7 @@ func TestSQLExportWithUnnamedColumns(t *testing.T) { // Set up StreamManager for streaming var buf bytes.Buffer - session.systemVariables.StreamManager = NewStreamManager(nil, &buf, &buf) + session.systemVariables.StreamManager = streamio.NewStreamManager(nil, &buf, &buf) query := "SELECT id + 100, CONCAT('Name: ', name) FROM TestTable" stmt, err := BuildStatement(query) @@ -712,7 +713,7 @@ func TestSQLExportWithUnnamedColumns(t *testing.T) { // Set up StreamManager for streaming var buf bytes.Buffer - session.systemVariables.StreamManager = NewStreamManager(nil, &buf, &buf) + session.systemVariables.StreamManager = streamio.NewStreamManager(nil, &buf, &buf) query := "SELECT id AS record_id, name AS customer_name FROM TestTable" stmt, err := BuildStatement(query) @@ -895,7 +896,7 @@ CREATE TABLE TestTable ( // Test buffered mode: format the result using printTableData // This verifies that auto-detected table name is preserved in Result struct var buf bytes.Buffer - session.systemVariables.StreamManager = NewStreamManager(nil, &buf, &buf) + session.systemVariables.StreamManager = streamio.NewStreamManager(nil, &buf, &buf) // Format the buffered result err = printTableData(session.systemVariables, 0, &buf, result) diff --git a/internal/mycli/integration_test.go b/internal/mycli/integration_test.go index 06f331be..116494de 100644 --- a/internal/mycli/integration_test.go +++ b/internal/mycli/integration_test.go @@ -39,6 +39,7 @@ import ( "google.golang.org/grpc/credentials/insecure" "github.com/apstndb/spanner-mycli/enums" + "github.com/apstndb/spanner-mycli/internal/mycli/streamio" "google.golang.org/protobuf/testing/protocmp" @@ -173,7 +174,7 @@ func initializeSession(ctx context.Context, emulator *tcspanner.Container, clien StatementTimeout: lo.ToPtr(1 * time.Hour), // Long timeout for integration tests } // Initialize StreamManager for tests - sysVars.StreamManager = NewStreamManager(io.NopCloser(strings.NewReader("")), io.Discard, io.Discard) + sysVars.StreamManager = streamio.NewStreamManager(io.NopCloser(strings.NewReader("")), io.Discard, io.Discard) // Initialize the registry sysVars.ensureRegistry() session, err = NewSession(ctx, sysVars, options...) @@ -261,7 +262,7 @@ func initializeAdminSession(t *testing.T) (clients *spanemuboost.Clients, sessio StatementTimeout: lo.ToPtr(1 * time.Hour), // Long timeout for integration tests } // Initialize StreamManager for tests - sysVars.StreamManager = NewStreamManager(io.NopCloser(strings.NewReader("")), io.Discard, io.Discard) + sysVars.StreamManager = streamio.NewStreamManager(io.NopCloser(strings.NewReader("")), io.Discard, io.Discard) // Initialize the registry sysVars.ensureRegistry() diff --git a/internal/mycli/meta_commands_test.go b/internal/mycli/meta_commands_test.go index 954472c6..417eb355 100644 --- a/internal/mycli/meta_commands_test.go +++ b/internal/mycli/meta_commands_test.go @@ -7,6 +7,8 @@ import ( "os" "strings" "testing" + + "github.com/apstndb/spanner-mycli/internal/mycli/streamio" ) func TestIsMetaCommand(t *testing.T) { @@ -345,7 +347,7 @@ func TestShellMetaCommand_Execute(t *testing.T) { t.Run("system commands disabled", func(t *testing.T) { sysVars := newSystemVariablesWithDefaults() sysVars.SkipSystemCommand = true - sysVars.StreamManager = NewStreamManager(os.Stdin, io.Discard, io.Discard) + sysVars.StreamManager = streamio.NewStreamManager(os.Stdin, io.Discard, io.Discard) session := &Session{ systemVariables: &sysVars, } @@ -365,7 +367,7 @@ func TestShellMetaCommand_Execute(t *testing.T) { var errOutput bytes.Buffer sysVars := newSystemVariablesWithDefaults() sysVars.SkipSystemCommand = false - sysVars.StreamManager = NewStreamManager(os.Stdin, &output, &errOutput) + sysVars.StreamManager = streamio.NewStreamManager(os.Stdin, &output, &errOutput) session := &Session{ systemVariables: &sysVars, } @@ -389,7 +391,7 @@ func TestShellMetaCommand_Execute(t *testing.T) { var errOutput bytes.Buffer sysVars := newSystemVariablesWithDefaults() sysVars.SkipSystemCommand = false - sysVars.StreamManager = NewStreamManager(os.Stdin, &output, &errOutput) + sysVars.StreamManager = streamio.NewStreamManager(os.Stdin, &output, &errOutput) session := &Session{ systemVariables: &sysVars, } @@ -589,7 +591,7 @@ func createTestSession(t *testing.T) (*Session, *systemVariables) { sysVars := newSystemVariablesWithDefaults() outBuf := &bytes.Buffer{} errBuf := &bytes.Buffer{} - sysVars.StreamManager = NewStreamManager(os.Stdin, outBuf, errBuf) + sysVars.StreamManager = streamio.NewStreamManager(os.Stdin, outBuf, errBuf) session := &Session{ systemVariables: &sysVars, } diff --git a/internal/mycli/screen_width_test.go b/internal/mycli/screen_width_test.go index f8ccc26d..244f305d 100644 --- a/internal/mycli/screen_width_test.go +++ b/internal/mycli/screen_width_test.go @@ -8,6 +8,7 @@ import ( "testing" "github.com/apstndb/spanner-mycli/enums" + "github.com/apstndb/spanner-mycli/internal/mycli/streamio" ) // TestCli_displayResult tests the displayResult method @@ -59,7 +60,7 @@ func TestCli_displayResult(t *testing.T) { AutoWrap: tt.autowrap, FixedWidth: tt.fixedWidth, CLIFormat: enums.DisplayModeTab, // Use TAB format for predictable output - StreamManager: NewStreamManager(io.NopCloser(bytes.NewReader(nil)), outBuf, outBuf), + StreamManager: streamio.NewStreamManager(io.NopCloser(bytes.NewReader(nil)), outBuf, outBuf), } cli := &Cli{ SystemVariables: sysVars, diff --git a/internal/mycli/screen_width_vty_test.go b/internal/mycli/screen_width_vty_test.go index f6b58eb9..5203c0c1 100644 --- a/internal/mycli/screen_width_vty_test.go +++ b/internal/mycli/screen_width_vty_test.go @@ -7,6 +7,7 @@ import ( "testing" "github.com/apstndb/spanner-mycli/enums" + "github.com/apstndb/spanner-mycli/internal/mycli/streamio" "github.com/creack/pty" ) @@ -159,7 +160,7 @@ func TestDisplayResultWithPty(t *testing.T) { // Create a Cli with our system variables and the pseudoterminal as output sysVars := &systemVariables{ - StreamManager: NewStreamManager(io.NopCloser(bytes.NewReader(nil)), tty, os.Stderr), + StreamManager: streamio.NewStreamManager(io.NopCloser(bytes.NewReader(nil)), tty, os.Stderr), AutoWrap: tt.autowrap, FixedWidth: tt.fixedWidth, CLIFormat: enums.DisplayModeTab, // Use TAB format for predictable output diff --git a/internal/mycli/session.go b/internal/mycli/session.go index 4b7c91e1..b652caff 100644 --- a/internal/mycli/session.go +++ b/internal/mycli/session.go @@ -32,6 +32,7 @@ import ( "github.com/apstndb/adcplus" "github.com/apstndb/adcplus/tokensource" "github.com/apstndb/go-grpcinterceptors/selectlogging" + "github.com/apstndb/spanner-mycli/internal/mycli/decoder" "github.com/gocql/gocql" "github.com/samber/lo" "google.golang.org/api/iterator" @@ -1161,7 +1162,7 @@ func (s *Session) runAnalyzeQueryOnTransaction(ctx context.Context, tx transacti // Using non-locked versions of methods like TransactionAttrs(), CurrentPriority(), // or QueryOptions() here will cause a deadlock. Always use the *Locked variants. func (s *Session) runUpdateOnTransaction(ctx context.Context, tx *spanner.ReadWriteStmtBasedTransaction, stmt spanner.Statement, implicit bool) (*UpdateResult, error) { - fc, err := formatConfigWithProto(s.systemVariables.ProtoDescriptor, s.systemVariables.MultilineProtoText) + fc, err := decoder.FormatConfigWithProto(s.systemVariables.ProtoDescriptor, s.systemVariables.MultilineProtoText) if err != nil { return nil, err } @@ -1342,7 +1343,7 @@ func (s *Session) runSingleUseQuery(ctx context.Context, stmt spanner.Statement, func (s *Session) RunUpdate(ctx context.Context, stmt spanner.Statement, implicit bool) ([]Row, map[string]any, int64, *sppb.ResultSetMetadata, *sppb.QueryPlan, error, ) { - fc, err := formatConfigWithProto(s.systemVariables.ProtoDescriptor, s.systemVariables.MultilineProtoText) + fc, err := decoder.FormatConfigWithProto(s.systemVariables.ProtoDescriptor, s.systemVariables.MultilineProtoText) if err != nil { return nil, nil, 0, nil, nil, err } diff --git a/internal/mycli/statement_processing.go b/internal/mycli/statement_processing.go index 1b1e3fdf..c297fea0 100644 --- a/internal/mycli/statement_processing.go +++ b/internal/mycli/statement_processing.go @@ -228,6 +228,14 @@ type Result struct { // Row is a row of string values. It is a type alias for format.Row (= []string). type Row = format.Row +func toRow(vs ...string) Row { + return vs +} + +func sliceOf[V any](vs ...V) []V { + return vs +} + // QueryStats contains query statistics. // Some fields may not have a valid value depending on the environment. // For example, only ElapsedTime and RowsReturned has valid value for Cloud Spanner Emulator. @@ -390,7 +398,7 @@ func BuildNativeStatementMemefish(stripped, raw string) (Statement, error) { return &SelectStatement{Query: raw}, nil case kind.IsDDL(): // Only CREATE DATABASE needs special treatment in DDL. - if instanceOf[*ast.CreateDatabase](stmt) { + if _, ok := stmt.(*ast.CreateDatabase); ok { return &CreateDatabaseStatement{CreateStatement: raw}, nil } diff --git a/internal/mycli/statements_explain_describe.go b/internal/mycli/statements_explain_describe.go index 50217aa7..a84facd4 100644 --- a/internal/mycli/statements_explain_describe.go +++ b/internal/mycli/statements_explain_describe.go @@ -27,6 +27,7 @@ import ( sppb "cloud.google.com/go/spanner/apiv1/spannerpb" "github.com/apstndb/lox" "github.com/apstndb/spanner-mycli/enums" + "github.com/apstndb/spanner-mycli/internal/mycli/decoder" "github.com/apstndb/spannerplan" "github.com/apstndb/spannerplan/plantree" "github.com/apstndb/spannerplan/protoyaml" @@ -214,7 +215,7 @@ func (s *DescribeStatement) Execute(ctx context.Context, session *Session) (*Res var rows []Row for _, field := range metadata.GetRowType().GetFields() { - rows = append(rows, toRow(field.GetName(), formatTypeVerbose(field.GetType()))) + rows = append(rows, toRow(field.GetName(), decoder.FormatTypeVerbose(field.GetType()))) } result := &Result{ diff --git a/internal/mycli/statements_params.go b/internal/mycli/statements_params.go index 585d84fd..bdb73fd2 100644 --- a/internal/mycli/statements_params.go +++ b/internal/mycli/statements_params.go @@ -1,6 +1,7 @@ package mycli import ( + "cmp" "context" "maps" "slices" @@ -26,7 +27,7 @@ func (s *ShowParamsStatement) Execute(ctx context.Context, session *Session) (*R scxiter.MapLower(maps.All(session.systemVariables.Params), func(k string, v ast.Node) Row { return toRow(k, lo.Ternary(lox.InstanceOf[ast.Type](v), "TYPE", "VALUE"), v.SQL()) }), - ToSortFunc(func(r Row) string { return r[0] /* parameter name */ })) + func(lhs, rhs Row) int { return cmp.Compare(lhs[0], rhs[0]) /* parameter name */ }) return &Result{ TableHeader: toTableHeader("Param_Name", "Param_Kind", "Param_Value"), diff --git a/internal/mycli/statements_schema.go b/internal/mycli/statements_schema.go index ead2b183..343949b3 100644 --- a/internal/mycli/statements_schema.go +++ b/internal/mycli/statements_schema.go @@ -10,6 +10,7 @@ import ( "cloud.google.com/go/spanner" "cloud.google.com/go/spanner/admin/database/apiv1/databasepb" "github.com/apstndb/lox" + "github.com/apstndb/spanner-mycli/internal/mycli/decoder" "github.com/ngicks/go-iterator-helper/hiter" "github.com/ngicks/go-iterator-helper/hiter/stringsiter" "github.com/samber/lo" @@ -159,7 +160,7 @@ func executeInformationSchemaBasedStatementImpl(ctx context.Context, session *Se return nil, fmt.Errorf(`%q can not be used in a read-write transaction`, stmtName) } - fc, err := formatConfigWithProto(session.systemVariables.ProtoDescriptor, session.systemVariables.MultilineProtoText) + fc, err := decoder.FormatConfigWithProto(session.systemVariables.ProtoDescriptor, session.systemVariables.MultilineProtoText) if err != nil { return nil, err } diff --git a/internal/mycli/statements_system_variable.go b/internal/mycli/statements_system_variable.go index dfc422b0..6faa727d 100644 --- a/internal/mycli/statements_system_variable.go +++ b/internal/mycli/statements_system_variable.go @@ -1,6 +1,7 @@ package mycli import ( + "cmp" "context" "fmt" "maps" @@ -65,7 +66,7 @@ func (s *ShowVariablesStatement) Execute(ctx context.Context, session *Session) rows := slices.SortedFunc( scxiter.MapLower(maps.All(merged), func(k, v string) Row { return toRow(k, v) }), - ToSortFunc(func(r Row) string { return r[0] /* name */ })) + func(lhs, rhs Row) int { return cmp.Compare(lhs[0], rhs[0]) /* name */ }) return &Result{ TableHeader: toTableHeader("name", "value"), diff --git a/internal/mycli/stream_manager.go b/internal/mycli/streamio/stream_manager.go similarity index 99% rename from internal/mycli/stream_manager.go rename to internal/mycli/streamio/stream_manager.go index 995cddb4..7af93302 100644 --- a/internal/mycli/stream_manager.go +++ b/internal/mycli/streamio/stream_manager.go @@ -1,6 +1,6 @@ // StreamManager manages all I/O streams for the CLI. // All methods are safe for concurrent use. -package mycli +package streamio import ( "fmt" diff --git a/internal/mycli/stream_manager_edge_test.go b/internal/mycli/streamio/stream_manager_edge_test.go similarity index 99% rename from internal/mycli/stream_manager_edge_test.go rename to internal/mycli/streamio/stream_manager_edge_test.go index 0c14adf1..46f3f8dd 100644 --- a/internal/mycli/stream_manager_edge_test.go +++ b/internal/mycli/streamio/stream_manager_edge_test.go @@ -1,4 +1,4 @@ -package mycli +package streamio import ( "bytes" diff --git a/internal/mycli/stream_manager_fifo_test.go b/internal/mycli/streamio/stream_manager_fifo_test.go similarity index 99% rename from internal/mycli/stream_manager_fifo_test.go rename to internal/mycli/streamio/stream_manager_fifo_test.go index b77b70f6..8277475a 100644 --- a/internal/mycli/stream_manager_fifo_test.go +++ b/internal/mycli/streamio/stream_manager_fifo_test.go @@ -1,4 +1,4 @@ -package mycli +package streamio import ( "os" diff --git a/internal/mycli/stream_manager_terminal_test.go b/internal/mycli/streamio/stream_manager_terminal_test.go similarity index 99% rename from internal/mycli/stream_manager_terminal_test.go rename to internal/mycli/streamio/stream_manager_terminal_test.go index 29be1203..724e1ab0 100644 --- a/internal/mycli/stream_manager_terminal_test.go +++ b/internal/mycli/streamio/stream_manager_terminal_test.go @@ -1,4 +1,4 @@ -package mycli +package streamio import ( "bytes" diff --git a/internal/mycli/stream_manager_test.go b/internal/mycli/streamio/stream_manager_test.go similarity index 99% rename from internal/mycli/stream_manager_test.go rename to internal/mycli/streamio/stream_manager_test.go index ef570dba..2fc2445b 100644 --- a/internal/mycli/stream_manager_test.go +++ b/internal/mycli/streamio/stream_manager_test.go @@ -1,4 +1,4 @@ -package mycli +package streamio import ( "bytes" diff --git a/internal/mycli/system_variables.go b/internal/mycli/system_variables.go index 453447fc..22749587 100644 --- a/internal/mycli/system_variables.go +++ b/internal/mycli/system_variables.go @@ -35,6 +35,8 @@ import ( "cloud.google.com/go/spanner" sppb "cloud.google.com/go/spanner/apiv1/spannerpb" + + "github.com/apstndb/spanner-mycli/internal/mycli/streamio" ) type LastQueryCache struct { @@ -139,7 +141,7 @@ type systemVariables struct { // Use StreamManager.GetTtyStream() instead. // StreamManager manages tee output functionality - StreamManager *StreamManager + StreamManager *streamio.StreamManager EnableProgressBar bool // CLI_ENABLE_PROGRESS_BAR ImpersonateServiceAccount string // CLI_IMPERSONATE_SERVICE_ACCOUNT @@ -218,6 +220,18 @@ func parseEndpoint(endpoint string) (host string, port int, err error) { var errIgnored = errors.New("ignored") +func projectPath(projectID string) string { + return fmt.Sprintf("projects/%v", projectID) +} + +func instancePath(projectID, instanceID string) string { + return fmt.Sprintf("projects/%v/instances/%v", projectID, instanceID) +} + +func databasePath(projectID, instanceID, databaseID string) string { + return fmt.Sprintf("projects/%v/instances/%v/databases/%v", projectID, instanceID, databaseID) +} + func (sv *systemVariables) InstancePath() string { return instancePath(sv.Project, sv.Instance) } diff --git a/internal/mycli/util.go b/internal/mycli/util.go deleted file mode 100644 index 40ae2482..00000000 --- a/internal/mycli/util.go +++ /dev/null @@ -1,46 +0,0 @@ -package mycli - -import ( - "fmt" - - "golang.org/x/exp/constraints" -) - -func projectPath(projectID string) string { - return fmt.Sprintf("projects/%v", projectID) -} - -func instancePath(projectID, instanceID string) string { - return fmt.Sprintf("projects/%v/instances/%v", projectID, instanceID) -} - -func databasePath(projectID, instanceID, databaseID string) string { - return fmt.Sprintf("projects/%v/instances/%v/databases/%v", projectID, instanceID, databaseID) -} - -func instanceOf[T any](v any) bool { - _, ok := v.(T) - return ok -} - -func ToSortFunc[T any, R constraints.Ordered](f func(T) R) func(T, T) int { - return func(lhs T, rhs T) int { - l, r := f(lhs), f(rhs) - switch { - case l < r: - return -1 - case l > r: - return 1 - default: - return 0 - } - } -} - -func toRow(vs ...string) Row { - return vs -} - -func sliceOf[V any](vs ...V) []V { - return vs -}