active_record-mti 0.2.1 → 0.3.0.pre.rc1

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