Skip to content

Commit 87ca988

Browse files
committed
feat(exporter): implement registry pattern for export formats
1 parent 18dfba9 commit 87ca988

File tree

13 files changed

+323
-190
lines changed

13 files changed

+323
-190
lines changed

cmd/root.go

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,12 @@ import (
66
"os"
77
"strings"
88

9-
"github.com/fbz-tec/pgexport/core/config"
10-
"github.com/fbz-tec/pgexport/core/db"
11-
"github.com/fbz-tec/pgexport/core/exporters"
12-
"github.com/fbz-tec/pgexport/core/validation"
13-
"github.com/fbz-tec/pgexport/internal/logger"
9+
"github.com/fbz-tec/pgxport/core/config"
10+
"github.com/fbz-tec/pgxport/core/db"
11+
"github.com/fbz-tec/pgxport/core/exporters"
12+
"github.com/fbz-tec/pgxport/core/validation"
13+
"github.com/fbz-tec/pgxport/internal/logger"
14+
"github.com/fbz-tec/pgxport/internal/version"
1415
"github.com/jackc/pgx/v5"
1516
"github.com/spf13/cobra"
1617
)
@@ -110,7 +111,7 @@ func Execute() {
110111
func runExport(cmd *cobra.Command, args []string) error {
111112

112113
logger.Debug("Initializing pgxport execution environment")
113-
logger.Debug("Version: %s, Build: %s, Commit: %s", Version, BuildTime, GitCommit)
114+
logger.Debug("Version: %s, Build: %s, Commit: %s", version.AppVersion, version.BuildTime, version.GitCommit)
114115

115116
logger.Debug("Validating export parameters")
116117

@@ -139,6 +140,7 @@ func runExport(cmd *cobra.Command, args []string) error {
139140
var err error
140141
var rowCount int
141142
var rows pgx.Rows
143+
var exporter exporters.Exporter
142144

143145
if sqlFile != "" {
144146
logger.Debug("Reading SQL from file: %s", sqlFile)
@@ -188,18 +190,27 @@ func runExport(cmd *cobra.Command, args []string) error {
188190
RowPerStatement: rowPerStatement,
189191
}
190192

193+
exporter, err = exporters.GetExporter(format)
194+
if err != nil {
195+
return err
196+
}
197+
191198
if format == "csv" && withCopy {
192199
logger.Debug("Using PostgreSQL COPY mode for fast CSV export")
193-
exporter := exporters.NewCopyExporter()
194-
rowCount, err = exporter.ExportCopy(store.GetConnection(), query, outputPath, options)
200+
201+
if copyExp, ok := exporter.(exporters.CopyCapable); ok {
202+
rowCount, err = copyExp.ExportCopy(store.GetConnection(), query, outputPath, options)
203+
} else {
204+
return fmt.Errorf("format %s does not support COPY mode", format)
205+
}
195206
} else {
196207
logger.Debug("Using standard export mode for format: %s", format)
197208
rows, err = store.ExecuteQuery(context.Background(), query)
198209
if err != nil {
199210
return err
200211
}
201212
defer rows.Close()
202-
exporter := exporters.NewExporter()
213+
203214
rowCount, err = exporter.Export(rows, outputPath, options)
204215
}
205216

@@ -222,7 +233,7 @@ func validateExportParams() error {
222233

223234
// Normalize and validate format
224235
format = strings.ToLower(strings.TrimSpace(format))
225-
validFormats := []string{"csv", "json", "xml", "sql"}
236+
validFormats := exporters.ListExporters()
226237

227238
isValid := false
228239
for _, f := range validFormats {

core/db/connection.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import (
66
"net/url"
77
"time"
88

9-
"github.com/fbz-tec/pgexport/internal/logger"
9+
"github.com/fbz-tec/pgxport/internal/logger"
1010
"github.com/jackc/pgx/v5"
1111
)
1212

core/exporters/compression.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import (
1010
"strings"
1111
"time"
1212

13-
"github.com/fbz-tec/pgexport/internal/logger"
13+
"github.com/fbz-tec/pgxport/internal/logger"
1414
)
1515

1616
const (

core/exporters/csv_exporter.go

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,14 @@ import (
88
"strings"
99
"time"
1010

11-
"github.com/fbz-tec/pgexport/internal/logger"
11+
"github.com/fbz-tec/pgxport/internal/logger"
1212
"github.com/jackc/pgx/v5"
1313
)
1414

15-
// exportToCSV writes query results to a CSV file with buffered I/O
16-
func (e *dataExporter) writeCSV(rows pgx.Rows, csvPath string, options ExportOptions) (int, error) {
15+
type csvExporter struct{}
16+
17+
// Export writes query results to a CSV file with buffered I/O.
18+
func (e *csvExporter) Export(rows pgx.Rows, csvPath string, options ExportOptions) (int, error) {
1719
start := time.Now()
1820

1921
logger.Debug("Preparing CSV export (delimiter=%q, noHeader=%v, compression=%s)",
@@ -126,7 +128,7 @@ func (e *dataExporter) writeCSV(rows pgx.Rows, csvPath string, options ExportOpt
126128
return rowCount, nil
127129
}
128130

129-
func (e *dataExporter) writeCopyCSV(conn *pgx.Conn, query string, csvPath string, options ExportOptions) (int, error) {
131+
func (e *csvExporter) ExportCopy(conn *pgx.Conn, query string, csvPath string, options ExportOptions) (int, error) {
130132

131133
start := time.Now()
132134
logger.Debug("Starting PostgreSQL COPY export (noHeader=%v, compression=%s)", options.NoHeader, options.Compression)
@@ -151,3 +153,7 @@ func (e *dataExporter) writeCopyCSV(conn *pgx.Conn, query string, csvPath string
151153
return rowCount, nil
152154

153155
}
156+
157+
func init() {
158+
MustRegisterExporter(FormatCSV, func() Exporter { return &csvExporter{} })
159+
}

core/exporters/csv_exporter_test.go

Lines changed: 62 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import (
1212
"github.com/jackc/pgx/v5"
1313
)
1414

15-
func TestWriteCSV(t *testing.T) {
15+
func TestExportCSV(t *testing.T) {
1616
conn, cleanup := setupTestDB(t)
1717
defer cleanup()
1818

@@ -183,7 +183,10 @@ func TestWriteCSV(t *testing.T) {
183183
}
184184
defer rows.Close()
185185

186-
exporter := &dataExporter{}
186+
exporter, err := GetExporter(FormatCSV)
187+
if err != nil {
188+
t.Fatalf("Failed to get sql exporter: %v", err)
189+
}
187190
options := ExportOptions{
188191
Format: FormatCSV,
189192
Delimiter: tt.delimiter,
@@ -192,10 +195,10 @@ func TestWriteCSV(t *testing.T) {
192195
TimeZone: "",
193196
}
194197

195-
_, err = exporter.writeCSV(rows, outputPath, options)
198+
_, err = exporter.Export(rows, outputPath, options)
196199

197200
if (err != nil) != tt.wantErr {
198-
t.Errorf("writeCSV() error = %v, wantErr %v", err, tt.wantErr)
201+
t.Errorf("Export() error = %v, wantErr %v", err, tt.wantErr)
199202
return
200203
}
201204

@@ -266,7 +269,10 @@ func TestWriteCSVTimeFormatting(t *testing.T) {
266269
}
267270
defer rows.Close()
268271

269-
exporter := &dataExporter{}
272+
exporter, err := GetExporter(FormatCSV)
273+
if err != nil {
274+
t.Fatalf("Failed to get sql exporter: %v", err)
275+
}
270276
options := ExportOptions{
271277
Format: FormatCSV,
272278
Delimiter: ',',
@@ -275,9 +281,9 @@ func TestWriteCSVTimeFormatting(t *testing.T) {
275281
TimeZone: tt.timeZone,
276282
}
277283

278-
_, err = exporter.writeCSV(rows, outputPath, options)
284+
_, err = exporter.Export(rows, outputPath, options)
279285
if err != nil {
280-
t.Fatalf("writeCSV() error: %v", err)
286+
t.Fatalf("Export() error: %v", err)
281287
}
282288

283289
content, err := os.ReadFile(outputPath)
@@ -315,7 +321,10 @@ func TestWriteCSVDataTypes(t *testing.T) {
315321
}
316322
defer rows.Close()
317323

318-
exporter := &dataExporter{}
324+
exporter, err := GetExporter(FormatCSV)
325+
if err != nil {
326+
t.Fatalf("Failed to get sql exporter: %v", err)
327+
}
319328
options := ExportOptions{
320329
Format: FormatCSV,
321330
Delimiter: ',',
@@ -324,9 +333,9 @@ func TestWriteCSVDataTypes(t *testing.T) {
324333
TimeZone: "",
325334
}
326335

327-
rowCount, err := exporter.writeCSV(rows, outputPath, options)
336+
rowCount, err := exporter.Export(rows, outputPath, options)
328337
if err != nil {
329-
t.Fatalf("writeCSV() error: %v", err)
338+
t.Fatalf("Export() error: %v", err)
330339
}
331340

332341
if rowCount != 1 {
@@ -432,14 +441,24 @@ func TestWriteCopyCSV(t *testing.T) {
432441
tmpDir := t.TempDir()
433442
outputPath := filepath.Join(tmpDir, "output.csv")
434443

435-
exporter := &dataExporter{}
444+
exporter, err := GetExporter(FormatCSV)
445+
if err != nil {
446+
t.Fatalf("Failed to get sql exporter: %v", err)
447+
}
448+
436449
options := ExportOptions{
437450
Format: FormatCSV,
438451
Delimiter: tt.delimiter,
439452
Compression: "none",
440453
}
441454

442-
rowCount, err := exporter.writeCopyCSV(conn, tt.query, outputPath, options)
455+
copyExp, ok := exporter.(CopyCapable)
456+
457+
if !ok {
458+
t.Fatalf("Copy mode is not supported: %v", err)
459+
}
460+
461+
rowCount, err := copyExp.ExportCopy(conn, tt.query, outputPath, options)
443462

444463
if (err != nil) != tt.wantErr {
445464
t.Errorf("writeCopyCSV() error = %v, wantErr %v", err, tt.wantErr)
@@ -480,7 +499,10 @@ func TestWriteCSVLargeDataset(t *testing.T) {
480499
}
481500
defer rows.Close()
482501

483-
exporter := &dataExporter{}
502+
exporter, err := GetExporter(FormatCSV)
503+
if err != nil {
504+
t.Fatalf("Failed to get sql exporter: %v", err)
505+
}
484506
options := ExportOptions{
485507
Format: FormatCSV,
486508
Delimiter: ',',
@@ -490,11 +512,11 @@ func TestWriteCSVLargeDataset(t *testing.T) {
490512
}
491513

492514
start := time.Now()
493-
rowCount, err := exporter.writeCSV(rows, outputPath, options)
515+
rowCount, err := exporter.Export(rows, outputPath, options)
494516
duration := time.Since(start)
495517

496518
if err != nil {
497-
t.Fatalf("writeCSV() error: %v", err)
519+
t.Fatalf("Export() error: %v", err)
498520
}
499521

500522
if rowCount != 10000 {
@@ -688,7 +710,10 @@ func TestWriteCSVNoHeader(t *testing.T) {
688710
}
689711
defer rows.Close()
690712

691-
exporter := &dataExporter{}
713+
exporter, err := GetExporter(FormatCSV)
714+
if err != nil {
715+
t.Fatalf("Failed to get sql exporter: %v", err)
716+
}
692717
options := ExportOptions{
693718
Format: FormatCSV,
694719
Delimiter: ',',
@@ -698,9 +723,9 @@ func TestWriteCSVNoHeader(t *testing.T) {
698723
NoHeader: tt.noHeader,
699724
}
700725

701-
_, err = exporter.writeCSV(rows, outputPath, options)
726+
_, err = exporter.Export(rows, outputPath, options)
702727
if err != nil {
703-
t.Fatalf("writeCSV() error: %v", err)
728+
t.Fatalf("Export() error: %v", err)
704729
}
705730

706731
tt.checkFunc(t, outputPath, tt.noHeader)
@@ -805,15 +830,26 @@ func TestWriteCopyCSVNoHeader(t *testing.T) {
805830
tmpDir := t.TempDir()
806831
outputPath := filepath.Join(tmpDir, "output.csv")
807832

808-
exporter := &dataExporter{}
833+
exporter, err := GetExporter(FormatCSV)
834+
if err != nil {
835+
t.Fatalf("Failed to get sql exporter: %v", err)
836+
}
837+
809838
options := ExportOptions{
810839
Format: FormatCSV,
811840
Delimiter: ',',
812841
Compression: "none",
813842
NoHeader: tt.noHeader,
814843
}
815844

816-
_, err := exporter.writeCopyCSV(conn, tt.query, outputPath, options)
845+
copyExp, ok := exporter.(CopyCapable)
846+
847+
if !ok {
848+
t.Fatalf("Copy mode is not supported by this exporter")
849+
}
850+
851+
_, err = copyExp.ExportCopy(conn, tt.query, outputPath, options)
852+
817853
if err != nil {
818854
t.Fatalf("writeCopyCSV() error: %v", err)
819855
}
@@ -823,7 +859,7 @@ func TestWriteCopyCSVNoHeader(t *testing.T) {
823859
}
824860
}
825861

826-
func BenchmarkWriteCSV(b *testing.B) {
862+
func BenchmarkExportCSV(b *testing.B) {
827863
testURL := os.Getenv("DB_TEST_URL")
828864
if testURL == "" {
829865
b.Skip("Skipping benchmark: DB_TEST_URL not set")
@@ -837,7 +873,10 @@ func BenchmarkWriteCSV(b *testing.B) {
837873
defer conn.Close(ctx)
838874

839875
tmpDir := b.TempDir()
840-
exporter := &dataExporter{}
876+
exporter, err := GetExporter(FormatCSV)
877+
if err != nil {
878+
b.Fatalf("Failed to get sql exporter: %v", err)
879+
}
841880
options := ExportOptions{
842881
Format: FormatCSV,
843882
Delimiter: ',',
@@ -855,7 +894,7 @@ func BenchmarkWriteCSV(b *testing.B) {
855894
b.Fatalf("Query failed: %v", err)
856895
}
857896

858-
_, err = exporter.writeCSV(rows, outputPath, options)
897+
_, err = exporter.Export(rows, outputPath, options)
859898
if err != nil {
860899
b.Fatalf("writeCSV failed: %v", err)
861900
}

0 commit comments

Comments
 (0)