amalgalite 0.5.1-x86-mswin32-60 → 0.6.0-x86-mswin32-60
Sign up to get free protection for your applications and to get access to all the features.
- data/HISTORY +15 -2
- data/README +3 -0
- data/examples/define_aggregate.rb +75 -0
- data/examples/define_function.rb +104 -0
- data/examples/gems.db +0 -0
- data/ext/amalgalite3.h +28 -2
- data/ext/amalgalite3_blob.c +1 -2
- data/ext/amalgalite3_database.c +567 -46
- data/ext/amalgalite3_requires_bootstrap.c +1 -3
- data/ext/amalgalite3_statement.c +2 -2
- data/ext/extconf.rb +1 -0
- data/ext/sqlite3.c +6521 -4518
- data/ext/sqlite3.h +50 -43
- data/ext/test_error.c +77 -0
- data/lib/amalgalite.rb +5 -0
- data/lib/amalgalite/aggregate.rb +67 -0
- data/lib/amalgalite/busy_timeout.rb +47 -0
- data/lib/amalgalite/column.rb +1 -1
- data/lib/amalgalite/database.rb +271 -2
- data/lib/amalgalite/function.rb +61 -0
- data/lib/amalgalite/progress_handler.rb +21 -0
- data/lib/amalgalite/sqlite3.rb +1 -0
- data/lib/amalgalite/sqlite3/database/function.rb +48 -0
- data/lib/amalgalite/version.rb +2 -2
- data/lib/amalgalite3.so +0 -0
- data/spec/aggregate_spec.rb +168 -0
- data/spec/busy_handler.rb +164 -0
- data/spec/database_spec.rb +97 -3
- data/spec/function_spec.rb +86 -0
- data/spec/progress_handler_spec.rb +103 -0
- data/spec/spec_helper.rb +1 -0
- data/spec/sqlite3/version_spec.rb +2 -2
- metadata +21 -3
data/lib/amalgalite/column.rb
CHANGED
data/lib/amalgalite/database.rb
CHANGED
@@ -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
|
-
|
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
|
data/lib/amalgalite/sqlite3.rb
CHANGED
@@ -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
|