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,104 @@
1
+ module ROM
2
+ module SQL
3
+ class Relation < ROM::Relation
4
+ module Associations
5
+ # Join configured association.
6
+ #
7
+ # Uses INNER JOIN type.
8
+ #
9
+ # @example
10
+ #
11
+ # setup.relation(:tasks)
12
+ #
13
+ # setup.relations(:users) do
14
+ # one_to_many :tasks, key: :user_id
15
+ #
16
+ # def with_tasks
17
+ # association_join(:tasks, select: [:title])
18
+ # end
19
+ # end
20
+ #
21
+ # @api public
22
+ def association_join(name, options = {})
23
+ graph_join(name, :inner, options)
24
+ end
25
+
26
+ # Join configured association
27
+ #
28
+ # Uses LEFT JOIN type.
29
+ #
30
+ # @example
31
+ #
32
+ # setup.relation(:tasks)
33
+ #
34
+ # setup.relations(:users) do
35
+ # one_to_many :tasks, key: :user_id
36
+ #
37
+ # def with_tasks
38
+ # association_left_join(:tasks, select: [:title])
39
+ # end
40
+ # end
41
+ #
42
+ # @api public
43
+ def association_left_join(name, options = {})
44
+ graph_join(name, :left_outer, options)
45
+ end
46
+
47
+ # @api private
48
+ def graph_join(name, join_type, options = {})
49
+ assoc = model.association_reflection(name)
50
+
51
+ key = assoc[:key]
52
+ type = assoc[:type]
53
+
54
+ if type == :many_to_many
55
+ select = options[:select] || {}
56
+ graph_join_many_to_many(name, assoc, select)
57
+ else
58
+ graph_join_other(name, key, type, join_type, options)
59
+ end
60
+ end
61
+
62
+ # @api private
63
+ def graph(*args)
64
+ __new__(dataset.__send__(__method__, *args))
65
+ end
66
+
67
+ private
68
+
69
+ def graph_join_many_to_many(name, assoc, select)
70
+ l_select, r_select =
71
+ if select.is_a?(Hash)
72
+ [select[assoc[:join_table]] || [], select[name]]
73
+ else
74
+ [[], select]
75
+ end
76
+
77
+ l_graph = graph(
78
+ assoc[:join_table],
79
+ { assoc[:left_key] => primary_key },
80
+ select: l_select, implicit_qualifier: self.name
81
+ )
82
+
83
+ l_graph.graph(
84
+ name, { primary_key => assoc[:right_key] }, select: r_select
85
+ )
86
+ end
87
+
88
+ def graph_join_other(name, key, type, join_type, options)
89
+ join_keys =
90
+ if type == :many_to_one
91
+ { primary_key => key }
92
+ else
93
+ { key => primary_key }
94
+ end
95
+
96
+ graph(
97
+ name, join_keys,
98
+ options.merge(join_type: join_type, implicit_qualifier: self.name)
99
+ )
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,47 @@
1
+ module ROM
2
+ module SQL
3
+ class Relation < ROM::Relation
4
+ module ClassMethods
5
+ def inherited(klass)
6
+ klass.class_eval do
7
+ class << self
8
+ attr_reader :model, :associations
9
+ end
10
+ end
11
+ klass.instance_variable_set('@model', Class.new(Sequel::Model))
12
+ klass.instance_variable_set('@associations', [])
13
+ super
14
+ end
15
+
16
+ def one_to_many(name, options)
17
+ associations << [__method__, name, options.merge(relation: name)]
18
+ end
19
+
20
+ def many_to_many(name, options = {})
21
+ associations << [__method__, name, options.merge(relation: name)]
22
+ end
23
+
24
+ def many_to_one(name, options = {})
25
+ new_options = options.merge(relation: Inflector.pluralize(name).to_sym)
26
+ associations << [__method__, name, new_options]
27
+ end
28
+
29
+ def finalize(relations, relation)
30
+ model.set_dataset(relation.dataset)
31
+ model.dataset.naked!
32
+
33
+ associations.each do |*args, options|
34
+ model = relation.model
35
+ other = relations[options.fetch(:relation)].model
36
+
37
+ model.public_send(*args, options.merge(class: other))
38
+ end
39
+
40
+ model.freeze
41
+
42
+ super
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,16 @@
1
+ module ROM
2
+ module SQL
3
+ class Relation < ROM::Relation
4
+ module Inspection
5
+ def model
6
+ self.class.model
7
+ end
8
+
9
+ # @api private
10
+ def primary_key
11
+ model.primary_key
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,59 @@
1
+ require 'logger'
2
+
3
+ require 'rom/repository'
4
+ require 'rom/sql/commands'
5
+
6
+ module ROM
7
+ module SQL
8
+ class Repository < ROM::Repository
9
+ attr_reader :logger, :schema
10
+
11
+ def self.database_file?(scheme)
12
+ scheme.to_s.include?('sqlite')
13
+ end
14
+
15
+ def initialize(uri, options = {})
16
+ @connection = ::Sequel.connect(uri.to_s, options)
17
+ @schema = connection.tables
18
+ end
19
+
20
+ def disconnect
21
+ connection.disconnect
22
+ end
23
+
24
+ def [](name)
25
+ connection[name]
26
+ end
27
+
28
+ def use_logger(logger)
29
+ @logger = logger
30
+ connection.loggers << logger
31
+ end
32
+
33
+ def dataset(table)
34
+ connection[table]
35
+ end
36
+
37
+ def dataset?(name)
38
+ schema.include?(name)
39
+ end
40
+
41
+ def extend_command_class(klass, dataset)
42
+ type = dataset.db.database_type
43
+
44
+ if type == :postgres
45
+ ext =
46
+ if klass < Commands::Create
47
+ Commands::Postgres::Create
48
+ elsif klass < Commands::Update
49
+ Commands::Postgres::Update
50
+ end
51
+
52
+ klass.send(:include, ext) if ext
53
+ end
54
+
55
+ klass
56
+ end
57
+ end
58
+ end
59
+ end
@@ -19,7 +19,7 @@ module ROM
19
19
  name = color(name, :magenta, true)
20
20
  end
21
21
 
22
- debug " #{name} #{sql} #{binds}"
22
+ debug " #{name} #{sql} #{binds}"
23
23
  end
24
24
 
25
25
  def odd?
@@ -0,0 +1,56 @@
1
+ require "pathname"
2
+ require "fileutils"
3
+
4
+ namespace :db do
5
+ desc "Perform migration reset (full erase and migration up)"
6
+ task reset: :load_setup do
7
+ ROM::SQL::Migration.run(target: 0)
8
+ ROM::SQL::Migration.run
9
+ puts "<= db:reset executed"
10
+ end
11
+
12
+ desc "Migrate the database (options [version_number])]"
13
+ task :migrate, [:version] => :load_setup do |_, args|
14
+ version = args[:version]
15
+ if version.nil?
16
+ ROM::SQL::Migration.run
17
+ puts "<= db:migrate executed"
18
+ else
19
+ ROM::SQL::Migration.run(target: version.to_i)
20
+ puts "<= db:migrate version=[#{version}] executed"
21
+ end
22
+ end
23
+
24
+ desc "Perform migration down (erase all data)"
25
+ task clean: :load_setup do
26
+ ROM::SQL::Migration.run(target: 0)
27
+ puts "<= db:clean executed"
28
+ end
29
+
30
+ desc "Create a migration (parameters: NAME, VERSION)"
31
+ task :create_migration, [:name, :version] => :load_setup do |_, args|
32
+ name, version = args[:name], args[:version]
33
+
34
+ if name.nil?
35
+ puts "No NAME specified. Example usage:
36
+ `rake db:create_migration[create_users]`"
37
+ exit
38
+ end
39
+
40
+ version ||= Time.now.utc.strftime("%Y%m%d%H%M%S")
41
+
42
+ filename = "#{version}_#{name}.rb"
43
+ dirname = ROM::SQL::Migration.path
44
+ path = File.join(dirname, filename)
45
+
46
+ FileUtils.mkdir_p(dirname)
47
+ File.write path, <<-MIGRATION
48
+ ROM::SQL::Migration.create do
49
+ change do
50
+ end
51
+ end
52
+ MIGRATION
53
+
54
+ puts path
55
+ end
56
+ end
@@ -1,5 +1,5 @@
1
1
  module ROM
2
2
  module SQL
3
- VERSION = "0.3.2".freeze
3
+ VERSION = "0.4.0.beta1".freeze
4
4
  end
5
5
  end
data/rom-sql.gemspec CHANGED
@@ -18,11 +18,10 @@ 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.17"
21
+ spec.add_runtime_dependency "sequel", "~> 4.18"
22
22
  spec.add_runtime_dependency "equalizer", "~> 0.0", ">= 0.0.9"
23
- spec.add_runtime_dependency "rom", "~> 0.5", ">= 0.5.0"
23
+ spec.add_runtime_dependency "rom", "~> 0.6.0.beta1"
24
24
 
25
25
  spec.add_development_dependency "bundler"
26
26
  spec.add_development_dependency "rake", "~> 10.0"
27
- spec.add_development_dependency "rubocop", "~> 0.28.0"
28
27
  end
@@ -1,15 +1,25 @@
1
1
  require 'spec_helper'
2
+ require 'virtus'
2
3
 
3
4
  describe 'Commands / Create' do
4
- include_context 'users and tasks'
5
+ include_context 'database setup'
5
6
 
6
7
  subject(:users) { rom.commands.users }
7
8
 
8
9
  before do
9
- setup.relation(:users)
10
+ class Params
11
+ include Virtus.model
12
+
13
+ attribute :name
14
+
15
+ def self.[](input)
16
+ new(input)
17
+ end
18
+ end
10
19
 
11
20
  setup.commands(:users) do
12
21
  define(:create) do
22
+ input Params
13
23
  result :one
14
24
  end
15
25
 
@@ -17,33 +27,81 @@ describe 'Commands / Create' do
17
27
  result :many
18
28
  end
19
29
  end
30
+
31
+ setup.relation(:users)
32
+ end
33
+
34
+ context '#transaction' do
35
+ it 'create record if nothing was raised' do
36
+ result = users.create.transaction {
37
+ users.create.call(name: 'Jane')
38
+ }
39
+
40
+ expect(result.value).to eq(id: 1, name: 'Jane')
41
+ end
42
+
43
+ it 'allows for nested transactions' do
44
+ result = users.create.transaction {
45
+ users.create.transaction {
46
+ users.create.call(name: 'Jane')
47
+ }
48
+ }
49
+
50
+ expect(result.value).to eq(id: 1, name: 'Jane')
51
+ end
52
+
53
+ it 'create nothing if anything was raised' do
54
+ result = users.create.transaction(rollback: :always) {
55
+ users.create.call(name: 'Jane')
56
+ }
57
+
58
+ expect(result.value).to be_nil
59
+ end
60
+
61
+ it 'create nothing if anything was raised in any nested transaction' do
62
+ expect {
63
+ expect {
64
+ users.create.transaction {
65
+ users.create.call(name: 'John')
66
+ users.create.transaction {
67
+ users.create.call(name: 'Jane')
68
+ raise Exception
69
+ }
70
+ }
71
+ }.to raise_error(Exception)
72
+ }.to_not change { rom.relations.users.count }
73
+ end
20
74
  end
21
75
 
22
76
  it 'returns a single tuple when result is set to :one' do
23
- result = users.try { create(id: 2, name: 'Jane') }
77
+ result = users.try { users.create.call(name: 'Jane') }
24
78
 
25
- expect(result.value).to eql(id: 2, name: 'Jane')
79
+ expect(result.value).to eql(id: 1, name: 'Jane')
26
80
  end
27
81
 
28
82
  it 'returns tuples when result is set to :many' do
29
83
  result = users.try do
30
- create_many([{ id: 2, name: 'Jane' }, { id: 3, name: 'Jack' }])
84
+ users.create_many.call([{ name: 'Jane' }, { name: 'Jack' }])
31
85
  end
32
86
 
33
87
  expect(result.value.to_a).to match_array([
34
- { id: 2, name: 'Jane' }, { id: 3, name: 'Jack' }
88
+ { id: 1, name: 'Jane' }, { id: 2, name: 'Jack' }
35
89
  ])
36
90
  end
37
91
 
38
92
  it 'handles not-null constraint violation error' do
39
- result = users.try { create(id: nil, name: 'Jane') }
93
+ result = users.try { users.create.call(name: nil) }
40
94
 
41
95
  expect(result.error).to be_instance_of(ROM::SQL::ConstraintError)
42
96
  expect(result.error.message).to match(/not-null/)
43
97
  end
44
98
 
45
99
  it 'handles uniqueness constraint violation error' do
46
- result = users.try { create(id: 3, name: 'Piotr') }
100
+ result = users.try {
101
+ users.create.call(name: 'Jane')
102
+ } >-> user {
103
+ users.try { users.create.call(name: user[:name]) }
104
+ }
47
105
 
48
106
  expect(result.error).to be_instance_of(ROM::SQL::ConstraintError)
49
107
  expect(result.error.message).to match(/unique/)
@@ -21,16 +21,35 @@ describe 'Commands / Delete' do
21
21
  rom.relations.users.insert(id: 2, name: 'Jane')
22
22
  end
23
23
 
24
+ context '#transaction' do
25
+ it 'delete in normal way if no error raised' do
26
+ expect {
27
+ users.delete.transaction do
28
+ users.delete.by_name('Jane').call
29
+ end
30
+ }.to change { rom.relations.users.count }.by(-1)
31
+ end
32
+
33
+ it 'delete nothing if error was raised' do
34
+ expect {
35
+ users.delete.transaction do
36
+ users.delete.by_name('Jane').call
37
+ raise ROM::SQL::Rollback
38
+ end
39
+ }.to_not change { rom.relations.users.count }
40
+ end
41
+ end
42
+
24
43
  it 'raises error when tuple count does not match expectation' do
25
- result = users.try { delete }
44
+ result = users.try { users.delete.call }
26
45
 
27
46
  expect(result.value).to be(nil)
28
47
  expect(result.error).to be_instance_of(ROM::TupleCountMismatchError)
29
48
  end
30
49
 
31
50
  it 'deletes all tuples in a restricted relation' do
32
- result = users.try { delete(:by_name, 'Jane') }
51
+ result = users.try { users.delete.by_name('Jane').call }
33
52
 
34
- expect(result.value).to eql({ id: 2, name: 'Jane' })
53
+ expect(result.value).to eql(id: 2, name: 'Jane')
35
54
  end
36
55
  end