├── .gitattributes ├── .github └── workflows │ ├── ci-mssql.yml │ ├── ci-mysql.yml │ ├── ci-pgsql.yml │ └── main.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── composer.json ├── phpunit.xml └── src ├── ColumnInterface.php ├── Config ├── DatabaseConfig.php └── DatabasePartial.php ├── Database.php ├── DatabaseInterface.php ├── DatabaseManager.php ├── DatabaseProviderInterface.php ├── Driver ├── CachingCompilerInterface.php ├── Compiler.php ├── CompilerCache.php ├── CompilerInterface.php ├── Driver.php ├── DriverInterface.php ├── Handler.php ├── HandlerInterface.php ├── MySQL │ ├── Exception │ │ └── MySQLException.php │ ├── MySQLCompiler.php │ ├── MySQLDriver.php │ ├── MySQLHandler.php │ └── Schema │ │ ├── MySQLColumn.php │ │ ├── MySQLForeignKey.php │ │ ├── MySQLIndex.php │ │ └── MySQLTable.php ├── Postgres │ ├── PostgresCompiler.php │ ├── PostgresDriver.php │ ├── PostgresHandler.php │ ├── Query │ │ ├── PostgresInsertQuery.php │ │ └── PostgresSelectQuery.php │ └── Schema │ │ ├── PostgresColumn.php │ │ ├── PostgresForeignKey.php │ │ ├── PostgresIndex.php │ │ └── PostgresTable.php ├── Quoter.php ├── ReadonlyHandler.php ├── SQLServer │ ├── SQLServerCompiler.php │ ├── SQLServerDriver.php │ ├── SQLServerHandler.php │ └── Schema │ │ ├── SQLServerColumn.php │ │ ├── SQLServerIndex.php │ │ ├── SQLServerTable.php │ │ └── SQlServerForeignKey.php ├── SQLite │ ├── SQLiteCompiler.php │ ├── SQLiteDriver.php │ ├── SQLiteHandler.php │ └── Schema │ │ ├── SQLiteColumn.php │ │ ├── SQLiteForeignKey.php │ │ ├── SQLiteIndex.php │ │ └── SQLiteTable.php └── Statement.php ├── Exception ├── BuilderException.php ├── CompilerException.php ├── ConfigException.php ├── DBALException.php ├── DatabaseException.php ├── DefaultValueException.php ├── DriverException.php ├── HandlerException.php ├── InterpolatorException.php ├── ReadonlyConnectionException.php ├── SchemaException.php ├── StatementException.php ├── StatementException │ ├── ConnectionException.php │ └── ConstrainException.php └── StatementExceptionInterface.php ├── ForeignKeyInterface.php ├── IndexInterface.php ├── Injection ├── Expression.php ├── Fragment.php ├── FragmentInterface.php ├── Parameter.php ├── ParameterInterface.php └── ValueInterface.php ├── Query ├── ActiveQuery.php ├── BuilderInterface.php ├── DeleteQuery.php ├── InsertQuery.php ├── Interpolator.php ├── QueryBuilder.php ├── QueryInterface.php ├── QueryParameters.php ├── SelectQuery.php ├── Traits │ ├── HavingTrait.php │ ├── JoinTrait.php │ ├── TokenTrait.php │ └── WhereTrait.php └── UpdateQuery.php ├── Schema ├── AbstractColumn.php ├── AbstractForeignKey.php ├── AbstractIndex.php ├── AbstractTable.php ├── Comparator.php ├── ComparatorInterface.php ├── ElementInterface.php ├── Reflector.php ├── State.php └── Traits │ └── ElementTrait.php ├── StatementInterface.php ├── Table.php └── TableInterface.php /.gitattributes: -------------------------------------------------------------------------------- 1 | /tests export-ignore -------------------------------------------------------------------------------- /.github/workflows/ci-mssql.yml: -------------------------------------------------------------------------------- 1 | on: 2 | - pull_request 3 | - push 4 | 5 | name: ci-mssql 6 | 7 | jobs: 8 | tests: 9 | name: PHP ${{ matrix.php }}-mssql-${{ matrix.mssql }} 10 | 11 | env: 12 | key: cache 13 | 14 | runs-on: ubuntu-latest 15 | 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | include: 20 | - php: '7.2' 21 | extensions: pdo, pdo_sqlsrv-5.8.1 22 | mssql: 'server:2017-latest' 23 | - php: '7.3' 24 | extensions: pdo, pdo_sqlsrv-5.8.1 25 | mssql: 'server:2017-latest' 26 | - php: '7.4' 27 | extensions: pdo, pdo_sqlsrv 28 | mssql: 'server:2017-latest' 29 | - php: '7.4' 30 | extensions: pdo, pdo_sqlsrv 31 | mssql: 'server:2019-latest' 32 | - php: '8.0' 33 | extensions: pdo, pdo_sqlsrv 34 | mssql: 'server:2017-latest' 35 | - php: '8.0' 36 | extensions: pdo, pdo_sqlsrv 37 | mssql: 'server:2019-latest' 38 | 39 | services: 40 | mssql: 41 | image: mcr.microsoft.com/mssql/${{ matrix.mssql }} 42 | env: 43 | SA_PASSWORD: SSpaSS__1 44 | ACCEPT_EULA: Y 45 | MSSQL_PID: Developer 46 | ports: 47 | - 11433:1433 48 | options: --name=mssql --health-cmd="/opt/mssql-tools/bin/sqlcmd -S localhost -U SA -P 'SSpaSS__1' -Q 'SELECT 1'" --health-interval=10s --health-timeout=5s --health-retries=3 49 | 50 | steps: 51 | - name: Checkout 52 | uses: actions/checkout@v2.3.4 53 | 54 | - name: Install PHP with extensions 55 | uses: shivammathur/setup-php@v2 56 | with: 57 | php-version: ${{ matrix.php }} 58 | extensions: ${{ matrix.extensions }} 59 | ini-values: date.timezone='UTC' 60 | tools: composer:v2, pecl 61 | 62 | - name: Determine composer cache directory on Linux 63 | run: echo "COMPOSER_CACHE_DIR=$(composer config cache-dir)" >> $GITHUB_ENV 64 | 65 | - name: Cache dependencies installed with composer 66 | uses: actions/cache@v2 67 | with: 68 | path: ${{ env.COMPOSER_CACHE_DIR }} 69 | key: php${{ matrix.php }}-composer-${{ hashFiles('**/composer.json') }} 70 | restore-keys: | 71 | php${{ matrix.php }}-composer- 72 | 73 | - name: Update composer 74 | run: composer self-update 75 | 76 | - name: Install dependencies with composer 77 | run: composer update --prefer-dist --no-interaction --no-progress --optimize-autoloader --ansi 78 | 79 | - name: Install dependencies with composer php 8.0 80 | if: matrix.php == '8.0' 81 | run: composer update --ignore-platform-reqs --prefer-dist --no-interaction --no-progress --optimize-autoloader --ansi 82 | 83 | - name: Run tests with phpunit without coverage 84 | env: 85 | DB: sqlserver 86 | run: vendor/bin/phpunit --group driver-sqlserver --colors=always 87 | -------------------------------------------------------------------------------- /.github/workflows/ci-mysql.yml: -------------------------------------------------------------------------------- 1 | on: 2 | - pull_request 3 | - push 4 | 5 | name: ci-mysql 6 | 7 | jobs: 8 | tests: 9 | name: PHP ${{ matrix.php-version }}-mysql-${{ matrix.mysql-version }} 10 | env: 11 | extensions: curl, intl, pdo, pdo_mysql 12 | key: cache-v1 13 | 14 | runs-on: ${{ matrix.os }} 15 | 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | os: 20 | - ubuntu-latest 21 | 22 | php-version: 23 | - "7.4" 24 | - "8.0" 25 | 26 | mysql-version: 27 | - "5.7" 28 | - "8.0" 29 | 30 | services: 31 | mysql: 32 | image: mysql:${{ matrix.mysql-version }} 33 | env: 34 | MYSQL_ROOT_PASSWORD: root 35 | MYSQL_DATABASE: spiral 36 | MYSQL_AUTHENTICATION_PLUGIN: mysql_native_password 37 | ports: 38 | - 13306:3306 39 | options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 40 | 41 | steps: 42 | - name: Checkout 43 | uses: actions/checkout@v2 44 | 45 | - name: Setup cache environment 46 | id: cache-env 47 | uses: shivammathur/cache-extensions@v1 48 | with: 49 | php-version: ${{ matrix.php-version }} 50 | extensions: ${{ env.extensions }} 51 | key: ${{ env.key }} 52 | 53 | - name: Cache extensions 54 | uses: actions/cache@v1 55 | with: 56 | path: ${{ steps.cache-env.outputs.dir }} 57 | key: ${{ steps.cache-env.outputs.key }} 58 | restore-keys: ${{ steps.cache-env.outputs.key }} 59 | 60 | - name: Install PHP with extensions 61 | uses: shivammathur/setup-php@v2 62 | with: 63 | php-version: ${{ matrix.php-version }} 64 | extensions: ${{ env.extensions }} 65 | ini-values: date.timezone='UTC' 66 | coverage: pcov 67 | 68 | - name: Determine composer cache directory 69 | if: matrix.os == 'ubuntu-latest' 70 | run: echo "COMPOSER_CACHE_DIR=$(composer config cache-dir)" >> $GITHUB_ENV 71 | 72 | - name: Cache dependencies installed with composer 73 | uses: actions/cache@v1 74 | with: 75 | path: ${{ env.COMPOSER_CACHE_DIR }} 76 | key: php${{ matrix.php-version }}-composer-${{ matrix.dependencies }}-${{ hashFiles('**/composer.json') }} 77 | restore-keys: | 78 | php${{ matrix.php-version }}-composer-${{ matrix.dependencies }}- 79 | 80 | - name: Install dependencies with composer 81 | run: composer update --prefer-dist --no-interaction --no-progress --optimize-autoloader --ansi 82 | 83 | - name: Run mysql tests with phpunit 84 | env: 85 | DB: mysql 86 | MYSQL: ${{ matrix.mysql-version }} 87 | run: vendor/bin/phpunit --group driver-mysql --colors=always 88 | -------------------------------------------------------------------------------- /.github/workflows/ci-pgsql.yml: -------------------------------------------------------------------------------- 1 | on: 2 | - pull_request 3 | - push 4 | 5 | name: ci-pgsql 6 | 7 | jobs: 8 | tests: 9 | name: PHP ${{ matrix.php-version }}-pgsql-${{ matrix.pgsql-version }} 10 | env: 11 | extensions: curl, intl, pdo, pdo_pgsql 12 | key: cache-v1 13 | 14 | runs-on: ${{ matrix.os }} 15 | 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | os: 20 | - ubuntu-latest 21 | php-version: 22 | - "7.2" 23 | - "7.3" 24 | - "7.4" 25 | - "8.0" 26 | 27 | pgsql-version: 28 | - "10" 29 | - "11" 30 | - "12" 31 | - "13" 32 | 33 | services: 34 | postgres: 35 | image: postgres:${{ matrix.pgsql-version }} 36 | env: 37 | POSTGRES_USER: postgres 38 | POSTGRES_PASSWORD: postgres 39 | POSTGRES_DB: spiral 40 | ports: 41 | - 15432:5432 42 | options: --name=postgres --health-cmd="pg_isready" --health-interval=10s --health-timeout=5s --health-retries=3 43 | 44 | steps: 45 | - name: Checkout 46 | uses: actions/checkout@v2 47 | 48 | - name: Setup cache environment 49 | id: cache-env 50 | uses: shivammathur/cache-extensions@v1 51 | with: 52 | php-version: ${{ matrix.php-version }} 53 | extensions: ${{ env.extensions }} 54 | key: ${{ env.key }} 55 | 56 | - name: Cache extensions 57 | uses: actions/cache@v1 58 | with: 59 | path: ${{ steps.cache-env.outputs.dir }} 60 | key: ${{ steps.cache-env.outputs.key }} 61 | restore-keys: ${{ steps.cache-env.outputs.key }} 62 | 63 | - name: Install PHP with extensions 64 | uses: shivammathur/setup-php@v2 65 | with: 66 | php-version: ${{ matrix.php-version }} 67 | extensions: ${{ env.extensions }} 68 | ini-values: date.timezone='UTC' 69 | coverage: pcov 70 | 71 | - name: Determine composer cache directory 72 | if: matrix.os == 'ubuntu-latest' 73 | run: echo "COMPOSER_CACHE_DIR=$(composer config cache-dir)" >> $GITHUB_ENV 74 | 75 | - name: Cache dependencies installed with composer 76 | uses: actions/cache@v1 77 | with: 78 | path: ${{ env.COMPOSER_CACHE_DIR }} 79 | key: php${{ matrix.php-version }}-composer-${{ matrix.dependencies }}-${{ hashFiles('**/composer.json') }} 80 | restore-keys: | 81 | php${{ matrix.php-version }}-composer-${{ matrix.dependencies }}- 82 | 83 | - name: Install dependencies with composer 84 | run: composer update --prefer-dist --no-interaction --no-progress --optimize-autoloader --ansi 85 | 86 | - name: Run pgsql tests with phpunit 87 | env: 88 | DB: postgres 89 | POSTGRES: ${{ matrix.pgsql-version }} 90 | run: vendor/bin/phpunit --group driver-postgres --colors=always 91 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | lint: 7 | name: Check coding standards 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout 11 | uses: actions/checkout@v2 12 | - name: Get Composer Cache Directory 13 | id: composer-cache 14 | run: echo "::set-output name=dir::$(composer config cache-files-dir)" 15 | - name: Restore Composer Cache 16 | uses: actions/cache@v1 17 | with: 18 | path: ${{ steps.composer-cache.outputs.dir }} 19 | key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} 20 | restore-keys: ${{ runner.os }}-composer- 21 | - name: Install Dependencies 22 | run: composer install --no-interaction --prefer-dist 23 | - name: Check CS 24 | run: vendor/bin/spiral-cs check src tests 25 | test: 26 | needs: lint 27 | name: Test PHP ${{ matrix.php-versions }} with Code Coverage 28 | runs-on: ubuntu-latest 29 | strategy: 30 | matrix: 31 | php-versions: ['7.2', '7.4', '8.0'] 32 | steps: 33 | - name: Checkout 34 | uses: actions/checkout@v2 35 | - name: Setup DB services 36 | run: | 37 | cd tests 38 | docker-compose up -d 39 | cd .. 40 | - name: Setup PHP ${{ matrix.php-versions }} 41 | uses: shivammathur/setup-php@v2 42 | with: 43 | php-version: ${{ matrix.php-versions }} 44 | coverage: pcov 45 | tools: pecl 46 | extensions: mbstring, pdo, pdo_sqlite, pdo_pgsql, pdo_sqlsrv, pdo_mysql 47 | - name: Get Composer Cache Directory 48 | id: composer-cache 49 | run: echo "::set-output name=dir::$(composer config cache-files-dir)" 50 | - name: Restore Composer Cache 51 | uses: actions/cache@v1 52 | with: 53 | path: ${{ steps.composer-cache.outputs.dir }} 54 | key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} 55 | restore-keys: ${{ runner.os }}-composer- 56 | - name: Install Dependencies 57 | run: composer install --no-interaction --prefer-dist 58 | - name: Execute Tests 59 | run: | 60 | vendor/bin/phpunit --coverage-clover=coverage.xml 61 | - name: Upload coverage to Codecov 62 | continue-on-error: true # if is fork 63 | uses: codecov/codecov-action@v1 64 | with: 65 | token: ${{ secrets.CODECOV_TOKEN }} 66 | file: ./coverage.xml 67 | 68 | sqlite: 69 | name: SQLite PHP ${{ matrix.php-versions }} 70 | runs-on: ubuntu-latest 71 | strategy: 72 | matrix: 73 | php-versions: ['7.2', '7.3', '7.4', '8.0'] 74 | steps: 75 | - name: Checkout 76 | uses: actions/checkout@v2 77 | - name: Setup PHP ${{ matrix.php-versions }} 78 | uses: shivammathur/setup-php@v1 79 | with: 80 | php-version: ${{ matrix.php-versions }} 81 | coverage: pcov 82 | tools: pecl 83 | extensions: mbstring, pdo, pdo_sqlite 84 | - name: Get Composer Cache Directory 85 | id: composer-cache 86 | run: echo "::set-output name=dir::$(composer config cache-files-dir)" 87 | - name: Restore Composer Cache 88 | uses: actions/cache@v1 89 | with: 90 | path: ${{ steps.composer-cache.outputs.dir }} 91 | key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} 92 | restore-keys: ${{ runner.os }}-composer- 93 | - name: Install Dependencies 94 | run: composer install --no-interaction --prefer-dist 95 | - name: Execute Tests 96 | env: 97 | DB: sqlite 98 | run: | 99 | vendor/bin/phpunit --group driver-sqlite --colors=always 100 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .env 3 | .coveralls.yml 4 | composer.lock 5 | vendor/ 6 | tests/runtime/* 7 | build/logs/* 8 | build/ 9 | *.db 10 | clover.xml 11 | clover.json 12 | .php_cs.cache 13 | .phpunit.result.cache 14 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | CHANGELOG 2 | ====================== 3 | 4 | v2.7.20 (20.11.2020) 5 | ----- 6 | - [bugfix] invaliding spacing while using Fragments in JOIN ON statements by @iamsaint 7 | - [bugfix] disable parameter registration when value is Fragment by @thenotsoft 8 | 9 | v2.7.19 (10.11.2020) 10 | ----- 11 | - added the ability to pass parameters into fragments 12 | - added the ability to use fragments and expressions as part of select query columns 13 | 14 | v2.7.18 (14.10.2020) 15 | ----- 16 | - added the ability to fetch data as StdClass by @guilhermeaiolfi 17 | 18 | v2.7.17 (02.09.2020) 19 | ----- 20 | - added the ability to modify the database logger context by @Alexsisukin 21 | - added distinctOn method to Postgres Select Query 22 | 23 | v2.7.16 (28.08.2020) 24 | ----- 25 | - fixes bug with invalid transaction level when transaction can't be started 26 | - set isolation level after beginning the transaction for Postgres 27 | 28 | v2.7.15 (17.06.2020) 29 | ----- 30 | - handle Docker specific connection exceptions (broken pipe) 31 | 32 | v2.7.14 (23.04.2020) 33 | ----- 34 | - fixed bug with invalid compilation of multi-group-by statements by @yiiliveext 35 | 36 | v2.7.13 (04.04.2020) 37 | ----- 38 | - improved legacy config resolution (invalid `options` parsing) 39 | 40 | v2.7.12 (31.03.2020) 41 | ----- 42 | - default `json` type for Postgres fallbacks to text to unify with other drivers 43 | 44 | v2.7.11 (12.03.2020) 45 | ----- 46 | - Add PostgreSQL `timestamptz` mapping for `timestamp with time zone` by @rauanmayemir 47 | 48 | v2.7.10 (18.02.2020) 49 | ----- 50 | - catch postgres EOF exceptions on amazon as connection exception 51 | 52 | v2.7.9 (18.02.2020) 53 | ----- 54 | - added the ability to pass parameters into Expression in operators and values 55 | 56 | v2.7.8 (18.02.2020) 57 | ----- 58 | - added the ability to pass parameters into Expression 59 | 60 | v2.7.7 (11.02.2020) 61 | ----- 62 | - minor refactor in PostgresInsertQuery 63 | 64 | v2.7.6 (07.02.2020) 65 | ----- 66 | - added the support to force the returning key in Postgres insert queries 67 | 68 | v2.7.5 (03.02.2020) 69 | ----- 70 | - [bugfix] fixed invalid index introspection on legacy SQLite drivers 71 | 72 | v2.7.4 (30.01.2020) 73 | ----- 74 | - [bugfix] fixed `syncTable` behavious for SQLite tables with sorted indexes @rauanmayemir 75 | 76 | v2.7.3 (29.01.2020) 77 | ----- 78 | - added the ability to specify index direction by @rauanmayemir 79 | 80 | v2.7.2 (18.01.2020) 81 | ----- 82 | - [bugfix] invalid size detection for int, bigint, tinyint columns under latest MySQL 8.0+ 83 | 84 | 2.7.1 (14.01.2020) 85 | ----- 86 | - added AbstractColumn::getSize() typecasting 87 | - added the ability to serialize and de-serialize fragments and expressions 88 | 89 | 2.7.0 (13.01.2020) 90 | ----- 91 | - added sql compiler caching, up to 5x times faster query generation 92 | - added prepared statement caching 93 | - refactor of SchemaHandler 94 | - refactor of Query builders 95 | - added ComparatorInterface 96 | - deprecated MySQL 5.5 support 97 | 98 | 2.6.10 (26.12.2019) 99 | ----- 100 | - [bugfix] invalid change detection for nullable and zeroed default values 101 | - do not allow default values for text and blob columns of MySQL 102 | 103 | 2.6.9 (26.12.2019) 104 | ----- 105 | - added support for Postgres 12 updated constrain schemas 106 | 107 | 2.6.8 (24.12.2019) 108 | ----- 109 | - [bufgix] proper abstract type detection for primary UUID columns for SQLite driver 110 | 111 | 2.6.7 (23.12.2019) 112 | ----- 113 | - [bufgix] proper exception type for syntax errors in MariaDB (previously was ConnectionException) 114 | 115 | 2.6.6 (11.12.2019) 116 | ----- 117 | - allow drivers to handle low level error exceptions 118 | - qualify "Connection reset by peer" as connection exception 119 | - fixed interpolation of named parameters 120 | 121 | 2.6.5 (11.12.2019) 122 | ----- 123 | - added support for SELECT FOR UPDATE statements 124 | 125 | 2.6.4 (21.11.2019) 126 | ----- 127 | - disabled int typecasting for aggregate selections 128 | - minor inspection driven improvements 129 | 130 | 2.6.3 (20.11.2019) 131 | ----- 132 | - improved connection exception handling for Postgres 133 | 134 | 2.6.2 (14.11.2019) 135 | ----- 136 | - added native support for UUID type 137 | 138 | 2.6.1 (05.11.2019) 139 | ----- 140 | - force the database disconned in case of connection error 141 | 142 | 2.6.0 (08.10.2019) 143 | ----- 144 | - minimum PHP version is set as 7.2 145 | - added internal method to get declared column type 146 | - added support for `jsonb` in Postgres driver 147 | 148 | 2.5.1 (14.09.2019) 149 | ----- 150 | - statement cache is turned off by default 151 | - cacheStatement flag can be passed from Database 152 | 153 | 2.5.0 (14.09.2019) 154 | ----- 155 | - Drivers now able to reuse prepared statements inside the transaction scope 156 | - minor performance improvemenet on larger transactions 157 | 158 | 2.4.5 (28.08.2019) 159 | ----- 160 | - improved SQLite multi-insert query fallback 161 | - all query builders can be used without driver as standalone objects 162 | - memory and performance optimizations for query builders 163 | - simplified parameter flattening logic, parameters are now assembled via compiler 164 | 165 | 2.4.2 (26.08.2019) 166 | ----- 167 | - IS NULL and IS NOT NULL normalized across all database drivers 168 | 169 | 2.4.1 (13.08.2019) 170 | ----- 171 | - CS: @invisible renamed to @internal 172 | 173 | 2.4.0 (29.07.2019) 174 | ----- 175 | - added support for composite FKs 176 | 177 | 2.3.1 (15.07.2019) 178 | ----- 179 | - handle MySQL server has gone away messages when PDO exception code is invalid 180 | 181 | 2.3.0 (10.05.2019) 182 | ----- 183 | - the Statement class has been decoupled from PDO 184 | 185 | 2.2.5 (08.05.2019) 186 | ----- 187 | - proper table alias resolution when the joined table name is similar to the alias of another table 188 | 189 | 2.2.3 (24.04.2019) 190 | ----- 191 | - PSR-12 192 | - added incomplete sort for Reflector 193 | 194 | 2.2.2 (16.04.2019) 195 | ----- 196 | - added DatabaseProviderInterface 197 | 198 | 2.2.1 (08.04.2019) 199 | ----- 200 | - extended syntax for IS NULL and NOT NULL for SQLite 201 | 202 | 2.2.0 (29.04.2019) 203 | ----- 204 | - drivers can now automatically reconnect in case of connection interruption 205 | 206 | 2.1.8 (21.02.2019) 207 | ----- 208 | - phpType method renamed to getType 209 | - getType renamed to getInternalType 210 | 211 | 2.1.7 (11.02.2019) 212 | ----- 213 | - simpler pagination logic 214 | - simplified pagination interfaces 215 | - simplified logger interfaces 216 | - less dependencies 217 | 218 | 2.0.0 (21.09.2018) 219 | ----- 220 | - massive refactor 221 | - decoupling from Spiral\Component 222 | - no more additional dependencies on ContainerInterface 223 | - support for read/write database connections 224 | - more flexible configuration 225 | - less dependencies between classes 226 | - interfaces have been exposed for table, column, index and foreignKeys 227 | - new interface for driver, database, table, compiler and handler 228 | - immutable quoter 229 | - more tests 230 | - custom exceptions for connection and constrain exceptions 231 | 232 | 1.0.1 (15.06.2018) 233 | ----- 234 | - MySQL driver can reconnect now 235 | 236 | 1.0.0 (02.03.2018) 237 | ----- 238 | * Improved handling of renamed indexes associated with renamed columns 239 | 240 | 0.9.1 (07.02.2017) 241 | ----- 242 | * Pagination split into separate package 243 | 244 | 0.9.0 (03.02.2017) 245 | ----- 246 | * DBAL, Pagination and Migration component split from component repository 247 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 Spiral Scout 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | Spiral DBAL 3 | ======== 4 | 5 | ### PLEASE NOTE, THIS PROJECT IS MOVED TO A NEW [REPOSITORY](https://github.com/cycle/database) AND IS NO LONGER BEING MAINTAINED. 6 | 7 | License: 8 | -------- 9 | MIT License (MIT). Please see [`LICENSE`](./LICENSE) for more information. Maintained by [Spiral Scout](https://spiralscout.com). 10 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "spiral/database", 3 | "type": "library", 4 | "description": "DBAL, schema introspection, migration and pagination", 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "Anton Titov / Wolfy-J", 9 | "email": "wolfy.jd@gmail.com" 10 | } 11 | ], 12 | "require": { 13 | "php": ">=7.2", 14 | "ext-pdo": "*", 15 | "spiral/core": "^2.7", 16 | "spiral/logger": "^2.7", 17 | "spiral/pagination": "^2.7" 18 | }, 19 | "require-dev": { 20 | "phpunit/phpunit": "~8.0", 21 | "mockery/mockery": "^1.1", 22 | "spiral/dumper": "^2.7", 23 | "spiral/code-style": "^1.0", 24 | "spiral/tokenizer": "^2.7" 25 | }, 26 | "autoload": { 27 | "psr-4": { 28 | "Spiral\\Database\\": "src/" 29 | } 30 | }, 31 | "autoload-dev": { 32 | "psr-4": { 33 | "Spiral\\Database\\Tests\\": "tests/Database/" 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 15 | 16 | ./tests/ 17 | 18 | 19 | 20 | 21 | 22 | src/ 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/ColumnInterface.php: -------------------------------------------------------------------------------- 1 | self::DEFAULT_DATABASE, 32 | 'aliases' => [], 33 | 'databases' => [], 34 | 'connections' => [], 35 | ]; 36 | 37 | /** 38 | * @return string 39 | */ 40 | public function getDefaultDatabase(): string 41 | { 42 | return $this->config['default'] ?? 'default'; 43 | } 44 | 45 | /** 46 | * Get named list of all databases. 47 | * 48 | * @return DatabasePartial[] 49 | */ 50 | public function getDatabases(): array 51 | { 52 | $result = []; 53 | foreach (array_keys($this->config['databases'] ?? []) as $database) { 54 | $result[$database] = $this->getDatabase($database); 55 | } 56 | 57 | return $result; 58 | } 59 | 60 | /** 61 | * Get names list of all driver connections. 62 | * 63 | * @return Autowire[] 64 | */ 65 | public function getDrivers(): array 66 | { 67 | $result = []; 68 | foreach (array_keys($this->config['connections'] ?? $this->config['drivers'] ?? []) as $driver) { 69 | $result[$driver] = $this->getDriver($driver); 70 | } 71 | 72 | return $result; 73 | } 74 | 75 | /** 76 | * @param string $database 77 | * @return bool 78 | */ 79 | public function hasDatabase(string $database): bool 80 | { 81 | return isset($this->config['databases'][$database]); 82 | } 83 | 84 | /** 85 | * @param string $database 86 | * @return DatabasePartial 87 | * 88 | * @throws ConfigException 89 | */ 90 | public function getDatabase(string $database): DatabasePartial 91 | { 92 | if (!$this->hasDatabase($database)) { 93 | throw new ConfigException("Undefined database `{$database}`"); 94 | } 95 | 96 | $config = $this->config['databases'][$database]; 97 | 98 | return new DatabasePartial( 99 | $database, 100 | $config['tablePrefix'] ?? $config['prefix'] ?? '', 101 | $config['connection'] ?? $config['write'] ?? $config['driver'], 102 | $config['readConnection'] ?? $config['read'] ?? $config['readDriver'] ?? null 103 | ); 104 | } 105 | 106 | /** 107 | * @param string $driver 108 | * @return bool 109 | */ 110 | public function hasDriver(string $driver): bool 111 | { 112 | return isset($this->config['connections'][$driver]) || isset($this->config['drivers'][$driver]); 113 | } 114 | 115 | /** 116 | * @param string $driver 117 | * @return Autowire 118 | * 119 | * @throws ConfigException 120 | */ 121 | public function getDriver(string $driver): Autowire 122 | { 123 | if (!$this->hasDriver($driver)) { 124 | throw new ConfigException("Undefined driver `{$driver}`"); 125 | } 126 | 127 | $config = $this->config['connections'][$driver] ?? $this->config['drivers'][$driver]; 128 | if ($config instanceof Autowire) { 129 | return $config; 130 | } 131 | 132 | $options = $config; 133 | if (isset($config['options']) && $config['options'] !== []) { 134 | $options = $config['options'] + $config; 135 | } 136 | 137 | return new Autowire($config['driver'] ?? $config['class'], ['options' => $options]); 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/Config/DatabasePartial.php: -------------------------------------------------------------------------------- 1 | name = $name; 41 | $this->prefix = $prefix; 42 | $this->driver = $driver; 43 | $this->readDriver = $readDriver; 44 | } 45 | 46 | /** 47 | * @return string 48 | */ 49 | public function getName(): string 50 | { 51 | return $this->name; 52 | } 53 | 54 | /** 55 | * @return string 56 | */ 57 | public function getPrefix(): string 58 | { 59 | return $this->prefix; 60 | } 61 | 62 | /** 63 | * @return string 64 | */ 65 | public function getDriver(): string 66 | { 67 | return $this->driver; 68 | } 69 | 70 | /** 71 | * @return null|string 72 | */ 73 | public function getReadDriver(): ?string 74 | { 75 | return $this->readDriver; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/DatabaseInterface.php: -------------------------------------------------------------------------------- 1 | name($params, $q, $tokens['table'], true) 37 | ); 38 | } 39 | 40 | return parent::insertQuery($params, $q, $tokens); 41 | } 42 | 43 | /** 44 | * {@inheritdoc} 45 | * 46 | * @link http://dev.mysql.com/doc/refman/5.0/en/select.html#id4651990 47 | */ 48 | protected function limit(QueryParameters $params, Quoter $q, int $limit = null, int $offset = null): string 49 | { 50 | if ($limit === null && $offset === null) { 51 | return ''; 52 | } 53 | 54 | $statement = ''; 55 | if ($limit === null) { 56 | // When limit is not provided (or 0) but offset does we can replace 57 | // limit value with PHP_INT_MAX 58 | $statement .= 'LIMIT 18446744073709551615 '; 59 | } else { 60 | $statement .= 'LIMIT ? '; 61 | $params->push(new Parameter($limit)); 62 | } 63 | 64 | if ($offset !== null) { 65 | $statement .= 'OFFSET ?'; 66 | $params->push(new Parameter($offset)); 67 | } 68 | 69 | return trim($statement); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Driver/MySQL/MySQLDriver.php: -------------------------------------------------------------------------------- 1 | PDO::CASE_NATURAL, 26 | PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, 27 | PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES "UTF8"', 28 | PDO::ATTR_STRINGIFY_FETCHES => false, 29 | ]; 30 | 31 | /** 32 | * @param array $options 33 | */ 34 | public function __construct(array $options) 35 | { 36 | // default query builder 37 | parent::__construct( 38 | $options, 39 | new MySQLHandler(), 40 | new MySQLCompiler('``'), 41 | QueryBuilder::defaultBuilder() 42 | ); 43 | } 44 | 45 | /** 46 | * @inheritDoc 47 | */ 48 | public function getType(): string 49 | { 50 | return 'MySQL'; 51 | } 52 | 53 | /** 54 | * {@inheritdoc} 55 | * 56 | * @see https://dev.mysql.com/doc/refman/5.6/en/error-messages-client.html#error_cr_conn_host_error 57 | */ 58 | protected function mapException(\Throwable $exception, string $query): StatementException 59 | { 60 | if ((int)$exception->getCode() === 23000) { 61 | return new StatementException\ConstrainException($exception, $query); 62 | } 63 | 64 | $message = strtolower($exception->getMessage()); 65 | 66 | if ( 67 | strpos($message, 'server has gone away') !== false 68 | || strpos($message, 'broken pipe') !== false 69 | || strpos($message, 'connection') !== false 70 | || strpos($message, 'packets out of order') !== false 71 | || ((int)$exception->getCode() > 2000 && (int)$exception->getCode() < 2100) 72 | ) { 73 | return new StatementException\ConnectionException($exception, $query); 74 | } 75 | 76 | return new StatementException($exception, $query); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/Driver/MySQL/MySQLHandler.php: -------------------------------------------------------------------------------- 1 | driver, $table, $prefix ?? ''); 29 | } 30 | 31 | /** 32 | * {@inheritdoc} 33 | */ 34 | public function getTableNames(): array 35 | { 36 | $result = []; 37 | foreach ($this->driver->query('SHOW TABLES')->fetchAll(PDO::FETCH_NUM) as $row) { 38 | $result[] = $row[0]; 39 | } 40 | 41 | return $result; 42 | } 43 | 44 | /** 45 | * {@inheritdoc} 46 | */ 47 | public function hasTable(string $name): bool 48 | { 49 | $query = 'SELECT COUNT(*) FROM `information_schema`.`tables` WHERE `table_schema` = ? AND `table_name` = ?'; 50 | 51 | return (bool)$this->driver->query( 52 | $query, 53 | [$this->driver->getSource(), $name] 54 | )->fetchColumn(); 55 | } 56 | 57 | /** 58 | * {@inheritdoc} 59 | */ 60 | public function eraseTable(AbstractTable $table): void 61 | { 62 | $this->driver->execute( 63 | "TRUNCATE TABLE {$this->driver->identifier($table->getName())}" 64 | ); 65 | } 66 | 67 | /** 68 | * {@inheritdoc} 69 | */ 70 | public function alterColumn( 71 | AbstractTable $table, 72 | AbstractColumn $initial, 73 | AbstractColumn $column 74 | ): void { 75 | $foreignBackup = []; 76 | foreach ($table->getForeignKeys() as $foreign) { 77 | if ($column->getName() === $foreign->getColumns()) { 78 | $foreignBackup[] = $foreign; 79 | $this->dropForeignKey($table, $foreign); 80 | } 81 | } 82 | 83 | $this->run( 84 | "ALTER TABLE {$this->identify($table)} 85 | CHANGE {$this->identify($initial)} {$column->sqlStatement($this->driver)}" 86 | ); 87 | 88 | //Restoring FKs 89 | foreach ($foreignBackup as $foreign) { 90 | $this->createForeignKey($table, $foreign); 91 | } 92 | } 93 | 94 | /** 95 | * {@inheritdoc} 96 | */ 97 | public function dropIndex(AbstractTable $table, AbstractIndex $index): void 98 | { 99 | $this->run( 100 | "DROP INDEX {$this->identify($index)} ON {$this->identify($table)}" 101 | ); 102 | } 103 | 104 | /** 105 | * {@inheritdoc} 106 | */ 107 | public function alterIndex(AbstractTable $table, AbstractIndex $initial, AbstractIndex $index): void 108 | { 109 | $this->run( 110 | "ALTER TABLE {$this->identify($table)} 111 | DROP INDEX {$this->identify($initial)}, 112 | ADD {$index->sqlStatement($this->driver, false)}" 113 | ); 114 | } 115 | 116 | /** 117 | * {@inheritdoc} 118 | */ 119 | public function dropForeignKey(AbstractTable $table, AbstractForeignKey $foreignKey): void 120 | { 121 | $this->run( 122 | "ALTER TABLE {$this->identify($table)} DROP FOREIGN KEY {$this->identify($foreignKey)}" 123 | ); 124 | } 125 | 126 | /** 127 | * Get statement needed to create table. 128 | * 129 | * @param AbstractTable $table 130 | * @return string 131 | * 132 | * @throws SchemaException 133 | */ 134 | protected function createStatement(AbstractTable $table) 135 | { 136 | if (!$table instanceof MySQLTable) { 137 | throw new SchemaException('MySQLHandler can process only MySQL tables'); 138 | } 139 | 140 | return parent::createStatement($table) . " ENGINE {$table->getEngine()}"; 141 | } 142 | 143 | /** 144 | * @param AbstractColumn $column 145 | * 146 | * @throws MySQLException 147 | */ 148 | protected function assertValid(AbstractColumn $column): void 149 | { 150 | if ( 151 | $column->getDefaultValue() !== null 152 | && in_array( 153 | $column->getAbstractType(), 154 | ['text', 'tinyText', 'longText', 'blob', 'tinyBlob', 'longBlob'] 155 | ) 156 | ) { 157 | throw new MySQLException( 158 | "Column {$column} of type text/blob can not have non empty default value" 159 | ); 160 | } 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /src/Driver/MySQL/Schema/MySQLForeignKey.php: -------------------------------------------------------------------------------- 1 | columns = $schema['COLUMN_NAME']; 29 | $reference->foreignTable = $schema['REFERENCED_TABLE_NAME']; 30 | $reference->foreignKeys = $schema['REFERENCED_COLUMN_NAME']; 31 | 32 | $reference->deleteRule = $schema['DELETE_RULE']; 33 | $reference->updateRule = $schema['UPDATE_RULE']; 34 | 35 | return $reference; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Driver/MySQL/Schema/MySQLIndex.php: -------------------------------------------------------------------------------- 1 | type = $definition['Non_unique'] ? self::NORMAL : self::UNIQUE; 30 | $index->columns[] = $definition['Column_name']; 31 | if ($definition['Collation'] === 'D') { 32 | $index->sort[$definition['Column_name']] = 'DESC'; 33 | } 34 | } 35 | 36 | return $index; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Driver/MySQL/Schema/MySQLTable.php: -------------------------------------------------------------------------------- 1 | exists()) { 55 | throw new SchemaException('Table engine can be set only at moment of creation'); 56 | } 57 | 58 | $this->engine = $engine; 59 | 60 | return $this; 61 | } 62 | 63 | /** 64 | * @return string 65 | */ 66 | public function getEngine(): string 67 | { 68 | return $this->engine; 69 | } 70 | 71 | /** 72 | * Populate table schema with values from database. 73 | * 74 | * @param State $state 75 | */ 76 | protected function initSchema(State $state): void 77 | { 78 | parent::initSchema($state); 79 | 80 | //Reading table schema 81 | $this->engine = $this->driver->query( 82 | 'SHOW TABLE STATUS WHERE `Name` = ?', 83 | [ 84 | $state->getName() 85 | ] 86 | )->fetch()['Engine']; 87 | } 88 | 89 | protected function isIndexColumnSortingSupported(): bool 90 | { 91 | if (!$this->version) { 92 | $this->version = $this->driver->query('SELECT VERSION() AS version')->fetch()['version']; 93 | } 94 | 95 | if (strpos($this->version, 'MariaDB') !== false) { 96 | return false; 97 | } 98 | 99 | return version_compare($this->version, '8.0', '>='); 100 | } 101 | 102 | /** 103 | * {@inheritdoc} 104 | */ 105 | protected function fetchColumns(): array 106 | { 107 | $query = "SHOW FULL COLUMNS FROM {$this->driver->identifier($this->getName())}"; 108 | 109 | $result = []; 110 | foreach ($this->driver->query($query) as $schema) { 111 | $result[] = MySQLColumn::createInstance( 112 | $this->getName(), 113 | $schema, 114 | $this->driver->getTimezone() 115 | ); 116 | } 117 | 118 | return $result; 119 | } 120 | 121 | /** 122 | * {@inheritdoc} 123 | */ 124 | protected function fetchIndexes(): array 125 | { 126 | $query = "SHOW INDEXES FROM {$this->driver->identifier($this->getName())}"; 127 | 128 | //Gluing all index definitions together 129 | $schemas = []; 130 | foreach ($this->driver->query($query) as $index) { 131 | if ($index['Key_name'] === 'PRIMARY') { 132 | //Skipping PRIMARY index 133 | continue; 134 | } 135 | 136 | $schemas[$index['Key_name']][] = $index; 137 | } 138 | 139 | $result = []; 140 | foreach ($schemas as $name => $index) { 141 | $result[] = MySQLIndex::createInstance($this->getName(), $name, $index); 142 | } 143 | 144 | return $result; 145 | } 146 | 147 | /** 148 | * {@inheritdoc} 149 | */ 150 | protected function fetchReferences(): array 151 | { 152 | $references = $this->driver->query( 153 | 'SELECT * FROM `information_schema`.`referential_constraints` 154 | WHERE `constraint_schema` = ? AND `table_name` = ?', 155 | [$this->driver->getSource(), $this->getName()] 156 | ); 157 | 158 | $result = []; 159 | foreach ($references as $schema) { 160 | $columns = $this->driver->query( 161 | 'SELECT * FROM `information_schema`.`key_column_usage` 162 | WHERE `constraint_name` = ? AND `table_schema` = ? AND `table_name` = ?', 163 | [$schema['CONSTRAINT_NAME'], $this->driver->getSource(), $this->getName()] 164 | )->fetchAll(); 165 | 166 | $schema['COLUMN_NAME'] = []; 167 | $schema['REFERENCED_COLUMN_NAME'] = []; 168 | 169 | foreach ($columns as $column) { 170 | $schema['COLUMN_NAME'][] = $column['COLUMN_NAME']; 171 | $schema['REFERENCED_COLUMN_NAME'][] = $column['REFERENCED_COLUMN_NAME']; 172 | } 173 | 174 | $result[] = MySQLForeignKey::createInstance( 175 | $this->getName(), 176 | $this->getPrefix(), 177 | $schema 178 | ); 179 | } 180 | 181 | return $result; 182 | } 183 | 184 | /** 185 | * Fetching primary keys from table. 186 | * 187 | * @return array 188 | */ 189 | protected function fetchPrimaryKeys(): array 190 | { 191 | $query = "SHOW INDEXES FROM {$this->driver->identifier($this->getName())}"; 192 | 193 | $primaryKeys = []; 194 | foreach ($this->driver->query($query) as $index) { 195 | if ($index['Key_name'] === 'PRIMARY') { 196 | $primaryKeys[] = $index['Column_name']; 197 | } 198 | } 199 | 200 | return $primaryKeys; 201 | } 202 | 203 | /** 204 | * {@inheritdoc} 205 | */ 206 | protected function createColumn(string $name): AbstractColumn 207 | { 208 | return new MySQLColumn($this->getName(), $name, $this->driver->getTimezone()); 209 | } 210 | 211 | /** 212 | * {@inheritdoc} 213 | */ 214 | protected function createIndex(string $name): AbstractIndex 215 | { 216 | return new MySQLIndex($this->getName(), $name); 217 | } 218 | 219 | /** 220 | * {@inheritdoc} 221 | */ 222 | protected function createForeign(string $name): AbstractForeignKey 223 | { 224 | return new MySQLForeignKey($this->getName(), $this->getPrefix(), $name); 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /src/Driver/Postgres/PostgresCompiler.php: -------------------------------------------------------------------------------- 1 | quoteIdentifier($tokens['return']) 43 | ); 44 | } 45 | 46 | /** 47 | * {@inheritdoc} 48 | */ 49 | protected function distinct(QueryParameters $params, Quoter $q, $distinct): string 50 | { 51 | if ($distinct === false) { 52 | return ''; 53 | } 54 | 55 | if (is_array($distinct) && isset($distinct['on'])) { 56 | return sprintf('DISTINCT ON (%s)', $this->name($params, $q, $distinct['on'])); 57 | } 58 | 59 | if (is_string($distinct)) { 60 | return sprintf('DISTINCT (%s)', $this->name($params, $q, $distinct)); 61 | } 62 | 63 | return 'DISTINCT'; 64 | } 65 | 66 | /** 67 | * @param QueryParameters $params 68 | * @param Quoter $q 69 | * @param int|null $limit 70 | * @param int|null $offset 71 | * @return string 72 | */ 73 | protected function limit(QueryParameters $params, Quoter $q, int $limit = null, int $offset = null): string 74 | { 75 | if ($limit === null && $offset === null) { 76 | return ''; 77 | } 78 | 79 | $statement = ''; 80 | if ($limit !== null) { 81 | $statement = 'LIMIT ? '; 82 | $params->push(new Parameter($limit)); 83 | } 84 | 85 | if ($offset !== null) { 86 | $statement .= 'OFFSET ?'; 87 | $params->push(new Parameter($offset)); 88 | } 89 | 90 | return trim($statement); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/Driver/Postgres/PostgresDriver.php: -------------------------------------------------------------------------------- 1 | primaryKeys[$name])) { 79 | return $this->primaryKeys[$name]; 80 | } 81 | 82 | if (!$this->getSchemaHandler()->hasTable($name)) { 83 | throw new DriverException( 84 | "Unable to fetch table primary key, no such table '{$name}' exists" 85 | ); 86 | } 87 | 88 | $this->primaryKeys[$name] = $this->getSchemaHandler() 89 | ->getSchema($table, $prefix) 90 | ->getPrimaryKeys(); 91 | 92 | if (count($this->primaryKeys[$name]) === 1) { 93 | //We do support only single primary key 94 | $this->primaryKeys[$name] = $this->primaryKeys[$name][0]; 95 | } else { 96 | $this->primaryKeys[$name] = null; 97 | } 98 | 99 | return $this->primaryKeys[$name]; 100 | } 101 | 102 | /** 103 | * Reset primary keys cache. 104 | */ 105 | public function resetPrimaryKeys(): void 106 | { 107 | $this->primaryKeys = []; 108 | } 109 | 110 | /** 111 | * Start SQL transaction with specified isolation level (not all DBMS support it). Nested 112 | * transactions are processed using savepoints. 113 | * 114 | * @link http://en.wikipedia.org/wiki/Database_transaction 115 | * @link http://en.wikipedia.org/wiki/Isolation_(database_systems) 116 | * 117 | * @param string $isolationLevel 118 | * @return bool 119 | */ 120 | public function beginTransaction(string $isolationLevel = null): bool 121 | { 122 | ++$this->transactionLevel; 123 | 124 | if ($this->transactionLevel === 1) { 125 | if ($this->logger !== null) { 126 | $this->logger->info('Begin transaction'); 127 | } 128 | 129 | try { 130 | $ok = $this->getPDO()->beginTransaction(); 131 | if ($isolationLevel !== null) { 132 | $this->setIsolationLevel($isolationLevel); 133 | } 134 | 135 | return $ok; 136 | } catch (Throwable $e) { 137 | $e = $this->mapException($e, 'BEGIN TRANSACTION'); 138 | 139 | if ( 140 | $e instanceof StatementException\ConnectionException 141 | && $this->options['reconnect'] 142 | ) { 143 | $this->disconnect(); 144 | 145 | try { 146 | return $this->getPDO()->beginTransaction(); 147 | } catch (Throwable $e) { 148 | $this->transactionLevel = 0; 149 | throw $this->mapException($e, 'BEGIN TRANSACTION'); 150 | } 151 | } else { 152 | $this->transactionLevel = 0; 153 | throw $e; 154 | } 155 | } 156 | } 157 | 158 | $this->createSavepoint($this->transactionLevel); 159 | 160 | return true; 161 | } 162 | 163 | /** 164 | * {@inheritdoc} 165 | */ 166 | protected function createPDO(): \PDO 167 | { 168 | // spiral is purely UTF-8 169 | $pdo = parent::createPDO(); 170 | $pdo->exec("SET NAMES 'UTF-8'"); 171 | 172 | return $pdo; 173 | } 174 | 175 | /** 176 | * {@inheritdoc} 177 | */ 178 | protected function mapException(Throwable $exception, string $query): StatementException 179 | { 180 | $message = strtolower($exception->getMessage()); 181 | 182 | if ( 183 | strpos($message, 'eof detected') !== false 184 | || strpos($message, 'broken pipe') !== false 185 | || strpos($message, '0800') !== false 186 | || strpos($message, '080P') !== false 187 | || strpos($message, 'connection') !== false 188 | ) { 189 | return new StatementException\ConnectionException($exception, $query); 190 | } 191 | 192 | if ((int) $exception->getCode() >= 23000 && (int) $exception->getCode() < 24000) { 193 | return new StatementException\ConstrainException($exception, $query); 194 | } 195 | 196 | return new StatementException($exception, $query); 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /src/Driver/Postgres/PostgresHandler.php: -------------------------------------------------------------------------------- 1 | driver, $table, $prefix ?? ''); 29 | } 30 | 31 | /** 32 | * {@inheritdoc} 33 | */ 34 | public function getTableNames(): array 35 | { 36 | $query = "SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' 37 | AND table_type = 'BASE TABLE'"; 38 | 39 | $tables = []; 40 | foreach ($this->driver->query($query) as $row) { 41 | $tables[] = $row['table_name']; 42 | } 43 | 44 | return $tables; 45 | } 46 | 47 | /** 48 | * {@inheritdoc} 49 | */ 50 | public function hasTable(string $name): bool 51 | { 52 | return (bool)$this->driver->query( 53 | "SELECT COUNT(table_name) FROM information_schema.tables WHERE table_schema = 'public' 54 | AND table_type = 'BASE TABLE' AND table_name = ?", 55 | [$name] 56 | )->fetchColumn(); 57 | } 58 | 59 | /** 60 | * {@inheritdoc} 61 | */ 62 | public function eraseTable(AbstractTable $table): void 63 | { 64 | $this->driver->execute( 65 | "TRUNCATE TABLE {$this->driver->identifier($table->getName())}" 66 | ); 67 | } 68 | 69 | /** 70 | * {@inheritdoc} 71 | * 72 | * @throws SchemaException 73 | */ 74 | public function alterColumn( 75 | AbstractTable $table, 76 | AbstractColumn $initial, 77 | AbstractColumn $column 78 | ): void { 79 | if (!$initial instanceof PostgresColumn || !$column instanceof PostgresColumn) { 80 | throw new SchemaException('Postgres handler can work only with Postgres columns'); 81 | } 82 | 83 | //Rename is separate operation 84 | if ($column->getName() !== $initial->getName()) { 85 | $this->renameColumn($table, $initial, $column); 86 | 87 | //This call is required to correctly built set of alter operations 88 | $initial->setName($column->getName()); 89 | } 90 | 91 | //Postgres columns should be altered using set of operations 92 | $operations = $column->alterOperations($this->driver, $initial); 93 | if (empty($operations)) { 94 | return; 95 | } 96 | 97 | //Postgres columns should be altered using set of operations 98 | $query = sprintf( 99 | 'ALTER TABLE %s %s', 100 | $this->identify($table), 101 | trim(implode(', ', $operations), ', ') 102 | ); 103 | 104 | $this->run($query); 105 | } 106 | 107 | /** 108 | * @inheritdoc 109 | */ 110 | protected function run(string $statement, array $parameters = []): int 111 | { 112 | if ($this->driver instanceof PostgresDriver) { 113 | // invaliding primary key cache 114 | $this->driver->resetPrimaryKeys(); 115 | } 116 | 117 | return parent::run($statement, $parameters); 118 | } 119 | 120 | /** 121 | * @param AbstractTable $table 122 | * @param AbstractColumn $initial 123 | * @param AbstractColumn $column 124 | */ 125 | private function renameColumn( 126 | AbstractTable $table, 127 | AbstractColumn $initial, 128 | AbstractColumn $column 129 | ): void { 130 | $statement = sprintf( 131 | 'ALTER TABLE %s RENAME COLUMN %s TO %s', 132 | $this->identify($table), 133 | $this->identify($initial), 134 | $this->identify($column) 135 | ); 136 | 137 | $this->run($statement); 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/Driver/Postgres/Query/PostgresInsertQuery.php: -------------------------------------------------------------------------------- 1 | returning = $column; 59 | 60 | return $this; 61 | } 62 | 63 | /** 64 | * @return int|string|null 65 | */ 66 | public function run() 67 | { 68 | $params = new QueryParameters(); 69 | $queryString = $this->sqlStatement($params); 70 | 71 | if ($this->driver->isReadonly()) { 72 | throw ReadonlyConnectionException::onWriteStatementExecution(); 73 | } 74 | 75 | $result = $this->driver->query($queryString, $params->getParameters()); 76 | 77 | try { 78 | if ($this->getPrimaryKey() !== null) { 79 | return $result->fetchColumn(); 80 | } 81 | 82 | return null; 83 | } finally { 84 | $result->close(); 85 | } 86 | } 87 | 88 | /** 89 | * @return array 90 | */ 91 | public function getTokens(): array 92 | { 93 | return [ 94 | 'table' => $this->table, 95 | 'return' => $this->getPrimaryKey(), 96 | 'columns' => $this->columns, 97 | 'values' => $this->values 98 | ]; 99 | } 100 | 101 | /** 102 | * @return string 103 | */ 104 | private function getPrimaryKey(): ?string 105 | { 106 | $primaryKey = $this->returning; 107 | if ($primaryKey === null && $this->driver !== null && $this->table !== null) { 108 | try { 109 | $primaryKey = $this->driver->getPrimaryKey($this->prefix, $this->table); 110 | } catch (Throwable $e) { 111 | return null; 112 | } 113 | } 114 | 115 | return $primaryKey; 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/Driver/Postgres/Query/PostgresSelectQuery.php: -------------------------------------------------------------------------------- 1 | distinct = ['on' => $distinctOn]; 28 | 29 | return $this; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Driver/Postgres/Schema/PostgresForeignKey.php: -------------------------------------------------------------------------------- 1 | columns = $foreign->normalizeKeys($schema['column_name']); 29 | $foreign->foreignTable = $schema['foreign_table_name']; 30 | $foreign->foreignKeys = $foreign->normalizeKeys($schema['foreign_column_name']); 31 | 32 | $foreign->deleteRule = $schema['delete_rule']; 33 | $foreign->updateRule = $schema['update_rule']; 34 | 35 | return $foreign; 36 | } 37 | 38 | /** 39 | * @param array $columns 40 | * @return array 41 | */ 42 | private function normalizeKeys(array $columns): array 43 | { 44 | $result = []; 45 | foreach ($columns as $column) { 46 | if (array_search($column, $result, true) === false) { 47 | $result[] = $column; 48 | } 49 | } 50 | 51 | return $result; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Driver/Postgres/Schema/PostgresIndex.php: -------------------------------------------------------------------------------- 1 | type = strpos($schema['indexdef'], ' UNIQUE ') ? self::UNIQUE : self::NORMAL; 27 | 28 | if (preg_match('/\(([^)]+)\)/', $schema['indexdef'], $matches)) { 29 | $columns = explode(',', $matches[1]); 30 | 31 | foreach ($columns as $column) { 32 | //Postgres adds quotes to all columns with uppercase letters 33 | $column = trim($column, ' "\''); 34 | [$column, $order] = AbstractIndex::parseColumn($column); 35 | 36 | $index->columns[] = $column; 37 | if ($order) { 38 | $index->sort[$column] = $order; 39 | } 40 | } 41 | } 42 | 43 | return $index; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Driver/ReadonlyHandler.php: -------------------------------------------------------------------------------- 1 | parent = $parent; 30 | } 31 | 32 | /** 33 | * @inheritDoc 34 | */ 35 | public function withDriver(DriverInterface $driver): HandlerInterface 36 | { 37 | $handler = clone $this; 38 | $handler->parent = $handler->parent->withDriver($driver); 39 | 40 | return $handler; 41 | } 42 | 43 | /** 44 | * @inheritDoc 45 | */ 46 | public function getTableNames(): array 47 | { 48 | return $this->parent->getTableNames(); 49 | } 50 | 51 | /** 52 | * @inheritDoc 53 | */ 54 | public function hasTable(string $table): bool 55 | { 56 | return $this->parent->hasTable($table); 57 | } 58 | 59 | /** 60 | * @inheritDoc 61 | */ 62 | public function getSchema(string $table, string $prefix = null): AbstractTable 63 | { 64 | return $this->parent->getSchema($table, $prefix); 65 | } 66 | 67 | /** 68 | * @inheritDoc 69 | */ 70 | public function createTable(AbstractTable $table): void 71 | { 72 | } 73 | 74 | /** 75 | * @inheritDoc 76 | */ 77 | public function eraseTable(AbstractTable $table): void 78 | { 79 | $this->parent->eraseTable($table); 80 | } 81 | 82 | /** 83 | * @inheritDoc 84 | */ 85 | public function dropTable(AbstractTable $table): void 86 | { 87 | } 88 | 89 | /** 90 | * @inheritDoc 91 | */ 92 | public function syncTable(AbstractTable $table, int $operation = self::DO_ALL): void 93 | { 94 | } 95 | 96 | /** 97 | * @inheritDoc 98 | */ 99 | public function renameTable(string $table, string $name): void 100 | { 101 | } 102 | 103 | /** 104 | * @inheritDoc 105 | */ 106 | public function createColumn(AbstractTable $table, AbstractColumn $column): void 107 | { 108 | } 109 | 110 | /** 111 | * @inheritDoc 112 | */ 113 | public function dropColumn(AbstractTable $table, AbstractColumn $column): void 114 | { 115 | } 116 | 117 | /** 118 | * @inheritDoc 119 | */ 120 | public function alterColumn(AbstractTable $table, AbstractColumn $initial, AbstractColumn $column): void 121 | { 122 | } 123 | 124 | /** 125 | * @inheritDoc 126 | */ 127 | public function createIndex(AbstractTable $table, AbstractIndex $index): void 128 | { 129 | } 130 | 131 | /** 132 | * @inheritDoc 133 | */ 134 | public function dropIndex(AbstractTable $table, AbstractIndex $index): void 135 | { 136 | } 137 | 138 | /** 139 | * @inheritDoc 140 | */ 141 | public function alterIndex(AbstractTable $table, AbstractIndex $initial, AbstractIndex $index): void 142 | { 143 | } 144 | 145 | /** 146 | * @inheritDoc 147 | */ 148 | public function createForeignKey(AbstractTable $table, AbstractForeignKey $foreignKey): void 149 | { 150 | } 151 | 152 | /** 153 | * @inheritDoc 154 | */ 155 | public function dropForeignKey(AbstractTable $table, AbstractForeignKey $foreignKey): void 156 | { 157 | } 158 | 159 | /** 160 | * @inheritDoc 161 | */ 162 | public function alterForeignKey( 163 | AbstractTable $table, 164 | AbstractForeignKey $initial, 165 | AbstractForeignKey $foreignKey 166 | ): void { 167 | } 168 | 169 | /** 170 | * @inheritDoc 171 | */ 172 | public function dropConstrain(AbstractTable $table, string $constraint): void 173 | { 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /src/Driver/SQLServer/SQLServerCompiler.php: -------------------------------------------------------------------------------- 1 | name($params, $q, self::ROW_NUMBER) 60 | ) 61 | ); 62 | 63 | $tokens['limit'] = null; 64 | $tokens['offset'] = null; 65 | 66 | return sprintf( 67 | "SELECT * FROM (\n%s\n) AS [ORD_FALLBACK] %s", 68 | $this->baseSelect($params, $q, $tokens), 69 | $this->limit($params, $q, $limit, $offset, self::ROW_NUMBER) 70 | ); 71 | } 72 | 73 | /** 74 | * {@inheritdoc} 75 | * 76 | * @param string $rowNumber Row used in a fallback sorting mechanism, ONLY when no ORDER BY 77 | * specified. 78 | * 79 | * @link http://stackoverflow.com/questions/2135418/equivalent-of-limit-and-offset-for-sql-server 80 | */ 81 | protected function limit( 82 | QueryParameters $params, 83 | Quoter $q, 84 | int $limit = null, 85 | int $offset = null, 86 | string $rowNumber = null 87 | ): string { 88 | if ($limit === null && $offset === null) { 89 | return ''; 90 | } 91 | 92 | //Modern SQLServer are easier to work with 93 | if ($rowNumber === null) { 94 | $statement = 'OFFSET ? ROWS '; 95 | $params->push(new Parameter((int)$offset)); 96 | 97 | if ($limit !== null) { 98 | $statement .= 'FETCH FIRST ? ROWS ONLY'; 99 | $params->push(new Parameter($limit)); 100 | } 101 | 102 | return trim($statement); 103 | } 104 | 105 | $statement = "WHERE {$this->name($params, $q, $rowNumber)} "; 106 | 107 | //0 = row_number(1) 108 | ++$offset; 109 | 110 | if ($limit !== null) { 111 | $statement .= 'BETWEEN ? AND ?'; 112 | $params->push(new Parameter((int)$offset)); 113 | $params->push(new Parameter($offset + $limit - 1)); 114 | } else { 115 | $statement .= '>= ?'; 116 | $params->push(new Parameter((int)$offset)); 117 | } 118 | 119 | return $statement; 120 | } 121 | 122 | /** 123 | * @inheritDoc 124 | */ 125 | private function baseSelect(QueryParameters $params, Quoter $q, array $tokens): string 126 | { 127 | // This statement(s) parts should be processed first to define set of table and column aliases 128 | $tables = []; 129 | foreach ($tokens['from'] as $table) { 130 | $tables[] = $this->name($params, $q, $table, true); 131 | } 132 | 133 | $joins = $this->joins($params, $q, $tokens['join']); 134 | 135 | return sprintf( 136 | "SELECT%s %s\nFROM %s%s%s%s%s%s%s%s%s", 137 | $this->optional(' ', $this->distinct($params, $q, $tokens['distinct'])), 138 | $this->columns($params, $q, $tokens['columns']), 139 | implode(', ', $tables), 140 | $this->optional(' ', $tokens['forUpdate'] ? 'WITH (UPDLOCK,ROWLOCK)' : '', ' '), 141 | $this->optional(' ', $joins, ' '), 142 | $this->optional("\nWHERE", $this->where($params, $q, $tokens['where'])), 143 | $this->optional("\nGROUP BY", $this->groupBy($params, $q, $tokens['groupBy']), ' '), 144 | $this->optional("\nHAVING", $this->where($params, $q, $tokens['having'])), 145 | $this->optional("\n", $this->unions($params, $q, $tokens['union'])), 146 | $this->optional("\nORDER BY", $this->orderBy($params, $q, $tokens['orderBy'])), 147 | $this->optional("\n", $this->limit($params, $q, $tokens['limit'], $tokens['offset'])) 148 | ); 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/Driver/SQLServer/SQLServerDriver.php: -------------------------------------------------------------------------------- 1 | PDO::CASE_NATURAL, 28 | PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, 29 | PDO::ATTR_STRINGIFY_FETCHES => false 30 | ]; 31 | 32 | /** 33 | * {@inheritdoc} 34 | * 35 | * @throws DriverException 36 | */ 37 | public function __construct(array $options) 38 | { 39 | parent::__construct( 40 | $options, 41 | new SQLServerHandler(), 42 | new SQLServerCompiler('[]'), 43 | QueryBuilder::defaultBuilder() 44 | ); 45 | 46 | if ((int)$this->getPDO()->getAttribute(\PDO::ATTR_SERVER_VERSION) < 12) { 47 | throw new DriverException('SQLServer driver supports only 12+ version of SQLServer'); 48 | } 49 | } 50 | 51 | /** 52 | * @return string 53 | */ 54 | public function getType(): string 55 | { 56 | return 'SQLServer'; 57 | } 58 | 59 | /** 60 | * Bind parameters into statement. SQLServer need encoding to be specified for binary parameters. 61 | * 62 | * @param PDOStatement $statement 63 | * @param array $parameters 64 | * @return PDOStatement 65 | */ 66 | protected function bindParameters( 67 | PDOStatement $statement, 68 | iterable $parameters 69 | ): PDOStatement { 70 | $index = 0; 71 | foreach ($parameters as $name => $parameter) { 72 | if (is_string($name)) { 73 | $index = $name; 74 | } else { 75 | $index++; 76 | } 77 | 78 | $type = PDO::PARAM_STR; 79 | 80 | if ($parameter instanceof ParameterInterface) { 81 | $type = $parameter->getType(); 82 | $parameter = $parameter->getValue(); 83 | } 84 | 85 | if ($parameter instanceof DateTimeInterface) { 86 | $parameter = $this->formatDatetime($parameter); 87 | } 88 | 89 | if ($type === PDO::PARAM_LOB) { 90 | $statement->bindParam( 91 | $index, 92 | $parameter, 93 | $type, 94 | 0, 95 | PDO::SQLSRV_ENCODING_BINARY 96 | ); 97 | 98 | unset($parameter); 99 | continue; 100 | } 101 | 102 | // numeric, @see http://php.net/manual/en/pdostatement.bindparam.php 103 | $statement->bindValue($index, $parameter, $type); 104 | unset($parameter); 105 | } 106 | 107 | return $statement; 108 | } 109 | 110 | /** 111 | * Create nested transaction save point. 112 | * 113 | * @link http://en.wikipedia.org/wiki/Savepoint 114 | * 115 | * @param int $level Savepoint name/id, must not contain spaces and be valid database 116 | * identifier. 117 | */ 118 | protected function createSavepoint(int $level): void 119 | { 120 | if ($this->logger !== null) { 121 | $this->logger->info("Transaction: new savepoint 'SVP{$level}'"); 122 | } 123 | 124 | $this->execute('SAVE TRANSACTION ' . $this->identifier("SVP{$level}")); 125 | } 126 | 127 | /** 128 | * Commit/release savepoint. 129 | * 130 | * @link http://en.wikipedia.org/wiki/Savepoint 131 | * 132 | * @param int $level Savepoint name/id, must not contain spaces and be valid database identifier. 133 | */ 134 | protected function releaseSavepoint(int $level): void 135 | { 136 | if ($this->logger !== null) { 137 | $this->logger->info("Transaction: release savepoint 'SVP{$level}'"); 138 | } 139 | // SQLServer automatically commits nested transactions with parent transaction 140 | } 141 | 142 | /** 143 | * Rollback savepoint. 144 | * 145 | * @link http://en.wikipedia.org/wiki/Savepoint 146 | * 147 | * @param int $level Savepoint name/id, must not contain spaces and be valid database identifier. 148 | */ 149 | protected function rollbackSavepoint(int $level): void 150 | { 151 | if ($this->logger !== null) { 152 | $this->logger->info("Transaction: rollback savepoint 'SVP{$level}'"); 153 | } 154 | 155 | $this->execute('ROLLBACK TRANSACTION ' . $this->identifier("SVP{$level}")); 156 | } 157 | 158 | /** 159 | * {@inheritdoc} 160 | */ 161 | protected function mapException(\Throwable $exception, string $query): StatementException 162 | { 163 | $message = strtolower($exception->getMessage()); 164 | 165 | if ( 166 | strpos($message, '0800') !== false 167 | || strpos($message, '080P') !== false 168 | || strpos($message, 'connection') !== false 169 | ) { 170 | return new StatementException\ConnectionException($exception, $query); 171 | } 172 | 173 | if ((int)$exception->getCode() === 23000) { 174 | return new StatementException\ConstrainException($exception, $query); 175 | } 176 | 177 | return new StatementException($exception, $query); 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /src/Driver/SQLServer/SQLServerHandler.php: -------------------------------------------------------------------------------- 1 | driver, $table, $prefix ?? ''); 31 | } 32 | 33 | /** 34 | * {@inheritdoc} 35 | */ 36 | public function getTableNames(): array 37 | { 38 | $query = "SELECT [table_name] FROM [information_schema].[tables] WHERE [table_type] = 'BASE TABLE'"; 39 | 40 | $tables = []; 41 | foreach ($this->driver->query($query)->fetchAll(PDO::FETCH_NUM) as $name) { 42 | $tables[] = $name[0]; 43 | } 44 | 45 | return $tables; 46 | } 47 | 48 | /** 49 | * {@inheritdoc} 50 | */ 51 | public function hasTable(string $name): bool 52 | { 53 | $query = "SELECT COUNT(*) FROM [information_schema].[tables] 54 | WHERE [table_type] = 'BASE TABLE' AND [table_name] = ?"; 55 | 56 | return (bool)$this->driver->query($query, [$name])->fetchColumn(); 57 | } 58 | 59 | /** 60 | * {@inheritdoc} 61 | */ 62 | public function eraseTable(AbstractTable $table): void 63 | { 64 | $this->driver->execute( 65 | "TRUNCATE TABLE {$this->driver->identifier($table->getName())}" 66 | ); 67 | } 68 | 69 | /** 70 | * {@inheritdoc} 71 | */ 72 | public function renameTable(string $table, string $name): void 73 | { 74 | $this->run( 75 | 'sp_rename @objname = ?, @newname = ?', 76 | [$table, $name] 77 | ); 78 | } 79 | 80 | /** 81 | * {@inheritdoc} 82 | */ 83 | public function createColumn(AbstractTable $table, AbstractColumn $column): void 84 | { 85 | $this->run( 86 | "ALTER TABLE {$this->identify($table)} ADD {$column->sqlStatement($this->driver)}" 87 | ); 88 | } 89 | 90 | /** 91 | * Driver specific column alter command. 92 | * 93 | * @param AbstractTable $table 94 | * @param AbstractColumn $initial 95 | * @param AbstractColumn $column 96 | * 97 | * @throws SchemaException 98 | */ 99 | public function alterColumn( 100 | AbstractTable $table, 101 | AbstractColumn $initial, 102 | AbstractColumn $column 103 | ): void { 104 | if (!$initial instanceof SQLServerColumn || !$column instanceof SQLServerColumn) { 105 | throw new SchemaException('SQlServer handler can work only with SQLServer columns'); 106 | } 107 | 108 | //In SQLServer we have to drop ALL related indexes and foreign keys while 109 | //applying type change... yeah... 110 | 111 | $indexesBackup = []; 112 | $foreignBackup = []; 113 | foreach ($table->getIndexes() as $index) { 114 | if (in_array($column->getName(), $index->getColumns(), true)) { 115 | $indexesBackup[] = $index; 116 | $this->dropIndex($table, $index); 117 | } 118 | } 119 | 120 | foreach ($table->getForeignKeys() as $foreign) { 121 | if ($column->getName() === $foreign->getColumns()) { 122 | $foreignBackup[] = $foreign; 123 | $this->dropForeignKey($table, $foreign); 124 | } 125 | } 126 | 127 | //Column will recreate needed constraints 128 | foreach ($column->getConstraints() as $constraint) { 129 | $this->dropConstrain($table, $constraint); 130 | } 131 | 132 | //Rename is separate operation 133 | if ($column->getName() !== $initial->getName()) { 134 | $this->renameColumn($table, $initial, $column); 135 | 136 | //This call is required to correctly built set of alter operations 137 | $initial->setName($column->getName()); 138 | } 139 | 140 | foreach ($column->alterOperations($this->driver, $initial) as $operation) { 141 | $this->run("ALTER TABLE {$this->identify($table)} {$operation}"); 142 | } 143 | 144 | //Restoring indexes and foreign keys 145 | foreach ($indexesBackup as $index) { 146 | $this->createIndex($table, $index); 147 | } 148 | 149 | foreach ($foreignBackup as $foreign) { 150 | $this->createForeignKey($table, $foreign); 151 | } 152 | } 153 | 154 | /** 155 | * {@inheritdoc} 156 | */ 157 | public function dropIndex(AbstractTable $table, AbstractIndex $index): void 158 | { 159 | $this->run("DROP INDEX {$this->identify($index)} ON {$this->identify($table)}"); 160 | } 161 | 162 | /** 163 | * {@inheritdoc} 164 | */ 165 | private function renameColumn( 166 | AbstractTable $table, 167 | AbstractColumn $initial, 168 | AbstractColumn $column 169 | ): void { 170 | $this->run( 171 | "sp_rename ?, ?, 'COLUMN'", 172 | [ 173 | $table->getName() . '.' . $initial->getName(), 174 | $column->getName() 175 | ] 176 | ); 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /src/Driver/SQLServer/Schema/SQLServerIndex.php: -------------------------------------------------------------------------------- 1 | type = current($schema)['isUnique'] ? self::UNIQUE : self::NORMAL; 28 | 29 | foreach ($schema as $indexColumn) { 30 | $index->columns[] = $indexColumn['columnName']; 31 | if (intval($indexColumn['isDescendingKey']) === 1) { 32 | $index->sort[$indexColumn['columnName']] = 'DESC'; 33 | } 34 | } 35 | 36 | return $index; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Driver/SQLServer/Schema/SQLServerTable.php: -------------------------------------------------------------------------------- 1 | fetchColumns() as $column) { 33 | $currentColumn = $this->current->findColumn($column->getName()); 34 | if (!empty($currentColumn) && $column->compare($currentColumn)) { 35 | //SQLServer is going to add some automatic constrains, let's handle them 36 | $this->current->registerColumn($column); 37 | } 38 | } 39 | } 40 | } 41 | 42 | /** 43 | * {@inheritdoc} 44 | */ 45 | protected function fetchColumns(): array 46 | { 47 | $query = 'SELECT * FROM [information_schema].[columns] INNER JOIN [sys].[columns] AS [sysColumns] ' 48 | . 'ON (object_name([object_id]) = [table_name] AND [sysColumns].[name] = [COLUMN_NAME]) ' 49 | . 'WHERE [table_name] = ?'; 50 | 51 | $result = []; 52 | foreach ($this->driver->query($query, [$this->getName()]) as $schema) { 53 | //Column initialization needs driver to properly resolve enum type 54 | $result[] = SQLServerColumn::createInstance( 55 | $this->getName(), 56 | $schema, 57 | $this->driver 58 | ); 59 | } 60 | 61 | return $result; 62 | } 63 | 64 | /** 65 | * {@inheritdoc} 66 | */ 67 | protected function fetchIndexes(): array 68 | { 69 | $query = 'SELECT [indexes].[name] AS [indexName], ' 70 | . '[cl].[name] AS [columnName], [columns].[is_descending_key] AS [isDescendingKey], ' 71 | . "[is_primary_key] AS [isPrimary], [is_unique] AS [isUnique]\n" 72 | . "FROM [sys].[indexes] AS [indexes]\n" 73 | . "INNER JOIN [sys].[index_columns] as [columns]\n" 74 | . " ON [indexes].[object_id] = [columns].[object_id] AND [indexes].[index_id] = [columns].[index_id]\n" 75 | . "INNER JOIN [sys].[columns] AS [cl]\n" 76 | . " ON [columns].[object_id] = [cl].[object_id] AND [columns].[column_id] = [cl].[column_id]\n" 77 | . "INNER JOIN [sys].[tables] AS [t]\n" 78 | . " ON [indexes].[object_id] = [t].[object_id]\n" 79 | . "WHERE [t].[name] = ? AND [is_primary_key] = 0 \n" 80 | . 'ORDER BY [indexes].[name], [indexes].[index_id], [columns].[index_column_id]'; 81 | 82 | $result = $indexes = []; 83 | foreach ($this->driver->query($query, [$this->getName()]) as $index) { 84 | //Collecting schemas first 85 | $indexes[$index['indexName']][] = $index; 86 | } 87 | 88 | foreach ($indexes as $name => $schema) { 89 | //Once all columns are aggregated we can finally create an index 90 | $result[] = SQLServerIndex::createInstance($this->getName(), $schema); 91 | } 92 | 93 | return $result; 94 | } 95 | 96 | /** 97 | * {@inheritdoc} 98 | */ 99 | protected function fetchReferences(): array 100 | { 101 | $query = $this->driver->query('sp_fkeys @fktable_name = ?', [$this->getName()]); 102 | 103 | // join keys together 104 | $fks = []; 105 | foreach ($query as $schema) { 106 | if (!isset($fks[$schema['FK_NAME']])) { 107 | $fks[$schema['FK_NAME']] = $schema; 108 | $fks[$schema['FK_NAME']]['PKCOLUMN_NAME'] = [$schema['PKCOLUMN_NAME']]; 109 | $fks[$schema['FK_NAME']]['FKCOLUMN_NAME'] = [$schema['FKCOLUMN_NAME']]; 110 | continue; 111 | } 112 | 113 | $fks[$schema['FK_NAME']]['PKCOLUMN_NAME'][] = $schema['PKCOLUMN_NAME']; 114 | $fks[$schema['FK_NAME']]['FKCOLUMN_NAME'][] = $schema['FKCOLUMN_NAME']; 115 | } 116 | 117 | $result = []; 118 | foreach ($fks as $schema) { 119 | $result[] = SQlServerForeignKey::createInstance( 120 | $this->getName(), 121 | $this->getPrefix(), 122 | $schema 123 | ); 124 | } 125 | 126 | return $result; 127 | } 128 | 129 | /** 130 | * {@inheritdoc} 131 | */ 132 | protected function fetchPrimaryKeys(): array 133 | { 134 | $query = "SELECT [indexes].[name] AS [indexName], [cl].[name] AS [columnName]\n" 135 | . "FROM [sys].[indexes] AS [indexes]\n" 136 | . "INNER JOIN [sys].[index_columns] as [columns]\n" 137 | . " ON [indexes].[object_id] = [columns].[object_id] AND [indexes].[index_id] = [columns].[index_id]\n" 138 | . "INNER JOIN [sys].[columns] AS [cl]\n" 139 | . " ON [columns].[object_id] = [cl].[object_id] AND [columns].[column_id] = [cl].[column_id]\n" 140 | . "INNER JOIN [sys].[tables] AS [t]\n" 141 | . " ON [indexes].[object_id] = [t].[object_id]\n" 142 | . "WHERE [t].[name] = ? AND [is_primary_key] = 1 ORDER BY [indexes].[name], \n" 143 | . ' [indexes].[index_id], [columns].[index_column_id]'; 144 | 145 | $result = []; 146 | foreach ($this->driver->query($query, [$this->getName()]) as $schema) { 147 | $result[] = $schema['columnName']; 148 | } 149 | 150 | return $result; 151 | } 152 | 153 | /** 154 | * {@inheritdoc} 155 | */ 156 | protected function createColumn(string $name): AbstractColumn 157 | { 158 | return new SQLServerColumn($this->getName(), $name, $this->driver->getTimezone()); 159 | } 160 | 161 | /** 162 | * {@inheritdoc} 163 | */ 164 | protected function createIndex(string $name): AbstractIndex 165 | { 166 | return new SQLServerIndex($this->getName(), $name); 167 | } 168 | 169 | /** 170 | * {@inheritdoc} 171 | */ 172 | protected function createForeign(string $name): AbstractForeignKey 173 | { 174 | return new SQlServerForeignKey($this->getName(), $this->getPrefix(), $name); 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /src/Driver/SQLServer/Schema/SQlServerForeignKey.php: -------------------------------------------------------------------------------- 1 | columns = $schema['FKCOLUMN_NAME']; 29 | $foreign->foreignTable = $schema['PKTABLE_NAME']; 30 | $foreign->foreignKeys = $schema['PKCOLUMN_NAME']; 31 | 32 | $foreign->deleteRule = $schema['DELETE_RULE'] ? self::NO_ACTION : self::CASCADE; 33 | $foreign->updateRule = $schema['UPDATE_RULE'] ? self::NO_ACTION : self::CASCADE; 34 | 35 | return $foreign; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Driver/SQLite/SQLiteCompiler.php: -------------------------------------------------------------------------------- 1 | push(new Parameter($limit)); 40 | } 41 | 42 | if ($offset !== null) { 43 | $statement .= 'OFFSET ?'; 44 | $params->push(new Parameter($offset)); 45 | } 46 | 47 | return trim($statement); 48 | } 49 | 50 | /** 51 | * @inheritDoc 52 | */ 53 | protected function selectQuery(QueryParameters $params, Quoter $q, array $tokens): string 54 | { 55 | // FOR UPDATE is not available 56 | $tokens['forUpdate'] = false; 57 | 58 | return parent::selectQuery($params, $q, $tokens); 59 | } 60 | 61 | /** 62 | * {@inheritdoc} 63 | * 64 | * @see http://stackoverflow.com/questions/1609637/is-it-possible-to-insert-multiple-rows-at-a-time-in-an-sqlite-database 65 | */ 66 | protected function insertQuery(QueryParameters $params, Quoter $q, array $tokens): string 67 | { 68 | if ($tokens['columns'] === []) { 69 | return sprintf( 70 | 'INSERT INTO %s DEFAULT VALUES', 71 | $this->name($params, $q, $tokens['table'], true) 72 | ); 73 | } 74 | 75 | // @todo possibly different statement for versions higher than 3.7.11 76 | if (count($tokens['values']) === 1) { 77 | return parent::insertQuery($params, $q, $tokens); 78 | } 79 | 80 | // SQLite uses alternative syntax 81 | $statement = []; 82 | $statement[] = sprintf( 83 | 'INSERT INTO %s (%s)', 84 | $this->name($params, $q, $tokens['table'], true), 85 | $this->columns($params, $q, $tokens['columns']) 86 | ); 87 | 88 | foreach ($tokens['values'] as $rowset) { 89 | if (count($statement) !== 1) { 90 | // It is critically important to use UNION ALL, UNION will try to merge values together 91 | // which will cause non predictable insert order 92 | $statement[] = sprintf( 93 | 'UNION ALL SELECT %s', 94 | trim($this->value($params, $q, $rowset), '()') 95 | ); 96 | continue; 97 | } 98 | 99 | $selectColumns = []; 100 | 101 | if ($rowset instanceof ParameterInterface && $rowset->isArray()) { 102 | $rowset = $rowset->getValue(); 103 | } 104 | 105 | if (!is_array($rowset)) { 106 | throw new CompilerException( 107 | 'Insert parameter expected to be parametric array' 108 | ); 109 | } 110 | 111 | foreach ($tokens['columns'] as $index => $column) { 112 | $selectColumns[] = sprintf( 113 | '%s AS %s', 114 | $this->value($params, $q, $rowset[$index]), 115 | $this->name($params, $q, $column) 116 | ); 117 | } 118 | 119 | $statement[] = 'SELECT ' . implode(', ', $selectColumns); 120 | } 121 | 122 | return implode("\n", $statement); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/Driver/SQLite/SQLiteDriver.php: -------------------------------------------------------------------------------- 1 | getDSN(), 7); 49 | } 50 | 51 | /** 52 | * @inheritDoc 53 | */ 54 | protected function mapException(Throwable $exception, string $query): StatementException 55 | { 56 | if ((int)$exception->getCode() === 23000) { 57 | return new StatementException\ConstrainException($exception, $query); 58 | } 59 | 60 | return new StatementException($exception, $query); 61 | } 62 | 63 | /** 64 | * {@inheritdoc} 65 | */ 66 | protected function setIsolationLevel(string $level): void 67 | { 68 | if ($this->logger !== null) { 69 | $this->logger->alert( 70 | "Transaction isolation level is not fully supported by SQLite ({$level})" 71 | ); 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/Driver/SQLite/Schema/SQLiteForeignKey.php: -------------------------------------------------------------------------------- 1 | tablePrefix . $this->table . '_' . implode('_', $this->columns) . '_fk'; 27 | } 28 | 29 | /** 30 | * {@inheritdoc} 31 | */ 32 | public function sqlStatement(DriverInterface $driver): string 33 | { 34 | $statement = []; 35 | 36 | $statement[] = 'FOREIGN KEY'; 37 | $statement[] = '(' . $this->packColumns($driver, $this->columns) . ')'; 38 | 39 | $statement[] = 'REFERENCES ' . $driver->identifier($this->foreignTable); 40 | $statement[] = '(' . $this->packColumns($driver, $this->foreignKeys) . ')'; 41 | 42 | $statement[] = "ON DELETE {$this->deleteRule}"; 43 | $statement[] = "ON UPDATE {$this->updateRule}"; 44 | 45 | return implode(' ', $statement); 46 | } 47 | 48 | /** 49 | * Name insensitive compare. 50 | * 51 | * @param AbstractForeignKey $initial 52 | * @return bool 53 | */ 54 | public function compare(AbstractForeignKey $initial): bool 55 | { 56 | return $this->getColumns() === $initial->getColumns() 57 | && $this->getForeignTable() === $initial->getForeignTable() 58 | && $this->getForeignKeys() === $initial->getForeignKeys() 59 | && $this->getUpdateRule() === $initial->getUpdateRule() 60 | && $this->getDeleteRule() === $initial->getDeleteRule(); 61 | } 62 | 63 | /** 64 | * @param string $table 65 | * @param string $tablePrefix 66 | * @param array $schema 67 | * @return SQLiteForeignKey 68 | */ 69 | public static function createInstance(string $table, string $tablePrefix, array $schema): self 70 | { 71 | $reference = new self($table, $tablePrefix, (string) $schema['id']); 72 | 73 | $reference->columns = $schema['from']; 74 | $reference->foreignTable = $schema['table']; 75 | $reference->foreignKeys = $schema['to']; 76 | 77 | //In SQLLite we have to work with pre-defined reference names 78 | $reference->name = $reference->getName(); 79 | 80 | $reference->deleteRule = $schema['on_delete']; 81 | $reference->updateRule = $schema['on_update']; 82 | 83 | return $reference; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/Driver/SQLite/Schema/SQLiteIndex.php: -------------------------------------------------------------------------------- 1 | type = $schema['unique'] ? self::UNIQUE : self::NORMAL; 34 | 35 | if ($columns !== []) { 36 | foreach ($columns as $column) { 37 | // We only need key columns 38 | if (intval($column['cid']) > -1) { 39 | $index->columns[] = $column['name']; 40 | if (intval($column['desc']) === 1) { 41 | $index->sort[$column['name']] = 'DESC'; 42 | } 43 | } 44 | } 45 | } else { 46 | // use legacy format 47 | foreach ($fallbackColumns as $column) { 48 | $index->columns[] = $column['name']; 49 | } 50 | } 51 | 52 | return $index; 53 | } 54 | 55 | /** 56 | * @inheritdoc 57 | */ 58 | public function sqlStatement(DriverInterface $driver, bool $includeTable = true): string 59 | { 60 | $statement = [$this->isUnique() ? 'UNIQUE INDEX' : 'INDEX']; 61 | 62 | //SQLite love to add indexes without being asked for that 63 | $statement[] = 'IF NOT EXISTS'; 64 | $statement[] = $driver->identifier($this->name); 65 | 66 | if ($includeTable) { 67 | $statement[] = "ON {$driver->identifier($this->table)}"; 68 | } 69 | 70 | //Wrapping column names 71 | $columns = []; 72 | foreach ($this->columns as $column) { 73 | $quoted = $driver->identifier($column); 74 | if ($order = $this->sort[$column] ?? null) { 75 | $quoted = "$quoted $order"; 76 | } 77 | 78 | $columns[] = $quoted; 79 | } 80 | $columns = implode(', ', $columns); 81 | 82 | $statement[] = "({$columns})"; 83 | 84 | return implode(' ', $statement); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/Driver/SQLite/Schema/SQLiteTable.php: -------------------------------------------------------------------------------- 1 | driver->query( 30 | "SELECT sql FROM sqlite_master WHERE type = 'table' and name = ?", 31 | [$this->getName()] 32 | )->fetchColumn(); 33 | 34 | /* 35 | * There is not really many ways to get extra information about column in SQLite, let's parse 36 | * table schema. As mention, spiral SQLite schema reader will support fully only tables created 37 | * by spiral as we expecting every column definition be on new line. 38 | */ 39 | $definition = explode("\n", $definition); 40 | 41 | $result = []; 42 | foreach ($this->columnSchemas(['table' => $definition]) as $schema) { 43 | //Making new column instance 44 | $result[] = SQLiteColumn::createInstance( 45 | $this->getName(), 46 | $schema + [ 47 | 'quoted' => $this->driver->quote($schema['name']), 48 | 'identifier' => $this->driver->identifier($schema['name']) 49 | ], 50 | $this->driver->getTimezone() 51 | ); 52 | } 53 | 54 | return $result; 55 | } 56 | 57 | /** 58 | * {@inheritdoc} 59 | */ 60 | protected function fetchIndexes(): array 61 | { 62 | $primaryKeys = $this->fetchPrimaryKeys(); 63 | $query = "PRAGMA index_list({$this->driver->quote($this->getName())})"; 64 | 65 | $result = []; 66 | foreach ($this->driver->query($query) as $schema) { 67 | $index = SQLiteIndex::createInstance( 68 | $this->getName(), 69 | $schema, 70 | // 3+ format 71 | $this->driver->query( 72 | "PRAGMA INDEX_XINFO({$this->driver->quote($schema['name'])})" 73 | )->fetchAll(), 74 | // legacy format 75 | $this->driver->query( 76 | "PRAGMA INDEX_INFO({$this->driver->quote($schema['name'])})" 77 | )->fetchAll() 78 | ); 79 | 80 | if ($index->getColumns() === $primaryKeys) { 81 | // skip auto-generated index 82 | continue; 83 | } 84 | 85 | //Index schema and all related columns 86 | $result[] = $index; 87 | } 88 | 89 | return $result; 90 | } 91 | 92 | /** 93 | * {@inheritdoc} 94 | */ 95 | protected function fetchReferences(): array 96 | { 97 | $query = "PRAGMA foreign_key_list({$this->driver->quote($this->getName())})"; 98 | 99 | // join keys together 100 | $fks = []; 101 | foreach ($this->driver->query($query) as $schema) { 102 | if (!isset($fks[$schema['id']])) { 103 | $fks[$schema['id']] = $schema; 104 | $fks[$schema['id']]['from'] = [$schema['from']]; 105 | $fks[$schema['id']]['to'] = [$schema['to']]; 106 | continue; 107 | } 108 | 109 | $fks[$schema['id']]['from'][] = $schema['from']; 110 | $fks[$schema['id']]['to'][] = $schema['to']; 111 | } 112 | 113 | $result = []; 114 | foreach ($fks as $schema) { 115 | $result[] = SQLiteForeignKey::createInstance( 116 | $this->getName(), 117 | $this->getPrefix(), 118 | $schema 119 | ); 120 | } 121 | 122 | return $result; 123 | } 124 | 125 | /** 126 | * Fetching primary keys from table. 127 | * 128 | * @return array 129 | */ 130 | protected function fetchPrimaryKeys(): array 131 | { 132 | $primaryKeys = []; 133 | foreach ($this->columnSchemas() as $column) { 134 | if (!empty($column['pk'])) { 135 | $primaryKeys[] = $column['name']; 136 | } 137 | } 138 | 139 | return $primaryKeys; 140 | } 141 | 142 | /** 143 | * {@inheritdoc} 144 | */ 145 | protected function createColumn(string $name): AbstractColumn 146 | { 147 | return new SQLiteColumn($this->getName(), $name, $this->driver->getTimezone()); 148 | } 149 | 150 | /** 151 | * {@inheritdoc} 152 | */ 153 | protected function createIndex(string $name): AbstractIndex 154 | { 155 | return new SQLiteIndex($this->getName(), $name); 156 | } 157 | 158 | /** 159 | * {@inheritdoc} 160 | */ 161 | protected function createForeign(string $name): AbstractForeignKey 162 | { 163 | return new SQLiteForeignKey($this->getName(), $this->getPrefix(), $name); 164 | } 165 | 166 | /** 167 | * @param array $include Include following parameters into each line. 168 | * 169 | * @return array 170 | */ 171 | private function columnSchemas(array $include = []): array 172 | { 173 | $columns = $this->driver->query( 174 | 'PRAGMA TABLE_INFO(' . $this->driver->quote($this->getName()) . ')' 175 | ); 176 | 177 | $result = []; 178 | 179 | foreach ($columns as $column) { 180 | $result[] = $column + $include; 181 | } 182 | 183 | return $result; 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /src/Driver/Statement.php: -------------------------------------------------------------------------------- 1 | pdoStatement = $pdoStatement; 35 | $this->pdoStatement->setFetchMode(self::FETCH_ASSOC); 36 | } 37 | 38 | /** 39 | * @return string 40 | */ 41 | public function getQueryString(): string 42 | { 43 | return $this->pdoStatement->queryString; 44 | } 45 | 46 | /** 47 | * @return PDOStatement 48 | */ 49 | public function getPDOStatement(): PDOStatement 50 | { 51 | return $this->pdoStatement; 52 | } 53 | 54 | /** 55 | * @inheritDoc 56 | */ 57 | public function fetch(int $mode = self::FETCH_ASSOC) 58 | { 59 | return $this->pdoStatement->fetch($mode); 60 | } 61 | 62 | /** 63 | * @inheritDoc 64 | */ 65 | public function fetchColumn(int $columnNumber = null) 66 | { 67 | if ($columnNumber === null) { 68 | return $this->pdoStatement->fetchColumn(); 69 | } 70 | 71 | return $this->pdoStatement->fetchColumn($columnNumber); 72 | } 73 | 74 | /** 75 | * @param int $mode 76 | * @return array 77 | */ 78 | public function fetchAll(int $mode = self::FETCH_ASSOC): array 79 | { 80 | return $this->pdoStatement->fetchAll($mode); 81 | } 82 | 83 | /** 84 | * @return int 85 | */ 86 | public function rowCount(): int 87 | { 88 | return $this->pdoStatement->rowCount(); 89 | } 90 | 91 | /** 92 | * @return int 93 | */ 94 | public function columnCount(): int 95 | { 96 | return $this->pdoStatement->columnCount(); 97 | } 98 | 99 | /** 100 | * @return Generator 101 | */ 102 | public function getIterator(): Generator 103 | { 104 | foreach ($this->pdoStatement as $row) { 105 | yield $row; 106 | } 107 | 108 | $this->close(); 109 | } 110 | 111 | /** 112 | * {@inheritdoc} 113 | */ 114 | public function close(): void 115 | { 116 | $this->pdoStatement->closeCursor(); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/Exception/BuilderException.php: -------------------------------------------------------------------------------- 1 | getMessage(), $e->getCode(), $e); 25 | } 26 | 27 | /** 28 | * {@inheritdoc} 29 | */ 30 | public function getQuery(): string 31 | { 32 | return $this->getPrevious()->getQuery(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Exception/InterpolatorException.php: -------------------------------------------------------------------------------- 1 | getMessage(), (int)$previous->getCode(), $previous); 30 | $this->query = $query; 31 | } 32 | 33 | /** 34 | * {@inheritdoc} 35 | */ 36 | public function getQuery(): string 37 | { 38 | return $this->query; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Exception/StatementException/ConnectionException.php: -------------------------------------------------------------------------------- 1 | expression = $statement; 40 | 41 | foreach ($parameters as $parameter) { 42 | if ($parameter instanceof ParameterInterface) { 43 | $this->parameters[] = $parameter; 44 | } else { 45 | $this->parameters[] = new Parameter($parameter); 46 | } 47 | } 48 | } 49 | 50 | /** 51 | * @return string 52 | */ 53 | public function __toString(): string 54 | { 55 | return 'exp:' . $this->expression; 56 | } 57 | 58 | /** 59 | * @param array $an_array 60 | * @return Expression 61 | */ 62 | public static function __set_state(array $an_array): Expression 63 | { 64 | return new self( 65 | $an_array['expression'] ?? $an_array['statement'], 66 | ...($an_array['parameters'] ?? []) 67 | ); 68 | } 69 | 70 | /** 71 | * @return int 72 | */ 73 | public function getType(): int 74 | { 75 | return CompilerInterface::EXPRESSION; 76 | } 77 | 78 | /** 79 | * @return array 80 | */ 81 | public function getTokens(): array 82 | { 83 | return [ 84 | 'expression' => $this->expression, 85 | 'parameters' => $this->parameters 86 | ]; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/Injection/Fragment.php: -------------------------------------------------------------------------------- 1 | where('time_created', '>', new SQLFragment("NOW()")); 21 | */ 22 | class Fragment implements FragmentInterface 23 | { 24 | /** @var string */ 25 | private $fragment; 26 | 27 | /** @var ParameterInterface[] */ 28 | private $parameters = []; 29 | 30 | /** 31 | * @param string $fragment 32 | * @param mixed ...$parameters 33 | */ 34 | public function __construct(string $fragment, ...$parameters) 35 | { 36 | $this->fragment = $fragment; 37 | foreach ($parameters as $parameter) { 38 | if ($parameter instanceof ParameterInterface) { 39 | $this->parameters[] = $parameter; 40 | } else { 41 | $this->parameters[] = new Parameter($parameter); 42 | } 43 | } 44 | } 45 | 46 | /** 47 | * @return string 48 | */ 49 | public function __toString(): string 50 | { 51 | return $this->fragment; 52 | } 53 | 54 | /** 55 | * @param array $an_array 56 | * @return Fragment 57 | */ 58 | public static function __set_state(array $an_array): Fragment 59 | { 60 | return new self( 61 | $an_array['fragment'] ?? $an_array['statement'], 62 | ...($an_array['parameters'] ?? []) 63 | ); 64 | } 65 | 66 | /** 67 | * @return int 68 | */ 69 | public function getType(): int 70 | { 71 | return CompilerInterface::FRAGMENT; 72 | } 73 | 74 | /** 75 | * @return array 76 | */ 77 | public function getTokens(): array 78 | { 79 | return [ 80 | 'fragment' => $this->fragment, 81 | 'parameters' => $this->parameters 82 | ]; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/Injection/FragmentInterface.php: -------------------------------------------------------------------------------- 1 | setValue($value, $type); 40 | } 41 | 42 | /** 43 | * @return array 44 | */ 45 | public function __debugInfo() 46 | { 47 | return [ 48 | 'value' => $this->value, 49 | 'type' => $this->type 50 | ]; 51 | } 52 | 53 | /** 54 | * Parameter type. 55 | * 56 | * @return int 57 | */ 58 | public function getType(): int 59 | { 60 | return $this->type; 61 | } 62 | 63 | /** 64 | * @param mixed $value 65 | * @param int $type 66 | */ 67 | public function setValue($value, int $type = self::DETECT_TYPE): void 68 | { 69 | $this->value = $value; 70 | 71 | if ($value instanceof ValueInterface) { 72 | $this->type = $value->rawType(); 73 | return; 74 | } 75 | 76 | if ($type !== self::DETECT_TYPE) { 77 | $this->type = $type; 78 | } elseif (!is_array($value)) { 79 | $this->type = $this->detectType($value); 80 | } 81 | } 82 | 83 | /** 84 | * {@inheritdoc} 85 | */ 86 | public function getValue() 87 | { 88 | if ($this->value instanceof ValueInterface) { 89 | return $this->value->rawValue(); 90 | } 91 | 92 | return $this->value; 93 | } 94 | 95 | /** 96 | * @return bool 97 | */ 98 | public function isArray(): bool 99 | { 100 | return is_array($this->value); 101 | } 102 | 103 | /** 104 | * @return bool 105 | */ 106 | public function isNull(): bool 107 | { 108 | return $this->value === null; 109 | } 110 | 111 | /** 112 | * @param mixed $value 113 | * 114 | * @return int 115 | */ 116 | private function detectType($value): int 117 | { 118 | switch (gettype($value)) { 119 | case 'boolean': 120 | return PDO::PARAM_BOOL; 121 | case 'integer': 122 | return PDO::PARAM_INT; 123 | case 'NULL': 124 | return PDO::PARAM_NULL; 125 | default: 126 | return PDO::PARAM_STR; 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/Injection/ParameterInterface.php: -------------------------------------------------------------------------------- 1 | sqlStatement($parameters), 40 | $parameters->getParameters() 41 | ); 42 | } 43 | 44 | /** 45 | * @return array 46 | */ 47 | public function __debugInfo() 48 | { 49 | $parameters = new QueryParameters(); 50 | 51 | try { 52 | $queryString = $this->sqlStatement($parameters); 53 | } catch (Throwable $e) { 54 | $queryString = "[ERROR: {$e->getMessage()}]"; 55 | } 56 | 57 | return [ 58 | 'queryString' => Interpolator::interpolate($queryString, $parameters->getParameters()), 59 | 'parameters' => $parameters->getParameters(), 60 | 'driver' => $this->driver 61 | ]; 62 | } 63 | 64 | /** 65 | * @param DriverInterface $driver 66 | * @param string|null $prefix 67 | * @return QueryInterface|$this 68 | */ 69 | public function withDriver(DriverInterface $driver, string $prefix = null): QueryInterface 70 | { 71 | $query = clone $this; 72 | $query->driver = $driver; 73 | $query->prefix = $prefix; 74 | 75 | return $query; 76 | } 77 | 78 | /** 79 | * @return DriverInterface|null 80 | */ 81 | public function getDriver(): ?DriverInterface 82 | { 83 | return $this->driver; 84 | } 85 | 86 | /** 87 | * @return string|null 88 | */ 89 | public function getPrefix(): ?string 90 | { 91 | return $this->prefix; 92 | } 93 | 94 | /** 95 | * Generate SQL query, must have associated driver instance. 96 | * 97 | * @param QueryParameters|null $parameters 98 | * @return string 99 | */ 100 | public function sqlStatement(QueryParameters $parameters = null): string 101 | { 102 | if ($this->driver === null) { 103 | throw new BuilderException('Unable to build query without associated driver'); 104 | } 105 | 106 | return $this->driver->getQueryCompiler()->compile( 107 | $parameters ?? new QueryParameters(), 108 | $this->prefix, 109 | $this 110 | ); 111 | } 112 | 113 | /** 114 | * Compile and run query. 115 | * 116 | * @return mixed 117 | * 118 | * @throws BuilderException 119 | * @throws StatementException 120 | */ 121 | abstract public function run(); 122 | 123 | /** 124 | * Helper methods used to correctly fetch and split identifiers provided by function 125 | * parameters. Example: fI(['name, email']) => 'name', 'email' 126 | * 127 | * @param array $identifiers 128 | * 129 | * @return array 130 | */ 131 | protected function fetchIdentifiers(array $identifiers): array 132 | { 133 | if (count($identifiers) === 1 && is_string($identifiers[0])) { 134 | return array_map('trim', explode(',', $identifiers[0])); 135 | } 136 | 137 | if (count($identifiers) === 1 && is_array($identifiers[0])) { 138 | return $identifiers[0]; 139 | } 140 | 141 | return $identifiers; 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/Query/BuilderInterface.php: -------------------------------------------------------------------------------- 1 | table = $table ?? ''; 36 | 37 | if ($where !== []) { 38 | $this->where($where); 39 | } 40 | } 41 | 42 | /** 43 | * Change target table. 44 | * 45 | * @param string $into Table name without prefix. 46 | * @return self 47 | */ 48 | public function from(string $into): DeleteQuery 49 | { 50 | $this->table = $into; 51 | 52 | return $this; 53 | } 54 | 55 | /** 56 | * Alias for execute method(); 57 | * 58 | * @return int 59 | */ 60 | public function run(): int 61 | { 62 | $params = new QueryParameters(); 63 | $queryString = $this->sqlStatement($params); 64 | 65 | return $this->driver->execute($queryString, $params->getParameters()); 66 | } 67 | 68 | /** 69 | * @return int 70 | */ 71 | public function getType(): int 72 | { 73 | return CompilerInterface::DELETE_QUERY; 74 | } 75 | 76 | /** 77 | * @return array 78 | */ 79 | public function getTokens(): array 80 | { 81 | return [ 82 | 'table' => $this->table, 83 | 'where' => $this->whereTokens 84 | ]; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/Query/InsertQuery.php: -------------------------------------------------------------------------------- 1 | table = $table ?? ''; 37 | } 38 | 39 | /** 40 | * Set target insertion table. 41 | * 42 | * @param string $into 43 | * @return self 44 | */ 45 | public function into(string $into): InsertQuery 46 | { 47 | $this->table = $into; 48 | 49 | return $this; 50 | } 51 | 52 | /** 53 | * Set insertion column names. Names can be provided as array, set of parameters or comma 54 | * separated string. 55 | * 56 | * Examples: 57 | * $insert->columns(["name", "email"]); 58 | * $insert->columns("name", "email"); 59 | * $insert->columns("name, email"); 60 | * 61 | * @param array|string $columns 62 | * @return self 63 | */ 64 | public function columns(...$columns): InsertQuery 65 | { 66 | $this->columns = $this->fetchIdentifiers($columns); 67 | 68 | return $this; 69 | } 70 | 71 | /** 72 | * Set insertion rowset values or multiple rowsets. Values can be provided in multiple forms 73 | * (method parameters, array of values, array or rowsets). Columns names will be automatically 74 | * fetched (if not already specified) from first provided rowset based on rowset keys. 75 | * 76 | * Examples: 77 | * $insert->columns("name", "balance")->values("Wolfy-J", 10); 78 | * $insert->values([ 79 | * "name" => "Wolfy-J", 80 | * "balance" => 10 81 | * ]); 82 | * $insert->values([ 83 | * [ 84 | * "name" => "Wolfy-J", 85 | * "balance" => 10 86 | * ], 87 | * [ 88 | * "name" => "Ben", 89 | * "balance" => 20 90 | * ] 91 | * ]); 92 | * 93 | * @param mixed $rowsets 94 | * @return self 95 | */ 96 | public function values($rowsets): InsertQuery 97 | { 98 | if (!is_array($rowsets)) { 99 | return $this->values(func_get_args()); 100 | } 101 | 102 | if ($rowsets === []) { 103 | return $this; 104 | } 105 | 106 | //Checking if provided set is array of multiple 107 | reset($rowsets); 108 | 109 | if (!is_array($rowsets[key($rowsets)])) { 110 | if ($this->columns === []) { 111 | $this->columns = array_keys($rowsets); 112 | } 113 | 114 | $this->values[] = new Parameter(array_values($rowsets)); 115 | } else { 116 | foreach ($rowsets as $values) { 117 | $this->values[] = new Parameter(array_values($values)); 118 | } 119 | } 120 | 121 | return $this; 122 | } 123 | 124 | /** 125 | * Run the query and return last insert id. 126 | * 127 | * @return int|string|null 128 | */ 129 | public function run() 130 | { 131 | $params = new QueryParameters(); 132 | $queryString = $this->sqlStatement($params); 133 | 134 | $this->driver->execute( 135 | $queryString, 136 | $params->getParameters() 137 | ); 138 | 139 | $lastID = $this->driver->lastInsertID(); 140 | if (is_numeric($lastID)) { 141 | return (int)$lastID; 142 | } 143 | 144 | return $lastID; 145 | } 146 | 147 | /** 148 | * @return int 149 | */ 150 | public function getType(): int 151 | { 152 | return CompilerInterface::INSERT_QUERY; 153 | } 154 | 155 | /** 156 | * @return array 157 | */ 158 | public function getTokens(): array 159 | { 160 | return [ 161 | 'table' => $this->table, 162 | 'columns' => $this->columns, 163 | 'values' => $this->values 164 | ]; 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /src/Query/Interpolator.php: -------------------------------------------------------------------------------- 1 | $named, 'unnamed' => $unnamed] = self::normalizeParameters($parameters); 37 | $params = self::findParams($query); 38 | 39 | $caret = 0; 40 | $result = ''; 41 | foreach ($params as $pos => $ph) { 42 | $result .= \substr($query, $caret, $pos - $caret); 43 | $caret = $pos + \strlen($ph); 44 | // find param 45 | if ($ph === '?' && \count($unnamed) > 0) { 46 | $result .= self::resolveValue(\array_shift($unnamed)); 47 | } elseif (\array_key_exists($ph, $named)) { 48 | $result .= self::resolveValue($named[$ph]); 49 | } else { 50 | $result .= $ph; 51 | } 52 | } 53 | $result .= \substr($query, $caret); 54 | 55 | return $result; 56 | } 57 | 58 | /** 59 | * Get parameter value. 60 | * 61 | * @psalm-return non-empty-string 62 | */ 63 | private static function resolveValue($parameter): string 64 | { 65 | if ($parameter instanceof ParameterInterface) { 66 | return self::resolveValue($parameter->getValue()); 67 | } 68 | 69 | switch (\gettype($parameter)) { 70 | case 'boolean': 71 | return $parameter ? 'TRUE' : 'FALSE'; 72 | 73 | case 'integer': 74 | return (string)$parameter; 75 | 76 | case 'NULL': 77 | return 'NULL'; 78 | 79 | case 'double': 80 | return \sprintf('%F', $parameter); 81 | 82 | case 'string': 83 | return "'" . self::escapeStringValue($parameter) . "'"; 84 | 85 | case 'object': 86 | if (method_exists($parameter, '__toString')) { 87 | return "'" . self::escapeStringValue((string)$parameter) . "'"; 88 | } 89 | 90 | if ($parameter instanceof DateTimeInterface) { 91 | return "'" . $parameter->format(DateTimeInterface::ATOM) . "'"; 92 | } 93 | } 94 | 95 | return '[UNRESOLVED]'; 96 | } 97 | 98 | private static function escapeStringValue(string $value): string 99 | { 100 | return \strtr($value, [ 101 | '\\%' => '\\%', 102 | '\\_' => '\\_', 103 | \chr(26) => '\\Z', 104 | \chr(0) => '\\0', 105 | "'" => "\\'", 106 | \chr(8) => '\\b', 107 | "\n" => '\\n', 108 | "\r" => '\\r', 109 | "\t" => '\\t', 110 | '\\' => '\\\\', 111 | ]); 112 | } 113 | 114 | /** 115 | * @return array 116 | */ 117 | private static function findParams(string $query): array 118 | { 119 | \preg_match_all( 120 | '/(?"(?:\\\\\"|[^"])*")|(?\'(?:\\\\\'|[^\'])*\')|(?\\?)|(?:[a-z_\\d]+)/', 121 | $query, 122 | $placeholders, 123 | PREG_OFFSET_CAPTURE | PREG_UNMATCHED_AS_NULL 124 | ); 125 | $result = []; 126 | foreach (array_merge($placeholders['named'], $placeholders['ph']) as $tuple) { 127 | if ($tuple[0] === null) { 128 | continue; 129 | } 130 | $result[$tuple[1]] = $tuple[0]; 131 | } 132 | \ksort($result); 133 | 134 | return $result; 135 | } 136 | 137 | /** 138 | * @return array{named: array, unnamed: array} 139 | */ 140 | private static function normalizeParameters(iterable $parameters): array 141 | { 142 | $result = ['named' => [], 'unnamed' => []]; 143 | foreach ($parameters as $k => $v) { 144 | if (\is_int($k)) { 145 | $result['unnamed'][$k] = $v; 146 | } else { 147 | $result['named'][':' . \ltrim($k, ':')] = $v; 148 | } 149 | } 150 | 151 | return $result; 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/Query/QueryBuilder.php: -------------------------------------------------------------------------------- 1 | selectQuery = $selectQuery; 51 | $this->insertQuery = $insertQuery; 52 | $this->updateQuery = $updateQuery; 53 | $this->deleteQuery = $deleteQuery; 54 | } 55 | 56 | /** 57 | * @param DriverInterface $driver 58 | * @return BuilderInterface 59 | */ 60 | public function withDriver(DriverInterface $driver): BuilderInterface 61 | { 62 | $builder = clone $this; 63 | $builder->driver = $driver; 64 | 65 | return $builder; 66 | } 67 | 68 | /** 69 | * Get InsertQuery builder with driver specific query compiler. 70 | * 71 | * @param string $prefix 72 | * @param string|null $table 73 | * @return InsertQuery 74 | */ 75 | public function insertQuery( 76 | string $prefix, 77 | string $table = null 78 | ): InsertQuery { 79 | $insert = $this->insertQuery->withDriver($this->driver, $prefix); 80 | 81 | if ($table !== null) { 82 | $insert->into($table); 83 | } 84 | 85 | return $insert; 86 | } 87 | 88 | /** 89 | * Get SelectQuery builder with driver specific query compiler. 90 | * 91 | * @param string $prefix 92 | * @param array $from 93 | * @param array $columns 94 | * @return SelectQuery 95 | */ 96 | public function selectQuery( 97 | string $prefix, 98 | array $from = [], 99 | array $columns = [] 100 | ): SelectQuery { 101 | $select = $this->selectQuery->withDriver($this->driver, $prefix); 102 | 103 | if ($columns === []) { 104 | $columns = ['*']; 105 | } 106 | 107 | return $select->from($from)->columns($columns); 108 | } 109 | 110 | /** 111 | * @param string $prefix 112 | * @param string|null $from 113 | * @param array $where 114 | * @return DeleteQuery 115 | */ 116 | public function deleteQuery( 117 | string $prefix, 118 | string $from = null, 119 | array $where = [] 120 | ): DeleteQuery { 121 | $delete = $this->deleteQuery->withDriver($this->driver, $prefix); 122 | 123 | if ($from !== null) { 124 | $delete->from($from); 125 | } 126 | 127 | return $delete->where($where); 128 | } 129 | 130 | /** 131 | * Get UpdateQuery builder with driver specific query compiler. 132 | * 133 | * @param string $prefix 134 | * @param string|null $table 135 | * @param array $where 136 | * @param array $values 137 | * @return UpdateQuery 138 | */ 139 | public function updateQuery( 140 | string $prefix, 141 | string $table = null, 142 | array $where = [], 143 | array $values = [] 144 | ): UpdateQuery { 145 | $update = $this->updateQuery->withDriver($this->driver, $prefix); 146 | 147 | if ($table !== null) { 148 | $update->in($table); 149 | } 150 | 151 | return $update->where($where)->values($values); 152 | } 153 | 154 | /** 155 | * @return QueryBuilder 156 | */ 157 | public static function defaultBuilder(): QueryBuilder 158 | { 159 | return new self( 160 | new SelectQuery(), 161 | new InsertQuery(), 162 | new UpdateQuery(), 163 | new DeleteQuery() 164 | ); 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /src/Query/QueryInterface.php: -------------------------------------------------------------------------------- 1 | isArray()) { 29 | foreach ($parameter->getValue() as $value) { 30 | $this->flatten[] = $value; 31 | } 32 | } else { 33 | $this->flatten[] = $parameter; 34 | } 35 | } 36 | 37 | /** 38 | * @return array 39 | */ 40 | public function getParameters(): array 41 | { 42 | return $this->flatten; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Query/Traits/HavingTrait.php: -------------------------------------------------------------------------------- 1 | registerToken( 36 | 'AND', 37 | $args, 38 | $this->havingTokens, 39 | $this->havingWrapper() 40 | ); 41 | 42 | return $this; 43 | } 44 | 45 | /** 46 | * Simple AND HAVING condition with various set of arguments. 47 | * 48 | * @param mixed ...$args [(column, value), (column, operator, value)] 49 | * @return self|$this 50 | * 51 | * @throws BuilderException 52 | */ 53 | public function andHaving(...$args): self 54 | { 55 | $this->registerToken( 56 | 'AND', 57 | $args, 58 | $this->havingTokens, 59 | $this->havingWrapper() 60 | ); 61 | 62 | return $this; 63 | } 64 | 65 | /** 66 | * Simple OR HAVING condition with various set of arguments. 67 | * 68 | * @param mixed ...$args [(column, value), (column, operator, value)] 69 | * 70 | * @return self|$this 71 | * 72 | * @throws BuilderException 73 | */ 74 | public function orHaving(...$args): self 75 | { 76 | $this->registerToken( 77 | 'OR', 78 | $args, 79 | $this->havingTokens, 80 | $this->havingWrapper() 81 | ); 82 | 83 | return $this; 84 | } 85 | 86 | /** 87 | * Convert various amount of where function arguments into valid where token. 88 | * 89 | * @param string $boolean Boolean joiner (AND | OR). 90 | * @param array $params Set of parameters collected from where functions. 91 | * @param array $tokens Array to aggregate compiled tokens. Reference. 92 | * @param callable $wrapper Callback or closure used to wrap/collect every potential 93 | * parameter. 94 | * 95 | * @throws BuilderException 96 | */ 97 | abstract protected function registerToken( 98 | $boolean, 99 | array $params, 100 | &$tokens, 101 | callable $wrapper 102 | ); 103 | 104 | /** 105 | * Applied to every potential parameter while having tokens generation. 106 | * 107 | * @return Closure 108 | */ 109 | private function havingWrapper(): Closure 110 | { 111 | return static function ($parameter) { 112 | if (is_array($parameter)) { 113 | throw new BuilderException( 114 | 'Arrays must be wrapped with Parameter instance' 115 | ); 116 | } 117 | 118 | if (!$parameter instanceof ParameterInterface && !$parameter instanceof FragmentInterface) { 119 | return new Parameter($parameter); 120 | } 121 | 122 | return $parameter; 123 | }; 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/Query/Traits/WhereTrait.php: -------------------------------------------------------------------------------- 1 | registerToken( 36 | 'AND', 37 | $args, 38 | $this->whereTokens, 39 | $this->whereWrapper() 40 | ); 41 | 42 | return $this; 43 | } 44 | 45 | /** 46 | * Simple AND WHERE condition with various set of arguments. 47 | * 48 | * @param mixed ...$args [(column, value), (column, operator, value)] 49 | * @return self|$this 50 | * 51 | * @throws BuilderException 52 | */ 53 | public function andWhere(...$args): self 54 | { 55 | $this->registerToken( 56 | 'AND', 57 | $args, 58 | $this->whereTokens, 59 | $this->whereWrapper() 60 | ); 61 | 62 | return $this; 63 | } 64 | 65 | /** 66 | * Simple OR WHERE condition with various set of arguments. 67 | * 68 | * @param mixed ...$args [(column, value), (column, operator, value)] 69 | * @return self|$this 70 | * 71 | * @throws BuilderException 72 | */ 73 | public function orWhere(...$args): self 74 | { 75 | $this->registerToken( 76 | 'OR', 77 | $args, 78 | $this->whereTokens, 79 | $this->whereWrapper() 80 | ); 81 | 82 | return $this; 83 | } 84 | 85 | /** 86 | * Convert various amount of where function arguments into valid where token. 87 | * 88 | * @param string $boolean Boolean joiner (AND | OR). 89 | * @param array $params Set of parameters collected from where functions. 90 | * @param array $tokens Array to aggregate compiled tokens. Reference. 91 | * @param callable $wrapper Callback or closure used to wrap/collect every potential 92 | * parameter. 93 | * @throws BuilderException 94 | */ 95 | abstract protected function registerToken( 96 | $boolean, 97 | array $params, 98 | &$tokens, 99 | callable $wrapper 100 | ); 101 | 102 | /** 103 | * Applied to every potential parameter while where tokens generation. Used to prepare and 104 | * collect where parameters. 105 | * 106 | * @return Closure 107 | */ 108 | private function whereWrapper(): Closure 109 | { 110 | return static function ($parameter) { 111 | if (is_array($parameter)) { 112 | throw new BuilderException( 113 | 'Arrays must be wrapped with Parameter instance' 114 | ); 115 | } 116 | 117 | if (!$parameter instanceof ParameterInterface && !$parameter instanceof FragmentInterface) { 118 | return new Parameter($parameter); 119 | } 120 | 121 | return $parameter; 122 | }; 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/Query/UpdateQuery.php: -------------------------------------------------------------------------------- 1 | table = $table ?? ''; 43 | $this->values = $values; 44 | 45 | if ($where !== []) { 46 | $this->where($where); 47 | } 48 | } 49 | 50 | /** 51 | * Change target table. 52 | * 53 | * @param string $table Table name without prefix. 54 | * @return self|$this 55 | */ 56 | public function in(string $table): UpdateQuery 57 | { 58 | $this->table = $table; 59 | 60 | return $this; 61 | } 62 | 63 | /** 64 | * Change value set to be updated, must be represented by array of columns associated with new 65 | * value to be set. 66 | * 67 | * @param array $values 68 | * @return self|$this 69 | */ 70 | public function values(array $values): UpdateQuery 71 | { 72 | $this->values = $values; 73 | 74 | return $this; 75 | } 76 | 77 | /** 78 | * Set update value. 79 | * 80 | * @param string $column 81 | * @param mixed $value 82 | * @return self|$this 83 | */ 84 | public function set(string $column, $value): UpdateQuery 85 | { 86 | $this->values[$column] = $value; 87 | 88 | return $this; 89 | } 90 | 91 | /** 92 | * {@inheritdoc} 93 | * 94 | * Affect queries will return count of affected rows. 95 | * 96 | * @return int 97 | */ 98 | public function run(): int 99 | { 100 | $params = new QueryParameters(); 101 | $queryString = $this->sqlStatement($params); 102 | 103 | return $this->driver->execute($queryString, $params->getParameters()); 104 | } 105 | 106 | /** 107 | * @return int 108 | */ 109 | public function getType(): int 110 | { 111 | return CompilerInterface::UPDATE_QUERY; 112 | } 113 | 114 | /** 115 | * @return array 116 | */ 117 | public function getTokens(): array 118 | { 119 | return [ 120 | 'table' => $this->table, 121 | 'values' => $this->values, 122 | 'where' => $this->whereTokens 123 | ]; 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/Schema/AbstractForeignKey.php: -------------------------------------------------------------------------------- 1 | table = $table; 76 | $this->name = $name; 77 | $this->tablePrefix = $tablePrefix; 78 | } 79 | 80 | /** 81 | * {@inheritdoc} 82 | */ 83 | public function getColumns(): array 84 | { 85 | return $this->columns; 86 | } 87 | 88 | /** 89 | * {@inheritdoc} 90 | */ 91 | public function getForeignTable(): string 92 | { 93 | return $this->foreignTable; 94 | } 95 | 96 | /** 97 | * {@inheritdoc} 98 | */ 99 | public function getForeignKeys(): array 100 | { 101 | return $this->foreignKeys; 102 | } 103 | 104 | /** 105 | * {@inheritdoc} 106 | */ 107 | public function getDeleteRule(): string 108 | { 109 | return $this->deleteRule; 110 | } 111 | 112 | /** 113 | * {@inheritdoc} 114 | */ 115 | public function getUpdateRule(): string 116 | { 117 | return $this->updateRule; 118 | } 119 | 120 | /** 121 | * Set local column names foreign key relates to. Make sure column type is the same as foreign 122 | * column one. 123 | * 124 | * @param array $columns 125 | * @return self 126 | */ 127 | public function columns(array $columns): AbstractForeignKey 128 | { 129 | $this->columns = $columns; 130 | 131 | return $this; 132 | } 133 | 134 | /** 135 | * Set foreign table name and key local column must reference to. Make sure local and foreign 136 | * column types are identical. 137 | * 138 | * @param string $table Foreign table name with or without database prefix (see 3rd 139 | * argument). 140 | * @param array $columns Foreign key names (id by default). 141 | * @param bool $forcePrefix When true foreign table will get same prefix as table being 142 | * modified. 143 | * 144 | * @return self 145 | */ 146 | public function references( 147 | string $table, 148 | array $columns = ['id'], 149 | bool $forcePrefix = true 150 | ): AbstractForeignKey { 151 | $this->foreignTable = ($forcePrefix ? $this->tablePrefix : '') . $table; 152 | $this->foreignKeys = $columns; 153 | 154 | return $this; 155 | } 156 | 157 | /** 158 | * Set foreign key delete behaviour. 159 | * 160 | * @param string $rule Possible values: NO ACTION, CASCADE, etc (driver specific). 161 | * @return self 162 | */ 163 | public function onDelete(string $rule = self::NO_ACTION): AbstractForeignKey 164 | { 165 | $this->deleteRule = strtoupper($rule); 166 | 167 | return $this; 168 | } 169 | 170 | /** 171 | * Set foreign key update behaviour. 172 | * 173 | * @param string $rule Possible values: NO ACTION, CASCADE, etc (driver specific). 174 | * @return self 175 | */ 176 | public function onUpdate(string $rule = self::NO_ACTION): AbstractForeignKey 177 | { 178 | $this->updateRule = strtoupper($rule); 179 | 180 | return $this; 181 | } 182 | 183 | /** 184 | * Foreign key creation syntax. 185 | * 186 | * @param DriverInterface $driver 187 | * @return string 188 | */ 189 | public function sqlStatement(DriverInterface $driver): string 190 | { 191 | $statement = []; 192 | 193 | $statement[] = 'CONSTRAINT'; 194 | $statement[] = $driver->identifier($this->name); 195 | $statement[] = 'FOREIGN KEY'; 196 | $statement[] = '(' . $this->packColumns($driver, $this->columns) . ')'; 197 | 198 | $statement[] = 'REFERENCES ' . $driver->identifier($this->foreignTable); 199 | $statement[] = '(' . $this->packColumns($driver, $this->foreignKeys) . ')'; 200 | 201 | $statement[] = "ON DELETE {$this->deleteRule}"; 202 | $statement[] = "ON UPDATE {$this->updateRule}"; 203 | 204 | return implode(' ', $statement); 205 | } 206 | 207 | /** 208 | * @param AbstractForeignKey $initial 209 | * @return bool 210 | */ 211 | public function compare(AbstractForeignKey $initial): bool 212 | { 213 | // soft compare 214 | return $this == clone $initial; 215 | } 216 | 217 | /** 218 | * @param DriverInterface $driver 219 | * @param array $columns 220 | * @return string 221 | */ 222 | protected function packColumns(DriverInterface $driver, array $columns): string 223 | { 224 | return join(', ', array_map([$driver, 'identifier'], $columns)); 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /src/Schema/AbstractIndex.php: -------------------------------------------------------------------------------- 1 | table = $table; 61 | $this->name = $name; 62 | } 63 | 64 | /** 65 | * {@inheritdoc} 66 | */ 67 | public function isUnique(): bool 68 | { 69 | return $this->type === self::UNIQUE; 70 | } 71 | 72 | /** 73 | * {@inheritdoc} 74 | */ 75 | public function getColumns(): array 76 | { 77 | return $this->columns; 78 | } 79 | 80 | /** 81 | * {@inheritdoc} 82 | */ 83 | public function getSort(): array 84 | { 85 | return $this->sort; 86 | } 87 | 88 | /** 89 | * Will return columns list with their corresponding order expressions 90 | */ 91 | public function getColumnsWithSort(): array 92 | { 93 | return array_map(function ($column) { 94 | if ($order = $this->sort[$column] ?? null) { 95 | return "$column $order"; 96 | } 97 | 98 | return $column; 99 | }, $this->columns); 100 | } 101 | 102 | /** 103 | * Declare index type and behaviour to unique/non-unique state. 104 | * 105 | * @param bool $unique 106 | * @return self 107 | */ 108 | public function unique(bool $unique = true): AbstractIndex 109 | { 110 | $this->type = $unique ? self::UNIQUE : self::NORMAL; 111 | 112 | return $this; 113 | } 114 | 115 | /** 116 | * Change set of index forming columns. Method must support both array and string parameters. 117 | * 118 | * Example: 119 | * $index->columns('key'); 120 | * $index->columns('key', 'key2'); 121 | * $index->columns(['key', 'key2']); 122 | * 123 | * @param string|array $columns Columns array or comma separated list of parameters. 124 | * @return self 125 | */ 126 | public function columns($columns): AbstractIndex 127 | { 128 | if (!is_array($columns)) { 129 | $columns = func_get_args(); 130 | } 131 | 132 | $this->columns = $columns; 133 | 134 | return $this; 135 | } 136 | 137 | /** 138 | * Change a columns order mapping if needed. 139 | * 140 | * Example: 141 | * $index->sort(['key2' => 'DESC']); 142 | * 143 | * @param array $sort Associative array of columns to sort order. 144 | * @return self 145 | */ 146 | public function sort(array $sort): AbstractIndex 147 | { 148 | $this->sort = $sort; 149 | 150 | return $this; 151 | } 152 | 153 | /** 154 | * Index sql creation syntax. 155 | * 156 | * @param DriverInterface $driver 157 | * @param bool $includeTable Include table ON statement (not required for inline index creation). 158 | * @return string 159 | */ 160 | public function sqlStatement(DriverInterface $driver, bool $includeTable = true): string 161 | { 162 | $statement = [$this->isUnique() ? 'UNIQUE INDEX' : 'INDEX']; 163 | 164 | $statement[] = $driver->identifier($this->name); 165 | 166 | if ($includeTable) { 167 | $statement[] = "ON {$driver->identifier($this->table)}"; 168 | } 169 | 170 | //Wrapping column names 171 | $columns = []; 172 | foreach ($this->columns as $column) { 173 | $quoted = $driver->identifier($column); 174 | if ($order = $this->sort[$column] ?? null) { 175 | $quoted = "$quoted $order"; 176 | } 177 | 178 | $columns[] = $quoted; 179 | } 180 | $columns = implode(', ', $columns); 181 | 182 | $statement[] = "({$columns})"; 183 | 184 | return implode(' ', $statement); 185 | } 186 | 187 | /** 188 | * @param AbstractIndex $initial 189 | * @return bool 190 | */ 191 | public function compare(AbstractIndex $initial): bool 192 | { 193 | return $this == clone $initial; 194 | } 195 | 196 | 197 | /** 198 | * Parse column name and order from column expression 199 | * 200 | * @param mixed $column 201 | * 202 | * @return array 203 | */ 204 | public static function parseColumn($column) 205 | { 206 | if (is_array($column)) { 207 | return $column; 208 | } 209 | 210 | // Contains ASC 211 | if (substr($column, -4) === ' ASC') { 212 | return [ 213 | substr($column, 0, strlen($column) - 4), 214 | 'ASC' 215 | ]; 216 | } elseif (substr($column, -5) === ' DESC') { 217 | return [ 218 | substr($column, 0, strlen($column) - 5), 219 | 'DESC' 220 | ]; 221 | } 222 | 223 | return [ 224 | $column, 225 | null 226 | ]; 227 | } 228 | } 229 | -------------------------------------------------------------------------------- /src/Schema/Comparator.php: -------------------------------------------------------------------------------- 1 | initial = $initial; 32 | $this->current = $current; 33 | } 34 | 35 | /** 36 | * @return bool 37 | */ 38 | public function hasChanges(): bool 39 | { 40 | if ($this->isRenamed()) { 41 | return true; 42 | } 43 | 44 | if ($this->isPrimaryChanged()) { 45 | return true; 46 | } 47 | 48 | $difference = [ 49 | count($this->addedColumns()), 50 | count($this->droppedColumns()), 51 | count($this->alteredColumns()), 52 | count($this->addedIndexes()), 53 | count($this->droppedIndexes()), 54 | count($this->alteredIndexes()), 55 | count($this->addedForeignKeys()), 56 | count($this->droppedForeignKeys()), 57 | count($this->alteredForeignKeys()), 58 | ]; 59 | 60 | return array_sum($difference) !== 0; 61 | } 62 | 63 | /** 64 | * @return bool 65 | */ 66 | public function isRenamed(): bool 67 | { 68 | return $this->current->getName() !== $this->initial->getName(); 69 | } 70 | 71 | /** 72 | * @return bool 73 | */ 74 | public function isPrimaryChanged(): bool 75 | { 76 | return $this->current->getPrimaryKeys() !== $this->initial->getPrimaryKeys(); 77 | } 78 | 79 | /** 80 | * @return AbstractColumn[] 81 | */ 82 | public function addedColumns(): array 83 | { 84 | $difference = []; 85 | 86 | $initialColumns = $this->initial->getColumns(); 87 | foreach ($this->current->getColumns() as $name => $column) { 88 | if (!isset($initialColumns[$name])) { 89 | $difference[] = $column; 90 | } 91 | } 92 | 93 | return $difference; 94 | } 95 | 96 | /** 97 | * @return AbstractColumn[] 98 | */ 99 | public function droppedColumns(): array 100 | { 101 | $difference = []; 102 | 103 | $currentColumns = $this->current->getColumns(); 104 | foreach ($this->initial->getColumns() as $name => $column) { 105 | if (!isset($currentColumns[$name])) { 106 | $difference[] = $column; 107 | } 108 | } 109 | 110 | return $difference; 111 | } 112 | 113 | /** 114 | * Returns array where each value contain current and initial element state. 115 | * 116 | * @return array 117 | */ 118 | public function alteredColumns(): array 119 | { 120 | $difference = []; 121 | 122 | $initialColumns = $this->initial->getColumns(); 123 | foreach ($this->current->getColumns() as $name => $column) { 124 | if (!isset($initialColumns[$name])) { 125 | //Added into schema 126 | continue; 127 | } 128 | 129 | if (!$column->compare($initialColumns[$name])) { 130 | $difference[] = [$column, $initialColumns[$name]]; 131 | } 132 | } 133 | 134 | return $difference; 135 | } 136 | 137 | /** 138 | * @return AbstractIndex[] 139 | */ 140 | public function addedIndexes(): array 141 | { 142 | $difference = []; 143 | foreach ($this->current->getIndexes() as $name => $index) { 144 | if (!$this->initial->hasIndex($index->getColumnsWithSort())) { 145 | $difference[] = $index; 146 | } 147 | } 148 | 149 | return $difference; 150 | } 151 | 152 | /** 153 | * @return AbstractIndex[] 154 | */ 155 | public function droppedIndexes(): array 156 | { 157 | $difference = []; 158 | foreach ($this->initial->getIndexes() as $name => $index) { 159 | if (!$this->current->hasIndex($index->getColumnsWithSort())) { 160 | $difference[] = $index; 161 | } 162 | } 163 | 164 | return $difference; 165 | } 166 | 167 | /** 168 | * Returns array where each value contain current and initial element state. 169 | * 170 | * @return array 171 | */ 172 | public function alteredIndexes(): array 173 | { 174 | $difference = []; 175 | 176 | foreach ($this->current->getIndexes() as $name => $index) { 177 | if (!$this->initial->hasIndex($index->getColumnsWithSort())) { 178 | //Added into schema 179 | continue; 180 | } 181 | 182 | $initial = $this->initial->findIndex($index->getColumnsWithSort()); 183 | if (!$index->compare($initial)) { 184 | $difference[] = [$index, $initial]; 185 | } 186 | } 187 | 188 | return $difference; 189 | } 190 | 191 | /** 192 | * @return AbstractForeignKey[] 193 | */ 194 | public function addedForeignKeys(): array 195 | { 196 | $difference = []; 197 | foreach ($this->current->getForeignKeys() as $name => $foreignKey) { 198 | if (!$this->initial->hasForeignKey($foreignKey->getColumns())) { 199 | $difference[] = $foreignKey; 200 | } 201 | } 202 | 203 | return $difference; 204 | } 205 | 206 | /** 207 | * @return AbstractForeignKey[] 208 | */ 209 | public function droppedForeignKeys(): array 210 | { 211 | $difference = []; 212 | foreach ($this->initial->getForeignKeys() as $name => $foreignKey) { 213 | if (!$this->current->hasForeignKey($foreignKey->getColumns())) { 214 | $difference[] = $foreignKey; 215 | } 216 | } 217 | 218 | return $difference; 219 | } 220 | 221 | /** 222 | * Returns array where each value contain current and initial element state. 223 | * 224 | * @return array 225 | */ 226 | public function alteredForeignKeys(): array 227 | { 228 | $difference = []; 229 | 230 | foreach ($this->current->getForeignKeys() as $name => $foreignKey) { 231 | if (!$this->initial->hasForeignKey($foreignKey->getColumns())) { 232 | //Added into schema 233 | continue; 234 | } 235 | 236 | $initial = $this->initial->findForeignKey($foreignKey->getColumns()); 237 | if (!$foreignKey->compare($initial)) { 238 | $difference[] = [$foreignKey, $initial]; 239 | } 240 | } 241 | 242 | return $difference; 243 | } 244 | } 245 | -------------------------------------------------------------------------------- /src/Schema/ComparatorInterface.php: -------------------------------------------------------------------------------- 1 | tables[$table->getName()] = $table; 51 | $this->dependencies[$table->getName()] = $table->getDependencies(); 52 | 53 | $this->collectDrivers(); 54 | } 55 | 56 | /** 57 | * @return AbstractTable[] 58 | */ 59 | public function getTables(): array 60 | { 61 | return array_values($this->tables); 62 | } 63 | 64 | /** 65 | * Return sorted stack. 66 | * 67 | * @return array 68 | */ 69 | public function sortedTables(): array 70 | { 71 | $items = array_keys($this->tables); 72 | $this->states = $this->stack = []; 73 | 74 | foreach ($items as $item) { 75 | $this->sort($item, $this->dependencies[$item]); 76 | } 77 | 78 | return $this->stack; 79 | } 80 | 81 | /** 82 | * Synchronize tables. 83 | * 84 | * @throws Throwable 85 | */ 86 | public function run(): void 87 | { 88 | $hasChanges = false; 89 | foreach ($this->tables as $table) { 90 | if ( 91 | $table->getStatus() === AbstractTable::STATUS_DECLARED_DROPPED 92 | || $table->getComparator()->hasChanges() 93 | ) { 94 | $hasChanges = true; 95 | break; 96 | } 97 | } 98 | 99 | if (!$hasChanges) { 100 | //Nothing to do 101 | return; 102 | } 103 | 104 | $this->beginTransaction(); 105 | 106 | try { 107 | //Drop not-needed foreign keys and alter everything else 108 | $this->dropForeignKeys(); 109 | 110 | //Drop not-needed indexes 111 | $this->dropIndexes(); 112 | 113 | //Other changes [NEW TABLES WILL BE CREATED HERE!] 114 | foreach ($this->commitChanges() as $table) { 115 | $table->save(HandlerInterface::CREATE_FOREIGN_KEYS, true); 116 | } 117 | } catch (Throwable $e) { 118 | $this->rollbackTransaction(); 119 | throw $e; 120 | } 121 | 122 | $this->commitTransaction(); 123 | } 124 | 125 | /** 126 | * Drop all removed table references. 127 | */ 128 | protected function dropForeignKeys(): void 129 | { 130 | foreach ($this->sortedTables() as $table) { 131 | if ($table->exists()) { 132 | $table->save(HandlerInterface::DROP_FOREIGN_KEYS, false); 133 | } 134 | } 135 | } 136 | 137 | /** 138 | * Drop all removed table indexes. 139 | */ 140 | protected function dropIndexes(): void 141 | { 142 | foreach ($this->sortedTables() as $table) { 143 | if ($table->exists()) { 144 | $table->save(HandlerInterface::DROP_INDEXES, false); 145 | } 146 | } 147 | } 148 | 149 | /*** 150 | * @return AbstractTable[] Created or updated tables. 151 | */ 152 | protected function commitChanges(): array 153 | { 154 | $updated = []; 155 | foreach ($this->sortedTables() as $table) { 156 | if ($table->getStatus() === AbstractTable::STATUS_DECLARED_DROPPED) { 157 | $table->save(HandlerInterface::DO_DROP); 158 | continue; 159 | } 160 | 161 | $updated[] = $table; 162 | $table->save( 163 | HandlerInterface::DO_ALL 164 | ^ HandlerInterface::DROP_FOREIGN_KEYS 165 | ^ HandlerInterface::DROP_INDEXES 166 | ^ HandlerInterface::CREATE_FOREIGN_KEYS 167 | ); 168 | } 169 | 170 | return $updated; 171 | } 172 | 173 | /** 174 | * Begin mass transaction. 175 | */ 176 | protected function beginTransaction(): void 177 | { 178 | foreach ($this->drivers as $driver) { 179 | if ($driver instanceof Driver) { 180 | // do not cache statements for this transaction 181 | $driver->beginTransaction(null, false); 182 | } else { 183 | /** @var DriverInterface $driver */ 184 | $driver->beginTransaction(null); 185 | } 186 | } 187 | } 188 | 189 | /** 190 | * Commit mass transaction. 191 | */ 192 | protected function commitTransaction(): void 193 | { 194 | foreach ($this->drivers as $driver) { 195 | /** @var DriverInterface $driver */ 196 | $driver->commitTransaction(); 197 | } 198 | } 199 | 200 | /** 201 | * Roll back mass transaction. 202 | */ 203 | protected function rollbackTransaction(): void 204 | { 205 | foreach (array_reverse($this->drivers) as $driver) { 206 | /** @var DriverInterface $driver */ 207 | $driver->rollbackTransaction(); 208 | } 209 | } 210 | 211 | /** 212 | * Collecting all involved drivers. 213 | */ 214 | private function collectDrivers(): void 215 | { 216 | foreach ($this->tables as $table) { 217 | if (!in_array($table->getDriver(), $this->drivers, true)) { 218 | $this->drivers[] = $table->getDriver(); 219 | } 220 | } 221 | } 222 | 223 | /** 224 | * @param string $key 225 | * @param array $dependencies 226 | */ 227 | private function sort(string $key, array $dependencies): void 228 | { 229 | if (isset($this->states[$key])) { 230 | return; 231 | } 232 | 233 | $this->states[$key] = self::STATE_NEW; 234 | foreach ($dependencies as $dependency) { 235 | if (isset($this->dependencies[$dependency])) { 236 | $this->sort($dependency, $this->dependencies[$dependency]); 237 | } 238 | } 239 | 240 | $this->stack[] = $this->tables[$key]; 241 | $this->states[$key] = self::STATE_PASSED; 242 | } 243 | } 244 | -------------------------------------------------------------------------------- /src/Schema/Traits/ElementTrait.php: -------------------------------------------------------------------------------- 1 | table; 32 | } 33 | 34 | /** 35 | * Set element name. 36 | * 37 | * @param string $name 38 | * @return self|$this 39 | */ 40 | public function setName(string $name): self 41 | { 42 | $this->name = $name; 43 | 44 | return $this; 45 | } 46 | 47 | /** 48 | * Get element name (unquoted). 49 | * 50 | * @return string 51 | */ 52 | public function getName(): string 53 | { 54 | return $this->name; 55 | } 56 | 57 | /** 58 | * Element creation/definition syntax (specific to parent driver). 59 | * 60 | * @param Driver $driver 61 | * @return string 62 | */ 63 | abstract public function sqlStatement(Driver $driver): string; 64 | } 65 | -------------------------------------------------------------------------------- /src/StatementInterface.php: -------------------------------------------------------------------------------- 1 |