feat: migrate system + cross-DB engine + IMPORT/EXPORT syntax -- 22 files, client+server+docs
CI / test (push) Has been cancelled
CI / verify (push) Has been cancelled
Clients CI / build-server (push) Has been cancelled
Clients CI / test-python (push) Has been cancelled
Clients CI / test-javascript (push) Has been cancelled
Clients CI / test-nim (push) Has been cancelled
Clients CI / test-rust (push) Has been cancelled

This commit is contained in:
2026-05-21 19:32:14 +03:00
parent f427ba5968
commit bb843b9a03
23 changed files with 2306 additions and 52 deletions
+100
View File
@@ -23,6 +23,7 @@ import ../storage/btree
import ../storage/wal
import ../core/mvcc
import ../core/tracing
import ../client/fileops
import ../fts/engine as fts
import ../core/registry
@@ -219,6 +220,24 @@ proc sqlEscapeString*(s: string): string =
## Escape SQL string literals by doubling single-quotes.
result = s.replace("'", "''")
proc buildInsertSql*(table: string, columns: seq[string], rows: seq[seq[string]]): string =
## Build a multi-row INSERT statement for bulk import.
result = "INSERT INTO \"" & sqlEscapeIdent(table) & "\" ("
for i, col in columns:
if i > 0: result &= ", "
result &= "\"" & sqlEscapeIdent(col) & "\""
result &= ") VALUES "
for ri, row in rows:
if ri > 0: result &= ", "
result &= "("
for ci, val in row:
if ci > 0: result &= ", "
if val.len == 0 or val == "\\N":
result &= "NULL"
else:
result &= "'" & sqlEscapeString(val) & "'"
result &= ")"
proc exprToSql(node: Node): string =
if node == nil:
return ""
@@ -5279,6 +5298,87 @@ proc executeQueryImpl(ctx: ExecutionContext, astNode: Node, params: seq[WireValu
return okResult(msg="Rolled back " & $rolledBackCount & " migrations")
of nkImportFrom:
let path = stmt.impPath
let table = stmt.impTable
let format = stmt.impFormat
if not fileExists(path):
return errResult("File not found: " & path)
let content = readFile(path)
var columns: seq[string] = @[]
var rows: seq[seq[string]] = @[]
case format
of "csv":
(columns, rows) = parseCsvTable(content, stmt.impDelimiter, stmt.impHasHeader)
of "json":
(columns, rows) = parseJsonTable(content)
of "ndjson":
(columns, rows) = parseNdjsonTable(content)
else:
return errResult("Unsupported import format: " & format)
if columns.len == 0:
return errResult("No columns found in import file")
var inserted = 0
let batchSize = stmt.impBatchSize
var batchRows: seq[seq[string]] = @[]
for row in rows:
batchRows.add(row)
if batchRows.len >= batchSize:
# Generate INSERT INTO t (c1,c2) VALUES (...),(...) and execute
let sql = buildInsertSql(table, columns, batchRows)
let tokens = qlex.tokenize(sql)
let astNode = qpar.parse(tokens)
if astNode.stmts.len > 0:
let insResult = executeQueryImpl(ctx, astNode)
if insResult.success:
inserted += batchRows.len
batchRows.setLen(0)
if batchRows.len > 0:
let sql = buildInsertSql(table, columns, batchRows)
let tokens = qlex.tokenize(sql)
let astNode = qpar.parse(tokens)
if astNode.stmts.len > 0:
let insResult = executeQueryImpl(ctx, astNode)
if insResult.success:
inserted += batchRows.len
return okResult(msg="IMPORTED " & $inserted & " rows into " & table)
of nkExportTo:
let path = stmt.expPath
let table = stmt.expTable
let format = stmt.expFormat
var rows: seq[Row] = @[]
var cols: seq[string] = @[]
let scanResult = execScan(ctx, table)
if scanResult.len == 0:
let tbl = ctx.getTableDef(table)
if tbl.columns.len == 0:
return errResult("Table not found: " & table)
for col in tbl.columns:
cols.add(col.name)
else:
for k, v in scanResult[0].pairs:
cols.add(k)
rows = scanResult
var content = ""
var strRows: seq[seq[string]] = @[]
for row in rows:
var strRow: seq[string] = @[]
for col in cols:
strRow.add(if col in row: valueToString(row[col]) else: "")
strRows.add(strRow)
case format
of "csv":
content = toCsv(cols, strRows, stmt.expDelimiter, stmt.expIncludeHeader)
of "json":
content = toJson(cols, strRows)
of "ndjson":
content = toNdjson(cols, strRows)
else:
return errResult("Unsupported export format: " & format)
writeFile(path, content)
return okResult(msg="EXPORTED " & $strRows.len & " rows to " & path)
of nkMigrationDryRun:
let (found, upBody, downBody) = getMigrationBody(ctx, stmt.mdrName)
if not found: