The url_alias table needs a UNIQUE constraint to ensure that there are no duplicate entries. Currently, there is a test to ensure that only the most recent duplicate alias is returned. It would be far better to ensure that there are no such entries.
Original report
This report is identical to #818214: Improper table structure for url_alias. In that issue a patch was applied and then rolled back. I believe it should be applied again.
This is the original issue description:
The table structure for the url_alias table is improper right now. There is a UNIQUE index set on the dst_language_pid grouping. The pid column should be removed from that index as it's now possible to have duplicate destinations for the same language because pid is ALWAYS a unique value.
This should be changed to UNIQUE KEY (`dst`, `language`) to correct this structural issue.
The point is: It doesn't make sense to have the same alias in a given language pointing to more than one source path.
The current database schema was originally introduced (to both D6 and D7) in #358315: drupal_lookup_path() not respects alias' order. The argument for changing the key from (alias, language) to (alias, language, pid) was to remove a filesort from this query:
SELECT source FROM {url_alias} WHERE alias = :alias AND language IN (:language, :language_none) ORDER BY language DESC, pid DESC
However, simply removing "pid DESC" from the ORDER BY should give the same result, because (alias, language) is already unique so there is no need for the "pid DESC".
| Comment | File | Size | Author |
|---|---|---|---|
| #83 | core-url_alias_unique-925474-83.patch | 1.18 KB | liam morland |
| #81 | core-url_alias_unique-925474-81.patch | 1.88 KB | siva_epari |
| #68 | core-url_alias_unique-925474-68.patch | 1.88 KB | liam morland |
Comments
Comment #1
andypostsubscribe
Comment #2
EvanDonovan commentedIf this is actually a bug, and not a feature request, it should be against 7.x, since 8.x is not open for development yet.
Comment #3
stefan freudenberg commentedWell. After reading the original thread I vaguely understand that the unique key has been changed because there are means in the UI to prevent duplicate aliases. Well there are other methods to create content in Drupal that don't use the UI. Since march we have accumulated a bunch of duplicates and one case caused a lot of trouble for us today. I am evaluating if something has been coded improperly on our end...
But the unique key is a constraint which has a functional meaning besides improving performance. Also... introducing those kinds of changes in updates that come along with security fixes should be reviewed in general.
Comment #4
stefan freudenberg commentedThe mechanism guarding against duplicate aliases as of now will not work if the aliases are created at the same time. Removing the unique key constraint has opened the opportunity for duplicates to occur. This is definitely undesired behaviour (a bug) and should be fixed by either reverting to the old schema or using a lock.
Comment #5
catchIf anything, we should add a new unique index on alias, language. For existing sites that have this problem it'll be necessary to have an update that finds and deletes duplicate url aliases as well.
Comment #6
catchComment #7
sunThe "bug" is hypothetical at this point, so this is a task, which might be backported to D7.
Comment #8
stefan freudenberg commentedWell, it's definitely not hypothetical. I can assure you it happened on my site.
Comment #9
chx commentedI am working on a site that has thousands of same src-dst pairs, some of those more than 70 times -- and no language in sight.
Comment #10
tim.plunkettThe dst_language_pid index in the OP doesn't exist anymore, is this still valid?
Comment #11
stefan freudenberg commentedThis is valid as long as the UNIQUE KEY constraint is missing, unless someone prefers a different solution for the problem.
Comment #12
liam morlandI spent most of the day trying to figure out why I had duplicate entries in the url_alias table. Turns out it was because of #1737048: 2 URL aliases created when saving new node as published. If there has been a unique constraint in places, this would have been caught long ago in development, before it became a problem.
The attached patch adds the unique key. It would be best to have an update hook clear away duplicates and add the key to existing databases. This fixes it for new installations.
Comment #14
tim.plunkettComment #15
liam morlandIn AliasTest.php, the test that is causing the failure is 'Newer alias record is returned when comparing two LANGUAGE_NOT_SPECIFIED paths with the same alias.' So, this test deliberately violates the unique key that my patch adds.
I think I should change this test so that it tests that the unique constraint is working. Can I use a try()catch() block for this? Am I on the right track?
Comment #16
liam morlandI tried putting a try()catch() block around the database insertion which ought to fail if the constraint is working, but the exception gets caught elsewhere, so the test still doesn't work.
This revised patch just removes the obsolete test.
Comment #17
liam morlandSorry, this is the correct patch.
Comment #22
liam morland#17: core_url_alias_unique_925474.patch queued for re-testing.
Comment #24
liam morlandReroll.
Comment #25
liam morlandIssue summary has been updated.
This removes an unneeded test. The UNIQUE constraint cannot be tested with the Drupal testing system.
Comment #26
liam morlandReroll.
Comment #27
liam morlandTags
Comment #27.0
liam morlandCreate short explanation of current state of issue.
Comment #28
liam morlandReroll.
Comment #29
andypostMaybe better to use
db_merge()because there's no support for handling unique key exceptionsComment #30
liam morland@andypost, I don't understand. Can you elaborate?
Comment #31
mgifford28: core_url_alias_unique_925474.patch queued for re-testing.
Comment #32
mgiffordI don't understand @andypost's comment either, but must be instead of:
+ 'unique keys' => array('alias_langcode' => array('alias', 'langcode')),This is a small patch after all.
Comment #33
liam morlandRe-reading it, I understand: My patch would cause an attempt to insert a duplicate alias to fail, which preserves data integrity.
If in addition to my patch, the alias creation code used db_merge() instead of db_insert(), then such an attempt would update the existing alias instead of failing, which might be a preferred fail condition.
Either way, my patch can go in.
Comment #36
liam morlandReroll.
Comment #38
liam morlandReroll. This version adds the UNIQUE constraint and changes the code for adding aliases so that if you try to add an alias on the same language and path as an existing one, it instead updates the existing alias. The resulting behavior is identical to before, so no changes are required to any of the tests.
Comment #39
liam morlandComment #40
liam morlandComment #41
liam morlandReroll.
Comment #45
liam morlandComment #47
liam morlandComment #48
liam morlandComment #49
liam morlandComment #50
jhedstromPatch in #49 still applies.
Why is this change necessary?
Comment #51
liam morlandI don't remember. Does it work without it? My D8 install is not working right now.
Comment #52
liam morlandI think it is a better test: Do the test starting with the source and ensure that the right alias comes out.
Comment #54
mgiffordThis makes sense to me. It still applies nicely. Drupal 8 is going to be the most multilingual version of Drupal ever, seems kinda silly to not include this.
Tagging for i18n.
Would there be any performance issues with the merge?
Comment #56
andypostsuppose proper tags
Comment #57
liam morlandI don't think the merge should cause a performance issue. It may be a little bit slower than insert, but it only runs once, not in a loop.
Comment #58
liam morlandDoes anyone understand why there are two test failures of the same item? Why does this test run twice?
Comment #61
liam morlandReroll without change to test.
Comment #62
liam morlandComment #64
liam morlandThe code already has a check for whether it should insert or update, so all that is needed is the unique constraint. This passes when I run it on my local.
Comment #66
liam morlandOK, so the merge is needed. The only problem is that with the merge, the D6 migration stuff doesn't work. I don't understand what is going wrong there.
Comment #67
mgiffordThanks for plugging away at this. Would be good if we could bring in more i18n folks to review it.
Comment #68
liam morlandThis is a reroll of the last version of this patch which passes the testbot.
Comment #80
andypostComment #81
siva_epari commentedPatch rerolled.
Comment #83
liam morlandReroll. I'm getting a new error when I try this locally. I want to see if the testbot gets the same thing.
Comment #84
dawehnerthis would need an update function
Comment #86
liam morland@#84: Yes, I wanted to get it working first, then worry about the update hook.
I am getting the MySQL error "1071 Specified key was too long". We didn't used to get this error for that line of code. I don't know what is different. I can get around it by making the key only include the first part of the alias, but that could prevent the insertion of valid aliases.
Comment #95
quietone commentedaliases were converted to entities in #2336597: Convert path aliases to full featured entities and included adding a constraint for uniqueness, UniquePathAlias. So, I reckon this is now outdated.
Therefore, closing as outdated. If this is incorrect reopen the issue, by setting the status to 'Active', and add a comment explaining what still needs to be done.
Thanks!