bormashino-sequel-sqljs-adapter 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BormashinoSequelSqljsAdapter
4
+ VERSION = '0.0.1'
5
+ end
@@ -0,0 +1,6 @@
1
+ require_relative 'bormashino_sequel_sqljs_adapter/version'
2
+ require_relative 'bormashino_sequel_sqljs_adapter/sqljs'
3
+
4
+ # top level module of BormashinoSequelSqljsAdapter
5
+ module BormashinoSequelSqljsAdapter
6
+ 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: []