require 'dm-core' require 'dm-core/adapters/abstract_adapter' require 'rbridge' module DataMapper::Adapters class MnesiaAdapter < AbstractAdapter #Initialize the adapter with the given options # 1. hostname: location of the rbridge server as an Ip or url, default is localhost # 2. port: the port to comunicate with rbridge, default is 9900 # 3. mod: rbridge offers several modes of operation. # 4. ids_table: the table in mnesia that will handle the ids of each record created # 5. id_function: in case an erlang function is wished to be used for record id generations, it can be set here. If the id is a BinaryString, this is required def initialize(name, options) super @hosname = @options[:hostname] ||= "localhost" @port = @options[:port] ||= 9900 @mod = @options[:mod] @ids_table = @options[:ids_table] @id_function = @options[:id_function] @con = con @table_attributes = nil ## Refreshes the spec table so that spec test run properly con.erl("mnesia:clear_table(heffalump).") if name == :default end # Initializes the connection to the rbridge server def con(&blk) # connect to the server if @con.nil? @con = RBridge.new(@mod,@hostname, @port) if @con.erl("1*1.") != 1 raise "Error opening connection to database: #{@con.erl("1*1.")}" end end @con end # Create function required by the adapter spec def create(resources) resources.each do |resource| initialize_identity_field(resource,get_key(resource)) save(con, parse_resource(resource)) end end # Read function required by the adapter spec #TODO change query so that the mnesia filters the results # and does not bring all records on the table every time def read(query) temp_records = erl_read(query) records = [] temp_records.each do |value| records << value if value end result = query.filter_records(records.reverse) result end # Update function required by the adapter spec def update(attributes, collection) attributes = attributes_as_fields(attributes) collection.each do |resource| attributes = resource.attributes(:field).merge(attributes) save(con, parse_resource(resource)) end end # Delete function required by the adapter spec def delete(collection) collection.each do |resource| erl_delete(resource.model,resource.key.first) end end protected # On record creation the primary id must be generated # If the id_function option is set and the key for the model is a binary string, an erlan function is used. # otherwise, and mnesia unique id table can be used or a random number def get_key resource model = resource.model identity_field = get_identity_field resource if !@id_function.nil? and identity_field.is_a? DataMapper::Property::BinaryString con.erl("#{@id_function}.") elsif @ids_tables.nil? con.erl("mnesia:dirty_update_counter(#{@ids_table},#{model.to_s.downcase},1).") else rand(2**32) end end # Before update or create the resource must be parse to be properly pass to rbridge def parse_resource(resource) model = resource.model fields = [] types = Hash.new # Get all properties defined on the datamapper model resource.send(:properties).map do |property| if property.is_a? DataMapper::Property::Serial types[property.name] = 0 elsif property.is_a? DataMapper::Property::Integer types[property.name] = 0 elsif property.is_a? DataMapper::Property::Boolean types[property.name] = false elsif property.is_a? DataMapper::Property::Record types[property.name] = {} elsif property.is_a? DataMapper::Property::List types[property.name] = [] elsif property.is_a? DataMapper::Property::BinaryString types[property.name] = 0b100 elsif property.is_a? DataMapper::Property::DateTime types[property.name] = Time.now elsif property.is_a? DataMapper::Property::String types[property.name] = "" end end # Set the value for the identity field identity_field = get_identity_field resource fields.push(identity_field.dump(resource.key.first)) # Loop through the attributes defined in the mnesia record # and set the value for each one except the identity field table_attributes(model).each do |p| if p.to_s!= identity_field.field if types[p.to_sym].is_a? Integer value = resource[p.to_sym] == nil ? 'undefined' : resource[p.to_sym] fields.push(value) elsif types[p.to_sym].is_a? Array field = resource.model.properties.detect{|f|f.field == p.to_s} value = resource[p.to_sym] if resource[p.to_sym].length == 0 fields.push("'undefined'") else fields.push(field.dump(value)) end elsif types[p.to_sym].is_a? Hash value = resource[p.to_sym] if resource[p.to_sym].nil? fields.push("'undefined'") else fields.push(value) end else if resource[p.to_sym].nil? fields.push("'undefined'") else fields.push("\"" + resource[p.to_sym].to_s.gsub('"','\"') + "\"") end end end end "#{model.to_s.downcase},{#{model.to_s.downcase},#{fields.join(',').to_s}}" end # Function called by create and update def save(con, record) if !erl_save(con,record) raise WriteError, erl_save(con,record) end end # Retrived the attribues defined for the table in mnesia def table_attributes model if @table_attribute.nil? @table_attributes = con.erl("mnesia:table_info(" + model.to_s.downcase + ",attributes).") end @table_attributes end # Set the initial value for the identity field on record creation def initialize_identity_field resource, value identity_field = get_identity_field resource identity_field.set(resource,value) end # Returns the field object defined as a key in the model def get_identity_field resource resource.model.key(name).detect{|p|p.key?} end # Gets the id value from the conditions passed in the query def get_id_from_conditions conditions conditions.map do |condition| return condition.loaded_value if condition.subject.name == :id end nil end # ERLAND FUNCTIONS def erl_save(con,record) con.erl("mnesia:dirty_write(#{record}).") end # Reads the recuested records from mnesia and parses them to be pased to the Read function def erl_read query model = query.model if query.limit.nil? records = con.erl("mnesia:transaction(fun() ->qlc:eval( qlc:q([ X || X <- mnesia:table(#{model.to_s.downcase}) ]))end ).")[1] elsif query.limit == 1 && !get_id_from_conditions(query.conditions).nil? id = get_id_from_conditions(query.conditions) if id.is_a? Integer records = con.erl("mnesia:dirty_read({#{model.to_s.downcase},#{id}}).") else records = con.erl("mnesia:dirty_read({#{model.to_s.downcase},<<\"" + id.to_s + "\">>}).") end elsif query.limit > 1 records = con.erl("mnesia:transaction(fun() ->qlc:eval( qlc:next_answers(qlc:cursor(qlc:q([ X || X <- mnesia:table(#{query.model.to_s.downcase}) ])),#{query.limit}) )end ).")[1] else records = con.erl("mnesia:transaction(fun() ->qlc:eval( qlc:q([ X || X <- mnesia:table(#{model.to_s.downcase}) ]))end ).")[1] end result = [] attributes = table_attributes model records.each do | r | r.delete(model.to_s.downcase) h = Hash.new r.each_with_index do |item,i| h[attributes[i]] = r[i] == 'undefined' ? nil : r[i] end result << h end result.reverse end # Sends the delete command to erland. def erl_delete model, id command = "" if id.is_a? Integer command = "mnesia:dirty_delete({#{model.to_s.downcase},#{id.to_s}})." else command = "mnesia:dirty_delete({#{model.to_s.downcase},<<\"" + id.to_s + "\">>})." end if !con.erl(command) raise WriteError, con.erl(command) end end class ConnectError < StandardError end class WriteError < StandardError end class ReadError < StandardError end end end