rom-sql 1.0.3 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: fb7aa3ae6fc950b9149086e31c3162e99a524b37
4
- data.tar.gz: c324b47d45d23e01fc606d65f853b163e2699a59
3
+ metadata.gz: d4ee8c56fc0c1b34efb44c1d32c844765bc6c891
4
+ data.tar.gz: a2aeb62deb514cc3912f5fd512c9c02e2c9a24df
5
5
  SHA512:
6
- metadata.gz: 172e07e4a8073164d7b9f8eec06eaefa16b052f7439a2b4d3f211cd2f932da9976df4d3a9863d525083b5e37a5dd230151b9a126f923cf8b5acdfb12b2b36469
7
- data.tar.gz: 69352c745545be3e04a9eba2a196e0e2a3b59cd1d5f4782fa0ec9e78d78b4a5893b8bfa98f80a6bf208914ecc93a11dcc3f7e0f9549a10d764862eb110a2cf69
6
+ metadata.gz: '0942e1aeec2211371d871e1510f2668187581abcd5f4cbe447f2666a125678c4392c4e39fd0e8ceaecae227562d691b3ef8ab715a9ef111ed1ebb260ba5d64d4'
7
+ data.tar.gz: 3f08eac6477071a28cabb927690eaef0a0db41ee1bba0804f01a45e5317f5ba882765a06001313bed14309c8c169198071b45f07e8e22579a7956db669a0bf32
@@ -1,3 +1,19 @@
1
+ ## v1.1.0 2017-03-01
2
+
3
+ ### Added
4
+
5
+ * Added inferring for database indices (flash-gordon)
6
+ * Restriction conditions are now coerced using schema attributes (solnic)
7
+ * `:instrumentation` relation plugin that can be configured with any instrumentation backend (solnic)
8
+ * `:auto_restrictions` relation plugin, which defines `by_*` views restricting relations by their indexed attributes (solnic)
9
+
10
+ ### Fixed
11
+
12
+ * Missing `group` method was added to legacy `SequelAPI` module (solnic)
13
+ * Associations properly maintain `order` if it was set (solnic)
14
+
15
+ [Compare v1.0.3...v1.1.0](https://github.com/rom-rb/rom-sql/compare/v1.0.3...v1.1.0)
16
+
1
17
  ## v1.0.3 2017-02-23
2
18
 
3
19
  ### Changed
@@ -0,0 +1,74 @@
1
+ module ROM
2
+ module Plugins
3
+ module Relation
4
+ module SQL
5
+ # Generates methods for restricting relations by their indexed attributes
6
+ #
7
+ # This plugin must be enabled for the whole adapter, `use` won't work as
8
+ # schema is not yet available, unless it was defined explicitly.
9
+ #
10
+ # @example
11
+ # rom = ROM.container(:sql, 'sqlite::memory') do |config|
12
+ # config.create_table(:users) do
13
+ # primary_key :id
14
+ # column :email, String, null: false, unique: true
15
+ # end
16
+ #
17
+ # config.plugin(:sql, relations: :auto_restrictions)
18
+ #
19
+ # config.relation(:users) do
20
+ # schema(infer: true)
21
+ # end
22
+ # end
23
+ #
24
+ # # now `by_email` is available automatically
25
+ # rom.relations[:users].by_email('jane@doe.org')
26
+ #
27
+ # @api public
28
+ module AutoRestrictions
29
+ EmptySchemaError = Class.new(ArgumentError) do
30
+ def initialize(klass)
31
+ super("#{klass} relation has no schema. " \
32
+ "Make sure :auto_restrictions is enabled after defining a schema")
33
+ end
34
+ end
35
+
36
+ def self.included(klass)
37
+ super
38
+ schema = klass.schema
39
+ raise EmptySchemaError, klass if schema.nil?
40
+ methods, mod = restriction_methods(schema)
41
+ klass.include(mod)
42
+ methods.each { |meth| klass.auto_curry(meth) }
43
+ end
44
+
45
+ def self.restriction_methods(schema)
46
+ mod = Module.new
47
+
48
+ indexed_attrs = schema.select { |attr| attr.meta[:index] }
49
+
50
+ methods = indexed_attrs.map do |attr|
51
+ meth_name = :"by_#{attr.name}"
52
+
53
+ mod.module_eval do
54
+ define_method(meth_name) do |value|
55
+ where(attr.is(value))
56
+ end
57
+ end
58
+
59
+ meth_name
60
+ end
61
+
62
+ [methods, mod]
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
69
+
70
+ ROM.plugins do
71
+ adapter :sql do
72
+ register :auto_restrictions, ROM::Plugins::Relation::SQL::AutoRestrictions, type: :relation
73
+ end
74
+ end
@@ -0,0 +1,31 @@
1
+ require 'rom/plugins/relation/instrumentation'
2
+
3
+ module ROM
4
+ module Plugins
5
+ module Relation
6
+ module SQL
7
+ # @api private
8
+ module Instrumentation
9
+ def self.included(klass)
10
+ super
11
+
12
+ klass.class_eval do
13
+ include ROM::Plugins::Relation::Instrumentation
14
+
15
+ # @api private
16
+ def notification_payload(relation)
17
+ super.merge(query: relation.dataset.sql)
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+
27
+ ROM.plugins do
28
+ adapter :sql do
29
+ register :instrumentation, ROM::Plugins::Relation::SQL::Instrumentation, type: :relation
30
+ end
31
+ end
@@ -37,9 +37,7 @@ module ROM
37
37
  right.schema.merge(join_rel.schema.project(left_fk))
38
38
  end.qualified
39
39
 
40
- relation = left
41
- .inner_join(source, join_keys(relations))
42
- .order(*right.schema.project_pk.qualified)
40
+ relation = left.inner_join(source, join_keys(relations))
43
41
 
44
42
  if view
45
43
  apply_view(schema, relation)
@@ -21,9 +21,7 @@ module ROM
21
21
  left_schema.merge(right_schema.project_fk(left_pk => right_fk))
22
22
  end.qualified
23
23
 
24
- relation = left
25
- .inner_join(source_table, join_keys(relations))
26
- .order(*right_schema.qualified)
24
+ relation = left.inner_join(source_table, join_keys(relations))
27
25
 
28
26
  if view
29
27
  apply_view(schema, relation)
@@ -8,9 +8,7 @@ module ROM
8
8
  def call(relations, right = relations[target.relation])
9
9
  schema = right.schema.qualified
10
10
 
11
- relation = right
12
- .inner_join(source_table, join_keys(relations))
13
- .order(*right.schema.project_pk.qualified)
11
+ relation = right.inner_join(source_table, join_keys(relations))
14
12
 
15
13
  if view
16
14
  apply_view(schema, relation)
@@ -9,6 +9,8 @@ module ROM
9
9
  #
10
10
  # @api public
11
11
  class Attribute < ROM::Schema::Attribute
12
+ OPERATORS = %i[>= <= > <].freeze
13
+
12
14
  # Error raised when an attribute cannot be qualified
13
15
  QualifyError = Class.new(StandardError)
14
16
 
@@ -237,7 +239,9 @@ module ROM
237
239
  #
238
240
  # @api private
239
241
  def method_missing(meth, *args, &block)
240
- if sql_expr.respond_to?(meth)
242
+ if OPERATORS.include?(meth)
243
+ __cmp__(meth, args[0])
244
+ elsif sql_expr.respond_to?(meth)
241
245
  meta(sql_expr: sql_expr.__send__(meth, *args, &block))
242
246
  else
243
247
  super
@@ -248,8 +252,16 @@ module ROM
248
252
  # the left part is the attribute value
249
253
  #
250
254
  # @api private
251
- def __cmp__(op, r)
252
- Sequel::SQL::BooleanExpression.new(op, self, r)
255
+ def __cmp__(op, other)
256
+ value =
257
+ case other
258
+ when Sequel::SQL::Expression
259
+ value
260
+ else
261
+ type[other]
262
+ end
263
+
264
+ Sequel::SQL::BooleanExpression.new(op, self, value)
253
265
  end
254
266
  end
255
267
  end
@@ -1,3 +1,6 @@
1
+ require 'rom/plugins/relation/sql/instrumentation'
2
+ require 'rom/plugins/relation/sql/auto_restrictions'
3
+
1
4
  require 'rom/sql/plugin/associates'
2
5
  require 'rom/sql/plugin/pagination'
3
6
  require 'rom/sql/plugin/timestamps'
@@ -45,7 +45,7 @@ module ROM
45
45
  #
46
46
  # @api public
47
47
  def first
48
- dataset.first
48
+ limit(1).to_a.first
49
49
  end
50
50
 
51
51
  # Get last tuple from the relation
@@ -58,7 +58,7 @@ module ROM
58
58
  #
59
59
  # @api public
60
60
  def last
61
- dataset.last
61
+ reverse.limit(1).first
62
62
  end
63
63
 
64
64
  # Prefix all columns in a relation
@@ -338,9 +338,11 @@ module ROM
338
338
  # @api public
339
339
  def where(*args, &block)
340
340
  if block
341
- new(dataset.where(*args).where(self.class.schema.restriction(&block)))
341
+ where(*args).where(self.class.schema.restriction(&block))
342
+ elsif args.size == 1 && args[0].is_a?(Hash)
343
+ new(dataset.where(coerce_conditions(args[0])))
342
344
  else
343
- new(dataset.__send__(__method__, *args))
345
+ new(dataset.where(*args))
344
346
  end
345
347
  end
346
348
 
@@ -784,11 +786,24 @@ module ROM
784
786
  #
785
787
  # @api public
786
788
  def read(sql)
787
- new(dataset.db[sql])
789
+ new(dataset.db[sql], schema: schema.empty)
788
790
  end
789
791
 
790
792
  private
791
793
 
794
+ # Apply input types to condition values
795
+ #
796
+ # @api private
797
+ def coerce_conditions(conditions)
798
+ conditions.each_with_object({}) { |(k, v), h|
799
+ if k.is_a?(Symbol) && self.class.schema.key?(k)
800
+ h[k] = self.class.schema[k][v]
801
+ else
802
+ h[k] = v
803
+ end
804
+ }
805
+ end
806
+
792
807
  # Common join method used by other join methods
793
808
  #
794
809
  # @api private
@@ -114,6 +114,20 @@ module ROM
114
114
  def left_join(*args, &block)
115
115
  new(dataset.__send__(__method__, *args, &block))
116
116
  end
117
+
118
+ # Group by specific columns
119
+ #
120
+ # @example
121
+ # tasks.group(:user_id)
122
+ #
123
+ # @param [Array<Symbol>] *args A list of column names
124
+ #
125
+ # @return [Relation]
126
+ #
127
+ # @api public
128
+ def group(*args, &block)
129
+ new(dataset.__send__(__method__, *args, &block))
130
+ end
117
131
  end
118
132
  end
119
133
  end
@@ -95,6 +95,15 @@ module ROM
95
95
  relation.new(relation.dataset.select(*self), schema: self)
96
96
  end
97
97
 
98
+ # Return an empty schema
99
+ #
100
+ # @return [Schema]
101
+ #
102
+ # @api public
103
+ def empty
104
+ new(EMPTY_ARRAY)
105
+ end
106
+
98
107
  # @api private
99
108
  def finalize!(*)
100
109
  super do
@@ -1,3 +1,4 @@
1
+ require 'set'
1
2
  require 'dry/core/class_attributes'
2
3
 
3
4
  module ROM
@@ -55,10 +56,12 @@ module ROM
55
56
  dataset = source.dataset
56
57
 
57
58
  columns = filter_columns(gateway.connection.schema(dataset))
59
+ all_indexes = indexes_for(gateway, dataset)
58
60
  fks = fks_for(gateway, dataset)
59
61
 
60
62
  inferred = columns.map do |(name, definition)|
61
- type = build_type(definition.merge(foreign_key: fks[name]))
63
+ indexes = column_indexes(all_indexes, name)
64
+ type = build_type(**definition, foreign_key: fks[name], indexes: indexes)
62
65
 
63
66
  if type
64
67
  type.meta(name: name, source: source)
@@ -74,7 +77,7 @@ module ROM
74
77
  schema.reject { |(_, definition)| definition[:db_type] == CONSTRAINT_DB_TYPE }
75
78
  end
76
79
 
77
- def build_type(primary_key:, db_type:, type:, allow_null:, foreign_key:, **rest)
80
+ def build_type(primary_key:, db_type:, type:, allow_null:, foreign_key:, indexes:, **rest)
78
81
  if primary_key
79
82
  map_pk_type(type, db_type)
80
83
  else
@@ -84,6 +87,8 @@ module ROM
84
87
  read_type = mapped_type.meta[:read]
85
88
  mapped_type = mapped_type.optional if allow_null
86
89
  mapped_type = mapped_type.meta(foreign_key: true, target: foreign_key) if foreign_key
90
+ mapped_type = mapped_type.meta(index: indexes) unless indexes.empty?
91
+
87
92
  if read_type && allow_null
88
93
  mapped_type.meta(read: read_type.optional)
89
94
  elsif read_type
@@ -118,6 +123,24 @@ module ROM
118
123
  end
119
124
  end
120
125
 
126
+ # @api private
127
+ def indexes_for(gateway, dataset)
128
+ if gateway.connection.respond_to?(:indexes)
129
+ gateway.connection.indexes(dataset)
130
+ else
131
+ # index listing is not implemented
132
+ EMPTY_HASH
133
+ end
134
+ end
135
+
136
+ # @api private
137
+ def column_indexes(indexes, column)
138
+ indexes.each_with_object(Set.new) do |(name, idx), indexes|
139
+ indexes << name if idx[:columns][0] == column
140
+ end
141
+ end
142
+
143
+ # @api private
121
144
  def build_fk(columns: , table: , **rest)
122
145
  if columns.size == 1
123
146
  [columns[0], table]
@@ -1,5 +1,5 @@
1
1
  module ROM
2
2
  module SQL
3
- VERSION = '1.0.3'.freeze
3
+ VERSION = '1.1.0'.freeze
4
4
  end
5
5
  end
@@ -21,7 +21,7 @@ Gem::Specification.new do |spec|
21
21
  spec.add_runtime_dependency 'dry-equalizer', '~> 0.2'
22
22
  spec.add_runtime_dependency 'dry-types', '~> 0.9', '>= 0.9.4'
23
23
  spec.add_runtime_dependency 'dry-core', '~> 0.2', '>= 0.2.3'
24
- spec.add_runtime_dependency 'rom', '~> 3.0'
24
+ spec.add_runtime_dependency 'rom', '~> 3.1'
25
25
 
26
26
  spec.add_development_dependency 'bundler'
27
27
  spec.add_development_dependency 'rake', '~> 10.0'
@@ -79,6 +79,21 @@ RSpec.describe ROM::SQL::Association::ManyToMany do
79
79
 
80
80
  expect(relation.to_a).to eql([id: 1, tag: 'important', name: 'important', task_id: 1])
81
81
  end
82
+
83
+ it 'respects custom order' do
84
+ conn[:tags].insert id: 2, name: 'boring'
85
+ conn[:task_tags].insert(tag_id: 2, task_id: 1)
86
+
87
+ relation = tags.
88
+ order(tags[:name].qualified).
89
+ for_combine(assoc).call(tasks.call)
90
+
91
+ expect(relation.to_a).
92
+ to eql([
93
+ { id: 2, name: 'boring', task_id: 1 },
94
+ { id: 1, name: 'important', task_id: 1 }
95
+ ])
96
+ end
82
97
  end
83
98
  end
84
99
 
@@ -52,8 +52,10 @@ RSpec.describe ROM::SQL::Association::ManyToOne, helpers: true do
52
52
  expect(relation.where(user_id: 2).one).to eql(id: 2, task_id: 1, name: 'Joe')
53
53
 
54
54
  expect(relation.to_a).
55
- to eql([{ id: 2, task_id: 1, name: 'Joe' },
56
- { id: 1, task_id: 2, name: 'Jane' }])
55
+ to eql([
56
+ { id: 1, task_id: 2, name: 'Jane' },
57
+ { id: 2, task_id: 1, name: 'Joe' }
58
+ ])
57
59
  end
58
60
  end
59
61
 
@@ -62,12 +64,14 @@ RSpec.describe ROM::SQL::Association::ManyToOne, helpers: true do
62
64
  relation = users.for_combine(assoc).call(tasks.call)
63
65
 
64
66
  expect(relation.to_a).
65
- to eql([{ id: 2, task_id: 1, name: 'Joe' },
66
- { id: 1, task_id: 2, name: 'Jane' }])
67
+ to eql([
68
+ { id: 1, task_id: 2, name: 'Jane' },
69
+ { id: 2, task_id: 1, name: 'Joe' }
70
+ ])
67
71
  end
68
72
 
69
73
  it 'maintains original relation' do
70
- users.accounts.insert(user_id: 2, number: 'a1', balance: 0)
74
+ users.accounts.insert(user_id: 2, number: '31', balance: 0)
71
75
 
72
76
  relation = users.
73
77
  join(:accounts, user_id: :id).
@@ -76,7 +80,7 @@ RSpec.describe ROM::SQL::Association::ManyToOne, helpers: true do
76
80
  for_combine(assoc).call(tasks.call)
77
81
 
78
82
  expect(relation.to_a).
79
- to eql([{ id: 2, task_id: 1, name: 'Joe', account_num: 'a1' },
83
+ to eql([{ id: 2, task_id: 1, name: 'Joe', account_num: '31' },
80
84
  { id: 1, task_id: 2, name: 'Jane', account_num: '42' }])
81
85
  end
82
86
  end
@@ -59,6 +59,15 @@ RSpec.describe ROM::SQL::Association::OneToMany do
59
59
 
60
60
  expect(relation.to_a).to eql([{ id: 1, user_id: 2, title: "Joe's task", tag_id: 1 }])
61
61
  end
62
+
63
+ it 'respects custom order' do
64
+ relation = tasks.
65
+ order(tasks[:title].qualified).
66
+ for_combine(assoc).call(users.call)
67
+
68
+ expect(relation.to_a).
69
+ to eql([{ id: 2, user_id: 1, title: "Jane's task" }, { id: 1, user_id: 2, title: "Joe's task" }])
70
+ end
62
71
  end
63
72
  end
64
73
  end
@@ -6,7 +6,7 @@ RSpec.describe 'Commands / Create', :postgres, seeds: false do
6
6
  let(:users) { commands[:users] }
7
7
  let(:tasks) { commands[:tasks] }
8
8
 
9
- before do
9
+ before do |ex|
10
10
  module Test
11
11
  class Params < Dry::Struct
12
12
  attribute :name, Types::Strict::String.optional
@@ -19,6 +19,12 @@ RSpec.describe 'Commands / Create', :postgres, seeds: false do
19
19
 
20
20
  conn.add_index :users, :name, unique: true
21
21
 
22
+ if sqlite?(ex)
23
+ conn.add_index :tasks, :title, unique: true
24
+ else
25
+ conn.execute "ALTER TABLE tasks add CONSTRAINT tasks_title_key UNIQUE (title)"
26
+ end
27
+
22
28
  conf.commands(:users) do
23
29
  define(:create) do
24
30
  input Test::Params
@@ -6,6 +6,8 @@ RSpec.describe 'Commands / Postgres / Upsert', :postgres, seeds: false do
6
6
  let(:tasks) { commands[:tasks] }
7
7
 
8
8
  before do
9
+ conn.execute "ALTER TABLE tasks add CONSTRAINT tasks_title_key UNIQUE (title)"
10
+
9
11
  conn[:users].insert id: 1, name: 'Jane'
10
12
  conn[:users].insert id: 2, name: 'Joe'
11
13
  conn[:users].insert id: 3, name: 'Jean'
@@ -0,0 +1,54 @@
1
+ RSpec.describe 'Plugins / :auto_restrictions', seeds: true do
2
+ include_context 'users and tasks'
3
+
4
+ with_adapters do
5
+ before do
6
+ conn.add_index :tasks, :title, unique: true
7
+ end
8
+
9
+ shared_context 'auto-generated restriction view' do
10
+ it 'defines restriction views for all indexed attributes' do
11
+ expect(tasks.select(:id).by_title("Jane's task").one).to eql(id: 2)
12
+ end
13
+
14
+ it 'defines curried methods' do
15
+ expect(tasks.by_title.("Jane's task").first).to eql(id: 2, user_id: 1, title: "Jane's task")
16
+ end
17
+ end
18
+
19
+ context 'with an inferred schema' do
20
+ before do
21
+ conf.plugin(:sql, relations: :auto_restrictions)
22
+ end
23
+
24
+ include_context 'auto-generated restriction view'
25
+ end
26
+
27
+ context 'with explicit schema' do
28
+ before do
29
+ conf.relation(:tasks) do
30
+ schema do
31
+ attribute :id, ROM::SQL::Types::Serial
32
+ attribute :user_id, ROM::SQL::Types::Int
33
+ attribute :title, ROM::SQL::Types::String.meta(index: true)
34
+ end
35
+
36
+ use :auto_restrictions
37
+ end
38
+ end
39
+
40
+ include_context 'auto-generated restriction view'
41
+ end
42
+
43
+ it 'raises error when enabled w/o a schema' do
44
+ expect {
45
+ conf.relation(:tasks) do
46
+ use :auto_restrictions
47
+ end
48
+ }.to raise_error(
49
+ ROM::Plugins::Relation::SQL::AutoRestrictions::EmptySchemaError,
50
+ "ROM::Relation[Tasks] relation has no schema. Make sure :auto_restrictions is enabled after defining a schema"
51
+ )
52
+ end
53
+ end
54
+ end
@@ -38,7 +38,13 @@ RSpec.describe 'Schema inference for common datatypes', seeds: false do
38
38
  let(:dataset) { :tasks }
39
39
  let(:source) { ROM::Relation::Name[:tasks] }
40
40
 
41
- it 'can infer attributes for dataset' do
41
+ it 'can infer attributes for dataset' do |ex|
42
+ if mysql?(ex)
43
+ indexes = { index: %i(user_id).to_set }
44
+ else
45
+ indexes = {}
46
+ end
47
+
42
48
  expect(schema.to_h).
43
49
  to eql(
44
50
  id: ROM::SQL::Types::Serial.meta(name: :id, source: source),
@@ -47,7 +53,8 @@ RSpec.describe 'Schema inference for common datatypes', seeds: false do
47
53
  name: :user_id,
48
54
  foreign_key: true,
49
55
  source: source,
50
- target: :users
56
+ target: :users,
57
+ **indexes
51
58
  )
52
59
  )
53
60
  end
@@ -87,14 +94,15 @@ RSpec.describe 'Schema inference for common datatypes', seeds: false do
87
94
  it 'can infer attributes for dataset' do |ex|
88
95
  date_type = oracle?(ex) ? ROM::SQL::Types::Time : ROM::SQL::Types::Date
89
96
 
90
- expect(schema.to_h).to eql(
91
- id: ROM::SQL::Types::Serial.meta(name: :id, source: source),
92
- text: ROM::SQL::Types::String.meta(name: :text, source: source),
93
- time: ROM::SQL::Types::Time.optional.meta(name: :time, source: source),
94
- date: date_type.optional.meta(name: :date, source: source),
95
- datetime: ROM::SQL::Types::Time.meta(name: :datetime, source: source),
96
- data: ROM::SQL::Types::Blob.optional.meta(name: :data, source: source),
97
- )
97
+ expect(schema.to_h).
98
+ to eql(
99
+ id: ROM::SQL::Types::Serial.meta(name: :id, source: source),
100
+ text: ROM::SQL::Types::String.meta(name: :text, source: source),
101
+ time: ROM::SQL::Types::Time.optional.meta(name: :time, source: source),
102
+ date: date_type.optional.meta(name: :date, source: source),
103
+ datetime: ROM::SQL::Types::Time.meta(name: :datetime, source: source),
104
+ data: ROM::SQL::Types::Blob.optional.meta(name: :data, source: source),
105
+ )
98
106
  end
99
107
  end
100
108
 
@@ -130,16 +138,17 @@ RSpec.describe 'Schema inference for common datatypes', seeds: false do
130
138
 
131
139
  pending 'Add precision inferrence for Oracle' if oracle?(example)
132
140
 
133
- expect(schema.to_h).to eql(
134
- id: ROM::SQL::Types::Serial.meta(name: :id, source: source),
135
- dec: default_precision,
136
- dec_prec: decimal.meta(name: :dec_prec, precision: 12, scale: 0),
137
- num: decimal.meta(name: :num, precision: 5, scale: 2),
138
- small: ROM::SQL::Types::Int.optional.meta(name: :small, source: source),
139
- int: ROM::SQL::Types::Int.optional.meta(name: :int, source: source),
140
- floating: ROM::SQL::Types::Float.optional.meta(name: :floating, source: source),
141
- double_p: ROM::SQL::Types::Float.optional.meta(name: :double_p, source: source),
142
- )
141
+ expect(schema.to_h).
142
+ to eql(
143
+ id: ROM::SQL::Types::Serial.meta(name: :id, source: source),
144
+ dec: default_precision,
145
+ dec_prec: decimal.meta(name: :dec_prec, precision: 12, scale: 0),
146
+ num: decimal.meta(name: :num, precision: 5, scale: 2),
147
+ small: ROM::SQL::Types::Int.optional.meta(name: :small, source: source),
148
+ int: ROM::SQL::Types::Int.optional.meta(name: :int, source: source),
149
+ floating: ROM::SQL::Types::Float.optional.meta(name: :floating, source: source),
150
+ double_p: ROM::SQL::Types::Float.optional.meta(name: :double_p, source: source),
151
+ )
143
152
  end
144
153
  end
145
154
  end
@@ -300,5 +309,39 @@ RSpec.describe 'Schema inference for common datatypes', seeds: false do
300
309
  end
301
310
  end
302
311
  end
312
+
313
+ describe 'inferring indices', oracle: false do
314
+ before do |ex|
315
+ ctx = self
316
+
317
+ conn.create_table :test_inferrence do
318
+ primary_key :id
319
+ Integer :foo
320
+ Integer :bar, null: false
321
+ Integer :baz, null: false
322
+
323
+ index :foo, name: :foo_idx
324
+ index :bar, name: :bar_idx
325
+ index :baz, name: :baz1_idx
326
+ index :baz, name: :baz2_idx
327
+
328
+ index %i(bar baz), name: :composite_idx
329
+ end
330
+ end
331
+
332
+ let(:dataset) { :test_inferrence }
333
+ let(:source) { ROM::Relation::Name[dataset] }
334
+
335
+ it 'infers types with indices' do
336
+ int = ROM::SQL::Types::Int
337
+ expect(schema.to_h).
338
+ to eql(
339
+ id: int.meta(name: :id, source: source, primary_key: true),
340
+ foo: int.optional.meta(name: :foo, source: source, index: %i(foo_idx).to_set),
341
+ bar: int.meta(name: :bar, source: source, index: %i(bar_idx composite_idx).to_set),
342
+ baz: int.meta(name: :baz, source: source, index: %i(baz1_idx baz2_idx).to_set)
343
+ )
344
+ end
345
+ end
303
346
  end
304
347
  end
@@ -28,4 +28,10 @@ RSpec.describe 'Using legacy sequel api', :sqlite do
28
28
  expect(users.where(name: 'Jane').first).to eql(id: 1, name: 'Jane')
29
29
  end
30
30
  end
31
+
32
+ describe '#order' do
33
+ it 'orders relation' do
34
+ expect(users.order(:users__name).first).to eql(id: 1, name: 'Jane')
35
+ end
36
+ end
31
37
  end
@@ -13,7 +13,7 @@ RSpec.shared_context 'users and tasks' do
13
13
  conn.create_table :tasks do
14
14
  primary_key :id
15
15
  foreign_key :user_id, :users
16
- String :title, unique: true
16
+ String :title
17
17
  constraint(:title_length) { char_length(title) > 1 } if ctx.postgres?(example)
18
18
  constraint(:title_length) { length(title) > 1 } if ctx.sqlite?(example)
19
19
  end
@@ -0,0 +1,45 @@
1
+ RSpec.describe ROM::SQL::Relation, '#instrument', :sqlite do
2
+ include_context 'users and tasks'
3
+
4
+ subject(:relation) do
5
+ relations[:users]
6
+ end
7
+
8
+ let(:notifications) do
9
+ spy(:notifications)
10
+ end
11
+
12
+ before do
13
+ conf.plugin(:sql, relations: :instrumentation) do |p|
14
+ p.notifications = notifications
15
+ end
16
+ end
17
+
18
+ it 'instruments relation materialization' do
19
+ users.to_a
20
+
21
+ expect(notifications).
22
+ to have_received(:instrument).with(:sql, name: :users, query: users.dataset.sql)
23
+ end
24
+
25
+ it 'instruments methods that return a single tuple' do
26
+ users.first
27
+
28
+ expect(notifications).
29
+ to have_received(:instrument).with(:sql, name: :users, query: users.limit(1).dataset.sql)
30
+
31
+ users.last
32
+
33
+ expect(notifications).
34
+ to have_received(:instrument).with(:sql, name: :users, query: users.reverse.limit(1).dataset.sql)
35
+ end
36
+
37
+ it 'instruments aggregation methods' do
38
+ pending "no idea how to make this work with sequel"
39
+
40
+ users.count
41
+
42
+ expect(notifications).
43
+ to have_received(:instrument).with(:sql, name: :users, query: 'SELECT COUNT(*) FROM users')
44
+ end
45
+ end
@@ -17,5 +17,9 @@ RSpec.describe ROM::Relation, '#read' do
17
17
  expect(materialized).to match_array([{ name: 'Jane' }, { name: 'Joe' }])
18
18
  expect(materialized.source).to be(users)
19
19
  end
20
+
21
+ it 'has empty schema' do
22
+ expect(users.schema).to be_empty
23
+ end
20
24
  end
21
25
  end
@@ -3,54 +3,100 @@ RSpec.describe ROM::Relation, '#where' do
3
3
 
4
4
  include_context 'users and tasks'
5
5
 
6
- before do
7
- conf.relation(:tasks) { schema(infer: true) }
8
- end
9
-
10
6
  with_adapters do
11
- it 'restricts relation using provided conditions' do
12
- expect(relation.where(id: 1).to_a).
13
- to eql([{ id: 1, title: "Joe's task" }])
14
- end
7
+ context 'without :read types' do
8
+ it 'restricts relation using provided conditions' do
9
+ expect(relation.where(id: 1).to_a).
10
+ to eql([{ id: 1, title: "Joe's task" }])
11
+ end
15
12
 
16
- it 'restricts relation using provided conditions and block' do
17
- expect(relation.where(id: 1) { title.like("%Jane%") }.to_a).to be_empty
18
- end
13
+ it 'restricts relation using provided conditions and block' do
14
+ expect(relation.where(id: 1) { title.like("%Jane%") }.to_a).to be_empty
15
+ end
19
16
 
20
- it 'restricts relation using provided conditions in a block' do
21
- expect(relation.where { (id > 2) & title.like("%Jane%") }.to_a).to be_empty
22
- end
17
+ it 'restricts relation using provided conditions in a block' do
18
+ expect(relation.where { (id > 2) & title.like("%Jane%") }.to_a).to be_empty
19
+ end
23
20
 
24
- it 'restricts relation using canonical attributes' do
25
- expect(relation.rename(id: :user_id).where { id > 3 }.to_a).to be_empty
26
- end
21
+ it 'restricts relation using canonical attributes' do
22
+ expect(relation.rename(id: :user_id).where { id > 3 }.to_a).to be_empty
23
+ end
27
24
 
28
- it 'restricts with or condition' do
29
- expect(relation.where { id.is(1) | id.is(2) }.to_a).
30
- to eql([{ id: 1, title: "Joe's task" }, { id: 2, title: "Jane's task" }])
31
- end
25
+ it 'restricts with or condition' do
26
+ expect(relation.where { id.is(1) | id.is(2) }.to_a).
27
+ to eql([{ id: 1, title: "Joe's task" }, { id: 2, title: "Jane's task" }])
28
+ end
32
29
 
33
- it 'restricts with a range condition' do
34
- expect(relation.where { id.in(-1...2) }.to_a).
35
- to eql([{ id: 1, title: "Joe's task" }])
30
+ it 'restricts with a range condition' do
31
+ expect(relation.where { id.in(-1...2) }.to_a).
32
+ to eql([{ id: 1, title: "Joe's task" }])
36
33
 
37
- expect(relation.where { id.in(0...3) }.to_a).
38
- to eql([{ id: 1, title: "Joe's task" }, { id: 2, title: "Jane's task" }])
39
- end
34
+ expect(relation.where { id.in(0...3) }.to_a).
35
+ to eql([{ id: 1, title: "Joe's task" }, { id: 2, title: "Jane's task" }])
36
+ end
40
37
 
41
- it 'restricts with an inclusive range' do
42
- expect(relation.where { id.in(0..2) }.to_a).
43
- to eql([{ id: 1, title: "Joe's task" }, { id: 2, title: "Jane's task" }])
44
- end
38
+ it 'restricts with an inclusive range' do
39
+ expect(relation.where { id.in(0..2) }.to_a).
40
+ to eql([{ id: 1, title: "Joe's task" }, { id: 2, title: "Jane's task" }])
41
+ end
45
42
 
46
- it 'restricts with an ordinary enum' do
47
- expect(relation.where { id.in(2, 3) }.to_a).
48
- to eql([{ id: 2, title: "Jane's task" }])
43
+ it 'restricts with an ordinary enum' do
44
+ expect(relation.where { id.in(2, 3) }.to_a).
45
+ to eql([{ id: 2, title: "Jane's task" }])
46
+ end
47
+
48
+ it 'restricts with enum using self syntax' do
49
+ expect(relation.where(relation[:id].in(2, 3)).to_a).
50
+ to eql([{ id: 2, title: "Jane's task" }])
51
+ end
49
52
  end
50
53
 
51
- it 'restricts with enum using self syntax' do
52
- expect(relation.where(relation[:id].in(2, 3)).to_a).
53
- to eql([{ id: 2, title: "Jane's task" }])
54
+ context 'with :read types' do
55
+ before do
56
+ conf.relation(:tasks) do
57
+ schema(infer: true) do
58
+ attribute :id, ROM::SQL::Types::Serial.constructor(&:to_i)
59
+ attribute :title, ROM::SQL::Types::Coercible::String
60
+ end
61
+ end
62
+
63
+ module Test
64
+ Id = Struct.new(:v) do
65
+ def to_i
66
+ v.to_i
67
+ end
68
+ end
69
+
70
+ Title = Struct.new(:v) do
71
+ def to_s
72
+ v.to_s
73
+ end
74
+ end
75
+ end
76
+ end
77
+
78
+ it 'applies write_schema to hash conditions' do
79
+ rel = tasks.where(id: Test::Id.new('2'), title: Test::Title.new(:"Jane's task"))
80
+
81
+ expect(rel.first).
82
+ to eql(id: 2, user_id: 1, title: "Jane's task")
83
+ end
84
+
85
+ it 'applies write_schema to conditions with operators other than equality' do
86
+ rel = tasks.where { id >= Test::Id.new('2') }
87
+
88
+ expect(rel.first).
89
+ to eql(id: 2, user_id: 1, title: "Jane's task")
90
+ end
91
+
92
+ it 'applies write_schema to conditions in a block' do
93
+ rel = tasks.where {
94
+ id.is(Test::Id.new('2')) & title.is(Test::Title.new(:"Jane's task"))
95
+ }
96
+
97
+ expect(rel.first).
98
+ to eql(id: 2, user_id: 1, title: "Jane's task")
99
+ end
54
100
  end
55
101
  end
56
102
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rom-sql
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.3
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Piotr Solnica
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-02-23 00:00:00.000000000 Z
11
+ date: 2017-03-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: sequel
@@ -84,14 +84,14 @@ dependencies:
84
84
  requirements:
85
85
  - - "~>"
86
86
  - !ruby/object:Gem::Version
87
- version: '3.0'
87
+ version: '3.1'
88
88
  type: :runtime
89
89
  prerelease: false
90
90
  version_requirements: !ruby/object:Gem::Requirement
91
91
  requirements:
92
92
  - - "~>"
93
93
  - !ruby/object:Gem::Version
94
- version: '3.0'
94
+ version: '3.1'
95
95
  - !ruby/object:Gem::Dependency
96
96
  name: bundler
97
97
  requirement: !ruby/object:Gem::Requirement
@@ -154,7 +154,9 @@ files:
154
154
  - circle.yml
155
155
  - lib/rom-sql.rb
156
156
  - lib/rom/plugins/relation/sql/auto_combine.rb
157
+ - lib/rom/plugins/relation/sql/auto_restrictions.rb
157
158
  - lib/rom/plugins/relation/sql/auto_wrap.rb
159
+ - lib/rom/plugins/relation/sql/instrumentation.rb
158
160
  - lib/rom/sql.rb
159
161
  - lib/rom/sql/association.rb
160
162
  - lib/rom/sql/association/many_to_many.rb
@@ -241,6 +243,7 @@ files:
241
243
  - spec/integration/migration_spec.rb
242
244
  - spec/integration/plugins/associates/many_to_many_spec.rb
243
245
  - spec/integration/plugins/associates_spec.rb
246
+ - spec/integration/plugins/auto_restrictions_spec.rb
244
247
  - spec/integration/plugins/auto_wrap_spec.rb
245
248
  - spec/integration/relation_schema_spec.rb
246
249
  - spec/integration/schema/call_spec.rb
@@ -297,6 +300,7 @@ files:
297
300
  - spec/unit/relation/having_spec.rb
298
301
  - spec/unit/relation/inner_join_spec.rb
299
302
  - spec/unit/relation/inspect_spec.rb
303
+ - spec/unit/relation/instrument_spec.rb
300
304
  - spec/unit/relation/invert_spec.rb
301
305
  - spec/unit/relation/left_join_spec.rb
302
306
  - spec/unit/relation/map_spec.rb
@@ -372,6 +376,7 @@ test_files:
372
376
  - spec/integration/migration_spec.rb
373
377
  - spec/integration/plugins/associates/many_to_many_spec.rb
374
378
  - spec/integration/plugins/associates_spec.rb
379
+ - spec/integration/plugins/auto_restrictions_spec.rb
375
380
  - spec/integration/plugins/auto_wrap_spec.rb
376
381
  - spec/integration/relation_schema_spec.rb
377
382
  - spec/integration/schema/call_spec.rb
@@ -428,6 +433,7 @@ test_files:
428
433
  - spec/unit/relation/having_spec.rb
429
434
  - spec/unit/relation/inner_join_spec.rb
430
435
  - spec/unit/relation/inspect_spec.rb
436
+ - spec/unit/relation/instrument_spec.rb
431
437
  - spec/unit/relation/invert_spec.rb
432
438
  - spec/unit/relation/left_join_spec.rb
433
439
  - spec/unit/relation/map_spec.rb