From 477582c6fb16c7b42151c30dd66ad4375352bb1b Mon Sep 17 00:00:00 2001 From: Edvard Majakari Date: Mon, 26 Feb 2024 16:07:33 +0200 Subject: [PATCH 1/6] 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 d7861200..0aecb352 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 6ce18772840d54c2f37ab7eae2f43fde79b03cfe Mon Sep 17 00:00:00 2001 From: Edvard Majakari Date: Mon, 26 Feb 2024 16:34:34 +0200 Subject: [PATCH 2/6] 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 03f97b8e5e3f2064c3be5e76aad843d417b3943d Mon Sep 17 00:00:00 2001 From: Edvard Majakari Date: Mon, 26 Feb 2024 17:02:30 +0200 Subject: [PATCH 3/6] 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 0aecb352..c410592f 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 bf019394ee2450ec5eb67c66ef806c2d54c12225 Mon Sep 17 00:00:00 2001 From: Edvard Majakari Date: Mon, 11 Mar 2024 17:14:14 +0200 Subject: [PATCH 4/6] insert_into() example to README --- README.rst | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/README.rst b/README.rst index 67946435..19810f4c 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: From 4f0330f8c9c9c3b3311b2369c9ef68d1cabdc2b7 Mon Sep 17 00:00:00 2001 From: Edvard Majakari Date: Fri, 22 Nov 2024 09:04:31 +0000 Subject: [PATCH 5/6] run pre-commit hooks --- pypika/tests/test_inserts.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pypika/tests/test_inserts.py b/pypika/tests/test_inserts.py index 49071721..ba180978 100644 --- a/pypika/tests/test_inserts.py +++ b/pypika/tests/test_inserts.py @@ -169,6 +169,7 @@ 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") @@ -183,7 +184,10 @@ def test_inserting_dictionary_goes_through_value_quoting_logic(self): 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)) + 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"): @@ -192,6 +196,7 @@ 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).insert_dict({"num": 1, "key": "foo"}).columns("num", "key") + class PostgresInsertIntoOnConflictTests(unittest.TestCase): table_abc = Table("abc") From 32bc612a734f0bd971c2d7886b7f68cb89ada352 Mon Sep 17 00:00:00 2001 From: Yusuke Hayashi Date: Wed, 20 Nov 2024 04:18:23 +0900 Subject: [PATCH 6/6] fix: README.rst (#801)