- 🇺🇸United States mfb San Francisco
Regardless of this patch being applied, memory usage is surprisingly high if you select all rows of a large table without actually fetching them, for example:
echo format_size(memory_get_usage()), PHP_EOL; // prints 25.8 MB $result = \Drupal::database()->select('search_index') ->fields('search_index') ->execute(); echo format_size(memory_get_usage()), PHP_EOL; // prints 111.59 MB unset($result); echo format_size(memory_get_usage()), PHP_EOL; // prints 26.04 MB
And it takes more time to execute() the bigger the table. So is the whole table being read from the database even if you don't fetch it?
- Status changed to Needs work
almost 2 years ago 6:50am 23 January 2023 - 🇮🇹Italy mondrake 🇮🇹
#42 and #43 are very interesting, thanks @mfb.
It’s worth trying to do that in the MR and see the impact?
- 🇺🇸United States mfb San Francisco
Could be worth experimenting but according to https://www.php.net/manual/en/mysqlinfo.concepts.buffering.php USE_BUFFERED is needed if you want to keep executing other queries while iterating the results; and sounds like it should be more performant for typical queries.
- 🇺🇸United States mfb San Francisco
Yeah, no that doesn't work, but at least this little side tangent allowed me to learn about buffered queries
- Status changed to RTBC
almost 2 years ago 8:51am 23 January 2023 - 🇮🇹Italy mondrake 🇮🇹
So #44 is not a good idea, reverted and returned to RTBC.
#45is right, we can't do queries while an unbuffered query is active, see this clearly in Drupal\Tests\mysql\Kernel\mysql\SyntaxTest::testAllowSquareBrackets from the test run:
PHPUnit 9.5.26 by Sebastian Bergmann and contributors. Testing Drupal\Tests\mysql\Kernel\mysql\SyntaxTest E 1 / 1 (100%) Time: 00:00.993, Memory: 4.00 MB There was 1 error: 1) Drupal\Tests\mysql\Kernel\mysql\SyntaxTest::testAllowSquareBrackets Drupal\Core\Database\DatabaseExceptionWrapper: SQLSTATE[HY000]: General error: 2014 Cannot execute queries while other unbuffered queries are active. Consider using PDOStatement::fetchAll(). Alternatively, if your code is only ever going to run against mysql, you may enable query buffering by setting the PDO::MYSQL_ATTR_USE_BUFFERED_QUERY attribute.: select "name" from "test58670028test" where "name" = :value; Array ( [:value] => [square] ) /var/www/html/core/modules/mysql/src/Driver/Database/mysql/ExceptionHandler.php:46 /var/www/html/core/lib/Drupal/Core/Database/Connection.php:812 /var/www/html/core/tests/Drupal/KernelTests/Core/Database/DriverSpecificSyntaxTestBase.php:34 /var/www/html/vendor/phpunit/phpunit/src/Framework/TestResult.php:728
It would be hard to change that generally for Drupal now. So this issue could help for specific cases when passing a value for PDO::MYSQL_ATTR_USE_BUFFERED_QUERY in the query $options['pdo'] when iterating through large data sets in a controlled manner, i.e. ensuring no other queries are run in the in-between the cursor navigation.
- Status changed to Needs work
almost 2 years ago 7:49am 28 January 2023 - Status changed to Needs review
almost 2 years ago 12:59pm 28 January 2023 - Status changed to RTBC
almost 2 years ago 9:19pm 28 January 2023 - 🇳🇱Netherlands daffie
Both points of @catch have been addressed.
Back to RTBC. - Status changed to Needs review
almost 2 years ago 5:01pm 30 January 2023 - 🇮🇹Italy mondrake 🇮🇹
I have the mysqli driver failing tests with the patch here applied. Setting to NR while I figure out whether the problem is here or in the mysqli driver code.
- Assigned to mondrake
- Status changed to Needs work
almost 2 years ago 5:46pm 30 January 2023 - 🇮🇹Italy mondrake 🇮🇹
No, the changes are not BC, because
::markResultsetIterable
and friends are not called by downstream code ::execute and ::fetch* methods currently, obviously. So we'll need to deprecateStatementWrapper
and introduce a new statement class instead, e.g. StatementWrapperIterator. - Status changed to Needs review
almost 2 years ago 8:18pm 31 January 2023 - Status changed to Needs work
almost 2 years ago 9:44pm 31 January 2023 - Status changed to Needs review
almost 2 years ago 9:48pm 31 January 2023 - 🇮🇹Italy mondrake 🇮🇹
I just picked for testing a version of pgsql too old for D10.
- Issue was unassigned.
- Status changed to RTBC
almost 2 years ago 2:02pm 3 February 2023 - 🇳🇱Netherlands daffie
All the code changes look good to me.
The IS and the CR are in order.
For me it is RTBC. - Status changed to Needs review
almost 2 years ago 2:54pm 3 February 2023 - 🇬🇧United Kingdom alexpott 🇪🇺🌍
Isn't the smallest change here
public function getIterator() { while (($row = $this->fetch()) !== FALSE) { yield $row; } }
We don't need any deprecations - we don't couple this any more or any less to the underlying PDO Statement.
- Assigned to mondrake
- Status changed to Needs work
almost 2 years ago 4:53pm 3 February 2023 - Issue was unassigned.
- 🇮🇹Italy mondrake 🇮🇹
PHPUnit 9.5.26 by Sebastian Bergmann and contributors. 13 14 Runtime: PHP 8.1.14 15 Configuration: /home/runner/work/d8-unit/d8-unit/core/phpunit.xml.dist 18 19 Testing Drupal\Tests\migrate\Kernel\TrackChangesTest 20 F 1 / 1 (100%) 21 22 Time: 00:00.671, Memory: 10.00 MB 23 24 There was 1 failure: 25 26 1) Drupal\Tests\migrate\Kernel\TrackChangesTest::testTrackChanges 27 Migration failed with source plugin exception: Cannot rewind a generator that was already run in /home/runner/work/d8-unit/d8-unit/core/modules/migrate/src/Plugin/migrate/source/SourcePluginBase.php line 384 28 Failed asserting that two strings are equal. 29 --- Expected 30 +++ Actual 31 @@ @@ 32 -'status' 33 +'error' 34 35 /home/runner/work/d8-unit/d8-unit/vendor/phpunit/phpunit/src/Framework/Constraint/Equality/IsEqual.php:96 36 /home/runner/work/d8-unit/d8-unit/core/modules/migrate/tests/src/Kernel/MigrateTestBase.php:205 37 /home/runner/work/d8-unit/d8-unit/core/modules/migrate/src/MigrateExecutable.php:191 36 /home/runner/work/d8-unit/d8-unit/core/modules/migrate/tests/src/Kernel/MigrateTestBase.php:177 37 /home/runner/work/d8-unit/d8-unit/core/modules/migrate/tests/src/Kernel/TrackChangesTest.php:106 38 /home/runner/work/d8-unit/d8-unit/vendor/phpunit/phpunit/src/Framework/TestResult.php:728
- Status changed to RTBC
almost 2 years ago 10:55pm 3 February 2023 - 🇮🇹Italy mondrake 🇮🇹
So #62 fails on this
/** * Rewinds the iterator. * * Implementation of \Iterator::rewind() - subclasses of SourcePluginBase * should implement initializeIterator() to do any class-specific setup for * iterating source records. */ #[\ReturnTypeWillChange] public function rewind() { $this->getIterator()->rewind(); $this->next(); }
when rewinding the StatementWrapper iterator,
\ArrayIterator
(current HEAD) is rewindable,Generator
(after the patch) is not.https://www.php.net/manual/en/generator.rewind.php
If iteration has already begun, this will throw an exception.
So for me #62 is breaking BC. Back to RTBC for the MR.
- 🇬🇧United Kingdom alexpott 🇪🇺🌍
@mondrake hmmm.... but in the MR
/** * {@inheritdoc} * * @internal This method should not be called directly. */ public function rewind(): void { // Nothing to do: our DatabaseStatement can't be rewound. }
So i'm not sure that that test is actually real. You can't rewind the MR either. it just does error properly.
- 🇮🇹Italy mondrake 🇮🇹
The MR no-ops. The Generator throws an exception.
I do not know why the migrate plugin rewinds the iterator. But no matter why, HEAD and MR are fine, #62 fails. That’s enough for me to say #62 is not BC.
Note: StatementPrefetch (used by Sqlite) implements \Iterator and its rewind() method is exactly equal to the MR (actually, copy/pasted)
- 🇬🇧United Kingdom alexpott 🇪🇺🌍
I do not know why the migrate plugin rewinds the iterator.
Yeah but what it is doing fails. Like silently. So it's bogus API.
- Assigned to mondrake
- Status changed to Needs work
almost 2 years ago 7:05am 4 February 2023 - 🇮🇹Italy mondrake 🇮🇹
Yes. To fix that, though, we need to fix the bogus API and the bogus usage of the API.
IMHO we can do the former here, taking care also of StatementPrefetch, but as a deprecation, because otherwise we introduce a BC break.
The latter, though, should be a followup.
- 🇮🇹Italy mondrake 🇮🇹
Split the \Iterator related methods in a trait, so that the new class uses it but also StatementPrefetch can start using it. Added a deprecation ignore for the statement rewinding error thrown by TrackChangesTest that should be fixed separately.
- Issue was unassigned.
- Status changed to Needs review
almost 2 years ago 1:59pm 5 February 2023 - 🇮🇹Italy mondrake 🇮🇹
Ended up rewriting entirely StatementPrefetchIterator, benefitting from the StatementIteratorTrait - now both StatementWrapperIterator and StatementPrefetchIterator share the \Iterator implementation code. In the process, added a test for
fetchAllAssoc()
to FetchTest. - 🇮🇹Italy mondrake 🇮🇹
Filed 🐛 Drupal\migrate\Plugin\migrate\source\SourcePluginBase::rewind() is rewinding database statements Fixed as a separate issue.
- 🇮🇹Italy mondrake 🇮🇹
Couple of tweaks. Now the \Iterator methods (
current()
,valid()
,key()
etc.) are implemented ONLY in the iterator trait code and not used anywhere by the statement code.In my opinion, these methods should be there only to allow PHP internals to execute the
foreach()
loop on the object implementing \Iterator. They should *not* be called directly by consuming code. - Status changed to Needs work
over 1 year ago 11:47am 26 February 2023 - 🇳🇱Netherlands daffie
The MR looks good.
The IS and the CR need to be updated.
- Status changed to Needs review
over 1 year ago 11:44am 27 February 2023 - 🇮🇹Italy mondrake 🇮🇹
Updated IS and issue title. Appreciate a review before going for the CR update.
- 🇳🇱Netherlands daffie
@mondrake: Could you address my remark on calling markResultsetIterable() twice gives a unexpected result. The rest of the MR looks great.
- 🇮🇹Italy mondrake 🇮🇹
Thanks @daffie for finding #78. All comments in MR addressed.
- Status changed to Needs work
over 1 year ago 4:26pm 27 February 2023 - 🇬🇧United Kingdom alexpott 🇪🇺🌍
I still think we are taking the wrong more disruptive approach here. as pointed out in #66 the bit on #62 that fails is actually failing on HEAD it's just not telling us it is because we've incorrectly implemented the API. I think we should fix that here rather then introducing more disruption to correct usages.
- Status changed to Needs review
over 1 year ago 4:35pm 27 February 2023 - 🇮🇹Italy mondrake 🇮🇹
I do not think we are disrupting anything. We're improving and cleaning up the statement classes, rather. SQLite has code that has been useless for 15 years. Non-core drivers (are there many?) can still use the previous implementations until Drupal 11 comes. But, obviously that is my opinion. Please mark this as won't fix if you disagree so not to spend more time on moonwatching.
- 🇬🇧United Kingdom alexpott 🇪🇺🌍
This code is a big performance improvement for SQLite so we need to do something. I just think that fixing this should not require us to refactor our statement wrapper classes yet again.
- Status changed to RTBC
over 1 year ago 8:26am 28 February 2023 - 🇳🇱Netherlands daffie
All code changes look good to me.
I have updated the CR for SQLite.
For me it is RTBC. - 🇬🇧United Kingdom alexpott 🇪🇺🌍
The current fix still allows the bogus rewind to occur in the migrate code. Doing a no op is not a fix.
- Status changed to Needs work
over 1 year ago 9:01am 28 February 2023 - 🇬🇧United Kingdom alexpott 🇪🇺🌍
This MR adds and skips a deprecation. Without an on issue discussion as to whether this is correct here. In my opinion it is not correct. If we are to go the way of the most recent MRs then we need to fix the code that triggers the deprecation in core. Because adding it to the skip list makes it harder for code that triggers it to discover.
- Status changed to Postponed
over 1 year ago 9:31am 28 February 2023 - 🇮🇹Italy mondrake 🇮🇹
Let's postpone on 🐛 Drupal\migrate\Plugin\migrate\source\SourcePluginBase::rewind() is rewinding database statements Fixed . Solving that will remove the need to skip the deprecation here, and hopefully can be done independently from this issue.
- Status changed to Needs review
over 1 year ago 5:52pm 28 February 2023 - 🇮🇹Italy mondrake 🇮🇹
... or, alternatively, fail when a statement is tried to be rewound (deprecation error now, excpetion in d11), which I would prefer. See MR.
- Status changed to RTBC
over 1 year ago 10:17pm 28 February 2023 - 🇳🇱Netherlands daffie
🐛 Drupal\migrate\Plugin\migrate\source\SourcePluginBase::rewind() is rewinding database statements Fixed has landed and the MR has been updated for it.
Back to RTBC. - 🇬🇧United Kingdom alexpott 🇪🇺🌍
There's no need for all the change to issue a deprecation or even better an exception. If you iterate twice over a statement that's an error and your code is not working the way you think it is. In my opinion there is no need for an deprecation in this instance - we can move straight to an exception.
But also adding the deprecation/exception is not part of fixing the memory issue that this issue was originally filed for. That's fixed by #87 and it adds a test that proves consistent rewind behaviour on all core drivers.
- Status changed to Needs review
over 1 year ago 9:06am 1 March 2023 - 🇬🇧United Kingdom alexpott 🇪🇺🌍
Here's a patch that issues a deprecation on rewind without all the API change in the MR.
- 🇬🇧United Kingdom catch
#92 looks a lot more straightforward. I think if we wanted to do the broader changes in the MR anyway, we could look at a follow-up task for that.
- 🇮🇹Italy mondrake 🇮🇹
Please correct me if I am wrong...
More conceptual than anything, but if I read correctly #92 will throw deprecation only if a cursor has been fully fetched. But, in #65, we found that
rewinding the StatementWrapper iterator,
\ArrayIterator
(current HEAD) is rewindable,Generator
(after the patch) is not.https://www.php.net/manual/en/generator.rewind.php
If iteration has already begun, this will throw an exception.
Aren't we getting different results in #92 for the case of full fetching vs. the case of partial fetching? Maybe we need a separate test for that?
- 🇬🇧United Kingdom alexpott 🇪🇺🌍
@mondrake so your comment got me to thinking about writing a test for that.
public function testStatementRewindForEach() { $statement = $this->connection->query('SELECT * FROM {test}'); $count = 0; foreach ($statement as $row) { $count++; $this->assertNotNull($row); if ($count > 2) { break; } } $this->assertGreaterThan(0, $count); // Continue iterating through the same statement. $count = 0; foreach ($statement as $row) { $count++; $this->assertNotNull($row); } $this->assertSame(2, $count); }
Both the MR and patch fail this test and incorrect trigger a deprecation. I think we should do #87 and add the above test and then, maybe, file a follow-up to think about how to do the deprecation or exception on rewind. DB statements in Drupal have not been rewindable for years and only our error in #3174662: Encapsulate \PDOStatement instead of extending from it → made this possible for MySQL and Postgres... but not SQLite.
- 🇬🇧United Kingdom alexpott 🇪🇺🌍
When you change the test in #95 to assert on actual counts it reveals an existing bug in resumption of looping around a statement in \Drupal\Core\Database\StatementPrefetch - ie. SQLite. I;ve added the test here and added @todo for SQLite because I think adding the test coverage here is more important than fixing the existing bug here.
- 🇮🇹Italy mondrake 🇮🇹
+++ b/core/tests/Drupal/KernelTests/Core/Database/StatementTest.php @@ -43,4 +43,47 @@ public function testRepeatedInsertStatementReuse() { + /** + * Tests a follow-on iteration on a statement using foreach. + */ + public function testStatementForEachTwice() { + $statement = $this->connection->query('SELECT * FROM {test}'); + + $count = 0; + foreach ($statement as $row) { + $count++; + $this->assertNotNull($row); + if ($count > 2) { + break; + } + } + $this->assertSame(3, $count); + + // Continue iterating through the same statement. + foreach ($statement as $row) { + $count++; + $this->assertNotNull($row); + } + // @todo Fix SQLite bug. + $this->assertSame($this->connection->databaseType() === 'sqlite' ? 5 : 4, $count); + }
I think this is wrong expectation... you cannot start a
foreach
, break, and resume with another
foreach
from where you left.Internally PHP always calls (unconditionally, I assume)
rewind()
when starting a foreach loop,This is the first method called when starting a foreach loop. It will not be executed after foreach loops.
- Assigned to mondrake
- Status changed to Needs work
over 1 year ago 2:04pm 3 March 2023 - Issue was unassigned.
- Status changed to Needs review
over 1 year ago 5:31pm 3 March 2023 - 🇮🇹Italy mondrake 🇮🇹
The MR now incorporates tests from #96, with the assumption that
- rewinding a non-empty statement after fetching has started throws a deprecation and no-ops in D10, will throw exception in D11
- rewinding an empty statement no-ops
The beauty of the MR is that since all the iteration code is in the trait, fixing it there fixes both statment classes in core. I like the trait and am planning to use in in contrib for mysqli and DruDbal drivers, that require their own statement classes anyway (they're not PDO based).
- Status changed to RTBC
over 1 year ago 5:56pm 3 March 2023 - 🇮🇹Italy mondrake 🇮🇹
Thank you.
I wonder whether we should use this occasion, we have two new statement classes, also to have a new StatementInterface, fully typehinted, so to finally leave PHP 5 behnd.
- 🇳🇱Netherlands daffie
The deprecation has been removed.
The IS has been updated.
Back to RTBC. - Status changed to Needs work
over 1 year ago 11:41pm 8 March 2023 - 🇬🇧United Kingdom alexpott 🇪🇺🌍
Added a review to the MR. I have some concerns about scope. For example code is removed from \Drupal\sqlite\Driver\Database\sqlite\Statement that is not related to fixing this. We're also changing calls to low level PDO functions that are not part of fixing this change.
- 🇬🇧United Kingdom alexpott 🇪🇺🌍
The conversions from switches to matches could happen in a follow-up.
- Status changed to RTBC
over 1 year ago 8:15am 9 March 2023 - 🇮🇹Italy mondrake 🇮🇹
code is removed from \Drupal\sqlite\Driver\Database\sqlite\Statement that is not related to fixing this
no it is related to fixing this, because that piece of code is doing strange things with the resultset that are incompatible with the new trait that keeps the iterator related properties private on the base class. But yes, that can be removed independently from this, I opened a spin-off for that and we'll have to wait that before continuing here.
TIL
match()
. Thanks!Postponed on 📌 Remove obsolete code from Drupal\sqlite\Driver\Database\sqlite\Statement Fixed .
- Status changed to Postponed
over 1 year ago 8:18am 9 March 2023 - Assigned to mondrake
- Status changed to Needs work
over 1 year ago 1:17pm 9 March 2023 - Issue was unassigned.
- Status changed to Needs review
over 1 year ago 11:45am 10 March 2023 - 🇮🇹Italy mondrake 🇮🇹
Addressed @alexpott's feedback inline in the MR, with the exception of two points where I'd like to have a further thinking before doing changes. Thanks!
- Status changed to Needs work
over 1 year ago 10:55pm 10 March 2023 - Status changed to Needs review
over 1 year ago 7:30am 11 March 2023 - Status changed to RTBC
over 1 year ago 8:18am 11 March 2023 - Assigned to mondrake
- Status changed to Needs work
over 1 year ago 9:31am 11 March 2023 - 🇮🇹Italy mondrake 🇮🇹
Found an issue with latest changes, which uncovers a lack of test coverage.
- Issue was unassigned.
- Status changed to Needs review
over 1 year ago 1:47pm 11 March 2023 - 🇮🇹Italy mondrake 🇮🇹
The problem was with '0' being confused with FALSE in fetchField, i.e. a valid scalar interpreted as end of recordset.
I was also very tempted to add a trait to split out the code that emulates the conversion, in StatementPrefecth, of data in FETCH_ASSOC format to other FETCH_* modes - would be very useful in contrib for non-pdo based drivers. But I'll leave that to a follow up.
- Assigned to mondrake
- Status changed to Needs work
over 1 year ago 5:33pm 11 March 2023 - 🇮🇹Italy mondrake 🇮🇹
Second thinking, I'm after the trait that will make all more organised.
- Issue was unassigned.
- Status changed to Needs review
over 1 year ago 9:22pm 11 March 2023 - 🇮🇹Italy mondrake 🇮🇹
Fixed the CS of the snake_case vs camelCase.
Added a trait to manage emulation of fetch modes, implemented in the StatementPrefetchIterator. Will be very useful for the mysqli driver that is not PDO based and therefore con only emulate PDO’s fetch modes.
- Status changed to Needs work
over 1 year ago 3:50pm 12 March 2023 - 🇬🇧United Kingdom alexpott 🇪🇺🌍
Posted some small nits to the MR review.
@mondrake FetchModeTrait is yet more API - the new trait is another API surface for us to support. Ideally improvements in API are not mixed in with bug fixes. Can't this be part of a follow-up? It's not necessary to fix the bug and unlike the introduction of the other trait we can't even argue that this is a necessary refactor to make iteration of all db statements consistent.
- Assigned to mondrake
- 🇮🇹Italy mondrake 🇮🇹
OK, I tried. Let me revert that and put in a follow up.
- Status changed to Needs review
over 1 year ago 6:43pm 12 March 2023 - Issue was unassigned.
- 🇮🇹Italy mondrake 🇮🇹
Filed 📌 Introduce a FetchModeTrait to allow emulating PDO fetch modes Fixed .
- First commit to issue fork.
- 🇬🇧United Kingdom alexpott 🇪🇺🌍
I added one question to the MR otherwise I think we're finally there with this one :)
@mondrake thanks for the recent changes and opening of follow-ups and blockers.
- Assigned to mondrake
- Status changed to Needs work
over 1 year ago 9:45am 13 March 2023 - 🇮🇹Italy mondrake 🇮🇹
@VladimirAus can you please explain what was the purpose of your push, and did you use
push --force
? Why?I'm working on the test addition.
- Issue was unassigned.
- Status changed to Needs review
over 1 year ago 10:00am 13 March 2023 - 🇮🇹Italy mondrake 🇮🇹
Added a test. There can be certainly more (for instance, testing that fetchAll* returns the remaining resultset in a partially iterated statement), but test hardening can be postponed to later on IMHO.
- Assigned to mondrake
- Status changed to Needs work
over 1 year ago 11:23am 13 March 2023 - Issue was unassigned.
- Status changed to Needs review
over 1 year ago 11:59am 13 March 2023 - 🇮🇹Italy mondrake 🇮🇹
Made fixes to the StatementWrapperIterator methods that are 'foreaching' the statement themselves - in a way that if the statement was already traversed fully, they can return an empty array straight ahead instead of erroring - in respect to the API.
- Status changed to Needs work
over 1 year ago 7:42am 16 March 2023 - Status changed to Needs review
over 1 year ago 8:10am 16 March 2023 - Status changed to RTBC
over 1 year ago 8:54am 16 March 2023 - Assigned to mondrake
- Status changed to Needs work
over 1 year ago 7:50pm 16 March 2023 - 🇮🇹Italy mondrake 🇮🇹
Needs adjustment after commit of ✨ Allow the database query log to be dispatched as log events Fixed . On it.
- Issue was unassigned.
- Status changed to Needs review
over 1 year ago 9:44pm 16 March 2023 - Status changed to RTBC
over 1 year ago 7:42am 17 March 2023 - 🇳🇱Netherlands daffie
The reroll looks good to me.
The testbots are all green.
Back to RTBC. - Status changed to Fixed
over 1 year ago 3:48pm 17 March 2023 -
alexpott →
committed c6532d8c on 10.1.x
Issue #3265086 by mondrake, alexpott, pwolanin, VladimirAus, daffie, mfb...
-
alexpott →
committed c6532d8c on 10.1.x
- 🇮🇹Italy mondrake 🇮🇹
A @todo that was left over from a previous version of the MR got committed inadvertently. Filed 🐛 Remove @todo leftover of #3265086 Fixed to remove it.
Automatically closed - issue fixed for 2 weeks with no activity.
- Status changed to Fixed
over 1 year ago 10:58am 11 April 2023 - 🇺🇸United States bnjmnm Ann Arbor, MI
Adjusting issue credit as credit is not provided for button-click rebases without additional contribution.