# Ruport : Extensible Reporting System # # query.rb provides a basic wrapper around RubyDBI for SQL interaction # # Original work began by Gregory Brown based on ideas from James Edward Gray II # in August, 2005. # # Copyright (C) 2005-2007, Gregory Brown # All Rights Reserved. # # This is free software distributed under the same terms as Ruby 1.8 # See LICENSE and COPYING for details. require "generator" module Ruport # === Overview # # Query offers a way to interact with databases via RubyDBI. It supports # returning result sets in either Ruport's Data::Table, or in their # raw form as DBI::Rows. # # Query allows you to treat your result sets as an Enumerable data structure # that plays well with the rest of Ruport. # # If you are using ActiveRecord, you might prefer our acts_as_reportable # extension. # class Query include Enumerable # Ruport::Query provides an interface for dealing with raw SQL queries. # The SQL can be single or multistatement, but the resulting Data::Table # will consist only of the result of the last statement. # # Available options: # # :source:: A source specified in # Ruport::Query.sources, defaults to # :default. # :dsn:: If specifed, the Query object will # manually override Ruport::Query. # :user:: If a DSN is specified, the user can # be set with this option. # :password:: If a DSN is specified, the password # can be set with this option. # :row_type:: When set to :raw, DBI::Rows will be # returned instead of a Data::Table # # Examples: # # # uses Ruport::Query's default source # Ruport::Query.new("select * from fo") # # # uses the Ruport::Query's source labeled :my_source # Ruport::Query.new("select * from fo", :source => :my_source) # # # uses a manually entered source # Ruport::Query.new("select * from fo", :dsn => "dbi:mysql:my_db", # :user => "greg", :password => "chunky_bacon" ) # # # uses a SQL file stored on disk # Ruport::Query.new("my_query.sql") # # # explicitly use a file, even if it doesn't end in .sql # Ruport::Query.new(:file => "foo") # # # query with parameter substitution # Ruport::Query.new("select * from fo where bar=?", :params => [1234]) # Ruport::Query.new(:file => "foo", :params => [1234]) # # # query with parameter substitution (ActiveRecord style) # Ruport::Query.new(["select * from fo where bar=?", 1234]) # def initialize(sql, options={}) if sql.kind_of?(Hash) options = { :source => :default }.merge(sql) sql = options[:file] || options[:string] else if sql.kind_of?(Array) options[:params] = sql[1..-1] sql = sql.first end options = { :source => :default, :string => sql }.merge(options) options[:file] = sql if sql =~ /\.sql$/ end origin = options[:file] ? :file : :string @statements = SqlSplit.new(get_query(origin,sql)) @sql = @statements.join if options[:dsn] Ruport::Query.add_source :temp, :dsn => options[:dsn], :user => options[:user], :password => options[:password] options[:source] = :temp end select_source(options[:source]) @raw_data = options[:row_type].eql?(:raw) @params = options[:params] end # Returns an OpenStruct with the configuration options for the default # database source. # def self.default_source sources[:default] end # Returns a hash of database sources, keyed by label. def self.sources @sources ||= {} end # Allows you to add a labeled DBI source configuration. # # Query objects will use the source labeled :default, # unless another source is specified. # # Examples: # # # a connection to a MySQL database foo with user root, pass chunkybacon # Query.add_source :default, :dsn => "dbi:mysql:foo", # :user => "root", # :password => "chunkybacon" # # # # a second connection to a MySQL database bar # Query.add_source :test, :dsn => "dbi:mysql:bar", # :user => "tester", # :password => "blinky" # # def self.add_source(name,options={}) sources[name] = OpenStruct.new(options) check_source(sources[name],name) end attr_accessor :raw_data # The original SQL for the Query object attr_reader :sql # This will set the dsn, username, and password # to one specified by a source in Ruport::Query. # def select_source(label) @dsn = Ruport::Query.sources[label].dsn @user = Ruport::Query.sources[label].user @password = Ruport::Query.sources[label].password end # Yields result set by row. def each(&action) raise(LocalJumpError, "No block given!") unless action fetch(&action) self end # Runs the SQL query and returns the result set def result; fetch; end # Runs the query without returning its results. def execute; fetch; nil; end # Returns a Data::Table, even if in raw_data mode. def to_table data_flag, @raw_data = @raw_data, false data = fetch; @raw_data = data_flag; return data end # Returns a csv dump of the query. def to_csv fetch.to_csv end # Returns a Generator object of the result set. def generator Generator.new(fetch) end private def query_data(query_text, params=@params) require "dbi" data = @raw_data ? [] : Data::Table.new DBI.connect(@dsn, @user, @password) do |dbh| dbh.execute(query_text, *(params || [])) do |sth| # Work-around for inconsistent DBD behavior w/ resultless queries names = sth.column_names rescue [] if names.empty? # Work-around for SQLite3 DBD bug sth.cancel rescue nil return nil end data.column_names = names unless @raw_data sth.each do |row| row = row.to_a row = Data::Record.new(row, :attributes => names) unless @raw_data yield row if block_given? data << row if !block_given? end end end data end def get_query(type,query) type.eql?(:file) ? load_file( query ) : query end def fetch(&block) data = nil final = @statements.size - 1 @statements.each_with_index do |query_text, index| data = query_data(query_text, &(index == final ? block : nil)) end return data end def load_file(query_file) begin File.read( query_file ).strip rescue raise LoadError, "Could not open #{query_file}" end end def self.check_source(settings,label) # :nodoc: raise ArgumentError unless settings.dsn end end # Created by Francis Hwang, 2005.12.31 # Copyright (c) 2005, All Rights Reserved. #++ class SqlSplit < Array #:nodoc: def initialize( sql ) super() next_sql = '' sql.each do |line| unless line =~ /^--/ or line =~ %r{^/\*.*\*/;} or line =~ /^\s*$/ next_sql << line if line =~ /;$/ next_sql.gsub!( /;\s$/, '' ) self << next_sql next_sql = '' end end end self << next_sql if next_sql != '' end end end