module Arcade
##
# Implements the Database-Adapter
#
# currently, only attributes of type String are supported
#
# {Database-instance}.database points to the connected Aradedb-database
# DB.hi
#
##
class Database
include Logging
extend Dry::Core::ClassAttributes
include Support::Model # provides allocate_model
defines :namespace
defines :environment
def initialize environment=:development
self.class.configure_logger( Config.logger )
if self.class.environment.nil? # class attribute is set on the first call
# further instances of Database share the same environment
self.class.environment environment
end
@session_id = nil # declare session_id
self.class.namespace Object.const_get( Config.namespace )
end
def database
@database ||= Config.database[self.class.environment]
end
# ------------ types ------------...
# returns an Array of type-attributes
# [{:name=>"Account", :type=>"document"},
# {:name=>"test", :type=>"vertex"},
# {:name=>"test1", :type=>"vertex"},
# {:parentTypes=>["test1"], :name=>"test2", :type=>"vertex"}]
#
def types refresh=false
# uses API
if @types.nil? || refresh
@types = Api.query(database, "select from schema:types" )
.map{ |y| y.delete_if{|_,b,| b.blank? } } # eliminate empty entries
end
@types
## upon startup, this is the first access to the database-server
rescue NoMethodError => e
logger.fatal "Could not read Database Types. \n Is the database running?"
Kernel.exit
end
def indexes refresh=false
types(refresh).find_all{|x| x.key? :indexes }.map{|y| y[:indexes]}.flatten
end
# ------------ hierarchy -------------
# returns an Array of types
#
# each entry is an Array
# => [["test"], ["test1", "test2"]] (using the example above)
#
# Parameter: type -- one of 'vertex', 'document', 'edge'
def hierarchy type: 'vertex'
# uses API
# gets all types depending on the parent-type
pt = ->( s ) { types.find_all{ |x| x[:parentTypes] &.include?(s) }.map{ |v| v[:name] } }
# takes an array of base-types. gets recursivly all childs
child_types = -> (base_types) do
base_types.map do | bt |
if pt[ bt ].empty?
[ bt ]
else
[bt, child_types[ pt[ bt ] ] ].flatten
end
end
end
# gets child-types for all base-types
child_types[ types.find_all{ |x| !x[:parentTypes] && x[:type] == type.to_s }.map{ |v| v[:name] } ]
end
# ------------ create type -----------
# returns an Array
# Example: > create_type :vertex, :my_vertex
# => [{"typeName"=>"my_vertex", "operation"=>"create vertex type"}]
#
# takes additional arguments: extends: '' (for inheritance)
# bucket:
# buckets:
#
# additional arguments are just added to the command
#
# its aliased as `create_class`
#
def create_type kind, type, **args
exe = -> do
case kind.to_s.downcase
when /^v/
"create vertex type #{type} "
when /^d/
"create document type #{type} "
when /^e/
"create edge type #{type} "
end.concat( args.map{|x,y| "#{x} #{y} "}.join)
end
dbe= Api.execute database, &exe
types( true ) # update cached schema
dbe
rescue Arcade::QueryError => e
if e.message =~/Type\s+.+\salready\s+exists/
Arcade::Database.logger.debug "Database type #{type} already present"
else
raise
end
end
alias create_class create_type
# ------------ drop type -----------
# delete any record prior to the attempt to drop a type.
# The `unsafe` option is not implemented.
def drop_type type
Api.execute database, "drop type #{type} if exists"
end
# ------------------------------ transaction ----------------------------------------------------- #
# Encapsulates simple transactions
#
# nested transactions are not supported.
# * use the low-leve api.begin_tranaction for that purpose
# * reuses an existing transaction
#
def begin_transaction
@session_id ||= Api.begin_transaction database
end
# ------------------------------ commit ----------------------------------------------------- #
def commit
r = Api.commit( database, session_id: session)
@session_id = nil
true if r == 204
end
# ------------------------------ rollback ----------------------------------------------------- #
#
def rollback
r = Api.rollback( database, session_id: session)
@session_id = nil
true if r == 500
rescue HTTPX::HTTPError => e
raise
end
# ------------ create -----------
# returns an rid of the successfully created vertex or document
#
# Parameter: name of the vertex or document type
# Hash of attributes
#
# Example: > DB.create :my_vertex, a: 14, name: "Hugo"
# => "#177:0"
#
def create type, **params
# uses API
Api.create_document database, type, session_id: session, **params
end
# ------------------------------ insert ------------------------------------------------------ #
#
# translates the given parameters to
# INSERT INTO [TYPE:]|BUCKET:|INDEX:
# [([,]*) VALUES ([,]*)[,]*]|
# [CONTENT {}|[{}[,]*]]
#
# :from and :return are not supported
#
# If a transaction is active, the insert is executed in that context.
# Nested transactions are not supported
def insert **params
content_params = params.except( :type, :bucket, :index, :from, :return, :session_id )
target_params = params.slice( :type, :bucket, :index )
# session_id = params[:session_id] # extraxt session_id --> future-use?
if target_params.empty?
raise "Could not insert: target missing (type:, bucket:, index:)"
elsif content_params.empty?
logger.error "Nothing to Insert"
else
content = "CONTENT #{ content_params.to_json }"
target = target_params.map{|y,z| y==:type ? z : "#{y.to_s} #{ z } "}.join
result = Api.execute( database, session_id: session ){ "INSERT INTO #{target} #{content} "}
result &.first.allocate_model(false)
end
end
# ------------------------------ get ------------------------------------------------------ #
# Get fetches the record associated with the rid given as parameter.
#
# The rid is accepted as
# DB.get "#123:123", DB.get "123:123" or DB.get 123, 123
#
# Links are autoloaded (can be suppressed by the optional Block (false))
#
# puts DB.get( 19,0 )
# ", ""]>
# puts DB.get( 19,0 ){ false }
#
#
#
def get *rid
autocomplete = block_given? ? yield : true
rid = rid.join(':')
rid = rid[1..-1] if rid[0]=="#"
if rid.rid?
Api.query( database, "select from #{rid}", session_id: session ).first &.allocate_model(autocomplete)
else
raise Arcade::QueryError "Get requires a rid input", caller
end
end
# ------------------------------ get ------------------------------------------------------ #
#
# Delete the specified rid
#
def delete rid
r = Api.execute( database, session_id: session ){ "delete from #{rid}" }
success = r == [{ :count => 1 }]
end
# ------------------------------ transmit ------------------------------------------------------ #
# transmits a command which potentially modifies the database
#
# Uses the given session_id for transaction-based operations
#
# Otherwise just performs the operation
def transmit &block
response = Api.execute database, session_id: session, &block
if response.is_a? Hash
_allocate_model res
else
response
end
end
# ------------------------------ execute ------------------------------------------------------ #
# execute a command which modifies the database
#
# The operation is performed via Transaction/Commit
# If an Error occurs, its rolled back
#
# If a transaction is already active, a nested transation is initiated
#
def execute &block
# initiate a new transaction
s= Api.begin_transaction database
response = Api.execute database, session_id: s, &block
r= if response.is_a? Hash
_allocate_model response
else
response
end
if Api.commit( database, session_id: s) == 204
r # return associated array of Arcade::Base-objects
else
[]
end
rescue Dry::Struct::Error, Arcade::QueryError => e
Api.rollback database, session_id: s, log: false
logger.info "Execution FAILED --> Status #{e.status}"
[] # return empty result
end
# returns an array of results
#
# detects database-records and allocates them as model-objects
#
def query query_object
Api.query database, query_object.to_s, session_id: session
end
# returns an array of rid's (same logic as create)
def create_edge edge_class, from:, to:, **attributes
content = attributes.empty? ? "" : "CONTENT #{attributes.to_json}"
cr = ->( f, t ) do
begin
cmd = -> (){ "create edge #{edge_class} from #{f.rid} to #{t.rid} #{content}" }
edges = transmit( &cmd ).allocate_model(false)
rescue Arcade::QueryError => e
raise unless e.message =~ /Found duplicate key/
puts "#"+e.detail.split("#").last[0..-3]
end
end
from = [from] unless from.is_a? Array
to = [to] unless to.is_a? Array
from.map do | from_record |
to.map { | to_record | cr[ from_record, to_record ] if to_record.rid? } if from_record.rid?
end.flatten
end
def session
@session_id
end
def session?
!session.nil?
end
end # class
end # module