Understanding CSV → SQL
The cleanest export, the messiest import.
Why every CSV-to-SQL tool eventually has to handle commas inside quotes, NULL vs empty, and the date that nobody can agree how to parse.
What the converter is really doing.
A CSV-to-SQL converter parses each row, infers a column type, escapes each value according to the target dialect, and emits INSERT statements. Three of those steps are where almost every bug lives. Parsing has to handle quoted fields with embedded delimiters, embedded newlines, and double-double-quote escaping. Type inference has to decide between INTEGER, FLOAT, DATE and TEXT from a small sample. Escaping has to match the target database's rules — quote characters, NULL representations, identifier quoting.
The quoting rules.
Standard CSV (RFC 4180): fields containing the delimiter, a quote, or a newline must be quoted with double quotes; embedded double quotes are escaped by doubling them. A field like She said "hi" becomes "She said ""hi""". Many real-world CSV files break this rule — single quotes around fields, backslash- escaped quotes, mixed delimiters — and every robust parser has to handle the deviations.
Type inference from a sample.
A column with values "1", "42", "1000" is probably INTEGER. With "1.5" mixed in it becomes FLOAT. With "2026-05-13" it's DATE; with "true" / "false" it's BOOLEAN. The decision flips when even one value doesn't fit — a single "N/A" in a numeric column forces TEXT. Sensible converters report the inferred type per column so the user can override; silently picking TEXT for a column of integers because of one stray value turns every downstream query into a CAST.
NULL vs empty string.
CSV has no native concept of NULL — the field is either absent (two consecutive delimiters) or contains a value. Different exporters use different conventions: empty-string-means-null, the literal word "NULL", "\N" (the MySQL/Postgres tab- delimited convention), "NA", "—". The right import behaviour is configurable: tell the converter which token represents NULL, and it emits SQL NULL there instead of an empty string. The wrong-default version is "empty equals empty string" which silently breaks aggregations and joins.
A worked row.
Input row: 7,"O'Brien, Q",2026-05-13,"She said ""hi""" with column names id, name, joined, note. Inferred types: INTEGER, TEXT, DATE, TEXT. Quotes inside the name field need to be doubled in target SQL string literals (PostgreSQL syntax): 'O''Brien, Q'. The double-double-quote in the note unwraps to a single double-quote, then needs no further escaping inside SQL single-quoted strings. The complete statement is INSERT INTO t (id, name, joined, note) VALUES (7, 'O''Brien, Q', '2026-05-13', 'She said "hi"');.
Single row, four columns
commas inside quotes, embedded quote, ISO date
Parse → infer types → escape per target dialect → emit INSERT
7, "O'Brien, Q", 2026-05-13, "She said ""hi"""
= INSERT … VALUES (7, 'O''Brien, Q', '2026-05-13', 'She said "hi"');
Dialect choices.
SQL dialects diverge on more things than you'd expect: identifier quoting (double- quotes in standard SQL/Postgres, backticks in MySQL, square brackets in T-SQL), string escaping (doubled-single-quotes everywhere, plus backslash escapes in MySQL by default), date formats (Postgres flexible, MySQL stricter), and how to express inserting many rows at once. A converter that targets "SQL" generically usually targets ANSI SQL and lets the user pick. Always test the output against your real database — generic SQL almost works, doesn't quite.
Performance: batch your INSERTs.
One INSERT per row is the wrong way to import 100,000 rows. Modern databases support multi-row inserts: INSERT INTO t (a, b) VALUES (1, 'x'), (2, 'y'), (3, 'z'). For large imports use COPY (Postgres) or LOAD DATA INFILE (MySQL) directly — they bypass the SQL parser and run orders of magnitude faster. The converter here emits standard INSERTs for portability; for a 10M-row import, generate a CSV that COPY can consume directly.