sqlite3-ffi 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.
@@ -0,0 +1,242 @@
1
+ module SQLite3
2
+ class Statement
3
+ def prepare(db, sql)
4
+ sql = FFI.string_value(sql)
5
+
6
+ @db = db # so can check if discarded
7
+ db = db.instance_variable_get(:@db)
8
+ stmt = ::FFI::MemoryPointer.new(:pointer)
9
+ tail = ::FFI::MemoryPointer.new(:pointer)
10
+ status = FFI::CApi.sqlite3_prepare_v2(db, sql, sql.bytesize, stmt, tail)
11
+ FFI.check_prepare(db, status, sql)
12
+
13
+ @stmt = stmt.read_pointer
14
+ @db.instance_variable_set(:@stmt_deadline, nil)
15
+
16
+ tail.read_pointer.read_string.force_encoding(Encoding::UTF_8)
17
+ end
18
+
19
+ def close
20
+ require_open_stmt
21
+
22
+ FFI::CApi.sqlite3_finalize(@stmt)
23
+ @stmt = nil
24
+ end
25
+
26
+ def closed?
27
+ @stmt.nil? || @stmt.null?
28
+ end
29
+
30
+ def step
31
+ require_live_db
32
+ require_open_stmt
33
+
34
+ return nil if @done
35
+
36
+ stmt = @stmt
37
+
38
+ value = FFI::CApi.sqlite3_step(stmt)
39
+ if FFI.rb_errinfo
40
+ exception = FFI.rb_errinfo
41
+ FFI.rb_errinfo = nil
42
+ raise exception
43
+ end
44
+
45
+ length = FFI::CApi.sqlite3_column_count(stmt)
46
+ list = []
47
+
48
+ case value
49
+ when FFI::CApi::SQLITE_ROW
50
+ length.times do |i|
51
+ case FFI::CApi.sqlite3_column_type(stmt, i)
52
+ when FFI::CApi::SQLITE_INTEGER
53
+ val = FFI::CApi.sqlite3_column_int64(stmt, i)
54
+ when FFI::CApi::SQLITE_FLOAT
55
+ val = FFI::CApi.sqlite3_column_double(stmt, i)
56
+ when FFI::CApi::SQLITE_TEXT
57
+ len = FFI::CApi.sqlite3_column_bytes(stmt, i)
58
+ val = FFI::CApi.sqlite3_column_text(stmt, i).read_bytes(len).force_encoding(Encoding::UTF_8)
59
+ if Encoding.default_internal
60
+ val = val.encode(Encoding.default_internal)
61
+ end
62
+ val.freeze
63
+ when FFI::CApi::SQLITE_BLOB
64
+ len = FFI::CApi.sqlite3_column_bytes(stmt, i)
65
+ val = FFI::CApi.sqlite3_column_blob(stmt, i).read_bytes(len)
66
+ val.freeze
67
+ when FFI::CApi::SQLITE_NULL
68
+ val = nil
69
+ else
70
+ raise RuntimeError, "bad type"
71
+ end
72
+ list << val
73
+ end
74
+ when FFI::CApi::SQLITE_DONE
75
+ @done = true
76
+ return nil
77
+ else
78
+ FFI::CApi.sqlite3_reset(stmt)
79
+ @done = false
80
+ FFI.check(FFI::CApi.sqlite3_db_handle(stmt), value)
81
+ end
82
+
83
+ list.freeze
84
+ list
85
+ end
86
+
87
+ def bind_param(key, value)
88
+ require_live_db
89
+ require_open_stmt
90
+
91
+ case key
92
+ when Symbol, String
93
+ key = key.to_s
94
+ key = ":" + key if key[0] != ":"
95
+ index = FFI::CApi.sqlite3_bind_parameter_index(@stmt, key)
96
+ else
97
+ index = key.to_i
98
+ end
99
+
100
+ if index == 0
101
+ raise SQLite3::Exception, "no such bind parameter"
102
+ end
103
+
104
+ case value
105
+ when String
106
+ if value.is_a?(Blob) || value.encoding == Encoding::BINARY
107
+ status = FFI::CApi.sqlite3_bind_blob(@stmt, index, value, value.bytesize, FFI::CApi::SQLITE_TRANSIENT)
108
+ elsif value.encoding == Encoding::UTF_16LE || value.encoding == Encoding::UTF_16BE
109
+ status = FFI::CApi.sqlite3_bind_text16(@stmt, index, value, value.bytesize, FFI::CApi::SQLITE_TRANSIENT)
110
+ else
111
+ if value.encoding != Encoding::UTF_8 && value.encoding != Encoding::US_ASCII
112
+ value = value.encode(Encoding::UTF_8)
113
+ end
114
+ status = FFI::CApi.sqlite3_bind_text(@stmt, index, value, value.bytesize, FFI::CApi::SQLITE_TRANSIENT)
115
+ end
116
+ when Float
117
+ status = FFI::CApi.sqlite3_bind_double(@stmt, index, value)
118
+ when Integer
119
+ status = FFI::CApi.sqlite3_bind_int64(@stmt, index, value)
120
+ when NilClass
121
+ status = FFI::CApi.sqlite3_bind_null(@stmt, index)
122
+ else
123
+ raise RuntimeError, "can't prepare #{value.class.name}"
124
+ end
125
+
126
+ FFI.check(FFI::CApi.sqlite3_db_handle(@stmt), status)
127
+
128
+ self
129
+ end
130
+
131
+ def reset!
132
+ require_live_db
133
+ require_open_stmt
134
+
135
+ FFI::CApi.sqlite3_reset(@stmt)
136
+ @done = false
137
+ self
138
+ end
139
+
140
+ def clear_bindings!
141
+ require_live_db
142
+ require_open_stmt
143
+
144
+ FFI::CApi.sqlite3_clear_bindings(@stmt)
145
+ @done = false
146
+ end
147
+
148
+ def done?
149
+ @done
150
+ end
151
+
152
+ def column_count
153
+ require_live_db
154
+ require_open_stmt
155
+
156
+ FFI::CApi.sqlite3_column_count(@stmt)
157
+ end
158
+
159
+ def column_name(index)
160
+ require_live_db
161
+ require_open_stmt
162
+
163
+ name = FFI::CApi.sqlite3_column_name(@stmt, index)
164
+ name ? FFI.interned_utf8_cstr(name) : nil
165
+ end
166
+
167
+ def column_decltype(index)
168
+ require_live_db
169
+ require_open_stmt
170
+
171
+ FFI::CApi.sqlite3_column_decltype(@stmt, index)
172
+ end
173
+
174
+ def bind_parameter_count
175
+ require_live_db
176
+ require_open_stmt
177
+
178
+ FFI::CApi.sqlite3_bind_parameter_count(@stmt)
179
+ end
180
+
181
+ STMT_STAT_SYMBOLS = {
182
+ fullscan_steps: FFI::CApi::SQLITE_STMTSTATUS_FULLSCAN_STEP,
183
+ sorts: FFI::CApi::SQLITE_STMTSTATUS_SORT,
184
+ autoindexes: FFI::CApi::SQLITE_STMTSTATUS_AUTOINDEX,
185
+ vm_steps: FFI::CApi::SQLITE_STMTSTATUS_VM_STEP
186
+ }
187
+ private_constant :STMT_STAT_SYMBOLS
188
+
189
+ def stats_as_hash
190
+ require_live_db
191
+ require_open_stmt
192
+
193
+ STMT_STAT_SYMBOLS.to_h do |k, stat_type|
194
+ [k, FFI::CApi.sqlite3_stmt_status(@stmt, stat_type, 0)]
195
+ end
196
+ end
197
+
198
+ def stat_for(key)
199
+ require_live_db
200
+ require_open_stmt
201
+
202
+ if !key.is_a?(Symbol)
203
+ raise TypeError, "non-symbol given"
204
+ end
205
+
206
+ stat_type = STMT_STAT_SYMBOLS[key]
207
+ if !stat_type
208
+ raise ArgumentError, "unknown key: #{key}"
209
+ end
210
+
211
+ FFI::CApi.sqlite3_stmt_status(@stmt, stat_type, 0)
212
+ end
213
+
214
+ def sql
215
+ require_live_db
216
+ require_open_stmt
217
+
218
+ FFI::CApi.sqlite3_sql(@stmt).force_encoding(Encoding::UTF_8).freeze
219
+ end
220
+
221
+ def expanded_sql
222
+ require_live_db
223
+ require_open_stmt
224
+
225
+ FFI::CApi.sqlite3_expanded_sql(@stmt).force_encoding(Encoding::UTF_8).freeze
226
+ end
227
+
228
+ private
229
+
230
+ def require_open_stmt
231
+ if closed?
232
+ raise SQLite3::Exception, "cannot use a closed statement"
233
+ end
234
+ end
235
+
236
+ def require_live_db
237
+ if @db.instance_variable_get(:@discarded)
238
+ raise SQLite3::Exception, "cannot use a statement associated with a discarded database"
239
+ end
240
+ end
241
+ end
242
+ end
@@ -0,0 +1,83 @@
1
+ module SQLite3
2
+ module FFI
3
+ def self.sqlite3val2rb(val)
4
+ case CApi.sqlite3_value_type(val)
5
+ when CApi::SQLITE_INTEGER
6
+ CApi.sqlite3_value_int64(val)
7
+ when CApi::SQLITE_FLOAT
8
+ CApi.sqlite3_value_double(val)
9
+ when CApi::SQLITE_TEXT
10
+ len = CApi.sqlite3_value_bytes(val)
11
+ CApi.sqlite3_value_text(val).read_bytes(len).force_encoding(Encoding::UTF_8).freeze
12
+ when CApi::SQLITE_BLOB
13
+ len = CApi.sqlite3_value_bytes(val)
14
+ CApi.sqlite3_value_text(val).read_bytes(len).freeze
15
+ when CApi::SQLITE_NULL
16
+ nil
17
+ else
18
+ raise RuntimeError, "bad type"
19
+ end
20
+ end
21
+
22
+ def self.set_sqlite3_func_result(ctx, result)
23
+ case result
24
+ when NilClass
25
+ CApi.sqlite3_result_null(ctx)
26
+ when Integer
27
+ CApi.sqlite3_result_int64(ctx, result)
28
+ when Float
29
+ CApi.sqlite3_result_double(ctx, result)
30
+ when String
31
+ if result.is_a?(Blob) || result.encoding == Encoding::BINARY
32
+ CApi.sqlite3_result_blob(ctx, result, result.bytesize, CApi::SQLITE_TRANSIENT)
33
+ else
34
+ CApi.sqlite3_result_text(ctx, result, result.bytesize, CApi::SQLITE_TRANSIENT)
35
+ end
36
+ else
37
+ raise RuntimeError, "can't return #{result.class.name}"
38
+ end
39
+ end
40
+
41
+ # TODO intern
42
+ def self.interned_utf8_cstr(str)
43
+ str.freeze
44
+ end
45
+
46
+ def self.string_value(obj)
47
+ unless obj.respond_to?(:to_str)
48
+ val =
49
+ case obj
50
+ when nil
51
+ "nil"
52
+ when true, false
53
+ obj.to_s
54
+ else
55
+ obj.class.name
56
+ end
57
+ raise TypeError, "no implicit conversion of #{val} into String"
58
+ end
59
+ obj.to_str
60
+ end
61
+
62
+ RB_ERRINFO = :sqlite3_ffi_rb_errinfo
63
+
64
+ def self.rb_errinfo
65
+ Thread.current[RB_ERRINFO]
66
+ end
67
+
68
+ def self.rb_errinfo=(e)
69
+ Thread.current[RB_ERRINFO] = e
70
+ end
71
+
72
+ OBJECT_REGISTRY = ObjectSpace::WeakMap.new
73
+
74
+ def self.wrap(obj)
75
+ OBJECT_REGISTRY[obj.object_id] = obj
76
+ ::FFI::Pointer.new(obj.object_id)
77
+ end
78
+
79
+ def self.unwrap(ptr)
80
+ OBJECT_REGISTRY[ptr.to_i] || (raise "object not found")
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,5 @@
1
+ module SQLite3
2
+ module FFI
3
+ VERSION = "0.1.0"
4
+ end
5
+ end
@@ -0,0 +1 @@
1
+ require_relative "../sqlite3"
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "weakref"
4
+
5
+ module SQLite3
6
+ # based on Rails's active_support/fork_tracker.rb
7
+ module ForkSafety
8
+ module CoreExt # :nodoc:
9
+ def _fork
10
+ pid = super
11
+ if pid == 0
12
+ ForkSafety.discard
13
+ end
14
+ pid
15
+ end
16
+ end
17
+
18
+ @databases = []
19
+ @mutex = Mutex.new
20
+ @suppress = false
21
+
22
+ class << self
23
+ def hook! # :nodoc:
24
+ ::Process.singleton_class.prepend(CoreExt)
25
+ end
26
+
27
+ def track(database) # :nodoc:
28
+ @mutex.synchronize do
29
+ @databases << WeakRef.new(database)
30
+ end
31
+ end
32
+
33
+ def discard # :nodoc:
34
+ warned = @suppress
35
+ @databases.each do |db|
36
+ next unless db.weakref_alive?
37
+
38
+ begin
39
+ unless db.closed? || db.readonly?
40
+ unless warned
41
+ # If you are here, you may want to read
42
+ # https://github.com/sparklemotion/sqlite3-ruby/pull/558
43
+ warn("Writable sqlite database connection(s) were inherited from a forked process. " \
44
+ "This is unsafe and the connections are being closed to prevent possible data " \
45
+ "corruption. Please close writable sqlite database connections before forking.",
46
+ uplevel: 0)
47
+ warned = true
48
+ end
49
+ db.close
50
+ end
51
+ rescue WeakRef::RefError
52
+ # GC may run while this method is executing, and that's OK
53
+ end
54
+ end
55
+ @databases.clear
56
+ end
57
+
58
+ # Call to suppress the fork-related warnings.
59
+ def suppress_warnings!
60
+ @suppress = true
61
+ end
62
+ end
63
+ end
64
+ end
65
+
66
+ SQLite3::ForkSafety.hook!