oculus 0.9.2 → 0.9.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,34 @@
1
+ ## 0.9.3 (June 19, 2012)
2
+
3
+ Breaking Changes:
4
+
5
+ * `Oculus.cache_path` removed, please use `Oculus.storage_options[:cache_path]`
6
+ instead.
7
+
8
+ Features:
9
+
10
+ * Support for database-backed result storage (SequelStore). Usage:
11
+
12
+ Oculus.storage_options = {
13
+ adapter: 'sequel',
14
+ uri: 'mysql://localhost/your_db',
15
+ table: 'your_table' # default: oculus
16
+ }
17
+
18
+ *Jonathan Rudenberg*
19
+
20
+ * Hover actions for the SQL statement on the query detail page, to send it to
21
+ the editor or rerun it.
22
+
23
+ * Command-R/Ctrl-R executes the query instead of reloading the page in most
24
+ browsers.
25
+
26
+ Bugfixes:
27
+
28
+ * Escape special characters in query results.
29
+
30
+ *Jonathan Rudenberg*
31
+
32
+ ## 0.9.2 (June 2, 2012)
33
+
34
+ Initial public release
data/README.md CHANGED
@@ -10,7 +10,7 @@ and the results they returned, so your research is always at hand, easy to share
10
10
  and easy to repeat or reproduce in the future.
11
11
 
12
12
  **Oculus will not prevent you from doing stupid things! I recommend using a
13
- readonly MySQL account.**
13
+ readonly SQL account.**
14
14
 
15
15
  ## Installation
16
16
 
data/Rakefile CHANGED
@@ -9,7 +9,7 @@ require 'pg'
9
9
  desc 'Run RSpec tests'
10
10
  RSpec::Core::RakeTask.new(:spec) do |task|
11
11
  task.rspec_opts = %w[--color --format documentation]
12
- task.pattern = 'spec/*_spec.rb'
12
+ task.pattern = 'spec/**/*_spec.rb'
13
13
  end
14
14
 
15
15
  desc 'Run Cucumber features'
@@ -44,12 +44,12 @@ namespace :db do
44
44
 
45
45
  # Postgres
46
46
  #
47
- client = PG::Connection.new(:host => "localhost", :dbname => "postgres")
47
+ client = PG::Connection.new(:host => "localhost", :user => "postgres", :dbname => "postgres")
48
48
  client.query "DROP DATABASE IF EXISTS oculus_test"
49
49
  client.query "CREATE DATABASE oculus_test"
50
50
  client.close
51
51
 
52
- client = PG::Connection.new(:host => "localhost", :dbname => "oculus_test")
52
+ client = PG::Connection.new(:host => "localhost", :user => "postgres", :dbname => "oculus_test")
53
53
  client.query %[
54
54
  CREATE TABLE oculus_users (
55
55
  id INT NOT NULL UNIQUE,
data/bin/oculus CHANGED
@@ -31,6 +31,7 @@ Vegas::Runner.new(Oculus::Server, 'oculus') do |runner, opts, app|
31
31
  Oculus.connection_options[:adapter] = adapter
32
32
  end
33
33
  opts.on("-d", "--data DIRECTORY", "Data cache path (default: tmp/data)") do |path|
34
- Oculus.cache_path = path
34
+ Oculus.storage_options[:adapter] = "file"
35
+ Oculus.storage_options[:cache_path] = path
35
36
  end
36
37
  end
@@ -7,7 +7,11 @@ require 'capybara/cucumber'
7
7
  Capybara.app = Oculus::Server
8
8
  Capybara.default_wait_time = 10
9
9
 
10
- Oculus.cache_path = 'tmp/test_cache'
10
+ Oculus.storage_options = {
11
+ :adapter => 'file',
12
+ :cache_path => 'tmp/test_cache'
13
+ }
14
+
11
15
  Oculus.connection_options = {
12
16
  :adapter => 'mysql',
13
17
  :host => 'localhost',
@@ -7,17 +7,18 @@ module Oculus
7
7
  extend self
8
8
 
9
9
  DEFAULT_CONNECTION_OPTIONS = { :adapter => 'mysql', :host => 'localhost' }
10
+ DEFAULT_STORAGE_OPTIONS = { :adapter => 'file', :host => 'localhost' }
10
11
 
11
- attr_writer :cache_path
12
+ attr_writer :data_store
12
13
 
13
- def cache_path
14
- @cache_path ||= 'tmp/data'
14
+ def data_store
15
+ @data_store ||= Oculus::Storage.create(Oculus.storage_options)
15
16
  end
16
17
 
17
- attr_writer :data_store
18
+ attr_writer :storage_options
18
19
 
19
- def data_store
20
- @data_store ||= Oculus::Storage::FileStore.new(Oculus.cache_path)
20
+ def storage_options
21
+ @storage_options ||= DEFAULT_STORAGE_OPTIONS
21
22
  end
22
23
 
23
24
  attr_writer :connection_options
@@ -1,3 +1,5 @@
1
+ require 'csv'
2
+
1
3
  module Oculus
2
4
  class Query
3
5
  attr_accessor :id
@@ -118,6 +118,21 @@ button.starred .unstarred-contents {
118
118
  display: none;
119
119
  }
120
120
 
121
+ #query-sql-container {
122
+ position: relative;
123
+ }
124
+
125
+ #query-sql-actions {
126
+ display:none;
127
+ position: absolute;
128
+ top: 4px;
129
+ right: 5px;
130
+ }
131
+
132
+ #query-sql-container:hover #query-sql-actions {
133
+ display: block;
134
+ }
135
+
121
136
  #loading .cancel-container {
122
137
  float: right;
123
138
  margin: -1px -26px 0 0;
@@ -165,7 +165,7 @@ QueryEditor.prototype.initCodeMirror = function() {
165
165
  QueryEditor.prototype.bindEvents = function() {
166
166
  var self = this;
167
167
  $(document).on('keydown', 'form', function(e) {
168
- if ((e.keyCode === 13 && (e.ctrlKey || e.metaKey)) || (e.metaKey && e.altKey && e.keyCode === 82)) {
168
+ if ((e.keyCode === 13 || e.keyCode === 82) && (e.ctrlKey || e.metaKey)) {
169
169
  e.preventDefault();
170
170
 
171
171
  // CodeMirror normally saves itself automatically, but since there is no
@@ -2,7 +2,7 @@
2
2
 
3
3
  <form id="query-form" method="POST" action="/queries">
4
4
  <div id="editor-container">
5
- <textarea name="query" id="query-field"></textarea>
5
+ <textarea name="query" id="query-field"><%= h(params[:q]) %></textarea>
6
6
  </div>
7
7
  <div class="form-actions">
8
8
  <input type="submit" class="btn" name="submit" value="Run" />
@@ -86,7 +86,7 @@
86
86
  var row = $('<tr></tr>');
87
87
 
88
88
  for (var j = 0; j < result.length; j++) {
89
- row.append('<td>' + result[j] + '</td>');
89
+ row.append($('<td></td>').text(result[j]));
90
90
  }
91
91
 
92
92
  container.append(row);
@@ -102,4 +102,8 @@
102
102
  });
103
103
 
104
104
  editor.focus();
105
+
106
+ <% if params[:run] %>
107
+ editor.submit();
108
+ <% end %>
105
109
  </script>
@@ -38,7 +38,13 @@
38
38
  <input type="submit" class="btn" name="submit" value="Save" />
39
39
  </form>
40
40
 
41
- <pre class="cm-s-default"><%= @query.query %></pre>
41
+ <div id="query-sql-container">
42
+ <div id="query-sql-actions">
43
+ <a class="btn btn-mini" href="/?q=<%= URI.escape(@query.query) %>"><i class="icon-edit"></i> Send to editor</a>
44
+ <a class="btn btn-mini" href="/?q=<%= URI.escape(@query.query) %>&run=1"><i class="icon-repeat"></i> Rerun</a>
45
+ </div>
46
+ <pre class="cm-s-default"><%= @query.query %></pre>
47
+ </div>
42
48
 
43
49
  <div id="loading">
44
50
  <div class="alert">
@@ -96,7 +102,7 @@
96
102
  var row = $('<tr></tr>');
97
103
 
98
104
  for (var j = 0; j < result.length; j++) {
99
- row.append('<td>' + result[j] + '</td>');
105
+ row.append($('<td></td>').text(result[j]));
100
106
  }
101
107
 
102
108
  container.append(row);
@@ -1,7 +1,19 @@
1
- require 'oculus/storage/file_store'
2
-
3
1
  module Oculus
4
2
  module Storage
5
3
  class QueryNotFound < RuntimeError; end
4
+ class AdapterNotFound < StandardError; end
5
+
6
+ def self.create(options)
7
+ case options[:adapter]
8
+ when 'file'
9
+ require 'oculus/storage/file_store'
10
+ FileStore
11
+ when 'sequel'
12
+ require 'oculus/storage/sequel_store'
13
+ SequelStore
14
+ else
15
+ raise AdapterNotFound, "#{options[:adapter]} is not currently implemented. You should write it!"
16
+ end.new(options)
17
+ end
6
18
  end
7
19
  end
@@ -5,8 +5,8 @@ require 'fileutils'
5
5
  module Oculus
6
6
  module Storage
7
7
  class FileStore
8
- def initialize(root)
9
- @root = root
8
+ def initialize(options)
9
+ @root = options[:cache_path] || 'tmp/data'
10
10
  end
11
11
 
12
12
  def all_queries
@@ -0,0 +1,108 @@
1
+ require 'sequel'
2
+ require 'csv'
3
+
4
+ module Oculus
5
+ module Storage
6
+ class SequelStore
7
+ attr_reader :table_name
8
+
9
+ def initialize(options = {})
10
+ raise ArgumentError, "URI is required" unless options[:uri]
11
+ @uri = options[:uri]
12
+ @table_name = options[:table] || :oculus
13
+ create_table
14
+ end
15
+
16
+ def with_db
17
+ db = Sequel.connect(@uri, :encoding => 'utf8')
18
+ result = yield db
19
+ db.disconnect
20
+ result
21
+ end
22
+
23
+ def with_table
24
+ with_db { |db| yield db.from(@table_name) }
25
+ end
26
+
27
+ def all_queries
28
+ with_table do |table|
29
+ to_queries table.order(:id.desc)
30
+ end
31
+ end
32
+
33
+ def starred_queries
34
+ with_table do |table|
35
+ to_queries table.where(:starred => true).order(:id.desc)
36
+ end
37
+ end
38
+
39
+ def save_query(query)
40
+ attrs = serialize(query)
41
+ with_table do |table|
42
+ if query.id
43
+ table.where(:id => query.id).update(attrs)
44
+ else
45
+ query.id = table.insert(attrs)
46
+ end
47
+ end
48
+ end
49
+
50
+ def load_query(id)
51
+ with_table do |table|
52
+ if query = table.where(:id => id).first
53
+ deserialize query
54
+ else
55
+ raise QueryNotFound, id
56
+ end
57
+ end
58
+ end
59
+
60
+ def delete_query(id)
61
+ with_table do |table|
62
+ raise ArgumentError unless id.to_i > 0
63
+ raise QueryNotFound, id unless table.where(:id => id).delete == 1
64
+ end
65
+ end
66
+
67
+ def create_table
68
+ with_db do |db|
69
+ db.create_table?(table_name) do
70
+ primary_key :id
71
+ Integer :thread_id
72
+ String :name
73
+ String :author
74
+ String :query
75
+ String :results
76
+ Time :started_at
77
+ Time :finished_at
78
+ TrueClass :starred
79
+ String :error
80
+ end
81
+ end
82
+ end
83
+
84
+ def drop_table
85
+ with_db { |db| db.drop_table(table_name) }
86
+ end
87
+
88
+ private
89
+
90
+ def to_queries(rows)
91
+ rows.map { |r| Query.new deserialize(r) }
92
+ end
93
+
94
+ def deserialize(row)
95
+ row[:results] = row[:results] ? CSV.new(row[:results]).to_a : []
96
+ row.delete(:error) unless row[:error]
97
+ row
98
+ end
99
+
100
+ def serialize(query)
101
+ attrs = query.attributes
102
+ attrs[:starred] ||= false
103
+ attrs[:results] = query.to_csv if query.results
104
+ attrs
105
+ end
106
+ end
107
+ end
108
+ end
@@ -1,3 +1,3 @@
1
1
  module Oculus
2
- VERSION = "0.9.2"
2
+ VERSION = "0.9.3"
3
3
  end
@@ -25,4 +25,5 @@ Gem::Specification.new do |gem|
25
25
 
26
26
  gem.add_development_dependency "mysql2", [">= 0.3.11"]
27
27
  gem.add_development_dependency "pg", [">= 0.13.2"]
28
+ gem.add_development_dependency "sequel", [">= 3"]
28
29
  end
@@ -2,7 +2,7 @@ require 'oculus'
2
2
  require 'oculus/connection/postgres'
3
3
 
4
4
  describe Oculus::Connection::Postgres do
5
- subject { Oculus::Connection::Postgres.new(:host => 'localhost', :database => 'oculus_test') }
5
+ subject { Oculus::Connection::Postgres.new(:host => 'localhost', :username => 'postgres', :database => 'oculus_test') }
6
6
 
7
7
  it "fetches a result set" do
8
8
  subject.execute("SELECT * FROM oculus_users").should == [['id', 'name'],
@@ -18,14 +18,14 @@ describe Oculus::Connection do
18
18
 
19
19
  describe "postgres adapter option" do
20
20
  it "returns a new instance of Postgres adapter" do
21
- adapter = Oculus::Connection.connect adapter: 'postgres', database: 'oculus_test'
21
+ adapter = Oculus::Connection.connect adapter: 'postgres', database: 'oculus_test', :host => 'localhost', :username => 'postgres'
22
22
  adapter.should be_an_instance_of Oculus::Connection::Postgres
23
23
  end
24
24
  end
25
25
 
26
26
  describe "pg adapter alias" do
27
27
  it "returns a new instance of Postgres adapter" do
28
- adapter = Oculus::Connection.connect adapter: 'pg', database: 'oculus_test'
28
+ adapter = Oculus::Connection.connect adapter: 'pg', database: 'oculus_test', :host => 'localhost', :username => 'postgres'
29
29
  adapter.should be_an_instance_of Oculus::Connection::Postgres
30
30
  end
31
31
  end
@@ -1,8 +1,28 @@
1
1
  require 'oculus'
2
+ require 'oculus/storage/file_store'
3
+ require 'oculus/storage/sequel_store'
4
+
5
+ describe Oculus::Storage do
6
+ describe "#create" do
7
+ it "should initialize FileStore" do
8
+ Oculus::Storage::FileStore.should_receive(:new)
9
+ Oculus::Storage.create(:adapter => 'file')
10
+ end
2
11
 
3
- describe Oculus::Storage::FileStore do
4
- subject { Oculus::Storage::FileStore.new('tmp/test_cache') }
12
+ it "should initialize SequelStore" do
13
+ Oculus::Storage::SequelStore.should_receive(:new)
14
+ Oculus::Storage.create(:adapter => 'sequel')
15
+ end
5
16
 
17
+ it "should forward its options to the storage" do
18
+ opts = { :adapter => 'file' }
19
+ Oculus::Storage::FileStore.should_receive(:new).with(opts)
20
+ Oculus::Storage.create(opts)
21
+ end
22
+ end
23
+ end
24
+
25
+ shared_examples "storage" do |subject|
6
26
  let(:query) do
7
27
  Oculus::Query.new(:name => "All users",
8
28
  :query => "SELECT * FROM oculus_users",
@@ -27,18 +47,10 @@ describe Oculus::Storage::FileStore do
27
47
  :error => "You have an error in your SQL syntax")
28
48
  end
29
49
 
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
50
+ it "round-trips a query with no results" do
39
51
  query = Oculus::Query.new(:name => "Unfinished query", :author => "Me")
40
- subject.save_query(query)
41
- subject.load_query(query.id).should == {
52
+ storage.save_query(query)
53
+ storage.load_query(query.id).should == {
42
54
  :id => query.id,
43
55
  :name => query.name,
44
56
  :author => query.author,
@@ -51,9 +63,9 @@ describe Oculus::Storage::FileStore do
51
63
  }
52
64
  end
53
65
 
54
- it "round-trips a query with an error to disk" do
55
- subject.save_query(broken_query)
56
- subject.load_query(broken_query.id).should == {
66
+ it "round-trips a query with an error" do
67
+ storage.save_query(broken_query)
68
+ storage.load_query(broken_query.id).should == {
57
69
  :id => broken_query.id,
58
70
  :name => broken_query.name,
59
71
  :error => broken_query.error,
@@ -67,9 +79,9 @@ describe Oculus::Storage::FileStore do
67
79
  }
68
80
  end
69
81
 
70
- it "round-trips a query to disk" do
71
- subject.save_query(query)
72
- subject.load_query(query.id).should == {
82
+ it "round-trips a query" do
83
+ storage.save_query(query)
84
+ storage.load_query(query.id).should == {
73
85
  :id => query.id,
74
86
  :name => query.name,
75
87
  :author => query.author,
@@ -83,75 +95,100 @@ describe Oculus::Storage::FileStore do
83
95
  end
84
96
 
85
97
  it "doesn't overwrite an existing query id when saving" do
86
- subject.save_query(query)
98
+ storage.save_query(query)
87
99
  original_id = query.id
88
- subject.save_query(query)
100
+ storage.save_query(query)
89
101
  query.id.should == original_id
90
102
  end
91
103
 
92
104
  it "raises QueryNotFound for missing queries" do
93
105
  lambda {
94
- subject.load_query(39827493)
106
+ storage.load_query(39827493)
95
107
  }.should raise_error(Oculus::Storage::QueryNotFound)
96
108
  end
97
109
 
98
110
  it "fetches all queries in reverse chronological order" do
99
- subject.save_query(query)
100
- subject.save_query(other_query)
111
+ storage.save_query(query)
112
+ storage.save_query(other_query)
101
113
 
102
- subject.all_queries.map(&:results).should == [other_query.results, query.results]
114
+ storage.all_queries.map(&:results).should == [other_query.results, query.results]
103
115
  end
104
116
 
105
117
  it "fetches starred queries" do
106
118
  query.starred = true
107
- subject.save_query(query)
108
- subject.save_query(other_query)
119
+ storage.save_query(query)
120
+ storage.save_query(other_query)
109
121
 
110
- results = subject.starred_queries
122
+ results = storage.starred_queries
111
123
  results.map(&:results).should == [query.results]
112
124
  results.first.starred.should be true
113
125
  end
114
126
 
115
127
  it "deletes queries" do
116
- subject.save_query(query)
117
- subject.load_query(query.id)[:name].should == query.name
118
- subject.delete_query(query.id)
128
+ storage.save_query(query)
129
+ storage.load_query(query.id)[:name].should == query.name
130
+ storage.delete_query(query.id)
119
131
 
120
132
  lambda {
121
- subject.load_query(query.id)
133
+ storage.load_query(query.id)
122
134
  }.should raise_error(Oculus::Storage::QueryNotFound)
123
135
  end
124
136
 
125
137
  it "raises QueryNotFound when deleting a nonexistent query" do
126
138
  lambda {
127
- subject.delete_query(10983645)
139
+ storage.delete_query(10983645)
128
140
  }.should raise_error(Oculus::Storage::QueryNotFound)
129
141
  end
130
142
 
131
143
  it "sanitizes query IDs" do
132
144
  lambda {
133
- subject.delete_query('..')
145
+ storage.delete_query('..')
134
146
  }.should raise_error(ArgumentError)
135
147
  end
148
+ end
149
+
150
+ describe Oculus::Storage::FileStore do
151
+ it_behaves_like "storage" do
152
+ let(:storage) { Oculus::Storage::FileStore.new(:cache_path => 'tmp/test_cache') }
136
153
 
137
- context "when cache dir does not exist (like for a new install)" do
138
154
  before do
155
+ FileUtils.mkdir_p('tmp/test_cache')
156
+ end
157
+
158
+ after do
139
159
  FileUtils.rm_r('tmp/test_cache')
140
160
  end
141
161
 
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
- }
162
+ context "when cache dir does not exist (like for a new install)" do
163
+ before do
164
+ FileUtils.rm_r('tmp/test_cache')
165
+ end
166
+
167
+ it "round-trips a query to disk" do
168
+ storage.save_query(query)
169
+ storage.load_query(query.id).should == {
170
+ :id => query.id,
171
+ :name => query.name,
172
+ :author => query.author,
173
+ :query => query.query,
174
+ :results => query.results,
175
+ :thread_id => query.thread_id,
176
+ :starred => false,
177
+ :started_at => query.started_at,
178
+ :finished_at => query.finished_at
179
+ }
180
+ end
181
+ end
182
+ end
183
+ end
184
+
185
+ describe Oculus::Storage::SequelStore do
186
+ it_behaves_like "storage" do
187
+ let(:storage) { Oculus::Storage::SequelStore.new(:uri => 'postgres://postgres@localhost/oculus_test') }
188
+
189
+ before do
190
+ storage.drop_table
191
+ storage.create_table
155
192
  end
156
193
  end
157
194
  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.9.2
4
+ version: 0.9.3
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-06-03 00:00:00.000000000 Z
12
+ date: 2012-06-20 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: sinatra
@@ -139,6 +139,22 @@ dependencies:
139
139
  - - ! '>='
140
140
  - !ruby/object:Gem::Version
141
141
  version: 0.13.2
142
+ - !ruby/object:Gem::Dependency
143
+ name: sequel
144
+ requirement: !ruby/object:Gem::Requirement
145
+ none: false
146
+ requirements:
147
+ - - ! '>='
148
+ - !ruby/object:Gem::Version
149
+ version: '3'
150
+ type: :development
151
+ prerelease: false
152
+ version_requirements: !ruby/object:Gem::Requirement
153
+ none: false
154
+ requirements:
155
+ - - ! '>='
156
+ - !ruby/object:Gem::Version
157
+ version: '3'
142
158
  description: Oculus is a web-based logging SQL client. It keeps a history of your
143
159
  queries and the results they returned, so your research is always at hand, easy
144
160
  to share and easy to repeat or reproduce in the future.
@@ -151,6 +167,7 @@ extra_rdoc_files: []
151
167
  files:
152
168
  - .gitignore
153
169
  - .travis.yml
170
+ - CHANGELOG.md
154
171
  - Gemfile
155
172
  - LICENSE
156
173
  - README.md
@@ -186,14 +203,15 @@ files:
186
203
  - lib/oculus/server/views/starred.erb
187
204
  - lib/oculus/storage.rb
188
205
  - lib/oculus/storage/file_store.rb
206
+ - lib/oculus/storage/sequel_store.rb
189
207
  - lib/oculus/version.rb
190
208
  - oculus.gemspec
191
209
  - spec/connection/mysql2_spec.rb
192
210
  - spec/connection/postgres_spec.rb
193
211
  - spec/connection_spec.rb
194
- - spec/file_store_spec.rb
195
212
  - spec/query_presenter_spec.rb
196
213
  - spec/query_spec.rb
214
+ - spec/storage_spec.rb
197
215
  homepage: http://oculusapp.com
198
216
  licenses: []
199
217
  post_install_message: