@@ -2074,6 +2074,85 @@ func convertPostgreSQLValue(value interface{}, pgType string) interface{} {
20742074 return value
20752075}
20762076
2077+ // calculateUncompressedRowSize estimates the uncompressed size of a single row
2078+ // based on the output format. This is used for metrics and capacity planning.
2079+ func calculateUncompressedRowSize (row map [string ]interface {}, format string , columnNames []string ) int64 {
2080+ switch format {
2081+ case formatters .FormatJSONL :
2082+ // JSONL: JSON marshaling + newline
2083+ jsonBytes , _ := json .Marshal (row )
2084+ return int64 (len (jsonBytes )) + 1 // +1 for newline
2085+
2086+ case formatters .FormatCSV :
2087+ // CSV: Estimate size accounting for commas, escaping, and newline
2088+ // CSV writer handles escaping, so we estimate conservatively
2089+ var size int64
2090+ for i , col := range columnNames {
2091+ if i > 0 {
2092+ size ++ // comma
2093+ }
2094+ val := row [col ]
2095+ if val == nil {
2096+ // Empty field
2097+ continue
2098+ }
2099+ // Estimate: string representation + potential escaping (double quotes + escaped quotes)
2100+ // CSV escaping: if value contains comma, newline, or quote, it's wrapped in quotes
2101+ // and internal quotes are doubled
2102+ valStr := fmt .Sprintf ("%v" , val )
2103+ needsEscaping := strings .Contains (valStr , "," ) || strings .Contains (valStr , "\n " ) || strings .Contains (valStr , `"` )
2104+ if needsEscaping {
2105+ size += int64 (len (valStr )) + 2 + int64 (strings .Count (valStr , `"` )) // quotes + escaped quotes
2106+ } else {
2107+ size += int64 (len (valStr ))
2108+ }
2109+ }
2110+ size ++ // newline
2111+ return size
2112+
2113+ case formatters .FormatParquet :
2114+ // Parquet: Binary columnar format - estimate based on data types
2115+ // Parquet is typically more compact than JSON, but we can't easily calculate
2116+ // without actually writing. Use a type-based estimate as approximation.
2117+ var size int64
2118+ for _ , val := range row {
2119+ if val == nil {
2120+ continue // Nulls take minimal space in Parquet
2121+ }
2122+ switch v := val .(type ) {
2123+ case bool :
2124+ size += 1 // 1 byte
2125+ case int , int8 , int16 , int32 :
2126+ size += 4 // 4 bytes
2127+ case int64 :
2128+ size += 8 // 8 bytes
2129+ case float32 :
2130+ size += 4 // 4 bytes
2131+ case float64 :
2132+ size += 8 // 8 bytes
2133+ case string :
2134+ // String length + overhead (dictionary encoding can reduce this, but we estimate conservatively)
2135+ size += int64 (len (v )) + 4 // length prefix + string data
2136+ case []byte :
2137+ size += int64 (len (v )) + 4 // length prefix + bytes
2138+ case time.Time :
2139+ size += 8 // timestamp typically 8 bytes
2140+ default :
2141+ // Fallback: estimate as string representation
2142+ size += int64 (len (fmt .Sprintf ("%v" , v ))) + 4
2143+ }
2144+ }
2145+ // Add overhead for Parquet metadata (column overhead, row group overhead)
2146+ // This is a rough estimate - actual Parquet files have additional overhead
2147+ return size + int64 (len (row )* 2 ) // ~2 bytes overhead per column
2148+
2149+ default :
2150+ // Fallback: use JSON size
2151+ jsonBytes , _ := json .Marshal (row )
2152+ return int64 (len (jsonBytes )) + 1
2153+ }
2154+ }
2155+
20772156// extractPartitionDataStreaming extracts partition data using streaming architecture
20782157// This streams data in chunks to a temp file, avoiding loading everything into memory
20792158//
@@ -2167,14 +2246,23 @@ func (a *Archiver) extractPartitionDataStreaming(partition PartitionInfo, progra
21672246 // Build column list for SELECT query
21682247 columns := schema .GetColumns ()
21692248 columnNames := make ([]string , len (columns ))
2249+ unquotedColumnNames := make ([]string , len (columns ))
21702250 for i , col := range columns {
2251+ unquotedColumnNames [i ] = col .GetName ()
21712252 columnNames [i ] = pq .QuoteIdentifier (col .GetName ())
21722253 }
21732254
21742255 quotedTable := pq .QuoteIdentifier (partition .TableName )
21752256 //nolint:gosec // G201: SQL string formatting is safe here - all identifiers are properly quoted via pq.QuoteIdentifier
21762257 query := fmt .Sprintf ("SELECT %s FROM %s" , strings .Join (columnNames , ", " ), quotedTable )
21772258
2259+ // Account for CSV header row in uncompressed size
2260+ if a .config .OutputFormat == formatters .FormatCSV {
2261+ // CSV header: column names separated by commas + newline
2262+ headerSize := int64 (len (strings .Join (unquotedColumnNames , "," )) + 1 ) // commas + newline
2263+ uncompressedSize += headerSize
2264+ }
2265+
21782266 rows , queryErr := a .db .QueryContext (a .ctx , query )
21792267 if queryErr != nil {
21802268 streamWriter .Close ()
@@ -2251,10 +2339,9 @@ func (a *Archiver) extractPartitionDataStreaming(partition PartitionInfo, progra
22512339 return
22522340 }
22532341
2254- // Track uncompressed size (approximate - JSON size of chunk)
2342+ // Track uncompressed size based on output format
22552343 for _ , row := range chunk {
2256- jsonBytes , _ := json .Marshal (row )
2257- uncompressedSize += int64 (len (jsonBytes )) + 1 // +1 for newline
2344+ uncompressedSize += calculateUncompressedRowSize (row , a .config .OutputFormat , unquotedColumnNames )
22582345 }
22592346
22602347 chunk = chunk [:0 ] // Reset slice, keeping capacity
@@ -2289,8 +2376,7 @@ func (a *Archiver) extractPartitionDataStreaming(partition PartitionInfo, progra
22892376
22902377 // Track uncompressed size of final chunk
22912378 for _ , row := range chunk {
2292- jsonBytes , _ := json .Marshal (row )
2293- uncompressedSize += int64 (len (jsonBytes )) + 1
2379+ uncompressedSize += calculateUncompressedRowSize (row , a .config .OutputFormat , unquotedColumnNames )
22942380 }
22952381 }
22962382
0 commit comments