Generate SQL that runs: identifiers quoted rather than stripped, values escaped per dialect, and every change to your data reported
Initializing in your browser…
View, sort and edit a CSV, with an undo stack that goes back exactly one step and a parse report that tells a broken quote from a ragged row
Convert both ways, converting a type only when it reads back the same, and naming every value that cannot
Convert CSV to XLSX or ODS with cell types that keep leading zeros, long IDs and dates intact
You need to load an export into PostgreSQL, and it has a column called "order", a name with an apostrophe, a Windows path and a value that is just padding.
Input
id,order,customer name 1,O'Brien & Co," padded " 2,C:\share\x,NULL
PostgreSQL
CREATE TABLE "orders" ("id" TEXT, "order" TEXT, "customer name" TEXT);
INSERT INTO "orders" ("id", "order", "customer name")
VALUES ('1', 'O''Brien & Co', ' padded ');
INSERT INTO "orders" ("id", "order", "customer name")
VALUES ('2', 'C:\share\x', 'NULL');The reserved word "order" and the name with a space both survive because every identifier is quoted rather than stripped. The apostrophe is doubled, the padding is kept, and the four letters NULL stay text rather than becoming a missing value. Switch to MySQL and the same run writes 'C:\\share\\x' instead, because MySQL treats a backslash as an escape character and PostgreSQL does not. The whole thing is verified by executing it in SQLite and comparing the table to the CSV.
Turn a CSV into CREATE TABLE and INSERT statements for MySQL, PostgreSQL, SQLite, SQL Server or Oracle. The test for this tool is not that the escaping looks right: it is that SQLite executes the generated script and the resulting table contains the same strings the CSV did, cell for cell, over a corpus of thirty deliberately awkward values.
Generating SQL is easy to do almost correctly, and almost correctly is the worst outcome, because the script runs and the data is quietly different. Measured against the previous version of this tool on 2026-09-01, over thirty awkward values and fifteen awkward column names:
Six of the thirty values came back changed. Leading and trailing whitespace was trimmed away, so " padded" arrived as "padded". A cell holding a single space became NULL. An empty field became NULL with no way to say otherwise. And the four-letter strings NULL and null became SQL NULL, so a genuine piece of text turned into a missing value. Every one of those is silent: the script runs, no error appears, and the difference only shows up later.
The CREATE TABLE did not run at all. Column names were made safe by replacing every character outside A to Z, 0 to 9 and underscore with an underscore, which turned five different headers into the same name and made SQLite refuse the statement with "duplicate column name". A header with an accent became mojibake and an empty header became an empty quoted identifier.
And the MySQL output did not escape backslashes. That one is worth stating precisely, because it is not merely wrong. In the SQL standard, and therefore in SQLite, in PostgreSQL with standard_conforming_strings on, in SQL Server and in Oracle, a backslash inside a string literal is an ordinary character. In MySQL, unless NO_BACKSLASH_ESCAPES is set (it is not by default), a backslash starts an escape sequence. A value of C:\path\to\file written with only the quote doubled is read by MySQL as C:path, a tab, o, a form feed, ile. A value containing a backslash immediately before a quote ends the literal early, which breaks the statement and is the mechanism by which a quote escapes a string.
This version fixes all of it at the source. Values are written exactly as the cell holds them, with no trimming and no guessing: a space is a space, and the letters NULL are the letters NULL unless you say otherwise. Whether an empty field becomes NULL or an empty string is a setting rather than an assumption, and you can name additional text to treat as NULL. Identifiers are always quoted, with the delimiter escaped by doubling it, which is what makes select, order, first name, 2nd_place and a name containing a quote all work without a reserved-word list. Nothing is stripped: an awkward character in a header is a reason to quote the identifier, not a reason to lose the name.
Three things quoting cannot fix are handled explicitly and reported. Names longer than the server accepts are shortened (PostgreSQL truncates to 63 bytes silently, which makes two long names that share a prefix collide inside the database rather than in the script). Names that collide are renamed, using each server's own comparison rule: PostgreSQL and Oracle compare quoted identifiers case-sensitively so "ID" and "id" are two real columns, while SQLite, MySQL and SQL Server treat them as one. And an empty header gets a name, because no dialect allows an empty identifier.
Values destined for a numeric column go through a decimal grammar rather than through JavaScript's Number(), so 0x1F is never emitted as a bare unquoted token and a grouped 1,234 is written as 1234 rather than having its comma silently dropped. A value that is not a boolean is written as NULL rather than being guessed as false. A NUL byte, which no dialect can carry in an ordinary string literal, is removed and reported instead of producing a script most clients cannot even load. Anything that could not be carried faithfully appears in a panel above the SQL with the row count.
Get a script that runs on the first try, with the identifiers quoted and the awkward values escaped for the dialect you are actually using.
Names like O'Brien and Windows paths are exactly where naive generators break or corrupt data. Both are handled per dialect and verified by running the result.
Headers with spaces, dots, dashes, reserved words or accents keep their names instead of collapsing into each other and failing the CREATE TABLE.
The warnings panel lists every value that could not be carried faithfully and every column that had to be renamed, before anything reaches a server.
By running it. The test suite executes the script in SQLite and compares the resulting table to the CSV as Python's own csv module reads it, over a corpus of thirty awkward values and fifteen awkward column names. For the four dialects that are not installed, each literal is read back with a lexer written from that vendor's documentation, and that lexer is validated first by requiring it to agree with real SQLite.
Because MySQL reads string literals differently. Unless NO_BACKSLASH_ESCAPES is set, which is not the default, a backslash in a MySQL literal starts an escape sequence, so backslashes have to be doubled. In PostgreSQL, SQLite, SQL Server and Oracle a backslash is an ordinary character and doubling it would insert a second one. Getting this wrong in either direction corrupts data.
It keeps its name. Every identifier is quoted, with the delimiter escaped by doubling it, which is what makes reserved words and spaces work. Nothing is stripped from a header; if a name genuinely has to change (it is empty, too long for the server, or collides with another under that server's comparison rule) it is renamed and the reason is shown.
Whichever you choose. Only a genuinely empty field is affected: a cell holding a space is written as a space, and a cell holding the four letters NULL is written as that text, unless you name it as a null marker.
Because it is not a number, and the alternative is worse. A cell reading 0x1F is a string in a CSV even though JavaScript reads it as 31, and emitting the bare token 0x1F would be a blob literal in SQLite and a syntax error in PostgreSQL. Anything refused this way is listed in the warnings panel with the value and the reason.
MySQL, PostgreSQL, SQLite, SQL Server and Oracle. Each carries its own identifier delimiter, identifier length limit, identifier comparison rule, backslash behaviour, boolean literals, and whether it accepts a multi-row INSERT. Oracle does not, so it always gets one statement per row.
Rows and columns are parsed and transformed in memory in your browser. No record ever reaches a server.