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