rom-sql 2.0.0.beta2 → 2.0.0.beta3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (170) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +66 -0
  3. data/lib/rom/plugins/relation/sql/postgres/explain.rb +54 -0
  4. data/lib/rom/sql.rb +1 -1
  5. data/lib/rom/sql/attribute.rb +17 -18
  6. data/lib/rom/sql/errors.rb +3 -0
  7. data/lib/rom/sql/extensions/mysql.rb +1 -1
  8. data/lib/rom/sql/extensions/mysql/type_builder.rb +28 -0
  9. data/lib/rom/sql/extensions/postgres.rb +3 -1
  10. data/lib/rom/sql/extensions/postgres/commands.rb +30 -13
  11. data/lib/rom/sql/extensions/postgres/{attributes_inferrer.rb → type_builder.rb} +24 -28
  12. data/lib/rom/sql/extensions/postgres/type_serializer.rb +39 -0
  13. data/lib/rom/sql/extensions/postgres/types.rb +24 -477
  14. data/lib/rom/sql/extensions/postgres/types/array.rb +163 -0
  15. data/lib/rom/sql/extensions/postgres/types/geometric.rb +135 -0
  16. data/lib/rom/sql/extensions/postgres/types/json.rb +235 -0
  17. data/lib/rom/sql/extensions/postgres/types/network.rb +15 -0
  18. data/lib/rom/sql/extensions/sqlite.rb +1 -1
  19. data/lib/rom/sql/extensions/sqlite/{attributes_inferrer.rb → type_builder.rb} +5 -5
  20. data/lib/rom/sql/extensions/sqlite/types.rb +8 -3
  21. data/lib/rom/sql/foreign_key.rb +17 -0
  22. data/lib/rom/sql/function.rb +86 -8
  23. data/lib/rom/sql/gateway.rb +26 -26
  24. data/lib/rom/sql/index.rb +4 -0
  25. data/lib/rom/sql/migration.rb +3 -3
  26. data/lib/rom/sql/migration/inline_runner.rb +9 -83
  27. data/lib/rom/sql/migration/migrator.rb +35 -12
  28. data/lib/rom/sql/migration/recorder.rb +21 -0
  29. data/lib/rom/sql/migration/runner.rb +115 -0
  30. data/lib/rom/sql/migration/schema_diff.rb +108 -53
  31. data/lib/rom/sql/migration/writer.rb +61 -0
  32. data/lib/rom/sql/relation.rb +2 -1
  33. data/lib/rom/sql/relation/reading.rb +63 -3
  34. data/lib/rom/sql/relation/writing.rb +38 -0
  35. data/lib/rom/sql/schema.rb +9 -3
  36. data/lib/rom/sql/schema/attributes_inferrer.rb +3 -119
  37. data/lib/rom/sql/schema/inferrer.rb +99 -18
  38. data/lib/rom/sql/schema/type_builder.rb +94 -0
  39. data/lib/rom/sql/type_dsl.rb +30 -0
  40. data/lib/rom/sql/type_extensions.rb +11 -6
  41. data/lib/rom/sql/type_serializer.rb +46 -0
  42. data/lib/rom/sql/types.rb +12 -0
  43. data/lib/rom/sql/version.rb +1 -1
  44. metadata +26 -244
  45. data/.codeclimate.yml +0 -15
  46. data/.gitignore +0 -17
  47. data/.rspec +0 -3
  48. data/.travis.yml +0 -39
  49. data/.yardopts +0 -2
  50. data/Gemfile +0 -33
  51. data/Guardfile +0 -24
  52. data/LICENSE.txt +0 -22
  53. data/Rakefile +0 -19
  54. data/circle.yml +0 -10
  55. data/lib/rom/sql/extensions/mysql/attributes_inferrer.rb +0 -10
  56. data/lib/rom/sql/relation/sequel_api.rb +0 -133
  57. data/log/.gitkeep +0 -0
  58. data/rom-sql.gemspec +0 -29
  59. data/spec/extensions/postgres/attribute_spec.rb +0 -217
  60. data/spec/extensions/postgres/integration_spec.rb +0 -59
  61. data/spec/extensions/postgres/types_spec.rb +0 -252
  62. data/spec/extensions/sqlite/types_spec.rb +0 -11
  63. data/spec/fixtures/migrations/20150403090603_create_carrots.rb +0 -8
  64. data/spec/integration/associations/many_to_many/custom_fks_spec.rb +0 -76
  65. data/spec/integration/associations/many_to_many/from_view_spec.rb +0 -88
  66. data/spec/integration/associations/many_to_many_spec.rb +0 -162
  67. data/spec/integration/associations/many_to_one/custom_fks_spec.rb +0 -64
  68. data/spec/integration/associations/many_to_one/from_view_spec.rb +0 -84
  69. data/spec/integration/associations/many_to_one/self_ref_spec.rb +0 -53
  70. data/spec/integration/associations/many_to_one_spec.rb +0 -117
  71. data/spec/integration/associations/one_to_many/custom_fks_spec.rb +0 -54
  72. data/spec/integration/associations/one_to_many/from_view_spec.rb +0 -57
  73. data/spec/integration/associations/one_to_many/self_ref_spec.rb +0 -54
  74. data/spec/integration/associations/one_to_many_spec.rb +0 -86
  75. data/spec/integration/associations/one_to_one_spec.rb +0 -69
  76. data/spec/integration/associations/one_to_one_through_spec.rb +0 -92
  77. data/spec/integration/auto_migrations/errors_spec.rb +0 -31
  78. data/spec/integration/auto_migrations/indexes_spec.rb +0 -253
  79. data/spec/integration/auto_migrations/managing_columns_spec.rb +0 -156
  80. data/spec/integration/auto_migrations/postgres/column_types_spec.rb +0 -63
  81. data/spec/integration/combine_with_spec.rb +0 -43
  82. data/spec/integration/commands/create_spec.rb +0 -304
  83. data/spec/integration/commands/delete_spec.rb +0 -84
  84. data/spec/integration/commands/update_spec.rb +0 -90
  85. data/spec/integration/commands/upsert_spec.rb +0 -83
  86. data/spec/integration/gateway_spec.rb +0 -107
  87. data/spec/integration/migration_spec.rb +0 -55
  88. data/spec/integration/plugins/associates/many_to_many_spec.rb +0 -69
  89. data/spec/integration/plugins/associates_spec.rb +0 -250
  90. data/spec/integration/plugins/auto_restrictions_spec.rb +0 -74
  91. data/spec/integration/relation_schema_spec.rb +0 -271
  92. data/spec/integration/schema/call_spec.rb +0 -24
  93. data/spec/integration/schema/inferrer/mysql_spec.rb +0 -45
  94. data/spec/integration/schema/inferrer/postgres_spec.rb +0 -203
  95. data/spec/integration/schema/inferrer/sqlite_spec.rb +0 -37
  96. data/spec/integration/schema/inferrer_spec.rb +0 -390
  97. data/spec/integration/schema/prefix_spec.rb +0 -16
  98. data/spec/integration/schema/qualified_spec.rb +0 -16
  99. data/spec/integration/schema/rename_spec.rb +0 -21
  100. data/spec/integration/schema/view_spec.rb +0 -29
  101. data/spec/integration/sequel_api_spec.rb +0 -36
  102. data/spec/integration/setup_spec.rb +0 -26
  103. data/spec/integration/support/active_support_notifications_spec.rb +0 -24
  104. data/spec/integration/support/rails_log_subscriber_spec.rb +0 -30
  105. data/spec/integration/wrap_spec.rb +0 -91
  106. data/spec/shared/accounts.rb +0 -48
  107. data/spec/shared/database_setup.rb +0 -70
  108. data/spec/shared/notes.rb +0 -23
  109. data/spec/shared/posts.rb +0 -34
  110. data/spec/shared/puppies.rb +0 -15
  111. data/spec/shared/relations.rb +0 -8
  112. data/spec/shared/users.rb +0 -32
  113. data/spec/shared/users_and_tasks.rb +0 -50
  114. data/spec/spec_helper.rb +0 -122
  115. data/spec/support/env_helper.rb +0 -25
  116. data/spec/support/helpers.rb +0 -24
  117. data/spec/support/oracle/create_users.sql +0 -7
  118. data/spec/support/oracle/set_sys_passwords.sql +0 -2
  119. data/spec/support/test_configuration.rb +0 -16
  120. data/spec/unit/attribute_spec.rb +0 -104
  121. data/spec/unit/function_spec.rb +0 -48
  122. data/spec/unit/gateway_spec.rb +0 -70
  123. data/spec/unit/logger_spec.rb +0 -14
  124. data/spec/unit/migration_tasks_spec.rb +0 -111
  125. data/spec/unit/migrator_spec.rb +0 -25
  126. data/spec/unit/order_dsl_spec.rb +0 -43
  127. data/spec/unit/plugin/associates_spec.rb +0 -94
  128. data/spec/unit/plugin/pagination_spec.rb +0 -91
  129. data/spec/unit/plugin/timestamp_spec.rb +0 -117
  130. data/spec/unit/projection_dsl_spec.rb +0 -110
  131. data/spec/unit/relation/assoc_spec.rb +0 -87
  132. data/spec/unit/relation/associations_spec.rb +0 -27
  133. data/spec/unit/relation/avg_spec.rb +0 -11
  134. data/spec/unit/relation/by_pk_spec.rb +0 -62
  135. data/spec/unit/relation/dataset_spec.rb +0 -50
  136. data/spec/unit/relation/distinct_spec.rb +0 -15
  137. data/spec/unit/relation/exclude_spec.rb +0 -11
  138. data/spec/unit/relation/exist_predicate_spec.rb +0 -25
  139. data/spec/unit/relation/exists_spec.rb +0 -18
  140. data/spec/unit/relation/fetch_spec.rb +0 -21
  141. data/spec/unit/relation/group_spec.rb +0 -61
  142. data/spec/unit/relation/having_spec.rb +0 -22
  143. data/spec/unit/relation/inner_join_spec.rb +0 -158
  144. data/spec/unit/relation/inspect_spec.rb +0 -11
  145. data/spec/unit/relation/instrument_spec.rb +0 -45
  146. data/spec/unit/relation/invert_spec.rb +0 -11
  147. data/spec/unit/relation/left_join_spec.rb +0 -55
  148. data/spec/unit/relation/lock_spec.rb +0 -93
  149. data/spec/unit/relation/map_spec.rb +0 -16
  150. data/spec/unit/relation/max_spec.rb +0 -11
  151. data/spec/unit/relation/min_spec.rb +0 -11
  152. data/spec/unit/relation/order_spec.rb +0 -51
  153. data/spec/unit/relation/pluck_spec.rb +0 -11
  154. data/spec/unit/relation/prefix_spec.rb +0 -29
  155. data/spec/unit/relation/primary_key_spec.rb +0 -27
  156. data/spec/unit/relation/project_spec.rb +0 -24
  157. data/spec/unit/relation/qualified_columns_spec.rb +0 -30
  158. data/spec/unit/relation/qualified_spec.rb +0 -25
  159. data/spec/unit/relation/read_spec.rb +0 -25
  160. data/spec/unit/relation/rename_spec.rb +0 -23
  161. data/spec/unit/relation/right_join_spec.rb +0 -57
  162. data/spec/unit/relation/select_append_spec.rb +0 -21
  163. data/spec/unit/relation/select_spec.rb +0 -40
  164. data/spec/unit/relation/sum_spec.rb +0 -11
  165. data/spec/unit/relation/union_spec.rb +0 -19
  166. data/spec/unit/relation/unique_predicate_spec.rb +0 -18
  167. data/spec/unit/relation/where_spec.rb +0 -133
  168. data/spec/unit/restriction_dsl_spec.rb +0 -34
  169. data/spec/unit/schema_spec.rb +0 -25
  170. data/spec/unit/types_spec.rb +0 -65
@@ -6,7 +6,6 @@ require 'rom/sql/transaction'
6
6
 
7
7
  require 'rom/sql/relation/reading'
8
8
  require 'rom/sql/relation/writing'
9
- require 'rom/sql/relation/sequel_api'
10
9
 
11
10
  module ROM
12
11
  module SQL
@@ -49,6 +48,8 @@ module ROM
49
48
 
50
49
  # @api private
51
50
  def self.define_default_views!
51
+ undef_method :by_pk if method_defined?(:by_pk)
52
+
52
53
  if schema.primary_key.size > 1
53
54
  # @!method by_pk(val1, val2)
54
55
  # Return a relation restricted by its composite primary key
@@ -105,8 +105,8 @@ module ROM
105
105
  # @return [Relation]
106
106
  #
107
107
  # @api public
108
- def qualified
109
- schema.qualified.(self)
108
+ def qualified(table_alias = nil)
109
+ schema.qualified(table_alias).(self)
110
110
  end
111
111
 
112
112
  # Return a list of qualified column names
@@ -768,7 +768,7 @@ module ROM
768
768
  # @option options [TrueClass, FalseClass] :all Set to true to use UNION ALL instead of UNION, so duplicate rows can occur
769
769
  # @option options [TrueClass, FalseClass] :from_self Set to false to not wrap the returned dataset in a #from_self, use with care.
770
770
  #
771
- # @return [Relation]
771
+ # @returRelation]
772
772
  #
773
773
  # @api public
774
774
  def union(relation, options = EMPTY_HASH, &block)
@@ -859,6 +859,66 @@ module ROM
859
859
  end
860
860
  end
861
861
 
862
+ # Restrict with rows from another relation.
863
+ # Accepts only SQL relations and uses the EXISTS
864
+ # clause under the hood
865
+ #
866
+ # @example using associations
867
+ # users.exists(tasks)
868
+ #
869
+ # @example using provided condition
870
+ # users.exists(tasks, tasks[:user_id] => users[:id])
871
+ #
872
+ # @param [SQL::Relation] other The other relation
873
+ # @param [Hash,Object] condition An optional join condition
874
+ #
875
+ # @return [SQL::Relation]
876
+ #
877
+ # @api public
878
+ def exists(other, condition = nil)
879
+ join_condition = condition || associations[other.name].join_keys
880
+ where(other.where(join_condition).dataset.exists)
881
+ end
882
+
883
+ # Process the dataset in batches.
884
+ # The method yields a relation restricted by a primary key value.
885
+ # This means it discards any order internally and uses the PK sort.
886
+ # Currently, works only with a single-column primary key.
887
+ #
888
+ # @example update in batches
889
+ # users.each_batch do |rel|
890
+ # rel.
891
+ # command(:update).
892
+ # call(name: users[:first_name].concat(users[:last_name])
893
+ # end
894
+ #
895
+ # @option [Integer] size The size of a batch (max number of records)
896
+ # @yieldparam [SQL::Relation]
897
+ #
898
+ # @api public
899
+ def each_batch(size: 1000)
900
+ pks = schema.primary_key
901
+
902
+ if pks.size > 1
903
+ raise ArgumentError, 'Composite primary keys are not supported yet'
904
+ end
905
+
906
+ source = order(pks[0]).limit(size)
907
+ rel = source
908
+
909
+ loop do
910
+ ids = rel.pluck(primary_key)
911
+
912
+ break if ids.empty?
913
+
914
+ yield(rel)
915
+
916
+ break if ids.size < size
917
+
918
+ rel = source.where(pks[0] > ids.last)
919
+ end
920
+ end
921
+
862
922
  private
863
923
 
864
924
  # Build a locking clause
@@ -77,6 +77,44 @@ module ROM
77
77
  def delete(*args, &block)
78
78
  dataset.delete(*args, &block)
79
79
  end
80
+
81
+ # Insert tuples from other relation
82
+ # NOTE: The method implicitly uses a transaction
83
+ #
84
+ # @example
85
+ # users.import(new_users)
86
+ #
87
+ # @overload import(other_sql_relation, options)
88
+ # If both relations uses the same gateway
89
+ # the INSERT ... SELECT statement will
90
+ # be used for importing the data
91
+ #
92
+ # @params [SQL::Relation] other_sql_relation
93
+ #
94
+ # @option [Integer] :slice
95
+ # Split loading into batches of provided size,
96
+ # every batch will be processed in a separate
97
+ # transaction block
98
+ #
99
+ # @overload import(other, options)
100
+ # Import data from another relation. The source
101
+ # relation will be materialized before loading
102
+ #
103
+ # @params [Relation] other
104
+ #
105
+ # @option [Integer] :slice
106
+ #
107
+ # @api public
108
+ def import(other, options = EMPTY_HASH)
109
+ if other.gateway.eql?(gateway)
110
+ columns = other.schema.map { |a| a.alias || a.name }
111
+ dataset.import(columns, other.dataset, options)
112
+ else
113
+ columns = other.schema.map { |a| a.alias || a.name }
114
+ keys = columns.map(&:to_sym)
115
+ dataset.import(columns, other.to_a.map { |record| record.to_h.values_at(*keys) }, options)
116
+ end
117
+ end
80
118
  end
81
119
  end
82
120
  end
@@ -6,15 +6,20 @@ require 'rom/sql/group_dsl'
6
6
  require 'rom/sql/projection_dsl'
7
7
  require 'rom/sql/restriction_dsl'
8
8
  require 'rom/sql/index'
9
+ require 'rom/sql/foreign_key'
9
10
  require 'rom/sql/schema/inferrer'
10
11
 
11
12
  module ROM
12
13
  module SQL
13
14
  class Schema < ROM::Schema
14
- # @!attribute [r] attributes
15
+ # @!attribute [r] indexes
15
16
  # @return [Array<Index>] Array with schema indexes
16
17
  option :indexes, default: -> { EMPTY_SET }
17
18
 
19
+ # @!attribute [r] foreign_keys
20
+ # @return [Array<ForeignKey>] Array with foreign keys
21
+ option :foreign_keys, default: -> { EMPTY_SET }
22
+
18
23
  # @api public
19
24
  def restriction(&block)
20
25
  RestrictionDSL.new(self).call(&block)
@@ -35,8 +40,8 @@ module ROM
35
40
  # @return [Schema]
36
41
  #
37
42
  # @api public
38
- def qualified
39
- new(map(&:qualified))
43
+ def qualified(table_alias = nil)
44
+ new(map { |attr| attr.qualified(table_alias) })
40
45
  end
41
46
 
42
47
  # Return a new schema with attributes restored to canonical form
@@ -100,6 +105,7 @@ module ROM
100
105
  # @api private
101
106
  def finalize_attributes!(options = EMPTY_HASH)
102
107
  super do
108
+ @attributes = map(&:qualified)
103
109
  initialize_primary_key_names
104
110
  end
105
111
  end
@@ -8,42 +8,11 @@ module ROM
8
8
  extend Dry::Core::ClassAttributes
9
9
  extend Initializer
10
10
 
11
- defines :ruby_type_mapping, :numeric_pk_type, :db_type, :registry
12
-
13
- class << self
14
- def inherited(klass)
15
- super
16
-
17
- registry[klass.db_type] = klass.new.freeze unless klass.name.nil?
18
- end
19
-
20
- def [](type)
21
- Class.new(self) { db_type(type) }
22
- end
23
-
24
- def get(db_type)
25
- registry[db_type]
26
- end
27
- end
11
+ defines :type_builders
28
12
 
29
13
  CONSTRAINT_DB_TYPE = 'add_constraint'.freeze
30
- DECIMAL_REGEX = /(?:decimal|numeric)\((\d+)(?:,\s*(\d+))?\)/.freeze
31
14
 
32
- registry Hash.new(new.freeze)
33
-
34
- ruby_type_mapping(
35
- integer: Types::Int,
36
- string: Types::String,
37
- time: Types::Time,
38
- date: Types::Date,
39
- datetime: Types::Time,
40
- boolean: Types::Bool,
41
- decimal: Types::Decimal,
42
- float: Types::Float,
43
- blob: Types::Blob
44
- ).freeze
45
-
46
- numeric_pk_type Types::Serial
15
+ option :type_builder
47
16
 
48
17
  option :attr_class, optional: true
49
18
 
@@ -52,10 +21,9 @@ module ROM
52
21
  dataset = schema.name.dataset
53
22
 
54
23
  columns = filter_columns(gateway.connection.schema(dataset))
55
- fks = fks_for(gateway, dataset)
56
24
 
57
25
  inferred = columns.map do |(name, definition)|
58
- type = build_type(**definition, foreign_key: fks[name])
26
+ type = type_builder.(definition)
59
27
 
60
28
  attr_class.new(type.meta(name: name, source: schema.name)) if type
61
29
  end.compact
@@ -70,94 +38,10 @@ module ROM
70
38
  self.class.new(options.merge(new_options))
71
39
  end
72
40
 
73
-
74
41
  # @api private
75
42
  def filter_columns(schema)
76
43
  schema.reject { |(_, definition)| definition[:db_type] == CONSTRAINT_DB_TYPE }
77
44
  end
78
-
79
- # @api private
80
- def build_type(primary_key:, db_type:, type:, allow_null:, foreign_key:, **rest)
81
- if primary_key
82
- map_pk_type(type, db_type)
83
- else
84
- mapped_type = map_type(type, db_type, rest)
85
-
86
- if mapped_type
87
- read_type = mapped_type.meta[:read]
88
- mapped_type = mapped_type.optional if allow_null
89
- mapped_type = mapped_type.meta(foreign_key: true, target: foreign_key) if foreign_key
90
-
91
- if read_type && allow_null
92
- mapped_type.meta(read: read_type.optional)
93
- elsif read_type
94
- mapped_type.meta(read: read_type)
95
- else
96
- mapped_type
97
- end
98
- end
99
- end
100
- end
101
-
102
- # @api private
103
- def map_pk_type(_ruby_type, _db_type)
104
- self.class.numeric_pk_type.meta(primary_key: true)
105
- end
106
-
107
- # @api private
108
- def map_type(ruby_type, db_type, **kw)
109
- type = self.class.ruby_type_mapping[ruby_type]
110
-
111
- if db_type.is_a?(String) && db_type.include?('numeric') || db_type.include?('decimal')
112
- map_decimal_type(db_type)
113
- elsif db_type.is_a?(String) && db_type.include?('char') && kw[:max_length]
114
- type.meta(limit: kw[:max_length])
115
- else
116
- type
117
- end
118
- end
119
-
120
- # @api private
121
- def fks_for(gateway, dataset)
122
- gateway.connection.foreign_key_list(dataset).each_with_object({}) do |definition, fks|
123
- column, fk = build_fk(definition)
124
-
125
- fks[column] = fk if fk
126
- end
127
- end
128
-
129
- # @api private
130
- def column_indexes(indexes, column)
131
- indexes.each_with_object(Set.new) do |(name, idx), set|
132
- set << name if idx[:columns][0] == column
133
- end
134
- end
135
-
136
- # @api private
137
- def build_fk(columns: , table: , **rest)
138
- if columns.size == 1
139
- [columns[0], table]
140
- else
141
- # We don't have support for multicolumn foreign keys
142
- columns[0]
143
- end
144
- end
145
-
146
- # @api private
147
- def map_decimal_type(type)
148
- precision = DECIMAL_REGEX.match(type)
149
-
150
- if precision
151
- prcsn, scale = precision[1..2].map(&:to_i)
152
-
153
- self.class.ruby_type_mapping[:decimal].meta(
154
- precision: prcsn,
155
- scale: scale
156
- )
157
- else
158
- self.class.ruby_type_mapping[:decimal]
159
- end
160
- end
161
45
  end
162
46
  end
163
47
  end
@@ -1,5 +1,6 @@
1
1
  require 'set'
2
2
 
3
+ require 'rom/sql/schema/type_builder'
3
4
  require 'rom/sql/schema/attributes_inferrer'
4
5
  require 'rom/sql/attribute'
5
6
 
@@ -8,8 +9,12 @@ module ROM
8
9
  class Schema < ROM::Schema
9
10
  # @api private
10
11
  class Inferrer < ROM::Schema::Inferrer
12
+ defines :type_builders
13
+
11
14
  attributes_inferrer -> (schema, gateway, options) do
12
- AttributesInferrer.get(gateway.database_type).with(options).(schema, gateway)
15
+ builder = TypeBuilder[gateway.database_type]
16
+ inferrer = AttributesInferrer.new(type_builder: builder, **options)
17
+ inferrer.(schema, gateway)
13
18
  end
14
19
 
15
20
  attr_class SQL::Attribute
@@ -18,39 +23,87 @@ module ROM
18
23
 
19
24
  option :raise_on_error, default: -> { true }
20
25
 
21
- FALLBACK_SCHEMA = { attributes: EMPTY_ARRAY, indexes: EMPTY_SET }.freeze
26
+ FALLBACK_SCHEMA = {
27
+ attributes: EMPTY_ARRAY,
28
+ indexes: EMPTY_SET,
29
+ foreign_keys: EMPTY_SET
30
+ }.freeze
22
31
 
23
32
  # @api private
24
33
  def call(schema, gateway)
25
- inferred = super
26
-
27
- indexes = get_indexes(gateway, schema, inferred[:attributes])
28
-
29
- { **inferred, indexes: indexes }
34
+ if enabled?
35
+ infer_from_database(gateway, schema, super)
36
+ else
37
+ infer_from_attributes(gateway, schema, super)
38
+ end
30
39
  rescue Sequel::Error => error
31
40
  on_error(schema.name, error)
32
- FALLBACK_SCHEMA
41
+ { **FALLBACK_SCHEMA, indexes: schema.indexes }
33
42
  end
34
43
 
35
44
  # @api private
36
- def get_indexes(gateway, schema, attributes)
37
- dataset = schema.name.dataset
45
+ def infer_from_database(gateway, schema, attributes:, **rest)
46
+ idx = attributes_index(attributes)
47
+ indexes = indexes_from_database(gateway, schema, idx)
48
+ foreign_keys = foreign_keys_from_database(gateway, schema, idx)
49
+
50
+ { **rest,
51
+ attributes: attributes.map { |attr| mark_fk(mark_indexed(attr, indexes), foreign_keys) },
52
+ foreign_keys: foreign_keys,
53
+ indexes: indexes }
54
+ end
38
55
 
39
- if enabled? && gateway.connection.respond_to?(:indexes)
40
- gateway.connection.indexes(dataset).map { |name, body|
41
- columns = body[:columns].map { |name|
42
- attributes.find { |attr| attr.name == name }.unwrap
43
- }
56
+ # @api private
57
+ def infer_from_attributes(gateway, schema, attributes:, **rest)
58
+ indexes = schema.indexes | indexes_from_attributes(attributes)
59
+ foreign_keys = foreign_keys_from_attributes(attributes)
60
+
61
+ { **rest,
62
+ attributes: attributes.map { |attr| mark_indexed(attr, indexes) },
63
+ foreign_keys: foreign_keys,
64
+ indexes: indexes }
65
+ end
44
66
 
45
- SQL::Index.new(columns, name: name, unique: body[:unique])
67
+ # @api private
68
+ def indexes_from_database(gateway, schema, attributes)
69
+ if gateway.connection.respond_to?(:indexes)
70
+ dataset = schema.name.dataset
71
+
72
+ gateway.connection.indexes(dataset).map { |index_name, columns:, unique:, **rest|
73
+ attrs = columns.map { |name| attributes[name] }
74
+
75
+ SQL::Index.new(attrs, name: index_name, unique: unique)
46
76
  }.to_set
47
77
  else
48
- schema.indexes | indexes_from_attributes(attributes)
78
+ EMPTY_SET
49
79
  end
50
80
  end
51
81
 
82
+ # @api private
83
+ def foreign_keys_from_database(gateway, schema, attributes)
84
+ dataset = schema.name.dataset
85
+
86
+ gateway.connection.foreign_key_list(dataset).map { |columns:, table:, key:, **rest|
87
+ attrs = columns.map { |name| attributes[name] }
88
+
89
+ SQL::ForeignKey.new(attrs, table, parent_keys: key)
90
+ }.to_set
91
+ end
92
+
93
+ # @api private
52
94
  def indexes_from_attributes(attributes)
53
- attributes.select(&:indexed?).map { |attr| SQL::Index.new([attr.unwrap]) }.to_set
95
+ attributes.
96
+ select(&:indexed?).
97
+ map { |attr| SQL::Index.new([attr.unwrap]) }.
98
+ to_set
99
+ end
100
+
101
+ # @api private
102
+ def foreign_keys_from_attributes(attributes)
103
+ attributes.
104
+ select(&:foreign_key?).
105
+ map { |attr| SQL::ForeignKey.new([attr.unwrap], attr.target) }.
106
+ to_set
54
107
  end
55
108
 
56
109
  # @api private
@@ -60,6 +113,34 @@ module ROM
60
113
 
61
114
  private
62
115
 
116
+ def attributes_index(attributes)
117
+ Hash.new { |idx, name| idx[name] = attributes.find { |attr| attr.name == name }.unwrap }
118
+ end
119
+
120
+ # @private
121
+ def mark_indexed(attribute, indexes)
122
+ if !attribute.indexed? && indexes.any? { |index| index.can_access?(attribute) }
123
+ attribute.indexed
124
+ else
125
+ attribute
126
+ end
127
+ end
128
+
129
+ # @private
130
+ def mark_fk(attribute, foreign_keys)
131
+ if attribute.foreign_key?
132
+ attribute
133
+ else
134
+ foreign_key = foreign_keys.find { |fk| fk.attributes.map(&:name) == [attribute.name] }
135
+
136
+ if foreign_key.nil?
137
+ attribute
138
+ else
139
+ attribute.meta(foreign_key: true, target: foreign_key.parent_table)
140
+ end
141
+ end
142
+ end
143
+
63
144
  # @api private
64
145
  def on_error(dataset, e)
65
146
  if raise_on_error