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 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