You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
699 lines
27 KiB
699 lines
27 KiB
package NGCP::Panel::Utils::Datatables;
|
|
use strict;
|
|
use warnings;
|
|
|
|
use Sipwise::Base;
|
|
use NGCP::Panel::Utils::DateTime qw();
|
|
use NGCP::Panel::Utils::Generic qw(:all);
|
|
use List::Util qw/first/;
|
|
use Scalar::Util qw/blessed/;
|
|
|
|
sub process {
|
|
my ($c, $rs, $cols, $row_func, $params) = @_;
|
|
|
|
$params //= {};
|
|
my $total_row_func = $params->{total_row_func};
|
|
my $use_rs_cb = ('CODE' eq (ref $rs));
|
|
my $aaData = [];
|
|
my $totalRecords = 0;
|
|
my $displayRecords = 0;
|
|
my $totalRecordCountClipped = 0;
|
|
my $displayRecordCountClipped = 0;
|
|
my $aggregate_cols = [];
|
|
my $aggregations = {};
|
|
|
|
my $user_tz = $c->session->{user_tz};
|
|
|
|
# check if we need to join more tables
|
|
# TODO: can we nest it deeper than once level?
|
|
set_columns($c, $cols);
|
|
my $totalRecords_rs = $rs;
|
|
$rs = _resolve_joins($rs,$cols,$aggregate_cols) if (!$use_rs_cb);
|
|
|
|
#all joins already implemented, and filters aren't applied. But count we will take only if there are search and no other aggregations
|
|
#= $use_rs_cb ? 0 : $rs->count;
|
|
|
|
### Search processing section
|
|
|
|
($rs,my @searchColumns) = _apply_search_filters($c,$rs,$cols,$use_rs_cb,$params->{extra_or},$params->{extra_or_descr});
|
|
|
|
my $is_set_operations = 0;
|
|
($displayRecords, $displayRecordCountClipped, $is_set_operations) = _get_count_safe($c,$rs,$params) if (!$use_rs_cb);
|
|
#my $footer = ((scalar @$aggregate_cols) > 0 and $displayRecordCountClipped);
|
|
#$aggregate_cols = [] if $displayRecordCountClipped;
|
|
if(@$aggregate_cols){
|
|
unless ($displayRecordCountClipped or $displayRecords == 0) {
|
|
my(@select, @as);
|
|
if(!$use_rs_cb){
|
|
push @select, { 'count' => '*', '-as' => 'count' };
|
|
push @as, 'count';
|
|
}
|
|
foreach my $col (@$aggregate_cols){
|
|
my $col_accessor = $col->{literal_sql} ? \[ $col->{literal_sql} ] : $col->{accessor} ;#_get_joined_column_name_($col->{name});
|
|
push @select, { $col->{show_total} => $col_accessor, '-as' => $col->{accessor} };
|
|
push @as, $col->{accessor};
|
|
}
|
|
my $aggregate_rs = $rs->search_rs(undef,{
|
|
'select' => \@select,
|
|
'as' => \@as,
|
|
});
|
|
if(my $row = $aggregate_rs->first){
|
|
if(!$use_rs_cb){
|
|
$displayRecords = $row->get_column('count');
|
|
}
|
|
foreach my $col (@$aggregate_cols){
|
|
$aggregations->{$col->{accessor}} = $row->get_column($col->{accessor});
|
|
}
|
|
}
|
|
if (defined $total_row_func) {
|
|
$aggregations = {%{$aggregations}, $total_row_func->($aggregations) };
|
|
}
|
|
} else {
|
|
foreach my $col (@$aggregate_cols){
|
|
$aggregations->{$col->{accessor}} = '~';
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!$use_rs_cb) {
|
|
if (@searchColumns) {
|
|
($totalRecords, $totalRecordCountClipped) = _get_count_safe($c,$totalRecords_rs,$params);
|
|
} else {
|
|
($totalRecords,$totalRecordCountClipped) = ($displayRecords,$displayRecordCountClipped);
|
|
}
|
|
}
|
|
|
|
# show specific row on top (e.g. if we come back from a newly created entry)
|
|
my $topId = $c->request->params->{iIdOnTop};
|
|
if(defined $topId) {
|
|
if(defined(my $row = $rs->find($topId))) {
|
|
push @{ $aaData }, _prune_row($user_tz, $cols, $row->get_inflated_columns);
|
|
if (defined $row_func) {
|
|
$aaData->[-1] = {%{$aaData->[-1]}, $row_func->($row)};
|
|
}
|
|
$rs = $rs->search({ 'me.id' => { '!=', $topId} });
|
|
}
|
|
}
|
|
|
|
# sorting
|
|
my $sortColumn = $c->request->params->{iSortCol_0};
|
|
my $sortDirection = $c->request->params->{sSortDir_0} || 'asc';
|
|
my $sortName;
|
|
if(defined $sortColumn && defined $sortDirection && ! $use_rs_cb) {
|
|
if('desc' eq lc $sortDirection) {
|
|
$sortDirection = 'desc';
|
|
} else {
|
|
$sortDirection = 'asc';
|
|
}
|
|
|
|
# first, get the fields we're actually showing
|
|
my @displayedFields = ();
|
|
for my $col(@{ $cols }) {
|
|
next if $col->{no_column};
|
|
next unless $col->{title};
|
|
my $name = get_column_order_name($col);
|
|
push @displayedFields, $name;
|
|
}
|
|
# ... and pick the name defined by the dt index
|
|
$sortName = $displayedFields[$sortColumn];
|
|
|
|
$rs = $rs->search(undef, {
|
|
order_by => {
|
|
"-$sortDirection" => $sortName,
|
|
}
|
|
});
|
|
}
|
|
#/ sorting
|
|
|
|
# pagination
|
|
my $pageStart = $c->request->params->{iDisplayStart};
|
|
my $pageSize = $c->request->params->{iDisplayLength};
|
|
my $searchString = $c->request->params->{sSearch} // "";
|
|
my @rows = ();
|
|
if ($use_rs_cb) {
|
|
($rs, $totalRecords, $displayRecords) = $rs->(
|
|
offset => $pageStart || 0,
|
|
rows => $pageSize || 5,
|
|
searchstring => $searchString,
|
|
);
|
|
@rows = $rs->all;
|
|
} else {
|
|
if($displayRecords > 0 and defined $pageStart and defined $pageSize and $pageSize > 0) {
|
|
if ($is_set_operations and $displayRecordCountClipped) { # and defined $params->{count_limit} and $params->{count_limit} > 0) { #and $displayRecordCountClipped) {
|
|
my ($stmt, @bind_vals) = @{${$totalRecords_rs->as_query}};
|
|
($is_set_operations,$stmt) = _limit_set_queries($stmt,sub {
|
|
my $part_stmt = shift;
|
|
if ($sortName) {
|
|
$part_stmt .= ' order by ' . $sortName . ' ' . $sortDirection;
|
|
}
|
|
return $part_stmt . ' limit ' . $params->{count_limit};
|
|
});
|
|
@bind_vals = map { $_->[1]; } @bind_vals;
|
|
$c->log->debug("page stmt: " . $stmt);
|
|
$c->log->debug("page stmt bind: " . join(",",@bind_vals));
|
|
my $attrs = $totalRecords_rs->_resolved_attrs;
|
|
$rs = $totalRecords_rs->result_source->resultset->search(undef, {
|
|
alias => $totalRecords_rs->current_source_alias,
|
|
from => [{
|
|
$totalRecords_rs->current_source_alias => \[ $stmt, @bind_vals ],
|
|
-alias => $totalRecords_rs->current_source_alias,
|
|
-source_handle => $totalRecords_rs->result_source->handle,
|
|
}],
|
|
columns => $attrs->{as},
|
|
result_class => $rs->result_class,
|
|
});
|
|
$rs = _resolve_joins($rs,$cols);
|
|
if ($sortName) {
|
|
$rs = $rs->search(undef, {
|
|
order_by => {
|
|
"-$sortDirection" => $sortName,
|
|
}
|
|
});
|
|
}
|
|
}
|
|
$rs = $rs->search(undef, {
|
|
offset => $pageStart,
|
|
rows => $pageSize,
|
|
});
|
|
}
|
|
#for case $displayRecords < 0, that means All. And for all other cases too.
|
|
@rows = $rs->all if $displayRecords;
|
|
}
|
|
|
|
for my $row (@rows) {
|
|
push @{ $aaData }, _prune_row($user_tz, $cols, $row->get_inflated_columns);
|
|
if (defined $row_func) {
|
|
$aaData->[-1] = {%{$aaData->[-1]}, $row_func->($row)} ;
|
|
}
|
|
}
|
|
|
|
if (keys %{ $aggregations }) {
|
|
$c->stash(dt_custom_footer => $aggregations);
|
|
}
|
|
|
|
add_arbitrary_data($c, $aaData, $params->{topData}, $cols, $row_func, $params);
|
|
|
|
expose_data($c, $aaData, $totalRecords, $totalRecordCountClipped, $displayRecords, $displayRecordCountClipped);
|
|
|
|
}
|
|
|
|
sub apply_dt_joins_filters {
|
|
my ($c,$rs, $cols, $extra_or, $extra_or_descr) = @_;
|
|
$rs = _resolve_joins($rs, $cols, undef, 1, 1);
|
|
($rs,my @searchColumns) = _apply_search_filters($c, $rs, $cols, $extra_or, $extra_or_descr);
|
|
return $rs;
|
|
}
|
|
|
|
sub _resolve_joins {
|
|
|
|
my ($rs, $cols, $aggregate_cols, $skip_aggregates,$join_only) = @_;
|
|
for my $col(@{ $cols }) {
|
|
next unless $col->{name};
|
|
if ($col->{show_total}) {
|
|
push(@$aggregate_cols, $col) if defined $aggregate_cols;
|
|
next if $skip_aggregates;
|
|
}
|
|
my @parts = split /\./, $col->{name};
|
|
if($col->{literal_sql}) {
|
|
$rs = $rs->search_rs(undef, {
|
|
$col->{join} ? ( join => $col->{join} ) : (),
|
|
($col->{no_column} or $join_only) ? () : (
|
|
'+select' => { '' => \[$col->{literal_sql}], -as => $col->{accessor} },
|
|
'+as' => [ $col->{accessor} ],
|
|
)
|
|
});
|
|
} elsif( @parts > 1 ) {
|
|
my $join = $parts[$#parts-1];
|
|
foreach my $table(reverse @parts[0..($#parts-2)]){
|
|
$join = { $table => $join };
|
|
}
|
|
$rs = $rs->search_rs(undef, {
|
|
join => $join,
|
|
($join_only ? () : ('+select' => [ $parts[($#parts-1)].'.'.$parts[$#parts] ],
|
|
'+as' => [ $col->{accessor} ],)),
|
|
});
|
|
}
|
|
}
|
|
return $rs;
|
|
|
|
}
|
|
|
|
sub get_search_string_pattern {
|
|
|
|
my ($c,$no_pattern) = @_;
|
|
my $searchString = $c->request->params->{sSearch} // "";
|
|
my $is_pattern = 0;
|
|
return ($searchString,$is_pattern) if $no_pattern;
|
|
my $searchString_escaped = join('',map {
|
|
my $token = $_;
|
|
if ($token ne '\\\\') {
|
|
$token =~ s/%/\\%/g;
|
|
$token =~ s/_/\\_/g;
|
|
if ($token =~ s/(?<!\\)\*/%/g) {
|
|
$is_pattern = 1;
|
|
}
|
|
$token =~ s/\\\*/*/g;
|
|
}
|
|
$token;
|
|
} split(/(\\\\)/,$searchString,-1));
|
|
if (not $is_pattern and not $no_pattern) {
|
|
$searchString_escaped .= '%';
|
|
$is_pattern = 1;
|
|
}
|
|
return ($searchString_escaped,$is_pattern);
|
|
|
|
}
|
|
|
|
sub _apply_search_filters {
|
|
|
|
my ($c,$rs,$cols,$use_rs_cb,$extra_or,$extra_or_descr) = @_;
|
|
# generic searching
|
|
my @searchColumns = ();
|
|
my %conjunctSearchColumns = ();
|
|
#processing single search input - group1 from groups to be joined by 'AND'
|
|
my $searchString = $c->request->params->{sSearch} // "";
|
|
if (length($searchString) && !$use_rs_cb) {
|
|
#for search string from one search input we need to check all columns which contain the 'search' spec (now: qw/search search_lower_column search_upper_column/). so, for example user entered into search input ip address - we don't know that it is ip address, so we check that name like search OR id like search OR search is between network_lower_value and network upper value
|
|
foreach my $col(@{ $cols }) {
|
|
next unless $col->{name};
|
|
my ($name,$search_value,$op,$convert);
|
|
# avoid amigious column names if we have the same column in different joined tables
|
|
if($col->{search} or $col->{strict_search} or $col->{int_search}){
|
|
my $is_pattern = 0;
|
|
(my $searchString_escaped, $is_pattern) = get_search_string_pattern($c,(
|
|
$col->{strict_search} || $col->{int_search}
|
|
));
|
|
if ($is_pattern) {
|
|
$op = 'like';
|
|
$search_value = $searchString_escaped;
|
|
} elsif ($col->{strict_search}) {
|
|
$op = '=';
|
|
$searchString_escaped = $searchString;
|
|
$searchString_escaped =~ s/\\\*/*/g;
|
|
$searchString_escaped =~ s/\\\\/\\/g;
|
|
$search_value = $searchString_escaped;
|
|
} elsif ($col->{int_search}) {
|
|
$op = '=';
|
|
$search_value = $searchString;
|
|
} else {
|
|
$op = 'like';
|
|
$search_value = '%' . $searchString_escaped . '%';
|
|
}
|
|
$name = _get_joined_column_name_($col->{name});
|
|
$op = $col->{comparison_op} if (defined $col->{comparison_op});
|
|
$search_value = $col->{convert_code}->($searchString) if (ref $col->{convert_code} eq 'CODE');
|
|
my $stmt;
|
|
if (defined $search_value) {
|
|
if ($col->{literal_sql}) {
|
|
if (!ref $col->{literal_sql}) {
|
|
#we can't use just accessor because of the count query
|
|
$stmt = \[$col->{literal_sql} . " $op ?", [ {} => $search_value] ];
|
|
} else {
|
|
if ($col->{literal_sql}->{format}) {
|
|
$stmt = \[sprintf($col->{literal_sql}->{format}, " $op ?"), [ {} => $search_value] ];
|
|
}
|
|
}
|
|
} elsif (not $col->{int_search} or $searchString =~ /^\d{1,10}$/) {
|
|
$stmt = { $name => { $op => $search_value } };
|
|
}
|
|
}
|
|
if ($stmt) {
|
|
push @{$searchColumns[0]}, $stmt;
|
|
}
|
|
} elsif ( $col->{search_lower_column} || $col->{search_upper_column} ) {
|
|
# searching lower and upper limit columns
|
|
foreach my $search_spec (qw/search_lower_column search_upper_column/){
|
|
if ($col->{$search_spec}) {
|
|
$op = (defined $col->{comparison_op} ? $col->{comparison_op} : ( $search_spec eq 'search_lower_column' ? '<=' : '>=') );
|
|
$name = _get_joined_column_name_($col->{name});
|
|
$search_value = (ref $col->{convert_code} eq 'CODE') ? $col->{convert_code}->($searchString) : $searchString ;
|
|
if (defined $search_value) {
|
|
$conjunctSearchColumns{$col->{$search_spec}} = [] unless exists $conjunctSearchColumns{$col->{$search_spec}};
|
|
push(@{$conjunctSearchColumns{$col->{$search_spec}}},{$name => { $op => $search_value }});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
foreach my $conjunct_column (keys %conjunctSearchColumns) {
|
|
#...things in arrays are OR'ed, and things in hashes are AND'ed
|
|
push @{$searchColumns[0]}, { map { %{$_} } @{$conjunctSearchColumns{$conjunct_column}} };
|
|
}
|
|
push @{$searchColumns[0]}, @{$extra_or} if $extra_or;
|
|
}
|
|
#/processing single search input
|
|
#processing dates search input - group2 from groups to be joined by 'AND'
|
|
{
|
|
# date-range searching
|
|
my $from_date_in = $c->request->params->{sSearch_0} // "";
|
|
my $to_date_in = $c->request->params->{sSearch_1} // "";
|
|
my($from_date,$to_date);
|
|
if($from_date_in) {
|
|
$from_date = NGCP::Panel::Utils::DateTime::from_forminput_string($from_date_in, $c->session->{user_tz});
|
|
}
|
|
if($to_date_in) {
|
|
$to_date = NGCP::Panel::Utils::DateTime::from_forminput_string($to_date_in, $c->session->{user_tz});
|
|
}
|
|
foreach my $col(@{ $cols }) {
|
|
next unless $col->{name};
|
|
# avoid amigious column names if we have the same column in different joined tables
|
|
my $name = _get_joined_column_name_($col->{name});
|
|
if($col->{search_from_epoch} && $from_date) {
|
|
push @searchColumns, { $name => { '>=' => $col->{search_use_datetime} ? $from_date_in : $from_date->epoch } };
|
|
}
|
|
if($col->{search_to_epoch} && $to_date) {
|
|
push @searchColumns, { $name => { '<=' => $col->{search_use_datetime} ? $to_date_in : $to_date->epoch } };
|
|
}
|
|
}
|
|
}
|
|
#/processing dates search input
|
|
if(@searchColumns){
|
|
$rs = $rs->search_rs({
|
|
"-and" => [@searchColumns],
|
|
});
|
|
}
|
|
### /Search processing section
|
|
|
|
return ($rs,@searchColumns);
|
|
|
|
}
|
|
|
|
sub _get_count_safe {
|
|
my ($c,$rs,$params) = @_;
|
|
my $count_limit = $params->{count_limit};
|
|
#$count_limit = 12;
|
|
my $is_set_operations;
|
|
if ($c and defined $count_limit and $count_limit > 0) {
|
|
#use Data::Dumper;
|
|
#$c->log->debug("count_limit: $count_limit " . Dumper($params));
|
|
my ($count_clipped) = $c->model('DB')->storage->dbh_do(sub {
|
|
my ($storage, $dbh, $stmt, @bind_vals) = @_;
|
|
$c->log->debug("entered dbdo");
|
|
($is_set_operations,$stmt) = _limit_set_queries($stmt,sub {
|
|
my $part_stmt = shift;
|
|
return $part_stmt . ' limit ' . ($count_limit + 1);
|
|
});
|
|
@bind_vals = map { $_->[1]; } @bind_vals;
|
|
$c->log->debug("count stmt: " . "select count(1) from ($stmt) as query_clipped");
|
|
$c->log->debug("count stmt bind: " . join(",",@bind_vals));
|
|
return $dbh->selectrow_array("select count(1) from ($stmt) as query_clipped",undef,@bind_vals);
|
|
},@{${$rs->search_rs(undef,{
|
|
page => 1,
|
|
rows => ($count_limit + 1),
|
|
#below is required if fields with identical name are selected by $rs:
|
|
'select' => (defined $params->{count_projection_column} ? $params->{count_projection_column} : \"1"),
|
|
#select => $rs->_resolved_attrs->{select},
|
|
#as => $rs->_resolved_attrs->{as},
|
|
})->as_query}});
|
|
if ($count_clipped > $count_limit) {
|
|
$c->log->debug("result count clipped");
|
|
return ($count_limit,1,$is_set_operations);
|
|
} else {
|
|
$c->log->debug("result count not clipped");
|
|
return ($count_clipped,0,$is_set_operations);
|
|
}
|
|
} else {
|
|
return ($rs->count,0,$is_set_operations);
|
|
}
|
|
}
|
|
|
|
sub add_arbitrary_data {
|
|
my ($c, $aaData, $topData, $cols, $row_func, $params) = @_;
|
|
# show any arbitrary data rows on top, just like a union would do
|
|
# hash is expected or array of hashes expected
|
|
my $user_tz = $params->{user_tz} // $c->session->{user_tz};
|
|
if (defined $topData) {
|
|
my $topDataArray;
|
|
if (ref $topData eq 'HASH') {
|
|
$topDataArray = [$topData];
|
|
} else {
|
|
$topDataArray = $topData;
|
|
}
|
|
foreach my $topDataRow (@$topDataArray) {
|
|
unshift @{ $aaData }, _prune_row($user_tz, $cols, %$topDataRow);
|
|
if (defined $row_func) {
|
|
$aaData->[0] = {%{$aaData->[0]}, $row_func->($topDataRow)};
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
sub expose_data {
|
|
my($c, $aaData, $totalRecords, $totalRecordCountClipped, $displayRecords, $displayRecordCountClipped) = @_;
|
|
$c->stash(
|
|
aaData => $aaData,
|
|
iTotalRecords => $totalRecords,
|
|
iTotalDisplayRecords => $displayRecords,
|
|
iTotalRecordCountClipped => ($totalRecordCountClipped ? \1 : \0),
|
|
iTotalDisplayRecordCountClipped => ($displayRecordCountClipped ? \1 : \0),
|
|
sEcho => int($c->request->params->{sEcho} // 1),
|
|
);
|
|
}
|
|
|
|
sub process_static_data {
|
|
my ($c, $data, $cols, $row_func, $params) = @_;
|
|
$params //= {};
|
|
|
|
foreach my $field (qw/total_row_func/) {
|
|
#todo: error here about unsupported functionality
|
|
}
|
|
|
|
my $aaData = [];
|
|
add_arbitrary_data($c, $aaData, $data, $cols, $row_func, $params);
|
|
my $totalRecords = scalar @$aaData;
|
|
my $displayRecords = $totalRecords;
|
|
expose_data($c, $aaData, $totalRecords, 0, $displayRecords, 0);
|
|
}
|
|
|
|
sub set_columns {
|
|
my ($c, $cols) = @_;
|
|
|
|
for my $col(@{ $cols }) {
|
|
next if defined $col->{accessor};
|
|
next unless $col->{name};
|
|
$col->{accessor} = $col->{name};
|
|
$col->{accessor} =~ s/\./_/g;
|
|
}
|
|
return $cols;
|
|
}
|
|
|
|
sub _prune_row {
|
|
my ($user_tz, $columns, %row) = @_;
|
|
while (my ($k,$v) = each %row) {
|
|
unless (first { !$_->{no_column} && $_->{accessor} eq $k && ($_->{title} || $_->{field}) } @{ $columns }) {
|
|
delete $row{$k};
|
|
next;
|
|
}
|
|
if(blessed($v) && $v->isa('DateTime')) {
|
|
if($user_tz) {
|
|
$v->set_time_zone('local'); # starting point for conversion
|
|
$v->set_time_zone($user_tz); # desired time zone
|
|
}
|
|
$row{$k} = $v->ymd('-') . ' ' . $v->hms(':');
|
|
$row{$k} .= '.'.sprintf("%03d",$v->millisecond) if $v->millisecond > 0.0;
|
|
}
|
|
}
|
|
return { %row };
|
|
}
|
|
|
|
|
|
sub get_column_order_name{
|
|
my $col = shift;
|
|
my $name;
|
|
if($col->{literal_sql}){
|
|
$name = $col->{accessor};
|
|
}else{
|
|
$name = _get_joined_column_name_($col->{name});
|
|
}
|
|
return $name;
|
|
}
|
|
|
|
sub _get_joined_column_name {
|
|
my $cname = shift;
|
|
my $name;
|
|
if($cname !~ /\./) {
|
|
if ($cname !~ /^v_(max|min|count)_/) {
|
|
$name = 'me.'.$cname;
|
|
} else { # virtual agrregated columns (count, min, max)
|
|
$name = $cname;
|
|
}
|
|
} else {
|
|
my @parts = split /\./, $cname;
|
|
if(@parts == 2) {
|
|
$name = $cname;
|
|
} elsif(@parts == 3) {
|
|
$name = $parts[1].'.'.$parts[2];
|
|
} elsif(@parts == 4) {
|
|
#I can suggest that in this case parts[1]may be schema name. If it isn't, then it will be incorrect sql for example for order (and in other cases too). But I didn't see schema names usage in tt (at least accounting or billing). So, switched to new sub.
|
|
$name = $parts[1].'.'.$parts[2].'.'.$parts[3];
|
|
} else {
|
|
# TODO throw an error for now as we only support one and two level
|
|
$name = join('.',@parts[1 .. $#parts]);
|
|
}
|
|
}
|
|
return $name;
|
|
}
|
|
|
|
sub _get_joined_column_name_{
|
|
my $cname = shift;
|
|
my $name;
|
|
if($cname !~ /\./) {
|
|
if ($cname !~ /^v_(max|min|count)_/) {
|
|
$name = 'me.'.$cname;
|
|
} else { # virtual agrregated columns (count, min, max)
|
|
$name = $cname;
|
|
}
|
|
} else {
|
|
my @parts = split /\./, $cname;
|
|
if(@parts == 2){
|
|
$name = $cname;
|
|
}else{
|
|
$name = join('.',@parts[($#parts-1) .. $#parts]);
|
|
}
|
|
}
|
|
return $name;
|
|
}
|
|
|
|
sub _limit_set_queries {
|
|
my ($stmt,$sub) = @_;
|
|
return (undef,$stmt) unless $sub;
|
|
#simple lexer for parsing sql stmts with a single level of set operations.
|
|
#caveat: set operator names must not appear in colnames, table names, literals etc.
|
|
my $set_operation_re = "union\\s+distinct|union\\s+all|union|intersect|except";
|
|
my @frags = split(/\s($set_operation_re)\s/i,$stmt, -1);
|
|
return (0,$stmt) unless (scalar @frags) > 1;
|
|
my @frags_rebuilt = ();
|
|
|
|
my ($preamble,$postamble) = (undef,undef);
|
|
foreach my $frag (@frags) {
|
|
if ($frag =~ /($set_operation_re)/i) {
|
|
push(@frags_rebuilt,$1);
|
|
} else {
|
|
my $set_stmt = $frag;
|
|
$set_stmt =~ s/\s+$//g;
|
|
$set_stmt =~ s/^\s+//g;
|
|
my $last = (((scalar @frags) - (scalar @frags_rebuilt)) == 1 ? 1 : 0);
|
|
my $first = ((scalar @frags_rebuilt) == 0 ? 1 : 0);
|
|
my $quoted = 0;
|
|
my $depth = 0;
|
|
my ($left_parenthesis_count,$right_parenthesis_count) = (0,0);
|
|
my $rebuilt = '';
|
|
my $balanced;
|
|
if ($last) {
|
|
for (my $i = 0; $i < length($set_stmt); $i++) {
|
|
my $char = substr($set_stmt, $i, 1);
|
|
my $escape = substr($set_stmt, $i, 2);
|
|
if ($escape eq '\\\\' or $escape eq '\\"' or $escape eq "\\'") {
|
|
$rebuilt .= $escape;
|
|
$i++;
|
|
} else {
|
|
if ($char eq "'" or $char eq '"') {
|
|
$quoted = ($quoted ? 0 : 1);
|
|
} elsif (not $quoted and $char eq ')') {
|
|
last if ($depth == 0);
|
|
$depth--;
|
|
$right_parenthesis_count++;
|
|
} elsif (not $quoted and $char eq '(') {
|
|
$depth++;
|
|
$left_parenthesis_count++;
|
|
}
|
|
$rebuilt .= $char;
|
|
}
|
|
if ($left_parenthesis_count == $right_parenthesis_count and $depth == 0) {
|
|
$balanced = $rebuilt;
|
|
}
|
|
}
|
|
$postamble = substr($set_stmt,length($balanced));
|
|
} else {
|
|
for (my $i = length($set_stmt) - 1; $i >= 0; $i--) {
|
|
my $char = substr($set_stmt, $i, 1);
|
|
my $escape = substr($set_stmt, $i - 1, 2);
|
|
if ($escape eq '\\\\' or $escape eq '\\"' or $escape eq "\\'") {
|
|
$rebuilt = $escape . $rebuilt;
|
|
$i--;
|
|
} else {
|
|
if ($char eq "'" or $char eq '"') {
|
|
$quoted = ($quoted ? 0 : 1);
|
|
} elsif (not $quoted and $char eq ')') {
|
|
$depth--;
|
|
$right_parenthesis_count++;
|
|
} elsif (not $quoted and $char eq '(') {
|
|
$depth++;
|
|
$left_parenthesis_count++;
|
|
}
|
|
$rebuilt = $char . $rebuilt;
|
|
}
|
|
if ($left_parenthesis_count == $right_parenthesis_count and $depth == 0) {
|
|
$balanced = $rebuilt;
|
|
last if ($first and not $quoted and 'select' eq lc(substr($rebuilt,0,6)));
|
|
}
|
|
}
|
|
if ($first) {
|
|
$preamble = substr($set_stmt,0, length($set_stmt) - length($balanced));
|
|
}
|
|
}
|
|
#normalize outer parentheses for easier handling in $sub:
|
|
while ($balanced =~ /^\s*\(\s*/g and $balanced =~ /\s*\)\s*$/g) {
|
|
$balanced =~ s/^\s*\(\s*//g;
|
|
$balanced =~ s/\s*\)\s*$//g;
|
|
}
|
|
$balanced = &$sub($balanced);
|
|
push(@frags_rebuilt,'(' . $balanced . ')');
|
|
}
|
|
}
|
|
unshift(@frags_rebuilt,$preamble) if $preamble;
|
|
push(@frags_rebuilt,$postamble) if $postamble;
|
|
|
|
#my $i=0;
|
|
#print(join("\n",map { $i++;$i.'. '.$_; } @frags_rebuilt));
|
|
return (1,join(' ',@frags_rebuilt));
|
|
|
|
}
|
|
|
|
1;
|
|
|
|
|
|
__END__
|
|
|
|
=encoding UTF-8
|
|
|
|
=head1 NAME
|
|
|
|
NGCP::Panel::Utils::Datatables
|
|
|
|
=head1 DESCRIPTION
|
|
|
|
=head2 Format of Columns
|
|
|
|
Array with the following fields (preprocessed by set_columns):
|
|
name: String
|
|
search: Boolean
|
|
search_from_epoch: Boolean
|
|
search_to_epoch: Boolean
|
|
title: String (Should be localized)
|
|
show_total: String (if set, something like sum,max,...)
|
|
|
|
|
|
=head1 METHODS
|
|
|
|
=head2 C<process>
|
|
|
|
Query DB on datatables ajax request.
|
|
|
|
Format of the resultset callback (if used):
|
|
Arguments as hash
|
|
ARGUMENTS: offset, rows, searchstring
|
|
RETURNS: ($rs, $totalcount, $displaycount)
|
|
|
|
=head1 AUTHOR
|
|
|
|
Gerhard Jungwirth C<< <gjungwirth@sipwise.com> >>
|
|
|
|
=head1 LICENSE
|
|
|
|
This library is free software. You can redistribute it and/or modify
|
|
it under the same terms as Perl itself.
|
|
|
|
|
|
# vim: set tabstop=4 expandtab:
|