extralite-bundle 2.14 → 3.0.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.
- checksums.yaml +4 -4
- data/.github/workflows/test-bundle.yml +2 -3
- data/.github/workflows/test.yml +1 -1
- data/CHANGELOG.md +15 -0
- data/README.md +111 -47
- data/TODO.md +2 -11
- data/examples/transform.rb +61 -0
- data/ext/extralite/changeset.c +11 -11
- data/ext/extralite/common.c +234 -22
- data/ext/extralite/database.c +157 -100
- data/ext/extralite/extralite.h +52 -6
- data/ext/extralite/extralite_ext.c +2 -0
- data/ext/extralite/query.c +67 -41
- data/ext/extralite/transform.c +420 -0
- data/ext/sqlite3/sqlite3.c +8299 -4638
- data/ext/sqlite3/sqlite3.h +486 -105
- data/gemspec.rb +3 -3
- data/lib/extralite/version.rb +1 -1
- data/lib/extralite.rb +102 -1
- data/test/perf_array.rb +1 -1
- data/test/perf_hash.rb +1 -1
- data/test/perf_hash_prepared.rb +2 -2
- data/test/perf_splat.rb +1 -1
- data/test/perf_transform.rb +58 -0
- data/test/test_database.rb +37 -10
- data/test/test_query.rb +11 -15
- data/test/test_transform.rb +817 -0
- metadata +13 -9
data/gemspec.rb
CHANGED
|
@@ -15,11 +15,11 @@ def common_spec(s)
|
|
|
15
15
|
s.rdoc_options = ['--title', 'Extralite', '--main', 'README.md']
|
|
16
16
|
s.extra_rdoc_files = ['README.md']
|
|
17
17
|
s.require_paths = ['lib']
|
|
18
|
-
s.required_ruby_version = '>= 3.
|
|
18
|
+
s.required_ruby_version = '>= 3.4'
|
|
19
19
|
|
|
20
20
|
s.add_development_dependency 'rake-compiler', '1.3.1'
|
|
21
|
-
s.add_development_dependency 'minitest'
|
|
21
|
+
s.add_development_dependency 'minitest'
|
|
22
22
|
s.add_development_dependency 'simplecov', '0.22.0'
|
|
23
23
|
s.add_development_dependency 'yard', '0.9.38'
|
|
24
|
-
s.add_development_dependency 'sequel', '5.
|
|
24
|
+
s.add_development_dependency 'sequel', '5.105.0'
|
|
25
25
|
end
|
data/lib/extralite/version.rb
CHANGED
data/lib/extralite.rb
CHANGED
|
@@ -236,7 +236,7 @@ module Extralite
|
|
|
236
236
|
# @return [Any] the given block's return value
|
|
237
237
|
def transaction(mode = :immediate)
|
|
238
238
|
abort = false
|
|
239
|
-
execute "begin #{mode} transaction"
|
|
239
|
+
execute "begin #{mode} transaction"
|
|
240
240
|
yield self
|
|
241
241
|
rescue => e
|
|
242
242
|
abort = true
|
|
@@ -296,6 +296,10 @@ module Extralite
|
|
|
296
296
|
raise Rollback
|
|
297
297
|
end
|
|
298
298
|
|
|
299
|
+
def quote(str)
|
|
300
|
+
str.gsub("'", "''")
|
|
301
|
+
end
|
|
302
|
+
|
|
299
303
|
private
|
|
300
304
|
|
|
301
305
|
def pragma_set(values)
|
|
@@ -311,4 +315,101 @@ module Extralite
|
|
|
311
315
|
class Query
|
|
312
316
|
alias_method :execute_multi, :batch_execute
|
|
313
317
|
end
|
|
318
|
+
|
|
319
|
+
class Transform
|
|
320
|
+
alias_method :orig_initialize, :initialize
|
|
321
|
+
|
|
322
|
+
# call-seq:
|
|
323
|
+
# Extralite::Transform.new(literal_spec) -> transform
|
|
324
|
+
# Extralite::Transform.new { dsl_spec } -> transform
|
|
325
|
+
#
|
|
326
|
+
# Initializes a new transform with with the given spec. The spec may be
|
|
327
|
+
# expressed as a hash containing the columns and any nested entities, or as a
|
|
328
|
+
# block with the transform DSL.
|
|
329
|
+
#
|
|
330
|
+
# # literal transform spec
|
|
331
|
+
# transform = Extralite::Transform.new(
|
|
332
|
+
# columns: {
|
|
333
|
+
# id: { type: :integer, identity: true },
|
|
334
|
+
# content: { type: :text },
|
|
335
|
+
# author: {
|
|
336
|
+
# type: :relation,
|
|
337
|
+
# columns: {
|
|
338
|
+
# id: { type: :integer, identity: true },
|
|
339
|
+
# name: { type: :text },
|
|
340
|
+
# }
|
|
341
|
+
# }
|
|
342
|
+
# }
|
|
343
|
+
# )
|
|
344
|
+
#
|
|
345
|
+
# # DSL spec
|
|
346
|
+
# transform = Extralite::Transform.new do
|
|
347
|
+
# {
|
|
348
|
+
# id: integer.identity,
|
|
349
|
+
# content: text,
|
|
350
|
+
# author: {
|
|
351
|
+
# id: integer.identity,
|
|
352
|
+
# name: text
|
|
353
|
+
# }
|
|
354
|
+
# }
|
|
355
|
+
# end
|
|
356
|
+
#
|
|
357
|
+
# @param spec [Hash, nil] literal spec
|
|
358
|
+
# @param block [Proc, nil] DSL spec
|
|
359
|
+
# @return [void]
|
|
360
|
+
def initialize(spec = nil, &block)
|
|
361
|
+
return orig_initialize(spec) if spec
|
|
362
|
+
raise "No spec given" if !block
|
|
363
|
+
|
|
364
|
+
orig_initialize(dsl_to_spec(block))
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
private
|
|
368
|
+
|
|
369
|
+
class DSLContext
|
|
370
|
+
attr_reader :hash
|
|
371
|
+
|
|
372
|
+
def initialize(hash = {})
|
|
373
|
+
@hash = hash
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
def auto = mutate(type: nil)
|
|
377
|
+
def integer = mutate(type: :integer)
|
|
378
|
+
def float = mutate(type: :float)
|
|
379
|
+
def text = mutate(type: :text)
|
|
380
|
+
def bool = mutate(type: :bool)
|
|
381
|
+
def json = mutate(type: :json)
|
|
382
|
+
def identity = mutate(identity: true)
|
|
383
|
+
|
|
384
|
+
private
|
|
385
|
+
def mutate(opts)
|
|
386
|
+
DSLContext.new(@hash.merge(opts))
|
|
387
|
+
end
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
def dsl_to_spec(block)
|
|
391
|
+
intermediate = DSLContext.new.instance_eval(&block)
|
|
392
|
+
dsl_intermediate_to_spec(intermediate)
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
def dsl_intermediate_to_spec(intermediate)
|
|
396
|
+
columns = intermediate.each_with_object({}) do |(k, v), h|
|
|
397
|
+
h[k] = dsl_translate_intermediate_value(v)
|
|
398
|
+
end
|
|
399
|
+
{ type: :relation, columns: }
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
def dsl_translate_intermediate_value(v)
|
|
403
|
+
case v
|
|
404
|
+
when DSLContext
|
|
405
|
+
v.hash
|
|
406
|
+
when Hash
|
|
407
|
+
dsl_intermediate_to_spec(v)
|
|
408
|
+
when Array
|
|
409
|
+
[dsl_intermediate_to_spec(v[0])]
|
|
410
|
+
else
|
|
411
|
+
raise Extralite::Error, 'Invalid transform spec'
|
|
412
|
+
end
|
|
413
|
+
end
|
|
414
|
+
end
|
|
314
415
|
end
|
data/test/perf_array.rb
CHANGED
data/test/perf_hash.rb
CHANGED
data/test/perf_hash_prepared.rb
CHANGED
|
@@ -5,7 +5,7 @@ require 'bundler/inline'
|
|
|
5
5
|
gemfile do
|
|
6
6
|
source 'https://rubygems.org'
|
|
7
7
|
gem 'extralite', path: '..'
|
|
8
|
-
gem 'sqlite3', '2.
|
|
8
|
+
gem 'sqlite3', '2.9.5'
|
|
9
9
|
gem 'benchmark-ips'
|
|
10
10
|
end
|
|
11
11
|
|
|
@@ -18,7 +18,7 @@ puts "DB_PATH = #{DB_PATH.inspect}"
|
|
|
18
18
|
def prepare_database(count)
|
|
19
19
|
$sqlite3_db = SQLite3::Database.new(DB_PATH, results_as_hash: true)
|
|
20
20
|
$extralite_db = Extralite::Database.new(DB_PATH, gvl_release_threshold: -1)
|
|
21
|
-
|
|
21
|
+
|
|
22
22
|
$extralite_db.query('create table if not exists foo ( a integer primary key, b text )')
|
|
23
23
|
$extralite_db.query('delete from foo')
|
|
24
24
|
$extralite_db.query('begin')
|
data/test/perf_splat.rb
CHANGED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Run on Ruby 3.3 with YJIT enabled
|
|
4
|
+
|
|
5
|
+
require 'bundler/inline'
|
|
6
|
+
|
|
7
|
+
gemfile do
|
|
8
|
+
source 'https://rubygems.org'
|
|
9
|
+
gem 'extralite', path: '..'
|
|
10
|
+
gem 'benchmark-ips'
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
require 'benchmark/ips'
|
|
14
|
+
require 'fileutils'
|
|
15
|
+
|
|
16
|
+
DB_PATH = "/tmp/extralite_sqlite3_perf-#{Time.now.to_i}-#{rand(10000)}.db"
|
|
17
|
+
puts "DB_PATH = #{DB_PATH.inspect}"
|
|
18
|
+
|
|
19
|
+
$extralite_db = Extralite::Database.new(DB_PATH, gvl_release_threshold: -1)
|
|
20
|
+
|
|
21
|
+
def prepare_database(count)
|
|
22
|
+
$extralite_db.query('create table if not exists foo ( a integer primary key, b text )')
|
|
23
|
+
$extralite_db.query('delete from foo')
|
|
24
|
+
$extralite_db.query('begin')
|
|
25
|
+
count.times { $extralite_db.query('insert into foo (b) values (?)', "hello#{rand(1000)}" )}
|
|
26
|
+
$extralite_db.query('commit')
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def run_normal(count)
|
|
30
|
+
results = $extralite_db.query('select * from foo')
|
|
31
|
+
raise unless results.size == count
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
@transform = Extralite::Transform.new {
|
|
35
|
+
{ a: integer, b: text }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
def run_transform(count)
|
|
39
|
+
results = $extralite_db.query(@transform, 'select * from foo')
|
|
40
|
+
raise unless results.size == count
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
[10, 1000, 100000].each do |c|
|
|
44
|
+
puts "Record count: #{c}"
|
|
45
|
+
prepare_database(c)
|
|
46
|
+
|
|
47
|
+
bm = Benchmark.ips do |x|
|
|
48
|
+
x.config(:time => 5, :warmup => 2)
|
|
49
|
+
|
|
50
|
+
x.report("normal") { run_normal(c) }
|
|
51
|
+
x.report("transform") { run_transform(c) }
|
|
52
|
+
|
|
53
|
+
x.compare!
|
|
54
|
+
end
|
|
55
|
+
puts;
|
|
56
|
+
bm.entries.each { |e| puts "#{e.label}: #{(e.ips * c).round.to_i} rows/s" }
|
|
57
|
+
puts;
|
|
58
|
+
end
|
data/test/test_database.rb
CHANGED
|
@@ -302,6 +302,15 @@ class DatabaseTest < Minitest::Test
|
|
|
302
302
|
|
|
303
303
|
r = @db.query_single_splat("select reverse('abcd')")
|
|
304
304
|
assert_equal 'dcba', r
|
|
305
|
+
|
|
306
|
+
e = nil
|
|
307
|
+
begin
|
|
308
|
+
@db.execute("select load_extension('/extensions/foo')")
|
|
309
|
+
rescue => e
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
assert_kind_of Extralite::SQLError, e
|
|
313
|
+
assert_equal 'not authorized', e.message
|
|
305
314
|
end
|
|
306
315
|
|
|
307
316
|
def test_tables
|
|
@@ -341,7 +350,7 @@ class DatabaseTest < Minitest::Test
|
|
|
341
350
|
|
|
342
351
|
def test_pragma
|
|
343
352
|
assert_equal 'memory', @db.pragma('journal_mode')
|
|
344
|
-
assert_equal
|
|
353
|
+
assert_equal 1, @db.pragma('synchronous')
|
|
345
354
|
|
|
346
355
|
assert_equal 1, @db.pragma(:schema_version)
|
|
347
356
|
assert_equal 0, @db.pragma(:recursive_triggers)
|
|
@@ -866,14 +875,17 @@ class DatabaseTest < Minitest::Test
|
|
|
866
875
|
db = Extralite::Database.new(':memory:', gvl_release_threshold: 23)
|
|
867
876
|
assert_equal 23, db.gvl_release_threshold
|
|
868
877
|
|
|
869
|
-
fn = Tempfile.new('
|
|
870
|
-
db = Extralite::Database.new(fn
|
|
878
|
+
fn = Tempfile.new('extralite_test_database_initialize_modern').path
|
|
879
|
+
db = Extralite::Database.new(fn)
|
|
871
880
|
assert_equal 'wal', db.pragma(:journal_mode)
|
|
872
881
|
assert_equal 1, db.pragma(:synchronous)
|
|
882
|
+
assert_equal 1, db.pragma(:foreign_keys)
|
|
873
883
|
|
|
874
|
-
fn = Tempfile.new('
|
|
875
|
-
db = Extralite::Database.new(fn,
|
|
876
|
-
assert_equal
|
|
884
|
+
fn = Tempfile.new('extralite_test_database_initialize_legacy').path
|
|
885
|
+
db = Extralite::Database.new(fn, legacy: true)
|
|
886
|
+
assert_equal 'delete', db.pragma(:journal_mode)
|
|
887
|
+
assert_equal 2, db.pragma(:synchronous)
|
|
888
|
+
assert_equal 0, db.pragma(:foreign_keys)
|
|
877
889
|
end
|
|
878
890
|
|
|
879
891
|
def test_database_inspect
|
|
@@ -1052,7 +1064,7 @@ class DatabaseTest < Minitest::Test
|
|
|
1052
1064
|
def test_wal_checkpoint
|
|
1053
1065
|
fn = Tempfile.new('extralite_test_wal_checkpoint').path
|
|
1054
1066
|
|
|
1055
|
-
db = Extralite::Database.new(fn
|
|
1067
|
+
db = Extralite::Database.new(fn)
|
|
1056
1068
|
|
|
1057
1069
|
db.execute 'create table t (x, y, z)'
|
|
1058
1070
|
rows = (1..1000).map { [rand, rand, rand] }
|
|
@@ -1121,12 +1133,27 @@ class DatabaseTest < Minitest::Test
|
|
|
1121
1133
|
assert_nil result
|
|
1122
1134
|
assert_equal [7, 8], @db.query_splat('SELECT x FROM t ORDER BY x')
|
|
1123
1135
|
end
|
|
1136
|
+
|
|
1137
|
+
def test_quote
|
|
1138
|
+
assert_equal '"abc', @db.quote('"abc')
|
|
1139
|
+
assert_equal "''abc''def", @db.quote("'abc'def")
|
|
1140
|
+
|
|
1141
|
+
assert_equal "'abc'def", @db.query_single_splat("select '#{@db.quote("'abc'def")}'")
|
|
1142
|
+
end
|
|
1143
|
+
|
|
1144
|
+
def test_cache_flush
|
|
1145
|
+
assert_equal @db, @db.cache_flush
|
|
1146
|
+
end
|
|
1147
|
+
|
|
1148
|
+
def test_release_memory
|
|
1149
|
+
assert_equal @db, @db.release_memory
|
|
1150
|
+
end
|
|
1124
1151
|
end
|
|
1125
1152
|
|
|
1126
1153
|
class ScenarioTest < Minitest::Test
|
|
1127
1154
|
def setup
|
|
1128
1155
|
@fn = Tempfile.new('extralite_scenario_test').path
|
|
1129
|
-
@db = Extralite::Database.new(@fn)
|
|
1156
|
+
@db = Extralite::Database.new(@fn, legacy: true)
|
|
1130
1157
|
@db.query('create table if not exists t (x,y,z)')
|
|
1131
1158
|
@db.query('delete from t')
|
|
1132
1159
|
@db.query('insert into t values (1, 2, 3)')
|
|
@@ -1136,7 +1163,7 @@ class ScenarioTest < Minitest::Test
|
|
|
1136
1163
|
def test_concurrent_transactions
|
|
1137
1164
|
done = false
|
|
1138
1165
|
t = Thread.new do
|
|
1139
|
-
db = Extralite::Database.new(@fn)
|
|
1166
|
+
db = Extralite::Database.new(@fn, legacy: true)
|
|
1140
1167
|
db.query 'begin immediate'
|
|
1141
1168
|
sleep 0.01 until done
|
|
1142
1169
|
|
|
@@ -1806,7 +1833,7 @@ class RactorTest < Minitest::Test
|
|
|
1806
1833
|
assert_equal [10] * 10, buf
|
|
1807
1834
|
|
|
1808
1835
|
final_check = Ractor.new(fn) do |rfn|
|
|
1809
|
-
rdb = Extralite::Database.new(rfn
|
|
1836
|
+
rdb = Extralite::Database.new(rfn)
|
|
1810
1837
|
count = rdb.query_single_splat('select count(*) from stress_test')
|
|
1811
1838
|
Ractor.yield count
|
|
1812
1839
|
end
|
data/test/test_query.rb
CHANGED
|
@@ -27,7 +27,7 @@ class QueryTest < Minitest::Test
|
|
|
27
27
|
def test_mode
|
|
28
28
|
query = @db.prepare('select 1')
|
|
29
29
|
assert_equal :hash, query.mode
|
|
30
|
-
|
|
30
|
+
|
|
31
31
|
query.mode = :splat
|
|
32
32
|
assert_equal :splat, query.mode
|
|
33
33
|
|
|
@@ -554,7 +554,7 @@ class QueryTest < Minitest::Test
|
|
|
554
554
|
def test_query_execute_with_mixed_params
|
|
555
555
|
@db.execute 'delete from t'
|
|
556
556
|
q = @db.prepare('insert into t values (?, ?, ?)')
|
|
557
|
-
|
|
557
|
+
|
|
558
558
|
q.execute(1, [2], 3)
|
|
559
559
|
q.execute([4, 5], 6)
|
|
560
560
|
q.execute([7], 8, [9])
|
|
@@ -1014,7 +1014,7 @@ class QueryTransformTest < Minitest::Test
|
|
|
1014
1014
|
|
|
1015
1015
|
@q1 = @db.prepare_splat('select c from t where a = ?')
|
|
1016
1016
|
@q2 = @db.prepare_splat('select c from t order by a')
|
|
1017
|
-
|
|
1017
|
+
|
|
1018
1018
|
@q3 = @db.prepare('select * from t where a = ?')
|
|
1019
1019
|
@q4 = @db.prepare('select * from t order by a')
|
|
1020
1020
|
|
|
@@ -1038,8 +1038,7 @@ class QueryTransformTest < Minitest::Test
|
|
|
1038
1038
|
end
|
|
1039
1039
|
|
|
1040
1040
|
def test_transform_hash
|
|
1041
|
-
|
|
1042
|
-
assert_equal @q5, q
|
|
1041
|
+
@q5.transform = ->(h) { MyModel.new(h) }
|
|
1043
1042
|
|
|
1044
1043
|
o = @q5.bind(1).next
|
|
1045
1044
|
assert_kind_of MyModel, o
|
|
@@ -1054,7 +1053,7 @@ class QueryTransformTest < Minitest::Test
|
|
|
1054
1053
|
[{ a: 4, b: 5 }]
|
|
1055
1054
|
], @q5.batch_query([[1], [4]]).map { |a| a.map(&:values) }
|
|
1056
1055
|
|
|
1057
|
-
@q6.transform
|
|
1056
|
+
@q6.transform = ->(h) { MyModel.new(h) }
|
|
1058
1057
|
assert_equal [
|
|
1059
1058
|
{ a: 1, b: 2 },
|
|
1060
1059
|
{ a: 4, b: 5 }
|
|
@@ -1070,8 +1069,7 @@ class QueryTransformTest < Minitest::Test
|
|
|
1070
1069
|
|
|
1071
1070
|
def test_transform_array
|
|
1072
1071
|
@q5.mode = :array
|
|
1073
|
-
|
|
1074
|
-
assert_equal @q5, q
|
|
1072
|
+
@q5.transform = ->(h) { MyModel.new(h) }
|
|
1075
1073
|
|
|
1076
1074
|
o = @q5.bind(1).next
|
|
1077
1075
|
assert_kind_of MyModel, o
|
|
@@ -1087,7 +1085,7 @@ class QueryTransformTest < Minitest::Test
|
|
|
1087
1085
|
], @q5.batch_query([[1], [4]]).map { |a| a.map(&:values) }
|
|
1088
1086
|
|
|
1089
1087
|
@q6.mode = :array
|
|
1090
|
-
@q6.transform
|
|
1088
|
+
@q6.transform = ->(h) { MyModel.new(h) }
|
|
1091
1089
|
assert_equal [
|
|
1092
1090
|
[1, 2],
|
|
1093
1091
|
[4, 5]
|
|
@@ -1102,8 +1100,7 @@ class QueryTransformTest < Minitest::Test
|
|
|
1102
1100
|
end
|
|
1103
1101
|
|
|
1104
1102
|
def test_transform_splat_single_column
|
|
1105
|
-
|
|
1106
|
-
assert_equal @q1, q
|
|
1103
|
+
@q1.transform = ->(c) { JSON.parse(c, symbolize_names: true) }
|
|
1107
1104
|
|
|
1108
1105
|
assert_equal({ foo: 42, bar: 43 }, @q1.bind(1).next)
|
|
1109
1106
|
assert_equal({ foo: 45, bar: 46 }, @q1.bind(4).next)
|
|
@@ -1113,7 +1110,7 @@ class QueryTransformTest < Minitest::Test
|
|
|
1113
1110
|
[{ foo: 45, bar: 46 }]
|
|
1114
1111
|
], @q1.batch_query([[1], [4]])
|
|
1115
1112
|
|
|
1116
|
-
@q2.transform
|
|
1113
|
+
@q2.transform = ->(c) { JSON.parse(c, symbolize_names: true) }
|
|
1117
1114
|
assert_equal [
|
|
1118
1115
|
{ foo: 42, bar: 43 },
|
|
1119
1116
|
{ foo: 45, bar: 46 }
|
|
@@ -1129,8 +1126,7 @@ class QueryTransformTest < Minitest::Test
|
|
|
1129
1126
|
|
|
1130
1127
|
def test_transform_splat_multi_column
|
|
1131
1128
|
@q3.mode = :splat
|
|
1132
|
-
|
|
1133
|
-
assert_equal @q3, q
|
|
1129
|
+
@q3.transform = ->(a, b, c) { { a: a, b: b, c: JSON.parse(c, symbolize_names: true) } }
|
|
1134
1130
|
|
|
1135
1131
|
assert_equal({ a: 1, b: 2, c: { foo: 42, bar: 43 }}, @q3.bind(1).next)
|
|
1136
1132
|
assert_equal({ a: 4, b: 5, c: { foo: 45, bar: 46 }}, @q3.bind(4).next)
|
|
@@ -1141,7 +1137,7 @@ class QueryTransformTest < Minitest::Test
|
|
|
1141
1137
|
], @q3.batch_query([[1], [4]])
|
|
1142
1138
|
|
|
1143
1139
|
@q4.mode = :splat
|
|
1144
|
-
@q4.transform
|
|
1140
|
+
@q4.transform = ->(a, b, c) { { a: a, b: b, c: JSON.parse(c, symbolize_names: true) } }
|
|
1145
1141
|
assert_equal [
|
|
1146
1142
|
{ a: 1, b: 2, c: { foo: 42, bar: 43 }},
|
|
1147
1143
|
{ a: 4, b: 5, c: { foo: 45, bar: 46 }}
|