feat: add support for SQLite (#3984)

* feat: add support for sqlite

* chore: add warning on install

* fix: ignore constraints before transaction begins

* chore: update workflow

* Apply fixes from StyleCI

* chore: generate sqlite dump and manually add foreign keys

* chore: fix actions

* chore: fix actions

* chore: fix actions

* chore: fix actions

* chore: fix actions

* chore: fix actions

* test: fix

* Apply fixes from StyleCI

* fix: sqlite with db prefix

* Apply fixes from StyleCI

* fix: statistics sqlite
This commit is contained in:
Sami Mazouz 2024-06-21 07:25:11 +01:00 committed by GitHub
parent 5ce1aeab47
commit eb6e599df1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
61 changed files with 801 additions and 263 deletions

View File

@ -23,3 +23,6 @@ indent_size = 2
[*.neon]
indent_style = tab
[{install,update}.php]
indent_size = 2

View File

@ -44,7 +44,7 @@ on:
description: Versions of databases to test with. Should be array of strings encoded as JSON array
type: string
required: false
default: '["mysql:5.7", "mysql:8.0.30", "mysql:8.1.0", "mariadb"]'
default: '["mysql:5.7", "mysql:8.0.30", "mysql:8.1.0", "mariadb", "sqlite:3"]'
php_ini_values:
description: PHP ini values
@ -85,32 +85,49 @@ jobs:
# Expands the matrix by naming DBs.
- service: 'mysql:5.7'
db: MySQL 5.7
driver: mysql
- service: 'mysql:8.0.30'
db: MySQL 8.0
driver: mysql
- service: mariadb
db: MariaDB
driver: mysql
- service: 'mysql:8.1.0'
db: MySQL 8.1
driver: mysql
- service: 'sqlite:3'
db: SQLite
driver: sqlite
# Include Database prefix tests with only one PHP version.
- php: ${{ fromJSON(inputs.php_versions)[0] }}
service: 'mysql:5.7'
db: MySQL 5.7
driver: mysql
prefix: flarum_
prefixStr: (prefix)
- php: ${{ fromJSON(inputs.php_versions)[0] }}
service: 'mysql:8.0.30'
db: MySQL 8.0
driver: mysql
prefix: flarum_
prefixStr: (prefix)
- php: ${{ fromJSON(inputs.php_versions)[0] }}
service: mariadb
db: MariaDB
driver: mysql
prefix: flarum_
prefixStr: (prefix)
- php: ${{ fromJSON(inputs.php_versions)[0] }}
service: 'mysql:8.1.0'
db: MySQL 8.1
driver: mysql
prefix: flarum_
prefixStr: (prefix)
- php: ${{ fromJSON(inputs.php_versions)[0] }}
service: 'sqlite:3'
db: SQLite
driver: sqlite
prefix: flarum_
prefixStr: (prefix)
@ -118,10 +135,22 @@ jobs:
exclude:
- php: ${{ fromJSON(inputs.php_versions)[1] }}
service: 'mysql:8.0.30'
- php: ${{ fromJSON(inputs.php_versions)[0] }}
service: mariadb
- php: ${{ fromJSON(inputs.php_versions)[1] }}
service: mariadb
- php: ${{ fromJSON(inputs.php_versions)[0] }}
service: 'mysql:8.1.0'
- php: ${{ fromJSON(inputs.php_versions)[1] }}
service: 'mysql:8.1.0'
- php: ${{ fromJSON(inputs.php_versions)[0] }}
service: 'sqlite:3'
- php: ${{ fromJSON(inputs.php_versions)[1] }}
service: 'sqlite:3'
services:
mysql:
image: ${{ matrix.service }}
image: ${{ matrix.service != 'sqlite:3' && matrix.service || '' }}
ports:
- 13306:3306
@ -144,6 +173,7 @@ jobs:
ini-values: ${{ matrix.php_ini_values }}
- name: Create MySQL Database
if: ${{ matrix.service != 'sqlite:3' }}
run: |
sudo systemctl start mysql
mysql -uroot -proot -e 'CREATE DATABASE flarum_test;' --port 13306
@ -173,6 +203,7 @@ jobs:
DB_PORT: 13306
DB_PASSWORD: root
DB_PREFIX: ${{ matrix.prefix }}
DB_DRIVER: ${{ matrix.driver }}
COMPOSER_PROCESS_TIMEOUT: 600
phpstan:

View File

@ -14,6 +14,7 @@ use Flarum\Database\AbstractModel;
use Flarum\Database\ScopeVisibilityTrait;
use Flarum\Post\Post;
use Flarum\User\User;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
/**
@ -30,6 +31,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Flag extends AbstractModel
{
use ScopeVisibilityTrait;
use HasFactory;
protected $casts = ['created_at' => 'datetime'];

View File

@ -0,0 +1,30 @@
<?php
/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/
namespace Flarum\Flags;
use Carbon\Carbon;
use Flarum\Post\Post;
use Flarum\User\User;
use Illuminate\Database\Eloquent\Factories\Factory;
class FlagFactory extends Factory
{
public function definition(): array
{
return [
'type' => 'user',
'post_id' => Post::factory(),
'user_id' => User::factory(),
'reason' => $this->faker->sentence,
'reason_detail' => $this->faker->sentence,
'created_at' => Carbon::now(),
];
}
}

View File

@ -10,6 +10,7 @@
namespace Flarum\Flags\Tests\integration\api\flags;
use Flarum\Discussion\Discussion;
use Flarum\Flags\Flag;
use Flarum\Group\Group;
use Flarum\Post\Post;
use Flarum\Testing\integration\RetrievesAuthorizedUsers;
@ -55,7 +56,7 @@ class ListTest extends TestCase
['id' => 2, 'discussion_id' => 1, 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p></p></t>'],
['id' => 3, 'discussion_id' => 1, 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p></p></t>'],
],
'flags' => [
Flag::class => [
['id' => 1, 'post_id' => 1, 'user_id' => 1],
['id' => 2, 'post_id' => 1, 'user_id' => 2],
['id' => 3, 'post_id' => 1, 'user_id' => 3],

View File

@ -10,6 +10,7 @@
namespace Flarum\Flags\Tests\integration\api\flags;
use Flarum\Discussion\Discussion;
use Flarum\Flags\Flag;
use Flarum\Group\Group;
use Flarum\Post\Post;
use Flarum\Tags\Tag;
@ -83,7 +84,7 @@ class ListWithTagsTest extends TestCase
['id' => 6, 'discussion_id' => 4, 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p></p></t>'],
['id' => 7, 'discussion_id' => 5, 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p></p></t>'],
],
'flags' => [
Flag::class => [
// From regular ListTest
['id' => 1, 'post_id' => 1, 'user_id' => 1],
['id' => 2, 'post_id' => 1, 'user_id' => 2],

View File

@ -16,10 +16,9 @@ return [
$table->timestamp('created_at')->nullable();
});
// do this manually because dbal doesn't recognize timestamp columns
$connection = $schema->getConnection();
$prefix = $connection->getTablePrefix();
$connection->statement("ALTER TABLE `{$prefix}post_mentions_post` MODIFY created_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP");
$schema->table('post_mentions_post', function (Blueprint $table) {
$table->timestamp('created_at')->nullable()->useCurrent()->change();
});
},
'down' => function (Builder $schema) {

View File

@ -16,10 +16,9 @@ return [
$table->timestamp('created_at')->nullable();
});
// do this manually because dbal doesn't recognize timestamp columns
$connection = $schema->getConnection();
$prefix = $connection->getTablePrefix();
$connection->statement("ALTER TABLE `{$prefix}post_mentions_user` MODIFY created_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP");
$schema->table('post_mentions_user', function (Blueprint $table) {
$table->timestamp('created_at')->nullable()->useCurrent()->change();
});
},
'down' => function (Builder $schema) {

View File

@ -32,7 +32,7 @@ class LoadMentionedByRelationship
$query
->with(['mentionsPosts', 'mentionsPosts.user', 'mentionsPosts.discussion', 'mentionsUsers'])
->whereVisibleTo($actor)
->oldest()
->oldest('posts.created_at')
// Limiting a relationship results is only possible because
// the Post model uses the \Staudenmeir\EloquentEagerLimit\HasEagerLimit
// trait.

View File

@ -130,12 +130,16 @@ class ShowStatisticsData implements RequestHandlerInterface
$endDate = new DateTime();
}
// if within the last 24 hours, group by hour
$format = 'CASE WHEN '.$column.' > ? THEN \'%Y-%m-%d %H:00:00\' ELSE \'%Y-%m-%d\' END';
$dbFormattedDatetime = match ($query->getConnection()->getDriverName()) {
'sqlite' => 'strftime('.$format.', '.$column.')',
default => 'DATE_FORMAT('.$column.', '.$format.')',
};
$results = $query
->selectRaw(
'DATE_FORMAT(
@date := '.$column.',
IF(@date > ?, \'%Y-%m-%d %H:00:00\', \'%Y-%m-%d\') -- if within the last 24 hours, group by hour
) as time_group',
$dbFormattedDatetime.' as time_group',
[new DateTime('-25 hours')]
)
->selectRaw('COUNT(id) as count')

View File

@ -102,7 +102,7 @@ class CanRequestCustomTimedStatisticsTest extends TestCase
$body = json_decode($response->getBody()->getContents(), true);
$this->assertEquals(200, $response->getStatusCode());
$this->assertEquals(200, $response->getStatusCode(), $body['errors'][0]['detail'] ?? '');
$this->assertEquals(
$data,

View File

@ -12,6 +12,7 @@ namespace Flarum\Sticky;
use Flarum\Search\Database\DatabaseSearchState;
use Flarum\Search\SearchCriteria;
use Flarum\Tags\Search\Filter\TagFilter;
use Illuminate\Database\Query\Builder;
class PinStickiedDiscussionsToTop
{
@ -45,22 +46,26 @@ class PinStickiedDiscussionsToTop
$sticky->where('is_sticky', true);
unset($sticky->orders);
/** @var Builder $q */
foreach ([$sticky, $query] as $q) {
$read = $q->newQuery()
->selectRaw('1')
->from('discussion_user as sticky')
->whereColumn('sticky.discussion_id', 'id')
->where('sticky.user_id', '=', $state->getActor()->id)
->whereColumn('sticky.last_read_post_number', '>=', 'last_post_number');
// Add the bindings manually (rather than as the second
// argument in orderByRaw) for now due to a bug in Laravel which
// would add the bindings in the wrong order.
$q->selectRaw('(is_sticky and not exists ('.$read->toSql().') and last_posted_at > ?) as is_unread_sticky', array_merge($read->getBindings(), [$state->getActor()->marked_all_as_read_at ?: 0]));
}
$query->union($sticky);
$read = $query->newQuery()
->selectRaw('1')
->from('discussion_user as sticky')
->whereColumn('sticky.discussion_id', 'id')
->where('sticky.user_id', '=', $state->getActor()->id)
->whereColumn('sticky.last_read_post_number', '>=', 'last_post_number');
$query->orderByDesc('is_unread_sticky');
// Add the bindings manually (rather than as the second
// argument in orderByRaw) for now due to a bug in Laravel which
// would add the bindings in the wrong order.
$query->orderByRaw('is_sticky and not exists ('.$read->toSql().') and last_posted_at > ? desc')
->addBinding(array_merge($read->getBindings(), [$state->getActor()->marked_all_as_read_at ?: 0]), 'union');
$query->unionOrders = array_merge($query->unionOrders, $query->orders);
$query->unionOrders = array_merge($query->unionOrders ?? [], $query->orders ?? []);
$query->unionLimit = $query->limit;
$query->unionOffset = $query->offset;

View File

@ -14,9 +14,14 @@ return [
'up' => function (Builder $schema) {
$schema->table('tags', function (Blueprint $table) {
$table->renameColumn('discussions_count', 'discussion_count');
});
$schema->table('tags', function (Blueprint $table) {
$table->renameColumn('last_time', 'last_posted_at');
});
$schema->table('tags', function (Blueprint $table) {
$table->renameColumn('last_discussion_id', 'last_posted_discussion_id');
});
$schema->table('tags', function (Blueprint $table) {
$table->integer('parent_id')->unsigned()->nullable()->change();
$table->integer('last_posted_user_id')->unsigned()->nullable();
@ -26,11 +31,17 @@ return [
'down' => function (Builder $schema) {
$schema->table('tags', function (Blueprint $table) {
$table->dropColumn('last_posted_user_id');
});
$schema->table('tags', function (Blueprint $table) {
$table->integer('parent_id')->nullable()->change();
});
$schema->table('tags', function (Blueprint $table) {
$table->renameColumn('discussion_count', 'discussions_count');
});
$schema->table('tags', function (Blueprint $table) {
$table->renameColumn('last_posted_at', 'last_time');
});
$schema->table('tags', function (Blueprint $table) {
$table->renameColumn('last_posted_discussion_id', 'last_discussion_id');
});
}

View File

@ -17,17 +17,15 @@ return [
$table->timestamp('updated_at')->nullable();
});
// do this manually because dbal doesn't recognize timestamp columns
$connection = $schema->getConnection();
$prefix = $connection->getTablePrefix();
$connection->statement("ALTER TABLE `{$prefix}tags` MODIFY created_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP");
$connection->statement("ALTER TABLE `{$prefix}tags` MODIFY updated_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP");
$schema->table('tags', function (Blueprint $table) {
$table->timestamp('created_at')->nullable()->useCurrent()->change();
$table->timestamp('updated_at')->nullable()->useCurrent()->useCurrentOnUpdate()->change();
});
},
'down' => function (Builder $schema) {
$schema->table('tags', function (Blueprint $table) {
$table->dropColumn('created_at');
$table->dropColumn('updated_at');
$table->dropColumn('created_at', 'updated_at');
});
}
];

View File

@ -16,10 +16,9 @@ return [
$table->timestamp('created_at')->nullable();
});
// do this manually because dbal doesn't recognize timestamp columns
$connection = $schema->getConnection();
$prefix = $connection->getTablePrefix();
$connection->statement("ALTER TABLE `{$prefix}discussion_tag` MODIFY created_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP");
$schema->table('discussion_tag', function (Blueprint $table) {
$table->timestamp('created_at')->nullable()->useCurrent()->change();
});
},
'down' => function (Builder $schema) {

View File

@ -34,7 +34,7 @@ export default class StatusWidget extends DashboardWidget {
items.add('version-flarum', [<strong>Flarum</strong>, <br />, app.forum.attribute('version')], 100);
items.add('version-php', [<strong>PHP</strong>, <br />, app.data.phpVersion], 90);
items.add('version-mysql', [<strong>MySQL</strong>, <br />, app.data.mysqlVersion], 80);
items.add('version-db', [<strong>{app.data.dbDriver}</strong>, <br />, app.data.dbVersion], 80);
if (app.data.schedulerStatus) {
items.add(
'schedule-status',

View File

@ -34,7 +34,10 @@ return [
$connection = $schema->getConnection();
$prefix = $connection->getTablePrefix();
$connection->statement('ALTER TABLE '.$prefix.'posts ADD FULLTEXT content (content)');
if ($connection->getDriverName() !== 'sqlite') {
$connection->statement('ALTER TABLE '.$prefix.'posts ADD FULLTEXT content (content)');
}
},
'down' => function (Builder $schema) {

View File

@ -12,10 +12,12 @@ use Illuminate\Database\Schema\Builder;
return [
'up' => function (Builder $schema) {
$schema->table('access_tokens', function (Blueprint $table) {
$table->dropColumn('created_at', 'expires_at');
});
$schema->table('access_tokens', function (Blueprint $table) {
$table->string('id', 40)->change();
$table->dropColumn('created_at');
$table->dropColumn('expires_at');
$table->integer('last_activity');
$table->integer('lifetime');
});
@ -24,8 +26,7 @@ return [
'down' => function (Builder $schema) {
$schema->table('access_tokens', function (Blueprint $table) {
$table->string('id', 100)->change();
$table->dropColumn('last_activity');
$table->dropColumn('lifetime');
$table->dropColumn('last_activity', 'lifetime');
$table->timestamp('created_at');
$table->timestamp('expires_at');
});

View File

@ -14,14 +14,20 @@ return [
'up' => function (Builder $schema) {
$schema->table('access_tokens', function (Blueprint $table) {
$table->renameColumn('id', 'token');
});
$schema->table('access_tokens', function (Blueprint $table) {
$table->renameColumn('lifetime', 'lifetime_seconds');
});
$schema->table('access_tokens', function (Blueprint $table) {
$table->renameColumn('last_activity', 'last_activity_at');
});
$schema->table('access_tokens', function (Blueprint $table) {
$table->dateTime('created_at');
$table->integer('user_id')->unsigned()->change();
});
// Use a separate schema instance because this column gets renamed
// in the first one.
// in the previous one.
$schema->table('access_tokens', function (Blueprint $table) {
$table->dateTime('last_activity_at')->change();
});
@ -31,12 +37,19 @@ return [
$schema->table('access_tokens', function (Blueprint $table) {
$table->integer('last_activity_at')->change();
});
$schema->table('access_tokens', function (Blueprint $table) {
$table->renameColumn('token', 'id');
});
$schema->table('access_tokens', function (Blueprint $table) {
$table->renameColumn('lifetime_seconds', 'lifetime');
});
$schema->table('access_tokens', function (Blueprint $table) {
$table->renameColumn('last_activity_at', 'last_activity');
});
$schema->table('access_tokens', function (Blueprint $table) {
$table->dropColumn('created_at');
});
$schema->table('access_tokens', function (Blueprint $table) {
$table->integer('user_id')->change();
});
}

View File

@ -12,13 +12,7 @@ use Illuminate\Database\Schema\Builder;
return [
'up' => function (Builder $schema) {
$schema->table('api_keys', function (Blueprint $table) {
$table->dropPrimary(['id']);
$table->renameColumn('id', 'key');
$table->unique('key');
});
$schema->table('api_keys', function (Blueprint $table) {
$definition = function (Blueprint $table) {
$table->increments('id');
$table->string('allowed_ips')->nullable();
$table->string('scopes')->nullable();
@ -27,7 +21,23 @@ return [
$table->dateTime('last_activity_at')->nullable();
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
});
};
if ($schema->getConnection()->getDriverName() !== 'sqlite') {
$schema->table('api_keys', function (Blueprint $table) {
$table->dropPrimary(['id']);
$table->renameColumn('id', 'key');
$table->unique('key');
});
$schema->table('api_keys', $definition);
} else {
$schema->drop('api_keys');
$schema->create('api_keys', function (Blueprint $table) use ($definition) {
$table->string('key', 100)->unique();
$definition($table);
});
}
},
'down' => function (Builder $schema) {
@ -36,10 +46,13 @@ return [
$table->dropColumn('id', 'allowed_ips', 'user_id', 'scopes', 'created_at');
});
$schema->table('api_keys', function (Blueprint $table) {
$schema->table('api_keys', function (Blueprint $table) use ($schema) {
$table->dropUnique(['key']);
$table->renameColumn('key', 'id');
$table->primary('id');
if ($schema->getConnection()->getDriverName() !== 'sqlite') {
$table->primary('id');
}
});
}
];

View File

@ -7,19 +7,19 @@
* LICENSE file that was distributed with this source code.
*/
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Schema\Builder;
return [
'up' => function (Builder $schema) {
// do this manually because dbal doesn't recognize timestamp columns
$connection = $schema->getConnection();
$prefix = $connection->getTablePrefix();
$connection->statement("ALTER TABLE {$prefix}registration_tokens MODIFY created_at DATETIME");
$schema->table('registration_tokens', function (Blueprint $table) {
$table->dateTime('created_at')->change();
});
},
'down' => function (Builder $schema) {
$connection = $schema->getConnection();
$prefix = $connection->getTablePrefix();
$connection->statement("ALTER TABLE {$prefix}registration_tokens MODIFY created_at TIMESTAMP");
$schema->table('registration_tokens', function (Blueprint $table) {
$table->timestamp('created_at')->change();
});
}
];

View File

@ -12,13 +12,19 @@ use Illuminate\Database\Schema\Builder;
return [
'up' => function (Builder $schema) {
$connection = $schema->getConnection();
$prefix = $connection->getTablePrefix();
$connection->statement('ALTER TABLE '.$prefix.'posts ENGINE = InnoDB');
if ($connection->getDriverName() === 'mysql') {
$prefix = $connection->getTablePrefix();
$connection->statement('ALTER TABLE '.$prefix.'posts ENGINE = InnoDB');
}
},
'down' => function (Builder $schema) {
$connection = $schema->getConnection();
$prefix = $connection->getTablePrefix();
$connection->statement('ALTER TABLE '.$prefix.'posts ENGINE = MyISAM');
if ($connection->getDriverName() === 'mysql') {
$prefix = $connection->getTablePrefix();
$connection->statement('ALTER TABLE '.$prefix.'posts ENGINE = MyISAM');
}
}
];

View File

@ -7,19 +7,19 @@
* LICENSE file that was distributed with this source code.
*/
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Schema\Builder;
return [
'up' => function (Builder $schema) {
// do this manually because dbal doesn't recognize timestamp columns
$connection = $schema->getConnection();
$prefix = $connection->getTablePrefix();
$connection->statement("ALTER TABLE {$prefix}email_tokens MODIFY created_at DATETIME");
$schema->table('email_tokens', function (Blueprint $table) {
$table->dateTime('created_at')->change();
});
},
'down' => function (Builder $schema) {
$connection = $schema->getConnection();
$prefix = $connection->getTablePrefix();
$connection->statement("ALTER TABLE {$prefix}email_tokens MODIFY created_at TIMESTAMP");
$schema->table('email_tokens', function (Blueprint $table) {
$table->timestamp('created_at')->change();
});
}
];

View File

@ -15,10 +15,14 @@ return [
'up' => function (Builder $schema) {
$schema->table('notifications', function (Blueprint $table) {
$table->dropColumn('subject_type');
});
$schema->table('notifications', function (Blueprint $table) {
$table->renameColumn('time', 'created_at');
});
$schema->table('notifications', function (Blueprint $table) {
$table->renameColumn('sender_id', 'from_user_id');
});
$schema->table('notifications', function (Blueprint $table) {
$table->dateTime('read_at')->nullable();
});
@ -36,8 +40,11 @@ return [
$table->string('subject_type', 200)->nullable();
$table->renameColumn('created_at', 'time');
});
$schema->table('notifications', function (Blueprint $table) {
$table->renameColumn('from_user_id', 'sender_id');
});
$schema->table('notifications', function (Blueprint $table) {
$table->boolean('is_read');
});

View File

@ -7,19 +7,19 @@
* LICENSE file that was distributed with this source code.
*/
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Schema\Builder;
return [
'up' => function (Builder $schema) {
// do this manually because dbal doesn't recognize timestamp columns
$connection = $schema->getConnection();
$prefix = $connection->getTablePrefix();
$connection->statement("ALTER TABLE {$prefix}password_tokens MODIFY created_at DATETIME");
$schema->table('password_tokens', function (Blueprint $table) {
$table->dateTime('created_at')->change();
});
},
'down' => function (Builder $schema) {
$connection = $schema->getConnection();
$prefix = $connection->getTablePrefix();
$connection->statement("ALTER TABLE {$prefix}password_tokens MODIFY created_at TIMESTAMP");
$schema->table('password_tokens', function (Blueprint $table) {
$table->timestamp('created_at')->change();
});
}
];

View File

@ -13,7 +13,10 @@ return [
'up' => function (Builder $schema) {
$connection = $schema->getConnection();
$prefix = $connection->getTablePrefix();
$connection->statement('ALTER TABLE '.$prefix.'discussions ADD FULLTEXT title (title)');
if ($connection->getDriverName() !== 'sqlite') {
$connection->statement('ALTER TABLE '.$prefix.'discussions ADD FULLTEXT title (title)');
}
},
'down' => function (Builder $schema) {

View File

@ -12,24 +12,42 @@ use Illuminate\Database\Schema\Builder;
return [
'up' => function (Builder $schema) {
$schema->table('access_tokens', function (Blueprint $table) {
// Replace primary key with unique index so we can create a new primary
$table->dropPrimary('token');
$table->unique('token');
});
if ($schema->getConnection()->getDriverName() !== 'sqlite') {
$schema->table('access_tokens', function (Blueprint $table) {
// Replace primary key with unique index so we can create a new primary
$table->dropPrimary('token');
$table->unique('token');
});
// This needs to be done in a second statement because of the order Laravel runs operations in
$schema->table('access_tokens', function (Blueprint $table) {
// Introduce new increment-based ID
$table->increments('id')->first();
});
// This needs to be done in a second statement because of the order Laravel runs operations in
$schema->table('access_tokens', function (Blueprint $table) {
// Introduce new increment-based ID
$table->increments('id')->first();
});
} else {
$schema->drop('access_tokens');
$schema->create('access_tokens', function (Blueprint $table) {
$table->increments('id');
$table->string('token', 100)->unique();
$table->integer('user_id')->unsigned();
$table->dateTime('last_activity_at')->nullable();
$table->dateTime('created_at');
$table->string('type', 100)->nullable();
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
$table->index('type');
});
}
},
'down' => function (Builder $schema) {
$schema->table('access_tokens', function (Blueprint $table) {
$schema->table('access_tokens', function (Blueprint $table) use ($schema) {
$table->dropColumn('id');
$table->dropIndex('token');
$table->primary('token');
if ($schema->getConnection()->getDriverName() !== 'sqlite') {
$table->primary('token');
}
});
}
];

View File

@ -17,17 +17,15 @@ return [
$table->timestamp('updated_at')->nullable();
});
// do this manually because dbal doesn't recognize timestamp columns
$connection = $schema->getConnection();
$prefix = $connection->getTablePrefix();
$connection->statement("ALTER TABLE `${prefix}groups` MODIFY created_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP");
$connection->statement("ALTER TABLE `${prefix}groups` MODIFY updated_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP");
$schema->table('groups', function (Blueprint $table) {
$table->timestamp('created_at')->nullable()->useCurrent()->change();
$table->timestamp('updated_at')->nullable()->useCurrentOnUpdate()->change();
});
},
'down' => function (Builder $schema) {
$schema->table('groups', function (Blueprint $table) {
$table->dropColumn('created_at');
$table->dropColumn('updated_at');
$table->dropColumn('created_at', 'updated_at');
});
}
];

View File

@ -16,10 +16,9 @@ return [
$table->timestamp('created_at')->nullable();
});
// do this manually because dbal doesn't recognize timestamp columns
$connection = $schema->getConnection();
$prefix = $connection->getTablePrefix();
$connection->statement("ALTER TABLE `${prefix}group_user` MODIFY created_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP");
$schema->table('group_user', function (Blueprint $table) {
$table->timestamp('created_at')->nullable()->useCurrent()->change();
});
},
'down' => function (Builder $schema) {

View File

@ -16,10 +16,9 @@ return [
$table->timestamp('created_at')->nullable();
});
// do this manually because dbal doesn't recognize timestamp columns
$connection = $schema->getConnection();
$prefix = $connection->getTablePrefix();
$connection->statement("ALTER TABLE `${prefix}group_permission` MODIFY created_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP");
$schema->table('group_permission', function (Blueprint $table) {
$table->timestamp('created_at')->nullable()->useCurrent()->change();
});
},
'down' => function (Builder $schema) {

View File

@ -0,0 +1,47 @@
<?php
/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/
use Illuminate\Database\Schema\Builder;
return [
'up' => function (Builder $schema) {
if ($schema->getConnection()->getDriverName() === 'sqlite') {
$schema->getConnection()->statement('PRAGMA foreign_keys = OFF');
$schema->getConnection()->statement('PRAGMA writable_schema = ON');
$prefix = $schema->getConnection()->getTablePrefix();
$foreignKeysSqlite = [
'discussions' => <<<SQL
foreign key("user_id") references "{$prefix}users"("id") on delete set null,
foreign key("last_posted_user_id") references "{$prefix}users"("id") on delete set null,
foreign key("hidden_user_id") references "{$prefix}users"("id") on delete set null,
foreign key("first_post_id") references "{$prefix}posts"("id") on delete set null,
foreign key("last_post_id") references "{$prefix}posts"("id") on delete set null
SQL,
'posts' => <<<SQL
foreign key("discussion_id") references "{$prefix}discussions"("id") on delete cascade,
foreign key("user_id") references "{$prefix}users"("id") on delete set null,
foreign key("edited_user_id") references "{$prefix}users"("id") on delete set null,
foreign key("hidden_user_id") references "{$prefix}users"("id") on delete set null
SQL,
];
foreach (['discussions', 'posts'] as $table) {
$tableDefinition = $schema->getConnection()->select('SELECT sql FROM sqlite_master WHERE type = "table" AND name = "'.$prefix.$table.'"')[0]->sql;
$modifiedTableDefinition = str($tableDefinition)->beforeLast(')')->append(', '.$foreignKeysSqlite[$table].')')->toString();
$modifiedTableDefinitionWithEscapedQuotes = str($modifiedTableDefinition)->replace('"', '""')->toString();
$schema->getConnection()->statement('UPDATE sqlite_master SET sql = "'.$modifiedTableDefinitionWithEscapedQuotes.'" WHERE type = "table" AND name = "'.$prefix.$table.'"');
}
$schema->getConnection()->statement('PRAGMA writable_schema = OFF');
$schema->getConnection()->statement('PRAGMA foreign_keys = ON');
}
},
];

View File

@ -0,0 +1,132 @@
CREATE TABLE IF NOT EXISTS "db_prefix_migrations" ("id" integer primary key autoincrement not null, "migration" varchar not null, "extension" varchar);
CREATE TABLE IF NOT EXISTS "db_prefix_groups" ("id" integer primary key autoincrement not null, "name_singular" varchar not null, "name_plural" varchar not null, "color" varchar, "icon" varchar, "is_hidden" tinyint(1) not null default '0', "created_at" datetime, "updated_at" datetime);
CREATE TABLE IF NOT EXISTS "db_prefix_group_permission" ("group_id" integer not null, "permission" varchar not null, "created_at" datetime, primary key ("group_id", "permission"));
CREATE TABLE IF NOT EXISTS "db_prefix_group_user" ("user_id" integer not null, "group_id" integer not null, "created_at" datetime, primary key ("user_id", "group_id"));
CREATE TABLE db_prefix_settings ("key" VARCHAR(255) NOT NULL, value CLOB DEFAULT NULL, PRIMARY KEY("key"));
CREATE TABLE IF NOT EXISTS "db_prefix_api_keys" ("key" varchar not null, "id" integer primary key autoincrement not null, "allowed_ips" varchar, "scopes" varchar, "user_id" integer, "created_at" datetime not null, "last_activity_at" datetime, foreign key("user_id") references "db_prefix_users"("id") on delete cascade);
CREATE UNIQUE INDEX "db_prefix_api_keys_key_unique" on "db_prefix_api_keys" ("key");
CREATE TABLE db_prefix_discussion_user (user_id INTEGER NOT NULL, discussion_id INTEGER NOT NULL, last_read_at DATETIME DEFAULT NULL, last_read_post_number INTEGER DEFAULT NULL, PRIMARY KEY(user_id, discussion_id));
CREATE TABLE db_prefix_email_tokens (token VARCHAR(255) NOT NULL, email VARCHAR(255) NOT NULL COLLATE "BINARY", user_id INTEGER NOT NULL, created_at DATETIME NOT NULL, PRIMARY KEY(token));
CREATE TABLE db_prefix_notifications (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, user_id INTEGER NOT NULL, from_user_id INTEGER DEFAULT NULL, type VARCHAR(255) NOT NULL COLLATE "BINARY", subject_id INTEGER DEFAULT NULL, data BLOB DEFAULT NULL, created_at DATETIME NOT NULL, is_deleted BOOLEAN DEFAULT 0 NOT NULL, read_at DATETIME DEFAULT NULL);
CREATE TABLE db_prefix_password_tokens (token VARCHAR(255) NOT NULL, user_id INTEGER NOT NULL, created_at DATETIME NOT NULL, PRIMARY KEY(token));
CREATE TABLE IF NOT EXISTS "db_prefix_post_user" ("post_id" integer not null, "user_id" integer not null, foreign key("post_id") references "db_prefix_posts"("id") on delete cascade, foreign key("user_id") references "db_prefix_users"("id") on delete cascade, primary key ("post_id", "user_id"));
CREATE TABLE db_prefix_users (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, username VARCHAR(255) NOT NULL COLLATE "BINARY", email VARCHAR(255) NOT NULL COLLATE "BINARY", is_email_confirmed BOOLEAN DEFAULT 0 NOT NULL, password VARCHAR(255) NOT NULL COLLATE "BINARY", avatar_url VARCHAR(255) DEFAULT NULL, preferences BLOB DEFAULT NULL, joined_at DATETIME DEFAULT NULL, last_seen_at DATETIME DEFAULT NULL, marked_all_as_read_at DATETIME DEFAULT NULL, read_notifications_at DATETIME DEFAULT NULL, discussion_count INTEGER DEFAULT 0 NOT NULL, comment_count INTEGER DEFAULT 0 NOT NULL);
CREATE UNIQUE INDEX db_prefix_users_email_unique ON db_prefix_users (email);
CREATE UNIQUE INDEX db_prefix_users_username_unique ON db_prefix_users (username);
CREATE INDEX "db_prefix_users_joined_at_index" on "db_prefix_users" ("joined_at");
CREATE INDEX "db_prefix_users_last_seen_at_index" on "db_prefix_users" ("last_seen_at");
CREATE INDEX "db_prefix_users_discussion_count_index" on "db_prefix_users" ("discussion_count");
CREATE INDEX "db_prefix_users_comment_count_index" on "db_prefix_users" ("comment_count");
CREATE INDEX "db_prefix_notifications_user_id_index" on "db_prefix_notifications" ("user_id");
CREATE TABLE db_prefix_registration_tokens (token VARCHAR(255) NOT NULL, payload CLOB DEFAULT NULL, created_at DATETIME NOT NULL, "provider" varchar not null, "identifier" varchar not null, "user_attributes" text, PRIMARY KEY(token));
CREATE TABLE IF NOT EXISTS "db_prefix_login_providers" ("id" integer primary key autoincrement not null, "user_id" integer not null, "provider" varchar not null, "identifier" varchar not null, "created_at" datetime, "last_login_at" datetime, foreign key("user_id") references "db_prefix_users"("id") on delete cascade);
CREATE UNIQUE INDEX "db_prefix_login_providers_provider_identifier_unique" on "db_prefix_login_providers" ("provider", "identifier");
CREATE TABLE IF NOT EXISTS "db_prefix_access_tokens" ("id" integer primary key autoincrement not null, "token" varchar not null, "user_id" integer not null, "last_activity_at" datetime, "created_at" datetime not null, "type" varchar, "title" varchar, "last_ip_address" varchar, "last_user_agent" varchar, foreign key("user_id") references "db_prefix_users"("id") on delete cascade);
CREATE INDEX "db_prefix_access_tokens_type_index" on "db_prefix_access_tokens" ("type");
CREATE UNIQUE INDEX "db_prefix_access_tokens_token_unique" on "db_prefix_access_tokens" ("token");
CREATE TABLE db_prefix_posts (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, discussion_id INTEGER NOT NULL, number INTEGER DEFAULT NULL, created_at DATETIME NOT NULL, user_id INTEGER DEFAULT NULL, type VARCHAR(255) DEFAULT NULL, content CLOB DEFAULT NULL --
, edited_at DATETIME DEFAULT NULL, edited_user_id INTEGER DEFAULT NULL, hidden_at DATETIME DEFAULT NULL, hidden_user_id INTEGER DEFAULT NULL, ip_address VARCHAR(255) DEFAULT NULL, is_private BOOLEAN DEFAULT 0 NOT NULL, foreign key("discussion_id") references "db_prefix_discussions"("id") on delete cascade,
foreign key("user_id") references "db_prefix_users"("id") on delete set null,
foreign key("edited_user_id") references "db_prefix_users"("id") on delete set null,
foreign key("hidden_user_id") references "db_prefix_users"("id") on delete set null);
CREATE INDEX db_prefix_posts_user_id_created_at_index ON db_prefix_posts (user_id, created_at);
CREATE INDEX db_prefix_posts_discussion_id_created_at_index ON db_prefix_posts (discussion_id, created_at);
CREATE INDEX db_prefix_posts_discussion_id_number_index ON db_prefix_posts (discussion_id, number);
CREATE UNIQUE INDEX db_prefix_posts_discussion_id_number_unique ON db_prefix_posts (discussion_id, number);
CREATE INDEX "db_prefix_posts_type_index" on "db_prefix_posts" ("type");
CREATE INDEX "db_prefix_posts_type_created_at_index" on "db_prefix_posts" ("type", "created_at");
CREATE TABLE IF NOT EXISTS "db_prefix_unsubscribe_tokens" ("id" integer primary key autoincrement not null, "user_id" integer not null, "email_type" varchar not null, "token" varchar not null, "unsubscribed_at" datetime, "created_at" datetime, "updated_at" datetime, foreign key("user_id") references "db_prefix_users"("id") on delete cascade);
CREATE INDEX "db_prefix_unsubscribe_tokens_user_id_index" on "db_prefix_unsubscribe_tokens" ("user_id");
CREATE INDEX "db_prefix_unsubscribe_tokens_email_type_index" on "db_prefix_unsubscribe_tokens" ("email_type");
CREATE INDEX "db_prefix_unsubscribe_tokens_token_index" on "db_prefix_unsubscribe_tokens" ("token");
CREATE INDEX "db_prefix_unsubscribe_tokens_user_id_email_type_index" on "db_prefix_unsubscribe_tokens" ("user_id", "email_type");
CREATE UNIQUE INDEX "db_prefix_unsubscribe_tokens_token_unique" on "db_prefix_unsubscribe_tokens" ("token");
CREATE TABLE db_prefix_discussions (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, title VARCHAR(255) NOT NULL COLLATE "BINARY", comment_count INTEGER DEFAULT 1 NOT NULL, participant_count INTEGER DEFAULT 0 NOT NULL, created_at DATETIME NOT NULL, user_id INTEGER DEFAULT NULL, first_post_id INTEGER DEFAULT NULL, last_posted_at DATETIME DEFAULT NULL, last_posted_user_id INTEGER DEFAULT NULL, last_post_id INTEGER DEFAULT NULL, last_post_number INTEGER DEFAULT NULL, hidden_at DATETIME DEFAULT NULL, hidden_user_id INTEGER DEFAULT NULL, slug VARCHAR(255) NOT NULL COLLATE "BINARY", is_private BOOLEAN DEFAULT 0 NOT NULL, foreign key("user_id") references "db_prefix_users"("id") on delete set null,
foreign key("last_posted_user_id") references "db_prefix_users"("id") on delete set null,
foreign key("hidden_user_id") references "db_prefix_users"("id") on delete set null,
foreign key("first_post_id") references "db_prefix_posts"("id") on delete set null,
foreign key("last_post_id") references "db_prefix_posts"("id") on delete set null);
CREATE INDEX db_prefix_discussions_last_posted_at_index ON db_prefix_discussions (last_posted_at);
CREATE INDEX db_prefix_discussions_last_posted_user_id_index ON db_prefix_discussions (last_posted_user_id);
CREATE INDEX db_prefix_discussions_created_at_index ON db_prefix_discussions (created_at);
CREATE INDEX db_prefix_discussions_user_id_index ON db_prefix_discussions (user_id);
CREATE INDEX db_prefix_discussions_comment_count_index ON db_prefix_discussions (comment_count);
CREATE INDEX db_prefix_discussions_participant_count_index ON db_prefix_discussions (participant_count);
CREATE INDEX db_prefix_discussions_hidden_at_index ON db_prefix_discussions (hidden_at);
INSERT INTO db_prefix_migrations VALUES(1,'2015_02_24_000000_create_access_tokens_table',NULL);
INSERT INTO db_prefix_migrations VALUES(2,'2015_02_24_000000_create_api_keys_table',NULL);
INSERT INTO db_prefix_migrations VALUES(3,'2015_02_24_000000_create_config_table',NULL);
INSERT INTO db_prefix_migrations VALUES(4,'2015_02_24_000000_create_discussions_table',NULL);
INSERT INTO db_prefix_migrations VALUES(5,'2015_02_24_000000_create_email_tokens_table',NULL);
INSERT INTO db_prefix_migrations VALUES(6,'2015_02_24_000000_create_groups_table',NULL);
INSERT INTO db_prefix_migrations VALUES(7,'2015_02_24_000000_create_notifications_table',NULL);
INSERT INTO db_prefix_migrations VALUES(8,'2015_02_24_000000_create_password_tokens_table',NULL);
INSERT INTO db_prefix_migrations VALUES(9,'2015_02_24_000000_create_permissions_table',NULL);
INSERT INTO db_prefix_migrations VALUES(10,'2015_02_24_000000_create_posts_table',NULL);
INSERT INTO db_prefix_migrations VALUES(11,'2015_02_24_000000_create_users_discussions_table',NULL);
INSERT INTO db_prefix_migrations VALUES(12,'2015_02_24_000000_create_users_groups_table',NULL);
INSERT INTO db_prefix_migrations VALUES(13,'2015_02_24_000000_create_users_table',NULL);
INSERT INTO db_prefix_migrations VALUES(14,'2015_09_15_000000_create_auth_tokens_table',NULL);
INSERT INTO db_prefix_migrations VALUES(15,'2015_09_20_224327_add_hide_to_discussions',NULL);
INSERT INTO db_prefix_migrations VALUES(16,'2015_09_22_030432_rename_notification_read_time',NULL);
INSERT INTO db_prefix_migrations VALUES(17,'2015_10_07_130531_rename_config_to_settings',NULL);
INSERT INTO db_prefix_migrations VALUES(18,'2015_10_24_194000_add_ip_address_to_posts',NULL);
INSERT INTO db_prefix_migrations VALUES(19,'2015_12_05_042721_change_access_tokens_columns',NULL);
INSERT INTO db_prefix_migrations VALUES(20,'2015_12_17_194247_change_settings_value_column_to_text',NULL);
INSERT INTO db_prefix_migrations VALUES(21,'2016_02_04_095452_add_slug_to_discussions',NULL);
INSERT INTO db_prefix_migrations VALUES(22,'2017_04_07_114138_add_is_private_to_discussions',NULL);
INSERT INTO db_prefix_migrations VALUES(23,'2017_04_07_114138_add_is_private_to_posts',NULL);
INSERT INTO db_prefix_migrations VALUES(24,'2018_01_11_093900_change_access_tokens_columns',NULL);
INSERT INTO db_prefix_migrations VALUES(25,'2018_01_11_094000_change_access_tokens_add_foreign_keys',NULL);
INSERT INTO db_prefix_migrations VALUES(26,'2018_01_11_095000_change_api_keys_columns',NULL);
INSERT INTO db_prefix_migrations VALUES(27,'2018_01_11_101800_rename_auth_tokens_to_registration_tokens',NULL);
INSERT INTO db_prefix_migrations VALUES(28,'2018_01_11_102000_change_registration_tokens_rename_id_to_token',NULL);
INSERT INTO db_prefix_migrations VALUES(29,'2018_01_11_102100_change_registration_tokens_created_at_to_datetime',NULL);
INSERT INTO db_prefix_migrations VALUES(30,'2018_01_11_120604_change_posts_table_to_innodb',NULL);
INSERT INTO db_prefix_migrations VALUES(31,'2018_01_11_155200_change_discussions_rename_columns',NULL);
INSERT INTO db_prefix_migrations VALUES(32,'2018_01_11_155300_change_discussions_add_foreign_keys',NULL);
INSERT INTO db_prefix_migrations VALUES(33,'2018_01_15_071700_rename_users_discussions_to_discussion_user',NULL);
INSERT INTO db_prefix_migrations VALUES(34,'2018_01_15_071800_change_discussion_user_rename_columns',NULL);
INSERT INTO db_prefix_migrations VALUES(35,'2018_01_15_071900_change_discussion_user_add_foreign_keys',NULL);
INSERT INTO db_prefix_migrations VALUES(36,'2018_01_15_072600_change_email_tokens_rename_id_to_token',NULL);
INSERT INTO db_prefix_migrations VALUES(37,'2018_01_15_072700_change_email_tokens_add_foreign_keys',NULL);
INSERT INTO db_prefix_migrations VALUES(38,'2018_01_15_072800_change_email_tokens_created_at_to_datetime',NULL);
INSERT INTO db_prefix_migrations VALUES(39,'2018_01_18_130400_rename_permissions_to_group_permission',NULL);
INSERT INTO db_prefix_migrations VALUES(40,'2018_01_18_130500_change_group_permission_add_foreign_keys',NULL);
INSERT INTO db_prefix_migrations VALUES(41,'2018_01_18_130600_rename_users_groups_to_group_user',NULL);
INSERT INTO db_prefix_migrations VALUES(42,'2018_01_18_130700_change_group_user_add_foreign_keys',NULL);
INSERT INTO db_prefix_migrations VALUES(43,'2018_01_18_133000_change_notifications_columns',NULL);
INSERT INTO db_prefix_migrations VALUES(44,'2018_01_18_133100_change_notifications_add_foreign_keys',NULL);
INSERT INTO db_prefix_migrations VALUES(45,'2018_01_18_134400_change_password_tokens_rename_id_to_token',NULL);
INSERT INTO db_prefix_migrations VALUES(46,'2018_01_18_134500_change_password_tokens_add_foreign_keys',NULL);
INSERT INTO db_prefix_migrations VALUES(47,'2018_01_18_134600_change_password_tokens_created_at_to_datetime',NULL);
INSERT INTO db_prefix_migrations VALUES(48,'2018_01_18_135000_change_posts_rename_columns',NULL);
INSERT INTO db_prefix_migrations VALUES(49,'2018_01_18_135100_change_posts_add_foreign_keys',NULL);
INSERT INTO db_prefix_migrations VALUES(50,'2018_01_30_112238_add_fulltext_index_to_discussions_title',NULL);
INSERT INTO db_prefix_migrations VALUES(51,'2018_01_30_220100_create_post_user_table',NULL);
INSERT INTO db_prefix_migrations VALUES(52,'2018_01_30_222900_change_users_rename_columns',NULL);
INSERT INTO db_prefix_migrations VALUES(55,'2018_09_15_041340_add_users_indicies',NULL);
INSERT INTO db_prefix_migrations VALUES(56,'2018_09_15_041828_add_discussions_indicies',NULL);
INSERT INTO db_prefix_migrations VALUES(57,'2018_09_15_043337_add_notifications_indices',NULL);
INSERT INTO db_prefix_migrations VALUES(58,'2018_09_15_043621_add_posts_indices',NULL);
INSERT INTO db_prefix_migrations VALUES(59,'2018_09_22_004100_change_registration_tokens_columns',NULL);
INSERT INTO db_prefix_migrations VALUES(60,'2018_09_22_004200_create_login_providers_table',NULL);
INSERT INTO db_prefix_migrations VALUES(61,'2018_10_08_144700_add_shim_prefix_to_group_icons',NULL);
INSERT INTO db_prefix_migrations VALUES(62,'2019_10_12_195349_change_posts_add_discussion_foreign_key',NULL);
INSERT INTO db_prefix_migrations VALUES(63,'2020_03_19_134512_change_discussions_default_comment_count',NULL);
INSERT INTO db_prefix_migrations VALUES(64,'2020_04_21_130500_change_permission_groups_add_is_hidden',NULL);
INSERT INTO db_prefix_migrations VALUES(65,'2021_03_02_040000_change_access_tokens_add_type',NULL);
INSERT INTO db_prefix_migrations VALUES(66,'2021_03_02_040500_change_access_tokens_add_id',NULL);
INSERT INTO db_prefix_migrations VALUES(67,'2021_03_02_041000_change_access_tokens_add_title_ip_agent',NULL);
INSERT INTO db_prefix_migrations VALUES(68,'2021_04_18_040500_change_migrations_add_id_primary_key',NULL);
INSERT INTO db_prefix_migrations VALUES(69,'2021_04_18_145100_change_posts_content_column_to_mediumtext',NULL);
INSERT INTO db_prefix_migrations VALUES(70,'2021_05_10_000000_rename_permissions',NULL);
INSERT INTO db_prefix_migrations VALUES(71,'2022_05_20_000000_add_timestamps_to_groups_table',NULL);
INSERT INTO db_prefix_migrations VALUES(72,'2022_05_20_000001_add_created_at_to_group_user_table',NULL);
INSERT INTO db_prefix_migrations VALUES(73,'2022_05_20_000002_add_created_at_to_group_permission_table',NULL);
INSERT INTO db_prefix_migrations VALUES(74,'2022_07_14_000000_add_type_index_to_posts',NULL);
INSERT INTO db_prefix_migrations VALUES(75,'2022_07_14_000001_add_type_created_at_composite_index_to_posts',NULL);
INSERT INTO db_prefix_migrations VALUES(76,'2022_08_06_000000_change_access_tokens_last_activity_at_to_nullable',NULL);
INSERT INTO db_prefix_migrations VALUES(77,'2023_08_19_000000_create_unsubscribe_tokens_table',NULL);
INSERT INTO db_prefix_migrations VALUES(78,'2023_10_23_000000_drop_post_number_index_column_from_discussions_table',NULL);
INSERT INTO db_prefix_migrations VALUES(79,'2024_05_05_000000_add_sqlite_keys',NULL);

View File

@ -60,7 +60,8 @@ class AdminPayload
$document->payload['searchDrivers'] = $this->getSearchDrivers();
$document->payload['phpVersion'] = $this->appInfo->identifyPHPVersion();
$document->payload['mysqlVersion'] = $this->appInfo->identifyDatabaseVersion();
$document->payload['dbDriver'] = $this->appInfo->identifyDatabaseDriver();
$document->payload['dbVersion'] = $this->appInfo->identifyDatabaseVersion();
$document->payload['debugEnabled'] = Arr::get($this->config, 'debug');
if ($this->appInfo->scheduledTasksRegistered()) {

View File

@ -31,13 +31,14 @@ class Index
$extensionsEnabled = json_decode($this->settings->get('extensions_enabled', '{}'), true);
$csrfToken = $request->getAttribute('session')->token();
$mysqlVersion = $document->payload['mysqlVersion'];
$dbDriver = $document->payload['dbDriver'];
$dbVersion = $document->payload['dbVersion'];
$phpVersion = $document->payload['phpVersion'];
$flarumVersion = Application::VERSION;
$document->content = $this->view->make(
'flarum.admin::frontend.content.admin',
compact('extensions', 'extensionsEnabled', 'csrfToken', 'flarumVersion', 'phpVersion', 'mysqlVersion')
compact('extensions', 'extensionsEnabled', 'csrfToken', 'flarumVersion', 'phpVersion', 'dbVersion', 'dbDriver')
);
return $document;

View File

@ -68,6 +68,13 @@ abstract class AbstractModel extends Eloquent
*/
protected ?string $tableAlias = null;
/**
* If a model has unique keys, they should be defined here.
*
* @var array<string>|null
*/
public ?array $uniqueKeys = null;
public static function boot()
{
parent::boot();

View File

@ -10,6 +10,7 @@
namespace Flarum\Database\Console;
use Flarum\Console\AbstractCommand;
use Flarum\Foundation\Config;
use Flarum\Foundation\Paths;
use Illuminate\Database\Connection;
use Illuminate\Database\MySqlConnection;
@ -19,6 +20,7 @@ class GenerateDumpCommand extends AbstractCommand
{
public function __construct(
protected Connection $connection,
protected Config $config,
protected Paths $paths
) {
parent::__construct();
@ -33,7 +35,8 @@ class GenerateDumpCommand extends AbstractCommand
protected function fire(): int
{
$dumpPath = __DIR__.'/../../../migrations/install.dump';
$driver = $this->config['database.driver'];
$dumpPath = __DIR__."/../../../migrations/$driver-install.dump";
/** @var Connection&MySqlConnection $connection */
$connection = resolve('db.connection');

View File

@ -49,6 +49,26 @@ class DatabaseMigrationRepository implements MigrationRepositoryInterface
$query->delete();
}
/**
* Create the migration repository data store.
*
* @return void
*/
public function createRepository(): void
{
if ($this->repositoryExists()) {
return;
}
$schema = $this->connection->getSchemaBuilder();
$schema->create($this->table, function ($table) {
$table->increments('id');
$table->string('migration');
$table->string('extension')->nullable();
});
}
public function repositoryExists(): bool
{
$schema = $this->connection->getSchemaBuilder();

View File

@ -12,6 +12,7 @@ namespace Flarum\Database;
use Faker\Factory as FakerFactory;
use Faker\Generator as FakerGenerator;
use Flarum\Foundation\AbstractServiceProvider;
use Flarum\Foundation\Paths;
use Illuminate\Container\Container as ContainerImplementation;
use Illuminate\Contracts\Container\Container;
use Illuminate\Database\Capsule\Manager;
@ -32,7 +33,13 @@ class DatabaseServiceProvider extends AbstractServiceProvider
$manager = new Manager($container);
$config = $container['flarum']->config('database');
$config['engine'] = 'InnoDB';
if ($config['driver'] === 'mysql') {
$config['engine'] = 'InnoDB';
} elseif ($config['driver'] === 'sqlite' && ! file_exists($config['database'])) {
$config['database'] = $container->make(Paths::class)->base.'/'.$config['database'];
}
$config['prefix_indexes'] = true;
$manager->addConnection($config, 'flarum');

View File

@ -101,18 +101,18 @@ abstract class Migration
{
return [
'up' => function (Builder $schema) use ($tableName, $columnNames) {
$schema->table($tableName, function (Blueprint $table) use ($columnNames) {
foreach ($columnNames as $from => $to) {
foreach ($columnNames as $from => $to) {
$schema->table($tableName, function (Blueprint $table) use ($from, $to) {
$table->renameColumn($from, $to);
}
});
});
}
},
'down' => function (Builder $schema) use ($tableName, $columnNames) {
$schema->table($tableName, function (Blueprint $table) use ($columnNames) {
foreach ($columnNames as $to => $from) {
foreach ($columnNames as $to => $from) {
$schema->table($tableName, function (Blueprint $table) use ($from, $to) {
$table->renameColumn($from, $to);
}
});
});
}
}
];
}

View File

@ -9,13 +9,13 @@
namespace Flarum\Database;
use Doctrine\DBAL\Types\Type;
use Flarum\Database\Exception\MigrationKeyMissing;
use Flarum\Extension\Extension;
use Illuminate\Contracts\Filesystem\FileNotFoundException;
use Illuminate\Database\ConnectionInterface;
use Illuminate\Database\MySqlConnection;
use Illuminate\Database\DBAL\TimestampType;
use Illuminate\Filesystem\Filesystem;
use InvalidArgumentException;
use RuntimeException;
use Symfony\Component\Console\Output\OutputInterface;
@ -31,12 +31,14 @@ class Migrator
protected ConnectionInterface $connection,
protected Filesystem $files
) {
if (! ($connection instanceof MySqlConnection)) {
throw new InvalidArgumentException('Only MySQL connections are supported');
$doctrine = $connection->getDoctrineConnection()->getDatabasePlatform();
if (! Type::hasType('timestamp')) {
Type::addType('timestamp', TimestampType::class);
}
// Workaround for https://github.com/laravel/framework/issues/1186
$connection->getDoctrineSchemaManager()->getDatabasePlatform()->registerDoctrineTypeMapping('enum', 'string');
$doctrine->registerDoctrineTypeMapping('enum', 'string');
}
/**
@ -207,9 +209,13 @@ class Migrator
*
* @param string $path to the directory containing the dump.
*/
public function installFromSchema(string $path): void
public function installFromSchema(string $path, string $driver): bool
{
$schemaPath = "$path/install.dump";
$schemaPath = "$path/$driver-install.dump";
if (! file_exists($schemaPath)) {
return false;
}
$startTime = microtime(true);
@ -236,6 +242,8 @@ class Migrator
$runTime = number_format((microtime(true) - $startTime) * 1000, 2);
$this->note('<info>Loaded stored database schema.</info> ('.$runTime.'ms)');
return true;
}
public function setOutput(OutputInterface $output): static
@ -250,6 +258,16 @@ class Migrator
$this->output?->writeln($message);
}
/**
* Get the migration repository instance.
*
* @return MigrationRepositoryInterface
*/
public function getRepository()
{
return $this->repository;
}
public function repositoryExists(): bool
{
return $this->repository->repositoryExists();

View File

@ -109,6 +109,9 @@ class Discussion extends AbstractModel
$discussion->raise(new Deleted($discussion));
Notification::whereSubject($discussion)->delete();
// SQLite foreign constraints don't work since they were added *after* the table creation.
$discussion->posts()->delete();
});
}

View File

@ -24,12 +24,33 @@ class FulltextFilter extends AbstractFulltextFilter
{
public function search(SearchState $state, string $value): void
{
/** @var Builder $query */
$query = $state->getQuery();
if ($query->getConnection()->getDriverName() === 'sqlite') {
$query->where(function (Builder $query) use ($state, $value) {
$query->where('discussions.title', 'like', "%$value%")
->orWhereExists(function (Builder $query) use ($state, $value) {
$query->selectRaw('1')
->from(
Post::whereVisibleTo($state->getActor())
->whereColumn('discussion_id', 'discussions.id')
->where('type', 'comment')
->where('content', 'like', "%$value%")
->limit(1)
->toBase()
);
});
});
return;
}
// Replace all non-word characters with spaces.
// We do this to prevent MySQL fulltext search boolean mode from taking
// effect: https://dev.mysql.com/doc/refman/5.7/en/fulltext-boolean.html
$value = preg_replace('/[^\p{L}\p{N}\p{M}_]+/u', ' ', $value);
$query = $state->getQuery();
$grammar = $query->getGrammar();
$discussionSubquery = Discussion::select('id')

View File

@ -70,7 +70,20 @@ class ApplicationInfoProvider
public function identifyDatabaseVersion(): string
{
return $this->db->selectOne('select version() as version')->version;
return match ($this->config['database.driver']) {
'mysql' => $this->db->selectOne('select version() as version')->version,
'sqlite' => $this->db->selectOne('select sqlite_version() as version')->version,
default => 'Unknown',
};
}
public function identifyDatabaseDriver(): string
{
return match ($this->config['database.driver']) {
'mysql' => 'MySQL',
'sqlite' => 'SQLite',
default => $this->config['database.driver'],
};
}
/**

View File

@ -45,7 +45,7 @@ class InfoCommand extends AbstractCommand
$this->output->writeln("<info>Flarum core:</info> $coreVersion");
$this->output->writeln('<info>PHP version:</info> '.$this->appInfo->identifyPHPVersion());
$this->output->writeln('<info>MySQL version:</info> '.$this->appInfo->identifyDatabaseVersion());
$this->output->writeln('<info>'.$this->appInfo->identifyDatabaseDriver().' version:</info> '.$this->appInfo->identifyDatabaseVersion());
$phpExtensions = implode(', ', get_loaded_extensions());
$this->output->writeln("<info>Loaded extensions:</info> $phpExtensions");

View File

@ -50,7 +50,7 @@ class UserDataProvider implements DataProviderInterface
}
return new DatabaseConfig(
'mysql',
$this->ask('Database driver (mysql, sqlite) (Default: mysql):', 'mysql'),
$host,
intval($port),
$this->ask('Database name (required):'),

View File

@ -84,10 +84,10 @@ class InstallController implements RequestHandlerInterface
}
return new DatabaseConfig(
'mysql',
Arr::get($input, 'dbDriver'),
$host,
intval($port),
Arr::get($input, 'mysqlDatabase'),
Arr::get($input, 'dbName'),
Arr::get($input, 'mysqlUsername'),
Arr::get($input, 'mysqlPassword'),
Arr::get($input, 'tablePrefix')

View File

@ -9,6 +9,7 @@
namespace Flarum\Install;
use Flarum\Foundation\Paths;
use Illuminate\Contracts\Support\Arrayable;
class DatabaseConfig implements Arrayable
@ -17,7 +18,7 @@ class DatabaseConfig implements Arrayable
private readonly string $driver,
private readonly string $host,
private readonly int $port,
private readonly string $database,
private string $database,
private readonly string $username,
private readonly string $password,
private readonly string $prefix
@ -27,20 +28,12 @@ class DatabaseConfig implements Arrayable
public function toArray(): array
{
return [
return array_merge([
'driver' => $this->driver,
'host' => $this->host,
'port' => $this->port,
'database' => $this->database,
'username' => $this->username,
'password' => $this->password,
'charset' => 'utf8mb4',
'collation' => 'utf8mb4_unicode_ci',
'prefix' => $this->prefix,
'strict' => false,
'engine' => 'InnoDB',
'prefix_indexes' => true
];
], $this->driverOptions());
}
private function validate(): void
@ -49,15 +42,15 @@ class DatabaseConfig implements Arrayable
throw new ValidationFailed('Please specify a database driver.');
}
if ($this->driver !== 'mysql') {
throw new ValidationFailed('Currently, only MySQL/MariaDB is supported.');
if (! in_array($this->driver, ['mysql', 'sqlite'])) {
throw new ValidationFailed('Currently, only MySQL/MariaDB and SQLite are supported.');
}
if (empty($this->host)) {
if ($this->driver === 'mysql' && empty($this->host)) {
throw new ValidationFailed('Please specify the hostname of your database server.');
}
if ($this->port < 1 || $this->port > 65535) {
if ($this->driver === 'mysql' && ($this->port < 1 || $this->port > 65535)) {
throw new ValidationFailed('Please provide a valid port number between 1 and 65535.');
}
@ -65,7 +58,7 @@ class DatabaseConfig implements Arrayable
throw new ValidationFailed('Please specify the database name.');
}
if (empty($this->username)) {
if ($this->driver === 'mysql' && empty($this->username)) {
throw new ValidationFailed('Please specify the username for accessing the database.');
}
@ -79,4 +72,32 @@ class DatabaseConfig implements Arrayable
}
}
}
public function prepare(Paths $paths): void
{
if ($this->driver === 'sqlite' && ! file_exists($this->database)) {
$this->database = str_replace('.sqlite', '', $this->database).'.sqlite';
touch($paths->base.'/'.$this->database);
}
}
private function driverOptions(): array
{
return match ($this->driver) {
'mysql' => [
'host' => $this->host,
'port' => $this->port,
'username' => $this->username,
'password' => $this->password,
'charset' => 'utf8mb4',
'collation' => 'utf8mb4_unicode_ci',
'engine' => 'InnoDB',
'strict' => false,
],
'sqlite' => [
'foreign_key_constraints' => true,
],
default => []
};
}
}

View File

@ -116,12 +116,15 @@ class Installation
{
$pipeline = new Pipeline;
$this->dbConfig->prepare($this->paths);
$pipeline->pipe(function () {
return new Steps\ConnectToDatabase(
$this->dbConfig,
function ($connection) {
$this->db = $connection;
}
},
$this->paths->base
);
});
@ -135,7 +138,7 @@ class Installation
});
$pipeline->pipe(function () {
return new Steps\RunMigrations($this->db, $this->getMigrationPath());
return new Steps\RunMigrations($this->db, $this->dbConfig->toArray()['driver'], $this->getMigrationPath());
});
$pipeline->pipe(function () {

View File

@ -13,15 +13,19 @@ use Closure;
use Flarum\Install\DatabaseConfig;
use Flarum\Install\Step;
use Illuminate\Database\Connectors\MySqlConnector;
use Illuminate\Database\Connectors\SQLiteConnector;
use Illuminate\Database\MySqlConnection;
use Illuminate\Database\SQLiteConnection;
use Illuminate\Support\Str;
use InvalidArgumentException;
use RangeException;
class ConnectToDatabase implements Step
{
public function __construct(
private readonly DatabaseConfig $dbConfig,
private readonly Closure $store
private readonly Closure $store,
private readonly string $basePath
) {
}
@ -33,17 +37,27 @@ class ConnectToDatabase implements Step
public function run(): void
{
$config = $this->dbConfig->toArray();
match ($config['driver']) {
'mysql' => $this->mysql($config),
'sqlite' => $this->sqlite($config),
default => throw new InvalidArgumentException('Unsupported database driver: '.$config['driver']),
};
}
private function mysql(array $config): void
{
$pdo = (new MySqlConnector)->connect($config);
$version = $pdo->query('SELECT VERSION()')->fetchColumn();
if (Str::contains($version, 'MariaDB')) {
if (version_compare($version, '10.0.5', '<')) {
if (version_compare($version, '10.10.0', '<')) {
throw new RangeException('MariaDB version too low. You need at least MariaDB 10.0.5');
}
} else {
if (version_compare($version, '5.6.0', '<')) {
throw new RangeException('MySQL version too low. You need at least MySQL 5.6.');
if (version_compare($version, '5.7.0', '<')) {
throw new RangeException('MySQL version too low. You need at least MySQL 5.7');
}
}
@ -56,4 +70,28 @@ class ConnectToDatabase implements Step
)
);
}
private function sqlite(array $config): void
{
if (! file_exists($config['database'])) {
$config['database'] = $this->basePath.'/'.$config['database'];
}
$pdo = (new SQLiteConnector())->connect($config);
$version = $pdo->query('SELECT sqlite_version()')->fetchColumn();
if (version_compare($version, '3.8.8', '<')) {
throw new RangeException('SQLite version too low. You need at least SQLite 3.8.8');
}
($this->store)(
new SQLiteConnection(
$pdo,
$config['database'],
$config['prefix'],
$config
)
);
}
}

View File

@ -19,6 +19,7 @@ class RunMigrations implements Step
{
public function __construct(
private readonly ConnectionInterface $database,
private readonly string $driver,
private readonly string $path
) {
}
@ -32,7 +33,10 @@ class RunMigrations implements Step
{
$migrator = $this->getMigrator();
$migrator->installFromSchema($this->path);
if (! $migrator->installFromSchema($this->path, $this->driver)) {
$migrator->getRepository()->createRepository();
}
$migrator->run($this->path);
}

View File

@ -95,8 +95,7 @@ class Post extends AbstractModel
$post->number = new Expression('('.
$db->table('posts', 'pn')
->whereRaw($db->getTablePrefix().'pn.discussion_id = '.intval($post->discussion_id))
// IFNULL only works on MySQL/MariaDB
->selectRaw('IFNULL(MAX('.$db->getTablePrefix().'pn.number), 0) + 1')
->selectRaw('COALESCE(MAX('.$db->getTablePrefix().'pn.number), 0) + 1')
->toSql()
.')');
});

View File

@ -37,7 +37,6 @@ class GroupFilter implements FilterInterface
protected function constrain(Builder $query, User $actor, string|array $rawQuery, bool $negate): void
{
$groupIdentifiers = $this->asStringArray($rawQuery);
$groupQuery = Group::whereVisibleTo($actor);
$ids = [];
$names = [];
@ -49,11 +48,15 @@ class GroupFilter implements FilterInterface
}
}
$groupQuery->whereIn('groups.id', $ids)
->orWhereIn('name_singular', $names)
->orWhereIn('name_plural', $names);
$groupQuery = Group::whereVisibleTo($actor)
->join('group_user', 'groups.id', 'group_user.group_id')
->where(function (\Illuminate\Database\Eloquent\Builder $query) use ($ids, $names) {
$query->whereIn('groups.id', $ids)
->orWhereIn($query->raw('lower(name_singular)'), $names)
->orWhereIn($query->raw('lower(name_plural)'), $names);
});
$userIds = $groupQuery->join('group_user', 'groups.id', 'group_user.group_id')
$userIds = $groupQuery
->pluck('group_user.user_id')
->all();

View File

@ -73,6 +73,10 @@ class ListWithFulltextSearchTest extends TestCase
*/
public function can_search_for_word_or_title_in_post()
{
if ($this->database()->getDriverName() === 'sqlite') {
return $this->markTestSkipped('No fulltext search in SQLite.');
}
$response = $this->send(
$this->request('GET', '/api/discussions')
->withQueryParams([
@ -94,6 +98,10 @@ class ListWithFulltextSearchTest extends TestCase
*/
public function ignores_non_word_characters_when_searching()
{
if ($this->database()->getDriverName() === 'sqlite') {
return $this->markTestSkipped('No fulltext search in SQLite.');
}
$response = $this->send(
$this->request('GET', '/api/discussions')
->withQueryParams([
@ -115,6 +123,10 @@ class ListWithFulltextSearchTest extends TestCase
*/
public function can_search_telugu_like_languages()
{
if ($this->database()->getDriverName() === 'sqlite') {
return $this->markTestSkipped('No fulltext search in SQLite.');
}
$response = $this->send(
$this->request('GET', '/api/discussions')
->withQueryParams([
@ -137,6 +149,10 @@ class ListWithFulltextSearchTest extends TestCase
*/
public function can_search_cjk_languages()
{
if ($this->database()->getDriverName() === 'sqlite') {
return $this->markTestSkipped('No fulltext search in SQLite.');
}
$response = $this->send(
$this->request('GET', '/api/discussions')
->withQueryParams([
@ -159,6 +175,10 @@ class ListWithFulltextSearchTest extends TestCase
*/
public function search_for_special_characters_gives_empty_result()
{
if ($this->database()->getDriverName() === 'sqlite') {
return $this->markTestSkipped('No fulltext search in SQLite.');
}
$response = $this->send(
$this->request('GET', '/api/discussions')
->withQueryParams([

View File

@ -15,8 +15,8 @@
<td>{{ $phpVersion }}</td>
</tr>
<tr>
<td>MySQL</td>
<td>{{ $mysqlVersion }}</td>
<td>{{ $dbDriver }}</td>
<td>{{ $dbVersion }}</td>
</tr>
</tbody>
</table>

View File

@ -15,7 +15,7 @@
padding: 0;
line-height: 1.5;
}
body, input, button {
body, .FormControl, button {
font-family: 'Open Sans', sans-serif;
font-size: 16px;
color: #7E96B3;
@ -50,15 +50,19 @@
.FormGroup {
margin-bottom: 20px;
}
.FormGroup .FormField:first-child input {
.FormGroup .FormField:first-child .FormControl {
border-top-left-radius: 4px;
border-top-right-radius: 4px;
}
.FormGroup .FormField:last-child input {
.FormGroup .FormField:last-child .FormControl {
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
}
.FormField input {
.FormField select.FormControl {
-webkit-appearance: none;
-moz-appearance: none;
}
.FormField .FormControl {
background: #EDF2F7;
margin: 0 0 1px;
border: 2px solid transparent;
@ -67,7 +71,7 @@
padding: 15px 15px 15px 180px;
box-sizing: border-box;
}
.FormField input:focus {
.FormField .FormControl:focus {
border-color: #e7652e;
background: #fff;
color: #444;
@ -106,6 +110,17 @@
margin-bottom: 20px;
}
.Alert {
padding: 15px 20px;
border-radius: 4px;
margin-bottom: 20px;
}
.Alert--warning {
background: #fff2ae;
color: #ad6c00;
}
.animated {
-webkit-animation-fill-mode: both;
animation-fill-mode: both;

View File

@ -8,56 +8,74 @@
<div class="FormGroup">
<div class="FormField">
<label>Forum Title</label>
<input name="forumTitle">
<input class="FormControl" name="forumTitle">
</div>
</div>
<div class="FormGroup">
<div data-group="sqlite" style="display:none">
<div class="Alert Alert--warning">
<strong>Warning:</strong> Please keep in mind that while Flarum supports SQLite, not all ecosystem extensions do. If you're planning to install extensions, you should expect some of them to not work properly or at all.
</div>
</div>
</div>
<div class="FormGroup">
<div class="FormField">
<label>MySQL Host</label>
<input name="mysqlHost" value="localhost">
<label>Database Driver</label>
<select class="FormControl" name="dbDriver">
<option value="mysql">MySQL</option>
<option value="sqlite">SQLite</option>
</select>
</div>
<div class="FormField">
<label>MySQL Database</label>
<input name="mysqlDatabase">
<label>Database</label>
<input class="FormControl" name="dbName" value="flarum">
</div>
<div class="FormField">
<label>MySQL Username</label>
<input name="mysqlUsername">
</div>
<div data-group="mysql">
<div class="FormField">
<label>MySQL Host</label>
<input class="FormControl" name="mysqlHost" value="localhost">
</div>
<div class="FormField">
<label>MySQL Password</label>
<input type="password" name="mysqlPassword">
<div class="FormField">
<label>MySQL Username</label>
<input class="FormControl" name="mysqlUsername">
</div>
<div class="FormField">
<label>MySQL Password</label>
<input class="FormControl" type="password" name="mysqlPassword">
</div>
</div>
<div class="FormField">
<label>Table Prefix</label>
<input type="text" name="tablePrefix">
<input class="FormControl" type="text" name="tablePrefix">
</div>
</div>
<div class="FormGroup">
<div class="FormField">
<label>Admin Username</label>
<input name="adminUsername">
<input class="FormControl" name="adminUsername">
</div>
<div class="FormField">
<label>Admin Email</label>
<input name="adminEmail">
<input class="FormControl" name="adminEmail">
</div>
<div class="FormField">
<label>Admin Password</label>
<input type="password" name="adminPassword">
<input class="FormControl" type="password" name="adminPassword">
</div>
<div class="FormField">
<label>Confirm Password</label>
<input type="password" name="adminPasswordConfirmation">
<input class="FormControl" type="password" name="adminPasswordConfirmation">
</div>
</div>
@ -70,6 +88,18 @@
document.addEventListener('DOMContentLoaded', function() {
document.querySelector('form input').select();
document.querySelector('select[name="dbDriver"]').addEventListener('change', function() {
document.querySelectorAll('[data-group]').forEach(function(group) {
group.style.display = 'none';
});
const groups = document.querySelectorAll('[data-group="' + this.value + '"]');
groups.forEach(function(group) {
group.style.display = 'block';
});
});
document.querySelector('form').addEventListener('submit', function(e) {
e.preventDefault();

View File

@ -8,7 +8,7 @@
<div class="FormGroup">
<div class="FormField">
<label>Database Password</label>
<input type="password" name="databasePassword">
<input class="FormControl" type="password" name="databasePassword">
</div>
</div>

View File

@ -12,6 +12,7 @@ namespace Flarum\Testing\integration\Extend;
use Flarum\Extend\ExtenderInterface;
use Flarum\Extension\Extension;
use Illuminate\Contracts\Container\Container;
use Illuminate\Database\Connection;
use Illuminate\Database\ConnectionInterface;
class BeginTransactionAndSetDatabase implements ExtenderInterface
@ -28,8 +29,14 @@ class BeginTransactionAndSetDatabase implements ExtenderInterface
public function extend(Container $container, Extension $extension = null): void
{
/** @var Connection $db */
$db = $container->make(ConnectionInterface::class);
// SQLite requires this be done outside a transaction.
if ($db->getDriverName() === 'sqlite') {
$db->getSchemaBuilder()->disableForeignKeyConstraints();
}
$db->beginTransaction();
($this->setDbOnTestCase)($db);

View File

@ -21,61 +21,24 @@ class SetupScript
{
use UsesTmpDir;
/**
* Test database host.
*
* @var string
*/
protected $host;
protected string $driver;
protected string $host;
protected int $port;
protected string $name;
protected string $user;
protected string $pass;
protected string $pref;
/**
* Test database port.
*
* @var int
*/
protected $port;
/**
* Test database name.
*
* @var string
*/
protected $name;
/**
* Test database username.
*
* @var string
*/
protected $user;
/**
* Test database password.
*
* @var string
*/
protected $pass;
/**
* Test database prefix.
*
* @var string
*/
protected $pref;
/**
* @var DatabaseConfig
*/
protected $dbConfig;
protected DatabaseConfig $dbConfig;
/**
* Settings to be applied during installation.
* @var array
*/
protected $settings = ['mail_driver' => 'log'];
protected array $settings = ['mail_driver' => 'log'];
public function __construct()
{
$this->driver = getenv('DB_DRIVER') ?: 'mysql';
$this->host = getenv('DB_HOST') ?: 'localhost';
$this->port = intval(getenv('DB_PORT') ?: 3306);
$this->name = getenv('DB_DATABASE') ?: 'flarum_test';
@ -88,7 +51,12 @@ class SetupScript
{
$tmp = $this->tmpDir();
echo "Connecting to database $this->name at $this->host:$this->port.\n";
if ($this->driver === 'sqlite') {
echo "Connecting to SQLite database at $this->name.\n";
} else {
echo "Connecting to database $this->name at $this->host:$this->port.\n";
}
echo "Warning: all tables will be dropped to ensure clean state. DO NOT use your production database!\n";
echo "Logging in as $this->user with password '$this->pass'.\n";
echo "Table prefix: '$this->pref'\n";
@ -103,22 +71,31 @@ class SetupScript
echo "\nOff we go...\n";
$this->dbConfig = new DatabaseConfig('mysql', $this->host, $this->port, $this->name, $this->user, $this->pass, $this->pref);
$this->dbConfig = new DatabaseConfig(
$this->driver,
$this->host,
$this->port,
$this->name,
$this->user,
$this->pass,
$this->pref
);
echo "\nWiping DB to ensure clean state\n";
$this->wipeDb();
echo "Success! Proceeding to installation...\n";
$paths = new Paths([
'base' => $tmp,
'public' => "$tmp/public",
'storage' => "$tmp/storage",
'vendor' => getenv('FLARUM_TEST_VENDOR_PATH') ?: getcwd().'/vendor',
]);
$this->setupTmpDir();
$this->dbConfig->prepare($paths);
$installation = new Installation(
new Paths([
'base' => $tmp,
'public' => "$tmp/public",
'storage' => "$tmp/storage",
'vendor' => getenv('FLARUM_TEST_VENDOR_PATH') ?: getcwd().'/vendor',
])
);
echo "\nWiping DB to ensure clean state\n";
$this->wipeDb($paths);
echo "Success! Proceeding to installation...\n";
$installation = new Installation($paths);
$pipeline = $installation
->configPath('config.php')
@ -140,7 +117,7 @@ class SetupScript
echo "Installation complete\n";
}
protected function wipeDb()
protected function wipeDb(Paths $paths)
{
// Reuse the connection step to include version checks
(new ConnectToDatabase($this->dbConfig, function ($db) {
@ -149,7 +126,7 @@ class SetupScript
$builder->dropAllTables();
$builder->dropAllViews();
}))->run();
}, $paths->base))->run();
}
/**

View File

@ -193,7 +193,12 @@ abstract class TestCase extends \PHPUnit\Framework\TestCase
protected function populateDatabase(): void
{
// We temporarily disable foreign key checks to simplify this process.
/**
* We temporarily disable foreign key checks to simplify this process.
* SQLite ignores this statement since we are inside a transaction.
* So we do that before starting a transaction.
* @see BeginTransactionAndSetDatabase
*/
$this->database()->getSchemaBuilder()->disableForeignKeyConstraints();
$databaseContent = [];