I have several nodes added to the Main Menu in Drupal 7.
I have changed their order by visiting the menu configuration page, dragging them around, and saving.
Then I go to edit one of the pages. Before editing, it's the first page in the menu, at that level of the hierarchy. I change the node title and save the page. Now it's the last item in the menu, at that level of the hierarchy.
I didn't do anything in the menu section of the vertical tabs. All I did was change the node title. The menu item name didn't change, and the order in the menu should not have changed either.
Comment | File | Size | Author |
---|---|---|---|
#60 | menu-weight-fix.patch | 1.82 KB | Gábor Hojtsy |
#58 | menu-weight-fix.patch | 2.33 KB | Gábor Hojtsy |
#52 | 605926g-with-test-20100213.patch | 2.02 KB | dman |
#41 | 605926g-with-test-02.patch | 2.18 KB | dman |
#38 | 605926g-with-test.patch | 2.4 KB | dman |
Comments
Comment #1
jhodgdonI think the reason this is happening is that in includes/menu.inc, in function menu_link_load, the query it runs is joining the menu_links table to the menu_router table, and taking all the fields of each.
This would be OK, except that both menu_links and menu_router have a 'weight' field, and in the result, at least on my machine, $item is ending up with weight=0 (from menu_router) rather than the weight of the item in my Main Menu (which in this case has been set to be -30 or something like that).
This information is eventually used by menu_form_alter() in modules/menu/menu.module to build the menu section of the node edit form. So then when you save the node, this weight of zero gets stuck into the menu_links table, and your menu is out of order.
So the solution is probably to make menu_link_load() take all the fields except weight from menu_router, because the primary thing it's doing here is loading the menu link item. I think.
Comment #2
jhodgdonJust as a note, I tried reversing the order of the ->fields calls in the code above, but that didn't help. Weight still came out 0.
So I am not sure how to patch this -- is there a function that will list all the fields from the menu router table, so that we can just remove 'weight' from the list and pass the resulting array into the fields() call? The alternative would be to list all the fields out, but that is a bad idea (hard to maintain as the table spec changes).
Comment #3
sunThis sounds like a fairly critical bug to me.
Comment #4
jurgenhaasI'm having the same issue and haven't found a way to resolve it.
Comment #5
jim0203 CreditAttribution: jim0203 commentedCracked it, I hope. Patch attached.
This was caused by translating a query from D6 into DBTNG-speak in D7. In D6, the query was executed as follows:
Whereas in D7 it is executed like this:
Spot the difference - apart from the parentheses in the last line and the more explicit LEFT OUTER JOIN? Yep, m.* and ml.* are switched in the first line. When a query like this is run in MySQL and the two tables being queried have row that share the same name, which ever one is called second is the one that is returned - this really confused me, as running the query through Navicat returns both headers, but I've been told this is not how raw MySQL works.
This meant that in the D6 query, weight was returned from menu_links, but in the D7 query it was returned from menu_router and was therefore always loaded into the form as "0" - hence menu items moving around when the form was submitted. However, because of DBTNG switching m.* and ml.* back isn't straightforward. The function that calls the query in menu.inc looks like this:
I couldn't quite see how I was going to switch m.* and ml.* in this, and I didn't want to change the order of the join, so I opted for switching it out for the corresponding RIGHT JOIN:
This may not be the right way to do things but I hope that with this information someone with more experience than me will be able to easily get this fixed in the right way.
Comment #6
sunWhy didn't you just flip these two lines?
Comment #7
jim0203 CreditAttribution: jim0203 commented@sun - Aaah - /that's/ how you do it :-P
New patch, flips the lines.
Comment #8
sunI don't think that you need to change FROM and JOIN.
I'm on crack. Are you, too?
Comment #9
jim0203 CreditAttribution: jim0203 commentedWhoops - didn't clear out the code like I thought I had.
But in any event I've done some further testing, and it seems that the order of those two lines doesn't dictate the order in which the tables are queried, so we're back to the original patch - reuploaded for clarity, along with a couple of screenshots showing what's going on here.
Comment #10
sunTry the simple flip again with this one.
Comment #14
jim0203 CreditAttribution: jim0203 commentedPatch seems to apply fine for me - maybe this is a naming issue? Re-attach patch from #10 with different name.
Comment #15
jhodgdonComment #17
jim0203 CreditAttribution: jim0203 commentedFor the record, the patch from #10 that I re-uploaded in #14 doesn't work. I completely agree with the rationale, but because of the way __toString() constructs queries the patch changes
to
i.e., it uses the weight to decide more than just the order of m.* and ml.* in the first line, which it should never do.
As stated, I do really like how this works and I think that it should be possible to decide the order that tables are queried. I'll have a further look at this tomorrow to see if I can get it to work.
Comment #18
jhodgdonHas someone verified that changing the order of the m.*, ml.* in a query actually causes the query to get the right information into the object consistently, independent of whether it's MySQL, PostgreSQL, etc.? I am not at all certain it does, and I'm actually not sure it does even on MySQL, and if it is an undocumented "feature" of MySQL and/or other databases and/or the PHP functions that are picking out the information from the query result, should we be relying on it?
I still think the safest course of action would be to make sure the unwanted weight field is not selected in the query at all.
You don't want m.*, you want m.* except weight.
Comment #19
jhodgdonI guess it would be in the PHP part and not in the DB, though, because the DB query returns both weight fields. It is when the db row is converted into a PHP object that it chooses one or the other.
Comment #20
jim0203 CreditAttribution: jim0203 commentedI think this is a PHP thing, yes - and it had been relied upon to work this way in Drupal 6, with whatever database backend you want to throw it at. Drupal 7 constructs the database query differently from Drupal 6, sure; but if you look a thte query itself the only thing that has changed is the order of m and ml.
Comment #21
jim0203 CreditAttribution: jim0203 commentedNew patch attached. It's commented pretty thoroughly; it uses sun's idea for sorting the fields by weight, but it un-sorts them before the seirous query-building work is done. I think it's a little unelegant but I do think that tables should be queried in the order that they're selected, and I'm not sure quite how else to do this.
Comment #22
sunTemporarily changing component, in the hope to get attention from the right folks.
Comment #23
Crell CreditAttribution: Crell commentedSilly question: Why don't we just list out the fields we want and get rid of the duplicate fields in the first place? Then the order won't matter, and the query is more readable.
Relying on a quirk of PHP that screws with the result set is a bad idea in the first place.
Comment #24
sunAlright, that's the kind of feedback / statement I wanted to hear from DB maintainer. Thanks! :)
Comment #25
jhodgdonThanks Crell.
The patch needs work, in that case, and should address only the fact that the menu system shouldn't be asking for the weight field from the menu router table, to avoid this problem completely.
Comment #26
jim0203 CreditAttribution: jim0203 commentedRemoving the duplicate fields is really straightforward, but do we want to force the query to be executed and interpreted in the order that it's built?
Comment #27
Crell CreditAttribution: Crell commentedjim0203: The query builder will do exactly as it's told in terms of the order stuff gets built. It's up to the developer to make sure that it tells the query builder something reasonable and rational. Over-using * is neither reasonable nor rational, and is in fact slower to execute as well as, in this case, buggy.
If your code depends on the order of fields in the first place, then you have a bug in your code.
Comment #28
jim0203 CreditAttribution: jim0203 commentedCrell: thanks, I thought as much. Will upload a patch in a bit.
Comment #29
jim0203 CreditAttribution: jim0203 commentedNew patch as per above. Style taken from other uses of $query->fields() already in core: e.g., in menu_tree_all_data() on line 987 of menu.inc.
All of the columns except weight are now selected from menu_router - it may be possible to select fewer columns, I don't know - but I'm guessing that since this is such a general function it will need to select everything.
Comment #30
jhodgdonThe patch looks good to me. I applied it and tested, and it appears to have fixed the problem.
Comment #31
sunWe can shorten this and there's no reason to scream.
Select all columns from {menu_router} except 'weight'.
err. Did anyone think of renaming one of both columns? That would likely allow us to simplify a couple of queries...
I'm on crack. Are you, too?
Comment #32
jhodgdonI think that renaming one of the weight columns would be a much bigger operation with many many side effects... and besides, weight is the name used for that type of column all over drupal's table structure, so probably it should stay.
sun: you didn't change the issue status. Did you mean to make it "needs work"?
Comment #33
Crell CreditAttribution: Crell commentedActually capitalizing key SQL terms is a very common style. I believe DBTNG uses it all over the place. SELECT is not screaming, it's referencing a specific part of an SQL query string.
Comment #34
jim0203 CreditAttribution: jim0203 commentedRe #31, #32 #33:
I agree with @johodgson that changing the name of the weight column is problematic. It would almost certainly require quite a few code changes and a number of tables have a weight column - it's easier just to pick and choose which weight column is needed when building a query, which is what we've done here.
And yes, I was imitating the style of other SQL comments in core when I wrote SELECT in caps.
Assuming @sun meant to change this to "needs work", I would change it back to RTBC now :-)
Comment #36
sunComment #37
webchickCan we get tests for this?
Comment #38
dman CreditAttribution: dman commentedOMG, my very first actual actual core test patch. Such a long learning road for such a small line.
eileenmcnaughton and I finally took the bull by the horns and overcame out fear of deep testing. Thanks heaps to the mentoring from Webchick here at DrupalSouth!
The error exists (even just trying to change the weight on an existing node through the edit form wouldn't work) , the above patch fixes it. This test proves it.
(is my comment too verbose?) Testbot, hit me!
Comment #39
catchI think we could skip these comments:
Since the assertions are self explanatory, otherwise looks rtbc.
Comment #40
dman CreditAttribution: dman commentedTrue!
They are largely what remains from notes to ourself as we workshopped it at a hackfest.
Figuring out tests was anything but self-explanatory for us at the time :-)
What we ended up with here is a distillation of 5 other false starts and long ways to get what we wanted using the available tools.
I WILL chop those comments out.
Then I WILL update the documentation with the half-dozen hidden functions from the "missing manual" that made me think I had to build an xpath parser myself ... But now I know better! It was a learning experience.
I'll be getting on to a few more (less verbose) tests now... :-B
Comment #41
dman CreditAttribution: dman commentedtrying again, quieter this time.
Thanks for your patience as I learn!
Comment #42
catchLast question - instead of specifying all the fields except one, we could maybe use drupal_schema_fields_sql() then unset weight similar to entity.inc loader - might be more resistant to schema changes. Rest all looks great :)
Comment #43
dman CreditAttribution: dman commentedThe actual patch - replacing one line with a long hard-coded array - does make me squirm.
Comment #44
jhodgdonRe #43 @dman: Yes, it makes me squirm too. But the same thing is done already elsewhere in core for this same reason (the underlying reason being that the two DB tables related to the menu system, two tables that are often joined, use the same column name weight for different things).
Comment #45
pwolanin CreditAttribution: pwolanin commentedLooks like a different in PDO vs. using a direct SQL query where we rely on the specified order (look at D6)
Comment #46
ksenzeeSpecifying column names is considered a best practice for several reasons. It helps prevent collisions like this, for one thing. For another, it's better for performance to specify just the columns you want, instead of blindly grabbing everything in the table. So it might seem a bit iffy, but it's actually better style IMO.
Comment #47
jhodgdonSo should we follow #42's suggestion (list all columns, remove weight), or should we RTBC the current patch in #41?
Comment #48
jhodgdonActually, my vote would be to use the patch in #41, and if someone wants to file a different issue about not listing out all the fields in queries, they can fix up all the places in the core where that's done.
However, looking at the patch, I don't think the test that was added actually tests the issue. The test should follow the sequence given in the original issue report:
- Create a few nodes and put them into the menu.
- Change the weights of the menu items on the menu admin pages.
- Edit one (or better yet all) of the nodes, not changing the menu item weights, just changing the title of the node itself.
- Verify that the menu order has not changed.
Comment #49
dman CreditAttribution: dman commentedThe test tests the actual error, not the surrounding steps.
Following the long description exactly would have to include emulating drag & drop menu re-arrangement, not just form submissions.
However, tracing the state of the database while following the the steps described in the issues report, it boiled down to the issue described in the the title here : Menu link weights are not saved, and actually reset, when using the node edit form. This partially obliterates anything done to that menu link in the other screens and is the actual cause of the described problem.
You can add any number of irrelevant steps before and after the original issue report, but this is the actual issue being exposed.
Without the patch, this test fails - which is clearly an error. With patch, it doesn't.
Comment #50
jhodgdonAh, I see what you are saying about the test. Thanks for the clarification.
Comment #51
pwolanin CreditAttribution: pwolanin commentedPatch seems to be loading several router fields that are not needed - I would consider this a bug in D6 for this function.
compare to: http://api.drupal.org/api/function/menu_tree_all_data/7
where we load only needed router columns:
I would consider it a bug if the loaded columns are different in that function and menu_link_load. Ideally we could centralize this.
Comment #52
dman CreditAttribution: dman commentedAre you suggesting an updated patch that skips some of those columns?
If so, here that is for testbot to chew on, but I don't know enough about the implications to suggest what else in that change would now need testing.
Missing out some data that used to be there could be a little dangerous, but I really dunno. It still passes all the existing tests.
Comment #53
dman CreditAttribution: dman commentedYep, side-effects.
With the above modifications, I'm now seeing
because something, somewhere, was assuming that all the required fields would be loaded and available. Could be any number of those.
Comment #54
dman CreditAttribution: dman commentedBenchmark
For the lulz, I ran a quick benchmark on the difference between a 'wildcard' lookup (before patch, no array) and the limited list of fields (as per #51 and the patch I rolled).
My benchmark may have any number of inconsistencies due to the effect of MySQL caching or things I don't know, but my gut was that
SELECT * FROM..
would be optimized better thanSELECT a,b,c,d,.... FROM
... so getting all and ignoring what we know we don't want could have benefits.Results (3 runs over 1000 lookups):
( the third time I put a consistent load on the server to see if it was affected)
I also reversed the order of operations and saw the same results.
Sooooo. Without getting too deep into arcane DB optimizations, It looks like we are 20% better off to
SELECT *
andunset($item['weight']));
than we are to be so specific about the columns we want or don't want.Being economical about what we select is measurably inefficient. Best to get the lot and filter in code.
And that approach makes me feel less squirmy also. Selecting and discarding the problem is more robust, maintainable code than a hard-coded schema list. My gut (and this benchmark) agrees with catch in #42.
You want the code? I'd be educated to hear that my back-of-the-envelope approach was wrong.
The built-in benefits of the lazy SELECT * may be common knowledge among SQL gurus, it may also be a fallacy - which is why I benchmarked.
I'm saying I think the code would be better in the long run if we didn't get the DB to to our work for us.
Comment #55
jhodgdonDoes unset($item['weight']) work? You need the weight field. You just don't want both of the weight fields, because they conflict.
Comment #56
dman CreditAttribution: dman commentedYeah, I'll admit I don't yet grok DBNG so if there is a join there that is actually getting the real weight we want as well - well I dunno then. Could it be the DB API needs the ability for us to name variables that isn't being utilized?
Would a second DB lookup for that be so bad if the JOIN is incapable of doing what we want?
I think I've reached the limit of my depth here.
What I've seen from looking at it is that selecting some but not all of the relevant fields is a minefield.
Comment #57
jhodgdonRe: #54 & %56 - yes, that was the whole problem. We need one weight field and not the other weight field. So in the join, one of the tables' fields has to be listed out (without weight) so they don't collide. I don't think there's any way around the "minefield" of having to list out the fields in that case.
Comment #58
Gábor HojtsyOk, looks like since DBTNG does not have a way to say "get me all fields except ...", we need to list all fields except the weight field ourselves. Not listing all fields seemed to cause problems in testing above and it would be an API change at this point.
So let's list everything but the weight field to get weight from the menu link. Worked in manual testing. Test hunk still included for automated testing. I also run the node menu tests locally, and they passed. Did not run the whole test suite.
Agreed this is pretty critical.
Comment #59
catchdbtng doesn't, schema API does:
http://api.drupal.org/api/function/drupal_schema_fields_sql/7
See DrupalDefaultEntityController for an example.
Comment #60
Gábor HojtsyThis simplifies the code a lot indeed.
Comment #61
catchThat's the one. Everything else looks great. Please wait for the test bot before commit.
Comment #62
dman CreditAttribution: dman commentedAwesome. THAT's what clean code looks like!
Comment #63
jhodgdonLooks like the test bot says it passed, if you click on "view details", but the cron bot hasn't come through and updated it to a nice pretty green line with PASSED all over it.
Comment #64
webchickWicked. :) Thanks, folks!!
Committed to HEAD.
Comment #66
pav CreditAttribution: pav commentedHi,
Sorry to necromance this thread back to life. I'm getting the same issue with the current release version.
It looks like includes/menu.inc has changed substantially over the last couple of years, so I can't easily adapt the patch from #60 was written (for a start, all the variable names have changed).
This is a serious pain in the neck.
Can anybody help me or at least point me in the right direction?
Thanks
Comment #67
pav CreditAttribution: pav commentedHi,
Sorry to necromance this thread back to life. I'm getting the same issue with the current release version.
It looks like includes/menu.inc has changed substantially over the last couple of years, so I can't easily adapt the patch from #60 was written (for a start, all the variable names have changed).
This is a serious pain in the neck.
Can anybody help me or at least point me in the right direction?
Thanks
Comment #68
steveaz98 CreditAttribution: steveaz98 commentedI'm seeing this issue now also, did you find a solution pav? I'm on 7.14 with all the latest modules.
Comment #69
jhodgdonAt this point, please file another issue and explain the problem you are having. This issue was closed (fixed) long ago, and if you are seeing something similar now, it likely has a completely different cause and a completely different fix. So it should be a different issue. Thanks! Also, please don't change the version on the issue -- we leave it at "7.x-dev" because that is where this issue was fixed.
Comment #70
deoashish CreditAttribution: deoashish commentedhave you got any solution of this.please help it still coming in recent version