oculus 0.8.0 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
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. Make your changes
32
- 3. Send me a pull request
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
@@ -2,7 +2,8 @@
2
2
 
3
3
  ## Upcoming (pre-1.0)
4
4
 
5
- * Input validation
5
+ * Setup instructions
6
+ * New user experience
6
7
 
7
8
  ## Eventually (1.1 or later)
8
9
 
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
@@ -5,6 +5,7 @@ Feature: Users can query the database
5
5
  When I execute "SELECT * FROM oculus_users"
6
6
  Then I should see 3 rows of results
7
7
 
8
+ @javascript
8
9
  Scenario: Loading a cached query
9
10
  Given a query is cached with results:
10
11
  | id | users |
@@ -9,6 +9,7 @@ Capybara.default_wait_time = 10
9
9
 
10
10
  Oculus.cache_path = 'tmp/test_cache'
11
11
  Oculus.connection_options = {
12
+ :adapter => 'mysql',
12
13
  :host => 'localhost',
13
14
  :username => 'root',
14
15
  :database => 'oculus_test'
@@ -14,6 +14,10 @@ module Oculus
14
14
  raise Connection::Error.new(e.message)
15
15
  end
16
16
 
17
+ def kill(id)
18
+ execute("KILL QUERY #{id}")
19
+ end
20
+
17
21
  def thread_id
18
22
  @connection.thread_id
19
23
  end
@@ -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
@@ -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
@@ -117,3 +117,8 @@ button.starred .unstarred-contents {
117
117
  #query-properties-form {
118
118
  display: none;
119
119
  }
120
+
121
+ #loading .cancel-container {
122
+ float: right;
123
+ margin: -1px -26px 0 0;
124
+ }
@@ -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._options.onQueryStart) this._options.onQueryStart();
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.monitorQuery(data.id);
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.monitorQuery = function(id) {
122
- this._monitorQueryId = id;
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; or &#8984;&crarr;</span>
9
+ <span class="deemphasized help-inline"><abbr title="Ctrl+Enter">Ctrl+&crarr;</abbr> or <abbr title="Command+Enter">&#8984;&crarr;</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
- mysql://<%= Oculus.connection_options[:username] %>@<%= Oculus.connection_options[:host] %>:<%= Oculus.connection_options[:port] %>/<%= Oculus.connection_options[:database] %>
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
- <% if @query.succeeded? %>
44
- <% if @headers %>
45
- <table class="table table-condensed" id="results-table">
46
- <thead>
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
- <% end %>
69
- <% elsif @query.error %>
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
- <% else %>
75
- <div class="alert alert-info">
76
- <strong>Heads Up!</strong>
77
- This query is in progress, and has not returned any results yet.
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
- <% end %>
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::Mysql2.new(Oculus.connection_options)
39
- connection.execute("KILL QUERY #{query.thread_id}")
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::Mysql2.new(Oculus.connection_options)
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
@@ -1,3 +1,3 @@
1
1
  module Oculus
2
- VERSION = "0.8.0"
2
+ VERSION = "0.9.0"
3
3
  end
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 = { :host => 'localhost', :username => 'root' }
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
@@ -1,42 +1,33 @@
1
1
  require 'oculus'
2
2
 
3
3
  describe Oculus::Connection 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.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
- it "raises a Connection::Error on syntax errors" do
22
- lambda {
23
- subject.execute("FOO BAZ QUUX")
24
- }.should raise_error(Oculus::Connection::Error)
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
- 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)
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
- it "provides the connection's thread_id" do
40
- subject.thread_id.should be_an Integer
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
+
@@ -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.8.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-06 00:00:00.000000000 Z
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: &70114293249540 !ruby/object:Gem::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: *70114293249540
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: &70114293248540 !ruby/object:Gem::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: *70114293248540
35
+ version_requirements: *70346142609780
47
36
  - !ruby/object:Gem::Dependency
48
37
  name: rake
49
- requirement: &70114288904480 !ruby/object:Gem::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: *70114288904480
46
+ version_requirements: *70346142609320
58
47
  - !ruby/object:Gem::Dependency
59
48
  name: cucumber
60
- requirement: &70114288903580 !ruby/object:Gem::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: *70114288903580
57
+ version_requirements: *70346142608500
69
58
  - !ruby/object:Gem::Dependency
70
59
  name: rspec
71
- requirement: &70114288902160 !ruby/object:Gem::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: *70114288902160
68
+ version_requirements: *70346142607820
80
69
  - !ruby/object:Gem::Dependency
81
70
  name: capybara
82
- requirement: &70114288901540 !ruby/object:Gem::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: *70114288901540
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