From 6fa49a98f164597f358f5bb1fffd5aeee345f4b3 Mon Sep 17 00:00:00 2001 From: Veesh Goldman Date: Thu, 18 May 2023 16:42:54 +0300 Subject: [PATCH 1/6] Case Expressions! (#12) * feat(CaseExpr): implement CASE plugin! --- lib/DBIx/Class/SQLA2.pm | 6 +- lib/SQL/Abstract/Plugin/CaseExpr.pm | 103 ++++++++++++++++++++++++++++ t/case_expr.t | 71 +++++++++++++++++++ 3 files changed, 178 insertions(+), 2 deletions(-) create mode 100644 lib/SQL/Abstract/Plugin/CaseExpr.pm create mode 100644 t/case_expr.t diff --git a/lib/DBIx/Class/SQLA2.pm b/lib/DBIx/Class/SQLA2.pm index cbd90f2..6106da3 100644 --- a/lib/DBIx/Class/SQLA2.pm +++ b/lib/DBIx/Class/SQLA2.pm @@ -68,8 +68,10 @@ sub expand_clause { sub new { my $new = shift->next::method(@_); - $new->plugin('+ExtraClauses')->plugin('+WindowFunctions')->plugin('+Upsert')->plugin('+BangOverrides') - unless (grep {m/^with$/} $new->clauses_of('select')); + unless (grep {m/^with$/} $new->clauses_of('select')) { + $new->plugin("+$_") for qw/ExtraClauses WindowFunctions Upsert BangOverrides CaseExpr/; + } + return $new } our $VERSION = '0.01'; diff --git a/lib/SQL/Abstract/Plugin/CaseExpr.pm b/lib/SQL/Abstract/Plugin/CaseExpr.pm new file mode 100644 index 0000000..ec9aea0 --- /dev/null +++ b/lib/SQL/Abstract/Plugin/CaseExpr.pm @@ -0,0 +1,103 @@ +package SQL::Abstract::Plugin::CaseExpr; +use feature qw/signatures postderef/; + +our $VERSION = '0.01'; +use Moo; +with 'SQL::Abstract::Role::Plugin'; +use List::Util qw/pairmap/; + +no warnings 'experimental::signatures'; + +sub register_extensions ($self, $sqla) { + + $sqla->expander( + case => sub ($sqla, $name, $value) { + # if the user passed in the double array-ref, then we assume it's already expanded + return { -case => $value } if ref $value->[0] eq 'ARRAY'; + my $else; + my @conditions = $value->@*; + $else = pop @conditions unless @conditions * %2; + return { + -case => [ + [ map +($sqla->expand_expr($_->{if}, -ident), $sqla->expand_expr($_->{then}, -value)), @conditions ], + $else ? $sqla->expand_expr($else, -value) : () + ] + }; + } + ); + $sqla->renderer( + case => sub ($sqla, $name, $value) { + my $else = $value->[1]; + $sqla->join_query_parts( + ' ', + { -keyword => 'CASE' }, + (pairmap { ({ -keyword => 'WHEN' }, $a, { -keyword => 'THEN' }, $b) } $value->[0]->@*), + $else ? ({ -keyword => 'ELSE' }, $else) : (), + { -keyword => 'END' } + ); + } + ); + +} + +1; + +=encoding utf8 + +=head1 NAME + +SQL::Abstract::Plugin::CaseExpr - Case Expression support for SQLA2! + +=head1 SYNOPSIS + + # pass this to anything that SQLA will render + # arrayref b/c order matters + { -case => [ + # if/then is a bit more familiar than WHEN/THEN + { + if => { sales => { '>' => 9000 } }, + # scalars default to bind params + then => 'Scouter Breaking' + }, + { + if => { sales => { '>' => 0 } }, + then => 'At least something' + }, + # if the final node does not contain if, it's the ELSE clause + 'How did this happen?' + ]} + # CASE WHEN sales > 9000 THEN ? WHEN sales > 0 THEN ? ELSE ? END + # [ 'Scouter Breaking', 'At least something', 'How did this happen?' ] + +=head1 DESCRIPTION + +This is a work in progress to support CASE expressions in SQLA2 + +B + +=head2 Using with DBIx::Class + +In order to use this with DBIx::Class, you simply need to apply the DBIC-SQLA2 plugin, and +then your SQLMaker will support this syntax! + +=head2 New Syntax + +=head3 -case node + +The entry point for the new handling is the -case node. This takes an arrayref of hashrefs which represent the branches of the conditional tree, and optionally a final entry as the default clause. + +The hashrefs must have the following two keys: + +=over 4 + +=item if + +The condition to be checked against. It is processed like a WHERE clause. + +=item then + +The value to be returned if this condition is true. Scalars here default to -value, which means they are taken as bind parameters + +=back + +=cut diff --git a/t/case_expr.t b/t/case_expr.t new file mode 100644 index 0000000..988b17d --- /dev/null +++ b/t/case_expr.t @@ -0,0 +1,71 @@ +use strict; +use warnings; +use Test::More; +use File::Temp (); +use lib 't/lib'; +use Local::Schema; + +my $tmpdir = File::Temp->newdir; +my $schema = Local::Schema->connect("dbi:SQLite:$tmpdir/on_conflict.sqlite"); +ok $schema, 'created'; +$schema->storage->ensure_connected; + +# deploy + populate +$schema->deploy({ add_drop_table => 1 }); +$schema->resultset('Artist')->populate([ + { + artistid => 2, + name => 'Portishead', + albums => + [ { title => 'Portishead', rank => 2 }, { title => 'Dummy', rank => 3 }, { title => 'Third', rank => 4 }, ] + }, + { artistid => 1, name => 'Stone Roses', albums => [ { title => 'Second Coming', rank => 1 }, ] }, + { artistid => 3, name => 'LSG' } +]); + +subtest 'using a CASE expression' => sub { + my @oddity = $schema->resultset('Album')->search(undef, { + 'columns' => [ + 'rank', + 'title', + { oddity => \{ -case => [ + { if => { -mod => [ 'rank', 2 ] }, then => 'Quite Odd' }, + 'Even' + ] , + -as => 'oddity'} + }], + order_by => { -asc => 'rank'} + })->all; + + is_deeply \@oddity, [ + { title => 'Second Coming', rank => 1, oddity => 'Quite Odd'}, + { title => 'Portishead', rank => 2, oddity => 'Even'}, + { title => 'Dummy', rank => 3, oddity => 'Quite Odd'}, + { title => 'Third', rank => 4, oddity => 'Even'}, + ], 'got expected result'; +}; + +subtest 'passes through the double arrayref syntax' => sub { + my @oddity = $schema->resultset('Album')->search(undef, { + 'columns' => [ + 'rank', + 'title', + { oddity => \{ -case => [ + [{ -func => [ 'mod', 'rank', 2 ] } => { -bind => [ undef, 'Quite Odd' ] }], + { -bind => [ undef, 'Even']} + ] , + -as => 'oddity'} + }], + order_by => { -asc => 'rank'} + })->all; + + is_deeply \@oddity, [ + { title => 'Second Coming', rank => 1, oddity => 'Quite Odd'}, + { title => 'Portishead', rank => 2, oddity => 'Even'}, + { title => 'Dummy', rank => 3, oddity => 'Quite Odd'}, + { title => 'Third', rank => 4, oddity => 'Even'}, + ], 'got expected result'; + +}; + +done_testing; From 1867b85ad0ffaa561c27417d072f69db41b70e26 Mon Sep 17 00:00:00 2001 From: Veesh Goldman Date: Sun, 21 May 2023 14:51:41 +0000 Subject: [PATCH 2/6] chore: update Changes for first dev release! --- Changes | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Changes b/Changes index 810101e..d3a4ff1 100644 --- a/Changes +++ b/Changes @@ -1,3 +1,3 @@ {{$NEXT}} - Initial release; ExtraClauses, BangOverrides and Upsert support! + Initial release; ExtraClauses, BangOverrides, Upsert, WindowFunction and CaseExpr support! SUPER EXPERIMENTAL! From f55b0cece6408d48a8f2c52a51d1b8827846340b Mon Sep 17 00:00:00 2001 From: Veesh Goldman Date: Sun, 21 May 2023 15:01:31 +0000 Subject: [PATCH 3/6] Checking in changes prior to tagging of version 0.01. Changelog diff is: diff --git a/Changes b/Changes index d3a4ff1..f198de8 100644 --- a/Changes +++ b/Changes @@ -1,3 +1,5 @@ {{$NEXT}} + +0.01 2023-05-21T15:01:10Z Initial release; ExtraClauses, BangOverrides, Upsert, WindowFunction and CaseExpr support! SUPER EXPERIMENTAL! --- Changes | 2 ++ README.md | 8 ++++++++ 2 files changed, 10 insertions(+) diff --git a/Changes b/Changes index d3a4ff1..f198de8 100644 --- a/Changes +++ b/Changes @@ -1,3 +1,5 @@ {{$NEXT}} + +0.01 2023-05-21T15:01:10Z Initial release; ExtraClauses, BangOverrides, Upsert, WindowFunction and CaseExpr support! SUPER EXPERIMENTAL! diff --git a/README.md b/README.md index 6f9ebf6..5d8536e 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,10 @@ For a simple way of using this, take a look at [DBIx::Class::Schema::SQLA2Suppor **EXPERIMENTAL** +This role itself will add handling of hashref-refs to select lists + group by clauses, +which will render the inner hashref as if it had been passed through to SQLA2 rather than +doing the recursive function rendering that DBIC does. + ## Included Plugins This will add the following SQLA2 plugins: @@ -23,6 +27,10 @@ This will add the following SQLA2 plugins: Adds support for CTEs, and other fun new SQL syntax +- [SQL::Abstract::Plugin::WindowFunctions](https://metacpan.org/pod/SQL%3A%3AAbstract%3A%3APlugin%3A%3AWindowFunctions) + + Adds support for window functions and advanced aggregates. + - [SQL::Abstract::Plugin::Upsert](https://metacpan.org/pod/SQL%3A%3AAbstract%3A%3APlugin%3A%3AUpsert) Adds support for Upserts (ON CONFLICT clause) From ec6d3bd5cecd2435a48921426dcf4a589ac34f9c Mon Sep 17 00:00:00 2001 From: Veesh Goldman Date: Sat, 27 May 2023 20:34:31 +0000 Subject: [PATCH 4/6] fix: change hashrefref handling so join pruning is t3h work --- Changes | 3 +++ lib/DBIx/Class/SQLA2.pm | 53 ++++++++++++++++++++++++----------------- 2 files changed, 34 insertions(+), 22 deletions(-) diff --git a/Changes b/Changes index f198de8..1c68998 100644 --- a/Changes +++ b/Changes @@ -1,5 +1,8 @@ {{$NEXT}} + Update hashrefref support to hook _recurse_fields such that join pruning no longer + explodes + 0.01 2023-05-21T15:01:10Z Initial release; ExtraClauses, BangOverrides, Upsert, WindowFunction and CaseExpr support! SUPER EXPERIMENTAL! diff --git a/lib/DBIx/Class/SQLA2.pm b/lib/DBIx/Class/SQLA2.pm index 6106da3..441de6d 100644 --- a/lib/DBIx/Class/SQLA2.pm +++ b/lib/DBIx/Class/SQLA2.pm @@ -13,31 +13,40 @@ use base qw( use Role::Tiny; +sub _render_hashrefrefs { + my ($self, $list) = @_; + my @fields = ref $list eq 'ARRAY' ? @$list : $list; + return [ + map { + ref $_ eq 'REF' && ref $$_ eq 'HASH' + ? do { + my %f = $$_->%*; + my $as = delete $f{-as}; + \[ + $as + ? $self->render_expr({ -op => [ 'as', \%f, { -ident => $as } ] }) + : $self->render_expr(\%f) + ]; + } + : $_ + } @fields + ]; +} + +sub _recurse_fields { + my ($self, $fields) = @_; + if (ref $fields eq 'REF' && ref $$fields eq 'HASH') { + return $self->next::method($self->_render_hashrefrefs($fields)->[0]); + } + return $self->next::method($fields); + +} + sub select { my ($self, $table, $fields, $where, $rs_attrs, $limit, $offset) = @_; - my $expand_hashrefref = sub { - my $list = shift; - my @fields = ref $list eq 'ARRAY' ? @$list : $list; - return [ - map { - ref $_ eq 'REF' && ref $$_ eq 'HASH' - ? do { - my %f = $$_->%*; - my $as = delete $f{-as}; - \[ - $as - ? $self->render_expr({ -op => [ 'as', \%f, { -ident => $as } ] }) - : $self->render_expr(\%f) - ]; - } - : $_ - } @fields - ]; - }; - $fields = $expand_hashrefref->($fields); if (my $gb = $rs_attrs->{group_by}) { - $rs_attrs = { %$rs_attrs, group_by => $expand_hashrefref->($gb) }; + $rs_attrs = { %$rs_attrs, group_by => $self->_render_hashrefrefs($gb) }; } $self->next::method($table, $fields, $where, $rs_attrs, $limit, $offset); } @@ -71,7 +80,7 @@ sub new { unless (grep {m/^with$/} $new->clauses_of('select')) { $new->plugin("+$_") for qw/ExtraClauses WindowFunctions Upsert BangOverrides CaseExpr/; } - return $new + return $new; } our $VERSION = '0.01'; From babc8948d69c094aadb38dc17b5e6a10328b60b6 Mon Sep 17 00:00:00 2001 From: Veesh Goldman Date: Sat, 27 May 2023 20:35:08 +0000 Subject: [PATCH 5/6] Checking in changes prior to tagging of version 0.01_2. Changelog diff is: diff --git a/Changes b/Changes index 1c68998..d57c25f 100644 --- a/Changes +++ b/Changes @@ -1,5 +1,7 @@ {{$NEXT}} +0.01_2 2023-05-27T20:34:54Z + Update hashrefref support to hook _recurse_fields such that join pruning no longer explodes --- Changes | 2 ++ META.json | 2 +- lib/DBIx/Class/SQLA2.pm | 2 +- lib/SQL/Abstract/Plugin/CaseExpr.pm | 2 +- lib/SQL/Abstract/Plugin/Upsert.pm | 2 +- lib/SQL/Abstract/Plugin/WindowFunctions.pm | 2 +- 6 files changed, 7 insertions(+), 5 deletions(-) diff --git a/Changes b/Changes index 1c68998..d57c25f 100644 --- a/Changes +++ b/Changes @@ -1,5 +1,7 @@ {{$NEXT}} +0.01_2 2023-05-27T20:34:54Z + Update hashrefref support to hook _recurse_fields such that join pruning no longer explodes diff --git a/META.json b/META.json index f2ddf59..e29a328 100644 --- a/META.json +++ b/META.json @@ -66,7 +66,7 @@ "web" : "https://github.com/rabbiveesh/dbic-sqla2" } }, - "version" : "0.01", + "version" : "0.01_2", "x_contributors" : [ "Roy Storey ", "Veesh Goldman " diff --git a/lib/DBIx/Class/SQLA2.pm b/lib/DBIx/Class/SQLA2.pm index 441de6d..4c2216f 100644 --- a/lib/DBIx/Class/SQLA2.pm +++ b/lib/DBIx/Class/SQLA2.pm @@ -83,7 +83,7 @@ sub new { return $new; } -our $VERSION = '0.01'; +our $VERSION = '0.01_2'; 1; diff --git a/lib/SQL/Abstract/Plugin/CaseExpr.pm b/lib/SQL/Abstract/Plugin/CaseExpr.pm index ec9aea0..2c4c2e5 100644 --- a/lib/SQL/Abstract/Plugin/CaseExpr.pm +++ b/lib/SQL/Abstract/Plugin/CaseExpr.pm @@ -1,7 +1,7 @@ package SQL::Abstract::Plugin::CaseExpr; use feature qw/signatures postderef/; -our $VERSION = '0.01'; +our $VERSION = '0.01_2'; use Moo; with 'SQL::Abstract::Role::Plugin'; use List::Util qw/pairmap/; diff --git a/lib/SQL/Abstract/Plugin/Upsert.pm b/lib/SQL/Abstract/Plugin/Upsert.pm index b5d3f9b..8ed0a1c 100644 --- a/lib/SQL/Abstract/Plugin/Upsert.pm +++ b/lib/SQL/Abstract/Plugin/Upsert.pm @@ -50,7 +50,7 @@ sub register_extensions { ); } -our $VERSION = '0.01'; +our $VERSION = '0.01_2'; 1; diff --git a/lib/SQL/Abstract/Plugin/WindowFunctions.pm b/lib/SQL/Abstract/Plugin/WindowFunctions.pm index 9c46a40..adb1629 100644 --- a/lib/SQL/Abstract/Plugin/WindowFunctions.pm +++ b/lib/SQL/Abstract/Plugin/WindowFunctions.pm @@ -1,7 +1,7 @@ package SQL::Abstract::Plugin::WindowFunctions; use feature qw/signatures postderef/; -our $VERSION = '0.01'; +our $VERSION = '0.01_2'; use Moo; with 'SQL::Abstract::Role::Plugin'; From 6e6293dd2feae944c08e62e56179727c85339aaa Mon Sep 17 00:00:00 2001 From: Veesh Goldman Date: Wed, 31 May 2023 18:06:27 +0000 Subject: [PATCH 6/6] chore: remove ideas/ from dist, and depend on 5.22 explicitly --- META.json | 3 ++- cpanfile | 1 + minil.toml | 3 +++ 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/META.json b/META.json index e29a328..4be9bf4 100644 --- a/META.json +++ b/META.json @@ -43,7 +43,8 @@ "runtime" : { "requires" : { "DBIx::Class" : "0.082843", - "SQL::Abstract" : "2.000001" + "SQL::Abstract" : "2.000001", + "perl" : "5.22" } }, "test" : { diff --git a/cpanfile b/cpanfile index 735a30d..941d558 100644 --- a/cpanfile +++ b/cpanfile @@ -1,4 +1,5 @@ # -*- mode: perl -*- +requires 'perl' => '5.22'; requires 'DBIx::Class' => '0.082843'; requires 'SQL::Abstract' => '2.000001'; diff --git a/minil.toml b/minil.toml index 2bd39be..bff889e 100644 --- a/minil.toml +++ b/minil.toml @@ -1 +1,4 @@ name = 'DBIx-Class-SQLA2' + +[FileGatherer] +exclude_match = [ '^ideas/*' ]