activerecord-nuodb-adapter 1.0.0.rc.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,778 @@
1
+ #
2
+ # Copyright (c) 2012, NuoDB, Inc.
3
+ # All rights reserved.
4
+ #
5
+ # Redistribution and use in source and binary forms, with or without
6
+ # modification, are permitted provided that the following conditions are met:
7
+ #
8
+ # * Redistributions of source code must retain the above copyright
9
+ # notice, this list of conditions and the following disclaimer.
10
+ # * Redistributions in binary form must reproduce the above copyright
11
+ # notice, this list of conditions and the following disclaimer in the
12
+ # documentation and/or other materials provided with the distribution.
13
+ # * Neither the name of NuoDB, Inc. nor the names of its contributors may
14
+ # be used to endorse or promote products derived from this software
15
+ # without specific prior written permission.
16
+ #
17
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
18
+ # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
19
+ # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
20
+ # DISCLAIMED. IN NO EVENT SHALL NUODB, INC. BE LIABLE FOR ANY DIRECT, INDIRECT,
21
+ # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
+ # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
23
+ # OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
24
+ # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
25
+ # OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
26
+ # ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
+ #
28
+
29
+ require 'arel/visitors/nuodb'
30
+ require 'active_record'
31
+ require 'active_record/base'
32
+ require 'active_record/connection_adapters/abstract_adapter'
33
+ require 'active_record/connection_adapters/statement_pool'
34
+ require 'active_record/connection_adapters/nuodb/version'
35
+ require 'arel/visitors/bind_visitor'
36
+ require 'active_support/core_ext/hash/keys'
37
+
38
+ require 'nuodb'
39
+
40
+ module ActiveRecord
41
+
42
+ class Base
43
+
44
+ def self.nuodb_connection(config) #:nodoc:
45
+ config.symbolize_keys!
46
+ unless config[:database]
47
+ raise ArgumentError, "No database file specified. Missing argument: database"
48
+ end
49
+ # supply configuration defaults
50
+ config.reverse_merge! :host => 'localhost'
51
+ ConnectionAdapters::NuoDBAdapter.new nil, logger, nil, config
52
+ end
53
+
54
+ end
55
+
56
+ class LostConnection < WrappedDatabaseException
57
+ end
58
+
59
+ module ConnectionAdapters
60
+
61
+ class NuoDBColumn < Column
62
+
63
+ def initialize(name, default, sql_type = nil, null = true, options = {})
64
+ @options = options.symbolize_keys
65
+ super(name, default, sql_type, null)
66
+ @primary = @options[:is_identity] || @options[:is_primary]
67
+ end
68
+
69
+ class << self
70
+
71
+ def string_to_binary(value)
72
+ "0x#{value.unpack("H*")[0]}"
73
+ end
74
+
75
+ def binary_to_string(value)
76
+ value =~ /[^[:xdigit:]]/ ? value : [value].pack('H*')
77
+ end
78
+
79
+ end
80
+
81
+ def is_identity?
82
+ @options[:is_identity]
83
+ end
84
+
85
+ def is_primary?
86
+ @options[:is_primary]
87
+ end
88
+
89
+ def is_utf8?
90
+ !!(@sql_type =~ /nvarchar|ntext|nchar/i)
91
+ end
92
+
93
+ def is_integer?
94
+ !!(@sql_type =~ /int/i)
95
+ end
96
+
97
+ def is_real?
98
+ !!(@sql_type =~ /real/i)
99
+ end
100
+
101
+ def sql_type_for_statement
102
+ if is_integer? || is_real?
103
+ sql_type.sub(/\((\d+)?\)/, '')
104
+ else
105
+ sql_type
106
+ end
107
+ end
108
+
109
+ def default_function
110
+ @options[:default_function]
111
+ end
112
+
113
+ def table_name
114
+ @options[:table_name]
115
+ end
116
+
117
+ def table_klass
118
+ @table_klass ||= begin
119
+ table_name.classify.constantize
120
+ rescue StandardError, NameError, LoadError
121
+ nil
122
+ end
123
+ (@table_klass && @table_klass < ActiveRecord::Base) ? @table_klass : nil
124
+ end
125
+
126
+ private
127
+
128
+ def extract_limit(sql_type)
129
+ case sql_type
130
+ when /^smallint/i
131
+ 2
132
+ when /^int/i
133
+ 4
134
+ when /^bigint/i
135
+ 8
136
+ when /\(max\)/, /decimal/, /numeric/
137
+ nil
138
+ else
139
+ super
140
+ end
141
+ end
142
+
143
+ def simplified_type(field_type)
144
+ case field_type
145
+ when /real/i then
146
+ :float
147
+ when /money/i then
148
+ :decimal
149
+ when /image/i then
150
+ :binary
151
+ when /bit/i then
152
+ :boolean
153
+ when /uniqueidentifier/i then
154
+ :string
155
+ when /datetime/i then
156
+ :datetime
157
+ when /varchar\(max\)/ then
158
+ :text
159
+ when /timestamp/ then
160
+ :binary
161
+ else
162
+ super
163
+ end
164
+ end
165
+
166
+ end #class NuoDBColumn
167
+
168
+ class NuoDBAdapter < AbstractAdapter
169
+
170
+ class StatementPool < ConnectionAdapters::StatementPool
171
+
172
+ attr_reader :max, :connection
173
+
174
+ def initialize(connection, max)
175
+ super
176
+ @cache = Hash.new { |h, pid| h[pid] = {} }
177
+ end
178
+
179
+ def each(&block)
180
+ cache.each(&block)
181
+ end
182
+
183
+ def key?(key)
184
+ cache.key?(key)
185
+ end
186
+
187
+ def [](key)
188
+ cache[key]
189
+ end
190
+
191
+ def []=(sql, key)
192
+ while max <= cache.size
193
+ dealloc cache.shift.last[:stmt]
194
+ end
195
+ cache[sql] = key
196
+ end
197
+
198
+ def length
199
+ cache.length
200
+ end
201
+
202
+ def delete(key)
203
+ dealloc cache[key][:stmt]
204
+ cache.delete(key)
205
+ end
206
+
207
+ def clear
208
+ cache.each_value do |hash|
209
+ dealloc hash[:stmt]
210
+ end
211
+ cache.clear
212
+ end
213
+
214
+ private
215
+
216
+ def cache
217
+ @cache[Process.pid]
218
+ end
219
+
220
+ def dealloc(stmt)
221
+ stmt.close if connection.ping
222
+ end
223
+ end
224
+
225
+ def process_id
226
+ Process.pid
227
+ end
228
+
229
+ attr_accessor :config, :statements
230
+
231
+ class BindSubstitution < Arel::Visitors::NuoDB
232
+ include Arel::Visitors::BindVisitor
233
+ end
234
+
235
+ def initialize(connection, logger, pool, config)
236
+ super(connection, logger, pool)
237
+ @visitor = Arel::Visitors::NuoDB.new self
238
+ @config = config.clone
239
+ # prefer to run with prepared statements unless otherwise specified
240
+ if @config.fetch(:prepared_statements) { true }
241
+ @visitor = Arel::Visitors::NuoDB.new self
242
+ else
243
+ @visitor = BindSubstitution.new self
244
+ end
245
+ connect!
246
+ end
247
+
248
+ # ABSTRACT ADAPTER #######################################
249
+
250
+ # ADAPTER NAME ===========================================
251
+
252
+ def adapter_name
253
+ 'NuoDB'
254
+ end
255
+
256
+ # FEATURES ===============================================
257
+
258
+ def supports_migrations?
259
+ true
260
+ end
261
+
262
+ def supports_primary_key?
263
+ true
264
+ end
265
+
266
+ def supports_count_distinct?
267
+ true
268
+ end
269
+
270
+ def supports_ddl_transactions?
271
+ true
272
+ end
273
+
274
+ def supports_bulk_alter?
275
+ false
276
+ end
277
+
278
+ def supports_savepoints?
279
+ true
280
+ end
281
+
282
+ def supports_index_sort_order?
283
+ true
284
+ end
285
+
286
+ def supports_partial_index?
287
+ false
288
+ end
289
+
290
+ def supports_explain?
291
+ false
292
+ end
293
+
294
+ # CONNECTION MANAGEMENT ==================================
295
+
296
+ def reconnect!
297
+ disconnect!
298
+ connect!
299
+ super
300
+ end
301
+
302
+ def connect!
303
+ # todo add native method for new and initialize where init takes a hash
304
+ @connection = ::NuoDB::Connection.createSqlConnection(
305
+ "#{config[:database]}@#{config[:host]}", config[:schema],
306
+ config[:username], config[:password])
307
+ @statements = StatementPool.new(@connection, @config.fetch(:statement_limit) { 1000 })
308
+ @quoted_column_names, @quoted_table_names = {}, {}
309
+ end
310
+
311
+ def disconnect!
312
+ super
313
+ clear_cache!
314
+ raw_connection.disconnect rescue nil
315
+ end
316
+
317
+ def reset!
318
+ reconnect!
319
+ end
320
+
321
+ def clear_cache!
322
+ @statements.clear
323
+ @statements = nil
324
+ end
325
+
326
+ # SAVEPOINT SUPPORT ======================================
327
+
328
+ def create_savepoint
329
+ execute("SAVEPOINT #{current_savepoint_name}")
330
+ end
331
+
332
+ def rollback_to_savepoint
333
+ execute("ROLLBACK TO SAVEPOINT #{current_savepoint_name}")
334
+ end
335
+
336
+ def release_savepoint
337
+ execute("RELEASE SAVEPOINT #{current_savepoint_name}")
338
+ end
339
+
340
+ # EXCEPTION TRANSLATION ==================================
341
+
342
+ protected
343
+
344
+ LOST_CONNECTION_MESSAGES = [/remote connection closed/i].freeze
345
+
346
+ def lost_connection_messages
347
+ LOST_CONNECTION_MESSAGES
348
+ end
349
+
350
+ CONNECTION_NOT_ESTABLISHED_MESSAGES = [/can't find broker for database/i, /no .* nodes are available for database/i]
351
+
352
+ def connection_not_established_messages
353
+ CONNECTION_NOT_ESTABLISHED_MESSAGES
354
+ end
355
+
356
+ def translate_exception(exception, message)
357
+ case message
358
+ when /duplicate value in unique index/i
359
+ RecordNotUnique.new(message, exception)
360
+ when /too few values specified in the value list/i
361
+ # defaults to StatementInvalid, so we are okay, but just to be explicit...
362
+ super
363
+ when *lost_connection_messages
364
+ LostConnection.new(message, exception)
365
+ when *connection_not_established_messages
366
+ ConnectionNotEstablished.new(message)
367
+ else
368
+ super
369
+ end
370
+ end
371
+
372
+ # SCHEMA DEFINITIONS #####################################
373
+
374
+ public
375
+
376
+ def primary_key(table_name)
377
+ # n.b. active record does not support composite primary keys!
378
+ row = exec_query(<<-eosql, 'SCHEMA', [config[:schema], table_name.to_s]).rows.first
379
+ SELECT
380
+ indexfields.field as field_name
381
+ FROM
382
+ system.indexfields AS indexfields
383
+ WHERE
384
+ indexfields.schema = ? AND
385
+ indexfields.tablename = ?
386
+ eosql
387
+ row && row.first
388
+ end
389
+
390
+ def version
391
+ self.class::VERSION
392
+ end
393
+
394
+ # SCHEMA STATEMENTS ######################################
395
+
396
+ public
397
+
398
+ def table_exists?(table_name)
399
+ return false unless table_name
400
+
401
+ table_name = table_name.to_s.downcase
402
+ schema, table = table_name.split('.', 2)
403
+
404
+ unless table
405
+ table = schema
406
+ schema = nil
407
+ end
408
+
409
+ tables('SCHEMA', schema).include? table
410
+ end
411
+
412
+ def tables(name = 'SCHEMA', schema = nil)
413
+ result = exec_query(<<-eosql, name, [schema || config[:schema]])
414
+ SELECT
415
+ tablename
416
+ FROM
417
+ system.tables
418
+ WHERE
419
+ schema = ?
420
+ eosql
421
+ result.inject([]) do |tables, row|
422
+ row.symbolize_keys!
423
+ tables << row[:tablename].downcase
424
+ end
425
+ end
426
+
427
+ # Returns an array of indexes for the given table. Skip primary keys.
428
+ def indexes(table_name, name = nil)
429
+
430
+ # the following query returns something like this:
431
+ #
432
+ # INDEXNAME TABLENAME NON_UNIQUE FIELD LENGTH
433
+ # ---------------------- --------- ---------- --------- ------
434
+ # COMPANIES..PRIMARY_KEY COMPANIES 0 ID 4
435
+ # COMPANY_INDEX COMPANIES 1 FIRM_ID 4
436
+ # COMPANY_INDEX COMPANIES 1 TYPE 255
437
+ # COMPANY_INDEX COMPANIES 1 RATING 4
438
+ # COMPANY_INDEX COMPANIES 1 RUBY_TYPE 255
439
+
440
+ result = exec_query(<<-eosql, 'SCHEMA', [config[:schema], table_name.to_s])
441
+ SELECT
442
+ indexes.indexname as index_name,
443
+ indexes.tablename as table_name,
444
+ CASE indexes.indextype WHEN 2 THEN 1 ELSE 0 END AS non_unique,
445
+ indexfields.field as field_name,
446
+ fields.length as field_length
447
+ FROM
448
+ system.indexes AS indexes, system.indexfields AS indexfields, system.fields AS fields
449
+ WHERE
450
+ indexes.schema = ? AND
451
+ indexes.tablename = ? AND
452
+ indexes.indexname = indexfields.indexname AND
453
+ indexfields.field = fields.field AND
454
+ indexfields.schema = fields.schema AND
455
+ indexfields.tablename = fields.tablename
456
+ eosql
457
+ indexes = []
458
+ current_index = nil
459
+ result.map do |row|
460
+ row.symbolize_keys!
461
+ index_name = row[:index_name]
462
+ if current_index != index_name
463
+ next if !!(/PRIMARY/ =~ index_name)
464
+ current_index = index_name
465
+ table_name = row[:table_name]
466
+ is_unique = row[:non_unique].to_i == 1
467
+ indexes << IndexDefinition.new(table_name, index_name, is_unique, [], [], [])
468
+ end
469
+ indexes.last.columns << row[:field_name] unless row[:field_name].nil?
470
+ indexes.last.lengths << row[:field_length] unless row[:field_length].nil?
471
+ end
472
+ indexes
473
+ end
474
+
475
+ def columns(table_name, name = nil)
476
+ sql = 'select field,datatype,length from system.fields where schema = ? and tablename = ?'
477
+ cache = statements[sql] ||= {
478
+ :stmt => raw_connection.prepare(sql)
479
+ }
480
+ stmt = cache[:stmt]
481
+
482
+ schema_name, table_name = split_table_name table_name
483
+ stmt.setString 1, schema_name
484
+ stmt.setString 2, table_name
485
+
486
+ rset = stmt.executeQuery
487
+ cols = []
488
+ while rset.next
489
+ name = rset.getString(1).downcase
490
+ # todo this was unimplemented, fix this mess
491
+ default = nil
492
+ sql_type = nil # TODO should come from query
493
+ null = true # TODO should come from query
494
+ cols << Column.new(name, default, sql_type, null)
495
+ end
496
+ cols
497
+ end
498
+
499
+ # todo implement the remaining schema statements methods: rename columns, tables, etc...
500
+ # todo, and these methods have to clear the cache!!!
501
+
502
+ public
503
+
504
+ def native_database_types
505
+ NATIVE_DATABASE_TYPES
506
+ end
507
+
508
+ def type_to_sql(type, limit = nil, precision = nil, scale = nil)
509
+ case type.to_s
510
+ when 'integer'
511
+ return 'integer' unless limit
512
+ case limit
513
+ when 1, 2
514
+ 'smallint'
515
+ when 3, 4
516
+ 'integer'
517
+ when 5..8
518
+ 'bigint'
519
+ else
520
+ raise(ActiveRecordError, "No integer type has byte size #{limit}. Use a numeric with precision 0 instead.")
521
+ end
522
+ else
523
+ super
524
+ end
525
+ end
526
+
527
+ private
528
+
529
+ NATIVE_DATABASE_TYPES = {
530
+ :primary_key => 'int not null generated by default as identity primary key',
531
+ :string => {:name => 'varchar', :limit => 255},
532
+ :text => {:name => 'varchar', :limit => 255},
533
+ :integer => {:name => 'integer'},
534
+ :float => {:name => 'float', :limit => 8},
535
+ :decimal => {:name => 'decimal'},
536
+ :datetime => {:name => 'datetime'},
537
+ :timestamp => {:name => 'datetime'},
538
+ :time => {:name => 'time'},
539
+ :date => {:name => 'date'},
540
+ :binary => {:name => 'binary'},
541
+ :boolean => {:name => 'boolean'},
542
+ :char => {:name => 'char'},
543
+ :varchar_max => {:name => 'varchar(max)'},
544
+ :nchar => {:name => 'nchar'},
545
+ :nvarchar => {:name => 'nvarchar', :limit => 255},
546
+ :nvarchar_max => {:name => 'nvarchar(max)'},
547
+ :ntext => {:name => 'ntext', :limit => 255},
548
+ :ss_timestamp => {:name => 'timestamp'}
549
+ }
550
+
551
+ def split_table_name(table)
552
+ name_parts = table.split '.'
553
+ case name_parts.length
554
+ when 1
555
+ schema_name = raw_connection.getSchema
556
+ table_name = name_parts[0]
557
+ when 2
558
+ schema_name = name_parts[0]
559
+ table_name = name_parts[1]
560
+ else
561
+ raise "Invalid table name: #{table}"
562
+ end
563
+ [schema_name, table_name]
564
+ end
565
+
566
+ # QUOTING ################################################
567
+
568
+ public
569
+
570
+ def quote_column_name(name)
571
+ @quoted_column_names[name] ||= "`#{name.to_s.gsub('`', '``')}`"
572
+ end
573
+
574
+ def quote_table_name(name)
575
+ @quoted_table_names[name] ||= quote_column_name(name).gsub('.', '`.`')
576
+ end
577
+
578
+ def quoted_true
579
+ "'true'"
580
+ end
581
+
582
+ def quoted_false
583
+ "'false'"
584
+ end
585
+
586
+ # DATABASE STATEMENTS ####################################
587
+
588
+ public
589
+
590
+ def outside_transaction?
591
+ false
592
+ end
593
+
594
+ def supports_statement_cache?
595
+ true
596
+ end
597
+
598
+ # Begins the transaction (and turns off auto-committing).
599
+ def begin_db_transaction()
600
+ log('begin transaction', nil) {
601
+ raw_connection.autocommit = false if raw_connection.autocommit?
602
+ }
603
+ end
604
+
605
+ # Commits the transaction (and turns on auto-committing).
606
+ def commit_db_transaction()
607
+ log('commit transaction', nil) {
608
+ raw_connection.autocommit = true unless raw_connection.autocommit?
609
+ raw_connection.commit
610
+ }
611
+ end
612
+
613
+ # Rolls back the transaction (and turns on auto-committing). Must be
614
+ # done if the transaction block raises an exception or returns false.
615
+ def rollback_db_transaction()
616
+ log('rollback transaction', nil) {
617
+ raw_connection.autocommit = true unless raw_connection.autocommit?
618
+ raw_connection.rollback
619
+ }
620
+ end
621
+
622
+ def execute(sql, name = 'SQL')
623
+ log(sql, name) do
624
+ cache = statements[process_id] ||= {
625
+ :stmt => raw_connection.createStatement
626
+ }
627
+ stmt = cache[:stmt]
628
+ stmt.execute(sql)
629
+ end
630
+ end
631
+
632
+ def exec_query(sql, name = 'SQL', binds = [])
633
+ log(sql, name, binds) do
634
+ if binds.empty?
635
+
636
+ cache = statements[process_id] ||= {
637
+ :stmt => raw_connection.createStatement
638
+ }
639
+ stmt = cache[:stmt]
640
+
641
+ stmt.execute(sql)
642
+ genkeys = stmt.getGeneratedKeys
643
+ row = genkeys ? next_row(genkeys) : nil
644
+ @last_inserted_id = row ? row[0] : nil
645
+ result = stmt.getResultSet
646
+ if result
647
+ names = column_names result
648
+ rows = all_rows result
649
+ ActiveRecord::Result.new(names, rows)
650
+ else
651
+ nil
652
+ end
653
+
654
+ else
655
+
656
+ cache = statements[sql] ||= {
657
+ :stmt => raw_connection.prepare(sql)
658
+ }
659
+ stmt = cache[:stmt]
660
+ binds.to_enum.with_index(1).each { |bind, column|
661
+ value = bind[1]
662
+ case value
663
+ when String
664
+ stmt.setString column, value
665
+ when Integer
666
+ stmt.setInteger column, value
667
+ when Fixnum
668
+ stmt.setInteger column, value
669
+ when Float
670
+ stmt.setDouble column, value
671
+ when TrueClass
672
+ stmt.setBoolean column, true
673
+ when FalseClass
674
+ stmt.setBoolean column, false
675
+ when Time
676
+ stmt.setTime column, value
677
+ else
678
+ raise "don't know how to bind #{value.class} to parameter #{column}"
679
+ end
680
+ }
681
+
682
+ stmt.execute
683
+
684
+ genkeys = stmt.getGeneratedKeys
685
+ row = genkeys ? next_row(genkeys) : nil
686
+ @last_inserted_id = row ? row[0] : nil
687
+
688
+ result = stmt.getResultSet
689
+
690
+ if result
691
+ names = column_names result
692
+ rows = all_rows result
693
+ ActiveRecord::Result.new(names, rows)
694
+ else
695
+ nil
696
+ end
697
+
698
+ end
699
+ end
700
+ end
701
+
702
+ def last_inserted_id(result)
703
+ @last_inserted_id
704
+ end
705
+
706
+ protected
707
+
708
+ def select(sql, name = nil, binds = [])
709
+ exec_query(sql, name, binds).to_a
710
+ end
711
+
712
+ protected
713
+
714
+ def select_rows(sql, name = nil)
715
+ rows = exec_query(sql, name).rows
716
+ end
717
+
718
+ private
719
+
720
+ def column_names (result)
721
+ return [] if result.nil?
722
+ names = []
723
+ meta = result.getMetaData
724
+ count = meta.getColumnCount
725
+ (1..count).each { |i|
726
+ names << meta.getColumnName(i).downcase
727
+ }
728
+ names
729
+ end
730
+
731
+ def all_rows(result)
732
+ rows = []
733
+ while (row = next_row(result)) != nil
734
+ rows << row
735
+ end
736
+ rows
737
+ end
738
+
739
+ def next_row(result)
740
+ return nil if result.nil?
741
+ if result.next
742
+ meta = result.getMetaData
743
+ count = meta.getColumnCount
744
+ row = []
745
+ (1..count).each { |i|
746
+ type = meta.getType(i)
747
+ case type
748
+ when :SQL_INTEGER
749
+ row << result.getInteger(i)
750
+ when :SQL_DOUBLE
751
+ row << result.getDouble(i)
752
+ when :SQL_STRING
753
+ row << result.getString(i)
754
+ when :SQL_DATE
755
+ row << result.getDate(i)
756
+ when :SQL_TIME
757
+ row << result.getTime(i)
758
+ when :SQL_TIMESTAMP
759
+ row << result.getTimestamp(i)
760
+ when :SQL_CHAR
761
+ row << result.getChar(i)
762
+ when :SQL_BOOLEAN
763
+ row << result.getBoolean(i)
764
+ else
765
+ raise "unknown type #{type} for column #{i}"
766
+ end
767
+ }
768
+ row
769
+ else
770
+ nil
771
+ end
772
+ end
773
+
774
+ end
775
+
776
+ end
777
+
778
+ end