groupdate 4.1.2 → 6.0.1

Sign up to get free protection for your applications and to get access to all the features.
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