oculus 0.8.0 → 0.9.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/README.md +4 -2
- data/Rakefile +26 -0
- data/TODO.md +2 -1
- data/bin/oculus +4 -0
- data/features/query.feature +1 -0
- data/features/support/env.rb +1 -0
- data/lib/oculus/connection/mysql2.rb +4 -0
- data/lib/oculus/connection/postgres.rb +31 -0
- data/lib/oculus/connection.rb +13 -0
- data/lib/oculus/server/public/css/style.css +5 -0
- data/lib/oculus/server/public/js/application.js +93 -60
- data/lib/oculus/server/views/index.erb +9 -1
- data/lib/oculus/server/views/layout.erb +1 -1
- data/lib/oculus/server/views/show.erb +92 -34
- data/lib/oculus/server.rb +3 -3
- data/lib/oculus/storage/file_store.rb +13 -0
- data/lib/oculus/version.rb +1 -1
- data/lib/oculus.rb +7 -1
- data/oculus.gemspec +3 -1
- data/spec/connection/mysql2_spec.rb +42 -0
- data/spec/connection/postgres_spec.rb +22 -0
- data/spec/connection_spec.rb +22 -31
- data/spec/file_store_spec.rb +21 -0
- metadata +41 -25
data/README.md
CHANGED
@@ -28,7 +28,9 @@ For details on command line options, run:
|
|
28
28
|
## Contributing
|
29
29
|
|
30
30
|
1. Fork it
|
31
|
-
2.
|
32
|
-
3.
|
31
|
+
2. Run `rake db:test:populate`
|
32
|
+
3. Make your changes
|
33
|
+
4. Run tests (`rake`)
|
34
|
+
5. Send me a pull request
|
33
35
|
|
34
36
|
If you're making a big change, please open an Issue first, so we can discuss.
|
data/Rakefile
CHANGED
@@ -4,6 +4,7 @@ require "bundler/gem_tasks"
|
|
4
4
|
require 'rspec/core/rake_task'
|
5
5
|
require 'cucumber/rake/task'
|
6
6
|
require 'mysql2'
|
7
|
+
require 'pg'
|
7
8
|
|
8
9
|
desc 'Run RSpec tests'
|
9
10
|
RSpec::Core::RakeTask.new(:spec) do |task|
|
@@ -20,6 +21,8 @@ namespace :db do
|
|
20
21
|
namespace :test do
|
21
22
|
desc "Populate the test database"
|
22
23
|
task :populate do
|
24
|
+
# MySQL
|
25
|
+
#
|
23
26
|
client = Mysql2::Client.new(:host => "localhost", :username => "root")
|
24
27
|
client.query "CREATE DATABASE IF NOT EXISTS oculus_test"
|
25
28
|
client.query "USE oculus_test"
|
@@ -36,6 +39,29 @@ namespace :db do
|
|
36
39
|
client.query %[
|
37
40
|
INSERT INTO oculus_users (name) VALUES ('Paul'), ('Amy'), ('Peter')
|
38
41
|
]
|
42
|
+
|
43
|
+
client.close
|
44
|
+
|
45
|
+
# Postgres
|
46
|
+
#
|
47
|
+
client = PG::Connection.new(:host => "localhost", :dbname => "postgres")
|
48
|
+
client.query "DROP DATABASE IF EXISTS oculus_test"
|
49
|
+
client.query "CREATE DATABASE oculus_test"
|
50
|
+
client.close
|
51
|
+
|
52
|
+
client = PG::Connection.new(:host => "localhost", :dbname => "oculus_test")
|
53
|
+
client.query %[
|
54
|
+
CREATE TABLE oculus_users (
|
55
|
+
id INT NOT NULL UNIQUE,
|
56
|
+
name VARCHAR(255)
|
57
|
+
);
|
58
|
+
]
|
59
|
+
|
60
|
+
client.query %[
|
61
|
+
INSERT INTO oculus_users (id, name) VALUES (1, 'Paul'), (2, 'Amy'), (3, 'Peter')
|
62
|
+
]
|
63
|
+
|
64
|
+
client.close
|
39
65
|
end
|
40
66
|
end
|
41
67
|
end
|
data/TODO.md
CHANGED
data/bin/oculus
CHANGED
@@ -26,6 +26,10 @@ Vegas::Runner.new(Oculus::Server, 'oculus') do |runner, opts, app|
|
|
26
26
|
opts.on("-D", "--database DATABASE", "Database to use") do |db|
|
27
27
|
Oculus.connection_options[:database] = db
|
28
28
|
end
|
29
|
+
opts.on("-t", "--adapter ADAPTER", "Database adapter") do |adapter|
|
30
|
+
abort "oculus: unknown adapter '#{adapter}'" unless ['mysql', 'postgres', 'pg'].include?(adapter)
|
31
|
+
Oculus.connection_options[:adapter] = adapter
|
32
|
+
end
|
29
33
|
opts.on("-d", "--data DIRECTORY", "Data cache path (default: tmp/data)") do |path|
|
30
34
|
Oculus.cache_path = path
|
31
35
|
end
|
data/features/query.feature
CHANGED
data/features/support/env.rb
CHANGED
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'pg'
|
2
|
+
|
3
|
+
module Oculus
|
4
|
+
module Connection
|
5
|
+
class Postgres
|
6
|
+
def initialize(options = {})
|
7
|
+
@connection = ::PG::Connection.new(options[:host],
|
8
|
+
options[:port],
|
9
|
+
nil, nil,
|
10
|
+
options[:database],
|
11
|
+
options[:username],
|
12
|
+
options[:password])
|
13
|
+
end
|
14
|
+
|
15
|
+
def execute(sql)
|
16
|
+
results = @connection.exec(sql)
|
17
|
+
[results.fields] + results.values if results
|
18
|
+
rescue ::PG::Error => e
|
19
|
+
raise Connection::Error.new(e.message)
|
20
|
+
end
|
21
|
+
|
22
|
+
def kill(id)
|
23
|
+
@connection.execute("SELECT pg_cancel_backend(#{id})")
|
24
|
+
end
|
25
|
+
|
26
|
+
def thread_id
|
27
|
+
@connection.backend_pid
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
data/lib/oculus/connection.rb
CHANGED
@@ -1,7 +1,20 @@
|
|
1
1
|
require 'oculus/connection/mysql2'
|
2
|
+
require 'oculus/connection/postgres'
|
2
3
|
|
3
4
|
module Oculus
|
4
5
|
module Connection
|
5
6
|
class Error < StandardError; end
|
7
|
+
class AdapterNotFound < Error; end
|
8
|
+
|
9
|
+
def self.connect(options)
|
10
|
+
case options[:adapter]
|
11
|
+
when 'mysql'
|
12
|
+
Mysql2
|
13
|
+
when 'postgres', 'pg'
|
14
|
+
Postgres
|
15
|
+
else
|
16
|
+
raise AdapterNotFound, "#{options[:adapter]} is not currently implemented. You should write it!"
|
17
|
+
end.new(options)
|
18
|
+
end
|
6
19
|
end
|
7
20
|
end
|
@@ -57,6 +57,89 @@ $(function() {
|
|
57
57
|
}
|
58
58
|
});
|
59
59
|
|
60
|
+
function Query(id) {
|
61
|
+
this.id = id;
|
62
|
+
this._monitoring = false;
|
63
|
+
}
|
64
|
+
|
65
|
+
Query.MONITOR_TIMEOUT = 1000;
|
66
|
+
|
67
|
+
Query.prototype.cancel = function() {
|
68
|
+
$.ajax({
|
69
|
+
url: '/queries/' + this.id + '/cancel',
|
70
|
+
type: 'POST'
|
71
|
+
});
|
72
|
+
}
|
73
|
+
|
74
|
+
Query.prototype.check = function(callbacks) {
|
75
|
+
$.ajax({
|
76
|
+
url: '/queries/' + this.id + '/status',
|
77
|
+
type: 'GET',
|
78
|
+
success: callbacks.success
|
79
|
+
});
|
80
|
+
}
|
81
|
+
|
82
|
+
// For now, multiple calls to monitor will overwrite callbacks.
|
83
|
+
// Someday, a pubsub mechanism might make sense here.
|
84
|
+
Query.prototype.monitor = function(callbacks) {
|
85
|
+
this._monitoringCallbacks = callbacks;
|
86
|
+
|
87
|
+
if (!this._monitoring) {
|
88
|
+
this._monitoring = true;
|
89
|
+
|
90
|
+
var self = this;
|
91
|
+
setTimeout(function() {
|
92
|
+
self._tick();
|
93
|
+
}, Query.MONITOR_TIMEOUT);
|
94
|
+
}
|
95
|
+
}
|
96
|
+
|
97
|
+
Query.prototype.detach = function() {
|
98
|
+
this._monitoring = false;
|
99
|
+
this._monitoringCallbacks = null;
|
100
|
+
}
|
101
|
+
|
102
|
+
Query.prototype._tick = function() {
|
103
|
+
if (this._monitoring) {
|
104
|
+
var self = this;
|
105
|
+
|
106
|
+
this.check({
|
107
|
+
success: function(status) {
|
108
|
+
// Ignore responses for queries we're no longer interested in
|
109
|
+
if (self._monitoring) {
|
110
|
+
if (status !== 'loading') {
|
111
|
+
self.load(self._monitoringCallbacks);
|
112
|
+
} else {
|
113
|
+
setTimeout(function() {
|
114
|
+
self._tick();
|
115
|
+
}, Query.MONITOR_TIMEOUT);
|
116
|
+
}
|
117
|
+
}
|
118
|
+
}
|
119
|
+
});
|
120
|
+
}
|
121
|
+
}
|
122
|
+
|
123
|
+
Query.prototype.load = function(callbacks) {
|
124
|
+
var self = this;
|
125
|
+
|
126
|
+
$.ajax({
|
127
|
+
url: '/queries/' + this.id + '.json',
|
128
|
+
type: 'GET',
|
129
|
+
dataType: 'json',
|
130
|
+
success: function(data) {
|
131
|
+
if (data.results) {
|
132
|
+
if (callbacks.success) callbacks.success(data);
|
133
|
+
} else {
|
134
|
+
if (callbacks.error) callbacks.error(data.error);
|
135
|
+
}
|
136
|
+
},
|
137
|
+
error: function() {
|
138
|
+
if (callbacks.error) callbacks.error("Failed to load results");
|
139
|
+
}
|
140
|
+
});
|
141
|
+
}
|
142
|
+
|
60
143
|
function QueryEditor(form, options) {
|
61
144
|
this._monitorQueryId = null;
|
62
145
|
this._form = form;
|
@@ -65,8 +148,6 @@ function QueryEditor(form, options) {
|
|
65
148
|
this.bindEvents();
|
66
149
|
}
|
67
150
|
|
68
|
-
QueryEditor.MONITOR_TIMEOUT = 1000;
|
69
|
-
|
70
151
|
QueryEditor.prototype.getQueryField = function() {
|
71
152
|
return document.getElementById('query-field');
|
72
153
|
}
|
@@ -84,7 +165,7 @@ QueryEditor.prototype.initCodeMirror = function() {
|
|
84
165
|
QueryEditor.prototype.bindEvents = function() {
|
85
166
|
var self = this;
|
86
167
|
$(document).on('keydown', 'form', function(e) {
|
87
|
-
if (e.keyCode === 13 && (e.ctrlKey || e.metaKey)) {
|
168
|
+
if ((e.keyCode === 13 && (e.ctrlKey || e.metaKey)) || (e.metaKey && e.altKey && e.keyCode === 82)) {
|
88
169
|
e.preventDefault();
|
89
170
|
|
90
171
|
// CodeMirror normally saves itself automatically, but since there is no
|
@@ -101,7 +182,8 @@ QueryEditor.prototype.bindEvents = function() {
|
|
101
182
|
}
|
102
183
|
|
103
184
|
QueryEditor.prototype.submit = function() {
|
104
|
-
if(this.
|
185
|
+
if (this._query) this._query.detach();
|
186
|
+
if (this._options.onQueryStart) this._options.onQueryStart();
|
105
187
|
|
106
188
|
var self = this;
|
107
189
|
$.ajax({
|
@@ -110,7 +192,11 @@ QueryEditor.prototype.submit = function() {
|
|
110
192
|
data: this._form.serialize(),
|
111
193
|
dataType: 'json',
|
112
194
|
success: function(data) {
|
113
|
-
self.
|
195
|
+
self._query = new Query(data.id);
|
196
|
+
self._query.monitor({
|
197
|
+
success: self._options.onQuerySuccess,
|
198
|
+
error: self._options.onQueryError
|
199
|
+
});
|
114
200
|
},
|
115
201
|
error: function() {
|
116
202
|
if (self._options.onQueryError) self._options.onQueryError();
|
@@ -118,59 +204,6 @@ QueryEditor.prototype.submit = function() {
|
|
118
204
|
});
|
119
205
|
}
|
120
206
|
|
121
|
-
QueryEditor.prototype.
|
122
|
-
this.
|
123
|
-
|
124
|
-
var self = this;
|
125
|
-
setTimeout(function() {
|
126
|
-
self._tick();
|
127
|
-
}, QueryEditor.MONITOR_TIMEOUT);
|
128
|
-
}
|
129
|
-
|
130
|
-
QueryEditor.prototype._tick = function() {
|
131
|
-
if (this._monitorQueryId !== null) {
|
132
|
-
var queryId = this._monitorQueryId,
|
133
|
-
self = this;
|
134
|
-
|
135
|
-
$.ajax({
|
136
|
-
url: '/queries/' + queryId + '/status',
|
137
|
-
type: 'GET',
|
138
|
-
success: function(status) {
|
139
|
-
// Ignore responses for queries we're no longer interested in
|
140
|
-
if (queryId === self._monitorQueryId) {
|
141
|
-
if (status !== 'loading') {
|
142
|
-
self._loadQueryResult(queryId);
|
143
|
-
} else {
|
144
|
-
setTimeout(function() {
|
145
|
-
self._tick();
|
146
|
-
}, QueryEditor.MONITOR_TIMEOUT);
|
147
|
-
}
|
148
|
-
}
|
149
|
-
}
|
150
|
-
});
|
151
|
-
}
|
152
|
-
}
|
153
|
-
|
154
|
-
QueryEditor.prototype._loadQueryResult = function(id) {
|
155
|
-
var self = this;
|
156
|
-
$.ajax({
|
157
|
-
url: '/queries/' + id + '.json',
|
158
|
-
type: 'GET',
|
159
|
-
dataType: 'json',
|
160
|
-
success: function(data) {
|
161
|
-
// Ignore responses for queries we're no longer interested in
|
162
|
-
if (id === self._monitorQueryId) {
|
163
|
-
if (data.results) {
|
164
|
-
if (self._options.onQuerySuccess) self._options.onQuerySuccess(data);
|
165
|
-
} else {
|
166
|
-
if (self._options.onQueryError) self._options.onQueryError(data.error);
|
167
|
-
}
|
168
|
-
}
|
169
|
-
},
|
170
|
-
error: function() {
|
171
|
-
if (id === self._monitorQueryId && self._options.onQueryError) {
|
172
|
-
self._options.onQueryError("Failed to load results");
|
173
|
-
}
|
174
|
-
}
|
175
|
-
});
|
207
|
+
QueryEditor.prototype.cancelQuery = function() {
|
208
|
+
if (this._query) this._query.cancel();
|
176
209
|
}
|
@@ -6,12 +6,15 @@
|
|
6
6
|
</div>
|
7
7
|
<div class="form-actions">
|
8
8
|
<input type="submit" class="btn" name="submit" value="Run" />
|
9
|
-
<span class="deemphasized help-inline">Ctrl+&crarr
|
9
|
+
<span class="deemphasized help-inline"><abbr title="Ctrl+Enter">Ctrl+↵</abbr> or <abbr title="Command+Enter">⌘↵</abbr></span>
|
10
10
|
</div>
|
11
11
|
</form>
|
12
12
|
|
13
13
|
<div id="loading">
|
14
14
|
<div class="alert">
|
15
|
+
<div class="cancel-container">
|
16
|
+
<a class="btn btn-mini btn-warning cancel">Cancel</a>
|
17
|
+
</div>
|
15
18
|
<span id="spinner"></span>
|
16
19
|
Executing query...
|
17
20
|
</div>
|
@@ -37,6 +40,11 @@
|
|
37
40
|
</div>
|
38
41
|
|
39
42
|
<script type="text/javascript">
|
43
|
+
$('#loading .cancel').click(function(e) {
|
44
|
+
e.preventDefault();
|
45
|
+
editor.cancelQuery();
|
46
|
+
});
|
47
|
+
|
40
48
|
var opts = {
|
41
49
|
lines: 11,
|
42
50
|
length: 4,
|
@@ -28,7 +28,7 @@
|
|
28
28
|
</li>
|
29
29
|
</ul>
|
30
30
|
<div class="pull-right navbar-text">
|
31
|
-
|
31
|
+
<%= Oculus.connection_string %>
|
32
32
|
</div>
|
33
33
|
</div>
|
34
34
|
</div>
|
@@ -40,45 +40,103 @@
|
|
40
40
|
|
41
41
|
<pre class="cm-s-default"><%= @query.query %></pre>
|
42
42
|
|
43
|
-
|
44
|
-
|
45
|
-
<
|
46
|
-
<
|
47
|
-
<tr>
|
48
|
-
<% @headers.each do |label| %>
|
49
|
-
<th><%= label %></th>
|
50
|
-
<% end %>
|
51
|
-
</tr>
|
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.
|
43
|
+
<div id="loading">
|
44
|
+
<div class="alert">
|
45
|
+
<div class="cancel-container">
|
46
|
+
<a class="btn btn-mini btn-warning cancel">Cancel</a>
|
67
47
|
</div>
|
68
|
-
|
69
|
-
|
70
|
-
<div class="alert alert-error">
|
71
|
-
<strong>Sorry!</strong>
|
72
|
-
<%= @query.error %>
|
48
|
+
<span id="spinner"></span>
|
49
|
+
Executing query...
|
73
50
|
</div>
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
51
|
+
</div>
|
52
|
+
|
53
|
+
<div id="error" class="alert alert-error">
|
54
|
+
<strong>Problem!</strong>
|
55
|
+
<span class="message"></span>
|
56
|
+
</div>
|
57
|
+
|
58
|
+
<div id="results">
|
59
|
+
<div class="alert alert-success">
|
60
|
+
<strong>Success!</strong>
|
61
|
+
Query returned <span class="row-count"></span> row<span class="row-count-plural">s</span>.
|
78
62
|
</div>
|
79
|
-
|
63
|
+
|
64
|
+
<table class="table table-condensed" id="results-table">
|
65
|
+
<thead>
|
66
|
+
<tr class="headers"></tr>
|
67
|
+
</thead>
|
68
|
+
<tbody class="results"></tbody>
|
69
|
+
</table>
|
70
|
+
</div>
|
80
71
|
|
81
72
|
<script type="text/javascript">
|
73
|
+
var id = window.location.href.match(/queries\/([0-9]+)/)[1];
|
74
|
+
|
75
|
+
var query = new Query(id);
|
76
|
+
var callbacks = {
|
77
|
+
success: function(query) {
|
78
|
+
$('#loading, #error').hide();
|
79
|
+
$('#results')
|
80
|
+
.show()
|
81
|
+
.find('.row-count')
|
82
|
+
.text(query.results.length - 1)
|
83
|
+
.end()
|
84
|
+
.find('.row-count-plural')
|
85
|
+
.toggle(query.results.length !== 2);
|
86
|
+
|
87
|
+
var headerRow = $('#results .headers').empty();
|
88
|
+
var container = $('#results .results').empty();
|
89
|
+
|
90
|
+
for (var i = 0; i < query.results[0].length; i++) {
|
91
|
+
headerRow.append('<th>' + query.results[0][i] + '</th>');
|
92
|
+
}
|
93
|
+
|
94
|
+
for (var i = 1; i < query.results.length; i++) {
|
95
|
+
var result = query.results[i];
|
96
|
+
var row = $('<tr></tr>');
|
97
|
+
|
98
|
+
for (var j = 0; j < result.length; j++) {
|
99
|
+
row.append('<td>' + result[j] + '</td>');
|
100
|
+
}
|
101
|
+
|
102
|
+
container.append(row);
|
103
|
+
}
|
104
|
+
},
|
105
|
+
error: function(error) {
|
106
|
+
$('#loading, #results').hide();
|
107
|
+
$('#error')
|
108
|
+
.show()
|
109
|
+
.find('.message')
|
110
|
+
.text(error);
|
111
|
+
}
|
112
|
+
};
|
113
|
+
|
114
|
+
$('#results, #error').hide();
|
115
|
+
|
116
|
+
<% if @query.succeeded? || @query.error %>
|
117
|
+
$('#loading').hide();
|
118
|
+
query.load(callbacks);
|
119
|
+
<% else %>
|
120
|
+
query.monitor(callbacks);
|
121
|
+
<% end %>
|
122
|
+
|
123
|
+
$('#loading .cancel').click(function(e) {
|
124
|
+
e.preventDefault();
|
125
|
+
query.cancel();
|
126
|
+
});
|
127
|
+
|
128
|
+
var opts = {
|
129
|
+
lines: 11,
|
130
|
+
length: 4,
|
131
|
+
width: 2,
|
132
|
+
radius: 4,
|
133
|
+
color: '#C09853',
|
134
|
+
speed: 1.2,
|
135
|
+
trail: 60
|
136
|
+
};
|
137
|
+
var target = document.getElementById('spinner');
|
138
|
+
var spinner = new Spinner(opts).spin(target);
|
139
|
+
|
82
140
|
var queryNode = $('pre.cm-s-default');
|
83
141
|
CodeMirror.runMode(queryNode.text(), 'mysql', queryNode[0]);
|
84
142
|
$('a.disabled').click(function(e) {
|
data/lib/oculus/server.rb
CHANGED
@@ -35,8 +35,8 @@ module Oculus
|
|
35
35
|
|
36
36
|
post '/queries/:id/cancel' do
|
37
37
|
query = Oculus::Query.find(params[:id])
|
38
|
-
connection = Oculus::Connection
|
39
|
-
connection.
|
38
|
+
connection = Oculus::Connection.connect(Oculus.connection_options)
|
39
|
+
connection.kill(query.thread_id)
|
40
40
|
[200, "OK"]
|
41
41
|
end
|
42
42
|
|
@@ -45,7 +45,7 @@ module Oculus
|
|
45
45
|
|
46
46
|
pid = fork do
|
47
47
|
query = Oculus::Query.find(query.id)
|
48
|
-
connection = Oculus::Connection
|
48
|
+
connection = Oculus::Connection.connect(Oculus.connection_options)
|
49
49
|
query.execute(connection)
|
50
50
|
end
|
51
51
|
|
@@ -1,5 +1,6 @@
|
|
1
1
|
require 'yaml'
|
2
2
|
require 'csv'
|
3
|
+
require 'fileutils'
|
3
4
|
|
4
5
|
module Oculus
|
5
6
|
module Storage
|
@@ -21,11 +22,15 @@ module Oculus
|
|
21
22
|
end
|
22
23
|
|
23
24
|
def save_query(query)
|
25
|
+
ensure_root_path
|
26
|
+
|
24
27
|
query.id = next_id if query.id.nil?
|
25
28
|
|
26
29
|
File.open(filename_for_id(query.id), 'w') do |file|
|
30
|
+
file.flock(File::LOCK_EX)
|
27
31
|
file.write_prelude(query.attributes)
|
28
32
|
file.write_results(query.results) if query.results && query.results.length > 0
|
33
|
+
file.flock(File::LOCK_UN)
|
29
34
|
end
|
30
35
|
|
31
36
|
FileUtils.mkdir_p(File.join(root, "starred")) unless Dir.exist?(File.join(root, "starred"))
|
@@ -66,8 +71,12 @@ module Oculus
|
|
66
71
|
class File < ::File
|
67
72
|
def self.parse(path)
|
68
73
|
file = File.open(path)
|
74
|
+
|
75
|
+
file.flock(File::LOCK_EX)
|
69
76
|
attributes = file.attributes
|
70
77
|
attributes[:results] = file.results
|
78
|
+
file.flock(File::LOCK_UN)
|
79
|
+
|
71
80
|
attributes[:id] = File.basename(path).split('.').first.to_i
|
72
81
|
attributes[:starred] ||= false
|
73
82
|
attributes
|
@@ -146,6 +155,10 @@ module Oculus
|
|
146
155
|
File.join(root, 'NEXT_ID')
|
147
156
|
end
|
148
157
|
|
158
|
+
def ensure_root_path
|
159
|
+
FileUtils.mkdir_p root unless File.exists? root
|
160
|
+
end
|
161
|
+
|
149
162
|
attr_reader :root
|
150
163
|
end
|
151
164
|
end
|
data/lib/oculus/version.rb
CHANGED
data/lib/oculus.rb
CHANGED
@@ -6,7 +6,7 @@ require "oculus/query"
|
|
6
6
|
module Oculus
|
7
7
|
extend self
|
8
8
|
|
9
|
-
DEFAULT_CONNECTION_OPTIONS = { :
|
9
|
+
DEFAULT_CONNECTION_OPTIONS = { :adapter => 'mysql', :host => 'localhost' }
|
10
10
|
|
11
11
|
attr_writer :cache_path
|
12
12
|
|
@@ -25,5 +25,11 @@ module Oculus
|
|
25
25
|
def connection_options
|
26
26
|
@connection_options ||= DEFAULT_CONNECTION_OPTIONS
|
27
27
|
end
|
28
|
+
|
29
|
+
def connection_string
|
30
|
+
user = "#{connection_options[:user]}@" if connection_options[:user]
|
31
|
+
port = ":#{connection_options[:port]}" if connection_options[:port]
|
32
|
+
"#{connection_options[:adapter]}://#{user}#{connection_options[:host]}#{port}/#{connection_options[:database]}"
|
33
|
+
end
|
28
34
|
end
|
29
35
|
|
data/oculus.gemspec
CHANGED
@@ -16,11 +16,13 @@ Gem::Specification.new do |gem|
|
|
16
16
|
gem.version = Oculus::VERSION
|
17
17
|
|
18
18
|
gem.add_runtime_dependency "sinatra", [">= 1.3.0"]
|
19
|
-
gem.add_runtime_dependency "mysql2", [">= 0.3.11"]
|
20
19
|
gem.add_runtime_dependency "vegas", [">= 0.1.4"]
|
21
20
|
|
22
21
|
gem.add_development_dependency "rake"
|
23
22
|
gem.add_development_dependency "cucumber", [">= 1"]
|
24
23
|
gem.add_development_dependency "rspec", [">= 2"]
|
25
24
|
gem.add_development_dependency "capybara", [">= 1"]
|
25
|
+
|
26
|
+
gem.add_development_dependency "mysql2", [">= 0.3.11"]
|
27
|
+
gem.add_development_dependency "pg", [">= 0.13.2"]
|
26
28
|
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'oculus'
|
2
|
+
|
3
|
+
describe Oculus::Connection::Mysql2 do
|
4
|
+
subject { Oculus::Connection::Mysql2.new(:host => 'localhost', :database => 'oculus_test', :username => 'root') }
|
5
|
+
|
6
|
+
it "fetches a result set" do
|
7
|
+
subject.execute("SELECT * FROM oculus_users").should == [['id', 'name'], [1, 'Paul'], [2, 'Amy'], [3, 'Peter']]
|
8
|
+
end
|
9
|
+
|
10
|
+
it "returns nil for queries that don't return result sets" do
|
11
|
+
query_connection = Mysql2::Client.new(:host => "localhost", :database => "oculus_test", :username => "root")
|
12
|
+
thread_id = query_connection.thread_id
|
13
|
+
Thread.new {
|
14
|
+
query_connection.execute("SELECT * FROM oculus_users WHERE SLEEP(2)")
|
15
|
+
}
|
16
|
+
|
17
|
+
sleep 0.1
|
18
|
+
subject.kill(thread_id).should be_nil
|
19
|
+
end
|
20
|
+
|
21
|
+
it "raises a Connection::Error on syntax errors" do
|
22
|
+
lambda {
|
23
|
+
subject.execute("FOO BAZ QUUX")
|
24
|
+
}.should raise_error(Oculus::Connection::Error)
|
25
|
+
end
|
26
|
+
|
27
|
+
it "raises a Connection::Error when the query is interrupted" do
|
28
|
+
thread_id = subject.thread_id
|
29
|
+
Thread.new {
|
30
|
+
sleep 0.1
|
31
|
+
Mysql2::Client.new(:host => "localhost", :username => "root").query("KILL QUERY #{thread_id}")
|
32
|
+
}
|
33
|
+
|
34
|
+
lambda {
|
35
|
+
subject.execute("SELECT * FROM oculus_users WHERE SLEEP(2)")
|
36
|
+
}.should raise_error(Oculus::Connection::Error)
|
37
|
+
end
|
38
|
+
|
39
|
+
it "provides the connection's thread_id" do
|
40
|
+
subject.thread_id.should be_an Integer
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'oculus'
|
2
|
+
|
3
|
+
describe Oculus::Connection::Postgres do
|
4
|
+
subject { Oculus::Connection::Postgres.new(:host => 'localhost', :database => 'oculus_test') }
|
5
|
+
|
6
|
+
it "fetches a result set" do
|
7
|
+
subject.execute("SELECT * FROM oculus_users").should == [['id', 'name'],
|
8
|
+
['1', 'Paul'],
|
9
|
+
['2', 'Amy'],
|
10
|
+
['3', 'Peter']]
|
11
|
+
end
|
12
|
+
|
13
|
+
it "raises a Connection::Error on syntax errors" do
|
14
|
+
lambda {
|
15
|
+
subject.execute("FOO BAZ QUUX")
|
16
|
+
}.should raise_error(Oculus::Connection::Error)
|
17
|
+
end
|
18
|
+
|
19
|
+
it "provides the connection's thread_id" do
|
20
|
+
subject.thread_id.should be_an Integer
|
21
|
+
end
|
22
|
+
end
|
data/spec/connection_spec.rb
CHANGED
@@ -1,42 +1,33 @@
|
|
1
1
|
require 'oculus'
|
2
2
|
|
3
3
|
describe Oculus::Connection do
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
it "returns nil for queries that don't return result sets" do
|
11
|
-
query_connection = Mysql2::Client.new(:host => "localhost", :database => "oculus_test", :username => "root")
|
12
|
-
thread_id = query_connection.thread_id
|
13
|
-
Thread.new {
|
14
|
-
query_connection.execute("SELECT * FROM oculus_users WHERE SLEEP(2)")
|
15
|
-
}
|
16
|
-
|
17
|
-
sleep 0.1
|
18
|
-
subject.execute("KILL QUERY #{thread_id}").should be_nil
|
4
|
+
describe "non-nonexistent adapter" do
|
5
|
+
it "raises an adapter not found error" do
|
6
|
+
lambda {
|
7
|
+
Oculus::Connection.connect adapter: 'nonexistent-adapter'
|
8
|
+
}.should raise_error Oculus::Connection::AdapterNotFound, "nonexistent-adapter is not currently implemented. You should write it!"
|
9
|
+
end
|
19
10
|
end
|
20
11
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
12
|
+
describe "mysql adapter option" do
|
13
|
+
it "returns a new instance of MySQL adapter" do
|
14
|
+
adapter = Oculus::Connection.connect adapter: 'mysql'
|
15
|
+
adapter.should be_an_instance_of Oculus::Connection::Mysql2
|
16
|
+
end
|
25
17
|
end
|
26
18
|
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
}
|
33
|
-
|
34
|
-
lambda {
|
35
|
-
subject.execute("SELECT * FROM oculus_users WHERE SLEEP(2)")
|
36
|
-
}.should raise_error(Oculus::Connection::Error)
|
19
|
+
describe "postgres adapter option" do
|
20
|
+
it "returns a new instance of Postgres adapter" do
|
21
|
+
adapter = Oculus::Connection.connect adapter: 'postgres', database: 'oculus_test'
|
22
|
+
adapter.should be_an_instance_of Oculus::Connection::Postgres
|
23
|
+
end
|
37
24
|
end
|
38
25
|
|
39
|
-
|
40
|
-
|
26
|
+
describe "pg adapter alias" do
|
27
|
+
it "returns a new instance of Postgres adapter" do
|
28
|
+
adapter = Oculus::Connection.connect adapter: 'pg', database: 'oculus_test'
|
29
|
+
adapter.should be_an_instance_of Oculus::Connection::Postgres
|
30
|
+
end
|
41
31
|
end
|
42
32
|
end
|
33
|
+
|
data/spec/file_store_spec.rb
CHANGED
@@ -133,4 +133,25 @@ describe Oculus::Storage::FileStore do
|
|
133
133
|
subject.delete_query('..')
|
134
134
|
}.should raise_error(ArgumentError)
|
135
135
|
end
|
136
|
+
|
137
|
+
context "when cache dir does not exist (like for a new install)" do
|
138
|
+
before do
|
139
|
+
FileUtils.rm_r('tmp/test_cache')
|
140
|
+
end
|
141
|
+
|
142
|
+
it "round-trips a query to disk" do
|
143
|
+
subject.save_query(query)
|
144
|
+
subject.load_query(query.id).should == {
|
145
|
+
:id => query.id,
|
146
|
+
:name => query.name,
|
147
|
+
:author => query.author,
|
148
|
+
:query => query.query,
|
149
|
+
:results => query.results,
|
150
|
+
:thread_id => query.thread_id,
|
151
|
+
:starred => false,
|
152
|
+
:started_at => query.started_at,
|
153
|
+
:finished_at => query.finished_at
|
154
|
+
}
|
155
|
+
end
|
156
|
+
end
|
136
157
|
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.9.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-24 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: sinatra
|
16
|
-
requirement: &
|
16
|
+
requirement: &70346142610300 !ruby/object:Gem::Requirement
|
17
17
|
none: false
|
18
18
|
requirements:
|
19
19
|
- - ! '>='
|
@@ -21,21 +21,10 @@ dependencies:
|
|
21
21
|
version: 1.3.0
|
22
22
|
type: :runtime
|
23
23
|
prerelease: false
|
24
|
-
version_requirements: *
|
25
|
-
- !ruby/object:Gem::Dependency
|
26
|
-
name: mysql2
|
27
|
-
requirement: &70114293249020 !ruby/object:Gem::Requirement
|
28
|
-
none: false
|
29
|
-
requirements:
|
30
|
-
- - ! '>='
|
31
|
-
- !ruby/object:Gem::Version
|
32
|
-
version: 0.3.11
|
33
|
-
type: :runtime
|
34
|
-
prerelease: false
|
35
|
-
version_requirements: *70114293249020
|
24
|
+
version_requirements: *70346142610300
|
36
25
|
- !ruby/object:Gem::Dependency
|
37
26
|
name: vegas
|
38
|
-
requirement: &
|
27
|
+
requirement: &70346142609780 !ruby/object:Gem::Requirement
|
39
28
|
none: false
|
40
29
|
requirements:
|
41
30
|
- - ! '>='
|
@@ -43,10 +32,10 @@ dependencies:
|
|
43
32
|
version: 0.1.4
|
44
33
|
type: :runtime
|
45
34
|
prerelease: false
|
46
|
-
version_requirements: *
|
35
|
+
version_requirements: *70346142609780
|
47
36
|
- !ruby/object:Gem::Dependency
|
48
37
|
name: rake
|
49
|
-
requirement: &
|
38
|
+
requirement: &70346142609320 !ruby/object:Gem::Requirement
|
50
39
|
none: false
|
51
40
|
requirements:
|
52
41
|
- - ! '>='
|
@@ -54,10 +43,10 @@ dependencies:
|
|
54
43
|
version: '0'
|
55
44
|
type: :development
|
56
45
|
prerelease: false
|
57
|
-
version_requirements: *
|
46
|
+
version_requirements: *70346142609320
|
58
47
|
- !ruby/object:Gem::Dependency
|
59
48
|
name: cucumber
|
60
|
-
requirement: &
|
49
|
+
requirement: &70346142608500 !ruby/object:Gem::Requirement
|
61
50
|
none: false
|
62
51
|
requirements:
|
63
52
|
- - ! '>='
|
@@ -65,10 +54,10 @@ dependencies:
|
|
65
54
|
version: '1'
|
66
55
|
type: :development
|
67
56
|
prerelease: false
|
68
|
-
version_requirements: *
|
57
|
+
version_requirements: *70346142608500
|
69
58
|
- !ruby/object:Gem::Dependency
|
70
59
|
name: rspec
|
71
|
-
requirement: &
|
60
|
+
requirement: &70346142607820 !ruby/object:Gem::Requirement
|
72
61
|
none: false
|
73
62
|
requirements:
|
74
63
|
- - ! '>='
|
@@ -76,10 +65,10 @@ dependencies:
|
|
76
65
|
version: '2'
|
77
66
|
type: :development
|
78
67
|
prerelease: false
|
79
|
-
version_requirements: *
|
68
|
+
version_requirements: *70346142607820
|
80
69
|
- !ruby/object:Gem::Dependency
|
81
70
|
name: capybara
|
82
|
-
requirement: &
|
71
|
+
requirement: &70346142607300 !ruby/object:Gem::Requirement
|
83
72
|
none: false
|
84
73
|
requirements:
|
85
74
|
- - ! '>='
|
@@ -87,7 +76,29 @@ dependencies:
|
|
87
76
|
version: '1'
|
88
77
|
type: :development
|
89
78
|
prerelease: false
|
90
|
-
version_requirements: *
|
79
|
+
version_requirements: *70346142607300
|
80
|
+
- !ruby/object:Gem::Dependency
|
81
|
+
name: mysql2
|
82
|
+
requirement: &70346142606800 !ruby/object:Gem::Requirement
|
83
|
+
none: false
|
84
|
+
requirements:
|
85
|
+
- - ! '>='
|
86
|
+
- !ruby/object:Gem::Version
|
87
|
+
version: 0.3.11
|
88
|
+
type: :development
|
89
|
+
prerelease: false
|
90
|
+
version_requirements: *70346142606800
|
91
|
+
- !ruby/object:Gem::Dependency
|
92
|
+
name: pg
|
93
|
+
requirement: &70346142606280 !ruby/object:Gem::Requirement
|
94
|
+
none: false
|
95
|
+
requirements:
|
96
|
+
- - ! '>='
|
97
|
+
- !ruby/object:Gem::Version
|
98
|
+
version: 0.13.2
|
99
|
+
type: :development
|
100
|
+
prerelease: false
|
101
|
+
version_requirements: *70346142606280
|
91
102
|
description: Oculus is a web-based logging SQL client. It keeps a history of your
|
92
103
|
queries and the results they returned, so your research is always at hand, easy
|
93
104
|
to share and easy to repeat or reproduce in the future.
|
@@ -112,6 +123,7 @@ files:
|
|
112
123
|
- lib/oculus.rb
|
113
124
|
- lib/oculus/connection.rb
|
114
125
|
- lib/oculus/connection/mysql2.rb
|
126
|
+
- lib/oculus/connection/postgres.rb
|
115
127
|
- lib/oculus/presenters.rb
|
116
128
|
- lib/oculus/presenters/query_presenter.rb
|
117
129
|
- lib/oculus/query.rb
|
@@ -136,6 +148,8 @@ files:
|
|
136
148
|
- lib/oculus/storage/file_store.rb
|
137
149
|
- lib/oculus/version.rb
|
138
150
|
- oculus.gemspec
|
151
|
+
- spec/connection/mysql2_spec.rb
|
152
|
+
- spec/connection/postgres_spec.rb
|
139
153
|
- spec/connection_spec.rb
|
140
154
|
- spec/file_store_spec.rb
|
141
155
|
- spec/query_presenter_spec.rb
|
@@ -168,6 +182,8 @@ test_files:
|
|
168
182
|
- features/query.feature
|
169
183
|
- features/step_definitions/query_steps.rb
|
170
184
|
- features/support/env.rb
|
185
|
+
- spec/connection/mysql2_spec.rb
|
186
|
+
- spec/connection/postgres_spec.rb
|
171
187
|
- spec/connection_spec.rb
|
172
188
|
- spec/file_store_spec.rb
|
173
189
|
- spec/query_presenter_spec.rb
|