JSON to SQL INSERT — Swift source
Convert a JSON array of objects into SQL INSERT statements. Properly escapes strings, handles nulls, booleans, numbers, nested objects, and multi-row inserts.
This is the Swift implementation — the same logic the interactive tool runs, in a shareable, citable form.
// json-to-sql — pure JSON → SQL INSERT converter. Swift port (canonical TS:
// src/lib/jsonToSql.ts; Go twin: cli/json-to-sql). JSONSerialization loses key
// order, so — like the Rust port — this carries a small hand-rolled JSON DOM
// parser that keeps first-seen order (number tokens stay raw text, so literals
// match the TS reference). Errors return through Res — never thrown.
import Foundation
struct JErr: Error {}
struct Res { let ok: Bool; let sql: String; let rows: Int; let error: String? }
indirect enum JVal { case nul, bool(Bool), num(String), str(String), arr([JVal]), obj([(String, JVal)]) }
struct JParser {
let b: [UInt8]; var i = 0
init(_ s: String) { b = Array(s.utf8) }
var c: UInt8? { i < b.count ? b[i] : nil }
mutating func ws() { while i < b.count, [32, 9, 10, 13].contains(b[i]) { i += 1 } }
mutating func value() throws -> JVal {
ws(); guard let ch = c else { throw JErr() }
if ch == 0x7B { // { — insertion-ordered (key, value) pairs
i += 1; var out: [(String, JVal)] = []; ws()
if c == 0x7D { i += 1; return .obj(out) }
while true {
ws(); let k = try string(); ws()
guard c == 0x3A else { throw JErr() }; i += 1
out.append((k, try value())); ws()
guard let d = c, d == 0x2C || d == 0x7D else { throw JErr() }; i += 1
if d == 0x7D { return .obj(out) }
}
}
if ch == 0x5B { // [ — array
i += 1; var out: [JVal] = []; ws()
if c == 0x5D { i += 1; return .arr(out) }
while true {
out.append(try value()); ws()
guard let d = c, d == 0x2C || d == 0x5D else { throw JErr() }; i += 1
if d == 0x5D { return .arr(out) }
}
}
if ch == 0x22 { return .str(try string()) }
if ch == 0x6E { i += 4; return .nul } // null
if ch == 0x74 { i += 4; return .bool(true) } // true
if ch == 0x66 { i += 5; return .bool(false) }// false
return .num(try number())
}
mutating func string() throws -> String {
guard c == 0x22 else { throw JErr() }; i += 1
var out: [UInt8] = []
while let ch = c {
i += 1
if ch == 0x22 { return String(decoding: out, as: UTF8.self) }
if ch == 0x5C { // escape
guard let e = c else { throw JErr() }; i += 1
switch e {
case 0x22: out.append(0x22); case 0x5C: out.append(0x5C); case 0x2F: out.append(0x2F)
case 0x62: out.append(0x08); case 0x66: out.append(0x0C); case 0x6E: out.append(0x0A)
case 0x72: out.append(0x0D); case 0x74: out.append(0x09)
case 0x75: // \uXXXX — no surrogate pairing (demo scope)
var cp = 0
for _ in 0..<4 {
guard let h = c else { throw JErr() }; i += 1
let v = h >= 0x30 && h <= 0x39 ? h - 0x30 : (h | 32) >= 0x61 && (h | 32) <= 0x66 ? (h | 32) - 0x57 : 0xFF
if v == 0xFF { throw JErr() }; cp = cp * 16 + Int(v)
}
out.append(contentsOf: String(UnicodeScalar(cp)!).utf8)
default: throw JErr() }
} else if ch < 0x20 { throw JErr() } else { out.append(ch) }
}
throw JErr() // unterminated
}
mutating func number() throws -> String { // raw token, String(n)-safe
let start = i
while let ch = c, "-+.eE0123456789".utf8.contains(ch) { i += 1 }
guard i > start else { throw JErr() }
return String(decoding: b[start..<i], as: UTF8.self)
}
}
func escapeSqlString(_ s: String, _ mysql: Bool) -> String {
var o = s.replacingOccurrences(of: "'", with: "''")
if mysql { o = o.replacingOccurrences(of: "\\", with: "\\\\").replacingOccurrences(of: "\u{0}", with: "\\0")
.replacingOccurrences(of: "\n", with: "\\n").replacingOccurrences(of: "\r", with: "\\r")
.replacingOccurrences(of: "\u{1a}", with: "\\Z") } // SUB — ends MySQL input
return o
}
func sqlLiteral(_ v: JVal, _ mysql: Bool) -> String {
switch v {
case .nul: return "NULL"
case .bool(let b): return mysql ? (b ? "1" : "0") : (b ? "TRUE" : "FALSE")
case .num(let raw): return raw
case .str(let s): return "'\(escapeSqlString(s, mysql))'"
case .arr, .obj: return "'\(escapeSqlString(compact(v), mysql))'" } // JSON text
}
func compact(_ v: JVal) -> String { // JSON.stringify-style re-emission
switch v {
case .nul: return "null"; case .bool(let b): return b ? "true" : "false"; case .num(let raw): return raw
case .str(let s):
return "\"" + s.replacingOccurrences(of: "\\", with: "\\\\").replacingOccurrences(of: "\"", with: "\\\"")
.replacingOccurrences(of: "\n", with: "\\n").replacingOccurrences(of: "\r", with: "\\r")
.replacingOccurrences(of: "\t", with: "\\t") + "\""
case .arr(let a): return "[" + a.map(compact).joined(separator: ",") + "]"
case .obj(let o): return "{" + o.map { compact(.str($0.0)) + ":" + compact($0.1) }.joined(separator: ",") + "}" }
}
func quoteIdent(_ n: String, _ mysql: Bool, _ quote: Bool) -> String { guard quote else { return n }; return mysql ? "`\(n)`" : "\"\(n)\"" }
func sanitizeIdent(_ n: String) -> String { // [^A-Za-z0-9_] -> "_", default "tbl"
let cleaned = String(n.map { ($0.isASCII && $0.isLetter) || ($0.isASCII && $0.isNumber) || $0 == "_" ? $0 : "_" })
return cleaned.isEmpty ? "tbl" : cleaned
}
// Convert a JSON document into a single multi-row INSERT. A bare object is one
// row; an array is many. Heterogeneous rows unify via first-seen columns.
func jsonToInsert(_ json: String, table: String, mysql: Bool = false, quote: Bool = true) -> Res {
guard var p = try? JParser(json), let root = try? p.value() else { return Res(ok: false, sql: "", rows: 0, error: "Invalid JSON") }
var rows: [JVal] = []
if case .arr(let a) = root { rows = a } else { rows = [root] }
if rows.isEmpty { return Res(ok: false, sql: "", rows: 0, error: "No rows to insert.") }
guard rows.allSatisfy({ if case .obj = $0 { return true }; return false }) else { return Res(ok: false, sql: "", rows: 0, error: "Rows must be objects.") }
var cols: [String] = [] // first-seen union of every row's keys
for case .obj(let pairs) in rows { for (k, _) in pairs where !cols.contains(k) { cols.append(k) } }
let colList = cols.map { quoteIdent($0, mysql, quote) }.joined(separator: ", ")
let tbl = quoteIdent(sanitizeIdent(table), mysql, quote)
let lines = rows.map { row -> String in
guard case .obj(let pairs) = row else { return "" } // unreachable: verified above
let m = Dictionary(pairs, uniquingKeysWith: { first, _ in first })
return " (" + cols.map { m[$0].map { sqlLiteral($0, mysql) } ?? "NULL" }.joined(separator: ", ") + ")" }
return Res(ok: true, sql: "INSERT INTO \(tbl) (\(colList)) VALUES\n" + lines.joined(separator: ",\n") + ";", rows: rows.count, error: nil)
}
print(jsonToInsert("[{\"id\":1,\"name\":\"O'Hara\"},{\"id\":2,\"age\":30.5,\"tags\":[\"a\",\"b\"]}]", table: "users").sql)
// INSERT INTO "users" ("id", "name", "age", "tags") VALUES ...
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 →