Hand-writing DDL is nobody's favorite chore

The CSV lands in your downloads folder — 40,000 rows of exported orders — and the database wants a table for it before it will accept a single one. So you open an editor and start typing: column names, guessed types, a length for the VARCHARs, a primary key you hope is actually unique. You are transcribing a file you cannot see, one `CREATE TABLE` line at a time, and any mistake surfaces two hours later as an import error or, worse, as silently truncated data.

The information you need is sitting right there in the file. Every column's type is discoverable by looking at its values; the only question is whether a machine or a squinting human does the looking. This generator does it the machine way: paste or load the CSV, get columns with inferred types in an editable grid, adjust what needs adjusting, and export a `CREATE TABLE` statement for MySQL, PostgreSQL, SQLite, or SQL Server — each one spelled correctly for its dialect, keys and defaults included.

It also works design-first, if you already know your schema and just refuse to babysit dialect trivia. Add columns in the grid, set types and constraints, and collect the DDL. Either road ends at the same export button.

What you get out of it

The core is a table model — columns with names, types, nullability, defaults, and constraints — rendered into whatever dialect you point it at. The model is the source of truth; the SQL is a view. That separation is what makes multi-dialect export cheap: change nothing, switch the dropdown from PostgreSQL to MySQL, and the same table re-emits with backticks, `AUTO_INCREMENT`, and an `ENGINE` clause instead of identity columns and double quotes.

Type inference from a CSV is the headline feature, because it kills the transcription step entirely. Headers become column names, values become evidence, and the grid arrives pre-filled with a defensible guess per column. You keep the veto: every inferred type is a dropdown, and one click rewrites it everywhere.

Constraint support covers what real tables actually carry: single and composite primary keys, foreign keys with reference columns, unique constraints, NOT NULL flags, default values, and CHECK constraints generated from low-cardinality columns — a status column whose values are all `new`, `paid`, or `refunded` gets offered a CHECK with those values enumerated. Constraint names follow the `pk_`/`fk_`/`uq_`/`ck_` prefix convention so error messages later in your life are readable.

From paste to DDL in five moves

  1. Feed it data

    Paste CSV text, drop a file in, or paste just a header row plus a few data rows if that is all you have. The parser follows RFC 4180 quoting, so commas inside quoted values will not scare it.

  2. Review the inferred grid

    Each column arrives with a proposed type, NULL/NOT NULL based on whether blanks appeared, and the observed max length for text. The panel shows why each type was chosen — the strictest rule every value in the column satisfied.

  3. Adjust the model, not the SQL

    Change types, rename columns, toggle NOT NULL, set defaults, mark the primary key, add foreign keys and uniques. Every edit lands in the model, and all dialect previews update together.

  4. Pick your dialects

    Select any of MySQL, PostgreSQL, SQLite, and SQL Server. The output pane shows one tab per dialect, all live.

  5. Copy or download

    Take the statement into your migration, or download a .sql file per dialect. If you marked foreign keys, the CREATE INDEX statements ride along beneath the table definition.

The whole loop is seconds for a 20-column file. The time goes where it should — into judgment calls like whether `amount_usd` should be `NUMERIC(10,2)` or something with more headroom — instead of into remembering whether SQLite spells booleans differently than Postgres. (It does, incidentally.)

How the type inference decides

Inference runs per column, strictest-first. Every non-empty value in the column must pass a type's test for that type to win, and the checks are ordered so that a stricter type always beats a looser one when both fit:

for each column:
  values = non-empty cells
  if all match ^-?\d+$ ..................... INTEGER
  elif all match ^-?\d+\.\d+$ .............. DECIMAL(p, s)
  elif all match ^\d{4}-\d{2}-\d{2}$ ....... DATE
  elif all match ISO timestamp .............. TIMESTAMP
  elif all in {true, false, 0, 1} .......... BOOLEAN
  else ..................................... TEXT (max len reported)

DECIMAL precision:
  p = max(integer digits) + max(fraction digits) + headroom
  s = max(fraction digits)

The strictest-first ordering matters more than it looks. The string `120` is a valid integer and a valid decimal and a valid timestamp under some sloppy parsers; testing integer first means a column of `120`, `49.5`, `250.75` correctly lands on DECIMAL, because the decimal values fail the integer gate. A column of pure integers stays INTEGER instead of decaying into something numeric-and-vague.

Precision comes from measurement, not from a default. The generator counts the maximum integer digits and maximum fraction digits actually observed, then adds configurable headroom — 10 integer digits is the default cushion, so a `NUMERIC` that fits today's data also fits next year's price list without a migration. The example below shows the arithmetic on real numbers.

Nullability is handled with a policy, not a reflex: a column with any blank cell is proposed as nullable, and a column that is fully populated is proposed NOT NULL. It is a suggestion with a one-click override, because "every row happens to have a value" and "every row must have a value" are different claims, and only you know which one your schema is allowed to make.

The same file, three dialects

Here is a four-row sample of an orders export — small enough to check every inference by eye, with one missing email and a mix of integer and decimal amounts to keep things honest:

order_id,customer_email,amount_usd,status,created_at
1001,ada@example.com,59.99,paid,2026-01-04 09:12:33
1002,grace@example.com,120,refunded,2026-01-04 10:03:11
1003,,49.5,new,2026-01-05 08:44:02
1004,linus@example.com,250.75,paid,2026-01-06 17:20:45

The inference reads it as follows. `order_id`: four values, all integers, four digits — INTEGER. `customer_email`: text with a blank in row three, so nullable; observed maximum length 17, proposed VARCHAR with headroom. `amount_usd`: `49.5` fails the integer test, so DECIMAL with max 3 integer digits and 2 fraction digits, plus headroom. `status`: three distinct short strings — text, and a CHECK constraint is offered. `created_at`: every value matches an ISO timestamp — TIMESTAMP. The exported DDL per dialect:

-- PostgreSQL
CREATE TABLE orders (
    order_id        INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    customer_email  VARCHAR(255),
    amount_usd      NUMERIC(10,2) NOT NULL,
    status          VARCHAR(12) NOT NULL DEFAULT 'new',
    created_at      TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
-- MySQL 8
CREATE TABLE orders (
    `order_id`       INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    `customer_email` VARCHAR(255) NULL,
    `amount_usd`     DECIMAL(10,2) NOT NULL,
    `status`         VARCHAR(12) NOT NULL DEFAULT 'new',
    `created_at`     TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- SQLite
CREATE TABLE orders (
    order_id        INTEGER PRIMARY KEY,
    customer_email  TEXT,
    amount_usd      NUMERIC(10,2) NOT NULL,
    status          TEXT NOT NULL DEFAULT 'new',
    created_at      TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
);
Precision math on amount_usd

Input

Observed values: 59.99, 120, 49.5, 250.75
max integer digits = 3 (250) · max fraction digits = 2 (59.99, 250.75)

Output

NUMERIC(10,2) — 3 observed integer digits + 2 fraction digits + 5 headroom digits = precision 10, scale 2

Three dialects, one model, and the differences are exactly the dialect trivia the tool exists to absorb. Postgres gets `GENERATED ALWAYS AS IDENTITY`, the modern spelling of an auto-assigning integer key. MySQL gets `AUTO_INCREMENT` with backticks around every identifier — here they are optional, but reserved-word collisions in MySQL are common enough that quoting by default saves an entire class of error. SQLite gets the famous `INTEGER PRIMARY KEY`, which is not syntax sugar but the rowid alias: it auto-assigns, and it is the one place SQLite's type affinity system makes a real behavioral promise.

The `status` column hides a second dialect fork, one that bites anyone who copies schemas between databases: enumerated types. MySQL has a native ENUM — `status ENUM('new','paid','refunded') NOT NULL DEFAULT 'new'` compiles, stores each value as a one-byte index into the declared list, and rejects anything outside it under strict mode. PostgreSQL has no inline equivalent: ENUM exists, but you have to create the type first with `CREATE TYPE order_status AS ENUM (...)` and then reference it, which means two statements where MySQL needs one. SQLite has no enumerated types at all, and its documentation is blunt about the prospects. The portable spelling everywhere is `TEXT` plus `CHECK (status IN ('new','paid','refunded'))`, which is exactly what the generator emits for each dialect: the MySQL DBA gives up the one-byte storage, the Postgres DBA gives up nothing, and the constraint enforces the same three values on all three engines.

SQLite's output also deserves its caveat: `NUMERIC(10,2)` there is an affinity declaration, not an enforcement. SQLite will happily store a string in that column if you insert one, because its type system is a suggestion engine with good manners. The generator still emits the precision for documentation value, and for the day the same schema is ported somewhere with a stricter conscience.

SQL Server completes the set, and its rendering is where dialect awareness pays its own rent: the identity column becomes `INT IDENTITY(1,1)`, `CURRENT_TIMESTAMP` becomes `GETDATE()`, and identifiers are bracketed — `[order_id]` — in keeping with T-SQL's house style. A fourth export from the same dropdown, zero additional decisions on your part. That is the quiet argument for model-over-text tools: dialect knowledge compounds, and you only pay for it once, in the renderer.

One more cross-dialect wrinkle worth knowing before you run any of these in anger: unquoted identifiers fold to lowercase in PostgreSQL, stay case-sensitive in quoted form everywhere, and MySQL's case sensitivity for table names depends on the operating system underneath the server. The generator quotes consistently per dialect, which sidesteps the worst of it, but if you hand-edit afterward, keep the quoting style uniform — a schema where half the columns are quoted and half are not is a schema with a future full of surprises.

Where inference needs adult supervision

Automated typing has well-known blind spots, and pretending otherwise would be a disservice to your data:

  • Leading zeros and identifier numbers. ZIP codes, phone numbers, account numbers, and anything where `02134` differs from `2134`. If the column parses as integers, the tool proposes INTEGER — override to TEXT whenever digits are names rather than quantities. This is the single most common inference mistake in the wild.
  • Ambiguous dates. `03/04/2026` parses as a date under two readings and the tool refuses both, defaulting to TEXT rather than guessing your continent. ISO dates infer cleanly, which is one of several hundred reasons to prefer them.
  • Booleans in disguise. Columns of 0/1 infer as INTEGER; toggle them to BOOLEAN and MySQL gets TINYINT(1), Postgres gets BOOLEAN, SQLite gets INTEGER affinity — each dialect's native spelling of yes-or-no.
  • Sampling limits. Inference reads up to a configurable number of rows (default: the first 10,000). A column that is clean for 9,999 rows and then contains a postal code with letters will infer as INTEGER from the sample. Raise the cap, or eyeball the exported type for identifier-ish columns.
  • Headers with spaces or reserved words. Column names like `order`, `group`, or `total due` need quoting; the tool quotes identifiers per dialect automatically, so `total due` becomes `"total due"` in Postgres and `` `total due` `` in MySQL.
  • One data row. A single value can prove a type fits but says nothing about the schema. The tool still infers; you should still read.
  • Completely empty columns. No evidence, so TEXT is proposed with a note. Fill the type yourself once you know what the column is for.

Keys, constraints, and the defaults that matter

A column list is not a table; the constraint section is where the database starts making promises. The generator's constraint panel covers the standard set with sensible naming. Primary keys can be single-column or composite — for a junction table, mark both columns and the export writes `PRIMARY KEY (order_id, product_sku)` as a table constraint rather than inline marks. Foreign keys get a column, a target table, and a target column, and the export emits `REFERENCES` clauses in your dialect's spelling along with an index on the child column.

Defaults are dialect-aware in the ways that bite people. "Now" is `CURRENT_TIMESTAMP` in all three major dialects but `GETDATE()` in SQL Server, and the tool tracks that mapping so you do not have to. String defaults arrive properly quoted; numeric defaults unquoted; and the `DEFAULT` keyword placement follows each dialect's grammar rather than a lowest-common-denominator blend that works nowhere.

Naming conventions sound cosmetic until the first constraint violation error crosses your screen. `INSERT fails: violation of constraint fk_orders_customer` is a debuggable message; `INSERT fails: constraint 17` is an archaeology project. The generator names every constraint it emits — `pk_` for primary keys, `fk_` for foreign keys, `uq_` for uniques, `ck_` for checks, prefixed with the table name — and lets you edit every one.

Who reaches for a DDL generator

Analysts, mostly, and data engineers standing up staging tables. The classic path is a CSV export that has outgrown spreadsheet treatment — a few hundred thousand rows, some filtering, a join that Excel would deliver via circular reference and tears. The database is the right next step, and the `CREATE TABLE` is the toll booth on the way there. Inferring it from the file removes the last excuse.

Developers use it during migrations and prototypes: a client hands over their data as spreadsheets, and the schema needs to exist before the code that reads it can be written. Instructors use it to teach DDL without spending the first hour of lab on syntax errors from dialect drift. And a quiet population uses it as a dialect translator — a table that exists in a Postgres brain and needs to materialize in MySQL, with the model doing the spelling.

The surrounding workflow has tools for every neighboring step: once the table exists, SQL on CSV can query the file with real SQL before any import, the ER Diagram Generator turns the resulting DDL back into a picture of the schema, and SQL Query Explainer decodes the first query you write against the new table. Going the other direction, from data to full INSERT statements, is the job of the CSV to SQL INSERT converter.

Generator versus hand-writing versus a chatbot

ApproachType accuracyDialect correctnessData privacyIteration
This generatorMeasured from your actual valuesGuaranteed — one model, four renderersLocal onlyEdit grid, re-export instantly
Hand-writing DDLWhatever you guessedWhatever you rememberedLocalRetype, re-test, re-sigh
Asking a general chatbotPlausible, based on a description you typedOften mixed dialects in one statementYour schema and sample rows leave the machineRe-prompt and hope
Database IDE wizardsManual forms per columnCorrect for that one databaseLocal, heavyweight installReopen the wizard
Four ways to end up with a CREATE TABLE statement.

The chatbot row is not a cheap shot — conversational assistants are genuinely useful for sketching schemas — but their output has a specific failure signature: beautifully formatted DDL mixing `AUTO_INCREMENT` with `IDENTITY` with quoted identifiers in ways no single database accepts, plus types invented for columns they never saw. A deterministic generator that reads the actual file cannot hallucinate a column, and cannot drift between dialects, because the model and the rendering are separate pieces.

Hand-writing remains the right choice for tables you can hold in your head, and the wizard for shops already living inside one IDE. The generator earns its keep at the boundary between data you have and schema you need, which is where an enormous amount of ordinary work happens to sit.

Your data never crosses the network

The CSV you load here is read with the browser's file APIs and scanned in memory; the grid, the inference, and the DDL rendering are all plain JavaScript in the page. There is no upload endpoint behind the drop zone — a claim you can verify in about ten seconds with your browser's network inspector, which will show the page load and then nothing.

The stakes are higher than they look for a schema tool. A CSV of orders with customer emails is customer data; a generated table definition can leak column names that encode business logic. Keeping both on the machine means the tool is usable on exactly the data that most needs the convenience: exports under NDA, health-adjacent records, financials during a quiet period.

And like everything else in this cluster, the page keeps working with the network switched off after load. Offline is not a feature anyone asks for until the day it is the only feature that matters.

CREATE TABLE questions

Which databases can the generator target?

MySQL and MariaDB, PostgreSQL, SQLite, and SQL Server. The same internal table model renders into each dialect's spelling of types, auto-incrementing keys, quoting style, and defaults — you design once and export whichever flavor the destination speaks, including several at once.

How does it infer types from my CSV?

Each column is scanned across the file (up to the sampling cap you can raise), and every non-empty value must pass a type's test for that type to win. Integers, decimals, ISO dates, timestamps, and true/false booleans are checked in strictest-first order; the first type all values satisfy is the column's type. If nothing fits — a column mixing 42 and late — it falls back to text with the observed maximum length reported.

Why did my ZIP code column come out as TEXT?

Because somewhere in that column there is a value that is not a plain integer — a leading zero like 02134, a Canadian postal code, or a ZIP+4 like 02134-5678. Numbers that are identifiers rather than quantities belong in text columns, and the inference is conservative on purpose: it would rather hand you TEXT than silently destroy a leading zero.

Does the tool create indexes?

You can mark any column as indexed and the export emits a CREATE INDEX statement after the table. The generator also proposes an index automatically on any column you designate as a foreign key, since that is the one index every real schema wants. It will not invent indexes beyond what you asked for — index strategy is a workload question, not a schema question.

Can I edit the generated SQL?

The output is a plain text panel with copy and download, and the visual model stays live — change a type in the grid and every dialect's export updates together. The generated file is yours to hand-edit afterward; nothing re-validates it or locks it.

Does it run the SQL against a database?

No. The tool generates text and stops there. It never connects to a database, and executing the DDL is a step you take in your own environment where migrations, permissions, and review processes live.

Is my CSV uploaded anywhere?

No. The file is read locally through the browser's File API, inference runs in the tab, and the generated DDL is assembled in memory. There is no server component in the data path, so nothing you paste or load is transmitted anywhere.

Tools that pair well with it

Round out the pipeline: SQL on CSV for querying files before you commit to a schema, ER Diagram Generator to visualize the DDL you just generated, and SQL Query Explainer to dissect the first SELECT you point at the new table.

Where the dialect facts come from

Sources and further reading

We cite primary sources (standards bodies, government agencies, official documentation) wherever they exist so you can verify every number on this page yourself.