empty_eye 0.4.0
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/.gitignore +4 -0
- data/CHANGELOG.md +43 -0
- data/Gemfile +4 -0
- data/MIT-LICENSE +20 -0
- data/README.md +109 -0
- data/Rakefile +17 -0
- data/empty_eye.gemspec +32 -0
- data/lib/empty_eye/active_record/base.rb +133 -0
- data/lib/empty_eye/active_record/connection_adapter.rb +46 -0
- data/lib/empty_eye/active_record/schema_dumper.rb +18 -0
- data/lib/empty_eye/associations/builder/shard_has_one.rb +20 -0
- data/lib/empty_eye/associations/shard_association_scope.rb +79 -0
- data/lib/empty_eye/associations/shard_has_one_association.rb +35 -0
- data/lib/empty_eye/errors.rb +5 -0
- data/lib/empty_eye/persistence.rb +50 -0
- data/lib/empty_eye/primary_view_extension.rb +137 -0
- data/lib/empty_eye/relation.rb +85 -0
- data/lib/empty_eye/shard.rb +130 -0
- data/lib/empty_eye/shard_association_reflection.rb +28 -0
- data/lib/empty_eye/version.rb +9 -0
- data/lib/empty_eye/view_extension.rb +115 -0
- data/lib/empty_eye/view_extension_collection.rb +222 -0
- data/lib/empty_eye.rb +30 -0
- data/spec/configuration_spec.rb +35 -0
- data/spec/mti_crud_spec.rb +130 -0
- data/spec/mti_to_sti_to_mti_crud_spec.rb +160 -0
- data/spec/spec_helper.rb +120 -0
- data/spec/sti_to_mti_crud_spec.rb +138 -0
- data/spec/validation_spec.rb +84 -0
- metadata +160 -0
@@ -0,0 +1,137 @@
|
|
1
|
+
module EmptyEye
|
2
|
+
class PrimaryViewExtension
|
3
|
+
|
4
|
+
#primary extension for parent class
|
5
|
+
#manages associations for database updates
|
6
|
+
#has many of the same interfaces as view extensions
|
7
|
+
|
8
|
+
def initialize(table_name, parent)
|
9
|
+
@table = table_name
|
10
|
+
@parent = parent
|
11
|
+
create_shard
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.connection
|
15
|
+
ActiveRecord::Base.connection
|
16
|
+
end
|
17
|
+
|
18
|
+
#never include the type field as it shouldnt be needed and cant be updated anyway
|
19
|
+
def self.exclude_always
|
20
|
+
['type']
|
21
|
+
end
|
22
|
+
|
23
|
+
#class to which this extension belongs
|
24
|
+
def parent
|
25
|
+
@parent
|
26
|
+
end
|
27
|
+
|
28
|
+
#to let the outside word know it is primary
|
29
|
+
def primary
|
30
|
+
true
|
31
|
+
end
|
32
|
+
|
33
|
+
#class that will mimic the associations of the parent for updating db
|
34
|
+
def shard
|
35
|
+
@shard
|
36
|
+
end
|
37
|
+
|
38
|
+
# the tablename
|
39
|
+
def table
|
40
|
+
@table
|
41
|
+
end
|
42
|
+
|
43
|
+
# the alias for the table; for primary we just use the table name
|
44
|
+
def name
|
45
|
+
@table
|
46
|
+
end
|
47
|
+
|
48
|
+
# arel table for generating the view
|
49
|
+
def arel_table
|
50
|
+
@arel_table ||= Arel::Table.new(table)
|
51
|
+
end
|
52
|
+
|
53
|
+
#this may change but for now the key is the primary id of the parent and shard
|
54
|
+
def key
|
55
|
+
arel_table[:id]
|
56
|
+
end
|
57
|
+
|
58
|
+
def foreign_key
|
59
|
+
"id"
|
60
|
+
end
|
61
|
+
|
62
|
+
def sti_also?
|
63
|
+
!parent.descends_from_active_record?
|
64
|
+
end
|
65
|
+
|
66
|
+
#arel column of type field
|
67
|
+
def type_column
|
68
|
+
if sti_also?
|
69
|
+
arel_table[parent.inheritance_column.to_sym]
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
#value of the polymorphic column
|
74
|
+
def type_value
|
75
|
+
parent.name if type_column
|
76
|
+
end
|
77
|
+
|
78
|
+
#always null for primary
|
79
|
+
def polymorphic_type
|
80
|
+
nil
|
81
|
+
end
|
82
|
+
|
83
|
+
#table columns
|
84
|
+
def table_columns
|
85
|
+
self.class.connection.columns(table).collect(&:name)
|
86
|
+
end
|
87
|
+
|
88
|
+
def exclude
|
89
|
+
self.class.exclude_always
|
90
|
+
end
|
91
|
+
|
92
|
+
#the table columns that will be extended in sql
|
93
|
+
def columns
|
94
|
+
table_columns - exclude
|
95
|
+
end
|
96
|
+
|
97
|
+
#create associations for shard class to mimic parent
|
98
|
+
def have_one(ext)
|
99
|
+
#this is myself; dont associate
|
100
|
+
return if ext.primary
|
101
|
+
mimic = ext.association
|
102
|
+
return if shard.reflect_on_association(mimic.name)
|
103
|
+
options = mimic.options.dup
|
104
|
+
options.merge!(default_has_one_options)
|
105
|
+
options.merge!(:foreign_key => ext.foreign_key)
|
106
|
+
shard.send(mimic.macro, mimic.name, options)
|
107
|
+
end
|
108
|
+
|
109
|
+
#delegate setters to appropriate associations
|
110
|
+
def delegate_to(col, ext)
|
111
|
+
return if ext.primary
|
112
|
+
shard.send(:delegate, "#{col}=", {:to => ext.name})
|
113
|
+
end
|
114
|
+
|
115
|
+
private
|
116
|
+
|
117
|
+
#MTI wouldnt make any sense if these were not forced in the associations
|
118
|
+
def default_has_one_options
|
119
|
+
{:autosave => true, :validate => true, :dependent => :destroy}
|
120
|
+
end
|
121
|
+
|
122
|
+
#if possible shard inherits from the superclass
|
123
|
+
def shard_inherit_from
|
124
|
+
parent.base_class == parent ? ActiveRecord::Base : parent.send(:superclass)
|
125
|
+
end
|
126
|
+
|
127
|
+
#create a class to manage the parents associations
|
128
|
+
def create_shard
|
129
|
+
new_class = Class.new(shard_inherit_from)
|
130
|
+
new_class.send(:include, Shard)
|
131
|
+
new_class.table_name = table
|
132
|
+
new_class.mti_master_class = parent
|
133
|
+
@shard = EmptyEye.const_set("#{parent.to_s}Shard", new_class)
|
134
|
+
end
|
135
|
+
|
136
|
+
end
|
137
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
module EmptyEye
|
2
|
+
module Relation
|
3
|
+
|
4
|
+
def self.included(base)
|
5
|
+
base.extend ClassMethods
|
6
|
+
end
|
7
|
+
|
8
|
+
module ClassMethods
|
9
|
+
|
10
|
+
def delete_all(conditions = nil)
|
11
|
+
return super unless mti_class?
|
12
|
+
mti_clear_identity_map
|
13
|
+
affected = 0
|
14
|
+
#if something goes wrong forget it all
|
15
|
+
transaction do
|
16
|
+
if conditions
|
17
|
+
#batch up ids
|
18
|
+
ids = select("`#{table_name}`.`#{primary_key}`").where(conditions).collect(&:id)
|
19
|
+
#delete all the shards of the mti class matching ids
|
20
|
+
mti_batch_perform(ids) do |ext, batch|
|
21
|
+
if ext.polymorphic_type
|
22
|
+
ext.shard.delete_all(ext.foreign_key => batch, ext.polymorphic_type => ext.type_value)
|
23
|
+
else
|
24
|
+
ext.shard.delete_all(ext.foreign_key => batch)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
else
|
28
|
+
#way simpler if there are no conditions; kill everyone
|
29
|
+
extended_with.each do |ext|
|
30
|
+
affected = [affected, ext.shard.delete_all].max
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
affected
|
35
|
+
end
|
36
|
+
|
37
|
+
def update_all(updates, conditions = nil, options = {})
|
38
|
+
return super unless mti_class?
|
39
|
+
raise(EmptyEye::InvalidUpdate, "update values for a MTI class must be a hash") unless updates.is_a?(Hash)
|
40
|
+
mti_clear_identity_map
|
41
|
+
stringified_updates = updates.stringify_keys
|
42
|
+
affected = 0
|
43
|
+
transaction do
|
44
|
+
if conditions
|
45
|
+
#batch up ids
|
46
|
+
ids = select(arel_table[primary_key.to_sym]).where(conditions).apply_finder_options(options.slice(:limit, :order)).collect(&:id)
|
47
|
+
#update all the shards of the mti class matching ids
|
48
|
+
affected = mti_batch_perform(ids) do |ext, batch|
|
49
|
+
#delegate map ingests the update hash and regurgitates a smaller hash of the values the shard can handle
|
50
|
+
cols = extended_with.delegate_map(ext.name, stringified_updates)
|
51
|
+
cols.empty? ? 0 : ext.shard.update_all(cols, ext.foreign_key => batch)
|
52
|
+
end
|
53
|
+
else
|
54
|
+
#way simpler if there are no conditions; change the world!
|
55
|
+
extended_with.each do |ext|
|
56
|
+
cols = extended_with.delegate_map(ext.name, stringified_updates)
|
57
|
+
affected = [(cols.empty? ? 0 : ext.shard.update_all(cols)), affected].max
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
affected
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
|
66
|
+
def mti_clear_identity_map
|
67
|
+
ActiveRecord::IdentityMap.repository[symbolized_base_class].clear if ActiveRecord::IdentityMap.enabled?
|
68
|
+
end
|
69
|
+
|
70
|
+
#lets do 10000 at a time
|
71
|
+
def mti_batch_perform(ids)
|
72
|
+
affected = 0
|
73
|
+
until ids.to_a.empty?
|
74
|
+
current_ids = ids.pop(10000)
|
75
|
+
extended_with.each do |ext|
|
76
|
+
rtn = yield(ext, current_ids)
|
77
|
+
affected = [affected,rtn].max
|
78
|
+
end
|
79
|
+
end
|
80
|
+
affected
|
81
|
+
end
|
82
|
+
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,130 @@
|
|
1
|
+
module EmptyEye
|
2
|
+
module Shard
|
3
|
+
|
4
|
+
#a module that extendd the class that serves as a pointer to the primary table
|
5
|
+
#when there is a superclass the will inherit from that else it will inherit from ActiveRecord
|
6
|
+
#the primary shard manages all the MTI associated tables for the master class
|
7
|
+
|
8
|
+
def self.included(base)
|
9
|
+
base.extend ClassMethods
|
10
|
+
end
|
11
|
+
|
12
|
+
#the instance that owns this primary shard
|
13
|
+
def mti_instance
|
14
|
+
@mti_instance
|
15
|
+
end
|
16
|
+
|
17
|
+
#setter used to associate the primary shard with the master instance
|
18
|
+
def mti_instance=(instance)
|
19
|
+
@mti_instance = instance
|
20
|
+
end
|
21
|
+
|
22
|
+
#special save so that the primary shard can keep the master instances tables consistent
|
23
|
+
def cascade_save
|
24
|
+
#make sure all the shards are there
|
25
|
+
cascade_build_associations
|
26
|
+
#this will propagate setters to the appropriate shards
|
27
|
+
assign_attributes(mti_safe_attributes)
|
28
|
+
self.type = mti_master_class.name if respond_to?("type=")
|
29
|
+
#this will autosave shards
|
30
|
+
save
|
31
|
+
#reset the id and then reload
|
32
|
+
mti_instance.id = id
|
33
|
+
mti_instance.reload
|
34
|
+
end
|
35
|
+
|
36
|
+
#reflection on master class; this should never change
|
37
|
+
def mti_master_class
|
38
|
+
self.class.mti_master_class
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
#make sure the primary shard only tries to update what he should
|
44
|
+
def mti_safe_attributes
|
45
|
+
mti_instance.attributes.except(
|
46
|
+
*self.mti_master_class.extended_with.primary.exclude
|
47
|
+
)
|
48
|
+
end
|
49
|
+
|
50
|
+
#all the instance shards should exist but lets be certain
|
51
|
+
#using an autobuild would be more efficient here
|
52
|
+
#we shouldnt load associations we dont need to
|
53
|
+
def cascade_build_associations
|
54
|
+
#go through each extension making sure it is exists and is loaded
|
55
|
+
mti_instance.class.extended_with.each do |ext|
|
56
|
+
next if ext.primary
|
57
|
+
assoc = send(ext.name)
|
58
|
+
assoc ||= send("build_#{ext.name}")
|
59
|
+
send("#{ext.name}=", assoc)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
module ClassMethods
|
64
|
+
|
65
|
+
#the shard uses special reflection; overriden here
|
66
|
+
def create_reflection(macro, name, options, active_record)
|
67
|
+
raise(EmptyEye::NotYetSupported, "through associations are not yet spported") if options[:through]
|
68
|
+
klass = options[:through] ? ShardThroughReflection : ShardAssociationReflection
|
69
|
+
reflection = klass.new(macro, name, options, active_record)
|
70
|
+
|
71
|
+
self.reflections = self.reflections.merge(name => reflection)
|
72
|
+
reflection
|
73
|
+
end
|
74
|
+
|
75
|
+
#finder methods should use the master classes base class not the shards
|
76
|
+
def type_condition(table = arel_table)
|
77
|
+
sti_column = table[inheritance_column.to_sym]
|
78
|
+
|
79
|
+
sti_column.eq(mti_master_class.name)
|
80
|
+
end
|
81
|
+
|
82
|
+
#overriding find_by_id
|
83
|
+
#this is used to retrieve the shard instance for the master instance
|
84
|
+
#the type column is removed
|
85
|
+
def find_by_id(val)
|
86
|
+
query = columns_except_type
|
87
|
+
query = query.where(arel_table[:id].eq(val))
|
88
|
+
find_by_sql(query.to_sql).first
|
89
|
+
end
|
90
|
+
|
91
|
+
#the shard uses a special association builder
|
92
|
+
def has_one(name, options = {})
|
93
|
+
Associations::Builder::ShardHasOne.build(self, name, options)
|
94
|
+
end
|
95
|
+
|
96
|
+
#reflection on master class; this should never change
|
97
|
+
def mti_master_class
|
98
|
+
@mti_master_class
|
99
|
+
end
|
100
|
+
|
101
|
+
#the mti_master_class value is set with this setter; should happen only once
|
102
|
+
def mti_master_class=(klass)
|
103
|
+
@mti_master_class = klass
|
104
|
+
end
|
105
|
+
|
106
|
+
#overriding to reset the special instance variable
|
107
|
+
def reset_column_information
|
108
|
+
@columns_except_type = nil
|
109
|
+
super
|
110
|
+
end
|
111
|
+
|
112
|
+
private
|
113
|
+
|
114
|
+
#build the arel query once and memoize it
|
115
|
+
#this is essentially the select to remove type column
|
116
|
+
def columns_except_type
|
117
|
+
@columns_except_type ||= begin
|
118
|
+
query = arel_table
|
119
|
+
(column_names - [inheritance_column]).each do |c|
|
120
|
+
query = query.project(arel_table[c.to_sym])
|
121
|
+
end
|
122
|
+
query
|
123
|
+
end
|
124
|
+
@columns_except_type.dup
|
125
|
+
end
|
126
|
+
|
127
|
+
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module EmptyEye
|
2
|
+
class ShardAssociationReflection < ActiveRecord::Reflection::AssociationReflection
|
3
|
+
#special reflection for shard
|
4
|
+
#very verbose but will be easier to update later
|
5
|
+
#better than monkey patching
|
6
|
+
|
7
|
+
def association_class
|
8
|
+
EmptyEye::Associations::ShardHasOneAssociation
|
9
|
+
#later we will support all singular associations; for now only has one
|
10
|
+
|
11
|
+
# case macro
|
12
|
+
# when :belongs_to
|
13
|
+
# if options[:polymorphic]
|
14
|
+
# EmptyEye::Associations::ShardBelongsToPolymorphicAssociation
|
15
|
+
# else
|
16
|
+
# EmptyEye::Associations::ShardBelongsToAssociation
|
17
|
+
# end
|
18
|
+
# when :has_one
|
19
|
+
# if options[:through]
|
20
|
+
# EmptyEye::Associations::ShardHasOneThroughAssociation
|
21
|
+
# else
|
22
|
+
# EmptyEye::Associations::ShardHasOneAssociation
|
23
|
+
# end
|
24
|
+
# end
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,115 @@
|
|
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
|
+
#value computed to remove this from column map; no need for the view to have it
|
72
|
+
def polymorphic_type
|
73
|
+
return unless association.options[:as]
|
74
|
+
"#{association.options[:as]}_type"
|
75
|
+
end
|
76
|
+
|
77
|
+
private
|
78
|
+
|
79
|
+
#class to whom this extension belongs
|
80
|
+
def parent
|
81
|
+
association.active_record
|
82
|
+
end
|
83
|
+
|
84
|
+
#class of the extension table
|
85
|
+
def klass
|
86
|
+
association.klass
|
87
|
+
end
|
88
|
+
|
89
|
+
#uses association name to create alias to prevent non unique aliases
|
90
|
+
def alias_name
|
91
|
+
name.to_s.pluralize
|
92
|
+
end
|
93
|
+
|
94
|
+
#user declared exceptions ... exclude these columns from the parent inheritance
|
95
|
+
def exceptions
|
96
|
+
association.options[:except].to_a.collect(&:to_s)
|
97
|
+
end
|
98
|
+
|
99
|
+
#user declared restrictions ... restrict parent inheritance columns to these
|
100
|
+
def restrictions
|
101
|
+
only = association.options[:only].to_a.collect(&:to_s)
|
102
|
+
only.empty? ? table_columns : only
|
103
|
+
end
|
104
|
+
|
105
|
+
#we want to omit these columns
|
106
|
+
def exclude
|
107
|
+
[exceptions, self.class.exclude_always, foreign_key, polymorphic_type].flatten.uniq
|
108
|
+
end
|
109
|
+
|
110
|
+
#all the columns of the extensions table
|
111
|
+
def table_columns
|
112
|
+
klass.column_names
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|