Skip to content

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.
#include <algorithm>
#include <iostream>
#include <map>
#include <string>
#include <vector>

// RFC 4180 parser mirroring the TS loop: lenient quotes, CR dropped, a
// trailing field without a newline still completes its row.
std::vector<std::vector<std::string>> csvToRows(const std::string &csv) {
    std::vector<std::vector<std::string>> rows;
    std::string field;
    std::vector<std::string> row;
    bool inQ = false;
    for (size_t i = 0; i < csv.size(); i++) {
        char ch = csv[i];
        if (inQ) {
            if (ch == '"') {
                if (i + 1 < csv.size() && csv[i + 1] == '"') { field += '"'; i++; }
                else inQ = false;
            } else field += ch;
        } else if (ch == '"') inQ = true;
        else if (ch == ',') { row.push_back(field); field.clear(); }
        else if (ch == '\n') { row.push_back(field); rows.push_back(row); row.clear(); field.clear(); }
        else if (ch != '\r') field += ch;
    }
    if (!field.empty() || !row.empty()) { row.push_back(field); rows.push_back(row); }
    return rows;
}

bool isIdentChar(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.
bool isNumeric(const std::string &s) {
    size_t i = 0, n = s.size();
    if (i < n && s[i] == '-') i++;
    size_t intStart = i;
    while (i < n && isdigit((unsigned char)s[i])) i++;
    bool hadInt = i > intStart;
    bool hadFrac = false, sawDot = false;
    if (i < n && s[i] == '.') {
        sawDot = true; i++;
        size_t fs = i;
        while (i < n && isdigit((unsigned char)s[i])) i++;
        hadFrac = i > fs;
    }
    if (sawDot && !hadFrac) return false;
    if (!hadInt && !hadFrac) return false;
    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 && isdigit((unsigned char)s[i])) i++;
        if (i == es) return false;
    }
    return i == n;
}

std::string escapeSqlString(const std::string &s, bool mysql) {
    std::string out;
    for (char c : s) {
        if (c == '\'') out += "''";
        else if (mysql && c == '\\') out += "\\\\";
        else if (mysql && c == '\0') out += "\\0";
        else if (mysql && c == '\n') out += "\\n";
        else if (mysql && c == '\r') out += "\\r";
        else if (mysql && c == '\x1a') out += "\\Z";
        else out += c;
    }
    return out;
}

std::string fieldLiteral(const std::string &v, bool mysql, bool inferTypes) {
    if (inferTypes) {
        if (v.empty()) return "NULL";
        if (isNumeric(v)) return v; // verbatim — no float round-trip
    }
    return "'" + escapeSqlString(v, mysql) + "'";
}

std::string sanitizeIdent(const std::string &name) {
    std::string out;
    for (char c : name) out += isIdentChar(c) ? c : '_';
    return out;
}

std::vector<std::string> sanitizeHeaders(const std::vector<std::string> &headers) {
    std::map<std::string, int> seen;
    std::vector<std::string> out;
    for (size_t i = 0; i < headers.size(); i++) {
        std::string h = headers[i];
        // trim
        h.erase(0, h.find_first_not_of(" \t"));
        h.erase(h.find_last_not_of(" \t") + 1);
        std::string id = sanitizeIdent(h);
        if (id.empty()) id = "col" + std::to_string(i + 1);
        seen[id]++;
        if (seen[id] > 1) id += "_" + std::to_string(seen[id]);
        out.push_back(id);
    }
    return out;
}

std::string csvEscape(const std::string &f) {
    if (f.find_first_of(",\n\r\"") == std::string::npos) return f;
    std::string out = "\"";
    for (char c : f) {
        if (c == '"') out += '"';
        out += c;
    }
    return out + "\"";
}

struct Options {
    std::string table;
    std::string format = "insert";        // insert | copy | load-data
    std::string dialect = "standard";     // standard | mysql | postgres
    long batchSize = 100;
    bool inferTypes = true;
    bool quoteIdentifiers = true;
    std::string fileName = "import.csv";
};

struct Result {
    bool ok = false;
    std::string sql;
    long rows = 0;
    std::string error;
};

std::string quoteIdent(const std::string &name, bool mysql, bool quoteIds) {
    if (!quoteIds) return name;
    return mysql ? "`" + name + "`" : "\"" + name + "\"";
}

Result csvToSql(const std::string &csv, const Options &opts) {
    Result res;
    auto fail = [&](const char *msg) { res.error = msg; return res; };

    // trim
    size_t b = csv.find_first_not_of(" \t\n\r");
    size_t e = csv.find_last_not_of(" \t\n\r");
    std::string text = b == std::string::npos ? "" : csv.substr(b, e - b + 1);

    auto rows = csvToRows(text);
    if (text.empty() || rows.size() < 2) return fail("No rows to import.");

    auto cols = sanitizeHeaders(rows[0]);
    auto data = std::vector<std::vector<std::string>>(rows.begin() + 1, rows.end());
    bool mysql = opts.dialect == "mysql";
    bool fmtCopy = opts.format == "copy";
    bool fmtLoad = opts.format == "load-data";
    bool identMysql = fmtLoad ? true : fmtCopy ? false : mysql;

    std::string tblRaw = sanitizeIdent(opts.table);
    std::string tbl = quoteIdent(tblRaw.empty() ? "tbl" : tblRaw, identMysql, opts.quoteIdentifiers);
    std::string colList;
    for (size_t c = 0; c < cols.size(); c++) {
        if (c) colList += ", ";
        colList += quoteIdent(cols[c], identMysql, opts.quoteIdentifiers);
    }
    auto cellAt = [&](const std::vector<std::string> &r, size_t ci) {
        return ci < r.size() ? r[ci] : "";
    };

    if (fmtCopy || fmtLoad) {
        std::vector<std::string> payload;
        std::string header;
        for (size_t c = 0; c < cols.size(); c++) {
            if (c) header += ',';
            header += csvEscape(cols[c]);
        }
        payload.push_back(header);
        for (const auto &r : data) {
            std::string line;
            for (size_t c = 0; c < cols.size(); c++) {
                if (c) line += ',';
                line += csvEscape(cellAt(r, c));
            }
            payload.push_back(line);
        }
        std::string body;
        for (size_t i = 0; i < payload.size(); i++) {
            if (i) body += '\n';
            body += payload[i];
        }
        if (fmtCopy) {
            res.sql = "COPY " + tbl + " (" + colList +
                      ") FROM STDIN WITH (FORMAT csv, HEADER true);\n" + body + "\n\\.";
        } else {
            std::string clean;
            for (char c : opts.fileName)
                if (isIdentChar(c) || c == '.' || c == '-' || c == '/') clean += c;
            if (clean.empty()) clean = "import.csv";
            res.sql = "LOAD DATA LOCAL INFILE '" + clean + "'\nINTO TABLE " + tbl +
                      "\nFIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '\"'"
                      "\nLINES TERMINATED BY '\\n'\nIGNORE 1 LINES;\n\n" + body;
        }
        res.ok = true;
        res.rows = (long)data.size();
        return res;
    }

    long size = std::max(1L, opts.batchSize);
    std::vector<std::string> stmts;
    for (size_t start = 0; start < data.size(); start += size) {
        std::string values;
        size_t end = std::min(data.size(), start + size);
        for (size_t r = start; r < end; r++) {
            if (r > start) values += ",\n";
            values += "  (";
            for (size_t c = 0; c < cols.size(); c++) {
                if (c) values += ", ";
                values += fieldLiteral(cellAt(data[r], c), mysql, opts.inferTypes);
            }
            values += ")";
        }
        stmts.push_back("INSERT INTO " + tbl + " (" + colList + ") VALUES\n" + values + ";");
    }
    res.ok = true;
    for (size_t i = 0; i < stmts.size(); i++) {
        if (i) res.sql += '\n';
        res.sql += stmts[i];
    }
    res.rows = (long)data.size();
    return res;
}

// Example:
//   Options o; o.table = "users";
//   Result r = csvToSql("id,name\n1,Ada\n2,", o);
//   std::cout << r.sql << "\n";
//   // INSERT INTO "users" ("id", "name") VALUES
//   //   (1, 'Ada'),
//   //   (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 →