amalgalite 0.1.0
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 +4 -0
- data/LICENSE +31 -0
- data/README +28 -0
- data/ext/amalgalite3.c +191 -0
- data/ext/amalgalite3.h +97 -0
- data/ext/amalgalite3_constants.c +179 -0
- data/ext/amalgalite3_database.c +458 -0
- data/ext/amalgalite3_statement.c +546 -0
- data/ext/gen_constants.rb +114 -0
- data/ext/mkrf_conf.rb +6 -0
- data/ext/sqlite3.c +87003 -0
- data/ext/sqlite3.h +5638 -0
- data/ext/sqlite3_options.h +4 -0
- data/ext/sqlite3ext.h +362 -0
- data/gemspec.rb +50 -0
- data/lib/amalgalite.rb +28 -0
- data/lib/amalgalite/blob.rb +14 -0
- data/lib/amalgalite/boolean.rb +42 -0
- data/lib/amalgalite/column.rb +83 -0
- data/lib/amalgalite/database.rb +505 -0
- data/lib/amalgalite/index.rb +27 -0
- data/lib/amalgalite/paths.rb +70 -0
- data/lib/amalgalite/profile_tap.rb +130 -0
- data/lib/amalgalite/schema.rb +90 -0
- data/lib/amalgalite/sqlite3.rb +4 -0
- data/lib/amalgalite/sqlite3/constants.rb +48 -0
- data/lib/amalgalite/sqlite3/version.rb +38 -0
- data/lib/amalgalite/statement.rb +307 -0
- data/lib/amalgalite/table.rb +34 -0
- data/lib/amalgalite/taps/console.rb +27 -0
- data/lib/amalgalite/taps/io.rb +71 -0
- data/lib/amalgalite/trace_tap.rb +35 -0
- data/lib/amalgalite/type_map.rb +60 -0
- data/lib/amalgalite/type_maps/default_map.rb +153 -0
- data/lib/amalgalite/type_maps/storage_map.rb +41 -0
- data/lib/amalgalite/type_maps/text_map.rb +23 -0
- data/lib/amalgalite/version.rb +32 -0
- data/lib/amalgalite/view.rb +24 -0
- data/spec/amalgalite_spec.rb +4 -0
- data/spec/boolean_spec.rb +26 -0
- data/spec/database_spec.rb +222 -0
- data/spec/default_map_spec.rb +85 -0
- data/spec/integeration_spec.rb +111 -0
- data/spec/paths_spec.rb +28 -0
- data/spec/schema_spec.rb +46 -0
- data/spec/spec_helper.rb +25 -0
- data/spec/sqlite3/constants_spec.rb +25 -0
- data/spec/sqlite3/version_spec.rb +14 -0
- data/spec/sqlite3_spec.rb +34 -0
- data/spec/statement_spec.rb +116 -0
- data/spec/storage_map_spec.rb +41 -0
- data/spec/tap_spec.rb +59 -0
- data/spec/text_map_spec.rb +23 -0
- data/spec/type_map_spec.rb +17 -0
- data/spec/version_spec.rb +9 -0
- data/tasks/announce.rake +38 -0
- data/tasks/config.rb +108 -0
- data/tasks/distribution.rake +38 -0
- data/tasks/documentation.rake +31 -0
- data/tasks/extension.rake +45 -0
- data/tasks/rspec.rake +32 -0
- data/tasks/rubyforge.rake +48 -0
- data/tasks/utils.rb +80 -0
- metadata +165 -0
@@ -0,0 +1,27 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright (c) 2008 Jeremy Hinegardner
|
3
|
+
# All rights reserved. See LICENSE and/or COPYING for details.
|
4
|
+
#++
|
5
|
+
|
6
|
+
module Amalgalite
|
7
|
+
#
|
8
|
+
# a class representing the meta information about an SQLite index
|
9
|
+
#
|
10
|
+
class Index
|
11
|
+
# the name of the index
|
12
|
+
attr_reader :name
|
13
|
+
|
14
|
+
# the sql statement that created the index
|
15
|
+
attr_reader :sql
|
16
|
+
|
17
|
+
# the table the index is for
|
18
|
+
attr_accessor :table
|
19
|
+
|
20
|
+
def initialize( name, sql, table )
|
21
|
+
@name = name
|
22
|
+
@sql = sql
|
23
|
+
@table = table
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
@@ -0,0 +1,70 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright (c) 2008 Jeremy Hinegardner
|
3
|
+
# All rights reserved. See LICENSE and/or COPYING for details.
|
4
|
+
#++
|
5
|
+
module Amalgalite
|
6
|
+
#
|
7
|
+
# Paths contains helpful methods to determine paths of files inside the
|
8
|
+
# Amalgalite library
|
9
|
+
#
|
10
|
+
module Paths
|
11
|
+
#
|
12
|
+
# The root directory of the project is considered to be the parent directory
|
13
|
+
# of the 'lib' directory.
|
14
|
+
#
|
15
|
+
# returns:: [String] The full expanded path of the parent directory of 'lib'
|
16
|
+
# going up the path from the current file. Trailing
|
17
|
+
# File::SEPARATOR is guaranteed.
|
18
|
+
#
|
19
|
+
def self.root_dir
|
20
|
+
unless @root_dir
|
21
|
+
path_parts = ::File.expand_path(__FILE__).split(::File::SEPARATOR)
|
22
|
+
lib_index = path_parts.rindex("lib")
|
23
|
+
@root_dir = path_parts[0...lib_index].join(::File::SEPARATOR) + ::File::SEPARATOR
|
24
|
+
end
|
25
|
+
return @root_dir
|
26
|
+
end
|
27
|
+
|
28
|
+
# returns:: [String] The full expanded path of the +config+ directory
|
29
|
+
# below _root_dir_. All parameters passed in are joined onto the
|
30
|
+
# result. Trailing File::SEPARATOR is guaranteed if _args_ are
|
31
|
+
# *not* present.
|
32
|
+
#
|
33
|
+
def self.config_path(*args)
|
34
|
+
self.sub_path("config", *args)
|
35
|
+
end
|
36
|
+
|
37
|
+
# returns:: [String] The full expanded path of the +data+ directory below
|
38
|
+
# _root_dir_. All parameters passed in are joined onto the
|
39
|
+
# result. Trailing File::SEPARATOR is guaranteed if
|
40
|
+
# _*args_ are *not* present.
|
41
|
+
#
|
42
|
+
def self.data_path(*args)
|
43
|
+
self.sub_path("data", *args)
|
44
|
+
end
|
45
|
+
|
46
|
+
# returns:: [String] The full expanded path of the +lib+ directory below
|
47
|
+
# _root_dir_. All parameters passed in are joined onto the
|
48
|
+
# result. Trailing File::SEPARATOR is guaranteed if
|
49
|
+
# _*args_ are *not* present.
|
50
|
+
#
|
51
|
+
def self.lib_path(*args)
|
52
|
+
self.sub_path("lib", *args)
|
53
|
+
end
|
54
|
+
|
55
|
+
# returns:: [String] The full expanded path of the +ext+ directory below
|
56
|
+
# _root_dir_. All parameters passed in are joined onto the
|
57
|
+
# result. Trailing File::SEPARATOR is guaranteed if
|
58
|
+
# _*args_ are *not* present.
|
59
|
+
#
|
60
|
+
def self.ext_path(*args)
|
61
|
+
self.sub_path("ext", *args)
|
62
|
+
end
|
63
|
+
|
64
|
+
def self.sub_path(sub,*args)
|
65
|
+
sp = ::File.join(root_dir, sub) + File::SEPARATOR
|
66
|
+
sp = ::File.join(sp, *args) if args
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
@@ -0,0 +1,130 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright (c) 2008 Jeremy Hinegardner
|
3
|
+
# All rights reserved. See LICENSE and/or COPYING for details.
|
4
|
+
#++
|
5
|
+
|
6
|
+
module Amalgalite
|
7
|
+
#
|
8
|
+
# A ProfileSampler is a sampler of profile times. It aggregates up profile
|
9
|
+
# events that happen for the same source. It is based upon the RFuzz::Sampler
|
10
|
+
# class from the rfuzz gem
|
11
|
+
#
|
12
|
+
class ProfileSampler
|
13
|
+
#
|
14
|
+
# create a new sampler with the given name
|
15
|
+
#
|
16
|
+
def initialize( name )
|
17
|
+
@name = name
|
18
|
+
reset!
|
19
|
+
end
|
20
|
+
|
21
|
+
##
|
22
|
+
# reset the internal state so it may be used again
|
23
|
+
#
|
24
|
+
def reset!
|
25
|
+
@sum = 0.0
|
26
|
+
@sumsq = 0.0
|
27
|
+
@n = 0
|
28
|
+
@min = 0.0
|
29
|
+
@max = 0.0
|
30
|
+
end
|
31
|
+
|
32
|
+
##
|
33
|
+
# add a sample to the calculations
|
34
|
+
#
|
35
|
+
def sample( value )
|
36
|
+
@sum += value
|
37
|
+
@sumsq += (value * value)
|
38
|
+
if @n == 0 then
|
39
|
+
@min = @max = value
|
40
|
+
else
|
41
|
+
@min = value if value < @min
|
42
|
+
@max = value if value > @max
|
43
|
+
end
|
44
|
+
@n += 1
|
45
|
+
end
|
46
|
+
|
47
|
+
##
|
48
|
+
# return the mean of the data
|
49
|
+
#
|
50
|
+
def mean
|
51
|
+
@sum / @n
|
52
|
+
end
|
53
|
+
|
54
|
+
##
|
55
|
+
# returns the standard deviation of the data
|
56
|
+
#
|
57
|
+
def stddev
|
58
|
+
begin
|
59
|
+
Math.sqrt( (@sumsq - ( @sum * @sum / @n)) / (@n-1) )
|
60
|
+
rescue Errno::EDOM
|
61
|
+
return 0.0
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
##
|
66
|
+
# return all the values as an array
|
67
|
+
#
|
68
|
+
def to_a
|
69
|
+
[ @name, @sum, @sumsq, @n, mean, stddev, @min, @max ]
|
70
|
+
end
|
71
|
+
|
72
|
+
##
|
73
|
+
# return all the values as a hash
|
74
|
+
#
|
75
|
+
def to_h
|
76
|
+
{ 'name' => @name, 'n' => @n,
|
77
|
+
'sum' => @sum, 'sumsq' => @sumsq, 'mean' => mean,
|
78
|
+
'stddev' => stddev, 'min' => @min, 'max' => @max }
|
79
|
+
end
|
80
|
+
|
81
|
+
##
|
82
|
+
# return a string containing the sampler summary
|
83
|
+
#
|
84
|
+
def to_s
|
85
|
+
"[%s] => sum: %d, sumsq: %d, n: %d, mean: %0.6f, stddev: %0.6f, min: %d, max: %d" % to_a
|
86
|
+
end
|
87
|
+
|
88
|
+
end
|
89
|
+
|
90
|
+
#
|
91
|
+
# A Profile Tap recives +profile+ events from SQLite which involve the number of
|
92
|
+
# nanoseconds in wall-clock time it took for a particular thing to happen. In
|
93
|
+
# general this +thing+ is an SQL statement.
|
94
|
+
#
|
95
|
+
# It has a well known +profile+ method which when invoked will write the event
|
96
|
+
# to a delegate object.
|
97
|
+
#
|
98
|
+
#
|
99
|
+
class ProfileTap
|
100
|
+
|
101
|
+
attr_reader :samplers
|
102
|
+
|
103
|
+
#
|
104
|
+
# Create a new ProfileTap object that wraps the given object and calls the
|
105
|
+
# method named in +send_to+ ever time a profile event happens.
|
106
|
+
#
|
107
|
+
def initialize( wrapped_obj, send_to = 'profile' )
|
108
|
+
unless wrapped_obj.respond_to?( send_to )
|
109
|
+
raise Amalgalite::Error, "#{wrapped_obj.class.name} does not respond to #{send_to.to_s} "
|
110
|
+
end
|
111
|
+
|
112
|
+
@delegate_obj = wrapped_obj
|
113
|
+
@delegate_method = send_to
|
114
|
+
@samplers = {}
|
115
|
+
end
|
116
|
+
|
117
|
+
#
|
118
|
+
# Record the profile information and send the delegate object the msg and
|
119
|
+
# time information.
|
120
|
+
#
|
121
|
+
def profile( msg, time )
|
122
|
+
unless sampler = @samplers[msg]
|
123
|
+
msg = msg.gsub(/\s+/,' ')
|
124
|
+
sampler = @samplers[msg] = ProfileSampler.new( msg )
|
125
|
+
end
|
126
|
+
sampler.sample( time )
|
127
|
+
@delegate_obj.send( @delegate_method, msg, time )
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright (c) 2008 Jeremy Hinegardner
|
3
|
+
# All rights reserved. See LICENSE and/or COPYING for details.
|
4
|
+
#++
|
5
|
+
|
6
|
+
require 'amalgalite/table'
|
7
|
+
require 'amalgalite/index'
|
8
|
+
require 'amalgalite/column'
|
9
|
+
require 'amalgalite/view'
|
10
|
+
|
11
|
+
module Amalgalite
|
12
|
+
#
|
13
|
+
# An object view of the schema in the SQLite database. If the schema changes
|
14
|
+
# after this class is created, it has no knowledge of that.
|
15
|
+
#
|
16
|
+
class Schema
|
17
|
+
|
18
|
+
attr_reader :catalog
|
19
|
+
attr_reader :schema
|
20
|
+
attr_reader :tables
|
21
|
+
attr_reader :views
|
22
|
+
|
23
|
+
#
|
24
|
+
# Create a new instance of Schema
|
25
|
+
#
|
26
|
+
def initialize( db, catalog = 'main', schema = 'sqlite')
|
27
|
+
@db = db
|
28
|
+
@catalog = catalog
|
29
|
+
@schema = schema
|
30
|
+
|
31
|
+
load_schema!
|
32
|
+
end
|
33
|
+
|
34
|
+
#
|
35
|
+
# load the schema from the database
|
36
|
+
def load_schema!
|
37
|
+
load_tables
|
38
|
+
load_views
|
39
|
+
end
|
40
|
+
|
41
|
+
##
|
42
|
+
# load all the tables
|
43
|
+
#
|
44
|
+
def load_tables
|
45
|
+
@tables = {}
|
46
|
+
@db.execute("SELECT tbl_name, sql FROM sqlite_master WHERE type = 'table'") do |table_info|
|
47
|
+
table = Amalgalite::Table.new( table_info['tbl_name'], table_info['sql'] )
|
48
|
+
table.columns = load_columns( table )
|
49
|
+
|
50
|
+
@db.prepare("SELECT name, sql FROM sqlite_master WHERE type ='index' and tbl_name = @name") do |idx_stmt|
|
51
|
+
idx_stmt.execute( "@name" => table.name) do |idx_info|
|
52
|
+
table.indexes << Amalgalite::Index.new( idx_info['name'], idx_info['sql'], table )
|
53
|
+
end
|
54
|
+
end
|
55
|
+
@tables[table.name] = table
|
56
|
+
end
|
57
|
+
|
58
|
+
@tables
|
59
|
+
end
|
60
|
+
|
61
|
+
##
|
62
|
+
# load all the columns for a particular table
|
63
|
+
#
|
64
|
+
def load_columns( table )
|
65
|
+
cols = {}
|
66
|
+
@db.execute("PRAGMA table_info(#{table.name})") do |row|
|
67
|
+
col = Amalgalite::Column.new( "main", row['name'], table )
|
68
|
+
|
69
|
+
col.default_value = row['dflt_value']
|
70
|
+
@db.api.table_column_metadata( "main", table.name, col.name ).each_pair do |key, value|
|
71
|
+
col.send("#{key}=", value)
|
72
|
+
end
|
73
|
+
cols[col.name] = col
|
74
|
+
end
|
75
|
+
cols
|
76
|
+
end
|
77
|
+
|
78
|
+
##
|
79
|
+
# load all the views for the database
|
80
|
+
#
|
81
|
+
def load_views
|
82
|
+
@views = {}
|
83
|
+
@db.execute("SELECT name, sql FROM sqlite_master WHERE type = 'view'") do |view_info|
|
84
|
+
view = Amalgalite::View.new( view_info['name'], view_info['sql'] )
|
85
|
+
@views[view.name] = view
|
86
|
+
end
|
87
|
+
@views
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright (c) 2008 Jeremy Hinegardner
|
3
|
+
# All rights reserved. See LICENSE and/or COPYING for details.
|
4
|
+
#++
|
5
|
+
|
6
|
+
require 'amalgalite3'
|
7
|
+
module Amalgalite::SQLite3
|
8
|
+
##
|
9
|
+
# module containing all constants used from the SQLite C extension
|
10
|
+
#
|
11
|
+
module Constants
|
12
|
+
##
|
13
|
+
# DataType defines the namespace for all possible SQLite data types.
|
14
|
+
#
|
15
|
+
module DataType
|
16
|
+
end
|
17
|
+
DataType.freeze
|
18
|
+
|
19
|
+
##
|
20
|
+
# Open defines the namespace for all possible flags to the Database.open
|
21
|
+
# method
|
22
|
+
#
|
23
|
+
module Open
|
24
|
+
end
|
25
|
+
Open.freeze
|
26
|
+
|
27
|
+
##
|
28
|
+
# ResultCode defines the namespace for all possible result codes from an
|
29
|
+
# SQLite API call.
|
30
|
+
#
|
31
|
+
module ResultCode
|
32
|
+
#
|
33
|
+
# convert an integer value into the string representation of the associated
|
34
|
+
# ResultCode constant.
|
35
|
+
#
|
36
|
+
def self.from_int( value )
|
37
|
+
unless @const_map_from_int
|
38
|
+
@const_map_from_int = {}
|
39
|
+
constants.each do |const_name|
|
40
|
+
c_int = const_get( const_name )
|
41
|
+
@const_map_from_int[c_int] = const_name
|
42
|
+
end
|
43
|
+
end
|
44
|
+
return @const_map_from_int[ value ]
|
45
|
+
end
|
46
|
+
end # end ResultCode
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright (c) 2008 Jeremy Hinegardner
|
3
|
+
# All rights reserved. See LICENSE and/or COPYING for details.
|
4
|
+
#++
|
5
|
+
require 'amalgalite3'
|
6
|
+
module Amalgalite
|
7
|
+
module SQLite3
|
8
|
+
module Version
|
9
|
+
# Sqlite3 version number is equal to
|
10
|
+
# MAJOR * 1_000_000 + MINOR * 1_000 + RELEASE
|
11
|
+
|
12
|
+
# major version number of the SQLite C library
|
13
|
+
MAJOR = (to_i / 1_000_000).freeze
|
14
|
+
|
15
|
+
# minor version number of the SQLite C library
|
16
|
+
MINOR = ((to_i % 1_000_000) / 1_000).freeze
|
17
|
+
|
18
|
+
# release version number of the SQLite C library
|
19
|
+
RELEASE = (to_i % 1_000).freeze
|
20
|
+
|
21
|
+
#
|
22
|
+
# call-seq:
|
23
|
+
# Amalgalite::SQLite3::Version.to_a -> [ MAJOR, MINOR, RELEASE ]
|
24
|
+
#
|
25
|
+
# Return the SQLite C library version number as an array of MAJOR, MINOR,
|
26
|
+
# RELEASE
|
27
|
+
#
|
28
|
+
def self.to_a
|
29
|
+
[ MAJOR, MINOR, RELEASE ]
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
|
34
|
+
# Version of SQLite that ships with Amalgalite
|
35
|
+
VERSION = Version.to_s.freeze
|
36
|
+
end
|
37
|
+
Version.freeze
|
38
|
+
end
|
@@ -0,0 +1,307 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright (c) 2008 Jeremy Hinegardner
|
3
|
+
# All rights reserved. See LICENSE and/or COPYING for details.
|
4
|
+
#++
|
5
|
+
#
|
6
|
+
require 'amalgalite3'
|
7
|
+
require 'date'
|
8
|
+
require 'arrayfields'
|
9
|
+
require 'ostruct'
|
10
|
+
|
11
|
+
module Amalgalite
|
12
|
+
class Statement
|
13
|
+
|
14
|
+
include ::Amalgalite::SQLite3::Constants
|
15
|
+
|
16
|
+
attr_reader :db
|
17
|
+
attr_reader :sql
|
18
|
+
attr_reader :api
|
19
|
+
|
20
|
+
##
|
21
|
+
# Initialize a new statement on the database.
|
22
|
+
#
|
23
|
+
def initialize( db, sql )
|
24
|
+
@db = db
|
25
|
+
prepare_method = @db.utf16? ? :prepare16 : :prepare
|
26
|
+
@param_positions = {}
|
27
|
+
@stmt_api = @db.api.send( prepare_method, sql )
|
28
|
+
end
|
29
|
+
|
30
|
+
##
|
31
|
+
# reset the Statement back to it state right after the constructor returned,
|
32
|
+
# except if any variables have been bound to parameters, those are still
|
33
|
+
# bound.
|
34
|
+
#
|
35
|
+
def reset!
|
36
|
+
@stmt_api.reset!
|
37
|
+
@column_names = nil
|
38
|
+
@param_positions = {}
|
39
|
+
end
|
40
|
+
|
41
|
+
##
|
42
|
+
# reset the Statement back to it state right after the constructor returned,
|
43
|
+
# AND clear all parameter bindings.
|
44
|
+
#
|
45
|
+
def reset_and_clear_bindings!
|
46
|
+
reset!
|
47
|
+
@stmt_api.clear_bindings!
|
48
|
+
end
|
49
|
+
|
50
|
+
##
|
51
|
+
# Execute the statement with the given parameters
|
52
|
+
#
|
53
|
+
# If a block is given, then yield each returned row to the block. If no
|
54
|
+
# block is given then return all rows from the result
|
55
|
+
#
|
56
|
+
def execute( *params )
|
57
|
+
bind( *params )
|
58
|
+
if block_given? then
|
59
|
+
while row = next_row
|
60
|
+
yield row
|
61
|
+
end
|
62
|
+
else
|
63
|
+
all_rows
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
##
|
68
|
+
# Bind parameters to the sql statement.
|
69
|
+
#
|
70
|
+
# Bindings in SQLite can have a number of formats:
|
71
|
+
#
|
72
|
+
# ?
|
73
|
+
# ?num
|
74
|
+
# :var
|
75
|
+
# @var
|
76
|
+
# $var
|
77
|
+
#
|
78
|
+
# Where 'num' is an Integer and 'var'is an alphanumerical variable.
|
79
|
+
# They may exist in the SQL for which this Statement was created.
|
80
|
+
#
|
81
|
+
# Amalgalite binds parameters to these variables in the following manner:
|
82
|
+
#
|
83
|
+
# If bind is passed in an Array, either as +bind( "foo", "bar", "baz")+ or
|
84
|
+
# as bind( ["foo", "bar", "baz"] ) then each of the params is assumed to be
|
85
|
+
# positionally bound to the statement( ?, ?num ).
|
86
|
+
#
|
87
|
+
# If bind is passed a Hash, either as +bind( :foo => 1, :bar => 'sqlite' )+
|
88
|
+
# or as bind( { :foo => 1, 'bar' => 'sqlite' }) then it is assumed that each
|
89
|
+
# parameter should be bound as a named parameter (:var, @var, $var).
|
90
|
+
#
|
91
|
+
# If bind is not passed any parameters, or nil, then nothing happens.
|
92
|
+
#
|
93
|
+
def bind( *params )
|
94
|
+
if params.nil? or params.empty? then
|
95
|
+
check_parameter_count!( 0 )
|
96
|
+
return nil
|
97
|
+
end
|
98
|
+
|
99
|
+
if params.first.instance_of?( Hash ) then
|
100
|
+
bind_named_parameters( params.first )
|
101
|
+
else
|
102
|
+
bind_positional_parameters( params )
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
##
|
107
|
+
# Bind parameters to the statement based upon named parameters
|
108
|
+
#
|
109
|
+
def bind_named_parameters( params )
|
110
|
+
check_parameter_count!( params.size )
|
111
|
+
params.each_pair do | param, value |
|
112
|
+
position = param_position_of( param )
|
113
|
+
if position > 0 then
|
114
|
+
bind_parameter_to( position, value )
|
115
|
+
else
|
116
|
+
raise Amalgalite::Error, "Unable to find parameter '#{param}' in SQL statement [#{sql}]"
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
##
|
122
|
+
# Bind parameters to the statements based upon positions.
|
123
|
+
#
|
124
|
+
def bind_positional_parameters( params )
|
125
|
+
check_parameter_count!( params.size )
|
126
|
+
params.each_with_index do |value, index|
|
127
|
+
position = index + 1
|
128
|
+
bind_parameter_to( position, value )
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
##
|
133
|
+
# bind a single parameter to a particular position
|
134
|
+
#
|
135
|
+
def bind_parameter_to( position, value )
|
136
|
+
bind_type = db.type_map.bind_type_of( value )
|
137
|
+
case bind_type
|
138
|
+
when DataType::FLOAT
|
139
|
+
@stmt_api.bind_double( position, value )
|
140
|
+
when DataType::INTEGER
|
141
|
+
@stmt_api.bind_int64( position, value )
|
142
|
+
when DataType::NULL
|
143
|
+
@stmt_api.bind_null( position )
|
144
|
+
when DataType::TEXT
|
145
|
+
@stmt_api.bind_text( position, value.to_s )
|
146
|
+
when DataType::BLOB
|
147
|
+
raise NotImplemented, "Blob binding is not implemented yet"
|
148
|
+
else
|
149
|
+
raise ::Amalgalite::Error, "Unknown binding type of #{bind_type} from #{db.type_map.class.name}.bind_type_of"
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
|
154
|
+
##
|
155
|
+
# Find and cache the binding parameter indexes
|
156
|
+
#
|
157
|
+
def param_position_of( name )
|
158
|
+
ns = name.to_s
|
159
|
+
unless pos = @param_positions[ns]
|
160
|
+
pos = @param_positions[ns] = @stmt_api.parameter_index( ns )
|
161
|
+
end
|
162
|
+
return pos
|
163
|
+
end
|
164
|
+
|
165
|
+
##
|
166
|
+
# Check and make sure that the number of parameters aligns with the number
|
167
|
+
# that sqlite expects
|
168
|
+
#
|
169
|
+
def check_parameter_count!( num )
|
170
|
+
expected = @stmt_api.parameter_count
|
171
|
+
if num != expected then
|
172
|
+
raise Amalgalite::Error, "#{sql} has #{expected} parameters, but #{num} were passed to bind."
|
173
|
+
end
|
174
|
+
return expected
|
175
|
+
end
|
176
|
+
|
177
|
+
|
178
|
+
##
|
179
|
+
# Iterate over the results of the statement returning each row of results
|
180
|
+
# as a hash by +column_name+. The column names are the value after an
|
181
|
+
# 'AS' in the query or default chosen by sqlite.
|
182
|
+
#
|
183
|
+
def each
|
184
|
+
while row = next_row
|
185
|
+
yield row
|
186
|
+
end
|
187
|
+
return self
|
188
|
+
end
|
189
|
+
|
190
|
+
##
|
191
|
+
# Return the next row of data, with type conversion as indicated by the
|
192
|
+
# Database#type_map
|
193
|
+
#
|
194
|
+
def next_row
|
195
|
+
row = []
|
196
|
+
case rc = @stmt_api.step
|
197
|
+
when ResultCode::ROW
|
198
|
+
result_meta.each_with_index do |col, idx|
|
199
|
+
value = nil
|
200
|
+
column_type = @stmt_api.column_type( idx )
|
201
|
+
case column_type
|
202
|
+
when DataType::TEXT
|
203
|
+
value = @stmt_api.column_text( idx )
|
204
|
+
when DataType::FLOAT
|
205
|
+
value = @stmt_api.column_double( idx )
|
206
|
+
when DataType::INTEGER
|
207
|
+
value = @stmt_api.column_int64( idx )
|
208
|
+
when DataType::NULL
|
209
|
+
value = nil
|
210
|
+
when DataType::BLOB
|
211
|
+
raise NotImplemented, "returning a blob is not supported yet"
|
212
|
+
else
|
213
|
+
raise ::Amalgalite::Error, "BUG! : Unknown SQLite column type of #{column_type}"
|
214
|
+
end
|
215
|
+
|
216
|
+
row << db.type_map.result_value_of( col.schema.declared_data_type, value )
|
217
|
+
end
|
218
|
+
row.fields = result_fields
|
219
|
+
when ResultCode::DONE
|
220
|
+
row = nil
|
221
|
+
else
|
222
|
+
raise Amalgalite::SQLite3::Error,
|
223
|
+
"Received unexepcted result code #{rc} : #{Amalgalite::SQLite3::Constants::ResultCode.from_int( rc )}"
|
224
|
+
end
|
225
|
+
return row
|
226
|
+
end
|
227
|
+
|
228
|
+
##
|
229
|
+
# Return all rows from the statement as one array
|
230
|
+
#
|
231
|
+
def all_rows
|
232
|
+
rows = []
|
233
|
+
while row = next_row
|
234
|
+
rows << row
|
235
|
+
end
|
236
|
+
return rows
|
237
|
+
end
|
238
|
+
|
239
|
+
##
|
240
|
+
# Inspect the statement and gather all the meta information about the
|
241
|
+
# results, include the name of the column result column and the origin
|
242
|
+
# column. The origin column is the original database.table.column the value
|
243
|
+
# comes from.
|
244
|
+
#
|
245
|
+
# The full meta information from teh origin column is also obtained for help
|
246
|
+
# in doing type conversion.
|
247
|
+
#
|
248
|
+
def result_meta
|
249
|
+
unless @result_meta
|
250
|
+
meta = []
|
251
|
+
column_count.times do |idx|
|
252
|
+
column_meta = ::OpenStruct.new
|
253
|
+
column_meta.name = @stmt_api.column_name( idx )
|
254
|
+
|
255
|
+
db_name = @stmt_api.column_database_name( idx )
|
256
|
+
tbl_name = @stmt_api.column_table_name( idx )
|
257
|
+
col_name = @stmt_api.column_origin_name( idx )
|
258
|
+
|
259
|
+
column_meta.schema = ::Amalgalite::Column.new( db_name, tbl_name, col_name )
|
260
|
+
column_meta.schema.declared_data_type = @stmt_api.column_declared_type( idx )
|
261
|
+
|
262
|
+
meta << column_meta
|
263
|
+
end
|
264
|
+
@result_meta = meta
|
265
|
+
end
|
266
|
+
return @result_meta
|
267
|
+
end
|
268
|
+
|
269
|
+
##
|
270
|
+
# Return the array of field names for the result set, the field names are
|
271
|
+
# all strings
|
272
|
+
#
|
273
|
+
def result_fields
|
274
|
+
@fields ||= result_meta.collect { |m| m.name }
|
275
|
+
end
|
276
|
+
|
277
|
+
##
|
278
|
+
# Return any unsued SQL from the statement
|
279
|
+
#
|
280
|
+
def remaining_sql
|
281
|
+
@stmt_api.remaining_sql
|
282
|
+
end
|
283
|
+
|
284
|
+
|
285
|
+
##
|
286
|
+
# return the number of columns in the result of this query
|
287
|
+
#
|
288
|
+
def column_count
|
289
|
+
@stmt_api.column_count
|
290
|
+
end
|
291
|
+
|
292
|
+
##
|
293
|
+
# return the raw sql that was originally used to prepare the statement
|
294
|
+
#
|
295
|
+
def sql
|
296
|
+
@stmt_api.sql
|
297
|
+
end
|
298
|
+
|
299
|
+
##
|
300
|
+
# Close the statement. The statement is no longer valid for use after it
|
301
|
+
# has been closed.
|
302
|
+
#
|
303
|
+
def close
|
304
|
+
@stmt_api.close
|
305
|
+
end
|
306
|
+
end
|
307
|
+
end
|