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 +12 -0
- data/README.md +16 -0
- data/lib/empty_eye/active_record/base.rb +3 -87
- data/lib/empty_eye/base_methods.rb +89 -0
- data/lib/empty_eye/persistence.rb +0 -1
- data/lib/empty_eye/shard_collection.rb +0 -3
- data/lib/empty_eye/shard_wrangler.rb +15 -52
- data/lib/empty_eye/version.rb +1 -1
- data/lib/empty_eye/view_manager.rb +82 -0
- data/lib/empty_eye.rb +4 -3
- data/lib/rails/generators/empty_eye/empty_eye_generator.rb +13 -0
- data/lib/rails/generators/empty_eye/templates/migration.rb +14 -0
- metadata +8 -4
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
|
-
|
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
|
@@ -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
|
41
|
-
@
|
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
|
46
|
-
@
|
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
|
-
|
56
|
-
|
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
|
-
|
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
|
-
|
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
|
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
|
-
|
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
|
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
|
-
|
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
|
data/lib/empty_eye/version.rb
CHANGED
@@ -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
|
-
|
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:
|
4
|
+
hash: 5
|
5
5
|
prerelease:
|
6
6
|
segments:
|
7
7
|
- 0
|
8
8
|
- 4
|
9
|
-
-
|
10
|
-
version: 0.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-
|
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
|