bormashino-sequel-sqljs-adapter 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE.txt +43 -0
- data/README.md +5 -0
- data/lib/bormashino_sequel_sqljs_adapter/shared/sqljs.rb +1068 -0
- data/lib/bormashino_sequel_sqljs_adapter/sqljs.rb +458 -0
- data/lib/bormashino_sequel_sqljs_adapter/version.rb +5 -0
- data/lib/bormashino_sequel_sqljs_adapter.rb +6 -0
- metadata +67 -0
@@ -0,0 +1,458 @@
|
|
1
|
+
require_relative 'shared/sqljs'
|
2
|
+
Sequel.single_threaded = true
|
3
|
+
|
4
|
+
module Sequel
|
5
|
+
module Sqljs
|
6
|
+
class Wrapper
|
7
|
+
class Error < StandardError
|
8
|
+
end
|
9
|
+
|
10
|
+
def initialize(jsobject)
|
11
|
+
@db = jsobject
|
12
|
+
end
|
13
|
+
|
14
|
+
def exec(sql)
|
15
|
+
columns = @db.call(:prepareGetColumnNames, sql).to_rb
|
16
|
+
ret = @db.call(:exec, sql).to_rb
|
17
|
+
|
18
|
+
if ret.is_a?(Hash) && ret['type'] == 'exception'
|
19
|
+
err = Error.new(ret['message']).tap do |e|
|
20
|
+
e.set_backtrace(ret['stack'])
|
21
|
+
end
|
22
|
+
|
23
|
+
raise err
|
24
|
+
end
|
25
|
+
|
26
|
+
{ result: ret, columns: }
|
27
|
+
end
|
28
|
+
|
29
|
+
def get_rows_modified
|
30
|
+
@db.call(:getRowsModified).to_rb
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
FALSE_VALUES = (%w'0 false f no n'.each(&:freeze) + [0]).freeze
|
35
|
+
|
36
|
+
blob = Object.new
|
37
|
+
def blob.call(s)
|
38
|
+
Sequel::SQL::Blob.new(s.to_s)
|
39
|
+
end
|
40
|
+
|
41
|
+
boolean = Object.new
|
42
|
+
def boolean.call(s)
|
43
|
+
s = s.downcase if s.is_a?(String)
|
44
|
+
!FALSE_VALUES.include?(s)
|
45
|
+
end
|
46
|
+
|
47
|
+
date = Object.new
|
48
|
+
def date.call(s)
|
49
|
+
case s
|
50
|
+
when String
|
51
|
+
Sequel.string_to_date(s)
|
52
|
+
when Integer
|
53
|
+
Date.jd(s)
|
54
|
+
when Float
|
55
|
+
Date.jd(s.to_i)
|
56
|
+
else
|
57
|
+
raise Sequel::Error, "unhandled type when converting to date: #{s.inspect} (#{s.class.inspect})"
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
integer = Object.new
|
62
|
+
def integer.call(s)
|
63
|
+
s ? s.to_i : nil
|
64
|
+
end
|
65
|
+
|
66
|
+
float = Object.new
|
67
|
+
def float.call(s)
|
68
|
+
s.to_f
|
69
|
+
end
|
70
|
+
|
71
|
+
numeric = Object.new
|
72
|
+
def numeric.call(s)
|
73
|
+
s = s.to_s unless s.is_a?(String)
|
74
|
+
begin
|
75
|
+
BigDecimal(s)
|
76
|
+
rescue StandardError
|
77
|
+
s
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
time = Object.new
|
82
|
+
def time.call(s)
|
83
|
+
case s
|
84
|
+
when String
|
85
|
+
Sequel.string_to_time(s)
|
86
|
+
when Integer
|
87
|
+
Sequel::SQLTime.create(s / 3600, (s % 3600) / 60, s % 60)
|
88
|
+
when Float
|
89
|
+
s, f = s.divmod(1)
|
90
|
+
Sequel::SQLTime.create(s / 3600, (s % 3600) / 60, s % 60, (f * 1000000).round)
|
91
|
+
else
|
92
|
+
raise Sequel::Error, "unhandled type when converting to date: #{s.inspect} (#{s.class.inspect})"
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
# Hash with string keys and callable values for converting SQLite types.
|
97
|
+
sqlite_types = {}
|
98
|
+
{
|
99
|
+
%w[date] => date,
|
100
|
+
%w[time] => time,
|
101
|
+
%w[bit bool boolean] => boolean,
|
102
|
+
%w[integer smallint mediumint int bigint] => integer,
|
103
|
+
%w[numeric decimal money] => numeric,
|
104
|
+
%w[float double real dec fixed] + ['double precision'] => float,
|
105
|
+
%w[blob] => blob,
|
106
|
+
}.each do |k, v|
|
107
|
+
k.each { |n| sqlite_types[n] = v }
|
108
|
+
end
|
109
|
+
SQLITE_TYPES = sqlite_types
|
110
|
+
|
111
|
+
class Database < Sequel::Database
|
112
|
+
include ::Sequel::Sqljs::DatabaseMethods
|
113
|
+
|
114
|
+
set_adapter_scheme :sqljs
|
115
|
+
|
116
|
+
# Mimic the file:// uri, by having 2 preceding slashes specify a relative
|
117
|
+
# path, and 3 preceding slashes specify an absolute path.
|
118
|
+
def self.uri_to_options(uri) # :nodoc:
|
119
|
+
{ database: uri.host.nil? && uri.path == '/' ? nil : "#{uri.host}#{uri.path}" }
|
120
|
+
end
|
121
|
+
|
122
|
+
private_class_method :uri_to_options
|
123
|
+
|
124
|
+
# The conversion procs to use for this database
|
125
|
+
attr_reader :conversion_procs
|
126
|
+
|
127
|
+
def initialize(opts = OPTS)
|
128
|
+
super
|
129
|
+
@allow_regexp = typecast_value_boolean(opts[:setup_regexp_function])
|
130
|
+
end
|
131
|
+
|
132
|
+
# Connect to the database. Since SQLite is a file based database,
|
133
|
+
# available options are limited:
|
134
|
+
#
|
135
|
+
# :database :: database name (filename or ':memory:' or file: URI)
|
136
|
+
# :readonly :: open database in read-only mode; useful for reading
|
137
|
+
# static data that you do not want to modify
|
138
|
+
# :timeout :: how long to wait for the database to be available if it
|
139
|
+
# is locked, given in milliseconds (default is 5000)
|
140
|
+
def connect(server)
|
141
|
+
opts = server_opts(server)
|
142
|
+
# db = ::SQLite3::Database.new(opts[:database].to_s, sqlite3_opts)
|
143
|
+
db = Wrapper.new(JS.global[opts[:database].to_sym])
|
144
|
+
|
145
|
+
# db.busy_timeout(typecast_value_integer(opts.fetch(:timeout, 5000)))
|
146
|
+
|
147
|
+
# db.extended_result_codes = true
|
148
|
+
|
149
|
+
connection_pragmas.each { |s| log_connection_yield(s, db) { db.exec(s) } }
|
150
|
+
|
151
|
+
if typecast_value_boolean(opts[:setup_regexp_function])
|
152
|
+
db.create_function('regexp', 2) do |func, regexp_str, string|
|
153
|
+
func.result = Regexp.new(regexp_str).match(string) ? 1 : 0
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
class << db
|
158
|
+
attr_reader :prepared_statements
|
159
|
+
end
|
160
|
+
db.instance_variable_set(:@prepared_statements, {})
|
161
|
+
|
162
|
+
db
|
163
|
+
end
|
164
|
+
|
165
|
+
# Whether this Database instance is setup to allow regexp matching.
|
166
|
+
# True if the :setup_regexp_function option was passed when creating the Database.
|
167
|
+
def allow_regexp?
|
168
|
+
@allow_regexp
|
169
|
+
end
|
170
|
+
|
171
|
+
# Disconnect given connections from the database.
|
172
|
+
def disconnect_connection(c)
|
173
|
+
c.prepared_statements.each_value { |v| v.first.close }
|
174
|
+
c.close
|
175
|
+
end
|
176
|
+
|
177
|
+
# Run the given SQL with the given arguments and yield each row.
|
178
|
+
def execute(sql, opts = OPTS, &)
|
179
|
+
_execute(:select, sql, opts, &)
|
180
|
+
end
|
181
|
+
|
182
|
+
# Run the given SQL with the given arguments and return the number of changed rows.
|
183
|
+
def execute_dui(sql, opts = OPTS)
|
184
|
+
_execute(:update, sql, opts)
|
185
|
+
end
|
186
|
+
|
187
|
+
# Drop any prepared statements on the connection when executing DDL. This is because
|
188
|
+
# prepared statements lock the table in such a way that you can't drop or alter the
|
189
|
+
# table while a prepared statement that references it still exists.
|
190
|
+
def execute_ddl(sql, opts = OPTS)
|
191
|
+
synchronize(opts[:server]) do |conn|
|
192
|
+
conn.prepared_statements.each_value { |cps, _s| cps.close }
|
193
|
+
conn.prepared_statements.clear
|
194
|
+
super
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
def execute_insert(sql, opts = OPTS)
|
199
|
+
_execute(:insert, sql, opts)
|
200
|
+
end
|
201
|
+
|
202
|
+
def freeze
|
203
|
+
@conversion_procs.freeze
|
204
|
+
super
|
205
|
+
end
|
206
|
+
|
207
|
+
# Handle Integer and Float arguments, since SQLite can store timestamps as integers and floats.
|
208
|
+
def to_application_timestamp(s)
|
209
|
+
case s
|
210
|
+
when String
|
211
|
+
super
|
212
|
+
when Integer
|
213
|
+
super(Time.zone.at(s).to_s)
|
214
|
+
when Float
|
215
|
+
super(DateTime.jd(s).to_s)
|
216
|
+
else
|
217
|
+
raise Sequel::Error, "unhandled type when converting to : #{s.inspect} (#{s.class.inspect})"
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
private
|
222
|
+
|
223
|
+
def adapter_initialize
|
224
|
+
@conversion_procs = SQLITE_TYPES.dup
|
225
|
+
@conversion_procs['datetime'] = @conversion_procs['timestamp'] = method(:to_application_timestamp)
|
226
|
+
set_integer_booleans
|
227
|
+
end
|
228
|
+
|
229
|
+
# Yield an available connection. Rescue
|
230
|
+
# any SQLite3::Exceptions and turn them into DatabaseErrors.
|
231
|
+
def _execute(type, sql, opts, &)
|
232
|
+
synchronize(opts[:server]) do |conn|
|
233
|
+
return execute_prepared_statement(conn, type, sql, opts, &) if sql.is_a?(Symbol)
|
234
|
+
|
235
|
+
log_args = opts[:arguments]
|
236
|
+
args = {}
|
237
|
+
opts.fetch(:arguments, OPTS).each { |k, v| args[k] = prepared_statement_argument(v) }
|
238
|
+
case type
|
239
|
+
when :select
|
240
|
+
log_connection_yield(sql, conn, log_args) do
|
241
|
+
result = conn.exec(sql)
|
242
|
+
# fetch table names
|
243
|
+
tables = conn.exec("select name from sqlite_master where type='table'")[:result].first['values']
|
244
|
+
target_table = tables.map(&:first).find { |n| sql.include?("FROM \`#{n}\`") }
|
245
|
+
if target_table
|
246
|
+
types_result = conn.exec("pragma table_info('#{target_table}')")
|
247
|
+
result[:types] = types_result[:result].first['values'].to_h { |e| [e[1], e[2]] }
|
248
|
+
end
|
249
|
+
|
250
|
+
yield result
|
251
|
+
end
|
252
|
+
when :insert
|
253
|
+
log_connection_yield(sql, conn, log_args) { conn.exec(sql) }
|
254
|
+
conn.exec('select last_insert_rowid()')[:result].first['values'].first.first
|
255
|
+
when :update
|
256
|
+
log_connection_yield(sql, conn, log_args) { conn.exec(sql) }
|
257
|
+
conn.get_rows_modified
|
258
|
+
end
|
259
|
+
end
|
260
|
+
rescue StandardError => e
|
261
|
+
raise_error(e)
|
262
|
+
end
|
263
|
+
|
264
|
+
# The SQLite adapter does not need the pool to convert exceptions.
|
265
|
+
# Also, force the max connections to 1 if a memory database is being
|
266
|
+
# used, as otherwise each connection gets a separate database.
|
267
|
+
def connection_pool_default_options
|
268
|
+
o = super.dup
|
269
|
+
# Default to only a single connection if a memory database is used,
|
270
|
+
# because otherwise each connection will get a separate database
|
271
|
+
o[:max_connections] = 1 if @opts[:database] == ':memory:' || blank_object?(@opts[:database])
|
272
|
+
o
|
273
|
+
end
|
274
|
+
|
275
|
+
def prepared_statement_argument(arg)
|
276
|
+
case arg
|
277
|
+
when Date, DateTime, Time
|
278
|
+
literal(arg)[1...-1]
|
279
|
+
when SQL::Blob
|
280
|
+
arg.to_blob
|
281
|
+
when true, false
|
282
|
+
if integer_booleans
|
283
|
+
arg ? 1 : 0
|
284
|
+
else
|
285
|
+
literal(arg)[1...-1]
|
286
|
+
end
|
287
|
+
else
|
288
|
+
arg
|
289
|
+
end
|
290
|
+
end
|
291
|
+
|
292
|
+
# Execute a prepared statement on the database using the given name.
|
293
|
+
def execute_prepared_statement(conn, type, name, opts, &block)
|
294
|
+
ps = prepared_statement(name)
|
295
|
+
sql = ps.prepared_sql
|
296
|
+
args = opts[:arguments]
|
297
|
+
ps_args = {}
|
298
|
+
args.each { |k, v| ps_args[k] = prepared_statement_argument(v) }
|
299
|
+
if cpsa = conn.prepared_statements[name]
|
300
|
+
cps, cps_sql = cpsa
|
301
|
+
if cps_sql != sql
|
302
|
+
cps.close
|
303
|
+
cps = nil
|
304
|
+
end
|
305
|
+
end
|
306
|
+
unless cps
|
307
|
+
cps = log_connection_yield("PREPARE #{name}: #{sql}", conn) { conn.prepare(sql) }
|
308
|
+
conn.prepared_statements[name] = [cps, sql]
|
309
|
+
end
|
310
|
+
log_sql = String.new
|
311
|
+
log_sql << "EXECUTE #{name}"
|
312
|
+
if ps.log_sql
|
313
|
+
log_sql << ' ('
|
314
|
+
log_sql << sql
|
315
|
+
log_sql << ')'
|
316
|
+
end
|
317
|
+
if block
|
318
|
+
log_connection_yield(log_sql, conn, args) { cps.execute(ps_args, &block) }
|
319
|
+
else
|
320
|
+
log_connection_yield(log_sql, conn, args) { cps.execute!(ps_args) { |r| } }
|
321
|
+
case type
|
322
|
+
when :insert
|
323
|
+
conn.last_insert_row_id
|
324
|
+
when :update
|
325
|
+
conn.changes
|
326
|
+
end
|
327
|
+
end
|
328
|
+
end
|
329
|
+
|
330
|
+
# SQLite3 raises ArgumentError in addition to SQLite3::Exception in
|
331
|
+
# some cases, such as operations on a closed database.
|
332
|
+
def database_error_classes
|
333
|
+
[::Sequel::Sqljs::Wrapper::Error, ArgumentError]
|
334
|
+
end
|
335
|
+
|
336
|
+
def dataset_class_default
|
337
|
+
Dataset
|
338
|
+
end
|
339
|
+
|
340
|
+
# Support SQLite exception codes if ruby-sqlite3 supports them.
|
341
|
+
def sqlite_error_code(exception)
|
342
|
+
exception.code if exception.respond_to?(:code)
|
343
|
+
end
|
344
|
+
|
345
|
+
def connection_execute_method
|
346
|
+
:exec
|
347
|
+
end
|
348
|
+
end
|
349
|
+
|
350
|
+
class Dataset < Sequel::Dataset
|
351
|
+
include ::Sequel::Sqljs::DatasetMethods
|
352
|
+
|
353
|
+
module ArgumentMapper
|
354
|
+
include Sequel::Dataset::ArgumentMapper
|
355
|
+
|
356
|
+
protected
|
357
|
+
|
358
|
+
# Return a hash with the same values as the given hash,
|
359
|
+
# but with the keys converted to strings.
|
360
|
+
def map_to_prepared_args(hash)
|
361
|
+
args = {}
|
362
|
+
hash.each { |k, v| args[k.to_s.gsub('.', '__')] = v }
|
363
|
+
args
|
364
|
+
end
|
365
|
+
|
366
|
+
private
|
367
|
+
|
368
|
+
# SQLite uses a : before the name of the argument for named
|
369
|
+
# arguments.
|
370
|
+
def prepared_arg(k)
|
371
|
+
LiteralString.new("#{prepared_arg_placeholder}#{k.to_s.gsub('.', '__')}")
|
372
|
+
end
|
373
|
+
end
|
374
|
+
|
375
|
+
BindArgumentMethods = prepared_statements_module(:bind, ArgumentMapper)
|
376
|
+
PreparedStatementMethods = prepared_statements_module(:prepare, BindArgumentMethods)
|
377
|
+
|
378
|
+
# Support regexp functions if using :setup_regexp_function Database option.
|
379
|
+
def complex_expression_sql_append(sql, op, args)
|
380
|
+
case op
|
381
|
+
when :~, :!~, :'~*', :'!~*'
|
382
|
+
return super unless supports_regexp?
|
383
|
+
|
384
|
+
case_insensitive = %i[~* !~*].include?(op)
|
385
|
+
sql << 'NOT ' if %i[!~ !~*].include?(op)
|
386
|
+
sql << '('
|
387
|
+
sql << 'LOWER(' if case_insensitive
|
388
|
+
literal_append(sql, args[0])
|
389
|
+
sql << ')' if case_insensitive
|
390
|
+
sql << ' REGEXP '
|
391
|
+
sql << 'LOWER(' if case_insensitive
|
392
|
+
literal_append(sql, args[1])
|
393
|
+
sql << ')' if case_insensitive
|
394
|
+
sql << ')'
|
395
|
+
else
|
396
|
+
super
|
397
|
+
end
|
398
|
+
end
|
399
|
+
|
400
|
+
def fetch_rows(sql, &)
|
401
|
+
execute(sql) do |result|
|
402
|
+
columns = result[:columns].map(&:to_sym)
|
403
|
+
type_procs =
|
404
|
+
if types = result[:types]
|
405
|
+
cps = db.conversion_procs
|
406
|
+
columns.map { |n| types[n.to_s] }.map { |t| cps[base_type_name(t)] }
|
407
|
+
end
|
408
|
+
|
409
|
+
result[:result].each do |r|
|
410
|
+
r['values'].map do |values|
|
411
|
+
values.map.with_index do |v, i|
|
412
|
+
value =
|
413
|
+
if type_procs && type_proc = type_procs[i]
|
414
|
+
type_proc.call(v)
|
415
|
+
else
|
416
|
+
v
|
417
|
+
end
|
418
|
+
[columns[i].to_sym, value]
|
419
|
+
end.to_h
|
420
|
+
end.each(&)
|
421
|
+
end
|
422
|
+
self.columns = columns
|
423
|
+
end
|
424
|
+
end
|
425
|
+
|
426
|
+
# Support regexp if using :setup_regexp_function Database option.
|
427
|
+
def supports_regexp?
|
428
|
+
db.allow_regexp?
|
429
|
+
end
|
430
|
+
|
431
|
+
private
|
432
|
+
|
433
|
+
# The base type name for a given type, without any parenthetical part.
|
434
|
+
def base_type_name(t)
|
435
|
+
(t =~ /^(.*?)\(/ ? Regexp.last_match(1) : t).downcase if t
|
436
|
+
end
|
437
|
+
|
438
|
+
# Quote the string using the adapter class method.
|
439
|
+
def literal_string_append(sql, v)
|
440
|
+
# sql << "'" << ::SQLite3::Database.quote(v) << "'"
|
441
|
+
sql << "'" << v.gsub(/'/, "''") << "'"
|
442
|
+
end
|
443
|
+
|
444
|
+
def bound_variable_modules
|
445
|
+
[BindArgumentMethods]
|
446
|
+
end
|
447
|
+
|
448
|
+
def prepared_statement_modules
|
449
|
+
[PreparedStatementMethods]
|
450
|
+
end
|
451
|
+
|
452
|
+
# SQLite uses a : before the name of the argument as a placeholder.
|
453
|
+
def prepared_arg_placeholder
|
454
|
+
':'
|
455
|
+
end
|
456
|
+
end
|
457
|
+
end
|
458
|
+
end
|
metadata
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: bormashino-sequel-sqljs-adapter
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Kenichiro Yasuda
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2022-10-16 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bormashino
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 0.1.9
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 0.1.9
|
27
|
+
description: " SQL.JS adapter for Sequel on browser with Bormaŝino / ruby.wasm\n"
|
28
|
+
email:
|
29
|
+
- keyasuda@users.noreply.github.com
|
30
|
+
executables: []
|
31
|
+
extensions: []
|
32
|
+
extra_rdoc_files: []
|
33
|
+
files:
|
34
|
+
- LICENSE.txt
|
35
|
+
- README.md
|
36
|
+
- lib/bormashino_sequel_sqljs_adapter.rb
|
37
|
+
- lib/bormashino_sequel_sqljs_adapter/shared/sqljs.rb
|
38
|
+
- lib/bormashino_sequel_sqljs_adapter/sqljs.rb
|
39
|
+
- lib/bormashino_sequel_sqljs_adapter/version.rb
|
40
|
+
homepage: https://github.com/keyasuda/bormashino-sequel-sqljs-adapter
|
41
|
+
licenses:
|
42
|
+
- MIT
|
43
|
+
metadata:
|
44
|
+
homepage_uri: https://github.com/keyasuda/bormashino-sequel-sqljs-adapter
|
45
|
+
source_code_uri: https://github.com/keyasuda/bormashino-sequel-sqljs-adapter
|
46
|
+
changelog_uri: https://github.com/keyasuda/bormashino-sequel-sqljs-adapter/blob/main/CHANGELOG.md
|
47
|
+
rubygems_mfa_required: 'true'
|
48
|
+
post_install_message:
|
49
|
+
rdoc_options: []
|
50
|
+
require_paths:
|
51
|
+
- lib
|
52
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
53
|
+
requirements:
|
54
|
+
- - ">="
|
55
|
+
- !ruby/object:Gem::Version
|
56
|
+
version: 3.2.0.pre.preview1
|
57
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
requirements: []
|
63
|
+
rubygems_version: 3.4.0.dev
|
64
|
+
signing_key:
|
65
|
+
specification_version: 4
|
66
|
+
summary: SQL.JS adapter for Sequel
|
67
|
+
test_files: []
|