├── .github └── workflows │ ├── build.yml │ └── release.yml ├── .goreleaser.yaml ├── LICENSE ├── Makefile ├── README.md ├── cmd ├── list.go ├── query.go ├── root.go ├── table.go └── xlsxsql │ └── main.go ├── docs └── xlsxsql.png ├── go.mod ├── go.sum ├── reader.go ├── reader_test.go ├── testdata ├── test1.xlsx ├── test2.xlsx └── test3.xlsx ├── writer.go └── writer_test.go /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | on: [push] 3 | jobs: 4 | 5 | build: 6 | runs-on: ubuntu-latest 7 | strategy: 8 | matrix: 9 | go-version: [ 'stable' ] 10 | name: Go ${{ matrix.go-version }} 11 | steps: 12 | - uses: actions/checkout@v4 13 | - name: Setup Go 14 | uses: actions/setup-go@v5 15 | with: 16 | go-version: ${{ matrix.go-version }} 17 | id: go 18 | 19 | - name: Build 20 | run: make 21 | 22 | - name: test 23 | run: make test -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | on: 3 | push: 4 | tags: 5 | - "*" 6 | jobs: 7 | goreleaser: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout 11 | uses: actions/checkout@v4 12 | with: 13 | fetch-depth: 0 14 | - name: Setup Go 15 | uses: actions/setup-go@v5 16 | with: 17 | go-version: 'stable' 18 | - name: Run GoReleaser 19 | uses: goreleaser/goreleaser-action@v6 20 | with: 21 | version: latest 22 | args: release --rm-dist 23 | env: 24 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 25 | TAP_GITHUB_TOKEN: ${{ secrets.TAP_GITHUB_TOKEN }} 26 | -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | # This is an example .goreleaser.yml file with some sensible defaults. 2 | # Make sure to check the documentation at https://goreleaser.com 3 | before: 4 | hooks: 5 | # You may remove this if you don't use go modules. 6 | - go mod tidy 7 | # you may remove this if you don't need go generate 8 | - go generate ./... 9 | builds: 10 | - env: 11 | - CGO_ENABLED=0 12 | goos: 13 | - linux 14 | - windows 15 | - darwin 16 | id: "xlsxsql" 17 | ldflags: 18 | - -s -w -X main.version={{.Version}} -X main.revision={{.Commit}} 19 | main: ./cmd/xlsxsql 20 | ignore: 21 | - goos: windows 22 | goarch: "386" 23 | archives: 24 | - format: tar.gz 25 | # this name template makes the OS and Arch compatible with the results of uname. 26 | name_template: >- 27 | {{ .ProjectName }}_{{ .Version }}_ 28 | {{- title .Os }}_ 29 | {{- if eq .Arch "amd64" }}x86_64 30 | {{- else if eq .Arch "386" }}i386 31 | {{- else }}{{ .Arch }}{{ end }} 32 | {{- if .Arm }}v{{ .Arm }}{{ end }} 33 | # use zip for windows archives 34 | format_overrides: 35 | - goos: windows 36 | format: zip 37 | checksum: 38 | name_template: 'checksums.txt' 39 | snapshot: 40 | name_template: "{{ incpatch .Version }}-next" 41 | changelog: 42 | sort: asc 43 | filters: 44 | exclude: 45 | - '^docs:' 46 | - '^test:' 47 | nfpms: 48 | - 49 | package_name: "xlsxsql" 50 | homepage: "https://github.com/noborus/xlsxsql" 51 | maintainer: "Noboru Saito " 52 | description: "A CLI tool to execute SQL queries on xlsx files" 53 | license: "MIT" 54 | formats: 55 | - deb 56 | - rpm 57 | 58 | brews: 59 | - 60 | name: xlsxsql 61 | repository: 62 | owner: noborus 63 | name: homebrew-tap 64 | token: "{{ .Env.TAP_GITHUB_TOKEN }}" 65 | commit_author: 66 | name: noborus 67 | email: noborusai@gmail.com 68 | homepage: https://github.com/noborus/xlsxsql 69 | description: "Execute SQL to xlsx and convert to other format" 70 | # modelines, feel free to remove those if you don't want/use them: 71 | # yaml-language-server: $schema=https://goreleaser.com/static/schema.json 72 | # vim: set ts=2 sw=2 tw=0 fo=cnqoj 73 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright © 2023 Noboru Saito 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | BINARY_NAME := xlsxsql 2 | SRCS := $(shell git ls-files '*.go') 3 | LDFLAGS := "-X main.version=$(shell git describe --tags --abbrev=0 --always) -X main.revision=$(shell git rev-parse --short HEAD)" 4 | 5 | all: build 6 | 7 | test: $(SRCS) 8 | go test ./... 9 | 10 | build: $(BINARY_NAME) 11 | 12 | $(BINARY_NAME): $(SRCS) 13 | go build -ldflags $(LDFLAGS) -o $(BINARY_NAME) ./cmd/xlsxsql 14 | 15 | install: 16 | go install -ldflags $(LDFLAGS) ./cmd/xlsxsql 17 | 18 | clean: 19 | rm -f $(BINARY_NAME) 20 | 21 | .PHONY: all test build install clean -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # xlsxsql 2 | 3 | [![PkgGoDev](https://pkg.go.dev/badge/github.com/noborus/xlsxsql)](https://pkg.go.dev/github.com/noborus/xlsxsql) 4 | [![Actions Status](https://github.com/noborus/xlsxsql/workflows/Go/badge.svg)](https://github.com/noborus/xlsxsql/actions) 5 | 6 | A CLI tool that executes SQL queries on various files including xlsx files and outputs the results to various files. 7 | 8 | ![xlsxsql query -H -o md "SELECT a.id,a.name,b.price FROM testdata/test3.xlsx::.C1 AS a LEFT JOIN testdata/test3.xlsx::.F4 AS b ON a.id=b.id"](docs/xlsxsql.png) 9 | 10 | | id | name | price | 11 | |----|--------|-------| 12 | | 1 | apple | 100 | 13 | | 2 | orange | 50 | 14 | | 3 | melon | 500 | 15 | 16 | A CLI tool that executes SQL queries on xlsx files and outputs the results to various files, and also executes SQL queries on various files and outputs them to xlsx files. 17 | Built using [excelize](https://github.com/qax-os/excelize) and [trdsql](https://github.com/noborus/trdsql). 18 | 19 | ## Install 20 | 21 | ### Go install 22 | 23 | ```console 24 | go install github.com/noborus/xlsxsql/cmd/xlsxsql@latest 25 | ``` 26 | 27 | ### Homebrew 28 | 29 | You can install Homebrew's xlsxsql with the following command: 30 | 31 | ```console 32 | brew install noborus/tap/xlsxsql 33 | ``` 34 | 35 | ### Binary Downloads 36 | 37 | Precompiled binaries for xlsxsql are available for various platforms and architectures. You can download them from the [GitHub Releases](https://github.com/noborus/xlsxsql/releases) page. 38 | 39 | The following binaries can be downloaded from release. 40 | 41 | - Darwin_arm64 42 | - Darwin_x86_64 43 | - Linux_arm64 44 | - Linux_i386 45 | - Linux_x86_64 46 | - Windows_arm64 47 | - Windows_x86_64 48 | 49 | To install a binary, download the appropriate file for your system, extract it, and place the `xlsxsql` executable in a directory included in your system's `PATH`. 50 | 51 | For example, on a Unix-like system, you might do: 52 | 53 | ```console 54 | tar xvf xlsxsql_Darwin_x86_64.tar.gz 55 | mv xlsxsql /usr/local/bin/ 56 | ``` 57 | 58 | ## Usage 59 | 60 | ```console 61 | $ xlsxsql --help 62 | Execute SQL against xlsx file. 63 | Output to CSV and various formats. 64 | 65 | Usage: 66 | xlsxsql [flags] 67 | xlsxsql [command] 68 | 69 | Available Commands: 70 | completion Generate the autocompletion script for the specified shell 71 | help Help about any command 72 | list List the sheets of the xlsx file 73 | query Executes the specified SQL query against the xlsx file 74 | table SQL(SELECT * FROM table) for xlsx 75 | 76 | Flags: 77 | --clear-sheet Clear sheet when outputting to xlsx file 78 | --debug debug mode 79 | -H, --header Input header 80 | -h, --help help for xlsxsql 81 | -o, --out string Output Format[CSV|AT|LTSV|JSON|JSONL|TBLN|RAW|MD|VF|YAML|XLSX] (default "GUESS") 82 | --out-cell string Cell name to output to xlsx file 83 | -O, --out-file string File name to output to file 84 | --out-header Output header 85 | --out-sheet string Sheet name to output to xlsx file 86 | -s, --skip int Skip the number of lines 87 | -v, --version display version information 88 | 89 | Use "xlsxsql [command] --help" for more information about a command. 90 | ``` 91 | 92 | ### List sheets 93 | 94 | ```console 95 | $ xlsxsql list test.xlsx 96 | Sheet1 97 | Sheet2 98 | ``` 99 | 100 | ### Basic usage 101 | 102 | The basic usage of xlsxsql is to run a SQL query against an Excel file. 103 | The `query` command is used followed by the SQL query in quotes. 104 | The SQL query should include the name of the Excel file. If no sheet is specified, the first sheet will be targeted. 105 | 106 | ```console 107 | xlsxsql query "SELECT * FROM test.xlsx" 108 | ``` 109 | 110 | For example, if test.xlsx contains the following data in its first sheet: 111 | 112 | | Name | Age | 113 | | ----- | --- | 114 | | Alice | 20 | 115 | | Bob | 25 | 116 | | Carol | 30 | 117 | 118 | The output will be: 119 | 120 | ```csv 121 | Name,Age 122 | Alice,20 123 | Bob,25 124 | Carol,30 125 | ``` 126 | 127 | `xlsxsql` is an extended version of [trdsql](https://github.com/noborus/trdsql), 128 | so you can execute SQL on files such as CSV and JSON. 129 | 130 | ```console 131 | xlsxsql query "SELECT * FROM test.csv" 132 | ``` 133 | 134 | In other words, you can also do CSV and JOIN. 135 | 136 | ```console 137 | xlsxsql query -H -o md \ 138 | "SELECT a.id,a.name,b.price 139 | FROM testdata/test3.xlsx::.C1 AS a 140 | LEFT JOIN test.csv AS b 141 | ON a.id=b.id" 142 | ``` 143 | 144 | ### Specify sheet 145 | 146 | The sheet can be specified by using a double colon "::" after the file name 147 | (the first sheet is selected by default if not specified). 148 | 149 | ```console 150 | xlsxsql query "SELECT * FROM test.xlsx::Sheet2" 151 | ``` 152 | 153 | ### Specify cell 154 | 155 | Cell can be specified by using a dot "." after the sheet. 156 | 157 | ```console 158 | xlsxsql query "SELECT * FROM test3.xlsx::Sheet1.C1" 159 | ``` 160 | 161 | Optional if the sheet is the first sheet. 162 | 163 | ```console 164 | xlsxsql query "SELECT * FROM test3.xlsx::.C1" 165 | ``` 166 | 167 | > [!NOTE] 168 | > If cell is specified, the table up to the blank column is considered to be the table. 169 | ​ 170 | This allows multiple tables to be specified on one sheet, and JOIN is also possible. 171 | 172 | ```console 173 | xlsxsql query -H -o md \ 174 | "SELECT a.id,a.name,b.price 175 | FROM testdata/test3.xlsx::.C1 AS a 176 | LEFT JOIN testdata/test3.xlsx::.F4 AS b 177 | ON a.id=b.id" 178 | ``` 179 | 180 | ### Shorthand designation 181 | 182 | The `table` command is a shorthand that allows you to quickly display the contents of a specified sheet in a table format. 183 | The syntax is `xlsxsql table ::.`. 184 | If no sheet name is specified, the first sheet of the Excel file will be targeted. 185 | 186 | Here is an example: 187 | 188 | ```console 189 | xlsxsql table test.xlsx::Sheet2.C1 190 | ``` 191 | 192 | It can be omitted for the first sheet. 193 | 194 | ```console 195 | xlsxsql table test.xlsx::.C1 196 | ``` 197 | 198 | ### Skip Options 199 | 200 | The `--skip` or `-s` option skips the specified number of lines. 201 | For example, you would use it like this: 202 | 203 | ```console 204 | xlsxsql query --skip 1 "SELECT * FROM test.xlsx::Sheet2" 205 | ``` 206 | 207 | Skip is useful when specifying sheets, allowing you to skip unnecessary rows. 208 | (There seems to be no advantage to using skip when specifying Cell.) 209 | 210 | ### Output format 211 | 212 | ```console 213 | xlsxsql query --out JSONL "SELECT * FROM test.xlsx::Sheet2" 214 | ``` 215 | 216 | You can choose from CSV, LTSV, JSON, JSONL, TBLN, RAW, MD, VF, YAML, (XLSX). 217 | 218 | ### Output to xlsx file 219 | 220 | You can output the result to an xlsx file by specifying a file name with the `.xlsx` extension as the `--out-file` option. For example: 221 | 222 | ```console 223 | xlsxsql query --out-file test2.xlsx "SELECT * FROM test.xlsx::Sheet2" 224 | ``` 225 | 226 | > [!NOTE] 227 | > You can also output to the same xlsx file as the input file. Please be careful as the contents will be overwritten. 228 | 229 | > [!NOTE] 230 | > Even if you specify XLSX with --out, you must specify a file name with the extension `.xlsx`. 231 | 232 | This command will execute the SQL query on the Sheet1 of test.xlsx and output the result to result.xlsx. 233 | If the file does not exist, it will be created. If the file already exists, the results will be updated. 234 | 235 | You can specify the `sheet` and `cell` to output, if you want to output to an xlsx file. For example: 236 | 237 | ```console 238 | xlsxsql query --out-file test2.xlsx --out-sheet Sheet2 --out-cell C1 "SELECT * FROM test.xlsx::Sheet2" 239 | ``` 240 | 241 | You can clear the sheet before outputting to an xlsx file by specifying the `--clear-sheet` option. For example: 242 | 243 | ```console 244 | xlsxsql query --out-file test2.xlsx --clear-sheet "SELECT * FROM test.xlsx::Sheet2" 245 | ``` 246 | 247 | ### Multiple queries 248 | 249 | It is also possible to output after executing an update query. 250 | A SELECT query is required for output. 251 | 252 | ```console 253 | xlsxsql query --header --out-header --out-file test.xlsx --out-sheet Sheet2 \ 254 | "UPDATE test.xlsx SET Age=Age+1 WHERE Name='Alice'; 255 | SELECT * FROM test.xlsx" 256 | ``` 257 | -------------------------------------------------------------------------------- /cmd/list.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/noborus/xlsxsql" 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | // listSheetsCmd represents the list command. 11 | var listSheetsCmd = &cobra.Command{ 12 | Use: "list", 13 | Short: "List the sheets of the xlsx file", 14 | Long: `List the sheets of the xlsx file.`, 15 | RunE: func(cmd *cobra.Command, args []string) error { 16 | if len(args) != 1 { 17 | return fmt.Errorf("please specify the xlsx file") 18 | } 19 | list, err := xlsxsql.XLSXSheet(args[0]) 20 | if err != nil { 21 | fmt.Println(err) 22 | return err 23 | } 24 | for _, v := range list { 25 | fmt.Println(v) 26 | } 27 | return nil 28 | }, 29 | } 30 | 31 | func init() { 32 | rootCmd.AddCommand(listSheetsCmd) 33 | } 34 | -------------------------------------------------------------------------------- /cmd/query.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os" 7 | "path/filepath" 8 | "strings" 9 | 10 | "github.com/noborus/trdsql" 11 | "github.com/noborus/xlsxsql" 12 | "github.com/spf13/cobra" 13 | ) 14 | 15 | func exec(args []string) error { 16 | trdsql.EnableMultipleQueries() 17 | if Debug { 18 | trdsql.EnableDebug() 19 | } 20 | query := strings.Join(args, " ") 21 | 22 | writer, err := setWriter(OutFileName) 23 | if err != nil { 24 | return err 25 | } 26 | trd := trdsql.NewTRDSQL( 27 | trdsql.NewImporter( 28 | trdsql.InSkip(Skip), 29 | trdsql.InHeader(Header), 30 | trdsql.InPreRead(100), 31 | ), 32 | trdsql.NewExporter(writer), 33 | ) 34 | return trd.Exec(query) 35 | } 36 | 37 | func setWriter(fileName string) (trdsql.Writer, error) { 38 | dotExt := strings.TrimLeft(filepath.Ext(fileName), ".") 39 | if OutFormat == "GUESS" && dotExt != "" { 40 | OutFormat = strings.ToUpper(dotExt) 41 | } 42 | 43 | if strings.ToUpper(OutFormat) != "XLSX" { 44 | return stdWriter(fileName) 45 | } 46 | 47 | // XLSX Writer 48 | if fileName == "" { 49 | return nil, fmt.Errorf("a valid file name (--out-file) is required to output in XLSX format") 50 | } 51 | 52 | if OutSheetName == "" { 53 | OutSheetName = "Sheet1" 54 | } 55 | 56 | writer, err := xlsxsql.NewXLSXWriter( 57 | xlsxsql.FileName(fileName), 58 | xlsxsql.Sheet(OutSheetName), 59 | xlsxsql.Cell(OutCell), 60 | xlsxsql.ClearSheet(ClearSheet), 61 | xlsxsql.Header(OutHeader), 62 | ) 63 | if err != nil { 64 | return nil, err 65 | } 66 | 67 | return writer, nil 68 | } 69 | 70 | func stdWriter(fileName string) (trdsql.Writer, error) { 71 | var file io.Writer 72 | if fileName == "" { 73 | file = os.Stdout 74 | } else { 75 | f, err := os.Create(fileName) 76 | if err != nil { 77 | return nil, err 78 | } 79 | file = f 80 | } 81 | format := trdsql.OutputFormat(strings.ToUpper(OutFormat)) 82 | w := trdsql.NewWriter( 83 | trdsql.OutStream(file), 84 | trdsql.OutHeader(OutHeader), 85 | trdsql.OutFormat(format), 86 | ) 87 | return w, nil 88 | } 89 | 90 | // queryCmd represents the query command. 91 | var queryCmd = &cobra.Command{ 92 | Use: "query", 93 | Short: "Executes the specified SQL query against the xlsx file", 94 | Long: `Executes the specified SQL query against the xlsx file. 95 | Output to CSV and various formats.`, 96 | RunE: func(cmd *cobra.Command, args []string) error { 97 | return exec(args) 98 | }, 99 | } 100 | 101 | func init() { 102 | rootCmd.AddCommand(queryCmd) 103 | } 104 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | 8 | _ "github.com/noborus/xlsxsql" 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | // rootCmd represents the base command when called without any subcommands. 13 | var rootCmd = &cobra.Command{ 14 | Use: "xlsxsql", 15 | Short: "Execute SQL against xlsx file.", 16 | Long: `Execute SQL against xlsx file. 17 | Output to CSV and various formats.`, 18 | SilenceUsage: true, 19 | Run: func(cmd *cobra.Command, args []string) { 20 | if Ver { 21 | fmt.Printf("xlsxsql version %s rev:%s\n", Version, Revision) 22 | return 23 | } 24 | if err := cmd.Help(); err != nil { 25 | cmd.SetOutput(os.Stderr) 26 | os.Exit(1) 27 | } 28 | }, 29 | } 30 | 31 | var ( 32 | // Version represents the version. 33 | Version string 34 | // Revision set "git rev-parse --short HEAD" 35 | Revision string 36 | ) 37 | 38 | // Execute adds all child commands to the root command and sets flags appropriately. 39 | // This is called by main.main(). It only needs to happen once to the rootCmd. 40 | func Execute(version string, revision string) { 41 | Version = version 42 | Revision = revision 43 | if err := rootCmd.Execute(); err != nil { 44 | rootCmd.SetOutput(os.Stderr) 45 | os.Exit(1) 46 | } 47 | } 48 | 49 | // Ver is version information. 50 | var Ver bool 51 | 52 | // Debug is debug mode. 53 | var Debug bool 54 | 55 | // OutFormat is output format. 56 | var OutFormat = "CSV" 57 | 58 | // OutHeader is output header. 59 | var OutHeader bool 60 | 61 | // Skip is skip lines. 62 | var Skip int 63 | 64 | // Header is input header. 65 | var Header bool 66 | 67 | // OutFileName is output file name. 68 | var OutFileName string 69 | 70 | // OutSheetName is output sheet name. 71 | var OutSheetName string 72 | 73 | // OutCell is output cell name. 74 | var OutCell string 75 | 76 | // ClearSheet is clear sheet if true. 77 | var ClearSheet bool 78 | 79 | func init() { 80 | rootCmd.PersistentFlags().BoolVarP(&Ver, "version", "v", false, "display version information") 81 | rootCmd.PersistentFlags().BoolVarP(&Debug, "debug", "", false, "debug mode") 82 | 83 | // Input 84 | rootCmd.PersistentFlags().IntVarP(&Skip, "skip", "s", 0, "Skip the number of lines") 85 | rootCmd.PersistentFlags().BoolVarP(&Header, "header", "H", false, "Input header") 86 | // Output 87 | validOutFormats := []string{"GUESS", "CSV", "AT", "LTSV", "JSON", "JSONL", "TBLN", "RAW", "MD", "VF", "YAML", "XLSX"} 88 | outputFormats := fmt.Sprintf("Output Format[%s]", strings.Join(validOutFormats, "|")) 89 | rootCmd.PersistentFlags().StringVarP(&OutFormat, "out", "o", "GUESS", outputFormats) 90 | rootCmd.PersistentPreRunE = func(cmd *cobra.Command, args []string) error { 91 | format := strings.ToUpper(OutFormat) 92 | for _, valid := range validOutFormats { 93 | if format == valid { 94 | return nil 95 | } 96 | } 97 | return fmt.Errorf("invalid output format: %s", OutFormat) 98 | } 99 | // Register the completion function for the --out flag 100 | _ = rootCmd.RegisterFlagCompletionFunc("out", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { 101 | return validOutFormats, cobra.ShellCompDirectiveDefault 102 | }) 103 | rootCmd.PersistentFlags().StringVarP(&OutFileName, "out-file", "O", "", "File name to output to file") 104 | rootCmd.PersistentFlags().BoolVarP(&OutHeader, "out-header", "", false, "Output header") 105 | rootCmd.PersistentFlags().StringVarP(&OutSheetName, "out-sheet", "", "", "Sheet name to output to xlsx file") 106 | rootCmd.PersistentFlags().StringVarP(&OutCell, "out-cell", "", "", "Cell name to output to xlsx file") 107 | rootCmd.PersistentFlags().BoolVarP(&ClearSheet, "clear-sheet", "", false, "Clear sheet when outputting to xlsx file") 108 | } 109 | -------------------------------------------------------------------------------- /cmd/table.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | // tableCmd represents the table command. 10 | var tableCmd = &cobra.Command{ 11 | Use: "table", 12 | Short: "SQL(SELECT * FROM table) for xlsx", 13 | Long: `Execute SELECT * FROM table, assuming the xlsx file as a table.`, 14 | RunE: func(cmd *cobra.Command, args []string) error { 15 | if len(args) != 1 { 16 | return fmt.Errorf("please specify the xlsx file") 17 | } 18 | query := "SELECT * FROM " + args[0] 19 | return exec([]string{query}) 20 | }, 21 | } 22 | 23 | func init() { 24 | rootCmd.AddCommand(tableCmd) 25 | } 26 | -------------------------------------------------------------------------------- /cmd/xlsxsql/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/noborus/xlsxsql/cmd" 4 | 5 | // version represents the version 6 | var version = "dev" 7 | 8 | // revision set "git rev-parse --short HEAD" 9 | var revision = "HEAD" 10 | 11 | func main() { 12 | cmd.Execute(version, revision) 13 | } 14 | -------------------------------------------------------------------------------- /docs/xlsxsql.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noborus/xlsxsql/61e3ccb0d8df808f5507c7ae58c9b5746dfac29c/docs/xlsxsql.png -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/noborus/xlsxsql 2 | 3 | go 1.22.0 4 | 5 | toolchain go1.23.2 6 | 7 | require ( 8 | github.com/noborus/trdsql v1.1.0 9 | github.com/spf13/cobra v1.8.1 10 | github.com/xuri/excelize/v2 v2.9.0 11 | ) 12 | 13 | require ( 14 | filippo.io/edwards25519 v1.1.0 // indirect 15 | github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de // indirect 16 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 17 | github.com/dustin/go-humanize v1.0.1 // indirect 18 | github.com/go-sql-driver/mysql v1.8.1 // indirect 19 | github.com/goccy/go-yaml v1.15.10 // indirect 20 | github.com/google/uuid v1.6.0 // indirect 21 | github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect 22 | github.com/iancoleman/orderedmap v0.3.0 // indirect 23 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 24 | github.com/itchyny/gojq v0.12.17 // indirect 25 | github.com/itchyny/timefmt-go v0.1.6 // indirect 26 | github.com/jwalton/gchalk v1.3.0 // indirect 27 | github.com/jwalton/go-supportscolor v1.2.0 // indirect 28 | github.com/klauspost/compress v1.17.11 // indirect 29 | github.com/lib/pq v1.10.9 // indirect 30 | github.com/mattn/go-isatty v0.0.20 // indirect 31 | github.com/mattn/go-runewidth v0.0.16 // indirect 32 | github.com/mattn/go-sqlite3 v1.14.24 // indirect 33 | github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect 34 | github.com/multiprocessio/go-sqlite3-stdlib v0.0.0-20220822170115-9f6825a1cd25 // indirect 35 | github.com/ncruces/go-strftime v0.1.9 // indirect 36 | github.com/noborus/guesswidth v0.4.0 // indirect 37 | github.com/noborus/sqlss v0.1.0 // indirect 38 | github.com/noborus/tbln v0.0.2 // indirect 39 | github.com/olekukonko/tablewriter v0.0.5 // indirect 40 | github.com/pierrec/lz4/v4 v4.1.22 // indirect 41 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect 42 | github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect 43 | github.com/richardlehane/mscfb v1.0.4 // indirect 44 | github.com/richardlehane/msoleps v1.0.4 // indirect 45 | github.com/rivo/uniseg v0.4.7 // indirect 46 | github.com/spf13/pflag v1.0.5 // indirect 47 | github.com/ulikunitz/xz v0.5.12 // indirect 48 | github.com/xuri/efp v0.0.0-20241211021726-c4e992084aa6 // indirect 49 | github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7 // indirect 50 | golang.org/x/crypto v0.31.0 // indirect 51 | golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67 // indirect 52 | golang.org/x/net v0.32.0 // indirect 53 | golang.org/x/sys v0.28.0 // indirect 54 | golang.org/x/term v0.27.0 // indirect 55 | golang.org/x/text v0.21.0 // indirect 56 | gonum.org/v1/gonum v0.15.1 // indirect 57 | modernc.org/gc/v3 v3.0.0-20241213165251-3bc300f6d0c9 // indirect 58 | modernc.org/libc v1.61.4 // indirect 59 | modernc.org/mathutil v1.6.0 // indirect 60 | modernc.org/memory v1.8.0 // indirect 61 | modernc.org/sqlite v1.34.2 // indirect 62 | modernc.org/strutil v1.2.0 // indirect 63 | modernc.org/token v1.1.0 // indirect 64 | ) 65 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= 2 | filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= 3 | github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de h1:FxWPpzIjnTlhPwqqXc4/vE0f7GvRjuAsbW+HOIe8KnA= 4 | github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de/go.mod h1:DCaWoUhZrYW9p1lxo/cm8EmUOOzAPSEZNGF2DK1dJgw= 5 | github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 6 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= 8 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 9 | github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= 10 | github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= 11 | github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= 12 | github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= 13 | github.com/goccy/go-yaml v1.15.10 h1:9exV2CDYm/FWHPptIIgcDiPQS+X/4uTR+HEl+GF9xJU= 14 | github.com/goccy/go-yaml v1.15.10/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= 15 | github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo= 16 | github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw= 17 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 18 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 19 | github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= 20 | github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= 21 | github.com/iancoleman/orderedmap v0.3.0 h1:5cbR2grmZR/DiVt+VJopEhtVs9YGInGIxAoMJn+Ichc= 22 | github.com/iancoleman/orderedmap v0.3.0/go.mod h1:XuLcCUkdL5owUCQeF2Ue9uuw1EptkJDkXXS7VoV7XGE= 23 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 24 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 25 | github.com/itchyny/gojq v0.12.17 h1:8av8eGduDb5+rvEdaOO+zQUjA04MS0m3Ps8HiD+fceg= 26 | github.com/itchyny/gojq v0.12.17/go.mod h1:WBrEMkgAfAGO1LUcGOckBl5O726KPp+OlkKug0I/FEY= 27 | github.com/itchyny/timefmt-go v0.1.6 h1:ia3s54iciXDdzWzwaVKXZPbiXzxxnv1SPGFfM/myJ5Q= 28 | github.com/itchyny/timefmt-go v0.1.6/go.mod h1:RRDZYC5s9ErkjQvTvvU7keJjxUYzIISJGxm9/mAERQg= 29 | github.com/jwalton/gchalk v1.3.0 h1:uTfAaNexN8r0I9bioRTksuT8VGjrPs9YIXR1PQbtX/Q= 30 | github.com/jwalton/gchalk v1.3.0/go.mod h1:ytRlj60R9f7r53IAElbpq4lVuPOPNg2J4tJcCxtFqr8= 31 | github.com/jwalton/go-supportscolor v1.1.0/go.mod h1:hFVUAZV2cWg+WFFC4v8pT2X/S2qUUBYMioBD9AINXGs= 32 | github.com/jwalton/go-supportscolor v1.2.0 h1:g6Ha4u7Vm3LIsQ5wmeBpS4gazu0UP1DRDE8y6bre4H8= 33 | github.com/jwalton/go-supportscolor v1.2.0/go.mod h1:hFVUAZV2cWg+WFFC4v8pT2X/S2qUUBYMioBD9AINXGs= 34 | github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= 35 | github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= 36 | github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= 37 | github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 38 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 39 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 40 | github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= 41 | github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= 42 | github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= 43 | github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 44 | github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM= 45 | github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= 46 | github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= 47 | github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= 48 | github.com/multiprocessio/go-sqlite3-stdlib v0.0.0-20220822170115-9f6825a1cd25 h1:bnhGk2UFFPqylhxTEffs1ehDRn4bEZsEoDH53Z4HqA8= 49 | github.com/multiprocessio/go-sqlite3-stdlib v0.0.0-20220822170115-9f6825a1cd25/go.mod h1:RrGEZqqiyEcLyTVLDSgtNZVLqJykj0F4vwuuqvMdT60= 50 | github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= 51 | github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= 52 | github.com/noborus/guesswidth v0.4.0 h1:+PPh+Z+GM4mKmVrhYR4lpjeyBuLMSVo2arM+VErdHIc= 53 | github.com/noborus/guesswidth v0.4.0/go.mod h1:ghA6uh9RcK+uSmaDDmBMj/tRZ3BSpspDP6DMF5Xk3bc= 54 | github.com/noborus/sqlss v0.1.0 h1:GSrOeBuswHaBn6enegZeMVudPPeyVoYo+LCapTc+b7Q= 55 | github.com/noborus/sqlss v0.1.0/go.mod h1:34KdYx3QxMFfD05RhUi7Uw5M1i6KOBQ1NHtMIuNVnWM= 56 | github.com/noborus/tbln v0.0.2 h1:pQIv+ZO38KPz52FOuhs/W3inpgmd5qwL8XFDqI+KKyY= 57 | github.com/noborus/tbln v0.0.2/go.mod h1:kS3WhEDRJhNwF3+aRGl9iaUzu/r3lExDagcPPENtNQ0= 58 | github.com/noborus/trdsql v1.1.0 h1:Yf3ThX3cuEGFR6/DR+fa5Vxa+qJglcbhAIIYvWJOblY= 59 | github.com/noborus/trdsql v1.1.0/go.mod h1:lgl2mIhA9wH1eflNYorasdu1LYCLzlgwUiQH2blokN8= 60 | github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= 61 | github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= 62 | github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU= 63 | github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= 64 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 65 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= 66 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 67 | github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= 68 | github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= 69 | github.com/richardlehane/mscfb v1.0.4 h1:WULscsljNPConisD5hR0+OyZjwK46Pfyr6mPu5ZawpM= 70 | github.com/richardlehane/mscfb v1.0.4/go.mod h1:YzVpcZg9czvAuhk9T+a3avCpcFPMUWm7gK3DypaEsUk= 71 | github.com/richardlehane/msoleps v1.0.1/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg= 72 | github.com/richardlehane/msoleps v1.0.4 h1:WuESlvhX3gH2IHcd8UqyCuFY5yiq/GR/yqaSM/9/g00= 73 | github.com/richardlehane/msoleps v1.0.4/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg= 74 | github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 75 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 76 | github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= 77 | github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 78 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 79 | github.com/scylladb/termtables v0.0.0-20191203121021-c4c0b6d42ff4/go.mod h1:C1a7PQSMz9NShzorzCiG2fk9+xuCgLkPeCvMHYR2OWg= 80 | github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= 81 | github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= 82 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 83 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 84 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 85 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 86 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 87 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 88 | github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc= 89 | github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= 90 | github.com/xuri/efp v0.0.0-20241211021726-c4e992084aa6 h1:8m6DWBG+dlFNbx5ynvrE7NgI+Y7OlZVMVTpayoW+rCc= 91 | github.com/xuri/efp v0.0.0-20241211021726-c4e992084aa6/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI= 92 | github.com/xuri/excelize/v2 v2.9.0 h1:1tgOaEq92IOEumR1/JfYS/eR0KHOCsRv/rYXXh6YJQE= 93 | github.com/xuri/excelize/v2 v2.9.0/go.mod h1:uqey4QBZ9gdMeWApPLdhm9x+9o2lq4iVmjiLfBS5hdE= 94 | github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7 h1:hPVCafDV85blFTabnqKgNhDCkJX25eik94Si9cTER4A= 95 | github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ= 96 | golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 97 | golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= 98 | golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= 99 | golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67 h1:1UoZQm6f0P/ZO0w1Ri+f+ifG/gXhegadRdwBIXEFWDo= 100 | golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c= 101 | golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ= 102 | golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E= 103 | golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4= 104 | golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= 105 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 106 | golang.org/x/net v0.32.0 h1:ZqPmj8Kzc+Y6e0+skZsuACbx+wzMgo5MQsJh9Qd6aYI= 107 | golang.org/x/net v0.32.0/go.mod h1:CwU0IoeOlnQQWJ6ioyFrfRuomB8GKF6KbYXZVyeXNfs= 108 | golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= 109 | golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 110 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 111 | golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 112 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 113 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 114 | golang.org/x/sys v0.0.0-20211004093028-2c5d950f24ef/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 115 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 116 | golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= 117 | golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 118 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 119 | golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 120 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 121 | golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q= 122 | golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= 123 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 124 | golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= 125 | golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= 126 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 127 | golang.org/x/tools v0.28.0 h1:WuB6qZ4RPCQo5aP3WdKZS7i595EdWqWR8vqJTlwTVK8= 128 | golang.org/x/tools v0.28.0/go.mod h1:dcIOrVd3mfQKTgrDVQHqCPMWy6lnhfhtX3hLXYVLfRw= 129 | gonum.org/v1/gonum v0.15.1 h1:FNy7N6OUZVUaWG9pTiD+jlhdQ3lMP+/LcTpJ6+a8sQ0= 130 | gonum.org/v1/gonum v0.15.1/go.mod h1:eZTZuRFrzu5pcyjN5wJhcIhnUdNijYxX1T2IcrOGY0o= 131 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 132 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 133 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 134 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 135 | modernc.org/cc/v4 v4.23.1 h1:WqJoPL3x4cUufQVHkXpXX7ThFJ1C4ik80i2eXEXbhD8= 136 | modernc.org/cc/v4 v4.23.1/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ= 137 | modernc.org/ccgo/v4 v4.23.1 h1:N49a7JiWGWV7lkPE4yYcvjkBGZQi93/JabRYjdWmJXc= 138 | modernc.org/ccgo/v4 v4.23.1/go.mod h1:JoIUegEIfutvoWV/BBfDFpPpfR2nc3U0jKucGcbmwDU= 139 | modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE= 140 | modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ= 141 | modernc.org/gc/v2 v2.5.0 h1:bJ9ChznK1L1mUtAQtxi0wi5AtAs5jQuw4PrPHO5pb6M= 142 | modernc.org/gc/v2 v2.5.0/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU= 143 | modernc.org/gc/v3 v3.0.0-20241213165251-3bc300f6d0c9 h1:ovz6yUKX71igz2yvk4NpiCL5fvdjZAI+DhuDEGx1xyU= 144 | modernc.org/gc/v3 v3.0.0-20241213165251-3bc300f6d0c9/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4= 145 | modernc.org/libc v1.61.4 h1:wVyqEx6tlltte9lPTjq0kDAdtdM9c4JH8rU6M1ZVawA= 146 | modernc.org/libc v1.61.4/go.mod h1:VfXVuM/Shh5XsMNrh3C6OkfL78G3loa4ZC/Ljv9k7xc= 147 | modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4= 148 | modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo= 149 | modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E= 150 | modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU= 151 | modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4= 152 | modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= 153 | modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc= 154 | modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss= 155 | modernc.org/sqlite v1.34.2 h1:J9n76TPsfYYkFkZ9Uy1QphILYifiVEwwOT7yP5b++2Y= 156 | modernc.org/sqlite v1.34.2/go.mod h1:dnR723UrTtjKpoHCAMN0Q/gZ9MT4r+iRvIBb9umWFkU= 157 | modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA= 158 | modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0= 159 | modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= 160 | modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= 161 | -------------------------------------------------------------------------------- /reader.go: -------------------------------------------------------------------------------- 1 | // Package xlsxsql provides a reader for XLSX files. 2 | // It uses the trdsql and excelize/v2 packages to read XLSX files and convert them into SQL tables. 3 | // The main type is XLSXReader, which implements the trdsql.Reader interface. 4 | package xlsxsql 5 | 6 | import ( 7 | "fmt" 8 | "io" 9 | "strings" 10 | 11 | "github.com/noborus/trdsql" 12 | "github.com/xuri/excelize/v2" 13 | ) 14 | 15 | var ( 16 | ErrSheetNotFound = fmt.Errorf("sheet not found") 17 | ErrNoData = fmt.Errorf("no data") 18 | ) 19 | 20 | // XLSXReader is a reader for XLSX files. 21 | type XLSXReader struct { 22 | names []string 23 | types []string 24 | body [][]any 25 | } 26 | 27 | // NewXLSXReader function takes an io.Reader and trdsql.ReadOpts, and returns a new XLSXReader. 28 | // It reads the XLSX file, retrieves the sheet specified by the InJQuery option, and reads the rows into the XLSXReader. 29 | func NewXLSXReader(reader io.Reader, opts *trdsql.ReadOpts) (trdsql.Reader, error) { 30 | f, err := excelize.OpenReader(reader) 31 | if err != nil { 32 | return nil, err 33 | } 34 | extSheet, extCell := parseExtend(opts.InJQuery) 35 | sheet, err := getSheet(f, extSheet) 36 | if err != nil { 37 | return nil, err 38 | } 39 | 40 | isFilter := false 41 | cellX, cellY := 0, 0 42 | if extCell != "" { 43 | cellX, cellY, err = excelize.CellNameToCoordinates(extCell) 44 | if err != nil { 45 | return nil, err 46 | } 47 | isFilter = true 48 | cellX-- 49 | cellY-- 50 | } 51 | 52 | rows, err := f.GetRows(sheet) 53 | if err != nil { 54 | return nil, err 55 | } 56 | 57 | r := XLSXReader{} 58 | skip := cellY 59 | if opts.InSkip > 0 { 60 | skip = opts.InSkip 61 | } 62 | 63 | columnNum := 0 64 | header := 0 65 | for i := 0; i < len(rows); i++ { 66 | if i < skip { 67 | header = i + 1 68 | continue 69 | } 70 | row := rows[i] 71 | columnNum = max(columnNum, len(row)-cellX) 72 | if i > opts.InPreRead { 73 | break 74 | } 75 | } 76 | if columnNum <= 0 { 77 | return nil, ErrNoData 78 | } 79 | 80 | if header > len(rows) { 81 | header = 0 82 | } else { 83 | if opts.InHeader { 84 | skip++ 85 | } 86 | } 87 | 88 | r.names, r.types = nameType(rows[header], cellX, cellY, columnNum, opts.InHeader) 89 | rowNum := len(rows) - skip 90 | body := make([][]any, 0, rowNum) 91 | validColumns := make([]bool, columnNum) 92 | for i := 0; i < len(r.names); i++ { 93 | if r.names[i] != "" { 94 | validColumns[i] = true 95 | } else { 96 | name, err := cellName(cellX+i, cellY) 97 | if err != nil { 98 | return nil, err 99 | } 100 | r.names[i] = name 101 | } 102 | } 103 | for j, row := range rows { 104 | if j < skip { 105 | continue 106 | } 107 | data := make([]any, columnNum) 108 | for c, i := 0, cellX; i < len(row); i++ { 109 | if c >= columnNum { 110 | break 111 | } 112 | data[c] = row[i] 113 | if data[c] != "" { 114 | validColumns[c] = true 115 | } 116 | c++ 117 | } 118 | body = append(body, data) 119 | } 120 | 121 | if !isFilter { 122 | r.body = body 123 | return r, nil 124 | } 125 | 126 | r.body = filterColumns(body, validColumns) 127 | if len(r.body) == 0 { 128 | return nil, ErrNoData 129 | } 130 | r.names = r.names[:len(r.body[0])] 131 | r.types = r.types[:len(r.body[0])] 132 | return r, nil 133 | } 134 | 135 | func cellName(x int, y int) (string, error) { 136 | cn, err := excelize.CoordinatesToCellName(x+1, y+1) 137 | if err != nil { 138 | return "", err 139 | } 140 | return cn, nil 141 | } 142 | 143 | func filterColumns(src [][]any, validColumns []bool) [][]any { 144 | num := columnNum(validColumns) 145 | dst := make([][]any, 0, len(src)) 146 | startRow := false 147 | for _, row := range src { 148 | cols := make([]any, num) 149 | valid := false 150 | for i := 0; i < num; i++ { 151 | cols[i] = row[i] 152 | if cols[i] != nil && cols[i] != "" { 153 | valid = true 154 | } 155 | } 156 | if valid { 157 | startRow = true 158 | dst = append(dst, cols) 159 | continue 160 | } 161 | if startRow { 162 | break 163 | } else { 164 | continue 165 | } 166 | } 167 | return dst 168 | } 169 | 170 | func columnNum(validColumns []bool) int { 171 | count := len(validColumns) 172 | startCol := false 173 | for i, f := range validColumns { 174 | if f { 175 | startCol = true 176 | } 177 | if startCol && !f { 178 | count = i 179 | break 180 | } 181 | } 182 | return count 183 | } 184 | 185 | func parseExtend(ext string) (string, string) { 186 | e := strings.Split(ext, ".") 187 | if len(e) == 1 { 188 | return e[0], "" 189 | } else if len(e) == 2 { 190 | return e[0], e[1] 191 | } else { 192 | return e[0], strings.Join(e[1:], ".") 193 | } 194 | } 195 | 196 | func nameType(row []string, cellX int, cellY int, columnNum int, header bool) ([]string, []string) { 197 | nameMap := make(map[string]bool) 198 | names := make([]string, columnNum) 199 | types := make([]string, columnNum) 200 | c := 0 201 | for i := cellX; i < cellX+columnNum; i++ { 202 | if header && len(row) > i && row[i] != "" { 203 | if _, ok := nameMap[row[i]]; ok { 204 | name, err := cellName(cellX+i, cellY) 205 | if err != nil { 206 | names[c] = row[i] + "_" + fmt.Sprint(i) 207 | } else { 208 | names[c] = name 209 | } 210 | } else { 211 | nameMap[row[i]] = true 212 | names[c] = row[i] 213 | } 214 | } else { 215 | names[c] = "" 216 | } 217 | types[c] = "text" 218 | c++ 219 | } 220 | return names, types 221 | } 222 | 223 | func getSheet(f *excelize.File, sheet string) (string, error) { 224 | list := f.GetSheetList() 225 | if len(list) == 0 { 226 | return "", ErrSheetNotFound 227 | } 228 | if sheet == "" { 229 | sheet = list[0] 230 | } 231 | for _, s := range list { 232 | if s == sheet { 233 | return s, nil 234 | } 235 | } 236 | return "", ErrSheetNotFound 237 | } 238 | 239 | // Names returns the column names of the XLSX file. 240 | func (r XLSXReader) Names() ([]string, error) { 241 | return r.names, nil 242 | } 243 | 244 | // Types returns the column types of the XLSX file. 245 | func (r XLSXReader) Types() ([]string, error) { 246 | return r.types, nil 247 | } 248 | 249 | // PreReadRow returns the rows of the XLSX file. 250 | func (r XLSXReader) PreReadRow() [][]any { 251 | return r.body 252 | } 253 | 254 | // ReadRow only returns EOF. 255 | func (r XLSXReader) ReadRow(row []any) ([]any, error) { 256 | return nil, io.EOF 257 | } 258 | 259 | // XLSXSheet returns the sheet name of the XLSX file. 260 | func XLSXSheet(fileName string) ([]string, error) { 261 | f, err := excelize.OpenFile(fileName) 262 | if err != nil { 263 | return nil, err 264 | } 265 | return f.GetSheetList(), nil 266 | } 267 | 268 | func init() { 269 | // Use XLSXReader for extension xlsx. 270 | trdsql.RegisterReaderFunc("XLSX", NewXLSXReader) 271 | } 272 | -------------------------------------------------------------------------------- /reader_test.go: -------------------------------------------------------------------------------- 1 | // Package xlsxsql provides a reader for XLSX files. 2 | // It uses the trdsql and excelize/v2 packages to read XLSX files and convert them into SQL tables. 3 | // The main type is XLSXReader, which implements the trdsql.Reader interface. 4 | package xlsxsql 5 | 6 | import ( 7 | "os" 8 | "reflect" 9 | "testing" 10 | 11 | "github.com/noborus/trdsql" 12 | ) 13 | 14 | func TestXLSXSheet(t *testing.T) { 15 | type args struct { 16 | fileName string 17 | } 18 | tests := []struct { 19 | name string 20 | args args 21 | want []string 22 | wantErr bool 23 | }{ 24 | { 25 | "test1", 26 | args{"testdata/test1.xlsx"}, 27 | []string{"Sheet1"}, 28 | false, 29 | }, 30 | { 31 | "test2", 32 | args{"testdata/test2.xlsx"}, 33 | []string{"Sheet1", "Sheet2"}, 34 | false, 35 | }, 36 | } 37 | for _, tt := range tests { 38 | t.Run(tt.name, func(t *testing.T) { 39 | got, err := XLSXSheet(tt.args.fileName) 40 | if (err != nil) != tt.wantErr { 41 | t.Errorf("XLSXSheet() error = %v, wantErr %v", err, tt.wantErr) 42 | return 43 | } 44 | if !reflect.DeepEqual(got, tt.want) { 45 | t.Errorf("XLSXSheet() = %v, want %v", got, tt.want) 46 | } 47 | }) 48 | } 49 | } 50 | 51 | type xlsxReader struct { 52 | fileName string 53 | opts *trdsql.ReadOpts 54 | } 55 | 56 | func createXLSXReader(t *testing.T, xlsx xlsxReader) (trdsql.Reader, error) { 57 | t.Helper() 58 | reader, err := os.Open(xlsx.fileName) 59 | if err != nil { 60 | t.Errorf("os.Open() error = %v", err) 61 | return nil, err 62 | } 63 | r, err := NewXLSXReader(reader, xlsx.opts) 64 | if err != nil { 65 | t.Errorf("NewXLSXReader() error = %v", err) 66 | return nil, err 67 | } 68 | return r, nil 69 | } 70 | 71 | func TestXLSXReader_PreReadRow(t *testing.T) { 72 | tests := []struct { 73 | name string 74 | xlsx xlsxReader 75 | want [][]any 76 | }{ 77 | { 78 | "test1", 79 | xlsxReader{ 80 | fileName: "testdata/test1.xlsx", 81 | opts: &trdsql.ReadOpts{InPreRead: 1}, 82 | }, 83 | [][]any{ 84 | {"1", "a"}, 85 | {"2", "b"}, 86 | {"3", "c"}, 87 | {"4", "d"}, 88 | {"5", "e"}, 89 | {"6", "f"}, 90 | }, 91 | }, 92 | { 93 | "test2", 94 | xlsxReader{ 95 | fileName: "testdata/test2.xlsx", 96 | opts: &trdsql.ReadOpts{ 97 | InHeader: true, 98 | InPreRead: 1, 99 | }, 100 | }, 101 | [][]any{ 102 | {"1", "apple"}, 103 | {"2", "orange"}, 104 | {"3", "melon"}, 105 | }, 106 | }, 107 | { 108 | "test3", 109 | xlsxReader{ 110 | fileName: "testdata/test3.xlsx", 111 | opts: &trdsql.ReadOpts{ 112 | InHeader: true, 113 | InJQuery: "Sheet1.C1", 114 | }, 115 | }, 116 | [][]any{ 117 | {"1", "apple"}, 118 | {"2", "orange"}, 119 | {"3", "melon"}, 120 | }, 121 | }, 122 | } 123 | for _, tt := range tests { 124 | t.Run(tt.name, func(t *testing.T) { 125 | r, err := createXLSXReader(t, tt.xlsx) 126 | if err != nil { 127 | t.Errorf("createXLSXReader() error = %v", err) 128 | return 129 | } 130 | if got := r.PreReadRow(); !reflect.DeepEqual(got, tt.want) { 131 | t.Errorf("XLSXReader.PreReadRow() = %v, want %v", got, tt.want) 132 | } 133 | }) 134 | } 135 | } 136 | 137 | func TestXLSXReader_Names(t *testing.T) { 138 | tests := []struct { 139 | name string 140 | xlsx xlsxReader 141 | want []string 142 | wantErr bool 143 | }{ 144 | { 145 | "test1", 146 | xlsxReader{ 147 | fileName: "testdata/test1.xlsx", 148 | opts: &trdsql.ReadOpts{InPreRead: 1}, 149 | }, 150 | []string{"A1", "B1"}, 151 | false, 152 | }, 153 | { 154 | "test2", 155 | xlsxReader{ 156 | fileName: "testdata/test2.xlsx", 157 | opts: &trdsql.ReadOpts{ 158 | InHeader: true, 159 | InPreRead: 1, 160 | }, 161 | }, 162 | []string{"id", "name"}, 163 | false, 164 | }, 165 | } 166 | for _, tt := range tests { 167 | t.Run(tt.name, func(t *testing.T) { 168 | r, err := createXLSXReader(t, tt.xlsx) 169 | if err != nil { 170 | t.Errorf("createXLSXReader() error = %v", err) 171 | return 172 | } 173 | got, err := r.Names() 174 | if (err != nil) != tt.wantErr { 175 | t.Errorf("XLSXReader.Names() error = %v, wantErr %v", err, tt.wantErr) 176 | return 177 | } 178 | if !reflect.DeepEqual(got, tt.want) { 179 | t.Errorf("XLSXReader.Names() = %v, want %v", got, tt.want) 180 | } 181 | }) 182 | } 183 | } 184 | 185 | func TestXLSXReader_Types(t *testing.T) { 186 | tests := []struct { 187 | name string 188 | xlsx xlsxReader 189 | want []string 190 | wantErr bool 191 | }{ 192 | { 193 | "test1", 194 | xlsxReader{ 195 | fileName: "testdata/test1.xlsx", 196 | opts: &trdsql.ReadOpts{InPreRead: 1}, 197 | }, 198 | []string{"text", "text"}, 199 | false, 200 | }, 201 | { 202 | "test2", 203 | xlsxReader{ 204 | fileName: "testdata/test2.xlsx", 205 | opts: &trdsql.ReadOpts{ 206 | InHeader: true, 207 | InPreRead: 1, 208 | }, 209 | }, 210 | []string{"text", "text"}, 211 | false, 212 | }, 213 | } 214 | for _, tt := range tests { 215 | t.Run(tt.name, func(t *testing.T) { 216 | r, err := createXLSXReader(t, tt.xlsx) 217 | if err != nil { 218 | t.Errorf("createXLSXReader() error = %v", err) 219 | return 220 | } 221 | got, err := r.Types() 222 | if (err != nil) != tt.wantErr { 223 | t.Errorf("XLSXReader.Types() error = %v, wantErr %v", err, tt.wantErr) 224 | return 225 | } 226 | if !reflect.DeepEqual(got, tt.want) { 227 | t.Errorf("XLSXReader.Types() = %v, want %v", got, tt.want) 228 | } 229 | }) 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /testdata/test1.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noborus/xlsxsql/61e3ccb0d8df808f5507c7ae58c9b5746dfac29c/testdata/test1.xlsx -------------------------------------------------------------------------------- /testdata/test2.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noborus/xlsxsql/61e3ccb0d8df808f5507c7ae58c9b5746dfac29c/testdata/test2.xlsx -------------------------------------------------------------------------------- /testdata/test3.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noborus/xlsxsql/61e3ccb0d8df808f5507c7ae58c9b5746dfac29c/testdata/test3.xlsx -------------------------------------------------------------------------------- /writer.go: -------------------------------------------------------------------------------- 1 | package xlsxsql 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io" 7 | "os" 8 | "strings" 9 | 10 | "github.com/xuri/excelize/v2" 11 | ) 12 | 13 | var ErrInvalidFileName = errors.New("file name must end with .xlsx") 14 | 15 | // XLSXWriter is a writer for XLSX files. 16 | type XLSXWriter struct { 17 | fileName string 18 | f *excelize.File 19 | sheet string 20 | header bool 21 | cellX int 22 | cellY int 23 | rowID int 24 | } 25 | 26 | // WriteOpts represents options that determine the behavior of the writer. 27 | type WriteOpts struct { 28 | // ErrStream is the error output destination. 29 | ErrStream io.Writer 30 | // FileName is the output file name. 31 | FileName string 32 | // Sheet is the sheet name. 33 | Sheet string 34 | // Cell is the cell name. 35 | Cell string 36 | // ClearSheet is the flag to clear the sheet. 37 | ClearSheet bool 38 | // WriteHeader is the flag to write the header. 39 | Header bool 40 | } 41 | 42 | // WriteOpt is a function to set WriteOpts. 43 | type WriteOpt func(*WriteOpts) 44 | 45 | // ErrStream sets the error output destination. 46 | func ErrStream(f io.Writer) WriteOpt { 47 | return func(args *WriteOpts) { 48 | args.ErrStream = f 49 | } 50 | } 51 | 52 | // FileName sets the output file name. 53 | func FileName(f string) WriteOpt { 54 | return func(args *WriteOpts) { 55 | args.FileName = f 56 | } 57 | } 58 | 59 | // Sheet sets the sheet name. 60 | func Sheet(f string) WriteOpt { 61 | return func(args *WriteOpts) { 62 | args.Sheet = f 63 | } 64 | } 65 | 66 | // Cell sets the cell name. 67 | func Cell(f string) WriteOpt { 68 | return func(args *WriteOpts) { 69 | args.Cell = f 70 | } 71 | } 72 | 73 | // ClearSheet sets the flag to clear the sheet. 74 | func ClearSheet(f bool) WriteOpt { 75 | return func(args *WriteOpts) { 76 | args.ClearSheet = f 77 | } 78 | } 79 | 80 | func Header(f bool) WriteOpt { 81 | return func(args *WriteOpts) { 82 | args.Header = f 83 | } 84 | } 85 | 86 | // NewXLSXWriter function takes an io.Writer and trdsql.WriteOpts, and returns a new XLSXWriter. 87 | func NewXLSXWriter(options ...WriteOpt) (*XLSXWriter, error) { 88 | writeOpts := &WriteOpts{ 89 | ErrStream: os.Stderr, 90 | FileName: "", 91 | Sheet: "Sheet1", 92 | Cell: "", 93 | ClearSheet: false, 94 | Header: true, 95 | } 96 | for _, option := range options { 97 | option(writeOpts) 98 | } 99 | 100 | f, err := openXLSXFile(writeOpts.FileName) 101 | if err != nil { 102 | return nil, err 103 | } 104 | cellX, cellY := getCell(writeOpts.Cell) 105 | 106 | n, err := f.GetSheetIndex(writeOpts.Sheet) 107 | if err != nil { 108 | return nil, err 109 | } 110 | // Only attempt to clear the sheet if it exists. 111 | if writeOpts.ClearSheet && n >= 0 { 112 | // Sheet exists,clear it 113 | if err := clearSheet(f, writeOpts.Sheet); err != nil { 114 | return nil, err 115 | } 116 | } 117 | 118 | if n < 0 { 119 | // Sheet does not exist, create a new one 120 | if _, err := f.NewSheet(writeOpts.Sheet); err != nil { 121 | return nil, err 122 | } 123 | } 124 | 125 | return &XLSXWriter{ 126 | fileName: writeOpts.FileName, 127 | f: f, 128 | cellX: cellX, 129 | cellY: cellY, 130 | sheet: writeOpts.Sheet, 131 | header: writeOpts.Header, 132 | }, nil 133 | } 134 | 135 | // openXLSXFile function opens the XLSX file. 136 | func openXLSXFile(fileName string) (*excelize.File, error) { 137 | var f *excelize.File 138 | var err error 139 | 140 | // Check if file name ends with .xlsx 141 | if !strings.HasSuffix(fileName, ".xlsx") { 142 | return nil, fmt.Errorf("%w: [%s]", ErrInvalidFileName, fileName) 143 | } 144 | 145 | if _, err = os.Stat(fileName); err != nil && !os.IsNotExist(err) { 146 | return nil, err 147 | } 148 | if os.IsNotExist(err) { 149 | // File does not exist, create a new one 150 | f = excelize.NewFile() 151 | } else { 152 | // File exists, open it 153 | f, err = excelize.OpenFile(fileName) 154 | if err != nil { 155 | return nil, err 156 | } 157 | } 158 | return f, nil 159 | } 160 | 161 | func getCell(cellName string) (int, int) { 162 | if cellName == "" { 163 | return 0, 0 164 | } 165 | x, y, err := excelize.CellNameToCoordinates(cellName) 166 | if err != nil { 167 | return 0, 0 168 | } 169 | return x - 1, y - 1 170 | } 171 | 172 | // clearSheet function clears the sheet. 173 | func clearSheet(f *excelize.File, sheet string) error { 174 | rows, err := f.GetRows(sheet) 175 | if err != nil { 176 | return err 177 | } 178 | 179 | for i, row := range rows { 180 | for j := range row { 181 | axis, err := excelize.CoordinatesToCellName(j+1, i+1) 182 | if err != nil { 183 | return err 184 | } 185 | if err := f.SetCellStr(sheet, axis, ""); err != nil { 186 | return err 187 | } 188 | } 189 | } 190 | return nil 191 | } 192 | 193 | // PreWrite function opens the XLSXWriter. 194 | func (w *XLSXWriter) PreWrite(columns []string, types []string) error { 195 | if !w.header { 196 | return nil 197 | } 198 | // Write header 199 | for i, v := range columns { 200 | cell, err := excelize.CoordinatesToCellName(w.cellX+i+1, w.cellY+1) 201 | if err != nil { 202 | return err 203 | } 204 | if err := w.f.SetCellValue(w.sheet, cell, v); err != nil { 205 | return err 206 | } 207 | } 208 | w.rowID++ 209 | return nil 210 | } 211 | 212 | // WriteRow function writes a row to the XLSXWriter. 213 | func (w *XLSXWriter) WriteRow(row []any, columns []string) error { 214 | w.rowID++ 215 | for i, v := range row { 216 | if v == nil { 217 | continue 218 | } 219 | cell, err := excelize.CoordinatesToCellName(w.cellX+i+1, w.cellY+w.rowID) 220 | if err != nil { 221 | return err 222 | } 223 | if err := w.f.SetCellValue(w.sheet, cell, v); err != nil { 224 | return err 225 | } 226 | } 227 | return nil 228 | } 229 | 230 | // PostWrite function closes the XLSXWriter. 231 | func (w *XLSXWriter) PostWrite() error { 232 | return w.f.SaveAs(w.fileName) 233 | } 234 | -------------------------------------------------------------------------------- /writer_test.go: -------------------------------------------------------------------------------- 1 | package xlsxsql 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | "github.com/xuri/excelize/v2" 8 | ) 9 | 10 | func TestXLSXWriter_WriteRow(t *testing.T) { 11 | type opts struct { 12 | fileName string 13 | f *excelize.File 14 | sheet string 15 | cellName string 16 | } 17 | type args struct { 18 | row []any 19 | columns []string 20 | types []string 21 | } 22 | tests := []struct { 23 | name string 24 | fields opts 25 | args args 26 | wantErr bool 27 | wantRows [][]string 28 | }{ 29 | { 30 | name: "test1", 31 | fields: opts{ 32 | fileName: "dummy.xlsx", 33 | f: excelize.NewFile(), 34 | sheet: "Sheet1", 35 | cellName: "A1", 36 | }, 37 | args: args{ 38 | row: []any{"a", "b", "c"}, 39 | columns: []string{"A", "B", "C"}, 40 | types: []string{"string", "string", "string"}, 41 | }, 42 | wantErr: false, 43 | wantRows: [][]string{ 44 | {"A", "B", "C"}, 45 | {"a", "b", "c"}, 46 | }, 47 | }, 48 | { 49 | name: "test2", 50 | fields: opts{ 51 | fileName: "dummy.xlsx", 52 | f: excelize.NewFile(), 53 | sheet: "Sheet1", 54 | cellName: "A2", 55 | }, 56 | args: args{ 57 | row: []any{"a", "b", "c"}, 58 | columns: []string{"A", "B", "C"}, 59 | types: []string{"string", "string", "string"}, 60 | }, 61 | wantErr: false, 62 | wantRows: [][]string{ 63 | nil, 64 | {"A", "B", "C"}, 65 | {"a", "b", "c"}, 66 | }, 67 | }, 68 | } 69 | for _, tt := range tests { 70 | t.Run(tt.name, func(t *testing.T) { 71 | w, err := NewXLSXWriter( 72 | FileName(tt.fields.fileName), 73 | Sheet(tt.fields.sheet), 74 | Cell(tt.fields.cellName), 75 | ClearSheet(true), 76 | ) 77 | if err != nil { 78 | t.Fatal(err) 79 | } 80 | if err := w.PreWrite(tt.args.columns, tt.args.types); err != nil { 81 | t.Fatal(err) 82 | } 83 | if err := w.WriteRow(tt.args.row, tt.args.columns); (err != nil) != tt.wantErr { 84 | t.Errorf("XLSXWriter.WriteRow() error = %v, wantErr %v", err, tt.wantErr) 85 | } 86 | rows, err := w.f.GetRows(tt.fields.sheet) 87 | if err != nil { 88 | t.Fatal(err) 89 | } 90 | if !reflect.DeepEqual(rows, tt.wantRows) { 91 | t.Errorf("XLSXWriter.WriteRow() rows = %#v, wantRows %#v", rows, tt.wantRows) 92 | } 93 | }) 94 | } 95 | } 96 | --------------------------------------------------------------------------------