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 +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
|