CSV to SQL Importer — PHP 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 PHP implementation — the same logic the interactive tool runs, in a shareable, citable form.
<?php
// csv-to-sql — pure CSV → SQL import generator. PHP 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 text stays verbatim; empty
// fields become NULL. Never throws — errors travel in the result array.
function csv_to_rows(string $csv): array
{
$rows = [];
$field = '';
$row = [];
$inQ = false;
$n = strlen($csv);
for ($i = 0; $i < $n; $i++) {
$ch = $csv[$i];
if ($inQ) {
if ($ch === '"') {
if ($i + 1 < $n && $csv[$i + 1] === '"') { $field .= '"'; $i++; }
else { $inQ = false; }
} else {
$field .= $ch;
}
} elseif ($ch === '"') {
$inQ = true;
} elseif ($ch === ',') {
$row[] = $field; $field = '';
} elseif ($ch === "\n") {
$row[] = $field; $rows[] = $row; $row = []; $field = '';
} elseif ($ch !== "\r") {
$field .= $ch;
}
}
if ($field !== '' || $row !== []) { $row[] = $field; $rows[] = $row; }
return $rows;
}
function csv_to_sql_escape(string $s, string $dialect = 'standard'): string
{
$out = str_replace("'", "''", $s);
if ($dialect === 'mysql') {
$out = str_replace(["\\", "\0", "\n", "\r", "\x1a"], ["\\\\", "\\0", "\\n", "\\r", "\\Z"], $out);
}
return $out;
}
function csv_to_sql_field(string $value, string $dialect = 'standard', bool $inferTypes = true): string
{
if ($inferTypes) {
if ($value === '') return 'NULL';
// Verbatim numeric match — no float round-trip, keeps parity with TS/Go.
if (preg_match('/^-?(\d+(\.\d+)?|\.\d+)([eE][+-]?\d+)?$/', $value)) return $value;
}
return "'" . csv_to_sql_escape($value, $dialect) . "'";
}
function csv_to_sql_sanitize(string $name): string
{
return preg_replace('/[^A-Za-z0-9_]/', '_', $name);
}
function csv_to_sql_headers(array $headers): array
{
$seen = [];
$out = [];
foreach ($headers as $i => $h) {
$id = csv_to_sql_sanitize(trim($h));
if ($id === '') $id = 'col' . ($i + 1);
$seen[$id] = ($seen[$id] ?? 0) + 1;
if ($seen[$id] > 1) $id = $id . '_' . $seen[$id];
$out[] = $id;
}
return $out;
}
function csv_to_sql_escape_csv(string $field): string
{
if (strpbrk($field, ",\n\r\"") !== false) {
return '"' . str_replace('"', '""', $field) . '"';
}
return $field;
}
/**
* @return array{ok: bool, sql: string, rows: int, error: ?string}
*/
function csv_to_sql(string $csv, string $table, string $format = 'insert', string $dialect = 'standard',
int $batchSize = 100, bool $inferTypes = true, bool $quoteIdentifiers = true,
string $fileName = 'import.csv'): array
{
$fail = function (string $msg): array {
return ['ok' => false, 'sql' => '', 'rows' => 0, 'error' => $msg];
};
$text = trim($csv);
$rows = csv_to_rows($text);
if ($text === '' || count($rows) < 2) return $fail('No rows to import.');
$cols = csv_to_sql_headers($rows[0]);
$data = array_slice($rows, 1);
$identDialect = $format === 'copy' ? 'postgres' : ($format === 'load-data' ? 'mysql' : $dialect);
$q = function (string $name) use ($quoteIdentifiers, $identDialect): string {
if (!$quoteIdentifiers) return $name;
return $identDialect === 'mysql' ? "`{$name}`" : "\"{$name}\"";
};
$tblIdent = csv_to_sql_sanitize($table);
$tbl = $q($tblIdent === '' ? 'tbl' : $tblIdent);
$colList = implode(', ', array_map($q, $cols));
if ($format === 'copy' || $format === 'load-data') {
$payload = [implode(',', array_map('csv_to_sql_escape_csv', $cols))];
foreach ($data as $r) {
$fields = [];
for ($ci = 0; $ci < count($cols); $ci++) {
$fields[] = csv_to_sql_escape_csv($r[$ci] ?? '');
}
$payload[] = implode(',', $fields);
}
if ($format === 'copy') {
$sql = "COPY {$tbl} ({$colList}) FROM STDIN WITH (FORMAT csv, HEADER true);\n"
. implode("\n", $payload) . "\n\\.";
} else {
$clean = preg_replace('/[^A-Za-z0-9._\-\/]/', '', $fileName);
if ($clean === '' || $clean === null) $clean = 'import.csv';
$sql = "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" . implode("\n", $payload);
}
return ['ok' => true, 'sql' => $sql, 'rows' => count($data), 'error' => null];
}
$size = max(1, $batchSize);
$stmts = [];
for ($start = 0; $start < count($data); $start += $size) {
$values = [];
foreach (array_slice($data, $start, $size) as $r) {
$vals = [];
for ($ci = 0; $ci < count($cols); $ci++) {
$vals[] = csv_to_sql_field($r[$ci] ?? '', $dialect, $inferTypes);
}
$values[] = ' (' . implode(', ', $vals) . ')';
}
$stmts[] = "INSERT INTO {$tbl} ({$colList}) VALUES\n" . implode(",\n", $values) . ';';
}
return ['ok' => true, 'sql' => implode("\n", $stmts), 'rows' => count($data), 'error' => null];
}
// Example:
// csv_to_sql("id,name\n1,Ada\n2,", '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 →