-
Notifications
You must be signed in to change notification settings - Fork 10
/
Copy pathsqlcsv.go
206 lines (191 loc) · 4.98 KB
/
sqlcsv.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
package datatools
import (
"database/sql"
"encoding/csv"
"fmt"
"os"
"strings"
// 3rd Party packages
//sql "github.com/jmoiron/sqlx"
// Database specific drivers
_ "github.com/glebarez/go-sqlite"
_ "github.com/go-sql-driver/mysql"
_ "github.com/lib/pq"
)
// SQLCfg holds the information for connecting to
// a SQLStore and options for the CSV output.
type SQLCfg struct {
DSN string `json:"dsn_url,omitempty"`
WriteHeaderRow bool `json:"header_row,omitempty"`
Delimiter string `json:"delimiter,omitempty"`
UseCRLF bool `json:"use_crlf,omitempty"`
}
// SQLSrouce represents a wrapper SQL database drivers
// using a common struct.
type SQLStore struct {
// Protocol holds the database type string, e.g. mysql, sqlite, pg
Protocol string
// Host name of service where to connect
Host string
// Port of service
Port string
// Database name you're going to query against
Database string
// User name for access a database service
User string
// Password for accessing a database service
Password string
// WriteHeaderRow tracks desired behavior about generating
// a header row in the CSV encoded output. NOTE: using OpenSQLStore()
// sets this value to true.
WriteHeaderRow bool
// workpath is the working directory to use when accessing SQLite3
// related database paths
workPath string
// driverName is the database driver is the type of database we're accessing
driverName string
// the data source name
dsn string
// The db handle of the opened connection
db *sql.DB
}
func dsnFixUp(driverName string, dsn string, workPath string) string {
switch driverName {
case "postgres":
return fmt.Sprintf("%s://%s", driverName, dsn)
case "sqlite":
// NOTE: the db needs to be stored in the dataset directory
// to keep the dataset easily movable.
//dbName := path.Base(dsn)
return dsn
//path.Join(workPath, dbName)
}
return dsn
}
// OpenSQLStore opens a mysql, postgres or SQLite database
// based on a data source name expressed as a URL.
// The URL is formed by using the "protocol" to identify
// the service (e.g. "mysql://", "sqlite3://", "pg://")
// followed by a data source name per golang sql package
// documentation.
func OpenSQLStore(dsnURL string) (*SQLStore, error) {
if !strings.Contains(dsnURL, "://") {
return nil, fmt.Errorf("missing protocol in url scheme")
}
driverName, dsn, ok := strings.Cut(dsnURL, "://")
if !ok {
return nil, fmt.Errorf("could not parse DSN URI, got %q", dsnURL)
}
var err error
store := new(SQLStore)
store.driverName = driverName
store.workPath, err = os.Getwd()
if err != nil {
return nil, err
}
store.dsn = dsnFixUp(driverName, dsn, store.workPath)
db, err := sql.Open(store.driverName, store.dsn)
if err != nil {
return nil, err
}
store.db = db
store.WriteHeaderRow = true
return store, nil
}
// Close the previously openned database resource
func (store *SQLStore) Close() error {
if store.db != nil {
return store.db.Close()
}
return nil
}
// QueryToCSV runs a SQL query statement and returns to the results
// CSV encoded via an io.Writer
func (store *SQLStore) QueryToCSV(out *csv.Writer, stmt string) error {
rows, err := store.db.Query(stmt)
if err != nil {
return err
}
defer rows.Close()
columns, err := rows.Columns()
if err != nil {
return err
}
columnTypes, err := rows.ColumnTypes()
if err != nil {
return err
}
// Write out our header row is configurable
if store.WriteHeaderRow {
if err := out.Write(columns); err != nil {
return err
}
}
// Make an array of cells
cells := make([]string, len(columnTypes))
vals := make([]interface{}, len(columnTypes))
for i := 0; i < len(columnTypes); i++ {
vals[i] = new(interface{})
}
for rows.Next() {
// Retrieve the raw column data from the row
err := rows.Scan(vals[:]...)
if err != nil {
return nil
}
for i := 0; i < len(columnTypes); i++ {
val := *vals[i].(*interface{})
if val == nil {
// FIXME: this should be configurable as 'NULL' or empty
// string.
cells[i] = "NULL"
} else {
switch val.(type) {
case []byte:
s := fmt.Sprintf("%s", val.([]byte))
cells[i] = s
case string:
s := val.(string)
cells[i] = s
case bool:
x := val.(bool)
cells[i] = fmt.Sprintf("%T", x)
case float32:
x := val.(float32)
cells[i] = fmt.Sprintf("%f", x)
case float64:
x := val.(float64)
cells[i] = fmt.Sprintf("%f", x)
case int:
x := val.(int)
cells[i] = fmt.Sprintf("%d", x)
case int8:
x := val.(int8)
cells[i] = fmt.Sprintf("%d", x)
case int16:
x := val.(int16)
cells[i] = fmt.Sprintf("%d", x)
case int32:
x := val.(int32)
cells[i] = fmt.Sprintf("%d", x)
case int64:
x := val.(int64)
cells[i] = fmt.Sprintf("%d", x)
default:
cells[i] = fmt.Sprintf("%+v", val)
}
}
}
if err := out.Write(cells); err != nil {
return err
}
}
out.Flush()
if err := out.Error(); err != nil {
return err
}
if err := rows.Err(); err != nil {
return err
}
return nil
}