composite_primary_keys 14.0.6 → 14.0.7

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 (36) hide show
  1. checksums.yaml +4 -4
  2. data/History.rdoc +6 -0
  3. data/Rakefile +1 -1
  4. data/lib/composite_primary_keys/associations/association.rb +2 -2
  5. data/lib/composite_primary_keys/associations/association_scope.rb +1 -1
  6. data/lib/composite_primary_keys/associations/has_many_through_association.rb +19 -0
  7. data/lib/composite_primary_keys/connection_adapters/abstract/database_statements.rb +1 -2
  8. data/lib/composite_primary_keys/relation/query_methods.rb +14 -16
  9. data/lib/composite_primary_keys/relation.rb +199 -197
  10. data/lib/composite_primary_keys/version.rb +1 -1
  11. data/lib/composite_primary_keys.rb +117 -119
  12. data/scripts/console.rb +2 -2
  13. data/tasks/databases/trilogy.rake +23 -0
  14. data/test/abstract_unit.rb +124 -118
  15. data/test/connections/databases.ci.yml +10 -0
  16. data/test/fixtures/admin.rb +4 -0
  17. data/test/fixtures/comments.yml +6 -0
  18. data/test/fixtures/db_definitions/db2-create-tables.sql +34 -0
  19. data/test/fixtures/db_definitions/db2-drop-tables.sql +7 -1
  20. data/test/fixtures/db_definitions/mysql.sql +23 -0
  21. data/test/fixtures/db_definitions/oracle.drop.sql +4 -0
  22. data/test/fixtures/db_definitions/oracle.sql +21 -0
  23. data/test/fixtures/db_definitions/postgresql.sql +23 -0
  24. data/test/fixtures/db_definitions/sqlite.sql +21 -0
  25. data/test/fixtures/db_definitions/sqlserver.sql +23 -0
  26. data/test/fixtures/moderator.rb +4 -0
  27. data/test/fixtures/room.rb +4 -1
  28. data/test/fixtures/staff_room.rb +6 -0
  29. data/test/fixtures/staff_room_key.rb +6 -0
  30. data/test/fixtures/user.rb +3 -0
  31. data/test/fixtures/user_with_polymorphic_name.rb +9 -0
  32. data/test/test_associations.rb +403 -403
  33. data/test/test_has_one_through.rb +30 -0
  34. data/test/test_polymorphic.rb +6 -0
  35. metadata +11 -4
  36. data/lib/composite_primary_keys/associations/through_association.rb +0 -24
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 76a010358e1d2cacd9b05f6229c1fe4225f20f888fefb57b0bd6a9f62589a720
4
- data.tar.gz: 1b7870c774bbed21d4556cf53518054860903cae4a0987f5978e02fc3883597f
3
+ metadata.gz: de97dfff0f237f22dd32a32189824739087b30c0ea0e14cdd996bd8b69359caf
4
+ data.tar.gz: 722a9e0d02b6fa8ee2010d47e5a37705a43b302df6abf7c7a6d199b0a84b0201
5
5
  SHA512:
6
- metadata.gz: 117d7be0b54619f7adcb05ddb200c3c97571cc74bf2fcf33c99034efaec08924cf6d56e998db1135c92bf71497697b30ca531a882d755329d5b6f9192d606013
7
- data.tar.gz: a9f330398e5b1a9153a67f229a28e83d65c5bc02334e6e9f3456229c4c1e011a3bf03909fa45e37739d4ab47a37663a3ee6ef09c7d7194721e91849e520c0a01
6
+ metadata.gz: e57b5330dc2b0cdb7374f14ed6c7efed22c269cf42298527efc1ec26ee2e2ea8ab9edd97e97a7f4c656662e54a1faa5da28fef365c602060878d375e4caa646a
7
+ data.tar.gz: c2981385774399cdd3e64385d9f7595dd2a3972706b04e825b604f83ed4fe59438df2f114f196ff0fb6ceb389060554259cab7132c2a75073a382101da533bd0
data/History.rdoc CHANGED
@@ -1,3 +1,9 @@
1
+ == 14.0.7 (2023-11-04)
2
+ * Add support for Trilogy Adapter (Zack Mariscal)
3
+ * Add Dockerfile and Docker compose support (Zack Mariscal)
4
+ * Support polymorphic_name (Vladimir Kochnev)
5
+ * Fix HasOneThrough association (kyori19)
6
+
1
7
  == 14.0.6 (2023-02-04)
2
8
  * Port fix for #573 (Charlie Savage)
3
9
  * Port fix for fix #577 (Charlie Savage)
data/Rakefile CHANGED
@@ -23,7 +23,7 @@ Dir.glob('tasks/**/*.rake').each do |rake_file|
23
23
  end
24
24
 
25
25
  # Set up test tasks for each supported connection adapter
26
- %w(mysql sqlite oracle oracle_enhanced postgresql ibm_db sqlserver).each do |adapter|
26
+ %w(mysql sqlite oracle oracle_enhanced postgresql ibm_db sqlserver trilogy).each do |adapter|
27
27
  namespace adapter do
28
28
  desc "Run tests using the #{adapter} adapter"
29
29
  task "test" do
@@ -11,8 +11,8 @@ module ActiveRecord
11
11
  attributes[key1] = owner[key2]
12
12
  end
13
13
 
14
- if reflection.options[:as]
15
- attributes[reflection.type] = owner.class.base_class.name
14
+ if reflection.type
15
+ attributes[reflection.type] = owner.class.polymorphic_name
16
16
  end
17
17
  end
18
18
 
@@ -64,4 +64,4 @@ module ActiveRecord
64
64
  end
65
65
  end
66
66
  end
67
- end
67
+ end
@@ -23,6 +23,25 @@ module ActiveRecord
23
23
  end
24
24
  end
25
25
  end
26
+
27
+ alias :original_construct_join_attributes :construct_join_attributes
28
+
29
+ def construct_join_attributes(*records)
30
+ # CPK
31
+ if !self.source_reflection.polymorphic? && source_reflection.klass.composite?
32
+ ensure_mutable
33
+
34
+ ids = records.map do |record|
35
+ source_reflection.association_primary_key(reflection.klass).map do |key|
36
+ record.send(key)
37
+ end
38
+ end
39
+
40
+ cpk_in_predicate(through_association.scope.klass.arel_table, source_reflection.foreign_key, ids)
41
+ else
42
+ original_construct_join_attributes(*records)
43
+ end
44
+ end
26
45
  end
27
46
  end
28
47
  end
@@ -6,8 +6,7 @@ module ActiveRecord
6
6
  value = exec_insert(sql, name, binds, pk, sequence_name)
7
7
 
8
8
  return id_value if id_value
9
-
10
- if pk.is_a?(Array) && !value.empty?
9
+ if pk.is_a?(Array) && value.respond_to?(:empty?) && !value.empty?
11
10
  # This is a CPK model and the query result is not empty. Thus we can figure out the new ids for each
12
11
  # auto incremented field
13
12
  pk.map {|key| value.first[key]}
@@ -18,23 +18,21 @@ module CompositePrimaryKeys
18
18
  end
19
19
 
20
20
  order_query.flat_map do |o|
21
- order_query.flat_map do |o|
22
- case o
23
- when Arel::Attribute
24
- o.desc
25
- when Arel::Nodes::Ordering
26
- o.reverse
27
- when String
28
- if does_not_support_reverse?(o)
29
- raise IrreversibleOrderError, "Order #{o.inspect} can not be reversed automatically"
30
- end
31
- o.split(",").map! do |s|
32
- s.strip!
33
- s.gsub!(/\sasc\Z/i, " DESC") || s.gsub!(/\sdesc\Z/i, " ASC") || (s << " DESC")
34
- end
35
- else
36
- o
21
+ case o
22
+ when Arel::Attribute
23
+ o.desc
24
+ when Arel::Nodes::Ordering
25
+ o.reverse
26
+ when String
27
+ if does_not_support_reverse?(o)
28
+ raise IrreversibleOrderError, "Order #{o.inspect} can not be reversed automatically"
37
29
  end
30
+ o.split(",").map! do |s|
31
+ s.strip!
32
+ s.gsub!(/\sasc\Z/i, " DESC") || s.gsub!(/\sdesc\Z/i, " ASC") || (s << " DESC")
33
+ end
34
+ else
35
+ o
38
36
  end
39
37
  end
40
38
  end
@@ -1,197 +1,199 @@
1
- module ActiveRecord
2
- class Relation
3
- alias :initialize_without_cpk :initialize
4
- def initialize(klass, table: klass.arel_table, predicate_builder: klass.predicate_builder, values: {})
5
- initialize_without_cpk(klass, table: table, predicate_builder: predicate_builder, values: values)
6
- add_cpk_support if klass && klass.composite?
7
- end
8
-
9
- alias :initialize_copy_without_cpk :initialize_copy
10
- def initialize_copy(other)
11
- initialize_copy_without_cpk(other)
12
- add_cpk_support if klass.composite?
13
- end
14
-
15
- def add_cpk_support
16
- extend CompositePrimaryKeys::CompositeRelation
17
- end
18
-
19
- def update_all(updates)
20
- raise ArgumentError, "Empty list of attributes to change" if updates.blank?
21
-
22
- if eager_loading?
23
- relation = apply_join_dependency
24
- return relation.update_all(updates)
25
- end
26
-
27
- stmt = Arel::UpdateManager.new
28
- stmt.table(arel.join_sources.empty? ? table : arel.source)
29
- stmt.key = table[primary_key]
30
-
31
- # CPK
32
- if @klass.composite?
33
- stmt = Arel::UpdateManager.new
34
- stmt.table(arel_table)
35
- cpk_subquery(stmt)
36
- else
37
- stmt.wheres = arel.constraints
38
- end
39
- stmt.take(arel.limit)
40
- stmt.offset(arel.offset)
41
- stmt.order(*arel.orders)
42
-
43
- if updates.is_a?(Hash)
44
- if klass.locking_enabled? &&
45
- !updates.key?(klass.locking_column) &&
46
- !updates.key?(klass.locking_column.to_sym)
47
- attr = table[klass.locking_column]
48
- updates[attr.name] = _increment_attribute(attr)
49
- end
50
- stmt.set _substitute_values(updates)
51
- else
52
- stmt.set Arel.sql(klass.sanitize_sql_for_assignment(updates, table.name))
53
- end
54
-
55
- @klass.connection.update stmt, "#{@klass} Update All"
56
- end
57
-
58
- def delete_all
59
- invalid_methods = INVALID_METHODS_FOR_DELETE_ALL.select do |method|
60
- value = @values[method]
61
- method == :distinct ? value : value&.any?
62
- end
63
- if invalid_methods.any?
64
- raise ActiveRecordError.new("delete_all doesn't support #{invalid_methods.join(', ')}")
65
- end
66
-
67
- if eager_loading?
68
- relation = apply_join_dependency
69
- return relation.delete_all
70
- end
71
-
72
- stmt = Arel::DeleteManager.new
73
- stmt.from(arel.join_sources.empty? ? table : arel.source)
74
- stmt.key = table[primary_key]
75
-
76
- # CPK
77
- if @klass.composite?
78
- stmt = Arel::DeleteManager.new
79
- stmt.from(arel_table)
80
- cpk_subquery(stmt)
81
- else
82
- stmt.wheres = arel.constraints
83
- end
84
-
85
- stmt.take(arel.limit)
86
- stmt.offset(arel.offset)
87
- stmt.order(*arel.orders)
88
-
89
- affected = @klass.connection.delete(stmt, "#{@klass} Destroy")
90
-
91
- reset
92
- affected
93
- end
94
-
95
- # CPK
96
- def cpk_subquery(stmt)
97
- # For update and delete statements we need a way to specify which records should
98
- # get updated. By default, Rails creates a nested IN subquery that uses the primary
99
- # key. Postgresql, Sqlite, MariaDb and Oracle support IN subqueries with multiple
100
- # columns but MySQL and SqlServer do not. Instead SQL server supports EXISTS queries
101
- # and MySQL supports obfuscated IN queries. Thus we need to check the type of
102
- # database adapter to decide how to proceed.
103
- if defined?(ActiveRecord::ConnectionAdapters::Mysql2Adapter) && connection.is_a?(ActiveRecord::ConnectionAdapters::Mysql2Adapter)
104
- cpk_mysql_subquery(stmt)
105
- elsif defined?(ActiveRecord::ConnectionAdapters::SQLServerAdapter) && connection.is_a?(ActiveRecord::ConnectionAdapters::SQLServerAdapter)
106
- cpk_exists_subquery(stmt)
107
- else
108
- cpk_in_subquery(stmt)
109
- end
110
- end
111
-
112
- # Used by postgresql, sqlite, mariadb and oracle. Example query:
113
- #
114
- # UPDATE reference_codes
115
- # SET ...
116
- # WHERE (reference_codes.reference_type_id, reference_codes.reference_code) IN
117
- # (SELECT reference_codes.reference_type_id, reference_codes.reference_code
118
- # FROM reference_codes)
119
- def cpk_in_subquery(stmt)
120
- # Setup the subquery
121
- subquery = arel.clone
122
- subquery.projections = primary_keys.map do |key|
123
- arel_table[key]
124
- end
125
-
126
- where_fields = primary_keys.map do |key|
127
- arel_table[key]
128
- end
129
- where = Arel::Nodes::Grouping.new(where_fields).in(subquery)
130
- stmt.wheres = [where]
131
- end
132
-
133
- # CPK. This is an alternative to IN subqueries. It is used by sqlserver.
134
- # Example query:
135
- #
136
- # UPDATE reference_codes
137
- # SET ...
138
- # WHERE EXISTS
139
- # (SELECT 1
140
- # FROM reference_codes cpk_child
141
- # WHERE reference_codes.reference_type_id = cpk_child.reference_type_id AND
142
- # reference_codes.reference_code = cpk_child.reference_code)
143
- def cpk_exists_subquery(stmt)
144
- arel_attributes = primary_keys.map do |key|
145
- table[key]
146
- end.to_composite_keys
147
-
148
- # Clone the query
149
- subselect = arel.clone
150
-
151
- # Alias the table - we assume just one table
152
- aliased_table = subselect.froms.first
153
- aliased_table.table_alias = "cpk_child"
154
-
155
- # Project - really we could just set this to "1"
156
- subselect.projections = arel_attributes
157
-
158
- # Setup correlation to the outer query via where clauses
159
- primary_keys.map do |key|
160
- outer_attribute = arel_table[key]
161
- inner_attribute = aliased_table[key]
162
- where = outer_attribute.eq(inner_attribute)
163
- subselect.where(where)
164
- end
165
- stmt.wheres = [Arel::Nodes::Exists.new(subselect)]
166
- end
167
-
168
- # CPK. This is the old way CPK created subqueries and is used by MySql.
169
- # MySQL does not support referencing the same table that is being UPDATEd or
170
- # DELETEd in a subquery so we obfuscate it. The ugly query looks like this:
171
- #
172
- # UPDATE `reference_codes`
173
- # SET ...
174
- # WHERE (reference_codes.reference_type_id, reference_codes.reference_code) IN
175
- # (SELECT reference_type_id,reference_code
176
- # FROM (SELECT DISTINCT `reference_codes`.`reference_type_id`, `reference_codes`.`reference_code`
177
- # FROM `reference_codes`) __active_record_temp)
178
- def cpk_mysql_subquery(stmt)
179
- arel_attributes = primary_keys.map do |key|
180
- table[key]
181
- end.to_composite_keys
182
-
183
- subselect = arel.clone
184
- subselect.projections = arel_attributes
185
-
186
- # Materialize subquery by adding distinct
187
- # to work with MySQL 5.7.6 which sets optimizer_switch='derived_merge=on'
188
- subselect.distinct unless arel.limit || arel.offset || arel.orders.any?
189
-
190
- key_name = arel_attributes.map(&:name).join(',')
191
-
192
- manager = Arel::SelectManager.new(subselect.as("__active_record_temp")).project(Arel.sql(key_name))
193
-
194
- stmt.wheres = [Arel::Nodes::In.new(arel_attributes, manager.ast)]
195
- end
196
- end
197
- end
1
+ module ActiveRecord
2
+ class Relation
3
+ alias :initialize_without_cpk :initialize
4
+ def initialize(klass, table: klass.arel_table, predicate_builder: klass.predicate_builder, values: {})
5
+ initialize_without_cpk(klass, table: table, predicate_builder: predicate_builder, values: values)
6
+ add_cpk_support if klass && klass.composite?
7
+ end
8
+
9
+ alias :initialize_copy_without_cpk :initialize_copy
10
+ def initialize_copy(other)
11
+ initialize_copy_without_cpk(other)
12
+ add_cpk_support if klass.composite?
13
+ end
14
+
15
+ def add_cpk_support
16
+ extend CompositePrimaryKeys::CompositeRelation
17
+ end
18
+
19
+ def update_all(updates)
20
+ raise ArgumentError, "Empty list of attributes to change" if updates.blank?
21
+
22
+ if eager_loading?
23
+ relation = apply_join_dependency
24
+ return relation.update_all(updates)
25
+ end
26
+
27
+ stmt = Arel::UpdateManager.new
28
+ stmt.table(arel.join_sources.empty? ? table : arel.source)
29
+ stmt.key = table[primary_key]
30
+
31
+ # CPK
32
+ if @klass.composite?
33
+ stmt = Arel::UpdateManager.new
34
+ stmt.table(arel_table)
35
+ cpk_subquery(stmt)
36
+ else
37
+ stmt.wheres = arel.constraints
38
+ end
39
+ stmt.take(arel.limit)
40
+ stmt.offset(arel.offset)
41
+ stmt.order(*arel.orders)
42
+
43
+ if updates.is_a?(Hash)
44
+ if klass.locking_enabled? &&
45
+ !updates.key?(klass.locking_column) &&
46
+ !updates.key?(klass.locking_column.to_sym)
47
+ attr = table[klass.locking_column]
48
+ updates[attr.name] = _increment_attribute(attr)
49
+ end
50
+ stmt.set _substitute_values(updates)
51
+ else
52
+ stmt.set Arel.sql(klass.sanitize_sql_for_assignment(updates, table.name))
53
+ end
54
+
55
+ @klass.connection.update stmt, "#{@klass} Update All"
56
+ end
57
+
58
+ def delete_all
59
+ invalid_methods = INVALID_METHODS_FOR_DELETE_ALL.select do |method|
60
+ value = @values[method]
61
+ method == :distinct ? value : value&.any?
62
+ end
63
+ if invalid_methods.any?
64
+ raise ActiveRecordError.new("delete_all doesn't support #{invalid_methods.join(', ')}")
65
+ end
66
+
67
+ if eager_loading?
68
+ relation = apply_join_dependency
69
+ return relation.delete_all
70
+ end
71
+
72
+ stmt = Arel::DeleteManager.new
73
+ stmt.from(arel.join_sources.empty? ? table : arel.source)
74
+ stmt.key = table[primary_key]
75
+
76
+ # CPK
77
+ if @klass.composite?
78
+ stmt = Arel::DeleteManager.new
79
+ stmt.from(arel_table)
80
+ cpk_subquery(stmt)
81
+ else
82
+ stmt.wheres = arel.constraints
83
+ end
84
+
85
+ stmt.take(arel.limit)
86
+ stmt.offset(arel.offset)
87
+ stmt.order(*arel.orders)
88
+
89
+ affected = @klass.connection.delete(stmt, "#{@klass} Destroy")
90
+
91
+ reset
92
+ affected
93
+ end
94
+
95
+ # CPK
96
+ def cpk_subquery(stmt)
97
+ # For update and delete statements we need a way to specify which records should
98
+ # get updated. By default, Rails creates a nested IN subquery that uses the primary
99
+ # key. Postgresql, Sqlite, MariaDb and Oracle support IN subqueries with multiple
100
+ # columns but MySQL and SqlServer do not. Instead SQL server supports EXISTS queries
101
+ # and MySQL supports obfuscated IN queries. Thus we need to check the type of
102
+ # database adapter to decide how to proceed.
103
+ if defined?(ActiveRecord::ConnectionAdapters::Mysql2Adapter) && connection.is_a?(ActiveRecord::ConnectionAdapters::Mysql2Adapter)
104
+ cpk_mysql_subquery(stmt)
105
+ elsif defined?(ActiveRecord::ConnectionAdapters::TrilogyAdapter) && connection.is_a?(ActiveRecord::ConnectionAdapters::TrilogyAdapter)
106
+ cpk_mysql_subquery(stmt)
107
+ elsif defined?(ActiveRecord::ConnectionAdapters::SQLServerAdapter) && connection.is_a?(ActiveRecord::ConnectionAdapters::SQLServerAdapter)
108
+ cpk_exists_subquery(stmt)
109
+ else
110
+ cpk_in_subquery(stmt)
111
+ end
112
+ end
113
+
114
+ # Used by postgresql, sqlite, mariadb and oracle. Example query:
115
+ #
116
+ # UPDATE reference_codes
117
+ # SET ...
118
+ # WHERE (reference_codes.reference_type_id, reference_codes.reference_code) IN
119
+ # (SELECT reference_codes.reference_type_id, reference_codes.reference_code
120
+ # FROM reference_codes)
121
+ def cpk_in_subquery(stmt)
122
+ # Setup the subquery
123
+ subquery = arel.clone
124
+ subquery.projections = primary_keys.map do |key|
125
+ arel_table[key]
126
+ end
127
+
128
+ where_fields = primary_keys.map do |key|
129
+ arel_table[key]
130
+ end
131
+ where = Arel::Nodes::Grouping.new(where_fields).in(subquery)
132
+ stmt.wheres = [where]
133
+ end
134
+
135
+ # CPK. This is an alternative to IN subqueries. It is used by sqlserver.
136
+ # Example query:
137
+ #
138
+ # UPDATE reference_codes
139
+ # SET ...
140
+ # WHERE EXISTS
141
+ # (SELECT 1
142
+ # FROM reference_codes cpk_child
143
+ # WHERE reference_codes.reference_type_id = cpk_child.reference_type_id AND
144
+ # reference_codes.reference_code = cpk_child.reference_code)
145
+ def cpk_exists_subquery(stmt)
146
+ arel_attributes = primary_keys.map do |key|
147
+ table[key]
148
+ end.to_composite_keys
149
+
150
+ # Clone the query
151
+ subselect = arel.clone
152
+
153
+ # Alias the table - we assume just one table
154
+ aliased_table = subselect.froms.first
155
+ aliased_table.table_alias = "cpk_child"
156
+
157
+ # Project - really we could just set this to "1"
158
+ subselect.projections = arel_attributes
159
+
160
+ # Setup correlation to the outer query via where clauses
161
+ primary_keys.map do |key|
162
+ outer_attribute = arel_table[key]
163
+ inner_attribute = aliased_table[key]
164
+ where = outer_attribute.eq(inner_attribute)
165
+ subselect.where(where)
166
+ end
167
+ stmt.wheres = [Arel::Nodes::Exists.new(subselect)]
168
+ end
169
+
170
+ # CPK. This is the old way CPK created subqueries and is used by MySql.
171
+ # MySQL does not support referencing the same table that is being UPDATEd or
172
+ # DELETEd in a subquery so we obfuscate it. The ugly query looks like this:
173
+ #
174
+ # UPDATE `reference_codes`
175
+ # SET ...
176
+ # WHERE (reference_codes.reference_type_id, reference_codes.reference_code) IN
177
+ # (SELECT reference_type_id,reference_code
178
+ # FROM (SELECT DISTINCT `reference_codes`.`reference_type_id`, `reference_codes`.`reference_code`
179
+ # FROM `reference_codes`) __active_record_temp)
180
+ def cpk_mysql_subquery(stmt)
181
+ arel_attributes = primary_keys.map do |key|
182
+ table[key]
183
+ end.to_composite_keys
184
+
185
+ subselect = arel.clone
186
+ subselect.projections = arel_attributes
187
+
188
+ # Materialize subquery by adding distinct
189
+ # to work with MySQL 5.7.6 which sets optimizer_switch='derived_merge=on'
190
+ subselect.distinct unless arel.limit || arel.offset || arel.orders.any?
191
+
192
+ key_name = arel_attributes.map(&:name).join(',')
193
+
194
+ manager = Arel::SelectManager.new(subselect.as("__active_record_temp")).project(Arel.sql(key_name))
195
+
196
+ stmt.wheres = [Arel::Nodes::In.new(arel_attributes, manager.ast)]
197
+ end
198
+ end
199
+ end
@@ -2,7 +2,7 @@ module CompositePrimaryKeys
2
2
  module VERSION
3
3
  MAJOR = 14
4
4
  MINOR = 0
5
- TINY = 6
5
+ TINY = 7
6
6
  STRING = [MAJOR, MINOR, TINY].join('.')
7
7
  end
8
8
  end