orientdb_client 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: aeff7a5103216c207de656f5dd0addb641d2265a
4
+ data.tar.gz: 54538b3766d81b1f42d244909b302a9789d71a67
5
+ SHA512:
6
+ metadata.gz: b7338004884c5dac72cbee3816ab4e1865c1a30f4389270f081e62a055f0d951b35845e8cdebfabfa6d18fcbf9ebd13b543e2c158043ac2335854db142eb63b7
7
+ data.tar.gz: bd0fd3eebb9028e72e9de1857eeff9e4c73b826153f291b39746f97acd73bd9dff60838910def0381478549480b77eec31da3817b2b01a5b43bf9c2f68e8b919
data/.gitignore ADDED
@@ -0,0 +1,22 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ *.bundle
19
+ *.so
20
+ *.o
21
+ *.a
22
+ mkmf.log
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --require spec_helper
data/Gemfile ADDED
@@ -0,0 +1,8 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in orientdb_client.gemspec
4
+ gemspec
5
+
6
+ group :test do
7
+ gem 'curb', '~> 0.8'
8
+ end
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2015 Luke Rodgers
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,95 @@
1
+ # OrientdbClient
2
+
3
+ Ruby client for Orientdb. Probably not quite ready for production yet.
4
+ Inspired by https://github.com/veny/orientdb4r
5
+
6
+ Goals:
7
+
8
+ * speed (as much as possible with ruby)
9
+ * fine-grained handling of Orientdb errors, via rich set of ruby exceptions
10
+
11
+ Tested on:
12
+ * 2.1.5
13
+ * 2.0.6 - specs may fail due to Orientdb bug with deleting database (https://github.com/orientechnologies/orientdb/issues/3746)
14
+ * 2.0.4
15
+ * 2.0.2
16
+
17
+ ## Installation
18
+
19
+ Add this line to your application's Gemfile:
20
+
21
+ gem 'orientdb_client'
22
+
23
+ And then execute:
24
+
25
+ $ bundle
26
+
27
+ Or install it yourself as:
28
+
29
+ $ gem install orientdb_client
30
+
31
+ ## Usage
32
+
33
+ ```ruby
34
+ # basic usage
35
+ my_client = OrientdbClient.client
36
+ # connect to default Orientdb database
37
+ my_client.connect(username: 'root', password: 'YOURPASSWORD', db: 'GratefulDeadConcerts')
38
+ my_client.query('select * from V')
39
+
40
+ # create database
41
+ my_client.create_database('new_db', 'plocal', 'graph')
42
+
43
+ # use a different logger
44
+ class MyLogger
45
+ def info(message)
46
+ puts "my message: #{message}"
47
+ end
48
+ end
49
+ Orientdb::logger = MyLogger.new
50
+
51
+ # use a different HttpAdapter
52
+ require 'orientdb_client'
53
+ require 'orientdb_client/http_adapters/curb_adapter'
54
+ client = OrientdbClient.cient(adapter: 'CurbAdapter')
55
+ ```
56
+
57
+ ## HTTP Adapters
58
+
59
+ OrientdbClient currently supports Typhoeus and Curb HTTP adapters.
60
+
61
+ Benchmarks:
62
+
63
+ ```ruby
64
+ #tc is typhoeus client, cc is curb client
65
+
66
+ require 'benchmark'
67
+ Benchmark.bmbm do |x|
68
+ x.report('typhoeus') { 100.times { tc.query('select * from V') } }
69
+ x.report('curb') { 100.times { cc.query('select * from V') } }
70
+ end
71
+ Rehearsal --------------------------------------------
72
+ typhoeus 0.100000 0.010000 0.110000 ( 0.392666)
73
+ curb 0.060000 0.000000 0.060000 ( 0.347496)
74
+ ----------------------------------- total: 0.170000sec
75
+
76
+ user system total real
77
+ typhoeus 0.100000 0.010000 0.110000 ( 0.387320)
78
+ curb 0.060000 0.010000 0.070000 ( 0.331764)
79
+ ```
80
+
81
+ ## Development
82
+
83
+ Launch pry session with the gem: `rake console`, in pry use `reload!` to reload all gem files.
84
+
85
+ Run tests: `rake db:test:create` (consult `test.rb` for information on customizing auth credentials via env variables).
86
+
87
+ Turn on/off rudimentary debug mode with `client.debug = true/false`.
88
+
89
+ ## Contributing
90
+
91
+ 1. Fork it ( https://github.com/[my-github-username]/orientdb_client/fork )
92
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
93
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
94
+ 4. Push to the branch (`git push origin my-new-feature`)
95
+ 5. Create a new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,30 @@
1
+ require "bundler/gem_tasks"
2
+ require 'orientdb_client'
3
+ require 'orientdb_client/test'
4
+
5
+ task :console do
6
+ require 'pry'
7
+ require 'orientdb_client'
8
+
9
+ def reload!
10
+ files = $LOADED_FEATURES.select { |feat| feat =~ /\/orientdb_client\// }
11
+ files.each { |file| load file }
12
+ end
13
+
14
+ ARGV.clear
15
+ Pry.start
16
+ end
17
+
18
+ namespace :db do
19
+ namespace :test do
20
+ task :create do
21
+ client = OrientdbClient.client
22
+ db = OrientdbClient::Test::DatabaseName
23
+ username = OrientdbClient::Test::Username
24
+ password = OrientdbClient::Test::Password
25
+ if !client.database_exists?(db)
26
+ client.create_database(db, 'plocal', 'graph', username: username, password: password)
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,378 @@
1
+ require "orientdb_client/version"
2
+ require "orientdb_client/errors"
3
+ require "orientdb_client/http_adapters"
4
+ require "orientdb_client/http_adapters/typhoeus_adapter"
5
+ require "orientdb_client/class_configurator"
6
+
7
+ require 'oj'
8
+ require 'cgi'
9
+ require 'logger'
10
+ require 'rainbow'
11
+
12
+ module OrientdbClient
13
+ class << self
14
+ def client(options = {})
15
+ Client.new(options)
16
+ end
17
+ end
18
+
19
+ DATABASE_TYPES = ['document', 'graph']
20
+
21
+ class Client
22
+ attr_reader :http_client
23
+ attr_accessor :logger
24
+
25
+ def initialize(options)
26
+ options = {
27
+ host: 'localhost',
28
+ port: '2480'
29
+ }.merge(options)
30
+ @host = options[:host]
31
+ @port = options[:port]
32
+ adapter_klass = if options[:adapter]
33
+ HttpAdapters.const_get(options[:adapter])
34
+ else
35
+ HttpAdapters::TyphoeusAdapter
36
+ end
37
+ @http_client = adapter_klass.new
38
+ @node = Node.new(host: @host, port: @port, http_client: @http_client, client: self)
39
+ @connected = false
40
+ @logger = Logger.new(STDOUT)
41
+ self
42
+ end
43
+
44
+ def connect(username:, password:, db:)
45
+ raise ClientError.new('Already connected') if connected?
46
+ @username = username
47
+ @password = password
48
+ @db = db
49
+ @http_client.username = @username
50
+ @http_client.password = @password
51
+ @node.connect(@db)
52
+ end
53
+
54
+ def disconnect
55
+ raise ClientError.new('Not connected') unless connected?
56
+ @username = nil
57
+ @password = nil
58
+ @db = nil
59
+ @http_client.reset_credentials
60
+ @node.disconnect
61
+ end
62
+
63
+ def create_database(name, storage, type, options = {})
64
+ raise ArgumentError, "Invalid database type: #{type}" unless DATABASE_TYPES.include?(type)
65
+ @node.create_database(name, storage, type, options)
66
+ end
67
+
68
+ def delete_database(name, options = {})
69
+ @node.delete_database(name, options)
70
+ end
71
+
72
+ def create_class(name, options = {})
73
+ response = @node.create_class(name, options)
74
+ if block_given?
75
+ yield ClassConfigurator.new(name, @node)
76
+ end
77
+ response
78
+ end
79
+
80
+ def create_property(class_name, property_name, type, options = {})
81
+ @node.create_property(class_name, property_name, type, options)
82
+ end
83
+
84
+ def alter_property(class_name, property_name, field, value)
85
+ @node.alter_property(class_name, property_name, field, value)
86
+ end
87
+
88
+ def get_class(name)
89
+ @node.get_class(name)
90
+ end
91
+
92
+ def has_class?(name)
93
+ @node.has_class?(name)
94
+ end
95
+
96
+ def drop_class(name)
97
+ @node.drop_class(name)
98
+ end
99
+
100
+ def get_database(name, options = {})
101
+ @node.get_database(name, options)
102
+ end
103
+
104
+ def database_exists?(name)
105
+ list_databases.include?(name)
106
+ end
107
+
108
+ def list_databases
109
+ @node.list_databases
110
+ end
111
+
112
+ def query(sql, options = {})
113
+ @node.query(sql, options)
114
+ end
115
+
116
+ def query_unparsed(sql, options = {})
117
+ @node.query_unparsed(sql, options)
118
+ end
119
+
120
+ def command(sql)
121
+ @node.command(sql)
122
+ end
123
+
124
+ def connected?
125
+ @node.connected?
126
+ end
127
+
128
+ def database
129
+ @node.database
130
+ end
131
+
132
+ def debug=(val)
133
+ @node.debug = val
134
+ end
135
+ end
136
+
137
+ class Node
138
+
139
+ attr_reader :database
140
+ attr_writer :debug
141
+
142
+ def initialize(host:, port:, http_client: http_client, client: client)
143
+ @host = host
144
+ @port = port
145
+ @http_client = http_client
146
+ @client = client
147
+ @connected = false
148
+ @database = nil
149
+ @debug = false
150
+ end
151
+
152
+ def connect(database)
153
+ request(:get, "connect/#{database}")
154
+ @connected = true
155
+ @database = database
156
+ true
157
+ end
158
+
159
+ def disconnect
160
+ request(:get, 'disconnect') rescue UnauthorizedError
161
+ @connected = false
162
+ true
163
+ end
164
+
165
+ def create_database(name, storage, type, options)
166
+ r = request(:post, "database/#{name}/#{storage}/#{type}", options)
167
+ parse_response(r)
168
+ end
169
+
170
+ def delete_database(name, options)
171
+ r = request(:delete, "database/#{name}", options)
172
+ parse_response(r)
173
+ end
174
+
175
+ def get_database(name, options)
176
+ r = request(:get, "database/#{name}", options)
177
+ r = parse_response(r)
178
+ rescue UnauthorizedError => e
179
+ # Attempt to get not-found db, when connected, will return 401 error.
180
+ if connected?
181
+ raise NotFoundError.new("Database #{name} not found, or you are not authorized to access it.", 401)
182
+ else
183
+ raise e
184
+ end
185
+ end
186
+
187
+ def list_databases
188
+ r = request(:get, 'listDatabases')
189
+ parse_response(r)['databases']
190
+ end
191
+
192
+ def create_class(name, options)
193
+ sql = "CREATE CLASS #{name}"
194
+ sql << " EXTENDS #{options[:extends]}" if options.key?(:extends)
195
+ sql << " CLUSTER #{options[:cluster]}" if options.key?(:cluster)
196
+ sql << ' ABSTRACT' if options.key?(:abstract)
197
+ command(sql)
198
+ end
199
+
200
+ def drop_class(name)
201
+ command("DROP CLASS #{name}")
202
+ end
203
+
204
+ def create_property(class_name, property_name, type, options)
205
+ command("CREATE PROPERTY #{class_name}.#{property_name} #{type}")
206
+ options.each do |k, v|
207
+ alter_property(class_name, property_name, k, v)
208
+ end
209
+ end
210
+
211
+ def alter_property(class_name, property_name, field, value)
212
+ command("ALTER PROPERTY #{class_name}.#{property_name} #{field} #{value}")
213
+ end
214
+
215
+ def query(sql, options)
216
+ parse_response(query_unparsed(sql, options))['result']
217
+ end
218
+
219
+ def query_unparsed(sql, options)
220
+ limit = limit_string(options)
221
+ request(:get, "query/#{@database}/sql/#{CGI::escape(sql)}#{limit}")
222
+ rescue NegativeArraySizeException
223
+ raise NotFoundError
224
+ end
225
+
226
+ def command(sql)
227
+ r = request(:post, "command/#{@database}/sql/#{CGI::escape(sql)}")
228
+ parse_response(r)
229
+ end
230
+
231
+ def get_class(name)
232
+ r = request(:get, "class/#{@database}/#{name}")
233
+ parse_response(r)
234
+ rescue IllegalArgumentException
235
+ raise NotFoundError
236
+ end
237
+
238
+ def has_class?(name)
239
+ if get_class(name)
240
+ return true
241
+ end
242
+ rescue NotFoundError
243
+ return false
244
+ end
245
+
246
+ def connected?
247
+ @connected == true
248
+ end
249
+
250
+ private
251
+
252
+ def request(method, path, options = {})
253
+ url = build_url(path)
254
+ t1 = Time.now
255
+ response = @http_client.request(method, url, options)
256
+ time = Time.now - t1
257
+ r = handle_response(response)
258
+ info("request (#{time}), #{response.response_code}: #{method} #{url}")
259
+ r
260
+ end
261
+
262
+ def build_url(path)
263
+ "http://#{@host}:#{@port}/#{path}"
264
+ end
265
+
266
+ def handle_response(response)
267
+ return response if @debug
268
+ case response.response_code
269
+ when 0
270
+ raise ConnectionError.new("No server at #{@host}:#{@port}", 0, nil)
271
+ when 200, 201, 204
272
+ return response
273
+ when 401
274
+ raise UnauthorizedError.new('Unauthorized', response.response_code, response.body)
275
+ when 404
276
+ raise NotFoundError.new('Not found', response.response_code, response.body)
277
+ when 409
278
+ translate_error(response)
279
+ when 500
280
+ translate_error(response)
281
+ else
282
+ raise ServerError.new("Unexpected HTTP status code: #{response.response_code}", response.response_code, response.body)
283
+ end
284
+ end
285
+
286
+ def parse_response(response)
287
+ return nil if response.body.empty?
288
+ @debug ? response : Oj.load(response.body)
289
+ end
290
+
291
+ def limit_string(options)
292
+ options[:limit] ? "/#{options[:limit]}" : ''
293
+ end
294
+
295
+ def translate_error(response)
296
+ odb_error_class, odb_error_message = if response.content_type.start_with?('application/json')
297
+ extract_odb_error_from_json(response)
298
+ else
299
+ extract_odb_error_from_text(response)
300
+ end
301
+ code = response.response_code
302
+ body = response.body
303
+ case odb_error_class
304
+ when /OCommandSQLParsingException/
305
+ raise ClientError.new("#{odb_error_class}: #{odb_error_message}", code, body)
306
+ when /OQueryParsingException/
307
+ raise ClientError.new("#{odb_error_class}: #{odb_error_message}", code, body)
308
+ when /OCommandExecutorNotFoundException/
309
+ raise ClientError.new("#{odb_error_class}: #{odb_error_message}", code, body)
310
+ when /IllegalArgumentException/
311
+ raise IllegalArgumentException.new("#{odb_error_class}: #{odb_error_message}", code, body)
312
+ when /OConfigurationException/
313
+ raise ClientError.new("#{odb_error_class}: #{odb_error_message}", code, body)
314
+ when /OCommandExecutionException/
315
+ raise CommandExecutionException.new("#{odb_error_class}: #{odb_error_message}", code, body)
316
+ when /OSchemaException/
317
+ raise ClientError.new("#{odb_error_class}: #{odb_error_message}", code, body)
318
+ when /OConcurrentModification/
319
+ raise MVCCError.new("#{odb_error_class}: #{odb_error_message}", response.response_code, response.body)
320
+ when /IllegalStateException/
321
+ raise ServerError.new("#{odb_error_class}: #{odb_error_message}", response.response_code, response.body)
322
+ when /ORecordDuplicate/
323
+ raise DuplicateRecordError.new("#{odb_error_class}: #{odb_error_message}", response.response_code, response.body)
324
+ when /OTransactionException/
325
+ if odb_error_message.match(/ORecordDuplicate/)
326
+ raise DistributedDuplicateRecordError.new("#{odb_error_class}: #{odb_error_message}", response.response_code, response.body)
327
+ elsif odb_error_message.match(/distributed/)
328
+ raise DistributedTransactionException.new("#{odb_error_class}: #{odb_error_message}", response.response_code, response.body)
329
+ else
330
+ raise TransactionException.new("#{odb_error_class}: #{odb_error_message}", response.response_code, response.body)
331
+ end
332
+ when /ODatabaseException/
333
+ if odb_error_message.match(/already exists/)
334
+ klass = ConflictError
335
+ else
336
+ klass = ServerError
337
+ end
338
+ raise klass.new("#{odb_error_class}: #{odb_error_message}", response.response_code, response.body)
339
+ end
340
+ end
341
+
342
+ def extract_odb_error_from_json(response)
343
+ body = response.body
344
+ json = Oj.load(body)
345
+ # odb > 2.1 (?) errors are in JSON format
346
+ matches = json['errors'].first['content'].match(/\A([^:]+):\s?(.+)/m)
347
+ [matches[1], matches[2]]
348
+ rescue => e
349
+ if (response.body.match(/Database.*already exists/))
350
+ raise ConflictError.new(e.message, response.response_code, response.body)
351
+ elsif (response.body.match(/NegativeArraySizeException/))
352
+ raise NegativeArraySizeException.new(e.message, response.response_code, response.body)
353
+ else
354
+ raise OrientdbError.new("Could not parse Orientdb server error", response.response_code, response.body)
355
+ end
356
+ end
357
+
358
+ def extract_odb_error_from_text(response)
359
+ body = response.body
360
+ matches = body.match(/\A([^:]+):\s(.*)$/)
361
+ [matches[1], matches[2]]
362
+ rescue => e
363
+ if (response.body.match(/Database.*already exists/))
364
+ raise ConflictError.new(e.message, response.response_code, response.body)
365
+ elsif (response.body.match(/NegativeArraySizeException/))
366
+ raise NegativeArraySizeException.new(e.message, response.response_code, response.body)
367
+ else
368
+ raise OrientdbError.new("Could not parse Orientdb server error", response.response_code, response.body)
369
+ end
370
+ end
371
+
372
+ def info(message)
373
+ wrapped_message = "#{Rainbow('OrientdbClient:').yellow} #{message}"
374
+ @client.logger.info(wrapped_message)
375
+ end
376
+
377
+ end
378
+ end