App-AltSQL

 view release on metacpan or  search on metacpan

Changes  view on Meta::CPAN

Revision history for App::AltSQL

v0.05 Fri, 01 Jun 2012 12:49:25 -0400
  
  - Adding pre-release Term::Plugin::SyntaxHighlight and View::Plugin::VerticalSorted
  - Customizable prompt in altsql config with substitutions (and test file)
  - Cache autocomplete and fix minor issue with it
  - Allow CLI -p to override .my.cnf if empty string
  - Refactor Model::MySQL render_prompt into parse_prompt
  - Add setup() method to subclasses
  - View::Plugin::UnicodeBox header_reminder option (default: 100)

v0.04 Tue, 29 May 2012 08:51:45 -0400

  - Fixing build dependency issue

v0.03  Fri, 25 May 2012 15:39:15 -0400

  - Adding Dump plugin [contributed by https://github.com/joeshin]
  - Parsing of .my.cnf w/ tests [contributed by https://github.com/spikegrobstein]
  - Change the prompt with .my.cnf 'prompt' setting
  - Adding INSTALL document and some minor build changes
  - Use JSON instead of JSON::XS
  - Make non-critical dependencies optional

v0.02  Fri May 18 10:33:00 2012 -0400

  - Remove unused dependency on Switch

v0.01  Wed May 16 10:07:51 2012 -0400

MANIFEST  view on Meta::CPAN

Build.PL
Makefile.PL
MANIFEST			This list of files
MANIFEST.SKIP
META.json
META.yml
README.pod
INSTALL
t/001_arguments.t
t/002_my_cnf.t
t/003_custom_prompt.t
t/sql/sakila-data.sql
t/sql/sakila-schema.sql
lib/App/AltSQL/View/Plugin/VerticalSorted.pm
lib/App/AltSQL/Term/Plugin/SyntaxHighlight.pm
SIGNATURE    Added here by Module::Build

META.json  view on Meta::CPAN

{
   "abstract" : "A drop in replacement to the MySQL prompt with a pluggable Perl interface",
   "author" : [
      "Eric Waters <ewaters@gmail.com>"
   ],
   "dynamic_config" : 1,
   "generated_by" : "Module::Build version 0.4, CPAN::Meta::Converter version 2.120921",
   "license" : [
      "perl_5"
   ],
   "meta-spec" : {
      "url" : "http://search.cpan.org/perldoc?CPAN::Meta::Spec",

META.yml  view on Meta::CPAN

---
abstract: 'A drop in replacement to the MySQL prompt with a pluggable Perl interface'
author:
  - 'Eric Waters <ewaters@gmail.com>'
build_requires:
  Data::Structure::Util: 0
  File::Temp: 0
  Test::Deep: 0
  Test::More: 0
configure_requires:
  Module::Build: 0.4
dynamic_config: 1

README.pod  view on Meta::CPAN

=encoding utf-8

=head1 NAME

App::AltSQL - A drop in replacement to the MySQL prompt with a pluggable Perl interface

=head1 SYNOPSIS

  ./altsql -h <host> -u <username> -D <database> -p<password>

  altsql> select * from film limit 4;
  ╒═════════╤══════════════════╤════════════════════════════
  │ film_id │ title            │ description                
  ╞═════════╪══════════════════╪════════════════════════════
  │       1 │ ACADEMY DINOSAUR │ A Epic Drama of a Feminist 

SIGNATURE  view on Meta::CPAN

SHA1 3777b4e109f92b1c23133e2b8d0c0d9d3921a3af lib/App/AltSQL/Plugin/Tail.pm
SHA1 f6f428f3638ce210530ae2f0bd0ca013fca1d5f3 lib/App/AltSQL/Role.pm
SHA1 77e74e69405edaf19d1e754a76f819ace1f4c66c lib/App/AltSQL/Term.pm
SHA1 e2317928d205a77cb468b6b62467309da14c65c0 lib/App/AltSQL/Term/Plugin/SyntaxHighlight.pm
SHA1 6f33fba35b8fba9fc394433b758d5ad75ff93d3e lib/App/AltSQL/View.pm
SHA1 fcedef0e932f6fb7075a3efb972f1d0024ca7d4a lib/App/AltSQL/View/Plugin/Color.pm
SHA1 b9fc3f044721e50774eee7f6d1237372a9ddbdd2 lib/App/AltSQL/View/Plugin/UnicodeBox.pm
SHA1 1ab97c3a0aed03f82381e8f93d28786e58c05008 lib/App/AltSQL/View/Plugin/VerticalSorted.pm
SHA1 9bdf133e2624f3313301047fb9f3fa6cd04a90fe t/001_arguments.t
SHA1 23bfff35f585a5ed0b41663d10a170c394a4e13a t/002_my_cnf.t
SHA1 2dc4a0db89a14e56f423730c5ed7f33d495c847e t/003_custom_prompt.t
SHA1 f766cc9f9823bb28809c7c5110754941b8428599 t/sql/sakila-data.sql
SHA1 4c2ea3fd4b8dfd1b9fb8bf40df59dca068673708 t/sql/sakila-schema.sql
-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.4.11 (GNU/Linux)

iEYEARECAAYFAk/I8+8ACgkQHHIpFH/pKkIRKgCfYW0r4GZn9VRB/h0lCjsWOMC3
XOAAn06qVRa/QLTufTRIB/Ux+glV+JGr
=6Whb
-----END PGP SIGNATURE-----

lib/App/AltSQL.pm  view on Meta::CPAN

package App::AltSQL;

=encoding utf-8

=head1 NAME

App::AltSQL - A drop in replacement to the MySQL prompt with a pluggable Perl interface

=head1 SYNOPSIS

  ./altsql -h <host> -u <username> -D <database> -p<password>

  altsql> select * from film limit 4;
  ╒═════════╤══════════════════╤════════════════════════════
  │ film_id │ title            │ description                
  ╞═════════╪══════════════════╪════════════════════════════
  │       1 │ ACADEMY DINOSAUR │ A Epic Drama of a Feminist 

lib/App/AltSQL.pm  view on Meta::CPAN

=head1 DESCRIPTION

AltSQL is a way to improve your user experience with C<mysql>, C<sqlite3>, C<psql> and other tools that Perl has L<DBI> drivers for.  Currently written for MySQL only, the long term goal of this project is to provide users of the various SQL-based da...

There are a few key issues that this programmer has had with using the mysql client every day.  After looking for alternatives and other ways to fix the problems, reimplementing the client in Perl seemed like the easiest approach, and lent towards th...

=over 4

=item Ctrl-C kills the program

All of the shells that we used on a daily basis allow you to abandon the half-written statement on the prompt by typing Ctrl-C.  Spending all day in shells, you expect this behavior to be consistent, but you do this in mysql and you will be thrown to...

=item Wide output wraps

We are grateful that mysql at least uses ASCII art for table formatting (unlike C<sqlite3> for some reason).  But there are some tables that I work with that have many columns, with long names (it's often easier to keep adding columns to a table over...

Suffice it to say, it's a much better experience if, just like with C<git diff>, wide output is left wide, and you are optionally able to scroll horizontally with your arrow keys like you wanted in the first place.

=item Color

Most other modern programs we developers use on a daily basis (vim, ls, top, git, tmux, screen) offer to provide additional context to you via color.  By consistently setting colors on a variable type or file type, programs can convey to us additiona...

lib/App/AltSQL.pm  view on Meta::CPAN


=item /etc/altsql.(yml|cnf|ini|js|pl)

=item ~/.altsql.(yml|cnf|ini|js|pl)

Write your configuration file to either the system or the local configuration locations.  The local file will inherit from the global configuration but with local modifications.  For purposes of this example I'll be writing out the config in YAML, bu...

=back

  ---
  prompt: 'altsql> '

  plugins:
    - Tail
    - Dump

  view_plugins:
    - Color
    - UnicodeBox

  App::AltSQL::View::Plugin::Color:

lib/App/AltSQL.pm  view on Meta::CPAN


  App::AltSQL::View::Plugin::UnicodeBox:
    style: heavy_header
    split_lines: 1
    plain_ascii: 0
  
This is the default configuration, and currently encompasses all the configurable settings.  This should be future safe; as you can see, plugins may use this file for their own variables as there are namespaced sections.

=over 4

=item B<prompt>

  prompt: "%u@%h[%d]> "
  # 'username@hostname[database]> '
  prompt: "%c{red}%u%c{reset} %t{%F %T}> '
  # 'username' (in red) ' YYYY-MM-DD HH:MM:SS> '

Provide a custom prompt.  The following variables will be interpolated:

=over 4

=item B<%u>

The username used to connect to the model

=item B<%d>

The current database or '(none)'

lib/App/AltSQL/Model/MySQL.pm  view on Meta::CPAN

=over 4

=item B<user>

=item B<password>

=item B<host>

=item B<port>

=item B<prompt>

=item B<safe_update>

=item B<select_limit>

=tiem B<no_auto_rehash>

=back

=cut

lib/App/AltSQL/Model/MySQL.pm  view on Meta::CPAN

	};
	if ($@) {
		return 0; # when we use this we check for definedness as well as boolean
	}
	return DBIx::MyParsePP->new();
});
has 'dbh'        => (is => 'rw');
has 'current_database' => (is => 'rw');

has [qw(host user password database port)] => ( is => 'ro' );
has [qw(no_auto_rehash select_limit safe_update prompt)] => ( is => 'ro' );

sub args_spec {
	return (
		host => {
			cli  => 'host|h=s',
			help => '-h HOSTNAME | --host HOSTNAME',
		},
		user => {
			cli  => 'user|u=s',
			help => '-u USERNAME | --user USERNAME',

lib/App/AltSQL/Model/MySQL.pm  view on Meta::CPAN

			cli  => 'no-auto-rehash|A',
			help => "-A --no-auto-rehash -- Don't scan the information schema for tab autocomplete data",
		},
	);
}

sub setup {
	my $self = shift;
	$self->find_and_read_configs();

	# If the user has configured a custom prompt in .my.cnf and not one in the config, use that in the Term instance
	if ($self->prompt && ! $self->app->config->{prompt}) {
		$self->app->term->prompt( $self->parse_prompt() );
	}
}

sub find_and_read_configs {
	my $self = shift;
	my @config_paths = ( 
		"$ENV{HOME}/.my.cnf",
	);

	foreach my $path (@config_paths) {
		(-e $path) or next;
		$self->read_my_dot_cnf($path);
	}
}

sub read_my_dot_cnf {
	my $self = shift;
	my $path = shift;

	my @valid_keys = qw( user password host port database prompt safe_update select_limit no_auto_rehash ); # keys we'll read
	my @valid_sections = qw( client mysql ); # valid [section] names
	my @boolean_keys = qw( safe_update no_auto_rehash );

	open MYCNF, "<$path";

	# ignore lines in file until we hit a valid [section]
	# then read key=value pairs
	my $in_valid_section = 0;
	while(<MYCNF>) {

lib/App/AltSQL/Model/MySQL.pm  view on Meta::CPAN

sub show_sql_error {
	my ($self, $input, $char_number, $line_number) = @_;

	my @lines = split /\n/, $input;
	my $line = $lines[ $line_number - 1 ];
	$self->log_error("There was an error parsing the SQL statement on line $line_number:");
	$self->log_error($line);
	$self->log_error(('-' x ($char_number - 1)) . '^');
}

my %prompt_substitutions = (
	S    => ';',
	"'"  => "'",
	'"'  => '"',
	v    => 'TODO-server-version',
	p    => sub { shift->{self}->port },
	'\\' => '\\',
	n    => "\n",
	t    => "\t",
	'_'  => ' ',
	' '  => ' ',

lib/App/AltSQL/Model/MySQL.pm  view on Meta::CPAN

	O    => '%t{%b}',
	R    => '%t{%k}',
	r    => '%t{%I}',
	m    => '%t{%M}',
	s    => '%t{%S}',
	P    => '%t{%p}',
);

=cut

Take a .my.cnf prompt format and convert it into Term escape options

Reference:
http://www.thegeekstuff.com/2010/02/mysql_ps1-6-examples-to-make-your-mysql-prompt-like-angelina-jolie/

=cut

sub parse_prompt {
	my $self = shift;

	my $parsed_prompt = $self->prompt;
	$parsed_prompt =~ s{\\\\(.)}{
		my $substitute = $prompt_substitutions{$1};
		if (! $substitute) {
			"$1";
		}
		elsif (ref $substitute) {
			$substitute->($self);
		}
		else {
			$substitute;
		}
	}exg;

	return $parsed_prompt;
}

no Moose;
__PACKAGE__->meta->make_immutable;

=head1 COPYRIGHT

Copyright (c) 2012 Eric Waters and Shutterstock Images (http://shutterstock.com).  All rights reserved.  This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself.

The full text of the license can be found in the LICENSE file included with this module.

lib/App/AltSQL/Term.pm  view on Meta::CPAN

use JSON qw(encode_json decode_json);
use Term::ANSIColor;

with 'App::AltSQL::Role';
with 'MooseX::Object::Pluggable';

has 'term' => (
	is         => 'ro',
	lazy_build => 1,
);
has 'prompt' => (
	is      => 'rw',
	default => 'altsql> ',
);
has 'history_fn'           => ( is => 'ro' );
has 'autocomplete_entries' => ( is => 'rw' );

sub args_spec {
	return (
		history_fn => {
			cli     => 'history=s',

lib/App/AltSQL/Term.pm  view on Meta::CPAN

	);
}

sub BUILD {
	my $self = shift;
	$self->log_info("Ctrl-C to reset the line; Ctrl-D to exit");
}

sub setup {
	my $self = shift;
	if (my $custom_prompt = $self->app->config->{prompt}) {
		$self->prompt($custom_prompt);
	}
}

sub _build_term {
	my $self = shift;

	my $term = Term::ReadLine::Zoid->new("altsql-shell");
	$self->{term} = $term;

	# Require the tab key to be hit twice before showing the list of autocomplete items

lib/App/AltSQL/Term.pm  view on Meta::CPAN

		$self->term->accept_line();
	}
	else {
		$self->term->insert_line();
	}
}

sub readline {
	my $self = shift;

	return $self->term->readline($self->render_prompt());
}

sub completion_function {
	my ($self, $word, $buffer, $start) = @_;

	#$self->log_debug("\ncompletion_function: '$word', '$buffer', '$start'");

	my $hash = $self->autocomplete_entries;
	return () unless $hash;

lib/App/AltSQL/Term.pm  view on Meta::CPAN

	my ($width, $height) = $self->term->TermSize();
	return $width;
}

sub get_term_height {
	my $self = shift;
	my ($width, $height) = $self->term->TermSize();
	return $height;
}

my %prompt_substitutions = (
	u => sub { shift->{self}->app->model->user },
	d => sub { shift->{self}->app->model->current_database || '(none)' },
	h => sub { shift->{self}->app->model->host },
	'%' => '%',
);

my %block_prompt_substitutions = (
	c => sub {
		my ($context, $block) = @_;
		return color($block);
	},
	e => sub {
		my ($context, $block) = @_;
		# Make '$self' expected in the current scope so the $block can reference it
		my $self = $context->{self};
		my $return = eval $block;
		if (my $ex = $@) {

lib/App/AltSQL/Term.pm  view on Meta::CPAN

	t => sub {
		my ($context, $format) = @_;
		my $now = $context->{date};
		if (! $now) {
			return 'err';
		}
		return $now->strftime($format);
	},
);

sub render_prompt {
	my ($self, $now) = @_;

	if (! defined $self->{_has_datetime}) {
		eval { require DateTime; };
		$self->{_has_datetime} = $@ ? 0 : 1;
	}

	if (! $now && $self->{_has_datetime}) {
		$now = DateTime->now( time_zone => 'local' );
	}

	my %context = (
		self => $self,
		date => $now,
	);

	my $prompt = $self->prompt;
	my $output = '';

	while (length $prompt) {
		my $char = substr $prompt, 0, 1, '';

		# We're looking for a closing brace
		if ($context{requires_block}) {
			if ($char eq '}' && --$context{brace_count} == 0) {
				# Block is complete
				my $sub = $block_prompt_substitutions{ $context{symbol} };
				$output .= $sub->(\%context, delete $context{block});
				delete $context{requires_block};
				delete $context{brace_count};
				next;
			}

			$context{block} .= $char;

			if ($char eq '{') {
				$context{brace_count}++;
			}

			next;
		}

		if ($char eq '%') {
			$context{symbol} = substr $prompt, 0, 1, '';
			if ($block_prompt_substitutions{ $context{symbol} } && substr($prompt, 0, 1) eq '{') {
				substr $prompt, 0, 1, ''; # shift the '{'
				$context{requires_block} = 1;
				$context{block} = '';
				$context{brace_count} = 1;
			}
			else {
				my $sub = $prompt_substitutions{ $context{symbol} };
				if (! $sub) {
					$self->log_error("Unrecognized prompt substitution '$context{symbol}'");
					$output .= $char;
				}
				elsif (ref $sub) {
					$output .= $sub->(\%context);
				}
				else {
					$output .= $sub;
				}
			}
			next;

t/002_my_cnf.t  view on Meta::CPAN


	my $filename = write_config(<<ENDFILE);
[client]
user = ewaters
password = 12345
host = localhost

[mysql]
database = sakila
default-character-set = utf8
prompt = \\u@\\h[\\R:\\m:\\s]>
safe-update = false
ENDFILE

	$instance->read_my_dot_cnf($filename);
	unbless( $instance );
	cmp_deeply(
		$instance,
		superhashof({
			user        => 'ewaters',
			password    => '12345',
			host        => 'localhost',
			database    => 'sakila',
			safe_update => 0,
			prompt      => '\\u@\\h[\\R:\\m:\\s]>',
		}),
		'Multi-section my.cnf',
	);
	unlink $filename;
}
{
	my $instance = App::AltSQL::Model::MySQL->new( app => $app );

	my $filename = write_config(<<ENDFILE);
[client]
user = ewaters
password = 12345
host = localhost

[mysql]
database = sakila
default-character-set = utf8
prompt = \\u@\\h[\\R:\\m:\\s]>
safe-update = false
ENDFILE

	$instance->read_my_dot_cnf($filename);
	unbless($instance);
	cmp_deeply(
		$instance,
		superhashof({
			user        => 'ewaters',
			password    => '12345',
			host        => 'localhost',
			database    => 'sakila',
			safe_update => 0,
			prompt      => '\\u@\\h[\\R:\\m:\\s]>',
		}),
		'Multi-section my.cnf',
	);
	unlink $filename;
}

{
	my $instance = App::AltSQL::Model::MySQL->new( app => $app );

	my $filename = write_config(<<ENDFILE);

t/003_custom_prompt.t  view on Meta::CPAN

my $term = App::AltSQL::Term->new(
	app => $app,
);
$app->{term} = $term;

#$model->setup();
#$term->setup();

## Testing

$term->prompt('myprompt> ');
is $term->render_prompt(), 'myprompt> ', "Basic, non-special prompt";

$term->prompt('myprompt%%> ');
is $term->render_prompt(), 'myprompt%> ', "Escaped percent sign";

$term->prompt('%u@%h> ');
is $term->render_prompt(), 'testuser@localhost> ', "Some substitutions";

$term->prompt('(%u@%h) [%d]> ');
is $term->render_prompt(), '(testuser@localhost) [(none)]> ', "Issue #28, without database"; 

$model->current_database('saklia');

$term->prompt('(%u@%h) [%d]> ');
is $term->render_prompt(), '(testuser@localhost) [saklia]> ', "Issue #28, with database"; 

## Perl eval

$term->prompt('%e{ ++( $self->{_statement_counter} ) }> ');
is $term->render_prompt(), '1> ', "Arbitrary perl";
is $term->render_prompt(), '2> ', "Arbitrary perl, part two";

$term->prompt('%e{ missing end brace');
is $term->render_prompt(), '', "Missing end brace";

$term->prompt('%e{ invalid perl code }');
is $term->render_prompt(), 'err', "Invalid perl code";

## Color

$term->prompt('%c{bold}%u%c{reset}%c{bold red}@%h%c{reset}> ');
is $term->render_prompt(),
	colored('testuser', 'bold') . colored('@localhost', 'bold red') . '> ',
	"Colored prompt";

## MySQL .my.cnf format

$model->{prompt} = '\\\\u@\\\\h[\\\\d]\\\\_';
is $model->parse_prompt(), '%u@%h[%d] ', "MySQL-style prompt converted into local style";

$model->{prompt} = '(\\\\u@\\\\h) [\\\\d]>\\\\_';
$term->prompt( $model->parse_prompt );
is $term->render_prompt(), '(testuser@localhost) [saklia]> ', "Issue #28 from .my.cnf";

## DateTime testing

eval { require DateTime; };
if ($@) {
	print STDERR "Can't continue testing without DateTime\n";
	done_testing;
	exit;
}

my $now = DateTime->now();

$term->prompt('%u@%h[%t{%H:%M:%S}]> ');
is $term->render_prompt($now), 'testuser@localhost['.$now->strftime('%H:%M:%S').']> ', "Simple DateTime substitution";

done_testing;



( run in 1.263 second using v1.01-cache-2.11-cpan-6aa56a78535 )