active_median 0.1.4 → 0.2.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 +5 -5
- data/CHANGELOG.md +9 -0
- data/README.md +24 -11
- data/lib/active_median.rb +5 -104
- data/lib/active_median/model.rb +37 -0
- data/lib/active_median/version.rb +1 -1
- metadata +21 -38
- data/.gitignore +0 -17
- data/.travis.yml +0 -21
- data/Gemfile +0 -6
- data/Rakefile +0 -8
- data/active_median.gemspec +0 -27
- data/test/active_median_test.rb +0 -38
- data/test/gemfiles/activerecord32.gemfile +0 -6
- data/test/gemfiles/activerecord40.gemfile +0 -6
- data/test/gemfiles/activerecord41.gemfile +0 -6
- data/test/gemfiles/activerecord42.gemfile +0 -6
- data/test/test_helper.rb +0 -20
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 898f80afa3c78ec2137b1c8f2f2dae6e4604ff657f472725f10f9486a82fc32f
|
4
|
+
data.tar.gz: '080da5da53ea03e6a20f8a5e80d70afdec3e6fa2d1d140e18f680ac361bc077d'
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e60fa9abc00e89da32390c0608a8e297e8ba4a36a869eea44b0ac94c6dcff00a49b71823f1942313d7709489c9af78b52260f8d1c48d739ec842b2aebdc0ebf6
|
7
|
+
data.tar.gz: a1ee8deb9ba6352732ed008f8bb255c4a7f248bc63beb27d3c74893306af57a0f0d00ae7848c1e6356263a345fbf70151d0b59d633071aa407685117dc6ccb1c
|
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
@@ -1,9 +1,15 @@
|
|
1
1
|
# ActiveMedian
|
2
2
|
|
3
|
-
Median for ActiveRecord
|
3
|
+
Median for ActiveRecord
|
4
|
+
|
5
|
+
Supports PostgreSQL 9.4+, MariaDB 10.3.3+, and SQLite
|
6
|
+
|
7
|
+
:fire: Uses native functions for blazing performance
|
4
8
|
|
5
9
|
[](https://travis-ci.org/ankane/active_median)
|
6
10
|
|
11
|
+
## Usage
|
12
|
+
|
7
13
|
```ruby
|
8
14
|
Item.median(:price)
|
9
15
|
```
|
@@ -22,24 +28,31 @@ Add this line to your application’s Gemfile:
|
|
22
28
|
gem 'active_median'
|
23
29
|
```
|
24
30
|
|
25
|
-
|
31
|
+
For SQLite, also follow the instructions below.
|
32
|
+
|
33
|
+
### SQLite
|
34
|
+
|
35
|
+
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:
|
26
36
|
|
27
37
|
```sh
|
28
|
-
|
38
|
+
gcc -g -fPIC -dynamiclib extension-functions.c -o extension-functions.dylib
|
29
39
|
```
|
30
40
|
|
31
|
-
with:
|
41
|
+
To load it in Rails, create an initializer with:
|
32
42
|
|
33
43
|
```ruby
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
def down
|
39
|
-
ActiveMedian.drop_function
|
40
|
-
end
|
44
|
+
db = ActiveRecord::Base.connection.raw_connection
|
45
|
+
db.enable_load_extension(1)
|
46
|
+
db.load_extension("extension-functions.dylib")
|
47
|
+
db.enable_load_extension(0)
|
41
48
|
```
|
42
49
|
|
50
|
+
## Upgrading
|
51
|
+
|
52
|
+
### 0.2.0
|
53
|
+
|
54
|
+
A user-defined function is no longer needed. Create a migration with `ActiveMedian.drop_function` to remove it.
|
55
|
+
|
43
56
|
## Contributing
|
44
57
|
|
45
58
|
Everyone is encouraged to help improve this project. Here are a few ways you can help:
|
data/lib/active_median.rb
CHANGED
@@ -1,48 +1,9 @@
|
|
1
|
+
require "active_support"
|
2
|
+
|
3
|
+
require "active_median/model"
|
1
4
|
require "active_median/version"
|
2
|
-
require "active_record"
|
3
5
|
|
4
6
|
module ActiveMedian
|
5
|
-
def self.create_function
|
6
|
-
# create median method
|
7
|
-
# http://wiki.postgresql.org/wiki/Aggregate_Median
|
8
|
-
ActiveRecord::Base.connection.execute <<-SQL
|
9
|
-
CREATE OR REPLACE FUNCTION median(anyarray)
|
10
|
-
RETURNS float8 AS
|
11
|
-
$$
|
12
|
-
WITH q AS
|
13
|
-
(
|
14
|
-
SELECT val
|
15
|
-
FROM unnest($1) val
|
16
|
-
WHERE VAL IS NOT NULL
|
17
|
-
ORDER BY 1
|
18
|
-
),
|
19
|
-
cnt AS
|
20
|
-
(
|
21
|
-
SELECT COUNT(*) AS c FROM q
|
22
|
-
)
|
23
|
-
SELECT AVG(val)::float8
|
24
|
-
FROM
|
25
|
-
(
|
26
|
-
SELECT val FROM q
|
27
|
-
LIMIT 2 - MOD((SELECT c FROM cnt), 2)
|
28
|
-
OFFSET GREATEST(CEIL((SELECT c FROM cnt) / 2.0) - 1,0)
|
29
|
-
) q2;
|
30
|
-
$$
|
31
|
-
LANGUAGE sql IMMUTABLE;
|
32
|
-
|
33
|
-
DROP AGGREGATE IF EXISTS median(numeric);
|
34
|
-
DROP AGGREGATE IF EXISTS median(double precision);
|
35
|
-
DROP AGGREGATE IF EXISTS median(anyelement);
|
36
|
-
CREATE AGGREGATE median(anyelement) (
|
37
|
-
SFUNC=array_append,
|
38
|
-
STYPE=anyarray,
|
39
|
-
FINALFUNC=median,
|
40
|
-
INITCOND='{}'
|
41
|
-
);
|
42
|
-
SQL
|
43
|
-
true
|
44
|
-
end
|
45
|
-
|
46
7
|
def self.drop_function
|
47
8
|
ActiveRecord::Base.connection.execute <<-SQL
|
48
9
|
DROP AGGREGATE IF EXISTS median(anyelement);
|
@@ -52,66 +13,6 @@ module ActiveMedian
|
|
52
13
|
end
|
53
14
|
end
|
54
15
|
|
55
|
-
|
56
|
-
|
57
|
-
def median(*args)
|
58
|
-
calculate(:median, *args)
|
59
|
-
end
|
60
|
-
end
|
61
|
-
end
|
62
|
-
|
63
|
-
module ActiveRecord
|
64
|
-
module Querying
|
65
|
-
delegate :median, to: (Gem::Version.new(Arel::VERSION) >= Gem::Version.new("4.0.1") ? :all : :scoped)
|
66
|
-
end
|
67
|
-
end
|
68
|
-
|
69
|
-
module Arel
|
70
|
-
module Nodes
|
71
|
-
const_set("Median", Class.new(Function))
|
72
|
-
end
|
73
|
-
end
|
74
|
-
|
75
|
-
module Arel
|
76
|
-
module Expressions
|
77
|
-
def median
|
78
|
-
Nodes::Median.new [self], Nodes::SqlLiteral.new("median_id")
|
79
|
-
end
|
80
|
-
end
|
81
|
-
end
|
82
|
-
|
83
|
-
module Arel
|
84
|
-
module Visitors
|
85
|
-
class ToSql
|
86
|
-
def visit_Arel_Nodes_Median(o, a = nil)
|
87
|
-
if Gem::Version.new(Arel::VERSION) >= Gem::Version.new("6.0.0")
|
88
|
-
aggregate "MEDIAN", o, a
|
89
|
-
elsif a
|
90
|
-
"MEDIAN(#{o.distinct ? 'DISTINCT ' : ''}#{o.expressions.map do |x|
|
91
|
-
visit x, a
|
92
|
-
end.join(', ')})#{o.alias ? " AS #{visit o.alias, a}" : ''}"
|
93
|
-
else
|
94
|
-
"MEDIAN(#{o.distinct ? 'DISTINCT ' : ''}#{o.expressions.map do |x|
|
95
|
-
visit x
|
96
|
-
end.join(', ')})#{o.alias ? " AS #{visit o.alias}" : ''}"
|
97
|
-
end
|
98
|
-
end
|
99
|
-
end
|
100
|
-
end
|
101
|
-
end
|
102
|
-
|
103
|
-
module Arel
|
104
|
-
module Visitors
|
105
|
-
class DepthFirst
|
106
|
-
alias :visit_Arel_Nodes_Median :function
|
107
|
-
end
|
108
|
-
end
|
109
|
-
end
|
110
|
-
|
111
|
-
module Arel
|
112
|
-
module Visitors
|
113
|
-
class Dot
|
114
|
-
alias :visit_Arel_Nodes_Median :function
|
115
|
-
end
|
116
|
-
end
|
16
|
+
ActiveSupport.on_load(:active_record) do
|
17
|
+
extend(ActiveMedian::Model)
|
117
18
|
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module ActiveMedian
|
2
|
+
module Model
|
3
|
+
def median(column)
|
4
|
+
group_values = all.group_values
|
5
|
+
|
6
|
+
relation =
|
7
|
+
case connection.adapter_name
|
8
|
+
when /mysql/i
|
9
|
+
if group_values.any?
|
10
|
+
over = "PARTITION BY #{group_values.join(", ")}"
|
11
|
+
end
|
12
|
+
|
13
|
+
select(*group_values, "PERCENTILE_CONT(0.50) WITHIN GROUP (ORDER BY #{column}) OVER (#{over})").unscope(:group)
|
14
|
+
when /sqlite/i
|
15
|
+
select(*group_values, "MEDIAN(#{column})")
|
16
|
+
else
|
17
|
+
select(*group_values, "PERCENTILE_CONT(0.50) WITHIN GROUP (ORDER BY #{column})")
|
18
|
+
end
|
19
|
+
|
20
|
+
result = connection.select_all(relation.to_sql)
|
21
|
+
|
22
|
+
# typecast
|
23
|
+
rows = []
|
24
|
+
columns = result.columns
|
25
|
+
cast_method = ActiveRecord::VERSION::MAJOR < 5 ? :type_cast : :cast_value
|
26
|
+
result.rows.each do |untyped_row|
|
27
|
+
rows << (result.column_types.empty? ? untyped_row : columns.each_with_index.map { |c, i| untyped_row[i] ? result.column_types[c].send(cast_method, untyped_row[i]) : untyped_row[i] })
|
28
|
+
end
|
29
|
+
|
30
|
+
if group_values.any?
|
31
|
+
Hash[rows.map { |r| [r.size == 2 ? r[0] : r[0..-2], r[-1]] }]
|
32
|
+
else
|
33
|
+
rows[0] && rows[0][0]
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
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.2.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:
|
11
|
+
date: 2018-10-15 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -16,30 +16,30 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - ">="
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: '
|
19
|
+
version: '4.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: '
|
26
|
+
version: '4.2'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: bundler
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
|
-
- - "
|
31
|
+
- - ">="
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version: '
|
33
|
+
version: '0'
|
34
34
|
type: :development
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
|
-
- - "
|
38
|
+
- - ">="
|
39
39
|
- !ruby/object:Gem::Version
|
40
|
-
version: '
|
40
|
+
version: '0'
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
|
-
name:
|
42
|
+
name: minitest
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
44
44
|
requirements:
|
45
45
|
- - ">="
|
@@ -53,21 +53,21 @@ dependencies:
|
|
53
53
|
- !ruby/object:Gem::Version
|
54
54
|
version: '0'
|
55
55
|
- !ruby/object:Gem::Dependency
|
56
|
-
name:
|
56
|
+
name: pg
|
57
57
|
requirement: !ruby/object:Gem::Requirement
|
58
58
|
requirements:
|
59
|
-
- - "
|
59
|
+
- - "<"
|
60
60
|
- !ruby/object:Gem::Version
|
61
|
-
version: '
|
61
|
+
version: '1'
|
62
62
|
type: :development
|
63
63
|
prerelease: false
|
64
64
|
version_requirements: !ruby/object:Gem::Requirement
|
65
65
|
requirements:
|
66
|
-
- - "
|
66
|
+
- - "<"
|
67
67
|
- !ruby/object:Gem::Version
|
68
|
-
version: '
|
68
|
+
version: '1'
|
69
69
|
- !ruby/object:Gem::Dependency
|
70
|
-
name:
|
70
|
+
name: rake
|
71
71
|
requirement: !ruby/object:Gem::Requirement
|
72
72
|
requirements:
|
73
73
|
- - ">="
|
@@ -80,29 +80,18 @@ dependencies:
|
|
80
80
|
- - ">="
|
81
81
|
- !ruby/object:Gem::Version
|
82
82
|
version: '0'
|
83
|
-
description:
|
84
|
-
email:
|
85
|
-
- acekane1@gmail.com
|
83
|
+
description:
|
84
|
+
email: andrew@chartkick.com
|
86
85
|
executables: []
|
87
86
|
extensions: []
|
88
87
|
extra_rdoc_files: []
|
89
88
|
files:
|
90
|
-
- ".gitignore"
|
91
|
-
- ".travis.yml"
|
92
89
|
- CHANGELOG.md
|
93
|
-
- Gemfile
|
94
90
|
- LICENSE.txt
|
95
91
|
- README.md
|
96
|
-
- Rakefile
|
97
|
-
- active_median.gemspec
|
98
92
|
- lib/active_median.rb
|
93
|
+
- lib/active_median/model.rb
|
99
94
|
- lib/active_median/version.rb
|
100
|
-
- test/active_median_test.rb
|
101
|
-
- test/gemfiles/activerecord32.gemfile
|
102
|
-
- test/gemfiles/activerecord40.gemfile
|
103
|
-
- test/gemfiles/activerecord41.gemfile
|
104
|
-
- test/gemfiles/activerecord42.gemfile
|
105
|
-
- test/test_helper.rb
|
106
95
|
homepage: https://github.com/ankane/active_median
|
107
96
|
licenses:
|
108
97
|
- MIT
|
@@ -115,7 +104,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
115
104
|
requirements:
|
116
105
|
- - ">="
|
117
106
|
- !ruby/object:Gem::Version
|
118
|
-
version: '
|
107
|
+
version: '2.2'
|
119
108
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
120
109
|
requirements:
|
121
110
|
- - ">="
|
@@ -123,14 +112,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
123
112
|
version: '0'
|
124
113
|
requirements: []
|
125
114
|
rubyforge_project:
|
126
|
-
rubygems_version: 2.
|
115
|
+
rubygems_version: 2.7.7
|
127
116
|
signing_key:
|
128
117
|
specification_version: 4
|
129
118
|
summary: Median for ActiveRecord
|
130
|
-
test_files:
|
131
|
-
- test/active_median_test.rb
|
132
|
-
- test/gemfiles/activerecord32.gemfile
|
133
|
-
- test/gemfiles/activerecord40.gemfile
|
134
|
-
- test/gemfiles/activerecord41.gemfile
|
135
|
-
- test/gemfiles/activerecord42.gemfile
|
136
|
-
- test/test_helper.rb
|
119
|
+
test_files: []
|
data/.gitignore
DELETED
data/.travis.yml
DELETED
@@ -1,21 +0,0 @@
|
|
1
|
-
language: ruby
|
2
|
-
rvm:
|
3
|
-
- 2.3.0
|
4
|
-
sudo: false
|
5
|
-
script: bundle exec rake test
|
6
|
-
before_script:
|
7
|
-
- gem install bundler
|
8
|
-
- psql -c 'create database active_median_test;' -U postgres
|
9
|
-
notifications:
|
10
|
-
email:
|
11
|
-
on_success: never
|
12
|
-
on_failure: change
|
13
|
-
gemfile:
|
14
|
-
- Gemfile
|
15
|
-
- test/gemfiles/activerecord42.gemfile
|
16
|
-
- test/gemfiles/activerecord41.gemfile
|
17
|
-
- test/gemfiles/activerecord40.gemfile
|
18
|
-
- test/gemfiles/activerecord32.gemfile
|
19
|
-
matrix:
|
20
|
-
allow_failures:
|
21
|
-
- gemfile: test/gemfiles/activerecord32.gemfile
|
data/Gemfile
DELETED
data/Rakefile
DELETED
data/active_median.gemspec
DELETED
@@ -1,27 +0,0 @@
|
|
1
|
-
# coding: utf-8
|
2
|
-
lib = File.expand_path("../lib", __FILE__)
|
3
|
-
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
-
require "active_median/version"
|
5
|
-
|
6
|
-
Gem::Specification.new do |spec|
|
7
|
-
spec.name = "active_median"
|
8
|
-
spec.version = ActiveMedian::VERSION
|
9
|
-
spec.authors = ["Andrew Kane"]
|
10
|
-
spec.email = ["acekane1@gmail.com"]
|
11
|
-
spec.description = "Median for ActiveRecord"
|
12
|
-
spec.summary = "Median for ActiveRecord"
|
13
|
-
spec.homepage = "https://github.com/ankane/active_median"
|
14
|
-
spec.license = "MIT"
|
15
|
-
|
16
|
-
spec.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR)
|
17
|
-
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
-
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
-
spec.require_paths = ["lib"]
|
20
|
-
|
21
|
-
spec.add_dependency "activerecord"
|
22
|
-
|
23
|
-
spec.add_development_dependency "bundler", "~> 1.3"
|
24
|
-
spec.add_development_dependency "rake"
|
25
|
-
spec.add_development_dependency "minitest"
|
26
|
-
spec.add_development_dependency "pg"
|
27
|
-
end
|
data/test/active_median_test.rb
DELETED
@@ -1,38 +0,0 @@
|
|
1
|
-
require_relative "test_helper"
|
2
|
-
|
3
|
-
class TestActiveMedian < Minitest::Test
|
4
|
-
def setup
|
5
|
-
ActiveMedian.create_function
|
6
|
-
User.delete_all
|
7
|
-
end
|
8
|
-
|
9
|
-
def test_even
|
10
|
-
[1, 1, 2, 3, 4, 100].each { |n| User.create!(visits_count: n) }
|
11
|
-
assert_equal 2.5, User.median(:visits_count)
|
12
|
-
end
|
13
|
-
|
14
|
-
def test_odd
|
15
|
-
[1, 1, 2, 4, 100].each { |n| User.create!(visits_count: n) }
|
16
|
-
assert_equal 2, User.median(:visits_count)
|
17
|
-
end
|
18
|
-
|
19
|
-
def test_empty
|
20
|
-
assert_nil User.median(:visits_count)
|
21
|
-
end
|
22
|
-
|
23
|
-
def test_decimal
|
24
|
-
6.times { |n| User.create!(latitude: n * 0.1) }
|
25
|
-
assert_equal 0.25, User.median(:latitude)
|
26
|
-
end
|
27
|
-
|
28
|
-
def test_float
|
29
|
-
6.times { |n| User.create!(rating: n * 0.1) }
|
30
|
-
assert_equal 0.25, User.median(:rating)
|
31
|
-
end
|
32
|
-
|
33
|
-
def test_drop
|
34
|
-
ActiveMedian.drop_function
|
35
|
-
error = assert_raises(ActiveRecord::StatementInvalid) { User.median(:visits_count) }
|
36
|
-
assert_includes error.message, "PG::UndefinedFunction"
|
37
|
-
end
|
38
|
-
end
|
data/test/test_helper.rb
DELETED
@@ -1,20 +0,0 @@
|
|
1
|
-
require "bundler/setup"
|
2
|
-
Bundler.require :default
|
3
|
-
require "minitest/autorun"
|
4
|
-
require "minitest/pride"
|
5
|
-
require "logger"
|
6
|
-
|
7
|
-
Minitest::Test = Minitest::Unit::TestCase unless defined?(Minitest::Test)
|
8
|
-
|
9
|
-
ActiveRecord::Base.establish_connection adapter: "postgresql", database: "active_median_test"
|
10
|
-
|
11
|
-
# ActiveRecord::Base.logger = Logger.new(STDOUT)
|
12
|
-
|
13
|
-
ActiveRecord::Migration.create_table :users, force: true do |t|
|
14
|
-
t.integer :visits_count
|
15
|
-
t.decimal :latitude
|
16
|
-
t.float :rating
|
17
|
-
end
|
18
|
-
|
19
|
-
class User < ActiveRecord::Base
|
20
|
-
end
|