empty_eye 0.4.2 → 0.4.3

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG.md CHANGED
@@ -1,4 +1,9 @@
1
- # 0.4.1 / 2012-03-11 / Grady Griffin
1
+ # 0.4.3 / 2012-03-11 / Grady Griffin
2
+
3
+ * major refactor to do less in active record base
4
+ * add view versioning to prevent creating views when not necessary
5
+
6
+ # 0.4.2 / 2012-03-11 / Grady Griffin
2
7
 
3
8
  * reorganized some methods
4
9
  * added logic to free up complex data structures once they are not needed
@@ -5,122 +5,78 @@ module ActiveRecord
5
5
 
6
6
  #am i a mti class? easier than making a new class type ... i tried
7
7
  def mti_class?
8
- extended_with.any?
8
+ !!@shard_wrangler
9
9
  end
10
-
10
+
11
11
  #interface for building mti_class
12
12
  #primary table is not necessary if the table named correctly (Bar => bars_core)
13
13
  #OR if the class inherits a primary table
14
14
  #simply wrap your greasy associations in this block
15
15
  def mti_class(primary_table = nil)
16
- self.primary_key = "id"
17
16
  raise(EmptyEye::AlreadyExtended, "MTI class method already invoked") if mti_class?
18
- set_mti_primary_table(primary_table)
19
- self.table_name = compute_view_name
20
- extended_with.primary_table(mti_primary_table)
17
+ self.primary_key = "id"
18
+ @shard_wrangler = EmptyEye::ShardWrangler.create(self, primary_table)
19
+ self.table_name = @shard_wrangler.compute_view_name
21
20
  before_yield = reflect_on_multiple_associations(:has_one)
22
21
  yield nil if block_given?
23
- mti_associations = reflect_on_multiple_associations(:has_one) - before_yield
24
- extend_mti_class(mti_associations)
22
+ mti_ancestors = reflect_on_multiple_associations(:has_one) - before_yield
23
+ @shard_wrangler.wrangle_shards(mti_ancestors)
25
24
  true
26
25
  end
27
-
28
- #all data for mti class is stored here
29
- #when empty it is not so MT-I
30
- def extended_with
31
- @extended_with ||= EmptyEye::ViewExtensionCollection.new(self)
26
+
27
+ #we need this when we add new associaton types to extend with
28
+ #we could use the baked in version for now
29
+ def reflect_on_multiple_associations(*assoc_types)
30
+ assoc_types.collect do |assoc_type|
31
+ reflect_on_all_associations(assoc_type)
32
+ end.flatten.uniq
32
33
  end
33
34
 
34
- #the class of primary shard
35
- def mti_primary_shard
36
- extended_with.primary.shard
37
- end
38
-
39
35
  #we dont need no freakin' type condition
40
36
  #the view handles this
41
37
  def finder_needs_type_condition?
42
38
  !mti_class? and super
43
39
  end
44
-
45
- private
46
-
47
- #we know the associations and we know what they can do
48
- #we will make a mti class accordingly here
49
- def extend_mti_class(mti_associations)
50
- mti_associations.each do |assoc|
51
- extended_with.association(assoc)
52
- end
53
- create_view
54
- reset_column_information
55
- inherit_mti_validations
56
- end
57
-
58
- #we need a name for the view
59
- #need to have a way to set this
60
- def compute_view_name
61
- descends_from_active_record? ? compute_table_name : name.underscore.pluralize
62
- end
63
-
64
- #determine the primary table
65
- #first determine if our view name exists; this will need to change one day
66
- #if they didnt specify try using the core convention else the superclass
67
- #if they specified use what they set
68
- def set_mti_primary_table(primary_table_name)
69
- @mti_primary_table = if ordinary_table_exists?
70
- raise(EmptyEye::ViewNameError, "MTI view cannot be created because a table named '#{compute_view_name}' already exists")
71
- elsif primary_table_name.nil?
72
- descends_from_active_record? ? "#{compute_table_name}_core" : superclass.table_name
73
- else
74
- primary_table_name
75
- end
76
- end
77
-
78
- def mti_primary_table
79
- @mti_primary_table
80
- end
81
-
82
- #we need this when we add new associaton types to extend with
83
- #we could use the baked in version for now
84
- def reflect_on_multiple_associations(*assoc_types)
85
- assoc_types.collect do |assoc_type|
86
- reflect_on_all_associations(assoc_type)
87
- end.flatten.uniq
40
+
41
+ #remove the schema_version for mti_class views
42
+ def column_names
43
+ @column_names ||= columns.map { |column| column.name } - (mti_class? ? ["schema_version"] : [])
88
44
  end
89
45
 
90
- #determine if what we want to name our view already exists
91
- def ordinary_table_exists?
92
- connection.tables_without_views.include?(compute_view_name)
46
+ #the class of primary shard
47
+ def shard_wrangler
48
+ @shard_wrangler
93
49
  end
94
50
 
95
- #drop the view; dont check if we can, just rescue any errors
96
- #create the view
97
- def create_view
98
- connection.execute("DROP VIEW #{table_name}") rescue nil
99
- connection.execute(extended_with.create_view_sql)
51
+ def descends_from_active_record?
52
+ if superclass.abstract_class?
53
+ superclass.descends_from_active_record?
54
+ elsif mti_class?
55
+ superclass == Base
56
+ else
57
+ superclass == Base || !columns_hash.include?(inheritance_column)
58
+ end
100
59
  end
101
60
 
102
- #we may need to inherit these... not using for now
103
- def superclass_extensions
104
- superclass.extended_with.dup.descend(self) unless descends_from_active_record?
105
- end
61
+ private
106
62
 
107
- #we know how to rebuild the validations from the shards
108
- #lets call our inherited validations here
109
- def inherit_mti_validations
110
- extended_with.validations.each {|args| send(*args)}
111
- #no need to keep these in memory
112
- extended_with.free_validations
113
- end
63
+ end
64
+
65
+ def valid?(context = nil)
66
+ context ||= (new_record? ? :create : :update)
67
+ output = super(context)
68
+ return errors.empty? && output unless mti_class?
69
+ shard_wrangler.valid?(context) && errors.empty? && output
114
70
  end
115
71
 
116
72
  private
117
73
 
118
74
  #a pseudo association method mapping us back to instances primary shard
119
- def mti_primary_shard
120
- @mti_primary_shard ||= if new_record?
121
- self.class.mti_primary_shard.new(:mti_instance => self)
75
+ def shard_wrangler
76
+ @shard_wrangler ||= if new_record?
77
+ self.class.shard_wrangler.new(:mti_instance => self)
122
78
  else
123
- rtn = self.class.mti_primary_shard.find_by_id(id)
79
+ rtn = self.class.shard_wrangler.find_by_id(id)
124
80
  rtn.mti_instance = self
125
81
  rtn
126
82
  end
@@ -45,7 +45,7 @@ module EmptyEye
45
45
  scope = scope.where(table[key].eq(owner[foreign_key]))
46
46
 
47
47
  if reflection.type
48
- scope = scope.where(table[reflection.type].eq(owner.mti_master_class.base_class.name))
48
+ scope = scope.where(table[reflection.type].eq(owner.master_class.base_class.name))
49
49
  end
50
50
 
51
51
  conditions.each do |condition|
@@ -22,7 +22,7 @@ module EmptyEye
22
22
  attributes[reflection.foreign_key] = owner[reflection.active_record_primary_key]
23
23
 
24
24
  if reflection.options[:as]
25
- attributes[reflection.type] = owner.mti_master_class.base_class.name
25
+ attributes[reflection.type] = owner.master_class.base_class.name
26
26
  end
27
27
  end
28
28
 
@@ -6,7 +6,7 @@ module EmptyEye
6
6
  #else let the primary shard do the saving
7
7
  def update(attribute_names = @attributes.keys)
8
8
  return super unless mti_class?
9
- mti_primary_shard.cascade_save
9
+ shard_wrangler.cascade_save
10
10
  1
11
11
  end
12
12
 
@@ -15,7 +15,7 @@ module EmptyEye
15
15
  #come back and cleanup
16
16
  def create
17
17
  return super unless mti_class?
18
- mti_primary_shard.cascade_save
18
+ shard_wrangler.cascade_save
19
19
  ActiveRecord::IdentityMap.add(self) if ActiveRecord::IdentityMap.enabled?
20
20
  @new_record = false
21
21
  self.id
@@ -26,7 +26,7 @@ module EmptyEye
26
26
  #come back and cleanup
27
27
  def destroy
28
28
  return super unless mti_class?
29
- mti_primary_shard.destroy
29
+ shard_wrangler.destroy
30
30
  if ActiveRecord::IdentityMap.enabled? and persisted?
31
31
  ActiveRecord::IdentityMap.remove(self)
32
32
  end
@@ -39,7 +39,7 @@ module EmptyEye
39
39
  #come back and cleanup
40
40
  def delete
41
41
  return super unless mti_class?
42
- self.class.delete_all(:id => id)
42
+ shard_wrangler.class.cascade_delete_all(:id => id)
43
43
  if ActiveRecord::IdentityMap.enabled? and persisted?
44
44
  ActiveRecord::IdentityMap.remove(self)
45
45
  end
@@ -1,14 +1,14 @@
1
1
  module EmptyEye
2
- class PrimaryViewExtension
2
+ class PrimaryShard
3
3
 
4
- #primary extension for parent class
4
+ #primary shard for master_class class
5
5
  #manages associations for database updates
6
- #has many of the same interfaces as view extensions
6
+ #has many of the same interfaces as view shards
7
7
 
8
- def initialize(table_name, parent)
9
- @table = table_name
10
- @parent = parent
11
- create_shard
8
+ def initialize(wrangler)
9
+ @table = wrangler.table_name
10
+ @master_class = wrangler.master_class
11
+ @klass = wrangler
12
12
  end
13
13
 
14
14
  def self.connection
@@ -17,12 +17,12 @@ module EmptyEye
17
17
 
18
18
  #never include the type field as it shouldnt be needed and cant be updated anyway
19
19
  def self.exclude_always
20
- ['type']
20
+ ['type', 'mti_schema_version']
21
21
  end
22
22
 
23
- #class to which this extension belongs
24
- def parent
25
- @parent
23
+ #class to which this shard belongs
24
+ def master_class
25
+ @master_class
26
26
  end
27
27
 
28
28
  #to let the outside word know it is primary
@@ -30,9 +30,9 @@ module EmptyEye
30
30
  true
31
31
  end
32
32
 
33
- #class that will mimic the associations of the parent for updating db
34
- def shard
35
- @shard
33
+ #class that will mimic the associations of the master_class for updating db
34
+ def klass
35
+ @klass
36
36
  end
37
37
 
38
38
  # the tablename
@@ -50,7 +50,7 @@ module EmptyEye
50
50
  @arel_table ||= Arel::Table.new(table)
51
51
  end
52
52
 
53
- #this may change but for now the key is the primary id of the parent and shard
53
+ #this may change but for now the key is the primary id of the master_class and shard
54
54
  def key
55
55
  arel_table[:id]
56
56
  end
@@ -60,19 +60,19 @@ module EmptyEye
60
60
  end
61
61
 
62
62
  def sti_also?
63
- !parent.descends_from_active_record?
63
+ !master_class.descends_from_active_record?
64
64
  end
65
65
 
66
66
  #arel column of type field
67
67
  def type_column
68
68
  if sti_also?
69
- arel_table[parent.inheritance_column.to_sym]
69
+ arel_table[master_class.inheritance_column.to_sym]
70
70
  end
71
71
  end
72
72
 
73
73
  #value of the polymorphic column
74
74
  def type_value
75
- parent.name if type_column
75
+ master_class.name if type_column
76
76
  end
77
77
 
78
78
  #always null for primary
@@ -82,7 +82,8 @@ module EmptyEye
82
82
 
83
83
  #table columns
84
84
  def table_columns
85
- self.class.connection.columns(table).collect(&:name)
85
+ klass.column_names
86
+ #self.class.connection.columns(table).collect(&:name)
86
87
  end
87
88
 
88
89
  def exclude
@@ -94,22 +95,22 @@ module EmptyEye
94
95
  table_columns - exclude
95
96
  end
96
97
 
97
- #create associations for shard class to mimic parent
98
- def have_one(ext)
98
+ #create associations for shard class to mimic master_class
99
+ def has_another(shard)
99
100
  #this is myself; dont associate
100
- return if ext.primary
101
- mimic = ext.association
102
- return if shard.reflect_on_association(mimic.name)
101
+ return if shard.primary
102
+ mimic = shard.association
103
+ return if klass.reflect_on_association(mimic.name)
103
104
  options = mimic.options.dup
104
105
  options.merge!(default_has_one_options)
105
- options.merge!(:foreign_key => ext.foreign_key)
106
- shard.send(mimic.macro, mimic.name, options)
106
+ options.merge!(:foreign_key => shard.foreign_key)
107
+ klass.send(mimic.macro, mimic.name, options)
107
108
  end
108
109
 
109
110
  #delegate setters to appropriate associations
110
- def delegate_to(col, ext)
111
- return if ext.primary
112
- shard.send(:delegate, "#{col}=", {:to => ext.name})
111
+ def delegate_to(col, shard)
112
+ return if shard.primary
113
+ klass.send(:delegate, "#{col}=", {:to => shard.name})
113
114
  end
114
115
 
115
116
  private
@@ -118,20 +119,6 @@ module EmptyEye
118
119
  def default_has_one_options
119
120
  {:autosave => true, :validate => true, :dependent => :destroy}
120
121
  end
121
-
122
- #if possible shard inherits from the superclass
123
- def shard_inherit_from
124
- parent.base_class == parent ? ActiveRecord::Base : parent.send(:superclass)
125
- end
126
-
127
- #create a class to manage the parents associations
128
- def create_shard
129
- new_class = Class.new(shard_inherit_from)
130
- new_class.send(:include, Shard)
131
- new_class.table_name = table
132
- new_class.mti_master_class = parent
133
- @shard = EmptyEye.const_set("#{parent.to_s}Shard", new_class)
134
- end
135
122
 
136
123
  end
137
124
  end
@@ -9,77 +9,14 @@ module EmptyEye
9
9
 
10
10
  def delete_all(conditions = nil)
11
11
  return super unless mti_class?
12
- mti_clear_identity_map
13
- affected = 0
14
- #if something goes wrong forget it all
15
- transaction do
16
- if conditions
17
- #batch up ids
18
- ids = select("`#{table_name}`.`#{primary_key}`").where(conditions).collect(&:id)
19
- #delete all the shards of the mti class matching ids
20
- mti_batch_perform(ids) do |ext, batch|
21
- if ext.polymorphic_type
22
- ext.shard.delete_all(ext.foreign_key => batch, ext.polymorphic_type => ext.type_value)
23
- else
24
- ext.shard.delete_all(ext.foreign_key => batch)
25
- end
26
- end
27
- else
28
- #way simpler if there are no conditions; kill everyone
29
- extended_with.each do |ext|
30
- affected = [affected, ext.shard.delete_all].max
31
- end
32
- end
33
- end
34
- affected
12
+ shard_wrangler.cascade_delete_all(conditions)
35
13
  end
36
14
 
37
15
  def update_all(updates, conditions = nil, options = {})
38
16
  return super unless mti_class?
39
17
  raise(EmptyEye::InvalidUpdate, "update values for a MTI class must be a hash") unless updates.is_a?(Hash)
40
- mti_clear_identity_map
41
- stringified_updates = updates.stringify_keys
42
- affected = 0
43
- transaction do
44
- if conditions
45
- #batch up ids
46
- ids = select(arel_table[primary_key.to_sym]).where(conditions).apply_finder_options(options.slice(:limit, :order)).collect(&:id)
47
- #update all the shards of the mti class matching ids
48
- affected = mti_batch_perform(ids) do |ext, batch|
49
- #delegate map ingests the update hash and regurgitates a smaller hash of the values the shard can handle
50
- cols = extended_with.delegate_map(ext.name, stringified_updates)
51
- cols.empty? ? 0 : ext.shard.update_all(cols, ext.foreign_key => batch)
52
- end
53
- else
54
- #way simpler if there are no conditions; change the world!
55
- extended_with.each do |ext|
56
- cols = extended_with.delegate_map(ext.name, stringified_updates)
57
- affected = [(cols.empty? ? 0 : ext.shard.update_all(cols)), affected].max
58
- end
59
- end
60
- end
61
- affected
18
+ shard_wrangler.cascade_update_all(updates, conditions, options)
62
19
  end
63
-
64
- private
65
-
66
- def mti_clear_identity_map
67
- ActiveRecord::IdentityMap.repository[symbolized_base_class].clear if ActiveRecord::IdentityMap.enabled?
68
- end
69
-
70
- #lets do 10000 at a time
71
- def mti_batch_perform(ids)
72
- affected = 0
73
- until ids.to_a.empty?
74
- current_ids = ids.pop(10000)
75
- extended_with.each do |ext|
76
- rtn = yield(ext, current_ids)
77
- affected = [affected,rtn].max
78
- end
79
- end
80
- affected
81
- end
82
-
83
20
  end
84
21
  end
85
22
  end
@@ -1,133 +1,108 @@
1
1
  module EmptyEye
2
- module Shard
2
+ class Shard
3
3
 
4
- #module which extends the class that serves as a pointer to the primary table
5
- #when there is a superclass the shard will inherit from that, else it will inherit from ActiveRecord
6
- #the primary shard manages all the MTI associated tables for the master class
7
-
8
- def self.included(base)
9
- base.extend ClassMethods
4
+ #shard for master_class class
5
+ #tracks associations for database updates managed by primary shard
6
+ #has many of the same interfaces as primary view shard
7
+
8
+ def initialize(association)
9
+ @association = association
10
10
  end
11
11
 
12
- #the instance that owns this primary shard
13
- #we usually know the master instance ahead of time
14
- #so we should take care to set this manually
15
- #we want to avoid the lookup
16
- def mti_instance
17
- @mti_instance || mti_master_class.find_by_id(id)
12
+ #exclude from view generation always
13
+ def self.exclude_always
14
+ ['id','created_at','updated_at','deleted_at', 'type', 'mti_schema_version']
18
15
  end
19
16
 
20
- #setter used to associate the primary shard with the master instance
21
- def mti_instance=(instance)
22
- @mti_instance = instance
17
+ #association that this shard will build upon
18
+ def association
19
+ @association
23
20
  end
24
21
 
25
- #special save so that the primary shard can keep the master instances tables consistent
26
- def cascade_save
27
- #make sure all the shards are there
28
- cascade_build_associations
29
- #this will propagate setters to the appropriate shards
30
- assign_attributes(mti_safe_attributes)
31
- self.type = mti_master_class.name if respond_to?("type=")
32
- #this will autosave shards
33
- save
34
- #reset the id and then reload
35
- mti_instance.id = id
36
- mti_instance.reload
22
+ #the table columns that will be extended in sql
23
+ def columns
24
+ restrictions - exclude
25
+ end
26
+
27
+ #never the primary
28
+ def primary
29
+ false
37
30
  end
38
31
 
39
- #reflection on master class; this should never change
40
- def mti_master_class
41
- self.class.mti_master_class
32
+ #table of the shard
33
+ def table
34
+ association.table_name
42
35
  end
43
-
44
- private
45
-
46
- #make sure the primary shard only tries to update what he should
47
- def mti_safe_attributes
48
- mti_instance.attributes.except(
49
- *self.mti_master_class.extended_with.primary.exclude
50
- )
51
- end
52
-
53
- #all the instance shards should exist but lets be certain
54
- #using an autobuild would be more efficient here
55
- #we shouldnt load associations we dont need to
56
- def cascade_build_associations
57
- #go through each extension making sure it is exists and is loaded
58
- mti_instance.class.extended_with.each do |ext|
59
- next if ext.primary
60
- assoc = send(ext.name)
61
- assoc ||= send("build_#{ext.name}")
62
- send("#{ext.name}=", assoc)
63
- end
36
+
37
+ #name of the association
38
+ def name
39
+ association.name
64
40
  end
65
41
 
66
- module ClassMethods
67
-
68
- #the shard uses special reflection; overriden here
69
- def create_reflection(macro, name, options, active_record)
70
- raise(EmptyEye::NotYetSupported, "through associations are not yet spported") if options[:through]
71
- klass = options[:through] ? ShardThroughReflection : ShardAssociationReflection
72
- reflection = klass.new(macro, name, options, active_record)
73
-
74
- self.reflections = self.reflections.merge(name => reflection)
75
- reflection
42
+ #used to create view
43
+ def arel_table
44
+ @arel_table ||= begin
45
+ t= Arel::Table.new(table)
46
+ t.table_alias = alias_name if alias_name != table
47
+ t
76
48
  end
49
+ end
77
50
 
78
- #finder methods should use the master class's type not the shard's
79
- def type_condition(table = arel_table)
80
- sti_column = table[inheritance_column.to_sym]
81
-
82
- sti_column.eq(mti_master_class.name)
83
- end
84
-
85
- #overriding find_by_id
86
- #this is used to retrieve the shard instance for the master instance
87
- #the type column is removed
88
- def find_by_id(val)
89
- query = columns_except_type
90
- query = query.where(arel_table[:id].eq(val))
91
- find_by_sql(query.to_sql).first
92
- end
93
-
94
- #the shard uses a special association builder
95
- def has_one(name, options = {})
96
- Associations::Builder::ShardHasOne.build(self, name, options)
97
- end
51
+ #foreign key of the shard; used in view generation and database updates
52
+ def foreign_key
53
+ association.foreign_key
54
+ end
98
55
 
99
- #reflection on master class; this should never change
100
- def mti_master_class
101
- @mti_master_class
102
- end
56
+ def klass
57
+ association.klass
58
+ end
59
+
60
+ #arel column of polymorphic type field
61
+ def type_column
62
+ arel_table[polymorphic_type.to_sym] if polymorphic_type
63
+ end
64
+
65
+ #value of the polymorphic column
66
+ def type_value
67
+ master_class.base_class.name if polymorphic_type
68
+ end
103
69
 
104
- #the mti_master_class value is set with this setter; should happen only once
105
- def mti_master_class=(klass)
106
- @mti_master_class = klass
107
- end
108
-
109
- #overriding to reset the special instance variable
110
- def reset_column_information
111
- @columns_except_type = nil
112
- super
113
- end
114
-
115
- private
116
-
117
- #build the arel query once and memoize it
118
- #this is essentially the select to remove type column
119
- def columns_except_type
120
- @columns_except_type ||= begin
121
- query = arel_table
122
- (column_names - [inheritance_column]).each do |c|
123
- query = query.project(arel_table[c.to_sym])
124
- end
125
- query
126
- end
127
- @columns_except_type.dup
128
- end
70
+ def polymorphic_type
71
+ return unless association.options[:as]
72
+ "#{association.options[:as]}_type"
73
+ end
74
+
75
+ private
129
76
 
77
+ #class to whom this shard belongs
78
+ def master_class
79
+ association.active_record
80
+ end
81
+
82
+ #uses association name to create alias to prevent non unique aliases
83
+ def alias_name
84
+ name.to_s.pluralize
85
+ end
86
+
87
+ #user declared exceptions ... exclude these columns from the master_class inheritance
88
+ def exceptions
89
+ association.options[:except].to_a.collect(&:to_s)
90
+ end
91
+
92
+ #user declared restrictions ... restrict master_class inheritance columns to these
93
+ def restrictions
94
+ only = association.options[:only].to_a.collect(&:to_s)
95
+ only.empty? ? table_columns : only
96
+ end
97
+
98
+ #we want to omit these columns
99
+ def exclude
100
+ [exceptions, self.class.exclude_always, foreign_key, polymorphic_type].flatten.uniq
101
+ end
130
102
 
103
+ #all the columns of the shards table
104
+ def table_columns
105
+ klass.column_names
131
106
  end
132
107
  end
133
108
  end