[{"data":1,"prerenderedAt":2118},["ShallowReactive",2],{"doc:\u002Fadvanced-data-transformation-and-cleaning\u002Fmoving-data-between-excel-and-databases\u002Fgenerate-sql-insert-statements-from-an-excel-sheet":3,"surround:\u002Fadvanced-data-transformation-and-cleaning\u002Fmoving-data-between-excel-and-databases\u002Fgenerate-sql-insert-statements-from-an-excel-sheet":2085,"ancestors:\u002Fadvanced-data-transformation-and-cleaning\u002Fmoving-data-between-excel-and-databases\u002Fgenerate-sql-insert-statements-from-an-excel-sheet":2093,"children:\u002Fadvanced-data-transformation-and-cleaning\u002Fmoving-data-between-excel-and-databases\u002Fgenerate-sql-insert-statements-from-an-excel-sheet":2098,"related:\u002Fadvanced-data-transformation-and-cleaning\u002Fmoving-data-between-excel-and-databases\u002Fgenerate-sql-insert-statements-from-an-excel-sheet":2099},{"id":4,"title":5,"body":6,"dateModified":2052,"datePublished":2052,"description":2053,"extension":2054,"faq":2055,"keyword":2064,"kind":2065,"meta":2066,"navigation":131,"path":2074,"seo":2075,"slug":2078,"stem":2079,"topics":2080,"type":2083,"__hash__":2084},"docs\u002Fadvanced-data-transformation-and-cleaning\u002Fmoving-data-between-excel-and-databases\u002Fgenerate-sql-insert-statements-from-an-excel-sheet\u002Findex.md","Generate SQL INSERT Statements from an Excel Sheet",{"type":7,"value":8,"toc":2040},"minimark",[9,37,42,70,77,81,607,653,832,836,1194,1214,1218,1434,1457,1593,1597,1625,1689,1693,1857,1879,1883,1939,1943,1960,1964,1977,1988,1997,2003,2007,2036],[10,11,12,13,17,18,22,23,26,27,30,31,36],"p",{},"Sometimes the right way to get spreadsheet data into a database is not to connect to it at all. Production databases are often locked down: changes go through a change request, a database administrator reviews a script, and the script is run by someone with the access. What the business user has is an Excel sheet — a list of new product codes, a price change, a set of reference values — and what the DBA wants is a ",[14,15,16],"code",{},".sql"," file they can read, test and run. Writing that file by hand, or with a spreadsheet formula that concatenates strings, breaks on the first product called ",[19,20,21],"em",{},"O'Brien's Ale"," or the first blank cell. This page generates the script from the sheet with pandas, with each value turned into a correct SQL literal: text quoted with embedded quotes escaped, blanks as ",[14,24,25],{},"NULL",", dates as ISO literals, numbers unquoted. Rows are batched into multi-row ",[14,28,29],{},"INSERT"," statements inside a single transaction, an upsert variant is produced for PostgreSQL, and the script is proved by running it against a scratch SQLite database and comparing every value with the sheet. It belongs to ",[32,33,35],"a",{"href":34},"\u002Fadvanced-data-transformation-and-cleaning\u002Fmoving-data-between-excel-and-databases\u002F","Moving Data Between Excel and Databases",".",[38,39,41],"h2",{"id":40},"prerequisites","Prerequisites",[43,44,49],"pre",{"className":45,"code":46,"language":47,"meta":48,"style":48},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install pandas openpyxl\n","bash","",[14,50,51],{"__ignoreMap":48},[52,53,56,60,64,67],"span",{"class":54,"line":55},"line",1,[52,57,59],{"class":58},"sMTad","pip",[52,61,63],{"class":62},"srMev"," install",[52,65,66],{"class":62}," pandas",[52,68,69],{"class":62}," openpyxl\n",[10,71,72,73,76],{},"The sheet holds five new products. It has been chosen to contain the values that break naive scripts: a name with apostrophes, a name with a semicolon, a name containing ",[14,74,75],{},"--"," (the start of an SQL comment), a missing launch date, a missing price and a true\u002Ffalse column.",[38,78,80],{"id":79},"step-1-one-function-for-every-sql-literal","Step 1 — One function for every SQL literal",[43,82,86],{"className":83,"code":84,"language":85,"meta":48,"style":48},"language-python shiki shiki-themes github-light github-dark-high-contrast","import math\nfrom datetime import date, datetime\nimport pandas as pd\n\ndef sql_literal(value):\n    \"\"\"Return the SQL text for one value: quoted strings, NULL, ISO dates, numbers, booleans.\"\"\"\n    if value is None or (isinstance(value, float) and math.isnan(value)) or value is pd.NaT:\n        return \"NULL\"\n    if isinstance(value, bool):\n        return \"TRUE\" if value else \"FALSE\"\n    if isinstance(value, (int, float)):\n        return repr(value) if isinstance(value, int) else format(value, \".15g\")\n    if isinstance(value, (pd.Timestamp, datetime)):\n        return f\"'{value:%Y-%m-%d %H:%M:%S}'\" if (value.hour or value.minute or value.second) else f\"'{value:%Y-%m-%d}'\"\n    if isinstance(value, date):\n        return f\"'{value:%Y-%m-%d}'\"\n    text = str(value).replace(\"'\", \"''\")                  # the only escape standard SQL needs\n    return f\"'{text}'\"\n\nfor v in [\"O'Brien's Ale\", \"Gin -- small batch\", None, float(\"nan\"), 3.45, True, pd.Timestamp(\"2026-10-01\")]:\n    print(f\"{v!r:28} -> {sql_literal(v)}\")\n","python",[14,87,88,98,112,126,133,146,152,202,211,227,246,267,302,312,418,428,457,487,506,511,569],{"__ignoreMap":48},[52,89,90,94],{"class":54,"line":55},[52,91,93],{"class":92},"s-kum","import",[52,95,97],{"class":96},"skGVy"," math\n",[52,99,101,104,107,109],{"class":54,"line":100},2,[52,102,103],{"class":92},"from",[52,105,106],{"class":96}," datetime ",[52,108,93],{"class":92},[52,110,111],{"class":96}," date, datetime\n",[52,113,115,117,120,123],{"class":54,"line":114},3,[52,116,93],{"class":92},[52,118,119],{"class":96}," pandas ",[52,121,122],{"class":92},"as",[52,124,125],{"class":96}," pd\n",[52,127,129],{"class":54,"line":128},4,[52,130,132],{"emptyLinePlaceholder":131},true,"\n",[52,134,136,139,143],{"class":54,"line":135},5,[52,137,138],{"class":92},"def",[52,140,142],{"class":141},"s_Opv"," sql_literal",[52,144,145],{"class":96},"(value):\n",[52,147,149],{"class":54,"line":148},6,[52,150,151],{"class":62},"    \"\"\"Return the SQL text for one value: quoted strings, NULL, ISO dates, numbers, booleans.\"\"\"\n",[52,153,155,158,161,164,168,171,174,177,180,183,186,189,192,195,197,199],{"class":54,"line":154},7,[52,156,157],{"class":92},"    if",[52,159,160],{"class":96}," value ",[52,162,163],{"class":92},"is",[52,165,167],{"class":166},"sP0c6"," None",[52,169,170],{"class":92}," or",[52,172,173],{"class":96}," (",[52,175,176],{"class":166},"isinstance",[52,178,179],{"class":96},"(value, ",[52,181,182],{"class":166},"float",[52,184,185],{"class":96},") ",[52,187,188],{"class":92},"and",[52,190,191],{"class":96}," math.isnan(value)) ",[52,193,194],{"class":92},"or",[52,196,160],{"class":96},[52,198,163],{"class":92},[52,200,201],{"class":96}," pd.NaT:\n",[52,203,205,208],{"class":54,"line":204},8,[52,206,207],{"class":92},"        return",[52,209,210],{"class":62}," \"NULL\"\n",[52,212,214,216,219,221,224],{"class":54,"line":213},9,[52,215,157],{"class":92},[52,217,218],{"class":166}," isinstance",[52,220,179],{"class":96},[52,222,223],{"class":166},"bool",[52,225,226],{"class":96},"):\n",[52,228,230,232,235,238,240,243],{"class":54,"line":229},10,[52,231,207],{"class":92},[52,233,234],{"class":62}," \"TRUE\"",[52,236,237],{"class":92}," if",[52,239,160],{"class":96},[52,241,242],{"class":92},"else",[52,244,245],{"class":62}," \"FALSE\"\n",[52,247,249,251,253,256,259,262,264],{"class":54,"line":248},11,[52,250,157],{"class":92},[52,252,218],{"class":166},[52,254,255],{"class":96},"(value, (",[52,257,258],{"class":166},"int",[52,260,261],{"class":96},", ",[52,263,182],{"class":166},[52,265,266],{"class":96},")):\n",[52,268,270,272,275,278,281,283,285,287,289,291,294,296,299],{"class":54,"line":269},12,[52,271,207],{"class":92},[52,273,274],{"class":166}," repr",[52,276,277],{"class":96},"(value) ",[52,279,280],{"class":92},"if",[52,282,218],{"class":166},[52,284,179],{"class":96},[52,286,258],{"class":166},[52,288,185],{"class":96},[52,290,242],{"class":92},[52,292,293],{"class":166}," format",[52,295,179],{"class":96},[52,297,298],{"class":62},"\".15g\"",[52,300,301],{"class":96},")\n",[52,303,305,307,309],{"class":54,"line":304},13,[52,306,157],{"class":92},[52,308,218],{"class":166},[52,310,311],{"class":96},"(value, (pd.Timestamp, datetime)):\n",[52,313,315,317,320,323,327,330,333,336,339,341,344,346,349,351,354,357,360,362,365,368,371,373,376,378,381,383,386,388,390,392,394,397,399,402,405,408,410,413,415],{"class":54,"line":314},14,[52,316,207],{"class":92},[52,318,319],{"class":92}," f",[52,321,322],{"class":62},"\"'",[52,324,326],{"class":325},"sSjpA","{",[52,328,329],{"class":96},"value",[52,331,332],{"class":92},":",[52,334,335],{"class":96},"%",[52,337,338],{"class":92},"Y-",[52,340,335],{"class":96},[52,342,343],{"class":92},"m-",[52,345,335],{"class":96},[52,347,348],{"class":92},"d ",[52,350,335],{"class":96},[52,352,353],{"class":92},"H",[52,355,356],{"class":96},":%",[52,358,359],{"class":92},"M",[52,361,356],{"class":96},[52,363,364],{"class":92},"S",[52,366,367],{"class":325},"}",[52,369,370],{"class":62},"'\"",[52,372,237],{"class":92},[52,374,375],{"class":96}," (value.hour ",[52,377,194],{"class":92},[52,379,380],{"class":96}," value.minute ",[52,382,194],{"class":92},[52,384,385],{"class":96}," value.second) ",[52,387,242],{"class":92},[52,389,319],{"class":92},[52,391,322],{"class":62},[52,393,326],{"class":325},[52,395,396],{"class":96},"value:",[52,398,335],{"class":92},[52,400,401],{"class":96},"Y",[52,403,404],{"class":92},"-%",[52,406,407],{"class":96},"m",[52,409,404],{"class":92},[52,411,412],{"class":96},"d",[52,414,367],{"class":325},[52,416,417],{"class":62},"'\"\n",[52,419,421,423,425],{"class":54,"line":420},15,[52,422,157],{"class":92},[52,424,218],{"class":166},[52,426,427],{"class":96},"(value, date):\n",[52,429,431,433,435,437,439,441,443,445,447,449,451,453,455],{"class":54,"line":430},16,[52,432,207],{"class":92},[52,434,319],{"class":92},[52,436,322],{"class":62},[52,438,326],{"class":325},[52,440,396],{"class":96},[52,442,335],{"class":92},[52,444,401],{"class":96},[52,446,404],{"class":92},[52,448,407],{"class":96},[52,450,404],{"class":92},[52,452,412],{"class":96},[52,454,367],{"class":325},[52,456,417],{"class":62},[52,458,460,463,466,469,472,475,477,480,483],{"class":54,"line":459},17,[52,461,462],{"class":96},"    text ",[52,464,465],{"class":92},"=",[52,467,468],{"class":166}," str",[52,470,471],{"class":96},"(value).replace(",[52,473,474],{"class":62},"\"'\"",[52,476,261],{"class":96},[52,478,479],{"class":62},"\"''\"",[52,481,482],{"class":96},")                  ",[52,484,486],{"class":485},"s-wDw","# the only escape standard SQL needs\n",[52,488,490,493,495,497,499,502,504],{"class":54,"line":489},18,[52,491,492],{"class":92},"    return",[52,494,319],{"class":92},[52,496,322],{"class":62},[52,498,326],{"class":325},[52,500,501],{"class":96},"text",[52,503,367],{"class":325},[52,505,417],{"class":62},[52,507,509],{"class":54,"line":508},19,[52,510,132],{"emptyLinePlaceholder":131},[52,512,514,517,520,523,526,529,531,534,536,539,541,543,546,549,552,555,557,560,563,566],{"class":54,"line":513},20,[52,515,516],{"class":92},"for",[52,518,519],{"class":96}," v ",[52,521,522],{"class":92},"in",[52,524,525],{"class":96}," [",[52,527,528],{"class":62},"\"O'Brien's Ale\"",[52,530,261],{"class":96},[52,532,533],{"class":62},"\"Gin -- small batch\"",[52,535,261],{"class":96},[52,537,538],{"class":166},"None",[52,540,261],{"class":96},[52,542,182],{"class":166},[52,544,545],{"class":96},"(",[52,547,548],{"class":62},"\"nan\"",[52,550,551],{"class":96},"), ",[52,553,554],{"class":166},"3.45",[52,556,261],{"class":96},[52,558,559],{"class":166},"True",[52,561,562],{"class":96},", pd.Timestamp(",[52,564,565],{"class":62},"\"2026-10-01\"",[52,567,568],{"class":96},")]:\n",[52,570,572,575,577,580,583,585,588,591,593,596,598,601,603,605],{"class":54,"line":571},21,[52,573,574],{"class":166},"    print",[52,576,545],{"class":96},[52,578,579],{"class":92},"f",[52,581,582],{"class":62},"\"",[52,584,326],{"class":325},[52,586,587],{"class":96},"v",[52,589,590],{"class":92},"!r:28",[52,592,367],{"class":325},[52,594,595],{"class":62}," -> ",[52,597,326],{"class":325},[52,599,600],{"class":96},"sql_literal(v)",[52,602,367],{"class":325},[52,604,582],{"class":62},[52,606,301],{"class":96},[10,608,609,610,261,612,615,616,619,620,622,623,626,627,630,631,634,635,638,639,642,643,646,647,630,650,652],{},"Every value in the sheet passes through one function that knows how to write it as SQL. Blanks — ",[14,611,538],{},[14,613,614],{},"NaN"," or pandas' missing timestamp ",[14,617,618],{},"NaT"," — become ",[14,621,25],{},", never an empty string or the text ",[19,624,625],{},"nan",". Booleans become ",[14,628,629],{},"TRUE"," and ",[14,632,633],{},"FALSE",". Numbers are written without quotes and with enough digits to round-trip exactly. Dates become ISO literals such as ",[14,636,637],{},"'2026-10-01'",", which every major database reads unambiguously, with the time added only when there is one. Text is wrapped in single quotes with every embedded single quote doubled: ",[19,640,641],{},"O'Brien's"," becomes ",[14,644,645],{},"'O''Brien''s'",". That doubling is the whole of the escaping that standard SQL requires, and once a value is properly quoted, characters such as ",[14,648,649],{},";",[14,651,75],{}," inside it are just characters — they cannot end the statement or start a comment.",[654,655,663,664,663,668,663,672,663,680,663,689,663,693,663,699,663,703,663,707,663,711,663,716,663,721,663,725,663,727,663,730,663,733,663,737,663,739,663,742,663,746,663,750,663,753,663,757,663,759,663,762,663,764,663,767,663,770,663,774,663,776,663,779,663,783,663,786,663,789,663,793,663,795,663,797,663,799,663,801,663,804,663,808,663,810,663,812,663,814,663,816,663,819,663,823,663,825,663,828,663,830],"svg",{"viewBox":656,"role":657,"ariaLabelledBy":658,"xmlns":661,"style":662},"210 0 340 252","img",[659,660],"sqlgen-literals-t","sqlgen-literals-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:340px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[665,666,667],"title",{"id":659},"Values and the SQL written for them",[669,670,671],"desc",{"id":660},"A name with apostrophes has each apostrophe doubled inside single quotes. Text containing two hyphens stays inside quotes, so it is not a comment. Blank and NaN values become NULL, 3.45 is written unquoted, True becomes TRUE and a date becomes an ISO literal.",[673,674],"rect",{"x":675,"y":676,"width":677,"height":678,"fill":679},"210","0","340","252","#ffffff",[673,681],{"x":682,"y":683,"width":684,"height":685,"rx":676,"fill":686,"stroke":687,"style":688},"226.4","16.0","32.0","24.0","#e7ebef","var(--line,#cdd5e6)","stroke-width:1px",[673,690],{"x":691,"y":683,"width":692,"height":685,"rx":676,"fill":686,"stroke":687,"style":688},"258.4","133.3",[501,694,698],{"x":695,"y":696,"style":697},"325.1","33.0","font-size:11px;font-weight:600;fill:var(--muted,#5b6780);text-anchor:middle","A",[673,700],{"x":701,"y":683,"width":702,"height":685,"rx":676,"fill":686,"stroke":687,"style":688},"391.7","141.8",[501,704,706],{"x":705,"y":696,"style":697},"462.7","B",[673,708],{"x":682,"y":709,"width":684,"height":710,"rx":676,"fill":686,"stroke":687,"style":688},"40.0","28.0",[501,712,715],{"x":713,"y":714,"style":697},"242.4","59.0","1",[673,717],{"x":691,"y":709,"width":692,"height":710,"rx":676,"fill":718,"stroke":719,"style":720},"#f0f4ff","var(--brand,#5b5cf0)","stroke-width:1.5px",[501,722,724],{"x":695,"y":714,"style":723},"font-size:12px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Excel value",[673,726],{"x":701,"y":709,"width":702,"height":710,"rx":676,"fill":718,"stroke":719,"style":720},[501,728,729],{"x":705,"y":714,"style":723},"SQL literal",[673,731],{"x":682,"y":732,"width":684,"height":710,"rx":676,"fill":686,"stroke":687,"style":688},"68.0",[501,734,736],{"x":713,"y":735,"style":697},"87.0","2",[673,738],{"x":691,"y":732,"width":692,"height":710,"rx":676,"fill":679,"stroke":687,"style":688},[501,740,21],{"x":695,"y":735,"style":741},"font-size:12px;font-weight:400;fill:var(--text,#172033);text-anchor:middle",[673,743],{"x":701,"y":732,"width":702,"height":710,"rx":676,"fill":744,"stroke":745,"style":720},"#d9f4f1","var(--teal,#0f9488)",[501,747,749],{"x":705,"y":735,"style":748},"font-size:12px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","'O''Brien''s Ale'",[673,751],{"x":682,"y":752,"width":684,"height":710,"rx":676,"fill":686,"stroke":687,"style":688},"96.0",[501,754,756],{"x":713,"y":755,"style":697},"115.0","3",[673,758],{"x":691,"y":752,"width":692,"height":710,"rx":676,"fill":679,"stroke":687,"style":688},[501,760,761],{"x":695,"y":755,"style":741},"Gin -- small batch",[673,763],{"x":701,"y":752,"width":702,"height":710,"rx":676,"fill":679,"stroke":687,"style":688},[501,765,766],{"x":705,"y":755,"style":741},"'Gin -- small batch'",[673,768],{"x":682,"y":769,"width":684,"height":710,"rx":676,"fill":686,"stroke":687,"style":688},"124.0",[501,771,773],{"x":713,"y":772,"style":697},"143.0","4",[673,775],{"x":691,"y":769,"width":692,"height":710,"rx":676,"fill":679,"stroke":687,"style":688},[501,777,778],{"x":695,"y":772,"style":741},"(blank)",[673,780],{"x":701,"y":769,"width":702,"height":710,"rx":676,"fill":781,"stroke":782,"style":720},"#fdefd8","var(--gold,#b4740a)",[501,784,25],{"x":705,"y":772,"style":785},"font-size:12px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle",[673,787],{"x":682,"y":788,"width":684,"height":710,"rx":676,"fill":686,"stroke":687,"style":688},"152.0",[501,790,792],{"x":713,"y":791,"style":697},"171.0","5",[673,794],{"x":691,"y":788,"width":692,"height":710,"rx":676,"fill":679,"stroke":687,"style":688},[501,796,554],{"x":695,"y":791,"style":741},[673,798],{"x":701,"y":788,"width":702,"height":710,"rx":676,"fill":679,"stroke":687,"style":688},[501,800,554],{"x":705,"y":791,"style":741},[673,802],{"x":682,"y":803,"width":684,"height":710,"rx":676,"fill":686,"stroke":687,"style":688},"180.0",[501,805,807],{"x":713,"y":806,"style":697},"199.0","6",[673,809],{"x":691,"y":803,"width":692,"height":710,"rx":676,"fill":679,"stroke":687,"style":688},[501,811,629],{"x":695,"y":806,"style":741},[673,813],{"x":701,"y":803,"width":702,"height":710,"rx":676,"fill":679,"stroke":687,"style":688},[501,815,629],{"x":705,"y":806,"style":741},[673,817],{"x":682,"y":818,"width":684,"height":710,"rx":676,"fill":686,"stroke":687,"style":688},"208.0",[501,820,822],{"x":713,"y":821,"style":697},"227.0","7",[673,824],{"x":691,"y":818,"width":692,"height":710,"rx":676,"fill":679,"stroke":687,"style":688},[501,826,827],{"x":695,"y":821,"style":741},"2026-10-01",[673,829],{"x":701,"y":818,"width":702,"height":710,"rx":676,"fill":679,"stroke":687,"style":688},[501,831,637],{"x":705,"y":821,"style":741},[38,833,835],{"id":834},"step-2-batched-inserts-in-one-transaction","Step 2 — Batched INSERTs in one transaction",[43,837,839],{"className":83,"code":838,"language":85,"meta":48,"style":48},"df = pd.read_excel(\"new_products.xlsx\")\nTABLE = \"products\"\nCOLUMNS = list(df.columns)\n\ndef insert_script(df, table, batch=500):\n    head = f\"INSERT INTO {table} ({', '.join(COLUMNS)}) VALUES\"\n    lines = [\"-- generated from new_products.xlsx\", f\"-- {len(df)} rows\", \"BEGIN;\"]\n    rows = [f\"  ({', '.join(sql_literal(v) for v in rec)})\" for rec in df.astype(object).itertuples(index=False)]\n    for i in range(0, len(rows), batch):\n        lines.append(head)\n        lines.append(\",\\n\".join(rows[i:i + batch]) + \";\")\n    lines.append(\"COMMIT;\")\n    return \"\\n\".join(lines) + \"\\n\"\n\nscript = insert_script(df, TABLE)\nfrom pathlib import Path\nPath(\"insert_products.sql\").write_text(script, encoding=\"utf-8\")\nprint(script)\n",[14,840,841,856,867,880,884,901,940,980,1044,1068,1073,1102,1112,1135,1139,1153,1165,1186],{"__ignoreMap":48},[52,842,843,846,848,851,854],{"class":54,"line":55},[52,844,845],{"class":96},"df ",[52,847,465],{"class":92},[52,849,850],{"class":96}," pd.read_excel(",[52,852,853],{"class":62},"\"new_products.xlsx\"",[52,855,301],{"class":96},[52,857,858,861,864],{"class":54,"line":100},[52,859,860],{"class":166},"TABLE",[52,862,863],{"class":92}," =",[52,865,866],{"class":62}," \"products\"\n",[52,868,869,872,874,877],{"class":54,"line":114},[52,870,871],{"class":166},"COLUMNS",[52,873,863],{"class":92},[52,875,876],{"class":166}," list",[52,878,879],{"class":96},"(df.columns)\n",[52,881,882],{"class":54,"line":128},[52,883,132],{"emptyLinePlaceholder":131},[52,885,886,888,891,894,896,899],{"class":54,"line":135},[52,887,138],{"class":92},[52,889,890],{"class":141}," insert_script",[52,892,893],{"class":96},"(df, table, batch",[52,895,465],{"class":92},[52,897,898],{"class":166},"500",[52,900,226],{"class":96},[52,902,903,906,908,910,913,915,918,920,922,924,927,930,932,935,937],{"class":54,"line":148},[52,904,905],{"class":96},"    head ",[52,907,465],{"class":92},[52,909,319],{"class":92},[52,911,912],{"class":62},"\"INSERT INTO ",[52,914,326],{"class":325},[52,916,917],{"class":96},"table",[52,919,367],{"class":325},[52,921,173],{"class":62},[52,923,326],{"class":325},[52,925,926],{"class":62},"', '",[52,928,929],{"class":96},".join(",[52,931,871],{"class":166},[52,933,934],{"class":96},")",[52,936,367],{"class":325},[52,938,939],{"class":62},") VALUES\"\n",[52,941,942,945,947,949,952,954,956,959,961,964,967,969,972,974,977],{"class":54,"line":154},[52,943,944],{"class":96},"    lines ",[52,946,465],{"class":92},[52,948,525],{"class":96},[52,950,951],{"class":62},"\"-- generated from new_products.xlsx\"",[52,953,261],{"class":96},[52,955,579],{"class":92},[52,957,958],{"class":62},"\"-- ",[52,960,326],{"class":325},[52,962,963],{"class":166},"len",[52,965,966],{"class":96},"(df)",[52,968,367],{"class":325},[52,970,971],{"class":62}," rows\"",[52,973,261],{"class":96},[52,975,976],{"class":62},"\"BEGIN;\"",[52,978,979],{"class":96},"]\n",[52,981,982,985,987,989,991,994,996,998,1001,1003,1005,1007,1010,1012,1015,1018,1021,1023,1026,1029,1032,1036,1038,1041],{"class":54,"line":204},[52,983,984],{"class":96},"    rows ",[52,986,465],{"class":92},[52,988,525],{"class":96},[52,990,579],{"class":92},[52,992,993],{"class":62},"\"  (",[52,995,326],{"class":325},[52,997,926],{"class":62},[52,999,1000],{"class":96},".join(sql_literal(v) ",[52,1002,516],{"class":92},[52,1004,519],{"class":96},[52,1006,522],{"class":92},[52,1008,1009],{"class":96}," rec)",[52,1011,367],{"class":325},[52,1013,1014],{"class":62},")\"",[52,1016,1017],{"class":92}," for",[52,1019,1020],{"class":96}," rec ",[52,1022,522],{"class":92},[52,1024,1025],{"class":96}," df.astype(",[52,1027,1028],{"class":166},"object",[52,1030,1031],{"class":96},").itertuples(",[52,1033,1035],{"class":1034},"sa561","index",[52,1037,465],{"class":92},[52,1039,1040],{"class":166},"False",[52,1042,1043],{"class":96},")]\n",[52,1045,1046,1049,1052,1054,1057,1059,1061,1063,1065],{"class":54,"line":213},[52,1047,1048],{"class":92},"    for",[52,1050,1051],{"class":96}," i ",[52,1053,522],{"class":92},[52,1055,1056],{"class":166}," range",[52,1058,545],{"class":96},[52,1060,676],{"class":166},[52,1062,261],{"class":96},[52,1064,963],{"class":166},[52,1066,1067],{"class":96},"(rows), batch):\n",[52,1069,1070],{"class":54,"line":229},[52,1071,1072],{"class":96},"        lines.append(head)\n",[52,1074,1075,1078,1081,1084,1086,1089,1092,1095,1097,1100],{"class":54,"line":248},[52,1076,1077],{"class":96},"        lines.append(",[52,1079,1080],{"class":62},"\",",[52,1082,1083],{"class":325},"\\n",[52,1085,582],{"class":62},[52,1087,1088],{"class":96},".join(rows[i:i ",[52,1090,1091],{"class":92},"+",[52,1093,1094],{"class":96}," batch]) ",[52,1096,1091],{"class":92},[52,1098,1099],{"class":62}," \";\"",[52,1101,301],{"class":96},[52,1103,1104,1107,1110],{"class":54,"line":269},[52,1105,1106],{"class":96},"    lines.append(",[52,1108,1109],{"class":62},"\"COMMIT;\"",[52,1111,301],{"class":96},[52,1113,1114,1116,1119,1121,1123,1126,1128,1130,1132],{"class":54,"line":304},[52,1115,492],{"class":92},[52,1117,1118],{"class":62}," \"",[52,1120,1083],{"class":325},[52,1122,582],{"class":62},[52,1124,1125],{"class":96},".join(lines) ",[52,1127,1091],{"class":92},[52,1129,1118],{"class":62},[52,1131,1083],{"class":325},[52,1133,1134],{"class":62},"\"\n",[52,1136,1137],{"class":54,"line":314},[52,1138,132],{"emptyLinePlaceholder":131},[52,1140,1141,1144,1146,1149,1151],{"class":54,"line":420},[52,1142,1143],{"class":96},"script ",[52,1145,465],{"class":92},[52,1147,1148],{"class":96}," insert_script(df, ",[52,1150,860],{"class":166},[52,1152,301],{"class":96},[52,1154,1155,1157,1160,1162],{"class":54,"line":430},[52,1156,103],{"class":92},[52,1158,1159],{"class":96}," pathlib ",[52,1161,93],{"class":92},[52,1163,1164],{"class":96}," Path\n",[52,1166,1167,1170,1173,1176,1179,1181,1184],{"class":54,"line":459},[52,1168,1169],{"class":96},"Path(",[52,1171,1172],{"class":62},"\"insert_products.sql\"",[52,1174,1175],{"class":96},").write_text(script, ",[52,1177,1178],{"class":1034},"encoding",[52,1180,465],{"class":92},[52,1182,1183],{"class":62},"\"utf-8\"",[52,1185,301],{"class":96},[52,1187,1188,1191],{"class":54,"line":489},[52,1189,1190],{"class":166},"print",[52,1192,1193],{"class":96},"(script)\n",[10,1195,1196,1197,630,1200,1203,1204,1206,1207,1209,1210,1213],{},"The script starts with comments saying where it came from and how many rows it holds, wraps everything in ",[14,1198,1199],{},"BEGIN",[14,1201,1202],{},"COMMIT"," so it either loads completely or not at all, and writes rows as multi-row ",[14,1205,29],{}," statements — one statement per batch of up to 500 rows rather than one per row, which is much faster to run and easier to read. For the five products here the whole script is a dozen lines a reviewer can check by eye. Converting the DataFrame to ",[14,1208,1028],{}," before iterating makes each value a plain Python object, so integers, floats and timestamps reach ",[14,1211,1212],{},"sql_literal"," as themselves. The column list is written explicitly in every statement, so the script still does the right thing if the table's columns are in a different order.",[38,1215,1217],{"id":1216},"step-3-prove-it-by-running-it","Step 3 — Prove it by running it",[43,1219,1221],{"className":83,"code":1220,"language":85,"meta":48,"style":48},"import sqlite3\nfrom contextlib import closing\n\nwith closing(sqlite3.connect(\":memory:\")) as con:\n    con.execute(\"\"\"CREATE TABLE products (product_code TEXT PRIMARY KEY, name TEXT NOT NULL, launch_date TEXT,\n                                          price REAL, abv REAL, is_seasonal INTEGER)\"\"\")\n    con.executescript(script)\n    back = pd.read_sql(\"SELECT * FROM products ORDER BY product_code\", con)\n\nexpected = df.sort_values(\"product_code\").reset_index(drop=True)\nexpected[\"launch_date\"] = expected[\"launch_date\"].dt.strftime(\"%Y-%m-%d\")\nexpected[\"is_seasonal\"] = expected[\"is_seasonal\"].astype(int)\npd.testing.assert_frame_equal(back, expected, check_dtype=False)\nprint(back.to_string(index=False))\nprint(\"round trip OK:\", len(back), \"rows\")\n",[14,1222,1223,1230,1242,1246,1265,1273,1280,1285,1301,1305,1330,1361,1383,1397,1413],{"__ignoreMap":48},[52,1224,1225,1227],{"class":54,"line":55},[52,1226,93],{"class":92},[52,1228,1229],{"class":96}," sqlite3\n",[52,1231,1232,1234,1237,1239],{"class":54,"line":100},[52,1233,103],{"class":92},[52,1235,1236],{"class":96}," contextlib ",[52,1238,93],{"class":92},[52,1240,1241],{"class":96}," closing\n",[52,1243,1244],{"class":54,"line":114},[52,1245,132],{"emptyLinePlaceholder":131},[52,1247,1248,1251,1254,1257,1260,1262],{"class":54,"line":128},[52,1249,1250],{"class":92},"with",[52,1252,1253],{"class":96}," closing(sqlite3.connect(",[52,1255,1256],{"class":62},"\":memory:\"",[52,1258,1259],{"class":96},")) ",[52,1261,122],{"class":92},[52,1263,1264],{"class":96}," con:\n",[52,1266,1267,1270],{"class":54,"line":135},[52,1268,1269],{"class":96},"    con.execute(",[52,1271,1272],{"class":62},"\"\"\"CREATE TABLE products (product_code TEXT PRIMARY KEY, name TEXT NOT NULL, launch_date TEXT,\n",[52,1274,1275,1278],{"class":54,"line":148},[52,1276,1277],{"class":62},"                                          price REAL, abv REAL, is_seasonal INTEGER)\"\"\"",[52,1279,301],{"class":96},[52,1281,1282],{"class":54,"line":154},[52,1283,1284],{"class":96},"    con.executescript(script)\n",[52,1286,1287,1290,1292,1295,1298],{"class":54,"line":204},[52,1288,1289],{"class":96},"    back ",[52,1291,465],{"class":92},[52,1293,1294],{"class":96}," pd.read_sql(",[52,1296,1297],{"class":62},"\"SELECT * FROM products ORDER BY product_code\"",[52,1299,1300],{"class":96},", con)\n",[52,1302,1303],{"class":54,"line":213},[52,1304,132],{"emptyLinePlaceholder":131},[52,1306,1307,1310,1312,1315,1318,1321,1324,1326,1328],{"class":54,"line":229},[52,1308,1309],{"class":96},"expected ",[52,1311,465],{"class":92},[52,1313,1314],{"class":96}," df.sort_values(",[52,1316,1317],{"class":62},"\"product_code\"",[52,1319,1320],{"class":96},").reset_index(",[52,1322,1323],{"class":1034},"drop",[52,1325,465],{"class":92},[52,1327,559],{"class":166},[52,1329,301],{"class":96},[52,1331,1332,1335,1338,1341,1343,1346,1348,1351,1354,1357,1359],{"class":54,"line":248},[52,1333,1334],{"class":96},"expected[",[52,1336,1337],{"class":62},"\"launch_date\"",[52,1339,1340],{"class":96},"] ",[52,1342,465],{"class":92},[52,1344,1345],{"class":96}," expected[",[52,1347,1337],{"class":62},[52,1349,1350],{"class":96},"].dt.strftime(",[52,1352,1353],{"class":62},"\"%Y-%m-",[52,1355,1356],{"class":325},"%d",[52,1358,582],{"class":62},[52,1360,301],{"class":96},[52,1362,1363,1365,1368,1370,1372,1374,1376,1379,1381],{"class":54,"line":269},[52,1364,1334],{"class":96},[52,1366,1367],{"class":62},"\"is_seasonal\"",[52,1369,1340],{"class":96},[52,1371,465],{"class":92},[52,1373,1345],{"class":96},[52,1375,1367],{"class":62},[52,1377,1378],{"class":96},"].astype(",[52,1380,258],{"class":166},[52,1382,301],{"class":96},[52,1384,1385,1388,1391,1393,1395],{"class":54,"line":304},[52,1386,1387],{"class":96},"pd.testing.assert_frame_equal(back, expected, ",[52,1389,1390],{"class":1034},"check_dtype",[52,1392,465],{"class":92},[52,1394,1040],{"class":166},[52,1396,301],{"class":96},[52,1398,1399,1401,1404,1406,1408,1410],{"class":54,"line":314},[52,1400,1190],{"class":166},[52,1402,1403],{"class":96},"(back.to_string(",[52,1405,1035],{"class":1034},[52,1407,465],{"class":92},[52,1409,1040],{"class":166},[52,1411,1412],{"class":96},"))\n",[52,1414,1415,1417,1419,1422,1424,1426,1429,1432],{"class":54,"line":420},[52,1416,1190],{"class":166},[52,1418,545],{"class":96},[52,1420,1421],{"class":62},"\"round trip OK:\"",[52,1423,261],{"class":96},[52,1425,963],{"class":166},[52,1427,1428],{"class":96},"(back), ",[52,1430,1431],{"class":62},"\"rows\"",[52,1433,301],{"class":96},[10,1435,1436,1437,1440,1441,1443,1444,1446,1447,1449,1450,630,1453,1456],{},"A generated script should be tested before it goes to anyone, and the test is to run it. A scratch SQLite database in memory, with a table shaped like the target, executes the script exactly as written, and reading the table back gives values to compare with the sheet. ",[14,1438,1439],{},"assert_frame_equal"," checks every cell: the apostrophes in ",[19,1442,21],{}," survived, the text with ",[14,1445,75],{}," is intact, the missing date and price are ",[14,1448,25],{},", and the numbers match. SQLite has no boolean or date type, so they come back as integers and text, which the comparison allows for; on PostgreSQL the same script loads them into proper ",[14,1451,1452],{},"BOOLEAN",[14,1454,1455],{},"DATE"," columns.",[654,1458,663,1464,663,1467,663,1470,663,1474,663,1481,663,1487,663,1492,663,1496,663,1501,663,1506,663,1509,663,1515,663,1517,663,1521,663,1524,663,1528,663,1531,663,1534,663,1539,663,1542,663,1545,663,1547,663,1551,663,1554,663,1557,663,1562,663,1565,663,1568,663,1571,663,1575,663,1578,663,1581,663,1585,663,1587,663,1590],{"viewBox":1459,"role":657,"ariaLabelledBy":1460,"xmlns":661,"style":1463},"4 0 752 126",[1461,1462],"sqlgen-flow-t","sqlgen-flow-d","width:100%;max-width:752px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[665,1465,1466],{"id":1461},"From sheet to tested script",[669,1468,1469],{"id":1462},"Each Excel value is turned into an SQL literal, rows are grouped into multi-row INSERT statements inside a transaction, and the script is run against a scratch database whose contents are compared with the sheet before the file is handed over.",[673,1471],{"x":773,"y":676,"width":1472,"height":1473,"fill":679},"752","126",[673,1475],{"x":1476,"y":683,"width":1477,"height":1478,"rx":1479,"fill":686,"stroke":687,"style":1480},"20.0","115.2","94.0","12","stroke-width:2px",[501,1482,1486],{"x":1483,"y":1484,"style":1485},"77.6","50.2","font-size:13.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","Excel sheet",[501,1488,1491],{"x":1483,"y":1489,"style":1490},"69.8","font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","read with",[501,1493,1495],{"x":1483,"y":1494,"style":1490},"87.2","pandas",[54,1497],{"x1":1498,"y1":1499,"x2":1500,"y2":1499,"stroke":719,"style":1480},"140.2","63.0","162.2",[1502,1503],"polygon",{"points":1504,"fill":1505},"166.2,63.0 157.2,68.0 157.2,58.0","#5b5cf0",[673,1507],{"x":1508,"y":683,"width":1477,"height":1478,"rx":1479,"fill":718,"stroke":719,"style":1480},"171.2",[501,1510,1514],{"x":1511,"y":1512,"style":1513},"228.8","41.5","font-size:13.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","sql_literal per",[501,1516,329],{"x":1511,"y":714,"style":1513},[501,1518,1520],{"x":1511,"y":1519,"style":1490},"78.5","quotes, NULL,",[501,1522,1523],{"x":1511,"y":752,"style":1490},"dates",[54,1525],{"x1":1526,"y1":1499,"x2":1527,"y2":1499,"stroke":719,"style":1480},"291.4","313.4",[1502,1529],{"points":1530,"fill":1505},"317.4,63.0 308.4,68.0 308.4,58.0",[673,1532],{"x":1533,"y":683,"width":1477,"height":1478,"rx":1479,"fill":781,"stroke":782,"style":1480},"322.4",[501,1535,1538],{"x":1536,"y":1512,"style":1537},"380.0","font-size:13.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","Batched",[501,1540,1541],{"x":1536,"y":714,"style":1537},"INSERTs",[501,1543,1544],{"x":1536,"y":1519,"style":1490},"inside BEGIN …",[501,1546,1202],{"x":1536,"y":752,"style":1490},[54,1548],{"x1":1549,"y1":1499,"x2":1550,"y2":1499,"stroke":719,"style":1480},"442.6","464.6",[1502,1552],{"points":1553,"fill":1505},"468.6,63.0 459.6,68.0 459.6,58.0",[673,1555],{"x":1556,"y":683,"width":1477,"height":1478,"rx":1479,"fill":744,"stroke":745,"style":1480},"473.6",[501,1558,1561],{"x":1559,"y":1512,"style":1560},"531.2","font-size:13.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","Run on",[501,1563,1564],{"x":1559,"y":714,"style":1560},"scratch DB",[501,1566,1567],{"x":1559,"y":1519,"style":1490},"compare with",[501,1569,1570],{"x":1559,"y":752,"style":1490},"the sheet",[54,1572],{"x1":1573,"y1":1499,"x2":1574,"y2":1499,"stroke":719,"style":1480},"593.8","615.8",[1502,1576],{"points":1577,"fill":1505},"619.8,63.0 610.8,68.0 610.8,58.0",[673,1579],{"x":1580,"y":683,"width":1477,"height":1478,"rx":1479,"fill":718,"stroke":719,"style":1480},"624.8",[501,1582,1584],{"x":1583,"y":1512,"style":1513},"682.4","Hand over",[501,1586,16],{"x":1583,"y":714,"style":1513},[501,1588,1589],{"x":1583,"y":1519,"style":1490},"for review and",[501,1591,1592],{"x":1583,"y":752,"style":1490},"run",[38,1594,1596],{"id":1595},"column-names-need-care-too","Column names need care too",[10,1598,1599,1600,261,1603,261,1606,1609,1610,1613,1614,261,1617,1620,1621,1624],{},"Values are not the only thing that can break a script. Spreadsheet headings often become column names, and headings such as ",[19,1601,1602],{},"Order",[19,1604,1605],{},"Date",[19,1607,1608],{},"Group"," or ",[19,1611,1612],{},"Unit price"," are either reserved words in SQL or contain spaces. Standard SQL quotes identifiers with double quotes — ",[14,1615,1616],{},"\"Order\"",[14,1618,1619],{},"\"Unit price\""," — and PostgreSQL, SQLite and Oracle follow it, while SQL Server traditionally uses square brackets and MySQL backticks. The safest approach is to map the sheet's headings to the table's real column names explicitly, in a dictionary reviewed once, and to quote identifiers only when the target database requires it. Quoted identifiers in PostgreSQL are also case-sensitive, which is a common source of ",[19,1622,1623],{},"column does not exist"," errors when a script quotes a name the table defines in lower case.",[654,1626,663,1631,663,1634,663,1637,663,1640,663,1645,663,1651,663,1656,663,1660,663,1664,663,1668,663,1671,663,1676,663,1680,663,1683,663,1686],{"viewBox":1627,"role":657,"ariaLabelledBy":1628,"xmlns":661,"style":1463},"4 0 752 173",[1629,1630],"sqlgen-when-t","sqlgen-when-d",[665,1632,1633],{"id":1629},"A script file or a direct load",[669,1635,1636],{"id":1630},"A generated script suits locked-down databases where a DBA reviews and runs changes, and leaves an auditable file. A direct load with parameterised queries suits jobs with their own database access and needs no escaping at all because values are sent separately from the SQL.",[673,1638],{"x":773,"y":676,"width":1472,"height":1639,"fill":679},"173",[673,1641],{"x":1476,"y":683,"width":1642,"height":1643,"rx":1644,"fill":718,"stroke":719,"style":1480},"351.0","141.0","14",[501,1646,1650],{"x":1647,"y":1648,"style":1649},"195.5","44.0","font-size:14px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Generated .sql script",[54,1652],{"x1":1653,"y1":1654,"x2":1655,"y2":1654,"stroke":719,"style":688},"36.0","57.0","355.0",[501,1657,1659],{"x":1647,"y":1658,"style":741},"77.0","reviewed and run by a DBA",[501,1661,1663],{"x":1647,"y":1662,"style":741},"103.0","auditable file",[501,1665,1667],{"x":1647,"y":1666,"style":741},"129.0","needs correct literals",[673,1669],{"x":1670,"y":683,"width":1642,"height":1643,"rx":1644,"fill":744,"stroke":745,"style":1480},"389.0",[501,1672,1675],{"x":1673,"y":1648,"style":1674},"564.5","font-size:14px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","Parameterised direct load",[54,1677],{"x1":1678,"y1":1654,"x2":1679,"y2":1654,"stroke":745,"style":688},"405.0","724.0",[501,1681,1682],{"x":1673,"y":1658,"style":741},"job has database access",[501,1684,1685],{"x":1673,"y":1662,"style":741},"values sent separately",[501,1687,1688],{"x":1673,"y":1666,"style":741},"no escaping needed",[38,1690,1692],{"id":1691},"step-4-an-upsert-variant-for-postgresql","Step 4 — An upsert variant for PostgreSQL",[43,1694,1696],{"className":83,"code":1695,"language":85,"meta":48,"style":48},"def upsert_script(df, table, key):\n    body = insert_script(df, table).rstrip().removesuffix(\"COMMIT;\").rstrip().rstrip(\";\")\n    updates = \", \".join(f\"{c} = EXCLUDED.{c}\" for c in COLUMNS if c != key)\n    return f\"{body}\\nON CONFLICT ({key}) DO UPDATE SET {updates};\\nCOMMIT;\\n\"\n\nprint(upsert_script(df, TABLE, \"product_code\").splitlines()[-2])\n",[14,1697,1698,1708,1728,1782,1829,1833],{"__ignoreMap":48},[52,1699,1700,1702,1705],{"class":54,"line":55},[52,1701,138],{"class":92},[52,1703,1704],{"class":141}," upsert_script",[52,1706,1707],{"class":96},"(df, table, key):\n",[52,1709,1710,1713,1715,1718,1720,1723,1726],{"class":54,"line":100},[52,1711,1712],{"class":96},"    body ",[52,1714,465],{"class":92},[52,1716,1717],{"class":96}," insert_script(df, table).rstrip().removesuffix(",[52,1719,1109],{"class":62},[52,1721,1722],{"class":96},").rstrip().rstrip(",[52,1724,1725],{"class":62},"\";\"",[52,1727,301],{"class":96},[52,1729,1730,1733,1735,1738,1740,1742,1744,1746,1749,1751,1754,1756,1758,1760,1762,1764,1767,1769,1772,1774,1776,1779],{"class":54,"line":114},[52,1731,1732],{"class":96},"    updates ",[52,1734,465],{"class":92},[52,1736,1737],{"class":62}," \", \"",[52,1739,929],{"class":96},[52,1741,579],{"class":92},[52,1743,582],{"class":62},[52,1745,326],{"class":325},[52,1747,1748],{"class":96},"c",[52,1750,367],{"class":325},[52,1752,1753],{"class":62}," = EXCLUDED.",[52,1755,326],{"class":325},[52,1757,1748],{"class":96},[52,1759,367],{"class":325},[52,1761,582],{"class":62},[52,1763,1017],{"class":92},[52,1765,1766],{"class":96}," c ",[52,1768,522],{"class":92},[52,1770,1771],{"class":166}," COLUMNS",[52,1773,237],{"class":92},[52,1775,1766],{"class":96},[52,1777,1778],{"class":92},"!=",[52,1780,1781],{"class":96}," key)\n",[52,1783,1784,1786,1788,1790,1792,1795,1798,1801,1803,1806,1808,1811,1813,1816,1818,1820,1822,1825,1827],{"class":54,"line":128},[52,1785,492],{"class":92},[52,1787,319],{"class":92},[52,1789,582],{"class":62},[52,1791,326],{"class":325},[52,1793,1794],{"class":96},"body",[52,1796,1797],{"class":325},"}\\n",[52,1799,1800],{"class":62},"ON CONFLICT (",[52,1802,326],{"class":325},[52,1804,1805],{"class":96},"key",[52,1807,367],{"class":325},[52,1809,1810],{"class":62},") DO UPDATE SET ",[52,1812,326],{"class":325},[52,1814,1815],{"class":96},"updates",[52,1817,367],{"class":325},[52,1819,649],{"class":62},[52,1821,1083],{"class":325},[52,1823,1824],{"class":62},"COMMIT;",[52,1826,1083],{"class":325},[52,1828,1134],{"class":62},[52,1830,1831],{"class":54,"line":135},[52,1832,132],{"emptyLinePlaceholder":131},[52,1834,1835,1837,1840,1842,1844,1846,1849,1852,1854],{"class":54,"line":148},[52,1836,1190],{"class":166},[52,1838,1839],{"class":96},"(upsert_script(df, ",[52,1841,860],{"class":166},[52,1843,261],{"class":96},[52,1845,1317],{"class":62},[52,1847,1848],{"class":96},").splitlines()[",[52,1850,1851],{"class":92},"-",[52,1853,736],{"class":166},[52,1855,1856],{"class":96},"])\n",[10,1858,1859,1860,1862,1863,1866,1867,1870,1871,1874,1875,1878],{},"When the sheet may contain rows that already exist — a price list resent with changes — a plain ",[14,1861,29],{}," fails on the first duplicate key. PostgreSQL's ",[14,1864,1865],{},"ON CONFLICT … DO UPDATE"," turns the statement into an upsert: new rows are inserted and existing ones updated from the incoming values, referred to as ",[14,1868,1869],{},"EXCLUDED",". SQLite accepts the same syntax; SQL Server uses ",[14,1872,1873],{},"MERGE"," instead, which needs a different template. This variant assumes a single batch — for several batches, each statement gets its own ",[14,1876,1877],{},"ON CONFLICT"," clause. The script is still a file for review, not something run blindly: the comment lines at the top, the row count and a successful run against a scratch database are what make it reviewable.",[38,1880,1882],{"id":1881},"common-pitfalls","Common pitfalls",[1884,1885,1886,1894,1911,1922,1933],"ul",{},[1887,1888,1889,1893],"li",{},[1890,1891,1892],"strong",{},"Building values with string concatenation in Excel."," Quotes, blanks and dates break; generate the literals in code.",[1887,1895,1896,1899,1900,1903,1904,1907,1908,36],{},[1890,1897,1898],{},"Not doubling single quotes."," ",[19,1901,1902],{},"O'Brien"," ends the string early; replace ",[14,1905,1906],{},"'"," with ",[14,1909,1910],{},"''",[1887,1912,1913,1919,1920,36],{},[1890,1914,1915,1916,1918],{},"Writing ",[14,1917,625],{}," or empty strings for blanks."," Use ",[14,1921,25],{},[1887,1923,1924,1927,1928,1930,1931,36],{},[1890,1925,1926],{},"One statement per row with no transaction."," Batch rows and wrap the script in ",[14,1929,1199],{},"\u002F",[14,1932,1202],{},[1887,1934,1935,1938],{},[1890,1936,1937],{},"Running generated SQL from your own code."," For a live connection, use parameterised queries instead; scripts are for handing over.",[38,1940,1942],{"id":1941},"conclusion","Conclusion",[10,1944,1945,1946,1948,1949,1930,1951,1953,1954,1956,1957,1959],{},"To turn an Excel sheet into a SQL script, convert every value with one function — ",[14,1947,25],{}," for blanks, unquoted numbers, ",[14,1950,629],{},[14,1952,633],{},", ISO date literals and single-quoted text with embedded quotes doubled — write the rows as batched multi-row ",[14,1955,29],{}," statements inside a transaction, and add an ",[14,1958,1877],{}," upsert where rows may already exist. Before handing the file over, run it against a scratch database and compare every value with the sheet.",[38,1961,1963],{"id":1962},"frequently-asked-questions","Frequently asked questions",[10,1965,1966,1969,1970,1973,1974,1976],{},[1890,1967,1968],{},"How do I generate INSERT statements from Excel with Python?","\nRead the sheet with pandas, turn each value into an SQL literal, and write ",[14,1971,1972],{},"INSERT INTO table (columns) VALUES (…), (…);"," statements to a ",[14,1975,16],{}," file.",[10,1978,1979,1982,1983,642,1985,36],{},[1890,1980,1981],{},"How do I escape apostrophes in SQL strings?","\nDouble them inside single quotes: ",[14,1984,1902],{},[14,1986,1987],{},"'O''Brien'",[10,1989,1990,1993,1994,1996],{},[1890,1991,1992],{},"How should blank Excel cells appear in SQL?","\nAs ",[14,1995,25],{},", not as an empty string or the text \"nan\".",[10,1998,1999,2002],{},[1890,2000,2001],{},"Should I use generated SQL strings to load data from Python?","\nNo; when connecting directly, use parameterised queries. Generated scripts are for review and execution by someone else.",[38,2004,2006],{"id":2005},"related","Related",[1884,2008,2009,2015,2022,2029],{},[1887,2010,2011,2012,2014],{},"Up one level: ",[32,2013,35],{"href":34}," — Excel and databases.",[1887,2016,2017,2021],{},[32,2018,2020],{"href":2019},"\u002Fadvanced-data-transformation-and-cleaning\u002Fmoving-data-between-excel-and-databases\u002Fload-an-excel-file-into-a-sql-database-with-pandas\u002F","Load an Excel File into a SQL Database with pandas"," — loading directly.",[1887,2023,2024,2028],{},[32,2025,2027],{"href":2026},"\u002Fadvanced-data-transformation-and-cleaning\u002Fmoving-data-between-excel-and-databases\u002Fload-excel-into-sqlite-with-a-defined-schema\u002F","Load Excel into SQLite with a Defined Schema"," — table design.",[1887,2030,2031,2035],{},[32,2032,2034],{"href":2033},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fprevent-formula-injection-in-excel-exports\u002F","Prevent Formula Injection in Excel Exports"," — escaping in the other direction.",[2037,2038,2039],"style",{},"html pre.shiki code .sMTad, html code.shiki .sMTad{--shiki-default:#6F42C1;--shiki-dark:#FFB757}html pre.shiki code .srMev, html code.shiki .srMev{--shiki-default:#032F62;--shiki-dark:#ADDCFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s-kum, html code.shiki .s-kum{--shiki-default:#D73A49;--shiki-dark:#FF9492}html pre.shiki code .skGVy, html code.shiki .skGVy{--shiki-default:#24292E;--shiki-dark:#F0F3F6}html pre.shiki code .s_Opv, html code.shiki .s_Opv{--shiki-default:#6F42C1;--shiki-dark:#DBB7FF}html pre.shiki code .sP0c6, html code.shiki .sP0c6{--shiki-default:#005CC5;--shiki-dark:#91CBFF}html pre.shiki code .sSjpA, html code.shiki .sSjpA{--shiki-default:#005CC5;--shiki-dark:#FF9492}html pre.shiki code .s-wDw, html code.shiki .s-wDw{--shiki-default:#6A737D;--shiki-dark:#BDC4CC}html pre.shiki code .sa561, html code.shiki .sa561{--shiki-default:#E36209;--shiki-dark:#FFB757}",{"title":48,"searchDepth":100,"depth":100,"links":2041},[2042,2043,2044,2045,2046,2047,2048,2049,2050,2051],{"id":40,"depth":100,"text":41},{"id":79,"depth":100,"text":80},{"id":834,"depth":100,"text":835},{"id":1216,"depth":100,"text":1217},{"id":1595,"depth":100,"text":1596},{"id":1691,"depth":100,"text":1692},{"id":1881,"depth":100,"text":1882},{"id":1941,"depth":100,"text":1942},{"id":1962,"depth":100,"text":1963},{"id":2005,"depth":100,"text":2006},"2026-10-02","Turn an Excel sheet into a reviewable .sql script for a database administrator to run: map pandas types to SQL literals, quote text safely by doubling single quotes, write NULL for blanks, dates as ISO literals, batch rows into multi-row INSERTs inside one transaction, add an upsert variant for PostgreSQL, and prove the script by running it against a scratch database and comparing every value with the sheet.","md",[2056,2058,2060,2062],{"q":1968,"a":2057},"Read the sheet with pandas, turn each value into an SQL literal, and write INSERT INTO table (columns) VALUES (…), (…); statements to a .sql file.",{"q":1981,"a":2059},"Double them inside single quotes: O'Brien becomes 'O''Brien'.",{"q":1992,"a":2061},"As NULL, not as an empty string or the text \"nan\".",{"q":2001,"a":2063},"No; when connecting directly, use parameterised queries. Generated scripts are for review and execution by someone else.","generate sql insert from excel python",null,{"breadcrumb":2067},[2068,2070,2073],{"name":2069,"item":1930},"Home",{"name":2071,"item":2072},"Advanced Data Transformation and Cleaning for Excel Automation","\u002Fadvanced-data-transformation-and-cleaning\u002F",{"name":35,"item":34},"\u002Fadvanced-data-transformation-and-cleaning\u002Fmoving-data-between-excel-and-databases\u002Fgenerate-sql-insert-statements-from-an-excel-sheet",{"title":2076,"description":2077},"Generate SQL INSERT Statements from an Excel Sheet in Python","Write a .sql script from Excel with pandas: safe quoting, NULLs, dates, numbers, multi-row batched INSERTs in a transaction, a PostgreSQL upsert variant, and a round-trip test in SQLite.","generate-sql-insert-statements-from-an-excel-sheet","advanced-data-transformation-and-cleaning\u002Fmoving-data-between-excel-and-databases\u002Fgenerate-sql-insert-statements-from-an-excel-sheet\u002Findex",[2081,2082],"data-sources","writing-excel","how-to","iL5bNa0Rhvnt_pCxGWmJn0XxJ0eSgbmoKo3CXThsWi0",[2086,2090],{"title":2087,"path":2088,"stem":2089,"children":-1},"Fetch Paginated API Data into Excel","\u002Fadvanced-data-transformation-and-cleaning\u002Fmoving-data-between-excel-and-databases\u002Ffetch-paginated-api-data-into-excel","advanced-data-transformation-and-cleaning\u002Fmoving-data-between-excel-and-databases\u002Ffetch-paginated-api-data-into-excel\u002Findex",{"title":2020,"path":2091,"stem":2092,"children":-1},"\u002Fadvanced-data-transformation-and-cleaning\u002Fmoving-data-between-excel-and-databases\u002Fload-an-excel-file-into-a-sql-database-with-pandas","advanced-data-transformation-and-cleaning\u002Fmoving-data-between-excel-and-databases\u002Fload-an-excel-file-into-a-sql-database-with-pandas\u002Findex",[2094,2096],{"path":2095,"title":2071},"\u002Fadvanced-data-transformation-and-cleaning",{"path":2097,"title":35},"\u002Fadvanced-data-transformation-and-cleaning\u002Fmoving-data-between-excel-and-databases",[],[2100,2103,2106,2109,2112,2115],{"path":2101,"title":2102,"kind":2083},"\u002Fautomating-reporting-workflows\u002Fpublishing-excel-reports-to-cloud-storage\u002Fread-an-excel-file-directly-from-amazon-s3-with-pandas","Read an Excel File Directly from Amazon S3 with pandas",{"path":2104,"title":2105,"kind":2083},"\u002Fadvanced-data-transformation-and-cleaning\u002Fmoving-data-between-excel-and-databases\u002Fcache-api-responses-when-building-excel-reports","Cache API Responses When Building Excel Reports",{"path":2107,"title":2108,"kind":2083},"\u002Fadvanced-data-transformation-and-cleaning\u002Fmoving-data-between-excel-and-databases\u002Fcompare-an-excel-file-with-a-database-table","Compare an Excel File with a Database Table",{"path":2110,"title":2111,"kind":2083},"\u002Fadvanced-data-transformation-and-cleaning\u002Fmoving-data-between-excel-and-databases\u002Fdownload-new-excel-files-from-an-sftp-server","Download New Excel Files from an SFTP Server",{"path":2113,"title":2114,"kind":2083},"\u002Fadvanced-data-transformation-and-cleaning\u002Fmoving-data-between-excel-and-databases\u002Fexport-a-large-database-table-to-excel-in-chunks","Export a Large Database Table to Excel in Chunks",{"path":2116,"title":2117,"kind":2083},"\u002Fadvanced-data-transformation-and-cleaning\u002Fmoving-data-between-excel-and-databases\u002Fexport-database-lookup-tables-as-excel-dropdown-lists","Export Database Lookup Tables as Excel Dropdown Lists",1790972096749]