arcadedb 0.3.1 → 0.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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