ashikawa-core 0.4.1 → 0.5.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -29,16 +29,19 @@ module Ashikawa
29
29
  # @api public
30
30
  def initialize(database, raw_document)
31
31
  @database = database
32
- @is_persistent = raw_document.has_key?('_id') and raw_document.has_key?('rev')
33
-
34
- if @is_persistent
35
- @collection_id, @id = raw_document['_id'].split('/').map { |id| id.to_i }
36
- @revision = raw_document['_rev'].to_i
37
- end
38
-
32
+ @collection_id, @id = raw_document['_id'].split('/').map { |id| id.to_i } unless raw_document['_id'].nil?
33
+ @revision = raw_document['_rev'].to_i unless raw_document['_rev'].nil?
39
34
  @content = raw_document.delete_if { |key, value| key[0] == "_" }
40
35
  end
41
36
 
37
+ # Raises an exception if the document is not persisted
38
+ #
39
+ # @raise [DocumentNotFoundException]
40
+ # @api semi-public
41
+ def check_if_persisted!
42
+ raise DocumentNotFoundException if @id.nil?
43
+ end
44
+
42
45
  # Get the value of an attribute of the document
43
46
  #
44
47
  # @param [String] attribute_name
@@ -52,7 +55,7 @@ module Ashikawa
52
55
  #
53
56
  # @api public
54
57
  def delete
55
- raise DocumentNotFoundException unless @is_persistent
58
+ check_if_persisted!
56
59
  @database.send_request "document/#{@collection_id}/#{@id}", delete: {}
57
60
  end
58
61
 
@@ -62,7 +65,7 @@ module Ashikawa
62
65
  # @param [Object] value
63
66
  # @api public
64
67
  def []=(attribute_name, value)
65
- raise DocumentNotFoundException unless @is_persistent
68
+ check_if_persisted!
66
69
  @content[attribute_name] = value
67
70
  end
68
71
 
@@ -78,7 +81,7 @@ module Ashikawa
78
81
  #
79
82
  # @api public
80
83
  def save()
81
- raise DocumentNotFoundException unless @is_persistent
84
+ check_if_persisted!
82
85
  @database.send_request "document/#{@collection_id}/#{@id}", put: @content
83
86
  end
84
87
  end
@@ -1,8 +1,11 @@
1
1
  module Ashikawa
2
2
  module Core
3
- # This Exception is thrown, when a document was requested from
3
+ # This Exception is thrown when a document was requested from
4
4
  # the server that does not exist.
5
5
  class DocumentNotFoundException < RuntimeError
6
+ def to_s
7
+ "You requested a document from the server that does not exist"
8
+ end
6
9
  end
7
10
  end
8
11
  end
@@ -0,0 +1,11 @@
1
+ module Ashikawa
2
+ module Core
3
+ # This Exception is thrown when a Query object should execute a simple query
4
+ # but no collection was provided upon creation
5
+ class NoCollectionProvidedException < RuntimeError
6
+ def to_s
7
+ "A simple query can't be executed by a Query object without a collection"
8
+ end
9
+ end
10
+ end
11
+ end
@@ -25,15 +25,15 @@ module Ashikawa
25
25
  # Create a new Index
26
26
  #
27
27
  # @param [Collection] collection The collection the index is defined on
28
- # @param [Hash] raw_data The JSON representation of the index
28
+ # @param [Hash] raw_index The JSON representation of the index
29
29
  # @return [Index]
30
30
  # @api
31
- def initialize(collection, raw_data)
31
+ def initialize(collection, raw_index)
32
32
  @collection = collection
33
- @id = raw_data["id"].split("/")[1].to_i if raw_data["id"]
34
- @on = raw_data["fields"].map { |field| field.to_sym } if raw_data.has_key? "fields"
35
- @type = raw_data["type"].to_sym if raw_data.has_key? "type"
36
- @unique = raw_data["unique"] if raw_data.has_key? "unique"
33
+ @id = raw_index["id"].split("/")[1].to_i if raw_index.has_key? "id"
34
+ @on = raw_index["fields"].map { |field| field.to_sym } if raw_index.has_key? "fields"
35
+ @type = raw_index["type"].to_sym if raw_index.has_key? "type"
36
+ @unique = raw_index["unique"] if raw_index.has_key? "unique"
37
37
  end
38
38
 
39
39
  # Remove the index from the collection
@@ -0,0 +1,251 @@
1
+ require 'ashikawa-core/cursor'
2
+ require 'ashikawa-core/document'
3
+ require 'ashikawa-core/exceptions/no_collection_provided'
4
+ require 'forwardable'
5
+
6
+ module Ashikawa
7
+ module Core
8
+ # Formulate a Query on a collection or on a database
9
+ class Query
10
+ extend Forwardable
11
+
12
+ # Delegate sending requests to the connection
13
+ delegate send_request: :@connection
14
+
15
+ # Initializes a Query
16
+ #
17
+ # @param [Collection, Database] connection
18
+ # @return [Query]
19
+ def initialize(connection)
20
+ @connection = connection
21
+ end
22
+
23
+ # Retrieves all documents for a collection
24
+ #
25
+ # @note It is advised to NOT use this method due to possible HUGE data amounts requested
26
+ # @option options [Integer] :limit limit the maximum number of queried and returned elements.
27
+ # @option options [Integer] :skip skip the first <n> documents of the query.
28
+ # @return [Cursor]
29
+ # @raise [NoCollectionProvidedException] If you provided a database, no collection
30
+ # @api public
31
+ # @example Get an array with all documents
32
+ # query = Ashikawa::Core::Query.new collection
33
+ # query.all # => #<Cursor id=33>
34
+ def all(options={})
35
+ simple_query_request "/simple/all",
36
+ options,
37
+ [:limit, :skip]
38
+ end
39
+
40
+ # Looks for documents in a collection which match the given criteria
41
+ #
42
+ # @option example [Hash] a Hash with data matching the documents you are looking for.
43
+ # @option options [Hash] a Hash with additional settings for the query.
44
+ # @option options [Integer] :limit limit the maximum number of queried and returned elements.
45
+ # @option options [Integer] :skip skip the first <n> documents of the query.
46
+ # @return [Cursor]
47
+ # @raise [NoCollectionProvidedException] If you provided a database, no collection
48
+ # @api public
49
+ # @example Find all documents in a collection that are red
50
+ # query = Ashikawa::Core::Query.new collection
51
+ # query.by_example { "color" => "red" }, :options => { :limit => 1 } # => #<Cursor id=2444>
52
+ def by_example(example={}, options={})
53
+ simple_query_request "/simple/by-example",
54
+ { example: example }.merge(options),
55
+ [:limit, :skip, :example]
56
+ end
57
+
58
+ # Looks for one document in a collection which matches the given criteria
59
+ #
60
+ # @param [Hash] example a Hash with data matching the document you are looking for.
61
+ # @return [Document]
62
+ # @raise [NoCollectionProvidedException] If you provided a database, no collection
63
+ # @api public
64
+ # @example Find one document in a collection that is red
65
+ # query = Ashikawa::Core::Query.new collection
66
+ # query.first_example { "color" => "red"} # => #<Document id=2444 color="red">
67
+ def first_example(example = {})
68
+ response = simple_query_request "/simple/first-example",
69
+ { example: example },
70
+ [:example]
71
+ response.first
72
+ end
73
+
74
+ # Looks for documents in a collection based on location
75
+ #
76
+ # @option options [Integer] :latitude Latitude location for your search.
77
+ # @option options [Integer] :longitude Longitude location for your search.
78
+ # @option options [Integer] :skip The documents to skip in the query.
79
+ # @option options [Integer] :distance If given, the attribute key used to store the distance.
80
+ # @option options [Integer] :limit The maximal amount of documents to return (default: 100).
81
+ # @option options [Integer] :geo If given, the identifier of the geo-index to use.
82
+ # @return [Cursor]
83
+ # @raise [NoCollectionProvidedException] If you provided a database, no collection
84
+ # @api public
85
+ # @example Find all documents at Infinite Loop
86
+ # query = Ashikawa::Core::Query.new collection
87
+ # query.near latitude: 37.331693, longitude: -122.030468
88
+ def near(options={})
89
+ simple_query_request "/simple/near",
90
+ options,
91
+ [:latitude, :longitude, :distance, :skip, :limit, :geo]
92
+ end
93
+
94
+ # Looks for documents in a collection within a radius
95
+ #
96
+ # @option options [Integer] :latitude Latitude location for your search.
97
+ # @option options [Integer] :longitude Longitude location for your search.
98
+ # @option options [Integer] :radius Radius around the given location you want to search in.
99
+ # @option options [Integer] :skip The documents to skip in the query.
100
+ # @option options [Integer] :distance If given, the attribute key used to store the distance.
101
+ # @option options [Integer] :limit The maximal amount of documents to return (default: 100).
102
+ # @option options [Integer] :geo If given, the identifier of the geo-index to use.
103
+ # @return [Cursor]
104
+ # @api public
105
+ # @raise [NoCollectionProvidedException] If you provided a database, no collection
106
+ # @example Find all documents within a radius of 100 to Infinite Loop
107
+ # query = Ashikawa::Core::Query.new collection
108
+ # query.within latitude: 37.331693, longitude: -122.030468, radius: 100
109
+ def within(options={})
110
+ simple_query_request "/simple/within",
111
+ options,
112
+ [:latitude, :longitude, :radius, :distance, :skip, :limit, :geo]
113
+ end
114
+
115
+ # Looks for documents in a collection with an attribute between two values
116
+ #
117
+ # @option options [Integer] :attribute The attribute path to check.
118
+ # @option options [Integer] :left The lower bound
119
+ # @option options [Integer] :right The upper bound
120
+ # @option options [Integer] :closed If true, use intervall including left and right, otherwise exclude right, but include left.
121
+ # @option options [Integer] :skip The documents to skip in the query (optional).
122
+ # @option options [Integer] :limit The maximal amount of documents to return (optional).
123
+ # @return [Cursor]
124
+ # @raise [NoCollectionProvidedException] If you provided a database, no collection
125
+ # @api public
126
+ # @example Find all documents within a radius of 100 to Infinite Loop
127
+ # query = Ashikawa::Core::Query.new collection
128
+ # query.within latitude: 37.331693, longitude: -122.030468, radius: 100
129
+ def in_range(options={})
130
+ simple_query_request "/simple/range",
131
+ options,
132
+ [:attribute, :left, :right, :closed, :limit, :skip]
133
+ end
134
+
135
+ # Send an AQL query to the database
136
+ #
137
+ # @param [String] query
138
+ # @option options [Integer] :count Should the number of results be counted?
139
+ # @option options [Integer] :batch_size Set the number of results returned at once
140
+ # @return [Cursor]
141
+ # @api public
142
+ # @example Send an AQL query to the database
143
+ # query = Ashikawa::Core::Query.new collection
144
+ # query.execute "FOR u IN users LIMIT 2" # => #<Cursor id=33>
145
+ def execute(query, options = {})
146
+ post_request "/cursor",
147
+ options.merge({ query: query }),
148
+ [:query, :count, :batch_size]
149
+ end
150
+
151
+ # Test if an AQL query is valid
152
+ #
153
+ # @param [String] query
154
+ # @return [Boolean]
155
+ # @api public
156
+ # @example Validate an AQL query
157
+ # query = Ashikawa::Core::Query.new collection
158
+ # query.valid? "FOR u IN users LIMIT 2" # => true
159
+ def valid?(query)
160
+ begin
161
+ !!post_request("/query", { query: query })
162
+ rescue RestClient::BadRequest
163
+ false
164
+ end
165
+ end
166
+
167
+ private
168
+
169
+ # The database object
170
+ #
171
+ # @return [Database]
172
+ # @api private
173
+ def database
174
+ @connection.respond_to?(:database) ? @connection.database : @connection
175
+ end
176
+
177
+ # The collection object
178
+ #
179
+ # @return [collection]
180
+ # @api private
181
+ def collection
182
+ raise NoCollectionProvidedException unless @connection.respond_to? :database
183
+ @connection
184
+ end
185
+
186
+ # Removes the keys that are not allowed from an object
187
+ #
188
+ # @param [Hash] options
189
+ # @param [Array<Symbol>] allowed_keys
190
+ # @return [Hash] The filtered Hash
191
+ # @api private
192
+ def allowed_options(options, allowed_keys)
193
+ options.keep_if { |key, _| allowed_keys.include? key }
194
+ end
195
+
196
+ # Transforms the keys into strings, camelizes them and removes pairs without a value
197
+ #
198
+ # @param [Hash] request_data
199
+ # @return [Hash] Cleaned request data
200
+ # @api private
201
+ def prepare_request_data(request_data)
202
+ Hash[request_data.map { |key, value|
203
+ [key.to_s.gsub(/_(.)/) { $1.upcase }, value]
204
+ }].reject { |_, value| value.nil? }
205
+ end
206
+
207
+ # Send a simple query to the server
208
+ #
209
+ # @param [String] path The path for the request
210
+ # @param [Hash] request_data The data send to the database
211
+ # @param [Array<Symbol>] keys The keys allowed for this request
212
+ # @return [String] Server response
213
+ # @raise [NoCollectionProvidedException] If you provided a database, no collection
214
+ # @api private
215
+ def simple_query_request(path, request_data, allowed_keys)
216
+ request_data = request_data.merge({ collection: collection.name })
217
+ put_request path,
218
+ request_data,
219
+ allowed_keys << :collection
220
+ end
221
+
222
+ # Perform a put request
223
+ #
224
+ # @param [String] path The path for the request
225
+ # @param [Hash] request_data The data send to the database
226
+ # @param [Array] allowed_keys Keys allowed in request_data, if nil: All keys are allowed
227
+ # @return [Cursor]
228
+ # @api private
229
+ def put_request(path, request_data, allowed_keys = nil)
230
+ request_data = allowed_options request_data, allowed_keys unless allowed_keys.nil?
231
+ request_data = prepare_request_data request_data
232
+ server_response = send_request path, :put => request_data
233
+ Cursor.new database, server_response
234
+ end
235
+
236
+ # Perform a post request
237
+ #
238
+ # @param [String] path The path for the request
239
+ # @param [Hash] request_data The data send to the database
240
+ # @param [Array] allowed_keys Keys allowed in request_data, if nil: All keys are allowed
241
+ # @return [Cursor]
242
+ # @api private
243
+ def post_request(path, request_data, allowed_keys = nil)
244
+ request_data = allowed_options request_data, allowed_keys unless allowed_keys.nil?
245
+ request_data = prepare_request_data request_data
246
+ server_response = send_request path, :post => request_data
247
+ Cursor.new database, server_response
248
+ end
249
+ end
250
+ end
251
+ end
@@ -1,6 +1,6 @@
1
1
  module Ashikawa
2
2
  module Core
3
3
  # Current version of Ashikawa::Core
4
- VERSION = "0.4.1"
4
+ VERSION = "0.5.1"
5
5
  end
6
6
  end
@@ -1,7 +1,7 @@
1
1
  RSpec.configure do |config|
2
2
  raise "Could not find arangod. Please install it or check if it is in your path." if `which arangod` == ""
3
3
 
4
- database_directory = "/tmp/ashikawa-integration"
4
+ database_directory = "/tmp/ashikawa-acceptance"
5
5
  arango_process = false
6
6
 
7
7
  config.before(:suite) do
@@ -1,11 +1,11 @@
1
- require 'integration/spec_helper'
1
+ require 'acceptance/spec_helper'
2
2
 
3
3
  describe "Basics" do
4
4
  subject { ARANGO_HOST }
5
5
 
6
- it "should have booted up an ArangoDB instance" do
7
- expect { RestClient.get(subject) }.to_not raise_error
8
- end
6
+ # it "should have booted up an ArangoDB instance" do
7
+ # expect { RestClient.get(subject) }.to_not raise_error
8
+ # end
9
9
 
10
10
  describe "initialized database" do
11
11
  subject { Ashikawa::Core::Database.new ARANGO_HOST }
@@ -1,4 +1,4 @@
1
- require 'integration/spec_helper'
1
+ require 'acceptance/spec_helper'
2
2
 
3
3
  describe "Indices" do
4
4
  let(:database) { Ashikawa::Core::Database.new ARANGO_HOST }
@@ -0,0 +1,90 @@
1
+ require 'acceptance/spec_helper'
2
+
3
+ describe "Queries" do
4
+ let(:database) { Ashikawa::Core::Database.new ARANGO_HOST }
5
+ let(:collection) { database["my_collection"] }
6
+
7
+ describe "AQL query via the database" do
8
+ it "should return the documents" do
9
+ collection << { "name" => "Jeff Lebowski", "bowling" => true }
10
+ collection << { "name" => "Walter Sobchak", "bowling" => true }
11
+ collection << { "name" => "Donny Kerabatsos", "bowling" => true }
12
+ collection << { "name" => "Jeffrey Lebowski", "bowling" => false }
13
+
14
+ query = "FOR u IN my_collection FILTER u.bowling == true RETURN u"
15
+ results = database.query.execute query, batch_size: 2, count: true
16
+
17
+ results.length.should == 3
18
+ results = results.map { |person| person["name"] }
19
+ results.should include "Jeff Lebowski"
20
+ results.should_not include "Jeffrey Lebowski"
21
+ end
22
+
23
+ it "should be possible to validate" do
24
+ valid_query = "FOR u IN my_collection FILTER u.bowling == true RETURN u"
25
+ database.query.valid?(valid_query).should be_true
26
+
27
+ invalid_query = "FOR u IN my_collection FILTER u.bowling == true"
28
+ database.query.valid?(invalid_query).should be_false
29
+ end
30
+ end
31
+
32
+ describe "simple query via collection object" do
33
+ subject { collection }
34
+ before(:each) { subject.truncate! }
35
+
36
+ it "should return all documents of a collection" do
37
+ subject << { name: "testname", age: 27}
38
+ subject.query.all.first["name"].should == "testname"
39
+ end
40
+
41
+ it "should be possible to limit and skip results" do
42
+ subject << { name: "test1"}
43
+ subject << { name: "test2"}
44
+ subject << { name: "test3"}
45
+
46
+ subject.query.all(limit: 2).length.should == 2
47
+ subject.query.all(skip: 2).length.should == 1
48
+ end
49
+
50
+ it "should be possible to query documents by example" do
51
+ subject << { "name" => "Random Document" }
52
+ result = subject.query.by_example name: "Random Document"
53
+ result.length.should == 1
54
+ end
55
+
56
+ describe "query by geo coordinates" do
57
+ before :each do
58
+ subject.add_index :geo, on: [:latitude, :longitude]
59
+ subject << { "name" => "cologne", "latitude" => 50.948045, "longitude" => 6.961212 }
60
+ subject << { "name" => "san francisco", "latitude" => -122.395899, "longitude" => 37.793621 }
61
+ end
62
+
63
+ it "should be possible to query documents near a certain location" do
64
+ found_places = subject.query.near latitude: 50, longitude: 6
65
+ found_places.first["name"].should == "cologne"
66
+ end
67
+
68
+ it "should be possible to query documents within a certain range" do
69
+ found_places = subject.query.within latitude: 50.948040, longitude: 6.961210, radius: 2
70
+ found_places.length.should == 1
71
+ found_places.first["name"].should == "cologne"
72
+ end
73
+ end
74
+
75
+ describe "queries by integer ranges" do
76
+ before :each do
77
+ subject.add_index :skiplist, on: [:age]
78
+ subject << { "name" => "Georg", "age" => 12 }
79
+ subject << { "name" => "Anne", "age" => 21 }
80
+ subject << { "name" => "Jens", "age" => 49 }
81
+ end
82
+
83
+ it "should be possible to query documents for numbers in a certain range" do
84
+ found_people = subject.query.in_range attribute: "age", left: 20, right: 30, closed: true
85
+ found_people.length.should == 1
86
+ found_people.first["name"].should == "Anne"
87
+ end
88
+ end
89
+ end
90
+ end
File without changes
@@ -1,7 +1,7 @@
1
1
  RSpec.configure do |config|
2
2
  raise "Could not find arangod. Please install it or check if it is in your path." if `which arangod` == ""
3
3
 
4
- database_directory = "/tmp/ashikawa-integration-auth"
4
+ database_directory = "/tmp/ashikawa-acceptance-auth"
5
5
  arango_process = false
6
6
 
7
7
  config.before(:suite) do
@@ -1,4 +1,4 @@
1
- require 'integration/spec_helper'
1
+ require 'acceptance_auth/spec_helper'
2
2
 
3
3
  describe "authenticated database" do
4
4
  subject { ARANGO_HOST }
@@ -0,0 +1,5 @@
1
+ {
2
+ "error": false,
3
+ "bindVars": [],
4
+ "code": 200
5
+ }
@@ -0,0 +1,68 @@
1
+ #!/bin/bash
2
+
3
+ DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
4
+ cd $DIR
5
+
6
+ #VERSION=1.0.2
7
+ VERSION=1.1.beta1
8
+
9
+ NAME=ArangoDB-$VERSION
10
+
11
+ if [ ! -d "$DIR/$NAME" ]; then
12
+ # download ArangoDB
13
+ wget http://www.arangodb.org/travisCI/$NAME.tar.gz
14
+ tar zxf $NAME.tar.gz
15
+ fi
16
+
17
+
18
+ PID=$(echo $PPID)
19
+ TMP_DIR="/tmp/arangodb.$PID"
20
+ PID_FILE="/tmp/arangodb.$PID.pid"
21
+ ARANGODB_DIR="$DIR/$NAME"
22
+ UPDATE_SCRIPT="${ARANGODB_DIR}/js/server/arango-upgrade.js"
23
+
24
+ # create database directory
25
+ mkdir ${TMP_DIR}
26
+
27
+ # check for update script
28
+ echo "looking for: $UPDATE_SCRIPT"
29
+ if [ -f "$UPDATE_SCRIPT" ] ; then
30
+ # version 1.1
31
+ ${ARANGODB_DIR}/bin/arangod \
32
+ --database.directory ${TMP_DIR} \
33
+ --configuration none \
34
+ --server.endpoint tcp://127.0.0.1:8529 \
35
+ --javascript.startup-directory ${ARANGODB_DIR}/js \
36
+ --javascript.modules-path ${ARANGODB_DIR}/js/server/modules:${ARANGODB_DIR}/js/common/modules \
37
+ --javascript.script "$UPDATE_SCRIPT"
38
+
39
+ ${ARANGODB_DIR}/bin/arangod \
40
+ --database.directory ${TMP_DIR} \
41
+ --configuration none \
42
+ --server.endpoint tcp://127.0.0.1:8529 \
43
+ --javascript.startup-directory ${ARANGODB_DIR}/js \
44
+ --javascript.modules-path ${ARANGODB_DIR}/js/server/modules:${ARANGODB_DIR}/js/common/modules \
45
+ --javascript.action-directory ${ARANGODB_DIR}/js/actions/system \
46
+ --database.maximal-journal-size 1048576 \
47
+ --server.disable-admin-interface true \
48
+ --server.disable-authentication true \
49
+ --javascript.gc-interval 1 &
50
+ else
51
+ # version 1.0
52
+ ${ARANGODB_DIR}/bin/arangod ${TMP_DIR} \
53
+ --configuration none \
54
+ --pid-file ${PID_FILE} \
55
+ --javascript.startup-directory ${ARANGODB_DIR}/js \
56
+ --javascript.modules-path ${ARANGODB_DIR}/js/server/modules:${ARANGODB_DIR}/js/common/modules \
57
+ --javascript.action-directory ${ARANGODB_DIR}/js/actions/system \
58
+ --database.maximal-journal-size 1000000 \
59
+ --javascript.gc-interval 1 &
60
+ fi
61
+
62
+ echo "Waiting until ArangoDB is ready on port 8529"
63
+ while [[ -z `curl -s 'http://127.0.0.1:8529/_api/version' ` ]] ; do
64
+ echo -n "."
65
+ sleep 2s
66
+ done
67
+
68
+ echo "ArangoDB is up"