amalgalite 0.5.1-x86-mswin32-60 → 0.6.0-x86-mswin32-60
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/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
|