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 +4 -4
- data/CHANGELOG.md +7 -0
- data/README.md +11 -18
- data/lib/active_median/model.rb +22 -18
- data/lib/active_median/version.rb +1 -1
- metadata +6 -90
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 10eda8c3a8100441196584a8188a6bbd09829e5494d1f39a8faa8e9f77e98fcd
|
4
|
+
data.tar.gz: a6c2a49151b3ead3d6e7cf0d2eff4fc8d5d455b7d3fcee35b15645372863f37f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
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
|
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("
|
95
|
+
db.load_extension("percentile.dylib")
|
109
96
|
db.enable_load_extension(0)
|
110
97
|
```
|
111
98
|
|
112
99
|
## Upgrading
|
113
100
|
|
114
|
-
### 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
|
-
|
109
|
+
Also, percentiles are now supported with SQLite. Use the [percentile extension](#sqlite) instead of `extension-functions`.
|
117
110
|
|
118
111
|
## Contributing
|
119
112
|
|
data/lib/active_median/model.rb
CHANGED
@@ -1,20 +1,34 @@
|
|
1
1
|
module ActiveMedian
|
2
2
|
module Model
|
3
3
|
def median(column)
|
4
|
-
|
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)
|
15
|
-
|
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
|
-
|
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
|
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.
|
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-
|
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@
|
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.
|
53
|
+
version: '2.6'
|
138
54
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
139
55
|
requirements:
|
140
56
|
- - ">="
|