arcadedb 0.3.1 → 0.3.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,194 @@
1
+ ##
2
+ ## This example realises a bidirectional 1:n relation using Edges & Vertices
3
+ #
4
+ ## The schema is implemented in modelfiles located in spec/model
5
+ ## /spec/models/ex/human.rb # Vertex
6
+ ## /spec/models/ex/depend_on.rb # Edge
7
+ #
8
+ # This script runs in the test environment.
9
+ ##
10
+ require 'bundler/setup'
11
+ require 'zeitwerk'
12
+ require 'arcade'
13
+
14
+ include Arcade
15
+ ## require modelfiles
16
+ loader = Zeitwerk::Loader.new
17
+ loader.push_dir ("#{__dir__}/../spec/model")
18
+ loader.setup
19
+
20
+ ## clear test database
21
+
22
+ databases = Arcade::Api.databases
23
+ if databases.include?(Arcade::Config.database[:test])
24
+ Arcade::Api.drop_database Arcade::Config.database[:test]
25
+ end
26
+ Arcade::Api.create_database Arcade::Config.database[:test]
27
+
28
+ ## Universal Database handle
29
+ DB = Arcade::Init.connect 'test'
30
+
31
+ ## ------------------------------------------------------ End Setup ------------------------------------- ##
32
+ ##
33
+ ## We are realising a self referencing relation
34
+ ## parent <--> children
35
+ #
36
+
37
+
38
+ Ex::Human.create_type # initialize the database
39
+ Ex::DependOn.create_type
40
+
41
+ nodes = %w( Guthorn Fulkerson Sniezek Tomasulo Portwine Keala Revelli Jacks Gorby Alcaoa ).map do | name |
42
+ Ex::Human.insert name: name, birth: 2022 - rand(99), married: rand(2)==1
43
+ end
44
+
45
+ puts Ex::Human.count.to_s + " Human Vertices created"
46
+
47
+ puts "------------------------------ get a sorted list of married humans ---------------------------------"
48
+ puts
49
+
50
+ merried = Ex::Human.query( where: { married: true })
51
+ merried.order 'birth'
52
+ new_merried = Query.new projection: 'name, 2022-birth as age ', from: merried # merge two queries
53
+ puts new_merried.query
54
+
55
+ puts "------------------------------ and one for not married humans ---------------------------------"
56
+ puts
57
+
58
+ singles = Ex::Human.query( where: { married: false })
59
+ singles.order 'birth'
60
+ new_singles = Query.new projection: 'name, 2022-birth as age ', from: singles # merge two queries
61
+ puts new_singles.query
62
+
63
+
64
+ puts "------------------------------ connect married humans with children ------------------------------"
65
+ children = singles.query.allocate_model
66
+
67
+ begin
68
+ children_enumerator = children.each
69
+ merried.query.allocate_model.map do | parent |
70
+ parent.assign via: Ex::DependOn, vertex: children_enumerator.next
71
+ end
72
+ rescue StopIteration
73
+ puts "No more children"
74
+ end
75
+
76
+ # Ex::Human.parents is essential
77
+ # EX::Human.query projection: 'out()' , whee: { married: true }
78
+ # Ex::Human.children is essential
79
+ # EX::Human.query projection: 'out()' , whee: { married: true }
80
+ puts "--------------------------- Parent and Children ---------------------------------------------------"
81
+ puts
82
+ puts "%10s %7s %10s %30s " % ["Parent", "Age", "Child", "sorted by Child"]
83
+ puts "- " * 50
84
+ Ex::Human.parents( order: 'name' ).each do |parent| # note: order: 'name' is included
85
+ # in the query
86
+ puts "%10s %7d %10s " % [parent.name, 2022 - parent.birth, parent.out.first.name]
87
+ end
88
+
89
+ puts "--------------------------- child and parent -----------------------------------------------------"
90
+ puts
91
+ puts "%10s %7s %10s %30s " % ["Child", "Age", "Parent", "sorted by Parent"]
92
+ puts "- " * 50
93
+ Ex::Human.children( order: 'name' ).each do |child|
94
+ puts "%10s %7d %10s " % [child.name, 2022 - child.birth, child.in.first.name]
95
+ end
96
+
97
+ puts "--------------------------- Add child to a parent -----------------------------------------------"
98
+ puts
99
+
100
+ Ex::Human.parents.first.assign via: Ex::DependOn, vertex: Ex::Human.insert( name: "TestBaby", birth: 2022, married: false)
101
+
102
+ puts "Parent: " + Ex::Human.parents.first.to_human
103
+ puts "Children: \n " + Ex::Human.parents.first.out.to_human.join("\n ")
104
+
105
+ ## Expected output
106
+ __END__
107
+
108
+ Using default database credentials and settings fron /home/ubuntu/workspace/arcadedb
109
+ 27.04.(18:35:05) INFO->Q: create vertex type ex_human
110
+ 27.04.(18:35:05) INFO->Q: CREATE PROPERTY ex_human.name STRING
111
+ 27.04.(18:35:05) INFO->Q: CREATE PROPERTY ex_human.married BOOLEAN
112
+ 27.04.(18:35:05) INFO->Q: CREATE INDEX `Example[human]` ON ex_human ( name ) UNIQUE
113
+ 27.04.(18:35:05) INFO->Q: create edge type ex_depend_on
114
+ 27.04.(18:35:05) INFO->Q: CREATE INDEX depends_out_in ON ex_depend_on (`@out`, `@in`) UNIQUE
115
+ 27.04.(18:35:05) INFO->Q: INSERT INTO ex_human CONTENT {"name":"Guthorn","birth":1962,"married":false}
116
+ 27.04.(18:35:05) INFO->Q: INSERT INTO ex_human CONTENT {"name":"Fulkerson","birth":1962,"married":true}
117
+ 27.04.(18:35:05) INFO->Q: INSERT INTO ex_human CONTENT {"name":"Sniezek","birth":1972,"married":true}
118
+ 27.04.(18:35:05) INFO->Q: INSERT INTO ex_human CONTENT {"name":"Tomasulo","birth":1953,"married":false}
119
+ 27.04.(18:35:05) INFO->Q: INSERT INTO ex_human CONTENT {"name":"Portwine","birth":1975,"married":true}
120
+ 27.04.(18:35:05) INFO->Q: INSERT INTO ex_human CONTENT {"name":"Keala","birth":1961,"married":false}
121
+ 27.04.(18:35:05) INFO->Q: INSERT INTO ex_human CONTENT {"name":"Revelli","birth":1948,"married":true}
122
+ 27.04.(18:35:05) INFO->Q: INSERT INTO ex_human CONTENT {"name":"Jacks","birth":1993,"married":true}
123
+ 27.04.(18:35:05) INFO->Q: INSERT INTO ex_human CONTENT {"name":"Gorby","birth":1979,"married":false}
124
+ 27.04.(18:35:05) INFO->Q: INSERT INTO ex_human CONTENT {"name":"Alcaoa","birth":1960,"married":false}
125
+ 27.04.(18:35:05) INFO->Q: select count(*) from ex_human
126
+ 10 Human Vertices created
127
+ ------------------------------ get a sorted list of married humans ---------------------------------
128
+
129
+ 27.04.(18:35:05) INFO->Q: select name, 2022-birth as age from ( select from ex_human where married = true order by birth )
130
+ {:name=>"Revelli", :age=>74}
131
+ {:name=>"Fulkerson", :age=>60}
132
+ {:name=>"Sniezek", :age=>50}
133
+ {:name=>"Portwine", :age=>47}
134
+ {:name=>"Jacks", :age=>29}
135
+ ------------------------------ and one for not married humans ---------------------------------
136
+
137
+ 27.04.(18:35:05) INFO->Q: select name, 2022-birth as age from ( select from ex_human where married = false order by birth )
138
+ {:name=>"Tomasulo", :age=>69}
139
+ {:name=>"Alcaoa", :age=>62}
140
+ {:name=>"Keala", :age=>61}
141
+ {:name=>"Guthorn", :age=>60}
142
+ {:name=>"Gorby", :age=>43}
143
+ ------------------------------ connect married humans with children ------------------------------
144
+ 27.04.(18:35:05) INFO->Q: select from ex_human where married = false order by birth
145
+ 27.04.(18:35:05) INFO->Q: select from ex_human where married = true order by birth
146
+ 27.04.(18:35:05) INFO->Q: create edge ex_depend_on from #19:0 to #10:0 CONTENT {"set":{}}
147
+ 27.04.(18:35:05) INFO->Q: create edge ex_depend_on from #4:0 to #4:1 CONTENT {"set":{}}
148
+ 27.04.(18:35:05) INFO->Q: create edge ex_depend_on from #7:0 to #16:0 CONTENT {"set":{}}
149
+ 27.04.(18:35:05) INFO->Q: create edge ex_depend_on from #13:0 to #1:0 CONTENT {"set":{}}
150
+ 27.04.(18:35:05) INFO->Q: create edge ex_depend_on from #22:0 to #1:1 CONTENT {"set":{}}
151
+ --------------------------- Parent and Children ---------------------------------------------------
152
+
153
+ Parent Age Child sorted by Child
154
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
155
+ 27.04.(18:35:05) INFO->Q: select in() from ex_human where married = false order by name
156
+ 27.04.(18:35:05) INFO->Q: select out() from #4:0
157
+ Fulkerson 60 Alcaoa
158
+ 27.04.(18:35:05) INFO->Q: select out() from #22:0
159
+ Jacks 29 Gorby
160
+ 27.04.(18:35:05) INFO->Q: select out() from #13:0
161
+ Portwine 47 Guthorn
162
+ 27.04.(18:35:05) INFO->Q: select out() from #7:0
163
+ Sniezek 50 Keala
164
+ 27.04.(18:35:05) INFO->Q: select out() from #19:0
165
+ Revelli 74 Tomasulo
166
+ --------------------------- child and parent -----------------------------------------------------
167
+
168
+ Child Age Parent sorted by Parent
169
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
170
+ 27.04.(18:35:05) INFO->Q: select out() from ex_human where married = true order by name
171
+ 27.04.(18:35:05) INFO->Q: select in() from #4:1
172
+ Alcaoa 62 Fulkerson
173
+ 27.04.(18:35:05) INFO->Q: select in() from #1:1
174
+ Gorby 43 Jacks
175
+ 27.04.(18:35:05) INFO->Q: select in() from #1:0
176
+ Guthorn 60 Portwine
177
+ 27.04.(18:35:05) INFO->Q: select in() from #10:0
178
+ Tomasulo 69 Revelli
179
+ 27.04.(18:35:05) INFO->Q: select in() from #16:0
180
+ Keala 61 Sniezek
181
+ --------------------------- Add child to a parent -----------------------------------------------
182
+
183
+ 27.04.(18:35:05) INFO->Q: select in() from ex_human where married = false
184
+ 27.04.(18:35:05) INFO->Q: INSERT INTO ex_human CONTENT {"name":"TestBaby","birth":2022,"married":false}
185
+ 27.04.(18:35:05) INFO->Q: create edge ex_depend_on from #13:0 to #7:1 CONTENT {"set":{}}
186
+ 27.04.(18:35:05) INFO->Q: select in() from ex_human where married = false
187
+ Parent: <ex_human[#13:0]: {0->}{->2}}, birth: 1975, married: true, name: Portwine>
188
+ 27.04.(18:35:05) INFO->Q: select in() from ex_human where married = false
189
+ 27.04.(18:35:05) INFO->Q: select out() from #13:0
190
+ Children:
191
+ <ex_human[#1:0]: {->}{->}}, birth: 1962, married: false, name: Guthorn>
192
+ <ex_human[#7:1]: {->}{->}}, birth: 2022, married: false, name: TestBaby>
193
+
194
+
@@ -0,0 +1,306 @@
1
+ module Arcade
2
+ module Api
3
+ =begin
4
+ This is a simple admin interface
5
+
6
+ $ Arcade::Api.databases # returns an Array of known databases
7
+ $ Arcade::Api.create_database <a string> # returns true if succesfull
8
+ $ Arcade::Api.drop_database <a string> # returns true if successfull
9
+
10
+ $ Arcade::Api.create_document <database>, <type>, attributes
11
+ $ Arcade::Api.execute( <database> ) { <query> }
12
+ $ Arcade::Api.query( <database> ) { <query> }
13
+ $ Arcade::Api.get_record <database>, rid # returns a hash
14
+
15
+
16
+ <query> is either a string
17
+ or a hash { :query => " ",
18
+ :language => one of :sql, :cypher, :gmelion: :neo4j ,
19
+ :params => a hash of parameters,
20
+ :limit => a number ,
21
+ :serializer: one of :graph, :record }
22
+
23
+ =end
24
+
25
+
26
+ def self.databases
27
+ get_data 'databases'
28
+ end
29
+
30
+ def self.create_database name
31
+ unless databases.include?( name.to_s )
32
+ payload = { "command" => "create database #{name}" }.to_json
33
+ post_data "server", { body: payload }.merge( auth ).merge( json )
34
+ end
35
+ rescue QueryError => e
36
+ logger.fatal "Create database #{name} through \"POST create/#{name}\" failed"
37
+ logger.fatal e
38
+ raise
39
+ end
40
+
41
+ def self.drop_database name
42
+ if databases.include?( name.to_s )
43
+ payload = { "command" => "drop database #{name}" }.to_json
44
+ post_data "server", { body: payload }.merge( auth ).merge( json )
45
+ end
46
+ end
47
+ # ------------------------------ create document ------------------------------------------------- #
48
+ # adds a document to the database
49
+ #
50
+ # specify database-fields as hash-type parameters
51
+ #
52
+ # i.e Arcade::Api.create_document 'devel', 'documents', name: 'herta meyer', age: 56, sex: 'f'
53
+ #
54
+ # returns the rid of the inserted dataset
55
+ #
56
+ def self.create_document database, type, **attributes
57
+ payload = { "@type" => type }.merge( attributes ).to_json
58
+ logger.debug "C: #{payload}"
59
+ options = if session.nil?
60
+ { body: payload }.merge( auth ).merge( json )
61
+ else
62
+ { body: payload }.merge( auth ).merge( json ).merge( headers: { "arcadedb-session-id" => session })
63
+ end
64
+ post_data "document/#{database}", options
65
+ end
66
+
67
+ # ------------------------------ execute ------------------------------------------------- #
68
+ # executes a sql-query in the specified database
69
+ #
70
+ # the query is provided as block
71
+ #
72
+ # returns an Array of results (if propriate)
73
+ # i.e
74
+ # Arcade::Api.execcute( "devel" ) { 'select from test ' }
75
+ # =y [{"@rid"=>"#57:0", "@type"=>"test", "name"=>"Hugo"}, {"@rid"=>"#60:0", "@type"=>"test", "name"=>"Hubert"}]
76
+ #
77
+ def self.execute database, query=nil
78
+ pl = query.nil? ? provide_payload(yield) : provide_payload(query)
79
+ options = { body: pl }.merge( auth ).merge( json )
80
+ unless session.nil?
81
+ options = options.merge( headers: { "arcadedb-session-id" => session })
82
+ end
83
+ post_data "command/#{database}" , options
84
+ rescue Arcade::QueryError => e
85
+ # puts e.methods
86
+ #puts e.exception
87
+ # puts e.full_message
88
+ if e.message =~ /retry/
89
+ retry
90
+ else
91
+ raise e.message
92
+ end
93
+ end
94
+
95
+ # ------------------------------ query ------------------------------------------------- #
96
+ # same for idempotent queries
97
+ def self.query database, query
98
+ options = { body: provide_payload(query) }.merge( auth ).merge( json )
99
+ post_data "query/#{database}" , options
100
+ end
101
+
102
+ # ------------------------------ get_record ------------------------------------------------- #
103
+ # fetches a record by providing database and rid
104
+ # and returns the result as hash
105
+ #
106
+ # > Api.get_record 'devel', '225:6'
107
+ # > Api.get_record 'devel', 225, 6
108
+ # > Api.get_record 'devel', '#225:6'
109
+ # => {:@out=>0, :@rid=>"#225:6", :@in=>0, :@type=>"my_names", :name=>"Zaber", :@cat=>"v"}
110
+
111
+ def self.get_record database, *rid
112
+ rid = rid.join(':')
113
+ rid = rid[1..-1] if rid[0]=="#"
114
+ if rid.rid?
115
+ get_data "document/#{database}/#{rid}"
116
+ else
117
+ raise Arcade::Error "Get requires a rid input"
118
+ end
119
+ end
120
+
121
+ # ------------------------------ property ------------------------------------------------- #
122
+ # Adds properties to the type
123
+ #
124
+ # call via
125
+ # Api.property <database>, <type>, name1: a_format , name2: a_format
126
+ #
127
+ # Format is one of
128
+ # Boolean, Integer, Short, Long, Float, Double, String
129
+ # Datetime, Binary, Byte, Decimal, Link
130
+ # Embedded, EmbeddedList, EmbeddedMap
131
+ #
132
+ # In case of an Error, anything is rolled back and nil is returned
133
+ #
134
+ def self.property database, type, **args
135
+
136
+ begin_transaction database
137
+ success = args.map do | name, format |
138
+ r= execute(database) {" create property #{type.to_s}.#{name.to_s} #{format.to_s} " } &.first
139
+ if r.nil?
140
+ false
141
+ else
142
+ r.keys == [ :propertyName, :typeName, :operation ] && r[:operation] == 'create property'
143
+ end
144
+ end.uniq
145
+ if success == [true]
146
+ commit database
147
+ true
148
+ else
149
+ rollback database
150
+ end
151
+
152
+
153
+ end
154
+
155
+ # ------------------------------ index ------------------------------------------------- #
156
+ def self.index database, type, name , *properties
157
+ properties = properties.map( &:to_s )
158
+ unique_requested = "unique" if properties.delete("unique")
159
+ unique_requested = "notunique" if properties.delete("notunique" )
160
+ automatic = true if
161
+ properties << name if properties.empty?
162
+ # puts " create index #{type.to_s}[#{name.to_s}] on #{type} ( #{properties.join(',')} ) #{unique_requested}"
163
+ # VV 22.10: providing an index-name raises an Error ( Encountered " "(" "( "" at line 1, column 44. Was expecting one of: <EOF> <SCHEMA> ... <NULL_STRATEGY> ... ";" ... "," ... )) )
164
+ # named indices droped for now
165
+ success = execute(database) {" create index on #{type} ( #{properties.join(',')} ) #{unique_requested}" } &.first
166
+ # puts "success: #{success}"
167
+ success && success.keys == [ :totalIndexed, :name, :operation ] && success[:operation] == 'create index'
168
+
169
+ end
170
+
171
+
172
+ # ------------------------------ transaction ------------------------------------------------- #
173
+ #
174
+ def self.begin_transaction database
175
+ result = Typhoeus.post Arcade::Config.base_uri + "begin/#{database}", auth
176
+ @session_id = result.headers["arcadedb-session-id"]
177
+
178
+ # returns the session-id
179
+ end
180
+
181
+
182
+ # ------------------------------ commit ------------------------------------------------- #
183
+ def self.commit database
184
+ options = auth.merge( headers: { "arcadedb-session-id" => session })
185
+ post_data "commit/#{database}", options
186
+ @session_id = nil
187
+ end
188
+
189
+ # ------------------------------ rollback ------------------------------------------------- #
190
+ def self.rollback database, publish_error=true
191
+ options = auth.merge( headers: { "arcadedb-session-id" => session })
192
+ post_data "rollback/#{database}", options
193
+ @session_id = nil
194
+ raise Arcade::RollbackError "A Transaction has been rolled back" if publish_error
195
+ end
196
+
197
+ private
198
+
199
+ def self.logger
200
+ Database.logger
201
+ end
202
+
203
+ def self.session
204
+ @session_id
205
+ end
206
+
207
+ def self. provide_payload( the_yield, action: :post )
208
+ unless the_yield.is_a? Hash
209
+ logger.info "Q: #{the_yield}"
210
+ the_yield = { :query => the_yield }
211
+ end
212
+ { language: 'sql' }.merge(
213
+ the_yield.map do | key, value |
214
+ case key
215
+ when :query
216
+ action == :post ? [ :command, value ] : [ :query, value ]
217
+ when :limit
218
+ [ :limit , value ]
219
+ when :params
220
+ if value.is_a? Hash
221
+ [ :params, value ]
222
+ end
223
+ # serializer (optional) specify the serializer used for the result:
224
+ # graph: returns as a graph separating vertices from edges
225
+ # record: returns everything as records
226
+ # by default it’s like record but with additional metadata for vertex records,
227
+ # such as the number of outgoing edges in @out property and total incoming edges
228
+ # in @in property. This serialzier is used by Studio
229
+ when :serializer
230
+ if [:graph, :record].include? value.to_sym
231
+ [ :serializer, value.to_sym ]
232
+ end
233
+ when :language
234
+ if [:sql, :cypher, :gremlin, :neo4j ].include? value.to_sym
235
+ [ :language, value.to_sym ]
236
+ end
237
+ end # case
238
+ end .to_h ).to_json # map
239
+ end
240
+
241
+ def self.get_data command, options = auth
242
+ result = Typhoeus.get Arcade::Config.base_uri + command, options
243
+ analyse_result(result, command)
244
+ end
245
+
246
+
247
+ def self.post_data command, options = auth
248
+ # puts "Post DATA #{command} #{options}" # debug
249
+ i = 0; a=""
250
+ loop do
251
+ result = Typhoeus.post Arcade::Config.base_uri + command, options
252
+ # Code: 503 – Service Unavailable
253
+ if result.response_code.to_i == 503 # retry two times
254
+ i += 1
255
+ raise Arcade::QueryError, JSON.parse( result.response_body, symbolize_names: true )[:result] if i > 3
256
+ sleep 0.1
257
+ else
258
+ a= analyse_result(result, command )
259
+ break
260
+ end
261
+ end
262
+ a
263
+ end
264
+
265
+ # returns the json-response
266
+ def self.analyse_result r, command
267
+ if r.success?
268
+ return nil if r.response_code == 204 # no content
269
+ result = JSON.parse( r.response_body, symbolize_names: true )[:result]
270
+ if result == [{}]
271
+ []
272
+ else
273
+ result
274
+ end
275
+ elsif r.timed_out?
276
+ raise Arcade::Error "Timeout Error", caller
277
+ []
278
+ elsif r.response_code > 0
279
+ logger.error "Execution Failure – Code: #{ r.response_code } – #{r.status_message} "
280
+ error_message = JSON.parse( r.response_body, symbolize_names: true )
281
+ logger.error "ErrorMessage: #{ error_message[:detail]} "
282
+ if error_message[:detail] =~ /Duplicated key/
283
+ raise Arcade::IndexError, error_message[:detail]
284
+ else
285
+ # available fields: :detail, :exception, error
286
+ puts error_message[:detail]
287
+ #raise error_message[:detail], caller
288
+ end
289
+ end
290
+ end
291
+ def self.auth
292
+ @a ||= { httpauth: :basic,
293
+ username: Arcade::Config.admin[:user],
294
+ password: Arcade::Config.admin[:pass] }
295
+ end
296
+
297
+ def self.json
298
+ { headers: { "Content-Type" => "application/json"} }
299
+ end
300
+ # not tested
301
+ def self.delete_data command
302
+ result = Typhoeus.delete Arcade::Config.base_uri + command, auth
303
+ analyse_result(result, command)
304
+ end
305
+ end
306
+ end
@@ -0,0 +1,5 @@
1
+ module Arcade
2
+ module Api
3
+ VERSION = 0.1
4
+ end
5
+ end