CSV to SQL Importer — C 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 C implementation — the same logic the interactive tool runs, in a shareable, citable form.
/* csv-to-sql — pure CSV → SQL import generator. C 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. Caller frees result.sql. */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/* ---- small growable string ---- */
typedef struct { char *buf; size_t len, cap; } Str;
static void str_reserve(Str *s, size_t need) {
if (need >= s->cap) {
size_t cap = s->cap ? s->cap : 64;
while (need >= cap) cap *= 2;
s->buf = realloc(s->buf, cap);
s->cap = cap;
}
}
static void str_push(Str *s, char c) {
str_reserve(s, s->len + 1);
s->buf[s->len++] = c;
s->buf[s->len] = '\0';
}
static void str_app(Str *s, const char *t) {
size_t tl = strlen(t);
str_reserve(s, s->len + tl);
memcpy(s->buf + s->len, t, tl + 1);
s->len += tl;
}
static void str_clear(Str *s) {
if (s->buf) s->buf[s->len = 0] = '\0';
}
/* ---- RFC 4180 parser (mirrors the TS loop) ---- */
typedef struct { char **fields; size_t n; } Row;
typedef struct { Row *rows; size_t n; } Table;
static Row row_push_field(Row r, const char *f) {
r.fields = realloc(r.fields, (r.n + 2) * sizeof(char *));
r.fields[r.n++] = strdup(f);
r.fields[r.n] = NULL;
return r;
}
static Table table_push_row(Table t, Row r) {
t.rows = realloc(t.rows, (t.n + 1) * sizeof(Row));
t.rows[t.n++] = r;
return t;
}
static Table csv_to_rows(const char *csv) {
Table t = {0};
Str field = {0};
Row row = {0};
int in_q = 0;
for (size_t i = 0, n = strlen(csv); i < n; i++) {
char ch = csv[i];
if (in_q) {
if (ch == '"') {
if (i + 1 < n && csv[i + 1] == '"') { str_push(&field, '"'); i++; }
else in_q = 0;
} else str_push(&field, ch);
} else if (ch == '"') {
in_q = 1;
} else if (ch == ',') {
row = row_push_field(row, field.buf ? field.buf : "");
str_clear(&field);
} else if (ch == '\n') {
row = row_push_field(row, field.buf ? field.buf : "");
t = table_push_row(t, row);
row = (Row){0};
str_clear(&field);
} else if (ch != '\r') {
str_push(&field, ch);
}
}
if (field.len > 0 || row.n > 0) {
row = row_push_field(row, field.buf ? field.buf : "");
t = table_push_row(t, row);
}
free(field.buf);
return t;
}
static void free_table(Table t) {
for (size_t r = 0; r < t.n; r++) {
for (size_t f = 0; f < t.rows[r].n; f++) free(t.rows[r].fields[f]);
free(t.rows[r].fields);
}
free(t.rows);
}
/* ---- SQL helpers ---- */
static int is_ident_char(char c) {
return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') ||
(c >= '0' && c <= '9') || c == '_';
}
/* Verbatim numeric literal: -?(digits[.digits] | .digits)(e[+-]?digits)?
* "5." is rejected (digits required after the dot) — matches the TS regex. */
static int is_numeric(const char *s) {
size_t i = 0, n = strlen(s);
if (i < n && s[i] == '-') i++;
size_t int_start = i;
while (i < n && s[i] >= '0' && s[i] <= '9') i++;
int had_int = i > int_start;
int had_frac = 0, saw_dot = 0;
if (i < n && s[i] == '.') {
saw_dot = 1; i++;
size_t fs = i;
while (i < n && s[i] >= '0' && s[i] <= '9') i++;
had_frac = i > fs;
}
if (saw_dot && !had_frac) return 0;
if (!had_int && !had_frac) return 0;
if (i < n && (s[i] == 'e' || s[i] == 'E')) {
i++;
if (i < n && (s[i] == '+' || s[i] == '-')) i++;
size_t es = i;
while (i < n && s[i] >= '0' && s[i] <= '9') i++;
if (i == es) return 0;
}
return i == n;
}
static void escape_sql_string(Str *out, const char *s, int mysql) {
for (const char *p = s; *p; p++) {
if (*p == '\'') str_app(out, "''");
else if (mysql && *p == '\\') str_app(out, "\\\\");
else if (mysql && (unsigned char)*p == 0x00) str_app(out, "\\0");
else if (mysql && *p == '\n') str_app(out, "\\n");
else if (mysql && *p == '\r') str_app(out, "\\r");
else if (mysql && (unsigned char)*p == 0x1a) str_app(out, "\\Z");
else str_push(out, *p);
}
}
static void field_literal(Str *out, const char *v, int mysql, int infer_types) {
if (infer_types && v[0] == '\0') { str_app(out, "NULL"); return; }
if (infer_types && is_numeric(v)) { str_app(out, v); return; } /* verbatim */
str_push(out, '\'');
escape_sql_string(out, v, mysql);
str_push(out, '\'');
}
/* Sanitize into a malloc'd identifier. */
static char *sanitize_ident(const char *name) {
char *out = malloc(strlen(name) + 1);
size_t j = 0;
for (const char *p = name; *p; p++) out[j++] = is_ident_char(*p) ? *p : '_';
out[j] = '\0';
return out;
}
static void quote_ident(Str *out, const char *name, int mysql, int quote_ids) {
if (!quote_ids) { str_app(out, name); return; }
str_app(out, mysql ? "`" : "\"");
str_app(out, name);
str_app(out, mysql ? "`" : "\"");
}
static void csv_escape(Str *out, const char *f) {
if (!strpbrk(f, ",\n\r\"")) { str_app(out, f); return; }
str_push(out, '"');
for (const char *p = f; *p; p++) {
if (*p == '"') str_push(out, '"');
str_push(out, *p);
}
str_push(out, '"');
}
/* ---- main entry ---- */
typedef struct { int ok; char *sql; long rows; const char *error; } CsvToSqlResult;
static char *cell(Table *t, size_t r, size_t c) {
return c < t->rows[r].n ? t->rows[r].fields[c] : "";
}
CsvToSqlResult csv_to_sql(const char *csv, const char *table, const char *format,
const char *dialect, long batch_size, int infer_types,
int quote_ids, const char *file_name) {
CsvToSqlResult res = {0};
/* trim surrounding whitespace */
const char *p = csv;
while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++;
const char *e = p + strlen(p);
while (e > p && (e[-1] == ' ' || e[-1] == '\t' || e[-1] == '\n' || e[-1] == '\r')) e--;
size_t tlen = (size_t)(e - p);
int empty = tlen == 0;
char *text = malloc(tlen + 1);
memcpy(text, p, tlen);
text[tlen] = '\0';
Table t = csv_to_rows(text);
if (empty || t.n < 2) {
free_table(t); free(text);
res.error = "No rows to import.";
return res;
}
size_t n_cols = t.rows[0].n;
long n_data = (long)t.n - 1;
int mysql = strcmp(dialect, "mysql") == 0;
int fmt_copy = strcmp(format, "copy") == 0;
int fmt_load = strcmp(format, "load-data") == 0;
int ident_mysql = fmt_load ? 1 : fmt_copy ? 0 : mysql;
/* sanitize + de-duplicate headers, then quote the column list */
char **cols = malloc(n_cols * sizeof(char *));
for (size_t c = 0; c < n_cols; c++) {
char *h = strdup(t.rows[0].fields[c]);
size_t hn = strlen(h);
while (hn && (h[0] == ' ' || h[0] == '\t')) memmove(h, h + 1, hn--);
while (hn && (h[hn - 1] == ' ' || h[hn - 1] == '\t')) h[--hn] = '\0';
char *id = sanitize_ident(h);
free(h);
if (!id[0]) { free(id); id = malloc(16); snprintf(id, 16, "col%zu", c + 1); }
int dup = 0;
for (size_t k = 0; k < c; k++) if (strcmp(cols[k], id) == 0) dup++;
if (dup > 0) {
char *suff = malloc(strlen(id) + 8);
snprintf(suff, strlen(id) + 8, "%s_%d", id, dup + 1);
free(id);
id = suff;
}
cols[c] = id;
}
char *tbl_raw = sanitize_ident(table);
Str tbl = {0};
quote_ident(&tbl, tbl_raw[0] ? tbl_raw : "tbl", ident_mysql, quote_ids);
Str col_list = {0};
for (size_t c = 0; c < n_cols; c++) {
if (c) str_app(&col_list, ", ");
quote_ident(&col_list, cols[c], ident_mysql, quote_ids);
}
Str sql = {0};
if (fmt_copy || fmt_load) {
/* payload: sanitized header + data rows, re-emitted as clean CSV */
for (size_t c = 0; c < n_cols; c++) {
if (c) str_push(&sql, ',');
csv_escape(&sql, cols[c]);
}
for (size_t r = 1; r < t.n; r++) {
str_push(&sql, '\n');
for (size_t c = 0; c < n_cols; c++) {
if (c) str_push(&sql, ',');
csv_escape(&sql, cell(&t, r, c));
}
}
Str out = {0};
if (fmt_copy) {
str_app(&out, "COPY ");
str_app(&out, tbl.buf);
str_app(&out, " (");
str_app(&out, col_list.buf);
str_app(&out, ") FROM STDIN WITH (FORMAT csv, HEADER true);\n");
str_app(&out, sql.buf);
str_app(&out, "\n\\.");
} else {
char *clean = malloc(strlen(file_name) + 1);
size_t cn = 0;
for (const char *fp = file_name; *fp; fp++)
if (is_ident_char(*fp) || *fp == '.' || *fp == '-' || *fp == '/')
clean[cn++] = *fp;
clean[cn] = '\0';
if (!cn) { strcpy(clean, "import.csv"); }
str_app(&out, "LOAD DATA LOCAL INFILE '");
str_app(&out, clean);
str_app(&out, "'\nINTO TABLE ");
str_app(&out, tbl.buf);
str_app(&out, "\nFIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '\"'\nLINES TERMINATED BY '\\n'\nIGNORE 1 LINES;\n\n");
str_app(&out, sql.buf);
free(clean);
}
sql = out;
} else {
long size = batch_size > 0 ? batch_size : 100;
for (long start = 1; start < (long)t.n; start += size) {
if (start > 1) str_push(&sql, '\n');
str_app(&sql, "INSERT INTO ");
str_app(&sql, tbl.buf);
str_app(&sql, " (");
str_app(&sql, col_list.buf);
str_app(&sql, ") VALUES\n");
long end = start + size;
if (end > (long)t.n) end = (long)t.n;
for (long r = start; r < end; r++) {
if (r > start) str_app(&sql, ",\n");
str_app(&sql, " (");
for (size_t c = 0; c < n_cols; c++) {
if (c) str_app(&sql, ", ");
field_literal(&sql, cell(&t, (size_t)r, c), mysql, infer_types);
}
str_push(&sql, ')');
}
str_push(&sql, ';');
}
}
for (size_t c = 0; c < n_cols; c++) free(cols[c]);
free(cols); free(tbl_raw); free(tbl.buf); free(col_list.buf);
free_table(t); free(text);
res.ok = 1;
res.sql = sql.buf;
res.rows = n_data;
return res;
}
/* Example:
* CsvToSqlResult r = csv_to_sql("id,name\n1,Ada\n2,", "users",
* "insert", "standard", 100, 1, 1, "import.csv");
* printf("%s\n", r.sql);
* // INSERT INTO "users" ("id", "name") VALUES
* // (1, 'Ada'),
* // (2, NULL);
* free(r.sql); */
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 →