active_record-mti 0.3.0.pre.rc1 → 0.3.0.pre.rc2

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: aed8fa7772f145269a995f4e2d139ebbb8670023
4
- data.tar.gz: c40c16ae6a90a66aa9dc3aa6839fe9678761218b
3
+ metadata.gz: d60ec74fdc811ec4b99d12daed96d8d44989026b
4
+ data.tar.gz: 5e11732fcfec73c329050915107a088876886b37
5
5
  SHA512:
6
- metadata.gz: 520d9654c833f837d3cb93e2320cfb44e86c18802f3d4df1e4b2edccbbec9b9621b257c372eb0ac0dfe288c92135d07593cb3af80c910bd3679a50319efd1562
7
- data.tar.gz: 7e51e4286a33db38af13e12ee6a5bb05f43519b4d8cb7e2be1bbfeef71c46caa370a25d691e80eccf9f61871717c0d130f4861f370c7d4aadcbd8e1639edb1ee
6
+ metadata.gz: 62ce99ba9014e671d53c84b1bab0ac37de5dc7ee3317767b27b3dcc8084b6f544b0a1ffe25674a83723b9f8ee8e19d4ccc0fa22ee728df9b21d2186075ee8ecf
7
+ data.tar.gz: 8fac323f73f8d83aeb417a28243c711ff3073d58a541179aaa5ccb9f985fe2329917c286368a17b9d50426304eb10d3661352ad6f0b1896ed4106e6e17bfdaa7
data/README.md CHANGED
@@ -27,26 +27,27 @@ Or install it yourself as:
27
27
 
28
28
  ### Application Code
29
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)
30
+ In most cases, you shouldn't have to do anything beyond installing the gem. `ActiveRecord::MTI` will do it's best to determine the nature of inheritance in your models. If your models map to their own tables, `ActiveRecord::MTI` will step in and make sure inheritance is treated appropriately. Otherwise it will gracefully aquiece to `ActiveRecord`'s built-in `STI`.
34
31
 
35
32
  ```ruby
36
33
  class Account < ::ActiveRecord::Base
37
- uses_mti
38
-
34
+ # ...
39
35
  end
40
36
 
41
37
  class User < Account
42
-
38
+ # ...
43
39
  end
44
40
 
45
41
  class Developer < Account
46
-
42
+ # ...
47
43
  end
48
44
  ```
49
45
 
46
+ `ActiveRecord` queries work as usual with the following differences:
47
+
48
+ - 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)
49
+
50
+ Note
50
51
  ### Migrations
51
52
 
52
53
  In your migrations define a table to inherit from another table:
@@ -1,20 +1,19 @@
1
1
  module ActiveRecord
2
2
  module MTI
3
3
  module Calculations
4
- extend ActiveSupport::Concern
5
4
 
6
5
  private
7
6
 
8
7
  def perform_calculation(*args)
9
- result = swap_and_restore_tableoid_cast(true) do
8
+ swap_and_restore_tableoid_cast(true) do
10
9
  super
11
10
  end
12
11
  end
13
12
 
14
- def swap_and_restore_tableoid_cast(value, &block)
13
+ def swap_and_restore_tableoid_cast(value)
15
14
  orignal_value = Thread.current['skip_tableoid_cast']
16
15
  Thread.current['skip_tableoid_cast'] = value
17
- return_value = yield
16
+ return_value = yield if block_given?
18
17
  Thread.current['skip_tableoid_cast'] = orignal_value
19
18
  return return_value
20
19
  end
@@ -21,13 +21,13 @@ module ActiveRecord
21
21
  options.delete(:primary_key)
22
22
  end
23
23
 
24
- if schema = options.delete(:schema)
24
+ if (schema = options.delete(:schema))
25
25
  # If we specify a schema then we only create it if it doesn't exist
26
26
  # and we only force create it if only the specific schema is in the search path
27
27
  table_name = %Q("#{schema}"."#{table_name}")
28
28
  end
29
29
 
30
- if inherited_table = options.delete(:inherits)
30
+ if (inherited_table = options.delete(:inherits))
31
31
  # options[:options] = options[:options].sub("INHERITS", "() INHERITS") if td.columns.empty?
32
32
  options[:options] = [%Q(INHERITS ("#{inherited_table}")), options[:options]].compact.join
33
33
  end
@@ -1,183 +1,126 @@
1
- require 'active_support/concern'
2
-
3
1
  module ActiveRecord
4
2
  # == Multi-Table Inheritance
5
3
  module MTI
6
4
  module Inheritance
7
- extend ActiveSupport::Concern
8
-
9
- included do
10
- @@mti_tableoids = {}
11
- scope :discern_inheritance, -> {
12
5
 
13
- }
6
+ def self.prepended(subclass)
7
+ subclass.extend(ClassMethods)
8
+ class << subclass
9
+ class_attribute :mti_type_column
10
+ class_attribute :tableoid_column
11
+ end
14
12
  end
15
13
 
16
14
  module ClassMethods
15
+ def has_tableoid_column?
16
+ tableoid_column != false
17
+ end
17
18
 
18
- @uses_mti = nil
19
- @mti_setup = false
20
- @mti_type_column = nil
21
-
22
- def uses_mti(custom_table_name = nil, inheritance_column = nil)
23
- self.inheritance_column = inheritance_column
24
-
25
- @uses_mti = true
26
- @tableoid_column = nil
19
+ def inherited(subclass)
20
+ super
21
+ subclass.using_multi_table_inheritance?
27
22
  end
28
23
 
29
- def using_multi_table_inheritance?(klass = self)
30
- klass.uses_mti?
24
+ def uses_mti(*args)
25
+ # ActiveRecord::MTI.logger.
26
+ warn "DEPRECATED - `uses_mti` is no longer needed (nor has any effect)"
31
27
  end
32
28
 
33
- def uses_mti?
34
- inheritance_check = check_inheritance_of(@table_name) unless @mti_setup
29
+ def using_multi_table_inheritance?
30
+ mti = ActiveRecord::MTI::Registry.tableoid?(self)
31
+ return (mti != false) unless mti == nil
35
32
 
36
- if @uses_mti.nil? && @uses_mti = inheritance_check
37
- descendants.each do |d|
38
- d.uses_mti?
33
+ if (mti = check_inheritance_of(@table_name))
34
+ if (self != base_class && self.table_name == base_class.table_name)
35
+ mti = false
36
+ else
37
+ mti = detect_tableoid(table_name)
39
38
  end
40
39
  end
41
40
 
42
- @uses_mti
43
- end
41
+ ActiveRecord::MTI::Registry[self] = mti
44
42
 
45
- def has_tableoid_column?
46
- @tableoid_column != false
47
- end
48
-
49
- def mti_type_column
50
- @mti_type_column
51
- end
43
+ descendants.each do |d|
44
+ d.using_multi_table_inheritance?
45
+ end
52
46
 
53
- def mti_type_column=(value)
54
- @mti_type_column = value
47
+ return mti && mti != false
55
48
  end
56
49
 
57
50
  private
58
51
 
59
- def check_inheritance_of(table_name)
60
- ActiveRecord::MTI.logger.debug "Trying to check inheritance of table with no table name (#{self})" unless table_name
61
- return nil unless table_name
62
-
63
- ActiveRecord::MTI.logger.debug "Checking inheritance for #{table_name}"
52
+ def check_inheritance_of(table_name, table_schema = 'public')
53
+ ActiveRecord::MTI.logger.debug("Trying to check inheritance of table with no table name (#{self})") and return nil unless table_name
54
+ ActiveRecord::MTI.logger.debug "Checking inheritance for #{table_schema}.#{table_name}"
64
55
 
65
56
  result = connection.execute <<-SQL
66
57
  SELECT EXISTS (
67
58
  SELECT 1
68
- FROM pg_catalog.pg_inherits AS i
69
- LEFT JOIN pg_catalog.pg_rewrite AS r ON r.ev_class = 'public.#{table_name}'::regclass::oid
70
- LEFT JOIN pg_catalog.pg_depend AS d ON d.objid = r.oid
71
- LEFT JOIN pg_catalog.pg_class AS cl_d ON cl_d.oid = d.refobjid
72
- WHERE inhrelid = COALESCE(cl_d.relname, 'public.#{table_name}')::regclass::oid
73
- OR inhparent = COALESCE(cl_d.relname, 'public.#{table_name}')::regclass::oid
59
+ FROM pg_catalog.pg_inherits AS i
60
+ JOIN information_schema.tables AS t ON t.table_schema = '#{table_schema}' AND t.table_name = '#{table_name}'
61
+ LEFT JOIN pg_catalog.pg_rewrite AS r ON r.ev_class = t.table_name::regclass::oid
62
+ LEFT JOIN pg_catalog.pg_depend AS d ON d.objid = r.oid
63
+ LEFT JOIN pg_catalog.pg_class AS c ON c.oid = d.refobjid
64
+ WHERE i.inhrelid = COALESCE(c.relname, t.table_name)::regclass::oid
65
+ OR i.inhparent = COALESCE(c.relname, t.table_name)::regclass::oid
74
66
  ) AS uses_inheritance;
75
67
  SQL
76
68
 
77
- uses_inheritance = ActiveRecord::MTI.testify(result.try(:first)['uses_inheritance'])
78
-
79
- register_tableoid(table_name) if uses_inheritance
80
-
81
- @mti_setup = true
82
- # Some versions of PSQL return {"?column?"=>"t"}
83
- # instead of {"exists"=>"t"}, so we're saying screw it,
84
- # just give me the first value of whatever is returned
85
-
86
- # Ensure a boolean is returned
87
- return uses_inheritance == true
69
+ return ActiveRecord::MTI.testify(result.try(:first)['uses_inheritance']) == true
88
70
  end
89
71
 
90
- def register_tableoid(table_name)
72
+ def detect_tableoid(table_name, table_schema = 'public')
91
73
 
92
74
  tableoid_query = connection.execute(<<-SQL
93
- SELECT '#{table_name}'::regclass::oid AS tableoid, (SELECT EXISTS (
94
- SELECT 1
75
+ SELECT 1 AS has_tableoid_column, t.table_name::regclass::oid as tableoid
95
76
  FROM pg_catalog.pg_attribute
96
- WHERE attrelid = '#{table_name}'::regclass
77
+ JOIN information_schema.tables t ON t.table_schema = '#{table_schema}' AND t.table_name = '#{table_name}'
78
+ WHERE attrelid = t.table_name::regclass
97
79
  AND attname = 'tableoid'
98
- AND NOT attisdropped
99
- )) AS has_tableoid_column
80
+ AND NOT attisdropped;
100
81
  SQL
101
82
  ).first
102
- tableoid = tableoid_query['tableoid']
103
- @tableoid_column = ActiveRecord::MTI.testify(tableoid_query['has_tableoid_column'])
83
+
84
+ tableoid = tableoid_query.try(:[], 'tableoid') || false
85
+ self.tableoid_column = ActiveRecord::MTI.testify(tableoid_query.try(:[], 'has_tableoid_column'))
104
86
 
105
87
  if (has_tableoid_column?)
106
- ActiveRecord::MTI.logger.debug "#{table_name} has tableoid column! (#{tableoid})"
88
+ ActiveRecord::MTI.logger.debug "#{table_schema}.#{table_name} has tableoid column! (#{tableoid})"
107
89
  add_tableoid_column
108
- @mti_type_column = arel_table[:tableoid]
90
+ self.mti_type_column = arel_table[:tableoid]
109
91
  else
110
- @mti_type_column = nil
92
+ self.mti_type_column = nil
111
93
  end
112
94
 
113
- Inheritance.add_mti(tableoid, self)
95
+ tableoid
114
96
  end
115
97
 
116
98
  # Called by +instantiate+ to decide which class to use for a new
117
99
  # record instance. For single-table inheritance, we check the record
118
100
  # for a +type+ column and return the corresponding class.
119
101
  def discriminate_class_for_record(record)
120
- if using_multi_table_inheritance?(base_class)
121
- find_mti_class(record) || base_class
122
- elsif using_single_table_inheritance?(record)
123
- find_sti_class(record[inheritance_column])
102
+ if using_multi_table_inheritance?
103
+ ActiveRecord::MTI::Registry.find_mti_class(record['tableoid']) || self
124
104
  else
125
105
  super
126
106
  end
127
107
  end
128
108
 
129
- # Search descendants for one who's table_name is equal to the returned tableoid.
130
- # This indicates the class of the record
131
- def find_mti_class(record)
132
- if (has_tableoid_column?)
133
- Inheritance.find_mti(record['tableoid'])
134
- else
135
- self
136
- end
137
- end
138
-
139
109
  # Type condition only applies if it's STI, otherwise it's
140
110
  # done for free by querying the inherited table in MTI
141
111
  def type_condition(table = arel_table)
142
- if using_multi_table_inheritance?
143
- nil
144
- else
145
- sti_column = table[inheritance_column]
146
- sti_names = ([self] + descendants).map { |model| model.sti_name }
147
-
148
- sti_column.in(sti_names)
149
- end
112
+ return nil if using_multi_table_inheritance?
113
+ super
150
114
  end
151
115
 
152
116
  def add_tableoid_column
153
117
  if self.respond_to? :attribute
154
- self.attribute :tableoid, get_integer_oid_class.new
118
+ self.attribute :tableoid, ActiveRecord::MTI.oid_class.new
155
119
  else
156
- columns.unshift ActiveRecord::ConnectionAdapters::PostgreSQLColumn.new('tableoid', nil, ActiveRecord::ConnectionAdapters::PostgreSQLAdapter::OID::Integer.new, "oid", false)
120
+ columns.unshift ActiveRecord::ConnectionAdapters::PostgreSQLColumn.new('tableoid', nil, ActiveRecord::MTI.oid_class.new, "oid", false)
157
121
  end
158
122
  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
-
171
123
  end
172
-
173
- def self.add_mti(tableoid, klass)
174
- @@mti_tableoids[tableoid.to_s.to_sym] = klass
175
- end
176
-
177
- def self.find_mti(tableoid)
178
- @@mti_tableoids[tableoid.to_s.to_sym]
179
- end
180
-
181
124
  end
182
125
  end
183
126
  end
@@ -1,35 +1,15 @@
1
1
  module ActiveRecord
2
2
  module MTI
3
3
  module ModelSchema
4
- extend ActiveSupport::Concern
5
4
 
5
+ def self.prepended(base)
6
+ base.extend(ClassMethods)
7
+ end
6
8
 
7
9
  module ClassMethods
8
-
9
- # Computes the table name, (re)sets it internally, and returns it.
10
- def reset_table_name #:nodoc:
11
- self.table_name = if abstract_class?
12
- superclass == Base ? nil : superclass.table_name
13
- elsif superclass.abstract_class?# || superclass.using_multi_table_inheritance?
14
- superclass.table_name || compute_table_name
15
- else
16
- compute_table_name
17
- end
18
- end
19
-
20
10
  # Computes and returns a table name according to default conventions.
21
11
  def compute_table_name
22
- base = base_class
23
- if self == base
24
- # Nested classes are prefixed with singular parent table name.
25
- if parent < Base && !parent.abstract_class?
26
- contained = parent.table_name
27
- contained = contained.singularize if parent.pluralize_table_names
28
- contained += '_'
29
- end
30
-
31
- "#{full_table_name_prefix}#{contained}#{undecorated_table_name(name)}#{full_table_name_suffix}"
32
- elsif uses_mti?
12
+ if self != base_class
33
13
  # Nested classes are prefixed with singular parent table name.
34
14
  if superclass < Base && !superclass.abstract_class?
35
15
  contained = superclass.table_name
@@ -37,30 +17,39 @@ module ActiveRecord
37
17
  contained += '/'
38
18
  end
39
19
 
40
- "#{full_table_name_prefix}#{contained}#{decorated_table_name(name)}#{full_table_name_suffix}"
20
+ potential_table_name = "#{full_table_name_prefix}#{contained}#{decorated_table_name(name)}#{full_table_name_suffix}"
21
+
22
+ if check_inheritance_of(potential_table_name)
23
+ potential_table_name
24
+ else
25
+ superclass.table_name
26
+ end
41
27
  else
42
- # STI subclasses always use their superclass' table.
43
- superclass.table_name
28
+ super
44
29
  end
45
30
  end
46
31
 
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
32
  def full_table_name_suffix #:nodoc:
52
- (parents.detect {|p| p.respond_to?(:table_name_suffix) } || self).table_name_suffix
33
+ super
34
+ rescue NoMethodError
35
+ full_table_name_rescue(:table_name_suffix)
53
36
  end
54
37
 
55
38
  private
56
39
 
40
+ def full_table_name_rescue(which)
41
+ (parents.detect{ |p| p.respond_to?(which) } || self).send(which)
42
+ end
43
+
57
44
  # Guesses the table name, but does not decorate it with prefix and suffix information.
58
45
  def decorated_table_name(class_name = base_class.name)
46
+ super
47
+ rescue NoMethodError
59
48
  table_name = class_name.to_s.underscore
60
49
  pluralize_table_names ? table_name.pluralize : table_name
61
50
  end
62
-
63
51
  end
52
+
64
53
  end
65
54
  end
66
55
  end
@@ -1,7 +1,6 @@
1
1
  module ActiveRecord
2
2
  module MTI
3
3
  module QueryMethods
4
- extend ActiveSupport::Concern
5
4
 
6
5
  def build_arel
7
6
  select_by_tableoid = select_values.delete(:tableoid) == :tableoid
@@ -21,28 +20,16 @@ module ActiveRecord
21
20
 
22
21
  def tableoid?(klass)
23
22
  !Thread.current['skip_tableoid_cast'] &&
24
- @klass.using_multi_table_inheritance? &&
25
- @klass.has_tableoid_column?
26
- end
27
-
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?
23
+ klass.using_multi_table_inheritance? &&
24
+ klass.has_tableoid_column?
36
25
  end
37
26
 
38
27
  def tableoid_project(klass)
39
- # Arel::Nodes::NamedFunction.new('CAST', [klass.arel_table[:tableoid].as('regclass')])
40
- # Arel::Nodes::NamedFunction.new('CAST', [@klass.arel_table['tableoid::regclass'].as('regclass')])
41
- @klass.mti_type_column.as('tableoid')
28
+ klass.mti_type_column.as('tableoid')
42
29
  end
43
30
 
44
31
  def tableoid_group(klass)
45
- @klass.mti_type_column
32
+ klass.mti_type_column
46
33
  end
47
34
 
48
35
  end
@@ -0,0 +1,23 @@
1
+ module ActiveRecord
2
+ module MTI
3
+ module Registry
4
+
5
+ mattr_accessor :tableoids
6
+ self.tableoids = { ActiveRecord::Base => false }
7
+
8
+ def self.[]=(klass, tableoid)
9
+ ActiveRecord::MTI.logger.debug "Adding #{klass} to MTI list with #{tableoid}"
10
+ tableoids[klass] = tableoid
11
+ end
12
+
13
+ def self.find_mti_class(tableoid)
14
+ tableoids.key(tableoid)
15
+ end
16
+
17
+ def self.tableoid?(klass)
18
+ tableoids[klass]
19
+ end
20
+
21
+ end
22
+ end
23
+ end
@@ -1,5 +1,3 @@
1
- require 'active_support/concern'
2
-
3
1
  # Modified SchemaDumper that knows how to dump
4
2
  # inherited tables. Key is that we have to dump parent
5
3
  # tables before we dump child tables (of course).
@@ -12,169 +10,68 @@ module ActiveRecord
12
10
  # output format (i.e., ActiveRecord::Schema).
13
11
  module MTI
14
12
  module SchemaDumper #:nodoc:
15
- extend ActiveSupport::Concern
16
-
17
-
18
- included do
19
-
20
- private
21
-
22
- def dumped_tables
23
- @dumped_tables ||= []
24
- end
25
-
26
- # Output table and columns - but don't output columns that are inherited from
27
- # a parent table.
28
- #
29
- # TODO: Qualify with the schema name IF the table is in a schema other than the first
30
- # schema in the search path (not including the $user schema)
31
- def table(table, stream)
32
- return if already_dumped?(table)
33
-
34
- if parent_table = @connection.parent_table(table)
35
- table(parent_table, stream)
36
- parent_column_names = @connection.columns(parent_table).map(&:name)
37
- end
38
-
39
- columns = @connection.columns(table)
40
- begin
41
- tbl = StringIO.new
42
-
43
- # first dump primary key column
44
- pk = @connection.primary_key(table)
45
-
46
- tbl.print " create_table #{remove_prefix_and_suffix(table).inspect}"
47
- if parent_table
48
- tbl.print %Q(, inherits: '#{parent_table}')
49
- else
50
- pkcol = columns.detect { |c| c.name == pk }
51
- if pkcol
52
- if pk != 'id'
53
- tbl.print %Q(, primary_key: '#{pk}')
54
- elsif pkcol.sql_type == 'bigint'
55
- tbl.print ", id: :bigserial"
56
- elsif pkcol.sql_type == 'uuid'
57
- tbl.print ", id: :uuid"
58
- tbl.print %Q(, default: #{pkcol.default_function.inspect})
59
- end
60
- else
61
- tbl.print ", id: false"
62
- end
63
- tbl.print ", force: :cascade"
64
- end
65
- tbl.puts " do |t|"
66
-
67
- # then dump all non-primary key columns
68
- column_specs = columns.map do |column|
69
- raise StandardError, "Unknown type '#{column.sql_type}' for column '#{column.name}'" unless @connection.valid_type?(column.type)
70
- next if column.name == pk
71
-
72
- # Except columns in parent table
73
- next if parent_column_names && parent_column_names.include?(column.name)
74
-
75
- @connection.column_spec(column, @types)
76
- end.compact
77
-
78
- # find all migration keys used in this table
79
- keys = @connection.migration_keys
80
-
81
- # figure out the lengths for each column based on above keys
82
- lengths = keys.map { |key|
83
- column_specs.map { |spec|
84
- spec[key] ? spec[key].length + 2 : 0
85
- }.max
86
- }
87
-
88
- # the string we're going to sprintf our values against, with standardized column widths
89
- format_string = lengths.map{ |len| "%-#{len}s" }
90
-
91
- # find the max length for the 'type' column, which is special
92
- type_length = column_specs.map{ |column| column[:type].length }.max
93
13
 
94
- # add column type definition to our format string
95
- format_string.unshift " t.%-#{type_length}s "
96
-
97
- format_string *= ''
14
+ def dumped_tables
15
+ @dumped_tables ||= []
16
+ end
98
17
 
99
- column_specs.each do |colspec|
100
- values = keys.zip(lengths).map{ |key, len| colspec.key?(key) ? colspec[key] + ", " : " " * len }
101
- values.unshift colspec[:type]
102
- tbl.print((format_string % values).gsub(/,\s*$/, ''))
103
- tbl.puts
104
- end
18
+ # Output table and columns - but don't output columns that are inherited from
19
+ # a parent table.
20
+ def table(table, stream)
21
+ return if already_dumped?(table)
105
22
 
106
- tbl.puts " end"
107
- tbl.puts
23
+ new_stream = StringIO.new
24
+ super(table, new_stream)
25
+ string = new_stream.string
108
26
 
109
- indexes(table, tbl)
27
+ if (parent_table = @connection.parent_table(table))
28
+ table(parent_table, stream)
29
+ string = inject_inherits_for_create_table(string, table, parent_table)
30
+ string = remove_parent_table_columns(string, @connection.columns(parent_table))
110
31
 
111
- tbl.rewind
112
- stream.print tbl.read
113
- rescue => e
114
- stream.puts "# Could not dump table #{table.inspect} because of following #{e.class}"
115
- stream.puts "# #{e.message}"
116
- stream.puts
117
- end
32
+ pindexes = Hash[@connection.indexes(parent_table).map { |index| [index.columns, index] }]
33
+ cindexes = Hash[@connection.indexes(table).map { |index| [index.columns, index] }]
118
34
 
119
- dumped_tables << table
120
- stream
35
+ string = remove_parent_table_indexes(string, (pindexes & cindexes).values)
121
36
  end
122
37
 
123
- # Output indexes but don't output indexes that are inherited from parent tables
124
- # since those will be created by create_table.
125
- def indexes(table, stream)
126
- if (indexes = @connection.indexes(table)).any?
127
- if parent_table = @connection.parent_table(table)
128
- parent_indexes = @connection.indexes(parent_table)
129
- end
130
-
131
- indexes.delete_if {|i| is_parent_index?(i, parent_indexes) } if parent_indexes
132
- return if indexes.empty?
133
-
134
- add_index_statements = indexes.map do |index|
135
- statement_parts = [
136
- ('add_index ' + remove_prefix_and_suffix(index.table).inspect),
137
- index.columns.inspect,
138
- ('name: ' + index.name.inspect),
139
- ]
140
- statement_parts << 'unique: true' if index.unique
141
-
142
- index_lengths = (index.lengths || []).compact
143
- statement_parts << ('length: ' + Hash[index.columns.zip(index.lengths)].inspect) unless index_lengths.empty?
38
+ # We've done this table
39
+ dumped_tables << table
144
40
 
145
- index_orders = (index.orders || {})
146
- statement_parts << ('order: ' + index.orders.inspect) unless index_orders.empty?
147
-
148
- statement_parts << ('where: ' + index.where.inspect) if index.where
149
-
150
- statement_parts << ('using: ' + index.using.inspect) if index.using
151
-
152
- statement_parts << ('type: ' + index.type.inspect) if index.type
41
+ stream.write string
42
+ stream
43
+ end
153
44
 
154
- ' ' + statement_parts.join(', ')
155
- end
45
+ def inject_inherits_for_create_table(string, table, parent_table)
46
+ tbl_start = "create_table #{remove_prefix_and_suffix(table).inspect}"
47
+ tbl_end = " do |t|"
48
+ tbl_inherit = ", inherits: '#{parent_table}'"
49
+ string.gsub!(/#{Regexp.escape(tbl_start)}.*#{Regexp.escape(tbl_end)}/, tbl_start + tbl_inherit + tbl_end)
50
+ end
156
51
 
157
- stream.puts add_index_statements.sort.join("\n")
158
- stream.puts
159
- end
52
+ def remove_parent_table_columns(string, columns)
53
+ columns.each do |col|
54
+ string.gsub!(/\s+t\.\w+\s+("|')#{col.name}("|').*/, '')
160
55
  end
56
+ string
57
+ end
161
58
 
162
-
163
- def remove_prefix_and_suffix(table)
164
- table.gsub(/^(#{ActiveRecord::Base.table_name_prefix})(.+)(#{ActiveRecord::Base.table_name_suffix})$/, "\\2")
59
+ def remove_parent_table_indexes(string, indexes)
60
+ indexes.each do |index|
61
+ string.gsub!(/\s*add_index .*name: #{Regexp.escape(index.name.inspect)}.*/, '') # Rails 4.x
62
+ string.gsub!(/\s+t\.index.*("|')#{index.name}("|').*/, '') # Rails 5.x
165
63
  end
64
+ string
65
+ end
166
66
 
167
- def already_dumped?(table)
168
- dumped_tables.include? table
169
- end
67
+ def remove_prefix_and_suffix(table)
68
+ table.gsub(/^(#{ActiveRecord::Base.table_name_prefix})(.+)(#{ActiveRecord::Base.table_name_suffix})$/, "\\2")
69
+ end
170
70
 
171
- def is_parent_index?(index, parent_indexes)
172
- parent_indexes.each do |pindex|
173
- return true if pindex.columns == index.columns
174
- end
175
- return false
176
- end
71
+ def already_dumped?(table)
72
+ dumped_tables.include? table
177
73
  end
74
+
178
75
  end
179
76
  end
180
77
  end
@@ -1,5 +1,5 @@
1
1
  module ActiveRecord
2
2
  module MTI
3
- VERSION = '0.3.0-rc1'
3
+ VERSION = '0.3.0-rc2'
4
4
  end
5
5
  end
@@ -1,7 +1,14 @@
1
1
  require 'active_record/mti/version'
2
+
3
+ require 'active_support/all'
4
+
2
5
  require 'active_record'
3
6
  require 'active_record/connection_handling'
7
+
8
+ require 'core_ext/hash'
9
+
4
10
  require 'active_record/mti/schema_dumper'
11
+ require 'active_record/mti/registry'
5
12
  require 'active_record/mti/inheritance'
6
13
  require 'active_record/mti/model_schema'
7
14
  require 'active_record/mti/query_methods'
@@ -14,6 +21,24 @@ require 'active_record/mti/railtie' if defined?(Rails::Railtie)
14
21
  module ActiveRecord
15
22
  module MTI
16
23
 
24
+ # Rails likes to make breaking changes in it's minor versions (like 4.1 - 4.2) :P
25
+ mattr_accessor :oid_class
26
+
27
+ # Cannot assign default inside block because of rails 4.0
28
+ self.oid_class =
29
+ [
30
+ '::ActiveRecord::ConnectionAdapters::PostgreSQLAdapter::OID::Integer', # 4.0, 4.1
31
+ '::ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Integer', # 4.2
32
+ '::ActiveRecord::Type::Integer' # 5.0, 5.1
33
+ ].find(nil) { |klass|
34
+ begin
35
+ klass.constantize
36
+ true
37
+ rescue NameError
38
+ false
39
+ end
40
+ }.constantize
41
+
17
42
  class << self
18
43
  attr_writer :logger
19
44
 
@@ -30,13 +55,13 @@ module ActiveRecord
30
55
  end
31
56
 
32
57
  def self.load
33
- ::ActiveRecord::Base.send :include, Inheritance
34
- ::ActiveRecord::Base.send :include, ModelSchema
58
+ ::ActiveRecord::Base.send :prepend, ModelSchema
59
+ ::ActiveRecord::Base.send :prepend, Inheritance
35
60
  ::ActiveRecord::Relation.send :prepend, QueryMethods
36
61
  ::ActiveRecord::Relation.send :prepend, Calculations
37
62
  ::ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.send :prepend, ConnectionAdapters::PostgreSQL::Adapter
38
- ::ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.send :include, ConnectionAdapters::PostgreSQL::SchemaStatements
39
- ::ActiveRecord::SchemaDumper.send :include, SchemaDumper
63
+ ::ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.send :prepend, ConnectionAdapters::PostgreSQL::SchemaStatements
64
+ ::ActiveRecord::SchemaDumper.send :prepend, SchemaDumper
40
65
  end
41
66
 
42
67
  def self.testify(value)
@@ -0,0 +1,7 @@
1
+ class Hash
2
+
3
+ def &(other)
4
+ Hash[(self.keys & other.keys).zip(other.values_at(*(self.keys & other.keys)))]
5
+ end
6
+
7
+ end
@@ -2,28 +2,33 @@ require 'spec_helper'
2
2
 
3
3
  describe ActiveRecord::MTI::Inheritance do
4
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)
5
+ it "creates a column even if class doesn't respond to :attribute" do
6
+ allow(Admin).to receive(:respond_to?).with(:attribute).and_return(false)
7
+
8
+ ActiveRecord::MTI::Registry.tableoids[Admin] = nil
9
+
10
+ expect(Admin.using_multi_table_inheritance?).to eq(true)
8
11
  end
9
12
 
10
13
  context 'class definition' do
11
14
 
12
15
  describe 'for classes that use MTI' do
13
16
  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
17
+ # Due to the anonymous class ("god = Class.new(Admin)") rspec can't properly distinquish
18
+ # between the two classes. So at most 2 times!
19
+ expect(Admin).to receive(:check_inheritance_of).and_call_original.at_most(2).times
16
20
 
17
21
  Admin.create(email: 'foo@bar.baz', god_powers: 3)
18
22
  Admin.create(email: 'foo2@bar.baz', god_powers: 3)
19
23
  Admin.create(email: 'foo24@bar.baz', god_powers: 3)
24
+ Admin.create(email: 'foo246@bar.baz', god_powers: 3)
20
25
 
21
26
  end
22
27
  end
23
28
 
24
29
  describe "for classes that don't use MTI" do
25
30
  it "doesn't check inheritance multiple times" do
26
- Post.instance_variable_set(:@uses_mti, nil)
31
+ # ActiveRecord::MTI::Inheritance.register(Post, false)
27
32
  expect(Post).to receive(:check_inheritance_of).and_call_original.exactly(1).times
28
33
 
29
34
  Post.create(title: 'foo@bar.baz')
@@ -73,19 +78,34 @@ describe ActiveRecord::MTI::Inheritance do
73
78
  god = Class.new(Admin)
74
79
  expect(god.table_name).to eql(Admin.table_name)
75
80
  end
81
+
82
+ it 'infers the table_name when defined dynamically' do
83
+
84
+ class Scrub < ActiveRecord::Base
85
+ const_set(:All, Class.new(Scrub) do |klass|
86
+ class_eval <<-AAA
87
+ self.table_name = 'scrubs/all'
88
+ AAA
89
+ end)
90
+ end
91
+
92
+ expect(Scrub::All.table_name).to eq('scrubs/all')
93
+ end
76
94
  end
77
95
  end
78
96
 
79
97
  describe 'views' do
80
98
  before(:each) do
99
+
100
+ User.connection.execute <<-SQL
101
+ CREATE OR REPLACE VIEW "users_all"
102
+ AS SELECT * FROM "users"
103
+ SQL
104
+
81
105
  class UserView < User
82
106
  self.table_name = "users_all"
83
107
  end
84
108
 
85
- UserView.connection.execute <<-SQL
86
- CREATE OR REPLACE VIEW "users_all"
87
- AS #{ User.all.to_sql }
88
- SQL
89
109
  end
90
110
 
91
111
  if ActiveRecord::Base.connection.version >= Gem::Version.new('9.4')
@@ -99,10 +119,7 @@ describe ActiveRecord::MTI::Inheritance do
99
119
  describe 'dynamic class creation' do
100
120
  it 'infers the table_name from superclass not base_class' do
101
121
  God = Class.new(Admin)
102
-
103
- Hacker = Class.new(Admin) do
104
- uses_mti
105
- end
122
+ Hacker = Class.new(Admin)
106
123
 
107
124
  expect(God.table_name).to eql(Admin.table_name)
108
125
  expect(Hacker.table_name).to eql('admin/hackers')
@@ -0,0 +1,11 @@
1
+ require 'spec_helper'
2
+
3
+ describe ActiveRecord::MTI::ModelSchema do
4
+
5
+ it 'rescues suffix' do
6
+ f = ActiveRecord::ModelSchema::ClassMethods
7
+ allow_any_instance_of(f).to receive(:full_table_name_suffix).and_raise(NoMethodError)
8
+ expect( Admin.full_table_name_suffix ).to eq("")
9
+ end
10
+
11
+ end
@@ -0,0 +1,22 @@
1
+ require 'spec_helper'
2
+
3
+ describe ActiveRecord::MTI::SchemaDumper do
4
+
5
+ before(:each) do
6
+ ActiveRecord::SchemaMigration.create_table
7
+ end
8
+
9
+ let(:hacker_sql) {
10
+ <<-FOO
11
+ create_table "admin/hackers", inherits: 'admins' do |t|
12
+ end
13
+ FOO
14
+ }
15
+
16
+ it 'does not dump indexes for child table' do
17
+ stream = StringIO.new
18
+ ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, stream)
19
+
20
+ expect(stream.string).to include(hacker_sql)
21
+ end
22
+ end
@@ -0,0 +1,24 @@
1
+ require 'spec_helper'
2
+
3
+ describe ActiveRecord::Inheritance do
4
+
5
+ context 'class definition' do
6
+
7
+ describe 'for classes that use STI' do
8
+ it "doesn't check inheritance multiple times" do
9
+
10
+ Transportation::Military::Vehicle.create(color: :red)
11
+ Transportation::Military::Vehicle.create(color: :blue)
12
+ Transportation::Military::Vehicle.create(color: :green)
13
+ Transportation::Military::Vehicle.create(color: :gold)
14
+
15
+ vehicle = Transportation::Military::Vehicle.first
16
+ expect(vehicle.class.name).to eq('Transportation::Military::Vehicle')
17
+ expect(vehicle.color).to eq('red')
18
+
19
+ end
20
+ end
21
+
22
+ end
23
+
24
+ end
data/spec/schema.rb CHANGED
@@ -1,8 +1,8 @@
1
1
  ActiveRecord::Schema.define do
2
2
  self.verbose = false
3
3
 
4
- create_table :users, force: true do |t|
5
- t.string :email
4
+ create_table :users, schema: :public, force: true do |t|
5
+ t.string :email, index: :btree
6
6
  t.timestamps null: false
7
7
  end
8
8
 
data/spec/spec_helper.rb CHANGED
@@ -25,7 +25,7 @@ ActiveRecord::Base.establish_connection db_config
25
25
 
26
26
  load File.dirname(__FILE__) + '/schema.rb'
27
27
 
28
- Dir[File.join(File.dirname(__FILE__), '..', 'spec', 'support', '**', '**.rb')].each do |f|
28
+ Dir[ActiveRecord::MTI.root.join('spec', 'support', '**', '**.rb')].each do |f|
29
29
  require f
30
30
  end
31
31
 
@@ -12,7 +12,6 @@ class Post < ::ActiveRecord::Base
12
12
  end
13
13
 
14
14
  class User < ::ActiveRecord::Base
15
- uses_mti
16
15
 
17
16
  has_many :posts
18
17
  has_many :comments
@@ -25,10 +24,15 @@ end
25
24
 
26
25
  module Transportation
27
26
  class Vehicle < ::ActiveRecord::Base
28
- uses_mti
29
27
  end
30
28
 
31
29
  class Truck < Vehicle
32
30
  self.table_name = 'vehicles/trucks'
33
31
  end
32
+
33
+ module Military
34
+ class Vehicle < ::Transportation::Vehicle
35
+ self.inheritance_column = 'type'
36
+ end
37
+ end
34
38
  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.3.0.pre.rc1
4
+ version: 0.3.0.pre.rc2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dale Stevens
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-11-13 00:00:00.000000000 Z
11
+ date: 2017-11-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: pg
@@ -116,11 +116,16 @@ files:
116
116
  - lib/active_record/mti/query_methods.rb
117
117
  - lib/active_record/mti/querying.rb
118
118
  - lib/active_record/mti/railtie.rb
119
+ - lib/active_record/mti/registry.rb
119
120
  - lib/active_record/mti/schema_dumper.rb
120
121
  - lib/active_record/mti/version.rb
122
+ - lib/core_ext/hash.rb
121
123
  - spec/active_record/calculations_spec.rb
122
- - spec/active_record/mti/inheritence_spec.rb
124
+ - spec/active_record/mti/inheritance_spec.rb
125
+ - spec/active_record/mti/model_schema_spec.rb
126
+ - spec/active_record/mti/schema_dumper_spec.rb
123
127
  - spec/active_record/mti_spec.rb
128
+ - spec/active_record/sti/inheritance_spec.rb
124
129
  - spec/schema.rb
125
130
  - spec/spec_helper.rb
126
131
  - spec/support/models.rb
@@ -146,14 +151,17 @@ required_rubygems_version: !ruby/object:Gem::Requirement
146
151
  version: 1.3.1
147
152
  requirements: []
148
153
  rubyforge_project:
149
- rubygems_version: 2.6.11
154
+ rubygems_version: 2.6.13
150
155
  signing_key:
151
156
  specification_version: 4
152
157
  summary: Multi Table Inheritance for PostgreSQL in Rails
153
158
  test_files:
154
159
  - spec/active_record/calculations_spec.rb
155
- - spec/active_record/mti/inheritence_spec.rb
160
+ - spec/active_record/mti/inheritance_spec.rb
161
+ - spec/active_record/mti/model_schema_spec.rb
162
+ - spec/active_record/mti/schema_dumper_spec.rb
156
163
  - spec/active_record/mti_spec.rb
164
+ - spec/active_record/sti/inheritance_spec.rb
157
165
  - spec/schema.rb
158
166
  - spec/spec_helper.rb
159
167
  - spec/support/models.rb