├── .gitattributes
├── .github
├── FUNDING.yml
├── ISSUE_TEMPLATE
│ └── bug_report.md
└── workflows
│ └── php.yml
├── .gitignore
├── .php-cs-fixer.dist.php
├── LICENSE.md
├── README.md
├── composer.json
├── phpunit.xml
├── src
└── Medoo.php
└── tests
├── AggregateTest.php
├── CreateTest.php
├── DeleteTest.php
├── DropTest.php
├── GetTest.php
├── HasTest.php
├── InsertTest.php
├── MedooTestCase.php
├── QueryTest.php
├── QuoteTest.php
├── RandTest.php
├── RawTest.php
├── ReplaceTest.php
├── SelectTest.php
├── UpdateTest.php
└── WhereTest.php
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Auto detect text files and perform LF normalization
2 | * text=auto
3 |
4 | # Custom for Visual Studio
5 | *.cs diff=csharp
6 | *.sln merge=union
7 | *.csproj merge=union
8 | *.vbproj merge=union
9 | *.fsproj merge=union
10 | *.dbproj merge=union
11 |
12 | # Standard to msysgit
13 | *.doc diff=astextplain
14 | *.DOC diff=astextplain
15 | *.docx diff=astextplain
16 | *.DOCX diff=astextplain
17 | *.dot diff=astextplain
18 | *.DOT diff=astextplain
19 | *.pdf diff=astextplain
20 | *.PDF diff=astextplain
21 | *.rtf diff=astextplain
22 | *.RTF diff=astextplain
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | open_collective: medoo
4 | custom: ["https://paypal.me/AngelaonLai"]
5 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | ---
5 |
6 | **Information**
7 | - **Version of Medoo:** [Enter the version number]
8 | - **Version of PHP:** [Enter the version number]
9 | - **Type of Database:** [MySQL, MSSQL, SQLite, etc.]
10 | - **System:** [Linux|Windows|Mac]
11 |
12 | **Describe the Problem**
13 | A clear and concise description of what the problem is.
14 |
15 | **Code Snippet**
16 | The detail code you are using that causes the problem:
17 | ```php
18 | // Write your PHP code here
19 |
20 | ```
21 |
22 | **Expected Behavior**
23 | A clear and concise description of what you expected to happen.
24 |
25 | **Actual Behavior**
26 | A clear and concise description of what actually happened.
27 |
--------------------------------------------------------------------------------
/.github/workflows/php.yml:
--------------------------------------------------------------------------------
1 | name: build
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 | pull_request:
8 | branches:
9 | - master
10 |
11 | jobs:
12 | build:
13 |
14 | runs-on: ubuntu-latest
15 |
16 | strategy:
17 | matrix:
18 | php: ['7.3', '7.4', '8.2', '8.3']
19 |
20 | steps:
21 | - uses: actions/checkout@v3.3.0
22 |
23 | - name: Validate composer.json and composer.lock
24 | run: composer validate --strict
25 |
26 | - name: Cache dependencies
27 | uses: actions/cache@v3
28 | with:
29 | path: vendor
30 | key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
31 | restore-keys: |
32 | ${{ runner.os }}-composer-
33 |
34 | - name: Install dependencies
35 | run: composer install --prefer-dist --no-progress
36 |
37 | - name: Run test suite
38 | run: composer run-script test
39 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | #################
2 | ## Eclipse
3 | #################
4 |
5 | *.pydevproject
6 | .project
7 | .metadata
8 | bin/
9 | tmp/
10 | *.tmp
11 | *.bak
12 | *.swp
13 | *~.nib
14 | local.properties
15 | .classpath
16 | .settings/
17 | .loadpath
18 |
19 | # External tool builders
20 | .externalToolBuilders/
21 |
22 | # Locally stored "Eclipse launch configurations"
23 | *.launch
24 |
25 | # CDT-specific
26 | .cproject
27 |
28 | # PDT-specific
29 | .buildpath
30 |
31 |
32 | #################
33 | ## Visual Studio
34 | #################
35 |
36 | ## Ignore Visual Studio temporary files, build results, and
37 | ## files generated by popular Visual Studio add-ons.
38 |
39 | # User-specific files
40 | *.suo
41 | *.user
42 | *.sln.docstates
43 |
44 | # Build results
45 |
46 | [Dd]ebug/
47 | [Rr]elease/
48 | x64/
49 | build/
50 | [Bb]in/
51 | [Oo]bj/
52 |
53 | # MSTest test Results
54 | [Tt]est[Rr]esult*/
55 | [Bb]uild[Ll]og.*
56 |
57 | *_i.c
58 | *_p.c
59 | *.ilk
60 | *.meta
61 | *.obj
62 | *.pch
63 | *.pdb
64 | *.pgc
65 | *.pgd
66 | *.rsp
67 | *.sbr
68 | *.tlb
69 | *.tli
70 | *.tlh
71 | *.tmp
72 | *.tmp_proj
73 | *.log
74 | *.vspscc
75 | *.vssscc
76 | .builds
77 | *.pidb
78 | *.log
79 | *.scc
80 |
81 | # Visual C++ cache files
82 | ipch/
83 | *.aps
84 | *.ncb
85 | *.opensdf
86 | *.sdf
87 | *.cachefile
88 |
89 | # Visual Studio profiler
90 | *.psess
91 | *.vsp
92 | *.vspx
93 |
94 | # Guidance Automation Toolkit
95 | *.gpState
96 |
97 | # ReSharper is a .NET coding add-in
98 | _ReSharper*/
99 | *.[Rr]e[Ss]harper
100 |
101 | # TeamCity is a build add-in
102 | _TeamCity*
103 |
104 | # DotCover is a Code Coverage Tool
105 | *.dotCover
106 |
107 | # NCrunch
108 | *.ncrunch*
109 | .*crunch*.local.xml
110 |
111 | # Installshield output folder
112 | [Ee]xpress/
113 |
114 | # DocProject is a documentation generator add-in
115 | DocProject/buildhelp/
116 | DocProject/Help/*.HxT
117 | DocProject/Help/*.HxC
118 | DocProject/Help/*.hhc
119 | DocProject/Help/*.hhk
120 | DocProject/Help/*.hhp
121 | DocProject/Help/Html2
122 | DocProject/Help/html
123 |
124 | # Click-Once directory
125 | publish/
126 |
127 | # Publish Web Output
128 | *.Publish.xml
129 | *.pubxml
130 | *.publishproj
131 |
132 | # NuGet Packages Directory
133 | ## TODO: If you have NuGet Package Restore enabled, uncomment the next line
134 | #packages/
135 |
136 | # Windows Azure Build Output
137 | csx
138 | *.build.csdef
139 |
140 | # Windows Store app package directory
141 | AppPackages/
142 |
143 | # Others
144 | sql/
145 | *.Cache
146 | ClientBin/
147 | [Ss]tyle[Cc]op.*
148 | ~$*
149 | *~
150 | *.dbmdl
151 | *.[Pp]ublish.xml
152 | *.pfx
153 | *.publishsettings
154 |
155 | # RIA/Silverlight projects
156 | Generated_Code/
157 |
158 | # Backup & report files from converting an old project file to a newer
159 | # Visual Studio version. Backup files are not needed, because we have git ;-)
160 | _UpgradeReport_Files/
161 | Backup*/
162 | UpgradeLog*.XML
163 | UpgradeLog*.htm
164 |
165 | # SQL Server files
166 | App_Data/*.mdf
167 | App_Data/*.ldf
168 |
169 | #############
170 | ## Windows detritus
171 | #############
172 |
173 | # Windows image file caches
174 | Thumbs.db
175 | ehthumbs.db
176 |
177 | # Folder config file
178 | Desktop.ini
179 |
180 | # Recycle Bin used on file shares
181 | $RECYCLE.BIN/
182 |
183 | # Mac crap
184 | .DS_Store
185 |
186 |
187 | #############
188 | ## Python
189 | #############
190 |
191 | *.py[cod]
192 |
193 | # Packages
194 | *.egg
195 | *.egg-info
196 | dist/
197 | build/
198 | eggs/
199 | parts/
200 | var/
201 | sdist/
202 | develop-eggs/
203 | .installed.cfg
204 |
205 | # Installer logs
206 | pip-log.txt
207 |
208 | # Unit test / coverage reports
209 | .coverage
210 | .tox
211 |
212 | #Translations
213 | *.mo
214 |
215 | #Mr Developer
216 | .mr.developer.cfg
217 |
218 | #php-cs-fixer
219 | /.php_cs
220 |
221 | #PHPUnit
222 | /.phpunit.cache
223 |
224 | /composer.lock
225 | /vendor
--------------------------------------------------------------------------------
/.php-cs-fixer.dist.php:
--------------------------------------------------------------------------------
1 | setRules([
6 | '@PSR2' => true,
7 | '@PSR12' => true,
8 | 'array_syntax' => ['syntax' => 'short'],
9 | 'nullable_type_declaration_for_default_null_value' => true
10 | ])
11 | ->setFinder(
12 | PhpCsFixer\Finder::create()
13 | ->in(__DIR__ . '/src')
14 | ->in(__DIR__ . '/tests')
15 | )
16 | ;
17 |
18 | return $config;
19 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2025 Angel Lai
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 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | > The lightweight PHP database framework to accelerate development.
15 |
16 | ## Features
17 |
18 | * **Lightweight** - Single-file framework with minimal dependencies.
19 |
20 | * **Easy** - Simple and intuitive API for quick integration.
21 |
22 | * **Powerful** - Supports complex SQL queries, data mapping, and SQL injection prevention.
23 |
24 | * **Compatible** - Works with MySQL, MariaDB, PostgreSQL, SQLite, MSSQL, Oracle, Sybase, and more.
25 |
26 | * **Friendly** - Integrates seamlessly with Laravel, CodeIgniter, Yii, Slim, and other PHP frameworks.
27 |
28 | * **Free** - Licensed under MIT, free to use for any purpose.
29 |
30 | ## Requirements
31 |
32 | - PHP 7.3 or later
33 | - PDO extension enabled
34 |
35 | ## Get Started
36 |
37 | ### Install via composer
38 |
39 | Add Medoo to the `composer.json` configuration file.
40 | ```bash
41 | $ composer require catfan/medoo
42 | ```
43 |
44 | Then update Composer
45 | ```bash
46 | $ composer update
47 | ```
48 |
49 | ```php
50 | // Require Composer's autoloader
51 | require 'vendor/autoload.php';
52 |
53 | // Import Medoo namespace
54 | use Medoo\Medoo;
55 |
56 | // Initialize database connection
57 | $database = new Medoo([
58 | 'type' => 'mysql',
59 | 'host' => 'localhost',
60 | 'database' => 'name',
61 | 'username' => 'your_username',
62 | 'password' => 'your_password'
63 | ]);
64 |
65 | // Insert data
66 | $database->insert('account', [
67 | 'user_name' => 'foo',
68 | 'email' => 'foo@bar.com'
69 | ]);
70 |
71 | // Retrieve data
72 | $data = $database->select('account', [
73 | 'user_name',
74 | 'email'
75 | ], [
76 | 'user_id' => 50
77 | ]);
78 |
79 | echo json_encode($data);
80 |
81 | // [{
82 | // "user_name" : "foo",
83 | // "email" : "foo@bar.com",
84 | // }]
85 | ```
86 |
87 | ## Contribution Guidelines
88 | Before submitting a pull request, ensure compatibility with multiple database engines and include unit tests when possible.
89 |
90 | ### Testing & Code Style
91 | - Run `phpunit tests` to execute unit tests.
92 | - Use `php-cs-fixer fix` to enforce code style consistency.
93 |
94 | ### Commit Message Format
95 | Each commit should begin with a tag indicating the type of change:
96 |
97 | - `[fix]` for bug fixes
98 | - `[feature]` for new features
99 | - `[update]` for improvements
100 |
101 | Keep contributions simple and well-documented.
102 |
103 | ## License
104 |
105 | Medoo is released under the MIT License.
106 |
107 | ## Links
108 |
109 | * Official website: [https://medoo.in](https://medoo.in)
110 |
111 | * Documentation: [https://medoo.in/doc](https://medoo.in/doc)
112 |
113 | * Twitter: [https://twitter.com/MedooPHP](https://twitter.com/MedooPHP)
114 |
115 | * Open Collective: [https://opencollective.com/medoo](https://opencollective.com/medoo)
116 |
117 | ## Support Our Other Product
118 | [Gear Browser - Web Browser for Geek](https://gear4.app)
119 |
120 | [](https://gear4.app)
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "catfan/medoo",
3 | "type": "framework",
4 | "description": "The lightweight PHP database framework to accelerate development",
5 | "keywords": ["database", "database library", "lightweight", "PHP framework", "SQL", "MySQL", "MSSQL", "SQLite", "PostgreSQL", "MariaDB", "Oracle"],
6 | "homepage": "https://medoo.in",
7 | "license": "MIT",
8 | "support": {
9 | "issues": "https://github.com/catfan/Medoo/issues",
10 | "source": "https://github.com/catfan/Medoo"
11 | },
12 | "authors": [
13 | {"name": "Angel Lai", "email": "angel@medoo.in"}
14 | ],
15 | "require": {
16 | "php": ">=7.3",
17 | "ext-pdo": "*"
18 | },
19 | "require-dev": {
20 | "phpunit/phpunit": "^9.0"
21 | },
22 | "suggest": {
23 | "ext-pdo_mysql": "For MySQL or MariaDB database",
24 | "ext-pdo_sqlsrv": "For MSSQL database on both Window/Liunx platform",
25 | "ext-pdo_dblib": "For MSSQL or Sybase database on Linux/UNIX platform",
26 | "ext-pdo_oci": "For Oracle database",
27 | "ext-pdo_pqsql": "For PostgreSQL database",
28 | "ext-pdo_sqlite": "For SQLite database"
29 | },
30 | "autoload": {
31 | "psr-4": {
32 | "Medoo\\": "src/"
33 | }
34 | },
35 | "autoload-dev": {
36 | "psr-4": {
37 | "Medoo\\Tests\\": "tests/"
38 | }
39 | },
40 | "scripts": {
41 | "test": "vendor/bin/phpunit tests"
42 | }
43 | }
--------------------------------------------------------------------------------
/phpunit.xml:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
12 | ./tests
13 |
14 |
15 |
16 |
18 |
19 | src
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/src/Medoo.php:
--------------------------------------------------------------------------------
1 | 'mysql',
229 | * 'database' => 'name',
230 | * 'host' => 'localhost',
231 | * 'username' => 'your_username',
232 | * 'password' => 'your_password',
233 | *
234 | * // [optional]
235 | * 'charset' => 'utf8mb4',
236 | * 'port' => 3306,
237 | * 'prefix' => 'PREFIX_'
238 | * ]);
239 | * ```
240 | *
241 | * @param array $options Connection options
242 | * @return Medoo
243 | * @throws PDOException If the connection fails
244 | * @link https://medoo.in/api/new
245 | * @codeCoverageIgnore
246 | */
247 |
248 | public function __construct(array $options)
249 | {
250 | if (isset($options['prefix'])) {
251 | $this->prefix = $options['prefix'];
252 | }
253 |
254 | if (isset($options['testMode']) && $options['testMode'] == true) {
255 | $this->testMode = true;
256 | return;
257 | }
258 |
259 | $options['type'] = $options['type'] ?? $options['database_type'] ?? null;
260 |
261 | if (!$options['type']) {
262 | throw new InvalidArgumentException('Database type is required.');
263 | }
264 |
265 | if (!isset($options['pdo'])) {
266 | $options['database'] = $options['database'] ?? $options['database_name'];
267 |
268 | if (!isset($options['socket'])) {
269 | $options['host'] = $options['host'] ?? $options['server'] ?? false;
270 | }
271 | }
272 |
273 | $this->setupType($options['type']);
274 |
275 | if (isset($options['logging']) && is_bool($options['logging'])) {
276 | $this->logging = $options['logging'];
277 | }
278 |
279 | $commands = [];
280 |
281 | switch ($this->type) {
282 |
283 | case 'mysql':
284 | // Make MySQL using standard quoted identifier.
285 | $commands[] = 'SET SQL_MODE=ANSI_QUOTES';
286 |
287 | break;
288 |
289 | case 'mssql':
290 | // Keep MSSQL QUOTED_IDENTIFIER is ON for standard quoting.
291 | $commands[] = 'SET QUOTED_IDENTIFIER ON';
292 |
293 | // Make ANSI_NULLS is ON for NULL value.
294 | $commands[] = 'SET ANSI_NULLS ON';
295 |
296 | break;
297 | }
298 |
299 | if (isset($options['pdo'])) {
300 | if (!$options['pdo'] instanceof PDO) {
301 | throw new InvalidArgumentException('Invalid PDO object supplied.');
302 | }
303 |
304 | $this->pdo = $options['pdo'];
305 |
306 | foreach ($commands as $value) {
307 | $this->pdo->exec($value);
308 | }
309 |
310 | return;
311 | }
312 |
313 | if (isset($options['dsn'])) {
314 | if (is_array($options['dsn']) && isset($options['dsn']['driver'])) {
315 | $attr = $options['dsn'];
316 | } else {
317 | throw new InvalidArgumentException('Invalid DSN option supplied.');
318 | }
319 | } else {
320 | if (isset($options['port']) && is_numeric($options['port'])) {
321 | $port = $options['port'];
322 | }
323 |
324 | $isPort = isset($port);
325 |
326 | switch ($this->type) {
327 |
328 | case 'mysql':
329 | $attr = [
330 | 'driver' => 'mysql',
331 | 'dbname' => $options['database']
332 | ];
333 |
334 | if (isset($options['socket'])) {
335 | $attr['unix_socket'] = $options['socket'];
336 | } else {
337 | $attr['host'] = $options['host'];
338 |
339 | if ($isPort) {
340 | $attr['port'] = $port;
341 | }
342 | }
343 |
344 | break;
345 |
346 | case 'pgsql':
347 | $attr = [
348 | 'driver' => 'pgsql',
349 | 'host' => $options['host'],
350 | 'dbname' => $options['database']
351 | ];
352 |
353 | if ($isPort) {
354 | $attr['port'] = $port;
355 | }
356 |
357 | break;
358 |
359 | case 'sybase':
360 | $attr = [
361 | 'driver' => 'dblib',
362 | 'host' => $options['host'],
363 | 'dbname' => $options['database']
364 | ];
365 |
366 | if ($isPort) {
367 | $attr['port'] = $port;
368 | }
369 |
370 | break;
371 |
372 | case 'oracle':
373 | $attr = [
374 | 'driver' => 'oci',
375 | 'dbname' => $options['host'] ?
376 | '//' . $options['host'] . ($isPort ? ':' . $port : ':1521') . '/' . $options['database'] :
377 | $options['database']
378 | ];
379 |
380 | if (isset($options['charset'])) {
381 | $attr['charset'] = $options['charset'];
382 | }
383 |
384 | break;
385 |
386 | case 'mssql':
387 | if (isset($options['driver']) && $options['driver'] === 'dblib') {
388 | $attr = [
389 | 'driver' => 'dblib',
390 | 'host' => $options['host'] . ($isPort ? ':' . $port : ''),
391 | 'dbname' => $options['database']
392 | ];
393 |
394 | if (isset($options['appname'])) {
395 | $attr['appname'] = $options['appname'];
396 | }
397 |
398 | if (isset($options['charset'])) {
399 | $attr['charset'] = $options['charset'];
400 | }
401 | } else {
402 | $attr = [
403 | 'driver' => 'sqlsrv',
404 | 'Server' => $options['host'] . ($isPort ? ',' . $port : ''),
405 | 'Database' => $options['database']
406 | ];
407 |
408 | if (isset($options['appname'])) {
409 | $attr['APP'] = $options['appname'];
410 | }
411 |
412 | $config = [
413 | 'ApplicationIntent',
414 | 'AttachDBFileName',
415 | 'Authentication',
416 | 'ColumnEncryption',
417 | 'ConnectionPooling',
418 | 'Encrypt',
419 | 'Failover_Partner',
420 | 'KeyStoreAuthentication',
421 | 'KeyStorePrincipalId',
422 | 'KeyStoreSecret',
423 | 'LoginTimeout',
424 | 'MultipleActiveResultSets',
425 | 'MultiSubnetFailover',
426 | 'Scrollable',
427 | 'TraceFile',
428 | 'TraceOn',
429 | 'TransactionIsolation',
430 | 'TransparentNetworkIPResolution',
431 | 'TrustServerCertificate',
432 | 'WSID',
433 | ];
434 |
435 | foreach ($config as $value) {
436 | $keyname = strtolower(preg_replace(['/([a-z\d])([A-Z])/', '/([^_])([A-Z][a-z])/'], '$1_$2', $value));
437 |
438 | if (isset($options[$keyname])) {
439 | $attr[$value] = $options[$keyname];
440 | }
441 | }
442 | }
443 |
444 | break;
445 |
446 | case 'sqlite':
447 | $attr = [
448 | 'driver' => 'sqlite',
449 | $options['database']
450 | ];
451 |
452 | break;
453 | }
454 | }
455 |
456 | if (!isset($attr)) {
457 | throw new InvalidArgumentException('Incorrect connection options.');
458 | }
459 |
460 | $driver = $attr['driver'];
461 |
462 | if (!in_array($driver, PDO::getAvailableDrivers())) {
463 | throw new InvalidArgumentException("Unsupported PDO driver: {$driver}.");
464 | }
465 |
466 | unset($attr['driver']);
467 |
468 | $stack = [];
469 |
470 | foreach ($attr as $key => $value) {
471 | $stack[] = is_int($key) ? $value : $key . '=' . $value;
472 | }
473 |
474 | $dsn = $driver . ':' . implode(';', $stack);
475 |
476 | if (
477 | in_array($this->type, ['mysql', 'pgsql', 'sybase', 'mssql']) &&
478 | isset($options['charset'])
479 | ) {
480 | $commands[] = "SET NAMES '{$options['charset']}'" . (
481 | $this->type === 'mysql' && isset($options['collation']) ?
482 | " COLLATE '{$options['collation']}'" : ''
483 | );
484 | }
485 |
486 | $this->dsn = $dsn;
487 |
488 | try {
489 | $this->pdo = new PDO(
490 | $dsn,
491 | $options['username'] ?? null,
492 | $options['password'] ?? null,
493 | $options['option'] ?? []
494 | );
495 |
496 | if (isset($options['error'])) {
497 | $this->pdo->setAttribute(
498 | PDO::ATTR_ERRMODE,
499 | in_array($options['error'], [
500 | PDO::ERRMODE_SILENT,
501 | PDO::ERRMODE_WARNING,
502 | PDO::ERRMODE_EXCEPTION
503 | ]) ?
504 | $options['error'] :
505 | PDO::ERRMODE_SILENT
506 | );
507 | }
508 |
509 | if (isset($options['command']) && is_array($options['command'])) {
510 | $commands = array_merge($commands, $options['command']);
511 | }
512 |
513 | foreach ($commands as $value) {
514 | $this->pdo->exec($value);
515 | }
516 | } catch (PDOException $e) {
517 | throw new PDOException($e->getMessage());
518 | }
519 | }
520 |
521 | /**
522 | * Setup the database type.
523 | *
524 | * @param string The database type string.
525 | * @return void
526 | */
527 | public function setupType(string $type)
528 | {
529 | $databaseType = strtolower($type);
530 |
531 | if ($databaseType === 'mariadb') {
532 | $databaseType = 'mysql';
533 | }
534 |
535 | if ($databaseType === 'oracle') {
536 | $this->tableAliasConnector = ' ';
537 | } elseif ($databaseType === 'mysql') {
538 | $this->quotePattern = '`$1`';
539 | } elseif ($databaseType === 'mssql') {
540 | $this->quotePattern = '[$1]';
541 | }
542 |
543 | $this->type = $databaseType;
544 | }
545 |
546 | /**
547 | * Generate a new map key for the placeholder.
548 | *
549 | * @return string
550 | */
551 | protected function mapKey(): string
552 | {
553 | return ':MeD' . $this->guid++ . '_mK';
554 | }
555 |
556 | /**
557 | * Execute customized raw statement.
558 | *
559 | * @param string $statement The raw SQL statement.
560 | * @param array $map The array of input parameters value for prepared statement.
561 | * @return \PDOStatement|null
562 | */
563 | public function query(string $statement, array $map = []): ?PDOStatement
564 | {
565 | $raw = $this->raw($statement, $map);
566 | $statement = $this->buildRaw($raw, $map);
567 |
568 | return $this->exec($statement, $map);
569 | }
570 |
571 | /**
572 | * Execute the raw statement.
573 | *
574 | * @param string $statement The SQL statement.
575 | * @param array $map The array of input parameters value for prepared statement.
576 | * @codeCoverageIgnore
577 | * @return \PDOStatement|null
578 | */
579 | public function exec(string $statement, array $map = [], ?callable $callback = null): ?PDOStatement
580 | {
581 | $this->statement = null;
582 | $this->errorInfo = null;
583 | $this->error = null;
584 |
585 | if ($this->testMode) {
586 | $this->queryString = $this->generate($statement, $map);
587 | return null;
588 | }
589 |
590 | if ($this->debugMode) {
591 | if ($this->debugLogging) {
592 | $this->debugLogs[] = $this->generate($statement, $map);
593 | return null;
594 | }
595 |
596 | echo $this->generate($statement, $map);
597 |
598 | $this->debugMode = false;
599 |
600 | return null;
601 | }
602 |
603 | if ($this->logging) {
604 | $this->logs[] = [$statement, $map];
605 | } else {
606 | $this->logs = [[$statement, $map]];
607 | }
608 |
609 | $statement = $this->pdo->prepare($statement);
610 | $errorInfo = $this->pdo->errorInfo();
611 |
612 | if ($errorInfo[0] !== '00000') {
613 | $this->errorInfo = $errorInfo;
614 | $this->error = $errorInfo[2];
615 |
616 | return null;
617 | }
618 |
619 | foreach ($map as $key => $value) {
620 | $statement->bindValue($key, $value[0], $value[1]);
621 | }
622 |
623 | if (is_callable($callback)) {
624 | $this->pdo->beginTransaction();
625 | $callback($statement);
626 | $execute = $statement->execute();
627 | $this->pdo->commit();
628 | } else {
629 | $execute = $statement->execute();
630 | }
631 |
632 | $errorInfo = $statement->errorInfo();
633 |
634 | if ($errorInfo[0] !== '00000') {
635 | $this->errorInfo = $errorInfo;
636 | $this->error = $errorInfo[2];
637 |
638 | return null;
639 | }
640 |
641 | if ($execute) {
642 | $this->statement = $statement;
643 | }
644 |
645 | return $statement;
646 | }
647 |
648 | /**
649 | * Generate readable statement.
650 | *
651 | * @param string $statement
652 | * @param array $map
653 | * @codeCoverageIgnore
654 | * @return string
655 | */
656 | protected function generate(string $statement, array $map): string
657 | {
658 | $statement = preg_replace(
659 | '/(?!\'[^\s]+\s?)"(' . $this::COLUMN_PATTERN . ')"(?!\s?[^\s]+\')/u',
660 | $this->quotePattern,
661 | $statement
662 | );
663 |
664 | foreach ($map as $key => $value) {
665 | if ($value[1] === PDO::PARAM_STR) {
666 | $replace = $this->quote("{$value[0]}");
667 | } elseif ($value[1] === PDO::PARAM_NULL) {
668 | $replace = 'NULL';
669 | } elseif ($value[1] === PDO::PARAM_LOB) {
670 | $replace = '{LOB_DATA}';
671 | } else {
672 | $replace = $value[0] . '';
673 | }
674 |
675 | $statement = str_replace($key, $replace, $statement);
676 | }
677 |
678 | return $statement;
679 | }
680 |
681 | /**
682 | * Build a raw object.
683 | *
684 | * @param string $string The raw string.
685 | * @param array $map The array of mapping data for the raw string.
686 | * @return Medoo::raw
687 | */
688 | public static function raw(string $string, array $map = []): Raw
689 | {
690 | $raw = new Raw();
691 |
692 | $raw->map = $map;
693 | $raw->value = $string;
694 |
695 | return $raw;
696 | }
697 |
698 | /**
699 | * Finds whether the object is raw.
700 | *
701 | * @param object $object
702 | * @return bool
703 | */
704 | protected function isRaw($object): bool
705 | {
706 | return $object instanceof Raw;
707 | }
708 |
709 | /**
710 | * Generate the actual query from the raw object.
711 | *
712 | * @param mixed $raw
713 | * @param array $map
714 | * @return string|null
715 | */
716 | protected function buildRaw($raw, array &$map): ?string
717 | {
718 | if (!$this->isRaw($raw)) {
719 | return null;
720 | }
721 |
722 | $query = preg_replace_callback(
723 | '/(([`\'])[\<]*?)?((FROM|TABLE|TABLES LIKE|INTO|UPDATE|JOIN|TABLE IF EXISTS)\s*)?\<((' . $this::TABLE_PATTERN . ')(\.' . $this::COLUMN_PATTERN . ')?)\>([^,]*?\2)?/',
724 | function ($matches) {
725 | if (!empty($matches[2]) && isset($matches[8])) {
726 | return $matches[0];
727 | }
728 |
729 | if (!empty($matches[4])) {
730 | return $matches[1] . $matches[4] . ' ' . $this->tableQuote($matches[5]);
731 | }
732 |
733 | return $matches[1] . $this->columnQuote($matches[5]);
734 | },
735 | $raw->value
736 | );
737 |
738 | $rawMap = $raw->map;
739 |
740 | if (!empty($rawMap)) {
741 | foreach ($rawMap as $key => $value) {
742 | $map[$key] = $this->typeMap($value, gettype($value));
743 | }
744 | }
745 |
746 | return $query;
747 | }
748 |
749 | /**
750 | * Escape and quote a string for use in an SQL query.
751 | *
752 | * @param string $string The string to be quoted.
753 | * @return string
754 | */
755 | public function quote(string $string): string
756 | {
757 | if ($this->type === 'mysql') {
758 | return "'" . preg_replace(['/([\'"])/', '/(\\\\\\\")/'], ["\\\\\${1}", '\\\${1}'], $string) . "'";
759 | }
760 |
761 | return "'" . preg_replace('/\'/', '\'\'', $string) . "'";
762 | }
763 |
764 | /**
765 | * Quote a table name for use in an SQL query.
766 | *
767 | * @param string $table The table name to be quoted.
768 | * @return string
769 | * @throws InvalidArgumentException If the table name is invalid.
770 | */
771 | public function tableQuote(string $table): string
772 | {
773 | if (preg_match("/^" . $this::TABLE_PATTERN . "$/u", $table)) {
774 | return '"' . $this->prefix . $table . '"';
775 | }
776 |
777 | throw new InvalidArgumentException("Incorrect table name: {$table}.");
778 | }
779 |
780 | /**
781 | * Quote a column name for use in an SQL query.
782 | *
783 | * @param string $column The column name to be quoted.
784 | * @return string
785 | * @throws InvalidArgumentException If the column name is invalid.
786 | */
787 | public function columnQuote(string $column): string
788 | {
789 | if (preg_match("/^" . $this::TABLE_PATTERN . "(\.?" . $this::ALIAS_PATTERN . ")?$/u", $column)) {
790 | return strpos($column, '.') !== false ?
791 | '"' . $this->prefix . str_replace('.', '"."', $column) . '"' :
792 | '"' . $column . '"';
793 | }
794 |
795 | throw new InvalidArgumentException("Incorrect column name: {$column}.");
796 | }
797 |
798 | /**
799 | * Mapping the type name as PDO data type.
800 | *
801 | * @param mixed $value
802 | * @param string $type
803 | * @return array
804 | */
805 | protected function typeMap($value, string $type): array
806 | {
807 | $map = [
808 | 'NULL' => PDO::PARAM_NULL,
809 | 'integer' => PDO::PARAM_INT,
810 | 'double' => PDO::PARAM_STR,
811 | 'boolean' => PDO::PARAM_BOOL,
812 | 'string' => PDO::PARAM_STR,
813 | 'object' => PDO::PARAM_STR,
814 | 'resource' => PDO::PARAM_LOB
815 | ];
816 |
817 | if ($type === 'boolean') {
818 | $value = ($value ? '1' : '0');
819 | } elseif ($type === 'NULL') {
820 | $value = null;
821 | }
822 |
823 | return [$value, $map[$type]];
824 | }
825 |
826 | /**
827 | * Build the statement part for the column stack.
828 | *
829 | * @param array|string $columns
830 | * @param array $map
831 | * @param bool $root
832 | * @param bool $isJoin
833 | * @return string
834 | */
835 | protected function columnPush(&$columns, array &$map, bool $root, bool $isJoin = false): string
836 | {
837 | if ($columns === '*') {
838 | return $columns;
839 | }
840 |
841 | $stack = [];
842 | $hasDistinct = false;
843 |
844 | if (is_string($columns)) {
845 | $columns = [$columns];
846 | }
847 |
848 | foreach ($columns as $key => $value) {
849 | $isIntKey = is_int($key);
850 | $isArrayValue = is_array($value);
851 |
852 | if (!$isIntKey && $isArrayValue && $root && count(array_keys($columns)) === 1) {
853 | $stack[] = $this->columnQuote($key);
854 | $stack[] = $this->columnPush($value, $map, false, $isJoin);
855 | } elseif ($isArrayValue) {
856 | $stack[] = $this->columnPush($value, $map, false, $isJoin);
857 | } elseif (!$isIntKey && $raw = $this->buildRaw($value, $map)) {
858 | preg_match("/(?" . $this::COLUMN_PATTERN . ")(\s*\[(?(String|Bool|Int|Number))\])?/u", $key, $match);
859 | $stack[] = "{$raw} AS {$this->columnQuote($match['column'])}";
860 | } elseif ($isIntKey && is_string($value)) {
861 | if ($isJoin && strpos($value, '*') !== false) {
862 | throw new InvalidArgumentException('Cannot use table.* to select all columns while joining table.');
863 | }
864 |
865 | preg_match("/(?" . $this::COLUMN_PATTERN . ")(?:\s*\((?" . $this::ALIAS_PATTERN . ")\))?(?:\s*\[(?(?:String|Bool|Int|Number|Object|JSON))\])?/u", $value, $match);
866 |
867 | $columnString = '';
868 |
869 | if (!empty($match['alias'])) {
870 | $columnString = "{$this->columnQuote($match['column'])} AS {$this->columnQuote($match['alias'])}";
871 | $columns[$key] = $match['alias'];
872 |
873 | if (!empty($match['type'])) {
874 | $columns[$key] .= ' [' . $match['type'] . ']';
875 | }
876 | } else {
877 | $columnString = $this->columnQuote($match['column']);
878 | }
879 |
880 | if (!$hasDistinct && strpos($value, '@') === 0) {
881 | $columnString = 'DISTINCT ' . $columnString;
882 | $hasDistinct = true;
883 | array_unshift($stack, $columnString);
884 |
885 | continue;
886 | }
887 |
888 | $stack[] = $columnString;
889 | }
890 | }
891 |
892 | return implode(',', $stack);
893 | }
894 |
895 | /**
896 | * Implode the Where conditions.
897 | *
898 | * @param array $data
899 | * @param array $map
900 | * @param string $conjunctor
901 | * @return string
902 | */
903 | protected function dataImplode(array $data, array &$map, string $conjunctor): string
904 | {
905 | $stack = [];
906 |
907 | foreach ($data as $key => $value) {
908 | $type = gettype($value);
909 |
910 | if (
911 | $type === 'array' &&
912 | preg_match("/^(AND|OR)(\s+#.*)?$/", $key, $relationMatch)
913 | ) {
914 | $stack[] = '(' . $this->dataImplode($value, $map, ' ' . $relationMatch[1]) . ')';
915 | continue;
916 | }
917 |
918 | $mapKey = $this->mapKey();
919 | $isIndex = is_int($key);
920 |
921 | preg_match(
922 | "/(?" . $this::COLUMN_PATTERN . ")(\[(?.*)\])?(?" . $this::COLUMN_PATTERN . ")?/u",
923 | $isIndex ? $value : $key,
924 | $match
925 | );
926 |
927 | $column = $this->columnQuote($match['column']);
928 | $operator = $match['operator'] ?? null;
929 |
930 | if ($isIndex && isset($match['comparison']) && in_array($operator, ['>', '>=', '<', '<=', '=', '!='])) {
931 | $stack[] = "{$column} {$operator} " . $this->columnQuote($match['comparison']);
932 | continue;
933 | }
934 |
935 | if ($operator && $operator !== '=') {
936 | if (in_array($operator, ['>', '>=', '<', '<='])) {
937 | $condition = "{$column} {$operator} ";
938 |
939 | if (is_numeric($value)) {
940 | $condition .= $mapKey;
941 | $map[$mapKey] = [$value, is_float($value) ? PDO::PARAM_STR : PDO::PARAM_INT];
942 | } elseif ($raw = $this->buildRaw($value, $map)) {
943 | $condition .= $raw;
944 | } else {
945 | $condition .= $mapKey;
946 | $map[$mapKey] = [$value, PDO::PARAM_STR];
947 | }
948 |
949 | $stack[] = $condition;
950 | } elseif ($operator === '!') {
951 | switch ($type) {
952 |
953 | case 'NULL':
954 | $stack[] = $column . ' IS NOT NULL';
955 | break;
956 |
957 | case 'array':
958 | $values = [];
959 |
960 | foreach ($value as $index => $item) {
961 | if ($raw = $this->buildRaw($item, $map)) {
962 | $values[] = $raw;
963 | } else {
964 | $stackKey = $mapKey . $index . '_i';
965 |
966 | $values[] = $stackKey;
967 | $map[$stackKey] = $this->typeMap($item, gettype($item));
968 | }
969 | }
970 |
971 | $stack[] = $column . ' NOT IN (' . implode(', ', $values) . ')';
972 | break;
973 |
974 | case 'object':
975 | if ($raw = $this->buildRaw($value, $map)) {
976 | $stack[] = "{$column} != {$raw}";
977 | }
978 | break;
979 |
980 | case 'integer':
981 | case 'double':
982 | case 'boolean':
983 | case 'string':
984 | $stack[] = "{$column} != {$mapKey}";
985 | $map[$mapKey] = $this->typeMap($value, $type);
986 | break;
987 | }
988 | } elseif ($operator === '~' || $operator === '!~') {
989 | if ($type !== 'array') {
990 | $value = [$value];
991 | }
992 |
993 | $connector = ' OR ';
994 | $data = array_values($value);
995 |
996 | if (is_array($data[0])) {
997 | if (isset($value['AND']) || isset($value['OR'])) {
998 | $connector = ' ' . array_keys($value)[0] . ' ';
999 | $value = $data[0];
1000 | }
1001 | }
1002 |
1003 | $likeClauses = [];
1004 |
1005 | foreach ($value as $index => $item) {
1006 | $likeKey = "{$mapKey}_{$index}_i";
1007 | $item = strval($item);
1008 |
1009 | if (!preg_match('/((?' || $operator === '><') {
1019 | if ($type === 'array') {
1020 | if ($operator === '><') {
1021 | $column .= ' NOT';
1022 | }
1023 |
1024 | if ($this->isRaw($value[0]) && $this->isRaw($value[1])) {
1025 | $stack[] = "({$column} BETWEEN {$this->buildRaw($value[0], $map)} AND {$this->buildRaw($value[1], $map)})";
1026 | } else {
1027 | $stack[] = "({$column} BETWEEN {$mapKey}a AND {$mapKey}b)";
1028 | $dataType = (is_numeric($value[0]) && is_numeric($value[1])) ? PDO::PARAM_INT : PDO::PARAM_STR;
1029 |
1030 | $map[$mapKey . 'a'] = [$value[0], $dataType];
1031 | $map[$mapKey . 'b'] = [$value[1], $dataType];
1032 | }
1033 | }
1034 | } elseif ($operator === 'REGEXP') {
1035 | $stack[] = "{$column} REGEXP {$mapKey}";
1036 | $map[$mapKey] = [$value, PDO::PARAM_STR];
1037 | } else {
1038 | throw new InvalidArgumentException("Invalid operator [{$operator}] for column {$column} supplied.");
1039 | }
1040 |
1041 | continue;
1042 | }
1043 |
1044 | switch ($type) {
1045 |
1046 | case 'NULL':
1047 | $stack[] = $column . ' IS NULL';
1048 | break;
1049 |
1050 | case 'array':
1051 | $values = [];
1052 |
1053 | foreach ($value as $index => $item) {
1054 | if ($raw = $this->buildRaw($item, $map)) {
1055 | $values[] = $raw;
1056 | } else {
1057 | $stackKey = $mapKey . $index . '_i';
1058 |
1059 | $values[] = $stackKey;
1060 | $map[$stackKey] = $this->typeMap($item, gettype($item));
1061 | }
1062 | }
1063 |
1064 | $stack[] = $column . ' IN (' . implode(', ', $values) . ')';
1065 | break;
1066 |
1067 | case 'object':
1068 | if ($raw = $this->buildRaw($value, $map)) {
1069 | $stack[] = "{$column} = {$raw}";
1070 | }
1071 | break;
1072 |
1073 | case 'integer':
1074 | case 'double':
1075 | case 'boolean':
1076 | case 'string':
1077 | $stack[] = "{$column} = {$mapKey}";
1078 | $map[$mapKey] = $this->typeMap($value, $type);
1079 | break;
1080 | }
1081 | }
1082 |
1083 | return implode($conjunctor . ' ', $stack);
1084 | }
1085 |
1086 | /**
1087 | * Build the where clause.
1088 | *
1089 | * @param array|null $where
1090 | * @param array $map
1091 | * @return string
1092 | */
1093 | protected function whereClause($where, array &$map): string
1094 | {
1095 | $clause = '';
1096 |
1097 | if (is_array($where)) {
1098 | $conditions = array_diff_key($where, array_flip(
1099 | ['GROUP', 'ORDER', 'HAVING', 'LIMIT', 'LIKE', 'MATCH']
1100 | ));
1101 |
1102 | if (!empty($conditions)) {
1103 | $clause = ' WHERE ' . $this->dataImplode($conditions, $map, ' AND');
1104 | }
1105 |
1106 | if (isset($where['MATCH']) && $this->type === 'mysql') {
1107 | $match = $where['MATCH'];
1108 |
1109 | if (is_array($match) && isset($match['columns'], $match['keyword'])) {
1110 | $mode = '';
1111 |
1112 | $options = [
1113 | 'natural' => 'IN NATURAL LANGUAGE MODE',
1114 | 'natural+query' => 'IN NATURAL LANGUAGE MODE WITH QUERY EXPANSION',
1115 | 'boolean' => 'IN BOOLEAN MODE',
1116 | 'query' => 'WITH QUERY EXPANSION'
1117 | ];
1118 |
1119 | if (isset($match['mode'], $options[$match['mode']])) {
1120 | $mode = ' ' . $options[$match['mode']];
1121 | }
1122 |
1123 | $columns = implode(', ', array_map([$this, 'columnQuote'], $match['columns']));
1124 | $mapKey = $this->mapKey();
1125 | $map[$mapKey] = [$match['keyword'], PDO::PARAM_STR];
1126 | $clause .= ($clause !== '' ? ' AND ' : ' WHERE') . ' MATCH (' . $columns . ') AGAINST (' . $mapKey . $mode . ')';
1127 | }
1128 | }
1129 |
1130 | if (isset($where['GROUP'])) {
1131 | $group = $where['GROUP'];
1132 |
1133 | if (is_array($group)) {
1134 | $stack = [];
1135 |
1136 | foreach ($group as $column => $value) {
1137 | $stack[] = $this->columnQuote($value);
1138 | }
1139 |
1140 | $clause .= ' GROUP BY ' . implode(',', $stack);
1141 | } elseif ($raw = $this->buildRaw($group, $map)) {
1142 | $clause .= ' GROUP BY ' . $raw;
1143 | } else {
1144 | $clause .= ' GROUP BY ' . $this->columnQuote($group);
1145 | }
1146 | }
1147 |
1148 | if (isset($where['HAVING'])) {
1149 | $having = $where['HAVING'];
1150 |
1151 | if ($raw = $this->buildRaw($having, $map)) {
1152 | $clause .= ' HAVING ' . $raw;
1153 | } else {
1154 | $clause .= ' HAVING ' . $this->dataImplode($having, $map, ' AND');
1155 | }
1156 | }
1157 |
1158 | if (isset($where['ORDER'])) {
1159 | $order = $where['ORDER'];
1160 |
1161 | if (is_array($order)) {
1162 | $stack = [];
1163 |
1164 | foreach ($order as $column => $value) {
1165 | if (is_array($value)) {
1166 | $valueStack = [];
1167 |
1168 | foreach ($value as $item) {
1169 | $valueStack[] = is_int($item) ? $item : $this->quote($item);
1170 | }
1171 |
1172 | $valueString = implode(',', $valueStack);
1173 | $stack[] = "FIELD({$this->columnQuote($column)}, {$valueString})";
1174 | } elseif ($value === 'ASC' || $value === 'DESC') {
1175 | $stack[] = $this->columnQuote($column) . ' ' . $value;
1176 | } elseif (is_int($column)) {
1177 | $stack[] = $this->columnQuote($value);
1178 | }
1179 | }
1180 |
1181 | $clause .= ' ORDER BY ' . implode(',', $stack);
1182 | } elseif ($raw = $this->buildRaw($order, $map)) {
1183 | $clause .= ' ORDER BY ' . $raw;
1184 | } else {
1185 | $clause .= ' ORDER BY ' . $this->columnQuote($order);
1186 | }
1187 | }
1188 |
1189 | if (isset($where['LIMIT'])) {
1190 | $limit = $where['LIMIT'];
1191 |
1192 | if (in_array($this->type, ['oracle', 'mssql'])) {
1193 | if ($this->type === 'mssql' && !isset($where['ORDER'])) {
1194 | $clause .= ' ORDER BY (SELECT 0)';
1195 | }
1196 |
1197 | if (is_numeric($limit)) {
1198 | $limit = [0, $limit];
1199 | }
1200 |
1201 | if (
1202 | is_array($limit) &&
1203 | is_numeric($limit[0]) &&
1204 | is_numeric($limit[1])
1205 | ) {
1206 | $clause .= " OFFSET {$limit[0]} ROWS FETCH NEXT {$limit[1]} ROWS ONLY";
1207 | }
1208 | } else {
1209 | if (is_numeric($limit)) {
1210 | $clause .= ' LIMIT ' . $limit;
1211 | } elseif (
1212 | is_array($limit) &&
1213 | is_numeric($limit[0]) &&
1214 | is_numeric($limit[1])
1215 | ) {
1216 | $clause .= " LIMIT {$limit[1]} OFFSET {$limit[0]}";
1217 | }
1218 | }
1219 | }
1220 | } elseif ($raw = $this->buildRaw($where, $map)) {
1221 | $clause .= ' ' . $raw;
1222 | }
1223 |
1224 | return $clause;
1225 | }
1226 |
1227 | /**
1228 | * Build statement for the select query.
1229 | *
1230 | * @param string $table
1231 | * @param array $map
1232 | * @param array|string $join
1233 | * @param array|string $columns
1234 | * @param array $where
1235 | * @param string $columnFn
1236 | * @return string
1237 | */
1238 | protected function selectContext(
1239 | string $table,
1240 | array &$map,
1241 | $join,
1242 | &$columns = null,
1243 | $where = null,
1244 | $columnFn = null
1245 | ): string {
1246 | preg_match("/(?" . $this::TABLE_PATTERN . ")\s*\((?" . $this::ALIAS_PATTERN . ")\)/u", $table, $tableMatch);
1247 |
1248 | if (isset($tableMatch['table'], $tableMatch['alias'])) {
1249 | $table = $this->tableQuote($tableMatch['table']);
1250 | $tableAlias = $this->tableQuote($tableMatch['alias']);
1251 | $tableQuery = "{$table}{$this->tableAliasConnector}{$tableAlias}";
1252 | } else {
1253 | $table = $this->tableQuote($table);
1254 | $tableQuery = $table;
1255 | }
1256 |
1257 | $isJoin = $this->isJoin($join);
1258 |
1259 | if ($isJoin) {
1260 | $tableQuery .= ' ' . $this->buildJoin($tableAlias ?? $table, $join, $map);
1261 | } else {
1262 | if (is_null($columns)) {
1263 | if (
1264 | !is_null($where) ||
1265 | (is_array($join) && isset($columnFn))
1266 | ) {
1267 | $where = $join;
1268 | $columns = null;
1269 | } else {
1270 | $where = null;
1271 | $columns = $join;
1272 | }
1273 | } else {
1274 | $where = $columns;
1275 | $columns = $join;
1276 | }
1277 | }
1278 |
1279 | if (isset($columnFn)) {
1280 | if ($columnFn === 1) {
1281 | $column = '1';
1282 |
1283 | if (is_null($where)) {
1284 | $where = $columns;
1285 | }
1286 | } elseif ($raw = $this->buildRaw($columnFn, $map)) {
1287 | $column = $raw;
1288 | } else {
1289 | if (empty($columns) || $this->isRaw($columns)) {
1290 | $columns = '*';
1291 | $where = $join;
1292 | }
1293 |
1294 | $column = $columnFn . '(' . $this->columnPush($columns, $map, true) . ')';
1295 | }
1296 | } else {
1297 | $column = $this->columnPush($columns, $map, true, $isJoin);
1298 | }
1299 |
1300 | return 'SELECT ' . $column . ' FROM ' . $tableQuery . $this->whereClause($where, $map);
1301 | }
1302 |
1303 | /**
1304 | * Determine the array with join syntax.
1305 | *
1306 | * @param mixed $join
1307 | * @return bool
1308 | */
1309 | protected function isJoin($join): bool
1310 | {
1311 | if (!is_array($join)) {
1312 | return false;
1313 | }
1314 |
1315 | $keys = array_keys($join);
1316 |
1317 | if (
1318 | isset($keys[0]) &&
1319 | is_string($keys[0]) &&
1320 | strpos($keys[0], '[') === 0
1321 | ) {
1322 | return true;
1323 | }
1324 |
1325 | return false;
1326 | }
1327 |
1328 | /**
1329 | * Build the join statement.
1330 | *
1331 | * @param string $table
1332 | * @param array $join
1333 | * @param array $map
1334 | * @return string
1335 | */
1336 | protected function buildJoin(string $table, array $join, array &$map): string
1337 | {
1338 | $tableJoin = [];
1339 | $type = [
1340 | '>' => 'LEFT',
1341 | '<' => 'RIGHT',
1342 | '<>' => 'FULL',
1343 | '><' => 'INNER'
1344 | ];
1345 |
1346 | foreach ($join as $subtable => $relation) {
1347 | preg_match("/(\[(?\<\>?|\>\)\])?(?" . $this::TABLE_PATTERN . ")\s?(\((?" . $this::ALIAS_PATTERN . ")\))?/u", $subtable, $match);
1348 |
1349 | if ($match['join'] === '' || $match['table'] === '') {
1350 | continue;
1351 | }
1352 |
1353 | if (is_string($relation)) {
1354 | $relation = 'USING ("' . $relation . '")';
1355 | } elseif (is_array($relation)) {
1356 | // For ['column1', 'column2']
1357 | if (isset($relation[0])) {
1358 | $relation = 'USING ("' . implode('", "', $relation) . '")';
1359 | } else {
1360 | $joins = [];
1361 |
1362 | foreach ($relation as $key => $value) {
1363 | if ($key === 'AND' && is_array($value)) {
1364 | $joins[] = $this->dataImplode($value, $map, ' AND');
1365 | continue;
1366 | }
1367 |
1368 | $joins[] = (
1369 | strpos($key, '.') > 0 ?
1370 | // For ['tableB.column' => 'column']
1371 | $this->columnQuote($key) :
1372 |
1373 | // For ['column1' => 'column2']
1374 | $table . '.' . $this->columnQuote($key)
1375 | ) .
1376 | ' = ' .
1377 | $this->tableQuote($match['alias'] ?? $match['table']) . '.' . $this->columnQuote($value);
1378 | }
1379 |
1380 | $relation = 'ON ' . implode(' AND ', $joins);
1381 | }
1382 | } elseif ($raw = $this->buildRaw($relation, $map)) {
1383 | $relation = $raw;
1384 | }
1385 |
1386 | $tableName = $this->tableQuote($match['table']);
1387 |
1388 | if (isset($match['alias'])) {
1389 | $tableName .= $this->tableAliasConnector . $this->tableQuote($match['alias']);
1390 | }
1391 |
1392 | $tableJoin[] = $type[$match['join']] . " JOIN {$tableName} {$relation}";
1393 | }
1394 |
1395 | return implode(' ', $tableJoin);
1396 | }
1397 |
1398 | /**
1399 | * Mapping columns for the stack.
1400 | *
1401 | * @param array|string $columns
1402 | * @param array $stack
1403 | * @param bool $root
1404 | * @return array
1405 | */
1406 | protected function columnMap($columns, array &$stack, bool $root): array
1407 | {
1408 | if ($columns === '*') {
1409 | return $stack;
1410 | }
1411 |
1412 | foreach ($columns as $key => $value) {
1413 | if (is_int($key)) {
1414 | preg_match("/(" . $this::TABLE_PATTERN . "\.)?(?" . $this::COLUMN_PATTERN . ")(?:\s*\((?" . $this::ALIAS_PATTERN . ")\))?(?:\s*\[(?(?:String|Bool|Int|Number|Object|JSON))\])?/u", $value, $keyMatch);
1415 |
1416 | $columnKey = !empty($keyMatch['alias']) ?
1417 | $keyMatch['alias'] :
1418 | $keyMatch['column'];
1419 |
1420 | $stack[$value] = isset($keyMatch['type']) ?
1421 | [$columnKey, $keyMatch['type']] :
1422 | [$columnKey];
1423 | } elseif ($this->isRaw($value)) {
1424 | preg_match("/(" . $this::TABLE_PATTERN . "\.)?(?" . $this::COLUMN_PATTERN . ")(\s*\[(?(String|Bool|Int|Number))\])?/u", $key, $keyMatch);
1425 | $columnKey = $keyMatch['column'];
1426 |
1427 | $stack[$key] = isset($keyMatch['type']) ?
1428 | [$columnKey, $keyMatch['type']] :
1429 | [$columnKey];
1430 | } elseif (!is_int($key) && is_array($value)) {
1431 | if ($root && count(array_keys($columns)) === 1) {
1432 | $stack[$key] = [$key, 'String'];
1433 | }
1434 |
1435 | $this->columnMap($value, $stack, false);
1436 | }
1437 | }
1438 |
1439 | return $stack;
1440 | }
1441 |
1442 | /**
1443 | * Mapping the data from the table.
1444 | *
1445 | * @param array $data
1446 | * @param array $columns
1447 | * @param array $columnMap
1448 | * @param array $stack
1449 | * @param bool $root
1450 | * @param array $result
1451 | * @codeCoverageIgnore
1452 | * @return void
1453 | */
1454 | protected function dataMap(
1455 | array $data,
1456 | array $columns,
1457 | array $columnMap,
1458 | array &$stack,
1459 | bool $root,
1460 | ?array &$result = null
1461 | ): void {
1462 | if ($root) {
1463 | $columnsKey = array_keys($columns);
1464 |
1465 | if (count($columnsKey) === 1 && is_array($columns[$columnsKey[0]])) {
1466 | $indexKey = array_keys($columns)[0];
1467 | $dataKey = preg_replace("/^" . $this::COLUMN_PATTERN . "\./u", '', $indexKey);
1468 | $currentStack = [];
1469 |
1470 | foreach ($data as $item) {
1471 | $this->dataMap($data, $columns[$indexKey], $columnMap, $currentStack, false, $result);
1472 | $index = $data[$dataKey];
1473 |
1474 | if (isset($result)) {
1475 | $result[$index] = $currentStack;
1476 | } else {
1477 | $stack[$index] = $currentStack;
1478 | }
1479 | }
1480 | } else {
1481 | $currentStack = [];
1482 | $this->dataMap($data, $columns, $columnMap, $currentStack, false, $result);
1483 |
1484 | if (isset($result)) {
1485 | $result[] = $currentStack;
1486 | } else {
1487 | $stack = $currentStack;
1488 | }
1489 | }
1490 |
1491 | return;
1492 | }
1493 |
1494 | foreach ($columns as $key => $value) {
1495 | $isRaw = $this->isRaw($value);
1496 |
1497 | if (is_int($key) || $isRaw) {
1498 | $map = $columnMap[$isRaw ? $key : $value];
1499 | $columnKey = $map[0];
1500 | $item = $data[$columnKey];
1501 |
1502 | if (isset($map[1])) {
1503 | if ($isRaw && in_array($map[1], ['Object', 'JSON'])) {
1504 | continue;
1505 | }
1506 |
1507 | if (is_null($item)) {
1508 | $stack[$columnKey] = null;
1509 | continue;
1510 | }
1511 |
1512 | switch ($map[1]) {
1513 |
1514 | case 'Number':
1515 | $stack[$columnKey] = (float) $item;
1516 | break;
1517 |
1518 | case 'Int':
1519 | $stack[$columnKey] = (int) $item;
1520 | break;
1521 |
1522 | case 'Bool':
1523 | $stack[$columnKey] = (bool) $item;
1524 | break;
1525 |
1526 | case 'Object':
1527 | $stack[$columnKey] = unserialize($item);
1528 | break;
1529 |
1530 | case 'JSON':
1531 | $stack[$columnKey] = json_decode($item, true);
1532 | break;
1533 |
1534 | case 'String':
1535 | $stack[$columnKey] = (string) $item;
1536 | break;
1537 | }
1538 | } else {
1539 | $stack[$columnKey] = $item;
1540 | }
1541 | } else {
1542 | $currentStack = [];
1543 | $this->dataMap($data, $value, $columnMap, $currentStack, false, $result);
1544 |
1545 | $stack[$key] = $currentStack;
1546 | }
1547 | }
1548 | }
1549 |
1550 | /**
1551 | * Build and execute returning query.
1552 | *
1553 | * @param string $query
1554 | * @param array $map
1555 | * @param array $data
1556 | * @return \PDOStatement|null
1557 | */
1558 | private function returningQuery($query, &$map, &$data): ?PDOStatement
1559 | {
1560 | $returnColumns = array_map(
1561 | function ($value) {
1562 | return $value[0];
1563 | },
1564 | $data
1565 | );
1566 |
1567 | $query .= ' RETURNING ' .
1568 | implode(', ', array_map([$this, 'columnQuote'], $returnColumns)) .
1569 | ' INTO ' .
1570 | implode(', ', array_keys($data));
1571 |
1572 | return $this->exec($query, $map, function ($statement) use (&$data) {
1573 | // @codeCoverageIgnoreStart
1574 | foreach ($data as $key => $return) {
1575 | if (isset($return[3])) {
1576 | $statement->bindParam($key, $data[$key][1], $return[2], $return[3]);
1577 | } else {
1578 | $statement->bindParam($key, $data[$key][1], $return[2]);
1579 | }
1580 | }
1581 | // @codeCoverageIgnoreEnd
1582 | });
1583 | }
1584 |
1585 | /**
1586 | * Create a table.
1587 | *
1588 | * @param string $table
1589 | * @param array $columns Columns definition.
1590 | * @param array $options Additional table options for creating a table.
1591 | * @return \PDOStatement|null
1592 | */
1593 | public function create(string $table, $columns, $options = null): ?PDOStatement
1594 | {
1595 | $stack = [];
1596 | $tableOption = '';
1597 | $tableName = $this->tableQuote($table);
1598 |
1599 | foreach ($columns as $name => $definition) {
1600 | if (is_int($name)) {
1601 | $stack[] = preg_replace("/\<(" . $this::COLUMN_PATTERN . ")\>/u", '"$1"', $definition);
1602 | } elseif (is_array($definition)) {
1603 | $stack[] = $this->columnQuote($name) . ' ' . implode(' ', $definition);
1604 | } elseif (is_string($definition)) {
1605 | $stack[] = $this->columnQuote($name) . ' ' . $definition;
1606 | }
1607 | }
1608 |
1609 | if (is_array($options)) {
1610 | $optionStack = [];
1611 |
1612 | foreach ($options as $key => $value) {
1613 | if (is_string($value) || is_int($value)) {
1614 | $optionStack[] = "{$key} = {$value}";
1615 | }
1616 | }
1617 |
1618 | $tableOption = ' ' . implode(', ', $optionStack);
1619 | } elseif (is_string($options)) {
1620 | $tableOption = ' ' . $options;
1621 | }
1622 |
1623 | $command = 'CREATE TABLE';
1624 |
1625 | if (in_array($this->type, ['mysql', 'pgsql', 'sqlite'])) {
1626 | $command .= ' IF NOT EXISTS';
1627 | }
1628 |
1629 | return $this->exec("{$command} {$tableName} (" . implode(', ', $stack) . "){$tableOption}");
1630 | }
1631 |
1632 | /**
1633 | * Drop a table.
1634 | *
1635 | * @param string $table
1636 | * @return \PDOStatement|null
1637 | */
1638 | public function drop(string $table): ?PDOStatement
1639 | {
1640 | return $this->exec('DROP TABLE IF EXISTS ' . $this->tableQuote($table));
1641 | }
1642 |
1643 | /**
1644 | * Select data from the table.
1645 | *
1646 | * @param string $table
1647 | * @param array $join
1648 | * @param array|string $columns
1649 | * @param array $where
1650 | * @return array|null
1651 | */
1652 | public function select(string $table, $join, $columns = null, $where = null): ?array
1653 | {
1654 | $map = [];
1655 | $result = [];
1656 | $columnMap = [];
1657 |
1658 | $args = func_get_args();
1659 | $lastArgs = $args[array_key_last($args)];
1660 | $callback = is_callable($lastArgs) ? $lastArgs : null;
1661 |
1662 | $where = is_callable($where) ? null : $where;
1663 | $columns = is_callable($columns) ? null : $columns;
1664 |
1665 | $column = $where === null ? $join : $columns;
1666 | $isSingle = (is_string($column) && $column !== '*');
1667 |
1668 | $statement = $this->exec($this->selectContext($table, $map, $join, $columns, $where), $map);
1669 |
1670 | $this->columnMap($columns, $columnMap, true);
1671 |
1672 | if (!$this->statement) {
1673 | return $result;
1674 | }
1675 |
1676 | // @codeCoverageIgnoreStart
1677 | if ($columns === '*') {
1678 | if (isset($callback)) {
1679 | while ($data = $statement->fetch(PDO::FETCH_ASSOC)) {
1680 | $callback($data);
1681 | }
1682 |
1683 | return null;
1684 | }
1685 |
1686 | return $statement->fetchAll(PDO::FETCH_ASSOC);
1687 | }
1688 |
1689 | while ($data = $statement->fetch(PDO::FETCH_ASSOC)) {
1690 | $currentStack = [];
1691 |
1692 | if (isset($callback)) {
1693 | $this->dataMap($data, $columns, $columnMap, $currentStack, true);
1694 |
1695 | $callback(
1696 | $isSingle ?
1697 | $currentStack[$columnMap[$column][0]] :
1698 | $currentStack
1699 | );
1700 | } else {
1701 | $this->dataMap($data, $columns, $columnMap, $currentStack, true, $result);
1702 | }
1703 | }
1704 |
1705 | if (isset($callback)) {
1706 | return null;
1707 | }
1708 |
1709 | if ($isSingle) {
1710 | $singleResult = [];
1711 | $resultKey = $columnMap[$column][0];
1712 |
1713 | foreach ($result as $item) {
1714 | $singleResult[] = $item[$resultKey];
1715 | }
1716 |
1717 | return $singleResult;
1718 | }
1719 |
1720 | return $result;
1721 | }
1722 | // @codeCoverageIgnoreEnd
1723 |
1724 | /**
1725 | * Insert one or more records into the table.
1726 | *
1727 | * @param string $table
1728 | * @param array $values
1729 | * @param string $primaryKey
1730 | * @return \PDOStatement|null
1731 | */
1732 | public function insert(string $table, array $values, ?string $primaryKey = null): ?PDOStatement
1733 | {
1734 | $stack = [];
1735 | $columns = [];
1736 | $fields = [];
1737 | $map = [];
1738 | $returnings = [];
1739 |
1740 | if (!isset($values[0])) {
1741 | $values = [$values];
1742 | }
1743 |
1744 | foreach ($values as $data) {
1745 | foreach ($data as $key => $value) {
1746 | $columns[] = $key;
1747 | }
1748 | }
1749 |
1750 | $columns = array_unique($columns);
1751 |
1752 | foreach ($values as $data) {
1753 | $values = [];
1754 |
1755 | foreach ($columns as $key) {
1756 | $value = $data[$key];
1757 | $type = gettype($value);
1758 |
1759 | if ($this->type === 'oracle' && $type === 'resource') {
1760 | $values[] = 'EMPTY_BLOB()';
1761 | $returnings[$this->mapKey()] = [$key, $value, PDO::PARAM_LOB];
1762 | continue;
1763 | }
1764 |
1765 | if ($raw = $this->buildRaw($data[$key], $map)) {
1766 | $values[] = $raw;
1767 | continue;
1768 | }
1769 |
1770 | $mapKey = $this->mapKey();
1771 | $values[] = $mapKey;
1772 |
1773 | switch ($type) {
1774 |
1775 | case 'array':
1776 | $map[$mapKey] = [
1777 | strpos($key, '[JSON]') === strlen($key) - 6 ?
1778 | json_encode($value) :
1779 | serialize($value),
1780 | PDO::PARAM_STR
1781 | ];
1782 | break;
1783 |
1784 | case 'object':
1785 | $value = serialize($value);
1786 | break;
1787 |
1788 | case 'NULL':
1789 | case 'resource':
1790 | case 'boolean':
1791 | case 'integer':
1792 | case 'double':
1793 | case 'string':
1794 | $map[$mapKey] = $this->typeMap($value, $type);
1795 | break;
1796 | }
1797 | }
1798 |
1799 | $stack[] = '(' . implode(', ', $values) . ')';
1800 | }
1801 |
1802 | foreach ($columns as $key) {
1803 | $fields[] = $this->columnQuote(preg_replace("/(\s*\[JSON\]$)/i", '', $key));
1804 | }
1805 |
1806 | $query = 'INSERT INTO ' . $this->tableQuote($table) . ' (' . implode(', ', $fields) . ') VALUES ' . implode(', ', $stack);
1807 |
1808 | if (
1809 | $this->type === 'oracle' && (!empty($returnings) || isset($primaryKey))
1810 | ) {
1811 | if ($primaryKey) {
1812 | $returnings[':RETURNID'] = [$primaryKey, '', PDO::PARAM_INT, 8];
1813 | }
1814 |
1815 | $statement = $this->returningQuery($query, $map, $returnings);
1816 |
1817 | if ($primaryKey) {
1818 | $this->returnId = $returnings[':RETURNID'][1];
1819 | }
1820 |
1821 | return $statement;
1822 | }
1823 |
1824 | return $this->exec($query, $map);
1825 | }
1826 |
1827 | /**
1828 | * Modify data from the table.
1829 | *
1830 | * @param string $table
1831 | * @param array $data
1832 | * @param array $where
1833 | * @return \PDOStatement|null
1834 | */
1835 | public function update(string $table, $data, $where = null): ?PDOStatement
1836 | {
1837 | $fields = [];
1838 | $map = [];
1839 | $returnings = [];
1840 |
1841 | foreach ($data as $key => $value) {
1842 | $column = $this->columnQuote(preg_replace("/(\s*\[(JSON|\+|\-|\*|\/)\]$)/", '', $key));
1843 | $type = gettype($value);
1844 |
1845 | if ($this->type === 'oracle' && $type === 'resource') {
1846 | $fields[] = "{$column} = EMPTY_BLOB()";
1847 | $returnings[$this->mapKey()] = [$key, $value, PDO::PARAM_LOB];
1848 | continue;
1849 | }
1850 |
1851 | if ($raw = $this->buildRaw($value, $map)) {
1852 | $fields[] = "{$column} = {$raw}";
1853 | continue;
1854 | }
1855 |
1856 | preg_match("/" . $this::COLUMN_PATTERN . "(\[(?\+|\-|\*|\/)\])?/u", $key, $match);
1857 |
1858 | if (isset($match['operator'])) {
1859 | if (is_numeric($value)) {
1860 | $fields[] = "{$column} = {$column} {$match['operator']} {$value}";
1861 | }
1862 | } else {
1863 | $mapKey = $this->mapKey();
1864 | $fields[] = "{$column} = {$mapKey}";
1865 |
1866 | switch ($type) {
1867 |
1868 | case 'array':
1869 | $map[$mapKey] = [
1870 | strpos($key, '[JSON]') === strlen($key) - 6 ?
1871 | json_encode($value) :
1872 | serialize($value),
1873 | PDO::PARAM_STR
1874 | ];
1875 | break;
1876 |
1877 | case 'object':
1878 | $value = serialize($value);
1879 |
1880 | break;
1881 | case 'NULL':
1882 | case 'resource':
1883 | case 'boolean':
1884 | case 'integer':
1885 | case 'double':
1886 | case 'string':
1887 | $map[$mapKey] = $this->typeMap($value, $type);
1888 | break;
1889 | }
1890 | }
1891 | }
1892 |
1893 | $query = 'UPDATE ' . $this->tableQuote($table) . ' SET ' . implode(', ', $fields) . $this->whereClause($where, $map);
1894 |
1895 | if ($this->type === 'oracle' && !empty($returnings)) {
1896 | return $this->returningQuery($query, $map, $returnings);
1897 | }
1898 |
1899 | return $this->exec($query, $map);
1900 | }
1901 |
1902 | /**
1903 | * Delete data from the table.
1904 | *
1905 | * @param string $table
1906 | * @param array|Raw $where
1907 | * @return \PDOStatement|null
1908 | */
1909 | public function delete(string $table, $where): ?PDOStatement
1910 | {
1911 | $map = [];
1912 |
1913 | return $this->exec('DELETE FROM ' . $this->tableQuote($table) . $this->whereClause($where, $map), $map);
1914 | }
1915 |
1916 | /**
1917 | * Replace old data with a new one.
1918 | *
1919 | * @param string $table
1920 | * @param array $columns
1921 | * @param array $where
1922 | * @return \PDOStatement|null
1923 | */
1924 | public function replace(string $table, array $columns, $where = null): ?PDOStatement
1925 | {
1926 | $map = [];
1927 | $stack = [];
1928 |
1929 | foreach ($columns as $column => $replacements) {
1930 | if (is_array($replacements)) {
1931 | foreach ($replacements as $old => $new) {
1932 | $mapKey = $this->mapKey();
1933 | $columnName = $this->columnQuote($column);
1934 | $stack[] = "{$columnName} = REPLACE({$columnName}, {$mapKey}a, {$mapKey}b)";
1935 |
1936 | $map[$mapKey . 'a'] = [$old, PDO::PARAM_STR];
1937 | $map[$mapKey . 'b'] = [$new, PDO::PARAM_STR];
1938 | }
1939 | }
1940 | }
1941 |
1942 | if (empty($stack)) {
1943 | throw new InvalidArgumentException('Invalid columns supplied.');
1944 | }
1945 |
1946 | return $this->exec('UPDATE ' . $this->tableQuote($table) . ' SET ' . implode(', ', $stack) . $this->whereClause($where, $map), $map);
1947 | }
1948 |
1949 | /**
1950 | * Get only one record from the table.
1951 | *
1952 | * @param string $table
1953 | * @param array $join
1954 | * @param array|string $columns
1955 | * @param array $where
1956 | * @return mixed
1957 | */
1958 | public function get(string $table, $join = null, $columns = null, $where = null)
1959 | {
1960 | $map = [];
1961 | $result = [];
1962 | $columnMap = [];
1963 | $currentStack = [];
1964 |
1965 | if ($where === null) {
1966 | if ($this->isJoin($join)) {
1967 | $where['LIMIT'] = 1;
1968 | } else {
1969 | $columns['LIMIT'] = 1;
1970 | }
1971 |
1972 | $column = $join;
1973 | } else {
1974 | $column = $columns;
1975 | $where['LIMIT'] = 1;
1976 | }
1977 |
1978 | $isSingle = (is_string($column) && $column !== '*');
1979 | $query = $this->exec($this->selectContext($table, $map, $join, $columns, $where), $map);
1980 |
1981 | if (!$this->statement) {
1982 | return false;
1983 | }
1984 |
1985 | // @codeCoverageIgnoreStart
1986 | $data = $query->fetchAll(PDO::FETCH_ASSOC);
1987 |
1988 | if (isset($data[0])) {
1989 | if ($column === '*') {
1990 | return $data[0];
1991 | }
1992 |
1993 | $this->columnMap($columns, $columnMap, true);
1994 | $this->dataMap($data[0], $columns, $columnMap, $currentStack, true, $result);
1995 |
1996 | if ($isSingle) {
1997 | return $result[0][$columnMap[$column][0]];
1998 | }
1999 |
2000 | return $result[0];
2001 | }
2002 | }
2003 | // @codeCoverageIgnoreEnd
2004 |
2005 | /**
2006 | * Determine whether the target data existed from the table.
2007 | *
2008 | * @param string $table
2009 | * @param array $join
2010 | * @param array $where
2011 | * @return bool
2012 | */
2013 | public function has(string $table, $join, $where = null): bool
2014 | {
2015 | $map = [];
2016 | $column = null;
2017 |
2018 | $query = $this->exec(
2019 | $this->type === 'mssql' ?
2020 | $this->selectContext($table, $map, $join, $column, $where, Medoo::raw('TOP 1 1')) :
2021 | 'SELECT EXISTS(' . $this->selectContext($table, $map, $join, $column, $where, 1) . ')',
2022 | $map
2023 | );
2024 |
2025 | if (!$this->statement) {
2026 | return false;
2027 | }
2028 |
2029 | // @codeCoverageIgnoreStart
2030 | $result = $query->fetchColumn();
2031 |
2032 | return $result === '1' || $result === 1 || $result === true;
2033 | }
2034 | // @codeCoverageIgnoreEnd
2035 |
2036 | /**
2037 | * Randomly fetch data from the table.
2038 | *
2039 | * @param string $table
2040 | * @param array $join
2041 | * @param array|string $columns
2042 | * @param array $where
2043 | * @return array
2044 | */
2045 | public function rand(string $table, $join = null, $columns = null, $where = null): array
2046 | {
2047 | $orderRaw = $this->raw(
2048 | $this->type === 'mysql' ? 'RAND()'
2049 | : ($this->type === 'mssql' ? 'NEWID()'
2050 | : 'RANDOM()')
2051 | );
2052 |
2053 | if ($where === null) {
2054 | if ($this->isJoin($join)) {
2055 | $where['ORDER'] = $orderRaw;
2056 | } else {
2057 | $columns['ORDER'] = $orderRaw;
2058 | }
2059 | } else {
2060 | $where['ORDER'] = $orderRaw;
2061 | }
2062 |
2063 | return $this->select($table, $join, $columns, $where);
2064 | }
2065 |
2066 | /**
2067 | * Build for the aggregate function.
2068 | *
2069 | * @param string $type
2070 | * @param string $table
2071 | * @param array $join
2072 | * @param string $column
2073 | * @param array $where
2074 | * @return string|null
2075 | */
2076 | private function aggregate(string $type, string $table, $join = null, $column = null, $where = null): ?string
2077 | {
2078 | $map = [];
2079 |
2080 | $query = $this->exec($this->selectContext($table, $map, $join, $column, $where, $type), $map);
2081 |
2082 | if (!$this->statement) {
2083 | return null;
2084 | }
2085 |
2086 | // @codeCoverageIgnoreStart
2087 | return (string) $query->fetchColumn();
2088 | }
2089 | // @codeCoverageIgnoreEnd
2090 |
2091 | /**
2092 | * Count the number of rows from the table.
2093 | *
2094 | * @param string $table
2095 | * @param array $join
2096 | * @param string $column
2097 | * @param array $where
2098 | * @return int|null
2099 | */
2100 | public function count(string $table, $join = null, $column = null, $where = null): ?int
2101 | {
2102 | return (int) $this->aggregate('COUNT', $table, $join, $column, $where);
2103 | }
2104 |
2105 | /**
2106 | * Calculate the average value of the column.
2107 | *
2108 | * @param string $table
2109 | * @param array $join
2110 | * @param string $column
2111 | * @param array $where
2112 | * @return string|null
2113 | */
2114 | public function avg(string $table, $join, $column = null, $where = null): ?string
2115 | {
2116 | return $this->aggregate('AVG', $table, $join, $column, $where);
2117 | }
2118 |
2119 | /**
2120 | * Get the maximum value of the column.
2121 | *
2122 | * @param string $table
2123 | * @param array $join
2124 | * @param string $column
2125 | * @param array $where
2126 | * @return string|null
2127 | */
2128 | public function max(string $table, $join, $column = null, $where = null): ?string
2129 | {
2130 | return $this->aggregate('MAX', $table, $join, $column, $where);
2131 | }
2132 |
2133 | /**
2134 | * Get the minimum value of the column.
2135 | *
2136 | * @param string $table
2137 | * @param array $join
2138 | * @param string $column
2139 | * @param array $where
2140 | * @return string|null
2141 | */
2142 | public function min(string $table, $join, $column = null, $where = null): ?string
2143 | {
2144 | return $this->aggregate('MIN', $table, $join, $column, $where);
2145 | }
2146 |
2147 | /**
2148 | * Calculate the total value of the column.
2149 | *
2150 | * @param string $table
2151 | * @param array $join
2152 | * @param string $column
2153 | * @param array $where
2154 | * @return string|null
2155 | */
2156 | public function sum(string $table, $join, $column = null, $where = null): ?string
2157 | {
2158 | return $this->aggregate('SUM', $table, $join, $column, $where);
2159 | }
2160 |
2161 | /**
2162 | * Start a transaction.
2163 | *
2164 | * @param callable $actions
2165 | * @codeCoverageIgnore
2166 | * @return void
2167 | */
2168 | public function action(callable $actions): void
2169 | {
2170 | if (is_callable($actions)) {
2171 | $this->pdo->beginTransaction();
2172 |
2173 | try {
2174 | $result = $actions($this);
2175 |
2176 | if ($result === false) {
2177 | $this->pdo->rollBack();
2178 | } else {
2179 | $this->pdo->commit();
2180 | }
2181 | } catch (Exception $e) {
2182 | $this->pdo->rollBack();
2183 | throw $e;
2184 | }
2185 | }
2186 | }
2187 |
2188 | /**
2189 | * Return the ID for the last inserted row.
2190 | *
2191 | * @param string $name
2192 | * @codeCoverageIgnore
2193 | * @return string|null
2194 | */
2195 | public function id(?string $name = null): ?string
2196 | {
2197 | $type = $this->type;
2198 |
2199 | if ($type === 'oracle') {
2200 | return $this->returnId;
2201 | } elseif ($type === 'pgsql') {
2202 | $id = $this->pdo->query('SELECT LASTVAL()')->fetchColumn();
2203 |
2204 | return (string) $id ?: null;
2205 | }
2206 |
2207 | return $this->pdo->lastInsertId($name);
2208 | }
2209 |
2210 | /**
2211 | * Enable debug mode and output readable statement string.
2212 | *
2213 | * @codeCoverageIgnore
2214 | * @return Medoo
2215 | */
2216 | public function debug(): self
2217 | {
2218 | $this->debugMode = true;
2219 |
2220 | return $this;
2221 | }
2222 |
2223 | /**
2224 | * Enable debug logging mode.
2225 | *
2226 | * @codeCoverageIgnore
2227 | * @return void
2228 | */
2229 | public function beginDebug(): void
2230 | {
2231 | $this->debugMode = true;
2232 | $this->debugLogging = true;
2233 | }
2234 |
2235 | /**
2236 | * Disable debug logging and return all readable statements.
2237 | *
2238 | * @codeCoverageIgnore
2239 | * @return void
2240 | */
2241 | public function debugLog(): array
2242 | {
2243 | $this->debugMode = false;
2244 | $this->debugLogging = false;
2245 |
2246 | return $this->debugLogs;
2247 | }
2248 |
2249 | /**
2250 | * Return the last performed statement.
2251 | *
2252 | * @codeCoverageIgnore
2253 | * @return string|null
2254 | */
2255 | public function last(): ?string
2256 | {
2257 | if (empty($this->logs)) {
2258 | return null;
2259 | }
2260 |
2261 | $log = $this->logs[array_key_last($this->logs)];
2262 |
2263 | return $this->generate($log[0], $log[1]);
2264 | }
2265 |
2266 | /**
2267 | * Return all executed statements.
2268 | *
2269 | * @codeCoverageIgnore
2270 | * @return string[]
2271 | */
2272 | public function log(): array
2273 | {
2274 | return array_map(
2275 | function ($log) {
2276 | return $this->generate($log[0], $log[1]);
2277 | },
2278 | $this->logs
2279 | );
2280 | }
2281 |
2282 | /**
2283 | * Get information about the database connection.
2284 | *
2285 | * @codeCoverageIgnore
2286 | * @return array
2287 | */
2288 | public function info(): array
2289 | {
2290 | $output = [
2291 | 'server' => 'SERVER_INFO',
2292 | 'driver' => 'DRIVER_NAME',
2293 | 'client' => 'CLIENT_VERSION',
2294 | 'version' => 'SERVER_VERSION',
2295 | 'connection' => 'CONNECTION_STATUS'
2296 | ];
2297 |
2298 | foreach ($output as $key => $value) {
2299 | try {
2300 | $output[$key] = $this->pdo->getAttribute(constant('PDO::ATTR_' . $value));
2301 | } catch (PDOException $e) {
2302 | $output[$key] = $e->getMessage();
2303 | }
2304 | }
2305 |
2306 | $output['dsn'] = $this->dsn;
2307 |
2308 | return $output;
2309 | }
2310 | }
2311 |
--------------------------------------------------------------------------------
/tests/AggregateTest.php:
--------------------------------------------------------------------------------
1 | setType($type);
19 |
20 | $this->database->count("account", [
21 | "gender" => "female"
22 | ]);
23 |
24 | $this->assertQuery(
25 | <<database->queryString
31 | );
32 | }
33 |
34 | /**
35 | * @covers ::max()
36 | * @covers ::aggregate()
37 | * @covers ::selectContext()
38 | * @dataProvider typesProvider
39 | */
40 | public function testMax($type)
41 | {
42 | $this->setType($type);
43 |
44 | $this->database->max("account", "age");
45 |
46 | $this->assertQuery(
47 | <<database->queryString
52 | );
53 | }
54 |
55 | /**
56 | * @covers ::min()
57 | * @covers ::aggregate()
58 | * @covers ::selectContext()
59 | * @dataProvider typesProvider
60 | */
61 | public function testMin($type)
62 | {
63 | $this->setType($type);
64 |
65 | $this->database->min("account", "age");
66 |
67 | $this->assertQuery(
68 | <<database->queryString
73 | );
74 | }
75 |
76 | /**
77 | * @covers ::avg()
78 | * @covers ::aggregate()
79 | * @covers ::selectContext()
80 | * @dataProvider typesProvider
81 | */
82 | public function testAvg($type)
83 | {
84 | $this->setType($type);
85 |
86 | $this->database->avg("account", "age");
87 |
88 | $this->assertQuery(
89 | <<database->queryString
94 | );
95 | }
96 |
97 | /**
98 | * @covers ::sum()
99 | * @covers ::aggregate()
100 | * @covers ::selectContext()
101 | * @dataProvider typesProvider
102 | */
103 | public function testSum($type)
104 | {
105 | $this->setType($type);
106 |
107 | $this->database->sum("account", "money");
108 |
109 | $this->assertQuery(
110 | <<database->queryString
115 | );
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/tests/CreateTest.php:
--------------------------------------------------------------------------------
1 | setType($type);
17 |
18 | $this->database->create("account", [
19 | "id" => [
20 | "INT",
21 | "NOT NULL",
22 | "AUTO_INCREMENT"
23 | ],
24 | "email" => [
25 | "VARCHAR(70)",
26 | "NOT NULL",
27 | "UNIQUE"
28 | ],
29 | "PRIMARY KEY ()"
30 | ], [
31 | "AUTO_INCREMENT" => 200
32 | ]);
33 |
34 | $this->assertQuery(
35 | [
36 | 'default' => << << <<database->queryString
59 | );
60 | }
61 |
62 | /**
63 | * @covers ::create()
64 | * @dataProvider typesProvider
65 | */
66 | public function testCreateWithStringDefinition($type)
67 | {
68 | $this->setType($type);
69 |
70 | $this->database->create("account", [
71 | "id" => "INT NOT NULL AUTO_INCREMENT",
72 | "email" => "VARCHAR(70) NOT NULL UNIQUE"
73 | ]);
74 |
75 | $this->assertQuery(
76 | [
77 | 'default' => << << <<database->queryString
94 | );
95 | }
96 |
97 | /**
98 | * @covers ::create()
99 | * @dataProvider typesProvider
100 | */
101 | public function testCreateWithSingleOption($type)
102 | {
103 | $this->setType($type);
104 |
105 | $this->database->create("account", [
106 | "id" => [
107 | "INT",
108 | "NOT NULL",
109 | "AUTO_INCREMENT"
110 | ],
111 | "email" => [
112 | "VARCHAR(70)",
113 | "NOT NULL",
114 | "UNIQUE"
115 | ]
116 | ], "TABLESPACE tablespace_name");
117 |
118 | $this->assertQuery(
119 | [
120 | 'default' => << << <<database->queryString
140 | );
141 | }
142 | }
143 |
--------------------------------------------------------------------------------
/tests/DeleteTest.php:
--------------------------------------------------------------------------------
1 | setType($type);
19 |
20 | $this->database->delete("account", [
21 | "AND" => [
22 | "type" => "business",
23 | "age[<]" => 18
24 | ]
25 | ]);
26 |
27 | $this->assertQuery(
28 | <<database->queryString
33 | );
34 | }
35 |
36 | /**
37 | * @covers ::delete()
38 | * @dataProvider typesProvider
39 | */
40 | public function testDeleteRaw($type)
41 | {
42 | $this->setType($type);
43 |
44 | $whereClause = Medoo::raw("WHERE ( = :type AND < :age)", [
45 | ':type' => 'business',
46 | ':age' => 18,
47 | ]);
48 |
49 | $this->database->delete("account", $whereClause);
50 |
51 | $this->assertQuery(
52 | <<database->queryString
57 | );
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/tests/DropTest.php:
--------------------------------------------------------------------------------
1 | setType($type);
19 |
20 | $this->database->drop("account");
21 |
22 | $this->assertQuery(
23 | <<database->queryString
27 | );
28 | }
29 |
30 | /**
31 | * @covers ::drop()
32 | */
33 | public function testDropWithPrefix()
34 | {
35 | $database = new Medoo([
36 | 'testMode' => true,
37 | 'prefix' => 'PREFIX_'
38 | ]);
39 |
40 | $database->type = "sqlite";
41 |
42 | $database->drop("account");
43 |
44 | $this->assertQuery(
45 | <<queryString
49 | );
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/tests/GetTest.php:
--------------------------------------------------------------------------------
1 | setType($type);
17 |
18 | $this->database->get("account", "email", [
19 | "user_id" => 1234
20 | ]);
21 |
22 | $this->assertQuery([
23 | 'default' => << << <<database->queryString);
43 | }
44 |
45 | /**
46 | * @covers ::get()
47 | * @dataProvider typesProvider
48 | */
49 | public function testGetWithColumns($type)
50 | {
51 | $this->setType($type);
52 |
53 | $this->database->get("account", [
54 | "email",
55 | "location"
56 | ], [
57 | "user_id" => 1234
58 | ]);
59 |
60 | $this->assertQuery([
61 | 'default' => << << <<database->queryString);
81 | }
82 |
83 | /**
84 | * @covers ::get()
85 | * @dataProvider typesProvider
86 | */
87 | public function testGetWithJoin($type)
88 | {
89 | $this->setType($type);
90 |
91 | $this->database->get("post", [
92 | "[>]account" => "user_id"
93 | ], [
94 | "post.content",
95 | "account.user_name"
96 | ]);
97 |
98 | $this->assertQuery([
99 | 'default' => << << <<database->queryString);
119 | }
120 |
121 | /**
122 | * @covers ::get()
123 | * @dataProvider typesProvider
124 | */
125 | public function testGetWithJoinAndWhere($type)
126 | {
127 | $this->setType($type);
128 |
129 | $this->database->get("post", [
130 | "[>]account" => "user_id"
131 | ], [
132 | "post.content",
133 | "account.user_name"
134 | ], [
135 | 'account.age[>]' => 18
136 | ]);
137 |
138 | $this->assertQuery([
139 | 'default' => << 18
144 | LIMIT 1
145 | EOD,
146 | 'mssql' => << 18
151 | ORDER BY (SELECT 0)
152 | OFFSET 0 ROWS FETCH NEXT 1 ROWS ONLY
153 | EOD,
154 | 'oracle' => << 18
159 | OFFSET 0 ROWS FETCH NEXT 1 ROWS ONLY
160 | EOD,
161 | ], $this->database->queryString);
162 | }
163 | }
164 |
--------------------------------------------------------------------------------
/tests/HasTest.php:
--------------------------------------------------------------------------------
1 | setType($type);
18 |
19 | $this->database->has("account", [
20 | "user_name" => "foo"
21 | ]);
22 |
23 | $this->assertQuery([
24 | 'default' => << <<database->queryString);
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/tests/InsertTest.php:
--------------------------------------------------------------------------------
1 | setType($type);
20 |
21 | $this->database->insert("account", [
22 | "user_name" => "foo",
23 | "email" => "foo@bar.com"
24 | ]);
25 |
26 | $this->assertQuery(
27 | <<database->queryString
32 | );
33 | }
34 |
35 | /**
36 | * @covers ::insert()
37 | * @covers ::typeMap()
38 | * @dataProvider typesProvider
39 | */
40 | public function testInsertWithArray($type)
41 | {
42 | $this->setType($type);
43 |
44 | $this->database->insert("account", [
45 | "user_name" => "foo",
46 | "lang" => ["en", "fr"]
47 | ]);
48 |
49 | $this->assertQuery([
50 | 'default' => << <<database->queryString);
59 | }
60 |
61 | /**
62 | * @covers ::insert()
63 | * @covers ::typeMap()
64 | * @dataProvider typesProvider
65 | */
66 | public function testInsertWithJSON($type)
67 | {
68 | $this->setType($type);
69 |
70 | $this->database->insert("account", [
71 | "user_name" => "foo",
72 | "lang [JSON]" => ["en", "fr"]
73 | ]);
74 |
75 | $this->assertQuery([
76 | 'default' => << <<database->queryString);
85 | }
86 |
87 | /**
88 | * @covers ::insert()
89 | * @dataProvider typesProvider
90 | */
91 | public function testInsertWithRaw($type)
92 | {
93 | $this->setType($type);
94 |
95 | $this->database->insert("account", [
96 | "user_name" => Medoo::raw("UUID()")
97 | ]);
98 |
99 | $this->assertQuery(
100 | <<database->queryString
105 | );
106 | }
107 |
108 | /**
109 | * @covers ::insert()
110 | * @covers ::typeMap()
111 | * @dataProvider typesProvider
112 | */
113 | public function testInsertWithNull($type)
114 | {
115 | $this->setType($type);
116 |
117 | $this->database->insert("account", [
118 | "location" => null
119 | ]);
120 |
121 | $this->assertQuery(
122 | <<database->queryString
127 | );
128 | }
129 |
130 | /**
131 | * @covers ::insert()
132 | * @covers ::typeMap()
133 | * @dataProvider typesProvider
134 | */
135 | public function testInsertWithObject($type)
136 | {
137 | $this->setType($type);
138 |
139 | $objectData = new Foo();
140 |
141 | $this->database->insert("account", [
142 | "object" => $objectData
143 | ]);
144 |
145 | $this->assertQuery(
146 | <<database->queryString
151 | );
152 | }
153 |
154 | /**
155 | * @covers ::insert()
156 | * @dataProvider typesProvider
157 | */
158 | public function testMultiInsert($type)
159 | {
160 | $this->setType($type);
161 |
162 | $this->database->insert("account", [
163 | [
164 | "user_name" => "foo",
165 | "email" => "foo@bar.com"
166 | ],
167 | [
168 | "user_name" => "bar",
169 | "email" => "bar@foo.com"
170 | ]
171 | ]);
172 |
173 | $this->assertQuery(
174 | <<database->queryString
179 | );
180 | }
181 |
182 | public function testOracleWithPrimaryKeyInsert()
183 | {
184 | $this->setType("oracle");
185 |
186 | $this->database->insert("ACCOUNT", [
187 | "NAME" => "foo",
188 | "EMAIL" => "foo@bar.com"
189 | ], "ID");
190 |
191 | $this->assertQuery(
192 | <<database->queryString
198 | );
199 | }
200 |
201 | public function testOracleWithLOBsInsert()
202 | {
203 | $this->setType("oracle");
204 |
205 | $fp = fopen('README.md', 'r');
206 |
207 | $this->database->insert("ACCOUNT", [
208 | "NAME" => "foo",
209 | "DATA" => $fp
210 | ]);
211 |
212 | $this->assertQuery(
213 | <<database->queryString
219 | );
220 | }
221 |
222 | public function testOracleWithLOBsAndIdInsert()
223 | {
224 | $this->setType("oracle");
225 |
226 | $fp = fopen('README.md', 'r');
227 |
228 | $this->database->insert("ACCOUNT", [
229 | "NAME" => "foo",
230 | "DATA" => $fp
231 | ], "ID");
232 |
233 | $this->assertQuery(
234 | <<database->queryString
240 | );
241 | }
242 | }
243 |
--------------------------------------------------------------------------------
/tests/MedooTestCase.php:
--------------------------------------------------------------------------------
1 | database = new Medoo([
18 | 'testMode' => true
19 | ]);
20 | }
21 |
22 | public function typesProvider(): array
23 | {
24 | return [
25 | 'MySQL' => ['mysql'],
26 | 'MSSQL' => ['mssql'],
27 | 'SQLite' => ['sqlite'],
28 | 'PostgreSQL' => ['pgsql'],
29 | 'Oracle' => ['oracle']
30 | ];
31 | }
32 |
33 | public function setType($type): void
34 | {
35 | $this->database->setupType($type);
36 |
37 | if ($type === 'oracle') {
38 | $this->tableAliasConnector = ' ';
39 | } elseif ($type === 'mysql') {
40 | $this->quotePattern = '`$1`';
41 | } elseif ($type === 'mssql') {
42 | $this->quotePattern = '[$1]';
43 | }
44 | }
45 |
46 | public function expectedQuery($expected): string
47 | {
48 | $result = preg_replace(
49 | '/(?!\'[^\s]+\s?)"([\p{L}_][\p{L}\p{N}@$#\-_]*)"(?!\s?[^\s]+\')/u',
50 | $this->quotePattern,
51 | str_replace("\n", " ", $expected)
52 | );
53 |
54 | return str_replace(
55 | ' @AS ',
56 | $this->tableAliasConnector,
57 | $result
58 | );
59 | }
60 |
61 | public function assertQuery($expected, $query): void
62 | {
63 | if (is_array($expected)) {
64 | $this->assertEquals(
65 | $this->expectedQuery($expected[$this->database->type] ?? $expected['default']),
66 | $query
67 | );
68 | } else {
69 | $this->assertEquals($this->expectedQuery($expected), $query);
70 | }
71 | }
72 | }
73 |
74 | class Foo
75 | {
76 | public $bar = "cat";
77 |
78 | public function __wakeup()
79 | {
80 | $this->bar = "dog";
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/tests/QueryTest.php:
--------------------------------------------------------------------------------
1 | setType($type);
21 |
22 | $this->database->query("SELECT , FROM WHERE != 100");
23 |
24 | $this->assertQuery(
25 | <<database->queryString
31 | );
32 | }
33 |
34 | /**
35 | * @covers ::query()
36 | * @covers ::isRaw()
37 | * @covers ::buildRaw()
38 | */
39 | public function testQueryWithPrefix()
40 | {
41 | $database = new Medoo([
42 | 'testMode' => true,
43 | 'prefix' => 'PREFIX_'
44 | ]);
45 |
46 | $database->query("SELECT FROM ");
47 |
48 | $this->assertQuery(
49 | <<queryString
54 | );
55 | }
56 |
57 | /**
58 | * @covers ::query()
59 | * @covers ::isRaw()
60 | * @covers ::buildRaw()
61 | */
62 | public function testQueryTableWithPrefix()
63 | {
64 | $database = new Medoo([
65 | 'testMode' => true,
66 | 'prefix' => 'PREFIX_'
67 | ]);
68 |
69 | $database->query("DROP TABLE IF EXISTS ");
70 |
71 | $this->assertQuery(
72 | <<queryString
76 | );
77 | }
78 |
79 | /**
80 | * @covers ::query()
81 | * @covers ::isRaw()
82 | * @covers ::buildRaw()
83 | */
84 | public function testQueryShowTableWithPrefix()
85 | {
86 | $database = new Medoo([
87 | 'testMode' => true,
88 | 'prefix' => 'PREFIX_'
89 | ]);
90 |
91 | $database->query("SHOW TABLES LIKE ");
92 |
93 | $this->assertQuery(
94 | <<queryString
98 | );
99 | }
100 |
101 | /**
102 | * @covers ::query()
103 | * @covers ::isRaw()
104 | * @covers ::buildRaw()
105 | * @dataProvider typesProvider
106 | */
107 | public function testPreparedStatementQuery($type)
108 | {
109 | $this->setType($type);
110 |
111 | $this->database->query(
112 | "SELECT * FROM WHERE = :user_name AND = :age",
113 | [
114 | ":user_name" => "John Smite",
115 | ":age" => 20
116 | ]
117 | );
118 |
119 | $this->assertQuery(
120 | <<database->queryString
126 | );
127 | }
128 |
129 | /**
130 | * @covers ::query()
131 | * @covers ::isRaw()
132 | * @covers ::buildRaw()
133 | */
134 | public function testQueryEscape()
135 | {
136 | $database = new Medoo([
137 | 'testMode' => true,
138 | 'prefix' => 'PREFIX_'
139 | ]);
140 |
141 | $database->query("SELECT * FROM WHERE = ''");
142 |
143 | $this->assertQuery(
144 | <<'
148 | EOD,
149 | $database->queryString
150 | );
151 | }
152 | }
153 |
--------------------------------------------------------------------------------
/tests/QuoteTest.php:
--------------------------------------------------------------------------------
1 | setType($type);
20 |
21 | $quotedString = $this->database->quote("Co'mpl''ex \"st'\"ring");
22 |
23 | $expected = [
24 | 'mysql' => << << << << <<assertEquals($expected[$type], $quotedString);
42 | }
43 |
44 | /**
45 | * @covers ::columnQuote()
46 | */
47 | public function testColumnQuote()
48 | {
49 | $this->assertEquals('"ColumnName"', $this->database->columnQuote("ColumnName"));
50 | $this->assertEquals('"Column"."name"', $this->database->columnQuote("Column.name"));
51 | $this->assertEquals('"Column"."Name"', $this->database->columnQuote("Column.Name"));
52 |
53 | $this->assertEquals('"ネーム"', $this->database->columnQuote("ネーム"));
54 | }
55 |
56 | public function columnNamesProvider(): array
57 | {
58 | return [
59 | ["9ColumnName"],
60 | ["@ColumnName"],
61 | [".ColumnName"],
62 | ["ColumnName."],
63 | ["ColumnName (alias)"]
64 | ];
65 | }
66 |
67 | /**
68 | * @covers ::columnQuote()
69 | * @dataProvider columnNamesProvider
70 | */
71 | public function testIncorrectColumnQuote($column)
72 | {
73 | $this->expectException(InvalidArgumentException::class);
74 |
75 | $this->database->columnQuote($column);
76 | }
77 |
78 | /**
79 | * @covers ::tableQuote()
80 | */
81 | public function testTableQuote()
82 | {
83 | $this->assertEquals('"TableName"', $this->database->tableQuote("TableName"));
84 | $this->assertEquals('"_table"', $this->database->tableQuote("_table"));
85 |
86 | $this->assertEquals('"アカウント"', $this->database->tableQuote("アカウント"));
87 | }
88 |
89 | /**
90 | * @covers ::tableQuote()
91 | */
92 | public function testPrefixTableQuote()
93 | {
94 | $database = new Medoo([
95 | 'testMode' => true,
96 | 'prefix' => 'PREFIX_'
97 | ]);
98 |
99 | $this->assertEquals('"PREFIX_TableName"', $database->tableQuote("TableName"));
100 | }
101 |
102 | public function tableNamesProvider(): array
103 | {
104 | return [
105 | ["9TableName"],
106 | ["@TableName"],
107 | [".TableName"],
108 | ["TableName."],
109 | ["Table.name"]
110 | ];
111 | }
112 |
113 | /**
114 | * @covers ::tableQuote()
115 | * @dataProvider tableNamesProvider
116 | */
117 | public function testIncorrectTableQuote($table)
118 | {
119 | $this->expectException(InvalidArgumentException::class);
120 |
121 | $this->database->tableQuote($table);
122 | }
123 | }
124 |
--------------------------------------------------------------------------------
/tests/RandTest.php:
--------------------------------------------------------------------------------
1 | setType($type);
17 |
18 | $this->database->rand("account", [
19 | "user_name"
20 | ]);
21 |
22 | $this->assertQuery([
23 | 'default' => << << <<database->queryString);
39 | }
40 |
41 | /**
42 | * @covers ::rand()
43 | * @dataProvider typesProvider
44 | */
45 | public function testWhereRand($type)
46 | {
47 | $this->setType($type);
48 |
49 | $this->database->rand("account", [
50 | "user_name"
51 | ], [
52 | "location" => "Tokyo"
53 | ]);
54 |
55 | $this->assertQuery([
56 | 'default' => << << <<database->queryString);
75 | }
76 |
77 | /**
78 | * @covers ::rand()
79 | * @dataProvider typesProvider
80 | */
81 | public function testWhereWithJoinRand($type)
82 | {
83 | $this->setType($type);
84 |
85 | $this->database->rand("account", [
86 | "[>]album" => "user_id"
87 | ], [
88 | "account.user_name"
89 | ], [
90 | "album.location" => "Tokyo"
91 | ]);
92 |
93 | $this->assertQuery([
94 | 'default' => << << <<database->queryString);
116 | }
117 |
118 | /**
119 | * @covers ::rand()
120 | * @dataProvider typesProvider
121 | */
122 | public function testWithJoinRand($type)
123 | {
124 | $this->setType($type);
125 |
126 | $this->database->rand("account", [
127 | "[>]album" => "user_id"
128 | ], [
129 | "account.user_name"
130 | ]);
131 |
132 | $this->assertQuery([
133 | 'default' => << << <<database->queryString);
152 | }
153 | }
154 |
--------------------------------------------------------------------------------
/tests/RawTest.php:
--------------------------------------------------------------------------------
1 | setType($type);
21 |
22 | $this->database->select('account', [
23 | 'score' => Medoo::raw('SUM( + )')
24 | ]);
25 |
26 | $this->assertQuery(
27 | <<database->queryString
32 | );
33 | }
34 |
35 | /**
36 | * @covers ::raw()
37 | * @covers ::isRaw()
38 | * @covers ::buildRaw()
39 | * @dataProvider typesProvider
40 | */
41 | public function testRawWithSamePlaceholderName($type)
42 | {
43 | $this->setType($type);
44 |
45 | $this->database->select('account', [
46 | 'system' => Medoo::raw("COUNT( = 'window' OR = 'mac')")
47 | ]);
48 |
49 | $this->assertQuery(
50 | <<database->queryString
55 | );
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/tests/ReplaceTest.php:
--------------------------------------------------------------------------------
1 | setType($type);
19 |
20 | $this->database->replace("account", [
21 | "type" => [
22 | "user" => "new_user",
23 | "business" => "new_business"
24 | ],
25 | "column" => [
26 | "old_value" => "new_value"
27 | ]
28 | ], [
29 | "user_id[>]" => 1000
30 | ]);
31 |
32 | $this->assertQuery(
33 | << 1000
39 | EOD,
40 | $this->database->queryString
41 | );
42 | }
43 |
44 | /**
45 | * @covers ::replace()
46 | */
47 | public function testReplaceEmptyColumns()
48 | {
49 | $this->expectException(InvalidArgumentException::class);
50 |
51 | $this->database->replace("account", [], [
52 | "user_id[>]" => 1000
53 | ]);
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/tests/SelectTest.php:
--------------------------------------------------------------------------------
1 | setType($type);
24 |
25 | $this->database->select("account", "*");
26 |
27 | $this->assertQuery(
28 | <<database->queryString
32 | );
33 | }
34 |
35 | /**
36 | * @covers ::select()
37 | * @covers ::selectContext()
38 | * @dataProvider typesProvider
39 | */
40 | public function testSelectTableWithAlias($type)
41 | {
42 | $this->setType($type);
43 |
44 | $this->database->select("account (user)", "name");
45 |
46 | $this->assertQuery(
47 | <<database->queryString
52 | );
53 | }
54 |
55 | /**
56 | * @covers ::columnMap()
57 | * @covers ::columnPush()
58 | * @dataProvider typesProvider
59 | */
60 | public function testSelectSingleColumn($type)
61 | {
62 | $this->setType($type);
63 |
64 | $this->database->select("account", "name");
65 |
66 | $this->assertQuery(
67 | <<database->queryString
72 | );
73 | }
74 |
75 | /**
76 | * @covers ::columnMap()
77 | * @covers ::columnPush()
78 | * @dataProvider typesProvider
79 | */
80 | public function testSelectColumns($type)
81 | {
82 | $this->setType($type);
83 |
84 | $this->database->select("account", ["name", "id"]);
85 |
86 | $this->assertQuery(
87 | <<database->queryString
92 | );
93 | }
94 |
95 | /**
96 | * @covers ::columnMap()
97 | * @covers ::columnPush()
98 | * @dataProvider typesProvider
99 | */
100 | public function testSelectColumnsWithAlias($type)
101 | {
102 | $this->setType($type);
103 |
104 | $this->database->select("account", ["name(nickname)", "id"]);
105 |
106 | $this->assertQuery(
107 | <<database->queryString
112 | );
113 | }
114 |
115 | /**
116 | * @covers ::columnMap()
117 | * @covers ::columnPush()
118 | * @dataProvider typesProvider
119 | */
120 | public function testSelectColumnsWithType($type)
121 | {
122 | $this->setType($type);
123 |
124 | $this->database->select("account", ["name[String]", "data [JSON]"]);
125 |
126 | $this->assertQuery(
127 | <<database->queryString
132 | );
133 | }
134 |
135 | /**
136 | * @covers ::columnMap()
137 | * @covers ::columnPush()
138 | * @dataProvider typesProvider
139 | */
140 | public function testSelectColumnsWithAliasAndType($type)
141 | {
142 | $this->setType($type);
143 |
144 | $this->database->select("account", ["name (nickname) [String]", "data [JSON]"]);
145 |
146 | $this->assertQuery(
147 | <<database->queryString
152 | );
153 | }
154 |
155 | /**
156 | * @covers ::columnMap()
157 | * @covers ::columnPush()
158 | * @dataProvider typesProvider
159 | */
160 | public function testSelectColumnsWithRaw($type)
161 | {
162 | $this->setType($type);
163 |
164 | $this->database->select("account", [
165 | "id [String]" => Medoo::raw("UUID()")
166 | ]);
167 |
168 | $this->assertQuery(
169 | <<database->queryString
174 | );
175 | }
176 |
177 | /**
178 | * @covers ::select()
179 | * @covers ::selectContext()
180 | * @covers ::isJoin()
181 | * @dataProvider typesProvider
182 | */
183 | public function testSelectWithWhere($type)
184 | {
185 | $this->setType($type);
186 |
187 | $this->database->select("account", [
188 | "name",
189 | "id"
190 | ], [
191 | "ORDER" => "age"
192 | ]);
193 |
194 | $this->assertQuery(
195 | <<database->queryString
201 | );
202 | }
203 |
204 | /**
205 | * @covers ::select()
206 | * @covers ::selectContext()
207 | * @covers ::isJoin()
208 | * @covers ::buildJoin()
209 | * @dataProvider typesProvider
210 | */
211 | public function testSelectWithLeftJoin($type)
212 | {
213 | $this->setType($type);
214 |
215 | $this->database->select("account", [
216 | "[>]post" => "user_id"
217 | ], [
218 | "account.name",
219 | "post.title"
220 | ]);
221 |
222 | $this->assertQuery(
223 | <<database->queryString
230 | );
231 | }
232 |
233 | /**
234 | * @covers ::isJoin()
235 | * @covers ::buildJoin()
236 | * @dataProvider typesProvider
237 | */
238 | public function testSelectWithRightJoin($type)
239 | {
240 | $this->setType($type);
241 |
242 | $this->database->select("account", [
243 | "[<]post" => "user_id"
244 | ], [
245 | "account.name",
246 | "post.title"
247 | ]);
248 |
249 | $this->assertQuery(
250 | <<database->queryString
257 | );
258 | }
259 |
260 | /**
261 | * @covers ::isJoin()
262 | * @covers ::buildJoin()
263 | * @dataProvider typesProvider
264 | */
265 | public function testSelectWithFullJoin($type)
266 | {
267 | $this->setType($type);
268 |
269 | $this->database->select("account", [
270 | "[<>]post" => "user_id"
271 | ], [
272 | "account.name",
273 | "post.title"
274 | ]);
275 |
276 | $this->assertQuery(
277 | <<database->queryString
284 | );
285 | }
286 |
287 | /**
288 | * @covers ::isJoin()
289 | * @covers ::buildJoin()
290 | * @dataProvider typesProvider
291 | */
292 | public function testSelectWithInnerJoin($type)
293 | {
294 | $this->setType($type);
295 |
296 | $this->database->select("account", [
297 | "[><]post" => "user_id"
298 | ], [
299 | "account.name",
300 | "post.title"
301 | ]);
302 |
303 | $this->assertQuery(
304 | <<database->queryString
311 | );
312 | }
313 |
314 | /**
315 | * @covers ::isJoin()
316 | * @covers ::buildJoin()
317 | * @dataProvider typesProvider
318 | */
319 | public function testSelectWithSameKeysJoin($type)
320 | {
321 | $this->setType($type);
322 |
323 | $this->database->select("account", [
324 | "[>]photo" => ["user_id", "avatar_id"],
325 | ], [
326 | "account.name",
327 | "photo.link"
328 | ]);
329 |
330 | $this->assertQuery(
331 | <<database->queryString
338 | );
339 | }
340 |
341 | /**
342 | * @covers ::isJoin()
343 | * @covers ::buildJoin()
344 | * @dataProvider typesProvider
345 | */
346 | public function testSelectWithKeyJoin($type)
347 | {
348 | $this->setType($type);
349 |
350 | $this->database->select("account", [
351 | "[>]post" => ["user_id" => "author_id"],
352 | ], [
353 | "account.name",
354 | "post.title"
355 | ]);
356 |
357 | $this->assertQuery(
358 | <<database->queryString
365 | );
366 | }
367 |
368 | /**
369 | * @covers ::isJoin()
370 | * @covers ::buildJoin()
371 | * @dataProvider typesProvider
372 | */
373 | public function testSelectWithAliasJoin($type)
374 | {
375 | $this->setType($type);
376 |
377 | $this->database->select("account", [
378 | "[>]post (main_post)" => ["user_id" => "author_id"],
379 | ], [
380 | "account.name",
381 | "main_post.title"
382 | ]);
383 |
384 | $this->assertQuery(
385 | <<database->queryString
392 | );
393 | }
394 |
395 | /**
396 | * @covers ::isJoin()
397 | * @covers ::buildJoin()
398 | * @dataProvider typesProvider
399 | */
400 | public function testSelectWithReferJoin($type)
401 | {
402 | $this->setType($type);
403 |
404 | $this->database->select("account", [
405 | "[>]post" => ["user_id" => "author_id"],
406 | "[>]album" => ["post.author_id" => "user_id"],
407 | ], [
408 | "account.name",
409 | "post.title",
410 | "album.link"
411 | ]);
412 |
413 | $this->assertQuery(
414 | <<database->queryString
423 | );
424 | }
425 |
426 | /**
427 | * @covers ::isJoin()
428 | * @covers ::buildJoin()
429 | * @dataProvider typesProvider
430 | */
431 | public function testSelectWithMultipleConditionJoin($type)
432 | {
433 | $this->setType($type);
434 |
435 | $this->database->select("account", [
436 | "[>]album" => ["author_id" => "user_id"],
437 | "[>]post" => [
438 | "user_id" => "author_id",
439 | "album.user_id" => "owner_id"
440 | ]
441 | ], [
442 | "account.name",
443 | "post.title",
444 | "album.link"
445 | ]);
446 |
447 | $this->assertQuery(
448 | <<database->queryString
458 | );
459 | }
460 |
461 | /**
462 | * @covers ::isJoin()
463 | * @covers ::buildJoin()
464 | * @dataProvider typesProvider
465 | */
466 | public function testSelectWithAdditionalConditionJoin($type)
467 | {
468 | $this->setType($type);
469 |
470 | $this->database->select("account", [
471 | "[>]post" => [
472 | "user_id" => "author_id",
473 | "AND" => [
474 | "post.id[>]" => 10
475 | ]
476 | ]
477 | ], [
478 | "account.name",
479 | "post.title"
480 | ]);
481 |
482 | $this->assertQuery(
483 | << 10
489 | EOD,
490 | $this->database->queryString
491 | );
492 | }
493 |
494 | /**
495 | * @covers ::isJoin()
496 | * @covers ::buildJoin()
497 | * @dataProvider typesProvider
498 | */
499 | public function testSelectRawJoin($type)
500 | {
501 | $this->setType($type);
502 |
503 | $this->database->select("account", [
504 | "[>]post" => Medoo::raw("ON = ")
505 | ], [
506 | "account.name",
507 | "post.title"
508 | ]);
509 |
510 | $this->assertQuery(
511 | <<database->queryString
518 | );
519 | }
520 |
521 | /**
522 | * @covers ::columnMap()
523 | * @covers ::columnPush()
524 | * @dataProvider typesProvider
525 | */
526 | public function testSelectAllWithJoin($type)
527 | {
528 | $this->setType($type);
529 |
530 | $this->expectException(InvalidArgumentException::class);
531 |
532 | $this->database->select("account", [
533 | "[>]post" => "user_id"
534 | ], [
535 | "account.*"
536 | ]);
537 | }
538 |
539 | /**
540 | * @covers ::columnMap()
541 | * @covers ::columnPush()
542 | * @dataProvider typesProvider
543 | */
544 | public function testSelectWithDataMapping($type)
545 | {
546 | $this->setType($type);
547 |
548 | $this->database->select("post", [
549 | "[>]account" => ["user_id"]
550 | ], [
551 | "post.content",
552 |
553 | "userData" => [
554 | "account.user_id",
555 | "account.email",
556 |
557 | "meta" => [
558 | "account.location",
559 | "account.gender"
560 | ]
561 | ]
562 | ]);
563 |
564 | $this->assertQuery(
565 | <<database->queryString
572 | );
573 | }
574 |
575 | /**
576 | * @covers ::columnMap()
577 | * @covers ::columnPush()
578 | * @dataProvider typesProvider
579 | */
580 | public function testSelectWithIndexMapping($type)
581 | {
582 | $this->setType($type);
583 |
584 | $this->database->select("account", [
585 | "user_id" => [
586 | "name (nickname)",
587 | "location"
588 | ]
589 | ]);
590 |
591 | $this->assertQuery(
592 | <<database->queryString
597 | );
598 | }
599 |
600 | /**
601 | * @covers ::columnMap()
602 | * @covers ::columnPush()
603 | * @dataProvider typesProvider
604 | */
605 | public function testSelectWithDistinct($type)
606 | {
607 | $this->setType($type);
608 |
609 | $this->database->select("account", [
610 | "@location",
611 | "nickname"
612 | ]);
613 |
614 | $this->assertQuery(
615 | <<database->queryString
620 | );
621 | }
622 |
623 | /**
624 | * @covers ::columnMap()
625 | * @covers ::columnPush()
626 | * @dataProvider typesProvider
627 | */
628 | public function testSelectWithDistinctDiffOrder($type)
629 | {
630 | $this->setType($type);
631 |
632 | $this->database->select("account", [
633 | "location",
634 | "@nickname"
635 | ]);
636 |
637 | $this->assertQuery(
638 | <<database->queryString
643 | );
644 | }
645 |
646 | /**
647 | * @covers ::columnMap()
648 | * @covers ::columnPush()
649 | * @dataProvider typesProvider
650 | */
651 | public function testSelectWithUnicodeCharacter($type)
652 | {
653 | $this->setType($type);
654 |
655 | $this->database->select("considérer", [
656 | "name (名前)",
657 | "положение (ロケーション)"
658 | ]);
659 |
660 | $this->assertQuery(
661 | <<database->queryString
666 | );
667 | }
668 |
669 | /**
670 | * @covers ::columnMap()
671 | * @covers ::columnPush()
672 | * @dataProvider typesProvider
673 | */
674 | public function testSelectWithHyphenCharacter($type)
675 | {
676 | $this->setType($type);
677 |
678 | $this->database->select("account", [
679 | "nick-name"
680 | ]);
681 |
682 | $this->assertQuery(
683 | <<database->queryString
688 | );
689 | }
690 |
691 | /**
692 | * @covers ::columnMap()
693 | * @covers ::columnPush()
694 | * @dataProvider typesProvider
695 | */
696 | public function testSelectWithSingleCharacter($type)
697 | {
698 | $this->setType($type);
699 |
700 | $this->database->select("a", [
701 | "[>]e" => ["f"]
702 | ], [
703 | "b (c)"
704 | ]);
705 |
706 | $this->assertQuery(
707 | <<database->queryString
713 | );
714 | }
715 | }
716 |
--------------------------------------------------------------------------------
/tests/UpdateTest.php:
--------------------------------------------------------------------------------
1 | setType($type);
19 |
20 | $objectData = new Foo();
21 |
22 | $this->database->update("account", [
23 | "type" => "user",
24 | "money" => 23.2,
25 | "age[+]" => 1,
26 | "level[-]" => 5,
27 | "score[*]" => 2,
28 | "lang" => ["en", "fr"],
29 | "lang [JSON]" => ["en", "fr"],
30 | "is_locked" => true,
31 | "uuid" => Medoo::raw("UUID()"),
32 | "object" => $objectData
33 | ], [
34 | "user_id[<]" => 1000
35 | ]);
36 |
37 | $this->assertQuery([
38 | 'default' => << <<database->queryString);
67 | }
68 |
69 | public function testOracleLOBsUpdate()
70 | {
71 | $this->setType("oracle");
72 |
73 | $fp = fopen('README.md', 'r');
74 |
75 | $this->database->update("ACCOUNT", [
76 | "DATA" => $fp
77 | ], [
78 | "ID" => 1
79 | ]);
80 |
81 | $this->assertQuery(
82 | <<database->queryString
89 | );
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/tests/WhereTest.php:
--------------------------------------------------------------------------------
1 | setType($type);
22 |
23 | $this->database->select("account", "user_name", [
24 | "email" => "foo@bar.com",
25 | "user_id" => 200,
26 | "user_id[>]" => 200,
27 | "user_id[>=]" => 200,
28 | "user_id[!]" => 200,
29 | "age[<>]" => [200, 500],
30 | "age[><]" => [200, 500],
31 | "income[>]" => Medoo::raw("COUNT()"),
32 | "remote_id" => Medoo::raw("UUID()"),
33 | "location" => null,
34 | "is_selected" => true
35 | ]);
36 |
37 | $this->assertQuery(
38 | << 200 AND
45 | "user_id" >= 200 AND
46 | "user_id" != 200 AND
47 | ("age" BETWEEN 200 AND 500) AND
48 | ("age" NOT BETWEEN 200 AND 500) AND
49 | "income" > COUNT("average") AND
50 | "remote_id" = UUID() AND
51 | "location" IS NULL AND
52 | "is_selected" = 1
53 | EOD,
54 | $this->database->queryString
55 | );
56 | }
57 |
58 | /**
59 | * @covers ::select()
60 | * @covers ::dataImplode()
61 | * @covers ::whereClause()
62 | * @dataProvider typesProvider
63 | */
64 | public function testBetweenDateTimeWhere($type)
65 | {
66 | $this->setType($type);
67 |
68 | $this->database->select("account", "user_name", [
69 | "birthday[<>]" => [date("Y-m-d", mktime(0, 0, 0, 1, 1, 2015)), date("Y-m-d", mktime(0, 0, 0, 1, 1, 2045))]
70 | ]);
71 |
72 | $this->assertQuery(
73 | <<database->queryString
80 | );
81 | }
82 |
83 | /**
84 | * @covers ::select()
85 | * @covers ::dataImplode()
86 | * @covers ::whereClause()
87 | * @dataProvider typesProvider
88 | */
89 | public function testNotBetweenDateTimeWhere($type)
90 | {
91 | $this->setType($type);
92 |
93 | $this->database->select("account", "user_name", [
94 | "birthday[><]" => [date("Y-m-d", mktime(0, 0, 0, 1, 1, 2015)), date("Y-m-d", mktime(0, 0, 0, 1, 1, 2045))]
95 | ]);
96 |
97 | $this->assertQuery(
98 | <<database->queryString
105 | );
106 | }
107 |
108 | /**
109 | * @covers ::select()
110 | * @covers ::dataImplode()
111 | * @covers ::whereClause()
112 | * @dataProvider typesProvider
113 | */
114 | public function testBetweenStringWhere($type)
115 | {
116 | $this->setType($type);
117 |
118 | $this->database->select("account", "user_name", [
119 | "location[<>]" => ['New York', 'Santo']
120 | ]);
121 |
122 | $this->assertQuery(
123 | <<database->queryString
130 | );
131 | }
132 |
133 | /**
134 | * @covers ::select()
135 | * @covers ::dataImplode()
136 | * @covers ::whereClause()
137 | * @dataProvider typesProvider
138 | */
139 | public function testBetweenRawWhere($type)
140 | {
141 | $this->setType($type);
142 |
143 | $this->database->select("account", "user_name", [
144 | "birthday[<>]" => [
145 | Medoo::raw("to_date(:from, 'YYYY-MM-DD')", [":from" => '2015/05/15']),
146 | Medoo::raw("to_date(:to, 'YYYY-MM-DD')", [":to" => '2025/05/15'])
147 | ]
148 | ]);
149 |
150 | $this->assertQuery(
151 | <<database->queryString
158 | );
159 | }
160 |
161 | /**
162 | * @covers ::select()
163 | * @covers ::dataImplode()
164 | * @covers ::whereClause()
165 | * @dataProvider typesProvider
166 | */
167 | public function testGreaterDateTimeWhere($type)
168 | {
169 | $this->setType($type);
170 |
171 | $this->database->select("account", "user_name", [
172 | "birthday[>]" => date("Y-m-d", mktime(0, 0, 0, 1, 1, 2045))
173 | ]);
174 |
175 | $this->assertQuery(
176 | << '2045-01-01'
180 | EOD,
181 | $this->database->queryString
182 | );
183 | }
184 |
185 | /**
186 | * @covers ::select()
187 | * @covers ::dataImplode()
188 | * @covers ::whereClause()
189 | * @dataProvider typesProvider
190 | */
191 | public function testArrayIntValuesWhere($type)
192 | {
193 | $this->setType($type);
194 |
195 | $this->database->select("account", "user_name", [
196 | "user_id" => [2, 123, 234, 54]
197 | ]);
198 |
199 | $this->assertQuery(
200 | <<database->queryString
207 | );
208 | }
209 |
210 | /**
211 | * @covers ::select()
212 | * @covers ::dataImplode()
213 | * @covers ::whereClause()
214 | * @dataProvider typesProvider
215 | */
216 | public function testArrayStringValuesWhere($type)
217 | {
218 | $this->setType($type);
219 |
220 | $this->database->select("account", "user_name", [
221 | "email" => ["foo@bar.com", "cat@dog.com", "admin@medoo.in"]
222 | ]);
223 |
224 | $this->assertQuery(
225 | <<database->queryString
232 | );
233 | }
234 |
235 | /**
236 | * @covers ::select()
237 | * @covers ::dataImplode()
238 | * @covers ::whereClause()
239 | * @dataProvider typesProvider
240 | */
241 | public function testRawArrayValuesWhere($type)
242 | {
243 | $this->setType($type);
244 |
245 | $this->database->select("account", "user_name", [
246 | 'id' => [
247 | Medoo::raw('LOWER("FOO")'),
248 | Medoo::raw('LOWER("BAR")')
249 | ]
250 | ]);
251 |
252 | $this->assertQuery(
253 | <<database->queryString
260 | );
261 | }
262 |
263 | /**
264 | * @covers ::select()
265 | * @covers ::dataImplode()
266 | * @covers ::whereClause()
267 | * @dataProvider typesProvider
268 | */
269 | public function testRawNotInArrayValuesWhere($type)
270 | {
271 | $this->setType($type);
272 |
273 | $this->database->select("account", "user_name", [
274 | 'id[!]' => [
275 | Medoo::raw('LOWER("FOO")'),
276 | Medoo::raw('LOWER("BAR")')
277 | ]
278 | ]);
279 |
280 | $this->assertQuery(
281 | <<database->queryString
288 | );
289 | }
290 |
291 | /**
292 | * @covers ::select()
293 | * @covers ::dataImplode()
294 | * @covers ::whereClause()
295 | * @dataProvider typesProvider
296 | */
297 | public function testNegativeWhere($type)
298 | {
299 | $this->setType($type);
300 |
301 | $this->database->select("account", "user_name", [
302 | "AND" => [
303 | "user_name[!]" => "foo",
304 | "user_id[!]" => 1024,
305 | "email[!]" => ["foo@bar.com", "admin@medoo.in"],
306 | "city[!]" => null,
307 | "promoted[!]" => true,
308 | "location[!]" => Medoo::raw('LOWER("New York")')
309 | ]
310 | ]);
311 |
312 | $this->assertQuery(
313 | <<database->queryString
325 | );
326 | }
327 |
328 | /**
329 | * @covers ::select()
330 | * @covers ::dataImplode()
331 | * @covers ::whereClause()
332 | * @dataProvider typesProvider
333 | */
334 | public function testBasicAndRelativityWhere($type)
335 | {
336 | $this->setType($type);
337 |
338 | $this->database->select("account", "user_name", [
339 | "AND" => [
340 | "user_id[>]" => 200,
341 | "gender" => "female"
342 | ]
343 | ]);
344 |
345 | $this->assertQuery(
346 | << 200 AND "gender" = 'female')
351 | EOD,
352 | $this->database->queryString
353 | );
354 | }
355 |
356 | /**
357 | * @covers ::select()
358 | * @covers ::dataImplode()
359 | * @covers ::whereClause()
360 | * @dataProvider typesProvider
361 | */
362 | public function testBasicSingleRelativityWhere($type)
363 | {
364 | $this->setType($type);
365 |
366 | $this->database->select("account", "user_name", [
367 | "user_id[>]" => 200,
368 | "gender" => "female"
369 | ]);
370 |
371 | $this->assertQuery(
372 | << 200 AND "gender" = 'female'
377 | EOD,
378 | $this->database->queryString
379 | );
380 | }
381 |
382 | /**
383 | * @covers ::select()
384 | * @covers ::dataImplode()
385 | * @covers ::whereClause()
386 | * @dataProvider typesProvider
387 | */
388 | public function testBasicOrRelativityWhere($type)
389 | {
390 | $this->setType($type);
391 |
392 | $this->database->select("account", "user_name", [
393 | "OR" => [
394 | "user_id[>]" => 200,
395 | "age[<>]" => [18, 25],
396 | "gender" => "female"
397 | ]
398 | ]);
399 |
400 | $this->assertQuery(
401 | << 200 OR
406 | ("age" BETWEEN 18 AND 25) OR
407 | "gender" = 'female')
408 | EOD,
409 | $this->database->queryString
410 | );
411 | }
412 |
413 | /**
414 | * @covers ::select()
415 | * @covers ::dataImplode()
416 | * @covers ::whereClause()
417 | * @dataProvider typesProvider
418 | */
419 | public function testCompoundRelativityWhere($type)
420 | {
421 | $this->setType($type);
422 |
423 | $this->database->select("account", "user_name", [
424 | "AND" => [
425 | "OR" => [
426 | "user_name" => "foo",
427 | "email" => "foo@bar.com"
428 | ],
429 | "password" => "12345"
430 | ]
431 | ]);
432 |
433 | $this->assertQuery(
434 | <<database->queryString
441 | );
442 | }
443 |
444 | /**
445 | * @covers ::select()
446 | * @covers ::dataImplode()
447 | * @covers ::whereClause()
448 | * @dataProvider typesProvider
449 | */
450 | public function testCompoundDuplicatedKeysWhere($type)
451 | {
452 | $this->setType($type);
453 |
454 | $this->database->select("account", "user_name", [
455 | "AND #comment" => [
456 | "OR #first comment" => [
457 | "user_name" => "foo",
458 | "email" => "foo@bar.com"
459 | ],
460 | "OR #sencond comment" => [
461 | "user_name" => "bar",
462 | "email" => "bar@foo.com"
463 | ]
464 | ]
465 | ]);
466 |
467 | $this->assertQuery(
468 | <<database->queryString
476 | );
477 | }
478 |
479 | /**
480 | * @covers ::select()
481 | * @covers ::dataImplode()
482 | * @covers ::whereClause()
483 | * @dataProvider typesProvider
484 | */
485 | public function testColumnsRelationshipWhere($type)
486 | {
487 | $this->setType($type);
488 |
489 | $this->database->select("post", [
490 | "[>]account" => "user_id",
491 | ], [
492 | "post.content"
493 | ], [
494 | "post.restrict[<]account.age",
495 | "post.type[=]account.type"
496 | ]);
497 |
498 | $this->assertQuery(
499 | <<database->queryString
509 | );
510 | }
511 |
512 | /**
513 | * @covers ::select()
514 | * @covers ::dataImplode()
515 | * @covers ::whereClause()
516 | * @dataProvider typesProvider
517 | */
518 | public function testBasicLikeWhere($type)
519 | {
520 | $this->setType($type);
521 |
522 | $this->database->select("account", "user_name", [
523 | "city[~]" => "lon",
524 | "name[~]" => "some-name"
525 | ]);
526 |
527 | $this->assertQuery(
528 | <<database->queryString
536 | );
537 | }
538 |
539 | /**
540 | * @covers ::select()
541 | * @covers ::dataImplode()
542 | * @covers ::whereClause()
543 | * @dataProvider typesProvider
544 | */
545 | public function testGroupedLikeWhere($type)
546 | {
547 | $this->setType($type);
548 |
549 | $this->database->select("account", "user_name", [
550 | "city[~]" => ["lon", "foo", "bar"]
551 | ]);
552 |
553 | $this->assertQuery(
554 | <<database->queryString
563 | );
564 | }
565 |
566 | /**
567 | * @covers ::select()
568 | * @covers ::dataImplode()
569 | * @covers ::whereClause()
570 | * @dataProvider typesProvider
571 | */
572 | public function testNegativeLikeWhere($type)
573 | {
574 | $this->setType($type);
575 |
576 | $this->database->select("account", "user_name", [
577 | "city[!~]" => "lon"
578 | ]);
579 |
580 | $this->assertQuery(
581 | <<database->queryString
588 | );
589 | }
590 |
591 | /**
592 | * @covers ::select()
593 | * @covers ::dataImplode()
594 | * @covers ::whereClause()
595 | * @dataProvider typesProvider
596 | */
597 | public function testNonEscapeLikeWhere($type)
598 | {
599 | $this->setType($type);
600 |
601 | $this->database->select("account", "user_name", [
602 | "city[~]" => "some_where",
603 | "county[~]" => "[a-f]stan"
604 | ]);
605 |
606 | $this->assertQuery(
607 | <<database->queryString
615 | );
616 | }
617 |
618 | /**
619 | * @covers ::select()
620 | * @covers ::dataImplode()
621 | * @covers ::whereClause()
622 | * @dataProvider typesProvider
623 | */
624 | public function testEscapeLikeWhere($type)
625 | {
626 | $this->setType($type);
627 |
628 | $this->database->select("account", "user_name", [
629 | "city[~]" => "some\_where"
630 | ]);
631 |
632 | $this->assertQuery(
633 | <<database->queryString
640 | );
641 | }
642 |
643 | /**
644 | * @covers ::select()
645 | * @covers ::dataImplode()
646 | * @covers ::whereClause()
647 | * @dataProvider typesProvider
648 | */
649 | public function testCompoundLikeWhere($type)
650 | {
651 | $this->setType($type);
652 |
653 | $this->database->select("account", "user_name", [
654 | "content[~]" => ["AND" => ["lon", "on"]],
655 | "city[~]" => ["OR" => ["lon", "on"]]
656 | ]);
657 |
658 | $this->assertQuery(
659 | <<database->queryString
667 | );
668 | }
669 |
670 | /**
671 | * @covers ::select()
672 | * @covers ::dataImplode()
673 | * @covers ::whereClause()
674 | * @dataProvider typesProvider
675 | */
676 | public function testWildcardLikeWhere($type)
677 | {
678 | $this->setType($type);
679 |
680 | $this->database->select("account", "user_name", [
681 | "city[~]" => "%stan",
682 | "company[~]" => "Goo%",
683 | "location[~]" => "Londo_",
684 | "name[~]" => "[BCR]at",
685 | "nickname[~]" => "[!BCR]at"
686 | ]);
687 |
688 | $this->assertQuery(
689 | <<database->queryString
700 | );
701 | }
702 |
703 | /**
704 | * @covers ::select()
705 | * @covers ::dataImplode()
706 | * @covers ::whereClause()
707 | * @dataProvider typesProvider
708 | */
709 | public function testMultipleLikeWhere($type)
710 | {
711 | $this->setType($type);
712 |
713 | $words = [
714 | "one",
715 | "two",
716 | "three",
717 | "four",
718 | "five",
719 | "six",
720 | "seven",
721 | "eight",
722 | "nine",
723 | "ten",
724 | "eleven",
725 | "twelve"
726 | ];
727 |
728 | $this->database->select("account", ["title"], ["title[~]" => $words]);
729 |
730 | $this->assertQuery(
731 | <<database->queryString
738 | );
739 | }
740 |
741 | /**
742 | * @covers ::select()
743 | * @covers ::dataImplode()
744 | * @covers ::whereClause()
745 | * @dataProvider typesProvider
746 | */
747 | public function testBasicOrderWhere($type)
748 | {
749 | $this->setType($type);
750 |
751 | $this->database->select("account", "user_name", [
752 | "ORDER" => "user_id"
753 | ]);
754 |
755 | $this->assertQuery(
756 | <<database->queryString
762 | );
763 | }
764 |
765 | /**
766 | * @covers ::select()
767 | * @covers ::dataImplode()
768 | * @covers ::whereClause()
769 | * @dataProvider typesProvider
770 | */
771 | public function testMultipleOrderWhere($type)
772 | {
773 | $this->setType($type);
774 |
775 | $this->database->select("account", "user_name", [
776 | "ORDER" => [
777 | // Order by column with sorting by customized order.
778 | "user_id" => [43, 12, 57, 98, 144, 1],
779 |
780 | // Order by column.
781 | "register_date",
782 |
783 | // Order by column with descending sorting.
784 | "profile_id" => "DESC",
785 |
786 | // Order by column with ascending sorting.
787 | "date" => "ASC"
788 | ]
789 | ]);
790 |
791 | $this->assertQuery(
792 | <<database->queryString
798 | );
799 | }
800 |
801 | /**
802 | * @covers ::select()
803 | * @covers ::dataImplode()
804 | * @covers ::whereClause()
805 | * @dataProvider typesProvider
806 | */
807 | public function testOrderWithRawWhere($type)
808 | {
809 | $this->setType($type);
810 |
811 | $this->database->select("account", "user_name", [
812 | "ORDER" => Medoo::raw(", ")
813 | ]);
814 |
815 | $this->assertQuery(
816 | <<database->queryString
822 | );
823 | }
824 |
825 | /**
826 | * @covers ::select()
827 | * @covers ::dataImplode()
828 | * @covers ::whereClause()
829 | */
830 | public function testFullTextSearchWhere()
831 | {
832 | $this->setType("mysql");
833 |
834 | $this->database->select("account", "user_name", [
835 | "MATCH" => [
836 | "columns" => ["content", "title"],
837 | "keyword" => "foo",
838 | "mode" => "natural"
839 | ]
840 | ]);
841 |
842 | $this->assertQuery(
843 | <<database->queryString
849 | );
850 | }
851 |
852 | /**
853 | * @covers ::select()
854 | * @covers ::dataImplode()
855 | * @covers ::whereClause()
856 | * @dataProvider typesProvider
857 | */
858 | public function testRegularExpressionWhere($type)
859 | {
860 | $this->setType($type);
861 |
862 | $this->database->select("account", "user_name", [
863 | 'user_name[REGEXP]' => '[a-z0-9]*'
864 | ]);
865 |
866 | $this->assertQuery(
867 | <<database->queryString
873 | );
874 | }
875 |
876 | /**
877 | * @covers ::select()
878 | * @covers ::dataImplode()
879 | * @covers ::whereClause()
880 | * @dataProvider typesProvider
881 | */
882 | public function testRawWhere($type)
883 | {
884 | $this->setType($type);
885 |
886 | $this->database->select("account", "user_name", [
887 | 'datetime' => Medoo::raw('NOW()')
888 | ]);
889 |
890 | $this->assertQuery(
891 | <<database->queryString
897 | );
898 | }
899 |
900 | /**
901 | * @covers ::select()
902 | * @covers ::dataImplode()
903 | * @covers ::whereClause()
904 | * @dataProvider typesProvider
905 | */
906 | public function testLimitWhere($type)
907 | {
908 | $this->setType($type);
909 |
910 | $this->database->select("account", "user_name", [
911 | 'LIMIT' => 100
912 | ]);
913 |
914 | $this->assertQuery([
915 | 'default' => << << <<database->queryString);
932 | }
933 |
934 | /**
935 | * @covers ::select()
936 | * @covers ::dataImplode()
937 | * @covers ::whereClause()
938 | * @dataProvider typesProvider
939 | */
940 | public function testLimitOffsetWhere($type)
941 | {
942 | $this->setType($type);
943 |
944 | $this->database->select("account", "user_name", [
945 | 'LIMIT' => [20, 100]
946 | ]);
947 |
948 | $this->assertQuery([
949 | 'default' => << << <<database->queryString);
966 | }
967 |
968 | /**
969 | * @covers ::select()
970 | * @covers ::dataImplode()
971 | * @covers ::whereClause()
972 | * @dataProvider typesProvider
973 | */
974 | public function testGroupWhere($type)
975 | {
976 | $this->setType($type);
977 |
978 | $this->database->select("account", "user_name", [
979 | 'GROUP' => 'type',
980 | ]);
981 |
982 | $this->assertQuery(
983 | <<database->queryString
989 | );
990 | }
991 |
992 | /**
993 | * @covers ::select()
994 | * @covers ::dataImplode()
995 | * @covers ::whereClause()
996 | * @dataProvider typesProvider
997 | */
998 | public function testGroupWithArrayWhere($type)
999 | {
1000 | $this->setType($type);
1001 |
1002 | $this->database->select("account", "user_name", [
1003 | 'GROUP' => [
1004 | 'type',
1005 | 'age',
1006 | 'gender'
1007 | ]
1008 | ]);
1009 |
1010 | $this->assertQuery(
1011 | <<database->queryString
1017 | );
1018 | }
1019 |
1020 | /**
1021 | * @covers ::select()
1022 | * @covers ::dataImplode()
1023 | * @covers ::whereClause()
1024 | * @dataProvider typesProvider
1025 | */
1026 | public function testGroupWithRawWhere($type)
1027 | {
1028 | $this->setType($type);
1029 |
1030 | $this->database->select("account", "user_name", [
1031 | 'GROUP' => Medoo::raw(", ")
1032 | ]);
1033 |
1034 | $this->assertQuery(
1035 | <<database->queryString
1041 | );
1042 | }
1043 |
1044 | /**
1045 | * @covers ::select()
1046 | * @covers ::dataImplode()
1047 | * @covers ::whereClause()
1048 | * @dataProvider typesProvider
1049 | */
1050 | public function testHavingWhere($type)
1051 | {
1052 | $this->setType($type);
1053 |
1054 | $this->database->select("account", "user_name", [
1055 | 'HAVING' => [
1056 | 'user_id[>]' => 500
1057 | ]
1058 | ]);
1059 |
1060 | $this->assertQuery(
1061 | << 500
1065 | EOD,
1066 | $this->database->queryString
1067 | );
1068 | }
1069 |
1070 | /**
1071 | * @covers ::select()
1072 | * @covers ::dataImplode()
1073 | * @covers ::whereClause()
1074 | * @dataProvider typesProvider
1075 | */
1076 | public function testHavingWithRawWhere($type)
1077 | {
1078 | $this->setType($type);
1079 |
1080 | $this->database->select("account", "user_name", [
1081 | 'HAVING' => Medoo::raw(' = LOWER("NEW YORK")')
1082 | ]);
1083 |
1084 | $this->assertQuery(
1085 | <<database->queryString
1091 | );
1092 | }
1093 |
1094 | /**
1095 | * @covers ::select()
1096 | * @covers ::dataImplode()
1097 | * @covers ::whereClause()
1098 | * @dataProvider typesProvider
1099 | */
1100 | public function testHavingWithAggregateRawWhere($type)
1101 | {
1102 | $this->setType($type);
1103 |
1104 | $this->database->select("account", [
1105 | "total" => Medoo::raw('SUM()')
1106 | ], [
1107 | 'HAVING' => Medoo::raw('SUM() > 1000')
1108 | ]);
1109 |
1110 | $this->assertQuery(
1111 | << 1000
1115 | EOD,
1116 | $this->database->queryString
1117 | );
1118 | }
1119 |
1120 | /**
1121 | * @covers ::select()
1122 | * @covers ::dataImplode()
1123 | * @covers ::whereClause()
1124 | * @dataProvider typesProvider
1125 | */
1126 | public function testRawWhereClause($type)
1127 | {
1128 | $this->setType($type);
1129 |
1130 | $this->database->select(
1131 | "account",
1132 | "user_name",
1133 | Medoo::raw("WHERE => 10")
1134 | );
1135 |
1136 | $this->assertQuery(
1137 | << 10
1141 | EOD,
1142 | $this->database->queryString
1143 | );
1144 | }
1145 |
1146 | /**
1147 | * @covers ::select()
1148 | * @covers ::dataImplode()
1149 | * @covers ::whereClause()
1150 | * @dataProvider typesProvider
1151 | */
1152 | public function testRawWhereWithJoinClause($type)
1153 | {
1154 | $this->setType($type);
1155 |
1156 | $this->database->select(
1157 | "post",
1158 | [
1159 | "[>]account" => "user_id",
1160 | ],
1161 | [
1162 | "post.content"
1163 | ],
1164 | Medoo::raw("WHERE => 10")
1165 | );
1166 |
1167 | $this->assertQuery(
1168 | << 10
1173 | EOD,
1174 | $this->database->queryString
1175 | );
1176 | }
1177 | }
1178 |
--------------------------------------------------------------------------------