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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 97636a27f16c0024056aa067afe5d54dcb01247be3236573db0d4a5365cf72ee
4
- data.tar.gz: 5c7dc9c490730959e05f4a633765ded663612f02543d62bb17ab05dfd1828f3e
3
+ metadata.gz: 8fac5b8a4669bedd7b9a7bd104c375df5acf6c9e60bc2c91ec97efb2ebea6969
4
+ data.tar.gz: afc2609d7983f06b975244e57ab4f6890916fdc3943870974a56d50cca19a832
5
5
  SHA512:
6
- metadata.gz: 48a5936c99b5325a8b7ac43035c7207912911a2e5ddd0889ccc6a4478d35360482cb2062f1756b5e0ba499e54ffff7e79e8adfe9fe29851a028d70194a497949
7
- data.tar.gz: abb786892e89b6361e7bf107d702ac4be5e9439f1a5364a7395c490c335c286bcdbcb6c106d130a6ef0870d3b2f100d6ded7c5c69db3ab87fbef4380dca8b9ca
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
@@ -1,4 +1,4 @@
1
- Copyright (c) 2013-2020 Andrew Kane
1
+ Copyright (c) 2013-2021 Andrew Kane
2
2
 
3
3
  MIT License
4
4
 
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 9.4+
8
- - MariaDB 10.3.3+
9
- - MySQL and SQL (with extensions)
10
- - SQL Server 2012+
11
- - MongoDB 2.1+
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 and SQLite, also follow [these instructions](#additional-instructions).
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 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:
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 -dynamiclib extension-functions.c -o extension-functions.dylib
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("extension-functions.dylib")
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.2.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
- A user-defined function is no longer needed for Postgres. Create a migration with `ActiveMedian.drop_function` to remove it.
118
+ Also, percentiles are now supported with SQLite. Use the [percentile extension](#sqlite) instead of `extension-functions`.
117
119
 
118
120
  ## Contributing
119
121
 
@@ -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)
@@ -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
- 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"
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
@@ -11,6 +11,7 @@ module ActiveMedian
11
11
 
12
12
  relation =
13
13
  all
14
+ .where(column => {"$ne" => nil})
14
15
  .asc(column)
15
16
  .group(_id: nil, values: {"$push" => "$#{column}"}, count: {"$sum" => 1})
16
17
  .project(values: 1, count: {"$subtract" => ["$count", 1]})
@@ -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
@@ -1,3 +1,3 @@
1
1
  module ActiveMedian
2
- VERSION = "0.2.6"
2
+ VERSION = "0.3.3"
3
3
  end
data/lib/active_median.rb CHANGED
@@ -4,6 +4,7 @@ require "active_median/enumerable"
4
4
  require "active_median/version"
5
5
 
6
6
  module ActiveMedian
7
+ # TODO remove in 0.4.0
7
8
  def self.drop_function
8
9
  ActiveRecord::Base.connection.execute <<-SQL
9
10
  DROP AGGREGATE IF EXISTS median(anyelement);
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.6
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: 2020-12-19 00:00:00.000000000 Z
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@chartkick.com
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.4'
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.1.4
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