diff --git a/core/lib/Drupal/Core/Database/Query/Select.php b/core/lib/Drupal/Core/Database/Query/Select.php index 3be5a2f..4966744 100644 --- a/core/lib/Drupal/Core/Database/Query/Select.php +++ b/core/lib/Drupal/Core/Database/Query/Select.php @@ -44,7 +44,7 @@ class Select extends Query implements SelectInterface { * 'type' => $join_type (one of INNER, LEFT OUTER, RIGHT OUTER), * 'table' => $table, * 'alias' => $alias_of_the_table, - * 'condition' => $condition_clause_on_which_to_join, + * 'condition' => $join_condition (string or Condition object), * 'arguments' => $array_of_arguments_for_placeholders_in_the condition. * 'all_fields' => TRUE to SELECT $alias.*, FALSE or NULL otherwise. * ) @@ -52,6 +52,10 @@ class Select extends Query implements SelectInterface { * If $table is a string, it is taken as the name of a table. If it is * a Select query object, it is taken as a subquery. * + * If $join_condition is a Condition object, any arguments should be + * incorporated into the object; a separate array of arguments does not + * need to be provided. + * * @var array */ protected $tables = array(); @@ -201,6 +205,10 @@ public function arguments() { if ($table['table'] instanceof SelectInterface) { $args += $table['table']->arguments(); } + // If the join condition is an object, grab its arguments recursively. + if (!empty($table['condition']) && $table['condition'] instanceof ConditionInterface) { + $args += $table['condition']->arguments(); + } } foreach ($this->expressions as $expression) { @@ -230,6 +238,10 @@ public function compile(Connection $connection, PlaceholderInterface $queryPlace if ($table['table'] instanceof SelectInterface) { $table['table']->compile($connection, $queryPlaceholder); } + // Make sure join conditions are also compiled. + if (!empty($table['condition']) && $table['condition'] instanceof ConditionInterface) { + $table['condition']->compile($connection, $queryPlaceholder); + } } // If there are any dependent queries to UNION, compile it recursively. @@ -253,6 +265,11 @@ public function compiled() { return FALSE; } } + if (!empty($table['condition']) && $table['condition'] instanceof ConditionInterface) { + if (!$table['condition']->compiled()) { + return FALSE; + } + } } foreach ($this->union as $union) { @@ -827,7 +844,7 @@ public function __toString() { $query .= $table_string . ' ' . $this->connection->escapeTable($table['alias']); if (!empty($table['condition'])) { - $query .= ' ON ' . $table['condition']; + $query .= ' ON ' . (string) $table['condition']; } } diff --git a/core/modules/node/src/NodeGrantDatabaseStorage.php b/core/modules/node/src/NodeGrantDatabaseStorage.php index eb87a4d..276b6f8 100644 --- a/core/modules/node/src/NodeGrantDatabaseStorage.php +++ b/core/modules/node/src/NodeGrantDatabaseStorage.php @@ -157,10 +157,22 @@ public function alterQuery($query, array $tables, $op, AccountInterface $account $langcode = FALSE; } + // $tables_ref should effectively be the same as $tables, except it is a + // reference to the original copy of the data, within the $query object. + // Edits made to $tables_ref are retained within the $query object. + // $tables_ref would not be necessary if the version of $tables in + // the alterQuery arguments was passed by reference -- but that + // would technically be an API change. + $tables_ref = &$query->getTables(); + // Find all instances of the base table being joined -- could appear // more than once in the query, and could be aliased. Join each one to // the node_access table. $grants = node_access_grants($op, $account); + + // The main loop is using $tables instead of $tables_ref -- if for any + // reason the two arrays differ, technically this function has been + // told to operate on the entries listed in $tables. foreach ($tables as $nalias => $tableinfo) { $table = $tableinfo['table']; if (!($table instanceof SelectInterface) && $table == $base_table) { @@ -195,7 +207,26 @@ public function alterQuery($query, array $tables, $op, AccountInterface $account // Now handle entities. $subquery->where("$nalias.$field = na.nid"); - $query->exists($subquery); + if (empty($tableinfo['join type']) || empty($tables_ref[$nalias]['join type'])) { + $query->exists($subquery); + } + else { + // If it's a join, add the node access check to the join condition. + // This requires altering the table information -- and therefore has + // to use $tables_ref instead of $tables. + $join_cond = $query + ->andConditionGroup() + ->exists($subquery); + // Add the existing join conditions into the Condition object. + if ($tables_ref[$nalias]['condition'] instanceof ConditionInterface) { + $join_cond->condition($tables_ref[$nalias]['condition']); + } + else { + $join_cond->where($tables_ref[$nalias]['condition'], $tables_ref[$nalias]['arguments']); + $tables_ref[$nalias]['arguments'] = array(); + } + $tables_ref[$nalias]['condition'] = $join_cond; + } } } } diff --git a/core/modules/node/src/Tests/NodeAccessJoinTest.php b/core/modules/node/src/Tests/NodeAccessJoinTest.php new file mode 100644 index 0000000..b998aa3 --- /dev/null +++ b/core/modules/node/src/Tests/NodeAccessJoinTest.php @@ -0,0 +1,359 @@ + 'related_article', + 'entity_type' => 'node', + 'translatable' => FALSE, + 'entity_types' => [], + 'settings' => [ + 'target_type' => 'node', + ], + 'type' => 'entity_reference', + ]); + $field_storage->save(); + $field = FieldConfig::create([ + 'field_name' => 'related_article', + 'entity_type' => 'node', + 'bundle' => 'page', + 'label' => 'Related Article', + 'settings' => [ + 'handler' => 'default', + 'handler_settings' => [ + // Reference a single vocabulary. + 'target_bundles' => [ + 'article', + ], + ], + ], + ]); + $field->save(); + + entity_get_display('node', 'page', 'default') + ->setComponent('related_article') + ->save(); + entity_get_form_display('node', 'page', 'default') + ->setComponent('related_article', [ + 'type' => 'entity_reference_autocomplete', + ]) + ->save(); + + $field = FieldConfig::create([ + 'field_name' => 'related_article', + 'entity_type' => 'node', + 'bundle' => 'article', + 'label' => 'Related Article', + 'settings' => [ + 'handler' => 'default', + 'handler_settings' => [ + // Reference a single vocabulary. + 'target_bundles' => [ + 'article', + ], + ], + ], + ]); + $field->save(); + + entity_get_display('node', 'article', 'default') + ->setComponent('related_article') + ->save(); + entity_get_form_display('node', 'article', 'default') + ->setComponent('related_article', [ + 'type' => 'entity_reference_autocomplete', + ]) + ->save(); + + node_access_rebuild(); + \Drupal::state()->set('node_access_test.private', TRUE); + } + + /** + * Tests the accessibility of joined nodes. + * + * - Create two users with "access content" and "create article" permissions + * who can each access their own private articles but not others'. + * - Create article-type nodes with and without references to other articles. + * The articles and references represent all possible combinations of the + * tested access types. + * - Create page-type nodes referencing each of the articles, as well as a + * page with no reference. + * - Use a custom view that creates two joins between nodes and has a + * node_access tag. The view lists the page nodes, the article + * referenced by each page node, and the article referenced by each + * article. + * + * - Login with the author user and check that he does not have access to + * private nodes created by other users. Test access using total row + * count as well as checking for presence of individual page titles. + * - Repeat tests using a user with only the "access content" permission, + * confirming this user does not have access to any private nodes. + * - Repeat tests using a user with "access content" and "node test view" + * permissions, confirming this user sees the complete view. + */ + public function testNodeAccessJoin() { + + // User to add articles and test author access. + $this->authorUser = $this->drupalCreateUser(['access content', 'create article content']); + // Another user to add articles (whose private articles can not be accessed + // by authorUser). + $this->otherUser = $this->drupalCreateUser(array('access content', 'create article content')); + + // Create the articles. + + // The articles are stored in an array keyed by $article and $reference2, where + // $article is the access type of the article itself, and $reference2 is the + // access type of the reference linked to by the article. + // 'public' articles are created by otherUser with private=0. + // 'private' articles are created by otherUser with private=1. + // 'author_public' articles are created by authorUser with private=0. + // 'author_private' articles are created by authorUser with private=1. + // 'no_reference' is used for references when there is no related article. + foreach (['no_reference', 'public', 'private', 'author_public', 'author_private'] as $reference2) { + foreach (['public', 'private', 'author_public', 'author_private'] as $article) { +// // Enable this check to simulate tests in patch#299 +// if ($reference2 != 'no_reference' && ((substr($article, 0, 6) == 'author') != (substr($reference2, 0, 6) == 'author'))) { +// continue; +// } +// // Enable this check to simulate tests in patch#302 +// if ($reference2 != 'no_reference' && ((substr($article, 0, 6) == 'author') != (substr($reference2, 0, 6) == 'author')) && !(substr($article, -7) == 'private' && substr($reference2, -7) == 'private')) { +// continue; +// } + + $is_author = (substr($article, 0, 6) == 'author'); + $is_private = (substr($article, -7) == 'private'); + $edit = [ + 'type' => 'article', + 'uid' => $is_author ? $this->authorUser->id() : $this->otherUser->id(), + 'private[0][value]' => $is_private, + ]; + // The article names provide the access status of the article and the + // access status of the related article (if any). + // The naming system ensures that the text 'Article $article' will only appear + // in the view if an article with that access type is displayed in the view. (The text + // '$article' alone will appear in the titles of other nodes that reference + // an article.) + $edit['title'] = "Article $article - $reference2"; + if ($reference2 != 'no_reference') { + $edit['related_article'][0]['target_id'] = $this->articles[$reference2]['no_reference']; + } + $node = $this->drupalCreateNode($edit); + $this->articles[$article][$reference2] = $node->id(); + + $this->assertEqual((int) $is_private, (int) $node->private->value, 'The private status of the article node was properly set in the node_access_test table.' . $node->uid->target_id); + if ($reference2 != 'no reference') { + $this->assertEqual((int) $this->articles[$reference2]['no_reference'], (int) $node->related_article->target_id, 'Proper article attached to article.'); + } + } + } + + // Add a blank 'no_reference' entry to the article list, so that a page with + // no reference gets created. + $this->articles['no_reference']['no_reference'] = NULL; + + $total = 0; + $count_s_total = $count_s2_total = 0; + $count_s_public = $count_s2_public = 0; + $count_s_author = $count_s2_author = 0; + $total_public = $total_author = 0; + + // Create page nodes referencing each article, as well as a page with no reference. + foreach ($this->articles as $reference => $list) { + foreach ($list as $reference2 => $article_nid) { + $title = "Page - $reference"; + if ($reference != 'no_reference') { + $title .= " - $reference2"; + } + $edit = [ + 'type' => 'page', + 'title' => $title, + 'private[0][value]' => FALSE, + ]; + if ($article_nid) { + $edit['related_article'][0]['target_id'] = $article_nid; + } + $node = $this->drupalCreateNode($edit); + if ($article_nid) { + $this->assertEqual((int) $article_nid, (int) $node->related_article->target_id, 'Proper article attached to page.'); + } + + // Calculate totals expected for each user type + // Total number of pages. + $total++; + // Total number of primary and secondary references. + if ($reference != 'no_reference') { + $count_s_total++; + if ($reference2 != 'no_reference') { + $count_s2_total++; + } + } + // Public users only see 'public' and 'author_public' articles. + if (substr($reference, -6) == 'public') { + $count_s_public++; + if (substr($reference2, -6) == 'public') { + $count_s2_public++; + } + } + // authorUser sees 'public', 'author_public', and 'author_private' articles. + if (substr($reference, -6) == 'public' || substr($reference, 0, 6) == 'author') { + $count_s_author++; + if (substr($reference2, -6) == 'public' || substr($reference2, 0, 6) == 'author') { + $count_s2_author++; + } + } + + // $total_public and $total_author are not currently in use -- but + // represent the totals when joins are handled by adding an is-null + // check (i.e., if inaccessible references caused the entire row to be + // be hidden from view, instead of hiding just one cell of the table). + // Count of pages where all related articles are accessible by + // public users. + if (substr($reference, -7) != 'private' && substr($reference2, -7) != 'private') { + $total_public++; + } + // Count of pages where all related articles are accessible by + // authorUser. + if ($reference != 'private' && $reference2 != 'private') { + $total_author++; + } + } + } + + // Generate a view listing all the pages, and check the view's content for + // users with three different access levels. + + ViewTestData::createTestViews(get_class($this), ['node_test_views']); + + // Check the author of the 'author' articles. + $this->drupalLogin($this->authorUser); + $this->drupalGet('test-node-access-join'); + $chk_total = count($this->xpath("//td[@headers='view-title-table-column']")); + $this->assertEqual($chk_total, $total, 'Author should see ' . $total . ' rows. Actual: ' . $chk_total); + $chk_total = count($this->xpath("//td[@headers='view-title_1-table-column']")); + $this->assertEqual($chk_total, $count_s_author, 'Author should see ' . $count_s_author . ' primary references. Actual: ' . $chk_total); + $chk_total = count($this->xpath("//td[@headers='view-title_2-table-column']")); + $this->assertEqual($chk_total, $count_s2_author, 'Author should see ' . $count_s2_author . ' secondary references. Actual: ' . $chk_total); + $this->assertText('Page - no_reference', 'Author should see page with no reference.'); + $this->assertText('Page - public - no_reference', 'Author should see page with one public reference.'); + $this->assertText('Page - public - public', 'Author should see page with two public references.'); + $this->assertText('Page - author_private - no_reference', 'Author should see page with own private reference.'); + $this->assertText('Article public', 'Author should see articles with access type: public.'); + $this->assertNoText('Article private', 'Author should not see articles with access type: private.'); + $this->assertText('Article author_public', 'Author should see articles with access type: author_public.'); + $this->assertText('Article author_private', 'Author should see articles with access type: author_private.'); + // Following test is no longer relevant. + //$this->assertNoText('- private', 'Author should not see pages related to others\' private articles.'); + + // Check a regular user (who did not author any articles). + $this->regularUser = $this->drupalCreateUser(['access content']); + $this->drupalLogin($this->regularUser); + $this->drupalGet('test-node-access-join'); + $chk_total = count($this->xpath("//td[@headers='view-title-table-column']")); + $this->assertEqual($chk_total, $total, 'Public user should see ' . $total . ' rows. Actual: ' . $chk_total); + $chk_total = count($this->xpath("//td[@headers='view-title_1-table-column']")); + $this->assertEqual($chk_total, $count_s_public, 'Public user should see ' . $count_s_public . ' primary references. Actual: ' . $chk_total); + $chk_total = count($this->xpath("//td[@headers='view-title_2-table-column']")); + $this->assertEqual($chk_total, $count_s2_public, 'Public user should see ' . $count_s2_public . ' secondary references. Actual: ' . $chk_total); + $this->assertText('Page - no_reference', 'Public user should see page with no reference.'); + $this->assertText('Page - public - no_reference', 'Public user should see page with one public reference.'); + $this->assertText('Page - public - public', 'Public user should see page with two public references.'); + $this->assertText('Article public', 'Public user should see articles with access type: public.'); + $this->assertNoText('Article private', 'Public user should not see articles with access type: private.'); + $this->assertText('Article author_public', 'Public user should see articles with access type: author_public.'); + $this->assertNoText('Article author_private', 'Public user should not see articles with access type: author_private.'); + // Following test is no longer relevant. + //$this->assertNoText('private', 'Public user should not see pages related to any private articles.'); + + // Check a user with the special 'node test view' permission, who should + // be able to view all pages and articles. + $this->accessUser = $this->drupalCreateUser(['access content', 'node test view']); + $this->drupalLogin($this->accessUser); + $this->drupalGet('test-node-access-join'); + $chk_total = count($this->xpath("//td[@headers='view-title-table-column']")); + $this->assertEqual($chk_total, $total, 'Full-access user should see ' . $total . ' rows. Actual: ' . $chk_total); + $chk_total = count($this->xpath("//td[@headers='view-title_1-table-column']")); + $this->assertEqual($chk_total, $count_s_total, 'Full-access user should see ' . $count_s_total . ' primary references. Actual: ' . $chk_total); + $chk_total = count($this->xpath("//td[@headers='view-title_2-table-column']")); + $this->assertEqual($chk_total, $count_s2_total, 'Full-access user should see ' . $count_s2_total . ' secondary references. Actual: ' . $chk_total); + $this->assertText('Page - no_reference', 'Full-access user should see page with no reference.'); + $this->assertText('Page - public - no_reference', 'Full-access user should see page with one public reference.'); + $this->assertText('Page - public - public', 'Full-access user should see page with two public references.'); + $this->assertText('Page - author_private - no_reference', 'Full-access user should see page with author_private reference.'); + $this->assertText('Article public', 'Full-access user should see articles with access type: public.'); + $this->assertText('Article private', 'Full-access user should see articles with access type: private.'); + $this->assertText('Article author_public', 'Full-access user should see articles with access type: author_public.'); + $this->assertText('Article author_private', 'Full-access user should see articles with access type: author_private.'); + } +} diff --git a/core/modules/node/tests/modules/node_test_views/test_views/views.view.test_node_access_join.yml b/core/modules/node/tests/modules/node_test_views/test_views/views.view.test_node_access_join.yml new file mode 100644 index 0000000..1286118 --- /dev/null +++ b/core/modules/node/tests/modules/node_test_views/test_views/views.view.test_node_access_join.yml @@ -0,0 +1,339 @@ +uuid: 0a62a0f4-8322-49c3-964d-770997ac5ac0 +langcode: en +status: true +dependencies: + config: + - node.type.page + module: + - node + - user +id: test_node_access_join +label: 'Test Node Access Join' +module: views +description: '' +tag: '' +base_table: node_field_data +base_field: nid +core: 8.x +display: + default: + display_plugin: default + id: default + display_title: Master + position: 0 + display_options: + access: + type: perm + options: + perm: 'access content' + cache: + type: tag + options: { } + query: + type: views_query + options: + disable_sql_rewrite: false + distinct: false + replica: false + query_comment: '' + query_tags: { } + exposed_form: + type: basic + options: + submit_button: Apply + reset_button: false + reset_button_label: Reset + exposed_sorts_label: 'Sort by' + expose_sort_order: true + sort_asc_label: Asc + sort_desc_label: Desc + pager: + type: full + options: + items_per_page: 1000 + offset: 0 + id: 0 + total_pages: null + expose: + items_per_page: false + items_per_page_label: 'Items per page' + items_per_page_options: '5, 10, 25, 50' + items_per_page_options_all: false + items_per_page_options_all_label: '- All -' + offset: false + offset_label: Offset + tags: + previous: '‹ Previous' + next: 'Next ›' + first: '« First' + last: 'Last »' + quantity: 9 + style: + type: table + row: + type: fields + fields: + title: + id: title + table: node_field_data + field: title + entity_type: node + entity_field: title + alter: + alter_text: false + make_link: false + absolute: false + trim: false + word_boundary: false + ellipsis: false + strip_tags: false + html: false + hide_empty: false + empty_zero: false + settings: + link_to_entity: true + plugin_id: field + relationship: none + group_type: group + admin_label: '' + label: Title + exclude: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: true + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_alter_empty: true + click_sort_column: value + type: string + group_column: value + group_columns: { } + group_rows: true + delta_limit: 0 + delta_offset: 0 + delta_reversed: false + delta_first_last: false + multi_type: separator + separator: ', ' + field_api_classes: false + title_1: + id: title_1 + table: node_field_data + field: title + relationship: related_article + group_type: group + admin_label: '' + label: Article + exclude: false + alter: + alter_text: false + text: '' + make_link: false + path: '' + absolute: false + external: false + replace_spaces: false + path_case: none + trim_whitespace: false + alt: '' + rel: '' + link_class: '' + prefix: '' + suffix: '' + target: '' + nl2br: false + max_length: 0 + word_boundary: true + ellipsis: true + more_link: false + more_link_text: '' + more_link_path: '' + strip_tags: false + trim: false + preserve_tags: '' + html: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: true + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_empty: false + empty_zero: false + hide_alter_empty: true + click_sort_column: value + type: string + settings: + link_to_entity: false + group_column: value + group_columns: { } + group_rows: true + delta_limit: 0 + delta_offset: 0 + delta_reversed: false + delta_first_last: false + multi_type: separator + separator: ', ' + field_api_classes: false + entity_type: node + entity_field: title + plugin_id: field + title_2: + id: title_2 + table: node_field_data + field: title + relationship: related_article_2 + group_type: group + admin_label: '' + label: 'Article 2' + exclude: false + alter: + alter_text: false + text: '' + make_link: false + path: '' + absolute: false + external: false + replace_spaces: false + path_case: none + trim_whitespace: false + alt: '' + rel: '' + link_class: '' + prefix: '' + suffix: '' + target: '' + nl2br: false + max_length: 0 + word_boundary: true + ellipsis: true + more_link: false + more_link_text: '' + more_link_path: '' + strip_tags: false + trim: false + preserve_tags: '' + html: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: true + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_empty: false + empty_zero: false + hide_alter_empty: true + click_sort_column: value + type: string + settings: + link_to_entity: false + group_column: value + group_columns: { } + group_rows: true + delta_limit: 0 + delta_offset: 0 + delta_reversed: false + delta_first_last: false + multi_type: separator + separator: ', ' + field_api_classes: false + entity_type: node + entity_field: title + plugin_id: field + filters: + status: + value: true + table: node_field_data + field: status + plugin_id: boolean + entity_type: node + entity_field: status + id: status + expose: + operator: '' + group: 1 + type: + id: type + table: node_field_data + field: type + value: + page: page + entity_type: node + entity_field: type + plugin_id: bundle + sorts: + title: + id: title + table: node_field_data + field: title + relationship: none + group_type: group + admin_label: '' + order: ASC + exposed: false + expose: + label: '' + entity_type: node + entity_field: title + plugin_id: standard + title: 'Test Reference Access' + header: { } + footer: { } + empty: { } + relationships: + related_article: + id: related_article + table: node__related_article + field: related_article + relationship: none + group_type: group + admin_label: 'Page related article' + required: false + plugin_id: standard + related_article_2: + id: related_article_2 + table: node__related_article + field: related_article + relationship: related_article + group_type: group + admin_label: 'Article related article' + required: false + plugin_id: standard + arguments: { } + display_extenders: { } + cache_metadata: + max-age: 0 + contexts: + - 'languages:language_content' + - 'languages:language_interface' + - url.query_args + - 'user.node_grants:view' + - user.permissions + tags: { } + page_1: + display_plugin: page + id: page_1 + display_title: Page + position: 1 + display_options: + display_extenders: { } + path: test-node-access-join + cache_metadata: + max-age: 0 + contexts: + - 'languages:language_content' + - 'languages:language_interface' + - url.query_args + - 'user.node_grants:view' + - user.permissions + tags: { } diff --git a/core/modules/system/src/Tests/Database/SelectComplexTest.php b/core/modules/system/src/Tests/Database/SelectComplexTest.php index 880c6eb..50c3b51 100644 --- a/core/modules/system/src/Tests/Database/SelectComplexTest.php +++ b/core/modules/system/src/Tests/Database/SelectComplexTest.php @@ -379,4 +379,49 @@ function testSelectWithRowCount() { $this->assertTrue($exception, 'Exception was thrown'); } + /** + * Test that join conditions can use Condition objects. + */ + function testJoinConditionObject() { + // Same test as testDefaultJoin, but with a Condition object. + $query = db_select('test_task', 't'); + $join_cond = db_and()->where('t.pid = p.id'); + $people_alias = $query->join('test', 'p', $join_cond); + $name_field = $query->addField($people_alias, 'name', 'name'); + $query->addField('t', 'task', 'task'); + $priority_field = $query->addField('t', 'priority', 'priority'); + + $query->orderBy($priority_field); + $result = $query->execute(); + + $num_records = 0; + $last_priority = 0; + foreach ($result as $record) { + $num_records++; + $this->assertTrue($record->$priority_field >= $last_priority, 'Results returned in correct order.'); + $this->assertNotEqual($record->$name_field, 'Ringo', 'Taskless person not selected.'); + $last_priority = $record->$priority_field; + } + + $this->assertEqual($num_records, 7, 'Returned the correct number of rows.'); + + // Test a condition object that creates placeholders. + $t1_name = 'John'; + $t2_name = 'George'; + $join_cond = db_and() + ->condition('t1.name', $t1_name) + ->condition('t2.name', $t2_name); + $query = db_select('test', 't1'); + $query->innerJoin('test', 't2', $join_cond); + $query->addField('t1', 'name', 't1_name'); + $query->addField('t2', 'name', 't2_name'); + + $num_records = $query->countQuery()->execute()->fetchField(); + $this->assertEqual($num_records, 1, 'Query expected to return 1 row. Actual: ' . $num_records); + if ($num_records==1) { + $record = $query->execute()->fetchObject(); + $this->assertEqual($record->t1_name, $t1_name, 'Query expected to retrieve name ' . $t1_name . ' from table t1. Actual: ' . $record->t1_name); + $this->assertEqual($record->t2_name, $t2_name, 'Query expected to retrieve name ' . $t2_name . ' from table t2. Actual: ' . $record->t2_name); + } + } }