Class: VORuby::ActiveVOTable::Base

Inherits:
ActiveRecord::Base
  • Object
show all
Defined in:
lib/voruby/active_votable/active_votable.rb

Overview

Have you ever wished you could treat your VOTable as a database? Well, now you can. ActiveVOTable is a package for reading votables into a relational database. It uses the SAX parser in LibXML so it can handle votables of arbitrary size, and it wraps the resulting database tables in ActiveRecord so that you can easily execute queries against it. It can even handle votables with multiple tables in multiple resources.

ActiveVOTable::Base.logger = Logger.new(STDOUT) # set the logger to output to STDOUT, exactly as in ActiveRecord
ActiveVOTable::Base.establish_connection(:adapter => 'sqlite3', :database => 'votables.sqlite3') # connect to the database

# read in the file results.vot into a SQLite database
vot = ActiveVOTable::Base.from(:xml, File.new('results.vot'), 'results')

table = vot.first  # a votable can have more than one TABLE element, but we're interested in the first one.

# every table has a data section and schema section, both of which are accessible
data = table.data
schema = table.schema

# both data and schemata are ultimately simply subclasses of ActiveRecord::Base and have all the same methods
puts data.find(:first).inspect
  # => <VORuby::ActiveVOTable::Base::Result11::ResultData11 id: 1, resource_num: 1, table_num: 1, ra: 10.68, dec: 41.27, name: "N  224", rvel: -297, e_rvel: 5, r: 0.7>
puts schema.find(:first).inspect
  # => #<VORuby::ActiveVOTable::Base::Result11::ResultSchema11 id: 1, resource_num: 1, table_num: 1, vid: "col1", unit: "deg", datatype: "float", precision: "2", width: 6, ref: "J2000", name: "RA", ucd: "pos.eq.ra;meta.main", utype: nil, arraysize: nil, vtype: nil>

Note that in the above example the #vid method of the ResultSchema11 instance corresponds to the ID attribute of the FIELD and the #vtype method corresponds to the FIELD’s type attribute.

# when you're done, you can optionally delete any tables hanging around in the database
vot.cleanup()

# if the tables are already sitting around in the database (perhaps from a previous run)...
vot = ActiveVOTable::Base.from(:db, 'results')

ActiveVOTable::Base#from and ActiveVOTable::Base#cleanup both take an optional hash of connection parameters (the same kind ActiveRecord::Base#establish_connection does). This allows you the flexibility to upload your votables to different databases if desired. So something like this works:

 vot1 = ActiveVOTable::Base.from(:xml,
   File.new('results1.vot'),
   'results1', # this *must* be different from below
   :adapter => 'sqlite3', :database => 'votables.sqlite3'
 )

 vot2 = ActiveVOTable::Base.from(:xml,
   File.new('results2.vot'),
   'results2', # *must* be different from above
   :adapter => 'mysql', :host => 'localhost', :username => 'me', :password => 'secret', :database => 'votables'
 )

vot1.cleanup
vot2.cleanup

Constant Summary collapse

SCHEMA_ID =
'schema'
DATA_ID =
'data'

Class Method Summary collapse

Class Method Details

.cleanup(remove_class_on_cleanup = false) {|_self| ... } ⇒ Object

Delete any tables and (optionally) Ruby constants associated with an already existing votable.

# ActiveVOTable::Base::Result11, ActiveVOTable::Base::Result11Schema
# and ActiveVOTable::Base::Result11Data are still hanging around after this
vot.cleanup

or…

# ActiveVOTable::Base::Result11, ActiveVOTable::Base::Result11Schema
# and ActiveVOTable::Base::Result11Data no longer exist...
vot.cleanup(true)

There is also a block form which allows you access to the classes after their database tables have been destroyed but before the classes themselves have been disposed of. This is mostly for testing purposes and very rarely used in real life.

vot.cleanup(true) do |v|
  # the db tables are gone, but I can still play with the classes if I want...
end

Yields:

  • (_self)

Yield Parameters:



420
421
422
423
424
# File 'lib/voruby/active_votable/active_votable.rb', line 420

def self.cleanup(remove_class_on_cleanup=false)
  self.dbtables.each { |t| self.connection.drop_table(t) }
  yield self if block_given?
  superclass.send(:remove_const, self.table_name.classify) if remove_class_on_cleanup and superclass.send(:const_defined?, self.table_name.classify)
end

.dataObject

List all the data objects associated with the votable. Typically, you’ll use #tables instead (which associates a schema with its corresponding data), but this is occassionally useful.

puts vot.data.inspect
  # => [ActiveVOTable::Base::ResultData11]


374
375
376
377
378
379
380
381
# File 'lib/voruby/active_votable/active_votable.rb', line 374

def self.data
  self.dbtables(/_#{DATA_ID}_\d+_\d+$/).collect do |t|
    data_klass = self.find_or_create_class(t.classify, self)
    data_klass.extend(Pager)
    data_klass.set_table_name(t)
    data_klass
  end
end

.dbtables(regex = nil) ⇒ Object

List the names of all the database tables associated with the votable in question. And addition regex may be given to further refine the list, if desired.

puts vot.dbtables.inspect
  # => ['result_data_1_1', 'result_schema_1_1']


331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
# File 'lib/voruby/active_votable/active_votable.rb', line 331

def self.dbtables(regex=nil)
  raise "#{connection.adapter_name} does not support #tables" if !connection.respond_to?(:tables)
  
  self.connection.tables.find_all{ |t|
    base_match = t.match(/^(#{self.table_name()})#{self.table_search_pattern()}$/)
    regex ? (base_match and t.match(regex)) : base_match
  }.sort { |a, b|
    a_matches = a.match(/^(#{self.table_name})#{self.table_search_pattern()}$/)
    b_matches = b.match(/^(#{self.table_name})#{self.table_search_pattern()}$/)
    
    a_matches[1] <=> b_matches[1] and
      a_matches[2] <=> a_matches[2] and
      a_matches[3].to_i <=> a_matches[3].to_i and
      b_matches[4].to_i <=> b_matches[4].to_i
  }
end

.find_or_create_class(klass_name, subclass = Base) ⇒ Object

:nodoc:



348
349
350
# File 'lib/voruby/active_votable/active_votable.rb', line 348

def self.find_or_create_class(klass_name, subclass=Base) #:nodoc:
  const_defined?(klass_name) ? const_get(klass_name) : const_set(klass_name, Class.new(subclass))
end

.from(src, *args) ⇒ Object

A convenience method around #from_xml and #from_database. The connection parameters are only necessary if you haven’t previously called ActiveVOTable::Base#establish_connection.

ActiveVOTable::Base.from(:xml,
  File.new('votable.xml'),
  'my_votable'
  :adapter => 'mysql', :host => 'localhost', :username => 'me', :password => 'secret', :database => 'votables',
)

ActiveVOTable::Base.from(:db,
  'my_votable',
  :adapter => 'mysql', :host => 'localhost', :username => 'me', :password => 'secret', :database => 'votables',
)


501
502
503
504
505
506
507
508
# File 'lib/voruby/active_votable/active_votable.rb', line 501

def self.from(src, *args)
  case src
  when :xml     then self.from_xml(*args)
  when :db      then self.from_database(*args)
  else
    raise "Source must one of: :xml, :db (not '#{src}')"
  end
end

.from_database(name, conn_params = nil, logger = nil, tables_must_exist = true) ⇒ Object

Instantiate a votable from a database.

Identical to #from_xml, except that it is assumed the appropriate tables already exist in the database.

vot = ActiveVOTable::Base.from_database(
  'my_votable' # for those tables that begin with 'my_votable',
  :adapter => 'mysql', :host => 'localhost', :username => 'me', :password => 'secret', :database => 'votables', # look in the MySQL database
)


474
475
476
477
478
479
480
481
482
483
484
485
# File 'lib/voruby/active_votable/active_votable.rb', line 474

def self.from_database(name, conn_params=nil, logger=nil, tables_must_exist=true)
  k = self.find_or_create_class(name.classify, Base)
  
  k.establish_connection(conn_params) if conn_params
  k.table_name = name
  k.logger = logger if logger
  k.inheritance_column = nil # 'type' is a common votable keyword, so we really want to turn simple inheritance off
  
  raise "tables corresponding to '#{name}' do not appear to exist" if tables_must_exist and k.dbtables.size == 0
  
  k
end

.from_xml(xml, name = nil, conn_params = nil, logger = nil) ⇒ Object

Instantiate a votable from XML.

xml

May be a string or a File object.

name

A string representing the root names of the tables that will be created in the database. If none is specified the string ‘votable’ + a timestamp will be used.

dboptions

An optional hash of database connection parameters (exactly as you’d pass to ActiveRecord::Base#establish_connection). Necessary only if ActiveVOTable::Base#establish_connection hasn’t been called.

An array of objects representing the tables in the votables is returned. Each of these objects has a #data method and a #schema method corresponding to the TABLEDATA and FIELD elements in the VOTable specification.

vot = ActiveVOTable::Base.from_xml(
  File.new('votable.xml'),  # parse the file votable.xml
  'my_votable' # every table created will be prefixed with 'my_votable',
  :adapter => 'mysql', :host => 'localhost', :username => 'me', :password => 'secret', :database => 'votables', # stick it into a MySQL database
)

vot.each do |table|
  puts table.schema.find(:all).inspect  # retrieve rich semantic information about each column
  puts table.data.find(:all, :conditions => ['ra > ?', 10.2]).inspect  # find the actual data
end

Assuming votable.xml had 2 resources, each with one table (for example) the table structure created would look like: my_votable_fields_1_1, my_votable_rows_1_1, my_votable_fields_2_1, my_votable_rows_2_1.



449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
# File 'lib/voruby/active_votable/active_votable.rb', line 449

def self.from_xml(xml, name=nil, conn_params=nil, logger=nil)
  name ||= "votable_#{DateTime.now.strftime('%Y%m%d%H%M%S%L')}"
  raise "XML source must be a string or a File object, not '#{xml}'" if !xml.is_a?(String) and !xml.is_a?(File)
  
  parser = XML::SaxParser.new
  xml.is_a?(String) ? parser.string = xml : parser.filename = xml.path

  k = self.from_database(name, conn_params, logger, false)

  callbacks = Callbacks.new(k)
  parser.callbacks = callbacks
  parser.parse
  
  k
end

.schemataObject

List all the schema objects associated with the votable. Typically, you’ll use #tables instead (which associates a schema with its corresponding data), but this is occassionally useful.

puts vot.schemata.inspect
  # => [ActiveVOTable::Base::ResultSchema11]


359
360
361
362
363
364
365
# File 'lib/voruby/active_votable/active_votable.rb', line 359

def self.schemata
  self.dbtables(/_#{SCHEMA_ID}_\d+_\d+$/).collect do |t|
    schema_klass = self.find_or_create_class(t.classify, self)
    schema_klass.set_table_name(t)
    schema_klass
  end
end

.table_search_patternObject

:nodoc:



321
322
323
# File 'lib/voruby/active_votable/active_votable.rb', line 321

def self.table_search_pattern #:nodoc:
  "_(#{SCHEMA_ID}|#{DATA_ID})_(\\d+)_(\\d+)"
end

.tablesObject

List all the tables associated with the votable. Each member of the turned array responds to a #schema and #data method.

vot.tables.each do |t|
  puts t.schema
  puts t.data
end


391
392
393
394
395
396
397
398
399
# File 'lib/voruby/active_votable/active_votable.rb', line 391

def self.tables
  data_list = self.data
  
  list = []
  self.schemata.each_with_index do |s, i|
    list << OpenStruct.new(:schema => s, :data => data_list[i])
  end
  list
end