Skip to content

Commit 4b2e643

Browse files
committed
Commenting and fixes to FK validation
1 parent c1a4082 commit 4b2e643

6 files changed

Lines changed: 152 additions & 52 deletions

File tree

morango/models/core.py

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,10 @@
44
import json
55
import logging
66
import uuid
7-
from functools import reduce
87

9-
from functools import reduce
10-
from collections import namedtuple
118
from collections import defaultdict
9+
from collections import namedtuple
10+
from functools import reduce
1211

1312
from django.core import exceptions
1413
from django.db import connection
@@ -850,6 +849,13 @@ def delete(
850849
return collector.delete()
851850

852851
def cached_clean_fields(self, fk_lookup_cache):
852+
"""
853+
Immediately validates all fields, but uses a cache for foreign key (FK) lookups to reduce
854+
repeated queries for many records with the same FK
855+
856+
:param fk_lookup_cache: A dictionary to use as a cache to prevent querying the database if a
857+
FK exists in the cache, having already been validated
858+
"""
853859
excluded_fields = []
854860
fk_fields = [
855861
field for field in self._meta.fields if isinstance(field, models.ForeignKey)
@@ -883,10 +889,11 @@ def cached_clean_fields(self, fk_lookup_cache):
883889

884890
def deferred_clean_fields(self):
885891
"""
886-
Calls `.clean_fields()` but excludes all foreign key fields and instead adds them to the
887-
`fk_references` dictionary for deferred batch processing
892+
Calls `.clean_fields()` but excludes all foreign key fields and instead returns them as a
893+
dictionary for deferred batch processing
888894
889-
:param fk_references: A dictionary passed by reference
895+
:return: A dictionary containing lists of `ForeignKeyReference`s keyed by the name of the
896+
model being referenced by the FK
890897
"""
891898
excluded_fields = []
892899
deferred_fks = defaultdict(list)

morango/sync/backends/base.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,22 @@ def _create_placeholder_list(self, fields, db_values):
2121
def _bulk_full_record_upsert(self, cursor, table_name, fields, db_values):
2222
raise NotImplementedError("Subclass must implement this method.")
2323

24+
def _bulk_insert(self, cursor, table_name, fields, db_values):
25+
placeholder_str = ", ".join(
26+
self._create_placeholder_list(fields, db_values)
27+
).replace("'", "")
28+
fields_str = str(tuple(str(f.attname) for f in fields)).replace("'", "")
29+
insert = """
30+
INSERT INTO {table_name} {fields}
31+
VALUES {placeholder_str}
32+
""".format(
33+
table_name=table_name, fields=fields_str, placeholder_str=placeholder_str
34+
)
35+
cursor.execute(insert, db_values)
36+
37+
def _bulk_update(self, cursor, table_name, fields, db_values):
38+
raise NotImplementedError("Subclass must implement this method.")
39+
2440
def _dequeuing_delete_rmcb_records(self, cursor, transfersession_id):
2541
# delete all RMCBs which are a reverse FF (store version newer than buffer version)
2642
delete_rmcb_records = """DELETE FROM {rmcb}

morango/sync/backends/postgres.py

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,8 @@ def _prepare_set_casted_values(self, fields, source_table):
5252
def _bulk_full_record_upsert(self, cursor, table_name, fields, db_values):
5353
pk = get_pk_field(fields)
5454

55-
insert = """
55+
cte_name = "new_values"
56+
upsert = """
5657
{cte},
5758
updated as
5859
(
@@ -66,21 +67,18 @@ def _bulk_full_record_upsert(self, cursor, table_name, fields, db_values):
6667
SELECT {select_fields}
6768
FROM {cte_name} cte
6869
WHERE cte.{pk_field}::{pk_type} NOT IN (SELECT {pk_field} FROM updated)
69-
"""
70-
71-
cte_name = "new_values"
72-
insert = insert.format(
70+
""".format(
7371
cte=self._prepare_with_values(cte_name, fields, db_values),
7472
cte_name=cte_name,
7573
table_name=table_name,
7674
fields=str(tuple(str(f.column) for f in fields)).replace("'", ""),
77-
set_values=self._prepare_set_casted_values(fields, cte_name),
75+
set_values=self._prepare_set_casted_values(fields, "cte"),
7876
select_fields=self._prepare_casted_fields(fields),
7977
pk_field=pk.column,
8078
pk_type=pk.rel_db_type(self.connection),
8179
)
8280
# use DB-APIs parameter substitution (2nd parameter expects a sequence)
83-
cursor.execute(insert, db_values)
81+
cursor.execute(upsert, db_values)
8482

8583
def _bulk_update(self, cursor, table_name, fields, db_values):
8684
pk = get_pk_field(fields)
@@ -99,7 +97,7 @@ def _bulk_update(self, cursor, table_name, fields, db_values):
9997
cte_name=cte_name,
10098
table_name=table_name,
10199
fields=str(tuple(str(f.column) for f in fields)).replace("'", ""),
102-
set_values=self._prepare_set_casted_values(fields, cte_name),
100+
set_values=self._prepare_set_casted_values(fields, "cte"),
103101
pk_field=pk.column,
104102
pk_type=pk.rel_db_type(self.connection),
105103
)

morango/sync/backends/sqlite.py

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,17 +29,32 @@ def _bulk_full_record_upsert(self, cursor, table_name, fields, db_values):
2929
for x in range(0, len(placeholder_list), num_of_rows_able_to_insert)
3030
]
3131
# insert data chunks
32-
fields = str(tuple(str(f.attname) for f in fields)).replace("'", "")
32+
fields_str = str(tuple(str(f.attname) for f in fields)).replace("'", "")
3333
for values, params in zip(value_chunks, placeholder_chunks):
3434
placeholder_str = ", ".join(params).replace("'", "")
35-
insert = """REPLACE INTO {table_name} {fields}
36-
VALUES {placeholder_str}
35+
insert = """
36+
REPLACE INTO {table_name} {fields}
37+
VALUES {placeholder_str}
3738
""".format(
38-
table_name=table_name, fields=fields, placeholder_str=placeholder_str
39+
table_name=table_name,
40+
fields=fields_str,
41+
placeholder_str=placeholder_str,
3942
)
4043
# use DB-APIs parameter substitution (2nd parameter expects a sequence)
4144
cursor.execute(insert, values)
4245

46+
def _bulk_insert(self, cursor, table_name, fields, db_values):
47+
num_of_rows_able_to_insert = calculate_max_sqlite_variables() // len(fields)
48+
num_of_values_able_to_insert = num_of_rows_able_to_insert * len(fields)
49+
value_chunks = [
50+
db_values[x : x + num_of_values_able_to_insert]
51+
for x in range(0, len(db_values), num_of_values_able_to_insert)
52+
]
53+
for value_chunk in value_chunks:
54+
super(SQLWrapper, self)._bulk_insert(
55+
cursor, table_name, fields, value_chunk
56+
)
57+
4358
def _bulk_update(self, cursor, table_name, fields, db_values):
4459
"""
4560
Example query:
@@ -90,7 +105,6 @@ def _bulk_update(self, cursor, table_name, fields, db_values):
90105
),
91106
)
92107
# use DB-APIs parameter substitution (2nd parameter expects a sequence)
93-
print("EXECUTE BULK UPDATE:", update, params)
94108
cursor.execute(update, params)
95109

96110
def _dequeuing_merge_conflict_rmcb(self, cursor, transfersession_id):

morango/sync/backends/utils.py

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,12 @@ class TemporaryTable(object):
6767
__slots__ = ("connection", "name", "fields", "backend", "_meta")
6868

6969
def __init__(self, connection, name, **fields):
70+
"""
71+
:param connection: A database connection object
72+
:param name: A str name for the table in the database
73+
:param fields: Keyword arguments are assumed to be fields for defining the schema of the
74+
temporary table
75+
"""
7076
self.connection = connection
7177
self.name = name
7278
self.fields = []
@@ -86,13 +92,27 @@ def __exit__(self, exc_type, exc_val, exc_tb):
8692

8793
@property
8894
def sql_name(self):
95+
"""
96+
:return: The name actual name of the table used in the DB, prefixed to avoid collisions
97+
"""
8998
return self.connection.ops.quote_name("t_{}".format(self.name))
9099

100+
def get_field(self, name):
101+
"""
102+
:param name: A str of the name of which field to find
103+
:return: The field object
104+
"""
105+
return next(f for f in self.fields if f.name == name)
106+
91107
def create(self):
108+
"""
109+
Creates the temporary table within the database
110+
"""
92111
fields = []
93112
params = []
94113
with self.connection.schema_editor() as schema_editor:
95114
for field in self.fields:
115+
# generates the SQL expression for the table column
96116
field_sql, field_params = schema_editor.column_sql(
97117
self, field, include_default=True
98118
)
@@ -103,16 +123,24 @@ def create(self):
103123
self.backend._create_temporary_table(c, self.sql_name, fields, params)
104124

105125
def drop(self):
126+
"""
127+
Drops the temporary table within the database
128+
"""
106129
with self.connection.cursor() as c:
107130
c.execute("DROP TABLE IF EXISTS {name}".format(name=self.sql_name))
108131

109132
def bulk_insert(self, values):
133+
"""
134+
Bulk inserts a list of records into the temporary table
135+
136+
:param values: A list of dictionaries containing data to insert, keyed by field name
137+
"""
110138
params = []
111139
for value_dict in values:
112140
for field in self.fields:
113141
params.append(value_dict.get(field.attname))
114142
with self.connection.cursor() as c:
115-
self.backend._bulk_full_record_upsert(c, self.sql_name, self.fields, params)
143+
self.backend._bulk_insert(c, self.sql_name, self.fields, params)
116144

117145
class Meta:
118146
"""

0 commit comments

Comments
 (0)