empty_eye 0.4.2 → 0.4.3

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