adhoq 0.0.5 → 0.0.6

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.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +6 -6
  3. data/app/assets/javascripts/adhoq/current_tables.js.coffee +17 -3
  4. data/app/assets/javascripts/adhoq/previewer.js.coffee +10 -1
  5. data/app/assets/stylesheets/adhoq/adhoq.css.sass +38 -8
  6. data/app/controllers/adhoq/application_controller.rb +2 -0
  7. data/app/controllers/adhoq/current_tables_controller.rb +4 -1
  8. data/app/controllers/adhoq/executions_controller.rb +24 -3
  9. data/app/controllers/adhoq/explains_controller.rb +1 -1
  10. data/app/controllers/adhoq/previews_controller.rb +1 -1
  11. data/app/controllers/adhoq/queries_controller.rb +1 -1
  12. data/app/decorators/adhoq/execution_decorator.rb +20 -0
  13. data/app/decorators/adhoq/query_decorator.rb +9 -0
  14. data/app/helpers/adhoq/application_helper.rb +12 -2
  15. data/app/jobs/adhoq/execute_job.rb +11 -0
  16. data/app/models/adhoq/execution.rb +4 -6
  17. data/app/models/adhoq/report.rb +8 -0
  18. data/app/views/adhoq/current_tables/index.html.slim +10 -8
  19. data/app/views/adhoq/queries/_current_tables_leftbar.html.slim +9 -0
  20. data/app/views/adhoq/queries/_execution.html.slim +10 -0
  21. data/app/views/adhoq/queries/_form.html.slim +49 -30
  22. data/app/views/adhoq/queries/_queries.html.slim +14 -0
  23. data/app/views/adhoq/queries/_query.html.slim +20 -24
  24. data/app/views/adhoq/queries/edit.html.slim +11 -2
  25. data/app/views/adhoq/queries/index.html.slim +11 -1
  26. data/app/views/adhoq/queries/new.html.slim +10 -2
  27. data/app/views/adhoq/queries/show.html.slim +11 -1
  28. data/app/views/layouts/adhoq/application.html.slim +1 -4
  29. data/db/migrate/20141003095645_create_adhoq_queries.rb +1 -1
  30. data/db/migrate/20141006014750_create_adhoq_executions.rb +1 -1
  31. data/db/migrate/20141007052308_create_adhoq_reports.rb +1 -1
  32. data/lib/adhoq.rb +1 -0
  33. data/lib/adhoq/configuration.rb +10 -0
  34. data/lib/adhoq/engine.rb +1 -0
  35. data/lib/adhoq/executor.rb +4 -28
  36. data/lib/adhoq/executor/connection_wrapper.rb +32 -0
  37. data/lib/adhoq/global_variable.rb +1 -0
  38. data/lib/adhoq/storage.rb +1 -0
  39. data/lib/adhoq/storage/fog_storage.rb +4 -0
  40. data/lib/adhoq/storage/on_the_fly.rb +32 -0
  41. data/lib/adhoq/storage/s3.rb +26 -2
  42. data/lib/adhoq/version.rb +1 -1
  43. data/spec/adhoq/executor/connection_wrapper_spec.rb +16 -0
  44. data/spec/adhoq/executor_spec.rb +0 -13
  45. data/spec/adhoq/storage_spec.rb +14 -0
  46. data/spec/models/adhoq/execution_spec.rb +21 -0
  47. data/spec/support/activejob_helper.rb +26 -0
  48. metadata +43 -4
  49. data/app/views/adhoq/application/_sidebar_queries_index.html.slim +0 -10
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 9e0a340afd6e9ff7f1d8ba89b6b85f206dcb7f74
4
- data.tar.gz: 39043bf804ec87a7183aaa331a1858ea6fc27d86
3
+ metadata.gz: e44fcb3a79e299b2a337652f8439d58cd4babe71
4
+ data.tar.gz: 5312feb5fee5890cae4b27f48c541a36b937ef0e
5
5
  SHA512:
6
- metadata.gz: fd569b25d51fff86895925ecf0774c01b6cf48f8a6b1ee533b230caf5f19ceaac35be0ea70ef9473fd8a9e82ba8cec21cd3bae58f1be30f1f0bc0ccdb805650e
7
- data.tar.gz: 2bd75bfaaaee3ca6ede743e59b334f198ec74a4d8a2fed174e949ca2c9abdd113d0d0810be83e9a143e55ba4d542db660a2a5d27f3ef9c6a0abfbbcf97705c1d
6
+ metadata.gz: 19546d4c89ab4cbb7da38b46e02a7cc4bfe091d1d00bbd3110d186a26586f929cdccdab8bf2bf12f322cfaf052297d102eefcfe3660c47012da952a0464baae9
7
+ data.tar.gz: 914e24aabd3a6a896c62e3c276642f8a6ba37d85d8d6f285363e5c1dbf6b4a3025f714f065989475b4042edfb8ab513ef2462974fb45214fa754c5f640ef345e
data/README.md CHANGED
@@ -1,4 +1,4 @@
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)
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) [![Test Coverage](https://codeclimate.com/github/esminc/adhoq/badges/coverage.svg)](https://codeclimate.com/github/esminc/adhoq/coverage)
2
2
  ====
3
3
 
4
4
  Rails engine to generate instant reports from adhoc SQL query.
@@ -7,16 +7,16 @@ Rails engine to generate instant reports from adhoc SQL query.
7
7
 
8
8
  ## Features
9
9
 
10
- - Export ad-hoc SQL result to .xlsx file
10
+ - Export ad-hoc SQL reports in some formats:
11
+ - .csv
12
+ - .json
13
+ - .xlsx
11
14
  - Persist generated report as local file or in AWS S3
12
15
  - Rails 4.x & 3.2 support
13
16
  - Nice administration console with rails engine
14
17
 
15
18
  ### Future planning
16
19
 
17
- - Export reports in some formats:
18
- - [ ] .csv
19
- - [ ] .json
20
20
  - [ ] Label data substitution
21
21
 
22
22
  ## Installation
@@ -56,7 +56,7 @@ Rails.application.routes.draw do
56
56
  end
57
57
  ```
58
58
 
59
- Edit initialization file in `config/initializer/adhoq.rb`
59
+ Edit initialization file in `config/initializers/adhoq.rb`
60
60
 
61
61
  ```ruby
62
62
  Adhoq.configure do |config|
@@ -1,4 +1,18 @@
1
- Adhoq.loadCurrentTableTabOnce = ($el)->
2
- pane = $("#{$el.attr('href')}:has(.loading)")
1
+ loadCurrentTableTabOnce = ($el)->
2
+ $el.load($el.find('a.loading').attr('href'))
3
+
4
+ Adhoq.toggleCurrentTables = (action, elements)->
5
+ loadCurrentTableTabOnce($('#current-tables'))
6
+
7
+ $main = $(elements.main)
8
+ $tables = $(elements.tables)
9
+
10
+ if action is 'show'
11
+ $main.addClass('col-md-6').removeClass('col-md-12')
12
+ $tables.addClass('col-md-6').show()
13
+ else
14
+ $main.addClass('col-md-12').removeClass('col-md-6')
15
+ $tables.addClass('col-md-6').hide()
16
+
17
+ true
3
18
 
4
- pane.load(pane.find('a.loading').attr('href'))
@@ -2,8 +2,10 @@ class Previewer
2
2
  constructor: (@el)->
3
3
 
4
4
  init: ->
5
+ @el.on 'adhoq:updatePreview', => @update()
6
+
5
7
  @el.on 'click', =>
6
- @update()
8
+ @el.trigger 'adhoq:updatePreview'
7
9
  false
8
10
 
9
11
  update: ->
@@ -23,3 +25,10 @@ class Previewer
23
25
 
24
26
  Adhoq.enablePreview = ($el)->
25
27
  (new Previewer($el)).init()
28
+
29
+ Adhoq.enablePreviewKeybordShortCut= ($textarea, previewSelector)->
30
+ $textarea.on 'keyup', (ev)->
31
+ if(ev.ctrlKey && ev.keyCode is 82)
32
+ $(previewSelector).trigger('adhoq:updatePreview')
33
+
34
+ false
@@ -21,22 +21,41 @@ $short-span: $font-size-base / 2
21
21
  margin: 0 10px
22
22
  font-size: $font-size-base
23
23
 
24
- #sidebar
25
- h2 a.new-query
26
- float: right
24
+ #queries
25
+ a.new-query
26
+ margin-bottom: $short-span
27
27
 
28
- ul.queries
28
+ ol.queries-index
29
29
  li.panel
30
30
  margin-bottom: $short-span
31
31
 
32
- .panel-heading h2
33
- margin: 0
34
- font-size: $font-size-base
32
+ .panel-heading
33
+ padding: $short-span $short-span * 2
34
+ h2
35
+ margin: 0
36
+ font-size: $font-size-base
35
37
 
36
38
  p.panel-body.description
37
39
  margin-bottom: 0
40
+ font-size: $font-size-base * 0.9
41
+
42
+ #the-query
43
+ .page-header
44
+ margin-top: 0
45
+ h1
46
+ margin-top: 0
47
+
48
+ small
49
+ font-size: $font-size-base
38
50
 
39
51
  form.query-form
52
+ margin-bottom: 30px
53
+
54
+ h1
55
+ font-size: $font-size-base
56
+ margin-top: $font-size-base / 2
57
+ margin-bottom: $font-size-base / 4
58
+
40
59
  textarea#query_query
41
60
  font-family: monospace
42
61
 
@@ -57,10 +76,17 @@ form.query-form
57
76
  padding-left: $short-span / 2
58
77
 
59
78
  #current-tables
79
+ display: none
60
80
  font-size: $font-size-base * 0.9
61
81
 
82
+ h3
83
+ font-size: $font-size-base
84
+ margin-top: $font-size-base / 2
85
+ margin-bottom: $font-size-base / 4
86
+
87
+ small
88
+ margin-left: $font-size-base
62
89
  table
63
- width: 800px
64
90
 
65
91
  caption
66
92
  text-align: left
@@ -95,7 +121,11 @@ form.query-form
95
121
 
96
122
  .tab-pane > h3
97
123
  font-size: $font-size-base * 1.5
124
+ margin-top: $font-size-base / 2
98
125
 
99
126
  small
100
127
  font-size: $font-size-base
101
128
  margin-left: $font-size-base
129
+
130
+ .js-preview-result table, .js-explain-result pre
131
+ font-family: monospace
@@ -1,5 +1,7 @@
1
1
  module Adhoq
2
2
  class ApplicationController < ::ApplicationController
3
+ layout 'adhoq/application'
4
+
3
5
  include Adhoq::AuthorizationMethods
4
6
  end
5
7
  end
@@ -3,8 +3,11 @@ module Adhoq
3
3
  before_filter :eager_load_models
4
4
 
5
5
  def index
6
+ hidden_model_names = Array(Adhoq.config.hidden_model_names)
7
+ hidden_model_names << 'ActiveRecord::SchemaMigration'
8
+
6
9
  @ar_classes = ActiveRecord::Base.subclasses.
7
- reject {|klass| klass.name == 'ActiveRecord::SchemaMigration' }.
10
+ reject {|klass| hidden_model_names.include?(klass.name) }.
8
11
  sort_by(&:name)
9
12
 
10
13
  render layout: false
@@ -7,19 +7,40 @@ module Adhoq
7
7
  end
8
8
 
9
9
  def create
10
+ async_execution? ? asynced_create : synced_create
11
+ end
12
+
13
+ private
14
+
15
+ def synced_create
10
16
  @execution = current_query.execute!(params[:execution][:report_format])
11
17
 
12
- redirect_to current_query
18
+ if @execution.report.on_the_fly?
19
+ respond_report(@execution.report)
20
+ else
21
+ redirect_to current_query
22
+ end
13
23
  end
14
24
 
15
- private
25
+ def asynced_create
26
+ Adhoq::ExecuteJob.perform_later(current_query, params[:execution][:report_format])
27
+ redirect_to current_query
28
+ end
16
29
 
17
30
  def current_query
18
31
  @query ||= Adhoq::Query.find(params[:query_id])
19
32
  end
20
33
 
21
34
  def respond_report(report)
22
- send_data report.data, type: report.mime_type, filename: report.name, disposition: 'attachment'
35
+ if Adhoq.current_storage.direct_download?
36
+ redirect_to report.data_url
37
+ else
38
+ send_data report.data, type: report.mime_type, filename: report.name, disposition: 'attachment'
39
+ end
40
+ end
41
+
42
+ def async_execution?
43
+ Adhoq.config.async_execution? && !Adhoq.current_storage.is_a?(Adhoq::Storage::OnTheFly)
23
44
  end
24
45
  end
25
46
  end
@@ -1,5 +1,5 @@
1
1
  module Adhoq
2
- class ExplainsController < ApplicationController
2
+ class ExplainsController < Adhoq::ApplicationController
3
3
  layout false
4
4
 
5
5
  def create
@@ -1,5 +1,5 @@
1
1
  module Adhoq
2
- class PreviewsController < ApplicationController
2
+ class PreviewsController < Adhoq::ApplicationController
3
3
  layout false
4
4
 
5
5
  def create
@@ -1,5 +1,5 @@
1
1
  module Adhoq
2
- class QueriesController < ApplicationController
2
+ class QueriesController < Adhoq::ApplicationController
3
3
  def index
4
4
  @queries = Adhoq::Query.recent_first
5
5
  end
@@ -0,0 +1,20 @@
1
+ module Adhoq::ExecutionDecorator
2
+ def status_label
3
+ content_tag :span, class: ["label", status_label_class] do
4
+ status
5
+ end
6
+ end
7
+
8
+ def status_label_class
9
+ case status
10
+ when "success"
11
+ "label-success"
12
+ when "failure"
13
+ "label-danger"
14
+ when "requested"
15
+ "label-default"
16
+ else
17
+ "label-default"
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,9 @@
1
+ require 'rouge'
2
+
3
+ module Adhoq::QueryDecorator
4
+ def query_with_highlight
5
+ formatter = Rouge::Formatters::HTML.new(css_class: 'highlight query')
6
+ lexer = Rouge::Lexers::SQL.new
7
+ formatter.format(lexer.lex(query))
8
+ end
9
+ end
@@ -16,9 +16,19 @@ module Adhoq
16
16
  if defined?(ActiveRecord::SchemaMigration)
17
17
  ActiveRecord::SchemaMigration.maximum(:version)
18
18
  else
19
- result = Adhoq::Executor.select("SELECT MAX(version) AS current_version FROM #{ActiveRecord::Migrator.schema_migrations_table_name}")
20
- result.rows.first
19
+ connection = Adhoq::Executor::ConnectionWrapper.new
20
+ result = connection.select("SELECT MAX(version) AS current_version FROM #{ActiveRecord::Migrator.schema_migrations_table_name}")
21
+ result.rows.first.first
21
22
  end
22
23
  end
24
+
25
+ # TODO extract into presenter
26
+ def query_friendly_name(query)
27
+ "Query: #{query.name}"
28
+ end
29
+
30
+ def table_order_key(ar_class)
31
+ ar_class.primary_key || ar_class.columns.first.name
32
+ end
23
33
  end
24
34
  end
@@ -0,0 +1,11 @@
1
+ if defined?(ActiveJob)
2
+ class Adhoq::ExecuteJob < ActiveJob::Base
3
+ queue_as do
4
+ Adhoq.config.job_queue_name.try(:to_sym) || :default
5
+ end
6
+
7
+ def perform(query, *args)
8
+ query.execute!(*args)
9
+ end
10
+ end
11
+ end
@@ -9,6 +9,9 @@ module Adhoq
9
9
 
10
10
  def generate_report!
11
11
  build_report.generate!
12
+ update_attributes(status: :success)
13
+ rescue
14
+ update_attributes(status: :failure)
12
15
  end
13
16
 
14
17
  def name
@@ -16,12 +19,7 @@ module Adhoq
16
19
  end
17
20
 
18
21
  def success?
19
- report.try(:available?)
20
- end
21
-
22
- # TODO go decorator or view model or so
23
- def status_label
24
- success? ? :success : :failure
22
+ report.try(:available?) || status == "success"
25
23
  end
26
24
  end
27
25
  end
@@ -12,6 +12,10 @@ module Adhoq
12
12
  save!
13
13
  end
14
14
 
15
+ def on_the_fly?
16
+ storage.start_with?(Adhoq::Storage::OnTheFly::PREFIX)
17
+ end
18
+
15
19
  def available?
16
20
  identifier.present? && (storage == Adhoq.current_storage.identifier)
17
21
  end
@@ -20,6 +24,10 @@ module Adhoq
20
24
  storage.get(identifier)
21
25
  end
22
26
 
27
+ def data_url(storage = Adhoq.current_storage)
28
+ storage.get_url(self)
29
+ end
30
+
23
31
  def mime_type
24
32
  Adhoq::Reporter.lookup(execution.report_format).mime_type
25
33
  end
@@ -1,10 +1,14 @@
1
1
  h3
2
- | Current database schema
2
+ i.fa.fa-database.fa-pad-r
3
+ | Current tables
3
4
  small= "Version #{schema_version}"
5
+ .pull-right
6
+ button.close[data-trigger="toggleCurrentTables" role='close']
7
+ span[aria-hidden=true] &times;
4
8
 
5
9
  ul.list-unstyled.tables
6
10
  - @ar_classes.each do |ar_class|
7
- - first_record = ar_class.unscoped.order(:id).first
11
+ - first_record = ar_class.unscoped.order(table_order_key(ar_class)).first
8
12
 
9
13
  li.ar_class data-table-name=ar_class.table_name
10
14
  table.table.table-striped.table-hover.table-bordered
@@ -14,12 +18,11 @@ ul.list-unstyled.tables
14
18
  thead
15
19
  tr
16
20
  th.col-sm-1.pk PK
17
- th.col-sm-2.name Name
18
- th.col-sm-1.type Type
21
+ th.col-sm-3.name Name
22
+ th.col-sm-2.type Type
19
23
  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
24
+ th.col-sm-2.limit Limit
25
+ th.col-sm-3.default Default
23
26
  tbody
24
27
  - ar_class.columns.each do |column|
25
28
  tr
@@ -29,4 +32,3 @@ ul.list-unstyled.tables
29
32
  td.null.icon= column.null ? '' : icon_fa('check')
30
33
  td.limit.number= column.limit
31
34
  td= column.default
32
- td.monospace= first_record.try {|r| truncate(r.read_attribute_before_type_cast(column.name).to_s, length: 50) }
@@ -0,0 +1,9 @@
1
+ #current-tables
2
+ a.loading[href=current_tables_path]
3
+
4
+ javascript:
5
+ $(function() {
6
+ $(document).on('click', '[data-trigger="toggleCurrentTables"]', function($ev) {
7
+ Adhoq.toggleCurrentTables($($ev.target).attr('role'), {main: '#main', tables: '#current-tables'});
8
+ })
9
+ });
@@ -0,0 +1,10 @@
1
+ tr[exec]
2
+ td.wip
3
+ td.created_at= exec.created_at.localtime.iso8601
4
+ td.status
5
+ = exec.status_label
6
+ td.report
7
+ - if exec.success?
8
+ = link_to(query_execution_path(query, exec, format: exec.report_format), class: 'btn btn-sm btn-default') do
9
+ i.fa.fa-download.fa-pad-r
10
+ = exec.report_format
@@ -1,28 +1,54 @@
1
- = form_for query, html: {class: 'form form-horizontal query-form', role: 'form'} do |f|
1
+ = form_for query, html: {class: 'form query-form', role: 'form'} do |f|
2
2
  .page-header
3
3
  h1
4
- = title
5
- .actions.btn-in-header
6
- button.btn.btn-primary
7
- i.fa.fa-floppy-o.fa-pad-r
8
- | Save
9
- - if f.object.persisted?
10
- = link_to 'Cancel', f.object, class: 'btn btn-default'
4
+ = f.label :query, title, class: 'control-label'
5
+ .pull-right
6
+ a.btn.btn-default.btn-sm[href='#' data-trigger='toggleCurrentTables' role='show']
7
+ i.fa.fa-database.fa-pad-r
8
+ | Show tables
11
9
 
12
10
  .form-group
13
- = f.label :name, class: 'col-sm-2 control-label'
14
- .col-sm-8
15
- = f.text_field :name, class: 'form-control', required: true
11
+ = f.text_area :query, class: 'form-control', rows: 15, required: true
16
12
 
17
- .form-group
18
- = f.label :description, class: 'col-sm-2 control-label'
19
- .col-sm-8
20
- = f.text_area :description, class: 'form-control', required: true
13
+ .modal.fade#nameAndDesc[role='dialog']
14
+ .modal-dialog
15
+ .modal-content
16
+ .modal-header
17
+ button.close[data-dismiss='modal' aria-label='Close']
18
+ button.close[type='button' data-dismiss='modal' aria-label='Close']
19
+ span[aria-hidden='true'] &times;
21
20
 
22
- .form-group
23
- = f.label :query, class: 'col-sm-2 control-label'
24
- .col-sm-8
25
- = f.text_area :query, class: 'form-control', rows: 10, required: true
21
+ h4 Add name and description to query
22
+ .modal-body
23
+ .form-horizontal
24
+ .form-group
25
+ = f.label :name, class: 'control-label col-sm-2'
26
+ .col-sm-8
27
+ = f.text_field :name, class: 'form-control', required: true
28
+
29
+ .form-group
30
+ = f.label :description, class: 'control-label col-sm-2'
31
+ .col-sm-8
32
+ = f.text_area :description, class: 'form-control', required: true
33
+ .modal-footer
34
+ button.btn.btn-primary.center-block
35
+ i.fa.fa-floppy-o.fa-pad-r
36
+ | Save
37
+
38
+ .actions
39
+ - if query.persisted?
40
+ = link_to query do
41
+ i.fa.fa-arrow-left.fa-pad-r
42
+ | Cancel
43
+ - else
44
+ = link_to :queries do
45
+ i.fa.fa-arrow-left.fa-pad-r
46
+ | Back to Index
47
+
48
+ .pull-right
49
+ = link_to '#nameAndDesc', class: 'btn btn-default btn-sm', data: {toggle: 'modal', target: '#nameAndDesc'} do
50
+ i.fa.fa-floppy-o.fa-pad-r
51
+ | Save as...
26
52
 
27
53
  ul.nav.nav-tabs[role='tablist']
28
54
  li.active
@@ -33,19 +59,15 @@ ul.nav.nav-tabs[role='tablist']
33
59
  a[role='tab' data-toggle='tab' href='#explain' ]
34
60
  i.fa.fa-info.fa-pad-r
35
61
  | Explain
36
- li
37
- a[role='tab' data-toggle='tab' href='#current-tables']
38
- i.fa.fa-database.fa-pad-r
39
- | Tables
40
62
 
41
- .tab-content
63
+ #previews.tab-content
42
64
  #preview.tab-pane.active
43
65
  h3
44
66
  | Query preview
45
67
  small
46
68
  = link_to preview_path, class: 'js-preview-button', data: {source: '#query_query', result: '.js-preview-result', remote: true, method: 'POST'} do
47
69
  i.fa.fa-refresh.fa-pad-r[data-title='Refresh preview']
48
- | Reflesh
70
+ | Refresh
49
71
 
50
72
  .js-preview-result
51
73
  .alert.alert-info Preview is shown here
@@ -56,19 +78,16 @@ ul.nav.nav-tabs[role='tablist']
56
78
  small
57
79
  = link_to explain_path, class: 'js-explain-button', data: {source: '#query_query', result: '.js-explain-result', remote: true, method: 'POST'} do
58
80
  i.fa.fa-refresh.fa-pad-r[data-title='Refresh explain']
59
- | Reflesh
81
+ | Refresh
60
82
 
61
83
  .js-explain-result
62
84
  .alert.alert-info Explain result is shown here
63
85
 
64
- #current-tables.tab-pane
65
- a.loading[href=current_tables_path]
66
-
67
86
  javascript:
68
87
  $(function() {
69
88
  Adhoq.enablePreview($('#preview a.js-preview-button'));
70
89
  Adhoq.enablePreview($('#explain a.js-explain-button'));
71
90
 
72
- $('a[data-toggle="tab"]').on('show.bs.tab', function(ev) { Adhoq.loadCurrentTableTabOnce($(ev.target)) });
91
+ Adhoq.enablePreviewKeybordShortCut($('#query_query'), '#previews .tab-pane.active a:has(".fa-refresh")')
73
92
  });
74
93
 
@@ -0,0 +1,14 @@
1
+ - highlight ||= nil
2
+ section
3
+ = link_to :root, class: 'btn btn-default new-query btn-sm center-block' do
4
+ i.fa.fa-plus-square.fa-pad-r
5
+ | New query
6
+
7
+ ol.queries-index.list-unstyled
8
+ - queries.each do |query|
9
+ - css = (query == highlight) ? 'panel-success' : 'panel-default'
10
+ li.panel[query, class= css]
11
+ .panel-heading
12
+ h2= link_to query.name, query_path(query)
13
+ p.panel-body.description= query.description
14
+
@@ -2,13 +2,18 @@ section.query
2
2
  .page-header
3
3
  h1
4
4
  = query.name
5
+ .pull-right
6
+ = link_to [:edit, query], class: 'btn btn-default btn-sm' do
7
+ i.fa.fa-pencil.fa-pad-r
8
+ | Edit
9
+ .clearfix
5
10
  small= "Updated at #{l(query.updated_at, format: :short)}"
6
- = link_to [:edit, query], class: 'btn btn-default' do
7
- i.fa.fa-pencil.fa-pad-r
8
- | Edit
9
11
  p.description= query.description
10
12
 
11
- pre.query= query.query
13
+ css:
14
+ #{Rouge::Themes::Github.render(scope: '.highlight')}
15
+
16
+ = raw query.query_with_highlight
12
17
 
13
18
  section.new-execution
14
19
  h2 Create report
@@ -21,23 +26,14 @@ section.query
21
26
 
22
27
  section.past-executions
23
28
  h2 Reports
24
- .col-md-10
25
- table.executions.table.table-striped.table-hover
26
- thead
27
- tr
28
- th.wip &nbsp;
29
- th.created_at= human(Adhoq::Execution, :created_at)
30
- th.status= human(Adhoq::Execution, :status)
31
- th.report
32
- tbody
33
- - query.executions.recent_first.each do |exec|
34
- tr[exec]
35
- td.wip
36
- td.created_at= exec.created_at.localtime.iso8601
37
- td.status
38
- span.label[class=(exec.success? ? 'label-success' : 'label-danger')]= exec.status_label
39
- td.report
40
- - if exec.success?
41
- = link_to(adhoq.query_execution_path(query, exec, format: exec.report_format), class: 'btn btn-sm btn-default') do
42
- i.fa.fa-download.fa-pad-r
43
- = exec.report_format
29
+ table.executions.table.table-striped.table-hover
30
+ thead
31
+ tr
32
+ th.wip &nbsp;
33
+ th.created_at= human(Adhoq::Execution, :created_at)
34
+ th.status= human(Adhoq::Execution, :status)
35
+ th.report
36
+ tbody
37
+ - query.executions.recent_first.each do |exec|
38
+ - next if exec.report.try(:on_the_fly?)
39
+ = render 'execution', query: query, exec: exec
@@ -1,2 +1,11 @@
1
- section.edit-query
2
- = render 'form', query: @query, title: 'Edit query'
1
+ .col-md-12
2
+ ol.breadcrumb
3
+ li= link_to 'Index', :queries
4
+ li= link_to query_friendly_name(@query), @query
5
+ li.active Edit
6
+
7
+ #main.col-md-12
8
+ section.edit-query
9
+ = render 'form', query: @query, title: "Edit query > #{query_friendly_name(@query)}"
10
+
11
+ = render 'current_tables_leftbar'
@@ -1 +1,11 @@
1
- = render 'query', query: @queries.first
1
+ .col-md-12
2
+ ol.breadcrumb
3
+ li.active Index
4
+
5
+ #queries.col-md-3
6
+ section.queries
7
+ = render 'queries', queries: @queries
8
+
9
+ #the-query.col-md-9
10
+ - if first_query = @queries.first
11
+ = render 'query', query: first_query
@@ -1,2 +1,10 @@
1
- section.new-query
2
- = render 'form', query: @query, title: 'New query'
1
+ .col-md-12
2
+ ol.breadcrumb
3
+ li= link_to 'Index', :queries
4
+ li.active New query
5
+
6
+ #main.col-md-12
7
+ section.new-query
8
+ = render 'form', query: @query, title: 'New query'
9
+
10
+ = render 'current_tables_leftbar'
@@ -1 +1,11 @@
1
- = render 'query', query: @query
1
+ .col-md-12
2
+ ol.breadcrumb
3
+ li= link_to 'Index', :queries
4
+ li.active= query_friendly_name(@query)
5
+
6
+ #queries.col-md-3
7
+ section.queries
8
+ = render 'queries', queries: Adhoq::Query.recent_first, highlight: @query
9
+
10
+ #the-query.col-md-9
11
+ = render 'query', query: @query
@@ -11,8 +11,5 @@ html
11
11
  = render 'global_nav'
12
12
 
13
13
  #contents.row
14
- #sidebar.col-md-3
15
- = render 'sidebar_queries_index'
16
-
17
- #main.col-md-9= yield
14
+ = yield
18
15
 
@@ -5,7 +5,7 @@ class CreateAdhoqQueries < ActiveRecord::Migration
5
5
  t.string :description
6
6
  t.text :query
7
7
 
8
- t.timestamps
8
+ t.timestamps null: false
9
9
  end
10
10
  end
11
11
  end
@@ -7,7 +7,7 @@ class CreateAdhoqExecutions < ActiveRecord::Migration
7
7
  t.string :status, null: false, default: 'requested'
8
8
  t.text :log
9
9
 
10
- t.timestamps
10
+ t.timestamps null: false
11
11
  end
12
12
  end
13
13
  end
@@ -6,7 +6,7 @@ class CreateAdhoqReports < ActiveRecord::Migration
6
6
  t.time :generated_at, null: false
7
7
  t.string :storage, null: false
8
8
 
9
- t.timestamps
9
+ t.timestamps null: false
10
10
  end
11
11
  end
12
12
  end
data/lib/adhoq.rb CHANGED
@@ -14,5 +14,6 @@ module Adhoq
14
14
 
15
15
  configure do |config|
16
16
  config.authorization = proc { true }
17
+ config.database_connection = proc { ActiveRecord::Base.connection }
17
18
  end
18
19
  end
@@ -10,6 +10,12 @@ module Adhoq
10
10
 
11
11
  config_accessor :current_user
12
12
 
13
+ config_accessor :database_connection
14
+ config_accessor :hidden_model_names
15
+
16
+ config_accessor :async_execution
17
+ config_accessor :job_queue_name
18
+
13
19
  def callablize(name)
14
20
  if (c = config[name]).respond_to?(:call)
15
21
  c
@@ -17,5 +23,9 @@ module Adhoq
17
23
  c.to_proc
18
24
  end
19
25
  end
26
+
27
+ def async_execution?
28
+ defined?(ActiveJob) && Adhoq.config.async_execution
29
+ end
20
30
  end
21
31
  end
data/lib/adhoq/engine.rb CHANGED
@@ -2,6 +2,7 @@
2
2
  require 'font-awesome-sass'
3
3
  require 'jquery-rails'
4
4
  require 'slim-rails'
5
+ require 'active_decorator'
5
6
 
6
7
  module Adhoq
7
8
  class Engine < ::Rails::Engine
@@ -1,42 +1,18 @@
1
1
  module Adhoq
2
2
  class Executor
3
- class << self
4
- def select(query)
5
- with_sandbox do
6
- current_connection.exec_query(query)
7
- end
8
- end
9
-
10
- def explain(query)
11
- with_sandbox do
12
- current_connection.explain(query)
13
- end
14
- end
15
-
16
- def current_connection
17
- ActiveRecord::Base.connection
18
- end
19
-
20
- def with_sandbox
21
- result = nil
22
- ActiveRecord::Base.transaction do
23
- result = yield
24
- raise ActiveRecord::Rollback
25
- end
26
- result
27
- end
28
- end
3
+ autoload 'ConnectionWrapper', 'adhoq/executor/connection_wrapper'
29
4
 
30
5
  def initialize(query)
6
+ @connection = ConnectionWrapper.new
31
7
  @query = query
32
8
  end
33
9
 
34
10
  def execute
35
- wrap_result(self.class.select(@query))
11
+ wrap_result(@connection.select(@query))
36
12
  end
37
13
 
38
14
  def explain
39
- self.class.explain(@query)
15
+ @connection.explain(@query)
40
16
  end
41
17
 
42
18
  private
@@ -0,0 +1,32 @@
1
+ module Adhoq
2
+ class Executor
3
+ class ConnectionWrapper
4
+ attr_reader :connection
5
+
6
+ def initialize
7
+ @connection = Adhoq.config.callablize(:database_connection).call
8
+ end
9
+
10
+ def select(query)
11
+ with_sandbox do
12
+ connection.exec_query(query)
13
+ end
14
+ end
15
+
16
+ def explain(query)
17
+ with_sandbox do
18
+ connection.explain(query)
19
+ end
20
+ end
21
+
22
+ def with_sandbox
23
+ result = nil
24
+ connection.transaction do
25
+ result = yield
26
+ raise ActiveRecord::Rollback
27
+ end
28
+ result
29
+ end
30
+ end
31
+ end
32
+ end
@@ -27,6 +27,7 @@ module Adhoq
27
27
  case type
28
28
  when :local_file then Adhoq::Storage::LocalFile
29
29
  when :s3 then Adhoq::Storage::S3
30
+ when :on_the_fly then Adhoq::Storage::OnTheFly
30
31
  else
31
32
  raise NotImplementedError
32
33
  end
data/lib/adhoq/storage.rb CHANGED
@@ -3,6 +3,7 @@ module Adhoq
3
3
  autoload 'FogStorage', 'adhoq/storage/fog_storage'
4
4
  autoload 'LocalFile', 'adhoq/storage/local_file'
5
5
  autoload 'S3', 'adhoq/storage/s3'
6
+ autoload 'OnTheFly', 'adhoq/storage/on_the_fly'
6
7
 
7
8
  def with_new_identifier(suffix = nil, seed = Time.now)
8
9
  dirname, fname_seed = ['%Y-%m-%d', '%H%M%S.%L'].map {|f| seed.strftime(f) }
@@ -10,6 +10,10 @@ module Adhoq
10
10
  end
11
11
  end
12
12
 
13
+ def direct_download?
14
+ false
15
+ end
16
+
13
17
  def get(identifier)
14
18
  get_raw(identifier).body
15
19
  end
@@ -0,0 +1,32 @@
1
+ module Adhoq
2
+ module Storage
3
+ class OnTheFly
4
+ PREFIX = 'memory://adhoq-on-the-fly'
5
+
6
+ attr_reader :identifier, :reports
7
+
8
+ def initialize(id_base = Process.pid)
9
+ @identifier = "#{PREFIX}-#{id_base}"
10
+ @reports = {}
11
+ end
12
+
13
+ def store(suffix = nil, seed = Time.now, &block)
14
+ Adhoq::Storage.with_new_identifier(suffix, seed) do |identifier|
15
+ @reports[identifier] = yield.tap(&:rewind)
16
+ end
17
+ end
18
+
19
+ def direct_download?
20
+ false
21
+ end
22
+
23
+ def get(identifier)
24
+ if item = @reports.delete(identifier)
25
+ item.read.tap { item.close }
26
+ else
27
+ nil
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -4,14 +4,27 @@ module Adhoq
4
4
  module Storage
5
5
  class S3 < FogStorage
6
6
  def initialize(bucket, s3_options = {})
7
- @bucket = bucket
8
- @s3 = Fog::Storage.new({provider: 'AWS'}.merge(s3_options))
7
+ @bucket = bucket
8
+ @direct_download = s3_options.delete(:direct_download)
9
+ @direct_download_options = s3_options.delete(:direct_download_options) || default_direct_download_options
10
+ @s3 = Fog::Storage.new({provider: 'AWS'}.merge(s3_options))
11
+ end
12
+
13
+ def direct_download?
14
+ @direct_download
9
15
  end
10
16
 
11
17
  def identifier
12
18
  "s3://#{@bucket}"
13
19
  end
14
20
 
21
+ def get_url(report)
22
+ get_raw(report.identifier).url(
23
+ 1.minutes.from_now.to_i,
24
+ @direct_download_options.call(report)
25
+ )
26
+ end
27
+
15
28
  private
16
29
 
17
30
  def directory
@@ -19,6 +32,17 @@ module Adhoq
19
32
 
20
33
  @directory = @s3.directories.get(@bucket) || @s3.directories.create(key: @bucket, public: false)
21
34
  end
35
+
36
+ def default_direct_download_options
37
+ proc do |report|
38
+ {
39
+ query: {
40
+ 'response-content-disposition' => "attachment; filename*=UTF-8''#{URI.encode_www_form_component(report.name)}",
41
+ 'response-content-type' => report.mime_type,
42
+ }
43
+ }
44
+ end
45
+ end
22
46
  end
23
47
  end
24
48
  end
data/lib/adhoq/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Adhoq
2
- VERSION = "0.0.5"
2
+ VERSION = "0.0.6"
3
3
  end
@@ -0,0 +1,16 @@
1
+ module Adhoq
2
+ RSpec.describe Executor::ConnectionWrapper, type: :model do
3
+ describe '.select' do
4
+ specify 'Do not reflect write access' do
5
+ expect {
6
+ Executor::ConnectionWrapper.new.select(<<-INSERT_SQL.strip_heredoc)
7
+ INSERT INTO "adhoq_queries"
8
+ ("description", "name", "query", "updated_at", "created_at")
9
+ VALUES
10
+ ("description", "name", "SELECT 1", "NOW", "NOW")
11
+ INSERT_SQL
12
+ }.not_to change(Adhoq::Query, :count)
13
+ end
14
+ end
15
+ end
16
+ end
@@ -7,18 +7,5 @@ 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
23
10
  end
24
11
  end
@@ -25,5 +25,19 @@ module Adhoq
25
25
 
26
26
  specify { expect(storage.get(identifier)).to eq "Hello adhoq!\n" }
27
27
  end
28
+
29
+ describe Storage::OnTheFly do
30
+ let(:storage) { Storage::OnTheFly.new }
31
+
32
+ let!(:identifier) do
33
+ storage.store('.txt') { StringIO.new("Hello adhoq!\n") }
34
+ end
35
+
36
+ specify { expect(storage.get(identifier)).to eq "Hello adhoq!\n" }
37
+
38
+ specify do
39
+ expect { storage.get(identifier) }.to change { storage.reports.size }.by(-1)
40
+ end
41
+ end
28
42
  end
29
43
  end
@@ -1,4 +1,25 @@
1
1
  module Adhoq
2
2
  RSpec.describe Execution, :type => :model do
3
+ before do
4
+ storage = Adhoq::Storage::OnTheFly.new
5
+ allow(Adhoq).to receive(:current_storage) { storage }
6
+ end
7
+
8
+ let(:execution) do
9
+ query = create(:adhoq_query, query: 'SELECT name, description FROM adhoq_queries')
10
+ query.execute!('xlsx')
11
+ end
12
+
13
+ specify { expect(execution.report).to be_on_the_fly }
14
+
15
+ specify 'can get report only on execution' do
16
+ expect(execution.report.data).to have_values_in_xlsx_sheet([
17
+ ["name", "description"],
18
+ ["A query", "Simple simple SELECT"]
19
+ ])
20
+
21
+ # Accessable only once
22
+ expect(execution.report.data).to be_nil
23
+ end
3
24
  end
4
25
  end
@@ -0,0 +1,26 @@
1
+ RSpec.configure do |config|
2
+ config.around(:each, async_execution: true) do |ex|
3
+ current_async_execution = Adhoq.config.async_execution
4
+
5
+ Adhoq.config.async_execution = true
6
+
7
+ ex.call
8
+
9
+ Adhoq.config.async_execution = current_async_execution
10
+ end
11
+
12
+ config.around(:each, active_job_test_adapter: true) do |ex|
13
+ current_active_job_queue_adapter = Adhoq::Engine.config.active_job.queue_adapter
14
+ current_execute_job_queue_adapter = Adhoq::ExecuteJob.queue_adapter
15
+
16
+ Adhoq::Engine.config.active_job.queue_adapter = :test
17
+ Adhoq::ExecuteJob.queue_adapter = ActiveJob::QueueAdapters::TestAdapter.new
18
+ Adhoq::ExecuteJob.queue_adapter.perform_enqueued_jobs = true
19
+
20
+ ex.call
21
+
22
+ Adhoq::ExecuteJob.queue_adapter.performed_jobs.clear
23
+ Adhoq::Engine.config.active_job.queue_adapter = current_active_job_queue_adapter
24
+ Adhoq::ExecuteJob.queue_adapter = current_execute_job_queue_adapter
25
+ end
26
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: adhoq
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.5
4
+ version: 0.0.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kyosuke MOROHASHI
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-06-08 00:00:00.000000000 Z
11
+ date: 2015-10-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -122,6 +122,34 @@ dependencies:
122
122
  - - ">="
123
123
  - !ruby/object:Gem::Version
124
124
  version: '0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: active_decorator
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ type: :runtime
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
139
+ - !ruby/object:Gem::Dependency
140
+ name: rouge
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - ">="
144
+ - !ruby/object:Gem::Version
145
+ version: '0'
146
+ type: :runtime
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - ">="
151
+ - !ruby/object:Gem::Version
152
+ version: '0'
125
153
  - !ruby/object:Gem::Dependency
126
154
  name: capybara
127
155
  requirement: !ruby/object:Gem::Requirement
@@ -372,19 +400,24 @@ files:
372
400
  - app/controllers/adhoq/explains_controller.rb
373
401
  - app/controllers/adhoq/previews_controller.rb
374
402
  - app/controllers/adhoq/queries_controller.rb
403
+ - app/decorators/adhoq/execution_decorator.rb
404
+ - app/decorators/adhoq/query_decorator.rb
375
405
  - app/helpers/adhoq/application_helper.rb
406
+ - app/jobs/adhoq/execute_job.rb
376
407
  - app/models/adhoq/execution.rb
377
408
  - app/models/adhoq/query.rb
378
409
  - app/models/adhoq/report.rb
379
410
  - app/models/adhoq/time_based_orders.rb
380
411
  - app/views/adhoq/application/_global_nav.html.slim
381
- - app/views/adhoq/application/_sidebar_queries_index.html.slim
382
412
  - app/views/adhoq/current_tables/index.html.slim
383
413
  - app/views/adhoq/explains/create.html.slim
384
414
  - app/views/adhoq/explains/statement_invalid.html.slim
385
415
  - app/views/adhoq/previews/create.html.slim
386
416
  - app/views/adhoq/previews/statement_invalid.html.slim
417
+ - app/views/adhoq/queries/_current_tables_leftbar.html.slim
418
+ - app/views/adhoq/queries/_execution.html.slim
387
419
  - app/views/adhoq/queries/_form.html.slim
420
+ - app/views/adhoq/queries/_queries.html.slim
388
421
  - app/views/adhoq/queries/_query.html.slim
389
422
  - app/views/adhoq/queries/edit.html.slim
390
423
  - app/views/adhoq/queries/index.html.slim
@@ -401,6 +434,7 @@ files:
401
434
  - lib/adhoq/engine.rb
402
435
  - lib/adhoq/error.rb
403
436
  - lib/adhoq/executor.rb
437
+ - lib/adhoq/executor/connection_wrapper.rb
404
438
  - lib/adhoq/global_variable.rb
405
439
  - lib/adhoq/reporter.rb
406
440
  - lib/adhoq/reporter/csv.rb
@@ -410,9 +444,11 @@ files:
410
444
  - lib/adhoq/storage.rb
411
445
  - lib/adhoq/storage/fog_storage.rb
412
446
  - lib/adhoq/storage/local_file.rb
447
+ - lib/adhoq/storage/on_the_fly.rb
413
448
  - lib/adhoq/storage/s3.rb
414
449
  - lib/adhoq/version.rb
415
450
  - lib/tasks/adhoq_tasks.rake
451
+ - spec/adhoq/executor/connection_wrapper_spec.rb
416
452
  - spec/adhoq/executor_spec.rb
417
453
  - spec/adhoq/global_variable_spec.rb
418
454
  - spec/adhoq/reporter/csv_spec.rb
@@ -424,6 +460,7 @@ files:
424
460
  - spec/models/adhoq/query_spec.rb
425
461
  - spec/models/adhoq/report_spec.rb
426
462
  - spec/spec_helper.rb
463
+ - spec/support/activejob_helper.rb
427
464
  - spec/support/codeclimate_reporter.rb
428
465
  - spec/support/feature_spec_helper.rb
429
466
  - spec/support/have_values_in_xlsx_sheet_matcher.rb
@@ -447,11 +484,12 @@ required_rubygems_version: !ruby/object:Gem::Requirement
447
484
  version: '0'
448
485
  requirements: []
449
486
  rubyforge_project:
450
- rubygems_version: 2.2.2
487
+ rubygems_version: 2.4.5
451
488
  signing_key:
452
489
  specification_version: 4
453
490
  summary: DB management console in the wild.
454
491
  test_files:
492
+ - spec/adhoq/executor/connection_wrapper_spec.rb
455
493
  - spec/adhoq/executor_spec.rb
456
494
  - spec/adhoq/global_variable_spec.rb
457
495
  - spec/adhoq/reporter/csv_spec.rb
@@ -462,6 +500,7 @@ test_files:
462
500
  - spec/models/adhoq/execution_spec.rb
463
501
  - spec/models/adhoq/query_spec.rb
464
502
  - spec/models/adhoq/report_spec.rb
503
+ - spec/support/activejob_helper.rb
465
504
  - spec/support/codeclimate_reporter.rb
466
505
  - spec/support/feature_spec_helper.rb
467
506
  - spec/support/have_values_in_xlsx_sheet_matcher.rb
@@ -1,10 +0,0 @@
1
- h2
2
- | Queries
3
- = link_to 'Create new', :root, class: 'btn btn-primary new-query'
4
- ul.queries.list-unstyled
5
- - Adhoq::Query.recent_first.each do |query|
6
- li.panel.panel-default[query]
7
- .panel-heading
8
- h2= link_to query.name, query_path(query)
9
- p.panel-body.description= query.description
10
-