OSDN Git Service

Добавлены тесты функционала на страницы site/index, site/contact, site/login, site...
[invent/invent.git] / vendor / codeception / module-db / src / Codeception / Module / Db.php
1 <?php
2 namespace Codeception\Module;
3
4 use Codeception\Module as CodeceptionModule;
5 use Codeception\Configuration;
6 use Codeception\Exception\ModuleException;
7 use Codeception\Exception\ModuleConfigException;
8 use Codeception\Lib\Interfaces\Db as DbInterface;
9 use Codeception\Lib\Driver\Db as Driver;
10 use Codeception\Lib\DbPopulator;
11 use Codeception\TestInterface;
12 use Codeception\Lib\Notification;
13 use Codeception\Util\ActionSequence;
14
15 /**
16  * Access a database.
17  *
18  * The most important function of this module is to clean a database before each test.
19  * This module also provides actions to perform checks in a database, e.g. [seeInDatabase()](http://codeception.com/docs/modules/Db#seeInDatabase)
20  *
21  * In order to have your database populated with data you need a raw SQL dump.
22  * Simply put the dump in the `tests/_data` directory (by default) and specify the path in the config.
23  * The next time after the database is cleared, all your data will be restored from the dump.
24  * Don't forget to include `CREATE TABLE` statements in the dump.
25  *
26  * Supported and tested databases are:
27  *
28  * * MySQL
29  * * SQLite (i.e. just one file)
30  * * PostgreSQL
31  *
32  * Also available:
33  *
34  * * MS SQL
35  * * Oracle
36  *
37  * Connection is done by database Drivers, which are stored in the `Codeception\Lib\Driver` namespace.
38  * [Check out the drivers](https://github.com/Codeception/Codeception/tree/2.4/src/Codeception/Lib/Driver)
39  * if you run into problems loading dumps and cleaning databases.
40  *
41  * ## Config
42  *
43  * * dsn *required* - PDO DSN
44  * * user *required* - username to access database
45  * * password *required* - password
46  * * dump - path to database dump
47  * * populate: false - whether the the dump should be loaded before the test suite is started
48  * * cleanup: false - whether the dump should be reloaded before each test
49  * * reconnect: false - whether the module should reconnect to the database before each test
50  * * waitlock: 0 - wait lock (in seconds) that the database session should use for DDL statements
51  * * ssl_key - path to the SSL key (MySQL specific, @see http://php.net/manual/de/ref.pdo-mysql.php#pdo.constants.mysql-attr-key)
52  * * ssl_cert - path to the SSL certificate (MySQL specific, @see http://php.net/manual/de/ref.pdo-mysql.php#pdo.constants.mysql-attr-ssl-cert)
53  * * ssl_ca - path to the SSL certificate authority (MySQL specific, @see http://php.net/manual/de/ref.pdo-mysql.php#pdo.constants.mysql-attr-ssl-ca)
54  * * ssl_verify_server_cert - disables certificate CN verification (MySQL specific, @see http://php.net/manual/de/ref.pdo-mysql.php)
55  * * ssl_cipher - list of one or more permissible ciphers to use for SSL encryption (MySQL specific, @see http://php.net/manual/de/ref.pdo-mysql.php#pdo.constants.mysql-attr-cipher)
56  * * databases - include more database configs and switch between them in tests.
57  * * initial_queries - list of queries to be executed right after connection to the database has been initiated, i.e. creating the database if it does not exist or preparing the database collation
58  *
59  * ## Example
60  *
61  *     modules:
62  *        enabled:
63  *           - Db:
64  *              dsn: 'mysql:host=localhost;dbname=testdb'
65  *              user: 'root'
66  *              password: ''
67  *              dump: 'tests/_data/dump.sql'
68  *              populate: true
69  *              cleanup: true
70  *              reconnect: true
71  *              waitlock: 10
72  *              ssl_key: '/path/to/client-key.pem'
73  *              ssl_cert: '/path/to/client-cert.pem'
74  *              ssl_ca: '/path/to/ca-cert.pem'
75  *              ssl_verify_server_cert: false
76  *              ssl_cipher: 'AES256-SHA'
77  *              initial_queries:
78  *                  - 'CREATE DATABASE IF NOT EXISTS temp_db;'
79  *                  - 'USE temp_db;'
80  *                  - 'SET NAMES utf8;'
81  *
82  * ## Example with multi-dumps
83  *     modules:
84  *          enabled:
85  *             - Db:
86  *                dsn: 'mysql:host=localhost;dbname=testdb'
87  *                user: 'root'
88  *                password: ''
89  *                dump:
90  *                   - 'tests/_data/dump.sql'
91  *                   - 'tests/_data/dump-2.sql'
92  *
93  * ## Example with multi-databases
94  *
95  *     modules:
96  *        enabled:
97  *           - Db:
98  *              dsn: 'mysql:host=localhost;dbname=testdb'
99  *              user: 'root'
100  *              password: ''
101  *              databases:
102  *                 db2:
103  *                    dsn: 'mysql:host=localhost;dbname=testdb2'
104  *                    user: 'userdb2'
105  *                    password: ''
106  *
107  * ## SQL data dump
108  *
109  * There are two ways of loading the dump into your database:
110  *
111  * ### Populator
112  *
113  * The recommended approach is to configure a `populator`, an external command to load a dump. Command parameters like host, username, password, database
114  * can be obtained from the config and inserted into placeholders:
115  *
116  * For MySQL:
117  *
118  * ```yaml
119  * modules:
120  *    enabled:
121  *       - Db:
122  *          dsn: 'mysql:host=localhost;dbname=testdb'
123  *          user: 'root'
124  *          password: ''
125  *          dump: 'tests/_data/dump.sql'
126  *          populate: true # run populator before all tests
127  *          cleanup: true # run populator before each test
128  *          populator: 'mysql -u $user -h $host $dbname < $dump'
129  * ```
130  *
131  * For PostgreSQL (using pg_restore)
132  *
133  * ```
134  * modules:
135  *    enabled:
136  *       - Db:
137  *          dsn: 'pgsql:host=localhost;dbname=testdb'
138  *          user: 'root'
139  *          password: ''
140  *          dump: 'tests/_data/db_backup.dump'
141  *          populate: true # run populator before all tests
142  *          cleanup: true # run populator before each test
143  *          populator: 'pg_restore -u $user -h $host -D $dbname < $dump'
144  * ```
145  *
146  *  Variable names are being taken from config and DSN which has a `keyword=value` format, so you should expect to have a variable named as the
147  *  keyword with the full value inside it.
148  *
149  *  PDO dsn elements for the supported drivers:
150  *  * MySQL: [PDO_MYSQL DSN](https://secure.php.net/manual/en/ref.pdo-mysql.connection.php)
151  *  * SQLite: [PDO_SQLITE DSN](https://secure.php.net/manual/en/ref.pdo-sqlite.connection.php)
152  *  * PostgreSQL: [PDO_PGSQL DSN](https://secure.php.net/manual/en/ref.pdo-pgsql.connection.php)
153  *  * MSSQL: [PDO_SQLSRV DSN](https://secure.php.net/manual/en/ref.pdo-sqlsrv.connection.php)
154  *  * Oracle: [PDO_OCI DSN](https://secure.php.net/manual/en/ref.pdo-oci.connection.php)
155  *
156  * ### Dump
157  *
158  * Db module by itself can load SQL dump without external tools by using current database connection.
159  * This approach is system-independent, however, it is slower than using a populator and may have parsing issues (see below).
160  *
161  * Provide a path to SQL file in `dump` config option:
162  *
163  * ```yaml
164  * modules:
165  *    enabled:
166  *       - Db:
167  *          dsn: 'mysql:host=localhost;dbname=testdb'
168  *          user: 'root'
169  *          password: ''
170  *          populate: true # load dump before all tests
171  *          cleanup: true # load dump for each test
172  *          dump: 'tests/_data/dump.sql'
173  * ```
174  *
175  *  To parse SQL Db file, it should follow this specification:
176  *  * Comments are permitted.
177  *  * The `dump.sql` may contain multiline statements.
178  *  * The delimiter, a semi-colon in this case, must be on the same line as the last statement:
179  *
180  * ```sql
181  * -- Add a few contacts to the table.
182  * REPLACE INTO `Contacts` (`created`, `modified`, `status`, `contact`, `first`, `last`) VALUES
183  * (NOW(), NOW(), 1, 'Bob Ross', 'Bob', 'Ross'),
184  * (NOW(), NOW(), 1, 'Fred Flintstone', 'Fred', 'Flintstone');
185  *
186  * -- Remove existing orders for testing.
187  * DELETE FROM `Order`;
188  * ```
189  * ## Query generation
190  *
191  * `seeInDatabase`, `dontSeeInDatabase`, `seeNumRecords`, `grabFromDatabase` and `grabNumRecords` methods
192  * accept arrays as criteria. WHERE condition is generated using item key as a field name and
193  * item value as a field value.
194  *
195  * Example:
196  * ```php
197  * <?php
198  * $I->seeInDatabase('users', ['name' => 'Davert', 'email' => 'davert@mail.com']);
199  *
200  * ```
201  * Will generate:
202  *
203  * ```sql
204  * SELECT COUNT(*) FROM `users` WHERE `name` = 'Davert' AND `email` = 'davert@mail.com'
205  * ```
206  * Since version 2.1.9 it's possible to use LIKE in a condition, as shown here:
207  *
208  * ```php
209  * <?php
210  * $I->seeInDatabase('users', ['name' => 'Davert', 'email like' => 'davert%']);
211  *
212  * ```
213  * Will generate:
214  *
215  * ```sql
216  * SELECT COUNT(*) FROM `users` WHERE `name` = 'Davert' AND `email` LIKE 'davert%'
217  * ```
218  * ## Public Properties
219  * * dbh - contains the PDO connection
220  * * driver - contains the Connection Driver
221  *
222  */
223 class Db extends CodeceptionModule implements DbInterface
224 {
225     /**
226      * @var array
227      */
228     protected $config = [
229         'populate' => false,
230         'cleanup' => false,
231         'reconnect' => false,
232         'waitlock' => 0,
233         'dump' => null,
234         'populator' => null,
235     ];
236
237     /**
238      * @var array
239      */
240     protected $requiredFields = ['dsn', 'user', 'password'];
241     const DEFAULT_DATABASE = 'default';
242
243     /**
244      * @var Driver[]
245      */
246     public $drivers = [];
247     /**
248      * @var \PDO[]
249      */
250     public $dbhs = [];
251     public $databasesPopulated = [];
252     public $databasesSql = [];
253     protected $insertedRows = [];
254     public $currentDatabase = self::DEFAULT_DATABASE;
255
256     protected function getDatabases()
257     {
258         $databases = [$this->currentDatabase => $this->config];
259
260         if (!empty($this->config['databases'])) {
261             foreach ($this->config['databases'] as $databaseKey => $databaseConfig) {
262                 $databases[$databaseKey] = array_merge([
263                     'populate' => false,
264                     'cleanup' => false,
265                     'reconnect' => false,
266                     'waitlock' => 0,
267                     'dump' => null,
268                     'populator' => null,
269                 ], $databaseConfig);
270             }
271         }
272         return $databases;
273     }
274     protected function connectToDatabases()
275     {
276         foreach ($this->getDatabases() as $databaseKey => $databaseConfig) {
277             $this->connect($databaseKey, $databaseConfig);
278         }
279     }
280     protected function cleanUpDatabases()
281     {
282         foreach ($this->getDatabases() as $databaseKey => $databaseConfig) {
283             $this->_cleanup($databaseKey, $databaseConfig);
284         }
285     }
286     protected function populateDatabases($configKey)
287     {
288         foreach ($this->getDatabases() as $databaseKey => $databaseConfig) {
289             if ($databaseConfig[$configKey]) {
290                 if (!$databaseConfig['populate']) {
291                     return;
292                 }
293
294                 if (isset($this->databasesPopulated[$databaseKey]) && $this->databasesPopulated[$databaseKey]) {
295                     return;
296                 }
297                 $this->_loadDump($databaseKey, $databaseConfig);
298             }
299         }
300     }
301     protected function readSqlForDatabases()
302     {
303         foreach ($this->getDatabases() as $databaseKey => $databaseConfig) {
304             $this->readSql($databaseKey, $databaseConfig);
305         }
306     }
307     protected function removeInsertedForDatabases()
308     {
309         foreach ($this->getDatabases() as $databaseKey => $databaseConfig) {
310             $this->amConnectedToDatabase($databaseKey);
311             $this->removeInserted($databaseKey);
312         }
313     }
314     protected function disconnectDatabases()
315     {
316         foreach ($this->getDatabases() as $databaseKey => $databaseConfig) {
317             $this->disconnect($databaseKey);
318         }
319     }
320     protected function reconnectDatabases()
321     {
322         foreach ($this->getDatabases() as $databaseKey => $databaseConfig) {
323             if ($databaseConfig['reconnect']) {
324                 $this->disconnect($databaseKey);
325                 $this->connect($databaseKey, $databaseConfig);
326             }
327         }
328     }
329
330     public function __get($name)
331     {
332         Notification::deprecate("Properties dbh and driver are deprecated in favor of Db::_getDbh and Db::_getDriver", "Db module");
333
334         if ($name == 'driver') {
335             return $this->_getDriver();
336         }
337         if ($name == 'dbh') {
338             return $this->_getDbh();
339         }
340     }
341
342     /**
343      * @return Driver
344      */
345     public function _getDriver()
346     {
347         return $this->drivers[$this->currentDatabase];
348     }
349     public function _getDbh()
350     {
351         return $this->dbhs[$this->currentDatabase];
352     }
353
354     /**
355      * Make sure you are connected to the right database.
356      *
357      * ```php
358      * <?php
359      * $I->seeNumRecords(2, 'users');   //executed on default database
360      * $I->amConnectedToDatabase('db_books');
361      * $I->seeNumRecords(30, 'books');  //executed on db_books database
362      * //All the next queries will be on db_books
363      * ```
364      * @param $databaseKey
365      * @throws ModuleConfigException
366      */
367     public function amConnectedToDatabase($databaseKey)
368     {
369         if (empty($this->getDatabases()[$databaseKey]) && $databaseKey != self::DEFAULT_DATABASE) {
370             throw new ModuleConfigException(
371                 __CLASS__,
372                 "\nNo database $databaseKey in the key databases.\n"
373             );
374         }
375         $this->currentDatabase = $databaseKey;
376     }
377
378     /**
379      * Can be used with a callback if you don't want to change the current database in your test.
380      *
381      * ```php
382      * <?php
383      * $I->seeNumRecords(2, 'users');   //executed on default database
384      * $I->performInDatabase('db_books', function($I) {
385      *     $I->seeNumRecords(30, 'books');  //executed on db_books database
386      * });
387      * $I->seeNumRecords(2, 'users');  //executed on default database
388      * ```
389      * List of actions can be pragmatically built using `Codeception\Util\ActionSequence`:
390      *
391      * ```php
392      * <?php
393      * $I->performInDatabase('db_books', ActionSequence::build()
394      *     ->seeNumRecords(30, 'books')
395      * );
396      * ```
397      * Alternatively an array can be used:
398      *
399      * ```php
400      * $I->performInDatabase('db_books', ['seeNumRecords' => [30, 'books']]);
401      * ```
402      *
403      * Choose the syntax you like the most and use it,
404      *
405      * Actions executed from array or ActionSequence will print debug output for actions, and adds an action name to
406      * exception on failure.
407      *
408      * @param $databaseKey
409      * @param \Codeception\Util\ActionSequence|array|callable $actions
410      * @throws ModuleConfigException
411      */
412     public function performInDatabase($databaseKey, $actions)
413     {
414         $backupDatabase = $this->currentDatabase;
415         $this->amConnectedToDatabase($databaseKey);
416
417         if (is_callable($actions)) {
418             $actions($this);
419             $this->amConnectedToDatabase($backupDatabase);
420             return;
421         }
422         if (is_array($actions)) {
423             $actions = ActionSequence::build()->fromArray($actions);
424         }
425
426         if (!$actions instanceof ActionSequence) {
427             throw new \InvalidArgumentException("2nd parameter, actions should be callback, ActionSequence or array");
428         }
429
430         $actions->run($this);
431         $this->amConnectedToDatabase($backupDatabase);
432     }
433
434     public function _initialize()
435     {
436         $this->connectToDatabases();
437     }
438
439     public function __destruct()
440     {
441         $this->disconnectDatabases();
442     }
443
444     public function _beforeSuite($settings = [])
445     {
446         $this->readSqlForDatabases();
447         $this->connectToDatabases();
448         $this->cleanUpDatabases();
449         $this->populateDatabases('populate');
450     }
451
452     private function readSql($databaseKey = null, $databaseConfig = null)
453     {
454         if ($databaseConfig['populator']) {
455             return;
456         }
457         if (!$databaseConfig['cleanup'] && !$databaseConfig['populate']) {
458             return;
459         }
460         if (empty($databaseConfig['dump'])) {
461             return;
462         }
463
464         if (!is_array($databaseConfig['dump'])) {
465             $databaseConfig['dump'] = [$databaseConfig['dump']];
466         }
467
468         $sql = '';
469
470         foreach ($databaseConfig['dump'] as $filePath) {
471             $sql .= $this->readSqlFile($filePath);
472         }
473
474         if (!empty($sql)) {
475             // split SQL dump into lines
476             $this->databasesSql[$databaseKey] = preg_split('/\r\n|\n|\r/', $sql, -1, PREG_SPLIT_NO_EMPTY);
477         }
478     }
479
480     /**
481      * @param $filePath
482      *
483      * @return bool|null|string|string[]
484      * @throws \Codeception\Exception\ModuleConfigException
485      */
486     private function readSqlFile($filePath)
487     {
488         if (!file_exists(Configuration::projectDir() . $filePath)) {
489             throw new ModuleConfigException(
490                 __CLASS__,
491                 "\nFile with dump doesn't exist.\n"
492                 . "Please, check path for sql file: "
493                 . $filePath
494             );
495         }
496
497         $sql = file_get_contents(Configuration::projectDir() . $filePath);
498
499         // remove C-style comments (except MySQL directives)
500         $sql = preg_replace('%/\*(?!!\d+).*?\*/%s', '', $sql);
501
502         return $sql;
503     }
504
505     private function connect($databaseKey, $databaseConfig)
506     {
507         if (!empty($this->drivers[$databaseKey]) && !empty($this->dbhs[$databaseKey])) {
508             return;
509         }
510         $options = [];
511
512         /**
513          * @see http://php.net/manual/en/pdo.construct.php
514          * @see http://php.net/manual/de/ref.pdo-mysql.php#pdo-mysql.constants
515          */
516         if (array_key_exists('ssl_key', $databaseConfig)
517             && !empty($databaseConfig['ssl_key'])
518             && defined('\PDO::MYSQL_ATTR_SSL_KEY')
519         ) {
520             $options[\PDO::MYSQL_ATTR_SSL_KEY] = (string) $databaseConfig['ssl_key'];
521         }
522
523         if (array_key_exists('ssl_cert', $databaseConfig)
524             && !empty($databaseConfig['ssl_cert'])
525             && defined('\PDO::MYSQL_ATTR_SSL_CERT')
526         ) {
527             $options[\PDO::MYSQL_ATTR_SSL_CERT] = (string) $databaseConfig['ssl_cert'];
528         }
529
530         if (array_key_exists('ssl_ca', $databaseConfig)
531             && !empty($databaseConfig['ssl_ca'])
532             && defined('\PDO::MYSQL_ATTR_SSL_CA')
533         ) {
534             $options[\PDO::MYSQL_ATTR_SSL_CA] = (string) $databaseConfig['ssl_ca'];
535         }
536
537         if (array_key_exists('ssl_cipher', $databaseConfig)
538             && !empty($databaseConfig['ssl_cipher'])
539             && defined('\PDO::MYSQL_ATTR_SSL_CIPHER')
540         ) {
541             $options[\PDO::MYSQL_ATTR_SSL_CIPHER] = (string) $databaseConfig['ssl_cipher'];
542         }
543
544         if (array_key_exists('ssl_verify_server_cert', $databaseConfig)
545             && defined('\PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT')
546         ) {
547             $options[\PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT] = (boolean) $databaseConfig[ 'ssl_verify_server_cert' ];
548         }
549
550         try {
551             $this->debugSection('Connecting To Db', ['config' => $databaseConfig, 'options' => $options]);
552             $this->drivers[$databaseKey] = Driver::create($databaseConfig['dsn'], $databaseConfig['user'], $databaseConfig['password'], $options);
553         } catch (\PDOException $e) {
554             $message = $e->getMessage();
555             if ($message === 'could not find driver') {
556                 list ($missingDriver, ) = explode(':', $databaseConfig['dsn'], 2);
557                 $message = "could not find $missingDriver driver";
558             }
559
560             throw new ModuleException(__CLASS__, $message . ' while creating PDO connection');
561         }
562
563         if ($databaseConfig['waitlock']) {
564             $this->_getDriver()->setWaitLock($databaseConfig['waitlock']);
565         }
566
567         if (isset($databaseConfig['initial_queries'])) {
568             foreach ($databaseConfig['initial_queries'] as $initialQuery) {
569                 $this->drivers[$databaseKey]->executeQuery($initialQuery, []);
570             }
571         }
572
573         $this->debugSection('Db', 'Connected to ' . $databaseKey . ' ' . $this->drivers[$databaseKey]->getDb());
574         $this->dbhs[$databaseKey] = $this->drivers[$databaseKey]->getDbh();
575     }
576
577     private function disconnect($databaseKey)
578     {
579         $this->debugSection('Db', 'Disconnected from ' . $databaseKey);
580         $this->dbhs[$databaseKey] = null;
581         $this->drivers[$databaseKey] = null;
582     }
583
584     public function _before(TestInterface $test)
585     {
586         $this->reconnectDatabases();
587         $this->amConnectedToDatabase(self::DEFAULT_DATABASE);
588
589         $this->cleanUpDatabases();
590
591         $this->populateDatabases('cleanup');
592
593         parent::_before($test);
594     }
595
596     public function _after(TestInterface $test)
597     {
598         $this->removeInsertedForDatabases();
599         parent::_after($test);
600     }
601
602     protected function removeInserted($databaseKey = null)
603     {
604         $databaseKey = empty($databaseKey) ?  self::DEFAULT_DATABASE : $databaseKey;
605
606         if (empty($this->insertedRows[$databaseKey])) {
607             return;
608         }
609
610         foreach (array_reverse($this->insertedRows[$databaseKey]) as $row) {
611             try {
612                 $this->_getDriver()->deleteQueryByCriteria($row['table'], $row['primary']);
613             } catch (\Exception $e) {
614                 $this->debug("Couldn't delete record " . json_encode($row['primary']) ." from {$row['table']}");
615             }
616         }
617         $this->insertedRows[$databaseKey] = [];
618     }
619
620     public function _cleanup($databaseKey = null, $databaseConfig = null)
621     {
622         $databaseKey = empty($databaseKey) ?  self::DEFAULT_DATABASE : $databaseKey;
623         $databaseConfig = empty($databaseConfig) ?  $this->config : $databaseConfig;
624
625         if (!$databaseConfig['populate']) {
626             return;
627         }
628         if (!$databaseConfig['cleanup']) {
629             return;
630         }
631         if (isset($this->databasesPopulated[$databaseKey]) && !$this->databasesPopulated[$databaseKey]) {
632             return;
633         }
634         $dbh = $this->dbhs[$databaseKey];
635         if (!$dbh) {
636             throw new ModuleConfigException(
637                 __CLASS__,
638                 'No connection to database. Remove this module from config if you don\'t need database repopulation'
639             );
640         }
641         try {
642             if (false === $this->shouldCleanup($databaseConfig, $databaseKey)) {
643                 return;
644             }
645             $this->drivers[$databaseKey]->cleanup();
646             $this->databasesPopulated[$databaseKey] = false;
647         } catch (\Exception $e) {
648             throw new ModuleException(__CLASS__, $e->getMessage());
649         }
650     }
651
652     /**
653      * @param  array  $databaseConfig
654      * @param  string $databaseKey
655      * @return bool
656      */
657     protected function shouldCleanup($databaseConfig, $databaseKey)
658     {
659         // If using populator and it's not empty, clean up regardless
660         if (!empty($databaseConfig['populator'])) {
661             return true;
662         }
663
664         // If no sql dump for $databaseKey or sql dump is empty, don't clean up
665         return !empty($this->databasesSql[$databaseKey]);
666     }
667
668     public function _isPopulated()
669     {
670         return $this->databasesPopulated[$this->currentDatabase];
671     }
672
673     public function _loadDump($databaseKey = null, $databaseConfig = null)
674     {
675         $databaseKey = empty($databaseKey) ?  self::DEFAULT_DATABASE : $databaseKey;
676         $databaseConfig = empty($databaseConfig) ?  $this->config : $databaseConfig;
677
678         if ($databaseConfig['populator']) {
679             $this->loadDumpUsingPopulator($databaseKey, $databaseConfig);
680             return;
681         }
682         $this->loadDumpUsingDriver($databaseKey);
683     }
684
685     protected function loadDumpUsingPopulator($databaseKey, $databaseConfig)
686     {
687         $populator = new DbPopulator($databaseConfig);
688         $this->databasesPopulated[$databaseKey] = $populator->run();
689     }
690
691     protected function loadDumpUsingDriver($databaseKey)
692     {
693         if (!isset($this->databasesSql[$databaseKey])) {
694             return;
695         }
696         if (!$this->databasesSql[$databaseKey]) {
697             $this->debugSection('Db', 'No SQL loaded, loading dump skipped');
698             return;
699         }
700         $this->drivers[$databaseKey]->load($this->databasesSql[$databaseKey]);
701         $this->databasesPopulated[$databaseKey] = true;
702     }
703
704     /**
705      * Inserts an SQL record into a database. This record will be erased after the test.
706      *
707      * ```php
708      * <?php
709      * $I->haveInDatabase('users', array('name' => 'miles', 'email' => 'miles@davis.com'));
710      * ?>
711      * ```
712      *
713      * @param string $table
714      * @param array $data
715      *
716      * @return integer $id
717      */
718     public function haveInDatabase($table, array $data)
719     {
720         $lastInsertId = $this->_insertInDatabase($table, $data);
721
722         $this->addInsertedRow($table, $data, $lastInsertId);
723
724         return $lastInsertId;
725     }
726
727     public function _insertInDatabase($table, array $data)
728     {
729         $query = $this->_getDriver()->insert($table, $data);
730         $parameters = array_values($data);
731         $this->debugSection('Query', $query);
732         $this->debugSection('Parameters', $parameters);
733         $this->_getDriver()->executeQuery($query, $parameters);
734
735         try {
736             $lastInsertId = (int)$this->_getDriver()->lastInsertId($table);
737         } catch (\PDOException $e) {
738             // ignore errors due to uncommon DB structure,
739             // such as tables without _id_seq in PGSQL
740             $lastInsertId = 0;
741             $this->debugSection('DB error', $e->getMessage());
742         }
743         return $lastInsertId;
744     }
745
746     private function addInsertedRow($table, array $row, $id)
747     {
748         $primaryKey = $this->_getDriver()->getPrimaryKey($table);
749         $primary = [];
750         if ($primaryKey) {
751             if ($id && count($primaryKey) === 1) {
752                 $primary [$primaryKey[0]] = $id;
753             } else {
754                 foreach ($primaryKey as $column) {
755                     if (isset($row[$column])) {
756                         $primary[$column] = $row[$column];
757                     } else {
758                         throw new \InvalidArgumentException(
759                             'Primary key field ' . $column . ' is not set for table ' . $table
760                         );
761                     }
762                 }
763             }
764         } else {
765             $primary = $row;
766         }
767
768         $this->insertedRows[$this->currentDatabase][] = [
769             'table' => $table,
770             'primary' => $primary,
771         ];
772     }
773
774     public function seeInDatabase($table, $criteria = [])
775     {
776         $res = $this->countInDatabase($table, $criteria);
777         $this->assertGreaterThan(
778             0,
779             $res,
780             'No matching records found for criteria ' . json_encode($criteria) . ' in table ' . $table
781         );
782     }
783
784     /**
785      * Asserts that the given number of records were found in the database.
786      *
787      * ```php
788      * <?php
789      * $I->seeNumRecords(1, 'users', ['name' => 'davert'])
790      * ?>
791      * ```
792      *
793      * @param int $expectedNumber Expected number
794      * @param string $table Table name
795      * @param array $criteria Search criteria [Optional]
796      */
797     public function seeNumRecords($expectedNumber, $table, array $criteria = [])
798     {
799         $actualNumber = $this->countInDatabase($table, $criteria);
800         $this->assertEquals(
801             $expectedNumber,
802             $actualNumber,
803             sprintf(
804                 'The number of found rows (%d) does not match expected number %d for criteria %s in table %s',
805                 $actualNumber,
806                 $expectedNumber,
807                 json_encode($criteria),
808                 $table
809             )
810         );
811     }
812
813     public function dontSeeInDatabase($table, $criteria = [])
814     {
815         $count = $this->countInDatabase($table, $criteria);
816         $this->assertLessThan(
817             1,
818             $count,
819             'Unexpectedly found matching records for criteria ' . json_encode($criteria) . ' in table ' . $table
820         );
821     }
822
823     /**
824      * Count rows in a database
825      *
826      * @param string $table    Table name
827      * @param array  $criteria Search criteria [Optional]
828      *
829      * @return int
830      */
831     protected function countInDatabase($table, array $criteria = [])
832     {
833         return (int) $this->proceedSeeInDatabase($table, 'count(*)', $criteria);
834     }
835
836     /**
837      * Fetches all values from the column in database.
838      * Provide table name, desired column and criteria.
839      *
840      * @param string $table
841      * @param string $column
842      * @param array  $criteria
843      *
844      * @return mixed
845      */
846     protected function proceedSeeInDatabase($table, $column, $criteria)
847     {
848         $query = $this->_getDriver()->select($column, $table, $criteria);
849         $parameters = array_values($criteria);
850         $this->debugSection('Query', $query);
851         if (!empty($parameters)) {
852             $this->debugSection('Parameters', $parameters);
853         }
854         $sth = $this->_getDriver()->executeQuery($query, $parameters);
855
856         return $sth->fetchColumn();
857     }
858
859     /**
860      * Fetches all values from the column in database.
861      * Provide table name, desired column and criteria.
862      *
863      * ``` php
864      * <?php
865      * $mails = $I->grabColumnFromDatabase('users', 'email', array('name' => 'RebOOter'));
866      * ```
867      *
868      * @param string $table
869      * @param string $column
870      * @param array $criteria
871      *
872      * @return array
873      */
874     public function grabColumnFromDatabase($table, $column, array $criteria = [])
875     {
876         $query      = $this->_getDriver()->select($column, $table, $criteria);
877         $parameters = array_values($criteria);
878         $this->debugSection('Query', $query);
879         $this->debugSection('Parameters', $parameters);
880         $sth = $this->_getDriver()->executeQuery($query, $parameters);
881
882         return $sth->fetchAll(\PDO::FETCH_COLUMN, 0);
883     }
884
885     /**
886      * Fetches a single column value from a database.
887      * Provide table name, desired column and criteria.
888      *
889      * ``` php
890      * <?php
891      * $mail = $I->grabFromDatabase('users', 'email', array('name' => 'Davert'));
892      * ```
893      * Comparison expressions can be used as well:
894      *
895      * ```php
896      * <?php
897      * $post = $I->grabFromDatabase('posts', ['num_comments >=' => 100]);
898      * $user = $I->grabFromDatabase('users', ['email like' => 'miles%']);
899      * ```
900      *
901      * Supported operators: `<`, `>`, `>=`, `<=`, `!=`, `like`.
902      *
903      * @param string $table
904      * @param string $column
905      * @param array $criteria
906      *
907      * @return mixed Returns a single column value or false
908      */
909     public function grabFromDatabase($table, $column, $criteria = [])
910     {
911         return $this->proceedSeeInDatabase($table, $column, $criteria);
912     }
913
914     /**
915      * Returns the number of rows in a database
916      *
917      * @param string $table    Table name
918      * @param array  $criteria Search criteria [Optional]
919      *
920      * @return int
921      */
922     public function grabNumRecords($table, array $criteria = [])
923     {
924         return $this->countInDatabase($table, $criteria);
925     }
926
927     /**
928      * Update an SQL record into a database.
929      *
930      * ```php
931      * <?php
932      * $I->updateInDatabase('users', array('isAdmin' => true), array('email' => 'miles@davis.com'));
933      * ?>
934      * ```
935      *
936      * @param string $table
937      * @param array $data
938      * @param array $criteria
939      */
940     public function updateInDatabase($table, array $data, array $criteria = [])
941     {
942         $query = $this->_getDriver()->update($table, $data, $criteria);
943         $parameters = array_merge(array_values($data), array_values($criteria));
944         $this->debugSection('Query', $query);
945         if (!empty($parameters)) {
946             $this->debugSection('Parameters', $parameters);
947         }
948         $this->_getDriver()->executeQuery($query, $parameters);
949     }
950 }