Skip to content
Snippets Groups Projects
Commit 8ecdcc2d authored by João Victor Risso's avatar João Victor Risso
Browse files

Merge branch 'master' of gitlab.c3sl.ufpr.br:simcaq/activerecord-monetdb-adapter

parents a496d03d 698a8798
Branches
No related tags found
No related merge requests found
Pipeline #
Showing with 2872 additions and 0 deletions
.buildpath
.project
.idea
*.gem
* Tue 30 Jun 2009 22:14:38 CEST
Added test cases for the standalone driver (ruby unit test).
* Thu 25 Jun 2009 17:31:02 CEST
Fixed a major bug that resulted in data corruption;
MAPI implementation code cleanup and bug fixes;
Support to ruby 1.9;
Both the standalone driver and activerecord now support transactions; nested transactions
are simulated via savepoints in activerecord.
Added a Rakefile and script to run activerecord unit test suite.
Type conversion in the standalone driver is now performed after data has been retrieved and can be executed on single fields.
* Mon 25 May 2009 17:52:01 CEST
Imported last week changes (protocol 9 support, parametrized the connection options in the activerecord adapter, fixed a bug in the auth protocol v8).
Fixed a bug in the mapi protocol that resulted in data loss (wrong handling of TRANSACTIONS).
Began to port the activerecord test suite to monetdb (not all test cases can be performed).
Removed an unneeded file ('lib/MonetDBStatement.rb') from the gemspec of the standalone driver (the feature will be moved to HEAD).
Began to port the driver to ruby 1.9.
Removed *.gem files from cvs.
* Mon 18 May 2009 15:22:31 CEST
Fixed bugs that prevented the correct working of activerecords' migration;
The activerecord connector now supports insertion, update and alter table operations;
Type casting is working in activerecord;
Added a rubygem and rakefile for activerecord-monetdb-adapter;
Added a new usage example for activerecord to the README file;
Added an example directory to the cvs tree;
The driver now correctly works with merovingian.
* Sat 9 May 2009 15:58:36 CEST
Fixed bugs with the query processing in the standalone driver;
Added INSERT and UPDATE methods in the activerecord connector.
* Thu 7 May 2009 17:03:01 CEST
Added a check against the protocol version during authentication;
Imported the activerecord code (available under adapter/).
README 0 → 100644
== Standalone driver ==
This directory contains the a ruby interface to monetdb5
written in pure ruby.
lib/MonetDB.rb
lib/MonetDBConnection.rb
lib/MonetDBStatements.rb
lib/MonetDBData.rb
lib/MonetDBExceptions.rb
lib/hasher.rb
lib/demo.rb: demo application how to interact with the database
ruby-monetdb-sql-0.1.gemspec: make file for rubygems
doc/: rubydoc in HTML format
== Installation ==
The standalone monetdb driver can be installed using the RubyGems Package Manager.
First build a gem file starting from the gemspec configuration:
$ gem build ruby-monetdb-sql-0.1.gemspec
Then install with the command:
$ gem install ruby-monetdb-sql-0.1.gem
== Usage ==
To use the standalone driver import the 'MonetDB' class and 'rubygems' (in case you installed it using gems).
A typical sequence of events is as follows:
Invoke query using the database handle to send the statement to the server and get back a result set object.
A result set object has methods for fetching rows, moving around in the result set, obtaining column metadata, and releasing the result set.
Use a row fetching method such as fetch_row or an iterator such as each to access the rows of the result set.
If you want a count of the number of rows in the result set: invoke 'num_rows' method.
Invoke 'free' to release the result set.
== Example ==
require 'MonetDB'
db = MonetDB.new
db.connect(user = "monetdb", passwd = "monetdb", lang = "sql", host="127.0.0.1", port = 50000, db_name = "demo", auth_type = "SHA1")
# set type_cast=true to enable MonetDB to Ruby type mapping
res = db.query("SELECT * from tables;", type_cast = false)
#puts res.debug_columns_type
puts "Number of rows returned: " + res.num_rows.to_s
puts "Number of fields: " + res.num_fields.to_s
# Get the columns' name
col_names = res.name_fields
# Iterate over the record set and retrieve on row at a time
puts res.fetch
while row = res.fetch do
printf "%s \n", row
end
# Release the result set.
res.free
# Disconnect from server
db.close
See lib/demo.rb and the MonetDBDatar class documentation for more examples.
== ActiveRecord connector adapter ==
Active Record connects business objects and database tables to create a persistable domain model where logic and data are presented in one wrapping. It‘s an implementation of the object-relational mapping (ORM) pattern.
Required files:
adapter/lib/active_record/monetdb_adapter.rb
Usage example follows:
require 'active_record'
ActiveRecord::Base.logger = Logger.new(STDERR)
ActiveRecord::Base.colorize_logging = true
ActiveRecord::Base.establish_connection(
:adapter => "monetdb",
:host => "localhost",
:database => "demo"
)
# Create a new table
class AddTests < ActiveRecord::Migration
def self.up
create_table :tests do |table|
table.column :name, :string
table.column :surname, :string
end
end
def self.down
drop_table :tests
end
end
AddTests.up
# Migration: add a column name with a default value
class AddAge < ActiveRecord::Migration
def self.up
add_column :tests, :age, :smallint, :default => 18
end
def self.down
remove_column :tests, :age
end
end
class Test < ActiveRecord::Base
end
# Insert an entry in the table
Test.create(:name => 'X', :surname => 'Y')
# add a column
AddAge.up
# return the first result of the query SELECT * from tables
row = Test.find(:first)
printf "SELECT * from tests LIMIT 1:\n"
printf "Name: %s, Surname: %s, Age: %s\n", row.name, row.surname, row.age
# Drop the table
AddTests.down
== Rubygem ==
The standalone ruby driver can be distributed as a ruby gem.
A gem file is already available; however, it can be generated
starting from the ruby-monetdb-sql-0.1.gemspec file:
$ gem build ruby-monetdb-sql-0.1.gemspec
To install the file run the command:
$ gem install ruby-monetdb-sql-0.1.gem
Documentation in ri and html format will be generated and installed as well
TODO 0 → 100644
* test and improve utf8 and type conversion
* documentation cleanup
* OSM on rails demo (slowed down due to third party plugins requirements)
# The contents of this file are subject to the MonetDB Public License
# Version 1.1 (the "License"); you may not use this file except in
# compliance with the License. You may obtain a copy of the License at
# http://monetdb.cwi.nl/Legal/MonetDBLicense-1.1.html
#
# Software distributed under the License is distributed on an "AS IS"
# basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the
# License for the specific language governing rights and limitations
# under the License.
#
# The Original Code is the MonetDB Database System.
#
# The Initial Developer of the Original Code is CWI.
# Portions created by CWI are Copyright (C) 1997-July 2008 CWI.
# Copyright August 2008-2011 MonetDB B.V.
# All Rights Reserved.
gem_adapter = {
FILES = activerecord-monetdb-adapter-0.1.gemspec
DIR = $(prefix)/$(RUBY_DIR)
}
EXTRA_DIST = activerecord-monetdb-adapter-0.1.gemspec
EXTRA_DIST_DIR = active_record lib
Gem::Specification.new do |s|
s.required_ruby_version = '>= 2.1.0'
s.name = %q{activerecord-monetdb-adapter}
s.version = "0.2"
s.date = %q{2009-05-18}
s.authors = ["G Modena"]
s.email = %q{gm@cwi.nl}
s.summary = %q{ActiveRecord Connector for MonetDB}
s.homepage = %q{http://monetdb.cwi.nl/}
s.description = %q{ActiveRecord Connector for MonetDB built on top of the pure Ruby database driver}
s.files = [ "lib/active_record/connection_adapters/monetdb_adapter.rb" ]
s.has_rdoc = true
s.require_path = 'lib'
s.add_dependency(%q<activerecord>, [">= 2.3.2"])
s.add_dependency(%q<ruby-monetdb-sql>, [">= 0.1"])
# placeholder project to avoid warning about not having a rubyforge_project
s.rubyforge_project = "nowarning"
end
# The contents of this file are subject to the MonetDB Public License
# Version 1.1 (the "License"); you may not use this file except in
# compliance with the License. You may obtain a copy of the License at
# http://monetdb.cwi.nl/Legal/MonetDBLicense-1.1.html
#
# Software distributed under the License is distributed on an "AS IS"
# basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the
# License for the specific language governing rights and limitations
# under the License.
#
# The Original Code is the MonetDB Database System.
#
# The Initial Developer of the Original Code is CWI.
# Portions created by CWI are Copyright (C) 1997-July 2008 CWI.
# Copyright August 2008-2011 MonetDB B.V.
# All Rights Reserved.
# MonetDB Active Record adapter
# monetdb_adapter.rb
# The code is an adaption of the adapter developer by Michalis Polakis (2008), to work on top of the pure ruby MonetDB
# interface
# Refreshed by Martin Samson (2011)
MDB_SYS_SCHEMA = "sys."
MDB_NON_SYSTEM_TABLES_ONLY = "and system = false"
require 'active_record/connection_adapters/abstract_adapter'
require 'MonetDB'
module ActiveRecord
class Base
# Establishes a connection to the database that's used by all Active Record objects
def self.monetdb_connection(config)
# extract connection parameters
config = config.symbolize_keys
host = config[:host] || "127.0.0.1"
port = config[:port] || 50000
username = config[:username].to_s if config[:username]
password = config[:password].to_s if config[:password]
# Use "sql" as default language if none is specified
lang = config[:lang] || "sql"
if config.key?(:database)
database = config[:database]
else
raise ArgumentError, "No database specified. Missing argument: database."
end
dbh = MonetDB.new
ConnectionAdapters::MonetDBAdapter.new(dbh, logger, [host, port, username, password, database, lang], config)
end
end
module ConnectionAdapters
class MonetDBColumn < Column
# Handles the case where column type is int but default
# column value is the next value of a sequence(string).
# By overriding this function, extract_default in
# schema_definitions does not return a fixnum(0 or 1) but
# the correct default value.
def type_cast(value)
if value.nil?
nil
elsif type == :integer && value =~/next value for/
nil
else
super
end
end
private
def simplified_type(field_type)
case field_type
when /int|smallint/i
:integer
when /real|double/i
:float
when /datetime/i
:timestamp
when /timestamp/i
:timestamp
when /char/i, /varchar/i
:string
when /bigint/i
:bigint
else
super
end
end
end #end of MonetDBColumn class
class TableDefinition
# Override so that we handle the fact that MonetDB
# doesn't support "limit" on integer column.
# Otherwise same implementation
def column(name, type, options = {})
column = self[name] || ColumnDefinition.new(@base, name, type)
if type.to_sym != :integer and type.to_sym != :primary_key
column.limit = options[:limit] || native[type.to_sym][:limit] if options[:limit] or native[type.to_sym]
end
column.precision = options[:precision]
column.scale = options[:scale]
column.default = options[:default]
column.null = options[:null]
@columns << column unless @columns.include? column
self
end
end
class MonetDBAdapter < AbstractAdapter
class BindSubstitution < Arel::Visitors::MySQL
include Arel::Visitors::BindVisitor
end
def initialize(connection, logger, connection_options, config)
super(connection, logger)
@visitor = BindSubstitution.new self
@connection_options, @config = connection_options, config
connect
end
def adapter_name #:nodoc:
'MonetDB'
end
# Functions like rename_table, rename_column and
# change_column cannot be implemented in MonetDB.
def supports_migrations?
true
end
# testing savepoints in progress
def supports_savepoints? #:nodoc:
true
end
def support_transaction? #:nodoc:
false
end
def supports_ddl_transactions?
false
end
def native_database_types
{
:primary_key => "int NOT NULL auto_increment PRIMARY KEY",
:string => {:name => "varchar", :limit => 255},
:text => {:name => "clob"},
:integer => {:name => "int"},
:float => {:name => "float"},
:decimal => {:name => "decimal"},
:datetime => {:name => "timestamp"},
:timestamp => {:name => "timestamp"},
:time => {:name => "time"},
:date => {:name => "date"},
:binary => {:name => "blob"},
:boolean => {:name => "boolean"},
:bigint => {:name => "bigint"}
}
end
#MonetDB does not support using DISTINCT withing COUNT
#by default
def supports_count_distinct?
false
end
#----------CONNECTION MANAGEMENT------------------
# Check if the connection is active
def active?
if @connection != nil
@connection.is_connected?
end
return false
end
# Close this connection and open a new one in its place.
def reconnect!
if @connection != nil
#@connection.reconnect
false
end
end
def disconnect!
#@connection.auto_commit(flag=true)
@connection.close
end
# -------END OF CONNECTION MANAGEMENT----------------
# ===============SCHEMA DEFINITIONS===========#
def binary_to_string(value)
res = ""
value.scan(/../).each { |i| res << i.hex.chr }
res
end
# ===========END OF SCHEMA DEFINITIONS========#
#===============SCHEMA STATEMENTS===========#
# The following schema_statements.rb functions are not supported by MonetDB (19/5/2008).
#
# -rename_table : not such functionality by MonetDB's API. Altering some
# administratives' tables values has no result.
#
# -rename_column : Could be possible if I make a new_name column copy the
# data from the old column there and then drop that column. But I would
# also have to take care of references and other constraints. Not sure if
# this is desired.
#
# -change_column : Alteration of a column's datatype is not supported.
# NOTE WE MAY BE ABLE TO "CHANGE" A COLUMN DEFINITION IF WE DROP THE COLUMN
# AND CREATE A NEW ONE WITH THE OLD NAME. THIS COULD WORK AS LONG AS WE DON'T
# LOSE ANY DATA.
# Sets a new default value for a column.
# ===== Examples =====
# change_column_default(:suppliers, :qualification, 'new')
# change_column_default(:accounts, :authorized, 1)
def change_column_default(table_name, column_name, default)
sql = "ALTER TABLE #{table_name} ALTER COLUMN #{quote_column_name(column_name)} SET DEFAULT" #{quote(default)}"
if (default.nil? || (default.casecmp("NULL")==0))
sql << " NULL"
else
sql << quote(default)
end
p "SQL: " + sql + '\n'
hdl = execute(sql)
end
def remove_index(table_name, options = {})
hdl = execute("DROP INDEX #{index_name(table_name, options)}")
end
# MonetDB does not support limits on certain data types
# Limit is supported for the {char, varchar, clob, blob, time, timestamp} data types.
def type_to_sql(type, limit = nil, precision = nil, scale = nil)
return super if limit.nil?
# strip off limits on data types not supporting them
if [:integer, :double, :date, :bigint].include? type
type.to_s
else
super
end
end
# Returns an array of Column objects for the table specified by +table_name+.
def columns(table_name, name = nil)
return [] if table_name.to_s.strip.empty?
table_name = table_name.to_s if table_name.is_a?(Symbol)
table_name = table_name.split('.')[-1] unless table_name.nil?
hdl = execute(" SELECT name, type, type_digits, type_scale, \"default\", \"null\" FROM #{MDB_SYS_SCHEMA}_columns WHERE table_id in (SELECT id FROM #{MDB_SYS_SCHEMA}_tables WHERE name = '#{table_name}' #{MDB_NON_SYSTEM_TABLES_ONLY})", name)
num_rows = hdl.num_rows
return [] unless num_rows >= 1
result = []
while row = hdl.fetch_hash do
col_name = row['name']
col_default = row['default']
# If there is no default value, it assigns NIL
col_default = nil if (col_default && col_default.upcase == 'NULL')
# Removes single quotes from the default value
col_default.gsub!(/^'(.*)'$/, '\1') unless col_default.nil?
# A string is returned so we must convert it to boolean
col_nullable = row['null']
if (col_nullable.casecmp("true") == 0)
col_nullable = true
elsif (col_nullable.casecmp("false") == 0)
col_nullable = false
end
col_type = row['type']
type_digits = row['type_digits']
type_scale = row['type_scale']
# Don't care about datatypes that aren't supported by
# ActiveRecord, like interval.
# Also do nothing for datatypes that don't support limit
# like integer, double, date, bigint
if (col_type == "clob" || col_type == "blob")
if (type_digits.to_i > 0)
col_type << "(#{type_digits})"
end
elsif (col_type == "char" ||
col_type == "varchar" ||
col_type == "time" ||
col_type == "timestamp"
)
col_type << "(#{type_digits})"
elsif (col_type == "decimal")
if (type_scale.to_i == 0)
col_type << "(#{type_digits})"
else
col_type << "(#{type_digits},#{type_scale})"
end
end
# instantiate a new column and insert into the result array
result << MonetDBColumn.new(col_name, col_default, col_type, col_nullable)
end
# check that free has been correctly performed
hdl.free
return result
end
def primary_key(table)
'id'
end
# Adds a new column to the named table.
# See TableDefinition#column for details of the options you can use.
def add_column(table_name, column_name, type, options = {})
if ((type.to_sym == :decimal) && (options[:precision].to_i+options[:scale].to_i > 18))
raise StandardError, "It is not possible to have a decimal column where Precision + Scale > 18 . The column will not be added to the table!"
return
else
super
end
end
# Return an array with all non-system table names of the current
# database schema
def tables(name = nil)
cur_schema = select_value("select current_schema", name)
select_values(" SELECT t.name FROM #{MDB_SYS_SCHEMA}_tables t, sys.schemas s
WHERE s.name = '#{cur_schema}'
AND t.schema_id = s.id
AND t.system = false", name)
end
# Returns an array of indexes for the given table.
def indexes(table_name, name = nil)
sql_query = " SELECT distinct i.name as index_name, k.\"name\", k.nr
FROM
#{MDB_SYS_SCHEMA}idxs i, #{MDB_SYS_SCHEMA}_tables t, #{MDB_SYS_SCHEMA}objects k
WHERE
i.type = 0 AND i.name not like '%pkey'
AND i.id = k.id AND t.id = i.table_id
AND t.name = '#{table_name.to_s}'
ORDER BY i.name, k.nr;"
result = select_all(sql_query, name);
cur_index = nil
indexes = []
result.each do |row|
if cur_index != row['index_name']
indexes << IndexDefinition.new(table_name, row['index_name'], false, [])
cur_index = row['index_name']
end
indexes.last.columns << row['name']
end
indexes
end
# ===========END OF SCHEMA STATEMENTS========#
# ===========QUOTING=========================#
def quote(value, column = nil)
if value.kind_of?(String) && column && column.type == :binary && column.class.respond_to?(:string_to_binary)
s = column.class.string_to_binary(value).unpack("H*")[0]
"BLOB '#{s}'"
else
super
end
end
def quote_column_name(name) #:nodoc:
"\"#{name.to_s}\""
end
def quote_table_name(name) #:nodoc:
quote_column_name(name).gsub('.', '"."')
end
# If the quoted true is 'true' MonetDB throws a string cast exception
def quoted_true
"true"
end
# If the quoted false is 'false' MonetDB throws a string cast exception
def quoted_false
"false"
end
# ===========END-OF-QUOTING==================#
# =========DATABASE=STATEMENTS===============#
# Returns an array of arrays containing the field values.
# Order of columns in tuple arrays is not guaranteed.
def select_rows(sql, name = nil)
result = select(sql, name)
result.map { |v| v.values }
end
def execute(sql, name = nil)
sql = sql.gsub('!=', '<>')
sql += ';'
@connection.query(sql)
end
def exec_query(sql, name = nil, binds = [])
select_rows(sql, name)
end
def last_inserted_id(result)
result.last_insert_id
end
def delete(arel, name = nil, binds = [])
res = super(arel, name, binds)
res.affected_rows
end
# Begins the transaction.
def begin_db_transaction
hdl = execute("START TRANSACTION")
end
# Commits the transaction (ends TRANSACTIOM).
def commit_db_transaction
hdl = execute("COMMIT")
end
# Rolls back the transaction. Must be
# done if the transaction block raises an exception or returns false (ends TRANSACTIOM).
def rollback_db_transaction
hdl = execute("ROLLBACK")
end
def current_savepoint_name
@connection.transactions || 0
end
# Create a new savepoint
def create_savepoint
@connection.save
execute("SAVEPOINT #{current_savepoint_name}")
end
# rollback to the last savepoint
def rollback_to_savepoint
execute("ROLLBACK TO SAVEPOINT #{current_savepoint_name}")
end
# release current savepoint
def release_savepoint
execute("RELEASE SAVEPOINT #{current_savepoint_name}")
end
def add_lock!(sql, options)
@logger.info "Warning: MonetDB :lock option '#{options[:lock].inspect}' not supported. Returning unmodified sql statement!" if @logger && options.has_key?(:lock)
sql
end
def empty_insert_statement(table_name)
# Ensures that the auto-generated id value will not violate the primary key constraint.
# comment out for production code(?)
#make_sure_pk_works(table_name, nil)
#"INSERT INTO #{quote_table_name(table_name)}"
end
#=======END=OF=DATABASE=STATEMENTS=========#
# Returns an array of record hashes with the column names as keys and
# column values as values.
def select(sql, name = nil, binds = [])
hdl = execute(sql, name)
hdl.result_hashes
end
# Executes the update statement and returns the number of rows affected.
def update_sql(sql, name = nil)
hdl = execute(sql, name)
hdl.affected_rows
end
# Returns the last auto-generated ID from the affected table.
def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil)
# Ensures that the auto-generated id value will not violate the
# primary key constraint. Read the comments of make_sure_pk_works
# and documentation for further information.
# comment out for production code(?)
# table_name = extract_table_name_from_insertion_query(sql)
# make_sure_pk_works(table_name,name)
hdl = execute(sql, name)
hdl.last_insert_id
end
protected
# Some tests insert some tuples with the id values set. In other words, the sequence
# is not used to generate a value for the primary key column named id. When a new tuple
# it to be inserted, where the id value is not set explicitly, a primary key violation will
# be raised because the generated from the sequence value is the same as one of the existing
# id values. This happens in unit tests quite often. So this function serves that the unit tests
# pass. However it is very expensive( sends 4 queries to the server) and probably not suitable for
# production code. Check the implementation for further info/details.
def make_sure_pk_works(table_name, name)
# Ensure the auto-generated id will not violate the primary key constraint.
# This is expensive and it's used so that the tests pass. Comment out for production code(?).
# Assume that table name has one primary key column named id that is associated with a sequence,
# otherwise return
hdl = nil
sequence_name = extract_sequence_name(select_value("select \"default\" from #{MDB_SYS_SCHEMA}_columns where table_id in (select id from #{MDB_SYS_SCHEMA}_tables where name = '#{table_name}') and name='id';"))
return if sequence_name.blank?
max_id = select_value("select max(id) from #{table_name}").to_i
next_seq_val = select_value("select next value for #{sequence_name}").to_i
if (max_id > next_seq_val)
hdl = execute("ALTER SEQUENCE #{sequence_name} RESTART WITH #{max_id+1}", name)
else
hdl = execute("ALTER SEQUENCE #{sequence_name} RESTART WITH #{next_seq_val+1}", name)
end
end
# Auxiliary function that extracts the table name from an insertion query
# It's called by insert_sql in order to assist at make_sure_pk_works.
# Ideally, if make_sure_pk_works is commented out for production code, this
# function will be never called.
def extract_table_name_from_insertion_query(sql)
$1 if sql =~ /INSERT INTO "(.*)" \(/
end
# Auxiliary function that extracts the sequence name.
# It's called by make_sure_pk_works.
# Ideally, if make_sure_pk_works is commented out for production code, this
# function will be never called.
def extract_sequence_name(seqStr)
$1 if seqStr =~ /\."(.*)"/
end
private
def connect
@connection.connect(user = @connection_options[2], passwd = @connection_options[3], lang = @connection_options[5], host = @connection_options[0], port = @connection_options[1], db_name = @connection_options[4], auth_type = "SHA1") if @connection
end
end
end
end
= MonetDB on rails =
This file documents the steps necessary to make rails working with MonetDB.
Database management in rails is performend via the ActiveRecord (ORM) adapter (http://api.rubyonrails.org/files/vendor/rails/activerecord/README.html).
The ruby/monetdb driver comes packaged as:
1) a standalone driver (only depends on ruby 1.8 and ruby 1.9);
2) and activerecord connector built on top of the standalone driver.
The following steps assumes that you already have obtained and installed a working copy of the rails framework and the 'rubygems' package manager.
For more informations about rubygems please refer to: http://rubyforge.org/projects/rubygems/
== Installation ==
On rpm base systems both the activerecord and standalone driver should be installed via the monetdb-clients package.
Please note that the building and installing process do not require root privileges.
=== Install the standalone driver ===
If you are using a source code version of monetdb cd into the driver directory located at './clients/src/ruby'
First build a gem file starting from the given gemspec:
$ gem build ruby-monetdb-sql-0.1.gemspec
and install the resulting gem with the command:
$ gem install --local ruby-monetdb-sql-0.1.gem
=== Install the activerecord connector ===
If you are using a source code version of monetdb cd into the activerecord adapter directory located at './clients/src/ruby/adapter'
First build a gem file starting from the given gemspec:
$ gem build activerecord-monetdb-adapter-0.1.gemspec
and install the resulting gem with the command:
$ gem install --local activerecord-monetdb-adapter-0.1.gem
== Configuration ==
Create a new rails application with the 'rails' command and cd into it:
$ rails myapp
$ cd myapp
This will create the usual Rails folder structure.
In order to start using rails with MonetDB as a backend we need to properly configure the databases by editing the file 'config/database.yml'
This is a sample configuration used to connect to a database named 'test':
adapter: monetdb
username: monetdb
password: monetdb
hostname: localhost
port: 50000
database: test
Where adapter is the name of the activerecord connector you want to use (in this case 'monetdb').
The driver works either by directly connecting to a mserver5 instance or through merovingian. Make sure to create the database ('test') before proceeding.
Once the database.yml file as been properly configured proceed by creating the Controller files either manually or by using the scaffolding script:
$ ./script/generate scaffold
Operations on the database are performed in the fashion of rake tasks; once the Controllers have been properly created and customized, schemas can be generated with the task 'migrate':
$ rake db:migrate
The current schema version number can be obtained with the task 'version':
$ rake db:version
It is possible to revert to previous schemas definitions via the 'rollback' task:
$ rake db:rollback
In the same way it is possible to drop all generated schemas with the 'drop' task:
$ rake db:drop:all
== References ==
What follows is a list of tutorials about getting started with rails.
1) http://www.akitaonrails.com/2007/12/12/rolling-with-rails-2-0-the-first-full-tutorial
2) http://oreilly.com/ruby/archive/rails.html
3) http://developer.apple.com/tools/rubyonrails.html
# The contents of this file are subject to the MonetDB Public License
# Version 1.1 (the "License"); you may not use this file except in
# compliance with the License. You may obtain a copy of the License at
# http://monetdb.cwi.nl/Legal/MonetDBLicense-1.1.html
#
# Software distributed under the License is distributed on an "AS IS"
# basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the
# License for the specific language governing rights and limitations
# under the License.
#
# The Original Code is the MonetDB Database System.
#
# The Initial Developer of the Original Code is CWI.
# Portions created by CWI are Copyright (C) 1997-July 2008 CWI.
# Copyright August 2008-2011 MonetDB B.V.
# All Rights Reserved.
require 'rubygems'
require 'active_record'
ActiveRecord::Base.logger = Logger.new(STDERR)
ActiveRecord::Base.colorize_logging = true
ActiveRecord::Base.establish_connection(
:adapter => "monetdb",
:host => "localhost",
:database => "test",
:username => "monetdb",
:password => "monetdb",
:hostname => "localhost",
:port => 50000
)
# Create a new table
class AddTests < ActiveRecord::Migration
def self.up
create_table :tests do |table|
table.column :name, :string
table.column :surname, :string
end
end
def self.down
drop_table :tests
end
end
AddTests.up
# Migration: add a column name with a default value
class AddAge < ActiveRecord::Migration
def self.up
add_column :tests, :age, :smallint, :default => 18
end
def self.down
remove_column :tests, :age
end
end
class Test < ActiveRecord::Base
end
# Insert an entry in the table
Test.create(:name => 'X', :surname => 'Y')
# add a column
AddAge.up
# return the first result of the query SELECT * from tables
row = Test.find(:first)
printf "SELECT * from tests LIMIT 1:\n"
printf "Name: %s, Surname: %s, Age: %s\n", row.name, row.surname, row.age
# revert the added column
AddAge.down
# Drop the table
AddTests.down
# The contents of this file are subject to the MonetDB Public License
# Version 1.1 (the "License"); you may not use this file except in
# compliance with the License. You may obtain a copy of the License at
# http://monetdb.cwi.nl/Legal/MonetDBLicense-1.1.html
#
# Software distributed under the License is distributed on an "AS IS"
# basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the
# License for the specific language governing rights and limitations
# under the License.
#
# The Original Code is the MonetDB Database System.
#
# The Initial Developer of the Original Code is CWI.
# Portions created by CWI are Copyright (C) 1997-July 2008 CWI.
# Copyright August 2008-2011 MonetDB B.V.
# All Rights Reserved.
require 'rubygems'
require 'MonetDB'
db = MonetDB.new
db.connect(user = "monetdb", passwd = "monetdb", lang = "sql", host="127.0.0.1", port = 50000, db_name = "test", auth_type = "SHA1")
# set type_cast=true to enable MonetDB to Ruby type mapping
res = db.query("select * from tables");
puts "Number of rows returned: " + res.num_rows.to_s
puts "Number of fields: " + res.num_fields.to_s
# Get the columns' name
#col_names = res.name_fields
# Get the columns' type
#col_types = res.type_fields
###### Fetch all rows and store them
#puts res.fetch_all
# Iterate over the record set and retrieve on row at a time
while row = res.fetch do
printf "%s \n", row
end
###### Get all records and hash them by column name
#row = res.fetch_all_hash()
#puts col_names[0] + "\t\t" + col_names[1]
#0.upto(res.num_rows) { |i|
# puts row['id'][i] + "\t\t" + row['name'][i]
#}
###### Iterator over columns (on cell at a time), convert the "id" field to a ruby integer value.
#while row = res.fetch_hash do
# printf "%s, %i\n", row["name"], row["id"].getInt
#end
###### Transactions
db.query("START TRANSACTION")
db.auto_commit(false)
# create a savepoint
db.save
db.query("SAVEPOINT #{db.transactions} ;")
# Modify the database
db.query('CREATE TABLE test (col1 INT, col2 INT)')
# Rollback to previous savepoint, discard changes
db.query("ROLLBACK TO SAVEPOINT #{db.transactions}")
# Release the save point
db.release
# Switch to auto commit mode
db.auto_commit(true)
# Deallocate memory used for storing the record set
res.free
db.close
# The contents of this file are subject to the MonetDB Public License
# Version 1.1 (the "License"); you may not use this file except in
# compliance with the License. You may obtain a copy of the License at
# http://monetdb.cwi.nl/Legal/MonetDBLicense-1.1.html
#
# Software distributed under the License is distributed on an "AS IS"
# basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the
# License for the specific language governing rights and limitations
# under the License.
#
# The Original Code is the MonetDB Database System.
#
# The Initial Developer of the Original Code is CWI.
# Portions created by CWI are Copyright (C) 1997-July 2008 CWI.
# Copyright August 2008-2011 MonetDB B.V.
# All Rights Reserved.
# A typical sequence of events is as follows:
# Fire a query using the database handle to send the statement to the server and get back a result set object.
# A result set object has methods for fetching rows, moving around in the result set, obtaining column metadata, and releasing the result set.
# Use a row fetching method such as 'fetch_row' or an iterator such as each to access the rows of the result set.
# Call 'free' to release the result set.
#module MonetDB
require 'MonetDBConnection'
require 'MonetDBData'
require 'MonetDBExceptions'
# = Introduction
#
# A typical sequence of events is as follows:
# Create a database instance (handle), invoke query using the database handle to send the statement to the server and get back a result set object.
#
# A result set object has methods for fetching rows, moving around in the result set, obtaining column metadata, and releasing the result set.
# A result set object is an instance of the MonetDBData class.
#
# Records can be returneds as arrays and iterators over the set.
#
# A database handler (dbh) is and instance of the MonetDB class.
#
# = Connection management
#
# connect - establish a new connection
# * user: username (default is monetdb)
# * passwd: password (default is monetdb)
# * lang: language (default is sql)
# * host: server hostanme or ip (default is localhost)
# * port: server port (default is 50000)
# * db_name: name of the database to connect to
# * auth_type: hashing function to use during authentication (default is SHA1)
#
# is_connected? - returns true if there is an active connection to a server, false otherwise
# reconnect - recconnect to a server
# close - terminate a connection
# auto_commit? - returns ture if the session is running in auto commit mode, false otherwise
# auto_commit - enable/disable auto commit mode.
#
# query - fire a query
#
# Currently MAPI protocols 8 and 9 are supported.
#
# = Managing record sets
#
#
# A record set is represented as an instance of the MonetDBData class; the class provides methods to manage retrieved data.
#
#
# The following methods allow to iterate over data:
#
# fetch - iterates over the record set and retrieves on row at a time. Each row is returned as an array.
# fetch_hash - iterates over columns (on cell at a time).
# fetch_all_hash - returns record set entries hashed by column name orderd by column position.
#
# To return the record set as an array (with each tuple stored as array of fields) the following method can be used:
#
# fetch_all - fetch all rows and store them
#
#
# Information about the retrieved record set can be obtained via the following methods:
#
# num_rows - returns the number of rows present in the record set
# num_fields - returns the number of fields (columns) that compose the schema
# name_fields - returns the (ordered) name of the schema's columns
# type_fields - returns the (ordered) types list of the schema's columns
#
# To release a record set MonetDBData#free can be used.
#
# = Type conversion
#
# A mapping between SQL and ruby type is supported. Each retrieved field can be converted to a ruby datatype via
# a getTYPE method.
#
# The currently supported cast methods are:
#
# getInt - convert to an integer value
# getFloat - convert to a floating point value
# getString - return a string representation of the value, with trailing and leading " characters removed
# getBlob - convert an SQL stored HEX string to its binary representation
# getTime - return a string representation of a TIME field
# getDate - return a string representation of a DATE field
# getDateTime - convert a TIMESTAMP field to a ruby Time object
# getChar - on ruby >= 1.9, convert a CHAR field to char
# getBool - convert a BOOLEAN field to a ruby bool object. If the value of the field is unknown, nil is returned
# getNull - convert a NULL value to a nil object
#
#
# = Transactions
#
# By default monetdb works in auto_commit mode. To turn this feature off MonetDB#auto_commit(flag=false) can be used.
#
# Once auto_commit has been disable it is possible to start transactions, create/delete savepoints, rollback and commit with
# the usual SQL statements.
#
# Savepoints IDs can be generated using the MonetDB#save method. To release a savepoint ID use MonetDB#release.
#
# Savepoints can be accessed (as a stack) with the MonetDB#transactions method.
#
# example/standalone.rb contains usage example of the above mentioned methods.
# = Introduction
#
# A typical sequence of events is as follows:
# Create a database instance (handle), invoke query using the database handle to send the statement to the server and get back a result set object.
#
# A result set object has methods for fetching rows, moving around in the result set, obtaining column metadata, and releasing the result set.
# A result set object is an instance of the MonetDBData class.
#
# Records can be returneds as arrays and iterators over the set.
#
# A database handler (dbh) is and instance of the MonetDB class.
#
# = Connection management
#
# connect - establish a new connection
# * user: username (default is monetdb)
# * passwd: password (default is monetdb)
# * lang: language (default is sql)
# * host: server hostanme or ip (default is localhost)
# * port: server port (default is 50000)
# * db_name: name of the database to connect to
# * auth_type: hashing function to use during authentication (default is SHA1)
#
# is_connected? - returns true if there is an active connection to a server, false otherwise
# reconnect - recconnect to a server
# close - terminate a connection
# auto_commit? - returns ture if the session is running in auto commit mode, false otherwise
# auto_commit - enable/disable auto commit mode.
#
# query - fire a query
#
# Currently MAPI protocols 8 and 9 are supported.
#
# = Managing record sets
#
#
# A record set is represented as an instance of the MonetDBData class; the class provides methods to manage retrieved data.
#
#
# The following methods allow to iterate over data:
#
# fetch - iterates over the record set and retrieves on row at a time. Each row is returned as an array.
# fetch_hash - iterates over columns (on cell at a time).
# fetch_all_hash - returns record set entries hashed by column name orderd by column position.
#
# To return the record set as an array (with each tuple stored as array of fields) the following method can be used:
#
# fetch_all - fetch all rows and store them
#
#
# Information about the retrieved record set can be obtained via the following methods:
#
# num_rows - returns the number of rows present in the record set
# num_fields - returns the number of fields (columns) that compose the schema
# name_fields - returns the (ordered) name of the schema's columns
#
# To release a record set MonetDBData#free can be used.
#
# = Type conversion
#
# Invoking MonetDB#query with the flag type_conversion=true will result in a type cast of the record set fields from SQL types to ruby types
#
# demo.rb contains usage example of the above mentioned methods.
class MonetDB
DEFAULT_USERNAME = "monetdb"
DEFAULT_PASSWORD = "monetdb"
DEFAULT_LANG = LANG_SQL
DEFAULT_HOST = "127.0.0.1"
DEFAULT_PORT = 50000
DEFAULT_DATABASE = "test"
DEFAULT_AUTHTYPE = "SHA1"
def initalize()
@connection = nil
end
# Establish a new connection.
# * username: username (default is monetdb)
# * password: password (default is monetdb)
# * lang: language (default is sql)
# * host: server hostanme or ip (default is localhost)
# * port: server port (default is 50000)
# * db_name: name of the database to connect to
# * auth_type: hashing function to use during authentication (default is SHA1)
def connect(username=DEFAULT_USERNAME, password=DEFAULT_PASSWORD, lang=DEFAULT_LANG, host=DEFAULT_HOST, port=DEFAULT_PORT, db_name=DEFAULT_DATABASE, auth_type=DEFAULT_AUTHTYPE)
# TODO: handle pools of connections
@username = username
@password = password
@lang = lang
@host = host
@port = port
@db_name = db_name
@auth_type = auth_type
@connection = MonetDBConnection.new(user = @username, passwd = @password, lang = @lang, host = @host, port = @port)
@connection.connect(@db_name, @auth_type)
end
# Establish a new connection using named parameters.
# * user: username (default is monetdb)
# * passwd: password (default is monetdb)
# * language: lang (default is sql)
# * host: host to connect to (default is localhost)
# * port: port to connect to (default is 50000)
# * database: name of the database to connect to
# * auth_type: hashing function to use during authentication (default is SHA1)
#
# Conventionally named parameters are passed as an hash.
#
# Ruby 1.8:
# MonetDB::conn({ :user => "username", :passwd => "password", :database => "database"})
#
# Ruby 1.9:
# MonetDB::conn(user: "username", passwd: "password", database: "database")
def conn(options)
user = options[:user] || DEFAULT_USERNAME
passwd = options[:passwd] || DEFAULT_PASSWORD
language = options[:language] || DEFAULT_LANG
host = options[:host] || DEFAULT_HOST
port = options[:port] || DEFAULT_PORT
database = options[:database] || DEFAULT_DATABASE
auth_type = options[:auth_type] || DEFAULT_AUTHTYPE
connect(user, passwd, language, host, port, database, auth_type)
end
# Send a <b> user submitted </b> query to the server and store the response.
# Returns and instance of MonetDBData.
def query(q="")
if @connection != nil
@data = MonetDBData.new(@connection)
@data.execute(q)
end
return @data
end
# Return true if there exists a "connection" object
def is_connected?
if @connection == nil
return false
else
return true
end
end
# Reconnect to the server
def reconnect
if @connection != nil
self.close
@connection = MonetDBConnection.new(user = @username, passwd = @password, lang = @lang, host = @host, port = @port)
@connection.connect(db_name = @db_name, auth_type = @auth_type)
end
end
# Turn auto commit on/off
def auto_commit(flag=true)
@connection.set_auto_commit(flag)
end
# Returns the current auto commit (on/off) settings.
def auto_commit?
@connection.auto_commit?
end
# Returns the name of the last savepoint in a transactions pool
def transactions
@connection.savepoint
end
# Create a new savepoint ID
def save
@connection.transactions.save
end
# Release a savepoint ID
def release
@connection.transactions.release
end
# Close an active connection
def close()
@connection.disconnect
@connection = nil
end
end
#end
# The contents of this file are subject to the MonetDB Public License
# Version 1.1 (the "License"); you may not use this file except in
# compliance with the License. You may obtain a copy of the License at
# http://monetdb.cwi.nl/Legal/MonetDBLicense-1.1.html
#
# Software distributed under the License is distributed on an "AS IS"
# basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the
# License for the specific language governing rights and limitations
# under the License.
#
# The Original Code is the MonetDB Database System.
#
# The Initial Developer of the Original Code is CWI.
# Portions created by CWI are Copyright (C) 1997-July 2008 CWI.
# Copyright August 2008-2011 MonetDB B.V.
# All Rights Reserved.
# Implements the MAPI communication protocol
require 'socket'
require 'time'
require 'hasher'
require 'MonetDBExceptions'
require 'uri' # parse merovingian redirects
Q_TABLE = "1" # SELECT operation
Q_UPDATE = "2" # INSERT/UPDATE operations
Q_CREATE = "3" # CREATE/DROP TABLE operations
Q_TRANSACTION = "4" # TRANSACTION
Q_PREPARE = "5"
Q_BLOCK = "6" # QBLOCK message
MSG_REDIRECT = '^' # auth redirection through merovingian
MSG_QUERY = '&'
MSG_SCHEMA_HEADER = '%'
MSG_INFO = '!' # info response from mserver
MSG_TUPLE = '['
MSG_PROMPT = ""
REPLY_SIZE = '-1'
MAX_AUTH_ITERATION = 10 # maximum number of atuh iterations (thorough merovingian) allowed
MONET_ERROR = -1
LANG_SQL = "sql"
# Protocols
MAPIv8 = 8
MAPIv9 = 9
MONETDB_MEROVINGIAN = "merovingian"
MONETDB_MSERVER = "monetdb"
MEROVINGIAN_MAX_ITERATIONS = 10
class MonetDBConnection
# enable debug output
@@DEBUG = false
# hour in seconds, used for timezone calculation
@@HOUR = 3600
# maximum size (in bytes) for a monetdb message to be sent
@@MAX_MESSAGE_SIZE = 2048
# endianness of a message sent to the server
@@CLIENT_ENDIANNESS = "BIG"
# MAPI protocols supported by the driver
@@SUPPORTED_PROTOCOLS = [MAPIv8, MAPIv9]
attr_reader :socket, :auto_commit, :transactions, :lang
# Instantiates a new MonetDBConnection object
# * user: username (default is monetdb)
# * passwd: password (default is monetdb)
# * lang: language (default is sql)
# * host: server hostanme or ip (default is localhost)
# * port: server port (default is 50000)
def initialize(user = "monetdb", passwd = "monetdb", lang = "sql", host="127.0.0.1", port = "50000")
@user = user
@passwd = passwd
@lang = lang.downcase
@host = host
@port = port
@client_endianness = @@CLIENT_ENDIANNESS
@auth_iteration = 0
@connection_established = false
@transactions = MonetDBTransaction.new # handles a pool of transactions (generates and keeps track of savepoints)
if @@DEBUG == true
require 'logger'
end
if @lang[0, 3] == 'sql'
@lang = "sql"
end
end
# Connect to the database, creates a new socket
def connect(db_name = 'demo', auth_type = 'SHA1')
@database = db_name
@auth_type = auth_type
@socket = TCPSocket.new(@host, @port.to_i)
if real_connect
if @lang == LANG_SQL
set_timezone
set_reply_size
end
true
end
false
end
# perform a real connection; retrieve challenge, proxy through merovinginan, build challenge and set the timezone
def real_connect
server_challenge = retrieve_server_challenge()
if server_challenge != nil
salt = server_challenge.split(':')[0]
@server_name = server_challenge.split(':')[1]
@protocol = server_challenge.split(':')[2].to_i
@supported_auth_types = server_challenge.split(':')[3].split(',')
@server_endianness = server_challenge.split(':')[4]
if @protocol == MAPIv9
@pwhash = server_challenge.split(':')[5]
end
else
raise MonetDBConnectionError, "Error: server returned an empty challenge string."
end
# The server supports only RIPMED168 or crypt as an authentication hash function, but the driver does not.
if @supported_auth_types.length == 1
auth = @supported_auth_types[0]
if auth.upcase == "RIPEMD160" or auth.upcase == "CRYPT"
raise MonetDBConnectionError, auth.upcase + " " + ": algorithm not supported by ruby-monetdb."
end
end
# If the server protocol version is not 8: abort and notify the user.
if @@SUPPORTED_PROTOCOLS.include?(@protocol) == false
raise MonetDBProtocolError, "Protocol not supported. The current implementation of ruby-monetdb works with MAPI protocols #{@@SUPPORTED_PROTOCOLS} only."
elsif mapi_proto_v8?
reply = build_auth_string_v8(@auth_type, salt, @database)
elsif mapi_proto_v9?
reply = build_auth_string_v9(@auth_type, salt, @database)
end
if @socket != nil
@connection_established = true
send(reply)
monetdb_auth = receive
if monetdb_auth.length == 0
# auth succedeed
true
else
if monetdb_auth[0].chr == MSG_REDIRECT
#redirection
redirects = [] # store a list of possible redirects
monetdb_auth.split('\n').each do |m|
# strip the trailing ^mapi:
# if the redirect string start with something != "^mapi:" or is empty, the redirect is invalid and shall not be included.
if m[0..5] == "^mapi:"
redir = m[6..m.length]
# url parse redir
redirects.push(redir)
else
$stderr.print "Warning: Invalid Redirect #{m}"
end
end
if redirects.size == 0
raise MonetDBConnectionError, "No valid redirect received"
else
begin
uri = URI.split(redirects[0])
# Splits the string on following parts and returns array with result:
#
# * Scheme
# * Userinfo
# * Host
# * Port
# * Registry
# * Path
# * Opaque
# * Query
# * Fragment
server_name = uri[0]
host = uri[2]
port = uri[3]
database = uri[5].gsub(/^\//, '') if uri[5] != nil
rescue URI::InvalidURIError
raise MonetDBConnectionError, "Invalid redirect: #{redirects[0]}"
end
end
if server_name == MONETDB_MEROVINGIAN
if @auth_iteration <= MEROVINGIAN_MAX_ITERATIONS
@auth_iteration += 1
real_connect
else
raise MonetDBConnectionError, "Merovingian: too many iterations while proxying."
end
elsif server_name == MONETDB_MSERVER
begin
@socket.close
rescue
raise MonetDBConnectionError, "I/O error while closing connection to #{@socket}"
end
# reinitialize a connection
@host = host
@port = port
connect(database, @auth_type)
else
@connection_established = false
raise MonetDBConnectionError, monetdb_auth
end
elsif monetdb_auth[0].chr == MSG_INFO
raise MonetDBConnectionError, monetdb_auth
end
end
end
end
def savepoint
@transactions.savepoint
end
# Formats a <i>command</i> string so that it can be parsed by the server
def format_command(x)
return "X" + x + "\n"
end
# send an 'export' command to the server
def set_export(id, idx, offset)
send(format_command("export " + id.to_s + " " + idx.to_s + " " + offset.to_s))
end
# send a 'reply_size' command to the server
def set_reply_size
send(format_command(("reply_size " + REPLY_SIZE)))
response = receive
if response == MSG_PROMPT
true
elsif response[0] == MSG_INFO
raise MonetDBCommandError, "Unable to set reply_size: #{response}"
end
end
def set_output_seq
send(format_command("output seq"))
end
# Disconnect from server
def disconnect()
if @connection_established
begin
@socket.close
rescue => e
$stderr.print e
end
else
raise MonetDBConnectionError, "No connection established."
end
end
# send data to a monetdb5 server instance and returns server's response
def send(data)
encode_message(data).each do |m|
@socket.write(m)
end
end
# receive data from a monetdb5 server instance
def receive
is_final, chunk_size = recv_decode_hdr
if chunk_size == 0
return "" # needed on ruby-1.8.6 linux/64bit; recv(0) hangs on this configuration.
end
data = @socket.recv(chunk_size)
if is_final == false
while is_final == false
is_final, chunk_size = recv_decode_hdr
data += @socket.recv(chunk_size)
end
end
return data
end
# Builds and authentication string given the parameters submitted by the user (MAPI protocol v8).
#
def build_auth_string_v8(auth_type, salt, db_name)
# seed = password + salt
if (auth_type.upcase == "MD5" or auth_type.upcase == "SHA1") and @supported_auth_types.include?(auth_type.upcase)
auth_type = auth_type.upcase
digest = Hasher.new(auth_type, @passwd+salt)
hashsum = digest.hashsum
elsif auth_type.downcase == "plain" or not @supported_auth_types.include?(auth_type.upcase)
auth_type = 'plain'
hashsum = @passwd + salt
elsif auth_type.downcase == "crypt"
auth_type = @supported_auth_types[@supported_auth_types.index(auth_type)+1]
$stderr.print "The selected hashing algorithm is not supported by the Ruby driver. #{auth_type} will be used instead."
digest = Hasher.new(auth_type, @passwd+salt)
hashsum = digest.hashsum
else
# The user selected an auth type not supported by the server.
raise MonetDBConnectionError, "#{auth_type} not supported by the server. Please choose one from #{@supported_auth_types}"
end
# Build the reply message with header
reply = @client_endianness + ":" + @user + ":{" + auth_type + "}" + hashsum + ":" + @lang + ":" + db_name + ":"
end
#
# Builds and authentication string given the parameters submitted by the user (MAPI protocol v9).
#
def build_auth_string_v9(auth_type, salt, db_name)
if (auth_type.upcase == "MD5" or auth_type.upcase == "SHA1") and @supported_auth_types.include?(auth_type.upcase)
auth_type = auth_type.upcase
# Hash the password
pwhash = Hasher.new(@pwhash, @passwd)
digest = Hasher.new(auth_type, pwhash.hashsum + salt)
hashsum = digest.hashsum
elsif auth_type.downcase == "plain" # or not @supported_auth_types.include?(auth_type.upcase)
# Keep it for compatibility with merovingian
auth_type = 'plain'
hashsum = @passwd + salt
elsif @supported_auth_types.include?(auth_type.upcase)
if auth_type.upcase == "RIPEMD160"
auth_type = @supported_auth_types[@supported_auth_types.index(auth_type)+1]
$stderr.print "The selected hashing algorithm is not supported by the Ruby driver. #{auth_type} will be used instead."
end
# Hash the password
pwhash = Hasher.new(@pwhash, @passwd)
digest = Hasher.new(auth_type, pwhash.hashsum + salt)
hashsum = digest.hashsum
else
# The user selected an auth type not supported by the server.
raise MonetDBConnectionError, "#{auth_type} not supported by the server. Please choose one from #{@supported_auth_types}"
end
# Build the reply message with header
reply = @client_endianness + ":" + @user + ":{" + auth_type + "}" + hashsum + ":" + @lang + ":" + db_name + ":"
end
# builds a message to be sent to the server
def encode_message(msg = "")
message = Array.new
data = ""
hdr = 0 # package header
pos = 0
is_final = false # last package in the stream
while (!is_final)
data = msg[pos..pos+[@@MAX_MESSAGE_SIZE.to_i, (msg.length - pos).to_i].min]
pos += data.length
if (msg.length - pos) == 0
last_bit = 1
is_final = true
else
last_bit = 0
end
hdr = [(data.length << 1) | last_bit].pack('v')
message << hdr + data.to_s # Short Little Endian Encoding
end
message.freeze # freeze and return the encode message
end
# Used as the first step in the authentication phase; retrives a challenge string from the server.
def retrieve_server_challenge()
server_challenge = receive
end
# reads and decodes the header of a server message
def recv_decode_hdr()
if @socket != nil
fb = @socket.recv(1)
sb = @socket.recv(1)
# Use execeptions handling to keep compatibility between different ruby
# versions.
#
# Chars are treated differently in ruby 1.8 and 1.9
# try do to ascii to int conversion using ord (ruby 1.9)
# and if it fail fallback to character.to_i (ruby 1.8)
begin
fb = fb[0].ord
sb = sb[0].ord
rescue NoMethodError => one_eight
fb = fb[0].to_i
sb = sb[0].to_i
end
chunk_size = (sb << 7) | (fb >> 1)
is_final = false
if ((fb & 1) == 1)
is_final = true
end
# return the size of the chunk (in bytes)
return is_final, chunk_size
else
raise MonetDBSocketError, "Error while receiving data\n"
end
end
# Sets the time zone according to the Operating System settings
def set_timezone()
tz = Time.new
tz_offset = "%+03d:00" % (tz.gmt_offset / @@HOUR)
query_tz = "sSET TIME ZONE INTERVAL '#{tz_offset}' HOUR TO MINUTE;"
# Perform the query directly within the method
send(query_tz)
response = receive()
if response == MSG_PROMPT
true
elsif response[0].chr == MSG_INFO
raise MonetDBQueryError, response
end
end
# Turns auto commit on/off
def set_auto_commit(flag=true)
if flag == false
ac = " 0"
else
ac = " 1"
end
send(format_command("auto_commit " + ac))
response = receive
if response == MSG_PROMPT
@auto_commit = flag
elsif response[0].chr == MSG_INFO
raise MonetDBCommandError, response
return
end
end
# Check the auto commit status (on/off)
def auto_commit?
@auto_commit
end
# Check if monetdb is running behind the merovingian proxy and forward the connection in case
def merovingian?
if @server_name.downcase == MONETDB_MEROVINGIAN
true
else
false
end
end
def mserver?
if @server_name.downcase == MONETDB_MSERVER
true
else
false
end
end
# Check which protocol is spoken by the server
def mapi_proto_v8?
if @protocol == MAPIv8
true
else
false
end
end
def mapi_proto_v9?
if @protocol == MAPIv9
true
else
false
end
end
end
# handles transactions and savepoints. Can be used to simulate nested transactions.
class MonetDBTransaction
SAVEPOINT_STRING = "monetdbsp"
def initialize
@id = 0
@savepoint = ""
end
def savepoint
@savepoint = SAVEPOINT_STRING + @id.to_s
end
def release
prev_id
end
def save
next_id
end
private
def next_id
@id += 1
end
def prev_id
@id -= 1
end
end
# The contents of this file are subject to the MonetDB Public License
# Version 1.1 (the "License"); you may not use this file except in
# compliance with the License. You may obtain a copy of the License at
# http://monetdb.cwi.nl/Legal/MonetDBLicense-1.1.html
#
# Software distributed under the License is distributed on an "AS IS"
# basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the
# License for the specific language governing rights and limitations
# under the License.
#
# The Original Code is the MonetDB Database System.
#
# The Initial Developer of the Original Code is CWI.
# Portions created by CWI are Copyright (C) 1997-July 2008 CWI.
# Copyright August 2008-2011 MonetDB B.V.
# All Rights Reserved.
# Models a MonetDB RecordSet
require 'time'
require 'ostruct'
require "bigdecimal"
require 'MonetDBConnection'
require 'logger'
class MonetDBData
@@DEBUG = false
attr_accessor :last_insert_id, :affected_rows
def initialize(connection)
@connection = connection
@lang = @connection.lang
# Structure containing the header+results set for a fired Q_TABLE query
@header = []
@query = {}
@record_set = []
@index = 0 # Position of the last returned record
@row_count = 0
@row_offset = 10
@row_index = Integer(REPLY_SIZE)
end
# Fire a query and return the server response
def execute(q)
# fire a query and get ready to receive the data
@connection.send(format_query(q))
data = @connection.receive
return if data == nil
# temporarly store retrieved rows
record_set = receive_record_set(data)
if (@lang == LANG_SQL)
# the fired query is a SELECT; store and return the whole record set
if @action == Q_TABLE
@header = parse_header_table(@header)
@header.freeze
if @row_index.to_i < @row_count.to_i
block_rows = ""
while next_block
data = @connection.receive
block_rows += receive_record_set(data)
end
record_set += block_rows
end
end
# ruby string management seems to not properly understand the MSG_PROMPT escape character.
# In order to avoid data loss the @record_set array is built once that all tuples have been retrieved
@record_set = record_set.split("\t]\n")
if @record_set.length != @query['rows'].to_i
raise MonetDBQueryError, "Warning: Query #{@query['id']} declared to result in #{@query['rows']} but #{@record_set.length} returned instead"
end
end
@record_set.freeze
end
# Free memory used to store the record set
def free()
@connection = nil
@header = []
@query = {}
@record_set = []
@index = 0 # Position of the last returned record
@row_index = Integer(REPLY_SIZE)
@row_count = 0
@row_offset = 10
end
# Returns the record set entries hashed by column name orderd by column position
def fetch_all_hash()
columns = {}
@header["columns_name"].each do |col_name|
columns[col_name] = fetch_column_name(col_name)
end
return columns
end
def fetch_hash()
if @index >= @query['rows'].to_i
return false
else
columns = {}
@header["columns_name"].each do |col_name|
position = @header["columns_order"].fetch(col_name)
row = parse_tuple(@record_set[@index])
columns[col_name] = row[position]
end
@index += 1
return columns
end
end
# Returns the values for the column 'field'
def fetch_column_name(field="")
position = @header["columns_order"].fetch(field)
col = Array.new
# Scan the record set by row
@record_set.each do |row|
col << parse_tuple(row)[position]
end
return col
end
# returns result as an array of hashes
def result_hashes
result = []
@record_set.each do |row|
rec = parse_tuple(row)
hash = {}
@header['columns_name'].each_with_index do |item, i|
hash[item] = rec[i]
hash[item] = nil if hash[item] == 'NULL'
end
result << hash
end
result
end
def fetch()
@index
if @index > @query['rows'].to_i
false
else
parse_tuple(@record_set[@index])
@index += 1
end
end
# Cursor method that retrieves all the records present in a table and stores them in a cache.
def fetch_all()
if @query['type'] == Q_TABLE
rows = Array.new
@record_set.each do |row|
rows << parse_tuple(row)
end
@index = Integer(rows.length)
else
raise MonetDBDataError, "There is no record set currently available"
end
return rows
end
# Returns the number of rows in the record set
def num_rows()
return @query['rows'].to_i
end
# Returns the number of fields in the record set
def num_fields()
return @query['columns'].to_i
end
# Returns the (ordered) name of the columns in the record set
def name_fields()
return @header['columns_name']
end
# Returns the (ordered) name of the columns in the record set
def type_fields
return @header['columns_type']
end
private
# store block of data, parse it and store it.
def receive_record_set(response)
rows = ""
lines = response.lines.to_a
response.each_line do |row|
if row[0].chr == MSG_QUERY
if row[1].chr == Q_TABLE
@action = Q_TABLE
@query = parse_header_query(row)
@query.freeze
@row_count = @query['rows'].to_i #total number of rows in table
elsif row[1].chr == Q_BLOCK
# strip the block header from data
@action = Q_BLOCK
@block = parse_header_query(row)
elsif row[1].chr == Q_TRANSACTION
@action = Q_TRANSACTION
elsif row[1].chr == Q_CREATE
@action = Q_CREATE
elsif row[1].chr == Q_UPDATE
@action = Q_UPDATE
result = row.split(' ')
@affected_rows = result[1].to_i
@last_insert_id = result[2].to_i
end
elsif row[0].chr == MSG_INFO
raise MonetDBQueryError, row
elsif row[0].chr == MSG_SCHEMA_HEADER
# process header data
@header << row
elsif row[0].chr == MSG_TUPLE
if REPLY_SIZE.to_i == -1
# if all results are returned in this response, we don't have to look ahead further
return lines.join
end
rows += row
elsif row[0] == MSG_PROMPT
return rows
end
lines.shift
end
rows # return an array of unparsed tuples
end
def next_block
if REPLY_SIZE.to_i == -1 or @row_index == @row_count
return false
else
# The increment step is small to better deal with ruby socket's performance.
# For larger values of the step performance drop;
#
@row_offset = [@row_offset, (@row_count - @row_index)].min
# export offset amount
@connection.set_export(@query['id'], @row_index.to_s, @row_offset.to_s)
@row_index += @row_offset
@row_offset += 1
end
return true
end
# Formats a query <i>string</i> so that it can be parsed by the server
def format_query(q)
if @lang == LANG_SQL
return "s" + q + ";"
else
raise LanguageNotSupported, @lang
end
end
# parse one tuple as returned from the server
def parse_tuple(tuple)
fields = Array.new
# remove trailing "["
tuple = tuple.to_s.gsub(/^\[\s+/, '')
tuple.split(/,\t/).each do |f|
fields << f.gsub(/\\n/, "\n").gsub(/\\/, '').gsub(/^"/, '').gsub(/"$/, '').gsub(/\"/, '')
end
return fields.freeze
end
# Parses a query header and returns information about the query.
def parse_header_query(row)
type = row[1].chr
if type == Q_TABLE
# Performing a SELECT: store informations about the table size, query id, total number of records and returned.
id = row.split(' ')[1]
rows = row.split(' ')[2]
columns = row.split(' ')[3]
returned = row.split(' ')[4]
header = {"id" => id, "type" => type, "rows" => rows, "columns" => columns, "returned" => returned}
elsif type == Q_BLOCK
# processing block header
id = row.split(' ')[1]
columns = row.split(' ')[2]
remains = row.split(' ')[3]
offset = row.split(' ')[4]
header = {"id" => id, "type" => type, "remains" => remains, "columns" => columns, "offset" => offset}
else
header = {"type" => type}
end
return header.freeze
end
# Parses a Q_TABLE header and returns information about the schema.
def parse_header_table(header_t)
if @query["type"] == Q_TABLE
if header_t != nil
name_t = header_t[0].split(' ')[1].gsub(/,$/, '')
name_cols = Array.new
header_t[1].split('%')[1].gsub(/'^\%'/, '').split('#')[0].split(' ').each do |col|
name_cols << col.gsub(/,$/, '')
end
type_cols = {}
header_t[2].split('%')[1].gsub(/'^\%'/, '').split('#')[0].split(' ').each_with_index do |col, i|
if col.gsub(/,$/, '') != nil
type_cols[name_cols[i]] = col.gsub(/,$/, '')
end
end
length_cols = {}
header_t[3].split('%')[1].gsub(/'^\%'/, '').split('#')[0].split(' ').each_with_index do |col, i|
length_cols[name_cols[i]] = col.gsub(/,$/, '')
end
columns_order = {}
name_cols.each_with_index do |col, i|
columns_order[col] = i
end
return {"table_name" => name_t, "columns_name" => name_cols, "columns_type" => type_cols,
"columns_length" => length_cols, "columns_order" => columns_order}.freeze
end
end
end
end
# Overload the class string to convert monetdb to ruby types.
class String
def getInt
self.to_i
end
def getFloat
self.to_f
end
def getString
self.gsub(/^"/, '').gsub(/"$/, '')
end
def getBlob
# first strip trailing and leading " characters
self.gsub(/^"/, '').gsub(/"$/, '')
# convert from HEX to the origianl binary data.
blob = ""
self.scan(/../) { |tuple| blob += tuple.hex.chr }
return blob
end
# ruby currently supports only time + date frommatted timestamps;
# treat TIME and DATE as strings.
def getTime
# HH:MM:SS
self.gsub(/^"/, '').gsub(/"$/, '')
end
def getDate
self.gsub(/^"/, '').gsub(/"$/, '')
end
def getDateTime
#YYYY-MM-DD HH:MM:SS
date = self.split(' ')[0].split('-')
time = self.split(' ')[1].split(':')
Time.gm(date[0], date[1], date[2], time[0], time[1], time[2])
end
def getChar
# ruby < 1.9 does not have a Char datatype
begin
c = self.ord
rescue
c = self
end
return c
end
def getBool
if ['1', 'y', 't', 'true'].include?(self)
return true
elsif ['0', 'n', 'f', 'false'].include?(self)
return false
else
# unknown
return nil
end
end
def getNull
if self.upcase == 'NONE'
return nil
else
raise "Unknown value"
end
end
end
# The contents of this file are subject to the MonetDB Public License
# Version 1.1 (the "License"); you may not use this file except in
# compliance with the License. You may obtain a copy of the License at
# http://monetdb.cwi.nl/Legal/MonetDBLicense-1.1.html
#
# Software distributed under the License is distributed on an "AS IS"
# basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the
# License for the specific language governing rights and limitations
# under the License.
#
# The Original Code is the MonetDB Database System.
#
# The Initial Developer of the Original Code is CWI.
# Portions created by CWI are Copyright (C) 1997-July 2008 CWI.
# Copyright August 2008-2011 MonetDB B.V.
# All Rights Reserved.
# Exception classes for the ruby-monetdb driver
class MonetDBQueryError < StandardError; end
class MonetDBDataError < StandardError; end
class MonetDBCommandError < StandardError; end
class MonetDBConnectionError < StandardError; end
class MonetDBSocketError < StandardError; end
class MonetDBProtocolError < StandardError; end
\ No newline at end of file
# The contents of this file are subject to the MonetDB Public License
# Version 1.1 (the "License"); you may not use this file except in
# compliance with the License. You may obtain a copy of the License at
# http://monetdb.cwi.nl/Legal/MonetDBLicense-1.1.html
#
# Software distributed under the License is distributed on an "AS IS"
# basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the
# License for the specific language governing rights and limitations
# under the License.
#
# The Original Code is the MonetDB Database System.
#
# The Initial Developer of the Original Code is CWI.
# Portions created by CWI are Copyright (C) 1997-July 2008 CWI.
# Copyright August 2008-2011 MonetDB B.V.
# All Rights Reserved.
require 'MonetDB'
db = MonetDB.new
db.conn({ :user => "monetdb", :passwd => "monetdb", :port => 50000, :language => "sql", :host => "localhost", :database => "ruby_test", :auth_type => "SHA1" })
# set type_cast=true to enable MonetDB to Ruby type mapping
#res = db.query("select * from tables, tables, tables;")
#db.query("DROP TABLE tests2 ")
#db.query(" CREATE TABLE tests2 ( col1 varchar(255), col2 varchar(255)) " )
#puts "Number of rows returned: " + res.num_rows.to_s
#puts "Number of fields: " + res.num_fields.to_s
# Get the columns' name
# print res.name_fields
###### Fetch all rows and store them
#puts res.fetch_all
# Iterate over the record set and retrieve on row at a time
#puts res.fetch
#while row = res.fetch do
# printf "%s \n", row
#end
###### Get all records and hash them by column name
#row = res.fetch_all_hash()
#puts col_names[0] + "\t\t" + col_names[1]
#0.upto(res.num_rows) { |i|
# puts row['id'][i]
#}
###### Iterator over columns (on cell at a time)
#while row = res.fetch_hash do
# printf "%s\n", row["id"]
#end
# SQL TRANSACTIONS and SAVE POINTS
db.query('DROP TABLE tests2')
db.auto_commit(false)
puts db.auto_commit?
# create a savepoint
db.save
db.query("CREATE TABLE tests2 (col1 VARCHAR(255), col2 VARCHAR(255))")
res = db.query("SAVEPOINT #{db.transactions} ;")
res = db.query("INSERT INTO \"tests2\" VALUES ('€¿®µ¶¹', '€¿®µ¶¹')")
res = db.query("INSERT INTO \"tests2\" VALUES ('€¿®µ¶¹', '€¿®µ¶¹')")
#res = db.query("INSERT INTO \"tests2\" VALUES ('€¿®µ¶¹', '€¿®µ¶¹')")
#res = db.query("INSERT INTO \"tests2\" VALUES ('€¿®µ¶¹', '€¿®µ¶¹')")
#res = db.query("INSERT INTO \"tests2\" VALUES ('€¿®µ¶¹', '€¿®µ¶¹')")
#res = db.query("INSERT INTO \"tests2\" VALUES ('€¿®µ¶¹', '€¿®µ¶¹')")
#res = db.query("INSERT INTO \"tests2\" VALUES ('€¿®µ¶¹', '€¿®µ¶¹')")
#res = db.query("INSERT INTO \"tests2\" VALUES ('€¿®µ¶¹', '€¿®µ¶¹')")
db.query("COMMIT")
db.release
db.save
res = db.query("SAVEPOINT #{db.transactions} ;")
res = db.query("INSERT INTO \"tests2\" VALUES('NAME4', 'SURNAME4')")
res = db.query("ROLLBACK TO SAVEPOINT #{db.transactions};")
db.release
db.auto_commit(true)
puts db.auto_commit?
res = db.query('SELECT * from tests2')
while row = res.fetch do
printf "%s \n", row
end
res.free
db.close
# The contents of this file are subject to the MonetDB Public License
# Version 1.1 (the "License"); you may not use this file except in
# compliance with the License. You may obtain a copy of the License at
# http://monetdb.cwi.nl/Legal/MonetDBLicense-1.1.html
#
# Software distributed under the License is distributed on an "AS IS"
# basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the
# License for the specific language governing rights and limitations
# under the License.
#
# The Original Code is the MonetDB Database System.
#
# The Initial Developer of the Original Code is CWI.
# Portions created by CWI are Copyright (C) 1997-July 2008 CWI.
# Copyright August 2008-2011 MonetDB B.V.
# All Rights Reserved.
require 'digest/md5'
require 'digest/sha1'
require 'digest/sha2'
class Hasher
# Constructor
# method = "SHA1" or "MD5"
# pwd = Password
def initialize(method, pwd)
if (method.upcase == "SHA1")
@hashfunc = Digest::SHA1.new
@hashname = method.upcase
elsif (method.upcase == "SHA256")
@hashfunc = Digest::SHA256.new
@hashname = method.upcase
elsif (method.upcase == "SHA384")
@hashfunc = Digest::SHA384.new
@hashname = method.upcase
elsif (method.upcase == "SHA512")
@hashfunc = Digest::SHA512.new
@hashname = method.upcase
else
# default to MD5
@hashfunc = Digest::MD5.new
@hashname = "MD5"
end
@pwd = pwd
end
def hashname
@hashname
end
# Compute hash code
def hashsum
return @hashfunc.hexdigest(@pwd)
end
end
# unit test suite for monetdb.
# connects to the 'ruby_test' database and runs test on the server capabilities and SQL language.
# Create first a database with the command:
# $ monetdb create ruby_test
# $ monetdb start ruby_start
#
# Tests examples have been taken from the python and java internfaces and mysql driver.
require 'MonetDB'
require 'test/unit'
require 'time'
require 'date'
class TC_MonetDBCapabilities < Test::Unit::TestCase
# ruby rand function does not support MIN..MAX bounds.
# This alias adds that feature.
alias original_rand rand
def rand(arg1=nil, arg2=nil)
if !arg1.kind_of?(Enumerable) && arg2 == nil
original_rand(arg1)
elsif arg1.kind_of? Enumerable
as_array = arg1.to_a
as_array[original_rand(as_array.length)]
elsif arg1 != nil
arg1 + original_rand(arg2)
end
end
# check the existance of a table
def table_exists?(table='test_ruby')
begin
res = @db.query("select * from #{table} where 1=0")
return true
rescue
return false
end
end
def drop_table(table='test_ruby')
res = @db.query("DROP TABLE #{table}")
end
def setup
@db = MonetDB.new
@db.connect(user = "monetdb", passwd = "monetdb", lang = "sql", host="localhost", port = 50000, db_name = "ruby_test", auth_type = "SHA1")
end
def teardown
@db.close
end
# CREATE TABLE test
def test_create_table(table='test_ruby', cols = [ "First_Name varchar(255)", "Second_Name varchar(255)"])
if table_exists?(table)
drop_table(table)
end
colsdef = ""
cols.each do |c| colsdef += c + ',' end
colsdef = colsdef.chop # remove last ',' character
res = @db.query('CREATE TABLE ' + table + ' (' + colsdef + ')')
end
# perform an inserstion of 'data' into 'table' and check the resulting
# length
def test_data_integrity(table='test_ruby', data=["Gabriele", "MODENA"])
test_create_table
values = ""
data.each do |d| values += '\'' + d.to_s + '\'' + ',' end
values = values.chop # remove last ',' character
insert = 'INSERT INTO ' + table + ' VALUES (' + values + ' )'
@db.query(insert)
res = @db.query("SELECT * FROM #{table}")
rows = res.fetch_all
assert_equal(res.num_rows, rows.size)
end
# test TRANSACTION, COMMIT, ROLLBACK and SAVEPOINT in auto_commit=off mode
def test_transactions(table="test_monetdb_transactions", columndefs=['col1 INT', 'col2 VARCHAR(255)'])
test_create_table(table, columndefs)
data = [1, 'aa']
values = ""
data.each do |d| values += '\'' + d.to_s + '\'' + ',' end
values = values.chop # remove last ',' character
insert = "INSERT INTO " + table + " VALUES " + " ( " + values + " )"
@db.query('START TRANSACTION')
@db.auto_commit(flag=false) # if @db.auto_commit?
@db.query(insert)
@db.query("COMMIT")
res = @db.query('SELECT * FROM ' + table)
rows_committed = res.fetch_all
res.free
# create a save point
@db.save
@db.query("SAVEPOINT #{@db.transactions} ;")
@db.query(insert)
# rollback to savepoint
@db.query("ROLLBACK TO SAVEPOINT #{@db.transactions};")
@db.release
res = @db.query('SELECT * FROM ' + table)
rows_rolled_back = res.fetch_all
res.free
assert_equal(rows_committed, rows_rolled_back)
# restore autocommit for remaining tests
@db.auto_commit(flag=true)
end
# tests on datatypes conversion
def test_char(table="test_monetdb_char", coldefs=["char_field CHAR(1)"])
test_create_table(table, coldefs)
char = 'a'
@db.query("INSERT INTO " + table + " VALUES ( '" + char +"' ) ")
res = @db.query("SELECT char_field FROM " + table + " where char_field = '" + char +"'")
stored_string = res.fetch_hash
assert_equal(char, stored_string['char_field'])
end
def test_smallint(table="test_monetdb_smallint", coldefs=["int_field SMALLINT"])
test_create_table(table, coldefs)
original_num = rand(-32768, 32767)
num = original_num.to_s
@db.query("INSERT INTO " + table + " VALUES ('" + num +"') ")
res = @db.query("SELECT int_field FROM " + table + " where int_field = '" + num +"'")
stored_string = res.fetch_hash
assert_equal(num.to_i, stored_string['int_field'].getInt)
end
def test_int(table="test_monetdb_int", coldefs=["int_field INT"])
test_create_table(table, coldefs)
original_num = rand((2 ** 31 -1))
num = original_num.to_s
@db.query("INSERT INTO " + table + " VALUES ('" + num +"') ")
res = @db.query("SELECT int_field FROM " + table + " where int_field = '" + num +"'")
stored_string = res.fetch_hash
assert_equal(original_num, stored_string['int_field'].getInt)
end
def test_bigint(table="test_monetdb_bigint", coldefs=["int_field BIGINT"])
test_create_table(table, coldefs)
original_num = rand((2 ** 63 -1))
num = original_num.to_s
@db.query("INSERT INTO " + table + " VALUES ('" + num +"') ")
res = @db.query("SELECT int_field FROM " + table + " where int_field = '" + num +"'")
stored_string = res.fetch_hash
assert_equal(original_num, stored_string['int_field'].getInt)
end
def test_real(table="test_monetdb_real", coldefs=["float_field REAL"])
test_create_table(table, coldefs)
original_num = 1.6065851e+20
num = original_num.to_s
@db.query("INSERT INTO " + table + " VALUES ('" + num +"') ")
res = @db.query("SELECT float_field FROM " + table + " where float_field = '" + num +"'")
stored_string = res.fetch_hash
assert_equal(original_num, stored_string['float_field'].getFloat)
end
def test_double(table="test_monetdb_double", coldefs=["float_field DOUBLE"])
test_create_table(table, coldefs)
original_num = 1.6065851e+22
num = original_num.to_s
@db.query("INSERT INTO " + table + " VALUES ('" + num +"') ")
res = @db.query("SELECT float_field FROM " + table + " where float_field = '" + num +"'")
stored_string = res.fetch_hash
assert_equal(original_num, stored_string['float_field'].getFloat)
end
def test_boolean(table="test_monetdb_boolean", coldefs=["bool_field BOOLEAN"] )
test_create_table(table, coldefs)
original_bool = false
bool = original_bool.to_s
@db.query("INSERT INTO " + table + " VALUES ('" + bool +"') ")
res = @db.query("SELECT bool_field FROM " + table + " where bool_field = #{bool}")
stored_string = res.fetch_hash
assert_equal(original_bool, stored_string['bool_field'].getBool)
end
def test_datetime(table="test_monetdb_datetime", coldefs=["dt_field TIMESTAMP"])
test_create_table(table, coldefs)
timestamp = "2009-07-01 15:34:33"
date = timestamp.split(' ')[0].split('-')
time = timestamp.split(' ')[1].split(':')
dt = Time.gm(date[0], date[1], date[2], time[0], time[1], time[2])
@db.query("INSERT INTO " + table + " VALUES ('" + timestamp +"') ")
res = @db.query("SELECT dt_field FROM " + table + " where dt_field = '" + timestamp +"'")
stored_string = res.fetch_hash
assert_equal(dt, stored_string['dt_field'].getDateTime)
end
def test_date(table="test_monetdb_date", coldefs=["dt_field DATE"])
test_create_table(table, coldefs)
timestamp = "2009-07-01"
@db.query("INSERT INTO " + table + " VALUES ('" + timestamp +"') ")
res = @db.query("SELECT dt_field FROM " + table + " where dt_field = '" + timestamp +"'")
stored_string = res.fetch_hash
assert_equal(timestamp, stored_string['dt_field'].getDate)
end
def test_time(table="test_monetdb_time", coldefs=["dt_field TIME"])
test_create_table(table, coldefs)
timestamp = "15:34:33"
@db.query("INSERT INTO " + table + " VALUES ('" + timestamp +"') ")
res = @db.query("SELECT dt_field FROM " + table + " where dt_field = '" + timestamp +"'")
stored_string = res.fetch_hash
assert_equal(timestamp, stored_string['dt_field'].getTime)
end
def test_blob(table="test_monetdb_blob", coldefs = ["blob_field BLOB"])
test_create_table(table, coldefs)
blob = '0000000A146F777BB46B8FBD46AD503A54629C51'
@db.query("INSERT INTO " + table + " VALUES ('" + blob + "') ")
res = @db.query("SELECT blob_field FROM " + table + " where blob_field = '#{blob}'")
stored_string = res.fetch_hash
assert_equal(blob, stored_string['blob_field'])
end
def test_utf8(table="test_monetdb_utf8", coldefs=["utf8_field varchar(100000)"])
test_create_table(table, coldefs)
utf8_string = "€¿®µ¶¹€¿®µ¶¹€¿®µ¶¹"
@db.query("INSERT INTO " + table + " VALUES ( '#{utf8_string}' ) ")
res = @db.query("SELECT utf8_field FROM #{table} where utf8_field = '#{utf8_string}' ")
stored_string = res.fetch_hash
assert_equal(utf8_string, stored_string['utf8_field'])
end
# test MonetDB::conn() named parameters connection method.
def test_conn_with_named_parameters
db = MonetDB.new()
db.conn({ :user => "monetdb", :passwd => "monetdb", :port => 50000, :host => "localhost", :database => "ruby_test"})
assert_equal(true, db.is_connected?)
db.close
end
end
Gem::Specification.new do |s|
s.required_ruby_version = '>= 2.1.0'
s.name = %q{ruby-monetdb-sql}
s.version = "0.2"
s.date = %q{2009-04-27}
s.authors = ["G Modena"]
s.email = %q{gm@cwi.nl}
s.summary = %q{Pure Ruby database driver for MonetDB/SQL}
s.homepage = %q{http://monetdb.cwi.nl/}
s.description = %q{Pure Ruby database driver for the MonetDB/SQL columnar database management system}
s.files = ["README", "lib/MonetDB.rb", "lib/MonetDBConnection.rb", "lib/MonetDBData.rb", "lib/MonetDBExceptions.rb", "lib/hasher.rb"]
s.has_rdoc = true
s.require_path = './lib'
# placeholder project to avoid warning about not having a rubyforge_project
s.rubyforge_project = "nowarning"
end
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment