rom-sql 1.0.3 → 1.1.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.
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