bigrecord 0.0.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (104) hide show
  1. data/MIT-LICENSE +20 -0
  2. data/README.rdoc +44 -0
  3. data/Rakefile +17 -0
  4. data/VERSION +1 -0
  5. data/doc/bigrecord_specs.rdoc +36 -0
  6. data/doc/getting_started.rdoc +157 -0
  7. data/examples/bigrecord.yml +25 -0
  8. data/generators/bigrecord/bigrecord_generator.rb +17 -0
  9. data/generators/bigrecord/templates/bigrecord.rake +47 -0
  10. data/generators/bigrecord_migration/bigrecord_migration_generator.rb +13 -0
  11. data/generators/bigrecord_migration/templates/migration.rb +9 -0
  12. data/generators/bigrecord_model/bigrecord_model_generator.rb +28 -0
  13. data/generators/bigrecord_model/templates/migration.rb +13 -0
  14. data/generators/bigrecord_model/templates/model.rb +7 -0
  15. data/generators/bigrecord_model/templates/model_spec.rb +12 -0
  16. data/init.rb +9 -0
  17. data/install.rb +22 -0
  18. data/lib/big_record/abstract_base.rb +1088 -0
  19. data/lib/big_record/action_view_extensions.rb +266 -0
  20. data/lib/big_record/ar_associations/association_collection.rb +194 -0
  21. data/lib/big_record/ar_associations/association_proxy.rb +158 -0
  22. data/lib/big_record/ar_associations/belongs_to_association.rb +57 -0
  23. data/lib/big_record/ar_associations/belongs_to_many_association.rb +57 -0
  24. data/lib/big_record/ar_associations/has_and_belongs_to_many_association.rb +164 -0
  25. data/lib/big_record/ar_associations/has_many_association.rb +191 -0
  26. data/lib/big_record/ar_associations/has_one_association.rb +80 -0
  27. data/lib/big_record/ar_associations.rb +1608 -0
  28. data/lib/big_record/ar_reflection.rb +223 -0
  29. data/lib/big_record/attribute_methods.rb +75 -0
  30. data/lib/big_record/base.rb +618 -0
  31. data/lib/big_record/br_associations/association_collection.rb +194 -0
  32. data/lib/big_record/br_associations/association_proxy.rb +153 -0
  33. data/lib/big_record/br_associations/belongs_to_association.rb +52 -0
  34. data/lib/big_record/br_associations/belongs_to_many_association.rb +293 -0
  35. data/lib/big_record/br_associations/cached_item_proxy.rb +194 -0
  36. data/lib/big_record/br_associations/cached_item_proxy_factory.rb +62 -0
  37. data/lib/big_record/br_associations/has_and_belongs_to_many_association.rb +168 -0
  38. data/lib/big_record/br_associations/has_one_association.rb +80 -0
  39. data/lib/big_record/br_associations.rb +978 -0
  40. data/lib/big_record/br_reflection.rb +151 -0
  41. data/lib/big_record/callbacks.rb +367 -0
  42. data/lib/big_record/connection_adapters/abstract/connection_specification.rb +279 -0
  43. data/lib/big_record/connection_adapters/abstract/database_statements.rb +175 -0
  44. data/lib/big_record/connection_adapters/abstract/quoting.rb +58 -0
  45. data/lib/big_record/connection_adapters/abstract_adapter.rb +190 -0
  46. data/lib/big_record/connection_adapters/column.rb +491 -0
  47. data/lib/big_record/connection_adapters/hbase_adapter.rb +432 -0
  48. data/lib/big_record/connection_adapters/view.rb +27 -0
  49. data/lib/big_record/connection_adapters.rb +10 -0
  50. data/lib/big_record/deletion.rb +73 -0
  51. data/lib/big_record/dynamic_schema.rb +92 -0
  52. data/lib/big_record/embedded.rb +71 -0
  53. data/lib/big_record/embedded_associations/association_proxy.rb +148 -0
  54. data/lib/big_record/family_span_columns.rb +89 -0
  55. data/lib/big_record/fixtures.rb +1025 -0
  56. data/lib/big_record/migration.rb +380 -0
  57. data/lib/big_record/routing_ext.rb +65 -0
  58. data/lib/big_record/timestamp.rb +51 -0
  59. data/lib/big_record/validations.rb +830 -0
  60. data/lib/big_record.rb +125 -0
  61. data/lib/bigrecord.rb +1 -0
  62. data/rails/init.rb +9 -0
  63. data/spec/connections/bigrecord.yml +13 -0
  64. data/spec/connections/cassandra/connection.rb +2 -0
  65. data/spec/connections/hbase/connection.rb +2 -0
  66. data/spec/debug.log +281 -0
  67. data/spec/integration/br_associations_spec.rb +80 -0
  68. data/spec/lib/animal.rb +12 -0
  69. data/spec/lib/book.rb +10 -0
  70. data/spec/lib/broken_migrations/duplicate_name/20090706182535_add_animals_table.rb +14 -0
  71. data/spec/lib/broken_migrations/duplicate_name/20090706193019_add_animals_table.rb +9 -0
  72. data/spec/lib/broken_migrations/duplicate_version/20090706190623_add_books_table.rb +9 -0
  73. data/spec/lib/broken_migrations/duplicate_version/20090706190623_add_companies_table.rb +9 -0
  74. data/spec/lib/company.rb +14 -0
  75. data/spec/lib/embedded/web_link.rb +12 -0
  76. data/spec/lib/employee.rb +33 -0
  77. data/spec/lib/migrations/20090706182535_add_animals_table.rb +13 -0
  78. data/spec/lib/migrations/20090706190623_add_books_table.rb +15 -0
  79. data/spec/lib/migrations/20090706193019_add_companies_table.rb +14 -0
  80. data/spec/lib/migrations/20090706194512_add_employees_table.rb +13 -0
  81. data/spec/lib/migrations/20090706195741_add_zoos_table.rb +13 -0
  82. data/spec/lib/novel.rb +5 -0
  83. data/spec/lib/zoo.rb +17 -0
  84. data/spec/spec.opts +4 -0
  85. data/spec/spec_helper.rb +55 -0
  86. data/spec/unit/abstract_base_spec.rb +287 -0
  87. data/spec/unit/adapters/abstract_adapter_spec.rb +56 -0
  88. data/spec/unit/adapters/adapter_shared_spec.rb +51 -0
  89. data/spec/unit/adapters/hbase_adapter_spec.rb +15 -0
  90. data/spec/unit/ar_associations_spec.rb +8 -0
  91. data/spec/unit/base_spec.rb +6 -0
  92. data/spec/unit/br_associations_spec.rb +58 -0
  93. data/spec/unit/embedded_spec.rb +43 -0
  94. data/spec/unit/find_spec.rb +34 -0
  95. data/spec/unit/hash_helper_spec.rb +44 -0
  96. data/spec/unit/migration_spec.rb +144 -0
  97. data/spec/unit/model_spec.rb +315 -0
  98. data/spec/unit/validations_spec.rb +182 -0
  99. data/tasks/bigrecord_tasks.rake +47 -0
  100. data/tasks/data_store.rb +46 -0
  101. data/tasks/gem.rb +22 -0
  102. data/tasks/rdoc.rb +8 -0
  103. data/tasks/spec.rb +34 -0
  104. metadata +189 -0
@@ -0,0 +1,194 @@
1
+ module BigRecord
2
+ module BrAssociations
3
+ module CachedItemProxy #:nodoc:
4
+
5
+ CACHE_ATTRIBUTE = "attribute:associations_cache"
6
+
7
+ attr_reader :reflection
8
+ alias_method :proxy_respond_to?, :respond_to?
9
+ alias_method :proxy_extend, :extend
10
+ # delegate :to_param, :to => :proxy_target
11
+ # instance_methods.each { |m| undef_method m unless m =~ /(^__|^nil\?$|^send$|proxy_)/ }
12
+
13
+ def proxy_cache
14
+ @owner[CACHE_ATTRIBUTE] ||= {}
15
+ @owner[CACHE_ATTRIBUTE]["#{@reflection.klass.name}:#{id}"] ||= {}
16
+ end
17
+
18
+ def proxy_owner
19
+ @owner
20
+ end
21
+
22
+ def proxy_reflection
23
+ @reflection
24
+ end
25
+
26
+ def proxy_target
27
+ @target
28
+ end
29
+
30
+ def respond_to?(symbol, include_priv = false)
31
+ proxy_respond_to?(symbol, include_priv) || (load_target && @target.respond_to?(symbol, include_priv))
32
+ end
33
+
34
+ # Explicitly proxy === because the instance method removal above
35
+ # doesn't catch it.
36
+ def ===(other)
37
+ load_target
38
+ other === @target
39
+ end
40
+
41
+ def aliased_table_name
42
+ @reflection.klass.table_name
43
+ end
44
+
45
+ def reset
46
+ @loaded = false
47
+ @target = nil
48
+ end
49
+
50
+ def reload
51
+ reset
52
+ load_target
53
+ end
54
+
55
+ def loaded?
56
+ @loaded
57
+ end
58
+
59
+ def loaded
60
+ @loaded = true
61
+ end
62
+
63
+ def target
64
+ @target
65
+ end
66
+
67
+ def target=(target)
68
+ @target = target
69
+ loaded
70
+ end
71
+
72
+ # # Returns the contents of the record as a nicely formatted string.
73
+ # def inspect
74
+ # if loaded?
75
+ # @target.inspect
76
+ # else
77
+ # attributes_as_nice_string = @reflection.options[:cache].collect { |name|
78
+ # column = @owner.column_for_attribute("attribute:#{name}")
79
+ # "#{name}: #{column.type_cast(proxy_cache[name])}" if column
80
+ # }.compact.join(", ")
81
+ # "#<Cached#{@reflection.klass} #{attributes_as_nice_string}>"
82
+ # end
83
+ # end
84
+
85
+ # def is_a?(klass)
86
+ # @reflection.klass <= klass
87
+ # end
88
+ #
89
+ # def kind_of?(klass)
90
+ # @reflection.klass <= klass
91
+ # end
92
+ #
93
+ # def to_param
94
+ # @id
95
+ # end
96
+
97
+ # protected
98
+ # def dependent?
99
+ # @reflection.options[:dependent] || false
100
+ # end
101
+ #
102
+ # def quoted_record_ids(records)
103
+ # records.map { |record| record.quoted_id }.join(',')
104
+ # end
105
+ #
106
+ ## def interpolate_sql_options!(options, *keys)
107
+ ## keys.each { |key| options[key] &&= interpolate_sql(options[key]) }
108
+ ## end
109
+ ##
110
+ ## def interpolate_sql(sql, record = nil)
111
+ ## @owner.send(:interpolate_sql, sql, record)
112
+ ## end
113
+ ##
114
+ ## def sanitize_sql(sql)
115
+ ## @reflection.klass.send(:sanitize_sql, sql)
116
+ ## end
117
+ #
118
+ # def extract_options_from_args!(args)
119
+ # @owner.send(:extract_options_from_args!, args)
120
+ # end
121
+ #
122
+ # def set_belongs_to_association_for(record)
123
+ # if @reflection.options[:as]
124
+ # record["#{@reflection.options[:as]}_id"] = @owner.id unless @owner.new_record?
125
+ # record["#{@reflection.options[:as]}_type"] = @owner.class.base_class.name.to_s
126
+ # else
127
+ # record[@reflection.primary_key_name] = @owner.id unless @owner.new_record?
128
+ # end
129
+ # end
130
+ #
131
+ # def merge_options_from_reflection!(options)
132
+ # options.reverse_merge!(
133
+ # :group => @reflection.options[:group],
134
+ # :limit => @reflection.options[:limit],
135
+ # :offset => @reflection.options[:offset],
136
+ # :joins => @reflection.options[:joins],
137
+ # :include => @reflection.options[:include],
138
+ # :select => @reflection.options[:select]
139
+ # )
140
+ # end
141
+
142
+ # private
143
+ # def method_missing(method_id, *args, &block)
144
+ # if !loaded? and @reflection.options[:cache].include?(method_id)
145
+ # # FIXME: shouldn't be hard coded
146
+ # column = @owner.column_for_attribute("attribute:#{method_id}")
147
+ # if column
148
+ # if proxy_cache.has_key?(method_id)
149
+ # column.type_cast(proxy_cache[method_id])
150
+ # elsif load_target
151
+ # proxy_cache[method_id] = @target.send(method_id, *args, &block)
152
+ # end
153
+ # else
154
+ # @target.send(method_id, *args, &block)
155
+ # end
156
+ # elsif load_target
157
+ # value = @target.send(method_id, *args, &block)
158
+ # proxy_cache[method_id] = value if @reflection.options[:cache].include?(method_id)
159
+ # value
160
+ # end
161
+ # end
162
+
163
+ def load_target
164
+ return nil unless defined?(@loaded)
165
+
166
+ if !loaded? and (!@owner.new_record? || foreign_key_present)
167
+ @target = find_target
168
+ end
169
+
170
+ @loaded = true
171
+ @target
172
+ rescue BigRecord::RecordNotFound
173
+ reset
174
+ end
175
+
176
+ def find_target
177
+ @reflection.klass.find(self.id)
178
+ end
179
+
180
+ # # Can be overwritten by associations that might have the foreign key available for an association without
181
+ # # having the object itself (and still being a new record). Currently, only belongs_to present this scenario.
182
+ # def foreign_key_present
183
+ # false
184
+ # end
185
+ #
186
+ # def raise_on_type_mismatch(record)
187
+ # unless record.is_a?(@reflection.klass)
188
+ # raise BigRecordRecord::AssociationTypeMismatch, "#{@reflection.class_name} expected, got #{record.class}"
189
+ # end
190
+ # end
191
+
192
+ end
193
+ end
194
+ end
@@ -0,0 +1,62 @@
1
+ module BigRecord
2
+ module BrAssociations
3
+ class CachedItemProxyFactory
4
+
5
+ include Singleton
6
+
7
+ def create(id, owner, reflection)
8
+ cache = owner[CachedItemProxy::CACHE_ATTRIBUTE]
9
+ cached_attributes = cache["#{reflection.klass.name}:#{id}"] if cache
10
+ cached_attributes ||= {}
11
+ cached_attributes["id"] = id
12
+ proxy = reflection.klass.instantiate(cached_attributes)
13
+ proxy.extend CachedItemProxy
14
+ proxy.instance_variable_set(:@owner, owner)
15
+ proxy.instance_variable_set(:@reflection, reflection)
16
+ proxy.reset
17
+
18
+ # Overload the cached methods
19
+ reflection.options[:cache].each do |attribute_name|
20
+ eval "def proxy.#{attribute_name}\n"+
21
+ " proxy_cache[\"#{attribute_name}\"] ||= super\n"+
22
+ "end"
23
+ end
24
+
25
+ proxy
26
+ end
27
+
28
+ # def extended_class(reflection)
29
+ # @extended_classes ||= {}
30
+ # @extended_classes[reflection.klass.name] ||= create_extended_class(reflection)
31
+ # end
32
+ #
33
+ # def create_extended_class(reflection)
34
+ # extended_class = Class.new(reflection.klass)
35
+ # extended_class.class_eval do
36
+ # include CachedItemProxy
37
+ #
38
+ # attr_reader :reflection
39
+ # alias_method :proxy_respond_to?, :respond_to?
40
+ # alias_method :proxy_extend, :extend
41
+ ## delegate :to_param, :to => :proxy_target
42
+ #
43
+ # # Overload the methods
44
+ # instance_methods.each do |m|
45
+ # if reflection.options[:cache].include?(m.to_sym)
46
+ # define_method m do
47
+ # proxy_cache[m.to_s] ||= super
48
+ # end
49
+ # end
50
+ # end
51
+ #
52
+ # def class
53
+ # reflection.klass
54
+ # end
55
+ #
56
+ # end
57
+ # extended_class
58
+ # end
59
+
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,168 @@
1
+ module BigRecord
2
+ module BrAssociations
3
+ class HasAndBelongsToManyAssociation < AssociationCollection #:nodoc:
4
+ def initialize(owner, reflection)
5
+ super
6
+ construct_sql
7
+ end
8
+
9
+ def build(attributes = {})
10
+ load_target
11
+ build_record(attributes)
12
+ end
13
+
14
+ def create(attributes = {})
15
+ create_record(attributes) { |record| insert_record(record) }
16
+ end
17
+
18
+ def create!(attributes = {})
19
+ create_record(attributes) { |record| insert_record(record, true) }
20
+ end
21
+
22
+ def find_first
23
+ load_target.first
24
+ end
25
+
26
+ def find(*args)
27
+ options = args.extract_options!
28
+
29
+ # If using a custom finder_sql, scan the entire collection.
30
+ if @reflection.options[:finder_sql]
31
+ expects_array = args.first.kind_of?(Array)
32
+ ids = args.flatten.compact.uniq
33
+
34
+ if ids.size == 1
35
+ id = ids.first.to_i
36
+ record = load_target.detect { |record| id == record.id }
37
+ expects_array ? [record] : record
38
+ else
39
+ load_target.select { |record| ids.include?(record.id) }
40
+ end
41
+ else
42
+ # Generate the sql query. The join table has to be in mysql.
43
+ conditions = "#{@finder_sql}"
44
+
45
+ if sanitized_conditions = ActiveRecord::Base.send(:sanitize_sql, options[:conditions])
46
+ conditions << " AND (#{sanitized_conditions})"
47
+ end
48
+
49
+ if options[:order] && @reflection.options[:order]
50
+ order = "#{options[:order]}, #{@reflection.options[:order]}"
51
+ elsif @reflection.options[:order]
52
+ order = @reflection.options[:order]
53
+ end
54
+
55
+ query = "SELECT #{@reflection.association_foreign_key} FROM #{@reflection.options[:join_table]} WHERE #{conditions} #{order};"
56
+
57
+ # Execute the query
58
+ ids = []
59
+ ActiveRecord::Base.connection.execute(query).each do |result|
60
+ ids << result.first
61
+ end
62
+
63
+ # Find the big_record entries by id. Find them one at a time because duplicate entries must appear in
64
+ # the result set if it's the case. The default find by ids in big_record does the same thing anyway...
65
+ ids.collect{|id| @reflection.klass.find(id)}
66
+ end
67
+ end
68
+
69
+ protected
70
+ def count_records
71
+ load_target.size
72
+ end
73
+
74
+ def insert_record(record, force=true)
75
+ if record.new_record?
76
+ if force
77
+ record.save!
78
+ else
79
+ return false unless record.save
80
+ end
81
+ end
82
+
83
+ if @reflection.options[:insert_sql]
84
+ ar_connection.execute(interpolate_sql(@reflection.options[:insert_sql], record))
85
+ else
86
+ columns = ar_connection.columns(@reflection.options[:join_table], "#{@reflection.options[:join_table]} Columns")
87
+
88
+ attributes = columns.inject({}) do |attributes, column|
89
+ case column.name
90
+ when @reflection.primary_key_name
91
+ attributes[column.name] = @owner.quoted_id
92
+ when @reflection.association_foreign_key
93
+ attributes[column.name] = record.quoted_id
94
+ else
95
+ if record.attributes.has_key?(column.name)
96
+ value = @owner.send(:quote_value, record[column.name], column)
97
+ attributes[column.name] = value unless value.nil?
98
+ end
99
+ end
100
+ attributes
101
+ end
102
+
103
+ sql =
104
+ "INSERT INTO #{@reflection.options[:join_table]} (#{@owner.send(:quoted_column_names, attributes).join(', ')}) " +
105
+ "VALUES (#{attributes.values.join(', ')})"
106
+
107
+ ar_connection.execute(sql)
108
+ end
109
+
110
+ return true
111
+ end
112
+
113
+ def delete_records(records)
114
+ if sql = @reflection.options[:delete_sql]
115
+ records.each { |record| ar_connection.execute(interpolate_sql(sql, record)) }
116
+ else
117
+ ids = quoted_record_ids(records)
118
+ sql = "DELETE FROM #{@reflection.options[:join_table]} WHERE #{@reflection.primary_key_name} = #{@owner.quoted_id} AND #{@reflection.association_foreign_key} IN (#{ids})"
119
+ ar_connection.execute(sql)
120
+ end
121
+ end
122
+
123
+ def construct_sql
124
+ # interpolate_sql_options!(@reflection.options, :finder_sql)
125
+
126
+ if @reflection.options[:finder_sql]
127
+ @finder_sql = @reflection.options[:finder_sql]
128
+ else
129
+ @finder_sql = "#{@reflection.options[:join_table]}.#{@reflection.primary_key_name} = #{@owner.quoted_id} "
130
+ # @finder_sql << " AND (#{conditions})" if conditions
131
+ end
132
+ # @join_sql = "INNER JOIN #{@reflection.options[:join_table]} ON #{@reflection.klass.table_name}.#{@reflection.klass.primary_key} = #{@reflection.options[:join_table]}.#{@reflection.association_foreign_key}"
133
+ end
134
+
135
+ def construct_scope
136
+ { :find => { :conditions => @finder_sql,
137
+ # :joins => @join_sql,
138
+ :readonly => false,
139
+ :order => @reflection.options[:order],
140
+ :limit => @reflection.options[:limit] } }
141
+ end
142
+
143
+ # Join tables with additional columns on top of the two foreign keys must be considered ambiguous unless a select
144
+ # clause has been explicitly defined. Otherwise you can get broken records back, if, for example, the join column also has
145
+ # an id column. This will then overwrite the id column of the records coming back.
146
+ def finding_with_ambiguous_select?(select_clause)
147
+ !select_clause && ar_connection.columns(@reflection.options[:join_table], "Join Table Columns").size != 2
148
+ end
149
+
150
+ private
151
+ def create_record(attributes)
152
+ # Can't use Base.create because the foreign key may be a protected attribute.
153
+ ensure_owner_is_not_new
154
+ if attributes.is_a?(Array)
155
+ attributes.collect { |attr| create(attr) }
156
+ else
157
+ record = build(attributes)
158
+ yield(record)
159
+ record
160
+ end
161
+ end
162
+
163
+ def ar_connection
164
+ (@owner.class < ActiveRecord::Base) ? @owner.connection : ActiveRecord::Base.connection
165
+ end
166
+ end
167
+ end
168
+ end
@@ -0,0 +1,80 @@
1
+ module BigRecord
2
+ module BrAssociations
3
+ class HasOneAssociation < BelongsToAssociation #:nodoc:
4
+ def initialize(owner, reflection)
5
+ super
6
+ construct_sql
7
+ end
8
+
9
+ def create(attributes = {}, replace_existing = true)
10
+ record = build(attributes, replace_existing)
11
+ record.save
12
+ record
13
+ end
14
+
15
+ def build(attributes = {}, replace_existing = true)
16
+ record = @reflection.klass.new(attributes)
17
+
18
+ if replace_existing
19
+ replace(record, true)
20
+ else
21
+ record[@reflection.primary_key_name] = @owner.id unless @owner.new_record?
22
+ self.target = record
23
+ end
24
+
25
+ record
26
+ end
27
+
28
+ def replace(obj, dont_save = false)
29
+ load_target
30
+
31
+ unless @target.nil?
32
+ if dependent? && !dont_save && @target != obj
33
+ @target.destroy unless @target.new_record?
34
+ @owner.clear_association_cache
35
+ else
36
+ @target[@reflection.primary_key_name] = nil
37
+ @target.save unless @owner.new_record? || @target.new_record?
38
+ end
39
+ end
40
+
41
+ if obj.nil?
42
+ @target = nil
43
+ else
44
+ raise_on_type_mismatch(obj)
45
+ set_belongs_to_association_for(obj)
46
+ @target = (AssociationProxy === obj ? obj.target : obj)
47
+ end
48
+
49
+ @loaded = true
50
+
51
+ unless @owner.new_record? or obj.nil? or dont_save
52
+ return (obj.save ? self : false)
53
+ else
54
+ return (obj.nil? ? nil : self)
55
+ end
56
+ end
57
+
58
+ private
59
+ def find_target
60
+ @reflection.klass.find(:first,
61
+ :conditions => @finder_sql,
62
+ :order => @reflection.options[:order],
63
+ :include => @reflection.options[:include]
64
+ )
65
+ end
66
+
67
+ # def construct_sql
68
+ # case
69
+ # when @reflection.options[:as]
70
+ # @finder_sql =
71
+ # "#{@reflection.klass.table_name}.#{@reflection.options[:as]}_id = #{@owner.quoted_id} AND " +
72
+ # "#{@reflection.klass.table_name}.#{@reflection.options[:as]}_type = #{@owner.class.quote_value(@owner.class.base_class.name.to_s)}"
73
+ # else
74
+ # @finder_sql = "#{@reflection.table_name}.#{@reflection.primary_key_name} = #{@owner.quoted_id}"
75
+ # end
76
+ # @finder_sql << " AND (#{conditions})" if conditions
77
+ # end
78
+ end
79
+ end
80
+ end