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.
@@ -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 [Sequel::Database] connection a connection instance
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
@@ -2,41 +2,92 @@ require 'rom/sql/migration/migrator'
2
2
 
3
3
  module ROM
4
4
  module SQL
5
- # Trap for the migration runner. To create a migration
6
- # on a specific gateway, use ROM::SQL::Gateway#migration
7
- #
8
- # @example
9
- # rom = ROM.container(
10
- # default: [:sql, 'sqlite::memory'],
11
- # other: [:sql, 'postgres://localhost/test']
12
- # )
13
- #
14
- # # default gateway migrations
15
- # ROM::SQL.migration do
16
- # change do
17
- # create_table(:users) do
18
- # primary_key :id
19
- # String :name
20
- # end
21
- # end
22
- # end
23
- #
24
- # # other gateway migrations
25
- # rom.gateways[:other].migration do
26
- # change do
27
- # create_table(:users) do
28
- # primary_key :id
29
- # String :name
30
- # end
31
- # end
32
- # end
33
- #
34
- # @api public
35
- def self.migration(&block)
36
- ROM::SQL::Gateway.instance.migration(&block)
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
- migrator.pending?
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
- migrator.run(options)
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
- join_tuples = assoc.associate(__registry__, tuples, parent)
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 = __registry__[assoc.target]
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(relation.__registry__, tuple, parent)
159
+ assoc.associate(__registry__, tuple, parent)
164
160
  }
165
161
  end
166
162
 
@@ -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
- define_method(:by_pk) do |pk|
94
- if primary_key.nil?
95
- raise MissingPrimaryKeyError.new("Missing primary key for "\
96
- ":#{ schema.name }")
97
- else
98
- where(schema[primary_key] => pk)
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
- end
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 WHERE clause
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
- where(criteria).count.zero?
794
+ !exist?(criteria)
776
795
  end
777
796
 
778
797
  # Return a new relation from a raw SQL string
@@ -105,7 +105,7 @@ module ROM
105
105
  end
106
106
 
107
107
  # @api private
108
- def finalize!(*)
108
+ def finalize!(*args)
109
109
  super do
110
110
  initialize_primary_key_names
111
111
  end
@@ -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, **_kw)
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
- ROM::SQL::Gateway.instance
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
@@ -1,5 +1,5 @@
1
1
  module ROM
2
2
  module SQL
3
- VERSION = '1.2.2'.freeze
3
+ VERSION = '1.3.0'.freeze
4
4
  end
5
5
  end
@@ -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.9', '>= 0.9.4'
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