diff --git a/README.rst b/README.rst index 31bb94ff..99514b91 100644 --- a/README.rst +++ b/README.rst @@ -1420,6 +1420,20 @@ This produces: WHERE "date">NOW()-7 GROUP BY "col1","col2" +Inserting dictionaries +"""""""""""""""" + +There's a simple convenience function to insert dicts with ``pypika.Query.into("mytable").insert_dict()``. + +.. code-block:: python + Query.into(Table("foo")).insert_dict({"value": 42, "created_at": datetime(2024, 3, 15)}) + +This produces: + +.. code-block:: sql + INSERT INTO "foo" ("value", "created_at") + VALUES (42, '2024-03-15T00:00:00') + .. _tutorial_end: .. _contributing_start: diff --git a/pypika/queries.py b/pypika/queries.py index c51c6b2b..d5042bc6 100644 --- a/pypika/queries.py +++ b/pypika/queries.py @@ -1,6 +1,6 @@ from copy import copy from functools import reduce -from typing import Any, List, Optional, Sequence, Tuple as TypedTuple, Type, Union +from typing import Any, List, Optional, Sequence, Tuple as TypedTuple, Type, Union, Set, Dict from pypika.enums import Dialects, JoinType, ReferenceOption, SetOperation from pypika.terms import ( @@ -750,7 +750,7 @@ def __init__( self._select_star_tables = set() self._mysql_rollup = False self._select_into = False - + self._using_insert_dict = False self._subquery_count = 0 self._foreign_table = False @@ -890,6 +890,9 @@ def columns(self, *terms: Any) -> "QueryBuilder": if self._insert_table is None: raise AttributeError("'Query' object has no attribute '%s'" % "insert") + if self._using_insert_dict: + raise QueryException("Cannot mix use of columns() and insert_dict()") + if terms and isinstance(terms[0], (list, tuple)): terms = terms[0] @@ -903,6 +906,15 @@ def insert(self, *terms: Any) -> "QueryBuilder": self._apply_terms(*terms) self._replace = False + def insert_dict(self, data: Dict[str, Any]) -> "QueryBuilder": + if self._columns: + raise QueryException("Cannot mix use of columns() and insert_dict()") + + cols = data.keys() + builder = self.columns(*cols).insert(*data.values()) + builder._using_insert_dict = True + return builder + @builder def replace(self, *terms: Any) -> "QueryBuilder": self._apply_terms(*terms) diff --git a/pypika/tests/test_inserts.py b/pypika/tests/test_inserts.py index f86efd7a..49071721 100644 --- a/pypika/tests/test_inserts.py +++ b/pypika/tests/test_inserts.py @@ -17,6 +17,7 @@ ) from pypika.terms import Values from pypika.utils import QueryException +from datetime import datetime __author__ = "Timothy Heys" __email__ = "theys@kayak.com" @@ -168,6 +169,28 @@ def test_insert_with_statement(self): 'WITH sub_qs AS (SELECT "id" FROM "abc") INSERT INTO "abc" SELECT "sub_qs"."id" FROM sub_qs', str(q) ) +class InsertIntoWithDict(unittest.TestCase): + table_abc = Table("abc") + + def test_inserting_simple_dictionary(self): + q = Query.into(self.table_abc).insert_dict({"c1": "value", "c2": 1}) + self.assertEqual("INSERT INTO \"abc\" (\"c1\",\"c2\") VALUES ('value',1)", str(q)) + + def test_inserting_dictionary_goes_through_value_quoting_logic(self): + q = Query.into(self.table_abc).insert_dict({"num": 1, "timestamp": datetime(2023, 4, 18)}) + self.assertEqual("INSERT INTO \"abc\" (\"num\",\"timestamp\") VALUES (1,'2023-04-18T00:00:00')", str(q)) + + def test_inserting_dictionary_produces_builder(self): + q = Query.into(self.table_abc).insert_dict({"num": 1, "timestamp": datetime(2023, 4, 18)}) + q = q.insert(2, datetime(2023, 4, 19)) + self.assertEqual("INSERT INTO \"abc\" (\"num\",\"timestamp\") VALUES (1,'2023-04-18T00:00:00'),(2,'2023-04-19T00:00:00')", str(q)) + + def test_columns_is_not_allowed_with_insert_dict_even_with_matching_columns(self): + with self.assertRaisesRegex(QueryException, "Cannot mix use of columns.*and insert_dict"): + Query.into(self.table_abc).columns("num", "key").insert_dict({"num": 1, "key": "foo"}) + + with self.assertRaisesRegex(QueryException, "Cannot mix use of columns.*and insert_dict"): + Query.into(self.table_abc).insert_dict({"num": 1, "key": "foo"}).columns("num", "key") class PostgresInsertIntoOnConflictTests(unittest.TestCase): table_abc = Table("abc")