Passed
Pull Request — master (#29)
by Alexandru
01:21
created

PostgresQuerySet.update()   B

Complexity

Conditions 3

Size

Total Lines 25

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 3

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 3
c 1
b 0
f 0
dl 0
loc 25
ccs 13
cts 13
cp 1
crap 3
rs 8.8571
1 1
from typing import Dict, List, Union, Tuple
0 ignored issues
show
Configuration introduced by
The import typing could not be resolved.

This can be caused by one of the following:

1. Missing Dependencies

This error could indicate a configuration issue of Pylint. Make sure that your libraries are available by adding the necessary commands.

# .scrutinizer.yml
before_commands:
    - sudo pip install abc # Python2
    - sudo pip3 install abc # Python3
Tip: We are currently not using virtualenv to run pylint, when installing your modules make sure to use the command for the correct version.

2. Missing __init__.py files

This error could also result from missing __init__.py files in your module folders. Make sure that you place one file in each sub-folder.

Loading history...
2
3 1
import django
0 ignored issues
show
Configuration introduced by
The import django could not be resolved.

This can be caused by one of the following:

1. Missing Dependencies

This error could indicate a configuration issue of Pylint. Make sure that your libraries are available by adding the necessary commands.

# .scrutinizer.yml
before_commands:
    - sudo pip install abc # Python2
    - sudo pip3 install abc # Python3
Tip: We are currently not using virtualenv to run pylint, when installing your modules make sure to use the command for the correct version.

2. Missing __init__.py files

This error could also result from missing __init__.py files in your module folders. Make sure that you place one file in each sub-folder.

Loading history...
4 1
from django.conf import settings
0 ignored issues
show
Configuration introduced by
The import django.conf could not be resolved.

This can be caused by one of the following:

1. Missing Dependencies

This error could indicate a configuration issue of Pylint. Make sure that your libraries are available by adding the necessary commands.

# .scrutinizer.yml
before_commands:
    - sudo pip install abc # Python2
    - sudo pip3 install abc # Python3
Tip: We are currently not using virtualenv to run pylint, when installing your modules make sure to use the command for the correct version.

2. Missing __init__.py files

This error could also result from missing __init__.py files in your module folders. Make sure that you place one file in each sub-folder.

Loading history...
5 1
from django.db import models, transaction
0 ignored issues
show
Configuration introduced by
The import django.db could not be resolved.

This can be caused by one of the following:

1. Missing Dependencies

This error could indicate a configuration issue of Pylint. Make sure that your libraries are available by adding the necessary commands.

# .scrutinizer.yml
before_commands:
    - sudo pip install abc # Python2
    - sudo pip3 install abc # Python3
Tip: We are currently not using virtualenv to run pylint, when installing your modules make sure to use the command for the correct version.

2. Missing __init__.py files

This error could also result from missing __init__.py files in your module folders. Make sure that you place one file in each sub-folder.

Loading history...
6 1
from django.db.models.sql import UpdateQuery
0 ignored issues
show
Configuration introduced by
The import django.db.models.sql could not be resolved.

This can be caused by one of the following:

1. Missing Dependencies

This error could indicate a configuration issue of Pylint. Make sure that your libraries are available by adding the necessary commands.

# .scrutinizer.yml
before_commands:
    - sudo pip install abc # Python2
    - sudo pip3 install abc # Python3
Tip: We are currently not using virtualenv to run pylint, when installing your modules make sure to use the command for the correct version.

2. Missing __init__.py files

This error could also result from missing __init__.py files in your module folders. Make sure that you place one file in each sub-folder.

Loading history...
7 1
from django.db.models.sql.constants import CURSOR
0 ignored issues
show
Configuration introduced by
The import django.db.models.sql.constants could not be resolved.

This can be caused by one of the following:

1. Missing Dependencies

This error could indicate a configuration issue of Pylint. Make sure that your libraries are available by adding the necessary commands.

# .scrutinizer.yml
before_commands:
    - sudo pip install abc # Python2
    - sudo pip3 install abc # Python3
Tip: We are currently not using virtualenv to run pylint, when installing your modules make sure to use the command for the correct version.

2. Missing __init__.py files

This error could also result from missing __init__.py files in your module folders. Make sure that you place one file in each sub-folder.

Loading history...
8 1
from django.db.models.fields import NOT_PROVIDED
0 ignored issues
show
Configuration introduced by
The import django.db.models.fields could not be resolved.

This can be caused by one of the following:

1. Missing Dependencies

This error could indicate a configuration issue of Pylint. Make sure that your libraries are available by adding the necessary commands.

# .scrutinizer.yml
before_commands:
    - sudo pip install abc # Python2
    - sudo pip3 install abc # Python3
Tip: We are currently not using virtualenv to run pylint, when installing your modules make sure to use the command for the correct version.

2. Missing __init__.py files

This error could also result from missing __init__.py files in your module folders. Make sure that you place one file in each sub-folder.

Loading history...
9 1
from django.core.exceptions import ImproperlyConfigured, SuspiciousOperation
0 ignored issues
show
Configuration introduced by
The import django.core.exceptions could not be resolved.

This can be caused by one of the following:

1. Missing Dependencies

This error could indicate a configuration issue of Pylint. Make sure that your libraries are available by adding the necessary commands.

# .scrutinizer.yml
before_commands:
    - sudo pip install abc # Python2
    - sudo pip3 install abc # Python3
Tip: We are currently not using virtualenv to run pylint, when installing your modules make sure to use the command for the correct version.

2. Missing __init__.py files

This error could also result from missing __init__.py files in your module folders. Make sure that you place one file in each sub-folder.

Loading history...
10
11 1
from psqlextra import signals
12 1
from psqlextra.compiler import (PostgresReturningUpdateCompiler,
13
                                PostgresInsertCompiler)
14 1
from psqlextra.query import PostgresQuery, PostgresInsertQuery, ConflictAction
15
16
17 1
class PostgresQuerySet(models.QuerySet):
18
    """Adds support for PostgreSQL specifics."""
19
20 1
    def __init__(self, model=None, query=None, using=None, hints=None):
21
        """Initializes a new instance of :see:PostgresQuerySet."""
22
23 1
        super().__init__(model, query, using, hints)
24
25 1
        self.query = query or PostgresQuery(self.model)
0 ignored issues
show
Bug introduced by
The Instance of PostgresQuerySet does not seem to have a member named model.

This check looks for calls to members that are non-existent. These calls will fail.

The member could have been renamed or removed.

Loading history...
26
27 1
        self.conflict_target = None
28 1
        self.conflict_action = None
29 1
        self.index_predicate = None
30
31 1
    def annotate(self, **annotations):
32
        """Custom version of the standard annotate function
33
        that allows using field names as annotated fields.
34
35
        Normally, the annotate function doesn't allow you
36
        to use the name of an existing field on the model
37
        as the alias name. This version of the function does
38
        allow that.
39
        """
40
41 1
        fields = {
42
            field.name: field
43
            for field in self.model._meta.get_fields()
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like _meta was declared protected and should not be accessed from this context.

Prefixing a member variable _ is usually regarded as the equivalent of declaring it with protected visibility that exists in other languages. Consequentially, such a member should only be accessed from the same class or a child class:

class MyParent:
    def __init__(self):
        self._x = 1;
        self.y = 2;

class MyChild(MyParent):
    def some_method(self):
        return self._x    # Ok, since accessed from a child class

class AnotherClass:
    def some_method(self, instance_of_my_child):
        return instance_of_my_child._x   # Would be flagged as AnotherClass is not
                                         # a child class of MyParent
Loading history...
Bug introduced by
The Instance of PostgresQuerySet does not seem to have a member named model.

This check looks for calls to members that are non-existent. These calls will fail.

The member could have been renamed or removed.

Loading history...
44
        }
45
46
        # temporarily rename the fields that have the same
47
        # name as a field name, we'll rename them back after
48
        # the function in the base class ran
49 1
        new_annotations = {}
50 1
        renames = {}
51 1
        for name, value in annotations.items():
52 1
            if name in fields:
53
                new_name = '%s_new' % name
54
                new_annotations[new_name] = value
55
                renames[new_name] = name
56
            else:
57 1
                new_annotations[name] = value
58
59
        # run the base class's annotate function
60 1
        result = super().annotate(**new_annotations)
0 ignored issues
show
Coding Style introduced by
Usage of * or ** arguments should usually be done with care.

Generally, there is nothing wrong with usage of * or ** arguments. For readability of the code base, we suggest to not over-use these language constructs though.

For more information, we can recommend this blog post from Ned Batchelder including its comments which also touches this aspect.

Loading history...
61
62
        # rename the annotations back to as specified
63 1
        result.rename_annotations(**renames)
0 ignored issues
show
Coding Style introduced by
Usage of * or ** arguments should usually be done with care.

Generally, there is nothing wrong with usage of * or ** arguments. For readability of the code base, we suggest to not over-use these language constructs though.

For more information, we can recommend this blog post from Ned Batchelder including its comments which also touches this aspect.

Loading history...
64 1
        return result
65
66 1
    def rename_annotations(self, **annotations):
67
        """Renames the aliases for the specified annotations:
68
69
            .annotate(myfield=F('somestuf__myfield'))
70
            .rename_annotations(myfield='field')
71
72
        Arguments:
73
            annotations:
74
                The annotations to rename. Mapping the
75
                old name to the new name.
76
        """
77
78 1
        self.query.rename_annotations(annotations)
79 1
        return self
80
81 1
    def join(self, **conditions):
82
        """Adds extra conditions to existing joins.
83
84
        WARNING: This is an extremely experimental feature.
85
                 DO NOT USE unless you know what you're doing.
86
        """
87
88
        self.query.add_join_conditions(conditions)
89
        return self
90
91 1
    def update(self, **fields):
92
        """Updates all rows that match the filter."""
93
94
        # build up the query to execute
95 1
        self._for_write = True
0 ignored issues
show
Coding Style introduced by
The attribute _for_write was defined outside __init__.

It is generally a good practice to initialize all attributes to default values in the __init__ method:

class Foo:
    def __init__(self, x=None):
        self.x = x
Loading history...
96 1
        query = self.query.clone(UpdateQuery)
97 1
        query._annotations = None
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like _annotations was declared protected and should not be accessed from this context.

Prefixing a member variable _ is usually regarded as the equivalent of declaring it with protected visibility that exists in other languages. Consequentially, such a member should only be accessed from the same class or a child class:

class MyParent:
    def __init__(self):
        self._x = 1;
        self.y = 2;

class MyChild(MyParent):
    def some_method(self):
        return self._x    # Ok, since accessed from a child class

class AnotherClass:
    def some_method(self, instance_of_my_child):
        return instance_of_my_child._x   # Would be flagged as AnotherClass is not
                                         # a child class of MyParent
Loading history...
98 1
        query.add_update_values(fields)
99
100
        # build the compiler for for the query
101 1
        connection = django.db.connections[self.db]
0 ignored issues
show
Bug introduced by
The Instance of PostgresQuerySet does not seem to have a member named db.

This check looks for calls to members that are non-existent. These calls will fail.

The member could have been renamed or removed.

Loading history...
102 1
        compiler = PostgresReturningUpdateCompiler(query, connection, self.db)
0 ignored issues
show
Bug introduced by
The Instance of PostgresQuerySet does not seem to have a member named db.

This check looks for calls to members that are non-existent. These calls will fail.

The member could have been renamed or removed.

Loading history...
103
104
        # execute the query
105 1
        with transaction.atomic(using=self.db, savepoint=False):
0 ignored issues
show
Bug introduced by
The Instance of PostgresQuerySet does not seem to have a member named db.

This check looks for calls to members that are non-existent. These calls will fail.

The member could have been renamed or removed.

Loading history...
106 1
            rows = compiler.execute_sql(CURSOR)
107 1
        self._result_cache = None
0 ignored issues
show
Coding Style introduced by
The attribute _result_cache was defined outside __init__.

It is generally a good practice to initialize all attributes to default values in the __init__ method:

class Foo:
    def __init__(self, x=None):
        self.x = x
Loading history...
108
109
        # send out a signal for each row
110 1
        for row in rows:
111 1
            signals.update.send(self.model, pk=row[0])
0 ignored issues
show
Bug introduced by
The Instance of PostgresQuerySet does not seem to have a member named model.

This check looks for calls to members that are non-existent. These calls will fail.

The member could have been renamed or removed.

Loading history...
112
113
        # the original update(..) returns the amount of rows
114
        # affected, let's do the same
115 1
        return len(rows)
116
117 1
    def on_conflict(self, fields: List[Union[str, Tuple[str]]], action, index_predicate=None):
118
        """Sets the action to take when conflicts arise when attempting
119
        to insert/create a new row.
120
121
        Arguments:
122
            fields:
123
                The fields the conflicts can occur in.
124
125
            action:
126
                The action to take when the conflict occurs.
127
128
            index_predicate:
129
                The index predicate to satisfy an arbiter partial index (i.e. what partial index to use for checking
130
                conflicts)
131
        """
132
133 1
        self.conflict_target = fields
134 1
        self.conflict_action = action
135 1
        self.index_predicate = index_predicate
136
137 1
        return self
138
139 1
    def bulk_insert(self, rows):
140
        """Creates multiple new records in the database.
141
142
        This allows specifying custom conflict behavior using .on_conflict().
143
        If no special behavior was specified, this uses the normal Django create(..)
144
145
        Arguments:
146
            rows:
147
                An array of dictionaries, where each dictionary
148
                describes the fields to insert.
149
150
        Returns:
151
        """
152
153 1
        if self.conflict_target or self.conflict_action:
154 1
            compiler = self._build_insert_compiler(rows)
155 1
            compiler.execute_sql(return_id=True)
156 1
            return
157
158
        # no special action required, use the standard Django bulk_create(..)
159
        super().bulk_create([self.model(**fields) for fields in rows])
0 ignored issues
show
Coding Style introduced by
Usage of * or ** arguments should usually be done with care.

Generally, there is nothing wrong with usage of * or ** arguments. For readability of the code base, we suggest to not over-use these language constructs though.

For more information, we can recommend this blog post from Ned Batchelder including its comments which also touches this aspect.

Loading history...
Bug introduced by
The Instance of PostgresQuerySet does not seem to have a member named model.

This check looks for calls to members that are non-existent. These calls will fail.

The member could have been renamed or removed.

Loading history...
160
161 1
    def insert(self, **fields):
162
        """Creates a new record in the database.
163
164
        This allows specifying custom conflict behavior using .on_conflict().
165
        If no special behavior was specified, this uses the normal Django create(..)
166
167
        Arguments:
168
            fields:
169
                The fields of the row to create.
170
171
        Returns:
172
            The primary key of the record that was created.
173
        """
174
175 1
        if self.conflict_target or self.conflict_action:
176 1
            compiler = self._build_insert_compiler([fields])
177 1
            rows = compiler.execute_sql(return_id=True)
178 1
            if 'id' in rows[0]:
179 1
                return rows[0]['id']
180
            return None
181
182
        # no special action required, use the standard Django create(..)
183
        return super().create(**fields).id
184
185 1
    def insert_and_get(self, **fields):
186
        """Creates a new record in the database and then gets
187
        the entire row.
188
189
        This allows specifying custom conflict behavior using .on_conflict().
190
        If no special behavior was specified, this uses the normal Django create(..)
191
192
        Arguments:
193
            fields:
194
                The fields of the row to create.
195
196
        Returns:
197
            The model instance representing the row that was created.
198
        """
199
200 1
        if not self.conflict_target and not self.conflict_action:
201
            # no special action required, use the standard Django create(..)
202
            return super().create(**fields)
203
204 1
        compiler = self._build_insert_compiler([fields])
205 1
        rows = compiler.execute_sql(return_id=False)
206
207 1
        columns = rows[0]
208
209
        # get a list of columns that are officially part of the model
210 1
        model_columns = [
211
            field.column
212
            for field in self.model._meta.local_concrete_fields
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like _meta was declared protected and should not be accessed from this context.

Prefixing a member variable _ is usually regarded as the equivalent of declaring it with protected visibility that exists in other languages. Consequentially, such a member should only be accessed from the same class or a child class:

class MyParent:
    def __init__(self):
        self._x = 1;
        self.y = 2;

class MyChild(MyParent):
    def some_method(self):
        return self._x    # Ok, since accessed from a child class

class AnotherClass:
    def some_method(self, instance_of_my_child):
        return instance_of_my_child._x   # Would be flagged as AnotherClass is not
                                         # a child class of MyParent
Loading history...
Bug introduced by
The Instance of PostgresQuerySet does not seem to have a member named model.

This check looks for calls to members that are non-existent. These calls will fail.

The member could have been renamed or removed.

Loading history...
213
        ]
214
215
        # strip out any columns/fields returned by the db that
216
        # are not present in the model
217 1
        model_init_fields = {}
218 1
        for column_name, column_value in columns.items():
219 1
            if column_name not in model_columns:
220 1
                continue
221
222 1
            model_init_fields[column_name] = column_value
223
224 1
        return self.model(**model_init_fields)
0 ignored issues
show
Coding Style introduced by
Usage of * or ** arguments should usually be done with care.

Generally, there is nothing wrong with usage of * or ** arguments. For readability of the code base, we suggest to not over-use these language constructs though.

For more information, we can recommend this blog post from Ned Batchelder including its comments which also touches this aspect.

Loading history...
Bug introduced by
The Instance of PostgresQuerySet does not seem to have a member named model.

This check looks for calls to members that are non-existent. These calls will fail.

The member could have been renamed or removed.

Loading history...
225
226 1
    def upsert(self, conflict_target: List, fields: Dict, index_predicate=None) -> int:
227
        """Creates a new record or updates the existing one
228
        with the specified data.
229
230
        Arguments:
231
            conflict_target:
232
                Fields to pass into the ON CONFLICT clause.
233
234
            fields:
235
                Fields to insert/update.
236
237
            index_predicate:
238
                The index predicate to satisfy an arbiter partial index (i.e. what partial index to use for checking
239
                conflicts)
240
241
        Returns:
242
            The primary key of the row that was created/updated.
243
        """
244
245 1
        self.on_conflict(conflict_target, ConflictAction.UPDATE, index_predicate)
246 1
        return self.insert(**fields)
0 ignored issues
show
Coding Style introduced by
Usage of * or ** arguments should usually be done with care.

Generally, there is nothing wrong with usage of * or ** arguments. For readability of the code base, we suggest to not over-use these language constructs though.

For more information, we can recommend this blog post from Ned Batchelder including its comments which also touches this aspect.

Loading history...
247
248 1
    def upsert_and_get(self, conflict_target: List, fields: Dict):
249
        """Creates a new record or updates the existing one
250
        with the specified data and then gets the row.
251
252
        Arguments:
253
            conflict_target:
254
                Fields to pass into the ON CONFLICT clause.
255
256
            fields:
257
                Fields to insert/update.
258
259
        Returns:
260
            The model instance representing the row
261
            that was created/updated.
262
        """
263
264 1
        self.on_conflict(conflict_target, ConflictAction.UPDATE)
265 1
        return self.insert_and_get(**fields)
0 ignored issues
show
Coding Style introduced by
Usage of * or ** arguments should usually be done with care.

Generally, there is nothing wrong with usage of * or ** arguments. For readability of the code base, we suggest to not over-use these language constructs though.

For more information, we can recommend this blog post from Ned Batchelder including its comments which also touches this aspect.

Loading history...
266
267 1
    def bulk_upsert(self, conflict_target: List, rows: List[Dict]):
268
        """Creates a set of new records or updates the existing
269
        ones with the specified data.
270
271
        Arguments:
272
            conflict_target:
273
                Fields to pass into the ON CONFLICT clause.
274
275
            rows:
276
                Rows to upsert.
277
        """
278
279
        self.on_conflict(conflict_target, ConflictAction.UPDATE)
280
        return self.bulk_insert(rows)
281
282 1
    def _build_insert_compiler(self, rows: List[Dict]):
283
        """Builds the SQL compiler for a insert query.
284
285
        Arguments:
286
            rows:
287
                A list of dictionaries, where each entry
288
                describes a record to insert.
289
290
        Returns:
291
            The SQL compiler for the insert.
292
        """
293
294
        # create model objects, we also have to detect cases
295
        # such as:
296
        #   [dict(first_name='swen'), dict(fist_name='swen', last_name='kooij')]
297
        # we need to be certain that each row specifies the exact same
298
        # amount of fields/columns
299 1
        objs = []
300 1
        field_count = len(rows[0])
301 1
        for index, row in enumerate(rows):
302 1
            if field_count != len(row):
303
                raise SuspiciousOperation((
304
                    'In bulk upserts, you cannot have rows with different field '
305
                    'configurations. Row {0} has a different field config than '
306
                    'the first row.'
307
                ).format(index))
308
309 1
            objs.append(self.model(**row))
0 ignored issues
show
Coding Style introduced by
Usage of * or ** arguments should usually be done with care.

Generally, there is nothing wrong with usage of * or ** arguments. For readability of the code base, we suggest to not over-use these language constructs though.

For more information, we can recommend this blog post from Ned Batchelder including its comments which also touches this aspect.

Loading history...
Bug introduced by
The Instance of PostgresQuerySet does not seem to have a member named model.

This check looks for calls to members that are non-existent. These calls will fail.

The member could have been renamed or removed.

Loading history...
310
311
        # indicate this query is going to perform write
312 1
        self._for_write = True
0 ignored issues
show
Coding Style introduced by
The attribute _for_write was defined outside __init__.

It is generally a good practice to initialize all attributes to default values in the __init__ method:

class Foo:
    def __init__(self, x=None):
        self.x = x
Loading history...
313
314
        # get the fields to be used during update/insert
315 1
        insert_fields, update_fields = self._get_upsert_fields(rows[0])
316
317
        # build a normal insert query
318 1
        query = PostgresInsertQuery(self.model)
0 ignored issues
show
Bug introduced by
The Instance of PostgresQuerySet does not seem to have a member named model.

This check looks for calls to members that are non-existent. These calls will fail.

The member could have been renamed or removed.

Loading history...
319 1
        query.conflict_action = self.conflict_action
320 1
        query.conflict_target = self.conflict_target
321 1
        query.index_predicate = self.index_predicate
322 1
        query.values(objs, insert_fields, update_fields)
323
324
        # use the postgresql insert query compiler to transform the insert
325
        # into an special postgresql insert
326 1
        connection = django.db.connections[self.db]
0 ignored issues
show
Bug introduced by
The Instance of PostgresQuerySet does not seem to have a member named db.

This check looks for calls to members that are non-existent. These calls will fail.

The member could have been renamed or removed.

Loading history...
327 1
        compiler = PostgresInsertCompiler(query, connection, self.db)
0 ignored issues
show
Bug introduced by
The Instance of PostgresQuerySet does not seem to have a member named db.

This check looks for calls to members that are non-existent. These calls will fail.

The member could have been renamed or removed.

Loading history...
328
329 1
        return compiler
330
331 1
    def _is_magical_field(self, model_instance, field, is_insert: bool):
0 ignored issues
show
Coding Style introduced by
This method could be written as a function/class method.

If a method does not access any attributes of the class, it could also be implemented as a function or static method. This can help improve readability. For example

class Foo:
    def some_method(self, x, y):
        return x + y;

could be written as

class Foo:
    @classmethod
    def some_method(cls, x, y):
        return x + y;
Loading history...
332
        """Verifies whether this field is gonna modify something
333
        on its own.
334
335
        "Magical" means that a field modifies the field value
336
        during the pre_save.
337
338
        Arguments:
339
            model_instance:
340
                The model instance the field is defined on.
341
342
            field:
343
                The field to get of whether the field is
344
                magical.
345
346
            is_insert:
347
                Pretend whether this is an insert?
348
349
        Returns:
350
            True when this field modifies something.
351
        """
352
353
        # does this field modify someting upon insert?
354 1
        old_value = getattr(model_instance, field.name, None)
355 1
        field.pre_save(model_instance, is_insert)
356 1
        new_value = getattr(model_instance, field.name, None)
357
358 1
        return old_value != new_value
359
360 1
    def _get_upsert_fields(self, kwargs):
361
        """Gets the fields to use in an upsert.
362
363
        This some nice magic. We'll split the fields into
364
        a group of "insert fields" and "update fields":
365
366
        INSERT INTO bla ("val1", "val2") ON CONFLICT DO UPDATE SET val1 = EXCLUDED.val1
367
368
                         ^^^^^^^^^^^^^^                            ^^^^^^^^^^^^^^^^^^^^
369
                         insert_fields                                 update_fields
370
371
        Often, fields appear in both lists. But, for example,
372
        a :see:DateTime field with `auto_now_add=True` set, will
373
        only appear in "insert_fields", since it won't be set
374
        on existing rows.
375
376
        Other than that, the user specificies a list of fields
377
        in the upsert() call. That migt not be all fields. The
378
        user could decide to leave out optional fields. If we
379
        end up doing an update, we don't want to overwrite
380
        those non-specified fields.
381
382
        We cannot just take the list of fields the user
383
        specifies, because as mentioned, some fields
384
        make modifications to the model on their own.
385
386
        We'll have to detect which fields make modifications
387
        and include them in the list of insert/update fields.
388
        """
389
390 1
        model_instance = self.model(**kwargs)
0 ignored issues
show
Coding Style introduced by
Usage of * or ** arguments should usually be done with care.

Generally, there is nothing wrong with usage of * or ** arguments. For readability of the code base, we suggest to not over-use these language constructs though.

For more information, we can recommend this blog post from Ned Batchelder including its comments which also touches this aspect.

Loading history...
Bug introduced by
The Instance of PostgresQuerySet does not seem to have a member named model.

This check looks for calls to members that are non-existent. These calls will fail.

The member could have been renamed or removed.

Loading history...
391 1
        insert_fields = []
392 1
        update_fields = []
393
394 1
        for field in model_instance._meta.local_concrete_fields:
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like _meta was declared protected and should not be accessed from this context.

Prefixing a member variable _ is usually regarded as the equivalent of declaring it with protected visibility that exists in other languages. Consequentially, such a member should only be accessed from the same class or a child class:

class MyParent:
    def __init__(self):
        self._x = 1;
        self.y = 2;

class MyChild(MyParent):
    def some_method(self):
        return self._x    # Ok, since accessed from a child class

class AnotherClass:
    def some_method(self, instance_of_my_child):
        return instance_of_my_child._x   # Would be flagged as AnotherClass is not
                                         # a child class of MyParent
Loading history...
395 1
            has_default = field.default != NOT_PROVIDED
396 1
            if (field.name in kwargs or field.column in kwargs):
0 ignored issues
show
Unused Code Coding Style introduced by
There is an unnecessary parenthesis after if.
Loading history...
397 1
                insert_fields.append(field)
398 1
                update_fields.append(field)
399 1
                continue
400 1
            elif has_default:
401 1
                insert_fields.append(field)
402 1
                continue
403
404
            # special handling for 'pk' which always refers to
405
            # the primary key, so if we the user specifies `pk`
406
            # instead of a concrete field, we have to handle that
407 1
            if field.primary_key is True and 'pk' in kwargs:
408 1
                insert_fields.append(field)
409 1
                update_fields.append(field)
410 1
                continue
411
412 1
            if self._is_magical_field(model_instance, field, is_insert=True):
413 1
                insert_fields.append(field)
414
415 1
            if self._is_magical_field(model_instance, field, is_insert=False):
416 1
                update_fields.append(field)
417
418 1
        return insert_fields, update_fields
419
420
421 1
class PostgresManager(models.Manager):
422
    """Adds support for PostgreSQL specifics."""
423
424 1
    use_in_migrations = True
425
426 1
    def __init__(self, *args, **kwargs):
427
        """Initializes a new instance of :see:PostgresManager."""
428
429 1
        super(PostgresManager, self).__init__(*args, **kwargs)
430
431
        # make sure our back-end is set and refuse to proceed
432
        # if it's not set
433 1
        db_backend = settings.DATABASES['default']['ENGINE']
434 1
        if 'psqlextra' not in db_backend:
435
            raise ImproperlyConfigured((
436
                '\'%s\' is not the \'psqlextra.backend\'. '
437
                'django-postgres-extra cannot function without '
438
                'the \'psqlextra.backend\'. Set DATABASES.ENGINE.'
439
            ) % db_backend)
440
441
        # hook into django signals to then trigger our own
442
443 1
        django.db.models.signals.post_save.connect(
444
            self._on_model_save, sender=self.model, weak=False)
445
446 1
        django.db.models.signals.pre_delete.connect(
447
            self._on_model_delete, sender=self.model, weak=False)
448
449 1
        self._signals_connected = True
450
451 1
    def __del__(self):
452
        """Disconnects signals."""
453
454 1
        if self._signals_connected is False:
455
            return
456
457
        # django.db.models.signals.post_save.disconnect(
458
        #     self._on_model_save, sender=self.model)
459
460
        # django.db.models.signals.pre_delete.disconnect(
461
        #     self._on_model_delete, sender=self.model)
462
463 1
    def get_queryset(self):
464
        """Gets the query set to be used on this manager."""
465
466 1
        return PostgresQuerySet(self.model, using=self._db)
0 ignored issues
show
Bug introduced by
The Instance of PostgresManager does not seem to have a member named _db.

This check looks for calls to members that are non-existent. These calls will fail.

The member could have been renamed or removed.

Loading history...
467
468 1
    def on_conflict(self, fields: List[Union[str, Tuple[str]]], action):
469
        """Sets the action to take when conflicts arise when attempting
470
        to insert/create a new row.
471
472
        Arguments:
473
            fields:
474
                The fields the conflicts can occur in.
475
476
            action:
477
                The action to take when the conflict occurs.
478
        """
479 1
        return self.get_queryset().on_conflict(fields, action)
480
481 1
    def upsert(self, conflict_target: List, fields: Dict, index_predicate=None) -> int:
482
        """Creates a new record or updates the existing one
483
        with the specified data.
484
485
        Arguments:
486
            conflict_target:
487
                Fields to pass into the ON CONFLICT clause.
488
489
            fields:
490
                Fields to insert/update.
491
492
            index_predicate:
493
                The index predicate to satisfy an arbiter partial index.
494
495
        Returns:
496
            The primary key of the row that was created/updated.
497
        """
498
499 1
        return self.get_queryset().upsert(conflict_target, fields, index_predicate)
500
501 1
    def upsert_and_get(self, conflict_target: List, fields: Dict):
502
        """Creates a new record or updates the existing one
503
        with the specified data and then gets the row.
504
505
        Arguments:
506
            conflict_target:
507
                Fields to pass into the ON CONFLICT clause.
508
509
            fields:
510
                Fields to insert/update.
511
512
        Returns:
513
            The model instance representing the row
514
            that was created/updated.
515
        """
516
517 1
        return self.get_queryset().upsert_and_get(conflict_target, fields)
518
519 1
    def bulk_upsert(self, conflict_target: List, rows: List[Dict]):
520
        """Creates a set of new records or updates the existing
521
        ones with the specified data.
522
523
        Arguments:
524
            conflict_target:
525
                Fields to pass into the ON CONFLICT clause.
526
527
            rows:
528
                Rows to upsert.
529
        """
530
531
        return self.get_queryset().bulk_upsert(conflict_target, rows)
532
533 1
    @staticmethod
534
    def _on_model_save(sender, **kwargs):
535
        """When a model gets created or updated."""
536
537 1
        created, instance = kwargs['created'], kwargs['instance']
538
539 1
        if created:
540 1
            signals.create.send(sender, pk=instance.pk)
541
        else:
542 1
            signals.update.send(sender, pk=instance.pk)
543
544 1
    @staticmethod
545
    def _on_model_delete(sender, **kwargs):
546
        """When a model gets deleted."""
547
548 1
        instance = kwargs['instance']
549
        signals.delete.send(sender, pk=instance.pk)
550