Function drupal_lookup_path() uses ORDER BY language DESC, pid DESC to search for alias or source of alias.
This caused by LANGUAGE_NONE is now 'und' so idea "...The language should have priority over the empty language." is totally broken because there are different languages
So aliases with LANGUAGE_NONE have priority on languages that starts from 'u' letter - most of languages
Suppose this is a major issue that affects upgrade path
| Comment | File | Size | Author |
|---|---|---|---|
| #109 | path_alias_language_order-863318-109-test.patch | 1011 bytes | leksat |
| #109 | path_alias_language_order-863318-109-test-with-fix.patch | 1.72 KB | leksat |
| #108 | 863318-108.patch | 1.18 KB | adel-by |
| #104 | 863318-104.patch | 14.75 KB | carlos8f |
| #103 | 863318-103.patch | 14.67 KB | carlos8f |
Comments
Comment #1
David_Rothstein commentedHm, that's pretty ugly.
So basically it seems like all the "ORDER BY language" stuff has to be removed from the queries in path.inc, and replaced with code in PHP that orders the languages properly?
Comment #2
andypost@David_Rothstein I scare that putting additional processing into this function brings a big performance loss
Maybe better just replace 'und' before saving path (now it's API call) and back to 'und' when loading?
Comment #3
dave reidHrm, would using
ORDER BY FIELD(language,:language,:language_none)work on only MySQL? Looks like it. :/Comment #4
dave reidActually this might work:
SELECT * FROM url_alias WHERE source = :source AND language IN (:language, :language_none) ORDER BY (CASE WHEN language = :language_none THEN 1 ELSE 2 END) DESC, pid DESCComment #5
dave reidQuery in #4 actually works, but it makes it hard to optimize. DamZ, andypost and I discussed adding a language_sort field that will be INT(SMALL) that will automatically store a 0 if language = LANGUAGE_NONE or 1 if otherwise. Then we just need to change ORDER BY language DESC to language_sort DESC.
Comment #6
andypostI scares that adding column in update_fix_d7_requirements() this makes this run slower and slower, so maybe we proceed with CASE()
Comment #7
damien tournoud commentedHm, @andypost, could you clarify what you mean here?
We already have tests for URL aliases, and I don't see what's wrong with adding a column in update_fix_d7_requirements().
Comment #8
andypostI mean that this need test for case of multiple sources for the same alias/language assigned
EDIT: see original issue #358315: drupal_lookup_path() not respects alias' order
Comment #9
andypostPath with tests and CASE() solution
Comment #10
andypostAny reviews?
Comment #11
damien tournoud commentedI already said that we need to add a language_sort field here. Forcing the database to table sort is a no go.
Comment #12
andypostPatch with new column.
- Changed description of {url_alias}.language column to conform current state.
- Added field to schema
- changed update_fix_d7_requirements() to add new column so upgrade path should work
- changed path_save() to set this field
Patch 863318-path-alias-sort-no-def-d7 without default value to find a places where url_alias inserted not via API
Comment #14
andypostAdded filling new column at system_update_7048()
I think this update could take some time and update_fix_d7_requirements() is not a place for it.
Need help with comments and column description wording
Comment #15
andypostNo default to check api again
Comment #17
andypostAlternative version.
Suppose better remove sorting by language and make this checks in php.
Mostly there are only two aliases (one for language and another without language) so overhead should be minimal
Comment #19
plachsubscribe
Comment #20
andypostAnother round, taking into account #456824: drupal_lookup_path() speedup - cache system paths per page.
Cache should store aliases only for current $path_language
EDIT not sure in the first hunk... which fills a cache with all aliases does not taking into account their language
EDIT2 benchmarks
Comment #21
plachI tested the patch and, although it fixes the issue as advertised, it's breaking the D6 behavior, STR:
Expected behavior: both node titles link to
http://example.org/test, only the english one works.Actual behavior: just the english node title links to
http://example.org/test, the language neutral node title links tohttp://example.org/node/2. Moreover if you change the english node's alias to 'test_en' and visit the front page again, the language neutral node title still has an unaliased link.We need test coverage for this too.
I was wondering if a viable approach to solve this is joining on the
{languages}.weightcolumn: IMO having a priority based on the language code alphabetical order does not make much sense.Anyway, some suggestions to (hopefully) improve comments:
"Create a custom path alias."
"Check the language priority for the path alias by looking up its source path."
"Perform the same check for language 'xx'."
Perhaps: "Create nodes with a language assigned to check alias priorities."
Maybe: "Assign the custom path alias to the first node and change its language to English."
Maybe: "Assign the custom path alias to the second node and change its language to LANGUAGE_NONE."
Better? "Confirm that the custom path alias leads to the first node."
Better? "Confirm that the prefixed custom path alias leads to the second node."
Powered by Dreditor.
Comment #22
plachAs per the beta-3 announcement I'm asking core committers to evaluate this issue's priority: since it's partially breaking the upgrade path it might be upgraded to critical.
Comment #23
plachOk, it seems I just have to push this to critical.
Comment #24
webchickIt's not clear to me why this bug must be fixed before RC1, and couldn't be fixed after RC1, or even in 7.1 or 7.2. I read the OP and plach's #21 and I still don't understand why this is critical.
As a side note, looking at the patch, that schema change needs to be moved to its own update function now that we're post-alpha.
Comment #25
plach@webchick:
My only concern is the upgrade path: we might have sites with broken/misfunctioning URLs after the upgrade, so my point is we might want to fix this before 7.0. Feel free to downgrade if this does not sound compelling.
Comment #26
plachTrying a PHP-only solution, first benchmarks seem to be encouraging.
I'll post the result of more thorough benchmarks later.
Comment #27
plachComment #29
plachWeird failure
#26: path-863318-26.patch queued for re-testing.
Comment #31
plachAttached you can find the benchmarks on my box: a fresh installation with 5 couples of nodes on the front page each one having the two nodes sharing the same alias. I get no visible difference. I'd wish to hear a word about the patch before working on the tests (and fixing the failing ones).
Comment #32
dave reidI much prefer a PHP solution as we don't have to change our schemas, but I'd rather prefer a uasort() callback solution that could be re-used by other modules in contrib.
Comment #33
plach@Dave Reid:
I get your point but my solution is not actually sorting the result, it just relies on the query order, removes LANGUAGE_NONE and pushes it at the back of the result, and this should be more performant. Moreover the three queries involved in the patch will always have at maximum 2 values as result since the alias language is limited to $path_language or LANGUAGE_NONE, so there are not many possible orderings :)
Comment #34
catchPatch looks sensible to me, and preferable to a schema change. With only two results, uasort() does seem unnecessary, and the code here is perfectly readable. Didn't do a thorough review though.
This could do with tests. Since Also this could do with tests.there's no schema change it feels like not an RC blocker to me. As a regression from D6 (and potentially 404s on D6 sites after upgrade unless I misread) it should still be 'major' though.
Comment #35
dave reiddrupal_process_path_query() is just not re-usable for contrib modules' queries, which may have more than two results. I'll work on a uasort patch tonight and compare performance, because I have a feeling that it's about the same, and will make life easier for contrib.
Comment #36
plach@Dave Reid:
If your solution has not worse performance, I don't have objections about it. Just a remark: drupal_process_path_query() is designed for resultsets of any size: it's an O(n) algorithm where an uasort in the best scenario should be O(n*log(n)).
Comment #37
dave reidHere's what I was working on and would be re-usable for other modules. I still need to do a performance test for before/after.
Comment #38
dave reidDoesn't even require two different functions. We have array_reverse for a reason...
Comment #40
plach@catch:
Discussed with chx about this before posting my patch, here is the chat log:
Comment #41
carlos8f commented@plach, I followed the steps in #21 using HEAD (no patch), and
This was not the case. While both titles linked to /test, the language-neutral node was displayed. Do we have a regression in HEAD then?
By the way, the language I added was Lolspeak, in case that matters :)
Comment #42
chx commented#38: 863318-path-language-sorting-D7.patch queued for re-testing.
Comment #43
chx commentedlolspeak is xx-lolspeak so it's after und. There are not many languages after und.
Comment #44
carlos8f commentedI see, so we need a test to expose the ordering problem in HEAD, when adding a language with a code starting with one of 'vwxyz'.
Comment #46
carlos8f commentedI'm working from #38, trying to salvage tests from #20. Additionally, the unaliased link bug in #21 should get a test.
Comment #47
carlos8f commentedI don't think sorting the whole result is necessary. A foreach() which does break; as soon as it finds a non-LANGUAGE_NONE alias should be sufficient. I reversed the direction of the ORDER BY clause to make this work. I don't think it's critical to make this super contrib-friendly; the procedure of an uasort() and conditional array_reverse() is not exactly a friendly "API" either :)
I included the tests from #20, and added an additional bit to test for the bug in #21 (both node titles should link to the same alias).
Comment #48
chx commentedThis looks good but i do not understand why $aliases = db_query("SELECT alias, language FROM {url_alias} WHERE source = :source AND language IN (:language, :language_none) ORDER BY language ASC, pid ASC" differs in ordering. Why is that not DESC? if a path has several aliases then this query will have different results than D6. Let's see what the bot has to say about this.
Comment #49
carlos8f commentedAhh, I wrote #47 and didn't interpret the behavior of the fetch methods correctly. There is an important difference here between fetchAllKeyed() and fetchField() in terms of what happens when multiple rows match the key. fetchAllKeyed() will return you the bottom result while fetchField() returns the top. With #47 and #48 we are replacing that with a simple loop that prefers the top, so we must adjust the order clause appropriately (X means a mistake):
HEAD:
alias cache: fetchAllKeyed(), asc, prefer bottom
alias not cached: fetchField() desc, prefer top
source: fetchField() desc, prefer top
#47:
fetchAll() desc, prefer top
fetchAll() asc, prefer top (X)
fetchAll() asc, prefer top (X)
#48:
fetchAll() desc, prefer top
fetchAll() desc, prefer top
fetchAll() asc, prefer top (X)
#49:
fetchAll() desc, prefer top
fetchAll() desc, prefer top
fetchAll() desc, prefer top
In #49 we consistently use DESC and prefer the top, which should match the behavior of HEAD while leaving LANGUAGE_NONE as a fallback.
As the above bot passes show, the tests we have so far aren't sufficient enough to catch precedence errors. So we'll need more tests in that area.
I've also included a database test I wrote which helped me learn/verify the behavior of these fetch methods. If no one objects, I think it might be a good addition here.
Comment #51
carlos8f commentedLet's remove the db test. @chx explained that there is no precedence intended because normally fetchAllKeyed() and fetchField() are supposed to work on one result per key anyway. In path.inc we have an oddball situation and it might be a misuse of fetchAllKeyed() and fetchField() since there are multiple rows matched.
What we really need is a test to make sure the expected alias is returned when there are multiple aliases for a source/path, and vice-versa. The patches in #47 and #48 mixed that up without any test failures.
In other news, why would "node body (broken) - 37" found fail here? upgrade.node.test line 32
Comment #52
andypost- Added lost update hook for system module
- changed drupalPost() to path_save() - very different path_admin_form_validate() and path_form_element_validate()
Comment #53
carlos8f commented@andypost can you explain what difference it makes to use path_save() versus drupalPost() in the test?
Comment #55
carlos8f commented#52: 863318-path-lang-sort-D7.patch queued for re-testing.
Comment #56
carlos8f commentedThe test fail is because drupal-6.filled.database.php has *two* LANGUAGE_NONE aliases of 'content/1263769200', which point to different nids, node/13 and node/37. That is silly and probably just resulted from manually editing a db dump. The bug is exposed by the patch here sorting DESC and using the second result if both have LANGUAGE_NONE, so it uses node/13. I would call that a bug in the test db and not the patch.
However, there is a real problem with #47-and-on that the path cache is no longer used completely in the first hunk. I'll try to remedy this, and look into writing tests for multiple-lang-aliases-for-one-source.
Comment #57
carlos8f commentedThis fixes the first hunk which previously bungled the path cache. The first query is now back to
ORDER BY language ASC, pid ASCto help fix that, since we can't use the simple break; method like the 2nd and 3rd queries.Didn't have time to write additional tests yet.
Comment #58
carlos8f commentedNow after looking at path_save() I'm starting to doubt our method here. Although the path module forbids a language to have multiple aliases for a path via validation, there is no such validation when using path_save() and a language (especially LANGUAGE_NONE) might have many aliases per path, the effective one being MAX(pid). If the given language is LANGUAGE_NONE, my patch would effectively return MIN(pid), which is not so good. Needs more work then.
Comment #59
carlos8f commentedWent back to @Dave Reid's approach, after all that. Cleaned it up so it should now pass tests, added a docblock for drupal_sort_url_aliases(), and tested it, looks like we consistently now return MAX(pid) when language is found, then fallback to MAX(pid) LANGUAGE_NONE. That should make sense if there are multiple aliases in the table for a path, created directly with path_save() without specifying a language.
Comment #60
plachAm I wrong or we still need benchmarks for this approach?
Comment #61
andypostThis approach should be checked for performance. Also fetching all rows from DB could lead to visible memory consumption.
Comment #62
chx commented#58, carlos, please get back to the previous, simpler method. ordering on pid DESC and bailing out on the first non-und entry results in the MAX(pid).
Comment #63
carlos8f commented@chx, if we go back to the break; method, we need to fix this:
Although we return MAX(pid) if the language is specified, the approach in #57 returns MIN(pid) otherwise and the above code returns 'test' instead of 'test2'. If we can fix this in a way that doesn't make the code unreadable, and still performs better than #59, I am all for it.
Also if we want to reduce memory usage, we might want to avoid doing fetchAll(). The above example shows that {url_alias} can contain many duplicate aliases for any given language. If memory is really an issue, a schema change with a language_none bit would allow us to keep the existing fetchAllKeyed() and fetchField() calls and not load duplicate aliases into PHP, but then we have to deal with community pushback from a schema change this late.
It would also be nice to add the above example to path.test so we can prevent against this type of regression. The upgrade.node.test failure are the only thing that caught this regression so far.
Comment #64
carlos8f commentedCorrection, fetchAllKeyed() would still load duplicate aliases into memory (for the path cache), but fetchField() wouldn't (for individual lookups).
Also, it seems odd that path_save() doesn't do a db_merge on (source, language) if pid isn't provided. As it is there could be duplicate aliases floating around that are never used.
Comment #65
chx commentedThis avoids fetchAll and is based on #48 and passes the tests in #59 ie. I still dislike uasort here.
Comment #66
chx commentedPoor patch got ignored due to poor naming? Let's try this.
Comment #67
carlos8f commentedMaybe a problem in how you rolled the patch?
There are a few things I don't get about this patch:
Comment #68
chx commentedHm, I thought that using language ordering was necessary but you might be right that it's not, actually. It needs update indexes if it's not. The query might be faster, even. No, the patch does not replicate fetchAll because it does not store every result. I am happy to ask Dave Reid which approach he likes better. I consider my code pretty readable and straightforward of course :)
Comment #69
dave reidI'm going to prefer the uasort() solution since I know I can use in at least one module that I co-maintain. And I would be under the assumption that it would be ok to use the admittedly slower uasort() solution since the results of all these queries/sorts are cached as well via drupal_cache_system_paths() so that the next same page load wouldn't hit the db?
Comment #70
carlos8f commentedWell let's get some benchmark comparisons of HEAD, #59 (usort), and #66 (foreach) then, and if usort() performs OK, go with that. Preferably we test on a real D6-upgraded site with lots of aliases from pathauto. Edit: and multiple languages, if we can.
Comment #71
catchdrupal_cache_system_paths() in no way caches the alias fetching, that's why it's called drupal_cache_system_paths() ;). All it does is turn lots of little queries into one big IN() query, the PHP cost of sorting out the individual aliases will be the same.
Comment #72
dries commentedLet's add more comments to the code. I'd like to see us outline the intended behavior somewhere in the code comments to people can follow along. Also, the 'und' problem is not explained in the code. It was not discoverable from reading the proposed patch.
I recommend that we explain _why_ we want to keep the last one.
Would be good to extend this code comment. Why would it be preferred?
Comment #73
carlos8f commentedHere are some benchmarks. I used catch's "micro benchmark" technique of calling an API function in a loop, to emphasize that function in the results. The test file:
The test site is a standard D7 install, 8 nodes in the database, with several aliases for node/3 and several sources for 'test' in different langauges (English is the default on the site).
#65 and HEAD are neck-and-neck, and #59 was slightly behind. So as expected, #59 comes with a small performance regression but makes the sorting mechanism usable in contrib while also being a little more self-documenting.
Tough choice! Dries had trouble reading the logic of #65... which would have to be documented in multiple places. #59 however is more encapsulated and can be documented centrally.
I also did some more general benchmarks using <front> and /xx-lolspeak/test, and I couldn't get a definitive reading that was out of the margin of error. Therefore, within the context of a normal page load, the performance regression of #59 is probably extremely minor.
I suggest we dust off #59, document it a little bit better, and then we can be done here :)
Comment #74
dries commentedI think I'd be OK with #65 if it were better documented. #59 seems like it introduces a 5% performance regression which seems too much.
Comment #75
carlos8f commented@Dries: to be clear, this is not a 5% regression of an actual page load, because this is a "micro" benchmark: I used test.php to loop 100x and call drupal_lookup_path('alias', ...) and drupal_lookup_path('source', ...), to make the gain/loss magnified. So it's a ~5% regression in an artificially magnified test, which shows that it is indeed slower, but not by much. The difference shouldn't be noticeable in actual practice.
Comment #76
plach@carlos8f:
Why not introducing a new API function as in #26 to encapsulate the fetching logic? We would end with performant yet reusable code.
Comment #77
dries commented@carlos8f: I know it is micro-code. drupal_lookup_path() is called a lot though -- sometimes hundreds of times per page. Imagine a node with a hundred comments -- that quickly translates to 200+ links, and therefore 200+ calls to drupal_lookup_path(). If you're an administrator on that site, it quickly becomes 400+ links on such a page.
Comment #78
carlos8f commentedTrying to work out something based on @plach's suggestion.
Comment #79
carlos8f commentedHere is a more encapsulated approach (very usable in contrib), with the fallback logic simply implemented as an operator: $aliases[$path_language] + $aliases[LANGUAGE_NONE].
I also added some unit test goodies in path.test which ensure that the correct thing is returned, given a variety of situations. The update function # needed to be bumped, as well.
Looks like another round of micro benchmarks is in order.
Comment #81
dave reidIt's not re-usable for contrib when we need all results still, which is why I still preferred the uasort().
Comment #82
carlos8f commented@Dave Reid, can you elaborate on that? Do you mean that you'd rather keep the query separate from the sorting process, as in #26?
Is there a method that is usable in contrib but doesn't cause a performance regression? @Dries has stated that the ~%5 performance loss caused by the usort() method would be unacceptable.
Comment #83
carlos8f commented#79: 863318-79.patch queued for re-testing.
Comment #84
carlos8f commentedIf I'm understanding right, contrib needs a way to purely sort aliases without collapsing $path_language and LANGUAGE_NONE?
Since core doesn't provide that currently, I don't see why that's a requirement of this patch going in, which just fixes existing but broken behavior.
Comment #86
carlos8f commentedFixed that bug by applying an array_reverse() to the return value, and added a test for it:
Comment #88
carlos8f commented#86: 863318-86.patch queued for re-testing.
Comment #90
carlos8f commented#86: 863318-86.patch queued for re-testing.
Comment #92
carlos8f commentedNew benchmarks in, using more magnification in the micro loop:
ab -c 1 -n 100And now test.php loops 1000x instead of 100x.
With more magnification, we are now pushing past the margin of error. It looks like the usort() method (#59) is very close to HEAD. I had a hard time believing this so I ran the results several times, and it seems to be true. So I think we should still consider the usort() method.
Comment #93
chx commentedThrowing this idea here. Will add comments.
Comment #94
carlos8f commentedPasses tests. I'm not sure how, but it works :) Benchmark looks good...
This patch's method barely makes sense though. The usort() (#59) looks like the most readable/performing patch so far. Update: I re-ran the benchmark on #59 and got 786.980 ms average, which is 1 ms more than HEAD.
Also, it's been mentioned in #2 but can we get an opinion on this: is it really necessary that we store language-neutral aliases as 'und' in the database? If we rolled that back to a blank string, that would seem to solve the whole issue here.
Comment #95
chx commentedbarely makes sense? I am reusing the query setting the direction as dictated. I would be VERY cautious about re-encoding und as '' in the database (especially this late).
Comment #96
chx commentedWith comments.
Comment #97
catchI'm not sure why #96 doesn't make sense, if we only have one language + und to check at any one time, then changing the sort order like that works for me. As long as we can order in SQL without a schema change or unindexed query then there's no reason to do this in PHP.
Comment #98
carlos8f commentedIt must've been late last night; the patch does make sense especially with the comments. I cleaned up a couple grammar mistakes and added a note about pid ASC/DESC. Also, $path_language == LANGUAGE_NONE on the vast majority of sites, so I thought it was worth adding a case for.
Comment #99
dries commentedGlad to hear there is consensus. Let's see what the test bot has to say. We're close, folks!
Comment #100
chx commentedWhile the grammar changes are OK the query changes are not and I removed them. You can't remove a column from the middle of an index and expect it to work. Those == queries would've been unindexed.
Comment #101
carlos8f commented@chx I think you might be mistaken about the query being unindexed:
EXPLAIN SELECT alias FROM url_alias WHERE source = 'node/3' AND language IN ('und', 'und') ORDER BY language DESC, pid DESCEXPLAIN SELECT alias FROM url_alias WHERE source = 'node/3' AND language = 'und' ORDER BY pid DESCComment #102
chx commentedWell, then #100 is necessary becauseEdit: it failed because PDO wont tolerate passing in extra arguments.It passed while #98 didnt. Yes we could manipulate the $args array into submission but this is rapidly getting too much code for way too little benefit.Edit: MySQL is braindead to treat IN (foo, foo) as a range query. Go for the optimization then.
Comment #103
carlos8f commentedHere we avoid a useless "range" query for IN ('und', 'und'), which is braindead as @chx says.
Comment #104
carlos8f commentedchx's imaginary webchick filter asked me to add a code comment for unset($args[':language]): "Prevent PDO from complaining about a token the query doesn't use."
Comment #105
chx commentedI think we are good to go.
Comment #106
dries commentedI think this looks great now. Committed to CVS HEAD. Thanks all.
Comment #108
adel-by commentedHello,
I set Multilingual settings taxonomy vocabulary on Localize terms. then for taxonomy term pages, I created different aliases for each language.
on my menu, i'm always getting the alias for the undefined language, even when i'm on a different language than the default one.
I think this is because of the queries generating the cached alias data. the "order by" statment seems not ok.
Actually the queries (at line 111) are different from the ones fiew lines later.
this patch solved my problem.
Comment #109
leksat commentedchikipi is right. We have different behaviors for the cases when
$cache['first_call']is empty and when it's not. For example, let's assume that we have current language "de", and we have the following records in theurl_aliastable:Case 1,
$cache['first_call']is empty.Will be executed query from line 114. Language order is ASC. And
fetchAllKeyed()will returnarray('taxonomy/term/1' => 'page', ...).Case 2,
$cache['first_call']is not empty.Will be executed query from line 150. Language order is also ASC. But
fetchField(), which is used in this case, will return"seite".I guess the behavior in the first case is wrong, and the condition on line 113 should be reversed.
The same is reported here: https://drupal.org/node/2065977
Attached patches demonstrate the issue.
Comment #110
mcrittenden commented(Assuming this is ready for review).
Comment #111
mgiffordDoes #21 still have the best means to test this?
Do we need more tests than in #109? Patch still applies nicely.
Comment #112
mgifford@carlos8f last contributed to this issue 4 years ago.
Comment #113
webchickLooks like this was elevated in #23 as a way to get core committer attention. I can't see anything in this issue that would qualify as critical, however. It does not:
* Render [the] system unusable and have no workaround.
* Cause loss of data.
* Expose security vulnerabilities.
It seems to be solidly major, however.
Comment #118
chi commentedThe patch #109 is still relevant. That sort condition has to be reversed because fetchAllKeyed() always returns the last record if more than one record with the same key were found. The supplied test also works well for me.
Comment #120
eysz7x commentedFunction drupal_lookup_path() uses
ORDER BY language DESC, pid DESCto search for alias or source of alias.This caused by LANGUAGE_NONE is now 'und' so idea "...The language should have priority over the empty language." is totally broken because there are different languages
So aliases with LANGUAGE_NONE have priority on languages that starts from 'u' letter - most of languages
Suppose this is a major issue that affects upgrade path
Comment #122
joseph.olstad