-
Notifications
You must be signed in to change notification settings - Fork 19
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add date_trunc function to mysql #17
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -142,6 +142,8 @@ ActiveReporting::Configuration.setting = value | |
|
||
`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`) | ||
|
||
`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 choose your database's adapter (Default: `:sqlite3`). If you want MySQL put `:mysql2` and if you want PostGreSQL put `:postgresql`. | ||
|
||
|
||
## ActiveReporting::FactModel | ||
|
||
|
@@ -219,7 +221,7 @@ class PhoneFactModel < ActiveReporting::FactModel | |
end | ||
``` | ||
|
||
### Implicit hierarchies with datetime columns (PostgreSQL support only) | ||
### Implicit hierarchies with datetime columns (PostgreSQL and MySQL support only) | ||
|
||
The fastest approach to group by certain date metrics is to create so-called "date dimensions". For | ||
those Postgres users that are restricted from organizing their data in this way, Postgres provides | ||
|
@@ -235,7 +237,6 @@ end | |
|
||
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.) | ||
|
||
*NOTE*: PRs welcomed to support this functionality in other databases. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I guess PRs are still welcome to support other databases 😉 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. LOL, true but the rest of databases are SQLitle which I guess nobody use in production. I doubt someone will do that |
||
|
||
## Configuring Dimension Filters | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
# frozen_string_literal: true | ||
|
||
module ActiveReporting | ||
module DatabaseAdapters | ||
DatabaseNotSupported = Class.new(StandardError) | ||
MethodNotImplemented = Class.new(StandardError) | ||
# Database adapters are here to solve SQL problems in the | ||
# most idiomatic way in each database | ||
class Base | ||
attr_reader :name | ||
|
||
def initialize(name) | ||
@name = name | ||
end | ||
|
||
# @param [Symbol] interval | ||
# @return [Boolean] | ||
def allowed_datetime_hierarchy?(interval) | ||
datetime_hierarchies.include?(interval.try(:to_sym)) | ||
end | ||
|
||
protected | ||
|
||
# Allowed datetime hierchies in each adapter | ||
# By default (:sqlite) there is none | ||
# | ||
# @return [Array<Symbol>] | ||
def datetime_hierarchies | ||
@datetime_hierarchies ||= [] | ||
end | ||
end | ||
end | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
# frozen_string_literal: true | ||
|
||
module ActiveReporting | ||
module DatabaseAdapters | ||
class Factory | ||
class << self | ||
ADAPTERS = { | ||
sqlite3: SqliteAdapter, | ||
mysql2: MysqlAdapter, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's go with just |
||
postgresql: PostgresqlAdapter, | ||
postgis: PostgresqlAdapter | ||
}.freeze | ||
# @param [Symbol] | ||
def for_database(adapter_name) | ||
adapter = ADAPTERS[adapter_name] | ||
|
||
return adapter.new(adapter_name) unless adapter.nil? | ||
|
||
raise( | ||
DatabaseNotSupported, | ||
"Database with this #{adapter_name} is not supported by ActiveReporting" | ||
) | ||
end | ||
end | ||
end | ||
end | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
# frozen_string_literal: true | ||
|
||
module ActiveReporting | ||
module DatabaseAdapters | ||
# Database adapters are here to solve SQL problems in the | ||
# most idiomatic way in each database | ||
class MysqlAdapter < Base | ||
# Generate SQL snippet with DATE_TRUNC | ||
# @param [String] interval | ||
# @param [String] field | ||
# @return [String] | ||
def date_trunc(interval, field) | ||
clean_sql( | ||
<<-SQL | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The "squiggly heredoc" has been in Ruby since 2.3, I recommend using that instead. Also, I would say I personally am not concerned by multi-line SQL being generated, so I don't think |
||
DATE_ADD( | ||
"#{super_old_base_date}", | ||
INTERVAL TIMESTAMPDIFF( | ||
#{interval.upcase}, | ||
"#{super_old_base_date}", | ||
#{field} | ||
) #{interval.upcase} | ||
) | ||
SQL | ||
) | ||
end | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've been playing around with different variants of replicating Take a look at this code and consider if it would be useful. It is ugly but may provide a better parallel in functionality. |
||
|
||
protected | ||
|
||
# Remove spaces and put all in one line | ||
def clean_sql(sql) | ||
sql | ||
.strip | ||
.gsub(/\n+/, ' ') | ||
.gsub(/\s+/, ' ') | ||
.gsub(/\(\s+\)/, '(') | ||
.gsub(/\)\s+\)/, ')') | ||
end | ||
|
||
def datetime_hierarchies | ||
@datetime_hierarchies ||= %i[ | ||
year | ||
month | ||
week | ||
] | ||
end | ||
|
||
# Used to generate a diff when implementing | ||
# datetime truncation | ||
# | ||
# @return [String] | ||
def super_old_base_date | ||
'1900-01-01' | ||
end | ||
end | ||
end | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
# frozen_string_literal: true | ||
|
||
module ActiveReporting | ||
module DatabaseAdapters | ||
class PostgresqlAdapter < Base | ||
# Values for the Postgres `date_trunc` method. | ||
# See https://www.postgresql.org/docs/10/static/functions-datetime.html#FUNCTIONS-DATETIME-TRUNC | ||
|
||
# Generate SQL snippet with DATE_TRUNC | ||
# @param [String] interval | ||
# @param [String] field | ||
# @return [String] | ||
def date_trunc(interval, field) | ||
"DATE_TRUNC('#{interval}', #{field})" | ||
end | ||
|
||
protected | ||
|
||
def datetime_hierarchies | ||
@datetime_hierarchies ||= %i[ | ||
microseconds | ||
milliseconds | ||
second | ||
minute | ||
hour | ||
day | ||
week | ||
month | ||
quarter | ||
year | ||
decade | ||
century | ||
millennium | ||
] | ||
end | ||
end | ||
end | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
# frozen_string_literal: true | ||
|
||
module ActiveReporting | ||
module DatabaseAdapters | ||
class SqliteAdapter < Base | ||
end | ||
end | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -32,8 +32,23 @@ def test_report_runs_with_an_aggregate_other_than_count | |
assert data.all? { |r| r.key?('a_metric') } | ||
end | ||
|
||
def test_report_runs_with_a_date_grouping | ||
if ENV['DB'] == 'pg' | ||
def test_report_runs_with_week_date_grouping | ||
if valid_db_adapter? | ||
metric = ActiveReporting::Metric.new(:a_metric, fact_model: SaleFactModel, dimensions: [{created_at: :week}]) | ||
report = ActiveReporting::Report.new(metric) | ||
data = report.run | ||
assert data.all? { |r| r.key?('created_at_week') } | ||
assert data.size == 4 | ||
else | ||
assert_raises ActiveReporting::InvalidDimensionLabel do | ||
metric = ActiveReporting::Metric.new(:a_metric, fact_model: SaleFactModel, dimensions: [{created_at: :week}]) | ||
report = ActiveReporting::Report.new(metric) | ||
end | ||
end | ||
end | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would propose that you might want to add some more comprehensive tests for the MySQL date_trunc adaptation. You can run into a lot of edge cases. See here for some examples you could include. |
||
|
||
def test_report_runs_with_month_date_grouping | ||
if valid_db_adapter? | ||
metric = ActiveReporting::Metric.new(:a_metric, fact_model: UserFactModel, dimensions: [{created_at: :month}]) | ||
report = ActiveReporting::Report.new(metric) | ||
data = report.run | ||
|
@@ -46,4 +61,5 @@ def test_report_runs_with_a_date_grouping | |
end | ||
end | ||
end | ||
|
||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should add a note here explaining that while it is encouraged to set this value, it is not required if the user does not with to implement database-specific features such as the date functions.