sql-jarvis 2.1.1 → 2.1.2

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.
@@ -1,6 +1,7 @@
1
1
  module Blazer
2
2
  class Query < Record
3
3
  serialize :assignee_ids, Array
4
+ serialize :team_ids, Array
4
5
 
5
6
  belongs_to :creator, Blazer::BELONGS_TO_OPTIONAL.merge(class_name: Blazer.user_class.to_s) if Blazer.user_class
6
7
  has_many :checks, dependent: :destroy
@@ -8,6 +9,7 @@ module Blazer
8
9
  has_many :dashboards, through: :dashboard_queries
9
10
  has_many :audits
10
11
 
12
+ before_validation :statement_format
11
13
  validates :statement, presence: true
12
14
 
13
15
  scope :named, -> { where("blazer_queries.name <> ''") }
@@ -42,5 +44,12 @@ module Blazer
42
44
  def variables
43
45
  Blazer.extract_vars(statement)
44
46
  end
47
+
48
+ private
49
+
50
+ def statement_format
51
+ self.statement.gsub!(/\n/, '')
52
+ end
53
+
45
54
  end
46
55
  end
@@ -3,10 +3,8 @@
3
3
  #{blazer_js_var "timeZone", Blazer.time_zone.tzinfo.name}
4
4
  var now = moment.tz(timeZone)
5
5
  var format = "YYYY-MM-DD"
6
+ function toDate(time) { return moment.tz(time.format(format), timeZone) }
6
7
 
7
- function toDate(time) {
8
- return moment.tz(time.format(format), timeZone)
9
- }
10
8
  %form#bind.form-inline{:action => action, :method => "get", :style => "margin-bottom: 15px;"}
11
9
  - date_vars = ["start_time", "end_time"]
12
10
  - if (date_vars - @bind_vars).empty?
@@ -16,7 +14,7 @@
16
14
  - @bind_vars.each_with_index do |var, i|
17
15
  = label_tag var, var
18
16
  - if (data = @smart_vars[var])
19
- = select_tag var, options_for_select([[nil, nil]] + data, selected: params[var]), style: "margin-right: 20px; width: 200px; display: none;", multiple: true
17
+ = select_tag var, options_for_select(data, selected: params[var]), style: "margin-right: 20px; width: 200px; display: none;", multiple: true
20
18
  :javascript
21
19
  $("##{var}").selectize({
22
20
  create: true
@@ -100,3 +98,4 @@
100
98
  submitIfCompleted($("#start_time").closest("form"))
101
99
  }
102
100
  %input.btn.btn-success{:style => "vertical-align: top;", :type => "submit", :value => "Run"}/
101
+ %hr
@@ -0,0 +1,52 @@
1
+ - values = rows.first
2
+ - chart_id = SecureRandom.hex
3
+ - column_types = result.column_types
4
+ - chart_type = result.chart_type
5
+ - chart_options = { id: chart_id, height: '80vh' }
6
+
7
+ - if ["line", "line2"].include?(chart_type)
8
+ - chart_options.merge!(min: nil)
9
+ - if chart_type == "scatter"
10
+ - chart_options.merge!(library: {tooltips: {intersect: false}})
11
+ - elsif ["bar", "bar2"].include?(chart_type)
12
+ - chart_options.merge!(library: {tooltips: {intersect: false, axis: 'x'}})
13
+ - elsif chart_type != "pie"
14
+ - if column_types.size == 2 || forecast
15
+ - chart_options.merge!(library: {tooltips: {intersect: false, axis: 'x'}})
16
+ - else
17
+ - chart_options.merge!(library: {tooltips: {intersect: false}})
18
+ - series_library = {}
19
+ - target_index = columns.index { |k| k.downcase == "target" }
20
+ - if target_index
21
+ - series_library[target_index - 1] = {pointStyle: "line", hitRadius: 5, borderColor: "#109618", pointBackgroundColor: "#109618", backgroundColor: "#109618"}
22
+ - if forecast
23
+ - color = "#54a3ee"
24
+ - series_library[1] = {borderDash: [8], borderColor: color, pointBackgroundColor: color, backgroundColor: color, pointHoverBackgroundColor: color}
25
+ - elsif chart_type == "line"
26
+ - chart_data = columns[1..-1].each_with_index.map{ |k, i| {name: blazer_series_name(k), data: rows.map{ |r| [r[0], r[i + 1]] }, library: series_library[i]} }
27
+ = line_chart chart_data, chart_options
28
+ - elsif chart_type == "line2"
29
+ = line_chart rows.group_by { |r| v = r[1]; (boom[columns[1]] || {})[v.to_s] || v }.each_with_index.map { |(name, v), i| {name: blazer_series_name(name), data: v.map { |v2| [v2[0], v2[2]] }, library: series_library[i]} }, chart_options
30
+ - elsif chart_type == "pie"
31
+ = pie_chart rows.map { |r| [(boom[columns[0]] || {})[r[0].to_s] || r[0], r[1]] }, chart_options
32
+ - elsif chart_type == "bar"
33
+ = column_chart (values.size - 1).times.map { |i| name = columns[i + 1]; {name: blazer_series_name(name), data: rows.first(20).map { |r| [(boom[columns[0]] || {})[r[0].to_s] || r[0], r[i + 1]] } } }, chart_options
34
+ - elsif chart_type == "bar2"
35
+ - first_20 = rows.group_by { |r| r[0] }.values.first(20).flatten(1)
36
+ - labels = first_20.map { |r| r[0] }.uniq
37
+ - series = first_20.map { |r| r[1] }.uniq
38
+ - labels.each do |l|
39
+ - series.each do |s|
40
+ - first_20 << [l, s, 0] unless first_20.find { |r| r[0] == l && r[1] == s }
41
+ = column_chart first_20.group_by { |r| v = r[1]; (boom[columns[1]] || {})[v.to_s] || v }.each_with_index.map { |(name, v), i| {name: blazer_series_name(name), data: v.sort_by { |r2| labels.index(r2[0]) }.map { |v2| v3 = v2[0]; [(boom[columns[0]] || {})[v3.to_s] || v3, v2[2]] }} }, chart_options
42
+ - elsif chart_type == "scatter"
43
+ = scatter_chart rows, xtitle: columns[0], ytitle: columns[1], **chart_options
44
+ - elsif only_chart
45
+ - if rows.size == 1 && rows.first.size == 1
46
+ - v = rows.first.first
47
+ - if v.is_a?(String) && v == ""
48
+ .text-muted empty string
49
+ - else
50
+ %p{style: "font-size: 160px;"}= blazer_format_value(columns.first, v)
51
+ - else
52
+ - @no_chart = true
@@ -7,17 +7,24 @@
7
7
  .form-group
8
8
  = f.hidden_field :statement
9
9
  #editor-container
10
- #editor{":style" => "{ height: editorHeight }"}= @query.statement
11
- .form-group.text-right{:style => "margin-bottom: 8px;"}
12
- .pull-left{:style => "margin-top: 8px;"}
10
+ #editor{style: "{ height: editorHeight }"}= @query.statement.to_s.gsub("\n", '')
11
+ .form-group.text-right{style: "margin-bottom: 8px;"}
12
+ .pull-left{style: "margin-top: 8px;"}
13
13
  = link_to "Back", :back
14
- %a{":href" => "schemaPath", :style => "margin-left: 40px;", :target => "_blank"} Schema
14
+ = link_to 'Schema', 'schemaPath', style: 'margin-left: 40px;', target: :blank
15
+ - if Blazer.integration
16
+ = link_to 'Integration', '#integration-session', style: 'margin-left: 40px;', data: { toggle: :collapse }
15
17
  = f.select :data_source, Blazer.data_sources.values.select { |ds| q = @query.dup; q.data_source = ds.id; q.editable?(blazer_user) }.map { |ds| [ds.name, ds.id] }, {}, class: ("hide" if Blazer.data_sources.size <= 1), style: "width: 140px;"
16
- #tables{:style => "display: inline-block; width: 250px; margin-right: 10px;"}
17
- %select#table_names{:placeholder => "Preview table", :style => "width: 240px;"}
18
- %a.btn.btn-info{:style => "vertical-align: top; width: 70px;", "v-if" => "!running", "v-on:click" => "run"} Run
19
- %a.btn.btn-danger{:style => "vertical-align: top; width: 70px;", "v-if" => "running", "v-on:click" => "cancel"} Cancel
20
- %hr/
18
+ #tables{style: "display: inline-block; width: 250px; margin-right: 10px;"}
19
+ %select#table_names{placeholder: "Preview table", style: "width: 240px;"}
20
+ %a.btn.btn-info{style: "vertical-align: top; width: 70px;", "v-if" => "!running", "v-on:click" => "run"} Run
21
+ %a.btn.btn-danger{style: "vertical-align: top; width: 70px;", "v-if" => "running", "v-on:click" => "cancel"} Cancel
22
+ - if Blazer.integration
23
+ #integration-session.collapse
24
+ %hr
25
+ = f.hidden_field :integration
26
+ #integration-editor{style: 'height: 200px'}
27
+ %hr
21
28
  = render partial: "tips"
22
29
  .col-xs-4
23
30
  .form-group
@@ -30,6 +37,10 @@
30
37
  .form-group
31
38
  = f.label :assignees
32
39
  = f.collection_select :assignee_ids, @assignees, :first, :last, {}, { placeholder: "Assignees", style: "height: 80px;", class: "form-control", multiple: true }
40
+ - if @teams.any?
41
+ .form-group
42
+ = f.label :teams
43
+ = f.collection_select :team_ids, @teams, :first, :last, {}, { placeholder: "Teams", style: "height: 80px;", class: "form-control", multiple: true }
33
44
  .form-group.text-right
34
45
  = f.submit "For Enter Press", class: "hide"
35
46
  - if @query.persisted?
@@ -48,6 +59,7 @@
48
59
  #results
49
60
  %p.text-muted{"v-if" => "running"} Loading...
50
61
  #results-html{":class" => "{ 'query-error': error }", "v-if" => "!running"}
62
+
51
63
  :javascript
52
64
  #{blazer_js_var "params", variable_params}
53
65
  #{blazer_js_var "previewStatement", Hash[Blazer.data_sources.map { |k, v| [k, (v.preview_statement rescue "")] }]}
@@ -77,7 +89,11 @@
77
89
  this.error = false
78
90
  cancelAllQueries()
79
91
 
80
- var data = $.extend({}, params, {statement: this.getSQL(), data_source: $("#query_data_source").val()})
92
+ var data = $.extend({}, params, {
93
+ statement: this.getSQL(),
94
+ integration: $('#integration').val(),
95
+ data_source: $("#query_data_source").val()
96
+ })
81
97
 
82
98
  var _this = this
83
99
 
@@ -121,9 +137,25 @@
121
137
  selectize.refreshOptions(false)
122
138
  })
123
139
  },
140
+ showIntegrationEditor: function() {
141
+ integrationEditor = ace.edit("integration-editor")
142
+ integrationEditor.setTheme("ace/theme/twilight")
143
+ integrationEditor.getSession().setMode("ace/mode/ruby")
144
+ integrationEditor.setOptions({
145
+ fontSize: 12,
146
+ minLines: 10,
147
+ enableSnippets: false,
148
+ highlightActiveLine: false,
149
+ enableLiveAutocompletion: true,
150
+ enableBasicAutocompletion: true
151
+ })
152
+ integrationEditor.renderer.setShowGutter(true)
153
+ integrationEditor.renderer.setPrintMarginColumn(false)
154
+ integrationEditor.renderer.setPadding(10)
155
+ integrationEditor.getSession().setUseWrapMode(true)
156
+ },
124
157
  showEditor: function() {
125
158
  var _this = this
126
-
127
159
  editor = ace.edit("editor")
128
160
  editor.setTheme("ace/theme/twilight")
129
161
  editor.getSession().setMode("ace/mode/sql")
@@ -278,6 +310,7 @@
278
310
  })
279
311
 
280
312
  this.showEditor()
313
+ this.showIntegrationEditor()
281
314
  }
282
315
  })
283
316
 
@@ -292,7 +325,7 @@
292
325
  }
293
326
 
294
327
  $(document).ready(function() {
295
- $('#query_assignee_ids').select2();
328
+ $('#query_assignee_ids, #query_team_ids').select2();
296
329
  setInterval(function() {
297
330
  var lastVersion = $('#query_statement').val();
298
331
  if (localStorage.getItem('lastVersion') !== lastVersion) {
@@ -20,32 +20,12 @@
20
20
  - if @forecast_error
21
21
  .alert.alert-danger= @forecast_error
22
22
  - if @rows.any?
23
- - values = @rows.first
24
- - chart_id = SecureRandom.hex
25
- - column_types = @result.column_types
26
- - chart_type = @result.chart_type
27
- - chart_options = {id: chart_id}
28
- - if ["line", "line2"].include?(chart_type)
29
- - chart_options.merge!(min: nil)
30
- - if chart_type == "scatter"
31
- - chart_options.merge!(library: {tooltips: {intersect: false}})
32
- - elsif ["bar", "bar2"].include?(chart_type)
33
- - chart_options.merge!(library: {tooltips: {intersect: false, axis: 'x'}})
34
- - elsif chart_type != "pie"
35
- - if column_types.size == 2 || @forecast
36
- - chart_options.merge!(library: {tooltips: {intersect: false, axis: 'x'}})
37
- - else
38
- - chart_options.merge!(library: {tooltips: {intersect: false}})
39
- - series_library = {}
40
- - target_index = @columns.index { |k| k.downcase == "target" }
41
- - if target_index
42
- - series_library[target_index - 1] = {pointStyle: "line", hitRadius: 5, borderColor: "#109618", pointBackgroundColor: "#109618", backgroundColor: "#109618"}
43
- - if @forecast
44
- - color = "#54a3ee"
45
- - series_library[1] = {borderDash: [8], borderColor: color, pointBackgroundColor: color, backgroundColor: color, pointHoverBackgroundColor: color}
23
+ #main-chart
24
+ = render 'chart', rows: @rows, columns: @columns, result: @result, forecast: @forecast, only_chart: @only_chart, boom: @boom
46
25
  - if blazer_maps? && @markers.any?
47
- #map{style: "height: #{@only_chart ? 300 : 500}px;"}
26
+ #map{style: "height: 500px; width: 100%"}
48
27
  :javascript
28
+ $('#map').html('');
49
29
  L.mapbox.accessToken = '#{Blazer.mapbox_access_token}';
50
30
  var map = L.mapbox.map('map', 'ankane.ioo8nki0');
51
31
  #{blazer_js_var "markers", @markers}
@@ -71,34 +51,12 @@
71
51
  }
72
52
  featureLayer.setGeoJSON(geojson);
73
53
  map.fitBounds(featureLayer.getBounds());
74
- - elsif chart_type == "line"
75
- - chart_data = @columns[1..-1].each_with_index.map{ |k, i| {name: blazer_series_name(k), data: @rows.map{ |r| [r[0], r[i + 1]] }, library: series_library[i]} }
76
- = line_chart chart_data, chart_options
77
- - elsif chart_type == "line2"
78
- = line_chart @rows.group_by { |r| v = r[1]; (@boom[@columns[1]] || {})[v.to_s] || v }.each_with_index.map { |(name, v), i| {name: blazer_series_name(name), data: v.map { |v2| [v2[0], v2[2]] }, library: series_library[i]} }, chart_options
79
- - elsif chart_type == "pie"
80
- = pie_chart @rows.map { |r| [(@boom[@columns[0]] || {})[r[0].to_s] || r[0], r[1]] }, chart_options
81
- - elsif chart_type == "bar"
82
- = column_chart (values.size - 1).times.map { |i| name = @columns[i + 1]; {name: blazer_series_name(name), data: @rows.first(20).map { |r| [(@boom[@columns[0]] || {})[r[0].to_s] || r[0], r[i + 1]] } } }, chart_options
83
- - elsif chart_type == "bar2"
84
- - first_20 = @rows.group_by { |r| r[0] }.values.first(20).flatten(1)
85
- - labels = first_20.map { |r| r[0] }.uniq
86
- - series = first_20.map { |r| r[1] }.uniq
87
- - labels.each do |l|
88
- - series.each do |s|
89
- - first_20 << [l, s, 0] unless first_20.find { |r| r[0] == l && r[1] == s }
90
- = column_chart first_20.group_by { |r| v = r[1]; (@boom[@columns[1]] || {})[v.to_s] || v }.each_with_index.map { |(name, v), i| {name: blazer_series_name(name), data: v.sort_by { |r2| labels.index(r2[0]) }.map { |v2| v3 = v2[0]; [(@boom[@columns[0]] || {})[v3.to_s] || v3, v2[2]] }} }, chart_options
91
- - elsif chart_type == "scatter"
92
- = scatter_chart @rows, xtitle: @columns[0], ytitle: @columns[1], **chart_options
93
- - elsif @only_chart
94
- - if @rows.size == 1 && @rows.first.size == 1
95
- - v = @rows.first.first
96
- - if v.is_a?(String) && v == ""
97
- .text-muted empty string
98
- - else
99
- %p{style: "font-size: 160px;"}= blazer_format_value(@columns.first, v)
100
- - else
101
- - @no_chart = true
54
+ map.setZoom(5)._onResize();
55
+ - elsif @no_chart
56
+ :javascript
57
+ $('#chart-tab').hide();
58
+ $('#table-tab').click();
59
+
102
60
  - unless @only_chart && !@no_chart
103
61
  - header_width = 100 / @columns.size.to_f
104
62
  .results-container
@@ -110,7 +68,7 @@
110
68
  %code= @rows[0][0]
111
69
  - else
112
70
  %p.text-muted{style: "margin-bottom: 10px; margin-top: 15px"}
113
- = pluralize(@rows.size, "row")
71
+ = "#{@rows.size} rows" if @rows.size > 1
114
72
  - @checks.select(&:state).each do |check|
115
73
  ·
116
74
  %small{:class => "check-state #{check.state.parameterize.gsub("-", "_")}"}= link_to check.state.upcase, edit_check_path(check)
@@ -157,3 +115,7 @@
157
115
  .text-muted= v2
158
116
  - else
159
117
  %p.text-muted.text-center{style: "margin-top: 15px"} No rows
118
+
119
+ :javascript
120
+ $('#main-chart').appendTo('#chart');
121
+ $('#map').appendTo('#chart');
@@ -4,7 +4,7 @@
4
4
  .row{:style => "padding-top: 13px;"}
5
5
  .col-sm-8
6
6
  = render partial: "blazer/nav"
7
- %h3{:style => "line-height: 34px; display: inline; margin-left: 5px;"}
7
+ %h4{style: "line-height: 34px; display: inline; margin-left: 5px;"}
8
8
  = @query.name
9
9
  .col-sm-4.text-right
10
10
  = link_to "Edit", edit_query_path(@query, variable_params), class: "btn btn-default", disabled: !@query.editable?(blazer_user)
@@ -20,31 +20,54 @@
20
20
  %li= message
21
21
  - if @query.description.present?
22
22
  %p= @query.description
23
+
23
24
  = render partial: "blazer/variables", locals: {action: query_path(@query)}
24
- %pre#code
25
- %code= @statement
26
- - if @success
27
- #results
28
- %p.text-muted Loading...
29
- :javascript
30
- function showRun(data) {
31
- $("#results").html(data);
32
- $("#results table").stupidtable().stickyTableHeaders({
33
- fixedOffset: 60,
34
- });
35
- }
36
25
 
37
- function showError(message) {
38
- $("#results").addClass("query-error").html(message);
39
- }
26
+ %ul.nav.nav-tabs
27
+ %li#chart-tab.active= link_to 'Chart', '#chart', data: { toggle: :tab }
28
+ %li= link_to 'Table', '#table', data: { toggle: :tab }, id: 'table-tab'
29
+ %li= link_to 'Query', '#query', data: { toggle: :tab }
30
+ - if @query.integration.present?
31
+ %li= link_to 'Integration', '#integration', data: { toggle: :tab }
32
+
33
+ .tab-content
34
+ #chart.tab-pane.fade.in.active
35
+ #query.tab-pane.fade
36
+ %pre#code
37
+ %code= @statement.gsub("\r", "\n")
38
+ - if @query.integration.present?
39
+ #integration.tab-pane.fade
40
+ = @query.integration
41
+ #table.tab-pane.fade
42
+ - if @success
43
+ #results
44
+ %p.text-muted Loading...
45
+ :javascript
46
+ function showRun(data) {
47
+ $("#results").html(data);
48
+ $("#results table").stupidtable().stickyTableHeaders({
49
+ fixedOffset: 60,
50
+ });
51
+ }
52
+
53
+ function showError(message) {
54
+ $("#results").addClass("query-error").html(message);
55
+ }
56
+
57
+ #{blazer_js_var "data", variable_params.merge(statement: @statement, query_id: @query.id, data_source: @query.data_source)}
40
58
 
41
- #{blazer_js_var "data", variable_params.merge(statement: @statement, query_id: @query.id, data_source: @query.data_source)}
59
+ runQuery(data, showRun, showError)
60
+ - unless %w(mongodb).include?(Blazer.data_sources[@query.data_source].adapter)
61
+ :javascript
62
+ // do not highlight really long queries
63
+ // this can lead to performance issues
64
+ if ($("code").text().length < 10000) {
65
+ hljs.highlightBlock(document.getElementById("code"));
66
+ }
42
67
 
43
- runQuery(data, showRun, showError)
44
- - unless %w(mongodb).include?(Blazer.data_sources[@query.data_source].adapter)
45
- :javascript
46
- // do not highlight really long queries
47
- // this can lead to performance issues
48
- if ($("code").text().length < 10000) {
49
- hljs.highlightBlock(document.getElementById("code"));
50
- }
68
+ :javascript
69
+ $('ul.nav.nav-tabs li a').click(function(event) {
70
+ localStorage.setItem('currentQueryTab', $(event.currentTarget).attr('href'));
71
+ });
72
+ var currentQueryTab = localStorage.getItem('currentQueryTab');
73
+ $("ul.nav.nav-tabs li a[href='" + currentQueryTab + "']").click();
@@ -15,5 +15,5 @@
15
15
  = javascript_include_tag "https://api.mapbox.com/mapbox.js/v3.1.1/mapbox.js", integrity: "sha384-j81LqvtvYigFzGSUgAoFijpvoq4yGoCJSOXI9DFaUEpenR029MBE3E/X5Gr+WdO0", crossorigin: "anonymous"
16
16
  = csrf_meta_tags
17
17
  %body
18
- .container
18
+ .container-fluid
19
19
  = yield
@@ -12,6 +12,7 @@ require "blazer/data_source"
12
12
  require "blazer/result"
13
13
  require "blazer/excel_parser"
14
14
  require "blazer/run_statement"
15
+ require "blazer/run_integration"
15
16
 
16
17
  # adapters
17
18
  require "blazer/adapters/base_adapter"
@@ -49,9 +50,11 @@ module Blazer
49
50
  attr_accessor :check_schedules
50
51
  attr_accessor :mapbox_access_token
51
52
  attr_accessor :assignees
53
+ attr_accessor :teams
52
54
  attr_accessor :anomaly_checks
53
55
  attr_accessor :forecasting
54
56
  attr_accessor :async
57
+ attr_accessor :integration
55
58
  attr_accessor :images
56
59
  attr_accessor :query_viewable
57
60
  attr_accessor :query_editable
@@ -65,6 +68,7 @@ module Blazer
65
68
  self.check_schedules = ["5 minutes", "1 hour", "1 day"]
66
69
  self.mapbox_access_token = nil
67
70
  self.assignees = []
71
+ self.teams = []
68
72
  self.host = nil
69
73
  self.preview_rows_number = 365
70
74
  self.anomaly_checks = false
@@ -89,6 +93,11 @@ module Blazer
89
93
  @time_zone = time_zone.is_a?(ActiveSupport::TimeZone) ? time_zone : ActiveSupport::TimeZone[time_zone.to_s]
90
94
  end
91
95
 
96
+ def self.integration
97
+ return @integration if defined?(@integration)
98
+ @integration = settings.key?("integration") ? settings["integration"] : false
99
+ end
100
+
92
101
  def self.user_class
93
102
  if !defined?(@user_class)
94
103
  @user_class = settings.key?("user_class") ? settings["user_class"] : (User.name rescue nil)
@@ -52,11 +52,11 @@ module Blazer
52
52
 
53
53
  def preview_statement
54
54
  if postgresql?
55
- "SELECT * FROM \"{table}\" LIMIT 10"
55
+ "SELECT * FROM \"{table}\" LIMIT 30"
56
56
  elsif sqlserver?
57
- "SELECT TOP (10) * FROM {table}"
57
+ "SELECT TOP (30) * FROM {table}"
58
58
  else
59
- "SELECT *\nFROM {table}\nORDER BY id DESC\nLIMIT 10"
59
+ "SELECT *\nFROM {table}\nORDER BY id DESC\nLIMIT 30"
60
60
  end
61
61
  end
62
62