audrey 0.2 → 0.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,1609 @@
1
+ require 'sqlite3'
2
+ require 'json'
3
+
4
+ #===============================================================================
5
+ # Audrey::Engine::SQLite3
6
+ #
7
+ class Audrey::Engine::SQLite3 < Audrey::Engine
8
+ attr_reader :dbh
9
+ attr_reader :transactions
10
+ attr_reader :path
11
+ attr_reader :preps
12
+ attr_accessor :partition
13
+ extend Forwardable
14
+
15
+
16
+ #---------------------------------------------------------------------------
17
+ # initialize
18
+ #
19
+ def initialize(p_path, p_mode, opts={})
20
+ # default options
21
+ opts = {'immediate_commit'=>true}.merge(opts)
22
+
23
+ # set some properties
24
+ @path = p_path
25
+ @immediate_commit = opts['immediate_commit'] ? true : false
26
+ @preps = {}
27
+ @mode = p_mode
28
+
29
+ # KLUDGE: For now, default to partition 'm'
30
+ @partition = 'm'
31
+
32
+ # lock file
33
+ set_lock()
34
+
35
+ # if file exists
36
+ existed = File.exist?(@path)
37
+
38
+ # open database handle
39
+ @dbh = get_dbh()
40
+
41
+ # custom functions
42
+ custom_functions()
43
+
44
+ # create and structure database if the file doesn't exist
45
+ if not existed
46
+ require 'audrey/engine/sqlite3/init'
47
+ Audrey::Engine::SQLite3::Init.build @dbh
48
+ end
49
+
50
+ # transactions
51
+ @transactions = []
52
+
53
+ # initial transaction
54
+ if @immediate_commit
55
+ @transactions.push Audrey::Engine::SQLite3::Transaction::AutoCommit.new(self)
56
+ else
57
+ @transactions.push Audrey::Engine::SQLite3::Transaction::RC.new(self)
58
+ end
59
+
60
+ # create stuff just for this session
61
+ self.class.session_tables @dbh
62
+ end
63
+ #
64
+ # initialize
65
+ #---------------------------------------------------------------------------
66
+
67
+
68
+ #---------------------------------------------------------------------------
69
+ # get_dbh
70
+ #
71
+ def get_dbh
72
+ # get database handle
73
+ rv = ::SQLite3::Database.new(@path)
74
+
75
+ # enforce referential integrity
76
+ rv.execute 'pragma foreign_keys=on'
77
+
78
+ # results as hash
79
+ rv.results_as_hash = true
80
+
81
+ # return
82
+ return rv
83
+ end
84
+ #
85
+ # get_dbh
86
+ #---------------------------------------------------------------------------
87
+
88
+
89
+ #---------------------------------------------------------------------------
90
+ # commit, rollback
91
+ #
92
+ def commit
93
+ return @transactions[0].commit
94
+ end
95
+
96
+ def rollback
97
+ return @transactions[0].rollback
98
+ end
99
+ #
100
+ # commit, rollback
101
+ #---------------------------------------------------------------------------
102
+
103
+
104
+ #---------------------------------------------------------------------------
105
+ # in_transaction?
106
+ #
107
+ def in_transaction?
108
+ # loop through transactions
109
+ @transactions.each do |tr|
110
+ tr.is_a?(Audrey::Engine::SQLite3::Transaction::RC) and return true
111
+ end
112
+
113
+ # else not in transaction
114
+ return false
115
+ end
116
+ #
117
+ # in_transaction?
118
+ #---------------------------------------------------------------------------
119
+
120
+
121
+ #---------------------------------------------------------------------------
122
+ # transaction
123
+ #
124
+ def transaction(opts={})
125
+ # $tm.hrm
126
+ tr = Audrey::Engine::SQLite3::Transaction::RC.new(self)
127
+ @transactions.push tr
128
+
129
+ # block
130
+ if block_given?
131
+ begin
132
+ # yield
133
+ yield tr
134
+
135
+ # autocommit if necessary
136
+ if opts['autocommit']
137
+ tr.commit
138
+ end
139
+
140
+ # exit transaction
141
+ rescue Audrey::Exception::TransactionExit => e
142
+ unless e.tr == tr
143
+ raise e
144
+ end
145
+
146
+ # return out of this method
147
+ return nil
148
+ ensure
149
+ tr.terminate
150
+ @transactions.pop
151
+ end
152
+
153
+ # return nil
154
+ return nil
155
+
156
+ # non-block
157
+ else
158
+ return tr
159
+ end
160
+ end
161
+ #
162
+ # transaction
163
+ #---------------------------------------------------------------------------
164
+
165
+
166
+ #---------------------------------------------------------------------------
167
+ # reset_savepoints
168
+ #
169
+ def reset_savepoints(ftr)
170
+ found = false
171
+
172
+ @transactions.each do |tr|
173
+ if tr == ftr
174
+ found = true
175
+ end
176
+
177
+ if found
178
+ tr.start
179
+ end
180
+ end
181
+ end
182
+ #
183
+ # reset_savepoints
184
+ #---------------------------------------------------------------------------
185
+
186
+
187
+ #---------------------------------------------------------------------------
188
+ # close
189
+ #
190
+ def close(opts={})
191
+ # $tm.hrm
192
+
193
+ # close prepared statements
194
+ @preps.keys.each do |key|
195
+ stmt = @preps[key]
196
+
197
+ if stmt and (not stmt.closed?)
198
+ stmt.close
199
+ end
200
+
201
+ @preps.delete key
202
+ end
203
+
204
+ # purge
205
+ if opts['purge']
206
+ purge()
207
+ end
208
+
209
+ # close database handle
210
+ if not @dbh.closed?
211
+ @dbh.close
212
+ end
213
+
214
+ # unlock file
215
+ if @lock
216
+ @lock.flock File::LOCK_UN
217
+ @lock.close
218
+ end
219
+ end
220
+ #
221
+ # close
222
+ #---------------------------------------------------------------------------
223
+
224
+
225
+ #---------------------------------------------------------------------------
226
+ # custom_functions
227
+ #
228
+ def custom_functions()
229
+ #-----------------------------------------------------------------------
230
+ # partition
231
+ #
232
+ @dbh.create_function('current_partition', 0) do |func|
233
+ func.result = @partition
234
+ end
235
+ #
236
+ # partition
237
+ #-----------------------------------------------------------------------
238
+
239
+
240
+ #-----------------------------------------------------------------------
241
+ # insert_object_journal
242
+ #
243
+ @dbh.create_function('insert_object_journal', 4 ) do |func, partition, root, fclass, scalar, xt|
244
+ # $tm.hrm
245
+
246
+ # translate scalars to objects for storage in JSON
247
+ if converter = Audrey::AUDREY_SCALAR_TO_RUBY[fclass]
248
+ scalar = converter.call(scalar)
249
+ end
250
+
251
+ # initialize
252
+ rv = {}
253
+ rv['fc'] = fclass
254
+ rv['pt'] = partition
255
+
256
+ # scalar
257
+ if rv['s'].is_a?(String)
258
+ rv['s'] = scalar.force_encoding('UTF-8')
259
+ end
260
+
261
+ # xt
262
+ if xt
263
+ rv['xt'] = xt
264
+ end
265
+
266
+ # root
267
+ if root == 1
268
+ rv['root'] = true
269
+ end
270
+
271
+ # set return result
272
+ func.result = JSON.generate(rv)
273
+ end
274
+ #
275
+ # insert_object_journal
276
+ #-----------------------------------------------------------------------
277
+
278
+
279
+ #-----------------------------------------------------------------------
280
+ # fclass_fco, fclass_isolate
281
+ #
282
+ @dbh.create_function('fclass_fco', 1) do |func, fclass|
283
+ func.result = Audrey::Util.fclass_fco(fclass) ? 1 : 0
284
+ end
285
+
286
+ @dbh.create_function('fclass_isolate', 1) do |func, fclass|
287
+ func.result = Audrey::Util.fclass_isolate(fclass) ? 1 : 0
288
+ end
289
+ #
290
+ # fclass_fco, fclass_isolate
291
+ #-----------------------------------------------------------------------
292
+
293
+
294
+ #-----------------------------------------------------------------------
295
+ # graph_field
296
+ #
297
+ @dbh.create_function('graph_field', 2) do |func, fclass, hkey|
298
+ func.result = Audrey::Util.graph_field?(fclass, hkey) ? 1 : 0
299
+ end
300
+ #
301
+ # graph_field
302
+ #-----------------------------------------------------------------------
303
+
304
+
305
+ #-----------------------------------------------------------------------
306
+ # change_to_true
307
+ #
308
+ # @dbh.create_function('change_to_true', 1) do |func, fclass|
309
+ # func.result = 1
310
+ # end
311
+ #
312
+ # change_to_true
313
+ #-----------------------------------------------------------------------
314
+
315
+
316
+ #-----------------------------------------------------------------------
317
+ # custom_class
318
+ #
319
+ @dbh.create_function('custom_class', 1) do |func, fclass|
320
+ func.result = Audrey::Util.custom_class?(fclass) ? 1 : 0
321
+ end
322
+ #
323
+ # custom_class
324
+ #-----------------------------------------------------------------------
325
+
326
+
327
+ #-----------------------------------------------------------------------
328
+ # debug
329
+ #
330
+ @dbh.create_function('debug', 1) do |func, msg|
331
+ puts msg
332
+ func.result = 1
333
+ end
334
+ #
335
+ # debug
336
+ #-----------------------------------------------------------------------
337
+ end
338
+ #
339
+ # custom_functions
340
+ #---------------------------------------------------------------------------
341
+
342
+
343
+ #---------------------------------------------------------------------------
344
+ # purge
345
+ #
346
+ def purge
347
+ # $tm.hrm
348
+
349
+ if @mode.write
350
+ sql = 'delete from objects where pk in (select pk from fco_not_traced)'
351
+ @dbh.execute sql
352
+ end
353
+ end
354
+ #
355
+ # purge
356
+ #---------------------------------------------------------------------------
357
+
358
+
359
+ #---------------------------------------------------------------------------
360
+ # root_pk
361
+ #
362
+ def root_pk
363
+ sql = 'select pk from objects where root and partition=:partition'
364
+ return @dbh.get_first_value(sql, 'partition'=>@partition)
365
+ end
366
+ #
367
+ # root_pk
368
+ #---------------------------------------------------------------------------
369
+
370
+
371
+ #---------------------------------------------------------------------------
372
+ # insert_object
373
+ #
374
+ def insert_object(object, opts={})
375
+ # $tm.hrm
376
+
377
+ # pk
378
+ pk = opts['pk'] || Audrey::Util.uuid
379
+
380
+ # if scalar
381
+ if dfn = Audrey::Engine::SQLite3::RUBY_SCALAR_TO_SQLITE[object.class]
382
+ if not @preps['insert_scalar']
383
+ sql = <<~'SQL'
384
+ insert into objects(pk, partition, bclass, fclass, scalar)
385
+ values(:pk, :partition, :bclass, :fclass, :scalar)
386
+ SQL
387
+
388
+ @preps['insert_scalar'] = @dbh.prepare(sql)
389
+ end
390
+
391
+ # insert record
392
+ simple_exec(
393
+ @preps['insert_scalar'],
394
+ 'pk' => pk,
395
+ 'partition' => @partition,
396
+ 'bclass' => dfn['fclass'].bclass.to_s,
397
+ 'fclass' => dfn['fclass'].to_s,
398
+ 'scalar' => dfn['to_db'].call(object)
399
+ );
400
+
401
+ # if hash or array
402
+ elsif dfn = Audrey::RUBY_OBJECT_TO_AUDREY[object.class] and dfn['nclass']
403
+ simple_exec(
404
+ prepare_insert_collection(),
405
+ 'pk' => pk,
406
+ 'partition' => @partition,
407
+ 'bclass' => dfn['fclass'].bclass.to_s,
408
+ 'fclass' => dfn['fclass'].to_s
409
+ );
410
+
411
+ # if custom object
412
+ elsif object.is_a?(Audrey::Object::Custom)
413
+ simple_exec(
414
+ prepare_insert_collection(),
415
+ 'pk' => pk,
416
+ 'partition' => @partition,
417
+ 'bclass' => object.class.bclass.to_s,
418
+ 'fclass' => object.class.to_s,
419
+ );
420
+
421
+ # else unknown
422
+ else
423
+ raise 'unknown-object-class: ' + object.class.to_s
424
+ end
425
+
426
+ # return pk
427
+ return pk
428
+ end
429
+ #
430
+ # insert_object
431
+ #---------------------------------------------------------------------------
432
+
433
+
434
+ #---------------------------------------------------------------------------
435
+ # delete_object
436
+ #
437
+ def delete_object(pk)
438
+ # $tm.hrm
439
+
440
+ # ensure statement handle
441
+ if not @preps['delete_object']
442
+ sql = <<~'SQL'
443
+ delete from objects
444
+ where pk=:pk and partition=:partition
445
+ SQL
446
+
447
+ @preps['delete_object'] = @dbh.prepare(sql)
448
+ end
449
+
450
+ # execute
451
+ simple_exec @preps['delete_object'], 'pk' => pk, 'partition' => @partition
452
+ end
453
+ #
454
+ # delete_object
455
+ #---------------------------------------------------------------------------
456
+
457
+
458
+ #---------------------------------------------------------------------------
459
+ # add_xt
460
+ #
461
+ def add_xt(obj_pk, xt_pk)
462
+ # $tm.hrm
463
+
464
+ # prepare insert statement if necessary
465
+ if not @preps['insert_xt']
466
+ sql = <<~SQL
467
+ insert into objects(pk, partition, bclass, fclass)
468
+ values(?, (select partition from objects where pk=?), 'Audrey::Object::Hash', 'xt')
469
+ SQL
470
+
471
+ @preps['insert_xt'] ||= @dbh.prepare(sql)
472
+ end
473
+
474
+ # create xt
475
+ simple_exec @preps['insert_xt'], xt_pk, obj_pk
476
+
477
+ # associate record
478
+ @preps['add_xt'] ||= @dbh.prepare('update objects set xt=:xtpk where pk=:pk and partition=:partition')
479
+ simple_exec @preps['add_xt'], 'xtpk'=>xt_pk, 'pk'=>obj_pk, 'partition'=>@partition
480
+ end
481
+ #
482
+ # add_xt
483
+ #---------------------------------------------------------------------------
484
+
485
+
486
+ #---------------------------------------------------------------------------
487
+ # get_xt
488
+ #
489
+ def get_xt(obj_pk)
490
+ # prepare statement if necessary
491
+ @preps['existing_xt'] ||= @dbh.prepare('select xt from objects where pk=:pk and partition=:partition')
492
+
493
+ # search
494
+ @preps['existing_xt'].execute('pk'=>obj_pk, 'partition'=>@partition).each do |row|
495
+ return row['xt']
496
+ end
497
+
498
+ # create
499
+ xt_pk = Audrey::Util.uuid
500
+ add_xt obj_pk, xt_pk
501
+
502
+ # return
503
+ return xt_pk
504
+ ensure
505
+ @preps['existing_xt'] and @preps['existing_xt'].reset!
506
+ end
507
+ #
508
+ # get_xt
509
+ #---------------------------------------------------------------------------
510
+
511
+
512
+ #---------------------------------------------------------------------------
513
+ # add_relationship
514
+ #
515
+ def add_relationship(parent_pk, child_pk, opts={})
516
+ # ensure prepared statement
517
+ if not @preps['add_relationship']
518
+ sql = <<~SQL
519
+ insert into relationships(pk, parent, child, ord, hkey)
520
+ values(
521
+ :pk,
522
+ :parent,
523
+ :child,
524
+
525
+ case
526
+ when :ord is null then
527
+ coalesce((select max(ord) from relationships where parent=parent), 0) + 1
528
+ else :ord
529
+ end,
530
+ :hkey)
531
+ SQL
532
+
533
+ @preps['add_relationship'] = @dbh.prepare(sql)
534
+ end
535
+
536
+ # uuid for new relationship record
537
+ pk = Audrey::Util.uuid
538
+
539
+ # execute
540
+ begin
541
+ @preps['add_relationship'].execute(
542
+ sql,
543
+ 'pk'=>pk,
544
+ 'parent'=>parent_pk,
545
+ 'child'=>child_pk,
546
+ 'ord'=>opts['ord'],
547
+ 'hkey'=>opts['hkey']
548
+ );
549
+ ensure
550
+ @preps['add_relationship'].reset!
551
+ end
552
+
553
+ # return
554
+ return pk
555
+ end
556
+ #
557
+ # add_relationship
558
+ #---------------------------------------------------------------------------
559
+
560
+
561
+ #---------------------------------------------------------------------------
562
+ # hash_has_key?
563
+ #
564
+ def hash_has_key?(parent_pk, key)
565
+ # $tm.hrm
566
+ # puts "parent_pk: #{parent_pk}"
567
+ # puts "key: #{key}"
568
+
569
+ # prepare
570
+ if not @preps['hash_has_key']
571
+ sql = <<~'SQL'
572
+ select count(*) as count
573
+ from relationships
574
+ where parent=:parent and hkey=:hkey
575
+ SQL
576
+
577
+ @preps['hash_has_key'] ||= @dbh.prepare(sql)
578
+ end
579
+
580
+ # get count
581
+ @preps['hash_has_key'].execute('parent'=>parent_pk, 'hkey'=>key).each_hash do |row|
582
+ return row['count'] > 0
583
+ end
584
+ ensure
585
+ @preps['hash_has_key'] and @preps['hash_has_key'].reset!
586
+ end
587
+ #
588
+ # hash_has_key?
589
+ #---------------------------------------------------------------------------
590
+
591
+
592
+
593
+ #---------------------------------------------------------------------------
594
+ # row_hash_by_key
595
+ #
596
+ def row_hash_by_key(parent_pk, hkey)
597
+ # $tm.hrm
598
+
599
+ # ensure statement handle
600
+ if not @preps['row_hash_by_key']
601
+ sql = <<~SQL
602
+ select * from objects
603
+ where pk =
604
+ (select child from relationships where parent=:parent and hkey=:hkey) and
605
+ partition=:partition
606
+ SQL
607
+
608
+ @preps['row_hash_by_key'] = @dbh.prepare(sql)
609
+ end
610
+
611
+ # params
612
+ params = {}
613
+ params['parent'] = parent_pk
614
+ params['hkey'] = hkey
615
+ params['partition'] = @partition
616
+
617
+ # get record
618
+ @preps['row_hash_by_key'].execute(params).each_hash do |row|
619
+ return self.class.set_row_scalar(row.to_h)
620
+ end
621
+
622
+ ensure
623
+ @preps['row_hash_by_key'] and @preps['row_hash_by_key'].reset!
624
+ end
625
+ #
626
+ # row_hash_by_key
627
+ #---------------------------------------------------------------------------
628
+
629
+
630
+
631
+ #---------------------------------------------------------------------------
632
+ # hash_element_delete
633
+ #
634
+ def hash_element_delete(parent_pk, key)
635
+ # $tm.hrm
636
+
637
+ # get record
638
+ record = row_hash_by_key(parent_pk, key)
639
+
640
+ # execute
641
+ if record
642
+ if not @preps['delete_hash_key']
643
+ sql = <<~'SQL'
644
+ delete from relationships
645
+ where parent=:parent and hkey=:hkey
646
+ SQL
647
+
648
+ @preps['delete_hash_key'] = @dbh.prepare(sql)
649
+ end
650
+
651
+ simple_exec @preps['delete_hash_key'], 'parent'=>parent_pk, 'hkey'=>key
652
+ end
653
+
654
+ # return
655
+ return record
656
+ end
657
+ #
658
+ # hash_element_delete
659
+ #---------------------------------------------------------------------------
660
+
661
+
662
+ #---------------------------------------------------------------------------
663
+ # row_by_pk
664
+ #
665
+ def row_by_pk(pk)
666
+ # $tm.hrm
667
+
668
+ # ensure prepared statement
669
+ @preps['row_by_pk'] ||= @dbh.prepare('select * from objects where pk=:pk and partition=:partition')
670
+
671
+ # get record
672
+ @preps['row_by_pk'].execute('pk'=>pk, 'partition'=>@partition).each_hash do |row|
673
+ return self.class.set_row_scalar(row.to_h)
674
+ end
675
+
676
+ # didn't find the object, so return nil
677
+ return nil
678
+
679
+ ensure
680
+ @preps['row_by_pk'] and @preps['row_by_pk'].reset!
681
+ end
682
+ #
683
+ # row_by_pk
684
+ #---------------------------------------------------------------------------
685
+
686
+
687
+ #---------------------------------------------------------------------------
688
+ # row_by_array_index
689
+ #
690
+ def row_by_array_index(parent_pk, idx)
691
+ # $tm.hrm
692
+
693
+ # ensure statement handle
694
+ if not @preps['row_by_array_index']
695
+ sql = <<~SQL
696
+ select r.ord as ord, o.*
697
+ from relationships r, objects o
698
+ where r.parent=:parent and o.pk=r.child
699
+ order by r.ord
700
+ limit 1
701
+ offset :offset
702
+ SQL
703
+
704
+ @preps['row_by_array_index'] = @dbh.prepare(sql)
705
+ end
706
+
707
+ # get record
708
+ @preps['row_by_array_index'].execute('parent'=>parent_pk, 'offset'=>idx).each_hash do |row|
709
+ return self.class.set_row_scalar(row.to_h)
710
+ end
711
+
712
+ # else return nil
713
+ return nil
714
+ ensure
715
+ @preps['row_by_array_index'] and @preps['row_by_array_index'].reset!
716
+ end
717
+ #
718
+ # row_by_array_index
719
+ #---------------------------------------------------------------------------
720
+
721
+
722
+ #---------------------------------------------------------------------------
723
+ # hash_each
724
+ #
725
+ def hash_each(parent_pk)
726
+ # $tm.hrm
727
+ rv = nil
728
+
729
+ # loop through keys
730
+ hash_keys(parent_pk).each do |key|
731
+ if block_given?
732
+ yield key, row_hash_by_key(parent_pk, key)
733
+ else
734
+ rv ||= []
735
+ rv.push [key, row_hash_by_key(parent_pk, key)]
736
+ end
737
+ end
738
+
739
+ # return
740
+ return rv
741
+ end
742
+ #
743
+ # hash_each
744
+ #---------------------------------------------------------------------------
745
+
746
+
747
+
748
+ #---------------------------------------------------------------------------
749
+ # hash_keys
750
+ #
751
+ def hash_keys(parent_pk)
752
+ # $tm.hrm
753
+ rv = []
754
+
755
+ # prepare
756
+ @preps['hash_keys'] ||= @dbh.prepare('select hkey from relationships where parent=:parent order by ord')
757
+
758
+ # loop through records
759
+ @preps['hash_keys'].execute('parent'=>parent_pk).each_hash do |row|
760
+ rv.push row['hkey']
761
+ end
762
+
763
+ # return
764
+ return rv
765
+
766
+ ensure
767
+ @preps['hash_keys'] and @preps['hash_keys'].reset!
768
+ end
769
+ #
770
+ # hash_keys
771
+ #---------------------------------------------------------------------------
772
+
773
+
774
+ #---------------------------------------------------------------------------
775
+ # clear_collection
776
+ #
777
+ def clear_collection(parent_pk)
778
+ # $tm.hrm
779
+
780
+ # prepare
781
+ @preps['clear_collection'] ||= @dbh.prepare('delete from relationships where parent=:parent')
782
+
783
+ # execute
784
+ simple_exec @preps['clear_collection'], 'parent'=>parent_pk
785
+ end
786
+ #
787
+ # clear_collection
788
+ #---------------------------------------------------------------------------
789
+
790
+
791
+ #---------------------------------------------------------------------------
792
+ # changed_get
793
+ #
794
+ def changed_get(pk)
795
+ # $tm.hrm
796
+
797
+ # prepare
798
+ @preps['changed_get'] ||= @dbh.prepare('select changed from objects where pk=:pk')
799
+
800
+ # execute
801
+ @preps['changed_get'].execute('pk'=>pk).each_hash do |row|
802
+ return row['changed']
803
+ end
804
+ ensure
805
+ @preps['changed_get'] and @preps['changed_get'].reset!
806
+ end
807
+ #
808
+ # changed_get
809
+ #---------------------------------------------------------------------------
810
+
811
+
812
+ #---------------------------------------------------------------------------
813
+ # changed_set
814
+ #
815
+ def changed_set(pk, bool)
816
+ # $tm.hrm
817
+
818
+ # set to changed
819
+ if bool
820
+ @preps['changed_set_true'] ||= @dbh.prepare('update base_objects set changed=current_timestamp where pk=:pk')
821
+ simple_exec @preps['changed_set_true'], 'pk'=>pk
822
+ else
823
+ @preps['changed_set_false'] ||= @dbh.prepare('update base_objects set changed=null where pk=:pk')
824
+ simple_exec @preps['changed_set_false'], 'pk'=>pk
825
+ end
826
+ end
827
+ #
828
+ # changed_set
829
+ #---------------------------------------------------------------------------
830
+
831
+
832
+ #---------------------------------------------------------------------------
833
+ # collection_child_pks
834
+ #
835
+ def collection_child_pks(parent_pk)
836
+ # $tm.hrm
837
+
838
+ # prepare
839
+ if not @preps['collection_child_pks']
840
+ sql = <<~'SQL'
841
+ select child
842
+ from relationships
843
+ where parent=:parent
844
+ SQL
845
+
846
+ @preps['collection_child_pks'] ||= @dbh.prepare(sql)
847
+ end
848
+
849
+ # init
850
+ rv = []
851
+
852
+ # get count
853
+ @preps['collection_child_pks'].execute(parent_pk).each_hash do |row|
854
+ rv.push row['child']
855
+ end
856
+
857
+ # return
858
+ return rv
859
+ ensure
860
+ @preps['collection_child_pks'] and @preps['collection_child_pks'].reset!
861
+ end
862
+ #
863
+ # collection_child_pks
864
+ #---------------------------------------------------------------------------
865
+
866
+
867
+ #---------------------------------------------------------------------------
868
+ # collection_length
869
+ #
870
+ def collection_length(parent_pk)
871
+ # $tm.hrm
872
+
873
+ # prepare
874
+ if not @preps['collection_length']
875
+ sql = <<~'SQL'
876
+ select count(*) as count
877
+ from relationships
878
+ where parent=:parent
879
+ SQL
880
+
881
+ @preps['collection_length'] ||= @dbh.prepare(sql)
882
+ end
883
+
884
+ # get count
885
+ @preps['collection_length'].execute(parent_pk).each_hash do |row|
886
+ return row['count']
887
+ end
888
+ ensure
889
+ @preps['collection_length'] and @preps['collection_length'].reset!
890
+ end
891
+ #
892
+ # collection_length
893
+ #---------------------------------------------------------------------------
894
+
895
+
896
+ #---------------------------------------------------------------------------
897
+ # array_each
898
+ #
899
+ def array_each(parent_pk)
900
+ # $tm.hrm
901
+
902
+ # ensure statement handle
903
+ if not @preps['array_each']
904
+ sql = <<~SQL
905
+ select r.ord as ord, o.*
906
+ from relationships r, objects o
907
+ where r.parent=:parent and o.pk=r.child
908
+ order by r.ord
909
+ SQL
910
+
911
+ @preps['array_each'] = @dbh.prepare(sql)
912
+ end
913
+
914
+ # loop through records
915
+ @preps['array_each'].execute('parent'=>parent_pk).each_hash do |row|
916
+ yield row
917
+ end
918
+ ensure
919
+ @preps['array_each'] and @preps['array_each'].reset!
920
+ end
921
+ #
922
+ # array_each
923
+ #---------------------------------------------------------------------------
924
+
925
+
926
+ #---------------------------------------------------------------------------
927
+ # object_exists?
928
+ #
929
+ def object_exists?(pk)
930
+ # $tm.hrm
931
+
932
+ # ensure statement handle
933
+ if not @preps['object_exists']
934
+ sql = <<~'SQL'
935
+ select count(*) as count
936
+ from objects
937
+ where
938
+ pk=:pk and
939
+ partition=:partition
940
+ SQL
941
+
942
+ @preps['object_exists'] = @dbh.prepare(sql)
943
+ end
944
+
945
+ @preps['object_exists'].execute('pk'=>pk, 'partition'=>@partition).each do |row|
946
+ return row['count'] > 0
947
+ end
948
+ ensure
949
+ @preps['object_exists'] and @preps['object_exists'].reset!
950
+ end
951
+ #
952
+ # object_exists?
953
+ #---------------------------------------------------------------------------
954
+
955
+
956
+ #---------------------------------------------------------------------------
957
+ # ancestors
958
+ #
959
+ def ancestors(child_pk)
960
+ # sql to add parents
961
+ if not @preps['ancestors']
962
+ sql = <<~'SQL'
963
+ select *
964
+ from objects where pk in (
965
+ with recursive
966
+ ancestors(c) as (
967
+ values(:child_pk)
968
+ union
969
+ select parent from relationships, ancestors
970
+ where relationships.child=ancestors.c
971
+ )
972
+ select parent from relationships
973
+ where relationships.child in ancestors
974
+ );
975
+ SQL
976
+
977
+ @preps['ancestors'] = @dbh.prepare(sql)
978
+ end
979
+
980
+ # loop through results
981
+ @preps['ancestors'].execute('child_pk'=>child_pk).each do |row|
982
+ yield row
983
+ end
984
+ ensure
985
+ @preps['ancestors'] and @preps['ancestors'].reset!
986
+ end
987
+ #
988
+ # ancestors
989
+ #---------------------------------------------------------------------------
990
+
991
+
992
+ #---------------------------------------------------------------------------
993
+ # q0
994
+ #
995
+ def q0(p_fquery)
996
+ require 'audrey/engine/sqlite3/query/q0'
997
+ return Audrey::Engine::SQLite3::Query::Q0.new(self, p_fquery)
998
+ end
999
+ #
1000
+ # q0
1001
+ #---------------------------------------------------------------------------
1002
+
1003
+
1004
+ #---------------------------------------------------------------------------
1005
+ # import_csv
1006
+ #
1007
+ def import_csv(fclass, path, opts={})
1008
+ # $tm.hrm
1009
+ return Audrey::Engine::SQLite3::Importer::CSV.new(self, fclass, path, opts={})
1010
+ end
1011
+ #
1012
+ # import_csv
1013
+ #---------------------------------------------------------------------------
1014
+
1015
+
1016
+ #---------------------------------------------------------------------------
1017
+ # changed_objects
1018
+ #
1019
+ def changed_objects
1020
+ # $tm.hrm
1021
+
1022
+ # sql to add parents
1023
+ if not @preps['changed_objects']
1024
+ sql = <<~'SQL'
1025
+ select *
1026
+ from objects
1027
+ where
1028
+ changed and
1029
+ custom_class(fclass)
1030
+ order by
1031
+ changed
1032
+ SQL
1033
+
1034
+ @preps['changed_objects'] = @dbh.prepare(sql)
1035
+ end
1036
+
1037
+ # loop through results
1038
+ @preps['changed_objects'].execute().each do |row|
1039
+ yield row
1040
+ end
1041
+ ensure
1042
+ @preps['changed_objects'] and @preps['changed_objects'].reset!
1043
+ end
1044
+ #
1045
+ # changed_objects
1046
+ #---------------------------------------------------------------------------
1047
+
1048
+
1049
+ # private
1050
+ private
1051
+
1052
+
1053
+ #---------------------------------------------------------------------------
1054
+ # simple_exec
1055
+ #
1056
+ def simple_exec(stmt, *opts)
1057
+ begin
1058
+ stmt.execute(*opts)
1059
+ ensure
1060
+ stmt.reset!
1061
+ end
1062
+ end
1063
+ #
1064
+ # simple_exec
1065
+ #---------------------------------------------------------------------------
1066
+
1067
+
1068
+ #---------------------------------------------------------------------------
1069
+ # RUBY_SCALAR_TO_SQLITE
1070
+ #
1071
+ RUBY_SCALAR_TO_SQLITE = {}
1072
+
1073
+ RUBY_SCALAR_TO_SQLITE[::String] = {
1074
+ 'fclass' => Audrey::Object::Scalar::String,
1075
+ 'to_db' => Proc.new {|obj| obj}
1076
+ }
1077
+
1078
+ RUBY_SCALAR_TO_SQLITE[::Integer] = {
1079
+ 'fclass' => Audrey::Object::Scalar::Number,
1080
+ 'to_db' => Proc.new {|obj| obj.to_s}
1081
+ }
1082
+
1083
+ RUBY_SCALAR_TO_SQLITE[::Float] = RUBY_SCALAR_TO_SQLITE[::Integer]
1084
+
1085
+ RUBY_SCALAR_TO_SQLITE[::TrueClass] = {
1086
+ 'fclass' => Audrey::Object::Scalar::Boolean,
1087
+ 'to_db' => Proc.new {|obj| obj ? 1 : 0}
1088
+ }
1089
+
1090
+ RUBY_SCALAR_TO_SQLITE[::FalseClass] = RUBY_SCALAR_TO_SQLITE[::TrueClass]
1091
+
1092
+ RUBY_SCALAR_TO_SQLITE[::NilClass] = {
1093
+ 'fclass' => Audrey::Object::Scalar::Null,
1094
+ 'to_db' => Proc.new {|obj| nil}
1095
+ }
1096
+
1097
+ RUBY_SCALAR_TO_SQLITE.freeze
1098
+ #
1099
+ # RUBY_SCALAR_TO_SQLITE
1100
+ #---------------------------------------------------------------------------
1101
+
1102
+
1103
+ #---------------------------------------------------------------------------
1104
+ # prepare_insert_collection
1105
+ #
1106
+ def prepare_insert_collection
1107
+ if not @preps['insert_collection']
1108
+ sql = <<~'SQL'
1109
+ insert into objects(pk, partition, bclass, fclass)
1110
+ values(:pk, :partition, :bclass, :fclass)
1111
+ SQL
1112
+
1113
+ @preps['insert_collection'] = @dbh.prepare(sql)
1114
+ end
1115
+
1116
+ return @preps['insert_collection']
1117
+ end
1118
+ #
1119
+ # prepare_insert_collection
1120
+ #---------------------------------------------------------------------------
1121
+
1122
+
1123
+ #---------------------------------------------------------------------------
1124
+ # set_row_scalar
1125
+ #
1126
+ def self.set_row_scalar(row)
1127
+ # fix scalar if necessary
1128
+ if converter = Audrey::AUDREY_SCALAR_TO_RUBY[row['fclass']]
1129
+ row['scalar'] = converter.call(row['scalar'])
1130
+ end
1131
+
1132
+ # return
1133
+ return row
1134
+ end
1135
+ #
1136
+ # set_row_scalar
1137
+ #---------------------------------------------------------------------------
1138
+
1139
+
1140
+ #---------------------------------------------------------------------------
1141
+ # session_tables
1142
+ #
1143
+ def self.session_tables(dbh)
1144
+ # create temp tables
1145
+ # KLUDGE: query_rows references objects, but apparently you can't reference
1146
+ # from a temp table to a main table. For now just not enforcing referential
1147
+ # integrity.
1148
+ sql = <<~"SQL"
1149
+ create temp table connection(key varchar(5) primary key, val varchar(64));
1150
+ insert into connection(key, val) values('partition', null);
1151
+
1152
+ create temp table queries (pk varchar(5) primary key);
1153
+ create temp table subqueries (pk varchar(5) primary key, query not null references queries(pk));
1154
+
1155
+ create temp table query_rows (
1156
+ subquery varchar(5) not null references subqueries(pk) on delete cascade,
1157
+ parent varchar(36) not null, -- references objects(pk),
1158
+ unique(subquery, parent)
1159
+ );
1160
+ SQL
1161
+
1162
+ # execute
1163
+ dbh.execute_batch sql
1164
+ end
1165
+ #
1166
+ # session_tables
1167
+ #---------------------------------------------------------------------------
1168
+
1169
+
1170
+ #---------------------------------------------------------------------------
1171
+ # set_lock
1172
+ #
1173
+ def set_lock()
1174
+ # $tm.hrm
1175
+
1176
+ # open lock file
1177
+ @lock = File.open(@path + '.lock', File::RDWR|File::CREAT)
1178
+
1179
+ # exclusive
1180
+ if @mode.write
1181
+ @lock.flock File::LOCK_EX
1182
+
1183
+ # shared
1184
+ else
1185
+ @lock.flock File::LOCK_SH
1186
+ end
1187
+ end
1188
+ #
1189
+ # set_lock
1190
+ #---------------------------------------------------------------------------
1191
+ end
1192
+ #
1193
+ # Audrey::Engine::SQLite3
1194
+ #===============================================================================
1195
+
1196
+
1197
+ #===============================================================================
1198
+ # Audrey::Engine::SQLite3::Importer
1199
+ #
1200
+ class Audrey::Engine::SQLite3::Importer
1201
+ end
1202
+ #
1203
+ # Audrey::Engine::SQLite3::Importer
1204
+ #===============================================================================
1205
+
1206
+
1207
+ #===============================================================================
1208
+ # Audrey::Engine::SQLite3::Importer::CSV
1209
+ #
1210
+ class Audrey::Engine::SQLite3::Importer::CSV < Audrey::Engine::SQLite3::Importer
1211
+ attr_reader :fields
1212
+ attr_accessor :verbose
1213
+
1214
+ #---------------------------------------------------------------------------
1215
+ # initialize
1216
+ #
1217
+ def initialize(engine, fclass, path, opts={})
1218
+ # $tm.hrm
1219
+ require 'csv'
1220
+ @engine = engine
1221
+ @dbh = @engine.dbh
1222
+ @fclass = fclass.to_s
1223
+ @path = path
1224
+ @partition = 'm'
1225
+ @fields = {}
1226
+ @verbose = false
1227
+ end
1228
+ #
1229
+ # initialize
1230
+ #---------------------------------------------------------------------------
1231
+
1232
+
1233
+ #---------------------------------------------------------------------------
1234
+ # run
1235
+ #
1236
+ def run
1237
+ # $tm.hrm
1238
+ converters = {}
1239
+ count = 0
1240
+
1241
+ # set fclass primary keys
1242
+ fclass_pks()
1243
+
1244
+ # prepare statements
1245
+ prepare_statements()
1246
+
1247
+ # build converters
1248
+ @fields.each do |k, dfn|
1249
+ converters[k] = Audrey::AUDREY_SCALAR_TO_RUBY[dfn.to_s]
1250
+ end
1251
+
1252
+ # begin transaction
1253
+ @dbh.execute 'begin transaction'
1254
+
1255
+ # TESTING
1256
+ $tm.timer('import') do
1257
+ # loop through CSV
1258
+ ::CSV.foreach(@path, headers: true) do |row|
1259
+ # verbosify
1260
+ if @verbose
1261
+ count += 1
1262
+ puts count.to_s + ': ' + row.to_s
1263
+ end
1264
+
1265
+ # init collection ord
1266
+ ord = 0
1267
+
1268
+ # convert
1269
+ converters.each do |k, converter|
1270
+ if row.has_key?(k)
1271
+ row[k] = converter.call(row[k])
1272
+ end
1273
+ end
1274
+
1275
+ # object pk
1276
+ obj_pk = Audrey::Util.uuid
1277
+
1278
+ # add hash object
1279
+ @insert_hash.execute(
1280
+ 'pk'=>obj_pk,
1281
+ 'partition'=>@partition,
1282
+ 'bclass'=>@obj_bclass_pk,
1283
+ 'fclass'=>@obj_fclass_pk
1284
+ );
1285
+
1286
+ # add scalars and relationships
1287
+ row.each do |k,v|
1288
+ ord += 1
1289
+ scalar_uuid = Audrey::Util.uuid
1290
+
1291
+ # add scalar object
1292
+ @insert_scalar.execute(
1293
+ 'pk'=>scalar_uuid,
1294
+ 'partition'=>@partition,
1295
+ 'bclass'=>@scalar_bclass_pk,
1296
+ 'fclass'=>@bclasses[v.class],
1297
+ 'scalar'=>v
1298
+ );
1299
+
1300
+ # add relationship
1301
+ @insert_relationship.execute(
1302
+ 'pk'=>Audrey::Util.uuid,
1303
+ 'parent'=>obj_pk,
1304
+ 'child'=>scalar_uuid,
1305
+ 'hkey'=>k,
1306
+ 'ord'=>ord
1307
+ );
1308
+ end
1309
+ end
1310
+ end
1311
+
1312
+ # commit transaction
1313
+ $tm.timer('commit') do
1314
+ @dbh.execute 'commit'
1315
+ end
1316
+
1317
+ # close prepared statements
1318
+ ensure
1319
+ $tm.puts 'close prepared statements'
1320
+
1321
+ $tm.timer('close prepared statements') do
1322
+ @insert_hash and @insert_hash.close
1323
+ @insert_scalar and @insert_scalar.close
1324
+ @insert_relationship and @insert_relationship.close
1325
+ end
1326
+ end
1327
+ #
1328
+ # run
1329
+ #---------------------------------------------------------------------------
1330
+
1331
+
1332
+ #---------------------------------------------------------------------------
1333
+ # prepare_statements
1334
+ #
1335
+ def prepare_statements
1336
+ # $tm.hrm
1337
+
1338
+ # hash
1339
+ sql = <<~'SQL'
1340
+ insert into
1341
+ base_objects(
1342
+ pk,
1343
+ partition,
1344
+ bclass_pk,
1345
+ fclass_pk
1346
+ )
1347
+
1348
+ values(
1349
+ :pk,
1350
+ :partition,
1351
+ :bclass,
1352
+ :fclass
1353
+ );
1354
+ SQL
1355
+
1356
+ # prepare
1357
+ @insert_hash = @dbh.prepare(sql)
1358
+
1359
+ # scalar
1360
+ sql = <<~'SQL'
1361
+ insert into
1362
+ base_objects(
1363
+ pk,
1364
+ partition,
1365
+ bclass_pk,
1366
+ fclass_pk,
1367
+ scalar
1368
+ )
1369
+
1370
+ values(
1371
+ :pk,
1372
+ :partition,
1373
+ :bclass,
1374
+ :fclass,
1375
+ :scalar
1376
+ );
1377
+ SQL
1378
+
1379
+ # prepare
1380
+ @insert_scalar = @dbh.prepare(sql)
1381
+
1382
+ # relationship
1383
+ sql = <<~'SQL'
1384
+ insert into relationships(
1385
+ pk,
1386
+ parent,
1387
+ child,
1388
+ hkey,
1389
+ ord
1390
+ )
1391
+
1392
+ values(
1393
+ :pk,
1394
+ :parent,
1395
+ :child,
1396
+ :hkey,
1397
+ :ord
1398
+ )
1399
+ SQL
1400
+
1401
+ # prepare
1402
+ @insert_relationship = @dbh.prepare(sql)
1403
+ end
1404
+ #
1405
+ # prepare_statements
1406
+ #---------------------------------------------------------------------------
1407
+
1408
+
1409
+ #---------------------------------------------------------------------------
1410
+ # fclass_pks
1411
+ #
1412
+ def fclass_pks
1413
+ @bclasses = {}
1414
+
1415
+ # object bclass primary key
1416
+ @obj_bclass_pk = set_fclass_pk('Audrey::Object::Hash')
1417
+
1418
+ # object fclass primary key
1419
+ @obj_fclass_pk = set_fclass_pk(@fclass)
1420
+
1421
+ # scalar base class
1422
+ @scalar_bclass_pk = set_fclass_pk('Audrey::Object::Scalar')
1423
+
1424
+ # loop through scalar classes
1425
+ [String, Integer, Float, NilClass].each do |clss|
1426
+ @bclasses[clss] = set_fclass_pk(Audrey::RUBY_OBJECT_TO_AUDREY[clss]['fclass'].to_s)
1427
+ end
1428
+ end
1429
+ #
1430
+ # fclass_pks
1431
+ #---------------------------------------------------------------------------
1432
+
1433
+
1434
+ #---------------------------------------------------------------------------
1435
+ # set_fclass_pk
1436
+ #
1437
+ def set_fclass_pk(set_fclass)
1438
+ # $tm.hrm
1439
+
1440
+ # ensure fclass record
1441
+ sql = <<~'SQL'
1442
+ insert or ignore into
1443
+ fclasses(name)
1444
+ values(:fclass);
1445
+ SQL
1446
+
1447
+ # execute
1448
+ @dbh.execute sql, 'fclass'=>set_fclass
1449
+
1450
+ # get fclass pk
1451
+ sql = 'select pk from fclasses where name=:fclass'
1452
+ return @dbh.get_first_value(sql, 'fclass'=>set_fclass)
1453
+ end
1454
+ #
1455
+ # set_fclass_pk
1456
+ #---------------------------------------------------------------------------
1457
+
1458
+
1459
+ #---------------------------------------------------------------------------
1460
+ # set_bclass_pks
1461
+ #
1462
+ # def set_bclass_pks
1463
+ # $tm.hrm
1464
+ # end
1465
+ #
1466
+ # set_bclass_pks
1467
+ #---------------------------------------------------------------------------
1468
+ end
1469
+ #
1470
+ # Audrey::Engine::SQLite3::Importer::CSV
1471
+ #===============================================================================
1472
+
1473
+
1474
+ #===============================================================================
1475
+ # Audrey::Engine::SQLite3::Query
1476
+ #
1477
+ class Audrey::Engine::SQLite3::Query
1478
+ end
1479
+ #
1480
+ # Audrey::Engine::SQLite3::Query
1481
+ #===============================================================================
1482
+
1483
+
1484
+ #===============================================================================
1485
+ # Audrey::Engine::SQLite3::Transaction
1486
+ #
1487
+ class Audrey::Engine::SQLite3::Transaction < Audrey::Transaction
1488
+ #---------------------------------------------------------------------------
1489
+ # initialize
1490
+ #
1491
+ def initialize(p_engine)
1492
+ @engine = p_engine
1493
+ end
1494
+ #
1495
+ # initialize
1496
+ #---------------------------------------------------------------------------
1497
+ end
1498
+ #
1499
+ # Audrey::Engine::SQLite3::Transaction
1500
+ #===============================================================================
1501
+
1502
+
1503
+ #===============================================================================
1504
+ # Audrey::Engine::SQLite3::Transaction::AutoCommit
1505
+ #
1506
+ class Audrey::Engine::SQLite3::Transaction::AutoCommit < Audrey::Engine::SQLite3::Transaction
1507
+ def commit
1508
+ @engine.reset_savepoints self
1509
+ end
1510
+
1511
+ def start
1512
+ end
1513
+
1514
+ def rollback
1515
+ end
1516
+
1517
+ def release
1518
+ end
1519
+ end
1520
+ #
1521
+ # Audrey::Engine::SQLite3::Transaction::AutoCommit
1522
+ #===============================================================================
1523
+
1524
+
1525
+ #===============================================================================
1526
+ # Audrey::Engine::SQLite3::Transaction::RC
1527
+ # An object of this class represents a transaction that can be rolled back or
1528
+ # committed.
1529
+ #
1530
+ class Audrey::Engine::SQLite3::Transaction::RC < Audrey::Engine::SQLite3::Transaction
1531
+ attr_reader :savepoint
1532
+
1533
+ #---------------------------------------------------------------------------
1534
+ # initialize
1535
+ #
1536
+ def initialize(*opts)
1537
+ super(*opts)
1538
+ start()
1539
+ end
1540
+ #
1541
+ # initialize
1542
+ #---------------------------------------------------------------------------
1543
+
1544
+
1545
+ #---------------------------------------------------------------------------
1546
+ # start
1547
+ #
1548
+ def start
1549
+ @savepoint = Audrey::Util.randstr()
1550
+ @engine.dbh.execute "savepoint #{@savepoint}"
1551
+ end
1552
+ #
1553
+ # start
1554
+ #---------------------------------------------------------------------------
1555
+
1556
+
1557
+ #---------------------------------------------------------------------------
1558
+ # commit
1559
+ #
1560
+ def commit
1561
+ release()
1562
+ @engine.reset_savepoints self
1563
+ end
1564
+ #
1565
+ # commit
1566
+ #---------------------------------------------------------------------------
1567
+
1568
+
1569
+ #---------------------------------------------------------------------------
1570
+ # rollback
1571
+ #
1572
+ def rollback
1573
+ @engine.dbh.execute "rollback to #{@savepoint}"
1574
+ end
1575
+ #
1576
+ # rollback
1577
+ #---------------------------------------------------------------------------
1578
+
1579
+
1580
+ #---------------------------------------------------------------------------
1581
+ # terminate
1582
+ #
1583
+ def terminate
1584
+ rollback()
1585
+ release()
1586
+ end
1587
+ #
1588
+ # terminate
1589
+ #---------------------------------------------------------------------------
1590
+
1591
+
1592
+ # private
1593
+ private
1594
+
1595
+
1596
+ #---------------------------------------------------------------------------
1597
+ # release
1598
+ #
1599
+ def release
1600
+ @engine.dbh.execute "release #{@savepoint}"
1601
+ @savepoint = nil
1602
+ end
1603
+ #
1604
+ # release
1605
+ #---------------------------------------------------------------------------
1606
+ end
1607
+ #
1608
+ # Audrey::Engine::SQLite3::Transaction::RC
1609
+ #===============================================================================