audrey 0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (4) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +610 -0
  3. data/lib/audrey.rb +2503 -0
  4. metadata +74 -0
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 61cf118da49fbae7bf7dbc82dbf03f1460fe26d9925b135374e97a930039d198
4
+ data.tar.gz: ffc33814130bfcd6b24f70d3d4aebd5c71aff75922fa1cf9fb090789948d6fc9
5
+ SHA512:
6
+ metadata.gz: b8e4a31b17b175e5c038ba1063bddafca7c9e68e59655a764fade27d6459b43232336fc263d32040e7b8e8f75f7c58066af6a433711b3a020efdffe111f9963f
7
+ data.tar.gz: 8f015943ce4204efc60ba516bc6e259044d2be8b6b10d73c5873e042eaaecf1165425f5871afa2d251ea71e1deb3939528b9366f99e24ce11b0ec744a30e76a6
@@ -0,0 +1,610 @@
1
+ # Audrey
2
+
3
+ PLEASE NOTE: Audrey is in the very early stages of development. You're welcome
4
+ to try it out, but it's not ready for prime time yet.
5
+
6
+ Audrey is an easy, yet powerful database system. With Audrey you can create your
7
+ database and start using it in a few lines of code.
8
+
9
+ ## Install
10
+
11
+ ```ruby
12
+ <!-- gem install audrey -->
13
+ ```
14
+
15
+ ## Basic usage
16
+
17
+ By default, Audrey uses SQLite for storage, so the only dependencies are SQLite,
18
+ which is standard on most Unixish systems, and this gem. To create and start
19
+ using a Audrey database, all you have to do is open it with `Audrey.connect`,
20
+ giving a path to a database file and a read/write mode - `'rw'`, `'r'`, or
21
+ `'w'`. The file doesn't need to already exist; it will be created automatically
22
+ as needed.
23
+
24
+ ```ruby
25
+ require 'audrey'
26
+ path = '/tmp/my.db'
27
+
28
+ Audrey.connect(path, 'rw') do |db|
29
+ end
30
+ ```
31
+
32
+ It its simplest use, use the `db` object as a hash to store information:
33
+
34
+ ```text
35
+ Audrey.connect(path, 'rw') do |db|
36
+ db['hero'] = 'Thor'
37
+ db['antagonist'] = 'Loki'
38
+ db['score'] = 1.3
39
+ db['ready'] = true
40
+
41
+ db.each do |k, v|
42
+ puts k + ': ' + v.to_s
43
+ end
44
+ end
45
+ ```
46
+
47
+ That gives us this output:
48
+
49
+ ```text
50
+ hero: Thor
51
+ antagonist: Loki
52
+ score: 1.3
53
+ ready: true
54
+ ```
55
+
56
+ Audrey can store complex structures such as nested arrays and hashes.
57
+
58
+ ```ruby
59
+ Audrey.connect(path, 'rw') do |db|
60
+ db['people'] = {}
61
+ people = db['people']
62
+
63
+ people['fred'] = {'name'=>'Fred'}
64
+ people['mary'] = {'name'=>'Mary', 'towns'=>['Blacksburg', 'Seattle']}
65
+
66
+ people['mary']['friend'] = people['fred']
67
+
68
+ puts db['people']['mary']['name']
69
+ puts db['people']['mary']['towns'].join(', ')
70
+ puts db['people']['mary']['friend']['name']
71
+ end
72
+ ```
73
+
74
+ Take note of the line that reads
75
+ `people['mary']['friend'] = people['fred']`. Audrey doesn't use foreign
76
+ keys. Instead, you simply link objects directly to each other. So in this case,
77
+ `fred` is an element in the people hash, but it is *also* an element
78
+ in Mary's hash with the key `friend`. Objects can be linked in this
79
+ free form manner without the need for setting up lookup tables or defining
80
+ foreign keys.
81
+
82
+ ## Custom classes
83
+
84
+ Audrey provides a system for defining your own classes. In this way, you can
85
+ define your own classes, using their properties and methods as usual. Objects of
86
+ those classes are automatically stored in the Audrey database and can be
87
+ retrieved, as objects, for use.
88
+
89
+ Consider, for example, this simple class:
90
+
91
+ ```text
92
+ class Person < Audrey::Object::Custom
93
+ self.fco = true
94
+ field 'first'
95
+ field 'middle'
96
+ field 'surname'
97
+ end
98
+ ```
99
+
100
+ This class inherits `Audrey::Object::Custom`, which almost all of your
101
+ Audrey classes should do.
102
+
103
+ The next line,
104
+ `self.fco = true`, is important. Every Audrey class must be set at
105
+ `self.fco = true` or `self.fco = false`. `fco` mean "first class object". When
106
+ the database is closed, objects that are not first class objects, and are not
107
+ *descended* from first class objects, are purged. Every custom class must be
108
+ explicitly set with `fco=true` or `fco=false`. Think of clases with`fco=false`
109
+ as being "cascade delete".
110
+
111
+ The next few lines define fields for a person record, first, middle and surname.
112
+
113
+ So we can use our class like this:
114
+
115
+ ```text
116
+ Audrey.connect(path, 'rw') do |db|
117
+ mary = Person.new()
118
+ mary.first = 'Mary'
119
+ mary.middle = 'F.'
120
+ mary.surname = 'Sullivan'
121
+
122
+ fred = Person.new()
123
+ fred.first = 'Fred'
124
+ fred.middle = 'C.'
125
+ fred.surname = 'Murray'
126
+ end
127
+ ```
128
+
129
+ Later we're going to need to find the Person records. The simplest
130
+ way to get to them is to `each` them with the `Person` class
131
+ itself:
132
+
133
+ ```text
134
+ Audrey.connect(path, 'rw') do |db|
135
+ Person.each do |person|
136
+ puts person.first
137
+ end
138
+ end
139
+ ```
140
+
141
+ Notice that there is no need to reinstantiate the objects using data from the
142
+ database. Audrey automatically creates the objects from the stored data, and
143
+ makes them available for use as they were originally created.
144
+
145
+ The example above produces this output:
146
+
147
+ ```text
148
+ Fred
149
+ Mary
150
+ ```
151
+
152
+ Under the hood, `each` uses a type of query called q0. Later we'll look at more
153
+ details about how you can use Q0.
154
+
155
+ ### Subclassing
156
+
157
+ Like any Ruby class, you can subclass your Audrey classes. For example, let's
158
+ subclass `Person` with `Guest`, and subclass `Guest` with `Preferred`:
159
+
160
+ ```text
161
+ class Guest < Person
162
+ field 'stays'
163
+ end
164
+
165
+ class Preferred < Guest
166
+ field 'rating'
167
+ end
168
+ ```
169
+
170
+ Notice that we didn't bother to set these subclasses as first class objects --
171
+ they inherited that property from `Person`. We also added a field to each of
172
+ our new subclasses. Now we can add `Guest` and `Preferred` objects to the
173
+ database:
174
+
175
+ ```text
176
+ Audrey.connect(path, 'rw') do |db|
177
+ dan = Guest.new()
178
+ dan.first = 'Dan'
179
+ dan.stays = 10
180
+
181
+ pete = Preferred.new()
182
+ pete.first = 'Pete'
183
+ pete.stays = 12
184
+ pete.rating = 'gold'
185
+ end
186
+ ```
187
+
188
+ Because `Guest` and `Preferred` derive from `Person`, we can iterate through
189
+ all of the records using `Person`, including objects from its derived classes.
190
+
191
+ ```text
192
+ Audrey.connect(path, 'rw') do |db|
193
+ Person.each do |person|
194
+ puts person.first
195
+ end
196
+ end
197
+ ```
198
+
199
+ which produces this output:
200
+
201
+ ```text
202
+ Fred
203
+ Pete
204
+ Mary
205
+ Dan
206
+ ```
207
+
208
+
209
+ ## Autocommit and transactions
210
+
211
+ In all the examples so far, data has been written to the Audrey database
212
+ automatically as it has been produced. However, you might want to only
213
+ atomically commit data at specified points. You can do this using the
214
+ autocommit feature, or transactions.
215
+
216
+ ### autocommit
217
+
218
+ To keep Audrey from automatically writing data, use the `autocommit` option in
219
+ `connect`, like this:
220
+
221
+ ```ruby
222
+ Audrey.connect(path, 'rw', 'immediate_commit'=>false) do |db|
223
+ db['hero'] = 'Thor'
224
+ end
225
+ ```
226
+
227
+ In the example above, the data is never committed by the end of the session, so
228
+ if we try to retrieve the data:
229
+
230
+ ```ruby
231
+ Audrey.connect(path, 'rw') do |db|
232
+ puts 'hero: ', db['hero']
233
+ end
234
+ ```
235
+
236
+ \.\.\. we get this disappointing output:
237
+
238
+ ```text
239
+ hero:
240
+
241
+ ```
242
+
243
+ To commit, simply use the `commit` method:
244
+
245
+ ```ruby
246
+ Audrey.connect(path, 'rw', 'immediate_commit'=>false) do |db|
247
+ db['hero'] = 'Thor'
248
+ db.commit
249
+ end
250
+ ```
251
+
252
+ Which will give us more fulfilling results:
253
+
254
+ ```text
255
+ hero:
256
+ Thor
257
+ ```
258
+
259
+ Any time during the session you can use `rollback` to rollback to the previous
260
+ commit or to the state of the database when it was opened. So this code:
261
+
262
+ ```ruby
263
+ Audrey.connect(path, 'rw', 'immediate_commit'=>false) do |db|
264
+ db['antagonist'] = 'Loki'
265
+ db.rollback
266
+ puts 'antagonist: ', db['antagonist']
267
+ end
268
+ ```
269
+
270
+ \.\.\. will output without Loki:
271
+
272
+ ```text
273
+ antagonist:
274
+
275
+ ```
276
+
277
+ ### transaction
278
+
279
+ For more fine-grained control of when data is commited, you might prefer to use
280
+ `transaction`. Any time during a database session you can start a transaction
281
+ block. Changes to the database inside that block are not committed without an
282
+ explicit commit command. For example, consider this code::
283
+
284
+ ```text
285
+ Audrey.connect(path, 'rw') do |db|
286
+ db['hero'] = 'Thor'
287
+
288
+ db.transaction do |tr|
289
+ db['antagonist'] = 'Loki'
290
+ end
291
+
292
+ db.each do |k, v|
293
+ puts k + ': ' + v
294
+ end
295
+ end
296
+ ```
297
+
298
+ The database session is set to automatically commit data as it is created
299
+ (because `autocommit` defaults to true). However, when we use `db.transaction`
300
+ to start a transaction block. Within that block, data is not automatically
301
+ committed. So the output for this code will look like this:
302
+
303
+ ```text
304
+ hero: Thor
305
+ ```
306
+
307
+ Inside a transaction block, you can commit by calling the transaction's `commit`
308
+ method:
309
+
310
+ ```text
311
+ Audrey.connect(path, 'rw') do |db|
312
+ db['hero'] = 'Thor'
313
+
314
+ db.transaction do |tr|
315
+ db['antagonist'] = 'Loki'
316
+ tr.commit
317
+ end
318
+
319
+ db.each do |k, v|
320
+ puts k + ': ' + v
321
+ end
322
+ end
323
+ ```
324
+
325
+ That gives us this output:
326
+
327
+ ```text
328
+ hero: Thor
329
+ antagonist: Loki
330
+ ```
331
+
332
+ Rollback transactions with the `rollback` method. For example, in this code we
333
+ use both `rollback` and `commit`:
334
+
335
+ ```text
336
+ Audrey.connect(path, 'rw') do |db|
337
+ db['hero'] = 'Thor'
338
+
339
+ db.transaction do |tr|
340
+ db['antagonist'] = 'Loki'
341
+ tr.rollback
342
+ db['ally'] = 'Captain America'
343
+ tr.commit
344
+ end
345
+
346
+ db.each do |k, v|
347
+ puts k + ': ' + v
348
+ end
349
+ end
350
+ ```
351
+
352
+ In that example, we set
353
+ `db['antagonist'] = 'Loki'`. But in the next line we roll it back. Then in the next two lines we set
354
+ `db['ally'] = 'Captain America'` and commit it. The result is that `antagonist` is never committed but `ally`
355
+ is, producing this output:
356
+
357
+ ```text
358
+ hero: Thor
359
+ ally: Captain America
360
+ ```
361
+
362
+ Transactions can be nested. For example, in this code, the outer transaction is
363
+ committed, but not the inner transaction:
364
+
365
+ ```text
366
+ Audrey.connect(path, 'rw') do |db|
367
+ db['hero'] = 'Thor'
368
+
369
+ db.transaction do |tr1|
370
+ db['antagonist'] = 'Loki'
371
+
372
+ db.transaction do |tr2|
373
+ db['ally'] = 'Captain America'
374
+ end
375
+
376
+ tr1.commit
377
+ end
378
+
379
+ db.each do |k, v|
380
+ puts k + ': ' + v
381
+ end
382
+ end
383
+ ```
384
+
385
+ That gives us this output
386
+
387
+ ```text
388
+ hero: Thor
389
+ antagonist: Loki
390
+ ```
391
+
392
+ If an inner transaction is committed, but not the outer transaction, then
393
+ nothing in the inner transaction is finally committed. So, for example, consider
394
+ this code:
395
+
396
+ ```text
397
+ Audrey.connect(path, 'rw') do |db|
398
+ db['hero'] = 'Thor'
399
+
400
+ db.transaction do |tr1|
401
+ db['antagonist'] = 'Loki'
402
+
403
+ db.transaction do |tr2|
404
+ db['ally'] = 'Captain America'
405
+ tr2.commit
406
+ end
407
+ end
408
+
409
+ db.each do |k, v|
410
+ puts k + ': ' + v
411
+ end
412
+ end
413
+ ```
414
+
415
+ In that example we commit `tr2`. But `tr2` is nested inside `tr1`, which is
416
+ never committed. Therefore everything inside `tr1` is rolled back, giving us
417
+ this output:
418
+
419
+ ```text
420
+ hero: Thor
421
+ ```
422
+
423
+ ### Exiting database connections and transactions
424
+
425
+ You might find that in some situations you don't need to continue a transaction,
426
+ or even an entire database connection, if certain conditions are met. For example,
427
+ suppose you want to add a record using web parameters, but only if the surname
428
+ is given. You might do that like this:
429
+
430
+ ```ruby
431
+ Audrey.connect(path, 'rw') do |db|
432
+ db.transaction do |tr|
433
+ person = Person.new()
434
+ person.surname = cgi['surname']
435
+
436
+ if not person.surname
437
+ tr.exit
438
+ end
439
+
440
+ person.first = cgi['first']
441
+ person.middle= cgi['middle']
442
+ puts 'commit'
443
+ tr.commit
444
+ end
445
+ end
446
+ ```
447
+
448
+ In this example, if no surname is given, the transaction stops when it hits
449
+ `tr.exit`. Nothing else in the transaction after that line is run.
450
+
451
+ You can also exit an entire database session with `db.exit`. So, using the same
452
+ business rules as above, you might code your database connection like this:
453
+
454
+ ```ruby
455
+ Audrey.connect(path, 'rw') do |db|
456
+ if not cgi['surname']
457
+ db.exit
458
+ end
459
+
460
+ person = Person.new()
461
+ person.surname = cgi['surname']
462
+ person.first = cgi['first']
463
+ person.middle= cgi['middle']
464
+ end
465
+ ```
466
+
467
+ ## Queries with Q0
468
+
469
+ Audrey provides a query system called Q0. With Q0 you can perform basic queries
470
+ on objects, searching for by class and by field value.
471
+
472
+ ### Q0 will not be the only query language
473
+
474
+ Before we go further, though, it's important to understand what Q0 is *not*: it
475
+ is not the only query language that Audrey will ever have. One of the problems
476
+ that database systems often have is that their query languages becomes more and
477
+ more convoluted as needs evolve. SQL is a good example. What started as a simple
478
+ system with English-like syntax evolved into a bizarre language with all manner
479
+ of join and function syntaxes. So, instead of assuming that we can invent a query
480
+ language that will always provide all needs, Audrey is designed to allow for
481
+ multiple query languages. As needs evolve and Q0 becomes unsuitable for advanced
482
+ needs, new query languages can be invented and added into the Audrey system.
483
+
484
+ Audrey will always have Q0. As long as they don't create problems with backward
485
+ compatibility, new features can be added to Q0.
486
+
487
+ ### Search by class
488
+
489
+ Let's start with a simple example. In the following code, we create a Q0 object
490
+ with `db.q0`. We tell the query to look for everything in the `Person` class.
491
+ Then we `each` through the results:
492
+
493
+ ```text
494
+ Audrey.connect(path, 'rw') do |db|
495
+ query = db.q0
496
+ query.fclass = Person
497
+
498
+ query.each do |person|
499
+ puts person.surname
500
+ end
501
+ end
502
+ ```
503
+
504
+ Note that to search by class we use `fclass` (for *Audrey class*), not just
505
+ `class`. There are several reasons for this. First, the `class` property is
506
+ already taken. Second, as Audrey grows and is implemented by languages besides
507
+ Ruby, it will be necessary to distinguish because a Ruby class and a Audrey
508
+ class. Audrey classes have the same names as Ruby classes, but other languages,
509
+ such as Python, use a different naming scheme for classes.
510
+
511
+ The example above is functionally identical to one of the earlier examples in
512
+ which we use the `Person` class itself to search for records:
513
+
514
+ ```text
515
+ Audrey.connect(path, 'rw') do |db|
516
+ Person.each do |person|
517
+ puts person.first
518
+ end
519
+ end
520
+ ```
521
+
522
+ ### Search by field values
523
+
524
+ Now we're going to filter by the values of fields. In this example, we set the
525
+ query to look for records in which the object's `first` field is "Mary".
526
+
527
+ ```text
528
+ Audrey.connect(path, 'rw') do |db|
529
+ query = db.q0
530
+ query.fclass = Person
531
+ query.fields['first'] = 'Mary'
532
+
533
+ query.each do |person|
534
+ puts person.surname
535
+ end
536
+ end
537
+ ```
538
+
539
+ To search for multiple possible values of a field, use an array that contains
540
+ all possible values:
541
+
542
+ ```text
543
+ query.fields['first'] = ['Mary', 'Fred']
544
+ ```
545
+
546
+ To search for records in which the field is nil or missing, use `nil`:
547
+
548
+ ```text
549
+ query.fields['first'] = nil
550
+ ```
551
+
552
+ To search for any value, as long as it is not nil, use the query's `defined`
553
+ method:
554
+
555
+ ```text
556
+ query.fields['first'] = query.defined
557
+ ```
558
+
559
+ You can mix and match these options in an array:
560
+
561
+ ```text
562
+ query.fields['first'] = ['Mary', nil]
563
+ ```
564
+
565
+ ### Count
566
+
567
+ In addition to looping through query results, you can also get just a count of
568
+ how many objects are found. Simply use the query's count method:
569
+
570
+ ```text
571
+ puts query.count()
572
+ ```
573
+
574
+ ### Other query filters
575
+
576
+ Currently, Q0 only allows you to search by fclass and field values. More filters
577
+ will be added as Audrey develops.
578
+
579
+ ## Speed
580
+
581
+ I haven't done any benchmark tests on Audrey yet. I would be very interested to
582
+ see some if anybody would like to contribute in that way.
583
+
584
+ That being said, Audrey is probably not currently very fast. Keep in mind,
585
+ though, that it's worthwhile to balance execution speed against development
586
+ speed. Audrey, in its current implementation, probably isn't particularly fast
587
+ in execution, but it allows you go from no project to working project faster
588
+ than most database systems. Consider the tradeoff.
589
+
590
+
591
+ ## The name and logo
592
+
593
+ I chose the name *Audrey* because I like it\.\.\. there's no fancy reason beyond
594
+ that. Apparently in Irish, *Audrey* once meant "vine", so I decided to use the
595
+ leaf of the
596
+ [Lonicera periclymenum](https://en.wikipedia.org/wiki/Lonicera_periclymenum),
597
+ a vine common in Ireland, for the logo. The logo was designed by
598
+ [Inventicstudios](https://www.fiverr.com/inventicstudios).
599
+
600
+
601
+ ## Author
602
+
603
+ Mike O'Sullivan
604
+ mike@idocs.com
605
+
606
+ ## History
607
+
608
+ | version | date | notes |
609
+ |---------|-------------|--------------------------|
610
+ | 0.1 | Jan 7, 2020 | Initial upload. |