empty_eye 0.4.4 → 0.4.5

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,3 +1,15 @@
1
+ # 0.4.5 / 2012-03-11 / Grady Griffin
2
+
3
+ * refactored some validation logic code
4
+ * reorganized code to clean up active record base
5
+ * added generator to manage view versioning
6
+ * removed old view versioning code
7
+ * added view manager class to handle view versioning, creating and updating
8
+
9
+ # 0.4.4 / 2012-03-11 / Grady Griffin
10
+
11
+ * added various connection adapters; only mysql tested
12
+
1
13
  # 0.4.3 / 2012-03-11 / Grady Griffin
2
14
 
3
15
  * major refactor to do less in active record base
data/README.md CHANGED
@@ -2,6 +2,22 @@
2
2
 
3
3
  ActiveRecord based MTI gem powered by database views
4
4
 
5
+ add to your Gemfile
6
+
7
+ gem 'empty_eye'
8
+
9
+ and bundle or
10
+
11
+ gem install empty_eye
12
+
13
+ when using rails run the optional migration generator and the migration
14
+
15
+ this migration tracks view versions and its usage is highly recommended
16
+
17
+ rails generate empty_eye
18
+ => create db/migrate/20120313042059_create_empty_eye_views_table.rb
19
+ rake db:migrate
20
+
5
21
  #Issues
6
22
 
7
23
  * No known issues major issues; has been successful within data structures of high complexity (MTI to MTI, MTI to STI to MTI relationships)
@@ -1,91 +1,7 @@
1
1
  module ActiveRecord
2
2
  class Base
3
-
4
- class << self
5
-
6
- #am i a mti class? easier than making a new class type ... i tried
7
- def mti_class?
8
- !!@shard_wrangler
9
- end
10
-
11
- #interface for building mti_class
12
- #primary table is not necessary if the table named correctly (Bar => bars_core)
13
- #OR if the class inherits a primary table
14
- #simply wrap your greasy associations in this block
15
- def mti_class(primary_table = nil)
16
- raise(EmptyEye::AlreadyExtended, "MTI class method already invoked") if mti_class?
17
- self.primary_key = "id"
18
- @shard_wrangler = EmptyEye::ShardWrangler.create(self, primary_table)
19
- self.table_name = @shard_wrangler.compute_view_name
20
- before_yield = reflect_on_multiple_associations(:has_one)
21
- yield nil if block_given?
22
- mti_ancestors = reflect_on_multiple_associations(:has_one) - before_yield
23
- @shard_wrangler.wrangle_shards(mti_ancestors)
24
- true
25
- end
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
33
- end
34
-
35
- #we dont need no freakin' type condition
36
- #the view handles this
37
- def finder_needs_type_condition?
38
- !mti_class? and super
39
- end
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"] : [])
44
- end
45
-
46
- #the class of primary shard
47
- def shard_wrangler
48
- @shard_wrangler
49
- end
50
-
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
59
- end
60
-
61
- private
62
-
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
70
- end
71
-
72
- private
73
-
74
- #a pseudo association method mapping us back to instances primary shard
75
- def shard_wrangler
76
- @shard_wrangler ||= if new_record?
77
- self.class.shard_wrangler.new(:mti_instance => self)
78
- else
79
- rtn = self.class.shard_wrangler.find_by_id(id)
80
- rtn.mti_instance = self
81
- rtn
82
- end
83
- end
84
-
85
- #is the instance an instance of mti_class?
86
- def mti_class?
87
- self.class.mti_class?
88
- end
89
-
3
+ include EmptyEye::Persistence
4
+ include EmptyEye::Relation
5
+ include EmptyEye::BaseMethods
90
6
  end
91
7
  end
@@ -0,0 +1,89 @@
1
+ module EmptyEye
2
+ module BaseMethods
3
+
4
+ def self.included(base)
5
+ base.extend ClassMethods
6
+ end
7
+
8
+ module ClassMethods
9
+
10
+ #am i a mti class? easier than making a new class type ... i tried
11
+ def mti_class?
12
+ !!@shard_wrangler
13
+ end
14
+
15
+ #interface for building mti_class
16
+ #primary table is not necessary if the table named correctly (Bar => bars_core)
17
+ #OR if the class inherits a primary table
18
+ #simply wrap your greasy associations in this block
19
+ def mti_class(primary_table = nil)
20
+ raise(EmptyEye::AlreadyExtended, "MTI class method already invoked") if mti_class?
21
+ self.primary_key = "id"
22
+ @shard_wrangler = EmptyEye::ShardWrangler.create(self, primary_table)
23
+ self.table_name = @shard_wrangler.compute_view_name
24
+ before_yield = reflect_on_multiple_associations(:has_one)
25
+ yield nil if block_given?
26
+ mti_ancestors = reflect_on_multiple_associations(:has_one) - before_yield
27
+ @shard_wrangler.wrangle_shards(mti_ancestors)
28
+ true
29
+ end
30
+
31
+ #we need this when we add new associaton types to extend with
32
+ #we could use the baked in version for now
33
+ def reflect_on_multiple_associations(*assoc_types)
34
+ assoc_types.collect do |assoc_type|
35
+ reflect_on_all_associations(assoc_type)
36
+ end.flatten.uniq
37
+ end
38
+
39
+ #we dont need no freakin' type condition
40
+ #the view handles this
41
+ def finder_needs_type_condition?
42
+ !mti_class? and super
43
+ end
44
+
45
+ #the class of primary shard
46
+ def shard_wrangler
47
+ @shard_wrangler
48
+ end
49
+
50
+ def descends_from_active_record?
51
+ if superclass.abstract_class?
52
+ superclass.descends_from_active_record?
53
+ elsif mti_class?
54
+ superclass == ActiveRecord::Base
55
+ else
56
+ superclass == ActiveRecord::Base || !columns_hash.include?(inheritance_column)
57
+ end
58
+ end
59
+
60
+ private
61
+
62
+ end
63
+
64
+ def valid?(context = nil)
65
+ context ||= (new_record? ? :create : :update)
66
+ output = super(context)
67
+ return errors.empty? && output unless mti_class?
68
+ shard_wrangler.valid?(context) && errors.empty? && output
69
+ end
70
+
71
+ private
72
+
73
+ #a pseudo association method mapping us back to instances primary shard
74
+ def shard_wrangler
75
+ @shard_wrangler ||= if new_record?
76
+ self.class.shard_wrangler.new(:master_instance => self)
77
+ else
78
+ rtn = self.class.shard_wrangler.find_by_id(id)
79
+ rtn.master_instance = self
80
+ rtn
81
+ end
82
+ end
83
+
84
+ #is the instance an instance of mti_class?
85
+ def mti_class?
86
+ self.class.mti_class?
87
+ end
88
+ end
89
+ end
@@ -1,6 +1,5 @@
1
1
  module EmptyEye
2
2
  module Persistence
3
- extend ActiveSupport::Concern
4
3
 
5
4
  #if it is not a mti_class do what you do
6
5
  #else let the primary shard do the saving
@@ -112,9 +112,6 @@ module EmptyEye
112
112
  end
113
113
  end
114
114
 
115
- self.schema_version = Digest::MD5.hexdigest(query.to_sql)
116
- query.project("'#{schema_version}' AS mti_schema_version")
117
-
118
115
  #we dont need to keep this data
119
116
  free_arel_columns
120
117
 
@@ -37,13 +37,13 @@ module EmptyEye
37
37
  #we usually know the master instance ahead of time
38
38
  #so we should take care to set this manually
39
39
  #we want to avoid the lookup
40
- def mti_instance
41
- @mti_instance || master_class.find_by_id(id)
40
+ def master_instance
41
+ @master_instance || master_class.find_by_id(id)
42
42
  end
43
43
 
44
44
  #setter used to associate the wrangler with the master instance
45
- def mti_instance=(instance)
46
- @mti_instance = instance
45
+ def master_instance=(instance)
46
+ @master_instance = instance
47
47
  end
48
48
 
49
49
  #special save so that the wrangler can keep the master's instance tables consistent
@@ -52,8 +52,8 @@ module EmptyEye
52
52
  #this will autosave shards
53
53
  save
54
54
  #reset the id and then reload
55
- mti_instance.id = id
56
- mti_instance.reload
55
+ master_instance.id = id
56
+ master_instance.reload
57
57
  end
58
58
 
59
59
  #reflection on master class; this should never change
@@ -66,7 +66,8 @@ module EmptyEye
66
66
  write_attributes
67
67
  output = super(context)
68
68
  errors.each do |attr, message|
69
- mti_instance.errors.add(attr, message)
69
+ attr = attr.to_s.partition('.').last if attr.to_s =~ /\./
70
+ master_instance.errors.add(attr, message)
70
71
  end
71
72
  errors.empty? && output
72
73
  end
@@ -75,10 +76,11 @@ module EmptyEye
75
76
 
76
77
  def write_attributes
77
78
  #make sure all the shards are there
78
- cascade_build_associations
79
+ cascade_build_associations if master_instance.new_record?
79
80
  #this will propagate setters to the appropriate shards
80
81
  assign_attributes(mti_safe_attributes)
81
82
  self.type = master_class.name if respond_to?("type=")
83
+ self.updated_at = Time.now if respond_to?("updated_at=") and not changed?
82
84
  self
83
85
  end
84
86
 
@@ -88,21 +90,17 @@ module EmptyEye
88
90
 
89
91
  #make sure the primary shard only tries to update what he should
90
92
  def mti_safe_attributes
91
- mti_instance.attributes.except(
93
+ master_instance.attributes.except(
92
94
  *self.class.primary_shard.exclude
93
95
  )
94
96
  end
95
97
 
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
98
+ #all the instance shards should exist
99
99
  def cascade_build_associations
100
100
  #go through each shard making sure it is exists and is loaded
101
101
  shards.each do |shard|
102
102
  next if shard.primary
103
- assoc = send(shard.name)
104
- assoc ||= send("build_#{shard.name}")
105
- send("#{shard.name}=", assoc)
103
+ send(shard.name) || send("build_#{shard.name}")
106
104
  end
107
105
  end
108
106
 
@@ -166,7 +164,7 @@ module EmptyEye
166
164
  mti_ancestors.each do |assoc|
167
165
  shards.create_with(assoc)
168
166
  end
169
- create_view if create_view?
167
+ create_view
170
168
  master_class.reset_column_information
171
169
  end
172
170
 
@@ -239,46 +237,11 @@ module EmptyEye
239
237
  def mti_clear_identity_map
240
238
  ActiveRecord::IdentityMap.repository[symbolized_base_class].clear if ActiveRecord::IdentityMap.enabled?
241
239
  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
240
 
277
241
  #drop the view; dont check if we can, just rescue any errors
278
242
  #create the view
279
243
  def create_view
280
- connection.execute("DROP VIEW #{compute_view_name}") rescue nil
281
- connection.execute(shards.view_sql)
244
+ EmptyEye::ViewManager.create_view(compute_view_name, shards.create_view_sql)
282
245
  end
283
246
 
284
247
  #build the arel query once and memoize it
@@ -2,7 +2,7 @@ module EmptyEye
2
2
  module VERSION
3
3
  MAJOR = 0
4
4
  MINOR = 4
5
- TINY = 4
5
+ TINY = 5
6
6
 
7
7
  STRING = [MAJOR, MINOR, TINY].join('.')
8
8
  end
@@ -0,0 +1,82 @@
1
+ module EmptyEye
2
+ class ViewManager < ActiveRecord::Base
3
+ self.table_name = "empty_eye_views"
4
+
5
+ attr_accessor :sql
6
+
7
+ def self.create_view(view_name, sql)
8
+ if table_exists?
9
+ manager = find_by_view_name(view_name)
10
+ manager ||= new(:view_name => view_name)
11
+ manager.sql = sql
12
+ manager.create_view
13
+ else
14
+ drop_view(view_name)
15
+ execute_view(sql)
16
+ end
17
+ end
18
+
19
+ def self.execute_view(sql)
20
+ connection.execute(sql)
21
+ end
22
+
23
+ def self.drop_view(view_name)
24
+ connection.execute %{DROP VIEW #{view_name}} rescue nil
25
+ end
26
+
27
+ def create_view
28
+ return unless create_view?
29
+ drop_view if view_exists?
30
+ self.version = compute_version
31
+ save
32
+ execute_view_sql
33
+ end
34
+
35
+ private
36
+
37
+ def execute(sql)
38
+ self.class.connection.execute(sql)
39
+ end
40
+
41
+ def table_exists?
42
+ self.class.connection.table_exists?(view_name)
43
+ end
44
+
45
+ def ordinary_table_exists?
46
+ self.class.connection.tables_without_views.include?(view_name)
47
+ end
48
+
49
+ def compute_version
50
+ Digest::MD5.hexdigest(sql)
51
+ end
52
+
53
+ def version_current?
54
+ compute_version == version
55
+ end
56
+
57
+ def view_exists?
58
+ table_exists? and !ordinary_table_exists?
59
+ end
60
+
61
+ def create_view?
62
+ check_for_name_error
63
+ !(version_current? and view_exists?)
64
+ end
65
+
66
+ def drop_view
67
+ self.class.drop_view(view_name)
68
+ end
69
+
70
+ def execute_view_sql
71
+ self.class.execute_view(sql)
72
+ end
73
+
74
+ #determine if what we want to name our view already exists
75
+ def check_for_name_error
76
+ if ordinary_table_exists?
77
+ raise(EmptyEye::ViewNameError, "MTI view cannot be created because a table named '#{view_name}' already exists")
78
+ end
79
+ end
80
+
81
+ end
82
+ end
data/lib/empty_eye.rb CHANGED
@@ -6,7 +6,10 @@ require "empty_eye/version"
6
6
 
7
7
  require "empty_eye/persistence"
8
8
  require "empty_eye/relation"
9
+ require "empty_eye/base_methods"
9
10
  require "empty_eye/errors"
11
+
12
+ require "empty_eye/view_manager"
10
13
  require "empty_eye/shard"
11
14
  require "empty_eye/primary_shard"
12
15
  require "empty_eye/shard_collection"
@@ -30,8 +33,6 @@ module EmptyEye
30
33
 
31
34
  end
32
35
 
33
- ::ActiveRecord::Base.send :include, EmptyEye::Persistence
34
- ::ActiveRecord::Base.send :include, EmptyEye::Relation
35
36
  ::ActiveRecord::Associations::Builder::HasOne.valid_options += [:except, :only]
36
- ::ActiveRecord::Associations::Builder::BelongsTo.valid_options += [:except, :only]
37
+ #::ActiveRecord::Associations::Builder::BelongsTo.valid_options += [:except, :only]
37
38
 
@@ -0,0 +1,13 @@
1
+ module EmptyEye
2
+ module Generators
3
+ class EmptyEyeGenerator < Rails::Generators::Base
4
+ source_root File.expand_path("../templates", __FILE__)
5
+
6
+ def add_migration
7
+ timestamp = Time.now.utc.strftime("%Y%m%d%H%M%S")
8
+ template("migration.rb", "db/migrate/#{timestamp}_create_empty_eye_views_table.rb")
9
+ end
10
+
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,14 @@
1
+ class CreateEmptyEyeViewsTable < ActiveRecord::Migration
2
+ def up
3
+ create_table :empty_eye_views, :force => true do |t|
4
+ t.string :view_name, :null => false
5
+ t.string :version, :limit => 32, :null => false
6
+ end
7
+
8
+ add_index :empty_eye_views, :view_name
9
+ end
10
+
11
+ def down
12
+ drop_table :empty_eye_views
13
+ end
14
+ end
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: 7
4
+ hash: 5
5
5
  prerelease:
6
6
  segments:
7
7
  - 0
8
8
  - 4
9
- - 4
10
- version: 0.4.4
9
+ - 5
10
+ version: 0.4.5
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-12 00:00:00 -04:00
18
+ date: 2012-03-13 00:00:00 -04:00
19
19
  default_executable:
20
20
  dependencies:
21
21
  - !ruby/object:Gem::Dependency
@@ -108,6 +108,7 @@ files:
108
108
  - lib/empty_eye/associations/builder/shard_has_one.rb
109
109
  - lib/empty_eye/associations/shard_association_scope.rb
110
110
  - lib/empty_eye/associations/shard_has_one_association.rb
111
+ - lib/empty_eye/base_methods.rb
111
112
  - lib/empty_eye/errors.rb
112
113
  - lib/empty_eye/persistence.rb
113
114
  - lib/empty_eye/primary_shard.rb
@@ -117,6 +118,9 @@ files:
117
118
  - lib/empty_eye/shard_collection.rb
118
119
  - lib/empty_eye/shard_wrangler.rb
119
120
  - lib/empty_eye/version.rb
121
+ - lib/empty_eye/view_manager.rb
122
+ - lib/rails/generators/empty_eye/empty_eye_generator.rb
123
+ - lib/rails/generators/empty_eye/templates/migration.rb
120
124
  - spec/configuration_spec.rb
121
125
  - spec/mti_crud_spec.rb
122
126
  - spec/mti_to_sti_to_mti_crud_spec.rb