├── .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 |