rom-sql 1.0.0.beta2 → 1.0.0.beta3

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 (48) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +1 -1
  3. data/CHANGELOG.md +4 -0
  4. data/circle.yml +1 -1
  5. data/lib/rom/plugins/relation/sql/auto_combine.rb +1 -1
  6. data/lib/rom/sql/association.rb +5 -1
  7. data/lib/rom/sql/association/many_to_many.rb +8 -4
  8. data/lib/rom/sql/association/many_to_one.rb +2 -3
  9. data/lib/rom/sql/association/one_to_many.rb +2 -3
  10. data/lib/rom/sql/commands/create.rb +8 -1
  11. data/lib/rom/sql/commands/update.rb +7 -0
  12. data/lib/rom/sql/extensions.rb +8 -0
  13. data/lib/rom/sql/extensions/active_support_notifications.rb +7 -18
  14. data/lib/rom/sql/extensions/mysql.rb +1 -0
  15. data/lib/rom/sql/extensions/mysql/inferrer.rb +10 -0
  16. data/lib/rom/sql/extensions/postgres/commands.rb +1 -1
  17. data/lib/rom/sql/extensions/postgres/inferrer.rb +4 -0
  18. data/lib/rom/sql/extensions/postgres/types.rb +20 -22
  19. data/lib/rom/sql/extensions/sqlite.rb +1 -0
  20. data/lib/rom/sql/extensions/sqlite/inferrer.rb +10 -0
  21. data/lib/rom/sql/function.rb +6 -2
  22. data/lib/rom/sql/order_dsl.rb +1 -1
  23. data/lib/rom/sql/plugin/associates.rb +28 -5
  24. data/lib/rom/sql/relation.rb +18 -0
  25. data/lib/rom/sql/relation/reading.rb +2 -2
  26. data/lib/rom/sql/relation/sequel_api.rb +119 -0
  27. data/lib/rom/sql/schema/inferrer.rb +26 -2
  28. data/lib/rom/sql/tasks/migration_tasks.rake +1 -1
  29. data/lib/rom/sql/type.rb +13 -2
  30. data/lib/rom/sql/types.rb +5 -3
  31. data/lib/rom/sql/version.rb +1 -1
  32. data/spec/extensions/postgres/types_spec.rb +29 -0
  33. data/spec/integration/association/many_to_many_spec.rb +8 -0
  34. data/spec/integration/association/many_to_one_spec.rb +11 -0
  35. data/spec/integration/association/one_to_many_spec.rb +9 -0
  36. data/spec/integration/schema/inferrer/mysql_spec.rb +36 -0
  37. data/spec/integration/schema/inferrer/postgres_spec.rb +118 -0
  38. data/spec/integration/schema/inferrer/sqlite_spec.rb +36 -0
  39. data/spec/integration/{schema_inference_spec.rb → schema/inferrer_spec.rb} +44 -15
  40. data/spec/integration/sequel_api_spec.rb +31 -0
  41. data/spec/support/helpers.rb +4 -0
  42. data/spec/unit/function_spec.rb +35 -0
  43. data/spec/unit/order_dsl_spec.rb +35 -0
  44. data/spec/unit/relation/assoc_spec.rb +38 -0
  45. data/spec/unit/relation/inner_join_spec.rb +15 -0
  46. data/spec/unit/types_spec.rb +53 -1
  47. metadata +23 -6
  48. data/spec/extensions/postgres/inferrer_spec.rb +0 -59
@@ -3,6 +3,7 @@ require 'rom/sql/schema'
3
3
 
4
4
  require 'rom/sql/relation/reading'
5
5
  require 'rom/sql/relation/writing'
6
+ require 'rom/sql/relation/sequel_api'
6
7
 
7
8
  require 'rom/plugins/relation/key_inference'
8
9
  require 'rom/plugins/relation/sql/auto_combine'
@@ -88,6 +89,23 @@ module ROM
88
89
 
89
90
  option :primary_key, reader: true, default: -> rel { rel.schema.primary_key_name }
90
91
 
92
+ # Return relation that will load associated tuples of this relation
93
+ #
94
+ # This method is useful for defining custom relation views for relation
95
+ # composition when you want to enhance default association query
96
+ #
97
+ # @example
98
+ # assoc(:tasks).where(tasks[:title] => "Task One")
99
+ #
100
+ # @param [Symbol] name The association name
101
+ #
102
+ # @return [Relation]
103
+ #
104
+ # @api public
105
+ def assoc(name)
106
+ associations[name].(__registry__)
107
+ end
108
+
91
109
  # Return raw column names
92
110
  #
93
111
  # @return [Array<Symbol>]
@@ -558,10 +558,10 @@ module ROM
558
558
  private
559
559
 
560
560
  # @api private
561
- def __join__(type, other, opts = EMPTY_HASH, &block)
561
+ def __join__(type, other, join_cond = EMPTY_HASH, opts = EMPTY_HASH, &block)
562
562
  case other
563
563
  when Symbol, Association::Name
564
- new(dataset.__send__(type, other.to_sym, opts, &block))
564
+ new(dataset.__send__(type, other.to_sym, join_cond, opts, &block))
565
565
  when Relation
566
566
  __send__(type, other.name.dataset, join_keys(other))
567
567
  else
@@ -0,0 +1,119 @@
1
+ module ROM
2
+ module SQL
3
+ # Query API for SQL::Relation
4
+ #
5
+ # @api public
6
+ module SequelAPI
7
+ # Select specific columns for select clause
8
+ #
9
+ # @example
10
+ # users.select(:id, :name).first
11
+ # # {:id => 1, :name => "Jane" }
12
+ #
13
+ # @return [Relation]
14
+ #
15
+ # @api public
16
+ def select(*args, &block)
17
+ new(dataset.__send__(__method__, *args, &block))
18
+ end
19
+
20
+ # Append specific columns to select clause
21
+ #
22
+ # @example
23
+ # users.select(:id, :name).select_append(:email)
24
+ # # {:id => 1, :name => "Jane", :email => "jane@doe.org"}
25
+ #
26
+ # @param [Array<Symbol>] *args A list with column names
27
+ #
28
+ # @return [Relation]
29
+ #
30
+ # @api public
31
+ def select_append(*args, &block)
32
+ new(dataset.__send__(__method__, *args, &block))
33
+ end
34
+
35
+ # Restrict a relation to match criteria
36
+ #
37
+ # If block is passed it'll be executed in the context of a condition
38
+ # builder object.
39
+ #
40
+ # @example
41
+ # users.where(name: 'Jane')
42
+ #
43
+ # users.where { age >= 18 }
44
+ #
45
+ # @param [Hash] *args An optional hash with conditions for WHERE clause
46
+ #
47
+ # @return [Relation]
48
+ #
49
+ # @see http://sequel.jeremyevans.net/rdoc/files/doc/dataset_filtering_rdoc.html
50
+ #
51
+ # @api public
52
+ def where(*args, &block)
53
+ new(dataset.__send__(__method__, *args, &block))
54
+ end
55
+
56
+ # Restrict a relation to match grouping criteria
57
+ #
58
+ # @example
59
+ # users.with_task_count.having( task_count: 2 )
60
+ #
61
+ # users.with_task_count.having { task_count > 3 }
62
+ #
63
+ # @param [Hash] *args An optional hash with conditions for HAVING clause
64
+ #
65
+ # @return [Relation]
66
+ #
67
+ # @see http://sequel.jeremyevans.net/rdoc/files/doc/dataset_filtering_rdoc.html
68
+ #
69
+ # @api public
70
+ def having(*args, &block)
71
+ new(dataset.__send__(__method__, *args, &block))
72
+ end
73
+
74
+ # Set order for the relation
75
+ #
76
+ # @example
77
+ # users.order(:name)
78
+ #
79
+ # @param [Array<Symbol>] *args A list with column names
80
+ #
81
+ # @return [Relation]
82
+ #
83
+ # @api public
84
+ def order(*args, &block)
85
+ new(dataset.__send__(__method__, *args, &block))
86
+ end
87
+
88
+ # Join with another relation using INNER JOIN
89
+ #
90
+ # @example
91
+ # users.inner_join(:tasks, id: :user_id)
92
+ #
93
+ # @param [Symbol] relation name
94
+ # @param [Hash] join keys
95
+ #
96
+ # @return [Relation]
97
+ #
98
+ # @api public
99
+ def inner_join(*args, &block)
100
+ new(dataset.__send__(__method__, *args, &block))
101
+ end
102
+
103
+ # Join other relation using LEFT OUTER JOIN
104
+ #
105
+ # @example
106
+ # users.left_join(:tasks, id: :user_id)
107
+ #
108
+ # @param [Symbol] relation name
109
+ # @param [Hash] join keys
110
+ #
111
+ # @return [Relation]
112
+ #
113
+ # @api public
114
+ def left_join(*args, &block)
115
+ new(dataset.__send__(__method__, *args, &block))
116
+ end
117
+ end
118
+ end
119
+ end
@@ -26,6 +26,7 @@ module ROM
26
26
  db_registry Hash.new(self)
27
27
 
28
28
  CONSTRAINT_DB_TYPE = 'add_constraint'.freeze
29
+ DECIMAL_REGEX = /(?:decimal|numeric)\((\d+)(?:,\s*(\d+))?\)/.freeze
29
30
 
30
31
  def self.inherited(klass)
31
32
  super
@@ -72,9 +73,16 @@ module ROM
72
73
  mapped_type = map_type(type, db_type, rest)
73
74
 
74
75
  if mapped_type
76
+ read_type = mapped_type.meta[:read]
75
77
  mapped_type = mapped_type.optional if allow_null
76
78
  mapped_type = mapped_type.meta(foreign_key: true, target: foreign_key) if foreign_key
77
- mapped_type
79
+ if read_type && allow_null
80
+ mapped_type.meta(read: read_type.optional)
81
+ elsif read_type
82
+ mapped_type.meta(read: read_type)
83
+ else
84
+ mapped_type
85
+ end
78
86
  end
79
87
  end
80
88
  end
@@ -87,7 +95,7 @@ module ROM
87
95
  type = self.class.ruby_type_mapping[ruby_type]
88
96
 
89
97
  if db_type.is_a?(String) && db_type.include?('numeric') || db_type.include?('decimal')
90
- self.class.ruby_type_mapping[:decimal]
98
+ map_decimal_type(db_type)
91
99
  else
92
100
  type
93
101
  end
@@ -110,6 +118,22 @@ module ROM
110
118
  columns[0]
111
119
  end
112
120
  end
121
+
122
+ # @api private
123
+ def map_decimal_type(type)
124
+ precision = DECIMAL_REGEX.match(type)
125
+
126
+ if precision
127
+ prcsn, scale = precision[1..2].map(&:to_i)
128
+
129
+ self.class.ruby_type_mapping[:decimal].meta(
130
+ precision: prcsn,
131
+ scale: scale
132
+ )
133
+ else
134
+ self.class.ruby_type_mapping[:decimal]
135
+ end
136
+ end
113
137
  end
114
138
  end
115
139
  end
@@ -24,11 +24,11 @@ module ROM
24
24
  end
25
25
 
26
26
  namespace :db do
27
- desc "Perform migration reset (full erase and migration up)"
28
27
  task :rom_configuration do
29
28
  Rake::Task["db:setup"].invoke
30
29
  end
31
30
 
31
+ desc "Perform migration reset (full erase and migration up)"
32
32
  task reset: :rom_configuration do
33
33
  ROM::SQL::RakeSupport.run_migrations(target: 0)
34
34
  ROM::SQL::RakeSupport.run_migrations
data/lib/rom/sql/type.rb CHANGED
@@ -3,6 +3,8 @@ require 'rom/schema/type'
3
3
  module ROM
4
4
  module SQL
5
5
  class Type < ROM::Schema::Type
6
+ QualifyError = Class.new(StandardError)
7
+
6
8
  # Return a new type marked as a FK
7
9
  #
8
10
  # @return [SQL::Type]
@@ -13,9 +15,10 @@ module ROM
13
15
  end
14
16
 
15
17
  # @api public
16
- def as(name)
18
+ def aliased(name)
17
19
  super.meta(sql_expr: sql_expr.as(name))
18
20
  end
21
+ alias_method :as, :aliased
19
22
 
20
23
  # Return a new type marked as qualified
21
24
  #
@@ -23,7 +26,15 @@ module ROM
23
26
  #
24
27
  # @api public
25
28
  def qualified
26
- meta(qualified: true)
29
+ return self if qualified?
30
+
31
+ case sql_expr
32
+ when Sequel::SQL::AliasedExpression, Sequel::SQL::Identifier
33
+ type = meta(qualified: true)
34
+ type.meta(qualified: true, sql_expr: Sequel[type.to_sym])
35
+ else
36
+ raise QualifyError, "can't qualify #{name.inspect} (#{sql_expr.inspect})"
37
+ end
27
38
  end
28
39
 
29
40
  # Return a new type marked as joined
data/lib/rom/sql/types.rb CHANGED
@@ -5,11 +5,13 @@ module ROM
5
5
  module Types
6
6
  include ROM::Types
7
7
 
8
+ def self.Constructor(*args, &block)
9
+ ROM::Types.Constructor(*args, &block)
10
+ end
11
+
8
12
  Serial = Int.constrained(gt: 0).meta(primary_key: true)
9
13
 
10
- Blob = Dry::Types::Definition
11
- .new(Sequel::SQL::Blob)
12
- .constructor(Sequel::SQL::Blob.method(:new))
14
+ Blob = Constructor(Sequel::SQL::Blob, &Sequel::SQL::Blob.method(:new))
13
15
  end
14
16
  end
15
17
  end
@@ -1,5 +1,5 @@
1
1
  module ROM
2
2
  module SQL
3
- VERSION = '1.0.0.beta2'.freeze
3
+ VERSION = '1.0.0.beta3'.freeze
4
4
  end
5
5
  end
@@ -112,4 +112,33 @@ RSpec.describe 'ROM::SQL::Types' do
112
112
  expect(output).to be_instance_of(BigDecimal)
113
113
  end
114
114
  end
115
+
116
+ describe ROM::SQL::Types::PG::IPAddress do
117
+ it 'converts IPAddr to a string' do
118
+ expect(described_class[IPAddr.new('127.0.0.1')]).to eql('127.0.0.1')
119
+ end
120
+
121
+ it 'coerces to builtin IPAddr type on read' do
122
+ expect(described_class.meta[:read]['127.0.0.1']).to eql(IPAddr.new('127.0.0.1'))
123
+ end
124
+
125
+ it 'supports networks' do
126
+ class_a = described_class.meta[:read]['10.0.0.0/8']
127
+
128
+ expect(class_a).to eql(IPAddr.new('10.0.0.0/8'))
129
+ expect(class_a).to include(IPAddr.new('10.8.8.8'))
130
+ end
131
+ end
132
+
133
+ describe ROM::SQL::Types::PG::PointT do
134
+ let(:point) { ROM::SQL::Types::PG::Point.new(7.5, 30.5) }
135
+
136
+ it 'serializes a point down to a string' do
137
+ expect(described_class[point]).to eql('(7.5,30.5)')
138
+ end
139
+
140
+ it 'reads serialized format' do
141
+ expect(described_class.meta[:read]['(7.5,30.5)']).to eql(point)
142
+ end
143
+ end
115
144
  end
@@ -133,6 +133,14 @@ RSpec.describe ROM::SQL::Association::ManyToMany do
133
133
 
134
134
  expect(relation.to_a).to eql([id: 1, name: 'important', task_id: 1])
135
135
  end
136
+
137
+ it 'maintains original relation' do
138
+ relation = tags.
139
+ select_append(tags[:name].as(:tag)).
140
+ for_combine(assoc).call(tasks.call)
141
+
142
+ expect(relation.to_a).to eql([id: 1, tag: 'important', name: 'important', task_id: 1])
143
+ end
136
144
  end
137
145
  end
138
146
  end
@@ -66,6 +66,17 @@ RSpec.describe ROM::SQL::Association::ManyToOne, helpers: true do
66
66
  { id: 1, task_id: 2, name: 'Jane' }
67
67
  ])
68
68
  end
69
+
70
+ it 'maintains original relation' do
71
+ users.accounts.insert(user_id: 2, number: 'a1', balance: 0)
72
+
73
+ relation = users.
74
+ join(:accounts, user_id: :id).
75
+ select_append(users.accounts[:number].as(:account_num)).
76
+ for_combine(assoc).call(tasks.call)
77
+
78
+ expect(relation.to_a).to eql([{ id: 2, task_id: 1, name: 'Joe', account_num: 'a1' }])
79
+ end
69
80
  end
70
81
 
71
82
  context 'arbitrary name conventions' do
@@ -53,6 +53,15 @@ RSpec.describe ROM::SQL::Association::OneToMany do
53
53
  { id: 2, user_id: 1, title: "Jane's task" }
54
54
  ])
55
55
  end
56
+
57
+ it 'maintains original relation' do
58
+ relation = tasks.
59
+ join(:task_tags, tag_id: :id).
60
+ select_append(tasks.task_tags[:tag_id].qualified).
61
+ for_combine(assoc).call(users.call)
62
+
63
+ expect(relation.to_a).to eql([{ id: 1, user_id: 2, title: "Joe's task", tag_id: 1 }])
64
+ end
56
65
  end
57
66
  end
58
67
  end
@@ -0,0 +1,36 @@
1
+ RSpec.describe 'ROM::SQL::Schema::MysqlInferrer', :mysql do
2
+ include_context 'database setup'
3
+
4
+ before do
5
+ conn.drop_table?(:test_inferrence)
6
+
7
+ conn.create_table :test_inferrence do
8
+ tinyint :tiny
9
+ mediumint :medium
10
+ end
11
+ end
12
+
13
+ after do
14
+ conn.drop_table?(:test_inferrence)
15
+ end
16
+
17
+ let(:dataset) { :test_inferrence }
18
+
19
+ let(:schema) { container.relations[dataset].schema }
20
+
21
+ before do
22
+ dataset = self.dataset
23
+ conf.relation(dataset) do
24
+ schema(dataset, infer: true)
25
+ end
26
+ end
27
+
28
+ it 'can infer attributes for dataset' do
29
+ source = container.relations[:test_inferrence].name
30
+
31
+ expect(schema.to_h).to eql(
32
+ tiny: ROM::SQL::Types::Int.optional.meta(name: :tiny, source: source),
33
+ medium: ROM::SQL::Types::Int.optional.meta(name: :medium, source: source),
34
+ )
35
+ end
36
+ end
@@ -0,0 +1,118 @@
1
+
2
+ RSpec.describe 'ROM::SQL::Schema::PostgresInferrer', :postgres do
3
+ include_context 'database setup'
4
+
5
+ colors = %w(red orange yellow green blue purple)
6
+
7
+ before do
8
+ conn.extension :pg_enum
9
+
10
+ conn.drop_table?(:test_inferrence)
11
+ conn.drop_enum(:rainbow, if_exists: true)
12
+
13
+ conn.create_enum(:rainbow, colors)
14
+
15
+ conn.create_table :test_inferrence do
16
+ primary_key :id, :uuid
17
+ Json :json_data
18
+ Jsonb :jsonb_data
19
+ Decimal :money, null: false
20
+ column :tags, "text[]"
21
+ column :tag_ids, "bigint[]"
22
+ column :ip, "inet"
23
+ column :subnet, "cidr"
24
+ column :hw_address, "macaddr"
25
+ rainbow :color
26
+ point :center
27
+ end
28
+ end
29
+
30
+ after do
31
+ conn.drop_table?(:test_inferrence)
32
+ end
33
+
34
+ let(:dataset) { :test_inferrence }
35
+
36
+ let(:schema) { container.relations[dataset].schema }
37
+
38
+ context 'inferring db-specific attributes' do
39
+ before do
40
+ dataset = self.dataset
41
+ conf.relation(dataset) do
42
+ schema(dataset, infer: true)
43
+ end
44
+ end
45
+
46
+ it 'can infer attributes for dataset' do
47
+ source = container.relations[:test_inferrence].name
48
+
49
+ expect(schema.to_h).to eql(
50
+ id: ROM::SQL::Types::PG::UUID.meta(name: :id, source: source, primary_key: true),
51
+ json_data: ROM::SQL::Types::PG::JSON.optional.meta(name: :json_data, source: source),
52
+ jsonb_data: ROM::SQL::Types::PG::JSONB.optional.meta(name: :jsonb_data, source: source),
53
+ money: ROM::SQL::Types::Decimal.meta(name: :money, source: source),
54
+ tags: ROM::SQL::Types::PG::Array('text').optional.meta(name: :tags, source: source),
55
+ tag_ids: ROM::SQL::Types::PG::Array('biging').optional.meta(name: :tag_ids, source: source),
56
+ color: ROM::SQL::Types::String.enum(*colors).optional.meta(name: :color, source: source),
57
+ ip: ROM::SQL::Types::PG::IPAddress.optional.meta(
58
+ name: :ip,
59
+ source: source,
60
+ read: ROM::SQL::Types::PG::IPAddressR.optional
61
+ ),
62
+ subnet: ROM::SQL::Types::PG::IPAddress.optional.meta(
63
+ name: :subnet,
64
+ source: source,
65
+ read: ROM::SQL::Types::PG::IPAddressR.optional
66
+ ),
67
+ hw_address: ROM::SQL::Types::String.optional.meta(name: :hw_address, source: source),
68
+ center: ROM::SQL::Types::PG::PointT.optional.meta(
69
+ name: :center,
70
+ source: source,
71
+ read: ROM::SQL::Types::PG::PointTR.optional
72
+ )
73
+ )
74
+ end
75
+ end
76
+
77
+ context 'with a table without columns' do
78
+ before do
79
+ conn.create_table(:dummy) unless conn.table_exists?(:dummy)
80
+ conf.relation(:dummy) { schema(infer: true) }
81
+ end
82
+
83
+ it 'does not fail with a weird error when a relation does not have attributes' do
84
+ expect(container.relations[:dummy].schema).to be_empty
85
+ end
86
+ end
87
+
88
+ context 'with a column with bi-directional mapping' do
89
+ before do
90
+ conn.drop_table?(:test_bidirectional)
91
+ conn.create_table(:test_bidirectional) do
92
+ primary_key :id
93
+ inet :ip
94
+ point :center
95
+ end
96
+
97
+ conf.relation(:test_bidirectional) { schema(infer: true) }
98
+
99
+ conf.commands(:test_bidirectional) do
100
+ define(:create) do
101
+ result :one
102
+ end
103
+ end
104
+ end
105
+
106
+ let(:point) { ROM::SQL::Types::PG::Point.new(7.5, 30.5) }
107
+ let(:dns) { IPAddr.new('8.8.8.8') }
108
+
109
+ let(:relation) { container.relations[:test_bidirectional] }
110
+ let(:create) { commands[:test_bidirectional].create }
111
+
112
+ it 'writes and reads data' do
113
+ inserted = create.call(id: 1, center: point, ip: dns)
114
+ expect(inserted).to eql(id: 1, center: point, ip: dns)
115
+ expect(relation.to_a).to eql([inserted])
116
+ end
117
+ end
118
+ end