@@ -17,16 +17,22 @@ limitations under the License.
1717package immuadmin
1818
1919import (
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+
393629func prepareDatabaseNullableSettings (flags * pflag.FlagSet ) (* schema.DatabaseNullableSettings , error ) {
394630 var err error
395631
0 commit comments