I found this problem when using theme_table() to create a sortable table from a query that uses db_select's addExpression() method. It should output a 3 column table. The middle column's content should be the alias created by addExpression(). Instead, the content always ends up in the last column of the table, no matter what.

I have tried this with any number of columns, and any alias added by addExpression() always ends up at the right-hand side of the table. I don't know if this is a problem with db_select or theme_table().

Details on my platform: PHP 5.2.14, PostgreSQL 8.3.11, Apache2 2.2.11

Sample code:

  // $headers for theme_table and TableSort extender
  $headers = array(
    array( 'data' => 'Manufacturer', 'field' => 'company_name', 'sort' => 'asc' ),  // FIRST column
    array( 'data' => 'Item', 'field' => 'itemlink'),  // SECOND column
    array( 'data' => 'Abbr.', 'field' => 'abbreviation')  // THIRD column
  );

  // Set up the query
  $query = db_select('battery_model_list_view');
  $query = $query->extend('TableSort');
  $query = $query->orderByHeader($headers); 
  $query->addField('battery_model_list_view', 'company_name');  // FIRST db column
  $query->addExpression("'<a href=\"/batt/details/' || batt_model_id ||'\">' || model || '</a>'", 'itemlink');  // SECOND db column
  $query->addField('battery_model_list_view', 'abbreviation');  // THIRD db column

  // Execute the query
  $result = $query->execute();
  $rows = $result->fetchAll(PDO::FETCH_ASSOC);
 
// Create $table variables for theme_table()
  $table = array(
    'header' => $headers,
    'rows' => $rows,
    'caption' => null,
    'attributes' => array(),
    'colgroups' => array(),
    'sticky' => null,
    'empty' => null,
  );

   $out = "<p>The following is a list of items.  Click the model to see details of that model</p>";
 
  $out .= theme_table($table);
 
  return $out;
CommentFileSizeAuthor
#9 tablesort-error.gif9.37 KBJohnWoltman

Comments

JohnWoltman’s picture

I updated to HEAD (October 8, 2010 @ 13:20) and now receive this error when I try to resort by a column other than the default:

PDOException: SQLSTATE[42703]: Undefined column: 7 ERROR: column "itemlink" does not exist LINE 1: SELECT itemlink AS itemlink, battery_model_list_view.company... ^: SELECT itemlink AS itemlink, battery_model_list_view.company_name AS company_name, battery_model_list_view.abbreviation AS abbreviation, '<a href="/batt/details/' || batt_model_id ||'">' || model || '</a>' AS itemlink FROM {battery_model_list_view} battery_model_list_view ORDER BY itemlink ASC; Array ( ) in battery_info_batt_list() (line 93 of /var/www/drupal/sites/default/modules/battery_info/battery_info.module).

It looks like the generated query is incorrect, because it thinks that itemlink (an expression) is a database column. This may be independent of the original issue, which still occurs even with the revision of HEAD I am using.

Crell’s picture

Title: db_select's addExpression does not honor order in which it was added » TableSort issue with expressions
Status: Active » Postponed (maintainer needs more info)
Issue tags: -theme_table

#0 is by design. The query builder tracks literal fields and expressions separately and adds them to the select list separately, fields first and then expressions. That should be fine in 99% of cases because if you're relying on field order you're going to have extremely brittle code. Remember that selects also get run through query alter, which could add fields as well, so you cannot rely on field order. Just don't do that. :-)

#1 sounds like an issue with table sort specifically, so renaming the issue accordingly. I don't have time to look at it in detail right now, though. Can you paste the code you're using to create the query?

JohnWoltman’s picture

The query is built with the D7's db_select system:

// Set up the query
  $query = db_select('battery_model_list_view');
  $query = $query->extend('TableSort');
  $query = $query->orderByHeader($headers);
  $query->addField('battery_model_list_view', 'company_name');  // FIRST db column
  $query->addExpression("'<a href=\"/batt/details/' || batt_model_id ||'\">' || model || '</a>'", 'itemlink');  // SECOND db column
  $query->addField('battery_model_list_view', 'abbreviation');  // THIRD db column

  // Execute the query
  $result = $query->execute();
  $rows = $result->fetchAll(PDO::FETCH_ASSOC);
dave reid’s picture

Category: bug » support
Status: Postponed (maintainer needs more info) » Fixed

That is a horrible mis-use of PDO. You will be much better off building that link yourself with the proper API funciton l().

JohnWoltman’s picture

Version: 7.0-alpha7 » 7.x-dev
Category: support » bug
Status: Fixed » Active

The issue occurs when using TableSort with any PDO queries with an addExpression call, not just my above example. Just checked, and this is still an issue with 7.x-dev (as of Oct 21, 2010, 12:15PM EST)

dave reid’s picture

Category: bug » support
Status: Active » Fixed

This is not the proper way to use PDO, plus you are opening yourself up to XSS attacks by not properly escaping your output. I will go to the effort to show you how it should be done:

  // Previous code...

  $query = db_select('battery_model_list_view', 'bmlv')->extend('TableSort');
  $query->fields('bmlv', array('company_name', 'batt_model_id', 'model', 'abbreviation'));
  $query->orderByHeader($headers); 
  $results = $query->execute();
  
  $rows = array();
  foreach ($results as $result) {
    $row[] = check_plain($result->company_name);
    $row[] = l($result->model, 'batt/details/' . $result->batt_model_id);
    $row[] = check_plain($result->abbreviation);
    $rows[] = $row;
  }

  // Rest of code here...
JohnWoltman’s picture

Component: database system » theme system
Status: Fixed » Active

I don't see how that fixes the bug with TableSort's handling of expressions. If I want to use my database's built-in functions or stored procedures on a set of data, Drupal should accommodate this.

I understand that the example code I gave is insecure, but it is just an example. The database is able to transform data, and I don't have to run an additional PHP loop. There is a bug, as Crell pointed out in his post. If I should start a new bug report to make this a regular bug, let me know.

If there are limitations to PDO, I can write it out as SQL I suppose.

Crell’s picture

OK, first, let's not confuse things. PDO is JUST the connection abstraction provided by PHP. The Drupal DB layer, DBTNG, lives on top of it. The Select builder is entirely Drupal code, not PDO.

Second, John, we can't fix a bug that we cannot replicate. So please, if you can confirm that there is a bug with TableSelect and supported expressions please provide sample code.

Third, "supported expressions". Drupal aims to be database agnostic. That means we cannot use a lot of SQL functions as those are almost entirely database-specific. So:

If I want to use my database's built-in functions or stored procedures on a set of data, Drupal should accommodate this.

It's not going to, at least not directly. It is not possible to do so in a database independent way. Drupal uses a database for data storage and retrieval only, not for non-trivial manipulation. That is by design.

JohnWoltman’s picture

Status: Closed (won't fix) » Active
StatusFileSize
new9.37 KB

Crell, thank you for clarifying the differences between the Drupal's database system and the PDO-provided features.

Here's an example, that should work for most databases (I'm using PostgreSQL). First, create a table

CREATE TABLE drupal_inventory_example
(
  manufacturer character(32),
  model character(32),
  number_in_stock integer
)

The first column is the manufacturer of the equipment (like Ford or Jaguar). The second column is the model (like Mustang or XJ). The third column is the number of that item in stock (like 5).

Sample data:

INSERT INTO drupal_inventory_example (manufacturer, model, number_in_stock) VALUES ('Ford', 'Taurus, 50);
INSERT INTO drupal_inventory_example (manufacturer, model, number_in_stock) VALUES ('Ford', 'Mustang', 30);
INSERT INTO drupal_inventory_example (manufacturer, model, number_in_stock) VALUES ('GMC', 'Yukon, 20);
INSERT INTO drupal_inventory_example (manufacturer, model, number_in_stock) VALUES ('Hyundai', 'Sonata', 100);
INSERT INTO drupal_inventory_example (manufacturer, model, number_in_stock) VALUES ('Hyundai', 'Accent', 83);
INSERT INTO drupal_inventory_example (manufacturer, model, number_in_stock) VALUES ('Hyundai', 'Elantra', 3);

Now I have a page callback that looks like this:

/**
 * NOTE: This is for the inventory example.
 *
 * @return
 *  Formatted HTML for the listing
 */
function sample_sales_list {
  
  $out = "";  // output (html) to be returned
  
  // Create headers for the table, and define default sorting.
  // Each header has a 'data' item (used for the title) and a 
  // 'field' item (to map it to a database column).
  $headers = array(
    // "Manufacturer" heading
    array(
      'data' => 'Manufacturer',
      'field' => 'manufacturer',
      'sort' => 'asc',
    ),

    // "Model" heading
    array(
      'data' => 'Model',
      'field' => 'model',
    ),

    // Sales goal for next week (will be 20% of stock)  - This is the THIRD column
    array(
      'data' => 'Sales Goal for next week',
      'field' => 'sales_goal',
    ),
        
    // "Inventory" heading - Fourth column
    array(
      'data' => 'Number in Stock',
      'field' => 'number_in_stock'
    ),
  );

  $query = db_select('inventory_example', 'ie');
  $query = $query->extend('TableSort');

  $query->addField('ie', 'manufacturer');
  
  $query->addField('ie', 'model');
  $query->addExpression('ie.number_in_stock * .2', 'sales_goal');
  $query->addField('ie', 'number_in_stock');
  
  $query = $query->orderByHeader($headers);
  
  
  $result = $query->execute();
  
  $rows = $result->fetchAll(PDO::FETCH_ASSOC);
  
  $table = array(
    'header' => $headers,
    'rows' => $rows,
    'caption' => null,
    'attributes' => array(),
    'colgroups' => array(),
    'sticky' => null,
    'empty' => null,
  );
  
  $out .= theme_table($table);
  
  return $out;
  
}

Notice that the third $headers element, "Sales Goal for next week," is before the "Number in Stock" column. However, when the page is created the "Sales Goal" numbers get moved to the fourth column ("Number of Stock") (see attached image). This is, of course, a made-up example, but I just tested it with HEAD from earlier today, and it still happens.

Crell’s picture

Status: Active » Closed (won't fix)

Expressions are always ordered in the dynamic select after static fields. That's by design, and not something that can be changed without heavy internal changes. If you're relying on the order of fields in a select then your code it too brittle. That has nothing to do with the TableSort extender.

The solution here is "don't rely on the field order in the select query."

JohnWoltman’s picture

But that's not the point. I think that my example maybe make too much of the column order. The important thing is the header order. What I'm saying is that the $headers used by theme_table don't work right. Even when the headers are in the order I want them to be in $headers array, when I call theme_table then it moves the 'sales_goal' *data* from the third to the fourth column, even though 'sales_goal' is the third header. It does this with any $header column that gets its results from addExpression, not addField. This is illustrated in the screenshot attached to #9.

Isn't that a bug with theme_table? I updated #9 to make it clearer.

Crell’s picture

If you skip the tablesort and just throw the result set at theme_table, what's the order there?

If the order is different, then that could be a bug.

JohnWoltman’s picture

If I remove the $query = $query->extend('TableSort'); line and the $query = $query->orderByHeader($headers); line, then the table column data is still out of order. Does this mean that the bug is in theme_table?

Crell’s picture

Category: support » bug

No, it means that this block is the issue:

  $query->addField('ie', 'model');
  $query->addExpression('ie.number_in_stock * .2', 'sales_goal');
  $query->addField('ie', 'number_in_stock');

The select query itself will put the expression after the number_in_stock, as I said. That is by design. The code appears to be doing exactly what it was written to do.

However, that does bring up an interesting question for tablesort. It means that this:

  $rows = $result->fetchAll(PDO::FETCH_ASSOC);

Is simply insufficient when you are using expressions. You'll need to reorder the fields yourself. Which is, legitimately, non-optimal. However, I see no way to change that without a major rewrite of the guts of SelectQuery to change how fields and expressions are managed, which I am loathe to do at beta 2.

Blargh.

JohnWoltman’s picture

So will this bug remain open then? I will use Dave Reid's work around (#6) for now, but like you said in #14, it's a bit of a drag on performance.

Should it be reclassified as a database system bug?

Crell’s picture

Component: theme system » database system

Yes, #6 is the solution for now. I don't know if there's anything we can do in Drupal 7 about it at this point. Someone would need to dig into the code and try to rebuild the internals of SelectQuery to see if it's even possible.

JohnWoltman’s picture

Status: Active » Postponed
berdir’s picture

Version: 7.x-dev » 8.x-dev
Status: Postponed » Active

AFAIK, "postponed" is usually used when an issue is postponed on another. In this case, I think it's better to simply move the issue to 8.x..

jhedstrom’s picture

Issue summary: View changes
Issue tags: +Needs issue summary update

Is this still an issue in 8.0.x?

ben coleman’s picture

It's certainly still an issue in 7.39. This code in the AdSense Top Page report:

  // Create our base query.
  $query = db_select('adsense_clicks', 'ads');
  $query = $query->extend('PagerDefault')->extend('TableSort');

  $query
    ->fields('ads', array('path', 'title'))
    ->groupBy('path')
    ->limit(50)
    ->orderByHeader($header);

  $count = $query->addExpression('COUNT(*)', 'count');
  $max = $query->addExpression('MAX(timestamp)', 'last');

  $result = $query->execute();

produces this query:

SELECT ads.path AS path, ads.title AS title, count AS count, COUNT(*) AS count, MAX(timestamp) AS last FROM {adsense_clicks} ads GROUP BY path ORDER BY count DESC LIMIT 50 OFFSET 0

Somehow, it thinks count is a table field (it's not), and adds it in to the select fields.

Version: 8.0.x-dev » 8.1.x-dev

Drupal 8.0.6 was released on April 6 and is the final bugfix release for the Drupal 8.0.x series. Drupal 8.0.x will not receive any further development aside from security fixes. Drupal 8.1.0-rc1 is now available and sites should prepare to update to 8.1.0.

Bug reports should be targeted against the 8.1.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.2.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.1.x-dev » 8.2.x-dev

Drupal 8.1.9 was released on September 7 and is the final bugfix release for the Drupal 8.1.x series. Drupal 8.1.x will not receive any further development aside from security fixes. Drupal 8.2.0-rc1 is now available and sites should prepare to upgrade to 8.2.0.

Bug reports should be targeted against the 8.2.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.3.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.2.x-dev » 8.3.x-dev

Drupal 8.2.6 was released on February 1, 2017 and is the final full bugfix release for the Drupal 8.2.x series. Drupal 8.2.x will not receive any further development aside from critical and security fixes. Sites should prepare to update to 8.3.0 on April 5, 2017. (Drupal 8.3.0-alpha1 is available for testing.)

Bug reports should be targeted against the 8.3.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.4.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.3.x-dev » 8.4.x-dev

Drupal 8.3.6 was released on August 2, 2017 and is the final full bugfix release for the Drupal 8.3.x series. Drupal 8.3.x will not receive any further development aside from critical and security fixes. Sites should prepare to update to 8.4.0 on October 4, 2017. (Drupal 8.4.0-alpha1 is available for testing.)

Bug reports should be targeted against the 8.4.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.5.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.4.x-dev » 8.5.x-dev

Drupal 8.4.4 was released on January 3, 2018 and is the final full bugfix release for the Drupal 8.4.x series. Drupal 8.4.x will not receive any further development aside from critical and security fixes. Sites should prepare to update to 8.5.0 on March 7, 2018. (Drupal 8.5.0-alpha1 is available for testing.)

Bug reports should be targeted against the 8.5.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.6.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.5.x-dev » 8.6.x-dev

Drupal 8.5.6 was released on August 1, 2018 and is the final bugfix release for the Drupal 8.5.x series. Drupal 8.5.x will not receive any further development aside from security fixes. Sites should prepare to update to 8.6.0 on September 5, 2018. (Drupal 8.6.0-rc1 is available for testing.)

Bug reports should be targeted against the 8.6.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.7.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

sgdev’s picture

If you're relying on the order of fields in a select then your code it too brittle.

While it has been a long time since anyone responded to this issue, I'm going to offer the suggestion that saying "code is too brittle" ignores a real problem.

If you're attempting to create complex UNIONs (as we do), and have a combination of one table without expressions, and a second table with expressions, this issue is going to cause it to fail. Everything in a UNION must be in the same order to work correctly.

The only way we can accomplish our needs is to create separate queries with separate processing code for many different functions, which is a huge waste of resources. I wish we could get around the problem by writing our own db_query, but in this case we can't even do that.

Version: 8.6.x-dev » 8.8.x-dev

Drupal 8.6.x will not receive any further development aside from security fixes. Bug reports should be targeted against the 8.8.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.9.x-dev branch. For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

Version: 8.8.x-dev » 8.9.x-dev

Drupal 8.8.7 was released on June 3, 2020 and is the final full bugfix release for the Drupal 8.8.x series. Drupal 8.8.x will not receive any further development aside from security fixes. Sites should prepare to update to Drupal 8.9.0 or Drupal 9.0.0 for ongoing support.

Bug reports should be targeted against the 8.9.x-dev branch from now on, and new development or disruptive changes should be targeted against the 9.1.x-dev branch. For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

Version: 8.9.x-dev » 9.2.x-dev

Drupal 8 is end-of-life as of November 17, 2021. There will not be further changes made to Drupal 8. Bugfixes are now made to the 9.3.x and higher branches only. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 9.2.x-dev » 9.3.x-dev

Version: 9.3.x-dev » 9.4.x-dev

Drupal 9.3.15 was released on June 1st, 2022 and is the final full bugfix release for the Drupal 9.3.x series. Drupal 9.3.x will not receive any further development aside from security fixes. Drupal 9 bug reports should be targeted for the 9.4.x-dev branch from now on, and new development or disruptive changes should be targeted for the 9.5.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 9.4.x-dev » 9.5.x-dev

Drupal 9.4.9 was released on December 7, 2022 and is the final full bugfix release for the Drupal 9.4.x series. Drupal 9.4.x will not receive any further development aside from security fixes. Drupal 9 bug reports should be targeted for the 9.5.x-dev branch from now on, and new development or disruptive changes should be targeted for the 10.1.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 9.5.x-dev » 11.x-dev

Drupal core is moving towards using a “main” branch. As an interim step, a new 11.x branch has been opened, as Drupal.org infrastructure cannot currently fully support a branch named main. New developments and disruptive changes should now be targeted for the 11.x branch. For more information, see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

luke.stewart’s picture

Status: Active » Postponed (maintainer needs more info)
Issue tags: +Bug Smash Initiative

Found this at the end of the bug smash "Needs issue summary update" queue.
Looks like it got this take with a question as to whether this was still an issue in D8.
There was no response.
Given that and there has been a good 6 years since last comment. I'm going to mark as postponed needs info.
I think if there is no engagement in another 6 months then we can probably close this.

acbramley’s picture

Status: Postponed (maintainer needs more info) » Closed (outdated)

Given the lack of updates here I'm going to close this one out.

Please feel free to reopen with an updated issue summary including steps to reproduce if this still affects you.