dbview_cti 0.1.4 → 0.1.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 +5 -0
- data/README.md +3 -1
- data/lib/db_view_cti/connection_adapters/schema_statements.rb +15 -6
- data/lib/db_view_cti/model/cti/association_validations.rb +57 -0
- data/lib/db_view_cti/model/cti/associations.rb +180 -0
- data/lib/db_view_cti/model/cti/destroy.rb +30 -0
- data/lib/db_view_cti/model/cti/hierarchy.rb +58 -0
- data/lib/db_view_cti/model/cti/sql.rb +23 -0
- data/lib/db_view_cti/model/cti/type_conversion.rb +55 -0
- data/lib/db_view_cti/model/cti.rb +6 -322
- data/lib/db_view_cti/version.rb +1 -1
- data/lib/dbview_cti.rb +11 -0
- data/spec/dummy-rails-3/app/models/space_ship.rb +2 -0
- data/spec/dummy-rails-3/app/models/space_shuttle.rb +5 -0
- data/spec/dummy-rails-3/db/migrate/20140411001620_add_upgraded_from_to_space_shuttles.rb +15 -0
- data/spec/dummy-rails-3/db/migrate/20140425182847_add_field_to_base_class.rb +14 -0
- data/spec/dummy-rails-4/db/schema.rb +32 -1
- data/spec/models/car_spec.rb +2 -0
- data/spec/models/space_shuttle_spec.rb +15 -0
- metadata +12 -4
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
|
@@ -175,7 +175,7 @@ end
|
|
|
175
175
|
|
|
176
176
|
## Associations
|
|
177
177
|
|
|
178
|
-
Associations (`has_many`, `has_one`, etc.) work and are inherited as you would expect. There are
|
|
178
|
+
Associations (`has_many`, `has_one`, etc.) work and are inherited as you would expect. There are three caveats:
|
|
179
179
|
|
|
180
180
|
* In the base class, you have to call `cti_base_class` before defining any associations:
|
|
181
181
|
|
|
@@ -199,6 +199,8 @@ class SpaceShip < Vehicle
|
|
|
199
199
|
end
|
|
200
200
|
```
|
|
201
201
|
|
|
202
|
+
* You have to make sure that the association is defined in both classes, e.g. if you have `belongs_to :car` in a class called Part then Car should also define the association with `has_many :parts` (or `has_one :part`).
|
|
203
|
+
|
|
202
204
|
## API
|
|
203
205
|
|
|
204
206
|
### Models
|
|
@@ -22,19 +22,18 @@ module DBViewCTI
|
|
|
22
22
|
# use with block in up/down methods
|
|
23
23
|
def cti_recreate_views_after_change_to(class_name, options = {})
|
|
24
24
|
klass = class_name.constantize
|
|
25
|
-
classes =
|
|
25
|
+
classes = klass.cti_all_descendants
|
|
26
|
+
# only add class_name if it is not the base class
|
|
27
|
+
classes = classes.unshift( class_name ) unless klass.cti_base_class?
|
|
26
28
|
# drop all views in reverse order
|
|
27
29
|
classes.reverse.each do |kklass|
|
|
28
30
|
cti_drop_view(kklass, options)
|
|
29
31
|
end
|
|
30
32
|
yield # perform table changes in block (e.g. add column)
|
|
31
33
|
# recreate views in forward order
|
|
34
|
+
cti_reset_column_information(class_name) if klass.cti_base_class?
|
|
32
35
|
classes.each do |kklass|
|
|
33
|
-
|
|
34
|
-
# view cache, so we have to make sure it is cleared
|
|
35
|
-
true_klass = kklass.constantize
|
|
36
|
-
true_klass.connection.schema_cache.clear_table_cache!(true_klass.table_name)
|
|
37
|
-
true_klass.reset_column_information
|
|
36
|
+
cti_reset_column_information(kklass)
|
|
38
37
|
cti_create_view(kklass, options)
|
|
39
38
|
end
|
|
40
39
|
end
|
|
@@ -47,6 +46,16 @@ module DBViewCTI
|
|
|
47
46
|
execute(query)
|
|
48
47
|
end
|
|
49
48
|
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def cti_reset_column_information(class_name)
|
|
53
|
+
# any column changes are reflected in the real table cache, but not in the
|
|
54
|
+
# view cache, so we have to make sure it is cleared
|
|
55
|
+
klass = class_name.constantize
|
|
56
|
+
klass.connection.schema_cache.clear_table_cache!(klass.table_name)
|
|
57
|
+
klass.reset_column_information
|
|
58
|
+
end
|
|
50
59
|
|
|
51
60
|
end
|
|
52
61
|
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
module DBViewCTI
|
|
2
|
+
module Model
|
|
3
|
+
module CTI
|
|
4
|
+
module AssociationValidations
|
|
5
|
+
extend ActiveSupport::Concern
|
|
6
|
+
|
|
7
|
+
included do
|
|
8
|
+
# validations
|
|
9
|
+
validate :cti_validate_associations, :cti_no_disable => true
|
|
10
|
+
attr_accessor :cti_disable_validations
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def cti_validate_associations
|
|
14
|
+
return_value = true
|
|
15
|
+
self.class.cti_association_proxies.each_key do |proxy_name|
|
|
16
|
+
proxy = instance_variable_get(proxy_name)
|
|
17
|
+
if proxy && !proxy.valid?
|
|
18
|
+
errors.messages.merge!(proxy.errors.messages)
|
|
19
|
+
return_value = false
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
return_value
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
module ClassMethods
|
|
26
|
+
|
|
27
|
+
# redefine validate to always add :unless proc so we can disable the validations for an object
|
|
28
|
+
# by setting the cti_disable_validations accessor to true
|
|
29
|
+
def validate(*args, &block)
|
|
30
|
+
# we specifically don't want to disable balidations belonging to associations. Based on the naming
|
|
31
|
+
# rails uses, we return immediately in such cases (there must be a cleaner way to do this...)
|
|
32
|
+
return super if args.first && args.first.to_s =~ /^validate_associated_records_for_/
|
|
33
|
+
# rest of implementation insipred by the validate implementation in rails
|
|
34
|
+
options = args.extract_options!.dup
|
|
35
|
+
return super if options[:cti_no_disable]
|
|
36
|
+
if options.key?(:unless)
|
|
37
|
+
options[:unless] = Array(options[:unless])
|
|
38
|
+
options[:unless].unshift( cti_validation_unless_proc )
|
|
39
|
+
else
|
|
40
|
+
options[:unless] = cti_validation_unless_proc
|
|
41
|
+
end
|
|
42
|
+
args << options
|
|
43
|
+
return super(*args, &block)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def cti_validation_unless_proc
|
|
47
|
+
@cti_validation_unless_proc ||= Proc.new do |object|
|
|
48
|
+
object.respond_to?(:cti_disable_validations) && object.cti_disable_validations
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
module DBViewCTI
|
|
2
|
+
module Model
|
|
3
|
+
module CTI
|
|
4
|
+
module Associations
|
|
5
|
+
extend ActiveSupport::Concern
|
|
6
|
+
|
|
7
|
+
included do
|
|
8
|
+
# for associations:
|
|
9
|
+
alias_method_chain :association, :cti
|
|
10
|
+
# save callbacks (necessary for saving associations)
|
|
11
|
+
after_save :cti_save_associations
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def cti_save_associations
|
|
15
|
+
self.class.cti_association_proxies.each_key do |proxy_name|
|
|
16
|
+
proxy = instance_variable_get(proxy_name)
|
|
17
|
+
proxy.save if proxy
|
|
18
|
+
end
|
|
19
|
+
true
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def association_with_cti(*args)
|
|
23
|
+
return association_without_cti(*args) unless args.length == 1
|
|
24
|
+
association_name = args[0]
|
|
25
|
+
proxy = cti_association_proxy(association_name)
|
|
26
|
+
proxy ||= self
|
|
27
|
+
proxy.association_without_cti(association_name)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def cti_association_proxy(association_name)
|
|
31
|
+
return nil if self.class.reflect_on_all_associations(:belongs_to).map(&:name).include?(association_name.to_sym)
|
|
32
|
+
proxy_name = self.class.cti_association_proxy_name(association_name)
|
|
33
|
+
proxy = instance_variable_get(proxy_name)
|
|
34
|
+
if !proxy && !self.class.cti_has_association?(association_name)
|
|
35
|
+
instance_variable_set(proxy_name,
|
|
36
|
+
ModelDelegator.new(self, self.class.cti_association_proxies[proxy_name]))
|
|
37
|
+
proxy = instance_variable_get(proxy_name)
|
|
38
|
+
end
|
|
39
|
+
proxy
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
module ClassMethods
|
|
43
|
+
|
|
44
|
+
# redefine association class methods
|
|
45
|
+
[:has_many, :has_and_belongs_to_many, :has_one].each do |name|
|
|
46
|
+
self.class_eval <<-eos, __FILE__, __LINE__+1
|
|
47
|
+
def #{name}(*args, &block)
|
|
48
|
+
cti_initialize_cti_associations
|
|
49
|
+
@cti_associations[:#{name}] << args.first
|
|
50
|
+
super
|
|
51
|
+
end
|
|
52
|
+
eos
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def cti_create_association_proxies
|
|
56
|
+
# create hash with proxy and class names. The proxies themselves will be created
|
|
57
|
+
# by the 'association' instance method when the association is used for the first time.
|
|
58
|
+
@cti_association_proxies ||= {}
|
|
59
|
+
@cti_ascendants.each do |ascendant|
|
|
60
|
+
[:has_many, :has_and_belongs_to_many, :has_one].each do |association_type|
|
|
61
|
+
ascendant.constantize.cti_associations[association_type].each do |association|
|
|
62
|
+
proxy_name = cti_association_proxy_name(association)
|
|
63
|
+
@cti_association_proxies[proxy_name] = ascendant
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# fix the 'remote' (i.e. belongs_to) part of any has_one of has_many association in this class
|
|
70
|
+
def cti_redefine_remote_associations
|
|
71
|
+
cti_initialize_cti_associations
|
|
72
|
+
# redefine remote belongs_to associations
|
|
73
|
+
[:has_many, :has_one].each do |association_type|
|
|
74
|
+
@cti_associations[association_type].each do |association|
|
|
75
|
+
next if @cti_redefined_remote_associations[association_type].include?( association )
|
|
76
|
+
if cti_reciprocal_association_present_for?( association, :belongs_to )
|
|
77
|
+
remote_association = cti_reciprocal_association_for( association, :belongs_to )
|
|
78
|
+
remote_class = cti_association_name_to_class_name( association ).constantize
|
|
79
|
+
cti_redefine_remote_belongs_to_association(remote_class, remote_association.name.to_sym)
|
|
80
|
+
@cti_redefined_remote_associations[association_type] << association
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
# redefine remote has_many and has_and_belongs_to_many associations
|
|
85
|
+
[:has_many, :has_and_belongs_to_many].each do |association_type|
|
|
86
|
+
@cti_associations[association_type].each do |association|
|
|
87
|
+
next if @cti_redefined_remote_associations[association_type].include?( association )
|
|
88
|
+
if cti_reciprocal_association_present_for?( association, association_type)
|
|
89
|
+
remote_association = cti_reciprocal_association_for( association, association_type )
|
|
90
|
+
remote_class = cti_association_name_to_class_name( association ).constantize
|
|
91
|
+
cti_redefine_remote_to_many_association(remote_class, remote_association.name.to_sym)
|
|
92
|
+
@cti_redefined_remote_associations[association_type] << association
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Gets reciprocal association of type 'type' for the given association.
|
|
99
|
+
# (example: if a has_many association has a corresponding belongs_to in the remote class).
|
|
100
|
+
# Normally, the method checks if the remote association refers to this class, but it is possible to
|
|
101
|
+
# pass in 'class_name' to check different classes
|
|
102
|
+
def cti_reciprocal_association_for(association, type, class_name = nil)
|
|
103
|
+
class_name ||= self.name
|
|
104
|
+
remote_class = cti_association_name_to_class_name( association, class_name ).constantize
|
|
105
|
+
remote_associations = remote_class.reflect_on_all_associations( type ).select { |a| a.class_name == class_name }
|
|
106
|
+
remote_associations.first
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Check if a reciprocal association of type 'type' is present for the given association.
|
|
110
|
+
# (example: check if a has_many association has a corresponding belongs_to in the remote class).
|
|
111
|
+
# Normally, the method checks if the remote association refers to this class, but it is possible to
|
|
112
|
+
# pass in 'class_name' to check different classes
|
|
113
|
+
def cti_reciprocal_association_present_for?(association, type, class_name = nil)
|
|
114
|
+
!cti_reciprocal_association_for(association, type, class_name).nil?
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# converts e.g. :space_ships to SpaceShip
|
|
118
|
+
# Normally operates on associations of this class, but it is possible to
|
|
119
|
+
# pass in 'class_name' if 'association_name' is an association on a different classes
|
|
120
|
+
def cti_association_name_to_class_name(association_name, class_name = nil)
|
|
121
|
+
klass = self
|
|
122
|
+
klass = class_name.constantize if class_name
|
|
123
|
+
klass.reflect_on_all_associations.select { |a| a.name == association_name }.first.class_name
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def cti_redefine_remote_belongs_to_association(remote_class, remote_association)
|
|
127
|
+
remote_class.class_eval <<-eos, __FILE__, __LINE__+1
|
|
128
|
+
def #{remote_association}=(object, *args, &block)
|
|
129
|
+
super( object.try(:convert_to, '#{self.name}'), *args, &block )
|
|
130
|
+
end
|
|
131
|
+
eos
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def cti_redefine_remote_to_many_association(remote_class, remote_association)
|
|
135
|
+
remote_class.class_eval <<-eos, __FILE__, __LINE__+1
|
|
136
|
+
def #{remote_association}=(objects, *args, &block)
|
|
137
|
+
super( objects.map { |o| o.try(:convert_to, '#{self.name}') }, *args, &block)
|
|
138
|
+
end
|
|
139
|
+
def #{remote_association}(*args, &block)
|
|
140
|
+
collection = super
|
|
141
|
+
DBViewCTI::Model::CollectionDelegator.new(collection, '#{self.name}')
|
|
142
|
+
end
|
|
143
|
+
eos
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def cti_association_proxy_name(association)
|
|
147
|
+
"@cti_#{association}_association_proxy"
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def cti_associations
|
|
151
|
+
cti_initialize_cti_associations
|
|
152
|
+
@cti_associations
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def cti_has_association?(association_name)
|
|
156
|
+
if !@cti_all_associations
|
|
157
|
+
@cti_all_associations = @cti_associations.keys.inject([]) do |result, key|
|
|
158
|
+
result += @cti_associations[key]
|
|
159
|
+
result
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
@cti_all_associations.include?(association_name.to_sym)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def cti_initialize_cti_associations
|
|
166
|
+
@cti_associations ||= {}
|
|
167
|
+
@cti_redefined_remote_associations ||= {}
|
|
168
|
+
[:has_many, :has_and_belongs_to_many, :has_one].each do |name|
|
|
169
|
+
@cti_associations[name] ||= []
|
|
170
|
+
@cti_redefined_remote_associations[name] ||= []
|
|
171
|
+
end
|
|
172
|
+
@cti_association_proxies ||= {}
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
module DBViewCTI
|
|
2
|
+
module Model
|
|
3
|
+
module CTI
|
|
4
|
+
module Destroy
|
|
5
|
+
extend ActiveSupport::Concern
|
|
6
|
+
|
|
7
|
+
# change destroy and delete methods to operate on most specialized object
|
|
8
|
+
included do
|
|
9
|
+
alias_method_chain :destroy, :cti
|
|
10
|
+
alias_method_chain :delete, :cti
|
|
11
|
+
# destroy! seems te be defined in Rails 4
|
|
12
|
+
alias_method_chain :destroy!, :cti if self.method_defined?(:destroy!)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def destroy_with_cti
|
|
16
|
+
specialize.destroy_without_cti
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def destroy_with_cti!
|
|
20
|
+
specialize.destroy_without_cti!
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def delete_with_cti
|
|
24
|
+
specialize.delete_without_cti
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
module DBViewCTI
|
|
2
|
+
module Model
|
|
3
|
+
module CTI
|
|
4
|
+
module Hierarchy
|
|
5
|
+
extend ActiveSupport::Concern
|
|
6
|
+
|
|
7
|
+
module ClassMethods
|
|
8
|
+
|
|
9
|
+
def cti_base_class?
|
|
10
|
+
!!@cti_base_class
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def cti_derived_class?
|
|
14
|
+
!!@cti_derived_class
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
attr_accessor :cti_descendants, :cti_ascendants, :cti_association_proxies
|
|
18
|
+
|
|
19
|
+
# registers a derived class and its descendants in the current class
|
|
20
|
+
# class_name: name of derived class (the one calling cti_register_descendants on this class)
|
|
21
|
+
# descendants: the descendants of the derived class
|
|
22
|
+
def cti_register_descendants(class_name, descendants = {})
|
|
23
|
+
@cti_descendants ||= {}
|
|
24
|
+
@cti_descendants[class_name] = descendants
|
|
25
|
+
if cti_derived_class?
|
|
26
|
+
# call up the chain. This will also cause the register_ascendants callbacks
|
|
27
|
+
self.superclass.cti_register_descendants(self.name, @cti_descendants)
|
|
28
|
+
end
|
|
29
|
+
# call back to calling class
|
|
30
|
+
@cti_ascendants ||= []
|
|
31
|
+
class_name.constantize.cti_register_ascendants(@cti_ascendants + [ self.name ])
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# registers the ascendants of the current class. Called on this class by the parent class.
|
|
35
|
+
# ascendants: array of ascendants. The first element is the highest level class, derived
|
|
36
|
+
# classes follow, the last element is the parent of this class.
|
|
37
|
+
def cti_register_ascendants(ascendants)
|
|
38
|
+
@cti_ascendants = ascendants
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# returns a list of all descendants
|
|
42
|
+
def cti_all_descendants
|
|
43
|
+
result = []
|
|
44
|
+
block = Proc.new do |klass, descendants|
|
|
45
|
+
result << klass
|
|
46
|
+
descendants.each(&block)
|
|
47
|
+
end
|
|
48
|
+
@cti_descendants ||= {}
|
|
49
|
+
@cti_descendants.each(&block)
|
|
50
|
+
result
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
module DBViewCTI
|
|
2
|
+
module Model
|
|
3
|
+
module CTI
|
|
4
|
+
module SQL
|
|
5
|
+
extend ActiveSupport::Concern
|
|
6
|
+
|
|
7
|
+
module ClassMethods
|
|
8
|
+
|
|
9
|
+
include DBViewCTI::SQLGeneration::Model
|
|
10
|
+
|
|
11
|
+
# this method is only used in testing. It returns the number of rows present in the real database
|
|
12
|
+
# table, not the number of rows present in the view (as returned by count)
|
|
13
|
+
def cti_table_count
|
|
14
|
+
result = connection.execute("SELECT COUNT(*) FROM #{DBViewCTI::Names.table_name(self)};")
|
|
15
|
+
result[0]['count'].to_i
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
module DBViewCTI
|
|
2
|
+
module Model
|
|
3
|
+
module CTI
|
|
4
|
+
module TypeConversion
|
|
5
|
+
|
|
6
|
+
def specialize
|
|
7
|
+
class_name, id = type(true)
|
|
8
|
+
return self if class_name == self.class.name
|
|
9
|
+
class_name.constantize.find(id)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# Return the 'true' (i.e. most specialized) classname of this object
|
|
13
|
+
# When return_id is true, the 'specialized' database id is also returned
|
|
14
|
+
def type(return_id = false)
|
|
15
|
+
query, levels = self.class.cti_outer_join_sql(id)
|
|
16
|
+
result = self.class.connection.execute(query).first
|
|
17
|
+
# replace returned ids with the levels corresponding to their classes
|
|
18
|
+
result_levels = result.inject({}) do |hash, (k,v)|
|
|
19
|
+
hash[k] = levels[k] unless v.nil?
|
|
20
|
+
hash
|
|
21
|
+
end
|
|
22
|
+
# find class with maximum level value
|
|
23
|
+
foreign_key = result_levels.max_by { |k,v| v }.first
|
|
24
|
+
class_name = DBViewCTI::Names.table_to_class_name(foreign_key[0..-4])
|
|
25
|
+
if return_id
|
|
26
|
+
id_ = result[foreign_key].to_i
|
|
27
|
+
[class_name, id_]
|
|
28
|
+
else
|
|
29
|
+
class_name
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def convert_to(type)
|
|
34
|
+
return nil unless persisted?
|
|
35
|
+
type_string = type.to_s
|
|
36
|
+
type_string = type_string.camelize if type.is_a?(Symbol)
|
|
37
|
+
return self if type_string == self.class.name
|
|
38
|
+
query = self.class.cti_inner_join_sql(id, type_string)
|
|
39
|
+
# query is nil when we try to cenvert to a descendant class (instead of an ascendant),
|
|
40
|
+
# or when we try to convert to a class outside of the hierarchy
|
|
41
|
+
if query.nil?
|
|
42
|
+
specialized = specialize
|
|
43
|
+
return nil if specialized == self
|
|
44
|
+
return specialized.convert_to(type_string)
|
|
45
|
+
end
|
|
46
|
+
result = self.class.connection.execute(query).first
|
|
47
|
+
id = result[ DBViewCTI::Names.foreign_key(type.to_s) ]
|
|
48
|
+
return nil if id.nil?
|
|
49
|
+
type_string.constantize.find(id.to_i)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -3,328 +3,12 @@ module DBViewCTI
|
|
|
3
3
|
module CTI
|
|
4
4
|
extend ActiveSupport::Concern
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
# Return the 'true' (i.e. most specialized) classname of this object
|
|
13
|
-
# When return_id is true, the 'specialized' database id is also returned
|
|
14
|
-
def type(return_id = false)
|
|
15
|
-
query, levels = self.class.cti_outer_join_sql(id)
|
|
16
|
-
result = self.class.connection.execute(query).first
|
|
17
|
-
# replace returned ids with the levels corresponding to their classes
|
|
18
|
-
result_levels = result.inject({}) do |hash, (k,v)|
|
|
19
|
-
hash[k] = levels[k] unless v.nil?
|
|
20
|
-
hash
|
|
21
|
-
end
|
|
22
|
-
# find class with maximum level value
|
|
23
|
-
foreign_key = result_levels.max_by { |k,v| v }.first
|
|
24
|
-
class_name = DBViewCTI::Names.table_to_class_name(foreign_key[0..-4])
|
|
25
|
-
if return_id
|
|
26
|
-
id_ = result[foreign_key].to_i
|
|
27
|
-
[class_name, id_]
|
|
28
|
-
else
|
|
29
|
-
class_name
|
|
30
|
-
end
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
def convert_to(type)
|
|
34
|
-
return nil unless persisted?
|
|
35
|
-
type_string = type.to_s
|
|
36
|
-
type_string = type_string.camelize if type.is_a?(Symbol)
|
|
37
|
-
return self if type_string == self.class.name
|
|
38
|
-
query = self.class.cti_inner_join_sql(id, type_string)
|
|
39
|
-
# query is nil when we try to cenvert to a descendant class (instead of an ascendant),
|
|
40
|
-
# or when we try to convert to a class outside of the hierarchy
|
|
41
|
-
if query.nil?
|
|
42
|
-
specialized = specialize
|
|
43
|
-
return nil if specialized == self
|
|
44
|
-
return specialized.convert_to(type_string)
|
|
45
|
-
end
|
|
46
|
-
result = self.class.connection.execute(query).first
|
|
47
|
-
id = result[ DBViewCTI::Names.foreign_key(type.to_s) ]
|
|
48
|
-
return nil if id.nil?
|
|
49
|
-
type_string.constantize.find(id.to_i)
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
# change destroy and delete methods to operate on most specialized object
|
|
53
|
-
included do
|
|
54
|
-
alias_method_chain :destroy, :cti
|
|
55
|
-
alias_method_chain :delete, :cti
|
|
56
|
-
# destroy! seems te be defined in Rails 4
|
|
57
|
-
alias_method_chain :destroy!, :cti if self.method_defined?(:destroy!)
|
|
58
|
-
|
|
59
|
-
# for associations:
|
|
60
|
-
alias_method_chain :association, :cti
|
|
61
|
-
# save callbacks (necessary for saving associations)
|
|
62
|
-
after_save :cti_save_associations
|
|
63
|
-
|
|
64
|
-
# validations
|
|
65
|
-
validate :cti_validate_associations, :cti_no_disable => true
|
|
66
|
-
attr_accessor :cti_disable_validations
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
def destroy_with_cti
|
|
70
|
-
specialize.destroy_without_cti
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
def destroy_with_cti!
|
|
74
|
-
specialize.destroy_without_cti!
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
def delete_with_cti
|
|
78
|
-
specialize.delete_without_cti
|
|
79
|
-
end
|
|
80
|
-
|
|
81
|
-
def cti_validate_associations
|
|
82
|
-
return_value = true
|
|
83
|
-
self.class.cti_association_proxies.each_key do |proxy_name|
|
|
84
|
-
proxy = instance_variable_get(proxy_name)
|
|
85
|
-
if proxy && !proxy.valid?
|
|
86
|
-
errors.messages.merge!(proxy.errors.messages)
|
|
87
|
-
return_value = false
|
|
88
|
-
end
|
|
89
|
-
end
|
|
90
|
-
return_value
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
def cti_save_associations
|
|
94
|
-
self.class.cti_association_proxies.each_key do |proxy_name|
|
|
95
|
-
proxy = instance_variable_get(proxy_name)
|
|
96
|
-
proxy.save if proxy
|
|
97
|
-
end
|
|
98
|
-
true
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
def association_with_cti(*args)
|
|
102
|
-
return association_without_cti(*args) unless args.length == 1
|
|
103
|
-
association_name = args[0]
|
|
104
|
-
proxy = cti_association_proxy(association_name)
|
|
105
|
-
proxy ||= self
|
|
106
|
-
proxy.association_without_cti(association_name)
|
|
107
|
-
end
|
|
108
|
-
|
|
109
|
-
def cti_association_proxy(association_name)
|
|
110
|
-
return nil if self.class.reflect_on_all_associations(:belongs_to).map(&:name).include?(association_name.to_sym)
|
|
111
|
-
proxy_name = self.class.cti_association_proxy_name(association_name)
|
|
112
|
-
proxy = instance_variable_get(proxy_name)
|
|
113
|
-
if !proxy && !self.class.cti_has_association?(association_name)
|
|
114
|
-
instance_variable_set(proxy_name,
|
|
115
|
-
ModelDelegator.new(self, self.class.cti_association_proxies[proxy_name]))
|
|
116
|
-
proxy = instance_variable_get(proxy_name)
|
|
117
|
-
end
|
|
118
|
-
proxy
|
|
119
|
-
end
|
|
120
|
-
|
|
121
|
-
module ClassMethods
|
|
122
|
-
|
|
123
|
-
def cti_base_class?
|
|
124
|
-
!!@cti_base_class
|
|
125
|
-
end
|
|
126
|
-
|
|
127
|
-
def cti_derived_class?
|
|
128
|
-
!!@cti_derived_class
|
|
129
|
-
end
|
|
130
|
-
|
|
131
|
-
attr_accessor :cti_descendants, :cti_ascendants, :cti_association_proxies
|
|
132
|
-
|
|
133
|
-
# registers a derived class and its descendants in the current class
|
|
134
|
-
# class_name: name of derived class (the one calling cti_register_descendants on this class)
|
|
135
|
-
# descendants: the descendants of the derived class
|
|
136
|
-
def cti_register_descendants(class_name, descendants = {})
|
|
137
|
-
@cti_descendants ||= {}
|
|
138
|
-
@cti_descendants[class_name] = descendants
|
|
139
|
-
if cti_derived_class?
|
|
140
|
-
# call up the chain. This will also cause the register_ascendants callbacks
|
|
141
|
-
self.superclass.cti_register_descendants(self.name, @cti_descendants)
|
|
142
|
-
end
|
|
143
|
-
# call back to calling class
|
|
144
|
-
@cti_ascendants ||= []
|
|
145
|
-
class_name.constantize.cti_register_ascendants(@cti_ascendants + [ self.name ])
|
|
146
|
-
end
|
|
147
|
-
|
|
148
|
-
# registers the ascendants of the current class. Called on this class by the parent class.
|
|
149
|
-
# ascendants: array of ascendants. The first element is the highest level class, derived
|
|
150
|
-
# classes follow, the last element is the parent of this class.
|
|
151
|
-
def cti_register_ascendants(ascendants)
|
|
152
|
-
@cti_ascendants = ascendants
|
|
153
|
-
end
|
|
154
|
-
|
|
155
|
-
# returns a list of all descendants
|
|
156
|
-
def cti_all_descendants
|
|
157
|
-
result = []
|
|
158
|
-
block = Proc.new do |klass, descendants|
|
|
159
|
-
result << klass
|
|
160
|
-
descendants.each(&block)
|
|
161
|
-
end
|
|
162
|
-
@cti_descendants ||= {}
|
|
163
|
-
@cti_descendants.each(&block)
|
|
164
|
-
result
|
|
165
|
-
end
|
|
166
|
-
|
|
167
|
-
# redefine validate to always add :unless proc so we can disable the validations for an object
|
|
168
|
-
# by setting the cti_disable_validations accessor to true
|
|
169
|
-
def validate(*args, &block)
|
|
170
|
-
# we specifically don't want to disable balidations belonging to associations. Based on the naming
|
|
171
|
-
# rails uses, we return immediately in such cases (there must be a cleaner way to do this...)
|
|
172
|
-
return super if args.first && args.first.to_s =~ /^validate_associated_records_for_/
|
|
173
|
-
# rest of implementation insipred by the validate implementation in rails
|
|
174
|
-
options = args.extract_options!.dup
|
|
175
|
-
return super if options[:cti_no_disable]
|
|
176
|
-
if options.key?(:unless)
|
|
177
|
-
options[:unless] = Array(options[:unless])
|
|
178
|
-
options[:unless].unshift( cti_validation_unless_proc )
|
|
179
|
-
else
|
|
180
|
-
options[:unless] = cti_validation_unless_proc
|
|
181
|
-
end
|
|
182
|
-
args << options
|
|
183
|
-
return super(*args, &block)
|
|
184
|
-
end
|
|
185
|
-
|
|
186
|
-
def cti_validation_unless_proc
|
|
187
|
-
@cti_validation_unless_proc ||= Proc.new do |object|
|
|
188
|
-
object.respond_to?(:cti_disable_validations) && object.cti_disable_validations
|
|
189
|
-
end
|
|
190
|
-
end
|
|
191
|
-
|
|
192
|
-
# redefine association class methods
|
|
193
|
-
[:has_many, :has_and_belongs_to_many, :has_one].each do |name|
|
|
194
|
-
self.class_eval <<-eos, __FILE__, __LINE__+1
|
|
195
|
-
def #{name}(*args, &block)
|
|
196
|
-
cti_initialize_cti_associations
|
|
197
|
-
@cti_associations[:#{name}] << args.first
|
|
198
|
-
super
|
|
199
|
-
end
|
|
200
|
-
eos
|
|
201
|
-
end
|
|
202
|
-
|
|
203
|
-
def cti_create_association_proxies
|
|
204
|
-
# create hash with proxy and class names. The proxies themselves will be created
|
|
205
|
-
# by the 'association' instance method when the association is used for the first time.
|
|
206
|
-
@cti_association_proxies ||= {}
|
|
207
|
-
@cti_ascendants.each do |ascendant|
|
|
208
|
-
[:has_many, :has_and_belongs_to_many, :has_one].each do |association_type|
|
|
209
|
-
ascendant.constantize.cti_associations[association_type].each do |association|
|
|
210
|
-
proxy_name = cti_association_proxy_name(association)
|
|
211
|
-
@cti_association_proxies[proxy_name] = ascendant
|
|
212
|
-
end
|
|
213
|
-
end
|
|
214
|
-
end
|
|
215
|
-
end
|
|
216
|
-
|
|
217
|
-
# fix the 'remote' (i.e. belongs_to) part of any has_one of has_many association in this class
|
|
218
|
-
def cti_redefine_remote_associations
|
|
219
|
-
cti_initialize_cti_associations
|
|
220
|
-
# redefine remote belongs_to associations
|
|
221
|
-
[:has_many, :has_one].each do |association_type|
|
|
222
|
-
@cti_associations[association_type].each do |association|
|
|
223
|
-
next if @cti_redefined_remote_associations[association_type].include?( association )
|
|
224
|
-
if cti_reciprocal_association_present_for?( association, :belongs_to )
|
|
225
|
-
remote_class = cti_association_name_to_class_name( association ).constantize
|
|
226
|
-
cti_redefine_remote_belongs_to_association(remote_class, cti_class_name_to_association_name.to_sym)
|
|
227
|
-
@cti_redefined_remote_associations[association_type] << association
|
|
228
|
-
end
|
|
229
|
-
end
|
|
230
|
-
end
|
|
231
|
-
# redefine remote has_many and has_and_belongs_to_many associations
|
|
232
|
-
[:has_many, :has_and_belongs_to_many].each do |association_type|
|
|
233
|
-
@cti_associations[association_type].each do |association|
|
|
234
|
-
next if @cti_redefined_remote_associations[association_type].include?( association )
|
|
235
|
-
if cti_reciprocal_association_present_for?( association, association_type, true )
|
|
236
|
-
remote_class = cti_association_name_to_class_name( association ).constantize
|
|
237
|
-
cti_redefine_remote_to_many_association(remote_class, cti_class_name_to_association_name( true ).to_sym)
|
|
238
|
-
@cti_redefined_remote_associations[association_type] << association
|
|
239
|
-
end
|
|
240
|
-
end
|
|
241
|
-
end
|
|
242
|
-
end
|
|
243
|
-
|
|
244
|
-
# Check if a reciprocal association of type 'type' is present for the given association.
|
|
245
|
-
# (example: check if a has_many association has a corresponding belongs_to in the remote class).
|
|
246
|
-
# Plural indicates wether the remote association we're looking for is plural (i.e. has_many :space_ships)
|
|
247
|
-
# or singular (i.e. belongs_to :space_ship).
|
|
248
|
-
# Normally, the method checks if the remote association refers to this class, but it is possible to
|
|
249
|
-
# pass in 'class_name' to check different classes
|
|
250
|
-
def cti_reciprocal_association_present_for?(association, type, plural = false, class_name = nil)
|
|
251
|
-
remote_class = cti_association_name_to_class_name( association ).constantize
|
|
252
|
-
remote_associations = remote_class.reflect_on_all_associations( type ).map(&:name)
|
|
253
|
-
remote_associations.include?( cti_class_name_to_association_name(plural, class_name).to_sym )
|
|
254
|
-
end
|
|
255
|
-
|
|
256
|
-
# converts e.g. SpaceShip to :space_ship (for plural == false), or :space_ships (for plural true)
|
|
257
|
-
def cti_class_name_to_association_name(plural = false, class_name = nil)
|
|
258
|
-
class_name ||= self.name
|
|
259
|
-
association_name = class_name.underscore
|
|
260
|
-
association_name = association_name.pluralize if plural
|
|
261
|
-
association_name
|
|
262
|
-
end
|
|
263
|
-
|
|
264
|
-
# converts e.g. :space_ships to SpaceShip
|
|
265
|
-
def cti_association_name_to_class_name(association_name)
|
|
266
|
-
association_name.to_s.camelize.singularize
|
|
267
|
-
end
|
|
268
|
-
|
|
269
|
-
def cti_redefine_remote_belongs_to_association(remote_class, remote_association)
|
|
270
|
-
remote_class.class_eval <<-eos, __FILE__, __LINE__+1
|
|
271
|
-
def #{remote_association}=(object, *args, &block)
|
|
272
|
-
super( object.try(:convert_to, '#{self.name}'), *args, &block )
|
|
273
|
-
end
|
|
274
|
-
eos
|
|
275
|
-
end
|
|
276
|
-
|
|
277
|
-
def cti_redefine_remote_to_many_association(remote_class, remote_association)
|
|
278
|
-
remote_class.class_eval <<-eos, __FILE__, __LINE__+1
|
|
279
|
-
def #{remote_association}=(objects, *args, &block)
|
|
280
|
-
super( objects.map { |o| o.try(:convert_to, '#{self.name}') }, *args, &block)
|
|
281
|
-
end
|
|
282
|
-
def #{remote_association}(*args, &block)
|
|
283
|
-
collection = super
|
|
284
|
-
DBViewCTI::Model::CollectionDelegator.new(collection, '#{self.name}')
|
|
285
|
-
end
|
|
286
|
-
eos
|
|
287
|
-
end
|
|
288
|
-
|
|
289
|
-
def cti_association_proxy_name(association)
|
|
290
|
-
"@cti_#{association}_association_proxy"
|
|
291
|
-
end
|
|
292
|
-
|
|
293
|
-
def cti_associations
|
|
294
|
-
cti_initialize_cti_associations
|
|
295
|
-
@cti_associations
|
|
296
|
-
end
|
|
297
|
-
|
|
298
|
-
def cti_has_association?(association_name)
|
|
299
|
-
if !@cti_all_associations
|
|
300
|
-
@cti_all_associations = @cti_associations.keys.inject([]) do |result, key|
|
|
301
|
-
result += @cti_associations[key]
|
|
302
|
-
result
|
|
303
|
-
end
|
|
304
|
-
end
|
|
305
|
-
@cti_all_associations.include?(association_name.to_sym)
|
|
306
|
-
end
|
|
307
|
-
|
|
308
|
-
include DBViewCTI::SQLGeneration::Model
|
|
309
|
-
|
|
310
|
-
# this method is only used in testing. It returns the number of rows present in the real database
|
|
311
|
-
# table, not the number of rows present in the view (as returned by count)
|
|
312
|
-
def cti_table_count
|
|
313
|
-
result = connection.execute("SELECT COUNT(*) FROM #{DBViewCTI::Names.table_name(self)};")
|
|
314
|
-
result[0]['count'].to_i
|
|
315
|
-
end
|
|
316
|
-
|
|
317
|
-
def cti_initialize_cti_associations
|
|
318
|
-
@cti_associations ||= {}
|
|
319
|
-
@cti_redefined_remote_associations ||= {}
|
|
320
|
-
[:has_many, :has_and_belongs_to_many, :has_one].each do |name|
|
|
321
|
-
@cti_associations[name] ||= []
|
|
322
|
-
@cti_redefined_remote_associations[name] ||= []
|
|
323
|
-
end
|
|
324
|
-
@cti_association_proxies ||= {}
|
|
325
|
-
end
|
|
326
|
-
|
|
327
|
-
end
|
|
6
|
+
include Hierarchy
|
|
7
|
+
include TypeConversion
|
|
8
|
+
include Destroy
|
|
9
|
+
include SQL
|
|
10
|
+
include Associations
|
|
11
|
+
include AssociationValidations
|
|
328
12
|
end
|
|
329
13
|
end
|
|
330
14
|
end
|
data/lib/db_view_cti/version.rb
CHANGED
data/lib/dbview_cti.rb
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
ActiveSupport::Inflector.inflections do |inflect|
|
|
2
|
+
inflect.acronym('CTI')
|
|
3
|
+
inflect.acronym('CTIs')
|
|
4
|
+
end
|
|
5
|
+
|
|
1
6
|
module DBViewCTI
|
|
2
7
|
extend ActiveSupport::Autoload
|
|
3
8
|
autoload :Names
|
|
@@ -30,6 +35,12 @@ module DBViewCTI
|
|
|
30
35
|
autoload :Extensions
|
|
31
36
|
autoload :ModelDelegator
|
|
32
37
|
autoload :CollectionDelegator
|
|
38
|
+
autoload :TypeConversion, 'db_view_cti/model/cti/type_conversion'
|
|
39
|
+
autoload :Hierarchy, 'db_view_cti/model/cti/hierarchy'
|
|
40
|
+
autoload :Destroy, 'db_view_cti/model/cti/destroy'
|
|
41
|
+
autoload :SQL, 'db_view_cti/model/cti/sql'
|
|
42
|
+
autoload :Associations, 'db_view_cti/model/cti/associations'
|
|
43
|
+
autoload :AssociationValidations, 'db_view_cti/model/cti/association_validations'
|
|
33
44
|
end
|
|
34
45
|
end
|
|
35
46
|
|
|
@@ -16,4 +16,6 @@ class SpaceShip < Vehicle
|
|
|
16
16
|
has_many :experiment_space_ship_performances
|
|
17
17
|
has_many :experiments, :through => :experiment_space_ship_performances
|
|
18
18
|
accepts_nested_attributes_for :experiments
|
|
19
|
+
|
|
20
|
+
has_many :upgraded_to, :class_name => 'SpaceShuttle', :foreign_key => 'upgraded_from_id'
|
|
19
21
|
end
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
class SpaceShuttle < SpaceShip
|
|
2
2
|
attr_accessible :single_use unless Rails::VERSION::MAJOR > 3
|
|
3
|
+
|
|
4
|
+
belongs_to :upgraded_from, :class_name => 'SpaceShip'
|
|
5
|
+
|
|
6
|
+
# cti_derived_class has to come after te above belongs_to, otherwise the association will not work correctly.
|
|
7
|
+
# This is only because SpaceShuttle is a leaf class (i.e. had no descendants).
|
|
3
8
|
cti_derived_class
|
|
4
9
|
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
class AddUpgradedFromToSpaceShuttles < ActiveRecord::Migration
|
|
2
|
+
def up
|
|
3
|
+
cti_recreate_views_after_change_to('SpaceShuttle') do
|
|
4
|
+
add_column(:space_shuttles, :upgraded_from_id, :integer)
|
|
5
|
+
end
|
|
6
|
+
add_foreign_key :space_shuttles, :space_ships, :column => 'upgraded_from_id'
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def down
|
|
10
|
+
cti_recreate_views_after_change_to('SpaceShuttle') do
|
|
11
|
+
remove_column(:space_shuttles, :upgraded_from_id)
|
|
12
|
+
end
|
|
13
|
+
remove_foreign_key :space_shuttles, :space_ships, :column => 'upgraded_from_id'
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
class AddFieldToBaseClass < ActiveRecord::Migration
|
|
2
|
+
def up
|
|
3
|
+
# this makes cti_recreate_views_after_change_to also works for the base class
|
|
4
|
+
cti_recreate_views_after_change_to('Vehicle') do
|
|
5
|
+
add_column(:vehicles, :bogus_field, :string)
|
|
6
|
+
end
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def down
|
|
10
|
+
cti_recreate_views_after_change_to('Vehicle') do
|
|
11
|
+
remove_column(:vehicles, :bogus_field)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -11,7 +11,10 @@
|
|
|
11
11
|
#
|
|
12
12
|
# It's strongly recommended that you check this file into your version control system.
|
|
13
13
|
|
|
14
|
-
ActiveRecord::Schema.define(version:
|
|
14
|
+
ActiveRecord::Schema.define(version: 20140425182847) do
|
|
15
|
+
|
|
16
|
+
# These are extensions that must be enabled in order to support this database
|
|
17
|
+
enable_extension "plpgsql"
|
|
15
18
|
|
|
16
19
|
create_table "astronauts", force: true do |t|
|
|
17
20
|
t.string "name"
|
|
@@ -39,6 +42,26 @@ ActiveRecord::Schema.define(version: 20131022020655) do
|
|
|
39
42
|
t.datetime "updated_at"
|
|
40
43
|
end
|
|
41
44
|
|
|
45
|
+
create_table "categories", force: true do |t|
|
|
46
|
+
t.string "name"
|
|
47
|
+
t.datetime "created_at"
|
|
48
|
+
t.datetime "updated_at"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
create_table "experiment_space_ship_performances", force: true do |t|
|
|
52
|
+
t.integer "experiment_id"
|
|
53
|
+
t.integer "space_ship_id"
|
|
54
|
+
t.date "performed_at"
|
|
55
|
+
t.datetime "created_at"
|
|
56
|
+
t.datetime "updated_at"
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
create_table "experiments", force: true do |t|
|
|
60
|
+
t.string "name"
|
|
61
|
+
t.datetime "created_at"
|
|
62
|
+
t.datetime "updated_at"
|
|
63
|
+
end
|
|
64
|
+
|
|
42
65
|
create_table "launches", force: true do |t|
|
|
43
66
|
t.integer "space_ship_id"
|
|
44
67
|
t.date "date"
|
|
@@ -70,6 +93,7 @@ ActiveRecord::Schema.define(version: 20131022020655) do
|
|
|
70
93
|
|
|
71
94
|
create_table "space_ships", force: true do |t|
|
|
72
95
|
t.integer "vehicle_id"
|
|
96
|
+
t.integer "category_id"
|
|
73
97
|
t.boolean "single_use"
|
|
74
98
|
t.datetime "created_at"
|
|
75
99
|
t.datetime "updated_at"
|
|
@@ -81,6 +105,7 @@ ActiveRecord::Schema.define(version: 20131022020655) do
|
|
|
81
105
|
t.integer "power"
|
|
82
106
|
t.datetime "created_at"
|
|
83
107
|
t.datetime "updated_at"
|
|
108
|
+
t.integer "upgraded_from_id"
|
|
84
109
|
end
|
|
85
110
|
|
|
86
111
|
create_table "vehicles", force: true do |t|
|
|
@@ -88,6 +113,7 @@ ActiveRecord::Schema.define(version: 20131022020655) do
|
|
|
88
113
|
t.integer "mass"
|
|
89
114
|
t.datetime "created_at"
|
|
90
115
|
t.datetime "updated_at"
|
|
116
|
+
t.string "bogus_field"
|
|
91
117
|
end
|
|
92
118
|
|
|
93
119
|
cti_create_view('MotorVehicle')
|
|
@@ -101,12 +127,17 @@ ActiveRecord::Schema.define(version: 20131022020655) do
|
|
|
101
127
|
|
|
102
128
|
add_foreign_key "captains", "space_ships", :name => "captains_space_ship_id_fk"
|
|
103
129
|
|
|
130
|
+
add_foreign_key "experiment_space_ship_performances", "experiments", :name => "experiment_space_ship_performances_experiment_id_fk"
|
|
131
|
+
add_foreign_key "experiment_space_ship_performances", "space_ships", :name => "experiment_space_ship_performances_space_ship_id_fk"
|
|
132
|
+
|
|
104
133
|
add_foreign_key "launches", "space_ships", :name => "launches_space_ship_id_fk"
|
|
105
134
|
|
|
106
135
|
add_foreign_key "rocket_engines", "space_ships", :name => "rocket_engines_space_ship_id_fk"
|
|
107
136
|
|
|
137
|
+
add_foreign_key "space_ships", "categories", :name => "space_ships_category_id_fk"
|
|
108
138
|
add_foreign_key "space_ships", "vehicles", :name => "space_ships_vehicle_id_fk"
|
|
109
139
|
|
|
110
140
|
add_foreign_key "space_shuttles", "space_ships", :name => "space_shuttles_space_ship_id_fk"
|
|
141
|
+
add_foreign_key "space_shuttles", "space_ships", :name => "space_shuttles_upgraded_from_id_fk", :column => "upgraded_from_id"
|
|
111
142
|
|
|
112
143
|
end
|
data/spec/models/car_spec.rb
CHANGED
|
@@ -18,11 +18,13 @@ describe Car do
|
|
|
18
18
|
@car.name = 'Porsche'
|
|
19
19
|
@car.fuel = 'gasoline'
|
|
20
20
|
@car.convertible = true
|
|
21
|
+
@car.bogus_field = 'bogus'
|
|
21
22
|
@car.save!
|
|
22
23
|
car = Car.find(id)
|
|
23
24
|
car.name.should eq 'Porsche'
|
|
24
25
|
car.mass.should eq 1000
|
|
25
26
|
car.fuel.should eq 'gasoline'
|
|
27
|
+
car.bogus_field.should eq 'bogus'
|
|
26
28
|
car.stick_shift.should be_true
|
|
27
29
|
car.convertible.should be_true
|
|
28
30
|
end
|
|
@@ -441,6 +441,21 @@ describe SpaceShuttle do
|
|
|
441
441
|
}.to change(SpaceShuttle, :count).by(1)
|
|
442
442
|
end
|
|
443
443
|
|
|
444
|
+
it "association logic also works for associations with non-standard names" do
|
|
445
|
+
# check has_many side
|
|
446
|
+
shuttle2 = SpaceShuttle.create(:name => 'Endeavour', :reliability => 100)
|
|
447
|
+
@shuttle.upgraded_to << shuttle2
|
|
448
|
+
@shuttle.save!
|
|
449
|
+
shuttle2.reload
|
|
450
|
+
shuttle2.upgraded_from.specialize.id.should eq @shuttle.id
|
|
451
|
+
# check belongs_to side
|
|
452
|
+
shuttle3 = SpaceShuttle.create(:name => 'Endeavour', :reliability => 100)
|
|
453
|
+
@shuttle.upgraded_from = shuttle3
|
|
454
|
+
@shuttle.save!
|
|
455
|
+
@shuttle.reload
|
|
456
|
+
@shuttle.upgraded_from.specialize.id.should eq shuttle3.id
|
|
457
|
+
end
|
|
458
|
+
|
|
444
459
|
end
|
|
445
460
|
|
|
446
461
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: dbview_cti
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.5
|
|
5
5
|
prerelease:
|
|
6
6
|
platform: ruby
|
|
7
7
|
authors:
|
|
@@ -9,7 +9,7 @@ authors:
|
|
|
9
9
|
autorequire:
|
|
10
10
|
bindir: bin
|
|
11
11
|
cert_chain: []
|
|
12
|
-
date: 2014-04-
|
|
12
|
+
date: 2014-04-25 00:00:00.000000000 Z
|
|
13
13
|
dependencies:
|
|
14
14
|
- !ruby/object:Gem::Dependency
|
|
15
15
|
name: rails
|
|
@@ -80,6 +80,12 @@ files:
|
|
|
80
80
|
- lib/db_view_cti/migration/command_recorder.rb
|
|
81
81
|
- lib/db_view_cti/model/collection_delegator.rb
|
|
82
82
|
- lib/db_view_cti/model/cti.rb
|
|
83
|
+
- lib/db_view_cti/model/cti/association_validations.rb
|
|
84
|
+
- lib/db_view_cti/model/cti/associations.rb
|
|
85
|
+
- lib/db_view_cti/model/cti/destroy.rb
|
|
86
|
+
- lib/db_view_cti/model/cti/hierarchy.rb
|
|
87
|
+
- lib/db_view_cti/model/cti/sql.rb
|
|
88
|
+
- lib/db_view_cti/model/cti/type_conversion.rb
|
|
83
89
|
- lib/db_view_cti/model/extensions.rb
|
|
84
90
|
- lib/db_view_cti/model/model_delegator.rb
|
|
85
91
|
- lib/db_view_cti/names.rb
|
|
@@ -149,6 +155,8 @@ files:
|
|
|
149
155
|
- spec/dummy-rails-3/db/migrate/20131022030659_create_experiments.rb
|
|
150
156
|
- spec/dummy-rails-3/db/migrate/20131022030720_create_experiment_space_ship_performances.rb
|
|
151
157
|
- spec/dummy-rails-3/db/migrate/20140408013710_check_view_exists.rb
|
|
158
|
+
- spec/dummy-rails-3/db/migrate/20140411001620_add_upgraded_from_to_space_shuttles.rb
|
|
159
|
+
- spec/dummy-rails-3/db/migrate/20140425182847_add_field_to_base_class.rb
|
|
152
160
|
- spec/dummy-rails-3/db/schema.rb
|
|
153
161
|
- spec/dummy-rails-3/lib/assets/.gitkeep
|
|
154
162
|
- spec/dummy-rails-3/log/.gitkeep
|
|
@@ -215,7 +223,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
215
223
|
version: '0'
|
|
216
224
|
segments:
|
|
217
225
|
- 0
|
|
218
|
-
hash:
|
|
226
|
+
hash: 1166807647709519992
|
|
219
227
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
220
228
|
none: false
|
|
221
229
|
requirements:
|
|
@@ -224,7 +232,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
224
232
|
version: '0'
|
|
225
233
|
segments:
|
|
226
234
|
- 0
|
|
227
|
-
hash:
|
|
235
|
+
hash: 1166807647709519992
|
|
228
236
|
requirements: []
|
|
229
237
|
rubyforge_project:
|
|
230
238
|
rubygems_version: 1.8.25
|