#25475 closed Cleanup/optimization (fixed)
Document how to to use a literal % in Func.template
Reported by: | Matt Cooper | Owned by: | nobody |
---|---|---|---|
Component: | Documentation | Version: | 1.8 |
Severity: | Normal | Keywords: | func strftime |
Cc: | josh.smeaton@… | Triage Stage: | Ready for checkin |
Has patch: | yes | Needs documentation: | no |
Needs tests: | no | Patch needs improvement: | no |
Easy pickings: | no | UI/UX: | no |
Description
I have a database function to annotate the week of a date. It worked in 1.8 but breaks in 1.9a1, failing with a "ValueError: unsupported format character 'W'" exception.
class Week(Func): function = 'strftime' template = "%(function)s('%%W',%(expressions)s)" def data_last_n_weeks(user, week_count): assert week_count > 0, "week_count must be greater than 0" assert week_count < 52, "week_count must be less than 52" today = date.today() most_recent_monday = today - timedelta(days=(today.isoweekday()-1)) start_date = most_recent_monday - timedelta(days=7*(week_count-1)) data = user.serving_set.filter(date__range=(start_date, today) ).annotate(week=Week('date', output_field=IntegerField())).values( 'week').annotate(amt=Sum('amount'), cost=Sum('cost')) return data
yields
File "/Users/[me]/Projects/[proj]/ENV/lib/python3.4/site-packages/django/db/backends/sqlite3/operations.py", line 133, in last_executed_query return sql % params ValueError: unsupported format character 'W' (0x57) at index 18
Attachments (1)
Change History (26)
comment:1 by , 9 years ago
Summary: | Database function srtftime with argument %W fails in 1.9a1 → Database function strftime with argument %W fails in 1.9a1 |
---|
comment:2 by , 9 years ago
comment:3 by , 9 years ago
Severity: | Normal → Release blocker |
---|---|
Triage Stage: | Unreviewed → Accepted |
comment:4 by , 9 years ago
I'm having trouble getting the database function you provided working on 1.8. Am I doing something wrong? I tried Poll.objects.annotate(week=Week('pub_date', output_field=models.IntegerField())
.
This gives:
File "/home/tim/code/django/django/db/backends/utils.py", line 66, in execute return self.cursor.execute(sql, params) IndexError: tuple index out of range
with sql
and params
:
SELECT "polls_poll"."id", "polls_poll"."question", "polls_poll"."pub_date", "polls_poll"."amount", "polls_poll"."cost", strftime('%W',"polls_poll"."pub_date") AS "week" FROM "polls_poll" LIMIT 21
()
.
comment:5 by , 9 years ago
I don't see anything wrong with the simplified case you wrote. I'm not in a place where I can generate the sql from my repro but I'll get it tonight.
One possible difference is that Serving.date is a DateField while I suspect Poll.pub_date is a DateTimeField. That could matter.
Also, I've only tested with SQLite - possible that this is database dependent?
comment:6 by , 9 years ago
Looking at the excerpt of the stack trace you posted I'm pretty sure it only happens on SQLite.
comment:7 by , 9 years ago
I couldn't repro the IndexError in my setup. Here are two execution logs, one on 1.8.4 and the other on 1.9.0a1 using the exact same code and (a copy of) the exact same database. This time I haven't manually doctored the output for simplification as I did earlier.
1.8.4:
(3NV) mattc ~/Projects/beer30/beersite $ ./manage.py shell Python 3.4.2 (default, Oct 19 2014, 12:51:48) [GCC 4.2.1 Compatible Apple LLVM 6.0 (clang-600.0.54)] on darwin Type "help", "copyright", "credits" or "license" for more information. (InteractiveConsole) >>> from django.contrib.auth.models import User >>> from api.views import beer_data_last_n_weeks >>> u = User.objects.get(pk=1) >>> beer_data_last_n_weeks(u, 5) [{'cost': Decimal('41.00'), 'week': 35, 'oz': Decimal('228.0')}, {'cost': Decimal('41.50'), 'week': 36, 'oz': Decimal('158.0')}, {'cost': Decimal('28.93'), 'week': 37, 'oz': Decimal('198.6')}, {'cost': Decimal('24.66'), 'week': 38, 'oz': Decimal('128.0')}, {'cost': Decimal('3.33'), 'week': 39, 'oz': Decimal('12.0')}]
1.9.0a1:
(3NV) mattc ~/Projects/beer31/beersite $ ./manage.py shell Python 3.4.3 (default, Aug 11 2015, 08:57:25) [GCC 4.2.1 Compatible Apple LLVM 6.1.0 (clang-602.0.53)] on darwin Type "help", "copyright", "credits" or "license" for more information. (InteractiveConsole) >>> from django.contrib.auth.models import User >>> from api.views import beer_data_last_n_weeks >>> u = User.objects.get(pk=1) >>> beer_data_last_n_weeks(u, 5) Traceback (most recent call last): File "<console>", line 1, in <module> File "/Users/mattc/Projects/beer31/3NV/lib/python3.4/site-packages/django/db/models/query.py", line 234, in __repr__ data = list(self[:REPR_OUTPUT_SIZE + 1]) File "/Users/mattc/Projects/beer31/3NV/lib/python3.4/site-packages/django/db/models/query.py", line 258, in __iter__ self._fetch_all() File "/Users/mattc/Projects/beer31/3NV/lib/python3.4/site-packages/django/db/models/query.py", line 1074, in _fetch_all self._result_cache = list(self.iterator()) File "/Users/mattc/Projects/beer31/3NV/lib/python3.4/site-packages/django/db/models/query.py", line 112, in __iter__ for row in compiler.results_iter(): File "/Users/mattc/Projects/beer31/3NV/lib/python3.4/site-packages/django/db/models/sql/compiler.py", line 806, in results_iter results = self.execute_sql(MULTI) File "/Users/mattc/Projects/beer31/3NV/lib/python3.4/site-packages/django/db/models/sql/compiler.py", line 852, in execute_sql cursor.execute(sql, params) File "/Users/mattc/Projects/beer31/3NV/lib/python3.4/site-packages/django/db/backends/utils.py", line 83, in execute sql = self.db.ops.last_executed_query(self.cursor, sql, params) File "/Users/mattc/Projects/beer31/3NV/lib/python3.4/site-packages/django/db/backends/sqlite3/operations.py", line 133, in last_executed_query return sql % params ValueError: unsupported format character 'W' (0x57) at index 18
Relevant code snippets:
api/views.py
class Week(Func): function = 'strftime' template = "%(function)s('%%W',%(expressions)s)" def beer_data_last_n_weeks(user, week_count): assert week_count > 0, "week_count must be greater than 0" assert week_count < 52, "week_count must be less than 52" today = date.today() most_recent_monday = today - timedelta(days=(today.isoweekday()-1)) start_date = most_recent_monday - timedelta(days=7*(week_count-1)) beer_data = user.serving_set.filter(date__range=(start_date, today) ).annotate(week=Week('date', output_field=IntegerField())).values( 'week').annotate(oz=Sum('amount'), cost=Sum('cost')) return beer_data
datamodel/models.py
class Serving(models.Model): SERVING_CHOICES = ( (0, "unknown"), (1, "tap"), (2, "bottle"), (4, "can"), (3, "growler"), ) beer = models.ForeignKey(Beer) amount = models.DecimalField(max_digits=4, decimal_places=1) date = models.DateField() type = models.SmallIntegerField(choices=SERVING_CHOICES, default=1) owner = models.ForeignKey(User) location = models.CharField(max_length=100, blank=True) cost = models.DecimalField(max_digits=5, decimal_places=2, null=True, blank=True) def __str__(self): return " ".join([str(self.amount), "oz of", self.beer.name])
comment:8 by , 9 years ago
I'm attaching a sample app based on the models you provided (I removed the foreign key to Beer
since you didn't provide that model), however, the test works without error for me. Does it work for you? If so, could you say how to modify it to reproduce the error? If not, then perhaps it's something like a difference between versions of SQLite.
System details:
Python 3.4.3 (default, Mar 5 2015, 15:15:09)
[GCC 4.8.2] on linux
sqlite3 2.6.0
by , 9 years ago
Attachment: | t25475.tar.gz added |
---|
comment:9 by , 9 years ago
Your test case does not fail on the Windows machine I currently have available (Windows 10.0.10240 with SQLite3 2.6.0, sqlite3.sqlite_version 3.8.3.1). Will check on my Mac later tonight - that's where I've observed the failure.
From looking at Apple's online man pages, 3.1.3 is shown in the sample outputs.
comment:10 by , 9 years ago
Argh, that test case doesn't fail on my Mac either. The real app still exhibits the behavior, so I'll keep digging and see what it takes to make the test case fail.
comment:11 by , 9 years ago
I'm so sorry. Somewhere between my full app and your test case there's a cutover point where this starts failing on 1.9a1, but I can't find it. I've learned quite a bit more about Pdb tonight than I really intended but to no avail.
1.8 and 1.9 both accept a double-escaped percent sign, so I'm going to update my code to that.
class Week(Func): function = 'strftime' template = "%(function)s('%%%%W',%(expressions)s)"
I'd be happy to share the whole project with someone from the core team if it would help further debugging.
comment:12 by , 9 years ago
Cc: | added |
---|
comment:13 by , 9 years ago
If you could use git bisect to find the commit where the behavior changed, that might yield some insight.
comment:14 by , 9 years ago
The exception is triggered in code I added in [4f6a7663]. However it's unclear to me why this code would be incorrect.
comment:15 by , 9 years ago
It may be because _quote_params_for_last_executed_query
is bypassing the Django cursor wrapper. When the wrapper is executing a query, it takes care of not passing params
when it is None. Maybe we should do the same dance in _quote_params_for_last_executed_query
.
comment:16 by , 9 years ago
I think the bug is rather that the initial version didn't break in 1.8! You implicitly counted on the fact the the sqlite driver accepts the following query (2 placeholder-like occurrences, one param):
cursor.execute("SELECT strftime('%W', 'date') WHERE 42=%s", (42,))
It may be because the SQLite driver only recognizes the %s
placeholder (a guess). A similar query with MySQL and DATE_FORMAT by example would produce an error.
We might fix the bug by a documentation note somewhere in custom expressions.
comment:17 by , 9 years ago
Totally fair, I probably misunderstood the docs :)
So I should have double-escaped to begin with? In retrospect that makes sense. If you guys want to doc that and close this issue, I'm good with that.
class Week(Func): function = 'strftime' template = "%(function)s('%%%%W',%(expressions)s)"
comment:18 by , 9 years ago
Component: | Database layer (models, ORM) → Documentation |
---|---|
Severity: | Release blocker → Normal |
Type: | Bug → Cleanup/optimization |
Version: | 1.9a1 → 1.8 |
comment:19 by , 8 years ago
Looking at this now, I'm not sure what the documentation note should say. Can anyone suggest something or at least give a simple description of the issue?
comment:20 by , 8 years ago
I would create a documentation PR myself, but I don't understand the issue enough to feel comfortable educating others. Summary of behavior is that when I created a custom expression, something between the code I wrote and the SQLite driver was eating some of my escape characters. I had to express a logical "%W" as "%%%%W" (four percent characters) to get it working properly. I've lost most of the context about the failure, but I recall expecting to need two percents (%%W) and being surprised that it took four.
comment:21 by , 8 years ago
I would place the note here: https://docs.djangoproject.com/en/1.10/ref/models/expressions/#django.db.models.Func.template
Something like (feel free to reformulate!):
If you need a literal %
character to reach the database, you will need to quadruple it in the template
attribute because the string will be extrapoled twice (once during the template extrapolation in as_sql()
, and once in the final SQL extrapolation with the query parameters in the database cursor).
comment:22 by , 8 years ago
Has patch: | set |
---|---|
Summary: | Database function strftime with argument %W fails in 1.9a1 → Document how to to use a literal % in Func.template |
I think we need to re-escape % after executing
template % self.extra
inFunc.as_sql
.