11package cmd
22
33import (
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
1813var (
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-
3421var 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-
17858func init () {
17959 rootCmd .AddCommand (toSqliteCmd )
18060
0 commit comments