rom-sql 2.5.0 → 3.3.0

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 (93) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +456 -278
  3. data/LICENSE +20 -0
  4. data/README.md +14 -24
  5. data/lib/rom-sql.rb +2 -0
  6. data/lib/rom/plugins/relation/sql/auto_restrictions.rb +2 -0
  7. data/lib/rom/plugins/relation/sql/instrumentation.rb +2 -0
  8. data/lib/rom/plugins/relation/sql/postgres/explain.rb +6 -7
  9. data/lib/rom/plugins/relation/sql/postgres/full_text_search.rb +53 -0
  10. data/lib/rom/plugins/relation/sql/postgres/streaming.rb +97 -0
  11. data/lib/rom/sql.rb +2 -0
  12. data/lib/rom/sql/associations.rb +2 -0
  13. data/lib/rom/sql/associations/core.rb +10 -0
  14. data/lib/rom/sql/associations/many_to_many.rb +10 -2
  15. data/lib/rom/sql/associations/many_to_one.rb +2 -0
  16. data/lib/rom/sql/associations/one_to_many.rb +2 -0
  17. data/lib/rom/sql/associations/one_to_one.rb +2 -0
  18. data/lib/rom/sql/associations/one_to_one_through.rb +2 -0
  19. data/lib/rom/sql/associations/self_ref.rb +2 -0
  20. data/lib/rom/sql/attribute.rb +87 -29
  21. data/lib/rom/sql/attribute_aliasing.rb +88 -0
  22. data/lib/rom/sql/attribute_wrapping.rb +30 -0
  23. data/lib/rom/sql/commands.rb +2 -0
  24. data/lib/rom/sql/commands/create.rb +2 -0
  25. data/lib/rom/sql/commands/delete.rb +2 -0
  26. data/lib/rom/sql/commands/error_wrapper.rb +2 -0
  27. data/lib/rom/sql/commands/update.rb +2 -0
  28. data/lib/rom/sql/dsl.rb +39 -1
  29. data/lib/rom/sql/error.rb +2 -0
  30. data/lib/rom/sql/errors.rb +2 -0
  31. data/lib/rom/sql/extensions.rb +2 -0
  32. data/lib/rom/sql/extensions/active_support_notifications.rb +2 -0
  33. data/lib/rom/sql/extensions/mysql.rb +2 -0
  34. data/lib/rom/sql/extensions/mysql/type_builder.rb +2 -0
  35. data/lib/rom/sql/extensions/postgres.rb +4 -0
  36. data/lib/rom/sql/extensions/postgres/commands.rb +3 -1
  37. data/lib/rom/sql/extensions/postgres/type_builder.rb +6 -4
  38. data/lib/rom/sql/extensions/postgres/type_serializer.rb +2 -0
  39. data/lib/rom/sql/extensions/postgres/types.rb +2 -0
  40. data/lib/rom/sql/extensions/postgres/types/array.rb +9 -8
  41. data/lib/rom/sql/extensions/postgres/types/array_types.rb +3 -1
  42. data/lib/rom/sql/extensions/postgres/types/geometric.rb +2 -0
  43. data/lib/rom/sql/extensions/postgres/types/json.rb +76 -19
  44. data/lib/rom/sql/extensions/postgres/types/ltree.rb +27 -25
  45. data/lib/rom/sql/extensions/postgres/types/network.rb +2 -0
  46. data/lib/rom/sql/extensions/postgres/types/range.rb +6 -4
  47. data/lib/rom/sql/extensions/rails_log_subscriber.rb +2 -0
  48. data/lib/rom/sql/extensions/sqlite.rb +2 -0
  49. data/lib/rom/sql/extensions/sqlite/type_builder.rb +2 -0
  50. data/lib/rom/sql/extensions/sqlite/types.rb +2 -0
  51. data/lib/rom/sql/foreign_key.rb +3 -1
  52. data/lib/rom/sql/function.rb +84 -6
  53. data/lib/rom/sql/gateway.rb +9 -1
  54. data/lib/rom/sql/group_dsl.rb +2 -0
  55. data/lib/rom/sql/index.rb +2 -0
  56. data/lib/rom/sql/join_dsl.rb +11 -0
  57. data/lib/rom/sql/mapper_compiler.rb +14 -3
  58. data/lib/rom/sql/migration.rb +20 -3
  59. data/lib/rom/sql/migration/inline_runner.rb +2 -0
  60. data/lib/rom/sql/migration/migrator.rb +5 -3
  61. data/lib/rom/sql/migration/recorder.rb +2 -0
  62. data/lib/rom/sql/migration/runner.rb +4 -2
  63. data/lib/rom/sql/migration/schema_diff.rb +4 -2
  64. data/lib/rom/sql/migration/template.rb +2 -0
  65. data/lib/rom/sql/migration/writer.rb +12 -4
  66. data/lib/rom/sql/order_dsl.rb +2 -0
  67. data/lib/rom/sql/plugin/associates.rb +4 -3
  68. data/lib/rom/sql/plugin/nullify.rb +37 -0
  69. data/lib/rom/sql/plugin/pagination.rb +22 -0
  70. data/lib/rom/sql/plugins.rb +4 -0
  71. data/lib/rom/sql/projection_dsl.rb +10 -4
  72. data/lib/rom/sql/rake_task.rb +2 -0
  73. data/lib/rom/sql/relation.rb +3 -1
  74. data/lib/rom/sql/relation/reading.rb +105 -16
  75. data/lib/rom/sql/relation/writing.rb +2 -0
  76. data/lib/rom/sql/restriction_dsl.rb +8 -8
  77. data/lib/rom/sql/schema.rb +16 -2
  78. data/lib/rom/sql/schema/attributes_inferrer.rb +7 -5
  79. data/lib/rom/sql/schema/dsl.rb +3 -1
  80. data/lib/rom/sql/schema/index_dsl.rb +8 -3
  81. data/lib/rom/sql/schema/inferrer.rb +12 -8
  82. data/lib/rom/sql/schema/type_builder.rb +4 -2
  83. data/lib/rom/sql/spec/support.rb +5 -3
  84. data/lib/rom/sql/tasks/migration_tasks.rake +16 -11
  85. data/lib/rom/sql/transaction.rb +2 -0
  86. data/lib/rom/sql/type_dsl.rb +3 -1
  87. data/lib/rom/sql/type_extensions.rb +4 -4
  88. data/lib/rom/sql/type_serializer.rb +3 -1
  89. data/lib/rom/sql/types.rb +6 -4
  90. data/lib/rom/sql/version.rb +3 -1
  91. data/lib/rom/sql/wrap.rb +2 -0
  92. data/lib/rom/types/values.rb +2 -0
  93. metadata +39 -37
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ROM
2
4
  module SQL
3
5
  module Migration
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'pathname'
2
4
 
3
5
  require 'rom/types'
@@ -20,7 +22,7 @@ module ROM
20
22
 
21
23
  param :connection
22
24
 
23
- option :path, type: ROM::Types.Definition(Pathname), default: -> { DEFAULT_PATH }
25
+ option :path, type: ROM::Types.Nominal(Pathname), default: -> { DEFAULT_PATH }
24
26
 
25
27
  option :inferrer, default: -> { DEFAULT_INFERRER }
26
28
 
@@ -65,12 +67,12 @@ module ROM
65
67
  end
66
68
 
67
69
  # @api private
68
- def auto_migrate!(gateway, schemas, options = EMPTY_HASH, &block)
70
+ def auto_migrate!(gateway, schemas, options = EMPTY_HASH)
69
71
  diff_finder = SchemaDiff.new(gateway.database_type)
70
72
 
71
73
  changes = schemas.map { |target|
72
74
  empty = SQL::Schema.define(target.name)
73
- current = target.with(inferrer.(empty, gateway))
75
+ current = target.with(**inferrer.(empty, gateway))
74
76
 
75
77
  diff_finder.(current, target)
76
78
  }.reject(&:empty?)
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ROM
2
4
  module SQL
3
5
  module Migration
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ROM
2
4
  module SQL
3
5
  module Migration
@@ -67,8 +69,8 @@ module ROM
67
69
  if attribute.type_changed?
68
70
  from, to = attribute.current.unwrap, attribute.target.unwrap
69
71
  raise UnsupportedConversion.new(
70
- "Don't know how to convert #{ from.inspect } to #{ to.inspect }"
71
- )
72
+ "Don't know how to convert #{from.inspect} to #{to.inspect}"
73
+ )
72
74
  end
73
75
 
74
76
  if attribute.nullability_changed?
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'rom/sql/type_serializer'
2
4
 
3
5
  module ROM
@@ -75,8 +77,8 @@ module ROM
75
77
  attr.primary_key?
76
78
  end
77
79
 
78
- def unwrap(type)
79
- type.optional? ? SQL::Attribute[type.right].meta(type.meta) : type
80
+ def unwrap(attr)
81
+ attr.optional? ? SQL::Attribute[attr.right, attr.options].meta(attr.meta) : attr
80
82
  end
81
83
  end
82
84
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  ROM::SQL.migration do
2
4
  change do
3
5
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'rom/sql/migration/recorder'
2
4
 
3
5
  module ROM
@@ -32,7 +34,7 @@ module ROM
32
34
  operations.each do |operation|
33
35
  op, args, nested = operation
34
36
  buffer << indent << op.to_s << ' '
35
- write_arguments(buffer, *args)
37
+ write_arguments(buffer, args)
36
38
 
37
39
  if !nested.empty?
38
40
  buffer << ' do'
@@ -42,9 +44,15 @@ module ROM
42
44
  end
43
45
  end
44
46
 
45
- def write_arguments(buffer, *args, **kwargs)
47
+ def write_arguments(buffer, args)
48
+ if args.last.is_a?(::Hash)
49
+ args, options = args[0...-1], args.last
50
+ else
51
+ options = EMPTY_HASH
52
+ end
53
+
46
54
  buffer << args.map(&:inspect).join(', ')
47
- kwargs.each do |key, value|
55
+ options.each do |key, value|
48
56
  buffer << ', ' << key.to_s << ': ' << value.inspect
49
57
  end
50
58
  end
@@ -53,7 +61,7 @@ module ROM
53
61
  create_or_alter, args = op
54
62
  table_name = args[0]
55
63
 
56
- "#{ create_or_alter.to_s.sub('_table', '') }_#{ table_name }"
64
+ "#{create_or_alter.to_s.sub('_table', '')}_#{table_name}"
57
65
  end
58
66
  end
59
67
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'rom/sql/dsl'
2
4
 
3
5
  module ROM
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'rom/sql/associations'
2
4
 
3
5
  module ROM
@@ -48,7 +50,7 @@ module ROM
48
50
  # @see ROM::Command::ClassInterface.build
49
51
  #
50
52
  # @api public
51
- def build(relation, options = EMPTY_HASH)
53
+ def build(relation, **options)
52
54
  command = super
53
55
 
54
56
  configured_assocs = command.configured_associations
@@ -146,8 +148,7 @@ module ROM
146
148
  def with_association(name, opts = EMPTY_HASH)
147
149
  self.class.build(
148
150
  relation,
149
- **options,
150
- associations: associations.merge(name => opts)
151
+ **options, associations: associations.merge(name => opts)
151
152
  )
152
153
  end
153
154
  end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ROM
4
+ module SQL
5
+ module Plugin
6
+ # Nullify relation by
7
+ #
8
+ # @api public
9
+ module Nullify
10
+ if defined? JRUBY_VERSION
11
+ # Returns a relation that will never issue a query to the database. It
12
+ # implements the null object pattern for relations.
13
+ # Dataset#nullify doesn't work on JRuby, hence we fall back to SQL
14
+ #
15
+ # @api public
16
+ def nullify
17
+ where { `1 = 0` }
18
+ end
19
+ else
20
+ # Returns a relation that will never issue a query to the database. It
21
+ # implements the null object pattern for relations.
22
+ #
23
+ # @see http://sequel.jeremyevans.net/rdoc-plugins/files/lib/sequel/extensions/null_dataset_rb.html
24
+ # @example result will always be empty, regardless if records exists
25
+ # users.where(name: 'Alice').nullify
26
+ #
27
+ # @return [SQL::Relation]
28
+ #
29
+ # @api public
30
+ def nullify
31
+ new(dataset.where { `1 = 0` }.__send__(__method__))
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'rom/initializer'
2
4
 
3
5
  module ROM
@@ -72,6 +74,26 @@ module ROM
72
74
  (total / per_page.to_f).ceil
73
75
  end
74
76
 
77
+ # Return one-based index of first tuple in page
78
+ #
79
+ # @return [Integer]
80
+ #
81
+ # @api public
82
+ def first_in_page
83
+ ((current_page - 1) * per_page) + 1
84
+ end
85
+
86
+ # Return one-based index of last tuple in page
87
+ #
88
+ # @return [Integer]
89
+ #
90
+ # @api public
91
+ def last_in_page
92
+ return total if current_page == total_pages
93
+
94
+ current_page * per_page
95
+ end
96
+
75
97
  # @api private
76
98
  def at(dataset, current_page, per_page = self.per_page)
77
99
  current_page = current_page.to_i
@@ -1,11 +1,15 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'rom/plugins/relation/sql/instrumentation'
2
4
  require 'rom/plugins/relation/sql/auto_restrictions'
3
5
 
4
6
  require 'rom/sql/plugin/associates'
7
+ require 'rom/sql/plugin/nullify'
5
8
  require 'rom/sql/plugin/pagination'
6
9
 
7
10
  ROM.plugins do
8
11
  adapter :sql do
12
+ register :nullify, ROM::SQL::Plugin::Nullify, type: :relation
9
13
  register :pagination, ROM::SQL::Plugin::Pagination, type: :relation
10
14
  register :associates, ROM::SQL::Plugin::Associates, type: :command
11
15
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'rom/sql/dsl'
2
4
  require 'rom/sql/function'
3
5
 
@@ -29,13 +31,13 @@ module ROM
29
31
  # users.select { function(:count, :id).as(:total) }
30
32
  #
31
33
  # @param [Symbol] name SQL function
32
- # @param [Symbol] attr
34
+ # @param [Symbol] attrs
33
35
  #
34
36
  # @return [Rom::SQL::Function]
35
37
  #
36
38
  # @api public
37
- def function(name, attr)
38
- ::ROM::SQL::Function.new(::ROM::Types::Any, schema: schema).public_send(name, attr)
39
+ def function(name, *attrs)
40
+ ::ROM::SQL::Function.new(::ROM::Types::Any, schema: schema).public_send(name, *attrs)
39
41
  end
40
42
  alias_method :f, :function
41
43
 
@@ -54,7 +56,11 @@ module ROM
54
56
  type = type(meth)
55
57
 
56
58
  if type
57
- ::ROM::SQL::Function.new(type, schema: schema)
59
+ if args.empty?
60
+ ::ROM::SQL::Function.new(type, schema: schema)
61
+ else
62
+ ::ROM::SQL::Attribute[type].value(args[0])
63
+ end
58
64
  else
59
65
  super
60
66
  end
@@ -1,2 +1,4 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'rake'
2
4
  load 'rom/sql/tasks/migration_tasks.rake'
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'rom/sql/types'
2
4
  require 'rom/sql/schema'
3
5
  require 'rom/sql/attribute'
@@ -35,7 +37,7 @@ module ROM
35
37
  table = opts[:from].first
36
38
 
37
39
  if db.table_exists?(table)
38
- select(*schema.map(&:qualified)).order(*schema.project(*schema.primary_key_names).qualified)
40
+ select(*schema.qualified_projection).order(*schema.project(*schema.primary_key_names).qualified)
39
41
  else
40
42
  self
41
43
  end
@@ -1,4 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'rom/support/inflector'
4
+ require 'rom/sql/join_dsl'
2
5
 
3
6
  module ROM
4
7
  module SQL
@@ -147,15 +150,19 @@ module ROM
147
150
 
148
151
  # Pluck values from a specific column
149
152
  #
150
- # @example
153
+ # @example Single value
151
154
  # users.pluck(:id)
152
- # # [1, 2, 3]
155
+ # # [1, 2]
156
+ #
157
+ # @example Multiple values
158
+ # users.pluck(:id, :name)
159
+ # # [[1, "Jane"] [2, "Joe"]]
153
160
  #
154
161
  # @return [Array]
155
162
  #
156
163
  # @api public
157
- def pluck(name)
158
- select(name).map(name)
164
+ def pluck(*names)
165
+ select(*names).map(names.length == 1 ? names.first : names)
159
166
  end
160
167
 
161
168
  # Rename columns in a relation
@@ -217,7 +224,7 @@ module ROM
217
224
  # Project relation using column names and projection DSL
218
225
  #
219
226
  # @example using attributes
220
- # users.select(:id) { int::count(id).as(:count) }.group(:id).first
227
+ # users.select(:id) { integer::count(id).as(:count) }.group(:id).first
221
228
  # # {:id => 1, :count => 1}
222
229
  #
223
230
  # users.select { [id, name] }
@@ -387,7 +394,7 @@ module ROM
387
394
  # users.
388
395
  # qualified.
389
396
  # left_join(tasks).
390
- # select { [id, name, int::count(:tasks__id).as(:task_count)] }.
397
+ # select { [id, name, integer::count(:tasks__id).as(:task_count)] }.
391
398
  # group(users[:id].qualified).
392
399
  # having(task_count: 2)
393
400
  # first
@@ -402,7 +409,7 @@ module ROM
402
409
  # users.
403
410
  # qualified.
404
411
  # left_join(tasks).
405
- # select { [id, name, int::count(:tasks__id).as(:task_count)] }.
412
+ # select { [id, name, integer::count(:tasks__id).as(:task_count)] }.
406
413
  # group(users[:id].qualified).
407
414
  # having { count(id.qualified) >= 1 }.
408
415
  # first
@@ -558,6 +565,16 @@ module ROM
558
565
  #
559
566
  # @param [Relation] relation A relation for join
560
567
  #
568
+ # @overload join(relation, &block)
569
+ # Join with another relation using DSL
570
+ #
571
+ # @example
572
+ # users.join(tasks) { |users:, tasks:|
573
+ # tasks[:user_id].is(users[:id]) & users[:name].is('John')
574
+ # }
575
+ #
576
+ # @param [Relation] relation A relation for join
577
+ #
561
578
  # @return [Relation]
562
579
  #
563
580
  # @api public
@@ -598,6 +615,16 @@ module ROM
598
615
  #
599
616
  # @param [Relation] relation A relation for left_join
600
617
  #
618
+ # @overload join(relation, &block)
619
+ # Join with another relation using DSL
620
+ #
621
+ # @example
622
+ # users.left_join(tasks) { |users:, tasks:|
623
+ # tasks[:user_id].is(users[:id]) & users[:name].is('John')
624
+ # }
625
+ #
626
+ # @param [Relation] relation A relation for left_join
627
+ #
601
628
  # @return [Relation]
602
629
  #
603
630
  # @api public
@@ -637,6 +664,16 @@ module ROM
637
664
  #
638
665
  # @param [Relation] relation A relation for right_join
639
666
  #
667
+ # @overload join(relation, &block)
668
+ # Join with another relation using DSL
669
+ #
670
+ # @example
671
+ # users.right_join(tasks) { |users:, tasks:|
672
+ # tasks[:user_id].is(users[:id]) & users[:name].is('John')
673
+ # }
674
+ #
675
+ # @param [Relation] relation A relation for right_join
676
+ #
640
677
  # @return [Relation]
641
678
  #
642
679
  # @api public
@@ -772,7 +809,16 @@ module ROM
772
809
  #
773
810
  # @api public
774
811
  def union(relation, options = EMPTY_HASH, &block)
775
- new(dataset.__send__(__method__, relation.dataset, options, &block))
812
+ # We use the original relation name here if both relations have the
813
+ # same name. This makes it so if the user at some point references
814
+ # the relation directly by name later on things won't break in
815
+ # confusing ways.
816
+ same_relation = name == relation.name
817
+ alias_name = same_relation ? name : "#{name.to_sym}__#{relation.name.to_sym}"
818
+ opts = { alias: alias_name.to_sym, **options }
819
+
820
+ new_schema = schema.qualified(opts[:alias])
821
+ new_schema.(new(dataset.__send__(__method__, relation.dataset, opts, &block)))
776
822
  end
777
823
 
778
824
  # Checks whether a relation has at least one tuple
@@ -847,8 +893,8 @@ module ROM
847
893
  # @yieldparam relation [Array]
848
894
  #
849
895
  # @api public
850
- def lock(options = EMPTY_HASH, &block)
851
- clause = lock_clause(options)
896
+ def lock(**options, &block)
897
+ clause = lock_clause(**options)
852
898
 
853
899
  if block
854
900
  transaction do
@@ -944,17 +990,44 @@ module ROM
944
990
  # @example adding number of user tasks
945
991
  # tasks = relations[:tasks]
946
992
  # users = relations[:users]
947
- # user_tasks = tasks.where(tasks[:user_id].is(users[:id])
948
- # tasks_count = user_tasks.select { int::count(id) }
993
+ # user_tasks = tasks.where(tasks[:user_id].is(users[:id]))
994
+ # tasks_count = user_tasks.select { integer::count(id) }
949
995
  # users.select_append(tasks_count.as(:tasks_count))
950
996
  #
951
997
  # @return [SQL::Attribute]
952
998
  def query
953
999
  attr = schema.to_a[0]
954
- subquery = schema.project(attr).(self).dataset.unordered
1000
+ subquery = schema.project(attr).(self).dataset
955
1001
  SQL::Attribute[attr.type].meta(sql_expr: subquery)
956
1002
  end
957
1003
 
1004
+ # Discard restrictions in `WHERE` and `HAVING` clauses
1005
+ #
1006
+ # @example calling .by_pk has no effect
1007
+ # users.by_pk(1).unfiltered
1008
+ #
1009
+ # @return [SQL::Relation]
1010
+ #
1011
+ # @api public
1012
+ def unfiltered
1013
+ new(dataset.__send__(__method__))
1014
+ end
1015
+
1016
+ # Wrap other relations using association names
1017
+ #
1018
+ # @example
1019
+ # tasks.wrap(:owner)
1020
+ #
1021
+ # @param [Array<Symbol>] names A list with association identifiers
1022
+ #
1023
+ # @return [Wrap]
1024
+ #
1025
+ # @api public
1026
+ def wrap(*names)
1027
+ others = names.map { |name| associations[name].wrapped }
1028
+ wrap_around(*others)
1029
+ end
1030
+
958
1031
  private
959
1032
 
960
1033
  # Build a locking clause
@@ -965,7 +1038,7 @@ module ROM
965
1038
  stmt << ' OF ' << Array(of).join(', ') if of
966
1039
 
967
1040
  if skip_locked
968
- raise ArgumentError, "SKIP LOCKED cannot be used with (NO)WAIT clause" if !wait.nil?
1041
+ raise ArgumentError, 'SKIP LOCKED cannot be used with (NO)WAIT clause' if !wait.nil?
969
1042
 
970
1043
  stmt << ' SKIP LOCKED'
971
1044
  else
@@ -988,6 +1061,8 @@ module ROM
988
1061
  if k.is_a?(Symbol) && schema.canonical.key?(k)
989
1062
  type = schema.canonical[k]
990
1063
  h[k] = v.is_a?(Array) ? v.map { |e| type[e] } : type[v]
1064
+ elsif k.is_a?(ROM::SQL::Attribute)
1065
+ h[k.canonical] = v
991
1066
  else
992
1067
  h[k] = v
993
1068
  end
@@ -999,16 +1074,30 @@ module ROM
999
1074
  # @api private
1000
1075
  def __join__(type, other, join_cond = EMPTY_HASH, opts = EMPTY_HASH, &block)
1001
1076
  if other.is_a?(Symbol) || other.is_a?(ROM::Relation::Name)
1002
- if join_cond.empty?
1077
+ if join_cond.equal?(EMPTY_HASH) && !block
1003
1078
  assoc = associations[other]
1004
1079
  assoc.join(type, self)
1080
+ elsif block
1081
+ __join__(type, other, JoinDSL.new(schema).(&block), opts)
1005
1082
  else
1006
1083
  new(dataset.__send__(type, other.to_sym, join_cond, opts, &block))
1007
1084
  end
1008
1085
  elsif other.is_a?(Sequel::SQL::AliasedExpression)
1009
1086
  new(dataset.__send__(type, other, join_cond, opts, &block))
1010
1087
  elsif other.respond_to?(:name) && other.name.is_a?(Relation::Name)
1011
- associations[other.name.key].join(type, self, other)
1088
+ if block
1089
+ join_cond = JoinDSL.new(schema).(&block)
1090
+
1091
+ if other.name.aliaz
1092
+ join_opts = { table_alias: other.name.aliaz }
1093
+ else
1094
+ join_opts = EMPTY_HASH
1095
+ end
1096
+
1097
+ new(dataset.__send__(type, other.name.dataset.to_sym, join_cond, join_opts))
1098
+ else
1099
+ associations[other.name.key].join(type, self, other)
1100
+ end
1012
1101
  else
1013
1102
  raise ArgumentError, "+other+ must be either a symbol or a relation, #{other.class} given"
1014
1103
  end