diff --git a/lib/Explain.pm b/lib/Explain.pm index 45a334b..f9e2a15 100755 --- a/lib/Explain.pm +++ b/lib/Explain.pm @@ -41,6 +41,9 @@ sub startup { # route: 'index' $routes->route( '/' )->to( 'controller#index' )->name( 'new-explain' ); + # route: 'new-optimization' + $routes->route( '/new_optimization' )->to( 'controller#new_optimization' )->name( 'new-optimization' ); + # route: 'user-history' $routes->route( '/user-history/:direction/:key' )->to( 'controller#user_history', direction => undef, key => undef )->name( 'user-history' ); diff --git a/lib/Explain/Controller.pm b/lib/Explain/Controller.pm index c053502..007e9e3 100755 --- a/lib/Explain/Controller.pm +++ b/lib/Explain/Controller.pm @@ -161,6 +161,24 @@ sub login { return; } +sub new_optimization { + my $self = shift; + + my $original_plan_id = $self->req->param( 'original' ) // ''; + + return $self->redirect_to( 'new-explain' ) unless $original_plan_id =~ m{\A[a-zA-Z0-9]+\z}; + + my ( $original_plan, $original_title ) = $self->database->get_plan( $original_plan_id ); + + return $self->redirect_to( 'new-explain', status => 404 ) unless $original_plan; + + $self->stash->{ 'optimization' } = 1; + $self->stash->{ 'original_plan_id' } = $original_plan_id; + $self->stash->{ 'original_title' } = $original_title; + + return $self->render( 'controller/index' ); +} + sub index { my $self = shift; @@ -174,6 +192,12 @@ sub index { return $self->render( message => 'Your plan is too long.', status => 413 ) if 10_000_000 < length $plan; + # Get id of parent plan + my $parent_id = $self->req->param( 'optimization_for' ); + if ( defined $parent_id ) { + $parent_id = undef unless $self->database->plan_exists( $parent_id ); + } + # public my $is_public = $self->req->param( 'is_public' ) ? 1 : 0; @@ -214,7 +238,7 @@ sub index { } # save to database - my ( $id, $delete_key ) = $self->database->save_with_random_name( $title, $plan, $is_public, $is_anon, $self->session->{ 'user' } ); + my ( $id, $delete_key ) = $self->database->save_with_random_name( $title, $plan, $is_public, $is_anon, $self->session->{ 'user' }, $parent_id, ); # redirect to /show/:id $self->flash( delete_key => $delete_key ); @@ -306,6 +330,10 @@ sub show { $self->stash->{ title } = $title; $self->stash->{ stats } = $stats; + # Fetch path of optimizations + $self->stash->{ optimization_path } = $self->database->get_optimization_path( $id ); + $self->stash->{ suboptimizations } = $self->database->get_optimizations_for( $id ); + # render will be called automatically return; } diff --git a/lib/Explain/Plugin/Database.pm b/lib/Explain/Plugin/Database.pm index d1602d9..82f0740 100755 --- a/lib/Explain/Plugin/Database.pm +++ b/lib/Explain/Plugin/Database.pm @@ -211,12 +211,12 @@ sub update_plan { sub save_with_random_name { my $self = shift; - my ( $title, $content, $is_public, $is_anon, $username ) = @_; + my ( $title, $content, $is_public, $is_anon, $username, $optimization_for ) = @_; my @row = $self->dbh->selectrow_array( - 'SELECT id, delete_key FROM register_plan(?, ?, ?, ?, ?)', + 'SELECT id, delete_key FROM register_plan(?, ?, ?, ?, ?, ?)', undef, - $title, $content, $is_public, $is_anon, $username, + $title, $content, $is_public, $is_anon, $username, $optimization_for, ); # return id and delete_key @@ -242,7 +242,7 @@ sub get_plan { my ( $plan_id ) = @_; my @row = $self->dbh->selectrow_array( - 'SELECT plan, title FROM plans WHERE id = ? AND NOT is_deleted', + 'SELECT plan, title, optimization_for FROM plans WHERE id = ? AND NOT is_deleted', undef, $plan_id, ); @@ -251,6 +251,60 @@ sub get_plan { return @row; } +sub get_optimization_path { + my $self = shift; + my ($plan_id) = @_; + + my $rows = $self->dbh->selectall_arrayref( + ' + WITH RECURSIVE path AS ( + SELECT id, title, optimization_for, 0 as level FROM plans WHERE id = ? and not is_deleted + union all + SELECT p.id, p.title, p.optimization_for, x.level + 1 + FROM path x + join plans p on p.id = x.optimization_for + WHERE NOT p.is_deleted + AND x.optimization_for IS NOT NULL + ) + SELECT + id, title + FROM + path + ORDER BY level desc; + ', + { Slice => {} }, + $plan_id, + ); + return if 0 == scalar @{ $rows }; + return if 1 == scalar @{ $rows }; + return $rows; +} + +sub get_optimizations_for { + my $self = shift; + my ($plan_id) = @_; + my $rows = $self->dbh->selectall_arrayref( + 'select id, title from plans where optimization_for = ? and not is_deleted', + { Slice => {} }, + $plan_id + ); + return if 0 == scalar @{ $rows }; + return $rows; +} + +sub plan_exists { + my $self = shift; + my ( $plan_id ) = @_; + + my @row = $self->dbh->selectrow_array( + 'SELECT 1 FROM plans WHERE id = ? AND NOT is_deleted', + undef, + $plan_id, + ); + return if 0 == scalar @row; + return 1; +} + sub delete_plan { my $self = shift; my ( $plan_id, $delete_key ) = @_; diff --git a/sql/patch-009.sql b/sql/patch-009.sql new file mode 100644 index 0000000..74b279a --- /dev/null +++ b/sql/patch-009.sql @@ -0,0 +1,27 @@ +BEGIN; + ALTER TABLE public.plans add column optimization_for TEXT; +DO $$ +DECLARE + v_part_name TEXT; + v_sql TEXT; +BEGIN + FOR v_part_name IN + SELECT + c.relname + FROM + pg_catalog.pg_inherits i + JOIN pg_class c ON i.inhrelid = c.oid + JOIN pg_namespace n ON c.relnamespace = n.oid + WHERE + i.inhparent = 'public.plans'::regclass + AND n.nspname = 'plans' + ORDER BY c.relname + LOOP + raise notice 'Adding index on plans.% (optimization_for)', v_part_name; + v_sql := format( 'CREATE INDEX %I ON plans.%I (optimization_for)', v_part_name || '_optimization_for', v_part_name ); + execute v_sql; + END LOOP; +END; +$$; +COMMIT; + diff --git a/sql/patch-010.sql b/sql/patch-010.sql new file mode 100644 index 0000000..1cb759f --- /dev/null +++ b/sql/patch-010.sql @@ -0,0 +1,27 @@ +BEGIN; +CREATE OR REPLACE FUNCTION register_plan(in_title text, in_plan text, in_is_public boolean, in_is_anonymized boolean, in_username text, in_optimization_for TEXT) RETURNS register_plan_return + LANGUAGE plpgsql + AS $$ +DECLARE + use_hash_length int4 := 2; + reply register_plan_return; + use_sql TEXT; +BEGIN + reply.delete_key := get_random_string( 50 ); + LOOP + reply.id := get_random_string(use_hash_length); + use_sql := format( 'INSERT INTO plans.%I (id, title, plan, is_public, entered_on, is_anonymized, delete_key, added_by, optimization_for) VALUES ($1, $2, $3, $4, now(), $5, $6, $7, $8 )', 'part_' || substr(reply.id, 1, 1) ); + BEGIN + execute use_sql using reply.id, in_title, in_plan, in_is_public, in_is_anonymized, reply.delete_key, in_username, in_optimization_for; + RETURN reply; + EXCEPTION WHEN unique_violation THEN + -- do nothing + END; + use_hash_length := use_hash_length + 1; + IF use_hash_length >= 30 THEN + raise exception 'Random string of length == 30 requested. something''s wrong.'; + END IF; + END LOOP; +END; +$$; +COMMIT; diff --git a/templates/controller/index.html.ep b/templates/controller/index.html.ep index 7039ed7..2835a34 100755 --- a/templates/controller/index.html.ep +++ b/templates/controller/index.html.ep @@ -1,6 +1,7 @@ % layout 'default'; % my $title = 'New explain'; +% $title = 'New optimization for plan #' . stash('original_plan_id') if stash('optimization'); % title $title;