bormashino-sequel-sqljs-adapter 0.0.1

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,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: []