oculus 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. data/.gitignore +17 -0
  2. data/.travis.yml +7 -0
  3. data/Gemfile +3 -0
  4. data/LICENSE +22 -0
  5. data/README.md +34 -0
  6. data/Rakefile +18 -0
  7. data/TODO.md +12 -0
  8. data/bin/oculus +32 -0
  9. data/features/query.feature +26 -0
  10. data/features/step_definitions/query_steps.rb +35 -0
  11. data/features/support/env.rb +22 -0
  12. data/lib/oculus/connection/mysql2.rb +22 -0
  13. data/lib/oculus/connection.rb +7 -0
  14. data/lib/oculus/presenters/query_presenter.rb +40 -0
  15. data/lib/oculus/presenters.rb +1 -0
  16. data/lib/oculus/query.rb +61 -0
  17. data/lib/oculus/server/public/css/bootstrap.min.css +689 -0
  18. data/lib/oculus/server/public/css/codemirror.css +112 -0
  19. data/lib/oculus/server/public/css/reset.css +47 -0
  20. data/lib/oculus/server/public/css/style.css +107 -0
  21. data/lib/oculus/server/public/img/glyphicons-halflings-white.png +0 -0
  22. data/lib/oculus/server/public/img/glyphicons-halflings.png +0 -0
  23. data/lib/oculus/server/public/js/application.js +160 -0
  24. data/lib/oculus/server/public/js/bootstrap.min.js +6 -0
  25. data/lib/oculus/server/public/js/codemirror.min.js +1 -0
  26. data/lib/oculus/server/public/js/jquery.min.js +4 -0
  27. data/lib/oculus/server/public/js/spin.min.js +2 -0
  28. data/lib/oculus/server/views/history.erb +35 -0
  29. data/lib/oculus/server/views/index.erb +97 -0
  30. data/lib/oculus/server/views/layout.erb +37 -0
  31. data/lib/oculus/server/views/show.erb +59 -0
  32. data/lib/oculus/server.rb +88 -0
  33. data/lib/oculus/storage/file_store.rb +129 -0
  34. data/lib/oculus/storage.rb +7 -0
  35. data/lib/oculus/version.rb +3 -0
  36. data/lib/oculus.rb +29 -0
  37. data/oculus.gemspec +26 -0
  38. data/spec/connection_spec.rb +61 -0
  39. data/spec/file_store_spec.rb +111 -0
  40. data/spec/query_presenter_spec.rb +54 -0
  41. data/spec/query_spec.rb +107 -0
  42. metadata +173 -0
@@ -0,0 +1,129 @@
1
+ require 'yaml'
2
+ require 'csv'
3
+
4
+ module Oculus
5
+ module Storage
6
+ class FileStore
7
+ def initialize(root)
8
+ @root = root
9
+ end
10
+
11
+ def all_queries
12
+ Dir["#{root}/*.query"].map do |path|
13
+ File.parse(path)
14
+ end.sort { |a,b| b.id <=> a.id }
15
+ end
16
+
17
+ def save_query(query)
18
+ query.id = next_id if query.id.nil?
19
+
20
+ File.open(filename_for_id(query.id), 'w') do |file|
21
+ file.write_prelude(query.attributes)
22
+ file.write_results(query.results) if query.results
23
+ end
24
+ end
25
+
26
+ def load_query(id)
27
+ path = filename_for_id(id)
28
+
29
+ if File.exist?(path)
30
+ File.parse(path)
31
+ else
32
+ raise QueryNotFound, id
33
+ end
34
+ end
35
+
36
+ def delete_query(id)
37
+ path = filename_for_id(id)
38
+
39
+ if File.exist?(path)
40
+ File.unlink(path)
41
+ else
42
+ raise QueryNotFound, id
43
+ end
44
+ end
45
+
46
+ private
47
+
48
+ class File < ::File
49
+ def self.parse(path)
50
+ file = File.open(path)
51
+ attributes = file.attributes
52
+ attributes[:results] = file.results
53
+ Oculus::Query.new(attributes).tap do |query|
54
+ query.id = File.basename(path).split('.').first.to_i
55
+ end
56
+ end
57
+
58
+ def write_prelude(attributes)
59
+ write(YAML.dump(attributes))
60
+ puts("---")
61
+ end
62
+
63
+ def write_results(results)
64
+ csv_data = CSV.generate do |csv|
65
+ csv << results.first
66
+ results[1..-1].each do |result|
67
+ csv << result
68
+ end
69
+ end
70
+
71
+ write csv_data
72
+ end
73
+
74
+ def attributes
75
+ rewind
76
+
77
+ raw = gets
78
+
79
+ until (line = gets) == "---\n"
80
+ raw += line
81
+ end
82
+
83
+ YAML.load(raw)
84
+ end
85
+
86
+ def results
87
+ rewind
88
+
89
+ section = 0
90
+ section += 1 if gets.rstrip == "---" until section == 2 || eof?
91
+
92
+ CSV.new(read).to_a
93
+ end
94
+ end
95
+
96
+ def filename_for_id(id)
97
+ raise ArgumentError unless id.is_a?(Integer) || id =~ /^[0-9]+/
98
+ File.join(root, "#{id}.query")
99
+ end
100
+
101
+ def next_id
102
+ reset_primary_key unless File.exist?(primary_key_path)
103
+ id_file = File.open(primary_key_path, 'r+')
104
+ id_file.flock(File::LOCK_EX)
105
+
106
+ id = id_file.read.to_i
107
+
108
+ id_file.rewind
109
+ id_file.write(id + 1)
110
+ id_file.flock(File::LOCK_UN)
111
+ id_file.close
112
+
113
+ id
114
+ end
115
+
116
+ def reset_primary_key
117
+ File.open(primary_key_path, 'w') do |file|
118
+ file.puts '0'
119
+ end
120
+ end
121
+
122
+ def primary_key_path
123
+ File.join(root, 'NEXT_ID')
124
+ end
125
+
126
+ attr_reader :root
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,7 @@
1
+ require 'oculus/storage/file_store'
2
+
3
+ module Oculus
4
+ module Storage
5
+ class QueryNotFound < RuntimeError; end
6
+ end
7
+ end
@@ -0,0 +1,3 @@
1
+ module Oculus
2
+ VERSION = "0.5.0"
3
+ end
data/lib/oculus.rb ADDED
@@ -0,0 +1,29 @@
1
+ require "oculus/version"
2
+ require "oculus/storage"
3
+ require "oculus/connection"
4
+ require "oculus/query"
5
+
6
+ module Oculus
7
+ extend self
8
+
9
+ DEFAULT_CONNECTION_OPTIONS = { :host => 'localhost', :username => 'root' }
10
+
11
+ attr_writer :cache_path
12
+
13
+ def cache_path
14
+ @cache_path ||= 'tmp/data'
15
+ end
16
+
17
+ attr_writer :data_store
18
+
19
+ def data_store
20
+ @data_store ||= Oculus::Storage::FileStore.new(Oculus.cache_path)
21
+ end
22
+
23
+ attr_writer :connection_options
24
+
25
+ def connection_options
26
+ @connection_options ||= DEFAULT_CONNECTION_OPTIONS
27
+ end
28
+ end
29
+
data/oculus.gemspec ADDED
@@ -0,0 +1,26 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/oculus/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["Paul Rosania"]
6
+ gem.email = ["paul.rosania@gmail.com"]
7
+ gem.description = %q{Oculus is a web-based logging SQL client. It keeps a history of your queries and the results they returned, so your research is always at hand, easy to share and easy to repeat or reproduce in the future.}
8
+ gem.summary = %q{Oculus is a web-based logging SQL client.}
9
+ gem.homepage = "https://github.com/paulrosania/oculus"
10
+
11
+ gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
12
+ gem.files = `git ls-files`.split("\n")
13
+ gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
14
+ gem.name = "oculus"
15
+ gem.require_paths = ["lib"]
16
+ gem.version = Oculus::VERSION
17
+
18
+ gem.add_runtime_dependency "sinatra", [">= 1.3.0"]
19
+ gem.add_runtime_dependency "mysql2", [">= 0.3.11"]
20
+ gem.add_runtime_dependency "vegas", [">= 0.1.4"]
21
+
22
+ gem.add_development_dependency "rake"
23
+ gem.add_development_dependency "cucumber", [">= 1"]
24
+ gem.add_development_dependency "rspec", [">= 2"]
25
+ gem.add_development_dependency "capybara", [">= 1"]
26
+ end
@@ -0,0 +1,61 @@
1
+ require 'oculus'
2
+
3
+ describe Oculus::Connection do
4
+ before(:all) do
5
+ client = Mysql2::Client.new(:host => "localhost", :username => "root")
6
+ client.query "CREATE DATABASE IF NOT EXISTS test"
7
+ client.query "USE test"
8
+ client.query %[
9
+ CREATE TABLE IF NOT EXISTS oculus_users (
10
+ id MEDIUMINT NOT NULL AUTO_INCREMENT,
11
+ name VARCHAR(255),
12
+ PRIMARY KEY (id)
13
+ );
14
+ ]
15
+
16
+ client.query 'TRUNCATE oculus_users'
17
+
18
+ client.query %[
19
+ INSERT INTO oculus_users (name) VALUES ('Paul'), ('Amy'), ('Peter')
20
+ ]
21
+ end
22
+
23
+ subject { Oculus::Connection::Mysql2.new(:database => 'test') }
24
+
25
+ it "fetches a result set" do
26
+ subject.execute("SELECT * FROM oculus_users").should == [['id', 'name'], [1, 'Paul'], [2, 'Amy'], [3, 'Peter']]
27
+ end
28
+
29
+ it "returns nil for queries that don't return result sets" do
30
+ query_connection = Mysql2::Client.new(:host => "localhost", :database => "test")
31
+ thread_id = query_connection.thread_id
32
+ Thread.new {
33
+ query_connection.execute("SELECT * FROM oculus_users WHERE SLEEP(2)")
34
+ }
35
+
36
+ sleep 0.1
37
+ subject.execute("KILL QUERY #{thread_id}").should be_nil
38
+ end
39
+
40
+ it "raises a Connection::Error on syntax errors" do
41
+ lambda {
42
+ subject.execute("FOO BAZ QUUX")
43
+ }.should raise_error(Oculus::Connection::Error)
44
+ end
45
+
46
+ it "raises a Connection::Error when the query is interrupted" do
47
+ thread_id = subject.thread_id
48
+ Thread.new {
49
+ sleep 0.1
50
+ Mysql2::Client.new(:host => "localhost", :username => "root").query("KILL QUERY #{thread_id}")
51
+ }
52
+
53
+ lambda {
54
+ subject.execute("SELECT * FROM oculus_users WHERE SLEEP(2)")
55
+ }.should raise_error(Oculus::Connection::Error)
56
+ end
57
+
58
+ it "provides the connection's thread_id" do
59
+ subject.thread_id.should be_an Integer
60
+ end
61
+ end
@@ -0,0 +1,111 @@
1
+ require 'oculus'
2
+
3
+ describe Oculus::Storage::FileStore do
4
+ subject { Oculus::Storage::FileStore.new('tmp/test_cache') }
5
+
6
+ let(:query) do
7
+ Oculus::Query.new(:name => "All users",
8
+ :query => "SELECT * FROM oculus_users",
9
+ :author => "Paul",
10
+ :thread_id => 42,
11
+ :results => [['id', 'users'], ['1', 'Paul'], ['2', 'Amy']])
12
+ end
13
+
14
+ let(:other_query) do
15
+ Oculus::Query.new(:name => "Admin users",
16
+ :query => "SELECT * FROM oculus_users WHERE is_admin = 1",
17
+ :author => "Paul",
18
+ :thread_id => 42,
19
+ :results => [['id', 'users'], ['2', 'Amy']])
20
+ end
21
+
22
+ let(:broken_query) do
23
+ Oculus::Query.new(:name => "Admin users",
24
+ :query => "FOO BAZ QUUX",
25
+ :author => "Paul",
26
+ :thread_id => 42,
27
+ :error => "You have an error in your SQL syntax")
28
+ end
29
+
30
+ before do
31
+ FileUtils.mkdir_p('tmp/test_cache')
32
+ end
33
+
34
+ after do
35
+ FileUtils.rm_r('tmp/test_cache')
36
+ end
37
+
38
+ it "round-trips a query with no results to disk" do
39
+ query = Oculus::Query.new(:name => "Unfinished query", :author => "Me")
40
+ subject.save_query(query)
41
+ subject.load_query(query.id).results.should == []
42
+ subject.load_query(query.id).query.should == query.query
43
+ subject.load_query(query.id).date.should == query.date
44
+ subject.load_query(query.id).author.should == query.author
45
+ subject.load_query(query.id).id.should == query.id
46
+ subject.load_query(query.id).thread_id.should == query.thread_id
47
+ end
48
+
49
+ it "round-trips a query with an error to disk" do
50
+ subject.save_query(broken_query)
51
+ subject.load_query(broken_query.id).results.should == []
52
+ subject.load_query(broken_query.id).error.should == broken_query.error
53
+ subject.load_query(broken_query.id).query.should == broken_query.query
54
+ subject.load_query(broken_query.id).date.should == broken_query.date
55
+ subject.load_query(broken_query.id).author.should == broken_query.author
56
+ subject.load_query(broken_query.id).id.should == broken_query.id
57
+ subject.load_query(broken_query.id).thread_id.should == broken_query.thread_id
58
+ end
59
+
60
+ it "round-trips a query to disk" do
61
+ subject.save_query(query)
62
+ subject.load_query(query.id).results.should == query.results
63
+ subject.load_query(query.id).query.should == query.query
64
+ subject.load_query(query.id).date.should == query.date
65
+ subject.load_query(query.id).author.should == query.author
66
+ subject.load_query(query.id).id.should == query.id
67
+ subject.load_query(query.id).thread_id.should == query.thread_id
68
+ end
69
+
70
+ it "doesn't overwrite an existing query id when saving" do
71
+ subject.save_query(query)
72
+ original_id = query.id
73
+ subject.save_query(query)
74
+ query.id.should == original_id
75
+ end
76
+
77
+ it "raises QueryNotFound for missing queries" do
78
+ lambda {
79
+ subject.load_query(39827493)
80
+ }.should raise_error(Oculus::Storage::QueryNotFound)
81
+ end
82
+
83
+ it "fetches all queries in reverse chronological order" do
84
+ subject.save_query(query)
85
+ subject.save_query(other_query)
86
+
87
+ subject.all_queries.map(&:results).should == [other_query.results, query.results]
88
+ end
89
+
90
+ it "deletes queries" do
91
+ subject.save_query(query)
92
+ subject.load_query(query.id).name.should == query.name
93
+ subject.delete_query(query.id)
94
+
95
+ lambda {
96
+ subject.load_query(query.id)
97
+ }.should raise_error(Oculus::Storage::QueryNotFound)
98
+ end
99
+
100
+ it "raises QueryNotFound when deleting a nonexistent query" do
101
+ lambda {
102
+ subject.delete_query(10983645)
103
+ }.should raise_error(Oculus::Storage::QueryNotFound)
104
+ end
105
+
106
+ it "sanitizes query IDs" do
107
+ lambda {
108
+ subject.delete_query('..')
109
+ }.should raise_error(ArgumentError)
110
+ end
111
+ end
@@ -0,0 +1,54 @@
1
+ require_relative '../lib/oculus/query'
2
+ require_relative '../lib/oculus/presenters/query_presenter'
3
+
4
+ describe Oculus::Presenters::QueryPresenter do
5
+ let(:query) { Oculus::Query.new }
6
+ let(:presenter) { Oculus::Presenters::QueryPresenter.new(query) }
7
+
8
+ it "should delegate to the underlying query" do
9
+ query.name = 'foo'
10
+ presenter.description.should == 'foo'
11
+ end
12
+
13
+ it "has a formatted date" do
14
+ query.date = Time.mktime(2010, 1, 1, 12, 34)
15
+ presenter.formatted_date.should == '2010-01-01 12:34'
16
+ end
17
+
18
+ it "reports successful queries" do
19
+ query.stub(:complete?).and_return(true)
20
+ presenter.status.should == 'done'
21
+ end
22
+
23
+ it "reports failed queries" do
24
+ query.stub(:complete?).and_return(true)
25
+ query.stub(:error).and_return("you fail")
26
+ presenter.status.should == 'error'
27
+ end
28
+
29
+ it "reports loading queries" do
30
+ query.stub(:complete?).and_return(false)
31
+ presenter.status.should == 'loading'
32
+ end
33
+
34
+ it "uses name for a description when there is one" do
35
+ query.name = "foo"
36
+ presenter.description.should == "foo"
37
+ end
38
+
39
+ it "uses SQL for a description when there isn't a name" do
40
+ query.name = nil
41
+ query.query = "SELECT * FROM foo"
42
+ presenter.description.should == "SELECT * FROM foo"
43
+ end
44
+
45
+ it "reports that the query has been named" do
46
+ query.name = "Select all the things"
47
+ presenter.should be_named
48
+ end
49
+
50
+ it "reports that the query has not been named" do
51
+ query.name = nil
52
+ presenter.should_not be_named
53
+ end
54
+ end
@@ -0,0 +1,107 @@
1
+ require 'oculus'
2
+
3
+ describe Oculus::Query do
4
+ before do
5
+ Oculus.data_store = stub
6
+ end
7
+
8
+ it "runs the query against the supplied connection" do
9
+ connection = stub
10
+ query = Oculus::Query.new(:query => 'SELECT * FROM users')
11
+ connection.should_receive(:execute).with('SELECT * FROM users')
12
+ query.execute(connection)
13
+ end
14
+
15
+ it "stores the results of running the query" do
16
+ connection = stub(:execute => [['id', 'name'], [1, 'Paul']])
17
+ query = Oculus::Query.new(:query => 'SELECT * FROM users')
18
+ query.execute(connection)
19
+ query.results.should == [['id', 'name'], [1, 'Paul']]
20
+ end
21
+
22
+ it "stores errors when queries fail" do
23
+ connection = stub
24
+ query = Oculus::Query.new(:query => 'SELECT * FROM users')
25
+ connection.stub(:execute).and_raise(Oculus::Connection::Error.new('You have an error in your SQL syntax'))
26
+ query.execute(connection)
27
+ query.error.should == 'You have an error in your SQL syntax'
28
+ end
29
+
30
+ it "stores the query itself" do
31
+ query = Oculus::Query.new(:query => 'SELECT * FROM users')
32
+ query.query.should == 'SELECT * FROM users'
33
+ end
34
+
35
+ it "stores the querying connection's thread ID" do
36
+ query = Oculus::Query.new(:thread_id => 42)
37
+ query.thread_id.should == 42
38
+ end
39
+
40
+ it "has a date" do
41
+ query = Oculus::Query.new
42
+ query.date.should be nil
43
+ end
44
+
45
+ it "updates date on save" do
46
+ Oculus.data_store.stub(:save_query)
47
+ Time.stub(:now).and_return(now = stub)
48
+ query = Oculus::Query.create(:results => [['id', 'name'], [1, 'Paul']])
49
+ query.date.should == now
50
+ end
51
+
52
+ it "has a name" do
53
+ query = Oculus::Query.new(:name => 'foo')
54
+ query.name.should == 'foo'
55
+ end
56
+
57
+ it "has an author" do
58
+ query = Oculus::Query.new(:author => 'Paul')
59
+ query.author.should == 'Paul'
60
+ end
61
+
62
+ it "stores new queries in the data store on creation" do
63
+ Oculus.data_store.should_receive(:save_query)
64
+ query = Oculus::Query.create(:results => [['id', 'name'], [1, 'Paul']])
65
+ end
66
+
67
+ it "stores new queries in the data store on save" do
68
+ Oculus.data_store.should_receive(:save_query)
69
+ query = Oculus::Query.new(:results => [['id', 'name'], [1, 'Paul']])
70
+ query.save
71
+ end
72
+
73
+ it "retrieves cached queries from the data store" do
74
+ Oculus.data_store.should_receive(:load_query).with(1)
75
+ Oculus::Query.find(1)
76
+ end
77
+
78
+ it "is not complete when no results are present" do
79
+ query = Oculus::Query.new(:query => 'SELECT * FROM users')
80
+ query.should_not be_complete
81
+ end
82
+
83
+ it "is complete when results are present" do
84
+ query = Oculus::Query.new(:results => [['id', 'name'], [1, 'Paul']])
85
+ query.should be_complete
86
+ end
87
+
88
+ it "is complete when there is an error" do
89
+ query = Oculus::Query.new(:error => "That's not how to write SQL")
90
+ query.should be_complete
91
+ end
92
+
93
+ it "is not successful when it's not complete" do
94
+ query = Oculus::Query.new(:query => 'SELECT * FROM users')
95
+ query.succeeded?.should be false
96
+ end
97
+
98
+ it "is successful when results are present" do
99
+ query = Oculus::Query.new(:results => [['id', 'name'], [1, 'Paul']])
100
+ query.succeeded?.should be true
101
+ end
102
+
103
+ it "is not successful when there is an error" do
104
+ query = Oculus::Query.new(:error => "That's not how to write SQL")
105
+ query.succeeded?.should be false
106
+ end
107
+ end