empty_eye 0.4.4 → 0.4.5

Sign up to get free protection for your applications and to get access to all the features.
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