odba 1.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.
@@ -0,0 +1,628 @@
1
+ #!/usr/bin/env ruby
2
+ #-- Storage -- odba -- 29.04.2004 -- hwyss@ywesee.com rwaltert@ywesee.com mwalder@ywesee.com
3
+
4
+ require 'thread'
5
+ require 'singleton'
6
+ require 'dbi'
7
+
8
+ module ODBA
9
+ class Storage # :nodoc: all
10
+ include Singleton
11
+ attr_writer :dbi
12
+ BULK_FETCH_STEP = 2500
13
+ TABLES = [
14
+ # in table 'object', the isolated dumps of all objects are stored
15
+ ['object', <<-'SQL'],
16
+ CREATE TABLE object (
17
+ odba_id INTEGER NOT NULL, content TEXT,
18
+ name TEXT, prefetchable BOOLEAN, extent TEXT,
19
+ PRIMARY KEY(odba_id), UNIQUE(name)
20
+ );
21
+ SQL
22
+ ['prefetchable_index', <<-SQL],
23
+ CREATE INDEX prefetchable_index ON object(prefetchable);
24
+ SQL
25
+ ['extent_index', <<-SQL],
26
+ CREATE INDEX extent_index ON object(extent);
27
+ SQL
28
+ # helper table 'object_connection'
29
+ ['object_connection', <<-'SQL'],
30
+ CREATE TABLE object_connection (
31
+ origin_id integer, target_id integer,
32
+ PRIMARY KEY(origin_id, target_id)
33
+ );
34
+ SQL
35
+ ['target_id_index', <<-SQL],
36
+ CREATE INDEX target_id_index ON object_connection(target_id);
37
+ SQL
38
+ # helper table 'collection'
39
+ ['collection', <<-'SQL'],
40
+ CREATE TABLE collection (
41
+ odba_id integer NOT NULL, key text, value text,
42
+ PRIMARY KEY(odba_id, key)
43
+ );
44
+ SQL
45
+ ]
46
+ def initialize
47
+ @id_mutex = Mutex.new
48
+ end
49
+ def bulk_restore(bulk_fetch_ids)
50
+ if(bulk_fetch_ids.empty?)
51
+ []
52
+ else
53
+ bulk_fetch_ids = bulk_fetch_ids.uniq
54
+ rows = []
55
+ while(!(ids = bulk_fetch_ids.slice!(0, BULK_FETCH_STEP)).empty?)
56
+ sql = <<-SQL
57
+ SELECT odba_id, content FROM object
58
+ WHERE odba_id IN (#{ids.join(',')})
59
+ SQL
60
+ rows.concat(self.dbi.select_all(sql))
61
+ end
62
+ rows
63
+ end
64
+ end
65
+ def collection_fetch(odba_id, key_dump)
66
+ sql = <<-SQL
67
+ SELECT value FROM collection
68
+ WHERE odba_id = ? AND key = ?
69
+ SQL
70
+ row = self.dbi.select_one(sql, odba_id, key_dump)
71
+ row.first unless row.nil?
72
+ end
73
+ def collection_remove(odba_id, key_dump)
74
+ self.dbi.do <<-SQL, odba_id, key_dump
75
+ DELETE FROM collection
76
+ WHERE odba_id = ? AND key = ?
77
+ SQL
78
+ end
79
+ def collection_store(odba_id, key_dump, value_dump)
80
+ self.dbi.do <<-SQL, odba_id, key_dump, value_dump
81
+ INSERT INTO collection (odba_id, key, value)
82
+ VALUES (?, ?, ?)
83
+ SQL
84
+ end
85
+ def condition_index_delete(index_name, origin_id,
86
+ search_terms, target_id=nil)
87
+ values = []
88
+ sql = "DELETE FROM #{index_name}"
89
+ if(origin_id)
90
+ sql << " WHERE origin_id = ?"
91
+ else
92
+ sql << " WHERE origin_id IS ?"
93
+ end
94
+ search_terms.each { |key, value|
95
+ sql << " AND %s = ?" % key
96
+ values << value
97
+ }
98
+ if(target_id)
99
+ sql << " AND target_id = ?"
100
+ values << target_id
101
+ end
102
+ self.dbi.do sql, origin_id, *values
103
+ end
104
+ def condition_index_ids(index_name, id, id_name)
105
+ sql = <<-SQL
106
+ SELECT DISTINCT *
107
+ FROM #{index_name}
108
+ WHERE #{id_name}=?
109
+ SQL
110
+ self.dbi.select_all(sql, id)
111
+ end
112
+ def create_dictionary_map(language)
113
+ %w{lhword lpart_hword lword}.each { |token|
114
+ self.dbi.do <<-SQL
115
+ INSERT INTO pg_ts_cfgmap (ts_name, tok_alias, dict_name)
116
+ VALUES ('default_#{language}', '#{token}',
117
+ '{#{language}_ispell,#{language}_stem}')
118
+ SQL
119
+ }
120
+ [ 'url', 'host', 'sfloat', 'uri', 'int', 'float', 'email',
121
+ 'word', 'hword', 'nlword', 'nlpart_hword', 'part_hword',
122
+ 'nlhword', 'file', 'uint', 'version'
123
+ ].each { |token|
124
+ self.dbi.do <<-SQL
125
+ INSERT INTO pg_ts_cfgmap (ts_name, tok_alias, dict_name)
126
+ VALUES ('default_#{language}', '#{token}', '{simple}')
127
+ SQL
128
+ }
129
+ end
130
+ def create_condition_index(table_name, definition)
131
+ self.dbi.do <<-SQL
132
+ CREATE TABLE #{table_name} (
133
+ origin_id INTEGER,
134
+ #{definition.collect { |*pair| pair.join(' ') }.join(",\n ") },
135
+ target_id INTEGER
136
+ );
137
+ SQL
138
+ #index origin_id
139
+ self.dbi.do <<-SQL
140
+ CREATE INDEX origin_id_#{table_name} ON #{table_name}(origin_id);
141
+ SQL
142
+ #index search_term
143
+ definition.each { |name, datatype|
144
+ self.dbi.do <<-SQL
145
+ CREATE INDEX #{name}_#{table_name} ON #{table_name}(#{name});
146
+ SQL
147
+ }
148
+ #index target_id
149
+ self.dbi.do <<-SQL
150
+ CREATE INDEX target_id_#{table_name} ON #{table_name}(target_id);
151
+ SQL
152
+ end
153
+ def create_fulltext_index(table_name)
154
+ self.dbi.do <<-SQL
155
+ CREATE TABLE #{table_name} (
156
+ origin_id INTEGER,
157
+ search_term tsvector,
158
+ target_id INTEGER
159
+ );
160
+ SQL
161
+ #index origin_id
162
+ self.dbi.do <<-SQL
163
+ CREATE INDEX origin_id_#{table_name} ON #{table_name}(origin_id);
164
+ SQL
165
+ #index search_term
166
+ self.dbi.do <<-SQL
167
+ CREATE INDEX search_term_#{table_name}
168
+ ON #{table_name} USING gist(search_term);
169
+ SQL
170
+ #index target_id
171
+ self.dbi.do <<-SQL
172
+ CREATE INDEX target_id_#{table_name} ON #{table_name}(target_id);
173
+ SQL
174
+ end
175
+ def create_index(table_name)
176
+ self.dbi.do <<-SQL
177
+ CREATE TABLE #{table_name} (
178
+ origin_id INTEGER,
179
+ search_term TEXT,
180
+ target_id INTEGER
181
+ );
182
+ SQL
183
+ #index origin_id
184
+ self.dbi.do <<-SQL
185
+ CREATE INDEX origin_id_#{table_name}
186
+ ON #{table_name}(origin_id)
187
+ SQL
188
+ #index search_term
189
+ self.dbi.do <<-SQL
190
+ CREATE INDEX search_term_#{table_name}
191
+ ON #{table_name}(search_term)
192
+ SQL
193
+ #index target_id
194
+ self.dbi.do <<-SQL
195
+ CREATE INDEX target_id_#{table_name}
196
+ ON #{table_name}(target_id)
197
+ SQL
198
+ end
199
+ def dbi
200
+ Thread.current[:txn] || @dbi
201
+ end
202
+ def drop_index(index_name)
203
+ self.dbi.do "DROP TABLE #{index_name}"
204
+ end
205
+ def delete_index_element(index_name, odba_id, id_name)
206
+ self.dbi.do <<-SQL, odba_id
207
+ DELETE FROM #{index_name} WHERE #{id_name} = ?
208
+ SQL
209
+ end
210
+ def delete_persistable(odba_id)
211
+ # delete origin from connections
212
+ self.dbi.do <<-SQL, odba_id
213
+ DELETE FROM object_connection WHERE origin_id = ?
214
+ SQL
215
+ # delete target from connections
216
+ self.dbi.do <<-SQL, odba_id
217
+ DELETE FROM object_connection WHERE target_id = ?
218
+ SQL
219
+ # delete from collections
220
+ self.dbi.do <<-SQL, odba_id
221
+ DELETE FROM collection WHERE odba_id = ?
222
+ SQL
223
+ # delete from objects
224
+ self.dbi.do <<-SQL, odba_id
225
+ DELETE FROM object WHERE odba_id = ?
226
+ SQL
227
+ end
228
+ def ensure_object_connections(origin_id, target_ids)
229
+ sql = <<-SQL
230
+ SELECT target_id FROM object_connection
231
+ WHERE origin_id = ?
232
+ SQL
233
+ target_ids.uniq!
234
+ update_ids = target_ids
235
+ old_ids = []
236
+ ## use self.dbi instead of @dbi to get information about
237
+ ## object_connections previously stored within this transaction
238
+ if(rows = self.dbi.select_all(sql, origin_id))
239
+ old_ids = rows.collect { |row| row[0] }
240
+ old_ids.uniq!
241
+ delete_ids = old_ids - target_ids
242
+ update_ids = target_ids - old_ids
243
+ unless(delete_ids.empty?)
244
+ while(!(ids = delete_ids.slice!(0, BULK_FETCH_STEP)).empty?)
245
+ self.dbi.do <<-SQL, origin_id
246
+ DELETE FROM object_connection
247
+ WHERE origin_id = ? AND target_id IN (#{ids.join(',')})
248
+ SQL
249
+ end
250
+ end
251
+ end
252
+ sth = self.dbi.prepare <<-SQL
253
+ INSERT INTO object_connection (origin_id, target_id)
254
+ VALUES (?, ?)
255
+ SQL
256
+ update_ids.each { |id|
257
+ sth.execute(origin_id, id)
258
+ }
259
+ sth.finish
260
+ end
261
+ def ensure_target_id_index(table_name)
262
+ #index target_id
263
+ self.dbi.do <<-SQL
264
+ CREATE INDEX target_id_#{table_name}
265
+ ON #{table_name}(target_id)
266
+ SQL
267
+ rescue
268
+ end
269
+ def extent_count(klass)
270
+ self.dbi.select_one(<<-EOQ, klass.to_s).first
271
+ SELECT COUNT(odba_id) FROM object WHERE extent = ?
272
+ EOQ
273
+ end
274
+ def extent_ids(klass)
275
+ self.dbi.select_all(<<-EOQ, klass.to_s).flatten
276
+ SELECT odba_id FROM object WHERE extent = ?
277
+ EOQ
278
+ end
279
+ def fulltext_index_delete(index_name, id, id_name)
280
+ self.dbi.do <<-SQL, id
281
+ DELETE FROM #{index_name}
282
+ WHERE #{id_name} = ?
283
+ SQL
284
+ end
285
+ def fulltext_index_target_ids(index_name, origin_id)
286
+ sql = <<-SQL
287
+ SELECT DISTINCT target_id
288
+ FROM #{index_name}
289
+ WHERE origin_id=?
290
+ SQL
291
+ self.dbi.select_all(sql, origin_id)
292
+ end
293
+ def generate_dictionary(language, locale, dict_dir)
294
+ # setup configuration
295
+ self.dbi.do <<-SQL
296
+ INSERT INTO pg_ts_cfg (ts_name, prs_name, locale)
297
+ VALUES ('default_#{language}', 'default', '#{locale}');
298
+ SQL
299
+ # insert path to dictionary
300
+ sql = <<-SQL
301
+ INSERT INTO pg_ts_dict (
302
+ SELECT '#{language}_ispell', dict_init, ?, dict_lexize
303
+ FROM pg_ts_dict
304
+ WHERE dict_name = 'ispell_template'
305
+ );
306
+ SQL
307
+ prepath = File.expand_path("fulltext", dict_dir)
308
+ path = %w{Aff Dict Stop}.collect { |type|
309
+ sprintf('%sFile="%s.%s"', type, prepath, type.downcase)
310
+ }.join(',')
311
+ sth.do sql, path
312
+ create_dictionary_map(language)
313
+ self.dbi.do <<-SQL
314
+ INSERT INTO pg_ts_dict (
315
+ dict_name, dict_init, dict_lexize
316
+ )
317
+ VALUES (
318
+ '#{language}_stem', 'dinit_#{language}(internal)',
319
+ 'snb_lexize(internal, internal, int4)'
320
+ );
321
+ SQL
322
+ end
323
+ def index_delete_origin(index_name, odba_id, term)
324
+ self.dbi.do <<-SQL, odba_id, term
325
+ DELETE FROM #{index_name}
326
+ WHERE origin_id = ?
327
+ AND search_term = ?
328
+ SQL
329
+ end
330
+ def index_delete_target(index_name, origin_id, search_term, target_id)
331
+ self.dbi.do <<-SQL, origin_id, search_term, target_id
332
+ DELETE FROM #{index_name}
333
+ WHERE origin_id = ?
334
+ AND search_term = ?
335
+ AND target_id = ?
336
+ SQL
337
+ end
338
+ def index_fetch_keys(index_name, length=nil)
339
+ expr = if(length)
340
+ "substr(search_term, 1, #{length})"
341
+ else
342
+ "search_term"
343
+ end
344
+ sql = <<-SQL
345
+ SELECT DISTINCT #{expr} AS key
346
+ FROM #{index_name}
347
+ ORDER BY key
348
+ SQL
349
+ self.dbi.select_all(sql).flatten
350
+ end
351
+ def index_matches(index_name, substring, limit=nil, offset=0)
352
+ sql = <<-SQL
353
+ SELECT DISTINCT search_term AS key
354
+ FROM #{index_name}
355
+ WHERE search_term LIKE ?
356
+ ORDER BY key
357
+ SQL
358
+ if limit
359
+ sql << "LIMIT #{limit}\n"
360
+ end
361
+ if offset > 0
362
+ sql << "OFFSET #{offset}\n"
363
+ end
364
+ self.dbi.select_all(sql, substring + '%').flatten
365
+ end
366
+ def index_origin_ids(index_name, target_id)
367
+ sql = <<-SQL
368
+ SELECT DISTINCT origin_id, search_term
369
+ FROM #{index_name}
370
+ WHERE target_id=?
371
+ SQL
372
+ self.dbi.select_all(sql, target_id)
373
+ end
374
+ def index_target_ids(index_name, origin_id)
375
+ sql = <<-SQL
376
+ SELECT DISTINCT target_id, search_term
377
+ FROM #{index_name}
378
+ WHERE origin_id=?
379
+ SQL
380
+ self.dbi.select_all(sql, origin_id)
381
+ end
382
+ def max_id
383
+ @id_mutex.synchronize do
384
+ ensure_next_id_set
385
+ @next_id
386
+ end
387
+ end
388
+ def next_id
389
+ @id_mutex.synchronize do
390
+ ensure_next_id_set
391
+ @next_id += 1
392
+ end
393
+ end
394
+ def reserve_next_id(reserved_id)
395
+ @id_mutex.synchronize do
396
+ ensure_next_id_set
397
+ if @next_id < reserved_id
398
+ @next_id = reserved_id
399
+ else
400
+ raise OdbaDuplicateIdError,
401
+ "The id '#{reserved_id}' has already been assigned"
402
+ end
403
+ end
404
+ end
405
+ def remove_dictionary(language)
406
+ # remove configuration
407
+ self.dbi.do <<-SQL
408
+ DELETE FROM pg_ts_cfg
409
+ WHERE ts_name='default_#{language}'
410
+ SQL
411
+ # remove dictionaries
412
+ self.dbi.do <<-SQL
413
+ DELETE FROM pg_ts_dict
414
+ WHERE dict_name IN ('#{language}_ispell', '#{language}_stem')
415
+ SQL
416
+ # remove tokens
417
+ self.dbi.do <<-SQL
418
+ DELETE FROM pg_ts_cfgmap
419
+ WHERE ts_name='default_#{language}'
420
+ SQL
421
+ end
422
+ def restore(odba_id)
423
+ row = self.dbi.select_one("SELECT content FROM object WHERE odba_id = ?", odba_id)
424
+ row.first unless row.nil?
425
+ end
426
+ def retrieve_connected_objects(target_id)
427
+ sql = <<-SQL
428
+ SELECT origin_id FROM object_connection
429
+ WHERE target_id = ?
430
+ SQL
431
+ self.dbi.select_all(sql, target_id)
432
+ end
433
+ def retrieve_from_condition_index(index_name, conditions, limit=nil)
434
+ sql = <<-EOQ
435
+ SELECT target_id, COUNT(target_id) AS relevance
436
+ FROM #{index_name}
437
+ WHERE TRUE
438
+ EOQ
439
+ values = []
440
+ lines = conditions.collect { |name, info|
441
+ val = nil
442
+ condition = nil
443
+ if(info.is_a?(Hash))
444
+ condition = info['condition']
445
+ if(val = info['value'])
446
+ if(/i?like/i.match(condition))
447
+ val += '%'
448
+ end
449
+ condition = "#{condition || '='} ?"
450
+ values.push(val.to_s)
451
+ end
452
+ elsif(info)
453
+ condition = "= ?"
454
+ values.push(info.to_s)
455
+ end
456
+ sql << <<-EOQ
457
+ AND #{name} #{condition || 'IS NULL'}
458
+ EOQ
459
+ }
460
+ sql << " GROUP BY target_id\n"
461
+ if(limit)
462
+ sql << " LIMIT #{limit}"
463
+ end
464
+ self.dbi.select_all(sql, *values)
465
+ end
466
+ def retrieve_from_fulltext_index(index_name, search_term, dict, limit=nil)
467
+ ## this combination of gsub statements solves the problem of
468
+ # properly escaping strings of this form: "(2:1)" into
469
+ # '\(2\:1\)' (see test_retrieve_from_fulltext_index)
470
+ term = search_term.strip.gsub(/\s+/, '&').gsub(/&+/, '&')\
471
+ .gsub(/[():]/i, '\\ \\&').gsub(/\s/, '')
472
+ sql = <<-EOQ
473
+ SELECT target_id,
474
+ max(ts_rank(search_term, to_tsquery(?, ?))) AS relevance
475
+ FROM #{index_name}
476
+ WHERE search_term @@ to_tsquery(?, ?)
477
+ GROUP BY target_id
478
+ ORDER BY relevance DESC
479
+ EOQ
480
+ if(limit)
481
+ sql << " LIMIT #{limit}"
482
+ end
483
+ self.dbi.select_all(sql, dict, term, dict, term)
484
+ rescue DBI::ProgrammingError => e
485
+ warn("ODBA::Storage.retrieve_from_fulltext_index rescued a DBI::ProgrammingError(#{e.message}). Query:")
486
+ warn("self.dbi.select_all(#{sql}, #{dict}, #{term}, #{dict}, #{term})")
487
+ warn("returning empty result")
488
+ []
489
+ end
490
+ def retrieve_from_index(index_name, search_term,
491
+ exact=nil, limit=nil)
492
+ unless(exact)
493
+ search_term = search_term + "%"
494
+ end
495
+ sql = <<-EOQ
496
+ SELECT target_id, COUNT(target_id) AS relevance
497
+ FROM #{index_name}
498
+ WHERE search_term LIKE ?
499
+ GROUP BY target_id
500
+ EOQ
501
+ if(limit)
502
+ sql << " LIMIT #{limit}"
503
+ end
504
+ self.dbi.select_all(sql, search_term)
505
+ end
506
+ def restore_collection(odba_id)
507
+ self.dbi.select_all <<-EOQ
508
+ SELECT key, value FROM collection WHERE odba_id = #{odba_id}
509
+ EOQ
510
+ end
511
+ def restore_named(name)
512
+ row = self.dbi.select_one("SELECT content FROM object WHERE name = ?",
513
+ name)
514
+ row.first unless row.nil?
515
+ end
516
+ def restore_prefetchable
517
+ self.dbi.select_all <<-EOQ
518
+ SELECT odba_id, content FROM object WHERE prefetchable = true
519
+ EOQ
520
+ end
521
+ def setup
522
+ TABLES.each { |name, definition|
523
+ self.dbi.do(definition) rescue DBI::ProgrammingError
524
+ }
525
+ unless(self.dbi.columns('object').any? { |col| col.name == 'extent' })
526
+ self.dbi.do <<-EOS
527
+ ALTER TABLE object ADD COLUMN extent TEXT;
528
+ CREATE INDEX extent_index ON object(extent);
529
+ EOS
530
+ end
531
+ end
532
+ def store(odba_id, dump, name, prefetchable, klass)
533
+ sql = "SELECT name FROM object WHERE odba_id = ?"
534
+ if(row = self.dbi.select_one(sql, odba_id))
535
+ name ||= row['name']
536
+ self.dbi.do <<-SQL, dump, name, prefetchable, klass.to_s, odba_id
537
+ UPDATE object SET
538
+ content = ?,
539
+ name = ?,
540
+ prefetchable = ?,
541
+ extent = ?
542
+ WHERE odba_id = ?
543
+ SQL
544
+ else
545
+ self.dbi.do <<-SQL, odba_id, dump, name, prefetchable, klass.to_s
546
+ INSERT INTO object (odba_id, content, name, prefetchable, extent)
547
+ VALUES (?, ?, ?, ?, ?)
548
+ SQL
549
+ end
550
+ end
551
+ def transaction(&block)
552
+ dbi = nil
553
+ retval = nil
554
+ @dbi.transaction { |dbi|
555
+ ## this should not be necessary anymore:
556
+ #dbi['AutoCommit'] = false
557
+ Thread.current[:txn] = dbi
558
+ retval = block.call
559
+ }
560
+ retval
561
+ ensure
562
+ ## this should not be necessary anymore:
563
+ #dbi['AutoCommit'] = true
564
+ Thread.current[:txn] = nil
565
+ end
566
+ def update_condition_index(index_name, origin_id, search_terms, target_id)
567
+ keys = []
568
+ vals = []
569
+ search_terms.each { |key, val|
570
+ keys.push(key)
571
+ vals.push(val)
572
+ }
573
+ if(target_id)
574
+ self.dbi.do <<-SQL, origin_id, target_id, *vals
575
+ INSERT INTO #{index_name} (origin_id, target_id, #{keys.join(', ')})
576
+ VALUES (?, ?#{', ?' * keys.size})
577
+ SQL
578
+ else
579
+ key_str = keys.collect { |key| "#{key}=?" }.join(', ')
580
+ self.dbi.do <<-SQL, *(vals.push(origin_id))
581
+ UPDATE #{index_name} SET #{key_str}
582
+ WHERE origin_id = ?
583
+ SQL
584
+ end
585
+ end
586
+ def update_fulltext_index(index_name, origin_id, search_term, target_id, dict)
587
+ search_term = search_term.gsub(/\s+/, ' ').strip
588
+ if(target_id)
589
+ self.dbi.do <<-SQL, origin_id, dict, search_term, target_id
590
+ INSERT INTO #{index_name} (origin_id, search_term, target_id)
591
+ VALUES (?, to_tsvector(?, ?), ?)
592
+ SQL
593
+ else
594
+ self.dbi.do <<-SQL, dict, search_term, origin_id
595
+ UPDATE #{index_name} SET search_term=to_tsvector(?, ?)
596
+ WHERE origin_id=?
597
+ SQL
598
+ end
599
+ end
600
+ def update_index(index_name, origin_id, search_term, target_id)
601
+ if(target_id)
602
+ self.dbi.do <<-SQL, origin_id, search_term, target_id
603
+ INSERT INTO #{index_name} (origin_id, search_term, target_id)
604
+ VALUES (?, ?, ?)
605
+ SQL
606
+ else
607
+ self.dbi.do <<-SQL, search_term, origin_id
608
+ UPDATE #{index_name} SET search_term=?
609
+ WHERE origin_id=?
610
+ SQL
611
+ end
612
+ end
613
+ private
614
+ def ensure_next_id_set
615
+ if(@next_id.nil?)
616
+ @next_id = restore_max_id
617
+ end
618
+ end
619
+ def restore_max_id
620
+ row = self.dbi.select_one("SELECT odba_id FROM object ORDER BY odba_id DESC LIMIT 1")
621
+ unless(row.nil? || row.first.nil?)
622
+ row.first
623
+ else
624
+ 0
625
+ end
626
+ end
627
+ end
628
+ end