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