5

I have a dictionary with keys and values like:

my_dict = {'a':33, 'b': 'something', 'c': GETDATE(), 'd': 55}

Assume column names in the SQL table are also named like the keys of the dict, i.e. "a,b,c,d".

The actual dictionary is 20+ key:value pairs.

Code

I have used pyodbc.connect to create a cursor which I could use to execute an SQL INSERT statement:

for k in my_dict.keys():
    cursor.execute(
    '''
        INSERT INTO TABLEabc (%s)
        VALUES (%s)
    '''
    % (k, my_dict[k])
    )

This seems inefficient though because it's a new SQL operation each time.

  1. What is the easiest way to insert the values using a loop?
  2. How could I write it so that it just makes one insert with all the values?
hc_dev
  • 8,389
  • 1
  • 26
  • 38
Mitch
  • 553
  • 1
  • 9
  • 24
  • So your table like `TABLEabc` has 20+ columns (like the key-value pairs in your dict)? And you would expect 1 single INSERT statement for this single dict to create 1 table-row? – hc_dev Jun 07 '22 at 18:10

3 Answers3

2

If you're using pyodbc then this might work:

columns = {row.column_name for row in cursor.columns(table='TABLEabc')}

safe_dict = {key: val for key, val in my_dict.items() if key in columns}

# generate a parameterised query for the keys in our dict
query = "INSERT INTO TABLEabc ({columns}) VALUES ({value_placeholders})".format(
    columns=", ".join(safe_dict.keys()),
    value_placeholders=", ".join(["?"] * len(safe_dict)),
)

cursor.execute(query, list(safe_dict.values()))

It is intended to be safe from SQL injection because:

  • we filter for only keys which are actual column names in the db
  • we use pyodbc cursor execute params, so the values will be escaped properly

Where it possibly won't work:

  • if any of the column names need to be quoted and escaped, this won't happen automatically so it will fail

Quoting/escaping is db-specific so we would have to check the rules for our actual db and apply that to the dict keys that we format into the query. (or find some way to get pyodbc to do that for us, not sure if possible)

If you trust your my_dict not to contain malicious code then you can simplify to just:

query = "INSERT INTO TABLEabc ({columns}) VALUES ({value_placeholders})".format(
    columns=", ".join(my_dict.keys()),
    value_placeholders=", ".join(["?"] * len(my_dict)),
)

cursor.execute(query, list(my_dict.values()))
Anentropic
  • 32,188
  • 12
  • 99
  • 147
  • ```columns``` returns an empty set. Can I just use ```my_dict.keys()``` instead for columns? What is ```columns``` supposed to return? – Mitch Jun 08 '22 at 13:48
  • @Mitch yes you certainly can, as long as you trust the keys, e.g. if it is a dict you have hard-coded yourself and doesn't come from user input – Anentropic Jun 08 '22 at 13:49
  • I do trust the keys yes they are hard-coded. But I get an error ```TypeError: 'dict_keys' object is not callable```. What type is ```columns``` supposed to be? – Mitch Jun 08 '22 at 13:51
  • here `columns=", ".join(safe_dict.keys())` you should be able to just do `columns=", ".join(my_dict.keys())` ... just replace `safe_dict` with `my_dict` everywhere basically – Anentropic Jun 08 '22 at 13:53
  • and get rid of the first two lines of my answer, you don't need to construct `safe_dict`. `columns` is a set in my answer – Anentropic Jun 08 '22 at 13:55
  • updated the answer with what I mean – Anentropic Jun 08 '22 at 13:56
  • I get an error when trying to pass 'GETDATE()' ```DataError: ('22007', '[22007] [Microsoft][ODBC SQL Server Driver][SQL Server]Conversion failed when converting date and/or time from character string. (241) (SQLExecDirectW)')``` do you know how to convert it to be passed? – Mitch Jun 09 '22 at 17:08
  • @Mitch probably better as a separate question. Is `GETDATE` your own function? What does it return? This answer suggests just give pyodbc a python datetime object https://stackoverflow.com/a/24458420/202168 – Anentropic Jun 09 '22 at 17:11
1

Using SQLAlchemy, this dictionary matches my table construct in SQL Server

mydict = {
  "Name": "Laura", "Form": "4B","Year": "7", "DOB" : "", "Col_5" : "",
  "Col_6" : "","Col_7" : "","Col_8" : ""  
}

print(mydict)

and I use this to pass the keys and values to a string (with some manipulation) into my table, which is called pypupil) I also wanted to write data for all records, whether present in the Dictionary or not.

fields = (str(list(mydict.keys()))[1:-1])
values = (str(list(mydict.values()))[1:-1])

columns = ', '.join("`" + str(x).replace('/', '_') + "`" for x in mydict.keys())
values = ', '.join("'" + str(x).replace('/', '_') + "'" for x in mydict.values())
sql = "INSERT INTO %s ( %s ) VALUES ( %s );" % ('pypupil', columns, values)

sql = sql.replace ( "`","") 
print(sql)

with engine.connect() as con:
    rs = con.execute(sql)

A bit more info and lots of helpers for this approach here. https://discuss.dizzycoding.com/using-a-python-dict-for-a-sql-insert-statement/

JonTout
  • 618
  • 6
  • 14
  • Look for Answer 5 /6 in the above link – JonTout Jun 07 '22 at 19:19
  • 1
    NOTE: With SQLAlchemy 1.4+, exiting a `with engine.begin() as conn:` block will automatically *commit* if no errors have occurred. Exiting a `with engine.connect() as conn:` block will automatically *rollback* if no errors have occurred. – Gord Thompson Jun 07 '22 at 22:25
  • That's useful to know, thanks @GordThompson. – JonTout Jun 08 '22 at 07:43
  • @JonTout do you know how to add id auto-increment to it? I get error ArgumentError: Column type NUMERIC(18, 0) on column 'tableABC.p_ID' is not compatible with autoincrement=True – Mitch Jun 08 '22 at 19:50
1

Supplemental to JonTout's answer, if you use SQLAlchemy then your dict is ready-made to be consumed by a SQLAlchemy Core Table object:

import datetime

import sqlalchemy as sa

connection_url = sa.engine.URL.create("mssql+pyodbc", … )
engine = sa.create_engine(connection_url)

table_abc = sa.Table("TABLEabc", sa.MetaData(), autoload_with=engine)

my_dict = {'a':33, 'b': 'something', 'c': datetime.date.today(), 'd': 55}

with engine.begin() as conn:
    conn.execute(table_abc.insert(), my_dict)
Gord Thompson
  • 116,920
  • 32
  • 215
  • 418
  • do you know how to add id auto-increment to it? I get error ```ArgumentError: Column type NUMERIC(18, 0) on column 'tableABC.p_ID' is not compatible with autoincrement=True``` – Mitch Jun 08 '22 at 19:50
  • Sorry, but I don't understand the connection. Are you trying to modify the Table object after it has been reflected? You may need to ask a new question. – Gord Thompson Jun 08 '22 at 20:17
  • No I'm trying to complete the insert into a table that has an auto-increment PK, but I get that error. Do you need to specify its auto-increment in the sql insert? – Mitch Jun 08 '22 at 20:52
  • In my example below the columns specified are Name, Form, Year etc -you won't insert data into an auto increment. The print(sql) command will generate the sql string that you can paste into SSMS. – JonTout Jun 08 '22 at 21:27
  • 1
    @Mitch - You may have found a bug. https://github.com/sqlalchemy/sqlalchemy/issues/8111 – Gord Thompson Jun 08 '22 at 21:33
  • @GordThompson did you get the same Error? – Mitch Jun 08 '22 at 21:45
  • 1
    @Mitch - Yes, it's in the GitHub issue. – Gord Thompson Jun 08 '22 at 21:46
  • Is a NUMERIC ID column unusual? Are they usually INT that's why it wasn't seen before? – Mitch Jun 08 '22 at 22:36
  • @Mitch - Yes, indeed. – Gord Thompson Jun 08 '22 at 23:01