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,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