rom-sql 0.3.2 → 0.4.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
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