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/tests/modules/node_access_test/node_access_test.module b/core/modules/node/tests/modules/node_access_test/node_access_test.module
index e195ea8..1338eac 100644
--- a/core/modules/node/tests/modules/node_access_test/node_access_test.module
+++ b/core/modules/node/tests/modules/node_access_test/node_access_test.module
@@ -78,7 +78,7 @@ function node_access_test_node_grants($account, $op) {
 function node_access_test_node_access_records(NodeInterface $node) {
   $grants = array();
   // For NodeAccessBaseTableTestCase, only set records for private nodes.
-  if (!\Drupal::state()->get('node_access_test.private') || $node->private->value) {
+  if (!\Drupal::state()->get('node_access_test.private') || (isset($node->private) && $node->private->value)) {
     // Groups 8888 and 8889 for the node_access_test realm both receive a view
     // grant for all controlled nodes. See node_access_test_node_grants().
     $grants[] = array(
