summaryrefslogtreecommitdiffstats
path: root/lib
diff options
context:
space:
mode:
authorJesse Luehrs <doy@tozt.net>2016-08-13 16:42:57 -0400
committerJesse Luehrs <doy@tozt.net>2016-08-13 16:42:57 -0400
commita59e045a5bbafac95c3ebcaeafcce81e853bd332 (patch)
tree3bf7ae29826a0e372d06b30bc94d98c76ed0f45a /lib
parent7d5efb6cc442b311b3c8680d2b0214080b8a68b5 (diff)
downloadchart-clicker-sql-a59e045a5bbafac95c3ebcaeafcce81e853bd332.tar.gz
chart-clicker-sql-a59e045a5bbafac95c3ebcaeafcce81e853bd332.zip
completion
Diffstat (limited to 'lib')
-rw-r--r--lib/Chart/Clicker/SQL/REPL.pm144
1 files changed, 144 insertions, 0 deletions
diff --git a/lib/Chart/Clicker/SQL/REPL.pm b/lib/Chart/Clicker/SQL/REPL.pm
index 1c3502b..7955793 100644
--- a/lib/Chart/Clicker/SQL/REPL.pm
+++ b/lib/Chart/Clicker/SQL/REPL.pm
@@ -9,8 +9,10 @@ use Browser::Open;
use File::HomeDir;
use File::Spec;
use File::Temp;
+use List::Util;
use Module::Runtime;
use Path::Class;
+use Scalar::Util;
use Term::ReadLine;
use Try::Tiny;
@@ -44,6 +46,13 @@ has rl => (
for my $line (file($self->history_file)->slurp(chomp => 1)) {
$rl->addhistory($line);
}
+ Scalar::Util::weaken(my $weakself = $self);
+ $rl->Attribs->{completion_entry_function} = sub {
+ ()
+ };
+ $rl->Attribs->{attempted_completion_function} = sub {
+ $weakself->_attempt_completion(@_);
+ };
return $rl;
},
);
@@ -160,6 +169,141 @@ sub _can_draw ($self) {
$self->initialized && $self->has_last_query
}
+sub _attempt_completion ($self, $text, $line, $start, $end) {
+ my ($initial_word) = $line =~ /^(\S+)\s+/;
+ my @matches = $self->_get_completions($text, $initial_word);
+ return $self->rl->completion_matches($text, sub ($text, $index) {
+ return $matches[$index];
+ });
+}
+
+sub _get_completions ($self, $text, $initial_word) {
+ if ($initial_word) {
+ my $completion_method = "_complete_$initial_word";
+ return unless $self->can($completion_method);
+ return $self->$completion_method($text);
+ }
+ else {
+ my @keywords = grep {
+ !/^_/
+ } grep {
+ !Moose::Object->can($_)
+ } $self->meta->get_all_method_names;
+
+ return $self->_keyword_complete($text, \@keywords);
+ }
+}
+
+sub _complete_select ($self, $text) {
+ $self->_keyword_complete(
+ $text,
+ [
+ $self->_sql_keywords,
+ $self->_sql_functions,
+ $self->_sql_tables,
+ $self->_sql_columns,
+ ]
+ )
+}
+
+sub _complete_timechart ($self, $text) {
+ $self->_keyword_complete($text, [ qw(on off) ])
+}
+
+for my $method (qw(width height)) {
+ __PACKAGE__->meta->add_method("_complete_$method" => sub ($self, $text) {
+ $self->_default_complete($text, $method)
+ })
+}
+
+sub _default_complete ($self, $text, $method) {
+ $self->_keyword_complete($text, [ $self->$method ])
+}
+
+sub _keyword_complete ($self, $text, $keywords) {
+ my ($prefix) = $text =~ /.*\b(\w+)$/;
+ $prefix ||= '';
+ my $capital = $prefix eq uc($prefix);
+ return map {
+ $prefix . substr($capital ? $_ : lc, length($prefix))
+ } grep {
+ /^\Q$prefix/i
+ } $keywords->@*;
+}
+
+sub _sql_keywords ($self) {
+ # just using sqlite keywords for now, but would be nice to switch this on
+ # the backend engine in use based on the dsn at some point
+
+ # https://www.sqlite.org/lang_keywords.html
+ qw(
+ ABORT ACTION ADD AFTER ALL ALTER ANALYZE AND AS ASC ATTACH
+ AUTOINCREMENT BEFORE BEGIN BETWEEN BY CASCADE CASE CAST CHECK COLLATE
+ COLUMN COMMIT CONFLICT CONSTRAINT CREATE CROSS CURRENT_DATE
+ CURRENT_TIME CURRENT_TIMESTAMP DATABASE DEFAULT DEFERRABLE DEFERRED
+ DELETE DESC DETACH DISTINCT DROP EACH ELSE END ESCAPE EXCEPT EXCLUSIVE
+ EXISTS EXPLAIN FAIL FOR FOREIGN FROM FULL GLOB GROUP HAVING IF IGNORE
+ IMMEDIATE IN INDEX INDEXED INITIALLY INNER INSERT INSTEAD INTERSECT
+ INTO IS ISNULL JOIN KEY LEFT LIKE LIMIT MATCH NATURAL NO NOT NOTNULL
+ NULL OF OFFSET ON OR ORDER OUTER PLAN PRAGMA PRIMARY QUERY RAISE
+ RECURSIVE REFERENCES REGEXP REINDEX RELEASE RENAME REPLACE RESTRICT
+ RIGHT ROLLBACK ROW SAVEPOINT SELECT SET TABLE TEMP TEMPORARY THEN TO
+ TRANSACTION TRIGGER UNION UNIQUE UPDATE USING VACUUM VALUES VIEW
+ VIRTUAL WHEN WHERE WITH WITHOUT
+ )
+}
+
+sub _sql_functions ($self) {
+ # just using sqlite functions for now, but would be nice to switch this on
+ # the backend engine in use based on the dsn at some point
+
+ my @functions = (
+ # https://www.sqlite.org/lang_corefunc.html
+ qw(
+ abs changes char coalesce glob ifnull instr hex last_insert_rowid
+ length like likelihood likely load_extension lower ltrim max min
+ nullif printf quote random randomblob replace round rtrim soundex
+ sqlite_compileoption_get sqlite_compileoption_used sqlite_source_id
+ sqlite_version substr total_changes trim typeof unlikely unicode
+ upper zeroblob
+ ),
+ # https://www.sqlite.org/lang_datefunc.html
+ qw(
+ date time datetime julianday strftime
+ ),
+ # https://www.sqlite.org/lang_aggfunc.html
+ qw(
+ avg count group_concat max min sum total
+ ),
+ # https://www.sqlite.org/json1.html
+ qw(
+ json json_array json_array_length json_extract json_insert
+ json_object json_remove json_replace json_set json_type json_valid
+ json_quote json_group_array json_group_object json_each json_tree
+ ),
+ );
+
+ map { "$_(" } @functions
+}
+
+sub _sql_tables ($self) {
+ return unless $self->initialized;
+ my @tables = $self->sql->dbh->tables;
+ my @table_components = map { split /\./ } @tables;
+ my @all_tables = (@tables, @table_components);
+ return (
+ @all_tables,
+ map { s/"//gr } @all_tables,
+ );
+}
+
+sub _sql_columns ($self) {
+ return unless $self->initialized;
+ my $sth = $self->sql->dbh->column_info(undef, undef, undef, undef);
+ my $rows = $sth->fetchall_arrayref;
+ return List::Util::uniq map { $_->[3] } $rows->@*;
+}
+
sub run ($self) {
while (1) {
my $prompt = "> ";