rom-sql 1.3.5 → 2.0.0.beta1

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 (103) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +13 -7
  3. data/Gemfile +7 -5
  4. data/lib/rom/plugins/relation/sql/auto_restrictions.rb +11 -17
  5. data/lib/rom/sql.rb +3 -2
  6. data/lib/rom/sql/associations.rb +5 -0
  7. data/lib/rom/sql/associations/core.rb +20 -0
  8. data/lib/rom/sql/associations/many_to_many.rb +83 -0
  9. data/lib/rom/sql/associations/many_to_one.rb +55 -0
  10. data/lib/rom/sql/associations/one_to_many.rb +31 -0
  11. data/lib/rom/sql/{association → associations}/one_to_one.rb +3 -2
  12. data/lib/rom/sql/{association → associations}/one_to_one_through.rb +3 -2
  13. data/lib/rom/sql/associations/self_ref.rb +39 -0
  14. data/lib/rom/sql/attribute.rb +44 -54
  15. data/lib/rom/sql/errors.rb +2 -0
  16. data/lib/rom/sql/extensions/mysql.rb +1 -1
  17. data/lib/rom/sql/extensions/mysql/attributes_inferrer.rb +10 -0
  18. data/lib/rom/sql/extensions/postgres.rb +1 -1
  19. data/lib/rom/sql/extensions/postgres/{inferrer.rb → attributes_inferrer.rb} +4 -4
  20. data/lib/rom/sql/extensions/postgres/types.rb +9 -19
  21. data/lib/rom/sql/extensions/sqlite.rb +1 -1
  22. data/lib/rom/sql/extensions/sqlite/{inferrer.rb → attributes_inferrer.rb} +2 -2
  23. data/lib/rom/sql/gateway.rb +29 -30
  24. data/lib/rom/sql/index.rb +13 -0
  25. data/lib/rom/sql/migration.rb +10 -0
  26. data/lib/rom/sql/migration/inline_runner.rb +86 -0
  27. data/lib/rom/sql/migration/migrator.rb +17 -0
  28. data/lib/rom/sql/migration/schema_diff.rb +177 -0
  29. data/lib/rom/sql/plugin/associates.rb +11 -45
  30. data/lib/rom/sql/plugin/pagination.rb +4 -4
  31. data/lib/rom/sql/relation.rb +22 -42
  32. data/lib/rom/sql/relation/reading.rb +3 -3
  33. data/lib/rom/sql/schema.rb +14 -21
  34. data/lib/rom/sql/schema/associations_dsl.rb +7 -6
  35. data/lib/rom/sql/schema/attributes_inferrer.rb +164 -0
  36. data/lib/rom/sql/schema/inferrer.rb +40 -141
  37. data/lib/rom/sql/type_extensions.rb +44 -0
  38. data/lib/rom/sql/version.rb +1 -1
  39. data/lib/rom/sql/wrap.rb +25 -0
  40. data/rom-sql.gemspec +2 -2
  41. data/spec/integration/{association → associations}/many_to_many/custom_fks_spec.rb +4 -2
  42. data/spec/integration/{association → associations}/many_to_many/from_view_spec.rb +2 -2
  43. data/spec/integration/{association → associations}/many_to_many_spec.rb +25 -30
  44. data/spec/integration/{association → associations}/many_to_one/custom_fks_spec.rb +5 -3
  45. data/spec/integration/{association → associations}/many_to_one/from_view_spec.rb +3 -3
  46. data/spec/integration/{association → associations}/many_to_one/self_ref_spec.rb +2 -2
  47. data/spec/integration/{association → associations}/many_to_one_spec.rb +20 -38
  48. data/spec/integration/{association → associations}/one_to_many/custom_fks_spec.rb +4 -2
  49. data/spec/integration/{association → associations}/one_to_many/from_view_spec.rb +2 -2
  50. data/spec/integration/{association → associations}/one_to_many/self_ref_spec.rb +2 -2
  51. data/spec/integration/{association → associations}/one_to_many_spec.rb +24 -11
  52. data/spec/integration/{association → associations}/one_to_one_spec.rb +13 -9
  53. data/spec/integration/{association → associations}/one_to_one_through_spec.rb +15 -11
  54. data/spec/integration/auto_migrations/errors_spec.rb +31 -0
  55. data/spec/integration/auto_migrations/indexes_spec.rb +109 -0
  56. data/spec/integration/auto_migrations/managing_columns_spec.rb +156 -0
  57. data/spec/integration/auto_migrations/postgres/column_types_spec.rb +63 -0
  58. data/spec/integration/commands/create_spec.rb +2 -4
  59. data/spec/integration/commands/delete_spec.rb +2 -2
  60. data/spec/integration/commands/update_spec.rb +2 -0
  61. data/spec/integration/graph_spec.rb +9 -3
  62. data/spec/integration/plugins/associates_spec.rb +16 -55
  63. data/spec/integration/plugins/auto_restrictions_spec.rb +0 -11
  64. data/spec/integration/relation_schema_spec.rb +49 -25
  65. data/spec/integration/schema/inferrer/postgres_spec.rb +1 -1
  66. data/spec/integration/schema/inferrer_spec.rb +7 -18
  67. data/spec/integration/setup_spec.rb +4 -0
  68. data/spec/integration/{plugins/auto_wrap_spec.rb → wrap_spec.rb} +13 -36
  69. data/spec/shared/accounts.rb +4 -0
  70. data/spec/shared/database_setup.rb +2 -1
  71. data/spec/shared/notes.rb +2 -0
  72. data/spec/shared/posts.rb +2 -0
  73. data/spec/shared/puppies.rb +2 -0
  74. data/spec/shared/relations.rb +2 -2
  75. data/spec/shared/users.rb +2 -0
  76. data/spec/shared/users_and_tasks.rb +4 -0
  77. data/spec/spec_helper.rb +3 -6
  78. data/spec/support/helpers.rb +11 -8
  79. data/spec/support/test_configuration.rb +16 -0
  80. data/spec/unit/plugin/associates_spec.rb +5 -10
  81. data/spec/unit/plugin/pagination_spec.rb +9 -9
  82. data/spec/unit/plugin/timestamp_spec.rb +9 -9
  83. data/spec/unit/relation/dataset_spec.rb +7 -5
  84. data/spec/unit/relation/inner_join_spec.rb +2 -15
  85. data/spec/unit/relation/primary_key_spec.rb +1 -1
  86. data/spec/unit/schema_spec.rb +6 -4
  87. metadata +65 -70
  88. data/lib/rom/plugins/relation/sql/auto_combine.rb +0 -71
  89. data/lib/rom/plugins/relation/sql/auto_wrap.rb +0 -62
  90. data/lib/rom/sql/association.rb +0 -103
  91. data/lib/rom/sql/association/many_to_many.rb +0 -119
  92. data/lib/rom/sql/association/many_to_one.rb +0 -73
  93. data/lib/rom/sql/association/name.rb +0 -78
  94. data/lib/rom/sql/association/one_to_many.rb +0 -60
  95. data/lib/rom/sql/extensions/mysql/inferrer.rb +0 -10
  96. data/lib/rom/sql/qualified_attribute.rb +0 -53
  97. data/lib/rom/sql/schema/dsl.rb +0 -75
  98. data/spec/unit/association/many_to_many_spec.rb +0 -89
  99. data/spec/unit/association/many_to_one_spec.rb +0 -81
  100. data/spec/unit/association/name_spec.rb +0 -68
  101. data/spec/unit/association/one_to_many_spec.rb +0 -82
  102. data/spec/unit/association/one_to_one_spec.rb +0 -83
  103. data/spec/unit/association/one_to_one_through_spec.rb +0 -69
@@ -12,6 +12,8 @@ module ROM
12
12
  CheckConstraintError = Class.new(ConstraintError)
13
13
  UnknownDBTypeError = Class.new(StandardError)
14
14
  MissingPrimaryKeyError = Class.new(StandardError)
15
+ MigrationError = Class.new(StandardError)
16
+ UnsupportedConversion = Class.new(MigrationError)
15
17
 
16
18
  ERROR_MAP = {
17
19
  Sequel::DatabaseError => DatabaseError,
@@ -1 +1 @@
1
- require 'rom/sql/extensions/mysql/inferrer'
1
+ require 'rom/sql/extensions/mysql/attributes_inferrer'
@@ -0,0 +1,10 @@
1
+ require 'rom/sql/schema/attributes_inferrer'
2
+
3
+ module ROM
4
+ module SQL
5
+ class Schema
6
+ class MysqlInferrer < AttributesInferrer[:mysql]
7
+ end
8
+ end
9
+ end
10
+ end
@@ -1,3 +1,3 @@
1
1
  require 'rom/sql/extensions/postgres/commands'
2
2
  require 'rom/sql/extensions/postgres/types'
3
- require 'rom/sql/extensions/postgres/inferrer'
3
+ require 'rom/sql/extensions/postgres/attributes_inferrer'
@@ -1,11 +1,11 @@
1
1
  require 'set'
2
- require 'rom/sql/schema/inferrer'
2
+ require 'rom/sql/schema/attributes_inferrer'
3
3
  require 'rom/sql/extensions/postgres/types'
4
4
 
5
5
  module ROM
6
6
  module SQL
7
7
  class Schema
8
- class PostgresInferrer < Inferrer[:postgres]
8
+ class PostgresInferrer < AttributesInferrer[:postgres]
9
9
  defines :db_numeric_types, :db_type_mapping, :db_array_type_matcher
10
10
 
11
11
  db_numeric_types %w(
@@ -34,7 +34,7 @@ module ROM
34
34
  'path' => Types::PG::PathT
35
35
  ).freeze
36
36
 
37
- db_array_type_matcher '[]'.freeze
37
+ db_array_type_matcher Sequel::Postgres::PGArray::EMPTY_BRACKET
38
38
 
39
39
  private
40
40
 
@@ -50,7 +50,7 @@ module ROM
50
50
 
51
51
  def map_type(ruby_type, db_type, enum_values: nil, **_)
52
52
  if db_type.end_with?(self.class.db_array_type_matcher)
53
- Types::PG::Array(db_type[0...db_type.size-2])
53
+ Types::PG::Array(db_type)
54
54
  elsif enum_values
55
55
  Types::String.enum(*enum_values)
56
56
  else
@@ -1,7 +1,9 @@
1
- require 'dry-types'
1
+ require 'dry/types'
2
2
  require 'sequel'
3
3
  require 'ipaddr'
4
4
 
5
+ require 'rom/sql/type_extensions'
6
+
5
7
  Sequel.extension(*%i(pg_array pg_array_ops pg_json pg_json_ops pg_hstore))
6
8
 
7
9
  module ROM
@@ -16,16 +18,8 @@ module ROM
16
18
 
17
19
  Array = Types.Definition(Sequel::Postgres::PGArray)
18
20
 
19
- @array_types = ::Hash.new do |hash, type|
20
- name = "#{ type }[]"
21
- array_type = Array.constructor(-> (v) { Sequel.pg_array(v, type) }).
22
- meta(type: name, db_type: name, database: 'postgres')
23
- Attribute::TypeExtensions.register(array_type) { include ArrayMethods }
24
- hash[type] = array_type
25
- end
26
-
27
21
  def self.Array(db_type)
28
- @array_types[db_type]
22
+ Array.constructor(-> (v) { Sequel.pg_array(v, db_type) }).meta(type: db_type)
29
23
  end
30
24
 
31
25
  # @!parse
@@ -117,7 +111,7 @@ module ROM
117
111
  # #
118
112
  # # @api public
119
113
  # end
120
- module ArrayMethods
114
+ TypeExtensions.register(Array.constructor -> { }) do
121
115
  def contain(type, expr, other)
122
116
  Attribute[Types::Bool].meta(sql_expr: expr.pg_array.contains(type[other]))
123
117
  end
@@ -155,10 +149,6 @@ module ROM
155
149
  end
156
150
  end
157
151
 
158
- Attribute::TypeExtensions.register(Array.constructor -> { }) do
159
- include ArrayMethods
160
- end
161
-
162
152
  # JSON
163
153
 
164
154
  JSONArray = Types.Constructor(Sequel::Postgres::JSONArray, &Sequel.method(:pg_json))
@@ -167,7 +157,7 @@ module ROM
167
157
 
168
158
  JSONOp = Types.Constructor(Sequel::Postgres::JSONOp, &Sequel.method(:pg_json))
169
159
 
170
- JSON = (JSONArray | JSONHash | JSONOp).meta(database: 'postgres', db_type: 'jsonb')
160
+ JSON = JSONArray | JSONHash | JSONOp
171
161
 
172
162
  # JSONB
173
163
 
@@ -177,7 +167,7 @@ module ROM
177
167
 
178
168
  JSONBOp = Types.Constructor(Sequel::Postgres::JSONBOp, &Sequel.method(:pg_jsonb))
179
169
 
180
- JSONB = (JSONBArray | JSONBHash | JSONBOp).meta(database: 'postgres', db_type: 'jsonb')
170
+ JSONB = JSONBArray | JSONBHash | JSONBOp
181
171
 
182
172
  # @!parse
183
173
  # class ROM::SQL::Attribute
@@ -343,11 +333,11 @@ module ROM
343
333
  end
344
334
  end
345
335
 
346
- Attribute::TypeExtensions.register(JSON) do
336
+ TypeExtensions.register(JSON) do
347
337
  include JSONMethods[JSON, :pg_json.to_proc]
348
338
  end
349
339
 
350
- Attribute::TypeExtensions.register(JSONB) do
340
+ TypeExtensions.register(JSONB) do
351
341
  include JSONMethods[JSONB, :pg_jsonb.to_proc]
352
342
 
353
343
  def contain(type, expr, value)
@@ -1,2 +1,2 @@
1
1
  require 'rom/sql/extensions/sqlite/types'
2
- require 'rom/sql/extensions/sqlite/inferrer'
2
+ require 'rom/sql/extensions/sqlite/attributes_inferrer'
@@ -1,9 +1,9 @@
1
- require 'rom/sql/schema/inferrer'
1
+ require 'rom/sql/schema/attributes_inferrer'
2
2
 
3
3
  module ROM
4
4
  module SQL
5
5
  class Schema
6
- class SqliteInferrer < Inferrer[:sqlite]
6
+ class SqliteInferrer < AttributesInferrer[:sqlite]
7
7
  NO_TYPE = EMPTY_STRING
8
8
 
9
9
  def map_type(_, db_type, **_kw)
@@ -31,6 +31,23 @@ module ROM
31
31
  # @return [Hash] Options used for connection
32
32
  attr_reader :options
33
33
 
34
+ subscribe('configuration.commands.class.before_build') do |event|
35
+ klass = event[:command]
36
+ dataset = event[:dataset]
37
+ type = dataset.db.database_type
38
+
39
+ if type == :postgres
40
+ ext =
41
+ if klass < Commands::Create
42
+ Commands::Postgres::Create
43
+ elsif klass < Commands::Update
44
+ Commands::Postgres::Update
45
+ end
46
+
47
+ klass.send(:include, ext) if ext
48
+ end
49
+ end
50
+
34
51
  # Initialize an SQL gateway
35
52
  #
36
53
  # Gateways are typically initialized via ROM::Configuration object, gateway constructor
@@ -153,31 +170,6 @@ module ROM
153
170
  schema.include?(name)
154
171
  end
155
172
 
156
- # Extend the command class with database-specific behavior
157
- #
158
- # @param [Class] klass Command class
159
- # @param [Sequel::Dataset] dataset A dataset that will be used
160
- #
161
- # Note: Currently, only postgres is supported.
162
- #
163
- # @api public
164
- def extend_command_class(klass, dataset)
165
- type = dataset.db.database_type
166
-
167
- if type == :postgres
168
- ext =
169
- if klass < Commands::Create
170
- Commands::Postgres::Create
171
- elsif klass < Commands::Update
172
- Commands::Postgres::Update
173
- end
174
-
175
- klass.send(:include, ext) if ext
176
- end
177
-
178
- klass
179
- end
180
-
181
173
  # Create a table using the configured connection
182
174
  #
183
175
  # @api public
@@ -201,6 +193,15 @@ module ROM
201
193
  @schema ||= connection.tables
202
194
  end
203
195
 
196
+ # Underlying database type
197
+ #
198
+ # @return [Symbol]
199
+ #
200
+ # @api public
201
+ def database_type
202
+ @database_type ||= connection.database_type.to_sym
203
+ end
204
+
204
205
  private
205
206
 
206
207
  # Connect to database or reuse established connection instance
@@ -221,13 +222,11 @@ module ROM
221
222
  #
222
223
  # @api private
223
224
  def load_extensions(exts)
224
- db_type = connection.database_type.to_sym
225
-
226
- if ROM::SQL.available_extension?(db_type)
227
- ROM::SQL.load_extensions(db_type)
225
+ if ROM::SQL.available_extension?(database_type)
226
+ ROM::SQL.load_extensions(database_type)
228
227
  end
229
228
 
230
- extensions = (CONNECTION_EXTENSIONS.fetch(db_type, EMPTY_ARRAY) + exts).uniq
229
+ extensions = (CONNECTION_EXTENSIONS.fetch(database_type, EMPTY_ARRAY) + exts).uniq
231
230
  connection.extension(*extensions)
232
231
 
233
232
  # this will be default in Sequel 5.0.0 and since we don't rely
@@ -0,0 +1,13 @@
1
+ module ROM
2
+ module SQL
3
+ # @api private
4
+ class Index
5
+ extend Initializer
6
+ include Dry::Equalizer(:attributes, :name)
7
+
8
+ param :attributes
9
+
10
+ option :name, optional: true
11
+ end
12
+ end
13
+ end
@@ -1,4 +1,5 @@
1
1
  require 'rom/sql/migration/migrator'
2
+ require 'rom/sql/migration/schema_diff'
2
3
 
3
4
  module ROM
4
5
  module SQL
@@ -135,6 +136,15 @@ module ROM
135
136
  migrator.run(options)
136
137
  }
137
138
  end
139
+
140
+ # @api public
141
+ def auto_migrate!(conf)
142
+ schemas = conf.relation_classes(self).map do |klass|
143
+ klass.schema || klass.schema_proc.call.finalize_attributes!(gateway: self)
144
+ end
145
+
146
+ migrator.auto_migrate!(self, schemas)
147
+ end
138
148
  end
139
149
  end
140
150
  end
@@ -0,0 +1,86 @@
1
+ module ROM
2
+ module SQL
3
+ module Migration
4
+ class Migrator
5
+ # @api private
6
+ class InlineRunner
7
+ attr_reader :gateway
8
+
9
+ def initialize(gateway)
10
+ @gateway = gateway
11
+ end
12
+
13
+ def call(changes)
14
+ changes.each do |diff|
15
+ apply(diff)
16
+ end
17
+ end
18
+
19
+ def apply(diff)
20
+ case diff
21
+ when SchemaDiff::TableCreated
22
+ create_table(diff)
23
+ when SchemaDiff::TableAltered
24
+ alter_table(diff)
25
+ else
26
+ raise NotImplementedError
27
+ end
28
+ end
29
+
30
+ def create_table(diff)
31
+ gateway.create_table(diff.table_name) do
32
+ diff.attributes.each do |attribute|
33
+ if attribute.primary_key?
34
+ primary_key attribute.name
35
+ else
36
+ column attribute.name, attribute.type, null: attribute.null?
37
+ end
38
+ end
39
+
40
+ diff.indexes.each do |idx|
41
+ index idx.attribute
42
+ end
43
+ end
44
+ end
45
+
46
+ def alter_table(diff)
47
+ gateway.connection.alter_table(diff.table_name) do
48
+ diff.attribute_changes.each do |attribute|
49
+ case attribute
50
+ when SchemaDiff::AttributeAdded
51
+ add_column attribute.name, attribute.type, null: attribute.null?
52
+ when SchemaDiff::AttributeRemoved
53
+ drop_column attribute.name
54
+ when SchemaDiff::AttributeChanged
55
+ if attribute.type_changed?
56
+ from, to = attribute.to_a.map(&attribute.method(:unwrap))
57
+ raise UnsupportedConversion.new(
58
+ "Don't know how to convert #{ from.inspect } to #{ to.inspect }"
59
+ )
60
+ end
61
+
62
+ if attribute.nullability_changed?
63
+ if attribute.null?
64
+ set_column_allow_null attribute.name
65
+ else
66
+ set_column_not_null attribute.name
67
+ end
68
+ end
69
+ end
70
+ end
71
+
72
+ diff.index_changes.each do |index|
73
+ case index
74
+ when SchemaDiff::IndexAdded
75
+ add_index index.attribute
76
+ when SchemaDiff::IndexRemoved
77
+ drop_index index.attribute
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
@@ -2,6 +2,8 @@ require 'pathname'
2
2
 
3
3
  require 'rom/types'
4
4
  require 'rom/initializer'
5
+ require 'rom/sql/migration'
6
+ require 'rom/sql/migration/inline_runner'
5
7
 
6
8
  module ROM
7
9
  module SQL
@@ -53,6 +55,21 @@ module ROM
53
55
  def migration_file_content
54
56
  File.read(Pathname(__FILE__).dirname.join('template.rb').realpath)
55
57
  end
58
+
59
+ # @api private
60
+ def diff(gateway, inferrer, target)
61
+ empty = SQL::Schema.define(target.name)
62
+ current = target.with(inferrer.(empty, gateway))
63
+
64
+ SchemaDiff.new.(current, target)
65
+ end
66
+
67
+ def auto_migrate!(gateway, schemas)
68
+ runner = InlineRunner.new(gateway)
69
+ inferrer = ROM::SQL::Schema::Inferrer.new.suppress_errors
70
+ changes = schemas.map { |schema| diff(gateway, inferrer, schema) }.reject(&:empty?)
71
+ runner.(changes)
72
+ end
56
73
  end
57
74
  end
58
75
  end
@@ -0,0 +1,177 @@
1
+ module ROM
2
+ module SQL
3
+ module Migration
4
+ class SchemaDiff
5
+ class TableDiff
6
+ attr_reader :current_schema, :target_schema
7
+
8
+ def initialize(current_schema: nil, target_schema: nil)
9
+ @current_schema = current_schema
10
+ @target_schema = target_schema
11
+ end
12
+
13
+ def empty?
14
+ false
15
+ end
16
+
17
+ def table_name
18
+ target_schema.name.dataset
19
+ end
20
+ end
21
+
22
+ class Empty < TableDiff
23
+ def empty?
24
+ true
25
+ end
26
+ end
27
+
28
+ class TableCreated < TableDiff
29
+ alias_method :schema, :target_schema
30
+ attr_reader :attributes, :indexes
31
+
32
+ def initialize(attributes:, indexes: EMPTY_ARRAY, **rest)
33
+ super(rest)
34
+
35
+ @attributes = attributes
36
+ @indexes = indexes
37
+ end
38
+ end
39
+
40
+ class TableAltered < TableDiff
41
+ attr_reader :attribute_changes, :index_changes
42
+
43
+ def initialize(attribute_changes: EMPTY_ARRAY, index_changes: EMPTY_ARRAY, **rest)
44
+ super(rest)
45
+
46
+ @attribute_changes = attribute_changes
47
+ @index_changes = index_changes
48
+ end
49
+ end
50
+
51
+ class AttributeDiff
52
+ attr_reader :attr
53
+
54
+ def initialize(attr)
55
+ @attr = attr
56
+ end
57
+
58
+ def name
59
+ attr.name
60
+ end
61
+
62
+ def null?
63
+ attr.optional?
64
+ end
65
+
66
+ def primary_key?
67
+ attr.primary_key?
68
+ end
69
+
70
+ def unwrap(type)
71
+ type.optional? ? SQL::Attribute[type.right].meta(type.meta) : type
72
+ end
73
+ end
74
+
75
+ class AttributeAdded < AttributeDiff
76
+ def type
77
+ unwrap(attr).primitive
78
+ end
79
+ end
80
+
81
+ class AttributeRemoved < AttributeDiff
82
+ end
83
+
84
+ class AttributeChanged < AttributeDiff
85
+ attr_reader :current
86
+ alias_method :target, :attr
87
+
88
+ def initialize(current, target)
89
+ super(target)
90
+
91
+ @current = current
92
+ end
93
+
94
+ def to_a
95
+ [current, target]
96
+ end
97
+
98
+ def nullability_changed?
99
+ current.optional? ^ target.optional?
100
+ end
101
+
102
+ def type_changed?
103
+ unwrap(current).meta(index: Set.new) != unwrap(target).meta(index: Set.new)
104
+ end
105
+ end
106
+
107
+ class IndexDiff
108
+ attr_reader :index
109
+
110
+ def initialize(index)
111
+ @index = index
112
+ end
113
+
114
+ def attribute
115
+ index.attributes[0].name
116
+ end
117
+ end
118
+
119
+ class IndexAdded < IndexDiff
120
+ end
121
+
122
+ class IndexRemoved < IndexDiff
123
+ end
124
+
125
+ def call(current, target)
126
+ if current.empty?
127
+ TableCreated.new(
128
+ target_schema: target,
129
+ attributes: target.map { |attr| AttributeAdded.new(attr) },
130
+ indexes: target.indexes.map { |idx| IndexAdded.new(idx) }
131
+ )
132
+ else
133
+ attribute_changes = compare_attributes(current.to_h, target.to_h)
134
+ index_changes = compare_indexes(current, target)
135
+
136
+ if attribute_changes.empty? && index_changes.empty?
137
+ Empty.new(current_schema: current, target_schema: target)
138
+ else
139
+ TableAltered.new(
140
+ current_schema: current,
141
+ target_schema: target,
142
+ attribute_changes: attribute_changes,
143
+ index_changes: index_changes
144
+ )
145
+ end
146
+ end
147
+ end
148
+
149
+ def compare_attributes(current, target)
150
+ changed_attributes = target.select { |name, attr|
151
+ current.key?(name) && current[name] != attr
152
+ }.map { |name, target_attr|
153
+ [name, [current[name], target_attr]]
154
+ }.to_h
155
+ added_attributes = target.select { |name, _| !current.key?(name) }
156
+ removed_attributes = current.select { |name, _| !target.key?(name) }
157
+
158
+ removed_attributes.values.map { |attr| AttributeRemoved.new(attr) } +
159
+ added_attributes.values.map { |attr| AttributeAdded.new(attr) } +
160
+ changed_attributes.values.map { |attrs| AttributeChanged.new(*attrs) }
161
+ end
162
+
163
+ def compare_indexes(current, target)
164
+ added_indexes = target.indexes.reject { |idx|
165
+ current.indexes.any? { |curr_idx| curr_idx.attributes == idx.attributes }
166
+ }
167
+ removed_indexes = current.indexes.select { |idx|
168
+ target.indexes.none? { |tgt_idx| idx.attributes == tgt_idx }
169
+ }
170
+
171
+ removed_indexes.map { |idx| IndexRemoved.new(idx) } +
172
+ added_indexes.map { |idx| IndexAdded.new(idx) }
173
+ end
174
+ end
175
+ end
176
+ end
177
+ end