From 2db6efb691acb1d2ea432647d21f9c5d0cf50c01 Mon Sep 17 00:00:00 2001 From: HAYAMA Kaoru <3752189+hymkor@users.noreply.github.com> Date: Tue, 12 May 2026 14:47:15 +0900 Subject: [PATCH 1/5] "dialect": Add `FormatValue` hook to `dialect.Entry` for database-specific value formatting --- dialect/main.go | 4 ++++ dialect/oracle/main.go | 21 +++++++++++++++++---- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/dialect/main.go b/dialect/main.go index 6ae63ed..8e15a0d 100644 --- a/dialect/main.go +++ b/dialect/main.go @@ -56,6 +56,10 @@ type Entry struct { // IdentifierEncloser encloses an identifier with dialect-specific quotes. IdentifierEncloser func(name string) string + + // FormatValue converts a database value into a dialect-specific string representation. + // Returning false means the default conversion should be used instead. + FormatValue func(typeName string, value any) (string, bool) } // EncloseIdentifier returns the given name enclosed with diff --git a/dialect/oracle/main.go b/dialect/oracle/main.go index 8f919f3..9870153 100644 --- a/dialect/oracle/main.go +++ b/dialect/oracle/main.go @@ -4,6 +4,7 @@ import ( "database/sql" "fmt" "strings" + "time" _ "github.com/sijms/go-ora/v2" @@ -33,6 +34,18 @@ var oracleSpec = &dialect.Entry{ TableNameField: "tname", ColumnNameField: "name", PlaceHolder: new(placeHolder), + FormatValue: formatValue, +} + +func formatValue(typeName string, value any) (string, bool) { + t, ok := value.(time.Time) + if !ok { + return "", false + } + if typeName == "DATE" { + return t.Format("2006-01-02 15:04:05"), true + } + return t.Format("2006-01-02 15:04:05.999999"), true } type withFormat struct { @@ -45,11 +58,11 @@ func oracleTypeNameToConv(typeName string) func(string) (any, error) { var layout string if typeName == "DATE" { - format = "TO_DATE(:v%d,'YYYY/MM/DD HH24:MI:SS')" - layout = "2006/01/02 15:04:05" + format = "TO_DATE(:v%d,'YYYY-MM-DD HH24:MI:SS')" + layout = "2006-01-02 15:04:05" } else if strings.HasPrefix(typeName, "TIMESTAMP") { - format = "TO_TIMESTAMP(:v%d,'YYYY/MM/DD HH24:MI:SS.FF')" - layout = "2006/01/02 15:04:05.999999" + format = "TO_TIMESTAMP(:v%d,'YYYY-MM-DD HH24:MI:SS.FF')" + layout = "2006-01-02 15:04:05.999999" } else { return nil } From d5d22019719bea5344d16152d54f888f75b36bff Mon Sep 17 00:00:00 2001 From: HAYAMA Kaoru <3752189+hymkor@users.noreply.github.com> Date: Tue, 12 May 2026 14:59:32 +0900 Subject: [PATCH 2/5] "rowstocsv": Improve CSV conversion hook API --- rowstocsv/main.go | 52 ++++++++++++++++++++--------------------------- 1 file changed, 22 insertions(+), 30 deletions(-) diff --git a/rowstocsv/main.go b/rowstocsv/main.go index aa7da98..af0bd5a 100644 --- a/rowstocsv/main.go +++ b/rowstocsv/main.go @@ -19,19 +19,15 @@ type Source interface { Scan(dest ...any) error } -func anyToNullString(v any) sql.NullString { - var ns sql.NullString +func anyToNullString(v any) (string, bool) { if stamp, ok := v.(time.Time); ok { - ns.String = stamp.Format("2006-01-02 15:04:05.999999999 -07:00") - ns.Valid = true + return stamp.Format("2006-01-02 15:04:05.999999999 -07:00"), true } else if b, ok := v.([]byte); ok { - ns.String = string(b) - ns.Valid = true + return string(b), true } else if v != nil { - ns.String = fmt.Sprint(v) - ns.Valid = true + return fmt.Sprint(v), true } - return ns + return "", false } func makeBuffers[T any](n int) ([]any, []T) { @@ -43,7 +39,7 @@ func makeBuffers[T any](n int) ([]any, []T) { return refs, data } -func dump(ctx context.Context, rows Source, conv func(int, *sql.ColumnType, sql.NullString) string, debug bool, write func([]string) error) error { +func dump(ctx context.Context, rows Source, conv func(int, *sql.ColumnType, any) (string, bool), null string, debug bool, write func([]string) error) error { columns, err := rows.Columns() if err != nil { return fmt.Errorf("(sql.Rows) Columns: %w", err) @@ -55,7 +51,6 @@ func dump(ctx context.Context, rows Source, conv func(int, *sql.ColumnType, sql. n := len(columns) refs, data := makeBuffers[any](n) - //refs, data := makeBuffers[sql.RawBytes](n) strs := make([]string, len(columns)) columnTypes, err := rows.ColumnTypes() @@ -83,17 +78,25 @@ func dump(ctx context.Context, rows Source, conv func(int, *sql.ColumnType, sql. } for rows.Next() { - select { - case <-ctx.Done(): - return ctx.Err() - default: + if err := ctx.Err(); err != nil { + return err } if err := rows.Scan(refs...); err != nil { return err } for i, v := range data { - ns := anyToNullString(v) - strs[i] = conv(i, columnTypes[i], ns) + if conv != nil { + s, ok := conv(i, columnTypes[i], v) + if ok { + strs[i] = s + continue + } + } + if s, ok := anyToNullString(v); ok { + strs[i] = s + } else { + strs[i] = null + } } if err := write(strs); err != nil { return fmt.Errorf("(csv.Writer).Write: %w", err) @@ -110,17 +113,10 @@ type Config struct { UseCRLF bool Null string Debug bool - Conv func(int, *sql.ColumnType, sql.NullString) string + Conv func(int, *sql.ColumnType, any) (string, bool) AutoClose bool } -func (cfg Config) defaultConv(_ int, _ *sql.ColumnType, v sql.NullString) string { - if v.Valid { - return v.String - } - return cfg.Null -} - func (cfg Config) Dump(ctx context.Context, rows Source, w io.Writer) error { csvw := csv.NewWriter(w) defer csvw.Flush() @@ -128,12 +124,8 @@ func (cfg Config) Dump(ctx context.Context, rows Source, w io.Writer) error { csvw.Comma = cfg.Comma csvw.UseCRLF = cfg.UseCRLF - conv := cfg.defaultConv - if cfg.Conv != nil { - conv = cfg.Conv - } if cfg.AutoClose { defer rows.Close() } - return dump(ctx, rows, conv, cfg.Debug, csvw.Write) + return dump(ctx, rows, cfg.Conv, cfg.Null, cfg.Debug, csvw.Write) } From be52aa691814a495c89b7465bcc37a3d70f360d8 Mon Sep 17 00:00:00 2001 From: HAYAMA Kaoru <3752189+hymkor@users.noreply.github.com> Date: Tue, 12 May 2026 14:59:32 +0900 Subject: [PATCH 3/5] "spread": Use database-specific cell formatting for `edit` and `select` --- edit.go | 5 +++-- spread/edit.go | 7 ++++++- spread/view.go | 9 +++++++++ 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/edit.go b/edit.go index 759c006..0c7ca0d 100644 --- a/edit.go +++ b/edit.go @@ -51,6 +51,7 @@ func newViewer(ss *session) *spread.Viewer { hl = 1 } return &spread.Viewer{ + Entry: ss.Dialect, HeaderLines: hl, Comma: ss.comma(), Null: ss.Null, @@ -80,12 +81,12 @@ func chooseTable(ctx context.Context, tables []string, d *dialect.Entry, ttyout func doEdit(ctx context.Context, ss *session, command string, pilot commandIn) error { editor := &spread.Editor{ Viewer: &spread.Viewer{ + Entry: ss.Dialect, HeaderLines: 1, Comma: ss.comma(), Null: ss.Null, }, - Entry: ss.Dialect, - Exec: (&askSqlAndExecute{getKey: pilot.GetKey, session: ss}).Exec, + Exec: (&askSqlAndExecute{getKey: pilot.GetKey, session: ss}).Exec, } if a, ok := pilot.AutoPilotForCsvi(); ok { editor.Pilot = misc.AutoCsvi{GetKeyAndSize: a} diff --git a/spread/edit.go b/spread/edit.go index d60e244..95aea0f 100644 --- a/spread/edit.go +++ b/spread/edit.go @@ -93,7 +93,6 @@ func doubleQuoteIfNeed(s string) string { type Editor struct { *Viewer - *dialect.Entry Query func(context.Context, string, ...any) (*sql.Rows, error) Exec func(context.Context, string, ...any) (sql.Result, error) } @@ -195,6 +194,12 @@ func (editor *Editor) Edit(ctx context.Context, tableAndWhere string, termOut io Null: editor.Viewer.Null, Comma: rune(editor.Viewer.Comma), AutoClose: true, + Conv: func(_ int, ct *sql.ColumnType, v any) (string, bool) { + if f := editor.Entry.FormatValue; f != nil { + return f(ct.DatabaseTypeName(), v) + } + return "", false + }, }.Dump(ctx, rows, w) rows = nil return err diff --git a/spread/view.go b/spread/view.go index fce7c7e..47153b9 100644 --- a/spread/view.go +++ b/spread/view.go @@ -2,12 +2,14 @@ package spread import ( "context" + "database/sql" "errors" "io" "strings" "github.com/hymkor/csvi" "github.com/hymkor/csvi/uncsv" + "github.com/hymkor/sqlbless/dialect" "github.com/hymkor/sqlbless/rowstocsv" ) @@ -17,6 +19,7 @@ type KeyBinding struct { } type Viewer struct { + *dialect.Entry HeaderLines int Comma byte Null string @@ -41,6 +44,12 @@ func (viewer *Viewer) View(ctx context.Context, title string, rows rowstocsv.Sou Null: viewer.Null, Comma: rune(viewer.Comma), AutoClose: true, + Conv: func(_ int, ct *sql.ColumnType, v any) (string, bool) { + if f := viewer.Entry.FormatValue; f != nil { + return f(ct.DatabaseTypeName(), v) + } + return "", false + }, }.Dump(ctx, rows, w) } From 290a7c87254862edcac17bcb38f355a4c5886743 Mon Sep 17 00:00:00 2001 From: HAYAMA Kaoru <3752189+hymkor@users.noreply.github.com> Date: Tue, 12 May 2026 17:20:39 +0900 Subject: [PATCH 4/5] test/test-oracle.ps1: Expect timezone-less Oracle datetime output --- test/test-oracle.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/test-oracle.ps1 b/test/test-oracle.ps1 index 8e145d4..edcc004 100644 --- a/test/test-oracle.ps1 +++ b/test/test-oracle.ps1 @@ -53,11 +53,11 @@ ForEach-Object { Write-Host $field.Length return } - if ( $field[1] -notlike "2015-06-07 20:21:22*" ){ + if ( $field[1] -ne "2015-06-07 20:21:22" ){ Write-Host $field[1] return } - if ( $field[2] -notlike "2024-08-09 10:11:12.7878*" ){ + if ( $field[2] -ne "2024-08-09 10:11:12.7878" ){ Write-Host $field[2] return } From 37cb2fce4f4ff0a711d1fb9797080210991622c9 Mon Sep 17 00:00:00 2001 From: HAYAMA Kaoru <3752189+hymkor@users.noreply.github.com> Date: Tue, 12 May 2026 18:20:56 +0900 Subject: [PATCH 5/5] Update CHANGELOG (#57) --- CHANGELOG.md | 1 + CHANGELOG_ja.md | 1 + 2 files changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 33ec8e1..543c128 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ Changelog (English) - Upgrade Microsoft SQL Server driver to v1.10.0 - Upgrade PostgreSQL driver to v1.12.3 - Oracle datetime values are now passed as strings and converted using `TO_DATE`/`TO_TIMESTAMP` to avoid timezone-related comparison issues in sijms/go-ora. (#55) +- Oracle DATE/TIMESTAMP values are now displayed without timezone suffixes. (#57) v0.27.6 ------- diff --git a/CHANGELOG_ja.md b/CHANGELOG_ja.md index 29e3b00..2df1478 100644 --- a/CHANGELOG_ja.md +++ b/CHANGELOG_ja.md @@ -8,6 +8,7 @@ Changelog (Japanese) - Upgrade Microsoft SQL Server driver to v1.10.0 - Upgrade PostgreSQL driver to v1.12.3 - sijms/go-ora でのタイムゾーンに関する比較問題を回避するため、Oracle の日時値を文字列として引き渡し、`TO_DATE`/`TO_TIMESTAMP` を使ってコンバートするようにした (#55) +- Oracle の DATE/TIMESTAMP 値はタイムゾーンなしで表示するようにした (#57) v0.27.6 -------