rom-sql 1.2.2 → 1.3.0
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/.travis.yml +9 -5
- data/CHANGELOG.md +30 -0
- data/lib/rom/plugins/relation/sql/auto_wrap.rb +2 -2
- data/lib/rom/sql/association/many_to_many.rb +12 -0
- data/lib/rom/sql/attribute.rb +99 -10
- data/lib/rom/sql/extensions/postgres/commands.rb +5 -2
- data/lib/rom/sql/extensions/postgres/types.rb +160 -0
- data/lib/rom/sql/gateway.rb +1 -9
- data/lib/rom/sql/migration.rb +91 -34
- data/lib/rom/sql/plugin/associates.rb +4 -8
- data/lib/rom/sql/relation.rb +10 -8
- data/lib/rom/sql/relation/reading.rb +21 -2
- data/lib/rom/sql/schema.rb +1 -1
- data/lib/rom/sql/schema/inferrer.rb +3 -1
- data/lib/rom/sql/tasks/migration_tasks.rake +17 -1
- data/lib/rom/sql/version.rb +1 -1
- data/rom-sql.gemspec +2 -2
- data/spec/extensions/postgres/attribute_spec.rb +128 -0
- data/spec/extensions/postgres/integration_spec.rb +21 -0
- data/spec/integration/commands/update_spec.rb +1 -1
- data/spec/integration/migration_spec.rb +41 -10
- data/spec/integration/plugins/auto_wrap_spec.rb +52 -5
- data/spec/integration/schema/inferrer_spec.rb +36 -5
- data/spec/shared/users.rb +1 -1
- data/spec/shared/users_and_tasks.rb +1 -1
- data/spec/spec_helper.rb +10 -4
- data/spec/unit/attribute_spec.rb +84 -4
- data/spec/unit/migration_tasks_spec.rb +12 -1
- data/spec/unit/order_dsl_spec.rb +8 -0
- data/spec/unit/plugin/associates_spec.rb +99 -0
- data/spec/unit/relation/by_pk_spec.rb +8 -0
- data/spec/unit/relation/exist_predicate_spec.rb +25 -0
- metadata +19 -7
data/lib/rom/sql/gateway.rb
CHANGED
@@ -17,12 +17,6 @@ module ROM
|
|
17
17
|
include Dry::Core::Constants
|
18
18
|
include Migration
|
19
19
|
|
20
|
-
class << self
|
21
|
-
# FIXME: get rid of this and figure out a nicer way of handling migration DSL
|
22
|
-
# we want to have global access ONLY when running migration tasks
|
23
|
-
attr_accessor :instance
|
24
|
-
end
|
25
|
-
|
26
20
|
adapter :sql
|
27
21
|
|
28
22
|
CONNECTION_EXTENSIONS = {
|
@@ -48,7 +42,7 @@ module ROM
|
|
48
42
|
# @example
|
49
43
|
# ROM.container(:sql, 'postgres://localhost/db_name')
|
50
44
|
#
|
51
|
-
# @param [
|
45
|
+
# @param [String,Symbol] uri connection URI
|
52
46
|
#
|
53
47
|
# @overload initialize(uri, options)
|
54
48
|
# Connects to a database via URI and options
|
@@ -96,8 +90,6 @@ module ROM
|
|
96
90
|
@options = options
|
97
91
|
|
98
92
|
super
|
99
|
-
|
100
|
-
self.class.instance = self
|
101
93
|
end
|
102
94
|
|
103
95
|
# Disconnect from the gateway's database
|
data/lib/rom/sql/migration.rb
CHANGED
@@ -2,41 +2,92 @@ require 'rom/sql/migration/migrator'
|
|
2
2
|
|
3
3
|
module ROM
|
4
4
|
module SQL
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
5
|
+
class << self
|
6
|
+
# Trap for the migration runner. By default migrations are
|
7
|
+
# bound to the gateway you're using to run them.
|
8
|
+
# You also can explicitly pass a configuration object and a gateway name
|
9
|
+
# but this normally won't be not required.
|
10
|
+
#
|
11
|
+
# @example
|
12
|
+
# # Ordinary migration
|
13
|
+
# ROM::SQL.migration do
|
14
|
+
# change do
|
15
|
+
# create_table(:users) do
|
16
|
+
# primary_key :id
|
17
|
+
# String :name
|
18
|
+
# end
|
19
|
+
# end
|
20
|
+
# end
|
21
|
+
#
|
22
|
+
# @example
|
23
|
+
# # Providing a config
|
24
|
+
# rom = ROM::Configuration.new(
|
25
|
+
# default: [:sql, 'sqlite::memory'],
|
26
|
+
# other: [:sql, 'postgres://localhost/test']
|
27
|
+
# )
|
28
|
+
#
|
29
|
+
# # default gateway migrations
|
30
|
+
# ROM::SQL.migration(rom) do
|
31
|
+
# change do
|
32
|
+
# create_table(:users) do
|
33
|
+
# primary_key :id
|
34
|
+
# String :name
|
35
|
+
# end
|
36
|
+
# end
|
37
|
+
# end
|
38
|
+
#
|
39
|
+
# # other gateway migrations
|
40
|
+
# ROM::SQL.migration(rom, :other) do
|
41
|
+
# change do
|
42
|
+
# create_table(:users) do
|
43
|
+
# primary_key :id
|
44
|
+
# String :name
|
45
|
+
# end
|
46
|
+
# end
|
47
|
+
# end
|
48
|
+
#
|
49
|
+
# @param [ROM::Container] container The container instance used for accessing gateways
|
50
|
+
# @param [Symbol] gateway The gateway name, :default by default
|
51
|
+
#
|
52
|
+
# @api public
|
53
|
+
def migration(*args, &block)
|
54
|
+
if args.any?
|
55
|
+
container, gateway, * = args
|
56
|
+
with_gateway(container.gateways[gateway || :default]) { migration(&block) }
|
57
|
+
else
|
58
|
+
current_gateway.migration(&block)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# @api private
|
63
|
+
attr_accessor :current_gateway
|
64
|
+
|
65
|
+
# This method is used on loading migrations.
|
66
|
+
# Temporally sets the global "current_gateway", you shouln't access it.
|
67
|
+
#
|
68
|
+
# @api private
|
69
|
+
def with_gateway(gateway)
|
70
|
+
current = @current_gateway
|
71
|
+
@current_gateway = gateway
|
72
|
+
|
73
|
+
yield
|
74
|
+
ensure
|
75
|
+
@current_gateway = current
|
76
|
+
end
|
37
77
|
end
|
38
78
|
|
79
|
+
@current_gateway = nil
|
80
|
+
|
39
81
|
module Migration
|
82
|
+
# FIXME: remove in 2.0
|
83
|
+
#
|
84
|
+
# @api private
|
85
|
+
def self.included(base)
|
86
|
+
super
|
87
|
+
|
88
|
+
base.singleton_class.send(:attr_accessor, :instance)
|
89
|
+
end
|
90
|
+
|
40
91
|
Sequel.extension :migration
|
41
92
|
|
42
93
|
# @!attribute [r] migrator
|
@@ -46,6 +97,8 @@ module ROM
|
|
46
97
|
# @api private
|
47
98
|
def initialize(uri, options = EMPTY_HASH)
|
48
99
|
@migrator = options.fetch(:migrator) { Migrator.new(connection) }
|
100
|
+
|
101
|
+
self.class.instance ||= self
|
49
102
|
end
|
50
103
|
|
51
104
|
# Check if there are any pending migrations
|
@@ -54,7 +107,9 @@ module ROM
|
|
54
107
|
#
|
55
108
|
# @api public
|
56
109
|
def pending_migrations?
|
57
|
-
|
110
|
+
ROM::SQL.with_gateway(self) {
|
111
|
+
migrator.pending?
|
112
|
+
}
|
58
113
|
end
|
59
114
|
|
60
115
|
# Migration DSL
|
@@ -76,7 +131,9 @@ module ROM
|
|
76
131
|
#
|
77
132
|
# @api public
|
78
133
|
def run_migrations(options = {})
|
79
|
-
|
134
|
+
ROM::SQL.with_gateway(self) {
|
135
|
+
migrator.run(options)
|
136
|
+
}
|
80
137
|
end
|
81
138
|
end
|
82
139
|
end
|
@@ -142,25 +142,21 @@ module ROM
|
|
142
142
|
when Association::ManyToMany
|
143
143
|
result_type = tuples.is_a?(Array) ? :many : :one
|
144
144
|
|
145
|
-
|
146
|
-
join_relation = assoc.join_relation(__registry__)
|
147
|
-
join_relation.multi_insert(join_tuples)
|
145
|
+
assoc.persist(__registry__, tuples, parent)
|
148
146
|
|
149
|
-
pk, fk =
|
150
|
-
.associations[assoc.source]
|
151
|
-
.combine_keys(__registry__).to_a.flatten
|
147
|
+
pk, fk = assoc.parent_combine_keys(__registry__)
|
152
148
|
|
153
149
|
case parent
|
154
150
|
when Array
|
155
151
|
parent.map do |p|
|
156
|
-
tuples.map { |tuple| tuple.merge(fk => p[pk]) }
|
152
|
+
tuples.map { |tuple| Hash(tuple).merge(fk => p[pk]) }
|
157
153
|
end.flatten(1)
|
158
154
|
else
|
159
155
|
tuples.map { |tuple| Hash(tuple).update(fk => parent[pk]) }
|
160
156
|
end
|
161
157
|
when Association
|
162
158
|
with_input_tuples(tuples).map { |tuple|
|
163
|
-
assoc.associate(
|
159
|
+
assoc.associate(__registry__, tuple, parent)
|
164
160
|
}
|
165
161
|
end
|
166
162
|
|
data/lib/rom/sql/relation.rb
CHANGED
@@ -78,7 +78,7 @@ module ROM
|
|
78
78
|
# @api public
|
79
79
|
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
80
80
|
def by_pk(#{schema.primary_key.map(&:name).join(', ')})
|
81
|
-
where(#{schema.primary_key.map { |attr| "schema[:#{attr.name}] => #{attr.name}" }.join(', ')})
|
81
|
+
where(#{schema.primary_key.map { |attr| "self.class.schema[:#{attr.name}] => #{attr.name}" }.join(', ')})
|
82
82
|
end
|
83
83
|
RUBY
|
84
84
|
else
|
@@ -90,14 +90,16 @@ module ROM
|
|
90
90
|
# @return [SQL::Relation]
|
91
91
|
#
|
92
92
|
# @api public
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
93
|
+
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
94
|
+
def by_pk(pk)
|
95
|
+
if primary_key.nil?
|
96
|
+
raise MissingPrimaryKeyError.new(
|
97
|
+
"Missing primary key for :\#{schema.name}"
|
98
|
+
)
|
99
|
+
end
|
100
|
+
where(self.class.schema[self.class.schema.primary_key_name].qualified => pk)
|
99
101
|
end
|
100
|
-
|
102
|
+
RUBY
|
101
103
|
end
|
102
104
|
end
|
103
105
|
|
@@ -757,6 +757,25 @@ module ROM
|
|
757
757
|
new(dataset.__send__(__method__, relation.dataset, options, &block))
|
758
758
|
end
|
759
759
|
|
760
|
+
# Checks whether a relation has at least one tuple
|
761
|
+
#
|
762
|
+
# @example
|
763
|
+
# users.where(name: 'John').exist? # => true
|
764
|
+
#
|
765
|
+
# users.exist?(name: 'Klaus') # => false
|
766
|
+
#
|
767
|
+
# users.exist? { name.is('klaus') } # => false
|
768
|
+
#
|
769
|
+
# @param [Array<Object>] args Optional restrictions to filter the relation
|
770
|
+
# @yield An optional block filters the relation using `where DSL`
|
771
|
+
#
|
772
|
+
# @return [TrueClass, FalseClass]
|
773
|
+
#
|
774
|
+
# @api public
|
775
|
+
def exist?(*args, &block)
|
776
|
+
!where(*args, &block).limit(1).count.zero?
|
777
|
+
end
|
778
|
+
|
760
779
|
# Return if a restricted relation has 0 tuples
|
761
780
|
#
|
762
781
|
# @example
|
@@ -766,13 +785,13 @@ module ROM
|
|
766
785
|
#
|
767
786
|
# users.unique?(email: 'jane@doe.org') # false
|
768
787
|
#
|
769
|
-
# @param [Hash] criteria The condition hash for
|
788
|
+
# @param [Hash] criteria The condition hash for WHERE clause
|
770
789
|
#
|
771
790
|
# @return [TrueClass, FalseClass]
|
772
791
|
#
|
773
792
|
# @api public
|
774
793
|
def unique?(criteria)
|
775
|
-
|
794
|
+
!exist?(criteria)
|
776
795
|
end
|
777
796
|
|
778
797
|
# Return a new relation from a raw SQL string
|
data/lib/rom/sql/schema.rb
CHANGED
@@ -104,11 +104,13 @@ module ROM
|
|
104
104
|
self.class.numeric_pk_type.meta(primary_key: true)
|
105
105
|
end
|
106
106
|
|
107
|
-
def map_type(ruby_type, db_type, **
|
107
|
+
def map_type(ruby_type, db_type, **kw)
|
108
108
|
type = self.class.ruby_type_mapping[ruby_type]
|
109
109
|
|
110
110
|
if db_type.is_a?(String) && db_type.include?('numeric') || db_type.include?('decimal')
|
111
111
|
map_decimal_type(db_type)
|
112
|
+
elsif db_type.is_a?(String) && db_type.include?('char') && kw[:max_length]
|
113
|
+
type.meta(limit: kw[:max_length])
|
112
114
|
else
|
113
115
|
type
|
114
116
|
end
|
@@ -4,6 +4,8 @@ require "fileutils"
|
|
4
4
|
module ROM
|
5
5
|
module SQL
|
6
6
|
module RakeSupport
|
7
|
+
MissingEnv = Class.new(StandardError)
|
8
|
+
|
7
9
|
class << self
|
8
10
|
def run_migrations(options = {})
|
9
11
|
gateway.run_migrations(options)
|
@@ -13,12 +15,26 @@ module ROM
|
|
13
15
|
gateway.migrator.create_file(*args)
|
14
16
|
end
|
15
17
|
|
18
|
+
# Global environment used for running migrations. You normally
|
19
|
+
# set in the `db:setup` task with `ROM::SQL::RakeSupport.env = ROM.container(...)`
|
20
|
+
# or something similar.
|
21
|
+
#
|
22
|
+
# @api public
|
23
|
+
attr_accessor :env
|
24
|
+
|
16
25
|
private
|
17
26
|
|
18
27
|
def gateway
|
19
|
-
|
28
|
+
if env.nil?
|
29
|
+
Gateway.instance ||
|
30
|
+
raise(MissingEnv, "Set up a configutation with ROM::SQL::RakeSupport.env= in the db:setup task")
|
31
|
+
else
|
32
|
+
env.gateways[:default]
|
33
|
+
end
|
20
34
|
end
|
21
35
|
end
|
36
|
+
|
37
|
+
@env = nil
|
22
38
|
end
|
23
39
|
end
|
24
40
|
end
|
data/lib/rom/sql/version.rb
CHANGED
data/rom-sql.gemspec
CHANGED
@@ -19,9 +19,9 @@ Gem::Specification.new do |spec|
|
|
19
19
|
|
20
20
|
spec.add_runtime_dependency 'sequel', '~> 4.43'
|
21
21
|
spec.add_runtime_dependency 'dry-equalizer', '~> 0.2'
|
22
|
-
spec.add_runtime_dependency 'dry-types', '~> 0.
|
22
|
+
spec.add_runtime_dependency 'dry-types', '~> 0.10', '>= 0.10.2'
|
23
23
|
spec.add_runtime_dependency 'dry-core', '~> 0.2', '>= 0.2.3'
|
24
|
-
spec.add_runtime_dependency 'rom', '~> 3.2'
|
24
|
+
spec.add_runtime_dependency 'rom', '~> 3.2', '>= 3.2.1'
|
25
25
|
|
26
26
|
spec.add_development_dependency 'bundler'
|
27
27
|
spec.add_development_dependency 'rake', '~> 10.0'
|
@@ -0,0 +1,128 @@
|
|
1
|
+
RSpec.describe 'ROM::SQL::Attribute', :postgres do
|
2
|
+
include_context 'database setup'
|
3
|
+
|
4
|
+
jsonb_hash = -> v { Sequel::Postgres::JSONBHash.new(v) }
|
5
|
+
jsonb_array = -> v { Sequel::Postgres::JSONBArray.new(v) }
|
6
|
+
|
7
|
+
before do
|
8
|
+
conn.drop_table?(:pg_people)
|
9
|
+
conn.drop_table?(:people)
|
10
|
+
|
11
|
+
conf.relation(:people) do
|
12
|
+
schema(:pg_people, infer: true)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
let(:people) { relations[:people] }
|
17
|
+
let(:create_person) { commands[:people].create }
|
18
|
+
|
19
|
+
describe 'using arrays' do
|
20
|
+
before do
|
21
|
+
conn.create_table :pg_people do
|
22
|
+
primary_key :id
|
23
|
+
String :name
|
24
|
+
column :fields, :jsonb
|
25
|
+
end
|
26
|
+
|
27
|
+
conf.commands(:people) do
|
28
|
+
define(:create)
|
29
|
+
define(:update)
|
30
|
+
end
|
31
|
+
|
32
|
+
create_person.(name: 'John Doe', fields: [{ name: 'age', value: '30' },
|
33
|
+
{ name: 'height', value: 180 }])
|
34
|
+
create_person.(name: 'Jade Doe', fields: [{ name: 'age', value: '25' }])
|
35
|
+
end
|
36
|
+
|
37
|
+
it 'allows to query jsonb by inclusion' do
|
38
|
+
expect(people.select(:name).where { fields.contain([value: '30']) }.one).
|
39
|
+
to eql(name: 'John Doe')
|
40
|
+
end
|
41
|
+
|
42
|
+
it 'cat project result of contains' do
|
43
|
+
expect(people.select { fields.contain([value: '30']).as(:contains) }.to_a).
|
44
|
+
to eql([{ contains: true }, { contains: false }])
|
45
|
+
end
|
46
|
+
|
47
|
+
it 'fetches data from jsonb array by index' do
|
48
|
+
expect(people.select { [fields.get(1).as(:field)] }.where(name: 'John Doe').one).
|
49
|
+
to eql(field: jsonb_hash['name' => 'height', 'value' => 180])
|
50
|
+
end
|
51
|
+
|
52
|
+
it 'fetches data from jsonb array' do
|
53
|
+
expect(people.select { fields.get(1).get_text('value').as(:height) }.where(name: 'John Doe').one).
|
54
|
+
to eql(height: '180')
|
55
|
+
end
|
56
|
+
|
57
|
+
it 'fetches data with path' do
|
58
|
+
expect(people.select(people[:fields].get_text('1', 'value').as(:height)).to_a).
|
59
|
+
to eql([{ height: '180' }, { height: nil }])
|
60
|
+
end
|
61
|
+
|
62
|
+
it 'deletes key from result' do
|
63
|
+
expect(people.select { fields.delete(0).as(:result) }.limit(1).one).
|
64
|
+
to eq(result: jsonb_array[['name' => 'height', 'value' => 180]])
|
65
|
+
end
|
66
|
+
|
67
|
+
it 'deletes by path' do
|
68
|
+
expect(people.select { fields.delete('0', 'name').delete('1', 'name').as(:result) }.limit(1).one).
|
69
|
+
to eq(result: jsonb_array[[{ 'value' => '30' }, { 'value' => 180 }]])
|
70
|
+
end
|
71
|
+
|
72
|
+
it 'concatenates JSON values' do
|
73
|
+
expect(people.select { (fields + [name: 'height', value: 165]).as(:result) }.by_pk(2).one).
|
74
|
+
to eq(result: jsonb_array[[{ 'name' => 'age', 'value' => '25' },
|
75
|
+
{ 'name' => 'height', 'value' => 165 }]])
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
describe 'using map' do
|
80
|
+
before do
|
81
|
+
conn.create_table :pg_people do
|
82
|
+
primary_key :id
|
83
|
+
String :name
|
84
|
+
column :data, :jsonb
|
85
|
+
end
|
86
|
+
|
87
|
+
conf.commands(:people) do
|
88
|
+
define(:create)
|
89
|
+
define(:update)
|
90
|
+
end
|
91
|
+
|
92
|
+
create_person.(name: 'John Doe', data: { age: 30, height: 180 })
|
93
|
+
create_person.(name: 'Jade Doe', data: { age: 25 })
|
94
|
+
end
|
95
|
+
|
96
|
+
it 'queries data by inclusion' do
|
97
|
+
expect(people.select(:name).where { data.contain(age: 30) }.one).
|
98
|
+
to eql(name: 'John Doe')
|
99
|
+
end
|
100
|
+
|
101
|
+
it 'queries data by left inclusion' do
|
102
|
+
expect(people.select(:name).where { data.contained_by(age: 25, foo: 'bar') }.one).
|
103
|
+
to eql(name: 'Jade Doe')
|
104
|
+
end
|
105
|
+
|
106
|
+
it 'checks for key presence' do
|
107
|
+
expect(people.select { data.has_key('height').as(:there) }.to_a).
|
108
|
+
to eql([{ there: true }, { there: false }])
|
109
|
+
|
110
|
+
expect(people.select(:name).where { data.has_any_key('height', 'width') }.one).
|
111
|
+
to eql(name: 'John Doe')
|
112
|
+
|
113
|
+
expect(people.select(:name).where { data.has_all_keys('height', 'age') }.one).
|
114
|
+
to eql(name: 'John Doe')
|
115
|
+
end
|
116
|
+
|
117
|
+
it 'concatenates JSON values' do
|
118
|
+
expect(people.select { data.merge(height: 165).as(:result) }.by_pk(2).one).
|
119
|
+
to eql(result: jsonb_hash['age' => 25, 'height' => 165])
|
120
|
+
end
|
121
|
+
|
122
|
+
it 'deletes key from result' do
|
123
|
+
expect(people.select { data.delete('height').as(:result) }.to_a).
|
124
|
+
to eql([{ result: jsonb_hash['age' => 30] },
|
125
|
+
{ result: jsonb_hash['age' => 25] }])
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|