Sorry about creating a new critical issue, this should be easy to fix :)
This came on in #761976: DatabaseSchema_pgsql::changeField() is broken.
Currently, the upgrade test does this (to create the D6 database structure including content):
db_create_table('some_table', $table_definition_with_serial_column);
db_insert('some_table)
->fields(array(
'serial_column' => 1
// ....
The problem here is that InsertQuery automatically tries to fetch the last_insert_id after the row was inserted. It does that by using CURRVAL() on the column. The problem here is that if we explicitly set a value to the serial column, we never call NEXTVAL() and because of that, we don't have a "current value". So the currval() call fails.
Proposed fix:
- Catch the exception, check error code (55000) and in case it's that, set the $last_insert_id to NULL.
I also tried to automatically get the value from the passed in $serial_column but that is quite complex and I'm not sure if it's worth it. the value is passed by the caller so we can imho safely assume that he will not use the return value :)
Comment | File | Size | Author |
---|---|---|---|
#42 | postgresql_currval9.patch | 3.13 KB | Berdir |
#35 | pgsql-serial-insert-890090-35.patch | 4.72 KB | bjaspan |
#33 | postgresql_currval8.patch | 2.67 KB | Berdir |
#19 | postgresql_currval7.patch | 4.7 KB | Berdir |
#18 | postgresql_currval6.patch | 4.73 KB | Berdir |
Comments
Comment #1
BerdirForgot to set the status.
however, there might be a bigger problem here, so don't RTBC it just yet...
Debugging...
Comment #2
BerdirComment #3
BerdirFollow-up problem:
After inserting a row with a manually set value, trying to insert a row without setting the serial column fails with a unique key constraint error.
A possible fix is to automatically update the sequence when a value is inserted into the serial column.
The attached patch does this. I also just found out about #445214: dwr() fails when a NULL value is passed in a serial column (on PostgreSQL and SQLite) which fixes the same bug inside dwr() but doesn't contain the part with the auto-updating...
Comment #4
Damien Tournoud CreditAttribution: Damien Tournoud commentedThis looks good to me. Could you just please move the try/catch into an override of PDO::lastInsertId()?
Comment #5
BerdirOk, updated the patch.
I've created a simple test script, can someone convert this into a test?
Should be simple, we only need to verify that this doesn't fail with an exception and that $last_insert_id is > 1 (Not == 2!)
Comment #6
BerdirComment #7
jhedstromI'll write the tests.
Comment #8
bjaspan CreditAttribution: bjaspan commentedre #4: I think I disagree that the try/catch should be in lastInsertId(). If you call lastInsertId() in a situation in which there is no valid last insert id, shouldn't it return an exception? The problem here is that InsertQuery::query() is calling lastInsertId() in just such a situation. InsertQuery::query() could be smart enough to detect that there is no last insert id and not try to return it, or if it does not want to deal with that alternatively it can catch the exception when it occurs and handle it. If anyone else calls lastInsertId() under the same conditions, they should have to make the same choice.
Comment #9
bjaspan CreditAttribution: bjaspan commented@jhedstrom: I did not mean to unassign the issue from you, it was a cross-post, but I cannot re-assign you, so you'll have to.
Comment #10
jhedstromComment #11
Damien Tournoud CreditAttribution: Damien Tournoud commentedNo, typically lastInsertId() doesn't raise an exception. It does on PostgreSQL but not on the other database engines. This is something we want to solve directly there. Plus, we are only catching a very specific exception.
Comment #12
jhedstromHere's a test. It currently fails because when a manual serial is inserted after nextval has been called, lastInsertId returns the previous auto-incremented ID instead of the new one (in the testcase, 5 is returned instead of the expected 7).
Comment #13
BerdirOk, new patch:
- Merge and extend the tests from #12 (first insert the id 10, then 7, make sure $last_insert_id returns the correct value in both cases)
- Updated pgsql driver to manually get and return the value if passed in
- Handle racing conditions by setting the sequence to the bigger value of nextval() and max($serial_field)
Test passes on MySQL and PostgreSQL, someone should verify this on SQLite...
Comment #14
bjaspan CreditAttribution: bjaspan commentedNit: "fails"
Nit: whitespace.
This does an array search twice. Because FALSE is not a valid value for a serial column, we can replace this with one call to array_search, then test !== FALSE.
So, ummmm... I wanted to see if we could avoid the in_array on every element of serial_fields, because that is O(N^2) (though granted we almost never have more than one serial field, in fact I'm not sure even that is legal).
However, I cannot find anywhere in HEAD that uses or defines serial_fields at all. Does this property even exist?
It seems like we should have a test for inserting an explicit value, then verifying that an auto-increment value does not cause a unique key constraint.
Powered by Dreditor.
Comment #15
Berdir- Fixed the typo and whitespace
- Switched the code to a single array_search call
- Added a missing part that adds the $serial_fields information
Comment #16
marvil07 CreditAttribution: marvil07 commentedI am updating the patch as the styling suggestions, but this still needs review about the other logic suggestions.
BTW I tried the relevant tests on postgres and sqlite3 and they work fine in both databases types.
Comment #17
marvil07 CreditAttribution: marvil07 commentedfor the record, pay attention to the patch in #15, since my patch is a subset of that :-p
Comment #18
BerdirFound the problem for the failing test. $key needs to be checked type safe against FALSE, if ($key) doesn't work...
This passes on postgresql.
Comment #19
BerdirAnd now without debug calls...
Comment #20
puregin CreditAttribution: puregin commentedRan the database tests on the last patch with sqlite; everything passed except DatabaseTransactionTestCase->testCommittedTransaction() and DatabaseTransactionTestCase->testTransactionRollBackSupported() which give General error: 5 database is locked.
Comment #21
bjaspan CreditAttribution: bjaspan commentedThe patch in #19 looks good to me, and the new tests pass on mysql and (bedir asserts) pgsql, thus fixing the pgsql issue. The new tests apparently reveal an existing bug in the sqlite driver. I believe our policy is not to hold up a commit due to problems on non-mysql drivers. Thus, RTBC.
Comment #22
Damien Tournoud CreditAttribution: Damien Tournoud commentedSQLite is not affected by this patch. #20 is probably due to an old version of SQLite being used.
I'm not confortable with this part of the patch:
... as this is prone to race conditions. At the minimum we should use only one query here, but we should study if an atomic solution is possible.
Comment #23
puregin CreditAttribution: puregin commentedI reran the database transaction tests with sqlite 3.6.10, and am getting the same errors. I will try to update to 3.7.x and repeat
Comment #24
puregin CreditAttribution: puregin commentedSame errors with sqlite 3.7.0.1
Comment #25
bjaspan CreditAttribution: bjaspan commentedI found that "SELECT SETVAL('foo', (SELECT 30))" sets the counter to 30. So now I just need to find the syntax for selecting the max of (curval and max of column values) in a single expression... but at the moment I cannot access my pgsql server.
Comment #26
Damien Tournoud CreditAttribution: Damien Tournoud commentedWe discussed that with @berdir today, and the only way we can think of to make this purely atomic is to implement a while loop:
It will be slow and clunky, but it's the only way I can see to avoid race-conditions. Most of the cases, it should not be a concern because this feature (inserting values into a serial field) is only used during imports, and most of the time the loop should only be used once.
Comment #27
bjaspan CreditAttribution: bjaspan commentedokay
Comment #28
chx CreditAttribution: chx commentedWasn't this the problem with db_next_id? can't we reuse that code?
Comment #29
BerdirAfter thinking about it, I think there is a very simple, possible race condition:
If an INSERT query tries to insert a new row without a value set between another INSERT added a row with a manually set serial value which equals nextval(), it explodes :)
@chx: That might work, but I'm not sure how to do it exactly. Basically, we'd have to block other INSERTS while inserting rows which have a serial column set. Maybe we could simply lock the whole table for writes and unlock it after we have updated the sequence? Would stop other INSERTS *before* they call nextval()?
Comment #30
bjaspan CreditAttribution: bjaspan commentedWhat if we always called setval('seq', 1, false) after creating a table with a sequence? This ensures that currval() and nextval() will work, and the "false" argument means that the next call to nextval() will return 1 instead of 2 so we won't change expected behavior.
We could do a similar trick during update on all such tables that have not yet been inserted into.
Comment #31
bjaspan CreditAttribution: bjaspan commentedMy suggestion in #30 would avoid the initial problem, generating an exception when calling db_insert() with an explicit value for a serial column that has never been auto-inserted into, but does nothing to help the followup problem of the key collision when the sequence catches up to explicitly inserted value.
chx just approached me in the coder lounger to ask why db_next_id() doesn't solve the problem. berdir is concerned about concurrency issues, but the implementation of nextId() for pgsql seems to address that by getting an advisory lock on the sequence and incrementing it until it exceeds a specified value. When we insert an explicit value, we could call db_next_id($explicit_value) to make sure that particular explicit value is exceeded by the sequence.
Oh, now I see berdir's concern. While nextId() is iterating, unrelated inserts can insert their own value at whatever the then-currval of the sequence is, which might happen to be exactly the explicit value we just inserted. This can happen because insert queries do not get the advisory lock that nextId() does. So, can we resolve this by having insert queries with explicit values for serial columns get the same advisory lock that nextId() does?
Here's another option: When we are about to insert an explicit value, increment the sequence *before* the insert until it exceeds the value we are inserting. Then do the insert. If two different inserts are running concurrently, presumably for different values (if they are trying to insert the same explicit value one of them would fail in any case), the sequence will first be incremented past the first one (which will then insert) and then past the second one (which will then insert), and they will both be happy.
Comment #32
tpfeiffer CreditAttribution: tpfeiffer commentedJust discussed with berdir the following approach: *Before* the INSERT (say with id 42), do the following:
This will
.
Patch follows.
Comment #33
BerdirPatch for the suggestion in #32.
I also have a working implementation of the suggestion in #31 in case there is a reason that this doesn't work.
Comment #34
bjaspan CreditAttribution: bjaspan commentedYou and bedir appear to have hit on the same idea I suggested in #31 in the last paragraph, "Here's another option...". So obviously I like the approach. :-)
This confused me at first b/c I thought index 0 referred to the serial column's position in the table, not its position in the $table_information->serial_fields array. I suggest the comment should say: "This is only done for the first serial column in the table, because that is the column whose newly inserted value is returned as the last insert value."
Assuming that statement is true, of course...
See my suggestion in #30. If we use the optional third arg false to setval(), the next call to nextval() will return the value we are setting here.
Actually, now that you've gone and read #30, what we actually need to do is document for future that we CANNOT use that approach! I just realized it reintroduces a race condition. If we did that here, and another insert into the table occurred before the actual insert query lower in this function, the value we set here would be returned in the other query and the other query's value would be returned here. Wouldn't that be a fun bug to debug?
Let's document why we must do it the way this patch does it, and not using the third arg to setval().
I expect this will be RTBC when the comments are improved. Actually, I'll go improve them now and re-post.
Powered by Dreditor.
Comment #35
bjaspan CreditAttribution: bjaspan commentedNew patch. The only changes are comments, so I'm marking RTBC based on the code in the previous patch.
Comment #37
BerdirFatal error in trigger other actions?
Lets try again...
Comment #38
Berdir#35: pgsql-serial-insert-890090-35.patch queued for re-testing.
Comment #39
BerdirWas RTBC before.
Comment #40
Damien Tournoud CreditAttribution: Damien Tournoud commentedSame issue.
Is not concurrency safe. setval() never is, as far as I know. I believe that #26 is the only way forward.
Comment #42
BerdirDiscussed with DamZ and we came to the conclusion that using explicit values for a serial column is prone for concurrency issues anyway so we simply document that it is not safe.
Comment #43
BerdirThis was RTBC like three times already.. :)
Regarding SQLite, I can confirm two things:
- I get two Database Locked errors in the Transaction tests also, with sqlite 3.7.0
- These errors have nothing to do with this issue.
Comment #44
webchickCommitted to HEAD.