Skip to content

Conversation

@noborus
Copy link
Owner

@noborus noborus commented Nov 19, 2023

Added output file name and output sheet name to output to xlsx.
If an output file name with an xlsx extension is specified, an xlsx file will be output.

Summary by CodeRabbit

  • New Features

    • Introduced the ability to output query results directly into XLSX files.
    • Added command-line flags for specifying output file names and sheet names for XLSX.
    • Implemented functionality to clear existing data from a sheet in an XLSX file before writing new data.
    • Enhanced output format detection with a new default "GUESS" mode to determine the appropriate format based on the provided file extension.
  • Enhancements

    • Expanded output handling to support more complex operations with XLSX files, including setting options and writing data.
  • Bug Fixes

    • Adjusted the output format default value to improve user experience when no specific format is provided.

Added output file name and output sheet name to output to xlsx.
If an output file name with an xlsx extension is specified,
an xlsx file will be output.
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 19, 2023

Walkthrough

The update enhances a command-line tool to support XLSX file output. It introduces new writer types and functions for handling XLSX formats, along with additional CLI flags for file and sheet management. The default output format is now inferred, and the tool can clear existing sheets in XLSX files before writing new data.

Changes

File(s) Change Summary
cmd/query.go Added support for XLSX file output, new global variables for file and sheet handling, and changed default output format to "GUESS".
cmd/root.go Introduced new global variables for file and sheet handling, and changed default output format to "GUESS".
writer.go, writer_test.go Introduced XLSXWriter type with methods for XLSX file operations, added WriteOpts for configurable writing options, and included a test function for XLSXWriter in writer_test.go.

🐇🍂 "In the code's autumn, new leaves unfurl,
Sheets clear as the CLI takes a twirl.
XLSX support, a new tale to weave,
With rows and columns on crisp fall eves." 📊🍁


Tips

Chat with CodeRabbit Bot (@coderabbitai)

  • If you reply to a review comment from CodeRabbit, the bot will automatically respond.
  • To engage with CodeRabbit bot directly around the specific lines of code in the PR, mention @coderabbitai in your review comment
  • Note: Review comments are made on code diffs or files, not on the PR overview.
  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.

CodeRabbit Commands (invoked as PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger a review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai help to get help.

Note: For conversation with the bot, please use the review comments on code diffs or files.

CodeRabbit Configration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • The JSON schema for the configuration file is available here.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/coderabbit-overrides.v2.json

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review Status

Actionable comments generated: 6

Configuration used: CodeRabbit UI

Commits Files that changed from the base of the PR and between 1dc91ee and 4fb376a.
Files selected for processing (3)
  • cmd/query.go (2 hunks)
  • cmd/root.go (1 hunks)
  • writer.go (1 hunks)
Additional comments: 6
cmd/root.go (1)
  • 59-76: The introduction of new global variables and corresponding flags for handling XLSX output is a significant enhancement. However, it's important to ensure that these variables are used consistently throughout the codebase and that their introduction doesn't cause any side effects or conflicts with existing functionality.
  • The OutFormat default value change from "CSV" to "GUESS" could potentially affect existing users who rely on the default behavior. It's important to communicate this change clearly in the release notes or documentation to avoid confusion.
  • The new flags for specifying the output file name and sheet name (--out-xlsx and --out-sheet) are clear and align with the naming convention of other flags.
  • The --clear-sheet flag is a useful addition for users who need to clear the sheet before outputting new data.

Ensure that the new flags are documented and that any necessary error handling is in place for scenarios where the user might provide invalid file names or sheet names.

cmd/query.go (4)
  • 3-13: The new imports introduced here are necessary for the added functionality. Ensure that the xlsxsql package is properly documented and maintained, as it is critical for the new feature.

  • 17-33: The exec function has been updated to use the new setWriter function to determine the appropriate writer based on the output format. This is a logical change that aligns with the new feature of supporting XLSX output.

  • 62-79: The stdWriter function is well-implemented to handle non-XLSX formats. It correctly checks if a file name is provided and creates a file or defaults to os.Stdout. The use of trdsql.OutputFormat to parse the OutFormat is also appropriate.

  • 17-84: > Note: This review was outside of the patch, so it was mapped to the patch with the greatest overlap. Original lines [82-90]

The queryCmd command documentation has been updated to reflect the new functionality. Ensure that the help text and long description are clear and informative for the end-user.

writer.go (1)
  • 3-9: The imports are correctly organized, with standard library imports followed by third-party packages. Ensure that the github.com/noborus/trdsql and github.com/xuri/excelize/v2 packages are compatible with the rest of the project and are up-to-date.

Comment on lines 36 to 60
func setWriter(fileName string) (trdsql.Writer, error) {
dotExt := strings.TrimLeft(filepath.Ext(fileName), ".")
if OutFormat == "GUESS" && dotExt != "" {
OutFormat = strings.ToUpper(dotExt)
}

if strings.ToUpper(OutFormat) != "XLSX" {
return stdWriter(fileName)
}

// XLSX Writer
if fileName == "" {
return nil, fmt.Errorf("file name is required to output with XLSX")
}

if OutSheetName == "" {
OutSheetName = "Sheet1"
}
writer, err := xlsxsql.NewXLSXWriter(os.Stdout, OutFileName, OutSheetName, ClearSheet)
if err != nil {
return nil, err
}

return writer, nil
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The setWriter function correctly handles the logic to determine whether to use a standard writer or the new xlsxsql writer based on the OutFormat. However, the error message on line 48 should be more descriptive to help users understand that the file name is required specifically for XLSX output.

- return nil, fmt.Errorf("file name is required to output with XLSX")
+ return nil, fmt.Errorf("an output file name must be provided when exporting to XLSX format")

Commitable suggestion

[!IMPORTANT]
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation.

Suggested change
func setWriter(fileName string) (trdsql.Writer, error) {
dotExt := strings.TrimLeft(filepath.Ext(fileName), ".")
if OutFormat == "GUESS" && dotExt != "" {
OutFormat = strings.ToUpper(dotExt)
}
if strings.ToUpper(OutFormat) != "XLSX" {
return stdWriter(fileName)
}
// XLSX Writer
if fileName == "" {
return nil, fmt.Errorf("file name is required to output with XLSX")
}
if OutSheetName == "" {
OutSheetName = "Sheet1"
}
writer, err := xlsxsql.NewXLSXWriter(os.Stdout, OutFileName, OutSheetName, ClearSheet)
if err != nil {
return nil, err
}
return writer, nil
}
func setWriter(fileName string) (trdsql.Writer, error) {
dotExt := strings.TrimLeft(filepath.Ext(fileName), ".")
if OutFormat == "GUESS" && dotExt != "" {
OutFormat = strings.ToUpper(dotExt)
}
if strings.ToUpper(OutFormat) != "XLSX" {
return stdWriter(fileName)
}
// XLSX Writer
if fileName == "" {
return nil, fmt.Errorf("an output file name must be provided when exporting to XLSX format")
}
if OutSheetName == "" {
OutSheetName = "Sheet1"
}
writer, err := xlsxsql.NewXLSXWriter(os.Stdout, OutFileName, OutSheetName, ClearSheet)
if err != nil {
return nil, err
}
return writer, nil
}

writer.go Outdated
Comment on lines 19 to 34
func clear(f *excelize.File, sheet string) error {
rows, err := f.GetRows(sheet)
if err != nil {
return err
}

for i, row := range rows {
for j := range row {
axis, err := excelize.CoordinatesToCellName(j+1, i+1)
if err != nil {
return err
}
f.SetCellStr(sheet, axis, "")
}
}
return nil
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The clear function iterates over all cells in a sheet and sets their value to an empty string. This is a potentially expensive operation for large sheets and could be optimized. Consider if the library provides a method to clear an entire sheet at once, which would be more efficient.

writer.go Outdated
Comment on lines 37 to 77
// NewXLSXWriter function takes an io.Writer and trdsql.WriteOpts, and returns a new XLSXWriter.
func NewXLSXWriter(writer io.Writer, fileName string, sheet string, clearSheet bool) (trdsql.Writer, error) {
var f *excelize.File
var err error

if _, err = os.Stat(fileName); os.IsNotExist(err) {
// File does not exist, create a new one
f = excelize.NewFile()
} else {
// File exists, open it
f, err = excelize.OpenFile(fileName)
if err != nil {
return nil, err
}
}

n, err := f.GetSheetIndex(sheet)
if err != nil {
return nil, err
}
if clearSheet {
if n >= 0 {
// Sheet exists,clear it
if err := clear(f, sheet); err != nil {
return nil, err
}
}
}
if n < 0 {
// Sheet does not exist, create a new one
if _, err := f.NewSheet(sheet); err != nil {
return nil, err
}
}

return &XLSXWriter{
fileName: fileName,
f: f,
sheet: sheet,
}, nil
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The NewXLSXWriter function correctly handles the creation of a new file or opening an existing one. It also checks if the sheet exists and clears it if required. However, there is a potential issue with the error handling in lines 42-50. If os.Stat fails for reasons other than the file not existing (e.g., permission issues), the error is not handled, and the function proceeds to create a new file. This could lead to unexpected behavior.

+   if err != nil && !os.IsNotExist(err) {
+       return nil, err
+   }
+   if os.IsNotExist(err) {

Commitable suggestion

[!IMPORTANT]
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation.

Suggested change
// NewXLSXWriter function takes an io.Writer and trdsql.WriteOpts, and returns a new XLSXWriter.
func NewXLSXWriter(writer io.Writer, fileName string, sheet string, clearSheet bool) (trdsql.Writer, error) {
var f *excelize.File
var err error
if _, err = os.Stat(fileName); os.IsNotExist(err) {
// File does not exist, create a new one
f = excelize.NewFile()
} else {
// File exists, open it
f, err = excelize.OpenFile(fileName)
if err != nil {
return nil, err
}
}
n, err := f.GetSheetIndex(sheet)
if err != nil {
return nil, err
}
if clearSheet {
if n >= 0 {
// Sheet exists,clear it
if err := clear(f, sheet); err != nil {
return nil, err
}
}
}
if n < 0 {
// Sheet does not exist, create a new one
if _, err := f.NewSheet(sheet); err != nil {
return nil, err
}
}
return &XLSXWriter{
fileName: fileName,
f: f,
sheet: sheet,
}, nil
}
func NewXLSXWriter(writer io.Writer, fileName string, sheet string, clearSheet bool) (trdsql.Writer, error) {
var f *excelize.File
var err error
if _, err = os.Stat(fileName); err != nil && !os.IsNotExist(err) {
return nil, err
}
if os.IsNotExist(err) {
// File does not exist, create a new one
f = excelize.NewFile()
} else {
// File exists, open it
f, err = excelize.OpenFile(fileName)
if err != nil {
return nil, err
}
}
n, err := f.GetSheetIndex(sheet)
if err != nil {
return nil, err
}
if clearSheet {
if n >= 0 {
// Sheet exists,clear it
if err := clear(f, sheet); err != nil {
return nil, err
}
}
}
if n < 0 {
// Sheet does not exist, create a new one
if _, err := f.NewSheet(sheet); err != nil {
return nil, err
}
}
return &XLSXWriter{
fileName: fileName,
f: f,
sheet: sheet,
}, nil
}

Comment on lines 80 to 89
func (w *XLSXWriter) PreWrite(columns []string, types []string) error {
for i, v := range columns {
cell, error := excelize.CoordinatesToCellName(i+1, 1)
if error != nil {
return error
}
w.f.SetCellValue(w.sheet, cell, v)
}
w.rowID++
return nil
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PreWrite method sets up the column headers in the first row of the sheet. The variable error should be named err to follow Go's convention. Also, incrementing w.rowID here assumes that headers are always written, which is a valid assumption, but it should be documented or checked.

-  if error != nil {
-      return error
+  cell, err := excelize.CoordinatesToCellName(i+1, 1)
+  if err != nil {
+      return err

Commitable suggestion

[!IMPORTANT]
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation.

Suggested change
func (w *XLSXWriter) PreWrite(columns []string, types []string) error {
for i, v := range columns {
cell, error := excelize.CoordinatesToCellName(i+1, 1)
if error != nil {
return error
}
w.f.SetCellValue(w.sheet, cell, v)
}
w.rowID++
return nil
func (w *XLSXWriter) PreWrite(columns []string, types []string) error {
for i, v := range columns {
cell, err := excelize.CoordinatesToCellName(i+1, 1)
if err != nil {
return err
}
w.f.SetCellValue(w.sheet, cell, v)
}
w.rowID++
return nil

Comment on lines 92 to 105
// WriteRow function writes a row to the XLSXWriter.
func (w *XLSXWriter) WriteRow(row []interface{}, columns []string) error {
w.rowID++
for i, v := range row {
if v == nil {
continue
}
cell, error := excelize.CoordinatesToCellName(i+1, w.rowID)
if error != nil {
return error
}
w.f.SetCellValue(w.sheet, cell, v)
}
return nil
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The WriteRow method writes a row to the XLSX file. It correctly increments w.rowID before writing the row, which is good for keeping track of the current row index. However, it skips nil values without writing them, which might lead to misaligned columns if nil is a valid value in some columns. This behavior should be documented or reconsidered.

NewXLSXWriter changed argument to writeOpts.
Added option to specify Cell.
Header can now be specified.
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review Status

Actionable comments generated: 4

Configuration used: CodeRabbit UI

Commits Files that changed from the base of the PR and between 4fb376a and 1cafe09.
Files selected for processing (3)
  • cmd/query.go (2 hunks)
  • cmd/root.go (1 hunks)
  • writer.go (1 hunks)
Additional comments: 2
cmd/query.go (2)
  • 3-13: The import statements have been updated to include the necessary packages for the new functionality. Ensure that all new dependencies are properly managed (e.g., added to go.mod) and that there are no unused imports after the changes.

  • 17-33: The exec function has been updated to use the new setWriter function. Ensure that the error handling is consistent and that all possible error scenarios are accounted for. It's also important to verify that the trdsql.NewTRDSQL function's error handling is robust, as it's not visible in this hunk.

Comment on lines 55 to +86
// OutHeader is output header.
var OutHeader bool

// Skip is skip lines.
var Skip int

// Header is input header.
var Header bool

// OutFileName is output file name.
var OutFileName string

// OutSheetName is output sheet name.
var OutSheetName string

// OutCell is output cell name.
var OutCell string

// ClearSheet is clear sheet if true.
var ClearSheet bool

func init() {
rootCmd.PersistentFlags().BoolVarP(&Ver, "version", "v", false, "display version information")
rootCmd.PersistentFlags().BoolVarP(&Debug, "debug", "", false, "debug mode")
rootCmd.PersistentFlags().IntVarP(&Skip, "skip", "s", 0, "Skip the number of lines")
rootCmd.PersistentFlags().BoolVarP(&Header, "header", "H", false, "Input header")
rootCmd.PersistentFlags().BoolVarP(&OutHeader, "out-header", "O", false, "Output header")
rootCmd.PersistentFlags().StringVarP(&OutFormat, "out", "o", "CSV", "Output Format[CSV|AT|LTSV|JSON|JSONL|TBLN|RAW|MD|VF|YAML]")
rootCmd.PersistentFlags().StringVarP(&OutFormat, "out", "o", "GUESS", "Output Format[CSV|AT|LTSV|JSON|JSONL|TBLN|RAW|MD|VF|YAML]")
rootCmd.PersistentFlags().StringVarP(&OutFileName, "out-xlsx", "x", "", "File name to output to xlsx file")
rootCmd.PersistentFlags().StringVarP(&OutSheetName, "out-sheet", "S", "", "Sheet name to output to xlsx file")
rootCmd.PersistentFlags().StringVarP(&OutCell, "out-cell", "C", "", "Cell name to output to xlsx file")
rootCmd.PersistentFlags().BoolVarP(&ClearSheet, "clear-sheet", "D", false, "Clear sheet when outputting to xlsx file")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The introduction of new global variables and flags for handling XLSX file output is a significant change. It is important to ensure that the documentation and help messages for the tool are updated to reflect these new options. Additionally, the change in the default value of OutFormat from "CSV" to "GUESS" could potentially affect existing users who rely on the default behavior. It would be prudent to highlight this change in the release notes or documentation to inform users of the new behavior.

Moreover, the use of global variables for command-line flags can lead to potential issues with maintainability and testing. It might be beneficial to encapsulate these settings within a configuration struct that can be passed around, which would make it easier to manage the state and dependencies of the application.

Lastly, ensure that the new flags have been thoroughly tested, especially the ClearSheet flag, as it has the potential to modify or delete user data. It is crucial to confirm that the behavior is as expected and that there are safeguards in place to prevent accidental data loss.

Comment on lines 81 to 142
// NewXLSXWriter function takes an io.Writer and trdsql.WriteOpts, and returns a new XLSXWriter.
func NewXLSXWriter(options ...WriteOpt) (*XLSXWriter, error) {
writeOpts := &WriteOpts{
ErrStream: os.Stderr,
FileName: "",
Sheet: "Sheet1",
Cell: "",
ClearSheet: false,
Header: true,
}
for _, option := range options {
option(writeOpts)
}

var f *excelize.File
{
var err error
if _, err = os.Stat(writeOpts.FileName); err != nil && !os.IsNotExist(err) {
return nil, err
}
if os.IsNotExist(err) {
// File does not exist, create a new one
f = excelize.NewFile()
} else {
// File exists, open it
f, err = excelize.OpenFile(writeOpts.FileName)
if err != nil {
return nil, err
}
}
}

cellX, cellY := getCell(writeOpts.Cell)

n, err := f.GetSheetIndex(writeOpts.Sheet)
if err != nil {
return nil, err
}

if writeOpts.ClearSheet {
if n >= 0 {
// Sheet exists,clear it
if err := clearSheet(f, writeOpts.Sheet); err != nil {
return nil, err
}
}
}
if n < 0 {
// Sheet does not exist, create a new one
if _, err := f.NewSheet(writeOpts.Sheet); err != nil {
return nil, err
}
}

return &XLSXWriter{
fileName: writeOpts.FileName,
f: f,
cellX: cellX,
cellY: cellY,
sheet: writeOpts.Sheet,
header: writeOpts.Header,
}, nil
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The NewXLSXWriter function is responsible for creating a new XLSX file or opening an existing one. It also handles the case where the user wants to clear an existing sheet. However, there is a potential issue with the way the sheet index is checked and the sheet is cleared. If the sheet exists and the ClearSheet option is true, the sheet is cleared, but if the sheet does not exist, it is created. The problem arises when the sheet does not exist, and the ClearSheet option is true; the code will create the sheet but not clear it, which is inconsistent. The ClearSheet option should only be relevant if the sheet already exists. Consider revising the logic to ensure that the ClearSheet option is only applied to existing sheets and that the creation of a new sheet is handled separately.

+ // Only attempt to clear the sheet if it exists
+ if writeOpts.ClearSheet && n >= 0 {
+     // Sheet exists, clear it
+     if err := clearSheet(f, writeOpts.Sheet); err != nil {
+         return nil, err
+     }
+ }

Commitable suggestion

[!IMPORTANT]
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation.

Suggested change
// NewXLSXWriter function takes an io.Writer and trdsql.WriteOpts, and returns a new XLSXWriter.
func NewXLSXWriter(options ...WriteOpt) (*XLSXWriter, error) {
writeOpts := &WriteOpts{
ErrStream: os.Stderr,
FileName: "",
Sheet: "Sheet1",
Cell: "",
ClearSheet: false,
Header: true,
}
for _, option := range options {
option(writeOpts)
}
var f *excelize.File
{
var err error
if _, err = os.Stat(writeOpts.FileName); err != nil && !os.IsNotExist(err) {
return nil, err
}
if os.IsNotExist(err) {
// File does not exist, create a new one
f = excelize.NewFile()
} else {
// File exists, open it
f, err = excelize.OpenFile(writeOpts.FileName)
if err != nil {
return nil, err
}
}
}
cellX, cellY := getCell(writeOpts.Cell)
n, err := f.GetSheetIndex(writeOpts.Sheet)
if err != nil {
return nil, err
}
if writeOpts.ClearSheet {
if n >= 0 {
// Sheet exists,clear it
if err := clearSheet(f, writeOpts.Sheet); err != nil {
return nil, err
}
}
}
if n < 0 {
// Sheet does not exist, create a new one
if _, err := f.NewSheet(writeOpts.Sheet); err != nil {
return nil, err
}
}
return &XLSXWriter{
fileName: writeOpts.FileName,
f: f,
cellX: cellX,
cellY: cellY,
sheet: writeOpts.Sheet,
header: writeOpts.Header,
}, nil
func NewXLSXWriter(options ...WriteOpt) (*XLSXWriter, error) {
writeOpts := &WriteOpts{
ErrStream: os.Stderr,
FileName: "",
Sheet: "Sheet1",
Cell: "",
ClearSheet: false,
Header: true,
}
for _, option := range options {
option(writeOpts)
}
var f *excelize.File
{
var err error
if _, err = os.Stat(writeOpts.FileName); err != nil && !os.IsNotExist(err) {
return nil, err
}
if os.IsNotExist(err) {
// File does not exist, create a new one
f = excelize.NewFile()
} else {
// File exists, open it
f, err = excelize.OpenFile(writeOpts.FileName)
if err != nil {
return nil, err
}
}
}
cellX, cellY := getCell(writeOpts.Cell)
n, err := f.GetSheetIndex(writeOpts.Sheet)
if err != nil {
return nil, err
}
// Only attempt to clear the sheet if it exists
if writeOpts.ClearSheet && n >= 0 {
// Sheet exists, clear it
if err := clearSheet(f, writeOpts.Sheet); err != nil {
return nil, err
}
}
if n < 0 {
// Sheet does not exist, create a new one
if _, err := f.NewSheet(writeOpts.Sheet); err != nil {
return nil, err
}
}
return &XLSXWriter{
fileName: writeOpts.FileName,
f: f,
cellX: cellX,
cellY: cellY,
sheet: writeOpts.Sheet,
header: writeOpts.Header,
}, nil

Comment on lines 36 to 87
func setWriter(fileName string) (trdsql.Writer, error) {
dotExt := strings.TrimLeft(filepath.Ext(fileName), ".")
if OutFormat == "GUESS" && dotExt != "" {
OutFormat = strings.ToUpper(dotExt)
}

if strings.ToUpper(OutFormat) != "XLSX" {
return stdWriter(fileName)
}

// XLSX Writer
if fileName == "" {
return nil, fmt.Errorf("file name is required to output with XLSX")
}

if OutSheetName == "" {
OutSheetName = "Sheet1"
}

writer, err := xlsxsql.NewXLSXWriter(
xlsxsql.FileName(fileName),
xlsxsql.Sheet(OutSheetName),
xlsxsql.Cell(OutCell),
xlsxsql.ClearSheet(ClearSheet),
xlsxsql.Header(OutHeader),
)
if err != nil {
return nil, err
}

return writer, nil
}

func stdWriter(fileName string) (trdsql.Writer, error) {
var file io.Writer
if fileName == "" {
file = os.Stdout
} else {
f, err := os.Create(fileName)
if err != nil {
return nil, err
}
file = f
}
format := trdsql.OutputFormat(strings.ToUpper(OutFormat))
w := trdsql.NewWriter(
trdsql.OutStream(file),
trdsql.OutHeader(OutHeader),
trdsql.OutFormat(format),
)
return w, nil
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The setWriter and stdWriter functions have been introduced to handle output format determination and writer instantiation. This is a good approach for separating concerns and improving maintainability. However, there are a few points to consider:

  • The error message in line 48 should be more descriptive. It's good practice to provide clear error messages that can help users understand what went wrong.
  • The stdWriter function creates a new file for writing without checking if the file already exists and without providing a way to append to or overwrite the existing file. This could lead to data loss if the file already contains data.
  • The stdWriter function does not close the file it opens. This can lead to file descriptor leaks. Ensure that files are properly closed after their use.

Here is a proposed change for the error message and file handling:

	if fileName == "" {
-		return nil, fmt.Errorf("file name is required to output with XLSX")
+		return nil, fmt.Errorf("a valid file name is required to output in XLSX format")
	}
...
		f, err := os.Create(fileName)
+		defer f.Close()
		if err != nil {
			return nil, err
		}

Commitable suggestion

[!IMPORTANT]
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation.

Suggested change
func setWriter(fileName string) (trdsql.Writer, error) {
dotExt := strings.TrimLeft(filepath.Ext(fileName), ".")
if OutFormat == "GUESS" && dotExt != "" {
OutFormat = strings.ToUpper(dotExt)
}
if strings.ToUpper(OutFormat) != "XLSX" {
return stdWriter(fileName)
}
// XLSX Writer
if fileName == "" {
return nil, fmt.Errorf("file name is required to output with XLSX")
}
if OutSheetName == "" {
OutSheetName = "Sheet1"
}
writer, err := xlsxsql.NewXLSXWriter(
xlsxsql.FileName(fileName),
xlsxsql.Sheet(OutSheetName),
xlsxsql.Cell(OutCell),
xlsxsql.ClearSheet(ClearSheet),
xlsxsql.Header(OutHeader),
)
if err != nil {
return nil, err
}
return writer, nil
}
func stdWriter(fileName string) (trdsql.Writer, error) {
var file io.Writer
if fileName == "" {
file = os.Stdout
} else {
f, err := os.Create(fileName)
if err != nil {
return nil, err
}
file = f
}
format := trdsql.OutputFormat(strings.ToUpper(OutFormat))
w := trdsql.NewWriter(
trdsql.OutStream(file),
trdsql.OutHeader(OutHeader),
trdsql.OutFormat(format),
)
return w, nil
}
func setWriter(fileName string) (trdsql.Writer, error) {
dotExt := strings.TrimLeft(filepath.Ext(fileName), ".")
if OutFormat == "GUESS" && dotExt != "" {
OutFormat = strings.ToUpper(dotExt)
}
if strings.ToUpper(OutFormat) != "XLSX" {
return stdWriter(fileName)
}
// XLSX Writer
if fileName == "" {
return nil, fmt.Errorf("a valid file name is required to output in XLSX format")
}
if OutSheetName == "" {
OutSheetName = "Sheet1"
}
writer, err := xlsxsql.NewXLSXWriter(
xlsxsql.FileName(fileName),
xlsxsql.Sheet(OutSheetName),
xlsxsql.Cell(OutCell),
xlsxsql.ClearSheet(ClearSheet),
xlsxsql.Header(OutHeader),
)
if err != nil {
return nil, err
}
return writer, nil
}
func stdWriter(fileName string) (trdsql.Writer, error) {
var file io.Writer
if fileName == "" {
file = os.Stdout
} else {
f, err := os.Create(fileName)
if err != nil {
return nil, err
}
defer f.Close()
file = f
}
format := trdsql.OutputFormat(strings.ToUpper(OutFormat))
w := trdsql.NewWriter(
trdsql.OutStream(file),
trdsql.OutHeader(OutHeader),
trdsql.OutFormat(format),
)
return w, nil
}

writer.go Outdated
Comment on lines 208 to 213
// PostWrite function closes the XLSXWriter.
func (w *XLSXWriter) PostWrite() error {
if err := w.f.SaveAs(w.fileName); err != nil {
return err
}
return w.f.Close()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PostWrite method attempts to save and close the XLSX file. However, if the SaveAs method fails, the file is not closed, which could lead to a resource leak. It's important to ensure that the file is closed regardless of whether SaveAs succeeds or fails. This can be achieved by using a defer statement to close the file, which will execute when the function returns.

+ defer w.f.Close()
  if err := w.f.SaveAs(w.fileName); err != nil {
      return err
  }
- return w.f.Close()
+ return nil

Commitable suggestion

[!IMPORTANT]
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation.

Suggested change
// PostWrite function closes the XLSXWriter.
func (w *XLSXWriter) PostWrite() error {
if err := w.f.SaveAs(w.fileName); err != nil {
return err
}
return w.f.Close()
func (w *XLSXWriter) PostWrite() error {
defer w.f.Close()
if err := w.f.SaveAs(w.fileName); err != nil {
return err
}
return nil

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review Status

Actionable comments generated: 1

Configuration used: CodeRabbit UI

Commits Files that changed from the base of the PR and between 1cafe09 and 0274a1d.
Files selected for processing (1)
  • writer_test.go (1 hunks)

Comment on lines +10 to +95
func TestXLSXWriter_WriteRow(t *testing.T) {
type opts struct {
fileName string
f *excelize.File
sheet string
cellName string
}
type args struct {
row []interface{}
columns []string
types []string
}
tests := []struct {
name string
fields opts
args args
wantErr bool
wantRows [][]string
}{
{
name: "test1",
fields: opts{
fileName: "dummy.xlsx",
f: excelize.NewFile(),
sheet: "Sheet1",
cellName: "A1",
},
args: args{
row: []interface{}{"a", "b", "c"},
columns: []string{"A", "B", "C"},
types: []string{"string", "string", "string"},
},
wantErr: false,
wantRows: [][]string{
{"A", "B", "C"},
{"a", "b", "c"},
},
},
{
name: "test2",
fields: opts{
fileName: "dummy.xlsx",
f: excelize.NewFile(),
sheet: "Sheet1",
cellName: "A2",
},
args: args{
row: []interface{}{"a", "b", "c"},
columns: []string{"A", "B", "C"},
types: []string{"string", "string", "string"},
},
wantErr: false,
wantRows: [][]string{
nil,
{"A", "B", "C"},
{"a", "b", "c"},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
w, err := NewXLSXWriter(
FileName(tt.fields.fileName),
Sheet(tt.fields.sheet),
Cell(tt.fields.cellName),
ClearSheet(true),
)
if err != nil {
t.Fatal(err)
}
if err := w.PreWrite(tt.args.columns, tt.args.types); err != nil {
t.Fatal(err)
}
if err := w.WriteRow(tt.args.row, tt.args.columns); (err != nil) != tt.wantErr {
t.Errorf("XLSXWriter.WriteRow() error = %v, wantErr %v", err, tt.wantErr)
}
rows, err := w.f.GetRows(tt.fields.sheet)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(rows, tt.wantRows) {
t.Errorf("XLSXWriter.WriteRow() rows = %#v, wantRows %#v", rows, tt.wantRows)
}
})
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test cases in TestXLSXWriter_WriteRow are well-structured and cover different scenarios for writing rows to an XLSX file. However, there are a few points to consider:

  1. The opts struct contains a fileName field that is not used in the test. If the file name is not necessary for the test's logic, it should be removed to avoid confusion.
  2. The ClearSheet option is set to true in the NewXLSXWriter call, but the test does not verify if the sheet is actually cleared before writing. This should be tested to ensure the ClearSheet functionality works as expected.
  3. The test cases do not cover error scenarios extensively. It would be beneficial to add test cases that expect an error, such as passing invalid cell names or types, to ensure the error handling is robust.
  4. The test assumes that the PreWrite method will always succeed as it calls t.Fatal(err) if an error occurs. It might be worth adding a test case where PreWrite is expected to fail to ensure that it handles error conditions correctly.

Consider addressing these points to improve the test coverage and reliability.

-		fileName string

Commitable suggestion

[!IMPORTANT]
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation.

Suggested change
func TestXLSXWriter_WriteRow(t *testing.T) {
type opts struct {
fileName string
f *excelize.File
sheet string
cellName string
}
type args struct {
row []interface{}
columns []string
types []string
}
tests := []struct {
name string
fields opts
args args
wantErr bool
wantRows [][]string
}{
{
name: "test1",
fields: opts{
fileName: "dummy.xlsx",
f: excelize.NewFile(),
sheet: "Sheet1",
cellName: "A1",
},
args: args{
row: []interface{}{"a", "b", "c"},
columns: []string{"A", "B", "C"},
types: []string{"string", "string", "string"},
},
wantErr: false,
wantRows: [][]string{
{"A", "B", "C"},
{"a", "b", "c"},
},
},
{
name: "test2",
fields: opts{
fileName: "dummy.xlsx",
f: excelize.NewFile(),
sheet: "Sheet1",
cellName: "A2",
},
args: args{
row: []interface{}{"a", "b", "c"},
columns: []string{"A", "B", "C"},
types: []string{"string", "string", "string"},
},
wantErr: false,
wantRows: [][]string{
nil,
{"A", "B", "C"},
{"a", "b", "c"},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
w, err := NewXLSXWriter(
FileName(tt.fields.fileName),
Sheet(tt.fields.sheet),
Cell(tt.fields.cellName),
ClearSheet(true),
)
if err != nil {
t.Fatal(err)
}
if err := w.PreWrite(tt.args.columns, tt.args.types); err != nil {
t.Fatal(err)
}
if err := w.WriteRow(tt.args.row, tt.args.columns); (err != nil) != tt.wantErr {
t.Errorf("XLSXWriter.WriteRow() error = %v, wantErr %v", err, tt.wantErr)
}
rows, err := w.f.GetRows(tt.fields.sheet)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(rows, tt.wantRows) {
t.Errorf("XLSXWriter.WriteRow() rows = %#v, wantRows %#v", rows, tt.wantRows)
}
})
}
}
func TestXLSXWriter_WriteRow(t *testing.T) {
type opts struct {
f *excelize.File
sheet string
cellName string
}
type args struct {
row []interface{}
columns []string
types []string
}
tests := []struct {
name string
fields opts
args args
wantErr bool
wantRows [][]string
}{
{
name: "test1",
fields: opts{
f: excelize.NewFile(),
sheet: "Sheet1",
cellName: "A1",
},
args: args{
row: []interface{}{"a", "b", "c"},
columns: []string{"A", "B", "C"},
types: []string{"string", "string", "string"},
},
wantErr: false,
wantRows: [][]string{
{"A", "B", "C"},
{"a", "b", "c"},
},
},
{
name: "test2",
fields: opts{
f: excelize.NewFile(),
sheet: "Sheet1",
cellName: "A2",
},
args: args{
row: []interface{}{"a", "b", "c"},
columns: []string{"A", "B", "C"},
types: []string{"string", "string", "string"},
},
wantErr: false,
wantRows: [][]string{
nil,
{"A", "B", "C"},
{"a", "b", "c"},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
w, err := NewXLSXWriter(
Sheet(tt.fields.sheet),
Cell(tt.fields.cellName),
ClearSheet(true),
)
if err != nil {
t.Fatal(err)
}
if err := w.PreWrite(tt.args.columns, tt.args.types); err != nil {
t.Fatal(err)
}
if err := w.WriteRow(tt.args.row, tt.args.columns); (err != nil) != tt.wantErr {
t.Errorf("XLSXWriter.WriteRow() error = %v, wantErr %v", err, tt.wantErr)
}
rows, err := w.f.GetRows(tt.fields.sheet)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(rows, tt.wantRows) {
t.Errorf("XLSXWriter.WriteRow() rows = %#v, wantRows %#v", rows, tt.wantRows)
}
})
}
}

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review Status

Actionable comments generated: 4

Configuration used: CodeRabbit UI

Commits Files that changed from the base of the PR and between 0274a1d and d4dc742.
Files selected for processing (2)
  • cmd/query.go (2 hunks)
  • writer.go (1 hunks)
Additional comments: 6
cmd/query.go (4)
  • 3-13: The new import github.com/noborus/xlsxsql is introduced to support XLSX file output. Ensure that this package is properly vetted for security, performance, and compatibility with the existing codebase.

  • 17-33: The exec function has been updated to use the new setWriter function. Ensure that the rest of the codebase and any documentation are updated to reflect the changes in how writers are set, especially if there are any external integrations or scripts that rely on the previous behavior.

  • 51-53: The default sheet name is set to "Sheet1" if not provided. Ensure that this default value is documented and communicated to users, as it affects the output when the sheet name is not explicitly specified.

  • 69-86: In the stdWriter function, the output format is determined and a new writer is created. It is important to ensure that the OutFormat is validated against a list of supported formats to prevent any unexpected behavior if an unsupported format is provided.

writer.go (2)
  • 3-8: The import of the excelize/v2 package is correct, but ensure that the version of the excelize package used is compatible with the rest of the codebase and that it supports all the necessary features required by the XLSXWriter. If the package has been updated recently, check for any breaking changes that might affect the implementation.

  • 214-217: The PostWrite function is responsible for saving the XLSX file after all writing operations are complete. It correctly returns any error that occurs during the save operation. This is a critical step as it ensures that any issues with file saving are reported back to the caller. No issues are found in this part of the code.

Comment on lines +36 to +87
func setWriter(fileName string) (trdsql.Writer, error) {
dotExt := strings.TrimLeft(filepath.Ext(fileName), ".")
if OutFormat == "GUESS" && dotExt != "" {
OutFormat = strings.ToUpper(dotExt)
}

if strings.ToUpper(OutFormat) != "XLSX" {
return stdWriter(fileName)
}

// XLSX Writer
if fileName == "" {
return nil, fmt.Errorf("a valid file name is required to output in XLSX format")
}

if OutSheetName == "" {
OutSheetName = "Sheet1"
}

writer, err := xlsxsql.NewXLSXWriter(
xlsxsql.FileName(fileName),
xlsxsql.Sheet(OutSheetName),
xlsxsql.Cell(OutCell),
xlsxsql.ClearSheet(ClearSheet),
xlsxsql.Header(OutHeader),
)
if err != nil {
return nil, err
}

return writer, nil
}

func stdWriter(fileName string) (trdsql.Writer, error) {
var file io.Writer
if fileName == "" {
file = os.Stdout
} else {
f, err := os.Create(fileName)
if err != nil {
return nil, err
}
file = f
}
format := trdsql.OutputFormat(strings.ToUpper(OutFormat))
w := trdsql.NewWriter(
trdsql.OutStream(file),
trdsql.OutHeader(OutHeader),
trdsql.OutFormat(format),
)
return w, nil
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The setWriter and stdWriter functions have been introduced to handle output formatting based on the file extension or specified output format. It is important to ensure that the OutFormat global variable is properly sanitized and validated before use to prevent any potential security risks or logical errors. Additionally, the error handling in setWriter when the file name is empty or an error occurs while creating a new XLSX writer is appropriate. However, in stdWriter, when creating a file with os.Create, the file is not being closed, which can lead to file descriptor leaks. It is recommended to defer the close operation after opening the file.

		f, err := os.Create(fileName)
		if err != nil {
			return nil, err
		}
+		defer f.Close()

Commitable suggestion

[!IMPORTANT]
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation.

Suggested change
func setWriter(fileName string) (trdsql.Writer, error) {
dotExt := strings.TrimLeft(filepath.Ext(fileName), ".")
if OutFormat == "GUESS" && dotExt != "" {
OutFormat = strings.ToUpper(dotExt)
}
if strings.ToUpper(OutFormat) != "XLSX" {
return stdWriter(fileName)
}
// XLSX Writer
if fileName == "" {
return nil, fmt.Errorf("a valid file name is required to output in XLSX format")
}
if OutSheetName == "" {
OutSheetName = "Sheet1"
}
writer, err := xlsxsql.NewXLSXWriter(
xlsxsql.FileName(fileName),
xlsxsql.Sheet(OutSheetName),
xlsxsql.Cell(OutCell),
xlsxsql.ClearSheet(ClearSheet),
xlsxsql.Header(OutHeader),
)
if err != nil {
return nil, err
}
return writer, nil
}
func stdWriter(fileName string) (trdsql.Writer, error) {
var file io.Writer
if fileName == "" {
file = os.Stdout
} else {
f, err := os.Create(fileName)
if err != nil {
return nil, err
}
file = f
}
format := trdsql.OutputFormat(strings.ToUpper(OutFormat))
w := trdsql.NewWriter(
trdsql.OutStream(file),
trdsql.OutHeader(OutHeader),
trdsql.OutFormat(format),
)
return w, nil
}
func stdWriter(fileName string) (trdsql.Writer, error) {
var file io.Writer
if fileName == "" {
file = os.Stdout
} else {
f, err := os.Create(fileName)
if err != nil {
return nil, err
}
defer f.Close()
file = f
}
format := trdsql.OutputFormat(strings.ToUpper(OutFormat))
w := trdsql.NewWriter(
trdsql.OutStream(file),
trdsql.OutHeader(OutHeader),
trdsql.OutFormat(format),
)
return w, nil
}

Comment on lines +47 to +48
if fileName == "" {
return nil, fmt.Errorf("a valid file name is required to output in XLSX format")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error message for an empty file name when outputting in XLSX format is clear and user-friendly. However, consider also validating the file name for any illegal characters or patterns that might cause issues on the file system.

Comment on lines 81 to 129
// NewXLSXWriter function takes an io.Writer and trdsql.WriteOpts, and returns a new XLSXWriter.
func NewXLSXWriter(options ...WriteOpt) (*XLSXWriter, error) {
writeOpts := &WriteOpts{
ErrStream: os.Stderr,
FileName: "",
Sheet: "Sheet1",
Cell: "",
ClearSheet: false,
Header: true,
}
for _, option := range options {
option(writeOpts)
}

f, err := openXLSXFile(writeOpts.FileName)
if err != nil {
return nil, err
}

cellX, cellY := getCell(writeOpts.Cell)

n, err := f.GetSheetIndex(writeOpts.Sheet)
if err != nil {
return nil, err
}
// Only attempt to clear the sheet if it exists.
if writeOpts.ClearSheet && n >= 0 {
// Sheet exists,clear it
if err := clearSheet(f, writeOpts.Sheet); err != nil {
return nil, err
}
}

if n < 0 {
// Sheet does not exist, create a new one
if _, err := f.NewSheet(writeOpts.Sheet); err != nil {
return nil, err
}
}

return &XLSXWriter{
fileName: writeOpts.FileName,
f: f,
cellX: cellX,
cellY: cellY,
sheet: writeOpts.Sheet,
header: writeOpts.Header,
}, nil
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The NewXLSXWriter function is responsible for creating a new XLSXWriter instance. It correctly applies the provided options and handles the creation or opening of the XLSX file. However, there is a potential issue with the handling of the Sheet option. If the sheet does not exist and ClearSheet is true, the code attempts to clear a non-existent sheet before creating it. The order of operations should be adjusted to create the sheet first if it does not exist, and then clear it if necessary.

- if n < 0 {
-     // Sheet does not exist, create a new one
-     if _, err := f.NewSheet(writeOpts.Sheet); err != nil {
-         return nil, err
-     }
- }
- // Only attempt to clear the sheet if it exists.
- if writeOpts.ClearSheet && n >= 0 {
+ if n < 0 {
+     // Sheet does not exist, create a new one
+     if _, err := f.NewSheet(writeOpts.Sheet); err != nil {
+         return nil, err
+     }
+ } else if writeOpts.ClearSheet {
+     // Sheet exists, clear it

Commitable suggestion

[!IMPORTANT]
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation.

Suggested change
// NewXLSXWriter function takes an io.Writer and trdsql.WriteOpts, and returns a new XLSXWriter.
func NewXLSXWriter(options ...WriteOpt) (*XLSXWriter, error) {
writeOpts := &WriteOpts{
ErrStream: os.Stderr,
FileName: "",
Sheet: "Sheet1",
Cell: "",
ClearSheet: false,
Header: true,
}
for _, option := range options {
option(writeOpts)
}
f, err := openXLSXFile(writeOpts.FileName)
if err != nil {
return nil, err
}
cellX, cellY := getCell(writeOpts.Cell)
n, err := f.GetSheetIndex(writeOpts.Sheet)
if err != nil {
return nil, err
}
// Only attempt to clear the sheet if it exists.
if writeOpts.ClearSheet && n >= 0 {
// Sheet exists,clear it
if err := clearSheet(f, writeOpts.Sheet); err != nil {
return nil, err
}
}
if n < 0 {
// Sheet does not exist, create a new one
if _, err := f.NewSheet(writeOpts.Sheet); err != nil {
return nil, err
}
}
return &XLSXWriter{
fileName: writeOpts.FileName,
f: f,
cellX: cellX,
cellY: cellY,
sheet: writeOpts.Sheet,
header: writeOpts.Header,
}, nil
}
func NewXLSXWriter(options ...WriteOpt) (*XLSXWriter, error) {
writeOpts := &WriteOpts{
ErrStream: os.Stderr,
FileName: "",
Sheet: "Sheet1",
Cell: "",
ClearSheet: false,
Header: true,
}
for _, option := range options {
option(writeOpts)
}
f, err := openXLSXFile(writeOpts.FileName)
if err != nil {
return nil, err
}
cellX, cellY := getCell(writeOpts.Cell)
n, err := f.GetSheetIndex(writeOpts.Sheet)
if err != nil {
return nil, err
}
if n < 0 {
// Sheet does not exist, create a new one
if _, err := f.NewSheet(writeOpts.Sheet); err != nil {
return nil, err
}
} else if writeOpts.ClearSheet {
// Sheet exists, clear it
if err := clearSheet(f, writeOpts.Sheet); err != nil {
return nil, err
}
}
return &XLSXWriter{
fileName: writeOpts.FileName,
f: f,
cellX: cellX,
cellY: cellY,
sheet: writeOpts.Sheet,
header: writeOpts.Header,
}, nil
}

Comment on lines 181 to 195
// PreWrite function opens the XLSXWriter.
func (w *XLSXWriter) PreWrite(columns []string, types []string) error {
if !w.header {
return nil
}
// Write header
for i, v := range columns {
cell, err := excelize.CoordinatesToCellName(w.cellX+i+1, w.cellY+1)
if err != nil {
return err
}
w.f.SetCellValue(w.sheet, cell, v)
}
w.rowID++
return nil
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PreWrite function is designed to write the header to the XLSX file if the header flag is set. It correctly increments the rowID after writing the header. However, the function does not handle the case where the header writing fails. If an error occurs during the header writing process, it should be returned immediately to prevent further operations on a potentially corrupted file.

- cell, err := excelize.CoordinatesToCellName(w.cellX+i+1, w.cellY+1)
- if err != nil {
-     return err
- }
- w.f.SetCellValue(w.sheet, cell, v)
+ cell, err := excelize.CoordinatesToCellName(w.cellX+i+1, w.cellY+1)
+ if err != nil {
+     return err
+ }
+ if err := w.f.SetCellValue(w.sheet, cell, v); err != nil {
+     return err
+ }

Commitable suggestion

[!IMPORTANT]
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation.

Suggested change
// PreWrite function opens the XLSXWriter.
func (w *XLSXWriter) PreWrite(columns []string, types []string) error {
if !w.header {
return nil
}
// Write header
for i, v := range columns {
cell, err := excelize.CoordinatesToCellName(w.cellX+i+1, w.cellY+1)
if err != nil {
return err
}
w.f.SetCellValue(w.sheet, cell, v)
}
w.rowID++
return nil
// PreWrite function opens the XLSXWriter.
func (w *XLSXWriter) PreWrite(columns []string, types []string) error {
if !w.header {
return nil
}
// Write header
for i, v := range columns {
cell, err := excelize.CoordinatesToCellName(w.cellX+i+1, w.cellY+1)
if err != nil {
return err
}
if err := w.f.SetCellValue(w.sheet, cell, v); err != nil {
return err
}
}
w.rowID++
return nil

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review Status

Actionable comments generated: 6

Configuration used: CodeRabbit UI

Commits Files that changed from the base of the PR and between d4dc742 and adf97c6.
Files selected for processing (1)
  • writer.go (1 hunks)
Additional comments: 1
writer.go (1)
  • 199-215: The WriteRow function writes a row to the XLSX file. It currently skips nil values without writing them to the file. Depending on the use case, it might be necessary to write empty cells for nil values to maintain the structure of the data in the XLSX file. Consider whether nil values should be represented as empty cells in the output and adjust the code accordingly.

Comment on lines +81 to +127
// NewXLSXWriter function takes an io.Writer and trdsql.WriteOpts, and returns a new XLSXWriter.
func NewXLSXWriter(options ...WriteOpt) (*XLSXWriter, error) {
writeOpts := &WriteOpts{
ErrStream: os.Stderr,
FileName: "",
Sheet: "Sheet1",
Cell: "",
ClearSheet: false,
Header: true,
}
for _, option := range options {
option(writeOpts)
}

f, err := openXLSXFile(writeOpts.FileName)
if err != nil {
return nil, err
}
cellX, cellY := getCell(writeOpts.Cell)

n, err := f.GetSheetIndex(writeOpts.Sheet)
if err != nil {
return nil, err
}
// Only attempt to clear the sheet if it exists.
if writeOpts.ClearSheet && n >= 0 {
// Sheet exists,clear it
if err := clearSheet(f, writeOpts.Sheet); err != nil {
return nil, err
}
}

if n < 0 {
// Sheet does not exist, create a new one
if _, err := f.NewSheet(writeOpts.Sheet); err != nil {
return nil, err
}
}

return &XLSXWriter{
fileName: writeOpts.FileName,
f: f,
cellX: cellX,
cellY: cellY,
sheet: writeOpts.Sheet,
header: writeOpts.Header,
}, nil
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The NewXLSXWriter function is responsible for creating a new XLSXWriter instance. It is important to ensure that the error stream (ErrStream) is properly used throughout the XLSXWriter methods to report errors. Currently, the ErrStream is set in the WriteOpts but not used anywhere in the XLSXWriter methods. This could lead to a situation where errors are not reported or logged as expected, which can make debugging difficult. Consider using the ErrStream in methods like PreWrite, WriteRow, and PostWrite to handle and log errors appropriately.

Comment on lines +130 to +147
// openXLSXFile function opens the XLSX file.
func openXLSXFile(fileName string) (*excelize.File, error) {
var f *excelize.File
var err error
if _, err = os.Stat(fileName); err != nil && !os.IsNotExist(err) {
return nil, err
}
if os.IsNotExist(err) {
// File does not exist, create a new one
f = excelize.NewFile()
} else {
// File exists, open it
f, err = excelize.OpenFile(fileName)
if err != nil {
return nil, err
}
}
return f, nil
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The openXLSXFile function checks if the file exists and either creates a new file or opens an existing one. However, there is no check to ensure that the file is not a directory, which could lead to unexpected behavior if a directory with the expected file name exists. It's recommended to add a check to ensure that the specified fileName is not a directory before proceeding with file operations.

Comment on lines +150 to +158
func getCell(cellName string) (int, int) {
if cellName == "" {
return 0, 0
}
x, y, err := excelize.CellNameToCoordinates(cellName)
if err != nil {
return 0, 0
}
return x - 1, y - 1
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The getCell function returns (0, 0) when an error occurs in excelize.CellNameToCoordinates. This could lead to silent failures where the caller of getCell may not realize that an invalid cell name was provided. It would be better to return the error to the caller so that it can be handled appropriately. This change would require updating the getCell function signature to return an error and modifying the calling code to handle the error.

Comment on lines +161 to +177
// clearSheet function clears the sheet.
func clearSheet(f *excelize.File, sheet string) error {
rows, err := f.GetRows(sheet)
if err != nil {
return err
}

for i, row := range rows {
for j := range row {
axis, err := excelize.CoordinatesToCellName(j+1, i+1)
if err != nil {
return err
}
f.SetCellStr(sheet, axis, "")
}
}
return nil
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The clearSheet function iterates over all rows and columns to clear the sheet by setting each cell to an empty string. This approach can be inefficient for large sheets as it involves a lot of individual write operations. A more efficient approach would be to delete the sheet and recreate it, which can be done with the excelize library's DeleteSheet and NewSheet methods. This would reduce the number of operations and potentially improve performance.

Comment on lines +180 to +197
// PreWrite function opens the XLSXWriter.
func (w *XLSXWriter) PreWrite(columns []string, types []string) error {
if !w.header {
return nil
}
// Write header
for i, v := range columns {
cell, err := excelize.CoordinatesToCellName(w.cellX+i+1, w.cellY+1)
if err != nil {
return err
}
if err := w.f.SetCellValue(w.sheet, cell, v); err != nil {
return err
}
}
w.rowID++
return nil
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PreWrite function writes the header to the XLSX file if the header flag is set. However, there is no check to ensure that the columns slice is not empty before attempting to write the header. Writing an empty header could be considered a logical error, so it's recommended to add a check for an empty columns slice and handle it accordingly, either by returning an error or skipping the header writing process.

Comment on lines +217 to +220
// PostWrite function closes the XLSXWriter.
func (w *XLSXWriter) PostWrite() error {
return w.f.SaveAs(w.fileName)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PostWrite function saves the XLSX file using the SaveAs method. It's important to ensure that the fileName is not empty before attempting to save the file, as an empty file name could lead to an error or unexpected behavior. Consider adding a check for an empty fileName and handling the error if necessary.

@noborus noborus merged commit 8314e0f into main Nov 20, 2023
@noborus noborus deleted the add-writer branch November 20, 2023 21:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants