lib/baza/db.rb
# A wrapper of several possible database-types.
#
#===Examples
# db = Baza::Db.new(type: :mysql2, db: "mysql", user: "user", pass: "password")
# mysql_table = db.tables['mysql']
# name = mysql_table.name
# cols = mysql_table.columns
#
# db = Baza::Db.new(type: :sqlite3, path: "some_db.sqlite3")
#
# db.q("SELECT * FROM users") do |data|
# print data[:name]
# end
class Baza::Db
include SimpleDelegate
delegate :last_id, :upsert, :upsert_duplicate_key, to: :commands
delegate :current_database, :current_database_name, :with_database, to: :databases
delegate *%i[close count delete esc escape escape_column escape_table escape_database escape_index quote_database quote_column quote_table quote_value quote_database quote_index insert select single quote_value sql_make_where], to: :driver
attr_reader :sep_database, :sep_col, :sep_table, :sep_val, :sep_index, :opts, :driver, :int_types
# Returns an array containing hashes of information about each registered driver.
def self.drivers
path = "#{File.dirname(__FILE__)}/driver"
drivers = []
Dir.foreach(path) do |file|
next if file.to_s.slice(0, 1) == "."
fp = "#{path}/#{file}"
next unless File.directory?(fp)
driver_file = "#{fp}/#{file}.rb"
class_name = StringCases.snake_to_camel(file).to_sym
drivers << {
name: file,
driver_path: driver_file,
class_name: class_name
}
end
drivers
end
# Tries to create a database-object based on the given object which could be a SQLite3 object or a MySQL 2 object (or other supported).
def self.from_object(args)
args = {object: args} unless args.is_a?(Hash)
raise "No :object was given." unless args[:object]
Baza::Db.drivers.each do |driver|
const = Baza::Driver.const_get(driver[:class_name])
next unless const.respond_to?(:from_object)
obj = const.from_object(args)
next unless obj.is_a?(Hash) && obj[:type] == :success
if obj[:args]
new_args = obj[:args]
new_args = new_args.merge(args[:new_args]) if args[:new_args]
return Baza::Db.new(new_args)
else
raise "Didnt know what to do."
end
end
raise "Could not figure out what to do what object of type: '#{args[:object].class.name}'."
end
def initialize(opts)
@driver = opts.delete(:driver) if opts[:driver]
Baza.load_driver(opts.fetch(:type))
self.opts = opts unless opts.nil?
@int_types = [:int, :bigint, :tinyint, :smallint, :mediumint]
@debug = @opts[:debug]
@driver = spawn
@sep_database = @driver.sep_database
@sep_table = @driver.sep_table
@sep_col = @driver.sep_col
@sep_index = @driver.sep_index
@sep_val = @driver.sep_val
return unless block_given?
begin
yield self
ensure
close
end
end
def args
@opts
end
def opts=(arr_opts)
@opts = {}
arr_opts.each do |key, val|
@opts[key.to_sym] = val
end
if RUBY_PLATFORM == "java"
@opts[:type] = :sqlite3_java if @opts[:type] == :sqlite3
@opts[:type] = :mysql_java if @opts[:type] == :mysql || @opts[:type] == :mysql2
end
@type_cc = StringCases.snake_to_camel(@opts[:type])
end
# Spawns a new driver (useally done automatically).
#===Examples
# driver_instance = db.spawn
def spawn
raise "No type given (#{@opts.keys.join(",")})." unless @opts[:type]
rpath = "#{File.dirname(__FILE__)}/driver/#{@opts.fetch(:type)}.rb"
require rpath if File.exist?(rpath)
Baza::Driver.const_get(@type_cc).new(self)
end
# Registers a driver to the current thread.
def register_thread
raise "Baza-object is not in threadding mode" unless @conns
thread_cur = Thread.current
tid = __id__
thread_cur[:baza] = {} unless thread_cur[:baza]
if thread_cur[:baza][tid]
# An object has already been spawned - free that first to avoid endless "used" objects.
free_thread
end
thread_cur[:baza][tid] = @conns.get_and_lock unless thread_cur[:baza][tid]
# If block given then be ensure to free thread after yielding.
return unless block_given?
begin
yield
ensure
free_thread
end
end
# The all driver-database-connections.
def close
@driver.close if @driver
@driver = nil
@closed = true
end
def closed?
@closed
end
# Clones the current database-connection with possible extra arguments.
def clone_conn(args = {})
conn = Baza::Db.new(@opts.merge(args))
if block_given?
begin
yield(conn)
ensure
conn.close
end
return nil
else
return conn
end
end
COPY_TO_ALLOWED_ARGS = [:tables, :debug].freeze
# Copies the content of the current database to another instance of Baza::Db.
def copy_to(db, args = {})
debug = args[:debug]
raise "No tables given." unless data[:tables]
data[:tables].each do |table|
table_args = nil
table_args = args[:tables][table[:name]] if args && args[:tables] && args[:tables][table[:name].to_sym]
next if table_args && table_args[:skip]
table.delete(:indexes) if table.key?(:indexes) && args[:skip_indexes]
table_name = table.delete(:name)
puts "Creating table: '#{table_name}'." if debug
db.tables.create(table_name, **table)
limit_from = 0
limit_incr = 1000
loop do
puts "Copying rows (#{limit_from}, #{limit_incr})." if debug
ins_arr = []
select(table_name, {}, limit_from: limit_from, limit_to: limit_incr) do |d_rows|
col_args = nil
if table_args && table_args[:columns]
d_rows.each_key do |col_name|
col_args = table_args[:columns][col_name] if table_args && table_args[:columns]
d_rows[col_name] = "" if col_args && col_args[:empty]
end
end
ins_arr << d_rows
end
break if ins_arr.empty?
puts "Insertering #{ins_arr.length} rows." if debug
db.insert_multi(table_name, ins_arr)
limit_from += limit_incr
end
end
end
# Returns the data of this database in a hash.
#===Examples
# data = db.data
# tables_hash = data['tables']
def data
tables_ret = []
tables.list do |table|
tables_ret << table.data
end
{tables: tables_ret}
end
def add_sql_to_error(error, sql)
error.message << " (SQL: #{sql})"
end
# Simply and optimal insert multiple rows into a table in a single query. Uses the drivers functionality if supported or inserts each row manually.
#
#===Examples
# db.insert_multi(:users, [
# {name: "John", lastname: "Doe"},
# {name: "Kasper", lastname: "Johansen"}
# ])
def insert_multi(tablename, arr_hashes, args = {})
return false if arr_hashes.empty?
if @driver.respond_to?(:insert_multi)
if args && args[:return_sql]
res = @driver.insert_multi(tablename, arr_hashes, args)
if res.is_a?(String)
return [res]
elsif res.is_a?(Array)
return res
else
raise "Unknown result: '#{res.class.name}'."
end
end
@driver.insert_multi(tablename, arr_hashes, args)
else
transaction do
arr_hashes.each do |hash|
insert(tablename, hash, args)
end
end
return nil
end
end
# Simple updates rows.
#
#===Examples
# db.update(:users, {name: "John"}, {lastname: "Doe"})
def update(table_name, data, terms = {}, args = {})
command = Baza::SqlQueries::GenericUpdate.new(
db: self,
table_name: table_name,
data: data,
terms: terms,
buffer: args[:buffer]
)
if args[:return_sql]
command.to_sql
else
command.execute
end
end
def in_transaction?
@in_transaction
end
# Executes a query and returns the result.
#
#===Examples
# res = db.query('SELECT * FROM users')
# while data = res.fetch
# print data[:name]
# end
def query(string, args = nil, &block)
if @debug
print "SQL: #{string}\n"
if @debug.class.name == "Fixnum" && @debug >= 2
print caller.join("\n")
print "\n"
end
end
# If the query should be executed in a new connection unbuffered.
if args && args[:cloned_ubuf]
raise "No block given." unless block
cloned_conn(clone_args: args[:clone_args]) do |cloned_conn|
return cloned_conn.query_ubuf(string, args, &block)
end
return nil
end
return query_ubuf(string, args, &block) if args && args[:type] == :unbuffered
ret = @driver.query(string)
if block && ret
ret.each(&block)
return nil
end
ret
end
alias q query
# Execute an ubuffered query and returns the result.
#
#===Examples
# db.query_ubuf('SELECT * FROM users') do |data|
# print data[:name]
# end
def query_ubuf(string, _args = nil, &block)
ret = @driver.query_ubuf(string)
if block
ret.each(&block)
return nil
end
ret
end
# Yields a query-buffer and flushes at the end of the block given.
def q_buffer(args = {}, &block)
Baza::QueryBuffer.new(args.merge(db: self), &block)
nil
end
# Returns a string which can be used in SQL with the current driver.
#===Examples
# str = db.date_out(Time.now) #=> "2012-05-20 22:06:09"
def date_out(date_obj = Datet.new, args = {})
return @driver.date_out(date_obj, args) if @driver.respond_to?(:date_out)
Datet.in(date_obj).dbstr(args)
end
# Takes a valid date-db-string and converts it into a Datet.
#===Examples
# db.date_in('2012-05-20 22:06:09') #=> 2012-05-20 22:06:09 +0200
def date_in(date_obj)
return @driver.date_in(date_obj) if @driver.respond_to?(:date_in)
Datet.in(date_obj)
end
# Defines all the driver methods: tables, columns and so on
DRIVER_PARTS = [:databases, :foreign_keys, :tables, :commands, :columns, :indexes, :users, :sqlspecs].freeze
DRIVER_PARTS.each do |driver_part|
define_method(driver_part) do
if instance_variable_defined?(:"@#{driver_part}")
instance_variable_get(:"@#{driver_part}")
else
require_relative "driver/#{@opts.fetch(:type)}/#{driver_part}"
instance = Baza::Driver.const_get(@type_cc).const_get(StringCases.snake_to_camel(driver_part)).new(db: self)
instance_variable_set(:"@#{driver_part}", instance)
instance
end
end
end
def supports_multiple_databases?
if @driver.respond_to?(:supports_multiple_databases?)
@driver.supports_multiple_databases?
else
false
end
end
def supports_type_translation?
if @driver.respond_to?(:supports_type_translation?)
@driver.supports_multiple_databases?
else
false
end
end
# Beings a transaction and commits when the block ends.
#
#===Examples
# db.transaction do |db|
# db.insert(:users, name: "John")
# db.insert(:users, name: "Kasper")
# end
def transaction(&block)
@in_transaction = true
begin
@driver.transaction(&block)
ensure
@in_transaction = false
end
self
end
# Optimizes all tables in the database.
def optimize(args = nil)
STDOUT.puts "Beginning optimization of database." if @debug || (args && args[:debug])
tables.list do |table|
STDOUT.puts "Optimizing table: '#{table.name}'." if @debug || (args && args[:debug])
table.optimize
end
self
end
def to_s
"#<Baza::Db driver=\"#{@opts.fetch(:type)}\">"
end
def inspect
to_s
end
def new_query
Baza::SqlQueries::Select.new(db: self)
end
def sqlite?
@driver.class.name.downcase.include?("sqlite") ||
(@driver.class.name == "Baza::Driver::ActiveRecord" && @driver.driver_type == :sqlite3)
end
def mysql?
@driver.class.name.downcase.include?("mysql")
end
def mssql?
@driver.class.name.downcase.include?("tiny")
end
def postgres?
@driver.class.name.downcase.include?("pg")
end
end