Skip to content

Commit 9237a83

Browse files
committed
Implement CSV import/export of tables
Signed-off-by: Stefano Scafiti <[email protected]>
1 parent 0834409 commit 9237a83

File tree

4 files changed

+240
-3
lines changed

4 files changed

+240
-3
lines changed

cmd/immuadmin/command/commandline.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ func (cl *commandline) Register(rootCmd *cobra.Command) *cobra.Command {
9696
cl.stats(rootCmd)
9797
cl.serverConfig(rootCmd)
9898
cl.database(rootCmd)
99+
99100
return rootCmd
100101
}
101102

cmd/immuadmin/command/database.go

Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,22 @@ limitations under the License.
1717
package immuadmin
1818

1919
import (
20+
"encoding/csv"
2021
"fmt"
22+
"io"
23+
"os"
24+
"path"
2125
"strconv"
2226
"strings"
2327
"time"
2428

2529
"github.com/codenotary/immudb/cmd/helper"
2630
c "github.com/codenotary/immudb/cmd/helper"
31+
"github.com/codenotary/immudb/embedded/sql"
2732
"github.com/codenotary/immudb/embedded/store"
2833
"github.com/codenotary/immudb/embedded/tbtree"
2934
"github.com/codenotary/immudb/pkg/api/schema"
35+
"github.com/codenotary/immudb/pkg/client"
3036
"github.com/codenotary/immudb/pkg/database"
3137
"github.com/codenotary/immudb/pkg/replication"
3238
"github.com/spf13/cobra"
@@ -386,10 +392,240 @@ func (cl *commandline) database(cmd *cobra.Command) {
386392
dbCmd.AddCommand(flushCmd)
387393
dbCmd.AddCommand(compactCmd)
388394
dbCmd.AddCommand(truncateCmd)
395+
dbCmd.AddCommand(cl.createExportCmd())
396+
dbCmd.AddCommand(cl.createImportCmd())
389397

390398
cmd.AddCommand(dbCmd)
391399
}
392400

401+
func (cl *commandline) createExportCmd() *cobra.Command {
402+
exportCmd := &cobra.Command{
403+
Use: "export",
404+
Short: "Dump an SQL table to a CSV file",
405+
Aliases: []string{"e"},
406+
ArgAliases: []string{"table"},
407+
PersistentPreRunE: cl.ConfigChain(cl.connect),
408+
PersistentPostRun: cl.disconnect,
409+
RunE: func(cmd *cobra.Command, args []string) error {
410+
table := args[0]
411+
412+
outputPath, _ := cmd.Flags().GetString("o")
413+
if outputPath == "" {
414+
wd, err := os.Getwd()
415+
if err != nil {
416+
return err
417+
}
418+
outputPath = path.Join(wd, table) + ".csv"
419+
}
420+
421+
reader, err := cl.immuClient.SQLQueryReader(cl.context, fmt.Sprintf("SELECT * FROM %s", table), nil)
422+
if err != nil {
423+
return err
424+
}
425+
defer reader.Close()
426+
427+
csvFile, err := os.Create(outputPath)
428+
if err != nil {
429+
return err
430+
}
431+
defer csvFile.Close()
432+
433+
sep, err := cmd.Flags().GetString("s")
434+
if err != nil {
435+
return err
436+
}
437+
if len(sep) != 1 {
438+
return fmt.Errorf("invalid separator")
439+
}
440+
441+
writer := csv.NewWriter(csvFile)
442+
writer.Comma = rune(sep[0])
443+
writer.UseCRLF = true
444+
defer writer.Flush()
445+
446+
cols := reader.Columns()
447+
448+
colNames := make([]string, len(cols))
449+
for i, col := range cols {
450+
colNames[i] = formatColName(col.Name)
451+
}
452+
453+
if err := writer.Write(colNames); err != nil {
454+
return err
455+
}
456+
457+
out := make([]string, len(cols))
458+
for reader.Next() {
459+
row, err := reader.Read()
460+
if err != nil {
461+
return err
462+
}
463+
464+
if err := rowToCSV(row, cols, out); err != nil {
465+
return err
466+
}
467+
468+
if err := writer.Write(out); err != nil {
469+
return err
470+
}
471+
}
472+
return writer.Error()
473+
},
474+
Args: cobra.ExactArgs(1),
475+
}
476+
exportCmd.Flags().String("o", "", "output")
477+
exportCmd.Flags().String("s", ",", "separator")
478+
479+
return exportCmd
480+
}
481+
482+
func rowToCSV(row client.Row, cols []client.Column, out []string) error {
483+
for i, v := range row {
484+
colType := cols[i].Type
485+
rv, err := renderValue(v, colType)
486+
if err != nil {
487+
return err
488+
}
489+
out[i] = rv
490+
}
491+
return nil
492+
}
493+
494+
func renderValue(v interface{}, colType string) (string, error) {
495+
switch colType {
496+
case sql.VarcharType, sql.JSONType, sql.UUIDType:
497+
s, isStr := v.(string)
498+
if !isStr {
499+
return "", fmt.Errorf("invalid value received")
500+
}
501+
return s, nil
502+
default:
503+
sqlVal, err := schema.AsSQLValue(v)
504+
if err != nil {
505+
return "", err
506+
}
507+
return schema.RenderValue(sqlVal.Value), nil
508+
}
509+
}
510+
511+
func (cl *commandline) createImportCmd() *cobra.Command {
512+
importCmd := &cobra.Command{
513+
Use: "import",
514+
Short: "Insert data to an existing table from a csv file",
515+
Aliases: []string{"i"},
516+
ArgAliases: []string{"file"},
517+
PersistentPreRunE: cl.ConfigChain(cl.connect),
518+
PersistentPostRun: cl.disconnect,
519+
RunE: func(cmd *cobra.Command, args []string) error {
520+
inputPath := args[0]
521+
522+
csvFile, err := os.Open(inputPath)
523+
if err != nil {
524+
return err
525+
}
526+
defer csvFile.Close()
527+
528+
sep, err := cmd.Flags().GetString("s")
529+
if err != nil {
530+
return err
531+
}
532+
if len(sep) != 1 {
533+
return fmt.Errorf("invalid separator")
534+
}
535+
536+
reader := csv.NewReader(csvFile)
537+
reader.Comma = rune(sep[0])
538+
reader.ReuseRecord = true
539+
540+
hasHeader, err := cmd.Flags().GetBool("h")
541+
if err != nil {
542+
return err
543+
}
544+
545+
table, err := cmd.Flags().GetString("t")
546+
if err != nil {
547+
return err
548+
}
549+
if table == "" {
550+
return fmt.Errorf("table name not specified")
551+
}
552+
553+
if hasHeader {
554+
_, err := reader.Read()
555+
if err != nil && err != io.EOF {
556+
return nil
557+
}
558+
}
559+
560+
// fetch column information
561+
res, err := cl.immuClient.SQLQuery(cl.context, fmt.Sprintf("SELECT * FROM %s WHERE 1 = 0", table), nil, false)
562+
if err != nil {
563+
return err
564+
}
565+
566+
cols := make([]string, len(res.Columns))
567+
for i, col := range res.Columns {
568+
cols[i] = formatColName(col.Name)
569+
}
570+
571+
row, err := reader.Read()
572+
for err == nil {
573+
if len(row) != len(cols) {
574+
return fmt.Errorf("wrong number of columns")
575+
}
576+
577+
for i, v := range row {
578+
row[i] = formatInsertValue(v, res.Columns[i].Type)
579+
}
580+
581+
_, err = cl.immuClient.SQLExec(
582+
cl.context,
583+
fmt.Sprintf("INSERT INTO %s(%s) VALUES (%s)", table, strings.Join(cols, ","), strings.Join(row, ",")),
584+
nil,
585+
)
586+
if err != nil {
587+
return err
588+
}
589+
row, err = reader.Read()
590+
}
591+
if err != io.EOF {
592+
return err
593+
}
594+
return nil
595+
},
596+
Args: cobra.ExactArgs(1),
597+
}
598+
importCmd.Flags().String("t", "", "table")
599+
importCmd.Flags().Bool("h", true, "interpret the first column as header")
600+
importCmd.Flags().String("s", ",", "separator")
601+
602+
return importCmd
603+
}
604+
605+
func formatColName(col string) string {
606+
idx := strings.Index(col, ".")
607+
if idx >= 0 {
608+
return col[idx+1 : len(col)-1]
609+
}
610+
return col
611+
}
612+
613+
func formatInsertValue(v string, colType string) string {
614+
if v == "NULL" {
615+
return v
616+
}
617+
618+
switch colType {
619+
case sql.VarcharType:
620+
return fmt.Sprintf("'%s'", v)
621+
case sql.TimestampType, sql.JSONType, sql.UUIDType:
622+
return fmt.Sprintf("CAST ('%s' AS %s)", v, colType)
623+
case sql.BLOBType:
624+
return fmt.Sprintf("x'%s'", v)
625+
}
626+
return v
627+
}
628+
393629
func prepareDatabaseNullableSettings(flags *pflag.FlagSet) (*schema.DatabaseNullableSettings, error) {
394630
var err error
395631

pkg/api/schema/sql.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ func EncodeParams(params map[string]interface{}) ([]*NamedParam, error) {
3232

3333
i := 0
3434
for n, v := range params {
35-
sqlVal, err := asSQLValue(v)
35+
sqlVal, err := AsSQLValue(v)
3636
if err != nil {
3737
return nil, err
3838
}
@@ -52,7 +52,7 @@ func NamedParamsFromProto(protoParams []*NamedParam) map[string]interface{} {
5252
return params
5353
}
5454

55-
func asSQLValue(v interface{}) (*SQLValue, error) {
55+
func AsSQLValue(v interface{}) (*SQLValue, error) {
5656
if v == nil {
5757
return &SQLValue{Value: &SQLValue_Null{}}, nil
5858
}

pkg/api/schema/sql_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ func TestAsSQLValue(t *testing.T) {
106106
},
107107
} {
108108
t.Run(d.n, func(t *testing.T) {
109-
sqlVal, err := asSQLValue(d.val)
109+
sqlVal, err := AsSQLValue(d.val)
110110
require.EqualValues(t, d.sqlVal, sqlVal)
111111
if d.isErr {
112112
require.ErrorIs(t, err, sql.ErrInvalidValue)

0 commit comments

Comments
 (0)