active_median 0.1.4 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: f2e6418a4cb5e878f7f36dec69fe4bb169b666c4
4
- data.tar.gz: b0da140de2cdb3c84d4f0e8408dc13d4339de6b0
2
+ SHA256:
3
+ metadata.gz: 898f80afa3c78ec2137b1c8f2f2dae6e4604ff657f472725f10f9486a82fc32f
4
+ data.tar.gz: '080da5da53ea03e6a20f8a5e80d70afdec3e6fa2d1d140e18f680ac361bc077d'
5
5
  SHA512:
6
- metadata.gz: f29c2c2002f6438ab8f2d9ff209481f1785d5f8fba668ec2c0ed73bfe17deac908ca31823fb0b26e55cf06b1f9d5bab1d34d5f6c03dcd1f093adfb21fcfb271c
7
- data.tar.gz: 0201efbeb295a121298a153e67f4985fee623529fcca873bbcfa8e70f5f551c90af7009a7489941014c9a0b2644cc746f9b0c18bc32cb71d40274fa4caef5f10
6
+ metadata.gz: e60fa9abc00e89da32390c0608a8e297e8ba4a36a869eea44b0ac94c6dcff00a49b71823f1942313d7709489c9af78b52260f8d1c48d739ec842b2aebdc0ebf6
7
+ data.tar.gz: a1ee8deb9ba6352732ed008f8bb255c4a7f248bc63beb27d3c74893306af57a0f0d00ae7848c1e6356263a345fbf70151d0b59d633071aa407685117dc6ccb1c
@@ -1,3 +1,12 @@
1
+ ## 0.2.0
2
+
3
+ - Added support for MariaDB 10.3.3+ and SQLite
4
+ - Use `PERCENTILE_CONT` for 4x performance increase
5
+
6
+ Breaking
7
+
8
+ - Dropped support for Postgres < 9.4
9
+
1
10
  ## 0.1.4
2
11
 
3
12
  - Added `drop_function` method
data/README.md CHANGED
@@ -1,9 +1,15 @@
1
1
  # ActiveMedian
2
2
 
3
- Median for ActiveRecord - PostgreSQL only at the moment
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
  [![Build Status](https://travis-ci.org/ankane/active_median.svg)](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
- And create a migration to add the `median` function to the database.
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
- rails g migration create_median_function
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
- def up
35
- ActiveMedian.create_function
36
- end
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:
@@ -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
- module ActiveRecord
56
- module Calculations
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
@@ -1,3 +1,3 @@
1
1
  module ActiveMedian
2
- VERSION = "0.1.4"
2
+ VERSION = "0.2.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.1.4
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: 2016-12-03 00:00:00.000000000 Z
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: '0'
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: '0'
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: '1.3'
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: '1.3'
40
+ version: '0'
41
41
  - !ruby/object:Gem::Dependency
42
- name: rake
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: minitest
56
+ name: pg
57
57
  requirement: !ruby/object:Gem::Requirement
58
58
  requirements:
59
- - - ">="
59
+ - - "<"
60
60
  - !ruby/object:Gem::Version
61
- version: '0'
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: '0'
68
+ version: '1'
69
69
  - !ruby/object:Gem::Dependency
70
- name: pg
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: Median for ActiveRecord
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: '0'
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.5.1
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
@@ -1,17 +0,0 @@
1
- *.gem
2
- *.rbc
3
- .bundle
4
- .config
5
- .yardoc
6
- *.lock
7
- InstalledFiles
8
- _yardoc
9
- coverage
10
- doc/
11
- lib/bundler/man
12
- pkg
13
- rdoc
14
- spec/reports
15
- test/tmp
16
- test/version_tmp
17
- tmp
@@ -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
@@ -1,6 +0,0 @@
1
- source "https://rubygems.org"
2
-
3
- # Specify your gem's dependencies in active_median.gemspec
4
- gemspec
5
-
6
- gem "activerecord", "~> 5.0.0"
data/Rakefile DELETED
@@ -1,8 +0,0 @@
1
- require "bundler/gem_tasks"
2
- require "rake/testtask"
3
-
4
- task default: :test
5
- Rake::TestTask.new do |t|
6
- t.libs << "test"
7
- t.pattern = "test/**/*_test.rb"
8
- end
@@ -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
@@ -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
@@ -1,6 +0,0 @@
1
- source 'https://rubygems.org'
2
-
3
- # Specify your gem's dependencies in active_median.gemspec
4
- gemspec path: "../../"
5
-
6
- gem "activerecord", "~> 3.2.0"
@@ -1,6 +0,0 @@
1
- source 'https://rubygems.org'
2
-
3
- # Specify your gem's dependencies in active_median.gemspec
4
- gemspec path: "../../"
5
-
6
- gem "activerecord", "~> 4.0.0"
@@ -1,6 +0,0 @@
1
- source 'https://rubygems.org'
2
-
3
- # Specify your gem's dependencies in active_median.gemspec
4
- gemspec path: "../../"
5
-
6
- gem "activerecord", "~> 4.1.0"
@@ -1,6 +0,0 @@
1
- source 'https://rubygems.org'
2
-
3
- # Specify your gem's dependencies in active_median.gemspec
4
- gemspec path: "../../"
5
-
6
- gem "activerecord", "~> 4.2.0"
@@ -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