rom-sql 1.0.0 → 1.0.1

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: 83c439ad1fcf4ee22b0a4f8d92f924b41b2a0d19
4
- data.tar.gz: ebd68c2d754a959b7f669cbcc1269d23108a663c
3
+ metadata.gz: dd4d5efa9ebc24506ae85cf772c6d57f6d59530b
4
+ data.tar.gz: 1fe47e3b07ee7979e92d47257b1ecdf4a258246b
5
5
  SHA512:
6
- metadata.gz: 4a8081d7fb278e307d71b3be25fe4d386e2a33aa6794b4706a1d7096daf2a29364a867e52e945a41c388f211ecbc0c280af23fe1a9ab9b9c0e730a0a7e5991cc
7
- data.tar.gz: 8de227092aa395b288d281cbfc69c5370424d76c856ef24f96ef308a4f1f110633556c9021207f74f96b01ee279f0916fc66dcbc20d8d9f4a5d7a99992c5391e
6
+ metadata.gz: 7338e9310e9ea94d333489edde055d213e2b466640c1366702cd08f78d636ee0577b5ec2f16e9e4c47b67847ff50f5fe23476d0440ba4e3fec9ad7c1e265c663
7
+ data.tar.gz: 6c26fb847b68e9f5add7b8255fbf8d4b99ecd387dbcc5897343658c21cf75358b0dc3db369351c70fd592b2f90ce10b428512df0ffeabd747a80685a5b2da97f
@@ -17,14 +17,11 @@ rvm:
17
17
  - 2.3
18
18
  - 2.2
19
19
  - rbx-3
20
- - jruby-9.1.6.0
20
+ - jruby-9.1.7.0
21
21
  env:
22
22
  global:
23
23
  - CODECLIMATE_REPO_TOKEN=03d7f66589572702b12426d2bc71c4de6281a96139e33b335b894264b1f8f0b0
24
24
  - JRUBY_OPTS='--dev -J-Xmx1024M'
25
- matrix:
26
- allow_failures:
27
- - rvm: jruby-9.1.6.0
28
25
  notifications:
29
26
  webhooks:
30
27
  urls:
@@ -1,3 +1,16 @@
1
+ ## v1.0.1 2017-02-09
2
+
3
+ ### Added
4
+
5
+ * Support for inferring the PostgreSQL `hstore` data type (flash-gordon)
6
+ * Support for the rest of geometric PostgreSQL data types (box, lseg, polygon, etc.) (Morozzzko)
7
+ * Added inferring for timestamp types with specified precision (flash-gordon)
8
+ * Added `ROM::SQL::Attribute#in` to support range checks in conditions (flash-gordon)
9
+
10
+ ### Fixed
11
+
12
+ * Missing primary key now leads to a more meaningful error (flash-gordon)
13
+
1
14
  ## v1.0.0 2017-01-29
2
15
 
3
16
  This release is based on rom core 3.0.0 with its improved Schema API, which is extended with SQL-specific features.
data/Gemfile CHANGED
@@ -7,6 +7,7 @@ gem 'rom-mapper', git: 'https://github.com/rom-rb/rom-mapper.git', branch: 'mast
7
7
 
8
8
  group :test do
9
9
  gem 'byebug', platforms: :mri
10
+ gem 'pry', platforms: %i(jruby rbx)
10
11
  gem 'dry-struct'
11
12
  gem 'activesupport', '~> 5.0'
12
13
  gem 'rspec', '~> 3.1'
data/Rakefile CHANGED
@@ -1,3 +1,4 @@
1
+ require "bundler/gem_tasks"
1
2
  require "rspec/core/rake_task"
2
3
 
3
4
  RSpec::Core::RakeTask.new(:spec)
@@ -32,10 +32,21 @@ module ROM
32
32
  #
33
33
  # @api private
34
34
  def for_wrap(keys, name)
35
- other = __registry__[name]
36
- other_dataset = other.name.dataset
35
+ rel, other =
36
+ if associations.key?(name)
37
+ assoc = associations[name]
38
+ other = __registry__[assoc.target.to_sym]
37
39
 
38
- schema.merge(other.schema.wrap).qualified.(inner_join(other_dataset, keys))
40
+ [assoc.join(__registry__, :inner_join, self), other]
41
+ else
42
+ # TODO: deprecate this before 2.0
43
+ other = __registry__[name]
44
+ other_dataset = other.name.dataset
45
+
46
+ [qualified.inner_join(other_dataset, keys), other]
47
+ end
48
+
49
+ rel.schema.merge(other.schema.wrap).qualified.(rel)
39
50
  end
40
51
  end
41
52
  end
@@ -149,7 +149,37 @@ module ROM
149
149
  #
150
150
  # @api public
151
151
  def is(other)
152
- Sequel::SQL::BooleanExpression.new(:'=', self, other)
152
+ __cmp__(:'=', other)
153
+ end
154
+
155
+ # Return a boolean expression with an inclusion test
156
+ #
157
+ # If the single argument passed to the method is a Range object
158
+ # then the resulting expression will restrict the attribute value
159
+ # with range's bounds. Upper bound condition will be inclusive/non-inclusive
160
+ # depending on the range type.
161
+ #
162
+ # If more than one argument is passed to the method or the first
163
+ # argument is not Range then the result will be a simple IN check.
164
+ #
165
+ # @example
166
+ # users.where { id.in(1..100) | created_at(((Time.now - 86400)..Time.now)) }
167
+ # users.where { id.in(1, 2, 3) }
168
+ # users.where(users[:id].in(1, 2, 3))
169
+ #
170
+ # @param [Array<Object>] *args A range or a list of values for an inclusion check
171
+ #
172
+ # @api public
173
+ def in(*args)
174
+ if args.first.is_a?(Range)
175
+ range = args.first
176
+ lower_cond = __cmp__(:>=, range.begin)
177
+ upper_cond = __cmp__(range.exclude_end? ? :< : :<=, range.end)
178
+
179
+ Sequel::SQL::BooleanExpression.new(:AND, lower_cond, upper_cond)
180
+ else
181
+ __cmp__(:IN, args)
182
+ end
153
183
  end
154
184
 
155
185
  # Create a function DSL from the attribute
@@ -213,6 +243,14 @@ module ROM
213
243
  super
214
244
  end
215
245
  end
246
+
247
+ # A simple wrapper for the boolean expression constructor where
248
+ # the left part is the attribute value
249
+ #
250
+ # @api private
251
+ def __cmp__(op, r)
252
+ Sequel::SQL::BooleanExpression.new(op, self, r)
253
+ end
216
254
  end
217
255
  end
218
256
  end
@@ -11,6 +11,7 @@ module ROM
11
11
  ForeignKeyConstraintError = Class.new(ConstraintError)
12
12
  CheckConstraintError = Class.new(ConstraintError)
13
13
  UnknownDBTypeError = Class.new(StandardError)
14
+ MissingPrimaryKeyError = Class.new(StandardError)
14
15
 
15
16
  ERROR_MAP = {
16
17
  Sequel::DatabaseError => DatabaseError,
@@ -24,7 +24,14 @@ module ROM
24
24
  'cidr' => Types::PG::IPAddress,
25
25
  'macaddr' => Types::String,
26
26
  'point' => Types::PG::PointT,
27
- 'xml' => Types::String
27
+ 'xml' => Types::String,
28
+ 'hstore' => Types::PG::HStore,
29
+ 'line' => Types::PG::LineT,
30
+ 'circle' => Types::PG::CircleT,
31
+ 'box' => Types::PG::BoxT,
32
+ 'lseg' => Types::PG::LineSegmentT,
33
+ 'polygon' => Types::PG::PolygonT,
34
+ 'path' => Types::PG::PathT
28
35
  ).freeze
29
36
 
30
37
  db_array_type_matcher Sequel::Postgres::PGArray::EMPTY_BRACKET
@@ -52,7 +59,8 @@ module ROM
52
59
  end
53
60
 
54
61
  def map_db_type(db_type)
55
- self.class.db_type_mapping[db_type]
62
+ self.class.db_type_mapping[db_type] ||
63
+ (db_type.start_with?('timestamp') ? Types::Time : nil)
56
64
  end
57
65
 
58
66
  def numeric?(ruby_type, db_type)
@@ -2,7 +2,7 @@ require 'dry-types'
2
2
  require 'sequel'
3
3
  require 'ipaddr'
4
4
 
5
- Sequel.extension(*%i(pg_array pg_array_ops pg_json pg_json_ops))
5
+ Sequel.extension(*%i(pg_array pg_array_ops pg_json pg_json_ops pg_hstore))
6
6
 
7
7
  module ROM
8
8
  module SQL
@@ -14,8 +14,7 @@ module ROM
14
14
 
15
15
  # Array
16
16
 
17
- Array = Dry::Types::Definition
18
- .new(Sequel::Postgres::PGArray)
17
+ Array = Types.Definition(Sequel::Postgres::PGArray)
19
18
 
20
19
  def self.Array(db_type)
21
20
  Array.constructor(-> (v) { Sequel.pg_array(v, db_type) }).meta(type: db_type)
@@ -41,6 +40,11 @@ module ROM
41
40
 
42
41
  JSONB = JSONBArray | JSONBHash | JSONBOp
43
42
 
43
+ # HStore
44
+
45
+ HStoreR = Types.Constructor(Hash, &:to_hash)
46
+ HStore = Types.Constructor(Sequel::Postgres::HStore, &Sequel.method(:hstore)).meta(read: HStoreR)
47
+
44
48
  Bytea = Types.Constructor(Sequel::SQL::Blob, &Sequel::SQL::Blob.method(:new))
45
49
 
46
50
  IPAddressR = Types.Constructor(IPAddr) { |ip| IPAddr.new(ip.to_s) }
@@ -49,14 +53,112 @@ module ROM
49
53
 
50
54
  Money = Types::Decimal
51
55
 
56
+ # Geometric types
57
+
52
58
  Point = ::Struct.new(:x, :y)
53
59
 
60
+ PointD = Types.Definition(Point)
61
+
54
62
  PointTR = Types.Constructor(Point) do |p|
55
63
  x, y = p.to_s[1...-1].split(',', 2)
56
64
  Point.new(Float(x), Float(y))
57
65
  end
58
66
 
59
67
  PointT = Types.Constructor(Point) { |p| "(#{ p.x },#{ p.y })" }.meta(read: PointTR)
68
+
69
+ Line = ::Struct.new(:a, :b, :c)
70
+
71
+ LineTR = Types.Constructor(Line) do |ln|
72
+ a, b, c = ln.to_s[1..-2].split(',', 3)
73
+ Line.new(Float(a), Float(b), Float(c))
74
+ end
75
+
76
+ LineT = Types.Constructor(Line) { |ln| "{#{ ln.a },#{ ln.b },#{ln.c}}"}.meta(read: LineTR)
77
+
78
+ Circle = ::Struct.new(:center, :radius)
79
+
80
+ CircleTR = Types.Constructor(Circle) do |c|
81
+ x, y, r = c.to_s.tr('()<>', '').split(',', 3)
82
+ center = Point.new(Float(x), Float(y))
83
+ Circle.new(center, Float(r))
84
+ end
85
+
86
+ CircleT = Types.Constructor(Circle) { |c| "<(#{ c.center.x },#{ c.center.y }),#{ c.radius }>" }.meta(read: CircleTR)
87
+
88
+ Box = ::Struct.new(:upper_right, :lower_left)
89
+
90
+ BoxTR = Types.Constructor(Box) do |b|
91
+ x_right, y_right, x_left, y_left = b.to_s.tr('()', '').split(',', 4)
92
+ upper_right = Point.new(Float(x_right), Float(y_right))
93
+ lower_left = Point.new(Float(x_left), Float(y_left))
94
+ Box.new(upper_right, lower_left)
95
+ end
96
+
97
+ BoxT = Types.Constructor(Box) { |b| "((#{ b.upper_right.x },#{ b.upper_right.y }),(#{ b.lower_left.x },#{ b.lower_left.y }))" }.meta(read: BoxTR)
98
+
99
+ LineSegment = ::Struct.new(:begin, :end)
100
+
101
+ LineSegmentTR = Types.Constructor(LineSegment) do |lseg|
102
+ x_begin, y_begin, x_end, y_end = lseg.to_s.tr('()[]', '').split(',', 4)
103
+ point_begin = Point.new(Float(x_begin), Float(y_begin))
104
+ point_end = Point.new(Float(x_end), Float(y_end))
105
+ LineSegment.new(point_begin, point_end)
106
+ end
107
+
108
+ LineSegmentT = Types.Constructor(LineSegment) do |lseg|
109
+ "[(#{ lseg.begin.x },#{ lseg.begin.y }),(#{ lseg.end.x },#{ lseg.end.y })]"
110
+ end.meta(read: LineSegmentTR)
111
+
112
+ Polygon = Types::Strict::Array.member(PointD)
113
+
114
+ PolygonTR = Polygon.constructor do |p|
115
+ coordinates = p.to_s.tr('()', '').split(',').each_slice(2)
116
+ points = coordinates.map { |x, y| Point.new(Float(x), Float(y)) }
117
+ Polygon[points]
118
+ end
119
+
120
+ PolygonT = PointD.constructor do |path|
121
+ points_joined = path.map { |p| "(#{ p.x },#{ p.y })" }.join(',')
122
+ "(#{ points_joined })"
123
+ end.meta(read: PolygonTR)
124
+
125
+ Path = ::Struct.new(:points, :type) do
126
+ def open?
127
+ type == :open
128
+ end
129
+
130
+ def closed?
131
+ type == :closed
132
+ end
133
+
134
+ def to_a
135
+ points
136
+ end
137
+ end
138
+
139
+ PathD = Types.Definition(Path)
140
+
141
+ PathTR = PathD.constructor do |path|
142
+ open = path.to_s.start_with?('[') && path.to_s.end_with?(']')
143
+ coordinates = path.to_s.tr('()[]', '').split(',').each_slice(2)
144
+ points = coordinates.map { |x, y| Point.new(Float(x), Float(y)) }
145
+
146
+ if open
147
+ Path.new(points, :open)
148
+ else
149
+ Path.new(points, :closed)
150
+ end
151
+ end
152
+
153
+ PathT = PathD.constructor do |path|
154
+ points_joined = path.to_a.map { |p| "(#{ p.x },#{ p.y })" }.join(',')
155
+
156
+ if path.open?
157
+ "[#{ points_joined }]"
158
+ else
159
+ "(#{ points_joined })"
160
+ end
161
+ end.meta(read: PathTR)
60
162
  end
61
163
  end
62
164
  end
@@ -24,7 +24,7 @@ module ROM
24
24
  end
25
25
 
26
26
  CONNECTION_EXTENSIONS = {
27
- postgres: %i(pg_array pg_json pg_enum)
27
+ postgres: %i(pg_array pg_json pg_enum pg_hstore)
28
28
  }.freeze
29
29
 
30
30
  # @!attribute [r] logger
@@ -91,7 +91,12 @@ module ROM
91
91
  #
92
92
  # @api public
93
93
  define_method(:by_pk) do |pk|
94
- where(schema[primary_key] => pk)
94
+ if primary_key.nil?
95
+ raise MissingPrimaryKeyError.new("Missing primary key for "\
96
+ ":#{ schema.name }")
97
+ else
98
+ where(schema[primary_key] => pk)
99
+ end
95
100
  end
96
101
  end
97
102
  end
@@ -9,6 +9,10 @@ module ROM
9
9
  ROM::Types.Constructor(*args, &block)
10
10
  end
11
11
 
12
+ def self.Definition(*args, &block)
13
+ ROM::Types.Definition(*args, &block)
14
+ end
15
+
12
16
  Serial = Int.meta(primary_key: true)
13
17
 
14
18
  Blob = Constructor(Sequel::SQL::Blob, &Sequel::SQL::Blob.method(:new))
@@ -1,5 +1,5 @@
1
1
  module ROM
2
2
  module SQL
3
- VERSION = '1.0.0'.freeze
3
+ VERSION = '1.0.1'.freeze
4
4
  end
5
5
  end
@@ -18,7 +18,7 @@ Gem::Specification.new do |spec|
18
18
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
19
  spec.require_paths = ['lib']
20
20
 
21
- spec.add_runtime_dependency 'sequel', '~> 4.42'
21
+ spec.add_runtime_dependency 'sequel', '~> 4.43'
22
22
  spec.add_runtime_dependency 'dry-equalizer', '~> 0.2'
23
23
  spec.add_runtime_dependency 'dry-types', '~> 0.9', '>= 0.9.4'
24
24
  spec.add_runtime_dependency 'dry-core', '~> 0.2', '>= 0.2.3'
@@ -141,4 +141,112 @@ RSpec.describe 'ROM::SQL::Types' do
141
141
  expect(described_class.meta[:read]['(7.5,30.5)']).to eql(point)
142
142
  end
143
143
  end
144
+
145
+ describe ROM::SQL::Types::PG::HStore do
146
+ let(:mapping) { Hash['hot' => 'cold'] }
147
+ let(:read_type) { described_class.meta[:read] }
148
+
149
+ it 'covertss data to Sequel::Postgres::HStore' do
150
+ expect(described_class[mapping]).to be_a Sequel::Postgres::HStore
151
+ expect(described_class[mapping]).to eql(Sequel.hstore(hot: :cold))
152
+ end
153
+
154
+ it 'reads Sequel::Postgres::HStore as a Hash object' do
155
+ expect(read_type[Sequel.hstore(mapping)]).to eql(mapping)
156
+ expect(read_type[Sequel.hstore(mapping)]).to be_a(Hash)
157
+ end
158
+ end
159
+
160
+ describe ROM::SQL::Types::PG::LineT do
161
+ let(:line) { ROM::SQL::Types::PG::Line.new(2.3, 4.9, 3.1415) }
162
+
163
+ it 'serializes a line using the {A,B,C} format' do
164
+ expect(described_class[line]).to eql('{2.3,4.9,3.1415}')
165
+ end
166
+
167
+ it 'reads the {A,B,C} format' do
168
+ expect(described_class.meta[:read]['{2.3,4.9,3.1415}']).to eql(line)
169
+ end
170
+ end
171
+
172
+ describe ROM::SQL::Types::PG::CircleT do
173
+ let(:center) { ROM::SQL::Types::PG::Point.new(7.5, 30.5) }
174
+ let(:circle) { ROM::SQL::Types::PG::Circle.new(center, 1.2) }
175
+
176
+ it 'serializes a circle using the <(x,y),r> format' do
177
+ expect(described_class[circle]).to eql('<(7.5,30.5),1.2>')
178
+ end
179
+
180
+ it 'reads the <(x,y),r> format' do
181
+ expect(described_class.meta[:read]['<(7.5,30.5),1.2>']).to eql(circle)
182
+ end
183
+ end
184
+
185
+ describe ROM::SQL::Types::PG::BoxT do
186
+ let(:lower_left) { ROM::SQL::Types::PG::Point.new(7.5, 20.5) }
187
+ let(:upper_right) { ROM::SQL::Types::PG::Point.new(8.5, 30.5) }
188
+
189
+ let(:box) { ROM::SQL::Types::PG::Box.new(upper_right, lower_left) }
190
+
191
+ it 'serializes a box' do
192
+ expect(described_class[box]).to eql('((8.5,30.5),(7.5,20.5))')
193
+ end
194
+
195
+ it 'reads serialized format' do
196
+ expect(described_class.meta[:read]['((8.5,30.5),(7.5,20.5))']).to eql(box)
197
+ end
198
+ end
199
+
200
+ describe ROM::SQL::Types::PG::LineSegmentT do
201
+ let(:first) { ROM::SQL::Types::PG::Point.new(8.5, 30.5) }
202
+ let(:second) { ROM::SQL::Types::PG::Point.new(7.5, 20.5) }
203
+
204
+ let(:lseg) { ROM::SQL::Types::PG::LineSegment.new(first, second) }
205
+
206
+ it 'serializes a lseg using [ ( x1 , y1 ) , ( x2 , y2 ) ] format' do
207
+ expect(described_class[lseg]).to eql('[(8.5,30.5),(7.5,20.5)]')
208
+ end
209
+
210
+ it 'reads serialized format' do
211
+ expect(described_class.meta[:read]['[(8.5,30.5),(7.5,20.5)]']).to eql(lseg)
212
+ end
213
+ end
214
+
215
+ describe ROM::SQL::Types::PG::PolygonT do
216
+ let(:first) { ROM::SQL::Types::PG::Point.new(8.5, 30.5) }
217
+ let(:second) { ROM::SQL::Types::PG::Point.new(7.5, 20.5) }
218
+ let(:third) { ROM::SQL::Types::PG::Point.new(6.5, 10.5) }
219
+
220
+ let(:polygon) { ROM::SQL::Types::PG::Polygon[[first, second, third]] }
221
+
222
+ it 'serializes a polygon using ( ( x1 , y1 ) ... ( xn , yn ) ) format' do
223
+ expect(described_class[polygon]).to eql('((8.5,30.5),(7.5,20.5),(6.5,10.5))')
224
+ end
225
+
226
+ it 'reads serialized format' do
227
+ expect(described_class.meta[:read]['((8.5,30.5),(7.5,20.5),(6.5,10.5))']).to eql(polygon)
228
+ end
229
+ end
230
+
231
+ describe ROM::SQL::Types::PG::PathT do
232
+ let(:first) { ROM::SQL::Types::PG::Point.new(8.5, 30.5) }
233
+ let(:second) { ROM::SQL::Types::PG::Point.new(7.5, 20.5) }
234
+ let(:third) { ROM::SQL::Types::PG::Point.new(6.5, 10.5) }
235
+
236
+ let(:closed_path) { ROM::SQL::Types::PG::Path.new([first, second, third], :closed) }
237
+ let(:open_path) { ROM::SQL::Types::PG::Path.new([first, second, third], :open) }
238
+
239
+ it 'serializes a closed path using ( ( x1 , y1 ) ... ( xn , yn ) ) format' do
240
+ expect(described_class[closed_path]).to eql('((8.5,30.5),(7.5,20.5),(6.5,10.5))')
241
+ end
242
+
243
+ it 'serializes an open path' do
244
+ expect(described_class[open_path]).to eql('[(8.5,30.5),(7.5,20.5),(6.5,10.5)]')
245
+ end
246
+
247
+ it 'reads serialized format' do
248
+ expect(described_class.meta[:read]['((8.5,30.5),(7.5,20.5),(6.5,10.5))']).to eql(closed_path)
249
+ expect(described_class.meta[:read]['[(8.5,30.5),(7.5,20.5),(6.5,10.5)]']).to eql(open_path)
250
+ end
251
+ end
144
252
  end
@@ -201,7 +201,6 @@ RSpec.describe 'Commands / Create', :postgres do
201
201
  end
202
202
 
203
203
  it 're-raises fk constraint violation error' do |ex|
204
- pending 'Waits for https://github.com/jeremyevans/sequel/pull/1283' if jruby? && sqlite?(ex)
205
204
  expect {
206
205
  tasks.try {
207
206
  tasks.create.call(user_id: 918_273_645)
@@ -4,9 +4,13 @@ RSpec.describe 'Plugins / :auto_wrap' do
4
4
 
5
5
  describe '#for_wrap' do
6
6
  shared_context 'joined tuple' do
7
+ let(:name) do
8
+ users.name.relation
9
+ end
10
+
7
11
  it 'returns joined tuples' do
8
12
  task_with_user = tasks
9
- .for_wrap({ id: :user_id }, users.name.relation)
13
+ .for_wrap({ id: :user_id }, name)
10
14
  .where(tasks__id: 2)
11
15
  .one
12
16
 
@@ -51,6 +55,24 @@ RSpec.describe 'Plugins / :auto_wrap' do
51
55
 
52
56
  include_context 'joined tuple'
53
57
  end
58
+
59
+ context 'using association' do
60
+ subject(:tasks) { relations[:tasks] }
61
+
62
+ let(:users) { relations[:users] }
63
+
64
+ before do
65
+ conf.relation(:tasks) {
66
+ schema(infer: true) { associations { belongs_to :users, as: :assignee } }
67
+ }
68
+
69
+ conf.relation(:users) { schema(infer: true) }
70
+ end
71
+
72
+ include_context 'joined tuple' do
73
+ let(:name) { :assignee }
74
+ end
75
+ end
54
76
  end
55
77
  end
56
78
  end
@@ -7,6 +7,11 @@ RSpec.describe 'ROM::SQL::Schema::MysqlInferrer', :mysql do
7
7
  conn.create_table :test_inferrence do
8
8
  tinyint :tiny
9
9
  mediumint :medium
10
+ datetime :created_at
11
+ column :date_and_time, 'datetime(0)'
12
+ column :time_with_ms, 'datetime(3)'
13
+ timestamp :unix_time_usec
14
+ column :unix_time_sec, 'timestamp(0) null'
10
15
  end
11
16
  end
12
17
 
@@ -31,6 +36,11 @@ RSpec.describe 'ROM::SQL::Schema::MysqlInferrer', :mysql do
31
36
  expect(schema.to_h).to eql(
32
37
  tiny: ROM::SQL::Types::Int.optional.meta(name: :tiny, source: source),
33
38
  medium: ROM::SQL::Types::Int.optional.meta(name: :medium, source: source),
39
+ created_at: ROM::SQL::Types::Time.optional.meta(name: :created_at, source: source),
40
+ date_and_time: ROM::SQL::Types::Time.optional.meta(name: :date_and_time, source: source),
41
+ time_with_ms: ROM::SQL::Types::Time.optional.meta(name: :time_with_ms, source: source),
42
+ unix_time_usec: ROM::SQL::Types::Time.meta(name: :unix_time_usec, source: source),
43
+ unix_time_sec: ROM::SQL::Types::Time.optional.meta(name: :unix_time_sec, source: source)
34
44
  )
35
45
  end
36
46
  end
@@ -1,4 +1,3 @@
1
-
2
1
  RSpec.describe 'ROM::SQL::Schema::PostgresInferrer', :postgres do
3
2
  include_context 'database setup'
4
3
 
@@ -7,6 +6,7 @@ RSpec.describe 'ROM::SQL::Schema::PostgresInferrer', :postgres do
7
6
  before do
8
7
  conn.extension :pg_enum
9
8
 
9
+ conn.execute('create extension if not exists hstore')
10
10
  conn.drop_table?(:test_inferrence)
11
11
  conn.drop_enum(:rainbow, if_exists: true)
12
12
 
@@ -25,6 +25,16 @@ RSpec.describe 'ROM::SQL::Schema::PostgresInferrer', :postgres do
25
25
  rainbow :color
26
26
  point :center
27
27
  xml :page
28
+ hstore :mapping
29
+ line :line
30
+ circle :circle
31
+ box :box
32
+ lseg :lseg
33
+ polygon :polygon
34
+ path :path
35
+ timestamp :created_at
36
+ column :datetime, "timestamp(0) without time zone"
37
+ column :datetime_tz, "timestamp(0) with time zone"
28
38
  end
29
39
  end
30
40
 
@@ -71,7 +81,45 @@ RSpec.describe 'ROM::SQL::Schema::PostgresInferrer', :postgres do
71
81
  source: source,
72
82
  read: ROM::SQL::Types::PG::PointTR.optional
73
83
  ),
74
- page: ROM::SQL::Types::String.optional.meta(name: :page, source: source)
84
+ page: ROM::SQL::Types::String.optional.meta(name: :page, source: source),
85
+ mapping: ROM::SQL::Types::PG::HStore.optional.meta(
86
+ name: :mapping,
87
+ source: source,
88
+ read: ROM::SQL::Types::PG::HStoreR.optional
89
+ ),
90
+ line: ROM::SQL::Types::PG::LineT.optional.meta(
91
+ name: :line,
92
+ source: source,
93
+ read: ROM::SQL::Types::PG::LineTR.optional
94
+ ),
95
+ circle: ROM::SQL::Types::PG::CircleT.optional.meta(
96
+ name: :circle,
97
+ source: source,
98
+ read: ROM::SQL::Types::PG::CircleTR.optional
99
+ ),
100
+ box: ROM::SQL::Types::PG::BoxT.optional.meta(
101
+ name: :box,
102
+ source: source,
103
+ read: ROM::SQL::Types::PG::BoxTR.optional
104
+ ),
105
+ lseg: ROM::SQL::Types::PG::LineSegmentT.optional.meta(
106
+ name: :lseg,
107
+ source: source,
108
+ read: ROM::SQL::Types::PG::LineSegmentTR.optional
109
+ ),
110
+ polygon: ROM::SQL::Types::PG::PolygonT.optional.meta(
111
+ name: :polygon,
112
+ source: source,
113
+ read: ROM::SQL::Types::PG::PolygonTR.optional
114
+ ),
115
+ path: ROM::SQL::Types::PG::PathT.optional.meta(
116
+ name: :path,
117
+ source: source,
118
+ read: ROM::SQL::Types::PG::PathTR.optional
119
+ ),
120
+ created_at: ROM::SQL::Types::Time.optional.meta(name: :created_at, source: source),
121
+ datetime: ROM::SQL::Types::Time.optional.meta(name: :datetime, source: source),
122
+ datetime_tz: ROM::SQL::Types::Time.optional.meta(name: :datetime_tz, source: source)
75
123
  )
76
124
  end
77
125
  end
@@ -90,10 +138,20 @@ RSpec.describe 'ROM::SQL::Schema::PostgresInferrer', :postgres do
90
138
  context 'with a column with bi-directional mapping' do
91
139
  before do
92
140
  conn.drop_table?(:test_bidirectional)
141
+ conn.execute('create extension if not exists hstore')
142
+
93
143
  conn.create_table(:test_bidirectional) do
94
144
  primary_key :id
95
145
  inet :ip
96
146
  point :center
147
+ hstore :mapping
148
+ line :line
149
+ circle :circle
150
+ box :box
151
+ lseg :lseg
152
+ polygon :polygon
153
+ path :closed_path
154
+ path :open_path
97
155
  end
98
156
 
99
157
  conf.relation(:test_bidirectional) { schema(infer: true) }
@@ -106,14 +164,38 @@ RSpec.describe 'ROM::SQL::Schema::PostgresInferrer', :postgres do
106
164
  end
107
165
 
108
166
  let(:point) { ROM::SQL::Types::PG::Point.new(7.5, 30.5) }
167
+ let(:point_2) { ROM::SQL::Types::PG::Point.new(8.5, 35.5) }
168
+ let(:line) { ROM::SQL::Types::PG::Line.new(2.3, 4.9, 3.1415) }
109
169
  let(:dns) { IPAddr.new('8.8.8.8') }
170
+ let(:mapping) { Hash['hot' => 'cold'] }
171
+ let(:circle) { ROM::SQL::Types::PG::Circle.new(point, 1.0) }
172
+ let(:lseg) { ROM::SQL::Types::PG::LineSegment.new(point, point_2) }
173
+ let(:box_corrected) { ROM::SQL::Types::PG::Box.new(point_2, point) }
174
+ let(:box) do
175
+ upper_left = ROM::SQL::Types::PG::Point.new(point.x, point_2.y)
176
+ lower_right = ROM::SQL::Types::PG::Point.new(point_2.x, point.y)
177
+
178
+ ROM::SQL::Types::PG::Box.new(upper_left, lower_right)
179
+ end
180
+ let(:polygon) { ROM::SQL::Types::PG::Polygon[[point, point_2]] }
181
+ let(:closed_path) { ROM::SQL::Types::PG::Path.new([point, point_2], :closed) }
182
+ let(:open_path) { ROM::SQL::Types::PG::Path.new([point, point_2], :open) }
110
183
 
111
184
  let(:relation) { container.relations[:test_bidirectional] }
112
185
  let(:create) { commands[:test_bidirectional].create }
113
186
 
114
- it 'writes and reads data' do
115
- inserted = create.call(id: 1, center: point, ip: dns)
116
- expect(inserted).to eql(id: 1, center: point, ip: dns)
187
+ it 'writes and reads data & corrects data' do
188
+ # Box coordinates are reordered if necessary
189
+ inserted = create.call(
190
+ id: 1, center: point, ip: dns, mapping: mapping,
191
+ line: line, circle: circle, lseg: lseg, box: box,
192
+ polygon: polygon, closed_path: closed_path, open_path: open_path
193
+ )
194
+ expect(inserted).to eql(
195
+ id: 1, center: point, ip: dns, mapping: mapping,
196
+ line: line, circle: circle, lseg: lseg, box: box_corrected,
197
+ polygon: polygon, closed_path: closed_path, open_path: open_path
198
+ )
117
199
  expect(relation.to_a).to eql([inserted])
118
200
  end
119
201
  end
@@ -16,7 +16,8 @@ require 'tempfile'
16
16
 
17
17
  begin
18
18
  require 'byebug'
19
- rescue LoadError # rubocop:disable Lint/HandleExceptions
19
+ rescue LoadError
20
+ require 'pry'
20
21
  end
21
22
 
22
23
  LOGGER = Logger.new(File.open('./log/test.log', 'a'))
@@ -40,11 +41,14 @@ PG_LTE_95 = ENV.fetch('PG_LTE_95', 'true') == 'true'
40
41
 
41
42
  SPEC_ROOT = root = Pathname(__FILE__).dirname
42
43
 
43
- # Redirect inferrer warnings to a log file
44
- $stderr.reopen(SPEC_ROOT.join('../log/warnings.log'), 'w')
45
-
46
44
  TMP_PATH = root.join('../tmp')
47
45
 
46
+ class ROM::SQL::Schema::Inferrer
47
+ def self.on_error(*)
48
+ # quiet in specs
49
+ end
50
+ end
51
+
48
52
  Dir[root.join('shared/**/*')].each { |f| require f }
49
53
  Dir[root.join('support/**/*')].each { |f| require f }
50
54
 
@@ -50,7 +50,7 @@ RSpec.describe ROM::SQL::Gateway, :postgres do
50
50
  extensions = [:pg_array, :pg_array_ops]
51
51
  connection = Sequel.connect uri
52
52
 
53
- expect(connection).to receive(:extension).with(:pg_array, :pg_json, :pg_enum, :pg_array_ops)
53
+ expect(connection).to receive(:extension).with(:pg_array, :pg_json, :pg_enum, :pg_hstore, :pg_array_ops)
54
54
  expect(connection).to receive(:extension).with(:freeze_datasets) unless RUBY_ENGINE == 'rbx'
55
55
 
56
56
  ROM::SQL::Gateway.new(connection, extensions: extensions)
@@ -65,13 +65,17 @@ RSpec.describe 'Plugin / Timestamp' do
65
65
  expect(updated[:updated_at]).not_to eq initial[:updated_at]
66
66
  end
67
67
 
68
- it "allows overriding timestamps" do
68
+ it "allows overriding timestamps" do |ex|
69
69
  tomorrow = (Time.now + (60 * 60 * 24))
70
70
 
71
71
  container.command(:notes).create.call(text: "testing")
72
72
  updated = container.command(:notes).update.call(text: "updated test", updated_at: tomorrow).first
73
73
 
74
- expect(updated[:updated_at].iso8601).to eq tomorrow.iso8601
74
+ if jruby? && sqlite?(ex)
75
+ expect(updated[:updated_at]).to eql(tomorrow.strftime('%Y-%m-%d %H:%M:%S.%6N'))
76
+ else
77
+ expect(updated[:updated_at].iso8601).to eql(tomorrow.iso8601)
78
+ end
75
79
  end
76
80
  end
77
81
  end
@@ -25,5 +25,30 @@ RSpec.describe ROM::Relation, '#by_pk' do
25
25
  expect(relation.by_pk(1, 1).to_a).to eql([{ tag_id: 1, task_id: 1 }])
26
26
  end
27
27
  end
28
+
29
+ context 'without PK' do
30
+ subject(:relation) { relations[:people] }
31
+
32
+ before do
33
+ conn.drop_table?(:people)
34
+
35
+ conn.create_table(:people) do
36
+ column :name, String
37
+ end
38
+
39
+ conf.relation(:people) do
40
+ schema do
41
+ attribute :name, ROM::SQL::Types::String
42
+ end
43
+ end
44
+ end
45
+
46
+ it 'raises a meaningful exception' do
47
+ expect { relation.by_pk(1) }.to \
48
+ raise_error(
49
+ ROM::SQL::MissingPrimaryKeyError,
50
+ 'Missing primary key for :people')
51
+ end
52
+ end
28
53
  end
29
54
  end
@@ -24,5 +24,33 @@ RSpec.describe ROM::Relation, '#where' do
24
24
  it 'restricts relation using canonical attributes' do
25
25
  expect(relation.rename(id: :user_id).where { id > 3 }.to_a).to be_empty
26
26
  end
27
+
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
32
+
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" }])
36
+
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
40
+
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
45
+
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" }])
49
+ end
50
+
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
+ end
27
55
  end
28
56
  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.0
4
+ version: 1.0.1
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-01-29 00:00:00.000000000 Z
11
+ date: 2017-02-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: sequel
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: '4.42'
19
+ version: '4.43'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: '4.42'
26
+ version: '4.43'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: dry-equalizer
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -320,7 +320,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
320
320
  version: '0'
321
321
  requirements: []
322
322
  rubyforge_project:
323
- rubygems_version: 2.5.1
323
+ rubygems_version: 2.5.2
324
324
  signing_key:
325
325
  specification_version: 4
326
326
  summary: SQL databases support for ROM