├── .gitignore ├── src ├── Query │ ├── Grammars │ │ ├── UnsupportedGrammarException.php │ │ ├── SQLiteGrammar.php │ │ ├── PostgresGrammar.php │ │ ├── SqlServerGrammar.php │ │ ├── MySqlGrammar.php │ │ └── Grammar.php │ ├── Expression.php │ ├── OutfileClause.php │ ├── JoinClause.php │ ├── InsertBuffer.php │ └── InfileClause.php ├── Exception │ ├── ExceptionHandlerInterface.php │ ├── QueryException.php │ ├── ConnectionException.php │ └── ExceptionHandler.php ├── Connectors │ ├── ConnectorInterface.php │ ├── ConnectionFactoryInterface.php │ ├── SQLiteConnector.php │ ├── SqlServerConnector.php │ ├── Connector.php │ ├── PostgresConnector.php │ ├── MySqlConnector.php │ └── ConnectionFactory.php ├── ConnectionResolverInterface.php ├── QueryLogger.php ├── ConnectionInterface.php ├── ConnectionResolver.php └── Connection.php ├── docker ├── php │ └── Dockerfile └── docker-compose.yml ├── .travis.yml ├── tests ├── unit │ └── Database │ │ ├── ExpressionTest.php │ │ ├── QueryLoggerTest.php │ │ ├── DatabaseJoinMemoryLeakTest.php │ │ ├── DatabaseConnectionResolverTest.php │ │ ├── DatabaseConnectionFactoryTest.php │ │ ├── DatabaseConnectorTest.php │ │ └── DatabaseConnectionTest.php └── integration │ ├── config.php │ ├── AbstractDatabaseIntegrationTest.php │ ├── DatabaseConnectionIntegrationTest.php │ └── DatabaseQueryBuilderIntegrationTest.php ├── Makefile ├── composer.json ├── phpunit.xml ├── LICENSE ├── .github └── workflows │ └── php.yml └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | vendor 2 | composer.phar 3 | composer.lock 4 | .phpunit.result.cache -------------------------------------------------------------------------------- /src/Query/Grammars/UnsupportedGrammarException.php: -------------------------------------------------------------------------------- 1 | assertEquals('test expression = 1', (string)$expression); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /docker/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | mysql: 5 | image: mysql 6 | restart: unless-stopped 7 | ports: 8 | - 3306:3306 9 | environment: 10 | MYSQL_ROOT_PASSWORD: password 11 | command: --secure-file-priv="" 12 | php: 13 | build: php 14 | restart: unless-stopped 15 | volumes: 16 | - ../:/app 17 | working_dir: /app 18 | profiles: 19 | - donotstart 20 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | COMPOSE := docker compose -f docker/docker-compose.yml 2 | 3 | all: up install test 4 | 5 | up: 6 | $(COMPOSE) up -d 7 | 8 | install: 9 | $(COMPOSE) run --rm php composer install 10 | 11 | test: 12 | $(COMPOSE) run --rm php composer run-script test 13 | 14 | coverage: 15 | $(COMPOSE) run -e XDEBUG_MODE=coverage --rm php vendor/bin/phpunit --coverage-clover build/logs/clover.xml 16 | 17 | down: 18 | $(COMPOSE) down -v 19 | 20 | ci: up install coverage down -------------------------------------------------------------------------------- /src/Exception/QueryException.php: -------------------------------------------------------------------------------- 1 | code = $previous->getCode(); 18 | 19 | if ($previous instanceof PDOException) { 20 | $this->errorInfo = $previous->errorInfo; 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Exception/ConnectionException.php: -------------------------------------------------------------------------------- 1 | code = $previous->getCode(); 18 | 19 | if ($previous instanceof PDOException) { 20 | $this->errorInfo = $previous->errorInfo; 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/ConnectionResolverInterface.php: -------------------------------------------------------------------------------- 1 | =5.4.0", 22 | "psr/log": "~1.0" 23 | }, 24 | "require-dev": { 25 | "mockery/mockery": "*", 26 | "php-coveralls/php-coveralls": "^2.5", 27 | "phpunit/phpunit": "~9.0" 28 | }, 29 | "autoload": { 30 | "psr-4": { 31 | "Database\\": "src" 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/QueryLogger.php: -------------------------------------------------------------------------------- 1 | queryLog[] = array($message, $context); 15 | } 16 | 17 | /** 18 | * Get the connection query log. 19 | * 20 | * @return array 21 | */ 22 | public function getQueryLog() 23 | { 24 | return $this->queryLog; 25 | } 26 | 27 | /** 28 | * Clear the query log. 29 | * 30 | * @return LogArray 31 | */ 32 | public function flushQueryLog() 33 | { 34 | $this->queryLog = array(); 35 | 36 | return $this; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | src 6 | 7 | 8 | vendor 9 | 10 | 11 | 12 | 13 | ./tests/ 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/Query/Expression.php: -------------------------------------------------------------------------------- 1 | value = $value; 22 | } 23 | 24 | /** 25 | * Get the value of the expression. 26 | * 27 | * @return mixed 28 | */ 29 | public function getValue() 30 | { 31 | return $this->value; 32 | } 33 | 34 | /** 35 | * Get the value of the expression. 36 | * 37 | * @return string 38 | */ 39 | public function __toString() 40 | { 41 | return (string)$this->getValue(); 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /tests/integration/config.php: -------------------------------------------------------------------------------- 1 | 'mysql', 5 | 'port' => 3306, 6 | 'driver' => 'mysql', 7 | 'username' => 'root', 8 | 'password' => 'password', 9 | 'charset' => 'utf8', 10 | 'collation' => 'utf8_unicode_ci', 11 | 'prefix' => '', 12 | 'options' => array( 13 | PDO::ATTR_EMULATE_PREPARES => false 14 | ) 15 | ), 16 | array( 17 | 'host' => '127.0.0.1', 18 | 'port' => 3306, 19 | 'driver' => 'mysql', 20 | 'username' => 'root', 21 | 'password' => 'root', 22 | 'charset' => 'utf8', 23 | 'collation' => 'utf8_unicode_ci', 24 | 'prefix' => '', 25 | 'options' => array( 26 | PDO::ATTR_EMULATE_PREPARES => false 27 | ) 28 | ) 29 | ); 30 | -------------------------------------------------------------------------------- /src/Connectors/ConnectionFactoryInterface.php: -------------------------------------------------------------------------------- 1 | 11 | * array( 12 | * 'read' => array( 13 | * 'host' => '192.168.1.1', 14 | * ), 15 | * 'write' => array( 16 | * 'host' => '196.168.1.2' 17 | * ), 18 | * 'driver' => 'mysql', 19 | * 'database' => 'database', 20 | * 'username' => 'root', 21 | * 'password' => '', 22 | * 'charset' => 'utf8', 23 | * 'collation' => 'utf8_unicode_ci', 24 | * 'prefix' => '', 25 | * 'lazy' => true/false 26 | * ) 27 | * 28 | */ 29 | interface ConnectionFactoryInterface 30 | { 31 | /** 32 | * Establish a PDO connection based on the configuration. 33 | * 34 | * @param array $config 35 | * @return \Database\Connection 36 | */ 37 | public function make(array $config); 38 | } 39 | -------------------------------------------------------------------------------- /tests/unit/Database/QueryLoggerTest.php: -------------------------------------------------------------------------------- 1 | 'bar')), 7 | array('message2', array('bar' => 'foo')), 8 | ); 9 | 10 | public function testItStoresAndLogsQueries() 11 | { 12 | $log = new \Database\QueryLogger(); 13 | 14 | foreach($this->logMessages as $messages) 15 | { 16 | $log->debug($messages[0], $messages[1]); 17 | } 18 | 19 | $this->assertEquals($this->logMessages, $log->getQueryLog()); 20 | 21 | // Should be able to fetch the messages more than once 22 | $this->assertEquals($this->logMessages, $log->getQueryLog()); 23 | } 24 | 25 | public function testItFlushesQueries() 26 | { 27 | $log = new \Database\QueryLogger(); 28 | 29 | foreach($this->logMessages as $messages) 30 | { 31 | $log->debug($messages[0], $messages[1]); 32 | } 33 | 34 | $this->assertEquals(array(), $log->flushQueryLog()->getQueryLog()); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Joseph Green 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/php.yml: -------------------------------------------------------------------------------- 1 | name: Database Build 2 | 3 | on: 4 | push: 5 | branches: [master, actions] 6 | pull_request: 7 | branches: [master] 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v3 18 | 19 | - name: Validate composer.json and composer.lock 20 | run: composer validate --strict 21 | 22 | - name: Cache Composer packages 23 | id: composer-cache 24 | uses: actions/cache@v3 25 | with: 26 | path: vendor 27 | key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }} 28 | restore-keys: | 29 | ${{ runner.os }}-php- 30 | 31 | - name: Install dependencies 32 | run: composer install --prefer-dist --no-progress 33 | 34 | - name: Create build output folder 35 | run: mkdir -p build/logs 36 | 37 | - name: Run test suite 38 | run: make ci 39 | 40 | # - name: Upload coverage results to Coveralls 41 | # env: 42 | # COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }} 43 | # run: php vendor/bin/php-coveralls --coverage_clover=build/logs/clover.xml -v 44 | -------------------------------------------------------------------------------- /tests/integration/AbstractDatabaseIntegrationTest.php: -------------------------------------------------------------------------------- 1 | connection = $factory->make($config); 23 | 24 | $this->createTable(); 25 | 26 | return; 27 | } 28 | catch(\PDOException $e) {} 29 | } 30 | 31 | throw $e; 32 | } 33 | 34 | private function createTable() 35 | { 36 | $this->connection->query("CREATE DATABASE IF NOT EXISTS test"); 37 | 38 | $this->connection->query("CREATE TABLE IF NOT EXISTS $this->tableName (`name` varchar(255),`value` integer(8)) ENGINE=InnoDB DEFAULT CHARSET=utf8"); 39 | 40 | $this->connection->query("TRUNCATE TABLE $this->tableName"); 41 | } 42 | } -------------------------------------------------------------------------------- /src/Connectors/SQLiteConnector.php: -------------------------------------------------------------------------------- 1 | getOptions($config); 17 | 18 | // SQLite supports "in-memory" databases that only last as long as the owning 19 | // connection does. These are useful for tests or for short lifetime store 20 | // querying. In-memory databases may only have a single open connection. 21 | if ($config['database'] == ':memory:') { 22 | return $this->createConnection('sqlite::memory:', $config, $options); 23 | } 24 | 25 | $path = realpath($config['database']); 26 | 27 | // Here we'll verify that the SQLite database exists before going any further 28 | // as the developer probably wants to know if the database exists and this 29 | // SQLite driver will not throw any exception if it does not by default. 30 | if ($path === false) { 31 | throw new \InvalidArgumentException("Database does not exist."); 32 | } 33 | 34 | return $this->createConnection("sqlite:{$path}", $config, $options); 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /tests/integration/DatabaseConnectionIntegrationTest.php: -------------------------------------------------------------------------------- 1 | connection->getPdo(); 8 | $this->assertInstanceOf('PDO', $pdo); 9 | 10 | $driverName = $this->connection->getDriverName(); 11 | $this->assertEquals('mysql', $driverName); 12 | 13 | $grammar = $this->connection->getQueryGrammar(); 14 | $this->assertInstanceOf('Database\Query\Grammars\MySqlGrammar', $grammar); 15 | } 16 | 17 | public function testItPerformsTransactions() 18 | { 19 | $this->connection->transaction(function($connection){ 20 | $connection->query("INSERT INTO $this->tableName (name, value) VALUES (?,?)", array('joe', 1)); 21 | }); 22 | 23 | $rows = $this->connection->fetchAll("SELECT * FROM $this->tableName"); 24 | 25 | $this->assertCount(1, $rows); 26 | $this->assertEquals(array('name' => 'joe', 'value' => 1), $rows[0]); 27 | 28 | try{ 29 | $this->connection->transaction(function($connection){ 30 | $connection->query("INSERT INTO $this->tableName (name, value) VALUES (?,?)", array('joseph', 2)); 31 | 32 | throw new \Exception("rollback"); 33 | }); 34 | }catch (\Exception $e){} 35 | 36 | $rows = $this->connection->fetchAll("SELECT * FROM $this->tableName"); 37 | 38 | $this->assertCount(1, $rows); 39 | } 40 | } -------------------------------------------------------------------------------- /tests/unit/Database/DatabaseJoinMemoryLeakTest.php: -------------------------------------------------------------------------------- 1 | getBuilder(); 16 | 17 | $this->runMemoryTest(function() use($builderMain){ 18 | $builder = $builderMain->newQuery(); 19 | $builder->select('*')->from('users'); 20 | 21 | }); 22 | } 23 | 24 | public function testItDoesNotLeakMemoryOnNewQueryWithJoin() 25 | { 26 | $builderMain = $this->getBuilder(); 27 | 28 | $this->runMemoryTest(function() use($builderMain){ 29 | $builder = $builderMain->newQuery(); 30 | $builder->select('*')->join('new', 'col', '=', 'col2')->from('users'); 31 | 32 | }); 33 | } 34 | 35 | protected function runMemoryTest(\Closure $callback) 36 | { 37 | $i = 5; 38 | 39 | $last = null; 40 | 41 | while($i--) 42 | { 43 | $callback(); 44 | 45 | $prev = $last; 46 | $last = memory_get_usage(); 47 | } 48 | 49 | $this->assertEquals($prev, $last); 50 | } 51 | 52 | 53 | protected function getBuilder() 54 | { 55 | $grammar = new Database\Query\Grammars\Grammar; 56 | return new Builder(m::mock('Database\ConnectionInterface'), $grammar); 57 | } 58 | 59 | 60 | } 61 | -------------------------------------------------------------------------------- /tests/unit/Database/DatabaseConnectionResolverTest.php: -------------------------------------------------------------------------------- 1 | array( 17 | 'foo' => 'bar' 18 | ), 19 | ); 20 | 21 | $factory = m::mock('Database\Connectors\ConnectionFactory'); 22 | 23 | $connectionMock = m::mock('stdClass'); 24 | 25 | $factory->shouldReceive('make')->once()->with($configs['test1'])->andReturn($connectionMock); 26 | 27 | $resolver = new \Database\ConnectionResolver($configs, $factory); 28 | 29 | $this->assertTrue($resolver->hasConnection('test1')); 30 | 31 | $connection = $resolver->connection('test1'); 32 | 33 | $this->assertSame($connectionMock, $connection); 34 | } 35 | 36 | public function testItReturnsADefaultConnection() 37 | { 38 | $configs = array( 39 | 'test' => array( 40 | 'foo' => 'bar' 41 | ), 42 | ); 43 | 44 | $factory = m::mock('Database\Connectors\ConnectionFactory'); 45 | 46 | $connectionMock = m::mock('stdClass'); 47 | 48 | $factory->shouldReceive('make')->once()->with($configs['test'])->andReturn($connectionMock); 49 | 50 | $resolver = new \Database\ConnectionResolver($configs, $factory); 51 | 52 | $resolver->setDefaultConnection('test'); 53 | 54 | $connection = $resolver->connection(); 55 | 56 | $this->assertSame($connectionMock, $connection); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Connectors/SqlServerConnector.php: -------------------------------------------------------------------------------- 1 | PDO::CASE_NATURAL, 15 | PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, 16 | PDO::ATTR_ORACLE_NULLS => PDO::NULL_NATURAL, 17 | PDO::ATTR_STRINGIFY_FETCHES => false, 18 | ); 19 | 20 | /** 21 | * Establish a database connection. 22 | * 23 | * @param array $config 24 | * @return \PDO 25 | */ 26 | public function connect(array $config) 27 | { 28 | $options = $this->getOptions($config); 29 | 30 | return $this->createConnection($this->getDsn($config), $config, $options); 31 | } 32 | 33 | /** 34 | * Create a DSN string from a configuration. 35 | * 36 | * @param array $config 37 | * @return string 38 | */ 39 | protected function getDsn(array $config) 40 | { 41 | extract($config); 42 | 43 | // First we will create the basic DSN setup as well as the port if it is in 44 | // in the configuration options. This will give us the basic DSN we will 45 | // need to establish the PDO connections and return them back for use. 46 | $port = isset($config['port']) ? ',' . $port : ''; 47 | 48 | if (in_array('dblib', $this->getAvailableDrivers())) { 49 | return "dblib:host={$host}{$port};dbname={$database}"; 50 | } else { 51 | $dbName = $database != '' ? ";Database={$database}" : ''; 52 | 53 | return "sqlsrv:Server={$host}{$port}{$dbName}"; 54 | } 55 | } 56 | 57 | /** 58 | * Get the available PDO drivers. 59 | * 60 | * @return array 61 | */ 62 | protected function getAvailableDrivers() 63 | { 64 | return PDO::getAvailableDrivers(); 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /src/Connectors/Connector.php: -------------------------------------------------------------------------------- 1 | PDO::CASE_NATURAL, 16 | PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, 17 | PDO::ATTR_ORACLE_NULLS => PDO::NULL_NATURAL, 18 | PDO::ATTR_STRINGIFY_FETCHES => false, 19 | PDO::ATTR_EMULATE_PREPARES => false, 20 | ); 21 | 22 | /** 23 | * Get the PDO options based on the configuration. 24 | * 25 | * @param array $config 26 | * @return array 27 | */ 28 | public function getOptions(array $config) 29 | { 30 | $options = isset($config['options']) ? $config['options'] : array(); 31 | 32 | return array_diff_key($this->options, $options) + $options; 33 | } 34 | 35 | /** 36 | * Create a new PDO connection. 37 | * 38 | * @param string $dsn 39 | * @param array $config 40 | * @param array $options 41 | * @return \PDO 42 | */ 43 | public function createConnection($dsn, array $config, array $options) 44 | { 45 | $username = isset($config['username']) ? $config['username'] : null; 46 | 47 | $password = isset($config['password']) ? $config['password'] : null; 48 | 49 | try 50 | { 51 | return new PDO($dsn, $username, $password, $options); 52 | }catch (\PDOException $e) 53 | { 54 | throw new ConnectionException("Connection to '$dsn' failed: " . $e->getMessage(), $e); 55 | } 56 | 57 | } 58 | 59 | /** 60 | * Get the default PDO connection options. 61 | * 62 | * @return array 63 | */ 64 | public function getDefaultOptions() 65 | { 66 | return $this->options; 67 | } 68 | 69 | /** 70 | * Set the default PDO connection options. 71 | * 72 | * @param array $options 73 | * @return void 74 | */ 75 | public function setDefaultOptions(array $options) 76 | { 77 | $this->options = $options; 78 | } 79 | 80 | } 81 | -------------------------------------------------------------------------------- /tests/integration/DatabaseQueryBuilderIntegrationTest.php: -------------------------------------------------------------------------------- 1 | connection->table($this->tableName) 8 | ->insert(array( 9 | 'name' => 'joe', 10 | 'value' => 1 11 | )); 12 | 13 | $this->assertEquals(1, $statement->rowCount()); 14 | 15 | $statement = $this->connection->table($this->tableName) 16 | ->where('name', '=', 'joe') 17 | ->update(array( 18 | 'value' => 5 19 | )); 20 | 21 | $this->assertEquals(1, $statement->rowCount()); 22 | 23 | $this->connection->table($this->tableName) 24 | ->where('name', '=', 'joe') 25 | ->increment('value'); 26 | 27 | $rows = $this->connection->table($this->tableName)->get(); 28 | 29 | $this->assertEquals(array( 30 | array('name' => 'joe', 'value' => 6) 31 | ), $rows); 32 | 33 | $statement = $this->connection->table($this->tableName) 34 | ->where('name', '=', 'joe') 35 | ->delete(); 36 | 37 | $this->assertEquals(1, $statement->rowCount()); 38 | 39 | $exists = $this->connection->table($this->tableName) 40 | ->where('name', '=', 'joe') 41 | ->exists(); 42 | 43 | $this->assertFalse($exists); 44 | 45 | } 46 | 47 | public function testOutfile() 48 | { 49 | $file = '/var/tmp/db_integration_test_' . uniqid(); 50 | 51 | $res = $this->connection 52 | ->table($this->tableName) 53 | ->where('name', '=', 'joe') 54 | ->intoOutfile($file) 55 | ->query(); 56 | 57 | $this->assertEquals(0, $res->rowCount()); 58 | 59 | @unlink($file); 60 | } 61 | 62 | public function testOutfileWithTerminators() 63 | { 64 | $file = '/var/tmp/db_integration_test_' . uniqid(); 65 | 66 | $res = $this->connection 67 | ->table($this->tableName) 68 | ->where('name', '=', 'joe') 69 | ->intoOutfile($file, function(\Database\Query\OutfileClause $outfile){ 70 | $outfile 71 | ->linesTerminatedBy("\n") 72 | ->fieldsTerminatedBy("\t"); 73 | }) 74 | ->query(); 75 | 76 | $this->assertEquals(0, $res->rowCount()); 77 | 78 | @unlink($file); 79 | } 80 | } -------------------------------------------------------------------------------- /src/Query/OutfileClause.php: -------------------------------------------------------------------------------- 1 | getPathname(); 55 | } 56 | 57 | $this->file = $file; 58 | 59 | $this->type = $type; 60 | } 61 | 62 | /** 63 | * @param $characterSet 64 | * @return $this 65 | */ 66 | public function characterSet($characterSet) 67 | { 68 | $this->characterSet = $characterSet; 69 | 70 | return $this; 71 | } 72 | 73 | /** 74 | * @param $character 75 | * @return $this 76 | */ 77 | public function escapedBy($character) 78 | { 79 | $this->escapedBy = $character; 80 | 81 | return $this; 82 | } 83 | 84 | /** 85 | * @param $character 86 | * @param bool $optionally 87 | * @return $this 88 | */ 89 | public function enclosedBy($character, $optionally = false) 90 | { 91 | $this->optionallyEnclosedBy = $optionally; 92 | 93 | $this->enclosedBy = $character; 94 | 95 | return $this; 96 | } 97 | 98 | /** 99 | * @param $character 100 | * @return $this 101 | */ 102 | public function fieldsTerminatedBy($character) 103 | { 104 | $this->fieldsTerminatedBy = $character; 105 | 106 | return $this; 107 | } 108 | 109 | /** 110 | * @param $character 111 | * @return $this 112 | */ 113 | public function linesTerminatedBy($character) 114 | { 115 | $this->linesTerminatedBy = $character; 116 | 117 | return $this; 118 | } 119 | 120 | } 121 | -------------------------------------------------------------------------------- /src/Exception/ExceptionHandler.php: -------------------------------------------------------------------------------- 1 | parameters = $parameters; 18 | } 19 | 20 | /** 21 | * @param array $maxQueryLength 22 | */ 23 | public function setMaxQueryLength($maxQueryLength) 24 | { 25 | $this->maxQueryLength = $maxQueryLength; 26 | } 27 | 28 | /** 29 | * @param $query 30 | * @param array $bindings 31 | * @param \Exception $previousException 32 | */ 33 | public function handle($query, array $bindings, \Exception $previousException) 34 | { 35 | $parameters = $this->parameters; 36 | 37 | if($query){ 38 | $sql = $this->replaceArray('\?', $bindings, $query); 39 | 40 | if($this->maxQueryLength && strlen($sql) > $this->maxQueryLength){ 41 | $sql = substr($sql, 0, $this->maxQueryLength); 42 | } 43 | 44 | $parameters['SQL'] = $sql; 45 | } 46 | 47 | $message = $previousException->getMessage() . PHP_EOL . $this->formatArrayParameters($parameters); 48 | 49 | throw new QueryException($message, $previousException); 50 | } 51 | 52 | /** 53 | * @param array $parameters 54 | * @return string 55 | */ 56 | private function formatArrayParameters(array $parameters) 57 | { 58 | $parameters = $this->flattenArray($parameters); 59 | 60 | foreach($parameters as $name => $value) 61 | { 62 | $parameters[$name] = $name . ': ' . $value; 63 | } 64 | 65 | return implode(PHP_EOL, $parameters); 66 | } 67 | 68 | /** 69 | * @param array $array 70 | * @param string $prepend 71 | * @return array 72 | */ 73 | private function flattenArray(array $array, $prepend = '') 74 | { 75 | $results = array(); 76 | 77 | foreach ($array as $key => $value) { 78 | if (is_array($value)) { 79 | $results = array_merge($results, $this->flattenArray($value, $prepend . $key . '.')); 80 | } else { 81 | $results[$prepend . $key] = $value; 82 | } 83 | } 84 | 85 | return $results; 86 | } 87 | 88 | /** 89 | * @param $search 90 | * @param array $replace 91 | * @param $subject 92 | * @return mixed 93 | */ 94 | private function replaceArray($search, array $replace, $subject) 95 | { 96 | foreach ($replace as $value) { 97 | $subject = preg_replace('/' . $search . '/', $value, $subject, 1); 98 | } 99 | 100 | return $subject; 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/Connectors/PostgresConnector.php: -------------------------------------------------------------------------------- 1 | PDO::CASE_NATURAL, 15 | PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, 16 | PDO::ATTR_ORACLE_NULLS => PDO::NULL_NATURAL, 17 | PDO::ATTR_STRINGIFY_FETCHES => false, 18 | ); 19 | 20 | /** 21 | * Establish a database connection. 22 | * 23 | * @param array $config 24 | * @return \PDO 25 | */ 26 | public function connect(array $config) 27 | { 28 | // First we'll create the basic DSN and connection instance connecting to the 29 | // using the configuration option specified by the developer. We will also 30 | // set the default character set on the connections to UTF-8 by default. 31 | $dsn = $this->getDsn($config); 32 | 33 | $options = $this->getOptions($config); 34 | 35 | $connection = $this->createConnection($dsn, $config, $options); 36 | 37 | $charset = $config['charset']; 38 | 39 | $connection->prepare("set names '$charset'")->execute(); 40 | 41 | // Unlike MySQL, Postgres allows the concept of "schema" and a default schema 42 | // may have been specified on the connections. If that is the case we will 43 | // set the default schema search paths to the specified database schema. 44 | if (isset($config['schema'])) { 45 | $schema = $config['schema']; 46 | 47 | $connection->prepare("set search_path to {$schema}")->execute(); 48 | } 49 | 50 | return $connection; 51 | } 52 | 53 | /** 54 | * Create a DSN string from a configuration. 55 | * 56 | * @param array $config 57 | * @return string 58 | */ 59 | protected function getDsn(array $config) 60 | { 61 | // First we will create the basic DSN setup as well as the port if it is in 62 | // in the configuration options. This will give us the basic DSN we will 63 | // need to establish the PDO connections and return them back for use. 64 | 65 | $host = isset($config['host']) ? "host={$config['host']}" : ''; 66 | 67 | $dsn = "pgsql:$host"; 68 | 69 | if (isset($config['database'])) { 70 | $dsn .= ";dbname={$config['database']}"; 71 | } 72 | 73 | // If a port was specified, we will add it to this Postgres DSN connections 74 | // format. Once we have done that we are ready to return this connection 75 | // string back out for usage, as this has been fully constructed here. 76 | if (isset($config['port'])) { 77 | $dsn .= ";port={$config['port']}"; 78 | } 79 | 80 | if (isset($config['sslmode'])) { 81 | $dsn .= ";sslmode={$config['sslmode']}"; 82 | } 83 | 84 | return $dsn; 85 | } 86 | 87 | } 88 | -------------------------------------------------------------------------------- /src/Query/JoinClause.php: -------------------------------------------------------------------------------- 1 | type = $type; 43 | $this->table = $table; 44 | } 45 | 46 | /** 47 | * Add an "on" clause to the join. 48 | * 49 | * @param string $first 50 | * @param string $operator 51 | * @param string $second 52 | * @param string $boolean 53 | * @param bool $where 54 | * @return $this 55 | */ 56 | public function on($first, $operator, $second, $boolean = 'and', $where = false) 57 | { 58 | $this->clauses[] = compact('first', 'operator', 'second', 'boolean', 'where'); 59 | 60 | if ($where) $this->bindings[] = $second; 61 | 62 | return $this; 63 | } 64 | 65 | /** 66 | * Add an "or on" clause to the join. 67 | * 68 | * @param string $first 69 | * @param string $operator 70 | * @param string $second 71 | * @return \Database\Query\JoinClause 72 | */ 73 | public function orOn($first, $operator, $second) 74 | { 75 | return $this->on($first, $operator, $second, 'or'); 76 | } 77 | 78 | /** 79 | * Add an "on where" clause to the join. 80 | * 81 | * @param string $first 82 | * @param string $operator 83 | * @param string $second 84 | * @param string $boolean 85 | * @return \Database\Query\JoinClause 86 | */ 87 | public function where($first, $operator, $second, $boolean = 'and') 88 | { 89 | return $this->on($first, $operator, $second, $boolean, true); 90 | } 91 | 92 | /** 93 | * Add an "or on where" clause to the join. 94 | * 95 | * @param string $first 96 | * @param string $operator 97 | * @param string $second 98 | * @return \Database\Query\JoinClause 99 | */ 100 | public function orWhere($first, $operator, $second) 101 | { 102 | return $this->on($first, $operator, $second, 'or', true); 103 | } 104 | 105 | /** 106 | * Add an "on where is null" clause to the join 107 | * 108 | * @param $column 109 | * @param string $boolean 110 | * @return \Database\Query\JoinClause 111 | */ 112 | public function whereNull($column, $boolean = 'and') 113 | { 114 | return $this->on($column, 'is', new Expression('null'), $boolean, false); 115 | } 116 | 117 | } 118 | -------------------------------------------------------------------------------- /src/ConnectionInterface.php: -------------------------------------------------------------------------------- 1 | getDsn($config); 15 | 16 | $options = $this->getOptions($config); 17 | 18 | // We need to grab the PDO options that should be used while making the brand 19 | // new connection instance. The PDO options control various aspects of the 20 | // connection's behavior, and some might be specified by the developers. 21 | $connection = $this->createConnection($dsn, $config, $options); 22 | 23 | $collation = $config['collation']; 24 | 25 | // Next we will set the "names" and "collation" on the clients connections so 26 | // a correct character set will be used by this client. The collation also 27 | // is set on the server but needs to be set here on this client objects. 28 | $charset = $config['charset']; 29 | 30 | $names = "set names '$charset'" . 31 | (!is_null($collation) ? " collate '$collation'" : ''); 32 | 33 | $connection->prepare($names)->execute(); 34 | 35 | // If the "strict" option has been configured for the connection we'll enable 36 | // strict mode on all of these tables. This enforces some extra rules when 37 | // using the MySQL database system and is a quicker way to enforce them. 38 | if (isset($config['strict']) && $config['strict']) { 39 | $connection->prepare("set session sql_mode='STRICT_ALL_TABLES'")->execute(); 40 | } 41 | 42 | return $connection; 43 | } 44 | 45 | /** 46 | * Create a DSN string from a configuration. Chooses socket or host/port based on 47 | * the 'unix_socket' config value 48 | * 49 | * @param array $config 50 | * @return string 51 | */ 52 | protected function getDsn(array $config) 53 | { 54 | $dsn = $this->configHasSocket($config) ? $this->getSocketDsn($config) : $this->getHostDsn($config); 55 | 56 | isset($config['database']) and $dsn .= ";dbname={$config['database']}"; 57 | 58 | return $dsn; 59 | } 60 | 61 | /** 62 | * Determine if the given configuration array has a UNIX socket value. 63 | * 64 | * @param array $config 65 | * @return bool 66 | */ 67 | protected function configHasSocket(array $config) 68 | { 69 | return isset($config['unix_socket']) && !empty($config['unix_socket']); 70 | } 71 | 72 | /** 73 | * Get the DSN string for a socket configuration. 74 | * 75 | * @param array $config 76 | * @return string 77 | */ 78 | protected function getSocketDsn(array $config) 79 | { 80 | extract($config); 81 | 82 | $dsn = "mysql:unix_socket={$config['unix_socket']}"; 83 | 84 | return $dsn; 85 | } 86 | 87 | /** 88 | * Get the DSN string for a host / port configuration. 89 | * 90 | * @param array $config 91 | * @return string 92 | */ 93 | protected function getHostDsn(array $config) 94 | { 95 | $dsn = "mysql:host={$config['host']}"; 96 | 97 | isset($config['port']) and $dsn .= ";port={$config['port']}"; 98 | 99 | return $dsn; 100 | } 101 | 102 | } 103 | -------------------------------------------------------------------------------- /src/Query/InsertBuffer.php: -------------------------------------------------------------------------------- 1 | builder = $builder; 27 | $this->chunkSize = $chunkSize; 28 | } 29 | 30 | /** 31 | * Insert a new record into the database. 32 | * 33 | * @param Traversable $values 34 | * @return int 35 | */ 36 | public function insert(Traversable $values) 37 | { 38 | return $this->doInsert($values, 'insert'); 39 | } 40 | 41 | /** 42 | * Insert a new record into the database. 43 | * 44 | * @param Traversable $values 45 | * @return int 46 | */ 47 | public function insertIgnore(Traversable $values) 48 | { 49 | return $this->doInsert($values, 'insertIgnore'); 50 | } 51 | 52 | /** 53 | * Insert a new record into the database. 54 | * 55 | * @param Traversable $values 56 | * @return int 57 | */ 58 | public function replace(Traversable $values) 59 | { 60 | return $this->doInsert($values, 'replace'); 61 | } 62 | 63 | /** 64 | * Insert a new record into the database. 65 | * 66 | * @param Traversable $values 67 | * @param $type 68 | * @return int 69 | */ 70 | protected function doInsert(Traversable $values, $type) 71 | { 72 | $inserts = 0; 73 | 74 | $this->buffer($values, function(array $buffer) use($type, &$inserts){ 75 | $inserts += $this->builder->doInsert($buffer, $type)->rowCount(); 76 | }); 77 | 78 | return $inserts; 79 | } 80 | 81 | /** 82 | * Insert a new record into the database, with an update if it exists 83 | * 84 | * @param Traversable $values 85 | * @param array $updateValues an array of column => bindings pairs to update 86 | * @return int 87 | */ 88 | public function insertUpdate(Traversable $values, array $updateValues) 89 | { 90 | $upserts = 0; 91 | 92 | $this->buffer($values, function(array $buffer) use($updateValues, &$upserts){ 93 | $upserts += $this->builder->insertUpdate($buffer, $updateValues)->rowCount(); 94 | }); 95 | 96 | return $upserts; 97 | } 98 | 99 | /** 100 | * Alias for insertOnDuplicateKeyUpdate 101 | * 102 | * @param Traversable $values 103 | * @param array $updateValues 104 | * @return int 105 | */ 106 | public function insertOnDuplicateKeyUpdate(Traversable $values, array $updateValues) 107 | { 108 | return $this->insertUpdate($values, $updateValues); 109 | } 110 | 111 | /** 112 | * Loop through a traversable collection and call a closure after every X elements have been buffered 113 | * 114 | * @param Traversable $values 115 | * @param callable $callback 116 | */ 117 | private function buffer(Traversable $values, Closure $callback) 118 | { 119 | // Keeping count the number of items is an order of magnitude quicker than calling count($buffer) 120 | $size = 0; 121 | $buffer = array(); 122 | 123 | foreach($values as $row) 124 | { 125 | $buffer[] = $row; 126 | 127 | if(++$size >= $this->chunkSize) 128 | { 129 | $callback($buffer); 130 | 131 | $buffer = array(); 132 | $size = 0; 133 | } 134 | } 135 | 136 | // Insert the remainder 137 | if($size) 138 | { 139 | $callback($buffer); 140 | } 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/ConnectionResolver.php: -------------------------------------------------------------------------------- 1 | connectionFactory = $connectionFactory ?: new ConnectionFactory(); 44 | 45 | foreach ($connections as $name => $connection) { 46 | $this->addConnection($name, $connection); 47 | } 48 | } 49 | 50 | /** 51 | * Get a database connection instance. 52 | * 53 | * @param string $name 54 | * @return \Database\Connection 55 | */ 56 | public function connection($name = null) 57 | { 58 | if (is_null($name)) $name = $this->getDefaultConnection(); 59 | 60 | if (!isset($this->connectionCache[$name])) 61 | { 62 | $this->connectionCache[$name] = $this->newConnection($name); 63 | } 64 | 65 | return $this->connectionCache[$name]; 66 | } 67 | 68 | /** 69 | * Get a new database connection, without the 70 | * 71 | * @param $name 72 | * @return mixed 73 | */ 74 | public function newConnection($name = null) 75 | { 76 | if (is_null($name)) $name = $this->getDefaultConnection(); 77 | 78 | return $this->connectionFactory->make($this->connectionConfig($name)); 79 | } 80 | 81 | /** 82 | * Get a database connection instance. 83 | * 84 | * @param string $name 85 | * @return array 86 | */ 87 | public function connectionConfig($name = null) 88 | { 89 | if (is_null($name)) $name = $this->getDefaultConnection(); 90 | 91 | return $this->value($this->connections[$name]); 92 | } 93 | 94 | /** 95 | * Add a connection to the resolver. 96 | * 97 | * Can be an instance of \Database\Connection or a valid config array, if a connection factory has been set 98 | * 99 | * @param string $name 100 | * @param array $connection 101 | * @return void 102 | */ 103 | public function addConnection($name, $connection) 104 | { 105 | $this->connections[$name] = $connection; 106 | } 107 | 108 | /** 109 | * Check if a connection has been registered. 110 | * 111 | * @param string $name 112 | * @return bool 113 | */ 114 | public function hasConnection($name) 115 | { 116 | return isset($this->connections[$name]); 117 | } 118 | 119 | /** 120 | * Get the default connection name. 121 | * 122 | * @return string 123 | */ 124 | public function getDefaultConnection() 125 | { 126 | return $this->default; 127 | } 128 | 129 | /** 130 | * Set the default connection name. 131 | * 132 | * @param string $name 133 | * @return void 134 | */ 135 | public function setDefaultConnection($name) 136 | { 137 | $this->default = $name; 138 | } 139 | 140 | /** 141 | * @param $value 142 | * @return mixed 143 | */ 144 | protected function value($value) 145 | { 146 | return $value instanceof \Closure ? $value() : $value; 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/Query/InfileClause.php: -------------------------------------------------------------------------------- 1 | getPathname(); 81 | } 82 | 83 | $this->file = $file; 84 | 85 | $this->columns = $columns; 86 | } 87 | 88 | /** 89 | * @param $characterSet 90 | * @return $this 91 | */ 92 | public function characterSet($characterSet) 93 | { 94 | $this->characterSet = $characterSet; 95 | 96 | return $this; 97 | } 98 | 99 | /** 100 | * @param $character 101 | * @return $this 102 | */ 103 | public function escapedBy($character) 104 | { 105 | $this->escapedBy = $character; 106 | 107 | return $this; 108 | } 109 | 110 | /** 111 | * @param $character 112 | * @param bool $optionally 113 | * @return $this 114 | */ 115 | public function enclosedBy($character, $optionally = false) 116 | { 117 | $this->optionallyEnclosedBy = $optionally; 118 | 119 | $this->enclosedBy = $character; 120 | 121 | return $this; 122 | } 123 | 124 | /** 125 | * @param $character 126 | * @return $this 127 | */ 128 | public function fieldsTerminatedBy($character) 129 | { 130 | $this->fieldsTerminatedBy = $character; 131 | 132 | return $this; 133 | } 134 | 135 | /** 136 | * @param $character 137 | * @return $this 138 | */ 139 | public function linesStartingBy($character) 140 | { 141 | $this->linesStartingBy = $character; 142 | 143 | return $this; 144 | } 145 | 146 | /** 147 | * @param $character 148 | * @return $this 149 | */ 150 | public function linesTerminatedBy($character) 151 | { 152 | $this->linesTerminatedBy = $character; 153 | 154 | return $this; 155 | } 156 | 157 | /** 158 | * @param $lineCount 159 | * @return $this 160 | */ 161 | public function ignoreLines($lineCount) 162 | { 163 | if(!is_integer($lineCount) || $lineCount < 1) 164 | { 165 | throw new \InvalidArgumentException("Line count must be a positive, non-zero integer."); 166 | } 167 | 168 | $this->ignoreLines = $lineCount; 169 | 170 | return $this; 171 | } 172 | 173 | /** 174 | * Perform a load data infile, ignoring rows with a duplicate key 175 | * 176 | * @return $this 177 | */ 178 | public function ignore() 179 | { 180 | $this->type = 'ignore'; 181 | 182 | return $this; 183 | } 184 | 185 | /** 186 | * Perform a load data infile, replacing rows with a duplicate key 187 | * 188 | * @return $this 189 | */ 190 | public function replace() 191 | { 192 | $this->type = 'replace'; 193 | 194 | return $this; 195 | } 196 | 197 | /** 198 | * Perform a load data infile, replacing rows with a duplicate key 199 | * 200 | * @return $this 201 | */ 202 | public function local() 203 | { 204 | $this->local = true; 205 | 206 | return $this; 207 | } 208 | 209 | /** 210 | * @param array $rules 211 | * @return $this 212 | */ 213 | public function rules(array $rules) 214 | { 215 | $this->rules = $rules; 216 | 217 | return $this; 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /src/Query/Grammars/SQLiteGrammar.php: -------------------------------------------------------------------------------- 1 | ', '<=', '>=', '<>', '!=', 15 | 'like', 'not like', 'between', 'ilike', 16 | '&', '|', '<<', '>>', 17 | ); 18 | 19 | /** 20 | * Compile an insert statement into SQL. 21 | * 22 | * @param \Database\Query\Builder $query 23 | * @param array $values 24 | * @return string 25 | */ 26 | public function doCompileInsert(Builder $query, array $values, $type) 27 | { 28 | // Essentially we will force every insert to be treated as a batch insert which 29 | // simply makes creating the SQL easier for us since we can utilize the same 30 | // basic routine regardless of an amount of records given to us to insert. 31 | $table = $this->wrapTable($query->from); 32 | 33 | if (!is_array(reset($values))) { 34 | $values = array($values); 35 | } 36 | 37 | // If there is only one record being inserted, we will just use the usual query 38 | // grammar insert builder because no special syntax is needed for the single 39 | // row inserts in SQLite. However, if there are multiples, we'll continue. 40 | if (count($values) == 1) { 41 | return parent::doCompileInsert($query, reset($values), $type); 42 | } 43 | 44 | $names = $this->columnize(array_keys(reset($values))); 45 | 46 | $columns = array(); 47 | 48 | // SQLite requires us to build the multi-row insert as a listing of select with 49 | // unions joining them together. So we'll build out this list of columns and 50 | // then join them all together with select unions to complete the queries. 51 | foreach (array_keys(reset($values)) as $column) { 52 | $columns[] = '? as ' . $this->wrap($column); 53 | } 54 | 55 | $columns = array_fill(0, count($values), implode(', ', $columns)); 56 | 57 | return "$type into $table ($names) select " . implode(' union select ', $columns); 58 | } 59 | 60 | /** 61 | * Compile an insert statement into SQL. 62 | * 63 | * @param \Database\Query\Builder $query 64 | * @param array $values 65 | * @return string 66 | */ 67 | public function compileInsertIgnore(Builder $query, array $values) 68 | { 69 | return $this->doCompileInsert($query, $values, 'insert or ignore'); 70 | } 71 | 72 | /** 73 | * Compile a replace statement into SQL. 74 | * 75 | * @param \Database\Query\Builder $query 76 | * @param array $values 77 | * @return string 78 | */ 79 | public function compileReplace(Builder $query, array $values) 80 | { 81 | return $this->doCompileInsert($query, $values, 'insert or replace'); 82 | } 83 | 84 | /** 85 | * Compile a truncate table statement into SQL. 86 | * 87 | * @param \Database\Query\Builder $query 88 | * @return array 89 | */ 90 | public function compileTruncate(Builder $query) 91 | { 92 | $sql = array('delete from sqlite_sequence where name = ?' => array($query->from)); 93 | 94 | $sql['delete from ' . $this->wrapTable($query->from)] = array(); 95 | 96 | return $sql; 97 | } 98 | 99 | /** 100 | * Compile a "where day" clause. 101 | * 102 | * @param \Database\Query\Builder $query 103 | * @param array $where 104 | * @return string 105 | */ 106 | protected function whereDay(Builder $query, $where) 107 | { 108 | return $this->dateBasedWhere('%d', $query, $where); 109 | } 110 | 111 | /** 112 | * Compile a "where month" clause. 113 | * 114 | * @param \Database\Query\Builder $query 115 | * @param array $where 116 | * @return string 117 | */ 118 | protected function whereMonth(Builder $query, $where) 119 | { 120 | return $this->dateBasedWhere('%m', $query, $where); 121 | } 122 | 123 | /** 124 | * Compile a "where year" clause. 125 | * 126 | * @param \Database\Query\Builder $query 127 | * @param array $where 128 | * @return string 129 | */ 130 | protected function whereYear(Builder $query, $where) 131 | { 132 | return $this->dateBasedWhere('%Y', $query, $where); 133 | } 134 | 135 | /** 136 | * Compile a date based where clause. 137 | * 138 | * @param string $type 139 | * @param \Database\Query\Builder $query 140 | * @param array $where 141 | * @return string 142 | */ 143 | protected function dateBasedWhere($type, Builder $query, $where) 144 | { 145 | $value = str_pad($where['value'], 2, '0', STR_PAD_LEFT); 146 | 147 | $value = $this->parameter($value); 148 | 149 | return 'strftime(\'' . $type . '\', ' . $this->wrap($where['column']) . ') ' . $where['operator'] . ' ' . $value; 150 | } 151 | 152 | } 153 | -------------------------------------------------------------------------------- /src/Query/Grammars/PostgresGrammar.php: -------------------------------------------------------------------------------- 1 | ', '<=', '>=', '<>', '!=', 15 | 'like', 'not like', 'between', 'ilike', 16 | '&', '|', '#', '<<', '>>', 17 | ); 18 | 19 | /** 20 | * Compile the lock into SQL. 21 | * 22 | * @param \Database\Query\Builder $query 23 | * @param bool|string $value 24 | * @return string 25 | */ 26 | protected function compileLock(Builder $query, $value) 27 | { 28 | if (is_string($value)) return $value; 29 | 30 | return $value ? 'for update' : 'for share'; 31 | } 32 | 33 | /** 34 | * Compile an update statement into SQL. 35 | * 36 | * @param \Database\Query\Builder $query 37 | * @param array $values 38 | * @return string 39 | */ 40 | public function compileUpdate(Builder $query, $values) 41 | { 42 | $table = $this->wrapTable($query->from); 43 | 44 | // Each one of the columns in the update statements needs to be wrapped in the 45 | // keyword identifiers, also a place-holder needs to be created for each of 46 | // the values in the list of bindings so we can make the sets statements. 47 | $columns = $this->compileUpdateColumns($values); 48 | 49 | $from = $this->compileUpdateFrom($query); 50 | 51 | $where = $this->compileUpdateWheres($query); 52 | 53 | return trim("update {$table} set {$columns}{$from} $where"); 54 | } 55 | 56 | /** 57 | * Compile the columns for the update statement. 58 | * 59 | * @param array $values 60 | * @return string 61 | */ 62 | protected function compileUpdateColumns($values) 63 | { 64 | $columns = array(); 65 | 66 | // When gathering the columns for an update statement, we'll wrap each of the 67 | // columns and convert it to a parameter value. Then we will concatenate a 68 | // list of the columns that can be added into this update query clauses. 69 | foreach ($values as $key => $value) { 70 | $columns[] = $this->wrap($key) . ' = ' . $this->parameter($value); 71 | } 72 | 73 | return implode(', ', $columns); 74 | } 75 | 76 | /** 77 | * Compile the "from" clause for an update with a join. 78 | * 79 | * @param \Database\Query\Builder $query 80 | * @return string 81 | */ 82 | protected function compileUpdateFrom(Builder $query) 83 | { 84 | if (!isset($query->joins)) return ''; 85 | 86 | $froms = array(); 87 | 88 | // When using Postgres, updates with joins list the joined tables in the from 89 | // clause, which is different than other systems like MySQL. Here, we will 90 | // compile out the tables that are joined and add them to a from clause. 91 | foreach ($query->joins as $join) { 92 | $froms[] = $this->wrapTable($join->table); 93 | } 94 | 95 | if (count($froms) > 0) return ' from ' . implode(', ', $froms); 96 | } 97 | 98 | /** 99 | * Compile the additional where clauses for updates with joins. 100 | * 101 | * @param \Database\Query\Builder $query 102 | * @return string 103 | */ 104 | protected function compileUpdateWheres(Builder $query) 105 | { 106 | $baseWhere = $this->compileWheres($query); 107 | 108 | if (!isset($query->joins)) return $baseWhere; 109 | 110 | // Once we compile the join constraints, we will either use them as the where 111 | // clause or append them to the existing base where clauses. If we need to 112 | // strip the leading boolean we will do so when using as the only where. 113 | $joinWhere = $this->compileUpdateJoinWheres($query); 114 | 115 | if (trim($baseWhere) == '') { 116 | return 'where ' . $this->removeLeadingBoolean($joinWhere); 117 | } 118 | 119 | return $baseWhere . ' ' . $joinWhere; 120 | } 121 | 122 | /** 123 | * Compile the "join" clauses for an update. 124 | * 125 | * @param \Database\Query\Builder $query 126 | * @return string 127 | */ 128 | protected function compileUpdateJoinWheres(Builder $query) 129 | { 130 | $joinWheres = array(); 131 | 132 | // Here we will just loop through all of the join constraints and compile them 133 | // all out then implode them. This should give us "where" like syntax after 134 | // everything has been built and then we will join it to the real wheres. 135 | foreach ($query->joins as $join) { 136 | foreach ($join->clauses as $clause) { 137 | $joinWheres[] = $this->compileJoinConstraint($clause); 138 | } 139 | } 140 | 141 | return implode(' ', $joinWheres); 142 | } 143 | 144 | /** 145 | * Compile an insert and get ID statement into SQL. 146 | * 147 | * @param \Database\Query\Builder $query 148 | * @param array $values 149 | * @param string $sequence 150 | * @return string 151 | */ 152 | public function compileInsertGetId(Builder $query, $values, $sequence) 153 | { 154 | if (is_null($sequence)) $sequence = 'id'; 155 | 156 | return $this->compileInsert($query, $values) . ' returning ' . $this->wrap($sequence); 157 | } 158 | 159 | /** 160 | * Compile a truncate table statement into SQL. 161 | * 162 | * @param \Database\Query\Builder $query 163 | * @return array 164 | */ 165 | public function compileTruncate(Builder $query) 166 | { 167 | return array('truncate ' . $this->wrapTable($query->from) . ' restart identity' => array()); 168 | } 169 | 170 | } 171 | -------------------------------------------------------------------------------- /tests/unit/Database/DatabaseConnectionFactoryTest.php: -------------------------------------------------------------------------------- 1 | getMockBuilder(\Database\Connectors\ConnectionFactory::class) 20 | ->setMethods(array('createConnector', 'createConnection', 'createQueryGrammar', 'createExceptionHandler')) 21 | ->getMock(); 22 | 23 | $config = array('driver' => 'mysql', 'prefix' => 'prefix', 'database' => 'database'); 24 | 25 | $pdo = new DatabaseConnectionFactoryPDOStub; 26 | 27 | $connector = m::mock('stdClass'); 28 | $connector->shouldReceive('connect')->once()->with($config)->andReturn($pdo); 29 | 30 | $mockGrammar = $this->getMockBuilder(\Database\Query\Grammars\MysqlGrammar::class)->getMock(); 31 | $mockExceptionHandler = $this->getMockBuilder(\Database\Exception\ExceptionHandlerInterface::class)->getMock(); 32 | 33 | $mockConnection = $this->getMockConnectionWithExpectations($pdo, $mockGrammar); 34 | 35 | $factory->expects($this->once())->method('createConnector')->with($config['driver'])->will($this->returnValue($connector)); 36 | $factory->expects($this->once())->method('createQueryGrammar')->with('mysql')->will($this->returnValue($mockGrammar)); 37 | $factory->expects($this->once())->method('createConnection')->will($this->returnValue($mockConnection)); 38 | $factory->expects($this->once())->method('createExceptionHandler')->with($config)->will($this->returnValue($mockExceptionHandler)); 39 | 40 | $connection = $factory->make($config); 41 | 42 | $this->assertSame($mockConnection, $connection); 43 | } 44 | 45 | 46 | public function testMakeCallsCreateConnectionForReadWrite() 47 | { 48 | $factory = $this->getMockBuilder(\Database\Connectors\ConnectionFactory::class) 49 | ->setMethods(array('createConnector', 'createConnection', 'createQueryGrammar')) 50 | ->getMock(); 51 | 52 | $connector = m::mock('stdClass'); 53 | $config = array( 54 | 'read' => array('database' => 'database'), 55 | 'write' => array('database' => 'database'), 56 | 'driver' => 'mysql', 'prefix' => 'prefix', 'name' => 'foo' 57 | ); 58 | $expect = $config; 59 | unset($expect['read']); 60 | unset($expect['write']); 61 | $expect['database'] = 'database'; 62 | $pdo = new DatabaseConnectionFactoryPDOStub; 63 | $connector->shouldReceive('connect')->twice()->with($expect)->andReturn($pdo); 64 | 65 | $mockGrammar = $this->getMockBuilder(\Database\Query\Grammars\MysqlGrammar::class)->getMock(); 66 | 67 | $mockConnection = $this->getMockConnectionWithExpectations($pdo, $mockGrammar); 68 | 69 | $factory->expects($this->exactly(2))->method('createConnector')->with($expect['driver'])->will($this->returnValue($connector)); 70 | $factory->expects($this->once())->method('createQueryGrammar')->with('mysql')->will($this->returnValue($mockGrammar)); 71 | $factory->expects($this->once())->method('createConnection')->will($this->returnValue($mockConnection)); 72 | 73 | $connection = $factory->make($config, 'foo'); 74 | 75 | $this->assertSame($mockConnection, $connection); 76 | } 77 | 78 | private function getMockConnectionWithExpectations($pdo, $grammar) 79 | { 80 | $mockConnection = $this->getMockBuilder(\Database\Connection::class) 81 | ->setMethods(array('setPdo','setReconnector', 'setQueryGrammar', 'setExceptionHandler')) 82 | ->setConstructorArgs(array($pdo)) 83 | ->getMock(); 84 | 85 | $mockConnection->expects($this->once())->method('setReconnector')->will($this->returnSelf()); 86 | $mockConnection->expects($this->once())->method('setQueryGrammar')->with($grammar)->will($this->returnSelf()); 87 | $mockConnection->expects($this->once())->method('setExceptionHandler')->will($this->returnSelf()); 88 | 89 | $mockConnection->expects($this->once())->method('setPdo')->with($pdo)->will($this->returnValue($mockConnection)); 90 | 91 | return $mockConnection; 92 | } 93 | 94 | public function testProperInstancesAreReturnedForProperDrivers() 95 | { 96 | $factory = new Database\Connectors\ConnectionFactory(); 97 | $this->assertInstanceOf('Database\Connectors\MySqlConnector', $factory->createConnector('mysql')); 98 | $this->assertInstanceOf('Database\Connectors\PostgresConnector', $factory->createConnector('pgsql')); 99 | $this->assertInstanceOf('Database\Connectors\SQLiteConnector', $factory->createConnector('sqlite')); 100 | $this->assertInstanceOf('Database\Connectors\SqlServerConnector', $factory->createConnector('sqlsrv')); 101 | } 102 | 103 | /** 104 | * @dataProvider driversGrammarProvider 105 | */ 106 | public function testProperGrammarInstancesAreReturnedForProperDrivers($driver, $instance) 107 | { 108 | $factory = $this->getMockBuilder(\Database\Connectors\ConnectionFactory::class) 109 | ->setMethods(array('createConnector')) 110 | ->getMock(); 111 | 112 | if(is_null($instance)) 113 | { 114 | $this->expectException(InvalidArgumentException::class); 115 | } 116 | else 117 | { 118 | $mock = m::mock('stdClass'); 119 | $mock->shouldReceive('connect')->andReturn(m::mock('PDO')); 120 | 121 | $factory->expects($this->once())->method('createConnector')->willReturn($mock); 122 | } 123 | 124 | $connection = $factory->make(array( 125 | 'driver' => $driver 126 | )); 127 | 128 | if(!is_null($instance)) 129 | { 130 | $this->assertInstanceOf($instance, $connection->getQueryGrammar()); 131 | } 132 | } 133 | 134 | public function driversGrammarProvider() 135 | { 136 | return array( 137 | array('mysql', 'Database\Query\Grammars\MySqlGrammar'), 138 | array('pgsql', 'Database\Query\Grammars\PostgresGrammar'), 139 | array('sqlite', 'Database\Query\Grammars\SQLiteGrammar'), 140 | array('sqlsrv', 'Database\Query\Grammars\SqlServerGrammar'), 141 | //array('blahblah', null) 142 | ); 143 | } 144 | 145 | public function testIfDriverIsntSetExceptionIsThrown() 146 | { 147 | $this->expectException(InvalidArgumentException::class); 148 | 149 | $factory = new Database\Connectors\ConnectionFactory(); 150 | $factory->make(array('foo')); 151 | } 152 | 153 | public function testExceptionIsThrownOnUnsupportedDriver() 154 | { 155 | $this->expectException(InvalidArgumentException::class); 156 | 157 | $factory = new Database\Connectors\ConnectionFactory(); 158 | $factory->make(array('driver' => 'foo')); 159 | } 160 | 161 | } 162 | -------------------------------------------------------------------------------- /src/Query/Grammars/SqlServerGrammar.php: -------------------------------------------------------------------------------- 1 | ', '<=', '>=', '!<', '!>', '<>', '!=', 15 | 'like', 'not like', 'between', 'ilike', 16 | '&', '&=', '|', '|=', '^', '^=', 17 | ); 18 | 19 | /** 20 | * Compile a select query into SQL. 21 | * 22 | * @param \Database\Query\Builder 23 | * @return string 24 | */ 25 | public function compileSelect(Builder $query) 26 | { 27 | $components = $this->compileComponents($query); 28 | 29 | // If an offset is present on the query, we will need to wrap the query in 30 | // a big "ANSI" offset syntax block. This is very nasty compared to the 31 | // other database systems but is necessary for implementing features. 32 | if ($query->offset > 0) { 33 | return $this->compileAnsiOffset($query, $components); 34 | } 35 | 36 | return $this->concatenate($components); 37 | } 38 | 39 | /** 40 | * Compile the "select *" portion of the query. 41 | * 42 | * @param \Database\Query\Builder $query 43 | * @param array $columns 44 | * @return string 45 | */ 46 | protected function compileColumns(Builder $query, $columns) 47 | { 48 | if (!is_null($query->aggregate)) return; 49 | 50 | $select = $query->distinct ? 'select distinct ' : 'select '; 51 | 52 | // If there is a limit on the query, but not an offset, we will add the top 53 | // clause to the query, which serves as a "limit" type clause within the 54 | // SQL Server system similar to the limit keywords available in MySQL. 55 | if ($query->limit > 0 && $query->offset <= 0) { 56 | $select .= 'top ' . $query->limit . ' '; 57 | } 58 | 59 | return $select . $this->columnize($columns); 60 | } 61 | 62 | /** 63 | * Compile the "from" portion of the query. 64 | * 65 | * @param \Database\Query\Builder $query 66 | * @param string $table 67 | * @return string 68 | */ 69 | protected function compileFrom(Builder $query, $table) 70 | { 71 | $from = parent::compileFrom($query, $table); 72 | 73 | if (is_string($query->lock)) return $from . ' ' . $query->lock; 74 | 75 | if (!is_null($query->lock)) { 76 | return $from . ' with(rowlock,' . ($query->lock ? 'updlock,' : '') . 'holdlock)'; 77 | } 78 | 79 | return $from; 80 | } 81 | 82 | /** 83 | * Create a full ANSI offset clause for the query. 84 | * 85 | * @param \Database\Query\Builder $query 86 | * @param array $components 87 | * @return string 88 | */ 89 | protected function compileAnsiOffset(Builder $query, $components) 90 | { 91 | // An ORDER BY clause is required to make this offset query work, so if one does 92 | // not exist we'll just create a dummy clause to trick the database and so it 93 | // does not complain about the queries for not having an "order by" clause. 94 | if (!isset($components['orders'])) { 95 | $components['orders'] = 'order by (select 0)'; 96 | } 97 | 98 | // We need to add the row number to the query so we can compare it to the offset 99 | // and limit values given for the statements. So we will add an expression to 100 | // the "select" that will give back the row numbers on each of the records. 101 | $orderings = $components['orders']; 102 | 103 | $components['columns'] .= $this->compileOver($orderings); 104 | 105 | unset($components['orders']); 106 | 107 | // Next we need to calculate the constraints that should be placed on the query 108 | // to get the right offset and limit from our query but if there is no limit 109 | // set we will just handle the offset only since that is all that matters. 110 | $constraint = $this->compileRowConstraint($query); 111 | 112 | $sql = $this->concatenate($components); 113 | 114 | // We are now ready to build the final SQL query so we'll create a common table 115 | // expression from the query and get the records with row numbers within our 116 | // given limit and offset value that we just put on as a query constraint. 117 | return $this->compileTableExpression($sql, $constraint); 118 | } 119 | 120 | /** 121 | * Compile the over statement for a table expression. 122 | * 123 | * @param string $orderings 124 | * @return string 125 | */ 126 | protected function compileOver($orderings) 127 | { 128 | return ", row_number() over ({$orderings}) as row_num"; 129 | } 130 | 131 | /** 132 | * Compile the limit / offset row constraint for a query. 133 | * 134 | * @param \Database\Query\Builder $query 135 | * @return string 136 | */ 137 | protected function compileRowConstraint($query) 138 | { 139 | $start = $query->offset + 1; 140 | 141 | if ($query->limit > 0) { 142 | $finish = $query->offset + $query->limit; 143 | 144 | return "between {$start} and {$finish}"; 145 | } 146 | 147 | return ">= {$start}"; 148 | } 149 | 150 | /** 151 | * Compile a common table expression for a query. 152 | * 153 | * @param string $sql 154 | * @param string $constraint 155 | * @return string 156 | */ 157 | protected function compileTableExpression($sql, $constraint) 158 | { 159 | return "select * from ({$sql}) as temp_table where row_num {$constraint}"; 160 | } 161 | 162 | /** 163 | * Compile the "limit" portions of the query. 164 | * 165 | * @param \Database\Query\Builder $query 166 | * @param int $limit 167 | * @return string 168 | */ 169 | protected function compileLimit(Builder $query, $limit) 170 | { 171 | return ''; 172 | } 173 | 174 | /** 175 | * Compile the "offset" portions of the query. 176 | * 177 | * @param \Database\Query\Builder $query 178 | * @param int $offset 179 | * @return string 180 | */ 181 | protected function compileOffset(Builder $query, $offset) 182 | { 183 | return ''; 184 | } 185 | 186 | /** 187 | * Compile a truncate table statement into SQL. 188 | * 189 | * @param \Database\Query\Builder $query 190 | * @return array 191 | */ 192 | public function compileTruncate(Builder $query) 193 | { 194 | return array('truncate table ' . $this->wrapTable($query->from) => array()); 195 | } 196 | 197 | /** 198 | * Get the format for database stored dates. 199 | * 200 | * @return string 201 | */ 202 | public function getDateFormat() 203 | { 204 | return 'Y-m-d H:i:s.000'; 205 | } 206 | 207 | /** 208 | * Wrap a single string in keyword identifiers. 209 | * 210 | * @param string $value 211 | * @return string 212 | */ 213 | protected function wrapValue($value) 214 | { 215 | if ($value === '*') return $value; 216 | 217 | return '[' . str_replace(']', ']]', $value) . ']'; 218 | } 219 | 220 | } 221 | -------------------------------------------------------------------------------- /tests/unit/Database/DatabaseConnectorTest.php: -------------------------------------------------------------------------------- 1 | setDefaultOptions(array(0 => 'foo', 1 => 'bar')); 17 | $this->assertEquals(array(0 => 'baz', 1 => 'bar', 2 => 'boom'), $connector->getOptions(array('options' => array(0 => 'baz', 2 => 'boom')))); 18 | } 19 | 20 | 21 | /** 22 | * @dataProvider mySqlConnectProvider 23 | */ 24 | public function testMySqlConnectCallsCreateConnectionWithProperArguments($dsn, $config) 25 | { 26 | $connector = $this->getMockBuilder(Database\Connectors\MySqlConnector::class)->setMethods(array('createConnection', 'getOptions'))->getMock(); 27 | $connection = m::mock('stdClass'); 28 | $connector->expects($this->once())->method('getOptions')->with($this->equalTo($config))->will($this->returnValue(array('options'))); 29 | $connector->expects($this->once())->method('createConnection')->with($this->equalTo($dsn), $this->equalTo($config), $this->equalTo(array('options')))->will($this->returnValue($connection)); 30 | $connection->shouldReceive('prepare')->once()->with('set names \'utf8\' collate \'utf8_unicode_ci\'')->andReturn($connection); 31 | $connection->shouldReceive('execute')->once(); 32 | $connection->shouldReceive('exec')->zeroOrMoreTimes(); 33 | $result = $connector->connect($config); 34 | 35 | $this->assertSame($result, $connection); 36 | } 37 | 38 | 39 | public function mySqlConnectProvider() 40 | { 41 | return array( 42 | array('mysql:host=foo;dbname=bar', array('host' => 'foo', 'database' => 'bar', 'collation' => 'utf8_unicode_ci', 'charset' => 'utf8')), 43 | array('mysql:host=foo;port=111;dbname=bar', array('host' => 'foo', 'database' => 'bar', 'port' => 111, 'collation' => 'utf8_unicode_ci', 'charset' => 'utf8')), 44 | array('mysql:unix_socket=baz;dbname=bar', array('host' => 'foo', 'database' => 'bar', 'port' => 111, 'unix_socket' => 'baz', 'collation' => 'utf8_unicode_ci', 'charset' => 'utf8')), 45 | ); 46 | } 47 | 48 | 49 | public function testPostgresConnectCallsCreateConnectionWithProperArguments() 50 | { 51 | $dsn = 'pgsql:host=foo;dbname=bar;port=111'; 52 | $config = array('host' => 'foo', 'database' => 'bar', 'port' => 111, 'charset' => 'utf8'); 53 | $connector = $this->getMockBuilder(Database\Connectors\PostgresConnector::class)->setMethods(array('createConnection', 'getOptions'))->getMock(); 54 | $connection = m::mock('stdClass'); 55 | $connector->expects($this->once())->method('getOptions')->with($this->equalTo($config))->will($this->returnValue(array('options'))); 56 | $connector->expects($this->once())->method('createConnection')->with($this->equalTo($dsn), $this->equalTo($config), $this->equalTo(array('options')))->will($this->returnValue($connection)); 57 | $connection->shouldReceive('prepare')->once()->with('set names \'utf8\'')->andReturn($connection); 58 | $connection->shouldReceive('execute')->once(); 59 | $result = $connector->connect($config); 60 | 61 | $this->assertSame($result, $connection); 62 | } 63 | 64 | 65 | public function testPostgresSearchPathIsSet() 66 | { 67 | $dsn = 'pgsql:host=foo;dbname=bar'; 68 | $config = array('host' => 'foo', 'database' => 'bar', 'schema' => 'public', 'charset' => 'utf8'); 69 | $connector = $this->getMockBuilder(Database\Connectors\PostgresConnector::class)->setMethods(array('createConnection', 'getOptions'))->getMock(); 70 | $connection = m::mock('stdClass'); 71 | $connector->expects($this->once())->method('getOptions')->with($this->equalTo($config))->will($this->returnValue(array('options'))); 72 | $connector->expects($this->once())->method('createConnection')->with($this->equalTo($dsn), $this->equalTo($config), $this->equalTo(array('options')))->will($this->returnValue($connection)); 73 | $connection->shouldReceive('prepare')->once()->with('set names \'utf8\'')->andReturn($connection); 74 | $connection->shouldReceive('prepare')->once()->with("set search_path to public")->andReturn($connection); 75 | $connection->shouldReceive('execute')->twice(); 76 | $result = $connector->connect($config); 77 | 78 | $this->assertSame($result, $connection); 79 | } 80 | 81 | 82 | public function testSQLiteMemoryDatabasesMayBeConnectedTo() 83 | { 84 | $dsn = 'sqlite::memory:'; 85 | $config = array('database' => ':memory:'); 86 | $connector = $this->getMockBuilder(Database\Connectors\SQLiteConnector::class)->setMethods(array('createConnection', 'getOptions'))->getMock(); 87 | $connection = m::mock('stdClass'); 88 | $connector->expects($this->once())->method('getOptions')->with($this->equalTo($config))->will($this->returnValue(array('options'))); 89 | $connector->expects($this->once())->method('createConnection')->with($this->equalTo($dsn), $this->equalTo($config), $this->equalTo(array('options')))->will($this->returnValue($connection)); 90 | $result = $connector->connect($config); 91 | 92 | $this->assertSame($result, $connection); 93 | } 94 | 95 | 96 | public function testSQLiteFileDatabasesMayBeConnectedTo() 97 | { 98 | $dsn = 'sqlite:'.__DIR__; 99 | $config = array('database' => __DIR__); 100 | $connector = $this->getMockBuilder(Database\Connectors\SQLiteConnector::class)->setMethods(array('createConnection', 'getOptions'))->getMock(); 101 | $connection = m::mock('stdClass'); 102 | $connector->expects($this->once())->method('getOptions')->with($this->equalTo($config))->will($this->returnValue(array('options'))); 103 | $connector->expects($this->once())->method('createConnection')->with($this->equalTo($dsn), $this->equalTo($config), $this->equalTo(array('options')))->will($this->returnValue($connection)); 104 | $result = $connector->connect($config); 105 | 106 | $this->assertSame($result, $connection); 107 | } 108 | 109 | 110 | public function testSqlServerConnectCallsCreateConnectionWithProperArguments() 111 | { 112 | $config = array('host' => 'foo', 'database' => 'bar', 'port' => 111); 113 | $dsn = $this->getDsn($config); 114 | $connector = $this->getMockBuilder(Database\Connectors\SqlServerConnector::class)->setMethods(array('createConnection', 'getOptions'))->getMock(); 115 | $connection = m::mock('stdClass'); 116 | $connector->expects($this->once())->method('getOptions')->with($this->equalTo($config))->will($this->returnValue(array('options'))); 117 | $connector->expects($this->once())->method('createConnection')->with($this->equalTo($dsn), $this->equalTo($config), $this->equalTo(array('options')))->will($this->returnValue($connection)); 118 | $result = $connector->connect($config); 119 | 120 | $this->assertSame($result, $connection); 121 | } 122 | 123 | public function testItThrowsExceptionOnFailedConnection() 124 | { 125 | $this->expectException(\Database\Exception\ConnectionException::class, "Connection to 'dsn' failed: invalid data source name"); 126 | 127 | $connector = new \Database\Connectors\MySqlConnector(); 128 | 129 | $connector->createConnection("dsn", array(), array()); 130 | } 131 | 132 | protected function getDsn(array $config) 133 | { 134 | extract($config); 135 | 136 | $port = isset($config['port']) ? ','.$port : ''; 137 | 138 | if (in_array('dblib', PDO::getAvailableDrivers())) 139 | { 140 | return "dblib:host={$host}{$port};dbname={$database}"; 141 | } 142 | else 143 | { 144 | return "sqlsrv:Server={$host}{$port};Database={$database}"; 145 | } 146 | } 147 | 148 | } 149 | -------------------------------------------------------------------------------- /src/Connectors/ConnectionFactory.php: -------------------------------------------------------------------------------- 1 | 20 | * array( 21 | * 'read' => array( 22 | * 'host' => '192.168.1.1', 23 | * ), 24 | * 'write' => array( 25 | * 'host' => '196.168.1.2' 26 | * ), 27 | * 'driver' => 'mysql', 28 | * 'database' => 'database', 29 | * 'username' => 'root', 30 | * 'password' => '', 31 | * 'charset' => 'utf8', 32 | * 'collation' => 'utf8_unicode_ci', 33 | * 'prefix' => '', 34 | * 'lazy' => true/false 35 | * ) 36 | * 37 | */ 38 | class ConnectionFactory implements ConnectionFactoryInterface 39 | { 40 | 41 | /** 42 | * @var null|string 43 | */ 44 | protected $connectionClassName = 'Database\Connection'; 45 | 46 | /** 47 | * @var LoggerInterface 48 | */ 49 | protected $logger; 50 | 51 | /** 52 | * @var array 53 | */ 54 | protected $excludedLogParams = array('password'); 55 | 56 | public function __construct($connectionClassName = null, LoggerInterface $logger = null) 57 | { 58 | if ($connectionClassName) { 59 | $this->connectionClassName = $connectionClassName; 60 | } 61 | 62 | $this->logger = $logger ?: new QueryLogger(); 63 | } 64 | 65 | /** 66 | * Establish a PDO connection based on the configuration. 67 | * 68 | * @param array $config 69 | * @return \Database\Connection 70 | */ 71 | public function make(array $config) 72 | { 73 | if (!isset($config['driver'])) { 74 | throw new \InvalidArgumentException("A driver must be specified."); 75 | } 76 | 77 | return $this->makeConnection($config, !empty($config['lazy']))->setReconnector(function (Connection $connection) use ($config) { 78 | $fresh = $this->makeConnection($config, false); 79 | 80 | return $connection->setPdo($fresh->getPdo())->setReadPdo($fresh->getReadPdo()); 81 | }); 82 | } 83 | 84 | /** 85 | * Establish a PDO connection based on the configuration, return wrapped in a Connection instance. 86 | * 87 | * @param array $config 88 | * @param bool $lazy 89 | * @return \Database\Connection 90 | */ 91 | protected function makeConnection(array $config, $lazy) 92 | { 93 | if (isset($config['read'])) { 94 | return $this->createReadWriteConnection($config, $lazy); 95 | } 96 | 97 | return $this->createSingleConnection($config, $lazy); 98 | } 99 | 100 | /** 101 | * Create a single database connection instance. 102 | * 103 | * @param array $config 104 | * @param bool $lazy 105 | * @return \Database\Connection 106 | */ 107 | protected function createSingleConnection(array $config, $lazy) 108 | { 109 | $connection = $this->createConnection(); 110 | 111 | $connection 112 | ->setExceptionHandler($this->createExceptionHandler($config)) 113 | ->setQueryGrammar($this->createQueryGrammar($config['driver'])) 114 | ->setTablePrefix(isset($config['prefix']) ? $config['prefix'] : '') 115 | ->setLogger($this->logger); 116 | 117 | if(!$lazy) 118 | { 119 | $connection->setPdo($this->createConnector($config['driver'])->connect($config)); 120 | } 121 | 122 | return $connection; 123 | } 124 | 125 | 126 | /** 127 | * @return \Database\Connection 128 | */ 129 | protected function createConnection() 130 | { 131 | return new $this->connectionClassName; 132 | } 133 | 134 | /** 135 | * Create a single database connection instance. 136 | * 137 | * @param array $config 138 | * @return \Database\Connection 139 | */ 140 | protected function createReadWriteConnection(array $config, $lazy) 141 | { 142 | $connection = $this->createSingleConnection($this->getWriteConfig($config), $lazy); 143 | 144 | if(!$lazy) 145 | { 146 | $connection->setReadPdo($this->createReadPdo($config)); 147 | } 148 | 149 | return $connection; 150 | } 151 | 152 | /** 153 | * Create a new PDO instance for reading. 154 | * 155 | * @param array $config 156 | * @return \PDO 157 | */ 158 | protected function createReadPdo(array $config) 159 | { 160 | $readConfig = $this->getReadConfig($config); 161 | 162 | return $this->createConnector($readConfig['driver'])->connect($readConfig); 163 | } 164 | 165 | /** 166 | * Get the read configuration for a read / write connection. 167 | * 168 | * @param array $config 169 | * @return array 170 | */ 171 | protected function getReadConfig(array $config) 172 | { 173 | $readConfig = $this->getReadWriteConfig($config, 'read'); 174 | 175 | return $this->mergeReadWriteConfig($config, $readConfig); 176 | } 177 | 178 | /** 179 | * Get the read configuration for a read / write connection. 180 | * 181 | * @param array $config 182 | * @return array 183 | */ 184 | protected function getWriteConfig(array $config) 185 | { 186 | $writeConfig = $this->getReadWriteConfig($config, 'write'); 187 | 188 | return $this->mergeReadWriteConfig($config, $writeConfig); 189 | } 190 | 191 | /** 192 | * Get a read / write level configuration. 193 | * 194 | * @param array $config 195 | * @param string $type 196 | * @return array 197 | */ 198 | protected function getReadWriteConfig(array $config, $type) 199 | { 200 | if (isset($config[$type][0])) { 201 | return $config[$type][array_rand($config[$type])]; 202 | } 203 | 204 | return $config[$type]; 205 | } 206 | 207 | /** 208 | * Merge a configuration for a read / write connection. 209 | * 210 | * @param array $config 211 | * @param array $merge 212 | * @return array 213 | */ 214 | protected function mergeReadWriteConfig(array $config, array $merge) 215 | { 216 | return array_diff_key(array_merge($config, $merge), array_flip(array('read', 'write'))); 217 | } 218 | 219 | /** 220 | * Create a connector instance based on the configuration. 221 | * 222 | * @param string $driver 223 | * @return \Database\Connectors\ConnectorInterface 224 | * 225 | * @throws \InvalidArgumentException 226 | */ 227 | public function createConnector($driver) 228 | { 229 | switch ($driver) { 230 | case 'mysql': 231 | return new MySqlConnector; 232 | 233 | case 'pgsql': 234 | return new PostgresConnector; 235 | 236 | case 'sqlite': 237 | return new SQLiteConnector; 238 | 239 | case 'sqlsrv': 240 | return new SqlServerConnector; 241 | } 242 | 243 | throw new \InvalidArgumentException("Unsupported driver [$driver]"); 244 | } 245 | 246 | /** 247 | * Create a new connection instance. 248 | * 249 | * @param $driver 250 | * @return MySqlGrammar|PostgresGrammar|SQLiteGrammar|SqlServerGrammar 251 | * 252 | * @throws \InvalidArgumentException 253 | */ 254 | protected function createQueryGrammar($driver) 255 | { 256 | switch ($driver) { 257 | case 'mysql': 258 | return new MySqlGrammar(); 259 | break; 260 | 261 | case 'pgsql': 262 | return new PostgresGrammar(); 263 | break; 264 | 265 | case 'sqlite': 266 | return new SQLiteGrammar(); 267 | break; 268 | 269 | case 'sqlsrv': 270 | return new SqlServerGrammar(); 271 | break; 272 | } 273 | 274 | throw new \InvalidArgumentException("Unsupported driver [$driver]"); 275 | } 276 | 277 | protected function createExceptionHandler(array $config) 278 | { 279 | $logSafeParams = array_diff_key($config, array_flip($this->excludedLogParams)); 280 | 281 | return new ExceptionHandler($logSafeParams); 282 | } 283 | } 284 | -------------------------------------------------------------------------------- /src/Query/Grammars/MySqlGrammar.php: -------------------------------------------------------------------------------- 1 | unions) { 41 | $sql = '(' . $sql . ') ' . $this->compileUnions($query); 42 | } 43 | 44 | return $sql; 45 | } 46 | 47 | /** 48 | * Compile a single union statement. 49 | * 50 | * @param array $union 51 | * @return string 52 | */ 53 | protected function compileUnion(array $union) 54 | { 55 | $joiner = $union['all'] ? ' union all ' : ' union '; 56 | 57 | return $joiner . '(' . $union['query']->toSql() . ')'; 58 | } 59 | 60 | /** 61 | * Compile the lock into SQL. 62 | * 63 | * @param \Database\Query\Builder $query 64 | * @param bool|string $value 65 | * @return string 66 | */ 67 | protected function compileLock(Builder $query, $value) 68 | { 69 | if (is_string($value)) return $value; 70 | 71 | return $value ? 'for update' : 'lock in share mode'; 72 | } 73 | 74 | /** 75 | * Compile the "group by" portions of the query. 76 | * 77 | * @param \Database\Query\Builder $query 78 | * @param array $groups 79 | * @return string 80 | */ 81 | protected function compileGroups(Builder $query, $groups) 82 | { 83 | return parent::compileGroups($query, $groups) . ($query->rollup ? ' with rollup' : ''); 84 | } 85 | 86 | /** 87 | * Compile an insert statement into SQL. 88 | * 89 | * @param \Database\Query\Builder $query 90 | * @param array $values 91 | * @return string 92 | */ 93 | public function compileInsertIgnore(Builder $query, array $values) 94 | { 95 | return $this->doCompileInsert($query, $values, 'insert ignore'); 96 | } 97 | 98 | /** 99 | * Compile an insert statement into SQL. 100 | * 101 | * @param \Database\Query\Builder $query 102 | * @param array $values 103 | * @param array $updateValues 104 | * @return string 105 | */ 106 | public function compileInsertOnDuplicateKeyUpdate(Builder $query, array $values, array $updateValues) 107 | { 108 | $insert = $this->compileInsert($query, $values); 109 | 110 | $update = $this->getUpdateColumns($updateValues); 111 | 112 | return "$insert on duplicate key update $update"; 113 | } 114 | 115 | /** 116 | * Compile a replace statement into SQL. 117 | * 118 | * @param \Database\Query\Builder $query 119 | * @param array $values 120 | * @return string 121 | */ 122 | public function compileReplace(Builder $query, array $values) 123 | { 124 | return $this->doCompileInsert($query, $values, 'replace'); 125 | } 126 | 127 | /** 128 | * @param Builder $insert 129 | * @param array $columns 130 | * @param Builder $query 131 | * @return string 132 | */ 133 | public function compileInsertIgnoreSelect(Builder $insert, array $columns, Builder $query) 134 | { 135 | return $this->doCompileInsertSelect($insert, $columns, $query, 'insert ignore'); 136 | } 137 | 138 | /** 139 | * @param Builder $insert 140 | * @param array $columns 141 | * @param Builder $query 142 | * @return string 143 | */ 144 | public function compileReplaceSelect(Builder $insert, array $columns, Builder $query) 145 | { 146 | return $this->doCompileInsertSelect($insert, $columns, $query, 'replace'); 147 | } 148 | 149 | /** 150 | * Compile an insert select on duplicate key update statement into SQL. 151 | * 152 | * @param Builder $insert 153 | * @param array $columns 154 | * @param Builder $query 155 | * @param array $updateValues 156 | * @return string 157 | */ 158 | public function compileInsertSelectOnDuplicateKeyUpdate(Builder $insert, array $columns, Builder $query, array $updateValues) 159 | { 160 | $insert = $this->doCompileInsertSelect($insert, $columns, $query, 'insert'); 161 | 162 | $update = $this->getUpdateColumns($updateValues); 163 | 164 | return "$insert on duplicate key update $update"; 165 | } 166 | 167 | /** 168 | * Compile an update statement into SQL. 169 | * 170 | * @param \Database\Query\Builder $query 171 | * @param array $values 172 | * @return string 173 | */ 174 | public function compileUpdate(Builder $query, $values) 175 | { 176 | $sql = parent::compileUpdate($query, $values); 177 | 178 | if (isset($query->orders)) { 179 | $sql .= ' ' . $this->compileOrders($query, $query->orders); 180 | } 181 | 182 | if (isset($query->limit)) { 183 | $sql .= ' ' . $this->compileLimit($query, $query->limit); 184 | } 185 | 186 | return rtrim($sql); 187 | } 188 | 189 | /** 190 | * Compile a delete statement into SQL. 191 | * 192 | * @param \Database\Query\Builder $query 193 | * @return string 194 | */ 195 | public function compileDelete(Builder $query) 196 | { 197 | $table = $this->wrapTable($query->from); 198 | 199 | $where = is_array($query->wheres) ? $this->compileWheres($query) : ''; 200 | 201 | if (isset($query->joins)) { 202 | $joins = ' ' . $this->compileJoins($query, $query->joins); 203 | 204 | return trim("delete $table from {$table}{$joins} $where"); 205 | } 206 | 207 | return trim("delete from $table $where"); 208 | } 209 | 210 | /** 211 | * Wrap a single string in keyword identifiers. 212 | * 213 | * @param string $value 214 | * @return string 215 | */ 216 | protected function wrapValue($value) 217 | { 218 | if ($value === '*') return $value; 219 | 220 | return '`' . str_replace('`', '``', $value) . '`'; 221 | } 222 | 223 | /** 224 | * @param Builder $query 225 | * @param OutfileClause $outfileClause 226 | * @return string 227 | */ 228 | protected function compileOutfile(Builder $query, OutfileClause $outfileClause) 229 | { 230 | $sqlParts = array("into $outfileClause->type '$outfileClause->file'"); 231 | 232 | if($options = $this->buildInfileOutfileOptions($outfileClause)) 233 | { 234 | $sqlParts[] = $options; 235 | } 236 | 237 | return implode(' ', $sqlParts); 238 | } 239 | 240 | /** 241 | * @param Builder $query 242 | * @param InfileClause $infile 243 | * @return string 244 | */ 245 | public function compileInfile(Builder $query, InfileClause $infile) 246 | { 247 | $local = $infile->local ? 'local ' : ''; 248 | 249 | $type = $infile->type ? ($infile->type . ' ') : ''; 250 | 251 | $sqlParts = array("load data {$local}infile '$infile->file' {$type}into table " . $this->wrapTable($query->from)); 252 | 253 | if($options = $this->buildInfileOutfileOptions($infile)) 254 | { 255 | $sqlParts[] = $options; 256 | } 257 | 258 | if($infile->ignoreLines) 259 | { 260 | $sqlParts[] = "ignore $infile->ignoreLines lines"; 261 | } 262 | 263 | $sqlParts[] = '(' . $this->columnize($infile->columns) . ')'; 264 | 265 | if($infile->rules) 266 | { 267 | $sqlParts[] = 'set ' . $this->getUpdateColumns($infile->rules); 268 | } 269 | 270 | return implode(' ', $sqlParts); 271 | } 272 | 273 | /** 274 | * @param InfileClause|OutfileClause $infile 275 | * @return string 276 | */ 277 | private function buildInfileOutfileOptions($infile) 278 | { 279 | $sqlParts = array(); 280 | 281 | $optionally = $infile->optionallyEnclosedBy ? 'optionally ' : ''; 282 | 283 | if(isset($infile->characterSet)) 284 | { 285 | $sqlParts[] = "character set $infile->characterSet"; 286 | } 287 | 288 | $parts = array( 289 | 'fields' => array( 290 | 'fieldsTerminatedBy' => 'terminated by', 291 | 'enclosedBy' => $optionally . 'enclosed by', 292 | 'escapedBy' => 'escaped by', 293 | ), 294 | 'lines' => array( 295 | 'linesStartingBy' => 'starting by', 296 | 'linesTerminatedBy' => 'terminated by', 297 | ) 298 | ); 299 | 300 | foreach ($parts as $type => $components) 301 | { 302 | foreach($components as $property => $sql) 303 | { 304 | if(isset($infile->$property)) 305 | { 306 | $sqlParts[] = trim("$type $sql '{$infile->$property}'"); 307 | 308 | $type = ''; 309 | } 310 | } 311 | } 312 | 313 | return implode(' ', $sqlParts); 314 | } 315 | } 316 | -------------------------------------------------------------------------------- /tests/unit/Database/DatabaseConnectionTest.php: -------------------------------------------------------------------------------- 1 | getMockBuilder(DatabaseConnectionTestMockPDO::class)->setMethods(array('prepare'))->getMock(); 14 | $statement = $this->getMockBuilder(PDOStatement::class)->setMethods(array('execute', 'fetchColumn'))->getMock(); 15 | $pdo->expects($this->once())->method('prepare')->with($this->equalTo($query))->will($this->returnValue($statement)); 16 | $statement->expects($this->once())->method('execute')->with($this->equalTo($args)); 17 | return array($pdo, $statement); 18 | } 19 | 20 | 21 | public function testFetchOneCallsSelectAndReturnsSingleResult() 22 | { 23 | list($pdo, $statement) = $this->getMockPdoAndStatement('foo', array('bar' => 'baz')); 24 | $connection = new \Database\Connection($pdo); 25 | 26 | $statement->expects($this->once())->method('fetchColumn')->will($this->returnValue('boom')); 27 | 28 | $result = $connection->fetchOne('foo', array('bar' => 'baz')); 29 | $this->assertEquals('boom', $result); 30 | } 31 | 32 | 33 | public function testFetchProperlyCallsPDO() 34 | { 35 | $pdo = $this->getMockBuilder(DatabaseConnectionTestMockPDO::class)->setMethods(array('prepare'))->getMock(); 36 | $writePdo = $this->getMockBuilder(DatabaseConnectionTestMockPDO::class)->setMethods(array('prepare', 'inTransaction'))->getMock(); 37 | $writePdo->expects($this->never())->method('prepare'); 38 | $writePdo->expects($this->exactly(2))->method('inTransaction')->willReturn(false); 39 | $statement = $this->getMockBuilder(PDOStatement::class)->setMethods(array('execute', 'fetch'))->getMock(); 40 | $statement->expects($this->once())->method('execute')->with($this->equalTo(array('foo' => 'bar'))); 41 | $statement->expects($this->once())->method('fetch')->will($this->returnValue(array('boom'))); 42 | $pdo->expects($this->once())->method('prepare')->with('foo')->will($this->returnValue($statement)); 43 | $connection = new \Database\Connection($writePdo); 44 | $connection->setReadPdo($pdo); 45 | $connection->setPdo($writePdo); 46 | $results = $connection->fetch('foo', array('foo' => 'bar')); 47 | $this->assertEquals(array('boom'), $results); 48 | } 49 | 50 | public function testQueryProperlyCallsPDO() 51 | { 52 | list($pdo, $statement) = $this->getMockPdoAndStatement('foo', array('bar')); 53 | $connection = new \Database\Connection($pdo); 54 | 55 | $results = $connection->query('foo', array('bar')); 56 | $this->assertInstanceOf('PDOStatement', $results); 57 | } 58 | 59 | 60 | public function testTransactionMethodRunsSuccessfully() 61 | { 62 | $pdo = $this->getMockBuilder(DatabaseConnectionTestMockPDO::class)->setMethods(array('beginTransaction','commit'))->getMock(); 63 | $mock = new \Database\Connection($pdo); 64 | $pdo->expects($this->once())->method('beginTransaction'); 65 | $pdo->expects($this->once())->method('commit'); 66 | $result = $mock->transaction(function($db) { return $db; }); 67 | $this->assertEquals($mock, $result); 68 | } 69 | 70 | 71 | public function testTransactionMethodRollsbackAndThrows() 72 | { 73 | $pdo = $this->getMockBuilder(DatabaseConnectionTestMockPDO::class)->setMethods(array('beginTransaction','commit','rollback'))->getMock(); 74 | $mock = new \Database\Connection($pdo); 75 | $pdo->expects($this->once())->method('beginTransaction'); 76 | $pdo->expects($this->once())->method('rollBack'); 77 | $pdo->expects($this->never())->method('commit'); 78 | try 79 | { 80 | $mock->transaction(function() { throw new Exception('foo'); }); 81 | } 82 | catch (Exception $e) 83 | { 84 | $this->assertEquals('foo', $e->getMessage()); 85 | } 86 | } 87 | 88 | 89 | public function testFromCreatesNewQueryBuilder() 90 | { 91 | $conn = $this->getMockConnection(); 92 | $builder = $conn->table('users'); 93 | $this->assertInstanceOf('Database\Query\Builder', $builder); 94 | $this->assertEquals('users', $builder->from); 95 | } 96 | 97 | 98 | public function testPrepareBindings() 99 | { 100 | $date = m::mock('DateTime'); 101 | $date->shouldReceive('format')->once()->with('foo')->andReturn('bar'); 102 | $bindings = array('test' => $date); 103 | $conn = $this->getMockConnection(); 104 | $grammar = m::mock('Database\Query\Grammars\Grammar'); 105 | $grammar->shouldReceive('getDateFormat')->once()->andReturn('foo'); 106 | $conn->setQueryGrammar($grammar); 107 | $result = $conn->prepareBindings($bindings); 108 | $this->assertEquals(array('test' => 'bar'), $result); 109 | } 110 | 111 | public function testItProxiesInsertToBuilder() 112 | { 113 | $this->doInsertTypeProxyCallsToBuilder('insert', 'insert'); 114 | } 115 | 116 | public function testItProxiesInsertIgnoreToBuilder() 117 | { 118 | $this->doInsertTypeProxyCallsToBuilder('insertIgnore', 'insert ignore'); 119 | } 120 | 121 | public function testItProxiesReplaceToBuilder() 122 | { 123 | $this->doInsertTypeProxyCallsToBuilder('replace', 'replace'); 124 | } 125 | 126 | public function testItProxiesDeleteToBuilder() 127 | { 128 | list($pdo, $statement) = $this->getMockPdoAndStatement('delete from "testTable" where foo = ?', array('bar')); 129 | $connection = new \Database\Connection($pdo); 130 | 131 | $connection->delete('testTable', 'foo = ?', array('bar')); 132 | } 133 | 134 | public function testItProxiesUpdateToBuilder() 135 | { 136 | list($pdo, $statement) = $this->getMockPdoAndStatement('update "testTable" set "fuzz" = ? where foo = ?', array('buzz','bar')); 137 | $connection = new \Database\Connection($pdo); 138 | 139 | $res = $connection->update('testTable', array('fuzz' => 'buzz'), 'foo = ?', array('bar')); 140 | 141 | $this->assertSame($statement, $res); 142 | } 143 | 144 | public function testItProxiesInsertUpdateToBuilder() 145 | { 146 | list($pdo, $statement) = $this->getMockPdoAndStatement( 147 | "insert into `testTable` (`foo`) values (?) on duplicate key update `bar` = ?", 148 | array('a', 'b') 149 | ); 150 | $connection = new \Database\Connection($pdo, new \Database\Query\Grammars\MySqlGrammar()); 151 | 152 | $res = $connection->insertUpdate('testTable', array('foo' => 'a'), array('bar' => 'b')); 153 | 154 | $this->assertSame($statement, $res); 155 | } 156 | 157 | private function doInsertTypeProxyCallsToBuilder($type, $sql) 158 | { 159 | list($pdo, $statement) = $this->getMockPdoAndStatement("$sql into `testTable` (`foo`) values (?)", array('a')); 160 | $connection = new \Database\Connection($pdo, new \Database\Query\Grammars\MySqlGrammar()); 161 | 162 | $res = $connection->{$type}('testTable', array('foo' => 'a')); 163 | 164 | $this->assertSame($statement, $res); 165 | } 166 | 167 | public function testQuoteInto() 168 | { 169 | $pdo = $this->getMockBuilder(DatabaseConnectionTestMockPDO::class)->setMethods(array('quote'))->getMock(); 170 | 171 | $connection = new \Database\Connection($pdo, new \Database\Query\Grammars\MySqlGrammar()); 172 | 173 | $pdo 174 | ->expects($this->exactly(2)) 175 | ->method('quote') 176 | ->withConsecutive(array('foo'), array('bar')) 177 | ->willReturnOnConsecutiveCalls('`foo`', '`bar`'); 178 | 179 | $string = $connection->quoteInto('col1 = ? AND col2 = ?', array('foo', 'bar')); 180 | 181 | $this->assertEquals('col1 = `foo` AND col2 = `bar`', $string); 182 | } 183 | 184 | public function testQuote() 185 | { 186 | $pdo = $this->getMockBuilder(DatabaseConnectionTestMockPDO::class)->setMethods(array('quote'))->getMock(); 187 | $connection = new \Database\Connection($pdo); 188 | 189 | $pdo->expects($this->once())->method('quote')->with('foo')->willReturn('`foo`'); 190 | 191 | $string = $connection->quote('foo'); 192 | 193 | $this->assertEquals('`foo`', $string); 194 | } 195 | 196 | public function testQuoteArray() 197 | { 198 | $pdo = $this->getMockBuilder(DatabaseConnectionTestMockPDO::class)->setMethods(array('quote'))->getMock(); 199 | $connection = new \Database\Connection($pdo); 200 | 201 | $pdo 202 | ->expects($this->exactly(2)) 203 | ->method('quote') 204 | ->withConsecutive(array('foo'), array('bar')) 205 | ->willReturnOnConsecutiveCalls('`foo`', '`bar`'); 206 | 207 | $array = $connection->quote(array('foo', 'bar')); 208 | 209 | $this->assertEquals(array('`foo`', '`bar`'), $array); 210 | } 211 | 212 | public function testSetAndGetPrefix() 213 | { 214 | $connection = $this->getMockConnection(array()); 215 | 216 | $connection->setTablePrefix('foo'); 217 | $this->assertEquals('foo', $connection->getTablePrefix()); 218 | 219 | $connection->setTablePrefix('bar'); 220 | $this->assertEquals('bar', $connection->getTablePrefix()); 221 | } 222 | 223 | public function testItCorrectlyEnablesAndDisablesLogging() 224 | { 225 | $connection = $this->getMockConnection(array()); 226 | 227 | $connection->enableQueryLog(); 228 | $this->assertEquals(true, $connection->logging()); 229 | 230 | $connection->disableQueryLog(); 231 | $this->assertEquals(false, $connection->logging()); 232 | } 233 | 234 | public function testItCorrectlySetsTheFetchMode() 235 | { 236 | $connection = $this->getMockConnection(array()); 237 | 238 | $connection->setFetchMode(10); 239 | $this->assertEquals(10, $connection->getFetchMode()); 240 | 241 | $connection->setFetchMode(2); 242 | $this->assertEquals(2, $connection->getFetchMode()); 243 | } 244 | 245 | /** 246 | * @param array $methods 247 | * @param null $pdo 248 | * @return Database\Connection 249 | */ 250 | protected function getMockConnection($methods = array(), $pdo = null) 251 | { 252 | $pdo = $pdo ?: new DatabaseConnectionTestMockPDO; 253 | return new \Database\Connection($pdo); 254 | } 255 | } 256 | 257 | class DatabaseConnectionTestMockPDO extends PDO { public function __construct() {} } 258 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Database 2 | 3 | ![Build Status](https://github.com/mrjgreen/database/actions/workflows/php.yml/badge.svg) 4 | [![Coverage Status](https://coveralls.io/repos/github/mrjgreen/database/badge.svg)](https://coveralls.io/github/mrjgreen/database) 5 | [![Latest Stable Version](https://poser.pugx.org/mrjgreen/database/v/stable)](https://packagist.org/packages/mrjgreen/database) 6 | [![License](https://poser.pugx.org/mrjgreen/database/license)](https://packagist.org/packages/mrjgreen/database) 7 | [![Total Downloads](https://poser.pugx.org/mrjgreen/database/downloads)](https://packagist.org/packages/mrjgreen/database) 8 | 9 | The Database component is a framework agnostic PHP database abstraction layer, providing an expressive query builder. It currently supports MySQL, Postgres, SQL Server, and SQLite. 10 | 11 | Features: 12 | 13 | - Simple CRUD functions 14 | - Support for Insert Ignore / Replace 15 | - Support for Insert On Duplicate Key Update 16 | - Support for direct `INSERT INTO ... SELECT * FROM` queries 17 | - Buffered inserts from Traversable/Iterator interfaces 18 | - Joins 19 | - Sub Queries 20 | - Nested Queries 21 | - Bulk Inserts 22 | - MySQL `SELECT * INTO OUTFILE '...'` 23 | - MySQL `LOAD DATA INFILE '...'` 24 | - Lazy Connections 25 | - PSR Compatible Logging 26 | - Database Connection Resolver 27 | 28 | The component is based on Laravel's Illuminate\Database and has very familiar syntax. The core Query Builder is mostly compatible. The main alterations are to the composition of the objects, and most significantly the creation and resolution of connections within the ConnectionFactory and ConnectionResolver classes. 29 | 30 | ### Installation 31 | 32 | ``` 33 | composer require mrjgreen/database 34 | ``` 35 | 36 | ### Basic Example 37 | 38 | First, create a new "ConnectionFactory" instance. 39 | 40 | ```PHP 41 | $factory = new \Database\Connectors\ConnectionFactory(); 42 | 43 | $connection = $factory->make(array( 44 | 'driver' => 'mysql', 45 | 'host' => 'localhost', 46 | 'username' => 'root', 47 | 'password' => 'password', 48 | 'charset' => 'utf8', 49 | 'collation' => 'utf8_unicode_ci', 50 | 51 | // Don't connect until we execute our first query 52 | 'lazy' => true, 53 | 54 | // Set PDO attributes after connection 55 | 'options' => array( 56 | PDO::MYSQL_ATTR_LOCAL_INFILE => true, 57 | PDO::ATTR_EMULATE_PREPARES => true, 58 | ) 59 | )); 60 | 61 | $connection->query("SELECT id, username FROM customers"); 62 | ``` 63 | 64 | ## Documentation 65 | 66 | ### Table of Contents 67 | 68 | - [**Connection**](#connection) 69 | - [MySQL](#mysql) 70 | - [SQLite](#sqlite) 71 | - [Default Connection Options](#default-connection-options) 72 | - [**Connection Resolver**](#connection-resolver) 73 | - [**Raw Queries**](#raw-queries) 74 | - [Query Shortcuts](#query-shortcuts) 75 | - [**Query Builder**](#query-builder) 76 | - [Selects](#selects) 77 | - [Get All](#get-all) 78 | - [Get First Row](#get-first-row) 79 | - [Find By ID](#find-by-id) 80 | - [Select Columns](#select-columns) 81 | - [Limit and Offset](#limit-and-offset) 82 | - [Where](#where) 83 | - [Grouped Where](#grouped-where) 84 | - [Group By, Order By and Having](#group-by-order-by-and-having) 85 | - [Joins](#joins) 86 | - [Sub Selects](#sub-selects) 87 | - [MySQL Outfile](#mysql-outfile) 88 | - [Insert](#insert) 89 | - [Insert Ignore](#insert-ignore) 90 | - [Replace](#replace) 91 | - [Batch Insert](#batch-insert) 92 | - [On Duplicate Key Update](#on-duplicate-key-update) 93 | - [Insert Select](#insert-select) 94 | - [Buffered Iterator Insert](#buffered-iterator-insert) 95 | - [Update](#update) 96 | - [Delete](#delete) 97 | - [Raw Expressions](#raw-expressions) 98 | - [Get SQL](#get-sql-query-and-bindings) 99 | - [Raw PDO Instance](#raw-pdo-instance) 100 | 101 | ## Connection 102 | 103 | The Database component supports MySQL, SQLite, SqlServer and PostgreSQL drivers. You can specify the driver during connection and the associated configuration when creating a new connection. You can also create multiple connections, but you can use alias for only one connection at a time.; 104 | 105 | ```PHP 106 | $factory = new \Database\Connectors\ConnectionFactory(); 107 | ``` 108 | 109 | ### MySQL 110 | 111 | ```PHP 112 | $connection = $factory->make(array( 113 | 'driver' => 'mysql', 114 | 'host' => 'localhost', 115 | 'username' => 'root', 116 | 'password' => 'password', 117 | 'charset' => 'utf8', 118 | 'collation' => 'utf8_unicode_ci', 119 | )); 120 | 121 | $connection->fetchAll("SELECT id, username FROM customers"); 122 | 123 | $connection->table('customers') 124 | ->find(12); 125 | 126 | $connection->table('customers') 127 | ->join('products', 'customer.id', '=', 'customer_id') 128 | ->where('favourites', '=', 1) 129 | ->get(); 130 | ``` 131 | 132 | ### SQLite 133 | 134 | ```PHP 135 | $connection = $factory->make(array( 136 | 'driver' => 'sqlite', 137 | 'database' => '/path/to/sqlite.db', 138 | )); 139 | ``` 140 | 141 | ###Default Connection Options 142 | By default the following PDO attributes will be set on connection. You can override these or add to them in the 143 | `options` array parameter in the connection config. 144 | 145 | ```PHP 146 | PDO::ATTR_CASE => PDO::CASE_NATURAL, 147 | PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, 148 | PDO::ATTR_ORACLE_NULLS => PDO::NULL_NATURAL, 149 | PDO::ATTR_STRINGIFY_FETCHES => false, 150 | PDO::ATTR_EMULATE_PREPARES => false, 151 | ``` 152 | 153 | ## Connection Resolver 154 | 155 | Many complex applications may need more than one database connection. You can create a set of named connections inside the connection 156 | resolver, and reference them by name within in your application. 157 | 158 | ```PHP 159 | 160 | $resolver = new Database\ConnectionResolver(array( 161 | 'local' => array( 162 | 'driver' => 'mysql', 163 | 'host' => 'localhost', 164 | 'username' => 'root', 165 | 'password' => 'password', 166 | 'charset' => 'utf8', 167 | 'collation' => 'utf8_unicode_ci', 168 | ), 169 | 'archive' => array( 170 | 'driver' => 'mysql', 171 | 'host' => '1.2.3.456', 172 | 'username' => 'root', 173 | 'password' => 'password', 174 | 'charset' => 'utf8', 175 | 'collation' => 'utf8_unicode_ci', 176 | ), 177 | )); 178 | 179 | $dbLocal = $resolver->connection('local'); 180 | 181 | // Use it 182 | $dbLocal->table('users')->get(); 183 | 184 | 185 | $dbArchive = $resolver->connection('archive'); 186 | // Etc... 187 | ``` 188 | 189 | If you request a connection that you have used previously in your application, the connection resolver will return the same connection, rather than create a new one. 190 | 191 | You can set a default connection after creating the resolver, so you don't have to specify the connection name throughout your application. 192 | 193 | ```PHP 194 | $resolver->setDefaultConnection('local'); 195 | 196 | // Returns the `local` connection 197 | $resolver->connection(); 198 | ``` 199 | 200 | ##Raw Queries 201 | Perform a query, with bindings and return the PDOStatement object 202 | 203 | ```PHP 204 | $statement = $connection->query('SELECT * FROM users WHERE name = ?', array('John Smith')); 205 | 206 | // PDOStatement 207 | $statement->rowCount(); 208 | $statement->fetchAll(); 209 | ``` 210 | 211 | ###Query Shortcuts 212 | 213 | ```PHP 214 | $firstRow = $connection->fetch('SELECT * FROM users WHERE name = ?', array('John Smith')); 215 | 216 | $allRows = $connection->fetchAll('SELECT * FROM users WHERE name = ?', array('John Smith')); 217 | 218 | $firstColumnFirstRow = $connection->fetchOne('SELECT COUNT(*) FROM users WHERE name = ?', array('John Smith')); 219 | ``` 220 | 221 | ##Query Builder 222 | 223 | ###Selects 224 | 225 | ####Get PDOStatement 226 | If you intend to iterate through the rows, it may be more efficient to get the PDOStatement 227 | 228 | ```PHP 229 | $rows = $connection->table('users')->query(); 230 | ``` 231 | 232 | ####Get All 233 | 234 | ```PHP 235 | $rows = $connection->table('users')->get(); 236 | ``` 237 | 238 | ####Get First Row 239 | 240 | ```PHP 241 | $row = $connection->table('users')->first(); 242 | ``` 243 | 244 | ####Find By ID 245 | 246 | ```PHP 247 | $row = $connection->table('users')->find(6); 248 | ``` 249 | 250 | The query above assumes your table's primary key is `'id'` and you want to retreive all columns. You can specify the columns you want to fetch, and your primary key: 251 | 252 | ```PHP 253 | $connection->table('users')->find(3, array('user_id', 'name', 'email'), 'user_id'); 254 | ``` 255 | 256 | ####Select Columns 257 | 258 | ```PHP 259 | $rows = $connection->table('users')->select('name')->addSelect('age', 'dob')->get(); 260 | ``` 261 | 262 | ####Limit and Offset 263 | 264 | ```PHP 265 | $connection->table('users')->offset(100)->limit(10); 266 | ``` 267 | 268 | ####Where 269 | 270 | ```PHP 271 | $connection->table('user') 272 | ->where('username', '=', 'jsmith') 273 | ->whereNotIn('age', array(10,20,30)) 274 | ->orWhere('type', '=', 'admin') 275 | ->orWhereNot('name', 'LIKE', '%Smith%') 276 | ->get(); 277 | ``` 278 | 279 | #####Grouped Where 280 | 281 | ```PHP 282 | $connection->table('users') 283 | ->where('age', '>', 10) 284 | ->orWhere(function($subWhere) 285 | { 286 | $subWhere 287 | ->where('animal', '=', 'dog') 288 | ->where('age', '>', 1) 289 | }); 290 | 291 | SELECT * FROM `users` WHERE `age` > 10 or (`age` > 1 and `animal` = 'dog')`. 292 | ``` 293 | 294 | ####Group By, Order By and Having 295 | 296 | ```PHP 297 | $users = $connection->table('users') 298 | ->orderBy('name', 'desc') 299 | ->groupBy('count') 300 | ->having('count', '>', 100) 301 | ->get(); 302 | ``` 303 | 304 | #### Joins 305 | 306 | ```PHP 307 | $connection->table('users') 308 | ->join('products', 'user_id', '=', 'users.id') 309 | ->get(); 310 | /* 311 | ->leftJoin() 312 | ->rightJoin() 313 | */ 314 | ``` 315 | 316 | ##### Multiple Join Criteria 317 | 318 | If you need more than one criterion to join a table then you can pass a closure as second parameter. 319 | 320 | ```PHP 321 | ->join('products', function($table) 322 | { 323 | $table->on('users.id', '=', 'products.user_id'); 324 | $table->on('products.price', '>', 'users.max_price'); 325 | }) 326 | ``` 327 | 328 | ####Sub Selects 329 | 330 | ```PHP 331 | $query = $connection->table('users') 332 | ->selectSub(function($subQuery){ 333 | $subQuery 334 | ->from('customer') 335 | ->select('name') 336 | ->where('id', '=', $subQuery->raw('users.id')); 337 | }, 'tmp'); 338 | ``` 339 | 340 | This will produce a query like this: 341 | 342 | SELECT (SELECT `name` FROM `customer` WHERE `id` = users.id) as `tmp` FROM `users` 343 | 344 | ####Aggregates 345 | 346 | #####Count 347 | 348 | ```PHP 349 | $count = $connection->table('users')->count(); 350 | ``` 351 | 352 | #####Min 353 | 354 | ```PHP 355 | $count = $connection->table('users')->min('age'); 356 | ``` 357 | 358 | #####Max 359 | 360 | ```PHP 361 | $count = $connection->table('users')->max('age'); 362 | ``` 363 | 364 | #####Average 365 | 366 | ```PHP 367 | $count = $connection->table('users')->avg('age'); 368 | ``` 369 | 370 | #####Sum 371 | 372 | ```PHP 373 | $count = $connection->table('users')->sum('age'); 374 | ``` 375 | 376 | ####MySQL Outfile 377 | 378 | ```PHP 379 | $connection 380 | ->table('users') 381 | ->select('*') 382 | ->where('bar', '=', 'baz') 383 | ->intoOutfile('filename', function(\Database\Query\OutfileClause $out){ 384 | $out 385 | ->enclosedBy(".") 386 | ->escapedBy("\\") 387 | ->linesTerminatedBy("\n\r") 388 | ->fieldsTerminatedBy(','); 389 | })->query(); 390 | ``` 391 | 392 | ###Insert 393 | 394 | ```PHP 395 | $data = array( 396 | 'username' = 'jsmith', 397 | 'name' = 'John Smith' 398 | ); 399 | $connection->table('users')->insert($data); 400 | // Returns PDOStatement 401 | 402 | `->insertGetId($data)` method returns the insert id instead of a PDOStatement 403 | ``` 404 | 405 | ###Insert Ignore 406 | Ignore errors from any rows inserted with a duplicate unique key 407 | 408 | ```PHP 409 | $data = array( 410 | 'username' = 'jsmith', 411 | 'name' = 'John Smith' 412 | ); 413 | $connection->table('users')->insertIgnore($data); 414 | ``` 415 | 416 | ###Replace 417 | Replace existing rows with a matching unique key 418 | 419 | ```PHP 420 | $data = array( 421 | 'username' = 'jsmith', 422 | 'name' = 'John Smith' 423 | ); 424 | $connection->table('users')->replace($data); 425 | ``` 426 | 427 | ####Batch Insert 428 | The query builder will intelligently handle multiple insert rows: 429 | 430 | ```PHP 431 | $data = array( 432 | array( 433 | 'username' = 'jsmith', 434 | 'name' = 'John Smith' 435 | ), 436 | array( 437 | 'username' = 'jbloggs', 438 | 'name' = 'Joe Bloggs' 439 | ), 440 | ); 441 | $connection->table('users')->insert($data); 442 | ``` 443 | 444 | You can also pass bulk inserts to replace() and insertIgnore() 445 | 446 | ###On Duplicate Key Update 447 | 448 | ```PHP 449 | $data = array( 450 | 'username' = 'jsmith', 451 | 'name' = 'John Smith' 452 | ); 453 | 454 | $now = $connection->raw('NOW()'); 455 | 456 | $connection->table('users')->insertUpdate( 457 | array('username' => 'jsmith', 'active' => $now), // Insert this data 458 | array('active' => $now) // Or partially update the row if it exists 459 | ); 460 | 461 | //insertOnDuplicateKeyUpdate() is an alias of insertUpdate 462 | ``` 463 | 464 | ####Insert Select 465 | $connection->table('users')->insertSelect(function($select){ 466 | $select->from('admin') 467 | ->select('name', 'email') 468 | ->where('status', '=', 1); 469 | 470 | }, array('name','email')); 471 | 472 | `insertIgnoreSelect` and `replaceSelect` methods are supported for the MySQL grammar driver. 473 | 474 | ####Buffered Iterator Insert 475 | If you have a large data set you can insert in batches of a chosen size (insert ignore/replace/on duplicate key update supported). 476 | 477 | This is especially useful if you want to select large data-sets from one server and insert into another. 478 | 479 | ```PHP 480 | $pdoStatement = $mainServer->table('users')->query(); // Returns a PDOStatement (which implements the `Traversable` interface) 481 | 482 | // Will be inserted in batches of 1000 as it reads from the rowset iterator. 483 | $backupServer->table('users')->buffer(1000)->insertIgnore($pdoStatement); 484 | ``` 485 | 486 | ###Update 487 | 488 | ```PHP 489 | $data = array( 490 | 'username' = 'jsmith123', 491 | 'name' = 'John Smith' 492 | ); 493 | 494 | $connection->table('users')->where('id', 123)->update($data); 495 | ``` 496 | 497 | ###Delete 498 | 499 | ```PHP 500 | $connection->table('users')->where('last_active', '>', 12)->delete(); 501 | ``` 502 | 503 | Will delete all the rows where id is greater than 5. 504 | 505 | ###Raw Expressions 506 | 507 | Wrap raw queries with `$connection->raw()` to bypass query parameter binding. NB use with caution - no sanitisation will take place. 508 | 509 | ```PHP 510 | $connection->table('users') 511 | ->select($connection->raw('DATE(activity_time) as activity_date')) 512 | ->where('user', '=', 123) 513 | ->get(); 514 | ``` 515 | 516 | ###Get SQL Query and Bindings 517 | 518 | ```PHP 519 | $query = $connection->table('users')->find(1)->toSql(); 520 | $query->toSql(); 521 | // SELECT * FROM users where `id` = ? 522 | 523 | $query->getBindings(); 524 | // array(1) 525 | ``` 526 | 527 | ###Raw PDO Instance 528 | 529 | ```PHP 530 | $connection->getPdo(); 531 | ``` 532 | -------------------------------------------------------------------------------- /src/Connection.php: -------------------------------------------------------------------------------- 1 | pdo = $pdo; 89 | 90 | $this->queryGrammar = $queryGrammar ?: new Grammar(); 91 | 92 | $this->exceptionHandler = $exceptionHandler ?: new ExceptionHandler(); 93 | 94 | $this->tablePrefix = $tablePrefix; 95 | } 96 | 97 | /** 98 | * Begin a fluent query against a database table. 99 | * 100 | * @param string $table 101 | * @return \Database\Query\Builder 102 | */ 103 | public function table($table) 104 | { 105 | $query = new Query\Builder($this, $this->getQueryGrammar()); 106 | 107 | return $query->from($table); 108 | } 109 | 110 | /** 111 | * @param $table 112 | * @param $values 113 | * @return \PDOStatement 114 | */ 115 | public function insert($table, array $values) 116 | { 117 | return $this->table($table)->insert($values); 118 | } 119 | 120 | /** 121 | * @param $table 122 | * @param $values 123 | * @return \PDOStatement 124 | */ 125 | public function insertIgnore($table, array $values) 126 | { 127 | return $this->table($table)->insertIgnore($values); 128 | } 129 | 130 | /** 131 | * @param $table 132 | * @param $values 133 | * @return \PDOStatement 134 | */ 135 | public function replace($table, array $values) 136 | { 137 | return $this->table($table)->replace($values); 138 | } 139 | 140 | /** 141 | * @param $table 142 | * @param $values 143 | * @param $updateValues 144 | * @return \PDOStatement 145 | */ 146 | public function insertUpdate($table, array $values, array $updateValues) 147 | { 148 | return $this->table($table)->insertUpdate($values, $updateValues); 149 | } 150 | 151 | /** 152 | * @param $table 153 | * @param $where 154 | * @param $bindings 155 | * @return \PDOStatement 156 | */ 157 | public function delete($table, $where, array $bindings = array()) 158 | { 159 | return $this->table($table)->whereRaw($where, $bindings)->delete(); 160 | } 161 | 162 | /** 163 | * @param $table 164 | * @param $values 165 | * @param $where 166 | * @param $bindings 167 | * @return \PDOStatement 168 | */ 169 | public function update($table, $values, $where, array $bindings = array()) 170 | { 171 | return $this->table($table)->whereRaw($where, $bindings)->update($values); 172 | } 173 | 174 | /** 175 | * Get a new raw query expression. 176 | * 177 | * @param mixed $value 178 | * @return \Database\Query\Expression 179 | */ 180 | public function raw($value) 181 | { 182 | return new Query\Expression($value); 183 | } 184 | 185 | /** 186 | * Run a select statement and return a single result. 187 | * 188 | * @param string $query 189 | * @param array $bindings 190 | * @param bool $useReadPdo 191 | * @return mixed 192 | */ 193 | public function fetchOne($query, array $bindings = array(), $useReadPdo = true) 194 | { 195 | return $this->run($query, $bindings, $useReadPdo)->fetchColumn(); 196 | } 197 | 198 | /** 199 | * Run a select statement against the database, and return the first row based on the current fetch mode 200 | * 201 | * @param string $query 202 | * @param array $bindings 203 | * @param bool $useReadPdo 204 | * @return array 205 | */ 206 | public function fetch($query, array $bindings = array(), $useReadPdo = true) 207 | { 208 | return $this->run($query, $bindings, $useReadPdo)->fetch($this->getFetchMode()); 209 | } 210 | 211 | /** 212 | * Run a select statement against the database, and return the first row as a numeric array 213 | * 214 | * @param string $query 215 | * @param array $bindings 216 | * @param bool $useReadPdo 217 | * @return array 218 | */ 219 | public function fetchNumeric($query, array $bindings = array(), $useReadPdo = true) 220 | { 221 | return $this->run($query, $bindings, $useReadPdo)->fetch(PDO::FETCH_NUM); 222 | } 223 | 224 | /** 225 | * Run a select statement against the database, and return an array containing all rows based on the current fetch mode 226 | * 227 | * @param string $query 228 | * @param array $bindings 229 | * @param bool $useReadPdo 230 | * @return array 231 | */ 232 | public function fetchAll($query, array $bindings = array(), $useReadPdo = true) 233 | { 234 | return $this->run($query, $bindings, $useReadPdo)->fetchAll($this->getFetchMode()); 235 | } 236 | 237 | /** 238 | * @param string $query 239 | * @param array $bindings 240 | * @return \PDOStatement 241 | */ 242 | public function query($query, array $bindings = array()) 243 | { 244 | return $this->run($query, $bindings); 245 | } 246 | 247 | /** 248 | * Execute the given callback in "dry run" mode. 249 | * 250 | * @param \Closure $callback 251 | * @return array 252 | */ 253 | public function pretend(Closure $callback) 254 | { 255 | $this->pretending = true; 256 | 257 | // Basically to make the database connection "pretend", we will just return 258 | // the default values for all the query methods, then we will return an 259 | // array of queries that were "executed" within the Closure callback. 260 | $callback($this); 261 | 262 | $this->pretending = false; 263 | 264 | return $this; 265 | } 266 | 267 | /** 268 | * @param null $name 269 | * @return mixed|string 270 | */ 271 | public function lastInsertId($name = null) 272 | { 273 | return $this->pdo->lastInsertId($name); 274 | } 275 | 276 | /** 277 | * Escape a value ready to be inserted into the database 278 | * 279 | * @param $string 280 | * @return array|string 281 | */ 282 | public function quote($string) 283 | { 284 | if (is_array($string)) { 285 | return array_map(array($this->pdo, 'quote'), $string); 286 | } 287 | 288 | return $this->pdo->quote($string); 289 | } 290 | 291 | /** 292 | * Escape a value or array of values and bind them into an sql statement 293 | * 294 | * @param $sql 295 | * @param array $bind 296 | * @return mixed 297 | */ 298 | public function quoteInto($sql, array $bind = array()) 299 | { 300 | foreach ($bind as $key => $value) { 301 | $replace = (is_numeric($key) ? '?' : ':' . $key); 302 | 303 | $sql = substr_replace($sql, $this->quote($value), strpos($sql, $replace), strlen($replace)); 304 | } 305 | 306 | return $sql; 307 | } 308 | 309 | /** 310 | * Run a SQL statement and log its execution context. 311 | * 312 | * @param string $query 313 | * @param array $bindings 314 | * @param bool $useReadPdo 315 | * @return \PDOStatement 316 | * 317 | * @throws \Exception 318 | */ 319 | protected function run($query, $bindings, $useReadPdo = false) 320 | { 321 | $this->reconnectIfMissingConnection(); 322 | 323 | // We can calculate the time it takes to execute the query and log the SQL, bindings and time against our logger. 324 | $start = microtime(true); 325 | 326 | $statement = $this->execute($query, $bindings, $useReadPdo); 327 | 328 | $this->logQuery($query, $bindings, $start); 329 | 330 | return $statement; 331 | } 332 | 333 | /** 334 | * @param $query 335 | * @param $bindings 336 | * @param $useReadPdo 337 | * @return \PDOStatement 338 | * @throws \Exception 339 | */ 340 | private function execute($query, $bindings, $useReadPdo) 341 | { 342 | if ($this->pretending()) return new \PDOStatement(); 343 | 344 | $pdo = $useReadPdo ? $this->getReadPdo() : $this->getPdo(); 345 | 346 | try { 347 | // For update or delete statements, we want to get the number of rows affected 348 | // by the statement and return that back to the developer. We'll first need 349 | // to execute the statement and then we'll use PDO to fetch the affected. 350 | $statement = $pdo->prepare($query); 351 | 352 | $statement->execute($this->prepareBindings($bindings)); 353 | } 354 | // If an exception occurs when attempting to run a query, we'll call the exception handler 355 | // if there is one, or throw the exception if not 356 | catch (\Exception $e) { 357 | 358 | if($this->exceptionHandler) 359 | { 360 | $this->exceptionHandler->handle($query, $this->prepareBindings($bindings), $e); 361 | } 362 | 363 | throw $e; 364 | } 365 | 366 | return $statement; 367 | } 368 | 369 | /** 370 | * Prepare the query bindings for execution. 371 | * 372 | * @param array $bindings 373 | * @return array 374 | */ 375 | public function prepareBindings(array $bindings) 376 | { 377 | $grammar = $this->getQueryGrammar(); 378 | 379 | foreach ($bindings as $key => $value) { 380 | // We need to transform all instances of the DateTime class into an actual 381 | // date string. Each query grammar maintains its own date string format 382 | // so we'll just ask the grammar for the format to get from the date. 383 | if ($value instanceof DateTime) { 384 | $bindings[$key] = $value->format($grammar->getDateFormat()); 385 | } elseif ($value === false) { 386 | $bindings[$key] = 0; 387 | } 388 | } 389 | 390 | return $bindings; 391 | } 392 | 393 | /** 394 | * Execute a Closure within a transaction. 395 | * 396 | * @param \Closure $callback 397 | * @return mixed 398 | * 399 | * @throws \Exception 400 | */ 401 | public function transaction(Closure $callback) 402 | { 403 | $this->beginTransaction(); 404 | 405 | // We'll simply execute the given callback within a try / catch block 406 | // and if we catch any exception we can rollback the transaction 407 | // so that none of the changes are persisted to the database. 408 | try { 409 | $result = $callback($this); 410 | 411 | $this->commit(); 412 | } 413 | 414 | // If we catch an exception, we will roll back so nothing gets messed 415 | // up in the database. Then we'll re-throw the exception so it can 416 | // be handled how the developer sees fit for their applications. 417 | catch (\Exception $e) { 418 | $this->rollBack(); 419 | 420 | throw $e; 421 | } 422 | 423 | return $result; 424 | } 425 | 426 | /** 427 | * Start a new database transaction. 428 | * 429 | * @return $this 430 | */ 431 | public function beginTransaction() 432 | { 433 | $this->reconnectIfMissingConnection(); 434 | 435 | $this->pdo->beginTransaction(); 436 | 437 | return $this; 438 | } 439 | 440 | /** 441 | * Commit the active database transaction. 442 | * 443 | * @return $this 444 | */ 445 | public function commit() 446 | { 447 | $this->pdo->commit(); 448 | 449 | return $this; 450 | } 451 | 452 | /** 453 | * @return $this 454 | */ 455 | public function rollBack() 456 | { 457 | $this->pdo->rollBack(); 458 | 459 | return $this; 460 | } 461 | 462 | /** 463 | * @return bool 464 | */ 465 | public function inTransaction() 466 | { 467 | return $this->pdo->inTransaction(); 468 | } 469 | 470 | /** 471 | * Disconnect from the underlying PDO connection. 472 | * 473 | * @return $this 474 | */ 475 | public function disconnect() 476 | { 477 | $this->setPdo(null)->setReadPdo(null); 478 | 479 | return $this; 480 | } 481 | 482 | /** 483 | * 484 | */ 485 | public function connect() 486 | { 487 | $this->reconnectIfMissingConnection(); 488 | 489 | return $this; 490 | } 491 | 492 | /** 493 | * Reconnect to the database. 494 | * 495 | * @return void 496 | * 497 | * @throws \LogicException 498 | */ 499 | public function reconnect() 500 | { 501 | if (is_callable($this->reconnector)) { 502 | 503 | try 504 | { 505 | return call_user_func($this->reconnector, $this); 506 | } 507 | catch(\PDOException $e) 508 | { 509 | $this->exceptionHandler->handle("Connection attempt", array(), $e); 510 | } 511 | } 512 | 513 | throw new \LogicException("Lost connection and no reconnector available."); 514 | } 515 | 516 | /** 517 | * Reconnect to the database if a PDO connection is missing. 518 | * 519 | * @return void 520 | */ 521 | protected function reconnectIfMissingConnection() 522 | { 523 | if (is_null($this->getPdo()) || is_null($this->getReadPdo())) { 524 | $this->reconnect(); 525 | } 526 | } 527 | 528 | /** 529 | * Log a query in the connection's query log. 530 | * 531 | * @param string $query 532 | * @param array $bindings 533 | * @param float $start 534 | * @return void 535 | */ 536 | protected function logQuery($query, $bindings, $start = null) 537 | { 538 | if (!$this->loggingQueries || !$this->logger) return; 539 | 540 | $time = $start ? round((microtime(true) - $start) * 1000, 2) : null; 541 | 542 | $this->logger->debug($query, array( 543 | 'bindings' => $bindings, 544 | 'time' => $time 545 | )); 546 | } 547 | 548 | /** 549 | * Get the current PDO connection. 550 | * 551 | * @return \PDO 552 | */ 553 | public function getPdo() 554 | { 555 | return $this->pdo; 556 | } 557 | 558 | /** 559 | * Get the current PDO connection used for reading. 560 | * 561 | * @return \PDO 562 | */ 563 | public function getReadPdo() 564 | { 565 | if (!$this->readPdo || $this->pdo->inTransaction()) 566 | { 567 | return $this->getPdo(); 568 | } 569 | 570 | return $this->readPdo; 571 | } 572 | 573 | /** 574 | * Set the PDO connection. 575 | * 576 | * @param \PDO|null $pdo 577 | * @return $this 578 | */ 579 | public function setPdo($pdo) 580 | { 581 | $this->pdo = $pdo; 582 | 583 | return $this; 584 | } 585 | 586 | /** 587 | * Set the PDO connection used for reading. 588 | * 589 | * @param \PDO|null $pdo 590 | * @return $this 591 | */ 592 | public function setReadPdo($pdo) 593 | { 594 | $this->readPdo = $pdo; 595 | 596 | return $this; 597 | } 598 | 599 | /** 600 | * Set the reconnect instance on the connection. 601 | * 602 | * @param callable $reconnector 603 | * @return $this 604 | */ 605 | public function setReconnector(callable $reconnector) 606 | { 607 | $this->reconnector = $reconnector; 608 | 609 | return $this; 610 | } 611 | 612 | /** 613 | * Get the PDO driver name. 614 | * 615 | * @return string 616 | */ 617 | public function getDriverName() 618 | { 619 | return $this->pdo->getAttribute(\PDO::ATTR_DRIVER_NAME); 620 | } 621 | 622 | /** 623 | * Get the query grammar used by the connection. 624 | * 625 | * @return \Database\Query\Grammars\Grammar 626 | */ 627 | public function getQueryGrammar() 628 | { 629 | return $this->queryGrammar; 630 | } 631 | 632 | /** 633 | * Set the query grammar used by the connection. 634 | * 635 | * @param \Database\Query\Grammars\Grammar 636 | * @return $this 637 | */ 638 | public function setQueryGrammar(Query\Grammars\Grammar $grammar) 639 | { 640 | $this->queryGrammar = $grammar; 641 | 642 | return $this; 643 | } 644 | 645 | /** 646 | * Determine if the connection in a "dry run". 647 | * 648 | * @return bool 649 | */ 650 | public function pretending() 651 | { 652 | return $this->pretending === true; 653 | } 654 | 655 | /** 656 | * Get the default fetch mode for the connection. 657 | * 658 | * @return int 659 | */ 660 | public function getFetchMode() 661 | { 662 | return $this->fetchMode; 663 | } 664 | 665 | /** 666 | * Set the default fetch mode for the connection. 667 | * 668 | * @param int $fetchMode 669 | * @return int 670 | */ 671 | public function setFetchMode($fetchMode) 672 | { 673 | $this->fetchMode = $fetchMode; 674 | 675 | return $this; 676 | } 677 | 678 | /** 679 | * Enable the query log on the connection. 680 | * 681 | * @return $this 682 | */ 683 | public function enableQueryLog() 684 | { 685 | $this->loggingQueries = true; 686 | 687 | if(!$this->logger) 688 | { 689 | $this->logger = new QueryLogger(); 690 | } 691 | 692 | return $this; 693 | } 694 | 695 | /** 696 | * Disable the query log on the connection. 697 | * 698 | * @return $this 699 | */ 700 | public function disableQueryLog() 701 | { 702 | $this->loggingQueries = false; 703 | 704 | return $this; 705 | } 706 | 707 | /** 708 | * Determine whether we're logging queries. 709 | * 710 | * @return bool 711 | */ 712 | public function logging() 713 | { 714 | return $this->loggingQueries; 715 | } 716 | 717 | /** 718 | * Get the table prefix for the connection. 719 | * 720 | * @return string 721 | */ 722 | public function getTablePrefix() 723 | { 724 | return $this->tablePrefix; 725 | } 726 | 727 | /** 728 | * Set the table prefix in use by the connection. 729 | * 730 | * @param string $prefix 731 | * @return $this 732 | */ 733 | public function setTablePrefix($prefix) 734 | { 735 | $this->tablePrefix = $prefix; 736 | 737 | $this->getQueryGrammar()->setTablePrefix($prefix); 738 | 739 | return $this; 740 | } 741 | 742 | /** 743 | * Get the logger. 744 | * 745 | * @return LoggerInterface $logger 746 | */ 747 | public function getLogger() 748 | { 749 | return $this->logger; 750 | } 751 | 752 | /** 753 | * @param ExceptionHandlerInterface $exceptionHandler 754 | * @return $this 755 | */ 756 | public function setExceptionHandler(ExceptionHandlerInterface $exceptionHandler) 757 | { 758 | $this->exceptionHandler = $exceptionHandler; 759 | 760 | return $this; 761 | } 762 | } 763 | -------------------------------------------------------------------------------- /src/Query/Grammars/Grammar.php: -------------------------------------------------------------------------------- 1 | isExpression($table)) return $this->getValue($table); 57 | 58 | return $this->wrap($this->tablePrefix . $table); 59 | } 60 | 61 | /** 62 | * Wrap a value in keyword identifiers. 63 | * 64 | * @param string $value 65 | * @return string 66 | */ 67 | public function wrap($value) 68 | { 69 | if ($this->isExpression($value)) return $this->getValue($value); 70 | 71 | // If the value being wrapped has a column alias we will need to separate out 72 | // the pieces so we can wrap each of the segments of the expression on it 73 | // own, and then joins them both back together with the "as" connector. 74 | if (strpos(strtolower($value), ' as ') !== false) { 75 | $segments = explode(' ', $value); 76 | 77 | return $this->wrap($segments[0]) . ' as ' . $this->wrap($segments[2]); 78 | } 79 | 80 | $wrapped = array(); 81 | 82 | $segments = explode('.', $value); 83 | 84 | // If the value is not an aliased table expression, we'll just wrap it like 85 | // normal, so if there is more than one segment, we will wrap the first 86 | // segments as if it was a table and the rest as just regular values. 87 | foreach ($segments as $key => $segment) { 88 | if ($key == 0 && count($segments) > 1) { 89 | $wrapped[] = $this->wrapTable($segment); 90 | } else { 91 | $wrapped[] = $this->wrapValue($segment); 92 | } 93 | } 94 | 95 | return implode('.', $wrapped); 96 | } 97 | 98 | /** 99 | * Wrap a single string in keyword identifiers. 100 | * 101 | * @param string $value 102 | * @return string 103 | */ 104 | protected function wrapValue($value) 105 | { 106 | if ($value === '*') return $value; 107 | 108 | return '"' . str_replace('"', '""', $value) . '"'; 109 | } 110 | 111 | /** 112 | * Convert an array of column names into a delimited string. 113 | * 114 | * @param array $columns 115 | * @return string 116 | */ 117 | public function columnize(array $columns) 118 | { 119 | return implode(', ', array_map(array($this, 'wrap'), $columns)); 120 | } 121 | 122 | /** 123 | * Create query parameter place-holders for an array. 124 | * 125 | * @param array $values 126 | * @return string 127 | */ 128 | public function parameterize(array $values) 129 | { 130 | return implode(', ', array_map(array($this, 'parameter'), $values)); 131 | } 132 | 133 | /** 134 | * Get the appropriate query parameter place-holder for a value. 135 | * 136 | * @param mixed $value 137 | * @return string 138 | */ 139 | public function parameter($value) 140 | { 141 | return $this->isExpression($value) ? $this->getValue($value) : '?'; 142 | } 143 | 144 | /** 145 | * Get the value of a raw expression. 146 | * 147 | * @param \Database\Query\Expression $expression 148 | * @return string 149 | */ 150 | public function getValue(Expression $expression) 151 | { 152 | return $expression->getValue(); 153 | } 154 | 155 | /** 156 | * Determine if the given value is a raw expression. 157 | * 158 | * @param mixed $value 159 | * @return bool 160 | */ 161 | public function isExpression($value) 162 | { 163 | return $value instanceof Expression; 164 | } 165 | 166 | /** 167 | * Get the format for database stored dates. 168 | * 169 | * @return string 170 | */ 171 | public function getDateFormat() 172 | { 173 | return self::DATE_SQL; 174 | } 175 | 176 | /** 177 | * Get the grammar's table prefix. 178 | * 179 | * @return string 180 | */ 181 | public function getTablePrefix() 182 | { 183 | return $this->tablePrefix; 184 | } 185 | 186 | /** 187 | * Set the grammar's table prefix. 188 | * 189 | * @param string $prefix 190 | * @return $this 191 | */ 192 | public function setTablePrefix($prefix) 193 | { 194 | $this->tablePrefix = $prefix; 195 | 196 | return $this; 197 | } 198 | 199 | /** 200 | * Compile a select query into SQL. 201 | * 202 | * @param \Database\Query\Builder 203 | * @return string 204 | */ 205 | public function compileSelect(Builder $query) 206 | { 207 | if (is_null($query->columns)) $query->columns = array('*'); 208 | 209 | return trim($this->concatenate($this->compileComponents($query))); 210 | } 211 | 212 | /** 213 | * Compile the components necessary for a select clause. 214 | * 215 | * @param \Database\Query\Builder 216 | * @return array 217 | */ 218 | protected function compileComponents(Builder $query) 219 | { 220 | $sql = array(); 221 | 222 | foreach ($this->selectComponents as $component) { 223 | // To compile the query, we'll spin through each component of the query and 224 | // see if that component exists. If it does we'll just call the compiler 225 | // function for the component which is responsible for making the SQL. 226 | if (!is_null($query->$component)) { 227 | $method = 'compile' . ucfirst($component); 228 | 229 | $sql[$component] = $this->$method($query, $query->$component); 230 | } 231 | } 232 | 233 | return $sql; 234 | } 235 | 236 | /** 237 | * Compile an aggregated select clause. 238 | * 239 | * @param \Database\Query\Builder $query 240 | * @param array $aggregate 241 | * @return string 242 | */ 243 | protected function compileAggregate(Builder $query, $aggregate) 244 | { 245 | $column = $this->columnize($aggregate['columns']); 246 | 247 | // If the query has a "distinct" constraint and we're not asking for all columns 248 | // we need to prepend "distinct" onto the column name so that the query takes 249 | // it into account when it performs the aggregating operations on the data. 250 | if ($query->distinct && $column !== '*') { 251 | $column = 'distinct ' . $column; 252 | } 253 | 254 | return 'select ' . $aggregate['function'] . '(' . $column . ') as aggregate'; 255 | } 256 | 257 | /** 258 | * Compile the "select *" portion of the query. 259 | * 260 | * @param \Database\Query\Builder $query 261 | * @param array $columns 262 | * @return string 263 | */ 264 | protected function compileColumns(Builder $query, $columns) 265 | { 266 | // If the query is actually performing an aggregating select, we will let that 267 | // compiler handle the building of the select clauses, as it will need some 268 | // more syntax that is best handled by that function to keep things neat. 269 | if (!is_null($query->aggregate)) return; 270 | 271 | $select = $query->distinct ? 'select distinct ' : 'select '; 272 | 273 | return $select . $this->columnize($columns); 274 | } 275 | 276 | /** 277 | * Compile the "from" portion of the query. 278 | * 279 | * @param \Database\Query\Builder $query 280 | * @param string $table 281 | * @return string 282 | */ 283 | protected function compileFrom(Builder $query, $table) 284 | { 285 | return 'from ' . $this->wrapTable($table); 286 | } 287 | 288 | /** 289 | * Compile the "join" portions of the query. 290 | * 291 | * @param \Database\Query\Builder $query 292 | * @param array $joins 293 | * @return string 294 | */ 295 | protected function compileJoins(Builder $query, $joins) 296 | { 297 | $sql = array(); 298 | 299 | foreach ($joins as $join) { 300 | $table = $this->wrapTable($join->table); 301 | 302 | // First we need to build all of the "on" clauses for the join. There may be many 303 | // of these clauses so we will need to iterate through each one and build them 304 | // separately, then we'll join them up into a single string when we're done. 305 | $clauses = array(); 306 | 307 | foreach ($join->clauses as $clause) { 308 | $clauses[] = $this->compileJoinConstraint($clause); 309 | } 310 | 311 | foreach ($join->bindings as $binding) { 312 | $query->addBinding($binding, 'join'); 313 | } 314 | 315 | // Once we have constructed the clauses, we'll need to take the boolean connector 316 | // off of the first clause as it obviously will not be required on that clause 317 | // because it leads the rest of the clauses, thus not requiring any boolean. 318 | $clauses[0] = $this->removeLeadingBoolean($clauses[0]); 319 | 320 | $clauses = implode(' ', $clauses); 321 | 322 | $type = $join->type; 323 | 324 | // Once we have everything ready to go, we will just concatenate all the parts to 325 | // build the final join statement SQL for the query and we can then return the 326 | // final clause back to the callers as a single, stringified join statement. 327 | $sql[] = "$type join $table on $clauses"; 328 | } 329 | 330 | return implode(' ', $sql); 331 | } 332 | 333 | /** 334 | * Create a join clause constraint segment. 335 | * 336 | * @param array $clause 337 | * @return string 338 | */ 339 | protected function compileJoinConstraint(array $clause) 340 | { 341 | $first = $this->wrap($clause['first']); 342 | 343 | $second = $clause['where'] ? '?' : $this->wrap($clause['second']); 344 | 345 | return "{$clause['boolean']} $first {$clause['operator']} $second"; 346 | } 347 | 348 | /** 349 | * Compile the "where" portions of the query. 350 | * 351 | * @param \Database\Query\Builder $query 352 | * @return string 353 | */ 354 | protected function compileWheres(Builder $query) 355 | { 356 | $sql = array(); 357 | 358 | if (is_null($query->wheres)) return ''; 359 | 360 | // Each type of where clauses has its own compiler function which is responsible 361 | // for actually creating the where clauses SQL. This helps keep the code nice 362 | // and maintainable since each clause has a very small method that it uses. 363 | foreach ($query->wheres as $where) { 364 | $method = "where{$where['type']}"; 365 | 366 | $sql[] = $where['boolean'] . ' ' . $this->$method($query, $where); 367 | } 368 | 369 | // If we actually have some where clauses, we will strip off the first boolean 370 | // operator, which is added by the query builders for convenience so we can 371 | // avoid checking for the first clauses in each of the compilers methods. 372 | if (count($sql) > 0) { 373 | $sql = implode(' ', $sql); 374 | 375 | return 'where ' . preg_replace('/and |or /', '', $sql, 1); 376 | } 377 | 378 | return ''; 379 | } 380 | 381 | /** 382 | * Compile a nested where clause. 383 | * 384 | * @param \Database\Query\Builder $query 385 | * @param array $where 386 | * @return string 387 | */ 388 | protected function whereNested(Builder $query, $where) 389 | { 390 | $nested = $where['query']; 391 | 392 | return '(' . substr($this->compileWheres($nested), 6) . ')'; 393 | } 394 | 395 | /** 396 | * Compile a where condition with a sub-select. 397 | * 398 | * @param \Database\Query\Builder $query 399 | * @param array $where 400 | * @return string 401 | */ 402 | protected function whereSub(Builder $query, $where) 403 | { 404 | $select = $this->compileSelect($where['query']); 405 | 406 | return $this->wrap($where['column']) . ' ' . $where['operator'] . " ($select)"; 407 | } 408 | 409 | /** 410 | * Compile a basic where clause. 411 | * 412 | * @param \Database\Query\Builder $query 413 | * @param array $where 414 | * @return string 415 | */ 416 | protected function whereBasic(Builder $query, $where) 417 | { 418 | $value = $this->parameter($where['value']); 419 | 420 | return $this->wrap($where['column']) . ' ' . $where['operator'] . ' ' . $value; 421 | } 422 | 423 | /** 424 | * Compile a "between" where clause. 425 | * 426 | * @param \Database\Query\Builder $query 427 | * @param array $where 428 | * @return string 429 | */ 430 | protected function whereBetween(Builder $query, $where) 431 | { 432 | $between = $where['not'] ? 'not between' : 'between'; 433 | 434 | return $this->wrap($where['column']) . ' ' . $between . ' ? and ?'; 435 | } 436 | 437 | /** 438 | * Compile a where exists clause. 439 | * 440 | * @param \Database\Query\Builder $query 441 | * @param array $where 442 | * @return string 443 | */ 444 | protected function whereExists(Builder $query, $where) 445 | { 446 | return 'exists (' . $this->compileSelect($where['query']) . ')'; 447 | } 448 | 449 | /** 450 | * Compile a where exists clause. 451 | * 452 | * @param \Database\Query\Builder $query 453 | * @param array $where 454 | * @return string 455 | */ 456 | protected function whereNotExists(Builder $query, $where) 457 | { 458 | return 'not exists (' . $this->compileSelect($where['query']) . ')'; 459 | } 460 | 461 | /** 462 | * Compile a "where in" clause. 463 | * 464 | * @param \Database\Query\Builder $query 465 | * @param array $where 466 | * @return string 467 | */ 468 | protected function whereIn(Builder $query, $where) 469 | { 470 | $values = $this->parameterize($where['values']); 471 | 472 | return $this->wrap($where['column']) . ' in (' . $values . ')'; 473 | } 474 | 475 | /** 476 | * Compile a "where not in" clause. 477 | * 478 | * @param \Database\Query\Builder $query 479 | * @param array $where 480 | * @return string 481 | */ 482 | protected function whereNotIn(Builder $query, $where) 483 | { 484 | $values = $this->parameterize($where['values']); 485 | 486 | return $this->wrap($where['column']) . ' not in (' . $values . ')'; 487 | } 488 | 489 | /** 490 | * Compile a where in sub-select clause. 491 | * 492 | * @param \Database\Query\Builder $query 493 | * @param array $where 494 | * @return string 495 | */ 496 | protected function whereInSub(Builder $query, $where) 497 | { 498 | $select = $this->compileSelect($where['query']); 499 | 500 | return $this->wrap($where['column']) . ' in (' . $select . ')'; 501 | } 502 | 503 | /** 504 | * Compile a where not in sub-select clause. 505 | * 506 | * @param \Database\Query\Builder $query 507 | * @param array $where 508 | * @return string 509 | */ 510 | protected function whereNotInSub(Builder $query, $where) 511 | { 512 | $select = $this->compileSelect($where['query']); 513 | 514 | return $this->wrap($where['column']) . ' not in (' . $select . ')'; 515 | } 516 | 517 | /** 518 | * Compile a "where null" clause. 519 | * 520 | * @param \Database\Query\Builder $query 521 | * @param array $where 522 | * @return string 523 | */ 524 | protected function whereNull(Builder $query, $where) 525 | { 526 | return $this->wrap($where['column']) . ' is null'; 527 | } 528 | 529 | /** 530 | * Compile a "where not null" clause. 531 | * 532 | * @param \Database\Query\Builder $query 533 | * @param array $where 534 | * @return string 535 | */ 536 | protected function whereNotNull(Builder $query, $where) 537 | { 538 | return $this->wrap($where['column']) . ' is not null'; 539 | } 540 | 541 | /** 542 | * Compile a "where day" clause. 543 | * 544 | * @param \Database\Query\Builder $query 545 | * @param array $where 546 | * @return string 547 | */ 548 | protected function whereDay(Builder $query, $where) 549 | { 550 | return $this->dateBasedWhere('day', $query, $where); 551 | } 552 | 553 | /** 554 | * Compile a "where month" clause. 555 | * 556 | * @param \Database\Query\Builder $query 557 | * @param array $where 558 | * @return string 559 | */ 560 | protected function whereMonth(Builder $query, $where) 561 | { 562 | return $this->dateBasedWhere('month', $query, $where); 563 | } 564 | 565 | /** 566 | * Compile a "where year" clause. 567 | * 568 | * @param \Database\Query\Builder $query 569 | * @param array $where 570 | * @return string 571 | */ 572 | protected function whereYear(Builder $query, $where) 573 | { 574 | return $this->dateBasedWhere('year', $query, $where); 575 | } 576 | 577 | /** 578 | * Compile a date based where clause. 579 | * 580 | * @param string $type 581 | * @param \Database\Query\Builder $query 582 | * @param array $where 583 | * @return string 584 | */ 585 | protected function dateBasedWhere($type, Builder $query, $where) 586 | { 587 | $value = $this->parameter($where['value']); 588 | 589 | return $type . '(' . $this->wrap($where['column']) . ') ' . $where['operator'] . ' ' . $value; 590 | } 591 | 592 | /** 593 | * Compile a raw where clause. 594 | * 595 | * @param \Database\Query\Builder $query 596 | * @param array $where 597 | * @return string 598 | */ 599 | protected function whereRaw(Builder $query, $where) 600 | { 601 | return $where['sql']; 602 | } 603 | 604 | /** 605 | * Compile the "group by" portions of the query. 606 | * 607 | * @param \Database\Query\Builder $query 608 | * @param array $groups 609 | * @return string 610 | */ 611 | protected function compileGroups(Builder $query, $groups) 612 | { 613 | return 'group by ' . $this->columnize($groups); 614 | } 615 | 616 | /** 617 | * Compile the "having" portions of the query. 618 | * 619 | * @param \Database\Query\Builder $query 620 | * @param array $havings 621 | * @return string 622 | */ 623 | protected function compileHavings(Builder $query, $havings) 624 | { 625 | $sql = implode(' ', array_map(array($this, 'compileHaving'), $havings)); 626 | 627 | return 'having ' . preg_replace('/and /', '', $sql, 1); 628 | } 629 | 630 | /** 631 | * Compile a single having clause. 632 | * 633 | * @param array $having 634 | * @return string 635 | */ 636 | protected function compileHaving(array $having) 637 | { 638 | // If the having clause is "raw", we can just return the clause straight away 639 | // without doing any more processing on it. Otherwise, we will compile the 640 | // clause into SQL based on the components that make it up from builder. 641 | if ($having['type'] === 'raw') { 642 | return $having['boolean'] . ' ' . $having['sql']; 643 | } 644 | 645 | return $this->compileBasicHaving($having); 646 | } 647 | 648 | /** 649 | * Compile a basic having clause. 650 | * 651 | * @param array $having 652 | * @return string 653 | */ 654 | protected function compileBasicHaving($having) 655 | { 656 | $column = $this->wrap($having['column']); 657 | 658 | $parameter = $this->parameter($having['value']); 659 | 660 | return $having['boolean'] . ' ' . $column . ' ' . $having['operator'] . ' ' . $parameter; 661 | } 662 | 663 | /** 664 | * Compile the "order by" portions of the query. 665 | * 666 | * @param \Database\Query\Builder $query 667 | * @param array $orders 668 | * @return string 669 | */ 670 | protected function compileOrders(Builder $query, $orders) 671 | { 672 | return 'order by ' . implode(', ', array_map(function ($order) { 673 | if (isset($order['sql'])) return $order['sql']; 674 | 675 | return $this->wrap($order['column']) . ' ' . $order['direction']; 676 | } 677 | , $orders)); 678 | } 679 | 680 | /** 681 | * Compile the "limit" portions of the query. 682 | * 683 | * @param \Database\Query\Builder $query 684 | * @param int $limit 685 | * @return string 686 | */ 687 | protected function compileLimit(Builder $query, $limit) 688 | { 689 | return 'limit ' . (int)$limit; 690 | } 691 | 692 | /** 693 | * Compile the "offset" portions of the query. 694 | * 695 | * @param \Database\Query\Builder $query 696 | * @param int $offset 697 | * @return string 698 | */ 699 | protected function compileOffset(Builder $query, $offset) 700 | { 701 | return 'offset ' . (int)$offset; 702 | } 703 | 704 | /** 705 | * Compile the "union" queries attached to the main query. 706 | * 707 | * @param \Database\Query\Builder $query 708 | * @return string 709 | */ 710 | protected function compileUnions(Builder $query) 711 | { 712 | $sql = ''; 713 | 714 | foreach ($query->unions as $union) { 715 | $sql .= $this->compileUnion($union); 716 | } 717 | 718 | return ltrim($sql); 719 | } 720 | 721 | /** 722 | * Compile a single union statement. 723 | * 724 | * @param array $union 725 | * @return string 726 | */ 727 | protected function compileUnion(array $union) 728 | { 729 | $joiner = $union['all'] ? ' union all ' : ' union '; 730 | 731 | return $joiner . $union['query']->toSql(); 732 | } 733 | 734 | /** 735 | * Compile an insert statement into SQL. 736 | * 737 | * @param \Database\Query\Builder $query 738 | * @param array $values 739 | * @return string 740 | */ 741 | protected function doCompileInsert(Builder $query, array $values, $type) 742 | { 743 | // Essentially we will force every insert to be treated as a batch insert which 744 | // simply makes creating the SQL easier for us since we can utilize the same 745 | // basic routine regardless of an amount of records given to us to insert. 746 | $table = $this->wrapTable($query->from); 747 | 748 | if (!is_array(reset($values))) { 749 | $values = array($values); 750 | } 751 | 752 | $columns = $this->columnize(array_keys(reset($values))); 753 | 754 | // We need to build a list of parameter place-holders of values that are bound 755 | // to the query. Each insert should have the exact same amount of parameter 756 | // bindings so we can just go off the first list of values in this array. 757 | $parameters = $this->parameterize(reset($values)); 758 | 759 | $value = array_fill(0, count($values), "($parameters)"); 760 | 761 | $parameters = implode(', ', $value); 762 | 763 | return "$type into $table ($columns) values $parameters"; 764 | } 765 | 766 | /** 767 | * @param Builder $insert 768 | * @param array $columns 769 | * @param Builder $query 770 | * @return string 771 | */ 772 | public function doCompileInsertSelect(Builder $insert, array $columns, Builder $query, $type) 773 | { 774 | $table = $this->wrapTable($insert->from); 775 | 776 | $columns = $this->columnize($columns); 777 | 778 | $select = $this->compileSelect($query); 779 | 780 | return "$type into $table ($columns) $select"; 781 | } 782 | 783 | /** 784 | * @param Builder $insert 785 | * @param array $columns 786 | * @param Builder $query 787 | * @return string 788 | */ 789 | public function compileInsertSelect(Builder $insert, array $columns, Builder $query) 790 | { 791 | return $this->doCompileInsertSelect($insert, $columns, $query, 'insert'); 792 | } 793 | 794 | /** 795 | * @param Builder $insert 796 | * @param array $columns 797 | * @param Builder $query 798 | * @return string 799 | */ 800 | public function compileInsertIgnoreSelect(Builder $insert, array $columns, Builder $query) 801 | { 802 | $this->throwUnsupportedGrammarException("Insert ignore"); 803 | } 804 | 805 | /** 806 | * @param Builder $insert 807 | * @param array $columns 808 | * @param Builder $query 809 | * @return string 810 | */ 811 | public function compileReplaceSelect(Builder $insert, array $columns, Builder $query) 812 | { 813 | $this->throwUnsupportedGrammarException("Replace"); 814 | } 815 | 816 | /** 817 | * @param Builder $insert 818 | * @param array $columns 819 | * @param Builder $query 820 | * @param array $updateValues 821 | * @return string 822 | */ 823 | public function compileInsertSelectOnDuplicateKeyUpdate(Builder $insert, array $columns, Builder $query, array $updateValues) 824 | { 825 | $this->throwUnsupportedGrammarException("On duplicate key update"); 826 | } 827 | 828 | /** 829 | * Compile an insert statement into SQL. 830 | * 831 | * @param \Database\Query\Builder $query 832 | * @param array $values 833 | * @return string 834 | */ 835 | public function compileInsert(Builder $query, array $values) 836 | { 837 | return $this->doCompileInsert($query, $values, 'insert'); 838 | } 839 | 840 | /** 841 | * Compile an insert statement into SQL. 842 | * 843 | * @param \Database\Query\Builder $query 844 | * @param array $values 845 | * @return string 846 | */ 847 | public function compileInsertIgnore(Builder $query, array $values) 848 | { 849 | $this->throwUnsupportedGrammarException("Insert ignore"); 850 | } 851 | 852 | /** 853 | * Compile an insert statement into SQL. 854 | * 855 | * @param Builder $query 856 | * @param array $values 857 | * @param array $updateValues 858 | * @throws UnsupportedGrammarException 859 | */ 860 | public function compileInsertOnDuplicateKeyUpdate(Builder $query, array $values, array $updateValues) 861 | { 862 | $this->throwUnsupportedGrammarException("Insert on duplicate key update"); 863 | } 864 | 865 | /** 866 | * Compile a replace statement into SQL. 867 | * 868 | * @param Builder $query 869 | * @param array $values 870 | * @throws UnsupportedGrammarException 871 | */ 872 | public function compileReplace(Builder $query, array $values) 873 | { 874 | $this->throwUnsupportedGrammarException("Replace"); 875 | } 876 | 877 | /** 878 | * Compile an insert and get ID statement into SQL. 879 | * 880 | * @param \Database\Query\Builder $query 881 | * @param array $values 882 | * @param string $sequence 883 | * @return string 884 | */ 885 | public function compileInsertGetId(Builder $query, $values, $sequence) 886 | { 887 | return $this->compileInsert($query, $values); 888 | } 889 | 890 | /** 891 | * Compile an update statement into SQL. 892 | * 893 | * @param \Database\Query\Builder $query 894 | * @param array $values 895 | * @return string 896 | */ 897 | public function compileUpdate(Builder $query, $values) 898 | { 899 | $table = $this->wrapTable($query->from); 900 | 901 | $columns = $this->getUpdateColumns($values); 902 | 903 | // If the query has any "join" clauses, we will setup the joins on the builder 904 | // and compile them so we can attach them to this update, as update queries 905 | // can get join statements to attach to other tables when they're needed. 906 | if (isset($query->joins)) { 907 | $joins = ' ' . $this->compileJoins($query, $query->joins); 908 | } else { 909 | $joins = ''; 910 | } 911 | 912 | // Of course, update queries may also be constrained by where clauses so we'll 913 | // need to compile the where clauses and attach it to the query so only the 914 | // intended records are updated by the SQL statements we generate to run. 915 | $where = $this->compileWheres($query); 916 | 917 | return trim("update {$table}{$joins} set $columns $where"); 918 | } 919 | 920 | /** 921 | * Build an update spec from an array. 922 | * 923 | * E.G.: `col1` = ?, `col2` = ?, `col3` = `col3` + 1 924 | * 925 | * @param $values 926 | * @return string 927 | */ 928 | protected function getUpdateColumns($values) 929 | { 930 | // Each one of the columns in the update statements needs to be wrapped in the 931 | // keyword identifiers, also a place-holder needs to be created for each of 932 | // the values in the list of bindings so we can make the sets statements. 933 | $columns = array(); 934 | 935 | foreach ($values as $key => $value) { 936 | $columns[] = $this->wrap($key) . ' = ' . $this->parameter($value); 937 | } 938 | 939 | return implode(', ', $columns); 940 | } 941 | 942 | /** 943 | * Compile a delete statement into SQL. 944 | * 945 | * @param \Database\Query\Builder $query 946 | * @return string 947 | */ 948 | public function compileDelete(Builder $query) 949 | { 950 | $table = $this->wrapTable($query->from); 951 | 952 | $where = is_array($query->wheres) ? $this->compileWheres($query) : ''; 953 | 954 | return trim("delete from $table " . $where); 955 | } 956 | 957 | /** 958 | * Compile a truncate table statement into SQL. 959 | * 960 | * @param \Database\Query\Builder $query 961 | * @return array 962 | */ 963 | public function compileTruncate(Builder $query) 964 | { 965 | return array('truncate ' . $this->wrapTable($query->from) => array()); 966 | } 967 | 968 | /** 969 | * Compile the lock into SQL. 970 | * 971 | * @param \Database\Query\Builder $query 972 | * @param bool|string $value 973 | * @return string 974 | */ 975 | protected function compileLock(Builder $query, $value) 976 | { 977 | return is_string($value) ? $value : ''; 978 | } 979 | 980 | /** 981 | * Concatenate an array of segments, removing empties. 982 | * 983 | * @param array $segments 984 | * @return string 985 | */ 986 | protected function concatenate($segments) 987 | { 988 | return implode(' ', array_filter($segments, function ($value) { 989 | return (string)$value !== ''; 990 | })); 991 | } 992 | 993 | /** 994 | * Remove the leading boolean from a statement. 995 | * 996 | * @param string $value 997 | * @return string 998 | */ 999 | protected function removeLeadingBoolean($value) 1000 | { 1001 | return preg_replace('/and |or /', '', $value, 1); 1002 | } 1003 | 1004 | /** 1005 | * @param $grammarDescription 1006 | * @throws UnsupportedGrammarException 1007 | */ 1008 | private function throwUnsupportedGrammarException($grammarDescription) 1009 | { 1010 | throw new UnsupportedGrammarException("$grammarDescription is not supported by the " . get_called_class() . " grammar driver"); 1011 | } 1012 | 1013 | } 1014 | --------------------------------------------------------------------------------