active_median 0.2.8 → 0.3.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9f779fe5268fc1347cd94c47fc55a3322a3d52282539d0ca7d001e37c405f9a1
4
- data.tar.gz: 37d6b834f1857f6119d3ec7969d422f7d4f3fcbeaa1645d8bc0f8a3f10824c04
3
+ metadata.gz: 10eda8c3a8100441196584a8188a6bbd09829e5494d1f39a8faa8e9f77e98fcd
4
+ data.tar.gz: a6c2a49151b3ead3d6e7cf0d2eff4fc8d5d455b7d3fcee35b15645372863f37f
5
5
  SHA512:
6
- metadata.gz: 180be6b9f59bc10e2bc0e708f4d8ad01d4d3d7a27d95707162ef45c4478ff858d6b6b5a2f598b6c411bf0e4942f02f0a13a642b04746e79fb124c2c6d70e58cb
7
- data.tar.gz: db9436660c4fa4e2bdd80af101dbc8723f46781f17eeeecc57e8015a2510d9429bce6f38f93b210c82b2499b3dae787df26c1d0e898280af98174738a29eb984
6
+ metadata.gz: baeda2f4c8fa2b36c5fb29b2afc1daecbbd6a242e9e761b65503eba7868f55d7a9ccd0c39fe645c032cd5aab3f6d79a6ce63bdba6950277fab588883d89911e7
7
+ data.tar.gz: d8b888c42e7731b07c15a283b17ee28e2b6dc0a279f6dd417390b087fd6454bbcf3ff5659a73f3ec27067260499765584952e8e107ecd2be6d858a913d775cea
data/CHANGELOG.md CHANGED
@@ -1,3 +1,10 @@
1
+ ## 0.3.0 (2021-02-08)
2
+
3
+ - Added column alias
4
+ - Added support for percentiles with SQLite (switched extensions)
5
+ - Raise `ActiveRecord::UnknownAttributeReference` for non-attribute arguments
6
+ - Dropped support for Active Record < 5.2
7
+
1
8
  ## 0.2.8 (2021-01-16)
2
9
 
3
10
  - Fixed bug with removing order
data/README.md CHANGED
@@ -44,19 +44,6 @@ Works with grouping, too
44
44
  Order.group(:store_id).median(:total)
45
45
  ```
46
46
 
47
- ## User Input
48
-
49
- If passing user input as the column, be sure to sanitize it first [like you must](https://rails-sqli.org/) with other aggregate methods like `sum`.
50
-
51
- ```ruby
52
- column = params[:column]
53
-
54
- # check against permitted columns
55
- raise "Unpermitted column" unless ["column_a", "column_b"].include?(column)
56
-
57
- User.median(column)
58
- ```
59
-
60
47
  ## Arrays and Hashes
61
48
 
62
49
  Median
@@ -94,10 +81,10 @@ mysql <options> < load.sql
94
81
 
95
82
  ### SQLite
96
83
 
97
- SQLite requires a [community extension](https://www.sqlite.org/contrib). Download [extension-functions.c](https://www.sqlite.org/contrib/download/extension-functions.c) and follow the [instructions for compiling loadable extensions](https://www.sqlite.org/loadext.html#compiling_a_loadable_extension) for your platform. On Mac, use:
84
+ SQLite requires an extension. Download [percentile.c](https://www.sqlite.org/src/file?name=ext/misc/percentile.c&ci=d49c32e6e7cc341b) and follow the [instructions for compiling loadable extensions](https://www.sqlite.org/loadext.html#compiling_a_loadable_extension) for your platform. On Mac, use:
98
85
 
99
86
  ```sh
100
- gcc -g -fPIC -dynamiclib extension-functions.c -o extension-functions.dylib
87
+ gcc -g -fPIC -dynamiclib -L/usr/local/opt/sqlite/lib -lsqlite3 percentile.c -o percentile.dylib
101
88
  ```
102
89
 
103
90
  To load it in Rails, create an initializer with:
@@ -105,15 +92,21 @@ To load it in Rails, create an initializer with:
105
92
  ```ruby
106
93
  db = ActiveRecord::Base.connection.raw_connection
107
94
  db.enable_load_extension(1)
108
- db.load_extension("extension-functions.dylib")
95
+ db.load_extension("percentile.dylib")
109
96
  db.enable_load_extension(0)
110
97
  ```
111
98
 
112
99
  ## Upgrading
113
100
 
114
- ### 0.2.0
101
+ ### 0.3.0
102
+
103
+ ActiveMedian 0.3.0 protects against unsafe input by default. For non-attribute arguments, use:
104
+
105
+ ```ruby
106
+ Item.median(Arel.sql(known_safe_value))
107
+ ```
115
108
 
116
- A user-defined function is no longer needed for Postgres. Create a migration with `ActiveMedian.drop_function` to remove it.
109
+ Also, percentiles are now supported with SQLite. Use the [percentile extension](#sqlite) instead of `extension-functions`.
117
110
 
118
111
  ## Contributing
119
112
 
@@ -1,20 +1,34 @@
1
1
  module ActiveMedian
2
2
  module Model
3
3
  def median(column)
4
- percentile(column, 0.5)
4
+ calculate_percentile(column, 0.5, "median")
5
5
  end
6
6
 
7
7
  def percentile(column, percentile)
8
+ calculate_percentile(column, percentile, "percentile")
9
+ end
10
+
11
+ private
12
+
13
+ def calculate_percentile(column, percentile, operation)
8
14
  percentile = percentile.to_f
9
15
  raise ArgumentError, "percentile is not between 0 and 1" if percentile < 0 || percentile > 1
10
16
 
11
17
  # basic version of Active Record disallow_raw_sql!
12
18
  # symbol = column (safe), Arel node = SQL (safe), other = untrusted
13
19
  # matches table.column and column
14
- unless column.is_a?(Symbol) || column.is_a?(Arel::Nodes::SqlLiteral) || /\A\w+(\.\w+)?\z/i.match(column.to_s)
15
- warn "[active_median] Non-attribute argument: #{column}. Use Arel.sql() for known-safe values. This will raise an error in ActiveMedian 0.3.0"
20
+ unless column.is_a?(Symbol) || column.is_a?(Arel::Nodes::SqlLiteral)
21
+ column = column.to_s
22
+ unless /\A\w+(\.\w+)?\z/i.match(column)
23
+ raise ActiveRecord::UnknownAttributeReference, "Query method called with non-attribute argument(s): #{column.inspect}. Use Arel.sql() for known-safe values."
24
+ end
16
25
  end
17
26
 
27
+ column_alias = relation.send(:column_alias_for, "#{operation} #{column.to_s.downcase}")
28
+ # safety check
29
+ # could quote, but want to keep consistent with Active Record
30
+ raise "Bad column alias: #{column_alias}. Please report a bug." unless column_alias =~ /\A[a-z0-9_]+\z/
31
+
18
32
  # column resolution
19
33
  node = relation.send(:arel_columns, [column]).first
20
34
  node = Arel::Nodes::SqlLiteral.new(node) if node.is_a?(String)
@@ -37,31 +51,21 @@ module ActiveMedian
37
51
  over = "PARTITION BY #{group_values.join(", ")}"
38
52
  end
39
53
 
40
- select(*group_values, "PERCENTILE_CONT(#{percentile}) WITHIN GROUP (ORDER BY #{column}) OVER (#{over})").unscope(:group)
54
+ select(*group_values, "PERCENTILE_CONT(#{percentile}) WITHIN GROUP (ORDER BY #{column}) OVER (#{over}) AS #{column_alias}").unscope(:group)
41
55
  else
42
56
  # if mysql gets native function, check (and memoize) version first
43
- select(*group_values, "PERCENTILE_CONT(#{column}, #{percentile})")
57
+ select(*group_values, "PERCENTILE_CONT(#{column}, #{percentile}) AS #{column_alias}")
44
58
  end
45
59
  when /sqlserver/i
46
60
  if group_values.any?
47
61
  over = "PARTITION BY #{group_values.join(", ")}"
48
62
  end
49
63
 
50
- select(*group_values, "PERCENTILE_CONT(#{percentile}) WITHIN GROUP (ORDER BY #{column}) OVER (#{over})").unscope(:group)
64
+ select(*group_values, "PERCENTILE_CONT(#{percentile}) WITHIN GROUP (ORDER BY #{column}) OVER (#{over}) AS #{column_alias}").unscope(:group)
51
65
  when /sqlite/i
52
- case percentile.to_f
53
- when 0
54
- select(*group_values, "MIN(#{column})")
55
- when 0.5
56
- select(*group_values, "MEDIAN(#{column})")
57
- when 1
58
- select(*group_values, "MAX(#{column})")
59
- else
60
- # LOWER_QUARTILE and UPPER_QUARTILE use different calculation than 0.25 and 0.75
61
- raise "SQLite only supports 0, 0.5, and 1 percentiles"
62
- end
66
+ select(*group_values, "PERCENTILE(#{column}, #{percentile} * 100) AS #{column_alias}")
63
67
  when /postg/i, /redshift/i # postgis too
64
- select(*group_values, "PERCENTILE_CONT(#{percentile}) WITHIN GROUP (ORDER BY #{column})")
68
+ select(*group_values, "PERCENTILE_CONT(#{percentile}) WITHIN GROUP (ORDER BY #{column}) AS #{column_alias}")
65
69
  else
66
70
  raise "Connection adapter not supported: #{connection.adapter_name}"
67
71
  end
@@ -1,3 +1,3 @@
1
1
  module ActiveMedian
2
- VERSION = "0.2.8"
2
+ VERSION = "0.3.0"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: active_median
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.8
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Kane
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-01-17 00:00:00.000000000 Z
11
+ date: 2021-02-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -16,100 +16,16 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: '5'
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: '5'
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: minitest
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: pg
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: rake
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: activerecord
85
- requirement: !ruby/object:Gem::Requirement
86
- requirements:
87
- - - ">="
88
- - !ruby/object:Gem::Version
89
- version: '0'
90
- type: :development
91
- prerelease: false
92
- version_requirements: !ruby/object:Gem::Requirement
93
- requirements:
94
- - - ">="
95
- - !ruby/object:Gem::Version
96
- version: '0'
97
- - !ruby/object:Gem::Dependency
98
- name: groupdate
99
- requirement: !ruby/object:Gem::Requirement
100
- requirements:
101
- - - ">="
102
- - !ruby/object:Gem::Version
103
- version: '0'
104
- type: :development
105
- prerelease: false
106
- version_requirements: !ruby/object:Gem::Requirement
107
- requirements:
108
- - - ">="
109
- - !ruby/object:Gem::Version
110
- version: '0'
26
+ version: '5.2'
111
27
  description:
112
- email: andrew@chartkick.com
28
+ email: andrew@ankane.org
113
29
  executables: []
114
30
  extensions: []
115
31
  extra_rdoc_files: []
@@ -134,7 +50,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
134
50
  requirements:
135
51
  - - ">="
136
52
  - !ruby/object:Gem::Version
137
- version: '2.4'
53
+ version: '2.6'
138
54
  required_rubygems_version: !ruby/object:Gem::Requirement
139
55
  requirements:
140
56
  - - ">="