libsql 0.1.0-x64-mingw-ucrt
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CONTRIBUTING.md +60 -0
- data/HISTORY.md +6 -0
- data/LICENSE +31 -0
- data/Manifest.txt +96 -0
- data/README.md +59 -0
- data/Rakefile +28 -0
- data/TODO.md +57 -0
- data/examples/a.rb +9 -0
- data/examples/blob.rb +106 -0
- data/examples/define_aggregate.rb +75 -0
- data/examples/define_function.rb +104 -0
- data/examples/fts5.rb +152 -0
- data/examples/gem-db.rb +94 -0
- data/examples/schema-info.rb +34 -0
- data/ext/libsql/c/extconf.rb +86 -0
- data/ext/libsql/c/gen_constants.rb +353 -0
- data/ext/libsql/c/libsql_blob.c +240 -0
- data/ext/libsql/c/libsql_constants.c +1518 -0
- data/ext/libsql/c/libsql_database.c +1188 -0
- data/ext/libsql/c/libsql_ext.c +383 -0
- data/ext/libsql/c/libsql_ext.h +149 -0
- data/ext/libsql/c/libsql_statement.c +649 -0
- data/ext/libsql/c/notes.txt +134 -0
- data/ext/libsql/c/sqlite3.c +247030 -0
- data/ext/libsql/c/sqlite3.h +13436 -0
- data/lib/libsql/3.1/libsql_ext.so +0 -0
- data/lib/libsql/3.2/libsql_ext.so +0 -0
- data/lib/libsql/aggregate.rb +73 -0
- data/lib/libsql/blob.rb +186 -0
- data/lib/libsql/boolean.rb +42 -0
- data/lib/libsql/busy_timeout.rb +47 -0
- data/lib/libsql/column.rb +99 -0
- data/lib/libsql/csv_table_importer.rb +75 -0
- data/lib/libsql/database.rb +933 -0
- data/lib/libsql/function.rb +61 -0
- data/lib/libsql/index.rb +43 -0
- data/lib/libsql/memory_database.rb +15 -0
- data/lib/libsql/paths.rb +80 -0
- data/lib/libsql/profile_tap.rb +131 -0
- data/lib/libsql/progress_handler.rb +21 -0
- data/lib/libsql/schema.rb +225 -0
- data/lib/libsql/sqlite3/constants.rb +95 -0
- data/lib/libsql/sqlite3/database/function.rb +48 -0
- data/lib/libsql/sqlite3/database/status.rb +68 -0
- data/lib/libsql/sqlite3/libsql_version.rb +32 -0
- data/lib/libsql/sqlite3/status.rb +60 -0
- data/lib/libsql/sqlite3/version.rb +55 -0
- data/lib/libsql/sqlite3.rb +7 -0
- data/lib/libsql/statement.rb +421 -0
- data/lib/libsql/table.rb +91 -0
- data/lib/libsql/taps/console.rb +27 -0
- data/lib/libsql/taps/io.rb +74 -0
- data/lib/libsql/taps.rb +2 -0
- data/lib/libsql/trace_tap.rb +35 -0
- data/lib/libsql/type_map.rb +63 -0
- data/lib/libsql/type_maps/default_map.rb +166 -0
- data/lib/libsql/type_maps/storage_map.rb +38 -0
- data/lib/libsql/type_maps/text_map.rb +21 -0
- data/lib/libsql/version.rb +8 -0
- data/lib/libsql/view.rb +26 -0
- data/lib/libsql-ruby.rb +1 -0
- data/lib/libsql.rb +51 -0
- data/spec/aggregate_spec.rb +158 -0
- data/spec/blob_spec.rb +78 -0
- data/spec/boolean_spec.rb +24 -0
- data/spec/busy_handler.rb +157 -0
- data/spec/data/iso-3166-country.txt +242 -0
- data/spec/data/iso-3166-schema.sql +22 -0
- data/spec/data/iso-3166-subcountry.txt +3995 -0
- data/spec/data/make-iso-db.sh +12 -0
- data/spec/database_spec.rb +505 -0
- data/spec/default_map_spec.rb +92 -0
- data/spec/function_spec.rb +78 -0
- data/spec/integeration_spec.rb +97 -0
- data/spec/iso_3166_database.rb +58 -0
- data/spec/json_spec.rb +24 -0
- data/spec/libsql_spec.rb +4 -0
- data/spec/paths_spec.rb +28 -0
- data/spec/progress_handler_spec.rb +91 -0
- data/spec/rtree_spec.rb +66 -0
- data/spec/schema_spec.rb +131 -0
- data/spec/spec_helper.rb +48 -0
- data/spec/sqlite3/constants_spec.rb +108 -0
- data/spec/sqlite3/database_status_spec.rb +36 -0
- data/spec/sqlite3/libsql_version_spec.rb +16 -0
- data/spec/sqlite3/status_spec.rb +22 -0
- data/spec/sqlite3/version_spec.rb +28 -0
- data/spec/sqlite3_spec.rb +53 -0
- data/spec/statement_spec.rb +168 -0
- data/spec/storage_map_spec.rb +38 -0
- data/spec/tap_spec.rb +57 -0
- data/spec/text_map_spec.rb +20 -0
- data/spec/type_map_spec.rb +14 -0
- data/spec/version_spec.rb +8 -0
- data/tasks/custom.rake +134 -0
- data/tasks/default.rake +257 -0
- data/tasks/extension.rake +29 -0
- data/tasks/this.rb +208 -0
- metadata +329 -0
Binary file
|
Binary file
|
@@ -0,0 +1,73 @@
|
|
1
|
+
require 'libsql/sqlite3/database/function'
|
2
|
+
module ::Libsql
|
3
|
+
#
|
4
|
+
# A Base class to inherit from for creating your own SQL aggregate functions
|
5
|
+
# in ruby.
|
6
|
+
#
|
7
|
+
# These are SQL functions similar to _max(X)_, _count(X)_, _avg(X)_. The built
|
8
|
+
# in SQLite aggregate functions are:
|
9
|
+
#
|
10
|
+
# * http://www.sqlite.org/lang_aggfunc.html
|
11
|
+
#
|
12
|
+
# If you choose to use Aggregate as a parent class of your SQL scalar function
|
13
|
+
# implementation you must:
|
14
|
+
#
|
15
|
+
# * implement _initalize_ with 0 arguments
|
16
|
+
# * call super() in your _initialize_ method
|
17
|
+
# * set the @arity data member
|
18
|
+
# * set the @name data member
|
19
|
+
# * implement _step_ with arity of +@arity+
|
20
|
+
# * implement _finalize_ with arity of 0
|
21
|
+
#
|
22
|
+
# For instance to implement a <i>unique_word_count(X)</i> aggregate function you could
|
23
|
+
# implement it as:
|
24
|
+
#
|
25
|
+
# class UniqueWordCount < ::Libsql::Aggregate
|
26
|
+
# attr_accessor :words
|
27
|
+
#
|
28
|
+
# def initialize
|
29
|
+
# super
|
30
|
+
# @name = 'unique_word_count'
|
31
|
+
# @arity = 1
|
32
|
+
# @words = Hash.new { |h,k| h[k] = 0 }
|
33
|
+
# end
|
34
|
+
#
|
35
|
+
# def step( str )
|
36
|
+
# str.split(/\W+/).each do |word|
|
37
|
+
# words[ word.downcase ] += 1
|
38
|
+
# end
|
39
|
+
# return nil
|
40
|
+
# end
|
41
|
+
#
|
42
|
+
# def finalize
|
43
|
+
# return words.size
|
44
|
+
# end
|
45
|
+
# end
|
46
|
+
#
|
47
|
+
#
|
48
|
+
class Aggregate
|
49
|
+
# The name of the SQL function
|
50
|
+
attr_accessor :name
|
51
|
+
|
52
|
+
# The arity of the SQL function
|
53
|
+
attr_accessor :arity
|
54
|
+
|
55
|
+
def initialize
|
56
|
+
@_exception = nil
|
57
|
+
end
|
58
|
+
|
59
|
+
# finalize should return the final value of the aggregate function
|
60
|
+
def finalize
|
61
|
+
raise NotImplementedError, "Aggregate#finalize must be implemented"
|
62
|
+
end
|
63
|
+
|
64
|
+
# <b>Do Not Override</b>
|
65
|
+
#
|
66
|
+
# The function signature for use by the Amaglaite datase in tracking
|
67
|
+
# function creation.
|
68
|
+
#
|
69
|
+
def signature
|
70
|
+
@signature ||= ::Libsql::SQLite3::Database::Function.signature( self.name, self.arity )
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
data/lib/libsql/blob.rb
ADDED
@@ -0,0 +1,186 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright (c) 2023 Jeremy Hinegardner
|
3
|
+
# All rights reserved. See LICENSE and/or COPYING for details.
|
4
|
+
#++
|
5
|
+
module ::Libsql
|
6
|
+
##
|
7
|
+
# This is the interface to allow Blob objects to be written to and read from
|
8
|
+
# the SQLite database. When using statements, use a Blob object as
|
9
|
+
# the wrapper around the source to be written to the row, and a Blob object is
|
10
|
+
# returned if the the type mapping warrents during select queries.
|
11
|
+
#
|
12
|
+
# For instance during an insert:
|
13
|
+
#
|
14
|
+
# blob_column = db.schema.tables['blobs'].columns['data']
|
15
|
+
# db.execute("INSERT INTO blobs(name, data) VALUES ( $name, $blob )",
|
16
|
+
# { "$name" => "/path/to/file",
|
17
|
+
# "$blob" => ::Libsql::Blob.new( :file => '/path/to/file',
|
18
|
+
# :column => blob_column) } )
|
19
|
+
#
|
20
|
+
# db.execute("INSERT INTO blobs(id, data) VALUES ($id, $blob )",
|
21
|
+
# { "$name" => 'blobname',
|
22
|
+
# "$blob" => ::Libsql::Blob.new( :io => "something with .read and .length methods",
|
23
|
+
# :column => blob_column) } )
|
24
|
+
#
|
25
|
+
# On select the blob data needs to be read into an IO object
|
26
|
+
#
|
27
|
+
# all_rows = db.execute("SELECT name, blob FROM blobs WHERE name = '/path/to/file' ")
|
28
|
+
# blob_row = all_rows.first
|
29
|
+
# blob_row['blob'].write_to_file( blob_row['name'] )
|
30
|
+
#
|
31
|
+
# Or write to an IO object
|
32
|
+
#
|
33
|
+
# blob_results = {}
|
34
|
+
# db.execute("SELECT name, blob FROM blobs") do |row|
|
35
|
+
# io = StringIO.new
|
36
|
+
# row['blob'].write_to_io( io )
|
37
|
+
# blob_results[row['name']] = io
|
38
|
+
# # or use a shortcut
|
39
|
+
# # blob_results[row['name']] = row['blob'].to_string_io
|
40
|
+
# end
|
41
|
+
#
|
42
|
+
# If using a Blob as a conditional, for instance in a WHERE clause then the
|
43
|
+
# Blob must resolvable to a String.
|
44
|
+
#
|
45
|
+
# db.execute("SELECT FROM blobs(name, data) WHERE data = $blob",
|
46
|
+
# { "$blob' => ::Libsql::Blob.new( :string => "A string of data" ) })
|
47
|
+
#
|
48
|
+
class Blob
|
49
|
+
class Error < ::Libsql::Error; end
|
50
|
+
class << self
|
51
|
+
def valid_source_params
|
52
|
+
@valid_source_params ||= [ :file, :io, :string, :db_blob ]
|
53
|
+
end
|
54
|
+
|
55
|
+
def default_block_size
|
56
|
+
@default_block_size ||= 8192
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# the object representing the source of the blob
|
61
|
+
attr_reader :source
|
62
|
+
|
63
|
+
# the size in bytes of the of the blob
|
64
|
+
attr_reader :length
|
65
|
+
|
66
|
+
# the size in bytes of the blocks of data to move from the source
|
67
|
+
attr_reader :block_size
|
68
|
+
|
69
|
+
# the column the blob is associated with
|
70
|
+
attr_reader :column
|
71
|
+
|
72
|
+
##
|
73
|
+
# Initialize a new blob, it takes a single parameter, a hash which describes
|
74
|
+
# the source of the blob. The keys of the hash are one of:
|
75
|
+
#
|
76
|
+
# :file : the value is the path to a file on the file system
|
77
|
+
# :io : the value is an object that responds to the the methods +read+
|
78
|
+
# and +length+. +read+ should have the behavior of IO#read
|
79
|
+
# :db_blob : not normally used by an end user, used to initialize a blob
|
80
|
+
# object that is returned from an SQL query.
|
81
|
+
# :string : used when a Blob is part of a WHERE clause or result
|
82
|
+
#
|
83
|
+
# And additional key of :block_size may be used to indicate the maximum size
|
84
|
+
# of a single block of data to move from the source to the destination, this
|
85
|
+
# defaults ot 8192.
|
86
|
+
#
|
87
|
+
def initialize( params )
|
88
|
+
if (Blob.valid_source_params & params.keys).size > 1 then
|
89
|
+
raise Blob::Error, "Only a one of #{Blob.valid_source_params.join(', ')} is allowed to initialize a Blob. #{params.keys.join(', ')} were sent"
|
90
|
+
end
|
91
|
+
|
92
|
+
@source = nil
|
93
|
+
@source_length = 0
|
94
|
+
@close_source_after_read = false
|
95
|
+
@incremental = true
|
96
|
+
@block_size = params[:block_size] || Blob.default_block_size
|
97
|
+
@column = params[:column]
|
98
|
+
|
99
|
+
raise Blob::Error, "A :column parameter is required for a Blob" unless @column or params.has_key?( :string )
|
100
|
+
|
101
|
+
if params.has_key?( :file ) then
|
102
|
+
@source = File.open( params[:file], "r" )
|
103
|
+
@length = File.size( params[:file] )
|
104
|
+
@close_source_after_read = true
|
105
|
+
elsif params.has_key?( :io ) then
|
106
|
+
@source = params[:io]
|
107
|
+
@length = @source.length
|
108
|
+
elsif params.has_key?( :db_blob ) then
|
109
|
+
@source = params[:db_blob]
|
110
|
+
@length = @source.length
|
111
|
+
@close_source_after_read = true
|
112
|
+
elsif params.has_key?( :string ) then
|
113
|
+
@source = params[:string]
|
114
|
+
@length = @source.length
|
115
|
+
@incremental = false
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
##
|
120
|
+
# close the source when done reading from it
|
121
|
+
#
|
122
|
+
def close_source_after_read?
|
123
|
+
@close_source_after_read
|
124
|
+
end
|
125
|
+
|
126
|
+
##
|
127
|
+
# is this an incremental Blob or not
|
128
|
+
#
|
129
|
+
def incremental?
|
130
|
+
@incremental
|
131
|
+
end
|
132
|
+
|
133
|
+
##
|
134
|
+
# Write the Blob to an IO object
|
135
|
+
#
|
136
|
+
def write_to_io( io )
|
137
|
+
if source.respond_to?( :read ) then
|
138
|
+
while buf = source.read( block_size ) do
|
139
|
+
io.write( buf )
|
140
|
+
end
|
141
|
+
else
|
142
|
+
io.write( source.to_s )
|
143
|
+
end
|
144
|
+
|
145
|
+
if close_source_after_read? then
|
146
|
+
source.close
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
##
|
151
|
+
# conver the blob to a string
|
152
|
+
#
|
153
|
+
def to_s
|
154
|
+
to_string_io.string
|
155
|
+
end
|
156
|
+
|
157
|
+
##
|
158
|
+
# write the Blob contents to a StringIO
|
159
|
+
#
|
160
|
+
def to_string_io
|
161
|
+
sio = StringIO.new
|
162
|
+
write_to_io( sio )
|
163
|
+
return sio
|
164
|
+
end
|
165
|
+
|
166
|
+
##
|
167
|
+
# Write the Blob contents to a File.
|
168
|
+
#
|
169
|
+
def write_to_file( filename, modestring="w" )
|
170
|
+
File.open(filename, modestring) do |f|
|
171
|
+
write_to_io( f )
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
##
|
176
|
+
# Write the Blob contents to the column. This assumes that the row_id to
|
177
|
+
# insert into is the last row that was inserted into the db
|
178
|
+
#
|
179
|
+
def write_to_column!
|
180
|
+
last_rowid = column.schema.db.last_insert_rowid
|
181
|
+
SQLite3::Blob.new( column.schema.db.api, column.db, column.table, column.name, last_rowid, "w" ) do |sqlite_blob|
|
182
|
+
write_to_io( sqlite_blob )
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright (c) 2023 Jeremy Hinegardner
|
3
|
+
# All rights reserved. See LICENSE and/or COPYING for details.
|
4
|
+
#++
|
5
|
+
module ::Libsql
|
6
|
+
##
|
7
|
+
# Do type conversion on values that could be boolen values into
|
8
|
+
# real 'true' or 'false'
|
9
|
+
#
|
10
|
+
# This is pulled from the possible boolean values from PostgreSQL
|
11
|
+
#
|
12
|
+
class Boolean
|
13
|
+
class << self
|
14
|
+
#
|
15
|
+
# list of downcased strings are potential true values
|
16
|
+
#
|
17
|
+
def true_values
|
18
|
+
@true_values ||= %w[ true t yes y 1 ]
|
19
|
+
end
|
20
|
+
|
21
|
+
#
|
22
|
+
# list of downcased strings are potential false values
|
23
|
+
#
|
24
|
+
def false_values
|
25
|
+
@false_values ||= %w[ false f no n 0 ]
|
26
|
+
end
|
27
|
+
|
28
|
+
#
|
29
|
+
# Convert +val+ to a string and attempt to convert it to +true+ or +false+
|
30
|
+
#
|
31
|
+
def to_bool( val )
|
32
|
+
return false if val.nil?
|
33
|
+
unless defined? @to_bool
|
34
|
+
@to_bool = {}
|
35
|
+
true_values.each { |t| @to_bool[t] = true }
|
36
|
+
false_values.each { |f| @to_bool[f] = false }
|
37
|
+
end
|
38
|
+
return @to_bool[val.to_s.downcase]
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module ::Libsql
|
2
|
+
##
|
3
|
+
# A base class for use in creating your own busy handler classes
|
4
|
+
#
|
5
|
+
class BusyHandler
|
6
|
+
def to_proc
|
7
|
+
self
|
8
|
+
end
|
9
|
+
|
10
|
+
# the arity of the call method
|
11
|
+
def arity() 1 ; end
|
12
|
+
|
13
|
+
##
|
14
|
+
# Override this method, returning +false+ if the SQLite should return
|
15
|
+
# SQLITE_BUSY for all parties involved in the lock, and anything else if the
|
16
|
+
# lock attempt should be tried again.
|
17
|
+
def call( count )
|
18
|
+
raise NotImplementedError, "The busy handler call(N) method must be implemented"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
##
|
23
|
+
# A busy time out class for use in Database#define_busy_handler
|
24
|
+
#
|
25
|
+
class BusyTimeout < BusyHandler
|
26
|
+
attr_reader :call_count
|
27
|
+
##
|
28
|
+
# intialize by setting _count_ and _duration_ ( in milliseconds ).
|
29
|
+
#
|
30
|
+
def initialize( count = 20 , duration = 50 )
|
31
|
+
@count = count
|
32
|
+
@duration = duration.to_f / 1_000
|
33
|
+
@call_count = 0
|
34
|
+
end
|
35
|
+
|
36
|
+
##
|
37
|
+
# return +false+ if _callcount_ is > _count_ otherwise sleep for _duration_
|
38
|
+
# milliseconds and then return +true+
|
39
|
+
#
|
40
|
+
def call( call_count )
|
41
|
+
@call_count = call_count
|
42
|
+
return false if ( call_count > @count )
|
43
|
+
sleep @duration
|
44
|
+
return true
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright (c) 2023 Jeremy Hinegardner
|
3
|
+
# All rights reserved. See LICENSE and/or COPYING for details.
|
4
|
+
#++
|
5
|
+
|
6
|
+
require 'libsql/boolean'
|
7
|
+
require 'libsql/blob'
|
8
|
+
|
9
|
+
module ::Libsql
|
10
|
+
##
|
11
|
+
# a class representing the meta information about an SQLite column, this class
|
12
|
+
# serves both for general Schema level information, and for result set
|
13
|
+
# information from a SELECT query.
|
14
|
+
#
|
15
|
+
class Column
|
16
|
+
# the schema object this column is associated with
|
17
|
+
attr_accessor :schema
|
18
|
+
|
19
|
+
# the database name this column belongs to. This will be 'main' for the main
|
20
|
+
# database, 'temp' for the temp database and whatever an attached database
|
21
|
+
# was attached as.
|
22
|
+
attr_accessor :db
|
23
|
+
|
24
|
+
# the table to which this column belongs
|
25
|
+
attr_accessor :table
|
26
|
+
|
27
|
+
# the column name
|
28
|
+
attr_accessor :name
|
29
|
+
|
30
|
+
# the default value of the column. This may not have a value and that
|
31
|
+
# either means that there is no default value, or one could not be
|
32
|
+
# determined.
|
33
|
+
#
|
34
|
+
attr_accessor :default_value
|
35
|
+
|
36
|
+
# the declared data type of the column in the original sql that created the
|
37
|
+
# column
|
38
|
+
attr_accessor :declared_data_type
|
39
|
+
|
40
|
+
# the collation sequence name of the column
|
41
|
+
attr_accessor :collation_sequence_name
|
42
|
+
|
43
|
+
# The index (starting with 0) of this column in the table definition
|
44
|
+
# or result set
|
45
|
+
attr_accessor :order
|
46
|
+
|
47
|
+
##
|
48
|
+
# Create a column with its name and associated table
|
49
|
+
#
|
50
|
+
def initialize( db, table, name, order)
|
51
|
+
@db = db
|
52
|
+
@table = table
|
53
|
+
@name = name
|
54
|
+
@order = Float(order).to_i
|
55
|
+
@declared_data_type = nil
|
56
|
+
@default_value = nil
|
57
|
+
end
|
58
|
+
|
59
|
+
# true if the column has a default value
|
60
|
+
def has_default_value?
|
61
|
+
not default_value.nil?
|
62
|
+
end
|
63
|
+
|
64
|
+
# true if the column may have a NULL value
|
65
|
+
def nullable?
|
66
|
+
@not_null_constraint == false
|
67
|
+
end
|
68
|
+
|
69
|
+
# set whether or not the column has a not null constraint
|
70
|
+
def not_null_constraint=( other )
|
71
|
+
@not_null_constraint = Boolean.to_bool( other )
|
72
|
+
end
|
73
|
+
|
74
|
+
# true if the column as a NOT NULL constraint
|
75
|
+
def not_null_constraint?
|
76
|
+
@not_null_constraint
|
77
|
+
end
|
78
|
+
|
79
|
+
# set whether or not the column is a primary key column
|
80
|
+
def primary_key=( other )
|
81
|
+
@primary_key = Boolean.to_bool( other )
|
82
|
+
end
|
83
|
+
|
84
|
+
# true if the column is a primary key column
|
85
|
+
def primary_key?
|
86
|
+
@primary_key
|
87
|
+
end
|
88
|
+
|
89
|
+
# set whether or not the column is auto increment
|
90
|
+
def auto_increment=( other )
|
91
|
+
@auto_increment = Boolean.to_bool( other )
|
92
|
+
end
|
93
|
+
|
94
|
+
# true if the column is auto increment
|
95
|
+
def auto_increment?
|
96
|
+
@auto_increment
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
if RUBY_VERSION >= "1.9"
|
2
|
+
require 'csv'
|
3
|
+
else
|
4
|
+
require 'fastercsv'
|
5
|
+
::CSV = ::FasterCSV
|
6
|
+
end
|
7
|
+
module ::Libsql
|
8
|
+
##
|
9
|
+
# A class to deal with importing CSV data into a single table in the
|
10
|
+
# database.
|
11
|
+
#
|
12
|
+
class CSVTableImporter
|
13
|
+
def initialize( csv_path, database, table_name, options = {} )
|
14
|
+
@csv_path = File.expand_path( csv_path )
|
15
|
+
@database = database
|
16
|
+
@table_name = table_name
|
17
|
+
@table = @database.schema.tables[@table_name]
|
18
|
+
@options = options.dup
|
19
|
+
@encoding = options.delete("encoding") || "UTF-8"
|
20
|
+
validate
|
21
|
+
end
|
22
|
+
|
23
|
+
def run
|
24
|
+
@database.transaction do |db|
|
25
|
+
db.prepare( insert_sql ) do |stmt|
|
26
|
+
::CSV.foreach( @csv_path, "r:#{@encoding}", **@options ) do |row|
|
27
|
+
stmt.execute( row )
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
##
|
34
|
+
# The column names of the import table in definiation order
|
35
|
+
#
|
36
|
+
def column_names
|
37
|
+
@table.columns_in_order.collect { |c| c.name }
|
38
|
+
end
|
39
|
+
|
40
|
+
##
|
41
|
+
# The columns used for the insertion. This is either #column_names
|
42
|
+
# or the value out of @options[:headers] if that value is an Array
|
43
|
+
#
|
44
|
+
def insert_column_list
|
45
|
+
column_list = self.column_names
|
46
|
+
if Array === @options[:headers] then
|
47
|
+
column_list = @options[:headers]
|
48
|
+
end
|
49
|
+
return column_list
|
50
|
+
end
|
51
|
+
|
52
|
+
##
|
53
|
+
# The prepared statement SQL that is used for the import
|
54
|
+
#
|
55
|
+
def insert_sql
|
56
|
+
column_sql = insert_column_list.join(",")
|
57
|
+
vars = insert_column_list.collect { |x| "?" }.join(",")
|
58
|
+
return "INSERT INTO #{@table_name}(#{column_sql}) VALUES (#{vars})"
|
59
|
+
end
|
60
|
+
|
61
|
+
def table_list
|
62
|
+
@database.schema.tables.keys
|
63
|
+
end
|
64
|
+
|
65
|
+
##
|
66
|
+
# validate that the arguments for initialization are valid and that the #run
|
67
|
+
# method will probably execute
|
68
|
+
#
|
69
|
+
def validate
|
70
|
+
raise ArgumentError, "CSV file #{@csv_path} does not exist" unless File.exist?( @csv_path )
|
71
|
+
raise ArgumentError, "CSV file #{@csv_path} is not readable" unless File.readable?( @csv_path )
|
72
|
+
raise ArgumentError, "The table '#{@table_name} is not found in the database. The known tables are #{table_list.sort.join(", ")}" unless @table
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|