empty_eye 0.4.0

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.
@@ -0,0 +1,137 @@
1
+ module EmptyEye
2
+ class PrimaryViewExtension
3
+
4
+ #primary extension for parent class
5
+ #manages associations for database updates
6
+ #has many of the same interfaces as view extensions
7
+
8
+ def initialize(table_name, parent)
9
+ @table = table_name
10
+ @parent = parent
11
+ create_shard
12
+ end
13
+
14
+ def self.connection
15
+ ActiveRecord::Base.connection
16
+ end
17
+
18
+ #never include the type field as it shouldnt be needed and cant be updated anyway
19
+ def self.exclude_always
20
+ ['type']
21
+ end
22
+
23
+ #class to which this extension belongs
24
+ def parent
25
+ @parent
26
+ end
27
+
28
+ #to let the outside word know it is primary
29
+ def primary
30
+ true
31
+ end
32
+
33
+ #class that will mimic the associations of the parent for updating db
34
+ def shard
35
+ @shard
36
+ end
37
+
38
+ # the tablename
39
+ def table
40
+ @table
41
+ end
42
+
43
+ # the alias for the table; for primary we just use the table name
44
+ def name
45
+ @table
46
+ end
47
+
48
+ # arel table for generating the view
49
+ def arel_table
50
+ @arel_table ||= Arel::Table.new(table)
51
+ end
52
+
53
+ #this may change but for now the key is the primary id of the parent and shard
54
+ def key
55
+ arel_table[:id]
56
+ end
57
+
58
+ def foreign_key
59
+ "id"
60
+ end
61
+
62
+ def sti_also?
63
+ !parent.descends_from_active_record?
64
+ end
65
+
66
+ #arel column of type field
67
+ def type_column
68
+ if sti_also?
69
+ arel_table[parent.inheritance_column.to_sym]
70
+ end
71
+ end
72
+
73
+ #value of the polymorphic column
74
+ def type_value
75
+ parent.name if type_column
76
+ end
77
+
78
+ #always null for primary
79
+ def polymorphic_type
80
+ nil
81
+ end
82
+
83
+ #table columns
84
+ def table_columns
85
+ self.class.connection.columns(table).collect(&:name)
86
+ end
87
+
88
+ def exclude
89
+ self.class.exclude_always
90
+ end
91
+
92
+ #the table columns that will be extended in sql
93
+ def columns
94
+ table_columns - exclude
95
+ end
96
+
97
+ #create associations for shard class to mimic parent
98
+ def have_one(ext)
99
+ #this is myself; dont associate
100
+ return if ext.primary
101
+ mimic = ext.association
102
+ return if shard.reflect_on_association(mimic.name)
103
+ options = mimic.options.dup
104
+ options.merge!(default_has_one_options)
105
+ options.merge!(:foreign_key => ext.foreign_key)
106
+ shard.send(mimic.macro, mimic.name, options)
107
+ end
108
+
109
+ #delegate setters to appropriate associations
110
+ def delegate_to(col, ext)
111
+ return if ext.primary
112
+ shard.send(:delegate, "#{col}=", {:to => ext.name})
113
+ end
114
+
115
+ private
116
+
117
+ #MTI wouldnt make any sense if these were not forced in the associations
118
+ def default_has_one_options
119
+ {:autosave => true, :validate => true, :dependent => :destroy}
120
+ 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
+
136
+ end
137
+ end
@@ -0,0 +1,85 @@
1
+ module EmptyEye
2
+ module Relation
3
+
4
+ def self.included(base)
5
+ base.extend ClassMethods
6
+ end
7
+
8
+ module ClassMethods
9
+
10
+ def delete_all(conditions = nil)
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
35
+ end
36
+
37
+ def update_all(updates, conditions = nil, options = {})
38
+ return super unless mti_class?
39
+ 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
62
+ 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
+ end
84
+ end
85
+ end
@@ -0,0 +1,130 @@
1
+ module EmptyEye
2
+ module Shard
3
+
4
+ #a module that extendd the class that serves as a pointer to the primary table
5
+ #when there is a superclass the 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
10
+ end
11
+
12
+ #the instance that owns this primary shard
13
+ def mti_instance
14
+ @mti_instance
15
+ end
16
+
17
+ #setter used to associate the primary shard with the master instance
18
+ def mti_instance=(instance)
19
+ @mti_instance = instance
20
+ end
21
+
22
+ #special save so that the primary shard can keep the master instances tables consistent
23
+ def cascade_save
24
+ #make sure all the shards are there
25
+ cascade_build_associations
26
+ #this will propagate setters to the appropriate shards
27
+ assign_attributes(mti_safe_attributes)
28
+ self.type = mti_master_class.name if respond_to?("type=")
29
+ #this will autosave shards
30
+ save
31
+ #reset the id and then reload
32
+ mti_instance.id = id
33
+ mti_instance.reload
34
+ end
35
+
36
+ #reflection on master class; this should never change
37
+ def mti_master_class
38
+ self.class.mti_master_class
39
+ end
40
+
41
+ private
42
+
43
+ #make sure the primary shard only tries to update what he should
44
+ def mti_safe_attributes
45
+ mti_instance.attributes.except(
46
+ *self.mti_master_class.extended_with.primary.exclude
47
+ )
48
+ end
49
+
50
+ #all the instance shards should exist but lets be certain
51
+ #using an autobuild would be more efficient here
52
+ #we shouldnt load associations we dont need to
53
+ def cascade_build_associations
54
+ #go through each extension making sure it is exists and is loaded
55
+ mti_instance.class.extended_with.each do |ext|
56
+ next if ext.primary
57
+ assoc = send(ext.name)
58
+ assoc ||= send("build_#{ext.name}")
59
+ send("#{ext.name}=", assoc)
60
+ end
61
+ end
62
+
63
+ module ClassMethods
64
+
65
+ #the shard uses special reflection; overriden here
66
+ def create_reflection(macro, name, options, active_record)
67
+ raise(EmptyEye::NotYetSupported, "through associations are not yet spported") if options[:through]
68
+ klass = options[:through] ? ShardThroughReflection : ShardAssociationReflection
69
+ reflection = klass.new(macro, name, options, active_record)
70
+
71
+ self.reflections = self.reflections.merge(name => reflection)
72
+ reflection
73
+ end
74
+
75
+ #finder methods should use the master classes base class not the shards
76
+ def type_condition(table = arel_table)
77
+ sti_column = table[inheritance_column.to_sym]
78
+
79
+ sti_column.eq(mti_master_class.name)
80
+ end
81
+
82
+ #overriding find_by_id
83
+ #this is used to retrieve the shard instance for the master instance
84
+ #the type column is removed
85
+ def find_by_id(val)
86
+ query = columns_except_type
87
+ query = query.where(arel_table[:id].eq(val))
88
+ find_by_sql(query.to_sql).first
89
+ end
90
+
91
+ #the shard uses a special association builder
92
+ def has_one(name, options = {})
93
+ Associations::Builder::ShardHasOne.build(self, name, options)
94
+ end
95
+
96
+ #reflection on master class; this should never change
97
+ def mti_master_class
98
+ @mti_master_class
99
+ end
100
+
101
+ #the mti_master_class value is set with this setter; should happen only once
102
+ def mti_master_class=(klass)
103
+ @mti_master_class = klass
104
+ end
105
+
106
+ #overriding to reset the special instance variable
107
+ def reset_column_information
108
+ @columns_except_type = nil
109
+ super
110
+ end
111
+
112
+ private
113
+
114
+ #build the arel query once and memoize it
115
+ #this is essentially the select to remove type column
116
+ def columns_except_type
117
+ @columns_except_type ||= begin
118
+ query = arel_table
119
+ (column_names - [inheritance_column]).each do |c|
120
+ query = query.project(arel_table[c.to_sym])
121
+ end
122
+ query
123
+ end
124
+ @columns_except_type.dup
125
+ end
126
+
127
+
128
+ end
129
+ end
130
+ end
@@ -0,0 +1,28 @@
1
+ module EmptyEye
2
+ class ShardAssociationReflection < ActiveRecord::Reflection::AssociationReflection
3
+ #special reflection for shard
4
+ #very verbose but will be easier to update later
5
+ #better than monkey patching
6
+
7
+ def association_class
8
+ EmptyEye::Associations::ShardHasOneAssociation
9
+ #later we will support all singular associations; for now only has one
10
+
11
+ # case macro
12
+ # when :belongs_to
13
+ # if options[:polymorphic]
14
+ # EmptyEye::Associations::ShardBelongsToPolymorphicAssociation
15
+ # else
16
+ # EmptyEye::Associations::ShardBelongsToAssociation
17
+ # end
18
+ # when :has_one
19
+ # if options[:through]
20
+ # EmptyEye::Associations::ShardHasOneThroughAssociation
21
+ # else
22
+ # EmptyEye::Associations::ShardHasOneAssociation
23
+ # end
24
+ # end
25
+ end
26
+
27
+ end
28
+ end
@@ -0,0 +1,9 @@
1
+ module EmptyEye
2
+ module VERSION
3
+ MAJOR = 0
4
+ MINOR = 4
5
+ TINY = 0
6
+
7
+ STRING = [MAJOR, MINOR, TINY].join('.')
8
+ end
9
+ end
@@ -0,0 +1,115 @@
1
+ module EmptyEye
2
+ class ViewExtension
3
+
4
+ #extension for parent class
5
+ #tracks associations for database updates managed by primary extension
6
+ #has many of the same interfaces as primary view extension
7
+
8
+ def initialize(association)
9
+ @association = association
10
+ end
11
+
12
+ #exclude from view generation always
13
+ def self.exclude_always
14
+ ['id','created_at','updated_at','deleted_at', 'type']
15
+ end
16
+
17
+ #association that this extension will build upon
18
+ def association
19
+ @association
20
+ end
21
+
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
30
+ end
31
+
32
+ #table of the shard
33
+ def table
34
+ association.table_name
35
+ end
36
+
37
+ #name of the association
38
+ def name
39
+ association.name
40
+ end
41
+
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
48
+ end
49
+ end
50
+
51
+ #foreign key of the shard; used in view generation and database updates
52
+ def foreign_key
53
+ association.foreign_key
54
+ end
55
+
56
+ #the shard is simply the class of the association
57
+ def shard
58
+ association.klass
59
+ end
60
+
61
+ #arel column of polymorphic type field
62
+ def type_column
63
+ arel_table[polymorphic_type.to_sym] if polymorphic_type
64
+ end
65
+
66
+ #value of the polymorphic column
67
+ def type_value
68
+ parent.base_class.name if polymorphic_type
69
+ end
70
+
71
+ #value computed to remove this from column map; no need for the view to have it
72
+ def polymorphic_type
73
+ return unless association.options[:as]
74
+ "#{association.options[:as]}_type"
75
+ end
76
+
77
+ private
78
+
79
+ #class to whom this extension belongs
80
+ def parent
81
+ association.active_record
82
+ end
83
+
84
+ #class of the extension table
85
+ def klass
86
+ association.klass
87
+ end
88
+
89
+ #uses association name to create alias to prevent non unique aliases
90
+ def alias_name
91
+ name.to_s.pluralize
92
+ end
93
+
94
+ #user declared exceptions ... exclude these columns from the parent inheritance
95
+ def exceptions
96
+ association.options[:except].to_a.collect(&:to_s)
97
+ end
98
+
99
+ #user declared restrictions ... restrict parent inheritance columns to these
100
+ def restrictions
101
+ only = association.options[:only].to_a.collect(&:to_s)
102
+ only.empty? ? table_columns : only
103
+ end
104
+
105
+ #we want to omit these columns
106
+ def exclude
107
+ [exceptions, self.class.exclude_always, foreign_key, polymorphic_type].flatten.uniq
108
+ end
109
+
110
+ #all the columns of the extensions table
111
+ def table_columns
112
+ klass.column_names
113
+ end
114
+ end
115
+ end