active_record-mti 0.2.1 → 0.3.0.pre.rc1

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
2
  SHA1:
3
- metadata.gz: 83f66dc4e6bb45b2ca6a075be93334d2f9e761f3
4
- data.tar.gz: 4e4f3ec52bf515e9d9ffdfaab67306b480bbbdd1
3
+ metadata.gz: aed8fa7772f145269a995f4e2d139ebbb8670023
4
+ data.tar.gz: c40c16ae6a90a66aa9dc3aa6839fe9678761218b
5
5
  SHA512:
6
- metadata.gz: 0d08825e3d3f51e944963bcf9cf64cf27c16a9b3319fe2ccd50022d26ece1f608cae5f23d6ea8576b2fc98c4859726e72ee39a544aad33d8ebf38b4621528efc
7
- data.tar.gz: babb8e13a0371b408f3670059bb5482c4db455dd881c801c4ef1f62ddec542b36d79e0592a5e067a203cc3b2ee7b02e7fe9856bef3a0cc8aee1b1752f22cf696
6
+ metadata.gz: 520d9654c833f837d3cb93e2320cfb44e86c18802f3d4df1e4b2edccbbec9b9621b257c372eb0ac0dfe288c92135d07593cb3af80c910bd3679a50319efd1562
7
+ data.tar.gz: 7e51e4286a33db38af13e12ee6a5bb05f43519b4d8cb7e2be1bbfeef71c46caa370a25d691e80eccf9f61871717c0d130f4861f370c7d4aadcbd8e1639edb1ee
data/.travis.yml CHANGED
@@ -1,11 +1,6 @@
1
1
  language: ruby
2
2
  sudo: false
3
- rvm:
4
- - 2.0
5
- - 2.1
6
- - 2.2
7
- - 2.3.1
8
- - 2.4.0
3
+
9
4
  cache: bundler
10
5
  script:
11
6
  - bundle exec rspec
@@ -16,6 +11,33 @@ after_success:
16
11
  addons:
17
12
  postgresql: "9.3"
18
13
 
19
- env:
20
- - RSPEC_VERSION="<2.99"
21
- - RSPEC_VERSION="~>3.0
14
+ rvm:
15
+ - 2.0
16
+ - 2.1
17
+ - 2.2
18
+ - 2.3
19
+ - 2.4
20
+
21
+ gemfile:
22
+ - gemfiles/activerecord-4.0.Gemfile
23
+ - gemfiles/activerecord-4.1.Gemfile
24
+ - gemfiles/activerecord-4.2.Gemfile
25
+ - gemfiles/activerecord-5.0.Gemfile
26
+ - gemfiles/activerecord-5.1.Gemfile
27
+
28
+ matrix:
29
+ exclude:
30
+ - rvm: 2.0
31
+ gemfile: gemfiles/activerecord-5.0.Gemfile
32
+ - rvm: 2.0
33
+ gemfile: gemfiles/activerecord-5.1.Gemfile
34
+
35
+ - rvm: 2.1
36
+ gemfile: gemfiles/activerecord-5.0.Gemfile
37
+ - rvm: 2.1
38
+ gemfile: gemfiles/activerecord-5.1.Gemfile
39
+
40
+ - rvm: 2.4
41
+ gemfile: gemfiles/activerecord-4.0.Gemfile
42
+ - rvm: 2.4
43
+ gemfile: gemfiles/activerecord-4.1.Gemfile
data/CHANGELOG.md ADDED
@@ -0,0 +1,35 @@
1
+ # ActiveRecord::MTI
2
+
3
+ ## 0.3.0 _(November 7th 2017)_
4
+ - Greatly improved future-proofing injection strategy.
5
+ - No longer overwriting (and maintaining) ActiveRecord Calculation sub-routines.
6
+ - Instead of injecting at `build_select`, we're injecting at `build_arel` with one additional new sub-routine (`build_mti`)
7
+ - `build_mti` sub-routine detects if an MTI projection is needed based on grouping and selecting from query being built.
8
+
9
+ ## 0.2.1 _(September 20th 2017)_
10
+ - More reliable class discrimination
11
+ - Improved view support
12
+
13
+ ## 0.1.1 _(June 23rd 2017)_
14
+ - Fixes issue where inheritance check is called multiple times.
15
+ - Can handle a (simple) view that references a table that uses MTI
16
+
17
+ ## 0.1.0 _(May 12th 2017)_
18
+ - PSQL Adapter now responds to version
19
+ - Improved column pulls from DB
20
+
21
+ ## 0.0.7 _(May 11th 2017)_
22
+ - Specs!
23
+ - Breaking Change: must call `uses_mti` in models
24
+ - MTI class discrimination happens before STI
25
+ - More reliable projection/unprojection
26
+ - Improved table_name inference
27
+
28
+ ## 0.0.6 _(March 28th 2017)_
29
+ - Improve how `ActiveRecord::MTI` is injected into Rails
30
+
31
+ ## 0.0.5 _(September 27th 2016)_
32
+ - Allow SQL calculations (like `sum` and `count`) to execute by removing unneeded MTI projections
33
+
34
+ ## 0.0.2 _(September 21st 2016)_
35
+ - Default value to return when finding MTI class
data/Gemfile CHANGED
@@ -1,3 +1,20 @@
1
1
  source 'https://rubygems.org'
2
2
 
3
3
  gemspec
4
+
5
+ group :test do
6
+
7
+ # Generates coverage stats of specs
8
+ gem 'simplecov'
9
+
10
+ # Publishes coverage to codeclimate
11
+ gem 'codeclimate-test-reporter'
12
+
13
+ # Gives CircleCI more perspective on our tests
14
+ gem 'rspec_junit_formatter'
15
+
16
+ gem 'rspec'
17
+
18
+ gem 'database_cleaner'
19
+
20
+ end
data/README.md CHANGED
@@ -5,9 +5,11 @@
5
5
 
6
6
  # ActiveRecord::MTI
7
7
 
8
- Allows for true native inheritance of tables in PostgreSQL
8
+ ActiveRecord support for PostgreSQL's native inherited tables (multi-table inheritance)
9
9
 
10
- Currently requires Rails 4.2
10
+ Compatible with ActiveRecord `4.0`, `4.1`, `4.2`, `5.0`, `5.1`
11
+
12
+ Confirmed production use in `4.2`
11
13
 
12
14
  ## Usage
13
15
 
@@ -25,6 +27,11 @@ Or install it yourself as:
25
27
 
26
28
  ### Application Code
27
29
 
30
+ ActiveRecord queries work as usual with the following differences:
31
+
32
+ * You need to specify which model represents the base of your multi table inheritance tree. To do so, add `uses_mti` to the model definition of the base class.
33
+ * The default query of "*" is changed to include the OID of each row for subclass discrimination. The default select will be `SELECT "accounts"."tableoid" AS tableoid, "accounts".*` (for example)
34
+
28
35
  ```ruby
29
36
  class Account < ::ActiveRecord::Base
30
37
  uses_mti
@@ -40,11 +47,6 @@ class Developer < Account
40
47
  end
41
48
  ```
42
49
 
43
- ActiveRecord queries work as usual with the following differences:
44
-
45
- * You need to specify which model represents the base of your multi table inheritance tree. To do so, insert `uses_mti` in the model definition of the base class.
46
- * The default query of "*" is changed to include the OID of each row for subclass discrimination. The default select will be `SELECT cast("accounts"."tableoid"::regclass AS text), "accounts".*` (for example)
47
-
48
50
  ### Migrations
49
51
 
50
52
  In your migrations define a table to inherit from another table:
data/Rakefile CHANGED
@@ -1,2 +1,6 @@
1
1
  require "bundler/gem_tasks"
2
+ require 'rspec/core/rake_task'
2
3
 
4
+ RSpec::Core::RakeTask.new
5
+
6
+ task default: :spec
@@ -10,8 +10,8 @@ Gem::Specification.new do |spec|
10
10
  spec.email = ["dale@twilightcoders.net"]
11
11
 
12
12
  spec.summary = %q{Multi Table Inheritance for PostgreSQL in Rails}
13
- spec.description = %q{Allows use of native inherited tables in PostgreSQL}
14
- spec.homepage = ""
13
+ spec.description = %q{Gives ActiveRecord support for PostgreSQL's native inherited tables}
14
+ spec.homepage = "https://github.com/twilightcoders/active_record-mti"
15
15
  spec.license = "MIT"
16
16
 
17
17
  # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
@@ -22,23 +22,20 @@ Gem::Specification.new do |spec|
22
22
  raise 'RubyGems 2.0 or newer is required to protect against public gem pushes.'
23
23
  end
24
24
 
25
- spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
26
- spec.bindir = 'exe'
27
- spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
28
- spec.require_paths = ['lib']
25
+ spec.files = `git ls-files -z`.split("\x0")
26
+ spec.bindir = 'bin'
27
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
28
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
29
+ spec.require_paths = ['lib', 'spec']
29
30
 
30
- rails_versions = ['>= 4.1', '< 5']
31
- spec.required_ruby_version = '>= 2.0.0'
31
+ rails_versions = ['>= 4', '< 6']
32
+ spec.required_ruby_version = '>= 2.0'
32
33
 
33
34
  spec.add_runtime_dependency 'pg', '~> 0'
34
35
  spec.add_runtime_dependency 'activerecord', rails_versions
35
36
 
36
- spec.add_development_dependency 'pry-byebug'
37
+ spec.add_development_dependency 'pry-byebug', '~> 3'
37
38
  spec.add_development_dependency 'bundler', '~> 1.3'
38
39
  spec.add_development_dependency 'rake', '~> 10.0'
39
- spec.add_development_dependency 'rspec', '~> 3.0'
40
- spec.add_development_dependency 'simplecov', '~> 0.1'
41
- spec.add_development_dependency 'codeclimate-test-reporter', '~> 1.0'
42
- spec.add_development_dependency 'database_cleaner', '~> 1.6'
43
40
 
44
41
  end
@@ -0,0 +1,3 @@
1
+ eval_gemfile File.join(File.dirname(__FILE__), "../Gemfile")
2
+
3
+ gem 'activerecord', '~> 4.0.0'
@@ -0,0 +1,3 @@
1
+ eval_gemfile File.join(File.dirname(__FILE__), "../Gemfile")
2
+
3
+ gem 'activerecord', '~> 4.1.0'
@@ -0,0 +1,3 @@
1
+ eval_gemfile File.join(File.dirname(__FILE__), "../Gemfile")
2
+
3
+ gem 'activerecord', '~> 4.2.0'
@@ -0,0 +1,3 @@
1
+ eval_gemfile File.join(File.dirname(__FILE__), "../Gemfile")
2
+
3
+ gem 'activerecord', '~> 5.0.0'
@@ -0,0 +1,3 @@
1
+ eval_gemfile File.join(File.dirname(__FILE__), "../Gemfile")
2
+
3
+ gem 'activerecord', '~> 5.1.0'
@@ -1,121 +1,22 @@
1
1
  module ActiveRecord
2
2
  module MTI
3
3
  module Calculations
4
+ extend ActiveSupport::Concern
4
5
 
5
6
  private
6
7
 
7
- def execute_grouped_calculation(operation, column_name, distinct) #:nodoc:
8
- group_attrs = group_values
9
-
10
- if group_attrs.first.respond_to?(:to_sym)
11
- association = @klass._reflect_on_association(group_attrs.first)
12
- associated = group_attrs.size == 1 && association && association.belongs_to? # only count belongs_to associations
13
- group_fields = Array(associated ? association.foreign_key : group_attrs)
14
- else
15
- group_fields = group_attrs
8
+ def perform_calculation(*args)
9
+ result = swap_and_restore_tableoid_cast(true) do
10
+ super
16
11
  end
17
- group_fields = arel_columns(group_fields)
18
-
19
- group_aliases = group_fields.map { |field|
20
- column_alias_for(field)
21
- }
22
- group_columns = group_aliases.zip(group_fields).map { |aliaz,field|
23
- [aliaz, field]
24
- }
25
-
26
- group = group_fields
27
-
28
- if operation == 'count' && column_name == :all
29
- aggregate_alias = 'count_all'
30
- else
31
- aggregate_alias = column_alias_for([operation, column_name].join(' '))
32
- end
33
-
34
- select_values = [
35
- operation_over_aggregate_column(
36
- aggregate_column(column_name),
37
- operation,
38
- distinct).as(aggregate_alias)
39
- ]
40
- select_values += select_values unless having_values.empty?
41
-
42
- select_values.concat group_fields.zip(group_aliases).map { |field,aliaz|
43
- if field.respond_to?(:as)
44
- field.as(aliaz)
45
- else
46
- "#{field} AS #{aliaz}"
47
- end
48
- }
49
-
50
- relation = except(:group)
51
- relation.group_values = group
52
- relation.select_values = select_values
53
-
54
- # Remove our cast otherwise PSQL will insist that it be included in the GROUP
55
- relation.arel.projections.select!{ |p| p.to_s != tableoid_cast(klass) } if @klass.using_multi_table_inheritance?
56
-
57
- calculated_data = @klass.connection.select_all(relation, nil, relation.arel.bind_values + bind_values)
58
-
59
- if association
60
- key_ids = calculated_data.collect { |row| row[group_aliases.first] }
61
- key_records = association.klass.base_class.find(key_ids)
62
- key_records = Hash[key_records.map { |r| [r.id, r] }]
63
- end
64
-
65
- Hash[calculated_data.map do |row|
66
- key = group_columns.map { |aliaz, col_name|
67
- column = calculated_data.column_types.fetch(aliaz) do
68
- type_for(col_name)
69
- end
70
- type_cast_calculated_value(row[aliaz], column)
71
- }
72
- key = key.first if key.size == 1
73
- key = key_records[key] if associated
74
-
75
- column_type = calculated_data.column_types.fetch(aggregate_alias) { type_for(column_name) }
76
- [key, type_cast_calculated_value(row[aggregate_alias], column_type, operation)]
77
- end]
78
12
  end
79
13
 
80
- def execute_simple_calculation(operation, column_name, distinct) #:nodoc:
81
- # Postgresql doesn't like ORDER BY when there are no GROUP BY
82
- relation = unscope(:order)
83
-
84
- column_alias = column_name
85
-
86
- bind_values = nil
87
-
88
- if operation == "count" && (relation.limit_value || relation.offset_value)
89
- # Shortcut when limit is zero.
90
- return 0 if relation.limit_value == 0
91
-
92
- query_builder = build_count_subquery(relation, column_name, distinct)
93
- bind_values = query_builder.bind_values + relation.bind_values
94
- else
95
- column = aggregate_column(column_name)
96
-
97
- select_value = operation_over_aggregate_column(column, operation, distinct)
98
-
99
- column_alias = select_value.alias
100
- column_alias ||= @klass.connection.column_name_for_operation(operation, select_value)
101
- relation.select_values = [select_value]
102
-
103
- # Remove our cast otherwise PSQL will insist that it be included in the GROUP
104
- # Somewhere between line 82 and 101 relation.arel.projections gets reset :/
105
- relation.arel.projections.select!{ |p| p != tableoid_cast(klass) } if @klass.using_multi_table_inheritance?
106
-
107
- query_builder = relation.arel
108
- bind_values = query_builder.bind_values + relation.bind_values
109
- end
110
-
111
- result = @klass.connection.select_all(query_builder, nil, bind_values)
112
- row = result.first
113
- value = row && row.values.first
114
- column = result.column_types.fetch(column_alias) do
115
- type_for(column_name)
116
- end
117
-
118
- type_cast_calculated_value(value, column, operation)
14
+ def swap_and_restore_tableoid_cast(value, &block)
15
+ orignal_value = Thread.current['skip_tableoid_cast']
16
+ Thread.current['skip_tableoid_cast'] = value
17
+ return_value = yield
18
+ Thread.current['skip_tableoid_cast'] = orignal_value
19
+ return return_value
119
20
  end
120
21
 
121
22
  end
@@ -27,43 +27,18 @@ module ActiveRecord
27
27
  table_name = %Q("#{schema}"."#{table_name}")
28
28
  end
29
29
 
30
- if parent_table = options.delete(:inherits)
31
- options[:options] = [%Q(INHERITS ("#{parent_table}")), options[:options]].compact.join
30
+ if inherited_table = options.delete(:inherits)
31
+ # options[:options] = options[:options].sub("INHERITS", "() INHERITS") if td.columns.empty?
32
+ options[:options] = [%Q(INHERITS ("#{inherited_table}")), options[:options]].compact.join
32
33
  end
33
34
 
34
- td = create_table_definition table_name, options[:temporary], options[:options], options[:as]
35
-
36
- if options[:id] != false && !options[:as]
37
- pk = options.fetch(:primary_key) do
38
- Base.get_primary_key table_name.to_s.singularize
39
- end
40
-
41
- if pk.is_a?(Array)
42
- td.primary_keys pk
43
- else
44
- td.primary_key pk, options.fetch(:id, :primary_key), options
45
- end
46
- end
47
-
48
- yield td if block_given?
49
-
50
- if options[:force] && data_source_exists?(table_name)
51
- drop_table(table_name, options)
52
- end
53
-
54
- # Rails 5 wont create an empty column list which we might have if we're
55
- # working with inherited tables. So we need to do that manually
56
- sql = schema_creation.accept(td)
57
- # sql = sql.sub("INHERITS", "() INHERITS") if td.columns.empty?
58
-
59
- result = execute sql
60
-
61
- if parent_table
62
- parent_table_primary_key = primary_key(parent_table)
63
- execute %Q(ALTER TABLE "#{table_name}" ADD PRIMARY KEY ("#{parent_table_primary_key}"))
35
+ results = super(table_name, options)
64
36
 
37
+ if inherited_table
38
+ inherited_table_primary_key = primary_key(inherited_table)
39
+ execute %Q(ALTER TABLE "#{table_name}" ADD PRIMARY KEY ("#{inherited_table_primary_key}"))
65
40
 
66
- indexes(parent_table).each do |index|
41
+ indexes(inherited_table).each do |index|
67
42
  attributes = index.to_h.slice(:unique, :using, :where, :orders)
68
43
 
69
44
  # Why rails insists on being inconsistant with itself is beyond me.
@@ -71,14 +46,9 @@ module ActiveRecord
71
46
 
72
47
  add_index table_name, index.columns, attributes
73
48
  end
74
- # triggers_for_table(parent_table).each do |trigger|
75
- # name = trigger.first
76
- # definition = trigger.second.merge(on: table_name)
77
- # create_trigger name, definition
78
- # end
79
49
  end
80
50
 
81
- td.indexes.each_pair { |c,o| add_index table_name, c, o }
51
+ results
82
52
  end
83
53
 
84
54
  # Parent of inherited table
@@ -1,32 +1,13 @@
1
1
  require 'active_support/concern'
2
2
 
3
3
  module ActiveRecord
4
- # == Multi table inheritance
5
- #
6
- # PostgreSQL allows for table inheritance. To enable this in ActiveRecord, ensure that the
7
- # inheritance_column is named "tableoid" (can be changed by setting <tt>Base.inheritance_column</tt>).
8
- # This means that an inheritance looking like this:
9
- #
10
- # class Company < ActiveRecord::Base;
11
- # self.inheritance_column = 'tableoid'
12
- # end
13
- # class Firm < Company; end
14
- # class Client < Company; end
15
- # class PriorityClient < Client; end
16
- #
17
- # When you do <tt>Firm.create(name: "37signals")</tt>, this record will be saved in
18
- # the firms table which inherits from companies. You can then fetch this row again using
19
- # <tt>Company.where(name: '37signals').first</tt> and it will return a Firm object.
20
- #
21
- # Note, all the attributes for all the cases are kept in the same table. Read more:
22
- # http://www.martinfowler.com/eaaCatalog/singleTableInheritance.html
23
- #
4
+ # == Multi-Table Inheritance
24
5
  module MTI
25
6
  module Inheritance
26
7
  extend ActiveSupport::Concern
27
- @mti_tableoids = {}
28
8
 
29
9
  included do
10
+ @@mti_tableoids = {}
30
11
  scope :discern_inheritance, -> {
31
12
 
32
13
  }
@@ -34,12 +15,14 @@ module ActiveRecord
34
15
 
35
16
  module ClassMethods
36
17
 
18
+ @uses_mti = nil
19
+ @mti_setup = false
20
+ @mti_type_column = nil
21
+
37
22
  def uses_mti(custom_table_name = nil, inheritance_column = nil)
38
23
  self.inheritance_column = inheritance_column
39
24
 
40
25
  @uses_mti = true
41
- @mti_setup = false
42
- @mti_tableoid_projection = nil
43
26
  @tableoid_column = nil
44
27
  end
45
28
 
@@ -48,8 +31,14 @@ module ActiveRecord
48
31
  end
49
32
 
50
33
  def uses_mti?
51
- inheritence_check = check_inheritence_of(@table_name) unless @mti_setup
52
- @uses_mti = inheritence_check if @uses_mti.nil?
34
+ inheritance_check = check_inheritance_of(@table_name) unless @mti_setup
35
+
36
+ if @uses_mti.nil? && @uses_mti = inheritance_check
37
+ descendants.each do |d|
38
+ d.uses_mti?
39
+ end
40
+ end
41
+
53
42
  @uses_mti
54
43
  end
55
44
 
@@ -57,17 +46,17 @@ module ActiveRecord
57
46
  @tableoid_column != false
58
47
  end
59
48
 
60
- def mti_tableoid_projection
61
- @mti_tableoid_projection
49
+ def mti_type_column
50
+ @mti_type_column
62
51
  end
63
52
 
64
- def mti_tableoid_projection=(value)
65
- @mti_tableoid_projection = value
53
+ def mti_type_column=(value)
54
+ @mti_type_column = value
66
55
  end
67
56
 
68
57
  private
69
58
 
70
- def check_inheritence_of(table_name)
59
+ def check_inheritance_of(table_name)
71
60
  ActiveRecord::MTI.logger.debug "Trying to check inheritance of table with no table name (#{self})" unless table_name
72
61
  return nil unless table_name
73
62
 
@@ -82,21 +71,23 @@ module ActiveRecord
82
71
  LEFT JOIN pg_catalog.pg_class AS cl_d ON cl_d.oid = d.refobjid
83
72
  WHERE inhrelid = COALESCE(cl_d.relname, 'public.#{table_name}')::regclass::oid
84
73
  OR inhparent = COALESCE(cl_d.relname, 'public.#{table_name}')::regclass::oid
85
- );
74
+ ) AS uses_inheritance;
86
75
  SQL
87
76
 
88
- uses_inheritence = result.try(:first).try(:values).try(:first) == 't'
77
+ uses_inheritance = ActiveRecord::MTI.testify(result.try(:first)['uses_inheritance'])
89
78
 
90
- register_tableoid(table_name, uses_inheritence)
79
+ register_tableoid(table_name) if uses_inheritance
91
80
 
92
81
  @mti_setup = true
93
82
  # Some versions of PSQL return {"?column?"=>"t"}
94
83
  # instead of {"exists"=>"t"}, so we're saying screw it,
95
84
  # just give me the first value of whatever is returned
96
- return uses_inheritence
85
+
86
+ # Ensure a boolean is returned
87
+ return uses_inheritance == true
97
88
  end
98
89
 
99
- def register_tableoid(table_name, uses_mti=false)
90
+ def register_tableoid(table_name)
100
91
 
101
92
  tableoid_query = connection.execute(<<-SQL
102
93
  SELECT '#{table_name}'::regclass::oid AS tableoid, (SELECT EXISTS (
@@ -109,13 +100,14 @@ module ActiveRecord
109
100
  SQL
110
101
  ).first
111
102
  tableoid = tableoid_query['tableoid']
112
- @tableoid_column = tableoid_query['has_tableoid_column'] == 't'
103
+ @tableoid_column = ActiveRecord::MTI.testify(tableoid_query['has_tableoid_column'])
113
104
 
114
105
  if (has_tableoid_column?)
115
- ActiveRecord::MTI.logger.debug "#{table_name} has tableoid column!"
116
- @mti_tableoid_projection = arel_table[:tableoid].as('tableoid')
106
+ ActiveRecord::MTI.logger.debug "#{table_name} has tableoid column! (#{tableoid})"
107
+ add_tableoid_column
108
+ @mti_type_column = arel_table[:tableoid]
117
109
  else
118
- @mti_tableoid_projection = nil
110
+ @mti_type_column = nil
119
111
  end
120
112
 
121
113
  Inheritance.add_mti(tableoid, self)
@@ -156,14 +148,34 @@ module ActiveRecord
156
148
  sti_column.in(sti_names)
157
149
  end
158
150
  end
151
+
152
+ def add_tableoid_column
153
+ if self.respond_to? :attribute
154
+ self.attribute :tableoid, get_integer_oid_class.new
155
+ else
156
+ columns.unshift ActiveRecord::ConnectionAdapters::PostgreSQLColumn.new('tableoid', nil, ActiveRecord::ConnectionAdapters::PostgreSQLAdapter::OID::Integer.new, "oid", false)
157
+ end
158
+ end
159
+
160
+ # Rails decided to make a breaking change in it's 4.x series :P
161
+ def get_integer_oid_class
162
+ ::ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Integer
163
+ rescue NameError
164
+ begin
165
+ ::ActiveRecord::ConnectionAdapters::PostgreSQLAdapter::OID::Integer
166
+ rescue NameError
167
+ ::ActiveModel::Type::Integer
168
+ end
169
+ end
170
+
159
171
  end
160
172
 
161
173
  def self.add_mti(tableoid, klass)
162
- @mti_tableoids[tableoid.to_s.to_sym] = klass
174
+ @@mti_tableoids[tableoid.to_s.to_sym] = klass
163
175
  end
164
176
 
165
177
  def self.find_mti(tableoid)
166
- @mti_tableoids[tableoid.to_s.to_sym]
178
+ @@mti_tableoids[tableoid.to_s.to_sym]
167
179
  end
168
180
 
169
181
  end
@@ -44,6 +44,14 @@ module ActiveRecord
44
44
  end
45
45
  end
46
46
 
47
+ def full_table_name_prefix #:nodoc:
48
+ (parents.detect{ |p| p.respond_to?(:table_name_prefix) } || self).table_name_prefix
49
+ end
50
+
51
+ def full_table_name_suffix #:nodoc:
52
+ (parents.detect {|p| p.respond_to?(:table_name_suffix) } || self).table_name_suffix
53
+ end
54
+
47
55
  private
48
56
 
49
57
  # Guesses the table name, but does not decorate it with prefix and suffix information.
@@ -1,26 +1,48 @@
1
1
  module ActiveRecord
2
2
  module MTI
3
3
  module QueryMethods
4
+ extend ActiveSupport::Concern
4
5
 
5
- private
6
+ def build_arel
7
+ select_by_tableoid = select_values.delete(:tableoid) == :tableoid
8
+ group_by_tableoid = group_values.delete(:tableoid) == :tableoid
6
9
 
7
- # Retrieve the OID as well on a default select
8
- def build_select(arel)
9
- if @klass.using_multi_table_inheritance? && @klass.has_tableoid_column?
10
- arel.project(tableoid_cast(@klass))
11
- end
10
+ arel = super
12
11
 
13
- if select_values.any?
14
- arel.project(*arel_columns(select_values.uniq))
15
- else
16
- arel.project(@klass.arel_table[Arel.star])
12
+ if tableoid?(@klass) || group_by_tableoid || select_by_tableoid
13
+ arel.project(tableoid_project(@klass))
14
+ arel.group(tableoid_group(@klass)) if group_values.any? || group_by_tableoid
17
15
  end
16
+
17
+ arel
18
+ end
19
+
20
+ private
21
+
22
+ def tableoid?(klass)
23
+ !Thread.current['skip_tableoid_cast'] &&
24
+ @klass.using_multi_table_inheritance? &&
25
+ @klass.has_tableoid_column?
18
26
  end
19
27
 
20
- def tableoid_cast(klass)
28
+ def tableoid_project?(klass)
29
+ tableoid?(klass) &&
30
+ (group_values - [:tableoid]).any?
31
+ end
32
+
33
+ def tableoid_group?(klass)
34
+ tableoid?(klass) &&
35
+ group_values.any?
36
+ end
37
+
38
+ def tableoid_project(klass)
21
39
  # Arel::Nodes::NamedFunction.new('CAST', [klass.arel_table[:tableoid].as('regclass')])
22
40
  # Arel::Nodes::NamedFunction.new('CAST', [@klass.arel_table['tableoid::regclass'].as('regclass')])
23
- @klass.mti_tableoid_projection
41
+ @klass.mti_type_column.as('tableoid')
42
+ end
43
+
44
+ def tableoid_group(klass)
45
+ @klass.mti_type_column
24
46
  end
25
47
 
26
48
  end
@@ -4,7 +4,7 @@ module ActiveRecord
4
4
  module MTI
5
5
  class Railtie < Rails::Railtie
6
6
  initializer 'active_record-mti.load' do |_app|
7
- ActiveRecord::MTI.logger.info "ActiveRecord::MTI railtie initializer"
7
+ ActiveRecord::MTI.logger.debug "active_record-mti.load"
8
8
  ActiveSupport.on_load(:active_record) do
9
9
  ActiveRecord::MTI.load
10
10
  end
@@ -1,5 +1,5 @@
1
1
  module ActiveRecord
2
2
  module MTI
3
- VERSION = '0.2.1'
3
+ VERSION = '0.3.0-rc1'
4
4
  end
5
5
  end
@@ -32,11 +32,16 @@ module ActiveRecord
32
32
  def self.load
33
33
  ::ActiveRecord::Base.send :include, Inheritance
34
34
  ::ActiveRecord::Base.send :include, ModelSchema
35
- ::ActiveRecord::Relation.send :include, QueryMethods
36
- ::ActiveRecord::Relation.send :include, Calculations
35
+ ::ActiveRecord::Relation.send :prepend, QueryMethods
36
+ ::ActiveRecord::Relation.send :prepend, Calculations
37
37
  ::ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.send :prepend, ConnectionAdapters::PostgreSQL::Adapter
38
38
  ::ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.send :include, ConnectionAdapters::PostgreSQL::SchemaStatements
39
39
  ::ActiveRecord::SchemaDumper.send :include, SchemaDumper
40
40
  end
41
+
42
+ def self.testify(value)
43
+ value == true || value == 't' || value == 1 || value == '1'
44
+ end
45
+
41
46
  end
42
47
  end
@@ -0,0 +1,48 @@
1
+ require 'spec_helper'
2
+
3
+ describe ActiveRecord::MTI::Calculations do
4
+
5
+ context "don't project tableoid on" do
6
+ it "grouping" do
7
+
8
+ Admin.create(email: 'foo@bar.baz', god_powers: 3)
9
+ Admin.create(email: 'foo@bar.baz', god_powers: 3)
10
+ Admin.create(email: 'foo24@bar.baz', god_powers: 3)
11
+
12
+ grouped_count = Admin.group(:email).count
13
+
14
+ expect(grouped_count['foo24@bar.baz']).to eq(1)
15
+ expect(grouped_count['foo@bar.baz']).to eq(2)
16
+
17
+ end
18
+
19
+ it "count calculations" do
20
+
21
+ Admin.create(email: 'foo@bar.baz', god_powers: 3)
22
+ Admin.create(email: 'foo@bar.baz', god_powers: 3)
23
+ Admin.create(email: 'foo24@bar.baz', god_powers: 3)
24
+
25
+ expect(Admin.count(:email)).to eq(3)
26
+
27
+ end
28
+ end
29
+
30
+ context "projects tableoid" do
31
+ it "and groups tableoid when selecting :tableoid" do
32
+ sql = Admin.select(:email, :tableoid).group(:email).to_sql
33
+
34
+ expect(sql).to match(/SELECT .*, \"admins\".\"tableoid\" AS tableoid FROM \"admins\"/)
35
+
36
+ expect(sql).to match(/GROUP BY .*, \"admins\".\"tableoid\"/)
37
+ end
38
+
39
+ it "when grouping :tableoid" do
40
+ sql = Admin.select(:email).group(:email, :tableoid).to_sql
41
+
42
+ expect(sql).to match(/SELECT .*, \"admins\".\"tableoid\" AS tableoid FROM \"admins\"/)
43
+
44
+ expect(sql).to match(/GROUP BY .*, \"admins\".\"tableoid\"/)
45
+ end
46
+ end
47
+
48
+ end
@@ -0,0 +1,145 @@
1
+ require 'spec_helper'
2
+
3
+ describe ActiveRecord::MTI::Inheritance do
4
+
5
+ xit 'returns non-nil value when checking uses_mti?' do
6
+ # Mod = Class.new(User)
7
+ # expect(Mod.uses_mti?).to be(true)
8
+ end
9
+
10
+ context 'class definition' do
11
+
12
+ describe 'for classes that use MTI' do
13
+ it "doesn't check inheritance multiple times" do
14
+ Admin.instance_variable_set(:@mti_setup, false)
15
+ expect(Admin).to receive(:check_inheritance_of).and_call_original.exactly(1).times
16
+
17
+ Admin.create(email: 'foo@bar.baz', god_powers: 3)
18
+ Admin.create(email: 'foo2@bar.baz', god_powers: 3)
19
+ Admin.create(email: 'foo24@bar.baz', god_powers: 3)
20
+
21
+ end
22
+ end
23
+
24
+ describe "for classes that don't use MTI" do
25
+ it "doesn't check inheritance multiple times" do
26
+ Post.instance_variable_set(:@uses_mti, nil)
27
+ expect(Post).to receive(:check_inheritance_of).and_call_original.exactly(1).times
28
+
29
+ Post.create(title: 'foo@bar.baz')
30
+ Post.create(title: 'foo2@bar.baz')
31
+ Post.create(title: 'foo24@bar.baz')
32
+
33
+ end
34
+ end
35
+
36
+ end
37
+
38
+ context 'default inheritance_column model' do
39
+ let!(:user) { User.create(email: 'foo@bar.baz') }
40
+ let!(:admin) { Admin.create(email: 'foo@bar.baz', god_powers: 3) }
41
+
42
+ it 'casts properly' do
43
+ user = User.first
44
+ expect(user.class).to eq(User)
45
+ end
46
+
47
+ describe 'base class querying' do
48
+ it 'casts children properly' do
49
+ users = User.all
50
+ expect(users.select{ |u| u.is_a?(Admin) }.count).to eql(1)
51
+ end
52
+
53
+ xit 'deserializes children with child specific data' do
54
+ my_admin = User.find(admin.id)
55
+ expect(my_admin.god_powers).to eql(3)
56
+ end
57
+ end
58
+
59
+ describe 'has the correct count for' do
60
+ it 'parents' do
61
+ users = User.all
62
+ expect(users.count).to eq(2)
63
+ end
64
+
65
+ it 'children' do
66
+ admins = Admin.all
67
+ expect(admins.count).to eq(1)
68
+ end
69
+ end
70
+
71
+ describe 'dynamic class creation' do
72
+ it 'infers the table_name from superclass not base_class' do
73
+ god = Class.new(Admin)
74
+ expect(god.table_name).to eql(Admin.table_name)
75
+ end
76
+ end
77
+ end
78
+
79
+ describe 'views' do
80
+ before(:each) do
81
+ class UserView < User
82
+ self.table_name = "users_all"
83
+ end
84
+
85
+ UserView.connection.execute <<-SQL
86
+ CREATE OR REPLACE VIEW "users_all"
87
+ AS #{ User.all.to_sql }
88
+ SQL
89
+ end
90
+
91
+ if ActiveRecord::Base.connection.version >= Gem::Version.new('9.4')
92
+ it 'allows creation pass-through' do
93
+
94
+ UserView.create(email: 'dale@twilightcoders.net')
95
+ end
96
+ end
97
+ end
98
+
99
+ describe 'dynamic class creation' do
100
+ it 'infers the table_name from superclass not base_class' do
101
+ God = Class.new(Admin)
102
+
103
+ Hacker = Class.new(Admin) do
104
+ uses_mti
105
+ end
106
+
107
+ expect(God.table_name).to eql(Admin.table_name)
108
+ expect(Hacker.table_name).to eql('admin/hackers')
109
+ end
110
+ end
111
+
112
+ describe 'custom inheritance_column model' do
113
+ let!(:vehicle) { Transportation::Vehicle.create(color: :red) }
114
+ let!(:truck) { Transportation::Truck.create(color: :blue, bed_size: 10) }
115
+
116
+ describe 'inheritance_column' do
117
+ xit 'should set the custom column correctly' do
118
+ expect(vehicle.type).to eql('vehicles')
119
+ expect(truck.type).to eql('trucks')
120
+ end
121
+ end
122
+
123
+ describe 'base class querying' do
124
+ it 'casts children properly' do
125
+ expect(Transportation::Vehicle.all.select{ |v| v.is_a?(Transportation::Truck) }.count).to eql(1)
126
+ end
127
+
128
+ xit 'deserializes children with child specific data' do
129
+ my_truck = Transportation::Vehicle.find(truck.id)
130
+ expect(my_truck.bed_size).to eql(10)
131
+ end
132
+ end
133
+
134
+ describe 'has the correct count for' do
135
+ it 'parents' do
136
+ expect(Transportation::Vehicle.count).to eql(2)
137
+ end
138
+
139
+ it 'children' do
140
+ expect(Transportation::Truck.count).to eql(1)
141
+ end
142
+ end
143
+ end
144
+
145
+ end
@@ -0,0 +1,15 @@
1
+ require 'spec_helper'
2
+
3
+ describe ActiveRecord::MTI do
4
+
5
+ context 'helper' do
6
+
7
+ describe '#testify' do
8
+ it "returns true for truthy values" do
9
+ expect(ActiveRecord::MTI.testify('f')).to eq(false)
10
+ end
11
+ end
12
+
13
+ end
14
+
15
+ end
data/spec/schema.rb ADDED
@@ -0,0 +1,42 @@
1
+ ActiveRecord::Schema.define do
2
+ self.verbose = false
3
+
4
+ create_table :users, force: true do |t|
5
+ t.string :email
6
+ t.timestamps null: false
7
+ end
8
+
9
+ create_table :admins, force: true, inherits: :users do |t|
10
+ t.integer :god_powers
11
+ end
12
+
13
+ create_table 'admin/hackers', force: true, inherits: :admins do |t|
14
+ t.integer :god_powers
15
+ end
16
+
17
+ create_table :posts, force: true do |t|
18
+ t.integer :user_id
19
+ t.string :title
20
+ t.timestamps null: false
21
+ end
22
+
23
+ create_table :comments, force: true do |t|
24
+ t.integer :user_id
25
+ t.integer :post_id
26
+ t.timestamps null: false
27
+ end
28
+
29
+ #################################
30
+ ### Custom Inheritance Column ###
31
+ #################################
32
+
33
+ create_table :vehicles, force: true do |t|
34
+ t.string :color
35
+ t.string :type # Inheritance column
36
+ t.timestamps null: false
37
+ end
38
+
39
+ create_table "vehicles/trucks", force: true, inherits: :vehicles do |t|
40
+ t.integer :bed_size
41
+ end
42
+ end
@@ -0,0 +1,46 @@
1
+ ENV['RAILS_ENV'] = 'test'
2
+
3
+ require 'database_cleaner'
4
+
5
+ require 'simplecov'
6
+ SimpleCov.start do
7
+ # add_group 'Lib', 'lib'
8
+ add_filter 'spec'
9
+ end
10
+
11
+ require 'active_record/mti'
12
+
13
+ ActiveRecord::MTI.load
14
+
15
+ db_config = {
16
+ adapter: 'postgresql', database: 'active_record-mti-test'
17
+ }
18
+
19
+ db_config_admin = db_config.merge({ database: 'postgres', schema_search_path: 'public' })
20
+
21
+ ActiveRecord::Base.establish_connection db_config_admin
22
+ ActiveRecord::Base.connection.drop_database(db_config[:database])
23
+ ActiveRecord::Base.connection.create_database(db_config[:database])
24
+ ActiveRecord::Base.establish_connection db_config
25
+
26
+ load File.dirname(__FILE__) + '/schema.rb'
27
+
28
+ Dir[File.join(File.dirname(__FILE__), '..', 'spec', 'support', '**', '**.rb')].each do |f|
29
+ require f
30
+ end
31
+
32
+ RSpec.configure do |config|
33
+ config.order = 'random'
34
+
35
+ # Configure the DatabaseCleaner
36
+ config.before(:suite) do
37
+ DatabaseCleaner.strategy = :transaction
38
+ DatabaseCleaner.clean_with(:truncation)
39
+ end
40
+
41
+ config.around(:each) do |example|
42
+ DatabaseCleaner.cleaning do
43
+ example.run
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,34 @@
1
+ require 'active_record'
2
+
3
+ class Comment < ::ActiveRecord::Base
4
+ belongs_to :user
5
+ belongs_to :post
6
+
7
+ end
8
+
9
+ class Post < ::ActiveRecord::Base
10
+ belongs_to :user
11
+ has_many :comments
12
+ end
13
+
14
+ class User < ::ActiveRecord::Base
15
+ uses_mti
16
+
17
+ has_many :posts
18
+ has_many :comments
19
+
20
+ end
21
+
22
+ class Admin < User
23
+ self.table_name = 'admins'
24
+ end
25
+
26
+ module Transportation
27
+ class Vehicle < ::ActiveRecord::Base
28
+ uses_mti
29
+ end
30
+
31
+ class Truck < Vehicle
32
+ self.table_name = 'vehicles/trucks'
33
+ end
34
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: active_record-mti
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.3.0.pre.rc1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dale Stevens
8
8
  autorequire:
9
- bindir: exe
9
+ bindir: bin
10
10
  cert_chain: []
11
- date: 2017-09-20 00:00:00.000000000 Z
11
+ date: 2017-11-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: pg
@@ -30,34 +30,34 @@ dependencies:
30
30
  requirements:
31
31
  - - ">="
32
32
  - !ruby/object:Gem::Version
33
- version: '4.1'
33
+ version: '4'
34
34
  - - "<"
35
35
  - !ruby/object:Gem::Version
36
- version: '5'
36
+ version: '6'
37
37
  type: :runtime
38
38
  prerelease: false
39
39
  version_requirements: !ruby/object:Gem::Requirement
40
40
  requirements:
41
41
  - - ">="
42
42
  - !ruby/object:Gem::Version
43
- version: '4.1'
43
+ version: '4'
44
44
  - - "<"
45
45
  - !ruby/object:Gem::Version
46
- version: '5'
46
+ version: '6'
47
47
  - !ruby/object:Gem::Dependency
48
48
  name: pry-byebug
49
49
  requirement: !ruby/object:Gem::Requirement
50
50
  requirements:
51
- - - ">="
51
+ - - "~>"
52
52
  - !ruby/object:Gem::Version
53
- version: '0'
53
+ version: '3'
54
54
  type: :development
55
55
  prerelease: false
56
56
  version_requirements: !ruby/object:Gem::Requirement
57
57
  requirements:
58
- - - ">="
58
+ - - "~>"
59
59
  - !ruby/object:Gem::Version
60
- version: '0'
60
+ version: '3'
61
61
  - !ruby/object:Gem::Dependency
62
62
  name: bundler
63
63
  requirement: !ruby/object:Gem::Requirement
@@ -86,63 +86,7 @@ dependencies:
86
86
  - - "~>"
87
87
  - !ruby/object:Gem::Version
88
88
  version: '10.0'
89
- - !ruby/object:Gem::Dependency
90
- name: rspec
91
- requirement: !ruby/object:Gem::Requirement
92
- requirements:
93
- - - "~>"
94
- - !ruby/object:Gem::Version
95
- version: '3.0'
96
- type: :development
97
- prerelease: false
98
- version_requirements: !ruby/object:Gem::Requirement
99
- requirements:
100
- - - "~>"
101
- - !ruby/object:Gem::Version
102
- version: '3.0'
103
- - !ruby/object:Gem::Dependency
104
- name: simplecov
105
- requirement: !ruby/object:Gem::Requirement
106
- requirements:
107
- - - "~>"
108
- - !ruby/object:Gem::Version
109
- version: '0.1'
110
- type: :development
111
- prerelease: false
112
- version_requirements: !ruby/object:Gem::Requirement
113
- requirements:
114
- - - "~>"
115
- - !ruby/object:Gem::Version
116
- version: '0.1'
117
- - !ruby/object:Gem::Dependency
118
- name: codeclimate-test-reporter
119
- requirement: !ruby/object:Gem::Requirement
120
- requirements:
121
- - - "~>"
122
- - !ruby/object:Gem::Version
123
- version: '1.0'
124
- type: :development
125
- prerelease: false
126
- version_requirements: !ruby/object:Gem::Requirement
127
- requirements:
128
- - - "~>"
129
- - !ruby/object:Gem::Version
130
- version: '1.0'
131
- - !ruby/object:Gem::Dependency
132
- name: database_cleaner
133
- requirement: !ruby/object:Gem::Requirement
134
- requirements:
135
- - - "~>"
136
- - !ruby/object:Gem::Version
137
- version: '1.6'
138
- type: :development
139
- prerelease: false
140
- version_requirements: !ruby/object:Gem::Requirement
141
- requirements:
142
- - - "~>"
143
- - !ruby/object:Gem::Version
144
- version: '1.6'
145
- description: Allows use of native inherited tables in PostgreSQL
89
+ description: Gives ActiveRecord support for PostgreSQL's native inherited tables
146
90
  email:
147
91
  - dale@twilightcoders.net
148
92
  executables: []
@@ -152,11 +96,17 @@ files:
152
96
  - ".gitignore"
153
97
  - ".rspec"
154
98
  - ".travis.yml"
99
+ - CHANGELOG.md
155
100
  - Gemfile
156
101
  - LICENSE.txt
157
102
  - README.md
158
103
  - Rakefile
159
104
  - active_record-mti.gemspec
105
+ - gemfiles/activerecord-4.0.Gemfile
106
+ - gemfiles/activerecord-4.1.Gemfile
107
+ - gemfiles/activerecord-4.2.Gemfile
108
+ - gemfiles/activerecord-5.0.Gemfile
109
+ - gemfiles/activerecord-5.1.Gemfile
160
110
  - lib/active_record/mti.rb
161
111
  - lib/active_record/mti/calculations.rb
162
112
  - lib/active_record/mti/connection_adapters/postgresql/adapter.rb
@@ -168,7 +118,13 @@ files:
168
118
  - lib/active_record/mti/railtie.rb
169
119
  - lib/active_record/mti/schema_dumper.rb
170
120
  - lib/active_record/mti/version.rb
171
- homepage: ''
121
+ - spec/active_record/calculations_spec.rb
122
+ - spec/active_record/mti/inheritence_spec.rb
123
+ - spec/active_record/mti_spec.rb
124
+ - spec/schema.rb
125
+ - spec/spec_helper.rb
126
+ - spec/support/models.rb
127
+ homepage: https://github.com/twilightcoders/active_record-mti
172
128
  licenses:
173
129
  - MIT
174
130
  metadata:
@@ -177,20 +133,27 @@ post_install_message:
177
133
  rdoc_options: []
178
134
  require_paths:
179
135
  - lib
136
+ - spec
180
137
  required_ruby_version: !ruby/object:Gem::Requirement
181
138
  requirements:
182
139
  - - ">="
183
140
  - !ruby/object:Gem::Version
184
- version: 2.0.0
141
+ version: '2.0'
185
142
  required_rubygems_version: !ruby/object:Gem::Requirement
186
143
  requirements:
187
- - - ">="
144
+ - - ">"
188
145
  - !ruby/object:Gem::Version
189
- version: '0'
146
+ version: 1.3.1
190
147
  requirements: []
191
148
  rubyforge_project:
192
149
  rubygems_version: 2.6.11
193
150
  signing_key:
194
151
  specification_version: 4
195
152
  summary: Multi Table Inheritance for PostgreSQL in Rails
196
- test_files: []
153
+ test_files:
154
+ - spec/active_record/calculations_spec.rb
155
+ - spec/active_record/mti/inheritence_spec.rb
156
+ - spec/active_record/mti_spec.rb
157
+ - spec/schema.rb
158
+ - spec/spec_helper.rb
159
+ - spec/support/models.rb