diff --git a/app/Common.php b/app/Common.php new file mode 100644 index 000000000000..780ba3f80198 --- /dev/null +++ b/app/Common.php @@ -0,0 +1,15 @@ +showHeader(); -// fire off the command the main framework. -$console->run(); +// fire off the command in the main framework. +$response = $console->run(); +if ($response->getStatusCode() >= 300) +{ + exit($response->getStatusCode()); +} diff --git a/system/Autoloader/FileLocator.php b/system/Autoloader/FileLocator.php index 9de461a888f5..0bacd15d3718 100644 --- a/system/Autoloader/FileLocator.php +++ b/system/Autoloader/FileLocator.php @@ -126,19 +126,19 @@ public function locateFile(string $file, string $folder = null, string $ext = 'p $filename = implode('/', $segments); break; } - + // if no namespaces matched then quit if (empty($paths)) { return false; } - + // Check each path in the namespace foreach ($paths as $path) { // Ensure trailing slash $path = rtrim($path, '/') . '/'; - + // If we have a folder name, then the calling function // expects this file to be within that folder, like 'Views', // or 'libraries'. @@ -153,7 +153,7 @@ public function locateFile(string $file, string $folder = null, string $ext = 'p return $path; } } - + return false; } diff --git a/system/CLI/CommandRunner.php b/system/CLI/CommandRunner.php index 00c78747e489..2d811249cd09 100644 --- a/system/CLI/CommandRunner.php +++ b/system/CLI/CommandRunner.php @@ -71,6 +71,7 @@ class CommandRunner extends Controller * @param string $method * @param array ...$params * + * @return mixed * @throws \ReflectionException */ public function _remap($method, ...$params) @@ -81,7 +82,7 @@ public function _remap($method, ...$params) array_shift($params); } - $this->index($params); + return $this->index($params); } //-------------------------------------------------------------------- diff --git a/system/CodeIgniter.php b/system/CodeIgniter.php index 4222c336b300..8d332dcd126b 100644 --- a/system/CodeIgniter.php +++ b/system/CodeIgniter.php @@ -355,6 +355,12 @@ protected function handleRequest(RouteCollectionInterface $routes = null, $cache else { $response = $this->response; + + // Set response code for CLI command failures + if (is_numeric($returned) || $returned === false) + { + $response->setStatusCode(400); + } } if ($response instanceof Response) diff --git a/system/Commands/Database/CreateMigration.php b/system/Commands/Database/CreateMigration.php index b70d27a3578b..a674a397c6d4 100644 --- a/system/Commands/Database/CreateMigration.php +++ b/system/Commands/Database/CreateMigration.php @@ -123,7 +123,7 @@ public function run(array $params = []) if (! empty($ns)) { - // Get all namespaces form PSR4 paths. + // Get all namespaces from PSR4 paths. $config = new Autoload(); $namespaces = $config->psr4; diff --git a/system/Commands/Database/MigrateLatest.php b/system/Commands/Database/Migrate.php similarity index 85% rename from system/Commands/Database/MigrateLatest.php rename to system/Commands/Database/Migrate.php index 3df058bcff34..a122dcbb91be 100644 --- a/system/Commands/Database/MigrateLatest.php +++ b/system/Commands/Database/Migrate.php @@ -43,11 +43,11 @@ use Config\Services; /** - * Creates a new migration file. + * Runs all new migrations. * * @package CodeIgniter\Commands */ -class MigrateLatest extends BaseCommand +class Migrate extends BaseCommand { /** * The group the command is lumped under @@ -69,7 +69,7 @@ class MigrateLatest extends BaseCommand * * @var string */ - protected $description = 'Migrates the database to the latest schema.'; + protected $description = 'Locates and runs all new migrations against the database.'; /** * the Command's usage @@ -93,7 +93,7 @@ class MigrateLatest extends BaseCommand protected $options = [ '-n' => 'Set migration namespace', '-g' => 'Set database group', - '-all' => 'Set latest for all namespace, will ignore (-n) option', + '-all' => 'Set for all namespaces, will ignore (-n) option', ]; /** @@ -104,22 +104,31 @@ class MigrateLatest extends BaseCommand public function run(array $params = []) { $runner = Services::migrations(); + $runner->clearCliMessages(); - CLI::write(lang('Migrations.toLatest'), 'yellow'); + CLI::write(lang('Migrations.latest'), 'yellow'); $namespace = $params['-n'] ?? CLI::getOption('n'); $group = $params['-g'] ?? CLI::getOption('g'); try { + // Check for 'all' namespaces if ($this->isAllNamespace($params)) { - $runner->latestAll($group); + $runner->setNamespace(null); } - else + // Check for a specified namespace + elseif ($namespace) { - $runner->latest($namespace, $group); + $runner->setNamespace($namespace); } + + if (! $runner->latest($group)) + { + CLI::write(lang('Migrations.generalFault'), 'red'); + } + $messages = $runner->getCliMessages(); foreach ($messages as $message) { diff --git a/system/Commands/Database/MigrateRefresh.php b/system/Commands/Database/MigrateRefresh.php index 9d0bb886d980..4468f166c26f 100644 --- a/system/Commands/Database/MigrateRefresh.php +++ b/system/Commands/Database/MigrateRefresh.php @@ -104,7 +104,7 @@ class MigrateRefresh extends BaseCommand */ public function run(array $params = []) { - $this->call('migrate:rollback', ['-b' => 1]); + $this->call('migrate:rollback', ['-b' => 0]); $this->call('migrate'); } diff --git a/system/Commands/Database/MigrateRollback.php b/system/Commands/Database/MigrateRollback.php index 0a264733b0a1..515db070bb19 100644 --- a/system/Commands/Database/MigrateRollback.php +++ b/system/Commands/Database/MigrateRollback.php @@ -72,7 +72,7 @@ class MigrateRollback extends BaseCommand * * @var string */ - protected $description = 'Runs the down method for all migrations in the last batch.'; + protected $description = 'Runs the "down" method for all migrations in the last batch.'; /** * the Command's usage @@ -94,7 +94,7 @@ class MigrateRollback extends BaseCommand * @var array */ protected $options = [ - '-b' => 'Specify a batch to roll back to', + '-b' => 'Specify a batch to roll back to; e.g. "3" to return to batch #3 or "-2" to roll back twice', '-g' => 'Set database group', ]; @@ -108,8 +108,6 @@ public function run(array $params = []) { $runner = Services::migrations(); - CLI::write(lang('Migrations.rollingBack'), 'yellow'); - $group = $params['-g'] ?? CLI::getOption('g'); if (! is_null($group)) @@ -119,10 +117,14 @@ public function run(array $params = []) try { - $batch = $params['-b'] ?? $runner->getLastBatch(); - - $runner->version($runner->getBatchStart($batch)); - + $batch = $params['-b'] ?? CLI::getOption('b') ?? $runner->getLastBatch() - 1; + CLI::write(lang('Migrations.rollingBack') . ' ' . $batch, 'yellow'); + + if (! $runner->regress($batch)) + { + CLI::write(lang('Migrations.generalFault'), 'red'); + } + $messages = $runner->getCliMessages(); foreach ($messages as $message) { diff --git a/system/Commands/Database/MigrateStatus.php b/system/Commands/Database/MigrateStatus.php index 147e73801d41..f0bc4981a44b 100644 --- a/system/Commands/Database/MigrateStatus.php +++ b/system/Commands/Database/MigrateStatus.php @@ -160,17 +160,18 @@ public function run(array $params = []) CLI::write(' ' . str_pad(lang('Migrations.filename'), $max + 4) . lang('Migrations.on'), 'yellow'); - foreach ($migrations as $version => $migration) + foreach ($migrations as $uid => $migration) { $date = ''; foreach ($history as $row) { - if ($row['version'] !== $version) + + if ($runner->getObjectUid($row) !== $uid) { continue; } - $date = date('Y-m-d H:i:s', $row['time']); + $date = date('Y-m-d H:i:s', $row->time); } CLI::write(str_pad(' ' . $migration->name, $max + 6) . ($date ? $date : '---')); } diff --git a/system/Commands/Database/MigrateVersion.php b/system/Commands/Database/MigrateVersion.php deleted file mode 100644 index d781f6810835..000000000000 --- a/system/Commands/Database/MigrateVersion.php +++ /dev/null @@ -1,140 +0,0 @@ - 'The version number to migrate', - ]; - - /** - * the Command's Options - * - * @var array - */ - protected $options = [ - '-n' => 'Set migration namespace', - '-g' => 'Set database group', - ]; - - /** - * Migrates the database up or down to get to the specified version. - * - * @param array $params - */ - public function run(array $params = []) - { - $runner = Services::migrations(); - - // Get the version number - $version = array_shift($params); - - if (is_null($version)) - { - $version = CLI::prompt(lang('Migrations.version')); - } - - if (is_null($version)) - { - CLI::error(lang('Migrations.invalidVersion')); - exit(); - } - - CLI::write(sprintf(lang('Migrations.toVersionPH'), $version), 'yellow'); - - $namespace = $params['-n'] ?? CLI::getOption('n'); - $group = $params['-g'] ?? CLI::getOption('g'); - - try - { - $runner->version($version, $namespace, $group); - CLI::write('Done'); - } - catch (\Exception $e) - { - $this->showError($e); - } - } - -} diff --git a/system/Database/BaseBuilder.php b/system/Database/BaseBuilder.php index 37735fcb7c9c..f261dd84458a 100644 --- a/system/Database/BaseBuilder.php +++ b/system/Database/BaseBuilder.php @@ -384,7 +384,24 @@ public function selectSum(string $select = '', string $alias = '') //-------------------------------------------------------------------- /** - * SELECT [MAX|MIN|AVG|SUM]() + * Select Count + * + * Generates a SELECT COUNT(field) portion of a query + * + * @param string $select The field + * @param string $alias An alias + * + * @return BaseBuilder + */ + public function selectCount(string $select = '', string $alias = '') + { + return $this->maxMinAvgSum($select, $alias, 'COUNT'); + } + + //-------------------------------------------------------------------- + + /** + * SELECT [MAX|MIN|AVG|SUM|COUNT]() * * @used-by selectMax() * @used-by selectMin() @@ -413,7 +430,7 @@ protected function maxMinAvgSum(string $select = '', string $alias = '', string $type = strtoupper($type); - if (! in_array($type, ['MAX', 'MIN', 'AVG', 'SUM'])) + if (! in_array($type, ['MAX', 'MIN', 'AVG', 'SUM', 'COUNT'])) { throw new DatabaseException('Invalid function type: ' . $type); } @@ -1552,6 +1569,10 @@ public function countAllResults(bool $reset = true, bool $test = false) $this->QBOrderBy = null; } + // We cannot use a LIMIT when getting the single row COUNT(*) result + $limit = $this->QBLimit; + $this->QBLimit = false; + $sql = ($this->QBDistinct === true) ? $this->countString . $this->db->protectIdentifiers('numrows') . "\nFROM (\n" . $this->compileSelect() . "\n) CI_count_all_results" @@ -1575,6 +1596,9 @@ public function countAllResults(bool $reset = true, bool $test = false) $this->QBOrderBy = $orderBy ?? []; } + // Restore the LIMIT setting + $this->QBLimit = $limit; + $row = (! $result instanceof ResultInterface) ? null : $result->getRow(); diff --git a/system/Database/MigrationRunner.php b/system/Database/MigrationRunner.php index 205caba75abe..76331c8b0fb6 100644 --- a/system/Database/MigrationRunner.php +++ b/system/Database/MigrationRunner.php @@ -164,19 +164,11 @@ public function __construct(BaseConfig $config, $db = null) //-------------------------------------------------------------------- /** - * Migrate to a schema version + * Locate and run all new migrations * - * Calls each migration step required to get to the schema version of - * choice - * - * @param string $targetVersion Target schema version - * @param string|null $namespace * @param string|null $group - * - * @return mixed Current version string on success, FALSE on failure or no migrations are found - * @throws ConfigException */ - public function version(string $targetVersion, string $namespace = null, string $group = null) + public function latest(string $group = null) { if (! $this->enabled) { @@ -185,82 +177,194 @@ public function version(string $targetVersion, string $namespace = null, string $this->ensureTable(); - // Set Namespace if not null - if (! is_null($namespace)) - { - $this->setNamespace($namespace); - } - // Set database group if not null if (! is_null($group)) { $this->setGroup($group); } + // Locate the migrations $migrations = $this->findMigrations(); + // If nothing was found then we're done if (empty($migrations)) { return true; } - // Get Namespace current version - // Note: We use strings, so that timestamp versions work on 32-bit systems - $currentVersion = $this->getVersion(); + // Remove any migrations already in the history + foreach ($this->getHistory($this->group) as $history) + { + unset($migrations[$this->getObjectUid($history)]); + } + + // Start a new batch + $batch = $this->getLastBatch() + 1; + + // Run each migration + foreach ($migrations as $migration) + { + if ($this->migrate('up', $migration)) + { + $this->addHistory($migration, $batch); + } + // If a migration failed then try to back out what was done + else + { + $this->regress(-1); - list($method, $migrations) = $this->determineDirection($targetVersion, $currentVersion, $migrations); + $message = lang('Migrations.generalFault'); - // Check Migration consistency - $migrationStatus = $this->migrate($method, $migrations, $targetVersion); + if ($this->silent) + { + $this->cliMessages[] = "\t" . CLI::color($message, 'red'); + return false; + } + throw new \RuntimeException($message); + } + } - return ($migrationStatus) ? $targetVersion : false; + return true; } //-------------------------------------------------------------------- /** - * Sets the schema to the latest migration + * Migrate down to a previous batch * - * @param string|null $namespace + * Calls each migration step required to get to the provided batch + * + * @param integer $targetBatch Target batch number, or negative for a relative batch, 0 for all * @param string|null $group * - * @return mixed Current version string on success, FALSE on failure + * @return mixed Current batch number on success, FALSE on failure or no migrations are found + * @throws ConfigException */ - public function latest(string $namespace = null, string $group = null) + public function regress(int $targetBatch = 0, string $group = null) { - $this->ensureTable(); - - // Set Namespace if not null - if (! is_null($namespace)) + if (! $this->enabled) { - $this->setNamespace($namespace); + throw ConfigException::forDisabledMigrations(); } + // Set database group if not null if (! is_null($group)) { $this->setGroup($group); } - $migrations = $this->findMigrations(); + $this->ensureTable(); - $lastMigration = end($migrations)->version ?? 0; + // Get all the batches + $batches = $this->getBatches(); - // Calculate the last migration step from existing migration - // filenames and proceed to the standard version migration - return $this->version($lastMigration); + // Convert a relative batch to its absolute + if ($targetBatch < 0) + { + $targetBatch = $batches[count($batches) - 1 + $targetBatch] ?? 0; + } + + // If the goal was rollback then check if it is done + if (empty($batches) && $targetBatch === 0) + { + return true; + } + + // Make sure $targetBatch is found + if ($targetBatch !== 0 && ! in_array($targetBatch, $batches)) + { + $message = lang('Migrations.batchNotFound') . $targetBatch; + + if ($this->silent) + { + $this->cliMessages[] = "\t" . CLI::color($message, 'red'); + return false; + } + throw new \RuntimeException($message); + } + + // Get all migrations + $this->namespace = null; + $allMigrations = $this->findMigrations(); + + // Gather migrations down through each batch until reaching the target + $migrations = []; + while ($batch = array_pop($batches)) + { + // Check if reached target + if ($batch <= $targetBatch) + { + break; + } + + // Get the migrations from each history + foreach ($this->getBatchHistory($batch) as $history) + { + // Create a UID from the history to match its migration + $uid = $this->getObjectUid($history); + + // Make sure the migration is still available + if (! isset($allMigrations[$uid])) + { + $message = lang('Migrations.gap') . ' ' . $history->version; + + if ($this->silent) + { + $this->cliMessages[] = "\t" . CLI::color($message, 'red'); + return false; + } + throw new \RuntimeException($message); + } + + // Add the history and put it on the list + $migration = $allMigrations[$uid]; + $migration->history = $history; + $migrations[] = $migration; + } + } + + // Run each migration + foreach ($migrations as $migration) + { + if ($this->migrate('down', $migration)) + { + $this->removeHistory($migration->history); + } + // If a migration failed then quit so as not to ruin the whole batch + else + { + $message = lang('Migrations.generalFault'); + + if ($this->silent) + { + $this->cliMessages[] = "\t" . CLI::color($message, 'red'); + return false; + } + throw new \RuntimeException($message); + } + } + + return true; } //-------------------------------------------------------------------- /** - * Sets the schema to the latest migration for all namespaces + * Migrate a single file regardless of order or batches. + * Method "up" or "down" determined by presence in history. + * NOTE: This is not recommended and provided mostly for testing. * + * @param string $path Full path to a valid migration file + * @param string $path Namespace of the target migration * @param string|null $group - * - * @return boolean */ - public function latestAll(string $group = null): bool + public function force(string $path, string $namespace, string $group = null) { + if (! $this->enabled) + { + throw ConfigException::forDisabledMigrations(); + } + $this->ensureTable(); // Set database group if not null @@ -269,38 +373,102 @@ public function latestAll(string $group = null): bool $this->setGroup($group); } - // Get all namespaces from the autoloader - $namespaces = Services::autoloader()->getNamespace(); + // Create and validate the migration + $migration = $this->migrationFromFile($path, $namespace); + if (empty($migration)) + { + $message = lang('Migrations.notFound'); - // Collect the migrations to run - $migrations = []; + if ($this->silent) + { + $this->cliMessages[] = "\t" . CLI::color($message, 'red'); + return false; + } + throw new \RuntimeException($message); + } + + // Check the history for a match + $method = 'up'; + $this->setNamespace($migration->namespace); + foreach ($this->getHistory($this->group) as $history) + { + if ($this->getObjectUid($history) === $migration->uid) + { + $method = 'down'; + $migration->history = $history; + break; + } + } - foreach ($namespaces as $namespace => $paths) + // up + if ($method === 'up') { - $this->setNamespace($namespace); - $nsMigrations = $this->findMigrations(); + // Start a new batch + $batch = $this->getLastBatch() + 1; - if (empty($nsMigrations)) + if ($this->migrate('up', $migration)) { - continue; + $this->addHistory($migration, $batch); + return true; } + } - $migrations = array_merge($migrations, $nsMigrations); + // down + elseif ($this->migrate('down', $migration)) + { + $this->removeHistory($migration->history); + return true; } - $migrationStatus = $this->migrate('up', $migrations, end($migrations)->version); + // If it came this far the migration failed + $message = lang('Migrations.generalFault'); - return $migrationStatus; + if ($this->silent) + { + $this->cliMessages[] = "\t" . CLI::color($message, 'red'); + return false; + } + throw new \RuntimeException($message); } //-------------------------------------------------------------------- /** - * Retrieves list of available migration scripts for one namespace + * Retrieves list of available migration scripts * - * @return array list of migrations as $version for one namespace + * @return array List of all located migrations by their UID */ public function findMigrations(): array + { + // If a namespace is set then use it, otherwise load all namespaces from the autoloader + $namespaces = $this->namespace ? [$this->namespace] : array_keys(Services::autoloader()->getNamespace()); + + // Collect the migrations to run by their sortable UID + $migrations = []; + foreach ($namespaces as $namespace) + { + foreach ($this->findNamespaceMigrations($namespace) as $migration) + { + $migrations[$migration->uid] = $migration; + } + } + + // Sort migrations ascending by their UID (version) + ksort($migrations); + + return $migrations; + } + + //-------------------------------------------------------------------- + + /** + * Retrieves a list of available migration scripts for one namespace + * + * @param string $namespace The namespace to search for migrations + * + * @return array List of unsorted migrations from the namespace + */ + public function findNamespaceMigrations(string $namespace): array { $migrations = []; $locator = Services::locator(true); @@ -315,132 +483,79 @@ public function findMigrations(): array // Otherwise use FileLocator to search files in the subdirectory of the namespace else { - $files = $locator->listNamespaceFiles($this->namespace, '/Database/Migrations/'); + $files = $locator->listNamespaceFiles($namespace, '/Database/Migrations/'); } // Load all *_*.php files in the migrations path // We can't use glob if we want it to be testable.... foreach ($files as $file) { - if (substr($file, -4) !== '.php') - { - continue; - } - - // Remove the extension - $name = basename($file, '.php'); + // Clean up the file path + $file = empty($this->path) ? $file : $this->path . str_replace($this->path, '', $file); - // Filter out non-migration files - if (preg_match($this->regex, $name)) + // Create the migration object from the file and save it + if ($migration = $this->migrationFromFile($file, $namespace)) { - // Create migration object using stdClass - $migration = new \stdClass(); - - // Get migration version number - $migration->version = $this->getMigrationNumber($name); - $migration->name = $this->getMigrationName($name); - $migration->path = ! empty($this->path) && strpos($file, $this->path) !== 0 - ? $this->path . $file - : $file; - $migration->class = $locator->getClassname($file); - - // Add to migrations[version] - $migrations[$migration->version] = $migration; + $migrations[] = $migration; } } - ksort($migrations); - return $migrations; } //-------------------------------------------------------------------- /** - * checks if the list of available migration scripts list are consistent - * if timestamp check if consistent with migrations table if downgrading + * Create a migration object from a file path. * - * @param array $migrations - * @param string $method - * @param string $targetVersion + * @param string $path The path to the file + * @param string $path The namespace of the target migration * - * @return boolean + * @return object|false Returns the migration object, or false on failure */ - protected function checkMigrations(array $migrations, string $method, string $targetVersion): bool + protected function migrationFromFile(string $path, string $namespace) { - // Check if no migrations found - if (empty($migrations)) + if (substr($path, -4) !== '.php') { - if ($this->silent) - { - return false; - } - throw new \RuntimeException(lang('Migrations.empty')); + return false; } - // Check if $targetVersion file is found - if ((int)$targetVersion !== 0 && ! array_key_exists($targetVersion, $migrations)) + // Remove the extension + $name = basename($path, '.php'); + + // Filter out non-migration files + if (! preg_match($this->regex, $name)) { - if ($this->silent) - { - return false; - } - throw new \RuntimeException(lang('Migrations.notFound') . $targetVersion); + return false; } - ksort($migrations); + $locator = Services::locator(true); - if ($method === 'down') - { - $history_migrations = $this->getHistory($this->group); - $history_size = count($history_migrations) - 1; - } - // Check for sequence gaps - $loop = 0; + // Create migration object using stdClass + $migration = new \stdClass(); - foreach ($migrations as $migration) - { - // Check if all old migration files are all available to do downgrading - if ($method === 'down') - { - if ($loop <= $history_size && $history_migrations[$loop]['version'] !== $migration->version) - { - throw new \RuntimeException(lang('Migrations.gap') . ' ' . $migration->version); - } - } - $loop++; - } + // Get migration version number + $migration->version = $this->getMigrationNumber($name); + $migration->name = $this->getMigrationName($name); + $migration->path = $path; + $migration->class = $locator->getClassname($path); + $migration->namespace = $namespace; + $migration->uid = $this->getObjectUid($migration); - return true; + return $migration; } //-------------------------------------------------------------------- - /** - * Sets the path to the base directory that will be used - * when locating migrations. If left null, the value will - * be chosen from $this->namespace's directory. - * - * @param string|null $path - * - * @return $this - */ - public function setPath(string $path = null) - { - $this->path = $path; - - return $this; - } - /** * Set namespace. * Allows other scripts to modify on the fly as needed. * - * @param string $namespace + * @param string $namespace or null for "all" * * @return MigrationRunner */ - public function setNamespace(string $namespace) + public function setNamespace(?string $namespace) { $this->namespace = $namespace; @@ -482,33 +597,6 @@ public function setName(string $name) //-------------------------------------------------------------------- - /** - * Grabs the full migration history from the database. - * - * @param string $group - * - * @return array - */ - public function getHistory(string $group = 'default'): array - { - $this->ensureTable(); - - $query = $this->db->table($this->table) - ->where('group', $group) - ->where('namespace', $this->namespace) - ->orderBy('version', 'ASC') - ->get(); - - if (! $query) - { - return []; - } - - return $query->getResultArray(); - } - - //-------------------------------------------------------------------- - /** * If $silent == true, then will not throw exceptions and will * attempt to continue gracefully. @@ -560,28 +648,22 @@ protected function getMigrationName(string $migration): string //-------------------------------------------------------------------- /** - * Retrieves current schema version + * Uses the non-repeatable portions of a migration or history + * to create a sortable unique key + * + * @param object $migration or $history * - * @return string Current migration version + * @return string */ - protected function getVersion(): string + public function getObjectUid($object): string { - $this->ensureTable(); - - $row = $this->db->table($this->table) - ->select('version') - ->where('group', $this->group) - ->where('namespace', $this->namespace) - ->orderBy('version', 'DESC') - ->get(); - - return $row && ! is_null($row->getRow()) ? $row->getRow()->version : '0'; + return $object->version . $object->class; } //-------------------------------------------------------------------- /** - * Retrieves current schema version + * Retrieves messages formatted for CLI output * * @return array Current migration version */ @@ -593,34 +675,59 @@ public function getCliMessages(): array //-------------------------------------------------------------------- /** - * Stores the current schema version. + * Clears any CLI messages. * - * @param string $version - * @param integer $batch + * @return MigrationRunner + */ + public function clearCliMessages() + { + $this->cliMessages = []; + + return $this; + } + + //-------------------------------------------------------------------- + + /** + * Truncates the history table. * - * @return void - * @internal param string $migration Migration reached + * @return boolean */ - protected function addHistory(string $version, int $batch = null) + public function clearHistory() { - if (empty($batch)) + if ($this->db->tableExists($this->table)) { - $batch = $this->getLastBatch() + 1; + $this->db->table($this->table) + ->truncate(); } + } + //-------------------------------------------------------------------- + + /** + * Add a history to the table. + * + * @param object $migration + * @param integer $batch + * + * @return void + */ + protected function addHistory($migration, int $batch) + { $this->db->table($this->table) ->insert([ - 'version' => $version, - 'name' => $this->name, + 'version' => $migration->version, + 'class' => $migration->class, 'group' => $this->group, - 'namespace' => $this->namespace, + 'namespace' => $migration->namespace, 'time' => time(), 'batch' => $batch, ]); + if (is_cli()) { $this->cliMessages[] = "\t" . CLI::color(lang('Migrations.added'), - 'yellow') . "($this->namespace) " . $version . '_' . $this->name; + 'yellow') . "($migration->namespace) " . $migration->version . '_' . $migration->class; } } @@ -633,35 +740,86 @@ protected function addHistory(string $version, int $batch = null) * * @return void */ - protected function removeHistory(string $version) + protected function removeHistory($history) { - $this->db->table($this->table) - ->where('version', $version) - ->where('group', $this->group) - ->where('namespace', $this->namespace) - ->delete(); + $this->db->table($this->table)->where('id', $history->id)->delete(); if (is_cli()) { $this->cliMessages[] = "\t" . CLI::color(lang('Migrations.removed'), - 'yellow') . "($this->namespace) " . $version . '_' . $this->name; + 'yellow') . "($history->namespace) " . $history->version . '_' . $this->getMigrationName($history->class); } } //-------------------------------------------------------------------- /** - * Truncates the history table. + * Grabs the full migration history from the database for a group * - * @return boolean + * @param string $group + * + * @return array */ - public function clearHistory() + public function getHistory(string $group = 'default'): array { - if ($this->db->tableExists($this->table)) + $this->ensureTable(); + + $criteria = ['group' => $group]; + + // If a namespace was specified then use it + if ($this->namespace) { - $this->db->table($this->table) - ->truncate(); + $criteria['namespace'] = $this->namespace; } + + $query = $this->db->table($this->table) + ->where($criteria) + ->orderBy('id', 'ASC') + ->get(); + + return $query ? $query->getResultObject() : []; + } + + //-------------------------------------------------------------------- + + /** + * Returns the migration history for a single batch. + * + * @param integer $batch + * + * @return array + */ + public function getBatchHistory(int $batch): array + { + $this->ensureTable(); + + $query = $this->db->table($this->table) + ->where('batch', $batch) + ->orderBy('id', 'asc') + ->get(); + + return $query ? $query->getResultObject() : []; + } + + //-------------------------------------------------------------------- + + /** + * Returns all the batches from the database history in order + * + * @return array + */ + public function getBatches(): array + { + $this->ensureTable(); + + $batches = $this->db->table($this->table) + ->select('batch') + ->distinct() + ->orderBy('batch', 'asc') + ->get() + ->getResultArray(); + + return array_column($batches, 'batch'); } //-------------------------------------------------------------------- @@ -691,6 +849,7 @@ public function getLastBatch(): int /** * Returns the version number of the first migration for a batch. + * Mostly just for tests. * * @param integer $batch * @@ -698,6 +857,13 @@ public function getLastBatch(): int */ public function getBatchStart(int $batch): string { + // Convert a relative batch to its absolute + if ($batch < 0) + { + $batches = $this->getBatches(); + $batch = $batches[count($batches) - 1 + $targetBatch] ?? 0; + } + $migration = $this->db->table($this->table) ->where('batch', $batch) ->orderBy('id', 'asc') @@ -712,6 +878,7 @@ public function getBatchStart(int $batch): string /** * Returns the version number of the last migration for a batch. + * Mostly just for tests. * * @param integer $batch * @@ -719,6 +886,13 @@ public function getBatchStart(int $batch): string */ public function getBatchEnd(int $batch): string { + // Convert a relative batch to its absolute + if ($batch < 0) + { + $batches = $this->getBatches(); + $batch = $batches[count($batches) - 1 + $targetBatch] ?? 0; + } + $migration = $this->db->table($this->table) ->where('batch', $batch) ->orderBy('id', 'desc') @@ -778,6 +952,7 @@ public function ensureTable() 'batch' => [ 'type' => 'INT', 'constraint' => 11, + 'unsigned' => true, 'null' => false, ], ]); @@ -789,134 +964,50 @@ public function ensureTable() } /** - * @param string $targetVersion - * @param string $currentVersion - * @param array $migrations + * Handles the actual running of a migration. * - * @return array - */ - protected function determineDirection(string $targetVersion, string $currentVersion, array &$migrations): array - { - if ($targetVersion > $currentVersion) - { - // Moving Up - $method = 'up'; - ksort($migrations); - } - else - { - // Moving Down, apply in reverse order - $method = 'down'; - krsort($migrations); - } - - return [ - $method, - $migrations, - ]; - } - - //-------------------------------------------------------------------- - - /** - * Given an array of history items will either remove them - * of add them to the table. + * @param $direction "up" or "down" + * @param $migration The migration to run * - * @param array $histories - * @param string $method - * @param integer $batch + * @return boolean */ - protected function updateHistory(array $histories, string $method, int $batch) + protected function migrate($direction, $migration): bool { - if ($method === 'up') - { - $time = time(); + include_once $migration->path; - foreach ($histories as $history) - { - $this->db->table($this->table) - ->insert([ - 'version' => $history->version, - 'class' => $history->class, - 'group' => $this->group, - 'namespace' => $this->namespace, - 'time' => $time, - 'batch' => $batch, - ]); - } - } - elseif ($method === 'down') - { - $classes = []; + $class = $migration->class; + $this->setName($migration->name); - foreach ($histories as $history) - { - $classes[] = $history->class; - } + // Validate the migration file structure + if (! class_exists($class, false)) + { + $message = sprintf(lang('Migrations.classNotFound'), $class); - if (count($classes)) + if ($this->silent) { - $this->db->table($this->table) - ->whereIn('class', $classes) - ->delete(); + $this->cliMessages[] = "\t" . CLI::color($message, 'red'); + return false; } + throw new \RuntimeException($message); } - } - - /** - * Handles the actual running of migrations. - * - * @param $direction - * @param $migrations - * @param string $targetVersion - * - * @return boolean - */ - protected function migrate($direction, $migrations, string $targetVersion): bool - { - $this->checkMigrations($migrations, $direction, $targetVersion); - $batch = $this->getLastBatch() + 1; - $currentVersion = $this->getVersion(); + // Forcing migration to selected database group + $instance = new $class(\Config\Database::forge($this->group)); - // loop migration for each namespace (module) - $migrationStatus = false; - $history = []; - foreach ($migrations as $version => $migration) + if (! is_callable([$instance, $direction])) { - // Only include migrations within the scope - if (($direction === 'up' && $version > $currentVersion && $version <= $targetVersion) || ($direction === 'down' && $version <= $currentVersion && $version >= $targetVersion)) - { - $migrationStatus = false; - include_once $migration->path; - - $class = $migration->class; - $this->setName($migration->name); - - // Validate the migration file structure - if (! class_exists($class, false)) - { - throw new \RuntimeException(sprintf(lang('Migrations.classNotFound'), $class)); - } - - // Forcing migration to selected database group - $instance = new $class(\Config\Database::forge($this->group)); + $message = sprintf(lang('Migrations.missingMethod'), $direction); - if (! is_callable([$instance, $direction])) - { - throw new \RuntimeException(sprintf(lang('Migrations.missingMethod'), $direction)); - } - - $instance->{$direction}(); - - $history[] = $migration; - - $migrationStatus = true; + if ($this->silent) + { + $this->cliMessages[] = "\t" . CLI::color($message, 'red'); + return false; } + throw new \RuntimeException($message); } - $this->updateHistory($history, $direction, $batch); + $instance->{$direction}(); - return $migrationStatus; + return true; } } diff --git a/system/HTTP/Response.php b/system/HTTP/Response.php index a9b21c5c20a4..d9c02f59759b 100644 --- a/system/HTTP/Response.php +++ b/system/HTTP/Response.php @@ -689,8 +689,8 @@ public function send() } $this->sendHeaders(); - $this->sendBody(); $this->sendCookies(); + $this->sendBody(); return $this; } diff --git a/system/Images/Handlers/ImageMagickHandler.php b/system/Images/Handlers/ImageMagickHandler.php index f928b2868946..3adf8e7566c8 100644 --- a/system/Images/Handlers/ImageMagickHandler.php +++ b/system/Images/Handlers/ImageMagickHandler.php @@ -232,9 +232,10 @@ public function getVersion(): string * @param string $action * @param integer $quality * - * @return ImageMagickHandler|boolean + * @return array Lines of output from shell command + * @throws \Exception */ - protected function process(string $action, int $quality = 100) + protected function process(string $action, int $quality = 100): array { // Do we have a vaild library path? if (empty($this->config->libraryPath)) @@ -303,8 +304,8 @@ public function save(string $target = null, int $quality = 90): bool $result = $this->process($action, $quality); unlink($this->resource); - - return $result; + + return true; } //-------------------------------------------------------------------- diff --git a/system/Language/en/Migrations.php b/system/Language/en/Migrations.php index 1c3624a32ec0..8e3ca49d1a99 100644 --- a/system/Language/en/Migrations.php +++ b/system/Language/en/Migrations.php @@ -19,6 +19,7 @@ 'missingTable' => 'Migrations table must be set.', 'disabled' => 'Migrations have been loaded but are disabled or setup incorrectly.', 'notFound' => 'Migration file not found: ', + 'batchNotFound' => 'Target batch not found: ', 'empty' => 'No Migration files found', 'gap' => 'There is a gap in the migration sequence near version number: ', 'classNotFound' => 'The migration class "%s" could not be found.', @@ -37,11 +38,12 @@ 'writeError' => 'Error trying to create file.', 'migNumberError' => 'Migration number must be three digits, and there must not be any gaps in the sequence.', - 'toLatest' => 'Migrating to latest version...', + 'latest' => 'Running all new migrations...', + 'generalFault' => 'Migration failed!', 'migInvalidVersion' => 'Invalid version number provided.', 'toVersionPH' => 'Migrating to version %s...', 'toVersion' => 'Migrating to current version...', - 'rollingBack' => 'Rolling back all migrations...', + 'rollingBack' => 'Rolling back migrations to batch: ', 'noneFound' => 'No migrations were found.', 'on' => 'Migrated On: ', 'migSeeder' => 'Seeder name', diff --git a/system/Test/CIDatabaseTestCase.php b/system/Test/CIDatabaseTestCase.php index aadab3713137..aeedb8eecb17 100644 --- a/system/Test/CIDatabaseTestCase.php +++ b/system/Test/CIDatabaseTestCase.php @@ -37,6 +37,8 @@ namespace CodeIgniter\Test; +use CodeIgniter\Config\Config; +use Config\Autoload; use Config\Database; use Config\Migrations; use Config\Services; @@ -67,20 +69,19 @@ class CIDatabaseTestCase extends CIUnitTestCase protected $seed = ''; /** - * The path to where we can find the migrations - * and seeds directories. Allows overriding - * the default application directories. + * The path to where we can find the seeds directory. + * Allows overriding the default application directories. * * @var string */ protected $basePath = TESTPATH . '_support/Database'; /** - * The namespace to help us fird the migration classes. + * The namespace to help us find the migration classes. * * @var string */ - protected $namespace = 'Tests\Support'; + protected $namespace = 'Tests\Support\DatabaseTestMigrations'; /** * The name of the database group to connect to. @@ -161,6 +162,9 @@ protected function setUp() { parent::setUp(); + // Add namespaces we need for testing + Services::autoloader()->addNamespace('Tests\Support\DatabaseTestMigrations', TESTPATH . '_support/DatabaseTestMigrations'); + $this->loadDependencies(); if ($this->refresh === true) @@ -188,8 +192,8 @@ protected function setUp() } } - $this->migrations->version(0, null, 'tests'); - $this->migrations->latest(null, 'tests'); + $this->migrations->regress(0, 'tests'); + $this->migrations->latest('tests'); } if (! empty($this->seed)) diff --git a/system/bootstrap.php b/system/bootstrap.php index 780c4b0b2c09..f997e929cbfa 100644 --- a/system/bootstrap.php +++ b/system/bootstrap.php @@ -96,6 +96,13 @@ require_once APPPATH . 'Config/Constants.php'; } +// Let's see if an app/Common.php file exists +if (file_exists(APPPATH . 'Common.php')) +{ + require_once APPPATH . 'Common.php'; +} + +// Require system/Common.php require_once SYSTEMPATH . 'Common.php'; /* diff --git a/tests/_support/Database/Migrations/20160428212500_Create_test_tables.php b/tests/_support/DatabaseTestMigrations/Database/Migrations/20160428212500_Create_test_tables.php similarity index 97% rename from tests/_support/Database/Migrations/20160428212500_Create_test_tables.php rename to tests/_support/DatabaseTestMigrations/Database/Migrations/20160428212500_Create_test_tables.php index b980a80acfbc..bcd8effaf827 100644 --- a/tests/_support/Database/Migrations/20160428212500_Create_test_tables.php +++ b/tests/_support/DatabaseTestMigrations/Database/Migrations/20160428212500_Create_test_tables.php @@ -1,4 +1,4 @@ -forge->dropColumn('foo', 'value'); + if ($this->db->tableExists('foo')) + { + $this->forge->dropColumn('foo', 'value'); + } } } diff --git a/tests/system/Database/Builder/SelectTest.php b/tests/system/Database/Builder/SelectTest.php index 1623149e7317..0ac004695747 100644 --- a/tests/system/Database/Builder/SelectTest.php +++ b/tests/system/Database/Builder/SelectTest.php @@ -199,6 +199,32 @@ public function testSelectSumWithAlias() //-------------------------------------------------------------------- + public function testSelectCountWithNoAlias() + { + $builder = new BaseBuilder('invoices', $this->db); + + $builder->selectCount('payments'); + + $expected = 'SELECT COUNT("payments") AS "payments" FROM "invoices"'; + + $this->assertEquals($expected, str_replace("\n", ' ', $builder->getCompiledSelect())); + } + + //-------------------------------------------------------------------- + + public function testSelectCountWithAlias() + { + $builder = new BaseBuilder('invoices', $this->db); + + $builder->selectCount('payments', 'myAlias'); + + $expected = 'SELECT COUNT("payments") AS "myAlias" FROM "invoices"'; + + $this->assertEquals($expected, str_replace("\n", ' ', $builder->getCompiledSelect())); + } + + //-------------------------------------------------------------------- + public function testSelectMinThrowsExceptionOnEmptyValue() { $builder = new BaseBuilder('invoices', $this->db); diff --git a/tests/system/Database/Live/GroupTest.php b/tests/system/Database/Live/GroupTest.php index 1c735d5da2b7..e58de1ddf226 100644 --- a/tests/system/Database/Live/GroupTest.php +++ b/tests/system/Database/Live/GroupTest.php @@ -123,4 +123,15 @@ public function testOrNotGroups() //-------------------------------------------------------------------- + public function testGroupByCount() + { + $result = $this->db->table('user') + ->selectCount('id', 'count') + ->groupBy('country') + ->orderBy('country', 'desc') + ->get() + ->getResult(); + + $this->assertEquals(2, $result[0]->count); + } } diff --git a/tests/system/Database/Live/SelectTest.php b/tests/system/Database/Live/SelectTest.php index d6d45cb22e18..47699afa8353 100644 --- a/tests/system/Database/Live/SelectTest.php +++ b/tests/system/Database/Live/SelectTest.php @@ -91,7 +91,7 @@ public function testSelectAvg() //-------------------------------------------------------------------- - public function testSelectAvgWitAlias() + public function testSelectAvgWithAlias() { $result = $this->db->table('job')->selectAvg('id', 'xam')->get()->getRow(); @@ -109,7 +109,7 @@ public function testSelectSum() //-------------------------------------------------------------------- - public function testSelectSumWitAlias() + public function testSelectSumWithAlias() { $result = $this->db->table('job')->selectSum('id', 'xam')->get()->getRow(); @@ -118,6 +118,24 @@ public function testSelectSumWitAlias() //-------------------------------------------------------------------- + public function testSelectCount() + { + $result = $this->db->table('job')->selectCount('id')->get()->getRow(); + + $this->assertEquals(4, $result->id); + } + + //-------------------------------------------------------------------- + + public function testSelectCountWithAlias() + { + $result = $this->db->table('job')->selectCount('id', 'xam')->get()->getRow(); + + $this->assertEquals(4, $result->xam); + } + + //-------------------------------------------------------------------- + public function testSelectDistinctWorkTogether() { $users = $this->db->table('user')->select('country')->distinct()->get()->getResult(); diff --git a/tests/system/Database/Migrations/MigrationRunnerTest.php b/tests/system/Database/Migrations/MigrationRunnerTest.php index 1eb89e2e06bf..2fdcc8b9e747 100644 --- a/tests/system/Database/Migrations/MigrationRunnerTest.php +++ b/tests/system/Database/Migrations/MigrationRunnerTest.php @@ -5,6 +5,7 @@ use org\bovigo\vfs\vfsStream; use CodeIgniter\Test\CIDatabaseTestCase; use org\bovigo\vfs\visitor\vfsStreamStructureVisitor; +use Config\Services; /** * @group DatabaseLive @@ -25,6 +26,8 @@ public function setUp() $this->start = $this->root->url() . '/'; $this->config = new Migrations(); $this->config->enabled = true; + + Services::autoloader()->addNamespace('Tests\Support\MigrationTestMigrations', TESTPATH . '_support/MigrationTestMigrations'); } public function testLoadsDefaultDatabaseWhenNoneSpecified() @@ -72,7 +75,7 @@ public function testGetHistory() $this->hasInDatabase('migrations', $history); - $this->assertEquals($history, $runner->getHistory()[0]); + $this->assertEquals($history, (array) $runner->getHistory()[0]); } public function testGetHistoryReturnsEmptyArrayWithNoResults() @@ -162,7 +165,7 @@ public function testFindMigrationsReturnsEmptyArrayWithNoneFound() $config->type = 'timestamp'; $runner = new MigrationRunner($config); - $runner->setPath($this->start); + // $runner->setPath($this->start); $this->assertEquals([], $runner->findMigrations()); } @@ -173,20 +176,26 @@ public function testFindMigrationsSuccessTimestamp() $config->type = 'timestamp'; $runner = new MigrationRunner($config); - $runner = $runner->setPath(TESTPATH . '_support/Database/SupportMigrations'); - - $mig1 = (object)[ - 'name' => 'Some_migration', - 'path' => TESTPATH . '_support/Database/SupportMigrations/2018-01-24-102301_Some_migration.php', - 'version' => '2018-01-24-102301', - 'class' => 'App\Database\Migrations\Migration_some_migration', - ]; - $mig2 = (object)[ - 'name' => 'Another_migration', - 'path' => TESTPATH . '_support/Database/SupportMigrations/2018-01-24-102302_Another_migration.php', - 'version' => '2018-01-24-102302', - 'class' => 'App\Database\Migrations\Migration_another_migration', - ]; + $runner = $runner->setNamespace('Tests\Support\MigrationTestMigrations'); + + $mig1 = (object)[ + 'name' => 'Some_migration', + 'path' => TESTPATH . '_support/MigrationTestMigrations/Database/Migrations/2018-01-24-102301_Some_migration.php', + 'version' => '2018-01-24-102301', + 'class' => 'Tests\Support\MigrationTestMigrations\Database\Migrations\Migration_some_migration', + 'namespace' => 'Tests\Support\MigrationTestMigrations', + ]; + $mig1->uid = $runner->getObjectUid($mig1); + + $mig2 = (object)[ + 'name' => 'Another_migration', + 'path' => TESTPATH . '_support/MigrationTestMigrations/Database/Migrations/2018-01-24-102302_Another_migration.php', + 'version' => '2018-01-24-102302', + 'class' => 'Tests\Support\MigrationTestMigrations\Database\Migrations\Migration_another_migration', + 'namespace' => 'Tests\Support\MigrationTestMigrations', + 'uid' => '2018-01-24-102302Tests\Support\MigrationTestMigrations\Database\Migrations\Migration_another_migration', + ]; + $mig1->uid = $runner->getObjectUid($mig1); $migrations = $runner->findMigrations(); @@ -207,17 +216,17 @@ public function testMigrationThrowsDisabledException() $runner->setSilent(false); - $runner = $runner->setPath($this->start); + $runner = $runner->setNamespace('Tests\Support\MigrationTestMigrations'); vfsStream::copyFromFileSystem( - TESTPATH . '_support/Database/SupportMigrations', + TESTPATH . '_support/MigrationTestMigrations/Database/Migrations', $this->root ); $this->expectException(ConfigException::class); $this->expectExceptionMessage('Migrations have been loaded but are disabled or setup incorrectly.'); - $runner->version(1); + $runner->latest(); } public function testVersionReturnsUpDownSuccess() @@ -230,29 +239,32 @@ public function testVersionReturnsUpDownSuccess() $runner->setSilent(false); $runner->clearHistory(); - $runner = $runner->setPath(TESTPATH . '_support/Database/SupportMigrations'); + $runner = $runner->setNamespace('Tests\Support\MigrationTestMigrations'); - $version = $runner->version('2018-01-24-102301'); + $runner->latest(); + $version = $runner->getBatchEnd($runner->getLastBatch()); - $this->assertEquals('2018-01-24-102301', $version); + $this->assertEquals('2018-01-24-102302', $version); $this->seeInDatabase('foo', ['key' => 'foobar']); - $version = $runner->version(0); + $runner->regress(0); + $version = $runner->getBatchEnd($runner->getLastBatch()); $this->assertEquals('0', $version); $this->assertFalse($this->db->tableExists('foo')); } - public function testLatestSuccess() + public function testProgressSuccess() { $config = $this->config; $runner = new MigrationRunner($config); $runner->setSilent(false); $runner->clearHistory(); - $runner = $runner->setPath(TESTPATH . '_support/Database/SupportMigrations'); + $runner = $runner->setNamespace('Tests\Support\MigrationTestMigrations'); - $version = $runner->latest(); + $runner->latest(); + $version = $runner->getBatchEnd($runner->getLastBatch()); $this->assertEquals('2018-01-24-102302', $version); $this->assertTrue(db_connect()->tableExists('foo')); @@ -262,16 +274,17 @@ public function testLatestSuccess() ]); } - public function testVersionReturnsDownSuccess() + public function testRegressSuccess() { $config = $this->config; $runner = new MigrationRunner($config); $runner->setSilent(false); - $runner = $runner->setPath(TESTPATH . '_support/Database/SupportMigrations'); + $runner = $runner->setNamespace('Tests\Support\MigrationTestMigrations'); $runner->latest(); - $version = $runner->version(0); + $runner->regress(); + $version = $runner->getBatchEnd($runner->getLastBatch()); $this->assertEquals(0, $version); $this->assertFalse(db_connect()->tableExists('foo')); @@ -288,23 +301,19 @@ public function testHistoryRecordsBatches() $runner->clearHistory(); $this->resetTables(); - $runner = $runner->setPath(TESTPATH . '_support/Database/SupportMigrations'); - - $version = $runner->version('2018-01-24-102301'); + $runner = $runner->setNamespace('Tests\Support\MigrationTestMigrations'); - $this->assertEquals('2018-01-24-102301', $version); - - $history = $runner->getHistory('tests'); - $this->assertEquals(1, $history[0]['batch']); - - $version = $runner->version('2018-01-24-102302'); + $runner->latest(); + $version = $runner->getBatchEnd($runner->getLastBatch()); $this->assertEquals('2018-01-24-102302', $version); $history = $runner->getHistory('tests'); - $this->assertEquals(1, $history[0]['batch']); - $this->assertEquals(2, $history[1]['batch']); + $this->assertEquals(1, $history[0]->batch); + + $this->assertEquals(1, $history[0]->batch); + $this->assertEquals(1, $history[1]->batch); $this->seeInDatabase('migrations', [ 'batch' => 1, @@ -319,7 +328,7 @@ public function testGetBatchVersions() $runner->clearHistory(); $this->resetTables(); - $runner = $runner->setPath(TESTPATH . '_support/Database/SupportMigrations'); + $runner = $runner->setNamespace('Tests\Support\MigrationTestMigrations'); $runner->latest(); diff --git a/user_guide_src/source/database/query_builder.rst b/user_guide_src/source/database/query_builder.rst index 6bf1efcc6739..63b622fb3161 100755 --- a/user_guide_src/source/database/query_builder.rst +++ b/user_guide_src/source/database/query_builder.rst @@ -172,6 +172,20 @@ Writes a "SELECT SUM(field)" portion for your query. As with selectMax(), You can optionally include a second parameter to rename the resulting field. +:: + + $builder->selectSum('age'); + $query = $builder->get(); // Produces: SELECT SUM(age) as age FROM mytable + +**$builder->selectCount()** + +Writes a "SELECT COUNT(field)" portion for your query. As with +selectMax(), You can optionally include a second parameter to rename +the resulting field. + +.. note:: This method is particularly helpful when used with ``groupBy()``. For +counting results generally see ``countAll()`` or ``countAllResults()``. + :: $builder->selectSum('age'); diff --git a/user_guide_src/source/dbmgmt/migration.rst b/user_guide_src/source/dbmgmt/migration.rst index 8473758c60b1..585daa6968e3 100644 --- a/user_guide_src/source/dbmgmt/migration.rst +++ b/user_guide_src/source/dbmgmt/migration.rst @@ -11,8 +11,8 @@ need to be run against the production machines next time you deploy. The database table **migration** tracks which migrations have already been run so all you have to do is make sure your migrations are in place and call ``$migration->latest()`` to bring the database up to the most recent -state. You can also use ``$migration->latestAll()`` to include migrations -from all namespaces. +state. You can also use ``$migration->setNamespace(null)->progess()`` to +include migrations from all namespaces. .. contents:: :local: @@ -185,7 +185,7 @@ that wish to use them. The tools primarily provide access to the same methods th **migrate** -Migrates all database groups to the latest available migrations:: +Migrates a database group with all available migrations:: > php spark migrate @@ -195,50 +195,36 @@ You can use (migrate) with the following options: - (-n) to choose namespace, otherwise (App) namespace will be used. - (-all) to migrate all namespaces to the latest migration -This example will migrate Blog namespace to latest version on the test database group:: +This example will migrate Blog namespace with any new migrations on the test database group:: > php spark migrate -g test -n Blog -**version** - -Migrates to the specified version. If no version is provided, you will be prompted -for the version. :: - - // Asks you for the version... - > php spark migrate:version - Version: - - // Timestamp - > php spark migrate:version 20161426211300 - -You can use (version) with the following options: - -- (-g) to chose database group, otherwise default database group will be used. -- (-n) to choose namespace, otherwise (App) namespace will be used. +When using the `-all` option, it will scan through all namespaces attempting to find any migrations that have +not been ran. These will all be collected and then sorted as a group by date created. This should help +to minimize any potential conflicts between the main application and any modules. **rollback** -Rolls back all migrations, taking all database groups to a blank slate, effectively migration 0:: +Rolls back all migrations, taking the database group to a blank slate, effectively migration 0:: > php spark migrate:rollback You can use (rollback) with the following options: -- (-g) to chose database group, otherwise default database group will be used. -- (-n) to choose namespace, otherwise (App) namespace will be used. -- (-all) to migrate all namespaces to the latest migration +- (-g) to choose database group, otherwise default database group will be used. +- (-b) to choose a batch: natural numbers specify the batch, negatives indicate a relative batch **refresh** -Refreshes the database state by first rolling back all migrations, and then migrating to the latest version:: +Refreshes the database state by first rolling back all migrations, and then migrating all:: > php spark migrate:refresh You can use (refresh) with the following options: -- (-g) to chose database group, otherwise default database group will be used. +- (-g) to choose database group, otherwise default database group will be used. - (-n) to choose namespace, otherwise (App) namespace will be used. -- (-all) to migrate all namespaces to the latest migration +- (-all) to refresh all namespaces **status** @@ -248,9 +234,9 @@ Displays a list of all migrations and the date and time they ran, or '--' if the Filename Migrated On First_migration.php 2016-04-25 04:44:22 -You can use (refresh) with the following options: +You can use (status) with the following options: -- (-g) to chose database group, otherwise default database group will be used. +- (-g) to choose database group, otherwise default database group will be used. **create** @@ -293,38 +279,37 @@ Class Reference An array of migration filenames are returned that are found in the **path** property. - .. php:method:: latest($namespace, $group) - - :param mixed $namespace: application namespace, if null (App) namespace will be used. - :param mixed $group: database group name, if null default database group will be used. - :returns: Current version string on success, FALSE on failure - :rtype: mixed - - This works much the same way as ``current()`` but instead of looking for - the ``$currentVersion`` the Migration class will use the very - newest migration found in the filesystem. - .. php:method:: latestAll($group) + .. php:method:: latest($group) :param mixed $group: database group name, if null default database group will be used. :returns: TRUE on success, FALSE on failure - :rtype: mixed + :rtype: bool + + This locates migrations for a namespace (or all namespaces), determines which migrations + have not yet been run, and runs them in order of their version (namespaces intermingled). - This works much the same way as ``latest()`` but instead of looking for - one namespace, the Migration class will use the very - newest migration found for all namespaces. - .. php:method:: version($target_version, $namespace, $group) + .. php:method:: regress($batch, $group) - :param mixed $namespace: application namespace, if null (App) namespace will be used. + :param mixed $batch: previous batch to migrate down to; 1+ specifies the batch, 0 reverts all, negative refers to the relative batch (e.g. -3 means "three batches back") :param mixed $group: database group name, if null default database group will be used. - :param mixed $target_version: Migration version to process - :returns: Current version string on success, FALSE on failure or no migrations are found - :rtype: mixed + :returns: TRUE on success, FALSE on failure or no migrations are found + :rtype: bool - Version can be used to roll back changes or step forwards programmatically to - specific versions. It works just like ``current()`` but ignores ``$currentVersion``. + Regress can be used to roll back changes to a previous state, batch by batch. :: - $migration->version(5); + $migration->batch(5); + $migration->batch(-1); + + .. php:method:: force($path, $namespace, $group) + + :param mixed $path: path to a valid migration file. + :param mixed $namespace: namespace of the provided migration. + :param mixed $group: database group name, if null default database group will be used. + :returns: TRUE on success, FALSE on failure + :rtype: bool + + This forces a single file to migrate regardless of order or batches. Method "up" or "down" is detected based on whether it has already been migrated. **Note**: This method is recommended only for testing and could cause data consistency issues. .. php:method:: setNamespace($namespace) diff --git a/user_guide_src/source/testing/database.rst b/user_guide_src/source/testing/database.rst index 8edfb5089f6a..a0df1c1c8b10 100644 --- a/user_guide_src/source/testing/database.rst +++ b/user_guide_src/source/testing/database.rst @@ -94,10 +94,15 @@ test data prior to every test running. **$basePath** -By default, CodeIgniter will look in **tests/_support/database/migrations** and **tests/_support_database/seeds** -to locate the migrations and seeds that it should run during testing. You can change this directory by specifying -the path in the ``$basePath`` property. This should not include the **migrations** or **seeds** directories, but -the path to the single directory that holds both of those sub-directories. +By default, CodeIgniter will look in **tests/_support/Database/Seeds** to locate the seeds that it should run during testing. +You can change this directores by specifying the ``$basePath`` property. This should not include the **seeds** directory, +but the path to the single directory that holds the sub-directory. + +**$namespace** + +By default, CodeIgniter will look in **tests/_support/DatabaseTestMigrations/Database/Migrations** to locate the migrations +that it should run during testing. You can change this location by specifying a new namespace in the ``$namespace`` properties. +This should not include the **Database/Migrations** path, just the base namespace. Helper Methods ==============