Skip to content

Commit 41b3829

Browse files
ewoutpneunhoef
authored andcommitted
Adding Views API (#144)
* Adding Views API * Implementing Views * Adding basic view tests * Adding arangosearch properties & tests * Adding view inventory support * Updating View API to 3.4rc * Added more tests. (Untested) * Fixed AQL query wrt waitForSync * Additional check * Use different view name * Added test to create view with links and then remove a collection * Expose view properties in cluster inventory * Removed View.Remove in consistency with lack of Collection.Remove * Removed filtering out arangosearch indexes (must be fixed in arangod by now) * Added test for collection in multiple views. * Renamed stuff, fixed typos. * Added another test. Style fixed. Comments added.
1 parent d4b106d commit 41b3829

12 files changed

+1225
-1
lines changed

cluster.go

+26
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,8 @@ const (
9393
type DatabaseInventory struct {
9494
// Details of all collections
9595
Collections []InventoryCollection `json:"collections,omitempty"`
96+
// Details of all views
97+
Views []InventoryView `json:"views,omitempty"`
9698
}
9799

98100
// IsReady returns true if the IsReady flag of all collections is set.
@@ -124,6 +126,17 @@ func (i DatabaseInventory) CollectionByName(name string) (InventoryCollection, b
124126
return InventoryCollection{}, false
125127
}
126128

129+
// ViewByName returns the InventoryView with given name.
130+
// Return false if not found.
131+
func (i DatabaseInventory) ViewByName(name string) (InventoryView, bool) {
132+
for _, v := range i.Views {
133+
if v.Name == name {
134+
return v, true
135+
}
136+
}
137+
return InventoryView{}, false
138+
}
139+
127140
// InventoryCollection is a single element of a DatabaseInventory, containing all information
128141
// of a specific collection.
129142
type InventoryCollection struct {
@@ -196,6 +209,19 @@ func (i InventoryIndex) FieldsEqual(fields []string) bool {
196209
return stringSliceEqualsIgnoreOrder(i.Fields, fields)
197210
}
198211

212+
// InventoryView is a single element of a DatabaseInventory, containing all information
213+
// of a specific view.
214+
type InventoryView struct {
215+
Name string `json:"name,omitempty"`
216+
Deleted bool `json:"deleted,omitempty"`
217+
ID string `json:"id,omitempty"`
218+
IsSystem bool `json:"isSystem,omitempty"`
219+
PlanID string `json:"planId,omitempty"`
220+
Type ViewType `json:"type,omitempty"`
221+
// Include all properties from an arangosearch view.
222+
ArangoSearchViewProperties
223+
}
224+
199225
// stringSliceEqualsIgnoreOrder returns true when the given lists contain the same elements.
200226
// The order of elements is irrelevant.
201227
func stringSliceEqualsIgnoreOrder(a, b []string) bool {

collection_indexes_impl.go

+6-1
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,13 @@ type indexData struct {
3838
MinLength int `json:"minLength,omitempty"`
3939
}
4040

41+
type genericIndexData struct {
42+
ID string `json:"id,omitempty"`
43+
Type string `json:"type"`
44+
}
45+
4146
type indexListResponse struct {
42-
Indexes []indexData `json:"indexes,omitempty"`
47+
Indexes []genericIndexData `json:"indexes,omitempty"`
4348
}
4449

4550
// Index opens a connection to an existing index within the collection.

database.go

+3
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ type Database interface {
4646
// Collection functions
4747
DatabaseCollections
4848

49+
// View functions
50+
DatabaseViews
51+
4952
// Graph functions
5053
DatabaseGraphs
5154

database_views.go

+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
//
2+
// DISCLAIMER
3+
//
4+
// Copyright 2018 ArangoDB GmbH, Cologne, Germany
5+
//
6+
// Licensed under the Apache License, Version 2.0 (the "License");
7+
// you may not use this file except in compliance with the License.
8+
// You may obtain a copy of the License at
9+
//
10+
// http://www.apache.org/licenses/LICENSE-2.0
11+
//
12+
// Unless required by applicable law or agreed to in writing, software
13+
// distributed under the License is distributed on an "AS IS" BASIS,
14+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
// See the License for the specific language governing permissions and
16+
// limitations under the License.
17+
//
18+
// Copyright holder is ArangoDB GmbH, Cologne, Germany
19+
//
20+
// Author Ewout Prangsma
21+
//
22+
23+
package driver
24+
25+
import "context"
26+
27+
// DatabaseViews provides access to all views in a single database.
28+
// Views are only available in ArangoDB 3.4 and higher.
29+
type DatabaseViews interface {
30+
// View opens a connection to an existing view within the database.
31+
// If no collection with given name exists, an NotFoundError is returned.
32+
View(ctx context.Context, name string) (View, error)
33+
34+
// ViewExists returns true if a view with given name exists within the database.
35+
ViewExists(ctx context.Context, name string) (bool, error)
36+
37+
// Views returns a list of all views in the database.
38+
Views(ctx context.Context) ([]View, error)
39+
40+
// CreateArangoSearchView creates a new view of type ArangoSearch,
41+
// with given name and options, and opens a connection to it.
42+
// If a view with given name already exists within the database, a ConflictError is returned.
43+
CreateArangoSearchView(ctx context.Context, name string, options *ArangoSearchViewProperties) (ArangoSearchView, error)
44+
}
45+
46+
// ViewType is the type of a view.
47+
type ViewType string
48+
49+
const (
50+
// ViewTypeArangoSearch specifies an ArangoSearch view type.
51+
ViewTypeArangoSearch = ViewType("arangosearch")
52+
)

database_views_impl.go

+157
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
//
2+
// DISCLAIMER
3+
//
4+
// Copyright 2018 ArangoDB GmbH, Cologne, Germany
5+
//
6+
// Licensed under the Apache License, Version 2.0 (the "License");
7+
// you may not use this file except in compliance with the License.
8+
// You may obtain a copy of the License at
9+
//
10+
// http://www.apache.org/licenses/LICENSE-2.0
11+
//
12+
// Unless required by applicable law or agreed to in writing, software
13+
// distributed under the License is distributed on an "AS IS" BASIS,
14+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
// See the License for the specific language governing permissions and
16+
// limitations under the License.
17+
//
18+
// Copyright holder is ArangoDB GmbH, Cologne, Germany
19+
//
20+
// Author Ewout Prangsma
21+
//
22+
23+
package driver
24+
25+
import (
26+
"context"
27+
"path"
28+
)
29+
30+
type viewInfo struct {
31+
ID string `json:"id,omitempty"`
32+
Name string `json:"name,omitempty"`
33+
Type ViewType `json:"type,omitempty"`
34+
}
35+
36+
type getViewResponse struct {
37+
Result []viewInfo `json:"result,omitempty"`
38+
}
39+
40+
// View opens a connection to an existing view within the database.
41+
// If no collection with given name exists, an NotFoundError is returned.
42+
func (d *database) View(ctx context.Context, name string) (View, error) {
43+
escapedName := pathEscape(name)
44+
req, err := d.conn.NewRequest("GET", path.Join(d.relPath(), "_api/view", escapedName))
45+
if err != nil {
46+
return nil, WithStack(err)
47+
}
48+
applyContextSettings(ctx, req)
49+
resp, err := d.conn.Do(ctx, req)
50+
if err != nil {
51+
return nil, WithStack(err)
52+
}
53+
if err := resp.CheckStatus(200); err != nil {
54+
return nil, WithStack(err)
55+
}
56+
var data viewInfo
57+
if err := resp.ParseBody("", &data); err != nil {
58+
return nil, WithStack(err)
59+
}
60+
view, err := newView(name, data.Type, d)
61+
if err != nil {
62+
return nil, WithStack(err)
63+
}
64+
return view, nil
65+
}
66+
67+
// ViewExists returns true if a view with given name exists within the database.
68+
func (d *database) ViewExists(ctx context.Context, name string) (bool, error) {
69+
escapedName := pathEscape(name)
70+
req, err := d.conn.NewRequest("GET", path.Join(d.relPath(), "_api/view", escapedName))
71+
if err != nil {
72+
return false, WithStack(err)
73+
}
74+
applyContextSettings(ctx, req)
75+
resp, err := d.conn.Do(ctx, req)
76+
if err != nil {
77+
return false, WithStack(err)
78+
}
79+
if err := resp.CheckStatus(200); err == nil {
80+
return true, nil
81+
} else if IsNotFound(err) {
82+
return false, nil
83+
} else {
84+
return false, WithStack(err)
85+
}
86+
}
87+
88+
// Views returns a list of all views in the database.
89+
func (d *database) Views(ctx context.Context) ([]View, error) {
90+
req, err := d.conn.NewRequest("GET", path.Join(d.relPath(), "_api/view"))
91+
if err != nil {
92+
return nil, WithStack(err)
93+
}
94+
applyContextSettings(ctx, req)
95+
resp, err := d.conn.Do(ctx, req)
96+
if err != nil {
97+
return nil, WithStack(err)
98+
}
99+
if err := resp.CheckStatus(200); err != nil {
100+
return nil, WithStack(err)
101+
}
102+
var data getViewResponse
103+
if err := resp.ParseBody("", &data); err != nil {
104+
return nil, WithStack(err)
105+
}
106+
result := make([]View, 0, len(data.Result))
107+
for _, info := range data.Result {
108+
view, err := newView(info.Name, info.Type, d)
109+
if err != nil {
110+
return nil, WithStack(err)
111+
}
112+
result = append(result, view)
113+
}
114+
return result, nil
115+
}
116+
117+
// CreateArangoSearchView creates a new view of type ArangoSearch,
118+
// with given name and options, and opens a connection to it.
119+
// If a view with given name already exists within the database, a ConflictError is returned.
120+
func (d *database) CreateArangoSearchView(ctx context.Context, name string, options *ArangoSearchViewProperties) (ArangoSearchView, error) {
121+
input := struct {
122+
Name string `json:"name"`
123+
Type ViewType `json:"type"`
124+
ArangoSearchViewProperties // `json:"properties"`
125+
}{
126+
Name: name,
127+
Type: ViewTypeArangoSearch,
128+
}
129+
if options != nil {
130+
input.ArangoSearchViewProperties = *options
131+
}
132+
req, err := d.conn.NewRequest("POST", path.Join(d.relPath(), "_api/view"))
133+
if err != nil {
134+
return nil, WithStack(err)
135+
}
136+
if _, err := req.SetBody(input); err != nil {
137+
return nil, WithStack(err)
138+
}
139+
applyContextSettings(ctx, req)
140+
resp, err := d.conn.Do(ctx, req)
141+
if err != nil {
142+
return nil, WithStack(err)
143+
}
144+
if err := resp.CheckStatus(201); err != nil {
145+
return nil, WithStack(err)
146+
}
147+
view, err := newView(name, input.Type, d)
148+
if err != nil {
149+
return nil, WithStack(err)
150+
}
151+
result, err := view.ArangoSearchView()
152+
if err != nil {
153+
return nil, WithStack(err)
154+
}
155+
156+
return result, nil
157+
}

test/cluster_test.go

+107
Original file line numberDiff line numberDiff line change
@@ -205,3 +205,110 @@ func TestClusterMoveShard(t *testing.T) {
205205
}
206206
}
207207
}
208+
209+
// TestClusterMoveShardWithViews tests the Cluster.MoveShard method with collection
210+
// that are being used in views.
211+
func TestClusterMoveShardWithViews(t *testing.T) {
212+
ctx := context.Background()
213+
c := createClientFromEnv(t, true)
214+
skipBelowVersion(c, "3.4", t)
215+
cl, err := c.Cluster(ctx)
216+
if driver.IsPreconditionFailed(err) {
217+
t.Skip("Not a cluster")
218+
} else {
219+
db, err := c.Database(ctx, "_system")
220+
if err != nil {
221+
t.Fatalf("Failed to open _system database: %s", describe(err))
222+
}
223+
col, err := db.CreateCollection(ctx, "test_move_shard_with_view", &driver.CreateCollectionOptions{
224+
NumberOfShards: 12,
225+
})
226+
if err != nil {
227+
t.Fatalf("CreateCollection failed: %s", describe(err))
228+
}
229+
opts := &driver.ArangoSearchViewProperties{
230+
Links: driver.ArangoSearchLinks{
231+
"test_move_shard_with_view": driver.ArangoSearchElementProperties{},
232+
},
233+
}
234+
viewName := "test_move_shard_view"
235+
if _, err := db.CreateArangoSearchView(ctx, viewName, opts); err != nil {
236+
t.Fatalf("Failed to create view '%s': %s", viewName, describe(err))
237+
}
238+
h, err := cl.Health(ctx)
239+
if err != nil {
240+
t.Fatalf("Health failed: %s", describe(err))
241+
}
242+
inv, err := cl.DatabaseInventory(ctx, db)
243+
if err != nil {
244+
t.Fatalf("DatabaseInventory failed: %s", describe(err))
245+
}
246+
if len(inv.Collections) == 0 {
247+
t.Error("Expected multiple collections, got 0")
248+
}
249+
var targetServerID driver.ServerID
250+
for id, s := range h.Health {
251+
if s.Role == driver.ServerRoleDBServer {
252+
targetServerID = id
253+
break
254+
}
255+
}
256+
if len(targetServerID) == 0 {
257+
t.Fatalf("Failed to find any dbserver")
258+
}
259+
movedShards := 0
260+
for _, colInv := range inv.Collections {
261+
if colInv.Parameters.Name == col.Name() {
262+
for shardID, dbServers := range colInv.Parameters.Shards {
263+
if dbServers[0] != targetServerID {
264+
movedShards++
265+
var rawResponse []byte
266+
if err := cl.MoveShard(driver.WithRawResponse(ctx, &rawResponse), col, shardID, dbServers[0], targetServerID); err != nil {
267+
t.Errorf("MoveShard for shard %s in collection %s failed: %s (raw response '%s' %x)", shardID, col.Name(), describe(err), string(rawResponse), rawResponse)
268+
}
269+
}
270+
}
271+
}
272+
}
273+
if movedShards == 0 {
274+
t.Fatal("Expected to have moved at least 1 shard, all seem to be on target server already")
275+
}
276+
// Wait until all shards are on the targetServerID
277+
start := time.Now()
278+
maxTestTime := time.Minute
279+
lastShardsNotOnTargetServerID := movedShards
280+
for {
281+
shardsNotOnTargetServerID := 0
282+
inv, err := cl.DatabaseInventory(ctx, db)
283+
if err != nil {
284+
t.Errorf("DatabaseInventory failed: %s", describe(err))
285+
} else {
286+
for _, colInv := range inv.Collections {
287+
if colInv.Parameters.Name == col.Name() {
288+
for shardID, dbServers := range colInv.Parameters.Shards {
289+
if dbServers[0] != targetServerID {
290+
shardsNotOnTargetServerID++
291+
t.Logf("Shard %s in on %s, wanted %s", shardID, dbServers[0], targetServerID)
292+
}
293+
}
294+
}
295+
}
296+
}
297+
if shardsNotOnTargetServerID == 0 {
298+
// We're done
299+
break
300+
}
301+
if shardsNotOnTargetServerID != lastShardsNotOnTargetServerID {
302+
// Something changed, we give a bit more time
303+
maxTestTime = maxTestTime + time.Second*15
304+
lastShardsNotOnTargetServerID = shardsNotOnTargetServerID
305+
}
306+
if time.Since(start) > maxTestTime {
307+
t.Errorf("%d shards did not move within %s", shardsNotOnTargetServerID, maxTestTime)
308+
break
309+
}
310+
t.Log("Waiting a bit")
311+
time.Sleep(time.Second * 5)
312+
}
313+
}
314+
}

0 commit comments

Comments
 (0)