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.
@@ -0,0 +1,192 @@
1
+ module EmptyEye
2
+ class ShardCollection
3
+
4
+ #a collection of all the view_shards
5
+ #these are wranglers for the shards
6
+ #uses 'array' as a proxy
7
+ #performs array methods by passing things off in method missing
8
+
9
+ def initialize(primary_shard_klass)
10
+ @master_class = primary_shard_klass.master_class
11
+ @primary = PrimaryShard.new(primary_shard_klass)
12
+ @array = [@primary]
13
+ end
14
+
15
+ #the proxy object for instances
16
+ def array
17
+ @array
18
+ end
19
+
20
+ #we want to see the proxy object not the class info
21
+ def inspect
22
+ array.inspect
23
+ end
24
+
25
+ #the class to which the shards belongs
26
+ def master_class
27
+ @master_class
28
+ end
29
+
30
+ def descend(klass)
31
+ @master_class = klass
32
+ self
33
+ end
34
+
35
+ #add shard based on association from master_class
36
+ def create_with(assoc)
37
+ new_shard = Shard.new(assoc)
38
+ reject! {|shard| shard.name == new_shard.name}
39
+ push(new_shard)
40
+ new_shard
41
+ end
42
+
43
+ def schema_version
44
+ @schema_version
45
+ end
46
+
47
+ #takes the name of shard and a hash of intended updates from master instance
48
+ #returns a subset of hash with only values the shard handles
49
+ def delegate_map(name, hash)
50
+ keys = update_mapping[name] & hash.keys
51
+ keys.inject({}) do |res, col|
52
+ res[col] = hash[col] if hash[col]
53
+ res
54
+ end
55
+ end
56
+
57
+ #the primary shard
58
+ def primary
59
+ @primary
60
+ end
61
+
62
+ #array of shard classes
63
+ def klasses
64
+ map(&:klass)
65
+ end
66
+
67
+ def names
68
+ map(&:name)
69
+ end
70
+
71
+ #this object responds to array methods
72
+ def respond_to?(m)
73
+ super || array.respond_to?(m)
74
+ end
75
+
76
+ #delegate to the array proxy when the method is missing
77
+ def method_missing(m, *args, &block)
78
+ if respond_to?(m)
79
+ array.send(m, *args, &block)
80
+ else
81
+ super
82
+ end
83
+ end
84
+
85
+ def view_sql
86
+ @view_sql
87
+ end
88
+
89
+ #generates view sql
90
+ def create_view_sql
91
+ #determine what shard will handle what columns
92
+ map_attribute_management
93
+ #start with primary table
94
+ query = primary_arel_table
95
+
96
+ #build select clause with correct table handling the appropriate columns
97
+ query = query.project(*arel_columns)
98
+
99
+ #build joins
100
+ each do |shard|
101
+ next if shard.primary
102
+ current = shard.arel_table
103
+ key = shard.foreign_key.to_sym
104
+ if shard.type_column
105
+ query.join(current).on(
106
+ primary.key.eq(current[key]), shard.type_column.eq(shard.type_value)
107
+ )
108
+ else
109
+ query.join(current).on(
110
+ primary.key.eq(current[key])
111
+ )
112
+ end
113
+ end
114
+
115
+ self.schema_version = Digest::MD5.hexdigest(query.to_sql)
116
+ query.project("'#{schema_version}' AS mti_schema_version")
117
+
118
+ #we dont need to keep this data
119
+ free_arel_columns
120
+
121
+ #STI condition if needed
122
+ if primary.sti_also?
123
+ query.where(primary.type_column.eq(primary.type_value))
124
+ end
125
+
126
+ #build view creation statement
127
+ @view_sql = "CREATE VIEW #{primary.klass.compute_view_name} AS\n#{query.to_sql}"
128
+ end
129
+
130
+ private
131
+
132
+ def schema_version=(md5_hash)
133
+ @schema_version = md5_hash
134
+ end
135
+
136
+ #all of the arel columns mapped to the right arel tables
137
+ def arel_columns
138
+ @arel_columns ||= []
139
+ end
140
+
141
+ #we dont need to keep this data
142
+ def free_arel_columns
143
+ @arel_columns = nil
144
+ end
145
+
146
+ #tracks the attributes with the shard that will handle it
147
+ def update_mapping
148
+ @update_mapping ||= {}
149
+ end
150
+
151
+ #generate a foreign_key if it is missing
152
+ def default_foreign_key
153
+ view_name = master_class.table_name.singularize
154
+ "#{view_name}_id"
155
+ end
156
+
157
+ #the primary arel table
158
+ def primary_arel_table
159
+ primary.arel_table
160
+ end
161
+
162
+ #all the tables
163
+ def tables
164
+ map(&:table)
165
+ end
166
+
167
+ #map the columns to the shard that will handle it
168
+ def map_attribute_management
169
+ #clear out what we know
170
+ arel_columns.clear
171
+ #use this to track and remove dupes
172
+ tracker = {}
173
+ each do |shard|
174
+ #mimic the master_class's associations through primary shard
175
+ primary.has_another(shard)
176
+ shard.columns.each do |col|
177
+ column = col.to_sym
178
+ #skip if we already have this column
179
+ next if tracker[column]
180
+ #set to true so we wont do again
181
+ tracker[column] = true
182
+ #add the column based on the shard's arel_table
183
+ arel_columns << shard.arel_table[column]
184
+ #later we need to know how to update thing correctly
185
+ update_mapping[shard.name] = update_mapping[shard.name].to_a << col
186
+ #delegate the setter for column to klass of shard through primary shard
187
+ primary.delegate_to(column, shard) unless shard.primary
188
+ end
189
+ end
190
+ end
191
+ end
192
+ end
@@ -0,0 +1,300 @@
1
+ module EmptyEye
2
+ module ShardWrangler
3
+ #module which extends the class that serves as a pointer to the primary table
4
+ #when there is a superclass the shard will inherit from that, else it will inherit from ActiveRecord
5
+ #the primary shard manages all the MTI associated tables for the master class
6
+
7
+ def self.included(base)
8
+ base.extend ClassMethods
9
+ end
10
+
11
+ #this module method creates a ShardWrangler extended ActiveRecord inherited class
12
+ #the class will wrangle our shards
13
+ def self.create(master_class, t_name)
14
+ inherit_from = if master_class.base_class == master_class
15
+ ActiveRecord::Base
16
+ else
17
+ master_class.superclass
18
+ end
19
+
20
+ table_name = if t_name
21
+ t_name
22
+ elsif master_class.descends_from_active_record?
23
+ "#{master_class.name.underscore.pluralize}_core"
24
+ else
25
+ master_class.superclass.table_name
26
+ end
27
+
28
+ new_class = Class.new(inherit_from)
29
+ new_class.send(:include, ShardWrangler)
30
+ new_class.table_name = table_name
31
+ new_class.master_class = master_class
32
+ EmptyEye.const_set("#{master_class.to_s}Wrangler", new_class)
33
+ new_class
34
+ end
35
+
36
+ #the instance that owns this wrangler
37
+ #we usually know the master instance ahead of time
38
+ #so we should take care to set this manually
39
+ #we want to avoid the lookup
40
+ def mti_instance
41
+ @mti_instance || master_class.find_by_id(id)
42
+ end
43
+
44
+ #setter used to associate the wrangler with the master instance
45
+ def mti_instance=(instance)
46
+ @mti_instance = instance
47
+ end
48
+
49
+ #special save so that the wrangler can keep the master's instance tables consistent
50
+ def cascade_save
51
+ write_attributes
52
+ #this will autosave shards
53
+ save
54
+ #reset the id and then reload
55
+ mti_instance.id = id
56
+ mti_instance.reload
57
+ end
58
+
59
+ #reflection on master class; this should never change
60
+ def master_class
61
+ self.class.master_class
62
+ end
63
+
64
+ def valid?(context = nil)
65
+ context ||= (new_record? ? :create : :update)
66
+ write_attributes
67
+ output = super(context)
68
+ errors.each do |attr, message|
69
+ mti_instance.errors.add(attr, message)
70
+ end
71
+ errors.empty? && output
72
+ end
73
+
74
+ private
75
+
76
+ def write_attributes
77
+ #make sure all the shards are there
78
+ cascade_build_associations
79
+ #this will propagate setters to the appropriate shards
80
+ assign_attributes(mti_safe_attributes)
81
+ self.type = master_class.name if respond_to?("type=")
82
+ self
83
+ end
84
+
85
+ def shards
86
+ self.class.shards
87
+ end
88
+
89
+ #make sure the primary shard only tries to update what he should
90
+ def mti_safe_attributes
91
+ mti_instance.attributes.except(
92
+ *self.class.primary_shard.exclude
93
+ )
94
+ end
95
+
96
+ #all the instance shards should exist but lets be certain
97
+ #using an autobuild would be more efficient here
98
+ #we shouldnt load associations we dont need to
99
+ def cascade_build_associations
100
+ #go through each shard making sure it is exists and is loaded
101
+ shards.each do |shard|
102
+ next if shard.primary
103
+ assoc = send(shard.name)
104
+ assoc ||= send("build_#{shard.name}")
105
+ send("#{shard.name}=", assoc)
106
+ end
107
+ end
108
+
109
+ module ClassMethods
110
+
111
+ #the wrangler uses special reflection; overriden here
112
+ def create_reflection(macro, name, options, active_record)
113
+ raise(EmptyEye::NotYetSupported, "through associations are not yet spported") if options[:through]
114
+ klass = options[:through] ? ShardThroughReflection : ShardAssociationReflection
115
+ reflection = klass.new(macro, name, options, active_record)
116
+
117
+ self.reflections = self.reflections.merge(name => reflection)
118
+ reflection
119
+ end
120
+
121
+ #finder methods should use the master class's type not the wrangler's
122
+ def type_condition(table = arel_table)
123
+ sti_column = table[inheritance_column.to_sym]
124
+
125
+ sti_column.eq(master_class.name)
126
+ end
127
+
128
+ #overriding find_by_id
129
+ #this is used to retrieve the wrangler instance for the master instance
130
+ #the type column is removed
131
+ def find_by_id(val)
132
+ query = columns_except_type
133
+ query = query.where(arel_table[:id].eq(val))
134
+ find_by_sql(query.to_sql).first
135
+ end
136
+
137
+ #the wrangler uses a special association builder
138
+ def has_one(name, options = {})
139
+ Associations::Builder::ShardHasOne.build(self, name, options)
140
+ end
141
+
142
+ #reflection on master class; this should never change
143
+ def master_class
144
+ @master_class
145
+ end
146
+
147
+ #the master_class value is set with this setter; should happen only once
148
+ def master_class=(klass)
149
+ @master_class = klass
150
+ end
151
+
152
+ #overriding to reset the special instance variable
153
+ def reset_column_information
154
+ @columns_except_type = nil
155
+ super
156
+ end
157
+
158
+ #the primary shard
159
+ def primary_shard
160
+ shards.primary
161
+ end
162
+
163
+ #we know the associations and we know what they can do
164
+ #we will make a mti class accordingly here
165
+ def wrangle_shards(mti_ancestors)
166
+ mti_ancestors.each do |assoc|
167
+ shards.create_with(assoc)
168
+ end
169
+ create_view if create_view?
170
+ master_class.reset_column_information
171
+ end
172
+
173
+ #batch deletion when there are conditions
174
+ #kill indiscriminately otherwise
175
+ def cascade_delete_all(conditions)
176
+ mti_clear_identity_map
177
+ affected = 0
178
+ ids = []
179
+ ids = conditions ? select(arel_table[primary_key.to_sym]).where(conditions).collect(&:id) : []
180
+ transaction do
181
+ begin
182
+ batch = ids.pop(10000)
183
+ shards.each do |shard|
184
+ result = if conditions.nil?
185
+ shard.klass.delete_all
186
+ elsif shard.polymorphic_type
187
+ shard.klass.delete_all(shard.foreign_key => batch, shard.polymorphic_type => shard.type_value)
188
+ else
189
+ shard.klass.delete_all(shard.foreign_key => batch)
190
+ end
191
+ affected = [affected, result].max
192
+ end
193
+ end until ids.to_a.empty?
194
+ end
195
+ affected
196
+ end
197
+
198
+ def cascade_update_all(updates, conditions, options)
199
+ mti_clear_identity_map
200
+ affected = 0
201
+ stringified_updates = updates.stringify_keys
202
+ ids = conditions ? select(arel_table[primary_key.to_sym]).where(conditions).apply_finder_options(options.slice(:limit, :order)).collect(&:id) : []
203
+ transaction do
204
+ begin
205
+ batch = ids.pop(10000)
206
+ shards.each do |shard|
207
+ cols = shards.delegate_map(shard.name, stringified_updates)
208
+ next if cols.empty?
209
+ result = if conditions.nil?
210
+ shard.klass.update_all(cols)
211
+ elsif shard.polymorphic_type
212
+ shard.klass.update_all(cols, shard.foreign_key => batch, shard.polymorphic_type => shard.type_value)
213
+ else
214
+ shard.klass.update_all(cols, shard.foreign_key => batch)
215
+ end
216
+ affected = [affected, result].max
217
+ end
218
+ end until ids.to_a.empty?
219
+ end
220
+ affected
221
+ end
222
+
223
+ def shards
224
+ @shards ||= EmptyEye::ShardCollection.new(self)
225
+ end
226
+
227
+ #we need a name for the view
228
+ #need to have a way to set this
229
+ def compute_view_name
230
+ if master_class.descends_from_active_record?
231
+ master_class.send(:compute_table_name)
232
+ else
233
+ master_class.name.underscore.pluralize
234
+ end
235
+ end
236
+
237
+ private
238
+
239
+ def mti_clear_identity_map
240
+ ActiveRecord::IdentityMap.repository[symbolized_base_class].clear if ActiveRecord::IdentityMap.enabled?
241
+ end
242
+
243
+ #get the schema version
244
+ #we shouldnt recreate views that we donth have to
245
+ def mti_schema_version
246
+ check_for_name_error
247
+ return nil unless connection.table_exists?(compute_view_name)
248
+ return nil unless mti_view_versioned?
249
+ t = Arel::Table.new(compute_view_name)
250
+ q = t.project(t[:mti_schema_version])
251
+ connection.select_value(q.to_sql)
252
+ rescue
253
+ nil
254
+ end
255
+
256
+ #determine if what we want to name our view already exists
257
+ def check_for_name_error
258
+ if connection.tables_without_views.include?(compute_view_name)
259
+ raise(EmptyEye::ViewNameError, "MTI view cannot be created because a table named '#{compute_view_name}' already exists")
260
+ end
261
+ end
262
+
263
+ #we need to create the sql first to determine the schema_version
264
+ #if the current schema version is the same as the old dont recreate the view
265
+ #if it is nil then recreate
266
+ def create_view?
267
+ shards.create_view_sql
268
+ schema_version = mti_schema_version
269
+ schema_version.nil? or schema_version != shards.schema_version
270
+ end
271
+
272
+ #always recreate
273
+ def mti_view_versioned?
274
+ connection.columns(compute_view_name).any? {|c| c.name == 'mti_schema_version'}
275
+ end
276
+
277
+ #drop the view; dont check if we can, just rescue any errors
278
+ #create the view
279
+ def create_view
280
+ connection.execute("DROP VIEW #{compute_view_name}") rescue nil
281
+ connection.execute(shards.view_sql)
282
+ end
283
+
284
+ #build the arel query once and memoize it
285
+ #this is essentially the select to remove type column
286
+ def columns_except_type
287
+ @columns_except_type ||= begin
288
+ query = arel_table
289
+ (column_names - [inheritance_column]).each do |c|
290
+ query = query.project(arel_table[c.to_sym])
291
+ end
292
+ query
293
+ end
294
+ @columns_except_type.dup
295
+ end
296
+
297
+
298
+ end
299
+ end
300
+ end
@@ -2,7 +2,7 @@ module EmptyEye
2
2
  module VERSION
3
3
  MAJOR = 0
4
4
  MINOR = 4
5
- TINY = 2
5
+ TINY = 3
6
6
 
7
7
  STRING = [MAJOR, MINOR, TINY].join('.')
8
8
  end
data/lib/empty_eye.rb CHANGED
@@ -1,15 +1,16 @@
1
1
  require "active_record"
2
2
  require "arel"
3
+ require 'digest'
3
4
 
4
5
  require "empty_eye/version"
5
6
 
6
7
  require "empty_eye/persistence"
7
8
  require "empty_eye/relation"
8
9
  require "empty_eye/errors"
9
- require "empty_eye/view_extension"
10
- require "empty_eye/primary_view_extension"
11
- require "empty_eye/view_extension_collection"
12
10
  require "empty_eye/shard"
11
+ require "empty_eye/primary_shard"
12
+ require "empty_eye/shard_collection"
13
+ require "empty_eye/shard_wrangler"
13
14
  require "empty_eye/associations/builder/shard_has_one"
14
15
  require "empty_eye/associations/shard_has_one_association"
15
16
  require "empty_eye/associations/shard_association_scope"
@@ -20,7 +21,7 @@ require "empty_eye/active_record/schema_dumper"
20
21
  require "empty_eye/active_record/connection_adapter"
21
22
 
22
23
  module EmptyEye
23
- # Your code goes here...
24
+
24
25
  end
25
26
 
26
27
  ::ActiveRecord::Base.send :include, EmptyEye::Persistence
data/spec/spec_helper.rb CHANGED
@@ -50,12 +50,6 @@ ActiveRecord::Migration.create_table :eating_venues_core, :force => true do |t|
50
50
  t.string :longitude
51
51
  end
52
52
 
53
- ActiveRecord::Migration.create_table :eating_venues_core, :force => true do |t|
54
- t.string :api_venue_id
55
- t.string :latitude
56
- t.string :longitude
57
- end
58
-
59
53
  ActiveRecord::Migration.create_table :garages, :force => true do |t|
60
54
  t.boolean :privately_owned
61
55
  t.integer :max_wait_days
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: empty_eye
3
3
  version: !ruby/object:Gem::Version
4
- hash: 11
4
+ hash: 9
5
5
  prerelease:
6
6
  segments:
7
7
  - 0
8
8
  - 4
9
- - 2
10
- version: 0.4.2
9
+ - 3
10
+ version: 0.4.3
11
11
  platform: ruby
12
12
  authors:
13
13
  - thegboat
@@ -15,7 +15,7 @@ autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
17
 
18
- date: 2012-03-10 23:00:00 -05:00
18
+ date: 2012-03-12 00:00:00 -04:00
19
19
  default_executable:
20
20
  dependencies:
21
21
  - !ruby/object:Gem::Dependency
@@ -104,13 +104,13 @@ files:
104
104
  - lib/empty_eye/associations/shard_has_one_association.rb
105
105
  - lib/empty_eye/errors.rb
106
106
  - lib/empty_eye/persistence.rb
107
- - lib/empty_eye/primary_view_extension.rb
107
+ - lib/empty_eye/primary_shard.rb
108
108
  - lib/empty_eye/relation.rb
109
109
  - lib/empty_eye/shard.rb
110
110
  - lib/empty_eye/shard_association_reflection.rb
111
+ - lib/empty_eye/shard_collection.rb
112
+ - lib/empty_eye/shard_wrangler.rb
111
113
  - lib/empty_eye/version.rb
112
- - lib/empty_eye/view_extension.rb
113
- - lib/empty_eye/view_extension_collection.rb
114
114
  - spec/configuration_spec.rb
115
115
  - spec/mti_crud_spec.rb
116
116
  - spec/mti_to_sti_to_mti_crud_spec.rb
@@ -1,114 +0,0 @@
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
- def polymorphic_type
72
- return unless association.options[:as]
73
- "#{association.options[:as]}_type"
74
- end
75
-
76
- private
77
-
78
- #class to whom this extension belongs
79
- def parent
80
- association.active_record
81
- end
82
-
83
- #class of the extension table
84
- def klass
85
- association.klass
86
- end
87
-
88
- #uses association name to create alias to prevent non unique aliases
89
- def alias_name
90
- name.to_s.pluralize
91
- end
92
-
93
- #user declared exceptions ... exclude these columns from the parent inheritance
94
- def exceptions
95
- association.options[:except].to_a.collect(&:to_s)
96
- end
97
-
98
- #user declared restrictions ... restrict parent inheritance columns to these
99
- def restrictions
100
- only = association.options[:only].to_a.collect(&:to_s)
101
- only.empty? ? table_columns : only
102
- end
103
-
104
- #we want to omit these columns
105
- def exclude
106
- [exceptions, self.class.exclude_always, foreign_key, polymorphic_type].flatten.uniq
107
- end
108
-
109
- #all the columns of the extensions table
110
- def table_columns
111
- klass.column_names
112
- end
113
- end
114
- end