oculus 0.5.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.travis.yml +1 -0
- data/Rakefile +25 -0
- data/TODO.md +0 -1
- data/features/step_definitions/query_steps.rb +5 -1
- data/features/support/env.rb +2 -1
- data/lib/oculus/presenters/query_presenter.rb +28 -2
- data/lib/oculus/query.rb +36 -7
- data/lib/oculus/server/public/css/style.css +12 -0
- data/lib/oculus/server/public/js/application.js +16 -0
- data/lib/oculus/server/views/history.erb +1 -7
- data/lib/oculus/server/views/layout.erb +3 -0
- data/lib/oculus/server/views/show.erb +46 -18
- data/lib/oculus/server/views/starred.erb +29 -0
- data/lib/oculus/server.rb +23 -6
- data/lib/oculus/storage/file_store.rb +28 -5
- data/lib/oculus/version.rb +1 -1
- data/spec/connection_spec.rb +2 -21
- data/spec/file_store_spec.rb +45 -20
- data/spec/query_presenter_spec.rb +14 -3
- data/spec/query_spec.rb +60 -16
- metadata +17 -16
data/.travis.yml
CHANGED
data/Rakefile
CHANGED
@@ -3,6 +3,7 @@ require "bundler/gem_tasks"
|
|
3
3
|
|
4
4
|
require 'rspec/core/rake_task'
|
5
5
|
require 'cucumber/rake/task'
|
6
|
+
require 'mysql2'
|
6
7
|
|
7
8
|
desc 'Run RSpec tests'
|
8
9
|
RSpec::Core::RakeTask.new(:spec) do |task|
|
@@ -15,4 +16,28 @@ Cucumber::Rake::Task.new(:cucumber) do |task|
|
|
15
16
|
task.cucumber_opts = 'features --format pretty'
|
16
17
|
end
|
17
18
|
|
19
|
+
namespace :db do
|
20
|
+
namespace :test do
|
21
|
+
desc "Populate the test database"
|
22
|
+
task :populate do
|
23
|
+
client = Mysql2::Client.new(:host => "localhost", :username => "root")
|
24
|
+
client.query "CREATE DATABASE IF NOT EXISTS oculus_test"
|
25
|
+
client.query "USE oculus_test"
|
26
|
+
client.query %[
|
27
|
+
CREATE TABLE IF NOT EXISTS oculus_users (
|
28
|
+
id MEDIUMINT NOT NULL AUTO_INCREMENT,
|
29
|
+
name VARCHAR(255),
|
30
|
+
PRIMARY KEY (id)
|
31
|
+
);
|
32
|
+
]
|
33
|
+
|
34
|
+
client.query 'TRUNCATE oculus_users'
|
35
|
+
|
36
|
+
client.query %[
|
37
|
+
INSERT INTO oculus_users (name) VALUES ('Paul'), ('Amy'), ('Peter')
|
38
|
+
]
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
18
43
|
task :default => [:spec, :cucumber]
|
data/TODO.md
CHANGED
@@ -1,5 +1,9 @@
|
|
1
1
|
Given /^a query is cached with results:$/ do |results|
|
2
|
-
Oculus::Query.create(:name
|
2
|
+
Oculus::Query.create(:name => "all users",
|
3
|
+
:query => "SELECT * FROM oculus_users",
|
4
|
+
:results => results.raw,
|
5
|
+
:started_at => Time.now,
|
6
|
+
:finished_at => Time.now)
|
3
7
|
end
|
4
8
|
|
5
9
|
Given /^I am on the history page$/ do
|
data/features/support/env.rb
CHANGED
@@ -5,12 +5,13 @@ require 'oculus/server'
|
|
5
5
|
require 'capybara/cucumber'
|
6
6
|
|
7
7
|
Capybara.app = Oculus::Server
|
8
|
+
Capybara.default_wait_time = 10
|
8
9
|
|
9
10
|
Oculus.cache_path = 'tmp/test_cache'
|
10
11
|
Oculus.connection_options = {
|
11
12
|
:host => 'localhost',
|
12
13
|
:username => 'root',
|
13
|
-
:database => '
|
14
|
+
:database => 'oculus_test'
|
14
15
|
}
|
15
16
|
|
16
17
|
Before do
|
@@ -3,8 +3,34 @@ require 'delegate'
|
|
3
3
|
module Oculus
|
4
4
|
module Presenters
|
5
5
|
class QueryPresenter < SimpleDelegator
|
6
|
-
def
|
7
|
-
|
6
|
+
def formatted_start_time
|
7
|
+
started_at.strftime("%Y-%m-%d %I:%M %p") if started_at
|
8
|
+
end
|
9
|
+
|
10
|
+
def formatted_finish_time
|
11
|
+
finished_at.strftime("%Y-%m-%d %I:%M %p") if finished_at
|
12
|
+
end
|
13
|
+
|
14
|
+
def elapsed_time
|
15
|
+
return "" unless started_at && finished_at
|
16
|
+
|
17
|
+
seconds = (finished_at - started_at).round
|
18
|
+
|
19
|
+
if seconds < 60
|
20
|
+
"#{seconds} seconds"
|
21
|
+
else
|
22
|
+
minutes = (seconds / 60).floor
|
23
|
+
seconds %= 60
|
24
|
+
|
25
|
+
if minutes < 60
|
26
|
+
"#{minutes} minutes #{seconds} seconds"
|
27
|
+
else
|
28
|
+
hours = (minutes / 60).floor
|
29
|
+
minutes %= 60
|
30
|
+
|
31
|
+
"#{hours} hours #{minutes} minutes"
|
32
|
+
end
|
33
|
+
end
|
8
34
|
end
|
9
35
|
|
10
36
|
def status
|
data/lib/oculus/query.rb
CHANGED
@@ -6,7 +6,9 @@ module Oculus
|
|
6
6
|
attr_accessor :query
|
7
7
|
attr_accessor :results
|
8
8
|
attr_accessor :error
|
9
|
-
attr_accessor :
|
9
|
+
attr_accessor :started_at
|
10
|
+
attr_accessor :finished_at
|
11
|
+
attr_accessor :starred
|
10
12
|
attr_accessor :thread_id
|
11
13
|
|
12
14
|
def initialize(attributes = {})
|
@@ -20,7 +22,9 @@ module Oculus
|
|
20
22
|
:name => name,
|
21
23
|
:author => author,
|
22
24
|
:query => query,
|
23
|
-
:
|
25
|
+
:started_at => started_at,
|
26
|
+
:finished_at => finished_at,
|
27
|
+
:starred => starred,
|
24
28
|
:thread_id => thread_id
|
25
29
|
}
|
26
30
|
attrs[:error] = error if error
|
@@ -28,24 +32,40 @@ module Oculus
|
|
28
32
|
end
|
29
33
|
|
30
34
|
def execute(connection)
|
31
|
-
self.
|
35
|
+
self.started_at = Time.now
|
36
|
+
self.thread_id = connection.thread_id
|
37
|
+
self.save
|
38
|
+
results = connection.execute(query)
|
32
39
|
rescue Connection::Error => e
|
33
|
-
|
40
|
+
error = e.message
|
41
|
+
ensure
|
42
|
+
reload
|
43
|
+
self.results = results if results
|
44
|
+
self.error = error if error
|
45
|
+
self.finished_at = Time.now
|
46
|
+
self.save
|
34
47
|
end
|
35
48
|
|
36
49
|
def save
|
37
|
-
@date = Time.now
|
38
50
|
Oculus.data_store.save_query(self)
|
39
51
|
end
|
40
52
|
|
41
53
|
def complete?
|
42
|
-
!!
|
54
|
+
!!finished_at
|
43
55
|
end
|
44
56
|
|
45
57
|
def succeeded?
|
46
58
|
complete? && !error
|
47
59
|
end
|
48
60
|
|
61
|
+
def to_csv
|
62
|
+
CSV.generate do |csv|
|
63
|
+
results.each do |row|
|
64
|
+
csv << row
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
49
69
|
class << self
|
50
70
|
def create(attributes)
|
51
71
|
query = new(attributes)
|
@@ -54,8 +74,17 @@ module Oculus
|
|
54
74
|
end
|
55
75
|
|
56
76
|
def find(id)
|
57
|
-
Oculus.data_store.load_query(id)
|
77
|
+
new(Oculus.data_store.load_query(id))
|
58
78
|
end
|
59
79
|
end
|
80
|
+
|
81
|
+
private
|
82
|
+
|
83
|
+
def reload
|
84
|
+
Oculus.data_store.load_query(id).each do |attr, value|
|
85
|
+
send("#{attr}=", value)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
60
89
|
end
|
61
90
|
end
|
@@ -102,6 +102,18 @@ body {
|
|
102
102
|
margin-top: 1em;
|
103
103
|
}
|
104
104
|
|
105
|
+
button.star .starred-contents {
|
106
|
+
display: none;
|
107
|
+
}
|
108
|
+
|
109
|
+
button.starred .starred-contents {
|
110
|
+
display: inline;
|
111
|
+
}
|
112
|
+
|
113
|
+
button.starred .unstarred-contents {
|
114
|
+
display: none;
|
115
|
+
}
|
116
|
+
|
105
117
|
#query-properties-form {
|
106
118
|
display: none;
|
107
119
|
}
|
@@ -32,6 +32,22 @@ $(function() {
|
|
32
32
|
});
|
33
33
|
});
|
34
34
|
|
35
|
+
$('#query-actions .star').click(function() {
|
36
|
+
var btn = $(this),
|
37
|
+
newState = !btn.hasClass('starred');
|
38
|
+
|
39
|
+
$.ajax({
|
40
|
+
url: this.href,
|
41
|
+
type: 'PUT',
|
42
|
+
data: {
|
43
|
+
'starred': newState
|
44
|
+
},
|
45
|
+
success: function() {
|
46
|
+
btn.toggleClass('starred', newState);
|
47
|
+
}
|
48
|
+
});
|
49
|
+
});
|
50
|
+
|
35
51
|
$('#query-actions .edit').click(function() {
|
36
52
|
$('#query-properties-form').toggle();
|
37
53
|
});
|
@@ -13,7 +13,7 @@
|
|
13
13
|
<% @queries.each do |query| %>
|
14
14
|
<tr>
|
15
15
|
<td class="status"><span class="indicator indicator-<%= query.status %>">*</span></td>
|
16
|
-
<td class="date"><%= query.
|
16
|
+
<td class="date"><%= query.formatted_start_time %></td>
|
17
17
|
<td>
|
18
18
|
<a href="/queries/<%= query.id %>"><%= query.description %></a>
|
19
19
|
</td>
|
@@ -27,9 +27,3 @@
|
|
27
27
|
<% end %>
|
28
28
|
</tbody>
|
29
29
|
</table>
|
30
|
-
|
31
|
-
<script type="text/javascript">
|
32
|
-
CodeMirror.fromTextArea(document.getElementById('query-field'), {
|
33
|
-
tabSize: 2
|
34
|
-
});
|
35
|
-
</script>
|
@@ -20,6 +20,9 @@
|
|
20
20
|
<li<% if @current_tab == 'home' %> class="active"<% end %>>
|
21
21
|
<a href="/">Home</a>
|
22
22
|
</li>
|
23
|
+
<li<% if @current_tab == 'starred' %> class="active"<% end %>>
|
24
|
+
<a href="/starred">Starred</a>
|
25
|
+
</li>
|
23
26
|
<li<% if @current_tab == 'history' %> class="active"<% end %>>
|
24
27
|
<a href="/history">History</a>
|
25
28
|
</li>
|
@@ -1,17 +1,35 @@
|
|
1
1
|
<h1 class="description"><%= @query.description %></h1>
|
2
2
|
|
3
3
|
<div id="query-actions">
|
4
|
+
<button class="btn btn-success star<% if @query.starred %> starred<% end %>">
|
5
|
+
<span class="unstarred-contents">
|
6
|
+
<i class="icon-star-empty icon-white"></i>
|
7
|
+
Star
|
8
|
+
</span>
|
9
|
+
<span class="starred-contents">
|
10
|
+
<i class="icon-star icon-white"></i>
|
11
|
+
Starred
|
12
|
+
</span>
|
13
|
+
</button>
|
4
14
|
<button class="btn edit<% unless @query.named? %> active<% end %>" data-toggle="button">
|
5
15
|
<i class="icon-pencil"></i>
|
6
16
|
Edit
|
7
17
|
</button>
|
18
|
+
<a class="btn file<% unless @query.succeeded? && @headers %> disabled<% end %>" href="/queries/<%= @query.id %>/download">
|
19
|
+
<i class="icon-file"></i>
|
20
|
+
Download
|
21
|
+
</a>
|
8
22
|
</div>
|
9
23
|
|
10
24
|
<dl class="dl-horizontal query-properties">
|
11
25
|
<dt>Author</dt>
|
12
26
|
<dd class="author"><%= @query.author %> </dd>
|
13
|
-
<dt>
|
14
|
-
<dd><%= @query.
|
27
|
+
<dt>Started at</dt>
|
28
|
+
<dd><%= @query.formatted_start_time %></dd>
|
29
|
+
<% if @query.finished_at %>
|
30
|
+
<dt>Finished at</dt>
|
31
|
+
<dd><%= @query.formatted_finish_time %> (<%= @query.elapsed_time %>)</dd>
|
32
|
+
<% end %>
|
15
33
|
</dl>
|
16
34
|
|
17
35
|
<form class="well form-inline" id="query-properties-form">
|
@@ -23,24 +41,31 @@
|
|
23
41
|
<pre class="cm-s-default"><%= @query.query %></pre>
|
24
42
|
|
25
43
|
<% if @query.succeeded? %>
|
26
|
-
|
27
|
-
<
|
28
|
-
<
|
29
|
-
<% @headers.each do |label| %>
|
30
|
-
<th><%= label %></th>
|
31
|
-
<% end %>
|
32
|
-
</tr>
|
33
|
-
</thead>
|
34
|
-
<tbody class="results">
|
35
|
-
<% @results.each do |result| %>
|
44
|
+
<% if @headers %>
|
45
|
+
<table class="table table-condensed" id="results-table">
|
46
|
+
<thead>
|
36
47
|
<tr>
|
37
|
-
<%
|
38
|
-
<
|
48
|
+
<% @headers.each do |label| %>
|
49
|
+
<th><%= label %></th>
|
39
50
|
<% end %>
|
40
51
|
</tr>
|
41
|
-
|
42
|
-
|
43
|
-
|
52
|
+
</thead>
|
53
|
+
<tbody class="results">
|
54
|
+
<% @results.each do |result| %>
|
55
|
+
<tr>
|
56
|
+
<% result.each do |value| %>
|
57
|
+
<td><%= value %></td>
|
58
|
+
<% end %>
|
59
|
+
</tr>
|
60
|
+
<% end %>
|
61
|
+
</tbody>
|
62
|
+
</table>
|
63
|
+
<% else %>
|
64
|
+
<div class="alert alert-success">
|
65
|
+
<strong>Heads Up!</strong>
|
66
|
+
This query ran successfully, but returned no results.
|
67
|
+
</div>
|
68
|
+
<% end %>
|
44
69
|
<% elsif @query.error %>
|
45
70
|
<div class="alert alert-error">
|
46
71
|
<strong>Sorry!</strong>
|
@@ -48,7 +73,7 @@
|
|
48
73
|
</div>
|
49
74
|
<% else %>
|
50
75
|
<div class="alert alert-info">
|
51
|
-
<strong>
|
76
|
+
<strong>Heads Up!</strong>
|
52
77
|
This query is in progress, and has not returned any results yet.
|
53
78
|
</div>
|
54
79
|
<% end %>
|
@@ -56,4 +81,7 @@
|
|
56
81
|
<script type="text/javascript">
|
57
82
|
var queryNode = $('pre.cm-s-default');
|
58
83
|
CodeMirror.runMode(queryNode.text(), 'mysql', queryNode[0]);
|
84
|
+
$('a.disabled').click(function(e) {
|
85
|
+
e.preventDefault();
|
86
|
+
});
|
59
87
|
</script>
|
@@ -0,0 +1,29 @@
|
|
1
|
+
<% @current_tab = 'starred' %>
|
2
|
+
|
3
|
+
<h2>Starred Queries</h2>
|
4
|
+
<table class="table">
|
5
|
+
<thead>
|
6
|
+
<th></th>
|
7
|
+
<th>Timestamp</th>
|
8
|
+
<th>Query</th>
|
9
|
+
<th>Author</th>
|
10
|
+
<th></th>
|
11
|
+
</thead>
|
12
|
+
<tbody id="history">
|
13
|
+
<% @queries.each do |query| %>
|
14
|
+
<tr>
|
15
|
+
<td class="status"><span class="indicator indicator-<%= query.status %>">*</span></td>
|
16
|
+
<td class="date"><%= query.formatted_start_time %></td>
|
17
|
+
<td>
|
18
|
+
<a href="/queries/<%= query.id %>"><%= query.description %></a>
|
19
|
+
</td>
|
20
|
+
<td class="author">
|
21
|
+
<%= query.author %>
|
22
|
+
</td>
|
23
|
+
<td>
|
24
|
+
<a class="delete" href="/queries/<%= query.id %>"><i class="icon-remove"></i></a>
|
25
|
+
</td>
|
26
|
+
</tr>
|
27
|
+
<% end %>
|
28
|
+
</tbody>
|
29
|
+
</table>
|
data/lib/oculus/server.rb
CHANGED
@@ -21,6 +21,12 @@ module Oculus
|
|
21
21
|
erb :index
|
22
22
|
end
|
23
23
|
|
24
|
+
get '/starred' do
|
25
|
+
@queries = Oculus.data_store.starred_queries.map { |q| Oculus::Presenters::QueryPresenter.new(q) }
|
26
|
+
|
27
|
+
erb :starred
|
28
|
+
end
|
29
|
+
|
24
30
|
get '/history' do
|
25
31
|
@queries = Oculus.data_store.all_queries.map { |q| Oculus::Presenters::QueryPresenter.new(q) }
|
26
32
|
|
@@ -35,13 +41,12 @@ module Oculus
|
|
35
41
|
end
|
36
42
|
|
37
43
|
post '/queries' do
|
38
|
-
|
39
|
-
query = Oculus::Query.create(:query => params[:query],
|
40
|
-
:thread_id => connection.thread_id)
|
44
|
+
query = Oculus::Query.create(:query => params[:query])
|
41
45
|
|
42
46
|
pid = fork do
|
47
|
+
query = Oculus::Query.find(query.id)
|
48
|
+
connection = Oculus::Connection::Mysql2.new(Oculus.connection_options)
|
43
49
|
query.execute(connection)
|
44
|
-
query.save
|
45
50
|
end
|
46
51
|
|
47
52
|
Process.detach(pid)
|
@@ -67,14 +72,26 @@ module Oculus
|
|
67
72
|
erb :show
|
68
73
|
end
|
69
74
|
|
75
|
+
get '/queries/:id/download' do
|
76
|
+
query = Oculus::Query.find(params[:id])
|
77
|
+
timestamp = query.started_at.strftime('%Y%m%d%H%M')
|
78
|
+
|
79
|
+
attachment "#{timestamp}-query-#{query.id}-results.csv"
|
80
|
+
content_type "text/csv"
|
81
|
+
last_modified query.finished_at
|
82
|
+
|
83
|
+
query.to_csv
|
84
|
+
end
|
85
|
+
|
70
86
|
get '/queries/:id/status' do
|
71
87
|
Oculus::Presenters::QueryPresenter.new(Oculus::Query.find(params[:id])).status
|
72
88
|
end
|
73
89
|
|
74
90
|
put '/queries/:id' do
|
75
91
|
@query = Oculus::Query.find(params[:id])
|
76
|
-
@query.name
|
77
|
-
@query.author
|
92
|
+
@query.name = params[:name] if params[:name]
|
93
|
+
@query.author = params[:author] if params[:author]
|
94
|
+
@query.starred = params[:starred] == "true" if params[:starred]
|
78
95
|
@query.save
|
79
96
|
|
80
97
|
puts "true"
|
@@ -10,7 +10,13 @@ module Oculus
|
|
10
10
|
|
11
11
|
def all_queries
|
12
12
|
Dir["#{root}/*.query"].map do |path|
|
13
|
-
File.parse(path)
|
13
|
+
Query.new(File.parse(path))
|
14
|
+
end.sort { |a,b| b.id <=> a.id }
|
15
|
+
end
|
16
|
+
|
17
|
+
def starred_queries
|
18
|
+
Dir["#{root}/starred/*.query"].map do |path|
|
19
|
+
Query.new(File.parse(path))
|
14
20
|
end.sort { |a,b| b.id <=> a.id }
|
15
21
|
end
|
16
22
|
|
@@ -19,7 +25,16 @@ module Oculus
|
|
19
25
|
|
20
26
|
File.open(filename_for_id(query.id), 'w') do |file|
|
21
27
|
file.write_prelude(query.attributes)
|
22
|
-
file.write_results(query.results) if query.results
|
28
|
+
file.write_results(query.results) if query.results && query.results.length > 0
|
29
|
+
end
|
30
|
+
|
31
|
+
FileUtils.mkdir_p(File.join(root, "starred")) unless Dir.exist?(File.join(root, "starred"))
|
32
|
+
star_path = starred_filename_for_id(query.id)
|
33
|
+
|
34
|
+
if query.starred
|
35
|
+
File.symlink(File.expand_path(filename_for_id(query.id)), star_path) unless File.exist?(star_path)
|
36
|
+
elsif File.exist?(star_path)
|
37
|
+
File.unlink(star_path)
|
23
38
|
end
|
24
39
|
end
|
25
40
|
|
@@ -34,6 +49,9 @@ module Oculus
|
|
34
49
|
end
|
35
50
|
|
36
51
|
def delete_query(id)
|
52
|
+
star_path = starred_filename_for_id(id)
|
53
|
+
File.unlink(star_path) if File.exist?(star_path)
|
54
|
+
|
37
55
|
path = filename_for_id(id)
|
38
56
|
|
39
57
|
if File.exist?(path)
|
@@ -50,9 +68,9 @@ module Oculus
|
|
50
68
|
file = File.open(path)
|
51
69
|
attributes = file.attributes
|
52
70
|
attributes[:results] = file.results
|
53
|
-
|
54
|
-
|
55
|
-
|
71
|
+
attributes[:id] = File.basename(path).split('.').first.to_i
|
72
|
+
attributes[:starred] ||= false
|
73
|
+
attributes
|
56
74
|
end
|
57
75
|
|
58
76
|
def write_prelude(attributes)
|
@@ -93,6 +111,11 @@ module Oculus
|
|
93
111
|
end
|
94
112
|
end
|
95
113
|
|
114
|
+
def starred_filename_for_id(id)
|
115
|
+
raise ArgumentError unless id.is_a?(Integer) || id =~ /^[0-9]+/
|
116
|
+
File.join(root, "starred", "#{id}.query")
|
117
|
+
end
|
118
|
+
|
96
119
|
def filename_for_id(id)
|
97
120
|
raise ArgumentError unless id.is_a?(Integer) || id =~ /^[0-9]+/
|
98
121
|
File.join(root, "#{id}.query")
|
data/lib/oculus/version.rb
CHANGED
data/spec/connection_spec.rb
CHANGED
@@ -1,33 +1,14 @@
|
|
1
1
|
require 'oculus'
|
2
2
|
|
3
3
|
describe Oculus::Connection do
|
4
|
-
|
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') }
|
4
|
+
subject { Oculus::Connection::Mysql2.new(:host => 'localhost', :database => 'oculus_test', :username => 'root') }
|
24
5
|
|
25
6
|
it "fetches a result set" do
|
26
7
|
subject.execute("SELECT * FROM oculus_users").should == [['id', 'name'], [1, 'Paul'], [2, 'Amy'], [3, 'Peter']]
|
27
8
|
end
|
28
9
|
|
29
10
|
it "returns nil for queries that don't return result sets" do
|
30
|
-
query_connection = Mysql2::Client.new(:host => "localhost", :database => "
|
11
|
+
query_connection = Mysql2::Client.new(:host => "localhost", :database => "oculus_test", :username => "root")
|
31
12
|
thread_id = query_connection.thread_id
|
32
13
|
Thread.new {
|
33
14
|
query_connection.execute("SELECT * FROM oculus_users WHERE SLEEP(2)")
|
data/spec/file_store_spec.rb
CHANGED
@@ -38,33 +38,48 @@ describe Oculus::Storage::FileStore do
|
|
38
38
|
it "round-trips a query with no results to disk" do
|
39
39
|
query = Oculus::Query.new(:name => "Unfinished query", :author => "Me")
|
40
40
|
subject.save_query(query)
|
41
|
-
subject.load_query(query.id).
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
41
|
+
subject.load_query(query.id).should == {
|
42
|
+
:id => query.id,
|
43
|
+
:name => query.name,
|
44
|
+
:author => query.author,
|
45
|
+
:query => query.query,
|
46
|
+
:results => [],
|
47
|
+
:thread_id => query.thread_id,
|
48
|
+
:starred => false,
|
49
|
+
:started_at => query.started_at,
|
50
|
+
:finished_at => query.finished_at
|
51
|
+
}
|
47
52
|
end
|
48
53
|
|
49
54
|
it "round-trips a query with an error to disk" do
|
50
55
|
subject.save_query(broken_query)
|
51
|
-
subject.load_query(broken_query.id).
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
56
|
+
subject.load_query(broken_query.id).should == {
|
57
|
+
:id => broken_query.id,
|
58
|
+
:name => broken_query.name,
|
59
|
+
:error => broken_query.error,
|
60
|
+
:author => broken_query.author,
|
61
|
+
:query => broken_query.query,
|
62
|
+
:results => [],
|
63
|
+
:thread_id => broken_query.thread_id,
|
64
|
+
:starred => false,
|
65
|
+
:started_at => broken_query.started_at,
|
66
|
+
:finished_at => broken_query.finished_at
|
67
|
+
}
|
58
68
|
end
|
59
69
|
|
60
70
|
it "round-trips a query to disk" do
|
61
71
|
subject.save_query(query)
|
62
|
-
subject.load_query(query.id).
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
72
|
+
subject.load_query(query.id).should == {
|
73
|
+
:id => query.id,
|
74
|
+
:name => query.name,
|
75
|
+
:author => query.author,
|
76
|
+
:query => query.query,
|
77
|
+
:results => query.results,
|
78
|
+
:thread_id => query.thread_id,
|
79
|
+
:starred => false,
|
80
|
+
:started_at => query.started_at,
|
81
|
+
:finished_at => query.finished_at
|
82
|
+
}
|
68
83
|
end
|
69
84
|
|
70
85
|
it "doesn't overwrite an existing query id when saving" do
|
@@ -87,9 +102,19 @@ describe Oculus::Storage::FileStore do
|
|
87
102
|
subject.all_queries.map(&:results).should == [other_query.results, query.results]
|
88
103
|
end
|
89
104
|
|
105
|
+
it "fetches starred queries" do
|
106
|
+
query.starred = true
|
107
|
+
subject.save_query(query)
|
108
|
+
subject.save_query(other_query)
|
109
|
+
|
110
|
+
results = subject.starred_queries
|
111
|
+
results.map(&:results).should == [query.results]
|
112
|
+
results.first.starred.should be true
|
113
|
+
end
|
114
|
+
|
90
115
|
it "deletes queries" do
|
91
116
|
subject.save_query(query)
|
92
|
-
subject.load_query(query.id)
|
117
|
+
subject.load_query(query.id)[:name].should == query.name
|
93
118
|
subject.delete_query(query.id)
|
94
119
|
|
95
120
|
lambda {
|
@@ -10,9 +10,20 @@ describe Oculus::Presenters::QueryPresenter do
|
|
10
10
|
presenter.description.should == 'foo'
|
11
11
|
end
|
12
12
|
|
13
|
-
it "has a formatted
|
14
|
-
query.
|
15
|
-
presenter.
|
13
|
+
it "has a formatted start time" do
|
14
|
+
query.started_at = Time.mktime(2010, 1, 1, 12, 34)
|
15
|
+
presenter.formatted_start_time.should == '2010-01-01 12:34 PM'
|
16
|
+
end
|
17
|
+
|
18
|
+
it "has a formatted finish time" do
|
19
|
+
query.finished_at = Time.mktime(2010, 1, 1, 12, 34)
|
20
|
+
presenter.formatted_finish_time.should == '2010-01-01 12:34 PM'
|
21
|
+
end
|
22
|
+
|
23
|
+
it "has an elapsed time" do
|
24
|
+
query.started_at = Time.mktime(2010, 1, 1, 10, 30)
|
25
|
+
query.finished_at = Time.mktime(2010, 1, 1, 12, 34)
|
26
|
+
presenter.elapsed_time.should == '2 hours 4 minutes'
|
16
27
|
end
|
17
28
|
|
18
29
|
it "reports successful queries" do
|
data/spec/query_spec.rb
CHANGED
@@ -2,25 +2,25 @@ require 'oculus'
|
|
2
2
|
|
3
3
|
describe Oculus::Query do
|
4
4
|
before do
|
5
|
-
Oculus.data_store = stub
|
5
|
+
Oculus.data_store = stub(:load_query => {}, :save_query => nil)
|
6
6
|
end
|
7
7
|
|
8
8
|
it "runs the query against the supplied connection" do
|
9
|
-
connection = stub
|
9
|
+
connection = stub(:thread_id => 42)
|
10
10
|
query = Oculus::Query.new(:query => 'SELECT * FROM users')
|
11
11
|
connection.should_receive(:execute).with('SELECT * FROM users')
|
12
12
|
query.execute(connection)
|
13
13
|
end
|
14
14
|
|
15
15
|
it "stores the results of running the query" do
|
16
|
-
connection = stub(:execute => [['id', 'name'], [1, 'Paul']])
|
16
|
+
connection = stub(:execute => [['id', 'name'], [1, 'Paul']], :thread_id => 42)
|
17
17
|
query = Oculus::Query.new(:query => 'SELECT * FROM users')
|
18
18
|
query.execute(connection)
|
19
19
|
query.results.should == [['id', 'name'], [1, 'Paul']]
|
20
20
|
end
|
21
21
|
|
22
22
|
it "stores errors when queries fail" do
|
23
|
-
connection = stub
|
23
|
+
connection = stub(:thread_id => 42)
|
24
24
|
query = Oculus::Query.new(:query => 'SELECT * FROM users')
|
25
25
|
connection.stub(:execute).and_raise(Oculus::Connection::Error.new('You have an error in your SQL syntax'))
|
26
26
|
query.execute(connection)
|
@@ -37,16 +37,39 @@ describe Oculus::Query do
|
|
37
37
|
query.thread_id.should == 42
|
38
38
|
end
|
39
39
|
|
40
|
-
it "has a
|
40
|
+
it "has a start time" do
|
41
41
|
query = Oculus::Query.new
|
42
|
-
query.
|
42
|
+
query.started_at.should be nil
|
43
43
|
end
|
44
44
|
|
45
|
-
it "
|
46
|
-
Oculus.
|
45
|
+
it "has a finish time" do
|
46
|
+
query = Oculus::Query.new
|
47
|
+
query.finished_at.should be nil
|
48
|
+
end
|
49
|
+
|
50
|
+
it "updates start time when executing" do
|
47
51
|
Time.stub(:now).and_return(now = stub)
|
48
|
-
|
49
|
-
query.
|
52
|
+
connection = stub(:execute => [['id', 'name'], [1, 'Paul']], :thread_id => 42)
|
53
|
+
query = Oculus::Query.new(:query => 'SELECT * FROM users')
|
54
|
+
query.execute(connection)
|
55
|
+
query.started_at.should == now
|
56
|
+
end
|
57
|
+
|
58
|
+
it "updates finish time when executing" do
|
59
|
+
Time.stub(:now).and_return(now = stub)
|
60
|
+
connection = stub(:execute => [['id', 'name'], [1, 'Paul']], :thread_id => 42)
|
61
|
+
query = Oculus::Query.new(:query => 'SELECT * FROM users')
|
62
|
+
query.execute(connection)
|
63
|
+
query.finished_at.should == now
|
64
|
+
end
|
65
|
+
|
66
|
+
it "updates finish time when execution fails" do
|
67
|
+
Time.stub(:now).and_return(now = stub)
|
68
|
+
connection = stub(:thread_id => 42)
|
69
|
+
connection.stub(:execute).and_raise(Oculus::Connection::Error.new('You have an error in your SQL syntax'))
|
70
|
+
query = Oculus::Query.new(:query => 'SELECT * FROM users')
|
71
|
+
query.execute(connection)
|
72
|
+
query.finished_at.should == now
|
50
73
|
end
|
51
74
|
|
52
75
|
it "has a name" do
|
@@ -59,6 +82,11 @@ describe Oculus::Query do
|
|
59
82
|
query.author.should == 'Paul'
|
60
83
|
end
|
61
84
|
|
85
|
+
it "can be starred" do
|
86
|
+
query = Oculus::Query.new(:starred => true)
|
87
|
+
query.starred.should == true
|
88
|
+
end
|
89
|
+
|
62
90
|
it "stores new queries in the data store on creation" do
|
63
91
|
Oculus.data_store.should_receive(:save_query)
|
64
92
|
query = Oculus::Query.create(:results => [['id', 'name'], [1, 'Paul']])
|
@@ -75,18 +103,23 @@ describe Oculus::Query do
|
|
75
103
|
Oculus::Query.find(1)
|
76
104
|
end
|
77
105
|
|
78
|
-
it "is not complete when
|
106
|
+
it "is not complete when it has not been executed" do
|
79
107
|
query = Oculus::Query.new(:query => 'SELECT * FROM users')
|
80
108
|
query.should_not be_complete
|
81
109
|
end
|
82
110
|
|
83
|
-
it "is complete when
|
84
|
-
|
111
|
+
it "is complete when it has been executed" do
|
112
|
+
connection = stub(:execute => [['id', 'name'], [1, 'Paul']], :thread_id => 42)
|
113
|
+
query = Oculus::Query.new
|
114
|
+
query.execute(connection)
|
85
115
|
query.should be_complete
|
86
116
|
end
|
87
117
|
|
88
118
|
it "is complete when there is an error" do
|
89
|
-
|
119
|
+
connection = stub(:thread_id => 42)
|
120
|
+
connection.stub(:execute).and_raise(Oculus::Connection::Error.new('You have an error in your SQL syntax'))
|
121
|
+
query = Oculus::Query.new
|
122
|
+
query.execute(connection)
|
90
123
|
query.should be_complete
|
91
124
|
end
|
92
125
|
|
@@ -96,12 +129,23 @@ describe Oculus::Query do
|
|
96
129
|
end
|
97
130
|
|
98
131
|
it "is successful when results are present" do
|
99
|
-
|
132
|
+
connection = stub(:execute => [['id', 'name'], [1, 'Paul']], :thread_id => 42)
|
133
|
+
query = Oculus::Query.new
|
134
|
+
query.execute(connection)
|
100
135
|
query.succeeded?.should be true
|
101
136
|
end
|
102
137
|
|
103
138
|
it "is not successful when there is an error" do
|
104
|
-
|
139
|
+
connection = stub(:thread_id => 42)
|
140
|
+
connection.stub(:execute).and_raise(Oculus::Connection::Error.new('You have an error in your SQL syntax'))
|
141
|
+
query = Oculus::Query.new
|
142
|
+
query.execute(connection)
|
105
143
|
query.succeeded?.should be false
|
106
144
|
end
|
145
|
+
|
146
|
+
it "exports to CSV" do
|
147
|
+
query = Oculus::Query.new
|
148
|
+
query.results = [['id', 'name'], [1, 'Paul']]
|
149
|
+
query.to_csv.should == "id,name\n1,Paul\n"
|
150
|
+
end
|
107
151
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: oculus
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.8.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,11 +9,11 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2012-05-
|
12
|
+
date: 2012-05-06 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: sinatra
|
16
|
-
requirement: &
|
16
|
+
requirement: &70114293249540 !ruby/object:Gem::Requirement
|
17
17
|
none: false
|
18
18
|
requirements:
|
19
19
|
- - ! '>='
|
@@ -21,10 +21,10 @@ dependencies:
|
|
21
21
|
version: 1.3.0
|
22
22
|
type: :runtime
|
23
23
|
prerelease: false
|
24
|
-
version_requirements: *
|
24
|
+
version_requirements: *70114293249540
|
25
25
|
- !ruby/object:Gem::Dependency
|
26
26
|
name: mysql2
|
27
|
-
requirement: &
|
27
|
+
requirement: &70114293249020 !ruby/object:Gem::Requirement
|
28
28
|
none: false
|
29
29
|
requirements:
|
30
30
|
- - ! '>='
|
@@ -32,10 +32,10 @@ dependencies:
|
|
32
32
|
version: 0.3.11
|
33
33
|
type: :runtime
|
34
34
|
prerelease: false
|
35
|
-
version_requirements: *
|
35
|
+
version_requirements: *70114293249020
|
36
36
|
- !ruby/object:Gem::Dependency
|
37
37
|
name: vegas
|
38
|
-
requirement: &
|
38
|
+
requirement: &70114293248540 !ruby/object:Gem::Requirement
|
39
39
|
none: false
|
40
40
|
requirements:
|
41
41
|
- - ! '>='
|
@@ -43,10 +43,10 @@ dependencies:
|
|
43
43
|
version: 0.1.4
|
44
44
|
type: :runtime
|
45
45
|
prerelease: false
|
46
|
-
version_requirements: *
|
46
|
+
version_requirements: *70114293248540
|
47
47
|
- !ruby/object:Gem::Dependency
|
48
48
|
name: rake
|
49
|
-
requirement: &
|
49
|
+
requirement: &70114288904480 !ruby/object:Gem::Requirement
|
50
50
|
none: false
|
51
51
|
requirements:
|
52
52
|
- - ! '>='
|
@@ -54,10 +54,10 @@ dependencies:
|
|
54
54
|
version: '0'
|
55
55
|
type: :development
|
56
56
|
prerelease: false
|
57
|
-
version_requirements: *
|
57
|
+
version_requirements: *70114288904480
|
58
58
|
- !ruby/object:Gem::Dependency
|
59
59
|
name: cucumber
|
60
|
-
requirement: &
|
60
|
+
requirement: &70114288903580 !ruby/object:Gem::Requirement
|
61
61
|
none: false
|
62
62
|
requirements:
|
63
63
|
- - ! '>='
|
@@ -65,10 +65,10 @@ dependencies:
|
|
65
65
|
version: '1'
|
66
66
|
type: :development
|
67
67
|
prerelease: false
|
68
|
-
version_requirements: *
|
68
|
+
version_requirements: *70114288903580
|
69
69
|
- !ruby/object:Gem::Dependency
|
70
70
|
name: rspec
|
71
|
-
requirement: &
|
71
|
+
requirement: &70114288902160 !ruby/object:Gem::Requirement
|
72
72
|
none: false
|
73
73
|
requirements:
|
74
74
|
- - ! '>='
|
@@ -76,10 +76,10 @@ dependencies:
|
|
76
76
|
version: '2'
|
77
77
|
type: :development
|
78
78
|
prerelease: false
|
79
|
-
version_requirements: *
|
79
|
+
version_requirements: *70114288902160
|
80
80
|
- !ruby/object:Gem::Dependency
|
81
81
|
name: capybara
|
82
|
-
requirement: &
|
82
|
+
requirement: &70114288901540 !ruby/object:Gem::Requirement
|
83
83
|
none: false
|
84
84
|
requirements:
|
85
85
|
- - ! '>='
|
@@ -87,7 +87,7 @@ dependencies:
|
|
87
87
|
version: '1'
|
88
88
|
type: :development
|
89
89
|
prerelease: false
|
90
|
-
version_requirements: *
|
90
|
+
version_requirements: *70114288901540
|
91
91
|
description: Oculus is a web-based logging SQL client. It keeps a history of your
|
92
92
|
queries and the results they returned, so your research is always at hand, easy
|
93
93
|
to share and easy to repeat or reproduce in the future.
|
@@ -131,6 +131,7 @@ files:
|
|
131
131
|
- lib/oculus/server/views/index.erb
|
132
132
|
- lib/oculus/server/views/layout.erb
|
133
133
|
- lib/oculus/server/views/show.erb
|
134
|
+
- lib/oculus/server/views/starred.erb
|
134
135
|
- lib/oculus/storage.rb
|
135
136
|
- lib/oculus/storage/file_store.rb
|
136
137
|
- lib/oculus/version.rb
|