db-struct 0.1.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.
@@ -0,0 +1,452 @@
1
+
2
+ require 'spec_helper'
3
+ require 'dbstruct'
4
+ require 'set'
5
+
6
+
7
+
8
+ BOOK1_ITEMS = {title: "The Summoning of Dragons",
9
+ author: "Tubal de Malachite",
10
+ date: Time.new(1989, 11, 1),
11
+ edition: 1}
12
+
13
+ BOOK2_ITEMS = {title: "Howe To Kille Insects",
14
+ author: "Humptulip",
15
+ date: Time.new(1985, 8, 1), # guess
16
+ edition: 1}
17
+
18
+ BOOK3_ITEMS = {title: "Diseases of the Dragon",
19
+ author: "Lady Sybil Ramkin",
20
+ date: Time.new(1989, 11, 1),
21
+ edition: 1}
22
+
23
+
24
+ def make_post_items(fora, users, count)
25
+ posts = []
26
+ mc = 0
27
+ for f in fora
28
+ for u in users
29
+ (1..count).each do
30
+ mc += 1
31
+ posts << {
32
+ forum: f,
33
+ user: u,
34
+
35
+ subject: "subject number #{mc}",
36
+ date: Time.now,
37
+ contents: "message text #{mc}",
38
+ }
39
+ end
40
+ end
41
+ end
42
+
43
+ return posts
44
+ end
45
+
46
+ def make_posts(fora, users, count) =
47
+ make_post_items(fora, users, count)
48
+ .map{|pi| Posts.new(**pi)}
49
+
50
+
51
+
52
+
53
+ POSTS_ITEMS = proc{
54
+ fora = %w{forum1 forum2 forum3}
55
+ users = %w{u1 u2 u3}
56
+
57
+ make_post_items(fora, users, 1)
58
+ }.call
59
+
60
+
61
+ describe DBStruct do
62
+ before(:all) do
63
+ @tmpdir = TmpDir.dir
64
+
65
+ dbfile = TmpDir.filepath("db.sqlite3")
66
+ @db = jruby? ?
67
+ Sequel.connect("jdbc:sqlite:#{dbfile}") :
68
+ Sequel.sqlite(dbfile)
69
+ #puts "Created #{dbfile}"
70
+
71
+ end
72
+
73
+ after(:all) do
74
+ TmpDir.cleanup!
75
+ end
76
+
77
+ it "lets you define dbstruct types" do
78
+ Book = DBStruct.with(@db, :books) do
79
+ field :title, String
80
+ field :author, String
81
+ field :date, Time
82
+ field :edition, Integer
83
+
84
+ def foo(x) = x+x
85
+ end
86
+
87
+ expect( Book.items.size ) .to eq 0
88
+ end
89
+
90
+ book1 = nil
91
+ it "lets you create an instance" do
92
+ book1 = Book.new(**BOOK1_ITEMS)
93
+ expect( Book.items.size ) .to eq 1
94
+
95
+ BOOK1_ITEMS.each{|k, v| expect( book1.send(k) ) .to eq v }
96
+ expect( book1.to_h ) .to eq BOOK1_ITEMS
97
+
98
+ b1a = Book.items[book1.rowid]
99
+ expect( b1a ) .to eq book1
100
+ expect( b1a.to_h ) .to eq BOOK1_ITEMS
101
+
102
+ b1b = Book.items.values.first
103
+ expect( b1b ) .to eq book1
104
+ end
105
+
106
+ it "lets you define and call methods" do
107
+ expect( book1.foo(2) ) .to eq 4
108
+ end
109
+
110
+ book2 = nil
111
+ it "lets you create multiple instances" do
112
+ book2 = Book.new(**BOOK2_ITEMS)
113
+ expect( Book.items.size ) .to eq 2
114
+
115
+ expect( book2 ) .to_not eq book1
116
+ expect( book2.to_a ) .to_not eq BOOK1_ITEMS
117
+
118
+ Book.items.values.each{|item|
119
+ expect( Book.items[item.rowid] ) .to eq item
120
+ }
121
+ end
122
+
123
+ it "lets you change field values" do
124
+ book2.edition = 3
125
+
126
+ expect( book2.edition ) .to eq 3
127
+ expect( Book.items[book2.rowid].edition ) .to eq 3
128
+
129
+ b2a = Book.items.each
130
+ .map{|rowid, book| book}
131
+ .find{|book| book.title == BOOK2_ITEMS[:title]}
132
+ expect( b2a.edition ) .to eq 3
133
+
134
+ book2.edition = BOOK2_ITEMS[:edition]
135
+ expect( book2.edition ) .to eq BOOK2_ITEMS[:edition]
136
+ end
137
+
138
+ it "has various struct-like conversions" do
139
+ expect( book2.to_h ) .to eq BOOK2_ITEMS
140
+ expect( book2.to_a.to_h) .to eq BOOK2_ITEMS
141
+
142
+ BOOK2_ITEMS.each{|k, v|
143
+ expect( book2[k] ) .to eq v
144
+ }
145
+
146
+ book2[:edition] = 42
147
+ expect( book2.edition ) .to eq 42
148
+ book2.edition = BOOK2_ITEMS[:edition]
149
+ end
150
+
151
+ it "lets you categorize along group values" do
152
+ Posts = DBStruct.with(@db, :posts) do
153
+ group :forum, String
154
+ group :user, String
155
+
156
+ field :subject, String
157
+ field :date, Time
158
+ field :contents, String
159
+ end
160
+
161
+ fora = []
162
+ users = []
163
+ posts = []
164
+ POSTS_ITEMS.each{|post|
165
+ posts << Posts.new(**post)
166
+ fora << post[:forum]
167
+ users << post[:user]
168
+ }
169
+ users.uniq!
170
+ fora.uniq!
171
+
172
+ expect( Posts.items.size ) .to eq posts.size
173
+ expect( Posts.items.values.to_set ) .to eq posts.to_set
174
+
175
+ # There should be exactly one post per forum/user
176
+ for forum in fora
177
+ for user in users
178
+ i = Posts.items(forum, user)
179
+ expect( i.size ) .to eq 1
180
+
181
+ p = i.values.first
182
+ expect( p.forum ) .to eq forum
183
+ expect( p.user ) .to eq user
184
+ end
185
+ end
186
+
187
+ # nil is the wildcard when selecting across groups. Let's test
188
+ # those.
189
+ all = []
190
+ for forum in fora
191
+ i = Posts.items(forum)
192
+ expected = posts.select{|ii| ii.forum == forum}.to_set
193
+ expect( i.values.to_set ) .to eq expected
194
+ all += i.values
195
+ end
196
+ expect( all.uniq.size ) .to eq all.size
197
+ expect( all.to_set ) .to eq posts.to_set
198
+
199
+ all = []
200
+ for user in users
201
+ i = Posts.items(nil, user)
202
+ expected = posts.select{|ii| ii.user == user}.to_set
203
+ expect( i.values.to_set ) .to eq expected
204
+ all += i.values
205
+ end
206
+ expect( all.uniq.size ) .to eq all.size
207
+ expect( all.to_set ) .to eq posts.to_set
208
+ end
209
+
210
+ it "lets you create custom BogoStructs around Sequel queries" do
211
+ q1 = Posts.where(user: %w{u1 u3}, forum: "f2")
212
+ ex1 = POSTS_ITEMS.select{|i|
213
+ %w{u1 u3}.include?i[:user] && i[:forum] == "f2"
214
+ }
215
+ expect( q1.values.uniq ) .to eq q1.values
216
+ expect( q1.values.size ) .to eq ex1.size
217
+
218
+ q1.values.each{|item|
219
+ expect( item.forum ) .to eq "f2"
220
+ expect( %w{u1 u3} ) .to include(item.user)
221
+ }
222
+ end
223
+
224
+ it "and it lets you chain them together, possibly using blocks" do
225
+ q1 = Posts.where(user: %w{u1 u3}).where{forum == "f2"}
226
+ ex1 = POSTS_ITEMS.select{|i|
227
+ %w{u1 u3}.include?i[:user] && i[:forum] == "f2"
228
+ }
229
+ expect( q1.values.uniq ) .to eq q1.values
230
+ expect( q1.values.size ) .to eq ex1.size
231
+
232
+ q1.values.each{|item|
233
+ expect( item.forum ) .to eq "f2"
234
+ expect( %w{u1 u3} ) .to include(item.user)
235
+ }
236
+ end
237
+
238
+ book3 = nil
239
+ book4 = nil
240
+ it "allows uninitialized fields in new items" do
241
+ book3 = Book.new
242
+ expect( book3.to_a.size ) .to eq 4
243
+
244
+ expect( book3.title ) .to eq nil
245
+ expect( book3.author ) .to eq nil
246
+ expect( book3.date ) .to eq nil
247
+ expect( book3.edition ) .to eq nil
248
+
249
+ book3.title = BOOK3_ITEMS[:title]
250
+ book3.author = BOOK3_ITEMS[:author]
251
+ book3.date = BOOK3_ITEMS[:date]
252
+ book3.edition = BOOK3_ITEMS[:edition]
253
+
254
+ expect( book3.to_h ) .to eq BOOK3_ITEMS
255
+
256
+ book4 = Book.new(title: "The Unknown Book")
257
+ expect( book4.title ) .to eq "The Unknown Book"
258
+ expect( book4.author ) .to eq nil
259
+ expect( book4.date ) .to eq nil
260
+ expect( book4.edition ) .to eq nil
261
+ end
262
+
263
+ it "fails if you attempt to set an unknown field." do
264
+ expect{ Book.new(title: "bad book", badness: 42) }
265
+ .to raise_error DBStruct::FieldError
266
+
267
+ expect{ book1.floop = 42 }
268
+ .to raise_error NoMethodError
269
+
270
+ expect{ book1[:floop] = 42 }
271
+ .to raise_error DBStruct::FieldError
272
+ end
273
+
274
+ it "ensures field values are of the expected type" do
275
+ expect{ book1.title = Time.now }
276
+ .to raise_error DBStruct::TypeError
277
+ expect{ book1.edition = "42" }
278
+ .to raise_error DBStruct::TypeError
279
+ expect{ book1.edition = Time.now }
280
+ .to raise_error DBStruct::TypeError
281
+
282
+ expect( book1.to_h ) .to eq BOOK1_ITEMS
283
+ end
284
+
285
+ book5 = nil
286
+ it "always allows null as a type" do
287
+ book1.title = nil
288
+ expect( book1.title ) .to eq nil
289
+ book1.title = BOOK1_ITEMS[:title]
290
+ expect( book1.to_h ) .to eq BOOK1_ITEMS
291
+
292
+ book5 = Book.new(title: "An Anonymous Book",
293
+ author: nil)
294
+ expect( book5.title ) .to eq "An Anonymous Book"
295
+ expect( book5.author ) .to eq nil
296
+ expect( book5.date ) .to eq nil
297
+ expect( book5.edition ) .to eq nil
298
+ end
299
+
300
+ it "supports block enumeration over tables" do
301
+ expect( Book.items.size ) .to be > 1 # consistency check
302
+
303
+ kv = []
304
+ Book.items.each{|k,v| kv.push [k,v]}
305
+ expect( kv.size ) .to eq Book.items.size
306
+
307
+ kv.each{|k,v|
308
+ expect( Book.items[k] ) .to eq v
309
+ }
310
+
311
+ # Test each_key
312
+ keys = []
313
+ Book.items.each_key{|rowid|
314
+ expect( Book.items[rowid].rowid ) .to eq rowid
315
+ keys.push(rowid)
316
+ }
317
+ expect( keys.size ) .to eq kv.size
318
+ expect( keys.to_set ) .to eq Book.items.keys.to_set
319
+ expect( keys.to_set ) .to eq kv.map{|k,v| k}.to_set
320
+
321
+ # Test each_value
322
+ values = []
323
+ Book.items.each_value{|row|
324
+ expect( Book.items[row.rowid] ) .to eq row
325
+ values.push(row)
326
+ }
327
+ expect( values.size ) .to eq keys.size
328
+ expect( values.to_set ) .to eq Book.items.values.to_set
329
+ expect( values.to_set ) .to eq kv.map{|k,v| v}.to_set
330
+ end
331
+
332
+ it "provides enumerators as well" do
333
+ e = Book.items.each
334
+ expect( e.class ) .to eq Enumerator
335
+ expect( e.to_a.to_set ) .to eq Book.items.to_a.to_set
336
+
337
+ expect( Book.items.each_key.to_a.to_set ) .to eq Book.items.keys.to_set
338
+ expect( Book.items.each_value.to_a.to_set ) .to eq Book.items.values.to_set
339
+ end
340
+
341
+ it "includes Enumerable" do
342
+ # Just call a couple of methods to ensure that they work. We
343
+ # assume the rest will work correctly.
344
+ expect( Book.items.any?{true} ) .to be true
345
+ expect( Book.items.select{true} ) .to eq Book.items.to_a
346
+ end
347
+
348
+ it "fails gracefully when given an invalid row ID" do
349
+ # Invalid wrt the entire
350
+ maxrow = Posts.items.keys.max
351
+ expect( Posts.items[maxrow + 1] ) .to eq nil
352
+
353
+ # Garbage row Ids should also return nil
354
+ expect( Posts.items[ ["garbage!!!"] ] ) .to eq nil
355
+
356
+ # Invalid key but not part of the current group. Note that
357
+ # changes in the order of Post creation above could break this; we
358
+ # assume that there is at least one row-id that is larger than the
359
+ # largest row-id in this group. If not, the following test will
360
+ # fail.
361
+ grp = Posts.items("forum1", "u1")
362
+ maxrow_grp = grp.keys.max
363
+ expect( maxrow_grp ) .to be < maxrow
364
+
365
+ outside_row = maxrow_grp + 1
366
+ expect( grp[outside_row] ) .to eq nil
367
+ expect( Posts.items[outside_row] ) .not_to eq nil
368
+
369
+ # Ditto for `where`; we abuse it to select by row-id; this may or
370
+ # may not be supported.
371
+ grp2 = Posts.where{_id < maxrow - 3}
372
+ expect( grp2[maxrow - 1] ) .to eq nil
373
+ end
374
+
375
+ it "supports row deletion but only in its group of items" do
376
+ fx = %w{xforum1 xforum2}
377
+ ux = %w{alice bob charlene}
378
+
379
+ xposts = make_posts(fx, ux, 2)
380
+ xposts.each{|r| expect( Posts.items[r.rowid] ) .to eq r }
381
+
382
+ f1 = Posts.items("xforum1")
383
+ f2 = Posts.items("xforum2")
384
+
385
+ d1 = f1.keys.first
386
+
387
+ # Can't delete an item not in the current group
388
+ expect( f2.delete(d1) ) .to eq nil
389
+ expect( f2[d1] ) .to eq nil
390
+
391
+ # row objects can tell you if the underlying row exists
392
+ target = f1[d1]
393
+ expect( target.present? ) .to be true
394
+ expect( target.deleted? ) .to be false
395
+
396
+ # Deletion returns the value and removes it from the database
397
+ expect( f1[d1] ) .to eq target
398
+ expect( f1.delete(d1) ) .to eq target
399
+ expect( f1[d1] ) .to eq nil
400
+ expect( Posts.items[d1] ) .to eq nil
401
+
402
+ # And the lingering row object should handle this gracefully
403
+ expect( target.present? ) .to be false
404
+ expect( target.deleted? ) .to be true
405
+ expect{ target.user }
406
+ .to raise_error DBStruct::MissingRowError
407
+ end
408
+
409
+ it "supports 'delete_if'" do
410
+ c = Posts.items("xforum1").size
411
+ Posts.items.delete_if{|k, v|
412
+ next false unless v.forum == "xforum1"
413
+ c -= 1
414
+ true
415
+ }
416
+ expect( Posts.items("xforum1").size ) .to eq 0
417
+ expect( c ) .to eq 0
418
+ end
419
+
420
+ it "supports clearing per group" do
421
+ c = Posts.items("xforum2").size
422
+ expect( c ) .to be > 0
423
+
424
+ Posts.items("xforum2").clear
425
+ expect( Posts.items("xforum2").size ) .to eq 0
426
+ end
427
+
428
+ it "can deal with any Ruby type Sequel can handle" do
429
+ BigTbl = DBStruct.with(@db, :bigtbl) do
430
+ field :i1, Integer
431
+ field :s1, String
432
+ field :fn1, Fixnum
433
+ field :bn1, Bignum
434
+ field :f1, Float
435
+ field :bd1, BigDecimal
436
+ field :dt1, Date
437
+ field :dtt1, DateTime
438
+ field :tm1, Time
439
+ field :nm1, Numeric
440
+ field :b1, TrueClass
441
+ field :b2, FalseClass
442
+ end
443
+
444
+ b1 = BigTbl.new
445
+
446
+ # TrueClass and FalseClass are shorthand for either boolean
447
+ b1.b2 = true
448
+ b1.b1 = false
449
+ end
450
+
451
+
452
+ end
@@ -0,0 +1,48 @@
1
+ require 'securerandom'
2
+ require 'fileutils'
3
+
4
+ SpecDir = File.realpath( File.dirname(__FILE__) )
5
+
6
+ $: << File.join(SpecDir, '..', 'lib')
7
+ $: << File.join(SpecDir, '..', 'src')
8
+
9
+ def jruby?
10
+ return RUBY_PLATFORM == "java"
11
+ end
12
+
13
+ module TmpDir
14
+ @root = File.join( File.dirname(__FILE__), "tmpdata")
15
+ @tmpdir = File.join(@root, SecureRandom.uuid)
16
+ @count = 0
17
+
18
+ def self.dir
19
+ FileUtils.mkdir_p(@tmpdir) unless File.directory?(@tmpdir)
20
+ return @tmpdir
21
+ end
22
+
23
+ def self.filepath(name, versioned: false)
24
+ self.dir # ensure @tmpdir exists
25
+
26
+ filepath = File.join(@tmpdir, name)
27
+ if versioned
28
+ while File.exist?(filepath)
29
+ @count += 1
30
+
31
+ nameparts = name.split('.')
32
+ suffix = nameparts.pop if nameparts.size > 1
33
+ nameparts.push @count.to_s
34
+ nameparts.push suffix if suffix
35
+
36
+ newname = nameparts.join(".")
37
+ filepath = File.join(@tmpdir, newname)
38
+ end
39
+ end
40
+
41
+ return filepath
42
+ end
43
+
44
+ def self.cleanup!
45
+ return unless File.directory?(@root)
46
+ FileUtils.rm_rf(@root)
47
+ end
48
+ end
metadata ADDED
@@ -0,0 +1,138 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: db-struct
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Chris Reuter
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2024-09-09 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: 5.76.0
19
+ name: sequel
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 5.76.0
27
+ - !ruby/object:Gem::Dependency
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '3.10'
33
+ - - ">="
34
+ - !ruby/object:Gem::Version
35
+ version: 3.10.0
36
+ name: rspec
37
+ type: :development
38
+ prerelease: false
39
+ version_requirements: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - "~>"
42
+ - !ruby/object:Gem::Version
43
+ version: '3.10'
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: 3.10.0
47
+ - !ruby/object:Gem::Dependency
48
+ requirement: !ruby/object:Gem::Requirement
49
+ requirements:
50
+ - - "~>"
51
+ - !ruby/object:Gem::Version
52
+ version: 0.9.25
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ version: 0.9.25
56
+ name: yard
57
+ type: :development
58
+ prerelease: false
59
+ version_requirements: !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - "~>"
62
+ - !ruby/object:Gem::Version
63
+ version: 0.9.25
64
+ - - ">="
65
+ - !ruby/object:Gem::Version
66
+ version: 0.9.25
67
+ description: |2
68
+ DBStruct is a class that, like Struct, provides a convenient way
69
+ to create subclasses with named fields that can be accessed via
70
+ the usual Ruby setters and getters. Unlike Struct, their contents
71
+ are stored in a SQLite3 database.
72
+
73
+ In addition, each subclass also provides access to the database
74
+ via an interface that closely mimics a Ruby Hash, including
75
+ support for enumeration.
76
+ email: chris@remove-this-part.blit.ca
77
+ executables: []
78
+ extensions: []
79
+ extra_rdoc_files: []
80
+ files:
81
+ - README.md
82
+ - Rakefile
83
+ - db-struct.gemspec
84
+ - doc/DBStruct.html
85
+ - doc/DBStruct/BogoHash.html
86
+ - doc/DBStruct/ErrorHelpers.html
87
+ - doc/DBStruct/Failure.html
88
+ - doc/DBStruct/FieldError.html
89
+ - doc/DBStruct/MissingRowError.html
90
+ - doc/DBStruct/TypeError.html
91
+ - doc/_index.html
92
+ - doc/class_list.html
93
+ - doc/css/common.css
94
+ - doc/css/full_list.css
95
+ - doc/css/style.css
96
+ - doc/file.README.html
97
+ - doc/file_list.html
98
+ - doc/frames.html
99
+ - doc/index.html
100
+ - doc/js/app.js
101
+ - doc/js/full_list.js
102
+ - doc/js/jquery.js
103
+ - doc/method_list.html
104
+ - doc/top-level-namespace.html
105
+ - gem.deps.rb
106
+ - lib/dbstruct.rb
107
+ - lib/internal/bogohash.rb
108
+ - lib/internal/dbstruct_base.rb
109
+ - lib/internal/dbstruct_class.rb
110
+ - lib/internal/error.rb
111
+ - lib/internal/util.rb
112
+ - spec/dbstruct_spec.rb
113
+ - spec/spec_helper.rb
114
+ homepage: https://codeberg.org/suetanvil/db-struct
115
+ licenses:
116
+ - MIT
117
+ metadata: {}
118
+ post_install_message:
119
+ rdoc_options: []
120
+ require_paths:
121
+ - lib
122
+ required_ruby_version: !ruby/object:Gem::Requirement
123
+ requirements:
124
+ - - ">="
125
+ - !ruby/object:Gem::Version
126
+ version: 3.0.0
127
+ required_rubygems_version: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ requirements:
133
+ - Sequel
134
+ rubygems_version: 3.3.26
135
+ signing_key:
136
+ specification_version: 4
137
+ summary: Persistant storage of sets of structured records.
138
+ test_files: []