Red.git | lib/Red/ | ResultSeq.rakumod
use Red::DB;
use Red::AST;
use Red::Utils;
use Red::Model;
use Red::Column;
use Red::Driver;
use Red::AST::Next;
use Red::AST::Case;
use Red::AST::Empty;
use Red::AST::Value;
use Red::AST::Update;
use Red::AST::Delete;
use Red::AST::Comment;
use Red::Attr::Column;
use Red::AST::Infixes;
use Red::AST::Chained;
use Red::AST::Function;
use Red::AST::Select;
use Red::ResultSeqSeq;
use Red::AST::MultiSelect;
use Red::ResultAssociative;
use Red::ResultSeq::Iterator;
use Red::HiddenFromSQLCommenting;
use X::Red::Exceptions;
use Red::PrepareCode;
use Red::Phaser;
use Red::ResultSeqMethods;
use Red::Formatter::ResultSeq;
use Red::Formatter::ResultSeq::Vertical;
use Red::LockType;
=head2 Red::ResultSeq
#| Class that represents a Seq of query results
unit role Red::ResultSeq[Mu $of = Any];
also does Sequence;
also does Positional;
also does Red::ResultSeqMethods;
method format(Red::Formatter::ResultSeq:U $formatter = $*RED-FORMATTER-RESULTSEQ // Red::Formatter::ResultSeq::Vertical) {
$formatter.new(:rs(self)).format
}
sub create-resultseq($rs-class-name, Mu \type) is export is raw {
use Red::DefaultResultSeq;
my $rs-class := Metamodel::ClassHOW.new_type: :name($rs-class-name);
$rs-class.^add_parent: Red::DefaultResultSeq;
$rs-class.^add_role: Red::ResultSeq[type];
$rs-class.^add_role: Iterable;
$rs-class.^compose;
$rs-class
}
method create-comment-to-caller is hidden-from-sql-commenting {
my %data;
%data<meth-name> = callframe(1).code.name;
given Backtrace.new.tail(*-3).first: {
.subname and .subname ne "<anon>" and !.code.?is-hidden-from-sql-commenting
} {
%data<file> = .file;
%data<block> = .code.?name;
%data<line> = .line;
}
@!comments.push: Red::AST::Comment.new:
:msg(
&*RED-COMMENT-SQL
?? &*RED-COMMENT-SQL.(|%data)
!! comment-sql(|%data)
)
}
#| Add a comment to SQL query
sub comment-sql(:$meth-name, :$file, :$block, :$line) {
"method '{ $meth-name // '<anon>' }' called at: { $file // '<none>' } #{ $line // '' }"
}
#| The type of the ResultSeq
method of is hidden-from-sql-commenting { $of }
#method is-lazy { True }
method cache is hidden-from-sql-commenting {
List.from-iterator: self.iterator
}
has Pair @.update;
has Red::AST::Comment @.comments;
has Red::Driver $.with;
has $.obj;
has Red::AST::Chained $.chain handles <filter limit offset post order group table-list> .= new: |(:filter(.^id-filter) with $!obj);
has Lock::Async $!lock .= new;
has Red::LockType $.for;
has @.prefetches;
has Bool $.prefetches-set = False;
multi method with { $!with }
multi method with(Red::Driver $with) {
self.clone: :$with
}
multi method with(Str $with) {
self.with: %GLOBAL::RED-DEFAULT-DRIVERS{$with}
}
method iterator(--> Red::ResultSeq::Iterator) is hidden-from-sql-commenting {
my $*RED-INTERNAL = True;
Red::ResultSeq::Iterator.new: :$.of, :$.ast, :&.post, |(:driver($_) with $!with)
}
#| Returns a Seq with the result of the SQL query
method Seq is hidden-from-sql-commenting {
$!lock.protect: {
self.create-comment-to-caller;
Seq.new: self.iterator
}
}
method do-it(*%pars) is hidden-from-sql-commenting {
self.create-comment-to-caller;
self.clone(|%pars, :chain($!chain.clone: |%pars)).Seq
}
method skip-locked {
self.clone: :for(SKIP_LOCKED)
}
#multi method grep(::?CLASS: &filter) { nextwith :filter( filter self.of.^alias: "me" ) }
multi method where(::?CLASS:U: Red::AST:U $filter) is hidden-from-sql-commenting { self.WHAT }
multi method where(::?CLASS:D: Red::AST:U $filter) is hidden-from-sql-commenting { self.clone }
multi method where(::?CLASS:U: Red::AST:D $filter) is hidden-from-sql-commenting { self.new: :chain($!chain.clone: :$filter) }
multi method where(::?CLASS:D: Red::AST:D $filter) is hidden-from-sql-commenting {
self.clone: :chain($!chain.clone: :filter(($.filter, $filter).grep({ .defined }).reduce: -> $a, $b? {
$b.DEFINITE ?? Red::AST::AND.new($a, $b) !! $a
}))
}
method transform-item(*%data) is hidden-from-sql-commenting {
self.of.bless: |%data
}
#| Adds a new filter on the query (does not run the query)
method grep(&filter) is hidden-from-sql-commenting {
X::Red::NoOperatorsDefined.new(:meth<grep>).throw
if !$*from-first && !::CALLERS::<&__RED_OPERATOR_LOADED__> && !::CALLERS::OUTERS::<&__RED_OPERATOR_LOADED__> && !$*RED-DONT-FAIL-WITHOUT-OPERATORS;
my $*RED-INTERNAL = True;
self.create-comment-to-caller;
CATCH {
default {
if !$*RED-FALLBACK.defined || $*RED-FALLBACK {
if !$*RED-FALLBACK.defined {
note "falling back (to mute this message, please define the \$*RED-FALLBACK variable): { .?message }";
}
return self.Seq.grep: &filter
}
.rethrow
}
}
my $*OUT = class :: {
method put(|) { die "Trying to print inside the grep's block" }
method print(|) { die "Trying to print inside the grep's block" }
}
my Red::AST $*RED-GREP-FILTER;
my $filter = do given what-does-it-do(&filter, self.of) {
do if [eqv] .values {
.values.head
} else {
.kv.map(-> $test, $ret {
do with $test {
Red::AST::AND.new: $test, ast-value $ret
} else {
$ret
}
}).reduce: { Red::AST::OR.new: $^agg, $^fil }
}
}
with $*RED-GREP-FILTER {
$filter = Red::AST::AND.new: ($_ ~~ Red::AST ?? $_ !! .&ast-value), $filter
}
self.where: $filter;
}
#| Changes the query to return only the first row that matches the condition and run it (.grep(...).head)
multi method first(&filter --> Red::Model) is hidden-from-sql-commenting {
X::Red::NoOperatorsDefined.new(:meth<first>).throw
if !::CALLERS::<&__RED_OPERATOR_LOADED__> && !::CALLERS::OUTERS::<&__RED_OPERATOR_LOADED__> && !$*RED-DONT-FAIL-WITHOUT-OPERATORS;
self.create-comment-to-caller;
my $*from-first = True;
self.grep(&filter).head
}
multi method first(--> Red::Model) is hidden-from-sql-commenting {
X::Red::NoOperatorsDefined.new(:meth<grep>).throw
if !::CALLERS::<&__RED_OPERATOR_LOADED__> && !::CALLERS::OUTERS::<&__RED_OPERATOR_LOADED__> && !$*RED-DONT-FAIL-WITHOUT-OPERATORS;
self.create-comment-to-caller;
self.head
}
#multi method create-map($, :&filter) { self.do-it.map: &filter }
multi method create-map(\SELF: Red::Model $model is copy, :filter(&)) is hidden-from-sql-commenting {
self but role :: { method of { $model } }
}
multi method create-map(\SELF: *@ret where .all ~~ Red::AST, :&filter) is hidden-from-sql-commenting {
my @*table-list = |@.table-list, self.of;
my \model = SELF.of.^specialise(|@ret);
my role CMModel [Mu:U \m] {
has &.last-filter = &filter;
has $.last-rs = SELF;
method of { model }
}
@*table-list .= unique;
SELF.clone(
:chain($!chain.clone:
:$.filter,
:post{ my @data = do for model.^columns -> $attr { ."{$attr.name.substr: 2}"() }; @data == 1 ?? @data.head !! |@data },
:@*table-list,
|%_
)
) but CMModel[model]
}
#| Change what will be returned (does not run the query)
method map(\SELF: &filter) is hidden-from-sql-commenting {
X::Red::NoOperatorsDefined.new(:meth<map>).throw
if !::CALLERS::<&__RED_OPERATOR_LOADED__> && !::CALLERS::OUTERS::<&__RED_OPERATOR_LOADED__> && !$*RED-DONT-FAIL-WITHOUT-OPERATORS;
my $*RED-INTERNAL = True;
SELF.create-comment-to-caller;
CATCH {
default {
if !$*RED-FALLBACK.defined || $*RED-FALLBACK {
if !$*RED-FALLBACK.defined {
note "falling back (to mute this message, please define the \$*RED-FALLBACK variable): { .?message }";
}
return self.Seq.map: &filter
}
.rethrow
}
}
my $*OUT = class :: {
method put(|) { die "Trying to print inside the map's block" }
method print(|) { die "Trying to print inside the map's block" }
}
die "Count bigger than 1" if &filter.count > 1;
my Red::AST %next{Red::AST};
my Red::AST %when{Red::AST};
my @*UPDATE := @!update;
for what-does-it-do(&filter, SELF.of) -> Pair $_ {
(.value ~~ (Red::AST::Next | Red::AST::Empty) ?? %next !! %when){.key} = .value
}
my \seq := do if %next {
SELF.where(%next.keys.reduce(-> $agg, $n { Red::AST::OR.new: $agg, $n }))
} else { SELF }
my \ast = Red::AST::Case.new(:%when);
#die "Map returning Red::Model is NYI" if ast ~~ Red::AST::Value and ast.type ~~ Red::Model;
return seq.create-map: ast.value, :&filter if ast ~~ Red::AST::Value and ast.type ~~ Red::Model;
return seq.create-map: ast.value, :&filter if ast ~~ Red::AST::Value and ast.type ~~ Positional;
seq.create-map: ast, :&filter
}
#method flatmap(&filter) {
# treat-map :flat, $.filter, filter(self.of), &filter
#}
#| Defines the order of the query (does not run the query)
method sort(&order --> Red::ResultSeq) is hidden-from-sql-commenting {
my $*RED-INTERNAL = True;
self.create-comment-to-caller;
my @order = order self.of;
self.clone: :chain($!chain.clone: :@order)
}
#| Sets the query to return the rows in a randomic order (does not run the query)
multi method pick(Whatever --> Red::ResultSeq) is hidden-from-sql-commenting {
self.create-comment-to-caller;
self.clone: :chain($!chain.clone: :order[Red::AST::Function.new: :func<random>])
}
multi method pick(Int() $num) is hidden-from-sql-commenting {
self.create-comment-to-caller;
self.pick(*).head: |($_ with $num)
}
multi method pick() is hidden-from-sql-commenting {
self.create-comment-to-caller;
self.pick(*).head
}
#| Returns a ResultAssociative classified by the passed code (does not run the query)
method classify(\SELF: &func, :&as = { $_ }, :&reduce --> Red::ResultAssociative) is hidden-from-sql-commenting {
my $*RED-INTERNAL = True;
SELF.create-comment-to-caller;
do if self.?last-rs.DEFINITE {
self.last-rs.classify(&func o self.last-filter, :as{ ast-value True })
} else {
# TODO: use what-does-it-do() for &func, &as and &reduce
my \key = func SELF.of;
my \value = as SELF.of;
Red::ResultAssociative[value, key.head].new: :rs(SELF), |(:next-level(key.skip) if key.elems > 1), |(:&reduce if &reduce)
}
}
#| Runs a query to create a Bag
multi method Bag {
nextsame unless self.?last-rs.DEFINITE;
self.last-rs.classify(self.last-filter, :as{ ast-value True }).Bag
}
#| Runs a query to create a Set
multi method Set {
nextsame unless self.?last-rs.DEFINITE;
self.last-rs.classify(self.last-filter, :as{ ast-value True }).Set
}
#| Gets the first row returned by the query (run the query)
multi method head is hidden-from-sql-commenting {
self.create-comment-to-caller;
self.do-it(:1limit).head
}
# TODO: Return a Red::ResultSeq
multi method head(UInt(Numeric) $num) is hidden-from-sql-commenting {
self.create-comment-to-caller;
self.clone: :chain($!chain.clone: :limit(min $num, $.limit))
}
#| Skips the $num first items from ResultSeq (adds a limit on the SQL query)
method skip(UInt:D $num --> Red::ResultSeq) is hidden-from-sql-commenting {
self.create-comment-to-caller;
self.clone: :chain($!chain.clone: :offset(($.offset // 0) + $num));
}
# Alias for skip. It's going to be deprecated on a future version
method from(UInt:D $num --> Red::ResultSeq) is hidden-from-sql-commenting {
self.skip: $num
}
#| Returns the number of rows returned by the query (runs the query)
method elems( --> Int()) is hidden-from-sql-commenting {
self.create-comment-to-caller;
self.create-map(Red::AST::Function.new: :func<count>, :args[ast-value *]).head
}
#| Returns True if there are lines returned by the query False otherwise (runs the query)
method Bool( --> Bool()) is hidden-from-sql-commenting {
self.create-comment-to-caller;
self.create-map(Red::AST::Gt.new: Red::AST::Function.new(:func<count>, :args[ast-value *]), ast-value 0).head
}
method new-object(::?CLASS:D: *%pars) is hidden-from-sql-commenting {
my %data = $.filter.should-set;
my \obj = self.of.bless;#: |%pars, |%data;
for %(|%pars, |%data).kv -> $key, $val {
obj.^set-attr: $key, $val
}
obj
}
#| Returns a ResultSeqSeq containing ResultSeq that will return ResultSeqs with $size rows each (do not run the query)
method batch(Int $size --> Red::ResultSeqSeq) {
Red::ResultSeqSeq.new: :rs(self), :$size
}
#| Creates a new element of that set
method create(::?CLASS:D: *%pars) is hidden-from-sql-commenting {
self.create-comment-to-caller;
$.of.^orig.^create: |%pars, |%(|(.should-set with $.filter), |(.should-set with $.of.HOW.?join-on: $.of));
}
#| Alias for `create`
method push(::?CLASS:D: *%pars) is hidden-from-sql-commenting {
$.create(|%pars)
}
#| Deletes every row on that ResultSeq
method delete(::?CLASS:D:) is hidden-from-sql-commenting {
self.create-comment-to-caller;
my $*RED-INTERNAL = True;
my $iter = Red::ResultSeq::Iterator.new: :$.of, :ast(Red::AST::Delete.new: $.of, $.filter), :&.post, |(:driver($_) with $!with);
$!lock.protect: {
self.create-comment-to-caller;
Seq.new: $iter
}
}
#| Saves any change on any element of that ResultSet
method save(::?CLASS:D:) is hidden-from-sql-commenting {
self.create-comment-to-caller;
my @*UPDATE;
die "You should use a map updating value(s) before saving" unless $.of.^can: "orig-result-seq";
$.of.orig-result-seq.of.^apply-row-phasers(BeforeUpdate);
my $*RED-INTERNAL = True;
my $iter = Red::ResultSeq::Iterator.new: :of($.of.orig-result-seq.of), :ast(Red::AST::Update.new: :model($.table-list.head), :values[|@!update, |@*UPDATE], :filter($.filter));
$!lock.protect: {
self.create-comment-to-caller;
Seq.new: $iter
}
}
#| unifies 2 ResultSeqs
method union(::?CLASS:D: $other --> Red::ResultSeq) is hidden-from-sql-commenting {
self.create-comment-to-caller;
my Red::AST $filter = self.ast.union: $other.ast;
self.clone: :chain($!chain.clone: :$filter)
}
#| intersects 2 ResultSeqs
method intersect(::?CLASS:D: $other --> Red::ResultSeq) is hidden-from-sql-commenting {
self.create-comment-to-caller;
my Red::AST $filter = self.ast.intersect: $other.ast;
self.clone: :chain($!chain.clone: :$filter)
}
#| Removes 1 ResultSeq elements from other ResultSeq
method minus(::?CLASS:D: $other --> Red::ResultSeq) is hidden-from-sql-commenting {
self.create-comment-to-caller;
my Red::AST $filter = self.ast.minus: $other.ast;
self.clone: :chain($!chain.clone: :$filter)
}
#| Join (Positional join)
multi method join(Str() $sep) {
self.Seq.join: $sep
}
#| Create a custom join (SQL join)
multi method join-model(Str $model, |c) {
try require ::($model);
self.join-model(::($model), |c)
}
#| Create a custom join (SQL join)
multi method join-model(Red::Model \model, &on, :$name = "{ self.^shortname }_{ model.^shortname }", *%pars where { .elems == 0 || ( .elems == 1 && so .values.head ) }) {
my $*RED-INTERNAL = True;
do with self.obj {
my $filter = do given what-does-it-do(&on.assuming($_), model) {
do if [eqv] .values {
.values.head
} else {
.kv.map(-> $test, $ret {
do with $test {
Red::AST::AND.new: $test, ast-value $ret
} else {
$ret
}
}).reduce: { Red::AST::OR.new: $^agg, $^fil }
}
}
with $*RED-GREP-FILTER {
$filter = Red::AST::AND.new: ($_ ~~ Red::AST ?? $_ !! .&ast-value), $filter
}
model.^all.where: $filter
} else {
self.of.^join(model, &on, :$name, |%pars).^all.clone: :$!chain
}
}
method prefetch($defined? is copy, *%prefetches) {
my @prefetches =
|$.of.^pre-fetches,
|@!prefetches,
;
$defined = [$defined,] if $defined.defined && $defined !~~ Positional;
@prefetches .= grep: {
.name.substr(2) eq $defined.any
} with $defined;
my :(:@add, :@del) := %prefetches.pairs.classify: { .value ?? "add" !! "del" }, :as{ .key };
@prefetches.append: @.^attributes.grep: {
.name.substr(2) eq @add.any
} if @add;
@prefetches .= grep: {
.name.substr(2) eq @del.none
} if @del;
self.clone: :@prefetches, :prefetches-set
}
#| Returns the AST that will generate the SQL
method ast(Bool :$sub-select --> Red::AST) is hidden-from-sql-commenting {
if $.filter ~~ Red::AST::MultiSelect {
$.filter
} else {
my @prefetch := $!prefetches-set ?? @!prefetches !! @($.of.^pre-fetches);
Red::AST::Select.new:
:$.of, :$.filter,
:$.limit, :$.offset,
:@.order, :@.table-list,
:@.group, :@.comments,
:@prefetch, :$sub-select,
:$!for,
;
}
}