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.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/.rubocop.yml +55 -18
- data/.rubocop_todo.yml +15 -0
- data/.travis.yml +10 -5
- data/CHANGELOG.md +18 -0
- data/Gemfile +8 -1
- data/Guardfile +24 -0
- data/README.md +14 -22
- data/Rakefile +13 -5
- data/lib/rom/sql.rb +5 -5
- data/lib/rom/sql/commands.rb +7 -49
- data/lib/rom/sql/commands/create.rb +29 -0
- data/lib/rom/sql/commands/delete.rb +18 -0
- data/lib/rom/sql/commands/transaction.rb +17 -0
- data/lib/rom/sql/commands/update.rb +54 -0
- data/lib/rom/sql/commands_ext/postgres.rb +24 -0
- data/lib/rom/sql/header.rb +8 -9
- data/lib/rom/sql/migration.rb +26 -0
- data/lib/rom/sql/plugin/pagination.rb +93 -0
- data/lib/rom/sql/rake_task.rb +2 -0
- data/lib/rom/sql/relation.rb +320 -0
- data/lib/rom/sql/relation/associations.rb +104 -0
- data/lib/rom/sql/relation/class_methods.rb +47 -0
- data/lib/rom/sql/relation/inspection.rb +16 -0
- data/lib/rom/sql/repository.rb +59 -0
- data/lib/rom/sql/support/rails_log_subscriber.rb +1 -1
- data/lib/rom/sql/tasks/migration_tasks.rake +56 -0
- data/lib/rom/sql/version.rb +1 -1
- data/rom-sql.gemspec +2 -3
- data/spec/integration/commands/create_spec.rb +66 -8
- data/spec/integration/commands/delete_spec.rb +22 -3
- data/spec/integration/commands/update_spec.rb +57 -6
- data/spec/integration/read_spec.rb +42 -1
- data/spec/shared/database_setup.rb +10 -5
- data/spec/spec_helper.rb +17 -0
- data/spec/support/active_support_notifications_spec.rb +5 -4
- data/spec/support/rails_log_subscriber_spec.rb +2 -2
- data/spec/unit/logger_spec.rb +5 -3
- data/spec/unit/many_to_many_spec.rb +2 -2
- data/spec/unit/migration_spec.rb +34 -0
- data/spec/unit/migration_tasks_spec.rb +99 -0
- data/spec/unit/one_to_many_spec.rb +0 -2
- data/spec/unit/plugin/pagination_spec.rb +73 -0
- data/spec/unit/relation_spec.rb +49 -3
- data/spec/unit/repository_spec.rb +33 -0
- data/spec/unit/schema_spec.rb +5 -17
- metadata +32 -35
- data/lib/rom/sql/adapter.rb +0 -100
- data/lib/rom/sql/relation_inclusion.rb +0 -149
- data/lib/rom/sql/support/sequel_dataset_ext.rb +0 -33
- data/spec/unit/adapter_spec.rb +0 -48
- 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,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
|
@@ -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
|
data/lib/rom/sql/version.rb
CHANGED
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.
|
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.
|
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 '
|
5
|
+
include_context 'database setup'
|
5
6
|
|
6
7
|
subject(:users) { rom.commands.users }
|
7
8
|
|
8
9
|
before do
|
9
|
-
|
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(
|
77
|
+
result = users.try { users.create.call(name: 'Jane') }
|
24
78
|
|
25
|
-
expect(result.value).to eql(id:
|
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([{
|
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:
|
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(
|
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 {
|
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(
|
51
|
+
result = users.try { users.delete.by_name('Jane').call }
|
33
52
|
|
34
|
-
expect(result.value).to eql(
|
53
|
+
expect(result.value).to eql(id: 2, name: 'Jane')
|
35
54
|
end
|
36
55
|
end
|