amalgalite 0.5.1 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -34,7 +34,7 @@ module Amalgalite
34
34
  # the declared data type of the column in the original sql that created the
35
35
  # column
36
36
  attr_accessor :declared_data_type
37
-
37
+
38
38
  # the collation sequence name of the column
39
39
  attr_accessor :collation_sequence_name
40
40
 
@@ -7,6 +7,10 @@ require 'amalgalite/statement'
7
7
  require 'amalgalite/trace_tap'
8
8
  require 'amalgalite/profile_tap'
9
9
  require 'amalgalite/type_maps/default_map'
10
+ require 'amalgalite/function'
11
+ require 'amalgalite/aggregate'
12
+ require 'amalgalite/busy_timeout'
13
+ require 'amalgalite/progress_handler'
10
14
 
11
15
  module Amalgalite
12
16
  #
@@ -31,6 +35,18 @@ module Amalgalite
31
35
  # Error thrown if a database is opened with an invalid mode
32
36
  class InvalidModeError < ::Amalgalite::Error; end
33
37
 
38
+ # Error thrown if there is a failure in a user defined function
39
+ class FunctionError < ::Amalgalite::Error; end
40
+
41
+ # Error thrown if there is a failure in a user defined aggregate
42
+ class AggregateError < ::Amalgalite::Error; end
43
+
44
+ # Error thrown if there is a failure in defining a busy handler
45
+ class BusyHandlerError < ::Amalgalite::Error; end
46
+
47
+ # Error thrown if there is a failure in defining a progress handler
48
+ class ProgressHandlerError < ::Amalgalite::Error; end
49
+
34
50
  ##
35
51
  # container class for holding transaction behavior constants. These are the
36
52
  # SQLite values passed to a START TRANSACTION SQL statement.
@@ -43,7 +59,7 @@ module Amalgalite
43
59
  # a readlock is obtained immediately so that no other process can write to
44
60
  # the database
45
61
  IMMEDIATE = "IMMEDIATE"
46
-
62
+
47
63
  # a read+write lock is obtained, no other proces can read or write to the
48
64
  # database
49
65
  EXCLUSIVE = "EXCLUSIVE"
@@ -81,6 +97,12 @@ module Amalgalite
81
97
  # By default this is an instances of TypeMaps::DefaultMap
82
98
  attr_reader :type_map
83
99
 
100
+ # A list of the user defined functions
101
+ attr_reader :functions
102
+
103
+ # A list of the user defined aggregates
104
+ attr_reader :aggregates
105
+
84
106
  ##
85
107
  # Create a new Amalgalite database
86
108
  #
@@ -114,6 +136,8 @@ module Amalgalite
114
136
  @profile_tap = nil
115
137
  @trace_tap = nil
116
138
  @type_map = ::Amalgalite::TypeMaps::DefaultMap.new
139
+ @functions = Hash.new
140
+ @aggregates = Hash.new
117
141
 
118
142
  unless VALID_MODES.keys.include?( mode )
119
143
  raise InvalidModeError, "#{mode} is invalid, must be one of #{VALID_MODES.keys.join(', ')}"
@@ -509,6 +533,251 @@ module Amalgalite
509
533
  def rollback
510
534
  execute( "ROLLBACK" ) if in_transaction?
511
535
  end
512
- end
536
+
537
+ ##
538
+ # call-seq:
539
+ # db.function( "name", MyDBFunction.new )
540
+ # db.function( "my_func", callable )
541
+ # db.function( "my_func" ) do |x,y|
542
+ # ....
543
+ # return result
544
+ # end
545
+ #
546
+ # register a callback to be exposed as an SQL function. There are multiple
547
+ # ways to register this function:
548
+ #
549
+ # 1. db.function( "name" ) { |a| ... }
550
+ # * pass +function+ a _name_ and a block.
551
+ # * The SQL function _name_ taking _arity_ parameters will be registered,
552
+ # where _arity_ is the _arity_ of the block.
553
+ # * The return value of the block is the return value of the registred
554
+ # SQL function
555
+ # 2. db.function( "name", callable )
556
+ # * pass +function+ a _name_ and something that <tt>responds_to?( :to_proc )</tt>
557
+ # * The SQL function _name_ is registered taking _arity_ parameters is
558
+ # registered where _arity_ is the _arity_ of +callable.to_proc.call+
559
+ # * The return value of the +callable.to_proc.call+ is the return value
560
+ # of the SQL function
561
+ #
562
+ # See also ::Amalgalite::Function
563
+ #
564
+ def define_function( name, callable = nil, &block )
565
+ p = ( callable || block ).to_proc
566
+ raise FunctionError, "Use only mandatory or arbitrary parameters in an SQL Function, not both" if p.arity < -1
567
+ db_function = ::Amalgalite::SQLite3::Database::Function.new( name, p )
568
+ @api.define_function( db_function.name, db_function )
569
+ @functions[db_function.signature] = db_function
570
+ nil
571
+ end
572
+ alias :function :define_function
573
+
574
+ ##
575
+ # call-seq:
576
+ # db.remove_function( 'name', MyScalerFunctor.new )
577
+ # db.remove_function( 'name', callable )
578
+ # db.remove_function( 'name', arity )
579
+ # db.remove_function( 'name' )
580
+ #
581
+ # Remove a function from use in the database. Since the same function may
582
+ # be registered more than once with different arity, you may specify the
583
+ # arity, or the function object, or nil. If nil is used for the arity, then
584
+ # Amalgalite does its best to remove all functions of given name.
585
+ #
586
+ def remove_function( name, callable_or_arity = nil )
587
+ arity = nil
588
+ if callable_or_arity.respond_to?( :to_proc ) then
589
+ arity = callable_or_arity.to_proc.arity
590
+ elsif callable_or_arity.respond_to?( :to_int ) then
591
+ arity = callable_or_arity.to_int
592
+ end
593
+ to_remove = []
594
+
595
+ if arity then
596
+ signature = ::Amalgalite::SQLite3::Database::Function.signature( name, arity )
597
+ db_function = @functions[ signature ]
598
+ raise FunctionError, "db function '#{name}' with arity #{arity} does not appear to be defined" unless db_function
599
+ to_remove << db_function
600
+ else
601
+ possibles = @functions.values.select { |f| f.name == name }
602
+ raise FunctionError, "no db function '#{name}' appears to be defined" if possibles.empty?
603
+ to_remove = possibles
604
+ end
605
+
606
+ to_remove.each do |db_function|
607
+ @api.remove_function( db_function.name, db_function)
608
+ @functions.delete( db_function.signature )
609
+ end
610
+ end
611
+
612
+ ##
613
+ # call-seq:
614
+ # db.define_aggregate( 'name', MyAggregateClass )
615
+ #
616
+ # Define an SQL aggregate function, these are functions like max(), min(),
617
+ # avg(), etc. SQL functions that would be used when a GROUP BY clause is in
618
+ # effect. See also ::Amalgalite::Aggregate.
619
+ #
620
+ # A new instance of MyAggregateClass is created for each instance that the
621
+ # SQL aggregate is mentioned in SQL.
622
+ #
623
+ def define_aggregate( name, klass )
624
+ db_aggregate = klass
625
+ a = klass.new
626
+ raise AggregateError, "Use only mandatory or arbitrary parameters in an SQL Aggregate, not both" if a.arity < -1
627
+ raise AggregateError, "Aggregate implementation name '#{a.name}' does not match defined name '#{name}'"if a.name != name
628
+ @api.define_aggregate( name, a.arity, klass )
629
+ @aggregates[a.signature] = db_aggregate
630
+ nil
631
+ end
632
+ alias :aggregate :define_aggregate
633
+
634
+ ##
635
+ # call-seq:
636
+ # db.remove_aggregate( 'name', MyAggregateClass )
637
+ # db.remove_aggregate( 'name' )
638
+ #
639
+ # Remove an aggregate from use in the database. Since the same aggregate
640
+ # may be refistered more than once with different arity, you may specify the
641
+ # arity, or the aggregate class, or nil. If nil is used for the arity then
642
+ # Amalgalite does its best to remove all aggregates of the given name
643
+ #
644
+ def remove_aggregate( name, klass_or_arity = nil )
645
+ klass = nil
646
+ case klass_or_arity
647
+ when Integer
648
+ arity = klass_or_arity
649
+ when NilClass
650
+ arity = nil
651
+ else
652
+ klass = klass_or_arity
653
+ arity = klass.new.arity
654
+ end
655
+ to_remove = []
656
+ if arity then
657
+ signature = ::Amalgalite::SQLite3::Database::Function.signature( name, arity )
658
+ db_aggregate = @aggregates[ signature ]
659
+ raise AggregateError, "db aggregate '#{name}' with arity #{arity} does not appear to be defined" unless db_aggregate
660
+ to_remove << db_aggregate
661
+ else
662
+ possibles = @aggregates.values.select { |a| a.new.name == name }
663
+ raise AggregateError, "no db aggregate '#{name}' appears to be defined" if possibles.empty?
664
+ to_remove = possibles
665
+ end
666
+
667
+ to_remove.each do |db_aggregate|
668
+ i = db_aggregate.new
669
+ @api.remove_aggregate( i.name, i.arity, db_aggregate )
670
+ @aggregates.delete( i.signature )
671
+ end
672
+ end
673
+
674
+ ##
675
+ # call-seq:
676
+ # db.busy_handler( callable )
677
+ # db.define_busy_handler do |count|
678
+ # end
679
+ # db.busy_handler( Amalgalite::BusyTimeout.new( 30 ) )
680
+ #
681
+ # Register a busy handler for this database connection, the handler MUST
682
+ # follow the +to_proc+ protocol indicating that is will
683
+ # +respond_to?(:call)+. This is intrinsic to lambdas and blocks so
684
+ # those will work automatically.
685
+ #
686
+ # This exposes the sqlite busy handler api to ruby.
687
+ #
688
+ # * http://sqlite.org/c3ref/busy_handler.html
689
+ #
690
+ # The busy handler's _call(N)_ method may be invoked whenever an attempt is
691
+ # made to open a database table that another thread or process has locked.
692
+ # +N+ will be the number of times the _call(N)_ method has been invoked
693
+ # during this locking event.
694
+ #
695
+ # The handler may or maynot be called based upon what SQLite determins.
696
+ #
697
+ # If the handler returns _nil_ or _false_ then no more busy handler calls will
698
+ # be made in this lock event and you are probably going to see an
699
+ # SQLite::Error in your immediately future in another process or in another
700
+ # piece of code.
701
+ #
702
+ # If the handler returns non-nil or non-false then another attempt will be
703
+ # made to obtain the lock, lather, rinse, repeat.
704
+ #
705
+ # If an Exception happens in a busy handler, it will be the same as if the
706
+ # busy handler had returned _nil_ or _false_. The exception itself will not
707
+ # be propogated further.
708
+ #
709
+ def define_busy_handler( callable = nil, &block )
710
+ handler = ( callable || block ).to_proc
711
+ a = handler.arity
712
+ raise BusyHandlerError, "A busy handler expects 1 and only 1 argument, not #{a}" if a != 1
713
+ @api.busy_handler( handler )
714
+ end
715
+ alias :busy_handler :define_busy_handler
716
+
717
+ ##
718
+ # call-seq:
719
+ # db.remove_busy_handler
720
+ #
721
+ # Remove the busy handler for this database connection.
722
+ def remove_busy_handler
723
+ @api.busy_handler( nil )
724
+ end
725
+
726
+ ##
727
+ # call-seq:
728
+ # db.interrupt!
729
+ #
730
+ # Cause another thread with a handle on this database to be interrupted and
731
+ # return at the earliest opportunity as interrupted. It is not safe to call
732
+ # this method if the database might be closed before interrupt! returns.
733
+ #
734
+ def interrupt!
735
+ @api.interrupt!
736
+ end
737
+
738
+ ##
739
+ # call-seq:
740
+ # db.progress_handler( 50, MyProgressHandler.new )
741
+ # db.progress_handler( 25 , callable )
742
+ # db.progress_handler do
743
+ # ....
744
+ # return result
745
+ # end
746
+ #
747
+ # Register a progress handler for this database connection, the handler MUST
748
+ # follow the +to_proc+ protocol indicating that is will
749
+ # +respond_to?(:call)+. This is intrinsic to lambdas and blocks so
750
+ # those will work automatically.
751
+ #
752
+ # This exposes the sqlite progress handler api to ruby.
753
+ #
754
+ # * http://sqlite.org/c3ref/progress_handler.html
755
+ #
756
+ # The progress handler's _call()_ method may be invoked ever N SQLite op
757
+ # codes. If the progress handler returns anything that can evaluate to
758
+ # +true+ then current running sqlite statement is terminated at the earliest
759
+ # oppportunity.
760
+ #
761
+ # You can use this to be notified that a thread is still processingn a
762
+ # request.
763
+ #
764
+ def define_progress_handler( op_code_count = 25, callable = nil, &block )
765
+ handler = ( callable || block ).to_proc
766
+ a = handler.arity
767
+ raise ProgressHandlerError, "A progress handler expects 0 arguments, not #{a}" if a != 0
768
+ @api.progress_handler( op_code_count, handler )
769
+ end
770
+ alias :progress_handler :define_progress_handler
771
+
772
+
773
+ ##
774
+ # call-seq:
775
+ # db.remove_progress_handler
776
+ #
777
+ # Remove the progress handler for this database connection.
778
+ def remove_progress_handler
779
+ @api.progress_handler( nil, nil )
780
+ end
781
+ end
513
782
  end
514
783
 
@@ -0,0 +1,61 @@
1
+ require 'amalgalite/sqlite3/database/function'
2
+ module Amalgalite
3
+ #
4
+ # A Base class to inherit from for creating your own SQL scalar functions
5
+ # in ruby.
6
+ #
7
+ # These are SQL functions similar to _abs(X)_, _length(X)_, _random()_. Items
8
+ # that take parameters and return value. They have no state between
9
+ # calls. Built in SQLite scalar functions are :
10
+ #
11
+ # * http://www.sqlite.org/lang_corefunc.html
12
+ # * http://www.sqlite.org/lang_datefunc.html
13
+ #
14
+ # Functions defined in Amalgalite databases conform to the Proc interface.
15
+ # Everything that is defined in an Amalgalite database using +define_function+
16
+ # has its +to_proc+ method called. As a result, any Function must also
17
+ # conform to the +to_proc+ protocol.
18
+ #
19
+ # If you choose to use Function as a parent class of your SQL scalar function
20
+ # implementation you should only have implement +call+ with the appropriate
21
+ # _arity_.
22
+ #
23
+ # For instance to implement a _sha1(X)_ SQL function you could implement it as
24
+ #
25
+ # class SQLSha1 < ::Amalgalite::Function
26
+ # def initialize
27
+ # super( 'md5', 1 )
28
+ # end
29
+ # def call( s )
30
+ # ::Digest::MD5.hexdigest( s.to_s )
31
+ # end
32
+ # end
33
+ #
34
+ class Function
35
+ # The name of the SQL function
36
+ attr_accessor :name
37
+
38
+ # The arity of the SQL function
39
+ attr_accessor :arity
40
+
41
+ # Initialize the function with a name and arity
42
+ def initialize( name, arity )
43
+ @name = name
44
+ @arity = arity
45
+ end
46
+
47
+ # All SQL functions defined foloow the +to_proc+ protocol
48
+ def to_proc
49
+ self
50
+ end
51
+
52
+ # <b>Do Not Override</b>
53
+ #
54
+ # The function signature for use by the Amaglaite datase in tracking
55
+ # function definition and removal.
56
+ #
57
+ def signature
58
+ @signature ||= ::Amalgalite::SQLite3::Database::Function.signature( self.name, self.arity )
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,21 @@
1
+ module Amalgalite
2
+ ##
3
+ # A base class for use in creating your own progress handler classes
4
+ #
5
+ class ProgressHandler
6
+ def to_proc
7
+ self
8
+ end
9
+
10
+ # the arity of the call method
11
+ def arity() 0 ; end
12
+
13
+ ##
14
+ # Override this method, returning +false+ if the SQLite should act as if
15
+ # +interrupt!+ had been invoked.
16
+ #
17
+ def call
18
+ raise NotImplementedError, "The progress handler call() method must be implemented"
19
+ end
20
+ end
21
+ end
@@ -4,3 +4,4 @@ require 'amalgalite/sqlite3/version'
4
4
  require 'amalgalite/sqlite3/constants'
5
5
  require 'amalgalite/sqlite3/status'
6
6
  require 'amalgalite/sqlite3/database/status'
7
+ require 'amalgalite/sqlite3/database/function'
@@ -0,0 +1,48 @@
1
+ module Amalgalite::SQLite3
2
+ class Database
3
+ ##
4
+ # A wrapper around a proc for use as an SQLite Ddatabase fuction
5
+ #
6
+ # f = Function.new( 'md5', lambda { |x| Digest::MD5.hexdigest( x.to_s ) } )
7
+ #
8
+ class Function
9
+
10
+ # the name of the function, and how it will be called in SQL
11
+ attr_reader :name
12
+
13
+ # The unique signature of this function. This is used to determin if the
14
+ # function is already registered or not
15
+ #
16
+ def self.signature( name, arity )
17
+ "#{name}/#{arity}"
18
+ end
19
+
20
+ # Initialize with the name and the Proc
21
+ #
22
+ def initialize( name, _proc )
23
+ @name = name
24
+ @function = _proc
25
+ end
26
+
27
+ # The unique signature of this function
28
+ #
29
+ def signature
30
+ @signature ||= Function.signature( name, arity )
31
+ end
32
+ alias :to_s :signature
33
+
34
+ # The arity of SQL function, -1 means it is takes a variable number of
35
+ # arguments.
36
+ #
37
+ def arity
38
+ @function.arity
39
+ end
40
+
41
+ # Invoke the proc
42
+ #
43
+ def call( *args )
44
+ @function.call( *args )
45
+ end
46
+ end
47
+ end
48
+ end