diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..85dfb39505fd53416248ca766a8a03145427611e --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.buildpath +.project +.idea +*.gem diff --git a/Changelog b/Changelog new file mode 100644 index 0000000000000000000000000000000000000000..186f9163be42c803b68f4e49239d6c2b571b5b8d --- /dev/null +++ b/Changelog @@ -0,0 +1,38 @@ +* 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/). diff --git a/README b/README new file mode 100644 index 0000000000000000000000000000000000000000..4143b64561faa30531f831aab92ca74079468429 --- /dev/null +++ b/README @@ -0,0 +1,154 @@ +== 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 + diff --git a/TODO b/TODO new file mode 100644 index 0000000000000000000000000000000000000000..8a4470bb1ccf98ed2a4bbfa241340a2e8aeac868 --- /dev/null +++ b/TODO @@ -0,0 +1,5 @@ +* test and improve utf8 and type conversion + +* documentation cleanup + +* OSM on rails demo (slowed down due to third party plugins requirements) diff --git a/adapter/Makefile.ag b/adapter/Makefile.ag new file mode 100644 index 0000000000000000000000000000000000000000..b54b10b8363dcd124904aa54b1eecf3caed20665 --- /dev/null +++ b/adapter/Makefile.ag @@ -0,0 +1,24 @@ +# 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 diff --git a/adapter/activerecord-monetdb-adapter-0.1.gemspec b/adapter/activerecord-monetdb-adapter-0.1.gemspec new file mode 100644 index 0000000000000000000000000000000000000000..83d91d3b6aba6d3961f8e63e5c6696a8e832c798 --- /dev/null +++ b/adapter/activerecord-monetdb-adapter-0.1.gemspec @@ -0,0 +1,18 @@ +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 diff --git a/adapter/lib/active_record/connection_adapters/monetdb_adapter.rb b/adapter/lib/active_record/connection_adapters/monetdb_adapter.rb new file mode 100644 index 0000000000000000000000000000000000000000..fa5a7999f209c1dbe3a7f9fa7749ebb1ba411551 --- /dev/null +++ b/adapter/lib/active_record/connection_adapters/monetdb_adapter.rb @@ -0,0 +1,575 @@ +# 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 diff --git a/doc/rails.txt b/doc/rails.txt new file mode 100644 index 0000000000000000000000000000000000000000..ba869de6a9ee87c7cee2068d3ef9f848bcef42d5 --- /dev/null +++ b/doc/rails.txt @@ -0,0 +1,98 @@ += 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 diff --git a/examples/activerecord.rb b/examples/activerecord.rb new file mode 100644 index 0000000000000000000000000000000000000000..366d740db6841ac5215c00bd7f449ec82cb68643 --- /dev/null +++ b/examples/activerecord.rb @@ -0,0 +1,81 @@ +# 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 diff --git a/examples/standalone.rb b/examples/standalone.rb new file mode 100644 index 0000000000000000000000000000000000000000..bce8a6cb6f36264a03f7287b86626b335b9103be --- /dev/null +++ b/examples/standalone.rb @@ -0,0 +1,86 @@ +# 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 diff --git a/lib/MonetDB.rb b/lib/MonetDB.rb new file mode 100644 index 0000000000000000000000000000000000000000..d1936dc464ea7f74f7d87c025be17a2fa117d61f --- /dev/null +++ b/lib/MonetDB.rb @@ -0,0 +1,312 @@ +# 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 diff --git a/lib/MonetDBConnection.rb b/lib/MonetDBConnection.rb new file mode 100644 index 0000000000000000000000000000000000000000..e18df90bc14416eb5651a3457488dc53d2cd007e --- /dev/null +++ b/lib/MonetDBConnection.rb @@ -0,0 +1,548 @@ +# 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 diff --git a/lib/MonetDBData.rb b/lib/MonetDBData.rb new file mode 100644 index 0000000000000000000000000000000000000000..7dd8509aca9653976a73c9350167cbf17e6b054c --- /dev/null +++ b/lib/MonetDBData.rb @@ -0,0 +1,420 @@ +# 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 diff --git a/lib/MonetDBExceptions.rb b/lib/MonetDBExceptions.rb new file mode 100644 index 0000000000000000000000000000000000000000..75745b5144ff680da4a20d97c6fbcaa969efbb40 --- /dev/null +++ b/lib/MonetDBExceptions.rb @@ -0,0 +1,25 @@ +# 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 diff --git a/lib/demo.rb b/lib/demo.rb new file mode 100644 index 0000000000000000000000000000000000000000..94fd640a26e980f577b666bf14fc148637e8d5cd --- /dev/null +++ b/lib/demo.rb @@ -0,0 +1,101 @@ +# 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 diff --git a/lib/hasher.rb b/lib/hasher.rb new file mode 100644 index 0000000000000000000000000000000000000000..2501266dc54e34bdc0f5bee433b8f821a675f7db --- /dev/null +++ b/lib/hasher.rb @@ -0,0 +1,56 @@ +# 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 diff --git a/lib/test/test_capabilities.rb b/lib/test/test_capabilities.rb new file mode 100644 index 0000000000000000000000000000000000000000..fc2146b2d715795ca9fed5be7d081c1b7b38df90 --- /dev/null +++ b/lib/test/test_capabilities.rb @@ -0,0 +1,310 @@ +# 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 diff --git a/ruby-monetdb-sql-0.1.gemspec b/ruby-monetdb-sql-0.1.gemspec new file mode 100644 index 0000000000000000000000000000000000000000..e773f0738fc2abb00c259f2a18acc5a9534ab703 --- /dev/null +++ b/ruby-monetdb-sql-0.1.gemspec @@ -0,0 +1,17 @@ + +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