mirror of
https://github.com/nxshock/mssqlbulkloader.git
synced 2024-11-27 00:11:02 +05:00
Initial commit
This commit is contained in:
commit
917b473fa9
2
.gitattributes
vendored
Normal file
2
.gitattributes
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
# Auto detect text files and perform LF normalization
|
||||||
|
* text=auto
|
21
.gitignore
vendored
Normal file
21
.gitignore
vendored
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
# If you prefer the allow list template instead of the deny list, see community template:
|
||||||
|
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
|
||||||
|
#
|
||||||
|
# Binaries for programs and plugins
|
||||||
|
*.exe
|
||||||
|
*.exe~
|
||||||
|
*.dll
|
||||||
|
*.so
|
||||||
|
*.dylib
|
||||||
|
|
||||||
|
# Test binary, built with `go test -c`
|
||||||
|
*.test
|
||||||
|
|
||||||
|
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||||
|
*.out
|
||||||
|
|
||||||
|
# Dependency directories (remove the comment below to include it)
|
||||||
|
# vendor/
|
||||||
|
|
||||||
|
# Go workspace file
|
||||||
|
go.work
|
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2023 nxshock
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
209
app.go
Normal file
209
app.go
Normal file
@ -0,0 +1,209 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
_ "time/tzdata"
|
||||||
|
|
||||||
|
_ "github.com/denisenkom/go-mssqldb"
|
||||||
|
"github.com/urfave/cli"
|
||||||
|
)
|
||||||
|
|
||||||
|
var app = &cli.App{
|
||||||
|
Version: "2023.03.27",
|
||||||
|
Usage: "bulk loader into Microsoft SQL Server",
|
||||||
|
HideHelp: true,
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "filepath",
|
||||||
|
Usage: "input file path",
|
||||||
|
Required: true,
|
||||||
|
TakesFile: true},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "type",
|
||||||
|
Usage: "input file type",
|
||||||
|
Required: false,
|
||||||
|
Value: "auto",
|
||||||
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "encoding",
|
||||||
|
Usage: "input file encoding",
|
||||||
|
Required: false,
|
||||||
|
Value: "utf8",
|
||||||
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "sheetname",
|
||||||
|
Usage: "Excel file sheet name",
|
||||||
|
Required: false},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "server",
|
||||||
|
Usage: "database server address",
|
||||||
|
Value: "127.0.0.1"},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "database",
|
||||||
|
Usage: "database name",
|
||||||
|
Required: true},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "table",
|
||||||
|
Usage: "table name in schema.name format",
|
||||||
|
Required: true},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "fields",
|
||||||
|
Usage: "list of field types in [sifdt ]+ format",
|
||||||
|
Required: true},
|
||||||
|
&cli.BoolFlag{
|
||||||
|
Name: "create",
|
||||||
|
Usage: "create table"},
|
||||||
|
&cli.BoolFlag{
|
||||||
|
Name: "overwrite",
|
||||||
|
Usage: "overwrite existing table"},
|
||||||
|
&cli.IntFlag{
|
||||||
|
Name: "skiprows",
|
||||||
|
Usage: "number of rows to skip before read header"},
|
||||||
|
&cli.BoolFlag{
|
||||||
|
Name: "unknowncolumnnames",
|
||||||
|
Usage: "insert to table with unknown column names",
|
||||||
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "timezone",
|
||||||
|
Usage: "Time zone (IANA Time Zone database format)",
|
||||||
|
Value: "Local",
|
||||||
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "comma",
|
||||||
|
Usage: "CSV file delimiter",
|
||||||
|
Value: ",",
|
||||||
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "dateformat",
|
||||||
|
Usage: "date format (Go style)",
|
||||||
|
Value: "02.01.2006"},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "timestampformat",
|
||||||
|
Usage: "timestamp format (Go style)",
|
||||||
|
Value: "02.01.2006 15:04:05"},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "decompress",
|
||||||
|
Usage: "decompressor name for archived files",
|
||||||
|
},
|
||||||
|
&cli.BoolFlag{
|
||||||
|
Name: "silent",
|
||||||
|
Usage: "disable output",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Action: func(c *cli.Context) error {
|
||||||
|
initLogger(c.Bool("silent"))
|
||||||
|
|
||||||
|
var comma rune
|
||||||
|
if c.String("comma") == "\\t" {
|
||||||
|
comma = rune("\t"[0])
|
||||||
|
} else {
|
||||||
|
comma = rune(c.String("comma")[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
location, err := time.LoadLocation(c.String("timezone"))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("parse timezone: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
options := &Options{
|
||||||
|
filePath: c.String("filepath"),
|
||||||
|
fileType: c.String("type"),
|
||||||
|
sheetName: c.String("sheetname"),
|
||||||
|
server: c.String("server"),
|
||||||
|
database: c.String("database"),
|
||||||
|
tableName: c.String("table"),
|
||||||
|
fieldsTypes: c.String("fields"),
|
||||||
|
create: c.Bool("create"),
|
||||||
|
overwrite: c.Bool("overwrite"),
|
||||||
|
skipRows: c.Int("skiprows"),
|
||||||
|
encoding: c.String("encoding"),
|
||||||
|
dateFormat: c.String("dateformat"),
|
||||||
|
timestampFormat: c.String("timestampformat"),
|
||||||
|
timezone: location,
|
||||||
|
decompress: c.String("decompress"),
|
||||||
|
unknownColumnNames: c.Bool("unknowncolumnnames"),
|
||||||
|
silent: c.Bool("silent"),
|
||||||
|
comma: comma,
|
||||||
|
}
|
||||||
|
|
||||||
|
if options.decompress != "" {
|
||||||
|
var archiveType ArchiveType
|
||||||
|
err = archiveType.UnmarshalText([]byte(options.decompress))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
ar, err := archiveType.Open()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = ar.Process(options)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
f, err := os.Open(options.filePath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
err = process(f, options)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Print("Complete.")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}}
|
||||||
|
|
||||||
|
func process(r io.Reader, options *Options) error {
|
||||||
|
var fileType FileType
|
||||||
|
err := fileType.UnmarshalText([]byte(options.fileType))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
reader, err := fileType.Open(r, options)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer reader.Close()
|
||||||
|
|
||||||
|
db, err := sql.Open("sqlserver", fmt.Sprintf("sqlserver://%s?database=%s", options.server, options.database))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("open database: %w", err)
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
tx, err := db.Begin()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("begin transaction: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = prepareTable(reader, tx)
|
||||||
|
if err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
return fmt.Errorf("prepare table: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = insertData(reader, tx)
|
||||||
|
if err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
return fmt.Errorf("insert data: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = tx.Commit()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("commit transaction: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
50
archivetypes.go
Normal file
50
archivetypes.go
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ArchiveType int
|
||||||
|
|
||||||
|
const (
|
||||||
|
AutoDetectArchiveType ArchiveType = iota
|
||||||
|
Zip
|
||||||
|
)
|
||||||
|
|
||||||
|
type ArchiveProcessor interface {
|
||||||
|
Process(options *Options) error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ft ArchiveType) MarshalText() (text []byte, err error) {
|
||||||
|
switch ft {
|
||||||
|
case AutoDetectArchiveType:
|
||||||
|
return []byte("auto"), nil
|
||||||
|
case Zip:
|
||||||
|
return []byte("zip"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("unknown type id = %d", ft)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ft ArchiveType) Open() (ArchiveProcessor, error) {
|
||||||
|
switch ft {
|
||||||
|
case AutoDetectArchiveType:
|
||||||
|
case Zip:
|
||||||
|
return new(ZipReader), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("unknown type id = %d", ft)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ft *ArchiveType) UnmarshalText(text []byte) error {
|
||||||
|
switch string(text) {
|
||||||
|
case "auto":
|
||||||
|
*ft = AutoDetectArchiveType
|
||||||
|
return nil
|
||||||
|
case "zip":
|
||||||
|
*ft = Zip
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf(`unknown format code "%s"`, string(text))
|
||||||
|
}
|
66
charsets.go
Normal file
66
charsets.go
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"github.com/dimchansky/utfbom"
|
||||||
|
"golang.org/x/text/encoding/charmap"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Charset interface {
|
||||||
|
String(string) (string, error)
|
||||||
|
Reader(io.Reader) io.Reader
|
||||||
|
}
|
||||||
|
|
||||||
|
type Charsets map[string]Charset
|
||||||
|
|
||||||
|
var charsets = make(Charsets)
|
||||||
|
|
||||||
|
func (c Charsets) Register(name string, charset Charset) {
|
||||||
|
c[name] = charset
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Charsets) DecodeString(name string, input string) (string, error) {
|
||||||
|
decoder, ok := c[name]
|
||||||
|
if !ok {
|
||||||
|
return "", fmt.Errorf("unknown decoder: %s", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if decoder == nil {
|
||||||
|
return input, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return decoder.String(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Charsets) DecodeReader(name string, input io.Reader) (io.Reader, error) {
|
||||||
|
decoder, ok := charsets[name]
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("unknown decoder: %s", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if decoder == nil {
|
||||||
|
return input, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return decoder.Reader(input), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
charsets.Register("utf8", utf8decoder)
|
||||||
|
charsets.Register("win1251", charmap.Windows1251.NewDecoder())
|
||||||
|
charsets.Register("cp866", charmap.CodePage866.NewDecoder())
|
||||||
|
}
|
||||||
|
|
||||||
|
type Utf8decoder struct{}
|
||||||
|
|
||||||
|
var utf8decoder = new(Utf8decoder)
|
||||||
|
|
||||||
|
func (d *Utf8decoder) Reader(r io.Reader) io.Reader {
|
||||||
|
return utfbom.SkipOnly(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Utf8decoder) String(s string) (string, error) {
|
||||||
|
return s, nil
|
||||||
|
}
|
137
fieldtypes.go
Normal file
137
fieldtypes.go
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CustomDateParser interface {
|
||||||
|
Reader
|
||||||
|
ParseDate(rawValue string) (time.Time, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type CustomDateTimeParser interface {
|
||||||
|
Reader
|
||||||
|
ParseDateTime(rawValue string) (time.Time, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type FieldType int
|
||||||
|
|
||||||
|
const (
|
||||||
|
Skip FieldType = iota
|
||||||
|
Integer
|
||||||
|
String
|
||||||
|
Float
|
||||||
|
Money
|
||||||
|
Date
|
||||||
|
Timestamp
|
||||||
|
)
|
||||||
|
|
||||||
|
func (ft FieldType) ParseValue(reader Reader, s string) (any, error) {
|
||||||
|
s = strings.TrimSpace(s)
|
||||||
|
|
||||||
|
if s == "" {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ft {
|
||||||
|
case String:
|
||||||
|
return s, nil
|
||||||
|
case Integer:
|
||||||
|
return strconv.ParseInt(s, 10, 64)
|
||||||
|
case Float:
|
||||||
|
return strconv.ParseFloat(strings.ReplaceAll(s, ",", "."), 64)
|
||||||
|
case Date:
|
||||||
|
if i, ok := reader.(CustomDateParser); ok {
|
||||||
|
t, err := i.ParseDate(s)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return /*t.Truncate(24 * time.Hour)*/ t, nil // TODO: проверить, нужен ли Truncate
|
||||||
|
}
|
||||||
|
|
||||||
|
return time.ParseInLocation(reader.Options().dateFormat, s, reader.Options().timezone)
|
||||||
|
case Timestamp:
|
||||||
|
if i, ok := reader.(CustomDateTimeParser); ok {
|
||||||
|
t, err := i.ParseDateTime(s)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return t.Truncate(24 * time.Second), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return time.ParseInLocation(reader.Options().timestampFormat, s, reader.Options().timezone)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("unknown type id = %d", ft)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ft FieldType) SqlFieldType() string {
|
||||||
|
switch ft {
|
||||||
|
case Integer:
|
||||||
|
return "bigint"
|
||||||
|
case String:
|
||||||
|
return "nvarchar(255)"
|
||||||
|
case Float:
|
||||||
|
return "float"
|
||||||
|
case Money:
|
||||||
|
panic("do not implemented - see https://github.com/denisenkom/go-mssqldb/issues/460") // TODO: https://github.com/denisenkom/go-mssqldb/issues/460
|
||||||
|
case Date:
|
||||||
|
return "date"
|
||||||
|
case Timestamp:
|
||||||
|
return "datetime2"
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ft FieldType) MarshalText() (text []byte, err error) {
|
||||||
|
switch ft {
|
||||||
|
case Skip:
|
||||||
|
return []byte(" "), nil
|
||||||
|
case Integer:
|
||||||
|
return []byte("i"), nil
|
||||||
|
case String:
|
||||||
|
return []byte("s"), nil
|
||||||
|
case Float:
|
||||||
|
return []byte("f"), nil
|
||||||
|
case Money:
|
||||||
|
return []byte("m"), nil
|
||||||
|
case Date:
|
||||||
|
return []byte("d"), nil
|
||||||
|
case Timestamp:
|
||||||
|
return []byte("t"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("unknown type id = %d", ft)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ft *FieldType) UnmarshalText(text []byte) error {
|
||||||
|
switch string(text) {
|
||||||
|
case " ":
|
||||||
|
*ft = Skip
|
||||||
|
return nil
|
||||||
|
case "i":
|
||||||
|
*ft = Integer
|
||||||
|
return nil
|
||||||
|
case "s":
|
||||||
|
*ft = String
|
||||||
|
return nil
|
||||||
|
case "f":
|
||||||
|
*ft = Float
|
||||||
|
return nil
|
||||||
|
case "m":
|
||||||
|
*ft = Money
|
||||||
|
return nil
|
||||||
|
case "d":
|
||||||
|
*ft = Date
|
||||||
|
return nil
|
||||||
|
case "t":
|
||||||
|
*ft = Timestamp
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf(`unknown format code "%s"`, string(text))
|
||||||
|
}
|
63
filetypes.go
Normal file
63
filetypes.go
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
type FileType int
|
||||||
|
|
||||||
|
const (
|
||||||
|
AutoDetect FileType = iota
|
||||||
|
Csv
|
||||||
|
Xlsx
|
||||||
|
Dbf
|
||||||
|
)
|
||||||
|
|
||||||
|
func (ft FileType) MarshalText() (text []byte, err error) {
|
||||||
|
switch ft {
|
||||||
|
case AutoDetect:
|
||||||
|
return []byte("auto"), nil
|
||||||
|
case Csv:
|
||||||
|
return []byte("csv"), nil
|
||||||
|
case Xlsx:
|
||||||
|
return []byte("xlsx"), nil
|
||||||
|
case Dbf:
|
||||||
|
return []byte("dbf"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("unknown type id = %d", ft)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ft FileType) Open(r io.Reader, options *Options) (Reader, error) {
|
||||||
|
switch ft {
|
||||||
|
case AutoDetect:
|
||||||
|
case Csv:
|
||||||
|
return newCsvReader(r, options)
|
||||||
|
case Xlsx:
|
||||||
|
return newXlsxReader(r, options)
|
||||||
|
case Dbf:
|
||||||
|
return newDbfReader(r, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("unknown type id = %d", ft)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ft *FileType) UnmarshalText(text []byte) error {
|
||||||
|
switch string(text) {
|
||||||
|
case "auto":
|
||||||
|
*ft = AutoDetect
|
||||||
|
return nil
|
||||||
|
case "csv":
|
||||||
|
*ft = Csv
|
||||||
|
return nil
|
||||||
|
case "xlsx":
|
||||||
|
*ft = Xlsx
|
||||||
|
return nil
|
||||||
|
case "dbf":
|
||||||
|
*ft = Dbf
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf(`unknown format code "%s"`, string(text))
|
||||||
|
}
|
30
go.mod
Normal file
30
go.mod
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
module github.com/nxshock/mssqlbulkloader
|
||||||
|
|
||||||
|
go 1.20
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/SebastiaanKlippert/go-foxpro-dbf v1.2.0
|
||||||
|
github.com/denisenkom/go-mssqldb v0.12.3
|
||||||
|
github.com/dimchansky/utfbom v1.1.1
|
||||||
|
github.com/stretchr/testify v1.8.2
|
||||||
|
github.com/urfave/cli v1.22.12
|
||||||
|
github.com/xuri/excelize/v2 v2.7.0
|
||||||
|
golang.org/x/text v0.8.0
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
|
||||||
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
|
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
|
||||||
|
github.com/golang-sql/sqlexp v0.1.0 // indirect
|
||||||
|
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
|
github.com/richardlehane/mscfb v1.0.4 // indirect
|
||||||
|
github.com/richardlehane/msoleps v1.0.3 // indirect
|
||||||
|
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||||
|
github.com/xuri/efp v0.0.0-20220603152613-6918739fd470 // indirect
|
||||||
|
github.com/xuri/nfp v0.0.0-20220409054826-5e722a1d9e22 // indirect
|
||||||
|
golang.org/x/crypto v0.7.0 // indirect
|
||||||
|
golang.org/x/net v0.8.0 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
)
|
104
go.sum
Normal file
104
go.sum
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
github.com/Azure/azure-sdk-for-go/sdk/azcore v0.19.0/go.mod h1:h6H6c8enJmmocHUbLiiGY6sx7f9i+X3m1CHdd5c6Rdw=
|
||||||
|
github.com/Azure/azure-sdk-for-go/sdk/azidentity v0.11.0/go.mod h1:HcM1YX14R7CJcghJGOYCgdezslRSVzqwLf/q+4Y2r/0=
|
||||||
|
github.com/Azure/azure-sdk-for-go/sdk/internal v0.7.0/go.mod h1:yqy467j36fJxcRV2TzfVZ1pCb5vxm4BtZPUdYWe/Xo8=
|
||||||
|
github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||||
|
github.com/SebastiaanKlippert/go-foxpro-dbf v1.2.0 h1:11OnIKzaY952Atj9pLewuG09DdRv6CCm2XnZaTEcWn0=
|
||||||
|
github.com/SebastiaanKlippert/go-foxpro-dbf v1.2.0/go.mod h1:VnyVS1nyFfnCduBoWvjYuRp5Ce3KqZThky+ECDvJmEA=
|
||||||
|
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
|
||||||
|
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/denisenkom/go-mssqldb v0.12.3 h1:pBSGx9Tq67pBOTLmxNuirNTeB8Vjmf886Kx+8Y+8shw=
|
||||||
|
github.com/denisenkom/go-mssqldb v0.12.3/go.mod h1:k0mtMFOnU+AihqFxPMiF05rtiDrorD1Vrm1KEz5hxDo=
|
||||||
|
github.com/dimchansky/utfbom v1.1.1 h1:vV6w1AhK4VMnhBno/TPVCoK9U/LP0PkLCS9tbxHdi/U=
|
||||||
|
github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE=
|
||||||
|
github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ=
|
||||||
|
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
|
||||||
|
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA=
|
||||||
|
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
|
||||||
|
github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
|
||||||
|
github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI=
|
||||||
|
github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8=
|
||||||
|
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw=
|
||||||
|
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
|
||||||
|
github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/richardlehane/mscfb v1.0.4 h1:WULscsljNPConisD5hR0+OyZjwK46Pfyr6mPu5ZawpM=
|
||||||
|
github.com/richardlehane/mscfb v1.0.4/go.mod h1:YzVpcZg9czvAuhk9T+a3avCpcFPMUWm7gK3DypaEsUk=
|
||||||
|
github.com/richardlehane/msoleps v1.0.1/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
|
||||||
|
github.com/richardlehane/msoleps v1.0.3 h1:aznSZzrwYRl3rLKRT3gUk9am7T/mLNSnJINvN0AQoVM=
|
||||||
|
github.com/richardlehane/msoleps v1.0.3/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
|
||||||
|
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||||
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
|
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
|
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
|
||||||
|
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
|
github.com/urfave/cli v1.22.12 h1:igJgVw1JdKH+trcLWLeLwZjU9fEfPesQ+9/e4MQ44S8=
|
||||||
|
github.com/urfave/cli v1.22.12/go.mod h1:sSBEIC79qR6OvcmsD4U3KABeOTxDqQtdDnaFuUN30b8=
|
||||||
|
github.com/xuri/efp v0.0.0-20220603152613-6918739fd470 h1:6932x8ltq1w4utjmfMPVj09jdMlkY0aiA6+Skbtl3/c=
|
||||||
|
github.com/xuri/efp v0.0.0-20220603152613-6918739fd470/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI=
|
||||||
|
github.com/xuri/excelize/v2 v2.7.0 h1:Hri/czwyRCW6f6zrCDWXcXKshlq4xAZNpNOpdfnFhEw=
|
||||||
|
github.com/xuri/excelize/v2 v2.7.0/go.mod h1:ebKlRoS+rGyLMyUx3ErBECXs/HNYqyj+PbkkKRK5vSI=
|
||||||
|
github.com/xuri/nfp v0.0.0-20220409054826-5e722a1d9e22 h1:OAmKAfT06//esDdpi/DZ8Qsdt4+M5+ltca05dA5bG2M=
|
||||||
|
github.com/xuri/nfp v0.0.0-20220409054826-5e722a1d9e22/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ=
|
||||||
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
|
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||||
|
golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU=
|
||||||
|
golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A=
|
||||||
|
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
||||||
|
golang.org/x/image v0.0.0-20220902085622-e7cb96979f69 h1:Lj6HJGCSn5AjxRAH2+r35Mir4icalbqku+CLUtjnvXY=
|
||||||
|
golang.org/x/image v0.0.0-20220902085622-e7cb96979f69/go.mod h1:doUCurBvlfPMKfmIpRIywoHmhN3VyhnoFDbvIEWF4hY=
|
||||||
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
|
golang.org/x/net v0.0.0-20210610132358-84b48f89b13b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
|
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
|
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
|
golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
|
||||||
|
golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
|
||||||
|
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
||||||
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
|
golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
|
golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
|
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
|
golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=
|
||||||
|
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
21
logger.go
Normal file
21
logger.go
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Logger struct {
|
||||||
|
silent bool
|
||||||
|
}
|
||||||
|
|
||||||
|
var logger *log.Logger
|
||||||
|
|
||||||
|
func initLogger(silent bool) {
|
||||||
|
if silent {
|
||||||
|
logger = log.New(io.Discard, "", 0)
|
||||||
|
} else {
|
||||||
|
logger = log.New(os.Stderr, "", 0)
|
||||||
|
}
|
||||||
|
}
|
17
main.go
Normal file
17
main.go
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
log.SetFlags(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
err := app.Run(os.Args)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln(err)
|
||||||
|
}
|
||||||
|
}
|
1
make.bat
Normal file
1
make.bat
Normal file
@ -0,0 +1 @@
|
|||||||
|
go build -trimpath -buildmode=pie -ldflags "-linkmode external -s -w"
|
62
options.go
Normal file
62
options.go
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type Options struct {
|
||||||
|
// Source file path
|
||||||
|
filePath string
|
||||||
|
|
||||||
|
// Source file type
|
||||||
|
fileType string
|
||||||
|
|
||||||
|
// Server address
|
||||||
|
server string
|
||||||
|
|
||||||
|
// Database name
|
||||||
|
database string
|
||||||
|
|
||||||
|
// Table name
|
||||||
|
tableName string
|
||||||
|
|
||||||
|
// comma delimiter for CSV files
|
||||||
|
comma rune
|
||||||
|
|
||||||
|
// Number of rows to skip before reading of header
|
||||||
|
skipRows int
|
||||||
|
|
||||||
|
// List of fiels types
|
||||||
|
fieldsTypes string
|
||||||
|
|
||||||
|
// Date format
|
||||||
|
dateFormat string
|
||||||
|
|
||||||
|
// Date+time format
|
||||||
|
timestampFormat string
|
||||||
|
|
||||||
|
// Sheet name for Excel file
|
||||||
|
sheetName string
|
||||||
|
|
||||||
|
// CSV/DBF codepage
|
||||||
|
encoding string
|
||||||
|
|
||||||
|
// create table before inserting data
|
||||||
|
create bool
|
||||||
|
|
||||||
|
// Drop existing table before creating
|
||||||
|
overwrite bool
|
||||||
|
|
||||||
|
// Disable progress output
|
||||||
|
silent bool
|
||||||
|
|
||||||
|
// Input file dates timezone
|
||||||
|
timezone *time.Location
|
||||||
|
|
||||||
|
// Decompress before process
|
||||||
|
decompress string
|
||||||
|
|
||||||
|
// Unknown column names
|
||||||
|
unknownColumnNames bool
|
||||||
|
|
||||||
|
// Column names list
|
||||||
|
columnNames []string
|
||||||
|
}
|
28
reader.go
Normal file
28
reader.go
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
type Reader interface {
|
||||||
|
// GetHeaders returns list of column names
|
||||||
|
GetHeader() []string
|
||||||
|
|
||||||
|
// GetRows returns next one file row or io.EOF
|
||||||
|
GetRow(asString bool) ([]any, error)
|
||||||
|
|
||||||
|
// Options returns options
|
||||||
|
Options() *Options
|
||||||
|
|
||||||
|
Close() error
|
||||||
|
}
|
||||||
|
|
||||||
|
func getHeader(r Reader) ([]string, error) {
|
||||||
|
headerAny, err := r.GetRow(true)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
header := make([]string, 0, len(headerAny))
|
||||||
|
for _, v := range headerAny {
|
||||||
|
header = append(header, v.(string))
|
||||||
|
}
|
||||||
|
|
||||||
|
return header, nil
|
||||||
|
}
|
101
readercsv.go
Normal file
101
readercsv.go
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"encoding/csv"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CsvReader struct {
|
||||||
|
reader *csv.Reader
|
||||||
|
header []string
|
||||||
|
options *Options
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCsvReader(r io.Reader, options *Options) (*CsvReader, error) {
|
||||||
|
return newCsvReader(r, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newCsvReader(r io.Reader, options *Options) (*CsvReader, error) {
|
||||||
|
decoder, err := charsets.DecodeReader(options.encoding, r)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("enable decoder: %v", options.encoding)
|
||||||
|
}
|
||||||
|
|
||||||
|
bufReader := bufio.NewReaderSize(decoder, 4*1024*1024)
|
||||||
|
|
||||||
|
for i := 0; i < options.skipRows; i++ {
|
||||||
|
_, _, err := bufReader.ReadLine()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("skip rows: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
re := csv.NewReader(bufReader)
|
||||||
|
re.Comma = options.comma
|
||||||
|
re.FieldsPerRecord = len(options.fieldsTypes)
|
||||||
|
|
||||||
|
csvReader := &CsvReader{
|
||||||
|
reader: re,
|
||||||
|
options: options}
|
||||||
|
|
||||||
|
header, err := getHeader(csvReader)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
csvReader.header = header
|
||||||
|
|
||||||
|
return csvReader, nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *CsvReader) GetHeader() []string {
|
||||||
|
return r.header
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *CsvReader) Options() *Options {
|
||||||
|
return r.options
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *CsvReader) GetRow(asStrings bool) ([]any, error) {
|
||||||
|
record, err := r.reader.Read()
|
||||||
|
if err == io.EOF {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("read record: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var args []any
|
||||||
|
|
||||||
|
for i, v := range record {
|
||||||
|
var fieldType FieldType
|
||||||
|
err = fieldType.UnmarshalText([]byte{r.options.fieldsTypes[i]})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("get record type: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if fieldType == Skip {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if asStrings {
|
||||||
|
fieldType = String
|
||||||
|
}
|
||||||
|
|
||||||
|
parsedValue, err := fieldType.ParseValue(r, v)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("parse value: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
args = append(args, parsedValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
return args, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *CsvReader) Close() error {
|
||||||
|
return nil
|
||||||
|
}
|
42
readercsv_test.go
Normal file
42
readercsv_test.go
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCsvReaderBasic(t *testing.T) {
|
||||||
|
f, err := os.Open("testdata/csv/9729337841_20032023_084313667.csv")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
options := &Options{
|
||||||
|
encoding: "win1251",
|
||||||
|
comma: rune(";"[0]),
|
||||||
|
skipRows: 3,
|
||||||
|
fieldsTypes: "s ttffsssss",
|
||||||
|
dateFormat: "02.01.2006",
|
||||||
|
timestampFormat: "02.01.2006 15:04:05",
|
||||||
|
timezone: time.Local}
|
||||||
|
|
||||||
|
csvReader, err := NewCsvReader(f, options)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, []string{"RRN", "Дата операции", "Дата ПП", "Сумма операции", "Сумма расчета", "Номер карты", "Код авторизации", "Тип операции", "Доп. информация_1", "Доп. информация_2"}, csvReader.GetHeader())
|
||||||
|
|
||||||
|
row, err := csvReader.GetRow(false)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
t1 := time.Date(2023, 03, 19, 17, 49, 35, 0, time.Local)
|
||||||
|
t2 := time.Date(2023, 03, 20, 0, 0, 0, 0, time.Local)
|
||||||
|
assert.Equal(t, []any{"307814009186", t1, t2, 499.00, 488.52, "522598******7141", "REZE64", "Покупка", "35068281112", "307817403283"}, row)
|
||||||
|
|
||||||
|
row, err = csvReader.GetRow(false)
|
||||||
|
assert.Equal(t, err, io.EOF)
|
||||||
|
|
||||||
|
err = csvReader.Close()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
124
readerdbf.go
Normal file
124
readerdbf.go
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
dbf "github.com/SebastiaanKlippert/go-foxpro-dbf"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
dbf.SetValidFileVersionFunc(func(version byte) error {
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
type DbfReader struct {
|
||||||
|
reader *dbf.DBF
|
||||||
|
header []string
|
||||||
|
options *Options
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDbfReader(r io.Reader, options *Options) (*DbfReader, error) {
|
||||||
|
return newDbfReader(r, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newDbfReader(r io.Reader, options *Options) (*DbfReader, error) {
|
||||||
|
b, err := io.ReadAll(r)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
br := bytes.NewReader(b)
|
||||||
|
|
||||||
|
re, err := dbf.OpenStream(br, nil, &dbf.UTF8Decoder{})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
dbfReader := &DbfReader{
|
||||||
|
reader: re,
|
||||||
|
options: options}
|
||||||
|
|
||||||
|
fullHeader := re.FieldNames()
|
||||||
|
var header []string
|
||||||
|
for i, v := range options.fieldsTypes {
|
||||||
|
if v == ' ' {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
s, err := charsets.DecodeString(options.encoding, fullHeader[i])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
header = append(header, s)
|
||||||
|
}
|
||||||
|
|
||||||
|
dbfReader.header = header
|
||||||
|
|
||||||
|
return dbfReader, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *DbfReader) GetHeader() []string {
|
||||||
|
return r.header
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *DbfReader) Options() *Options {
|
||||||
|
return r.options
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *DbfReader) GetRow(asStrings bool) ([]any, error) {
|
||||||
|
if r.reader.EOF() {
|
||||||
|
return nil, io.EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
record, err := r.reader.Record()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("read record: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
r.reader.Skip(1)
|
||||||
|
|
||||||
|
var args []any
|
||||||
|
|
||||||
|
for i, v := range record.FieldSlice() {
|
||||||
|
var fieldType FieldType
|
||||||
|
err = fieldType.UnmarshalText([]byte{r.options.fieldsTypes[i]})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("get record type: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if fieldType == Skip {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
decV, err := charsets.DecodeString(r.options.encoding, fmt.Sprint(v))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
parsedValue, err := fieldType.ParseValue(r, decV)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("parse value: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
args = append(args, parsedValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
return args, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *DbfReader) Close() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *DbfReader) ParseDate(rawValue string) (time.Time, error) {
|
||||||
|
return time.ParseInLocation("02.01.2006", rawValue, r.options.timezone)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *DbfReader) ParseDateTime(rawValue string) (time.Time, error) {
|
||||||
|
return time.ParseInLocation("02.01.2006 15:04:05", rawValue, r.options.timezone)
|
||||||
|
}
|
38
readerdbf_test.go
Normal file
38
readerdbf_test.go
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDbfReaderBasic(t *testing.T) {
|
||||||
|
f, err := os.Open("testdata/dbf/38_052QB.dbf")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
options := &Options{
|
||||||
|
fieldsTypes: "sssssstdffsss",
|
||||||
|
timezone: time.Local,
|
||||||
|
encoding: "cp866"}
|
||||||
|
|
||||||
|
dbfReader, err := NewDbfReader(f, options)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, []string{"TRAN_ID", "БАНК", "ОТДЕЛЕНИЕ", "ТОЧКА", "НАЗВАНИЕ", "ТЕРМИНАЛ", "ДАТА_ТРАН", "ДАТА_РАСЧ", "СУММА_ТРАН", "СУММА_РАСЧ", "КАРТА", "КОД_АВТ", "ТИП"}, dbfReader.GetHeader())
|
||||||
|
|
||||||
|
row, err := dbfReader.GetRow(false)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
t1 := time.Date(2023, 02, 20, 5, 57, 12, 0, time.Local)
|
||||||
|
t2 := time.Date(2023, 02, 21, 0, 0, 0, 0, time.Local)
|
||||||
|
assert.Equal(t, []any{"719089383780", "44", "8644", "570000009312", "STOLOVAYA TSPP", "844417", t1, t2, 1757.08, 1713.15, "536829XXXXXX9388", "UM1TS8", "D"}, row)
|
||||||
|
|
||||||
|
row, err = dbfReader.GetRow(false)
|
||||||
|
assert.Equal(t, err, io.EOF)
|
||||||
|
|
||||||
|
err = dbfReader.Close()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
159
readerxlsx.go
Normal file
159
readerxlsx.go
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/xuri/excelize/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type XlsxReader struct {
|
||||||
|
streamReader *excelize.File
|
||||||
|
rows *excelize.Rows
|
||||||
|
header []string
|
||||||
|
options *Options
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewXlsxReader(r io.Reader, options *Options) (*XlsxReader, error) {
|
||||||
|
return newXlsxReader(r, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newXlsxReader(r io.Reader, options *Options) (*XlsxReader, error) {
|
||||||
|
streamReader, err := excelize.OpenReader(r)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("open reader: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
sheetName := options.sheetName
|
||||||
|
if sheetName == "" {
|
||||||
|
if len(streamReader.GetSheetList()) == 0 {
|
||||||
|
streamReader.Close()
|
||||||
|
return nil, fmt.Errorf("get sheet list: %w", errors.New("file does not contains any sheets"))
|
||||||
|
}
|
||||||
|
sheetName = streamReader.GetSheetList()[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
rows, err := streamReader.Rows(sheetName)
|
||||||
|
if err != nil {
|
||||||
|
streamReader.Close()
|
||||||
|
return nil, fmt.Errorf("read rows: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
xlsxReader := &XlsxReader{
|
||||||
|
streamReader: streamReader,
|
||||||
|
options: options,
|
||||||
|
rows: rows}
|
||||||
|
|
||||||
|
for i := 0; i < options.skipRows; i++ {
|
||||||
|
_, err := xlsxReader.GetRow(true)
|
||||||
|
if err != nil {
|
||||||
|
streamReader.Close()
|
||||||
|
return nil, fmt.Errorf("skip rows: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
header, err := getHeader(xlsxReader)
|
||||||
|
if err != nil {
|
||||||
|
streamReader.Close()
|
||||||
|
return nil, fmt.Errorf("read header: %w", err)
|
||||||
|
}
|
||||||
|
xlsxReader.header = header
|
||||||
|
|
||||||
|
return xlsxReader, nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *XlsxReader) GetHeader() []string {
|
||||||
|
return r.header
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *XlsxReader) Options() *Options {
|
||||||
|
return r.options
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *XlsxReader) GetRow(asStrings bool) ([]any, error) {
|
||||||
|
end := !r.rows.Next()
|
||||||
|
if end {
|
||||||
|
return nil, io.EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
record, err := r.rows.Columns(excelize.Options{RawCellValue: true})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var args []any
|
||||||
|
|
||||||
|
for i, v := range record {
|
||||||
|
var fieldType FieldType
|
||||||
|
err = fieldType.UnmarshalText([]byte{r.options.fieldsTypes[i]})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("get record type: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if fieldType == Skip {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if asStrings {
|
||||||
|
fieldType = String
|
||||||
|
}
|
||||||
|
|
||||||
|
parsedValue, err := fieldType.ParseValue(r, v)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("parse value: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
args = append(args, parsedValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
return args, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *XlsxReader) Close() error {
|
||||||
|
err := r.rows.Close()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = r.streamReader.Close()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *XlsxReader) ParseDate(rawValue string) (time.Time, error) {
|
||||||
|
f, err := strconv.ParseFloat(rawValue, 64)
|
||||||
|
if err != nil {
|
||||||
|
return time.Time{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
t, err := excelize.ExcelDateToTime(f, false)
|
||||||
|
if err != nil {
|
||||||
|
return time.Time{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
t = time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second(), t.Nanosecond(), r.options.timezone)
|
||||||
|
|
||||||
|
return t, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *XlsxReader) ParseDateTime(rawValue string) (time.Time, error) {
|
||||||
|
f, err := strconv.ParseFloat(rawValue, 64)
|
||||||
|
if err != nil {
|
||||||
|
return time.Time{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
t, err := excelize.ExcelDateToTime(f, false)
|
||||||
|
if err != nil {
|
||||||
|
return time.Time{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
t = time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second(), t.Nanosecond(), r.options.timezone)
|
||||||
|
|
||||||
|
return t, nil
|
||||||
|
}
|
36
readerxlsx_test.go
Normal file
36
readerxlsx_test.go
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestXlsxReaderBasic(t *testing.T) {
|
||||||
|
f, err := os.Open("testdata/xlsx/38_049RMZ_all.xlsx")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
options := &Options{skipRows: 0, fieldsTypes: "s sssssssssttfffssss", timezone: time.Local}
|
||||||
|
|
||||||
|
xlsxReader, err := NewXlsxReader(f, options)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, []string{"ИНН предприятия", "Город", "Адрес ТСТ", "Обслуживающее отделение", "Расчетное отделение", "RRN операции", "Название ТСТ", "Мерчант ТСТ", "Расчетный мерчант", "Терминал", "Дата проведения операции", "Дата обработки операции", "Сумма операции", "Комиссия за операцию", "Сумма к расчету", "Карта", "Код авторизации", "Тип операции", "Тип карты"}, xlsxReader.GetHeader())
|
||||||
|
|
||||||
|
row, err := xlsxReader.GetRow(false)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
t1 := time.Date(2023, 02, 17, 1, 5, 12, 0, time.Local)
|
||||||
|
t2 := time.Date(2023, 02, 18, 6, 24, 24, 0, time.Local) // TODO: в excel-файле 37 секунд?
|
||||||
|
|
||||||
|
assert.Equal(t, []any{"7710146208", nil, nil, "99386901", "99386901", "304722813269", "TSENTRALNYY TELEGRAF", "780000334079", "780000334079", "10432641", t1, t2, 50.00, 0.80, 49.20, "553691******1214", "026094", "D", "MC OTHER"}, row)
|
||||||
|
|
||||||
|
row, err = xlsxReader.GetRow(false)
|
||||||
|
assert.Equal(t, io.EOF, err)
|
||||||
|
|
||||||
|
err = xlsxReader.Close()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
151
sql.go
Normal file
151
sql.go
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
mssql "github.com/denisenkom/go-mssqldb"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TODO: add escaping
|
||||||
|
func prepareTable(reader Reader, tx *sql.Tx) error {
|
||||||
|
if reader.Options().unknownColumnNames {
|
||||||
|
var columnNames []string
|
||||||
|
|
||||||
|
sql := fmt.Sprintf("SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA + '.' + TABLE_NAME = '%s' ORDER BY ORDINAL_POSITION", reader.Options().tableName)
|
||||||
|
rows, err := tx.Query(sql)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("get column names from database: %w", err)
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
for rows.Next() {
|
||||||
|
if rows.Err() != nil {
|
||||||
|
return fmt.Errorf("get column names from database: %w", err)
|
||||||
|
}
|
||||||
|
var columnName string
|
||||||
|
err = rows.Scan(&columnName)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("get column names from database: %w", err)
|
||||||
|
}
|
||||||
|
columnNames = append(columnNames, columnName)
|
||||||
|
}
|
||||||
|
|
||||||
|
reader.Options().columnNames = columnNames
|
||||||
|
} else {
|
||||||
|
reader.Options().columnNames = reader.GetHeader()
|
||||||
|
}
|
||||||
|
|
||||||
|
if !reader.Options().create && !reader.Options().overwrite {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if !reader.Options().create && reader.Options().overwrite {
|
||||||
|
logger.Println("Truncating table...")
|
||||||
|
_, err := tx.Exec(fmt.Sprintf("TRUNCATE TABLE %s", reader.Options().tableName))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if reader.Options().overwrite {
|
||||||
|
logger.Println("Dropping table...")
|
||||||
|
_, err := tx.Exec(fmt.Sprintf("IF object_id('%s', 'U') IS NOT NULL DROP TABLE %s", reader.Options().tableName, reader.Options().tableName))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("drop table: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sql := fmt.Sprintf("CREATE TABLE %s (", reader.Options().tableName)
|
||||||
|
|
||||||
|
fieldTypes := strings.ReplaceAll(reader.Options().fieldsTypes, " ", "")
|
||||||
|
|
||||||
|
for i, columnName := range reader.Options().columnNames {
|
||||||
|
var fieldType FieldType
|
||||||
|
err := fieldType.UnmarshalText([]byte{fieldTypes[i]})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("detect field type: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
sql += fmt.Sprintf(`"%s" %s`, columnName, fieldType.SqlFieldType())
|
||||||
|
|
||||||
|
if i+1 < len(reader.GetHeader()) {
|
||||||
|
sql += ", "
|
||||||
|
} else {
|
||||||
|
sql += ") WITH (DATA_COMPRESSION = PAGE)" // TODO: add optional params
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Println("Creating table...")
|
||||||
|
logger.Println(sql)
|
||||||
|
_, err := tx.Exec(sql)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("execute table creation: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func insertData(reader Reader, tx *sql.Tx) error {
|
||||||
|
columnNames := reader.GetHeader()
|
||||||
|
if reader.Options().unknownColumnNames {
|
||||||
|
columnNames = reader.Options().columnNames
|
||||||
|
}
|
||||||
|
|
||||||
|
sql := mssql.CopyIn(reader.Options().tableName, mssql.BulkOptions{Tablock: true}, columnNames...)
|
||||||
|
|
||||||
|
stmt, err := tx.Prepare(sql)
|
||||||
|
if err != nil {
|
||||||
|
_ = tx.Rollback()
|
||||||
|
return fmt.Errorf("prepare statement: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
n := 0
|
||||||
|
for {
|
||||||
|
if n%100000 == 0 {
|
||||||
|
if !reader.Options().silent {
|
||||||
|
fmt.Fprintf(os.Stderr, "Processed %d records...\r", n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
record, err := reader.GetRow(false)
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("read record: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = stmt.Exec(record...)
|
||||||
|
if err != nil {
|
||||||
|
_ = stmt.Close()
|
||||||
|
_ = tx.Rollback()
|
||||||
|
return fmt.Errorf("execute statement: %w", err)
|
||||||
|
}
|
||||||
|
n++
|
||||||
|
}
|
||||||
|
result, err := stmt.Exec()
|
||||||
|
if err != nil {
|
||||||
|
_ = tx.Rollback()
|
||||||
|
return fmt.Errorf("execute statement: %w", err)
|
||||||
|
}
|
||||||
|
rowsAffected, err := result.RowsAffected()
|
||||||
|
if err != nil {
|
||||||
|
_ = tx.Rollback()
|
||||||
|
return fmt.Errorf("calc rows affected: %w", err)
|
||||||
|
}
|
||||||
|
if !reader.Options().silent {
|
||||||
|
fmt.Fprintf(os.Stderr, "Processed %d records. \n", rowsAffected)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = stmt.Close()
|
||||||
|
if err != nil {
|
||||||
|
_ = tx.Rollback()
|
||||||
|
return fmt.Errorf("close statement: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
5
testdata/csv/9729337841_20032023_084313667.csv
vendored
Normal file
5
testdata/csv/9729337841_20032023_084313667.csv
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
skip
|
||||||
|
skip
|
||||||
|
skip
|
||||||
|
RRN;Территориальный банк;ГОСБ;Номер мерчанта;Наименование ТСТ;Номер терминала;Дата операции;Дата ПП;Сумма операции;Сумма расчета;Номер карты;Код авторизации;Тип операции;Доп. информация_1;Доп. информация_2
|
||||||
|
307814009186;ПАО Сбербанк;Киевское ОСБ;781000815902;WINK;28403560;19.03.2023 17:49:35;20.03.2023 00:00:00;499,00;488,52;522598******7141;REZE64;Покупка;35068281112;307817403283
|
|
BIN
testdata/dbf/38_052QB.dbf
vendored
Normal file
BIN
testdata/dbf/38_052QB.dbf
vendored
Normal file
Binary file not shown.
BIN
testdata/xlsx/38_049RMZ_all.xlsx
vendored
Normal file
BIN
testdata/xlsx/38_049RMZ_all.xlsx
vendored
Normal file
Binary file not shown.
33
zipreader.go
Normal file
33
zipreader.go
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/zip"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ProcessFunc func(io.Reader, *Options) error
|
||||||
|
|
||||||
|
type ZipReader struct{}
|
||||||
|
|
||||||
|
func (zr *ZipReader) Process(options *Options) error {
|
||||||
|
z, err := zip.OpenReader(options.filePath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer z.Close()
|
||||||
|
|
||||||
|
for _, zFile := range z.File {
|
||||||
|
f, err := zFile.Open()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
err = process(f, options)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user