Repository: cayleygraph/cayley
Pull request #751 (2018-12-09 18:33:08)

Add sqlite backend


<!-- Reviewable:start -->
This change is [<img src="https://reviewable.io/review_button.svg" height="34" align="absmiddle" alt="Reviewable"/>](https://reviewable.io/reviews/cayleygraph/cayley/751)
<!-- Reviewable:end -->


BrendanBall: Thanks for the awesome response time :D . I've just pushed some docs. Let me know if you'd like to add anything else.
dennwc: @BrendanBall That might explain why they fail for the same SQL implementation that works for PG and MySQL.

I was confused by `Unexpected number of results, got:0 expect:2` without any errors, but that's probably a problem with context error propagation in iterators.

If setting a higher timeout helps, I will consider this solved.

Sure, we might need some optimizations for SQLite to be faster for those kinds of queries, but it's worth merging in the current state as well.
BrendanBall: the test `TestSqlite/qs/integration/three_huge_sets_with_small_intersection` didn't even complete within an hour :/ . So much for quickly adding a sqlite backend :P
dennwc: OK, I would say that the fact that backend is slow on integration test shouldn't block this PR. We can make some optimizations later, if necessary.

I will check the PR again and merge it if there are no objections.

Patch:
diff --git a/AUTHORS b/AUTHORS
index 4835189..ef938bd 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -11,6 +11,7 @@
 Alexander Peters <info@alexanderpeters.de>
 Andrew Dunham <andrew@du.nham.ca>
 Bram Leenders <bcleenders@gmail.com>
+Brendan Ball <ball.brendan50@gmail.com>
 Denys Smirnov <denis.smirnov.91@gmail.com>
 Derek Liang <fr.derekliang@gmail.com>
 Google Inc.
diff --git a/CONTRIBUTORS b/CONTRIBUTORS
index 1a0c9c9..4328481 100644
--- a/CONTRIBUTORS
+++ b/CONTRIBUTORS
@@ -11,6 +11,7 @@ Alexander Peters <info@alexanderpeters.de>
 Andrew Dunham <andrew@du.nham.ca>
 Barak Michener <barakmich@google.com> <barak@cayley.io> <me@barakmich.com>
 Bram Leenders <bcleenders@gmail.com>
+Brendan Ball <ball.brendan50@gmail.com>
 Connor Newton <connor@ifthenelse.io>
 Denys Smirnov <denis.smirnov.91@gmail.com>
 Derek Liang <fr.derekliang@gmail.com>
diff --git a/README.md b/README.md
index 2df4a9c..1d07f15 100644
--- a/README.md
+++ b/README.md
@@ -9,7 +9,7 @@ Its goal is to be a part of the developer's toolbox where [Linked Data](http://l
 [![Build Status](https://travis-ci.org/cayleygraph/cayley.svg?branch=master)](https://travis-ci.org/cayleygraph/cayley)
 [![Container Repository on Quay](https://quay.io/repository/cayleygraph/cayley/status "Container Repository on Quay")](https://quay.io/repository/cayleygraph/cayley)
 
-[![Slack Status](https://cayley-slackin.herokuapp.com/badge.svg)](https://cayley-slackin.herokuapp.com/)
+[![Get it from the Snap Store](https://snapcraft.io/static/images/badges/en/snap-store-white.svg)](https://snapcraft.io/cayley)
 
 ## Features
 
@@ -27,7 +27,7 @@ Its goal is to be a part of the developer's toolbox where [Linked Data](http://l
 * Plays well with multiple backend stores:
   * KVs: [Bolt](https://github.com/boltdb/bolt), [LevelDB](https://github.com/google/leveldb)
   * NoSQL: [MongoDB](https://www.mongodb.org), [ElasticSearch](https://www.elastic.co/products/elasticsearch), [CouchDB](http://couchdb.apache.org/)/[PouchDB](https://pouchdb.com/)
-  * SQL: [PostgreSQL](http://www.postgresql.org), [CockroachDB](https://www.cockroachlabs.com), [MySQL](https://www.mysql.com)
+  * SQL: [PostgreSQL](http://www.postgresql.org), [CockroachDB](https://www.cockroachlabs.com), [MySQL](https://www.mysql.com), [SQLite](https://www.sqlite.org)
   * In-memory, ephemeral
 * Modular design; easy to extend with new languages and backends
 * Good test coverage
diff --git a/docs/Configuration.md b/docs/Configuration.md
index 4c01652..e67c7e1 100644
--- a/docs/Configuration.md
+++ b/docs/Configuration.md
@@ -45,6 +45,7 @@ All command line flags take precedence over the configuration file.
   * `postgres`: Stores the graph data and indices in a [PostgreSQL](https://www.postgresql.org) instance.
   * `cockroach`: Stores the graph data and indices in a [CockroachDB](https://www.cockroachlabs.com/product/cockroachdb/) cluster.
   * `mysql`: Stores the graph data and indices in a [MySQL](https://www.mysql.com/) or [MariaDB](https://mariadb.org/) instance.
+  * `sqlite`: Stores the graph data and indices in a [SQLite](https://www.sqlite.org) database.
 
 #### **`store.address`**
 
@@ -62,6 +63,7 @@ All command line flags take precedence over the configuration file.
   * `couch`: `http://user:pass@host:port/dbname` of the desired CouchDB server.
   * `postgres`,`cockroach`: `postgres://[username:password@]host[:port]/database-name?sslmode=disable` of the PostgreSQL database and credentials. Sslmode is optional. More option available on [pq](https://godoc.org/github.com/lib/pq) page.
   * `mysql`: `[username:password@]tcp(host[:3306])/database-name` of the MqSQL database and credentials. More option available on [driver](https://github.com/go-sql-driver/mysql#dsn-data-source-name) page.
+  * `sqlite`: `filepath` of the SQLite database. More options available on [driver](https://github.com/mattn/go-sqlite3#connection-string) page.
 
 #### **`store.read_only`**
 
diff --git a/go.mod b/go.mod
index 2fe0ec0..cef8673 100644
--- a/go.mod
+++ b/go.mod
@@ -36,6 +36,7 @@ require (
 	github.com/lib/pq v0.0.0-20170324204654-2704adc878c2
 	github.com/linkeddata/gojsonld v0.0.0-20170418210642-4f5db6791326
 	github.com/magiconair/properties v0.0.0-20170321093039-51463bfca257 // indirect
+	github.com/mattn/go-sqlite3 v1.10.0
 	github.com/mitchellh/mapstructure v0.0.0-20170422000251-cc8532a8e9a5 // indirect
 	github.com/onsi/ginkgo v1.7.0 // indirect
 	github.com/onsi/gomega v1.4.3 // indirect
diff --git a/go.sum b/go.sum
index 5cf4ff2..8e92c75 100644
--- a/go.sum
+++ b/go.sum
@@ -99,6 +99,8 @@ github.com/linkeddata/gojsonld v0.0.0-20170418210642-4f5db6791326 h1:YP3lfXXYiQV
 github.com/linkeddata/gojsonld v0.0.0-20170418210642-4f5db6791326/go.mod h1:nfqkuSNlsk1bvti/oa7TThx4KmRMBmSxf3okHI9wp3E=
 github.com/magiconair/properties v0.0.0-20170321093039-51463bfca257 h1:nqPtTOaJx2zFKWinLXKQYQRfxXTZN3GoJ602Uo65VGY=
 github.com/magiconair/properties v0.0.0-20170321093039-51463bfca257/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
+github.com/mattn/go-sqlite3 v1.10.0 h1:jbhqpg7tQe4SupckyijYiy0mJJ/pRyHvXf7JdWK860o=
+github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
 github.com/mitchellh/mapstructure v0.0.0-20170422000251-cc8532a8e9a5 h1:AjVRQelY0a5wq4bUgs08/+iblph90u9H58uzaH7Cw3g=
 github.com/mitchellh/mapstructure v0.0.0-20170422000251-cc8532a8e9a5/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
 github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
diff --git a/graph/all/all.go b/graph/all/all.go
index fb496fc..3e01d48 100644
--- a/graph/all/all.go
+++ b/graph/all/all.go
@@ -12,4 +12,5 @@ import (
 	_ "github.com/cayleygraph/cayley/graph/sql/cockroach"
 	_ "github.com/cayleygraph/cayley/graph/sql/mysql"
 	_ "github.com/cayleygraph/cayley/graph/sql/postgres"
+	_ "github.com/cayleygraph/cayley/graph/sql/sqlite"
 )
diff --git a/graph/path/morphism_apply_functions.go b/graph/path/morphism_apply_functions.go
index 7375cbd..ec58868 100644
--- a/graph/path/morphism_apply_functions.go
+++ b/graph/path/morphism_apply_functions.go
@@ -338,7 +338,7 @@ func saveReverseMorphism(via interface{}, tag string) morphism {
 	return morphism{
 		Reversal: func(ctx *pathContext) (morphism, *pathContext) { return saveReverseMorphism(via, tag), ctx },
 		Apply: func(in shape.Shape, ctx *pathContext) (shape.Shape, *pathContext) {
-			return shape.SaveVia(in, buildVia(via), tag, true, false), ctx
+			return shape.SaveViaLabels(in, buildVia(via), ctx.labelSet, tag, true, false), ctx
 		},
 		tags: []string{tag},
 	}
@@ -348,7 +348,7 @@ func saveOptionalMorphism(via interface{}, tag string) morphism {
 	return morphism{
 		Reversal: func(ctx *pathContext) (morphism, *pathContext) { return saveOptionalMorphism(via, tag), ctx },
 		Apply: func(in shape.Shape, ctx *pathContext) (shape.Shape, *pathContext) {
-			return shape.SaveVia(in, buildVia(via), tag, false, true), ctx
+			return shape.SaveViaLabels(in, buildVia(via), ctx.labelSet, tag, false, true), ctx
 		},
 		tags: []string{tag},
 	}
@@ -358,7 +358,7 @@ func saveOptionalReverseMorphism(via interface{}, tag string) morphism {
 	return morphism{
 		Reversal: func(ctx *pathContext) (morphism, *pathContext) { return saveOptionalReverseMorphism(via, tag), ctx },
 		Apply: func(in shape.Shape, ctx *pathContext) (shape.Shape, *pathContext) {
-			return shape.SaveVia(in, buildVia(via), tag, true, true), ctx
+			return shape.SaveViaLabels(in, buildVia(via), ctx.labelSet, tag, true, true), ctx
 		},
 		tags: []string{tag},
 	}
diff --git a/graph/sql/database.go b/graph/sql/database.go
index e17cfcc..e017324 100644
--- a/graph/sql/database.go
+++ b/graph/sql/database.go
@@ -116,15 +116,15 @@ func (r Registration) quadIndexes(options graph.Options) []string {
 	if r.FillFactor {
 		factor, _ := options.IntKey("db_fill_factor", 50)
 		indexes = append(indexes,
-			fmt.Sprintf(`CREATE INDEX spo_index ON quads (subject_hash) WITH (FILLFACTOR = %d);`, factor),
-			fmt.Sprintf(`CREATE INDEX pos_index ON quads (predicate_hash) WITH (FILLFACTOR = %d);`, factor),
-			fmt.Sprintf(`CREATE INDEX osp_index ON quads (object_hash) WITH (FILLFACTOR = %d);`, factor),
+			fmt.Sprintf(`CREATE INDEX ops_index ON quads (object_hash, predicate_hash, subject_hash) WITH (FILLFACTOR = %d);`, factor),
+			fmt.Sprintf(`CREATE INDEX pos_index ON quads (predicate_hash, object_hash, subject_hash) WITH (FILLFACTOR = %d);`, factor),
+			fmt.Sprintf(`CREATE INDEX osp_index ON quads (object_hash, subject_hash, predicate_hash) WITH (FILLFACTOR = %d);`, factor),
 		)
 	} else {
 		indexes = append(indexes,
-			`CREATE INDEX spo_index ON quads (subject_hash);`,
-			`CREATE INDEX pos_index ON quads (predicate_hash);`,
-			`CREATE INDEX osp_index ON quads (object_hash);`,
+			`CREATE INDEX ops_index ON quads (object_hash, predicate_hash, subject_hash);`,
+			`CREATE INDEX pos_index ON quads (predicate_hash, object_hash, subject_hash);`,
+			`CREATE INDEX osp_index ON quads (object_hash, subject_hash, predicate_hash);`,
 		)
 	}
 	return indexes
diff --git a/graph/sql/sqlite/sqlite.go b/graph/sql/sqlite/sqlite.go
new file mode 100644
index 0000000..e5bc3c2
--- /dev/null
+++ b/graph/sql/sqlite/sqlite.go
@@ -0,0 +1,151 @@
+package sqlite
+
+import (
+	"database/sql"
+	"fmt"
+	"regexp"
+	"strings"
+
+	"github.com/cayleygraph/cayley/clog"
+	"github.com/cayleygraph/cayley/graph"
+	graphlog "github.com/cayleygraph/cayley/graph/log"
+	csql "github.com/cayleygraph/cayley/graph/sql"
+	"github.com/cayleygraph/cayley/quad"
+	sqlite3 "github.com/mattn/go-sqlite3"
+)
+
+const Type = "sqlite"
+
+var QueryDialect = csql.QueryDialect{
+	RegexpOp: "REGEXP",
+	FieldQuote: func(name string) string {
+		return "`" + name + "`"
+	},
+	Placeholder: func(n int) string { return "?" },
+}
+
+func init() {
+	regex := func(re, s string) (bool, error) {
+		return regexp.MatchString(re, s)
+	}
+	sql.Register("sqlite3-regexp",
+		&sqlite3.SQLiteDriver{
+			ConnectHook: func(conn *sqlite3.SQLiteConn) error {
+				return conn.RegisterFunc("regexp", regex, true)
+			},
+		})
+	csql.Register(Type, csql.Registration{
+		Driver:               "sqlite3-regexp",
+		HashType:             fmt.Sprintf(`BINARY(%d)`, quad.HashSize),
+		BytesType:            `BLOB`,
+		HorizonType:          `INTEGER`,
+		TimeType:             `DATETIME`,
+		QueryDialect:         QueryDialect,
+		NoOffsetWithoutLimit: true,
+		NoForeignKeys:        true,
+		Error: func(err error) error {
+			return err
+		},
+		Estimated: nil,
+		RunTx:     runTxSqlite,
+	})
+}
+
+func runTxSqlite(tx *sql.Tx, nodes []graphlog.NodeUpdate, quads []graphlog.QuadUpdate, opts graph.IgnoreOpts) error {
+	// update node ref counts and insert nodes
+	var (
+		// prepared statements for each value type
+		insertValue = make(map[csql.ValueType]*sql.Stmt)
+		updateValue *sql.Stmt
+	)
+	for _, n := range nodes {
+		if n.RefInc >= 0 {
+			nodeKey, values, err := csql.NodeValues(csql.NodeHash{n.Hash}, n.Val)
+			if err != nil {
+				return err
+			}
+			values = append([]interface{}{n.RefInc}, values...)
+			values = append(values, n.RefInc) // one more time for UPDATE
+			stmt, ok := insertValue[nodeKey]
+			if !ok {
+				var ph = make([]string, len(values)-1) // excluding last increment
+				for i := range ph {
+					ph[i] = "?"
+				}
+				stmt, err = tx.Prepare(`INSERT INTO nodes(refs, hash, ` +
+					strings.Join(nodeKey.Columns(), ", ") +
+					`) VALUES (` + strings.Join(ph, ", ") +
+					`) ON CONFLICT(hash) DO UPDATE SET refs = refs + ?;`)
+				if err != nil {
+					return err
+				}
+				insertValue[nodeKey] = stmt
+			}
+			_, err = stmt.Exec(values...)
+			err = convInsertError(err)
+			if err != nil {
+				clog.Errorf("couldn't exec INSERT statement: %v", err)
+				return err
+			}
+		} else {
+			panic("unexpected node update")
+		}
+	}
+	for _, s := range insertValue {
+		s.Close()
+	}
+	if s := updateValue; s != nil {
+		s.Close()
+	}
+	insertValue = nil
+	updateValue = nil
+
+	// now we can deal with quads
+	ignore := ""
+	if opts.IgnoreDup {
+		ignore = " OR IGNORE"
+	}
+
+	var (
+		insertQuad *sql.Stmt
+		err        error
+	)
+	for _, d := range quads {
+		dirs := make([]interface{}, 0, len(quad.Directions))
+		for _, h := range d.Quad.Dirs() {
+			dirs = append(dirs, csql.NodeHash{h}.SQLValue())
+		}
+		if !d.Del {
+			if insertQuad == nil {
+				insertQuad, err = tx.Prepare(`INSERT` + ignore + ` INTO quads(subject_hash, predicate_hash, object_hash, label_hash, ts) VALUES (?, ?, ?, ?, datetime());`)
+				if err != nil {
+					return err
+				}
+				insertValue = make(map[csql.ValueType]*sql.Stmt)
+			}
+			_, err := insertQuad.Exec(dirs...)
+			err = convInsertError(err)
+			if err != nil {
+				if _, ok := err.(*graph.DeltaError); !ok {
+					clog.Errorf("couldn't exec INSERT statement: %v", err)
+				}
+				return err
+			}
+		} else {
+			panic("unexpected quad delete")
+		}
+	}
+	return nil
+}
+
+func convInsertError(err error) error {
+	if err == nil {
+		return nil
+	}
+	if e, ok := err.(sqlite3.Error); ok {
+		if e.Code == sqlite3.ErrConstraint {
+			return &graph.DeltaError{Err: graph.ErrQuadExists}
+		}
+	}
+	return err
+}


Test patch:
diff --git a/build.sh b/build.sh
old mode 100644
new mode 100755
diff --git a/data/testdata_multigraph.nq b/data/testdata_multigraph.nq
new file mode 100644
index 0000000..627635c
--- /dev/null
+++ b/data/testdata_multigraph.nq
@@ -0,0 +1,16 @@
+<alice> <follows> <bob> .
+<bob> <follows> <fred> .
+<bob> <status> "cool_person" .
+<charlie> <follows> <bob> .
+<charlie> <follows> <dani> .
+<dani> <follows> <bob> .
+<dani> <follows> <greg> .
+<dani> <status> "cool_person" .
+<emily> <follows> <fred> .
+<fred> <follows> <greg> .
+<greg> <status> "cool_person" .
+<predicates> <are> <follows> .
+<predicates> <are> <status> .
+<emily> <status> "smart_person" <smart_graph> .
+<greg> <status> "smart_person" <smart_graph> .
+<fred> <status> "smart_person" <other_graph> .
diff --git a/graph/sql/cockroach/cockroach_test.go b/graph/sql/cockroach/cockroach_test.go
index 2a65783..358b94c 100644
--- a/graph/sql/cockroach/cockroach_test.go
+++ b/graph/sql/cockroach/cockroach_test.go
@@ -61,6 +61,7 @@ func makeCockroach(t testing.TB) (string, graph.Options, func()) {
 
 var conf = &sqltest.Config{
 	TimeRound: true,
+	TimeInMcs: true,
 }
 
 func TestCockroach(t *testing.T) {
diff --git a/graph/sql/postgres/postgres_test.go b/graph/sql/postgres/postgres_test.go
index d75d57c..dfe3153 100644
--- a/graph/sql/postgres/postgres_test.go
+++ b/graph/sql/postgres/postgres_test.go
@@ -35,6 +35,7 @@ func makePostgres(t testing.TB) (string, graph.Options, func()) {
 
 var conf = &sqltest.Config{
 	TimeRound: true,
+	TimeInMcs: true,
 }
 
 func TestPostgres(t *testing.T) {
diff --git a/graph/sql/sqlite/sqlite_test.go b/graph/sql/sqlite/sqlite_test.go
new file mode 100644
index 0000000..22da802
--- /dev/null
+++ b/graph/sql/sqlite/sqlite_test.go
@@ -0,0 +1,34 @@
+package sqlite
+
+import (
+	"fmt"
+	"io/ioutil"
+	"os"
+	"testing"
+
+	"github.com/cayleygraph/cayley/graph"
+	"github.com/cayleygraph/cayley/graph/sql/sqltest"
+)
+
+func makeSqlite(t testing.TB) (string, graph.Options, func()) {
+	tmpFile, err := ioutil.TempFile("", fmt.Sprintf("cayley_test_%s*", Type))
+	if err != nil {
+		t.Fatalf("Could not create working directory: %v", err)
+	}
+	return fmt.Sprintf("file:%s?_loc=UTC", tmpFile.Name()), nil, func() {
+		os.RemoveAll(tmpFile.Name())
+	}
+}
+
+var conf = &sqltest.Config{
+	TimeRound: true,
+	TimeInMcs: false,
+}
+
+func TestSqlite(t *testing.T) {
+	sqltest.TestAll(t, Type, makeSqlite, conf)
+}
+
+func BenchmarkSqlite(t *testing.B) {
+	sqltest.BenchmarkAll(t, Type, makeSqlite, conf)
+}
diff --git a/graph/sql/sqltest/sqltest.go b/graph/sql/sqltest/sqltest.go
index fd2db3a..632454e 100644
--- a/graph/sql/sqltest/sqltest.go
+++ b/graph/sql/sqltest/sqltest.go
@@ -14,12 +14,13 @@ import (
 
 type Config struct {
 	TimeRound bool
+	TimeInMcs bool
 }
 
 func (c Config) quadStore() *graphtest.Config {
 	return &graphtest.Config{
 		NoPrimitives:        true,
-		TimeInMcs:           true,
+		TimeInMcs:           c.TimeInMcs,
 		TimeRound:           c.TimeRound,
 		OptimizesComparison: true,
 	}
@@ -27,7 +28,7 @@ func (c Config) quadStore() *graphtest.Config {
 
 func TestAll(t *testing.T, typ string, fnc DatabaseFunc, c *Config) {
 	if c == nil {
-		c = &Config{}
+		c = &Config{TimeInMcs: true}
 	}
 	create := makeDatabaseFunc(typ, fnc)
 	t.Run("qs", func(t *testing.T) {
diff --git a/query/gizmo/gizmo_test.go b/query/gizmo/gizmo_test.go
index 2846910..ffe8399 100644
--- a/query/gizmo/gizmo_test.go
+++ b/query/gizmo/gizmo_test.go
@@ -61,12 +61,15 @@ func intVal(v int) string {
 	return quad.Int(v).String()
 }
 
+const multiGraphTestFile = "../../data/testdata_multigraph.nq"
+
 var testQueries = []struct {
 	message string
 	data    []quad.Quad
 	query   string
 	limit   int
 	tag     string
+	file 	string
 	expect  []string
 	err     bool // TODO(dennwc): define error types for Gizmo and handle them
 }{
@@ -597,6 +600,24 @@ var testQueries = []struct {
 		data:   issue718Graph(),
 		expect: issue718Nodes(),
 	},
+	{
+		message: "issue #758. Verify saveOpt respects label context",
+		query: `
+			g.V("<greg>").LabelContext("<smart_graph>").SaveOpt("<status>", "statusTag").All()
+		`,
+		tag:    "statusTag",
+		file: multiGraphTestFile,
+		expect: []string{"smart_person"},
+	},
+	{
+		message: "issue #758. Verify saveR respects label context.",
+		query: `
+			g.V("smart_person").LabelContext("<other_graph>").SaveR("<status>", "who").All()
+		`,
+		tag:    "who",
+		file: multiGraphTestFile,
+		expect: []string{"<fred>"},
+	},
 }
 
 func runQueryGetTag(rec func(), g []quad.Quad, qu string, tag string, limit int) ([]string, error) {
@@ -630,7 +651,10 @@ func runQueryGetTag(rec func(), g []quad.Quad, qu string, tag string, limit int)
 }
 
 func TestGizmo(t *testing.T) {
+
 	simpleGraph := testutil.LoadGraph(t, "../../data/testdata.nq")
+	multiGraph := testutil.LoadGraph(t, multiGraphTestFile)
+
 	for _, test := range testQueries {
 		test := test
 		t.Run(test.message, func(t *testing.T) {
@@ -644,6 +668,10 @@ func TestGizmo(t *testing.T) {
 				test.tag = TopResultTag
 			}
 			quads := simpleGraph
+			if (test.file == multiGraphTestFile){
+				quads = multiGraph
+			}
+			
 			if test.data != nil {
 				quads = test.data
 			}
@@ -736,3 +764,6 @@ func issue718Nodes() []string {
 	}
 	return nodes
 }
+
+
+