commit 1f10ef71ffa6059a0259ece2f84dd33aa026cff4 Author: depesz Date: Thu Mar 10 15:12:57 2011 +0000 reorganization diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e366653 --- /dev/null +++ b/LICENSE @@ -0,0 +1,26 @@ +Copyright (c) 2009-2010, depesz +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the depesz Software nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/Makefile.PL b/Makefile.PL new file mode 100644 index 0000000..3907fd4 --- /dev/null +++ b/Makefile.PL @@ -0,0 +1,24 @@ +# IMPORTANT: if you delete this file your app will not work as +# expected. you have been warned +use inc::Module::Install; + +name 'Explain_Depesz_Com'; +all_from 'lib/Explain_Depesz_Com.pm'; + +requires 'Catalyst::Runtime' => '5.7015'; +requires 'Catalyst::Plugin::ConfigLoader'; +requires 'Catalyst::Plugin::Static::Simple'; +requires 'Catalyst::Plugin::SmartURI'; +requires 'Catalyst::Action::RenderView'; +requires 'Catalyst::Model::DBI'; +requires 'Catalyst::View::Email'; +requires 'parent'; +requires 'Pg::Explain'; +requires 'Email::Valid'; +requires 'Config::General'; # This should reflect the config file format you've chosen + # See Catalyst::Plugin::ConfigLoader for supported formats +catalyst; + +install_script glob('script/*.pl'); +auto_install; +WriteAll; diff --git a/explain_depesz_com.conf b/explain_depesz_com.conf new file mode 100644 index 0000000..7850e6b --- /dev/null +++ b/explain_depesz_com.conf @@ -0,0 +1,34 @@ +# rename this file to Explain_Depesz_Com.yml and put a : in front of "name" if +# you want to use yaml like in old versions of Catalyst +name Explain_Depesz_Com +root_dir __HOME__ +default_view TT +using_frontend_proxy 1 + +disposition hostless + + + INCLUDE_PATH __path_to(templates)__ + POST_CHOMP 1 + PRE_CHOMP 1 + + + stash_key email + + content_type text/plain + charset utf-8 + + + mailer SMTP + + Host 127.0.0.1 + + + + + dsn dbi:Pg:dbname=depesz_explain;host=127.0.0.1;port=5840 + user depesz_explain + + auto_commit 1 + + diff --git a/lib/Explain_Depesz_Com.pm b/lib/Explain_Depesz_Com.pm new file mode 100644 index 0000000..6d749ef --- /dev/null +++ b/lib/Explain_Depesz_Com.pm @@ -0,0 +1,62 @@ +package Explain_Depesz_Com; + +use strict; +use warnings; + +use Catalyst::Runtime '5.70'; + +# Set flags and add plugins for the application +# +# -Debug: activates the debug mode for very useful log messages +# ConfigLoader: will load the configuration from a Config::General file in the +# application's home directory +# Static::Simple: will serve static files from the application's root +# directory + +use parent qw/Catalyst/; +use Catalyst qw/ ConfigLoader Static::Simple SmartURI/; +our $VERSION = '0.01'; + +# Configure the application. +# +# Note that settings in explain_depesz_com.conf (or other external +# configuration file that you set up manually) take precedence +# over this when using ConfigLoader. Thus configuration +# details given here can function as a default configuration, +# with a external configuration file acting as an override for +# local deployment. + +__PACKAGE__->config( name => 'Explain_Depesz_Com' ); + +# Start the application +__PACKAGE__->setup(); + + +=head1 NAME + +Explain_Depesz_Com - Catalyst based application + +=head1 SYNOPSIS + + script/explain_depesz_com_server.pl + +=head1 DESCRIPTION + +[enter your description here] + +=head1 SEE ALSO + +L, L + +=head1 AUTHOR + +hubert depesz lubaczewski,,, + +=head1 LICENSE + +This library is free software, you can redistribute it and/or modify +it under the same terms as Perl itself. + +=cut + +1; diff --git a/lib/Explain_Depesz_Com/Controller/Root.pm b/lib/Explain_Depesz_Com/Controller/Root.pm new file mode 100644 index 0000000..697fdc3 --- /dev/null +++ b/lib/Explain_Depesz_Com/Controller/Root.pm @@ -0,0 +1,162 @@ +package Explain_Depesz_Com::Controller::Root; + +use strict; +use warnings; +use parent 'Catalyst::Controller'; +use English qw( -no_match_vars ); +use Email::Valid; +use Pg::Explain; +use Data::Dumper; + +# +# Sets the actions in this controller to be registered with no prefix +# so they function identically to actions created in MyApp.pm +# +__PACKAGE__->config->{namespace} = ''; + +=head1 NAME + +Explain_Depesz_Com::Controller::Root - Root Controller for Explain_Depesz_Com + +=head1 DESCRIPTION + +[enter your description here] + +=head1 METHODS + +=cut + +=head2 index + +=cut + +sub index :Path :Args(0) { + my ( $self, $c ) = @_; + return; +} + +sub help : Local { + my ( $self, $c ) = @_; + return; +} + +sub contact : Local { + my ( $self, $c ) = @_; + + return unless $c->req->param('message'); + return unless $c->req->param('message') =~ m{\S}; + + unless (Email::Valid->address( $c->req->param('email') || '' )) { + $c->stash->{'errors'}->{'bad_email'} = 1; + return; + } + + my $message_body = 'Message from: ' . $c->req->param('name') . ' <' . $c->req->param('email') . '>'; + $message_body .= "\nPosted from: " . $c->req->address . " with " . $c->req->header('User-Agent'); + $message_body .= "\n\n***************************\n\n"; + $message_body .= $c->req->param('message'); + + $c->stash->{'email'} = { + 'header' => [ + 'To' => 'depesz@depesz.com', + 'From' => 'depesz@depesz.com', + 'Subject' => 'Contact form on explain.depesz.com', + ], + 'body' => $message_body, + }; + + $c->forward($c->view('Email')); + + return; +} + +sub history : Local { + my ( $self, $c ) = @_; + + my @plans = $c->model('DBI')->get_public_list(); + + $c->stash->{'plans'} = \@plans; + + return; +} + +sub show : Path('s') : Args(1) { + my ( $self, $c ) = @_; + + my $explain_code = $c->req->args->[0]; + + return $c->detach('default') unless $explain_code =~ m{\A[a-zA-Z0-9]+\z}; + + my $explain = eval { + Pg::Explain->new( + 'source' => $c->model('DBI')->get_plan( $explain_code ), + ); + }; + if ($EVAL_ERROR) { + print STDERR $EVAL_ERROR; + $c->detach('default'); + } + + eval { + my $tmp = $explain->top_node; + }; + return $c->res->redirect('/') if $EVAL_ERROR; + + $c->stash->{'explain_code'} = $explain_code; + $c->stash->{'explain'} = $explain; + + return; +} + +sub new_explain : Path('new') : Args(0) { + my ( $self, $c ) = @_; + + my $explain = $c->req->param('explain'); + my $public = $c->req->param('public') ? 1 : 0; + + eval { + my $e = Pg::Explain->new( 'source' => $explain, ); + my $t = $e->top_node(); + }; + return $c->res->redirect('/') if $EVAL_ERROR; + + if (length($explain) > 10_000_000) { + $c->response->body( 'Too long explain' ); + $c->response->status(413); + return; + } + + my $code = $c->model('DBI')->save_with_random_name( $explain, $public, ); + + $c->res->redirect( $c->uri_for( 's', $code )); + + return; +} + +sub default :Path { + my ( $self, $c ) = @_; + $c->response->body( 'Page not found' ); + $c->response->status(404); + return; +} + +=head2 end + +Attempt to render a view, if needed. + +=cut + +sub end : ActionClass('RenderView') { } + +=head1 AUTHOR + +hubert depesz lubaczewski,,, + +=head1 LICENSE + +This library is free software, you can redistribute it and/or modify +it under the same terms as Perl itself. + +=cut + +1; diff --git a/lib/Explain_Depesz_Com/Model/DBI.pm b/lib/Explain_Depesz_Com/Model/DBI.pm new file mode 100644 index 0000000..6a21920 --- /dev/null +++ b/lib/Explain_Depesz_Com/Model/DBI.pm @@ -0,0 +1,45 @@ +package Explain_Depesz_Com::Model::DBI; + +use strict; +use base 'Catalyst::Model::DBI'; + +sub save_with_random_name { + my $self = shift; + my $content = shift; + my $is_public = shift; + + my $sth = $self->dbh->prepare( 'SELECT register_plan(?, ?)' ); + $sth->execute( $content, $is_public ); + my @row = $sth->fetchrow_array; + $sth->finish; + + return $row[ 0 ]; +} + +sub get_plan { + my $self = shift; + my $code = shift; + + my $sth = $self->dbh->prepare( 'SELECT plan FROM plans WHERE id = ?' ); + $sth->execute( $code ); + my @row = $sth->fetchrow_array; + $sth->finish; + + return $row[ 0 ]; +} + +sub get_public_list { + my $self = shift; + + my @reply; + my $sth = $self->dbh->prepare( 'SELECT id, to_char(entered_on, ?) as day FROM plans WHERE is_public ORDER BY entered_on DESC' ); + $sth->execute( 'YYYY-MM-DD' ); + while ( my $row = $sth->fetchrow_hashref ) { + push @reply, $row; + } + $sth->finish; + + return @reply; +} + +1; diff --git a/lib/Explain_Depesz_Com/View/Email.pm b/lib/Explain_Depesz_Com/View/Email.pm new file mode 100644 index 0000000..bf98a94 --- /dev/null +++ b/lib/Explain_Depesz_Com/View/Email.pm @@ -0,0 +1,33 @@ +package Explain_Depesz_Com::View::Email; + +use strict; +use base 'Catalyst::View::Email'; + +__PACKAGE__->config( + stash_key => 'email' +); + +=head1 NAME + +Explain_Depesz_Com::View::Email - Email View for Explain_Depesz_Com + +=head1 DESCRIPTION + +View for sending email from Explain_Depesz_Com. + +=head1 AUTHOR + +hubert depesz lubaczewski,,, + +=head1 SEE ALSO + +L + +=head1 LICENSE + +This library is free software, you can redistribute it and/or modify +it under the same terms as Perl itself. + +=cut + +1; diff --git a/lib/Explain_Depesz_Com/View/TT.pm b/lib/Explain_Depesz_Com/View/TT.pm new file mode 100644 index 0000000..630d942 --- /dev/null +++ b/lib/Explain_Depesz_Com/View/TT.pm @@ -0,0 +1,31 @@ +package Explain_Depesz_Com::View::TT; + +use strict; +use base 'Catalyst::View::TT'; + +__PACKAGE__->config(TEMPLATE_EXTENSION => '.tt'); + +=head1 NAME + +Explain_Depesz_Com::View::TT - TT View for Explain_Depesz_Com + +=head1 DESCRIPTION + +TT View for Explain_Depesz_Com. + +=head1 AUTHOR + +=head1 SEE ALSO + +L + +hubert depesz lubaczewski,,, + +=head1 LICENSE + +This library is free software, you can redistribute it and/or modify +it under the same terms as Perl itself. + +=cut + +1; diff --git a/root/favicon.ico b/root/favicon.ico new file mode 100644 index 0000000..5ad723d Binary files /dev/null and b/root/favicon.ico differ diff --git a/root/static/images/arrow_out.png b/root/static/images/arrow_out.png new file mode 100755 index 0000000..2e9bc42 Binary files /dev/null and b/root/static/images/arrow_out.png differ diff --git a/root/static/images/asterisk_orange.png b/root/static/images/asterisk_orange.png new file mode 100755 index 0000000..1ebebde Binary files /dev/null and b/root/static/images/asterisk_orange.png differ diff --git a/root/static/images/btn_120x50_built.png b/root/static/images/btn_120x50_built.png new file mode 100644 index 0000000..c709fd6 Binary files /dev/null and b/root/static/images/btn_120x50_built.png differ diff --git a/root/static/images/btn_120x50_built_shadow.png b/root/static/images/btn_120x50_built_shadow.png new file mode 100644 index 0000000..15142fe Binary files /dev/null and b/root/static/images/btn_120x50_built_shadow.png differ diff --git a/root/static/images/btn_120x50_powered.png b/root/static/images/btn_120x50_powered.png new file mode 100644 index 0000000..7249b47 Binary files /dev/null and b/root/static/images/btn_120x50_powered.png differ diff --git a/root/static/images/btn_120x50_powered_shadow.png b/root/static/images/btn_120x50_powered_shadow.png new file mode 100644 index 0000000..e6876c0 Binary files /dev/null and b/root/static/images/btn_120x50_powered_shadow.png differ diff --git a/root/static/images/btn_88x31_built.png b/root/static/images/btn_88x31_built.png new file mode 100644 index 0000000..007b5db Binary files /dev/null and b/root/static/images/btn_88x31_built.png differ diff --git a/root/static/images/btn_88x31_built_shadow.png b/root/static/images/btn_88x31_built_shadow.png new file mode 100644 index 0000000..ccf4624 Binary files /dev/null and b/root/static/images/btn_88x31_built_shadow.png differ diff --git a/root/static/images/btn_88x31_powered.png b/root/static/images/btn_88x31_powered.png new file mode 100644 index 0000000..8f0cd9f Binary files /dev/null and b/root/static/images/btn_88x31_powered.png differ diff --git a/root/static/images/btn_88x31_powered_shadow.png b/root/static/images/btn_88x31_powered_shadow.png new file mode 100644 index 0000000..aa776fa Binary files /dev/null and b/root/static/images/btn_88x31_powered_shadow.png differ diff --git a/root/static/images/catalyst_logo.png b/root/static/images/catalyst_logo.png new file mode 100644 index 0000000..21f1cac Binary files /dev/null and b/root/static/images/catalyst_logo.png differ diff --git a/script/convert_storage_to_sql.pl b/script/convert_storage_to_sql.pl new file mode 100755 index 0000000..95d1b47 --- /dev/null +++ b/script/convert_storage_to_sql.pl @@ -0,0 +1,34 @@ +#!/usr/bin/perl +use strict; +use warnings; +use Carp; +use English qw( -no_match_vars ); +use File::Spec; +use FindBin; +use Data::Dumper; +use autodie; +use POSIX qw( strftime ); + +my $source_dir = File::Spec->catfile( $FindBin::Bin, '..', 'storage', ); + +my $dir; +opendir $dir, $source_dir; +my @all_files = grep { ! /^\./ && -f File::Spec->catfile( $source_dir, $_ ) } readdir $dir; +closedir $dir; + +for my $file (@all_files) { + my $full_filename = File::Spec->catfile( $source_dir, $file ); + my $mtime = (stat($full_filename))[9]; + open my $fh, '<', $full_filename; + undef $/; + my $content = <$fh>; + close $fh; + printf "INSERT INTO plans (id, plan, entered_on) VALUES ('%s', \$PLAN\$%s\$PLAN\$, '%s');\n", + $file, + $content, + strftime('%Y-%m-%d %H:%M:%S', localtime $mtime); + +} + +exit; + diff --git a/script/explain_depesz_com_cgi.pl b/script/explain_depesz_com_cgi.pl new file mode 100755 index 0000000..098f959 --- /dev/null +++ b/script/explain_depesz_com_cgi.pl @@ -0,0 +1,30 @@ +#!/usr/bin/env perl + +use Catalyst::ScriptRunner; +Catalyst::ScriptRunner->run('Explain_Depesz_Com', 'CGI'); + +1; + +=head1 NAME + +explain_depesz_com_cgi.pl - Catalyst CGI + +=head1 SYNOPSIS + +See L + +=head1 DESCRIPTION + +Run a Catalyst application as a cgi script. + +=head1 AUTHORS + +Catalyst Contributors, see Catalyst.pm + +=head1 COPYRIGHT + +This library is free software. You can redistribute it and/or modify +it under the same terms as Perl itself. + +=cut + diff --git a/script/explain_depesz_com_create.pl b/script/explain_depesz_com_create.pl new file mode 100755 index 0000000..9ea635b --- /dev/null +++ b/script/explain_depesz_com_create.pl @@ -0,0 +1,57 @@ +#!/usr/bin/env perl + +use strict; +use warnings; + +use Catalyst::ScriptRunner; +Catalyst::ScriptRunner->run('Explain_Depesz_Com', 'Create'); + +1; + +=head1 NAME + +explain_depesz_com_create.pl - Create a new Catalyst Component + +=head1 SYNOPSIS + +explain_depesz_com_create.pl [options] model|view|controller name [helper] [options] + + Options: + --force don't create a .new file where a file to be created exists + --mechanize use Test::WWW::Mechanize::Catalyst for tests if available + --help display this help and exits + + Examples: + explain_depesz_com_create.pl controller My::Controller + explain_depesz_com_create.pl -mechanize controller My::Controller + explain_depesz_com_create.pl view My::View + explain_depesz_com_create.pl view MyView TT + explain_depesz_com_create.pl view TT TT + explain_depesz_com_create.pl model My::Model + explain_depesz_com_create.pl model SomeDB DBIC::Schema MyApp::Schema create=dynamic\ + dbi:SQLite:/tmp/my.db + explain_depesz_com_create.pl model AnotherDB DBIC::Schema MyApp::Schema create=static\ + dbi:Pg:dbname=foo root 4321 + + See also: + perldoc Catalyst::Manual + perldoc Catalyst::Manual::Intro + +=head1 DESCRIPTION + +Create a new Catalyst Component. + +Existing component files are not overwritten. If any of the component files +to be created already exist the file will be written with a '.new' suffix. +This behavior can be suppressed with the C<-force> option. + +=head1 AUTHORS + +Catalyst Contributors, see Catalyst.pm + +=head1 COPYRIGHT + +This library is free software. You can redistribute it and/or modify +it under the same terms as Perl itself. + +=cut diff --git a/script/explain_depesz_com_fastcgi.pl b/script/explain_depesz_com_fastcgi.pl new file mode 100755 index 0000000..0fc19e6 --- /dev/null +++ b/script/explain_depesz_com_fastcgi.pl @@ -0,0 +1,47 @@ +#!/usr/bin/env perl + +use Catalyst::ScriptRunner; +Catalyst::ScriptRunner->run('Explain_Depesz_Com', 'FastCGI'); + +1; + +=head1 NAME + +explain_depesz_com_fastcgi.pl - Catalyst FastCGI + +=head1 SYNOPSIS + +explain_depesz_com_fastcgi.pl [options] + + Options: + -? -help display this help and exits + -l --listen Socket path to listen on + (defaults to standard input) + can be HOST:PORT, :PORT or a + filesystem path + -n --nproc specify number of processes to keep + to serve requests (defaults to 1, + requires -listen) + -p --pidfile specify filename for pid file + (requires -listen) + -d --daemon daemonize (requires -listen) + -M --manager specify alternate process manager + (FCGI::ProcManager sub-class) + or empty string to disable + -e --keeperr send error messages to STDOUT, not + to the webserver + +=head1 DESCRIPTION + +Run a Catalyst application as fastcgi. + +=head1 AUTHORS + +Catalyst Contributors, see Catalyst.pm + +=head1 COPYRIGHT + +This library is free software. You can redistribute it and/or modify +it under the same terms as Perl itself. + +=cut diff --git a/script/explain_depesz_com_server.pl b/script/explain_depesz_com_server.pl new file mode 100755 index 0000000..0006b23 --- /dev/null +++ b/script/explain_depesz_com_server.pl @@ -0,0 +1,60 @@ +#!/usr/bin/env perl + +BEGIN { + $ENV{CATALYST_SCRIPT_GEN} = 40; +} + +use Catalyst::ScriptRunner; +Catalyst::ScriptRunner->run('Explain_Depesz_Com', 'Server'); + +1; + +=head1 NAME + +explain_depesz_com_server.pl - Catalyst Test Server + +=head1 SYNOPSIS + +explain_depesz_com_server.pl [options] + + -d --debug force debug mode + -f --fork handle each request in a new process + (defaults to false) + -? --help display this help and exits + -h --host host (defaults to all) + -p --port port (defaults to 3000) + -k --keepalive enable keep-alive connections + -r --restart restart when files get modified + (defaults to false) + -rd --restart_delay delay between file checks + (ignored if you have Linux::Inotify2 installed) + -rr --restart_regex regex match files that trigger + a restart when modified + (defaults to '\.yml$|\.yaml$|\.conf|\.pm$') + --restart_directory the directory to search for + modified files, can be set mulitple times + (defaults to '[SCRIPT_DIR]/..') + --follow_symlinks follow symlinks in search directories + (defaults to false. this is a no-op on Win32) + --background run the process in the background + --pidfile specify filename for pid file + + See also: + perldoc Catalyst::Manual + perldoc Catalyst::Manual::Intro + +=head1 DESCRIPTION + +Run a Catalyst Testserver for this application. + +=head1 AUTHORS + +Catalyst Contributors, see Catalyst.pm + +=head1 COPYRIGHT + +This library is free software. You can redistribute it and/or modify +it under the same terms as Perl itself. + +=cut + diff --git a/script/explain_depesz_com_test.pl b/script/explain_depesz_com_test.pl new file mode 100755 index 0000000..817b397 --- /dev/null +++ b/script/explain_depesz_com_test.pl @@ -0,0 +1,40 @@ +#!/usr/bin/env perl + +use Catalyst::ScriptRunner; +Catalyst::ScriptRunner->run('Explain_Depesz_Com', 'Test'); + +1; + +=head1 NAME + +explain_depesz_com_test.pl - Catalyst Test + +=head1 SYNOPSIS + +explain_depesz_com_test.pl [options] uri + + Options: + --help display this help and exits + + Examples: + explain_depesz_com_test.pl http://localhost/some_action + explain_depesz_com_test.pl /some_action + + See also: + perldoc Catalyst::Manual + perldoc Catalyst::Manual::Intro + +=head1 DESCRIPTION + +Run a Catalyst action from the command line. + +=head1 AUTHORS + +Catalyst Contributors, see Catalyst.pm + +=head1 COPYRIGHT + +This library is free software. You can redistribute it and/or modify +it under the same terms as Perl itself. + +=cut diff --git a/sql/create.sql b/sql/create.sql new file mode 100644 index 0000000..424ff47 --- /dev/null +++ b/sql/create.sql @@ -0,0 +1,44 @@ +CREATE TABLE plans ( + id TEXT PRIMARY KEY, + plan TEXT NOT NULL, + entered_on TIMESTAMPTZ NOT NULL DEFAULT now(), + is_public BOOL NOT NULL DEFAULT 'true' +); + +CREATE OR REPLACE FUNCTION get_random_string(string_length INT4) RETURNS TEXT +AS $BODY$ +DECLARE + possible_chars TEXT = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; + output TEXT = ''; + i INT4; + pos INT4; +BEGIN + FOR i IN 1..string_length LOOP + pos := 1 + cast( random() * ( length(possible_chars) - 1) as INT4 ); + output := output || substr(possible_chars, pos, 1); + END LOOP; + RETURN output; +END; +$BODY$ LANGUAGE 'plpgsql'; + + +CREATE OR REPLACE FUNCTION register_plan( in_plan TEXT, in_is_public bool ) RETURNS TEXT as $$ +DECLARE + use_hash_length int4 := 2; + use_hash text; +BEGIN + LOOP + use_hash := get_random_string(use_hash_length); + BEGIN + INSERT INTO plans (id, plan, is_public, entered_on) values (use_hash, in_plan, in_is_public, now()); + RETURN use_hash; + 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; +$$ language plpgsql; diff --git a/start_fcgi.pl b/start_fcgi.pl new file mode 100644 index 0000000..46421a1 --- /dev/null +++ b/start_fcgi.pl @@ -0,0 +1,2 @@ +#!/bin/bash +./script/explain_depesz_com_fastcgi.pl -l 127.0.0.1:50001 -n 2 -d -p /home/depesz/sites/Explain_Depesz_Com.pid diff --git a/t/01app.t b/t/01app.t new file mode 100644 index 0000000..ca27ea2 --- /dev/null +++ b/t/01app.t @@ -0,0 +1,7 @@ +use strict; +use warnings; +use Test::More tests => 2; + +BEGIN { use_ok 'Catalyst::Test', 'Explain_Depesz_Com' } + +ok( request('/')->is_success, 'Request should succeed' ); diff --git a/t/02pod.t b/t/02pod.t new file mode 100644 index 0000000..251640d --- /dev/null +++ b/t/02pod.t @@ -0,0 +1,9 @@ +use strict; +use warnings; +use Test::More; + +eval "use Test::Pod 1.14"; +plan skip_all => 'Test::Pod 1.14 required' if $@; +plan skip_all => 'set TEST_POD to enable this test' unless $ENV{TEST_POD}; + +all_pod_files_ok(); diff --git a/t/03podcoverage.t b/t/03podcoverage.t new file mode 100644 index 0000000..ae59d4c --- /dev/null +++ b/t/03podcoverage.t @@ -0,0 +1,9 @@ +use strict; +use warnings; +use Test::More; + +eval "use Test::Pod::Coverage 1.04"; +plan skip_all => 'Test::Pod::Coverage 1.04 required' if $@; +plan skip_all => 'set TEST_POD to enable this test' unless $ENV{TEST_POD}; + +all_pod_coverage_ok(); diff --git a/t/model_DBI.t b/t/model_DBI.t new file mode 100644 index 0000000..c52b28f --- /dev/null +++ b/t/model_DBI.t @@ -0,0 +1,6 @@ +use strict; +use warnings; +use Test::More tests => 1; + +BEGIN { use_ok 'Explain_Depesz_Com::Model::DBI' } + diff --git a/t/view_Email.t b/t/view_Email.t new file mode 100644 index 0000000..aa0d38f --- /dev/null +++ b/t/view_Email.t @@ -0,0 +1,6 @@ +use strict; +use warnings; +use Test::More tests => 1; + +BEGIN { use_ok 'Explain_Depesz_Com::View::Email' } + diff --git a/t/view_TT.t b/t/view_TT.t new file mode 100644 index 0000000..591c835 --- /dev/null +++ b/t/view_TT.t @@ -0,0 +1,6 @@ +use strict; +use warnings; +use Test::More tests => 1; + +BEGIN { use_ok 'Explain_Depesz_Com::View::TT' } + diff --git a/templates/contact.tt b/templates/contact.tt new file mode 100644 index 0000000..278df7a --- /dev/null +++ b/templates/contact.tt @@ -0,0 +1,43 @@ + + + + + + explain.depesz.com + [% PROCESS scripts.tt %] + [% PROCESS styles.tt %] + + +

=# explain depesz.com;

+
+ [% WRAPPER tabs.tt %][% END %] +
+
+ + [% IF errors.bad_email %] + + [% ELSIF email %] + + [% END %] + + + + + + + + + + + + + + + +
Provided email address is not valid!
Your email has been sent.
Your name:
Your email:
Your message:
+
+

Or you can simply mail me at depesz@depesz.com

+
+
+ + diff --git a/templates/help.tt b/templates/help.tt new file mode 100644 index 0000000..e6e7c12 --- /dev/null +++ b/templates/help.tt @@ -0,0 +1,76 @@ + + + + + + explain.depesz.com + [% PROCESS scripts.tt %] + [% PROCESS styles.tt %] + + +

=# explain depesz.com;

+
+ [% WRAPPER tabs.tt %][% END %] +
+

explain.depesz.com is tool for finding real cause for slow queries.

+ +

Generally, one would use EXPLAIN ANALYZE query; and read the output. The problem is that not all + parts of the output are easily understandable by anybody, and it's not always obvious whether node + that executes in 17.3ms is faster or slower than the one that runs in 100ms - given the fact that + the first one is executed 7 times.

+ +

To use the site, simply go to its first page and paste there explain analyze output from your psql.

+ +

This output could look like this.

+ +

After uploading you will be directed to page which shows parsed, and nicely (well, at least nice for me :) colorized to put emphasis on important parts. This could look like this.

+ +

Side note: the url for colorized output is persistent, so you can simply use it to show it to +others - for example - for those nice guys on irc channel #postgresql on freenode.

+ +

This graph uses 4 colours to mark important things:

+
    +
  • white background - everything is fine
  • +
  • yellow background - given node is worrying
  • +
  • brown background - given node is more worrying
  • +
  • red background - given node is very worrying
  • +
+ +

Which color is used, is choosen based on which mode you will use: "Exclusive", "Inclusive" or "Rows X".

+ +

Their meaning:

+ +

Exclusive

+

This is total amount of time PostgreSQL spent evaluating this node, without time spent in its subnodes. If the node has been executed many times (for example because of Nested Loop plan), this time will be correctly multiplied.

+

Colors:

+
    +
  • white background - is choosen if exclusive time <= 10% of total query time
  • +
  • yellow background - is choosen if exclusive time ∈ (10%, 50%> of total query time
  • +
  • brown background - is choosen if exclusive time ∈ (50%, 90%> of total query time
  • +
  • red background - is choosen if exclusive time > 90% of total query time
  • +
+

Inclusive

+

This is just like Exclusive, but it doesn't exclude time of subnodes. So, by definition - top node will have Inclusive time equal to total time of query.

+

Colors:

+
    +
  • white background - is choosen if inclusive time <= 10% of total query time
  • +
  • yellow background - is choosen if inclusive time ∈ (10%, 50%> of total query time
  • +
  • brown background - is choosen if inclusive time ∈ (50%, 90%> of total query time
  • +
  • red background - is choosen if inclusive time > 90% of total query time
  • +
+

Rows X

+

This value stores information about how big was planner mistake when it estimated return row count.

+

For example - if planner estimated that given node will return 230 rows, but it returned 14118 rows, the error is 14118/230 == 61.4.

+

It has to be noted that if the numbers were the other way around (estimated 14118, but really only 230), the Rows X would be the same. To show whether planner underestimated or overestimated - there is an arrow showing either ↓ - if planner underestimated rowcount, or ↑ if it overestimated.

+

Colors:

+
    +
  • white background - is choosen if rows-x <= 10
  • +
  • yellow background - is choosen if rows-x ∈ (10, 100>
  • +
  • brown background - is choosen if rows-x ∈ (100, 1000>
  • +
  • red background - is choosen if rows-x > 1000
  • +
+ +
+
+ + diff --git a/templates/history.tt b/templates/history.tt new file mode 100644 index 0000000..2d30116 --- /dev/null +++ b/templates/history.tt @@ -0,0 +1,41 @@ + + + + + + explain.depesz.com + [% PROCESS scripts.tt %] + [% PROCESS styles.tt %] + + [% previous_day = plans.0.day %] + [% jsarr = previous_day %] + [% FOREACH plan IN plans %] + [% day = plan.day %] + [% IF day != previous_day %] + [% jsarr = jsarr _ ':' _ day %] + [% previous_day = day %] + [% END %] + [% jsarr = jsarr _ ',' _ plan.id %] + [% END %] + +

=# explain depesz.com;

+
+ [% WRAPPER tabs.tt %][% END %] +
+ +
+
+ + diff --git a/templates/index.tt b/templates/index.tt new file mode 100644 index 0000000..272dbcb --- /dev/null +++ b/templates/index.tt @@ -0,0 +1,24 @@ + + + + + + explain.depesz.com + [% PROCESS scripts.tt %] + [% PROCESS styles.tt %] + + +

=# explain depesz.com;

+
+ [% WRAPPER tabs.tt %][% END %] +
+

Paste your explain / explain analyze here:

+
+
+ I want this plan to be visible on previous explains page:
+ +
+
+
+ + diff --git a/templates/scripts.tt b/templates/scripts.tt new file mode 100644 index 0000000..749d82b --- /dev/null +++ b/templates/scripts.tt @@ -0,0 +1,117 @@ + diff --git a/templates/show.tt b/templates/show.tt new file mode 100644 index 0000000..1131383 --- /dev/null +++ b/templates/show.tt @@ -0,0 +1,43 @@ + + +[% row_class = 'odd' %] +[% prefix = "    " %] +[% PROCESS show_macro.tt %] + + + + + explain.depesz.com + [% PROCESS scripts.tt %] + [% PROCESS styles.tt %] + + +

=# explain depesz.com;

+
+[% WRAPPER tabs.tt %] +
  • text
  • +
  • html
  • +[% END %] + +
    + + + + + + + + + + + + + [% print_row( explain.top_node, 0 ) %] + +
    ExclusiveInclusiveRows xNode
    +
    +
    + + diff --git a/templates/show_macro.tt b/templates/show_macro.tt new file mode 100644 index 0000000..d3e9b68 --- /dev/null +++ b/templates/show_macro.tt @@ -0,0 +1,108 @@ +[% MACRO print_row(node, level) BLOCK %] + +[% exclusive_point = 1 %] +[% inclusive_point = 1 %] + +[% IF explain.top_node.total_inclusive_time != '' AND explain.top_node.total_inclusive_time > 0 AND node.total_exclusive_time != '' AND node.total_inclusive_time != '' %] + [% exclusive_point = node.total_exclusive_time / explain.top_node.total_inclusive_time %] + [% IF exclusive_point > 0.9 %] + [% exclusive_point = 4 %] + [% ELSIF exclusive_point > 0.5 %] + [% exclusive_point = 3 %] + [% ELSIF exclusive_point > 0.1 %] + [% exclusive_point = 2 %] + [% ELSE %] + [% exclusive_point = 1 %] + [% END %] + + [% inclusive_point = node.total_inclusive_time / explain.top_node.total_inclusive_time %] + [% IF inclusive_point > 0.9 %] + [% inclusive_point = 4 %] + [% ELSIF inclusive_point > 0.5 %] + [% inclusive_point = 3 %] + [% ELSIF inclusive_point > 0.1 %] + [% inclusive_point = 2 %] + [% ELSE %] + [% inclusive_point = 1 %] + [% END %] +[% END %] + +[% rows_x = 0 %] +[% rows_x_mark = '' %] +[% rows_point = 1 %] +[% IF node.estimated_rows != '' && node.actual_rows != '' && node.estimated_rows > 0 && node.actual_rows > 0 %] + [% IF node.actual_rows > node.estimated_rows %] + [% rows_x = node.actual_rows / node.estimated_rows %] + [% rows_x_mark = '↓' %] + [% ELSE %] + [% rows_x = node.estimated_rows / node.actual_rows %] + [% rows_x_mark = '↑' %] + [% END %] + [% IF rows_x > 1000 %] + [% rows_point = 4 %] + [% ELSIF rows_x > 100 %] + [% rows_point = 3 %] + [% ELSIF rows_x > 10 %] + [% rows_point = 2 %] + [% ELSE %] + [% rows_point = 1 %] + [% END %] +[% END %] + + + [% row_class = ( row_class == 'odd' ? 'even' : 'odd' ) %] + [% node.total_exclusive_time | format("%.3f") %] + [% node.total_inclusive_time | format("%.3f") %] + [% rows_x_mark %] [% rows_x | format("%.1f") %] + +
    + [% IF (level > 0) %] + -> + [% END %] + [% node.type %] + [% IF node.type == "Bitmap Heap Scan" %] on [% node.scan_on.table_name %] [% node.scan_on.table_alias %][% END %] + [% IF node.type == "Bitmap Index Scan" %] on [% node.scan_on.index_name %][% END %] + [% IF node.type == "Index Scan" %] using [% node.scan_on.index_name %] on [% node.scan_on.table_name %] [% node.scan_on.table_alias %][% END %] + [% IF node.type == "Seq Scan" %] on [% node.scan_on.table_name %] [% node.scan_on.table_alias %][% END %] + (cost=[% node.estimated_startup_cost %]..[% node.estimated_total_cost %] rows=[% node.estimated_rows %] width=[% node.estimated_row_width %]) + (actual time=[% node.actual_time_first %]..[% node.actual_time_last %] rows=[% node.actual_rows %] loops=[% node.actual_loops %]) + [% IF node.extra_info %] +
      + [% FOREACH line IN node.extra_info %]
    • [% line | html %]
    • [% END %] +
        + [% END %] +
    + + +[% IF node.initplans %] + + + + +
    + InitPlan ( for [% node.type %] ) +
    + + +[% FOREACH subnode IN node.initplans %] +[% print_row(subnode, level + 1) %] +[% END %] +[% END %] +[% FOREACH subnode IN node.sub_nodes %] +[% print_row(subnode, level + 1) %] +[% END %] +[% IF node.subplans %] + + + + +
    + SubPlan ( for [% node.type %] ) +
    + + +[% FOREACH subnode IN node.subplans %] +[% print_row(subnode, level + 1) %] +[% END %] +[% END %] +[% END %] diff --git a/templates/styles.tt b/templates/styles.tt new file mode 100644 index 0000000..ada0ecb --- /dev/null +++ b/templates/styles.tt @@ -0,0 +1,96 @@ + diff --git a/templates/tabs.tt b/templates/tabs.tt new file mode 100644 index 0000000..982dfd3 --- /dev/null +++ b/templates/tabs.tt @@ -0,0 +1,7 @@ +