rasti-db 1.3.0 → 2.0.1

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.
@@ -2,6 +2,7 @@ require 'coverage_helper'
2
2
  require 'rasti-db'
3
3
  require 'minitest/autorun'
4
4
  require 'minitest/colorin'
5
+ require 'minitest/line/describe_track'
5
6
  require 'pry-nav'
6
7
  require 'logger'
7
8
  require 'sequel/extensions/pg_hstore'
@@ -13,10 +14,11 @@ Rasti::DB.configure do |config|
13
14
  end
14
15
 
15
16
  User = Rasti::DB::Model[:id, :name, :posts, :comments, :person]
16
- Post = Rasti::DB::Model[:id, :title, :body, :user_id, :user, :comments, :categories]
17
+ Post = Rasti::DB::Model[:id, :title, :body, :user_id, :user, :comments, :categories, :language_id, :language]
17
18
  Comment = Rasti::DB::Model[:id, :text, :user_id, :user, :post_id, :post]
18
19
  Category = Rasti::DB::Model[:id, :name, :posts]
19
- Person = Rasti::DB::Model[:document_number, :first_name, :last_name, :birth_date, :user_id, :user]
20
+ Person = Rasti::DB::Model[:document_number, :first_name, :last_name, :birth_date, :user_id, :user, :languages]
21
+ Language = Rasti::DB::Model[:id, :name, :people]
20
22
 
21
23
 
22
24
  class Users < Rasti::DB::Collection
@@ -27,6 +29,7 @@ end
27
29
 
28
30
  class Posts < Rasti::DB::Collection
29
31
  many_to_one :user
32
+ many_to_one :language
30
33
  many_to_many :categories
31
34
  one_to_many :comments
32
35
 
@@ -38,9 +41,9 @@ class Posts < Rasti::DB::Collection
38
41
 
39
42
  query :commented_by do |user_id|
40
43
  chainable do
41
- dataset.join(with_schema(:comments), post_id: :id)
42
- .where(with_schema(:comments, :user_id) => user_id)
43
- .select_all(with_schema(:posts))
44
+ dataset.join(qualify(:comments), post_id: :id)
45
+ .where(Sequel[:comments][:user_id] => user_id)
46
+ .select_all(:posts)
44
47
  .distinct
45
48
  end
46
49
  end
@@ -52,8 +55,8 @@ class Comments < Rasti::DB::Collection
52
55
 
53
56
  def posts_commented_by(user_id)
54
57
  dataset.where(Sequel[:comments][:user_id] => user_id)
55
- .join(with_schema(:posts), id: :post_id)
56
- .select_all(with_schema(:posts))
58
+ .join(qualify(:posts, data_source_name: :default), id: :post_id)
59
+ .select_all(:posts)
57
60
  .map { |row| Post.new row }
58
61
  end
59
62
  end
@@ -69,24 +72,39 @@ class People < Rasti::DB::Collection
69
72
  set_model Person
70
73
 
71
74
  many_to_one :user
75
+ many_to_many :languages
76
+ end
77
+
78
+ class Languages < Rasti::DB::Collection
79
+ set_data_source_name :custom
80
+
81
+ many_to_many :people, collection: People, relation_data_source_name: :default
82
+ one_to_many :posts
72
83
  end
73
84
 
74
85
 
75
86
  class Minitest::Spec
76
87
 
77
- let(:users) { Users.new db }
88
+ let(:users) { Users.new environment }
78
89
 
79
- let(:posts) { Posts.new db }
90
+ let(:posts) { Posts.new environment }
80
91
 
81
- let(:comments) { Comments.new db }
92
+ let(:comments) { Comments.new environment }
82
93
 
83
- let(:categories) { Categories.new db }
94
+ let(:categories) { Categories.new environment }
84
95
 
85
- let(:people) { People.new db }
96
+ let(:people) { People.new environment }
86
97
 
87
- let :db do
88
- driver = (RUBY_ENGINE == 'jruby') ? 'jdbc:sqlite::memory:' : {adapter: :sqlite}
98
+ let(:languages) { Languages.new environment }
89
99
 
100
+ let(:driver) { (RUBY_ENGINE == 'jruby') ? 'jdbc:sqlite::memory:' : {adapter: :sqlite} }
101
+
102
+ let :environment do
103
+ Rasti::DB::Environment.new default: Rasti::DB::DataSource.new(db),
104
+ custom: Rasti::DB::DataSource.new(custom_db)
105
+ end
106
+
107
+ let :db do
90
108
  Sequel.connect(driver).tap do |db|
91
109
 
92
110
  db.create_table :users do
@@ -98,6 +116,7 @@ class Minitest::Spec
98
116
  primary_key :id
99
117
  String :title, null: false, unique: true
100
118
  String :body, null: false
119
+ Integer :language_id, null: false, index: true
101
120
  foreign_key :user_id, :users, null: false, index: true
102
121
  end
103
122
 
@@ -127,6 +146,23 @@ class Minitest::Spec
127
146
  foreign_key :user_id, :users, null: false, unique: true
128
147
  end
129
148
 
149
+ db.create_table :languages_people do
150
+ Integer :language_id, null: false, index: true
151
+ foreign_key :document_number, :people, type: String, null: false, index: true
152
+ primary_key [:language_id, :document_number]
153
+ end
154
+
155
+ end
156
+ end
157
+
158
+ let :custom_db do
159
+ Sequel.connect(driver).tap do |db|
160
+
161
+ db.create_table :languages do
162
+ primary_key :id
163
+ String :name, null: false, unique: true
164
+ end
165
+
130
166
  end
131
167
  end
132
168
 
@@ -10,7 +10,7 @@ describe 'Model' do
10
10
  end
11
11
 
12
12
  it 'Invalid definition' do
13
- error = proc { model = Rasti::DB::Model[:id, :name, :name] }.must_raise ArgumentError
13
+ error = proc { Rasti::DB::Model[:id, :name, :name] }.must_raise ArgumentError
14
14
  error.message.must_equal 'Attribute name already exists'
15
15
  end
16
16
 
@@ -79,4 +79,12 @@ describe 'Model' do
79
79
  refute_equal User.new(id: 1, name: 'User 1').hash, User.new(id: 2, name: 'User 2').hash
80
80
  end
81
81
 
82
+ it 'Merge' do
83
+ user = User.new(id: 1, name: 'User 1')
84
+ changed_user = user.merge(name: 'User 2')
85
+
86
+ user.must_equal User.new(id: 1, name: 'User 1')
87
+ changed_user.must_equal User.new(id: 1, name: 'User 2')
88
+ end
89
+
82
90
  end
@@ -3,18 +3,44 @@ require 'minitest_helper'
3
3
  describe 'Query' do
4
4
 
5
5
  before do
6
- 1.upto(10) { |i| db[:users].insert name: "User #{i}" }
6
+ custom_db[:languages].insert name: 'Spanish'
7
7
 
8
- db[:posts].insert user_id: 2, title: 'Sample post', body: '...'
9
- db[:posts].insert user_id: 1, title: 'Another post', body: '...'
10
- db[:posts].insert user_id: 4, title: 'Best post', body: '...'
11
- end
8
+ 1.upto(10) do |i|
9
+ db[:users].insert name: "User #{i}"
10
+
11
+ db[:people].insert user_id: i,
12
+ document_number: "document_#{i}",
13
+ first_name: "Name #{i}",
14
+ last_name: "Last Name #{i}",
15
+ birth_date: Date.parse('2020-04-24')
16
+
17
+ db[:languages_people].insert language_id: 1, document_number: "document_#{i}"
18
+ end
12
19
 
13
- let(:users_query) { Rasti::DB::Query.new Users, db[:users] }
20
+ db[:posts].insert user_id: 2, title: 'Sample post', body: '...', language_id: 1
21
+ db[:posts].insert user_id: 1, title: 'Another post', body: '...', language_id: 1
22
+ db[:posts].insert user_id: 4, title: 'Best post', body: '...', language_id: 1
14
23
 
15
- let(:posts_query) { Rasti::DB::Query.new Posts, db[:posts] }
24
+ 1.upto(3) { |i| db[:categories].insert name: "Category #{i}" }
25
+
26
+ db[:comments].insert post_id: 1, user_id: 5, text: 'Comment 1'
27
+ db[:comments].insert post_id: 1, user_id: 7, text: 'Comment 2'
28
+ db[:comments].insert post_id: 2, user_id: 2, text: 'Comment 3'
29
+
30
+ db[:categories_posts].insert post_id: 1, category_id: 1
31
+ db[:categories_posts].insert post_id: 1, category_id: 2
32
+ db[:categories_posts].insert post_id: 2, category_id: 2
33
+ db[:categories_posts].insert post_id: 2, category_id: 3
34
+ db[:categories_posts].insert post_id: 3, category_id: 3
35
+ end
36
+
37
+ let(:users_query) { Rasti::DB::Query.new collection_class: Users, dataset: db[:users], environment: environment }
38
+
39
+ let(:posts_query) { Rasti::DB::Query.new collection_class: Posts, dataset: db[:posts], environment: environment }
16
40
 
17
- let(:comments_query) { Rasti::DB::Query.new Comments, db[:comments] }
41
+ let(:comments_query) { Rasti::DB::Query.new collection_class: Comments, dataset: db[:comments], environment: environment }
42
+
43
+ let(:languages_query) { Rasti::DB::Query.new collection_class: Languages, dataset: custom_db[:languages], environment: environment }
18
44
 
19
45
  it 'Count' do
20
46
  users_query.count.must_equal 10
@@ -37,6 +63,83 @@ describe 'Query' do
37
63
  users_query.primary_keys.must_equal db[:users].map { |u| u[:id] }
38
64
  end
39
65
 
66
+ it 'Select attributes' do
67
+ posts_query.select_attributes(:id, :user_id).all.must_equal db[:posts].select(:id, :user_id).map { |r| Post.new r }
68
+ end
69
+
70
+ it 'Exclude attributes' do
71
+ posts_query.exclude_attributes(:body).all.must_equal db[:posts].select(:id, :user_id, :title, :language_id).map { |r| Post.new r }
72
+ end
73
+
74
+ it 'All attributes' do
75
+ posts_query.exclude_attributes(:body).all_attributes.all.must_equal db[:posts].map { |r| Post.new r }
76
+ end
77
+
78
+ it 'Select graph attributes' do
79
+ language = Language.new custom_db[:languages].where(id: 1).select(:id).first
80
+
81
+ person = Person.new db[:people].where(document_number: 'document_2').select(:document_number, :user_id).first.merge(languages: [language])
82
+
83
+ user = User.new db[:users].where(id: 2).select(:id).first.merge(person: person)
84
+
85
+ categories = db[:categories].where(id: [1,2]).select(:id).map { |c| Category.new c }
86
+
87
+ post = Post.new db[:posts].where(id: 1).first.merge(user: user, categories: categories)
88
+
89
+ selected_attributes = {
90
+ user: [:id],
91
+ 'user.person' => [:document_number, :user_id],
92
+ 'user.person.languages' => [:id],
93
+ categories: [:id]
94
+ }
95
+
96
+ posts_query.where(id: 1)
97
+ .graph(*selected_attributes.keys)
98
+ .select_graph_attributes(selected_attributes)
99
+ .all
100
+ .must_equal [post]
101
+ end
102
+
103
+ it 'Exclude graph attributes' do
104
+ language = Language.new custom_db[:languages].where(id: 1).select(:id).first
105
+
106
+ person = Person.new db[:people].where(document_number: 'document_2').select(:document_number, :user_id).first.merge(languages: [language])
107
+
108
+ user = User.new db[:users].where(id: 2).select(:id).first.merge(person: person)
109
+
110
+ categories = db[:categories].where(id: [1,2]).select(:id).map { |c| Category.new c }
111
+
112
+ post = Post.new db[:posts].where(id: 1).first.merge(user: user, categories: categories)
113
+
114
+ excluded_attributes = {
115
+ user: [:name],
116
+ 'user.person' => [:first_name, :last_name, :birth_date],
117
+ 'user.person.languages' => [:name],
118
+ categories: [:name]
119
+ }
120
+
121
+ posts_query.where(id: 1)
122
+ .graph(*excluded_attributes.keys)
123
+ .exclude_graph_attributes(excluded_attributes)
124
+ .all
125
+ .must_equal [post]
126
+ end
127
+
128
+ it 'All graph attributes' do
129
+ person = Person.new db[:people].where(document_number: 'document_2').first
130
+
131
+ user = User.new db[:users].where(id: 2).select(:id).first.merge(person: person)
132
+
133
+ post = Post.new db[:posts].where(id: 1).first.merge(user: user)
134
+
135
+ posts_query.where(id: 1)
136
+ .graph('user.person')
137
+ .exclude_graph_attributes(user: [:name], 'user.person' => [:birth_date, :first_name, :last_name])
138
+ .all_graph_attributes('user.person')
139
+ .all
140
+ .must_equal [post]
141
+ end
142
+
40
143
  it 'Map' do
41
144
  users_query.map(&:name).must_equal db[:users].map(:name)
42
145
  end
@@ -66,17 +169,17 @@ describe 'Query' do
66
169
 
67
170
  it 'Order' do
68
171
  posts_query.order(:title).all.must_equal [
69
- Post.new(id: 2, user_id: 1, title: 'Another post', body: '...'),
70
- Post.new(id: 3, user_id: 4, title: 'Best post', body: '...'),
71
- Post.new(id: 1, user_id: 2, title: 'Sample post', body: '...')
172
+ Post.new(id: 2, user_id: 1, title: 'Another post', body: '...', language_id: 1),
173
+ Post.new(id: 3, user_id: 4, title: 'Best post', body: '...', language_id: 1),
174
+ Post.new(id: 1, user_id: 2, title: 'Sample post', body: '...', language_id: 1)
72
175
  ]
73
176
  end
74
177
 
75
178
  it 'Reverse order' do
76
179
  posts_query.reverse_order(:title).all.must_equal [
77
- Post.new(id: 1, user_id: 2, title: 'Sample post', body: '...'),
78
- Post.new(id: 3, user_id: 4, title: 'Best post', body: '...'),
79
- Post.new(id: 2, user_id: 1, title: 'Another post', body: '...')
180
+ Post.new(id: 1, user_id: 2, title: 'Sample post', body: '...', language_id: 1),
181
+ Post.new(id: 3, user_id: 4, title: 'Best post', body: '...', language_id: 1),
182
+ Post.new(id: 2, user_id: 1, title: 'Another post', body: '...', language_id: 1)
80
183
  ]
81
184
  end
82
185
 
@@ -93,21 +196,48 @@ describe 'Query' do
93
196
  end
94
197
 
95
198
  it 'Graph' do
96
- users_query.graph(:posts).where(id: 1).first.must_equal User.new(id: 1, name: 'User 1', posts: [Post.new(id: 2, user_id: 1, title: 'Another post', body: '...')])
199
+ users_query.graph(:posts).where(id: 1).first.must_equal User.new(id: 1, name: 'User 1', posts: [Post.new(id: 2, user_id: 1, title: 'Another post', body: '...', language_id: 1)])
97
200
  end
98
201
 
99
- it 'Empty?' do
202
+ it 'Graph with multiple data sources' do
203
+ language = Language.new id: 1, name: 'Spanish'
204
+
205
+ person = Person.new user_id: 2,
206
+ document_number: 'document_2',
207
+ first_name: 'Name 2',
208
+ last_name: 'Last Name 2',
209
+ birth_date: Date.parse('2020-04-24'),
210
+ languages: [language]
211
+
212
+ user = User.new id: 2,
213
+ name: 'User 2',
214
+ person: person
215
+
216
+ post = Post.new id: 1,
217
+ user_id: 2,
218
+ user: user,
219
+ title: 'Sample post',
220
+ body: '...',
221
+ language_id: 1,
222
+ language: language
223
+
224
+ posts_query.where(id: 1).graph(:language, 'user.person.languages').first.must_equal post
225
+ end
226
+
227
+ it 'Any?' do
100
228
  users_query.empty?.must_equal false
101
229
  users_query.any?.must_equal true
102
230
  end
103
231
 
104
- it 'Any?' do
232
+ it 'Empty?' do
233
+ db[:comments].truncate
234
+
105
235
  comments_query.empty?.must_equal true
106
236
  comments_query.any?.must_equal false
107
237
  end
108
238
 
109
239
  it 'To String' do
110
- users_query.where(id: [1,2,3]).order(:name).to_s.must_equal '#<Rasti::DB::Query: "SELECT * FROM `users` WHERE (`id` IN (1, 2, 3)) ORDER BY `name`">'
240
+ users_query.where(id: [1,2,3]).order(:name).to_s.must_equal '#<Rasti::DB::Query: "SELECT `users`.* FROM `users` WHERE (`users`.`id` IN (1, 2, 3)) ORDER BY `users`.`name`">'
111
241
  end
112
242
 
113
243
  describe 'Named queries' do
@@ -126,108 +256,100 @@ describe 'Query' do
126
256
 
127
257
  describe 'Join' do
128
258
 
129
- before do
130
- 1.upto(10) do |i|
131
- db[:people].insert user_id: i,
132
- document_number: i,
133
- first_name: "Name #{i}",
134
- last_name: "Last Name #{i}",
135
- birth_date: Time.now
136
- end
137
-
138
- 1.upto(3) { |i| db[:categories].insert name: "Category #{i}" }
139
-
140
- db[:comments].insert post_id: 1, user_id: 5, text: 'Comment 1'
141
- db[:comments].insert post_id: 1, user_id: 7, text: 'Comment 2'
142
- db[:comments].insert post_id: 2, user_id: 2, text: 'Comment 3'
143
-
144
- db[:categories_posts].insert post_id: 1, category_id: 1
145
- db[:categories_posts].insert post_id: 1, category_id: 2
146
- db[:categories_posts].insert post_id: 2, category_id: 2
147
- db[:categories_posts].insert post_id: 2, category_id: 3
148
- db[:categories_posts].insert post_id: 3, category_id: 3
149
- end
150
-
151
259
  it 'One to Many' do
152
- users_query.join(:posts).where(title: 'Sample post').all.must_equal [User.new(id: 2, name: 'User 2')]
260
+ users_query.join(:posts).where(Sequel[:posts][:title] => 'Sample post').all.must_equal [User.new(id: 2, name: 'User 2')]
153
261
  end
154
262
 
155
263
  it 'Many to One' do
156
- posts_query.join(:user).where(name: 'User 4').all.must_equal [Post.new(id: 3, user_id: 4, title: 'Best post', body: '...')]
264
+ posts_query.join(:user).where(Sequel[:user][:name] => 'User 4').all.must_equal [Post.new(id: 3, user_id: 4, title: 'Best post', body: '...', language_id: 1)]
157
265
  end
158
266
 
159
267
  it 'One to One' do
160
- users_query.join(:person).where(document_number: 1).all.must_equal [User.new(id: 1, name: 'User 1')]
268
+ users_query.join(:person).where(Sequel[:person][:document_number] => 'document_1').all.must_equal [User.new(id: 1, name: 'User 1')]
161
269
  end
162
270
 
163
271
  it 'Many to Many' do
164
- posts_query.join(:categories).where(name: 'Category 3').order(:id).all.must_equal [
165
- Post.new(id: 2, user_id: 1, title: 'Another post', body: '...'),
166
- Post.new(id: 3, user_id: 4, title: 'Best post', body: '...'),
272
+ posts_query.join(:categories).where(Sequel[:categories][:name] => 'Category 3').order(:id).all.must_equal [
273
+ Post.new(id: 2, user_id: 1, title: 'Another post', body: '...', language_id: 1),
274
+ Post.new(id: 3, user_id: 4, title: 'Best post', body: '...', language_id: 1),
167
275
  ]
168
276
  end
169
277
 
170
278
  it 'Nested' do
171
279
  posts_query.join('categories', 'comments.user.person')
172
280
  .where(Sequel[:categories][:name] => 'Category 2')
173
- .where(Sequel[:comments__user__person][:document_number] => 7)
281
+ .where(Sequel[:comments__user__person][:document_number] => 'document_7')
174
282
  .all
175
- .must_equal [Post.new(id: 1, user_id: 2, title: 'Sample post', body: '...')]
283
+ .must_equal [Post.new(id: 1, user_id: 2, title: 'Sample post', body: '...', language_id: 1)]
176
284
  end
177
285
 
178
- end
286
+ it 'Excluded attributes permanents excluded when join' do
287
+ posts_query.join(:user)
288
+ .exclude_attributes(:body)
289
+ .where(Sequel[:user][:name] => 'User 4')
290
+ .all
291
+ .must_equal [Post.new(id: 3, title: 'Best post', user_id: 4, language_id: 1)]
179
292
 
180
- describe 'NQL' do
293
+ posts_query.exclude_attributes(:body)
294
+ .join(:user)
295
+ .where(Sequel[:user][:name] => 'User 4')
296
+ .all
297
+ .must_equal [Post.new(id: 3, title: 'Best post', user_id: 4, language_id: 1)]
298
+ end
181
299
 
182
- before do
183
- 1.upto(10) do |i|
184
- db[:people].insert user_id: i,
185
- document_number: i,
186
- first_name: "Name #{i}",
187
- last_name: "Last Name #{i}",
188
- birth_date: Time.now
300
+ describe 'Multiple data sources' do
301
+
302
+ it 'One to Many' do
303
+ error = proc { languages_query.join(:posts).all }.must_raise RuntimeError
304
+ error.message.must_equal 'Invalid join of multiple data sources: custom.languages > default.posts'
305
+ end
306
+
307
+ it 'Many to One' do
308
+ error = proc { posts_query.join(:language).all }.must_raise RuntimeError
309
+ error.message.must_equal 'Invalid join of multiple data sources: default.posts > custom.languages'
189
310
  end
190
311
 
191
- 1.upto(3) { |i| db[:categories].insert name: "Category #{i}" }
192
-
193
- db[:comments].insert post_id: 1, user_id: 5, text: 'Comment 1'
194
- db[:comments].insert post_id: 1, user_id: 7, text: 'Comment 2'
195
- db[:comments].insert post_id: 2, user_id: 2, text: 'Comment 3'
196
-
197
- db[:categories_posts].insert post_id: 1, category_id: 1
198
- db[:categories_posts].insert post_id: 1, category_id: 2
199
- db[:categories_posts].insert post_id: 2, category_id: 2
200
- db[:categories_posts].insert post_id: 2, category_id: 3
201
- db[:categories_posts].insert post_id: 3, category_id: 3
312
+ it 'Many to Many' do
313
+ error = proc { languages_query.join(:people).all }.must_raise RuntimeError
314
+ error.message.must_equal 'Invalid join of multiple data sources: custom.languages > default.people'
315
+ end
202
316
  end
203
317
 
318
+ end
319
+
320
+ describe 'NQL' do
321
+
204
322
  it 'Invalid expression' do
205
323
  error = proc { posts_query.nql('a + b') }.must_raise Rasti::DB::NQL::InvalidExpressionError
206
324
  error.message.must_equal 'Invalid filter expression: a + b'
207
325
  end
208
326
 
209
327
  it 'Filter to self table' do
210
- people_query = Rasti::DB::Query.new People, db[:people]
211
-
212
- people_query.nql('user_id > 7')
213
- .map(&:user_id)
214
- .sort
215
- .must_equal [8, 9, 10]
328
+ posts_query.nql('user_id > 1')
329
+ .pluck(:user_id)
330
+ .sort
331
+ .must_equal [2, 4]
216
332
  end
217
333
 
218
334
  it 'Filter to join table' do
219
335
  posts_query.nql('categories.name = Category 2')
220
- .map(&:id)
336
+ .pluck(:id)
221
337
  .sort
222
338
  .must_equal [1, 2]
223
339
  end
224
340
 
225
341
  it 'Filter to 2nd order relation' do
226
- posts_query.nql('comments.user.person.document_number = 7')
227
- .map(&:id)
342
+ posts_query.nql('comments.user.person.document_number = document_7')
343
+ .pluck(:id)
228
344
  .must_equal [1]
229
345
  end
230
346
 
347
+ it 'Filter combined' do
348
+ posts_query.nql('(categories.id = 1 | categories.id = 3) & comments.user.person.document_number = document_2')
349
+ .pluck(:id)
350
+ .must_equal [2]
351
+ end
352
+
231
353
  end
232
354
 
233
355
  end