active_reporting 0.4.1 → 0.6.1
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.
- checksums.yaml +4 -4
- data/.travis.yml +5 -17
- data/CHANGELOG.md +45 -0
- data/Gemfile +1 -17
- data/README.md +50 -22
- data/active_reporting.gemspec +1 -1
- data/lib/active_reporting.rb +1 -0
- data/lib/active_reporting/dimension.rb +0 -7
- data/lib/active_reporting/fact_model.rb +2 -0
- data/lib/active_reporting/report.rb +8 -5
- data/lib/active_reporting/reporting_dimension.rb +127 -37
- data/lib/active_reporting/version.rb +1 -1
- metadata +7 -7
    
        checksums.yaml
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            ---
         | 
| 2 2 | 
             
            SHA256:
         | 
| 3 | 
            -
              metadata.gz:  | 
| 4 | 
            -
              data.tar.gz:  | 
| 3 | 
            +
              metadata.gz: bd8ff0998a757ff875a17baad2fcb148ace4239da0781e1b7fbdd4c6367cef2c
         | 
| 4 | 
            +
              data.tar.gz: 316ee68af897e1b866b79b28d3f0889f116982a48b94a123cde5170ac851b4e1
         | 
| 5 5 | 
             
            SHA512:
         | 
| 6 | 
            -
              metadata.gz:  | 
| 7 | 
            -
              data.tar.gz:  | 
| 6 | 
            +
              metadata.gz: 73f92eed3389912530e9ff393e761bbc6e86a84e1c6beff4428de03f295a2fe91f2b230d25d8a3d0b783761b099dca43b4bc62ea05538a469c925eae5c9044d2
         | 
| 7 | 
            +
              data.tar.gz: b1217306b02aa8e40b78cea0d0d31dca84621c4b676e79a638bf8644891e301a3d850e2f645d9c0b745724c4a03c0837936cbe1776e9794a89e4e73580589256
         | 
    
        data/.travis.yml
    CHANGED
    
    | @@ -1,11 +1,13 @@ | |
| 1 1 | 
             
            sudo: false
         | 
| 2 2 | 
             
            language: ruby
         | 
| 3 | 
            +
            services:
         | 
| 4 | 
            +
              - mysql
         | 
| 3 5 | 
             
            addons:
         | 
| 4 6 | 
             
              postgresql: "9.6"
         | 
| 5 7 | 
             
            rvm:
         | 
| 6 | 
            -
              - 2. | 
| 7 | 
            -
              - 2. | 
| 8 | 
            -
              - 2. | 
| 8 | 
            +
              - 2.5.8
         | 
| 9 | 
            +
              - 2.6.6
         | 
| 10 | 
            +
              - 2.7.1
         | 
| 9 11 | 
             
            env:
         | 
| 10 12 | 
             
              - RAILS=6-0 DB=sqlite
         | 
| 11 13 | 
             
              - RAILS=6-0 DB=pg
         | 
| @@ -13,20 +15,6 @@ env: | |
| 13 15 | 
             
              - RAILS=5-2 DB=sqlite
         | 
| 14 16 | 
             
              - RAILS=5-2 DB=pg
         | 
| 15 17 | 
             
              - RAILS=5-2 DB=mysql
         | 
| 16 | 
            -
              - RAILS=5-1 DB=sqlite
         | 
| 17 | 
            -
              - RAILS=5-1 DB=pg
         | 
| 18 | 
            -
              - RAILS=5-1 DB=mysql
         | 
| 19 | 
            -
              - RAILS=4-2 DB=sqlite
         | 
| 20 | 
            -
              - RAILS=4-2 DB=pg
         | 
| 21 | 
            -
              - RAILS=4-2 DB=mysql
         | 
| 22 | 
            -
            matrix:
         | 
| 23 | 
            -
              exclude:
         | 
| 24 | 
            -
                - rvm: 2.4.6
         | 
| 25 | 
            -
                  env: RAILS=6-0 DB=mysql
         | 
| 26 | 
            -
                - rvm: 2.4.6
         | 
| 27 | 
            -
                  env: RAILS=6-0 DB=pg
         | 
| 28 | 
            -
                - rvm: 2.4.6
         | 
| 29 | 
            -
                  env: RAILS=6-0 DB=sqlite
         | 
| 30 18 | 
             
            before_script:
         | 
| 31 19 | 
             
              - psql -c 'create database active_reporting_test;' -U postgres
         | 
| 32 20 | 
             
              - mysql -e 'create database active_reporting_test collate utf8_general_ci;'
         | 
    
        data/CHANGELOG.md
    CHANGED
    
    | @@ -1,3 +1,48 @@ | |
| 1 | 
            +
            ## 0.6.1 (2020-08-29)
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            ### Misc
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            * Add `date` as an option for datetime drills - *germanotm*
         | 
| 6 | 
            +
             | 
| 7 | 
            +
            ## 0.6.0 (2020-08-21)
         | 
| 8 | 
            +
             | 
| 9 | 
            +
            ### Features
         | 
| 10 | 
            +
             | 
| 11 | 
            +
            * Support to implicit hierarchical on datetime columns in MySQL (#33) - *germanotm*
         | 
| 12 | 
            +
            * Added `{ datetime_drill: :month }` option for reporting dimentions to explicitly -  *germanotm*
         | 
| 13 | 
            +
             | 
| 14 | 
            +
              This depricates the use of key-value only use for report dimension options (ie, `dimensions: [{ dim: single_option }]`).
         | 
| 15 | 
            +
              Instead, use `dimensions: [{ dim: { option: value} }]` See the README for all reporting dimension options.
         | 
| 16 | 
            +
             | 
| 17 | 
            +
            ## 0.5.1 (2020-06-31)
         | 
| 18 | 
            +
             | 
| 19 | 
            +
            ### Features
         | 
| 20 | 
            +
             | 
| 21 | 
            +
            * Allow dimensions defined in a `Metric` to use LEFT OUTER JOINs via a new `:join_method` option (#32) - *germanotm*
         | 
| 22 | 
            +
             | 
| 23 | 
            +
            ### Misc
         | 
| 24 | 
            +
             | 
| 25 | 
            +
            * Fixed warning about initialized variables
         | 
| 26 | 
            +
            * Fixed Ruby 2.7 warning
         | 
| 27 | 
            +
             | 
| 28 | 
            +
            ## 0.5.0 (2020-06-30)
         | 
| 29 | 
            +
             | 
| 30 | 
            +
            ### Bug Fixes
         | 
| 31 | 
            +
             | 
| 32 | 
            +
            * Fix Missing quotation marks in column names causing SQL errors on MYSQL (#30) - *germanotm*
         | 
| 33 | 
            +
             | 
| 34 | 
            +
            ### Misc
         | 
| 35 | 
            +
             | 
| 36 | 
            +
            * Update matrix to only supported Rubies and Rails versions. Rails 5.2+ and Ruby 2.5+ are officially supported now.
         | 
| 37 | 
            +
             | 
| 38 | 
            +
            ## 0.4.2 (2019-11-01)
         | 
| 39 | 
            +
             | 
| 40 | 
            +
            ### Misc
         | 
| 41 | 
            +
             | 
| 42 | 
            +
            * Test against Rails 6.0 final
         | 
| 43 | 
            +
            * Fixed deprecated call to `to_hash` - *joshforbes*
         | 
| 44 | 
            +
            * Corrected readme entry for `dimesions` option for `ActiveReporting::Metric` - *joshforbes*
         | 
| 45 | 
            +
             | 
| 1 46 | 
             
            ## 0.4.1 (2019-05-28)
         | 
| 2 47 |  | 
| 3 48 | 
             
            ### Features
         | 
    
        data/Gemfile
    CHANGED
    
    | @@ -8,24 +8,8 @@ rails = ENV['RAILS'] || '5-2' | |
| 8 8 | 
             
            db = ENV['DB'] || 'sqlite'
         | 
| 9 9 |  | 
| 10 10 | 
             
            case rails
         | 
| 11 | 
            -
            when '4-2'
         | 
| 12 | 
            -
              gem 'activerecord', '~> 4.2.0'
         | 
| 13 | 
            -
            when '5-1'
         | 
| 14 | 
            -
              gem 'activerecord', '~> 5.1.0'
         | 
| 15 11 | 
             
            when '5-2'
         | 
| 16 12 | 
             
              gem 'activerecord', '~> 5.2.0'
         | 
| 17 13 | 
             
            when '6-0'
         | 
| 18 | 
            -
              gem 'activerecord', '6.0.0 | 
| 19 | 
            -
            end
         | 
| 20 | 
            -
             | 
| 21 | 
            -
            case rails
         | 
| 22 | 
            -
            when '4-2'
         | 
| 23 | 
            -
              case ENV['DB']
         | 
| 24 | 
            -
              when 'pg'
         | 
| 25 | 
            -
                gem 'pg', '~> 0.18'
         | 
| 26 | 
            -
              when 'mysql'
         | 
| 27 | 
            -
                gem 'mysql2', '~> 0.3.18'
         | 
| 28 | 
            -
              when 'sqlite'
         | 
| 29 | 
            -
                gem 'sqlite3', '~> 1.3.0'
         | 
| 30 | 
            -
              end
         | 
| 14 | 
            +
              gem 'activerecord', '~> 6.0.0'
         | 
| 31 15 | 
             
            end
         | 
    
        data/README.md
    CHANGED
    
    | @@ -8,9 +8,9 @@ ActiveReporting implements various terminology used in Relational Online Analyti | |
| 8 8 |  | 
| 9 9 | 
             
            ActiveReporting officially supports MySQL, PostgreSQL, and SQLite.
         | 
| 10 10 |  | 
| 11 | 
            -
            ActiveReporting officially supports Ruby 2. | 
| 11 | 
            +
            ActiveReporting officially supports Ruby 2.5 and later. Other versions may work, but are not supported.
         | 
| 12 12 |  | 
| 13 | 
            -
            ActiveReporting officially supports Rails  | 
| 13 | 
            +
            ActiveReporting officially supports Rails 5.2, and 6.0. Other versions may work, but are not supported.
         | 
| 14 14 |  | 
| 15 15 | 
             
            ## Installation
         | 
| 16 16 |  | 
| @@ -49,8 +49,9 @@ Rails: ActiveRecord model | |
| 49 49 | 
             
            A dimension is a point of data used to "slice and dice" data from a fact model. It's either a column that lives on the fact table or a foreign key to another table.
         | 
| 50 50 |  | 
| 51 51 | 
             
            Examples:
         | 
| 52 | 
            -
             | 
| 53 | 
            -
            * A  | 
| 52 | 
            +
             | 
| 53 | 
            +
            * A sales rep on a fact table of sales
         | 
| 54 | 
            +
            * A state of an sale on a state machine
         | 
| 54 55 | 
             
            * The manufacture on a fact table of widgets
         | 
| 55 56 |  | 
| 56 57 | 
             
            SQL Equivalent: JOIN, GROUP BY
         | 
| @@ -62,6 +63,7 @@ Rails: ActiveRecord relation or attribute | |
| 62 63 | 
             
            A hierarchy for a dimension is related attributes that live on a dimension table used to drill down and drill up through a dimension.
         | 
| 63 64 |  | 
| 64 65 | 
             
            Examples:
         | 
| 66 | 
            +
             | 
| 65 67 | 
             
            * Dates: Date, Month, Year, Quarter
         | 
| 66 68 | 
             
            * Mobile Phone: Model, Manufacture, OS, Wireless Technology
         | 
| 67 69 |  | 
| @@ -70,6 +72,7 @@ Examples: | |
| 70 72 | 
             
            This is information related to a dimension. When the dimension lives on the fact table, the label is the column used. When the dimension is a related table, the label is a column representing the hierarchy level.
         | 
| 71 73 |  | 
| 72 74 | 
             
            Examples:
         | 
| 75 | 
            +
             | 
| 73 76 | 
             
            * When dimensioning blog posts by category, the dimension is the category_id which leads to the categories table. The label would be the category name.
         | 
| 74 77 |  | 
| 75 78 | 
             
            ### Dimension Filter (or just "filter")
         | 
| @@ -85,6 +88,7 @@ Rails: `where()`, scopes, etc. | |
| 85 88 | 
             
            A measure is a column in a fact table (usually a numeric value) used in aggregations such as sum, maximum, average, etc.
         | 
| 86 89 |  | 
| 87 90 | 
             
            Examples:
         | 
| 91 | 
            +
             | 
| 88 92 | 
             
            * Total amount in a sale
         | 
| 89 93 | 
             
            * Number of units used in a transaction
         | 
| 90 94 |  | 
| @@ -178,7 +182,7 @@ end | |
| 178 182 | 
             
            ActiveReporting assumes the column of a fact model used for summing, averaging, etc. is called `value`. This may be changed on a fact model using `measure=`. You may pass in a string or symbol of the column you wish to use for aggregations.
         | 
| 179 183 |  | 
| 180 184 | 
             
            ```ruby
         | 
| 181 | 
            -
            class  | 
| 185 | 
            +
            class SaleFactModel < ActiveReporting::FactModel
         | 
| 182 186 | 
             
              self.measure = :total
         | 
| 183 187 | 
             
            end
         | 
| 184 188 | 
             
            ```
         | 
| @@ -219,11 +223,9 @@ class PhoneFactModel < ActiveReporting::FactModel | |
| 219 223 | 
             
            end
         | 
| 220 224 | 
             
            ```
         | 
| 221 225 |  | 
| 222 | 
            -
            ###  | 
| 226 | 
            +
            ### Drill down / Roll up (Drill up) with datetime columns
         | 
| 223 227 |  | 
| 224 | 
            -
            The fastest approach to group by certain date metrics is to create so-called "date dimensions". For
         | 
| 225 | 
            -
            those Postgres users that are restricted from organizing their data in this way, Postgres provides
         | 
| 226 | 
            -
            a way to group by `datetime` column data on the fly using the `date_trunc` function.
         | 
| 228 | 
            +
            The fastest approach to group by certain date metrics is to create so-called "date dimensions" and add on columns for each desired hierarchy. For those users that are restricted from organizing their data in this way,  ActiveRporting provides a `datetime_drill` option that can be passed with the dimension on the metric definition to drill datetime columns.
         | 
| 227 229 |  | 
| 228 230 | 
             
            To use, declare a datetime dimension on a fact model as normal:
         | 
| 229 231 |  | 
| @@ -233,7 +235,24 @@ class UserFactModel < ActiveReporting::FactModel | |
| 233 235 | 
             
            end
         | 
| 234 236 | 
             
            ```
         | 
| 235 237 |  | 
| 236 | 
            -
            When creating a metric, ActiveReporting will recognize  | 
| 238 | 
            +
            When creating a metric, ActiveReporting will recognize the following datetime hierarchies: (See example under the metric section, below.)
         | 
| 239 | 
            +
             | 
| 240 | 
            +
            - microseconds
         | 
| 241 | 
            +
            - milliseconds
         | 
| 242 | 
            +
            - second
         | 
| 243 | 
            +
            - minute
         | 
| 244 | 
            +
            - hour
         | 
| 245 | 
            +
            - day
         | 
| 246 | 
            +
            - week
         | 
| 247 | 
            +
            - month
         | 
| 248 | 
            +
            - quarter
         | 
| 249 | 
            +
            - year
         | 
| 250 | 
            +
            - decade
         | 
| 251 | 
            +
            - century
         | 
| 252 | 
            +
            - millennium
         | 
| 253 | 
            +
            - date
         | 
| 254 | 
            +
             | 
| 255 | 
            +
            Under the hood Active Reporting uses specific database functions to manipulate datetime columns. Postgres provides a way to group by `datetime` column data on the fly using the [`date_trunc` function](https://www.postgresql.org/docs/8.1/static/functions-datetime.html#FUNCTIONS-DATETIME-TRUNC). On Mysql this can be done using [Date and Time Functions](https://dev.mysql.com/doc/refman/8.0/en/date-and-time-functions.html).
         | 
| 237 256 |  | 
| 238 257 | 
             
            *NOTE*: PRs welcomed to support this functionality in other databases.
         | 
| 239 258 |  | 
| @@ -273,8 +292,8 @@ A `Metric` is the basic building block used to describe a question you want to a | |
| 273 292 |  | 
| 274 293 | 
             
            ```ruby
         | 
| 275 294 | 
             
            my_metric = ActiveReporting::Metric.new(
         | 
| 276 | 
            -
              : | 
| 277 | 
            -
              fact_model:  | 
| 295 | 
            +
              :sale_total,
         | 
| 296 | 
            +
              fact_model: SaleFactModel,
         | 
| 278 297 | 
             
              aggregate: :sum
         | 
| 279 298 | 
             
            )
         | 
| 280 299 | 
             
            ```
         | 
| @@ -285,7 +304,17 @@ my_metric = ActiveReporting::Metric.new( | |
| 285 304 |  | 
| 286 305 | 
             
            `aggregate` - The SQL aggregate used to calculate the metric. Supported aggregates include count, max, min, avg, and sum. (Default: `:count`)
         | 
| 287 306 |  | 
| 288 | 
            -
            `dimensions` - An array of dimensions used for the metric. When given just a symbol, the default dimension label will be used for the dimension. | 
| 307 | 
            +
            `dimensions` - An array of dimensions used for the metric. When given just a symbol, the default dimension label will be used for the dimension.
         | 
| 308 | 
            +
             | 
| 309 | 
            +
            You may pass a hash instead of a symbol to customize the dimension options (example: { dimension_name: { option1: value, option2: value}}). The avaliable options are:
         | 
| 310 | 
            +
             | 
| 311 | 
            +
            - `field` - Specify the hierarchy level that should be used instead the default dimension label. Ex: `[:sales_rep, {mobile_phone: { field :manufacture }}]`. If you use a hash instead of a Symbol to define a hierarchy the `field` item must be a valid field in your table.
         | 
| 312 | 
            +
             | 
| 313 | 
            +
            - `name` - You may costumize the label alias, by default the dimension name will be used. The `name` can be whatever label you want. Ex :`[{sale_date: { field: :month, name: :a_custom_name_for_month }}]`.
         | 
| 314 | 
            +
             | 
| 315 | 
            +
            - `join_method` - You may choose the join_method with the dimension. The default value for join_method is :joins which does a standard "INNER JOIN", but you can pass a :left_outer_joins to use "LEFT OUTER JOIN" instead. Ex: `[{sales_rep: { join_method: :left_outer_joins }}]`
         | 
| 316 | 
            +
             | 
| 317 | 
            +
            - `datetime_drill` - To drill up and down over datetime column you may pass a `datetime_drill`. Ex: `[:sales_rep, { order: { field: :created_at, datetime_drill: :month }}]`. This option will perform an implicit drill over datetime columns and not a date dimension relationship.
         | 
| 289 318 |  | 
| 290 319 | 
             
            `dimension_filter` - A hash were the keys are dimension filter names and the values are the values passed into the filter.
         | 
| 291 320 |  | 
| @@ -303,7 +332,7 @@ end | |
| 303 332 | 
             
            my_metric = ActiveReporting::Metric.new(
         | 
| 304 333 | 
             
              :my_total,
         | 
| 305 334 | 
             
              fact_model: UserFactModel,
         | 
| 306 | 
            -
              dimensions: [{ created_at: :quarter } ]
         | 
| 335 | 
            +
              dimensions: [{ created_at: { datetime_drill: :quarter }} ]
         | 
| 307 336 | 
             
            )
         | 
| 308 337 | 
             
            ```
         | 
| 309 338 |  | 
| @@ -313,20 +342,20 @@ A `Report` takes an `ActiveReporting::Metric` and ties everything together. It i | |
| 313 342 |  | 
| 314 343 | 
             
            ```ruby
         | 
| 315 344 | 
             
            metric = ActiveReporting::Metric.new(
         | 
| 316 | 
            -
              : | 
| 317 | 
            -
              fact_model:  | 
| 345 | 
            +
              :sale_count,
         | 
| 346 | 
            +
              fact_model: SaleFactModel,
         | 
| 318 347 | 
             
              dimensions: [:sales_rep],
         | 
| 319 348 | 
             
              dimension_filter: {months_ago: 1}
         | 
| 320 349 | 
             
            )
         | 
| 321 350 |  | 
| 322 351 | 
             
            report = ActiveReporting::Report.new(metric)
         | 
| 323 352 | 
             
            report.run
         | 
| 324 | 
            -
            => [{ | 
| 353 | 
            +
            => [{sale_count: 12, sales_rep: 'Fred Jones', sales_rep_identifier: 123},{sale_count: 17, sales_rep: 'Mary Sue', sales_rep_identifier: 123}]
         | 
| 325 354 | 
             
            ```
         | 
| 326 355 |  | 
| 327 356 | 
             
            A `Report` may also take additional arguments to merge with the `Metric`'s information. This can be user input for additional filters, or to expand on a base `Metric`.
         | 
| 328 357 |  | 
| 329 | 
            -
            `dimension_identifiers` - When true, the result will include the database identifier columns of the dimensions. For example, when running a report for the total number of  | 
| 358 | 
            +
            `dimension_identifiers` - When true, the result will include the database identifier columns of the dimensions. For example, when running a report for the total number of sales dimensioned by sales rep, the rep's IDs from the `sales_reps` table will be included. (Default `true`)
         | 
| 330 359 |  | 
| 331 360 | 
             
            `dimension_filter` - A hash that will be merged with the `Metric`'s dimension filters.
         | 
| 332 361 |  | 
| @@ -336,15 +365,15 @@ A `Report` may also take additional arguments to merge with the `Metric`'s infor | |
| 336 365 |  | 
| 337 366 | 
             
            ```ruby
         | 
| 338 367 | 
             
            metric = ActiveReporting::Metric.new(
         | 
| 339 | 
            -
              : | 
| 340 | 
            -
              fact_model:  | 
| 368 | 
            +
              :sale_count,
         | 
| 369 | 
            +
              fact_model: SaleFactModel,
         | 
| 341 370 | 
             
              dimensions: [:sales_rep],
         | 
| 342 371 | 
             
              dimension_filter: {months_ago: 1}
         | 
| 343 372 | 
             
            )
         | 
| 344 373 |  | 
| 345 374 | 
             
            report = ActiveReporting::Report.new(metric, dimension_filter: {from_region: 'North'}, dimension_identifiers: false)
         | 
| 346 375 | 
             
            report.run
         | 
| 347 | 
            -
            => [{ | 
| 376 | 
            +
            => [{sale_count: 17, sales_rep: 'Mary Sue'}]
         | 
| 348 377 | 
             
            ```
         | 
| 349 378 |  | 
| 350 379 | 
             
            It may be more DRY to store ready-made metrics in a database table or stored in memory to use as the bases for various reports. You can pass a string or symbol into a `Report` instead of a `Metric` to look up an pre-made metric. This is done by passing the symbol or string into the `lookup` class method on the constant defined in `ActiveReporting::Configuration.metric_lookup_class`.
         | 
| @@ -376,7 +405,6 @@ appropriate `DB` environment variable, e.g. `DB=pg rake test`. | |
| 376 405 |  | 
| 377 406 | 
             
            Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/active_reporting. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
         | 
| 378 407 |  | 
| 379 | 
            -
             | 
| 380 408 | 
             
            ## License
         | 
| 381 409 |  | 
| 382 410 | 
             
            The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
         | 
    
        data/active_reporting.gemspec
    CHANGED
    
    | @@ -31,7 +31,7 @@ Gem::Specification.new do |spec| | |
| 31 31 | 
             
              spec.add_development_dependency 'minitest', '~> 5.0'
         | 
| 32 32 | 
             
              spec.add_development_dependency 'mysql2'
         | 
| 33 33 | 
             
              spec.add_development_dependency 'pg'
         | 
| 34 | 
            -
              spec.add_development_dependency 'rake' | 
| 34 | 
            +
              spec.add_development_dependency 'rake'
         | 
| 35 35 | 
             
              spec.add_development_dependency 'ransack'
         | 
| 36 36 | 
             
              spec.add_development_dependency 'sqlite3'
         | 
| 37 37 | 
             
            end
         | 
    
        data/lib/active_reporting.rb
    CHANGED
    
    
| @@ -30,13 +30,6 @@ module ActiveReporting | |
| 30 30 | 
             
                            end
         | 
| 31 31 | 
             
                end
         | 
| 32 32 |  | 
| 33 | 
            -
                # Whether the dimension is a datetime column
         | 
| 34 | 
            -
                #
         | 
| 35 | 
            -
                # @return [Boolean]
         | 
| 36 | 
            -
                def datetime?
         | 
| 37 | 
            -
                  @datetime ||= type == TYPES[:degenerate] && model.column_for_attribute(@name).type == :datetime
         | 
| 38 | 
            -
                end
         | 
| 39 | 
            -
             | 
| 40 33 | 
             
                # Tells if the dimension is hierarchical
         | 
| 41 34 | 
             
                #
         | 
| 42 35 | 
             
                # @return [Boolean]
         | 
| @@ -85,6 +85,7 @@ module ActiveReporting | |
| 85 85 | 
             
                #
         | 
| 86 86 | 
             
                # @return [Symbol]
         | 
| 87 87 | 
             
                def self.dimension_label
         | 
| 88 | 
            +
                  @dimension_label ||= nil
         | 
| 88 89 | 
             
                  @dimension_label || Configuration.default_dimension_label
         | 
| 89 90 | 
             
                end
         | 
| 90 91 |  | 
| @@ -155,6 +156,7 @@ module ActiveReporting | |
| 155 156 | 
             
                #
         | 
| 156 157 | 
             
                # @return [Boolean]
         | 
| 157 158 | 
             
                def self.ransack_fallback
         | 
| 159 | 
            +
                  @ransack_fallback ||= false
         | 
| 158 160 | 
             
                  @ransack_fallback || Configuration.ransack_fallback
         | 
| 159 161 | 
             
                end
         | 
| 160 162 | 
             
                private_class_method :ransack_fallback
         | 
| @@ -36,7 +36,7 @@ module ActiveReporting | |
| 36 36 | 
             
                private ######################################################################
         | 
| 37 37 |  | 
| 38 38 | 
             
                def build_data
         | 
| 39 | 
            -
                  @data = model.connection.exec_query(statement.to_sql). | 
| 39 | 
            +
                  @data = model.connection.exec_query(statement.to_sql).to_a
         | 
| 40 40 | 
             
                  apply_dimension_callbacks
         | 
| 41 41 | 
             
                  @data
         | 
| 42 42 | 
             
                end
         | 
| @@ -52,7 +52,8 @@ module ActiveReporting | |
| 52 52 | 
             
                def statement
         | 
| 53 53 | 
             
                  parts = {
         | 
| 54 54 | 
             
                    select: select_statement,
         | 
| 55 | 
            -
                    joins: dimension_joins,
         | 
| 55 | 
            +
                    joins: dimension_joins(ReportingDimension::JOIN_METHODS[:joins]),
         | 
| 56 | 
            +
                    left_outer_joins: dimension_joins(ReportingDimension::JOIN_METHODS[:left_outer_joins]),
         | 
| 56 57 | 
             
                    group: group_by_statement,
         | 
| 57 58 | 
             
                    having: having_statement,
         | 
| 58 59 | 
             
                    order: order_by_statement
         | 
| @@ -84,8 +85,9 @@ module ActiveReporting | |
| 84 85 | 
             
                  end
         | 
| 85 86 | 
             
                end
         | 
| 86 87 |  | 
| 87 | 
            -
                def dimension_joins
         | 
| 88 | 
            -
                  @dimensions.select { |d| d.type == :standard  | 
| 88 | 
            +
                def dimension_joins(join_method)
         | 
| 89 | 
            +
                  @dimensions.select { |d| d.type == Dimension::TYPES[:standard] && d.join_method == join_method }.
         | 
| 90 | 
            +
                              map { |d| d.name.to_sym }
         | 
| 89 91 | 
             
                end
         | 
| 90 92 |  | 
| 91 93 | 
             
                def group_by_statement
         | 
| @@ -142,8 +144,9 @@ module ActiveReporting | |
| 142 144 | 
             
                  @dimensions.each do |dimension|
         | 
| 143 145 | 
             
                    callback = dimension.label_callback
         | 
| 144 146 | 
             
                    next unless callback
         | 
| 147 | 
            +
                    key = "#{dimension.name}_#{dimension.label}"
         | 
| 145 148 | 
             
                    @data.each do |hash|
         | 
| 146 | 
            -
                      hash[ | 
| 149 | 
            +
                      hash[key] = callback.call(hash[key])
         | 
| 147 150 | 
             
                    end
         | 
| 148 151 | 
             
                  end
         | 
| 149 152 | 
             
                end
         | 
| @@ -4,21 +4,32 @@ require 'forwardable' | |
| 4 4 | 
             
            module ActiveReporting
         | 
| 5 5 | 
             
              class ReportingDimension
         | 
| 6 6 | 
             
                extend Forwardable
         | 
| 7 | 
            -
                SUPPORTED_DBS = %w[PostgreSQL PostGIS].freeze
         | 
| 7 | 
            +
                SUPPORTED_DBS = %w[PostgreSQL PostGIS Mysql2].freeze
         | 
| 8 8 | 
             
                # Values for the Postgres `date_trunc` method.
         | 
| 9 9 | 
             
                # See https://www.postgresql.org/docs/10/static/functions-datetime.html#FUNCTIONS-DATETIME-TRUNC
         | 
| 10 10 | 
             
                DATETIME_HIERARCHIES = %i[microseconds milliseconds second minute hour day week month quarter year decade
         | 
| 11 | 
            -
                                          century millennium].freeze
         | 
| 12 | 
            -
                 | 
| 11 | 
            +
                                          century millennium date].freeze
         | 
| 12 | 
            +
                JOIN_METHODS = { joins: :joins, left_outer_joins: :left_outer_joins }.freeze
         | 
| 13 | 
            +
                attr_reader :join_method, :label
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                def_delegators :@dimension, :name, :type, :klass, :association, :model, :hierarchical?
         | 
| 13 16 |  | 
| 14 17 | 
             
                def self.build_from_dimensions(fact_model, dimensions)
         | 
| 15 18 | 
             
                  Array(dimensions).map do |dim|
         | 
| 16 | 
            -
                    dimension_name,  | 
| 19 | 
            +
                    dimension_name, options = dim.is_a?(Hash) ? Array(dim).flatten : [dim, nil]
         | 
| 17 20 | 
             
                    found_dimension = fact_model.dimensions[dimension_name.to_sym]
         | 
| 18 21 |  | 
| 19 | 
            -
                    raise(UnknownDimension, "Dimension '#{ | 
| 20 | 
            -
             | 
| 21 | 
            -
                     | 
| 22 | 
            +
                    raise(UnknownDimension, "Dimension '#{dimension_name}' not found on fact model '#{fact_model}'") if found_dimension.nil?
         | 
| 23 | 
            +
             | 
| 24 | 
            +
                    # Ambiguous behavior with string option for degenerate and standard dimension
         | 
| 25 | 
            +
                    if !options.is_a?(Hash) && found_dimension.type == Dimension::TYPES[:degenerate]
         | 
| 26 | 
            +
                      ActiveSupport::Deprecation.warn <<~EOS
         | 
| 27 | 
            +
                        direct use of implict hierarchies is deprecated and will be removed in future versions. \
         | 
| 28 | 
            +
                        Please use `:datetime_drill` option instead.
         | 
| 29 | 
            +
                      EOS
         | 
| 30 | 
            +
                      options = { datetime_drill: options }
         | 
| 31 | 
            +
                    end
         | 
| 32 | 
            +
                    new(found_dimension, **label_config(options))
         | 
| 22 33 | 
             
                  end
         | 
| 23 34 | 
             
                end
         | 
| 24 35 |  | 
| @@ -26,24 +37,30 @@ module ActiveReporting | |
| 26 37 | 
             
                # the field on that dimension. With a hash you can
         | 
| 27 38 | 
             
                # customize the name of the label
         | 
| 28 39 | 
             
                #
         | 
| 29 | 
            -
                # @param [Symbol|Hash]  | 
| 30 | 
            -
                def self.label_config( | 
| 31 | 
            -
                   | 
| 40 | 
            +
                # @param [Symbol|Hash] options
         | 
| 41 | 
            +
                def self.label_config(options)
         | 
| 42 | 
            +
                  unless options.is_a?(Hash)
         | 
| 43 | 
            +
                    return { label: options }
         | 
| 44 | 
            +
                  end
         | 
| 32 45 |  | 
| 33 46 | 
             
                  {
         | 
| 34 | 
            -
                    label:  | 
| 35 | 
            -
                    label_name:  | 
| 47 | 
            +
                    label: options[:field],
         | 
| 48 | 
            +
                    label_name: options[:name],
         | 
| 49 | 
            +
                    join_method: options[:join_method],
         | 
| 50 | 
            +
                    datetime_drill: options[:datetime_drill]
         | 
| 36 51 | 
             
                  }
         | 
| 37 52 | 
             
                end
         | 
| 38 53 |  | 
| 39 54 | 
             
                # @param dimension [ActiveReporting::Dimension]
         | 
| 40 55 | 
             
                # @option label [Maybe<Symbol>] Hierarchical dimension to be used as a label
         | 
| 41 56 | 
             
                # @option label_name [Maybe<Symbol|String>] Hierarchical dimension custom name
         | 
| 42 | 
            -
                def initialize(dimension, label: nil, label_name: nil)
         | 
| 57 | 
            +
                def initialize(dimension, label: nil, label_name: nil, join_method: nil, datetime_drill: nil)
         | 
| 43 58 | 
             
                  @dimension = dimension
         | 
| 44 59 |  | 
| 45 60 | 
             
                  determine_label_field(label)
         | 
| 61 | 
            +
                  determine_datetime_drill(datetime_drill)
         | 
| 46 62 | 
             
                  determine_label_name(label_name)
         | 
| 63 | 
            +
                  determine_join_method(join_method)
         | 
| 47 64 | 
             
                end
         | 
| 48 65 |  | 
| 49 66 | 
             
                # The foreign key to use in queries
         | 
| @@ -57,10 +74,8 @@ module ActiveReporting | |
| 57 74 | 
             
                #
         | 
| 58 75 | 
             
                # @return [Array]
         | 
| 59 76 | 
             
                def select_statement(with_identifier: true)
         | 
| 60 | 
            -
                   | 
| 61 | 
            -
             | 
| 62 | 
            -
                  ss = ["#{label_fragment} AS #{@label_name}"]
         | 
| 63 | 
            -
                  ss << "#{identifier_fragment} AS #{name}_identifier" if with_identifier
         | 
| 77 | 
            +
                  ss = ["#{label_fragment} AS #{label_fragment_alias}"]
         | 
| 78 | 
            +
                  ss << "#{identifier_fragment} AS #{identifier_fragment_alias}" if with_identifier && type == Dimension::TYPES[:standard]
         | 
| 64 79 | 
             
                  ss
         | 
| 65 80 | 
             
                end
         | 
| 66 81 |  | 
| @@ -68,10 +83,8 @@ module ActiveReporting | |
| 68 83 | 
             
                #
         | 
| 69 84 | 
             
                # @return [Array]
         | 
| 70 85 | 
             
                def group_by_statement(with_identifier: true)
         | 
| 71 | 
            -
                  return [degenerate_fragment] if type == Dimension::TYPES[:degenerate]
         | 
| 72 | 
            -
             | 
| 73 86 | 
             
                  group = [label_fragment]
         | 
| 74 | 
            -
                  group << identifier_fragment if with_identifier
         | 
| 87 | 
            +
                  group << identifier_fragment if with_identifier && type == Dimension::TYPES[:standard]
         | 
| 75 88 | 
             
                  group
         | 
| 76 89 | 
             
                end
         | 
| 77 90 |  | 
| @@ -81,7 +94,6 @@ module ActiveReporting | |
| 81 94 | 
             
                def order_by_statement(direction:)
         | 
| 82 95 | 
             
                  direction = direction.to_s.upcase
         | 
| 83 96 | 
             
                  raise "Ording direction should be 'asc' or 'desc'" unless %w[ASC DESC].include?(direction)
         | 
| 84 | 
            -
                  return "#{degenerate_fragment} #{direction}" if type == Dimension::TYPES[:degenerate]
         | 
| 85 97 | 
             
                  "#{label_fragment} #{direction}"
         | 
| 86 98 | 
             
                end
         | 
| 87 99 |  | 
| @@ -96,24 +108,47 @@ module ActiveReporting | |
| 96 108 |  | 
| 97 109 | 
             
                def determine_label_field(label_field)
         | 
| 98 110 | 
             
                  @label = if label_field.present? && validate_hierarchical_label(label_field)
         | 
| 99 | 
            -
                             label_field.to_sym
         | 
| 111 | 
            +
                             type == Dimension::TYPES[:degenerate] ? name : label_field.to_sym
         | 
| 112 | 
            +
                           elsif type == Dimension::TYPES[:degenerate]
         | 
| 113 | 
            +
                             name
         | 
| 100 114 | 
             
                           else
         | 
| 101 115 | 
             
                             dimension_fact_model.dimension_label || Configuration.default_dimension_label
         | 
| 102 116 | 
             
                           end
         | 
| 103 117 | 
             
                end
         | 
| 104 118 |  | 
| 105 119 | 
             
                def determine_label_name(label_name)
         | 
| 106 | 
            -
             | 
| 120 | 
            +
             | 
| 121 | 
            +
                  if label_name
         | 
| 122 | 
            +
                    @label_name = label_name
         | 
| 123 | 
            +
                  else
         | 
| 124 | 
            +
                    @label_name = name
         | 
| 125 | 
            +
                    @label_name += "_#{@label}" if (type == Dimension::TYPES[:standard] && @label != :name)
         | 
| 126 | 
            +
                    @label_name += "_#{@datetime_drill}" if @datetime_drill
         | 
| 127 | 
            +
                  end
         | 
| 128 | 
            +
                  @label_name
         | 
| 107 129 | 
             
                end
         | 
| 108 130 |  | 
| 109 | 
            -
                def  | 
| 110 | 
            -
                   | 
| 111 | 
            -
             | 
| 112 | 
            -
             | 
| 131 | 
            +
                def determine_datetime_drill(datetime_drill)
         | 
| 132 | 
            +
                  return unless datetime_drill
         | 
| 133 | 
            +
                  validate_supported_database_for_datetime_hierarchies
         | 
| 134 | 
            +
                  validate_against_datetime_hierarchies(datetime_drill)
         | 
| 135 | 
            +
                  validate_label_is_datetime
         | 
| 136 | 
            +
                  @datetime_drill = datetime_drill
         | 
| 137 | 
            +
                end
         | 
| 138 | 
            +
             | 
| 139 | 
            +
                def determine_join_method(join_method)
         | 
| 140 | 
            +
                  if join_method.blank?
         | 
| 141 | 
            +
                    @join_method = ReportingDimension::JOIN_METHODS[:joins]
         | 
| 142 | 
            +
                  elsif ReportingDimension::JOIN_METHODS.include?(join_method)
         | 
| 143 | 
            +
                    @join_method = join_method
         | 
| 113 144 | 
             
                  else
         | 
| 114 | 
            -
                     | 
| 115 | 
            -
                    validate_against_fact_model_properties(hierarchical_label)
         | 
| 145 | 
            +
                    raise UnknownJoinMethod, "Method '#{join_method}' not included in '#{ReportingDimension::JOIN_METHODS.values}'"
         | 
| 116 146 | 
             
                  end
         | 
| 147 | 
            +
                end
         | 
| 148 | 
            +
             | 
| 149 | 
            +
                def validate_hierarchical_label(hierarchical_label)
         | 
| 150 | 
            +
                  validate_dimension_is_hierachical(hierarchical_label)
         | 
| 151 | 
            +
                  validate_against_fact_model_properties(hierarchical_label)
         | 
| 117 152 | 
             
                  true
         | 
| 118 153 | 
             
                end
         | 
| 119 154 |  | 
| @@ -134,27 +169,82 @@ module ActiveReporting | |
| 134 169 | 
             
                  raise InvalidDimensionLabel, "#{hierarchical_label} is not a valid datetime grouping label in #{name}"
         | 
| 135 170 | 
             
                end
         | 
| 136 171 |  | 
| 172 | 
            +
                def validate_label_is_datetime
         | 
| 173 | 
            +
                  return if dimension_fact_model.model.column_for_attribute(@label).type == :datetime
         | 
| 174 | 
            +
                  raise InvalidDimensionLabel, "'#{@label}' is not a datetime column"
         | 
| 175 | 
            +
                end
         | 
| 176 | 
            +
             | 
| 137 177 | 
             
                def validate_against_fact_model_properties(hierarchical_label)
         | 
| 138 178 | 
             
                  return if dimension_fact_model.hierarchical_levels.include?(hierarchical_label.to_sym)
         | 
| 139 179 | 
             
                  raise InvalidDimensionLabel, "#{hierarchical_label} is not a hierarchical label in #{name}"
         | 
| 140 180 | 
             
                end
         | 
| 141 181 |  | 
| 142 | 
            -
                def  | 
| 143 | 
            -
                   | 
| 144 | 
            -
             | 
| 182 | 
            +
                def datetime_drill_label_fragment(column)
         | 
| 183 | 
            +
                  if model.connection.adapter_name == "Mysql2"
         | 
| 184 | 
            +
                    datetime_drill_mysql(column)
         | 
| 185 | 
            +
                  else # Postgress
         | 
| 186 | 
            +
                    datetime_drill_postgress(column)
         | 
| 187 | 
            +
                  end
         | 
| 145 188 | 
             
                end
         | 
| 146 189 |  | 
| 147 | 
            -
                def  | 
| 148 | 
            -
                   | 
| 149 | 
            -
                   | 
| 190 | 
            +
                def datetime_drill_postgress(column)
         | 
| 191 | 
            +
                  case @datetime_drill.to_sym
         | 
| 192 | 
            +
                  when :date
         | 
| 193 | 
            +
                    "DATE('#{column}')"
         | 
| 194 | 
            +
                  else
         | 
| 195 | 
            +
                    "DATE_TRUNC('#{@datetime_drill}', #{column})"
         | 
| 196 | 
            +
                  end
         | 
| 197 | 
            +
                end
         | 
| 198 | 
            +
             | 
| 199 | 
            +
                def datetime_drill_mysql(column)
         | 
| 200 | 
            +
                  case @datetime_drill.to_sym
         | 
| 201 | 
            +
                  when :microseconds
         | 
| 202 | 
            +
                    "MICROSECOND(#{column})"
         | 
| 203 | 
            +
                  when :milliseconds
         | 
| 204 | 
            +
                    "MICROSECOND(#{column}) DIV 1000"
         | 
| 205 | 
            +
                  when :second
         | 
| 206 | 
            +
                    "SECOND(#{column})"
         | 
| 207 | 
            +
                  when :minute
         | 
| 208 | 
            +
                    "MINUTE(#{column})"
         | 
| 209 | 
            +
                  when :hour
         | 
| 210 | 
            +
                    "HOUR(#{column})"
         | 
| 211 | 
            +
                  when :day
         | 
| 212 | 
            +
                    "DAY(#{column})"
         | 
| 213 | 
            +
                  when :week
         | 
| 214 | 
            +
                    "WEEKDAY(#{column})"
         | 
| 215 | 
            +
                  when :month
         | 
| 216 | 
            +
                    "MONTH(#{column})"
         | 
| 217 | 
            +
                  when :quarter
         | 
| 218 | 
            +
                    "QUARTER(#{column})"
         | 
| 219 | 
            +
                  when :year
         | 
| 220 | 
            +
                    "YEAR(#{column})"
         | 
| 221 | 
            +
                  when :decade
         | 
| 222 | 
            +
                    "YEAR(#{column}) DIV 10"
         | 
| 223 | 
            +
                  when :century
         | 
| 224 | 
            +
                    "YEAR(#{column}) DIV 100"
         | 
| 225 | 
            +
                  when :millennium
         | 
| 226 | 
            +
                    "YEAR(#{column}) DIV 1000"
         | 
| 227 | 
            +
                  when :date
         | 
| 228 | 
            +
                    "DATE(#{column})"
         | 
| 229 | 
            +
                  end
         | 
| 150 230 | 
             
                end
         | 
| 151 231 |  | 
| 152 232 | 
             
                def identifier_fragment
         | 
| 153 | 
            -
                  "#{klass.quoted_table_name}.#{klass.primary_key}"
         | 
| 233 | 
            +
                  "#{klass.quoted_table_name}.#{model.connection.quote_column_name(klass.primary_key)}"
         | 
| 234 | 
            +
                end
         | 
| 235 | 
            +
             | 
| 236 | 
            +
                def identifier_fragment_alias
         | 
| 237 | 
            +
                  "#{model.connection.quote_column_name("#{name}_identifier")}"
         | 
| 154 238 | 
             
                end
         | 
| 155 239 |  | 
| 156 240 | 
             
                def label_fragment
         | 
| 157 | 
            -
                  "#{klass.quoted_table_name}.#{@label}"
         | 
| 241 | 
            +
                  fragment = "#{klass.quoted_table_name}.#{model.connection.quote_column_name(@label)}"
         | 
| 242 | 
            +
                  fragment = datetime_drill_label_fragment(fragment) if @datetime_drill
         | 
| 243 | 
            +
                  fragment
         | 
| 244 | 
            +
                end
         | 
| 245 | 
            +
             | 
| 246 | 
            +
                def label_fragment_alias
         | 
| 247 | 
            +
                  "#{model.connection.quote_column_name(@label_name)}"
         | 
| 158 248 | 
             
                end
         | 
| 159 249 |  | 
| 160 250 | 
             
                def dimension_fact_model
         | 
    
        metadata
    CHANGED
    
    | @@ -1,14 +1,14 @@ | |
| 1 1 | 
             
            --- !ruby/object:Gem::Specification
         | 
| 2 2 | 
             
            name: active_reporting
         | 
| 3 3 | 
             
            version: !ruby/object:Gem::Version
         | 
| 4 | 
            -
              version: 0. | 
| 4 | 
            +
              version: 0.6.1
         | 
| 5 5 | 
             
            platform: ruby
         | 
| 6 6 | 
             
            authors:
         | 
| 7 7 | 
             
            - Tony Drake
         | 
| 8 8 | 
             
            autorequire: 
         | 
| 9 9 | 
             
            bindir: exe
         | 
| 10 10 | 
             
            cert_chain: []
         | 
| 11 | 
            -
            date:  | 
| 11 | 
            +
            date: 2020-08-29 00:00:00.000000000 Z
         | 
| 12 12 | 
             
            dependencies:
         | 
| 13 13 | 
             
            - !ruby/object:Gem::Dependency
         | 
| 14 14 | 
             
              name: activerecord
         | 
| @@ -98,16 +98,16 @@ dependencies: | |
| 98 98 | 
             
              name: rake
         | 
| 99 99 | 
             
              requirement: !ruby/object:Gem::Requirement
         | 
| 100 100 | 
             
                requirements:
         | 
| 101 | 
            -
                - - " | 
| 101 | 
            +
                - - ">="
         | 
| 102 102 | 
             
                  - !ruby/object:Gem::Version
         | 
| 103 | 
            -
                    version: ' | 
| 103 | 
            +
                    version: '0'
         | 
| 104 104 | 
             
              type: :development
         | 
| 105 105 | 
             
              prerelease: false
         | 
| 106 106 | 
             
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 107 107 | 
             
                requirements:
         | 
| 108 | 
            -
                - - " | 
| 108 | 
            +
                - - ">="
         | 
| 109 109 | 
             
                  - !ruby/object:Gem::Version
         | 
| 110 | 
            -
                    version: ' | 
| 110 | 
            +
                    version: '0'
         | 
| 111 111 | 
             
            - !ruby/object:Gem::Dependency
         | 
| 112 112 | 
             
              name: ransack
         | 
| 113 113 | 
             
              requirement: !ruby/object:Gem::Requirement
         | 
| @@ -185,7 +185,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement | |
| 185 185 | 
             
                - !ruby/object:Gem::Version
         | 
| 186 186 | 
             
                  version: '0'
         | 
| 187 187 | 
             
            requirements: []
         | 
| 188 | 
            -
            rubygems_version: 3.0. | 
| 188 | 
            +
            rubygems_version: 3.0.3
         | 
| 189 189 | 
             
            signing_key: 
         | 
| 190 190 | 
             
            specification_version: 4
         | 
| 191 191 | 
             
            summary: Add relational OLAP-like functionality for ActiveRecord
         |