Opened 15 years ago
Closed 14 years ago
#8947 closed defect (fixed)
Use the timestamp as an identifier when editing a comment
Reported by: | Remy Blank | Owned by: | Remy Blank |
---|---|---|---|
Priority: | normal | Milestone: | 0.12 |
Component: | ticket system | Version: | 0.12dev |
Severity: | normal | Keywords: | |
Cc: | leho@…, michaelc@… | Branch: | |
Release Notes: | |||
API Changes: | |||
Internal Changes: |
Description
Currently, Ticket.modify_comment()
uses the comment number stored in the oldvalue
column of ticket_change
to identify the comment that is edited. Unfortunately, it seems that some "comment adders" don't set this field correctly (e.g. XmlRpcPlugin). Also, databases from old versions of Trac (prior to the introduction of the threading feature) also don't have the oldvalue
column set correctly. This prevents such comments from being edited.
The comment editing feature should be changed to use the comment timestamp as an identifier instead of the comment number.
Attachments (1)
Change History (23)
comment:1 by , 15 years ago
Cc: | added |
---|
comment:2 by , 15 years ago
comment:3 by , 15 years ago
Fixed editing of comments where the comment number is not available in the oldvalue
column of ticket_change
in [9043]. The signature of Ticket.modify_comment()
was also changed to take the comment date as an identifier instead of the comment number.
comment:4 by , 15 years ago
[9043] introduced more robustness in the case where the comment number is not available in the oldvalue
column. We could also make Ticket.save_changes()
more robust, by having it find the cnum
when it isn't provided:
-
trac/ticket/model.py
diff --git a/trac/ticket/model.py b/trac/ticket/model.py
a b 301 301 "VALUES (%s, %s, %s, %s, %s, %s)", 302 302 (self.id, when_ts, author, name, self._old[name], 303 303 self[name])) 304 305 # find cnum if it isn't provided 306 if not cnum: 307 cursor.execute("SELECT oldvalue FROM ticket_change " 308 "WHERE ticket=%s AND field='comment' " 309 "ORDER BY time DESC", (self.id,)) 310 num = 0 311 for row in cursor: 312 try: 313 num = int((row[0] or '').rsplit('.', 1)[-1]) + num 314 break 315 except ValueError: 316 num += 1 317 cnum = str(num + 1) 318 304 319 # always save comment, even if empty (numbering support for timeline) 305 320 cursor.execute("INSERT INTO ticket_change " 306 321 "(ticket,time,author,field,oldvalue,newvalue) "
This would ensure that the cnum
is also inserted when plugins "forget" the argument. Thoughts?
comment:6 by , 15 years ago
by , 15 years ago
Attachment: | 8947-auto-cnum-r9153.patch added |
---|
Automatically add cnum
if not provided in save_changes()
.
comment:7 by , 15 years ago
The patch above was refreshed for current trunk, and calculates cnum
in save_changes()
if it's not provided.
Thoughts?
follow-up: 9 comment:8 by , 15 years ago
Seems fine. What about starting to use unique multiline """ ... """
strings for the SQL?
comment:9 by , 15 years ago
Replying to cboos:
What about starting to use unique multiline
""" ... """
strings for the SQL?
Ok, I'll do that one and then we can argue endlessly about the correct style (indentation, etc) to use ;-)
comment:10 by , 15 years ago
Resolution: | → fixed |
---|---|
Status: | new → closed |
Patch applied in [9158]. Ok with the SQL style?
follow-up: 12 comment:11 by , 15 years ago
Nice! But as you expected, I'd have done it slightly differently ;-)
-
trac/ticket/model.py
283 283 # find cnum if it isn't provided 284 284 if not cnum: 285 285 num = 0 286 cursor.execute(""" \286 cursor.execute(""" 287 287 SELECT DISTINCT tc1.time,COALESCE(tc2.oldvalue,'') 288 288 FROM ticket_change AS tc1 289 289 LEFT OUTER JOIN 290 290 (SELECT time,oldvalue FROM ticket_change 291 291 WHERE field='comment') AS tc2 292 292 ON (tc1.time = tc2.time) 293 WHERE ticket=%s ORDER BY tc1.time DESC """,294 (self.id,))293 WHERE ticket=%s ORDER BY tc1.time DESC 294 """, (self.id,)) 295 295 for ts, old in cursor: 296 296 # Use oldvalue if available, else count edits 297 297 try:
- I don't think we gain anything by avoiding the initial line break,
- if we don't move the
"""
at the beginning of the line, the vertical separation between the last line of the SQL and the query parameters is not immediately obvious
follow-up: 13 comment:12 by , 15 years ago
Replying to cboos:
Nice! But as you expected, I'd have done it slightly differently ;-)
I would have been surprised if that wasn't the case. Or, to paraphrase the last book I read: "If two programmers in a group agree, that's a majority".
- if we don't move the
"""
at the beginning of the line, the vertical separation between the last line of the SQL and the query parameters is not immediately obvious
It is if your editor does syntax highlighting ;-)
Feel free to apply, I don't really mind. I'll adapt to anything (as long as I don't have to write Perl, that is).
comment:13 by , 15 years ago
Replying to rblank:
Replying to cboos:
Nice! But as you expected, I'd have done it slightly differently ;-)
I would have been surprised if that wasn't the case. Or, to paraphrase the last book I read: "If two programmers in a group agree, that's a majority".
And in our group, it's even unanimity ;-)
- if we don't move the
"""
at the beginning of the line, the vertical separation between the last line of the SQL and the query parameters is not immediately obviousIt is if your editor does syntax highlighting ;-)
Feel free to apply, I don't really mind. I'll adapt to anything (as long as I don't have to write Perl, that is).
Oh yes, here-docs, that would be even better!
-
trac/ticket/model.py
283 283 # find cnum if it isn't provided 284 284 if not cnum: 285 285 num = 0 286 cursor.execute( """\286 cursor.execute(<<EOSQL, (self.id,)) 287 287 SELECT DISTINCT tc1.time,COALESCE(tc2.oldvalue,'') 288 288 FROM ticket_change AS tc1 289 289 LEFT OUTER JOIN … … 291 291 WHERE field='comment') AS tc2 292 292 ON (tc1.time = tc2.time) 293 293 WHERE ticket=%s ORDER BY tc1.time DESC""", 294 (self.id,))294 EOSQL 295 295 for ts, old in cursor: 296 296 # Use oldvalue if available, else count edits 297 297 try:
(we should switch to this as soon as the Grand-Unification-of-Scripting-Languages happen, I'll leave a reminder note for my kids ;-) ).
More seriously, the suggested style in comment:11 is very close to the one already adopted by the TracHacks:AnnouncerPlugin (example), which is the source of inspiration for this.
Committed in r9161.
comment:14 by , 14 years ago
I am trying to understand the SQL used to determine the cnum in the patch:
SELECT DISTINCT tc1.time,COALESCE(tc2.oldvalue,'') FROM ticket_change AS tc1 LEFT OUTER JOIN (SELECT time,oldvalue FROM ticket_change WHERE field='comment') AS tc2 ON (tc1.time = tc2.time) WHERE ticket=? ORDER BY tc1.time DESC
If there are two transactions with the same timestamp but affecting different tickets, would this not be liable to return the comment values for all of the tickets? This in turn could result in the cnum being pulled from the incorrect ticket.
In addition, when ticket_change gets large, the outer join becomes very slow.
Adding "ticket=?" as a clause to the inner select seems like it would resolve both of these problems, but the statement could then be further simplified to something like the one in comment:4, e.g.:
SELECT time,COALESCE(oldvalue,'') FROM ticket_change WHERE field='comment' AND ticket=? ORDER BY time DESC;
Please let me know if there is something which I am overlooking.
comment:15 by , 14 years ago
Resolution: | fixed |
---|---|
Status: | closed → reopened |
Well spotted! For correction this could probably be fixed. Even if we now use microsecond timestamps, we might deal with older tickets with a second only time resolution (I wonder if this couldn't explain some strange numbering we've seen recently on some tickets … should have kept track of which).
If additionally the fix improves the performance, all the better! Thanks, we'll look into it.
comment:16 by , 14 years ago
The current code looks… just horribly wrong. I can't for the life of me remember why I did it that way.
/me continues trying to understand
comment:17 by , 14 years ago
Oh, right, the outer join is necessary to handle the case where a change doesn't have the 'comment'
field. But yes, a ticket=?
is missing (the same in _find_change()
).
comment:18 by , 14 years ago
So the simple query from comment:14 won't work in the case where a ticket change doesn't have a comment field (which shouldn't normally happen, but does in practice).
The query should return one row (time,oldvalue)
for every ticket comment, where oldvalue
is taken from the comment
field if it exists, or an empty string otherwise. Adding the ticket=?
to the inner query would make the current query correct, but performance is probably not stellar. I'm of course open to suggestions how this could be improved.
We could at least avoid the sub-query but I don't know if it would make a difference in performance:
SELECT DISTINCT tc1.time,COALESCE(tc2.oldvalue,'') FROM ticket_change AS tc1 LEFT OUTER JOIN ticket_change AS tc2 ON tc2.ticket = ? AND tc1.time = tc2.time AND tc2.field = 'comment' WHERE tc1.ticket = ? ORDER BY tc1.time DESC
comment:19 by , 14 years ago
Suggested fix:
-
trac/ticket/model.py
diff --git a/trac/ticket/model.py b/trac/ticket/model.py
a b class Ticket(object): 297 297 cursor.execute(""" 298 298 SELECT DISTINCT tc1.time,COALESCE(tc2.oldvalue,'') 299 299 FROM ticket_change AS tc1 300 LEFT OUTER JOIN 301 (SELECT time,oldvalue FROM ticket_change 302 WHERE field='comment') AS tc2 303 ON (tc1.time = tc2.time) 304 WHERE ticket=%s ORDER BY tc1.time DESC 305 """, (self.id,)) 300 LEFT OUTER JOIN ticket_change AS tc2 301 ON tc2.ticket=%s AND tc2.time=tc1.time 302 AND tc2.field='comment' 303 WHERE tc1.ticket=%s ORDER BY tc1.time DESC 304 """, (self.id, self.id)) 306 305 for ts, old in cursor: 307 306 # Use oldvalue if available, else count edits 308 307 try: … … class Ticket(object): 604 603 # Fallback when comment number is not available in oldvalue 605 604 num = 0 606 605 cursor.execute(""" 607 SELECT DISTINCT tc1.time,COALESCE(tc2.oldvalue,''), 608 tc2.author,COALESCE(tc2.newvalue,'') 609 FROM ticket_change AS tc1 610 LEFT OUTER JOIN 611 (SELECT time,author,oldvalue,newvalue 612 FROM ticket_change 613 WHERE field='comment') AS tc2 614 ON (tc1.time = tc2.time) 615 WHERE ticket=%s 616 ORDER BY tc1.time 617 """, (self.id,)) 606 SELECT DISTINCT tc1.time,COALESCE(tc2.oldvalue,''), 607 tc2.author,COALESCE(tc2.newvalue,'') 608 FROM ticket_change AS tc1 609 LEFT OUTER JOIN ticket_change AS tc2 610 ON tc2.ticket=%s AND tc2.time=tc1.time AND tc2.field='comment' 611 WHERE tc1.ticket=%s ORDER BY tc1.time 612 """, (self.id, self.id)) 618 613 for ts, old, author, comment in cursor: 619 614 # Use oldvalue if available, else count edits 620 615 try:
Performance should not be too bad, because the outer join should be using the (ticket, time, field)
index.
comment:20 by , 14 years ago
Cc: | added |
---|
Thank you for the fix, we are testing it out.
I can confirm that in my initial testing, simply having the join use the ticket index resolves the performance issue. In our table (110k ticket, 1.6m ticket_change), the initial statement took up to 30 seconds to complete, with the new statement returning in less than 0.01s.
comment:21 by , 14 years ago
I have applied the patch in [10630:10631] as a first step. If the performance is still bad, we can optimize from there.
The patch above is slightly wrong. If you would like to test it (i.e. if you can't update to current 0.12-stable), then please add a DISTINCT
to the second SELECT
. Or re-get the patch from comment:19, as I'll fix it shortly.
comment:22 by , 14 years ago
Resolution: | → fixed |
---|---|
Status: | reopened → closed |
This seems to be working fine now. Closing again.
Mmh, using the comment time as an identifier is more difficult that I thought, because the comment number is used just about everywhere.
So another option would be to continue using the comment number, but to "interpolate" comment numbers for comments that don't have the
oldvalue
column set. Additionally, a database upgrade could be used to set the missingoldvalue
fields. Finally,Ticket.save_change()
could determine the comment number itself if the argument isn't provided.