From fbb4ded7000d5163344c67236fee7a1fae38991c Mon Sep 17 00:00:00 2001 From: Charles Leifer Date: Fri, 27 Apr 2018 08:37:50 -0500 Subject: [PATCH 01/12] Update docs and method signature for postgres cursor() method. --- docs/peewee/api.rst | 2 ++ playhouse/postgres_ext.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/peewee/api.rst b/docs/peewee/api.rst index deabd3ae0..528481949 100644 --- a/docs/peewee/api.rst +++ b/docs/peewee/api.rst @@ -110,6 +110,8 @@ Database .. py:method:: cursor([commit=None]) + :param commit: For internal use. + Return a ``cursor`` object on the current connection. If a connection is not open, one will be opened. The cursor will be whatever the underlying database-driver uses to encapsulate a database cursor. diff --git a/playhouse/postgres_ext.py b/playhouse/postgres_ext.py index 31dfe1d35..89c998ab2 100644 --- a/playhouse/postgres_ext.py +++ b/playhouse/postgres_ext.py @@ -428,7 +428,7 @@ def _connect(self): register_hstore(conn, globally=True) return conn - def cursor(self, commit): + def cursor(self, commit=None): if self.is_closed(): self.connect() if commit is __named_cursor__: From d92036e7602f29c5ce4dae4ff2ffd09c28fab218 Mon Sep 17 00:00:00 2001 From: Charles Leifer Date: Fri, 27 Apr 2018 08:52:19 -0500 Subject: [PATCH 02/12] Fix dependency graph when deferred foreign-keys are used. Replaces #1590. --- peewee.py | 7 +++++-- tests/models.py | 9 +++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/peewee.py b/peewee.py index 6a702f89f..d868e45e6 100644 --- a/peewee.py +++ b/peewee.py @@ -5552,8 +5552,11 @@ def sort_models(models): def dfs(model): if model in models and model not in seen: seen.add(model) - for rel_model in model._meta.refs.values(): - dfs(rel_model) + for foreign_key, rel_model in model._meta.refs.items(): + # Do not depth-first search deferred foreign-keys as this can + # cause tables to be created in the incorrect order. + if not foreign_key.deferred: + dfs(rel_model) if model._meta.depends_on: for dependency in model._meta.depends_on: dfs(dependency) diff --git a/tests/models.py b/tests/models.py index be2ea34aa..0f07217ef 100644 --- a/tests/models.py +++ b/tests/models.py @@ -4,6 +4,7 @@ import unittest from peewee import * +from peewee import sort_models from .base import db from .base import get_in_memory_db @@ -835,6 +836,14 @@ class Foo(TestModel): 'SELECT "t1"."id", "t1"."foo_id" FROM "note" AS "t1" ' 'WHERE ("t1"."foo_id" = ?)'), [1337]) + def test_deferred_fk_dependency_graph(self): + class AUser(TestModel): + foo = DeferredForeignKey('Tweet') + class ZTweet(TestModel): + user = ForeignKeyField(AUser, backref='ztweets') + + self.assertEqual(sort_models([AUser, ZTweet]), [AUser, ZTweet]) + def test_table_schema(self): class Schema(TestModel): pass From c16c3c07bc9f14f0b865b97c7ce60a5cd1b624c6 Mon Sep 17 00:00:00 2001 From: Charles Leifer Date: Sun, 29 Apr 2018 13:59:18 -0500 Subject: [PATCH 03/12] Update docs with missing changes reported in #1592 --- docs/peewee/changes.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/peewee/changes.rst b/docs/peewee/changes.rst index 1797ea690..472190557 100644 --- a/docs/peewee/changes.rst +++ b/docs/peewee/changes.rst @@ -24,6 +24,8 @@ Database well as :py:meth:`Model.create_table` and :py:meth:`Model.drop_table` all default to ``safe=True`` (``create_table`` will create if not exists, ``drop_table`` will drop if exists). * ``connect_kwargs`` attribute has been renamed to ``connect_params`` +* initialization parameter for custom field-type definitions has changed + from ``fields`` to ``field_types``. Model Meta options ^^^^^^^^^^^^^^^^^^ @@ -89,6 +91,9 @@ When using :py:func:`prefetch`, the collected instances will be stored in the same attribute as the foreign-key's ``backref``. Previously, you would access joined instances using ``(backref)_prefetch``. +The :py:class:`SQL` object, used to create a composable a SQL string, now +expects the second parameter to be a list/tuple of parameters. + Removed Extensions ^^^^^^^^^^^^^^^^^^ From 0146328c9a5b398ac77b8e7463370a6ec70805d2 Mon Sep 17 00:00:00 2001 From: Charles Leifer Date: Sun, 29 Apr 2018 14:01:36 -0500 Subject: [PATCH 04/12] Add union, union_all, intersect and except_ methods to select query. Refs #1592 --- docs/peewee/api.rst | 17 +++++++++++++++++ peewee.py | 8 ++++---- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/docs/peewee/api.rst b/docs/peewee/api.rst index 528481949..fec110910 100644 --- a/docs/peewee/api.rst +++ b/docs/peewee/api.rst @@ -1690,18 +1690,35 @@ Query-builder Select query helper-class that implements operator-overloads for creating compound queries. + .. py:method:: union_all(dest) + + Create a UNION ALL query with ``dest``. + .. py:method:: __add__(dest) Create a UNION ALL query with ``dest``. + .. py:method:: union(dest) + + Create a UNION query with ``dest``. + .. py:method:: __or__(dest) Create a UNION query with ``dest``. + .. py:method:: intersect(dest) + + Create an INTERSECT query with ``dest``. + .. py:method:: __and__(dest) Create an INTERSECT query with ``dest``. + .. py:method:: except_(dest) + + Create an EXCEPT query with ``dest``. Note that the method name has a + trailing "_" character since ``except`` is a Python reserved word. + .. py:method:: __sub__(dest) Create an EXCEPT query with ``dest``. diff --git a/peewee.py b/peewee.py index d868e45e6..4ba977e53 100644 --- a/peewee.py +++ b/peewee.py @@ -1725,10 +1725,10 @@ def method(self, other): class SelectQuery(Query): - __add__ = __compound_select__('UNION ALL') - __or__ = __compound_select__('UNION') - __and__ = __compound_select__('INTERSECT') - __sub__ = __compound_select__('EXCEPT') + union_all = __add__ = __compound_select__('UNION ALL') + union = __or__ = __compound_select__('UNION') + intersect = __and__ = __compound_select__('INTERSECT') + except_ = __sub__ = __compound_select__('EXCEPT') __radd__ = __compound_select__('UNION ALL', inverted=True) __ror__ = __compound_select__('UNION', inverted=True) __rand__ = __compound_select__('INTERSECT', inverted=True) From aa80a52e6a73991c40cbf1a61a869c7e9ff4170b Mon Sep 17 00:00:00 2001 From: Charles Leifer Date: Sun, 29 Apr 2018 14:07:46 -0500 Subject: [PATCH 05/12] Unit test for union/intersect compound ops. --- tests/sql.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/tests/sql.py b/tests/sql.py index 03b2a815b..d4d232f23 100644 --- a/tests/sql.py +++ b/tests/sql.py @@ -321,6 +321,34 @@ def test_compound_select(self): 'FROM "users" AS "U2" ' 'WHERE ("U2"."superuser" = ?)'), ['charlie', True, False]) + def test_compound_operations(self): + admin = (User + .select(User.c.username, Value('admin').alias('role')) + .where(User.c.is_admin == True)) + editors = (User + .select(User.c.username, Value('editor').alias('role')) + .where(User.c.is_editor == True)) + + union = admin.union(editors) + self.assertSQL(union, ( + 'SELECT "t1"."username", ? AS "role" ' + 'FROM "users" AS "t1" ' + 'WHERE ("t1"."is_admin" = ?) ' + 'UNION ' + 'SELECT "t2"."username", ? AS "role" ' + 'FROM "users" AS "t2" ' + 'WHERE ("t2"."is_editor" = ?)'), ['admin', 1, 'editor', 1]) + + xcept = editors.except_(admin) + self.assertSQL(xcept, ( + 'SELECT "t1"."username", ? AS "role" ' + 'FROM "users" AS "t1" ' + 'WHERE ("t1"."is_editor" = ?) ' + 'EXCEPT ' + 'SELECT "t2"."username", ? AS "role" ' + 'FROM "users" AS "t2" ' + 'WHERE ("t2"."is_admin" = ?)'), ['editor', 1, 'admin', 1]) + def test_join_on_query(self): inner = User.select(User.c.id).alias('j1') query = (Tweet From 930cd28eb7d467000f2851ba626331b173388c3c Mon Sep 17 00:00:00 2001 From: Charles Leifer Date: Sun, 29 Apr 2018 14:49:50 -0500 Subject: [PATCH 06/12] Remove Python 2.6-specific code for "is_model" helper. --- peewee.py | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/peewee.py b/peewee.py index 4ba977e53..1ef78e91a 100644 --- a/peewee.py +++ b/peewee.py @@ -343,17 +343,7 @@ def merge_dict(source, overrides): merged.update(overrides) return merged -if sys.version_info[:2] == (2, 6): - import types - def is_model(obj): - if isinstance(obj, (type, types.ClassType)): - return issubclass(obj, Model) - return False -else: - def is_model(obj): - if isclass(obj): - return issubclass(obj, Model) - return False +is_model = lambda o: isclass(o) and issubclass(o, Model) def ensure_tuple(value): if value is not None: From abddbea6780ebe35c3040bbf0268b2e6db3be126 Mon Sep 17 00:00:00 2001 From: Charles Leifer Date: Mon, 30 Apr 2018 08:58:27 -0500 Subject: [PATCH 07/12] Add update/from and fix compilation of FROM clause in UPDATE/FROM. --- peewee.py | 3 ++- tests/models.py | 25 +++++++++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/peewee.py b/peewee.py index 1ef78e91a..d48f732dc 100644 --- a/peewee.py +++ b/peewee.py @@ -2056,7 +2056,8 @@ def __sql__(self, ctx): .sql(CommaNodeList(expressions))) if self._from: - ctx.literal(' FROM ').sql(CommaNodeList(self._from)) + with ctx.scope_source(parentheses=False): + ctx.literal(' FROM ').sql(CommaNodeList(self._from)) if self._where: ctx.literal(' WHERE ').sql(self._where) diff --git a/tests/models.py b/tests/models.py index 0f07217ef..75c5332e2 100644 --- a/tests/models.py +++ b/tests/models.py @@ -4,6 +4,7 @@ import unittest from peewee import * +from peewee import QualifiedNames from peewee import sort_models from .base import db @@ -2991,3 +2992,27 @@ def test_sequence(self): self.assertEqual(s1.seq_id, 1) self.assertEqual(s2.seq_id, 2) self.assertEqual(s3.seq_id, 3) + + +@skip_case_unless(IS_POSTGRESQL) +class TestUpdateFrom(ModelTestCase): + requires = [User] + + def test_update_from(self): + u1 = User.create(username='u1') + u2 = User.create(username='u2') + data = [(u1.id, 'u1-x'), (u2.id, 'u2-x')] + vl = ValuesList(data, columns=('id', 'username'), alias='tmp') + query = (User + .update({User.username: QualifiedNames(vl.c.username)}) + .from_(vl) + .where(QualifiedNames(User.id == vl.c.id))) + self.assertSQL(query, ( + 'UPDATE "users" SET "username" = "tmp"."username" ' + 'FROM (VALUES (?, ?), (?, ?)) AS "tmp"("id", "username") ' + 'WHERE ("users"."id" = "tmp"."id")'), + [u1.id, 'u1-x', u2.id, 'u2-x']) + + query.execute() + usernames = [u.username for u in User.select().order_by(User.username)] + self.assertEqual(usernames, ['u1-x', 'u2-x']) From 1691519419225ab31c9efd8da916e8372dfdced3 Mon Sep 17 00:00:00 2001 From: Charles Leifer Date: Mon, 30 Apr 2018 08:59:13 -0500 Subject: [PATCH 08/12] Update failing test in model sql generation. --- tests/model_sql.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/model_sql.py b/tests/model_sql.py index fbb1ad5ca..d02494869 100644 --- a/tests/model_sql.py +++ b/tests/model_sql.py @@ -368,7 +368,7 @@ class Account(TestModel): 'UPDATE "account" SET ' '"contact_first" = "first", ' '"contact_last" = "last" ' - 'FROM "salesperson" ' + 'FROM "salesperson" AS "t1" ' 'WHERE ("sales_id" = "id")'), []) def test_delete(self): From 0537f35dcabc32945fa1205d9befa8dbe1096195 Mon Sep 17 00:00:00 2001 From: Charles Leifer Date: Mon, 30 Apr 2018 09:50:24 -0500 Subject: [PATCH 09/12] Add comment about vtable meta parameters. --- playhouse/sqlite_ext.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/playhouse/sqlite_ext.py b/playhouse/sqlite_ext.py index 6f1a6d48d..89aa8f319 100644 --- a/playhouse/sqlite_ext.py +++ b/playhouse/sqlite_ext.py @@ -213,6 +213,10 @@ def _create_virtual_table(self, safe=True, **options): options = self.model.clean_options( merge_dict(self.model._meta.options, options)) + # Structure: + # CREATE VIRTUAL TABLE + # USING + # ([prefix_arguments, ...] fields, ... [arguments, ...], [options...]) ctx = self._create_context() ctx.literal('CREATE VIRTUAL TABLE ') if safe: From 5562616331f0d505bb71f28d0b6c17d94adf69d6 Mon Sep 17 00:00:00 2001 From: Charles Leifer Date: Tue, 1 May 2018 09:02:07 -0500 Subject: [PATCH 10/12] Update / from subq --- tests/models.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/models.py b/tests/models.py index 75c5332e2..6fc67c0f4 100644 --- a/tests/models.py +++ b/tests/models.py @@ -3016,3 +3016,21 @@ def test_update_from(self): query.execute() usernames = [u.username for u in User.select().order_by(User.username)] self.assertEqual(usernames, ['u1-x', 'u2-x']) + + data = [(u1.id, 'u1-y'), (u2.id, 'u2-y')] + vl = ValuesList(data, columns=('id', 'username'), alias='tmp') + subq = vl.select(vl.c.id, vl.c.username) + query = (User + .update({User.username: QualifiedNames(subq.c.username)}) + .from_(subq) + .where(QualifiedNames(User.id == subq.c.id))) + self.assertSQL(query, ( + 'UPDATE "users" SET "username" = "t1"."username" FROM (' + 'SELECT "tmp"."id", "tmp"."username" ' + 'FROM (VALUES (?, ?), (?, ?)) AS "tmp"("id", "username")) AS "t1" ' + 'WHERE ("users"."id" = "t1"."id")'), + [u1.id, 'u1-y', u2.id, 'u2-y']) + + query.execute() + usernames = [u.username for u in User.select().order_by(User.username)] + self.assertEqual(usernames, ['u1-y', 'u2-y']) From 15f4b5b36009b43f51bd2d96bf11101dc50f5bf9 Mon Sep 17 00:00:00 2001 From: Charles Leifer Date: Tue, 1 May 2018 09:24:15 -0500 Subject: [PATCH 11/12] Flesh out tests for update/from. --- tests/models.py | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/tests/models.py b/tests/models.py index 6fc67c0f4..aa6d54143 100644 --- a/tests/models.py +++ b/tests/models.py @@ -2999,8 +2999,7 @@ class TestUpdateFrom(ModelTestCase): requires = [User] def test_update_from(self): - u1 = User.create(username='u1') - u2 = User.create(username='u2') + u1, u2 = [User.create(username=username) for username in ('u1', 'u2')] data = [(u1.id, 'u1-x'), (u2.id, 'u2-x')] vl = ValuesList(data, columns=('id', 'username'), alias='tmp') query = (User @@ -3017,6 +3016,8 @@ def test_update_from(self): usernames = [u.username for u in User.select().order_by(User.username)] self.assertEqual(usernames, ['u1-x', 'u2-x']) + def test_update_from_subselect(self): + u1, u2 = [User.create(username=username) for username in ('u1', 'u2')] data = [(u1.id, 'u1-y'), (u2.id, 'u2-y')] vl = ValuesList(data, columns=('id', 'username'), alias='tmp') subq = vl.select(vl.c.id, vl.c.username) @@ -3034,3 +3035,21 @@ def test_update_from(self): query.execute() usernames = [u.username for u in User.select().order_by(User.username)] self.assertEqual(usernames, ['u1-y', 'u2-y']) + + @requires_models(User, Tweet) + def test_update_from_simple(self): + u = User.create(username='u1') + t1 = Tweet.create(user=u, content='t1') + t2 = Tweet.create(user=u, content='t2') + + query = (User + .update({User.username: QualifiedNames(Tweet.content)}) + .from_(Tweet) + .where(Tweet.content == 't2')) + self.assertSQL(query, ( + 'UPDATE "users" SET "username" = "t1"."content" ' + 'FROM "tweet" AS "t1" ' + 'WHERE ("content" = ?)'), ['t2']) + query.execute() + + self.assertEqual(User.get(User.id == u.id).username, 't2') From fc04cc85ec52a79e3b589ff6d16200174b24420b Mon Sep 17 00:00:00 2001 From: Charles Leifer Date: Tue, 1 May 2018 10:48:24 -0500 Subject: [PATCH 12/12] 3.3.2 --- CHANGELOG.md | 11 +++++++++++ peewee.py | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 30702ccee..fb85ad579 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,17 @@ releases, visit GitHub: https://github.com/coleifer/peewee/releases +## 3.3.2 + +* Add methods for `union()`, `union_all`, `intersect()` and `except_()`. + Previously, these methods were only available as operator overloads. +* Removed some Python 2.6-specific support code, as 2.6 is no longer officially + supported. +* Fixed model-graph resolution logic for deferred foreign-keys. +* Better support for UPDATE...FROM queries (Postgresql). + +[View commits](https://github.com/coleifer/peewee/compare/3.3.1...3.3.2) + ## 3.3.1 * Fixed long-standing bug in 3.x regarding using column aliases with queries diff --git a/peewee.py b/peewee.py index d48f732dc..4fdaff7f4 100644 --- a/peewee.py +++ b/peewee.py @@ -57,7 +57,7 @@ mysql = None -__version__ = '3.3.1' +__version__ = '3.3.2' __all__ = [ 'AsIs', 'AutoField',