couchbase-orm 1.1.1 → 2.0.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.
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