groupdate 4.1.2 → 6.0.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.
data/lib/groupdate.rb CHANGED
@@ -1,20 +1,28 @@
1
+ # dependencies
2
+ require "active_support"
1
3
  require "active_support/core_ext/module/attribute_accessors"
2
4
  require "active_support/time"
3
- require "groupdate/version"
4
- require "groupdate/relation_builder"
5
- require "groupdate/series_builder"
5
+
6
+ # modules
6
7
  require "groupdate/magic"
8
+ require "groupdate/series_builder"
9
+ require "groupdate/version"
10
+
11
+ # adapters
12
+ require "groupdate/adapters/base_adapter"
13
+ require "groupdate/adapters/mysql_adapter"
14
+ require "groupdate/adapters/postgresql_adapter"
15
+ require "groupdate/adapters/sqlite_adapter"
7
16
 
8
17
  module Groupdate
9
18
  class Error < RuntimeError; end
10
19
 
11
- PERIODS = [:second, :minute, :hour, :day, :week, :month, :quarter, :year, :day_of_week, :hour_of_day, :minute_of_hour, :day_of_month, :month_of_year]
20
+ PERIODS = [:second, :minute, :hour, :day, :week, :month, :quarter, :year, :day_of_week, :hour_of_day, :minute_of_hour, :day_of_month, :day_of_year, :month_of_year]
12
21
  METHODS = PERIODS.map { |v| :"group_by_#{v}" } + [:group_by_period]
13
22
 
14
- mattr_accessor :week_start, :day_start, :time_zone, :dates
15
- self.week_start = :sun
23
+ mattr_accessor :week_start, :day_start, :time_zone
24
+ self.week_start = :sunday
16
25
  self.day_start = 0
17
- self.dates = true
18
26
 
19
27
  # api for gems like ActiveMedian
20
28
  def self.process_result(relation, result, **options)
@@ -23,8 +31,22 @@ module Groupdate
23
31
  end
24
32
  result
25
33
  end
34
+
35
+ def self.adapters
36
+ @adapters ||= {}
37
+ end
38
+
39
+ def self.register_adapter(name, adapter)
40
+ Array(name).each do |n|
41
+ adapters[n] = adapter
42
+ end
43
+ end
26
44
  end
27
45
 
46
+ Groupdate.register_adapter ["Mysql2", "Mysql2Spatial", "Mysql2Rgeo"], Groupdate::Adapters::MySQLAdapter
47
+ Groupdate.register_adapter ["PostgreSQL", "PostGIS", "Redshift"], Groupdate::Adapters::PostgreSQLAdapter
48
+ Groupdate.register_adapter "SQLite", Groupdate::Adapters::SQLiteAdapter
49
+
28
50
  require "groupdate/enumerable"
29
51
 
30
52
  ActiveSupport.on_load(:active_record) do
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: groupdate
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.1.2
4
+ version: 6.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Kane
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-05-27 00:00:00.000000000 Z
11
+ date: 2022-01-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -16,114 +16,16 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: '4.2'
19
+ version: '5.2'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: '4.2'
27
- - !ruby/object:Gem::Dependency
28
- name: bundler
29
- requirement: !ruby/object:Gem::Requirement
30
- requirements:
31
- - - ">="
32
- - !ruby/object:Gem::Version
33
- version: '0'
34
- type: :development
35
- prerelease: false
36
- version_requirements: !ruby/object:Gem::Requirement
37
- requirements:
38
- - - ">="
39
- - !ruby/object:Gem::Version
40
- version: '0'
41
- - !ruby/object:Gem::Dependency
42
- name: rake
43
- requirement: !ruby/object:Gem::Requirement
44
- requirements:
45
- - - ">="
46
- - !ruby/object:Gem::Version
47
- version: '0'
48
- type: :development
49
- prerelease: false
50
- version_requirements: !ruby/object:Gem::Requirement
51
- requirements:
52
- - - ">="
53
- - !ruby/object:Gem::Version
54
- version: '0'
55
- - !ruby/object:Gem::Dependency
56
- name: minitest
57
- requirement: !ruby/object:Gem::Requirement
58
- requirements:
59
- - - ">="
60
- - !ruby/object:Gem::Version
61
- version: '0'
62
- type: :development
63
- prerelease: false
64
- version_requirements: !ruby/object:Gem::Requirement
65
- requirements:
66
- - - ">="
67
- - !ruby/object:Gem::Version
68
- version: '0'
69
- - !ruby/object:Gem::Dependency
70
- name: activerecord
71
- requirement: !ruby/object:Gem::Requirement
72
- requirements:
73
- - - ">="
74
- - !ruby/object:Gem::Version
75
- version: '0'
76
- type: :development
77
- prerelease: false
78
- version_requirements: !ruby/object:Gem::Requirement
79
- requirements:
80
- - - ">="
81
- - !ruby/object:Gem::Version
82
- version: '0'
83
- - !ruby/object:Gem::Dependency
84
- name: pg
85
- requirement: !ruby/object:Gem::Requirement
86
- requirements:
87
- - - "<"
88
- - !ruby/object:Gem::Version
89
- version: '1'
90
- type: :development
91
- prerelease: false
92
- version_requirements: !ruby/object:Gem::Requirement
93
- requirements:
94
- - - "<"
95
- - !ruby/object:Gem::Version
96
- version: '1'
97
- - !ruby/object:Gem::Dependency
98
- name: mysql2
99
- requirement: !ruby/object:Gem::Requirement
100
- requirements:
101
- - - "<"
102
- - !ruby/object:Gem::Version
103
- version: '0.5'
104
- type: :development
105
- prerelease: false
106
- version_requirements: !ruby/object:Gem::Requirement
107
- requirements:
108
- - - "<"
109
- - !ruby/object:Gem::Version
110
- version: '0.5'
111
- - !ruby/object:Gem::Dependency
112
- name: sqlite3
113
- requirement: !ruby/object:Gem::Requirement
114
- requirements:
115
- - - "~>"
116
- - !ruby/object:Gem::Version
117
- version: 1.3.0
118
- type: :development
119
- prerelease: false
120
- version_requirements: !ruby/object:Gem::Requirement
121
- requirements:
122
- - - "~>"
123
- - !ruby/object:Gem::Version
124
- version: 1.3.0
125
- description:
126
- email: andrew@chartkick.com
26
+ version: '5.2'
27
+ description:
28
+ email: andrew@ankane.org
127
29
  executables: []
128
30
  extensions: []
129
31
  extra_rdoc_files: []
@@ -134,18 +36,21 @@ files:
134
36
  - README.md
135
37
  - lib/groupdate.rb
136
38
  - lib/groupdate/active_record.rb
39
+ - lib/groupdate/adapters/base_adapter.rb
40
+ - lib/groupdate/adapters/mysql_adapter.rb
41
+ - lib/groupdate/adapters/postgresql_adapter.rb
42
+ - lib/groupdate/adapters/sqlite_adapter.rb
137
43
  - lib/groupdate/enumerable.rb
138
44
  - lib/groupdate/magic.rb
139
45
  - lib/groupdate/query_methods.rb
140
46
  - lib/groupdate/relation.rb
141
- - lib/groupdate/relation_builder.rb
142
47
  - lib/groupdate/series_builder.rb
143
48
  - lib/groupdate/version.rb
144
49
  homepage: https://github.com/ankane/groupdate
145
50
  licenses:
146
51
  - MIT
147
52
  metadata: {}
148
- post_install_message:
53
+ post_install_message:
149
54
  rdoc_options: []
150
55
  require_paths:
151
56
  - lib
@@ -153,15 +58,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
153
58
  requirements:
154
59
  - - ">="
155
60
  - !ruby/object:Gem::Version
156
- version: '2.2'
61
+ version: '2.6'
157
62
  required_rubygems_version: !ruby/object:Gem::Requirement
158
63
  requirements:
159
64
  - - ">="
160
65
  - !ruby/object:Gem::Version
161
66
  version: '0'
162
67
  requirements: []
163
- rubygems_version: 3.0.3
164
- signing_key:
68
+ rubygems_version: 3.2.32
69
+ signing_key:
165
70
  specification_version: 4
166
71
  summary: The simplest way to group temporal data
167
72
  test_files: []
@@ -1,186 +0,0 @@
1
- module Groupdate
2
- class RelationBuilder
3
- attr_reader :period, :column, :day_start, :week_start
4
-
5
- def initialize(relation, column:, period:, time_zone:, time_range:, week_start:, day_start:)
6
- @relation = relation
7
- @column = resolve_column(relation, column)
8
- @period = period
9
- @time_zone = time_zone
10
- @time_range = time_range
11
- @week_start = week_start
12
- @day_start = day_start
13
-
14
- if relation.default_timezone == :local
15
- raise Groupdate::Error, "ActiveRecord::Base.default_timezone must be :utc to use Groupdate"
16
- end
17
- end
18
-
19
- def generate
20
- @relation.group(group_clause).where(*where_clause)
21
- end
22
-
23
- private
24
-
25
- def group_clause
26
- time_zone = @time_zone.tzinfo.name
27
- adapter_name = @relation.connection.adapter_name
28
- query =
29
- case adapter_name
30
- when "MySQL", "Mysql2", "Mysql2Spatial", 'Mysql2Rgeo'
31
- case period
32
- when :day_of_week
33
- ["DAYOFWEEK(CONVERT_TZ(DATE_SUB(#{column}, INTERVAL #{day_start} second), '+00:00', ?)) - 1", time_zone]
34
- when :hour_of_day
35
- ["(EXTRACT(HOUR from CONVERT_TZ(#{column}, '+00:00', ?)) + 24 - #{day_start / 3600}) % 24", time_zone]
36
- when :minute_of_hour
37
- ["(EXTRACT(MINUTE from CONVERT_TZ(#{column}, '+00:00', ?)))", time_zone]
38
- when :day_of_month
39
- ["DAYOFMONTH(CONVERT_TZ(DATE_SUB(#{column}, INTERVAL #{day_start} second), '+00:00', ?))", time_zone]
40
- when :month_of_year
41
- ["MONTH(CONVERT_TZ(DATE_SUB(#{column}, INTERVAL #{day_start} second), '+00:00', ?))", time_zone]
42
- when :week
43
- ["CONVERT_TZ(DATE_FORMAT(CONVERT_TZ(DATE_SUB(#{column}, INTERVAL ((#{7 - week_start} + WEEKDAY(CONVERT_TZ(#{column}, '+00:00', ?) - INTERVAL #{day_start} second)) % 7) DAY) - INTERVAL #{day_start} second, '+00:00', ?), '%Y-%m-%d 00:00:00') + INTERVAL #{day_start} second, ?, '+00:00')", time_zone, time_zone, time_zone]
44
- when :quarter
45
- ["DATE_ADD(CONVERT_TZ(DATE_FORMAT(DATE(CONCAT(EXTRACT(YEAR FROM CONVERT_TZ(DATE_SUB(#{column}, INTERVAL #{day_start} second), '+00:00', ?)), '-', LPAD(1 + 3 * (QUARTER(CONVERT_TZ(DATE_SUB(#{column}, INTERVAL #{day_start} second), '+00:00', ?)) - 1), 2, '00'), '-01')), '%Y-%m-%d %H:%i:%S'), ?, '+00:00'), INTERVAL #{day_start} second)", time_zone, time_zone, time_zone]
46
- else
47
- format =
48
- case period
49
- when :second
50
- "%Y-%m-%d %H:%i:%S"
51
- when :minute
52
- "%Y-%m-%d %H:%i:00"
53
- when :hour
54
- "%Y-%m-%d %H:00:00"
55
- when :day
56
- "%Y-%m-%d 00:00:00"
57
- when :month
58
- "%Y-%m-01 00:00:00"
59
- else # year
60
- "%Y-01-01 00:00:00"
61
- end
62
-
63
- ["DATE_ADD(CONVERT_TZ(DATE_FORMAT(CONVERT_TZ(DATE_SUB(#{column}, INTERVAL #{day_start} second), '+00:00', ?), '#{format}'), ?, '+00:00'), INTERVAL #{day_start} second)", time_zone, time_zone]
64
- end
65
- when "PostgreSQL", "PostGIS"
66
- case period
67
- when :day_of_week
68
- ["EXTRACT(DOW from #{column}::timestamptz AT TIME ZONE ? - INTERVAL '#{day_start} second')::integer", time_zone]
69
- when :hour_of_day
70
- ["EXTRACT(HOUR from #{column}::timestamptz AT TIME ZONE ? - INTERVAL '#{day_start} second')::integer", time_zone]
71
- when :minute_of_hour
72
- ["EXTRACT(MINUTE from #{column}::timestamptz AT TIME ZONE ? - INTERVAL '#{day_start} second')::integer", time_zone]
73
- when :day_of_month
74
- ["EXTRACT(DAY from #{column}::timestamptz AT TIME ZONE ? - INTERVAL '#{day_start} second')::integer", time_zone]
75
- when :month_of_year
76
- ["EXTRACT(MONTH from #{column}::timestamptz AT TIME ZONE ? - INTERVAL '#{day_start} second')::integer", time_zone]
77
- when :week # start on Sunday, not PostgreSQL default Monday
78
- ["(DATE_TRUNC('#{period}', (#{column}::timestamptz - INTERVAL '#{week_start} day' - INTERVAL '#{day_start} second') AT TIME ZONE ?) + INTERVAL '#{week_start} day' + INTERVAL '#{day_start} second') AT TIME ZONE ?", time_zone, time_zone]
79
- else
80
- ["(DATE_TRUNC('#{period}', (#{column}::timestamptz - INTERVAL '#{day_start} second') AT TIME ZONE ?) + INTERVAL '#{day_start} second') AT TIME ZONE ?", time_zone, time_zone]
81
- end
82
- when "SQLite"
83
- raise Groupdate::Error, "Time zones not supported for SQLite" unless @time_zone.utc_offset.zero?
84
- raise Groupdate::Error, "day_start not supported for SQLite" unless day_start.zero?
85
- raise Groupdate::Error, "week_start not supported for SQLite" unless week_start == 6
86
-
87
- if period == :week
88
- ["strftime('%%Y-%%m-%%d 00:00:00 UTC', #{column}, '-6 days', 'weekday 0')"]
89
- else
90
- format =
91
- case period
92
- when :hour_of_day
93
- "%H"
94
- when :minute_of_hour
95
- "%M"
96
- when :day_of_week
97
- "%w"
98
- when :day_of_month
99
- "%d"
100
- when :month_of_year
101
- "%m"
102
- when :second
103
- "%Y-%m-%d %H:%M:%S UTC"
104
- when :minute
105
- "%Y-%m-%d %H:%M:00 UTC"
106
- when :hour
107
- "%Y-%m-%d %H:00:00 UTC"
108
- when :day
109
- "%Y-%m-%d 00:00:00 UTC"
110
- when :month
111
- "%Y-%m-01 00:00:00 UTC"
112
- when :quarter
113
- raise Groupdate::Error, "Quarter not supported for SQLite"
114
- else # year
115
- "%Y-01-01 00:00:00 UTC"
116
- end
117
-
118
- ["strftime('#{format.gsub(/%/, '%%')}', #{column})"]
119
- end
120
- when "Redshift"
121
- case period
122
- when :day_of_week
123
- ["EXTRACT(DOW from CONVERT_TIMEZONE(?, #{column}::timestamp) - INTERVAL '#{day_start} second')::integer", time_zone]
124
- when :hour_of_day
125
- ["EXTRACT(HOUR from CONVERT_TIMEZONE(?, #{column}::timestamp) - INTERVAL '#{day_start} second')::integer", time_zone]
126
- when :minute_of_hour
127
- ["EXTRACT(MINUTE from CONVERT_TIMEZONE(?, #{column}::timestamp) - INTERVAL '#{day_start} second')::integer", time_zone]
128
- when :day_of_month
129
- ["EXTRACT(DAY from CONVERT_TIMEZONE(?, #{column}::timestamp) - INTERVAL '#{day_start} second')::integer", time_zone]
130
- when :month_of_year
131
- ["EXTRACT(MONTH from CONVERT_TIMEZONE(?, #{column}::timestamp) - INTERVAL '#{day_start} second')::integer", time_zone]
132
- when :week # start on Sunday, not Redshift default Monday
133
- # Redshift does not return timezone information; it
134
- # always says it is in UTC time, so we must convert
135
- # back to UTC to play properly with the rest of Groupdate.
136
- #
137
- ["CONVERT_TIMEZONE(?, 'Etc/UTC', DATE_TRUNC(?, CONVERT_TIMEZONE(?, #{column}) - INTERVAL '#{week_start} day' - INTERVAL '#{day_start} second'))::timestamp + INTERVAL '#{week_start} day' + INTERVAL '#{day_start} second'", time_zone, period, time_zone]
138
- else
139
- ["CONVERT_TIMEZONE(?, 'Etc/UTC', DATE_TRUNC(?, CONVERT_TIMEZONE(?, #{column}) - INTERVAL '#{day_start} second'))::timestamp + INTERVAL '#{day_start} second'", time_zone, period, time_zone]
140
- end
141
- else
142
- raise Groupdate::Error, "Connection adapter not supported: #{adapter_name}"
143
- end
144
-
145
- if adapter_name == "MySQL" && period == :week
146
- query[0] = "CAST(#{query[0]} AS DATETIME)"
147
- end
148
-
149
- clause = @relation.send(:sanitize_sql_array, query)
150
-
151
- # cleaner queries in logs
152
- clause = clean_group_clause_postgresql(clause)
153
- clean_group_clause_mysql(clause)
154
- end
155
-
156
- def clean_group_clause_postgresql(clause)
157
- clause.gsub(/ (\-|\+) INTERVAL '0 second'/, "")
158
- end
159
-
160
- def clean_group_clause_mysql(clause)
161
- clause = clause.gsub("DATE_SUB(#{column}, INTERVAL 0 second)", "#{column}")
162
- if clause.start_with?("DATE_ADD(") && clause.end_with?(", INTERVAL 0 second)")
163
- clause = clause[9..-21]
164
- end
165
- clause
166
- end
167
-
168
- def where_clause
169
- if @time_range.is_a?(Range)
170
- op = @time_range.exclude_end? ? "<" : "<="
171
- ["#{column} >= ? AND #{column} #{op} ?", @time_range.first, @time_range.last]
172
- else
173
- ["#{column} IS NOT NULL"]
174
- end
175
- end
176
-
177
- # resolves eagerly
178
- # need to convert both where_clause (easy)
179
- # and group_clause (not easy) if want to avoid this
180
- def resolve_column(relation, column)
181
- node = relation.send(:relation).send(:arel_columns, [column]).first
182
- node = Arel::Nodes::SqlLiteral.new(node) if node.is_a?(String)
183
- relation.connection.visitor.accept(node, Arel::Collectors::SQLString.new).value
184
- end
185
- end
186
- end