Skip to content

Commit 4035843

Browse files
committed
refactor(to-sqlite): migrate to pkg/csvops library
Completes the pkg/csvops migration — all 7 CSV operations now live as typed functions with Progress callbacks and context cancellation. ToSQLite exposes IfExistsAction as a typed enum (Replace/Skip/Append/Fail) and returns Skipped in the result instead of erroring. Identifier safety (QuoteIdent) and SanitizeTableName move to the library and get direct unit tests, including the adversarial-table-name regression that was fixed in v0.3.0.
1 parent 6f0366d commit 4035843

4 files changed

Lines changed: 416 additions & 163 deletions

File tree

cmd/to-sqlite.go

Lines changed: 21 additions & 141 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,13 @@
11
package cmd
22

33
import (
4-
"database/sql"
5-
"encoding/csv"
4+
"context"
65
"fmt"
7-
"io"
8-
"os"
96
"path/filepath"
10-
"regexp"
11-
"strings"
127

8+
"github.com/maherelgamil/csvops/pkg/csvops"
139
"github.com/schollz/progressbar/v3"
1410
"github.com/spf13/cobra"
15-
_ "modernc.org/sqlite"
1611
)
1712

1813
var (
@@ -23,14 +18,6 @@ var (
2318
csvToSqliteIfExists string
2419
)
2520

26-
var identSanitizer = regexp.MustCompile(`[^a-zA-Z0-9_]`)
27-
28-
// quoteIdent safely quotes a SQLite identifier by wrapping in double quotes
29-
// and escaping embedded quotes.
30-
func quoteIdent(name string) string {
31-
return `"` + strings.ReplaceAll(name, `"`, `""`) + `"`
32-
}
33-
3421
var toSqliteCmd = &cobra.Command{
3522
Use: "to-sqlite",
3623
Short: "Convert a CSV file into a SQLite database",
@@ -40,141 +27,34 @@ var toSqliteCmd = &cobra.Command{
4027
return err
4128
}
4229

43-
switch csvToSqliteIfExists {
44-
case "replace", "skip", "append", "fail":
45-
default:
46-
return fmt.Errorf("--if-exists must be one of: replace, skip, append, fail")
47-
}
48-
49-
if csvToSqliteTable == "" {
50-
base := filepath.Base(csvToSqliteInput)
51-
name := strings.TrimSuffix(base, filepath.Ext(base))
52-
csvToSqliteTable = identSanitizer.ReplaceAllString(name, "_")
53-
}
54-
55-
file, err := os.Open(csvToSqliteInput)
56-
if err != nil {
57-
return fmt.Errorf("failed to open input file: %w", err)
58-
}
59-
defer file.Close()
60-
61-
reader := csv.NewReader(file)
62-
reader.Comma = delim
63-
64-
headers, err := reader.Read()
65-
if err != nil {
66-
return fmt.Errorf("failed to read CSV header: %w", err)
67-
}
68-
69-
totalRows, err := countDataRows(csvToSqliteInput, delim)
70-
if err != nil {
71-
return err
72-
}
73-
74-
dbPath, _ := filepath.Abs(csvToSqliteOutput)
75-
db, err := sql.Open("sqlite", dbPath)
76-
if err != nil {
77-
return fmt.Errorf("failed to open sqlite db: %w", err)
78-
}
79-
defer db.Close()
80-
81-
tableIdent := quoteIdent(csvToSqliteTable)
82-
83-
exists, err := tableExists(db, csvToSqliteTable)
84-
if err != nil {
85-
return fmt.Errorf("failed to check if table exists: %w", err)
86-
}
87-
88-
if exists {
89-
switch csvToSqliteIfExists {
90-
case "fail":
91-
return fmt.Errorf("table %q already exists (use --if-exists replace|skip|append)", csvToSqliteTable)
92-
case "skip":
93-
fmt.Printf("⚠️ Table %q already exists, skipping.\n", csvToSqliteTable)
94-
return nil
95-
case "replace":
96-
if _, err := db.Exec("DROP TABLE " + tableIdent); err != nil {
97-
return fmt.Errorf("failed to drop existing table: %w", err)
30+
var bar *progressbar.ProgressBar
31+
res, err := csvops.ToSQLite(context.Background(), csvops.ToSQLiteOptions{
32+
Input: csvToSqliteInput,
33+
DBPath: csvToSqliteOutput,
34+
Table: csvToSqliteTable,
35+
Delimiter: delim,
36+
IfExists: csvops.IfExistsAction(csvToSqliteIfExists),
37+
Progress: func(done, total int64) {
38+
if bar == nil {
39+
bar = progressbar.Default(total, "Converting")
9840
}
99-
exists = false
100-
case "append":
101-
// keep existing table; rows will be inserted below
102-
}
103-
}
104-
105-
if !exists {
106-
cols := make([]string, len(headers))
107-
for i, h := range headers {
108-
cols[i] = quoteIdent(h) + " TEXT"
109-
}
110-
createStmt := fmt.Sprintf("CREATE TABLE %s (%s)", tableIdent, strings.Join(cols, ", "))
111-
if _, err := db.Exec(createStmt); err != nil {
112-
return fmt.Errorf("failed to create table: %w", err)
113-
}
114-
}
115-
116-
placeholders := strings.TrimRight(strings.Repeat("?,", len(headers)), ",")
117-
insertStmt := fmt.Sprintf("INSERT INTO %s VALUES (%s)", tableIdent, placeholders)
118-
119-
tx, err := db.Begin()
120-
if err != nil {
121-
return fmt.Errorf("failed to begin transaction: %w", err)
122-
}
123-
stmt, err := tx.Prepare(insertStmt)
41+
_ = bar.Set64(done)
42+
},
43+
})
12444
if err != nil {
125-
_ = tx.Rollback()
126-
return fmt.Errorf("failed to prepare insert statement: %w", err)
127-
}
128-
defer stmt.Close()
129-
130-
bar := progressbar.Default(totalRows, "Converting")
131-
rowCount := 0
132-
for {
133-
record, err := reader.Read()
134-
if err == io.EOF {
135-
break
136-
}
137-
if err != nil {
138-
_ = tx.Rollback()
139-
return fmt.Errorf("failed to read row: %w", err)
140-
}
141-
vals := make([]interface{}, len(record))
142-
for i := range record {
143-
vals[i] = record[i]
144-
}
145-
if _, err := stmt.Exec(vals...); err != nil {
146-
_ = tx.Rollback()
147-
return fmt.Errorf("failed to insert row: %w", err)
148-
}
149-
_ = bar.Add(1)
150-
rowCount++
45+
return err
15146
}
15247

153-
if err := tx.Commit(); err != nil {
154-
return fmt.Errorf("failed to commit transaction: %w", err)
48+
if res.Skipped {
49+
fmt.Printf("⚠️ Table %q already exists, skipped.\n", res.Table)
50+
return nil
15551
}
156-
157-
fmt.Printf("\n✅ Imported %d rows into %s\n", rowCount, dbPath)
52+
dbPath, _ := filepath.Abs(csvToSqliteOutput)
53+
fmt.Printf("\n✅ Imported %d rows into %s\n", res.RowsImported, dbPath)
15854
return nil
15955
},
16056
}
16157

162-
func tableExists(db *sql.DB, name string) (bool, error) {
163-
row := db.QueryRow(
164-
"SELECT 1 FROM sqlite_master WHERE type='table' AND name=?",
165-
name,
166-
)
167-
var one int
168-
err := row.Scan(&one)
169-
if err == sql.ErrNoRows {
170-
return false, nil
171-
}
172-
if err != nil {
173-
return false, err
174-
}
175-
return true, nil
176-
}
177-
17858
func init() {
17959
rootCmd.AddCommand(toSqliteCmd)
18060

cmd/to-sqlite_test.go

Lines changed: 0 additions & 22 deletions
This file was deleted.

0 commit comments

Comments
 (0)