couchbase-orm 1.1.1 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (87) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/test.yml +45 -0
  3. data/.gitignore +2 -0
  4. data/.travis.yml +3 -2
  5. data/CODEOWNERS +1 -0
  6. data/Gemfile +5 -3
  7. data/README.md +237 -31
  8. data/ci/run_couchbase.sh +22 -0
  9. data/couchbase-orm.gemspec +26 -20
  10. data/lib/couchbase-orm/active_record_compat.rb +92 -0
  11. data/lib/couchbase-orm/associations.rb +119 -0
  12. data/lib/couchbase-orm/base.rb +143 -166
  13. data/lib/couchbase-orm/changeable.rb +512 -0
  14. data/lib/couchbase-orm/connection.rb +28 -8
  15. data/lib/couchbase-orm/encrypt.rb +48 -0
  16. data/lib/couchbase-orm/error.rb +17 -2
  17. data/lib/couchbase-orm/inspectable.rb +37 -0
  18. data/lib/couchbase-orm/json_schema/json_validation_error.rb +13 -0
  19. data/lib/couchbase-orm/json_schema/loader.rb +47 -0
  20. data/lib/couchbase-orm/json_schema/validation.rb +18 -0
  21. data/lib/couchbase-orm/json_schema/validator.rb +45 -0
  22. data/lib/couchbase-orm/json_schema.rb +9 -0
  23. data/lib/couchbase-orm/json_transcoder.rb +27 -0
  24. data/lib/couchbase-orm/locale/en.yml +5 -0
  25. data/lib/couchbase-orm/n1ql.rb +133 -0
  26. data/lib/couchbase-orm/persistence.rb +61 -52
  27. data/lib/couchbase-orm/proxies/bucket_proxy.rb +36 -0
  28. data/lib/couchbase-orm/proxies/collection_proxy.rb +52 -0
  29. data/lib/couchbase-orm/proxies/n1ql_proxy.rb +40 -0
  30. data/lib/couchbase-orm/proxies/results_proxy.rb +23 -0
  31. data/lib/couchbase-orm/railtie.rb +6 -17
  32. data/lib/couchbase-orm/relation.rb +249 -0
  33. data/lib/couchbase-orm/strict_loading.rb +21 -0
  34. data/lib/couchbase-orm/timestamps/created.rb +20 -0
  35. data/lib/couchbase-orm/timestamps/updated.rb +21 -0
  36. data/lib/couchbase-orm/timestamps.rb +15 -0
  37. data/lib/couchbase-orm/types/array.rb +32 -0
  38. data/lib/couchbase-orm/types/date.rb +9 -0
  39. data/lib/couchbase-orm/types/date_time.rb +14 -0
  40. data/lib/couchbase-orm/types/encrypted.rb +17 -0
  41. data/lib/couchbase-orm/types/nested.rb +43 -0
  42. data/lib/couchbase-orm/types/timestamp.rb +18 -0
  43. data/lib/couchbase-orm/types.rb +20 -0
  44. data/lib/couchbase-orm/utilities/enum.rb +13 -1
  45. data/lib/couchbase-orm/utilities/has_many.rb +72 -36
  46. data/lib/couchbase-orm/utilities/ignored_properties.rb +15 -0
  47. data/lib/couchbase-orm/utilities/index.rb +18 -20
  48. data/lib/couchbase-orm/utilities/properties_always_exists_in_document.rb +16 -0
  49. data/lib/couchbase-orm/utilities/query_helper.rb +148 -0
  50. data/lib/couchbase-orm/utils.rb +25 -0
  51. data/lib/couchbase-orm/version.rb +1 -1
  52. data/lib/couchbase-orm/views.rb +38 -41
  53. data/lib/couchbase-orm.rb +44 -9
  54. data/lib/ext/query_n1ql.rb +124 -0
  55. data/lib/rails/generators/couchbase_orm/config/templates/couchbase.yml +3 -2
  56. data/spec/associations_spec.rb +219 -50
  57. data/spec/base_spec.rb +296 -14
  58. data/spec/collection_proxy_spec.rb +29 -0
  59. data/spec/connection_spec.rb +27 -0
  60. data/spec/couchbase-orm/active_record_compat_spec.rb +24 -0
  61. data/spec/couchbase-orm/changeable_spec.rb +16 -0
  62. data/spec/couchbase-orm/json_schema/validation_spec.rb +23 -0
  63. data/spec/couchbase-orm/json_schema/validator_spec.rb +13 -0
  64. data/spec/couchbase-orm/timestamps_spec.rb +85 -0
  65. data/spec/couchbase-orm/timestamps_spec_models.rb +36 -0
  66. data/spec/empty-json-schema/.gitkeep +0 -0
  67. data/spec/enum_spec.rb +34 -0
  68. data/spec/has_many_spec.rb +101 -54
  69. data/spec/index_spec.rb +13 -9
  70. data/spec/json-schema/JsonSchemaBaseTest.json +19 -0
  71. data/spec/json-schema/entity_snakecase.json +20 -0
  72. data/spec/json-schema/loader_spec.rb +42 -0
  73. data/spec/json-schema/specific_path.json +20 -0
  74. data/spec/json_schema_spec.rb +178 -0
  75. data/spec/n1ql_spec.rb +193 -0
  76. data/spec/persistence_spec.rb +49 -9
  77. data/spec/relation_nested_spec.rb +88 -0
  78. data/spec/relation_spec.rb +430 -0
  79. data/spec/support.rb +16 -8
  80. data/spec/type_array_spec.rb +52 -0
  81. data/spec/type_encrypted_spec.rb +114 -0
  82. data/spec/type_nested_spec.rb +191 -0
  83. data/spec/type_spec.rb +317 -0
  84. data/spec/utilities/ignored_properties_spec.rb +20 -0
  85. data/spec/utilities/properties_always_exists_in_document_spec.rb +24 -0
  86. data/spec/views_spec.rb +32 -11
  87. metadata +192 -29
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true, encoding: ASCII-8BIT
2
+
3
+ module MTLibcouchbase
4
+ class QueryN1QL
5
+ N1P_QUERY_STATEMENT = 1
6
+ N1P_CONSISTENCY_REQUEST = 2
7
+
8
+ def initialize(connection, reactor, n1ql, **_opts)
9
+ @connection = connection
10
+ @reactor = reactor
11
+
12
+ @n1ql = n1ql
13
+ @request_handle = FFI::MemoryPointer.new :pointer, 1
14
+ end
15
+
16
+ attr_reader :connection, :n1ql
17
+
18
+ def get_count(metadata)
19
+ metadata[:metrics][:resultCount]
20
+ end
21
+
22
+ def perform(limit: nil, **_options, &blk)
23
+ raise 'not connected' unless @connection.handle
24
+ raise 'query already in progress' if @query_text
25
+ raise 'callback required' unless block_given?
26
+
27
+ # customise the size based on the request being made
28
+ orig_limit = @n1ql.limit
29
+ begin
30
+ if orig_limit && limit
31
+ @n1ql.limit = limit if orig_limit > limit
32
+ end
33
+ @query_text = @n1ql.to_s
34
+ rescue StandardError
35
+ @query_text = nil
36
+ raise
37
+ ensure
38
+ @n1ql.limit = orig_limit
39
+ end
40
+
41
+ @reactor.schedule do
42
+ @error = nil
43
+ @callback = blk
44
+
45
+ @cmd = Ext::CMDN1QL.new
46
+ @params = Ext.n1p_new
47
+ err = Ext.n1p_setconsistency(@params, N1P_CONSISTENCY_REQUEST)
48
+ if err == :success
49
+ err = Ext.n1p_setquery(@params, @query_text, @query_text.bytesize, N1P_QUERY_STATEMENT)
50
+ if err == :success
51
+
52
+ err = Ext.n1p_mkcmd(@params, @cmd)
53
+ if err == :success
54
+ pointer = @cmd.to_ptr
55
+ @connection.requests[pointer.address] = self
56
+
57
+ @cmd[:callback] = @connection.get_callback(:n1ql_callback)
58
+ @cmd[:handle] = @request_handle
59
+
60
+ err = Ext.n1ql_query(@connection.handle, pointer, @cmd)
61
+ if err != :success
62
+ error(Error.lookup(err).new('full text search not scheduled'))
63
+ end
64
+ else
65
+ error(Error.lookup(err).new('failed to build full text search command'))
66
+ end
67
+ else
68
+ error(Error.lookup(err).new('failed to build full text search query structure'))
69
+ end
70
+ else
71
+ error(Error.lookup(err).new('failed set consistency value'))
72
+ end
73
+ end
74
+ end
75
+
76
+ # Row is JSON value representing the result
77
+ def received(row)
78
+ return if @error
79
+
80
+ @callback.call(false, row)
81
+ rescue StandardError => e
82
+ @error = e
83
+ cancel
84
+ end
85
+
86
+ # Example metadata
87
+ # {:requestID=>"36162fce-ef39-4821-bf03-449e4073185d", :signature=>{:*=>"*"}, :results=>[], :status=>"success",
88
+ # :metrics=>{:elapsedTime=>"15.298243ms", :executionTime=>"15.256975ms", :resultCount=>12, :resultSize=>8964}}
89
+ def received_final(metadata)
90
+ @query_text = nil
91
+
92
+ @connection.requests.delete(@cmd.to_ptr.address)
93
+ @cmd = nil
94
+
95
+ Ext.n1p_free(@params)
96
+ @params = nil
97
+
98
+ if @error
99
+ if @error == :cancelled
100
+ @callback.call(:final, metadata)
101
+ else
102
+ @callback.call(:error, @error)
103
+ end
104
+ else
105
+ @callback.call(:final, metadata)
106
+ end
107
+ end
108
+
109
+ def error(obj)
110
+ @error = obj
111
+ received_final(nil)
112
+ end
113
+
114
+ def cancel
115
+ @error ||= :cancelled
116
+ @reactor.schedule do
117
+ if @connection.handle && @cmd
118
+ Ext.n1ql_cancel(@connection.handle, @handle_ptr.get_pointer(0))
119
+ received_final(nil)
120
+ end
121
+ end
122
+ end
123
+ end
124
+ end
@@ -1,5 +1,6 @@
1
1
  common: &common
2
- hosts: localhost
2
+ connection_string: couchbase://localhost
3
+ bucket: <%= bucket_name || app_name %>
3
4
  username: <%= username || bucket_name || app_name %>
4
5
  password: <%= password %>
5
6
 
@@ -13,7 +14,7 @@ test:
13
14
 
14
15
  # set these environment variables on your production server
15
16
  production:
16
- hosts: <%%= ENV['COUCHBASE_HOST'] || ENV['COUCHBASE_HOSTS'] %>
17
+ connection_string: <%%= ENV['COUCHBASE_CONNECTION_STRING'] %>
17
18
  bucket: <%%= ENV['COUCHBASE_BUCKET'] %>
18
19
  username: <%%= ENV['COUCHBASE_USER'] %>
19
20
  password: <%%= ENV['COUCHBASE_PASSWORD'] %>
@@ -5,6 +5,13 @@ require File.expand_path("../support", __FILE__)
5
5
 
6
6
  class Parent < CouchbaseOrm::Base
7
7
  attribute :name
8
+ has_and_belongs_to_many :children
9
+ end
10
+
11
+ class StrictLoadingParent < CouchbaseOrm::Base
12
+ attribute :name
13
+ has_and_belongs_to_many :children
14
+ self.strict_loading_by_default = true
8
15
  end
9
16
 
10
17
  class RandomOtherType < CouchbaseOrm::Base
@@ -17,77 +24,239 @@ class Child < CouchbaseOrm::Base
17
24
  belongs_to :parent, dependent: :destroy
18
25
  end
19
26
 
27
+ class Assembly < CouchbaseOrm::Base
28
+ attribute :name
29
+
30
+ has_and_belongs_to_many :parts, autosave: true
31
+ end
32
+
33
+ class Part < CouchbaseOrm::Base
34
+ attribute :name
35
+
36
+ has_and_belongs_to_many :assemblies, dependent: :destroy, autosave: true
37
+ end
38
+
20
39
 
21
40
  describe CouchbaseOrm::Associations do
22
- it "should work with dependent associations" do
23
- parent = Parent.create!(name: 'joe')
24
- child = Child.create!(name: 'bob', parent_id: parent.id)
41
+ describe 'belongs_to' do
42
+ it "should work with dependent associations" do
43
+ parent = Parent.create!(name: 'joe')
44
+ child = Child.create!(name: 'bob', parent_id: parent.id)
25
45
 
26
- expect(parent.persisted?).to be(true)
27
- expect(child.persisted?).to be(true)
28
- id = parent.id
46
+ expect(parent.persisted?).to be(true)
47
+ expect(child.persisted?).to be(true)
48
+ id = parent.id
49
+
50
+ child.destroy
51
+ expect(child.destroyed?).to be(true)
52
+ expect(parent.destroyed?).to be(false)
29
53
 
30
- child.destroy
31
- expect(child.destroyed?).to be(true)
32
- expect(parent.destroyed?).to be(false)
54
+ # Ensure that parent has been destroyed
55
+ expect { Parent.find(id) }.to raise_error(Couchbase::Error::DocumentNotFound)
56
+
57
+ expect(Parent.find_by_id(id)).to be(nil)
33
58
 
34
- # Ensure that parent has been destroyed
35
- expect { Parent.find(id) }.to raise_error(Libcouchbase::Error::KeyNotFound)
36
- expect(Parent.find_by_id(id)).to be(nil)
59
+ expect { parent.reload }.to raise_error(Couchbase::Error::DocumentNotFound)
37
60
 
38
- expect { parent.reload }.to raise_error(Libcouchbase::Error::KeyNotFound)
61
+ # Save will always return true unless the model is changed (won't touch the database)
62
+ parent.name = 'should fail'
63
+ expect { parent.save }.to raise_error(Couchbase::Error::DocumentNotFound)
64
+ expect { parent.save! }.to raise_error(Couchbase::Error::DocumentNotFound)
65
+ end
39
66
 
40
- # Save will always return true unless the model is changed (won't touch the database)
41
- parent.name = 'should fail'
42
- expect { parent.save }.to raise_error(Libcouchbase::Error::KeyNotFound)
43
- expect { parent.save! }.to raise_error(Libcouchbase::Error::KeyNotFound)
44
- end
67
+ it "should cache associations" do
68
+ parent = Parent.create!(name: 'joe')
69
+ child = Child.create!(name: 'bob', parent_id: parent.id)
45
70
 
46
- it "should cache associations" do
47
- parent = Parent.create!(name: 'joe')
48
- child = Child.create!(name: 'bob', parent_id: parent.id)
71
+ id = child.parent.__id__
72
+ expect(parent.__id__).not_to eq(child.parent.__id__)
73
+ expect(parent).to eq(child.parent)
74
+ expect(child.parent.__id__).to eq(id)
49
75
 
50
- id = child.parent.__id__
51
- expect(parent.__id__).not_to eq(child.parent.__id__)
52
- expect(parent).to eq(child.parent)
53
- expect(child.parent.__id__).to eq(id)
76
+ child.reload
77
+ expect(parent).to eq(child.parent)
78
+ expect(child.parent.__id__).not_to eq(id)
54
79
 
55
- child.reload
56
- expect(parent).to eq(child.parent)
57
- expect(child.parent.__id__).not_to eq(id)
80
+ child.destroy
81
+ end
58
82
 
59
- child.destroy
60
- end
83
+ it "should ignore associations when delete is used" do
84
+ parent = Parent.create!(name: 'joe')
85
+ child = Child.create!(name: 'bob', parent_id: parent.id)
61
86
 
62
- it "should ignore associations when delete is used" do
63
- parent = Parent.create!(name: 'joe')
64
- child = Child.create!(name: 'bob', parent_id: parent.id)
87
+ id = child.id
88
+ child.delete
65
89
 
66
- id = child.id
67
- child.delete
90
+ expect(Child.exists?(id)).to be(false) # this is flaky
91
+ expect(Parent.exists?(parent.id)).to be(true)
68
92
 
69
- expect(Child.exists?(id)).to be(false)
70
- expect(Parent.exists?(parent.id)).to be(true)
93
+ id = parent.id
94
+ parent.delete
95
+ expect(Parent.exists?(id)).to be(false)
96
+ end
97
+
98
+ it "should raise an error if an invalid type is being assigned" do
99
+ begin
100
+ parent = RandomOtherType.create!(name: 'joe')
101
+ expect { Child.create!(name: 'bob', parent: parent) }.to raise_error(ArgumentError)
102
+ ensure
103
+ parent.delete
104
+ end
105
+ end
71
106
 
72
- id = parent.id
73
- parent.delete
74
- expect(Parent.exists?(id)).to be(false)
107
+ describe Parent do
108
+ it_behaves_like "ActiveModel"
109
+ end
110
+
111
+ describe Child do
112
+ it_behaves_like "ActiveModel"
113
+ end
75
114
  end
76
115
 
77
- it "should raise an error if an invalid type is being assigned" do
78
- begin
79
- parent = RandomOtherType.create!(name: 'joe')
80
- expect { Child.create!(name: 'bob', parent: parent) }.to raise_error(ArgumentError)
81
- ensure
82
- parent.delete
116
+ describe 'has_and_belongs_to_many' do
117
+ it "should work with dependent associations" do
118
+ assembly = Assembly.create!(name: 'a1')
119
+ part = Part.create!(name: 'p1', assemblies: [assembly])
120
+ assembly.reload
121
+
122
+ expect(assembly.persisted?).to be(true)
123
+ expect(part.persisted?).to be(true)
124
+
125
+ part.destroy
126
+ expect(part.destroyed?).to be(true)
127
+ expect(assembly.destroyed?).to be(true)
128
+ end
129
+
130
+ it "should cache associations" do
131
+ assembly = Assembly.create!(name: 'a1')
132
+ part = Part.create!(name: 'p1', assembly_ids: [assembly.id])
133
+ assembly.reload
134
+
135
+ id = part.assemblies.first.__id__
136
+ expect(assembly.__id__).not_to eq(part.assemblies.first.__id__)
137
+ expect(assembly).to eq(part.assemblies.first)
138
+ expect(part.assemblies.first.__id__).to eq(id)
139
+
140
+ part.reload
141
+ expect(assembly).to eq(part.assemblies.first)
142
+ expect(part.assemblies.first.__id__).not_to eq(id)
143
+
144
+ part.destroy
145
+ end
146
+
147
+ it "should ignore associations when delete is used" do
148
+ assembly = Assembly.create!(name: 'a1')
149
+ part = Part.create!(name: 'p1', assembly_ids: [assembly.id])
150
+ assembly.reload
151
+
152
+ id = part.id
153
+ part.delete
154
+
155
+ expect(Part.exists?(id)).to be(false)
156
+ expect(Assembly.exists?(assembly.id)).to be(true)
157
+
158
+ id = assembly.id
159
+ assembly.delete
160
+ expect(Assembly.exists?(id)).to be(false)
161
+ end
162
+
163
+ it "should raise an error if an invalid type is being assigned" do
164
+ begin
165
+ assembly = RandomOtherType.create!(name: 'a1')
166
+ expect { Part.create!(name: 'p1', assemblies: [assembly]) }.to raise_error(ArgumentError)
167
+ ensure
168
+ assembly.delete
169
+ end
170
+ end
171
+
172
+ it "should add association with single" do
173
+ assembly = Assembly.create!(name: 'a1')
174
+ part = Part.create!(name: 'p1', assemblies: [assembly])
175
+
176
+ expect(assembly.reload.parts.map(&:id)).to match_array([part.id])
177
+ end
178
+
179
+ it 'should add association with multiple' do
180
+ assembly = Assembly.create!(name: 'a1')
181
+ part1 = Part.create!(name: 'p1', assemblies: [assembly])
182
+ part2 = Part.create!(name: 'p2', assemblies: [assembly])
183
+
184
+ expect(assembly.reload.parts.map(&:id)).to match_array([part1.id, part2.id])
185
+ end
186
+
187
+ it "should remove association with single" do
188
+ assembly1 = Assembly.create!(name: 'a1')
189
+ assembly2 = Assembly.create!(name: 'a2')
190
+ part = Part.create!(name: 'p1', assemblies: [assembly1])
191
+ part.assemblies = [assembly2]
192
+ part.save!
193
+
194
+ expect(assembly1.reload.parts.map(&:id)).to be_empty
195
+ expect(assembly2.reload.parts.map(&:id)).to match_array([part.id])
196
+ end
197
+
198
+ it 'should remove association with multiple' do
199
+ assembly1 = Assembly.create!(name: 'a1')
200
+ assembly2 = Assembly.create!(name: 'a2')
201
+ part1 = Part.create!(name: 'p1', assemblies: [assembly1])
202
+ part2 = Part.create!(name: 'p2', assemblies: [assembly2])
203
+
204
+ part1.assemblies = part1.assemblies + [assembly2]
205
+ part1.save!
206
+
207
+ expect(assembly1.reload.parts.map(&:id)).to match_array([part1.id])
208
+ expect(assembly2.reload.parts.map(&:id)).to match_array([part1.id, part2.id])
209
+ end
210
+
211
+ describe Assembly do
212
+ it_behaves_like "ActiveModel"
213
+ end
214
+
215
+ describe Part do
216
+ it_behaves_like "ActiveModel"
83
217
  end
84
218
  end
85
219
 
86
- describe Parent do
87
- it_behaves_like "ActiveModel"
220
+ describe 'strict_loading' do
221
+ let(:parent) {Parent.create!(name: 'joe')}
222
+ let(:child) {Child.create!(name: 'bob', parent_id: parent.id)}
223
+ context 'instance strict loading' do
224
+ it 'raises StrictLoadingViolationError on lazy loading child relation' do
225
+ expect {child.parent.id}.not_to raise_error
226
+ expect_strict_loading_error_on_calling_parent(Child.find(child.id).tap{|child| child.strict_loading!})
227
+ end
228
+ end
229
+ context 'scope strict loading' do
230
+ it 'raises StrictLoadingViolationError on lazy loading child relation' do
231
+ expect_strict_loading_error_on_calling_parent(Child.where(id: child.id).strict_loading.first)
232
+ expect_strict_loading_error_on_calling_parent(Child.strict_loading.where(id: child.id).first)
233
+ expect_strict_loading_error_on_calling_parent(Child.strict_loading.where(id: child.id).last)
234
+ expect_strict_loading_error_on_calling_parent(Child.strict_loading.where(id: child.id).to_a.first)
235
+ expect_strict_loading_error_on_calling_parent(Child.strict_loading.all.to_a.first)
236
+ end
237
+
238
+ it 'does not raise StrictLoadingViolationError on lazy loading child relation without declaring it' do
239
+ expect_strict_loading_error_on_calling_parent(Child.strict_loading.where(id: child.id).first)
240
+ expect { Child.where(id: child.id).last.parent}.not_to raise_error
241
+ end
242
+
243
+ it 'raises StrictLoadingViolationError on lazy loading habtm relation' do
244
+ expect {Parent.strict_loading.where(id: parent.id).first.children}.to raise_error(CouchbaseOrm::StrictLoadingViolationError)
245
+ # NB any action called on model class breaks find return type (find return an enumerator instead of a record)
246
+ expect {Parent.strict_loading.find(parent.id).first.children}.to raise_error(CouchbaseOrm::StrictLoadingViolationError)
247
+ end
248
+
249
+ it 'raises StrictLoadingViolationError on lazy loading relation when model is by default strict_loading' do
250
+ strict_loading_parent = StrictLoadingParent.create!(name: 'joe')
251
+ expect {StrictLoadingParent.where(id: strict_loading_parent.id).first.children}.to raise_error(CouchbaseOrm::StrictLoadingViolationError)
252
+ expect {Parent.find(parent.id).children}.not_to raise_error
253
+ # NB any action called on model class breaks find return type (find return an enumerator instead of a record)
254
+ expect {Parent.strict_loading.find(strict_loading_parent.id).first.children}.to raise_error(CouchbaseOrm::StrictLoadingViolationError)
255
+ end
256
+ end
88
257
  end
89
258
 
90
- describe Child do
91
- it_behaves_like "ActiveModel"
259
+ def expect_strict_loading_error_on_calling_parent(child_instance)
260
+ expect {child_instance.parent}.to raise_error(CouchbaseOrm::StrictLoadingViolationError)
92
261
  end
93
262
  end