From 8822c88d7e2edefede39345e9cf035cc6f450322 Mon Sep 17 00:00:00 2001 From: Edvard Majakari Date: Mon, 26 Feb 2024 16:07:33 +0200 Subject: [PATCH 1/4] simple insert_dict() builder --- pypika/queries.py | 16 ++++++++++++++-- pypika/tests/test_inserts.py | 23 +++++++++++++++++++++++ 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/pypika/queries.py b/pypika/queries.py index c51c6b2b..96a2cc20 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": + cols = data.keys() + if self._columns and self._columns != cols: + raise QueryException("Current columns differs from columns in 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..d4542b2e 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(self): + with self.assertRaises(QueryException): + Query().into(self.table_abc).columns("a", "b").insert_dict({"num": 1}) + + with self.assertRaises(QueryException): + Query().into(self.table_abc).insert_dict({"num": 1}).columns("a", "b") class PostgresInsertIntoOnConflictTests(unittest.TestCase): table_abc = Table("abc") From 1d8b141e7a6d1ccf7e5ce65d2545ae25f75fa89f Mon Sep 17 00:00:00 2001 From: Edvard Majakari Date: Mon, 26 Feb 2024 16:34:34 +0200 Subject: [PATCH 2/4] CR: just use class method (Query().into -> Query.into) --- pypika/tests/test_inserts.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pypika/tests/test_inserts.py b/pypika/tests/test_inserts.py index d4542b2e..d838f920 100644 --- a/pypika/tests/test_inserts.py +++ b/pypika/tests/test_inserts.py @@ -173,24 +173,24 @@ 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}) + 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)}) + 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 = 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(self): with self.assertRaises(QueryException): - Query().into(self.table_abc).columns("a", "b").insert_dict({"num": 1}) + Query.into(self.table_abc).columns("a", "b").insert_dict({"num": 1}) with self.assertRaises(QueryException): - Query().into(self.table_abc).insert_dict({"num": 1}).columns("a", "b") + Query.into(self.table_abc).insert_dict({"num": 1}).columns("a", "b") class PostgresInsertIntoOnConflictTests(unittest.TestCase): table_abc = Table("abc") From 3167376de23bb5d2cde657cef76a1485cd1b9dde Mon Sep 17 00:00:00 2001 From: Edvard Majakari Date: Mon, 26 Feb 2024 17:02:30 +0200 Subject: [PATCH 3/4] make insert_dict() api less confusing in SQL builders, order of calling individual functions should not matter. Originally it was allowed to first call columns() and then insert_dict as long as the two matched, but I'm not even sure it worked, as we should have used set comparison instead. It is also better to have symmetric API in --- pypika/queries.py | 6 +++--- pypika/tests/test_inserts.py | 10 +++++----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/pypika/queries.py b/pypika/queries.py index 96a2cc20..d5042bc6 100644 --- a/pypika/queries.py +++ b/pypika/queries.py @@ -907,10 +907,10 @@ def insert(self, *terms: Any) -> "QueryBuilder": self._replace = False def insert_dict(self, data: Dict[str, Any]) -> "QueryBuilder": - cols = data.keys() - if self._columns and self._columns != cols: - raise QueryException("Current columns differs from columns in keys") + 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 diff --git a/pypika/tests/test_inserts.py b/pypika/tests/test_inserts.py index d838f920..49071721 100644 --- a/pypika/tests/test_inserts.py +++ b/pypika/tests/test_inserts.py @@ -185,12 +185,12 @@ def test_inserting_dictionary_produces_builder(self): 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(self): - with self.assertRaises(QueryException): - Query.into(self.table_abc).columns("a", "b").insert_dict({"num": 1}) + 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.assertRaises(QueryException): - Query.into(self.table_abc).insert_dict({"num": 1}).columns("a", "b") + 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") From 98cbf46ecceca9d4e00f4def40c6393a842b52ba Mon Sep 17 00:00:00 2001 From: Edvard Majakari Date: Mon, 11 Mar 2024 17:14:14 +0200 Subject: [PATCH 4/4] insert_into() example to README --- README.rst | 14 ++++++++++++++ 1 file changed, 14 insertions(+) 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: