From f74dfdf65a4db0fe6512b3abc8bda031c408ff1b Mon Sep 17 00:00:00 2001 From: Sami Mazouz Date: Sun, 5 May 2024 12:06:10 +0100 Subject: [PATCH 01/29] feat: add support for `PgSQL` --- .../Api/Controller/ListFlagsController.php | 7 +- .../tests/integration/api/flags/ListTest.php | 21 +-- .../api/flags/ListWithTagsTest.php | 29 ++-- .../likes/src/Api/LoadLikesRelationship.php | 1 + .../tests/integration/api/ListPostsTest.php | 22 +-- extensions/lock/src/Filter/LockedFilter.php | 2 +- .../integration/api/PostMentionsTest.php | 8 +- .../integration/api/UserMentionsTest.php | 7 +- .../src/Api/Controller/ShowStatisticsData.php | 13 +- .../src/PinStickiedDiscussionsToTop.php | 4 +- extensions/sticky/src/Query/StickyFilter.php | 2 +- .../integration/api/ListDiscussionsTest.php | 12 +- .../src/Filter/SubscriptionFilter.php | 2 +- .../suspend/src/Query/SuspendedFilter.php | 2 +- .../tags/src/Search/Filter/TagFilter.php | 7 +- extensions/tags/src/TagState.php | 2 + .../2015_02_24_000000_create_posts_table.php | 9 +- ...11_093900_change_access_tokens_columns.php | 21 ++- ...dd_fulltext_index_to_discussions_title.php | 18 +-- ...1_convert_preferences_to_json_in_users.php | 37 +++++ .../src/Database/DatabaseServiceProvider.php | 26 ++++ framework/core/src/Database/Migrator.php | 38 +++++- .../Discussion/Search/Filter/AuthorFilter.php | 2 +- .../Search/Filter/CreatedFilter.php | 4 +- .../Discussion/Search/Filter/HiddenFilter.php | 2 +- .../Discussion/Search/Filter/UnreadFilter.php | 12 +- .../src/Discussion/Search/FulltextFilter.php | 128 ++++++++++++++---- framework/core/src/Discussion/UserState.php | 2 + .../Foundation/ApplicationInfoProvider.php | 3 +- framework/core/src/Group/Permission.php | 2 + .../src/Install/Console/UserDataProvider.php | 27 ++-- .../Install/Controller/InstallController.php | 15 +- framework/core/src/Install/DatabaseConfig.php | 23 +++- .../src/Install/Steps/ConnectToDatabase.php | 30 +++- .../Notification/NotificationRepository.php | 2 +- .../src/Search/Database/AbstractSearcher.php | 4 +- .../Search/Database/DatabaseSearchState.php | 2 +- .../src/User/Search/Filter/EmailFilter.php | 2 +- .../src/User/Search/Filter/GroupFilter.php | 4 +- framework/core/src/User/User.php | 4 +- .../api/access_tokens/CreateTest.php | 2 +- .../integration/api/discussions/ListTest.php | 10 +- .../ListWithFulltextSearchTest.php | 5 +- .../api/notifications/ListTest.php | 2 +- .../integration/api/posts/DeleteTest.php | 15 +- .../integration/api/users/GroupSearchTest.php | 8 +- .../tests/integration/api/users/ListTest.php | 2 +- framework/core/views/install/install.php | 21 +-- .../src/integration/Setup/SetupScript.php | 6 +- .../testing/src/integration/TestCase.php | 8 ++ 50 files changed, 464 insertions(+), 173 deletions(-) create mode 100644 framework/core/migrations/2024_05_05_000001_convert_preferences_to_json_in_users.php diff --git a/extensions/flags/src/Api/Controller/ListFlagsController.php b/extensions/flags/src/Api/Controller/ListFlagsController.php index 534d3abe56..8218c6fb62 100644 --- a/extensions/flags/src/Api/Controller/ListFlagsController.php +++ b/extensions/flags/src/Api/Controller/ListFlagsController.php @@ -15,6 +15,7 @@ use Flarum\Flags\Flag; use Flarum\Http\RequestUtil; use Flarum\Http\UrlGenerator; +use Illuminate\Contracts\Database\Eloquent\Builder; use Psr\Http\Message\ServerRequestInterface; use Tobscure\JsonApi\Document; @@ -51,11 +52,13 @@ protected function data(ServerRequestInterface $request, Document $document): it $include[] = 'post.user.groups'; } + /** @var \Illuminate\Database\Eloquent\Collection $flags */ $flags = Flag::whereVisibleTo($actor) - ->latest('flags.created_at') - ->groupBy('post_id') ->limit($limit + 1) ->offset($offset) + ->whenMySql(fn (Builder $query) => $query->groupBy('post_id')) + ->whenPgSql(fn (Builder $query) => $query->distinct('post_id')->orderBy('post_id')) + ->latest('flags.id') ->get(); $this->loadRelations($flags, $include, $request); diff --git a/extensions/flags/tests/integration/api/flags/ListTest.php b/extensions/flags/tests/integration/api/flags/ListTest.php index ef4f17b293..7b99a1db92 100644 --- a/extensions/flags/tests/integration/api/flags/ListTest.php +++ b/extensions/flags/tests/integration/api/flags/ListTest.php @@ -9,6 +9,7 @@ namespace Flarum\Flags\Tests\integration\api\flags; +use Carbon\Carbon; use Flarum\Discussion\Discussion; use Flarum\Flags\Flag; use Flarum\Group\Group; @@ -57,11 +58,11 @@ protected function setUp(): void ['id' => 3, 'discussion_id' => 1, 'user_id' => 1, 'type' => 'comment', 'content' => '

'], ], 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], - ['id' => 4, 'post_id' => 2, 'user_id' => 2], - ['id' => 5, 'post_id' => 3, 'user_id' => 1], + ['id' => 1, 'post_id' => 1, 'user_id' => 1, 'created_at' => Carbon::now()->addMinutes(2)], + ['id' => 2, 'post_id' => 1, 'user_id' => 2, 'created_at' => Carbon::now()->addMinutes(3)], + ['id' => 3, 'post_id' => 1, 'user_id' => 3, 'created_at' => Carbon::now()->addMinutes(4)], + ['id' => 4, 'post_id' => 2, 'user_id' => 2, 'created_at' => Carbon::now()->addMinutes(5)], + ['id' => 5, 'post_id' => 3, 'user_id' => 1, 'created_at' => Carbon::now()->addMinutes(6)], ] ]); } @@ -77,12 +78,14 @@ public function admin_can_see_one_flag_per_post() ]) ); - $this->assertEquals(200, $response->getStatusCode()); + $body = $response->getBody()->getContents(); - $data = json_decode($response->getBody()->getContents(), true)['data']; + $this->assertEquals(200, $response->getStatusCode(), $body); + + $data = json_decode($body, true)['data']; $ids = Arr::pluck($data, 'id'); - $this->assertEqualsCanonicalizing(['1', '4', '5'], $ids); + $this->assertEqualsCanonicalizing(['3', '4', '5'], $ids); } /** @@ -120,7 +123,7 @@ public function mod_can_see_one_flag_per_post() $data = json_decode($response->getBody()->getContents(), true)['data']; $ids = Arr::pluck($data, 'id'); - $this->assertEqualsCanonicalizing(['1', '4', '5'], $ids); + $this->assertEqualsCanonicalizing(['3', '4', '5'], $ids); } /** diff --git a/extensions/flags/tests/integration/api/flags/ListWithTagsTest.php b/extensions/flags/tests/integration/api/flags/ListWithTagsTest.php index ab65c66fec..dbf63967ab 100644 --- a/extensions/flags/tests/integration/api/flags/ListWithTagsTest.php +++ b/extensions/flags/tests/integration/api/flags/ListWithTagsTest.php @@ -18,6 +18,7 @@ use Flarum\Testing\integration\TestCase; use Flarum\User\User; use Illuminate\Support\Arr; +use Illuminate\Support\Carbon; class ListWithTagsTest extends TestCase { @@ -86,16 +87,16 @@ protected function setUp(): void ], Flag::class => [ // From regular ListTest - ['id' => 1, 'post_id' => 1, 'user_id' => 1], - ['id' => 2, 'post_id' => 1, 'user_id' => 2], - ['id' => 3, 'post_id' => 1, 'user_id' => 3], - ['id' => 4, 'post_id' => 2, 'user_id' => 2], - ['id' => 5, 'post_id' => 3, 'user_id' => 1], + ['id' => 1, 'post_id' => 1, 'user_id' => 1, 'created_at' => Carbon::now()->addMinutes(2)], + ['id' => 2, 'post_id' => 1, 'user_id' => 2, 'created_at' => Carbon::now()->addMinutes(3)], + ['id' => 3, 'post_id' => 1, 'user_id' => 3, 'created_at' => Carbon::now()->addMinutes(4)], + ['id' => 4, 'post_id' => 2, 'user_id' => 2, 'created_at' => Carbon::now()->addMinutes(5)], + ['id' => 5, 'post_id' => 3, 'user_id' => 1, 'created_at' => Carbon::now()->addMinutes(6)], // In tags - ['id' => 6, 'post_id' => 4, 'user_id' => 1], - ['id' => 7, 'post_id' => 5, 'user_id' => 1], - ['id' => 8, 'post_id' => 6, 'user_id' => 1], - ['id' => 9, 'post_id' => 7, 'user_id' => 1], + ['id' => 6, 'post_id' => 4, 'user_id' => 1, 'created_at' => Carbon::now()->addMinutes(7)], + ['id' => 7, 'post_id' => 5, 'user_id' => 1, 'created_at' => Carbon::now()->addMinutes(8)], + ['id' => 8, 'post_id' => 6, 'user_id' => 1, 'created_at' => Carbon::now()->addMinutes(9)], + ['id' => 9, 'post_id' => 7, 'user_id' => 1, 'created_at' => Carbon::now()->addMinutes(10)], ] ]); } @@ -111,12 +112,14 @@ public function admin_can_see_one_flag_per_post() ]) ); - $this->assertEquals(200, $response->getStatusCode()); + $body = $response->getBody()->getContents(); - $data = json_decode($response->getBody()->getContents(), true)['data']; + $this->assertEquals(200, $response->getStatusCode(), $body); + + $data = json_decode($body, true)['data']; $ids = Arr::pluck($data, 'id'); - $this->assertEqualsCanonicalizing(['1', '4', '5', '6', '7', '8', '9'], $ids); + $this->assertEqualsCanonicalizing(['3', '4', '5', '6', '7', '8', '9'], $ids); } /** @@ -156,7 +159,7 @@ public function mod_can_see_one_flag_per_post() $ids = Arr::pluck($data, 'id'); // 7 is included, even though mods can't view discussions. // This is because the UI doesnt allow discussions.viewFlags without viewDiscussions. - $this->assertEqualsCanonicalizing(['1', '4', '5', '7', '8', '9'], $ids); + $this->assertEqualsCanonicalizing(['3', '4', '5', '7', '8', '9'], $ids); } /** diff --git a/extensions/likes/src/Api/LoadLikesRelationship.php b/extensions/likes/src/Api/LoadLikesRelationship.php index 4f9b626197..530be4fcdd 100644 --- a/extensions/likes/src/Api/LoadLikesRelationship.php +++ b/extensions/likes/src/Api/LoadLikesRelationship.php @@ -31,6 +31,7 @@ public static function mutateRelation(BelongsToMany $query, ServerRequestInterfa $query // So that we can tell if the current user has liked the post. ->orderBy(new Expression($grammar->wrap('user_id').' = '.$actor->id), 'desc') + ->orderBy('post_likes.created_at') // Limiting a relationship results is only possible because // the Post model uses the \Staudenmeir\EloquentEagerLimit\HasEagerLimit // trait. diff --git a/extensions/likes/tests/integration/api/ListPostsTest.php b/extensions/likes/tests/integration/api/ListPostsTest.php index 767de463cc..922234eda5 100644 --- a/extensions/likes/tests/integration/api/ListPostsTest.php +++ b/extensions/likes/tests/integration/api/ListPostsTest.php @@ -54,17 +54,17 @@ protected function setUp(): void ['id' => 112, 'username' => 'user112', 'email' => '112@machine.local', 'is_email_confirmed' => 1], ], 'post_likes' => [ - ['user_id' => 102, 'post_id' => 101], - ['user_id' => 104, 'post_id' => 101], - ['user_id' => 105, 'post_id' => 101], - ['user_id' => 106, 'post_id' => 101], - ['user_id' => 107, 'post_id' => 101], - ['user_id' => 108, 'post_id' => 101], - ['user_id' => 109, 'post_id' => 101], - ['user_id' => 110, 'post_id' => 101], - ['user_id' => 2, 'post_id' => 101], - ['user_id' => 111, 'post_id' => 101], - ['user_id' => 112, 'post_id' => 101], + ['user_id' => 102, 'post_id' => 101, 'created_at' => Carbon::now()->addMinutes(2)], + ['user_id' => 104, 'post_id' => 101, 'created_at' => Carbon::now()->addMinutes(3)], + ['user_id' => 105, 'post_id' => 101, 'created_at' => Carbon::now()->addMinutes(4)], + ['user_id' => 106, 'post_id' => 101, 'created_at' => Carbon::now()->addMinutes(5)], + ['user_id' => 107, 'post_id' => 101, 'created_at' => Carbon::now()->addMinutes(6)], + ['user_id' => 108, 'post_id' => 101, 'created_at' => Carbon::now()->addMinutes(7)], + ['user_id' => 109, 'post_id' => 101, 'created_at' => Carbon::now()->addMinutes(8)], + ['user_id' => 110, 'post_id' => 101, 'created_at' => Carbon::now()->addMinutes(9)], + ['user_id' => 2, 'post_id' => 101, 'created_at' => Carbon::now()->addMinutes(10)], + ['user_id' => 111, 'post_id' => 101, 'created_at' => Carbon::now()->addMinutes(11)], + ['user_id' => 112, 'post_id' => 101, 'created_at' => Carbon::now()->addMinutes(12)], ], 'group_permission' => [ ['group_id' => Group::GUEST_ID, 'permission' => 'searchUsers'], diff --git a/extensions/lock/src/Filter/LockedFilter.php b/extensions/lock/src/Filter/LockedFilter.php index 6fb2349f59..6d5cb2c2d8 100644 --- a/extensions/lock/src/Filter/LockedFilter.php +++ b/extensions/lock/src/Filter/LockedFilter.php @@ -12,7 +12,7 @@ use Flarum\Search\Database\DatabaseSearchState; use Flarum\Search\Filter\FilterInterface; use Flarum\Search\SearchState; -use Illuminate\Database\Query\Builder; +use Illuminate\Database\Eloquent\Builder; /** * @implements FilterInterface diff --git a/extensions/mentions/tests/integration/api/PostMentionsTest.php b/extensions/mentions/tests/integration/api/PostMentionsTest.php index 3d0304c45e..0ae3c1edf6 100644 --- a/extensions/mentions/tests/integration/api/PostMentionsTest.php +++ b/extensions/mentions/tests/integration/api/PostMentionsTest.php @@ -51,7 +51,7 @@ protected function setUp(): void ['id' => 8, 'number' => 6, 'discussion_id' => 2, 'created_at' => Carbon::now(), 'user_id' => 4, 'type' => 'comment', 'content' => '@"i_am_a_deleted_user"#p2020'], ['id' => 9, 'number' => 10, 'discussion_id' => 2, 'created_at' => Carbon::now(), 'user_id' => 5, 'type' => 'comment', 'content' => '

I am bad

'], ['id' => 10, 'number' => 11, 'discussion_id' => 2, 'created_at' => Carbon::now(), 'user_id' => 4, 'type' => 'comment', 'content' => '@"Bad "#p6 User"#p9'], - ['id' => 11, 'number' => 12, 'discussion_id' => 2, 'created_at' => Carbon::now(), 'user_id' => 40, 'type' => 'comment', 'content' => '@"Bad "#p6 User"#p9'], + ['id' => 11, 'number' => 12, 'discussion_id' => 2, 'created_at' => Carbon::now(), 'user_id' => null, 'type' => 'comment', 'content' => '@"Bad "#p6 User"#p9'], ['id' => 12, 'number' => 13, 'discussion_id' => 2, 'created_at' => Carbon::now(), 'user_id' => 4, 'type' => 'comment', 'content' => '@"acme"#p11'], // Restricted access @@ -504,9 +504,11 @@ public function editing_a_post_with_deleted_author_that_has_a_mention_works() ]) ); - $this->assertEquals(200, $response->getStatusCode()); + $body = $response->getBody()->getContents(); - $response = json_decode($response->getBody(), true); + $this->assertEquals(200, $response->getStatusCode(), $body); + + $response = json_decode($body, true); $this->assertStringContainsString('Bad "#p6 User', $response['data']['attributes']['contentHtml']); $this->assertEquals('@"Bad _ User"#p9', $response['data']['attributes']['content']); diff --git a/extensions/mentions/tests/integration/api/UserMentionsTest.php b/extensions/mentions/tests/integration/api/UserMentionsTest.php index 6566c8f878..e528a828f2 100644 --- a/extensions/mentions/tests/integration/api/UserMentionsTest.php +++ b/extensions/mentions/tests/integration/api/UserMentionsTest.php @@ -38,6 +38,7 @@ protected function setUp(): void ['id' => 3, 'username' => 'potato', 'email' => 'potato@machine.local', 'is_email_confirmed' => 1], ['id' => 4, 'username' => 'toby', 'email' => 'toby@machine.local', 'is_email_confirmed' => 1], ['id' => 5, 'username' => 'bad_user', 'email' => 'bad_user@machine.local', 'is_email_confirmed' => 1], + ['id' => 50] ], Discussion::class => [ ['id' => 2, 'title' => __CLASS__, 'created_at' => Carbon::now(), 'last_posted_at' => Carbon::now(), 'user_id' => 3, 'first_post_id' => 4, 'comment_count' => 2], @@ -488,9 +489,11 @@ public function editing_a_post_with_deleted_author_that_has_a_mention_works() ]) ); - $this->assertEquals(200, $response->getStatusCode()); + $body = $response->getBody()->getContents(); - $response = json_decode($response->getBody(), true); + $this->assertEquals(200, $response->getStatusCode(), $body); + + $response = json_decode($body, true); $this->assertStringContainsString('Bad "#p6 User', $response['data']['attributes']['contentHtml']); $this->assertEquals('@"Bad _ User"#5', $response['data']['attributes']['content']); diff --git a/extensions/statistics/src/Api/Controller/ShowStatisticsData.php b/extensions/statistics/src/Api/Controller/ShowStatisticsData.php index 7f5bb5b374..285bfcb5c7 100644 --- a/extensions/statistics/src/Api/Controller/ShowStatisticsData.php +++ b/extensions/statistics/src/Api/Controller/ShowStatisticsData.php @@ -130,11 +130,18 @@ private function getTimedCounts(Builder $query, string $column, ?DateTime $start $endDate = new DateTime(); } + $formats = match ($query->getConnection()->getDriverName()) { + 'pgsql' => ['YYYY-MM-DD HH24:00:00', 'YYYY-MM-DD'], + default => ['%Y-%m-%d %H:00:00', '%Y-%m-%d'], + }; + // 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'; + $format = "CASE WHEN $column > ? THEN '$formats[0]' ELSE '$formats[1]' END"; + $dbFormattedDatetime = match ($query->getConnection()->getDriverName()) { - 'sqlite' => 'strftime('.$format.', '.$column.')', - default => 'DATE_FORMAT('.$column.', '.$format.')', + 'sqlite' => "strftime($format, $column)", + 'pgsql' => "TO_CHAR($column, $format)", + default => "DATE_FORMAT($column, $format)", }; $results = $query diff --git a/extensions/sticky/src/PinStickiedDiscussionsToTop.php b/extensions/sticky/src/PinStickiedDiscussionsToTop.php index a8b57eebc0..c0dd58bde8 100755 --- a/extensions/sticky/src/PinStickiedDiscussionsToTop.php +++ b/extensions/sticky/src/PinStickiedDiscussionsToTop.php @@ -19,7 +19,7 @@ class PinStickiedDiscussionsToTop public function __invoke(DatabaseSearchState $state, SearchCriteria $criteria): void { if ($criteria->sortIsDefault && ! $state->isFulltextSearch()) { - $query = $state->getQuery(); + $query = $state->getQuery()->getQuery(); // If we are viewing a specific tag, then pin all stickied // discussions to the top no matter what. @@ -58,7 +58,7 @@ public function __invoke(DatabaseSearchState $state, SearchCriteria $criteria): // 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])); + $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 ?: '1970-01-01 00:00:00'])); } $query->union($sticky); diff --git a/extensions/sticky/src/Query/StickyFilter.php b/extensions/sticky/src/Query/StickyFilter.php index 74ab036ebf..af008f649c 100644 --- a/extensions/sticky/src/Query/StickyFilter.php +++ b/extensions/sticky/src/Query/StickyFilter.php @@ -12,7 +12,7 @@ use Flarum\Search\Database\DatabaseSearchState; use Flarum\Search\Filter\FilterInterface; use Flarum\Search\SearchState; -use Illuminate\Database\Query\Builder; +use Illuminate\Database\Eloquent\Builder; /** * @implements FilterInterface diff --git a/extensions/sticky/tests/integration/api/ListDiscussionsTest.php b/extensions/sticky/tests/integration/api/ListDiscussionsTest.php index 5647c50f4b..c3464b7011 100644 --- a/extensions/sticky/tests/integration/api/ListDiscussionsTest.php +++ b/extensions/sticky/tests/integration/api/ListDiscussionsTest.php @@ -62,9 +62,11 @@ public function list_discussions_shows_sticky_first_as_guest() $this->request('GET', '/api/discussions') ); - $this->assertEquals(200, $response->getStatusCode()); + $body = $response->getBody()->getContents(); - $data = json_decode($response->getBody()->getContents(), true); + $this->assertEquals(200, $response->getStatusCode(), $body); + + $data = json_decode($body, true); $this->assertEquals([3, 1, 2, 4], Arr::pluck($data['data'], 'id')); } @@ -114,9 +116,11 @@ public function list_discussions_shows_stick_first_on_a_tag() ]) ); - $this->assertEquals(200, $response->getStatusCode()); + $body = $response->getBody()->getContents(); - $data = json_decode($response->getBody()->getContents(), true); + $this->assertEquals(200, $response->getStatusCode(), $body); + + $data = json_decode($body, true); $this->assertEquals([3, 1, 2, 4], Arr::pluck($data['data'], 'id')); } diff --git a/extensions/subscriptions/src/Filter/SubscriptionFilter.php b/extensions/subscriptions/src/Filter/SubscriptionFilter.php index 6e6f6d2f44..993949ebd7 100644 --- a/extensions/subscriptions/src/Filter/SubscriptionFilter.php +++ b/extensions/subscriptions/src/Filter/SubscriptionFilter.php @@ -14,7 +14,7 @@ use Flarum\Search\SearchState; use Flarum\Search\ValidateFilterTrait; use Flarum\User\User; -use Illuminate\Database\Query\Builder; +use Illuminate\Database\Eloquent\Builder; /** * @implements FilterInterface diff --git a/extensions/suspend/src/Query/SuspendedFilter.php b/extensions/suspend/src/Query/SuspendedFilter.php index ed9080066e..e4bfbf3c47 100644 --- a/extensions/suspend/src/Query/SuspendedFilter.php +++ b/extensions/suspend/src/Query/SuspendedFilter.php @@ -15,7 +15,7 @@ use Flarum\Search\SearchState; use Flarum\User\Guest; use Flarum\User\UserRepository; -use Illuminate\Database\Query\Builder; +use Illuminate\Database\Eloquent\Builder; /** * @implements FilterInterface diff --git a/extensions/tags/src/Search/Filter/TagFilter.php b/extensions/tags/src/Search/Filter/TagFilter.php index 5c82c6bfde..f3fca59385 100644 --- a/extensions/tags/src/Search/Filter/TagFilter.php +++ b/extensions/tags/src/Search/Filter/TagFilter.php @@ -17,7 +17,8 @@ use Flarum\Tags\Tag; use Flarum\User\User; use Illuminate\Database\Eloquent\ModelNotFoundException; -use Illuminate\Database\Query\Builder; +use Illuminate\Database\Eloquent\Builder; +use Illuminate\Database\Query\Builder as QueryBuilder; /** * @implements FilterInterface @@ -53,7 +54,7 @@ protected function constrain(Builder $query, string|array $rawSlugs, bool $negat $query->where(function (Builder $query) use ($slugs, $negate, $actor) { foreach ($slugs as $slug) { if ($slug === 'untagged') { - $query->whereIn('discussions.id', function (Builder $query) { + $query->whereIn('discussions.id', function (QueryBuilder $query) { $query->select('discussion_id') ->from('discussion_tag'); }, 'or', ! $negate); @@ -65,7 +66,7 @@ protected function constrain(Builder $query, string|array $rawSlugs, bool $negat $id = null; } - $query->whereIn('discussions.id', function (Builder $query) use ($id) { + $query->whereIn('discussions.id', function (QueryBuilder $query) use ($id) { $query->select('discussion_id') ->from('discussion_tag') ->where('tag_id', $id); diff --git a/extensions/tags/src/TagState.php b/extensions/tags/src/TagState.php index c17c0f6b58..7ad4838107 100644 --- a/extensions/tags/src/TagState.php +++ b/extensions/tags/src/TagState.php @@ -31,6 +31,8 @@ class TagState extends AbstractModel protected $casts = ['marked_as_read_at' => 'datetime']; + public $incrementing = false; + public function tag(): BelongsTo { return $this->belongsTo(Tag::class); 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 412fe8d983..f822064310 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 @@ -32,11 +32,10 @@ $table->unique(['discussion_id', 'number']); }); - $connection = $schema->getConnection(); - $prefix = $connection->getTablePrefix(); - - if ($connection->getDriverName() !== 'sqlite') { - $connection->statement('ALTER TABLE '.$prefix.'posts ADD FULLTEXT content (content)'); + if ($schema->getConnection()->getDriverName() !== 'sqlite') { + $schema->table('posts', function (Blueprint $table) { + $table->fullText('content'); + }); } }, 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 303f2027fe..a60269e4ad 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 @@ -26,11 +26,22 @@ $table->integer('user_id')->unsigned()->change(); }); - // Use a separate schema instance because this column gets renamed - // in the previous one. - $schema->table('access_tokens', function (Blueprint $table) { - $table->dateTime('last_activity_at')->change(); - }); + if ($schema->getConnection()->getDriverName() !== 'pgsql') { + // Use a separate schema instance because this column gets renamed + // in the previous one. + $schema->table('access_tokens', function (Blueprint $table) { + $table->dateTime('last_activity_at')->change(); + }); + } else { + $prefix = $schema->getConnection()->getTablePrefix(); + + // Changing an integer col to datetime is an unusual operation in PostgreSQL. + $schema->getConnection()->statement(<< function (Builder $schema) { 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 e49130d5b6..714a8482a3 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 @@ -7,21 +7,23 @@ * LICENSE file that was distributed with this source code. */ +use Illuminate\Database\Schema\Blueprint; use Illuminate\Database\Schema\Builder; return [ 'up' => function (Builder $schema) { - $connection = $schema->getConnection(); - $prefix = $connection->getTablePrefix(); - - if ($connection->getDriverName() !== 'sqlite') { - $connection->statement('ALTER TABLE '.$prefix.'discussions ADD FULLTEXT title (title)'); + if ($schema->getConnection()->getDriverName() !== 'sqlite') { + $schema->table('discussions', function (Blueprint $table) { + $table->fullText('title'); + }); } }, 'down' => function (Builder $schema) { - $connection = $schema->getConnection(); - $prefix = $connection->getTablePrefix(); - $connection->statement('ALTER TABLE '.$prefix.'discussions DROP INDEX title'); + if ($schema->getConnection()->getDriverName() !== 'sqlite') { + $schema->table('discussions', function (Blueprint $table) { + $table->dropFullText('title'); + }); + } } ]; diff --git a/framework/core/migrations/2024_05_05_000001_convert_preferences_to_json_in_users.php b/framework/core/migrations/2024_05_05_000001_convert_preferences_to_json_in_users.php new file mode 100644 index 0000000000..e63ede3b42 --- /dev/null +++ b/framework/core/migrations/2024_05_05_000001_convert_preferences_to_json_in_users.php @@ -0,0 +1,37 @@ + function (Builder $schema) { + if ($schema->getConnection()->getDriverName() !== 'pgsql') { + $schema->table('users', function (Blueprint $table) { + $table->json('preferences')->nullable()->change(); + }); + } else { + $users = $schema->getConnection()->getSchemaGrammar()->wrapTable('users'); + $preferences = $schema->getConnection()->getSchemaGrammar()->wrap('preferences'); + $schema->getConnection()->statement("ALTER TABLE $users ALTER COLUMN $preferences TYPE JSON USING preferences::TEXT::JSON"); + } + }, + + 'down' => function (Builder $schema) { + if ($schema->getConnection()->getDriverName() !== 'pgsql') { + $schema->table('users', function (Blueprint $table) { + $table->binary('preferences')->nullable()->change(); + }); + } else { + $users = $schema->getConnection()->getSchemaGrammar()->wrapTable('users'); + $preferences = $schema->getConnection()->getSchemaGrammar()->wrap('preferences'); + $schema->getConnection()->statement("ALTER TABLE $users ALTER COLUMN $preferences TYPE BYTEA USING preferences::TEXT::BYTEA"); + } + } +]; diff --git a/framework/core/src/Database/DatabaseServiceProvider.php b/framework/core/src/Database/DatabaseServiceProvider.php index cb05162160..4885d8d424 100644 --- a/framework/core/src/Database/DatabaseServiceProvider.php +++ b/framework/core/src/Database/DatabaseServiceProvider.php @@ -19,6 +19,8 @@ use Illuminate\Database\ConnectionInterface; use Illuminate\Database\ConnectionResolverInterface; use Illuminate\Database\Eloquent\Factories\Factory; +use Illuminate\Database\Eloquent\Builder as EloquentBuilder; +use Illuminate\Database\Query\Builder as QueryBuilder; use Illuminate\Support\Str; class DatabaseServiceProvider extends AbstractServiceProvider @@ -28,6 +30,7 @@ class DatabaseServiceProvider extends AbstractServiceProvider public function register(): void { $this->registerEloquentFactory(); + $this->registerBuilderMacros(); $this->container->singleton(Manager::class, function (ContainerImplementation $container) { $manager = new Manager($container); @@ -78,6 +81,29 @@ public function register(): void }); } + protected function registerBuilderMacros(): void + { + $drivers = [ + 'mysql' => 'whenMySql', + 'pgsql' => 'whenPgSql', + 'sqlite' => 'whenSqlite', + ]; + + foreach ([QueryBuilder::class, EloquentBuilder::class] as $builder) { + foreach ($drivers as $driver => $macro) { + $builder::macro($macro, function ($callback) use ($driver) { + /** @var QueryBuilder|EloquentBuilder $this */ + + if ($this->getConnection()->getDriverName() === $driver) { + $callback($this); + } + + return $this; + }); + } + } + } + protected function registerEloquentFactory(): void { $this->app->singleton(FakerGenerator::class, function ($app, $parameters) { diff --git a/framework/core/src/Database/Migrator.php b/framework/core/src/Database/Migrator.php index a132a80370..cb381d30f5 100644 --- a/framework/core/src/Database/Migrator.php +++ b/framework/core/src/Database/Migrator.php @@ -69,8 +69,38 @@ public function runMigrationList(string $path, array $migrations, ?Extension $ex // Once we have the array of migrations, we will spin through them and run the // migrations "up" so the changes are made to the databases. We'll then log // that the migration was run so we don't repeat it next time we execute. - foreach ($migrations as $file) { - $this->runUp($path, $file, $extension); + $this->runUpMigrations($migrations, $path, $extension); + } + + protected function runUpMigrations(array $migrations, string $path, ?Extension $extension = null): void + { + $process = function () use ($migrations, $path, $extension) { + foreach ($migrations as $migration) { + $this->runUp($path, $migration, $extension); + } + }; + + // PgSQL allows DDL statements in transactions. + if ($this->connection->getDriverName() === 'pgsql') { + $this->connection->transaction($process); + } else { + $process(); + } + } + + protected function runDownMigrations(array $migrations, string $path, ?Extension $extension = null): void + { + $process = function () use ($migrations, $path, $extension) { + foreach ($migrations as $migration) { + $this->runDown($path, $migration, $extension); + } + }; + + // PgSQL allows DDL statements in transactions. + if ($this->connection->getDriverName() === 'pgsql') { + $this->connection->transaction($process); + } else { + $process(); } } @@ -103,9 +133,7 @@ public function reset(string $path, ?Extension $extension = null): int if ($count === 0) { $this->note('Nothing to rollback.'); } else { - foreach ($migrations as $migration) { - $this->runDown($path, $migration, $extension); - } + $this->runDownMigrations($migrations, $path, $extension); } return $count; diff --git a/framework/core/src/Discussion/Search/Filter/AuthorFilter.php b/framework/core/src/Discussion/Search/Filter/AuthorFilter.php index ef5c44dc79..138cbc975f 100644 --- a/framework/core/src/Discussion/Search/Filter/AuthorFilter.php +++ b/framework/core/src/Discussion/Search/Filter/AuthorFilter.php @@ -14,7 +14,7 @@ use Flarum\Search\SearchState; use Flarum\Search\ValidateFilterTrait; use Flarum\User\UserRepository; -use Illuminate\Database\Query\Builder; +use Illuminate\Database\Eloquent\Builder; /** * @implements FilterInterface diff --git a/framework/core/src/Discussion/Search/Filter/CreatedFilter.php b/framework/core/src/Discussion/Search/Filter/CreatedFilter.php index c66bac114a..bd93a2940a 100644 --- a/framework/core/src/Discussion/Search/Filter/CreatedFilter.php +++ b/framework/core/src/Discussion/Search/Filter/CreatedFilter.php @@ -13,7 +13,7 @@ use Flarum\Search\Filter\FilterInterface; use Flarum\Search\SearchState; use Flarum\Search\ValidateFilterTrait; -use Illuminate\Database\Query\Builder; +use Illuminate\Database\Eloquent\Builder; use Illuminate\Support\Arr; /** @@ -40,7 +40,7 @@ public function filter(SearchState $state, string|array $value, bool $negate): v $this->constrain($state->getQuery(), $from, $to, $negate); } - public function constrain(Builder $query, ?string $from, ?string $to, bool $negate): void + protected function constrain(Builder $query, ?string $from, ?string $to, bool $negate): void { // If we've just been provided with a single YYYY-MM-DD date, then find // discussions that were started on that exact date. But if we've been diff --git a/framework/core/src/Discussion/Search/Filter/HiddenFilter.php b/framework/core/src/Discussion/Search/Filter/HiddenFilter.php index e9e52cae86..37fca15605 100644 --- a/framework/core/src/Discussion/Search/Filter/HiddenFilter.php +++ b/framework/core/src/Discussion/Search/Filter/HiddenFilter.php @@ -12,7 +12,7 @@ use Flarum\Search\Database\DatabaseSearchState; use Flarum\Search\Filter\FilterInterface; use Flarum\Search\SearchState; -use Illuminate\Database\Query\Builder; +use Illuminate\Database\Eloquent\Builder; /** * @implements FilterInterface diff --git a/framework/core/src/Discussion/Search/Filter/UnreadFilter.php b/framework/core/src/Discussion/Search/Filter/UnreadFilter.php index 6c12ac6359..945c65d419 100644 --- a/framework/core/src/Discussion/Search/Filter/UnreadFilter.php +++ b/framework/core/src/Discussion/Search/Filter/UnreadFilter.php @@ -14,7 +14,7 @@ use Flarum\Search\Filter\FilterInterface; use Flarum\Search\SearchState; use Flarum\User\User; -use Illuminate\Database\Query\Builder; +use Illuminate\Database\Eloquent\Builder; /** * @implements FilterInterface @@ -41,11 +41,15 @@ protected function constrain(Builder $query, User $actor, bool $negate): void if ($actor->exists) { $readIds = $this->discussions->getReadIdsQuery($actor); - $query->where(function ($query) use ($readIds, $negate, $actor) { + $query->where(function (Builder $query) use ($readIds, $negate, $actor) { if (! $negate) { - $query->whereNotIn('id', $readIds)->where('last_posted_at', '>', $actor->marked_all_as_read_at ?: 0); + $query->whereNotIn('id', $readIds)->when($actor->marked_all_as_read_at, function (Builder $query) use ($actor) { + $query->where('last_posted_at', '>', $actor->marked_all_as_read_at); + }); } else { - $query->whereIn('id', $readIds)->orWhere('last_posted_at', '<=', $actor->marked_all_as_read_at ?: 0); + $query->whereIn('id', $readIds)->when($actor->marked_all_as_read_at, function (Builder $query) use ($actor) { + $query->orWhere('last_posted_at', '<=', $actor->marked_all_as_read_at); + }); } }); } diff --git a/framework/core/src/Discussion/Search/FulltextFilter.php b/framework/core/src/Discussion/Search/FulltextFilter.php index ada06a184b..6da4311b95 100644 --- a/framework/core/src/Discussion/Search/FulltextFilter.php +++ b/framework/core/src/Discussion/Search/FulltextFilter.php @@ -14,8 +14,9 @@ use Flarum\Search\AbstractFulltextFilter; use Flarum\Search\Database\DatabaseSearchState; use Flarum\Search\SearchState; -use Illuminate\Database\Query\Builder; +use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Query\Expression; +use RuntimeException; /** * @extends AbstractFulltextFilter @@ -23,28 +24,40 @@ class FulltextFilter extends AbstractFulltextFilter { public function search(SearchState $state, string $value): void + { + match ($state->getQuery()->getConnection()->getDriverName()) { + 'mysql' => $this->mysql($state, $value), + 'pgsql' => $this->pgsql($state, $value), + 'sqlite' => $this->sqlite($state, $value), + default => throw new RuntimeException('Unsupported database driver: '.$state->getQuery()->getConnection()->getDriverName()), + }; + } + + protected function sqlite(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; - } + $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() + ); + }); + }); + } + + protected function mysql(SearchState $state, string $value): void + { + /** @var Builder $query */ + $query = $state->getQuery(); // Replace all non-word characters with spaces. // We do this to prevent MySQL fulltext search boolean mode from taking @@ -53,10 +66,15 @@ public function search(SearchState $state, string $value): void $grammar = $query->getGrammar(); + $match = 'MATCH(' . $grammar->wrap('posts.content') . ') AGAINST (?)'; + $matchBooleanMode = 'MATCH(' . $grammar->wrap('posts.content') . ') AGAINST (? IN BOOLEAN MODE)'; + $matchTitle = 'MATCH(' . $grammar->wrap('discussions.title') . ') AGAINST (?)'; + $mostRelevantPostId = 'SUBSTRING_INDEX(GROUP_CONCAT('.$grammar->wrap('posts.id').' ORDER BY '.$match.' DESC, '.$grammar->wrap('posts.number').'), \',\', 1) as most_relevant_post_id'; + $discussionSubquery = Discussion::select('id') ->selectRaw('NULL as score') ->selectRaw('first_post_id as most_relevant_post_id') - ->whereRaw('MATCH('.$grammar->wrap('discussions.title').') AGAINST (? IN BOOLEAN MODE)', [$value]); + ->whereRaw($matchTitle, [$value]);; // Construct a subquery to fetch discussions which contain relevant // posts. Retrieve the collective relevance of each discussion's posts, @@ -64,10 +82,10 @@ public function search(SearchState $state, string $value): void // the ID of the most relevant post. $subquery = Post::whereVisibleTo($state->getActor()) ->select('posts.discussion_id') - ->selectRaw('SUM(MATCH('.$grammar->wrap('posts.content').') AGAINST (?)) as score', [$value]) - ->selectRaw('SUBSTRING_INDEX(GROUP_CONCAT('.$grammar->wrap('posts.id').' ORDER BY MATCH('.$grammar->wrap('posts.content').') AGAINST (?) DESC, '.$grammar->wrap('posts.number').'), \',\', 1) as most_relevant_post_id', [$value]) + ->selectRaw("SUM($match) as score", [$value]) + ->selectRaw($mostRelevantPostId, [$value]) ->where('posts.type', 'comment') - ->whereRaw('MATCH('.$grammar->wrap('posts.content').') AGAINST (? IN BOOLEAN MODE)', [$value]) + ->whereRaw($matchBooleanMode, [$value]) ->groupBy('posts.discussion_id') ->union($discussionSubquery); @@ -84,9 +102,69 @@ public function search(SearchState $state, string $value): void ->groupBy('discussions.id') ->addBinding($subquery->getBindings(), 'join'); - $state->setDefaultSort(function (Builder $query) use ($grammar, $value) { - $query->orderByRaw('MATCH('.$grammar->wrap('discussions.title').') AGAINST (?) desc', [$value]); + $state->setDefaultSort(function (Builder $query) use ($grammar, $value, $matchTitle) { + $query->orderByRaw("$matchTitle desc", [$value]); $query->orderBy('posts_ft.score', 'desc'); }); } + + protected function pgsql(SearchState $state, string $value): void + { + /** @var Builder $query */ + $query = $state->getQuery(); + + $grammar = $query->getGrammar(); + + $matchCondition = "to_tsvector('english', ".$grammar->wrap('posts.content').") @@ plainto_tsquery('english', ?)"; + $matchScore = "ts_rank(to_tsvector('english', ".$grammar->wrap('posts.content')."), plainto_tsquery('english', ?))"; + $matchTitleCondition = "to_tsvector('english', ".$grammar->wrap('discussions.title').") @@ plainto_tsquery('english', ?)"; + $matchTitleScore = "ts_rank(to_tsvector('english', " . $grammar->wrap('discussions.title') . "), plainto_tsquery('english', ?))"; + $mostRelevantPostId = "CAST(SPLIT_PART(STRING_AGG(CAST(".$grammar->wrap('posts.id')." AS VARCHAR), ',' ORDER BY ".$matchScore." DESC, ".$grammar->wrap('posts.number')."), ',', 1) AS INTEGER) as most_relevant_post_id"; + + $discussionSubquery = Discussion::select('id') + ->selectRaw('NULL as score') + ->selectRaw('first_post_id as most_relevant_post_id') + ->whereRaw($matchTitleCondition, [$value]);; + + // Construct a subquery to fetch discussions which contain relevant + // posts. Retrieve the collective relevance of each discussion's posts, + // which we will use later in the order by clause, and also retrieve + // the ID of the most relevant post. + $subquery = Post::whereVisibleTo($state->getActor()) + ->select('posts.discussion_id') + ->selectRaw("SUM($matchScore) as score", [$value]) + ->selectRaw($mostRelevantPostId, [$value]) + ->where('posts.type', 'comment') + ->whereRaw($matchCondition, [$value]) + ->groupBy('posts.discussion_id') + ->union($discussionSubquery); + + // Join the subquery into the main search query and scope results to + // discussions that have a relevant title or that contain relevant posts. + $query + ->distinct('discussions.id') + ->addSelect('posts_ft.most_relevant_post_id') + ->addSelect('posts_ft.score') + ->join( + new Expression('('.$subquery->toSql().') '.$grammar->wrapTable('posts_ft')), + 'posts_ft.discussion_id', + '=', + 'discussions.id' + ) + ->addBinding($subquery->getBindings(), 'join') + ->orderBy('discussions.id'); + + $state->setQuery( + $query + ->getModel() + ->newQuery() + ->select('*') + ->fromSub($query, 'discussions') + ); + + $state->setDefaultSort(function (Builder $query) use ($grammar, $value, $matchTitleScore) { + $query->orderByRaw("$matchTitleScore desc", [$value]); + $query->orderBy('discussions.score', 'desc'); + }); + } } diff --git a/framework/core/src/Discussion/UserState.php b/framework/core/src/Discussion/UserState.php index f777fed1eb..7aca423ac1 100644 --- a/framework/core/src/Discussion/UserState.php +++ b/framework/core/src/Discussion/UserState.php @@ -44,6 +44,8 @@ class UserState extends AbstractModel 'last_read_at' => 'datetime' ]; + public $incrementing = false; + /** * The attributes that are mass assignable. */ diff --git a/framework/core/src/Foundation/ApplicationInfoProvider.php b/framework/core/src/Foundation/ApplicationInfoProvider.php index 0f5aa22112..4dea735914 100644 --- a/framework/core/src/Foundation/ApplicationInfoProvider.php +++ b/framework/core/src/Foundation/ApplicationInfoProvider.php @@ -71,7 +71,7 @@ public function identifyQueueDriver(): string public function identifyDatabaseVersion(): string { return match ($this->config['database.driver']) { - 'mysql' => $this->db->selectOne('select version() as version')->version, + 'mysql', 'pgsql' => $this->db->selectOne('select version() as version')->version, 'sqlite' => $this->db->selectOne('select sqlite_version() as version')->version, default => 'Unknown', }; @@ -81,6 +81,7 @@ public function identifyDatabaseDriver(): string { return match ($this->config['database.driver']) { 'mysql' => 'MySQL', + 'pgsql' => 'PostgreSQL', 'sqlite' => 'SQLite', default => $this->config['database.driver'], }; diff --git a/framework/core/src/Group/Permission.php b/framework/core/src/Group/Permission.php index e6413c6509..60be408ff5 100644 --- a/framework/core/src/Group/Permission.php +++ b/framework/core/src/Group/Permission.php @@ -26,6 +26,8 @@ class Permission extends AbstractModel 'created_at' => 'datetime' ]; + public $incrementing = false; + public function group(): BelongsTo { return $this->belongsTo(Group::class); diff --git a/framework/core/src/Install/Console/UserDataProvider.php b/framework/core/src/Install/Console/UserDataProvider.php index 5ef3de9901..7e58774d9f 100644 --- a/framework/core/src/Install/Console/UserDataProvider.php +++ b/framework/core/src/Install/Console/UserDataProvider.php @@ -42,20 +42,31 @@ public function configure(Installation $installation): Installation private function getDatabaseConfiguration(): DatabaseConfig { - $host = $this->ask('Database host (required):'); - $port = 3306; + $driver = $this->ask('Database driver (mysql, sqlite, pgsql) (Default: mysql):', 'mysql'); + $port = match ($driver) { + 'mysql' => 3306, + 'pgsql' => 5432, + default => 0, + }; + + if (in_array($driver, ['mysql', 'pgsql'])) { + $host = $this->ask('Database host (required):'); + + if (Str::contains($host, ':')) { + list($host, $port) = explode(':', $host, 2); + } - if (Str::contains($host, ':')) { - list($host, $port) = explode(':', $host, 2); + $user = $this->ask('Database user (required):'); + $password = $this->secret('Database password:'); } return new DatabaseConfig( - $this->ask('Database driver (mysql, sqlite) (Default: mysql):', 'mysql'), - $host, + $driver, + $host ?? null, intval($port), $this->ask('Database name (required):'), - $this->ask('Database user (required):'), - $this->secret('Database password:'), + $user ?? null, + $password ?? null, $this->ask('Prefix:') ); } diff --git a/framework/core/src/Install/Controller/InstallController.php b/framework/core/src/Install/Controller/InstallController.php index f066c794c1..fee31cd69c 100644 --- a/framework/core/src/Install/Controller/InstallController.php +++ b/framework/core/src/Install/Controller/InstallController.php @@ -76,20 +76,25 @@ public function handle(Request $request): ResponseInterface private function makeDatabaseConfig(array $input): DatabaseConfig { - $host = Arr::get($input, 'mysqlHost'); - $port = 3306; + $driver = Arr::get($input, 'dbDriver'); + $host = Arr::get($input, 'dbHost'); + $port = match ($driver) { + 'mysql' => 3306, + 'pgsql' => 5432, + default => 0, + }; if (Str::contains($host, ':')) { list($host, $port) = explode(':', $host, 2); } return new DatabaseConfig( - Arr::get($input, 'dbDriver'), + $driver, $host, intval($port), Arr::get($input, 'dbName'), - Arr::get($input, 'mysqlUsername'), - Arr::get($input, 'mysqlPassword'), + Arr::get($input, 'dbUsername'), + Arr::get($input, 'dbPassword'), Arr::get($input, 'tablePrefix') ); } diff --git a/framework/core/src/Install/DatabaseConfig.php b/framework/core/src/Install/DatabaseConfig.php index b5e4c9ed78..ee81eccfbd 100644 --- a/framework/core/src/Install/DatabaseConfig.php +++ b/framework/core/src/Install/DatabaseConfig.php @@ -16,11 +16,11 @@ class DatabaseConfig implements Arrayable { public function __construct( private readonly string $driver, - private readonly string $host, + private readonly ?string $host, private readonly int $port, private string $database, - private readonly string $username, - private readonly string $password, + private readonly ?string $username, + private readonly ?string $password, private readonly string $prefix ) { $this->validate(); @@ -42,15 +42,15 @@ private function validate(): void throw new ValidationFailed('Please specify a database driver.'); } - if (! in_array($this->driver, ['mysql', 'sqlite'])) { + if (! in_array($this->driver, ['mysql', 'sqlite', 'pgsql'])) { throw new ValidationFailed('Currently, only MySQL/MariaDB and SQLite are supported.'); } - if ($this->driver === 'mysql' && empty($this->host)) { + if (in_array($this->driver, ['mysql', 'pgsql']) && empty($this->host)) { throw new ValidationFailed('Please specify the hostname of your database server.'); } - if ($this->driver === 'mysql' && ($this->port < 1 || $this->port > 65535)) { + if (in_array($this->driver, ['mysql', 'pgsql']) && ($this->port < 1 || $this->port > 65535)) { throw new ValidationFailed('Please provide a valid port number between 1 and 65535.'); } @@ -58,7 +58,7 @@ private function validate(): void throw new ValidationFailed('Please specify the database name.'); } - if ($this->driver === 'mysql' && empty($this->username)) { + if (in_array($this->driver, ['mysql', 'pgsql']) && empty($this->username)) { throw new ValidationFailed('Please specify the username for accessing the database.'); } @@ -94,6 +94,15 @@ private function driverOptions(): array 'engine' => 'InnoDB', 'strict' => false, ], + 'pgsql' => [ + 'host' => $this->host, + 'port' => $this->port, + 'username' => $this->username, + 'password' => $this->password, + 'charset' => 'utf8', + 'search_path' => 'public', + 'sslmode' => 'prefer', + ], 'sqlite' => [ 'foreign_key_constraints' => true, ], diff --git a/framework/core/src/Install/Steps/ConnectToDatabase.php b/framework/core/src/Install/Steps/ConnectToDatabase.php index a978c12b11..e12e14a524 100644 --- a/framework/core/src/Install/Steps/ConnectToDatabase.php +++ b/framework/core/src/Install/Steps/ConnectToDatabase.php @@ -13,8 +13,10 @@ use Flarum\Install\DatabaseConfig; use Flarum\Install\Step; use Illuminate\Database\Connectors\MySqlConnector; +use Illuminate\Database\Connectors\PostgresConnector; use Illuminate\Database\Connectors\SQLiteConnector; use Illuminate\Database\MySqlConnection; +use Illuminate\Database\PostgresConnection; use Illuminate\Database\SQLiteConnection; use Illuminate\Support\Str; use InvalidArgumentException; @@ -40,6 +42,7 @@ public function run(): void match ($config['driver']) { 'mysql' => $this->mysql($config), + 'pgsql' => $this->pgsql($config), 'sqlite' => $this->sqlite($config), default => throw new InvalidArgumentException('Unsupported database driver: '.$config['driver']), }; @@ -53,11 +56,11 @@ private function mysql(array $config): void if (Str::contains($version, 'MariaDB')) { if (version_compare($version, '10.10.0', '<')) { - throw new RangeException('MariaDB version too low. You need at least MariaDB 10.0.5'); + throw new RangeException("MariaDB version ($version) too low. You need at least MariaDB 10.0.5"); } } else { if (version_compare($version, '5.7.0', '<')) { - throw new RangeException('MySQL version too low. You need at least MySQL 5.7'); + throw new RangeException("MySQL version ($version) too low. You need at least MySQL 5.7"); } } @@ -71,6 +74,27 @@ private function mysql(array $config): void ); } + private function pgsql(array $config): void + { + $pdo = (new PostgresConnector)->connect($config); + + $version = $pdo->query('SHOW server_version')->fetchColumn(); + $version = Str::before($version, ' '); + + if (version_compare($version, '9.5.0', '<')) { + throw new RangeException("PostgreSQL version ($version) too low. You need at least PostgreSQL 9.5"); + } + + ($this->store)( + new PostgresConnection( + $pdo, + $config['database'], + $config['prefix'], + $config + ) + ); + } + private function sqlite(array $config): void { if (! file_exists($config['database'])) { @@ -82,7 +106,7 @@ private function sqlite(array $config): void $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'); + throw new RangeException("SQLite version ($version) too low. You need at least SQLite 3.8.8"); } ($this->store)( diff --git a/framework/core/src/Notification/NotificationRepository.php b/framework/core/src/Notification/NotificationRepository.php index 496c88e057..d1a889f9c7 100644 --- a/framework/core/src/Notification/NotificationRepository.php +++ b/framework/core/src/Notification/NotificationRepository.php @@ -22,7 +22,7 @@ public function findByUser(User $user, ?int $limit = null, int $offset = 0): Col { $primaries = Notification::query() ->selectRaw('MAX(id) AS id') - ->selectRaw('SUM(read_at IS NULL) AS unread_count') + ->selectRaw('COUNT(read_at IS NULL) AS unread_count') ->where('user_id', $user->id) ->whereIn('type', $user->getAlertableNotificationTypes()) ->where('is_deleted', false) diff --git a/framework/core/src/Search/Database/AbstractSearcher.php b/framework/core/src/Search/Database/AbstractSearcher.php index 86f026f0ea..c16ea64a60 100644 --- a/framework/core/src/Search/Database/AbstractSearcher.php +++ b/framework/core/src/Search/Database/AbstractSearcher.php @@ -33,7 +33,7 @@ public function search(SearchCriteria $criteria): SearchResults $query = $this->getQuery($actor); $search = new DatabaseSearchState($actor, $criteria->isFulltext()); - $search->setQuery($query->getQuery()); + $search->setQuery($query); $this->filters->apply($search, $criteria->filters); @@ -45,6 +45,8 @@ public function search(SearchCriteria $criteria): SearchResults $mutator($search, $criteria); } + $query = $search->getQuery(); + // Execute the search query and retrieve the results. We get one more // results than the user asked for, so that we can say if there are more // results. If there are, we will get rid of that extra result. diff --git a/framework/core/src/Search/Database/DatabaseSearchState.php b/framework/core/src/Search/Database/DatabaseSearchState.php index 1fd0c42f5b..8b3ff4718d 100644 --- a/framework/core/src/Search/Database/DatabaseSearchState.php +++ b/framework/core/src/Search/Database/DatabaseSearchState.php @@ -10,7 +10,7 @@ namespace Flarum\Search\Database; use Flarum\Search\SearchState; -use Illuminate\Database\Query\Builder; +use Illuminate\Database\Eloquent\Builder; class DatabaseSearchState extends SearchState { diff --git a/framework/core/src/User/Search/Filter/EmailFilter.php b/framework/core/src/User/Search/Filter/EmailFilter.php index 90e446c2f4..39d202c39a 100644 --- a/framework/core/src/User/Search/Filter/EmailFilter.php +++ b/framework/core/src/User/Search/Filter/EmailFilter.php @@ -13,7 +13,7 @@ use Flarum\Search\Filter\FilterInterface; use Flarum\Search\SearchState; use Flarum\Search\ValidateFilterTrait; -use Illuminate\Database\Query\Builder; +use Illuminate\Database\Eloquent\Builder; /** * @implements FilterInterface diff --git a/framework/core/src/User/Search/Filter/GroupFilter.php b/framework/core/src/User/Search/Filter/GroupFilter.php index aee788411e..e45a1f0eb4 100644 --- a/framework/core/src/User/Search/Filter/GroupFilter.php +++ b/framework/core/src/User/Search/Filter/GroupFilter.php @@ -15,7 +15,7 @@ use Flarum\Search\SearchState; use Flarum\Search\ValidateFilterTrait; use Flarum\User\User; -use Illuminate\Database\Query\Builder; +use Illuminate\Database\Eloquent\Builder; /** * @implements FilterInterface @@ -50,7 +50,7 @@ protected function constrain(Builder $query, User $actor, string|array $rawQuery $groupQuery = Group::whereVisibleTo($actor) ->join('group_user', 'groups.id', 'group_user.group_id') - ->where(function (\Illuminate\Database\Eloquent\Builder $query) use ($ids, $names) { + ->where(function (Builder $query) use ($ids, $names) { $query->whereIn('groups.id', $ids) ->orWhereIn($query->raw('lower(name_singular)'), $names) ->orWhereIn($query->raw('lower(name_plural)'), $names); diff --git a/framework/core/src/User/User.php b/framework/core/src/User/User.php index 6ad65e2c05..040eea5818 100644 --- a/framework/core/src/User/User.php +++ b/framework/core/src/User/User.php @@ -375,7 +375,9 @@ protected function getUnreadNotifications(): Collection public function getNewNotificationCount(): int { return $this->unreadNotifications() - ->where('created_at', '>', $this->read_notifications_at ?? 0) + ->when($this->read_notifications_at, function (Builder $query) { + $query->where('created_at', '>', $this->read_notifications_at); + }) ->count(); } diff --git a/framework/core/tests/integration/api/access_tokens/CreateTest.php b/framework/core/tests/integration/api/access_tokens/CreateTest.php index 07613e576d..21ffb0dbaf 100644 --- a/framework/core/tests/integration/api/access_tokens/CreateTest.php +++ b/framework/core/tests/integration/api/access_tokens/CreateTest.php @@ -61,7 +61,7 @@ public function user_can_create_developer_tokens(int $authenticatedAs) ]) ); - $this->assertEquals(201, $response->getStatusCode()); + $this->assertEquals(201, $response->getStatusCode(), $response->getBody()); } /** diff --git a/framework/core/tests/integration/api/discussions/ListTest.php b/framework/core/tests/integration/api/discussions/ListTest.php index ac8d699639..6a8aaf54e6 100644 --- a/framework/core/tests/integration/api/discussions/ListTest.php +++ b/framework/core/tests/integration/api/discussions/ListTest.php @@ -85,7 +85,11 @@ public function author_filter_works() ]) ); - $data = json_decode($response->getBody()->getContents(), true)['data']; + $body = $response->getBody()->getContents(); + + $this->assertEquals(200, $response->getStatusCode(), $body); + + $data = json_decode($body, true)['data']; // Order-independent comparison $this->assertEqualsCanonicalizing(['2', '3'], Arr::pluck($data, 'id'), 'IDs do not match'); @@ -123,7 +127,9 @@ public function created_filter_works_with_date() ]) ); - $data = json_decode($response->getBody()->getContents(), true)['data']; + $data = json_decode($body = $response->getBody()->getContents(), true)['data'] ?? null; + + $this->assertEquals(200, $response->getStatusCode(), $body); // Order-independent comparison $this->assertEquals(['3'], Arr::pluck($data, 'id'), 'IDs do not match'); diff --git a/framework/core/tests/integration/api/discussions/ListWithFulltextSearchTest.php b/framework/core/tests/integration/api/discussions/ListWithFulltextSearchTest.php index 395bf55ec5..40bd494638 100644 --- a/framework/core/tests/integration/api/discussions/ListWithFulltextSearchTest.php +++ b/framework/core/tests/integration/api/discussions/ListWithFulltextSearchTest.php @@ -85,7 +85,10 @@ public function can_search_for_word_or_title_in_post() ]) ); - $data = json_decode($response->getBody()->getContents(), true); + $data = json_decode($body = $response->getBody()->getContents(), true); + + $this->assertEquals(200, $response->getStatusCode(), $body); + $ids = array_map(function ($row) { return $row['id']; }, $data['data']); diff --git a/framework/core/tests/integration/api/notifications/ListTest.php b/framework/core/tests/integration/api/notifications/ListTest.php index 8b94feeebb..fff0b91fbe 100644 --- a/framework/core/tests/integration/api/notifications/ListTest.php +++ b/framework/core/tests/integration/api/notifications/ListTest.php @@ -54,6 +54,6 @@ public function shows_index_for_user() ]) ); - $this->assertEquals(200, $response->getStatusCode()); + $this->assertEquals(200, $response->getStatusCode(), $response->getBody()->getContents()); } } diff --git a/framework/core/tests/integration/api/posts/DeleteTest.php b/framework/core/tests/integration/api/posts/DeleteTest.php index cf87ecd79d..1bcb021bfb 100644 --- a/framework/core/tests/integration/api/posts/DeleteTest.php +++ b/framework/core/tests/integration/api/posts/DeleteTest.php @@ -30,19 +30,20 @@ protected function setUp(): void $this->prepareDatabase([ User::class => [ + $this->normalUser(), ['id' => 3, 'username' => 'acme', 'email' => 'acme@machine.local', 'is_email_confirmed' => 1], ['id' => 4, 'username' => 'acme2', 'email' => 'acme2@machine.local', 'is_email_confirmed' => 1], ], Discussion::class => [ - ['id' => 3, 'title' => __CLASS__, 'created_at' => Carbon::now(), 'last_posted_at' => Carbon::now(), 'user_id' => 2, 'first_post_id' => 1, 'comment_count' => 5, 'last_post_number' => 5, 'last_post_id' => 10], + ['id' => 3, 'title' => __CLASS__, 'created_at' => Carbon::now(), 'last_posted_at' => Carbon::now(), 'user_id' => 2, 'first_post_id' => 5, 'comment_count' => 5, 'last_post_number' => 5, 'last_post_id' => 10], ], Post::class => [ - ['id' => 5, 'discussion_id' => 3, 'created_at' => Carbon::createFromDate(1975, 5, 21)->toDateTimeString(), 'user_id' => 2, 'type' => 'comment', 'content' => '

foo bar

', 'number' => 1], - ['id' => 6, 'discussion_id' => 3, 'created_at' => Carbon::createFromDate(1975, 5, 21)->toDateTimeString(), 'user_id' => 1, 'type' => 'comment', 'content' => '

foo bar

', 'number' => 2], - ['id' => 7, 'discussion_id' => 3, 'created_at' => Carbon::createFromDate(1975, 5, 21)->toDateTimeString(), 'user_id' => 1, 'type' => 'comment', 'content' => '

foo bar

', 'number' => 3], - ['id' => 8, 'discussion_id' => 3, 'created_at' => Carbon::createFromDate(1975, 5, 21)->toDateTimeString(), 'user_id' => 1, 'type' => 'comment', 'content' => '

foo bar

', 'number' => 4], - ['id' => 9, 'discussion_id' => 3, 'created_at' => Carbon::createFromDate(1975, 5, 21)->toDateTimeString(), 'user_id' => 1, 'type' => 'comment', 'content' => '

foo bar

', 'number' => 5], - ['id' => 10, 'discussion_id' => 3, 'created_at' => Carbon::createFromDate(1975, 5, 21)->toDateTimeString(), 'user_id' => 1, 'type' => 'comment', 'content' => '

foo bar

', 'number' => 6], + ['id' => 5, 'discussion_id' => 3, 'created_at' => Carbon::createFromDate(1975, 5, 21)->addMinutes(2)->toDateTimeString(), 'user_id' => 2, 'type' => 'comment', 'content' => '

foo bar

', 'number' => 1], + ['id' => 6, 'discussion_id' => 3, 'created_at' => Carbon::createFromDate(1975, 5, 21)->addMinutes(3)->toDateTimeString(), 'user_id' => 1, 'type' => 'comment', 'content' => '

foo bar

', 'number' => 2], + ['id' => 7, 'discussion_id' => 3, 'created_at' => Carbon::createFromDate(1975, 5, 21)->addMinutes(4)->toDateTimeString(), 'user_id' => 1, 'type' => 'comment', 'content' => '

foo bar

', 'number' => 3], + ['id' => 8, 'discussion_id' => 3, 'created_at' => Carbon::createFromDate(1975, 5, 21)->addMinutes(5)->toDateTimeString(), 'user_id' => 1, 'type' => 'comment', 'content' => '

foo bar

', 'number' => 4], + ['id' => 9, 'discussion_id' => 3, 'created_at' => Carbon::createFromDate(1975, 5, 21)->addMinutes(6)->toDateTimeString(), 'user_id' => 1, 'type' => 'comment', 'content' => '

foo bar

', 'number' => 5], + ['id' => 10, 'discussion_id' => 3, 'created_at' => Carbon::createFromDate(1975, 5, 21)->addMinutes(7)->toDateTimeString(), 'user_id' => 1, 'type' => 'comment', 'content' => '

foo bar

', 'number' => 6], ], 'discussion_user' => [ ['discussion_id' => 3, 'user_id' => 2, 'last_read_post_number' => 6], diff --git a/framework/core/tests/integration/api/users/GroupSearchTest.php b/framework/core/tests/integration/api/users/GroupSearchTest.php index d38c5b7077..8714cfc745 100644 --- a/framework/core/tests/integration/api/users/GroupSearchTest.php +++ b/framework/core/tests/integration/api/users/GroupSearchTest.php @@ -46,7 +46,7 @@ public function allows_group_filter_for_admin() { $response = $this->createRequest(['admin'], 1); - $this->assertEquals(200, $response->getStatusCode()); + $this->assertEquals(200, $response->getStatusCode(), $response->getBody()->getContents()); } /** @@ -225,11 +225,7 @@ public function admin_can_select_multiple_groups_and_hidden() $responseBodyContents = json_decode($response->getBody()->getContents(), true); $this->assertCount(5, $responseBodyContents['data'], json_encode($responseBodyContents)); $this->assertCount(5, $responseBodyContents['included'], json_encode($responseBodyContents)); - $this->assertEquals(1, $responseBodyContents['included'][0]['id']); - $this->assertEquals(99, $responseBodyContents['included'][1]['id']); - $this->assertEquals(4, $responseBodyContents['included'][2]['id']); - $this->assertEquals(5, $responseBodyContents['included'][3]['id']); - $this->assertEquals(6, $responseBodyContents['included'][4]['id']); + $this->assertEqualsCanonicalizing([1, 99, 4, 5, 6], array_column($responseBodyContents['included'], 'id')); } private function createRequest(array $group, int $userId = null) diff --git a/framework/core/tests/integration/api/users/ListTest.php b/framework/core/tests/integration/api/users/ListTest.php index a16b73b7a0..ef21abc4c0 100644 --- a/framework/core/tests/integration/api/users/ListTest.php +++ b/framework/core/tests/integration/api/users/ListTest.php @@ -89,7 +89,7 @@ public function shows_full_results_without_search_or_filter() $this->assertEquals(200, $response->getStatusCode()); $data = json_decode($response->getBody()->getContents(), true)['data']; - $this->assertEquals(['1', '2'], Arr::pluck($data, 'id')); + $this->assertEqualsCanonicalizing(['1', '2'], Arr::pluck($data, 'id')); } /** diff --git a/framework/core/views/install/install.php b/framework/core/views/install/install.php index 37bb260f21..d94ef7cf95 100644 --- a/framework/core/views/install/install.php +++ b/framework/core/views/install/install.php @@ -13,9 +13,9 @@
-
+
- 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. + Warning: Please keep in mind that while Flarum supports SQLite and PostgreSQL, 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.
@@ -25,6 +25,7 @@
@@ -34,20 +35,20 @@ -
+
- - + +
- - + +
- - + +
@@ -93,7 +94,7 @@ group.style.display = 'none'; }); - const groups = document.querySelectorAll('[data-group="' + this.value + '"]'); + const groups = document.querySelectorAll('[data-group*="' + this.value + '"]'); groups.forEach(function(group) { group.style.display = 'block'; diff --git a/php-packages/testing/src/integration/Setup/SetupScript.php b/php-packages/testing/src/integration/Setup/SetupScript.php index 5b190cf944..0d0f2471db 100644 --- a/php-packages/testing/src/integration/Setup/SetupScript.php +++ b/php-packages/testing/src/integration/Setup/SetupScript.php @@ -40,7 +40,11 @@ public function __construct() { $this->driver = getenv('DB_DRIVER') ?: 'mysql'; $this->host = getenv('DB_HOST') ?: 'localhost'; - $this->port = intval(getenv('DB_PORT') ?: 3306); + $this->port = intval(getenv('DB_PORT') ?: match ($this->driver) { + 'mysql' => 3306, + 'pgsql' => 5432, + default => 0, + }); $this->name = getenv('DB_DATABASE') ?: 'flarum_test'; $this->user = getenv('DB_USERNAME') ?: 'root'; $this->pass = getenv('DB_PASSWORD') ?? 'root'; diff --git a/php-packages/testing/src/integration/TestCase.php b/php-packages/testing/src/integration/TestCase.php index b450be491d..dec3f09cb5 100644 --- a/php-packages/testing/src/integration/TestCase.php +++ b/php-packages/testing/src/integration/TestCase.php @@ -201,6 +201,10 @@ protected function populateDatabase(): void */ $this->database()->getSchemaBuilder()->disableForeignKeyConstraints(); + if ($this->database()->getDriverName() === 'pgsql') { + $this->database()->statement("SET session_replication_role = 'replica'"); + } + $databaseContent = []; foreach ($this->databaseContent as $tableOrModelClass => $_rows) { @@ -237,6 +241,10 @@ protected function populateDatabase(): void } } + if ($this->database()->getDriverName() === 'pgsql') { + $this->database()->statement("SET session_replication_role = 'origin'"); + } + // And finally, turn on foreign key checks again. $this->database()->getSchemaBuilder()->enableForeignKeyConstraints(); } From 95180a44b27331b22fbfe0069db32c3afb4dd4df Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Sun, 5 May 2024 11:06:45 +0000 Subject: [PATCH 02/29] Apply fixes from StyleCI --- .../tags/src/Search/Filter/TagFilter.php | 2 +- .../src/Database/DatabaseServiceProvider.php | 3 +-- .../src/Discussion/Search/FulltextFilter.php | 18 +++++++++--------- 3 files changed, 11 insertions(+), 12 deletions(-) diff --git a/extensions/tags/src/Search/Filter/TagFilter.php b/extensions/tags/src/Search/Filter/TagFilter.php index f3fca59385..552072aaa3 100644 --- a/extensions/tags/src/Search/Filter/TagFilter.php +++ b/extensions/tags/src/Search/Filter/TagFilter.php @@ -16,8 +16,8 @@ use Flarum\Search\ValidateFilterTrait; use Flarum\Tags\Tag; use Flarum\User\User; -use Illuminate\Database\Eloquent\ModelNotFoundException; use Illuminate\Database\Eloquent\Builder; +use Illuminate\Database\Eloquent\ModelNotFoundException; use Illuminate\Database\Query\Builder as QueryBuilder; /** diff --git a/framework/core/src/Database/DatabaseServiceProvider.php b/framework/core/src/Database/DatabaseServiceProvider.php index 4885d8d424..ae40acd033 100644 --- a/framework/core/src/Database/DatabaseServiceProvider.php +++ b/framework/core/src/Database/DatabaseServiceProvider.php @@ -18,8 +18,8 @@ use Illuminate\Database\Capsule\Manager; use Illuminate\Database\ConnectionInterface; use Illuminate\Database\ConnectionResolverInterface; -use Illuminate\Database\Eloquent\Factories\Factory; use Illuminate\Database\Eloquent\Builder as EloquentBuilder; +use Illuminate\Database\Eloquent\Factories\Factory; use Illuminate\Database\Query\Builder as QueryBuilder; use Illuminate\Support\Str; @@ -93,7 +93,6 @@ protected function registerBuilderMacros(): void foreach ($drivers as $driver => $macro) { $builder::macro($macro, function ($callback) use ($driver) { /** @var QueryBuilder|EloquentBuilder $this */ - if ($this->getConnection()->getDriverName() === $driver) { $callback($this); } diff --git a/framework/core/src/Discussion/Search/FulltextFilter.php b/framework/core/src/Discussion/Search/FulltextFilter.php index 6da4311b95..2d969e7811 100644 --- a/framework/core/src/Discussion/Search/FulltextFilter.php +++ b/framework/core/src/Discussion/Search/FulltextFilter.php @@ -66,15 +66,15 @@ protected function mysql(SearchState $state, string $value): void $grammar = $query->getGrammar(); - $match = 'MATCH(' . $grammar->wrap('posts.content') . ') AGAINST (?)'; - $matchBooleanMode = 'MATCH(' . $grammar->wrap('posts.content') . ') AGAINST (? IN BOOLEAN MODE)'; - $matchTitle = 'MATCH(' . $grammar->wrap('discussions.title') . ') AGAINST (?)'; + $match = 'MATCH('.$grammar->wrap('posts.content').') AGAINST (?)'; + $matchBooleanMode = 'MATCH('.$grammar->wrap('posts.content').') AGAINST (? IN BOOLEAN MODE)'; + $matchTitle = 'MATCH('.$grammar->wrap('discussions.title').') AGAINST (?)'; $mostRelevantPostId = 'SUBSTRING_INDEX(GROUP_CONCAT('.$grammar->wrap('posts.id').' ORDER BY '.$match.' DESC, '.$grammar->wrap('posts.number').'), \',\', 1) as most_relevant_post_id'; $discussionSubquery = Discussion::select('id') ->selectRaw('NULL as score') ->selectRaw('first_post_id as most_relevant_post_id') - ->whereRaw($matchTitle, [$value]);; + ->whereRaw($matchTitle, [$value]); // Construct a subquery to fetch discussions which contain relevant // posts. Retrieve the collective relevance of each discussion's posts, @@ -102,7 +102,7 @@ protected function mysql(SearchState $state, string $value): void ->groupBy('discussions.id') ->addBinding($subquery->getBindings(), 'join'); - $state->setDefaultSort(function (Builder $query) use ($grammar, $value, $matchTitle) { + $state->setDefaultSort(function (Builder $query) use ($value, $matchTitle) { $query->orderByRaw("$matchTitle desc", [$value]); $query->orderBy('posts_ft.score', 'desc'); }); @@ -118,13 +118,13 @@ protected function pgsql(SearchState $state, string $value): void $matchCondition = "to_tsvector('english', ".$grammar->wrap('posts.content').") @@ plainto_tsquery('english', ?)"; $matchScore = "ts_rank(to_tsvector('english', ".$grammar->wrap('posts.content')."), plainto_tsquery('english', ?))"; $matchTitleCondition = "to_tsvector('english', ".$grammar->wrap('discussions.title').") @@ plainto_tsquery('english', ?)"; - $matchTitleScore = "ts_rank(to_tsvector('english', " . $grammar->wrap('discussions.title') . "), plainto_tsquery('english', ?))"; - $mostRelevantPostId = "CAST(SPLIT_PART(STRING_AGG(CAST(".$grammar->wrap('posts.id')." AS VARCHAR), ',' ORDER BY ".$matchScore." DESC, ".$grammar->wrap('posts.number')."), ',', 1) AS INTEGER) as most_relevant_post_id"; + $matchTitleScore = "ts_rank(to_tsvector('english', ".$grammar->wrap('discussions.title')."), plainto_tsquery('english', ?))"; + $mostRelevantPostId = 'CAST(SPLIT_PART(STRING_AGG(CAST('.$grammar->wrap('posts.id')." AS VARCHAR), ',' ORDER BY ".$matchScore.' DESC, '.$grammar->wrap('posts.number')."), ',', 1) AS INTEGER) as most_relevant_post_id"; $discussionSubquery = Discussion::select('id') ->selectRaw('NULL as score') ->selectRaw('first_post_id as most_relevant_post_id') - ->whereRaw($matchTitleCondition, [$value]);; + ->whereRaw($matchTitleCondition, [$value]); // Construct a subquery to fetch discussions which contain relevant // posts. Retrieve the collective relevance of each discussion's posts, @@ -162,7 +162,7 @@ protected function pgsql(SearchState $state, string $value): void ->fromSub($query, 'discussions') ); - $state->setDefaultSort(function (Builder $query) use ($grammar, $value, $matchTitleScore) { + $state->setDefaultSort(function (Builder $query) use ($value, $matchTitleScore) { $query->orderByRaw("$matchTitleScore desc", [$value]); $query->orderBy('discussions.score', 'desc'); }); From 6b713c80d63932b38cb806b617d4cc2f851d253f Mon Sep 17 00:00:00 2001 From: Sami Mazouz Date: Sun, 5 May 2024 12:17:14 +0100 Subject: [PATCH 03/29] fix --- framework/core/src/Discussion/Search/FulltextFilter.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/framework/core/src/Discussion/Search/FulltextFilter.php b/framework/core/src/Discussion/Search/FulltextFilter.php index 2d969e7811..4bb6f9e338 100644 --- a/framework/core/src/Discussion/Search/FulltextFilter.php +++ b/framework/core/src/Discussion/Search/FulltextFilter.php @@ -15,6 +15,7 @@ use Flarum\Search\Database\DatabaseSearchState; use Flarum\Search\SearchState; use Illuminate\Database\Eloquent\Builder; +use Illuminate\Database\Query\Builder as QueryBuilder; use Illuminate\Database\Query\Expression; use RuntimeException; @@ -40,7 +41,7 @@ protected function sqlite(SearchState $state, string $value): void $query->where(function (Builder $query) use ($state, $value) { $query->where('discussions.title', 'like', "%$value%") - ->orWhereExists(function (Builder $query) use ($state, $value) { + ->orWhereExists(function (QueryBuilder $query) use ($state, $value) { $query->selectRaw('1') ->from( Post::whereVisibleTo($state->getActor()) From a065acf7ddc1285f5676c4cbe871bc1e95114599 Mon Sep 17 00:00:00 2001 From: Sami Mazouz Date: Sun, 5 May 2024 14:33:47 +0100 Subject: [PATCH 04/29] chore: generate dump --- framework/core/migrations/pgsql-install.dump | 1248 +++++++++++++++++ framework/core/src/Database/Migrator.php | 8 +- .../Install/Steps/EnableBundledExtensions.php | 17 +- 3 files changed, 1256 insertions(+), 17 deletions(-) create mode 100644 framework/core/migrations/pgsql-install.dump diff --git a/framework/core/migrations/pgsql-install.dump b/framework/core/migrations/pgsql-install.dump new file mode 100644 index 0000000000..96f9f15f75 --- /dev/null +++ b/framework/core/migrations/pgsql-install.dump @@ -0,0 +1,1248 @@ +-- +-- PostgreSQL database dump +-- + +-- Dumped from database version 14.11 (Debian 14.11-1.pgdg120+2) +-- Dumped by pg_dump version 15.6 (Debian 15.6-0+deb12u1) + +SET statement_timeout = 0; +SET lock_timeout = 0; +SET idle_in_transaction_session_timeout = 0; +SET client_encoding = 'UTF8'; +SET standard_conforming_strings = on; +SELECT pg_catalog.set_config('search_path', '', false); +SET check_function_bodies = false; +SET xmloption = content; +SET client_min_messages = warning; +SET row_security = off; + +-- +-- Name: public; Type: SCHEMA; Schema: -; Owner: - +-- + +-- *not* creating schema, since initdb creates it + + +-- +-- Name: SCHEMA public; Type: COMMENT; Schema: -; Owner: - +-- + +COMMENT ON SCHEMA public IS ''; + + +SET default_tablespace = ''; + +SET default_table_access_method = heap; + +-- +-- Name: db_prefix_access_tokens; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.db_prefix_access_tokens ( + token character varying(40) NOT NULL, + user_id integer NOT NULL, + last_activity_at timestamp(0) without time zone, + created_at timestamp(0) without time zone NOT NULL, + type character varying(100) NOT NULL, + id integer NOT NULL, + title character varying(150), + last_ip_address character varying(45), + last_user_agent character varying(255) +); + + +-- +-- Name: db_prefix_access_tokens_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.db_prefix_access_tokens_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: db_prefix_access_tokens_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.db_prefix_access_tokens_id_seq OWNED BY public.db_prefix_access_tokens.id; + + +-- +-- Name: db_prefix_api_keys; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.db_prefix_api_keys ( + key character varying(100) NOT NULL, + id integer NOT NULL, + allowed_ips character varying(255), + scopes character varying(255), + user_id integer, + created_at timestamp(0) without time zone NOT NULL, + last_activity_at timestamp(0) without time zone +); + + +-- +-- Name: db_prefix_api_keys_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.db_prefix_api_keys_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: db_prefix_api_keys_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.db_prefix_api_keys_id_seq OWNED BY public.db_prefix_api_keys.id; + + +-- +-- Name: db_prefix_discussion_user; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.db_prefix_discussion_user ( + user_id integer NOT NULL, + discussion_id integer NOT NULL, + last_read_at timestamp(0) without time zone, + last_read_post_number integer +); + + +-- +-- Name: db_prefix_discussions; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.db_prefix_discussions ( + id integer NOT NULL, + title character varying(200) NOT NULL, + comment_count integer DEFAULT 1 NOT NULL, + participant_count integer DEFAULT 0 NOT NULL, + created_at timestamp(0) without time zone NOT NULL, + user_id integer, + first_post_id integer, + last_posted_at timestamp(0) without time zone, + last_posted_user_id integer, + last_post_id integer, + last_post_number integer, + hidden_at timestamp without time zone, + hidden_user_id integer, + slug character varying(255) NOT NULL, + is_private boolean DEFAULT false NOT NULL +); + + +-- +-- Name: db_prefix_discussions_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.db_prefix_discussions_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: db_prefix_discussions_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.db_prefix_discussions_id_seq OWNED BY public.db_prefix_discussions.id; + + +-- +-- Name: db_prefix_email_tokens; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.db_prefix_email_tokens ( + token character varying(100) NOT NULL, + email character varying(150) NOT NULL, + user_id integer NOT NULL, + created_at timestamp(0) without time zone NOT NULL +); + + +-- +-- Name: db_prefix_group_permission; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.db_prefix_group_permission ( + group_id integer NOT NULL, + permission character varying(100) NOT NULL, + created_at timestamp(0) without time zone +); + + +-- +-- Name: db_prefix_group_user; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.db_prefix_group_user ( + user_id integer NOT NULL, + group_id integer NOT NULL, + created_at timestamp(0) without time zone +); + + +-- +-- Name: db_prefix_groups; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.db_prefix_groups ( + id integer NOT NULL, + name_singular character varying(100) NOT NULL, + name_plural character varying(100) NOT NULL, + color character varying(20), + icon character varying(100), + is_hidden boolean DEFAULT false NOT NULL, + created_at timestamp(0) without time zone, + updated_at timestamp(0) without time zone +); + + +-- +-- Name: db_prefix_groups_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.db_prefix_groups_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: db_prefix_groups_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.db_prefix_groups_id_seq OWNED BY public.db_prefix_groups.id; + + +-- +-- Name: db_prefix_login_providers; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.db_prefix_login_providers ( + id integer NOT NULL, + user_id integer NOT NULL, + provider character varying(100) NOT NULL, + identifier character varying(100) NOT NULL, + created_at timestamp(0) without time zone, + last_login_at timestamp(0) without time zone +); + + +-- +-- Name: db_prefix_login_providers_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.db_prefix_login_providers_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: db_prefix_login_providers_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.db_prefix_login_providers_id_seq OWNED BY public.db_prefix_login_providers.id; + + +-- +-- Name: db_prefix_migrations; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.db_prefix_migrations ( + id integer NOT NULL, + migration character varying(255) NOT NULL, + extension character varying(255) +); + + +-- +-- Name: db_prefix_migrations_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.db_prefix_migrations_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: db_prefix_migrations_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.db_prefix_migrations_id_seq OWNED BY public.db_prefix_migrations.id; + + +-- +-- Name: db_prefix_notifications; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.db_prefix_notifications ( + id integer NOT NULL, + user_id integer NOT NULL, + from_user_id integer, + type character varying(100) NOT NULL, + subject_id integer, + data bytea, + created_at timestamp(0) without time zone NOT NULL, + is_deleted boolean DEFAULT false NOT NULL, + read_at timestamp(0) without time zone +); + + +-- +-- Name: db_prefix_notifications_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.db_prefix_notifications_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: db_prefix_notifications_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.db_prefix_notifications_id_seq OWNED BY public.db_prefix_notifications.id; + + +-- +-- Name: db_prefix_password_tokens; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.db_prefix_password_tokens ( + token character varying(100) NOT NULL, + user_id integer NOT NULL, + created_at timestamp(0) without time zone NOT NULL +); + + +-- +-- Name: db_prefix_post_user; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.db_prefix_post_user ( + post_id integer NOT NULL, + user_id integer NOT NULL +); + + +-- +-- Name: db_prefix_posts; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.db_prefix_posts ( + id integer NOT NULL, + discussion_id integer NOT NULL, + number integer, + created_at timestamp(0) without time zone NOT NULL, + user_id integer, + type character varying(100), + content text, + edited_at timestamp(0) without time zone, + edited_user_id integer, + hidden_at timestamp(0) without time zone, + hidden_user_id integer, + ip_address character varying(45), + is_private boolean DEFAULT false NOT NULL +); + + +-- +-- Name: COLUMN db_prefix_posts.content; Type: COMMENT; Schema: public; Owner: - +-- + +COMMENT ON COLUMN public.db_prefix_posts.content IS ' '; + + +-- +-- Name: db_prefix_posts_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.db_prefix_posts_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: db_prefix_posts_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.db_prefix_posts_id_seq OWNED BY public.db_prefix_posts.id; + + +-- +-- Name: db_prefix_registration_tokens; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.db_prefix_registration_tokens ( + token character varying(100) NOT NULL, + payload text, + created_at timestamp(0) without time zone NOT NULL, + provider character varying(255) NOT NULL, + identifier character varying(255) NOT NULL, + user_attributes text +); + + +-- +-- Name: db_prefix_settings; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.db_prefix_settings ( + key character varying(100) NOT NULL, + value text +); + + +-- +-- Name: db_prefix_unsubscribe_tokens; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.db_prefix_unsubscribe_tokens ( + id bigint NOT NULL, + user_id integer NOT NULL, + email_type character varying(255) NOT NULL, + token character varying(100) NOT NULL, + unsubscribed_at timestamp(0) without time zone, + created_at timestamp(0) without time zone, + updated_at timestamp(0) without time zone +); + + +-- +-- Name: db_prefix_unsubscribe_tokens_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.db_prefix_unsubscribe_tokens_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: db_prefix_unsubscribe_tokens_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.db_prefix_unsubscribe_tokens_id_seq OWNED BY public.db_prefix_unsubscribe_tokens.id; + + +-- +-- Name: db_prefix_users; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.db_prefix_users ( + id integer NOT NULL, + username character varying(100) NOT NULL, + email character varying(150) NOT NULL, + is_email_confirmed boolean DEFAULT false NOT NULL, + password character varying(100) NOT NULL, + avatar_url character varying(100), + preferences json, + joined_at timestamp(0) without time zone, + last_seen_at timestamp(0) without time zone, + marked_all_as_read_at timestamp(0) without time zone, + read_notifications_at timestamp(0) without time zone, + discussion_count integer DEFAULT 0 NOT NULL, + comment_count integer DEFAULT 0 NOT NULL +); + + +-- +-- Name: db_prefix_users_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.db_prefix_users_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: db_prefix_users_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.db_prefix_users_id_seq OWNED BY public.db_prefix_users.id; + + +-- +-- Name: db_prefix_access_tokens id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.db_prefix_access_tokens ALTER COLUMN id SET DEFAULT nextval('public.db_prefix_access_tokens_id_seq'::regclass); + + +-- +-- Name: db_prefix_api_keys id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.db_prefix_api_keys ALTER COLUMN id SET DEFAULT nextval('public.db_prefix_api_keys_id_seq'::regclass); + + +-- +-- Name: db_prefix_discussions id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.db_prefix_discussions ALTER COLUMN id SET DEFAULT nextval('public.db_prefix_discussions_id_seq'::regclass); + + +-- +-- Name: db_prefix_groups id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.db_prefix_groups ALTER COLUMN id SET DEFAULT nextval('public.db_prefix_groups_id_seq'::regclass); + + +-- +-- Name: db_prefix_login_providers id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.db_prefix_login_providers ALTER COLUMN id SET DEFAULT nextval('public.db_prefix_login_providers_id_seq'::regclass); + + +-- +-- Name: db_prefix_migrations id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.db_prefix_migrations ALTER COLUMN id SET DEFAULT nextval('public.db_prefix_migrations_id_seq'::regclass); + + +-- +-- Name: db_prefix_notifications id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.db_prefix_notifications ALTER COLUMN id SET DEFAULT nextval('public.db_prefix_notifications_id_seq'::regclass); + + +-- +-- Name: db_prefix_posts id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.db_prefix_posts ALTER COLUMN id SET DEFAULT nextval('public.db_prefix_posts_id_seq'::regclass); + + +-- +-- Name: db_prefix_unsubscribe_tokens id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.db_prefix_unsubscribe_tokens ALTER COLUMN id SET DEFAULT nextval('public.db_prefix_unsubscribe_tokens_id_seq'::regclass); + + +-- +-- Name: db_prefix_users id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.db_prefix_users ALTER COLUMN id SET DEFAULT nextval('public.db_prefix_users_id_seq'::regclass); + + +-- +-- Name: db_prefix_access_tokens db_prefix_access_tokens_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.db_prefix_access_tokens + ADD CONSTRAINT db_prefix_access_tokens_pkey PRIMARY KEY (id); + + +-- +-- Name: db_prefix_access_tokens db_prefix_access_tokens_token_unique; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.db_prefix_access_tokens + ADD CONSTRAINT db_prefix_access_tokens_token_unique UNIQUE (token); + + +-- +-- Name: db_prefix_api_keys db_prefix_api_keys_key_unique; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.db_prefix_api_keys + ADD CONSTRAINT db_prefix_api_keys_key_unique UNIQUE (key); + + +-- +-- Name: db_prefix_api_keys db_prefix_api_keys_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.db_prefix_api_keys + ADD CONSTRAINT db_prefix_api_keys_pkey PRIMARY KEY (id); + + +-- +-- Name: db_prefix_registration_tokens db_prefix_auth_tokens_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.db_prefix_registration_tokens + ADD CONSTRAINT db_prefix_auth_tokens_pkey PRIMARY KEY (token); + + +-- +-- Name: db_prefix_settings db_prefix_config_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.db_prefix_settings + ADD CONSTRAINT db_prefix_config_pkey PRIMARY KEY (key); + + +-- +-- Name: db_prefix_discussions db_prefix_discussions_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.db_prefix_discussions + ADD CONSTRAINT db_prefix_discussions_pkey PRIMARY KEY (id); + + +-- +-- Name: db_prefix_email_tokens db_prefix_email_tokens_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.db_prefix_email_tokens + ADD CONSTRAINT db_prefix_email_tokens_pkey PRIMARY KEY (token); + + +-- +-- Name: db_prefix_groups db_prefix_groups_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.db_prefix_groups + ADD CONSTRAINT db_prefix_groups_pkey PRIMARY KEY (id); + + +-- +-- Name: db_prefix_login_providers db_prefix_login_providers_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.db_prefix_login_providers + ADD CONSTRAINT db_prefix_login_providers_pkey PRIMARY KEY (id); + + +-- +-- Name: db_prefix_login_providers db_prefix_login_providers_provider_identifier_unique; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.db_prefix_login_providers + ADD CONSTRAINT db_prefix_login_providers_provider_identifier_unique UNIQUE (provider, identifier); + + +-- +-- Name: db_prefix_migrations db_prefix_migrations_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.db_prefix_migrations + ADD CONSTRAINT db_prefix_migrations_pkey PRIMARY KEY (id); + + +-- +-- Name: db_prefix_notifications db_prefix_notifications_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.db_prefix_notifications + ADD CONSTRAINT db_prefix_notifications_pkey PRIMARY KEY (id); + + +-- +-- Name: db_prefix_password_tokens db_prefix_password_tokens_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.db_prefix_password_tokens + ADD CONSTRAINT db_prefix_password_tokens_pkey PRIMARY KEY (token); + + +-- +-- Name: db_prefix_group_permission db_prefix_permissions_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.db_prefix_group_permission + ADD CONSTRAINT db_prefix_permissions_pkey PRIMARY KEY (group_id, permission); + + +-- +-- Name: db_prefix_post_user db_prefix_post_user_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.db_prefix_post_user + ADD CONSTRAINT db_prefix_post_user_pkey PRIMARY KEY (post_id, user_id); + + +-- +-- Name: db_prefix_posts db_prefix_posts_discussion_id_number_unique; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.db_prefix_posts + ADD CONSTRAINT db_prefix_posts_discussion_id_number_unique UNIQUE (discussion_id, number); + + +-- +-- Name: db_prefix_posts db_prefix_posts_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.db_prefix_posts + ADD CONSTRAINT db_prefix_posts_pkey PRIMARY KEY (id); + + +-- +-- Name: db_prefix_unsubscribe_tokens db_prefix_unsubscribe_tokens_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.db_prefix_unsubscribe_tokens + ADD CONSTRAINT db_prefix_unsubscribe_tokens_pkey PRIMARY KEY (id); + + +-- +-- Name: db_prefix_unsubscribe_tokens db_prefix_unsubscribe_tokens_token_unique; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.db_prefix_unsubscribe_tokens + ADD CONSTRAINT db_prefix_unsubscribe_tokens_token_unique UNIQUE (token); + + +-- +-- Name: db_prefix_discussion_user db_prefix_users_discussions_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.db_prefix_discussion_user + ADD CONSTRAINT db_prefix_users_discussions_pkey PRIMARY KEY (user_id, discussion_id); + + +-- +-- Name: db_prefix_users db_prefix_users_email_unique; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.db_prefix_users + ADD CONSTRAINT db_prefix_users_email_unique UNIQUE (email); + + +-- +-- Name: db_prefix_group_user db_prefix_users_groups_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.db_prefix_group_user + ADD CONSTRAINT db_prefix_users_groups_pkey PRIMARY KEY (user_id, group_id); + + +-- +-- Name: db_prefix_users db_prefix_users_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.db_prefix_users + ADD CONSTRAINT db_prefix_users_pkey PRIMARY KEY (id); + + +-- +-- Name: db_prefix_users db_prefix_users_username_unique; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.db_prefix_users + ADD CONSTRAINT db_prefix_users_username_unique UNIQUE (username); + + +-- +-- Name: db_prefix_access_tokens_type_index; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX db_prefix_access_tokens_type_index ON public.db_prefix_access_tokens USING btree (type); + + +-- +-- Name: db_prefix_discussions_comment_count_index; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX db_prefix_discussions_comment_count_index ON public.db_prefix_discussions USING btree (comment_count); + + +-- +-- Name: db_prefix_discussions_created_at_index; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX db_prefix_discussions_created_at_index ON public.db_prefix_discussions USING btree (created_at); + + +-- +-- Name: db_prefix_discussions_hidden_at_index; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX db_prefix_discussions_hidden_at_index ON public.db_prefix_discussions USING btree (hidden_at); + + +-- +-- Name: db_prefix_discussions_last_posted_at_index; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX db_prefix_discussions_last_posted_at_index ON public.db_prefix_discussions USING btree (last_posted_at); + + +-- +-- Name: db_prefix_discussions_last_posted_user_id_index; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX db_prefix_discussions_last_posted_user_id_index ON public.db_prefix_discussions USING btree (last_posted_user_id); + + +-- +-- Name: db_prefix_discussions_participant_count_index; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX db_prefix_discussions_participant_count_index ON public.db_prefix_discussions USING btree (participant_count); + + +-- +-- Name: db_prefix_discussions_title_fulltext; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX db_prefix_discussions_title_fulltext ON public.db_prefix_discussions USING gin (to_tsvector('english'::regconfig, (title)::text)); + + +-- +-- Name: db_prefix_discussions_user_id_index; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX db_prefix_discussions_user_id_index ON public.db_prefix_discussions USING btree (user_id); + + +-- +-- Name: db_prefix_notifications_user_id_index; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX db_prefix_notifications_user_id_index ON public.db_prefix_notifications USING btree (user_id); + + +-- +-- Name: db_prefix_posts_content_fulltext; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX db_prefix_posts_content_fulltext ON public.db_prefix_posts USING gin (to_tsvector('english'::regconfig, content)); + + +-- +-- Name: db_prefix_posts_discussion_id_created_at_index; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX db_prefix_posts_discussion_id_created_at_index ON public.db_prefix_posts USING btree (discussion_id, created_at); + + +-- +-- Name: db_prefix_posts_discussion_id_number_index; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX db_prefix_posts_discussion_id_number_index ON public.db_prefix_posts USING btree (discussion_id, number); + + +-- +-- Name: db_prefix_posts_type_created_at_index; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX db_prefix_posts_type_created_at_index ON public.db_prefix_posts USING btree (type, created_at); + + +-- +-- Name: db_prefix_posts_type_index; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX db_prefix_posts_type_index ON public.db_prefix_posts USING btree (type); + + +-- +-- Name: db_prefix_posts_user_id_created_at_index; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX db_prefix_posts_user_id_created_at_index ON public.db_prefix_posts USING btree (user_id, created_at); + + +-- +-- Name: db_prefix_unsubscribe_tokens_email_type_index; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX db_prefix_unsubscribe_tokens_email_type_index ON public.db_prefix_unsubscribe_tokens USING btree (email_type); + + +-- +-- Name: db_prefix_unsubscribe_tokens_token_index; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX db_prefix_unsubscribe_tokens_token_index ON public.db_prefix_unsubscribe_tokens USING btree (token); + + +-- +-- Name: db_prefix_unsubscribe_tokens_user_id_email_type_index; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX db_prefix_unsubscribe_tokens_user_id_email_type_index ON public.db_prefix_unsubscribe_tokens USING btree (user_id, email_type); + + +-- +-- Name: db_prefix_unsubscribe_tokens_user_id_index; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX db_prefix_unsubscribe_tokens_user_id_index ON public.db_prefix_unsubscribe_tokens USING btree (user_id); + + +-- +-- Name: db_prefix_users_comment_count_index; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX db_prefix_users_comment_count_index ON public.db_prefix_users USING btree (comment_count); + + +-- +-- Name: db_prefix_users_discussion_count_index; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX db_prefix_users_discussion_count_index ON public.db_prefix_users USING btree (discussion_count); + + +-- +-- Name: db_prefix_users_joined_at_index; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX db_prefix_users_joined_at_index ON public.db_prefix_users USING btree (joined_at); + + +-- +-- Name: db_prefix_users_last_seen_at_index; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX db_prefix_users_last_seen_at_index ON public.db_prefix_users USING btree (last_seen_at); + + +-- +-- Name: db_prefix_access_tokens db_prefix_access_tokens_user_id_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.db_prefix_access_tokens + ADD CONSTRAINT db_prefix_access_tokens_user_id_foreign FOREIGN KEY (user_id) REFERENCES public.db_prefix_users(id) ON DELETE CASCADE; + + +-- +-- Name: db_prefix_api_keys db_prefix_api_keys_user_id_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.db_prefix_api_keys + ADD CONSTRAINT db_prefix_api_keys_user_id_foreign FOREIGN KEY (user_id) REFERENCES public.db_prefix_users(id) ON DELETE CASCADE; + + +-- +-- Name: db_prefix_discussion_user db_prefix_discussion_user_discussion_id_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.db_prefix_discussion_user + ADD CONSTRAINT db_prefix_discussion_user_discussion_id_foreign FOREIGN KEY (discussion_id) REFERENCES public.db_prefix_discussions(id) ON DELETE CASCADE; + + +-- +-- Name: db_prefix_discussion_user db_prefix_discussion_user_user_id_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.db_prefix_discussion_user + ADD CONSTRAINT db_prefix_discussion_user_user_id_foreign FOREIGN KEY (user_id) REFERENCES public.db_prefix_users(id) ON DELETE CASCADE; + + +-- +-- Name: db_prefix_discussions db_prefix_discussions_first_post_id_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.db_prefix_discussions + ADD CONSTRAINT db_prefix_discussions_first_post_id_foreign FOREIGN KEY (first_post_id) REFERENCES public.db_prefix_posts(id) ON DELETE SET NULL; + + +-- +-- Name: db_prefix_discussions db_prefix_discussions_hidden_user_id_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.db_prefix_discussions + ADD CONSTRAINT db_prefix_discussions_hidden_user_id_foreign FOREIGN KEY (hidden_user_id) REFERENCES public.db_prefix_users(id) ON DELETE SET NULL; + + +-- +-- Name: db_prefix_discussions db_prefix_discussions_last_post_id_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.db_prefix_discussions + ADD CONSTRAINT db_prefix_discussions_last_post_id_foreign FOREIGN KEY (last_post_id) REFERENCES public.db_prefix_posts(id) ON DELETE SET NULL; + + +-- +-- Name: db_prefix_discussions db_prefix_discussions_last_posted_user_id_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.db_prefix_discussions + ADD CONSTRAINT db_prefix_discussions_last_posted_user_id_foreign FOREIGN KEY (last_posted_user_id) REFERENCES public.db_prefix_users(id) ON DELETE SET NULL; + + +-- +-- Name: db_prefix_discussions db_prefix_discussions_user_id_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.db_prefix_discussions + ADD CONSTRAINT db_prefix_discussions_user_id_foreign FOREIGN KEY (user_id) REFERENCES public.db_prefix_users(id) ON DELETE SET NULL; + + +-- +-- Name: db_prefix_email_tokens db_prefix_email_tokens_user_id_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.db_prefix_email_tokens + ADD CONSTRAINT db_prefix_email_tokens_user_id_foreign FOREIGN KEY (user_id) REFERENCES public.db_prefix_users(id) ON DELETE CASCADE; + + +-- +-- Name: db_prefix_group_permission db_prefix_group_permission_group_id_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.db_prefix_group_permission + ADD CONSTRAINT db_prefix_group_permission_group_id_foreign FOREIGN KEY (group_id) REFERENCES public.db_prefix_groups(id) ON DELETE CASCADE; + + +-- +-- Name: db_prefix_group_user db_prefix_group_user_group_id_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.db_prefix_group_user + ADD CONSTRAINT db_prefix_group_user_group_id_foreign FOREIGN KEY (group_id) REFERENCES public.db_prefix_groups(id) ON DELETE CASCADE; + + +-- +-- Name: db_prefix_group_user db_prefix_group_user_user_id_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.db_prefix_group_user + ADD CONSTRAINT db_prefix_group_user_user_id_foreign FOREIGN KEY (user_id) REFERENCES public.db_prefix_users(id) ON DELETE CASCADE; + + +-- +-- Name: db_prefix_login_providers db_prefix_login_providers_user_id_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.db_prefix_login_providers + ADD CONSTRAINT db_prefix_login_providers_user_id_foreign FOREIGN KEY (user_id) REFERENCES public.db_prefix_users(id) ON DELETE CASCADE; + + +-- +-- Name: db_prefix_notifications db_prefix_notifications_from_user_id_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.db_prefix_notifications + ADD CONSTRAINT db_prefix_notifications_from_user_id_foreign FOREIGN KEY (from_user_id) REFERENCES public.db_prefix_users(id) ON DELETE SET NULL; + + +-- +-- Name: db_prefix_notifications db_prefix_notifications_user_id_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.db_prefix_notifications + ADD CONSTRAINT db_prefix_notifications_user_id_foreign FOREIGN KEY (user_id) REFERENCES public.db_prefix_users(id) ON DELETE CASCADE; + + +-- +-- Name: db_prefix_password_tokens db_prefix_password_tokens_user_id_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.db_prefix_password_tokens + ADD CONSTRAINT db_prefix_password_tokens_user_id_foreign FOREIGN KEY (user_id) REFERENCES public.db_prefix_users(id) ON DELETE CASCADE; + + +-- +-- Name: db_prefix_post_user db_prefix_post_user_post_id_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.db_prefix_post_user + ADD CONSTRAINT db_prefix_post_user_post_id_foreign FOREIGN KEY (post_id) REFERENCES public.db_prefix_posts(id) ON DELETE CASCADE; + + +-- +-- Name: db_prefix_post_user db_prefix_post_user_user_id_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.db_prefix_post_user + ADD CONSTRAINT db_prefix_post_user_user_id_foreign FOREIGN KEY (user_id) REFERENCES public.db_prefix_users(id) ON DELETE CASCADE; + + +-- +-- Name: db_prefix_posts db_prefix_posts_discussion_id_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.db_prefix_posts + ADD CONSTRAINT db_prefix_posts_discussion_id_foreign FOREIGN KEY (discussion_id) REFERENCES public.db_prefix_discussions(id) ON DELETE CASCADE; + + +-- +-- Name: db_prefix_posts db_prefix_posts_edited_user_id_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.db_prefix_posts + ADD CONSTRAINT db_prefix_posts_edited_user_id_foreign FOREIGN KEY (edited_user_id) REFERENCES public.db_prefix_users(id) ON DELETE SET NULL; + + +-- +-- Name: db_prefix_posts db_prefix_posts_hidden_user_id_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.db_prefix_posts + ADD CONSTRAINT db_prefix_posts_hidden_user_id_foreign FOREIGN KEY (hidden_user_id) REFERENCES public.db_prefix_users(id) ON DELETE SET NULL; + + +-- +-- Name: db_prefix_posts db_prefix_posts_user_id_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.db_prefix_posts + ADD CONSTRAINT db_prefix_posts_user_id_foreign FOREIGN KEY (user_id) REFERENCES public.db_prefix_users(id) ON DELETE SET NULL; + + +-- +-- Name: db_prefix_unsubscribe_tokens db_prefix_unsubscribe_tokens_user_id_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.db_prefix_unsubscribe_tokens + ADD CONSTRAINT db_prefix_unsubscribe_tokens_user_id_foreign FOREIGN KEY (user_id) REFERENCES public.db_prefix_users(id) ON DELETE CASCADE; + + +-- +-- PostgreSQL database dump complete +-- + +-- +-- PostgreSQL database dump +-- + +-- Dumped from database version 14.11 (Debian 14.11-1.pgdg120+2) +-- Dumped by pg_dump version 15.6 (Debian 15.6-0+deb12u1) + +SET statement_timeout = 0; +SET lock_timeout = 0; +SET idle_in_transaction_session_timeout = 0; +SET client_encoding = 'UTF8'; +SET standard_conforming_strings = on; +SELECT pg_catalog.set_config('search_path', '', false); +SET check_function_bodies = false; +SET xmloption = content; +SET client_min_messages = warning; +SET row_security = off; + +-- +-- Data for Name: db_prefix_migrations; Type: TABLE DATA; Schema: public; Owner: - +-- + +INSERT INTO public.db_prefix_migrations VALUES (1,'2015_02_24_000000_create_access_tokens_table',NULL); +INSERT INTO public.db_prefix_migrations VALUES (2,'2015_02_24_000000_create_api_keys_table',NULL); +INSERT INTO public.db_prefix_migrations VALUES (3,'2015_02_24_000000_create_config_table',NULL); +INSERT INTO public.db_prefix_migrations VALUES (4,'2015_02_24_000000_create_discussions_table',NULL); +INSERT INTO public.db_prefix_migrations VALUES (5,'2015_02_24_000000_create_email_tokens_table',NULL); +INSERT INTO public.db_prefix_migrations VALUES (6,'2015_02_24_000000_create_groups_table',NULL); +INSERT INTO public.db_prefix_migrations VALUES (7,'2015_02_24_000000_create_notifications_table',NULL); +INSERT INTO public.db_prefix_migrations VALUES (8,'2015_02_24_000000_create_password_tokens_table',NULL); +INSERT INTO public.db_prefix_migrations VALUES (9,'2015_02_24_000000_create_permissions_table',NULL); +INSERT INTO public.db_prefix_migrations VALUES (10,'2015_02_24_000000_create_posts_table',NULL); +INSERT INTO public.db_prefix_migrations VALUES (11,'2015_02_24_000000_create_users_discussions_table',NULL); +INSERT INTO public.db_prefix_migrations VALUES (12,'2015_02_24_000000_create_users_groups_table',NULL); +INSERT INTO public.db_prefix_migrations VALUES (13,'2015_02_24_000000_create_users_table',NULL); +INSERT INTO public.db_prefix_migrations VALUES (14,'2015_09_15_000000_create_auth_tokens_table',NULL); +INSERT INTO public.db_prefix_migrations VALUES (15,'2015_09_20_224327_add_hide_to_discussions',NULL); +INSERT INTO public.db_prefix_migrations VALUES (16,'2015_09_22_030432_rename_notification_read_time',NULL); +INSERT INTO public.db_prefix_migrations VALUES (17,'2015_10_07_130531_rename_config_to_settings',NULL); +INSERT INTO public.db_prefix_migrations VALUES (18,'2015_10_24_194000_add_ip_address_to_posts',NULL); +INSERT INTO public.db_prefix_migrations VALUES (19,'2015_12_05_042721_change_access_tokens_columns',NULL); +INSERT INTO public.db_prefix_migrations VALUES (20,'2015_12_17_194247_change_settings_value_column_to_text',NULL); +INSERT INTO public.db_prefix_migrations VALUES (21,'2016_02_04_095452_add_slug_to_discussions',NULL); +INSERT INTO public.db_prefix_migrations VALUES (22,'2017_04_07_114138_add_is_private_to_discussions',NULL); +INSERT INTO public.db_prefix_migrations VALUES (23,'2017_04_07_114138_add_is_private_to_posts',NULL); +INSERT INTO public.db_prefix_migrations VALUES (24,'2018_01_11_093900_change_access_tokens_columns',NULL); +INSERT INTO public.db_prefix_migrations VALUES (25,'2018_01_11_094000_change_access_tokens_add_foreign_keys',NULL); +INSERT INTO public.db_prefix_migrations VALUES (26,'2018_01_11_095000_change_api_keys_columns',NULL); +INSERT INTO public.db_prefix_migrations VALUES (27,'2018_01_11_101800_rename_auth_tokens_to_registration_tokens',NULL); +INSERT INTO public.db_prefix_migrations VALUES (28,'2018_01_11_102000_change_registration_tokens_rename_id_to_token',NULL); +INSERT INTO public.db_prefix_migrations VALUES (29,'2018_01_11_102100_change_registration_tokens_created_at_to_datetime',NULL); +INSERT INTO public.db_prefix_migrations VALUES (30,'2018_01_11_120604_change_posts_table_to_innodb',NULL); +INSERT INTO public.db_prefix_migrations VALUES (31,'2018_01_11_155200_change_discussions_rename_columns',NULL); +INSERT INTO public.db_prefix_migrations VALUES (32,'2018_01_11_155300_change_discussions_add_foreign_keys',NULL); +INSERT INTO public.db_prefix_migrations VALUES (33,'2018_01_15_071700_rename_users_discussions_to_discussion_user',NULL); +INSERT INTO public.db_prefix_migrations VALUES (34,'2018_01_15_071800_change_discussion_user_rename_columns',NULL); +INSERT INTO public.db_prefix_migrations VALUES (35,'2018_01_15_071900_change_discussion_user_add_foreign_keys',NULL); +INSERT INTO public.db_prefix_migrations VALUES (36,'2018_01_15_072600_change_email_tokens_rename_id_to_token',NULL); +INSERT INTO public.db_prefix_migrations VALUES (37,'2018_01_15_072700_change_email_tokens_add_foreign_keys',NULL); +INSERT INTO public.db_prefix_migrations VALUES (38,'2018_01_15_072800_change_email_tokens_created_at_to_datetime',NULL); +INSERT INTO public.db_prefix_migrations VALUES (39,'2018_01_18_130400_rename_permissions_to_group_permission',NULL); +INSERT INTO public.db_prefix_migrations VALUES (40,'2018_01_18_130500_change_group_permission_add_foreign_keys',NULL); +INSERT INTO public.db_prefix_migrations VALUES (41,'2018_01_18_130600_rename_users_groups_to_group_user',NULL); +INSERT INTO public.db_prefix_migrations VALUES (42,'2018_01_18_130700_change_group_user_add_foreign_keys',NULL); +INSERT INTO public.db_prefix_migrations VALUES (43,'2018_01_18_133000_change_notifications_columns',NULL); +INSERT INTO public.db_prefix_migrations VALUES (44,'2018_01_18_133100_change_notifications_add_foreign_keys',NULL); +INSERT INTO public.db_prefix_migrations VALUES (45,'2018_01_18_134400_change_password_tokens_rename_id_to_token',NULL); +INSERT INTO public.db_prefix_migrations VALUES (46,'2018_01_18_134500_change_password_tokens_add_foreign_keys',NULL); +INSERT INTO public.db_prefix_migrations VALUES (47,'2018_01_18_134600_change_password_tokens_created_at_to_datetime',NULL); +INSERT INTO public.db_prefix_migrations VALUES (48,'2018_01_18_135000_change_posts_rename_columns',NULL); +INSERT INTO public.db_prefix_migrations VALUES (49,'2018_01_18_135100_change_posts_add_foreign_keys',NULL); +INSERT INTO public.db_prefix_migrations VALUES (50,'2018_01_30_112238_add_fulltext_index_to_discussions_title',NULL); +INSERT INTO public.db_prefix_migrations VALUES (51,'2018_01_30_220100_create_post_user_table',NULL); +INSERT INTO public.db_prefix_migrations VALUES (52,'2018_01_30_222900_change_users_rename_columns',NULL); +INSERT INTO public.db_prefix_migrations VALUES (55,'2018_09_15_041340_add_users_indicies',NULL); +INSERT INTO public.db_prefix_migrations VALUES (56,'2018_09_15_041828_add_discussions_indicies',NULL); +INSERT INTO public.db_prefix_migrations VALUES (57,'2018_09_15_043337_add_notifications_indices',NULL); +INSERT INTO public.db_prefix_migrations VALUES (58,'2018_09_15_043621_add_posts_indices',NULL); +INSERT INTO public.db_prefix_migrations VALUES (59,'2018_09_22_004100_change_registration_tokens_columns',NULL); +INSERT INTO public.db_prefix_migrations VALUES (60,'2018_09_22_004200_create_login_providers_table',NULL); +INSERT INTO public.db_prefix_migrations VALUES (61,'2018_10_08_144700_add_shim_prefix_to_group_icons',NULL); +INSERT INTO public.db_prefix_migrations VALUES (62,'2019_10_12_195349_change_posts_add_discussion_foreign_key',NULL); +INSERT INTO public.db_prefix_migrations VALUES (63,'2020_03_19_134512_change_discussions_default_comment_count',NULL); +INSERT INTO public.db_prefix_migrations VALUES (64,'2020_04_21_130500_change_permission_groups_add_is_hidden',NULL); +INSERT INTO public.db_prefix_migrations VALUES (65,'2021_03_02_040000_change_access_tokens_add_type',NULL); +INSERT INTO public.db_prefix_migrations VALUES (66,'2021_03_02_040500_change_access_tokens_add_id',NULL); +INSERT INTO public.db_prefix_migrations VALUES (67,'2021_03_02_041000_change_access_tokens_add_title_ip_agent',NULL); +INSERT INTO public.db_prefix_migrations VALUES (68,'2021_04_18_040500_change_migrations_add_id_primary_key',NULL); +INSERT INTO public.db_prefix_migrations VALUES (69,'2021_04_18_145100_change_posts_content_column_to_mediumtext',NULL); +INSERT INTO public.db_prefix_migrations VALUES (70,'2021_05_10_000000_rename_permissions',NULL); +INSERT INTO public.db_prefix_migrations VALUES (71,'2022_05_20_000000_add_timestamps_to_groups_table',NULL); +INSERT INTO public.db_prefix_migrations VALUES (72,'2022_05_20_000001_add_created_at_to_group_user_table',NULL); +INSERT INTO public.db_prefix_migrations VALUES (73,'2022_05_20_000002_add_created_at_to_group_permission_table',NULL); +INSERT INTO public.db_prefix_migrations VALUES (74,'2022_07_14_000000_add_type_index_to_posts',NULL); +INSERT INTO public.db_prefix_migrations VALUES (75,'2022_07_14_000001_add_type_created_at_composite_index_to_posts',NULL); +INSERT INTO public.db_prefix_migrations VALUES (76,'2022_08_06_000000_change_access_tokens_last_activity_at_to_nullable',NULL); +INSERT INTO public.db_prefix_migrations VALUES (77,'2023_08_19_000000_create_unsubscribe_tokens_table',NULL); +INSERT INTO public.db_prefix_migrations VALUES (78,'2023_10_23_000000_drop_post_number_index_column_from_discussions_table',NULL); +INSERT INTO public.db_prefix_migrations VALUES (79,'2024_05_05_000000_add_sqlite_keys',NULL); +INSERT INTO public.db_prefix_migrations VALUES (80,'2024_05_05_000001_convert_preferences_to_json_in_users',NULL); + +-- +-- Name: db_prefix_migrations_id_seq; Type: SEQUENCE SET; Schema: public; Owner: - +-- + +SELECT pg_catalog.setval('public.db_prefix_migrations_id_seq', 80, true); + + +-- +-- PostgreSQL database dump complete +-- diff --git a/framework/core/src/Database/Migrator.php b/framework/core/src/Database/Migrator.php index cb381d30f5..2ecf66be08 100644 --- a/framework/core/src/Database/Migrator.php +++ b/framework/core/src/Database/Migrator.php @@ -249,9 +249,11 @@ public function installFromSchema(string $path, string $driver): bool $dump = file_get_contents($schemaPath); + $dumpWithoutComments = preg_replace('/^--.*$/m', '', $dump); + $this->connection->getSchemaBuilder()->disableForeignKeyConstraints(); - foreach (explode(';', $dump) as $statement) { + foreach (explode(';', $dumpWithoutComments) as $statement) { $statement = trim($statement); if (empty($statement) || str_starts_with($statement, '/*')) { @@ -266,6 +268,10 @@ public function installFromSchema(string $path, string $driver): bool $this->connection->statement($statement); } + if ($driver === 'pgsql') { + $this->connection->statement('SELECT pg_catalog.set_config(\'search_path\', \'public\', false)'); + } + $this->connection->getSchemaBuilder()->enableForeignKeyConstraints(); $runTime = number_format((microtime(true) - $startTime) * 1000, 2); diff --git a/framework/core/src/Install/Steps/EnableBundledExtensions.php b/framework/core/src/Install/Steps/EnableBundledExtensions.php index 6ce5ca2064..b39c7954d1 100644 --- a/framework/core/src/Install/Steps/EnableBundledExtensions.php +++ b/framework/core/src/Install/Steps/EnableBundledExtensions.php @@ -24,22 +24,7 @@ class EnableBundledExtensions implements Step { - public const EXTENSION_WHITELIST = [ - 'flarum-approval', - 'flarum-bbcode', - 'flarum-emoji', - 'flarum-lang-english', - 'flarum-flags', - 'flarum-likes', - 'flarum-lock', - 'flarum-markdown', - 'flarum-mentions', - 'flarum-statistics', - 'flarum-sticky', - 'flarum-subscriptions', - 'flarum-suspend', - 'flarum-tags', - ]; + public const EXTENSION_WHITELIST = []; /** * @var string[] From f78ac6b76c3bb969e72bb08b53f56aabfe9f9c17 Mon Sep 17 00:00:00 2001 From: Sami Mazouz Date: Sun, 5 May 2024 17:07:15 +0100 Subject: [PATCH 05/29] fix --- .github/workflows/REUSABLE_backend.yml | 15 ++++++++++++- .../Api/Controller/ListFlagsController.php | 6 ++++-- .../tests/integration/api/flags/ListTest.php | 4 ++-- .../api/flags/ListWithTagsTest.php | 4 ++-- .../src/Database/DatabaseServiceProvider.php | 21 +++++++++++++++---- 5 files changed, 39 insertions(+), 11 deletions(-) diff --git a/.github/workflows/REUSABLE_backend.yml b/.github/workflows/REUSABLE_backend.yml index 70adb15b8e..7380803e08 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", "sqlite:3"]' + default: '["mysql:5.7", "mysql:8.0.30", "mysql:8.1.0", "mariadb", "sqlite:3", "postgres:9.5"]' php_ini_values: description: PHP ini values @@ -98,6 +98,9 @@ jobs: - service: 'sqlite:3' db: SQLite driver: sqlite + - service: 'postgres:9.5' + db: PostgreSQL + driver: pgsql # Include Database prefix tests with only one PHP version. - php: ${{ fromJSON(inputs.php_versions)[0] }} @@ -130,6 +133,12 @@ jobs: driver: sqlite prefix: flarum_ prefixStr: (prefix) + - php: ${{ fromJSON(inputs.php_versions)[0] }} + service: 'postgres:9.5' + db: PostgreSQL + driver: pgsql + prefix: flarum_ + prefixStr: (prefix) # To reduce number of actions, we exclude some PHP versions from running with some DB versions. exclude: @@ -147,6 +156,10 @@ jobs: service: 'sqlite:3' - php: ${{ fromJSON(inputs.php_versions)[1] }} service: 'sqlite:3' + - php: ${{ fromJSON(inputs.php_versions)[0] }} + service: 'postgres:9.5' + - php: ${{ fromJSON(inputs.php_versions)[1] }} + service: 'postgres:9.5' services: mysql: diff --git a/extensions/flags/src/Api/Controller/ListFlagsController.php b/extensions/flags/src/Api/Controller/ListFlagsController.php index 8218c6fb62..21a64c34a4 100644 --- a/extensions/flags/src/Api/Controller/ListFlagsController.php +++ b/extensions/flags/src/Api/Controller/ListFlagsController.php @@ -56,8 +56,10 @@ protected function data(ServerRequestInterface $request, Document $document): it $flags = Flag::whereVisibleTo($actor) ->limit($limit + 1) ->offset($offset) - ->whenMySql(fn (Builder $query) => $query->groupBy('post_id')) - ->whenPgSql(fn (Builder $query) => $query->distinct('post_id')->orderBy('post_id')) + ->whenPgSql( + fn (Builder $query) => $query->distinct('post_id')->orderBy('post_id'), + else: fn (Builder $query) => $query->groupBy('post_id') + ) ->latest('flags.id') ->get(); diff --git a/extensions/flags/tests/integration/api/flags/ListTest.php b/extensions/flags/tests/integration/api/flags/ListTest.php index 7b99a1db92..c934f23d82 100644 --- a/extensions/flags/tests/integration/api/flags/ListTest.php +++ b/extensions/flags/tests/integration/api/flags/ListTest.php @@ -85,7 +85,7 @@ public function admin_can_see_one_flag_per_post() $data = json_decode($body, true)['data']; $ids = Arr::pluck($data, 'id'); - $this->assertEqualsCanonicalizing(['3', '4', '5'], $ids); + $this->assertCount(3, $data); } /** @@ -123,7 +123,7 @@ public function mod_can_see_one_flag_per_post() $data = json_decode($response->getBody()->getContents(), true)['data']; $ids = Arr::pluck($data, 'id'); - $this->assertEqualsCanonicalizing(['3', '4', '5'], $ids); + $this->assertCount(3, $data); } /** diff --git a/extensions/flags/tests/integration/api/flags/ListWithTagsTest.php b/extensions/flags/tests/integration/api/flags/ListWithTagsTest.php index dbf63967ab..f3de5bc50e 100644 --- a/extensions/flags/tests/integration/api/flags/ListWithTagsTest.php +++ b/extensions/flags/tests/integration/api/flags/ListWithTagsTest.php @@ -119,7 +119,7 @@ public function admin_can_see_one_flag_per_post() $data = json_decode($body, true)['data']; $ids = Arr::pluck($data, 'id'); - $this->assertEqualsCanonicalizing(['3', '4', '5', '6', '7', '8', '9'], $ids); + $this->assertCount(7, $data); } /** @@ -159,7 +159,7 @@ public function mod_can_see_one_flag_per_post() $ids = Arr::pluck($data, 'id'); // 7 is included, even though mods can't view discussions. // This is because the UI doesnt allow discussions.viewFlags without viewDiscussions. - $this->assertEqualsCanonicalizing(['3', '4', '5', '7', '8', '9'], $ids); + $this->assertCount(6, $data); } /** diff --git a/framework/core/src/Database/DatabaseServiceProvider.php b/framework/core/src/Database/DatabaseServiceProvider.php index ae40acd033..ce43aa623d 100644 --- a/framework/core/src/Database/DatabaseServiceProvider.php +++ b/framework/core/src/Database/DatabaseServiceProvider.php @@ -84,17 +84,30 @@ public function register(): void protected function registerBuilderMacros(): void { $drivers = [ - 'mysql' => 'whenMySql', - 'pgsql' => 'whenPgSql', - 'sqlite' => 'whenSqlite', + 'mysql' => 'MySql', + 'pgsql' => 'PgSql', + 'sqlite' => 'Sqlite', ]; foreach ([QueryBuilder::class, EloquentBuilder::class] as $builder) { foreach ($drivers as $driver => $macro) { - $builder::macro($macro, function ($callback) use ($driver) { + $builder::macro('when'.$macro, function ($callback, $else) use ($driver) { /** @var QueryBuilder|EloquentBuilder $this */ if ($this->getConnection()->getDriverName() === $driver) { $callback($this); + } else { + $else($this); + } + + return $this; + }); + + $builder::macro('unless'.$macro, function ($callback, $else) use ($driver) { + /** @var QueryBuilder|EloquentBuilder $this */ + if ($this->getConnection()->getDriverName() !== $driver) { + $callback($this); + } else { + $else($this); } return $this; From 34b0ee8b7e9d020b6963186a0a9bbd93d744574c Mon Sep 17 00:00:00 2001 From: Sami Mazouz Date: Sun, 5 May 2024 17:11:03 +0100 Subject: [PATCH 06/29] chore --- .github/workflows/REUSABLE_backend.yml | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/.github/workflows/REUSABLE_backend.yml b/.github/workflows/REUSABLE_backend.yml index 7380803e08..682a18caa2 100644 --- a/.github/workflows/REUSABLE_backend.yml +++ b/.github/workflows/REUSABLE_backend.yml @@ -99,7 +99,7 @@ jobs: db: SQLite driver: sqlite - service: 'postgres:9.5' - db: PostgreSQL + db: PostgreSQL 9.5 driver: pgsql # Include Database prefix tests with only one PHP version. @@ -135,7 +135,7 @@ jobs: prefixStr: (prefix) - php: ${{ fromJSON(inputs.php_versions)[0] }} service: 'postgres:9.5' - db: PostgreSQL + db: PostgreSQL 9.5 driver: pgsql prefix: flarum_ prefixStr: (prefix) @@ -163,9 +163,13 @@ jobs: services: mysql: - image: ${{ matrix.service != 'sqlite:3' && matrix.service || '' }} + image: ${{ matrix.driver == 'mysql' && matrix.service || '' }} ports: - 13306:3306 + postgres: + image: ${{ matrix.driver == 'pgsql' && matrix.service || '' }} + ports: + - 15432:5432 name: 'PHP ${{ matrix.php }} / ${{ matrix.db }} ${{ matrix.prefixStr }}' @@ -186,11 +190,17 @@ jobs: ini-values: ${{ matrix.php_ini_values }} - name: Create MySQL Database - if: ${{ matrix.service != 'sqlite:3' }} + if: ${{ matrix.driver == 'mysql' }} run: | sudo systemctl start mysql mysql -uroot -proot -e 'CREATE DATABASE flarum_test;' --port 13306 + - name: Create PostgreSQL Database + if: ${{ matrix.driver == 'pgsql' }} + run: | + sudo systemctl start postgres + psql -U postgres -c 'CREATE DATABASE flarum_test;' --port 15432 + - name: Install Composer dependencies run: composer install working-directory: ${{ inputs.backend_directory }} From a2d70bdf6cc1b9f647393ef915c282de2b87bace Mon Sep 17 00:00:00 2001 From: Sami Mazouz Date: Sun, 5 May 2024 17:22:02 +0100 Subject: [PATCH 07/29] chore --- .github/workflows/REUSABLE_backend.yml | 27 +++++++++++++------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/.github/workflows/REUSABLE_backend.yml b/.github/workflows/REUSABLE_backend.yml index 682a18caa2..745bb913bb 100644 --- a/.github/workflows/REUSABLE_backend.yml +++ b/.github/workflows/REUSABLE_backend.yml @@ -68,6 +68,9 @@ env: # `inputs.composer_directory` defaults to `inputs.backend_directory` FLARUM_TEST_TMP_DIR_LOCAL: tests/integration/tmp COMPOSER_AUTH: ${{ secrets.composer_auth }} + DB_DATABASE: flarum_test + DB_USERNAME: root + DB_PASSWORD: root jobs: test: @@ -164,10 +167,19 @@ jobs: services: mysql: image: ${{ matrix.driver == 'mysql' && matrix.service || '' }} + env: + MYSQL_DATABASE: ${{ env.DB_DATABASE }} + MYSQL_USER: ${{ env.DB_USERNAME }} + MYSQL_PASSWORD: ${{ env.DB_PASSWORD }} + MYSQL_ROOT_PASSWORD: ${{ env.DB_PASSWORD }} ports: - 13306:3306 postgres: image: ${{ matrix.driver == 'pgsql' && matrix.service || '' }} + env: + POSTGRES_DB: ${{ env.DB_DATABASE }} + POSTGRES_USER: ${{ env.DB_USERNAME }} + POSTGRES_PASSWORD: ${{ env.DB_PASSWORD }} ports: - 15432:5432 @@ -189,18 +201,6 @@ jobs: tools: phpunit, composer:v2 ini-values: ${{ matrix.php_ini_values }} - - name: Create MySQL Database - if: ${{ matrix.driver == 'mysql' }} - run: | - sudo systemctl start mysql - mysql -uroot -proot -e 'CREATE DATABASE flarum_test;' --port 13306 - - - name: Create PostgreSQL Database - if: ${{ matrix.driver == 'pgsql' }} - run: | - sudo systemctl start postgres - psql -U postgres -c 'CREATE DATABASE flarum_test;' --port 15432 - - name: Install Composer dependencies run: composer install working-directory: ${{ inputs.backend_directory }} @@ -223,8 +223,7 @@ jobs: fi working-directory: ${{ inputs.backend_directory }} env: - DB_PORT: 13306 - DB_PASSWORD: root + DB_PORT: ${{ matrix.driver == 'mysql' && 13306 || 15432 }} DB_PREFIX: ${{ matrix.prefix }} DB_DRIVER: ${{ matrix.driver }} COMPOSER_PROCESS_TIMEOUT: 600 From 28aad560c173819c463a7eba1882f2411aea2607 Mon Sep 17 00:00:00 2001 From: Sami Mazouz Date: Sun, 5 May 2024 17:23:47 +0100 Subject: [PATCH 08/29] chore --- .github/workflows/REUSABLE_backend.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/REUSABLE_backend.yml b/.github/workflows/REUSABLE_backend.yml index 745bb913bb..d8351c988a 100644 --- a/.github/workflows/REUSABLE_backend.yml +++ b/.github/workflows/REUSABLE_backend.yml @@ -182,6 +182,11 @@ jobs: POSTGRES_PASSWORD: ${{ env.DB_PASSWORD }} ports: - 15432:5432 + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 name: 'PHP ${{ matrix.php }} / ${{ matrix.db }} ${{ matrix.prefixStr }}' From a5fc68708cfcbf45703cc93a54a1ff0657c35f8e Mon Sep 17 00:00:00 2001 From: Sami Mazouz Date: Sun, 5 May 2024 17:28:56 +0100 Subject: [PATCH 09/29] chore --- .github/workflows/REUSABLE_backend.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/REUSABLE_backend.yml b/.github/workflows/REUSABLE_backend.yml index d8351c988a..4772b73efe 100644 --- a/.github/workflows/REUSABLE_backend.yml +++ b/.github/workflows/REUSABLE_backend.yml @@ -206,6 +206,12 @@ jobs: tools: phpunit, composer:v2 ini-values: ${{ matrix.php_ini_values }} + - name: Create MySQL Database + if: ${{ matrix.driver == 'mysql' }} + run: | + sudo systemctl start mysql + mysql -uroot -proot -e 'CREATE DATABASE flarum_test;' --port 13306 + - name: Install Composer dependencies run: composer install working-directory: ${{ inputs.backend_directory }} From a87d646c46d578561562aaa24a6997eceace8624 Mon Sep 17 00:00:00 2001 From: Sami Mazouz Date: Sun, 5 May 2024 17:40:31 +0100 Subject: [PATCH 10/29] chore --- .github/workflows/REUSABLE_backend.yml | 2 +- framework/core/src/Install/Steps/ConnectToDatabase.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/REUSABLE_backend.yml b/.github/workflows/REUSABLE_backend.yml index 4772b73efe..f5ae5e32f6 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", "sqlite:3", "postgres:9.5"]' + default: '["mysql:5.7", "mysql:8.0.30", "mysql:8.1.0", "mariadb", "sqlite:3", "postgres:10"]' php_ini_values: description: PHP ini values diff --git a/framework/core/src/Install/Steps/ConnectToDatabase.php b/framework/core/src/Install/Steps/ConnectToDatabase.php index e12e14a524..e389d62a88 100644 --- a/framework/core/src/Install/Steps/ConnectToDatabase.php +++ b/framework/core/src/Install/Steps/ConnectToDatabase.php @@ -105,8 +105,8 @@ private function sqlite(array $config): void $version = $pdo->query('SELECT sqlite_version()')->fetchColumn(); - if (version_compare($version, '3.8.8', '<')) { - throw new RangeException("SQLite version ($version) too low. You need at least SQLite 3.8.8"); + if (version_compare($version, '3.35.0', '<')) { + throw new RangeException("SQLite version ($version) too low. You need at least SQLite 3.35.0"); } ($this->store)( From 90918800774f6bc576385b356628cac648c32697 Mon Sep 17 00:00:00 2001 From: Sami Mazouz Date: Sun, 5 May 2024 17:41:54 +0100 Subject: [PATCH 11/29] chore --- .github/workflows/REUSABLE_backend.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/REUSABLE_backend.yml b/.github/workflows/REUSABLE_backend.yml index f5ae5e32f6..0f47a0b14d 100644 --- a/.github/workflows/REUSABLE_backend.yml +++ b/.github/workflows/REUSABLE_backend.yml @@ -101,8 +101,8 @@ jobs: - service: 'sqlite:3' db: SQLite driver: sqlite - - service: 'postgres:9.5' - db: PostgreSQL 9.5 + - service: 'postgres:10' + db: PostgreSQL 10 driver: pgsql # Include Database prefix tests with only one PHP version. @@ -137,8 +137,8 @@ jobs: prefix: flarum_ prefixStr: (prefix) - php: ${{ fromJSON(inputs.php_versions)[0] }} - service: 'postgres:9.5' - db: PostgreSQL 9.5 + service: 'postgres:10' + db: PostgreSQL 10 driver: pgsql prefix: flarum_ prefixStr: (prefix) @@ -160,9 +160,9 @@ jobs: - php: ${{ fromJSON(inputs.php_versions)[1] }} service: 'sqlite:3' - php: ${{ fromJSON(inputs.php_versions)[0] }} - service: 'postgres:9.5' + service: 'postgres:10' - php: ${{ fromJSON(inputs.php_versions)[1] }} - service: 'postgres:9.5' + service: 'postgres:10' services: mysql: From 70770d2499b771f557ae9f9b8595ba9816873857 Mon Sep 17 00:00:00 2001 From: Sami Mazouz Date: Sun, 5 May 2024 17:44:20 +0100 Subject: [PATCH 12/29] chore --- .github/workflows/REUSABLE_backend.yml | 12 ------------ framework/core/migrations/pgsql-install.dump | 2 -- 2 files changed, 14 deletions(-) diff --git a/.github/workflows/REUSABLE_backend.yml b/.github/workflows/REUSABLE_backend.yml index 0f47a0b14d..46e823c8e4 100644 --- a/.github/workflows/REUSABLE_backend.yml +++ b/.github/workflows/REUSABLE_backend.yml @@ -112,24 +112,12 @@ jobs: 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 diff --git a/framework/core/migrations/pgsql-install.dump b/framework/core/migrations/pgsql-install.dump index 96f9f15f75..440345a893 100644 --- a/framework/core/migrations/pgsql-install.dump +++ b/framework/core/migrations/pgsql-install.dump @@ -32,8 +32,6 @@ COMMENT ON SCHEMA public IS ''; SET default_tablespace = ''; -SET default_table_access_method = heap; - -- -- Name: db_prefix_access_tokens; Type: TABLE; Schema: public; Owner: - -- From 6568cbe8b562ef3c79921e82797000501c7ebe96 Mon Sep 17 00:00:00 2001 From: Sami Mazouz Date: Sun, 5 May 2024 19:11:15 +0100 Subject: [PATCH 13/29] fix --- .../migrations/2018_07_21_000000_seed_default_groups.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/framework/core/migrations/2018_07_21_000000_seed_default_groups.php b/framework/core/migrations/2018_07_21_000000_seed_default_groups.php index 1ba4e83e14..e8bbc71862 100644 --- a/framework/core/migrations/2018_07_21_000000_seed_default_groups.php +++ b/framework/core/migrations/2018_07_21_000000_seed_default_groups.php @@ -28,6 +28,11 @@ $db->table('groups')->insert(array_combine(['id', 'name_singular', 'name_plural', 'color', 'icon'], $group)); } + + // PgSQL doesn't auto-increment the sequence when inserting the IDs manually. + if ($db->getDriverName() === 'pgsql') { + $db->statement("SELECT setval('groups_id_seq', (SELECT MAX(id) FROM groups))"); + } }, 'down' => function (Builder $schema) { From 751bbd57f47a0fc350a7d406a4536f4d82875d34 Mon Sep 17 00:00:00 2001 From: Sami Mazouz Date: Sun, 5 May 2024 19:14:19 +0100 Subject: [PATCH 14/29] fix --- .../core/migrations/2018_07_21_000000_seed_default_groups.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/framework/core/migrations/2018_07_21_000000_seed_default_groups.php b/framework/core/migrations/2018_07_21_000000_seed_default_groups.php index e8bbc71862..b518eba4dc 100644 --- a/framework/core/migrations/2018_07_21_000000_seed_default_groups.php +++ b/framework/core/migrations/2018_07_21_000000_seed_default_groups.php @@ -31,7 +31,8 @@ // PgSQL doesn't auto-increment the sequence when inserting the IDs manually. if ($db->getDriverName() === 'pgsql') { - $db->statement("SELECT setval('groups_id_seq', (SELECT MAX(id) FROM groups))"); + $table = $db->getSchemaGrammar()->wrapTable('groups'); + $db->statement("SELECT setval('groups_id_seq', (SELECT MAX(id) FROM $table))"); } }, From 94ed77353555436beb8033f25c506b0e30dcf60f Mon Sep 17 00:00:00 2001 From: Sami Mazouz Date: Sun, 5 May 2024 19:14:50 +0100 Subject: [PATCH 15/29] fix --- .../core/migrations/2018_07_21_000000_seed_default_groups.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/framework/core/migrations/2018_07_21_000000_seed_default_groups.php b/framework/core/migrations/2018_07_21_000000_seed_default_groups.php index b518eba4dc..5574153faa 100644 --- a/framework/core/migrations/2018_07_21_000000_seed_default_groups.php +++ b/framework/core/migrations/2018_07_21_000000_seed_default_groups.php @@ -32,7 +32,8 @@ // PgSQL doesn't auto-increment the sequence when inserting the IDs manually. if ($db->getDriverName() === 'pgsql') { $table = $db->getSchemaGrammar()->wrapTable('groups'); - $db->statement("SELECT setval('groups_id_seq', (SELECT MAX(id) FROM $table))"); + $seq = $db->getSchemaGrammar()->wrapTable('groups_id_seq'); + $db->statement("SELECT setval('$seq', (SELECT MAX(id) FROM $table))"); } }, From 47d08db51b85141dcaba1980fb9767cc02107935 Mon Sep 17 00:00:00 2001 From: Sami Mazouz Date: Sun, 5 May 2024 19:35:05 +0100 Subject: [PATCH 16/29] fix --- .../tests/integration/api/GroupMentionsTest.php | 6 ++++-- .../tests/integration/api/PostMentionsTest.php | 12 ++++++++---- php-packages/testing/src/integration/TestCase.php | 13 +++++++++++++ 3 files changed, 25 insertions(+), 6 deletions(-) diff --git a/extensions/mentions/tests/integration/api/GroupMentionsTest.php b/extensions/mentions/tests/integration/api/GroupMentionsTest.php index 02f882fe13..4c1669edc2 100644 --- a/extensions/mentions/tests/integration/api/GroupMentionsTest.php +++ b/extensions/mentions/tests/integration/api/GroupMentionsTest.php @@ -245,9 +245,11 @@ public function mentioning_a_virtual_group_as_an_admin_user_does_not_work() ]) ); - $this->assertEquals(201, $response->getStatusCode()); + $body = $response->getBody()->getContents(); - $response = json_decode($response->getBody(), true); + $this->assertEquals(201, $response->getStatusCode(), $body); + + $response = json_decode($body, true); $this->assertStringNotContainsString('@Members', $response['data']['attributes']['contentHtml']); $this->assertStringNotContainsString('@Guests', $response['data']['attributes']['contentHtml']); diff --git a/extensions/mentions/tests/integration/api/PostMentionsTest.php b/extensions/mentions/tests/integration/api/PostMentionsTest.php index 0ae3c1edf6..2874a7a7e1 100644 --- a/extensions/mentions/tests/integration/api/PostMentionsTest.php +++ b/extensions/mentions/tests/integration/api/PostMentionsTest.php @@ -94,9 +94,11 @@ public function mentioning_a_valid_post_with_old_format_doesnt_work() ]) ); - $this->assertEquals(201, $response->getStatusCode()); + $body = $response->getBody()->getContents(); - $response = json_decode($response->getBody(), true); + $this->assertEquals(201, $response->getStatusCode(), $body); + + $response = json_decode($body, true); $this->assertStringNotContainsString('POTATO$', $response['data']['attributes']['contentHtml']); $this->assertEquals('@potato#4', $response['data']['attributes']['content']); @@ -187,9 +189,11 @@ public function mentioning_a_valid_post_with_new_format_with_smart_quotes_works_ ]) ); - $this->assertEquals(201, $response->getStatusCode()); + $body = $response->getBody()->getContents(); - $response = json_decode($response->getBody(), true); + $this->assertEquals(201, $response->getStatusCode(), $body); + + $response = json_decode($body, true); $this->assertStringContainsString('POTATO$', $response['data']['attributes']['contentHtml']); $this->assertEquals('@"POTATO$"#p4', $response['data']['attributes']['content']); diff --git a/php-packages/testing/src/integration/TestCase.php b/php-packages/testing/src/integration/TestCase.php index dec3f09cb5..e62011a7a1 100644 --- a/php-packages/testing/src/integration/TestCase.php +++ b/php-packages/testing/src/integration/TestCase.php @@ -224,6 +224,8 @@ protected function populateDatabase(): void } } + $tables = []; + // Then, insert all rows required for this test case. foreach ($databaseContent as $table => $data) { foreach ($data['rows'] as $row) { @@ -238,10 +240,21 @@ protected function populateDatabase(): void } $this->database()->table($table)->updateOrInsert($unique, $row); + + if (isset($row['id'])) { + $tables[$table] = 'id'; + } } } if ($this->database()->getDriverName() === 'pgsql') { + // PgSQL doesn't auto-increment the sequence when inserting the IDs manually. + foreach ($tables as $table => $id) { + $wrappedTable = $this->database()->getSchemaGrammar()->wrapTable($table); + $seq = $this->database()->getSchemaGrammar()->wrapTable($table . '_' . $id . '_seq'); + $this->database()->statement("SELECT setval('$seq', (SELECT MAX($id) FROM $wrappedTable))"); + } + $this->database()->statement("SET session_replication_role = 'origin'"); } From e9287fc7a0c2c9a7a552c5c6ebb8447277823b9c Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Sun, 5 May 2024 18:35:26 +0000 Subject: [PATCH 17/29] Apply fixes from StyleCI --- php-packages/testing/src/integration/TestCase.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/php-packages/testing/src/integration/TestCase.php b/php-packages/testing/src/integration/TestCase.php index e62011a7a1..27f74b3087 100644 --- a/php-packages/testing/src/integration/TestCase.php +++ b/php-packages/testing/src/integration/TestCase.php @@ -251,7 +251,7 @@ protected function populateDatabase(): void // PgSQL doesn't auto-increment the sequence when inserting the IDs manually. foreach ($tables as $table => $id) { $wrappedTable = $this->database()->getSchemaGrammar()->wrapTable($table); - $seq = $this->database()->getSchemaGrammar()->wrapTable($table . '_' . $id . '_seq'); + $seq = $this->database()->getSchemaGrammar()->wrapTable($table.'_'.$id.'_seq'); $this->database()->statement("SELECT setval('$seq', (SELECT MAX($id) FROM $wrappedTable))"); } From 6042c2fb78faa91f627ffa5bfdae776b0c7d0c8b Mon Sep 17 00:00:00 2001 From: Sami Mazouz Date: Sun, 5 May 2024 21:01:37 +0100 Subject: [PATCH 18/29] test --- .../tests/integration/api/ListPostsTest.php | 46 +++++++++---------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/extensions/mentions/tests/integration/api/ListPostsTest.php b/extensions/mentions/tests/integration/api/ListPostsTest.php index c70c1a6387..17613fbd35 100644 --- a/extensions/mentions/tests/integration/api/ListPostsTest.php +++ b/extensions/mentions/tests/integration/api/ListPostsTest.php @@ -120,31 +120,31 @@ protected function prepareMentionedByData(): void ], Post::class => [ ['id' => 101, 'discussion_id' => 100, 'created_at' => Carbon::now(), 'user_id' => 1, 'type' => 'comment', 'content' => '

text

'], - ['id' => 102, 'discussion_id' => 100, 'created_at' => Carbon::now(), 'user_id' => 1, 'type' => 'comment', 'content' => '

text

'], - ['id' => 103, 'discussion_id' => 100, 'created_at' => Carbon::now(), 'user_id' => 1, 'type' => 'comment', 'content' => '

text

', 'is_private' => 1], - ['id' => 104, 'discussion_id' => 100, 'created_at' => Carbon::now(), 'user_id' => 1, 'type' => 'comment', 'content' => '

text

'], - ['id' => 105, 'discussion_id' => 100, 'created_at' => Carbon::now(), 'user_id' => 1, 'type' => 'comment', 'content' => '

text

'], - ['id' => 106, 'discussion_id' => 100, 'created_at' => Carbon::now(), 'user_id' => 1, 'type' => 'comment', 'content' => '

text

'], - ['id' => 107, 'discussion_id' => 100, 'created_at' => Carbon::now(), 'user_id' => 1, 'type' => 'comment', 'content' => '

text

'], - ['id' => 108, 'discussion_id' => 100, 'created_at' => Carbon::now(), 'user_id' => 1, 'type' => 'comment', 'content' => '

text

'], - ['id' => 109, 'discussion_id' => 100, 'created_at' => Carbon::now(), 'user_id' => 1, 'type' => 'comment', 'content' => '

text

'], - ['id' => 110, 'discussion_id' => 100, 'created_at' => Carbon::now(), 'user_id' => 1, 'type' => 'comment', 'content' => '

text

'], - ['id' => 111, 'discussion_id' => 100, 'created_at' => Carbon::now(), 'user_id' => 1, 'type' => 'comment', 'content' => '

text

'], - ['id' => 112, 'discussion_id' => 100, 'created_at' => Carbon::now(), 'user_id' => 1, 'type' => 'comment', 'content' => '

text

'], + ['id' => 102, 'discussion_id' => 100, 'created_at' => Carbon::now()->addMinutes(2), 'user_id' => 1, 'type' => 'comment', 'content' => '

text

'], + ['id' => 103, 'discussion_id' => 100, 'created_at' => Carbon::now()->addMinutes(3), 'user_id' => 1, 'type' => 'comment', 'content' => '

text

', 'is_private' => 1], + ['id' => 104, 'discussion_id' => 100, 'created_at' => Carbon::now()->addMinutes(4), 'user_id' => 1, 'type' => 'comment', 'content' => '

text

'], + ['id' => 105, 'discussion_id' => 100, 'created_at' => Carbon::now()->addMinutes(5), 'user_id' => 1, 'type' => 'comment', 'content' => '

text

'], + ['id' => 106, 'discussion_id' => 100, 'created_at' => Carbon::now()->addMinutes(6), 'user_id' => 1, 'type' => 'comment', 'content' => '

text

'], + ['id' => 107, 'discussion_id' => 100, 'created_at' => Carbon::now()->addMinutes(7), 'user_id' => 1, 'type' => 'comment', 'content' => '

text

'], + ['id' => 108, 'discussion_id' => 100, 'created_at' => Carbon::now()->addMinutes(8), 'user_id' => 1, 'type' => 'comment', 'content' => '

text

'], + ['id' => 109, 'discussion_id' => 100, 'created_at' => Carbon::now()->addMinutes(9), 'user_id' => 1, 'type' => 'comment', 'content' => '

text

'], + ['id' => 110, 'discussion_id' => 100, 'created_at' => Carbon::now()->addMinutes(10), 'user_id' => 1, 'type' => 'comment', 'content' => '

text

'], + ['id' => 111, 'discussion_id' => 100, 'created_at' => Carbon::now()->addMinutes(11), 'user_id' => 1, 'type' => 'comment', 'content' => '

text

'], + ['id' => 112, 'discussion_id' => 100, 'created_at' => Carbon::now()->addMinutes(12), 'user_id' => 1, 'type' => 'comment', 'content' => '

text

'], ], 'post_mentions_post' => [ - ['post_id' => 102, 'mentions_post_id' => 101], - ['post_id' => 103, 'mentions_post_id' => 101], - ['post_id' => 104, 'mentions_post_id' => 101], - ['post_id' => 105, 'mentions_post_id' => 101], - ['post_id' => 106, 'mentions_post_id' => 101], - ['post_id' => 107, 'mentions_post_id' => 101], - ['post_id' => 108, 'mentions_post_id' => 101], - ['post_id' => 109, 'mentions_post_id' => 101], - ['post_id' => 110, 'mentions_post_id' => 101], - ['post_id' => 111, 'mentions_post_id' => 101], - ['post_id' => 112, 'mentions_post_id' => 101], - ['post_id' => 103, 'mentions_post_id' => 112], + ['post_id' => 102, 'mentions_post_id' => 101, 'created_at' => Carbon::now()->addMinutes(2)], + ['post_id' => 103, 'mentions_post_id' => 101, 'created_at' => Carbon::now()->addMinutes(3)], + ['post_id' => 104, 'mentions_post_id' => 101, 'created_at' => Carbon::now()->addMinutes(4)], + ['post_id' => 105, 'mentions_post_id' => 101, 'created_at' => Carbon::now()->addMinutes(5)], + ['post_id' => 106, 'mentions_post_id' => 101, 'created_at' => Carbon::now()->addMinutes(6)], + ['post_id' => 107, 'mentions_post_id' => 101, 'created_at' => Carbon::now()->addMinutes(7)], + ['post_id' => 108, 'mentions_post_id' => 101, 'created_at' => Carbon::now()->addMinutes(8)], + ['post_id' => 109, 'mentions_post_id' => 101, 'created_at' => Carbon::now()->addMinutes(9)], + ['post_id' => 110, 'mentions_post_id' => 101, 'created_at' => Carbon::now()->addMinutes(10)], + ['post_id' => 111, 'mentions_post_id' => 101, 'created_at' => Carbon::now()->addMinutes(11)], + ['post_id' => 112, 'mentions_post_id' => 101, 'created_at' => Carbon::now()->addMinutes(12)], + ['post_id' => 103, 'mentions_post_id' => 112, 'created_at' => Carbon::now()->addMinutes(13)], ], ]); } From a3cd2ffd9c6a3c817aebda7a7189ce96bd5362d1 Mon Sep 17 00:00:00 2001 From: Sami Mazouz Date: Sun, 5 May 2024 21:18:44 +0100 Subject: [PATCH 19/29] test --- .../tests/integration/api/ListPostsTest.php | 50 +++++++++---------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/extensions/mentions/tests/integration/api/ListPostsTest.php b/extensions/mentions/tests/integration/api/ListPostsTest.php index 17613fbd35..a1a1d6385b 100644 --- a/extensions/mentions/tests/integration/api/ListPostsTest.php +++ b/extensions/mentions/tests/integration/api/ListPostsTest.php @@ -116,35 +116,35 @@ protected function prepareMentionedByData(): void { $this->prepareDatabase([ Discussion::class => [ - ['id' => 100, 'title' => __CLASS__, 'created_at' => Carbon::now(), 'user_id' => 1, 'first_post_id' => 101, 'comment_count' => 12], + ['id' => 100, 'title' => __CLASS__, 'created_at' => Carbon::parse('2024-05-04'), 'user_id' => 1, 'first_post_id' => 101, 'comment_count' => 12], ], Post::class => [ - ['id' => 101, 'discussion_id' => 100, 'created_at' => Carbon::now(), 'user_id' => 1, 'type' => 'comment', 'content' => '

text

'], - ['id' => 102, 'discussion_id' => 100, 'created_at' => Carbon::now()->addMinutes(2), 'user_id' => 1, 'type' => 'comment', 'content' => '

text

'], - ['id' => 103, 'discussion_id' => 100, 'created_at' => Carbon::now()->addMinutes(3), 'user_id' => 1, 'type' => 'comment', 'content' => '

text

', 'is_private' => 1], - ['id' => 104, 'discussion_id' => 100, 'created_at' => Carbon::now()->addMinutes(4), 'user_id' => 1, 'type' => 'comment', 'content' => '

text

'], - ['id' => 105, 'discussion_id' => 100, 'created_at' => Carbon::now()->addMinutes(5), 'user_id' => 1, 'type' => 'comment', 'content' => '

text

'], - ['id' => 106, 'discussion_id' => 100, 'created_at' => Carbon::now()->addMinutes(6), 'user_id' => 1, 'type' => 'comment', 'content' => '

text

'], - ['id' => 107, 'discussion_id' => 100, 'created_at' => Carbon::now()->addMinutes(7), 'user_id' => 1, 'type' => 'comment', 'content' => '

text

'], - ['id' => 108, 'discussion_id' => 100, 'created_at' => Carbon::now()->addMinutes(8), 'user_id' => 1, 'type' => 'comment', 'content' => '

text

'], - ['id' => 109, 'discussion_id' => 100, 'created_at' => Carbon::now()->addMinutes(9), 'user_id' => 1, 'type' => 'comment', 'content' => '

text

'], - ['id' => 110, 'discussion_id' => 100, 'created_at' => Carbon::now()->addMinutes(10), 'user_id' => 1, 'type' => 'comment', 'content' => '

text

'], - ['id' => 111, 'discussion_id' => 100, 'created_at' => Carbon::now()->addMinutes(11), 'user_id' => 1, 'type' => 'comment', 'content' => '

text

'], - ['id' => 112, 'discussion_id' => 100, 'created_at' => Carbon::now()->addMinutes(12), 'user_id' => 1, 'type' => 'comment', 'content' => '

text

'], + ['id' => 101, 'discussion_id' => 100, 'created_at' => Carbon::parse('2024-05-04'), 'user_id' => 1, 'type' => 'comment', 'content' => '

text

'], + ['id' => 102, 'discussion_id' => 100, 'created_at' => Carbon::parse('2024-05-04')->addMinutes(2), 'user_id' => 1, 'type' => 'comment', 'content' => '

text

'], + ['id' => 103, 'discussion_id' => 100, 'created_at' => Carbon::parse('2024-05-04')->addMinutes(3), 'user_id' => 1, 'type' => 'comment', 'content' => '

text

', 'is_private' => 1], + ['id' => 104, 'discussion_id' => 100, 'created_at' => Carbon::parse('2024-05-04')->addMinutes(4), 'user_id' => 1, 'type' => 'comment', 'content' => '

text

'], + ['id' => 105, 'discussion_id' => 100, 'created_at' => Carbon::parse('2024-05-04')->addMinutes(5), 'user_id' => 1, 'type' => 'comment', 'content' => '

text

'], + ['id' => 106, 'discussion_id' => 100, 'created_at' => Carbon::parse('2024-05-04')->addMinutes(6), 'user_id' => 1, 'type' => 'comment', 'content' => '

text

'], + ['id' => 107, 'discussion_id' => 100, 'created_at' => Carbon::parse('2024-05-04')->addMinutes(7), 'user_id' => 1, 'type' => 'comment', 'content' => '

text

'], + ['id' => 108, 'discussion_id' => 100, 'created_at' => Carbon::parse('2024-05-04')->addMinutes(8), 'user_id' => 1, 'type' => 'comment', 'content' => '

text

'], + ['id' => 109, 'discussion_id' => 100, 'created_at' => Carbon::parse('2024-05-04')->addMinutes(9), 'user_id' => 1, 'type' => 'comment', 'content' => '

text

'], + ['id' => 110, 'discussion_id' => 100, 'created_at' => Carbon::parse('2024-05-04')->addMinutes(10), 'user_id' => 1, 'type' => 'comment', 'content' => '

text

'], + ['id' => 111, 'discussion_id' => 100, 'created_at' => Carbon::parse('2024-05-04')->addMinutes(11), 'user_id' => 1, 'type' => 'comment', 'content' => '

text

'], + ['id' => 112, 'discussion_id' => 100, 'created_at' => Carbon::parse('2024-05-04')->addMinutes(12), 'user_id' => 1, 'type' => 'comment', 'content' => '

text

'], ], 'post_mentions_post' => [ - ['post_id' => 102, 'mentions_post_id' => 101, 'created_at' => Carbon::now()->addMinutes(2)], - ['post_id' => 103, 'mentions_post_id' => 101, 'created_at' => Carbon::now()->addMinutes(3)], - ['post_id' => 104, 'mentions_post_id' => 101, 'created_at' => Carbon::now()->addMinutes(4)], - ['post_id' => 105, 'mentions_post_id' => 101, 'created_at' => Carbon::now()->addMinutes(5)], - ['post_id' => 106, 'mentions_post_id' => 101, 'created_at' => Carbon::now()->addMinutes(6)], - ['post_id' => 107, 'mentions_post_id' => 101, 'created_at' => Carbon::now()->addMinutes(7)], - ['post_id' => 108, 'mentions_post_id' => 101, 'created_at' => Carbon::now()->addMinutes(8)], - ['post_id' => 109, 'mentions_post_id' => 101, 'created_at' => Carbon::now()->addMinutes(9)], - ['post_id' => 110, 'mentions_post_id' => 101, 'created_at' => Carbon::now()->addMinutes(10)], - ['post_id' => 111, 'mentions_post_id' => 101, 'created_at' => Carbon::now()->addMinutes(11)], - ['post_id' => 112, 'mentions_post_id' => 101, 'created_at' => Carbon::now()->addMinutes(12)], - ['post_id' => 103, 'mentions_post_id' => 112, 'created_at' => Carbon::now()->addMinutes(13)], + ['post_id' => 102, 'mentions_post_id' => 101, 'created_at' => Carbon::parse('2024-05-04')->addMinutes(2)], + ['post_id' => 103, 'mentions_post_id' => 101, 'created_at' => Carbon::parse('2024-05-04')->addMinutes(3)], + ['post_id' => 104, 'mentions_post_id' => 101, 'created_at' => Carbon::parse('2024-05-04')->addMinutes(4)], + ['post_id' => 105, 'mentions_post_id' => 101, 'created_at' => Carbon::parse('2024-05-04')->addMinutes(5)], + ['post_id' => 106, 'mentions_post_id' => 101, 'created_at' => Carbon::parse('2024-05-04')->addMinutes(6)], + ['post_id' => 107, 'mentions_post_id' => 101, 'created_at' => Carbon::parse('2024-05-04')->addMinutes(7)], + ['post_id' => 108, 'mentions_post_id' => 101, 'created_at' => Carbon::parse('2024-05-04')->addMinutes(8)], + ['post_id' => 109, 'mentions_post_id' => 101, 'created_at' => Carbon::parse('2024-05-04')->addMinutes(9)], + ['post_id' => 110, 'mentions_post_id' => 101, 'created_at' => Carbon::parse('2024-05-04')->addMinutes(10)], + ['post_id' => 111, 'mentions_post_id' => 101, 'created_at' => Carbon::parse('2024-05-04')->addMinutes(11)], + ['post_id' => 112, 'mentions_post_id' => 101, 'created_at' => Carbon::parse('2024-05-04')->addMinutes(12)], + ['post_id' => 103, 'mentions_post_id' => 112, 'created_at' => Carbon::parse('2024-05-04')->addMinutes(13)], ], ]); } From 03a9cbe6fb6d697051ebc5d1f21e3e8f1d535971 Mon Sep 17 00:00:00 2001 From: Sami Mazouz Date: Mon, 6 May 2024 20:47:04 +0100 Subject: [PATCH 20/29] test --- extensions/mentions/tests/integration/api/ListPostsTest.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/extensions/mentions/tests/integration/api/ListPostsTest.php b/extensions/mentions/tests/integration/api/ListPostsTest.php index a1a1d6385b..655b03d9e8 100644 --- a/extensions/mentions/tests/integration/api/ListPostsTest.php +++ b/extensions/mentions/tests/integration/api/ListPostsTest.php @@ -187,17 +187,18 @@ public function mentioned_by_relation_returns_limited_results_and_shows_only_vis ])->withQueryParams([ 'filter' => ['discussion' => 100], 'include' => 'mentionedBy', + 'sort' => 'createdAt', ]) ); - $data = json_decode($response->getBody()->getContents(), true)['data']; + $data = json_decode($body = $response->getBody()->getContents(), true)['data']; $this->assertEquals(200, $response->getStatusCode()); $mentionedBy = $data[0]['relationships']['mentionedBy']['data']; // Only displays a limited amount of mentioned by posts - $this->assertCount(LoadMentionedByRelationship::$maxMentionedBy, $mentionedBy); + $this->assertCount(LoadMentionedByRelationship::$maxMentionedBy, $mentionedBy, $body); // Of the limited amount of mentioned by posts, they must be visible to the actor $this->assertEquals([102, 104, 105, 106], Arr::pluck($mentionedBy, 'id')); } From c5fcbcce9224b0c6ee5822bc50bf5d0ea429e6cc Mon Sep 17 00:00:00 2001 From: Sami Mazouz Date: Tue, 7 May 2024 21:54:24 +0100 Subject: [PATCH 21/29] fix --- .../api/discussions/ReplyNotificationTest.php | 18 ++++----- ..._convert_data_to_json_in_notifications.php | 37 +++++++++++++++++++ framework/core/migrations/pgsql-install.dump | 5 ++- framework/core/src/Discussion/Discussion.php | 2 +- .../core/src/Notification/Notification.php | 12 +++++- 5 files changed, 61 insertions(+), 13 deletions(-) create mode 100644 framework/core/migrations/2024_05_07_000001_convert_data_to_json_in_notifications.php diff --git a/extensions/subscriptions/tests/integration/api/discussions/ReplyNotificationTest.php b/extensions/subscriptions/tests/integration/api/discussions/ReplyNotificationTest.php index 540bcfb982..6555115eb0 100644 --- a/extensions/subscriptions/tests/integration/api/discussions/ReplyNotificationTest.php +++ b/extensions/subscriptions/tests/integration/api/discussions/ReplyNotificationTest.php @@ -41,15 +41,15 @@ protected function setUp(): void ['id' => 33, 'title' => __CLASS__, 'created_at' => Carbon::now(), 'last_posted_at' => Carbon::now(), 'user_id' => 1, 'first_post_id' => 33, 'comment_count' => 6, 'last_post_number' => 6, 'last_post_id' => 38], ], Post::class => [ - ['id' => 1, 'discussion_id' => 1, 'created_at' => Carbon::createFromDate(1975, 5, 21)->toDateTimeString(), 'user_id' => 1, 'type' => 'comment', 'content' => '

foo bar

', 'number' => 1], - ['id' => 2, 'discussion_id' => 2, 'created_at' => Carbon::createFromDate(1975, 5, 21)->toDateTimeString(), 'user_id' => 1, 'type' => 'comment', 'content' => '

foo bar

', 'number' => 1], - - ['id' => 33, 'discussion_id' => 33, 'created_at' => Carbon::createFromDate(1975, 5, 21)->toDateTimeString(), 'user_id' => 1, 'type' => 'comment', 'content' => '

foo bar

', 'number' => 1], - ['id' => 34, 'discussion_id' => 33, 'created_at' => Carbon::createFromDate(1975, 5, 21)->toDateTimeString(), 'user_id' => 1, 'type' => 'comment', 'content' => '

foo bar

', 'number' => 2], - ['id' => 35, 'discussion_id' => 33, 'created_at' => Carbon::createFromDate(1975, 5, 21)->toDateTimeString(), 'user_id' => 1, 'type' => 'comment', 'content' => '

foo bar

', 'number' => 3], - ['id' => 36, 'discussion_id' => 33, 'created_at' => Carbon::createFromDate(1975, 5, 21)->toDateTimeString(), 'user_id' => 1, 'type' => 'comment', 'content' => '

foo bar

', 'number' => 4], - ['id' => 37, 'discussion_id' => 33, 'created_at' => Carbon::createFromDate(1975, 5, 21)->toDateTimeString(), 'user_id' => 1, 'type' => 'comment', 'content' => '

foo bar

', 'number' => 5], - ['id' => 38, 'discussion_id' => 33, 'created_at' => Carbon::createFromDate(1975, 5, 21)->toDateTimeString(), 'user_id' => 1, 'type' => 'comment', 'content' => '

foo bar

', 'number' => 6], + ['id' => 1, 'discussion_id' => 1, 'created_at' => Carbon::createFromDate(1975, 5, 21)->addMinutes(1)->toDateTimeString(), 'user_id' => 1, 'type' => 'comment', 'content' => '

foo bar

', 'number' => 1], + ['id' => 2, 'discussion_id' => 2, 'created_at' => Carbon::createFromDate(1975, 5, 21)->addMinutes(2)->toDateTimeString(), 'user_id' => 1, 'type' => 'comment', 'content' => '

foo bar

', 'number' => 1], + + ['id' => 33, 'discussion_id' => 33, 'created_at' => Carbon::createFromDate(1975, 5, 21)->addMinutes(3)->toDateTimeString(), 'user_id' => 1, 'type' => 'comment', 'content' => '

foo bar

', 'number' => 1], + ['id' => 34, 'discussion_id' => 33, 'created_at' => Carbon::createFromDate(1975, 5, 21)->addMinutes(4)->toDateTimeString(), 'user_id' => 1, 'type' => 'comment', 'content' => '

foo bar

', 'number' => 2], + ['id' => 35, 'discussion_id' => 33, 'created_at' => Carbon::createFromDate(1975, 5, 21)->addMinutes(5)->toDateTimeString(), 'user_id' => 1, 'type' => 'comment', 'content' => '

foo bar

', 'number' => 3], + ['id' => 36, 'discussion_id' => 33, 'created_at' => Carbon::createFromDate(1975, 5, 21)->addMinutes(6)->toDateTimeString(), 'user_id' => 1, 'type' => 'comment', 'content' => '

foo bar

', 'number' => 4], + ['id' => 37, 'discussion_id' => 33, 'created_at' => Carbon::createFromDate(1975, 5, 21)->addMinutes(7)->toDateTimeString(), 'user_id' => 1, 'type' => 'comment', 'content' => '

foo bar

', 'number' => 5], + ['id' => 38, 'discussion_id' => 33, 'created_at' => Carbon::createFromDate(1975, 5, 21)->addMinutes(8)->toDateTimeString(), 'user_id' => 1, 'type' => 'comment', 'content' => '

foo bar

', 'number' => 6], ], 'discussion_user' => [ ['discussion_id' => 1, 'user_id' => 1, 'last_read_post_number' => 1, 'subscription' => 'follow'], diff --git a/framework/core/migrations/2024_05_07_000001_convert_data_to_json_in_notifications.php b/framework/core/migrations/2024_05_07_000001_convert_data_to_json_in_notifications.php new file mode 100644 index 0000000000..e915775ee6 --- /dev/null +++ b/framework/core/migrations/2024_05_07_000001_convert_data_to_json_in_notifications.php @@ -0,0 +1,37 @@ + function (Builder $schema) { + if ($schema->getConnection()->getDriverName() !== 'pgsql') { + $schema->table('notifications', function (Blueprint $table) { + $table->json('data')->nullable()->change(); + }); + } else { + $notifications = $schema->getConnection()->getSchemaGrammar()->wrapTable('notifications'); + $data = $schema->getConnection()->getSchemaGrammar()->wrap('data'); + $schema->getConnection()->statement("ALTER TABLE $notifications ALTER COLUMN $data TYPE JSON USING data::TEXT::JSON"); + } + }, + + 'down' => function (Builder $schema) { + if ($schema->getConnection()->getDriverName() !== 'pgsql') { + $schema->table('notifications', function (Blueprint $table) { + $table->binary('data')->nullable()->change(); + }); + } else { + $notifications = $schema->getConnection()->getSchemaGrammar()->wrapTable('notifications'); + $data = $schema->getConnection()->getSchemaGrammar()->wrap('data'); + $schema->getConnection()->statement("ALTER TABLE $notifications ALTER COLUMN $data TYPE BYTEA USING data::TEXT::BYTEA"); + } + } +]; diff --git a/framework/core/migrations/pgsql-install.dump b/framework/core/migrations/pgsql-install.dump index 440345a893..760c0b82dd 100644 --- a/framework/core/migrations/pgsql-install.dump +++ b/framework/core/migrations/pgsql-install.dump @@ -304,7 +304,7 @@ CREATE TABLE public.db_prefix_notifications ( from_user_id integer, type character varying(100) NOT NULL, subject_id integer, - data bytea, + data json, created_at timestamp(0) without time zone NOT NULL, is_deleted boolean DEFAULT false NOT NULL, read_at timestamp(0) without time zone @@ -1233,12 +1233,13 @@ INSERT INTO public.db_prefix_migrations VALUES (77,'2023_08_19_000000_create_uns INSERT INTO public.db_prefix_migrations VALUES (78,'2023_10_23_000000_drop_post_number_index_column_from_discussions_table',NULL); INSERT INTO public.db_prefix_migrations VALUES (79,'2024_05_05_000000_add_sqlite_keys',NULL); INSERT INTO public.db_prefix_migrations VALUES (80,'2024_05_05_000001_convert_preferences_to_json_in_users',NULL); +INSERT INTO public.db_prefix_migrations VALUES (81,'2024_05_07_000001_convert_data_to_json_in_notifications.php',NULL); -- -- Name: db_prefix_migrations_id_seq; Type: SEQUENCE SET; Schema: public; Owner: - -- -SELECT pg_catalog.setval('public.db_prefix_migrations_id_seq', 80, true); +SELECT pg_catalog.setval('public.db_prefix_migrations_id_seq', 81, true); -- diff --git a/framework/core/src/Discussion/Discussion.php b/framework/core/src/Discussion/Discussion.php index 924f9d6346..b0825a8da1 100644 --- a/framework/core/src/Discussion/Discussion.php +++ b/framework/core/src/Discussion/Discussion.php @@ -199,7 +199,7 @@ public function setLastPost(Post $post): static public function refreshLastPost(): static { - if ($lastPost = $this->comments()->latest()->first()) { + if ($lastPost = $this->comments()->latest()->latest('id')->first()) { /** @var Post $lastPost */ $this->setLastPost($lastPost); } diff --git a/framework/core/src/Notification/Notification.php b/framework/core/src/Notification/Notification.php index 7f9621b167..cdae9397ba 100644 --- a/framework/core/src/Notification/Notification.php +++ b/framework/core/src/Notification/Notification.php @@ -159,7 +159,17 @@ public function scopeWhereSubjectModel(Builder $query, string $class): Builder */ public function scopeMatchingBlueprint(Builder $query, BlueprintInterface $blueprint): Builder { - return $query->where(static::getBlueprintAttributes($blueprint)); + $attributes = static::getBlueprintAttributes($blueprint); + + $data = $attributes['data']; + unset($attributes['data']); + + return $query->where($attributes) + ->whenPgSql(function ($query) use ($data) { + return $query->whereRaw('data::text = ?', [$data]); + }, function ($query) use ($data) { + return $query->where('data', $data); + }); } /** From ad24a6fc208b628a037eb76f1c72e73cea99ca46 Mon Sep 17 00:00:00 2001 From: Sami Mazouz Date: Thu, 9 May 2024 12:49:39 +0100 Subject: [PATCH 22/29] fix --- framework/core/src/Install/Steps/ConnectToDatabase.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/framework/core/src/Install/Steps/ConnectToDatabase.php b/framework/core/src/Install/Steps/ConnectToDatabase.php index e389d62a88..4b21848f68 100644 --- a/framework/core/src/Install/Steps/ConnectToDatabase.php +++ b/framework/core/src/Install/Steps/ConnectToDatabase.php @@ -56,7 +56,7 @@ private function mysql(array $config): void if (Str::contains($version, 'MariaDB')) { if (version_compare($version, '10.10.0', '<')) { - throw new RangeException("MariaDB version ($version) too low. You need at least MariaDB 10.0.5"); + throw new RangeException("MariaDB version ($version) too low. You need at least MariaDB 10.10"); } } else { if (version_compare($version, '5.7.0', '<')) { From 50a23ff81a5de285497a515eafb1a1fad6717fa5 Mon Sep 17 00:00:00 2001 From: Sami Mazouz Date: Thu, 9 May 2024 13:07:38 +0100 Subject: [PATCH 23/29] feat: query exception errors db driver hint --- framework/core/js/src/common/Application.tsx | 6 ++++- framework/core/locale/core.yml | 1 + .../QueryExceptionHandler.php | 26 +++++++++++++++++++ .../Foundation/ErrorHandling/HandledError.php | 5 ++-- .../src/Foundation/ErrorServiceProvider.php | 2 ++ 5 files changed, 37 insertions(+), 3 deletions(-) create mode 100644 framework/core/src/Foundation/ErrorHandling/ExceptionHandler/QueryExceptionHandler.php diff --git a/framework/core/js/src/common/Application.tsx b/framework/core/js/src/common/Application.tsx index 2e362e9699..1389583b4c 100644 --- a/framework/core/js/src/common/Application.tsx +++ b/framework/core/js/src/common/Application.tsx @@ -559,7 +559,11 @@ export default class Application { break; default: - if (this.requestWasCrossOrigin(error)) { + const code = error.response?.errors?.[0]?.code; + + if (code === 'db_error' && app.session.user?.isAdmin()) { + content = app.translator.trans('core.lib.error.db_error_message'); + } else if (this.requestWasCrossOrigin(error)) { content = app.translator.trans('core.lib.error.generic_cross_origin_message'); } else { content = app.translator.trans('core.lib.error.generic_message'); diff --git a/framework/core/locale/core.yml b/framework/core/locale/core.yml index 7f540a16f0..1848f3807a 100644 --- a/framework/core/locale/core.yml +++ b/framework/core/locale/core.yml @@ -698,6 +698,7 @@ core: # These translations are displayed as error messages. error: circular_dependencies_message: "Circular dependencies detected: {extensions}. Aborting. Please disable one of the extensions and try again." + db_error_message: "An error occurred while communicating with the database. This could mean an incompatibility with your database driver." dependent_extensions_message: "Cannot disable {extension} until the following dependent extensions are disabled: {extensions}" extension_initialiation_failed_message: "{extension} failed to initialize, check the browser console for further information." generic_message: "Oops! Something went wrong. Please reload the page and try again." diff --git a/framework/core/src/Foundation/ErrorHandling/ExceptionHandler/QueryExceptionHandler.php b/framework/core/src/Foundation/ErrorHandling/ExceptionHandler/QueryExceptionHandler.php new file mode 100644 index 0000000000..340711044d --- /dev/null +++ b/framework/core/src/Foundation/ErrorHandling/ExceptionHandler/QueryExceptionHandler.php @@ -0,0 +1,26 @@ +withDetails([]); + } +} diff --git a/framework/core/src/Foundation/ErrorHandling/HandledError.php b/framework/core/src/Foundation/ErrorHandling/HandledError.php index 2c55cfb708..5b7fa73492 100644 --- a/framework/core/src/Foundation/ErrorHandling/HandledError.php +++ b/framework/core/src/Foundation/ErrorHandling/HandledError.php @@ -30,7 +30,8 @@ public static function unknown(Throwable $error): static public function __construct( private readonly Throwable $error, private readonly string $type, - private readonly int $statusCode + private readonly int $statusCode, + private bool $report = false ) { } @@ -58,7 +59,7 @@ public function getStatusCode(): int public function shouldBeReported(): bool { - return $this->type === 'unknown'; + return $this->type === 'unknown' || $this->report; } public function getDetails(): array diff --git a/framework/core/src/Foundation/ErrorServiceProvider.php b/framework/core/src/Foundation/ErrorServiceProvider.php index f272e8b807..c6708b7aea 100644 --- a/framework/core/src/Foundation/ErrorServiceProvider.php +++ b/framework/core/src/Foundation/ErrorServiceProvider.php @@ -12,6 +12,7 @@ use Flarum\Extension\Exception as ExtensionException; use Flarum\Foundation\ErrorHandling as Handling; use Illuminate\Database\Eloquent\ModelNotFoundException; +use Illuminate\Database\QueryException; use Illuminate\Validation\ValidationException as IlluminateValidationException; use Tobscure\JsonApi\Exception\InvalidParameterException; @@ -64,6 +65,7 @@ public function register(): void ExtensionException\CircularDependenciesException::class => ExtensionException\CircularDependenciesExceptionHandler::class, ExtensionException\DependentExtensionsException::class => ExtensionException\DependentExtensionsExceptionHandler::class, ExtensionException\MissingDependenciesException::class => ExtensionException\MissingDependenciesExceptionHandler::class, + QueryException::class => Handling\ExceptionHandler\QueryExceptionHandler::class, ]; }); From abc545f5a80db908bc7c697cbfa6727214850b4c Mon Sep 17 00:00:00 2001 From: Sami Mazouz Date: Thu, 6 Jun 2024 16:29:39 +0100 Subject: [PATCH 24/29] feat: allow defining supported databases --- .../core/js/src/admin/AdminApplication.tsx | 1 + .../js/src/admin/components/ExtensionPage.tsx | 21 +++++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/framework/core/js/src/admin/AdminApplication.tsx b/framework/core/js/src/admin/AdminApplication.tsx index 474013041d..978f92e158 100644 --- a/framework/core/js/src/admin/AdminApplication.tsx +++ b/framework/core/js/src/admin/AdminApplication.tsx @@ -32,6 +32,7 @@ export type Extension = { extra: { 'flarum-extension': { title: string; + 'database-support': undefined | string[]; }; }; require?: Record; diff --git a/framework/core/js/src/admin/components/ExtensionPage.tsx b/framework/core/js/src/admin/components/ExtensionPage.tsx index 9626b8cb20..8d1a42fa4d 100644 --- a/framework/core/js/src/admin/components/ExtensionPage.tsx +++ b/framework/core/js/src/admin/components/ExtensionPage.tsx @@ -225,6 +225,27 @@ export default class ExtensionPage { + return ( + { + mysql: 'MySQL', + sqlite: 'SQLite', + pgsql: 'PostgreSQL', + }[database] || database + ); + }); + + items.add( + 'database-support', + + + {supportedDatabases.join(', ')} + + ); + } + const extension = this.extension; items.add( 'readme', From 942d5c357b120b9c7ccaf1ce7c6aba7d47e85378 Mon Sep 17 00:00:00 2001 From: Sami Mazouz Date: Fri, 21 Jun 2024 08:37:40 +0100 Subject: [PATCH 25/29] chore: review comments --- .../Api/Controller/ListFlagsController.php | 2 +- .../tests/integration/api/flags/ListTest.php | 8 +++++++- .../src/Api/Controller/ShowStatisticsData.php | 4 +++- .../src/PinStickiedDiscussionsToTop.php | 5 ++++- .../core/js/src/admin/AdminApplication.tsx | 7 +++++++ .../js/src/admin/components/ExtensionPage.tsx | 15 ++++++++++++-- framework/core/locale/core.yml | 1 + ...11_093900_change_access_tokens_columns.php | 14 ++++++------- ...1_convert_preferences_to_json_in_users.php | 20 +++++++++---------- ..._convert_data_to_json_in_notifications.php | 20 +++++++++---------- .../Install/Steps/EnableBundledExtensions.php | 19 ++++++++++++++++-- 11 files changed, 80 insertions(+), 35 deletions(-) diff --git a/extensions/flags/src/Api/Controller/ListFlagsController.php b/extensions/flags/src/Api/Controller/ListFlagsController.php index 21a64c34a4..50931e29f9 100644 --- a/extensions/flags/src/Api/Controller/ListFlagsController.php +++ b/extensions/flags/src/Api/Controller/ListFlagsController.php @@ -60,7 +60,7 @@ protected function data(ServerRequestInterface $request, Document $document): it fn (Builder $query) => $query->distinct('post_id')->orderBy('post_id'), else: fn (Builder $query) => $query->groupBy('post_id') ) - ->latest('flags.id') + ->latest('flags.created_at') ->get(); $this->loadRelations($flags, $include, $request); diff --git a/extensions/flags/tests/integration/api/flags/ListTest.php b/extensions/flags/tests/integration/api/flags/ListTest.php index c934f23d82..92f36e1627 100644 --- a/extensions/flags/tests/integration/api/flags/ListTest.php +++ b/extensions/flags/tests/integration/api/flags/ListTest.php @@ -17,6 +17,7 @@ use Flarum\Testing\integration\RetrievesAuthorizedUsers; use Flarum\Testing\integration\TestCase; use Flarum\User\User; +use Illuminate\Database\PostgresConnection; use Illuminate\Support\Arr; class ListTest extends TestCase @@ -85,7 +86,12 @@ public function admin_can_see_one_flag_per_post() $data = json_decode($body, true)['data']; $ids = Arr::pluck($data, 'id'); - $this->assertCount(3, $data); + + if ($this->database() instanceof PostgresConnection) { + $this->assertEqualsCanonicalizing(['3', '4', '5'], $ids); + } else { + $this->assertEqualsCanonicalizing(['1', '4', '5'], $ids); + } } /** diff --git a/extensions/statistics/src/Api/Controller/ShowStatisticsData.php b/extensions/statistics/src/Api/Controller/ShowStatisticsData.php index 285bfcb5c7..82dc3cad4d 100644 --- a/extensions/statistics/src/Api/Controller/ShowStatisticsData.php +++ b/extensions/statistics/src/Api/Controller/ShowStatisticsData.php @@ -11,6 +11,7 @@ use Carbon\Carbon; use DateTime; +use Exception; use Flarum\Discussion\Discussion; use Flarum\Http\RequestUtil; use Flarum\Post\Post; @@ -141,7 +142,8 @@ private function getTimedCounts(Builder $query, string $column, ?DateTime $start $dbFormattedDatetime = match ($query->getConnection()->getDriverName()) { 'sqlite' => "strftime($format, $column)", 'pgsql' => "TO_CHAR($column, $format)", - default => "DATE_FORMAT($column, $format)", + 'mysql' => "DATE_FORMAT($column, $format)", + default => throw new Exception('Unsupported database driver'), }; $results = $query diff --git a/extensions/sticky/src/PinStickiedDiscussionsToTop.php b/extensions/sticky/src/PinStickiedDiscussionsToTop.php index c0dd58bde8..dcb53e3550 100755 --- a/extensions/sticky/src/PinStickiedDiscussionsToTop.php +++ b/extensions/sticky/src/PinStickiedDiscussionsToTop.php @@ -9,6 +9,7 @@ namespace Flarum\Sticky; +use DateTime; use Flarum\Search\Database\DatabaseSearchState; use Flarum\Search\SearchCriteria; use Flarum\Tags\Search\Filter\TagFilter; @@ -46,6 +47,8 @@ public function __invoke(DatabaseSearchState $state, SearchCriteria $criteria): $sticky->where('is_sticky', true); unset($sticky->orders); + $epochTime = (new DateTime('@0'))->format('Y-m-d H:i:s'); + /** @var Builder $q */ foreach ([$sticky, $query] as $q) { $read = $q->newQuery() @@ -58,7 +61,7 @@ public function __invoke(DatabaseSearchState $state, SearchCriteria $criteria): // 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 ?: '1970-01-01 00:00:00'])); + $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 ?: $epochTime])); } $query->union($sticky); diff --git a/framework/core/js/src/admin/AdminApplication.tsx b/framework/core/js/src/admin/AdminApplication.tsx index 978f92e158..512390aabe 100644 --- a/framework/core/js/src/admin/AdminApplication.tsx +++ b/framework/core/js/src/admin/AdminApplication.tsx @@ -49,6 +49,13 @@ export interface AdminApplicationData extends ApplicationData { maintenanceByConfig: boolean; safeModeExtensions?: string[] | null; safeModeExtensionsConfig?: string[] | null; + + dbDriver: string; + dbVersion: string; + phpVersion: string; + queueDriver: string; + schedulerStatus: string; + sessionDriver: string; } export default class AdminApplication extends Application { diff --git a/framework/core/js/src/admin/components/ExtensionPage.tsx b/framework/core/js/src/admin/components/ExtensionPage.tsx index 8d1a42fa4d..abf6166581 100644 --- a/framework/core/js/src/admin/components/ExtensionPage.tsx +++ b/framework/core/js/src/admin/components/ExtensionPage.tsx @@ -20,6 +20,7 @@ import Form from '../../common/components/Form'; import Icon from '../../common/components/Icon'; import { MaintenanceMode } from '../../common/Application'; import InfoTile from '../../common/components/InfoTile'; +import Alert from '../../common/components/Alert'; export interface ExtensionPageAttrs extends IPageAttrs { id: string; @@ -79,8 +80,19 @@ export default class ExtensionPage) { + const supportsDbDriver = + !this.extension.extra['flarum-extension']['database-support'] || + this.extension.extra['flarum-extension']['database-support'].map((driver) => driver.toLowerCase()).includes(app.data.dbDriver.toLowerCase()); + return this.isEnabled() ? ( -
{this.sections(vnode).toArray()}
+
+ {!supportsDbDriver && ( + + {app.translator.trans('core.admin.extension.database_driver_mismatch')} + + )} + {this.sections(vnode).toArray()} +
) : (

{app.translator.trans('core.admin.extension.enable_to_see')}

@@ -187,7 +199,6 @@ export default class ExtensionPage diff --git a/framework/core/locale/core.yml b/framework/core/locale/core.yml index 1848f3807a..459a2e1976 100644 --- a/framework/core/locale/core.yml +++ b/framework/core/locale/core.yml @@ -211,6 +211,7 @@ core: extension: configure_scopes: Configure Scopes confirm_purge: Purging will remove all database entries and assets related to the extension. It will not uninstall the extension; that must be done via Composer. Are you sure you want to continue? + database_driver_mismatch: This extension does not support your configured database driver. disabled: Disabled enable_to_see: Enable the extension to view and change settings. enabled: Enabled 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 a60269e4ad..09b392e0c9 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 @@ -26,13 +26,7 @@ $table->integer('user_id')->unsigned()->change(); }); - if ($schema->getConnection()->getDriverName() !== 'pgsql') { - // Use a separate schema instance because this column gets renamed - // in the previous one. - $schema->table('access_tokens', function (Blueprint $table) { - $table->dateTime('last_activity_at')->change(); - }); - } else { + if ($schema->getConnection()->getDriverName() === 'pgsql') { $prefix = $schema->getConnection()->getTablePrefix(); // Changing an integer col to datetime is an unusual operation in PostgreSQL. @@ -41,6 +35,12 @@ ALTER COLUMN last_activity_at TYPE TIMESTAMP(0) WITHOUT TIME ZONE USING to_timestamp(last_activity_at) SQL); + } else { + // Use a separate schema instance because this column gets renamed + // in the previous one. + $schema->table('access_tokens', function (Blueprint $table) { + $table->dateTime('last_activity_at')->change(); + }); } }, diff --git a/framework/core/migrations/2024_05_05_000001_convert_preferences_to_json_in_users.php b/framework/core/migrations/2024_05_05_000001_convert_preferences_to_json_in_users.php index e63ede3b42..1129e4f03c 100644 --- a/framework/core/migrations/2024_05_05_000001_convert_preferences_to_json_in_users.php +++ b/framework/core/migrations/2024_05_05_000001_convert_preferences_to_json_in_users.php @@ -12,26 +12,26 @@ return [ 'up' => function (Builder $schema) { - if ($schema->getConnection()->getDriverName() !== 'pgsql') { - $schema->table('users', function (Blueprint $table) { - $table->json('preferences')->nullable()->change(); - }); - } else { + if ($schema->getConnection()->getDriverName() === 'pgsql') { $users = $schema->getConnection()->getSchemaGrammar()->wrapTable('users'); $preferences = $schema->getConnection()->getSchemaGrammar()->wrap('preferences'); $schema->getConnection()->statement("ALTER TABLE $users ALTER COLUMN $preferences TYPE JSON USING preferences::TEXT::JSON"); + } else { + $schema->table('users', function (Blueprint $table) { + $table->json('preferences')->nullable()->change(); + }); } }, 'down' => function (Builder $schema) { - if ($schema->getConnection()->getDriverName() !== 'pgsql') { - $schema->table('users', function (Blueprint $table) { - $table->binary('preferences')->nullable()->change(); - }); - } else { + if ($schema->getConnection()->getDriverName() === 'pgsql') { $users = $schema->getConnection()->getSchemaGrammar()->wrapTable('users'); $preferences = $schema->getConnection()->getSchemaGrammar()->wrap('preferences'); $schema->getConnection()->statement("ALTER TABLE $users ALTER COLUMN $preferences TYPE BYTEA USING preferences::TEXT::BYTEA"); + } else { + $schema->table('users', function (Blueprint $table) { + $table->binary('preferences')->nullable()->change(); + }); } } ]; diff --git a/framework/core/migrations/2024_05_07_000001_convert_data_to_json_in_notifications.php b/framework/core/migrations/2024_05_07_000001_convert_data_to_json_in_notifications.php index e915775ee6..c433d0c087 100644 --- a/framework/core/migrations/2024_05_07_000001_convert_data_to_json_in_notifications.php +++ b/framework/core/migrations/2024_05_07_000001_convert_data_to_json_in_notifications.php @@ -12,26 +12,26 @@ return [ 'up' => function (Builder $schema) { - if ($schema->getConnection()->getDriverName() !== 'pgsql') { - $schema->table('notifications', function (Blueprint $table) { - $table->json('data')->nullable()->change(); - }); - } else { + if ($schema->getConnection()->getDriverName() === 'pgsql') { $notifications = $schema->getConnection()->getSchemaGrammar()->wrapTable('notifications'); $data = $schema->getConnection()->getSchemaGrammar()->wrap('data'); $schema->getConnection()->statement("ALTER TABLE $notifications ALTER COLUMN $data TYPE JSON USING data::TEXT::JSON"); + } else { + $schema->table('notifications', function (Blueprint $table) { + $table->json('data')->nullable()->change(); + }); } }, 'down' => function (Builder $schema) { - if ($schema->getConnection()->getDriverName() !== 'pgsql') { - $schema->table('notifications', function (Blueprint $table) { - $table->binary('data')->nullable()->change(); - }); - } else { + if ($schema->getConnection()->getDriverName() === 'pgsql') { $notifications = $schema->getConnection()->getSchemaGrammar()->wrapTable('notifications'); $data = $schema->getConnection()->getSchemaGrammar()->wrap('data'); $schema->getConnection()->statement("ALTER TABLE $notifications ALTER COLUMN $data TYPE BYTEA USING data::TEXT::BYTEA"); + } else { + $schema->table('notifications', function (Blueprint $table) { + $table->binary('data')->nullable()->change(); + }); } } ]; diff --git a/framework/core/src/Install/Steps/EnableBundledExtensions.php b/framework/core/src/Install/Steps/EnableBundledExtensions.php index b39c7954d1..2029347b79 100644 --- a/framework/core/src/Install/Steps/EnableBundledExtensions.php +++ b/framework/core/src/Install/Steps/EnableBundledExtensions.php @@ -24,7 +24,22 @@ class EnableBundledExtensions implements Step { - public const EXTENSION_WHITELIST = []; + public const DEFAULT_ENABLED_EXTENSIONS = [ + 'flarum-approval', + 'flarum-bbcode', + 'flarum-emoji', + 'flarum-lang-english', + 'flarum-flags', + 'flarum-likes', + 'flarum-lock', + 'flarum-markdown', + 'flarum-mentions', + 'flarum-statistics', + 'flarum-sticky', + 'flarum-subscriptions', + 'flarum-suspend', + 'flarum-tags', + ]; /** * @var string[] @@ -39,7 +54,7 @@ public function __construct( private readonly string $assetPath, ?array $enabledExtensions = null ) { - $this->enabledExtensions = $enabledExtensions ?? self::EXTENSION_WHITELIST; + $this->enabledExtensions = $enabledExtensions ?? self::DEFAULT_ENABLED_EXTENSIONS; } public function getMessage(): string From 10ebd4d768eb34a4daab19162d4a1f9193144d3e Mon Sep 17 00:00:00 2001 From: Sami Mazouz Date: Fri, 21 Jun 2024 08:38:59 +0100 Subject: [PATCH 26/29] chore: review comments --- framework/core/locale/core.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/framework/core/locale/core.yml b/framework/core/locale/core.yml index 459a2e1976..4b30f1e678 100644 --- a/framework/core/locale/core.yml +++ b/framework/core/locale/core.yml @@ -699,7 +699,7 @@ core: # These translations are displayed as error messages. error: circular_dependencies_message: "Circular dependencies detected: {extensions}. Aborting. Please disable one of the extensions and try again." - db_error_message: "An error occurred while communicating with the database. This could mean an incompatibility with your database driver." + db_error_message: "Database query failed. This may be caused by an incompatibility between an extension and your database driver." dependent_extensions_message: "Cannot disable {extension} until the following dependent extensions are disabled: {extensions}" extension_initialiation_failed_message: "{extension} failed to initialize, check the browser console for further information." generic_message: "Oops! Something went wrong. Please reload the page and try again." From ff96395941ea36e40672ff484bd79f8b8bf12a06 Mon Sep 17 00:00:00 2001 From: Sami Mazouz Date: Fri, 21 Jun 2024 09:28:52 +0100 Subject: [PATCH 27/29] feat: setting for pgsql preferred search config --- .../core/js/src/admin/AdminApplication.tsx | 9 ++++++++- .../js/src/admin/components/AdvancedPage.tsx | 20 +++++++++++++++++++ framework/core/locale/core.yml | 2 ++ .../core/src/Admin/Content/AdminPayload.php | 1 + .../src/Discussion/Search/FulltextFilter.php | 16 +++++++++++---- .../Foundation/ApplicationInfoProvider.php | 14 +++++++++++++ .../src/Settings/SettingsServiceProvider.php | 1 + 7 files changed, 58 insertions(+), 5 deletions(-) diff --git a/framework/core/js/src/admin/AdminApplication.tsx b/framework/core/js/src/admin/AdminApplication.tsx index 512390aabe..77c0f10e26 100644 --- a/framework/core/js/src/admin/AdminApplication.tsx +++ b/framework/core/js/src/admin/AdminApplication.tsx @@ -38,6 +38,12 @@ export type Extension = { require?: Record; }; +export enum DatabaseDriver { + MySQL = 'MySQL', + PostgreSQL = 'PostgreSQL', + SQLite = 'SQLite', +} + export interface AdminApplicationData extends ApplicationData { extensions: Record; settings: Record; @@ -50,8 +56,9 @@ export interface AdminApplicationData extends ApplicationData { safeModeExtensions?: string[] | null; safeModeExtensionsConfig?: string[] | null; - dbDriver: string; + dbDriver: DatabaseDriver; dbVersion: string; + dbOptions: Record; phpVersion: string; queueDriver: string; schedulerStatus: string; diff --git a/framework/core/js/src/admin/components/AdvancedPage.tsx b/framework/core/js/src/admin/components/AdvancedPage.tsx index feca76e015..47d3eeed08 100644 --- a/framework/core/js/src/admin/components/AdvancedPage.tsx +++ b/framework/core/js/src/admin/components/AdvancedPage.tsx @@ -11,6 +11,7 @@ import { MaintenanceMode } from '../../common/Application'; import Button from '../../common/components/Button'; import classList from '../../common/utils/classList'; import ExtensionBisect from './ExtensionBisect'; +import { DatabaseDriver } from '../AdminApplication'; export default class AdvancedPage extends AdminPage { searchDriverOptions: Record> = {}; @@ -68,6 +69,10 @@ export default class AdvancedPage e items.add('maintenance', this.maintenance(), 90); + if (app.data.dbDriver === DatabaseDriver.PostgreSQL) { + items.add(DatabaseDriver.PostgreSQL, this.pgsqlSettings(), 80); + } + return items; } @@ -187,4 +192,19 @@ export default class AdvancedPage e ); } + + pgsqlSettings() { + return ( + +
+ {this.buildSettingComponent({ + type: 'select', + setting: 'pgsql_search_configuration', + options: app.data.dbOptions.search_configurations, + label: app.translator.trans('core.admin.advanced.pgsql.search_configuration'), + })} +
+
+ ); + } } diff --git a/framework/core/locale/core.yml b/framework/core/locale/core.yml index 4b30f1e678..48c35db0c9 100644 --- a/framework/core/locale/core.yml +++ b/framework/core/locale/core.yml @@ -45,6 +45,8 @@ core: safe_mode_extensions: Extensions allowed to boot during safe mode safe_mode_extensions_override_help: "This setting is overridden by the safe_mode_extensions key in your config.php file. ({extensions})" section_label: Maintenance + pgsql: + search_configuration: Search configuration to use search: section_label: Search Drivers driver_heading: "Search Driver: {model}" diff --git a/framework/core/src/Admin/Content/AdminPayload.php b/framework/core/src/Admin/Content/AdminPayload.php index dea5431a16..134340a552 100644 --- a/framework/core/src/Admin/Content/AdminPayload.php +++ b/framework/core/src/Admin/Content/AdminPayload.php @@ -62,6 +62,7 @@ public function __invoke(Document $document, Request $request): void $document->payload['phpVersion'] = $this->appInfo->identifyPHPVersion(); $document->payload['dbDriver'] = $this->appInfo->identifyDatabaseDriver(); $document->payload['dbVersion'] = $this->appInfo->identifyDatabaseVersion(); + $document->payload['dbOptions'] = $this->appInfo->identifyDatabaseOptions(); $document->payload['debugEnabled'] = Arr::get($this->config, 'debug'); if ($this->appInfo->scheduledTasksRegistered()) { diff --git a/framework/core/src/Discussion/Search/FulltextFilter.php b/framework/core/src/Discussion/Search/FulltextFilter.php index 4bb6f9e338..46c3662699 100644 --- a/framework/core/src/Discussion/Search/FulltextFilter.php +++ b/framework/core/src/Discussion/Search/FulltextFilter.php @@ -14,6 +14,7 @@ use Flarum\Search\AbstractFulltextFilter; use Flarum\Search\Database\DatabaseSearchState; use Flarum\Search\SearchState; +use Flarum\Settings\SettingsRepositoryInterface; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Query\Builder as QueryBuilder; use Illuminate\Database\Query\Expression; @@ -24,6 +25,11 @@ */ class FulltextFilter extends AbstractFulltextFilter { + public function __construct( + protected SettingsRepositoryInterface $settings + ) { + } + public function search(SearchState $state, string $value): void { match ($state->getQuery()->getConnection()->getDriverName()) { @@ -111,15 +117,17 @@ protected function mysql(SearchState $state, string $value): void protected function pgsql(SearchState $state, string $value): void { + $searchConfig = $this->settings->get('pgsql_search_configuration'); + /** @var Builder $query */ $query = $state->getQuery(); $grammar = $query->getGrammar(); - $matchCondition = "to_tsvector('english', ".$grammar->wrap('posts.content').") @@ plainto_tsquery('english', ?)"; - $matchScore = "ts_rank(to_tsvector('english', ".$grammar->wrap('posts.content')."), plainto_tsquery('english', ?))"; - $matchTitleCondition = "to_tsvector('english', ".$grammar->wrap('discussions.title').") @@ plainto_tsquery('english', ?)"; - $matchTitleScore = "ts_rank(to_tsvector('english', ".$grammar->wrap('discussions.title')."), plainto_tsquery('english', ?))"; + $matchCondition = "to_tsvector('$searchConfig', ".$grammar->wrap('posts.content').") @@ plainto_tsquery('$searchConfig', ?)"; + $matchScore = "ts_rank(to_tsvector('$searchConfig', ".$grammar->wrap('posts.content')."), plainto_tsquery('$searchConfig', ?))"; + $matchTitleCondition = "to_tsvector('$searchConfig', ".$grammar->wrap('discussions.title').") @@ plainto_tsquery('$searchConfig', ?)"; + $matchTitleScore = "ts_rank(to_tsvector('$searchConfig', ".$grammar->wrap('discussions.title')."), plainto_tsquery('$searchConfig', ?))"; $mostRelevantPostId = 'CAST(SPLIT_PART(STRING_AGG(CAST('.$grammar->wrap('posts.id')." AS VARCHAR), ',' ORDER BY ".$matchScore.' DESC, '.$grammar->wrap('posts.number')."), ',', 1) AS INTEGER) as most_relevant_post_id"; $discussionSubquery = Discussion::select('id') diff --git a/framework/core/src/Foundation/ApplicationInfoProvider.php b/framework/core/src/Foundation/ApplicationInfoProvider.php index 4dea735914..075bb3ff95 100644 --- a/framework/core/src/Foundation/ApplicationInfoProvider.php +++ b/framework/core/src/Foundation/ApplicationInfoProvider.php @@ -87,6 +87,20 @@ public function identifyDatabaseDriver(): string }; } + public function identifyDatabaseOptions(): array + { + if ($this->config['database.driver'] === 'pgsql') { + return [ + 'search_configurations' => collect($this->db->select('SELECT * FROM pg_ts_config')) + ->pluck('cfgname') + ->mapWithKeys(fn (string $cfgname) => [$cfgname => $cfgname]) + ->toArray(), + ]; + } + + return []; + } + /** * Reports on the session driver in use based on three scenarios: * 1. If the configured session driver is valid and in use, it will be returned. diff --git a/framework/core/src/Settings/SettingsServiceProvider.php b/framework/core/src/Settings/SettingsServiceProvider.php index ec7e25672f..3f3d68e7b9 100644 --- a/framework/core/src/Settings/SettingsServiceProvider.php +++ b/framework/core/src/Settings/SettingsServiceProvider.php @@ -30,6 +30,7 @@ public function register(): void 'search_driver_Flarum\Group\Group' => 'default', 'search_driver_Flarum\Post\Post' => 'default', 'search_driver_Flarum\Http\AccessToken' => 'default', + 'pgsql_search_configuration' => 'english', ]); }); From eef0bb1aa25cfd3da4ed5dd92f2c115523a0bcac Mon Sep 17 00:00:00 2001 From: Sami Mazouz Date: Fri, 21 Jun 2024 12:16:50 +0100 Subject: [PATCH 28/29] fix --- .../integration/api/ApprovePostsTest.php | 20 ++++++++++------- .../tests/integration/api/CreatePostsTest.php | 12 ++++++---- .../Api/Controller/ListFlagsController.php | 0 .../flags/src/Api/Resource/FlagResource.php | 5 ++++- .../api/posts/IncludeFlagsVisibilityTest.php | 17 +++++++++----- .../likes/src/Api/LoadLikesRelationship.php | 0 .../likes/src/Api/PostResourceFields.php | 1 + .../integration/api/StickyDiscussionsTest.php | 22 ++++++++++++++----- .../api/discussions/SubscribeTest.php | 9 +++++--- .../core/src/Api/Resource/UserResource.php | 6 ++++- .../api/notifications/UpdateTest.php | 3 ++- .../extenders/ModelPrivateTest.php | 8 +++++++ 12 files changed, 73 insertions(+), 30 deletions(-) delete mode 100644 extensions/flags/src/Api/Controller/ListFlagsController.php delete mode 100644 extensions/likes/src/Api/LoadLikesRelationship.php diff --git a/extensions/approval/tests/integration/api/ApprovePostsTest.php b/extensions/approval/tests/integration/api/ApprovePostsTest.php index 6a9b000882..5d8a322d84 100644 --- a/extensions/approval/tests/integration/api/ApprovePostsTest.php +++ b/extensions/approval/tests/integration/api/ApprovePostsTest.php @@ -11,8 +11,12 @@ use Carbon\Carbon; use Flarum\Approval\Tests\integration\InteractsWithUnapprovedContent; +use Flarum\Discussion\Discussion; +use Flarum\Group\Group; +use Flarum\Post\Post; use Flarum\Testing\integration\RetrievesAuthorizedUsers; use Flarum\Testing\integration\TestCase; +use Flarum\User\User; class ApprovePostsTest extends TestCase { @@ -26,23 +30,23 @@ protected function setUp(): void $this->extension('flarum-approval'); $this->prepareDatabase([ - 'users' => [ + User::class => [ ['id' => 1, 'username' => 'Muralf', 'email' => 'muralf@machine.local', 'is_email_confirmed' => 1], $this->normalUser(), ['id' => 3, 'username' => 'acme', 'email' => 'acme@machine.local', 'is_email_confirmed' => 1], ['id' => 4, 'username' => 'luceos', 'email' => 'luceos@machine.local', 'is_email_confirmed' => 1], ], - 'discussions' => [ + Discussion::class => [ ['id' => 1, 'title' => __CLASS__, 'created_at' => Carbon::now(), 'last_posted_at' => Carbon::now(), 'user_id' => 4, 'first_post_id' => 1, 'comment_count' => 1, 'is_approved' => 1], ], - 'posts' => [ - ['id' => 1, 'discussion_id' => 1, 'user_id' => 4, 'type' => 'comment', 'content' => '

Text

', 'hidden_at' => 0, 'is_approved' => 1, 'number' => 1], - ['id' => 2, 'discussion_id' => 1, 'user_id' => 4, 'type' => 'comment', 'content' => '

Text

', 'hidden_at' => 0, 'is_approved' => 1, 'number' => 2], - ['id' => 3, 'discussion_id' => 1, 'user_id' => 4, 'type' => 'comment', 'content' => '

Text

', 'hidden_at' => 0, 'is_approved' => 0, 'number' => 3], + Post::class => [ + ['id' => 1, 'discussion_id' => 1, 'user_id' => 4, 'type' => 'comment', 'content' => '

Text

', 'hidden_at' => null, 'is_approved' => 1, 'number' => 1], + ['id' => 2, 'discussion_id' => 1, 'user_id' => 4, 'type' => 'comment', 'content' => '

Text

', 'hidden_at' => null, 'is_approved' => 1, 'number' => 2], + ['id' => 3, 'discussion_id' => 1, 'user_id' => 4, 'type' => 'comment', 'content' => '

Text

', 'hidden_at' => null, 'is_approved' => 0, 'number' => 3], ['id' => 4, 'discussion_id' => 1, 'user_id' => 4, 'type' => 'comment', 'content' => '

Text

', 'hidden_at' => Carbon::now(), 'is_approved' => 1, 'number' => 4], - ['id' => 5, 'discussion_id' => 1, 'user_id' => 4, 'type' => 'comment', 'content' => '

Text

', 'hidden_at' => 0, 'is_approved' => 0, 'number' => 5], + ['id' => 5, 'discussion_id' => 1, 'user_id' => 4, 'type' => 'comment', 'content' => '

Text

', 'hidden_at' => null, 'is_approved' => 0, 'number' => 5], ], - 'groups' => [ + Group::class => [ ['id' => 4, 'name_singular' => 'Acme', 'name_plural' => 'Acme', 'is_hidden' => 0], ['id' => 5, 'name_singular' => 'Acme', 'name_plural' => 'Acme', 'is_hidden' => 0], ], diff --git a/extensions/approval/tests/integration/api/CreatePostsTest.php b/extensions/approval/tests/integration/api/CreatePostsTest.php index 82b8857f93..7099f3fa96 100644 --- a/extensions/approval/tests/integration/api/CreatePostsTest.php +++ b/extensions/approval/tests/integration/api/CreatePostsTest.php @@ -11,9 +11,12 @@ use Carbon\Carbon; use Flarum\Approval\Tests\integration\InteractsWithUnapprovedContent; +use Flarum\Discussion\Discussion; use Flarum\Group\Group; +use Flarum\Post\Post; use Flarum\Testing\integration\RetrievesAuthorizedUsers; use Flarum\Testing\integration\TestCase; +use Flarum\User\User; class CreatePostsTest extends TestCase { @@ -27,18 +30,18 @@ protected function setUp(): void $this->extension('flarum-flags', 'flarum-approval'); $this->prepareDatabase([ - 'users' => [ + User::class => [ ['id' => 1, 'username' => 'Muralf', 'email' => 'muralf@machine.local', 'is_email_confirmed' => 1], $this->normalUser(), ['id' => 3, 'username' => 'acme', 'email' => 'acme@machine.local', 'is_email_confirmed' => 1], ['id' => 4, 'username' => 'luceos', 'email' => 'luceos@machine.local', 'is_email_confirmed' => 1], ], - 'discussions' => [ + Discussion::class => [ ['id' => 1, 'title' => __CLASS__, 'created_at' => Carbon::now(), 'last_posted_at' => Carbon::now(), 'user_id' => 4, 'first_post_id' => 1, 'comment_count' => 1, 'is_approved' => 1], ['id' => 2, 'title' => __CLASS__, 'created_at' => Carbon::now(), 'last_posted_at' => Carbon::now(), 'user_id' => 4, 'first_post_id' => 2, 'comment_count' => 1, 'is_approved' => 0], ['id' => 3, 'title' => __CLASS__, 'created_at' => Carbon::now(), 'last_posted_at' => Carbon::now(), 'user_id' => 4, 'first_post_id' => 3, 'comment_count' => 1, 'is_approved' => 0], ], - 'posts' => [ + Post::class => [ ['id' => 1, 'discussion_id' => 1, 'user_id' => 4, 'type' => 'comment', 'content' => '

Text

', 'is_private' => 0, 'is_approved' => 1, 'number' => 1], ['id' => 2, 'discussion_id' => 1, 'user_id' => 4, 'type' => 'comment', 'content' => '

Text

', 'is_private' => 0, 'is_approved' => 1, 'number' => 2], ['id' => 3, 'discussion_id' => 1, 'user_id' => 4, 'type' => 'comment', 'content' => '

Text

', 'is_private' => 0, 'is_approved' => 1, 'number' => 3], @@ -49,7 +52,7 @@ protected function setUp(): void ['id' => 8, 'discussion_id' => 3, 'user_id' => 4, 'type' => 'comment', 'content' => '

Text

', 'is_private' => 0, 'is_approved' => 1, 'number' => 2], ['id' => 9, 'discussion_id' => 3, 'user_id' => 4, 'type' => 'comment', 'content' => '

Text

', 'is_private' => 0, 'is_approved' => 0, 'number' => 3], ], - 'groups' => [ + Group::class => [ ['id' => 4, 'name_singular' => 'Acme', 'name_plural' => 'Acme', 'is_hidden' => 0], ['id' => 5, 'name_singular' => 'Acme', 'name_plural' => 'Acme', 'is_hidden' => 0], ], @@ -60,6 +63,7 @@ protected function setUp(): void 'group_permission' => [ ['group_id' => 4, 'permission' => 'discussion.startWithoutApproval'], ['group_id' => 5, 'permission' => 'discussion.replyWithoutApproval'], + ['group_id' => Group::MEMBER_ID, 'permission' => 'postWithoutThrottle'], ] ]); } diff --git a/extensions/flags/src/Api/Controller/ListFlagsController.php b/extensions/flags/src/Api/Controller/ListFlagsController.php deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/extensions/flags/src/Api/Resource/FlagResource.php b/extensions/flags/src/Api/Resource/FlagResource.php index a124abd521..aec1aefd07 100644 --- a/extensions/flags/src/Api/Resource/FlagResource.php +++ b/extensions/flags/src/Api/Resource/FlagResource.php @@ -53,7 +53,10 @@ public function model(): string public function query(Context $context): object { if ($context->listing(self::class)) { - $query = Flag::query()->groupBy('post_id'); + $query = Flag::query()->whenPgSql( + fn (Builder $query) => $query->distinct('post_id')->orderBy('post_id'), + else: fn (Builder $query) => $query->groupBy('post_id') + ); $this->scope($query, $context); diff --git a/extensions/flags/tests/integration/api/posts/IncludeFlagsVisibilityTest.php b/extensions/flags/tests/integration/api/posts/IncludeFlagsVisibilityTest.php index 7ae42b5139..02100a7d84 100644 --- a/extensions/flags/tests/integration/api/posts/IncludeFlagsVisibilityTest.php +++ b/extensions/flags/tests/integration/api/posts/IncludeFlagsVisibilityTest.php @@ -9,9 +9,14 @@ namespace Flarum\Flags\Tests\integration\api\posts; +use Flarum\Discussion\Discussion; +use Flarum\Flags\Flag; use Flarum\Group\Group; +use Flarum\Post\Post; +use Flarum\Tags\Tag; use Flarum\Testing\integration\RetrievesAuthorizedUsers; use Flarum\Testing\integration\TestCase; +use Flarum\User\User; use Illuminate\Support\Arr; class IncludeFlagsVisibilityTest extends TestCase @@ -28,7 +33,7 @@ protected function setup(): void $this->extension('flarum-tags', 'flarum-flags'); $this->prepareDatabase([ - 'users' => [ + User::class => [ $this->normalUser(), [ 'id' => 3, @@ -56,7 +61,7 @@ protected function setup(): void ['group_id' => 5, 'user_id' => 2], ['group_id' => 6, 'user_id' => 3], ], - 'groups' => [ + Group::class => [ ['id' => 5, 'name_singular' => 'group5', 'name_plural' => 'group5', 'color' => null, 'icon' => 'fas fa-crown', 'is_hidden' => false], ['id' => 6, 'name_singular' => 'group1', 'name_plural' => 'group1', 'color' => null, 'icon' => 'fas fa-cog', 'is_hidden' => false], ], @@ -67,11 +72,11 @@ protected function setup(): void ['group_id' => 6, 'permission' => 'tag1.discussion.viewFlags'], ['group_id' => 6, 'permission' => 'tag1.viewForum'], ], - 'tags' => [ + Tag::class => [ ['id' => 1, 'name' => 'Tag 1', 'slug' => 'tag-1', 'is_primary' => false, 'position' => null, 'parent_id' => null, 'is_restricted' => true], ['id' => 2, 'name' => 'Tag 2', 'slug' => 'tag-2', 'is_primary' => true, 'position' => 2, 'parent_id' => null, 'is_restricted' => false], ], - 'discussions' => [ + Discussion::class => [ ['id' => 1, 'title' => 'Test1', 'user_id' => 1, 'comment_count' => 1], ['id' => 2, 'title' => 'Test2', 'user_id' => 1, 'comment_count' => 1], ], @@ -79,7 +84,7 @@ protected function setup(): void ['discussion_id' => 1, 'tag_id' => 1], ['discussion_id' => 2, 'tag_id' => 2], ], - 'posts' => [ + Post::class => [ ['id' => 1, 'discussion_id' => 1, 'user_id' => 1, 'type' => 'comment', 'content' => '

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

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

'], @@ -87,7 +92,7 @@ protected function setup(): void ['id' => 4, 'discussion_id' => 2, 'user_id' => 1, 'type' => 'comment', 'content' => '

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

'], ], - 'flags' => [ + Flag::class => [ ['id' => 1, 'post_id' => 1, 'user_id' => 1], ['id' => 2, 'post_id' => 1, 'user_id' => 5], ['id' => 3, 'post_id' => 1, 'user_id' => 3], diff --git a/extensions/likes/src/Api/LoadLikesRelationship.php b/extensions/likes/src/Api/LoadLikesRelationship.php deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/extensions/likes/src/Api/PostResourceFields.php b/extensions/likes/src/Api/PostResourceFields.php index d40e9f628b..9f16b286f8 100644 --- a/extensions/likes/src/Api/PostResourceFields.php +++ b/extensions/likes/src/Api/PostResourceFields.php @@ -58,6 +58,7 @@ public function __invoke(): array // So that we can tell if the current user has liked the post. $query ->orderBy(new Expression($grammar->wrap('user_id').' = '.$actor->id), 'desc') + ->orderBy('created_at') ->limit(static::$maxLikes); }), ]; diff --git a/extensions/sticky/tests/integration/api/StickyDiscussionsTest.php b/extensions/sticky/tests/integration/api/StickyDiscussionsTest.php index 1554ec37a6..ec835c624e 100644 --- a/extensions/sticky/tests/integration/api/StickyDiscussionsTest.php +++ b/extensions/sticky/tests/integration/api/StickyDiscussionsTest.php @@ -10,8 +10,12 @@ namespace Flarum\Sticky\Tests\integration\api; use Carbon\Carbon; +use Flarum\Discussion\Discussion; +use Flarum\Group\Group; +use Flarum\Post\Post; use Flarum\Testing\integration\RetrievesAuthorizedUsers; use Flarum\Testing\integration\TestCase; +use Flarum\User\User; class StickyDiscussionsTest extends TestCase { @@ -24,18 +28,24 @@ protected function setUp(): void $this->extension('flarum-sticky'); $this->prepareDatabase([ - 'users' => [ + User::class => [ ['id' => 1, 'username' => 'Muralf', 'email' => 'muralf@machine.local', 'is_email_confirmed' => 1], $this->normalUser(), ['id' => 3, 'username' => 'Muralf_', 'email' => 'muralf_@machine.local', 'is_email_confirmed' => 1], ], - 'discussions' => [ + Discussion::class => [ ['id' => 1, 'title' => __CLASS__, 'created_at' => Carbon::now(), 'last_posted_at' => Carbon::now(), 'user_id' => 1, 'first_post_id' => 1, 'comment_count' => 1, 'is_sticky' => true, 'last_post_number' => 1], - ['id' => 2, 'title' => __CLASS__, 'created_at' => Carbon::now()->addMinutes(2), 'last_posted_at' => Carbon::now()->addMinutes(5), 'user_id' => 1, 'first_post_id' => 1, 'comment_count' => 1, 'is_sticky' => false, 'last_post_number' => 1], - ['id' => 3, 'title' => __CLASS__, 'created_at' => Carbon::now()->addMinutes(3), 'last_posted_at' => Carbon::now()->addMinute(), 'user_id' => 1, 'first_post_id' => 1, 'comment_count' => 1, 'is_sticky' => true, 'last_post_number' => 1], - ['id' => 4, 'title' => __CLASS__, 'created_at' => Carbon::now()->addMinutes(4), 'last_posted_at' => Carbon::now()->addMinutes(2), 'user_id' => 1, 'first_post_id' => 1, 'comment_count' => 1, 'is_sticky' => false, 'last_post_number' => 1], + ['id' => 2, 'title' => __CLASS__, 'created_at' => Carbon::now()->addMinutes(2), 'last_posted_at' => Carbon::now()->addMinutes(5), 'user_id' => 1, 'first_post_id' => 2, 'comment_count' => 1, 'is_sticky' => false, 'last_post_number' => 1], + ['id' => 3, 'title' => __CLASS__, 'created_at' => Carbon::now()->addMinutes(3), 'last_posted_at' => Carbon::now()->addMinute(), 'user_id' => 1, 'first_post_id' => 3, 'comment_count' => 1, 'is_sticky' => true, 'last_post_number' => 1], + ['id' => 4, 'title' => __CLASS__, 'created_at' => Carbon::now()->addMinutes(4), 'last_posted_at' => Carbon::now()->addMinutes(2), 'user_id' => 1, 'first_post_id' => 4, 'comment_count' => 1, 'is_sticky' => false, 'last_post_number' => 1], ], - 'groups' => [ + Post::class => [ + ['id' => 1, 'discussion_id' => 1, 'user_id' => 1, 'type' => 'comment', 'content' => '

Text

', 'number' => 1], + ['id' => 2, 'discussion_id' => 2, 'user_id' => 1, 'type' => 'comment', 'content' => '

Text

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

Text

', 'number' => 1], + ['id' => 4, 'discussion_id' => 4, 'user_id' => 1, 'type' => 'comment', 'content' => '

Text

', 'number' => 1], + ], + Group::class => [ ['id' => 5, 'name_singular' => 'Group', 'name_plural' => 'Groups', 'color' => 'blue'], ], 'group_user' => [ diff --git a/extensions/subscriptions/tests/integration/api/discussions/SubscribeTest.php b/extensions/subscriptions/tests/integration/api/discussions/SubscribeTest.php index 21a243867a..b592ed7e9f 100644 --- a/extensions/subscriptions/tests/integration/api/discussions/SubscribeTest.php +++ b/extensions/subscriptions/tests/integration/api/discussions/SubscribeTest.php @@ -10,8 +10,11 @@ namespace Flarum\Subscriptions\Tests\integration\api\discussions; use Carbon\Carbon; +use Flarum\Discussion\Discussion; +use Flarum\Post\Post; use Flarum\Testing\integration\RetrievesAuthorizedUsers; use Flarum\Testing\integration\TestCase; +use Flarum\User\User; class SubscribeTest extends TestCase { @@ -24,18 +27,18 @@ protected function setUp(): void $this->extension('flarum-subscriptions'); $this->prepareDatabase([ - 'users' => [ + User::class => [ $this->normalUser(), ['id' => 3, 'username' => 'acme', 'email' => 'acme@machine.local', 'is_email_confirmed' => 1, 'preferences' => json_encode(['flarum-subscriptions.notify_for_all_posts' => true])], ['id' => 4, 'username' => 'acme2', 'email' => 'acme2@machine.local', 'is_email_confirmed' => 1], ], - 'discussions' => [ + Discussion::class => [ ['id' => 1, 'title' => __CLASS__, 'created_at' => Carbon::now(), 'last_posted_at' => Carbon::now(), 'user_id' => 1, 'first_post_id' => 1, 'comment_count' => 1, 'last_post_number' => 1, 'last_post_id' => 1], ['id' => 2, 'title' => __CLASS__, 'created_at' => Carbon::now(), 'last_posted_at' => Carbon::now(), 'user_id' => 1, 'first_post_id' => 2, 'comment_count' => 1, 'last_post_number' => 1, 'last_post_id' => 2], ['id' => 33, 'title' => __CLASS__, 'created_at' => Carbon::now(), 'last_posted_at' => Carbon::now(), 'user_id' => 1, 'first_post_id' => 33, 'comment_count' => 6, 'last_post_number' => 6, 'last_post_id' => 38], ], - 'posts' => [ + Post::class => [ ['id' => 1, 'discussion_id' => 1, 'created_at' => Carbon::createFromDate(1975, 5, 21)->toDateTimeString(), 'user_id' => 1, 'type' => 'comment', 'content' => '

foo bar

', 'number' => 1], ['id' => 2, 'discussion_id' => 2, 'created_at' => Carbon::createFromDate(1975, 5, 21)->toDateTimeString(), 'user_id' => 1, 'type' => 'comment', 'content' => '

foo bar

', 'number' => 1], diff --git a/framework/core/src/Api/Resource/UserResource.php b/framework/core/src/Api/Resource/UserResource.php index 71746fdd21..8a5be23013 100644 --- a/framework/core/src/Api/Resource/UserResource.php +++ b/framework/core/src/Api/Resource/UserResource.php @@ -217,7 +217,11 @@ public function fields(): array || $context->getActor()->can('editCredentials', $user); }) ->set(function (User $user, ?string $value) { - $user->exists && $user->changePassword($value); + if ($user->exists) { + $user->changePassword($value); + } else { + $user->password = $value; + } }), // Registration token. Schema\Str::make('token') diff --git a/framework/core/tests/integration/api/notifications/UpdateTest.php b/framework/core/tests/integration/api/notifications/UpdateTest.php index 4a34b47390..331c7d84d8 100644 --- a/framework/core/tests/integration/api/notifications/UpdateTest.php +++ b/framework/core/tests/integration/api/notifications/UpdateTest.php @@ -9,6 +9,7 @@ namespace Flarum\Tests\integration\api\notifications; +use Carbon\Carbon; use Flarum\Discussion\Discussion; use Flarum\Notification\Notification; use Flarum\Post\Post; @@ -38,7 +39,7 @@ protected function setUp(): void ['id' => 1, 'discussion_id' => 1, 'user_id' => 2, 'type' => 'comment', 'content' => 'Foo'], ], Notification::class => [ - ['id' => 1, 'user_id' => 2, 'from_user_id' => 1, 'type' => 'discussionRenamed', 'subject_id' => 1, 'read_at' => null], + ['id' => 1, 'user_id' => 2, 'from_user_id' => 1, 'type' => 'discussionRenamed', 'subject_id' => 1, 'read_at' => null, 'created_at' => Carbon::now()], ] ]); } diff --git a/framework/core/tests/integration/extenders/ModelPrivateTest.php b/framework/core/tests/integration/extenders/ModelPrivateTest.php index 97e116296e..1642a9bda3 100644 --- a/framework/core/tests/integration/extenders/ModelPrivateTest.php +++ b/framework/core/tests/integration/extenders/ModelPrivateTest.php @@ -9,6 +9,7 @@ namespace Flarum\Tests\integration\extenders; +use Carbon\Carbon; use Flarum\Discussion\Discussion; use Flarum\Extend; use Flarum\Testing\integration\RetrievesAuthorizedUsers; @@ -38,6 +39,7 @@ public function discussion_isnt_saved_as_private_by_default() $discussion = Discussion::create([ 'title' => 'Some Discussion', 'user_id' => $user->id, + 'created_at' => Carbon::now(), ]); $this->assertNull($discussion->is_private); @@ -62,10 +64,12 @@ public function discussion_is_saved_as_private_if_privacy_checker_added() $privateDiscussion = Discussion::create([ 'title' => 'Private Discussion', 'user_id' => $user->id, + 'created_at' => Carbon::now(), ]); $publicDiscussion = Discussion::create([ 'title' => 'Public Discussion', 'user_id' => $user->id, + 'created_at' => Carbon::now(), ]); $this->assertTrue($privateDiscussion->is_private); @@ -89,10 +93,12 @@ public function discussion_is_saved_as_private_if_privacy_checker_added_via_invo $privateDiscussion = Discussion::create([ 'title' => 'Private Discussion', 'user_id' => $user->id, + 'created_at' => Carbon::now(), ]); $publicDiscussion = Discussion::create([ 'title' => 'Public Discussion', 'user_id' => $user->id, + 'created_at' => Carbon::now(), ]); $this->assertTrue($privateDiscussion->is_private); @@ -122,10 +128,12 @@ public function private_checkers_that_return_false_dont_matter() $privateDiscussion = Discussion::create([ 'title' => 'Private Discussion', 'user_id' => $user->id, + 'created_at' => Carbon::now(), ]); $publicDiscussion = Discussion::create([ 'title' => 'Public Discussion', 'user_id' => $user->id, + 'created_at' => Carbon::now(), ]); $this->assertTrue($privateDiscussion->is_private); From 60ed61706f55ba8ebd6132e983ca1c1add848fca Mon Sep 17 00:00:00 2001 From: Sami Mazouz Date: Fri, 21 Jun 2024 12:18:46 +0100 Subject: [PATCH 29/29] fix --- framework/core/src/Discussion/Search/FulltextFilter.php | 6 +++--- framework/core/src/User/User.php | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/framework/core/src/Discussion/Search/FulltextFilter.php b/framework/core/src/Discussion/Search/FulltextFilter.php index 46c3662699..44d2489c51 100644 --- a/framework/core/src/Discussion/Search/FulltextFilter.php +++ b/framework/core/src/Discussion/Search/FulltextFilter.php @@ -40,7 +40,7 @@ public function search(SearchState $state, string $value): void }; } - protected function sqlite(SearchState $state, string $value): void + protected function sqlite(DatabaseSearchState $state, string $value): void { /** @var Builder $query */ $query = $state->getQuery(); @@ -61,7 +61,7 @@ protected function sqlite(SearchState $state, string $value): void }); } - protected function mysql(SearchState $state, string $value): void + protected function mysql(DatabaseSearchState $state, string $value): void { /** @var Builder $query */ $query = $state->getQuery(); @@ -115,7 +115,7 @@ protected function mysql(SearchState $state, string $value): void }); } - protected function pgsql(SearchState $state, string $value): void + protected function pgsql(DatabaseSearchState $state, string $value): void { $searchConfig = $this->settings->get('pgsql_search_configuration'); diff --git a/framework/core/src/User/User.php b/framework/core/src/User/User.php index fe17d68d6a..1038c056c7 100644 --- a/framework/core/src/User/User.php +++ b/framework/core/src/User/User.php @@ -367,7 +367,7 @@ protected function getUnreadNotifications(): Collection public function getNewNotificationCount(): int { return $this->unreadNotifications() - ->when($this->read_notifications_at, function (Builder $query) { + ->when($this->read_notifications_at, function (Builder|HasMany $query) { $query->where('created_at', '>', $this->read_notifications_at); }) ->count();