adhoq 0.0.2 → 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +45 -12
  3. data/app/assets/javascripts/adhoq/application.js +1 -0
  4. data/app/assets/javascripts/adhoq/current_tables.js.coffee +4 -0
  5. data/app/assets/stylesheets/adhoq/adhoq.css.sass +44 -0
  6. data/app/assets/stylesheets/adhoq/bootstrap.scss +1 -1
  7. data/app/controllers/adhoq/authorization_methods.rb +1 -1
  8. data/app/controllers/adhoq/current_tables_controller.rb +20 -0
  9. data/app/controllers/adhoq/executions_controller.rb +2 -2
  10. data/app/helpers/adhoq/application_helper.rb +13 -0
  11. data/app/models/adhoq/execution.rb +1 -1
  12. data/app/models/adhoq/report.rb +10 -17
  13. data/app/views/adhoq/application/_global_nav.html.slim +1 -0
  14. data/app/views/adhoq/current_tables/index.html.slim +32 -0
  15. data/app/views/adhoq/queries/_form.html.slim +29 -9
  16. data/app/views/adhoq/queries/_query.html.slim +1 -1
  17. data/config/routes.rb +2 -1
  18. data/lib/adhoq.rb +7 -6
  19. data/lib/adhoq/adhoc_execution.rb +11 -0
  20. data/lib/adhoq/executor.rb +12 -1
  21. data/lib/adhoq/global_variable.rb +8 -5
  22. data/lib/adhoq/reporter.rb +14 -0
  23. data/lib/adhoq/storage.rb +12 -1
  24. data/lib/adhoq/storage/fog_storage.rb +22 -0
  25. data/lib/adhoq/storage/local_file.rb +10 -29
  26. data/lib/adhoq/storage/s3.rb +24 -0
  27. data/lib/adhoq/version.rb +1 -1
  28. data/spec/adhoq/executor_spec.rb +13 -0
  29. data/spec/adhoq/global_variable_spec.rb +37 -0
  30. data/spec/adhoq/reporter/xlsx_spec.rb +18 -0
  31. data/spec/adhoq/storage_spec.rb +12 -2
  32. data/spec/spec_helper.rb +29 -2
  33. data/spec/support/codeclimate_reporter.rb +4 -0
  34. data/spec/support/feature_spec_helper.rb +5 -0
  35. data/spec/support/have_values_in_xlsx_sheet_matcher.rb +9 -3
  36. metadata +97 -13
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: cdf4e3ebbc61cde7c4ab96819f90274043380030
4
- data.tar.gz: ba15a928b583929ebf15ef137eb72feb416cfdbd
3
+ metadata.gz: a9fac13cfa25151dce3d5c579c39ba8098f07b6f
4
+ data.tar.gz: 23f822e233cc7977a2461dbe681a30d2426fffd5
5
5
  SHA512:
6
- metadata.gz: b3ba6a550268f3a7b899b240b720a15fb5d5000434cd2812bc43b001f0c9e86251d24112abd06400be1c952cca25f6188b795cfb8c9a8d4fbbf1c716a3874273
7
- data.tar.gz: 7cdc68ee8876b133c3bfde9830d66ca1f06cd7c0af4ebedca813afdc8ce9573123ea97220a7f810dec73ecd35a68c895c9c3a3f533bece43ac6ef5ee4acb7541
6
+ metadata.gz: f6d1804945abba5952f20f671f68e5fab9141d474dfdcc70aab8a6317cd3c975296e244b6d480a27a2e19066ec2c56eeaadde874ddd3617281275b23c08de6bb
7
+ data.tar.gz: 25dac12d074dff4dead41cb23271447737f237103aa494f4148b509b0a6f8cb30be2c1bd3aff9e63fe83763836c744be5249e48422d78d290afdd26685473904
data/README.md CHANGED
@@ -1,4 +1,5 @@
1
- # Adhoq
1
+ Adhoq [![Build Status](https://travis-ci.org/esminc/adhoq.svg)](https://travis-ci.org/esminc/adhoq) [![Code Climate](https://codeclimate.com/github/esminc/adhoq/badges/gpa.svg)](https://codeclimate.com/github/esminc/adhoq)
2
+ ====
2
3
 
3
4
  Rails engine to generate instant reports from adhoc SQL query.
4
5
 
@@ -6,16 +7,17 @@ Rails engine to generate instant reports from adhoc SQL query.
6
7
 
7
8
  ## Features
8
9
 
9
- - [x] Rails 4.x support
10
- - [ ] Rails 3.2 support
10
+ - Export ad-hoc SQL result to .xlsx file
11
+ - Persist generated report as local file or in AWS S3
12
+ - Rails 4.x & 3.2 support
13
+ - Nice administration console with rails engine
14
+
15
+ ### Future planning
16
+
11
17
  - Export reports in some formats:
12
- - [x] .xlsx
13
18
  - [ ] .csv
14
19
  - [ ] .json
15
- - Report storage supports:
16
- - [x] as local file
17
- - [ ] S3 (via `Fog::Storage`)
18
- - [ ] In application export function helper
20
+ - [ ] Label data substitution
19
21
 
20
22
  ## Installation
21
23
 
@@ -58,22 +60,53 @@ Edit initialization file in `config/initializer/adhoq.rb`
58
60
 
59
61
  ```ruby
60
62
  Adhoq.configure do |config|
61
- config.storage = [:local_file, Rails.root + '/path/to/store/report/files']
63
+ config.storage = [:local_file, Rails.root + './path/to/store/report/files']
62
64
  config.authorization = ->(controller) { controller.signed_in? }
63
65
  end
64
66
  ```
65
67
 
66
- See configuration example in [dummy app](https://github.com/esminc/adhoq/commit/63cc31fe209baae7211c10f76e7b1c35f4c699ef).
68
+ See configuration example in [dummy app](https://github.com/esminc/adhoq/blob/master/spec/dummy/config/initializers/adhoq.rb).
67
69
 
68
70
  Then restart server and try it out.
69
71
 
70
72
  ### As Plain old library (application export helper)
71
73
 
72
- TODO: Write usage instructions here
74
+ Adhoq also provides report generation from SQL string, not from mounted rails engine.
75
+
76
+ ```ruby
77
+ execution = Adhoq::AdhocExecution.new(
78
+ 'xlsx',
79
+ 'SELECT "hello" AS name ,"English greeting message" AS description'
80
+ )
81
+
82
+ Adhoq::Reporter.generate(execution) #=> report data
83
+ ```
84
+
85
+ Persistence is also available without engine via `Adhoq::Storage::SomeClass#store`.
86
+ Below is example that generating report and persist to in Rails application report method.
87
+
88
+ ```ruby
89
+ execution = Adhoq::AdhocExecution.new(
90
+ 'xlsx',
91
+ 'SELECT "hello" AS name ,"English greeting message" AS description'
92
+ )
93
+
94
+ storage = Storage::S3.new(
95
+ 'my-adhoq-bucket',
96
+ aws_access_key_id: 'key_id',
97
+ aws_secret_access_key: 'access_key'
98
+ )
99
+
100
+ # generate report and store it to S3, returns `key` to get report data
101
+ key = storage.store('.xlsx') { Adhoq::Reporter.generate(execution) }
102
+
103
+ ...
104
+ storage.get(key) #=> report data
105
+ ```
73
106
 
74
107
  ## Contributing
75
108
 
76
- 1. Fork it ( https://github.com/[my-github-username]/adhoq/fork )
109
+ 1. Fork it ( https://github.com/esminc/adhoq/fork )
77
110
  2. Create your feature branch (`git checkout -b my-new-feature`)
78
111
  3. Commit your changes (`git commit -am 'Add some feature'`)
79
112
  4. Push to the branch (`git push origin my-new-feature`)
@@ -15,5 +15,6 @@
15
15
  //= require jquery_ujs
16
16
  //= require ./bootstrap
17
17
  //= require ./previewer
18
+ //= require ./current_tables
18
19
 
19
20
  window.Adhoq = {}
@@ -0,0 +1,4 @@
1
+ Adhoq.loadCurrentTableTabOnce = ($el)->
2
+ pane = $("#{$el.attr('href')}:has(.loading)")
3
+
4
+ pane.load(pane.find('a.loading').attr('href'))
@@ -55,3 +55,47 @@ form.query-form
55
55
 
56
56
  .fa-pad-l
57
57
  padding-left: $short-span / 2
58
+
59
+ #current-tables
60
+ font-size: $font-size-base * 0.9
61
+
62
+ table
63
+ width: 800px
64
+
65
+ caption
66
+ text-align: left
67
+ margin-bottom: $short-span
68
+ font-size: $font-size-base * 1.5
69
+
70
+ span.name
71
+ font-family: monospace
72
+ small.count
73
+ font-size: $font-size-base * 1
74
+ color: gray
75
+ margin-left: $font-size-base
76
+
77
+ th.pk, td.pk
78
+ width: $font-size-base * 1.5
79
+ th, td.icon
80
+ text-align: center
81
+ th.number, td.number
82
+ text-align: right
83
+ th.monospace, td.monospace
84
+ font-family: monospace
85
+ th.data
86
+ width: $font-size-base * 10
87
+
88
+ .tab-content
89
+ margin-top: $font-size-base
90
+ padding-left: 1em
91
+ padding-right: 1em
92
+
93
+ max-height: 600px
94
+ overflow: scroll
95
+
96
+ .tab-pane > h3
97
+ font-size: $font-size-base * 1.5
98
+
99
+ small
100
+ font-size: $font-size-base
101
+ margin-left: $font-size-base
@@ -5,7 +5,7 @@
5
5
  // Reset and dependencies
6
6
  @import "bootstrap/normalize";
7
7
  @import "bootstrap/print";
8
- @import "bootstrap/glyphicons";
8
+ // @import "bootstrap/glyphicons";
9
9
 
10
10
  // Core CSS
11
11
  @import "bootstrap/scaffolding";
@@ -3,7 +3,7 @@ module Adhoq
3
3
  extend ActiveSupport::Concern
4
4
 
5
5
  included do |controller|
6
- controller.before_action Authorizer.new
6
+ controller.before_filter Authorizer.new
7
7
 
8
8
  helper_method :adhoq_current_user
9
9
  hide_action :adhoq_current_user
@@ -0,0 +1,20 @@
1
+ module Adhoq
2
+ class CurrentTablesController < Adhoq::ApplicationController
3
+ before_filter :eager_load_models
4
+
5
+ def index
6
+ @ar_classes = ActiveRecord::Base.subclasses.
7
+ reject {|klass| klass.name == 'ActiveRecord::SchemaMigration' }.
8
+ sort_by(&:name)
9
+
10
+ render layout: false
11
+ end
12
+
13
+ private
14
+
15
+ def eager_load_models
16
+ return unless Rails.env.development?
17
+ [Rails.application, Adhoq::Engine].each(&:eager_load!)
18
+ end
19
+ end
20
+ end
@@ -1,5 +1,5 @@
1
1
  module Adhoq
2
- class ExecutionsController < ApplicationController
2
+ class ExecutionsController < Adhoq::ApplicationController
3
3
  def show
4
4
  @execution = current_query.executions.where(id: params[:id], report_format: params[:format]).first!
5
5
 
@@ -19,7 +19,7 @@ module Adhoq
19
19
  end
20
20
 
21
21
  def respond_report(report)
22
- send_data report.data.read, type: report.mime_type, filename: report.name, disposition: 'attachment'
22
+ send_data report.data, type: report.mime_type, filename: report.name, disposition: 'attachment'
23
23
  end
24
24
  end
25
25
  end
@@ -7,5 +7,18 @@ module Adhoq
7
7
  klass.model_name.humanize
8
8
  end
9
9
  end
10
+
11
+ def icon_fa(name, additional_classes = [])
12
+ tag('i', class: ['fa', "fa-#{name}", *additional_classes])
13
+ end
14
+
15
+ def schema_version
16
+ if defined?(ActiveRecord::SchemaMigration)
17
+ ActiveRecord::SchemaMigration.maximum(:version)
18
+ else
19
+ result = Adhoq::Executor.select("SELECT MAX(version) AS current_version FROM #{ActiveRecord::Migrator.schema_migrations_table_name}")
20
+ result.rows.first
21
+ end
22
+ end
10
23
  end
11
24
  end
@@ -18,7 +18,7 @@ module Adhoq
18
18
  end
19
19
 
20
20
  def success?
21
- report.try(:success?)
21
+ report.try(:available?)
22
22
  end
23
23
 
24
24
  # TODO go decorator or view model or so
@@ -1,11 +1,8 @@
1
1
  module Adhoq
2
2
  class Report < ActiveRecord::Base
3
- BUFSIZE = 10.kilobytes.to_i
4
-
5
3
  belongs_to :execution
6
4
 
7
- delegate :name, to: 'execution'
8
- delegate :mime_type, to: :reporter
5
+ delegate :name, to: 'execution'
9
6
 
10
7
  def generate!(storage = Adhoq.current_storage)
11
8
  self.identifier = generate_and_persist_report!(storage)
@@ -15,28 +12,24 @@ module Adhoq
15
12
  save!
16
13
  end
17
14
 
18
- def success?
19
- data.present?
15
+ def available?
16
+ identifier.present? && (storage == Adhoq.current_storage.identifier)
20
17
  end
21
18
 
22
19
  def data(storage = Adhoq.current_storage)
23
20
  storage.get(identifier)
24
21
  end
25
22
 
26
- private
27
-
28
- def reporter
29
- {'xlsx' => Adhoq::Reporter::Xlsx}[execution.report_format]
23
+ def mime_type
24
+ Adhoq::Reporter.lookup(execution.report_format).mime_type
30
25
  end
31
26
 
32
- def generate_and_persist_report!(storage)
33
- storage.store(".#{execution.report_format}") do |file, *|
34
- executor = Executor.new(execution.raw_sql)
27
+ private
35
28
 
36
- reporter.new(executor.execute).build_report.each(BUFSIZE) do |chunk|
37
- file.write chunk
38
- end
39
- end
29
+ def generate_and_persist_report!(storage)
30
+ storage.store(".#{execution.report_format}") {
31
+ Adhoq::Reporter.generate(execution)
32
+ }
40
33
  end
41
34
  end
42
35
  end
@@ -9,3 +9,4 @@ nav#global-nav.navbar.navbar-default(role='navigation')
9
9
  = link_to main_app.root_path do
10
10
  i.fa.fa-arrow-circle-right.fa-pad-r
11
11
  | Main app
12
+
@@ -0,0 +1,32 @@
1
+ h3
2
+ | Current database schema
3
+ small= "Version #{schema_version}"
4
+
5
+ ul.list-unstyled.tables
6
+ - @ar_classes.each do |ar_class|
7
+ - first_record = ar_class.unscoped.order(:id).first
8
+
9
+ li.ar_class data-table-name=ar_class.table_name
10
+ table.table.table-striped.table-hover.table-bordered
11
+ caption
12
+ span.name= ar_class.table_name
13
+ small.count #{ar_class.unscoped.count} rows
14
+ thead
15
+ tr
16
+ th.col-sm-1.pk PK
17
+ th.col-sm-2.name Name
18
+ th.col-sm-1.type Type
19
+ th.col-sm-1.null Non-Null
20
+ th.col-sm-1.limit Limit
21
+ th.col-sm-2.default Default
22
+ th.col-sm-4.data unscoped.first
23
+ tbody
24
+ - ar_class.columns.each do |column|
25
+ tr
26
+ td.pk.icon= column.primary ? icon_fa('check-circle') : ''
27
+ td.monospace= column.name
28
+ td= column.type
29
+ td.null.icon= column.null ? '' : icon_fa('check')
30
+ td.limit.number= column.limit
31
+ td= column.default
32
+ td.monospace= first_record.try {|r| truncate(r.read_attribute_before_type_cast(column.name).to_s, length: 50) }
@@ -24,15 +24,35 @@
24
24
  .col-sm-8
25
25
  = f.text_area :query, class: 'form-control', rows: 10, required: true
26
26
 
27
- .preview
28
- h2
29
- | Preview
30
- .btn-in-header
31
- = link_to preview_path, class: 'btn btn-default btn-sm js-preview-button', data: {source: '#query_query', result: '.js-preview-result', remote: true, method: 'POST'} do
32
- i.fa.fa-refresh[data-title='Refresh preview']
27
+ ul.nav.nav-tabs[role='tablist']
28
+ li.active
29
+ a[role='tab' data-toggle='tab' href='#preview' ]
30
+ i.fa.fa-eye.fa-pad-r
31
+ | Preview
32
+ li
33
+ a[role='tab' data-toggle='tab' href='#current-tables']
34
+ i.fa.fa-database.fa-pad-r
35
+ | Tables
33
36
 
34
- .js-preview-result
35
- .alert Preview is shown here
37
+ .tab-content
38
+ #preview.tab-pane.active
39
+ h3
40
+ | Query preview
41
+ small
42
+ = link_to preview_path, class: 'js-preview-button', data: {source: '#query_query', result: '.js-preview-result', remote: true, method: 'POST'} do
43
+ i.fa.fa-refresh.fa-pad-r[data-title='Refresh preview']
44
+ | Reflesh
45
+
46
+ .js-preview-result
47
+ .alert.alert-info Preview is shown here
48
+
49
+ #current-tables.tab-pane
50
+ a.loading[href=current_tables_path]
36
51
 
37
52
  javascript:
38
- $(function() { Adhoq.enablePreview($('.preview a.js-preview-button')) })
53
+ $(function() {
54
+ Adhoq.enablePreview($('#preview a.js-preview-button'));
55
+
56
+ $('a[data-toggle="tab"]').on('show.bs.tab', function(ev) { Adhoq.loadCurrentTableTabOnce($(ev.target)) });
57
+ });
58
+
@@ -38,6 +38,6 @@ section.query
38
38
  span.label[class=(exec.success? ? 'label-success' : 'label-danger')]= exec.status_label
39
39
  td.report
40
40
  - if exec.success?
41
- = link_to([query, exec, format: exec.report_format], class: 'btn btn-sm btn-default') do
41
+ = link_to(adhoq.query_execution_path(query, exec, format: exec.report_format), class: 'btn btn-sm btn-default') do
42
42
  i.fa.fa-download.fa-pad-r
43
43
  = exec.report_format
data/config/routes.rb CHANGED
@@ -5,5 +5,6 @@ Adhoq::Engine.routes.draw do
5
5
  resources :executions, only: %w(create show)
6
6
  end
7
7
 
8
- resource :preview, only: 'create'
8
+ resource :preview, only: 'create'
9
+ resources :current_tables, only: 'index'
9
10
  end
data/lib/adhoq.rb CHANGED
@@ -2,12 +2,13 @@ require 'adhoq/engine'
2
2
  require 'adhoq/global_variable'
3
3
 
4
4
  module Adhoq
5
- autoload 'Configuration', 'adhoq/configuration'
6
- autoload 'Error', 'adhoq/error'
7
- autoload 'Executor', 'adhoq/executor'
8
- autoload 'Reporter', 'adhoq/reporter'
9
- autoload 'Result', 'adhoq/result'
10
- autoload 'Storage', 'adhoq/storage'
5
+ autoload 'AdhocExecution', 'adhoq/adhoc_execution'
6
+ autoload 'Configuration', 'adhoq/configuration'
7
+ autoload 'Error', 'adhoq/error'
8
+ autoload 'Executor', 'adhoq/executor'
9
+ autoload 'Reporter', 'adhoq/reporter'
10
+ autoload 'Result', 'adhoq/result'
11
+ autoload 'Storage', 'adhoq/storage'
11
12
 
12
13
  extend Adhoq::GlobalVariable
13
14
  end
@@ -0,0 +1,11 @@
1
+ # Provides polymorphic API with Adhoc::Execution AR Model
2
+ module Adhoq
3
+ class AdhocExecution
4
+ attr_reader :report_format, :raw_sql
5
+
6
+ def initialize(report_format, raw_sql)
7
+ @report_format = report_format
8
+ @raw_sql = raw_sql
9
+ end
10
+ end
11
+ end
@@ -2,12 +2,23 @@ module Adhoq
2
2
  class Executor
3
3
  class << self
4
4
  def select(query)
5
- current_connection.send(:select, query)
5
+ with_sandbox do
6
+ current_connection.exec_query(query)
7
+ end
6
8
  end
7
9
 
8
10
  def current_connection
9
11
  ActiveRecord::Base.connection
10
12
  end
13
+
14
+ def with_sandbox
15
+ result = nil
16
+ ActiveRecord::Base.transaction do
17
+ result = yield
18
+ raise ActiveRecord::Rollback
19
+ end
20
+ result
21
+ end
11
22
  end
12
23
 
13
24
  def initialize(query)
@@ -22,13 +22,16 @@ module Adhoq
22
22
 
23
23
  private
24
24
 
25
- # TODO Implement S3
26
25
  def setup_storage(type, *args)
27
- unless type == :local_file
28
- raise NotImplementedError
29
- end
26
+ klass =
27
+ case type
28
+ when :local_file then Adhoq::Storage::LocalFile
29
+ when :s3 then Adhoq::Storage::S3
30
+ else
31
+ raise NotImplementedError
32
+ end
30
33
 
31
- Adhoq::Storage::LocalFile.new(*args)
34
+ klass.new(*args)
32
35
  end
33
36
  end
34
37
  end
@@ -1,5 +1,19 @@
1
1
  module Adhoq
2
2
  module Reporter
3
3
  autoload 'Xlsx', 'adhoq/reporter/xlsx'
4
+
5
+ class << self
6
+ def generate(execution)
7
+ executor = Executor.new(execution.raw_sql)
8
+ reporter = lookup(execution.report_format).new(executor.execute)
9
+
10
+ reporter.build_report
11
+ end
12
+
13
+ def lookup(format)
14
+ @map ||= {'xlsx' => Adhoq::Reporter::Xlsx}
15
+ @map[format]
16
+ end
17
+ end
4
18
  end
5
19
  end
data/lib/adhoq/storage.rb CHANGED
@@ -1,5 +1,16 @@
1
1
  module Adhoq
2
2
  module Storage
3
- autoload 'LocalFile', 'adhoq/storage/local_file'
3
+ autoload 'FogStorage', 'adhoq/storage/fog_storage'
4
+ autoload 'LocalFile', 'adhoq/storage/local_file'
5
+ autoload 'S3', 'adhoq/storage/s3'
6
+
7
+ def with_new_identifier(suffix = nil, seed = Time.now)
8
+ dirname, fname_seed = ['%Y-%m-%d', '%H%M%S.%L'].map {|f| seed.strftime(f) }
9
+
10
+ basename = "%s_%05d%s" % [fname_seed, Process.pid, suffix]
11
+
12
+ [dirname, basename].join('/').tap {|id| yield id }
13
+ end
14
+ module_function :with_new_identifier
4
15
  end
5
16
  end
@@ -0,0 +1,22 @@
1
+ module Adhoq
2
+ module Storage
3
+ class FogStorage
4
+ def store(suffix = nil, seed = Time.now, &block)
5
+ Adhoq::Storage.with_new_identifier(suffix, seed) do |identifier|
6
+ io = yield
7
+ io.rewind
8
+
9
+ directory.files.create(key: identifier, body: io, public: false)
10
+ end
11
+ end
12
+
13
+ def get(identifier)
14
+ get_raw(identifier).body
15
+ end
16
+
17
+ def get_raw(identifier)
18
+ directory.files.head(identifier)
19
+ end
20
+ end
21
+ end
22
+ end
@@ -1,44 +1,25 @@
1
+ require 'fog'
2
+
1
3
  module Adhoq
2
4
  module Storage
3
- class LocalFile
5
+ class LocalFile < FogStorage
4
6
  attr_reader :root
5
7
 
6
8
  def initialize(root_path)
7
- @root = Pathname.new(root_path)
8
- end
9
-
10
- def identifier
11
- "file://#{@root.realpath}"
12
- end
13
-
14
- def store(suffix = nil, seed = Time.now, &block)
15
- calculate_identifier(suffix, seed).tap do |identifier|
16
- mkpath!(identifier)
9
+ path = Pathname.new(root_path)
17
10
 
18
- (@root + identifier).open('w:BINARY') do |file|
19
- yield file, identifier
20
- file.flush
21
- end
22
- end
11
+ @fog = Fog::Storage.new(provider: 'Local', local_root: path.parent)
12
+ @dir = path.basename.to_s
23
13
  end
24
14
 
25
- def get(identifier)
26
- (@root + identifier).open('r:BINARY')
15
+ def identifier
16
+ "file://#{[@fog.local_root, @dir].join('/')}"
27
17
  end
28
18
 
29
19
  private
30
20
 
31
- def calculate_identifier(suffix, seed)
32
- dirname, fname_seed = ['%Y-%m-%d', '%H%M%S.%L'].map {|f| seed.strftime(f) }
33
-
34
- basename = "%s_%05d%s" % [fname_seed, Process.pid, suffix]
35
-
36
- identifier = [dirname, basename].join('/')
37
- end
38
-
39
- def mkpath!(identifier)
40
- dir = identifier.split('/').first
41
- (@root + dir).mkpath
21
+ def directory
22
+ @fog.directories.get(@dir) || @fog.directories.create(key: @dir)
42
23
  end
43
24
  end
44
25
  end
@@ -0,0 +1,24 @@
1
+ require 'fog'
2
+
3
+ module Adhoq
4
+ module Storage
5
+ class S3 < FogStorage
6
+ def initialize(bucket, s3_options = {})
7
+ @bucket = bucket
8
+ @s3 = Fog::Storage.new({provider: 'AWS'}.merge(s3_options))
9
+ end
10
+
11
+ def identifier
12
+ "s3://#{@bucket}"
13
+ end
14
+
15
+ private
16
+
17
+ def directory
18
+ return @directory if @directory
19
+
20
+ @directory = @s3.directories.get(@bucket) || @s3.directories.create(key: @bucket, public: false)
21
+ end
22
+ end
23
+ end
24
+ end
data/lib/adhoq/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Adhoq
2
- VERSION = "0.0.2"
2
+ VERSION = "0.0.3"
3
3
  end
@@ -7,5 +7,18 @@ module Adhoq
7
7
 
8
8
  specify { expect(executor.execute).to eq Adhoq::Result.new(%w[answer], [[42]]) }
9
9
  end
10
+
11
+ describe '.select' do
12
+ specify 'Do not reflect write access' do
13
+ expect {
14
+ Executor.select(<<-INSERT_SQL.strip_heredoc)
15
+ INSERT INTO "adhoq_queries"
16
+ ("description", "name", "query", "updated_at", "created_at")
17
+ VALUES
18
+ ("description", "name", "SELECT 1", "NOW", "NOW")
19
+ INSERT_SQL
20
+ }.not_to change(Adhoq::Query, :count)
21
+ end
22
+ end
10
23
  end
11
24
  end
@@ -0,0 +1,37 @@
1
+ module Adhoq
2
+ RSpec.describe GlobalVariable, type: :model do
3
+ def reset_storage_config(*storage_config)
4
+ Adhoq.instance_variable_set('@current_storage', nil)
5
+ Adhoq.config.storage = storage_config
6
+ end
7
+
8
+ around(:each) do |example|
9
+ begin
10
+ original_config = Adhoq.config.storage
11
+ example.run
12
+ ensure
13
+ reset_storage_config(*original_config)
14
+ end
15
+ end
16
+
17
+ context 'config.storage = [:local_file, ....]' do
18
+ before do
19
+ reset_storage_config(:local_file, Rails.root + "/tmp/adhoq/#{Rails.env}")
20
+ end
21
+
22
+ specify do
23
+ expect(Adhoq.current_storage).to be_instance_of Adhoq::Storage::LocalFile
24
+ end
25
+ end
26
+
27
+ context 'config.storage = [:s3, ....]' do
28
+ before do
29
+ reset_storage_config(:s3, 'my-bucket-name', aws_access_key_id: 'key-id', aws_secret_access_key: 'secret', region: 'paris-01')
30
+ end
31
+
32
+ specify do
33
+ expect(Adhoq.current_storage).to be_instance_of Adhoq::Storage::S3
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,18 @@
1
+ module Adhoq
2
+ RSpec.describe Reporter::Xlsx, type: :model do
3
+ context 'create xlsx report' do
4
+ let(:report_data) do
5
+ Adhoq::Reporter.generate(Adhoq::AdhocExecution.new('xlsx', <<-SQL.strip_heredoc))
6
+ SELECT "hello" AS name ,"English greeting message" AS description
7
+ SQL
8
+ end
9
+
10
+ specify do
11
+ expect(report_data.read).to have_values_in_xlsx_sheet([
12
+ %w[name description],
13
+ ['hello', 'English greeting message']
14
+ ])
15
+ end
16
+ end
17
+ end
18
+ end
@@ -10,10 +10,20 @@ module Adhoq
10
10
  let(:storage) { Storage::LocalFile.new(tempdir) }
11
11
 
12
12
  let(:identifier) do
13
- storage.store('.txt') {|file, ident| file.puts 'Hello adhoq!' }
13
+ storage.store('.txt') { StringIO.new("Hello adhoq!\n") }
14
14
  end
15
15
 
16
- specify { expect(storage.get(identifier).read).to eq "Hello adhoq!\n" }
16
+ specify { expect(storage.get(identifier)).to eq "Hello adhoq!\n" }
17
+ end
18
+
19
+ describe Storage::S3, :fog_mock do
20
+ let(:storage) { Storage::S3.new('my-adhoq-bucket', aws_access_key_id: 'key_id', aws_secret_access_key: 'access_key') }
21
+
22
+ let(:identifier) do
23
+ storage.store('.txt') { StringIO.new("Hello adhoq!\n") }
24
+ end
25
+
26
+ specify { expect(storage.get(identifier)).to eq "Hello adhoq!\n" }
17
27
  end
18
28
  end
19
29
  end
data/spec/spec_helper.rb CHANGED
@@ -2,13 +2,18 @@ ENV['RAILS_ENV'] ||= 'test'
2
2
  require_relative 'dummy/config/environment'
3
3
 
4
4
  require 'rspec/rails'
5
- require 'factory_girl_rails'
6
5
 
6
+ require 'capybara/rspec'
7
+ require 'capybara/poltergeist'
8
+ require 'database_cleaner'
9
+ require 'factory_girl_rails'
7
10
  require 'pry-byebug'
8
11
 
9
12
  Rails.backtrace_cleaner.remove_silencers!
10
13
  Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f }
11
14
 
15
+ Capybara.default_driver = :poltergeist
16
+
12
17
  RSpec.configure do |config|
13
18
  config.expect_with :rspec do |expectations|
14
19
  expectations.include_chain_clauses_in_custom_matcher_descriptions = true
@@ -18,7 +23,7 @@ RSpec.configure do |config|
18
23
  mocks.verify_partial_doubles = true
19
24
  end
20
25
 
21
- config.use_transactional_fixtures = true
26
+ config.use_transactional_fixtures = false
22
27
  config.filter_run :focus
23
28
  config.run_all_when_everything_filtered = true
24
29
 
@@ -33,4 +38,26 @@ RSpec.configure do |config|
33
38
 
34
39
  config.include FactoryGirl::Syntax::Methods
35
40
  Kernel.srand config.seed
41
+
42
+ config.around(:each, :fog_mock) do |example|
43
+ begin
44
+ Fog.mock!
45
+ Fog::Mock.reset
46
+
47
+ example.run
48
+ ensure
49
+ Fog.unmock!
50
+ end
51
+ end
52
+
53
+ config.before(:suite) do
54
+ DatabaseCleaner.strategy = :truncation
55
+ DatabaseCleaner.clean_with :truncation
56
+ end
57
+
58
+ config.around(:each) do |example|
59
+ DatabaseCleaner.cleaning do
60
+ example.run
61
+ end
62
+ end
36
63
  end
@@ -0,0 +1,4 @@
1
+ if ENV['CODECLIMATE_REPO_TOKEN'] && RUBY_VERSION >= '2.1'
2
+ require 'codeclimate-test-reporter'
3
+ CodeClimate::TestReporter.start
4
+ end
@@ -0,0 +1,5 @@
1
+ module FeatureSpecHelper
2
+ def table_contant(table)
3
+ first(table).all('tr').map {|row| row.all('th, td').map(&:text) }
4
+ end
5
+ end
@@ -1,16 +1,22 @@
1
1
  require 'rspec/matchers'
2
2
  require 'simple_xlsx_reader'
3
3
 
4
- RSpec::Matchers.define :have_values_in_xlsx_sheet do |values|
4
+ RSpec::Matchers.define :have_values_in_xlsx_sheet do |expect_values|
5
5
  match do |data|
6
- expect(extract_values(data)).to eq values
6
+ @actual_values = extract_values(data)
7
+ expect(@actual_values).to eq expect_values
8
+ end
9
+
10
+
11
+ failure_message do
12
+ RSpec::Expectations.differ.diff_as_object(@actual_values, expect_values)
7
13
  end
8
14
 
9
15
  private
10
16
 
11
17
  def extract_values(data)
12
18
  Tempfile.open(%w[actual .xlsx], Dir.tmpdir, encoding: 'BINARY') do |f|
13
- f.write data.read
19
+ f.write data
14
20
  f.flush
15
21
 
16
22
  sheet = SimpleXlsxReader::Document.new(f.path).sheets.first
metadata CHANGED
@@ -1,31 +1,45 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: adhoq
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: 0.0.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kyosuke MOROHASHI
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-10-08 00:00:00.000000000 Z
11
+ date: 2014-10-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '3.2'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '3.2'
27
+ - !ruby/object:Gem::Dependency
28
+ name: axlsx
15
29
  requirement: !ruby/object:Gem::Requirement
16
30
  requirements:
17
31
  - - "~>"
18
32
  - !ruby/object:Gem::Version
19
- version: '4.0'
33
+ version: '2.0'
20
34
  type: :runtime
21
35
  prerelease: false
22
36
  version_requirements: !ruby/object:Gem::Requirement
23
37
  requirements:
24
38
  - - "~>"
25
39
  - !ruby/object:Gem::Version
26
- version: '4.0'
40
+ version: '2.0'
27
41
  - !ruby/object:Gem::Dependency
28
- name: axlsx
42
+ name: coffee-rails
29
43
  requirement: !ruby/object:Gem::Requirement
30
44
  requirements:
31
45
  - - ">="
@@ -39,7 +53,35 @@ dependencies:
39
53
  - !ruby/object:Gem::Version
40
54
  version: '0'
41
55
  - !ruby/object:Gem::Dependency
42
- name: coffee-rails
56
+ name: fog
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '1.23'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '1.23'
69
+ - !ruby/object:Gem::Dependency
70
+ name: font-awesome-sass
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: 4.2.0
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: 4.2.0
83
+ - !ruby/object:Gem::Dependency
84
+ name: jquery-rails
43
85
  requirement: !ruby/object:Gem::Requirement
44
86
  requirements:
45
87
  - - ">="
@@ -53,7 +95,7 @@ dependencies:
53
95
  - !ruby/object:Gem::Version
54
96
  version: '0'
55
97
  - !ruby/object:Gem::Dependency
56
- name: font-awesome-sass
98
+ name: sass-rails
57
99
  requirement: !ruby/object:Gem::Requirement
58
100
  requirements:
59
101
  - - ">="
@@ -67,7 +109,7 @@ dependencies:
67
109
  - !ruby/object:Gem::Version
68
110
  version: '0'
69
111
  - !ruby/object:Gem::Dependency
70
- name: jquery-rails
112
+ name: slim-rails
71
113
  requirement: !ruby/object:Gem::Requirement
72
114
  requirements:
73
115
  - - ">="
@@ -81,13 +123,27 @@ dependencies:
81
123
  - !ruby/object:Gem::Version
82
124
  version: '0'
83
125
  - !ruby/object:Gem::Dependency
84
- name: sass-rails
126
+ name: capybara
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - "~>"
130
+ - !ruby/object:Gem::Version
131
+ version: 2.4.3
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - "~>"
137
+ - !ruby/object:Gem::Version
138
+ version: 2.4.3
139
+ - !ruby/object:Gem::Dependency
140
+ name: database_cleaner
85
141
  requirement: !ruby/object:Gem::Requirement
86
142
  requirements:
87
143
  - - ">="
88
144
  - !ruby/object:Gem::Version
89
145
  version: '0'
90
- type: :runtime
146
+ type: :development
91
147
  prerelease: false
92
148
  version_requirements: !ruby/object:Gem::Requirement
93
149
  requirements:
@@ -95,13 +151,13 @@ dependencies:
95
151
  - !ruby/object:Gem::Version
96
152
  version: '0'
97
153
  - !ruby/object:Gem::Dependency
98
- name: slim-rails
154
+ name: factory_girl_rails
99
155
  requirement: !ruby/object:Gem::Requirement
100
156
  requirements:
101
157
  - - ">="
102
158
  - !ruby/object:Gem::Version
103
159
  version: '0'
104
- type: :runtime
160
+ type: :development
105
161
  prerelease: false
106
162
  version_requirements: !ruby/object:Gem::Requirement
107
163
  requirements:
@@ -109,7 +165,7 @@ dependencies:
109
165
  - !ruby/object:Gem::Version
110
166
  version: '0'
111
167
  - !ruby/object:Gem::Dependency
112
- name: factory_girl_rails
168
+ name: launchy
113
169
  requirement: !ruby/object:Gem::Requirement
114
170
  requirements:
115
171
  - - ">="
@@ -122,6 +178,20 @@ dependencies:
122
178
  - - ">="
123
179
  - !ruby/object:Gem::Version
124
180
  version: '0'
181
+ - !ruby/object:Gem::Dependency
182
+ name: poltergeist
183
+ requirement: !ruby/object:Gem::Requirement
184
+ requirements:
185
+ - - "~>"
186
+ - !ruby/object:Gem::Version
187
+ version: 1.5.1
188
+ type: :development
189
+ prerelease: false
190
+ version_requirements: !ruby/object:Gem::Requirement
191
+ requirements:
192
+ - - "~>"
193
+ - !ruby/object:Gem::Version
194
+ version: 1.5.1
125
195
  - !ruby/object:Gem::Dependency
126
196
  name: pry-byebug
127
197
  requirement: !ruby/object:Gem::Requirement
@@ -217,6 +287,7 @@ files:
217
287
  - app/assets/javascripts/adhoq/bootstrap/tab.js
218
288
  - app/assets/javascripts/adhoq/bootstrap/tooltip.js
219
289
  - app/assets/javascripts/adhoq/bootstrap/transition.js
290
+ - app/assets/javascripts/adhoq/current_tables.js.coffee
220
291
  - app/assets/javascripts/adhoq/previewer.js.coffee
221
292
  - app/assets/stylesheets/adhoq/_bootstrap-compass.scss
222
293
  - app/assets/stylesheets/adhoq/_bootstrap-mincer.scss
@@ -296,6 +367,7 @@ files:
296
367
  - app/assets/stylesheets/adhoq/bootstrap/mixins/_vendor-prefixes.scss
297
368
  - app/controllers/adhoq/application_controller.rb
298
369
  - app/controllers/adhoq/authorization_methods.rb
370
+ - app/controllers/adhoq/current_tables_controller.rb
299
371
  - app/controllers/adhoq/executions_controller.rb
300
372
  - app/controllers/adhoq/previews_controller.rb
301
373
  - app/controllers/adhoq/queries_controller.rb
@@ -306,6 +378,7 @@ files:
306
378
  - app/models/adhoq/time_based_orders.rb
307
379
  - app/views/adhoq/application/_global_nav.html.slim
308
380
  - app/views/adhoq/application/_sidebar_queries_index.html.slim
381
+ - app/views/adhoq/current_tables/index.html.slim
309
382
  - app/views/adhoq/previews/create.html.slim
310
383
  - app/views/adhoq/previews/statement_invalid.html.slim
311
384
  - app/views/adhoq/queries/_form.html.slim
@@ -320,6 +393,7 @@ files:
320
393
  - db/migrate/20141006014750_create_adhoq_executions.rb
321
394
  - db/migrate/20141007052308_create_adhoq_reports.rb
322
395
  - lib/adhoq.rb
396
+ - lib/adhoq/adhoc_execution.rb
323
397
  - lib/adhoq/configuration.rb
324
398
  - lib/adhoq/engine.rb
325
399
  - lib/adhoq/error.rb
@@ -329,16 +403,22 @@ files:
329
403
  - lib/adhoq/reporter/xlsx.rb
330
404
  - lib/adhoq/result.rb
331
405
  - lib/adhoq/storage.rb
406
+ - lib/adhoq/storage/fog_storage.rb
332
407
  - lib/adhoq/storage/local_file.rb
408
+ - lib/adhoq/storage/s3.rb
333
409
  - lib/adhoq/version.rb
334
410
  - lib/tasks/adhoq_tasks.rake
335
411
  - spec/adhoq/executor_spec.rb
412
+ - spec/adhoq/global_variable_spec.rb
413
+ - spec/adhoq/reporter/xlsx_spec.rb
336
414
  - spec/adhoq/storage_spec.rb
337
415
  - spec/factories/adhoq_queries.rb
338
416
  - spec/models/adhoq/execution_spec.rb
339
417
  - spec/models/adhoq/query_spec.rb
340
418
  - spec/models/adhoq/report_spec.rb
341
419
  - spec/spec_helper.rb
420
+ - spec/support/codeclimate_reporter.rb
421
+ - spec/support/feature_spec_helper.rb
342
422
  - spec/support/have_values_in_xlsx_sheet_matcher.rb
343
423
  homepage: https://github.com/esminc/adhoq
344
424
  licenses:
@@ -366,10 +446,14 @@ specification_version: 4
366
446
  summary: DB management console in the wild.
367
447
  test_files:
368
448
  - spec/adhoq/executor_spec.rb
449
+ - spec/adhoq/global_variable_spec.rb
450
+ - spec/adhoq/reporter/xlsx_spec.rb
369
451
  - spec/adhoq/storage_spec.rb
370
452
  - spec/factories/adhoq_queries.rb
371
453
  - spec/models/adhoq/execution_spec.rb
372
454
  - spec/models/adhoq/query_spec.rb
373
455
  - spec/models/adhoq/report_spec.rb
456
+ - spec/support/codeclimate_reporter.rb
457
+ - spec/support/feature_spec_helper.rb
374
458
  - spec/support/have_values_in_xlsx_sheet_matcher.rb
375
459
  - spec/spec_helper.rb