├── phpunit.php ├── CONTRIBUTING.md ├── src ├── Exception.php ├── Pgsql │ ├── Select.php │ ├── DeleteBuilder.php │ ├── InsertBuilder.php │ ├── UpdateBuilder.php │ ├── Delete.php │ ├── Update.php │ ├── ReturningInterface.php │ ├── BuildReturningTrait.php │ ├── ReturningTrait.php │ └── Insert.php ├── Sqlite │ ├── Select.php │ ├── Delete.php │ ├── Insert.php │ └── Update.php ├── Sqlsrv │ ├── Delete.php │ ├── Insert.php │ ├── Update.php │ ├── Select.php │ ├── Quoter.php │ └── SelectBuilder.php ├── Common │ ├── OrderByInterface.php │ ├── DeleteBuilder.php │ ├── DeleteInterface.php │ ├── UpdateInterface.php │ ├── LimitInterface.php │ ├── LimitOffsetInterface.php │ ├── LimitTrait.php │ ├── LimitOffsetTrait.php │ ├── UpdateBuilder.php │ ├── WhereInterface.php │ ├── Delete.php │ ├── WhereTrait.php │ ├── ValuesInterface.php │ ├── InsertBuilder.php │ ├── QuoterInterface.php │ ├── InsertInterface.php │ ├── Update.php │ ├── SelectBuilder.php │ ├── AbstractBuilder.php │ ├── Quoter.php │ ├── SelectInterface.php │ ├── Insert.php │ └── Select.php ├── Mysql │ ├── Quoter.php │ ├── InsertBuilder.php │ ├── Update.php │ ├── Delete.php │ ├── Select.php │ └── Insert.php ├── QueryInterface.php ├── AbstractDmlQuery.php ├── QueryFactory.php └── AbstractQuery.php ├── docs ├── index.md ├── sqlsrv.md ├── delete.md ├── sqlite.md ├── other.md ├── pgsql.md ├── instantiation.md ├── update.md ├── mysql.md ├── insert.md └── select.md ├── LICENSE ├── autoload.php ├── composer.json ├── .github └── workflows │ └── continuous-integration.yml ├── README.md └── CHANGELOG.md /phpunit.php: -------------------------------------------------------------------------------- 1 | . 4 | 5 | The time between submitting a contribution and its review one may be extensive; do not be discouraged if there is not immediate feedback. 6 | 7 | Thanks! 8 | -------------------------------------------------------------------------------- /src/Exception.php: -------------------------------------------------------------------------------- 1 | builder->applyLimit(parent::build(), $this->getLimit(), $this->offset); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Pgsql/Delete.php: -------------------------------------------------------------------------------- 1 | builder->buildReturning($this->returning); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Pgsql/Update.php: -------------------------------------------------------------------------------- 1 | builder->buildReturning($this->returning); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Common/LimitInterface.php: -------------------------------------------------------------------------------- 1 | indentCsv($returning); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Common/LimitTrait.php: -------------------------------------------------------------------------------- 1 | limit = (int) $limit; 41 | return $this; 42 | } 43 | 44 | /** 45 | * 46 | * Returns the LIMIT value. 47 | * 48 | * @return int 49 | * 50 | */ 51 | public function getLimit() 52 | { 53 | return $this->limit; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /docs/delete.md: -------------------------------------------------------------------------------- 1 | # DELETE 2 | 3 | Build a _Delete_ query using the following methods. They do not need to 4 | be called in any particular order, and may be called multiple times. 5 | 6 | ```php 7 | $delete = $queryFactory->newDelete(); 8 | 9 | $delete 10 | ->from('foo') // FROM this table 11 | ->where('zim = :zim') // AND WHERE these conditions 12 | ->orWhere('gir = :gir') // OR WHERE these conditions 13 | ->bindValue('bar', 'bar_val') // bind one value to a placeholder 14 | ->bindValues([ // bind these values to the query 15 | 'baz' => 99, 16 | 'zim' => 'dib', 17 | 'gir' => 'doom', 18 | ]); 19 | ``` 20 | 21 | Once you have built the query, pass it to the database connection of your 22 | choice as a string, and send the bound values along with it. 23 | 24 | ```php 25 | // the PDO connection 26 | $pdo = new PDO(...); 27 | 28 | // prepare the statement 29 | $sth = $pdo->prepare($delete->getStatement()) 30 | 31 | // execute with bound values 32 | $sth->execute($delete->getBindValues()); 33 | ``` 34 | -------------------------------------------------------------------------------- /src/Pgsql/ReturningTrait.php: -------------------------------------------------------------------------------- 1 | returning[] = $this->quoter->quoteNamesIn($col); 45 | } 46 | return $this; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Common/LimitOffsetTrait.php: -------------------------------------------------------------------------------- 1 | offset = (int) $offset; 43 | return $this; 44 | } 45 | 46 | /** 47 | * 48 | * Returns the OFFSET value. 49 | * 50 | * @return int 51 | * 52 | */ 53 | public function getOffset() 54 | { 55 | return $this->offset; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Sqlite/Delete.php: -------------------------------------------------------------------------------- 1 | builder->buildLimitOffset($this->getLimit(), $this->offset); 35 | } 36 | 37 | /** 38 | * 39 | * Adds a column order to the query. 40 | * 41 | * @param array $spec The columns and direction to order by. 42 | * 43 | * @return $this 44 | * 45 | */ 46 | public function orderBy(array $spec) 47 | { 48 | return $this->addOrderBy($spec); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016-2022, Aura for PHP 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/Mysql/InsertBuilder.php: -------------------------------------------------------------------------------- 1 | $row) { 40 | $values[] = $this->indent(array($key . ' = ' . $row)); 41 | } 42 | 43 | return ' ON DUPLICATE KEY UPDATE' 44 | . implode (',', $values); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Common/UpdateBuilder.php: -------------------------------------------------------------------------------- 1 | $value) { 47 | $values[] = "{$col} = {$value}"; 48 | } 49 | return PHP_EOL . 'SET' . $this->indentCsv($values); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /autoload.php: -------------------------------------------------------------------------------- 1 | array( 10 | __DIR__ . '/src', 11 | __DIR__ . '/tests', 12 | ), 13 | ); 14 | 15 | // go through the prefixes 16 | foreach ($prefixes as $prefix => $dirs) { 17 | 18 | // does the requested class match the namespace prefix? 19 | $prefix_len = strlen($prefix); 20 | if (substr($class, 0, $prefix_len) !== $prefix) { 21 | continue; 22 | } 23 | 24 | // strip the prefix off the class 25 | $class = substr($class, $prefix_len); 26 | 27 | // a partial filename 28 | $part = str_replace('\\', DIRECTORY_SEPARATOR, $class) . '.php'; 29 | 30 | // go through the directories to find classes 31 | foreach ($dirs as $dir) { 32 | $dir = str_replace('/', DIRECTORY_SEPARATOR, $dir); 33 | $file = $dir . DIRECTORY_SEPARATOR . $part; 34 | if (is_readable($file)) { 35 | require $file; 36 | return; 37 | } 38 | } 39 | } 40 | 41 | }); 42 | -------------------------------------------------------------------------------- /docs/sqlite.md: -------------------------------------------------------------------------------- 1 | # SQLite Additions 2 | 3 | These 'sqlite' query objects have additional SQLite-specific behaviors. 4 | 5 | ## INSERT 6 | 7 | - `orAbort()` to add or remove an `OR ABORT` flag 8 | - `orFail()` to add or remove an `OR FAIL` flag 9 | - `orIgnore()` to add or remove an `OR IGNORE` flag 10 | - `orReplace()` to add or remove an `OR REPLACE` flag 11 | - `orRollback()` to add or remove an `OR ROLLBACK` flag 12 | 13 | ## UPDATE 14 | 15 | - `orAbort()` to add or remove an `OR ABORT` flag 16 | - `orFail()` to add or remove an `OR FAIL` flag 17 | - `orIgnore()` to add or remove an `OR IGNORE` flag 18 | - `orReplace()` to add or remove an `OR REPLACE` flag 19 | - `orRollback()` to add or remove an `OR ROLLBACK` flag 20 | - `orderBy()` to add an ORDER BY clause 21 | - `limit()` to set a LIMIT count 22 | - `offset()` to set an OFFSET count 23 | 24 | ## DELETE 25 | 26 | - `orAbort()` to add or remove an `OR ABORT` flag 27 | - `orFail()` to add or remove an `OR FAIL` flag 28 | - `orIgnore()` to add or remove an `OR IGNORE` flag 29 | - `orReplace()` to add or remove an `OR REPLACE` flag 30 | - `orRollback()` to add or remove an `OR ROLLBACK` flag 31 | - `orderBy()` to add an ORDER BY clause 32 | - `limit()` to set a LIMIT count 33 | - `offset()` to set an OFFSET count 34 | -------------------------------------------------------------------------------- /src/Pgsql/Insert.php: -------------------------------------------------------------------------------- 1 | builder->buildReturning($this->returning); 35 | } 36 | 37 | /** 38 | * 39 | * Returns the proper name for passing to `PDO::lastInsertId()`. 40 | * 41 | * @param string $col The last insert ID column. 42 | * 43 | * @return string The sequence name "{$into_table}_{$col}_seq", or the 44 | * value from `$last_insert_id_names`. 45 | * 46 | */ 47 | public function getLastInsertIdName($col) 48 | { 49 | $name = parent::getLastInsertIdName($col); 50 | if (! $name) { 51 | $name = "{$this->into_raw}_{$col}_seq"; 52 | } 53 | return $name; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /docs/other.md: -------------------------------------------------------------------------------- 1 | ## Identifier Quoting 2 | 3 | In most cases, the query objects will quote identifiers for you. For example, 4 | under the common _Select_ object with double-quotes for identifiers: 5 | 6 | ```php 7 | $select->cols(['foo', 'bar AS barbar']) 8 | ->from('table1') 9 | ->from('table2') 10 | ->where('table2.zim = 99'); 11 | 12 | echo $select->getStatement(); 13 | // SELECT 14 | // "foo", 15 | // "bar" AS "barbar" 16 | // FROM 17 | // "table1", 18 | // "table2" 19 | // WHERE 20 | // "table2"."zim" = 99 21 | 22 | ``` 23 | 24 | If you discover that a partially-qualified identifier has not been auto-quoted 25 | for you, change it to a fully-qualified identifier (e.g., from `col_name` to 26 | `table_name.col_name`). 27 | 28 | ## Table Prefixes 29 | 30 | One frequently-requested feature for this package is support for "automatic 31 | table prefixes" on all queries. This feature sounds great in theory, but in 32 | practice is it (1) difficult to implement well, and (2) even when implemented it 33 | turns out to be not as great as it seems in theory. This assessment is the 34 | result of the hard trials of experience. For those of you who want modifiable 35 | table prefixes, we suggest using constants with your table names prefixed as 36 | desired; as the prefixes change, you can then change your constants. 37 | -------------------------------------------------------------------------------- /src/Common/WhereInterface.php: -------------------------------------------------------------------------------- 1 | from = $this->quoter->quoteName($from); 45 | return $this; 46 | } 47 | 48 | /** 49 | * 50 | * Builds this query object into a string. 51 | * 52 | * @return string 53 | * 54 | */ 55 | protected function build() 56 | { 57 | return 'DELETE' 58 | . $this->builder->buildFlags($this->flags) 59 | . $this->builder->buildFrom($this->from) 60 | . $this->builder->buildWhere($this->where) 61 | . $this->builder->buildOrderBy($this->order_by); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Common/WhereTrait.php: -------------------------------------------------------------------------------- 1 | addClauseCondWithBind('where', 'AND', $cond, $bind); 34 | return $this; 35 | } 36 | 37 | /** 38 | * 39 | * Adds a WHERE condition to the query by OR. If the condition has 40 | * ?-placeholders, additional arguments to the method will be bound to 41 | * those placeholders sequentially. 42 | * 43 | * @param string $cond The WHERE condition. 44 | * 45 | * @param array $bind Values to be bound to placeholders 46 | * 47 | * @return $this 48 | * 49 | * @see where() 50 | * 51 | */ 52 | public function orWhere($cond, array $bind = []) 53 | { 54 | $this->addClauseCondWithBind('where', 'OR', $cond, $bind); 55 | return $this; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /docs/pgsql.md: -------------------------------------------------------------------------------- 1 | # PostgreSQL Additions 2 | 3 | These 'pgsql' query objects have additional PostgreSQL-specific behaviors. 4 | 5 | ## INSERT 6 | 7 | - `returning()` to add a `RETURNING` clause 8 | 9 | ### Last Insert IDs and Table Inheritance 10 | 11 | PostgreSQL determines the default sequence name for the last inserted ID by 12 | concatenating the table name, the column name, and a `seq` suffix, using 13 | underscore separators (e.g. `table_col_seq`). 14 | 15 | However, when inserting into an extended or inherited table, the parent table is 16 | used for the sequence name, not the child (insertion) table. This package allows 17 | you to override the default last-insert-id name with the method 18 | `setLastInsertIdNames()` on both _QueryFactory_ and the _Insert_ object itself. 19 | Pass an array of `inserttable.col` keys mapped to `parenttable_col_seq` values, 20 | and the _Insert_ object will use the mapped sequence names instead of the 21 | default names. 22 | 23 | ```php 24 | $queryFactory->setLastInsertIdNames([ 25 | 'child.id' => 'parent_id_seq' 26 | ]); 27 | 28 | $insert = $queryFactory->newInsert(); 29 | $insert->into('child'); 30 | // ... 31 | $seq = $insert->getLastInsertIdName('id'); 32 | ``` 33 | 34 | The `$seq` name is now `parent_id_seq`, not `child_id_seq` as it would have been 35 | by default. 36 | 37 | ## UPDATE 38 | 39 | - `returning()` to add a `RETURNING` clause 40 | 41 | ## DELETE 42 | 43 | - `returning()` to add a `RETURNING` clause 44 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aura/sqlquery", 3 | "type": "library", 4 | "description": "Object-oriented query builders for MySQL, Postgres, SQLite, and SQLServer; can be used with any database connection library.", 5 | "keywords": [ 6 | "mysql", 7 | "pdo", 8 | "pgsql", 9 | "postgres", 10 | "postgresql", 11 | "sqlite", 12 | "sql server", 13 | "sqlserver", 14 | "query", 15 | "select", 16 | "insert", 17 | "update", 18 | "delete", 19 | "db", 20 | "database", 21 | "sql", 22 | "dml" 23 | ], 24 | "homepage": "https://github.com/auraphp/Aura.SqlQuery", 25 | "license": "MIT", 26 | "authors": [ 27 | { 28 | "name": "Aura.SqlQuery Contributors", 29 | "homepage": "https://github.com/auraphp/Aura.SqlQuery/contributors" 30 | } 31 | ], 32 | "require": { 33 | "php": ">=5.6" 34 | }, 35 | "require-dev": { 36 | "ext-pdo_sqlite": "*", 37 | "yoast/phpunit-polyfills": "^1.0" 38 | }, 39 | "autoload": { 40 | "psr-4": { 41 | "Aura\\SqlQuery\\": "src/" 42 | } 43 | }, 44 | "autoload-dev": { 45 | "psr-4": { 46 | "Aura\\SqlQuery\\": "tests/" 47 | } 48 | }, 49 | "suggest": { 50 | "aura/sql": "Provides an extension to the native PDO along with a profiler and connection locator." 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Common/ValuesInterface.php: -------------------------------------------------------------------------------- 1 | newSelect(); 15 | $insert = $queryFactory->newInsert(); 16 | $update = $queryFactory->newUpdate(); 17 | $delete = $queryFactory->newDelete(); 18 | ``` 19 | 20 | Although you must specify a database type when instantiating a _QueryFactory_, 21 | you can tell the factory to return "common" query objects instead of database- 22 | specific ones. This will make only the common query methods available, which 23 | helps with writing database-portable applications. To do so, pass the constant 24 | `QueryFactory::COMMON` as the second constructor parameter. 25 | 26 | ```php 27 | use Aura\SqlQuery\QueryFactory; 28 | 29 | // return Common, not SQLite-specific, query objects 30 | $queryFactory = new QueryFactory('sqlite', QueryFactory::COMMON); 31 | ``` 32 | 33 | > N.b. You still need to pass a database type so that identifiers can be 34 | > quoted appropriately. 35 | 36 | All query objects implement the "Common" methods. 37 | 38 | The query objects do not execute queries against a database. When you are done 39 | building the query, you will need to pass it to a database connection of your 40 | choice. In later examples, we will use [PDO](http://php.net/pdo) for the 41 | database connection, but any database library that uses named placeholders and 42 | bound values should work just as well (e.g. the [Aura.Sql][] _ExtendedPdo_ 43 | class). 44 | 45 | [Aura.Sql]: https://github.com/auraphp/Aura.Sql 46 | 47 | -------------------------------------------------------------------------------- /.github/workflows/continuous-integration.yml: -------------------------------------------------------------------------------- 1 | name: Continuous Integration 2 | 3 | on: 4 | push: 5 | pull_request: 6 | workflow_dispatch: 7 | 8 | jobs: 9 | build: 10 | 11 | runs-on: ubuntu-latest 12 | 13 | strategy: 14 | matrix: 15 | operating-system: 16 | - ubuntu-latest 17 | php-version: 18 | - '5.6' 19 | - '7.0' 20 | - '7.1' 21 | - '7.2' 22 | - '7.3' 23 | - '7.4' 24 | - '8.0' 25 | - '8.1' 26 | - '8.2' 27 | - '8.3' 28 | steps: 29 | - name: Checkout 30 | uses: actions/checkout@v4 31 | 32 | - name: Setup PHP ${{ matrix.php-version }} 33 | uses: shivammathur/setup-php@v2 34 | with: 35 | php-version: ${{ matrix.php-version }} 36 | coverage: xdebug 37 | tools: composer:2.2 38 | ini-values: assert.exception=1, zend.assertions=1 39 | 40 | - name: Get composer cache directory 41 | id: composer-cache 42 | run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT 43 | 44 | - name: Cache dependencies 45 | uses: actions/cache@v4 46 | with: 47 | path: ${{ steps.composer-cache.outputs.dir }} 48 | key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} 49 | restore-keys: ${{ runner.os }}-composer- 50 | 51 | - name: Install dependencies 52 | run: composer install --no-interaction --prefer-dist 53 | 54 | - name: Run test suite 55 | run: php -d xdebug.mode=coverage ./vendor/bin/phpunit --coverage-clover=coverage.xml 56 | 57 | - name: Upload coverage report 58 | uses: codecov/codecov-action@v4 59 | with: 60 | fail_ci_if_error: false 61 | -------------------------------------------------------------------------------- /src/Mysql/Update.php: -------------------------------------------------------------------------------- 1 | builder->buildLimit($this->getLimit()); 35 | } 36 | 37 | /** 38 | * 39 | * Adds or removes LOW_PRIORITY flag. 40 | * 41 | * @param bool $enable Set or unset flag (default true). 42 | * 43 | * @return $this 44 | * 45 | */ 46 | public function lowPriority($enable = true) 47 | { 48 | $this->setFlag('LOW_PRIORITY', $enable); 49 | return $this; 50 | } 51 | 52 | /** 53 | * 54 | * Adds or removes IGNORE flag. 55 | * 56 | * @param bool $enable Set or unset flag (default true). 57 | * 58 | * @return $this 59 | * 60 | */ 61 | public function ignore($enable = true) 62 | { 63 | $this->setFlag('IGNORE', $enable); 64 | return $this; 65 | } 66 | 67 | /** 68 | * 69 | * Adds a column order to the query. 70 | * 71 | * @param array $spec The columns and direction to order by. 72 | * 73 | * @return $this 74 | * 75 | */ 76 | public function orderBy(array $spec) 77 | { 78 | return $this->addOrderBy($spec); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /docs/update.md: -------------------------------------------------------------------------------- 1 | # UPDATE 2 | 3 | Build an _Update_ query using the following methods. They do not need to 4 | be called in any particular order, and may be called multiple times. 5 | 6 | ```php 7 | $update = $queryFactory->newUpdate(); 8 | 9 | $update 10 | ->table('foo') // update this table 11 | ->cols([ // bind values as "SET bar = :bar" 12 | 'bar', 13 | 'baz', 14 | ]) 15 | ->set('ts', 'NOW()') // raw value as "(ts) VALUES (NOW())" 16 | ->where('zim = :zim') // AND WHERE these conditions 17 | ->where('gir = :gir', ['gir' => 'gir_val']) // bind this value to the condition 18 | ->orWhere('gir = :gir') // OR WHERE these conditions 19 | ->bindValue('bar', 'bar_val') // bind one value to a placeholder 20 | ->bindValues([ // bind these values to the query 21 | 'baz' => 99, 22 | 'zim' => 'dib', 23 | 'gir' => 'doom', 24 | ]); 25 | ``` 26 | 27 | The `cols()` method allows you to pass an array of key-value pairs where the 28 | key is the column name and the value is a bind value (not a raw value): 29 | 30 | ```php 31 | $update = $queryFactory->newUpdate(); 32 | 33 | $update->table('foo') // update this table 34 | ->cols([ // update these columns and bind these values 35 | 'foo' => 'foo_value', 36 | 'bar' => 'bar_value', 37 | 'baz' => 'baz_value', 38 | ]); 39 | ``` 40 | 41 | Once you have built the query, pass it to the database connection of your 42 | choice as a string, and send the bound values along with it. 43 | 44 | ```php 45 | // the PDO connection 46 | $pdo = new PDO(...); 47 | 48 | // prepare the statement 49 | $sth = $pdo->prepare($update->getStatement()) 50 | 51 | // execute with bound values 52 | $sth->execute($update->getBindValues()); 53 | ``` 54 | -------------------------------------------------------------------------------- /docs/mysql.md: -------------------------------------------------------------------------------- 1 | # MySQL Additions 2 | 3 | These 'mysql' query objects have additional MySQL-specific behaviors. 4 | 5 | ## SELECT 6 | 7 | - `calcFoundRows()` to add or remove `SQL_CALC_FOUND_ROWS` flag 8 | - `cache()` to add or remove `SQL_CACHE` flag 9 | - `noCache()` to add or remove `SQL_NO_CACHE` flag 10 | - `bigResult()` to add or remove `SQL_BIG_RESULT` flag 11 | - `smallResult()` to add or remove `SQL_SMALL_RESULT` flag 12 | - `bufferResult()` to add or remove `SQL_BUFFER_RESULT` flag 13 | - `highPriority()` to add or remove `HIGH_PRIORITY` flag 14 | - `straightJoin()` to add or remove `STRAIGHT_JOIN` flag 15 | 16 | ## INSERT 17 | 18 | - `orReplace()` to add or remove `OR REPLACE` 19 | - `highPriority()` to add or remove `HIGH_PRIORITY` flag 20 | - `lowPriority()` to add or remove `LOW_PRIORITY` flag 21 | - `ignore()` to add or remove `IGNORE` flag 22 | - `delayed()` to add or remove `DELAYED` flag 23 | 24 | In addition, the MySQL _Insert_ object has support for `ON DUPLICATE KEY UPDATE`: 25 | 26 | - `onDuplicateKeyUpdate($col, $raw_value)` sets a raw value 27 | - `onDuplicateKeyUpateCol($col, $value)` is a `col()` equivalent for the update 28 | - `onDuplicateKeyUpdateCols($cols)` is a `cols()`equivalent for the update 29 | 30 | Placeholders for bound values in the `ON DUPLICATE KEY UPDATE` portions will be 31 | automatically suffixed with `__on_duplicate key` to deconflict them from the 32 | insert placeholders. 33 | 34 | ## UPDATE 35 | 36 | - `lowPriority()` to add or remove `LOW_PRIORITY` flag 37 | - `ignore()` to add or remove `IGNORE` flag 38 | - `where()` and `orWhere()` to add WHERE conditions flag 39 | - `orderBy()` to add an ORDER BY clause flag 40 | - `limit()` to set a LIMIT count 41 | 42 | ## DELETE 43 | 44 | - `lowPriority()` to add or remove `LOW_PRIORITY` flag 45 | - `ignore()` to add or remove `IGNORE` flag 46 | - `quick()` to add or remove `QUICK` flag 47 | - `orderBy()` to add an ORDER BY clause 48 | - `limit()` to set a LIMIT count 49 | -------------------------------------------------------------------------------- /src/Sqlsrv/SelectBuilder.php: -------------------------------------------------------------------------------- 1 | indentCsv(array_keys($col_values)) 48 | . PHP_EOL . ') VALUES (' 49 | . $this->indentCsv(array_values($col_values)) 50 | . PHP_EOL . ')'; 51 | } 52 | 53 | /** 54 | * 55 | * Builds the bulk-inserted columns and values of the statement. 56 | * 57 | * @param array $col_order The column names to insert, in order. 58 | * 59 | * @param array $col_values_bulk The bulk-insert values, in the same order 60 | * the column names. 61 | * 62 | * @return string 63 | * 64 | */ 65 | public function buildValuesForBulkInsert(array $col_order, array $col_values_bulk) 66 | { 67 | $cols = " (" . implode(', ', $col_order) . ")"; 68 | $vals = array(); 69 | foreach ($col_values_bulk as $row_values) { 70 | $vals[] = " (" . implode(', ', $row_values) . ")"; 71 | } 72 | return PHP_EOL . $cols . PHP_EOL 73 | . "VALUES" . PHP_EOL 74 | . implode("," . PHP_EOL, $vals); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/QueryInterface.php: -------------------------------------------------------------------------------- 1 | builder->buildLimit($this->getLimit()); 35 | } 36 | 37 | /** 38 | * 39 | * Adds or removes LOW_PRIORITY flag. 40 | * 41 | * @param bool $enable Set or unset flag (default true). 42 | * 43 | * @return $this 44 | * 45 | */ 46 | public function lowPriority($enable = true) 47 | { 48 | $this->setFlag('LOW_PRIORITY', $enable); 49 | return $this; 50 | } 51 | 52 | /** 53 | * 54 | * Adds or removes IGNORE flag. 55 | * 56 | * @param bool $enable Set or unset flag (default true). 57 | * 58 | * @return $this 59 | * 60 | */ 61 | public function ignore($enable = true) 62 | { 63 | $this->setFlag('IGNORE', $enable); 64 | return $this; 65 | } 66 | 67 | /** 68 | * 69 | * Adds or removes QUICK flag. 70 | * 71 | * @param bool $enable Set or unset flag (default true). 72 | * 73 | * @return $this 74 | * 75 | */ 76 | public function quick($enable = true) 77 | { 78 | $this->setFlag('QUICK', $enable); 79 | return $this; 80 | } 81 | 82 | /** 83 | * 84 | * Adds a column order to the query. 85 | * 86 | * @param array $spec The columns and direction to order by. 87 | * 88 | * @return $this 89 | * 90 | */ 91 | public function orderBy(array $spec) 92 | { 93 | return $this->addOrderBy($spec); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/Common/QuoterInterface.php: -------------------------------------------------------------------------------- 1 | setFlag('OR ABORT', $enable); 34 | return $this; 35 | } 36 | 37 | /** 38 | * 39 | * Adds or removes OR FAIL flag. 40 | * 41 | * @param bool $enable Set or unset flag (default true). 42 | * 43 | * @return $this 44 | * 45 | */ 46 | public function orFail($enable = true) 47 | { 48 | $this->setFlag('OR FAIL', $enable); 49 | return $this; 50 | } 51 | 52 | /** 53 | * 54 | * Adds or removes OR IGNORE flag. 55 | * 56 | * @deprecated use ignore instead 57 | * @param bool $enable Set or unset flag (default true). 58 | * 59 | * @return $this 60 | * 61 | */ 62 | public function orIgnore($enable = true) 63 | { 64 | $this->ignore($enable); 65 | return $this; 66 | } 67 | 68 | /** 69 | * 70 | * Adds or removes OR IGNORE flag. 71 | * 72 | * @param bool $enable Set or unset flag (default true). 73 | * 74 | * @return $this 75 | * 76 | */ 77 | public function ignore($enable = true) 78 | { 79 | $this->setFlag('OR IGNORE', $enable); 80 | return $this; 81 | } 82 | 83 | /** 84 | * 85 | * Adds or removes OR REPLACE flag. 86 | * 87 | * @param bool $enable Set or unset flag (default true). 88 | * 89 | * @return $this 90 | * 91 | */ 92 | public function orReplace($enable = true) 93 | { 94 | $this->setFlag('OR REPLACE', $enable); 95 | return $this; 96 | } 97 | 98 | /** 99 | * 100 | * Adds or removes OR ROLLBACK flag. 101 | * 102 | * @param bool $enable Set or unset flag (default true). 103 | * 104 | * @return $this 105 | * 106 | */ 107 | public function orRollback($enable = true) 108 | { 109 | $this->setFlag('OR ROLLBACK', $enable); 110 | return $this; 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Aura.SqlQuery 2 | 3 | Provides query builders for MySQL, Postgres, SQLite, and Microsoft SQL Server. 4 | These builders are independent of any particular database connection library, 5 | although [PDO](http://php.net/PDO) in general is recommended. 6 | 7 | ## Installation and Autoloading 8 | 9 | This package is installable and PSR-4 autoloadable via Composer as 10 | [aura/sqlquery][]. 11 | 12 | Alternatively, [download a release][], or clone this repository, then map 13 | the `Aura\SqlQuery\` namespace to the package `src/` directory. 14 | 15 | 16 | ## Dependencies 17 | 18 | This package requires PHP 5.6 or later; it has been tested on PHP 5.6-8.1. We recommend using the latest available version of PHP as a matter of principle. 19 | 20 | Aura library packages may sometimes depend on external interfaces, but never on 21 | external implementations. This allows compliance with community standards 22 | without compromising flexibility. For specifics, please examine the package 23 | [composer.json][] file. 24 | 25 | ## Quality 26 | 27 | [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/auraphp/Aura.SqlQuery/badges/quality-score.png?b=3.x)](https://scrutinizer-ci.com/g/auraphp/Aura.SqlQuery/) 28 | [![codecov](https://codecov.io/gh/auraphp/Aura.SqlQuery/branch/3.x/graph/badge.svg?token=UASDouLxyc)](https://codecov.io/gh/auraphp/Aura.SqlQuery) 29 | [![Continuous Integration](https://github.com/auraphp/Aura.SqlQuery/actions/workflows/continuous-integration.yml/badge.svg?branch=3.x)](https://github.com/auraphp/Aura.SqlQuery/actions/workflows/continuous-integration.yml) 30 | 31 | This project adheres to [Semantic Versioning](http://semver.org/). 32 | 33 | To run the unit tests at the command line, issue `composer install` and then 34 | `./vendor/bin/phpunit` at the package root. This requires [Composer][] to be 35 | available as `composer`. 36 | 37 | This package attempts to comply with [PSR-1][], [PSR-2][], and [PSR-4][]. If 38 | you notice compliance oversights, please send a patch via pull request. 39 | 40 | ## Community 41 | 42 | To ask questions, provide feedback, or otherwise communicate with other Aura 43 | users, please join our [Google Group][], follow [@auraphp][], or chat with us 44 | on Freenode in the #auraphp channel. 45 | 46 | ## Documentation 47 | 48 | This package is fully documented [here](./docs/index.md). 49 | 50 | [PSR-1]: https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-1-basic-coding-standard.md 51 | [PSR-2]: https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md 52 | [PSR-4]: https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-4-autoloader.md 53 | [Composer]: http://getcomposer.org/ 54 | [PHPUnit]: http://phpunit.de/ 55 | [Google Group]: http://groups.google.com/group/auraphp 56 | [@auraphp]: http://twitter.com/auraphp 57 | [download a release]: https://github.com/auraphp/Aura.SqlQuery/releases 58 | [aura/sqlquery]: https://packagist.org/packages/aura/sqlquery 59 | [composer.json]: ./composer.json 60 | -------------------------------------------------------------------------------- /src/Sqlite/Update.php: -------------------------------------------------------------------------------- 1 | builder->buildLimitOffset($this->getLimit(), $this->offset); 35 | } 36 | 37 | /** 38 | * 39 | * Adds or removes OR ABORT flag. 40 | * 41 | * @param bool $enable Set or unset flag (default true). 42 | * 43 | * @return $this 44 | * 45 | */ 46 | public function orAbort($enable = true) 47 | { 48 | $this->setFlag('OR ABORT', $enable); 49 | return $this; 50 | } 51 | 52 | /** 53 | * 54 | * Adds or removes OR FAIL flag. 55 | * 56 | * @param bool $enable Set or unset flag (default true). 57 | * 58 | * @return $this 59 | * 60 | */ 61 | public function orFail($enable = true) 62 | { 63 | $this->setFlag('OR FAIL', $enable); 64 | return $this; 65 | } 66 | 67 | /** 68 | * 69 | * Adds or removes OR IGNORE flag. 70 | * 71 | * @param bool $enable Set or unset flag (default true). 72 | * 73 | * @return $this 74 | * 75 | */ 76 | public function orIgnore($enable = true) 77 | { 78 | $this->setFlag('OR IGNORE', $enable); 79 | return $this; 80 | } 81 | 82 | /** 83 | * 84 | * Adds or removes OR REPLACE flag. 85 | * 86 | * @param bool $enable Set or unset flag (default true). 87 | * 88 | * @return $this 89 | * 90 | */ 91 | public function orReplace($enable = true) 92 | { 93 | $this->setFlag('OR REPLACE', $enable); 94 | return $this; 95 | } 96 | 97 | /** 98 | * 99 | * Adds or removes OR ROLLBACK flag. 100 | * 101 | * @param bool $enable Set or unset flag (default true). 102 | * 103 | * @return $this 104 | * 105 | */ 106 | public function orRollback($enable = true) 107 | { 108 | $this->setFlag('OR ROLLBACK', $enable); 109 | return $this; 110 | } 111 | 112 | /** 113 | * 114 | * Adds a column order to the query. 115 | * 116 | * @param array $spec The columns and direction to order by. 117 | * 118 | * @return $this 119 | * 120 | */ 121 | public function orderBy(array $spec) 122 | { 123 | return $this->addOrderBy($spec); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/Common/Update.php: -------------------------------------------------------------------------------- 1 | table = $this->quoter->quoteName($table); 45 | return $this; 46 | } 47 | 48 | /** 49 | * 50 | * Builds this query object into a string. 51 | * 52 | * @return string 53 | * 54 | */ 55 | protected function build() 56 | { 57 | return 'UPDATE' 58 | . $this->builder->buildFlags($this->flags) 59 | . $this->builder->buildTable($this->table) 60 | . $this->builder->buildValuesForUpdate($this->col_values) 61 | . $this->builder->buildWhere($this->where) 62 | . $this->builder->buildOrderBy($this->order_by); 63 | } 64 | 65 | /** 66 | * 67 | * Sets one column value placeholder; if an optional second parameter is 68 | * passed, that value is bound to the placeholder. 69 | * 70 | * @param string $col The column name. 71 | * 72 | * @param array $value 73 | * 74 | * @return $this 75 | */ 76 | public function col($col, ...$value) 77 | { 78 | return $this->addCol($col, ...$value); 79 | } 80 | 81 | /** 82 | * 83 | * Sets multiple column value placeholders. If an element is a key-value 84 | * pair, the key is treated as the column name and the value is bound to 85 | * that column. 86 | * 87 | * @param array $cols A list of column names, optionally as key-value 88 | * pairs where the key is a column name and the value is a bind value for 89 | * that column. 90 | * 91 | * @return $this 92 | * 93 | */ 94 | public function cols(array $cols) 95 | { 96 | return $this->addCols($cols); 97 | } 98 | 99 | /** 100 | * 101 | * Sets a column value directly; the value will not be escaped, although 102 | * fully-qualified identifiers in the value will be quoted. 103 | * 104 | * @param string $col The column name. 105 | * 106 | * @param string $value The column value expression. 107 | * 108 | * @return $this 109 | * 110 | */ 111 | public function set($col, $value) 112 | { 113 | return $this->setCol($col, $value); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/Common/SelectBuilder.php: -------------------------------------------------------------------------------- 1 | indentCsv($cols); 39 | } 40 | 41 | /** 42 | * 43 | * Builds the FROM clause. 44 | * 45 | * @param array $from The FROM elements. 46 | * 47 | * @param array $join The JOIN elements. 48 | * 49 | * @return string 50 | * 51 | */ 52 | public function buildFrom(array $from, array $join) 53 | { 54 | if (empty($from)) { 55 | return ''; // not applicable 56 | } 57 | 58 | $refs = array(); 59 | foreach ($from as $from_key => $from_val) { 60 | if (isset($join[$from_key])) { 61 | $from_val = array_merge($from_val, $join[$from_key]); 62 | } 63 | $refs[] = implode(PHP_EOL, $from_val); 64 | } 65 | return PHP_EOL . 'FROM' . $this->indentCsv($refs); 66 | } 67 | 68 | /** 69 | * 70 | * Builds the GROUP BY clause. 71 | * 72 | * @param array $group_by The GROUP BY elements. 73 | * 74 | * @return string 75 | * 76 | */ 77 | public function buildGroupBy(array $group_by) 78 | { 79 | if (empty($group_by)) { 80 | return ''; // not applicable 81 | } 82 | 83 | return PHP_EOL . 'GROUP BY' . $this->indentCsv($group_by); 84 | } 85 | 86 | /** 87 | * 88 | * Builds the HAVING clause. 89 | * 90 | * @param array $having The HAVING elements. 91 | * 92 | * @return string 93 | * 94 | */ 95 | public function buildHaving(array $having) 96 | { 97 | if (empty($having)) { 98 | return ''; // not applicable 99 | } 100 | 101 | return PHP_EOL . 'HAVING' . $this->indent($having); 102 | } 103 | 104 | /** 105 | * 106 | * Builds the FOR UPDATE portion of the SELECT. 107 | * 108 | * @param bool $for_update True if FOR UPDATE, false if not. 109 | * 110 | * @return string 111 | * 112 | */ 113 | public function buildForUpdate($for_update) 114 | { 115 | if (! $for_update) { 116 | return ''; // not applicable 117 | } 118 | 119 | return PHP_EOL . 'FOR UPDATE'; 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/AbstractDmlQuery.php: -------------------------------------------------------------------------------- 1 | col_values); 40 | } 41 | 42 | /** 43 | * 44 | * Sets one column value placeholder; if an optional second parameter is 45 | * passed, that value is bound to the placeholder. 46 | * 47 | * @param string $col The column name. 48 | * 49 | * @param array $value Value of the column 50 | * 51 | * @return $this 52 | * 53 | */ 54 | protected function addCol($col, ...$value) 55 | { 56 | $key = $this->quoter->quoteName($col); 57 | $this->col_values[$key] = ":$col"; 58 | if (count($value) > 0) { 59 | $this->bindValue($col, $value[0]); 60 | } 61 | return $this; 62 | } 63 | 64 | /** 65 | * 66 | * Sets multiple column value placeholders. If an element is a key-value 67 | * pair, the key is treated as the column name and the value is bound to 68 | * that column. 69 | * 70 | * @param array $cols A list of column names, optionally as key-value 71 | * pairs where the key is a column name and the value is a bind value for 72 | * that column. 73 | * 74 | * @return $this 75 | * 76 | */ 77 | protected function addCols(array $cols) 78 | { 79 | foreach ($cols as $key => $val) { 80 | if (is_int($key)) { 81 | // integer key means the value is the column name 82 | $this->addCol($val); 83 | } else { 84 | // the key is the column name and the value is a value to 85 | // be bound to that column 86 | $this->addCol($key, $val); 87 | } 88 | } 89 | return $this; 90 | } 91 | 92 | /** 93 | * 94 | * Sets a column value directly; the value will not be escaped, although 95 | * fully-qualified identifiers in the value will be quoted. 96 | * 97 | * @param string $col The column name. 98 | * 99 | * @param string $value The column value expression. 100 | * 101 | * @return $this 102 | * 103 | */ 104 | protected function setCol($col, $value) 105 | { 106 | if ($value === null) { 107 | $value = 'NULL'; 108 | } 109 | 110 | $key = $this->quoter->quoteName($col); 111 | $value = $this->quoter->quoteNamesIn($value); 112 | $this->col_values[$key] = $value; 113 | return $this; 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/Mysql/Select.php: -------------------------------------------------------------------------------- 1 | setFlag('SQL_CALC_FOUND_ROWS', $enable); 34 | return $this; 35 | } 36 | 37 | /** 38 | * 39 | * Adds or removes SQL_CACHE flag. 40 | * 41 | * @param bool $enable Set or unset flag (default true). 42 | * 43 | * @return $this 44 | * 45 | */ 46 | public function cache($enable = true) 47 | { 48 | $this->setFlag('SQL_CACHE', $enable); 49 | return $this; 50 | } 51 | 52 | /** 53 | * 54 | * Adds or removes SQL_NO_CACHE flag. 55 | * 56 | * @param bool $enable Set or unset flag (default true). 57 | * 58 | * @return $this 59 | * 60 | */ 61 | public function noCache($enable = true) 62 | { 63 | $this->setFlag('SQL_NO_CACHE', $enable); 64 | return $this; 65 | } 66 | 67 | /** 68 | * 69 | * Adds or removes STRAIGHT_JOIN flag. 70 | * 71 | * @param bool $enable Set or unset flag (default true). 72 | * 73 | * @return $this 74 | * 75 | */ 76 | public function straightJoin($enable = true) 77 | { 78 | $this->setFlag('STRAIGHT_JOIN', $enable); 79 | return $this; 80 | } 81 | 82 | /** 83 | * 84 | * Adds or removes HIGH_PRIORITY flag. 85 | * 86 | * @param bool $enable Set or unset flag (default true). 87 | * 88 | * @return $this 89 | * 90 | */ 91 | public function highPriority($enable = true) 92 | { 93 | $this->setFlag('HIGH_PRIORITY', $enable); 94 | return $this; 95 | } 96 | 97 | /** 98 | * 99 | * Adds or removes SQL_SMALL_RESULT flag. 100 | * 101 | * @param bool $enable Set or unset flag (default true). 102 | * 103 | * @return $this 104 | * 105 | */ 106 | public function smallResult($enable = true) 107 | { 108 | $this->setFlag('SQL_SMALL_RESULT', $enable); 109 | return $this; 110 | } 111 | 112 | /** 113 | * 114 | * Adds or removes SQL_BIG_RESULT flag. 115 | * 116 | * @param bool $enable Set or unset flag (default true). 117 | * 118 | * @return $this 119 | * 120 | */ 121 | public function bigResult($enable = true) 122 | { 123 | $this->setFlag('SQL_BIG_RESULT', $enable); 124 | return $this; 125 | } 126 | 127 | /** 128 | * 129 | * Adds or removes SQL_BUFFER_RESULT flag. 130 | * 131 | * @param bool $enable Set or unset flag (default true). 132 | * 133 | * @return $this 134 | * 135 | */ 136 | public function bufferResult($enable = true) 137 | { 138 | $this->setFlag('SQL_BUFFER_RESULT', $enable); 139 | return $this; 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/Common/AbstractBuilder.php: -------------------------------------------------------------------------------- 1 | indent($where); 56 | } 57 | 58 | /** 59 | * 60 | * Builds the `ORDER BY ...` clause of the statement. 61 | * 62 | * @param array $order_by The ORDER BY elements. 63 | * 64 | * @return string 65 | * 66 | */ 67 | public function buildOrderBy(array $order_by) 68 | { 69 | if (empty($order_by)) { 70 | return ''; // not applicable 71 | } 72 | 73 | return PHP_EOL . 'ORDER BY' . $this->indentCsv($order_by); 74 | } 75 | 76 | /** 77 | * 78 | * Builds the `LIMIT` clause of the statement. 79 | * 80 | * @param int $limit The LIMIT element. 81 | * 82 | * @return string 83 | * 84 | */ 85 | public function buildLimit($limit) 86 | { 87 | if (empty($limit)) { 88 | return ''; 89 | } 90 | return PHP_EOL . "LIMIT {$limit}"; 91 | } 92 | 93 | /** 94 | * 95 | * Builds the `LIMIT ... OFFSET` clause of the statement. 96 | * 97 | * @param int $limit The LIMIT element. 98 | * 99 | * @param int $offset The OFFSET element. 100 | * 101 | * @return string 102 | * 103 | */ 104 | public function buildLimitOffset($limit, $offset) 105 | { 106 | $clause = ''; 107 | 108 | if (!empty($limit)) { 109 | $clause .= "LIMIT {$limit}"; 110 | } 111 | 112 | if (!empty($offset)) { 113 | $clause .= " OFFSET {$offset}"; 114 | } 115 | 116 | if (!empty($clause)) { 117 | $clause = PHP_EOL . trim($clause); 118 | } 119 | 120 | return $clause; 121 | } 122 | 123 | /** 124 | * 125 | * Returns an array as an indented comma-separated values string. 126 | * 127 | * @param array $list The values to convert. 128 | * 129 | * @return string 130 | * 131 | */ 132 | public function indentCsv(array $list) 133 | { 134 | return PHP_EOL . ' ' 135 | . implode(',' . PHP_EOL . ' ', $list); 136 | } 137 | 138 | /** 139 | * 140 | * Returns an array as an indented string. 141 | * 142 | * @param array $list The values to convert. 143 | * 144 | * @return string 145 | * 146 | */ 147 | public function indent(array $list) 148 | { 149 | return PHP_EOL . ' ' 150 | . implode(PHP_EOL . ' ', $list); 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /docs/insert.md: -------------------------------------------------------------------------------- 1 | # INSERT 2 | 3 | ## Single-Row Insert 4 | 5 | Build an _Insert_ query using the following methods. They do not need to 6 | be called in any particular order, and may be called multiple times. 7 | 8 | ```php 9 | $insert = $queryFactory->newInsert(); 10 | 11 | $insert 12 | ->into('foo') // INTO this table 13 | ->cols([ // bind values as "(col) VALUES (:col)" 14 | 'bar', 15 | 'baz', 16 | ]) 17 | ->set('ts', 'NOW()') // raw value as "(ts) VALUES (NOW())" 18 | ->bindValue('foo', 'foo_val') // bind one value to a placeholder 19 | ->bindValues([ // bind these values 20 | 'bar' => 'foo', 21 | 'baz' => 'zim', 22 | ]); 23 | ``` 24 | 25 | The `cols()` method allows you to pass an array of key-value pairs where the 26 | key is the column name and the value is a bind value (not a raw value): 27 | 28 | ```php 29 | $insert = $queryFactory->newInsert(); 30 | 31 | $insert->into('foo') // insert into this table 32 | ->cols([ // insert these columns and bind these values 33 | 'foo' => 'foo_value', 34 | 'bar' => 'bar_value', 35 | 'baz' => 'baz_value', 36 | ]); 37 | ``` 38 | 39 | Once you have built the query, pass it to the database connection of your 40 | choice as a string, and send the bound values along with it. 41 | 42 | ```php 43 | // the PDO connection 44 | $pdo = new PDO(...); 45 | 46 | // prepare the statement 47 | $sth = $pdo->prepare($insert->getStatement()); 48 | 49 | // execute with bound values 50 | $sth->execute($insert->getBindValues()); 51 | 52 | // get the last insert ID 53 | $name = $insert->getLastInsertIdName('id'); 54 | $id = $pdo->lastInsertId($name); 55 | ``` 56 | 57 | ## Multiple-Row (Bulk) Insert 58 | 59 | If you want to do a multiple-row or bulk insert, call the `addRow()` method 60 | after finishing the first row, then build the next row you want to insert. The 61 | columns in the rows after the first will be inserted in the same order as the 62 | first row. 63 | 64 | ```php 65 | $insert = $queryFactory->newInsert(); 66 | 67 | // insert into this table 68 | $insert->into('foo'); 69 | 70 | // set up the first row 71 | $insert->cols([ 72 | 'bar' => 'bar-0', 73 | 'baz' => 'baz-0' 74 | ]); 75 | $insert->set('ts', 'NOW()'); 76 | 77 | // set up the second row. the columns here are in a different order 78 | // than in the first row, but it doesn't matter; the INSERT object 79 | // keeps track and builds them the same order as the first row. 80 | $insert->addRow(); 81 | $insert->set('ts', 'NOW()'); 82 | $insert->cols([ 83 | 'bar' => 'bar-1', 84 | 'baz' => 'baz-1' 85 | ]); 86 | 87 | // set up further rows ... 88 | $insert->addRow(); 89 | // ... 90 | 91 | // execute a bulk insert of all rows 92 | $pdo = new PDO(...); 93 | $sth = $pdo->prepare($insert->getStatement()); 94 | $sth->execute($insert->getBindValues()); 95 | ``` 96 | 97 | > N.b.: If you add a row and do not specify a value for a column that was 98 | > present in the first row, the _Insert_ will throw an exception. 99 | 100 | If you pass an array of column key-value pairs to `addRow()`, they will be 101 | bound to the next row, thus allowing you to skip setting up the first row 102 | manually with `col()` and `cols()`: 103 | 104 | ```php 105 | // set up the first row 106 | $insert->addRow([ 107 | 'bar' => 'bar-0', 108 | 'baz' => 'baz-0' 109 | ]); 110 | $insert->set('ts', 'NOW()'); 111 | 112 | // set up the second row 113 | $insert->addRow([ 114 | 'bar' => 'bar-1', 115 | 'baz' => 'baz-1' 116 | ]); 117 | $insert->set('ts', 'NOW()'); 118 | 119 | // etc. 120 | ``` 121 | 122 | If you only need to use bound values, and do not need to set raw values, and 123 | have the entire data set as an array already, you can use `addRows()` to add 124 | them all at once: 125 | 126 | ```php 127 | $rows = [ 128 | [ 129 | 'bar' => 'bar-0', 130 | 'baz' => 'baz-0' 131 | ], 132 | [ 133 | 'bar' => 'bar-1', 134 | 'baz' => 'baz-1' 135 | ], 136 | ]; 137 | $insert->addRows($rows); 138 | ``` 139 | 140 | > N.b.: SQLite 3.7.10 and earlier do not support the "standard" multiple-row 141 | > insert syntax. Thus, bulk inserts with _Insert_ object will not work on those 142 | > earlier versions of SQLite. We suggest wrapping multiple INSERT operations 143 | > with a transaction as an alternative. 144 | -------------------------------------------------------------------------------- /src/QueryFactory.php: -------------------------------------------------------------------------------- 1 | db = ucfirst(strtolower($db)); 74 | $this->common = ($common === self::COMMON); 75 | } 76 | 77 | /** 78 | * 79 | * Sets the last-insert-id names to be used for Insert queries.. 80 | * 81 | * @param array $last_insert_id_names A map of `table.col` names to 82 | * last-insert-id names. 83 | * 84 | * @return null 85 | * 86 | */ 87 | public function setLastInsertIdNames(array $last_insert_id_names) 88 | { 89 | $this->last_insert_id_names = $last_insert_id_names; 90 | } 91 | 92 | /** 93 | * 94 | * Returns a new SELECT object. 95 | * 96 | * @return Common\SelectInterface 97 | * 98 | */ 99 | public function newSelect() 100 | { 101 | return $this->newInstance('Select'); 102 | } 103 | 104 | /** 105 | * 106 | * Returns a new INSERT object. 107 | * 108 | * @return Common\InsertInterface 109 | * 110 | */ 111 | public function newInsert() 112 | { 113 | $insert = $this->newInstance('Insert'); 114 | $insert->setLastInsertIdNames($this->last_insert_id_names); 115 | return $insert; 116 | } 117 | 118 | /** 119 | * 120 | * Returns a new UPDATE object. 121 | * 122 | * @return Common\UpdateInterface 123 | * 124 | */ 125 | public function newUpdate() 126 | { 127 | return $this->newInstance('Update'); 128 | } 129 | 130 | /** 131 | * 132 | * Returns a new DELETE object. 133 | * 134 | * @return Common\DeleteInterface 135 | * 136 | */ 137 | public function newDelete() 138 | { 139 | return $this->newInstance('Delete'); 140 | } 141 | 142 | /** 143 | * 144 | * Returns a new query object. 145 | * 146 | * @param string $query The query object type. 147 | * 148 | * @return Common\SelectInterface|Common\InsertInterface|Common\UpdateInterface|Common\DeleteInterface 149 | * 150 | */ 151 | protected function newInstance($query) 152 | { 153 | $queryClass = "Aura\SqlQuery\\{$this->db}\\{$query}"; 154 | if ($this->common) { 155 | $queryClass = "Aura\SqlQuery\Common\\{$query}"; 156 | } 157 | 158 | $builderClass = "Aura\SqlQuery\\{$this->db}\\{$query}Builder"; 159 | if ($this->common || ! class_exists($builderClass)) { 160 | $builderClass = "Aura\SqlQuery\Common\\{$query}Builder"; 161 | } 162 | 163 | return new $queryClass( 164 | $this->getQuoter(), 165 | $this->newBuilder($query) 166 | ); 167 | } 168 | 169 | /** 170 | * 171 | * Returns a new Builder for the database driver. 172 | * 173 | * @param string $query The query type. 174 | * 175 | * @return Common\AbstractBuilder 176 | * 177 | */ 178 | protected function newBuilder($query) 179 | { 180 | $builderClass = "Aura\SqlQuery\\{$this->db}\\{$query}Builder"; 181 | if ($this->common || ! class_exists($builderClass)) { 182 | $builderClass = "Aura\SqlQuery\Common\\{$query}Builder"; 183 | } 184 | return new $builderClass(); 185 | } 186 | 187 | /** 188 | * 189 | * Returns the Quoter object for queries; creates one if needed. 190 | * 191 | * @return Common\Quoter 192 | * 193 | */ 194 | protected function getQuoter() 195 | { 196 | if (! $this->quoter) { 197 | $this->quoter = $this->newQuoter(); 198 | } 199 | return $this->quoter; 200 | } 201 | 202 | /** 203 | * 204 | * Returns a new Quoter for the database driver. 205 | * 206 | * @return Common\QuoterInterface 207 | * 208 | */ 209 | protected function newQuoter() 210 | { 211 | $quoterClass = "Aura\SqlQuery\\{$this->db}\Quoter"; 212 | if (! class_exists($quoterClass)) { 213 | $quoterClass = "Aura\SqlQuery\Common\Quoter"; 214 | } 215 | return new $quoterClass(); 216 | } 217 | } 218 | -------------------------------------------------------------------------------- /src/Mysql/Insert.php: -------------------------------------------------------------------------------- 1 | use_replace = $enable; 54 | return $this; 55 | } 56 | 57 | /** 58 | * 59 | * Adds or removes HIGH_PRIORITY flag. 60 | * 61 | * @param bool $enable Set or unset flag (default true). 62 | * 63 | * @return $this 64 | * 65 | */ 66 | public function highPriority($enable = true) 67 | { 68 | $this->setFlag('HIGH_PRIORITY', $enable); 69 | return $this; 70 | } 71 | 72 | /** 73 | * 74 | * Adds or removes LOW_PRIORITY flag. 75 | * 76 | * @param bool $enable Set or unset flag (default true). 77 | * 78 | * @return $this 79 | * 80 | */ 81 | public function lowPriority($enable = true) 82 | { 83 | $this->setFlag('LOW_PRIORITY', $enable); 84 | return $this; 85 | } 86 | 87 | /** 88 | * 89 | * Adds or removes IGNORE flag. 90 | * 91 | * @param bool $enable Set or unset flag (default true). 92 | * 93 | * @return $this 94 | * 95 | */ 96 | public function ignore($enable = true) 97 | { 98 | $this->setFlag('IGNORE', $enable); 99 | return $this; 100 | } 101 | 102 | /** 103 | * 104 | * Adds or removes DELAYED flag. 105 | * 106 | * @param bool $enable Set or unset flag (default true). 107 | * 108 | * @return $this 109 | * 110 | */ 111 | public function delayed($enable = true) 112 | { 113 | $this->setFlag('DELAYED', $enable); 114 | return $this; 115 | } 116 | 117 | /** 118 | * 119 | * Sets one column value placeholder in ON DUPLICATE KEY UPDATE section; 120 | * if an optional second parameter is passed, that value is bound to the 121 | * placeholder. 122 | * 123 | * @param string $col The column name. 124 | * 125 | * @param array $value Optional: a value to bind to the placeholder. 126 | * 127 | * @return $this 128 | * 129 | */ 130 | public function onDuplicateKeyUpdateCol($col, ...$value) 131 | { 132 | $key = $this->quoter->quoteName($col); 133 | $bind = $col . '__on_duplicate_key'; 134 | $this->col_on_update_values[$key] = ":$bind"; 135 | if (count($value) > 0) { 136 | $this->bindValue($bind, $value[0]); 137 | } 138 | return $this; 139 | } 140 | 141 | /** 142 | * 143 | * Sets multiple column value placeholders in ON DUPLICATE KEY UPDATE 144 | * section. If an element is a key-value pair, the key is treated as the 145 | * column name and the value is bound to that column. 146 | * 147 | * @param array $cols A list of column names, optionally as key-value 148 | * pairs where the key is a column name and the value is a bind value for 149 | * that column. 150 | * 151 | * @return $this 152 | * 153 | */ 154 | public function onDuplicateKeyUpdateCols(array $cols) 155 | { 156 | foreach ($cols as $key => $val) { 157 | if (is_int($key)) { 158 | // integer key means the value is the column name 159 | $this->onDuplicateKeyUpdateCol($val); 160 | } else { 161 | // the key is the column name and the value is a value to 162 | // be bound to that column 163 | $this->onDuplicateKeyUpdateCol($key, $val); 164 | } 165 | } 166 | return $this; 167 | } 168 | 169 | /** 170 | * 171 | * Sets a column value directly in ON DUPLICATE KEY UPDATE section; the 172 | * value will not be escaped, although fully-qualified identifiers in the 173 | * value will be quoted. 174 | * 175 | * @param string $col The column name. 176 | * 177 | * @param string $value The column value expression. 178 | * 179 | * @return $this 180 | * 181 | */ 182 | public function onDuplicateKeyUpdate($col, $value) 183 | { 184 | if ($value === null) { 185 | $value = 'NULL'; 186 | } 187 | 188 | $key = $this->quoter->quoteName($col); 189 | $value = $this->quoter->quoteNamesIn($value); 190 | $this->col_on_update_values[$key] = $value; 191 | return $this; 192 | } 193 | 194 | /** 195 | * 196 | * Builds this query object into a string. 197 | * 198 | * @return string 199 | * 200 | */ 201 | protected function build() 202 | { 203 | $stm = parent::build(); 204 | 205 | if ($this->use_replace) { 206 | // change INSERT to REPLACE 207 | $stm = 'REPLACE' . substr($stm, 6); 208 | } 209 | 210 | return $stm 211 | . $this->builder->buildValuesForUpdateOnDuplicateKey($this->col_on_update_values); 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | ## 2.7.1 4 | 5 | Hygiene release: update README. 6 | 7 | ## 2.7.0 8 | 9 | - [DOC] Numerous docblock and README updates. 10 | 11 | - [ADD] Add various `Select::reset*()` methods. Fixes #84, #95, #94, #91. 12 | 13 | - [FIX] On SELECT, allow OFFSET even when LIMIT not specified. Fixes #88. 14 | 15 | - [FIX] On SELECT, allow `join*()` before `from*()`. Joins-before-from are added 16 | to the first from. If no from is ever added, the joins will never be built 17 | into the statement. Fixes #69, #90. 18 | 19 | - [BRK] Bumped the minimum version to PHP 5.3.9 (vs 5.3.0). Fixes #74. This is 20 | to address a language-level bug in PHP. Technically I think this is a BC 21 | break, but I hope it is understandable, given that PHP 5.3.x is end-of-life, 22 | and that Aura.SqlQuery itself simply will not operate on versions earlier 23 | than that. Updated README to reflect the version requirement. 24 | 25 | 26 | ## 2.6.0 27 | 28 | - (DOC) Docblock and README updates; in particular, add an `@method getStatement()` to the QueryInterface for IDE auto-completion. 29 | 30 | - (ADD) Select::hasCols() reports if there are any columsn in the Select. 31 | 32 | - (ADD) Select::getCols() gets the existing columns in the Select. 33 | 34 | - (ADD) Select::removeCol() removes a previously-added column. 35 | 36 | - (FIX) Select::reset() now properly resets the table refs for a UNION. 37 | 38 | - (FIX) Select::forUpdate() is now fluent. 39 | 40 | ## 2.5.0 41 | 42 | - Docblock and README updates 43 | 44 | - The Common\Select class, when binding values from a subselect, now checks for 45 | `instanceof SubselectInterface` instead of `self`; the Select class now 46 | implements SubselectInterface, so this should not be a BC break. 47 | 48 | - Subselects bound as where/having/etc conditions should now retain ?-bound 49 | params. 50 | 51 | ## 2.4.2 52 | 53 | This release modifies the testing structure and updates other support files. 54 | 55 | ## 2.4.1 56 | 57 | This release fixes Insert::addRows() so that adding only one row generates the correct SQL statement. 58 | 59 | ## 2.4.0 60 | 61 | This release incorporates two feature additions and one fix. 62 | 63 | - ADD: The _Insert_ objects now support multiple-row inserts with the new `addRow()` and `addRows()` methods. 64 | 65 | - ADD: The MySQL _Insert_ object now supports `ON DUPLICATE KEY UPDATE` functionality with the new `onDuplicateKeyUpdate*()` methods. 66 | 67 | - FIX: The _Select_ methods regarding paging now interact better with LIMIT and OFFSET; in particular, both `setPaging()` now re-calculates the LIMIT and OFFSET values. 68 | 69 | ## 2.3.0 70 | 71 | This release has several new features. 72 | 73 | 1. The various `join()` methods now have an extra `$bind` param that allows you to bind values to ?-placeholders in the condition, just as with `where()` and `having()`. 74 | 75 | 2. The _Select_ class now tracks table references internally, and will throw an exception if you try to use the same table name or alias more than once. 76 | 77 | 3. The method `getStatement()` has been added to all queries, to allow you to get the text of the statement being built. Among other things, this is to avoid exception-related blowups related to PHP's string casting. 78 | 79 | 4. When binding a value to a sequential placeholder in `where()`, `having()`, etc, the _Select_ class now examind the value to see if it is a query object. If so, it converts the object to a string and replaces the ?-placeholder inline with the string instead of attempting to bind it proper. It also binds the existing sequential placholder values into the current _Select_ in a non-conflicting fashion. (Previously, no binding from the sub-select took place at all.) 80 | 81 | 5. In `fromSubSelect()` and `joinSubSelect()`, the _Select_ class now binds the sub-select object sequential values to the current _Select_ in a non-conflicting fashion. (Previously, no binding from the sub-select took place at all.) 82 | 83 | The change log follows: 84 | 85 | - REF: Extract rebuilding of condition and binding sequential values. 86 | 87 | - FIX: Allow binding of values as part of join() methods. Fixes #27. 88 | 89 | - NEW: Method Select::addTableRef(), to track table references and prevent double-use of aliases. Fixes #38. 90 | 91 | - REF: Extract statement-building to AbstractQuery::getStatement() method. Fixes #30. 92 | 93 | - FIX: #47, if value for sequential placeholder is a Query, place it as a string inline 94 | 95 | - ADD: Sequential-placeholder prefixing 96 | 97 | - ADD: bind values from sub-selects, and modify indenting 98 | 99 | - ADD: QueryFactory now sets the sequntial bind prefix 100 | 101 | - FIX: Fix line endings in queries to be sure tests will pass on windows and mac. Merge pull request #53 from ksimka/fix-tests-remove-line-endings: Fixed tests for windows. 102 | 103 | - Merge pull request #50 from auraphp/bindonjoin: Allow binding of values as part of join() methods. 104 | 105 | - Merge pull request #51 from auraphp/aliastracking: Add table-reference tracking to disallow duplicate references. 106 | 107 | - Merge pull request #52 from auraphp/bindsubselect. Bind Values From Sub-Selects. 108 | 109 | - DOC: Update documentation and support files. 110 | 111 | ## 2.2.0 112 | 113 | To avoid mixing numbered and names placeholders, we now convert numbered ? placeholders in where() and having() to :_#_ named placeholders. This is because PDO is really touchy about sequence numbers on ? placeholders. If we have bound values [:foo, :bar, ?, :baz], the ? placeholder is not number 1, it is number 3. As it is nigh impossible to keep track of the numbering when done out-of-order, we now do a braindead check on the where/having condition string to see if it has ? placholders, and replace them with named :_#_ placeholders, where # is the current count of the $bind_values array. 114 | 115 | 116 | ## 2.1.0 117 | 118 | - ADD: Select::fromRaw() to allow raw FROM clause strings. 119 | 120 | - CHG: In Select, quote the columns at build time, not add time. 121 | 122 | - CHG: In Select, retain columns keyed on their aliases (when given). 123 | 124 | - DOC: Updates to README and docblocks. 125 | 126 | ## 2.0.0 127 | 128 | Initial 2.0 stable release. 129 | 130 | - The package has been renamed from Sql_Query to SqlQuery, in line with the new Aura naming standards. 131 | 132 | - Now compatible with PHP 5.3! 133 | 134 | - Refactored traits into interfaces (thanks @mindplay-dk). 135 | 136 | - Refactored the internal build process (thanks again @mindplay-dk). 137 | 138 | - Added Select::leftJoin()/innerJoin() methods (thanks @stanlemon). 139 | 140 | - Methods bindValue() and bindValues() are now fluent (thanks @karikt). 141 | 142 | - Select now throws an exception when there are no columns selected. 143 | 144 | - In joins, the condition type (ON or USING) may now be part of the condition. 145 | 146 | - Extracted new class, Quoter, for quoting identifer names. 147 | 148 | - Extracted new class, AbstractDmlQuery, for Insert/Update/Delete queries. 149 | 150 | - Select::cols() now accepts `colname => alias` pairs mixed in with sequential colname values. 151 | 152 | - Added functionality to map last-insert-id names to alternative sequence names, esp. for Postgres and inherited/extended tables. Cf. QueryFactory::setLastInsertIdNames() and Insert::setLastInsertIdNames(). 153 | 154 | ## 2.0.0-beta1 155 | 156 | Initial 2.0 beta release. 157 | 158 | -------------------------------------------------------------------------------- /docs/select.md: -------------------------------------------------------------------------------- 1 | # SELECT 2 | 3 | ## Building A Query 4 | 5 | Build a _Select_ query using the following methods. They do not need to 6 | be called in any particular order, and may be called multiple times. 7 | 8 | First, create a new SELECT object with the query factory: 9 | 10 | ```php 11 | $select = $queryFactory->newSelect(); 12 | ``` 13 | 14 | ## Columns 15 | 16 | To add columns to the select, use the `cols()` method. 17 | 18 | ```php 19 | $select->cols([ 20 | 'id', // column name 21 | 'name AS namecol', // one way of aliasing 22 | 'col_name' => 'col_alias', // another way of aliasing 23 | 'COUNT(foo) AS foo_count' // embed calculations directly 24 | ]) 25 | ``` 26 | 27 | Other related methods: 28 | 29 | - `removeCol($alias) : null` -- Removes a column from the SELECT. 30 | - `hasCol($alias) : bool` -- Will a column be SELECTed with this query? 31 | - `hasCols() : bool` -- Does the SELECT have any columns in it at all? 32 | - `getCols() : array` -- Returns the columns named in the SELECT. 33 | 34 | 35 | ## FROM 36 | 37 | To add a FROM clause, call the `from()` method as needed: 38 | 39 | ```php 40 | // FROM foo, "bar" as "b" 41 | $select 42 | ->from('foo') // table name 43 | ->from('bar AS b'); // alias the table as desired 44 | ``` 45 | 46 | The table names will automatically be quoted for you. If you don't want 47 | quoting applied, use the `fromRaw()` method instead. 48 | 49 | If you want to SELECT FROM a subselect, do so by calling `fromSubSelect()`. 50 | Pass both the subselect query string, and an alias for the subselect: 51 | 52 | ```php 53 | // FROM (SELECT ...) AS "my_sub" 54 | $select->fromSubSelect('SELECT ...', 'my_sub'); 55 | ``` 56 | 57 | You can also pass a SELECT object as the subselect, instead of a query string. 58 | This allows you to create an entire SELECT query and use it as a subselect. 59 | 60 | 61 | ## JOIN 62 | 63 | To add a JOIN clause, call the `join()` method as needed: 64 | 65 | ```php 66 | // LEFT JOIN doom AS d ON foo.id = d.foo_id 67 | $select->join( 68 | 'LEFT', // the join-type 69 | 'doom AS d', // join to this table ... 70 | 'foo.id = d.foo_id' // ... ON these conditions 71 | ); 72 | ``` 73 | 74 | For convenience, the methods `leftJoin()` and `innerJoin()` exist to allow you 75 | to elmininate the join-type argument for LEFT and INNER joins, respectively. 76 | 77 | As with FROM, you can join to a subselect using `joinSubSelect()`: 78 | 79 | ```php 80 | // INNER JOIN (SELECT ...) AS subjoin ON subjoin.id = foo.id 81 | $select->joinSubSelect( 82 | 'INNER', // left/inner/natural/etc 83 | 'SELECT ...', // the subselect to join on 84 | 'subjoin', // AS this name 85 | 'subjoin.id = foo.id' // ON these conditions 86 | ); 87 | ``` 88 | 89 | Also as with FROM, you can pass a SELECT object instead of a query string as the 90 | subselect. 91 | 92 | Finally, all of the `*join*()` methods take an optional final argument, a 93 | sequential array of values to bind to sequential question-mark placeholders in 94 | the condition clause. 95 | 96 | 97 | ## WHERE 98 | 99 | To add WHERE clauses, call the `where()` method as needed. Subsequent calls to 100 | `where()` will AND the condition, unless you call `orWhere()`, in which case it 101 | will OR the condition. 102 | 103 | ```php 104 | ->where('bar > :bar') // WHERE bar > :bar 105 | ->where('zim = :zim') // AND zim = :ZIM 106 | ->orWhere('baz < :baz') // OR baz < :baz 107 | ``` 108 | 109 | The `*where()` and `*having()` methods take a trailing trailing argument of a 110 | placeholder-to-value array, which will be bound to the query right then. 111 | 112 | // bind 'zim_val' to the :zim placeholder 113 | ->where('zim = :zim', ['zim' => 'zim_val']) 114 | 115 | You can also use `IN` conditions by binding an array to the placeholder. 116 | 117 | ```php 118 | ->where('bar IN (:bar)', ['bar' => [1, 2, 3]]) 119 | ``` 120 | 121 | // bind values to the :zims placeholder 122 | ->where('zims IN (:zims)', ['zims' => ['zim_val', 'zim_val2', 'zim_val3']]) 123 | 124 | 125 | ## GROUP BY 126 | 127 | ```php 128 | ->groupBy(['dib']) // GROUP BY these columns 129 | ``` 130 | 131 | ## HAVING 132 | 133 | ```php 134 | ->having('foo = :foo') // AND HAVING these conditions 135 | ->having('bar > :bar', ['bar' => 'bar_val']) // bind 'bar_val' to the :bar placeholder 136 | ->orHaving('baz < :baz') // OR HAVING these conditions 137 | ``` 138 | 139 | The `*where()` and `*having()` methods take an arbitrary number of 140 | trailing arguments, each of which is a value to bind to a sequential question- 141 | mark placeholder in the condition clause. 142 | 143 | ## ORDER BY 144 | 145 | ```php 146 | ->orderBy(['baz ASC']) // ORDER BY these columns 147 | ``` 148 | 149 | ## LIMIT, OFFSET, and Paging 150 | 151 | ```php 152 | ->limit(10) // LIMIT 10 153 | ->offset(40) // OFFSET 40 154 | public function page($page) 155 | public function getPage() 156 | public function setPaging($paging) 157 | public function getPaging() 158 | ``` 159 | 160 | ## UNION 161 | 162 | ```php 163 | ->union() // UNION with a followup SELECT 164 | ->unionAll() // UNION ALL with a followup SELECT 165 | ``` 166 | 167 | ## Flags 168 | 169 | ```php 170 | ->forUpdate() // FOR UPDATE 171 | ->distinct() // SELECT DISTINCT 172 | ->isDistinct() // returns true if query is DISTINCT 173 | ``` 174 | 175 | ## Binding Values 176 | 177 | ```php 178 | ->bindValue('foo', 'foo_val') // bind one value to a placeholder 179 | ->bindValues([ // bind these values to named placeholders 180 | 'bar' => 'bar_val', 181 | 'baz' => 'baz_val', 182 | ]); 183 | ``` 184 | 185 | ## Inspecting The Query 186 | 187 | 188 | ## Resetting Query Elements 189 | 190 | The _Select_ class comes with the following methods to "reset" various clauses 191 | a blank state. This can be useful when reusing the same query in different 192 | variations (e.g., to re-issue a query to get a `COUNT(*)` without a `LIMIT`, to 193 | find the total number of rows to be paginated over). 194 | 195 | - `resetCols()` removes all columns 196 | - `resetTables()` removes all `FROM` and `JOIN` clauses 197 | - `resetWhere()`, `resetGroupBy()`, `resetHaving()`, and `resetOrderBy()` 198 | remove the respective clauses 199 | - `resetUnions()` removes all `UNION` and `UNION ALL` clauses 200 | - `resetFlags()` removes all database-engine-specific flags 201 | - `resetBindValues()` removes all values bound to named placeholders 202 | 203 | public function reset() 204 | public function resetWhere() 205 | public function resetGroupBy() 206 | public function resetHaving() 207 | public function resetOrderBy() 208 | 209 | ## Issuing The Query 210 | 211 | Once you have built the query, pass it to the database connection of your 212 | choice as a string, and send the bound values along with it. 213 | 214 | ```php 215 | // a PDO connection 216 | $pdo = new PDO(...); 217 | 218 | // prepare the statement 219 | $sth = $pdo->prepare($select->getStatement()); 220 | 221 | // bind the values and execute 222 | $sth->execute($select->getBindValues()); 223 | 224 | // get the results back as an associative array 225 | $result = $sth->fetch(PDO::FETCH_ASSOC); 226 | ``` 227 | -------------------------------------------------------------------------------- /src/Common/Quoter.php: -------------------------------------------------------------------------------- 1 | quote_name_prefix; 48 | } 49 | 50 | /** 51 | * 52 | * Returns the suffix to use when quoting identifier names. 53 | * 54 | * @return string 55 | * 56 | */ 57 | public function getQuoteNameSuffix() 58 | { 59 | return $this->quote_name_suffix; 60 | } 61 | 62 | /** 63 | * 64 | * Quotes a single identifier name (table, table alias, table column, 65 | * index, sequence). 66 | * 67 | * If the name contains `' AS '`, this method will separately quote the 68 | * parts before and after the `' AS '`. 69 | * 70 | * If the name contains a space, this method will separately quote the 71 | * parts before and after the space. 72 | * 73 | * If the name contains a dot, this method will separately quote the 74 | * parts before and after the dot. 75 | * 76 | * @param string $spec The identifier name to quote. 77 | * 78 | * @return string The quoted identifier name. 79 | * 80 | * @see replaceName() 81 | * 82 | * @see quoteNameWithSeparator() 83 | * 84 | */ 85 | public function quoteName($spec) 86 | { 87 | $spec = trim($spec); 88 | $seps = array(' AS ', ' ', '.'); 89 | foreach ($seps as $sep) { 90 | $pos = strripos($spec, $sep); 91 | if ($pos) { 92 | return $this->quoteNameWithSeparator($spec, $sep, $pos); 93 | } 94 | } 95 | return $this->replaceName($spec); 96 | } 97 | 98 | /** 99 | * 100 | * Quotes an identifier that has a separator. 101 | * 102 | * @param string $spec The identifier name to quote. 103 | * 104 | * @param string $sep The separator, typically a dot or space. 105 | * 106 | * @param int $pos The position of the separator. 107 | * 108 | * @return string The quoted identifier name. 109 | * 110 | */ 111 | protected function quoteNameWithSeparator($spec, $sep, $pos) 112 | { 113 | $len = strlen($sep); 114 | $part1 = $this->quoteName(substr($spec, 0, $pos)); 115 | $part2 = $this->replaceName(substr($spec, $pos + $len)); 116 | return "{$part1}{$sep}{$part2}"; 117 | } 118 | 119 | /** 120 | * 121 | * Quotes all fully-qualified identifier names ("table.col") in a string, 122 | * typically an SQL snippet for a SELECT clause. 123 | * 124 | * Does not quote identifier names that are string literals (i.e., inside 125 | * single or double quotes). 126 | * 127 | * Looks for a trailing ' AS alias' and quotes the alias as well. 128 | * 129 | * @param string $text The string in which to quote fully-qualified 130 | * identifier names to quote. 131 | * 132 | * @return string|array The string with names quoted in it. 133 | * 134 | * @see replaceNamesIn() 135 | * 136 | */ 137 | public function quoteNamesIn($text) 138 | { 139 | $list = $this->getListForQuoteNamesIn($text); 140 | $last = count($list) - 1; 141 | $text = null; 142 | foreach ($list as $key => $val) { 143 | // skip elements 2, 5, 8, 11, etc. as artifacts of the back- 144 | // referenced split; these are the trailing/ending quote 145 | // portions, and already included in the previous element. 146 | // this is the same as skipping every third element from zero. 147 | if (($key+1) % 3) { 148 | $text .= $this->quoteNamesInLoop($val, $key == $last); 149 | } 150 | } 151 | return $text; 152 | } 153 | 154 | /** 155 | * 156 | * Returns a list of candidate elements for quoting. 157 | * 158 | * @param string $text The text to split into quoting candidates. 159 | * 160 | * @return array 161 | * 162 | */ 163 | protected function getListForQuoteNamesIn($text) 164 | { 165 | // look for ', ", \', or \" in the string. 166 | // match closing quotes against the same number of opening quotes. 167 | $apos = "'"; 168 | $quot = '"'; 169 | return preg_split( 170 | "/(($apos+|$quot+|\\$apos+|\\$quot+).*?\\2)/", 171 | $text, 172 | -1, 173 | PREG_SPLIT_DELIM_CAPTURE 174 | ); 175 | } 176 | 177 | /** 178 | * 179 | * The in-loop functionality for quoting identifier names. 180 | * 181 | * @param string $val The name to be quoted. 182 | * 183 | * @param bool $is_last Is this the last loop? 184 | * 185 | * @return string The quoted name. 186 | * 187 | */ 188 | protected function quoteNamesInLoop($val, $is_last) 189 | { 190 | if ($is_last) { 191 | return $this->replaceNamesAndAliasIn($val); 192 | } 193 | return $this->replaceNamesIn($val); 194 | } 195 | 196 | /** 197 | * 198 | * Replaces the names and alias in a string. 199 | * 200 | * @param string $val The name to be quoted. 201 | * 202 | * @return string The quoted name. 203 | * 204 | */ 205 | protected function replaceNamesAndAliasIn($val) 206 | { 207 | $quoted = $this->replaceNamesIn($val); 208 | $pos = strripos($quoted, ' AS '); 209 | if ($pos !== false) { 210 | $alias = $this->replaceName(substr($quoted, $pos + 4)); 211 | $quoted = substr($quoted, 0, $pos) . " AS $alias"; 212 | } 213 | return $quoted; 214 | } 215 | 216 | /** 217 | * 218 | * Quotes an identifier name (table, index, etc); ignores empty values and 219 | * values of '*'. 220 | * 221 | * @param string $name The identifier name to quote. 222 | * 223 | * @return string The quoted identifier name. 224 | * 225 | * @see quoteName() 226 | * 227 | */ 228 | protected function replaceName($name) 229 | { 230 | $name = trim($name); 231 | if ($name == '*') { 232 | return $name; 233 | } 234 | 235 | return $this->quote_name_prefix 236 | . $name 237 | . $this->quote_name_suffix; 238 | } 239 | 240 | /** 241 | * 242 | * Quotes all fully-qualified identifier names ("table.col") in a string. 243 | * 244 | * @param string $text The string in which to quote fully-qualified 245 | * identifier names to quote. 246 | * 247 | * @return string|array The string with names quoted in it. 248 | * 249 | * @see quoteNamesIn() 250 | * 251 | */ 252 | protected function replaceNamesIn($text) 253 | { 254 | $is_string_literal = strpos($text, "'") !== false 255 | || strpos($text, '"') !== false; 256 | if ($is_string_literal) { 257 | return $text; 258 | } 259 | 260 | $word = "[a-z_][a-z0-9_]*"; 261 | 262 | $find = "/(\\b)($word)\\.($word)(\\b)/i"; 263 | 264 | $repl = '$1' 265 | . $this->quote_name_prefix 266 | . '$2' 267 | . $this->quote_name_suffix 268 | . '.' 269 | . $this->quote_name_prefix 270 | . '$3' 271 | . $this->quote_name_suffix 272 | . '$4' 273 | ; 274 | 275 | $text = preg_replace($find, $repl, $text); 276 | 277 | return $text; 278 | } 279 | 280 | } 281 | -------------------------------------------------------------------------------- /src/Common/SelectInterface.php: -------------------------------------------------------------------------------- 1 | last_insert_id_names = $last_insert_id_names; 101 | } 102 | 103 | /** 104 | * 105 | * Sets the table to insert into. 106 | * 107 | * @param string $into The table to insert into. 108 | * 109 | * @return $this 110 | * 111 | */ 112 | public function into($into) 113 | { 114 | $this->into_raw = $into; 115 | $this->into = $this->quoter->quoteName($into); 116 | return $this; 117 | } 118 | 119 | /** 120 | * 121 | * Builds this query object into a string. 122 | * 123 | * @return string 124 | * 125 | */ 126 | protected function build() 127 | { 128 | $stm = 'INSERT' 129 | . $this->builder->buildFlags($this->flags) 130 | . $this->builder->buildInto($this->into); 131 | 132 | if ($this->row) { 133 | $this->finishRow(); 134 | $stm .= $this->builder->buildValuesForBulkInsert($this->col_order, $this->col_values_bulk); 135 | } else { 136 | $stm .= $this->builder->buildValuesForInsert($this->col_values); 137 | } 138 | 139 | return $stm; 140 | } 141 | 142 | /** 143 | * 144 | * Returns the proper name for passing to `PDO::lastInsertId()`. 145 | * 146 | * @param string $col The last insert ID column. 147 | * 148 | * @return mixed Normally null, since most drivers do not need a name; 149 | * alternatively, a string from `$last_insert_id_names`. 150 | * 151 | */ 152 | public function getLastInsertIdName($col) 153 | { 154 | $key = $this->into_raw . '.' . $col; 155 | if (isset($this->last_insert_id_names[$key])) { 156 | return $this->last_insert_id_names[$key]; 157 | } 158 | } 159 | 160 | /** 161 | * 162 | * Sets one column value placeholder; if an optional second parameter is 163 | * passed, that value is bound to the placeholder. 164 | * 165 | * @param string $col The column name. 166 | * 167 | * @param array $value Optional: a value to bind to the placeholder. 168 | * 169 | * @return $this 170 | * 171 | */ 172 | public function col($col, ...$value) 173 | { 174 | return $this->addCol($col, ...$value); 175 | } 176 | 177 | /** 178 | * 179 | * Sets multiple column value placeholders. If an element is a key-value 180 | * pair, the key is treated as the column name and the value is bound to 181 | * that column. 182 | * 183 | * @param array $cols A list of column names, optionally as key-value 184 | * pairs where the key is a column name and the value is a bind value for 185 | * that column. 186 | * 187 | * @return $this 188 | * 189 | */ 190 | public function cols(array $cols) 191 | { 192 | return $this->addCols($cols); 193 | } 194 | 195 | /** 196 | * 197 | * Sets a column value directly; the value will not be escaped, although 198 | * fully-qualified identifiers in the value will be quoted. 199 | * 200 | * @param string $col The column name. 201 | * 202 | * @param string $value The column value expression. 203 | * 204 | * @return $this 205 | * 206 | */ 207 | public function set($col, $value) 208 | { 209 | return $this->setCol($col, $value); 210 | } 211 | 212 | /** 213 | * 214 | * Gets the values to bind to placeholders. 215 | * 216 | * @return array 217 | * 218 | */ 219 | public function getBindValues() 220 | { 221 | return array_merge(parent::getBindValues(), $this->bind_values_bulk); 222 | } 223 | 224 | /** 225 | * 226 | * Adds multiple rows for bulk insert. 227 | * 228 | * @param array $rows An array of rows, where each element is an array of 229 | * column key-value pairs. The values are bound to placeholders. 230 | * 231 | * @return $this 232 | * 233 | */ 234 | public function addRows(array $rows) 235 | { 236 | foreach ($rows as $cols) { 237 | $this->addRow($cols); 238 | } 239 | if ($this->row > 1) { 240 | $this->finishRow(); 241 | } 242 | return $this; 243 | } 244 | 245 | /** 246 | * 247 | * Add one row for bulk insert; increments the row counter and optionally 248 | * adds columns to the new row. 249 | * 250 | * When adding the first row, the counter is not incremented. 251 | * 252 | * After calling `addRow()`, you can further call `col()`, `cols()`, and 253 | * `set()` to work with the newly-added row. Calling `addRow()` again will 254 | * finish off the current row and start a new one. 255 | * 256 | * @param array $cols An array of column key-value pairs; the values are 257 | * bound to placeholders. 258 | * 259 | * @return $this 260 | * 261 | */ 262 | public function addRow(array $cols = array()) 263 | { 264 | if (empty($this->col_values)) { 265 | return $this->cols($cols); 266 | } 267 | 268 | if (empty($this->col_order)) { 269 | $this->col_order = array_keys($this->col_values); 270 | } 271 | 272 | $this->finishRow(); 273 | $this->row ++; 274 | $this->cols($cols); 275 | return $this; 276 | } 277 | 278 | /** 279 | * 280 | * Adds IGNORE flag depending on DB syntax. 281 | * 282 | * @param bool $enable Set or unset flag (default true). 283 | * @throws Exception 284 | * @return \Aura\SqlQuery\Sqlite\Insert 285 | * 286 | */ 287 | public function ignore($enable = true) 288 | { 289 | // override in child classes 290 | throw new Exception(get_class($this) . " doesn't support IGNORE flag"); 291 | } 292 | 293 | /** 294 | * 295 | * Finishes off the current row in a bulk insert, collecting the bulk 296 | * values and resetting for the next row. 297 | * 298 | * @return null 299 | * 300 | */ 301 | protected function finishRow() 302 | { 303 | if (empty($this->col_values)) { 304 | return; 305 | } 306 | 307 | foreach ($this->col_order as $col) { 308 | $this->finishCol($col); 309 | } 310 | 311 | $this->col_values = array(); 312 | $this->bind_values = array(); 313 | } 314 | 315 | /** 316 | * 317 | * Finishes off a single column of the current row in a bulk insert. 318 | * 319 | * @param string $col The column to finish off. 320 | * 321 | * @return null 322 | * 323 | * @throws Exception on named column missing from row. 324 | * 325 | */ 326 | protected function finishCol($col) 327 | { 328 | if (! array_key_exists($col, $this->col_values)) { 329 | throw new Exception("Column $col missing from row {$this->row}."); 330 | } 331 | 332 | // get the current col_value 333 | $value = $this->col_values[$col]; 334 | 335 | // is it *not* a placeholder? 336 | if (substr($value, 0, 1) != ':') { 337 | // copy the value as-is 338 | $this->col_values_bulk[$this->row][$col] = $value; 339 | return; 340 | } 341 | 342 | // retain col_values in bulk with the row number appended 343 | $this->col_values_bulk[$this->row][$col] = "{$value}_{$this->row}"; 344 | 345 | // the existing placeholder name without : or row number 346 | $name = substr($value, 1); 347 | 348 | // retain bind_value in bulk with new placeholder 349 | if (array_key_exists($name, $this->bind_values)) { 350 | $this->bind_values_bulk["{$name}_{$this->row}"] = $this->bind_values[$name]; 351 | } 352 | } 353 | } 354 | -------------------------------------------------------------------------------- /src/AbstractQuery.php: -------------------------------------------------------------------------------- 1 | quoter = $quoter; 95 | $this->builder = $builder; 96 | } 97 | 98 | /** 99 | * 100 | * Returns this query object as an SQL statement string. 101 | * 102 | * @return string 103 | * 104 | */ 105 | public function __toString() 106 | { 107 | return $this->getStatement(); 108 | } 109 | 110 | /** 111 | * 112 | * Returns this query object as an SQL statement string. 113 | * 114 | * @return string 115 | * 116 | */ 117 | public function getStatement() 118 | { 119 | return $this->build(); 120 | } 121 | 122 | /** 123 | * 124 | * Builds this query object into a string. 125 | * 126 | * @return string 127 | * 128 | */ 129 | abstract protected function build(); 130 | 131 | /** 132 | * 133 | * Returns the prefix to use when quoting identifier names. 134 | * 135 | * @return string 136 | * 137 | */ 138 | public function getQuoteNamePrefix() 139 | { 140 | return $this->quoter->getQuoteNamePrefix(); 141 | } 142 | 143 | /** 144 | * 145 | * Returns the suffix to use when quoting identifier names. 146 | * 147 | * @return string 148 | * 149 | */ 150 | public function getQuoteNameSuffix() 151 | { 152 | return $this->quoter->getQuoteNameSuffix(); 153 | } 154 | 155 | /** 156 | * 157 | * Binds multiple values to placeholders; merges with existing values. 158 | * 159 | * @param array $bind_values Values to bind to placeholders. 160 | * 161 | * @return $this 162 | * 163 | */ 164 | public function bindValues(array $bind_values) 165 | { 166 | // array_merge() renumbers integer keys, which is bad for 167 | // question-mark placeholders 168 | foreach ($bind_values as $key => $val) { 169 | $this->bindValue($key, $val); 170 | } 171 | return $this; 172 | } 173 | 174 | /** 175 | * 176 | * Binds a single value to the query. 177 | * 178 | * @param string $name The placeholder name or number. 179 | * 180 | * @param mixed $value The value to bind to the placeholder. 181 | * 182 | * @return $this 183 | * 184 | */ 185 | public function bindValue($name, $value) 186 | { 187 | $this->bind_values[$name] = $value; 188 | return $this; 189 | } 190 | 191 | /** 192 | * 193 | * Gets the values to bind to placeholders. 194 | * 195 | * @return array 196 | * 197 | */ 198 | public function getBindValues() 199 | { 200 | return $this->bind_values; 201 | } 202 | 203 | /** 204 | * 205 | * Reset all values bound to named placeholders. 206 | * 207 | * @return $this 208 | * 209 | */ 210 | public function resetBindValues() 211 | { 212 | $this->bind_values = array(); 213 | return $this; 214 | } 215 | 216 | /** 217 | * 218 | * Sets or unsets specified flag. 219 | * 220 | * @param string $flag Flag to set or unset 221 | * 222 | * @param bool $enable Flag status - enabled or not (default true) 223 | * 224 | * @return null 225 | * 226 | */ 227 | protected function setFlag($flag, $enable = true) 228 | { 229 | if ($enable) { 230 | $this->flags[$flag] = true; 231 | } else { 232 | unset($this->flags[$flag]); 233 | } 234 | } 235 | 236 | /** 237 | * 238 | * Returns true if the specified flag was enabled by setFlag(). 239 | * 240 | * @param string $flag Flag to check 241 | * 242 | * @return bool 243 | * 244 | */ 245 | protected function hasFlag($flag) 246 | { 247 | return isset($this->flags[$flag]); 248 | } 249 | 250 | /** 251 | * 252 | * Reset all query flags. 253 | * 254 | * @return $this 255 | * 256 | */ 257 | public function resetFlags() 258 | { 259 | $this->flags = array(); 260 | return $this; 261 | } 262 | 263 | /** 264 | * 265 | * Adds conditions and binds values to a clause. 266 | * 267 | * @param string $clause The clause to work with, typically 'where' or 268 | * 'having'. 269 | * 270 | * @param string $andor Add the condition using this operator, typically 271 | * 'AND' or 'OR'. 272 | * 273 | * @param string $cond The WHERE condition. 274 | * 275 | * @param array $bind arguments to bind to placeholders 276 | * 277 | * @return null 278 | * 279 | */ 280 | protected function addClauseCondWithBind($clause, $andor, $cond, $bind) 281 | { 282 | if ($cond instanceof Closure) { 283 | $this->addClauseCondClosure($clause, $andor, $cond); 284 | $this->bindValues($bind); 285 | return; 286 | } 287 | 288 | $cond = $this->quoter->quoteNamesIn($cond); 289 | $cond = $this->rebuildCondAndBindValues($cond, $bind); 290 | 291 | $clause =& $this->$clause; 292 | if ($clause) { 293 | $clause[] = "$andor $cond"; 294 | } else { 295 | $clause[] = $cond; 296 | } 297 | } 298 | 299 | /** 300 | * 301 | * Adds to a clause through a closure, enclosing within parentheses. 302 | * 303 | * @param string $clause The clause to work with, typically 'where' or 304 | * 'having'. 305 | * 306 | * @param string $andor Add the condition using this operator, typically 307 | * 'AND' or 'OR'. 308 | * 309 | * @param callable $closure The closure that adds to the clause. 310 | * 311 | * @return null 312 | * 313 | */ 314 | protected function addClauseCondClosure($clause, $andor, $closure) 315 | { 316 | // retain the prior set of conditions, and temporarily reset the clause 317 | // for the closure to work with (otherwise there will be an extraneous 318 | // opening AND/OR keyword) 319 | $set = $this->$clause; 320 | $this->$clause = []; 321 | 322 | // invoke the closure, which will re-populate the $this->$clause 323 | $closure($this); 324 | 325 | // are there new clause elements? 326 | if (! $this->$clause) { 327 | // no: restore the old ones, and done 328 | $this->$clause = $set; 329 | return; 330 | } 331 | 332 | // append an opening parenthesis to the prior set of conditions, 333 | // with AND/OR as needed ... 334 | if ($set) { 335 | $set[] = "{$andor} ("; 336 | } else { 337 | $set[] = "("; 338 | } 339 | 340 | // append the new conditions to the set, with indenting 341 | foreach ($this->$clause as $cond) { 342 | $set[] = " {$cond}"; 343 | } 344 | $set[] = ")"; 345 | 346 | // ... then put the full set of conditions back into $this->$clause 347 | $this->$clause = $set; 348 | } 349 | 350 | /** 351 | * 352 | * Rebuilds a condition string, replacing sequential placeholders with 353 | * named placeholders, and binding the sequential values to the named 354 | * placeholders. 355 | * 356 | * @param string $cond The condition with sequential placeholders. 357 | * 358 | * @param array $bind_values The values to bind to the sequential 359 | * placeholders under their named versions. 360 | * 361 | * @return string The rebuilt condition string. 362 | * 363 | */ 364 | protected function rebuildCondAndBindValues($cond, array $bind_values) 365 | { 366 | $index = 0; 367 | $selects = []; 368 | 369 | foreach ($bind_values as $key => $val) { 370 | if ($val instanceof SelectInterface) { 371 | $selects[":{$key}"] = $val; 372 | } elseif (is_array($val)) { 373 | $cond = $this->getCond($key, $cond, $val, $index); 374 | } else { 375 | $this->bindValue($key, $val); 376 | } 377 | $index++; 378 | } 379 | 380 | foreach ($selects as $key => $select) { 381 | $selects[$key] = $select->getStatement(); 382 | $this->bind_values = array_merge( 383 | $this->bind_values, 384 | $select->getBindValues() 385 | ); 386 | } 387 | 388 | $cond = strtr($cond, $selects); 389 | return $cond; 390 | } 391 | 392 | protected function inlineArray(array $array) 393 | { 394 | $keys = []; 395 | foreach ($array as $val) { 396 | $this->inlineCount++; 397 | $key = "__{$this->inlineCount}__"; 398 | $this->bindValue($key, $val); 399 | $keys[] = ":{$key}"; 400 | } 401 | return implode(', ', $keys); 402 | } 403 | 404 | /** 405 | * 406 | * Adds a column order to the query. 407 | * 408 | * @param array $spec The columns and direction to order by. 409 | * 410 | * @return $this 411 | * 412 | */ 413 | protected function addOrderBy(array $spec) 414 | { 415 | foreach ($spec as $col) { 416 | $this->order_by[] = $this->quoter->quoteNamesIn($col); 417 | } 418 | return $this; 419 | } 420 | 421 | /** 422 | * @param int|string $key 423 | * @param string $cond 424 | * @param array $val 425 | * @param int $index 426 | * 427 | * @return string 428 | */ 429 | private function getCond($key, $cond, array $val, $index) 430 | { 431 | if (is_string($key)) { 432 | return str_replace(':' . $key, $this->inlineArray($val), $cond); 433 | } 434 | assert(is_int($key)); 435 | 436 | if (preg_match_all('/\?/', $cond, $matches, PREG_OFFSET_CAPTURE) !== false) { 437 | return substr_replace($cond, $this->inlineArray($val), $matches[0][$index][1], 1); 438 | } 439 | 440 | return $cond; 441 | } 442 | } 443 | -------------------------------------------------------------------------------- /src/Common/Select.php: -------------------------------------------------------------------------------- 1 | union)) { 136 | $union = implode(PHP_EOL, $this->union) . PHP_EOL; 137 | } 138 | return $union . $this->build(); 139 | } 140 | 141 | /** 142 | * 143 | * Sets the number of rows per page. 144 | * 145 | * @param int $paging The number of rows to page at. 146 | * 147 | * @return $this 148 | * 149 | */ 150 | public function setPaging($paging) 151 | { 152 | $this->paging = (int) $paging; 153 | if ($this->page) { 154 | $this->setPagingLimitOffset(); 155 | } 156 | return $this; 157 | } 158 | 159 | /** 160 | * 161 | * Gets the number of rows per page. 162 | * 163 | * @return int The number of rows per page. 164 | * 165 | */ 166 | public function getPaging() 167 | { 168 | return $this->paging; 169 | } 170 | 171 | /** 172 | * 173 | * Makes the select FOR UPDATE (or not). 174 | * 175 | * @param bool $enable Whether or not the SELECT is FOR UPDATE (default 176 | * true). 177 | * 178 | * @return $this 179 | * 180 | */ 181 | public function forUpdate($enable = true) 182 | { 183 | $this->for_update = (bool) $enable; 184 | return $this; 185 | } 186 | 187 | /** 188 | * 189 | * Makes the select DISTINCT (or not). 190 | * 191 | * @param bool $enable Whether or not the SELECT is DISTINCT (default 192 | * true). 193 | * 194 | * @return $this 195 | * 196 | */ 197 | public function distinct($enable = true) 198 | { 199 | $this->setFlag('DISTINCT', $enable); 200 | return $this; 201 | } 202 | 203 | /** 204 | * 205 | * Is the select DISTINCT? 206 | * 207 | * @return bool 208 | * 209 | */ 210 | public function isDistinct() 211 | { 212 | return $this->hasFlag('DISTINCT'); 213 | } 214 | 215 | /** 216 | * 217 | * Adds columns to the query. 218 | * 219 | * Multiple calls to cols() will append to the list of columns, not 220 | * overwrite the previous columns. 221 | * 222 | * @param array $cols The column(s) to add to the query. The elements can be 223 | * any mix of these: `array("col", "col AS alias", "col" => "alias")` 224 | * 225 | * @return $this 226 | * 227 | */ 228 | public function cols(array $cols) 229 | { 230 | foreach ($cols as $key => $val) { 231 | $this->addCol($key, $val); 232 | } 233 | return $this; 234 | } 235 | 236 | /** 237 | * 238 | * Adds a column and alias to the columns to be selected. 239 | * 240 | * @param mixed $key If an integer, ignored. Otherwise, the column to be 241 | * added. 242 | * 243 | * @param mixed $val If $key was an integer, the column to be added; 244 | * otherwise, the column alias. 245 | * 246 | * @return null 247 | * 248 | */ 249 | protected function addCol($key, $val) 250 | { 251 | if (is_string($key)) { 252 | // [col => alias] 253 | $this->cols[$val] = $key; 254 | } else { 255 | $this->addColWithAlias($val); 256 | } 257 | } 258 | 259 | /** 260 | * 261 | * Adds a column with an alias to the columns to be selected. 262 | * 263 | * @param string $spec The column specification: "col alias", 264 | * "col AS alias", or something else entirely. 265 | * 266 | * @return null 267 | * 268 | */ 269 | protected function addColWithAlias($spec) 270 | { 271 | $parts = explode(' ', $spec); 272 | $count = count($parts); 273 | if ($count == 2) { 274 | // "col alias" 275 | $this->cols[$parts[1]] = $parts[0]; 276 | } elseif ($count == 3 && strtoupper($parts[1]) == 'AS') { 277 | // "col AS alias" 278 | $this->cols[$parts[2]] = $parts[0]; 279 | } else { 280 | // no recognized alias 281 | $this->cols[] = $spec; 282 | } 283 | } 284 | 285 | /** 286 | * 287 | * Remove a column via its alias. 288 | * 289 | * @param string $alias The column to remove 290 | * 291 | * @return bool 292 | * 293 | */ 294 | public function removeCol($alias) 295 | { 296 | if (isset($this->cols[$alias])) { 297 | unset($this->cols[$alias]); 298 | 299 | return true; 300 | } 301 | 302 | $index = array_search($alias, $this->cols); 303 | if ($index !== false) { 304 | unset($this->cols[$index]); 305 | return true; 306 | } 307 | 308 | return false; 309 | } 310 | 311 | /** 312 | * 313 | * Has the column or alias been added to the query? 314 | * 315 | * @param string $alias The column or alias to look for 316 | * 317 | * @return bool 318 | * 319 | */ 320 | public function hasCol($alias) 321 | { 322 | return isset($this->cols[$alias]) || array_search($alias, $this->cols) !== false; 323 | } 324 | 325 | /** 326 | * 327 | * Does the query have any columns in it? 328 | * 329 | * @return bool 330 | * 331 | */ 332 | public function hasCols() 333 | { 334 | return (bool) $this->cols; 335 | } 336 | 337 | /** 338 | * 339 | * Returns a list of columns. 340 | * 341 | * @return array 342 | * 343 | */ 344 | public function getCols() 345 | { 346 | return $this->cols; 347 | } 348 | 349 | /** 350 | * 351 | * Tracks table references. 352 | * 353 | * @param string $type FROM, JOIN, etc. 354 | * 355 | * @param string $spec The table and alias name. 356 | * 357 | * @return null 358 | * 359 | * @throws Exception when the reference has already been used. 360 | * 361 | */ 362 | protected function addTableRef($type, $spec) 363 | { 364 | $name = $spec; 365 | 366 | $pos = strripos($name, ' AS '); 367 | if ($pos !== false) { 368 | $name = trim(substr($name, $pos + 4)); 369 | } 370 | 371 | if (isset($this->table_refs[$name])) { 372 | $used = $this->table_refs[$name]; 373 | throw new Exception("Cannot reference '$type $spec' after '$used'"); 374 | } 375 | 376 | $this->table_refs[$name] = "$type $spec"; 377 | } 378 | 379 | /** 380 | * 381 | * Adds a FROM element to the query; quotes the table name automatically. 382 | * 383 | * @param string $spec The table specification; "foo" or "foo AS bar". 384 | * 385 | * @return $this 386 | * 387 | */ 388 | public function from($spec) 389 | { 390 | $this->addTableRef('FROM', $spec); 391 | return $this->addFrom($this->quoter->quoteName($spec)); 392 | } 393 | 394 | /** 395 | * 396 | * Adds a raw unquoted FROM element to the query; useful for adding FROM 397 | * elements that are functions. 398 | * 399 | * @param string $spec The table specification, e.g. "function_name()". 400 | * 401 | * @return $this 402 | * 403 | */ 404 | public function fromRaw($spec) 405 | { 406 | $this->addTableRef('FROM', $spec); 407 | return $this->addFrom($spec); 408 | } 409 | 410 | /** 411 | * 412 | * Adds to the $from property and increments the key count. 413 | * 414 | * @param string $spec The table specification. 415 | * 416 | * @return $this 417 | * 418 | */ 419 | protected function addFrom($spec) 420 | { 421 | $this->from[] = array($spec); 422 | $this->from_key ++; 423 | return $this; 424 | } 425 | 426 | /** 427 | * 428 | * Adds an aliased sub-select to the query. 429 | * 430 | * @param string|Select $spec If a Select object, use as the sub-select; 431 | * if a string, the sub-select string. 432 | * 433 | * @param string $name The alias name for the sub-select. 434 | * 435 | * @return $this 436 | * 437 | */ 438 | public function fromSubSelect($spec, $name) 439 | { 440 | $this->addTableRef('FROM (SELECT ...) AS', $name); 441 | $spec = $this->subSelect($spec, ' '); 442 | $name = $this->quoter->quoteName($name); 443 | return $this->addFrom("({$spec} ) AS $name"); 444 | } 445 | 446 | /** 447 | * 448 | * Formats a sub-SELECT statement, binding values from a Select object as 449 | * needed. 450 | * 451 | * @param string|SelectInterface $spec A sub-SELECT specification. 452 | * 453 | * @param string $indent Indent each line with this string. 454 | * 455 | * @return string The sub-SELECT string. 456 | * 457 | */ 458 | protected function subSelect($spec, $indent) 459 | { 460 | if ($spec instanceof SelectInterface) { 461 | $this->bindValues($spec->getBindValues()); 462 | } 463 | 464 | return PHP_EOL . $indent 465 | . ltrim(preg_replace('/^/m', $indent, (string) $spec)) 466 | . PHP_EOL; 467 | } 468 | 469 | /** 470 | * 471 | * Adds a JOIN table and columns to the query. 472 | * 473 | * @param string $join The join type: inner, left, natural, etc. 474 | * 475 | * @param string $spec The table specification; "foo" or "foo AS bar". 476 | * 477 | * @param string $cond Join on this condition. 478 | * 479 | * @param array $bind Values to bind to ?-placeholders in the condition. 480 | * 481 | * @return $this 482 | * 483 | * @throws Exception 484 | * 485 | */ 486 | public function join($join, $spec, $cond = null, array $bind = array()) 487 | { 488 | $join = strtoupper(ltrim("$join JOIN")); 489 | $this->addTableRef($join, $spec); 490 | 491 | $spec = $this->quoter->quoteName($spec); 492 | $cond = $this->fixJoinCondition($cond, $bind); 493 | return $this->addJoin(rtrim("$join $spec $cond")); 494 | } 495 | 496 | /** 497 | * 498 | * Fixes a JOIN condition to quote names in the condition and prefix it 499 | * with a condition type ('ON' is the default and 'USING' is recognized). 500 | * 501 | * @param string $cond Join on this condition. 502 | * 503 | * @param array $bind Values to bind to ?-placeholders in the condition. 504 | * 505 | * @return string 506 | * 507 | */ 508 | protected function fixJoinCondition($cond, array $bind) 509 | { 510 | if (! $cond) { 511 | return ''; 512 | } 513 | 514 | $cond = $this->quoter->quoteNamesIn($cond); 515 | $cond = $this->rebuildCondAndBindValues($cond, $bind); 516 | 517 | if (strtoupper(substr(ltrim($cond), 0, 3)) == 'ON ') { 518 | return $cond; 519 | } 520 | 521 | if (strtoupper(substr(ltrim($cond), 0, 6)) == 'USING ') { 522 | return $cond; 523 | } 524 | 525 | return 'ON ' . $cond; 526 | } 527 | 528 | /** 529 | * 530 | * Adds a INNER JOIN table and columns to the query. 531 | * 532 | * @param string $spec The table specification; "foo" or "foo AS bar". 533 | * 534 | * @param string $cond Join on this condition. 535 | * 536 | * @param array $bind Values to bind to ?-placeholders in the condition. 537 | * 538 | * @return $this 539 | * 540 | * @throws Exception 541 | * 542 | */ 543 | public function innerJoin($spec, $cond = null, array $bind = array()) 544 | { 545 | return $this->join('INNER', $spec, $cond, $bind); 546 | } 547 | 548 | /** 549 | * 550 | * Adds a LEFT JOIN table and columns to the query. 551 | * 552 | * @param string $spec The table specification; "foo" or "foo AS bar". 553 | * 554 | * @param string $cond Join on this condition. 555 | * 556 | * @param array $bind Values to bind to ?-placeholders in the condition. 557 | * 558 | * @return $this 559 | * 560 | * @throws Exception 561 | * 562 | */ 563 | public function leftJoin($spec, $cond = null, array $bind = array()) 564 | { 565 | return $this->join('LEFT', $spec, $cond, $bind); 566 | } 567 | 568 | /** 569 | * 570 | * Adds a JOIN to an aliased subselect and columns to the query. 571 | * 572 | * @param string $join The join type: inner, left, natural, etc. 573 | * 574 | * @param string|Select $spec If a Select 575 | * object, use as the sub-select; if a string, the sub-select 576 | * command string. 577 | * 578 | * @param string $name The alias name for the sub-select. 579 | * 580 | * @param string $cond Join on this condition. 581 | * 582 | * @param array $bind Values to bind to ?-placeholders in the condition. 583 | * 584 | * @return $this 585 | * 586 | * @throws Exception 587 | * 588 | */ 589 | public function joinSubSelect($join, $spec, $name, $cond = null, array $bind = array()) 590 | { 591 | $join = strtoupper(ltrim("$join JOIN")); 592 | $this->addTableRef("$join (SELECT ...) AS", $name); 593 | 594 | $spec = $this->subSelect($spec, ' '); 595 | $name = $this->quoter->quoteName($name); 596 | $cond = $this->fixJoinCondition($cond, $bind); 597 | 598 | $text = rtrim("$join ($spec ) AS $name $cond"); 599 | return $this->addJoin(' ' . $text); 600 | } 601 | 602 | /** 603 | * 604 | * Adds the JOIN to the right place, given whether or not a FROM has been 605 | * specified yet. 606 | * 607 | * @param string $spec The JOIN clause. 608 | * 609 | * @return $this 610 | * 611 | */ 612 | protected function addJoin($spec) 613 | { 614 | $from_key = ($this->from_key == -1) ? 0 : $this->from_key; 615 | $this->join[$from_key][] = $spec; 616 | return $this; 617 | } 618 | 619 | /** 620 | * 621 | * Adds grouping to the query. 622 | * 623 | * @param array $spec The column(s) to group by. 624 | * 625 | * @return $this 626 | * 627 | */ 628 | public function groupBy(array $spec) 629 | { 630 | foreach ($spec as $col) { 631 | $this->group_by[] = $this->quoter->quoteNamesIn($col); 632 | } 633 | return $this; 634 | } 635 | 636 | /** 637 | * 638 | * Adds a HAVING condition to the query by AND. 639 | * 640 | * @param string $cond The HAVING condition. 641 | * 642 | * @param array $bind arguments to bind to placeholders 643 | * 644 | * @return $this 645 | * 646 | */ 647 | public function having($cond, array $bind = []) 648 | { 649 | $this->addClauseCondWithBind('having', 'AND', $cond, $bind); 650 | return $this; 651 | } 652 | 653 | /** 654 | * 655 | * Adds a HAVING condition to the query by OR. 656 | * 657 | * @param string $cond The HAVING condition. 658 | * 659 | * @param array $bind arguments to bind to placeholders 660 | * 661 | * @return $this 662 | * 663 | * @see having() 664 | * 665 | */ 666 | public function orHaving($cond, array $bind = []) 667 | { 668 | $this->addClauseCondWithBind('having', 'OR', $cond, $bind); 669 | return $this; 670 | } 671 | 672 | /** 673 | * 674 | * Sets the limit and count by page number. 675 | * 676 | * @param int $page Limit results to this page number. 677 | * 678 | * @return $this 679 | * 680 | */ 681 | public function page($page) 682 | { 683 | $this->page = (int) $page; 684 | $this->setPagingLimitOffset(); 685 | return $this; 686 | } 687 | 688 | /** 689 | * 690 | * Updates the limit and offset values when changing pagination. 691 | * 692 | * @return null 693 | * 694 | */ 695 | protected function setPagingLimitOffset() 696 | { 697 | $this->setLimit(0); 698 | $this->setOffset(0); 699 | if ($this->page) { 700 | $this->setLimit($this->paging); 701 | $this->setOffset($this->paging * ($this->page - 1)); 702 | } 703 | } 704 | 705 | /** 706 | * 707 | * Returns the page number being selected. 708 | * 709 | * @return int 710 | * 711 | */ 712 | public function getPage() 713 | { 714 | return $this->page; 715 | } 716 | 717 | /** 718 | * 719 | * Takes the current select properties and retains them, then sets 720 | * UNION for the next set of properties. 721 | * 722 | * @return $this 723 | * 724 | */ 725 | public function union() 726 | { 727 | $this->union[] = $this->build() . PHP_EOL . 'UNION'; 728 | $this->reset(); 729 | return $this; 730 | } 731 | 732 | /** 733 | * 734 | * Takes the current select properties and retains them, then sets 735 | * UNION ALL for the next set of properties. 736 | * 737 | * @return $this 738 | * 739 | */ 740 | public function unionAll() 741 | { 742 | $this->union[] = $this->build() . PHP_EOL . 'UNION ALL'; 743 | $this->reset(); 744 | return $this; 745 | } 746 | 747 | /** 748 | * 749 | * Clears the current select properties; generally used after adding a 750 | * union. 751 | * 752 | * @return null 753 | * 754 | */ 755 | public function reset() 756 | { 757 | $this->resetFlags(); 758 | $this->resetCols(); 759 | $this->resetTables(); 760 | $this->resetWhere(); 761 | $this->resetGroupBy(); 762 | $this->resetHaving(); 763 | $this->resetOrderBy(); 764 | $this->limit(0); 765 | $this->offset(0); 766 | $this->page(0); 767 | $this->forUpdate(false); 768 | } 769 | 770 | /** 771 | * 772 | * Resets the columns on the SELECT. 773 | * 774 | * @return $this 775 | * 776 | */ 777 | public function resetCols() 778 | { 779 | $this->cols = array(); 780 | return $this; 781 | } 782 | 783 | /** 784 | * 785 | * Resets the FROM and JOIN clauses on the SELECT. 786 | * 787 | * @return $this 788 | * 789 | */ 790 | public function resetTables() 791 | { 792 | $this->from = array(); 793 | $this->from_key = -1; 794 | $this->join = array(); 795 | $this->table_refs = array(); 796 | return $this; 797 | } 798 | 799 | /** 800 | * 801 | * Resets the WHERE clause on the SELECT. 802 | * 803 | * @return $this 804 | * 805 | */ 806 | public function resetWhere() 807 | { 808 | $this->where = array(); 809 | return $this; 810 | } 811 | 812 | /** 813 | * 814 | * Resets the GROUP BY clause on the SELECT. 815 | * 816 | * @return $this 817 | * 818 | */ 819 | public function resetGroupBy() 820 | { 821 | $this->group_by = array(); 822 | return $this; 823 | } 824 | 825 | /** 826 | * 827 | * Resets the HAVING clause on the SELECT. 828 | * 829 | * @return $this 830 | * 831 | */ 832 | public function resetHaving() 833 | { 834 | $this->having = array(); 835 | return $this; 836 | } 837 | 838 | /** 839 | * 840 | * Resets the ORDER BY clause on the SELECT. 841 | * 842 | * @return $this 843 | * 844 | */ 845 | public function resetOrderBy() 846 | { 847 | $this->order_by = array(); 848 | return $this; 849 | } 850 | 851 | /** 852 | * 853 | * Resets the UNION and UNION ALL clauses on the SELECT. 854 | * 855 | * @return $this 856 | * 857 | */ 858 | public function resetUnions() 859 | { 860 | $this->union = array(); 861 | return $this; 862 | } 863 | 864 | /** 865 | * 866 | * Builds this query object into a string. 867 | * 868 | * @return string 869 | * 870 | */ 871 | protected function build() 872 | { 873 | $cols = array(); 874 | foreach ($this->cols as $key => $val) { 875 | if (is_int($key)) { 876 | $cols[] = $this->quoter->quoteNamesIn($val); 877 | } else { 878 | $cols[] = $this->quoter->quoteNamesIn("$val AS $key"); 879 | } 880 | } 881 | 882 | return 'SELECT' 883 | . $this->builder->buildFlags($this->flags) 884 | . $this->builder->buildCols($cols) 885 | . $this->builder->buildFrom($this->from, $this->join) 886 | . $this->builder->buildWhere($this->where) 887 | . $this->builder->buildGroupBy($this->group_by) 888 | . $this->builder->buildHaving($this->having) 889 | . $this->builder->buildOrderBy($this->order_by) 890 | . $this->builder->buildLimitOffset($this->limit, $this->offset) 891 | . $this->builder->buildForUpdate($this->for_update); 892 | } 893 | 894 | /** 895 | * 896 | * Sets a limit count on the query. 897 | * 898 | * @param int $limit The number of rows to select. 899 | * 900 | * @return $this 901 | * 902 | */ 903 | public function limit($limit) 904 | { 905 | $this->setLimit($limit); 906 | if ($this->page) { 907 | $this->page = 0; 908 | $this->setOffset(0); 909 | } 910 | return $this; 911 | } 912 | 913 | /** 914 | * 915 | * Sets a limit offset on the query. 916 | * 917 | * @param int $offset Start returning after this many rows. 918 | * 919 | * @return $this 920 | * 921 | */ 922 | public function offset($offset) 923 | { 924 | $this->setOffset($offset); 925 | if ($this->page) { 926 | $this->page = 0; 927 | $this->setLimit(0); 928 | } 929 | return $this; 930 | } 931 | 932 | /** 933 | * 934 | * Adds a column order to the query. 935 | * 936 | * @param array $spec The columns and direction to order by. 937 | * 938 | * @return $this 939 | * 940 | */ 941 | public function orderBy(array $spec) 942 | { 943 | return $this->addOrderBy($spec); 944 | } 945 | } 946 | --------------------------------------------------------------------------------