Fix MariaDB driver detection and reblog caption null inserts

Laravel 11 exposes MariaDB as a dedicated 'mariadb' driver, so
config('database.default') === 'mysql' checks silently misclassified
MariaDB as the non-mysql (postgres) branch.

- Add App\Util\Database\DatabaseDriver with isMysqlLike()/isPgsql()
  plus db_is_mysql_like()/db_is_pgsql() global helpers.
- Route all database.default driver checks through the helpers so
  MySQL and MariaDB are treated as one group.
- Use '' (not null) for share/compose caption+rendered, valid whether
  the column is nullable or NOT NULL (it is NOT NULL on MySQL/MariaDB).
- Guard pgsql strtolower() in registration against missing fields.
- Scope CustomEmoji::duplicateShortcodes to the grouped column for
  Postgres GROUP BY validity.
- Remove stale Postgres guard in status:dedup; use havingRaw for
  cross-driver HAVING.
pull/7271/head
Your Name 1 week ago
parent 74e861b4c2
commit e3b6cebf27

@ -41,11 +41,6 @@ class StatusDedupe extends Command
public function handle()
{
if (config('database.default') == 'pgsql') {
$this->info('This command is not compatible with Postgres, we are working on a fix.');
return;
}
// Deterministically keep the earliest-fetched status per uri via
// MIN(id). Selecting a non-aggregated id under GROUP BY is
// nondeterministic and cannot be influenced by ORDER BY, so the
@ -55,7 +50,7 @@ class StatusDedupe extends Command
->whereNull('deleted_at')
->whereNotNull('uri')
->groupBy('uri')
->having('occurences', '>', 1)
->havingRaw('count(uri) > 1')
->orderBy('uri')
->chunk(50, function ($statuses) {
foreach ($statuses as $status) {

@ -77,7 +77,7 @@ class InstanceUpdateTotalLocalPosts extends Command
protected function getTotalLocalPosts()
{
if ((bool) config('instance.total_count_estimate') && config('database.default') === 'mysql') {
if ((bool) config('instance.total_count_estimate') && db_is_mysql_maria()) {
return DB::select("EXPLAIN SELECT COUNT(*) FROM statuses WHERE deleted_at IS NULL AND uri IS NULL and local = 1 AND type != 'share'")[0]->rows;
}

@ -36,8 +36,9 @@ trait AdminAutospamController
});
$thisWeek = Cache::remember('admin-dash:reports:spam-count-stats-this-week ', 86400, function () {
$sr = config('database.default') == 'pgsql' ? "to_char(created_at, 'MM-YYYY')" : "DATE_FORMAT(created_at, '%m-%Y')";
$gb = config('database.default') == 'pgsql' ? [DB::raw($sr)] : DB::raw($sr);
$isPgsql = db_is_pgsql();
$sr = $isPgsql ? "to_char(created_at, 'MM-YYYY')" : "DATE_FORMAT(created_at, '%m-%Y')";
$gb = $isPgsql ? [DB::raw($sr)] : DB::raw($sr);
$s = AccountInterstitial::select(
DB::raw('count(id) as count'),
DB::raw($sr.' as month_year')

@ -141,7 +141,7 @@ trait AdminReportController
});
$avg = Cache::remember('admin-dash:reports:spam-count:avg', 43200, function () {
if (config('database.default') != 'mysql') {
if (! db_is_mysql_maria()) {
return 0;
}
@ -153,7 +153,7 @@ trait AdminReportController
});
$avgOpen = Cache::remember('admin-dash:reports:spam-count:avgopen', 43200, function () {
if (config('database.default') != 'mysql') {
if (! db_is_mysql_maria()) {
return '0';
}
$seconds = AccountInterstitial::selectRaw('DATE(created_at) AS start_date, AVG(TIME_TO_SEC(TIMEDIFF(appeal_handled_at, created_at))) AS timediff')->whereType('post.autospam')->whereNotNull('appeal_handled_at')->where('created_at', '>', now()->subMonth())->get();

@ -299,11 +299,13 @@ trait AdminSettingsController
break;
case 'mysql':
case 'mariadb':
$exp = DB::raw('select version()');
$expQuery = $exp->getValue(DB::connection()->getQueryGrammar());
$version = DB::select($expQuery)[0]->{'version()'};
$sys['database'] = [
'name' => 'MySQL',
'version' => DB::select($expQuery)[0]->{'version()'},
'name' => stripos($version, 'mariadb') !== false ? 'MariaDB' : 'MySQL',
'version' => $version,
];
break;

@ -553,7 +553,7 @@ class AdminController extends Controller
return redirect(route('admin.custom-emoji'));
}
$pg = config('database.default') == 'pgsql';
$pg = db_is_pgsql();
$emojis = CustomEmoji::when($sort, function ($query, $sort) use ($request, $pg) {
if ($sort == 'all') {

@ -3327,7 +3327,7 @@ class ApiV1Controller extends Controller
$pid = $user->profile_id;
$isPgsql = config('database.default') == 'pgsql';
$isPgsql = db_is_pgsql();
if ($isPgsql) {
$dms = DirectMessage::when($scope === 'inbox', function ($q) use ($pid) {
@ -4126,10 +4126,9 @@ class ApiV1Controller extends Controller
}
}
$defaultCaption = config_cache('database.default') === 'mysql' ? null : '';
$share = Status::firstOrCreate([
'caption' => $defaultCaption,
'rendered' => $defaultCaption,
'caption' => '',
'rendered' => '',
'profile_id' => $user->profile_id,
'reblog_of_id' => $status->id,
'type' => 'share',
@ -4230,7 +4229,7 @@ class ApiV1Controller extends Controller
'Invalid permissions for this action'
);
if (config('database.default') === 'pgsql') {
if (db_is_pgsql()) {
$tag = Hashtag::where('name', 'ilike', $hashtag)
->orWhere('slug', 'ilike', $hashtag)
->first();

@ -561,7 +561,7 @@ class ApiV1Dot1Controller extends Controller
$username = $request->input('username');
$password = $request->input('password');
if (config('database.default') == 'pgsql') {
if (db_is_pgsql()) {
$username = strtolower($username);
$email = strtolower($email);
}

@ -52,7 +52,7 @@ class TagsController extends Controller
$pid = $request->user()->profile_id;
$account = AccountService::get($pid);
$operator = config('database.default') == 'pgsql' ? 'ilike' : 'like';
$operator = db_is_pgsql() ? 'ilike' : 'like';
$tag = Hashtag::where('name', $operator, $id)
->orWhere('slug', $operator, $id)
->first();
@ -94,7 +94,7 @@ class TagsController extends Controller
$pid = $request->user()->profile_id;
$account = AccountService::get($pid);
$operator = config('database.default') == 'pgsql' ? 'ilike' : 'like';
$operator = db_is_pgsql() ? 'ilike' : 'like';
$tag = Hashtag::where('name', $operator, $id)
->orWhere('slug', $operator, $id)
->first();
@ -139,7 +139,7 @@ class TagsController extends Controller
$pid = $request->user()->profile_id;
$account = AccountService::get($pid);
$operator = config('database.default') == 'pgsql' ? 'ilike' : 'like';
$operator = db_is_pgsql() ? 'ilike' : 'like';
$tag = Hashtag::where('name', $operator, $id)
->orWhere('slug', $operator, $id)
->first();

@ -59,9 +59,13 @@ class RegisterController extends Controller
*/
public function validator(array $data)
{
if (config('database.default') == 'pgsql') {
$data['username'] = strtolower($data['username']);
$data['email'] = strtolower($data['email']);
if (db_is_pgsql()) {
if (isset($data['username'])) {
$data['username'] = strtolower($data['username']);
}
if (isset($data['email'])) {
$data['email'] = strtolower($data['email']);
}
}
$usernameRules = [
@ -109,9 +113,13 @@ class RegisterController extends Controller
*/
public function create(array $data)
{
if (config('database.default') == 'pgsql') {
$data['username'] = strtolower($data['username']);
$data['email'] = strtolower($data['email']);
if (db_is_pgsql()) {
if (isset($data['username'])) {
$data['username'] = strtolower($data['username']);
}
if (isset($data['email'])) {
$data['email'] = strtolower($data['email']);
}
}
return User::create([

@ -265,7 +265,7 @@ class ComposeController extends Controller
$blocked = UserFilterService::searchExcludedProfileIds($request->user()->profile_id);
$operator = config('database.default') === 'pgsql' ? 'ilike' : 'like';
$operator = db_is_pgsql() ? 'ilike' : 'like';
$results = Profile::select([
'profiles.id',
'profiles.domain',
@ -354,7 +354,7 @@ class ComposeController extends Controller
$popular = Cache::remember('pf:search:location:v1:popular', 1209600, function () {
$minId = SnowflakeService::byDate(now()->subDays(290));
if (config('database.default') == 'pgsql') {
if (db_is_pgsql()) {
return Status::selectRaw('id, place_id, count(place_id) as pc')
->whereNotNull('place_id')
->where('id', '>', $minId)
@ -393,7 +393,7 @@ class ComposeController extends Controller
});
});
$wildcard = config('database.default') === 'pgsql' ? 'ilike' : 'like';
$wildcard = db_is_pgsql() ? 'ilike' : 'like';
$q = '%'.$raw.'%';
$placesQuery = DB::table('places')->where('name', $wildcard, $q);
@ -453,7 +453,7 @@ class ComposeController extends Controller
$blocked = UserFilterService::searchExcludedProfileIds($request->user()->profile_id);
$currentUserId = $request->user()->profile_id;
$operator = config('database.default') === 'pgsql' ? 'ilike' : 'like';
$operator = db_is_pgsql() ? 'ilike' : 'like';
$results = Profile::select([
'profiles.id',
@ -730,7 +730,10 @@ class ComposeController extends Controller
$place = $request->input('place');
$cw = $request->input('cw');
$tagged = $request->input('tagged');
$defaultCaption = config_cache('database.default') === 'mysql' ? null : '';
// Empty string is valid whether `caption`/`rendered` are nullable or
// NOT NULL (they are NOT NULL on MySQL/MariaDB in practice), so use it
// regardless of driver rather than inserting null.
$defaultCaption = '';
if ($place && is_array($place)) {
$status->place_id = $place['id'];

@ -59,7 +59,7 @@ class DirectMessageController extends Controller
'is_hidden', 'meta', 'created_at', 'read_at'
)->with(['author', 'status', 'recipient']);
if (config('database.default') == 'pgsql') {
if (db_is_pgsql()) {
$query = match ($action) {
'inbox' => $baseQuery->whereToId($profile)
->whereIsHidden(false)

@ -66,7 +66,7 @@ class DiscoverController extends Controller
$end = $page > 1 ? $page * 9 : (($page * 9) + 9);
$tag = $request->input('hashtag');
if (config('database.default') === 'pgsql') {
if (db_is_pgsql()) {
$hashtag = Hashtag::where('name', 'ilike', $tag)->firstOrFail();
} else {
$hashtag = Hashtag::whereName($tag)->firstOrFail();

@ -46,7 +46,7 @@ class PlaceController extends Controller
public function directoryCities(Request $request, $country): View
{
$country = urldecode($country);
$operator = config('database.default') === 'pgsql' ? 'ilike' : '=';
$operator = db_is_pgsql() ? 'ilike' : '=';
$places = Place::where('country', $operator, $country)
->orderBy('name', 'asc')

@ -37,7 +37,7 @@ class SearchController extends Controller
*/
protected function likeOperator(): string
{
return config('database.default') === 'pgsql' ? 'ilike' : 'like';
return db_is_pgsql() ? 'ilike' : 'like';
}
public function searchAPI(Request $request): JsonResponse

@ -25,7 +25,7 @@ class SeasonalController extends Controller
public function yearInReview(): View
{
abort_if(now()->gt('2021-03-01 00:00:00'), 404);
abort_if(config('database.default') != 'mysql', 404);
abort_if(! db_is_mysql_maria(), 404);
$profile = Auth::user()->profile;
@ -35,7 +35,7 @@ class SeasonalController extends Controller
public function getData(Request $request): JsonResponse
{
abort_if(now()->gt('2021-03-01 00:00:00'), 404);
abort_if(config('database.default') != 'mysql', 404);
abort_if(! db_is_mysql_maria(), 404);
$uid = $request->user()->id;
$pid = $request->user()->profile_id;
@ -227,7 +227,7 @@ class SeasonalController extends Controller
public function store(Request $request): JsonResponse
{
abort_if(now()->gt('2021-03-01 00:00:00'), 404);
abort_if(config('database.default') != 'mysql', 404);
abort_if(! db_is_mysql_maria(), 404);
$user = $request->user();

@ -221,11 +221,12 @@ class StatusController extends Controller
}
ReblogService::del($profile->id, $status->id);
} else {
$defaultCaption = config_cache('database.default') === 'mysql' ? null : '';
// A share carries no caption. Empty string is valid whether the
// column is nullable or NOT NULL (it is NOT NULL on MySQL/MariaDB),
// so use it regardless of driver rather than inserting null.
$share = new Status;
$share->caption = $defaultCaption;
$share->rendered = $defaultCaption;
$share->caption = '';
$share->rendered = '';
$share->profile_id = $profile->id;
$share->reblog_of_id = $status->id;
$share->in_reply_to_profile_id = $status->profile_id;

@ -42,7 +42,7 @@ class StoryApiV1Controller extends Controller
abort_if(! (bool) config_cache('instance.stories.enabled') || ! $request->user(), 404);
$pid = $request->user()->profile_id;
if (config('database.default') == 'pgsql') {
if (db_is_pgsql()) {
$s = Cache::remember(self::RECENT_KEY.$pid, self::RECENT_TTL, function () use ($pid) {
return Story::select('stories.*', 'followers.following_id')
->leftJoin('followers', 'followers.following_id', 'stories.profile_id')
@ -164,7 +164,7 @@ class StoryApiV1Controller extends Controller
abort_if(! (bool) config_cache('instance.stories.enabled') || ! $request->user(), 404);
$pid = $request->user()->profile_id;
if (config('database.default') == 'pgsql') {
if (db_is_pgsql()) {
$s = Cache::remember(self::RECENT_KEY.$pid, self::RECENT_TTL, function () use ($pid) {
return Story::select('stories.*', 'followers.following_id')
->leftJoin('followers', 'followers.following_id', 'stories.profile_id')

@ -34,7 +34,7 @@ class StoryController extends StoryComposeController
}
$pid = $user->profile_id;
if (config('database.default') == 'pgsql') {
if (db_is_pgsql()) {
$s = Cache::remember('pf:stories:recent-by-id:'.$pid, 900, function () use ($pid) {
return Story::select('stories.*', 'followers.following_id')
->leftJoin('followers', 'followers.following_id', 'stories.profile_id')

@ -83,7 +83,7 @@ class CommentPipeline implements ShouldQueue
return;
}
if (config('database.default') === 'mysql') {
if (db_is_mysql_maria()) {
// todo: refactor
// $exp = DB::raw("select id, in_reply_to_id from statuses, (select @pv := :kid) initialisation where id > @pv and find_in_set(in_reply_to_id, @pv) > 0 and @pv := concat(@pv, ',', id)");
// $expQuery = $exp->getValue(DB::connection()->getQueryGrammar());

@ -97,7 +97,7 @@ class StatusReplyPipeline implements ShouldQueue
return 1;
}
if (config('database.default') === 'mysql') {
if (db_is_mysql_maria()) {
// todo: refactor
// $exp = DB::raw("select id, in_reply_to_id from statuses, (select @pv := :kid) initialisation where id > @pv and find_in_set(in_reply_to_id, @pv) > 0 and @pv := concat(@pv, ',', id)");
// $expQuery = $exp->getValue(DB::connection()->getQueryGrammar());

@ -93,7 +93,7 @@ class StatusTagsPipeline implements ShouldQueue
}
}
if (config('database.default') === 'pgsql') {
if (db_is_pgsql()) {
$hashtag = DB::transaction(function () use ($name) {
$slug = Str::slug($name, '-', false);

@ -22,7 +22,10 @@ class CustomEmoji extends Model
*/
public function scopeDuplicateShortcodes($query)
{
return $query->groupBy('shortcode')->havingRaw('count(*) > 1');
// Select only the grouped column so the aggregate is valid on
// Postgres (a bare `select *` with GROUP BY is rejected because
// non-grouped columns must appear in GROUP BY or an aggregate).
return $query->select('shortcode')->groupBy('shortcode')->havingRaw('count(*) > 1');
}
public static function scan($text, $activitypub = false)

@ -75,7 +75,7 @@ class AdminStatsService
protected static function recentData()
{
$day = config('database.default') == 'pgsql' ? 'DATE_PART(\'day\',' : 'day(';
$day = db_is_pgsql() ? 'DATE_PART(\'day\',' : 'day(';
$ttl = now()->addMinutes(15);
return Cache::remember('admin:dashboard:home:data:v0:15min', $ttl, function () {
@ -126,7 +126,7 @@ class AdminStatsService
$ttl = now()->addHours(12);
return Cache::remember('admin:dashboard:home:data-postsGraph:v0.1:24hr', $ttl, function () {
$gb = config('database.default') == 'pgsql' ? ['statuses.id', 'created_at'] : DB::raw('Date(created_at)');
$gb = db_is_pgsql() ? ['statuses.id', 'created_at'] : DB::raw('Date(created_at)');
$s = Status::selectRaw('Date(created_at) as date, count(statuses.id) as count')
->where('created_at', '>=', now()->subWeek())
->groupBy($gb)

@ -250,7 +250,7 @@ class CustomEmojiService
public static function all()
{
return Cache::rememberForever('pf:custom_emoji', function () {
$pgsql = config('database.default') === 'pgsql';
$pgsql = db_is_pgsql();
return CustomEmoji::when(! $pgsql, function ($q, $pgsql) {
return $q->groupBy('shortcode');

@ -10,7 +10,7 @@ class DiscoverService
public static function getDailyIdPool()
{
$min_id = SnowflakeService::byDate(now()->subMonths(3));
$sqld = config('database.default') == 'mysql';
$sqld = db_is_mysql_maria();
return DB::table('statuses')
->whereNull('uri')

@ -99,7 +99,7 @@ class SearchApiV2Service
)
);
}
$operator = config('database.default') === 'pgsql' ? 'ilike' : 'like';
$operator = db_is_pgsql() ? 'ilike' : 'like';
$results = Profile::select('username', 'id', 'followers_count', 'domain')
->where('username', $operator, $query)
->orWhere('webfinger', $operator, $webfingerQuery)
@ -132,7 +132,7 @@ class SearchApiV2Service
$query = Str::startsWith($q, '#') ? substr($q, 1) : $q;
$query = $query.'%';
if (config('database.default') === 'pgsql') {
if (db_is_pgsql()) {
$baseQuery = Hashtag::query()
->where('name', 'ilike', $query)
->where('is_banned', false)

@ -0,0 +1,57 @@
<?php
namespace App\Util\Database;
use Illuminate\Support\Facades\DB;
/**
* Helpers for branching on the active database driver.
*
* Laravel 11 ships a dedicated `mariadb` driver, so `config('database.default')`
* returns `mariadb` (not `mysql`) when a MariaDB connection is active. MySQL and
* MariaDB share the same SQL dialect for the branches used in this codebase, so
* they must be treated as one group. Comparing directly against the string
* `'mysql'` silently misclassifies MariaDB as "other" (i.e. the Postgres path).
*
* Use these helpers instead of comparing driver strings by hand.
*/
class DatabaseDriver
{
/**
* Drivers that share MySQL's SQL dialect.
*
* @var array<int, string>
*/
public const MYSQL_LIKE = ['mysql', 'mariadb'];
/**
* The driver name for the given connection (defaults to the active one).
*
* Resolves the real driver rather than the connection name, so a connection
* named `mysql` that is actually configured with the `mariadb` driver is
* reported correctly.
*/
public static function name(?string $connection = null): ?string
{
return DB::connection($connection)->getDriverName();
}
/**
* True when the driver is MySQL or MariaDB.
*
* Prefer this over `config('database.default') === 'mysql'`, which excludes
* MariaDB.
*/
public static function isMysqlMaria(?string $connection = null): bool
{
return in_array(self::name($connection), self::MYSQL_LIKE, true);
}
/**
* True when the driver is PostgreSQL.
*/
public static function isPgsql(?string $connection = null): bool
{
return self::name($connection) === 'pgsql';
}
}

@ -1,6 +1,7 @@
<?php
use App\Services\ConfigCacheService;
use App\Util\Database\DatabaseDriver;
if (! function_exists('config_cache')) {
function config_cache($key)
@ -8,3 +9,26 @@ if (! function_exists('config_cache')) {
return ConfigCacheService::get($key);
}
}
if (! function_exists('db_is_mysql_maria')) {
/**
* True when the active (or given) connection uses MySQL or MariaDB.
*
* Use instead of config('database.default') === 'mysql', which misses
* MariaDB (Laravel exposes it as a distinct 'mariadb' driver).
*/
function db_is_mysql_maria(?string $connection = null): bool
{
return DatabaseDriver::isMysqlMaria($connection);
}
}
if (! function_exists('db_is_pgsql')) {
/**
* True when the active (or given) connection uses PostgreSQL.
*/
function db_is_pgsql(?string $connection = null): bool
{
return DatabaseDriver::isPgsql($connection);
}
}

Loading…
Cancel
Save