active_median 0.2.6 → 0.3.3
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 +27 -0
- data/LICENSE.txt +1 -1
- data/README.md +26 -24
- data/lib/active_median/model.rb +36 -17
- data/lib/active_median/mongoid.rb +1 -0
- data/lib/active_median/sqlite_handler.rb +33 -0
- data/lib/active_median/version.rb +1 -1
- data/lib/active_median.rb +1 -0
- metadata +8 -91
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8fac5b8a4669bedd7b9a7bd104c375df5acf6c9e60bc2c91ec97efb2ebea6969
|
4
|
+
data.tar.gz: afc2609d7983f06b975244e57ab4f6890916fdc3943870974a56d50cca19a832
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f3cff2041e5d602bd84f99f99cd75efbe6fba5c78faf4efbd6b3886edb91bf1db7deeca609ded57a859f10e670298f04a225f3f1b69ea8b4df9aecda68661b5e
|
7
|
+
data.tar.gz: d494fddff7e01063c99d549d4669f6efff207fef25f71761329d3c16166bc6a268f02020e12adde251be855ed0c31998f47fb73890689565bb67a0c320875b06
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,30 @@
|
|
1
|
+
## 0.3.3 (2021-08-17)
|
2
|
+
|
3
|
+
- Fixed null values for SQLite without an extension and MongoDB
|
4
|
+
|
5
|
+
## 0.3.2 (2021-08-16)
|
6
|
+
|
7
|
+
- Added support for SQLite without an extension
|
8
|
+
|
9
|
+
## 0.3.1 (2021-02-21)
|
10
|
+
|
11
|
+
- Fixed error with relations with `select` clauses
|
12
|
+
|
13
|
+
## 0.3.0 (2021-02-08)
|
14
|
+
|
15
|
+
- Added column alias
|
16
|
+
- Added support for percentiles with SQLite (switched extensions)
|
17
|
+
- Raise `ActiveRecord::UnknownAttributeReference` for non-attribute arguments
|
18
|
+
- Dropped support for Active Record < 5.2
|
19
|
+
|
20
|
+
## 0.2.8 (2021-01-16)
|
21
|
+
|
22
|
+
- Fixed bug with removing order
|
23
|
+
|
24
|
+
## 0.2.7 (2021-01-16)
|
25
|
+
|
26
|
+
- Fixed error with order
|
27
|
+
|
1
28
|
## 0.2.6 (2020-12-18)
|
2
29
|
|
3
30
|
- Fixed error with certain column types for Active Record 6.1
|
data/LICENSE.txt
CHANGED
data/README.md
CHANGED
@@ -4,11 +4,12 @@ Median and percentile for Active Record, Mongoid, arrays, and hashes
|
|
4
4
|
|
5
5
|
Supports:
|
6
6
|
|
7
|
-
- PostgreSQL
|
8
|
-
-
|
9
|
-
-
|
10
|
-
-
|
11
|
-
-
|
7
|
+
- PostgreSQL
|
8
|
+
- SQLite
|
9
|
+
- MariaDB
|
10
|
+
- MySQL (with an extension)
|
11
|
+
- SQL Server
|
12
|
+
- MongoDB
|
12
13
|
|
13
14
|
:fire: Uses native functions for blazing performance
|
14
15
|
|
@@ -22,7 +23,7 @@ Add this line to your application’s Gemfile:
|
|
22
23
|
gem 'active_median'
|
23
24
|
```
|
24
25
|
|
25
|
-
For MySQL
|
26
|
+
For MySQL, also follow [these instructions](#additional-instructions).
|
26
27
|
|
27
28
|
## Models
|
28
29
|
|
@@ -44,19 +45,6 @@ Works with grouping, too
|
|
44
45
|
Order.group(:store_id).median(:total)
|
45
46
|
```
|
46
47
|
|
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
48
|
## Arrays and Hashes
|
61
49
|
|
62
50
|
Median
|
@@ -94,10 +82,18 @@ mysql <options> < load.sql
|
|
94
82
|
|
95
83
|
### SQLite
|
96
84
|
|
97
|
-
SQLite
|
85
|
+
Improve performance with SQLite with 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.
|
86
|
+
|
87
|
+
On Linux, use:
|
98
88
|
|
99
89
|
```sh
|
100
|
-
gcc -g -fPIC -
|
90
|
+
gcc -g -fPIC -shared -lsqlite3 percentile.c -o percentile.so
|
91
|
+
```
|
92
|
+
|
93
|
+
On Mac, use:
|
94
|
+
|
95
|
+
```sh
|
96
|
+
gcc -g -fPIC -dynamiclib -L/usr/local/opt/sqlite/lib -lsqlite3 percentile.c -o percentile.dylib
|
101
97
|
```
|
102
98
|
|
103
99
|
To load it in Rails, create an initializer with:
|
@@ -105,15 +101,21 @@ To load it in Rails, create an initializer with:
|
|
105
101
|
```ruby
|
106
102
|
db = ActiveRecord::Base.connection.raw_connection
|
107
103
|
db.enable_load_extension(1)
|
108
|
-
db.load_extension("
|
104
|
+
db.load_extension("percentile.so") # or percentile.dylib
|
109
105
|
db.enable_load_extension(0)
|
110
106
|
```
|
111
107
|
|
112
108
|
## Upgrading
|
113
109
|
|
114
|
-
### 0.
|
110
|
+
### 0.3.0
|
111
|
+
|
112
|
+
ActiveMedian 0.3.0 protects against unsafe input by default. For non-attribute arguments, use:
|
113
|
+
|
114
|
+
```ruby
|
115
|
+
Item.median(Arel.sql(known_safe_value))
|
116
|
+
```
|
115
117
|
|
116
|
-
|
118
|
+
Also, percentiles are now supported with SQLite. Use the [percentile extension](#sqlite) instead of `extension-functions`.
|
117
119
|
|
118
120
|
## Contributing
|
119
121
|
|
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)
|
@@ -25,6 +39,9 @@ module ActiveMedian
|
|
25
39
|
|
26
40
|
group_values = all.group_values
|
27
41
|
|
42
|
+
# replace select to match behavior of average
|
43
|
+
relation = unscope(:select)
|
44
|
+
|
28
45
|
relation =
|
29
46
|
case connection.adapter_name
|
30
47
|
when /mysql/i
|
@@ -37,35 +54,37 @@ module ActiveMedian
|
|
37
54
|
over = "PARTITION BY #{group_values.join(", ")}"
|
38
55
|
end
|
39
56
|
|
40
|
-
select(*group_values, "PERCENTILE_CONT(#{percentile}) WITHIN GROUP (ORDER BY #{column}) OVER (#{over})").unscope(:group)
|
57
|
+
relation.select(*group_values, "PERCENTILE_CONT(#{percentile}) WITHIN GROUP (ORDER BY #{column}) OVER (#{over}) AS #{column_alias}").unscope(:group)
|
41
58
|
else
|
42
59
|
# if mysql gets native function, check (and memoize) version first
|
43
|
-
select(*group_values, "PERCENTILE_CONT(#{column}, #{percentile})")
|
60
|
+
relation.select(*group_values, "PERCENTILE_CONT(#{column}, #{percentile}) AS #{column_alias}")
|
44
61
|
end
|
45
62
|
when /sqlserver/i
|
46
63
|
if group_values.any?
|
47
64
|
over = "PARTITION BY #{group_values.join(", ")}"
|
48
65
|
end
|
49
66
|
|
50
|
-
select(*group_values, "PERCENTILE_CONT(#{percentile}) WITHIN GROUP (ORDER BY #{column}) OVER (#{over})").unscope(:group)
|
67
|
+
relation.select(*group_values, "PERCENTILE_CONT(#{percentile}) WITHIN GROUP (ORDER BY #{column}) OVER (#{over}) AS #{column_alias}").unscope(:group)
|
51
68
|
when /sqlite/i
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
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"
|
69
|
+
db = connection.raw_connection
|
70
|
+
unless db.instance_variable_get(:@active_median)
|
71
|
+
if db.get_first_value("SELECT 1 FROM pragma_function_list WHERE name = 'percentile'").nil?
|
72
|
+
require "active_median/sqlite_handler"
|
73
|
+
db.create_aggregate_handler(ActiveMedian::SQLiteHandler)
|
74
|
+
end
|
75
|
+
db.instance_variable_set(:@active_median, true)
|
62
76
|
end
|
77
|
+
|
78
|
+
relation.select(*group_values, "PERCENTILE(#{column}, #{percentile} * 100) AS #{column_alias}")
|
63
79
|
when /postg/i, /redshift/i # postgis too
|
64
|
-
select(*group_values, "PERCENTILE_CONT(#{percentile}) WITHIN GROUP (ORDER BY #{column})")
|
80
|
+
relation.select(*group_values, "PERCENTILE_CONT(#{percentile}) WITHIN GROUP (ORDER BY #{column}) AS #{column_alias}")
|
65
81
|
else
|
66
82
|
raise "Connection adapter not supported: #{connection.adapter_name}"
|
67
83
|
end
|
68
84
|
|
85
|
+
# same as average
|
86
|
+
relation = relation.unscope(:order).distinct!(false) if group_values.empty?
|
87
|
+
|
69
88
|
result = connection.select_all(relation.to_sql)
|
70
89
|
|
71
90
|
# typecast
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module ActiveMedian
|
2
|
+
class SQLiteHandler
|
3
|
+
def self.arity
|
4
|
+
2
|
5
|
+
end
|
6
|
+
|
7
|
+
def self.name
|
8
|
+
"percentile"
|
9
|
+
end
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
@values = []
|
13
|
+
@percentile = nil
|
14
|
+
end
|
15
|
+
|
16
|
+
# skip checks for
|
17
|
+
# 1. percentile between 0 and 100
|
18
|
+
# 2. percentile same for all rows
|
19
|
+
# since input is already checked
|
20
|
+
def step(ctx, value, percentile)
|
21
|
+
return if value.nil?
|
22
|
+
raise ActiveRecord::StatementInvalid, "1st argument to percentile() is not numeric" unless value.is_a?(Numeric)
|
23
|
+
@percentile ||= percentile
|
24
|
+
@values << value
|
25
|
+
end
|
26
|
+
|
27
|
+
def finalize(ctx)
|
28
|
+
if @values.any?
|
29
|
+
ctx.result = @values.percentile(@percentile / 100.0)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
data/lib/active_median.rb
CHANGED
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.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Andrew Kane
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2021-08-17 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: []
|
@@ -121,6 +37,7 @@ files:
|
|
121
37
|
- lib/active_median/enumerable.rb
|
122
38
|
- lib/active_median/model.rb
|
123
39
|
- lib/active_median/mongoid.rb
|
40
|
+
- lib/active_median/sqlite_handler.rb
|
124
41
|
- lib/active_median/version.rb
|
125
42
|
homepage: https://github.com/ankane/active_median
|
126
43
|
licenses:
|
@@ -134,14 +51,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
134
51
|
requirements:
|
135
52
|
- - ">="
|
136
53
|
- !ruby/object:Gem::Version
|
137
|
-
version: '2.
|
54
|
+
version: '2.6'
|
138
55
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
139
56
|
requirements:
|
140
57
|
- - ">="
|
141
58
|
- !ruby/object:Gem::Version
|
142
59
|
version: '0'
|
143
60
|
requirements: []
|
144
|
-
rubygems_version: 3.
|
61
|
+
rubygems_version: 3.2.22
|
145
62
|
signing_key:
|
146
63
|
specification_version: 4
|
147
64
|
summary: Median and percentile for Active Record, Mongoid, arrays, and hashes
|