Passed
Push — master ( 65c5ff...76b686 )
by Swen
01:40
created

PostgresQuerySet.insert_and_get()   B

Complexity

Conditions 6

Size

Total Lines 40

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 6.0163

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 6
c 2
b 0
f 0
dl 0
loc 40
ccs 12
cts 13
cp 0.9231
crap 6.0163
rs 7.5384
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
        if django.VERSION >= (2, 0):
97
            query = self.query.chain(UpdateQuery)
98
        else:
99 1
            query = self.query.clone(UpdateQuery)
100 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...
101 1
        query.add_update_values(fields)
102
103
        # build the compiler for for the query
104 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...
105 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...
106
107
        # execute the query
108 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...
109 1
            rows = compiler.execute_sql(CURSOR)
110 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...
111
112
        # send out a signal for each row
113 1
        for row in rows:
114 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...
115
116
        # the original update(..) returns the amount of rows
117
        # affected, let's do the same
118 1
        return len(rows)
119
120 1
    def on_conflict(self, fields: List[Union[str, Tuple[str]]], action, index_predicate: str=None):
121
        """Sets the action to take when conflicts arise when attempting
122
        to insert/create a new row.
123
124
        Arguments:
125
            fields:
126
                The fields the conflicts can occur in.
127
128
            action:
129
                The action to take when the conflict occurs.
130
131
            index_predicate:
132
                The index predicate to satisfy an arbiter partial index (i.e. what partial index to use for checking
133
                conflicts)
134
        """
135
136 1
        self.conflict_target = fields
137 1
        self.conflict_action = action
138 1
        self.index_predicate = index_predicate
139
140 1
        return self
141
142 1
    def bulk_insert(self, rows):
143
        """Creates multiple new records in the database.
144
145
        This allows specifying custom conflict behavior using .on_conflict().
146
        If no special behavior was specified, this uses the normal Django create(..)
147
148
        Arguments:
149
            rows:
150
                An array of dictionaries, where each dictionary
151
                describes the fields to insert.
152
153
        Returns:
154
        """
155
156 1
        if self.conflict_target or self.conflict_action:
157 1
            compiler = self._build_insert_compiler(rows)
158 1
            compiler.execute_sql(return_id=True)
159 1
            return
160
161
        # no special action required, use the standard Django bulk_create(..)
162
        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...
163
164 1
    def insert(self, **fields):
165
        """Creates a new record in the database.
166
167
        This allows specifying custom conflict behavior using .on_conflict().
168
        If no special behavior was specified, this uses the normal Django create(..)
169
170
        Arguments:
171
            fields:
172
                The fields of the row to create.
173
174
        Returns:
175
            The primary key of the record that was created.
176
        """
177
178 1
        if self.conflict_target or self.conflict_action:
179 1
            compiler = self._build_insert_compiler([fields])
180 1
            rows = compiler.execute_sql(return_id=True)
181 1
            if 'id' in rows[0]:
182 1
                return rows[0]['id']
183
            return None
184
185
        # no special action required, use the standard Django create(..)
186
        return super().create(**fields).id
187
188 1
    def insert_and_get(self, **fields):
189
        """Creates a new record in the database and then gets
190
        the entire row.
191
192
        This allows specifying custom conflict behavior using .on_conflict().
193
        If no special behavior was specified, this uses the normal Django create(..)
194
195
        Arguments:
196
            fields:
197
                The fields of the row to create.
198
199
        Returns:
200
            The model instance representing the row that was created.
201
        """
202
203 1
        if not self.conflict_target and not self.conflict_action:
204
            # no special action required, use the standard Django create(..)
205
            return super().create(**fields)
206
207 1
        compiler = self._build_insert_compiler([fields])
208 1
        rows = compiler.execute_sql(return_id=False)
209
210 1
        columns = rows[0]
211
212
        # get a list of columns that are officially part of the model
213 1
        model_columns = [
214
            field.column
215
            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...
216
        ]
217
218
        # strip out any columns/fields returned by the db that
219
        # are not present in the model
220 1
        model_init_fields = {}
221 1
        for column_name, column_value in columns.items():
222 1
            if column_name not in model_columns:
223 1
                continue
224
225 1
            model_init_fields[column_name] = column_value
226
227 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...
228
229 1
    def upsert(self, conflict_target: List, fields: Dict, index_predicate: str=None) -> int:
230
        """Creates a new record or updates the existing one
231
        with the specified data.
232
233
        Arguments:
234
            conflict_target:
235
                Fields to pass into the ON CONFLICT clause.
236
237
            fields:
238
                Fields to insert/update.
239
240
            index_predicate:
241
                The index predicate to satisfy an arbiter partial index (i.e. what partial index to use for checking
242
                conflicts)
243
244
        Returns:
245
            The primary key of the row that was created/updated.
246
        """
247
248 1
        self.on_conflict(conflict_target, ConflictAction.UPDATE, index_predicate)
249 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...
250
251 1
    def upsert_and_get(self, conflict_target: List, fields: Dict):
252
        """Creates a new record or updates the existing one
253
        with the specified data and then gets the row.
254
255
        Arguments:
256
            conflict_target:
257
                Fields to pass into the ON CONFLICT clause.
258
259
            fields:
260
                Fields to insert/update.
261
262
        Returns:
263
            The model instance representing the row
264
            that was created/updated.
265
        """
266
267 1
        self.on_conflict(conflict_target, ConflictAction.UPDATE)
268 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...
269
270 1
    def bulk_upsert(self, conflict_target: List, rows: List[Dict]):
271
        """Creates a set of new records or updates the existing
272
        ones with the specified data.
273
274
        Arguments:
275
            conflict_target:
276
                Fields to pass into the ON CONFLICT clause.
277
278
            rows:
279
                Rows to upsert.
280
        """
281
282
        self.on_conflict(conflict_target, ConflictAction.UPDATE)
283
        return self.bulk_insert(rows)
284
285 1
    def _build_insert_compiler(self, rows: List[Dict]):
286
        """Builds the SQL compiler for a insert query.
287
288
        Arguments:
289
            rows:
290
                A list of dictionaries, where each entry
291
                describes a record to insert.
292
293
        Returns:
294
            The SQL compiler for the insert.
295
        """
296
297
        # create model objects, we also have to detect cases
298
        # such as:
299
        #   [dict(first_name='swen'), dict(fist_name='swen', last_name='kooij')]
300
        # we need to be certain that each row specifies the exact same
301
        # amount of fields/columns
302 1
        objs = []
303 1
        field_count = len(rows[0])
304 1
        for index, row in enumerate(rows):
305 1
            if field_count != len(row):
306
                raise SuspiciousOperation((
307
                    'In bulk upserts, you cannot have rows with different field '
308
                    'configurations. Row {0} has a different field config than '
309
                    'the first row.'
310
                ).format(index))
311
312 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...
313
314
        # indicate this query is going to perform write
315 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...
316
317
        # get the fields to be used during update/insert
318 1
        insert_fields, update_fields = self._get_upsert_fields(rows[0])
319
320
        # build a normal insert query
321 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...
322 1
        query.conflict_action = self.conflict_action
323 1
        query.conflict_target = self.conflict_target
324 1
        query.index_predicate = self.index_predicate
325 1
        query.values(objs, insert_fields, update_fields)
326
327
        # use the postgresql insert query compiler to transform the insert
328
        # into an special postgresql insert
329 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...
330 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...
331
332 1
        return compiler
333
334 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...
335
        """Verifies whether this field is gonna modify something
336
        on its own.
337
338
        "Magical" means that a field modifies the field value
339
        during the pre_save.
340
341
        Arguments:
342
            model_instance:
343
                The model instance the field is defined on.
344
345
            field:
346
                The field to get of whether the field is
347
                magical.
348
349
            is_insert:
350
                Pretend whether this is an insert?
351
352
        Returns:
353
            True when this field modifies something.
354
        """
355
356
        # does this field modify someting upon insert?
357 1
        old_value = getattr(model_instance, field.name, None)
358 1
        field.pre_save(model_instance, is_insert)
359 1
        new_value = getattr(model_instance, field.name, None)
360
361 1
        return old_value != new_value
362
363 1
    def _get_upsert_fields(self, kwargs):
364
        """Gets the fields to use in an upsert.
365
366
        This some nice magic. We'll split the fields into
367
        a group of "insert fields" and "update fields":
368
369
        INSERT INTO bla ("val1", "val2") ON CONFLICT DO UPDATE SET val1 = EXCLUDED.val1
370
371
                         ^^^^^^^^^^^^^^                            ^^^^^^^^^^^^^^^^^^^^
372
                         insert_fields                                 update_fields
373
374
        Often, fields appear in both lists. But, for example,
375
        a :see:DateTime field with `auto_now_add=True` set, will
376
        only appear in "insert_fields", since it won't be set
377
        on existing rows.
378
379
        Other than that, the user specificies a list of fields
380
        in the upsert() call. That migt not be all fields. The
381
        user could decide to leave out optional fields. If we
382
        end up doing an update, we don't want to overwrite
383
        those non-specified fields.
384
385
        We cannot just take the list of fields the user
386
        specifies, because as mentioned, some fields
387
        make modifications to the model on their own.
388
389
        We'll have to detect which fields make modifications
390
        and include them in the list of insert/update fields.
391
        """
392
393 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...
394 1
        insert_fields = []
395 1
        update_fields = []
396
397 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...
398 1
            has_default = field.default != NOT_PROVIDED
399 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...
400 1
                insert_fields.append(field)
401 1
                update_fields.append(field)
402 1
                continue
403 1
            elif has_default:
404 1
                insert_fields.append(field)
405 1
                continue
406
407
            # special handling for 'pk' which always refers to
408
            # the primary key, so if we the user specifies `pk`
409
            # instead of a concrete field, we have to handle that
410 1
            if field.primary_key is True and 'pk' in kwargs:
411 1
                insert_fields.append(field)
412 1
                update_fields.append(field)
413 1
                continue
414
415 1
            if self._is_magical_field(model_instance, field, is_insert=True):
416 1
                insert_fields.append(field)
417
418 1
            if self._is_magical_field(model_instance, field, is_insert=False):
419 1
                update_fields.append(field)
420
421 1
        return insert_fields, update_fields
422
423
424 1
class PostgresManager(models.Manager):
425
    """Adds support for PostgreSQL specifics."""
426
427 1
    use_in_migrations = True
428
429 1
    def __init__(self, *args, **kwargs):
430
        """Initializes a new instance of :see:PostgresManager."""
431
432 1
        super(PostgresManager, self).__init__(*args, **kwargs)
433
434
        # make sure our back-end is set and refuse to proceed
435
        # if it's not set
436 1
        db_backend = settings.DATABASES['default']['ENGINE']
437 1
        if 'psqlextra' not in db_backend:
438
            raise ImproperlyConfigured((
439
                '\'%s\' is not the \'psqlextra.backend\'. '
440
                'django-postgres-extra cannot function without '
441
                'the \'psqlextra.backend\'. Set DATABASES.ENGINE.'
442
            ) % db_backend)
443
444
        # hook into django signals to then trigger our own
445
446 1
        django.db.models.signals.post_save.connect(
447
            self._on_model_save, sender=self.model, weak=False)
448
449 1
        django.db.models.signals.pre_delete.connect(
450
            self._on_model_delete, sender=self.model, weak=False)
451
452 1
        self._signals_connected = True
453
454 1
    def __del__(self):
455
        """Disconnects signals."""
456
457 1
        if self._signals_connected is False:
458
            return
459
460
        # django.db.models.signals.post_save.disconnect(
461
        #     self._on_model_save, sender=self.model)
462
463
        # django.db.models.signals.pre_delete.disconnect(
464
        #     self._on_model_delete, sender=self.model)
465
466 1
    def get_queryset(self):
467
        """Gets the query set to be used on this manager."""
468
469 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...
470
471 1
    def on_conflict(self, fields: List[Union[str, Tuple[str]]], action):
472
        """Sets the action to take when conflicts arise when attempting
473
        to insert/create a new row.
474
475
        Arguments:
476
            fields:
477
                The fields the conflicts can occur in.
478
479
            action:
480
                The action to take when the conflict occurs.
481
        """
482 1
        return self.get_queryset().on_conflict(fields, action)
483
484 1
    def upsert(self, conflict_target: List, fields: Dict, index_predicate: str=None) -> int:
485
        """Creates a new record or updates the existing one
486
        with the specified data.
487
488
        Arguments:
489
            conflict_target:
490
                Fields to pass into the ON CONFLICT clause.
491
492
            fields:
493
                Fields to insert/update.
494
495
            index_predicate:
496
                The index predicate to satisfy an arbiter partial index.
497
498
        Returns:
499
            The primary key of the row that was created/updated.
500
        """
501
502 1
        return self.get_queryset().upsert(conflict_target, fields, index_predicate)
503
504 1
    def upsert_and_get(self, conflict_target: List, fields: Dict):
505
        """Creates a new record or updates the existing one
506
        with the specified data and then gets the row.
507
508
        Arguments:
509
            conflict_target:
510
                Fields to pass into the ON CONFLICT clause.
511
512
            fields:
513
                Fields to insert/update.
514
515
        Returns:
516
            The model instance representing the row
517
            that was created/updated.
518
        """
519
520 1
        return self.get_queryset().upsert_and_get(conflict_target, fields)
521
522 1
    def bulk_upsert(self, conflict_target: List, rows: List[Dict]):
523
        """Creates a set of new records or updates the existing
524
        ones with the specified data.
525
526
        Arguments:
527
            conflict_target:
528
                Fields to pass into the ON CONFLICT clause.
529
530
            rows:
531
                Rows to upsert.
532
        """
533
534
        return self.get_queryset().bulk_upsert(conflict_target, rows)
535
536 1
    @staticmethod
537
    def _on_model_save(sender, **kwargs):
538
        """When a model gets created or updated."""
539
540 1
        created, instance = kwargs['created'], kwargs['instance']
541
542 1
        if created:
543 1
            signals.create.send(sender, pk=instance.pk)
544
        else:
545 1
            signals.update.send(sender, pk=instance.pk)
546
547 1
    @staticmethod
548
    def _on_model_delete(sender, **kwargs):
549
        """When a model gets deleted."""
550
551 1
        instance = kwargs['instance']
552
        signals.delete.send(sender, pk=instance.pk)
553