Skip to content

CSV to SQL Importer — Java 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 Java implementation — the same logic the interactive tool runs, in a shareable, citable form.

// csv-to-sql — pure CSV → SQL import generator. Java 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.
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;

// Package-private: display snippets keep the repo filename (csv-to-sql.java),
// which need not match the class name.
final class CsvToSql {

    /** RFC 4180 parser mirroring the TS loop: lenient quotes, CR dropped, a
     *  trailing field without a newline still completes its row. */
    public static List<String[]> csvToRows(String csv) {
        List<String[]> rows = new ArrayList<>();
        StringBuilder field = new StringBuilder();
        List<String> row = new ArrayList<>();
        boolean inQ = false;
        for (int i = 0; i < csv.length(); i++) {
            char ch = csv.charAt(i);
            if (inQ) {
                if (ch == '"') {
                    if (i + 1 < csv.length() && csv.charAt(i + 1) == '"') { field.append('"'); i++; }
                    else inQ = false;
                } else field.append(ch);
            } else if (ch == '"') inQ = true;
            else if (ch == ',') { row.add(field.toString()); field.setLength(0); }
            else if (ch == '\n') { row.add(field.toString()); rows.add(row.toArray(new String[0])); row.clear(); field.setLength(0); }
            else if (ch != '\r') field.append(ch);
        }
        if (field.length() > 0 || !row.isEmpty()) { row.add(field.toString()); rows.add(row.toArray(new String[0])); }
        return rows;
    }

    // Verbatim numeric literal — matched text emitted as-is (no float
    // round-trip, so "007" and "1e3" pass through unchanged).
    private static final Pattern NUMERIC = Pattern.compile("^-?(\\d+(\\.\\d+)?|\\.\\d+)([eE][+-]?\\d+)?$");

    public static String escapeSqlString(String s, String dialect) {
        String out = s.replace("'", "''");
        if ("mysql".equals(dialect)) {
            out = out.replace("\\", "\\\\").replace("\0", "\\0")
                     .replace("\n", "\\n").replace("\r", "\\r")
                     .replace("\u001a", "\\Z");
        }
        return out;
    }

    public static String fieldLiteral(String value, String dialect, boolean inferTypes) {
        if (inferTypes) {
            if (value.isEmpty()) return "NULL";
            if (NUMERIC.matcher(value).matches()) return value;
        }
        return "'" + escapeSqlString(value, dialect) + "'";
    }

    public static String sanitizeIdent(String name) {
        StringBuilder b = new StringBuilder();
        for (char c : name.toCharArray()) {
            boolean ok = (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z')
                      || (c >= '0' && c <= '9') || c == '_';
            b.append(ok ? c : '_');
        }
        return b.toString();
    }

    private static String[] sanitizeHeaders(String[] headers) {
        Map<String, Integer> seen = new HashMap<>();
        String[] out = new String[headers.length];
        for (int i = 0; i < headers.length; i++) {
            String id = sanitizeIdent(headers[i].trim());
            if (id.isEmpty()) id = "col" + (i + 1);
            int n = seen.merge(id, 1, Integer::sum);
            if (n > 1) id = id + "_" + n;
            out[i] = id;
        }
        return out;
    }

    private static String csvEscape(String field) {
        boolean needs = field.indexOf(',') >= 0 || field.indexOf('"') >= 0
                     || field.indexOf('\n') >= 0 || field.indexOf('\r') >= 0;
        return needs ? "\"" + field.replace("\"", "\"\"") + "\"" : field;
    }

    public static final class Options {
        public String table = "";
        public String format = "insert";        // insert | copy | load-data
        public String dialect = "standard";     // standard | mysql | postgres
        public int batchSize = 100;
        public boolean inferTypes = true;
        public boolean quoteIdentifiers = true;
        public String fileName = "import.csv";
    }

    public static final class Result {
        public boolean ok;
        public String sql = "";
        public int rows;
        public String error;
    }

    private static String quoteIdent(String name, boolean mysql, boolean quoteIds) {
        if (!quoteIds) return name;
        return mysql ? "`" + name + "`" : "\"" + name + "\"";
    }

    private static String cellAt(String[] row, int ci) {
        return ci < row.length ? row[ci] : "";
    }

    public static Result convert(String csv, Options opts) {
        Result res = new Result();
        String text = csv.trim();
        List<String[]> rows = csvToRows(text);
        if (text.isEmpty() || rows.size() < 2) {
            res.error = "No rows to import.";
            return res;
        }

        String[] cols = sanitizeHeaders(rows.get(0));
        List<String[]> data = rows.subList(1, rows.size());
        boolean mysql = "mysql".equals(opts.dialect);
        boolean identMysql = "load-data".equals(opts.format) ? true
                          : "copy".equals(opts.format) ? false
                          : mysql;

        String tblRaw = sanitizeIdent(opts.table);
        String tbl = quoteIdent(tblRaw.isEmpty() ? "tbl" : tblRaw, identMysql, opts.quoteIdentifiers);
        List<String> quotedCols = new ArrayList<>();
        for (String c : cols) quotedCols.add(quoteIdent(c, identMysql, opts.quoteIdentifiers));
        String colList = String.join(", ", quotedCols);

        if ("copy".equals(opts.format) || "load-data".equals(opts.format)) {
            List<String> payload = new ArrayList<>();
            List<String> header = new ArrayList<>();
            for (String c : cols) header.add(csvEscape(c));
            payload.add(String.join(",", header));
            for (String[] r : data) {
                List<String> fields = new ArrayList<>();
                for (int ci = 0; ci < cols.length; ci++) fields.add(csvEscape(cellAt(r, ci)));
                payload.add(String.join(",", fields));
            }
            String body = String.join("\n", payload);
            if ("copy".equals(opts.format)) {
                res.ok = true;
                res.rows = data.size();
                res.sql = "COPY " + tbl + " (" + colList
                        + ") FROM STDIN WITH (FORMAT csv, HEADER true);\n" + body + "\n\\.";
            } else {
                StringBuilder clean = new StringBuilder();
                for (char c : opts.fileName.toCharArray()) {
                    boolean ok = (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z')
                              || (c >= '0' && c <= '9') || c == '.' || c == '_' || c == '-' || c == '/';
                    if (ok) clean.append(c);
                }
                String file = clean.length() == 0 ? "import.csv" : clean.toString();
                res.ok = true;
                res.rows = data.size();
                res.sql = "LOAD DATA LOCAL INFILE '" + file + "'\nINTO TABLE " + tbl + "\n"
                        + "FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '\"'\n"
                        + "LINES TERMINATED BY '\\n'\nIGNORE 1 LINES;\n\n" + body;
            }
            return res;
        }

        int size = Math.max(1, opts.batchSize);
        List<String> stmts = new ArrayList<>();
        for (int start = 0; start < data.size(); start += size) {
            List<String> values = new ArrayList<>();
            int end = Math.min(data.size(), start + size);
            for (int r = start; r < end; r++) {
                List<String> vals = new ArrayList<>();
                for (int ci = 0; ci < cols.length; ci++) {
                    vals.add(fieldLiteral(cellAt(data.get(r), ci), opts.dialect, opts.inferTypes));
                }
                values.add("  (" + String.join(", ", vals) + ")");
            }
            stmts.add("INSERT INTO " + tbl + " (" + colList + ") VALUES\n"
                    + String.join(",\n", values) + ";");
        }
        res.ok = true;
        res.rows = data.size();
        res.sql = String.join("\n", stmts);
        return res;
    }

    // Example:
    //   Options o = new Options();
    //   o.table = "users";
    //   Result r = CsvToSql.convert("id,name\n1,Ada\n2,", o);
    //   System.out.println(r.sql);
    //   // 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 →