orientdb_client 0.0.1

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.
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