NAME

DBIx::Class::Async::SelectNormaliser - Normalise -ident clauses in ResultSet select attributes

VERSION

Version 0.63

SYNOPSIS

use DBIx::Class::Async::SelectNormaliser;

# Inline normalisation before passing attrs to search()
my ($clean_select, $clean_as) = DBIx::Class::Async::SelectNormaliser->normalise(
    select => [ { -ident => 'me.status', -as => 'current_status' } ],
    as     => [],
);

my $rs = $schema->resultset('Order')->search({}, {
    select => $clean_select,
    as     => $clean_as,
});

# Or call normalise_attrs() directly on a complete attrs hashref
my $attrs = DBIx::Class::Async::SelectNormaliser->normalise_attrs({
    select => [
        'me.id',
        { -ident => 'me.status',  -as => 'current_status' },
        { -ident => 'me.created', -as => 'created_at'     },
        { count  => 'me.id',      -as => 'total'          },   # function form -- left unchanged
    ],
    as      => [ 'id' ],          # partially specified -- filled in from -as
    where   => { active => 1 },
    order_by => 'me.id',
});

my $rs = $schema->resultset('Order')->search($attrs->{where}, $attrs);

DESCRIPTION

The problem

DBIx::Class supports a -ident operator in where, order_by, and group_by clauses to force a value to be treated as a SQL identifier (column or table name) rather than a literal string or a function call:

# In a where clause -- works correctly
$rs->search({ 'me.status' => { -ident => 'other_table.status' } });

However, -ident in the select attribute does not work:

# Broken -- produces: SELECT -IDENT(me.status) AS current_status
$rs->search({}, {
    select => [ { -ident => 'me.status', -as => 'current_status' } ],
});

The select attribute is processed by a different code path inside SQL::Abstract -- specifically _select_field -- which does not recognise -ident as a special sigil and instead treats it as a function name. The hash key -ident becomes the function, its value becomes the argument, and the result is the literal string -IDENT(me.status) in the SQL output, which is a syntax error on every database.

Why not fix upstream?

Extending select to support -ident in DBIx::Class or SQL::Abstract::More is non-trivial:

  • select hashrefs are already used for function calls: { count => 'me.id', -as => 'cnt' }. Adding -ident as a special sigil inside the same hashref form requires distinguishing { -ident => 'col', -as => 'alias' } (identifier alias) from { func => 'col', -as => 'alias' } (function call) without introducing ambiguity. A column named ident would be indistinguishable from the operator.

  • The select/as separation is deliberate DBIC design: select is a list of SQL expressions and as is a parallel list of Perl-side aliases. Adding inline -as to select as well (which DBIC already supports for functions) and now -ident would create three overlapping ways to alias a column, all with subtly different semantics.

  • Changing this in SQL::Abstract would affect all DBIC users and all other consumers of SQL::Abstract, requiring deprecation cycles and backwards compatibility guarantees that are beyond the scope of a single distribution.

The solution: pre-processing in DBIx::Class::Async

Rather than patching upstream, this module pre-processes the select and as attributes before they reach SQL::Abstract. Any { -ident => $col, -as => $alias } hashref is rewritten to its canonical DBIC form: a bare column name string in select and a corresponding entry in as. All other forms -- bare strings, function hashrefs, literal SQL references -- are left completely unchanged.

This approach:

  • Requires no upstream changes. The transformation happens entirely in DBIx::Class::Async before the attrs touch SQL::Abstract.

  • Is transparent to callers. Application code that already uses the canonical select/as form is unaffected. Callers who prefer the -ident form get intuitive, correct behaviour.

  • Is safe to compose. Function hashrefs ({ count => 'me.id' }) are detected by the absence of -ident and passed through untouched, so all existing query patterns continue to work.

  • Is explicit about intent. -ident says clearly "this is a column name, not a function and not a literal string", which is useful documentation in itself.

Integration Points

This module is called from two places in DBIx::Class::Async:

DBIx::Class::Async::ResultSet::search()

Before building the payload for the worker, search() calls "normalise_attrs" on the incoming attrs hashref. This means all ResultSet operations that flow through search (all, count, update, etc.) benefit automatically.

DBIx::Class::Async::ResultSet::search_rs()

The same normalisation is applied when building a new ResultSet object, so chained searches also produce correct SQL.

Interaction with -as in function hashrefs

DBIC supports an inline -as inside function hashrefs:

{ count => 'me.id', -as => 'total' }

This module does not touch that form. The -as key is only consumed when it appears alongside -ident. In a function hashref, -as is already handled correctly by DBIC and SQL::Abstract and must not be extracted into the as array, because DBIC expects the alias to come from the function hashref itself in that case.

Partial as arrays

The incoming as array may be shorter than select, absent entirely, or partially specified. This module fills in missing entries from -as values found in the select items. Entries already present in as take priority over any -as in the corresponding select item, preserving the behaviour of callers who specify both.

METHODS

normalise_attrs

my $clean_attrs = DBIx::Class::Async::SelectNormaliser->normalise_attrs(\%attrs);

Accepts a complete ResultSet attrs hashref. If the hashref contains a select key, rewrites any { -ident => $col, -as => $alias } items to bare column strings and populates as accordingly. All other keys in the attrs hashref (where, order_by, join, etc.) are passed through unchanged.

Returns a new hashref -- the input is never modified in place.

If select is absent or contains no -ident items, the returned hashref is a shallow copy of the input with no further changes.

normalise

my ($clean_select, $clean_as) = DBIx::Class::Async::SelectNormaliser->normalise(
    select => \@select_items,
    as     => \@as_items,        # may be empty or shorter than select
);

Lower-level method. Accepts select and as arrays directly and returns two new arrayrefs.

Each item in select is inspected:

{ -ident => $col } or { -ident => $col, -as => $alias }

Rewritten to the bare column string $col in $clean_select. If -as is present and the corresponding position in the incoming as array is not already set, the alias is placed into $clean_as.

Any other form

Passed through to $clean_select unchanged. If the corresponding position in the incoming as array is set, it is preserved in $clean_as. Otherwise the $clean_as slot is left as undef (DBIC omits undef alias entries).

The two returned arrays are always the same length.

_is_ident_hashref

my $bool = _is_ident_hashref($item);

Returns true if $item is a hashref with a -ident key. Returns false for everything else, including function hashrefs like { count => 'me.id' } which happen to also contain a -as key.

The check is intentionally minimal: we only require the presence of -ident. The -as key is optional (the caller may specify aliases via the as array instead).

EXAMPLES

Basic identifier aliasing

# Before normalisation (would produce broken SQL):
select => [ { -ident => 'me.status', -as => 'current_status' } ]

# After normalisation (correct canonical DBIC form):
select => [ 'me.status' ],
as     => [ 'current_status' ],

Mixed select list

# Input
select => [
    'me.id',
    { -ident => 'me.status',  -as => 'current_status' },
    { count  => 'me.id',      -as => 'total'          },  # function -- untouched
    { -ident => 'me.created'                          },  # no inline alias
],
as => [ 'id', undef, 'total', 'created_at' ],             # as takes priority

# Output
select => [ 'me.id', 'me.status', { count => 'me.id', -as => 'total' }, 'me.created' ],
as     => [ 'id', 'current_status', 'total', 'created_at' ],

# Note: slot 1 uses 'current_status' from -as (incoming as[1] was undef).
# Note: slot 3 uses 'created_at' from the as array (it was already set),
#       ignoring the missing -as in the -ident item.

Caller-specified as takes priority

# Input
select => [ { -ident => 'me.col', -as => 'from_ident' } ],
as     => [ 'from_as_array' ],

# Output
select => [ 'me.col' ],
as     => [ 'from_as_array' ],   # as array wins

Scalar select (single column, not an array)

# Input -- normalise() accepts a bare scalar or hashref too
select => { -ident => 'me.status', -as => 'current_status' },
as     => [],

# Output
select => [ 'me.status' ],
as     => [ 'current_status' ],

SEE ALSO

"select" in DBIx::Class::ResultSet

DBIC documentation for the select ResultSet attribute.

"as" in DBIx::Class::ResultSet

DBIC documentation for the as ResultSet attribute.

SQL::Abstract

The underlying SQL generation library. The _select_field method is the code path that does not handle -ident.

AUTHOR

Mohammad Sajid Anwar, <mohammad.anwar at yahoo.com>

REPOSITORY

https://github.com/manwar/DBIx-Class-Async

BUGS

Please report any bugs or feature requests through the web interface at https://github.com/manwar/DBIx-Class-Async/issues. I will be notified and then you'll automatically be notified of progress on your bug as I make changes.

SUPPORT

You can find documentation for this module with the perldoc command.

perldoc DBIx::Class::Async::SelectNormaliser

You can also look for information at:

LICENSE AND COPYRIGHT

Copyright (C) 2026 Mohammad Sajid Anwar.

This program is free software; you can redistribute it and / or modify it under the terms of the the Artistic License (2.0). You may obtain a copy of the full license at: http://www.perlfoundation.org/artistic_license_2_0 Any use, modification, and distribution of the Standard or Modified Versions is governed by this Artistic License.By using, modifying or distributing the Package, you accept this license. Do not use, modify, or distribute the Package, if you do not accept this license. If your Modified Version has been derived from a Modified Version made by someone other than you,you are nevertheless required to ensure that your Modified Version complies with the requirements of this license. This license does not grant you the right to use any trademark, service mark, tradename, or logo of the Copyright Holder. This license includes the non-exclusive, worldwide, free-of-charge patent license to make, have made, use, offer to sell, sell, import and otherwise transfer the Package with respect to any patent claims licensable by the Copyright Holder that are necessarily infringed by the Package. If you institute patent litigation (including a cross-claim or counterclaim) against any party alleging that the Package constitutes direct or contributory patent infringement,then this Artistic License to you shall terminate on the date that such litigation is filed. Disclaimer of Warranty: THE PACKAGE IS PROVIDED BY THE COPYRIGHT HOLDER AND CONTRIBUTORS "AS IS' AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES. THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, OR NON-INFRINGEMENT ARE DISCLAIMED TO THE EXTENT PERMITTED BY YOUR LOCAL LAW. UNLESS REQUIRED BY LAW, NO COPYRIGHT HOLDER OR CONTRIBUTOR WILL BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING IN ANY WAY OUT OF THE USE OF THE PACKAGE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.