Skip to content
Open
Show file tree
Hide file tree
Changes from 71 commits
Commits
Show all changes
72 commits
Select commit Hold shift + click to select a range
5d7e3d4
[pingcap/tidb#61999] improve dumpling string key handling
takaidohigasi Jul 1, 2025
443b673
remove unused adaptive chunking function
takaidohigasi Jul 3, 2025
b4e5bb7
fix: enable chunk progress tracking for streaming string key chunking
takaidohigasi Jul 3, 2025
2c909dc
disable ROW_NUMBER() implementation
takaidohigasi Jul 3, 2025
dd0d259
add unit tests and integration tests
takaidohigasi Jul 3, 2025
641588e
implement buffering strategy for string chunking to enable concatenab…
takaidohigasi Jul 3, 2025
dcf5296
fix: use standard SQL escaping in composite string key test
takaidohigasi Jul 13, 2025
44d8341
fix: update test expectation to match standard SQL escaping behavior
takaidohigasi Jul 13, 2025
b0b7735
docs: clarify escapeSQLString is for internal queries only
takaidohigasi Jul 13, 2025
f859cb0
fix: update Unicode test expectation for standard SQL escaping mode
takaidohigasi Jul 13, 2025
d653691
revert: restore default dumpling escaping behavior in tests
takaidohigasi Jul 13, 2025
f6384c5
fix: correct test expectation to match actual dumpling quote behavior
takaidohigasi Jul 13, 2025
9d599bb
fix: correct single quote escaping in test expectation
takaidohigasi Jul 13, 2025
30a00a7
fix: update composite string key test to expect escaped double quotes
takaidohigasi Jul 15, 2025
aec5def
fix: configure UTF8MB4 charset for composite string key test
takaidohigasi Jul 15, 2025
58ec5ea
fix: resolve linting warnings and add charset support
takaidohigasi Jul 15, 2025
ff6fd4e
revert: remove SET NAMES modification for backward compatibility
takaidohigasi Jul 15, 2025
648abb2
style: format Go files with gofmt
takaidohigasi Jul 15, 2025
004eb20
fix: configure UTF8MB4 charset for composite string key test
takaidohigasi Jul 15, 2025
53cf8a0
fix: implement UTF8MB4 charset detection and SET NAMES output
takaidohigasi Jul 15, 2025
34cfe34
revert: remove collation detection code for separate branch
takaidohigasi Jul 15, 2025
a09ebdb
fix: configure TiDB server charset settings for Unicode test
takaidohigasi Jul 15, 2025
70f3576
fix: restore parameter names in WriteInsert functions
takaidohigasi Jul 15, 2025
8b28936
fix: correct chunking logic for row-based vs string-based modes
takaidohigasi Jul 15, 2025
1aa9577
revert: restore original chunking logic
takaidohigasi Jul 15, 2025
870d508
fix: distinguish string chunking vs row chunking by table name
takaidohigasi Jul 15, 2025
3ebc221
Revert "fix: distinguish string chunking vs row chunking by table name"
takaidohigasi Jul 15, 2025
23303f1
feat: add isStringChunking parameter to WriteInsert functions
takaidohigasi Jul 15, 2025
c2518cb
fix: add missing isStringChunking parameter to writer_serial_test.go
takaidohigasi Jul 15, 2025
508267a
fix: resolve linting issues
takaidohigasi Jul 15, 2025
02976b9
make bazel_prepare
takaidohigasi Jul 15, 2025
1cd7cc6
delete extra file
takaidohigasi Jul 15, 2025
b0b7400
delete useless logic
takaidohigasi Jul 17, 2025
6713d20
store isStringChunking to conf
takaidohigasi Jul 17, 2025
5586425
remove extra condition
takaidohigasi Jul 18, 2025
e71d085
fix unused input to _
takaidohigasi Jul 18, 2025
592ae9b
fix chunking for statesize limit
takaidohigasi Jul 21, 2025
a4e73d4
fix: resolve INSERT statement duplication in string-based chunking
takaidohigasi Jul 21, 2025
2d67784
refactor: remove unused totalChunks parameter from writer functions
takaidohigasi Jul 21, 2025
2521058
fix test expectations for removed unused params
takaidohigasi Jul 22, 2025
4f3cd56
fix: handle column names containing commas in extractOrderByColumns
takaidohigasi Jul 31, 2025
9cb87f2
fix format
takaidohigasi Jul 31, 2025
dd6da56
revert failpoint removal
takaidohigasi Aug 6, 2025
525e0a0
fix -r option expectation
takaidohigasi Sep 4, 2025
238a6af
fix: update composite_string_key test expectations based on actual ro…
takaidohigasi Sep 5, 2025
82f2794
fix: use * for row estimation with string fields to handle composite …
takaidohigasi Sep 5, 2025
8cc1847
fix: add fallback to direct COUNT(*) when EXPLAIN returns 0 for strin…
takaidohigasi Sep 5, 2025
d378833
fix: add missing empty function parameter to QuerySQL call
takaidohigasi Sep 5, 2025
c13eba4
fix: address gofmt and security linter issues
takaidohigasi Sep 5, 2025
9af9864
fix: update test comments and simplify test assertions for WriteInsert
takaidohigasi Sep 18, 2025
2042078
fix: update composite_string_key test to use chunked result files
takaidohigasi Sep 18, 2025
92da39f
fix: add header comments to all chunk files in test expectations
takaidohigasi Sep 18, 2025
b1c4d8b
fix: use all composite key fields for string chunking
takaidohigasi Sep 18, 2025
1a87518
Merge branch 'upstream/master' into improve-string-key-handling
takaidohigasi Apr 24, 2026
919858e
dumpling/tests: add large-scale composite-string-key round-trip test
takaidohigasi Apr 24, 2026
629efbb
dumpling: drop LLM-noise helpers and flatten chunking API
takaidohigasi Apr 24, 2026
54566ad
dumpling/export: document tableChunkStat finalized invariant
takaidohigasi Apr 24, 2026
03f2801
dumpling: verify COUNT(*) when EXPLAIN under-estimates for string chu…
takaidohigasi Apr 24, 2026
0e2468c
dumpling/tests: regenerate composite_string_key expected fixtures
takaidohigasi Apr 24, 2026
0dc993b
dumpling/tests: escape single quotes in composite_string_key_large ge…
takaidohigasi Apr 24, 2026
bbbc580
dumpling/tests: scope composite_string_key_large chunk assertions to …
takaidohigasi Apr 24, 2026
42711a0
dumpling/export: satisfy nogo lint — fmt.Fprintf and drop unused numC…
takaidohigasi Apr 24, 2026
e2edf36
dumpling/export: gofmt writer_serial_test.go
takaidohigasi Apr 24, 2026
23e7d56
dumpling: close tableChunkStat double-increment race + CodeRabbit nits
takaidohigasi Apr 24, 2026
987f030
dumpling: drop tautology test and redundant WHERE-clause alias
takaidohigasi Apr 24, 2026
4e7015b
dumpling/tests: address CodeRabbit shell-script findings
takaidohigasi Apr 24, 2026
6f6ec69
dumpling/export: fix chunkedTables accounting in concat + TiDB TABLES…
takaidohigasi Apr 24, 2026
6c0c495
dumpling/export: drop tautological tests and tighten extractOrderByCo…
takaidohigasi Apr 24, 2026
5068102
dumpling: add license header and fix run.sh shebangs / backslash esca…
takaidohigasi Apr 24, 2026
3cf73d2
dumpling/export: address CodeRabbit follow-up review
takaidohigasi Apr 25, 2026
8e5eb75
dumpling/export: fail loudly on boundary-sampling errors and unknown …
takaidohigasi Apr 25, 2026
0610dc4
Merge remote-tracking branch 'upstream/master' into improve-string-ke…
takaidohigasi May 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions dumpling/export/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ go_library(
"retry.go",
"sql.go",
"sql_type.go",
"sql_util.go",
"status.go",
"string_chunking.go",
"task.go",
"util.go",
"writer.go",
Expand Down Expand Up @@ -88,7 +90,9 @@ go_test(
"prepare_test.go",
"sql_test.go",
"sql_type_test.go",
"sql_util_test.go",
"status_test.go",
"string_chunking_test.go",
"util_for_test.go",
"util_test.go",
"writer_serial_test.go",
Expand Down
226 changes: 208 additions & 18 deletions dumpling/export/dump.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"slices"
"strconv"
"strings"
"sync"
"sync/atomic"
"time"

Expand Down Expand Up @@ -54,6 +55,9 @@ var errEmptyHandleVals = errors.New("empty handleVals for TiDB table")
// see https://docs.pingcap.com/zh/tidb/dev/system-variables#tidb_enable_paging-%E4%BB%8E-v540-%E7%89%88%E6%9C%AC%E5%BC%80%E5%A7%8B%E5%BC%95%E5%85%A5
var enablePagingVersion = semver.New("6.2.0")

// Maximum number of chunks to prevent infinite loops during boundary sampling
const maxChunkLimit = 1000000

// Dumper is the dump progress structure
type Dumper struct {
tctx *tcontext.Context
Expand All @@ -73,6 +77,8 @@ type Dumper struct {
charsetAndDefaultCollationMap map[string]string

speedRecorder *SpeedRecorder

chunkedTables sync.Map
}

// NewDumper returns a new Dumper
Expand Down Expand Up @@ -355,14 +361,8 @@ func (d *Dumper) startWriters(tctx *tcontext.Context, wg *errgroup.Group, taskCh
writer := NewWriter(tctx, int64(i), conf, conn, d.extStore, d.metrics)
writer.rebuildConnFn = rebuildConnFn
writer.setFinishTableCallBack(func(task Task) {
// this is called when a file is finished.
if _, ok := task.(*TaskTableData); ok {
IncCounter(d.metrics.finishedTablesCounter)
// FIXME: actually finishing the last chunk doesn't means this table is 'finished'.
// We can call this table is 'finished' if all its chunks are finished.
// Comment this log now to avoid ambiguity.
// tctx.L().Debug("finished dumping table data",
// zap.String("database", td.Meta.DatabaseName()),
// zap.String("table", td.Meta.TableName()))
failpoint.Inject("EnableLogProgress", func() {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you explain why this failpoint is deleted?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll check again. thanks

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

reverted.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reverted in dd6da56 — the failpoint.Inject("EnableLogProgress", ...) in setFinishTableCallBack is present in the current tree (line shifted after the master merge, now at dump.go:366).

time.Sleep(1 * time.Second)
tctx.L().Debug("EnableLogProgress, sleep 1s")
Expand All @@ -373,6 +373,22 @@ func (d *Dumper) startWriters(tctx *tcontext.Context, wg *errgroup.Group, taskCh
IncGauge(d.metrics.taskChannelCapacity)
if td, ok := task.(*TaskTableData); ok {
d.metrics.completedChunks.Add(1)
if val, ok := d.chunkedTables.Load(td.Meta.ChunkKey()); ok {
chunkStats := val.(*tableChunkStat)
finishedChunks := chunkStats.finished.Add(1)
if chunkStats.finalized.Load() && finishedChunks == chunkStats.sent.Load() {
// LoadAndDelete is the atomic claim: only the winner
// sees `loaded==true`. The producer defer uses the
// same pattern, so the counter increments exactly
// once regardless of which side reaches the
// termination condition first.
if _, loaded := d.chunkedTables.LoadAndDelete(td.Meta.ChunkKey()); loaded {
IncCounter(d.metrics.finishedTablesCounter)
}
}
} else {
IncCounter(d.metrics.finishedTablesCounter)
}
tctx.L().Debug("finish dumping table data task",
zap.String("database", td.Meta.DatabaseName()),
zap.String("table", td.Meta.TableName()),
Expand Down Expand Up @@ -661,9 +677,25 @@ func (d *Dumper) dumpTableData(tctx *tcontext.Context, conn *BaseConn, meta Tabl
return nil
}

// Update total rows
fieldName, _ := pickupPossibleField(tctx, meta, conn)
c := estimateCount(tctx, meta.DatabaseName(), meta.TableName(), conn, fieldName, conf)
// Update total rows. Use "*" as the estimation column for string-leading
// composite keys; EXPLAIN on a single varchar column under-estimates and
// would otherwise make estimateTotalRowsCounter lag behind the chunked
// path's estimate (see concurrentDumpTable).
fields, isStringField, err := pickupPossibleField(tctx, meta, conn)
if err != nil {
tctx.L().Debug("pickupPossibleField failed for row estimate",
zap.String("database", meta.DatabaseName()),
zap.String("table", meta.TableName()),
log.ShortError(err))
}
estimateField := ""
if len(fields) > 0 {
estimateField = fields[0]
if isStringField {
estimateField = "*"
}
}
c := estimateCount(tctx, meta.DatabaseName(), meta.TableName(), conn, estimateField, conf)
AddCounter(d.metrics.estimateTotalRowsCounter, float64(c))

if conf.Rows == UnspecifiedSize {
Expand Down Expand Up @@ -710,6 +742,13 @@ func (d *Dumper) buildConcatTask(tctx *tcontext.Context, conn *BaseConn, meta Ta
task := <-tableChan
handleSubTask(task)
}
// concurrentDumpTable may have registered a chunkedTables entry
// for this meta, but handleSubTask intercepts every sub-task
// without bumping `finished`, so the inner defer leaves the
// entry behind with finished < sent. Drop it now so the concat
// (or fallback) task is counted exactly once via the
// no-tracking branch in startWriters.
d.chunkedTables.Delete(meta.ChunkKey())
if len(tableDataArr) <= 1 {
return nil, nil
}
Expand Down Expand Up @@ -781,9 +820,10 @@ func (d *Dumper) sequentialDumpTable(tctx *tcontext.Context, conn *BaseConn, met
}

// concurrentDumpTable tries to split table into several chunks to dump
func (d *Dumper) concurrentDumpTable(tctx *tcontext.Context, conn *BaseConn, meta TableMeta, taskChan chan<- Task) error {
func (d *Dumper) concurrentDumpTable(tctx *tcontext.Context, conn *BaseConn, meta TableMeta, taskChan chan<- Task) (err error) {
conf := d.conf
db, tbl := meta.DatabaseName(), meta.TableName()

if conf.ServerInfo.ServerType == version.ServerTypeTiDB &&
conf.ServerInfo.ServerVersion != nil &&
(conf.ServerInfo.ServerVersion.Compare(*tableSampleVersion) >= 0 ||
Expand Down Expand Up @@ -811,19 +851,50 @@ func (d *Dumper) concurrentDumpTable(tctx *tcontext.Context, conn *BaseConn, met
return err
}

field, err := pickupPossibleField(tctx, meta, conn)
if err != nil || field == "" {
// skip split chunk logic if not found proper field
fields, isStringField, err := pickupPossibleField(tctx, meta, conn)
if err != nil || len(fields) == 0 {
tctx.L().Info("fallback to sequential dump due to no proper field. This won't influence the whole dump process",
zap.String("database", db), zap.String("table", tbl), log.ShortError(err))
return d.dumpWholeTableDirectly(tctx, meta, taskChan, "", orderByClause, 0, 1)
}
field := fields[0]

// For composite string keys EXPLAIN on a single column under-estimates;
// use * so the row estimation covers the whole row.
estimateField := field
if isStringField {
estimateField = "*"
}

count := estimateCount(d.tctx, db, tbl, conn, field, conf)
tctx.L().Info("get estimated rows count",
count := estimateCount(d.tctx, db, tbl, conn, estimateField, conf)
tctx.L().Debug("get estimated rows count",
zap.String("database", db),
zap.String("table", tbl),
zap.Uint64("estimateCount", count))
zap.Uint64("estimateCount", count),
zap.Strings("fields", fields),
zap.Bool("isStringField", isStringField))

// EXPLAIN can under-estimate (0, or a small value like 1) on freshly
// populated tables whose InnoDB rows statistic hasn't refreshed yet.
// For string chunking a pessimistic estimate silently drops us into
// the sequential path, so when the EXPLAIN result is below the chunk
// threshold we verify with a direct COUNT(*) before giving up on
// parallelism. COUNT is authoritative so it's safe to replace count.
if isStringField && count < conf.Rows {
countQuery := fmt.Sprintf("SELECT COUNT(*) FROM `%s`.`%s` %s",
escapeString(db), escapeString(tbl), buildWhereCondition(conf, ""))
var directCount sql.NullInt64
// simpleQueryWithArgs already iterates rows.Next(); the handler
// must not call Next() itself or it will skip past the (single)
// COUNT row.
err := conn.QuerySQL(tctx, func(rows *sql.Rows) error {
return rows.Scan(&directCount)
}, func() { directCount = sql.NullInt64{} }, countQuery)
if err == nil && directCount.Valid && directCount.Int64 > 0 {
count = uint64(directCount.Int64)
}
}

if count < conf.Rows {
// skip chunk logic if estimates are low
tctx.L().Info("fallback to sequential dump due to estimate count < rows. This won't influence the whole dump process",
Expand All @@ -834,6 +905,10 @@ func (d *Dumper) concurrentDumpTable(tctx *tcontext.Context, conn *BaseConn, met
return d.dumpWholeTableDirectly(tctx, meta, taskChan, "", orderByClause, 0, 1)
}

if isStringField {
return d.concurrentDumpStringFields(tctx, conn, meta, taskChan, fields, orderByClause, count)
}

minv, maxv, err := d.selectMinAndMaxIntValue(tctx, conn, db, tbl, field)
if err != nil {
tctx.L().Info("fallback to sequential dump due to cannot get bounding values. This won't influence the whole dump process",
Expand All @@ -858,6 +933,16 @@ func (d *Dumper) concurrentDumpTable(tctx *tcontext.Context, conn *BaseConn, met

chunkIndex := 0
nullValueCondition := ""
chunkStats := newTableChunkStat()
d.chunkedTables.Store(meta.ChunkKey(), chunkStats)
defer func() {
chunkStats.finalized.Store(true)
if chunkStats.finished.Load() == chunkStats.sent.Load() {
if _, loaded := d.chunkedTables.LoadAndDelete(meta.ChunkKey()); loaded {
IncCounter(d.metrics.finishedTablesCounter)
}
}
}()
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
if conf.Where == "" {
nullValueCondition = fmt.Sprintf("`%s` IS NULL OR ", escapeString(field))
}
Expand All @@ -880,6 +965,11 @@ func (d *Dumper) concurrentDumpTable(tctx *tcontext.Context, conn *BaseConn, met
}

func (d *Dumper) sendTaskToChan(tctx *tcontext.Context, task Task, taskChan chan<- Task) (ctxDone bool) {
if td, ok := task.(*TaskTableData); ok {
if val, ok := d.chunkedTables.Load(td.Meta.ChunkKey()); ok {
val.(*tableChunkStat).sent.Add(1)
}
}
select {
case <-tctx.Done():
return true
Expand All @@ -891,6 +981,54 @@ func (d *Dumper) sendTaskToChan(tctx *tcontext.Context, task Task, taskChan chan
}
}

// tableChunkStat tracks per-table chunk progress so finishedTablesCounter
// is incremented exactly once, when the last chunk of a table is written
// (rather than when the first chunk finishes, which was the old behavior
// noted by the FIXME in startWriters).
//
// Termination handshake: the producer defer (after it has finished every
// sendTaskToChan call) sets finalized=true and then checks
// `finished == sent`. The consumer callback (startWriters) increments
// finished and then checks `finalized && finished == sent`. Both sides
// can observe the same termination condition concurrently, so the
// IncCounter + Delete pair must be atomic w.r.t. the other side.
// Both sites call chunkedTables.LoadAndDelete and only the side that
// gets `loaded==true` performs the increment. sent.Add happens
// synchronously inside sendTaskToChan before the blocking send, so
// once finalized flips true no new sent.Add can race in.
type tableChunkStat struct {
sent *gatomic.Int32
finished *gatomic.Int32
finalized *gatomic.Bool
}

func newTableChunkStat() *tableChunkStat {
return &tableChunkStat{
sent: gatomic.NewInt32(0),
finished: gatomic.NewInt32(0),
finalized: gatomic.NewBool(false),
}
}

// beginChunkTracking registers a chunk-stat entry for meta and returns a
// finalizer that closes the producer side of the termination handshake. Use
// this in producers that emit multiple TaskTableData tasks per table; without
// it, every task hits the no-tracking branch in startWriters and
// finishedTablesCounter is incremented once per chunk instead of once per
// table.
func (d *Dumper) beginChunkTracking(meta TableMeta) func() {
chunkStats := newTableChunkStat()
d.chunkedTables.Store(meta.ChunkKey(), chunkStats)
return func() {
chunkStats.finalized.Store(true)
if chunkStats.finished.Load() == chunkStats.sent.Load() {
if _, loaded := d.chunkedTables.LoadAndDelete(meta.ChunkKey()); loaded {
IncCounter(d.metrics.finishedTablesCounter)
}
}
}
}

func (d *Dumper) selectMinAndMaxIntValue(tctx *tcontext.Context, conn *BaseConn, db, tbl, field string) (minv, maxv *big.Int, err error) {
conf, zero := d.conf, &big.Int{}
query := fmt.Sprintf("SELECT MIN(`%s`),MAX(`%s`) FROM `%s`.`%s`",
Expand Down Expand Up @@ -958,6 +1096,7 @@ func (d *Dumper) concurrentDumpTiDBTables(tctx *tcontext.Context, conn *BaseConn
if err != nil {
return err
}
defer d.beginChunkTracking(meta)()
return d.sendConcurrentDumpTiDBTasks(tctx, meta, taskChan, handleColNames, handleVals, "", 0, len(handleVals)+1)
}

Expand All @@ -982,6 +1121,7 @@ func (d *Dumper) concurrentDumpTiDBPartitionTablesWithTableSample(tctx *tcontext
totalChunk += len(handleVals) + 1
}
startChunk := 0
defer d.beginChunkTracking(meta)()
for i, partition := range d.conf.Partitions {
err = d.sendConcurrentDumpTiDBTasks(tctx, meta, taskChan, pkFields, cachedHandleVals[i], partition, startChunk, totalChunk)
if err != nil {
Expand Down Expand Up @@ -1014,6 +1154,8 @@ func (d *Dumper) concurrentDumpTiDBPartitionTables(tctx *tcontext.Context, conn
totalChunk += len(handleVals) + 1
cachedHandleVals[i] = handleVals
}

defer d.beginChunkTracking(meta)()
for i, partition := range partitions {
err := d.sendConcurrentDumpTiDBTasks(tctx, meta, taskChan, handleColNames, cachedHandleVals[i], partition, startChunkIdx, totalChunk)
if err != nil {
Expand Down Expand Up @@ -2037,5 +2179,53 @@ func (d *Dumper) renewSelectTableRegionFuncForLowerTiDB(tctx *tcontext.Context)

func (d *Dumper) newTaskTableData(meta TableMeta, data TableDataIR, currentChunk, totalChunks int) *TaskTableData {
d.metrics.totalChunks.Add(1)
return NewTaskTableData(meta, data, currentChunk, totalChunks)
// Chunking mode is already set at table level in concurrentDumpTable
task := NewTaskTableData(meta, data, currentChunk, totalChunks)
return task
}

// extractOrderByColumns extracts column names from ORDER BY clause
// Input: "ORDER BY `item_id`,`photo_index`"
// Output: ["`item_id`", "`photo_index`"]
// Handles column names that contain commas by respecting backtick quoting
func extractOrderByColumns(orderByClause string) []string {
// Remove "ORDER BY " prefix
columnsStr := strings.TrimPrefix(orderByClause, "ORDER BY ")

// Handle empty clause: returning nil (rather than []string{""}) signals
// "no chunking columns" so callers can bail out instead of synthesizing
// SELECT lists or WHERE predicates from an empty column name. The prefix
// "ORDER BY " is matched case-sensitively with a single space; callers
// should normalize before passing in.
if columnsStr == "" {
return nil
}

var columns []string
var currentColumn strings.Builder
inBackticks := false

for i := range len(columnsStr) {
ch := columnsStr[i]

if ch == '`' {
inBackticks = !inBackticks
currentColumn.WriteByte(ch)
} else if ch == ',' && !inBackticks {
// Found a column separator outside of backticks
if col := strings.TrimSpace(currentColumn.String()); col != "" {
columns = append(columns, col)
}
currentColumn.Reset()
} else {
currentColumn.WriteByte(ch)
}
}

// Add the last column
if col := strings.TrimSpace(currentColumn.String()); col != "" {
columns = append(columns, col)
}

return columns
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Loading
Loading