bigrecord 0.0.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.
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,57 @@
1
+ module BigRecord
2
+ module ArAssociations
3
+ class BelongsToManyAssociation < AssociationProxy #:nodoc:
4
+
5
+ def create(attributes = {})
6
+ replace(@reflection.klass.create(attributes))
7
+ end
8
+
9
+ def build(attributes = {})
10
+ replace(@reflection.klass.new(attributes))
11
+ end
12
+
13
+ def replace(record)
14
+ counter_cache_name = @reflection.counter_cache_column
15
+
16
+ if record.nil?
17
+ if counter_cache_name && @owner[counter_cache_name] && !@owner.new_record?
18
+ @reflection.klass.decrement_counter(counter_cache_name, @owner[@reflection.primary_key_name]) if @owner[@reflection.primary_key_name]
19
+ end
20
+
21
+ @target = @owner[@reflection.primary_key_name] = nil
22
+ else
23
+ raise_on_type_mismatch(record)
24
+
25
+ if counter_cache_name && !@owner.new_record?
26
+ @reflection.klass.increment_counter(counter_cache_name, record.id)
27
+ @reflection.klass.decrement_counter(counter_cache_name, @owner[@reflection.primary_key_name]) if @owner[@reflection.primary_key_name]
28
+ end
29
+
30
+ @target = (AssociationProxy === record ? record.target : record)
31
+ @owner[@reflection.primary_key_name] = record.id unless record.new_record?
32
+ @updated = true
33
+ end
34
+
35
+ loaded
36
+ record
37
+ end
38
+
39
+ def updated?
40
+ @updated
41
+ end
42
+
43
+ private
44
+ def find_target
45
+ @reflection.klass.find(
46
+ @owner[@reflection.primary_key_name],
47
+ :conditions => conditions,
48
+ :include => @reflection.options[:include]
49
+ )
50
+ end
51
+
52
+ def foreign_key_present
53
+ !@owner[@reflection.primary_key_name].nil?
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,164 @@
1
+ module BigRecord
2
+ module ArAssociations
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
+ conditions = "#{@finder_sql}"
43
+
44
+ if sanitized_conditions = sanitize_sql(options[:conditions])
45
+ conditions << " AND (#{sanitized_conditions})"
46
+ end
47
+
48
+ options[:conditions] = conditions
49
+ options[:joins] = @join_sql
50
+ options[:readonly] = finding_with_ambiguous_select?(options[:select] || @reflection.options[:select])
51
+
52
+ if options[:order] && @reflection.options[:order]
53
+ options[:order] = "#{options[:order]}, #{@reflection.options[:order]}"
54
+ elsif @reflection.options[:order]
55
+ options[:order] = @reflection.options[:order]
56
+ end
57
+
58
+ merge_options_from_reflection!(options)
59
+
60
+ options[:select] ||= (@reflection.options[:select] || '*')
61
+
62
+ # Pass through args exactly as we received them.
63
+ args << options
64
+ @reflection.klass.find(*args)
65
+ end
66
+ end
67
+
68
+ protected
69
+ def count_records
70
+ load_target.size rescue 0
71
+ end
72
+
73
+ def insert_record(record, force=true)
74
+ if record.new_record?
75
+ if force
76
+ record.save!
77
+ else
78
+ return false unless record.save
79
+ end
80
+ end
81
+
82
+ if @reflection.options[:insert_sql]
83
+ @reflection.klass.connection.execute(interpolate_sql(@reflection.options[:insert_sql], record))
84
+ else
85
+ columns = @reflection.klass.connection.columns(@reflection.options[:join_table], "#{@reflection.options[:join_table]} Columns")
86
+
87
+ attributes = columns.inject({}) do |attributes, column|
88
+ case column.name
89
+ when @reflection.primary_key_name
90
+ attributes[column.name] = @owner.quoted_id
91
+ when @reflection.association_foreign_key
92
+ attributes[column.name] = record.quoted_id
93
+ else
94
+ if record.attributes.has_key?(column.name)
95
+ value = @owner.send(:quote_value, record[column.name], column)
96
+ attributes[column.name] = value unless value.nil?
97
+ end
98
+ end
99
+ attributes
100
+ end
101
+
102
+ sql =
103
+ "INSERT INTO #{@reflection.options[:join_table]} (#{record.send(:quoted_column_names, attributes).join(', ')}) " +
104
+ "VALUES (#{attributes.values.join(', ')})"
105
+
106
+ @reflection.klass.connection.execute(sql)
107
+ end
108
+
109
+ return true
110
+ end
111
+
112
+ def delete_records(records)
113
+ if sql = @reflection.options[:delete_sql]
114
+ records.each { |record| @owner.connection.execute(interpolate_sql(sql, record)) }
115
+ else
116
+ ids = quoted_record_ids(records)
117
+ sql = "DELETE FROM #{@reflection.options[:join_table]} WHERE #{@reflection.primary_key_name} = #{@owner.quoted_id} AND #{@reflection.association_foreign_key} IN (#{ids})"
118
+ @reflection.klass.connection.execute(sql)
119
+ end
120
+ end
121
+
122
+ def construct_sql
123
+ interpolate_sql_options!(@reflection.options, :finder_sql)
124
+
125
+ if @reflection.options[:finder_sql]
126
+ @finder_sql = @reflection.options[:finder_sql]
127
+ else
128
+ @finder_sql = "#{@reflection.options[:join_table]}.#{@reflection.primary_key_name} = #{@owner.quoted_id} "
129
+ @finder_sql << " AND (#{conditions})" if conditions
130
+ end
131
+
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 && @reflection.klass.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
+ end
163
+ end
164
+ end
@@ -0,0 +1,191 @@
1
+ module BigRecord
2
+ module ArAssociations
3
+ class HasManyAssociation < AssociationCollection #:nodoc:
4
+ def initialize(owner, reflection)
5
+ super
6
+ construct_sql
7
+ end
8
+
9
+ def build(attributes = {})
10
+ if attributes.is_a?(Array)
11
+ attributes.collect { |attr| build(attr) }
12
+ else
13
+ record = @reflection.klass.new(attributes)
14
+ set_belongs_to_association_for(record)
15
+
16
+ @target ||= [] unless loaded?
17
+ @target << record
18
+
19
+ record
20
+ end
21
+ end
22
+
23
+ # Count the number of associated records. All arguments are optional.
24
+ def count(*args)
25
+ if @reflection.options[:counter_sql]
26
+ @reflection.klass.count_by_sql(@counter_sql)
27
+ elsif @reflection.options[:finder_sql]
28
+ @reflection.klass.count_by_sql(@finder_sql)
29
+ else
30
+ column_name, options = @reflection.klass.send(:construct_count_options_from_legacy_args, *args)
31
+ options[:conditions] = options[:conditions].nil? ?
32
+ @finder_sql :
33
+ @finder_sql + " AND (#{sanitize_sql(options[:conditions])})"
34
+ options[:include] = @reflection.options[:include]
35
+
36
+ @reflection.klass.count(column_name, options)
37
+ end
38
+ end
39
+
40
+ def find(*args)
41
+ options = Base.send(:extract_options_from_args!, args)
42
+
43
+ # If using a custom finder_sql, scan the entire collection.
44
+ if @reflection.options[:finder_sql]
45
+ expects_array = args.first.kind_of?(Array)
46
+ ids = args.flatten.compact.uniq
47
+
48
+ if ids.size == 1
49
+ id = ids.first
50
+ record = load_target.detect { |record| id == record.id }
51
+ expects_array ? [ record ] : record
52
+ else
53
+ load_target.select { |record| ids.include?(record.id) }
54
+ end
55
+ else
56
+ conditions = "#{@finder_sql}"
57
+ if sanitized_conditions = sanitize_sql(options[:conditions])
58
+ conditions << " AND (#{sanitized_conditions})"
59
+ end
60
+ options[:conditions] = conditions
61
+
62
+ if options[:order] && @reflection.options[:order]
63
+ options[:order] = "#{options[:order]}, #{@reflection.options[:order]}"
64
+ elsif @reflection.options[:order]
65
+ options[:order] = @reflection.options[:order]
66
+ end
67
+
68
+ merge_options_from_reflection!(options)
69
+
70
+ # Pass through args exactly as we received them.
71
+ args << options
72
+ @reflection.klass.find(*args)
73
+ end
74
+ end
75
+
76
+ protected
77
+ def method_missing(method, *args, &block)
78
+ if @target.respond_to?(method) || (!@reflection.klass.respond_to?(method) && Class.respond_to?(method))
79
+ super
80
+ else
81
+ create_scoping = {}
82
+ set_belongs_to_association_for(create_scoping)
83
+
84
+ @reflection.klass.with_scope(
85
+ :create => create_scoping,
86
+ :find => {
87
+ :conditions => @finder_sql,
88
+ :joins => @join_sql,
89
+ :readonly => false
90
+ }
91
+ ) do
92
+ @reflection.klass.send(method, *args, &block)
93
+ end
94
+ end
95
+ end
96
+
97
+ def load_target
98
+ if !@owner.new_record? || foreign_key_present
99
+ begin
100
+ if !loaded?
101
+ if @target.is_a?(Array) && @target.any?
102
+ @target = (find_target + @target).uniq
103
+ else
104
+ @target = find_target
105
+ end
106
+ end
107
+ rescue ActiveRecord::RecordNotFound
108
+ reset
109
+ end
110
+ end
111
+
112
+ loaded if target
113
+ target
114
+ end
115
+
116
+ def count_records
117
+ count = if has_cached_counter?
118
+ @owner.send(:read_attribute, cached_counter_attribute_name)
119
+ elsif @reflection.options[:counter_sql]
120
+ @reflection.klass.count_by_sql(@counter_sql)
121
+ else
122
+ @reflection.klass.count(:conditions => @counter_sql)
123
+ end
124
+
125
+ @target = [] and loaded if count == 0
126
+
127
+ if @reflection.options[:limit]
128
+ count = [ @reflection.options[:limit], count ].min
129
+ end
130
+
131
+ return count
132
+ end
133
+
134
+ def has_cached_counter?
135
+ @owner.attribute_present?(cached_counter_attribute_name)
136
+ end
137
+
138
+ def cached_counter_attribute_name
139
+ "#{@reflection.name}_count"
140
+ end
141
+
142
+ def insert_record(record)
143
+ set_belongs_to_association_for(record)
144
+ record.save
145
+ end
146
+
147
+ def delete_records(records)
148
+ if @reflection.options[:dependent]
149
+ records.each { |r| r.destroy }
150
+ else
151
+ ids = quoted_record_ids(records)
152
+ @reflection.klass.update_all(
153
+ "#{@reflection.primary_key_name} = NULL",
154
+ "#{@reflection.primary_key_name} = #{@owner.quoted_id} AND #{@reflection.klass.primary_key} IN (#{ids})"
155
+ )
156
+ end
157
+ end
158
+
159
+ def target_obsolete?
160
+ false
161
+ end
162
+
163
+ def construct_sql
164
+ case
165
+ when @reflection.options[:finder_sql]
166
+ @finder_sql = interpolate_sql(@reflection.options[:finder_sql])
167
+
168
+ when @reflection.options[:as]
169
+ @finder_sql =
170
+ "#{@reflection.klass.table_name}.#{@reflection.options[:as]}_id = #{@owner.quoted_id} AND " +
171
+ "#{@reflection.klass.table_name}.#{@reflection.options[:as]}_type = #{@owner.class.quote_value(@owner.class.base_class.name.to_s)}"
172
+ @finder_sql << " AND (#{conditions})" if conditions
173
+
174
+ else
175
+ @finder_sql = "#{@reflection.klass.table_name}.#{@reflection.primary_key_name} = #{@owner.quoted_id}"
176
+ @finder_sql << " AND (#{conditions})" if conditions
177
+ end
178
+
179
+ if @reflection.options[:counter_sql]
180
+ @counter_sql = interpolate_sql(@reflection.options[:counter_sql])
181
+ elsif @reflection.options[:finder_sql]
182
+ # replace the SELECT clause with COUNT(*), preserving any hints within /* ... */
183
+ @reflection.options[:counter_sql] = @reflection.options[:finder_sql].sub(/SELECT (\/\*.*?\*\/ )?(.*)\bFROM\b/im) { "SELECT #{$1}COUNT(*) FROM" }
184
+ @counter_sql = interpolate_sql(@reflection.options[:counter_sql])
185
+ else
186
+ @counter_sql = @finder_sql
187
+ end
188
+ end
189
+ end
190
+ end
191
+ end
@@ -0,0 +1,80 @@
1
+ module BigRecord
2
+ module ArAssociations
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