diff --git a/.editorconfig b/.editorconfig index 1feb43fc7..0fb398499 100644 --- a/.editorconfig +++ b/.editorconfig @@ -23,3 +23,6 @@ indent_size = 2 [*.neon] indent_style = tab + +[{install,update}.php] +indent_size = 2 diff --git a/.github/workflows/REUSABLE_backend.yml b/.github/workflows/REUSABLE_backend.yml index 0fad56e8a..70adb15b8 100644 --- a/.github/workflows/REUSABLE_backend.yml +++ b/.github/workflows/REUSABLE_backend.yml @@ -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: diff --git a/extensions/flags/src/Flag.php b/extensions/flags/src/Flag.php index 1fd01dad7..c9050a8ba 100644 --- a/extensions/flags/src/Flag.php +++ b/extensions/flags/src/Flag.php @@ -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']; diff --git a/extensions/flags/src/FlagFactory.php b/extensions/flags/src/FlagFactory.php new file mode 100644 index 000000000..c73106528 --- /dev/null +++ b/extensions/flags/src/FlagFactory.php @@ -0,0 +1,30 @@ + 'user', + 'post_id' => Post::factory(), + 'user_id' => User::factory(), + 'reason' => $this->faker->sentence, + 'reason_detail' => $this->faker->sentence, + 'created_at' => Carbon::now(), + ]; + } +} diff --git a/extensions/flags/tests/integration/api/flags/ListTest.php b/extensions/flags/tests/integration/api/flags/ListTest.php index fb2c75171..ef4f17b29 100644 --- a/extensions/flags/tests/integration/api/flags/ListTest.php +++ b/extensions/flags/tests/integration/api/flags/ListTest.php @@ -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' => '

'], ['id' => 3, 'discussion_id' => 1, 'user_id' => 1, 'type' => 'comment', 'content' => '

'], ], - '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], diff --git a/extensions/flags/tests/integration/api/flags/ListWithTagsTest.php b/extensions/flags/tests/integration/api/flags/ListWithTagsTest.php index 7bd38b544..ab65c66fe 100644 --- a/extensions/flags/tests/integration/api/flags/ListWithTagsTest.php +++ b/extensions/flags/tests/integration/api/flags/ListWithTagsTest.php @@ -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' => '

'], ['id' => 7, 'discussion_id' => 5, 'user_id' => 1, 'type' => 'comment', 'content' => '

'], ], - 'flags' => [ + Flag::class => [ // From regular ListTest ['id' => 1, 'post_id' => 1, 'user_id' => 1], ['id' => 2, 'post_id' => 1, 'user_id' => 2], diff --git a/extensions/mentions/migrations/2022_05_20_000005_add_created_at_to_post_mentions_post_table.php b/extensions/mentions/migrations/2022_05_20_000005_add_created_at_to_post_mentions_post_table.php index d6c4114e1..56c60aa2c 100644 --- a/extensions/mentions/migrations/2022_05_20_000005_add_created_at_to_post_mentions_post_table.php +++ b/extensions/mentions/migrations/2022_05_20_000005_add_created_at_to_post_mentions_post_table.php @@ -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) { diff --git a/extensions/mentions/migrations/2022_05_20_000006_add_created_at_to_post_mentions_user_table.php b/extensions/mentions/migrations/2022_05_20_000006_add_created_at_to_post_mentions_user_table.php index f2991d607..7361a6436 100644 --- a/extensions/mentions/migrations/2022_05_20_000006_add_created_at_to_post_mentions_user_table.php +++ b/extensions/mentions/migrations/2022_05_20_000006_add_created_at_to_post_mentions_user_table.php @@ -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) { diff --git a/extensions/mentions/src/Api/LoadMentionedByRelationship.php b/extensions/mentions/src/Api/LoadMentionedByRelationship.php index 47bceafb7..905d7d48c 100644 --- a/extensions/mentions/src/Api/LoadMentionedByRelationship.php +++ b/extensions/mentions/src/Api/LoadMentionedByRelationship.php @@ -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. diff --git a/extensions/statistics/src/Api/Controller/ShowStatisticsData.php b/extensions/statistics/src/Api/Controller/ShowStatisticsData.php index 9319a6d1b..7f5bb5b37 100644 --- a/extensions/statistics/src/Api/Controller/ShowStatisticsData.php +++ b/extensions/statistics/src/Api/Controller/ShowStatisticsData.php @@ -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') diff --git a/extensions/statistics/tests/integration/api/CanRequestCustomTimedStatisticsTest.php b/extensions/statistics/tests/integration/api/CanRequestCustomTimedStatisticsTest.php index 536f77edc..adf5a89b0 100644 --- a/extensions/statistics/tests/integration/api/CanRequestCustomTimedStatisticsTest.php +++ b/extensions/statistics/tests/integration/api/CanRequestCustomTimedStatisticsTest.php @@ -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, diff --git a/extensions/sticky/src/PinStickiedDiscussionsToTop.php b/extensions/sticky/src/PinStickiedDiscussionsToTop.php index c8a044216..a8b57eebc 100755 --- a/extensions/sticky/src/PinStickiedDiscussionsToTop.php +++ b/extensions/sticky/src/PinStickiedDiscussionsToTop.php @@ -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; diff --git a/extensions/tags/migrations/2018_06_27_085200_change_tags_columns.php b/extensions/tags/migrations/2018_06_27_085200_change_tags_columns.php index 00cccbbd8..fc5f3793e 100644 --- a/extensions/tags/migrations/2018_06_27_085200_change_tags_columns.php +++ b/extensions/tags/migrations/2018_06_27_085200_change_tags_columns.php @@ -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'); }); } diff --git a/extensions/tags/migrations/2022_05_20_000003_add_timestamps_to_tags_table.php b/extensions/tags/migrations/2022_05_20_000003_add_timestamps_to_tags_table.php index b393ae8aa..86d8ad124 100644 --- a/extensions/tags/migrations/2022_05_20_000003_add_timestamps_to_tags_table.php +++ b/extensions/tags/migrations/2022_05_20_000003_add_timestamps_to_tags_table.php @@ -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'); }); } ]; diff --git a/extensions/tags/migrations/2022_05_20_000004_add_created_at_to_discussion_tag_table.php b/extensions/tags/migrations/2022_05_20_000004_add_created_at_to_discussion_tag_table.php index da0197a6d..314b74ded 100644 --- a/extensions/tags/migrations/2022_05_20_000004_add_created_at_to_discussion_tag_table.php +++ b/extensions/tags/migrations/2022_05_20_000004_add_created_at_to_discussion_tag_table.php @@ -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) { diff --git a/framework/core/js/src/admin/components/StatusWidget.js b/framework/core/js/src/admin/components/StatusWidget.js index e961f1d3a..15176d239 100644 --- a/framework/core/js/src/admin/components/StatusWidget.js +++ b/framework/core/js/src/admin/components/StatusWidget.js @@ -34,7 +34,7 @@ export default class StatusWidget extends DashboardWidget { items.add('version-flarum', [Flarum,
, app.forum.attribute('version')], 100); items.add('version-php', [PHP,
, app.data.phpVersion], 90); - items.add('version-mysql', [MySQL,
, app.data.mysqlVersion], 80); + items.add('version-db', [{app.data.dbDriver},
, app.data.dbVersion], 80); if (app.data.schedulerStatus) { items.add( 'schedule-status', diff --git a/framework/core/migrations/2015_02_24_000000_create_posts_table.php b/framework/core/migrations/2015_02_24_000000_create_posts_table.php index 47c4bb0f7..412fe8d98 100644 --- a/framework/core/migrations/2015_02_24_000000_create_posts_table.php +++ b/framework/core/migrations/2015_02_24_000000_create_posts_table.php @@ -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) { diff --git a/framework/core/migrations/2015_12_05_042721_change_access_tokens_columns.php b/framework/core/migrations/2015_12_05_042721_change_access_tokens_columns.php index 6b5a5a087..5ba7daff2 100644 --- a/framework/core/migrations/2015_12_05_042721_change_access_tokens_columns.php +++ b/framework/core/migrations/2015_12_05_042721_change_access_tokens_columns.php @@ -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'); }); diff --git a/framework/core/migrations/2018_01_11_093900_change_access_tokens_columns.php b/framework/core/migrations/2018_01_11_093900_change_access_tokens_columns.php index ebf66e593..303f2027f 100644 --- a/framework/core/migrations/2018_01_11_093900_change_access_tokens_columns.php +++ b/framework/core/migrations/2018_01_11_093900_change_access_tokens_columns.php @@ -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(); }); } diff --git a/framework/core/migrations/2018_01_11_095000_change_api_keys_columns.php b/framework/core/migrations/2018_01_11_095000_change_api_keys_columns.php index 421db1012..298213a86 100644 --- a/framework/core/migrations/2018_01_11_095000_change_api_keys_columns.php +++ b/framework/core/migrations/2018_01_11_095000_change_api_keys_columns.php @@ -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'); + } }); } ]; diff --git a/framework/core/migrations/2018_01_11_102100_change_registration_tokens_created_at_to_datetime.php b/framework/core/migrations/2018_01_11_102100_change_registration_tokens_created_at_to_datetime.php index a01d9d70c..f487a257b 100644 --- a/framework/core/migrations/2018_01_11_102100_change_registration_tokens_created_at_to_datetime.php +++ b/framework/core/migrations/2018_01_11_102100_change_registration_tokens_created_at_to_datetime.php @@ -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(); + }); } ]; diff --git a/framework/core/migrations/2018_01_11_120604_change_posts_table_to_innodb.php b/framework/core/migrations/2018_01_11_120604_change_posts_table_to_innodb.php index 500e5f79b..2cf3cadfa 100644 --- a/framework/core/migrations/2018_01_11_120604_change_posts_table_to_innodb.php +++ b/framework/core/migrations/2018_01_11_120604_change_posts_table_to_innodb.php @@ -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'); + } } ]; diff --git a/framework/core/migrations/2018_01_15_072800_change_email_tokens_created_at_to_datetime.php b/framework/core/migrations/2018_01_15_072800_change_email_tokens_created_at_to_datetime.php index e97fe64d0..e08875bea 100644 --- a/framework/core/migrations/2018_01_15_072800_change_email_tokens_created_at_to_datetime.php +++ b/framework/core/migrations/2018_01_15_072800_change_email_tokens_created_at_to_datetime.php @@ -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(); + }); } ]; diff --git a/framework/core/migrations/2018_01_18_133000_change_notifications_columns.php b/framework/core/migrations/2018_01_18_133000_change_notifications_columns.php index 34066ff23..24a6e7ce1 100644 --- a/framework/core/migrations/2018_01_18_133000_change_notifications_columns.php +++ b/framework/core/migrations/2018_01_18_133000_change_notifications_columns.php @@ -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'); }); diff --git a/framework/core/migrations/2018_01_18_134600_change_password_tokens_created_at_to_datetime.php b/framework/core/migrations/2018_01_18_134600_change_password_tokens_created_at_to_datetime.php index 940929de9..f3c73dc02 100644 --- a/framework/core/migrations/2018_01_18_134600_change_password_tokens_created_at_to_datetime.php +++ b/framework/core/migrations/2018_01_18_134600_change_password_tokens_created_at_to_datetime.php @@ -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(); + }); } ]; diff --git a/framework/core/migrations/2018_01_30_112238_add_fulltext_index_to_discussions_title.php b/framework/core/migrations/2018_01_30_112238_add_fulltext_index_to_discussions_title.php index b2ed1a5da..e49130d5b 100644 --- a/framework/core/migrations/2018_01_30_112238_add_fulltext_index_to_discussions_title.php +++ b/framework/core/migrations/2018_01_30_112238_add_fulltext_index_to_discussions_title.php @@ -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) { diff --git a/framework/core/migrations/2021_03_02_040500_change_access_tokens_add_id.php b/framework/core/migrations/2021_03_02_040500_change_access_tokens_add_id.php index 06d1ec248..3bdc52040 100644 --- a/framework/core/migrations/2021_03_02_040500_change_access_tokens_add_id.php +++ b/framework/core/migrations/2021_03_02_040500_change_access_tokens_add_id.php @@ -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'); + } }); } ]; diff --git a/framework/core/migrations/2022_05_20_000000_add_timestamps_to_groups_table.php b/framework/core/migrations/2022_05_20_000000_add_timestamps_to_groups_table.php index bab2b4a57..169f27c69 100644 --- a/framework/core/migrations/2022_05_20_000000_add_timestamps_to_groups_table.php +++ b/framework/core/migrations/2022_05_20_000000_add_timestamps_to_groups_table.php @@ -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'); }); } ]; diff --git a/framework/core/migrations/2022_05_20_000001_add_created_at_to_group_user_table.php b/framework/core/migrations/2022_05_20_000001_add_created_at_to_group_user_table.php index 36bf77265..c0d6ad41c 100644 --- a/framework/core/migrations/2022_05_20_000001_add_created_at_to_group_user_table.php +++ b/framework/core/migrations/2022_05_20_000001_add_created_at_to_group_user_table.php @@ -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) { diff --git a/framework/core/migrations/2022_05_20_000002_add_created_at_to_group_permission_table.php b/framework/core/migrations/2022_05_20_000002_add_created_at_to_group_permission_table.php index 1333128f2..02ea4712d 100644 --- a/framework/core/migrations/2022_05_20_000002_add_created_at_to_group_permission_table.php +++ b/framework/core/migrations/2022_05_20_000002_add_created_at_to_group_permission_table.php @@ -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) { diff --git a/framework/core/migrations/2024_05_05_000000_add_sqlite_keys.php b/framework/core/migrations/2024_05_05_000000_add_sqlite_keys.php new file mode 100644 index 000000000..0405a08aa --- /dev/null +++ b/framework/core/migrations/2024_05_05_000000_add_sqlite_keys.php @@ -0,0 +1,47 @@ + 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' => << <<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'); + } + }, +]; diff --git a/framework/core/migrations/install.dump b/framework/core/migrations/mysql-install.dump similarity index 100% rename from framework/core/migrations/install.dump rename to framework/core/migrations/mysql-install.dump diff --git a/framework/core/migrations/sqlite-install.dump b/framework/core/migrations/sqlite-install.dump new file mode 100644 index 000000000..6a4cda4bc --- /dev/null +++ b/framework/core/migrations/sqlite-install.dump @@ -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); diff --git a/framework/core/src/Admin/Content/AdminPayload.php b/framework/core/src/Admin/Content/AdminPayload.php index c2828d769..dea5431a1 100644 --- a/framework/core/src/Admin/Content/AdminPayload.php +++ b/framework/core/src/Admin/Content/AdminPayload.php @@ -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()) { diff --git a/framework/core/src/Admin/Content/Index.php b/framework/core/src/Admin/Content/Index.php index b70466fb5..a47d7a515 100644 --- a/framework/core/src/Admin/Content/Index.php +++ b/framework/core/src/Admin/Content/Index.php @@ -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; diff --git a/framework/core/src/Database/AbstractModel.php b/framework/core/src/Database/AbstractModel.php index 812952fbc..65c15a7dd 100644 --- a/framework/core/src/Database/AbstractModel.php +++ b/framework/core/src/Database/AbstractModel.php @@ -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|null + */ + public ?array $uniqueKeys = null; + public static function boot() { parent::boot(); diff --git a/framework/core/src/Database/Console/GenerateDumpCommand.php b/framework/core/src/Database/Console/GenerateDumpCommand.php index 559f41ab2..2e0838b90 100644 --- a/framework/core/src/Database/Console/GenerateDumpCommand.php +++ b/framework/core/src/Database/Console/GenerateDumpCommand.php @@ -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'); diff --git a/framework/core/src/Database/DatabaseMigrationRepository.php b/framework/core/src/Database/DatabaseMigrationRepository.php index 9bd1fba42..3fb06fba3 100644 --- a/framework/core/src/Database/DatabaseMigrationRepository.php +++ b/framework/core/src/Database/DatabaseMigrationRepository.php @@ -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(); diff --git a/framework/core/src/Database/DatabaseServiceProvider.php b/framework/core/src/Database/DatabaseServiceProvider.php index 99fef1674..cb0516216 100644 --- a/framework/core/src/Database/DatabaseServiceProvider.php +++ b/framework/core/src/Database/DatabaseServiceProvider.php @@ -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'); diff --git a/framework/core/src/Database/Migration.php b/framework/core/src/Database/Migration.php index 3ef10cbfb..a8b7c7055 100644 --- a/framework/core/src/Database/Migration.php +++ b/framework/core/src/Database/Migration.php @@ -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); - } - }); + }); + } } ]; } diff --git a/framework/core/src/Database/Migrator.php b/framework/core/src/Database/Migrator.php index 15432ef0c..a132a8037 100644 --- a/framework/core/src/Database/Migrator.php +++ b/framework/core/src/Database/Migrator.php @@ -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('Loaded stored database schema. ('.$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(); diff --git a/framework/core/src/Discussion/Discussion.php b/framework/core/src/Discussion/Discussion.php index 05a38ae65..924f9d634 100644 --- a/framework/core/src/Discussion/Discussion.php +++ b/framework/core/src/Discussion/Discussion.php @@ -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(); }); } diff --git a/framework/core/src/Discussion/Search/FulltextFilter.php b/framework/core/src/Discussion/Search/FulltextFilter.php index da0b62a03..ada06a184 100644 --- a/framework/core/src/Discussion/Search/FulltextFilter.php +++ b/framework/core/src/Discussion/Search/FulltextFilter.php @@ -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') diff --git a/framework/core/src/Foundation/ApplicationInfoProvider.php b/framework/core/src/Foundation/ApplicationInfoProvider.php index b88190616..0f5aa2211 100644 --- a/framework/core/src/Foundation/ApplicationInfoProvider.php +++ b/framework/core/src/Foundation/ApplicationInfoProvider.php @@ -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'], + }; } /** diff --git a/framework/core/src/Foundation/Console/InfoCommand.php b/framework/core/src/Foundation/Console/InfoCommand.php index 4256b79fe..d3967e152 100644 --- a/framework/core/src/Foundation/Console/InfoCommand.php +++ b/framework/core/src/Foundation/Console/InfoCommand.php @@ -45,7 +45,7 @@ class InfoCommand extends AbstractCommand $this->output->writeln("Flarum core: $coreVersion"); $this->output->writeln('PHP version: '.$this->appInfo->identifyPHPVersion()); - $this->output->writeln('MySQL version: '.$this->appInfo->identifyDatabaseVersion()); + $this->output->writeln(''.$this->appInfo->identifyDatabaseDriver().' version: '.$this->appInfo->identifyDatabaseVersion()); $phpExtensions = implode(', ', get_loaded_extensions()); $this->output->writeln("Loaded extensions: $phpExtensions"); diff --git a/framework/core/src/Install/Console/UserDataProvider.php b/framework/core/src/Install/Console/UserDataProvider.php index f72b55bb7..5ef3de990 100644 --- a/framework/core/src/Install/Console/UserDataProvider.php +++ b/framework/core/src/Install/Console/UserDataProvider.php @@ -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):'), diff --git a/framework/core/src/Install/Controller/InstallController.php b/framework/core/src/Install/Controller/InstallController.php index 0d98540a4..f066c794c 100644 --- a/framework/core/src/Install/Controller/InstallController.php +++ b/framework/core/src/Install/Controller/InstallController.php @@ -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') diff --git a/framework/core/src/Install/DatabaseConfig.php b/framework/core/src/Install/DatabaseConfig.php index b0d46134c..b5e4c9ed7 100644 --- a/framework/core/src/Install/DatabaseConfig.php +++ b/framework/core/src/Install/DatabaseConfig.php @@ -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 => [] + }; + } } diff --git a/framework/core/src/Install/Installation.php b/framework/core/src/Install/Installation.php index 28017d504..68926ee04 100644 --- a/framework/core/src/Install/Installation.php +++ b/framework/core/src/Install/Installation.php @@ -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 () { diff --git a/framework/core/src/Install/Steps/ConnectToDatabase.php b/framework/core/src/Install/Steps/ConnectToDatabase.php index d06907022..a978c12b1 100644 --- a/framework/core/src/Install/Steps/ConnectToDatabase.php +++ b/framework/core/src/Install/Steps/ConnectToDatabase.php @@ -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 + ) + ); + } } diff --git a/framework/core/src/Install/Steps/RunMigrations.php b/framework/core/src/Install/Steps/RunMigrations.php index 19d25c25c..254bbf1f5 100644 --- a/framework/core/src/Install/Steps/RunMigrations.php +++ b/framework/core/src/Install/Steps/RunMigrations.php @@ -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); } diff --git a/framework/core/src/Post/Post.php b/framework/core/src/Post/Post.php index df52c78fc..cf301568e 100644 --- a/framework/core/src/Post/Post.php +++ b/framework/core/src/Post/Post.php @@ -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() .')'); }); diff --git a/framework/core/src/User/Search/Filter/GroupFilter.php b/framework/core/src/User/Search/Filter/GroupFilter.php index b1de2035c..aee788411 100644 --- a/framework/core/src/User/Search/Filter/GroupFilter.php +++ b/framework/core/src/User/Search/Filter/GroupFilter.php @@ -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(); diff --git a/framework/core/tests/integration/api/discussions/ListWithFulltextSearchTest.php b/framework/core/tests/integration/api/discussions/ListWithFulltextSearchTest.php index ae598cb78..395bf55ec 100644 --- a/framework/core/tests/integration/api/discussions/ListWithFulltextSearchTest.php +++ b/framework/core/tests/integration/api/discussions/ListWithFulltextSearchTest.php @@ -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([ diff --git a/framework/core/views/frontend/content/admin.blade.php b/framework/core/views/frontend/content/admin.blade.php index 6f5b6ae55..44f98da1a 100644 --- a/framework/core/views/frontend/content/admin.blade.php +++ b/framework/core/views/frontend/content/admin.blade.php @@ -15,8 +15,8 @@ {{ $phpVersion }} - MySQL - {{ $mysqlVersion }} + {{ $dbDriver }} + {{ $dbVersion }} diff --git a/framework/core/views/install/app.php b/framework/core/views/install/app.php index 3e4f572de..1e312f4d9 100644 --- a/framework/core/views/install/app.php +++ b/framework/core/views/install/app.php @@ -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; diff --git a/framework/core/views/install/install.php b/framework/core/views/install/install.php index f28fea675..37bb260f2 100644 --- a/framework/core/views/install/install.php +++ b/framework/core/views/install/install.php @@ -8,56 +8,74 @@
- + +
+
+ +
+
+
+ Warning: 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. +
- - + +
- - + +
-
- - -
+
+
+ + +
-
- - +
+ + +
+ +
+ + +
- +
- +
- +
- +
- +
@@ -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(); diff --git a/framework/core/views/install/update.php b/framework/core/views/install/update.php index 1cbbc1639..bd8a05e7e 100644 --- a/framework/core/views/install/update.php +++ b/framework/core/views/install/update.php @@ -8,7 +8,7 @@
- +
diff --git a/php-packages/testing/src/integration/Extend/BeginTransactionAndSetDatabase.php b/php-packages/testing/src/integration/Extend/BeginTransactionAndSetDatabase.php index 565164bdb..fe919da30 100644 --- a/php-packages/testing/src/integration/Extend/BeginTransactionAndSetDatabase.php +++ b/php-packages/testing/src/integration/Extend/BeginTransactionAndSetDatabase.php @@ -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); diff --git a/php-packages/testing/src/integration/Setup/SetupScript.php b/php-packages/testing/src/integration/Setup/SetupScript.php index e6d70a9ea..5b190cf94 100644 --- a/php-packages/testing/src/integration/Setup/SetupScript.php +++ b/php-packages/testing/src/integration/Setup/SetupScript.php @@ -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(); } /** diff --git a/php-packages/testing/src/integration/TestCase.php b/php-packages/testing/src/integration/TestCase.php index 68f2d4764..b450be491 100644 --- a/php-packages/testing/src/integration/TestCase.php +++ b/php-packages/testing/src/integration/TestCase.php @@ -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 = [];