Skip to content
Snippets Groups Projects
Commit 98be3c32 authored by Elmar Ludwig's avatar Elmar Ludwig
Browse files

migration branches for 5.0

parent 8f1efe93
No related branches found
No related tags found
No related merge requests found
Showing
with 367 additions and 546 deletions
...@@ -17,8 +17,11 @@ class WebMigrateController extends StudipController ...@@ -17,8 +17,11 @@ class WebMigrateController extends StudipController
parent::before_filter($action, $args); parent::before_filter($action, $args);
DBSchemaVersion::validateSchemaVersion();
$this->target = Request::int('target'); $this->target = Request::int('target');
$this->version = new DBSchemaVersion('studip'); $this->branch = Request::get('branch', '0');
$this->version = new DBSchemaVersion('studip', $this->branch);
$this->migrator = new Migrator( $this->migrator = new Migrator(
"{$GLOBALS['STUDIP_BASE_PATH']}/db/migrations", "{$GLOBALS['STUDIP_BASE_PATH']}/db/migrations",
$this->version, $this->version,
...@@ -45,9 +48,7 @@ class WebMigrateController extends StudipController ...@@ -45,9 +48,7 @@ class WebMigrateController extends StudipController
$this->lock->lock(['timestamp' => time(), 'user_id' => $GLOBALS['user']->id]); $this->lock->lock(['timestamp' => time(), 'user_id' => $GLOBALS['user']->id]);
foreach (Request::getArray('versions') as $version) { $this->migrator->migrateTo($this->target);
$this->migrator->execute($version, 'up');
}
$this->lock->release(); $this->lock->release();
...@@ -78,51 +79,32 @@ class WebMigrateController extends StudipController ...@@ -78,51 +79,32 @@ class WebMigrateController extends StudipController
public function history_action() public function history_action()
{ {
$this->history = array_diff_key( $this->migrations = $this->migrator->relevantMigrations(0);
$this->migrator->relevantMigrations(0), $this->offset = -1;
$this->migrator->relevantMigrations(null) $this->target = 0;
); $this->render_action('index');
}
public function revert_action()
{
ob_start();
set_time_limit(0);
$this->lock->lock(['timestamp' => time(), 'user_id' => $GLOBALS['user']->id]);
foreach (Request::getArray('versions') as $version) {
$this->migrator->execute($version, 'down');
}
$this->lock->release();
$announcements = ob_get_clean();
PageLayout::postSuccess(
_('Die Datenbank wurde erfolgreich migriert.'),
array_filter(explode("\n", $announcements))
);
$_SESSION['migration-check'] = [
'timestamp' => time(),
'count' => 0,
];
$this->redirect('history');
} }
public function setupSidebar($action) public function setupSidebar($action)
{ {
$views = Sidebar::get()->addWidget(new ViewsWidget()); $views = Sidebar::get()->addWidget(new ViewsWidget());
$views->addLink( $views->addLink(
_('Offene Migrationen'), _('Migrationen ausführen'),
$this->url_for('index') $this->url_for('index', ['branch' => $this->branch])
)->setActive($action === 'index'); )->setActive($action === 'index');
$views->addLink( $views->addLink(
_('Ausgeführte Migrationen'), _('Migrationen zurücknehmen'),
$this->url_for('history') $this->url_for('history', ['branch' => $this->branch])
)->setActive($action === 'history'); )->setActive($action === 'history');
$widget = new SelectWidget(_('Branch'), $this->url_for($action), 'branch');
Sidebar::get()->addWidget($widget);
foreach ($this->version->getAllBranches() as $branch) {
$element = new SelectElement($branch, $branch ?: 'default', $branch == $this->branch);
$widget->addElement($element);
}
$widget = Sidebar::get()->addWidget(new SidebarWidget()); $widget = Sidebar::get()->addWidget(new SidebarWidget());
$widget->setTitle(_('Aktueller Versionsstand')); $widget->setTitle(_('Aktueller Versionsstand'));
$widget->addElement(new WidgetElement($this->version->get())); $widget->addElement(new WidgetElement($this->version->get()));
......
<? if (STUDIP\ENV === 'development'): ?>
<form method="post" action="<?= $controller->link_for('revert') ?>">
<?= CSRFProtection::tokenTag() ?>
<? endif; ?>
<table class="default" id="migration-list">
<caption>
<? if (STUDIP\ENV === 'development'): ?>
<?= _('Die markierten Anpassungen werden beim Klick auf "Starten" zurückgesetzt:') ?>
<? else: ?>
<?= _('Diese Anpassungen wurden im System bereits ausgeführt.') ?>
<? endif; ?>
</caption>
<colgroup>
<? if (STUDIP\ENV === 'development' && !$lock->isLocked($lock_data)): ?>
<col style="width: 24px">
<? endif; ?>
<col style="width: 120px">
<col>
</colgroup>
<thead>
<tr>
<? if (STUDIP\ENV === 'development' && !$lock->isLocked($lock_data)): ?>
<th>
<input type="checkbox"
data-proxyfor="#migration-list tbody :checkbox"
data-activates="#migration-list tfoot .button">
</th>
<? endif; ?>
<th><?= _('Nr.') ?></th>
<th><?= _('Beschreibung') ?></th>
</tr>
</thead>
<tbody>
<? foreach ($history as $number => $migration): ?>
<tr>
<? if (STUDIP\ENV === 'development' && !$lock->isLocked($lock_data)): ?>
<td>
<input type="checkbox"
name="versions[]" value="<?= htmlReady($number) ?>">
</td>
<? endif; ?>
<td>
<?= htmlReady($number) ?>
</td>
<td>
<? if ($migration->description()): ?>
<?= htmlReady($migration->description()) ?>
<? else: ?>
<em><?= _('keine Beschreibung vorhanden') ?></em>
<? endif ?>
</td>
</tr>
<? endforeach; ?>
</tbody>
<? if (STUDIP\ENV === 'development'): ?>
<tfoot>
<tr>
<td colspan="3">
<? if ($lock->isLocked($lock_data)): ?>
<?= MessageBox::info(sprintf(
_('Die Migration wurde %s von %s bereits angestossen und läuft noch.'),
reltime($lock_data['timestamp']),
htmlReady(User::find($lock_data['user_id'])->getFullName()
)), [
sprintf(
_('Sollte während der Migration ein Fehler aufgetreten sein, so können Sie '
. 'diese Sperre durch den unten stehenden Link oder das Löschen der Datei '
. '<em>%s</em> auflösen.'),
$lock->getFilename()
)
]) ?>
<?= Studip\LinkButton::create(_('Sperre aufheben'), $controller->link_for('release', $target)) ?>
<? else: ?>
<?= Studip\Button::createAccept(_('Starten'), 'start')?>
<? endif; ?>
</td>
</tr>
</tfoot>
<? endif; ?>
</table>
<? if (STUDIP\ENV === 'development'): ?>
</form>
<? endif; ?>
...@@ -3,54 +3,44 @@ ...@@ -3,54 +3,44 @@
<? else: ?> <? else: ?>
<form method="post" action="<?= $controller->link_for('migrate') ?>"> <form method="post" action="<?= $controller->link_for('migrate') ?>">
<?= CSRFProtection::tokenTag() ?> <?= CSRFProtection::tokenTag() ?>
<? if (isset($target)): ?> <? if (isset($target)): ?>
<input type="hidden" name="target" value="<?= htmlReady($target) ?>"> <input type="hidden" name="target" value="<?= htmlReady($target) ?>">
<? endif ?> <? endif ?>
<? if (STUDIP\ENV !== 'development'): ?> <input type="hidden" name="branch" value="<?= htmlReady($branch) ?>">
<?= addHiddenFields('versions', array_keys($migrations)) ?>
<? endif; ?>
<table class="default" id="migration-list"> <table class="default" id="migration-list">
<caption> <caption>
<? if (STUDIP\ENV === 'development'): ?>
<?= _('Die markierten Anpassungen werden beim Klick auf "Starten" ausgeführt:') ?>
<? else: ?>
<?= _('Die hier aufgeführten Anpassungen werden beim Klick auf "Starten" ausgeführt:') ?> <?= _('Die hier aufgeführten Anpassungen werden beim Klick auf "Starten" ausgeführt:') ?>
<? endif; ?>
</caption> </caption>
<colgroup> <colgroup>
<? if (STUDIP\ENV === 'development' && !$lock->isLocked($lock_data)): ?>
<col style="width: 24px"> <col style="width: 24px">
<? endif; ?>
<col style="width: 120px"> <col style="width: 120px">
<col> <col>
<col>
</colgroup> </colgroup>
<thead> <thead>
<tr> <tr>
<? if (STUDIP\ENV === 'development' && !$lock->isLocked($lock_data)): ?> <th></th>
<th>
<input type="checkbox"
data-proxyfor="#migration-list tbody :checkbox"
data-activates="#migration-list tfoot .button">
</th>
<? endif; ?>
<th><?= _('Nr.') ?></th> <th><?= _('Nr.') ?></th>
<th><?= _('Name') ?></th>
<th><?= _('Beschreibung') ?></th> <th><?= _('Beschreibung') ?></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<? foreach ($migrations as $number => $migration): ?> <? foreach ($migrations as $number => $migration): ?>
<? $version = $migrator->migrationBranchAndVersion($number) ?>
<tr> <tr>
<? if (STUDIP\ENV === 'development' && !$lock->isLocked($lock_data)): ?>
<td> <td>
<input type="checkbox" checked <? if ($version[0] === $branch): ?>
name="versions[]" value="<?= htmlReady($number) ?>"> <input type="radio" name="target" value="<?= $version[1] + $offset ?>">
<? endif ?>
</td> </td>
<? endif; ?>
<td> <td>
<?= htmlReady($number) ?> <?= htmlReady($number) ?>
</td> </td>
<td>
<?= htmlReady(get_class($migration)) ?>
</td>
<td> <td>
<? if ($migration->description()): ?> <? if ($migration->description()): ?>
<?= htmlReady($migration->description()) ?> <?= htmlReady($migration->description()) ?>
...@@ -63,7 +53,7 @@ ...@@ -63,7 +53,7 @@
</tbody> </tbody>
<tfoot> <tfoot>
<tr> <tr>
<td colspan="<?= 2 + (int) (STUDIP\ENV === 'development') ?>"> <td colspan="4">
<? if ($lock->isLocked($lock_data)): <? if ($lock->isLocked($lock_data)):
$user = User::find($lock_data['user_id']); $user = User::find($lock_data['user_id']);
?> ?>
......
...@@ -17,14 +17,14 @@ require_once __DIR__ . '/studip_cli_env.inc.php'; ...@@ -17,14 +17,14 @@ require_once __DIR__ . '/studip_cli_env.inc.php';
if (isset($_SERVER['argv'])) { if (isset($_SERVER['argv'])) {
# check for command line options # check for command line options
$options = getopt('1:d:lm:t:v'); $options = getopt('b:d:lm:t:v');
if ($options === false) { if ($options === false) {
exit(1); exit(1);
} }
# check for options # check for options
$single = false;
$domain = 'studip'; $domain = 'studip';
$branch = '0';
$list = false; $list = false;
$path = $STUDIP_BASE_PATH . '/db/migrations'; $path = $STUDIP_BASE_PATH . '/db/migrations';
$verbose = false; $verbose = false;
...@@ -32,8 +32,8 @@ if (isset($_SERVER['argv'])) { ...@@ -32,8 +32,8 @@ if (isset($_SERVER['argv'])) {
foreach ($options as $option => $value) { foreach ($options as $option => $value) {
switch ($option) { switch ($option) {
case '1': case 'b':
$single = (string) $value; $branch = (string) $value;
break; break;
case 'd': case 'd':
$domain = (string) $value; $domain = (string) $value;
...@@ -53,7 +53,9 @@ if (isset($_SERVER['argv'])) { ...@@ -53,7 +53,9 @@ if (isset($_SERVER['argv'])) {
} }
} }
$version = new DBSchemaVersion($domain); DBSchemaVersion::validateSchemaVersion();
$version = new DBSchemaVersion($domain, $branch);
$migrator = new Migrator($path, $version, $verbose); $migrator = new Migrator($path, $version, $verbose);
if ($list) { if ($list) {
...@@ -61,15 +63,8 @@ if (isset($_SERVER['argv'])) { ...@@ -61,15 +63,8 @@ if (isset($_SERVER['argv'])) {
foreach ($migrations as $number => $migration) { foreach ($migrations as $number => $migration) {
$description = $migration->description() ?: '(no description)'; $description = $migration->description() ?: '(no description)';
printf("%3d %s\n", $number, $description); printf("%6s %-20s %s\n", $number, get_class($migration), $description);
}
} elseif ($single) {
$direction = 'up';
if ($single[0] === '-') {
$direction = 'down';
$single = substr($single, 1);
} }
$migrator->execute($single, $direction);
} else { } else {
$migrator->migrateTo($target); $migrator->migrateTo($target);
} }
......
...@@ -132,18 +132,22 @@ if ($args) { ...@@ -132,18 +132,22 @@ if ($args) {
// show usage // show usage
if (!$pluginname) { if (!$pluginname) {
echo 'Usage: '. $args[0] .' migrate PLUGINNAME [-l] [-t] [-v]' . "\n"; echo 'Usage: '. $args[0] .' migrate PLUGINNAME [-l] [-v] [-t target] [-b branch]' . "\n";
exit(1); exit(1);
} }
// parse options // parse options
list($errors, $options, $args) = getopts(array('l' => 'Ss l list', 'v' => 'Ss v verbose', 't'=> 'Vs t target')); list($errors, $options, $args) = getopts(array('l' => 'Ss l list', 'v' => 'Ss v verbose', 't' => 'Vs t target', 'b' => 'Vs b branch'));
$list = false; $list = false;
$verbose = false; $verbose = false;
$target = NULL; $target = NULL;
$branch = '0';
foreach ($options as $option => $value) { foreach ($options as $option => $value) {
switch ($option) { switch ($option) {
case 'b':
$branch = ($value === false) ? '0' : $value;
break;
case 'l': case 'l':
$list = $value; $list = $value;
break; break;
...@@ -166,7 +170,7 @@ if ($args) { ...@@ -166,7 +170,7 @@ if ($args) {
if (is_dir($plugindir . '/migrations')) { if (is_dir($plugindir . '/migrations')) {
// if there are migrations, migrate // if there are migrations, migrate
$schema_version = new DBSchemaVersion($plugin['name']); $schema_version = new DBSchemaVersion($plugin['name'], $branch);
$migrator = new Migrator($plugindir . '/migrations', $schema_version, $verbose); $migrator = new Migrator($plugindir . '/migrations', $schema_version, $verbose);
if ($list) { if ($list) {
...@@ -175,7 +179,7 @@ if ($args) { ...@@ -175,7 +179,7 @@ if ($args) {
foreach ($migrations as $number => $migration) { foreach ($migrations as $number => $migration) {
$description = $migration->description() ?: '(no description)'; $description = $migration->description() ?: '(no description)';
printf("%3d %-20s %s\n", $number, get_class($migration), $description); printf("%6s %-20s %s\n", $number, get_class($migration), $description);
} }
} else { } else {
$migrator->migrateTo($target); $migrator->migrateTo($target);
...@@ -214,7 +218,7 @@ if ($args) { ...@@ -214,7 +218,7 @@ if ($args) {
if (is_dir($plugindir . '/migrations')) { if (is_dir($plugindir . '/migrations')) {
$schema_version = new DBSchemaVersion($plugin['name']); $schema_version = new DBSchemaVersion($plugin['name']);
$migrator = new Migrator($plugindir . '/migrations', $schema_version); $migrator = new Migrator($plugindir . '/migrations', $schema_version);
$migrator->migrate_to(0); $migrator->migrateTo(0);
} }
echo 'Das Plugin '. $plugin['name'] .' wurde ausgetragen.' . "\n"; echo 'Das Plugin '. $plugin['name'] .' wurde ausgetragen.' . "\n";
......
<?php <?php
$requirements = [
"lib/functions.php",
$GLOBALS["RELATIVE_PATH_EXTERN"]."/extern_config.inc.php",
$GLOBALS["RELATIVE_PATH_EXTERN"]."/lib/ExternConfig.class.php",
$GLOBALS["RELATIVE_PATH_EXTERN"]."/lib/ExternConfigIni.class.php",
$GLOBALS["RELATIVE_PATH_EXTERN"]."/lib/ExternConfigDb.class.php",
];
foreach ($requirements as $file) {
if (!file_exists($file)) {
throw new Exception('Migration is not compatible with your Stud.IP version');
}
require_once $file;
}
class Step87ExternConfigurations extends Migration class Step87ExternConfigurations extends Migration
{ {
......
<?php <?php
$requirements = [
__DIR__ . '/lib/datei.inc.php',
];
foreach ($requirements as $file) {
if (!file_exists($file)) {
throw new Exception('Migration is not compatible with your Stud.IP version');
}
require_once $file;
}
class Step00139UploadFileReorg extends Migration class Step00139UploadFileReorg extends Migration
{ {
......
...@@ -12,6 +12,14 @@ class AddMissingIndicesResources extends Migration ...@@ -12,6 +12,14 @@ class AddMissingIndicesResources extends Migration
public function up() public function up()
{ {
// avoid running this migration twice
$query = "SHOW INDEX FROM resource_temporary_permissions WHERE Key_name = 'user_id'";
$result = DBManager::get()->query($query);
if ($result && $result->rowCount() > 0) {
return;
}
$query = "ALTER TABLE `resource_temporary_permissions` ADD INDEX (`user_id`)"; $query = "ALTER TABLE `resource_temporary_permissions` ADD INDEX (`user_id`)";
DBManager::get()->exec($query); DBManager::get()->exec($query);
......
...@@ -13,6 +13,14 @@ class AddMissingIndices extends Migration ...@@ -13,6 +13,14 @@ class AddMissingIndices extends Migration
public function up() public function up()
{ {
// avoid running this migration twice
$query = "SHOW INDEX FROM activities WHERE Key_name = 'object_id'";
$result = DBManager::get()->query($query);
if ($result && $result->rowCount() > 0) {
return;
}
$query = "ALTER TABLE `activities` $query = "ALTER TABLE `activities`
ADD INDEX `object_id` (`object_id`(32))"; ADD INDEX `object_id` (`object_id`(32))";
DBManager::get()->exec($query); DBManager::get()->exec($query);
......
<?php
class MigrationHistory extends Migration
{
public function description()
{
return 'Extends migration table to allow a history of who did what when';
}
public function up()
{
$query = "ALTER TABLE `schema_versions`
ADD COLUMN `user_id` CHAR(32) CHARACTER SET latin1 COLLATE latin1_bin NULL DEFAULT NULL,
ADD COLUMN `mkdate` INT(11) UNSIGNED NULL DEFAULT NULL";
DBManager::get()->exec($query);
}
public function down()
{
$query = "ALTER TABLE `schema_versions`
DROP COLUMN `user_id`,
DROP COLUMN `mkdate`";
DBManager::get()->exec($query);
}
}
...@@ -8,6 +8,14 @@ class ChangeBlubberThreadFollowing extends Migration ...@@ -8,6 +8,14 @@ class ChangeBlubberThreadFollowing extends Migration
public function up() public function up()
{ {
// avoid running this migration twice
$query = "SHOW TABLES LIKE 'blubber_threads_followstates'";
$result = DBManager::get()->query($query);
if ($result && $result->rowCount() > 0) {
return;
}
// Alter table to reflect new state // Alter table to reflect new state
$query = "RENAME TABLE `blubber_threads_unfollow` TO `blubber_threads_followstates`"; $query = "RENAME TABLE `blubber_threads_unfollow` TO `blubber_threads_followstates`";
DBManager::get()->exec($query); DBManager::get()->exec($query);
......
...@@ -3,17 +3,11 @@ class MigrationHistoryReworked extends Migration ...@@ -3,17 +3,11 @@ class MigrationHistoryReworked extends Migration
{ {
public function description() public function description()
{ {
return 'Reverts migration 20201114 and adds appropriate log actions'; return 'Add log actions for migrations';
} }
public function up() public function up()
{ {
// Drop columns
$query = "ALTER TABLE `schema_versions`
DROP COLUMN `user_id`,
DROP COLUMN `mkdate`";
DBManager::get()->exec($query);
// Add log actions // Add log actions
$query = "INSERT IGNORE INTO log_actions ( $query = "INSERT IGNORE INTO log_actions (
`action_id`, `name`, `description`, `info_template`, `active`, `expires`, `mkdate`, `chdate` `action_id`, `name`, `description`, `info_template`, `active`, `expires`, `mkdate`, `chdate`
...@@ -46,11 +40,5 @@ class MigrationHistoryReworked extends Migration ...@@ -46,11 +40,5 @@ class MigrationHistoryReworked extends Migration
$statement = DBManager::get()->prepare($query); $statement = DBManager::get()->prepare($query);
$statement->execute([':name' => 'MIGRATE_UP']); $statement->execute([':name' => 'MIGRATE_UP']);
$statement->execute([':name' => 'MIGRATE_DOWN']); $statement->execute([':name' => 'MIGRATE_DOWN']);
// Add columns
$query = "ALTER TABLE `schema_versions`
ADD COLUMN `user_id` CHAR(32) CHARACTER SET latin1 COLLATE latin1_bin NULL DEFAULT NULL,
ADD COLUMN `mkdate` INT(11) UNSIGNED NULL DEFAULT NULL";
DBManager::get()->exec($query);
} }
} }
<?php <?php
class MigrationsReloaded extends Migration class MigrationsReloaded extends Migration
{ {
public function description() public function description()
{ {
return 'switch from a single successive migration version number to ' return 'add branch column to schema_version';
. 'a collection of already executed migrations';
} }
public function up() public function up()
{ {
$query = "CREATE TABLE IF NOT EXISTS `schema_versions` ( $db = DBManager::get();
`domain` VARCHAR(255) COLLATE latin1_bin NOT NULL,
`version` BIGINT(20) UNSIGNED NOT NULL, $sql = "ALTER TABLE schema_version
PRIMARY KEY `domain` (`domain`, `version`) CHANGE domain domain VARCHAR(255) COLLATE latin1_bin NOT NULL,
) ENGINE=InnoDB ROW_FORMAT=DYNAMIC"; ADD branch VARCHAR(64) COLLATE latin1_bin NOT NULL DEFAULT '0' AFTER domain,
DBManager::get()->exec($query); DROP PRIMARY KEY,
ADD PRIMARY KEY (domain, branch)";
$query = "SELECT `domain`, `version` $db->exec($sql);
FROM `schema_version`
WHERE `version` > 0";
$rows = DBManager::get()->query($query)->fetchAll(PDO::FETCH_NUM);
$query = "INSERT INTO `schema_versions`
VALUES (:domain, :version)";
$statement = DBManager::get()->prepare($query);
foreach ($rows as list($domain, $version)) {
$statement->bindValue(':domain', $domain);
for ($i = 1; $i <= $version; $i += 1) {
$statement->bindValue(':version', $i);
$statement->execute();
}
}
$query = "DROP TABLE IF EXISTS `schema_version`";
DBManager::get()->exec($query);
} }
public function down() public function down()
{ {
$query = "CREATE TABLE IF NOT EXISTS `schema_version` ( $db = DBManager::get();
`domain` VARCHAR(255) COLLATE latin1_bin NOT NULL,
`version` INT(11) UNSIGNED NOT NULL,
PRIMARY KEY (`domain`)
) ENGINE=InnoDB ROW_FORMAT=DYNAMIC";
DBManager::get()->exec($query);
$query = "INSERT IGNORE INTO `schema_version` $sql = "DELETE FROM schema_version WHERE branch != '0'";
SELECT `domain`, MAX(`version`) $db->exec($sql);
FROM `schema_versions`
GROUP BY `domain`";
DBManager::get()->exec($query);
$query = "DROP TABLE IF EXISTS `schema_versions`"; $sql = 'ALTER TABLE schema_version
DBManager::get()->exec($query); DROP PRIMARY KEY,
ADD PRIMARY KEY (domain),
DROP branch';
$db->exec($sql);
} }
}; }
...@@ -251,7 +251,8 @@ class PluginAdministration ...@@ -251,7 +251,8 @@ class PluginAdministration
if (is_dir($new_pluginpath . '/migrations')) { if (is_dir($new_pluginpath . '/migrations')) {
$migrator = new Migrator($new_pluginpath . '/migrations', $schema_version); $migrator = new Migrator($new_pluginpath . '/migrations', $schema_version);
$new_version = $migrator->topVersion(); $all_branches = array_fill_keys($schema_version->getAllBranches(), 0);
$new_version = $migrator->topVersion(true) + $all_branches;
} }
$migrator = new Migrator($plugindir . '/migrations', $schema_version); $migrator = new Migrator($plugindir . '/migrations', $schema_version);
......
...@@ -189,7 +189,7 @@ class StudipCacheFactory ...@@ -189,7 +189,7 @@ class StudipCacheFactory
# default class # default class
if (is_null($cache_class)) { if (is_null($cache_class)) {
$version = new DBSchemaVersion(); $version = new DBSchemaVersion();
if (!$version->contains(224)) { if ($version->get() < 224) {
// db cache is not yet available, use StudipMemoryCache // db cache is not yet available, use StudipMemoryCache
return 'StudipMemoryCache'; return 'StudipMemoryCache';
} }
......
...@@ -6,8 +6,8 @@ ...@@ -6,8 +6,8 @@
* *
* @author Elmar Ludwig * @author Elmar Ludwig
* @copyright 2007 Elmar Ludwig * @copyright 2007 Elmar Ludwig
* @license GPL2 or any later version * @license GPL2 or any later version
* @package migrations * @package migrations
*/ */
class DBSchemaVersion implements SchemaVersion class DBSchemaVersion implements SchemaVersion
{ {
...@@ -19,26 +19,32 @@ class DBSchemaVersion implements SchemaVersion ...@@ -19,26 +19,32 @@ class DBSchemaVersion implements SchemaVersion
private $domain; private $domain;
/** /**
* schema versions * branch of schema version
* *
* @var array * @var string
*/ */
private $versions = []; private $branch;
/** /**
* current schema version numbers
*
* @access private
* @var array * @var array
*/ */
private $data = []; private $versions;
/** /**
* Initialize a new DBSchemaVersion for a given domain. * Initialize a new DBSchemaVersion for a given domain.
* The default domain name is 'studip'. * The default domain name is 'studip'.
* *
* @param string $domain domain name (optional) * @param string $domain domain name (optional)
* @param string $branch schema branch (optional)
*/ */
public function __construct($domain = 'studip') public function __construct($domain = 'studip', $branch = 0)
{ {
$this->domain = $domain; $this->domain = $domain;
$this->branch = $branch;
$this->versions = [0];
$this->initSchemaInfo(); $this->initSchemaInfo();
} }
...@@ -53,132 +59,134 @@ class DBSchemaVersion implements SchemaVersion ...@@ -53,132 +59,134 @@ class DBSchemaVersion implements SchemaVersion
} }
/** /**
* Initialize the current schema version. * Retrieve the branch of this schema.
*
* @return string schema branch
*/ */
private function initSchemaInfo() public function getBranch()
{ {
$this->data = []; return $this->branch;
try {
$query = "SELECT version FROM schema_versions WHERE domain = ?";
$statement = DBManager::get()->prepare($query);
$statement->execute([$this->domain]);
$this->versions = $statement->fetchAll(PDO::FETCH_COLUMN);
} catch (PDOException $e) {
$query = "SELECT version FROM schema_version WHERE domain = ?";
$statement = DBManager::get()->prepare($query);
$statement->execute([$this->domain]);
$this->versions = range(1, $statement->fetchColumn());
}
} }
/** /**
* Retrieve the current schema version. * Retrieve all branches of this schema.
* *
* @return int schema version * @return array all schema branches
*/ */
public function get() public function getAllBranches()
{ {
return $this->versions ? max($this->versions) : 0; return array_keys($this->versions);
} }
/** /**
* Returns whether the given version is already present for the given * Check whether the current schema_version supports branches.
* domain.
*
* @param int $version Version number
* @return bool
*/ */
public function contains($version) private function branchSupported()
{ {
return in_array($version, $this->versions); $result = DBManager::get()->query("DESCRIBE schema_version 'branch'");
return $result && $result->rowCount() > 0;
} }
/** /**
* Set the current schema version. * Initialize the current schema versions.
*
* @param int $version new schema version
*/ */
public function add($version) private function initSchemaInfo()
{ {
$version = (int) $version; if (!$this->branchSupported()) {
$query = "SELECT 0, version FROM schema_version WHERE domain = ?";
try { } else {
$query = "INSERT INTO `schema_versions` (`domain`, `version`) $query = "SELECT branch, version FROM schema_version WHERE domain = ? ORDER BY branch";
VALUES (?, ?)"; }
DBManager::get()->execute($query, [ $statement = DBManager::get()->prepare($query);
$this->domain, $statement->execute([$this->domain]);
$version, $versions = $statement->fetchAll(PDO::FETCH_COLUMN | PDO::FETCH_GROUP | PDO::FETCH_UNIQUE);
]);
StudipLog::log( if ($versions) {
'MIGRATE_UP', $this->versions = array_map('intval', $versions);
$version,
$this->domain
);
} catch (PDOException $e) {
$query = "UPDATE `schema_version`
SET `version` = ?
WHERE `domain` = ?";;
DBManager::get()->execute($query, [
$version,
$this->domain,
]);
} }
NotificationCenter::postNotification(
'SchemaVersionDidUpdate',
$this->domain,
$version
);
} }
/** /**
* Removes a schema version. * Retrieve the current schema version.
*
* @param string $branch schema branch (optional)
* @return int schema version
*/
public function get($branch = 0)
{
return $this->versions[$branch ?: $this->branch];
}
/**
* Set the current schema version.
* *
* @param int $version schema version to remove * @param int $version new schema version
* @param string $branch schema branch (optional)
*/ */
public function remove($version) public function set($version, $branch = 0)
{ {
$version = (int) $version; $this->versions[$branch ?: $this->branch] = (int) $version;
try { if (!$this->branchSupported()) {
$query = "DELETE FROM `schema_versions` $query = "INSERT INTO schema_version (domain, version)
WHERE `domain` = ? AND `version` = ?"; VALUES (?, ?)
DBManager::get()->execute($query, [ ON DUPLICATE KEY UPDATE version = VALUES(version)";
$statement = DBManager::get()->prepare($query);
$statement->execute([
$this->domain, $this->domain,
$version $version
]); ]);
} else {
StudipLog::log( $query = "INSERT INTO schema_version (domain, branch, version)
'MIGRATE_DOWN', VALUES (?, ?, ?)
$version, ON DUPLICATE KEY UPDATE version = VALUES(version)";
$this->domain $statement = DBManager::get()->prepare($query);
); $statement->execute([
} catch (PDOException $e) {
$query = "UPDATE `schema_version`
SET `version` = ?
WHERE `domain` = ?";
DBManager::get()->execute($query, [
$version,
$this->domain, $this->domain,
$branch ?: $this->branch,
$version
]); ]);
} }
NotificationCenter::postNotification( NotificationCenter::postNotification(
'SchemaVersionDidDelete', 'SchemaVersionDidUpdate',
$this->domain, $this->domain,
$version $version
); );
} }
/** /**
* @param $domain * Validate correct structure of schema_version table.
* @param $version
* @return string
*/ */
static public function exists($domain, $version) public static function validateSchemaVersion()
{ {
return (bool)DBManager::get()->fetchColumn( $db = DBManager::get();
"SELECT 1 FROM schema_versions WHERE `domain` = ? AND `version` = ?", $result = $db->query("SHOW TABLES LIKE 'schema_versions'");
[$domain, $version]);
if ($result && $result->rowCount() > 0) {
$backported_migrations = [
20200306, 20200306, 20200713, 20200811, 20200909,
20200910, 20201002, 20201103, 202011031, 20210317
];
$query = "DELETE FROM schema_versions
WHERE domain = 'studip' AND version in (?)";
$db->execute($query, [$backported_migrations]);
$query = "CREATE TABLE schema_version (
domain VARCHAR(255) COLLATE latin1_bin NOT NULL,
branch VARCHAR(64) COLLATE latin1_bin NOT NULL DEFAULT '0',
version INT(11) UNSIGNED NOT NULL,
PRIMARY KEY (domain, branch)
) ENGINE=InnoDB ROW_FORMAT=DYNAMIC";
$db->exec($query);
$query = "INSERT INTO schema_version
SELECT domain, '0', MAX(version) FROM schema_versions
GROUP BY domain";
$db->exec($query);
$query = "DROP TABLE schema_versions";
$db->exec($query);
}
} }
} }
...@@ -100,12 +100,28 @@ abstract class Migration ...@@ -100,12 +100,28 @@ abstract class Migration
* *
* @param string $format,... printf-style format string and parameters * @param string $format,... printf-style format string and parameters
*/ */
protected function announce($format /* , ... */) public function announce($format /* , ... */)
{ {
# format message # format message
$args = func_get_args(); $args = func_get_args();
$message = vsprintf(array_shift($args), $args); $message = vsprintf(array_shift($args), $args);
return $this->write(Migrator::mark($message)); return $this->write($this->mark($message));
} }
/**
* Pads and highlights a given text to a specific length with the given
* sign.
*
* @param string $text
* @param string $sign
*/
protected function mark($text, $sign = '=')
{
$text = trim($text);
if ($text) {
$text = " {$text} ";
}
return str_pad("{$sign}{$sign}{$text}", 79, $sign, STR_PAD_RIGHT);
}
} }
...@@ -38,16 +38,14 @@ ...@@ -38,16 +38,14 @@
* *
* (\d+)_([a-z_]+).php // (index)_(name).php * (\d+)_([a-z_]+).php // (index)_(name).php
* *
* 20180524110400_my_first_migration.php * 001_my_first_migration.php
* 20180812152300_another_migration.php * 002_another_migration.php
* 20181110100900_and_one_last.php * 003_and_one_last.php
* *
* Those numbers are used to order your migrations. Use the current time to * Those numbers are used to order your migrations. The first migration has
* define the chronological order of migrations. Gaps are allowed. In previous * to be a 1 (but you can use leading 0). Every following migration has to be
* versions of the migration system, the numbers were naturally ordered starting * the successor to the previous migration. No gaps are allowed. Just use
* with 1 but that proved to be rather unflexible regarding bug fixes that * natural numbers starting with 1.
* needed a migration to be executed. Thus, every executed migration number is
* stored and you may add a migration lateron between two other migrations.
* *
* When migrating those numbers are used to determine the migrations needed to * When migrating those numbers are used to determine the migrations needed to
* fulfill the target version. * fulfill the target version.
...@@ -102,7 +100,7 @@ ...@@ -102,7 +100,7 @@
* $migrator = new Migrator($path, $version, $verbose); * $migrator = new Migrator($path, $version, $verbose);
* *
* # now migrate to target version * # now migrate to target version
* $migrator->migrateTo(20181128100139); * $migrator->migrateTo(5);
* *
* If you want to migrate to the highest migration, you can just use NULL as * If you want to migrate to the highest migration, you can just use NULL as
* parameter: * parameter:
...@@ -116,8 +114,6 @@ ...@@ -116,8 +114,6 @@
*/ */
class Migrator class Migrator
{ {
const FILE_REGEXP = '/\b(\d+)([_-][_a-z0-9]+)+\.php$/';
/** /**
* Direction of migration, either "up" or "down" * Direction of migration, either "up" or "down"
* *
...@@ -135,9 +131,9 @@ class Migrator ...@@ -135,9 +131,9 @@ class Migrator
/** /**
* Specifies the target version, may be NULL (alias for "highest migration") * Specifies the target version, may be NULL (alias for "highest migration")
* *
* @var int * @var array
*/ */
private $target_version; private $target_versions;
/** /**
* How verbose shall the migrator be? * How verbose shall the migrator be?
...@@ -192,8 +188,8 @@ class Migrator ...@@ -192,8 +188,8 @@ class Migrator
* the current schema version (provided by the SchemaVersion object) and a * the current schema version (provided by the SchemaVersion object) and a
* target version calling the methods #up and #down in sequence. * target version calling the methods #up and #down in sequence.
* *
* @param mixed the target version as an integer or NULL thus migrating to * @param mixed the target version as an integer, array or NULL thus
* the top migration * migrating to the top migrations
*/ */
public function migrateTo($target_version) public function migrateTo($target_version)
{ {
...@@ -201,83 +197,87 @@ class Migrator ...@@ -201,83 +197,87 @@ class Migrator
# you're on the right version # you're on the right version
if (empty($migrations)) { if (empty($migrations)) {
$this->log("You are already at %d.\n", $this->schema_version->get()); $this->log('You are already at %d.', $this->schema_version->get());
return; return;
} }
$this->log( $this->log(
"Currently at version %d. Now migrating %s to %d.\n", 'Currently at version %d. Now migrating %s to %d.',
$this->schema_version->get(), $this->schema_version->get(),
$this->direction, $this->direction,
$this->target_version max($this->target_versions)
); );
foreach ($migrations as $version => $migration) { foreach ($migrations as $number => $migration) {
$this->execute($version, $this->direction, $migration); list($branch, $version) = $this->migrationBranchAndVersion($number);
$action = $this->isUp() ? 'Migrating' : 'Reverting';
$migration->announce("{$action} %s", $number);
if ($migration->description()) {
$this->log($migration->description());
$this->log(str_repeat('-', 79));
}
$time_start = microtime(true);
$migration->migrate($this->direction);
$action = $this->isUp() ? 'Migrated' : 'Reverted';
$this->log('');
$migration->announce("{$action} in %ss", round(microtime(true) - $time_start, 3));
$this->log('');
$this->schema_version->set($this->isDown() ? $version - 1 : $version, $branch);
$action = $this->isUp() ? 'MIGRATE_UP' : 'MIGRATE_DOWN';
StudipLog::log($action, $number, $this->schema_version->getDomain());
} }
} }
/** /**
* Executes a migration's up or down method * Calculate the selected target versions for all relevant branches. If a
* single branch is selected for migration, only that branch and all its
* children are considered relevant.
* *
* @param string $version Version to execute * @param mixed the target version as an integer, array or NULL thus
* @param string $direction Up or down * migrating to the top migrations
* @param Migration $migration Migration to execute (optional, will be *
* loaded if missing) * @return array an associative array, whose keys are the branch names
* and whose values are the target versions
*/ */
public function execute($version, $direction, Migration $migration = null) public function targetVersions($target_version)
{ {
if ($this->isUp($direction) && $this->schema_version->contains($version)) { $top_versions = $this->topVersion(true);
$this->log("Version {$version} is already present.\n"); $target_branch = $this->schema_version->getBranch();
return;
if (is_array($target_version)) {
return $target_version;
} }
if ($this->isDown($direction) && !$this->schema_version->contains($version)) { $max_version = $target_branch ? $target_branch . '.' . $target_version : $target_version;
$this->log("Version {$version} is not present.\n");
return; foreach ($top_versions as $branch => $version) {
} if ($branch == $target_branch) {
if (isset($target_version)) {
if ($migration === null) { $top_versions[$branch] = $target_version;
$migrations = $this->migrationClasses(); }
if (!isset($migrations[$version])) { } else if ($target_branch && strpos($branch, $target_branch . '.') !== 0) {
throw new Exception("Version {$version} is invalid"); unset($top_versions[$branch]);
} else if (isset($target_version) && version_compare($branch, $max_version) >= 0) {
$top_versions[$branch] = 0;
} }
list($file, $class) = $migrations[$version];
$migration = $this->loadMigration($file, $class);
}
$action = $this->isUp($direction) ? 'Migrating' : 'Reverting';
$this->announce("{$action} %d", $version);
if ($migration->description()) {
$this->log($migration->description());
$this->log(self::mark('', '-'));
}
$time_start = microtime(true);
$migration->migrate($direction);
$action = $this->isUp($direction) ? 'Migrated' : 'Reverted';
$this->log('');
$this->announce("{$action} in %ss", round(microtime(true) - $time_start, 3));
$this->log('');
// Update schema version
if ($this->isDown($direction)) {
$this->schema_version->remove($version);
} else {
$this->schema_version->add($version);
} }
return $top_versions;
} }
/** /**
* Invoking this method will return a list of migrations with an index between * Invoking this method will return a list of migrations with an index between
* the current schema version (provided by the SchemaVersion object) and a * the current schema version (provided by the SchemaVersion object) and a
* target version calling the methods #up and #down in sequence. * target version calling the methods #up and #down in sequence.
* *
* @param mixed the target version as an integer or NULL thus migrating to * @param mixed the target version as an integer, array or NULL thus
* the top migration * migrating to the top migrations
* *
* @return array an associative array, whose keys are the migration's * @return array an associative array, whose keys are the migration's
* version and whose values are the migration objects * version and whose values are the migration objects
...@@ -287,35 +287,29 @@ class Migrator ...@@ -287,35 +287,29 @@ class Migrator
// Load migrations // Load migrations
$migrations = $this->migrationClasses(); $migrations = $this->migrationClasses();
// Determine correct target version // Determine correct target versions
$this->target_version = $target_version === null $this->target_versions = $this->targetVersions($target_version);
? $this->topVersion()
: (int) $target_version;
// Determine max version (might differ from max schema version in db in
// development systems)
$max_version = min($this->topVersion(), $this->schema_version->get());
// Determine migration direction // Determine migration direction
if ($this->target_version > 0 && $this->target_version >= $max_version) { foreach ($this->target_versions as $branch => $version) {
$this->direction = 'up'; if ($this->schema_version->get($branch) < $version) {
} else { $this->direction = 'up';
$this->direction = 'down'; break;
} else if ($version < $this->schema_version->get($branch)) {
$this->direction = 'down';
break;
}
} }
// Sort migrations in correct order // Sort migrations in correct order
uksort($migrations, function ($a, $b) { uksort($migrations, 'version_compare');
if (mb_strlen($a) > 8 && mb_strlen($b) > 8) {
return $a - $b;
}
return mb_substr($a, 0, 8) - mb_substr($b, 0, 8);
});
if (!$this->isUp()) { if (!$this->isUp()) {
$migrations = array_reverse($migrations, true); $migrations = array_reverse($migrations, true);
} }
$result = []; $result = [];
foreach ($migrations as $version => $migration_file_and_class) { foreach ($migrations as $version => $migration_file_and_class) {
if (!$this->relevantMigration($version)) { if (!$this->relevantMigration($version)) {
continue; continue;
...@@ -323,10 +317,15 @@ class Migrator ...@@ -323,10 +317,15 @@ class Migrator
list($file, $class) = $migration_file_and_class; list($file, $class) = $migration_file_and_class;
try { $migration = require_once $file;
$result[$version] = $this->loadMigration($file, $class);
} catch (Exception $e) { if (!$migration instanceof Migration) {
$migration = new $class($this->verbose);
} else {
$migration->setVerbose($this->verbose);
} }
$result[$version] = $migration;
} }
return $result; return $result;
...@@ -342,47 +341,30 @@ class Migrator ...@@ -342,47 +341,30 @@ class Migrator
*/ */
private function relevantMigration($version) private function relevantMigration($version)
{ {
if ($this->isUp()) { list($branch, $version) = $this->migrationBranchAndVersion($version);
return !$this->schema_version->contains($version) $current_version = $this->schema_version->get($branch);
&& $version <= $this->target_version;
} elseif ($this->isDown()) { if (!isset($this->target_versions[$branch])) {
return $this->schema_version->contains($version) return false;
&& $version > $this->target_version; } else if ($this->isUp()) {
return $current_version < $version
&& $version <= $this->target_versions[$branch];
} else if ($this->isDown()) {
return $current_version >= $version
&& $version > $this->target_versions[$branch];
} }
return false; return false;
} }
/**
* Loads a migration from the given file and creates and instance of it.
*
* @param string $file File name of migration to load
* @param string $class Class name to expect to be loaded from the file
* @return Migration instance
*/
private function loadMigration($file, $class)
{
if (class_exists($class)) {
$migration = new $class($this->verbose);
} else {
$migration = require $file;
if (!$migration instanceof Migration) {
$migration = new $class($this->verbose);
} else {
$migration->setVerbose($this->verbose);
}
}
return $migration;
}
/** /**
* Am I migrating up? * Am I migrating up?
* *
* @return bool TRUE if migrating up, FALSE otherwise * @return bool TRUE if migrating up, FALSE otherwise
*/ */
private function isUp($direction = null) private function isUp()
{ {
return ($direction ?: $this->direction) === 'up'; return $this->direction === 'up';
} }
/** /**
...@@ -390,9 +372,9 @@ class Migrator ...@@ -390,9 +372,9 @@ class Migrator
* *
* @return bool TRUE if migrating down, FALSE otherwise * @return bool TRUE if migrating down, FALSE otherwise
*/ */
private function isDown($direction = null) private function isDown()
{ {
return ($direction ?: $this->direction) === 'down'; return $this->direction === 'down';
} }
/** /**
...@@ -433,10 +415,7 @@ class Migrator ...@@ -433,10 +415,7 @@ class Migrator
*/ */
protected function migrationFiles() protected function migrationFiles()
{ {
$files = glob($this->migrations_path . '/*.php'); $files = glob($this->migrations_path . '/[0-9]*_*.php');
$files = array_filter($files, function ($file) {
return preg_match(self::FILE_REGEXP, $file);
});
return $files; return $files;
} }
...@@ -450,19 +429,43 @@ class Migrator ...@@ -450,19 +429,43 @@ class Migrator
protected function migrationVersionAndName($migration_file) protected function migrationVersionAndName($migration_file)
{ {
$matches = []; $matches = [];
preg_match(self::FILE_REGEXP, $migration_file, $matches); preg_match('/\b([0-9.]+)_([_a-z0-9]*)\.php$/', $migration_file, $matches);
return [(int) $matches[1], $matches[2]]; return [$matches[1], $matches[2]];
}
/**
* Split a migration version into its branch and version parts.
*
* @param string a migration version
* @return array an array of two elements containing the migration's branch
* and version on this branch.
*/
public function migrationBranchAndVersion($version)
{
if (preg_match('/^(.*)\.([0-9]+)$/', $version, $matches)) {
$branch = preg_replace('/\b0+/', '', $matches[1]);
$version = (int) $matches[2];
} else {
$branch = '0';
$version = (int) $version;
}
return [$branch, $version];
} }
/** /**
* Returns the top migration's version. * Returns the top migration's version.
* *
* @param bool return top version for all branches, not just default one
* @return int the top migration's version. * @return int the top migration's version.
*/ */
public function topVersion() public function topVersion($all_branches = false)
{ {
$versions = array_keys($this->migrationClasses()); $versions = [0];
return $versions ? max($versions) : 0; foreach (array_keys($this->migrationClasses()) as $version) {
list($branch, $version) = $this->migrationBranchAndVersion($version);
$versions[$branch] = max($versions[$branch], $version);
}
return $all_branches ? $versions : $versions[$this->schema_version->getBranch()];
} }
/** /**
...@@ -479,39 +482,6 @@ class Migrator ...@@ -479,39 +482,6 @@ class Migrator
} }
$args = func_get_args(); $args = func_get_args();
vprintf(trim(array_shift($args)) . "\n", $args); vprintf(array_shift($args) . "\n", $args);
}
/**
* Overridable method used to return a textual representation of a stronger
* ouput of what's going on in me. You can use me as you would use printf.
*
* @param string $format just a dummy value, instead use this method as you
* would use printf & co.
*/
protected function announce($format)
{
# format message
$args = func_get_args();
$message = vsprintf(array_shift($args), $args);
return $this->log(self::mark($message));
}
/**
* Pads and highlights a given text to a specific length with the given
* sign.
*
* @param string $text
* @param string $sign
*/
public static function mark($text, $sign = '=')
{
$text = trim($text);
if ($text) {
$text = " {$text} ";
}
return str_pad("{$sign}{$sign}{$text}", 79, $sign, STR_PAD_RIGHT);
} }
} }
...@@ -14,32 +14,32 @@ ...@@ -14,32 +14,32 @@
interface SchemaVersion interface SchemaVersion
{ {
/** /**
* Returns current schema version (as maximum number). * Retrieve the branch of this schema.
* *
* @return int schema version * @return string schema branch
*/ */
public function get(); public function getBranch();
/** /**
* Returns whether the given version is already present for the given * Retrieve all branches of this schema.
* domain.
* *
* @param int $version Version number * @return array all schema branches
* @return bool
*/ */
public function contains($version); public function getAllBranches();
/** /**
* Adds a schema version. * Returns current schema version.
* *
* @param int $version schema version * @param string $branch schema branch (optional)
* @return int schema version
*/ */
public function add($version); public function get($branch = 0);
/** /**
* Removes a schema version. * Sets the new schema version.
* *
* @param int $version schema version * @param int $version new schema version
* @param string $branch schema branch (optional)
*/ */
public function remove($version); public function set($version, $branch = 0);
} }
...@@ -109,9 +109,6 @@ class StudipNews extends SimpleORMap implements PrivacyObject ...@@ -109,9 +109,6 @@ class StudipNews extends SimpleORMap implements PrivacyObject
public static function CountUnread($range_id = 'studip', $user_id = false) public static function CountUnread($range_id = 'studip', $user_id = false)
{ {
if (!DBSchemaVersion::exists('studip', '20210201')) {
return 0;
}
$query = "SELECT SUM(nw.chdate > IFNULL(b.visitdate, :threshold) AND nw.user_id != :user_id) $query = "SELECT SUM(nw.chdate > IFNULL(b.visitdate, :threshold) AND nw.user_id != :user_id)
FROM news_range a FROM news_range a
LEFT JOIN news nw ON (a.news_id = nw.news_id AND UNIX_TIMESTAMP() BETWEEN date AND date + expire) LEFT JOIN news nw ON (a.news_id = nw.news_id AND UNIX_TIMESTAMP() BETWEEN date AND date + expire)
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment