Browse Source

Add support for cross schema joins

Marlin Cremers 10 years ago
parent
commit
93718a2380

+ 32 - 0
src/Database/Connection.php

@@ -694,6 +694,38 @@ class Connection implements ConnectionInterface
     }
 
     /**
+     * Check if cross talk is supported between two connections
+     *
+     * @param ConnectionInterface $target Connection to check cross talk with
+     *
+     * @return bool
+     */
+    public function supportsCrossWith(ConnectionInterface $target)
+    {
+        $sourceConfig = $this->config();
+        $targetConfig = $target->config();
+
+        // No need to do report cross support in case the same connection is being used
+        if ($sourceConfig['name'] === $targetConfig['name']) {
+            return false;
+        }
+
+        $configToCheck = [
+            'driver',
+            'host',
+            'port'
+        ];
+
+        foreach ($configToCheck as $config) {
+            if ((isset($sourceConfig[$config])) && ($sourceConfig[$config] !== $targetConfig[$config])) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    /**
      * Returns a new statement object that will log the activity
      * for the passed original statement instance.
      *

+ 10 - 0
src/Database/Driver.php

@@ -265,6 +265,16 @@ abstract class Driver
     }
 
     /**
+     * Returns the schema name that's being used
+     *
+     * @return string
+     */
+    public function schema()
+    {
+        return $this->_config['schema'];
+    }
+
+    /**
      * Returns last id generated for a table or sequence in database
      *
      * @param string $table table name or sequence to get last insert value from

+ 8 - 0
src/Database/Driver/Mysql.php

@@ -129,6 +129,14 @@ class Mysql extends Driver
     /**
      * {@inheritDoc}
      */
+    public function schema()
+    {
+        return $this->_config['database'];
+    }
+
+    /**
+     * {@inheritDoc}
+     */
     public function supportsDynamicConstraints()
     {
         return true;

+ 129 - 0
src/Database/Expression/CrossSchemaTableExpression.php

@@ -0,0 +1,129 @@
+<?php
+/**
+ * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
+ * Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
+ *
+ * Licensed under The MIT License
+ * For full copyright and license information, please see the LICENSE.txt
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright     Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
+ * @link          http://cakephp.org CakePHP(tm) Project
+ * @since         3.3.0
+ * @license       http://www.opensource.org/licenses/mit-license.php MIT License
+ */
+namespace Cake\Database\Expression;
+
+use Cake\Database\ExpressionInterface;
+use Cake\Database\ValueBinder;
+
+/**
+ * An expression object that represents a cross schema table name
+ *
+ * @internal
+ */
+class CrossSchemaTableExpression implements ExpressionInterface
+{
+
+    /**
+     * Name of the schema
+     *
+     * @var \Cake\Database\ExpressionInterface|string
+     */
+    protected $_schema;
+
+    /**
+     * Name of the table
+     *
+     * @var \Cake\Database\ExpressionInterface|string
+     */
+    protected $_table;
+
+    /**
+     * @inheritDoc
+     *
+     * @param string\\Cake\Database\ExpressionInterface $schema Name of the schema
+     * @param string|\Cake\Database\ExpressionInterface $table Name of the table
+     */
+    public function __construct($schema, $table)
+    {
+        $this->_schema = $schema;
+        $this->_table = $table;
+    }
+
+    /**
+     * Get or set the schema to use
+     *
+     * @param null|string|\Cake\Database\ExpressionInterface $schema The schema to set
+     * @return $this|string|\Cake\Database\ExpressionInterface The schema that has been set
+     */
+    public function schema($schema = null)
+    {
+        if ($schema !== null) {
+            $this->_schema = $schema;
+
+            return $this;
+        }
+
+        return $this->_schema;
+    }
+
+    /**
+     * Get or set the schema to use
+     *
+     * @param null|string|\Cake\Database\ExpressionInterface $table The table to set
+     * @return $this|string|\Cake\Database\ExpressionInterface The table that has been set
+     */
+    public function table($table = null)
+    {
+        if ($table !== null) {
+            $this->_table = $table;
+
+            return $this;
+        }
+
+        return $this->_table;
+    }
+
+    /**
+     * Converts the Node into a SQL string fragment.
+     *
+     * @param \Cake\Database\ValueBinder $generator Placeholder generator object
+     * @return string
+     */
+    public function sql(ValueBinder $generator)
+    {
+        $schema = $this->_schema;
+        if ($schema instanceof ExpressionInterface) {
+            $schema = $schema->sql($generator);
+        }
+
+        $table = $this->_table;
+        if ($table instanceof ExpressionInterface) {
+            $table = $table->sql($generator);
+        }
+
+        return sprintf('%s.%s', $schema, $table);
+    }
+
+    /**
+     * Iterates over each part of the expression recursively for every
+     * level of the expressions tree and executes the $visitor callable
+     * passing as first parameter the instance of the expression currently
+     * being iterated.
+     *
+     * @param callable $visitor The callable to apply to all nodes.
+     * @return void
+     */
+    public function traverse(callable $visitor)
+    {
+        if ($this->_schema instanceof ExpressionInterface) {
+            $visitor($this->_schema);
+            $this->_schema->traverse($visitor);
+        }
+        if ($this->_table instanceof ExpressionInterface) {
+            $visitor($this->_table);
+            $this->_table->traverse($visitor);
+        }
+    }
+}

+ 21 - 0
src/Database/IdentifierQuoter.php

@@ -14,6 +14,7 @@
  */
 namespace Cake\Database;
 
+use Cake\Database\Expression\CrossSchemaTableExpression;
 use Cake\Database\Expression\FieldInterface;
 use Cake\Database\Expression\IdentifierExpression;
 use Cake\Database\Expression\OrderByExpression;
@@ -90,6 +91,10 @@ class IdentifierQuoter
             $this->_quoteIdentifierExpression($expression);
             return;
         }
+        if ($expression instanceof CrossSchemaTableExpression) {
+            $this->_quoteCrossSchemaTableExpression($expression);
+            return;
+        }
     }
 
     /**
@@ -254,4 +259,20 @@ class IdentifierQuoter
             $this->_driver->quoteIdentifier($expression->getIdentifier())
         );
     }
+
+    /**
+     * Quotes the cross schema table identifier
+     *
+     * @param CrossSchemaTableExpression $expression The identifier to quote
+     * @return void
+     */
+    protected function _quoteCrossSchemaTableExpression(CrossSchemaTableExpression $expression)
+    {
+        if (!$expression->schema() instanceof ExpressionInterface) {
+            $expression->schema($this->_driver->quoteIdentifier($expression->schema()));
+        }
+        if (!$expression->table() instanceof ExpressionInterface) {
+            $expression->table($this->_driver->quoteIdentifier($expression->table()));
+        }
+    }
 }

+ 9 - 1
src/Database/QueryCompiler.php

@@ -14,6 +14,8 @@
  */
 namespace Cake\Database;
 
+use Cake\Database\Expression\QueryExpression;
+
 /**
  * Responsible for compiling a Query object into its SQL representation
  *
@@ -211,9 +213,15 @@ class QueryCompiler
     {
         $joins = '';
         foreach ($parts as $join) {
+            $subquery = $join['table'] instanceof Query || $join['table'] instanceof QueryExpression;
             if ($join['table'] instanceof ExpressionInterface) {
-                $join['table'] = '(' . $join['table']->sql($generator) . ')';
+                $join['table'] = $join['table']->sql($generator);
+            }
+
+            if ($subquery) {
+                $join['table'] = '(' . $join['table'] . ')';
             }
+
             $joins .= sprintf(' %s JOIN %s %s', $join['type'], $join['table'], $join['alias']);
             if (isset($join['conditions']) && count($join['conditions'])) {
                 $joins .= sprintf(' ON %s', $join['conditions']->sql($generator));

+ 11 - 1
src/ORM/Association.php

@@ -15,6 +15,7 @@
 namespace Cake\ORM;
 
 use Cake\Core\ConventionsTrait;
+use Cake\Database\Expression\CrossSchemaTableExpression;
 use Cake\Database\Expression\IdentifierExpression;
 use Cake\Datasource\EntityInterface;
 use Cake\Datasource\ResultSetDecorator;
@@ -524,13 +525,22 @@ abstract class Association
     {
         $target = $this->target();
         $joinType = empty($options['joinType']) ? $this->joinType() : $options['joinType'];
+
+        $table = $target->table();
+        if ($this->source()->connection()->supportsCrossWith($target->connection())) {
+            $table = new CrossSchemaTableExpression(
+                $target->connection()->driver()->schema(),
+                $table
+            );
+        }
+
         $options += [
             'includeFields' => true,
             'foreignKey' => $this->foreignKey(),
             'conditions' => [],
             'fields' => [],
             'type' => $joinType,
-            'table' => $target->table(),
+            'table' => $table,
             'finder' => $this->finder()
         ];
 

+ 28 - 0
tests/TestCase/Database/ConnectionTest.php

@@ -962,4 +962,32 @@ class ConnectionTest extends TestCase
         $connection->schemaCollection($schema);
         $this->assertSame($schema, $connection->schemaCollection());
     }
+
+    /**
+     * Tests supportsCrossWith
+     *
+     * @return void
+     */
+    public function testSupportsCrossWith()
+    {
+        $connection = new Connection(ConnectionManager::config('test'));
+        $targetConnection = new Connection(ConnectionManager::config('test'));
+
+        $this->assertFalse($connection->supportsCrossWith($targetConnection), 'The same connection can\'t used in cross');
+
+        $connection = new Connection(ConnectionManager::config('test'));
+        $targetConnection = new Connection(['name' => 'test2'] + ConnectionManager::config('test'));
+
+        $this->assertTrue($connection->supportsCrossWith($targetConnection), 'Cross should be supported on databases on the same server');
+
+        $connection = new Connection(ConnectionManager::config('test'));
+        $targetConnection = new Connection(['port' => 999999] + ConnectionManager::config('test'));
+
+        $this->assertFalse($connection->supportsCrossWith($targetConnection), 'Cross is not supported across different server instances');
+
+        $connection = new Connection(ConnectionManager::config('test'));
+        $targetConnection = new Connection(['host' => 'db2.example.com'] + ConnectionManager::config('test'));
+
+        $this->assertFalse($connection->supportsCrossWith($targetConnection), 'Cross is not supported across different server instances');
+    }
 }

+ 71 - 0
tests/TestCase/Database/Expression/CrossSchemaTableExpressionTest.php

@@ -0,0 +1,71 @@
+<?php
+/**
+ * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
+ * Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
+ *
+ * Licensed under The MIT License
+ * For full copyright and license information, please see the LICENSE.txt
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright     Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
+ * @link          http://cakephp.org CakePHP(tm) Project
+ * @since         3.3.0
+ * @license       http://www.opensource.org/licenses/mit-license.php MIT License
+ */
+namespace Cake\Test\TestCase\Database\Expression;
+
+use Cake\Database\Expression\CrossSchemaTableExpression;
+use Cake\Database\Expression\IdentifierExpression;
+use Cake\Database\ValueBinder;
+use Cake\TestSuite\TestCase;
+
+/**
+ * Tests CrossSchemaTableExpression class
+ */
+class CrossSchemaTableExpressionTest extends TestCase
+{
+
+    /**
+     * Test sql method with ExpressionInterfaces passed and without
+     */
+    public function testSql()
+    {
+        $expression = new CrossSchemaTableExpression(
+            new IdentifierExpression('schema'),
+            new IdentifierExpression('table')
+        );
+
+        $this->assertEquals('schema.table', $expression->sql(new ValueBinder()));
+
+        $expression = new CrossSchemaTableExpression('schema', 'table');
+
+        $this->assertEquals('schema.table', $expression->sql(new ValueBinder()));
+    }
+
+    /**
+     * Test traverse method with ExpressionInterfaces passed and without
+     */
+    public function testTraverse()
+    {
+        $expressions = [];
+
+        $collector = function ($e) use (&$expressions) {
+            $expressions[] = $e;
+        };
+
+        $expression = new CrossSchemaTableExpression(
+            new IdentifierExpression('schema'),
+            new IdentifierExpression('table')
+        );
+        $expression->traverse($collector);
+        $this->assertEquals([
+            new IdentifierExpression('schema'),
+            new IdentifierExpression('table')
+        ], $expressions);
+
+        $expressions = [];
+        $expression = new CrossSchemaTableExpression('schema', 'table');
+        $expression->traverse($collector);
+        $this->assertEquals([], $expressions);
+    }
+}