chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:32:25 +08:00
commit e014feafe1
2285 changed files with 1131979 additions and 0 deletions
+272
View File
@@ -0,0 +1,272 @@
// Copyright 2023 Dolthub, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package harness
import (
"context"
"database/sql"
"fmt"
"strings"
"github.com/dolthub/sqllogictest/go/logictest"
_ "github.com/jackc/pgx/v4/stdlib"
)
// sqllogictest harness for postgres databases.
type PostgresqlServerHarness struct {
dsn string
db *sql.DB
timeout int64
}
// compile check for interface compliance
var _ logictest.Harness = &PostgresqlServerHarness{}
// NewPostgresqlHarness returns a new Postgres test harness for the data source name given. Panics if it cannot open a
// connection using the DSN.
func NewPostgresqlHarness(dsn string, t int64) *PostgresqlServerHarness {
db, err := sql.Open("pgx", dsn)
if err != nil {
panic(err)
}
return &PostgresqlServerHarness{
dsn: dsn,
db: db,
timeout: t,
}
}
func (h *PostgresqlServerHarness) EngineStr() string {
return "postgresql"
}
func (h *PostgresqlServerHarness) Init() error {
if err := h.dropAllTables(); err != nil {
return err
}
return h.dropAllViews()
}
// See Harness.ExecuteStatement
func (h *PostgresqlServerHarness) ExecuteStatement(statement string) error {
_, err := h.db.Exec(statement)
return err
}
// See Harness.ExecuteQuery
func (h *PostgresqlServerHarness) ExecuteQuery(statement string) (schema string, results []string, err error) {
rows, err := h.db.Query(statement)
if rows != nil {
defer rows.Close()
}
if err != nil {
return "", nil, err
}
return h.getSchemaAndResults(rows)
}
func (h *PostgresqlServerHarness) ExecuteStatementContext(ctx context.Context, statement string) error {
_, err := h.db.ExecContext(ctx, statement)
return err
}
func (h *PostgresqlServerHarness) ExecuteQueryContext(ctx context.Context, statement string) (schema string, results []string, err error) {
rows, err := h.db.QueryContext(ctx, statement)
if rows != nil {
defer rows.Close()
}
if err != nil {
return "", nil, err
}
return h.getSchemaAndResults(rows)
}
func (h *PostgresqlServerHarness) getSchemaAndResults(rows *sql.Rows) (schema string, results []string, err error) {
schema, columns, err := columns(rows)
if err != nil {
return "", nil, err
}
for rows.Next() {
err := rows.Scan(columns...)
if err != nil {
return "", nil, err
}
for _, col := range columns {
results = append(results, stringVal(col))
}
}
if rows.Err() != nil {
return "", nil, rows.Err()
}
return schema, results, nil
}
func (h *PostgresqlServerHarness) GetTimeout() int64 {
return h.timeout
}
func (h *PostgresqlServerHarness) dropAllTables() error {
var rows *sql.Rows
var err error
rows, err = h.db.QueryContext(context.Background(), "SELECT table_name FROM information_schema.tables WHERE table_schema = 'sqllogictest' AND table_type = 'BASE TABLE';")
if rows != nil {
defer rows.Close()
}
if err != nil {
return err
}
_, columns, err := columns(rows)
if err != nil {
return err
}
var tableNames []string
for rows.Next() {
err := rows.Scan(columns...)
if err != nil {
return err
}
tableName := columns[0].(*sql.NullString)
tableNames = append(tableNames, tableName.String)
}
if len(tableNames) > 0 {
dropTables := "drop table if exists " + strings.Join(tableNames, ",")
_, err = h.db.Exec(dropTables)
if err != nil {
return err
}
}
return nil
}
func (h *PostgresqlServerHarness) dropAllViews() error {
rows, err := h.db.QueryContext(context.Background(), "select table_name from INFORMATION_SCHEMA.views")
if rows != nil {
defer rows.Close()
}
if err != nil {
return err
}
_, columns, err := columns(rows)
if err != nil {
return err
}
var viewNames []string
for rows.Next() {
err := rows.Scan(columns...)
if err != nil {
return err
}
viewName := columns[0].(*sql.NullString)
viewNames = append(viewNames, viewName.String)
}
if len(viewNames) > 0 {
dropView := "drop view if exists " + strings.Join(viewNames, ",")
_, err = h.db.Exec(dropView)
if err != nil {
return err
}
}
return nil
}
// Returns the string representation of the column value given
func stringVal(col interface{}) string {
switch v := col.(type) {
case *sql.NullBool:
if !v.Valid {
return "NULL"
}
if v.Bool {
return "1"
} else {
return "0"
}
case *sql.NullInt64:
if !v.Valid {
return "NULL"
}
return fmt.Sprintf("%d", v.Int64)
case *sql.NullFloat64:
if !v.Valid {
return "NULL"
}
return fmt.Sprintf("%.3f", v.Float64)
case *sql.NullString:
if !v.Valid {
return "NULL"
}
return v.String
default:
panic(fmt.Sprintf("unhandled type %T for value %v", v, v))
}
}
// Returns the schema for the rows given, as well as a slice of columns suitable for scanning values into.
func columns(rows *sql.Rows) (string, []interface{}, error) {
types, err := rows.ColumnTypes()
if err != nil {
return "", nil, err
}
sb := strings.Builder{}
var columns []interface{}
for _, columnType := range types {
switch columnType.DatabaseTypeName() {
case "BIT", "BOOL":
colVal := sql.NullBool{}
columns = append(columns, &colVal)
sb.WriteString("I")
case "TEXT", "VARCHAR", "MEDIUMTEXT", "CHAR", "TINYTEXT", "NAME", "BYTEA":
colVal := sql.NullString{}
columns = append(columns, &colVal)
sb.WriteString("T")
case "DECIMAL", "DOUBLE", "FLOAT", "FLOAT4", "FLOAT8", "NUMERIC":
colVal := sql.NullFloat64{}
columns = append(columns, &colVal)
sb.WriteString("R")
case "MEDIUMINT", "INT", "BIGINT", "TINYINT", "SMALLINT", "INT2", "INT4", "INT8":
colVal := sql.NullInt64{}
columns = append(columns, &colVal)
sb.WriteString("I")
case "UNKNOWN": // used for NULL values
colVal := sql.NullString{}
columns = append(columns, &colVal)
sb.WriteString("I") // is this right?
default:
return "", nil, fmt.Errorf("Unhandled type %s", columnType.DatabaseTypeName())
}
}
return sb.String(), columns, nil
}
@@ -0,0 +1,415 @@
// Copyright 2024 Dolthub, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package harness
import (
"context"
"database/sql"
"fmt"
"io"
"log"
"os"
"os/exec"
"os/signal"
"path/filepath"
"sync"
"syscall"
"time"
"golang.org/x/sync/errgroup"
"github.com/dolthub/sqllogictest/go/logictest"
_ "github.com/jackc/pgx/v4/stdlib"
)
const (
doltgresDBDir = "doltgresDatabases"
serverLogFile = "server.log"
harnessLogFile = "harness.log"
DefaultPort = 5432
defaultDbName = "sqllogictest"
)
func noDatabaseDSN(port int) string {
return fmt.Sprintf("postgresql://postgres:password@127.0.0.1:%d/?sslmode=disable", port)
}
func withDatabaseDSN(port int, dbName string) string {
return fmt.Sprintf("postgresql://postgres:password@127.0.0.1:%d/%s?sslmode=disable", port, dbName)
}
var _ logictest.Harness = &DoltgresHarness{}
// DoltgresHarness is a sqllogictest harness for doltgres databases.
type DoltgresHarness struct {
db *sql.DB
doltgresExec string
server *DoltgresServer
serverDir string
timeout int64
port int
dbName string
harnessLog *os.File
stashedLogOutput io.Writer
}
// NewDoltgresHarness returns a harness that manages its own server lifecycle. The server is
// started once at construction; Init resets the database without restarting the process.
func NewDoltgresHarness(doltgresExec string, t int64) *DoltgresHarness {
cwd, err := os.Getwd()
if err != nil {
logErr(err, "getting cwd")
}
serverDir := filepath.Join(cwd, doltgresDBDir)
err = os.RemoveAll(serverDir)
if err != nil {
logErr(err, fmt.Sprintf("running `RemoveAll` for '%s'", serverDir))
}
err = os.MkdirAll(serverDir, os.ModePerm)
if err != nil {
logErr(err, fmt.Sprintf("running `MkdirAll` for '%s'", serverDir))
}
hl, err := os.OpenFile(harnessLogFile, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
if err != nil {
log.Fatal(err)
}
stashLogOutput := log.Writer()
log.SetOutput(hl)
logMsg("creating a new DoltgresHarness")
h := &DoltgresHarness{
doltgresExec: doltgresExec,
serverDir: serverDir,
timeout: t,
port: DefaultPort,
dbName: defaultDbName,
harnessLog: hl,
stashedLogOutput: stashLogOutput,
}
h.server = startServerProcess(doltgresExec, serverDir)
return h
}
// NewDoltgresWorkerHarness returns a harness for use in a concurrent worker. It connects to an
// already-running server on the given port using dbName as its isolated database. The caller is
// responsible for starting and stopping the server (see StartSharedServer).
func NewDoltgresWorkerHarness(port int, dbName string, t int64) *DoltgresHarness {
return &DoltgresHarness{
timeout: t,
port: port,
dbName: dbName,
}
}
// StartSharedServer starts a single doltgres server process for use by multiple concurrent
// workers. The returned *DoltgresServer must be closed when testing is complete.
func StartSharedServer(doltgresExec string) *DoltgresServer {
cwd, err := os.Getwd()
if err != nil {
logErr(err, "getting cwd")
}
serverDir := filepath.Join(cwd, doltgresDBDir)
err = os.RemoveAll(serverDir)
if err != nil {
logErr(err, fmt.Sprintf("running `RemoveAll` for '%s'", serverDir))
}
err = os.MkdirAll(serverDir, os.ModePerm)
if err != nil {
logErr(err, fmt.Sprintf("running `MkdirAll` for '%s'", serverDir))
}
return startServerProcess(doltgresExec, serverDir)
}
// startServerProcess starts a doltgres subprocess in serverDir and waits for it to be ready.
func startServerProcess(doltgresExec string, serverDir string) *DoltgresServer {
withKeyCtx, cancel := context.WithCancel(context.Background())
gServer, serverCtx := errgroup.WithContext(withKeyCtx)
server := exec.CommandContext(serverCtx, doltgresExec, "--data-dir=.")
server.Dir = serverDir
l, err := os.OpenFile(serverLogFile, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
if err != nil {
logErr(err, fmt.Sprintf("opening %s file", serverLogFile))
}
server.Stdout = l
server.Stderr = l
quit := make(chan os.Signal, 1)
signal.Notify(quit, os.Interrupt, syscall.SIGTERM)
var wg sync.WaitGroup
wg.Add(1)
go func() {
<-quit
defer wg.Done()
signal.Stop(quit)
cancel()
}()
ds := &DoltgresServer{
dir: serverDir,
quit: quit,
wg: &wg,
gServer: gServer,
server: server,
serverLog: l,
}
ds.Start()
return ds
}
func (h *DoltgresHarness) EngineStr() string {
return "postgresql"
}
// Init resets the harness to a clean state for the next test file.
// For worker harnesses (no server process), it keeps a persistent connection and resets the
// public schema via DROP+CREATE — avoiding database-level connection races entirely.
// For server-owning harnesses, it drops and recreates the database (safe because only one worker
// uses each database in the non-concurrent path).
func (h *DoltgresHarness) Init() error {
if h.server == nil {
return h.initWorker()
}
return h.initServer()
}
// initWorker resets a worker harness between test files.
//
// When useDropSchema is true (PostgreSQL), it wipes and recreates the public schema on the
// persistent connection — no reconnect needed, no races.
func (h *DoltgresHarness) initWorker() error {
ctx := context.Background()
if h.db != nil {
h.db.Close()
h.db = nil
}
noDB, err := sql.Open("pgx", noDatabaseDSN(h.port))
if err != nil {
return err
}
_, err = noDB.ExecContext(ctx, "DROP DATABASE IF EXISTS "+h.dbName)
if err == nil {
_, err = noDB.ExecContext(ctx, "CREATE DATABASE "+h.dbName)
}
noDB.Close()
if err != nil {
return fmt.Errorf("resetting database %s: %w", h.dbName, err)
}
return h.openWorkerDB(ctx)
}
// openWorkerDB establishes a connection to the worker's database, creating it if necessary.
func (h *DoltgresHarness) openWorkerDB(ctx context.Context) error {
db, err := sql.Open("pgx", withDatabaseDSN(h.port, h.dbName))
if err != nil {
return err
}
if pingErr := db.Ping(); pingErr == nil {
h.db = db
return nil
}
db.Close()
// Database does not exist yet; create it.
noDB, err := sql.Open("pgx", noDatabaseDSN(h.port))
if err != nil {
return err
}
_, createErr := noDB.ExecContext(ctx, "CREATE DATABASE "+h.dbName)
noDB.Close()
if createErr != nil {
logErr(createErr, "creating database "+h.dbName)
return createErr
}
db, err = sql.Open("pgx", withDatabaseDSN(h.port, h.dbName))
if err != nil {
return err
}
if err = db.Ping(); err != nil {
db.Close()
return err
}
h.db = db
return nil
}
// initServer resets the server-owning harness by dropping and recreating the database.
// This is only used in the non-concurrent (single-worker) path.
func (h *DoltgresHarness) initServer() error {
ctx := context.Background()
if h.db != nil {
h.db.Close()
h.db = nil
}
noDB, openErr := sql.Open("pgx", noDatabaseDSN(h.port))
if openErr != nil {
logErr(openErr, "opening no-db connection")
return openErr
}
_, err := noDB.ExecContext(ctx, "DROP DATABASE IF EXISTS "+h.dbName)
if err == nil {
_, err = noDB.ExecContext(ctx, "CREATE DATABASE "+h.dbName)
}
noDB.Close()
if err != nil {
logErr(err, "resetting database "+h.dbName)
return err
}
db, err := sql.Open("pgx", withDatabaseDSN(h.port, h.dbName))
if err != nil {
logErr(err, "opening connection to "+h.dbName)
return err
}
if err = db.Ping(); err != nil {
db.Close()
return err
}
h.db = db
return nil
}
func (h *DoltgresHarness) Close() error {
if h.db != nil {
h.db.Close()
h.db = nil
}
if h.server != nil {
h.server.Close()
h.server = nil
}
if h.harnessLog != nil {
h.harnessLog.Close()
log.SetOutput(h.stashedLogOutput)
}
return os.RemoveAll(h.serverDir)
}
func (h *DoltgresHarness) ExecuteStatement(statement string) error {
_, err := h.db.Exec(statement)
return err
}
func (h *DoltgresHarness) ExecuteQuery(statement string) (schema string, results []string, err error) {
rows, err := h.db.Query(statement)
if rows != nil {
defer rows.Close()
}
if err != nil {
return "", nil, err
}
return h.getSchemaAndResults(rows)
}
func (h *DoltgresHarness) ExecuteStatementContext(ctx context.Context, statement string) error {
_, err := h.db.ExecContext(ctx, statement)
return err
}
func (h *DoltgresHarness) ExecuteQueryContext(ctx context.Context, statement string) (schema string, results []string, err error) {
rows, err := h.db.QueryContext(ctx, statement)
if rows != nil {
defer rows.Close()
}
if err != nil {
return "", nil, err
}
return h.getSchemaAndResults(rows)
}
func (h *DoltgresHarness) GetTimeout() int64 {
return h.timeout
}
func (h *DoltgresHarness) getSchemaAndResults(rows *sql.Rows) (schema string, results []string, err error) {
schema, cols, err := columns(rows)
if err != nil {
return "", nil, err
}
for rows.Next() {
if err = rows.Scan(cols...); err != nil {
return "", nil, err
}
for _, col := range cols {
results = append(results, stringVal(col))
}
}
if rows.Err() != nil {
return "", nil, rows.Err()
}
return schema, results, nil
}
type DoltgresServer struct {
dir string
quit chan os.Signal
wg *sync.WaitGroup
gServer *errgroup.Group
server *exec.Cmd
serverLog *os.File
}
func (s *DoltgresServer) Start() {
logMsg("starting doltgres server")
var err error
s.gServer.Go(func() error {
err = s.server.Run()
return err
})
// Allow the server time to start accepting connections.
time.Sleep(3 * time.Second)
if err != nil {
logErr(err, "from server.Start()")
}
}
func (s *DoltgresServer) Stop() {
select {
case <-s.quit:
return
default:
}
s.quit <- syscall.SIGTERM
err := s.gServer.Wait()
if err != nil {
if err.Error() == "signal: killed" {
logMsg("doltgres server stopped successfully")
} else {
logErr(err, "from server.Stop()")
}
}
close(s.quit)
s.wg.Wait()
}
func (s *DoltgresServer) Close() {
s.Stop()
if err := s.serverLog.Close(); err != nil {
logErr(err, "closing server.log")
}
}
func logErr(err error, cause string) {
log.Printf("ERROR: %s received from %s", err.Error(), cause)
}
func logMsg(msg string) {
log.Println(msg)
}
+241
View File
@@ -0,0 +1,241 @@
// Copyright 2023 Dolthub, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package main
import (
"bytes"
"encoding/csv"
"encoding/json"
"flag"
"fmt"
"log"
"os"
"sync/atomic"
"github.com/cockroachdb/errors"
"github.com/dolthub/sqllogictest/go/logictest"
"github.com/dolthub/doltgresql/testing/logictest/harness"
)
var resultFormat = flag.String("r", "json", "format of parsed results")
var manageServer = flag.Bool("server", false, "whether the runner should launch the server process")
var doltgres = flag.String("doltgres", "", "local doltgres binary")
var timeout = flag.Int64("timeout", 0, "set a timeout (in seconds) for each test query")
var workers = flag.Int("workers", 1, "number of parallel workers")
// Runs all sqllogictest test files (or directories containing them) given as arguments.
// Usage: $command (run|parse) [version] [file1.test dir1/ dir2/]
// In run mode, runs the tests and prints results to stdout.
// In parse mode, parses test results from the file given and prints them to STDOUT in a format to be imported by dolt.
func main() {
flag.Parse()
args := flag.Args()
if len(args) < 1 {
panic("Usage: logictest (run|parse) [version] file1 file2 ...")
}
if args[0] == "run" {
if *manageServer {
if *doltgres == "" {
log.Fatal("Must supply --doltgres=<path to doltgres> with --server=true")
}
if *workers > 1 {
srv := harness.StartSharedServer(*doltgres)
defer srv.Close()
runConcurrent(*workers, *timeout, args[1:]...)
} else {
dh := harness.NewDoltgresHarness(*doltgres, *timeout)
defer dh.Close()
logictest.RunTestFiles(dh, args[1:]...)
}
} else {
if *workers > 1 {
runConcurrent(*workers, *timeout, args[1:]...)
} else {
h := harness.NewPostgresqlHarness("postgresql://postgres:password@localhost:5432/sqllogictest?sslmode=disable", *timeout)
logictest.RunTestFiles(h, args[1:]...)
}
}
} else if args[0] == "parse" {
if len(args) < 3 {
panic("Usage: logictest [-r(csv|json)] parse <version> (file | dir/)")
}
parseTestResults(args[1], args[2])
} else {
panic("Unrecognized command " + args[0])
}
}
// runConcurrent fans out |numWorkers| goroutines over the test files specified in |paths|.
// Each worker gets its own isolated database (sqllogictest_1, sqllogictest_2, …).
func runConcurrent(numWorkers int, queryTimeout int64, paths ...string) {
var workerIdx int32
logictest.RunTestFilesConcurrently(func() logictest.Harness {
n := atomic.AddInt32(&workerIdx, 1)
dbName := fmt.Sprintf("sqllogictest_%d", n)
return harness.NewDoltgresWorkerHarness(harness.DefaultPort, dbName, queryTimeout)
}, numWorkers, paths...)
}
func parseTestResults(version, f string) {
entries, err := logictest.ParseResultFile(f)
if err != nil {
panic(err)
}
records := make([]*DoltResultRecord, len(entries))
for i, e := range entries {
records[i] = NewDoltRecordResult(e, version)
}
if *resultFormat == "csv" {
err := writeResultsCsv(records)
if err != nil {
panic(err)
}
} else {
b, err := JSONMarshal(records)
if err != nil {
panic(err)
}
_, err = os.Stdout.Write(b)
if err != nil {
panic(err)
}
}
}
// Custom json marshalling function is necessary to avoid escaping <, > and & to html unicode escapes
func JSONMarshal(records []*DoltResultRecord) ([]byte, error) {
rows := &TestResultArray{Rows: records}
buffer := &bytes.Buffer{}
encoder := json.NewEncoder(buffer)
encoder.SetEscapeHTML(false)
err := encoder.Encode(rows)
return buffer.Bytes(), err
}
func NewDoltRecordResult(e *logictest.ResultLogEntry, version string) *DoltResultRecord {
var result string
switch e.Result {
case logictest.Ok:
result = "ok"
case logictest.NotOk:
result = "not ok"
case logictest.Skipped:
result = "skipped"
case logictest.Timeout:
result = "timeout"
case logictest.DidNotRun:
result = "did not run"
}
return &DoltResultRecord{
Version: version,
TestFile: e.TestFile,
LineNum: e.LineNum,
Query: e.Query,
Duration: e.Duration.Milliseconds(),
Result: result,
ErrorMessage: e.ErrorMessage,
}
}
type TestResultArray struct {
Rows []*DoltResultRecord `json:"rows"`
}
type DoltResultRecord struct {
Version string `json:"version"`
TestFile string `json:"test_file"`
LineNum int `json:"line_num"`
Query string `json:"query_string"`
Duration int64 `json:"duration"`
Result string `json:"result"`
ErrorMessage string `json:"error_message,omitempty"`
}
// fromResultCsvHeaders returns supported csv headers for a Result
func fromResultCsvHeaders() []string {
return []string{
"version",
"test_file",
"line_num",
"query_string",
"duration",
"result",
"error_message",
}
}
// fromHeaderColumnValue returns the value from the DoltResultRecord for the given
// header field
func fromHeaderColumnValue(h string, r *DoltResultRecord) (string, error) {
var val string
switch h {
case "version":
val = r.Version
case "test_file":
val = r.TestFile
case "line_num":
val = fmt.Sprintf("%d", r.LineNum)
case "query_string":
val = r.Query
case "duration":
val = fmt.Sprintf("%d", r.Duration)
case "result":
val = r.Result
case "error_message":
val = r.ErrorMessage
default:
return "", errors.Errorf("unsupported header field")
}
return val, nil
}
// writeResultsCsv writes []*DoltResultRecord to stdout in csv format
func writeResultsCsv(results []*DoltResultRecord) (err error) {
csvWriter := csv.NewWriter(os.Stdout)
// write header
headers := fromResultCsvHeaders()
if err := csvWriter.Write(headers); err != nil {
return err
}
// write rows
for _, r := range results {
row := make([]string, 0)
for _, field := range headers {
val, err := fromHeaderColumnValue(field, r)
if err != nil {
return err
}
row = append(row, val)
}
err = csvWriter.Write(row)
if err != nil {
return err
}
}
csvWriter.Flush()
if err := csvWriter.Error(); err != nil {
return err
}
return
}