-
Notifications
You must be signed in to change notification settings - Fork 26
conform SQLCompiler.execute_sql() and results_iter() to Django's API #70
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,6 @@ | ||
from django.core.exceptions import EmptyResultSet, FieldDoesNotExist, FullResultSet | ||
from django.db import DatabaseError, IntegrityError, NotSupportedError | ||
from django.db.models import NOT_PROVIDED, Count, Expression | ||
from django.db.models import Count, Expression | ||
from django.db.models.aggregates import Aggregate | ||
from django.db.models.constants import LOOKUP_SEP | ||
from django.db.models.sql import compiler | ||
|
@@ -23,14 +23,21 @@ def execute_sql( | |
# QuerySet.count() | ||
if self.query.annotations == {"__count": Count("*")}: | ||
return [self.get_count()] | ||
# Specify columns if there are any annotations so that annotations are | ||
# computed via $project. | ||
columns = self.get_columns() if self.query.annotations else None | ||
|
||
columns = self.get_columns() | ||
try: | ||
query = self.build_query(columns) | ||
query = self.build_query( | ||
# Avoid $project (columns=None) if unneeded. | ||
columns if self.query.annotations or not self.query.default_cols else None | ||
) | ||
except EmptyResultSet: | ||
return None | ||
return query.fetch() | ||
return iter([]) if result_type == MULTI else None | ||
|
||
return ( | ||
(self._make_result(row, columns) for row in query.fetch()) | ||
if result_type == MULTI | ||
else self._make_result(next(query.fetch()), columns) | ||
) | ||
|
||
def results_iter( | ||
self, | ||
|
@@ -43,37 +50,23 @@ def results_iter( | |
Return an iterator over the results from executing query given | ||
to this compiler. Called by QuerySet methods. | ||
""" | ||
columns = self.get_columns() | ||
|
||
if results is None: | ||
# QuerySet.values() or values_list() | ||
try: | ||
results = self.build_query(columns).fetch() | ||
except EmptyResultSet: | ||
results = [] | ||
results = self.execute_sql(MULTI, chunked_fetch=chunked_fetch, chunk_size=chunk_size) | ||
|
||
converters = self.get_converters(columns) | ||
for entity in results: | ||
yield self._make_result(entity, columns, converters, tuple_expected=tuple_expected) | ||
fields = [s[0] for s in self.select[0 : self.col_count]] | ||
converters = self.get_converters(fields) | ||
rows = results | ||
if converters: | ||
rows = self.apply_converters(rows, converters) | ||
if tuple_expected: | ||
rows = map(tuple, rows) | ||
return rows | ||
|
||
def has_results(self): | ||
return bool(self.get_count(check_exists=True)) | ||
|
||
def get_converters(self, expressions): | ||
converters = {} | ||
for name_expr in expressions: | ||
try: | ||
name, expr = name_expr | ||
except TypeError: | ||
# e.g., Count("*") | ||
continue | ||
backend_converters = self.connection.ops.get_db_converters(expr) | ||
field_converters = expr.get_db_converters(self.connection) | ||
if backend_converters or field_converters: | ||
converters[name] = backend_converters + field_converters | ||
return converters | ||
|
||
def _make_result(self, entity, columns, converters, tuple_expected=False): | ||
def _make_result(self, entity, columns): | ||
""" | ||
Decode values for the given fields from the database entity. | ||
|
||
|
@@ -82,7 +75,6 @@ def _make_result(self, entity, columns, converters, tuple_expected=False): | |
""" | ||
result = [] | ||
for name, col in columns: | ||
field = col.field | ||
column_alias = getattr(col, "alias", None) | ||
obj = ( | ||
# Use the related object... | ||
|
@@ -91,16 +83,7 @@ def _make_result(self, entity, columns, converters, tuple_expected=False): | |
if column_alias is not None and column_alias != self.collection_name | ||
else entity | ||
) | ||
value = obj.get(name, NOT_PROVIDED) | ||
if value is NOT_PROVIDED: | ||
value = field.get_default() | ||
elif converters: | ||
# Decode values using Django's database converters API. | ||
for converter in converters.get(name, ()): | ||
value = converter(value, col, self.connection) | ||
result.append(value) | ||
if tuple_expected: | ||
result = tuple(result) | ||
result.append(obj.get(name, col.field.get_default())) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I see you added the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, there was. A JSONField test, don't remember the name (not_provided wasn't a json). But basically I copied the logic from Django. It is not that deep like types changes or so. |
||
return result | ||
|
||
def check_query(self): | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we lost an optimization here where
$project
is omitted unless needed (i.e. there are annotations).For example, the first query in
basic.tests.ModelLookupTest.test_equal_lookup
went from:to
We could fix it by making
self.get_columns() if self.query.annotations else None
tobuild_query()
but there might be a better place to put it.Adding a test to the Django fork for this at some point would probably be a good idea.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I added this new layer thinking that is not a big deal. If I change the code a bit, the old behavior is preserved. So, I will change this code in order to keep the previous behavior.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure how much it matters performance-wise, but particularly when debugging queries, I think it's cleaner not to include $project if it's not needed, as long as the solution for doing so doesn't get super complicated.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it's not. As you mention above, we need to pass a None to build query if not self.query.annotation. Running the test, will update soon.