rom-sql 0.3.2 → 0.4.0.beta1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.rubocop.yml +55 -18
  4. data/.rubocop_todo.yml +15 -0
  5. data/.travis.yml +10 -5
  6. data/CHANGELOG.md +18 -0
  7. data/Gemfile +8 -1
  8. data/Guardfile +24 -0
  9. data/README.md +14 -22
  10. data/Rakefile +13 -5
  11. data/lib/rom/sql.rb +5 -5
  12. data/lib/rom/sql/commands.rb +7 -49
  13. data/lib/rom/sql/commands/create.rb +29 -0
  14. data/lib/rom/sql/commands/delete.rb +18 -0
  15. data/lib/rom/sql/commands/transaction.rb +17 -0
  16. data/lib/rom/sql/commands/update.rb +54 -0
  17. data/lib/rom/sql/commands_ext/postgres.rb +24 -0
  18. data/lib/rom/sql/header.rb +8 -9
  19. data/lib/rom/sql/migration.rb +26 -0
  20. data/lib/rom/sql/plugin/pagination.rb +93 -0
  21. data/lib/rom/sql/rake_task.rb +2 -0
  22. data/lib/rom/sql/relation.rb +320 -0
  23. data/lib/rom/sql/relation/associations.rb +104 -0
  24. data/lib/rom/sql/relation/class_methods.rb +47 -0
  25. data/lib/rom/sql/relation/inspection.rb +16 -0
  26. data/lib/rom/sql/repository.rb +59 -0
  27. data/lib/rom/sql/support/rails_log_subscriber.rb +1 -1
  28. data/lib/rom/sql/tasks/migration_tasks.rake +56 -0
  29. data/lib/rom/sql/version.rb +1 -1
  30. data/rom-sql.gemspec +2 -3
  31. data/spec/integration/commands/create_spec.rb +66 -8
  32. data/spec/integration/commands/delete_spec.rb +22 -3
  33. data/spec/integration/commands/update_spec.rb +57 -6
  34. data/spec/integration/read_spec.rb +42 -1
  35. data/spec/shared/database_setup.rb +10 -5
  36. data/spec/spec_helper.rb +17 -0
  37. data/spec/support/active_support_notifications_spec.rb +5 -4
  38. data/spec/support/rails_log_subscriber_spec.rb +2 -2
  39. data/spec/unit/logger_spec.rb +5 -3
  40. data/spec/unit/many_to_many_spec.rb +2 -2
  41. data/spec/unit/migration_spec.rb +34 -0
  42. data/spec/unit/migration_tasks_spec.rb +99 -0
  43. data/spec/unit/one_to_many_spec.rb +0 -2
  44. data/spec/unit/plugin/pagination_spec.rb +73 -0
  45. data/spec/unit/relation_spec.rb +49 -3
  46. data/spec/unit/repository_spec.rb +33 -0
  47. data/spec/unit/schema_spec.rb +5 -17
  48. metadata +32 -35
  49. data/lib/rom/sql/adapter.rb +0 -100
  50. data/lib/rom/sql/relation_inclusion.rb +0 -149
  51. data/lib/rom/sql/support/sequel_dataset_ext.rb +0 -33
  52. data/spec/unit/adapter_spec.rb +0 -48
  53. data/spec/unit/config_spec.rb +0 -54
@@ -0,0 +1,18 @@
1
+ require 'rom/sql/commands'
2
+ require 'rom/sql/commands/transaction'
3
+
4
+ module ROM
5
+ module SQL
6
+ module Commands
7
+ class Delete < ROM::Commands::Delete
8
+ include Transaction
9
+
10
+ def execute
11
+ deleted = target.to_a
12
+ target.delete
13
+ deleted
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,17 @@
1
+ require 'rom/commands/result'
2
+
3
+ module ROM
4
+ module SQL
5
+ module Commands
6
+ module Transaction
7
+ ROM::SQL::Rollback = Class.new(Sequel::Rollback)
8
+
9
+ def transaction(options = {}, &block)
10
+ ROM::Commands::Result::Success.new(
11
+ relation.dataset.db.transaction(options, &block)
12
+ )
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,54 @@
1
+ require 'rom/sql/commands'
2
+ require 'rom/sql/commands/transaction'
3
+
4
+ module ROM
5
+ module SQL
6
+ module Commands
7
+ class Update < ROM::Commands::Update
8
+ include Transaction
9
+
10
+ option :original, type: Hash, reader: true
11
+
12
+ alias_method :to, :call
13
+
14
+ def execute(tuple)
15
+ attributes = input[tuple]
16
+ validator.call(attributes)
17
+
18
+ changed = diff(attributes.to_h)
19
+
20
+ if changed.any?
21
+ update(changed)
22
+ else
23
+ []
24
+ end
25
+ end
26
+
27
+ def change(original)
28
+ self.class.new(relation, options.merge(original: original))
29
+ end
30
+
31
+ def update(tuple)
32
+ pks = relation.map { |t| t[primary_key] }
33
+ dataset = relation.dataset
34
+ dataset.update(tuple)
35
+ dataset.unfiltered.where(primary_key => pks).to_a
36
+ end
37
+
38
+ def primary_key
39
+ relation.primary_key
40
+ end
41
+
42
+ private
43
+
44
+ def diff(tuple)
45
+ if original
46
+ Hash[tuple.to_a - (tuple.to_a & original.to_a)]
47
+ else
48
+ tuple
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,24 @@
1
+ require 'rom/sql/commands/create'
2
+ require 'rom/sql/commands/update'
3
+
4
+ module ROM
5
+ module SQL
6
+ module Commands
7
+ module Postgres
8
+ module Create
9
+ def insert(tuples)
10
+ tuples.map do |tuple|
11
+ relation.dataset.returning(*relation.columns).insert(tuple)
12
+ end.flatten
13
+ end
14
+ end
15
+
16
+ module Update
17
+ def update(tuple)
18
+ relation.dataset.returning(*relation.columns).update(tuple)
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -2,13 +2,12 @@ module ROM
2
2
  module SQL
3
3
  # @private
4
4
  class Header
5
- include Charlatan.new(:columns)
6
5
  include Equalizer.new(:columns, :table)
7
6
 
8
- attr_reader :table
7
+ attr_reader :columns, :table
9
8
 
10
9
  def initialize(columns, table)
11
- super
10
+ @columns = columns
12
11
  @table = table
13
12
  end
14
13
 
@@ -25,19 +24,19 @@ module ROM
25
24
  end
26
25
 
27
26
  def names
28
- map { |col| :"#{col.to_s.split('___').last}" }
27
+ columns.map { |col| :"#{col.to_s.split('___').last}" }
29
28
  end
30
29
 
31
30
  def project(*names)
32
- find_all { |col| names.include?(col) }
31
+ self.class.new(columns.find_all { |col| names.include?(col) }, table)
33
32
  end
34
33
 
35
34
  def qualified
36
- map { |col| :"#{table}__#{col}" }
35
+ self.class.new(columns.map { |col| :"#{table}__#{col}" }, table)
37
36
  end
38
37
 
39
38
  def rename(options)
40
- map do |col|
39
+ self.class.new(columns.map { |col|
41
40
  new_name = options[col]
42
41
 
43
42
  if new_name
@@ -45,11 +44,11 @@ module ROM
45
44
  else
46
45
  col
47
46
  end
48
- end
47
+ }, table)
49
48
  end
50
49
 
51
50
  def prefix(col_prefix)
52
- rename(Hash[map { |col| [col, :"#{col_prefix}_#{col}"] }])
51
+ rename(Hash[columns.map { |col| [col, :"#{col_prefix}_#{col}"] }])
53
52
  end
54
53
  end
55
54
  end
@@ -0,0 +1,26 @@
1
+ module ROM
2
+ module SQL
3
+ class Migration
4
+ ::Sequel.extension :migration
5
+
6
+ DEFAULT_PATH = 'db/migrate'
7
+
8
+ class << self
9
+ attr_writer :path
10
+ attr_accessor :connection
11
+
12
+ def path
13
+ @path || DEFAULT_PATH
14
+ end
15
+
16
+ def run(options = {})
17
+ ::Sequel::Migrator.run(connection, path, options)
18
+ end
19
+
20
+ def create(&block)
21
+ ::Sequel.migration(&block)
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,93 @@
1
+ module ROM
2
+ module SQL
3
+ module Plugin
4
+ module Pagination
5
+ class Pager
6
+ include Options
7
+ include Equalizer.new(:current_page, :per_page)
8
+
9
+ option :current_page, reader: true, default: 1
10
+ option :per_page, reader: true
11
+
12
+ attr_reader :dataset
13
+ attr_reader :current_page
14
+
15
+ def initialize(dataset, options = {})
16
+ @dataset = dataset
17
+ super
18
+ end
19
+
20
+ def next_page
21
+ num = current_page + 1
22
+ num if total_pages >= num
23
+ end
24
+
25
+ def prev_page
26
+ num = current_page - 1
27
+ num if num > 0
28
+ end
29
+
30
+ def total
31
+ dataset.unlimited.count
32
+ end
33
+
34
+ def total_pages
35
+ (total / per_page) + 1
36
+ end
37
+
38
+ def at(num, per_page = options[:per_page])
39
+ self.class.new(
40
+ dataset.offset((num-1)*per_page).limit(per_page),
41
+ options.merge(current_page: num, per_page: per_page)
42
+ )
43
+ end
44
+
45
+ alias_method :limit_value, :per_page
46
+ end
47
+
48
+ def self.included(klass)
49
+ super
50
+
51
+ klass.class_eval do
52
+ defines :per_page
53
+
54
+ option :pager, reader: true, default: proc { |relation|
55
+ Pager.new(relation.dataset, per_page: relation.class.per_page)
56
+ }
57
+
58
+ exposed_relations.merge([:pager, :page, :per_page])
59
+ end
60
+ end
61
+
62
+ # Paginate a relation
63
+ #
64
+ # @example
65
+ # rom.relation(:users).class.per_page(10)
66
+ # rom.relation(:users).page(1)
67
+ # rom.relation(:users).pager # => info about pagination
68
+ #
69
+ # @return [Relation]
70
+ #
71
+ # @api public
72
+ def page(num)
73
+ num = num.to_i
74
+ next_pager = pager.at(num)
75
+ __new__(next_pager.dataset, pager: next_pager)
76
+ end
77
+
78
+ # Set limit for pagination
79
+ #
80
+ # @example
81
+ # rom.relation(:users).page(2).per_page(10)
82
+ #
83
+ # @api public
84
+ def per_page(num)
85
+ num = num.to_i
86
+ next_pager = pager.at(pager.current_page, num)
87
+ __new__(next_pager.dataset, pager: next_pager)
88
+ end
89
+
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,2 @@
1
+ require 'rake'
2
+ load 'rom/sql/tasks/migration_tasks.rake'
@@ -0,0 +1,320 @@
1
+ require 'rom/sql/header'
2
+
3
+ require 'rom/sql/relation/class_methods'
4
+ require 'rom/sql/relation/inspection'
5
+ require 'rom/sql/relation/associations'
6
+
7
+ module ROM
8
+ module SQL
9
+ # Sequel-specific relation extensions
10
+ #
11
+ class Relation < ROM::Relation
12
+ extend ClassMethods
13
+
14
+ include Inspection
15
+ include Associations
16
+
17
+ attr_reader :header, :table
18
+
19
+ # @api private
20
+ def initialize(dataset, registry = {})
21
+ super
22
+ @table = dataset.opts[:from].first
23
+ end
24
+
25
+ # Return a header for this relation
26
+ #
27
+ # @return [Header]
28
+ #
29
+ # @api private
30
+ def header
31
+ @header ||= Header.new(dataset.opts[:select] || dataset.columns, table)
32
+ end
33
+
34
+ # Return raw column names
35
+ #
36
+ # @return [Array<Symbol>]
37
+ #
38
+ # @api private
39
+ def columns
40
+ dataset.columns
41
+ end
42
+
43
+ # @api public
44
+ def project(*names)
45
+ select(*header.project(*names))
46
+ end
47
+
48
+ # @api public
49
+ def rename(options)
50
+ select(*header.rename(options))
51
+ end
52
+
53
+ # @api public
54
+ def prefix(name = Inflector.singularize(table))
55
+ rename(header.prefix(name).to_h)
56
+ end
57
+
58
+ # @api public
59
+ def qualified
60
+ select(*qualified_columns)
61
+ end
62
+
63
+ # @api public
64
+ def qualified_columns
65
+ header.qualified.to_a
66
+ end
67
+
68
+ # Get first tuple from the relation
69
+ #
70
+ # @example
71
+ # users.first
72
+ #
73
+ # @return [Relation]
74
+ #
75
+ # @api public
76
+ def first
77
+ dataset.first
78
+ end
79
+
80
+ # Get last tuple from the relation
81
+ #
82
+ # @example
83
+ # users.first
84
+ #
85
+ # @return [Relation]
86
+ #
87
+ # @api public
88
+ def last
89
+ dataset.last
90
+ end
91
+
92
+ # Return relation count
93
+ #
94
+ # @example
95
+ # users.count # => 12
96
+ #
97
+ # @return [Relation]
98
+ #
99
+ # @api public
100
+ def count
101
+ dataset.count
102
+ end
103
+
104
+ # Select specific columns for select clause
105
+ #
106
+ # @example
107
+ # users.select(:id, :name)
108
+ #
109
+ # @return [Relation]
110
+ #
111
+ # @api public
112
+ def select(*args, &block)
113
+ __new__(dataset.__send__(__method__, *args, &block))
114
+ end
115
+
116
+ # Append specific columns to select clause
117
+ #
118
+ # @example
119
+ # users.select(:id, :name).select_append(:email)
120
+ #
121
+ # @return [Relation]
122
+ #
123
+ # @api public
124
+ def select_append(*args, &block)
125
+ __new__(dataset.__send__(__method__, *args, &block))
126
+ end
127
+
128
+ # Restrict a relation to match criteria
129
+ #
130
+ # @example
131
+ # users.where(name: 'Jane')
132
+ #
133
+ # @return [Relation]
134
+ #
135
+ # @api public
136
+ def where(*args, &block)
137
+ __new__(dataset.__send__(__method__, *args, &block))
138
+ end
139
+
140
+ # Set order for the relation
141
+ #
142
+ # @example
143
+ # users.order(:name)
144
+ #
145
+ # @return [Relation]
146
+ #
147
+ # @api public
148
+ def order(*args, &block)
149
+ __new__(dataset.__send__(__method__, *args, &block))
150
+ end
151
+
152
+ # Reverse the order of the relation
153
+ #
154
+ # @example
155
+ # users.order(:name).reverse
156
+ #
157
+ # @return [Relation]
158
+ #
159
+ # @api public
160
+ def reverse(*args, &block)
161
+ __new__(dataset.__send__(__method__, *args, &block))
162
+ end
163
+
164
+ # Limit a relation to a specific number of tuples
165
+ #
166
+ # @example
167
+ # users.limit(1)
168
+ #
169
+ # @return [Relation]
170
+ #
171
+ # @api public
172
+ def limit(*args, &block)
173
+ __new__(dataset.__send__(__method__, *args, &block))
174
+ end
175
+
176
+ # Set offset for the relation
177
+ #
178
+ # @example
179
+ # users.limit(10).offset(2)
180
+ #
181
+ # @return [Relation]
182
+ #
183
+ # @api public
184
+ def offset(*args, &block)
185
+ __new__(dataset.__send__(__method__, *args, &block))
186
+ end
187
+
188
+ # Map tuples from the relation
189
+ #
190
+ # @example
191
+ # users.map { |user| ... }
192
+ #
193
+ # @api public
194
+ def map(&block)
195
+ to_enum.map(&block)
196
+ end
197
+
198
+ # Join other relation using inner join
199
+ #
200
+ # @param [Symbol] relation name
201
+ # @param [Hash] join keys
202
+ #
203
+ # @return [Relation]
204
+ #
205
+ # @api public
206
+ def inner_join(*args, &block)
207
+ __new__(dataset.__send__(__method__, *args, &block))
208
+ end
209
+
210
+ # Join other relation using left outer join
211
+ #
212
+ # @param [Symbol] relation name
213
+ # @param [Hash] join keys
214
+ #
215
+ # @return [Relation]
216
+ #
217
+ # @api public
218
+ def left_join(*args, &block)
219
+ __new__(dataset.__send__(__method__, *args, &block))
220
+ end
221
+
222
+ # Group by specific columns
223
+ #
224
+ # @example
225
+ # tasks.group(:user_id)
226
+ #
227
+ # @return [Relation]
228
+ #
229
+ # @api public
230
+ def group(*args, &block)
231
+ __new__(dataset.__send__(__method__, *args, &block))
232
+ end
233
+
234
+ # Group by specific columns and count by group
235
+ #
236
+ # @example
237
+ # tasks.group_and_count(:user_id)
238
+ # # => [{ user_id: 1, count: 2 }, { user_id: 2, count: 3 }]
239
+ #
240
+ # @return [Relation]
241
+ #
242
+ # @api public
243
+ def group_and_count(*args, &block)
244
+ __new__(dataset.__send__(__method__, *args, &block))
245
+ end
246
+
247
+ # Select and group by specific columns
248
+ #
249
+ # @example
250
+ # tasks.select_group(:user_id)
251
+ # # => [{ user_id: 1 }, { user_id: 2 }]
252
+ #
253
+ # @return [Relation]
254
+ #
255
+ # @api public
256
+ def select_group(*args, &block)
257
+ __new__(dataset.__send__(__method__, *args, &block))
258
+ end
259
+
260
+ # Insert tuple into relation
261
+ #
262
+ # @example
263
+ # users.insert(name: 'Jane')
264
+ #
265
+ # @param [Hash] tuple
266
+ #
267
+ # @return [Relation]
268
+ #
269
+ # @api public
270
+ def insert(*args, &block)
271
+ dataset.insert(*args, &block)
272
+ end
273
+
274
+ # Update tuples in the relation
275
+ #
276
+ # @example
277
+ # users.update(name: 'Jane')
278
+ # users.where(name: 'Jane').update(name: 'Jane Doe')
279
+ #
280
+ # @return [Relation]
281
+ #
282
+ # @api public
283
+ def update(*args, &block)
284
+ dataset.update(*args, &block)
285
+ end
286
+
287
+ # Delete tuples from the relation
288
+ #
289
+ # @example
290
+ # users.delete # deletes all
291
+ # users.where(name: 'Jane').delete # delete tuples
292
+ # from restricted relation
293
+ #
294
+ # @return [Relation]
295
+ #
296
+ # @api public
297
+ def delete(*args, &block)
298
+ dataset.delete(*args, &block)
299
+ end
300
+
301
+ # Return if a restricted relation has 0 tuples
302
+ #
303
+ # @example
304
+ # users.unique?(email: 'jane@doe.org') # true
305
+ #
306
+ # users.insert(email: 'jane@doe.org')
307
+ #
308
+ # users.unique?(email: 'jane@doe.org') # false
309
+ #
310
+ # @param [Hash] criteria hash for the where clause
311
+ #
312
+ # @return [Relation]
313
+ #
314
+ # @api public
315
+ def unique?(criteria)
316
+ where(criteria).count.zero?
317
+ end
318
+ end
319
+ end
320
+ end