Skip to content

Commit c513381

Browse files
committed
Implement database adapters to be able to have diferent ways of implementing the same feature
1 parent 181e959 commit c513381

File tree

15 files changed

+231
-22
lines changed

15 files changed

+231
-22
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,7 @@
66
/doc/
77
/pkg/
88
/spec/reports/
9+
.ruby-version
10+
.byebug_history
911
/tmp/
1012
.DS_STORE

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,8 @@ ActiveReporting::Configuration.setting = value
142142

143143
`metric_lookup_class` - The name of a constant used to lookup prebuilt `Reporting::Metric` objects by name. The constant should define a class method called `#lookup` which can take a string or symbol of the metric name. (Default: `::Metric`)
144144

145+
`db_adapter` - Each database has a diferent way of doing the same thing on SQL. This is an abstraction layer on each database. For example `PostGreSQL` have a native `date_trunc` while `MySQL` doesn't so we do the same in another way. Make sure to chosse your database's adapter (Default: `:sqlite3`). If you want MySQL put `:mysql2` and if you want PostGreSQL put `:postgresql`.
146+
145147

146148
## ActiveReporting::FactModel
147149

@@ -219,7 +221,7 @@ class PhoneFactModel < ActiveReporting::FactModel
219221
end
220222
```
221223

222-
### Implicit hierarchies with datetime columns (PostgreSQL support only)
224+
### Implicit hierarchies with datetime columns (PostgreSQL and MySQL support only)
223225

224226
The fastest approach to group by certain date metrics is to create so-called "date dimensions". For
225227
those Postgres users that are restricted from organizing their data in this way, Postgres provides
@@ -235,7 +237,6 @@ end
235237

236238
When creating a metric, ActiveReporting will recognize implicit hierarchies for this dimension. The hierarchies correspond to the [values](https://www.postgresql.org/docs/8.1/static/functions-datetime.html#FUNCTIONS-DATETIME-TRUNC) supported by PostgreSQL. (See example under the metric section, below.)
237239

238-
*NOTE*: PRs welcomed to support this functionality in other databases.
239240

240241
## Configuring Dimension Filters
241242

lib/active_reporting.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
# frozen_string_literal: true
22

33
require 'active_record'
4+
require 'active_reporting/database_adapters/base'
5+
require 'active_reporting/database_adapters/sqlite_adapter'
6+
require 'active_reporting/database_adapters/mysql_adapter'
7+
require 'active_reporting/database_adapters/postgresql_adapter'
8+
require 'active_reporting/database_adapters/factory'
49
require 'active_reporting/active_record_adaptor'
510
require 'active_reporting/configuration'
611
require 'active_reporting/dimension'

lib/active_reporting/configuration.rb

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,22 @@ def self.config
1313
yield self
1414
end
1515

16+
# Set database adapter. There are some SQL functions that are only
17+
# available in some databases. You need to specify adapter
18+
#
19+
# Default db_adapter: SqliteAdapter
20+
def self.db_adapter
21+
@db_adapter ||= DatabaseAdapters::Factory.for_database(:sqlite3)
22+
end
23+
24+
# Set database adapter. There are some SQL functions that are only
25+
# available in some databases. You need to specify adapter
26+
#
27+
# @param [Symbol] adapter_name
28+
def self.db_adapter=(adapter_name)
29+
@db_adapter = DatabaseAdapters::Factory.for_database(adapter_name)
30+
end
31+
1632
# The default label used by all dimensions if not set otherwise
1733
#
1834
# Default value is `:name`
@@ -57,6 +73,7 @@ def self.ransack_fallback
5773
# @return [Boolean]
5874
def self.ransack_fallback=(fallback)
5975
raise RansackNotAvailable unless ransack_available
76+
6077
@ransack_fallback = fallback
6178
end
6279

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# frozen_string_literal: true
2+
3+
module ActiveReporting
4+
module DatabaseAdapters
5+
DatabaseNotSupported = Class.new(StandardError)
6+
MethodNotImplemented = Class.new(StandardError)
7+
# Database adapters are here to solve SQL problems in the
8+
# most idiomatic way in each database
9+
class Base
10+
attr_reader :name
11+
12+
def initialize(name)
13+
@name = name
14+
end
15+
16+
# @param [Symbol] interval
17+
# @return [Boolean]
18+
def allowed_datetime_hierarchy?(interval)
19+
datetime_hierarchies.include?(interval.try(:to_sym))
20+
end
21+
22+
protected
23+
24+
# Allowed datetime hierchies in each adapter
25+
# By default (:sqlite) there is none
26+
#
27+
# @return [Array<Symbol>]
28+
def datetime_hierarchies
29+
@datetime_hierarchies ||= []
30+
end
31+
end
32+
end
33+
end
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# frozen_string_literal: true
2+
3+
module ActiveReporting
4+
module DatabaseAdapters
5+
class Factory
6+
class << self
7+
ADAPTERS = {
8+
sqlite3: SqliteAdapter,
9+
mysql2: MysqlAdapter,
10+
postgresql: PostgresqlAdapter,
11+
postgis: PostgresqlAdapter
12+
}.freeze
13+
# @param [Symbol]
14+
def for_database(adapter_name)
15+
adapter = ADAPTERS[adapter_name]
16+
17+
return adapter.new(adapter_name) unless adapter.nil?
18+
19+
raise(
20+
DatabaseNotSupported,
21+
"Database with this #{adapter_name} is not supported by ActiveReporting"
22+
)
23+
end
24+
end
25+
end
26+
end
27+
end
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# frozen_string_literal: true
2+
3+
module ActiveReporting
4+
module DatabaseAdapters
5+
# Database adapters are here to solve SQL problems in the
6+
# most idiomatic way in each database
7+
class MysqlAdapter < Base
8+
# Generate SQL snippet with DATE_TRUNC
9+
# @param [String] interval
10+
# @param [String] field
11+
# @return [String]
12+
def date_trunc(interval, field)
13+
clean_sql(
14+
<<-SQL
15+
DATE_ADD(
16+
"#{super_old_base_date}",
17+
INTERVAL TIMESTAMPDIFF(
18+
#{interval.upcase},
19+
"#{super_old_base_date}",
20+
#{field}
21+
) #{interval.upcase}
22+
)
23+
SQL
24+
)
25+
end
26+
27+
protected
28+
29+
# Remove spaces and put all in one line
30+
def clean_sql(sql)
31+
sql
32+
.strip
33+
.gsub(/\n+/, ' ')
34+
.gsub(/\s+/, ' ')
35+
.gsub(/\(\s+\)/, '(')
36+
.gsub(/\)\s+\)/, ')')
37+
end
38+
39+
def datetime_hierarchies
40+
@datetime_hierarchies ||= %i[
41+
year
42+
month
43+
week
44+
]
45+
end
46+
47+
# Used to generate a diff when implementing
48+
# datetime truncation
49+
#
50+
# @return [String]
51+
def super_old_base_date
52+
'1900-01-01'
53+
end
54+
end
55+
end
56+
end
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# frozen_string_literal: true
2+
3+
module ActiveReporting
4+
module DatabaseAdapters
5+
class PostgresqlAdapter < Base
6+
# Values for the Postgres `date_trunc` method.
7+
# See https://www.postgresql.org/docs/10/static/functions-datetime.html#FUNCTIONS-DATETIME-TRUNC
8+
9+
# Generate SQL snippet with DATE_TRUNC
10+
# @param [String] interval
11+
# @param [String] field
12+
# @return [String]
13+
def date_trunc(interval, field)
14+
"DATE_TRUNC('#{interval}', #{field})"
15+
end
16+
17+
protected
18+
19+
def datetime_hierarchies
20+
@datetime_hierarchies ||= %i[
21+
microseconds
22+
milliseconds
23+
second
24+
minute
25+
hour
26+
day
27+
week
28+
month
29+
quarter
30+
year
31+
decade
32+
century
33+
millennium
34+
]
35+
end
36+
end
37+
end
38+
end
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# frozen_string_literal: true
2+
3+
module ActiveReporting
4+
module DatabaseAdapters
5+
class SqliteAdapter < Base
6+
end
7+
end
8+
end

lib/active_reporting/reporting_dimension.rb

Lines changed: 11 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,6 @@
44
module ActiveReporting
55
class ReportingDimension
66
extend Forwardable
7-
SUPPORTED_DBS = %w[PostgreSQL PostGIS].freeze
8-
# Values for the Postgres `date_trunc` method.
9-
# See https://www.postgresql.org/docs/10/static/functions-datetime.html#FUNCTIONS-DATETIME-TRUNC
10-
DATETIME_HIERARCHIES = %i[microseconds milliseconds second minute hour day week month quarter year decade
11-
century millennium].freeze
127
def_delegators :@dimension, :name, :type, :klass, :association, :model, :hierarchical?, :datetime?
138

149
def self.build_from_dimensions(fact_model, dimensions)
@@ -85,7 +80,6 @@ def determine_label(label)
8580

8681
def validate_hierarchical_label(hierarchical_label)
8782
if datetime?
88-
validate_supported_database_for_datetime_hierarchies
8983
validate_against_datetime_hierarchies(hierarchical_label)
9084
else
9185
validate_dimension_is_hierachical(hierarchical_label)
@@ -96,34 +90,35 @@ def validate_hierarchical_label(hierarchical_label)
9690

9791
def validate_dimension_is_hierachical(hierarchical_label)
9892
return if hierarchical?
99-
raise InvalidDimensionLabel, "#{name} must be hierarchical to use label #{hierarchical_label}"
100-
end
10193

102-
def validate_supported_database_for_datetime_hierarchies
103-
return if SUPPORTED_DBS.include?(model.connection.adapter_name)
104-
raise InvalidDimensionLabel,
105-
"Cannot utilize datetime grouping for #{name}; " \
106-
"database #{model.connection.adapter_name} is not supported"
94+
raise InvalidDimensionLabel, "#{name} must be hierarchical to use label #{hierarchical_label}"
10795
end
10896

10997
def validate_against_datetime_hierarchies(hierarchical_label)
110-
return if DATETIME_HIERARCHIES.include?(hierarchical_label.to_sym)
98+
return if Configuration.db_adapter.allowed_datetime_hierarchy?(hierarchical_label)
99+
111100
raise InvalidDimensionLabel, "#{hierarchical_label} is not a valid datetime grouping label in #{name}"
112101
end
113102

114103
def validate_against_fact_model_properties(hierarchical_label)
115104
return if dimension_fact_model.hierarchical_levels.include?(hierarchical_label.to_sym)
105+
116106
raise InvalidDimensionLabel, "#{hierarchical_label} is not a hierarchical label in #{name}"
117107
end
118108

119109
def degenerate_fragment
120110
return "#{name}_#{@label}" if datetime?
111+
121112
"#{model.quoted_table_name}.#{name}"
122113
end
123114

124115
def degenerate_select_fragment
125-
return "DATE_TRUNC('#{@label}', #{model.quoted_table_name}.#{name}) AS #{name}_#{@label}" if datetime?
126-
"#{model.quoted_table_name}.#{name}"
116+
return "#{model.quoted_table_name}.#{name}" unless datetime?
117+
118+
date_trunc = Configuration.db_adapter.date_trunc(
119+
@label, "#{model.quoted_table_name}.#{name}"
120+
)
121+
"#{date_trunc} AS #{name}_#{@label}"
127122
end
128123

129124
def identifier_fragment

0 commit comments

Comments
 (0)