og 0.23.0 → 0.24.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.
- data/ProjectInfo +58 -0
- data/README +5 -4
- data/Rakefile +2 -2
- data/doc/AUTHORS +10 -7
- data/doc/RELEASES +108 -0
- data/lib/og.rb +1 -3
- data/lib/og/collection.rb +4 -4
- data/lib/og/entity.rb +96 -27
- data/lib/og/evolution.rb +78 -0
- data/lib/og/manager.rb +29 -32
- data/lib/og/mixin/hierarchical.rb +1 -1
- data/lib/og/mixin/optimistic_locking.rb +5 -8
- data/lib/og/mixin/orderable.rb +15 -2
- data/lib/og/mixin/schema_inheritance_base.rb +12 -0
- data/lib/og/mixin/taggable.rb +29 -25
- data/lib/og/mixin/timestamped.rb +4 -2
- data/lib/og/mixin/tree.rb +0 -1
- data/lib/og/relation.rb +161 -116
- data/lib/og/relation/all.rb +6 -0
- data/lib/og/relation/belongs_to.rb +4 -1
- data/lib/og/relation/has_many.rb +6 -5
- data/lib/og/relation/joins_many.rb +13 -12
- data/lib/og/relation/refers_to.rb +3 -3
- data/lib/og/store.rb +9 -9
- data/lib/og/store/{filesys.rb → alpha/filesys.rb} +0 -0
- data/lib/og/store/alpha/kirby.rb +284 -0
- data/lib/og/store/{memory.rb → alpha/memory.rb} +2 -0
- data/lib/og/store/{sqlserver.rb → alpha/sqlserver.rb} +6 -6
- data/lib/og/store/kirby.rb +145 -162
- data/lib/og/store/mysql.rb +58 -27
- data/lib/og/store/psql.rb +15 -13
- data/lib/og/store/sql.rb +136 -135
- data/lib/og/store/sqlite.rb +13 -12
- data/lib/og/validation.rb +2 -2
- data/lib/vendor/kbserver.rb +20 -0
- data/lib/vendor/kirbybase.rb +2790 -1601
- data/test/og/CONFIG.rb +79 -0
- data/test/og/mixin/tc_hierarchical.rb +1 -1
- data/test/og/mixin/tc_optimistic_locking.rb +1 -3
- data/test/og/mixin/tc_orderable.rb +42 -1
- data/test/og/mixin/tc_taggable.rb +1 -1
- data/test/og/mixin/tc_timestamped.rb +1 -1
- data/test/og/store/tc_filesys.rb +1 -2
- data/test/og/tc_delete_all.rb +45 -0
- data/test/og/tc_inheritance.rb +10 -38
- data/test/og/tc_join.rb +2 -11
- data/test/og/tc_multiple.rb +3 -16
- data/test/og/tc_override.rb +3 -3
- data/test/og/tc_polymorphic.rb +3 -13
- data/test/og/tc_relation.rb +8 -6
- data/test/og/tc_reverse.rb +2 -11
- data/test/og/tc_select.rb +2 -15
- data/test/og/tc_store.rb +4 -63
- data/test/og/tc_types.rb +1 -2
- metadata +80 -77
data/lib/og/store/sqlite.rb
CHANGED
@@ -64,11 +64,12 @@ class SqliteStore < SqlStore
|
|
64
64
|
end
|
65
65
|
|
66
66
|
def enchant(klass, manager)
|
67
|
-
if klass.
|
68
|
-
unless klass.properties.
|
67
|
+
if klass.ann.this.primary_key.symbol == :oid
|
68
|
+
unless klass.properties.include? :oid
|
69
69
|
klass.property :oid, Fixnum, :sql => 'integer PRIMARY KEY'
|
70
70
|
end
|
71
71
|
end
|
72
|
+
|
72
73
|
super
|
73
74
|
end
|
74
75
|
|
@@ -117,17 +118,17 @@ private
|
|
117
118
|
|
118
119
|
sql = "CREATE TABLE #{klass::OGTABLE} (#{fields.join(', ')}"
|
119
120
|
|
120
|
-
# Create table
|
121
|
+
# Create table constraints.
|
121
122
|
|
122
|
-
if
|
123
|
-
sql << ", #{
|
123
|
+
if constraints = klass.ann.this[:sql_constraint]
|
124
|
+
sql << ", #{constraints.join(', ')}"
|
124
125
|
end
|
125
126
|
|
126
127
|
sql << ");"
|
127
128
|
|
128
129
|
# Create indices.
|
129
130
|
|
130
|
-
if
|
131
|
+
if indices = klass.ann.this[:index]
|
131
132
|
for data in indices
|
132
133
|
idx, options = *data
|
133
134
|
idx = idx.to_s
|
@@ -153,7 +154,7 @@ private
|
|
153
154
|
# Create join tables if needed. Join tables are used in
|
154
155
|
# 'many_to_many' relations.
|
155
156
|
|
156
|
-
if
|
157
|
+
if join_tables = klass.ann.this[:join_tables]
|
157
158
|
for info in join_tables
|
158
159
|
begin
|
159
160
|
create_join_table_sql(info).each do |sql|
|
@@ -172,7 +173,7 @@ private
|
|
172
173
|
end
|
173
174
|
end
|
174
175
|
|
175
|
-
def create_field_map(klass)
|
176
|
+
def create_field_map(klass)
|
176
177
|
res = @conn.query "SELECT * FROM #{klass::OGTABLE} LIMIT 1"
|
177
178
|
map = {}
|
178
179
|
|
@@ -188,12 +189,12 @@ private
|
|
188
189
|
end
|
189
190
|
|
190
191
|
def eval_og_insert(klass)
|
191
|
-
pk = klass.primary_key.
|
192
|
-
props = klass.properties.dup
|
192
|
+
pk = klass.primary_key.symbol
|
193
|
+
props = klass.properties.values.dup
|
193
194
|
values = props.collect { |p| write_prop(p) }.join(',')
|
194
195
|
|
195
|
-
if klass.
|
196
|
-
props << Property.new(:ogtype, String)
|
196
|
+
if klass.schema_inheritance?
|
197
|
+
props << Property.new(:symbol => :ogtype, :klass => String)
|
197
198
|
values << ", '#{klass}'"
|
198
199
|
end
|
199
200
|
|
data/lib/og/validation.rb
CHANGED
@@ -44,7 +44,7 @@ module Validation
|
|
44
44
|
end;
|
45
45
|
}
|
46
46
|
|
47
|
-
|
47
|
+
validations! << [code, c[:on]]
|
48
48
|
end
|
49
49
|
end
|
50
50
|
|
@@ -65,7 +65,7 @@ module Validation
|
|
65
65
|
end;
|
66
66
|
}
|
67
67
|
|
68
|
-
|
68
|
+
validations! << [code, c[:on]]
|
69
69
|
end
|
70
70
|
end
|
71
71
|
alias_method :validate_associated, :validate_related
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# Multi-user server script for KirbyBase.
|
2
|
+
|
3
|
+
require 'kirbybase'
|
4
|
+
require 'drb'
|
5
|
+
require 'benchmark'
|
6
|
+
include Benchmark
|
7
|
+
|
8
|
+
host = ''
|
9
|
+
port = 44444
|
10
|
+
|
11
|
+
puts 'Initializing database server and indexes...'
|
12
|
+
|
13
|
+
# Create an instance of the database.
|
14
|
+
db = KirbyBase.new(:server)
|
15
|
+
|
16
|
+
DRb.start_service('druby://:44444', db)
|
17
|
+
|
18
|
+
puts 'Server ready to receive connections...'
|
19
|
+
|
20
|
+
DRb.thread.join
|
data/lib/vendor/kirbybase.rb
CHANGED
@@ -1,1601 +1,2790 @@
|
|
1
|
-
require 'date'
|
2
|
-
require 'drb'
|
3
|
-
require 'csv'
|
4
|
-
require 'fileutils'
|
5
|
-
|
6
|
-
|
7
|
-
#
|
8
|
-
#
|
9
|
-
#
|
10
|
-
#
|
11
|
-
#
|
12
|
-
#
|
13
|
-
#
|
14
|
-
#
|
15
|
-
#
|
16
|
-
#
|
17
|
-
#
|
18
|
-
#
|
19
|
-
#
|
20
|
-
#
|
21
|
-
#
|
22
|
-
#
|
23
|
-
#
|
24
|
-
#
|
25
|
-
#
|
26
|
-
#
|
27
|
-
#
|
28
|
-
#
|
29
|
-
#
|
30
|
-
#
|
31
|
-
# * Added the ability to
|
32
|
-
#
|
33
|
-
#
|
34
|
-
#
|
35
|
-
#
|
36
|
-
#
|
37
|
-
#
|
38
|
-
#
|
39
|
-
#
|
40
|
-
#
|
41
|
-
#
|
42
|
-
#
|
43
|
-
#
|
44
|
-
#
|
45
|
-
#
|
46
|
-
#
|
47
|
-
#
|
48
|
-
#
|
49
|
-
#
|
50
|
-
#
|
51
|
-
#
|
52
|
-
#
|
53
|
-
#
|
54
|
-
#
|
55
|
-
#
|
56
|
-
|
57
|
-
# KirbyBase
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
#
|
180
|
-
|
181
|
-
|
182
|
-
#
|
183
|
-
#
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
#
|
213
|
-
#
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
#
|
224
|
-
#
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
#
|
246
|
-
#
|
247
|
-
|
248
|
-
|
249
|
-
#
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
#
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
return
|
287
|
-
end
|
288
|
-
|
289
|
-
#-----------------------------------------------------------------------
|
290
|
-
#
|
291
|
-
#-----------------------------------------------------------------------
|
292
|
-
#++
|
293
|
-
#
|
294
|
-
#
|
295
|
-
#
|
296
|
-
#
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
#
|
354
|
-
#
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
#
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
|
407
|
-
|
408
|
-
|
409
|
-
|
410
|
-
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
422
|
-
#-----------------------------------------------------------------------
|
423
|
-
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
|
435
|
-
|
436
|
-
|
437
|
-
|
438
|
-
|
439
|
-
|
440
|
-
|
441
|
-
|
442
|
-
|
443
|
-
|
444
|
-
|
445
|
-
|
446
|
-
|
447
|
-
|
448
|
-
|
449
|
-
|
450
|
-
|
451
|
-
|
452
|
-
|
453
|
-
|
454
|
-
|
455
|
-
|
456
|
-
|
457
|
-
|
458
|
-
|
459
|
-
|
460
|
-
|
461
|
-
|
462
|
-
|
463
|
-
|
464
|
-
|
465
|
-
|
466
|
-
|
467
|
-
|
468
|
-
|
469
|
-
|
470
|
-
|
471
|
-
|
472
|
-
|
473
|
-
|
474
|
-
|
475
|
-
|
476
|
-
|
477
|
-
|
478
|
-
|
479
|
-
|
480
|
-
|
481
|
-
|
482
|
-
|
483
|
-
|
484
|
-
|
485
|
-
|
486
|
-
|
487
|
-
|
488
|
-
|
489
|
-
|
490
|
-
|
491
|
-
|
492
|
-
|
493
|
-
|
494
|
-
|
495
|
-
|
496
|
-
|
497
|
-
|
498
|
-
|
499
|
-
|
500
|
-
|
501
|
-
|
502
|
-
|
503
|
-
|
504
|
-
|
505
|
-
|
506
|
-
|
507
|
-
|
508
|
-
|
509
|
-
|
510
|
-
|
511
|
-
|
512
|
-
|
513
|
-
|
514
|
-
|
515
|
-
|
516
|
-
|
517
|
-
|
518
|
-
|
519
|
-
|
520
|
-
|
521
|
-
|
522
|
-
|
523
|
-
|
524
|
-
|
525
|
-
|
526
|
-
|
527
|
-
|
528
|
-
|
529
|
-
|
530
|
-
|
531
|
-
|
532
|
-
|
533
|
-
|
534
|
-
|
535
|
-
|
536
|
-
|
537
|
-
|
538
|
-
|
539
|
-
|
540
|
-
|
541
|
-
|
542
|
-
|
543
|
-
|
544
|
-
|
545
|
-
|
546
|
-
|
547
|
-
end
|
548
|
-
|
549
|
-
#-----------------------------------------------------------------------
|
550
|
-
#
|
551
|
-
#-----------------------------------------------------------------------
|
552
|
-
|
553
|
-
|
554
|
-
|
555
|
-
|
556
|
-
#-----------------------------------------------------------------------
|
557
|
-
#
|
558
|
-
|
559
|
-
|
560
|
-
|
561
|
-
|
562
|
-
|
563
|
-
|
564
|
-
|
565
|
-
|
566
|
-
|
567
|
-
|
568
|
-
#-----------------------------------------------------------------------
|
569
|
-
#
|
570
|
-
|
571
|
-
|
572
|
-
|
573
|
-
|
574
|
-
|
575
|
-
|
576
|
-
|
577
|
-
|
578
|
-
|
579
|
-
|
580
|
-
|
581
|
-
|
582
|
-
|
583
|
-
|
584
|
-
|
585
|
-
|
586
|
-
|
587
|
-
|
588
|
-
|
589
|
-
|
590
|
-
|
591
|
-
|
592
|
-
|
593
|
-
|
594
|
-
|
595
|
-
|
596
|
-
|
597
|
-
|
598
|
-
|
599
|
-
|
600
|
-
|
601
|
-
|
602
|
-
|
603
|
-
|
604
|
-
|
605
|
-
|
606
|
-
|
607
|
-
|
608
|
-
|
609
|
-
|
610
|
-
|
611
|
-
|
612
|
-
|
613
|
-
|
614
|
-
|
615
|
-
|
616
|
-
|
617
|
-
|
618
|
-
|
619
|
-
|
620
|
-
|
621
|
-
|
622
|
-
|
623
|
-
|
624
|
-
|
625
|
-
|
626
|
-
|
627
|
-
|
628
|
-
|
629
|
-
|
630
|
-
|
631
|
-
|
632
|
-
|
633
|
-
|
634
|
-
|
635
|
-
|
636
|
-
|
637
|
-
|
638
|
-
|
639
|
-
|
640
|
-
|
641
|
-
|
642
|
-
|
643
|
-
|
644
|
-
|
645
|
-
|
646
|
-
|
647
|
-
|
648
|
-
|
649
|
-
|
650
|
-
|
651
|
-
|
652
|
-
|
653
|
-
|
654
|
-
|
655
|
-
|
656
|
-
|
657
|
-
|
658
|
-
|
659
|
-
|
660
|
-
|
661
|
-
|
662
|
-
|
663
|
-
|
664
|
-
|
665
|
-
|
666
|
-
|
667
|
-
|
668
|
-
|
669
|
-
|
670
|
-
|
671
|
-
|
672
|
-
|
673
|
-
|
674
|
-
|
675
|
-
|
676
|
-
|
677
|
-
|
678
|
-
|
679
|
-
|
680
|
-
|
681
|
-
|
682
|
-
|
683
|
-
|
684
|
-
|
685
|
-
|
686
|
-
|
687
|
-
|
688
|
-
|
689
|
-
|
690
|
-
|
691
|
-
|
692
|
-
|
693
|
-
|
694
|
-
|
695
|
-
|
696
|
-
|
697
|
-
|
698
|
-
|
699
|
-
|
700
|
-
|
701
|
-
|
702
|
-
|
703
|
-
|
704
|
-
|
705
|
-
|
706
|
-
|
707
|
-
|
708
|
-
|
709
|
-
|
710
|
-
|
711
|
-
|
712
|
-
|
713
|
-
|
714
|
-
|
715
|
-
|
716
|
-
|
717
|
-
|
718
|
-
|
719
|
-
|
720
|
-
|
721
|
-
|
722
|
-
|
723
|
-
|
724
|
-
|
725
|
-
|
726
|
-
|
727
|
-
|
728
|
-
|
729
|
-
|
730
|
-
|
731
|
-
|
732
|
-
|
733
|
-
|
734
|
-
|
735
|
-
|
736
|
-
|
737
|
-
|
738
|
-
|
739
|
-
|
740
|
-
|
741
|
-
|
742
|
-
|
743
|
-
|
744
|
-
|
745
|
-
|
746
|
-
|
747
|
-
|
748
|
-
|
749
|
-
|
750
|
-
|
751
|
-
|
752
|
-
|
753
|
-
|
754
|
-
|
755
|
-
|
756
|
-
|
757
|
-
|
758
|
-
|
759
|
-
|
760
|
-
|
761
|
-
|
762
|
-
|
763
|
-
|
764
|
-
|
765
|
-
|
766
|
-
|
767
|
-
|
768
|
-
|
769
|
-
|
770
|
-
|
771
|
-
|
772
|
-
|
773
|
-
|
774
|
-
|
775
|
-
|
776
|
-
|
777
|
-
|
778
|
-
|
779
|
-
|
780
|
-
|
781
|
-
|
782
|
-
|
783
|
-
|
784
|
-
|
785
|
-
|
786
|
-
|
787
|
-
|
788
|
-
|
789
|
-
|
790
|
-
|
791
|
-
|
792
|
-
|
793
|
-
|
794
|
-
|
795
|
-
|
796
|
-
|
797
|
-
|
798
|
-
|
799
|
-
|
800
|
-
|
801
|
-
|
802
|
-
|
803
|
-
|
804
|
-
|
805
|
-
|
806
|
-
|
807
|
-
|
808
|
-
|
809
|
-
|
810
|
-
|
811
|
-
|
812
|
-
|
813
|
-
|
814
|
-
|
815
|
-
|
816
|
-
|
817
|
-
|
818
|
-
|
819
|
-
|
820
|
-
|
821
|
-
|
822
|
-
|
823
|
-
|
824
|
-
|
825
|
-
|
826
|
-
|
827
|
-
|
828
|
-
|
829
|
-
|
830
|
-
|
831
|
-
|
832
|
-
|
833
|
-
|
834
|
-
|
835
|
-
|
836
|
-
|
837
|
-
|
838
|
-
|
839
|
-
|
840
|
-
|
841
|
-
|
842
|
-
|
843
|
-
|
844
|
-
|
845
|
-
|
846
|
-
|
847
|
-
|
848
|
-
|
849
|
-
|
850
|
-
|
851
|
-
|
852
|
-
|
853
|
-
|
854
|
-
|
855
|
-
|
856
|
-
|
857
|
-
|
858
|
-
|
859
|
-
|
860
|
-
|
861
|
-
|
862
|
-
|
863
|
-
|
864
|
-
|
865
|
-
|
866
|
-
|
867
|
-
|
868
|
-
|
869
|
-
|
870
|
-
|
871
|
-
|
872
|
-
|
873
|
-
|
874
|
-
|
875
|
-
|
876
|
-
|
877
|
-
|
878
|
-
|
879
|
-
|
880
|
-
|
881
|
-
|
882
|
-
|
883
|
-
|
884
|
-
|
885
|
-
|
886
|
-
|
887
|
-
|
888
|
-
|
889
|
-
|
890
|
-
|
891
|
-
|
892
|
-
|
893
|
-
|
894
|
-
|
895
|
-
|
896
|
-
|
897
|
-
|
898
|
-
|
899
|
-
|
900
|
-
|
901
|
-
|
902
|
-
|
903
|
-
#
|
904
|
-
|
905
|
-
|
906
|
-
|
907
|
-
|
908
|
-
|
909
|
-
|
910
|
-
|
911
|
-
|
912
|
-
|
913
|
-
|
914
|
-
|
915
|
-
|
916
|
-
|
917
|
-
|
918
|
-
|
919
|
-
|
920
|
-
|
921
|
-
|
922
|
-
|
923
|
-
|
924
|
-
|
925
|
-
|
926
|
-
|
927
|
-
|
928
|
-
|
929
|
-
|
930
|
-
|
931
|
-
|
932
|
-
|
933
|
-
|
934
|
-
|
935
|
-
|
936
|
-
|
937
|
-
|
938
|
-
|
939
|
-
|
940
|
-
|
941
|
-
|
942
|
-
|
943
|
-
|
944
|
-
|
945
|
-
|
946
|
-
|
947
|
-
|
948
|
-
|
949
|
-
|
950
|
-
|
951
|
-
|
952
|
-
|
953
|
-
|
954
|
-
|
955
|
-
|
956
|
-
|
957
|
-
|
958
|
-
|
959
|
-
|
960
|
-
|
961
|
-
|
962
|
-
|
963
|
-
|
964
|
-
|
965
|
-
|
966
|
-
|
967
|
-
|
968
|
-
|
969
|
-
|
970
|
-
|
971
|
-
|
972
|
-
|
973
|
-
|
974
|
-
|
975
|
-
#
|
976
|
-
|
977
|
-
|
978
|
-
|
979
|
-
|
980
|
-
|
981
|
-
|
982
|
-
|
983
|
-
|
984
|
-
|
985
|
-
|
986
|
-
|
987
|
-
|
988
|
-
|
989
|
-
|
990
|
-
|
991
|
-
|
992
|
-
|
993
|
-
|
994
|
-
|
995
|
-
|
996
|
-
|
997
|
-
|
998
|
-
|
999
|
-
|
1000
|
-
|
1001
|
-
|
1002
|
-
|
1003
|
-
|
1004
|
-
|
1005
|
-
|
1006
|
-
|
1007
|
-
|
1008
|
-
|
1009
|
-
|
1010
|
-
|
1011
|
-
|
1012
|
-
|
1013
|
-
|
1014
|
-
|
1015
|
-
|
1016
|
-
|
1017
|
-
|
1018
|
-
|
1019
|
-
|
1020
|
-
|
1021
|
-
|
1022
|
-
|
1023
|
-
|
1024
|
-
|
1025
|
-
|
1026
|
-
|
1027
|
-
|
1028
|
-
#
|
1029
|
-
|
1030
|
-
def
|
1031
|
-
|
1032
|
-
|
1033
|
-
|
1034
|
-
|
1035
|
-
|
1036
|
-
|
1037
|
-
|
1038
|
-
|
1039
|
-
|
1040
|
-
|
1041
|
-
|
1042
|
-
|
1043
|
-
|
1044
|
-
|
1045
|
-
|
1046
|
-
|
1047
|
-
|
1048
|
-
|
1049
|
-
|
1050
|
-
|
1051
|
-
|
1052
|
-
|
1053
|
-
|
1054
|
-
|
1055
|
-
|
1056
|
-
|
1057
|
-
|
1058
|
-
|
1059
|
-
|
1060
|
-
|
1061
|
-
|
1062
|
-
|
1063
|
-
|
1064
|
-
|
1065
|
-
|
1066
|
-
|
1067
|
-
|
1068
|
-
|
1069
|
-
|
1070
|
-
|
1071
|
-
|
1072
|
-
|
1073
|
-
|
1074
|
-
|
1075
|
-
|
1076
|
-
|
1077
|
-
return
|
1078
|
-
end
|
1079
|
-
end
|
1080
|
-
|
1081
|
-
#-----------------------------------------------------------------------
|
1082
|
-
#
|
1083
|
-
#-----------------------------------------------------------------------
|
1084
|
-
|
1085
|
-
|
1086
|
-
|
1087
|
-
|
1088
|
-
|
1089
|
-
|
1090
|
-
|
1091
|
-
|
1092
|
-
|
1093
|
-
|
1094
|
-
|
1095
|
-
|
1096
|
-
|
1097
|
-
|
1098
|
-
|
1099
|
-
|
1100
|
-
|
1101
|
-
|
1102
|
-
|
1103
|
-
|
1104
|
-
|
1105
|
-
|
1106
|
-
|
1107
|
-
|
1108
|
-
#-----------------------------------------------------------------------
|
1109
|
-
|
1110
|
-
|
1111
|
-
|
1112
|
-
#
|
1113
|
-
|
1114
|
-
def
|
1115
|
-
|
1116
|
-
|
1117
|
-
|
1118
|
-
|
1119
|
-
|
1120
|
-
|
1121
|
-
|
1122
|
-
|
1123
|
-
#
|
1124
|
-
|
1125
|
-
|
1126
|
-
|
1127
|
-
|
1128
|
-
|
1129
|
-
|
1130
|
-
|
1131
|
-
|
1132
|
-
|
1133
|
-
|
1134
|
-
|
1135
|
-
#
|
1136
|
-
|
1137
|
-
|
1138
|
-
|
1139
|
-
|
1140
|
-
|
1141
|
-
|
1142
|
-
|
1143
|
-
(
|
1144
|
-
|
1145
|
-
|
1146
|
-
|
1147
|
-
|
1148
|
-
|
1149
|
-
|
1150
|
-
|
1151
|
-
|
1152
|
-
|
1153
|
-
|
1154
|
-
|
1155
|
-
|
1156
|
-
|
1157
|
-
|
1158
|
-
|
1159
|
-
|
1160
|
-
|
1161
|
-
|
1162
|
-
|
1163
|
-
|
1164
|
-
|
1165
|
-
|
1166
|
-
|
1167
|
-
|
1168
|
-
|
1169
|
-
|
1170
|
-
|
1171
|
-
|
1172
|
-
|
1173
|
-
|
1174
|
-
|
1175
|
-
|
1176
|
-
|
1177
|
-
|
1178
|
-
|
1179
|
-
|
1180
|
-
|
1181
|
-
|
1182
|
-
|
1183
|
-
|
1184
|
-
|
1185
|
-
|
1186
|
-
|
1187
|
-
|
1188
|
-
|
1189
|
-
|
1190
|
-
|
1191
|
-
|
1192
|
-
|
1193
|
-
|
1194
|
-
|
1195
|
-
#
|
1196
|
-
|
1197
|
-
def
|
1198
|
-
|
1199
|
-
|
1200
|
-
|
1201
|
-
|
1202
|
-
|
1203
|
-
|
1204
|
-
|
1205
|
-
|
1206
|
-
|
1207
|
-
|
1208
|
-
|
1209
|
-
|
1210
|
-
|
1211
|
-
|
1212
|
-
|
1213
|
-
|
1214
|
-
|
1215
|
-
|
1216
|
-
|
1217
|
-
|
1218
|
-
|
1219
|
-
|
1220
|
-
|
1221
|
-
|
1222
|
-
#-----------------------------------------------------------------------
|
1223
|
-
|
1224
|
-
|
1225
|
-
|
1226
|
-
|
1227
|
-
|
1228
|
-
|
1229
|
-
|
1230
|
-
|
1231
|
-
|
1232
|
-
|
1233
|
-
|
1234
|
-
|
1235
|
-
|
1236
|
-
|
1237
|
-
|
1238
|
-
|
1239
|
-
|
1240
|
-
|
1241
|
-
|
1242
|
-
|
1243
|
-
|
1244
|
-
|
1245
|
-
|
1246
|
-
|
1247
|
-
|
1248
|
-
|
1249
|
-
|
1250
|
-
|
1251
|
-
|
1252
|
-
|
1253
|
-
|
1254
|
-
|
1255
|
-
|
1256
|
-
|
1257
|
-
|
1258
|
-
|
1259
|
-
|
1260
|
-
|
1261
|
-
|
1262
|
-
|
1263
|
-
|
1264
|
-
|
1265
|
-
|
1266
|
-
|
1267
|
-
|
1268
|
-
|
1269
|
-
|
1270
|
-
|
1271
|
-
|
1272
|
-
|
1273
|
-
|
1274
|
-
|
1275
|
-
|
1276
|
-
|
1277
|
-
|
1278
|
-
|
1279
|
-
|
1280
|
-
|
1281
|
-
|
1282
|
-
|
1283
|
-
|
1284
|
-
|
1285
|
-
|
1286
|
-
|
1287
|
-
|
1288
|
-
|
1289
|
-
|
1290
|
-
|
1291
|
-
|
1292
|
-
|
1293
|
-
|
1294
|
-
|
1295
|
-
|
1296
|
-
|
1297
|
-
|
1298
|
-
|
1299
|
-
|
1300
|
-
|
1301
|
-
|
1302
|
-
|
1303
|
-
|
1304
|
-
|
1305
|
-
|
1306
|
-
|
1307
|
-
|
1308
|
-
|
1309
|
-
|
1310
|
-
|
1311
|
-
|
1312
|
-
|
1313
|
-
|
1314
|
-
|
1315
|
-
|
1316
|
-
|
1317
|
-
|
1318
|
-
|
1319
|
-
|
1320
|
-
|
1321
|
-
|
1322
|
-
|
1323
|
-
#
|
1324
|
-
|
1325
|
-
|
1326
|
-
|
1327
|
-
|
1328
|
-
|
1329
|
-
|
1330
|
-
|
1331
|
-
|
1332
|
-
|
1333
|
-
|
1334
|
-
|
1335
|
-
|
1336
|
-
|
1337
|
-
|
1338
|
-
|
1339
|
-
|
1340
|
-
|
1341
|
-
|
1342
|
-
|
1343
|
-
|
1344
|
-
|
1345
|
-
|
1346
|
-
|
1347
|
-
|
1348
|
-
|
1349
|
-
|
1350
|
-
|
1351
|
-
|
1352
|
-
|
1353
|
-
|
1354
|
-
|
1355
|
-
|
1356
|
-
|
1357
|
-
|
1358
|
-
|
1359
|
-
|
1360
|
-
|
1361
|
-
|
1362
|
-
|
1363
|
-
|
1364
|
-
|
1365
|
-
|
1366
|
-
|
1367
|
-
|
1368
|
-
|
1369
|
-
|
1370
|
-
|
1371
|
-
|
1372
|
-
|
1373
|
-
|
1374
|
-
|
1375
|
-
|
1376
|
-
|
1377
|
-
|
1378
|
-
|
1379
|
-
|
1380
|
-
|
1381
|
-
|
1382
|
-
|
1383
|
-
|
1384
|
-
|
1385
|
-
|
1386
|
-
|
1387
|
-
|
1388
|
-
|
1389
|
-
|
1390
|
-
|
1391
|
-
|
1392
|
-
|
1393
|
-
|
1394
|
-
|
1395
|
-
|
1396
|
-
|
1397
|
-
|
1398
|
-
|
1399
|
-
|
1400
|
-
|
1401
|
-
|
1402
|
-
|
1403
|
-
|
1404
|
-
|
1405
|
-
|
1406
|
-
|
1407
|
-
|
1408
|
-
|
1409
|
-
|
1410
|
-
#
|
1411
|
-
|
1412
|
-
|
1413
|
-
|
1414
|
-
|
1415
|
-
|
1416
|
-
|
1417
|
-
|
1418
|
-
|
1419
|
-
|
1420
|
-
|
1421
|
-
|
1422
|
-
|
1423
|
-
|
1424
|
-
|
1425
|
-
|
1426
|
-
|
1427
|
-
|
1428
|
-
|
1429
|
-
|
1430
|
-
#
|
1431
|
-
|
1432
|
-
|
1433
|
-
|
1434
|
-
#
|
1435
|
-
|
1436
|
-
|
1437
|
-
|
1438
|
-
|
1439
|
-
|
1440
|
-
|
1441
|
-
|
1442
|
-
|
1443
|
-
|
1444
|
-
#
|
1445
|
-
|
1446
|
-
|
1447
|
-
|
1448
|
-
|
1449
|
-
|
1450
|
-
|
1451
|
-
|
1452
|
-
|
1453
|
-
|
1454
|
-
|
1455
|
-
|
1456
|
-
|
1457
|
-
|
1458
|
-
|
1459
|
-
|
1460
|
-
|
1461
|
-
|
1462
|
-
|
1463
|
-
|
1464
|
-
|
1465
|
-
#
|
1466
|
-
|
1467
|
-
|
1468
|
-
|
1469
|
-
|
1470
|
-
|
1471
|
-
|
1472
|
-
|
1473
|
-
|
1474
|
-
|
1475
|
-
|
1476
|
-
|
1477
|
-
|
1478
|
-
|
1479
|
-
|
1480
|
-
#-----------------------------------------------------------------------
|
1481
|
-
|
1482
|
-
|
1483
|
-
#
|
1484
|
-
def
|
1485
|
-
|
1486
|
-
|
1487
|
-
|
1488
|
-
|
1489
|
-
|
1490
|
-
|
1491
|
-
|
1492
|
-
|
1493
|
-
|
1494
|
-
|
1495
|
-
|
1496
|
-
|
1497
|
-
|
1498
|
-
|
1499
|
-
|
1500
|
-
|
1501
|
-
|
1502
|
-
|
1503
|
-
|
1504
|
-
|
1505
|
-
|
1506
|
-
|
1507
|
-
|
1508
|
-
|
1509
|
-
|
1510
|
-
|
1511
|
-
|
1512
|
-
|
1513
|
-
|
1514
|
-
|
1515
|
-
|
1516
|
-
|
1517
|
-
|
1518
|
-
|
1519
|
-
|
1520
|
-
|
1521
|
-
|
1522
|
-
|
1523
|
-
|
1524
|
-
|
1525
|
-
#
|
1526
|
-
|
1527
|
-
#
|
1528
|
-
|
1529
|
-
|
1530
|
-
|
1531
|
-
|
1532
|
-
|
1533
|
-
|
1534
|
-
|
1535
|
-
|
1536
|
-
|
1537
|
-
|
1538
|
-
|
1539
|
-
|
1540
|
-
|
1541
|
-
|
1542
|
-
|
1543
|
-
|
1544
|
-
|
1545
|
-
|
1546
|
-
|
1547
|
-
|
1548
|
-
|
1549
|
-
|
1550
|
-
|
1551
|
-
|
1552
|
-
|
1553
|
-
|
1554
|
-
|
1555
|
-
|
1556
|
-
|
1557
|
-
|
1558
|
-
|
1559
|
-
|
1560
|
-
|
1561
|
-
|
1562
|
-
|
1563
|
-
|
1564
|
-
|
1565
|
-
|
1566
|
-
|
1567
|
-
|
1568
|
-
|
1569
|
-
|
1570
|
-
|
1571
|
-
|
1572
|
-
|
1573
|
-
|
1574
|
-
|
1575
|
-
|
1576
|
-
|
1577
|
-
|
1578
|
-
|
1579
|
-
|
1580
|
-
|
1581
|
-
|
1582
|
-
|
1583
|
-
|
1584
|
-
|
1585
|
-
|
1586
|
-
|
1587
|
-
|
1588
|
-
|
1589
|
-
|
1590
|
-
|
1591
|
-
|
1592
|
-
|
1593
|
-
|
1594
|
-
|
1595
|
-
|
1596
|
-
|
1597
|
-
|
1598
|
-
|
1599
|
-
|
1600
|
-
|
1601
|
-
|
1
|
+
require 'date'
|
2
|
+
require 'drb'
|
3
|
+
require 'csv'
|
4
|
+
require 'fileutils'
|
5
|
+
require 'yaml'
|
6
|
+
|
7
|
+
#
|
8
|
+
# :main:KirbyBase
|
9
|
+
# :title:KirbyBase Class Documentation
|
10
|
+
# KirbyBase is a class that allows you to create and manipulate simple,
|
11
|
+
# plain-text databases. You can use it in either a single-user or
|
12
|
+
# client-server mode. You can select records for retrieval/updating using
|
13
|
+
# code blocks.
|
14
|
+
#
|
15
|
+
# Author:: Jamey Cribbs (mailto:jcribbs@twmi.rr.com)
|
16
|
+
# Homepage:: http://www.netpromi.com/kirbybase.html
|
17
|
+
# Copyright:: Copyright (c) 2005 NetPro Technologies, LLC
|
18
|
+
# License:: Distributes under the same terms as Ruby
|
19
|
+
# History:
|
20
|
+
# 2005-03-28:: Version 2.0
|
21
|
+
# * This is a completely new version. The interface has changed
|
22
|
+
# dramatically.
|
23
|
+
# 2005-04-11:: Version 2.1
|
24
|
+
# * Changed the interface to KirbyBase#new and KirbyBase#create_table. You
|
25
|
+
# now specify arguments via a code block or as part of the argument list.
|
26
|
+
# * Added the ability to specify a class at table creation time.
|
27
|
+
# Thereafter, whenever you do a #select, the result set will be an array
|
28
|
+
# of instances of that class, instead of instances of Struct. You can
|
29
|
+
# also use instances of this class as the argument to KBTable#insert,
|
30
|
+
# KBTable#update, and KBTable#set.
|
31
|
+
# * Added the ability to encrypt a table so that it is no longer stored as
|
32
|
+
# a plain-text file.
|
33
|
+
# * Added the ability to explicity specify that you want a result set to be
|
34
|
+
# sorted in ascending order.
|
35
|
+
# * Added the ability to import a csv file into an existing table.
|
36
|
+
# * Added the ability to select a record as if the table were a Hash with
|
37
|
+
# it's key being the recno field.
|
38
|
+
# * Added the ability to update a record as if the table were a Hash with
|
39
|
+
# it's key being the recno field.
|
40
|
+
# 2005-05-02:: Version 2.2
|
41
|
+
# * By far the biggest change in this version is that I have completely
|
42
|
+
# redesigned the internal structure of the database code. Because the
|
43
|
+
# KirbyBase and KBTable classes were too tightly coupled, I have created
|
44
|
+
# a KBEngine class and moved all low-level I/O logic and locking logic
|
45
|
+
# to this class. This allowed me to restructure the KirbyBase class to
|
46
|
+
# remove all of the methods that should have been private, but couldn't be
|
47
|
+
# because of the coupling to KBTable. In addition, it has allowed me to
|
48
|
+
# take all of the low-level code that should not have been in the KBTable
|
49
|
+
# class and put it where it belongs, as part of the underlying engine. I
|
50
|
+
# feel that the design of KirbyBase is much cleaner now. No changes were
|
51
|
+
# made to the class interfaces, so you should not have to change any of
|
52
|
+
# your code.
|
53
|
+
# * Changed str_to_date and str_to_datetime to use Date#parse method.
|
54
|
+
# * Changed #pack method so that it no longer reads the whole file into
|
55
|
+
# memory while packing it.
|
56
|
+
# * Changed code so that special character sequences like &linefeed; can be
|
57
|
+
# part of input data and KirbyBase will not interpret it as special
|
58
|
+
# characters.
|
59
|
+
# 2005-08-09:: Version 2.2.1
|
60
|
+
# * Fixed a bug in with_write_lock.
|
61
|
+
# * Fixed a bug that occurred if @record_class was a nested class.
|
62
|
+
# 2005-09-08:: Version 2.3 Beta 1
|
63
|
+
# * Added ability to specify one-to-one links between tables.
|
64
|
+
# * Added ability to specify one-to-many links between tables.
|
65
|
+
# * Added ability to specify calculated fields in tables.
|
66
|
+
# * Added Memo and Blob field types.
|
67
|
+
# * Added indexing to speed up queries.
|
68
|
+
# 2005-10-03:: Version 2.3 Beta 2
|
69
|
+
# * New column type: :YAML. Many thanks to Logan Capaldo for this idea!
|
70
|
+
# * Two new methods: #add_table_column and #drop_table_column.
|
71
|
+
# * I have refined the select code so that, when you are doing a one-to-one
|
72
|
+
# or one-to-many select, if an appropriate index exists for the child
|
73
|
+
# table, KirbyBase automatically uses it.
|
74
|
+
# * I have changed the designation for a one-to-one link from Link-> to
|
75
|
+
# Lookup-> after googling helped me see that this is a more correct term
|
76
|
+
# for what I am trying to convey with this link type.
|
77
|
+
# 2005-10-10:: Version 2.3 Production
|
78
|
+
# * Added the ability to designate a table field as the "key" field, for
|
79
|
+
# Lookup purposes. This simply makes it easier to define Lookup fields.
|
80
|
+
# * This led me to finally give in and add "the Hal Fulton Feature" as I am
|
81
|
+
# forever going to designate it. You can now specify a Lookup field
|
82
|
+
# simply by specifying it's field type as a table, for example:
|
83
|
+
# :manager, :person (where :manager is the field name, and :person is the
|
84
|
+
# name of a table). See the docs for the specifics or ask Hal. :)
|
85
|
+
#
|
86
|
+
#---------------------------------------------------------------------------
|
87
|
+
# KirbyBase
|
88
|
+
#---------------------------------------------------------------------------
|
89
|
+
class KirbyBase
|
90
|
+
include DRb::DRbUndumped
|
91
|
+
|
92
|
+
attr_reader :engine
|
93
|
+
|
94
|
+
attr_accessor(:connect_type, :host, :port, :path, :ext)
|
95
|
+
|
96
|
+
#-----------------------------------------------------------------------
|
97
|
+
# initialize
|
98
|
+
#-----------------------------------------------------------------------
|
99
|
+
#++
|
100
|
+
# Create a new database instance.
|
101
|
+
#
|
102
|
+
# *connect_type*:: Symbol (:local, :client, :server) specifying role to
|
103
|
+
# play.
|
104
|
+
# *host*:: String containing IP address or DNS name of server hosting
|
105
|
+
# database. (Only valid if connect_type is :client.)
|
106
|
+
# *port*:: Integer specifying port database server is listening on.
|
107
|
+
# (Only valid if connect_type is :client.)
|
108
|
+
# *path*:: String specifying path to location of database tables.
|
109
|
+
# *ext*:: String specifying extension of table files.
|
110
|
+
#
|
111
|
+
def initialize(connect_type=:local, host=nil, port=nil, path='./',
|
112
|
+
ext='.tbl')
|
113
|
+
@connect_type = connect_type
|
114
|
+
@host = host
|
115
|
+
@port = port
|
116
|
+
@path = path
|
117
|
+
@ext = ext
|
118
|
+
|
119
|
+
# See if user specified any method arguments via a code block.
|
120
|
+
yield self if block_given?
|
121
|
+
|
122
|
+
# After the yield, make sure the user doesn't change any of these
|
123
|
+
# instance variables.
|
124
|
+
class << self
|
125
|
+
private(:connect_type=, :host=, :path=, :ext=)
|
126
|
+
end
|
127
|
+
|
128
|
+
# Did user supply full and correct arguments to method?
|
129
|
+
raise ArgumentError, 'Invalid connection type specified' unless (
|
130
|
+
[:local, :client, :server].include?(@connect_type))
|
131
|
+
raise "Must specify hostname or IP address!" if \
|
132
|
+
@connect_type == :client and @host.nil?
|
133
|
+
raise "Must specify port number!" if @connect_type == :client and \
|
134
|
+
@port.nil?
|
135
|
+
raise "Invalid path!" if @path.nil?
|
136
|
+
raise "Invalid extension!" if @ext.nil?
|
137
|
+
|
138
|
+
@table_hash = {}
|
139
|
+
|
140
|
+
# If running as a client, start druby and connect to server.
|
141
|
+
if client?
|
142
|
+
DRb.start_service()
|
143
|
+
@server = DRbObject.new(nil, 'druby://%s:%d' % [@host, @port])
|
144
|
+
@engine = @server.engine
|
145
|
+
@path = @server.path
|
146
|
+
@ext = @server.ext
|
147
|
+
else
|
148
|
+
@engine = KBEngine.create_called_from_database_instance(self)
|
149
|
+
end
|
150
|
+
|
151
|
+
# The reason why I create all the table instances here is two
|
152
|
+
# reasons: (1) I want all of the tables ready to go when a user
|
153
|
+
# does a #get_table, so they don't have to wait for the instance
|
154
|
+
# to be created, and (2) I want all of the table indexes to get
|
155
|
+
# created at the beginning during database initialization so that
|
156
|
+
# they are ready for the user to use. Since index creation
|
157
|
+
# happens when the table instance is first created, I go ahead and
|
158
|
+
# create table instances right off the bat.
|
159
|
+
#
|
160
|
+
# Also, I use to only execute the code below if this was either a
|
161
|
+
# single-user instance of KirbyBase or if client-server, I would
|
162
|
+
# only let the client-side KirbyBase instance create the table
|
163
|
+
# instances, since there was no need for the server-side KirbyBase
|
164
|
+
# instance to create table instances. But, since I want indexes
|
165
|
+
# created at db initialization and the server's db instance might
|
166
|
+
# be initialized long before any client's db is initialized, I now
|
167
|
+
# let the server create table instances also. This is strictly to
|
168
|
+
# get the indexes created, there is no other use for the table
|
169
|
+
# instances on the server side as they will never be used.
|
170
|
+
# Everything should and does go through the table instances created
|
171
|
+
# on the client-side.
|
172
|
+
@engine.tables.each do |tbl|
|
173
|
+
@table_hash[tbl] = KBTable.create_called_from_database_instance(
|
174
|
+
self, tbl, File.join(@path, tbl.to_s + @ext))
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
#-----------------------------------------------------------------------
|
179
|
+
# server?
|
180
|
+
#-----------------------------------------------------------------------
|
181
|
+
#++
|
182
|
+
# Is this running as a server?
|
183
|
+
#
|
184
|
+
def server?
|
185
|
+
@connect_type == :server
|
186
|
+
end
|
187
|
+
|
188
|
+
#-----------------------------------------------------------------------
|
189
|
+
# client?
|
190
|
+
#-----------------------------------------------------------------------
|
191
|
+
#++
|
192
|
+
# Is this running as a client?
|
193
|
+
#
|
194
|
+
def client?
|
195
|
+
@connect_type == :client
|
196
|
+
end
|
197
|
+
|
198
|
+
#-----------------------------------------------------------------------
|
199
|
+
# local?
|
200
|
+
#-----------------------------------------------------------------------
|
201
|
+
#++
|
202
|
+
# Is this running in single-user, embedded mode?
|
203
|
+
#
|
204
|
+
def local?
|
205
|
+
@connect_type == :local
|
206
|
+
end
|
207
|
+
|
208
|
+
#-----------------------------------------------------------------------
|
209
|
+
# tables
|
210
|
+
#-----------------------------------------------------------------------
|
211
|
+
#++
|
212
|
+
# Return an array containing the names of all tables in this database.
|
213
|
+
#
|
214
|
+
def tables
|
215
|
+
return @engine.tables
|
216
|
+
end
|
217
|
+
|
218
|
+
#-----------------------------------------------------------------------
|
219
|
+
# get_table
|
220
|
+
#-----------------------------------------------------------------------
|
221
|
+
#++
|
222
|
+
# Return a reference to the requested table.
|
223
|
+
# *name*:: Symbol of table name.
|
224
|
+
#
|
225
|
+
def get_table(name)
|
226
|
+
raise('Do not call this method from a server instance!') if server?
|
227
|
+
raise('Table not found!') unless table_exists?(name)
|
228
|
+
|
229
|
+
if @table_hash.has_key?(name)
|
230
|
+
return @table_hash[name]
|
231
|
+
else
|
232
|
+
@table_hash[name] = \
|
233
|
+
KBTable.create_called_from_database_instance(self,
|
234
|
+
name, File.join(@path, name.to_s + @ext))
|
235
|
+
return @table_hash[name]
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
#-----------------------------------------------------------------------
|
240
|
+
# create_table
|
241
|
+
#-----------------------------------------------------------------------
|
242
|
+
#++
|
243
|
+
# Create new table and return a reference to the new table.
|
244
|
+
# *name*:: Symbol of table name.
|
245
|
+
# *field_defs*:: List of field names (Symbols), field types (Symbols),
|
246
|
+
# field indexes, and field extras (Indexes, Lookups,
|
247
|
+
# Link_manys, Calculateds, etc.)
|
248
|
+
# *Block*:: Optional code block allowing you to set the following:
|
249
|
+
# *encrypt*:: true/false specifying whether table should be encrypted.
|
250
|
+
# *record_class*:: Class or String specifying the user create class that
|
251
|
+
# will be associated with table records.
|
252
|
+
#
|
253
|
+
def create_table(name=nil, *field_defs)
|
254
|
+
raise "Can't call #create_table from server!" if server?
|
255
|
+
|
256
|
+
t_struct = Struct.new(:name, :field_defs, :encrypt, :record_class)
|
257
|
+
t = t_struct.new
|
258
|
+
t.name = name
|
259
|
+
t.field_defs = field_defs
|
260
|
+
t.encrypt = false
|
261
|
+
t.record_class = 'Struct'
|
262
|
+
|
263
|
+
yield t if block_given?
|
264
|
+
|
265
|
+
raise "Name must be a symbol!" unless t.name.is_a?(Symbol)
|
266
|
+
raise "No table name specified!" if t.name.nil?
|
267
|
+
raise "No table field definitions specified!" if t.field_defs.nil?
|
268
|
+
|
269
|
+
@engine.new_table(t.name, t.field_defs, t.encrypt,
|
270
|
+
t.record_class.to_s)
|
271
|
+
|
272
|
+
return get_table(t.name)
|
273
|
+
end
|
274
|
+
|
275
|
+
#-----------------------------------------------------------------------
|
276
|
+
# drop_table
|
277
|
+
#-----------------------------------------------------------------------
|
278
|
+
#++
|
279
|
+
# Delete a table.
|
280
|
+
#
|
281
|
+
# *tablename*:: Symbol of table name.
|
282
|
+
#
|
283
|
+
def drop_table(tablename)
|
284
|
+
raise "Table does not exist!" unless table_exists?(tablename)
|
285
|
+
@table_hash.delete(tablename)
|
286
|
+
return @engine.delete_table(tablename)
|
287
|
+
end
|
288
|
+
|
289
|
+
#-----------------------------------------------------------------------
|
290
|
+
# table_exists?
|
291
|
+
#-----------------------------------------------------------------------
|
292
|
+
#++
|
293
|
+
# Return true if table exists.
|
294
|
+
#
|
295
|
+
# *tablename*:: Symbol of table name.
|
296
|
+
#
|
297
|
+
def table_exists?(tablename)
|
298
|
+
return @engine.table_exists?(tablename)
|
299
|
+
end
|
300
|
+
|
301
|
+
#-----------------------------------------------------------------------
|
302
|
+
# add_table_column
|
303
|
+
#-----------------------------------------------------------------------
|
304
|
+
#++
|
305
|
+
# Add a column to a table.
|
306
|
+
#
|
307
|
+
# Make sure you are executing this method while in single-user mode
|
308
|
+
# (i.e. not running in client/server mode). After you run it, it is
|
309
|
+
# probably a good idea to release your handle on the db and
|
310
|
+
# re-initialize KirbyBase, as this method changes the table structure.
|
311
|
+
#
|
312
|
+
# *tablename*:: Symbol of table name.
|
313
|
+
# *col_name*:: Symbol of column name to add.
|
314
|
+
# *col_type*:: Symbol (or Hash if includes field extras) of column type
|
315
|
+
# to add.
|
316
|
+
# *after*:: Symbol of column name that you want to add this column
|
317
|
+
# after.
|
318
|
+
#
|
319
|
+
def add_table_column(tablename, col_name, col_type, after=nil)
|
320
|
+
raise "Do not execute this method from the server!!!" if server?
|
321
|
+
|
322
|
+
raise "Invalid table name!" unless table_exists?(tablename)
|
323
|
+
|
324
|
+
raise "Invalid field name in 'after': #{after}" unless after.nil? \
|
325
|
+
or @table_hash[tablename].field_names.include?(after)
|
326
|
+
|
327
|
+
# Does this new column have field extras (i.e. Index, Lookup, etc.)
|
328
|
+
if col_type.is_a?(Hash)
|
329
|
+
temp_type = col_type[:DataType]
|
330
|
+
else
|
331
|
+
temp_type = col_type
|
332
|
+
end
|
333
|
+
|
334
|
+
raise 'Invalid field type: %s' % temp_type unless \
|
335
|
+
KBTable.valid_field_type?(temp_type)
|
336
|
+
|
337
|
+
@engine.add_column(@table_hash[tablename], col_name, col_type,
|
338
|
+
after)
|
339
|
+
|
340
|
+
# Need to reinitialize the table instance and associated indexes.
|
341
|
+
@engine.remove_recno_index(tablename)
|
342
|
+
@engine.remove_indexes(tablename)
|
343
|
+
@table_hash.delete(tablename)
|
344
|
+
@table_hash[tablename] = \
|
345
|
+
KBTable.create_called_from_database_instance(self, tablename,
|
346
|
+
File.join(@path, tablename.to_s + @ext))
|
347
|
+
end
|
348
|
+
|
349
|
+
#-----------------------------------------------------------------------
|
350
|
+
# drop_table_column
|
351
|
+
#-----------------------------------------------------------------------
|
352
|
+
#++
|
353
|
+
# Drop a column from a table.
|
354
|
+
#
|
355
|
+
# Make sure you are executing this method while in single-user mode
|
356
|
+
# (i.e. not running in client/server mode). After you run it, it is
|
357
|
+
# probably a good idea to release your handle on the db and
|
358
|
+
# re-initialize KirbyBase, as this method changes the table structure.
|
359
|
+
#
|
360
|
+
# *tablename*:: Symbol of table name.
|
361
|
+
# *col_name*:: Symbol of column name to add.
|
362
|
+
#
|
363
|
+
def drop_table_column(tablename, col_name)
|
364
|
+
raise "Do not execute this method from the server!!!" if server?
|
365
|
+
|
366
|
+
raise "Invalid table name!" unless table_exists?(tablename)
|
367
|
+
|
368
|
+
raise 'Invalid column name: ' % col_name unless \
|
369
|
+
@table_hash[tablename].field_names.include?(col_name)
|
370
|
+
|
371
|
+
raise "Cannot drop :recno column!" if col_name == :recno
|
372
|
+
|
373
|
+
@engine.drop_column(@table_hash[tablename], col_name)
|
374
|
+
|
375
|
+
# Need to reinitialize the table instance and associated indexes.
|
376
|
+
@engine.remove_recno_index(tablename)
|
377
|
+
@engine.remove_indexes(tablename)
|
378
|
+
@table_hash.delete(tablename)
|
379
|
+
@table_hash[tablename] = \
|
380
|
+
KBTable.create_called_from_database_instance(self, tablename,
|
381
|
+
File.join(@path, tablename.to_s + @ext))
|
382
|
+
end
|
383
|
+
end
|
384
|
+
|
385
|
+
#---------------------------------------------------------------------------
|
386
|
+
# KBEngine
|
387
|
+
#---------------------------------------------------------------------------
|
388
|
+
class KBEngine
|
389
|
+
include DRb::DRbUndumped
|
390
|
+
|
391
|
+
EN_STR = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' + \
|
392
|
+
'0123456789.+-,$:|&;_ '
|
393
|
+
EN_STR_LEN = EN_STR.length
|
394
|
+
EN_KEY1 = ")2VER8GE\"87-E\n" #*** DO NOT CHANGE ***
|
395
|
+
EN_KEY = EN_KEY1.unpack("u")[0]
|
396
|
+
EN_KEY_LEN = EN_KEY.length
|
397
|
+
|
398
|
+
# Make constructor private.
|
399
|
+
private_class_method :new
|
400
|
+
|
401
|
+
#-----------------------------------------------------------------------
|
402
|
+
# KBEngine.create_called_from_database_instance
|
403
|
+
#-----------------------------------------------------------------------
|
404
|
+
def KBEngine.create_called_from_database_instance(db)
|
405
|
+
return new(db)
|
406
|
+
end
|
407
|
+
|
408
|
+
#-----------------------------------------------------------------------
|
409
|
+
# initialize
|
410
|
+
#-----------------------------------------------------------------------
|
411
|
+
def initialize(db)
|
412
|
+
@db = db
|
413
|
+
@recno_indexes = {}
|
414
|
+
@indexes = {}
|
415
|
+
|
416
|
+
# This hash will hold the table locks if in client/server mode.
|
417
|
+
@mutex_hash = {} if @db.server?
|
418
|
+
end
|
419
|
+
|
420
|
+
#-----------------------------------------------------------------------
|
421
|
+
# init_recno_index
|
422
|
+
#-----------------------------------------------------------------------
|
423
|
+
def init_recno_index(table)
|
424
|
+
return if recno_index_exists?(table)
|
425
|
+
|
426
|
+
with_write_locked_table(table) do |fptr|
|
427
|
+
@recno_indexes[table.name] = KBRecnoIndex.new(table)
|
428
|
+
@recno_indexes[table.name].rebuild(fptr)
|
429
|
+
end
|
430
|
+
end
|
431
|
+
|
432
|
+
#-----------------------------------------------------------------------
|
433
|
+
# rebuild_recno_index
|
434
|
+
#-----------------------------------------------------------------------
|
435
|
+
def rebuild_recno_index(table)
|
436
|
+
with_write_locked_table(table) do |fptr|
|
437
|
+
@recno_indexes[table.name].rebuild(fptr)
|
438
|
+
end
|
439
|
+
end
|
440
|
+
|
441
|
+
#-----------------------------------------------------------------------
|
442
|
+
# remove_recno_index
|
443
|
+
#-----------------------------------------------------------------------
|
444
|
+
def remove_recno_index(tablename)
|
445
|
+
@recno_indexes.delete(tablename)
|
446
|
+
end
|
447
|
+
|
448
|
+
#-----------------------------------------------------------------------
|
449
|
+
# recno_index_exists?
|
450
|
+
#-----------------------------------------------------------------------
|
451
|
+
def recno_index_exists?(table)
|
452
|
+
@recno_indexes.include?(table.name)
|
453
|
+
end
|
454
|
+
|
455
|
+
#-----------------------------------------------------------------------
|
456
|
+
# init_index
|
457
|
+
#-----------------------------------------------------------------------
|
458
|
+
def init_index(table, index_fields)
|
459
|
+
return if index_exists?(table, index_fields)
|
460
|
+
|
461
|
+
with_write_locked_table(table) do |fptr|
|
462
|
+
@indexes["#{table.name}_#{index_fields.join('_')}".to_sym] = \
|
463
|
+
KBIndex.new(table, index_fields)
|
464
|
+
@indexes["#{table.name}_#{index_fields.join('_')}".to_sym
|
465
|
+
].rebuild(fptr)
|
466
|
+
end
|
467
|
+
end
|
468
|
+
|
469
|
+
#-----------------------------------------------------------------------
|
470
|
+
# rebuild_index
|
471
|
+
#-----------------------------------------------------------------------
|
472
|
+
def rebuild_index(table, index_fields)
|
473
|
+
with_write_locked_table(table) do |fptr|
|
474
|
+
@indexes["#{table.name}_#{index_fields.join('_')}".to_sym
|
475
|
+
].rebuild(fptr)
|
476
|
+
end
|
477
|
+
end
|
478
|
+
|
479
|
+
#-----------------------------------------------------------------------
|
480
|
+
# remove_indexes
|
481
|
+
#-----------------------------------------------------------------------
|
482
|
+
def remove_indexes(tablename)
|
483
|
+
re_table_name = Regexp.new(tablename.to_s)
|
484
|
+
@indexes.delete_if { |k,v| k.to_s =~ re_table_name }
|
485
|
+
end
|
486
|
+
|
487
|
+
#-----------------------------------------------------------------------
|
488
|
+
# add_to_indexes
|
489
|
+
#-----------------------------------------------------------------------
|
490
|
+
def add_to_indexes(table, rec, fpos)
|
491
|
+
@recno_indexes[table.name].add_index_rec(rec.first, fpos)
|
492
|
+
|
493
|
+
re_table_name = Regexp.new(table.name.to_s)
|
494
|
+
@indexes.each_pair do |key, index|
|
495
|
+
index.add_index_rec(rec) if key.to_s =~ re_table_name
|
496
|
+
end
|
497
|
+
end
|
498
|
+
|
499
|
+
#-----------------------------------------------------------------------
|
500
|
+
# delete_from_indexes
|
501
|
+
#-----------------------------------------------------------------------
|
502
|
+
def delete_from_indexes(table, rec, fpos)
|
503
|
+
@recno_indexes[table.name].delete_index_rec(rec.recno)
|
504
|
+
|
505
|
+
re_table_name = Regexp.new(table.name.to_s)
|
506
|
+
@indexes.each_pair do |key, index|
|
507
|
+
index.delete_index_rec(rec.recno) if key.to_s =~ re_table_name
|
508
|
+
end
|
509
|
+
end
|
510
|
+
|
511
|
+
#-----------------------------------------------------------------------
|
512
|
+
# index_exists?
|
513
|
+
#-----------------------------------------------------------------------
|
514
|
+
def index_exists?(table, index_fields)
|
515
|
+
@indexes.include?("#{table.name}_#{index_fields.join('_')}".to_sym)
|
516
|
+
end
|
517
|
+
|
518
|
+
#-----------------------------------------------------------------------
|
519
|
+
# update_recno_index
|
520
|
+
#-----------------------------------------------------------------------
|
521
|
+
def update_recno_index(table, recno, fpos)
|
522
|
+
@recno_indexes[table.name].update_index_rec(recno, fpos)
|
523
|
+
end
|
524
|
+
|
525
|
+
#-----------------------------------------------------------------------
|
526
|
+
# update_to_indexes
|
527
|
+
#-----------------------------------------------------------------------
|
528
|
+
def update_to_indexes(table, rec)
|
529
|
+
re_table_name = Regexp.new(table.name.to_s)
|
530
|
+
@indexes.each_pair do |key, index|
|
531
|
+
index.update_index_rec(rec) if key.to_s =~ re_table_name
|
532
|
+
end
|
533
|
+
end
|
534
|
+
|
535
|
+
#-----------------------------------------------------------------------
|
536
|
+
# get_index
|
537
|
+
#-----------------------------------------------------------------------
|
538
|
+
def get_index(table, index_name)
|
539
|
+
return @indexes["#{table.name}_#{index_name}".to_sym].get_idx
|
540
|
+
end
|
541
|
+
|
542
|
+
#-----------------------------------------------------------------------
|
543
|
+
# get_recno_index
|
544
|
+
#-----------------------------------------------------------------------
|
545
|
+
def get_recno_index(table)
|
546
|
+
return @recno_indexes[table.name].get_idx
|
547
|
+
end
|
548
|
+
|
549
|
+
#-----------------------------------------------------------------------
|
550
|
+
# table_exists?
|
551
|
+
#-----------------------------------------------------------------------
|
552
|
+
def table_exists?(tablename)
|
553
|
+
return File.exists?(File.join(@db.path, tablename.to_s + @db.ext))
|
554
|
+
end
|
555
|
+
|
556
|
+
#-----------------------------------------------------------------------
|
557
|
+
# tables
|
558
|
+
#-----------------------------------------------------------------------
|
559
|
+
def tables
|
560
|
+
list = []
|
561
|
+
Dir.foreach(@db.path) { |filename|
|
562
|
+
list << File.basename(filename, '.*').to_sym if \
|
563
|
+
File.extname(filename) == @db.ext
|
564
|
+
}
|
565
|
+
return list
|
566
|
+
end
|
567
|
+
|
568
|
+
#-----------------------------------------------------------------------
|
569
|
+
# build_header_field_string
|
570
|
+
#-----------------------------------------------------------------------
|
571
|
+
def build_header_field_string(field_name_def, field_type_def)
|
572
|
+
# Put field name at start of string definition.
|
573
|
+
temp_field_def = field_name_def.to_s + ':'
|
574
|
+
|
575
|
+
# if field type is a hash, that means that it is not just a
|
576
|
+
# simple field. Either is is being used in an index, it is a
|
577
|
+
# Lookup field, it is a Link_many field, or it is a Calculated
|
578
|
+
# field. This next bit of code is to piece together a proper
|
579
|
+
# string so that it can be written out to the header rec.
|
580
|
+
if field_type_def.is_a?(Hash)
|
581
|
+
raise 'Missing :DataType key in field type hash!' unless \
|
582
|
+
field_type_def.has_key?(:DataType)
|
583
|
+
|
584
|
+
temp_type = field_type_def[:DataType]
|
585
|
+
|
586
|
+
raise 'Invalid field type: %s' % temp_type unless \
|
587
|
+
KBTable.valid_field_type?(temp_type)
|
588
|
+
|
589
|
+
temp_field_def += field_type_def[:DataType].to_s
|
590
|
+
|
591
|
+
if field_type_def.has_key?(:Key)
|
592
|
+
temp_field_def += ':Key->true'
|
593
|
+
end
|
594
|
+
if field_type_def.has_key?(:Index)
|
595
|
+
raise 'Invalid field type for index: %s' % temp_type \
|
596
|
+
unless KBTable.valid_index_type?(temp_type)
|
597
|
+
|
598
|
+
temp_field_def += ':Index->' + field_type_def[:Index].to_s
|
599
|
+
end
|
600
|
+
if field_type_def.has_key?(:Lookup)
|
601
|
+
if field_type_def[:Lookup].is_a?(Array)
|
602
|
+
temp_field_def += \
|
603
|
+
':Lookup->%s.%s' % field_type_def[:Lookup]
|
604
|
+
else
|
605
|
+
tbl = @db.get_table(field_type_def[:Lookup])
|
606
|
+
temp_field_def += \
|
607
|
+
':Lookup->%s.%s' % [field_type_def[:Lookup],
|
608
|
+
tbl.lookup_key]
|
609
|
+
end
|
610
|
+
elsif field_type_def.has_key?(:Link_many)
|
611
|
+
raise 'Field type for Link_many field must be :ResultSet' \
|
612
|
+
unless temp_type == :ResultSet
|
613
|
+
temp_field_def += \
|
614
|
+
':Link_many->%s=%s.%s' % field_type_def[:Link_many]
|
615
|
+
elsif field_type_def.has_key?(:Calculated)
|
616
|
+
temp_field_def += \
|
617
|
+
':Calculated->%s' % field_type_def[:Calculated]
|
618
|
+
end
|
619
|
+
else
|
620
|
+
if KBTable.valid_field_type?(field_type_def)
|
621
|
+
temp_field_def += field_type_def.to_s
|
622
|
+
elsif @db.table_exists?(field_type_def)
|
623
|
+
tbl = @db.get_table(field_type_def)
|
624
|
+
temp_field_def += \
|
625
|
+
'%s:Lookup->%s.%s' % [tbl.field_types[
|
626
|
+
tbl.field_names.index(tbl.lookup_key)], field_type_def,
|
627
|
+
tbl.lookup_key]
|
628
|
+
else
|
629
|
+
raise 'Invalid field type: %s' % field_type_def
|
630
|
+
end
|
631
|
+
end
|
632
|
+
return temp_field_def
|
633
|
+
end
|
634
|
+
|
635
|
+
#-----------------------------------------------------------------------
|
636
|
+
# new_table
|
637
|
+
#-----------------------------------------------------------------------
|
638
|
+
#++
|
639
|
+
# Create physical file holding table. This table should not be directly
|
640
|
+
# called in your application, but only called by #create_table.
|
641
|
+
#
|
642
|
+
def new_table(name, field_defs, encrypt, record_class)
|
643
|
+
# Can't create a table that already exists!
|
644
|
+
raise "Table already exists!" if table_exists?(name)
|
645
|
+
|
646
|
+
raise 'Must have a field type for each field name' \
|
647
|
+
unless field_defs.size.remainder(2) == 0
|
648
|
+
temp_field_defs = []
|
649
|
+
(0...field_defs.size).step(2) do |x|
|
650
|
+
temp_field_defs << build_header_field_string(field_defs[x],
|
651
|
+
field_defs[x+1])
|
652
|
+
end
|
653
|
+
|
654
|
+
# Header rec consists of last record no. used, delete count, and
|
655
|
+
# all field names/types. Here, I am inserting the 'recno' field
|
656
|
+
# at the beginning of the fields.
|
657
|
+
header_rec = ['000000', '000000', record_class, 'recno:Integer',
|
658
|
+
temp_field_defs].join('|')
|
659
|
+
|
660
|
+
header_rec = 'Z' + encrypt_str(header_rec) if encrypt
|
661
|
+
|
662
|
+
begin
|
663
|
+
fptr = open(File.join(@db.path, name.to_s + @db.ext), 'w')
|
664
|
+
fptr.write(header_rec + "\n")
|
665
|
+
ensure
|
666
|
+
fptr.close
|
667
|
+
end
|
668
|
+
end
|
669
|
+
|
670
|
+
#-----------------------------------------------------------------------
|
671
|
+
# delete_table
|
672
|
+
#-----------------------------------------------------------------------
|
673
|
+
def delete_table(tablename)
|
674
|
+
with_write_lock(tablename) do
|
675
|
+
remove_indexes(tablename)
|
676
|
+
remove_recno_index(tablename)
|
677
|
+
File.delete(File.join(@db.path, tablename.to_s + @db.ext))
|
678
|
+
return true
|
679
|
+
end
|
680
|
+
end
|
681
|
+
|
682
|
+
#----------------------------------------------------------------------
|
683
|
+
# get_total_recs
|
684
|
+
#----------------------------------------------------------------------
|
685
|
+
def get_total_recs(table)
|
686
|
+
return get_recs(table).size
|
687
|
+
end
|
688
|
+
|
689
|
+
#-----------------------------------------------------------------------
|
690
|
+
# get_header_vars
|
691
|
+
#-----------------------------------------------------------------------
|
692
|
+
def get_header_vars(table)
|
693
|
+
with_table(table) do |fptr|
|
694
|
+
line = get_header_record(table, fptr)
|
695
|
+
|
696
|
+
last_rec_no, del_ctr, record_class, *flds = line.split('|')
|
697
|
+
field_names = flds.collect { |x| x.split(':')[0].to_sym }
|
698
|
+
field_types = flds.collect { |x| x.split(':')[1].to_sym }
|
699
|
+
field_indexes = [nil] * field_names.size
|
700
|
+
field_extras = [nil] * field_names.size
|
701
|
+
|
702
|
+
flds.each_with_index do |x,i|
|
703
|
+
field_extras[i] = {}
|
704
|
+
if x.split(':').size > 2
|
705
|
+
x.split(':')[2..-1].each do |y|
|
706
|
+
if y =~ /Index/
|
707
|
+
field_indexes[i] = y
|
708
|
+
else
|
709
|
+
field_extras[i][y.split('->')[0]] = \
|
710
|
+
y.split('->')[1]
|
711
|
+
end
|
712
|
+
end
|
713
|
+
end
|
714
|
+
end
|
715
|
+
return [table.encrypted?, last_rec_no.to_i, del_ctr.to_i,
|
716
|
+
record_class, field_names, field_types, field_indexes,
|
717
|
+
field_extras]
|
718
|
+
end
|
719
|
+
end
|
720
|
+
|
721
|
+
#-----------------------------------------------------------------------
|
722
|
+
# get_recs
|
723
|
+
#-----------------------------------------------------------------------
|
724
|
+
def get_recs(table)
|
725
|
+
encrypted = table.encrypted?
|
726
|
+
recs = []
|
727
|
+
|
728
|
+
with_table(table) do |fptr|
|
729
|
+
begin
|
730
|
+
# Skip header rec.
|
731
|
+
fptr.readline
|
732
|
+
|
733
|
+
# Loop through table.
|
734
|
+
while true
|
735
|
+
# Record current position in table. Then read first
|
736
|
+
# detail record.
|
737
|
+
fpos = fptr.tell
|
738
|
+
line = fptr.readline
|
739
|
+
line.chomp!
|
740
|
+
line_length = line.length
|
741
|
+
|
742
|
+
line = unencrypt_str(line) if encrypted
|
743
|
+
line.strip!
|
744
|
+
|
745
|
+
# If blank line (i.e. 'deleted'), skip it.
|
746
|
+
next if line == ''
|
747
|
+
|
748
|
+
# Split the line up into fields.
|
749
|
+
rec = line.split('|', -1)
|
750
|
+
rec << fpos << line_length
|
751
|
+
recs << rec
|
752
|
+
end
|
753
|
+
# Here's how we break out of the loop...
|
754
|
+
rescue EOFError
|
755
|
+
end
|
756
|
+
return recs
|
757
|
+
end
|
758
|
+
end
|
759
|
+
|
760
|
+
#-----------------------------------------------------------------------
|
761
|
+
# get_recs_by_recno
|
762
|
+
#-----------------------------------------------------------------------
|
763
|
+
def get_recs_by_recno(table, recnos)
|
764
|
+
encrypted = table.encrypted?
|
765
|
+
recs = []
|
766
|
+
recno_idx = get_recno_index(table)
|
767
|
+
|
768
|
+
with_table(table) do |fptr|
|
769
|
+
# Skip header rec.
|
770
|
+
fptr.readline
|
771
|
+
|
772
|
+
recnos.collect { |r| [recno_idx[r], r]
|
773
|
+
}.sort.each do |r|
|
774
|
+
fptr.seek(r[0])
|
775
|
+
line = fptr.readline
|
776
|
+
line.chomp!
|
777
|
+
line_length = line.length
|
778
|
+
|
779
|
+
line = unencrypt_str(line) if encrypted
|
780
|
+
line.strip!
|
781
|
+
|
782
|
+
# If blank line (i.e. 'deleted'), skip it.
|
783
|
+
next if line == ''
|
784
|
+
|
785
|
+
# Split the line up into fields.
|
786
|
+
rec = line.split('|', -1)
|
787
|
+
raise "Index Corrupt!" unless rec[0].to_i == r[1]
|
788
|
+
rec << r[0] << line_length
|
789
|
+
recs << rec
|
790
|
+
end
|
791
|
+
return recs
|
792
|
+
end
|
793
|
+
end
|
794
|
+
|
795
|
+
#-----------------------------------------------------------------------
|
796
|
+
# get_rec_by_recno
|
797
|
+
#-----------------------------------------------------------------------
|
798
|
+
def get_rec_by_recno(table, recno)
|
799
|
+
encrypted = table.encrypted?
|
800
|
+
recno_idx = get_recno_index(table)
|
801
|
+
|
802
|
+
return nil unless recno_idx.has_key?(recno)
|
803
|
+
|
804
|
+
with_table(table) do |fptr|
|
805
|
+
fptr.seek(recno_idx[recno])
|
806
|
+
line = fptr.readline
|
807
|
+
line.chomp!
|
808
|
+
line_length = line.length
|
809
|
+
|
810
|
+
line = unencrypt_str(line) if encrypted
|
811
|
+
line.strip!
|
812
|
+
|
813
|
+
return nil if line == ''
|
814
|
+
|
815
|
+
# Split the line up into fields.
|
816
|
+
rec = line.split('|', -1)
|
817
|
+
|
818
|
+
raise "Index Corrupt!" unless rec[0].to_i == recno
|
819
|
+
rec << recno_idx[recno] << line_length
|
820
|
+
return rec
|
821
|
+
end
|
822
|
+
end
|
823
|
+
|
824
|
+
#-----------------------------------------------------------------------
|
825
|
+
# insert_record
|
826
|
+
#-----------------------------------------------------------------------
|
827
|
+
def insert_record(table, rec)
|
828
|
+
with_write_locked_table(table) do |fptr|
|
829
|
+
# Auto-increment the record number field.
|
830
|
+
rec_no = incr_rec_no_ctr(table, fptr)
|
831
|
+
|
832
|
+
# Insert the newly created record number value at the beginning
|
833
|
+
# of the field values.
|
834
|
+
rec[0] = rec_no
|
835
|
+
|
836
|
+
fptr.seek(0, IO::SEEK_END)
|
837
|
+
fpos = fptr.tell
|
838
|
+
|
839
|
+
write_record(table, fptr, 'end', rec.join('|'))
|
840
|
+
|
841
|
+
add_to_indexes(table, rec, fpos)
|
842
|
+
|
843
|
+
# Return the record number of the newly created record.
|
844
|
+
return rec_no
|
845
|
+
end
|
846
|
+
end
|
847
|
+
|
848
|
+
#-----------------------------------------------------------------------
|
849
|
+
# update_records
|
850
|
+
#-----------------------------------------------------------------------
|
851
|
+
def update_records(table, recs)
|
852
|
+
with_write_locked_table(table) do |fptr|
|
853
|
+
recs.each do |rec|
|
854
|
+
line = rec[:rec].join('|')
|
855
|
+
|
856
|
+
# This doesn't actually 'delete' the line, it just
|
857
|
+
# makes it all spaces. That way, if the updated
|
858
|
+
# record is the same or less length than the old
|
859
|
+
# record, we can write the record back into the
|
860
|
+
# same spot. If the updated record is greater than
|
861
|
+
# the old record, we will leave the now spaced-out
|
862
|
+
# line and write the updated record at the end of
|
863
|
+
# the file.
|
864
|
+
write_record(table, fptr, rec[:fpos],
|
865
|
+
' ' * rec[:line_length])
|
866
|
+
if line.length > rec[:line_length]
|
867
|
+
fptr.seek(0, IO::SEEK_END)
|
868
|
+
new_fpos = fptr.tell
|
869
|
+
write_record(table, fptr, 'end', line)
|
870
|
+
incr_del_ctr(table, fptr)
|
871
|
+
|
872
|
+
update_recno_index(table, rec[:rec].first, new_fpos)
|
873
|
+
else
|
874
|
+
write_record(table, fptr, rec[:fpos], line)
|
875
|
+
end
|
876
|
+
update_to_indexes(table, rec[:rec])
|
877
|
+
end
|
878
|
+
# Return the number of records updated.
|
879
|
+
return recs.size
|
880
|
+
end
|
881
|
+
end
|
882
|
+
|
883
|
+
#-----------------------------------------------------------------------
|
884
|
+
# delete_records
|
885
|
+
#-----------------------------------------------------------------------
|
886
|
+
def delete_records(table, recs)
|
887
|
+
with_write_locked_table(table) do |fptr|
|
888
|
+
recs.each do |rec|
|
889
|
+
# Go to offset within the file where the record is and
|
890
|
+
# replace it with all spaces.
|
891
|
+
write_record(table, fptr, rec.fpos, ' ' * rec.line_length)
|
892
|
+
incr_del_ctr(table, fptr)
|
893
|
+
|
894
|
+
delete_from_indexes(table, rec, rec.fpos)
|
895
|
+
end
|
896
|
+
|
897
|
+
# Return the number of records deleted.
|
898
|
+
return recs.size
|
899
|
+
end
|
900
|
+
end
|
901
|
+
|
902
|
+
#-----------------------------------------------------------------------
|
903
|
+
# add_column
|
904
|
+
#-----------------------------------------------------------------------
|
905
|
+
def add_column(table, col_name, col_type, after)
|
906
|
+
temp_field_def = build_header_field_string(col_name, col_type)
|
907
|
+
|
908
|
+
if after.nil?
|
909
|
+
insert_after = -1
|
910
|
+
else
|
911
|
+
if table.field_names.last == after
|
912
|
+
insert_after = -1
|
913
|
+
else
|
914
|
+
insert_after = table.field_names.index(after)+1
|
915
|
+
end
|
916
|
+
end
|
917
|
+
|
918
|
+
with_write_lock(table.name) do
|
919
|
+
fptr = open(table.filename, 'r')
|
920
|
+
new_fptr = open(table.filename+'temp', 'w')
|
921
|
+
|
922
|
+
line = fptr.readline.chomp
|
923
|
+
|
924
|
+
if line[0..0] == 'Z'
|
925
|
+
header_rec = unencrypt_str(line[1..-1]).split('|')
|
926
|
+
if insert_after == -1
|
927
|
+
header_rec.insert(insert_after, temp_field_def)
|
928
|
+
else
|
929
|
+
header_rec.insert(insert_after+3, temp_field_def)
|
930
|
+
end
|
931
|
+
new_fptr.write('Z' + encrypt_str(header_rec.join('|')) +
|
932
|
+
"\n")
|
933
|
+
else
|
934
|
+
header_rec = line.split('|')
|
935
|
+
if insert_after == -1
|
936
|
+
header_rec.insert(insert_after, temp_field_def)
|
937
|
+
else
|
938
|
+
header_rec.insert(insert_after+3, temp_field_def)
|
939
|
+
end
|
940
|
+
new_fptr.write(header_rec.join('|') + "\n")
|
941
|
+
end
|
942
|
+
|
943
|
+
begin
|
944
|
+
while true
|
945
|
+
line = fptr.readline.chomp
|
946
|
+
|
947
|
+
if table.encrypted?
|
948
|
+
temp_line = unencrypt_str(line)
|
949
|
+
else
|
950
|
+
temp_line = line
|
951
|
+
end
|
952
|
+
|
953
|
+
rec = temp_line.split('|')
|
954
|
+
rec.insert(insert_after, '')
|
955
|
+
|
956
|
+
if table.encrypted?
|
957
|
+
new_fptr.write(encrypt_str(rec.join('|')) + "\n")
|
958
|
+
else
|
959
|
+
new_fptr.write(rec.join('|') + "\n")
|
960
|
+
end
|
961
|
+
end
|
962
|
+
# Here's how we break out of the loop...
|
963
|
+
rescue EOFError
|
964
|
+
end
|
965
|
+
|
966
|
+
# Close the table and release the write lock.
|
967
|
+
fptr.close
|
968
|
+
new_fptr.close
|
969
|
+
File.delete(table.filename)
|
970
|
+
FileUtils.mv(table.filename+'temp', table.filename)
|
971
|
+
end
|
972
|
+
end
|
973
|
+
|
974
|
+
#-----------------------------------------------------------------------
|
975
|
+
# drop_column
|
976
|
+
#-----------------------------------------------------------------------
|
977
|
+
def drop_column(table, col_name)
|
978
|
+
col_index = table.field_names.index(col_name)
|
979
|
+
with_write_lock(table.name) do
|
980
|
+
fptr = open(table.filename, 'r')
|
981
|
+
new_fptr = open(table.filename+'temp', 'w')
|
982
|
+
|
983
|
+
line = fptr.readline.chomp
|
984
|
+
|
985
|
+
if line[0..0] == 'Z'
|
986
|
+
header_rec = unencrypt_str(line[1..-1]).split('|')
|
987
|
+
header_rec.delete_at(col_index+3)
|
988
|
+
new_fptr.write('Z' + encrypt_str(header_rec.join('|')) +
|
989
|
+
"\n")
|
990
|
+
else
|
991
|
+
header_rec = line.split('|')
|
992
|
+
header_rec.delete_at(col_index+3)
|
993
|
+
new_fptr.write(header_rec.join('|') + "\n")
|
994
|
+
end
|
995
|
+
|
996
|
+
begin
|
997
|
+
while true
|
998
|
+
line = fptr.readline.chomp
|
999
|
+
|
1000
|
+
if table.encrypted?
|
1001
|
+
temp_line = unencrypt_str(line)
|
1002
|
+
else
|
1003
|
+
temp_line = line
|
1004
|
+
end
|
1005
|
+
|
1006
|
+
rec = temp_line.split('|')
|
1007
|
+
rec.delete_at(col_index)
|
1008
|
+
|
1009
|
+
if table.encrypted?
|
1010
|
+
new_fptr.write(encrypt_str(rec.join('|')) + "\n")
|
1011
|
+
else
|
1012
|
+
new_fptr.write(rec.join('|') + "\n")
|
1013
|
+
end
|
1014
|
+
end
|
1015
|
+
# Here's how we break out of the loop...
|
1016
|
+
rescue EOFError
|
1017
|
+
end
|
1018
|
+
|
1019
|
+
# Close the table and release the write lock.
|
1020
|
+
fptr.close
|
1021
|
+
new_fptr.close
|
1022
|
+
File.delete(table.filename)
|
1023
|
+
FileUtils.mv(table.filename+'temp', table.filename)
|
1024
|
+
end
|
1025
|
+
end
|
1026
|
+
|
1027
|
+
#-----------------------------------------------------------------------
|
1028
|
+
# pack_table
|
1029
|
+
#-----------------------------------------------------------------------
|
1030
|
+
def pack_table(table)
|
1031
|
+
with_write_lock(table.name) do
|
1032
|
+
fptr = open(table.filename, 'r')
|
1033
|
+
new_fptr = open(table.filename+'temp', 'w')
|
1034
|
+
|
1035
|
+
line = fptr.readline.chomp
|
1036
|
+
# Reset the delete counter in the header rec to 0.
|
1037
|
+
if line[0..0] == 'Z'
|
1038
|
+
header_rec = unencrypt_str(line[1..-1]).split('|')
|
1039
|
+
header_rec[1] = '000000'
|
1040
|
+
new_fptr.write('Z' + encrypt_str(header_rec.join('|')) +
|
1041
|
+
"\n")
|
1042
|
+
else
|
1043
|
+
header_rec = line.split('|')
|
1044
|
+
header_rec[1] = '000000'
|
1045
|
+
new_fptr.write(header_rec.join('|') + "\n")
|
1046
|
+
end
|
1047
|
+
|
1048
|
+
lines_deleted = 0
|
1049
|
+
|
1050
|
+
begin
|
1051
|
+
while true
|
1052
|
+
line = fptr.readline
|
1053
|
+
|
1054
|
+
if table.encrypted?
|
1055
|
+
temp_line = unencrypt_str(line)
|
1056
|
+
else
|
1057
|
+
temp_line = line
|
1058
|
+
end
|
1059
|
+
|
1060
|
+
if temp_line.strip == ''
|
1061
|
+
lines_deleted += 1
|
1062
|
+
else
|
1063
|
+
new_fptr.write(line)
|
1064
|
+
end
|
1065
|
+
end
|
1066
|
+
# Here's how we break out of the loop...
|
1067
|
+
rescue EOFError
|
1068
|
+
end
|
1069
|
+
|
1070
|
+
# Close the table and release the write lock.
|
1071
|
+
fptr.close
|
1072
|
+
new_fptr.close
|
1073
|
+
File.delete(table.filename)
|
1074
|
+
FileUtils.mv(table.filename+'temp', table.filename)
|
1075
|
+
|
1076
|
+
# Return the number of deleted records that were removed.
|
1077
|
+
return lines_deleted
|
1078
|
+
end
|
1079
|
+
end
|
1080
|
+
|
1081
|
+
#-----------------------------------------------------------------------
|
1082
|
+
# get_memo
|
1083
|
+
#-----------------------------------------------------------------------
|
1084
|
+
def get_memo(filepath)
|
1085
|
+
begin
|
1086
|
+
f = File.new(filepath)
|
1087
|
+
return f.readlines
|
1088
|
+
ensure
|
1089
|
+
f.close
|
1090
|
+
end
|
1091
|
+
end
|
1092
|
+
|
1093
|
+
#-----------------------------------------------------------------------
|
1094
|
+
# get_blob
|
1095
|
+
#-----------------------------------------------------------------------
|
1096
|
+
def get_blob(filepath)
|
1097
|
+
begin
|
1098
|
+
f = File.new(filepath, 'rb')
|
1099
|
+
return f.read
|
1100
|
+
ensure
|
1101
|
+
f.close
|
1102
|
+
end
|
1103
|
+
end
|
1104
|
+
|
1105
|
+
|
1106
|
+
#-----------------------------------------------------------------------
|
1107
|
+
# PRIVATE METHODS
|
1108
|
+
#-----------------------------------------------------------------------
|
1109
|
+
private
|
1110
|
+
|
1111
|
+
#-----------------------------------------------------------------------
|
1112
|
+
# with_table
|
1113
|
+
#-----------------------------------------------------------------------
|
1114
|
+
def with_table(table, access='r')
|
1115
|
+
begin
|
1116
|
+
yield fptr = open(table.filename, access)
|
1117
|
+
ensure
|
1118
|
+
fptr.close
|
1119
|
+
end
|
1120
|
+
end
|
1121
|
+
|
1122
|
+
#-----------------------------------------------------------------------
|
1123
|
+
# with_write_lock
|
1124
|
+
#-----------------------------------------------------------------------
|
1125
|
+
def with_write_lock(tablename)
|
1126
|
+
begin
|
1127
|
+
write_lock(tablename) if @db.server?
|
1128
|
+
yield
|
1129
|
+
ensure
|
1130
|
+
write_unlock(tablename) if @db.server?
|
1131
|
+
end
|
1132
|
+
end
|
1133
|
+
|
1134
|
+
#-----------------------------------------------------------------------
|
1135
|
+
# with_write_locked_table
|
1136
|
+
#-----------------------------------------------------------------------
|
1137
|
+
def with_write_locked_table(table, access='r+')
|
1138
|
+
begin
|
1139
|
+
write_lock(table.name) if @db.server?
|
1140
|
+
yield fptr = open(table.filename, access)
|
1141
|
+
ensure
|
1142
|
+
fptr.close
|
1143
|
+
write_unlock(table.name) if @db.server?
|
1144
|
+
end
|
1145
|
+
end
|
1146
|
+
|
1147
|
+
#-----------------------------------------------------------------------
|
1148
|
+
# write_lock
|
1149
|
+
#-----------------------------------------------------------------------
|
1150
|
+
def write_lock(tablename)
|
1151
|
+
# Unless an key already exists in the hash holding mutex records
|
1152
|
+
# for this table, create a write key for this table in the mutex
|
1153
|
+
# hash. Then, place a lock on that mutex.
|
1154
|
+
@mutex_hash[tablename] = Mutex.new unless (
|
1155
|
+
@mutex_hash.has_key?(tablename))
|
1156
|
+
@mutex_hash[tablename].lock
|
1157
|
+
|
1158
|
+
return true
|
1159
|
+
end
|
1160
|
+
|
1161
|
+
#----------------------------------------------------------------------
|
1162
|
+
# write_unlock
|
1163
|
+
#----------------------------------------------------------------------
|
1164
|
+
def write_unlock(tablename)
|
1165
|
+
# Unlock the write mutex for this table.
|
1166
|
+
@mutex_hash[tablename].unlock
|
1167
|
+
|
1168
|
+
return true
|
1169
|
+
end
|
1170
|
+
|
1171
|
+
#----------------------------------------------------------------------
|
1172
|
+
# write_record
|
1173
|
+
#----------------------------------------------------------------------
|
1174
|
+
def write_record(table, fptr, pos, record)
|
1175
|
+
if table.encrypted?
|
1176
|
+
temp_rec = encrypt_str(record)
|
1177
|
+
else
|
1178
|
+
temp_rec = record
|
1179
|
+
end
|
1180
|
+
|
1181
|
+
# If record is to be appended, go to end of table and write
|
1182
|
+
# record, adding newline character.
|
1183
|
+
if pos == 'end'
|
1184
|
+
fptr.seek(0, IO::SEEK_END)
|
1185
|
+
fptr.write(temp_rec + "\n")
|
1186
|
+
else
|
1187
|
+
# Otherwise, overwrite another record (that's why we don't
|
1188
|
+
# add the newline character).
|
1189
|
+
fptr.seek(pos)
|
1190
|
+
fptr.write(temp_rec)
|
1191
|
+
end
|
1192
|
+
end
|
1193
|
+
|
1194
|
+
#----------------------------------------------------------------------
|
1195
|
+
# write_header_record
|
1196
|
+
#----------------------------------------------------------------------
|
1197
|
+
def write_header_record(table, fptr, record)
|
1198
|
+
fptr.seek(0)
|
1199
|
+
|
1200
|
+
if table.encrypted?
|
1201
|
+
fptr.write('Z' + encrypt_str(record) + "\n")
|
1202
|
+
else
|
1203
|
+
fptr.write(record + "\n")
|
1204
|
+
end
|
1205
|
+
end
|
1206
|
+
|
1207
|
+
#----------------------------------------------------------------------
|
1208
|
+
# get_header_record
|
1209
|
+
#----------------------------------------------------------------------
|
1210
|
+
def get_header_record(table, fptr)
|
1211
|
+
fptr.seek(0)
|
1212
|
+
|
1213
|
+
if table.encrypted?
|
1214
|
+
return unencrypt_str(fptr.readline[1..-1].chomp)
|
1215
|
+
else
|
1216
|
+
return fptr.readline.chomp
|
1217
|
+
end
|
1218
|
+
end
|
1219
|
+
|
1220
|
+
#-----------------------------------------------------------------------
|
1221
|
+
# reset_rec_no_ctr
|
1222
|
+
#-----------------------------------------------------------------------
|
1223
|
+
def reset_rec_no_ctr(table, fptr)
|
1224
|
+
last_rec_no, rest_of_line = get_header_record(table, fptr).split(
|
1225
|
+
'|', 2)
|
1226
|
+
write_header_record(table, fptr, ['%06d' % 0, rest_of_line].join(
|
1227
|
+
'|'))
|
1228
|
+
return true
|
1229
|
+
end
|
1230
|
+
|
1231
|
+
#-----------------------------------------------------------------------
|
1232
|
+
# incr_rec_no_ctr
|
1233
|
+
#-----------------------------------------------------------------------
|
1234
|
+
def incr_rec_no_ctr(table, fptr)
|
1235
|
+
last_rec_no, rest_of_line = get_header_record(table, fptr).split(
|
1236
|
+
'|', 2)
|
1237
|
+
last_rec_no = last_rec_no.to_i + 1
|
1238
|
+
|
1239
|
+
write_header_record(table, fptr, ['%06d' % last_rec_no,
|
1240
|
+
rest_of_line].join('|'))
|
1241
|
+
|
1242
|
+
# Return the new recno.
|
1243
|
+
return last_rec_no
|
1244
|
+
end
|
1245
|
+
|
1246
|
+
#-----------------------------------------------------------------------
|
1247
|
+
# incr_del_ctr
|
1248
|
+
#-----------------------------------------------------------------------
|
1249
|
+
def incr_del_ctr(table, fptr)
|
1250
|
+
last_rec_no, del_ctr, rest_of_line = get_header_record(table,
|
1251
|
+
fptr).split('|', 3)
|
1252
|
+
del_ctr = del_ctr.to_i + 1
|
1253
|
+
|
1254
|
+
write_header_record(table, fptr, [last_rec_no, '%06d' % del_ctr,
|
1255
|
+
rest_of_line].join('|'))
|
1256
|
+
|
1257
|
+
return true
|
1258
|
+
end
|
1259
|
+
|
1260
|
+
#-----------------------------------------------------------------------
|
1261
|
+
# encrypt_str
|
1262
|
+
#-----------------------------------------------------------------------
|
1263
|
+
def encrypt_str(s)
|
1264
|
+
# Returns an encrypted string, using the Vignere Cipher.
|
1265
|
+
|
1266
|
+
new_str = ''
|
1267
|
+
i_key = -1
|
1268
|
+
s.each_byte do |c|
|
1269
|
+
if i_key < EN_KEY_LEN - 1
|
1270
|
+
i_key += 1
|
1271
|
+
else
|
1272
|
+
i_key = 0
|
1273
|
+
end
|
1274
|
+
|
1275
|
+
if EN_STR.index(c.chr).nil?
|
1276
|
+
new_str << c.chr
|
1277
|
+
next
|
1278
|
+
end
|
1279
|
+
|
1280
|
+
i_from_str = EN_STR.index(EN_KEY[i_key]) + EN_STR.index(c.chr)
|
1281
|
+
i_from_str = i_from_str - EN_STR_LEN if i_from_str >= EN_STR_LEN
|
1282
|
+
new_str << EN_STR[i_from_str]
|
1283
|
+
end
|
1284
|
+
return new_str
|
1285
|
+
end
|
1286
|
+
|
1287
|
+
#-----------------------------------------------------------------------
|
1288
|
+
# unencrypt_str
|
1289
|
+
#-----------------------------------------------------------------------
|
1290
|
+
def unencrypt_str(s)
|
1291
|
+
# Returns an unencrypted string, using the Vignere Cipher.
|
1292
|
+
|
1293
|
+
new_str = ''
|
1294
|
+
i_key = -1
|
1295
|
+
s.each_byte do |c|
|
1296
|
+
if i_key < EN_KEY_LEN - 1
|
1297
|
+
i_key += 1
|
1298
|
+
else
|
1299
|
+
i_key = 0
|
1300
|
+
end
|
1301
|
+
|
1302
|
+
if EN_STR.index(c.chr).nil?
|
1303
|
+
new_str << c.chr
|
1304
|
+
next
|
1305
|
+
end
|
1306
|
+
|
1307
|
+
i_from_str = EN_STR.index(c.chr) - EN_STR.index(EN_KEY[i_key])
|
1308
|
+
i_from_str = i_from_str + EN_STR_LEN if i_from_str < 0
|
1309
|
+
new_str << EN_STR[i_from_str]
|
1310
|
+
end
|
1311
|
+
return new_str
|
1312
|
+
end
|
1313
|
+
end
|
1314
|
+
|
1315
|
+
|
1316
|
+
#---------------------------------------------------------------------------
|
1317
|
+
# KBTypeConversionsMixin
|
1318
|
+
#---------------------------------------------------------------------------
|
1319
|
+
module KBTypeConversionsMixin
|
1320
|
+
UNENCODE_RE = /&(?:amp|linefeed|carriage_return|substitute|pipe);/
|
1321
|
+
|
1322
|
+
#-----------------------------------------------------------------------
|
1323
|
+
# convert_to
|
1324
|
+
#-----------------------------------------------------------------------
|
1325
|
+
def convert_to(data_type, s)
|
1326
|
+
return nil if s.empty? or s.nil?
|
1327
|
+
|
1328
|
+
case data_type
|
1329
|
+
when :String
|
1330
|
+
if s =~ UNENCODE_RE
|
1331
|
+
return s.gsub('&linefeed;', "\n").gsub('&carriage_return;',
|
1332
|
+
"\r").gsub('&substitute;', "\032").gsub('&pipe;', "|"
|
1333
|
+
).gsub('&', "&")
|
1334
|
+
else
|
1335
|
+
return s
|
1336
|
+
end
|
1337
|
+
when :Integer
|
1338
|
+
return s.to_i
|
1339
|
+
when :Float
|
1340
|
+
return s.to_f
|
1341
|
+
when :Boolean
|
1342
|
+
if ['false', 'False', nil, false].include?(s)
|
1343
|
+
return false
|
1344
|
+
else
|
1345
|
+
return true
|
1346
|
+
end
|
1347
|
+
when :Date
|
1348
|
+
return Date.parse(s)
|
1349
|
+
when :Time
|
1350
|
+
return Time.parse(s)
|
1351
|
+
when :DateTime
|
1352
|
+
return DateTime.parse(s)
|
1353
|
+
when :YAML
|
1354
|
+
# This code is here in case the YAML field is the last
|
1355
|
+
# field in the record. Because YAML normall defines a
|
1356
|
+
# nil value as "--- ", but KirbyBase strips trailing
|
1357
|
+
# spaces off the end of the record, so if this is the
|
1358
|
+
# last field in the record, KirbyBase will strip the
|
1359
|
+
# trailing space off and make it "---". When KirbyBase
|
1360
|
+
# attempts to convert this value back using to_yaml,
|
1361
|
+
# you get an exception.
|
1362
|
+
if s == "---"
|
1363
|
+
return nil
|
1364
|
+
elsif s =~ UNENCODE_RE
|
1365
|
+
y = s.gsub('&linefeed;', "\n").gsub('&carriage_return;',
|
1366
|
+
"\r").gsub('&substitute;', "\032").gsub('&pipe;', "|"
|
1367
|
+
).gsub('&', "&")
|
1368
|
+
return YAML.load(y)
|
1369
|
+
else
|
1370
|
+
return YAML.load(s)
|
1371
|
+
end
|
1372
|
+
when :Memo
|
1373
|
+
return KBMemo.new(@tbl.db, s)
|
1374
|
+
when :Blob
|
1375
|
+
return KBBlob.new(@tbl.db, s)
|
1376
|
+
else
|
1377
|
+
raise "Invalid field type: %s" % data_type
|
1378
|
+
end
|
1379
|
+
end
|
1380
|
+
end
|
1381
|
+
|
1382
|
+
|
1383
|
+
#---------------------------------------------------------------------------
|
1384
|
+
# KBTable
|
1385
|
+
#---------------------------------------------------------------------------
|
1386
|
+
class KBTable
|
1387
|
+
include DRb::DRbUndumped
|
1388
|
+
|
1389
|
+
# Make constructor private. KBTable instances should only be created
|
1390
|
+
# from KirbyBase#get_table.
|
1391
|
+
private_class_method :new
|
1392
|
+
|
1393
|
+
VALID_FIELD_TYPES = [:String, :Integer, :Float, :Boolean, :Date, :Time,
|
1394
|
+
:DateTime, :Memo, :ResultSet, :YAML]
|
1395
|
+
|
1396
|
+
VALID_INDEX_TYPES = [:String, :Integer, :Float, :Boolean, :Date, :Time,
|
1397
|
+
:DateTime]
|
1398
|
+
|
1399
|
+
# Regular expression used to determine if field needs to be
|
1400
|
+
# encoded.
|
1401
|
+
ENCODE_RE = /&|\n|\r|\032|\|/
|
1402
|
+
|
1403
|
+
attr_reader :filename, :name, :table_class, :db, :lookup_key
|
1404
|
+
|
1405
|
+
#-----------------------------------------------------------------------
|
1406
|
+
# KBTable.valid_field_type
|
1407
|
+
#-----------------------------------------------------------------------
|
1408
|
+
#++
|
1409
|
+
# Return true if valid field type.
|
1410
|
+
#
|
1411
|
+
# *field_type*:: Symbol specifying field type.
|
1412
|
+
#
|
1413
|
+
def KBTable.valid_field_type?(field_type)
|
1414
|
+
VALID_FIELD_TYPES.include?(field_type)
|
1415
|
+
end
|
1416
|
+
|
1417
|
+
#-----------------------------------------------------------------------
|
1418
|
+
# KBTable.valid_index_type
|
1419
|
+
#-----------------------------------------------------------------------
|
1420
|
+
#++
|
1421
|
+
# Return true if valid index type.
|
1422
|
+
#
|
1423
|
+
# *field_type*:: Symbol specifying field type.
|
1424
|
+
#
|
1425
|
+
def KBTable.valid_index_type?(field_type)
|
1426
|
+
VALID_INDEX_TYPES.include?(field_type)
|
1427
|
+
end
|
1428
|
+
|
1429
|
+
#-----------------------------------------------------------------------
|
1430
|
+
# create_called_from_database_instance
|
1431
|
+
#-----------------------------------------------------------------------
|
1432
|
+
#++
|
1433
|
+
# Return a new instance of KBTable. Should never be called directly by
|
1434
|
+
# your application. Should only be called from KirbyBase#get_table.
|
1435
|
+
#
|
1436
|
+
def KBTable.create_called_from_database_instance(db, name, filename)
|
1437
|
+
return new(db, name, filename)
|
1438
|
+
end
|
1439
|
+
|
1440
|
+
#-----------------------------------------------------------------------
|
1441
|
+
# initialize
|
1442
|
+
#-----------------------------------------------------------------------
|
1443
|
+
#++
|
1444
|
+
# This has been declared private so user's cannot create new instances
|
1445
|
+
# of KBTable from their application. A user gets a handle to a KBTable
|
1446
|
+
# instance by calling KirbyBase#get_table for an existing table or
|
1447
|
+
# KirbyBase.create_table for a new table.
|
1448
|
+
#
|
1449
|
+
def initialize(db, name, filename)
|
1450
|
+
@db = db
|
1451
|
+
@name = name
|
1452
|
+
@filename = filename
|
1453
|
+
@encrypted = false
|
1454
|
+
@lookup_key = :recno
|
1455
|
+
|
1456
|
+
# Alias delete_all to clear method.
|
1457
|
+
alias delete_all clear
|
1458
|
+
|
1459
|
+
update_header_vars
|
1460
|
+
create_indexes
|
1461
|
+
create_table_class unless @db.server?
|
1462
|
+
end
|
1463
|
+
|
1464
|
+
#-----------------------------------------------------------------------
|
1465
|
+
# encrypted?
|
1466
|
+
#-----------------------------------------------------------------------
|
1467
|
+
#++
|
1468
|
+
# Returns true if table is encrypted.
|
1469
|
+
#
|
1470
|
+
def encrypted?
|
1471
|
+
if @encrypted
|
1472
|
+
return true
|
1473
|
+
else
|
1474
|
+
return false
|
1475
|
+
end
|
1476
|
+
end
|
1477
|
+
|
1478
|
+
#-----------------------------------------------------------------------
|
1479
|
+
# field_names
|
1480
|
+
#-----------------------------------------------------------------------
|
1481
|
+
#++
|
1482
|
+
# Return array containing table field names.
|
1483
|
+
#
|
1484
|
+
def field_names
|
1485
|
+
return @field_names
|
1486
|
+
end
|
1487
|
+
|
1488
|
+
#-----------------------------------------------------------------------
|
1489
|
+
# field_types
|
1490
|
+
#-----------------------------------------------------------------------
|
1491
|
+
#++
|
1492
|
+
# Return array containing table field types.
|
1493
|
+
#
|
1494
|
+
def field_types
|
1495
|
+
return @field_types
|
1496
|
+
end
|
1497
|
+
|
1498
|
+
#-----------------------------------------------------------------------
|
1499
|
+
# field_extras
|
1500
|
+
#-----------------------------------------------------------------------
|
1501
|
+
#++
|
1502
|
+
# Return array containing table field extras.
|
1503
|
+
#
|
1504
|
+
def field_extras
|
1505
|
+
return @field_extras
|
1506
|
+
end
|
1507
|
+
|
1508
|
+
#-----------------------------------------------------------------------
|
1509
|
+
# field_indexes
|
1510
|
+
#-----------------------------------------------------------------------
|
1511
|
+
#++
|
1512
|
+
# Return array containing table field indexes.
|
1513
|
+
#
|
1514
|
+
def field_indexes
|
1515
|
+
return @field_indexes
|
1516
|
+
end
|
1517
|
+
|
1518
|
+
#-----------------------------------------------------------------------
|
1519
|
+
# insert
|
1520
|
+
#-----------------------------------------------------------------------
|
1521
|
+
#++
|
1522
|
+
# Insert a new record into a table, return unique record number.
|
1523
|
+
#
|
1524
|
+
# *data*:: Array, Hash, Struct instance containing field values of
|
1525
|
+
# new record.
|
1526
|
+
# *insert_proc*:: Proc instance containing insert code. This and the
|
1527
|
+
# data parameter are mutually exclusive.
|
1528
|
+
#
|
1529
|
+
def insert(*data, &insert_proc)
|
1530
|
+
raise 'Cannot specify both a hash/array/struct and a ' + \
|
1531
|
+
'proc for method #insert!' unless data.empty? or insert_proc.nil?
|
1532
|
+
|
1533
|
+
raise 'Must specify either hash/array/struct or insert ' + \
|
1534
|
+
'proc for method #insert!' if data.empty? and insert_proc.nil?
|
1535
|
+
|
1536
|
+
# Update the header variables.
|
1537
|
+
update_header_vars
|
1538
|
+
|
1539
|
+
# Convert input, which could be an array, a hash, or a Struct
|
1540
|
+
# into a common format (i.e. hash).
|
1541
|
+
if data.empty?
|
1542
|
+
input_rec = convert_input_data(insert_proc)
|
1543
|
+
else
|
1544
|
+
input_rec = convert_input_data(data)
|
1545
|
+
end
|
1546
|
+
|
1547
|
+
# Check the field values to make sure they are proper types.
|
1548
|
+
validate_input(input_rec)
|
1549
|
+
|
1550
|
+
return @db.engine.insert_record(self, @field_names.zip(@field_types
|
1551
|
+
).collect do |fn, ft|
|
1552
|
+
convert_to_string(ft, input_rec.fetch(fn, ''))
|
1553
|
+
end)
|
1554
|
+
end
|
1555
|
+
|
1556
|
+
#-----------------------------------------------------------------------
|
1557
|
+
# update_all
|
1558
|
+
#-----------------------------------------------------------------------
|
1559
|
+
#++
|
1560
|
+
# Return array of records (Structs) to be updated, in this case all
|
1561
|
+
# records.
|
1562
|
+
#
|
1563
|
+
# *updates*:: Hash or Struct containing updates.
|
1564
|
+
#
|
1565
|
+
def update_all(*updates)
|
1566
|
+
update(*updates) { true }
|
1567
|
+
end
|
1568
|
+
|
1569
|
+
#-----------------------------------------------------------------------
|
1570
|
+
# update
|
1571
|
+
#-----------------------------------------------------------------------
|
1572
|
+
#++
|
1573
|
+
# Return array of records (Structs) to be updated based on select cond.
|
1574
|
+
#
|
1575
|
+
# *updates*:: Hash or Struct containing updates.
|
1576
|
+
# *select_cond*:: Proc containing code to select records to update.
|
1577
|
+
#
|
1578
|
+
def update(*updates, &select_cond)
|
1579
|
+
raise ArgumentError, "Must specify select condition code " + \
|
1580
|
+
"block. To update all records, use #update_all instead." if \
|
1581
|
+
select_cond.nil?
|
1582
|
+
|
1583
|
+
# Update the header variables.
|
1584
|
+
update_header_vars
|
1585
|
+
|
1586
|
+
# Get all records that match the selection criteria and
|
1587
|
+
# return them in an array.
|
1588
|
+
result_set = get_matches(:update, @field_names, select_cond)
|
1589
|
+
|
1590
|
+
return result_set if updates.empty?
|
1591
|
+
|
1592
|
+
set(result_set, updates)
|
1593
|
+
end
|
1594
|
+
|
1595
|
+
#-----------------------------------------------------------------------
|
1596
|
+
# []=
|
1597
|
+
#-----------------------------------------------------------------------
|
1598
|
+
#++
|
1599
|
+
# Update record whose recno field equals index.
|
1600
|
+
#
|
1601
|
+
# *index*:: Integer specifying recno you wish to select.
|
1602
|
+
# *updates*:: Hash, Struct, or Array containing updates.
|
1603
|
+
#
|
1604
|
+
def []=(index, updates)
|
1605
|
+
return update(updates) { |r| r.recno == index }
|
1606
|
+
end
|
1607
|
+
|
1608
|
+
#-----------------------------------------------------------------------
|
1609
|
+
# set
|
1610
|
+
#-----------------------------------------------------------------------
|
1611
|
+
#++
|
1612
|
+
# Set fields of records to updated values. Returns number of records
|
1613
|
+
# updated.
|
1614
|
+
#
|
1615
|
+
# *recs*:: Array of records (Structs) that will be updated.
|
1616
|
+
# *data*:: Hash, Struct, Proc containing updates.
|
1617
|
+
#
|
1618
|
+
def set(recs, data)
|
1619
|
+
# Convert updates, which could be an array, a hash, or a Struct
|
1620
|
+
# into a common format (i.e. hash).
|
1621
|
+
update_rec = convert_input_data(data)
|
1622
|
+
|
1623
|
+
# Make sure all of the fields of the update rec are of the proper
|
1624
|
+
# type.
|
1625
|
+
validate_input(update_rec)
|
1626
|
+
|
1627
|
+
updated_recs = []
|
1628
|
+
|
1629
|
+
# For each one of the recs that matched the update query, apply the
|
1630
|
+
# updates to it and write it back to the database table.
|
1631
|
+
recs.each do |rec|
|
1632
|
+
updated_rec = {}
|
1633
|
+
updated_rec[:rec] = \
|
1634
|
+
@field_names.zip(@field_types).collect do |fn, ft|
|
1635
|
+
convert_to_string(ft, update_rec.fetch(fn, rec.send(fn)))
|
1636
|
+
end
|
1637
|
+
updated_rec[:fpos] = rec.fpos
|
1638
|
+
updated_rec[:line_length] = rec.line_length
|
1639
|
+
updated_recs << updated_rec
|
1640
|
+
end
|
1641
|
+
@db.engine.update_records(self, updated_recs)
|
1642
|
+
|
1643
|
+
# Return the number of records updated.
|
1644
|
+
return recs.size
|
1645
|
+
end
|
1646
|
+
|
1647
|
+
#-----------------------------------------------------------------------
|
1648
|
+
# delete
|
1649
|
+
#-----------------------------------------------------------------------
|
1650
|
+
#++
|
1651
|
+
# Delete records from table and return # deleted.
|
1652
|
+
#
|
1653
|
+
# *select_cond*:: Proc containing code to select records.
|
1654
|
+
#
|
1655
|
+
def delete(&select_cond)
|
1656
|
+
raise ArgumentError, 'Must specify select condition code ' + \
|
1657
|
+
'block. To delete all records, use #clear instead.' if \
|
1658
|
+
select_cond.nil?
|
1659
|
+
|
1660
|
+
# Get all records that match the selection criteria and
|
1661
|
+
# return them in an array.
|
1662
|
+
result_set = get_matches(:delete, [:recno], select_cond)
|
1663
|
+
|
1664
|
+
@db.engine.delete_records(self, result_set)
|
1665
|
+
|
1666
|
+
# Return the number of records deleted.
|
1667
|
+
return result_set.size
|
1668
|
+
end
|
1669
|
+
|
1670
|
+
#-----------------------------------------------------------------------
|
1671
|
+
# clear
|
1672
|
+
#-----------------------------------------------------------------------
|
1673
|
+
#++
|
1674
|
+
# Delete all records from table. You can also use #delete_all.
|
1675
|
+
#
|
1676
|
+
# *reset_recno_ctr*:: true/false specifying whether recno counter should
|
1677
|
+
# be reset to 0.
|
1678
|
+
#
|
1679
|
+
def clear(reset_recno_ctr=true)
|
1680
|
+
delete { true }
|
1681
|
+
pack
|
1682
|
+
|
1683
|
+
@db.engine.reset_recno_ctr if reset_recno_ctr
|
1684
|
+
end
|
1685
|
+
|
1686
|
+
#-----------------------------------------------------------------------
|
1687
|
+
# []
|
1688
|
+
#-----------------------------------------------------------------------
|
1689
|
+
#++
|
1690
|
+
# Return the record(s) whose recno field is included in index.
|
1691
|
+
#
|
1692
|
+
# *index*:: Array of Integer(s) specifying recno(s) you wish to select.
|
1693
|
+
#
|
1694
|
+
def [](*index)
|
1695
|
+
return nil if index[0].nil?
|
1696
|
+
|
1697
|
+
return get_match_by_recno(:select, @field_names, index[0]) if \
|
1698
|
+
index.size == 1
|
1699
|
+
|
1700
|
+
recs = select_by_recno_index(*@field_names) { |r|
|
1701
|
+
index.includes?(r.recno)
|
1702
|
+
}
|
1703
|
+
|
1704
|
+
return recs
|
1705
|
+
end
|
1706
|
+
|
1707
|
+
#-----------------------------------------------------------------------
|
1708
|
+
# select
|
1709
|
+
#-----------------------------------------------------------------------
|
1710
|
+
#++
|
1711
|
+
# Return array of records (Structs) matching select conditions.
|
1712
|
+
#
|
1713
|
+
# *filter*:: List of field names (Symbols) to include in result set.
|
1714
|
+
# *select_cond*:: Proc containing select code.
|
1715
|
+
#
|
1716
|
+
def select(*filter, &select_cond)
|
1717
|
+
# Declare these variables before the code block so they don't go
|
1718
|
+
# after the code block is done.
|
1719
|
+
result_set = []
|
1720
|
+
|
1721
|
+
# Validate that all names in filter are valid field names.
|
1722
|
+
validate_filter(filter)
|
1723
|
+
|
1724
|
+
filter = @field_names if filter.empty?
|
1725
|
+
|
1726
|
+
# Get all records that match the selection criteria and
|
1727
|
+
# return them in an array of Struct instances.
|
1728
|
+
return get_matches(:select, filter, select_cond)
|
1729
|
+
end
|
1730
|
+
|
1731
|
+
#-----------------------------------------------------------------------
|
1732
|
+
# select_by_recno_index
|
1733
|
+
#-----------------------------------------------------------------------
|
1734
|
+
#++
|
1735
|
+
# Return array of records (Structs) matching select conditions. Select
|
1736
|
+
# condition block should not contain references to any table column
|
1737
|
+
# except :recno. If you need to select by other table columns than just
|
1738
|
+
# :recno, use #select instead.
|
1739
|
+
#
|
1740
|
+
# *filter*:: List of field names (Symbols) to include in result set.
|
1741
|
+
# *select_cond*:: Proc containing select code.
|
1742
|
+
#
|
1743
|
+
def select_by_recno_index(*filter, &select_cond)
|
1744
|
+
# Declare these variables before the code block so they don't go
|
1745
|
+
# after the code block is done.
|
1746
|
+
result_set = []
|
1747
|
+
|
1748
|
+
# Validate that all names in filter are valid field names.
|
1749
|
+
validate_filter(filter)
|
1750
|
+
|
1751
|
+
filter = @field_names if filter.empty?
|
1752
|
+
|
1753
|
+
# Get all records that match the selection criteria and
|
1754
|
+
# return them in an array of Struct instances.
|
1755
|
+
return get_matches_by_recno_index(:select, filter, select_cond)
|
1756
|
+
end
|
1757
|
+
|
1758
|
+
#-----------------------------------------------------------------------
|
1759
|
+
# pack
|
1760
|
+
#-----------------------------------------------------------------------
|
1761
|
+
#++
|
1762
|
+
# Remove blank records from table, return total removed.
|
1763
|
+
#
|
1764
|
+
def pack
|
1765
|
+
lines_deleted = @db.engine.pack_table(self)
|
1766
|
+
|
1767
|
+
update_header_vars
|
1768
|
+
|
1769
|
+
@db.engine.remove_recno_index(@name)
|
1770
|
+
@db.engine.remove_indexes(@name)
|
1771
|
+
|
1772
|
+
create_indexes
|
1773
|
+
create_table_class unless @db.server?
|
1774
|
+
|
1775
|
+
return lines_deleted
|
1776
|
+
end
|
1777
|
+
|
1778
|
+
#-----------------------------------------------------------------------
|
1779
|
+
# total_recs
|
1780
|
+
#-----------------------------------------------------------------------
|
1781
|
+
#++
|
1782
|
+
# Return total number of undeleted (blank) records in table.
|
1783
|
+
#
|
1784
|
+
def total_recs
|
1785
|
+
return @db.engine.get_total_recs(self)
|
1786
|
+
end
|
1787
|
+
|
1788
|
+
#-----------------------------------------------------------------------
|
1789
|
+
# import_csv
|
1790
|
+
#-----------------------------------------------------------------------
|
1791
|
+
#++
|
1792
|
+
# Import csv file into table.
|
1793
|
+
#
|
1794
|
+
# *csv_filename*:: filename of csv file to import.
|
1795
|
+
#
|
1796
|
+
def import_csv(csv_filename)
|
1797
|
+
tbl_rec = @table_class.new(self)
|
1798
|
+
|
1799
|
+
CSV.open(csv_filename, 'r') do |row|
|
1800
|
+
tbl_rec.populate([nil] + row)
|
1801
|
+
insert(tbl_rec)
|
1802
|
+
end
|
1803
|
+
end
|
1804
|
+
|
1805
|
+
#-----------------------------------------------------------------------
|
1806
|
+
# PRIVATE METHODS
|
1807
|
+
#-----------------------------------------------------------------------
|
1808
|
+
private
|
1809
|
+
|
1810
|
+
#-----------------------------------------------------------------------
|
1811
|
+
# create_indexes
|
1812
|
+
#-----------------------------------------------------------------------
|
1813
|
+
def create_indexes
|
1814
|
+
# Create the recno index. A recno index always gets created even if
|
1815
|
+
# there are no user-defined indexes for the table.
|
1816
|
+
@db.engine.init_recno_index(self)
|
1817
|
+
|
1818
|
+
# There can be up to 5 different indexes on a table. Any of these
|
1819
|
+
# indexes can be single or compound.
|
1820
|
+
['Index->1', 'Index->2', 'Index->3', 'Index->4',
|
1821
|
+
'Index->5'].each do |idx|
|
1822
|
+
index_col_names = []
|
1823
|
+
@field_indexes.each_with_index do |fi,i|
|
1824
|
+
next if fi.nil?
|
1825
|
+
index_col_names << @field_names[i] if fi.include?(idx)
|
1826
|
+
end
|
1827
|
+
|
1828
|
+
# If no fields were indexed on this number (1..5), go to the
|
1829
|
+
# next index number.
|
1830
|
+
next if index_col_names.empty?
|
1831
|
+
|
1832
|
+
# Create this index on the engine.
|
1833
|
+
@db.engine.init_index(self, index_col_names)
|
1834
|
+
|
1835
|
+
# For each index found, add an instance method for it so that
|
1836
|
+
# it can be used for #selects.
|
1837
|
+
select_meth_str = <<-END_OF_STRING
|
1838
|
+
def select_by_#{index_col_names.join('_')}_index(*filter,
|
1839
|
+
&select_cond)
|
1840
|
+
result_set = []
|
1841
|
+
validate_filter(filter)
|
1842
|
+
filter = @field_names if filter.empty?
|
1843
|
+
return get_matches_by_index(:select,
|
1844
|
+
[:#{index_col_names.join(',:')}], filter, select_cond)
|
1845
|
+
end
|
1846
|
+
END_OF_STRING
|
1847
|
+
self.class.class_eval(select_meth_str)
|
1848
|
+
end
|
1849
|
+
end
|
1850
|
+
|
1851
|
+
#-----------------------------------------------------------------------
|
1852
|
+
# create_table_class
|
1853
|
+
#-----------------------------------------------------------------------
|
1854
|
+
def create_table_class
|
1855
|
+
#This is the class that will be used in #select condition blocks.
|
1856
|
+
@table_class = Class.new(KBTableRec)
|
1857
|
+
|
1858
|
+
get_meth_str = ''
|
1859
|
+
get_meth_upd_res_str = ''
|
1860
|
+
set_meth_str = ''
|
1861
|
+
|
1862
|
+
@field_names.zip(@field_types, @field_extras) do |x|
|
1863
|
+
field_name, field_type, field_extra = x
|
1864
|
+
|
1865
|
+
@lookup_key = field_name if field_extra.has_key?('Key')
|
1866
|
+
|
1867
|
+
# These are the default get/set methods for the table column.
|
1868
|
+
get_meth_str = <<-END_OF_STRING
|
1869
|
+
def #{field_name}
|
1870
|
+
return @#{field_name}
|
1871
|
+
end
|
1872
|
+
END_OF_STRING
|
1873
|
+
get_meth_upd_res_str = <<-END_OF_STRING
|
1874
|
+
def #{field_name}_upd_res
|
1875
|
+
return @#{field_name}
|
1876
|
+
end
|
1877
|
+
END_OF_STRING
|
1878
|
+
set_meth_str = <<-END_OF_STRING
|
1879
|
+
def #{field_name}=(s)
|
1880
|
+
@#{field_name} = convert_to(:#{field_type}, s)
|
1881
|
+
end
|
1882
|
+
END_OF_STRING
|
1883
|
+
|
1884
|
+
# If this is a Lookup field, modify the get_method.
|
1885
|
+
if field_extra.has_key?('Lookup')
|
1886
|
+
lookup_table, key_field = field_extra['Lookup'].split('.')
|
1887
|
+
if key_field == 'recno'
|
1888
|
+
get_meth_str = <<-END_OF_STRING
|
1889
|
+
def #{field_name}
|
1890
|
+
table = @tbl.db.get_table(:#{lookup_table})
|
1891
|
+
return table[@#{field_name}]
|
1892
|
+
end
|
1893
|
+
END_OF_STRING
|
1894
|
+
else
|
1895
|
+
begin
|
1896
|
+
@db.get_table(lookup_table)
|
1897
|
+
rescue RuntimeError
|
1898
|
+
raise "Must create child table first when using " +
|
1899
|
+
"'Lookup'"
|
1900
|
+
end
|
1901
|
+
|
1902
|
+
if @db.get_table(lookup_table).respond_to?(
|
1903
|
+
'select_by_%s_index' % key_field)
|
1904
|
+
get_meth_str = <<-END_OF_STRING
|
1905
|
+
def #{field_name}
|
1906
|
+
table = @tbl.db.get_table(:#{lookup_table})
|
1907
|
+
return table.select_by_#{key_field}_index { |r|
|
1908
|
+
r.#{key_field} == @#{field_name} }.first
|
1909
|
+
end
|
1910
|
+
END_OF_STRING
|
1911
|
+
else
|
1912
|
+
get_meth_str = <<-END_OF_STRING
|
1913
|
+
def #{field_name}
|
1914
|
+
table = @tbl.db.get_table(:#{lookup_table})
|
1915
|
+
return table.select { |r|
|
1916
|
+
r.#{key_field} == @#{field_name} }.first
|
1917
|
+
end
|
1918
|
+
END_OF_STRING
|
1919
|
+
end
|
1920
|
+
end
|
1921
|
+
end
|
1922
|
+
|
1923
|
+
# If this is a Link_many field, modify the get/set methods.
|
1924
|
+
if field_extra.has_key?('Link_many')
|
1925
|
+
lookup_field, rest = field_extra['Link_many'].split('=')
|
1926
|
+
link_table, link_field = rest.split('.')
|
1927
|
+
|
1928
|
+
begin
|
1929
|
+
@db.get_table(link_table)
|
1930
|
+
rescue RuntimeError
|
1931
|
+
raise "Must create child table first when using " +
|
1932
|
+
"'Link_many'"
|
1933
|
+
end
|
1934
|
+
|
1935
|
+
if @db.get_table(link_table).respond_to?(
|
1936
|
+
'select_by_%s_index' % link_field)
|
1937
|
+
get_meth_str = <<-END_OF_STRING
|
1938
|
+
def #{field_name}
|
1939
|
+
table = @tbl.db.get_table(:#{link_table})
|
1940
|
+
return table.select_by_#{link_field}_index { |r|
|
1941
|
+
r.send(:#{link_field}) == @#{lookup_field} }
|
1942
|
+
end
|
1943
|
+
END_OF_STRING
|
1944
|
+
else
|
1945
|
+
get_meth_str = <<-END_OF_STRING
|
1946
|
+
def #{field_name}
|
1947
|
+
table = @tbl.db.get_table(:#{link_table})
|
1948
|
+
return table.select { |r|
|
1949
|
+
r.send(:#{link_field}) == @#{lookup_field} }
|
1950
|
+
end
|
1951
|
+
END_OF_STRING
|
1952
|
+
end
|
1953
|
+
|
1954
|
+
get_meth_upd_res_str = <<-END_OF_STRING
|
1955
|
+
def #{field_name}_upd_res
|
1956
|
+
return nil
|
1957
|
+
end
|
1958
|
+
END_OF_STRING
|
1959
|
+
set_meth_str = <<-END_OF_STRING
|
1960
|
+
def #{field_name}=(s)
|
1961
|
+
@#{field_name} = nil
|
1962
|
+
end
|
1963
|
+
END_OF_STRING
|
1964
|
+
end
|
1965
|
+
|
1966
|
+
# If this is a Calculated field, modify the get/set methods.
|
1967
|
+
if field_extra.has_key?('Calculated')
|
1968
|
+
calculation = field_extra['Calculated']
|
1969
|
+
|
1970
|
+
get_meth_str = <<-END_OF_STRING
|
1971
|
+
def #{field_name}()
|
1972
|
+
return #{calculation}
|
1973
|
+
end
|
1974
|
+
END_OF_STRING
|
1975
|
+
get_meth_upd_res_str = <<-END_OF_STRING
|
1976
|
+
def #{field_name}_upd_res()
|
1977
|
+
return nil
|
1978
|
+
end
|
1979
|
+
END_OF_STRING
|
1980
|
+
set_meth_str = <<-END_OF_STRING
|
1981
|
+
def #{field_name}=(s)
|
1982
|
+
@#{field_name} = nil
|
1983
|
+
end
|
1984
|
+
END_OF_STRING
|
1985
|
+
end
|
1986
|
+
|
1987
|
+
@table_class.class_eval(get_meth_str)
|
1988
|
+
@table_class.class_eval(get_meth_upd_res_str)
|
1989
|
+
@table_class.class_eval(set_meth_str)
|
1990
|
+
end
|
1991
|
+
end
|
1992
|
+
|
1993
|
+
#-----------------------------------------------------------------------
|
1994
|
+
# convert_to_string
|
1995
|
+
#-----------------------------------------------------------------------
|
1996
|
+
def convert_to_string(data_type, x)
|
1997
|
+
case data_type
|
1998
|
+
when :YAML
|
1999
|
+
y = x.to_yaml
|
2000
|
+
if y =~ ENCODE_RE
|
2001
|
+
return y.gsub("&", '&').gsub("\n", '&linefeed;').gsub(
|
2002
|
+
"\r", '&carriage_return;').gsub("\032", '&substitute;'
|
2003
|
+
).gsub("|", '&pipe;')
|
2004
|
+
else
|
2005
|
+
return y
|
2006
|
+
end
|
2007
|
+
when :String
|
2008
|
+
if x =~ ENCODE_RE
|
2009
|
+
return x.gsub("&", '&').gsub("\n", '&linefeed;').gsub(
|
2010
|
+
"\r", '&carriage_return;').gsub("\032", '&substitute;'
|
2011
|
+
).gsub("|", '&pipe;')
|
2012
|
+
else
|
2013
|
+
return x
|
2014
|
+
end
|
2015
|
+
else
|
2016
|
+
return x.to_s
|
2017
|
+
end
|
2018
|
+
end
|
2019
|
+
|
2020
|
+
#-----------------------------------------------------------------------
|
2021
|
+
# validate_filter
|
2022
|
+
#-----------------------------------------------------------------------
|
2023
|
+
#++
|
2024
|
+
# Check that filter contains valid field names.
|
2025
|
+
#
|
2026
|
+
def validate_filter(filter)
|
2027
|
+
# Each field in the filter array must be a valid fieldname in the
|
2028
|
+
# table.
|
2029
|
+
filter.each { |f|
|
2030
|
+
raise 'Invalid field name: %s in filter!' % f unless \
|
2031
|
+
@field_names.include?(f)
|
2032
|
+
}
|
2033
|
+
end
|
2034
|
+
|
2035
|
+
#-----------------------------------------------------------------------
|
2036
|
+
# convert_input_data
|
2037
|
+
#-----------------------------------------------------------------------
|
2038
|
+
#++
|
2039
|
+
# Convert data passed to #input, #update, or #set to a common format.
|
2040
|
+
#
|
2041
|
+
def convert_input_data(values)
|
2042
|
+
if values.class == Proc
|
2043
|
+
tbl_struct = Struct.new(*@field_names[1..-1])
|
2044
|
+
tbl_rec = tbl_struct.new
|
2045
|
+
begin
|
2046
|
+
values.call(tbl_rec)
|
2047
|
+
rescue NoMethodError
|
2048
|
+
raise 'Invalid field name in code block: %s' % $!
|
2049
|
+
end
|
2050
|
+
temp_hash = {}
|
2051
|
+
@field_names[1..-1].collect { |f|
|
2052
|
+
temp_hash[f] = tbl_rec[f] unless tbl_rec[f].nil?
|
2053
|
+
}
|
2054
|
+
return temp_hash
|
2055
|
+
elsif values[0].class.to_s == @record_class or \
|
2056
|
+
values[0].class == @table_class
|
2057
|
+
temp_hash = {}
|
2058
|
+
@field_names[1..-1].collect { |f|
|
2059
|
+
temp_hash[f] = values[0].send(f) if values[0].respond_to?(f)
|
2060
|
+
}
|
2061
|
+
return temp_hash
|
2062
|
+
elsif values[0].class == Hash
|
2063
|
+
return values[0].dup
|
2064
|
+
elsif values[0].kind_of?(Struct)
|
2065
|
+
temp_hash = {}
|
2066
|
+
@field_names[1..-1].collect { |f|
|
2067
|
+
temp_hash[f] = values[0][f] if values[0].members.include?(
|
2068
|
+
f.to_s)
|
2069
|
+
}
|
2070
|
+
return temp_hash
|
2071
|
+
elsif values[0].class == Array
|
2072
|
+
raise ArgumentError, 'Must specify all fields in input array!' \
|
2073
|
+
unless values[0].size == @field_names[1..-1].size
|
2074
|
+
temp_hash = {}
|
2075
|
+
@field_names[1..-1].collect { |f|
|
2076
|
+
temp_hash[f] = values[0][@field_names.index(f)-1]
|
2077
|
+
}
|
2078
|
+
return temp_hash
|
2079
|
+
elsif values.class == Array
|
2080
|
+
raise ArgumentError, 'Must specify all fields in input array!' \
|
2081
|
+
unless values.size == @field_names[1..-1].size
|
2082
|
+
temp_hash = {}
|
2083
|
+
@field_names[1..-1].collect { |f|
|
2084
|
+
temp_hash[f] = values[@field_names.index(f)-1]
|
2085
|
+
}
|
2086
|
+
return temp_hash
|
2087
|
+
else
|
2088
|
+
raise(ArgumentError, 'Invalid type for values container!')
|
2089
|
+
end
|
2090
|
+
end
|
2091
|
+
|
2092
|
+
#-----------------------------------------------------------------------
|
2093
|
+
# validate_input
|
2094
|
+
#-----------------------------------------------------------------------
|
2095
|
+
#++
|
2096
|
+
# Check input data to ensure proper data types.
|
2097
|
+
#
|
2098
|
+
def validate_input(data)
|
2099
|
+
raise 'Cannot insert/update recno field!' if data.has_key?(:recno)
|
2100
|
+
|
2101
|
+
@field_names[1..-1].each do |f|
|
2102
|
+
next unless data.has_key?(f)
|
2103
|
+
|
2104
|
+
next if data[f].nil?
|
2105
|
+
case @field_types[@field_names.index(f)]
|
2106
|
+
when /:String|:Memo|:Blob/
|
2107
|
+
raise 'Invalid String value for: %s' % f unless \
|
2108
|
+
data[f].respond_to?(:to_str)
|
2109
|
+
when :Boolean
|
2110
|
+
raise 'Invalid Boolean value for: %s' % f unless \
|
2111
|
+
data[f].is_a?(TrueClass) or data[f].kind_of?(FalseClass)
|
2112
|
+
when :Integer
|
2113
|
+
raise 'Invalid Integer value for: %s' % f unless \
|
2114
|
+
data[f].respond_to?(:to_int)
|
2115
|
+
when :Float
|
2116
|
+
raise 'Invalid Float value for: %s' % f unless \
|
2117
|
+
data[f].respond_to?(:to_f)
|
2118
|
+
when :Date
|
2119
|
+
raise 'Invalid Date value for: %s' % f unless \
|
2120
|
+
data[f].is_a?(Date)
|
2121
|
+
when :Time
|
2122
|
+
raise 'Invalid Time value for: %s' % f unless \
|
2123
|
+
data[f].is_a?(Time)
|
2124
|
+
when :DateTime
|
2125
|
+
raise 'Invalid DateTime value for: %s' % f unless \
|
2126
|
+
data[f].is_a?(DateTime)
|
2127
|
+
when :YAML
|
2128
|
+
raise 'Invalid YAML value for: %s' % f unless \
|
2129
|
+
data[f].respond_to?(:to_yaml)
|
2130
|
+
end
|
2131
|
+
end
|
2132
|
+
end
|
2133
|
+
|
2134
|
+
#-----------------------------------------------------------------------
|
2135
|
+
# update_header_vars
|
2136
|
+
#-----------------------------------------------------------------------
|
2137
|
+
#++
|
2138
|
+
# Read header record and update instance variables.
|
2139
|
+
#
|
2140
|
+
def update_header_vars
|
2141
|
+
@encrypted, @last_rec_no, @del_ctr, @record_class, @field_names, \
|
2142
|
+
@field_types, @field_indexes, @field_extras = \
|
2143
|
+
@db.engine.get_header_vars(self)
|
2144
|
+
end
|
2145
|
+
|
2146
|
+
#-----------------------------------------------------------------------
|
2147
|
+
# get_result_struct
|
2148
|
+
#-----------------------------------------------------------------------
|
2149
|
+
def get_result_struct(query_type, filter)
|
2150
|
+
case query_type
|
2151
|
+
when :select
|
2152
|
+
return Struct.new(*filter) if @record_class == 'Struct'
|
2153
|
+
when :update
|
2154
|
+
return Struct.new(*(filter + [:fpos, :line_length]))
|
2155
|
+
when :delete
|
2156
|
+
return Struct.new(:recno, :fpos, :line_length)
|
2157
|
+
end
|
2158
|
+
return nil
|
2159
|
+
end
|
2160
|
+
|
2161
|
+
#-----------------------------------------------------------------------
|
2162
|
+
# create_result_rec
|
2163
|
+
#-----------------------------------------------------------------------
|
2164
|
+
def create_result_rec(query_type, filter, result_struct, tbl_rec, rec)
|
2165
|
+
# If this isn't a select query or if it is a select query, but
|
2166
|
+
# the table record class is simply a Struct, then we will use
|
2167
|
+
# a Struct for the result record type.
|
2168
|
+
if query_type != :select
|
2169
|
+
result_rec = result_struct.new(*filter.collect { |f|
|
2170
|
+
tbl_rec.send("#{f}_upd_res".to_sym) })
|
2171
|
+
elsif @record_class == 'Struct'
|
2172
|
+
result_rec = result_struct.new(*filter.collect { |f|
|
2173
|
+
tbl_rec.send(f) })
|
2174
|
+
else
|
2175
|
+
if Object.full_const_get(@record_class).respond_to?(:kb_create)
|
2176
|
+
result_rec = Object.full_const_get(@record_class
|
2177
|
+
).kb_create(*@field_names.collect { |f|
|
2178
|
+
# Just a warning here: If you specify a filter on
|
2179
|
+
# a select, you are only going to get those fields
|
2180
|
+
# you specified in the result set, EVEN IF
|
2181
|
+
# record_class is a custom class instead of Struct.
|
2182
|
+
if filter.include?(f)
|
2183
|
+
tbl_rec.send(f)
|
2184
|
+
else
|
2185
|
+
nil
|
2186
|
+
end
|
2187
|
+
})
|
2188
|
+
elsif Object.full_const_get(@record_class).respond_to?(
|
2189
|
+
:kb_defaults)
|
2190
|
+
result_rec = Object.full_const_get(@record_class).new(
|
2191
|
+
*@field_names.collect { |f|
|
2192
|
+
tbl_rec.send(f) || Object.full_const_get(
|
2193
|
+
@record_class).kb_defaults[@field_names.index(f)]
|
2194
|
+
}
|
2195
|
+
)
|
2196
|
+
end
|
2197
|
+
end
|
2198
|
+
|
2199
|
+
unless query_type == :select
|
2200
|
+
result_rec.fpos = rec[-2]
|
2201
|
+
result_rec.line_length = rec[-1]
|
2202
|
+
end
|
2203
|
+
return result_rec
|
2204
|
+
end
|
2205
|
+
|
2206
|
+
#-----------------------------------------------------------------------
|
2207
|
+
# get_matches
|
2208
|
+
#-----------------------------------------------------------------------
|
2209
|
+
#++
|
2210
|
+
# Return records from table that match select condition.
|
2211
|
+
#
|
2212
|
+
def get_matches(query_type, filter, select_cond)
|
2213
|
+
result_struct = get_result_struct(query_type, filter)
|
2214
|
+
match_array = KBResultSet.new(self, filter, filter.collect { |f|
|
2215
|
+
@field_types[@field_names.index(f)] })
|
2216
|
+
|
2217
|
+
tbl_rec = @table_class.new(self)
|
2218
|
+
|
2219
|
+
# Loop through table.
|
2220
|
+
@db.engine.get_recs(self).each do |rec|
|
2221
|
+
tbl_rec.populate(rec)
|
2222
|
+
next unless select_cond.call(tbl_rec) unless select_cond.nil?
|
2223
|
+
|
2224
|
+
match_array << create_result_rec(query_type, filter,
|
2225
|
+
result_struct, tbl_rec, rec)
|
2226
|
+
|
2227
|
+
end
|
2228
|
+
return match_array
|
2229
|
+
end
|
2230
|
+
|
2231
|
+
#-----------------------------------------------------------------------
|
2232
|
+
# get_matches_by_index
|
2233
|
+
#-----------------------------------------------------------------------
|
2234
|
+
#++
|
2235
|
+
# Return records from table that match select condition using one of
|
2236
|
+
# the table's indexes instead of searching the whole file.
|
2237
|
+
#
|
2238
|
+
def get_matches_by_index(query_type, index_fields, filter, select_cond)
|
2239
|
+
good_matches = []
|
2240
|
+
|
2241
|
+
idx_struct = Struct.new(*(index_fields + [:recno]))
|
2242
|
+
|
2243
|
+
begin
|
2244
|
+
@db.engine.get_index(self, index_fields.join('_')).each do |rec|
|
2245
|
+
good_matches << rec[-1] if select_cond.call(
|
2246
|
+
idx_struct.new(*rec))
|
2247
|
+
end
|
2248
|
+
rescue NoMethodError
|
2249
|
+
raise 'Field name in select block not part of index!'
|
2250
|
+
end
|
2251
|
+
|
2252
|
+
return get_matches_by_recno(query_type, filter, good_matches)
|
2253
|
+
end
|
2254
|
+
|
2255
|
+
#-----------------------------------------------------------------------
|
2256
|
+
# get_matches_by_recno_index
|
2257
|
+
#-----------------------------------------------------------------------
|
2258
|
+
#++
|
2259
|
+
# Return records from table that match select condition using the
|
2260
|
+
# table's recno index instead of searching the whole file.
|
2261
|
+
#
|
2262
|
+
def get_matches_by_recno_index(query_type, filter, select_cond)
|
2263
|
+
good_matches = []
|
2264
|
+
|
2265
|
+
idx_struct = Struct.new(:recno)
|
2266
|
+
|
2267
|
+
begin
|
2268
|
+
@db.engine.get_recno_index(self).each_key do |key|
|
2269
|
+
good_matches << key if select_cond.call(
|
2270
|
+
idx_struct.new(key))
|
2271
|
+
end
|
2272
|
+
rescue NoMethodError
|
2273
|
+
raise "Field name in select block not part of index!"
|
2274
|
+
end
|
2275
|
+
|
2276
|
+
return nil if good_matches.empty?
|
2277
|
+
return get_matches_by_recno(query_type, filter, good_matches)
|
2278
|
+
end
|
2279
|
+
|
2280
|
+
#-----------------------------------------------------------------------
|
2281
|
+
# get_match_by_recno
|
2282
|
+
#-----------------------------------------------------------------------
|
2283
|
+
#++
|
2284
|
+
# Return record from table that matches supplied recno.
|
2285
|
+
#
|
2286
|
+
def get_match_by_recno(query_type, filter, recno)
|
2287
|
+
result_struct = get_result_struct(query_type, filter)
|
2288
|
+
match_array = KBResultSet.new(self, filter, filter.collect { |f|
|
2289
|
+
@field_types[@field_names.index(f)] })
|
2290
|
+
|
2291
|
+
tbl_rec = @table_class.new(self)
|
2292
|
+
|
2293
|
+
rec = @db.engine.get_rec_by_recno(self, recno)
|
2294
|
+
return nil if rec.nil?
|
2295
|
+
tbl_rec.populate(rec)
|
2296
|
+
|
2297
|
+
return create_result_rec(query_type, filter, result_struct,
|
2298
|
+
tbl_rec, rec)
|
2299
|
+
end
|
2300
|
+
|
2301
|
+
#-----------------------------------------------------------------------
|
2302
|
+
# get_matches_by_recno
|
2303
|
+
#-----------------------------------------------------------------------
|
2304
|
+
#++
|
2305
|
+
# Return records from table that match select condition.
|
2306
|
+
#
|
2307
|
+
def get_matches_by_recno(query_type, filter, recnos)
|
2308
|
+
result_struct = get_result_struct(query_type, filter)
|
2309
|
+
match_array = KBResultSet.new(self, filter, filter.collect { |f|
|
2310
|
+
@field_types[@field_names.index(f)] })
|
2311
|
+
|
2312
|
+
tbl_rec = @table_class.new(self)
|
2313
|
+
|
2314
|
+
@db.engine.get_recs_by_recno(self, recnos).each do |rec|
|
2315
|
+
next if rec.nil?
|
2316
|
+
tbl_rec.populate(rec)
|
2317
|
+
|
2318
|
+
match_array << create_result_rec(query_type, filter,
|
2319
|
+
result_struct, tbl_rec, rec)
|
2320
|
+
end
|
2321
|
+
return match_array
|
2322
|
+
end
|
2323
|
+
end
|
2324
|
+
|
2325
|
+
|
2326
|
+
#---------------------------------------------------------------------------
|
2327
|
+
# KBMemo
|
2328
|
+
#---------------------------------------------------------------------------
|
2329
|
+
class KBMemo
|
2330
|
+
attr_reader :filepath, :memo
|
2331
|
+
|
2332
|
+
#-----------------------------------------------------------------------
|
2333
|
+
# initialize
|
2334
|
+
#-----------------------------------------------------------------------
|
2335
|
+
def initialize(db, filepath)
|
2336
|
+
@filepath = filepath
|
2337
|
+
@memo = db.engine.get_memo(@filepath)
|
2338
|
+
end
|
2339
|
+
end
|
2340
|
+
|
2341
|
+
#---------------------------------------------------------------------------
|
2342
|
+
# KBBlob
|
2343
|
+
#---------------------------------------------------------------------------
|
2344
|
+
class KBBlob
|
2345
|
+
attr_reader :filepath, :blob
|
2346
|
+
|
2347
|
+
#-----------------------------------------------------------------------
|
2348
|
+
# initialize
|
2349
|
+
#-----------------------------------------------------------------------
|
2350
|
+
def initialize(db, filepath)
|
2351
|
+
@filepath = filepath
|
2352
|
+
@blob = db.engine.get_blob(@filepath)
|
2353
|
+
end
|
2354
|
+
end
|
2355
|
+
|
2356
|
+
|
2357
|
+
#---------------------------------------------------------------------------
|
2358
|
+
# KBIndex
|
2359
|
+
#---------------------------------------------------------------------------
|
2360
|
+
class KBIndex
|
2361
|
+
include KBTypeConversionsMixin
|
2362
|
+
|
2363
|
+
UNENCODE_RE = /&(?:amp|linefeed|carriage_return|substitute|pipe);/
|
2364
|
+
|
2365
|
+
#-----------------------------------------------------------------------
|
2366
|
+
# initialize
|
2367
|
+
#-----------------------------------------------------------------------
|
2368
|
+
def initialize(table, index_fields)
|
2369
|
+
@idx_arr = []
|
2370
|
+
@table = table
|
2371
|
+
@index_fields = index_fields
|
2372
|
+
@col_poss = index_fields.collect {|i| table.field_names.index(i) }
|
2373
|
+
@col_names = index_fields
|
2374
|
+
@col_types = index_fields.collect {|i|
|
2375
|
+
table.field_types[table.field_names.index(i)]}
|
2376
|
+
end
|
2377
|
+
|
2378
|
+
#-----------------------------------------------------------------------
|
2379
|
+
# get_idx
|
2380
|
+
#-----------------------------------------------------------------------
|
2381
|
+
def get_idx
|
2382
|
+
return @idx_arr
|
2383
|
+
end
|
2384
|
+
|
2385
|
+
#-----------------------------------------------------------------------
|
2386
|
+
# rebuild
|
2387
|
+
#-----------------------------------------------------------------------
|
2388
|
+
def rebuild(fptr)
|
2389
|
+
@idx_arr.clear
|
2390
|
+
|
2391
|
+
encrypted = @table.encrypted?
|
2392
|
+
|
2393
|
+
# Skip header rec.
|
2394
|
+
fptr.readline
|
2395
|
+
|
2396
|
+
begin
|
2397
|
+
# Loop through table.
|
2398
|
+
while true
|
2399
|
+
line = fptr.readline
|
2400
|
+
|
2401
|
+
line = unencrypt_str(line) if encrypted
|
2402
|
+
line.strip!
|
2403
|
+
|
2404
|
+
# If blank line (i.e. 'deleted'), skip it.
|
2405
|
+
next if line == ''
|
2406
|
+
|
2407
|
+
# Split the line up into fields.
|
2408
|
+
rec = line.split('|', @col_poss.max+2)
|
2409
|
+
|
2410
|
+
# Create the index record by pulling out the record fields
|
2411
|
+
# that make up this index and converting them to their
|
2412
|
+
# native types.
|
2413
|
+
idx_rec = []
|
2414
|
+
@col_poss.zip(@col_types).each do |col_pos, col_type|
|
2415
|
+
idx_rec << convert_to(col_type, rec[col_pos])
|
2416
|
+
end
|
2417
|
+
|
2418
|
+
# Were all the index fields for this record equal to NULL?
|
2419
|
+
# Then don't add this index record to index array; skip to
|
2420
|
+
# next record.
|
2421
|
+
next if idx_rec.compact.empty?
|
2422
|
+
|
2423
|
+
# Add recno to the end of this index record.
|
2424
|
+
idx_rec << rec.first.to_i
|
2425
|
+
|
2426
|
+
# Add index record to index array.
|
2427
|
+
@idx_arr << idx_rec
|
2428
|
+
end
|
2429
|
+
# Here's how we break out of the loop...
|
2430
|
+
rescue EOFError
|
2431
|
+
end
|
2432
|
+
end
|
2433
|
+
|
2434
|
+
#-----------------------------------------------------------------------
|
2435
|
+
# add_index_rec
|
2436
|
+
#-----------------------------------------------------------------------
|
2437
|
+
def add_index_rec(rec)
|
2438
|
+
@idx_arr << @col_poss.zip(@col_types).collect do |col_pos, col_type|
|
2439
|
+
convert_to(col_type, rec[col_pos])
|
2440
|
+
end + [rec.first.to_i]
|
2441
|
+
end
|
2442
|
+
|
2443
|
+
#-----------------------------------------------------------------------
|
2444
|
+
# delete_index_rec
|
2445
|
+
#-----------------------------------------------------------------------
|
2446
|
+
def delete_index_rec(recno)
|
2447
|
+
i = @idx_arr.rassoc(recno.to_i)
|
2448
|
+
@idx_arr.delete_at(@idx_arr.index(i)) unless i.nil?
|
2449
|
+
end
|
2450
|
+
|
2451
|
+
#-----------------------------------------------------------------------
|
2452
|
+
# update_index_rec
|
2453
|
+
#-----------------------------------------------------------------------
|
2454
|
+
def update_index_rec(rec)
|
2455
|
+
delete_index_rec(rec.first.to_i)
|
2456
|
+
add_index_rec(rec)
|
2457
|
+
end
|
2458
|
+
end
|
2459
|
+
|
2460
|
+
|
2461
|
+
#---------------------------------------------------------------------------
|
2462
|
+
# KBRecnoIndex
|
2463
|
+
#---------------------------------------------------------------------------
|
2464
|
+
class KBRecnoIndex
|
2465
|
+
#-----------------------------------------------------------------------
|
2466
|
+
# initialize
|
2467
|
+
#-----------------------------------------------------------------------
|
2468
|
+
def initialize(table)
|
2469
|
+
@idx_hash = {}
|
2470
|
+
@table = table
|
2471
|
+
end
|
2472
|
+
|
2473
|
+
#-----------------------------------------------------------------------
|
2474
|
+
# get_idx
|
2475
|
+
#-----------------------------------------------------------------------
|
2476
|
+
def get_idx
|
2477
|
+
return @idx_hash
|
2478
|
+
end
|
2479
|
+
|
2480
|
+
#-----------------------------------------------------------------------
|
2481
|
+
# rebuild
|
2482
|
+
#-----------------------------------------------------------------------
|
2483
|
+
def rebuild(fptr)
|
2484
|
+
@idx_hash.clear
|
2485
|
+
|
2486
|
+
encrypted = @table.encrypted?
|
2487
|
+
|
2488
|
+
begin
|
2489
|
+
# Skip header rec.
|
2490
|
+
fptr.readline
|
2491
|
+
|
2492
|
+
# Loop through table.
|
2493
|
+
while true
|
2494
|
+
# Record current position in table. Then read first
|
2495
|
+
# detail record.
|
2496
|
+
fpos = fptr.tell
|
2497
|
+
line = fptr.readline
|
2498
|
+
|
2499
|
+
line = unencrypt_str(line) if encrypted
|
2500
|
+
line.strip!
|
2501
|
+
|
2502
|
+
# If blank line (i.e. 'deleted'), skip it.
|
2503
|
+
next if line == ''
|
2504
|
+
|
2505
|
+
# Split the line up into fields.
|
2506
|
+
rec = line.split('|', 2)
|
2507
|
+
|
2508
|
+
@idx_hash[rec.first.to_i] = fpos
|
2509
|
+
end
|
2510
|
+
# Here's how we break out of the loop...
|
2511
|
+
rescue EOFError
|
2512
|
+
end
|
2513
|
+
end
|
2514
|
+
|
2515
|
+
#-----------------------------------------------------------------------
|
2516
|
+
# add_index_rec
|
2517
|
+
#-----------------------------------------------------------------------
|
2518
|
+
def add_index_rec(recno, fpos)
|
2519
|
+
raise 'Table already has index record for recno: %s' % recno if \
|
2520
|
+
@idx_hash.has_key?(recno.to_i)
|
2521
|
+
@idx_hash[recno.to_i] = fpos
|
2522
|
+
end
|
2523
|
+
|
2524
|
+
#-----------------------------------------------------------------------
|
2525
|
+
# update_index_rec
|
2526
|
+
#-----------------------------------------------------------------------
|
2527
|
+
def update_index_rec(recno, fpos)
|
2528
|
+
raise 'Table has no index record for recno: %s' % recno unless \
|
2529
|
+
@idx_hash.has_key?(recno.to_i)
|
2530
|
+
@idx_hash[recno.to_i] = fpos
|
2531
|
+
end
|
2532
|
+
|
2533
|
+
#-----------------------------------------------------------------------
|
2534
|
+
# delete_index_rec
|
2535
|
+
#-----------------------------------------------------------------------
|
2536
|
+
def delete_index_rec(recno)
|
2537
|
+
raise 'Table has no index record for recno: %s' % recno unless \
|
2538
|
+
@idx_hash.has_key?(recno.to_i)
|
2539
|
+
@idx_hash.delete(recno.to_i)
|
2540
|
+
end
|
2541
|
+
end
|
2542
|
+
|
2543
|
+
|
2544
|
+
#---------------------------------------------------------------------------
|
2545
|
+
# KBTableRec
|
2546
|
+
#---------------------------------------------------------------------------
|
2547
|
+
class KBTableRec
|
2548
|
+
include KBTypeConversionsMixin
|
2549
|
+
|
2550
|
+
def initialize(tbl)
|
2551
|
+
@tbl = tbl
|
2552
|
+
end
|
2553
|
+
|
2554
|
+
def populate(rec)
|
2555
|
+
@tbl.field_names.zip(rec).each do |fn, val|
|
2556
|
+
send("#{fn}=", val)
|
2557
|
+
end
|
2558
|
+
end
|
2559
|
+
|
2560
|
+
def clear
|
2561
|
+
@tbl.field_names.each do |fn|
|
2562
|
+
send("#{fn}=", nil)
|
2563
|
+
end
|
2564
|
+
end
|
2565
|
+
end
|
2566
|
+
|
2567
|
+
|
2568
|
+
#---------------------------------------------------------
|
2569
|
+
# KBResultSet
|
2570
|
+
#---------------------------------------------------------------------------
|
2571
|
+
class KBResultSet < Array
|
2572
|
+
#-----------------------------------------------------------------------
|
2573
|
+
# KBResultSet.reverse
|
2574
|
+
#-----------------------------------------------------------------------
|
2575
|
+
def KBResultSet.reverse(sort_field)
|
2576
|
+
return [sort_field, :desc]
|
2577
|
+
end
|
2578
|
+
|
2579
|
+
#-----------------------------------------------------------------------
|
2580
|
+
# initialize
|
2581
|
+
#-----------------------------------------------------------------------
|
2582
|
+
def initialize(table, filter, filter_types, *args)
|
2583
|
+
@table = table
|
2584
|
+
@filter = filter
|
2585
|
+
@filter_types = filter_types
|
2586
|
+
super(*args)
|
2587
|
+
|
2588
|
+
@filter.each do |f|
|
2589
|
+
get_meth_str = <<-END_OF_STRING
|
2590
|
+
def #{f}()
|
2591
|
+
if defined?(@#{f}) then
|
2592
|
+
return @#{f}
|
2593
|
+
else
|
2594
|
+
@#{f} = self.collect { |x| x.#{f} }
|
2595
|
+
return @#{f}
|
2596
|
+
end
|
2597
|
+
end
|
2598
|
+
END_OF_STRING
|
2599
|
+
self.class.class_eval(get_meth_str)
|
2600
|
+
end
|
2601
|
+
end
|
2602
|
+
|
2603
|
+
#-----------------------------------------------------------------------
|
2604
|
+
# to_ary
|
2605
|
+
#-----------------------------------------------------------------------
|
2606
|
+
def to_ary
|
2607
|
+
to_a
|
2608
|
+
end
|
2609
|
+
|
2610
|
+
#-----------------------------------------------------------------------
|
2611
|
+
# set
|
2612
|
+
#-----------------------------------------------------------------------
|
2613
|
+
#++
|
2614
|
+
# Update record(s) in table, return number of records updated.
|
2615
|
+
#
|
2616
|
+
def set(*updates, &update_cond)
|
2617
|
+
raise 'Cannot specify both a hash and a proc for method #set!' \
|
2618
|
+
unless updates.empty? or update_cond.nil?
|
2619
|
+
|
2620
|
+
raise 'Must specify update proc or hash for method #set!' if \
|
2621
|
+
updates.empty? and update_cond.nil?
|
2622
|
+
|
2623
|
+
if updates.empty?
|
2624
|
+
@table.set(self, update_cond)
|
2625
|
+
else
|
2626
|
+
@table.set(self, updates)
|
2627
|
+
end
|
2628
|
+
end
|
2629
|
+
|
2630
|
+
#-----------------------------------------------------------------------
|
2631
|
+
# sort
|
2632
|
+
#-----------------------------------------------------------------------
|
2633
|
+
def sort(*sort_fields)
|
2634
|
+
sort_fields_arrs = []
|
2635
|
+
sort_fields.each do |f|
|
2636
|
+
if f.to_s[0..0] == '-'
|
2637
|
+
sort_fields_arrs << [f.to_s[1..-1].to_sym, :desc]
|
2638
|
+
elsif f.to_s[0..0] == '+'
|
2639
|
+
sort_fields_arrs << [f.to_s[1..-1].to_sym, :asc]
|
2640
|
+
else
|
2641
|
+
sort_fields_arrs << [f, :asc]
|
2642
|
+
end
|
2643
|
+
end
|
2644
|
+
|
2645
|
+
sort_fields_arrs.each do |f|
|
2646
|
+
raise "Invalid sort field" unless @filter.include?(f[0])
|
2647
|
+
end
|
2648
|
+
|
2649
|
+
super() { |a,b|
|
2650
|
+
x = []
|
2651
|
+
y = []
|
2652
|
+
sort_fields_arrs.each do |s|
|
2653
|
+
if [:Integer, :Float].include?(
|
2654
|
+
@filter_types[@filter.index(s[0])])
|
2655
|
+
a_value = a.send(s[0]) || 0
|
2656
|
+
b_value = b.send(s[0]) || 0
|
2657
|
+
else
|
2658
|
+
a_value = a.send(s[0])
|
2659
|
+
b_value = b.send(s[0])
|
2660
|
+
end
|
2661
|
+
if s[1] == :desc
|
2662
|
+
x << b_value
|
2663
|
+
y << a_value
|
2664
|
+
else
|
2665
|
+
x << a_value
|
2666
|
+
y << b_value
|
2667
|
+
end
|
2668
|
+
end
|
2669
|
+
x <=> y
|
2670
|
+
}
|
2671
|
+
end
|
2672
|
+
|
2673
|
+
#-----------------------------------------------------------------------
|
2674
|
+
# to_report
|
2675
|
+
#-----------------------------------------------------------------------
|
2676
|
+
def to_report(recs_per_page=0, print_rec_sep=false)
|
2677
|
+
result = collect { |r| @filter.collect {|f| r.send(f)} }
|
2678
|
+
|
2679
|
+
# How many records before a formfeed.
|
2680
|
+
delim = ' | '
|
2681
|
+
|
2682
|
+
# columns of physical rows
|
2683
|
+
columns = [@filter].concat(result).transpose
|
2684
|
+
|
2685
|
+
max_widths = columns.collect { |c|
|
2686
|
+
c.max { |a,b| a.to_s.length <=> b.to_s.length }.to_s.length
|
2687
|
+
}
|
2688
|
+
|
2689
|
+
row_dashes = '-' * (max_widths.inject {|sum, n| sum + n} +
|
2690
|
+
delim.length * (max_widths.size - 1))
|
2691
|
+
|
2692
|
+
justify_hash = { :String => :ljust, :Integer => :rjust,
|
2693
|
+
:Float => :rjust, :Boolean => :ljust, :Date => :ljust,
|
2694
|
+
:Time => :ljust, :DateTime => :ljust }
|
2695
|
+
|
2696
|
+
header_line = @filter.zip(max_widths, @filter.collect { |f|
|
2697
|
+
@filter_types[@filter.index(f)] }).collect { |x,y,z|
|
2698
|
+
x.to_s.send(justify_hash[z], y) }.join(delim)
|
2699
|
+
|
2700
|
+
output = ''
|
2701
|
+
recs_on_page_cnt = 0
|
2702
|
+
|
2703
|
+
result.each do |row|
|
2704
|
+
if recs_on_page_cnt == 0
|
2705
|
+
output << header_line + "\n" << row_dashes + "\n"
|
2706
|
+
end
|
2707
|
+
|
2708
|
+
output << row.zip(max_widths, @filter.collect { |f|
|
2709
|
+
@filter_types[@filter.index(f)] }).collect { |x,y,z|
|
2710
|
+
x.to_s.send(justify_hash[z], y) }.join(delim) + "\n"
|
2711
|
+
|
2712
|
+
output << row_dashes + '\n' if print_rec_sep
|
2713
|
+
recs_on_page_cnt += 1
|
2714
|
+
|
2715
|
+
if recs_per_page > 0 and (recs_on_page_cnt ==
|
2716
|
+
num_recs_per_page)
|
2717
|
+
output << '\f'
|
2718
|
+
recs_on_page_count = 0
|
2719
|
+
end
|
2720
|
+
end
|
2721
|
+
return output
|
2722
|
+
end
|
2723
|
+
end
|
2724
|
+
|
2725
|
+
|
2726
|
+
#---------------------------------------------------------------------------
|
2727
|
+
# Object
|
2728
|
+
#---------------------------------------------------------------------------
|
2729
|
+
class Object
|
2730
|
+
def full_const_get(name)
|
2731
|
+
list = name.split("::")
|
2732
|
+
obj = Object
|
2733
|
+
list.each {|x| obj = obj.const_get(x) }
|
2734
|
+
obj
|
2735
|
+
end
|
2736
|
+
end
|
2737
|
+
|
2738
|
+
|
2739
|
+
#---------------------------------------------------------------------------
|
2740
|
+
# NilClass
|
2741
|
+
#---------------------------------------------------------------------------
|
2742
|
+
class NilClass
|
2743
|
+
#-----------------------------------------------------------------------
|
2744
|
+
# method_missing
|
2745
|
+
#-----------------------------------------------------------------------
|
2746
|
+
#
|
2747
|
+
# This code is necessary because if, inside a select condition code
|
2748
|
+
# block, there is a case where you are trying to do an expression
|
2749
|
+
# against a table field that is equal to nil, I don't want a method
|
2750
|
+
# missing exception to occur. I just want the expression to be nil. I
|
2751
|
+
# initially had this method returning false, but then I had an issue
|
2752
|
+
# where I had a YAML field that was supposed to hold an Array. If the
|
2753
|
+
# field was empty (i.e. nil) it was actually returning false when it
|
2754
|
+
# should be returning nil. Since nil evaluates to false, it works if I
|
2755
|
+
# return nil.
|
2756
|
+
# Here's an example:
|
2757
|
+
# #select { |r| r.speed > 300 }
|
2758
|
+
# What happens if speed is nil (basically NULL in DBMS terms)? Without
|
2759
|
+
# this code, an exception is going to be raised, which is not what we
|
2760
|
+
# really want. We really want this expression to return nil.
|
2761
|
+
def method_missing(method_id, *stuff)
|
2762
|
+
return nil
|
2763
|
+
end
|
2764
|
+
end
|
2765
|
+
|
2766
|
+
|
2767
|
+
#---------------------------------------------------------------------------
|
2768
|
+
# Symbol
|
2769
|
+
#---------------------------------------------------------------------------
|
2770
|
+
class Symbol
|
2771
|
+
#-----------------------------------------------------------------------
|
2772
|
+
# -@
|
2773
|
+
#-----------------------------------------------------------------------
|
2774
|
+
#
|
2775
|
+
# This allows you to put a minus sign in front of a field name in order
|
2776
|
+
# to specify descending sort order.
|
2777
|
+
def -@
|
2778
|
+
("-"+self.to_s).to_sym
|
2779
|
+
end
|
2780
|
+
|
2781
|
+
#-----------------------------------------------------------------------
|
2782
|
+
# +@
|
2783
|
+
#-----------------------------------------------------------------------
|
2784
|
+
#
|
2785
|
+
# This allows you to put a plus sign in front of a field name in order
|
2786
|
+
# to specify ascending sort order.
|
2787
|
+
def +@
|
2788
|
+
("+"+self.to_s).to_sym
|
2789
|
+
end
|
2790
|
+
end
|