CSV to SQL Importer — Python source
Turn CSV data into SQL import statements: batched multi-row INSERTs, a Postgres COPY FROM STDIN block, or a MySQL LOAD DATA statement. Infers numeric columns, emits NULL for empty fields, sanitizes and de-duplicates header names into SQL identifiers.
This is the Python implementation — the same logic the interactive tool runs, in a shareable, citable form.
"""csv-to-sql — pure CSV → SQL import generator. Python port (canonical TS:
src/lib/csv-to-sql.ts; Go twin: cli/csv-to-sql). RFC 4180 parse, header
sanitizing into SQL identifiers, then batched INSERTs, a Postgres COPY block,
or a MySQL LOAD DATA statement. Numeric-looking text emits bare and verbatim;
empty fields become NULL. Never raises — errors travel in the result dict.
"""
import re
_NUMERIC = re.compile(r"^-?(\d+(\.\d+)?|\.\d+)([eE][+-]?\d+)?$")
_IDENT = re.compile(r"[^A-Za-z0-9_]")
def csv_to_rows(csv):
"""RFC 4180 parser mirroring the TS loop (lenient quotes, CR dropped)."""
rows, field, row, in_q = [], [], [], False
i, n = 0, len(csv)
while i < n:
ch = csv[i]
if in_q:
if ch == '"':
if i + 1 < n and csv[i + 1] == '"':
field.append('"')
i += 1
else:
in_q = False
else:
field.append(ch)
elif ch == '"':
in_q = True
elif ch == ",":
row.append("".join(field))
field = []
elif ch == "\n":
row.append("".join(field))
rows.append(row)
row, field = [], []
elif ch != "\r":
field.append(ch)
i += 1
if field or row:
row.append("".join(field))
rows.append(row)
return rows
def escape_sql_string(s, dialect="standard"):
out = s.replace("'", "''")
if dialect == "mysql":
out = (out.replace("\\", "\\\\").replace("\0", "\\0").replace("\n", "\\n")
.replace("\r", "\\r").replace("\x1a", "\\Z"))
return out
def field_literal(value, dialect="standard", infer_types=True):
if infer_types:
if value == "":
return "NULL"
if _NUMERIC.match(value):
return value # verbatim — no float round-trip
return "'" + escape_sql_string(value, dialect) + "'"
def sanitize_ident(name):
return _IDENT.sub("_", name)
def sanitize_headers(headers):
out, seen = [], {}
for i, h in enumerate(headers):
ident = sanitize_ident(h.strip()) or f"col{i + 1}"
seen[ident] = seen.get(ident, 0) + 1
if seen[ident] > 1:
ident = f"{ident}_{seen[ident]}"
out.append(ident)
return out
def csv_escape(field):
if any(c in field for c in ',\n\r"'):
return '"' + field.replace('"', '""') + '"'
return field
def csv_to_sql(csv, table, format="insert", dialect="standard", batch_size=100,
infer_types=True, quote_identifiers=True, file_name="import.csv"):
text = csv.strip()
rows = csv_to_rows(text)
if not text or len(rows) < 2:
return {"ok": False, "sql": "", "rows": 0, "error": "No rows to import."}
cols = sanitize_headers(rows[0])
data = rows[1:]
ident_dialect = {"copy": "postgres", "load-data": "mysql"}.get(format, dialect)
def q(name):
if not quote_identifiers:
return name
return f"`{name}`" if ident_dialect == "mysql" else f'"{name}"'
tbl = q(sanitize_ident(table) or "tbl")
col_list = ", ".join(q(c) for c in cols)
if format in ("copy", "load-data"):
payload = [",".join(csv_escape(c) for c in cols)]
payload += [",".join(csv_escape(r[ci] if ci < len(r) else "") for ci in range(len(cols)))
for r in data]
if format == "copy":
sql = (f"COPY {tbl} ({col_list}) FROM STDIN WITH (FORMAT csv, HEADER true);\n"
+ "\n".join(payload) + "\n\\.")
else:
clean = re.sub(r"[^A-Za-z0-9._\-/]", "", file_name) or "import.csv"
sql = (f"LOAD DATA LOCAL INFILE '{clean}'\nINTO TABLE {tbl}\n"
"FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '\"'\n"
"LINES TERMINATED BY '\\n'\nIGNORE 1 LINES;\n\n" + "\n".join(payload))
return {"ok": True, "sql": sql, "rows": len(data), "error": None}
size = max(1, batch_size)
stmts = []
for start in range(0, len(data), size):
values = [" (" + ", ".join(field_literal(r[ci] if ci < len(r) else "", dialect, infer_types)
for ci in range(len(cols))) + ")"
for r in data[start:start + size]]
stmts.append(f"INSERT INTO {tbl} ({col_list}) VALUES\n" + ",\n".join(values) + ";")
return {"ok": True, "sql": "\n".join(stmts), "rows": len(data), "error": None}
# Example:
# csv_to_sql("id,name\n1,Ada\n2,", table="users")["sql"]
# → 'INSERT INTO "users" ("id", "name") VALUES\n (1, \'Ada\'),\n (2, NULL);'
Also available in 13 other languages
Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →