Skip to content

[12.x] feat: query builder aliases for expressions#58436

Merged
taylorotwell merged 1 commit intolaravel:12.xfrom
tpetry:12.x-select-expression
Jan 20, 2026
Merged

[12.x] feat: query builder aliases for expressions#58436
taylorotwell merged 1 commit intolaravel:12.xfrom
tpetry:12.x-select-expression

Conversation

@tpetry
Copy link
Contributor

@tpetry tpetry commented Jan 20, 2026

Currently, you can add subqueries to a query builder with an alias.

// select (select "allowed_to_x" from "configuration") as "sub" from "users"
User::select([
    'sub' => Configuration::select('allowed_to_x'),
]);

But the same is not yet possible for expressions There's just no way to define an alias. This PR fixes this

// current: select available_a - used_a from "users"
// with pr: select (available_a - used_a) as "remaining_a" from "users"
User::select([
    'remaining_a' => new Expression('available_a - used_a'),
]);

So this PR changes:

  • adds selectExpression (like selectSub)
  • adds expression alias support to select
  • adds expression alias support to addSelect
  • adds units to test the behaviour of array keys for select and addSelect which was not covered yet

@tpetry tpetry force-pushed the 12.x-select-expression branch 2 times, most recently from 4bebe0e to d97f59b Compare January 20, 2026 12:13
@tpetry tpetry force-pushed the 12.x-select-expression branch from d97f59b to 38ccfa0 Compare January 20, 2026 12:17
@tpetry
Copy link
Contributor Author

tpetry commented Jan 20, 2026

The failing static analysis check is because of some blade compiler stuff. Not touched that part.

@taylorotwell taylorotwell merged commit 8b4032f into laravel:12.x Jan 20, 2026
69 of 70 checks passed
@tpetry tpetry deleted the 12.x-select-expression branch January 21, 2026 05:18
@u01jmg3
Copy link
Contributor

u01jmg3 commented Jan 21, 2026

Double aliasing in MySQL when using an existing associative array of selects with column aliases

This looks like a really useful feature, but it caused a problem in our test suite in a very specific scenario:

If you already have an existing associative array of selects with your own column aliases, and then pass it to select(), this new feature that automatically applies array keys as aliases results in double aliasing, which will error with MySQL.

Example:

$selects = [
    'conference_registration_id' => 'conference_registrations.id AS conference_registration_id',
    'user_id'                    => 'conference_registrations.user_id',
];

-Model::select($selects)->get();
+Model::select(array_values($selects))->get();

A user can work around this by applying array_values() to $selects, or take advantage of the new feature by removing their custom AS conference_registration_id. Either approach prevents double aliasing and resolves the issue. However, this behaviour was unexpected in a minor release and caught us off guard.

Could this feature be opt-in? Otherwise, Laravel may automatically create aliases in users' queries, potentially causing issues, whether through double aliasing or the addition of unintended aliases.

@tpetry
Copy link
Contributor Author

tpetry commented Jan 21, 2026

@u01jmg3 I did only change the behaviour when an Expression was used. Nothing did change for you. In fact, your example is producing exactly the same SQL before and after my change:

DB::table('example')->select([
    'conference_registration_id' => 'conference_registrations.id AS conference_registration_id',
    'user_id'                    => 'conference_registrations.user_id',
])->dd();

// Laravel 12.47.0:
// select
//   "conference_registrations"."id" as "conference_registration_id",
//   "conference_registrations"."user_id"
// from "example

// Laravel 12.48.0:
// select
//   "conference_registrations"."id" as "conference_registration_id",
//   "conference_registrations"."user_id"
// from "example

So honestly, I don't understand your issue and how it relates to my pr. You must provide more information. Like how the sql was before/after to understand the impact.

@u01jmg3
Copy link
Contributor

u01jmg3 commented Jan 21, 2026

Syntax error or access violation: 1064
You have an error in your SQL syntax; check the manual that corresponds to your
MySQL server version for the right syntax to use near

'AS requirements) as requirements,
 (COALESCE(IF(STRCMP(dietary_requirements.tex'

at line 1

Connection: mysql
Host: 127.0.0.1
Port: 3306
Database: test
PHP
/**
 * Combine necessary tables to return Conference Attendee data.
 *
 * @param  Conference $conference
 * @param  array      $excludedColumns
 * @param  boolean    $withHtml
 * @return \Illuminate\Database\Eloquent\Collection
 */
public static function getAttendees(Conference $conference, array $excludedColumns = [], $withHtml = true)
{
    $selects = [
        'conference_registration_id'            => 'conference_registrations.id AS conference_registration_id',
        'conference_id'                         => 'conference_registrations.conference_id',
        'user_id'                               => 'conference_registrations.user_id',
        'ars_membership_no'                     => 'ars_membership_no',
        'ars_validated'                         => 'ars_validated',
        'email'                                 => 'email',
        'requirements'                          => \DB::raw('IF(conference_registration_categories.id IS NULL, IF(includes_accommodation = "yes" AND includes_dinner = "yes", "Accommodation and Dinner", IF(includes_accommodation = "yes", "Accommodation only", IF(includes_dinner = "yes", "Dinner only", "-"))), CONCAT(conference_fees.text, IF(includes_dinner = "yes", " + with dinner", ""))) AS requirements'),
        'dietary'                               => \DB::raw('COALESCE(IF(STRCMP(dietary_requirements.text, "Other"), dietary_requirements.text, dietary_other), "-") AS dietary'),
        'work_info'                             => \DB::raw('COALESCE(CONCAT(IF(conference_registration_categories.id IS NULL, "Guest Speaker", conference_registration_categories.text), IF(workplaces.id IS NULL, "", CONCAT(" // ", IF(STRCMP(workplaces.text, "Other"), workplaces.text, workplace_other)))), "-") AS work_info'),
        'conference_registration_categories_id' => 'conference_registration_categories.id AS conference_registration_categories_id',
        'conference_fee_id'                     => 'conference_fees.id AS conference_fee_id',
        'updated_at'                            => 'conference_registrations.updated_at',
        'updated_by'                            => 'conference_registrations.updated_by',
        'invoice_id'                            => 'conference_registrations.invoice_id',
        'stripe_payment_intent_id'              => 'conference_registrations.stripe_payment_intent_id',
        'is_ars_member'                         => 'conference_registrations.is_ars_member',
    ];

    if ($excludedColumns) {
        $excludedColumns = array_flip($excludedColumns);
        $selects         = array_diff_key($selects, $excludedColumns);
    }

    if (!$withHtml) {
        $selects = ['name' => \DB::raw('CONCAT(users.forename, " ", users.surname) AS name')] + $selects;
    }

    return self::select($selects)
               ->where('conference_registrations.conference_id', $conference->id)
               ->wherePaid('yes')
               ->withConferenceFees('leftJoin')
               ->withConferenceRegistrationCategories('leftJoin')
               ->withDietaryRequirements()
               ->withUsers()
               ->withWorkplaces()
               ->orderBy('surname')
               ->get();
}
SQL
SELECT
    conference_registrations.id AS conference_registration_id,
    conference_registrations.conference_id,
    conference_registrations.user_id,
    ars_membership_no,
    ars_validated,
    email,
    (
        IF(
            conference_registration_categories.id IS NULL,
            IF(
                includes_accommodation = "yes"
                AND includes_dinner = "yes",
                "Accommodation and Dinner",
                IF(
                    includes_accommodation = "yes",
                    "Accommodation only",
                    IF(
                        includes_dinner = "yes",
                        "Dinner only",
                        "-"
                    )
                )
            ),
            CONCAT(
                conference_fees.text,
                IF(includes_dinner = "yes", " + with dinner", "")
            )
        ) AS requirements
    ) AS requirements,
    (
        COALESCE(
            IF(
                STRCMP(dietary_requirements.text, "Other"),
                dietary_requ

@tpetry
Copy link
Contributor Author

tpetry commented Jan 21, 2026

Thats a completely different code than you initially provided. I see no way to fix this in code. You basically used the select() function in a very special way. Because you provided array keys which don't have any effect at all. Didn't have before. There are two options from here:

  1. You remove the AS colname from your expressions because thats now generated by the implementation
  2. You open an issue (because Taylor doesnt read comments on closed prs) to escalate the issue and start a discussion whether the change should be reverted.

@u01jmg3
Copy link
Contributor

u01jmg3 commented Jan 21, 2026

My apologies - I only shared a shortened snippet of the first few entries in my $selects array, assuming the issue would be clear from that alone. I now realise it was more specifically the requirements select I've written. But no problem, we got there in the end.

Using an associative array in my code is not anything special - it is a practical way to dynamically include or exclude columns, which I suspect is a fairly common use case. Prior to this change, it caused no issues with MySQL.

To work around this, I'll use array_values($selects) so that aliases are only applied where I explicitly want them.


You open an issue (because Taylor doesn't read comments on closed PRs) to escalate the issue and start a discussion whether the change should be reverted.

If you're asking me to raise the issue, does that mean you think my use case isn't valid and the current behaviour should remain as is?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants