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:
selecthashrefs are already used for function calls:{ count => 'me.id', -as => 'cnt' }. Adding-identas 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 namedidentwould be indistinguishable from the operator.The
select/asseparation is deliberate DBIC design:selectis a list of SQL expressions andasis a parallel list of Perl-side aliases. Adding inline-astoselectas well (which DBIC already supports for functions) and now-identwould create three overlapping ways to alias a column, all with subtly different semantics.Changing this in
SQL::Abstractwould 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/asform is unaffected. Callers who prefer the-identform get intuitive, correct behaviour.Is safe to compose. Function hashrefs (
{ count => 'me.id' }) are detected by the absence of-identand passed through untouched, so all existing query patterns continue to work.Is explicit about intent.
-identsays 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 throughsearch(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
$colin$clean_select. If-asis present and the corresponding position in the incomingasarray is not already set, the alias is placed into$clean_as. - Any other form
-
Passed through to
$clean_selectunchanged. If the corresponding position in the incomingasarray is set, it is preserved in$clean_as. Otherwise the$clean_asslot is left asundef(DBIC omitsundefalias 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
selectResultSet attribute. - "as" in DBIx::Class::ResultSet
-
DBIC documentation for the
asResultSet attribute. - SQL::Abstract
-
The underlying SQL generation library. The
_select_fieldmethod 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:
BUG Report
CPAN Ratings
Search MetaCPAN
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.