rom-sql 1.3.5 → 2.0.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
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