amalgalite 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|