├── .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 | Build Status 7 | Total Downloads 8 | Latest Stable Version 9 | License 10 | Backers on Open Collective 11 | Sponsors on Open Collective 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 | [![Gear Browser](https://github.com/catfan/Medoo/assets/1467904/bc5059d4-6a2d-4bbf-90d9-a9f71bae3335)](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 | <<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 | --------------------------------------------------------------------------------