INSTANCE METHODS: Override SQL based methods in ActiveRecord::Base Instance methods: everything invoked from records instances, e.g. book = Book.find(:first); book.destroy

Methods
Constants
SQL_FRAGMENT_TRANSLATIONS = [ [/1\s*=\s*1/, 'true'], ['rec.', ''], ['==', '='], [/(\w+)\s*=\s*/, 'rec.\1 == '], [/(\w+)\s*<>\s*?/, 'rec.\1 !='], [/(\w+)\s*<\s*?/, 'rec.\1 <'], [/(\w+)\s*>\s*?/, 'rec.\1 >'], [/(\w+)\s*IS\s+NOT\s*?/, 'rec.\1 !='], [/(\w+)\s*IS\s*?/, 'rec.\1 =='], [/(\w+)\s+IN\s+/, 'rec.\1.in'], [/\.id(\W)/i, '.recno\1'], ['<>', '!='], [/\bNULL\b/i, 'nil'], [/\bAND\b/i, 'and'], [/\bOR\b/i, 'or'], ["'%s'", '?'], ['%d', '?'], [/:\w+/, '?'], [/\bid\b/i, 'rec.recno'], ]
  TODO: handle LIKE
Public Class methods
begin_db_transaction()

NOT SUPPORTED!!!

     # File kirbybase_adapter.rb, line 674
674:     def self.begin_db_transaction
675:       raise ArgumentError, "#begin_db_transaction called"
676:       # connection.transaction

677:     end
build_conditions_block(conditions)

Builds the :conditions block from various forms of input.

  • Procs are passed as is
  • Arrays are assumed to be in the format of [‘name = ?’, ‘Assaph’]
  • Fragment String are translated to code Full SQL statements will raise an error
  • No parameters will assume a true for all records
     # File kirbybase_adapter.rb, line 607
607:     def self.build_conditions_block conditions
608:       case conditions
609:         when Proc   then conditions
610:         when Array  then parse_conditions_from_sql_array(conditions)
611:         when String
612:           if conditions.match(/^(SELECT|INSERT|DELETE|UPDATE)/i)
613:             raise ArgumentError, "KirbyBase does not support SQL for :conditions! '#{conditions.inspect}''"
614:           else
615:             conditions_string = translate_sql_to_code(conditions)
616:             lambda{|rec| eval conditions_string }
617:           end
618: 
619:         when nil
620:           if block_given?
621:             Proc.new
622:           else
623:             lambda{|r| true}
624:           end
625:       end # case conditions

626:     end
build_conditions_from_options(options)

One of the main methods: Assembles the :conditions block from the options argument (See build_conditions_block for actual translation). Then adds the scope and inheritance-type conditions (if present).

     # File kirbybase_adapter.rb, line 549
549:     def self.build_conditions_from_options options
550:       basic_selector_block = case options
551:         when Array
552:           if options[0].is_a? Proc
553:             options[0]
554:           elsif options.flatten.length == 1
555:             translate_sql_to_code options.flatten[0]
556:           else
557:             parse_conditions_from_sql_array options.flatten
558:           end
559: 
560:         when Hash
561:           build_conditions_block options[:conditions]
562: 
563:         when Proc
564:           options
565: 
566:         else
567:           raise ArgumentError, "Don't know how to process (#{options.inspect})"
568:       end
569: 
570:       selector_with_scope = if scope(:find, :conditions)
571:         scope_conditions_block = build_conditions_block(scope(:find, :conditions))
572:         lambda{|rec| basic_selector_block[rec] && scope_conditions_block[rec]}
573:       else
574:         basic_selector_block
575:       end
576: 
577:       conditions_block = if descends_from_active_record?
578:         selector_with_scope
579:       else
580:         untyped_conditions_block = selector_with_scope
581:         type_condition_block = type_condition(options.is_a?(Hash) ? options[:class_name] : nil)
582:         lambda{|rec| type_condition_block[rec] && untyped_conditions_block[rec]}
583:       end
584: 
585:       conditions_block
586:     end
commit_db_transaction()

NOT SUPPORTED!!!

     # File kirbybase_adapter.rb, line 680
680:     def self.commit_db_transaction
681:       raise ArgumentError, "#commit_db_transaction"
682:       # connection.commit

683:     end
construct_conditions_from_arguments(attribute_names, arguments)

Override of AR::Base SQL construction to build a conditions block. Used only by AR::Base#method_missing to support dynamic finders (e.g. find_by_name).

     # File kirbybase_adapter.rb, line 416
416:     def self.construct_conditions_from_arguments(attribute_names, arguments)
417:       conditions = []
418:       attribute_names.each_with_index { |name, idx| conditions << "#{name} #{attribute_condition(arguments[idx])} " }
419:       build_conditions_from_options :conditions => [ conditions.join(" and ").strip, *arguments[0...attribute_names.length] ]
420:     end
count(*args)

May also be called with a block, e.g.:

  Book.count {|rec| rec.author_id == @author.id}
     # File kirbybase_adapter.rb, line 661
661:     def self.count(*args)
662:       if args.compact.empty?
663:         if block_given?
664:           find(:all, :conditions => Proc.new).size
665:         else
666:           self.find(:all).size
667:         end
668:       else
669:         self.find(:all, :conditions => build_conditions_from_options(args)).size
670:       end
671:     end
count_by_sql(*args)

NOT SUPPORTED !!!

     # File kirbybase_adapter.rb, line 339
339:     def self.count_by_sql(*args)
340:       raise StatementInvalid, "SQL not Supported"
341:     end
db()

The KirbyBase object

     # File kirbybase_adapter.rb, line 298
298:     def self.db
299:       #db ||= connection.db

300:       connection.db
301:     end
decrement_counter(counter_name, ids)

Override of AR::Base that was using raw SQL

     # File kirbybase_adapter.rb, line 430
430:     def self.decrement_counter(counter_name, ids)
431:       [ids].flatten.each do |id|
432:         table.update{|rec| rec.recno == id }.set{ |rec| rec.send "#{counter_name}=", (rec.send(counter_name)-1) }
433:       end
434:     end
delete(ids)

Deletes the selected rows from the DB.

     # File kirbybase_adapter.rb, line 344
344:     def self.delete(ids)
345:       ids = [ids].flatten
346:       table.delete {|r| ids.include? r.recno }
347:     end
delete_all(conditions = nil)

Deletes the matching rows from the table. If no conditions are specified, will clear the whole table.

     # File kirbybase_adapter.rb, line 351
351:     def self.delete_all(conditions = nil)
352:       if conditions.nil? and !block_given?
353:         table.delete_all
354:       else
355:         table.delete &build_conditions_from_options(:conditions => conditions)
356:       end
357:     end
find(*args)

This methods differs in the API from ActiveRecord::Base#find! The changed options are:

  • :conditions this should be a block for selecting the records
  • :order this should be the symbol of the field name
  • :include: Names associations that should be loaded alongside using KirbyBase Lookup fields

The following work as before:

  • :offset: An integer determining the offset from where the rows should be fetched. So at 5, it would skip the first 4 rows.
  • :readonly: Mark the returned records read-only so they cannot be saved or updated.
  • :limit: Max numer of records returned
  • :select: Field names from the table. Not as useful, as joins are irrelevant

The following are not supported (silently ignored);

  • :joins: An SQL fragment for additional joins like "LEFT JOIN comments ON comments.post_id = id".

As a more Kirby-ish way, you can also pass a block to find that will be used to select the matching records. It’s a shortcut to :conditions.

     # File kirbybase_adapter.rb, line 451
451:     def self.find(*args)
452:       options = extract_options_from_args!(args)
453:       conditions = Proc.new if block_given?
454:       raise ArgumentError, "Please specify EITHER :conditions OR a block!" if conditions and options[:conditions]
455:       options[:conditions] ||= conditions
456:       options[:conditions] = build_conditions_from_options(options)
457:       filter = options[:select] ? [:recno, options[:select]].flatten.map{|s| s.to_sym} : nil
458: 
459:       # Inherit :readonly from finder scope if set.  Otherwise,

460:       # if :joins is not blank then :readonly defaults to true.

461:       unless options.has_key?(:readonly)
462:         if scoped?(:find, :readonly)
463:           options[:readonly] = scope(:find, :readonly)
464:         elsif !options[:joins].blank?
465:           options[:readonly] = true
466:         end
467:       end
468: 
469:       case args.first
470:       when :first
471:         return find(:all, options.merge(options[:include] ? { } : { :limit => 1 })).first
472:       when :all
473:         records = options[:include] ?
474:                     find_with_associations(options) :
475:                     filter ? table.select( *filter, &options[:conditions] ) : table.select( &options[:conditions] )
476:         records = apply_options_to_result_set records, options
477:         records = instantiate_records(records, :filter => filter, :readonly => options[:readonly])
478:         records
479:       else
480:         return args.first if args.first.kind_of?(Array) && args.first.empty?
481:         raise RecordNotFound, "Expecting a list of IDs!" unless args.flatten.all?{|i| i.is_a?(Numeric) || (i.is_a?(String) && i.match(/^\d+$/)) }
482: 
483:         expects_array = ( args.is_a?(Array) and args.first.kind_of?(Array) )
484:         ids = args.flatten.compact.collect{ |i| i.to_i }.uniq
485: 
486:         records = filter ?

487:                     table.select_by_recno_index(*filter) { |r| ids.include?(r.recno) } :
488:                     table.select_by_recno_index { |r| ids.include?(r.recno) }
489:         records = apply_options_to_result_set(records, options) rescue records
490: 
491:         conditions_message = options[:conditions] ? " and conditions: #{options[:conditions].inspect}" : ''
492:         case ids.size
493:           when 0
494:             raise RecordNotFound, "Couldn't find #{name} without an ID#{conditions_message}"
495:           when 1
496:             if records.nil? or records.empty?
497:               raise RecordNotFound, "Couldn't find #{name} with ID=#{ids.first}#{conditions_message}"
498:             end
499:             records = instantiate_records(records, :filter => filter, :readonly => options[:readonly])
500:             expects_array ? records : records.first
501:           else
502:             if records.size == ids.size
503:               return instantiate_records(records, :filter => filter, :readonly => options[:readonly])
504:             else
505:               raise RecordNotFound, "Couldn't find all #{name.pluralize} with IDs (#{ids.join(', ')})#{conditions_message}"
506:             end
507:         end
508:       end
509:     end
find_by_sql(*args)

NOT SUPPORTED !!!

     # File kirbybase_adapter.rb, line 334
334:     def self.find_by_sql(*args)
335:       raise StatementInvalid, "SQL not Supported"
336:     end
increment_counter(counter_name, ids)

Override of AR::Base that was using raw SQL

     # File kirbybase_adapter.rb, line 423
423:     def self.increment_counter(counter_name, ids)
424:       [ids].flatten.each do |id|
425:         table.update{|rec| rec.recno == id }.set{ |rec| rec.send "#{counter_name}=", (rec.send(counter_name)+1) }
426:       end
427:     end
parse_conditions_from_sql_array(sql_parameters_array)

Attempt to parse parameters in the format of [‘name = ? AND value = ?’, some_name, 1] in the :conditions clause

     # File kirbybase_adapter.rb, line 393
393:     def self.parse_conditions_from_sql_array(sql_parameters_array)
394:       query = sql_parameters_array[0]
395:       args = sql_parameters_array[1..-1].map{|arg| arg.is_a?(Hash) ? (raise PreparedStatementInvalid if arg.size > 1; arg.values[0]) : arg }
396: 
397:       query = translate_sql_to_code query
398:       raise PreparedStatementInvalid if query.count('?') != args.size
399:       query_components = query.split('?').zip(args.map{ |a|
400:         case a
401:         when String, Array  then a.inspect
402:         when nil            then 'nil'
403:         else a
404:         end
405:       })
406:       block_string = query_components.to_s
407:       begin
408:         eval "lambda{ |rec| #{block_string} }"
409:       rescue Exception => detail
410:         raise PreparedStatementInvalid, detail.to_s
411:       end
412:     end
parse_updates_from_sql_array(sql_parameters_array)

Attempt to parse parameters in the format of [‘name = ?’, some_name] for updates

     # File kirbybase_adapter.rb, line 376
376:     def self.parse_updates_from_sql_array sql_parameters_array
377:       updates_string = sql_parameters_array[0]
378:       args = sql_parameters_array[1..-1]
379: 
380:       update_code = table.field_names.inject(updates_string) {|updates, fld| fld == :id ? updates.gsub(/\bid\b/, 'rec.recno') : updates.gsub(/\b(#{fld})\b/, 'rec.\1') }
381:       update_code = update_code.split(',').zip(args).map {|i,v| [i.gsub('?', ''), v.inspect]}.to_s.gsub(/\bNULL\b/i, 'nil')
382:       eval "lambda{ |rec| #{update_code} }"
383:     end
parse_updates_from_sql_string(sql_string)

Attempt to parse parameters in the format of ‘name = "Some Name"’ for updates

     # File kirbybase_adapter.rb, line 386
386:     def self.parse_updates_from_sql_string sql_string
387:       update_code = table.field_names.inject(sql_string) {|updates, fld| fld == :id ? updates.gsub(/\bid\b/, 'rec.recno') : updates.gsub(/\b(#{fld})\b/, 'rec.\1') }.gsub(/\bNULL\b/i, 'nil')
388:       eval "lambda{ |rec| #{update_code} }"
389:     end
rollback_db_transaction()

NOT SUPPORTED!!!

     # File kirbybase_adapter.rb, line 686
686:     def self.rollback_db_transaction
687:       raise ArgumentError, "#rollback_db_transaction"
688:       # connection.rollback

689:     end
select_all(sql, name = nil)

NOT SUPPORTED !!!

     # File kirbybase_adapter.rb, line 313
313:     def self.select_all(sql, name = nil)
314:       raise StatementInvalid, "select_all(#{sql}, #{name}"
315:       execute(sql, name).map do |row|
316:         record = {}
317:         row.each_key do |key|
318:           if key.is_a?(String)
319:             record[key.sub(/^\w+\./, '')] = row[key]
320:           end
321:         end
322:         record
323:       end
324:     end
select_one(sql, name = nil)

NOT SUPPORTED !!!

     # File kirbybase_adapter.rb, line 327
327:     def self.select_one(sql, name = nil)
328:       raise StatementInvalid, "select_one(#{sql}, #{name}"
329:       result = select_all(sql, name)
330:       result.nil? ? nil : result.first
331:     end
serialize(attr_name, class_name = Object)

Serializing a column will cause it to change the column type to :YAML in the database.

     # File kirbybase_adapter.rb, line 696
696:       def serialize(attr_name, class_name = Object)
697:         __before_ackbar_serialize(attr_name, class_name)
698:         connection.change_column(table_name, attr_name, :yaml)
699:       end
table()

The KBTable object for this AR model object

     # File kirbybase_adapter.rb, line 304
304:     def self.table
305:       begin
306:         db.get_table(table_name.to_sym)
307:       rescue RuntimeError => detail
308:         raise StatementInvalid, detail.message
309:       end
310:     end
translate_sql_to_code(sql_string)

Translates SQL fragments to a code string. This code string can then be used to construct a code block for KirbyBase. Relies on the SQL_FRAGMENT_TRANSLATIONS series of transformations. Will also remove table names (e.g. people.name) so not safe to use for joins.

     # File kirbybase_adapter.rb, line 654
654:     def self.translate_sql_to_code sql_string
655:       block_string = SQL_FRAGMENT_TRANSLATIONS.inject(sql_string) {|str, (from, to)| str.gsub(from, to)}
656:       block_string.gsub(/#{table_name}\./, '')
657:     end
type_condition(class_name = nil)

For handling the table inheritance column.

     # File kirbybase_adapter.rb, line 589
589:     def self.type_condition class_name = nil
590:       type_condition = if class_name
591:         "rec.#{inheritance_column} == '#{class_name}'"
592:       else
593:         subclasses.inject("rec.#{inheritance_column}.to_s == '#{name.demodulize}' ") do |condition, subclass|
594:           condition << "or rec.#{inheritance_column}.to_s == '#{subclass.name.demodulize}' "
595:         end
596:       end
597: 
598:       eval "lambda{ |rec| #{type_condition} }"
599:     end
update_all(updates, conditions = nil)

Updates the matching rows from the table. If no conditions are specified, will update all rows in the table.

     # File kirbybase_adapter.rb, line 361
361:     def self.update_all(updates, conditions = nil)
362:       finder = build_conditions_from_options :conditions => conditions
363:       updater = case updates
364:                  when Proc   then updates
365:                  when Hash   then updates
366:                  when Array  then parse_updates_from_sql_array(updates)
367:                  when String then parse_updates_from_sql_string(updates)
368:                  else        raise ArgumentError, "Don't know how to process updates: #{updates.inspect}"
369:                 end
370:       updater.is_a?(Proc) ?
371:         table.update(&finder).set(&updater) :
372:         table.update(&finder).set(updater)
373:     end
Private Class methods
apply_options_to_result_set(records, options)

Applies the limit/offset/readonly/order and other options to the result set. Will also reapply the conditions.

     # File kirbybase_adapter.rb, line 524
524:     def self.apply_options_to_result_set records, options
525:       records = [records].flatten.compact
526:       records = records.select( &options[:conditions] ) if options[:conditions]
527:       if options[:order]
528:         options[:order].split(',').reverse.each do |order_field|
529:           # this algorithm is probably incorrect for complex sorts, like

530:           # col_a, col_b DESC, col_C

531:           reverse = order_field =~ /\bDESC\b/i
532:           order_field = order_field.strip.split[0] # clear any DESC, ASC

533:           records = records.stable_sort_by(order_field.to_sym == :id ? :recno : order_field.to_sym)
534:           records.reverse! if reverse
535:         end
536:       end
537:       offset = options[:offset] || scope(:find, :offset)
538:       records = records.slice!(offset..-1) if offset
539:       limit = options[:limit] || scope(:find, :limit)
540:       records = records.slice!(0, limit) if limit
541:       records
542:     end
instantiate_records(rec_array, options = {})

Instantiates the model record-objects from the KirbyBase structs. Will also apply the limit/offset/readonly/order and other options.

     # File kirbybase_adapter.rb, line 513
513:     def self.instantiate_records rec_array, options = {}
514:       field_names = ['id', table.field_names[1..-1]].flatten.map { |f| f.to_s }
515:       field_names &= ['id', options[:filter]].flatten.map{|f| f.to_s} if options[:filter]
516:       records = [rec_array].flatten.compact.map { |rec| instantiate( field_names.zip(rec.values).inject({}){|h, (k,v)| h[k] = v; h} ) }
517:       records.each { |record| record.readonly! } if options[:readonly]
518:       records
519:     end
Public Instance methods
attributes_to_input_rec()

translates the Active-Record instance attributes to a input hash for KirbyBase to be used in insert or update

     # File kirbybase_adapter.rb, line 763
763:     def attributes_to_input_rec
764:       field_types = Hash[ *table.field_names.zip(table.field_types).flatten ]
765:       attributes.inject({}) do |irec, (key, val)|
766:         irec[key.to_sym] = case field_types[key.to_sym]
767:           when :Integer
768:             case val
769:               when false then 0
770:               when true then 1
771:               else val
772:             end
773: 
774:           when :Boolean
775:             case val
776:               when 0 then false
777:               when 1 then true
778:               else val
779:             end
780: 
781:           when :Date
782:             val.is_a?(Time) ? val.to_date : val
783: 
784:           else val
785:         end
786:         irec
787:       end
788:     end
create_without_callbacks()

Creates a new record with values matching those of the instance attributes.

     # File kirbybase_adapter.rb, line 746
746:     def create_without_callbacks
747:       input_rec = attributes_to_input_rec
748:       (input_rec.keys - table.field_names + [:id]).each {|unknown_attribute| input_rec.delete(unknown_attribute)}
749:       self.id = table.insert(input_rec)
750:       @new_record = false
751:     end
db()

KirbyBase DB Object

     # File kirbybase_adapter.rb, line 710
710:     def db
711:       self.class.db
712:     end
destroy_without_callbacks()

Deletes the matching row for this object

     # File kirbybase_adapter.rb, line 754
754:     def destroy_without_callbacks
755:       unless new_record?
756:         table.delete{ |rec| rec.recno ==  id }
757:       end
758:       freeze
759:     end
table()

Table for the AR Model class for this record

     # File kirbybase_adapter.rb, line 715
715:     def table
716:       self.class.table
717:     end
update_with_lock()

Updates the associated record with values matching those of the instance attributes. Will also check for a lock (See ActiveRecord::Locking.

This method is also aliased as update_without_callbacks
     # File kirbybase_adapter.rb, line 728
728:     def update_with_lock
729:       if locking_enabled?
730:         previous_value    = self.lock_version
731:         self.lock_version = previous_value + 1
732: 
733:         pk = self.class.primary_key == 'id' ? :recno : :id
734:         affected_rows = table.update(attributes_to_input_rec){|rec| rec.send(pk) == id and rec.lock_version == previous_value}
735: 
736:         unless affected_rows == 1
737:           raise ActiveRecord::StaleObjectError, "Attempted to update a stale object"
738:         end
739:       else
740:         update_without_lock
741:       end
742:     end
update_without_callbacks()

Alias for update_with_lock

update_without_lock()

Updates the associated record with values matching those of the instance attributes.

     # File kirbybase_adapter.rb, line 722
722:     def update_without_lock
723:       table.update{ |rec| rec.recno == id }.set(attributes_to_input_rec)
724:     end