Alias the sum of two columns in a DBIx::Class resultset - perl

SELECT me.id, me.date_created, me.date_updated, me.yes,
me.name, me.description, me.currency, me.locked, me.skip,
me.uri_part, me.user_id,
yes + currency as weight
FROM ideas me having ((weight < 5)) order by weight;
How can I generate that query in DBIx::Class without using literal SQL like this:
my $query = $rs->search({},
{
'+select' => \[
'yes + currency as weight',
],
rows => 1,
order_by => { -desc => [qw/weight name/] },
having => {
weight => { '<' => $self->yes + $self->currency },
},
});
use Data::Dumper;
warn Dumper($query->as_query);
I tried using -as, however, it seems to only be useful for working with columns generated from functions, as this:
'+select' => {
'yes + currency', '-as' => 'weight'
}
generates an error
"Odd number of elements in anonymous hash at
/data/TGC/lib/TGC/DB/Result/Idea.pm line 105, line 1000.
DBIx::Class::SQLMaker::_recurse_fields(): Malformed select argument -
too many keys in hash: SCALAR(0xbf14c40),weight"

Probably the most idiomatic thing I can think of in SQL Abstract expression, without straining too hard:
#!/usr/bin/env perl
use Modern::Perl;
use MySchema;
use Data::Dumper;
my $schema = MySchema->connect('dbi:SQLite:example.db');
my $rs = $schema->resultset('Sample')->search(
{
weight => { '<' => 5 },
},
{
'+select' => [
{ '' => \'`me`.`yes` + `me`.`currency`', -as => 'weight' }
]
}
);
say Dumper( $rs->as_query() );
Which is a contrived wrapping of the column names, but it does the job, sort of. Just don't know of any way to abstract the + here. But stil:
'(SELECT me.name, me.yes, me.currency, ( me.yes + me.currency ) AS weight FROM sample me WHERE ( weight < ? ))',
Unless you are just going for idiomatic perl, in which case:
{ '' => \(join " + ", qw/`me`.`yes` `me`.`currency`/), -as => 'weight' }
But either way seems a little contrived considering both forms are longer than the literal string.
Also note that weight is referenced in WHERE and not HAVING which is because that will blow up on various SQL engines.

The '+as' should be at the same level as '+select'
$idea = $rs->search({-and => [
name => { '<' => $self->name },
]},
{
'+select' => [
'yes + currency'
],
'+as' => [qw/weight/],
rows => 1,
order_by => ['weight', 'name'],
having => {
weight => { '<' => $self->yes + $self->currency },
},
})->single;

Related

Inserting one hash into another using Perl

I've tried many different versions of using push and splice, but can't seem to combine two hashes as needed. Trying to insert the second hash into the first inside the 'Item' array:
(
ItemData => { Item => { ItemNum => 2, PriceList => "25.00", UOM => " " } },
)
(
Alternate => {
Description => "OIL FILTER",
InFile => "Y",
MfgCode => "FRA",
QtyAvailable => 29,
Stocked => "Y",
},
)
And I need to insert the second 'Alternate' hash into the 'Item' array of the first hash for this result:
(
ItemData => {
Item => {
Alternate => {
Description => "OIL FILTER",
InFile => "Y",
MfgCode => "FRA",
QtyAvailable => 29,
Stocked => "Y",
},
ItemNum => 2,
PriceList => "25.00",
UOM => " ",
},
},
)
Can someone suggest how I can accomplish this?
Assuming you have two hash references, this is straight-forward.
my $item = {
'ItemData' => {
'Item' => {
'PriceList' => '25.00',
'UOM' => ' ',
'ItemNum' => '2'
}
}
};
my $alt = {
'Alternate' => {
'MfgCode' => 'FRA',
'Description' => 'OIL FILTER',
'Stocked' => 'Y',
'InFile' => 'Y',
'QtyAvailable' => '29'
}
};
$item->{ItemData}->{Item}->{Alternate} = $alt->{Alternate};
The trick here is not to actually merge $alt into some part of $item, but to only take the specific part you want and put it where you want it. We take the Alternate key from $alt and put it's content into a new Alternate key inside the guts of $item.
Adam Millerchip pointed out in a hence deleted comment that this is not a copy. If you alter any of the keys inside of $alt->{Alternative} after sticking it into $item, the data will be changed inside of $item as well because we are dealing with references.
$item->{ItemData}->{Item}->{Alternate} = $alt->{Alternate};
$alt->{Alternate}->{InFile} = 'foobar';
This will actually also change the value of $item->{ItemData}->{Item}->{Alternate}->{InFile} to foobar as seen below.
$VAR1 = {
'ItemData' => {
'Item' => {
'ItemNum' => '2',
'Alternate' => {
'Stocked' => 'Y',
'MfgCode' => 'FRA',
'InFile' => 'foobar',
'Description' => 'OIL FILTER',
'QtyAvailable' => '29'
},
'UOM' => ' ',
'PriceList' => '25.00'
}
}
};
References are supposed to do that, because they only reference something. That's what's good about them.
To make a real copy, you need to dereference and create a new anonymous hash reference.
# create a new ref
# deref
$item->{ItemData}->{Item}->{Alternate} = { %{ $alt->{Alternate} } };
This will create a shallow copy. The values directly inside of the Alternate key will be copies, but if they contain references, those will not be copied, but referenced.
If you do want to merge larger data structures where more than the content of one key needs to be merged, take a look at Hash::Merge instead.

DBIx::Class: How can I sort on multiple substrings of a column?

I have a SQLite-database with table with a document number following this schema:
16-145-45
16-127-30
16-141-42
16-122-14
15-090-04
15-089-15
15-089-05
I'd like to sort the ResultSet on the first and last part of the number, like this. First, all documents starting with the highest two-digit prefix (16) sorted by the last 2 digits and then the same with the next block, and so on.
16-145-45
16-141-42
16-127-30
16-122-14
15-089-15
15-089-05
15-090-04
Is there a way to do this in DBIx::Class with some sort of custom order_by clause, or what would be the approach?
I have tried the following, which does not work, because the middle part of the number is also considered for sorting:
my #rs = $self->search(undef,
{
order_by => { -desc => 'me.number' }
}
);
If you want the database to sort the results, you have to use literal SQL.
Here's an example for Postgres (I added a space after the backslash to fix the syntax highlighting):
my #rs = $self->search(undef,
{
order_by => \ "split_part(number, '-', 1) || split_part(number, '-', 3) DESC",
}
);
Or, by creating an output column with the +select result set attribute:
my #rs = $self->search(undef,
{
'+select' => [
{ sort_key => \ "split_part(number, '-', 1) || split_part(number, '-', 3)" },
],
'+as' => [ qw(sort_key) ], # Make sort key accessible from DBIC.
order_by => { -desc => 'sort_key' },
}
);
Another approach is to retreive the whole unsorted result set, and sort it on the client side. DBIC doesn't have any specific features to help you with that, so simply use Perl's sort function.
Since the answer from #nwellnhof works like a charm, I just wanted to provide the corresponding syntax for SQLite, which does not know the split_part() function.
# SQL for filtering the doc number in SQLite
my #rs = $self->search(undef,
{
order_by => \ "SUBSTR(me.number, 1, 2) || SUBSTR(me.number, -2, 2) DESC"
}
);
You need to extract additional columns from the result set which are equal to the value of the function that you want to sort by. Then you can just put those columns in an order_by clause as normal
This assumes that your document number field is called docnum. It fetches all the columns from Table plus the two substrings of docnum called docnum1 and docnum3
my $rs = $schema->resultset('Table')->search(undef,
{
'+select' => [
{ substr => [ 'docnum', 1, 2 ], -as => 'docnum1' },
{ substr => [ 'docnum', -2 ], -as => 'docnum3' },
],
order_by => [ { -desc => 'docnum1' }, { -desc => 'docnum3' } ],
}
);

Why do I get a syntax error when I try to print a nested hash that has keys containing colons?

I'm trying to print an element of a nested data structure:
$VAR1 = {
'SOAP:Body' => {
'ns1:MT_DF_AssetMaster_Response' => {
'SUBNUMBER' => {},
'ASSETCREATED' => {
'SUBNUMBER' => {},
'ASSET' => {},
'COMPANYCODE' => {}
},
'RETURN' => {
'PARAMETER' => 'timedependentdata',
'MESSAGE_V2' => {},
'ID' => 'BAPI1022',
'MESSAGE_V1' => 'HW5790',
'ROW' => '0',
'TYPE' => 'E',
'FIELD' => 'plate_no',
'LOG_NO' => {},
'MESSAGE_V3' => {},
'SYSTEM' => 'xxx',
'MESSAGE' => 'Invalid date transferred for field xxx:',
'MESSAGE_V4' => {},
'NUMBER' => '041',
'LOG_MSG_NO' => '000000'
},
'xmlns:ns1' => 'urn:ariba.com:xi:OnDemand:Asset',
'ASSET' => {},
'COMPANYCODE' => {}
}
},
'xmlns:SOAP' => 'http://schemas.xmlsoap.org/soap/envelope/',
'SOAP:Header' => {}
};
print "$data->{SOAP:Body}->{ns1:MT_DF_AssetMaster_Response}->{ASSETCREATED}=>{ASSET}\n";
But I get a syntax error:
syntax error at ./asset_creation.pl line 85, near "{SOAP:"
How can I fix this?
You must write something like this
my $asset = $data->{'SOAP:Body'}{'ns1:MT_DF_AssetMaster_Response'}{ASSETCREATED}{ASSET};
print %$asset ? "Asset is NOT empty\n" : "Asset is empty\n";
The following are the syntaxes for a hash lookup via reference:
REF->{IDENTIFIER}
REF->{EXPR}
ns1:MT_DF_AssetMaster_Response is neither a valid identifier, nor a valid expression. Replace
->{ns1:MT_DF_AssetMaster_Response}
with
->{'ns1:MT_DF_AssetMaster_Response'}
Perl allows an expression inside the subscripts for an array or a hash. For instance, a variable:
$array[ $i ]
Or the result of an addition:
$array[ $i + $offset ]
Or something
$array[ cos( $i ) ]
It's the same with a hash:
$hash{ $key }
The dot is still the string concatenation operator:
$hash{ $key . $prefix }
It's harder to spot when you have barewords smooshed together:
$hash{first.last}
And in this case, the : is still part of the namespace separator. Perl thinks you aren't done with this expression in the braces so it complains:
$hash{ SOAP:Body } # !!! Error
SOAP:Body must be quoted: The : may not be used without quoting as hash key:
print $data->{'SOAP:Body'}->{'ns1:MT_DF_AssetMaster_Response'}
You should also remove the " " around the variable name in your print statement. The hashref-dereferencing doesn't work otherwise. Your output would be something like
HASH(0x123456)->{SOAP:Body}->{ns1:MT_DF_AssetMaster_Response}
...but you may still have another error if line 85 isn't the print command.

Extracting date parts using DBIC while keeping queries database agnostic

I use a MySQL database in production and a SQLite database for running tests. One part of my application is used to gather monthly statistics for a year. I've successfully done this, however it came at a cost of not being able to automate tests because I'm using MySQL specific functions when querying for the data:
my $criteria = {
status => ['Complete'],
'YEAR(completed_on)' => DateTime->now()->year(),
};
my $attributes = {
select => [ { count => 'title' }, 'completed_on' ],
as => [qw/num_completed datetime/],
group_by => [qw/MONTH(completed_on)/],
};
Notice I'm using YEAR and MONTH MySQL functions.
I know one way I can substitute the where clause to eliminate the use of MySQLs YEAR function, something like this:
my $dtf = $schema->storage->datetime_parser;
my $begin_date = DateTime->from_day_of_year( year => DateTime->now()->year(), day_of_year => 1 ); #inception o_O
my $end_date = DateTime->from_day_of_year( year => DateTime->now()->year(), day_of_year => 36[56] );
my $criteria = {
status => ['Complete'],
completed_on =>
-between => [
$dtf->format_datetime($begin_date),
$dtf->format_datetime($end_date),
]
};
Using the recommended way to query date fields using DBIC
But I'm stumped as to what to do with the group_by clause and how to make the grouping of this fields date value by month database agnostic as well. Wondering if anyone has any ideas?
Thanks!
Sometimes you will have to make engine specific code in DBIx::Class if you're trying to do special things. You can use $schema->storage->sqlt_type to make different SQL.
Note you can also use substr(completed_on,1,4) to get the year in SQLite.
This will solve your problem:
my $type = $schema->storage->sqlt_type;
my $criteria;
my $attributes;
if ($type eq 'MySQL') {
$criteria = {
status => ['Complete'],
'YEAR(completed_on)' => DateTime->now()->year(),
};
$attributes = {
select => [ { count => 'title' }, 'completed_on' ],
as => [qw/num_completed datetime/],
group_by => [qw/MONTH(completed_on)/],
};
}
elsif ($type eq 'SQLite') {
my $dtf = $schema->storage->datetime_parser;
my $begin_date = DateTime->from_day_of_year( year => DateTime->now()->year(), day_of_year => 1 ); #inception o_O
my $end_date = DateTime->from_day_of_year( year => DateTime->now()->year() + 1, day_of_year => 1 )->add( seconds => -1 );
$criteria = {
status => ['Complete'],
completed_on => {
-between => [
$dtf->format_datetime($begin_date),
$dtf->format_datetime($end_date),
]
}
};
$attributes = {
select => [ { count => 'title' }, 'completed_on' ],
as => [qw/num_completed datetime/],
group_by => ['substr(completed_on,6,2)'],
};
}

Odd number of elements in Perl hash

I'm having some trouble with hashes. I need to get a list of version with key => value pairs into a hash but kept getting an error. The code just below is my recent attempt. Some of the strings have been changed and unneeded code isn't included.
I've looked all over the net but haven't been able to find anything that can help me. I've used Perl for a long time but haven't used hashes and arrays much. Most of my Perl experience has had to do with regex and shell execution. If I was using PHP, I would just use a multidimensional array but this is Perl and there's a lot more to the script I'm writing than what is shown or I'd switch to PHP.
I appreciate whoever takes the time to help. Thanks!
sub sub1 {
# Read file which populates #rows with each line.
my %data;
for (my $i=2; $i <= scalar #rows - 1; $i++) {
$ver =~ s/\s//m;
$data{ $ver } = [
'version', $ver,
'available', $table_tree->cell($jt,1)->as_text,
'bsupport', $table_tree->cell($jt,2)->as_text,
'ssupport', $table_tree->cell($jt,3)->as_text,
'eol', $table_tree->cell($jt,3)->as_text,
'utype', $table_tree->cell($jt,5)->as_text,
'lreleases', $table_tree->cell($jt,7)->as_text
];
};
return %data;
}
sub check_ {
# line 199 follows
my (%hash) = #_;
print Dumper (\%hash)."\n";
}
my %data = sub1($file);
check_(%data);
Warning:
Odd number of elements in hash assignment at ./file.pl line 199 (#1)
(W misc) You specified an odd number of elements to initialize a hash,
which is odd, because hashes come in key/value pairs.
The %hash when dumped is:
$VAR1 = {
'string1' => [
'version',
'string1',
'available',
'stringa',
'bsupport',
'stringb',
'ssupport',
'stringc',
'eol',
'stringd',
'utype',
'stringe',
'lreleases',
'stringf'
],
'string2' => [
'version',
'string2',
'available',
'stringa',
'bsupport',
'stringb',
'ssupport',
'stringc',
'eol',
'stringd',
'utype',
'stringe',
'lreleases',
'stringf'
],
'string3' => [
'version',
'string3',
'available',
'stringa',
'bsupport',
'stringb',
'ssupport',
'stringc',
'eol',
'stringd',
'utype',
'stringe',
'lreleases',
'stringf'
],
# ...
}
I was originally trying to have my has be as follows. Where $VAR1 = { 'stringN' => { ... } would be any number with any number of key => value pairs but would also get the same error. I had it working but it would always generate the error.
$VAR1 = {
'string1' => {
'version' => 'string1',
'available' => 'stringa',
'bsupport' => 'stringb',
'ssupport' => 'stringc',
'eol' => 'stringd',
'utype' => 'stringe',
'lreleases' => 'stringf'
},
'string2' => {
'version' => 'string2',
'available' => 'stringa',
'bsupport' => 'stringb',
'ssupport' => 'stringc',
'eol' => 'stringd',
'utype' => 'stringe',
'lreleases' => 'stringf'
},
'string3' => {
'version' => 'string3',
'available' => 'stringa',
'bsupport' => 'stringb',
'ssupport' => 'stringc',
'eol' => 'stringd',
'utype' => 'stringe',
'lreleases' => 'stringf'
}
# ...
}
It's because you're using [ which is the anonymous array constructor. Try using { instead.
And it might be a bit more idiomatic if you did:
$data{ $ver } = {
version => $jver,
available => $table_tree->cell($jt,1)->as_text,
};
Oh, and indent your code. That for loop doesn't finish where you (might!) think it does. Especially - check where the return happens. (And what $jt is set to - it appears unrelated to $i)