├── .gitignore ├── .travis.yml ├── LICENSE.md ├── README.md ├── TODO.org.txt ├── client.go ├── client_m_test.go ├── client_test.go ├── commands.go ├── commands_result.go ├── commands_result_test.go ├── decimal.go ├── dial_example_test.go ├── document.go ├── document_test.go ├── errors.go ├── functions.go ├── global_property.go ├── graph_test.go ├── link.go ├── logo ├── orientgo.png └── orientgo.xcf ├── mapstructure.go ├── obinary ├── binserde │ └── typeserializer.go ├── client.go ├── command.go ├── commands_db.go ├── commands_srv.go ├── constants.go ├── database.go ├── errors.go ├── export_test.go ├── obinary_test.go ├── rw │ ├── assert_test.go │ ├── reader.go │ ├── reader_test.go │ ├── writer.go │ └── writer_test.go └── util.go ├── oclass.go ├── orient_test.go ├── property.go ├── proto.go ├── records.go ├── rid.go ├── rid_test.go ├── serializer.go ├── serializer_binary.go ├── serializer_binary_decimal.go ├── serializer_binary_test.go ├── serializer_string.go ├── sql.go ├── sql_test.go ├── type.go └── util.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io 2 | 3 | ### Go ### 4 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 5 | *.o 6 | *.a 7 | *.so 8 | ogonori 9 | ogo 10 | *BUFFER* 11 | FINDINGS.txt 12 | extry 13 | obinary/NEWdbcommands 14 | OLDclient.go 15 | currTODO.txt 16 | 17 | # Folders 18 | _obj 19 | _test 20 | travis/environment 21 | doc/pprof* 22 | obinary/binserdeORIG 23 | tmp/ 24 | .tmp/ 25 | obinary/writer 26 | obinary/playground/writer-playground 27 | 28 | # Architecture specific extensions/prefixes 29 | *.[568vq] 30 | [568vq].out 31 | out 32 | *.out 33 | 34 | *.cgo1.go 35 | *.cgo2.c 36 | _cgo_defun.c 37 | _cgo_gotypes.go 38 | _cgo_export.* 39 | 40 | _testmain.go 41 | 42 | *.exe 43 | *.test 44 | *.prof 45 | *pprof 46 | *.html 47 | 48 | *.iml 49 | .idea/ 50 | /out/ 51 | .idea_modules/ 52 | *.ipr 53 | *.iws -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | sudo: required 4 | dist: trusty 5 | 6 | services: 7 | - docker 8 | 9 | go: 10 | - 1.6 11 | 12 | env: 13 | - ORIENT_VERS=2.1.5 14 | 15 | matrix: 16 | include: 17 | - go: 1.6 18 | env: ORIENT_VERS=2.1.2 19 | - go: 1.6 20 | env: ORIENT_VERS=2.0 21 | 22 | install: 23 | - mkdir -p $GOPATH/src/gopkg.in/istreamdata 24 | - mv $TRAVIS_BUILD_DIR $GOPATH/src/gopkg.in/istreamdata/orientgo.v2 25 | - export TRAVIS_BUILD_DIR=$GOPATH/src/gopkg.in/istreamdata/orientgo.v2 26 | - cd $GOPATH/src/gopkg.in/istreamdata/orientgo.v2 27 | - docker pull dennwc/orientdb:${ORIENT_VERS} 28 | # - go get golang.org/x/tools/cmd/cover 29 | # - go get github.com/golang/lint/golint 30 | - go get -t -v ./... 31 | 32 | script: 33 | - go test -v -race ./... 34 | - go vet . 35 | - go vet ./obinary 36 | - go tool vet -methods=false ./obinary/rw 37 | # - go test -covermode=atomic ./... 38 | # - $HOME/gopath/bin/golint . 39 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Michael Peterson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | [![Build Status](https://travis-ci.org/istreamdata/orientgo.svg?branch=v2)](https://travis-ci.org/istreamdata/orientgo) 3 | [![GoDoc](https://godoc.org/gopkg.in/istreamdata/orientgo.v2?status.svg)](https://godoc.org/gopkg.in/istreamdata/orientgo.v2) 4 | 5 | **OrientGo** is a Go client for the [OrientDB](http://orientdb.com/orientdb/) database. 6 | 7 | ![OrientGo Logo](https://raw.github.com/istreamdata/orientgo/v2/logo/orientgo.png) 8 | 9 | # Status 10 | 11 | OrientDB versions supported: **2.0.15 - 2.1.5** 12 | 13 | **Not supported versions:** 14 | 15 | - 2.1.0 (bug in OrientDB, see [#28](https://github.com/istreamdata/orientgo/issues/28)) 16 | - 2.1.3 (broken protocol, see [#39](https://github.com/istreamdata/orientgo/issues/39)) 17 | 18 | Driver is no longer maintained and seeking for a new owner. 19 | 20 | ### Ogonori 21 | 22 | Original ogonori API is deprecated. Still, it's source code have been frozen in [v1.0](https://github.com/istreamdata/orientgo/tree/v1.0) branch. 23 | To use it, simply replace `github.com/quux00/ogonori` imports with `gopkg.in/istreamdata/orientgo.v1`. 24 | 25 | ### Supported features: 26 | - Mostly any SQL [queries](http://godoc.org/gopkg.in/istreamdata/orientgo.v2#SQLQuery), [commands](http://godoc.org/gopkg.in/istreamdata/orientgo.v2#SQLCommand) and [batch requests](http://godoc.org/gopkg.in/istreamdata/orientgo.v2#ScriptCommand). 27 | - Server-side scripts (via [ScriptCommand](http://godoc.org/gopkg.in/istreamdata/orientgo.v2#ScriptCommand) or [functions](http://godoc.org/gopkg.in/istreamdata/orientgo.v2#Function)). 28 | - Command results conversion to custom types via [mapstructure](http://github.com/mitchellh/mapstructure). 29 | - Direct CRUD operations on `Document` or `BytesRecord` objects. 30 | - Management of databases and record clusters. 31 | - Can be used for the golang `database/sql` API, with some cautions (see below). 32 | - Only supports OrientDB 2.x series. 33 | 34 | ### Not supported yet: 35 | - OrientDB 1.x. 36 | - Servers with cluster configuration (not tested). 37 | - Fetch plans are temporary disabled due to internal changes. 38 | - Transactions in Go. Transactions in JS can be used instead. 39 | - Live queries. 40 | - Command results streaming ([#26](https://github.com/istreamdata/orientgo/issues/26)). 41 | - OrientDB CUSTOM type. 42 | - ORM-like API. See Issue [#6](https://github.com/istreamdata/orientgo/issues/6). 43 | 44 | #### Caveat on using OrientGo as a database/sql API driver 45 | 46 | **WARNING: database/sql API is disabled for now.** 47 | 48 | The golang `database/sql` API has some constraints that can be make it painful to work with OrientDB. For example: 49 | 50 | * When you insert a record, the Go `database/sql` API only allows one to return a single int64 identifier for the record, but OrientDB uses as a compound int16:int64 RID, so getting the RID of records you just inserted requires another round trip to the database to query the RID. 51 | 52 | Also, since OrientDB transactions are not supported, the `Tx` portion of the `database/sql` API is not yet implemented. 53 | 54 | # Development 55 | 56 | You are welcome to initiate pull request and suggest a more user-friendly API. We will try to review them ASAP. 57 | 58 | ## How to run functional tests: 59 | 60 | 1) Install [Docker](https://docs.docker.com) 61 | 62 | 2) Pull OrientDB image: `docker pull dennwc/orientdb:2.1` 63 | 64 | 3) `go test -v ./...` 65 | 66 | ## Examples 67 | 68 | Dial example - dial_example_test.go 69 | 70 | ## LICENSE 71 | 72 | The MIT License 73 | -------------------------------------------------------------------------------- /TODO.org.txt: -------------------------------------------------------------------------------- 1 | * TODO Need to get server output for 'l', 'r' versions from SQLCommand and/or SQLQuery 2 | ** Document these clearly in the docs folder. Then decide how we can move the reading of the final 0 byte out of readResultSet 3 | * TODO Write GitHub issue for stale GlobalProperties problem and possible solutions (including parsing of the queries and adding specific "create property" commands in the native API -> how does Java client do it? 4 | * DONE Queries to try: 5 | ** DONE CREATE PROPERTY Cat.tags EMBEDDEDLIST STRING 6 | ** DONE ALTER PROPERTY Cat.gender REGEXP [M|F] 7 | ** DONE DROP PROPERTY Cat.gender => feeds into the stale GlobalProperties problem 8 | ** DONE Try query: insert into Person content {"name": "Charlie", "age":10} 9 | * DONE Write ReloadSchema in obinary/dbCommands 10 | * TODO Learn what different fetchPlan options are in OrientDB and try those in ogonori 11 | ** I suspect any that pull links will fail, since I don't yet know how to support links 12 | * TODO Run the go static analysis tool that checks for places not checking error return values 13 | * TODO Write SerDe Serializer 14 | * TODO Advanced queries to try: 15 | ** DONE INSERT INTO Diver SET name = 'Bill', buddy = (SELECT FROM Diver WHERE name = 'Max') 16 | ** DONE INSERT INTO Husband SET name = 'Wayne', wife = (INSERT into Diver name = 'Jill') 17 | ** DONE SELECT FROM [#10:3, #10:4, #10:5] 18 | ** DONE SELECT FROM Profile ORDER BY @rid DESC 19 | ** TODO SELECT FROM Profile 20 | LET $city = address.city 21 | WHERE $city.name like '%Saint%' and 22 | ($city.country.name = 'Italy' or $city.country.name = 'France') 23 | * TODO [#C] ODatabase#ClustCfg is just a binary blob -> parse that and decide what to keep 24 | * TODO [#C] Determine if result of SQLQuery needs a ResultSet cursor for large queries? Review what Java client does. 25 | * DONE Re-evaluate db/network reader API -> think about building better byteBuffer -> DONE for reader, NOT for writer 26 | ** See https://github.com/djherbis/buffer for ideas 27 | 28 | * TODO Test with MANDATORY/NOT NULL fields 29 | * TODO Test with documents that have different types for the same field (this is allowed - one could have a string and another an int, eg, as long as you haven't defined a schema property (?)) 30 | 31 | # ---[ Command Implementations ]--- # 32 | * TODO REQUEST_SHUTDOWN = 1 33 | * DONE REQUEST_CONNECT = 2 34 | * DONE REQUEST_DB_OPEN = 3 35 | * DONE REQUEST_DB_CREATE = 4 36 | * DONE REQUEST_DB_CLOSE = 5 37 | * DONE REQUEST_DB_EXIST = 6 38 | * DONE REQUEST_DB_DROP = 7 39 | * DONE REQUEST_DB_SIZE = 8 40 | * DONE REQUEST_DB_COUNTRECORDS = 9 41 | * DONE REQUEST_DATACLUSTER_ADD = 10 42 | * DONE REQUEST_DATACLUSTER_DROP = 11 43 | * DONE REQUEST_DATACLUSTER_COUNT = 12 44 | * DONE REQUEST_DATACLUSTER_DATARANGE = 13 45 | * TODO REQUEST_DATACLUSTER_COPY = 14 46 | * TODO REQUEST_DATACLUSTER_LH_CLUSTER_IS_USED = 16 // since 1.2.0 47 | * TODO REQUEST_DATASEGMENT_ADD = 20 48 | * TODO REQUEST_DATASEGMENT_DROP = 21 49 | * TODO REQUEST_RECORD_METADATA = 29 // since 1.4.0 50 | * DONE REQUEST_RECORD_LOAD = 30 => fully done ?? 51 | * TODO REQUEST_RECORD_CREATE = 31 52 | * TODO REQUEST_RECORD_UPDATE = 32 53 | * DONE REQUEST_RECORD_DELETE = 33 54 | * TODO REQUEST_RECORD_COPY = 34 55 | * TODO REQUEST_POSITIONS_HIGHER = 36 // since 1.3.0 56 | * TODO REQUEST_POSITIONS_LOWER = 37 // since 1.3.0 57 | * TODO REQUEST_RECORD_CLEAN_OUT = 38 // since 1.3.0 58 | * TODO REQUEST_POSITIONS_FLOOR = 39 // since 1.3.0 59 | * DEFERRED REQUEST_COUNT = 40 // DEPRECATED: USE REQUEST_DATACLUSTER_COUNT 60 | * DONE REQUEST_COMMAND = 41 61 | * TODO REQUEST_POSITIONS_CEILING = 42 // since 1.3.0 62 | * TODO REQUEST_RECORD_HIDE = 43 // since 1.7 63 | * TODO REQUEST_TX_COMMIT = 60 64 | * TODO REQUEST_CONFIG_GET = 70 65 | * TODO REQUEST_CONFIG_SET = 71 66 | * TODO REQUEST_CONFIG_LIST = 72 67 | * TODO REQUEST_DB_RELOAD = 73 // SINCE 1.0rc4 68 | * DONE REQUEST_DB_LIST = 74 // SINCE 1.0rc6 69 | * TODO REQUEST_PUSH_DISTRIB_CONFIG = 80 70 | * TODO REQUEST_DB_COPY = 90 // SINCE 1.0rc8 71 | * TODO REQUEST_REPLICATION = 91 // SINCE 1.0 72 | * TODO REQUEST_CLUSTER = 92 // SINCE 1.0 73 | * TODO REQUEST_DB_TRANSFER = 93 // SINCE 1.0.2 74 | * TODO REQUEST_DB_FREEZE = 94 // SINCE 1.1.0 75 | * TODO REQUEST_DB_RELEASE = 95 // SINCE 1.1.0 76 | * TODO REQUEST_DATACLUSTER_FREEZE = 96 77 | * TODO REQUEST_DATACLUSTER_RELEASE = 97 78 | * TODO REQUEST_CREATE_SBTREE_BONSAI = 110 79 | * TODO REQUEST_SBTREE_BONSAI_GET = 111 80 | * DONE REQUEST_SBTREE_BONSAI_FIRST_KEY = 112 81 | * DONE REQUEST_SBTREE_BONSAI_GET_ENTRIES_MAJOR = 113 82 | * DONE REQUEST_RIDBAG_GET_SIZE = 114 83 | 84 | 85 | Issues: 86 | 87 | New REQUEST_TYPES in latest version of binary protocol ... 88 | 89 | Multi-threading testing => properties look ups 90 | 91 | Handling of OType.ANY 92 | -------------------------------------------------------------------------------- /client.go: -------------------------------------------------------------------------------- 1 | package orient // import "gopkg.in/istreamdata/orientgo.v2" 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "strings" 7 | "time" 8 | ) 9 | 10 | const concurrentRetriesDefault = 5 11 | 12 | var ( 13 | concurrentRetries = concurrentRetriesDefault 14 | ) 15 | 16 | // SetRetryCountConcurrent sets a retry count when ErrConcurrentModification occurs. 17 | // 18 | // n == 0 - use default value 19 | // 20 | // n < 0 - no limit for retries 21 | // 22 | // n > 0 - maximum of n retries 23 | func SetRetryCountConcurrent(n int) { 24 | if n == 0 { 25 | n = concurrentRetriesDefault 26 | } else if n < 0 { 27 | n = -1 28 | } 29 | concurrentRetries = n 30 | } 31 | 32 | // MaxConnections limits the number of opened connections. 33 | var MaxConnections = 6 34 | 35 | // FetchPlan is an additional parameter to queries, that instructs DB how to handle linked documents. 36 | // 37 | // The format is: 38 | // 39 | // (field:depth)* 40 | // 41 | // Field is the name of the field to specify the depth-level. Wildcard '*' means any fields. 42 | // 43 | // Depth is the depth level to fetch. -1 means infinite, 0 means no fetch at all and 1-N the depth level value. 44 | // 45 | // WARN: currently fetch plan have no effect on returned results, as records cache is not implemented yet. 46 | type FetchPlan string 47 | 48 | const ( 49 | // DefaultFetchPlan is an empty fetch plan. Let the database decide. Usually means "do not follow any links". 50 | DefaultFetchPlan = FetchPlan("") 51 | // NoFollow is a fetch plan that does not follow any links 52 | NoFollow = FetchPlan("*:0") 53 | // FollowAll is a fetch plan that follows all links 54 | FollowAll = FetchPlan("*:-1") 55 | ) 56 | 57 | // Dial opens a new connection to OrientDB server. 58 | // 59 | // For now, user must import protocol implementation, which will be used for connection: 60 | // 61 | // import _ "gopkg.in/istreamdata/orientgo.v2/obinary" 62 | // 63 | // Address must be in host:port format. Connection to OrientDB cluster is not supported yet. 64 | // 65 | // Returned Client uses connection pool under the hood, so it can be shared between goroutines. 66 | func Dial(addr string) (*Client, error) { 67 | dial := protos[ProtoBinary] 68 | if dial == nil { 69 | return nil, fmt.Errorf("orientgo: no protocols are active; forgot to import obinary package?") 70 | } 71 | cli := &Client{ 72 | dial: func() (DBConnection, error) { 73 | return dial(addr) 74 | }, 75 | } 76 | conn, err := cli.dial() 77 | if err != nil { 78 | return nil, err 79 | } 80 | cli.mconn = conn 81 | return cli, nil 82 | } 83 | 84 | func newConnPool(size int, dial func() (DBSession, error)) *connPool { 85 | if size == 0 { 86 | size = MaxConnections 87 | } 88 | p := &connPool{ 89 | dial: dial, 90 | } 91 | if size > 0 { 92 | p.ch = make(chan DBSession, size) 93 | p.toks = make(chan struct{}, size) 94 | for i := 0; i < size; i++ { 95 | p.toks <- struct{}{} 96 | } 97 | } else { 98 | p.ch = make(chan DBSession, 10) 99 | } 100 | return p 101 | } 102 | 103 | type connPool struct { 104 | dial func() (DBSession, error) 105 | ch chan DBSession 106 | toks chan struct{} 107 | } 108 | 109 | func (p *connPool) getConn() (DBSession, error) { 110 | var dt <-chan time.Time 111 | if p.toks == nil { 112 | dt = time.After(time.Millisecond * 100) 113 | } 114 | select { 115 | case conn := <-p.ch: 116 | return conn, nil 117 | case <-p.toks: 118 | case <-dt: 119 | } 120 | if p.dial == nil { 121 | return nil, nil 122 | } 123 | conn, err := p.dial() 124 | if err != nil { 125 | return nil, err 126 | } 127 | return conn, nil 128 | } 129 | func (p *connPool) putConn(conn DBSession) { 130 | select { 131 | case p.ch <- conn: 132 | default: 133 | if p.toks != nil { 134 | select { 135 | case p.toks <- struct{}{}: 136 | default: 137 | } 138 | } 139 | conn.Close() 140 | } 141 | } 142 | func (p *connPool) clear() { 143 | loop: 144 | for { 145 | select { 146 | case conn := <-p.ch: 147 | if conn != nil { 148 | conn.Close() 149 | } 150 | case <-p.toks: 151 | default: 152 | break loop 153 | } 154 | } 155 | for len(p.toks) < cap(p.toks) { 156 | p.toks <- struct{}{} 157 | } 158 | } 159 | 160 | // Client represents connection to OrientDB server. It is safe for concurrent use. 161 | type Client struct { 162 | mconn DBConnection 163 | dial func() (DBConnection, error) 164 | } 165 | 166 | // Auth initiates a new administration session with OrientDB server, allowing to manage databases. 167 | func (c *Client) Auth(user, pass string) (*Admin, error) { 168 | if c.mconn == nil { 169 | conn, err := c.dial() 170 | if err != nil { 171 | return nil, err 172 | } 173 | c.mconn = conn 174 | } 175 | m, err := c.mconn.Auth(user, pass) 176 | if err != nil { 177 | return nil, err 178 | } 179 | return &Admin{c, m}, nil 180 | } 181 | 182 | type sessionAndConn struct { 183 | DBSession 184 | conn DBConnection 185 | } 186 | 187 | func (s sessionAndConn) Close() error { 188 | err := s.DBSession.Close() 189 | if err1 := s.conn.Close(); err == nil { 190 | err = err1 191 | } 192 | return err 193 | } 194 | 195 | // Open initiates a new database session, allowing to make queries to selected database. 196 | // 197 | // For database management use Auth instead. 198 | func (c *Client) Open(name string, dbType DatabaseType, user, pass string) (*Database, error) { 199 | db := &Database{pool: newConnPool(0, func() (DBSession, error) { 200 | conn, err := c.dial() 201 | if err != nil { 202 | return nil, err 203 | } 204 | ds, err := conn.Open(name, dbType, user, pass) 205 | if err != nil { 206 | conn.Close() 207 | return nil, err 208 | } 209 | return sessionAndConn{DBSession: ds, conn: conn}, nil 210 | }), cli: c} 211 | conn, err := db.pool.getConn() 212 | if err != nil { 213 | return nil, err 214 | } 215 | db.pool.putConn(conn) 216 | return db, nil 217 | } 218 | 219 | // Close must be called to close all active DB connections. 220 | func (c *Client) Close() error { 221 | if c.mconn != nil { 222 | c.mconn.Close() 223 | } 224 | return nil 225 | } 226 | 227 | // Admin wraps a database management session. 228 | type Admin struct { 229 | cli *Client 230 | db DBAdmin 231 | } 232 | 233 | // DatabaseExists checks if database with given name and storage type exists. 234 | func (a *Admin) DatabaseExists(name string, storageType StorageType) (bool, error) { 235 | return a.db.DatabaseExists(name, storageType) 236 | } 237 | 238 | // CreateDatabase creates a new database with given database type (Document or Graph) and storage type (Persistent or Volatile). 239 | func (a *Admin) CreateDatabase(name string, dbType DatabaseType, storageType StorageType) error { 240 | return a.db.CreateDatabase(name, dbType, storageType) 241 | } 242 | 243 | // DropDatabase removes database from the server. 244 | func (a *Admin) DropDatabase(name string, storageType StorageType) error { 245 | return a.db.DropDatabase(name, storageType) 246 | } 247 | 248 | // ListDatabases returns a list of databases in a form: 249 | // 250 | // dbname: dbpath 251 | // 252 | func (a *Admin) ListDatabases() (map[string]string, error) { 253 | return a.db.ListDatabases() 254 | } 255 | 256 | // Close closes DB management session. 257 | func (a *Admin) Close() error { 258 | return a.db.Close() 259 | } 260 | 261 | // Database wraps a database session. It is safe for concurrent use. 262 | type Database struct { 263 | pool *connPool 264 | cli *Client 265 | } 266 | 267 | // Size return the size of current database (in bytes). 268 | func (db *Database) Size() (int64, error) { 269 | conn, err := db.pool.getConn() 270 | if err != nil { 271 | return 0, err 272 | } 273 | defer db.pool.putConn(conn) 274 | return conn.Size() 275 | } 276 | 277 | // Close closes database session. 278 | func (db *Database) Close() error { 279 | if db != nil && db.pool != nil { 280 | db.pool.clear() 281 | } 282 | return nil 283 | } 284 | 285 | // ReloadSchema reloads documents schema from database. 286 | func (db *Database) ReloadSchema() error { 287 | conn, err := db.pool.getConn() 288 | if err != nil { 289 | return err 290 | } 291 | defer db.pool.putConn(conn) 292 | return conn.ReloadSchema() 293 | } 294 | 295 | // GetCurDB returns database metadata 296 | func (db *Database) GetCurDB() *ODatabase { 297 | conn, err := db.pool.getConn() 298 | if err != nil { 299 | return nil 300 | } 301 | defer db.pool.putConn(conn) 302 | return conn.GetCurDB() 303 | } 304 | 305 | // AddCluster creates new cluster with given name and returns its ID. 306 | func (db *Database) AddCluster(name string) (int16, error) { 307 | return db.AddClusterWithID(name, -1) // -1 means generate new cluster id 308 | } 309 | 310 | // AddClusterWithID creates new cluster with given cluster position and name 311 | func (db *Database) AddClusterWithID(name string, clusterID int16) (int16, error) { 312 | conn, err := db.pool.getConn() 313 | if err != nil { 314 | return 0, err 315 | } 316 | defer db.pool.putConn(conn) 317 | return conn.AddClusterWithID(name, clusterID) 318 | } 319 | 320 | // DropCluster deletes cluster from database 321 | func (db *Database) DropCluster(name string) error { 322 | conn, err := db.pool.getConn() 323 | if err != nil { 324 | return err 325 | } 326 | defer db.pool.putConn(conn) 327 | return conn.DropCluster(name) 328 | } 329 | 330 | // GetClusterDataRange returns the begin and end positions of data in the requested cluster. 331 | func (db *Database) GetClusterDataRange(clusterName string) (begin, end int64, err error) { 332 | conn, err := db.pool.getConn() 333 | if err != nil { 334 | return 0, 0, err 335 | } 336 | defer db.pool.putConn(conn) 337 | return conn.GetClusterDataRange(clusterName) 338 | } 339 | 340 | // ClustersCount returns total count of records in given clusters 341 | func (db *Database) ClustersCount(withDeleted bool, clusterNames ...string) (int64, error) { 342 | conn, err := db.pool.getConn() 343 | if err != nil { 344 | return 0, err 345 | } 346 | defer db.pool.putConn(conn) 347 | return conn.ClustersCount(withDeleted, clusterNames...) 348 | } 349 | 350 | // CreateRecord saves a record to the database. Record RID and version will be changed. 351 | func (db *Database) CreateRecord(rec ORecord) error { 352 | conn, err := db.pool.getConn() 353 | if err != nil { 354 | return err 355 | } 356 | defer db.pool.putConn(conn) 357 | return conn.CreateRecord(rec) 358 | } 359 | 360 | // DeleteRecordByRID removes a record from database 361 | func (db *Database) DeleteRecordByRID(rid RID, recVersion int) error { 362 | conn, err := db.pool.getConn() 363 | if err != nil { 364 | return err 365 | } 366 | defer db.pool.putConn(conn) 367 | return conn.DeleteRecordByRID(rid, recVersion) 368 | } 369 | 370 | // GetRecordByRID returns a record using specified fetch plan. If ignoreCache is set to true implementations will 371 | // not use local records cache and will fetch record from database. 372 | func (db *Database) GetRecordByRID(rid RID, fetchPlan FetchPlan, ignoreCache bool) (ORecord, error) { 373 | conn, err := db.pool.getConn() 374 | if err != nil { 375 | return nil, err 376 | } 377 | defer db.pool.putConn(conn) 378 | return conn.GetRecordByRID(rid, fetchPlan, ignoreCache) 379 | } 380 | 381 | // UpdateRecord updates given record in a database. Record version will be changed after the call. 382 | func (db *Database) UpdateRecord(rec ORecord) error { 383 | conn, err := db.pool.getConn() 384 | if err != nil { 385 | return err 386 | } 387 | defer db.pool.putConn(conn) 388 | return conn.UpdateRecord(rec) 389 | } 390 | 391 | // CountRecords returns total records count. 392 | func (db *Database) CountRecords() (int64, error) { 393 | conn, err := db.pool.getConn() 394 | if err != nil { 395 | return 0, err 396 | } 397 | defer db.pool.putConn(conn) 398 | return conn.CountRecords() 399 | } 400 | 401 | // Command executes command against current database. Example: 402 | // 403 | // result := db.Command(NewSQLQuery("SELECT FROM V WHERE id = ?", id).Limit(10)) 404 | // 405 | func (db *Database) Command(cmd OCommandRequestText) Results { 406 | conn, err := db.pool.getConn() 407 | if err != nil { 408 | return errorResult{err: err} 409 | } 410 | defer db.pool.putConn(conn) 411 | var result interface{} 412 | for i := 0; concurrentRetries < 0 || i < concurrentRetries; i++ { 413 | result, err = conn.Command(cmd) 414 | err = convertError(err) 415 | switch err.(type) { 416 | case ErrConcurrentModification: 417 | continue 418 | } 419 | break 420 | } 421 | if err != nil { 422 | return errorResult{err: convertError(err)} 423 | } 424 | return newResults(result) 425 | } 426 | 427 | func sqlEscape(s string) string { // TODO: get rid of it 428 | s = strings.Replace(s, `\`, `\\`, -1) 429 | s = strings.Replace(s, `"`, `\"`, -1) 430 | return `"` + s + `"` 431 | } 432 | 433 | // CreateScriptFunc is a helper for saving server-side functions to database. 434 | func (db *Database) CreateScriptFunc(fnc Function) error { 435 | sql := `CREATE FUNCTION ` + fnc.Name + ` ` + sqlEscape(fnc.Code) // TODO: pass as parameter 436 | if len(fnc.Params) > 0 { 437 | sql += ` PARAMETERS [` + strings.Join(fnc.Params, ", ") + `]` 438 | } 439 | sql += ` IDEMPOTENT ` + fmt.Sprint(fnc.Idemp) 440 | if fnc.Lang != "" { 441 | sql += ` LANGUAGE ` + string(fnc.Lang) 442 | } 443 | return db.Command(NewSQLCommand(sql)).Err() 444 | } 445 | 446 | // DeleteScriptFunc deletes server-side function with a given name from current database. 447 | func (db *Database) DeleteScriptFunc(name string) error { 448 | return db.Command(NewSQLCommand(`DELETE FROM OFunction WHERE name = ?`, name)).Err() 449 | } 450 | 451 | // UpdateScriptFunc updates code of server-side function 452 | func (db *Database) UpdateScriptFunc(name string, script string) error { 453 | return db.Command(NewSQLCommand(`UPDATE OFunction SET code = ? WHERE name = ?`, script, name)).Err() 454 | } 455 | 456 | // CallScriptFunc is a helper for calling server-side functions (especially JS). Ideally should be a shorthand for 457 | // 458 | // db.Command(NewFunctionCommand(name, params...)) 459 | // 460 | // but it uses some workarounds to allow to return JS objects from that functions. 461 | func (db *Database) CallScriptFunc(name string, params ...interface{}) Results { 462 | // conn, err := db.pool.getConn() 463 | // if err != nil { 464 | // return nil, err 465 | // } 466 | // defer db.pool.putConn(conn) 467 | // recs, err := conn.CallScriptFunc(name, params...) 468 | // if err != nil { 469 | // return recs, err 470 | // } 471 | // if result != nil { 472 | // err = recs.DeserializeAll(result) 473 | // } 474 | // return recs, err 475 | sparams := make([]string, 0, len(params)) 476 | for _, p := range params { 477 | data, _ := json.Marshal(p) 478 | 479 | sparams = append(sparams, string(data)) 480 | } 481 | cmd := fmt.Sprintf(`var out = %s(%s); (typeof(out) == "object" && out.toString() == "[object Object]" ? (new com.orientechnologies.orient.core.record.impl.ODocument()).fromJSON(JSON.stringify(out)) : out)`, 482 | name, strings.Join(sparams, ",")) 483 | return db.Command(NewScriptCommand(LangJS, cmd)) 484 | } 485 | 486 | // InitScriptFunc is a helper for updating all server-side functions to specified state. 487 | func (db *Database) InitScriptFunc(fncs ...Function) (err error) { 488 | for _, fnc := range fncs { 489 | if fnc.Lang == "" { 490 | err = fmt.Errorf("no language provided for function '%s'", fnc.Name) 491 | return 492 | } 493 | db.DeleteScriptFunc(fnc.Name) 494 | err = db.CreateScriptFunc(fnc) 495 | if err != nil && !strings.Contains(err.Error(), "found duplicated key") { 496 | return 497 | } 498 | } 499 | return nil 500 | } 501 | 502 | // MarshalContent is a helper for constructing SQL commands with CONTENT keyword. 503 | // Shorthand for json.Marshal. Will panic on errors. 504 | func MarshalContent(o interface{}) string { 505 | data, err := json.Marshal(o) 506 | if err != nil { 507 | panic(err) 508 | } 509 | return string(data) 510 | } 511 | -------------------------------------------------------------------------------- /commands.go: -------------------------------------------------------------------------------- 1 | package orient 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "reflect" 7 | 8 | "gopkg.in/istreamdata/orientgo.v2/obinary/rw" 9 | ) 10 | 11 | var ( 12 | _ Serializable = (*textReqCommand)(nil) 13 | 14 | _ OCommandRequestText = SQLQuery{} 15 | _ OCommandRequestText = SQLCommand{} 16 | _ OCommandRequestText = ScriptCommand{} 17 | _ OCommandRequestText = FunctionCommand{} 18 | ) 19 | 20 | // OCommandRequestText is an interface for text-based database commands, 21 | // which can be executed using database.Command function. 22 | type OCommandRequestText interface { 23 | CustomSerializable 24 | GetText() string 25 | } 26 | 27 | func arrayToParamsMap(params []interface{}) interface{} { 28 | if len(params) == 1 && reflect.TypeOf(params[0]).Kind() == reflect.Map { 29 | return params[0] 30 | } 31 | mp := make(map[int32]interface{}, len(params)) 32 | for i, p := range params { 33 | if ide, ok := p.(OIdentifiable); ok { 34 | p = ide.GetIdentity() // use RID only 35 | } 36 | mp[int32(i)] = p 37 | } 38 | return mp 39 | } 40 | 41 | func newTextReqCommand(text string, params []interface{}) textReqCommand { 42 | return textReqCommand{text: text, params: params} 43 | } 44 | 45 | // textReqCommand is a generic text-based command. 46 | // 47 | // OCommandTextAbstract in Java world. 48 | type textReqCommand struct { 49 | //OCommandReq 50 | text string 51 | params []interface{} 52 | } 53 | 54 | func (rq textReqCommand) GetText() string { 55 | return rq.text 56 | } 57 | 58 | func (rq textReqCommand) ToStream(w io.Writer) error { 59 | params := arrayToParamsMap(rq.params) 60 | buf := bytes.NewBuffer(nil) 61 | doc := NewEmptyDocument() 62 | doc.SetField("parameters", params) 63 | if err := GetDefaultRecordSerializer().ToStream(buf, doc); err != nil { 64 | return err 65 | } 66 | 67 | bw := rw.NewWriter(w) 68 | 69 | bw.WriteString(rq.text) 70 | if params == nil || reflect.ValueOf(params).Len() == 0 { 71 | bw.WriteBool(false) // simple params are absent 72 | bw.WriteBool(false) // composite keys are absent 73 | return bw.Err() 74 | } 75 | 76 | bw.WriteBool(true) // simple params 77 | bw.WriteBytes(buf.Bytes()) 78 | 79 | // TODO: check for composite keys 80 | bw.WriteBool(false) // composite keys 81 | return bw.Err() 82 | } 83 | 84 | // FunctionCommand is a command to call server-side function. 85 | // 86 | // OCommandFunction in Java world. 87 | type FunctionCommand struct { 88 | textReqCommand 89 | } 90 | 91 | // NewFunctionCommand creates a new call request to server-side function with given name and arguments. 92 | func NewFunctionCommand(name string, params ...interface{}) FunctionCommand { 93 | return FunctionCommand{ 94 | textReqCommand: newTextReqCommand(name, params), 95 | } 96 | } 97 | 98 | // GetClassName returns Java class name 99 | func (rq FunctionCommand) GetClassName() string { 100 | return "com.orientechnologies.orient.core.command.script.OCommandFunction" 101 | } 102 | 103 | // ScriptCommand is a way to execute batch-like commands. 104 | // 105 | // OCommandScript in Java world. 106 | type ScriptCommand struct { 107 | lang string 108 | textReqCommand 109 | } 110 | 111 | // NewScriptCommand creates a new script request written in a given language (SQL/JS/Groovy/...), 112 | // with specified body code and params. 113 | // 114 | // Example: 115 | // 116 | // NewScriptCommand(LangJS, `var out = db.command("SELECT FROM V"); out`) 117 | // 118 | func NewScriptCommand(lang ScriptLang, body string, params ...interface{}) ScriptCommand { 119 | return ScriptCommand{ 120 | lang: string(lang), 121 | textReqCommand: newTextReqCommand(body, params), 122 | } 123 | } 124 | 125 | // GetClassName returns Java class name 126 | func (rq ScriptCommand) GetClassName() string { return "s" } 127 | 128 | // ToStream serializes command to specified Writer 129 | func (rq ScriptCommand) ToStream(w io.Writer) error { 130 | if err := rw.NewWriter(w).WriteString(rq.lang); err != nil { 131 | return err 132 | } 133 | return rq.textReqCommand.ToStream(w) 134 | } 135 | 136 | // SQLCommand is a non-SELECT sql command (EXEC/INSERT/DELETE). 137 | // 138 | // OCommandSQL in Java world. 139 | type SQLCommand struct { 140 | textReqCommand 141 | } 142 | 143 | // NewSQLCommand creates a new SQL command request with given params. 144 | // 145 | // Example: 146 | // 147 | // NewSQLCommand("INSERT INTO People (id, name) VALUES (?, ?)", id, name) 148 | // 149 | func NewSQLCommand(sql string, params ...interface{}) SQLCommand { 150 | return SQLCommand{newTextReqCommand(sql, params)} 151 | } 152 | 153 | // GetClassName returns Java class name 154 | func (rq SQLCommand) GetClassName() string { return "c" } 155 | 156 | // SQLQuery is a SELECT-like SQL command. 157 | // 158 | // OSQLQuery in Java world. 159 | type SQLQuery struct { 160 | text string 161 | limit int 162 | plan string 163 | params []interface{} 164 | } 165 | 166 | // NewSQLQuery creates a new SQL query with given params. 167 | // 168 | // Example: 169 | // 170 | // NewSQLQuery("SELECT FROM V WHERE id = ?", id) 171 | // 172 | func NewSQLQuery(sql string, params ...interface{}) SQLQuery { 173 | return SQLQuery{text: sql, params: params, limit: -1} 174 | } 175 | 176 | // GetText returns query text 177 | func (rq SQLQuery) GetText() string { return rq.text } 178 | 179 | // GetClassName returns Java class name 180 | func (rq SQLQuery) GetClassName() string { return "q" } 181 | 182 | // Limit sets a query record limit 183 | func (rq SQLQuery) Limit(n int) SQLQuery { 184 | rq.limit = n 185 | return rq 186 | } 187 | 188 | // FetchPlan sets a query fetch plan 189 | func (rq SQLQuery) FetchPlan(plan FetchPlan) SQLQuery { 190 | rq.plan = string(plan) 191 | return rq 192 | } 193 | 194 | // ToStream serializes command to specified Writer 195 | func (rq SQLQuery) ToStream(w io.Writer) error { 196 | sparams, err := rq.serializeQueryParameters(rq.params) 197 | if err != nil { 198 | return err 199 | } 200 | bw := rw.NewWriter(w) 201 | bw.WriteString(rq.text) 202 | bw.WriteInt(int32(rq.limit)) 203 | bw.WriteString(rq.plan) 204 | bw.WriteBytes(sparams) 205 | return bw.Err() 206 | } 207 | func (rq SQLQuery) serializeQueryParameters(params []interface{}) ([]byte, error) { 208 | if len(params) == 0 { 209 | return nil, nil 210 | } 211 | doc := NewEmptyDocument() 212 | doc.SetField("params", arrayToParamsMap(params)) // TODO: convertToRIDsIfPossible 213 | buf := bytes.NewBuffer(nil) 214 | if err := GetDefaultRecordSerializer().ToStream(buf, doc); err != nil { 215 | return nil, err 216 | } 217 | return buf.Bytes(), nil 218 | } 219 | -------------------------------------------------------------------------------- /commands_result.go: -------------------------------------------------------------------------------- 1 | package orient 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | ) 7 | 8 | var ( 9 | _ Results = errorResult{} 10 | _ Results = (*unknownResult)(nil) 11 | ) 12 | 13 | // Results is an interface for database command results. Must be closed. 14 | // 15 | // Individual results can be iterated in a next way: 16 | // 17 | // results := db.Command(cmd) 18 | // if err := results.Err(); err != nil { 19 | // // handle command errors; can be omitted and checked later with Err or Close 20 | // } 21 | // var one SomeStruct 22 | // for results.Next(&one) { 23 | // // process result, if any 24 | // } 25 | // if err := results.Close(); err != nil { 26 | // // handle command and/or type conversion errors 27 | // } 28 | // 29 | // Or just retrieved all at once: 30 | // 31 | // var arr []SomeStruct 32 | // if err := results.All(&arr); err != nil { 33 | // // handle command and/or type conversion errors 34 | // } 35 | // 36 | // Some commands may return just one int/bool value: 37 | // 38 | // var affected int 39 | // results.All(&affected) 40 | // 41 | // Also results can be handled manually: 42 | // 43 | // var out interface{} 44 | // results.All(&out) 45 | // switch out.(type) { 46 | // case []OIdentifiable: 47 | // // ... 48 | // case *DocumentRecord: 49 | // // ... 50 | // } 51 | // 52 | type Results interface { 53 | Err() error 54 | Close() error 55 | Next(result interface{}) bool 56 | All(result interface{}) error 57 | } 58 | 59 | // errorResult is a simple result type that returns one specific error. Useful for server-side errors. 60 | type errorResult struct { 61 | err error 62 | } 63 | 64 | func (e errorResult) Err() error { return e.err } 65 | func (e errorResult) Close() error { return e.err } 66 | func (e errorResult) Next(result interface{}) bool { return false } 67 | func (e errorResult) All(result interface{}) error { return e.err } 68 | 69 | func newResults(o interface{}) Results { 70 | return &unknownResult{result: o} 71 | } 72 | 73 | // unknownResult is a generic result type that uses reflection to iterate over returned records 74 | type unknownResult struct { 75 | err error 76 | parsed bool 77 | result interface{} 78 | } 79 | 80 | func (r *unknownResult) Err() error { return r.err } 81 | func (r *unknownResult) Close() error { return r.err } 82 | func (r *unknownResult) Next(result interface{}) bool { // TODO: implement 83 | if r.parsed { 84 | return false 85 | } 86 | r.parsed = true 87 | r.All(result) 88 | return false 89 | } 90 | func (r *unknownResult) All(result interface{}) error { 91 | // if r.parsed { 92 | // return fmt.Errorf("results are already parsed") 93 | // } 94 | // r.parsed = true 95 | 96 | // check for pointer 97 | targ := reflect.ValueOf(result) 98 | if targ.Kind() != reflect.Ptr { 99 | return fmt.Errorf("result is not a pointer: %T", result) 100 | } else if targ.IsNil() { 101 | return fmt.Errorf("nil result pointer") 102 | } 103 | targ = targ.Elem() 104 | 105 | return convertTypes(targ, reflect.ValueOf(r.result)) 106 | } 107 | 108 | type ErrUnsupportedConversion struct { 109 | From reflect.Value 110 | To reflect.Value 111 | } 112 | 113 | func (e ErrUnsupportedConversion) Error() string { 114 | var a, b string 115 | if e.From.IsValid() { 116 | a = fmt.Sprintf("%v(%v)", e.From.Type(), e.From.Kind()) 117 | } else { 118 | a = "" 119 | } 120 | if e.To.IsValid() { 121 | b = fmt.Sprintf("%v(%v)", e.To.Type(), e.To.Kind()) 122 | } else { 123 | b = "" 124 | } 125 | return fmt.Sprintf("unsupported conversion: %v -> %v", a, b) 126 | } 127 | 128 | func mapToStruct(m interface{}, val interface{}) error { 129 | dec, err := newMapDecoder(val) 130 | if err != nil { 131 | return err 132 | } 133 | return dec.Decode(m) 134 | } 135 | 136 | const debugTypeConversion = false 137 | 138 | func convertTypes(targ, src reflect.Value) error { 139 | if debugTypeConversion { 140 | fmt.Printf("conv: %T -> %T, %+v -> %+v\n", src.Interface(), targ.Interface(), src.Interface(), targ.Interface()) 141 | defer func() { 142 | fmt.Printf("conv out: %T -> %T, %+v -> %+v\n", src.Interface(), targ.Interface(), src.Interface(), targ.Interface()) 143 | }() 144 | } 145 | if targ.Type() == src.Type() { 146 | targ.Set(src) 147 | return nil 148 | } else if src.Type().ConvertibleTo(targ.Type()) { 149 | targ.Set(src.Convert(targ.Type())) 150 | return nil 151 | } else if src.Kind() == reflect.Interface { 152 | return convertTypes(targ, src.Elem()) 153 | } 154 | // if targ.Kind() == reflect.Ptr { 155 | // if targ.IsNil() { 156 | // targ.Set(reflect.New(targ.Type().Elem())) 157 | // } 158 | // targ = targ.Elem() 159 | // } 160 | // if src.Kind() == reflect.Ptr { 161 | // src = src.Elem() 162 | // } 163 | 164 | if targ.Kind() == reflect.Struct || (targ.Kind() == reflect.Ptr && targ.Type().Elem().Kind() == reflect.Struct) { 165 | if targ.Kind() == reflect.Ptr && targ.IsNil() { 166 | targ.Set(reflect.New(targ.Type().Elem())) 167 | } 168 | if src.Kind() == reflect.Map { 169 | return mapToStruct(src.Interface(), targ.Addr().Interface()) 170 | } 171 | } else if targ.Kind() == reflect.Slice { 172 | if src.Kind() == reflect.Slice { // slice into slice 173 | if targ.Len() != src.Len() { 174 | targ.Set(reflect.MakeSlice(targ.Type(), src.Len(), src.Len())) 175 | } 176 | for i := 0; i < src.Len(); i++ { 177 | if err := convertTypes(targ.Index(i), src.Index(i)); err != nil { 178 | return err 179 | } 180 | } 181 | return nil 182 | } 183 | // one value into slice 184 | targ.Set(reflect.MakeSlice(targ.Type(), 1, 1)) 185 | if err := convertTypes(targ.Index(0), src); err != nil { 186 | targ.Set(reflect.Zero(targ.Type())) 187 | return err 188 | } 189 | return nil 190 | } else if targ.Kind() == reflect.Map { 191 | if src.Kind() == reflect.Map { 192 | targ.Set(reflect.MakeMap(targ.Type())) 193 | for _, k := range src.MapKeys() { 194 | nk := reflect.New(targ.Type().Key()).Elem() 195 | if err := convertTypes(nk, k); err != nil { 196 | return err 197 | } 198 | nv := reflect.New(targ.Type().Elem()).Elem() 199 | if err := convertTypes(nv, src.MapIndex(k)); err != nil { 200 | return err 201 | } 202 | targ.SetMapIndex(nk, nv) 203 | } 204 | return nil 205 | } 206 | } 207 | 208 | switch rec := src.Interface().(type) { 209 | case MapSerializable: 210 | m, err := rec.ToMap() 211 | if err != nil { 212 | return err 213 | } 214 | return convertTypes(targ, reflect.ValueOf(m)) 215 | case *Document: // Document implements DocumentSerializable for convenience, no need to convert it 216 | case DocumentSerializable: 217 | doc, err := rec.ToDocument() 218 | if err != nil { 219 | return err 220 | } 221 | return convertTypes(targ, reflect.ValueOf(doc)) 222 | } 223 | 224 | // Target is now converted, process the result set 225 | if src.Kind() == reflect.Slice { 226 | switch src.Len() { 227 | case 0: 228 | return ErrNoRecord 229 | case 1: 230 | return convertTypes(targ, src.Index(0)) 231 | default: 232 | return ErrMultipleRecords{N: src.Len(), Err: ErrUnsupportedConversion{From: src, To: targ}} 233 | } 234 | } 235 | 236 | return ErrUnsupportedConversion{From: src, To: targ} 237 | } 238 | -------------------------------------------------------------------------------- /commands_result_test.go: -------------------------------------------------------------------------------- 1 | package orient 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func documentFrom(o interface{}) *Document { 9 | doc := NewEmptyDocument() 10 | if err := doc.From(o); err != nil { 11 | panic(err) 12 | } 13 | return doc 14 | } 15 | 16 | func testResults(t testing.TB, input, dest, expect interface{}) { 17 | if err := newResults(input).All(dest); err != nil { 18 | t.Fatalf("check failed: %v", err) 19 | } else if !reflect.DeepEqual(reflect.ValueOf(dest).Elem().Interface(), expect) { 20 | t.Fatalf("wrong data: %T(%+v) != %T(%+v)", dest, dest, expect, expect) 21 | } 22 | } 23 | 24 | func TestResultsRecordsOneToMap(t *testing.T) { 25 | src := map[string]interface{}{"name": "record"} 26 | doc := documentFrom(src) 27 | var dst map[string]interface{} 28 | testResults(t, []OIdentifiable{doc}, &dst, src) 29 | } 30 | 31 | func TestResultsRecordToMap(t *testing.T) { 32 | doc := NewEmptyDocument() 33 | doc.SetFieldWithType("one", map[string]string{"name": "record"}, EMBEDDEDMAP) 34 | type Item struct { 35 | Name string 36 | } 37 | var dst map[string]*Item 38 | testResults(t, doc, &dst, map[string]*Item{ 39 | "one": &Item{"record"}, 40 | }) 41 | } 42 | 43 | func TestResultsInnerStruct(t *testing.T) { 44 | type Inner struct { 45 | Name string 46 | } 47 | type Item struct { 48 | One Inner 49 | Inner []Inner 50 | } 51 | one, two := Inner{Name: "one"}, Inner{Name: "two"} 52 | doc := NewDocument("V") 53 | doc.From(Item{One: one, Inner: []Inner{one, two}}) 54 | var dst *Item 55 | testResults(t, doc, &dst, &Item{One: one, Inner: []Inner{one, two}}) 56 | } 57 | -------------------------------------------------------------------------------- /decimal.go: -------------------------------------------------------------------------------- 1 | package orient 2 | 3 | import "math/big" 4 | 5 | func isDecimal(o interface{}) bool { 6 | switch o.(type) { 7 | case *big.Int, Decimal: 8 | return true 9 | default: 10 | return false 11 | } 12 | } 13 | 14 | type Decimal struct { 15 | Scale int 16 | Value *big.Int 17 | } 18 | -------------------------------------------------------------------------------- /dial_example_test.go: -------------------------------------------------------------------------------- 1 | package orient_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "gopkg.in/istreamdata/orientgo.v2" 7 | _ "gopkg.in/istreamdata/orientgo.v2/obinary" 8 | ) 9 | 10 | // Dial example 11 | func TestDial(t *testing.T) { 12 | addr, _ := SpinOrientServer(t) 13 | testDbName := "test_db_example" 14 | testDbUser := "root" 15 | testDbPass := "root" 16 | 17 | client, err := orient.Dial(addr) 18 | if err != nil { 19 | panic(err) 20 | } 21 | 22 | admin, err := client.Auth(testDbUser, testDbPass) 23 | if err != nil { 24 | panic(err) 25 | } 26 | 27 | // There are 2 options 28 | // 1. orient.Persistent - represents on-disk database 29 | // 2. orient.Volatile - represents in-memory database 30 | ok, err := admin.DatabaseExists(testDbName, orient.Persistent) 31 | if err != nil { 32 | panic(err) 33 | } 34 | 35 | // If database does not exist let's create it 36 | if !ok { 37 | // There are 2 options 38 | // 1. orient.GraphDB - graph database 39 | // 2. orient.DocumentDB - document database 40 | err = admin.CreateDatabase(testDbName, orient.GraphDB, orient.Persistent) 41 | if err != nil { 42 | panic(err) 43 | } 44 | } 45 | 46 | // Open wanted database & operate further 47 | database, err := client.Open(testDbName, orient.GraphDB, testDbUser, testDbPass) 48 | if err != nil { 49 | panic(err) 50 | } 51 | defer database.Close() 52 | } 53 | -------------------------------------------------------------------------------- /document.go: -------------------------------------------------------------------------------- 1 | package orient 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "time" 7 | 8 | // "database/sql/driver" 9 | "reflect" 10 | "strings" 11 | ) 12 | 13 | var ( 14 | _ OIdentifiable = (*Document)(nil) 15 | _ DocumentSerializable = (*Document)(nil) 16 | _ MapSerializable = (*Document)(nil) 17 | _ ORecord = (*Document)(nil) 18 | ) 19 | 20 | // DocEntry is a generic data holder that goes in Documents. 21 | type DocEntry struct { 22 | Name string 23 | Type OType 24 | Value interface{} 25 | } 26 | 27 | func (fld *DocEntry) String() string { 28 | if id, ok := fld.Value.(OIdentifiable); ok { 29 | return fmt.Sprintf("{%s(%s): %v}", fld.Name, fld.Type, id.GetIdentity()) 30 | } 31 | return fmt.Sprintf("{%s(%s): %v}", fld.Name, fld.Type, fld.Value) 32 | } 33 | 34 | type Document struct { 35 | BytesRecord 36 | serialized bool 37 | fieldsOrder []string // field names in the order they were added to the Document 38 | fields map[string]*DocEntry 39 | classname string // TODO: probably needs to change *OClass (once that is built) 40 | dirty bool 41 | ser RecordSerializer 42 | } 43 | 44 | func (doc *Document) ClassName() string { return doc.classname } 45 | 46 | // NewDocument should be called to create new Document objects, 47 | // since some internal data structures need to be initialized 48 | // before the Document is ready to use. 49 | func NewDocument(className string) *Document { 50 | doc := NewEmptyDocument() 51 | doc.classname = className 52 | return doc 53 | } 54 | 55 | // NewEmptyDocument creates new empty document. 56 | func NewEmptyDocument() *Document { 57 | return &Document{ 58 | BytesRecord: BytesRecord{ 59 | RID: NewEmptyRID(), 60 | Vers: -1, 61 | }, 62 | fields: make(map[string]*DocEntry), 63 | ser: GetDefaultRecordSerializer(), 64 | } 65 | } 66 | 67 | // NewDocumentFromRID creates new empty document with given RID. 68 | func NewDocumentFromRID(rid RID) *Document { 69 | return &Document{ 70 | BytesRecord: BytesRecord{ 71 | RID: rid, 72 | Vers: -1, 73 | }, 74 | fields: make(map[string]*DocEntry), 75 | ser: GetDefaultRecordSerializer(), 76 | } 77 | } 78 | 79 | func (doc *Document) ensureDecoded() error { 80 | if doc == nil { 81 | return fmt.Errorf("nil document") 82 | } 83 | if !doc.serialized { 84 | return nil 85 | } 86 | o, err := doc.ser.FromStream(doc.BytesRecord.Data) 87 | if err != nil { 88 | return err 89 | } 90 | ndoc, ok := o.(*Document) 91 | if !ok { 92 | return fmt.Errorf("expected document, got %T", o) 93 | } 94 | doc.classname = ndoc.classname 95 | doc.fields = ndoc.fields 96 | doc.fieldsOrder = ndoc.fieldsOrder 97 | doc.serialized = false 98 | return nil 99 | } 100 | 101 | func (doc *Document) Content() ([]byte, error) { 102 | // TODO: can track field changes and invalidate content if necessary - no need to serialize each time 103 | if doc.serialized { 104 | return doc.BytesRecord.Content() 105 | } else if doc.ser == nil { 106 | return nil, fmt.Errorf("document is not serialized and no serializer is set") 107 | } 108 | buf := bytes.NewBuffer(nil) 109 | if err := doc.ser.ToStream(buf, doc); err != nil { 110 | return nil, err 111 | } 112 | doc.BytesRecord.Data = buf.Bytes() 113 | return doc.BytesRecord.Content() 114 | } 115 | 116 | func (doc *Document) GetIdentity() RID { 117 | if doc == nil { 118 | return NewEmptyRID() 119 | } 120 | return doc.BytesRecord.GetIdentity() 121 | } 122 | 123 | func (doc *Document) GetRecord() interface{} { 124 | if doc == nil { 125 | return nil 126 | } 127 | return doc 128 | } 129 | 130 | // FieldNames returns the names of all the fields currently in this Document 131 | // in "entry order". These fields may not have already been committed to the database. 132 | func (doc *Document) FieldNames() []string { 133 | doc.ensureDecoded() 134 | names := make([]string, len(doc.fieldsOrder)) 135 | copy(names, doc.fieldsOrder) 136 | return names 137 | } 138 | 139 | func (doc *Document) Fields() map[string]*DocEntry { 140 | doc.ensureDecoded() 141 | return doc.fields // TODO: copy map? 142 | } 143 | 144 | // FieldsArray return the OField objects in the Document in "entry order". 145 | // There is some overhead to getting them in entry order, so if you 146 | // don't care about that order, just access the Fields field of the 147 | // Document struct directly. 148 | func (doc *Document) FieldsArray() []*DocEntry { 149 | doc.ensureDecoded() 150 | fields := make([]*DocEntry, len(doc.fieldsOrder)) 151 | for i, name := range doc.fieldsOrder { 152 | fields[i] = doc.fields[name] 153 | } 154 | return fields 155 | } 156 | 157 | // GetFieldByName looks up the OField in this document with the specified field. 158 | // If no field is found with that name, nil is returned. 159 | func (doc *Document) GetField(fname string) *DocEntry { 160 | doc.ensureDecoded() 161 | return doc.fields[fname] 162 | } 163 | 164 | // AddField adds a fully created field directly rather than by some of its 165 | // attributes, as the other "Field" methods do. 166 | // The same *Document is returned to allow call chaining. 167 | func (doc *Document) AddField(name string, field *DocEntry) *Document { 168 | doc.ensureDecoded() 169 | doc.fields[name] = field 170 | doc.fieldsOrder = append(doc.fieldsOrder, name) 171 | doc.dirty = true 172 | return doc 173 | } 174 | 175 | func (doc *Document) SetDirty(b bool) { 176 | doc.dirty = b 177 | } 178 | 179 | // SetField is used to add a new field to a document. This will usually be done just 180 | // before calling Save and sending it to the database. The field type will be inferred 181 | // via type switch analysis on `val`. Use FieldWithType to specify the type directly. 182 | // The same *Document is returned to allow call chaining. 183 | func (doc *Document) SetField(name string, val interface{}) *Document { 184 | doc.ensureDecoded() 185 | return doc.SetFieldWithType(name, val, OTypeForValue(val)) 186 | } 187 | 188 | // FieldWithType is used to add a new field to a document. This will usually be done just 189 | // before calling Save and sending it to the database. The `fieldType` must correspond 190 | // one of the OrientDB type in the schema pkg constants. It will follow the same list 191 | // as: https://github.com/orientechnologies/orientdb/wiki/Types 192 | // The same *Document is returned to allow call chaining. 193 | func (doc *Document) SetFieldWithType(name string, val interface{}, fieldType OType) *Document { 194 | doc.ensureDecoded() 195 | fld := &DocEntry{ 196 | Name: name, 197 | Value: val, 198 | Type: fieldType, 199 | } 200 | 201 | if fieldType == DATE { 202 | fld.Value = adjustDateToMidnight(val) 203 | } else if fieldType == DATETIME { 204 | fld.Value = roundDateTimeToMillis(val) 205 | } 206 | 207 | return doc.AddField(name, fld) 208 | } 209 | 210 | func (doc *Document) RawContainsField(name string) bool { 211 | doc.ensureDecoded() 212 | return doc != nil && doc.fields[name] != nil 213 | } 214 | 215 | func (doc *Document) RawSetField(name string, val interface{}, fieldType OType) { 216 | doc.SetFieldWithType(name, val, fieldType) // TODO: implement in a right way 217 | } 218 | 219 | // roundDateTimeToMillis zeros out the micro and nanoseconds of a 220 | // time.Time object in order to match the precision with which 221 | // the OrientDB stores DATETIME values 222 | func roundDateTimeToMillis(val interface{}) interface{} { 223 | tm, ok := val.(time.Time) 224 | if !ok { 225 | // if the type is wrong, we will flag it as an error when the user tries 226 | // to save it, rather than here while buidling the document 227 | return val 228 | } 229 | 230 | return tm.Round(time.Millisecond) 231 | } 232 | 233 | // adjustDateToMidnight zeros out the hour, minute, second, etc. 234 | // to set the time of a DATE to midnight. This matches the 235 | // precision with which the OrientDB stores DATE values. 236 | func adjustDateToMidnight(val interface{}) interface{} { 237 | tm, ok := val.(time.Time) 238 | if !ok { 239 | // if the type is wrong, we will flag it as an error when the user tries 240 | // to save it, rather than here while buidling the document 241 | return val 242 | } 243 | tmMidnight := time.Date(tm.Year(), tm.Month(), tm.Day(), 0, 0, 0, 0, tm.Location()) 244 | return interface{}(tmMidnight) 245 | } 246 | 247 | func (doc *Document) String() string { 248 | class := doc.classname 249 | if class == "" { 250 | class = "" 251 | } 252 | if doc.serialized { 253 | return fmt.Sprintf("Document{Class: %s, RID: %s, Vers: %d, Fields: [serialized]}", 254 | class, doc.RID, doc.Vers) 255 | } 256 | buf := new(bytes.Buffer) 257 | _, err := buf.WriteString(fmt.Sprintf("Document{Class: %s, RID: %s, Vers: %d, Fields: [\n", 258 | class, doc.RID, doc.Vers)) 259 | if err != nil { 260 | panic(err) 261 | } 262 | 263 | for _, fld := range doc.fields { 264 | _, err = buf.WriteString(fmt.Sprintf(" %s,\n", fld.String())) 265 | if err != nil { 266 | panic(err) 267 | } 268 | } 269 | 270 | buf.WriteString("]}\n") 271 | return buf.String() 272 | } 273 | 274 | func (doc *Document) ToMap() (map[string]interface{}, error) { 275 | if doc == nil { 276 | return nil, nil 277 | } 278 | if err := doc.ensureDecoded(); err != nil { 279 | return nil, err 280 | } 281 | out := make(map[string]interface{}, len(doc.fields)) 282 | for name, fld := range doc.fields { 283 | out[name] = fld.Value 284 | } 285 | if doc.classname != "" { 286 | out["@class"] = doc.classname 287 | } 288 | if doc.RID.IsPersistent() { // TODO: is this correct? 289 | out["@rid"] = doc.RID 290 | } 291 | return out, nil 292 | } 293 | 294 | func (doc *Document) FillClassNameIfNeeded(name string) { 295 | if doc.classname == "" { 296 | doc.SetClassNameIfExists(name) 297 | } 298 | } 299 | 300 | func (doc *Document) SetClassNameIfExists(name string) { 301 | // TODO: implement class lookup 302 | // _immutableClazz = null; 303 | // _immutableSchemaVersion = -1; 304 | 305 | doc.classname = name 306 | if name == "" { 307 | return 308 | } 309 | 310 | // final ODatabaseDocument db = getDatabaseIfDefined(); 311 | // if (db != null) { 312 | // final OClass _clazz = ((OMetadataInternal) db.getMetadata()).getImmutableSchemaSnapshot().getClass(iClassName); 313 | // if (_clazz != null) { 314 | // _className = _clazz.getName(); 315 | // convertFieldsToClass(_clazz); 316 | // } 317 | // } 318 | } 319 | 320 | // SetSerializer sets RecordSerializer for encoding/decoding a Document 321 | func (doc *Document) SetSerializer(ser RecordSerializer) { 322 | doc.ser = ser 323 | } 324 | func (doc *Document) Fill(rid RID, version int, content []byte) error { 325 | doc.serialized = doc.serialized || doc.BytesRecord.Data == nil || bytes.Compare(content, doc.BytesRecord.Data) != 0 326 | return doc.BytesRecord.Fill(rid, version, content) 327 | } 328 | func (doc *Document) RecordType() RecordType { return RecordTypeDocument } 329 | 330 | // ToDocument implement DocumentSerializable interface. In this case, Document just returns itself. 331 | func (doc *Document) ToDocument() (*Document, error) { 332 | return doc, nil 333 | } 334 | 335 | // ToStruct fills provided struct with content of a Document. Argument must be a pointer to structure. 336 | func (doc *Document) ToStruct(o interface{}) error { 337 | mp, err := doc.ToMap() 338 | if err != nil { 339 | return err 340 | } 341 | return mapToStruct(mp, o) 342 | } 343 | 344 | func (doc *Document) setFieldsFrom(rv reflect.Value) error { 345 | switch rv.Kind() { 346 | case reflect.Struct: 347 | rt := rv.Type() 348 | for i := 0; i < rt.NumField(); i++ { 349 | fld := rt.Field(i) 350 | if !isExported(fld.Name) { 351 | continue 352 | } 353 | name := fld.Name 354 | tags := strings.Split(fld.Tag.Get(TagName), ",") 355 | if tags[0] == "-" { 356 | continue 357 | } 358 | if tags[0] != "" { 359 | name = tags[0] 360 | } 361 | squash := (len(tags) > 1 && tags[1] == "squash") // TODO: change default behavior to squash if field is anonymous 362 | if squash { 363 | if err := doc.setFieldsFrom(rv.Field(i)); err != nil { 364 | return fmt.Errorf("field '%s': %s", name, err) 365 | } 366 | } else { 367 | doc.SetField(name, rv.Field(i).Interface()) 368 | } 369 | } 370 | return nil 371 | case reflect.Map: 372 | for _, key := range rv.MapKeys() { 373 | doc.SetField(fmt.Sprint(key.Interface()), rv.MapIndex(key).Interface()) 374 | } 375 | return nil 376 | default: 377 | return fmt.Errorf("only maps and structs are supported, got: %T", rv.Interface()) 378 | } 379 | } 380 | 381 | // From sets Document fields to values provided in argument (which can be a map or a struct). 382 | // 383 | // From uses TagName field tag to determine field name and conversion parameters. 384 | // For now it supports only one special tag parameter: ",squash" which can be used to inline fields into parent struct. 385 | func (doc *Document) From(o interface{}) error { 386 | // TODO: clear fields and serialized data 387 | if o == nil { 388 | return nil 389 | } 390 | rv := reflect.ValueOf(o) 391 | if rv.Kind() == reflect.Ptr || rv.Kind() == reflect.Interface { 392 | rv = rv.Elem() 393 | } 394 | return doc.setFieldsFrom(rv) 395 | } 396 | 397 | /* 398 | // Implements database/sql.Scanner interface 399 | func (doc *Document) Scan(src interface{}) error { 400 | switch v := src.(type) { 401 | case *Document: 402 | *doc = *v 403 | default: 404 | return fmt.Errorf("Document: cannot convert from %T to %T", src, doc) 405 | } 406 | return nil 407 | } 408 | 409 | // Implements database/sql/driver.Valuer interface 410 | // TODO: haven't detected when this is called yet (probably when serializing Document for insertion into DB??) 411 | func (doc *Document) Value() (driver.Value, error) { 412 | if glog.V(10) { 413 | glog.Infoln("** Document.Value") 414 | } 415 | return []byte(`{"b": 2}`), nil // FIXME: bogus 416 | } 417 | 418 | // Implements database/sql/driver.ValueConverter interface 419 | // TODO: haven't detected when this is called yet 420 | func (doc *Document) ConvertValue(v interface{}) (driver.Value, error) { 421 | if glog.V(10) { 422 | glog.Infof("** Document.ConvertValue: %T: %v", v, v) 423 | } 424 | return []byte(`{"a": 1}`), nil // FIXME: bogus 425 | }*/ 426 | -------------------------------------------------------------------------------- /document_test.go: -------------------------------------------------------------------------------- 1 | package orient_test 2 | 3 | import ( 4 | "gopkg.in/istreamdata/orientgo.v2" 5 | "reflect" 6 | "testing" 7 | ) 8 | 9 | func TestDocumentFromStruct(t *testing.T) { 10 | doc := orient.NewEmptyDocument() 11 | type item struct { 12 | Ind int 13 | Name string 14 | } 15 | a := item{11, "named"} 16 | if err := doc.From(a); err != nil { 17 | t.Fatal(err) 18 | } 19 | var b item 20 | if err := doc.ToStruct(&b); err != nil { 21 | t.Fatal(err) 22 | } else if b != a { 23 | t.Fatal("data differs") 24 | } 25 | } 26 | 27 | func TestDocumentFromStructEmbedded(t *testing.T) { 28 | doc := orient.NewEmptyDocument() 29 | type Item struct { 30 | Ind int 31 | Name string 32 | } 33 | type obj struct { 34 | Item Item 35 | Data string 36 | } 37 | a := obj{Item: Item{11, "named"}, Data: "dataz"} 38 | if err := doc.From(a); err != nil { 39 | t.Fatal(err) 40 | } else if doc.GetField("Item").Type != orient.EMBEDDED { 41 | t.Fatal("wrong field type") 42 | } 43 | var b obj 44 | if err := doc.ToStruct(&b); err != nil { 45 | t.Fatal(err) 46 | } else if b != a { 47 | t.Fatal("data differs") 48 | } 49 | } 50 | 51 | func TestDocumentFromStructEmbeddedAnon(t *testing.T) { 52 | doc := orient.NewEmptyDocument() 53 | type Item struct { 54 | Ind int 55 | Name string 56 | } 57 | type obj struct { 58 | Item `mapstructure:",squash"` 59 | Data string 60 | } 61 | a := obj{Item: Item{11, "named"}, Data: "dataz"} 62 | if err := doc.From(a); err != nil { 63 | t.Fatal(err) 64 | } else if doc.GetField("Item") != nil { 65 | t.Fatal("default behavior should be squash") 66 | } 67 | var b obj 68 | if err := doc.ToStruct(&b); err != nil { 69 | t.Fatal(err) 70 | } else if b != a { 71 | t.Fatalf("data differs: %+v vs %+v", a, b) 72 | } 73 | } 74 | 75 | func TestDocumentFromMap(t *testing.T) { 76 | doc := orient.NewEmptyDocument() 77 | type item struct { 78 | Ind int 79 | Name string 80 | } 81 | a := map[string]interface{}{"Ind": 11, "Name": "named"} 82 | if err := doc.From(a); err != nil { 83 | t.Fatal(err) 84 | } 85 | if b, err := doc.ToMap(); err != nil { 86 | t.Fatal(err) 87 | } else if !reflect.DeepEqual(a, b) { 88 | t.Fatal("data differs") 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /errors.go: -------------------------------------------------------------------------------- 1 | package orient 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | ) 7 | 8 | var ( 9 | exceptions = make(map[string]func(e Exception) Exception) 10 | ) 11 | 12 | // RegException registers a function to convert server exception based on it's class. 13 | func RegException(class string, fnc func(e Exception) Exception) { 14 | exceptions[class] = fnc 15 | } 16 | 17 | func init() { 18 | RegException("com.orientechnologies.orient.core.exception.OConcurrentModificationException", func(e Exception) Exception { 19 | return ErrConcurrentModification{e} 20 | }) 21 | } 22 | 23 | // Exception is an interface for Java-based Exceptions. 24 | type Exception interface { 25 | error 26 | // Returns Java exception class 27 | ExcClass() string 28 | // Returns exception message 29 | ExcMessage() string 30 | } 31 | 32 | // UnknownException is an arbitrary exception from Java side. 33 | // If exception class is not recognized by this driver, it will return UnknownException. 34 | type UnknownException struct { 35 | Class string 36 | Message string 37 | } 38 | 39 | // ExcClass returns Java exception class 40 | func (e UnknownException) ExcClass() string { 41 | return e.Class 42 | } 43 | 44 | // ExcMessage returns exception message 45 | func (e UnknownException) ExcMessage() string { 46 | return e.Message 47 | } 48 | func (e UnknownException) Error() string { 49 | return e.Class + ": " + e.Message 50 | } 51 | 52 | // OServerException encapsulates Java-based Exceptions from 53 | // the OrientDB server. OrientDB can return multiple exceptions 54 | // for a single query/command, so they are all encapsulated in 55 | // one OServerException object. 56 | type OServerException struct { 57 | Exceptions []Exception 58 | } 59 | 60 | func (e OServerException) Error() string { 61 | var buf bytes.Buffer 62 | buf.WriteString("OrientDB Server Exception: ") 63 | for _, ex := range e.Exceptions { 64 | buf.WriteString("\n ") 65 | buf.WriteString(ex.Error()) 66 | } 67 | return buf.String() 68 | } 69 | 70 | // ErrInvalidConn is returned than DB functions are called without active DB connection 71 | type ErrInvalidConn struct { 72 | Msg string 73 | } 74 | 75 | func (e ErrInvalidConn) Error() string { 76 | return "Invalid Connection: %s" + e.Msg 77 | } 78 | 79 | // ErrNoRecord is returned when trying to deserialize an empty result set into a single value. 80 | var ErrNoRecord = fmt.Errorf("no records returned, while expecting one") 81 | 82 | // ErrMultipleRecords is returned when trying to deserialize a result set with multiple records into a single value. 83 | type ErrMultipleRecords struct { 84 | N int 85 | Err error 86 | } 87 | 88 | func (e ErrMultipleRecords) Error() string { 89 | return fmt.Sprintf("multiple records returned (%d), while expecting one: %s", e.N, e.Err) 90 | } 91 | 92 | func convertError(err error) error { 93 | if err == nil { 94 | return nil 95 | } 96 | if errs, ok := err.(OServerException); ok { 97 | for _, e := range errs.Exceptions { 98 | if fnc, ok := exceptions[e.ExcClass()]; ok { 99 | return fnc(e) 100 | } 101 | } 102 | } 103 | return err 104 | } 105 | 106 | type ErrConcurrentModification struct { 107 | Exception 108 | } 109 | 110 | func (e ErrConcurrentModification) Error() string { 111 | return fmt.Sprintf("concurrent modification: %v", e.Exception) 112 | } 113 | -------------------------------------------------------------------------------- /functions.go: -------------------------------------------------------------------------------- 1 | package orient 2 | 3 | // List of supported server-side script languages 4 | const ( 5 | LangSQL = ScriptLang("sql") 6 | LangJS = ScriptLang("javascript") 7 | LangGroovy = ScriptLang("groovy") 8 | ) 9 | 10 | // ScriptLang is a type for supported server-side script languages 11 | type ScriptLang string 12 | 13 | // Function is a server-side function description 14 | type Function struct { 15 | Name string 16 | Lang ScriptLang 17 | Params []string 18 | Idemp bool // is idempotent 19 | Code string 20 | } 21 | -------------------------------------------------------------------------------- /global_property.go: -------------------------------------------------------------------------------- 1 | package orient 2 | 3 | // OGlobalProperty is used by OrientDB to efficiently store "property" (field) 4 | // types and names (but not values) across all clusters in a database 5 | // These are stored in record #0:1 of a database and loaded when the DBClient 6 | // starts up. (TODO: it will also need to be updated when new fields are added 7 | // at runtime) 8 | type OGlobalProperty struct { 9 | Id int32 // TODO: change to int? 10 | Name string 11 | Type OType 12 | } 13 | 14 | // based on how the Java client does it ; TODO: document usage 15 | func NewGlobalPropertyFromDocument(doc *Document) OGlobalProperty { 16 | // set defaults 17 | id := int32(-1) 18 | name := "" 19 | typ := ANY // TODO: this may not be the right choice - might need to create UNKNOWN ? 20 | 21 | if fld := doc.GetField("id"); fld != nil { 22 | id = fld.Value.(int32) 23 | } 24 | if fld := doc.GetField("name"); fld != nil { 25 | name = fld.Value.(string) 26 | } 27 | if fld := doc.GetField("type"); fld != nil { 28 | typ = OTypeFromString(fld.Value.(string)) 29 | } 30 | 31 | return OGlobalProperty{id, name, typ} 32 | } 33 | 34 | // TODO: Java client also has `toDocument` 35 | // func (gp OGlobalProperty) ToDocument() *Document { 36 | // // TODO: impl me ??? 37 | // } 38 | -------------------------------------------------------------------------------- /link.go: -------------------------------------------------------------------------------- 1 | package orient 2 | 3 | import ( 4 | "fmt" 5 | "github.com/nu7hatch/gouuid" 6 | "gopkg.in/istreamdata/orientgo.v2/obinary/rw" 7 | "io" 8 | ) 9 | 10 | type OIdentifiableCollection interface { 11 | Len() int 12 | OIdentifiableIterator() <-chan OIdentifiable 13 | } 14 | 15 | func NewRidBag() *RidBag { 16 | return &RidBag{delegate: newEmbeddedRidBag()} 17 | } 18 | 19 | // RidBag can have a tree-based or an embedded representation. 20 | // 21 | // Embedded stores its content directly to the document that owns it. 22 | // It is used when only small numbers of links are stored in the bag. 23 | // 24 | // The tree-based implementation stores its content in a separate data 25 | // structure called on OSBTreeBonsai on the server. It fits great for cases 26 | // when you have a large number of links. This is used to efficiently 27 | // manage relationships (particularly in graph databases). 28 | // 29 | // The RidBag struct corresponds to ORidBag in Java client codebase. 30 | type RidBag struct { 31 | id uuid.UUID 32 | delegate ridBagDelegate 33 | owner *Document 34 | } 35 | 36 | func (bag *RidBag) SetOwner(doc *Document) { 37 | bag.owner = doc 38 | } 39 | func (bag *RidBag) FromStream(r io.Reader) error { 40 | br := rw.NewReader(r) 41 | first := br.ReadByte() 42 | if err := br.Err(); err != nil { 43 | return err 44 | } 45 | if first&0x1 != 0 { 46 | bag.delegate = newEmbeddedRidBag() 47 | } else { 48 | bag.delegate = newSBTreeRidBag() 49 | } 50 | if first&0x2 != 0 { 51 | br.ReadRawBytes(bag.id[:]) 52 | } 53 | if err := br.Err(); err != nil { 54 | return err 55 | } 56 | return bag.delegate.deserializeDelegate(br) 57 | } 58 | func (bag *RidBag) ToStream(w io.Writer) error { 59 | var first byte 60 | hasUUID := false // TODO: do we need to send it? 61 | if !bag.IsRemote() { 62 | first |= 0x1 63 | } 64 | if hasUUID { 65 | first |= 0x2 66 | } 67 | bw := rw.NewWriter(w) 68 | bw.WriteByte(first) 69 | if hasUUID { 70 | bw.WriteRawBytes(bag.id[:]) 71 | } 72 | return bag.delegate.serializeDelegate(bw) 73 | } 74 | func (bag *RidBag) IsRemote() bool { 75 | switch bag.delegate.(type) { 76 | case *sbTreeRidBag: 77 | return true 78 | default: 79 | return false 80 | } 81 | } 82 | 83 | type ridBagDelegate interface { 84 | deserializeDelegate(br *rw.Reader) error 85 | serializeDelegate(bw *rw.Writer) error 86 | } 87 | 88 | func newEmbeddedRidBag() ridBagDelegate { return &embeddedRidBag{} } 89 | 90 | type embeddedRidBag struct { 91 | links []OIdentifiable 92 | } 93 | 94 | func (bag *embeddedRidBag) deserializeDelegate(br *rw.Reader) error { 95 | n := int(br.ReadInt()) 96 | bag.links = make([]OIdentifiable, n) 97 | for i := range bag.links { 98 | var rid RID 99 | if err := rid.FromStream(br); err != nil { 100 | return err 101 | } 102 | bag.links[i] = rid 103 | } 104 | return br.Err() 105 | } 106 | func (bag *embeddedRidBag) serializeDelegate(bw *rw.Writer) error { 107 | bw.WriteInt(int32(len(bag.links))) 108 | for _, l := range bag.links { 109 | if err := l.GetIdentity().ToStream(bw); err != nil { 110 | return err 111 | } 112 | } 113 | return bw.Err() 114 | } 115 | 116 | func newSBTreeRidBag() ridBagDelegate { return &sbTreeRidBag{} } 117 | func newBonsaiCollectionPtr(fileId int64, pageIndex int64, pageOffset int) *bonsaiCollectionPtr { 118 | return &bonsaiCollectionPtr{ 119 | fileId: fileId, 120 | pageIndex: pageIndex, 121 | pageOffset: pageOffset, 122 | } 123 | } 124 | 125 | type bonsaiCollectionPtr struct { 126 | fileId int64 127 | pageIndex int64 128 | pageOffset int 129 | } 130 | type sbTreeRidBag struct { 131 | collectionPtr *bonsaiCollectionPtr 132 | changes map[RID][]interface{} 133 | size int 134 | } 135 | 136 | func (bag *sbTreeRidBag) serializeDelegate(bw *rw.Writer) error { 137 | if bag.collectionPtr == nil { 138 | bw.WriteLong(-1) 139 | bw.WriteLong(-1) 140 | bw.WriteInt(-1) 141 | } else { 142 | bw.WriteLong(bag.collectionPtr.fileId) 143 | bw.WriteLong(bag.collectionPtr.pageIndex) 144 | bw.WriteInt(int32(bag.collectionPtr.pageOffset)) 145 | } 146 | bw.WriteInt(-1) // TODO: cached size; need a real value for compatibility with <= 1.7.5 147 | bw.WriteInt(0) // TODO: support changes in sbTreeRidBag 148 | return bw.Err() 149 | } 150 | func (bag *sbTreeRidBag) deserializeDelegate(br *rw.Reader) error { 151 | fileId := br.ReadLong() 152 | pageIndex := br.ReadLong() 153 | pageOffset := int(br.ReadInt()) 154 | br.ReadInt() // Cached bag size. Not used after 1.7.5 155 | if err := br.Err(); err != nil { 156 | return err 157 | } 158 | if fileId == -1 { 159 | bag.collectionPtr = nil 160 | } else { 161 | bag.collectionPtr = newBonsaiCollectionPtr(fileId, pageIndex, pageOffset) 162 | } 163 | bag.size = -1 164 | return bag.deserializeChanges(br) 165 | } 166 | func (bag *sbTreeRidBag) deserializeChanges(r *rw.Reader) (err error) { 167 | n := int(r.ReadInt()) 168 | changes := make(map[RID][]interface{}) 169 | 170 | type change struct { 171 | diff bool 172 | val int 173 | } 174 | 175 | for i := 0; i < n; i++ { 176 | var rid RID 177 | if err = rid.FromStream(r); err != nil { 178 | return err 179 | } 180 | chval := int(r.ReadInt()) 181 | chtp := int(r.ReadByte()) 182 | arr := changes[rid] 183 | switch chtp { 184 | case 1: // abs 185 | arr = append(arr, change{diff: false, val: chval}) 186 | case 0: // diff 187 | arr = append(arr, change{diff: true, val: chval}) 188 | default: 189 | return fmt.Errorf("unknown change type: %d", chtp) 190 | } 191 | changes[rid] = arr 192 | } 193 | bag.changes = changes 194 | return r.Err() 195 | } 196 | -------------------------------------------------------------------------------- /logo/orientgo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/istreamdata/orientgo/625dcbcc6aa95a11c5ac4e246f49421c5a633d1f/logo/orientgo.png -------------------------------------------------------------------------------- /logo/orientgo.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/istreamdata/orientgo/625dcbcc6aa95a11c5ac4e246f49421c5a633d1f/logo/orientgo.xcf -------------------------------------------------------------------------------- /mapstructure.go: -------------------------------------------------------------------------------- 1 | package orient 2 | 3 | import ( 4 | "github.com/mitchellh/mapstructure" 5 | "reflect" 6 | "time" 7 | ) 8 | 9 | // TagName is a name for a struct tag used for types conversion using reflect 10 | var TagName = "mapstructure" 11 | 12 | var mapDecoderHooks = []mapstructure.DecodeHookFunc{ 13 | stringToTimeHookFunc, 14 | stringToByteSliceHookFunc, 15 | documentToMapHookFunc, 16 | } 17 | 18 | // RegisterMapDecoderHook allows to register additional hook for map decoder 19 | func RegisterMapDecoderHook(hook mapstructure.DecodeHookFunc) { 20 | mapDecoderHooks = append(mapDecoderHooks, hook) 21 | } 22 | 23 | // NewMapDecoder returns decoder configured for decoding data into result with all registered hooks. 24 | func newMapDecoder(result interface{}) (*mapstructure.Decoder, error) { 25 | return mapstructure.NewDecoder(&mapstructure.DecoderConfig{ 26 | DecodeHook: mapstructure.ComposeDecodeHookFunc(mapDecoderHooks...), 27 | Metadata: nil, 28 | Result: result, 29 | TagName: TagName, 30 | }) 31 | } 32 | 33 | var reflTimeType = reflect.TypeOf((*time.Time)(nil)).Elem() 34 | 35 | // StringToTimeHookFunc returns a DecodeHookFunc that converts 36 | // strings to time.Time using RFC3339Nano format. 37 | func stringToTimeHookFunc(f reflect.Type, t reflect.Type, data interface{}) (interface{}, error) { 38 | if f.Kind() != reflect.String || t != reflTimeType { 39 | return data, nil 40 | } 41 | return time.Parse(time.RFC3339Nano, data.(string)) 42 | } 43 | 44 | var reflByteSliceType = reflect.TypeOf(([]byte)(nil)) 45 | 46 | // StringToByteSliceHookFunc returns a DecodeHookFunc that converts strings to []byte. 47 | func stringToByteSliceHookFunc(f reflect.Type, t reflect.Type, data interface{}) (interface{}, error) { 48 | if f.Kind() != reflect.String || t != reflByteSliceType { 49 | return data, nil 50 | } 51 | return []byte(data.(string)), nil 52 | } 53 | 54 | var reflDocumentType = reflect.TypeOf((*Document)(nil)) 55 | 56 | func documentToMapHookFunc(f reflect.Type, t reflect.Type, data interface{}) (interface{}, error) { 57 | if f != reflDocumentType { 58 | return data, nil 59 | } 60 | return data.(*Document).ToMap() 61 | } 62 | -------------------------------------------------------------------------------- /obinary/binserde/typeserializer.go: -------------------------------------------------------------------------------- 1 | package binserde 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "reflect" 7 | 8 | "gopkg.in/istreamdata/orientgo.v2" 9 | "gopkg.in/istreamdata/orientgo.v2/obinary/rw" 10 | ) 11 | 12 | // There is apparently a second "binary serialization" system 13 | // in OrientDB that has inconsistent type constants with the 14 | // other one. 15 | // 16 | // Until I understand it better, for now I'm calling it, the 17 | // "typeserializer" though that is misleading since the binserde.go 18 | // Serializer also reads/writes (de/serializes) types. 19 | 20 | // from Java client code base where all these extend 21 | // com.orientechnologies.common.serialization.types.OBinarySerializer 22 | const ( 23 | BooleanSerializer = 1 24 | ByteSerializer = 2 25 | CharSerializer = 3 26 | DateSerializer = 4 27 | DateTimeSerializer = 5 28 | DoubleSerializer = 6 29 | FloatSerializer = 7 30 | IntegerSerializer = 8 31 | LinkSerializer = 9 32 | LongSerializer = 10 33 | NullSerializer = 11 34 | ShortSerializer = 12 35 | StringSerializer = 13 36 | CompositeKeySerializer = 14 37 | SimpleKeySerializer = 15 38 | StreamSerializerRID = 16 39 | BinaryTypeSerializer = 17 40 | DecimalSerializer = 18 41 | StreamSerializerListRID = 19 42 | StreamSerializerOldRIDContainer = 20 43 | StreamSerializerSBTreeIndexRIDContainer = 21 44 | PhysicalPositionSerializer = 50 45 | ) 46 | 47 | type OBinaryTypeSerializer interface { 48 | Deserialize(r io.Reader) (interface{}, error) 49 | Serialize(val interface{}) ([]byte, error) 50 | } 51 | 52 | var TypeSerializers [21]OBinaryTypeSerializer 53 | 54 | type OLinkSerializer struct{} 55 | 56 | func (ols OLinkSerializer) Deserialize(r io.Reader) (v interface{}, err error) { 57 | return ols.DeserializeLink(r) 58 | } 59 | func (ols OLinkSerializer) DeserializeLink(r io.Reader) (v orient.OIdentifiable, err error) { 60 | defer func() { 61 | if r := recover(); r != nil { 62 | err = fmt.Errorf("deserialize error: %v", r) 63 | } 64 | }() 65 | var rid orient.RID 66 | if err = rid.FromStream(r); err != nil { 67 | return 68 | } 69 | return rid, nil 70 | } 71 | 72 | // Serialize serializes a *orient.OLink into the binary format 73 | // required by the OrientDB server. If the `val` passed in is 74 | // not a *orient.OLink, the method will panic. 75 | func (ols OLinkSerializer) Serialize(val interface{}) ([]byte, error) { 76 | lnk, ok := val.(orient.OIdentifiable) 77 | if !ok { 78 | return nil, fmt.Errorf("Invalid LINK should be orient.OLink, got %s", reflect.TypeOf(val)) 79 | } 80 | rid := lnk.GetIdentity() 81 | 82 | bs := make([]byte, rw.SizeShort+rw.SizeLong) 83 | rw.Order.PutUint16(bs[:rw.SizeShort], uint16(rid.ClusterID)) 84 | rw.Order.PutUint64(bs[rw.SizeShort:], uint64(rid.ClusterPos)) 85 | return bs, nil 86 | } 87 | 88 | func init() { 89 | TypeSerializers[LinkSerializer] = OLinkSerializer{} 90 | } 91 | -------------------------------------------------------------------------------- /obinary/client.go: -------------------------------------------------------------------------------- 1 | package obinary 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "io" 7 | "log" 8 | "net" 9 | "sync" 10 | "time" 11 | 12 | "gopkg.in/istreamdata/orientgo.v2" 13 | "gopkg.in/istreamdata/orientgo.v2/obinary/rw" 14 | ) 15 | 16 | func init() { 17 | orient.RegisterProto(orient.ProtoBinary, func(addr string) (orient.DBConnection, error) { 18 | return Dial(addr) 19 | }) 20 | } 21 | 22 | func validateAddr(addr string) (string, error) { 23 | var host, port string 24 | if addr != "" { 25 | var err error 26 | host, port, err = net.SplitHostPort(addr) 27 | if err != nil { 28 | return "", err 29 | } 30 | } 31 | if host == "" { 32 | host = "localhost" 33 | } 34 | if port == "" { 35 | port = "2424" // binary port range is: 2424-2430 36 | } 37 | return net.JoinHostPort(host, port), nil 38 | } 39 | 40 | // Dial creates a new binary connection to OrientDB server. 41 | // The Client returned is ready to make calls to the OrientDB but has not 42 | // yet established a database session or a session with the OrientDB server. 43 | // After this, the user needs to call either OpenDatabase or CreateServerSession. 44 | func Dial(addr string) (*Client, error) { 45 | addr, err := validateAddr(addr) 46 | if err != nil { 47 | return nil, err 48 | } 49 | conn, err := net.DialTimeout("tcp", addr, time.Minute) 50 | if err != nil { 51 | return nil, err 52 | } 53 | c := &Client{ 54 | addr: addr, conn: conn, done: make(chan struct{}), 55 | br: bufio.NewReader(conn), bw: bufio.NewWriter(conn), 56 | } 57 | c.pr = rw.NewReader(c.br) 58 | c.pw = rw.NewWriter(c.bw) 59 | if err := c.handshakeVersion(); err != nil { 60 | conn.Close() 61 | return nil, err 62 | } 63 | c.sess = make(map[int32]*session) 64 | c.root = c.newSess(noSessionId) 65 | go c.run() 66 | return c, nil 67 | } 68 | 69 | // Client encapsulates the active TCP connection to an OrientDB server 70 | // to be used with the Network Binary Protocol. 71 | // It also may be connected to up to one database at a time. 72 | // Do not create a Client struct directly. You should use NewClient, 73 | // followed immediately by ConnectToServer, to connect to the OrientDB server, 74 | // or OpenDatabase, to connect to a database on the server. 75 | type Client struct { 76 | addr string 77 | 78 | done chan struct{} 79 | 80 | conn net.Conn 81 | br *bufio.Reader 82 | pr *rw.Reader 83 | bw *bufio.Writer 84 | pw *rw.Writer 85 | cmuw sync.Mutex 86 | 87 | root *session 88 | 89 | sessmu sync.RWMutex 90 | sess map[int32]*session 91 | 92 | currmu sync.RWMutex 93 | currdb *Database // only one db session open at a time 94 | 95 | srvProtoVers int 96 | curProtoVers int 97 | 98 | recordFormat orient.RecordSerializer 99 | } 100 | 101 | func (c *Client) handshakeVersion() error { 102 | c.conn.SetReadDeadline(time.Now().Add(time.Second * 5)) 103 | defer c.conn.SetReadDeadline(time.Time{}) 104 | 105 | c.srvProtoVers = int(c.pr.ReadShort()) 106 | if err := c.pr.Err(); err != nil { 107 | return err 108 | } 109 | if c.srvProtoVers < MinProtocolVersion { // || c.srvProtoVers > MaxProtocolVersion { 110 | return ErrUnsupportedVersion(c.srvProtoVers) 111 | } else if c.srvProtoVers < minBinarySerializerVersion { // may switch to CSV serialization, but we don't care for now 112 | return ErrUnsupportedVersion(c.srvProtoVers) 113 | } else if c.srvProtoVers > MaxProtocolVersion { 114 | log.Printf("OrientDB version is unsupported by driver: %d vs %d. Will fallback to protocol %d.", 115 | MaxProtocolVersion, c.srvProtoVers, CurrentProtoVersion) 116 | } 117 | c.recordFormat = orient.GetDefaultRecordSerializer() 118 | c.curProtoVers = CurrentProtoVersion 119 | if c.curProtoVers > c.srvProtoVers { 120 | c.curProtoVers = c.srvProtoVers 121 | } 122 | return nil 123 | } 124 | 125 | func (c *Client) writeCmd(op byte, sid int32, wr func(*rw.Writer) error) error { 126 | c.cmuw.Lock() 127 | defer c.cmuw.Unlock() 128 | c.pw.WriteByte(op) 129 | c.pw.WriteInt(sid) 130 | if wr != nil { 131 | if err := wr(c.pw); err != nil { 132 | return err 133 | } 134 | } 135 | c.bw.Flush() 136 | return c.pw.Err() 137 | } 138 | 139 | func (c *Client) newSess(id int32) *session { 140 | c.sessmu.Lock() 141 | s := c.sess[id] 142 | if s == nil { 143 | s = &session{id: id, cli: c, in: make(chan resp)} 144 | c.sess[id] = s 145 | } 146 | c.sessmu.Unlock() 147 | return s 148 | } 149 | 150 | func (c *Client) closeSess(id int32, ref *Database) { 151 | c.sessmu.Lock() 152 | delete(c.sess, id) 153 | c.sessmu.Unlock() 154 | c.currmu.Lock() 155 | if c.currdb == ref { 156 | c.currdb = nil 157 | } 158 | c.currmu.Unlock() 159 | } 160 | 161 | func newReadChanCloser(r io.Reader, ch chan struct{}) *readChanCloser { 162 | return &readChanCloser{ 163 | r: r, done: ch, 164 | } 165 | } 166 | 167 | type readChanCloser struct { 168 | r io.Reader 169 | done chan struct{} 170 | } 171 | 172 | func (r *readChanCloser) Read(p []byte) (int, error) { 173 | select { 174 | case <-r.done: 175 | return 0, ErrClosedConnection 176 | default: 177 | return r.r.Read(p) 178 | } 179 | } 180 | func (r *readChanCloser) Close() error { 181 | select { 182 | case <-r.done: 183 | default: 184 | close(r.done) 185 | } 186 | return nil 187 | } 188 | 189 | func (c *Client) pushResp(id int32, r io.Reader, e error) { 190 | var to <-chan time.Time 191 | c.sessmu.Lock() 192 | s := c.sess[id] 193 | c.sessmu.Unlock() 194 | if s == nil { 195 | to = time.After(time.Second) 196 | s = c.newSess(id) 197 | } 198 | if r == nil { // no reader, error returned 199 | select { 200 | case <-c.done: 201 | case <-to: 202 | case s.in <- resp{err: e}: 203 | } 204 | return 205 | } 206 | done := make(chan struct{}) 207 | select { 208 | case <-c.done: 209 | case <-to: // connection expects that response will be read, so stream is broken 210 | panic(ErrBrokenProtocol{fmt.Errorf("no session %d found", id)}) 211 | case s.in <- resp{ReadCloser: newReadChanCloser(r, done)}: 212 | <-done 213 | } 214 | } 215 | 216 | // ReadErrorResponse reads an "Exception" message from the OrientDB server. 217 | // The OrientDB server can return multiple exceptions, all of which are 218 | // incorporated into a single OServerException Error struct. 219 | // If error (the second return arg) is not nil, then there was a 220 | // problem reading the server exception on the wire. 221 | func readErrorResponse(r *rw.Reader, protoVers int) (serverException error) { 222 | var ( 223 | exClass, exMsg string 224 | ) 225 | exc := make([]orient.Exception, 0, 1) // usually only one ? 226 | for { 227 | // before class/message combo there is a 1 (continue) or 0 (no more) 228 | marker := r.ReadByte() 229 | if marker == byte(0) { 230 | break 231 | } 232 | exClass = r.ReadString() 233 | exMsg = r.ReadString() 234 | exc = append(exc, orient.UnknownException{Class: exClass, Message: exMsg}) 235 | } 236 | 237 | // Next there is a serialized exception of bytes, but it is only 238 | // useful to Java clients, so read and ignore if present. 239 | if protoVers >= ProtoVersion19 { 240 | _ = r.ReadBytes() 241 | } 242 | 243 | for _, e := range exc { 244 | switch e.ExcClass() { 245 | case "com.orientechnologies.orient.core.storage.ORecordDuplicatedException": 246 | return ODuplicatedRecordException{OServerException: orient.OServerException{Exceptions: exc}} 247 | } 248 | } 249 | return orient.OServerException{Exceptions: exc} 250 | } 251 | 252 | func (c *Client) run() error { 253 | defer close(c.done) 254 | var ( 255 | status byte 256 | sessId int32 257 | ) 258 | for { // TODO: close safely 259 | status = c.pr.ReadByte() 260 | sessId = c.pr.ReadInt() 261 | if err := c.pr.Err(); err != nil { 262 | return err 263 | } 264 | switch status { 265 | case responseStatusOk: 266 | c.pushResp(sessId, c.br, nil) 267 | case responseStatusError: 268 | e := readErrorResponse(c.pr, c.curProtoVers) 269 | c.pushResp(sessId, nil, e) 270 | case responseStatusPush: 271 | return ErrBrokenProtocol{fmt.Errorf("server push is not supported yet")} 272 | default: 273 | return ErrBrokenProtocol{fmt.Errorf("unknown resp status: %d", status)} 274 | } 275 | } 276 | } 277 | 278 | type resp struct { 279 | io.ReadCloser 280 | err error 281 | } 282 | 283 | type session struct { 284 | mu sync.Mutex 285 | id int32 286 | in chan resp 287 | cli *Client 288 | } 289 | 290 | func (s *session) catch(err *error) { 291 | if r := recover(); r != nil { 292 | switch rr := r.(type) { 293 | case error: 294 | *err = rr 295 | default: 296 | *err = fmt.Errorf("%v", r) 297 | } 298 | go s.cli.Close() // panic means that stream is likely to be broken 299 | } 300 | } 301 | 302 | func (s *session) sendCmd(op byte, wr func(*rw.Writer) error, rd func(*rw.Reader) error) error { 303 | s.mu.Lock() 304 | defer s.mu.Unlock() 305 | if err := s.cli.writeCmd(op, s.id, wr); err != nil { 306 | return err 307 | } 308 | if op == requestDbClose { 309 | return nil 310 | } 311 | select { 312 | case <-s.cli.done: 313 | return fmt.Errorf("server gone") 314 | case resp, ok := <-s.in: 315 | if !ok { 316 | return ErrClosedConnection 317 | } else if resp.err != nil { 318 | return resp.err 319 | } 320 | defer resp.Close() 321 | if rd != nil { 322 | br := rw.NewReader(resp.ReadCloser.(io.Reader)) 323 | if err := rd(br); err != nil { 324 | return err 325 | } else if err = br.Err(); err != nil { 326 | return err 327 | } 328 | } 329 | return nil 330 | } 331 | } 332 | 333 | func (c *Client) getCurrDB() *Database { 334 | c.currmu.RLock() 335 | defer c.currmu.RUnlock() 336 | return c.currdb 337 | } 338 | 339 | // GetCurDB returns database metadata 340 | func (db *Database) GetCurDB() *orient.ODatabase { 341 | if db == nil || db.db == nil { 342 | return nil 343 | } 344 | return &orient.ODatabase{ 345 | Name: db.db.Name, 346 | Type: db.db.Type, 347 | Classes: db.db.Classes, 348 | } 349 | } 350 | 351 | // Close closes database connection 352 | func (c *Client) Close() error { 353 | if c == nil { 354 | return nil 355 | } 356 | if db := c.getCurrDB(); db != nil { 357 | // ignoring any error here, since closing the conx also terminates the session 358 | db.Close() 359 | } 360 | return c.conn.Close() 361 | } 362 | 363 | func (db *Database) readIdentifiable(r *rw.Reader) (orient.OIdentifiable, error) { 364 | classId := r.ReadShort() 365 | if err := r.Err(); err != nil { 366 | return nil, err 367 | } 368 | switch classId { 369 | case RecordNull: 370 | return nil, nil 371 | case RecordRID: 372 | var rid orient.RID 373 | if err := rid.FromStream(r); err != nil { 374 | return nil, err 375 | } 376 | return rid, nil 377 | default: 378 | tp := orient.RecordType(r.ReadByte()) 379 | if err := r.Err(); err != nil { 380 | return nil, err 381 | } 382 | record := orient.NewRecordOfType(tp) 383 | switch rec := record.(type) { 384 | case *orient.Document: 385 | rec.SetSerializer(db.sess.cli.recordFormat) 386 | } 387 | 388 | var rid orient.RID 389 | if err := rid.FromStream(r); err != nil { 390 | return nil, err 391 | } 392 | version := int(r.ReadInt()) 393 | content := r.ReadBytes() 394 | 395 | if err := record.Fill(rid, version, content); err != nil { 396 | return nil, fmt.Errorf("cannot create record %T from content: %s", record, err) 397 | } 398 | return record, nil 399 | } 400 | } 401 | -------------------------------------------------------------------------------- /obinary/command.go: -------------------------------------------------------------------------------- 1 | package obinary 2 | 3 | import ( 4 | "fmt" 5 | "gopkg.in/istreamdata/orientgo.v2" 6 | "gopkg.in/istreamdata/orientgo.v2/obinary/rw" 7 | ) 8 | 9 | func (db *Database) serializer() orient.RecordSerializer { 10 | return db.sess.cli.recordFormat 11 | } 12 | 13 | func (db *Database) updateCachedRecord(rec orient.ORecord) { 14 | // TODO: implement records cache 15 | } 16 | 17 | func (db *Database) readSynchResult(r *rw.Reader) (result interface{}, err error) { 18 | resType := rune(r.ReadByte()) 19 | if err = r.Err(); err != nil { 20 | return nil, err 21 | } 22 | switch resType { 23 | case 'n': // null result 24 | result = nil 25 | case 'r': // single record 26 | rec, err := db.readIdentifiable(r) 27 | if err != nil { 28 | return nil, err 29 | } 30 | if rec, ok := rec.(orient.ORecord); ok { 31 | db.updateCachedRecord(rec) 32 | } 33 | result = rec 34 | case 'l', 's': // collection of records 35 | n := int(r.ReadInt()) 36 | recs := make([]orient.OIdentifiable, n) // TODO: do something special for Set type? 37 | for i := range recs { 38 | rec, err := db.readIdentifiable(r) 39 | if err != nil { 40 | return nil, err 41 | } 42 | if rec, ok := rec.(orient.ORecord); ok { 43 | db.updateCachedRecord(rec) 44 | } 45 | recs[i] = rec 46 | } 47 | result = recs 48 | case 'i': 49 | var recs []orient.OIdentifiable 50 | for { 51 | status := r.ReadByte() 52 | if status <= 0 { 53 | break 54 | } 55 | if rec, err := db.readIdentifiable(r); err != nil { 56 | return nil, err 57 | } else if rec == nil { 58 | continue 59 | } else if status == 1 { 60 | if rec, ok := rec.(orient.ORecord); ok { 61 | db.updateCachedRecord(rec) 62 | } 63 | recs = append(recs, rec) 64 | } 65 | } 66 | result = recs 67 | case 'a': // serialized type 68 | s := r.ReadString() 69 | if err = r.Err(); err != nil { 70 | return nil, err 71 | } 72 | format := orient.StringRecordFormatAbs{} 73 | result = format.FieldTypeFromStream(format.GetType(s), s) 74 | default: 75 | panic(fmt.Errorf("readSynchResult: not supported result type %v", resType)) 76 | } 77 | if db.sess.cli.curProtoVers >= ProtoVersion17 { 78 | for { 79 | status := r.ReadByte() 80 | if status <= 0 { 81 | break 82 | } 83 | rec, err := db.readIdentifiable(r) 84 | if err != nil { 85 | return result, err 86 | } 87 | if rec != nil && status == 2 { 88 | if rec, ok := rec.(orient.ORecord); ok { 89 | db.updateCachedRecord(rec) 90 | } 91 | } 92 | } 93 | } 94 | return result, r.Err() 95 | } 96 | 97 | func (db *Database) Command(cmd orient.CustomSerializable) (result interface{}, err error) { 98 | var data []byte 99 | data, err = orient.SerializeAnyStreamable(cmd) 100 | if err != nil { 101 | return 102 | } 103 | 104 | live, async := false, false // synchronous only supported for now 105 | 106 | // for synchronous commands the remaining content is an array of form: 107 | // [(synch-result-type:byte)[(synch-result-content:?)]]+ 108 | // so the final value will by byte(0) to indicate the end of the array 109 | // and we must use a loop here 110 | err = db.sess.sendCmd(requestCommand, func(w *rw.Writer) error { 111 | if live { 112 | w.WriteByte(byte('l')) 113 | } else if async { 114 | w.WriteByte(byte('a')) 115 | } else { 116 | w.WriteByte(byte('s')) 117 | } 118 | w.WriteBytes(data) 119 | return w.Err() 120 | }, func(r *rw.Reader) error { 121 | if async { 122 | // TODO: async 123 | } else { 124 | result, err = db.readSynchResult(r) 125 | if err != nil { 126 | return err 127 | } 128 | if live { 129 | // TODO: live 130 | } 131 | } 132 | return r.Err() 133 | }) 134 | return result, err 135 | } 136 | -------------------------------------------------------------------------------- /obinary/commands_srv.go: -------------------------------------------------------------------------------- 1 | package obinary 2 | 3 | import ( 4 | "io" 5 | 6 | "gopkg.in/istreamdata/orientgo.v2" 7 | "gopkg.in/istreamdata/orientgo.v2/obinary/rw" 8 | ) 9 | 10 | type Manager struct { 11 | sess *session 12 | } 13 | 14 | /// In the Java client the "server command" functionality is encapsulated 15 | /// the OServerAdmin class. TODO: may want to follow suit rather than 16 | /// using the same DBClient for both server-commands and db-commands, 17 | /// especially since (I think) they have separate logins. 18 | 19 | // ConnectToServer logs into the OrientDB server with the appropriate 20 | // admin privileges in order to execute server-level commands (as opposed 21 | // to database-level commands). This must be called to establish a server 22 | // session before any other server-level commands. The username and password 23 | // required are for the server (admin) not any particular database. 24 | func (c *Client) ConnectToServer(adminUser, adminPassw string) (mgr *Manager, err error) { 25 | var ( 26 | sessId int32 27 | //token []byte 28 | ) 29 | err = c.root.sendCmd(requestConnect, func(w *rw.Writer) error { 30 | w.WriteStrings(driverName, driverVersion) 31 | w.WriteShort(int16(c.curProtoVers)) 32 | w.WriteNull() // dbclient id - only for cluster config // TODO: change to use dbc.clusteredConfig once that is added 33 | w.WriteString(c.recordFormat.String()) 34 | w.WriteBool(false) // use token (true) or session (false) 35 | w.WriteStrings(adminUser, adminPassw) 36 | return w.Err() 37 | }, func(r *rw.Reader) error { 38 | sessId = r.ReadInt() 39 | _ = r.ReadBytes() // token - ignore for now 40 | return r.Err() 41 | }) 42 | if err != nil { 43 | return 44 | } 45 | mgr = &Manager{sess: c.newSess(sessId)} 46 | return 47 | } 48 | 49 | func (c *Client) Auth(adminUser, adminPassw string) (orient.DBAdmin, error) { 50 | return c.ConnectToServer(adminUser, adminPassw) 51 | } 52 | 53 | // CreateDatabase will create a `remote` database of the type and storageType specified. 54 | // dbType must be type DocumentDBType or GraphDBType. 55 | // storageType must type PersistentStorageType or VolatileStorageType. 56 | func (m *Manager) CreateDatabase(dbname string, dbtype orient.DatabaseType, storageType orient.StorageType) error { 57 | return m.sess.sendCmd(requestDbCreate, func(w *rw.Writer) error { 58 | return w.WriteStrings(dbname, string(dbtype), string(storageType)) 59 | }, nil) 60 | } 61 | 62 | // DropDatabase drops the specified database. The caller must provide 63 | // both the name and the type of the database. The type should either: 64 | // 65 | // obinary.DocumentDBType 66 | // obinary.GraphDBType 67 | // 68 | // This is a "server" command, so you must have already called 69 | // ConnectToServer before calling this function. 70 | func (m *Manager) DropDatabase(dbname string, dbtype orient.StorageType) (err error) { 71 | return m.sess.sendCmd(requestDbDrop, func(w *rw.Writer) error { 72 | return w.WriteStrings(dbname, string(dbtype)) 73 | }, nil) 74 | } 75 | 76 | // DatabaseExists is a server-level command, so must be preceded by calling 77 | // ConnectToServer, otherwise an authorization error will be returned. 78 | // The storageType param must be either PersistentStorageType or VolatileStorageType. 79 | func (m *Manager) DatabaseExists(dbname string, storageType orient.StorageType) (val bool, err error) { 80 | err = m.sess.sendCmd(requestDbExists, func(w *rw.Writer) error { 81 | return w.WriteStrings(dbname, string(storageType)) 82 | }, func(r *rw.Reader) error { 83 | val = r.ReadBool() 84 | return r.Err() 85 | }) 86 | return 87 | } 88 | 89 | // RequestDBList works like the "list databases" command from the OrientDB client. 90 | // The result is put into a map, where the key of the map is the name of the 91 | // database and the value is the type concatenated with the path, like so: 92 | // 93 | // key: cars 94 | // val: plocal:/path/to/orientdb-community-2.0.1/databases/cars 95 | func (m *Manager) ListDatabases() (list map[string]string, err error) { 96 | var data []byte 97 | err = m.sess.sendCmd(requestDbLIST, nil, func(r *rw.Reader) error { 98 | data = r.ReadBytes() 99 | return r.Err() 100 | }) 101 | if err != nil { 102 | return 103 | } else if len(data) == 0 { 104 | err = io.ErrUnexpectedEOF 105 | return 106 | } 107 | // the bytes returned as a serialized EMBEDDEDMAP, so send it to the SerDe 108 | ser := m.sess.cli.recordFormat 109 | 110 | var ( 111 | o interface{} 112 | ) 113 | o, err = ser.FromStream(data) 114 | if err != nil { 115 | return 116 | } 117 | doc := o.(*orient.Document) 118 | 119 | list = doc.GetField("databases").Value.(map[string]string) 120 | return 121 | } 122 | 123 | func (m *Manager) Close() error { 124 | // TODO: what can we do? 125 | return m.sess.cli.Close() 126 | } 127 | -------------------------------------------------------------------------------- /obinary/constants.go: -------------------------------------------------------------------------------- 1 | package obinary 2 | 3 | // constants specific to the Network Binary Protocol 4 | 5 | // internal client constants 6 | const ( 7 | noSessionId = -1 8 | MaxProtocolVersion = 32 // max protocol supported by this client 9 | CurrentProtoVersion = 28 10 | MinProtocolVersion = 28 // min protocol supported by this client 11 | minBinarySerializerVersion = 22 // if server protocol version is less, use csv serde, not binary serde 12 | driverName = "OrientDB Go client" 13 | driverVersion = "1.0" 14 | serializeTypeBinary = "ORecordSerializerBinary" // do not change: required by server 15 | serializeTypeCsv = "ORecordDocument2csv" // do not change: required by server 16 | ) 17 | 18 | const ( 19 | // binary protocol sentinel values when reading single records 20 | RecordNull = -2 21 | RecordRID = -3 22 | ) 23 | 24 | // command and server-related constants 25 | // copied from Java OChannelBinaryProtocol 26 | const ( 27 | // OUTGOING 28 | requestShutdown = 1 29 | requestConnect = 2 30 | requestDbOpen = 3 31 | requestDbCreate = 4 32 | requestDbClose = 5 33 | requestDbExists = 6 34 | requestDbDrop = 7 35 | requestDbSIZE = 8 36 | requestDbCOUNTRECORDS = 9 37 | requestDataClusterADD = 10 38 | requestDataClusterDROP = 11 39 | requestDataClusterCOUNT = 12 40 | requestDataClusterDATARANGE = 13 41 | requestDataClusterCOPY = 14 42 | requestDataClusterLH_CLUSTER_IS_USED = 16 // since 1.2.0 43 | requestDataSegmentADD = 20 44 | requestDataSegmentDROP = 21 45 | requestRecordMETADATA = 29 // since 1.4.0 46 | requestRecordLOAD = 30 47 | requestRecordCREATE = 31 48 | requestRecordUPDATE = 32 49 | requestRecordDELETE = 33 50 | requestRecordCOPY = 34 51 | requestPositionsHIGHER = 36 // since 1.3.0 52 | requestPositionsLOWER = 37 // since 1.3.0 53 | requestRecordCLEAN_OUT = 38 // since 1.3.0 54 | requestPositionsFLOOR = 39 // since 1.3.0 55 | requestCount = 40 // DEPRECATED: USE REQUEST_DATACLUSTER_COUNT 56 | requestCommand = 41 57 | requestPositionsCEILING = 42 // since 1.3.0 58 | requestRecordHIDE = 43 // since 1.7 59 | requestTxCommit = 60 60 | requestConfigGET = 70 61 | requestConfigSET = 71 62 | requestConfigLIST = 72 63 | requestDbRELOAD = 73 // SINCE 1.0rc4 64 | requestDbLIST = 74 // SINCE 1.0rc6 65 | requestPushDistribConfig = 80 66 | // DISTRIBUTED 67 | requestDbCOPY = 90 // SINCE 1.0rc8 68 | requestREPLICATION = 91 // SINCE 1.0 69 | requestCLUSTER = 92 // SINCE 1.0 70 | requestDbTRANSFER = 93 // SINCE 1.0.2 71 | // Lock + sync 72 | requestDbFREEZE = 94 // SINCE 1.1.0 73 | requestDbRELEASE = 95 // SINCE 1.1.0 74 | requestDataClusterFREEZE = 96 75 | requestDataClusterRELEASE = 97 76 | // REMOTE SB-TREE COLLECTIONS 77 | requestCREATE_SBTREE_BONSAI = 110 78 | requestSBTREE_BONSAI_GET = 111 79 | requestSBTREE_BONSAI_FIRST_KEY = 112 80 | requestSBTREE_BONSAI_GET_ENTRIES_MAJOR = 113 81 | requestRIDBAG_GET_SIZE = 114 82 | 83 | // INCOMING 84 | responseStatusOk = 0 85 | responseStatusError = 1 86 | responseStatusPush = 3 87 | 88 | // FOR MORE INFO: 89 | // https://github.com/orientechnologies/orientdb/wiki/Network-Binary-Protocol#wiki-Compatibility 90 | ProtoVersion7 = 7 91 | ProtoVersion8 = 8 92 | ProtoVersion9 = 9 93 | ProtoVersion13 = 13 94 | ProtoVersion17 = 17 95 | ProtoVersion19 = 19 96 | ProtoVersion21 = 21 97 | ProtoVersion23 = 23 98 | ProtoVersion24 = 24 99 | ProtoVersion25 = 25 100 | ProtoVersion26 = 26 101 | ProtoVersion27 = 27 102 | ProtoVersion28 = 28 103 | ProtoVersion29 = 29 104 | ProtoVersion30 = 30 105 | ProtoVersion31 = 31 106 | ProtoVersion32 = 32 107 | ) 108 | -------------------------------------------------------------------------------- /obinary/database.go: -------------------------------------------------------------------------------- 1 | package obinary 2 | 3 | import ( 4 | "gopkg.in/istreamdata/orientgo.v2" 5 | "strconv" 6 | "strings" 7 | "sync" 8 | ) 9 | 10 | type ODatabase struct { 11 | Name string 12 | Type orient.DatabaseType 13 | Clusters []OCluster 14 | ClustCfg []byte // TODO: why is this a byte array? Just placeholder? What is it in the Java client? 15 | SchemaVersion int 16 | Classes map[string]*orient.OClass 17 | storageMu sync.RWMutex 18 | StorageCfg OStorageConfiguration // TODO: redundant to ClustCfg ?? 19 | globalPropMu sync.RWMutex 20 | globalProperties map[int]orient.OGlobalProperty 21 | } 22 | 23 | func (db *ODatabase) SetGlobalProperty(id int, p orient.OGlobalProperty) { 24 | if db == nil { 25 | return 26 | } 27 | db.globalPropMu.Lock() 28 | if db.globalProperties == nil { 29 | db.globalProperties = make(map[int]orient.OGlobalProperty) 30 | } 31 | db.globalProperties[id] = p 32 | db.globalPropMu.Unlock() 33 | } 34 | func (db *ODatabase) GetGlobalProperty(id int) (p orient.OGlobalProperty, ok bool) { 35 | if db == nil { 36 | ok = false 37 | return 38 | } 39 | db.globalPropMu.RLock() 40 | if db.globalProperties != nil { 41 | p, ok = db.globalProperties[id] 42 | } 43 | db.globalPropMu.RUnlock() 44 | return 45 | } 46 | 47 | func NewDatabase(name string, dbtype orient.DatabaseType) *ODatabase { 48 | return &ODatabase{ 49 | Name: name, 50 | Type: dbtype, 51 | SchemaVersion: -1, 52 | Classes: make(map[string]*orient.OClass), 53 | } 54 | } 55 | 56 | // OStorageConfiguration holds (some of) the information in the "Config Record" 57 | // #0:0. At this time, I'm throwing away a lot of the info in record #0:0 58 | // until proven that the ogonori client needs them. 59 | type OStorageConfiguration struct { 60 | version byte // TODO: of what? (=14 for OrientDB 2.1) 61 | name string 62 | schemaRID orient.RID // usually #0:1 63 | dictionaryRID string 64 | idxMgrRID orient.RID // usually #0:2 65 | localeLang string 66 | localeCountry string 67 | dateFmt string 68 | dateTimeFmt string 69 | timezone string 70 | } 71 | 72 | // parseConfigRecord takes the pipe-separate values that comes back 73 | // from reading record #0:0 and turns it into an OStorageConfiguration 74 | // object, which it adds to the db database object. 75 | func (sc *OStorageConfiguration) parse(psvData string) error { 76 | toks := strings.Split(psvData, "|") 77 | 78 | version, err := strconv.ParseInt(toks[0], 10, 8) 79 | if err != nil { 80 | return err 81 | } 82 | 83 | sc.version = byte(version) 84 | sc.name = strings.TrimSpace(toks[1]) 85 | sc.schemaRID = orient.MustParseRID(toks[2]) 86 | sc.dictionaryRID = strings.TrimSpace(toks[3]) 87 | sc.idxMgrRID = orient.MustParseRID(toks[4]) 88 | sc.localeLang = strings.TrimSpace(toks[5]) 89 | sc.localeCountry = strings.TrimSpace(toks[6]) 90 | sc.dateFmt = strings.TrimSpace(toks[7]) 91 | sc.dateTimeFmt = strings.TrimSpace(toks[8]) 92 | sc.timezone = strings.TrimSpace(toks[9]) 93 | 94 | return nil 95 | } 96 | 97 | type OCluster struct { 98 | Name string 99 | Id int16 100 | } 101 | -------------------------------------------------------------------------------- /obinary/errors.go: -------------------------------------------------------------------------------- 1 | package obinary 2 | 3 | import ( 4 | "fmt" 5 | "gopkg.in/istreamdata/orientgo.v2" 6 | "regexp" 7 | "strings" 8 | ) 9 | 10 | type ErrUnsupportedVersion int 11 | 12 | func (e ErrUnsupportedVersion) Error() string { 13 | return fmt.Sprintf("server protocol version %d is not supported (valid: %d-%d)", int(e), MinProtocolVersion, MaxProtocolVersion) 14 | } 15 | 16 | var ErrClosedConnection = fmt.Errorf("closed connection") 17 | 18 | type ErrBrokenProtocol struct { 19 | Reason error 20 | } 21 | 22 | func (e ErrBrokenProtocol) Error() string { 23 | return fmt.Sprintf("connection stream broken: %s", e.Reason.Error()) 24 | } 25 | 26 | // InvalidDatabaseType is an Error that indicates that the db type value 27 | // is not one that the OrientDB server will recognize. For OrientDB 2.x, the 28 | // valid types are "document" or "graph". Constants for these values are 29 | // provided in the obinary ogonori code base. 30 | type ErrDataTypeMismatch struct { 31 | ExpectedDataType orient.OType 32 | ExpectedGoType string 33 | ActualValue interface{} 34 | } 35 | 36 | func (e ErrDataTypeMismatch) Error() string { 37 | gotype := "" 38 | if e.ExpectedGoType != "" { 39 | gotype = " (" + e.ExpectedGoType + ")" 40 | } 41 | return fmt.Sprintf("DataTypeMismatch: Actual: %v of type %T; Expected %s%s", 42 | e.ActualValue, e.ActualValue, e.ExpectedDataType, 43 | gotype) 44 | } 45 | 46 | var ErrStaleGlobalProperties = fmt.Errorf("stale global properties") 47 | 48 | type ODuplicatedRecordException struct { 49 | orient.OServerException 50 | } 51 | 52 | func (e ODuplicatedRecordException) Error() string { 53 | re := regexp.MustCompile(".* found duplicated key '(?P.+)' in index " + 54 | "'(?P[\\w\\s\\.]+)' previously assigned to the record [\\d\\#\\:]*") 55 | for _, ex := range e.Exceptions { 56 | message := ex.ExcMessage() 57 | if re.MatchString(message) { 58 | key := fmt.Sprintf("${%s}", re.SubexpNames()[1]) 59 | key = re.ReplaceAllString(message, key) 60 | index := fmt.Sprintf("${%s}", re.SubexpNames()[2]) 61 | index = re.ReplaceAllString(message, index) 62 | i := strings.Split(index, ".") 63 | if len(i) != 2 { 64 | break 65 | } 66 | className := i[0] 67 | propertyName := i[1] 68 | return fmt.Sprintf(`%s with %s "%s" already exists`, className, strings.ToLower(propertyName), key) 69 | } 70 | } 71 | return e.OServerException.Error() 72 | } 73 | -------------------------------------------------------------------------------- /obinary/export_test.go: -------------------------------------------------------------------------------- 1 | package obinary 2 | 3 | import ( 4 | "gopkg.in/istreamdata/orientgo.v2/obinary/rw" 5 | ) 6 | 7 | func ReadErrorResponse(r *rw.Reader) (serverException error) { 8 | return readErrorResponse(r, CurrentProtoVersion) 9 | } 10 | -------------------------------------------------------------------------------- /obinary/obinary_test.go: -------------------------------------------------------------------------------- 1 | package obinary_test 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "gopkg.in/istreamdata/orientgo.v2" 7 | "gopkg.in/istreamdata/orientgo.v2/obinary" 8 | "gopkg.in/istreamdata/orientgo.v2/obinary/rw" 9 | "path/filepath" 10 | "reflect" 11 | "runtime" 12 | "testing" 13 | ) 14 | 15 | // equals fails the test if exp is not equal to act. 16 | func equals(tb testing.TB, exp, act interface{}) { 17 | if !reflect.DeepEqual(exp, act) { 18 | _, file, line, _ := runtime.Caller(1) 19 | fmt.Printf("\033[31m%s:%d:\n\n\texp: %#v\n\n\tgot: %#v\033[39m\n\n", 20 | filepath.Base(file), line, exp, act) 21 | tb.FailNow() 22 | } 23 | } 24 | 25 | func TestReadErrorResponseWithSingleException(t *testing.T) { 26 | buf := new(bytes.Buffer) 27 | bw := rw.NewWriter(buf) 28 | bw.WriteByte(byte(1)) // indicates continue of exception class/msg array 29 | bw.WriteStrings("org.foo.BlargException", "wibble wibble!!") 30 | bw.WriteByte(byte(0)) // indicates end of exception class/msg array 31 | bw.WriteBytes([]byte("this is a stacktrace simulator\nEOL")) 32 | 33 | var serverExc error 34 | serverExc = obinary.ReadErrorResponse(rw.NewReader(buf)) 35 | 36 | e, ok := serverExc.(orient.OServerException) 37 | if !ok { 38 | t.Fatal("wrong exception type") 39 | } 40 | equals(t, 1, len(e.Exceptions)) 41 | 42 | equals(t, "org.foo.BlargException", e.Exceptions[0].ExcClass()) 43 | equals(t, "wibble wibble!!", e.Exceptions[0].ExcMessage()) 44 | } 45 | 46 | func TestReadErrorResponseWithMultipleExceptions(t *testing.T) { 47 | buf := new(bytes.Buffer) 48 | bw := rw.NewWriter(buf) 49 | bw.WriteByte(byte(1)) // indicates more exceptions to come 50 | bw.WriteStrings("org.foo.BlargException", "Too many blorgles!!") 51 | bw.WriteByte(byte(1)) // indicates more exceptions to come 52 | bw.WriteStrings("org.foo.FeebleException", "Not enough juice") 53 | bw.WriteByte(byte(1)) // indicates more exceptions to come 54 | bw.WriteStrings("org.foo.WobbleException", "Orbital decay") 55 | bw.WriteByte(byte(0)) // indicates end of exceptions 56 | bw.WriteBytes([]byte("this is a stacktrace simulator\nEOL")) 57 | 58 | serverExc := obinary.ReadErrorResponse(rw.NewReader(buf)) 59 | 60 | e, ok := serverExc.(orient.OServerException) 61 | if !ok { 62 | t.Fatal("wrong exception type") 63 | } 64 | 65 | equals(t, "org.foo.BlargException", e.Exceptions[0].ExcClass()) 66 | equals(t, "Not enough juice", e.Exceptions[1].ExcMessage()) 67 | equals(t, "org.foo.WobbleException", e.Exceptions[2].ExcClass()) 68 | equals(t, "Orbital decay", e.Exceptions[2].ExcMessage()) 69 | } 70 | -------------------------------------------------------------------------------- /obinary/rw/assert_test.go: -------------------------------------------------------------------------------- 1 | package rw 2 | 3 | import ( 4 | "fmt" 5 | "path/filepath" 6 | "reflect" 7 | "runtime" 8 | "testing" 9 | ) 10 | 11 | // assert fails the test if the condition is false. 12 | func assert(tb testing.TB, condition bool, msg string, v ...interface{}) { 13 | if !condition { 14 | _, file, line, _ := runtime.Caller(1) 15 | fmt.Printf("\033[31m%s:%d: "+msg+"\033[39m\n\n", 16 | append([]interface{}{filepath.Base(file), line}, v...)...) 17 | tb.FailNow() 18 | } 19 | } 20 | 21 | // ok fails the test if an err is not nil. 22 | func ok(tb testing.TB, err error) { 23 | if err != nil { 24 | _, file, line, _ := runtime.Caller(1) 25 | fmt.Printf("\033[31m%s:%d: unexpected error: %s\033[39m\n\n", 26 | filepath.Base(file), line, err.Error()) 27 | tb.FailNow() 28 | } 29 | } 30 | 31 | // equals fails the test if exp is not equal to act. 32 | func equals(tb testing.TB, exp, act interface{}) { 33 | if !reflect.DeepEqual(exp, act) { 34 | _, file, line, _ := runtime.Caller(1) 35 | fmt.Printf("\033[31m%s:%d:\n\n\texp: %#v\n\n\tgot: %#v\033[39m\n\n", 36 | filepath.Base(file), line, exp, act) 37 | tb.FailNow() 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /obinary/rw/reader.go: -------------------------------------------------------------------------------- 1 | // rw is the read-write package for reading and writing types 2 | // from the OrientDB binary network protocol. Reading is done 3 | // via io.Reader and writing is done to bytes.Buffer (since the 4 | // extra functionality of byte.Buffer is desired). All the 5 | // OrientDB types are represented here for non-encoded forms. 6 | // For varint and zigzag encoding/decoding handling use the 7 | // obinary/varint package instead. 8 | package rw 9 | 10 | import ( 11 | "encoding/binary" 12 | "fmt" 13 | "io" 14 | ) 15 | 16 | var Order = binary.BigEndian 17 | 18 | func NewReader(r io.Reader) *Reader { 19 | switch br := r.(type) { 20 | case *Reader: 21 | return br 22 | case *ReadSeeker: 23 | return br.Reader 24 | } 25 | br := &Reader{R: r} 26 | br.br = byteReader{br} 27 | return br 28 | } 29 | 30 | type Reader struct { 31 | err error 32 | br byteReader 33 | R io.Reader 34 | } 35 | 36 | func (r *Reader) Err() error { 37 | return r.err 38 | } 39 | 40 | func (r *Reader) Read(p []byte) (int, error) { 41 | if r.err != nil { 42 | return 0, r.err 43 | } 44 | return r.R.Read(p) 45 | } 46 | 47 | func (r *Reader) read(v interface{}) error { 48 | if r.err != nil { 49 | return r.err 50 | } 51 | if err := binary.Read(r.R, Order, v); err != nil { 52 | r.err = err 53 | } 54 | return r.err 55 | } 56 | 57 | func (r *Reader) ReadRawBytes(buf []byte) error { 58 | if r.err != nil { 59 | return r.err 60 | } 61 | if _, err := io.ReadFull(r.R, buf); err != nil { 62 | r.err = err 63 | } 64 | return r.err 65 | } 66 | 67 | // ReadBytes reads in an OrientDB byte array. It reads the first 4 bytes 68 | // from the Reader as an int to determine the length of the byte array 69 | // to read in. 70 | // If the specified size of the byte array is 0 (empty) or negative (null) 71 | // nil is returned for the []byte. 72 | func (r *Reader) ReadBytes() []byte { 73 | // the first four bytes give the length of the remaining byte array 74 | sz := r.ReadInt() 75 | // sz of 0 indicates empty byte array 76 | // sz of -1 indicates null value 77 | // for now, I'm returning nil []byte for both 78 | if sz <= 0 { 79 | return nil 80 | } 81 | 82 | b := make([]byte, sz) 83 | r.ReadRawBytes(b) 84 | return b 85 | } 86 | 87 | // ReadString xxxx 88 | // If the string size is 0 an empty string and nil error are returned 89 | func (r *Reader) ReadString() string { 90 | return string(r.ReadBytes()) 91 | } 92 | 93 | func (r *Reader) ReadByte() byte { 94 | readbuf := make([]byte, 1) 95 | r.Read(readbuf) 96 | return readbuf[0] 97 | } 98 | 99 | func (r *Reader) ReadInt() (v int32) { 100 | r.read(&v) 101 | return 102 | } 103 | 104 | func (r *Reader) ReadLong() (v int64) { 105 | r.read(&v) 106 | return 107 | } 108 | 109 | func (r *Reader) ReadShort() (v int16) { 110 | r.read(&v) 111 | return 112 | } 113 | 114 | func (r *Reader) ReadFloat() (v float32) { 115 | r.read(&v) 116 | return 117 | } 118 | 119 | func (r *Reader) ReadDouble() (v float64) { 120 | r.read(&v) 121 | return 122 | } 123 | 124 | // Reads one byte from the Reader. If the byte is zero, then false is returned, 125 | // otherwise true. If error is non-nil, then the bool value is undefined. 126 | func (r *Reader) ReadBool() bool { 127 | // non-zero is true 128 | return r.ReadByte() != byte(0) 129 | } 130 | 131 | type ByteReader interface { 132 | io.Reader 133 | io.ByteReader 134 | } 135 | 136 | type byteReader struct { 137 | r *Reader 138 | } 139 | 140 | func (r byteReader) ReadByte() (byte, error) { 141 | return r.r.ReadByte(), r.r.Err() 142 | } 143 | 144 | // ReadVarIntAndDecode64 reads a varint from r to a uint64 145 | // and then zigzag decodes it to an int64 value. 146 | func (r *Reader) ReadVarint() int64 { 147 | v, err := binary.ReadVarint(r.br) 148 | if err != nil { 149 | r.err = err 150 | } 151 | return v 152 | } 153 | 154 | // ReadVarIntToUint reads a variable length integer from the input buffer. 155 | // The inflated integer is written is returned as a uint64 value. 156 | // This method only "inflates" the varint into a uint64; it does NOT 157 | // zigzag decode it. 158 | func (r *Reader) ReadUvarint() uint64 { 159 | v, err := binary.ReadUvarint(r.br) 160 | if err != nil { 161 | r.err = err 162 | } 163 | return v 164 | } 165 | 166 | // varint.ReadBytes, like rw.ReadBytes, first reads a length from the 167 | // input buffer and then that number of bytes into a []byte from the 168 | // input buffer. The difference is that the integer indicating the length 169 | // of the byte array to follow is a zigzag encoded varint. 170 | func (r *Reader) ReadBytesVarint() []byte { 171 | // an encoded varint give the length of the remaining byte array 172 | lenbytes := r.ReadVarint() 173 | if lenbytes == 0 { 174 | return nil 175 | } else if lenbytes < 0 { 176 | panic(fmt.Errorf("Error in varint.ReadBytes: size of bytes was less than zero: %v", lenbytes)) 177 | } 178 | 179 | data := make([]byte, int(lenbytes)) 180 | r.ReadRawBytes(data) 181 | return data 182 | } 183 | 184 | // varint.ReadString, like rw.ReadString, first reads a length from the 185 | // input buffer and then that number of bytes (of ASCII chars) into a string 186 | // from the input buffer. The difference is that the integer indicating the 187 | // length of the byte array to follow is a zigzag encoded varint. 188 | func (r *Reader) ReadStringVarint() string { 189 | return string(r.ReadBytesVarint()) 190 | } 191 | 192 | func NewReadSeeker(r io.ReadSeeker) *ReadSeeker { 193 | if br, ok := r.(*ReadSeeker); ok { 194 | return br 195 | } 196 | return &ReadSeeker{Reader: NewReader(r), S: r} 197 | } 198 | 199 | type ReadSeeker struct { 200 | *Reader 201 | S io.Seeker 202 | } 203 | 204 | func (r *ReadSeeker) Seek(off int64, whence int) (int64, error) { 205 | if r.Reader.err != nil { 206 | cur, _ := r.S.Seek(0, 1) 207 | return cur, r.Reader.err 208 | } 209 | cur, err := r.S.Seek(off, whence) 210 | if err != nil { 211 | r.Reader.err = err 212 | } 213 | return cur, err 214 | } 215 | -------------------------------------------------------------------------------- /obinary/rw/reader_test.go: -------------------------------------------------------------------------------- 1 | package rw 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "testing" 7 | ) 8 | 9 | const ( 10 | MaxUint16 = ^uint16(0) 11 | MinUint16 = 0 12 | MaxInt16 = int16(MaxUint16 >> 1) 13 | MinInt16 = -MaxInt16 - 1 14 | 15 | MaxUint = ^uint32(0) 16 | MinUint = 0 17 | MaxInt = int32(MaxUint >> 1) 18 | MinInt = -MaxInt - 1 19 | 20 | MaxUint64 = ^uint64(0) 21 | MinUint64 = 0 22 | MaxInt64 = int64(MaxUint64 >> 1) 23 | MinInt64 = -MaxInt64 - 1 24 | ) 25 | 26 | func TestReadBytes(t *testing.T) { 27 | var bs []byte 28 | 29 | // data[0:4] gets interpreted as a big-endian int (=4) which specifies the number of bytes to be read 30 | // bytes data are then data[1:5], since int32(data[0:4])==4) 31 | data := []byte{0, 0, 0, 4, 1, 2, 3, 4} 32 | rdr := bytes.NewBuffer(data) 33 | 34 | bs = NewReader(rdr).ReadBytes() 35 | equals(t, 4, len(bs)) 36 | equals(t, byte(1), bs[0]) 37 | equals(t, byte(2), bs[1]) 38 | equals(t, byte(3), bs[2]) 39 | equals(t, byte(4), bs[3]) 40 | 41 | // ensure more than 4 entries are not read 42 | data = []byte{0, 0, 0, 4, 1, 2, 3, 4, 5, 6} 43 | rdr = bytes.NewBuffer(data) 44 | 45 | bs = NewReader(rdr).ReadBytes() 46 | equals(t, 4, len(bs)) 47 | equals(t, byte(1), bs[0]) 48 | equals(t, byte(2), bs[1]) 49 | equals(t, byte(3), bs[2]) 50 | equals(t, byte(4), bs[3]) 51 | } 52 | 53 | func TestReadBytesWithNullBytesArray(t *testing.T) { 54 | var bs []byte 55 | 56 | // data[0:4] gets interpreted as a big-endian int (=0) which specifies an "empty" 57 | // byte array has been encoded 58 | data := []byte{0, 0, 0, 0, 1, 2, 3, 4, 5} 59 | rdr := bytes.NewBuffer(data) 60 | bs = NewReader(rdr).ReadBytes() 61 | assert(t, bs == nil, "bs should be nil") 62 | } 63 | 64 | func TestReadShort(t *testing.T) { 65 | var outval int16 66 | data := []int16{0, 1, -112, int16(MaxInt16) - 23, MaxInt16, MinInt16} 67 | 68 | buf := new(bytes.Buffer) 69 | for _, inval := range data { 70 | buf.Reset() 71 | // turn int16 into bytes 72 | err := binary.Write(buf, binary.BigEndian, inval) 73 | ok(t, err) 74 | 75 | // turn bytes back into int using obinary.ReadLong (fn under test) 76 | outval = NewReader(buf).ReadShort() 77 | equals(t, int16(inval), outval) 78 | } 79 | } 80 | 81 | func TestReadLong(t *testing.T) { 82 | var outval int64 83 | data := []int64{0, 1, -100000, int64(MaxInt) + 99999, MaxInt64, MinInt64} 84 | 85 | buf := new(bytes.Buffer) 86 | for _, inval := range data { 87 | buf.Reset() 88 | // turn int64 into bytes 89 | err := binary.Write(buf, binary.BigEndian, inval) 90 | ok(t, err) 91 | 92 | // turn bytes back into int using obinary.ReadLong (fn under test) 93 | outval = NewReader(buf).ReadLong() 94 | equals(t, int64(inval), outval) 95 | } 96 | } 97 | 98 | func TestReadInt(t *testing.T) { 99 | var outval int32 100 | data := []int32{0, 1, -100000, 200000, MaxInt, MinInt} 101 | 102 | buf := new(bytes.Buffer) 103 | for _, inval := range data { 104 | buf.Reset() 105 | // turn int32 into bytes 106 | err := binary.Write(buf, binary.BigEndian, inval) 107 | ok(t, err) 108 | 109 | // turn bytes back into int using obinary.ReadInt (fn under test) 110 | outval = NewReader(buf).ReadInt() 111 | equals(t, inval, outval) 112 | } 113 | } 114 | 115 | func TestReadFloat(t *testing.T) { 116 | var outval float32 117 | data := []float32{0, -0.00003, 893421.883472, -88842.255} 118 | 119 | buf := new(bytes.Buffer) 120 | for _, inval := range data { 121 | buf.Reset() 122 | 123 | // turn float32 into bytes 124 | err := binary.Write(buf, binary.BigEndian, inval) 125 | ok(t, err) 126 | 127 | // bytes -> float32 128 | outval = NewReader(buf).ReadFloat() 129 | equals(t, inval, outval) 130 | } 131 | } 132 | 133 | func TestReadDouble(t *testing.T) { 134 | var outval float64 135 | data := []float64{0, -0.0000000000000003, 9000000088880000000893421.8838800472, -388842.255} 136 | 137 | buf := new(bytes.Buffer) 138 | for _, inval := range data { 139 | buf.Reset() 140 | 141 | // turn float32 into bytes 142 | err := binary.Write(buf, binary.BigEndian, inval) 143 | ok(t, err) 144 | 145 | // bytes -> float64 146 | outval = NewReader(buf).ReadDouble() 147 | equals(t, inval, outval) 148 | } 149 | } 150 | 151 | func TestReadBoolFalse(t *testing.T) { 152 | exp := false 153 | buf := new(bytes.Buffer) 154 | data := []byte{0} // 0=false in OrientDB 155 | buf.Write(data) 156 | 157 | actual := NewReader(buf).ReadBool() 158 | equals(t, exp, actual) 159 | } 160 | 161 | func TestReadBoolTrue(t *testing.T) { 162 | exp := true 163 | buf := new(bytes.Buffer) 164 | data := []byte{1} // 1=true in OrientDB 165 | buf.Write(data) 166 | 167 | actual := NewReader(buf).ReadBool() 168 | equals(t, exp, actual) 169 | } 170 | 171 | func TestReadString(t *testing.T) { 172 | s := "one two 345" 173 | buf := new(bytes.Buffer) 174 | data := []byte{0, 0, 0, byte(len(s))} // integer sz of string 175 | buf.Write(data) 176 | buf.WriteString(s) 177 | 178 | outstr := NewReader(buf).ReadString() 179 | equals(t, s, outstr) 180 | } 181 | 182 | func TestReadStringWithNullString(t *testing.T) { 183 | // first with only integer in the Reader 184 | data := []byte{0, 0, 0, 0} 185 | buf := bytes.NewBuffer(data) 186 | outstr := NewReader(buf).ReadString() 187 | equals(t, "", outstr) 188 | 189 | // next with string in the buffer - still shouldn't be read 190 | s := "one two 345" 191 | buf.Reset() 192 | buf.Write(data) 193 | buf.WriteString(s) 194 | 195 | outstr = NewReader(buf).ReadString() 196 | equals(t, "", outstr) 197 | } 198 | 199 | func TestReadBytesVarint_GoodData_5Bytes(t *testing.T) { 200 | // varint.ReadBytes expects a varint encoded int, followed by that many bytes 201 | buf := new(bytes.Buffer) 202 | buf.WriteByte(byte(10)) // varint encoded 10 == 5 203 | buf.Write([]byte("total")) // 5 bytes 204 | 205 | outbytes := NewReader(buf).ReadBytesVarint() 206 | equals(t, 5, len(outbytes)) 207 | equals(t, "total", string(outbytes)) 208 | } 209 | 210 | func TestReadBytesVarint_GoodData_0Bytes(t *testing.T) { 211 | // 0 as the varint means no bytes follow 212 | buf := new(bytes.Buffer) 213 | buf.WriteByte(byte(0)) // varint encoded 0 == 0 214 | 215 | outbytes := NewReader(buf).ReadBytesVarint() 216 | assert(t, outbytes == nil, "outbytes should be nil") 217 | } 218 | 219 | func TestReadStringVarint_GoodData(t *testing.T) { 220 | // varint.ReadBytes expects a varint encoded int, followed by that many bytes 221 | buf := new(bytes.Buffer) 222 | buf.WriteByte(byte(12)) // varint encoded 12 == 6 223 | buf.Write([]byte("ZAXXON")) // 6 bytes 224 | 225 | outstr := NewReader(buf).ReadStringVarint() 226 | equals(t, 6, len(outstr)) 227 | equals(t, "ZAXXON", outstr) 228 | } 229 | 230 | func TestReadStringVarint_Empty(t *testing.T) { 231 | // varint.ReadBytes expects a varint encoded int, followed by that many bytes 232 | buf := new(bytes.Buffer) 233 | buf.WriteByte(byte(0)) // varint encoded 12 == 6 234 | 235 | outstr := NewReader(buf).ReadStringVarint() 236 | equals(t, "", outstr) 237 | } 238 | 239 | func TestReadStringVarint_LargeString(t *testing.T) { 240 | /* ---[ setup ]--- */ 241 | strlen := int32(len(largeString)) 242 | buf := new(bytes.Buffer) 243 | 244 | // the encoded varint will be 2 bytes in length 245 | NewWriter(buf).WriteVarint(int64(strlen)) 246 | 247 | _, err := buf.WriteString(largeString) 248 | ok(t, err) 249 | 250 | /* ---[ code under test ]--- */ 251 | outstr := NewReader(buf).ReadStringVarint() 252 | ok(t, err) 253 | equals(t, int(strlen), len(outstr)) 254 | equals(t, largeString, outstr) 255 | } 256 | 257 | var largeString = ` 258 | For a number of years I have been familiar with the observation that the quality of programmers is a decreasing function of the density of go to statements in the programs they produce. More recently I discovered why the use of the go to statement has such disastrous effects, and I became convinced that the go to statement should be abolished from all "higher level" programming languages (i.e. everything except, perhaps, plain machine code). At that time I did not attach too much importance to this discovery; I now submit my considerations for publication because in very recent discussions in which the subject turned up, I have been urged to do so. 259 | 260 | My first remark is that, although the programmer's activity ends when he has constructed a correct program, the process taking place under control of his program is the true subject matter of his activity, for it is this process that has to accomplish the desired effect; it is this process that in its dynamic behavior has to satisfy the desired specifications. Yet, once the program has been made, the "making' of the corresponding process is delegated to the machine. 261 | 262 | My second remark is that our intellectual powers are rather geared to master static relations and that our powers to visualize processes evolving in time are relatively poorly developed. For that reason we should do (as wise programmers aware of our limitations) our utmost to shorten the conceptual gap between the static program and the dynamic process, to make the correspondence between the program (spread out in text space) and the process (spread out in time) as trivial as possible. 263 | 264 | Let us now consider how we can characterize the progress of a process. (You may think about this question in a very concrete manner: suppose that a process, considered as a time succession of actions, is stopped after an arbitrary action, what data do we have to fix in order that we can redo the process until the very same point?) If the program text is a pure concatenation of, say, assignment statements (for the purpose of this discussion regarded as the descriptions of single actions) it is sufficient to point in the program text to a point between two successive action descriptions. (In the absence of go to statements I can permit myself the syntactic ambiguity in the last three words of the previous sentence: if we parse them as "successive (action descriptions)" we mean successive in text space; if we parse as "(successive action) descriptions" we mean successive in time.) Let us call such a pointer to a suitable place in the text a "textual index." 265 | 266 | When we include conditional clauses (if B then A), alternative clauses (if B then A1 else A2), choice clauses as introduced by C. A. R. Hoare (case[i] of (A1, A2,···, An)),or conditional expressions as introduced by J. McCarthy (B1 -> E1, B2 -> E2, ···, Bn -> En), the fact remains that the progress of the process remains characterized by a single textual index. 267 | 268 | As soon as we include in our language procedures we must admit that a single textual index is no longer sufficient. In the case that a textual index points to the interior of a procedure body the dynamic progress is only characterized when we also give to which call of the procedure we refer. With the inclusion of procedures we can characterize the progress of the process via a sequence of textual indices, the length of this sequence being equal to the dynamic depth of procedure calling. 269 | 270 | Let us now consider repetition clauses (like, while B repeat A or repeat A until B). Logically speaking, such clauses are now superfluous, because we can express repetition with the aid of recursive procedures. For reasons of realism I don't wish to exclude them: on the one hand, repetition clauses can be implemented quite comfortably with present day finite equipment; on the other hand, the reasoning pattern known as "induction" makes us well equipped to retain our intellectual grasp on the processes generated by repetition clauses. With the inclusion of the repetition clauses textual indices are no longer sufficient to describe the dynamic progress of the process. With each entry into a repetition clause, however, we can associate a so-called "dynamic index," inexorably counting the ordinal number of the corresponding current repetition. As repetition clauses (just as procedure calls) may be applied nestedly, we find that now the progress of the process can always be uniquely characterized by a (mixed) sequence of textual and/or dynamic indices. 271 | 272 | The main point is that the values of these indices are outside programmer's control; they are generated (either by the write-up of his program or by the dynamic evolution of the process) whether he wishes or not. They provide independent coordinates in which to describe the progress of the process. 273 | 274 | Why do we need such independent coordinates? The reason is - and this seems to be inherent to sequential processes - that we can interpret the value of a variable only with respect to the progress of the process. If we wish to count the number, n say, of people in an initially empty room, we can achieve this by increasing n by one whenever we see someone entering the room. In the in-between moment that we have observed someone entering the room but have not yet performed the subsequent increase of n, its value equals the number of people in the room minus one!` 275 | -------------------------------------------------------------------------------- /obinary/rw/writer.go: -------------------------------------------------------------------------------- 1 | package rw 2 | 3 | import ( 4 | "encoding/binary" 5 | "fmt" 6 | "io" 7 | ) 8 | 9 | const ( 10 | SizeByte = 1 11 | SizeShort = 2 12 | SizeInt = 4 13 | SizeLong = 8 14 | SizeFloat = SizeInt 15 | SizeDouble = SizeLong 16 | ) 17 | 18 | func NewWriter(w io.Writer) *Writer { 19 | if bw, ok := w.(*Writer); ok { 20 | return bw 21 | } 22 | return &Writer{W: w} 23 | } 24 | 25 | type Writer struct { 26 | err error 27 | W io.Writer 28 | } 29 | 30 | func (w Writer) Err() error { 31 | return w.err 32 | } 33 | func (w *Writer) Write(p []byte) (int, error) { 34 | if w.err != nil { 35 | return 0, w.err 36 | } 37 | return w.W.Write(p) 38 | } 39 | func (w *Writer) write(o interface{}) error { 40 | if w.err != nil { 41 | return w.err 42 | } 43 | if err := binary.Write(w.W, Order, o); err != nil { 44 | w.err = err 45 | } 46 | return w.err 47 | } 48 | 49 | // WriteRawBytes just writes the bytes, not prefixed by the size of the []byte 50 | func (w *Writer) WriteRawBytes(bs []byte) error { 51 | if w.err != nil { 52 | return w.err 53 | } 54 | if n, err := w.W.Write(bs); err != nil { 55 | w.err = err 56 | } else if n != len(bs) { 57 | w.err = fmt.Errorf("incorrect number of bytes written: %d", n) 58 | } 59 | return w.err 60 | } 61 | func (w *Writer) WriteByte(b byte) error { 62 | return w.WriteRawBytes([]byte{b}) 63 | } 64 | 65 | // WriteShort writes a int16 in big endian order to Writer 66 | func (w *Writer) WriteShort(n int16) error { 67 | buf := make([]byte, SizeShort) 68 | Order.PutUint16(buf, uint16(n)) 69 | return w.WriteRawBytes(buf) 70 | } 71 | 72 | // WriteInt writes a int32 in big endian order to Writer 73 | func (w *Writer) WriteInt(n int32) error { 74 | buf := make([]byte, SizeInt) 75 | Order.PutUint32(buf, uint32(n)) 76 | return w.WriteRawBytes(buf) 77 | } 78 | 79 | // WriteLong writes a int64 in big endian order to Writer 80 | func (w *Writer) WriteLong(n int64) error { 81 | buf := make([]byte, SizeLong) 82 | Order.PutUint64(buf, uint64(n)) 83 | return w.WriteRawBytes(buf) 84 | } 85 | 86 | func (w *Writer) WriteNull() error { 87 | return w.WriteInt(-1) 88 | } 89 | 90 | // WriteBytes is meant to be used for writing a structure that the OrientDB will 91 | // interpret as a byte array, usually a serialized data structure. This means the 92 | // first thing written to Writer is the size of the byte array. If you want 93 | // to write bytes without the the size prefix, use WriteRawBytes instead. 94 | func (w *Writer) WriteBytes(bs []byte) error { 95 | w.WriteInt(int32(len(bs))) 96 | w.WriteRawBytes(bs) 97 | return w.err 98 | } 99 | 100 | func (w *Writer) WriteString(s string) error { 101 | return w.WriteBytes([]byte(s)) 102 | } 103 | 104 | func (w *Writer) WriteStrings(ss ...string) error { 105 | for _, s := range ss { 106 | w.WriteString(s) 107 | } 108 | return w.err 109 | } 110 | 111 | // WriteBool writes byte(1) for true and byte(0) for false to Writer, 112 | // as specified by the OrientDB spec. 113 | func (w *Writer) WriteBool(b bool) error { 114 | if b { 115 | w.WriteByte(byte(1)) 116 | } else { 117 | w.WriteByte(byte(0)) 118 | } 119 | return w.err 120 | } 121 | 122 | // WriteFloat writes a float32 in big endian order to Writer 123 | func (w *Writer) WriteFloat(f float32) error { 124 | return w.write(f) 125 | } 126 | 127 | // WriteDouble writes a float64 in big endian order to Writer 128 | func (w *Writer) WriteDouble(f float64) error { 129 | return w.write(f) 130 | } 131 | 132 | // WriteVarint zigzag encodes the int64 passed in and then 133 | // translates that number to a protobuf/OrientDB varint, writing 134 | // the bytes of that varint to the io.Writer. 135 | func (w *Writer) WriteVarint(v int64) (int, error) { 136 | buf := make([]byte, binary.MaxVarintLen64) 137 | n := binary.PutVarint(buf, v) 138 | return n, w.WriteRawBytes(buf[:n]) 139 | } 140 | 141 | func (w *Writer) WriteBytesVarint(bs []byte) (int, error) { 142 | vn, _ := w.WriteVarint(int64(len(bs))) 143 | return vn + len(bs), w.WriteRawBytes(bs) 144 | } 145 | 146 | func (w *Writer) WriteStringVarint(s string) (int, error) { 147 | return w.WriteBytesVarint([]byte(s)) 148 | } 149 | -------------------------------------------------------------------------------- /obinary/rw/writer_test.go: -------------------------------------------------------------------------------- 1 | package rw 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | 7 | "testing" 8 | ) 9 | 10 | func TestWriteBytes(t *testing.T) { 11 | buf := new(bytes.Buffer) 12 | byteMsg := []byte("I like Ike") 13 | NewWriter(buf).WriteBytes(byteMsg) 14 | 15 | equals(t, 4+len(byteMsg), buf.Len()) 16 | bs := buf.Next(4) 17 | equals(t, len(byteMsg), bigEndianConvertToInt(bs)) 18 | 19 | bs = buf.Next(len(byteMsg)) 20 | equals(t, byteMsg, bs) 21 | } 22 | 23 | func TestWriteRawBytes(t *testing.T) { 24 | buf := new(bytes.Buffer) 25 | byteMsg := []byte("I like Ike") 26 | NewWriter(buf).WriteRawBytes(byteMsg) 27 | 28 | bs := buf.Next(len(byteMsg)) 29 | equals(t, byteMsg, bs) 30 | 31 | // write empty bytes 32 | buf = new(bytes.Buffer) 33 | byteMsg = []byte{} 34 | NewWriter(buf).WriteRawBytes(byteMsg) 35 | 36 | equals(t, 0, buf.Len()) 37 | } 38 | 39 | func TestWriteNull(t *testing.T) { 40 | buf := new(bytes.Buffer) 41 | NewWriter(buf).WriteNull() 42 | 43 | equals(t, 4, buf.Len()) // null in OrientDB is -1 (int32) 44 | 45 | var actInt int32 46 | binary.Read(buf, binary.BigEndian, &actInt) 47 | equals(t, int32(-1), actInt) 48 | } 49 | 50 | func TestWriteBool(t *testing.T) { 51 | buf := new(bytes.Buffer) 52 | bw := NewWriter(buf) 53 | bw.WriteBool(true) 54 | bw.WriteBool(false) 55 | bw.WriteBool(true) 56 | 57 | equals(t, 3, buf.Len()) 58 | bs := buf.Bytes() 59 | equals(t, byte(1), bs[0]) 60 | equals(t, byte(0), bs[1]) 61 | equals(t, byte(1), bs[2]) 62 | } 63 | 64 | func TestWriteFloat(t *testing.T) { 65 | f := float32(55.668209) 66 | buf := new(bytes.Buffer) 67 | NewWriter(buf).WriteFloat(f) 68 | 69 | equals(t, 4, buf.Len()) 70 | 71 | f2 := NewReader(buf).ReadFloat() 72 | equals(t, f, f2) 73 | } 74 | 75 | func TestWriteDouble(t *testing.T) { 76 | f := float64(199999999999999999955.6682090323333337298) 77 | buf := new(bytes.Buffer) 78 | NewWriter(buf).WriteDouble(f) 79 | 80 | equals(t, 8, buf.Len()) 81 | 82 | f2 := NewReader(buf).ReadDouble() 83 | equals(t, f, f2) 84 | } 85 | 86 | func TestWriteString(t *testing.T) { 87 | var buf bytes.Buffer 88 | NewWriter(&buf).WriteString("hello") 89 | equals(t, 9, buf.Len()) 90 | 91 | n, s := nextBinaryString(&buf) 92 | equals(t, 5, n) 93 | equals(t, "hello", s) 94 | } 95 | 96 | func TestWriteStrings(t *testing.T) { 97 | buf := new(bytes.Buffer) 98 | NewWriter(buf).WriteStrings("a", "a longer string", "golang") 99 | equals(t, (4*3)+len("a")+len("a longer string")+len("golang"), buf.Len()) 100 | 101 | // read back first string 102 | n, s := nextBinaryString(buf) 103 | equals(t, 1, n) 104 | equals(t, "a", s) 105 | 106 | // read back second string 107 | n, s = nextBinaryString(buf) 108 | equals(t, len("a longer string"), n) 109 | equals(t, "a longer string", s) 110 | 111 | // read back third string 112 | n, s = nextBinaryString(buf) 113 | equals(t, len("golang"), n) 114 | equals(t, "golang", s) 115 | } 116 | 117 | func TestWriteManyTypes(t *testing.T) { 118 | var ( 119 | buf bytes.Buffer 120 | bs []byte 121 | bw = NewWriter(&buf) 122 | ) 123 | bw.WriteByte(0x1) 124 | bw.WriteString("vått og tørt") 125 | bw.WriteShort(int16(29876)) 126 | bw.WriteShort(int16(444)) 127 | bw.WriteInt(9999999) 128 | bw.WriteLong(MaxInt64) 129 | equals(t, nil, bw.Err()) 130 | 131 | // read back 132 | bs = buf.Next(1) // byte 133 | equals(t, byte(0x1), bs[0]) 134 | 135 | bs = buf.Next(4) // str length 136 | equals(t, 14, bigEndianConvertToInt(bs)) 137 | 138 | bs = buf.Next(14) // str contents 139 | equals(t, "vått og tørt", string(bs)) 140 | 141 | var act int16 142 | binary.Read(&buf, binary.BigEndian, &act) // use the binary.Read to convert rather than manual 143 | equals(t, int16(29876), act) 144 | 145 | binary.Read(&buf, binary.BigEndian, &act) // use the binary.Read to convert rather than manual 146 | equals(t, int16(444), act) 147 | 148 | var actInt int32 149 | binary.Read(&buf, binary.BigEndian, &actInt) 150 | equals(t, int32(9999999), actInt) 151 | 152 | var actLong int64 153 | binary.Read(&buf, binary.BigEndian, &actLong) 154 | equals(t, MaxInt64, actLong) 155 | 156 | } 157 | 158 | /* ---[ helper fns ]--- */ 159 | 160 | func nextBinaryString(buf *bytes.Buffer) (int, string) { 161 | intBytes := buf.Next(4) 162 | intVal := int(intBytes[3]) | int(intBytes[2])<<8 | int(intBytes[1])<<16 | int(intBytes[0])<<24 163 | 164 | strBytes := buf.Next(intVal) 165 | return intVal, string(strBytes) 166 | } 167 | 168 | func bigEndianConvertToInt(bs []byte) int { 169 | return int(binary.BigEndian.Uint32(bs)) 170 | } 171 | 172 | func TestWriteBytesVarint_GoodData_5Bytes(t *testing.T) { 173 | buf := new(bytes.Buffer) 174 | NewWriter(buf).WriteBytesVarint([]byte("total")) 175 | 176 | equals(t, 6, buf.Len()) 177 | 178 | n, err := buf.ReadByte() 179 | equals(t, byte(10), n) // zigzag encoded value of 5 is 10 180 | ok(t, err) 181 | 182 | strbytes := buf.Next(5) 183 | equals(t, "total", string(strbytes)) 184 | 185 | // // varint.ReadBytes expects a varint encoded int, followed by that many bytes 186 | // outbytes, err := ReadBytes(buf) 187 | // ok(t, err) 188 | // equals(t, 5, len(outbytes)) 189 | // equals(t, "total", string(outbytes)) 190 | } 191 | 192 | func TestWriteStringVarint_GoodData_5Bytes(t *testing.T) { 193 | buf := new(bytes.Buffer) 194 | NewWriter(buf).WriteStringVarint("total") 195 | 196 | equals(t, 6, buf.Len()) 197 | 198 | n, err := buf.ReadByte() 199 | equals(t, byte(10), n) // zigzag encoded value of 5 is 10 200 | ok(t, err) 201 | 202 | strbytes := buf.Next(5) 203 | equals(t, "total", string(strbytes)) 204 | } 205 | 206 | func TestWriteStringVarint_GoodData_EmptyString(t *testing.T) { 207 | buf := new(bytes.Buffer) 208 | NewWriter(buf).WriteStringVarint("") 209 | 210 | equals(t, 1, buf.Len()) 211 | 212 | n, err := buf.ReadByte() 213 | equals(t, byte(0), n) 214 | ok(t, err) 215 | } 216 | -------------------------------------------------------------------------------- /obinary/util.go: -------------------------------------------------------------------------------- 1 | package obinary 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | func catch2(err *error) { 8 | if r := recover(); r != nil { 9 | switch rr := r.(type) { 10 | case error: 11 | *err = rr 12 | default: 13 | *err = fmt.Errorf("%v", r) 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /oclass.go: -------------------------------------------------------------------------------- 1 | package orient 2 | 3 | import "log" 4 | 5 | type OClass struct { 6 | Name string 7 | ShortName string 8 | Properties map[string]*OProperty 9 | DefaultClusterId int32 10 | ClusterIds []int32 11 | SuperClass string 12 | OverSize float32 13 | StrictMode bool 14 | AbstractClass bool 15 | ClusterSelection string // OClusterSelectionStrategy in Java code - needed? 16 | CustomFields map[string]string 17 | } 18 | 19 | // Should be passed an Document that comes from a load schema 20 | // request to the database. 21 | func NewOClassFromDocument(doc *Document) *OClass { 22 | oclass := &OClass{Properties: make(map[string]*OProperty)} 23 | 24 | if fld := doc.GetField("name"); fld != nil && fld.Value != nil { 25 | oclass.Name = fld.Value.(string) 26 | } 27 | if fld := doc.GetField("shortName"); fld != nil && fld.Value != nil { 28 | oclass.ShortName = fld.Value.(string) 29 | } 30 | 31 | // properties comes back as an Document 32 | if fld := doc.GetField("properties"); fld != nil && fld.Value != nil { 33 | propsDocs := convertToODocumentRefSlice(fld.Value.([]interface{})) 34 | for _, propDoc := range propsDocs { 35 | oprop := NewOPropertyFromDocument(propDoc) 36 | oclass.Properties[oprop.Name] = oprop 37 | } 38 | } 39 | if fld := doc.GetField("defaultClusterId"); fld != nil && fld.Value != nil { 40 | oclass.DefaultClusterId = fld.Value.(int32) 41 | } 42 | if fld := doc.GetField("clusterIds"); fld != nil && fld.Value != nil { 43 | oclass.ClusterIds = convertToInt32Slice(fld.Value.([]interface{})) 44 | } 45 | if fld := doc.GetField("superClass"); fld != nil && fld.Value != nil { 46 | oclass.SuperClass = fld.Value.(string) 47 | } 48 | if fld := doc.GetField("overSize"); fld != nil && fld.Value != nil { 49 | oclass.OverSize = fld.Value.(float32) 50 | } 51 | if fld := doc.GetField("strictMode"); fld != nil && fld.Value != nil { 52 | oclass.StrictMode = fld.Value.(bool) 53 | } 54 | if fld := doc.GetField("abstract"); fld != nil && fld.Value != nil { 55 | oclass.AbstractClass = fld.Value.(bool) 56 | } 57 | if fld := doc.GetField("clusterSelection"); fld != nil && fld.Value != nil { 58 | oclass.ClusterSelection = fld.Value.(string) 59 | } 60 | if fld := doc.GetField("customFields"); fld != nil && fld.Value != nil { 61 | if m, ok := fld.Value.(map[string]string); ok { 62 | oclass.CustomFields = m 63 | } else { 64 | log.Printf("unknown type for customFields: %T\n", fld.Value) 65 | oclass.CustomFields = make(map[string]string) 66 | } 67 | } 68 | 69 | return oclass 70 | } 71 | 72 | func convertToODocumentRefSlice(x []interface{}) []*Document { 73 | y := make([]*Document, len(x)) 74 | for i, v := range x { 75 | y[i] = v.(*Document) 76 | } 77 | return y 78 | } 79 | 80 | func convertToInt32Slice(x []interface{}) []int32 { 81 | y := make([]int32, len(x)) 82 | for i, v := range x { 83 | y[i] = v.(int32) 84 | } 85 | return y 86 | } 87 | -------------------------------------------------------------------------------- /orient_test.go: -------------------------------------------------------------------------------- 1 | package orient_test 2 | 3 | import ( 4 | "fmt" 5 | "math/rand" 6 | "net" 7 | "net/http" 8 | _ "net/http/pprof" 9 | "os" 10 | "testing" 11 | "time" 12 | 13 | "github.com/fsouza/go-dockerclient" 14 | "gopkg.in/istreamdata/orientgo.v2" 15 | _ "gopkg.in/istreamdata/orientgo.v2/obinary" 16 | "reflect" 17 | ) 18 | 19 | var orientVersion = "2.1" 20 | 21 | const ( 22 | dbName = "default" 23 | dbUser = "admin" 24 | dbPass = "admin" 25 | 26 | srvUser = "root" 27 | srvPass = "root" 28 | ) 29 | 30 | func init() { 31 | rand.Seed(time.Now().UnixNano()) 32 | if vers := os.Getenv("ORIENT_VERS"); vers != "" { 33 | orientVersion = vers 34 | } 35 | fmt.Printf("Testing against OrientDB %s\n", orientVersion) 36 | go func() { 37 | fmt.Println("pprof: ", http.ListenAndServe(":6060", nil)) 38 | }() 39 | } 40 | 41 | func TestNewDB(t *testing.T) { 42 | _, closer := SpinOrient(t) 43 | defer closer() 44 | } 45 | 46 | func TestDBAuth(t *testing.T) { 47 | db, closer := SpinOrient(t) 48 | defer closer() 49 | if _, err := db.Auth(srvUser, srvPass); err != nil { 50 | t.Fatal("Connection to database failed") 51 | } 52 | } 53 | 54 | func TestDBAuthWrong(t *testing.T) { 55 | db, closer := SpinOrient(t) 56 | defer closer() 57 | if _, err := db.Auth(srvUser, srvPass+"pass"); err == nil { 58 | t.Fatal("auth error expected") 59 | } 60 | } 61 | 62 | func SpinOrientServer(t *testing.T) (string, func()) { 63 | const port = 2424 64 | if orientVersion == "local" { 65 | return fmt.Sprintf("localhost:%d", port), func() {} 66 | } 67 | 68 | dport_api := docker.Port("2424/tcp") 69 | dport_web := docker.Port("2480/tcp") 70 | binds := make(map[docker.Port][]docker.PortBinding) 71 | // binds[dport_api] = []docker.PortBinding{docker.PortBinding{HostPort: fmt.Sprint(port)}} 72 | // binds[dport_web] = []docker.PortBinding{docker.PortBinding{HostPort: fmt.Sprint(port + 1)}} 73 | 74 | cl, err := docker.NewClient("unix:///var/run/docker.sock") 75 | if err != nil { 76 | t.Skip(err) 77 | } 78 | cont, err := cl.CreateContainer(docker.CreateContainerOptions{ 79 | Config: &docker.Config{ 80 | OpenStdin: true, Tty: true, 81 | ExposedPorts: map[docker.Port]struct{}{dport_api: struct{}{}, dport_web: struct{}{}}, 82 | Image: `dennwc/orientdb:` + orientVersion, 83 | }, HostConfig: &docker.HostConfig{ 84 | PortBindings: binds, 85 | }, 86 | }) 87 | if err != nil { 88 | t.Skip(err) 89 | } 90 | 91 | rm := func() { 92 | cl.RemoveContainer(docker.RemoveContainerOptions{ID: cont.ID, Force: true}) 93 | } 94 | 95 | if err := cl.StartContainer(cont.ID, &docker.HostConfig{PortBindings: binds}); err != nil { 96 | rm() 97 | t.Skip(err) 98 | } 99 | 100 | info, err := cl.InspectContainer(cont.ID) 101 | if err != nil { 102 | rm() 103 | t.Skip(err) 104 | } 105 | { 106 | start := time.Now() 107 | ok := false 108 | for time.Since(start) < time.Second*5 { 109 | conn, err := net.DialTimeout("tcp", net.JoinHostPort(info.NetworkSettings.IPAddress, fmt.Sprint(port)), time.Second/4) 110 | if err == nil { 111 | ok = true 112 | conn.Close() 113 | break 114 | } 115 | } 116 | if !ok { 117 | rm() 118 | t.Fatal("Orient container did not come up in time") 119 | } 120 | } 121 | 122 | return fmt.Sprintf("%s:%d", info.NetworkSettings.IPAddress, port), rm 123 | } 124 | 125 | func SpinOrient(t *testing.T) (*orient.Client, func()) { 126 | addr, rm := SpinOrientServer(t) 127 | cli, err := orient.Dial(addr) 128 | if err != nil { 129 | rm() 130 | t.Fatal(err) 131 | } 132 | return cli, func() { 133 | cli.Close() 134 | rm() 135 | } 136 | } 137 | 138 | func SpinOrientAndOpenDB(t *testing.T, graph bool) (*orient.Database, func()) { 139 | cli, closer := SpinOrient(t) 140 | tp := orient.DocumentDB 141 | if graph { 142 | tp = orient.GraphDB 143 | } 144 | db, err := cli.Open(dbName, tp, dbUser, dbPass) 145 | if err != nil { 146 | closer() 147 | t.Fatal(err) 148 | } 149 | return db, closer 150 | } 151 | 152 | var DocumentDBSeeds = []string{ 153 | "CREATE CLASS Animal", 154 | "CREATE property Animal.name string", 155 | "CREATE property Animal.age integer", 156 | "CREATE CLASS Cat extends Animal", 157 | "CREATE property Cat.caretaker string", 158 | "INSERT INTO Cat (name, age, caretaker) VALUES ('Linus', 15, 'Michael'), ('Keiko', 10, 'Anna')", 159 | } 160 | 161 | func SeedDB(t *testing.T, db *orient.Database) { 162 | for _, seed := range DocumentDBSeeds { 163 | if err := db.Command(orient.NewSQLCommand(seed)).Err(); err != nil { 164 | t.Fatal(err) 165 | } 166 | } 167 | } 168 | 169 | func testOUserCommand(t *testing.T, fnc func(*orient.Database) orient.Results) { 170 | cli, closer := SpinOrientAndOpenDB(t, false) 171 | defer closer() 172 | 173 | var docs []orient.OIdentifiable 174 | err := fnc(cli).All(&docs) 175 | if err != nil { 176 | t.Fatal(err) 177 | } else if len(docs) != 3 { 178 | t.Error("wrong docs count") 179 | } 180 | //t.Logf("docs[%d]: %+v", len(docs), docs) 181 | } 182 | 183 | func TestSelect(t *testing.T) { 184 | testOUserCommand(t, func(cli *orient.Database) orient.Results { 185 | if orientVersion < "2.1" { 186 | return cli.Command(orient.NewSQLQuery("SELECT FROM OUser LIMIT 3")) 187 | } 188 | return cli.Command(orient.NewSQLQuery("SELECT FROM OUser LIMIT ?", 3)) 189 | }) 190 | } 191 | 192 | func TestSelectCommand(t *testing.T) { 193 | testOUserCommand(t, func(cli *orient.Database) orient.Results { 194 | if orientVersion < "2.1" { 195 | return cli.Command(orient.NewSQLCommand("SELECT FROM OUser LIMIT 3")) 196 | } 197 | return cli.Command(orient.NewSQLCommand("SELECT FROM OUser LIMIT ?", 3)) 198 | }) 199 | } 200 | 201 | func TestSelectScript(t *testing.T) { 202 | testOUserCommand(t, func(cli *orient.Database) orient.Results { 203 | return cli.Command(orient.NewScriptCommand(orient.LangSQL, "SELECT FROM OUser")) 204 | }) 205 | } 206 | 207 | func TestSelectScriptJS(t *testing.T) { 208 | testOUserCommand(t, func(cli *orient.Database) orient.Results { 209 | return cli.Command(orient.NewScriptCommand(orient.LangJS, `var docs = db.query('SELECT FROM OUser'); docs`)) 210 | }) 211 | } 212 | 213 | func TestSelectSaveFunc(t *testing.T) { 214 | cli, closer := SpinOrientAndOpenDB(t, false) 215 | defer closer() 216 | 217 | name := "tempFuncOne" 218 | code := ` 219 | var param = one 220 | if (param != "some") { 221 | response.send(500, "err", "text/plain", "err" ) 222 | } 223 | if (typeof(two) != "object") { 224 | response.send(500, "err2", "text/plain", "err2" ) 225 | } else if (two.Name != "one") { 226 | response.send(500, "err3", "text/plain", "err3" ) 227 | } 228 | var unused = "\\" 229 | var tbl = 'OUser' 230 | var docs = db.query("SELECT FROM "+tbl) 231 | return docs 232 | ` 233 | if err := cli.CreateScriptFunc(orient.Function{ 234 | Name: name, Code: code, Idemp: false, 235 | Lang: orient.LangJS, Params: []string{"one", "two"}, 236 | }); err != nil { 237 | t.Fatal(err) 238 | } 239 | 240 | var fnc []struct { 241 | Name string 242 | Code string 243 | } 244 | if err := cli.Command(orient.NewSQLQuery("SELECT FROM OFunction")).All(&fnc); err != nil { 245 | t.Fatal(err) 246 | } else if len(fnc) != 1 { 247 | t.Fatal("wrong func count") 248 | } else if fnc[0].Name != name { 249 | t.Fatalf("wrong func name: '%v' vs '%v'", fnc[0].Name, name) 250 | } else if fnc[0].Code != code { 251 | t.Fatal(fmt.Errorf("wrong func code:\n\n%s\nvs\n%s\n", fnc[0].Code, code)) 252 | } 253 | 254 | var o interface{} 255 | err := cli.CallScriptFunc(name, "some", struct{ Name string }{"one"}).All(&o) 256 | if err != nil { 257 | t.Fatal(err) 258 | } else if docs, ok := o.([]orient.OIdentifiable); !ok { 259 | t.Errorf("expected list, got: %T", o) 260 | } else if len(docs) != 3 { 261 | t.Error("wrong docs count") 262 | } 263 | //t.Logf("docs[%d]: %+v", len(recs), recs) 264 | } 265 | 266 | func TestSelectSaveFunc2(t *testing.T) { 267 | cli, closer := SpinOrientAndOpenDB(t, false) 268 | defer closer() 269 | 270 | name := "tempFuncTwo" 271 | code := `return {"params": [one, two]}` 272 | if err := cli.CreateScriptFunc(orient.Function{ 273 | Name: name, Code: code, Idemp: false, 274 | Lang: orient.LangJS, Params: []string{"one", "two"}, 275 | }); err != nil { 276 | t.Fatal(err) 277 | } 278 | 279 | var fnc []struct { 280 | Name string 281 | Code string 282 | } 283 | if err := cli.Command(orient.NewSQLQuery("SELECT FROM OFunction")).All(&fnc); err != nil { 284 | t.Fatal(err) 285 | } else if len(fnc) != 1 { 286 | t.Fatal("wrong func count") 287 | } else if fnc[0].Name != name { 288 | t.Fatal("wrong func name") 289 | } else if fnc[0].Code != code { 290 | t.Fatal(fmt.Errorf("wrong func code:\n\n%s\nvs\n%s\n", fnc[0].Code, code)) 291 | } 292 | 293 | var res struct { 294 | Params []string 295 | } 296 | err := cli.CallScriptFunc(name, "some", "one").All(&res) 297 | if err != nil { 298 | t.Fatal(err) 299 | } else if len(res.Params) != 2 { 300 | t.Error("wrong result count") 301 | } 302 | } 303 | 304 | func TestSelectSaveFuncResult(t *testing.T) { 305 | cli, closer := SpinOrientAndOpenDB(t, false) 306 | defer closer() 307 | 308 | name := "tempFuncOne" 309 | code := `return {"name":"ori","props":{"data":"ok","num":10,"custom":one}}` 310 | if err := cli.CreateScriptFunc(orient.Function{ 311 | Name: name, Code: code, Idemp: false, 312 | Lang: orient.LangJS, Params: []string{"one"}, 313 | }); err != nil { 314 | t.Fatal(err) 315 | } 316 | var result struct { 317 | Name string 318 | Props map[string]interface{} 319 | } 320 | err := cli.CallScriptFunc(name, "some").All(&result) 321 | if err != nil { 322 | t.Fatal(err) 323 | } else if result.Name != "ori" { 324 | t.Fatal("wrong object name property") 325 | } else if len(result.Props) == 0 { 326 | t.Fatal("empty object props") 327 | } 328 | //t.Logf("doc: %+v", result) 329 | } 330 | 331 | func TestScriptParams(t *testing.T) { 332 | cli, closer := SpinOrientAndOpenDB(t, false) 333 | defer closer() 334 | 335 | name := "tempFuncOne" 336 | code := `return {"aaa": one, "bbb": two}` 337 | if err := cli.CreateScriptFunc(orient.Function{ 338 | Name: name, Code: code, Idemp: false, 339 | Lang: orient.LangJS, Params: []string{"one", "two"}, 340 | }); err != nil { 341 | t.Fatal(err) 342 | } 343 | var o map[string]interface{} 344 | err := cli.CallScriptFunc(name, map[string]string{"one": "first"}, "two").All(&o) 345 | if err != nil { 346 | t.Fatal(err) 347 | } else if len(o) != 2 { 348 | t.Fatal("wrong map leng") 349 | } else if av, ok := o["aaa"]; !ok { 350 | t.Fatal("'a' value not found") 351 | } else if bv, ok := o["bbb"]; !ok { 352 | t.Fatal("'b' value not found") 353 | } else if am, ok := av.(map[string]string); !ok { 354 | t.Fatal("wrong type for 'a' value") 355 | } else if len(am) != 1 { 356 | t.Fatal("wrong value for 'a'") // TODO: check data 357 | } else if bs, ok := bv.(string); !ok { 358 | t.Fatal("wrong type for 'b' value") 359 | } else if bs != "two" { 360 | t.Fatal("wrong value for 'b'") 361 | } 362 | t.Logf("%+v(%T)\n", o, o) 363 | } 364 | 365 | func TestScriptJSMap(t *testing.T) { 366 | cli, closer := SpinOrientAndOpenDB(t, false) 367 | defer closer() 368 | 369 | var o []orient.OIdentifiable 370 | err := cli.Command(orient.NewScriptCommand(orient.LangJS, `var a = {"aaa":"one","bbb": 2}; a`)).All(&o) 371 | if err != nil { 372 | t.Fatal(err) 373 | } else if len(o) != 1 { 374 | t.Skipf("wrong array leng: %+v(%d)", o, len(o)) 375 | } else if o[0] == nil { 376 | t.Skipf("nil record: %+v(%T)", o, o) 377 | } 378 | } 379 | 380 | func TestCommandStringQuotes(t *testing.T) { 381 | cli, closer := SpinOrientAndOpenDB(t, false) 382 | defer closer() 383 | const name = `Para Que Peliar "Besame"` 384 | doc := orient.NewEmptyDocument() 385 | doc.SetField("name", name) 386 | doc.SetClassNameIfExists("V") 387 | err := cli.CreateRecord(doc) 388 | if err != nil { 389 | t.Fatal(err) 390 | } 391 | var odoc *orient.Document 392 | err = cli.Command(orient.NewSQLQuery(`SELECT FROM V WHERE name = ?`, name)).All(&odoc) 393 | if err != nil { 394 | t.Fatal(err) 395 | } else if val := odoc.GetField("name").Value.(string); val != name { 396 | t.Fatalf("strings are different: %v vs %v", val, name) 397 | } 398 | } 399 | 400 | func TestSQLQueryParams(t *testing.T) { 401 | notShort(t) 402 | db, closer := SpinOrientAndOpenDB(t, false) 403 | defer closer() 404 | defer catch(t) 405 | SeedDB(t, db) 406 | 407 | var doc *orient.Document 408 | err := db.Command(orient.NewSQLQuery(`SELECT FROM Cat WHERE name=? AND age=?`, "Linus", 15)).All(&doc) 409 | if err != nil { 410 | t.Fatal(err) 411 | } else if doc.GetField("name").Value.(string) != "Linus" { 412 | t.Fatal("wrong field value") 413 | } 414 | } 415 | 416 | func TestSQLCommandParams(t *testing.T) { 417 | notShort(t) 418 | db, closer := SpinOrientAndOpenDB(t, false) 419 | defer closer() 420 | defer catch(t) 421 | SeedDB(t, db) 422 | 423 | var doc *orient.Document 424 | err := db.Command(orient.NewSQLCommand(`SELECT FROM Cat WHERE name=? AND age=?`, "Linus", 15)).All(&doc) 425 | if err != nil { 426 | t.Fatal(err) 427 | } else if doc.GetField("name").Value.(string) != "Linus" { 428 | t.Fatal("wrong field value") 429 | } 430 | } 431 | 432 | func TestSQLCommandParamsCustomType(t *testing.T) { 433 | notShort(t) 434 | db, closer := SpinOrientAndOpenDB(t, false) 435 | defer closer() 436 | defer catch(t) 437 | SeedDB(t, db) 438 | 439 | type Age int 440 | type Name string 441 | 442 | var doc *orient.Document 443 | err := db.Command(orient.NewSQLCommand(`SELECT FROM Cat WHERE name=? AND age=?`, Name("Linus"), Age(15))).All(&doc) 444 | if err != nil { 445 | t.Fatal(err) 446 | } else if doc.GetField("name").Value.(string) != "Linus" { 447 | t.Fatal("wrong field value") 448 | } else if doc.GetField("age").Value.(int32) != 15 { 449 | t.Fatal("wrong field value") 450 | } 451 | 452 | var cat *struct { 453 | Name Name 454 | Age Age 455 | } 456 | err = db.Command(orient.NewSQLCommand(`SELECT FROM Cat WHERE name=? AND age=?`, Name("Linus"), Age(15))).All(&cat) 457 | if err != nil { 458 | t.Fatal(err) 459 | } else if cat.Name != "Linus" { 460 | t.Fatal("wrong field value") 461 | } else if cat.Age != 15 { 462 | t.Fatal("wrong field value") 463 | } 464 | } 465 | 466 | func TestSQLInnerStruct(t *testing.T) { 467 | notShort(t) 468 | db, closer := SpinOrientAndOpenDB(t, false) 469 | defer closer() 470 | defer catch(t) 471 | SeedDB(t, db) 472 | 473 | type Inner struct { 474 | Name string 475 | } 476 | type Item struct { 477 | One Inner 478 | Inner []Inner 479 | } 480 | 481 | one, two := Inner{Name: "one"}, Inner{Name: "two"} 482 | 483 | err := db.Command(orient.NewSQLCommand(`CREATE VERTEX V CONTENT ` + orient.MarshalContent(Item{One: one, Inner: []Inner{one, two}}))).Err() 484 | if err != nil { 485 | t.Fatal(err) 486 | } 487 | var obj *Item 488 | err = db.Command(orient.NewSQLQuery(`SELECT FROM V WHERE One IS NOT NULL`)).All(&obj) 489 | if err != nil { 490 | t.Fatal(err) 491 | } else if obj.One != one { 492 | t.Fatal("wrong value") 493 | } else if len(obj.Inner) != 2 || obj.Inner[0] != one || obj.Inner[1] != two { 494 | t.Fatal("wrong list value") 495 | } 496 | } 497 | 498 | func TestSQLStructSelect(t *testing.T) { 499 | notShort(t) 500 | db, closer := SpinOrientAndOpenDB(t, false) 501 | defer closer() 502 | defer catch(t) 503 | 504 | type Item struct { 505 | Name string 506 | } 507 | 508 | arr := []Item{ 509 | {Name: "one"}, {Name: "two"}, 510 | } 511 | 512 | for _, it := range arr { 513 | doc := orient.NewDocument("V") 514 | if err := doc.From(it); err != nil { 515 | t.Fatal(err) 516 | } 517 | if err := db.CreateRecord(doc); err != nil { 518 | t.Fatal(err) 519 | } 520 | } 521 | 522 | { 523 | var out []Item 524 | err := db.Command(orient.NewSQLQuery(`SELECT * FROM V WHERE Name IS NOT NULL`)).All(&out) 525 | if err != nil { 526 | t.Fatal(err) 527 | } else if !reflect.DeepEqual(arr, out) { 528 | t.Fatal("wrong results") 529 | } 530 | 531 | out = nil 532 | err = db.Command(orient.NewSQLQuery(`SELECT * FROM V WHERE Name = "one"`)).All(&out) 533 | if err != nil { 534 | t.Fatal(err) 535 | } else if !reflect.DeepEqual(arr[:1], out) { 536 | t.Fatal("wrong results") 537 | } 538 | 539 | out = nil 540 | err = db.Command(orient.NewSQLQuery(`SELECT * FROM V WHERE Name = "unk"`)).All(&out) 541 | if err != nil { 542 | t.Fatal(err) 543 | } else if len(out) != 0 { 544 | t.Fatal("wrong results") 545 | } 546 | } 547 | { 548 | var out *Item 549 | err := db.Command(orient.NewSQLQuery(`SELECT * FROM V WHERE Name IS NOT NULL`)).All(&out) 550 | if err == nil { 551 | t.Fatal("error expected") 552 | } else if _, ok := err.(orient.ErrMultipleRecords); !ok { 553 | t.Fatal("wrong error type returned") 554 | } 555 | 556 | out = nil 557 | err = db.Command(orient.NewSQLQuery(`SELECT * FROM V WHERE Name = "one"`)).All(&out) 558 | if err != nil { 559 | t.Fatal(err) 560 | } else if !reflect.DeepEqual(&arr[0], out) { 561 | t.Fatal("wrong results") 562 | } 563 | 564 | out = nil 565 | err = db.Command(orient.NewSQLQuery(`SELECT * FROM V WHERE Name = "unk"`)).All(&out) 566 | if err != orient.ErrNoRecord { 567 | t.Fatal("wrong error type returned") 568 | } 569 | } 570 | } 571 | 572 | func TestNativeInnerStruct(t *testing.T) { 573 | notShort(t) 574 | db, closer := SpinOrientAndOpenDB(t, false) 575 | defer closer() 576 | defer catch(t) 577 | SeedDB(t, db) 578 | 579 | type Inner struct { 580 | Name string 581 | } 582 | type Item struct { 583 | One Inner 584 | Inner []Inner 585 | } 586 | 587 | one, two := Inner{Name: "one"}, Inner{Name: "two"} 588 | 589 | doc := orient.NewDocument("V") 590 | err := doc.From(Item{One: one, Inner: []Inner{one, two}}) 591 | if err != nil { 592 | t.Fatal(err) 593 | } 594 | err = db.CreateRecord(doc) 595 | if err != nil { 596 | t.Fatal(err) 597 | } 598 | 599 | var obj *Item 600 | err = db.Command(orient.NewSQLQuery(`SELECT FROM V WHERE One IS NOT NULL`)).All(&obj) 601 | if err != nil { 602 | t.Fatal(err) 603 | } else if obj.One != one { 604 | t.Fatal("wrong value") 605 | } else if len(obj.Inner) != 2 || obj.Inner[0] != one || obj.Inner[1] != two { 606 | t.Fatal("wrong list value") 607 | } 608 | } 609 | 610 | func TestSQLBatchParams(t *testing.T) { 611 | notShort(t) 612 | db, closer := SpinOrientAndOpenDB(t, false) 613 | defer closer() 614 | defer catch(t) 615 | SeedDB(t, db) 616 | 617 | var doc *orient.Document 618 | err := db.Command(orient.NewScriptCommand(orient.LangSQL, ` 619 | LET cat = SELECT FROM Cat WHERE name=? AND age=? 620 | RETURN $cat 621 | `, "Linus", 15)).All(&doc) 622 | if err != nil { 623 | t.Fatal(err) 624 | } else if doc.GetField("name").Value.(string) != "Linus" { 625 | t.Fatal("wrong field value") 626 | } 627 | } 628 | 629 | func TestSelectDocumentAsMap(t *testing.T) { 630 | cli, closer := SpinOrientAndOpenDB(t, false) 631 | defer closer() 632 | 633 | type Item struct { 634 | Name string 635 | } 636 | var out map[string]*Item 637 | err := cli.Command(orient.NewScriptCommand(orient.LangJS, ` 638 | var docs = (new com.orientechnologies.orient.core.record.impl.ODocument()).fromJSON('{"one":{"Name":"record"}}'); docs`, 639 | )).All(&out) 640 | if err != nil { 641 | t.Fatal(err) 642 | } else if len(out) != 1 { 643 | t.Error("wrong docs count") 644 | } else if !reflect.DeepEqual(out, map[string]*Item{"one": &Item{"record"}}) { 645 | t.Error("wrong data returned") 646 | } 647 | } 648 | 649 | func TestConcurrentModification(t *testing.T) { 650 | notShort(t) 651 | db, closer := SpinOrientAndOpenDB(t, false) 652 | defer closer() 653 | defer catch(t) 654 | 655 | const n = 5 656 | 657 | doc := orient.NewEmptyDocument() 658 | doc.SetField("name", "") 659 | err := db.CreateRecord(doc) 660 | if err != nil { 661 | t.Fatal(err) 662 | } 663 | 664 | pause := make(chan struct{}) 665 | errc := make(chan error, n) 666 | for i := 0; i < n; i++ { 667 | go func(i int) { 668 | <-pause 669 | errc <- db.Command(orient.NewSQLCommand( 670 | fmt.Sprintf(`UPDATE %s SET name=?`, doc.RID), 671 | fmt.Sprintf("data%d", i), 672 | )).Err() 673 | }(i) 674 | } 675 | close(pause) 676 | for i := 0; i < n; i++ { 677 | if err := <-errc; err != nil { 678 | t.Errorf("(%T): %v", err, err) 679 | } 680 | } 681 | } 682 | -------------------------------------------------------------------------------- /property.go: -------------------------------------------------------------------------------- 1 | package orient 2 | 3 | // OProperty roughly corresponds to OProperty in the Java client. 4 | // It represents a property of a class in OrientDB. 5 | // A property represents the metadata of a field. A field (OField) 6 | // is the actual data of a field in an Document. 7 | type OProperty struct { 8 | Id int32 9 | Name string 10 | Fullname string // Classname.propertyName 11 | Type byte // corresponds to one of the type constants above 12 | NotNull bool 13 | Collate string // is OCollate in Java client 14 | Mandatory bool 15 | Min string 16 | Max string 17 | Regexp string 18 | CustomFields map[string]string 19 | Readonly bool 20 | } 21 | 22 | // NewOPropertyFromDocument creates a new OProperty from an Document 23 | // that was created after a load schema call to the OrientDB server. 24 | func NewOPropertyFromDocument(doc *Document) *OProperty { 25 | oprop := &OProperty{} 26 | if fld := doc.GetField("globalId"); fld != nil && fld.Value != nil { 27 | oprop.Id = fld.Value.(int32) 28 | } 29 | if fld := doc.GetField("name"); fld != nil && fld.Value != nil { 30 | oprop.Name = fld.Value.(string) 31 | } 32 | if fld := doc.GetField("type"); fld != nil && fld.Value != nil { 33 | oprop.Type = byte(fld.Value.(int32)) 34 | } 35 | if fld := doc.GetField("notNull"); fld != nil && fld.Value != nil { 36 | oprop.NotNull = fld.Value.(bool) 37 | } 38 | if fld := doc.GetField("collate"); fld != nil && fld.Value != nil { 39 | oprop.Collate = fld.Value.(string) 40 | } 41 | if fld := doc.GetField("mandatory"); fld != nil && fld.Value != nil { 42 | oprop.Mandatory = fld.Value.(bool) 43 | } 44 | if fld := doc.GetField("min"); fld != nil && fld.Value != nil { 45 | oprop.Min = fld.Value.(string) 46 | } 47 | if fld := doc.GetField("max"); fld != nil && fld.Value != nil { 48 | oprop.Max = fld.Value.(string) 49 | } 50 | if fld := doc.GetField("regexp"); fld != nil && fld.Value != nil { 51 | oprop.Regexp = fld.Value.(string) 52 | } 53 | if fld := doc.GetField("customFields"); fld != nil && fld.Value != nil { 54 | oprop.CustomFields = make(map[string]string) 55 | panic("customFields handling NOT IMPLEMENTED: Don't know what data structure is coming back from the server (need example)") 56 | } 57 | if fld := doc.GetField("readonly"); fld != nil && fld.Value != nil { 58 | oprop.Readonly = fld.Value.(bool) 59 | } 60 | 61 | return oprop 62 | } 63 | -------------------------------------------------------------------------------- /proto.go: -------------------------------------------------------------------------------- 1 | package orient 2 | 3 | // Default protocols 4 | const ( 5 | ProtoBinary = "binary" 6 | ) 7 | 8 | // DatabaseType defines database access type (Document or Graph) 9 | type DatabaseType string 10 | 11 | // List of database access types 12 | const ( 13 | DocumentDB DatabaseType = "document" 14 | GraphDB DatabaseType = "graph" 15 | ) 16 | 17 | // StorageType defines supported database storage types 18 | type StorageType string 19 | 20 | const ( 21 | // Persistent type represents on-disk database 22 | Persistent StorageType = "plocal" 23 | // Volatile type represents in-memory database 24 | Volatile StorageType = "memory" 25 | ) 26 | 27 | var ( 28 | protos = make(map[string]func(addr string) (DBConnection, error)) 29 | ) 30 | 31 | // RegisterProto registers a new protocol for Dial command 32 | func RegisterProto(name string, dial func(addr string) (DBConnection, error)) { 33 | protos[name] = dial 34 | } 35 | 36 | // ODatabase stores database metadata 37 | type ODatabase struct { 38 | Name string 39 | Type DatabaseType 40 | Classes map[string]*OClass 41 | } 42 | 43 | // DBAdmin is a minimal interface for database management API implementation 44 | type DBAdmin interface { 45 | DatabaseExists(name string, storageType StorageType) (bool, error) 46 | CreateDatabase(name string, dbType DatabaseType, storageType StorageType) error 47 | DropDatabase(name string, storageType StorageType) error 48 | ListDatabases() (map[string]string, error) 49 | Close() error 50 | } 51 | 52 | // DBSession is a minimal interface for database API implementation 53 | type DBSession interface { 54 | Close() error 55 | Size() (int64, error) 56 | ReloadSchema() error 57 | GetCurDB() *ODatabase 58 | 59 | AddClusterWithID(clusterName string, id int16) (clusterID int16, err error) 60 | DropCluster(clusterName string) (err error) 61 | GetClusterDataRange(clusterName string) (begin, end int64, err error) 62 | ClustersCount(withDeleted bool, clusterNames ...string) (int64, error) 63 | 64 | CreateRecord(rec ORecord) (err error) 65 | DeleteRecordByRID(rid RID, recVersion int) error 66 | GetRecordByRID(rid RID, fetchPlan FetchPlan, ignoreCache bool) (rec ORecord, err error) 67 | UpdateRecord(rec ORecord) error 68 | CountRecords() (int64, error) 69 | 70 | Command(cmd CustomSerializable) (result interface{}, err error) 71 | } 72 | 73 | // DBConnection is a minimal interface for OrientDB server API implementation 74 | type DBConnection interface { 75 | Auth(user, pass string) (DBAdmin, error) 76 | Open(name string, dbType DatabaseType, user, pass string) (DBSession, error) 77 | Close() error 78 | } 79 | -------------------------------------------------------------------------------- /records.go: -------------------------------------------------------------------------------- 1 | package orient 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | type ORecord interface { 8 | OIdentifiable 9 | Fill(rid RID, version int, content []byte) error // TODO: put to separate interface? 10 | Content() ([]byte, error) 11 | Version() int 12 | SetVersion(v int) 13 | SetRID(rid RID) 14 | RecordType() RecordType 15 | } 16 | 17 | var ( 18 | _ ORecord = (*BytesRecord)(nil) 19 | _ ORecord = (*Document)(nil) 20 | ) 21 | 22 | // List of standard record types 23 | const ( 24 | RecordTypeDocument RecordType = 'd' 25 | RecordTypeBytes RecordType = 'b' 26 | RecordTypeFlat RecordType = 'f' 27 | ) 28 | 29 | func init() { 30 | declareRecordType(RecordTypeDocument, "document", func() ORecord { return NewEmptyDocument() }) 31 | //declareRecordType(RecordTypeFlat,"flat",func() orient.ORecord { panic("flat record type is not implemented") }) // TODO: implement 32 | declareRecordType(RecordTypeBytes, "bytes", func() ORecord { return NewBytesRecord() }) 33 | } 34 | 35 | // RecordType defines a registered record type 36 | type RecordType byte 37 | 38 | var recordFactories = make(map[RecordType]RecordFactory) 39 | 40 | // RecordFactory is a function to create records of certain type 41 | type RecordFactory func() ORecord 42 | 43 | func declareRecordType(tp RecordType, name string, fnc RecordFactory) { 44 | if _, ok := recordFactories[tp]; ok { 45 | panic(fmt.Errorf("record type byte '%v' already in use", tp)) 46 | } 47 | recordFactories[tp] = fnc 48 | } 49 | 50 | // GetRecordFactory returns RecordFactory for a given type 51 | func GetRecordFactory(tp RecordType) RecordFactory { 52 | return recordFactories[tp] 53 | } 54 | 55 | // NewRecordOfType creates a new record of specified type 56 | func NewRecordOfType(tp RecordType) ORecord { 57 | fnc := GetRecordFactory(tp) 58 | if fnc == nil { 59 | panic(fmt.Errorf("record type '%c' is not supported", tp)) 60 | } 61 | return fnc() 62 | } 63 | 64 | func NewBytesRecord() *BytesRecord { return &BytesRecord{} } 65 | 66 | // BytesRecord is a rawest representation of a record. It's schema less. 67 | // Use this if you need to store []byte without matter about the content. 68 | // Useful also to store multimedia contents and binary files. 69 | type BytesRecord struct { 70 | RID RID 71 | Vers int 72 | Data []byte 73 | } 74 | 75 | func (r BytesRecord) Content() ([]byte, error) { 76 | return r.Data, nil 77 | } 78 | 79 | func (r BytesRecord) Version() int { 80 | return r.Vers 81 | } 82 | func (r *BytesRecord) SetVersion(v int) { 83 | r.Vers = v 84 | } 85 | func (r *BytesRecord) SetRID(rid RID) { 86 | r.RID = rid 87 | } 88 | 89 | func (r BytesRecord) RecordType() RecordType { 90 | return RecordTypeBytes 91 | } 92 | 93 | // GetIdentity returns a record RID 94 | func (r BytesRecord) GetIdentity() RID { 95 | return r.RID 96 | } 97 | 98 | // GetRecord returns a record data 99 | func (r BytesRecord) GetRecord() interface{} { 100 | if r.Data == nil { 101 | return nil 102 | } 103 | return r.Data 104 | } 105 | 106 | // Fill sets identity, version and raw data of the record 107 | func (r *BytesRecord) Fill(rid RID, version int, content []byte) error { 108 | r.RID = rid 109 | r.Vers = version 110 | r.Data = content 111 | return nil 112 | } 113 | 114 | func (r BytesRecord) String() string { 115 | return fmt.Sprintf("Bytes{RID: %s, Vers: %d, Data: [%d]}", r.RID, r.Vers, len(r.Data)) 116 | } 117 | -------------------------------------------------------------------------------- /rid.go: -------------------------------------------------------------------------------- 1 | package orient 2 | 3 | // ref: com.orientechnologies.orient.core.id 4 | 5 | import ( 6 | "fmt" 7 | "gopkg.in/istreamdata/orientgo.v2/obinary/rw" 8 | "io" 9 | "strconv" 10 | "strings" 11 | ) 12 | 13 | type OIdentifiable interface { 14 | GetIdentity() RID 15 | GetRecord() interface{} 16 | } 17 | 18 | var ( 19 | _ OIdentifiable = RID{} 20 | ) 21 | 22 | const ( 23 | ridPrefix = '#' 24 | ridSeparator = ':' 25 | 26 | clusterIdMax = 32767 27 | clusterIdInvalid = -1 28 | clusterPosInvalid = -1 29 | 30 | ridSerializedSize = rw.SizeShort + rw.SizeLong 31 | ) 32 | 33 | // RID encapsulates the two aspects of an OrientDB RecordID - ClusterID:ClusterPos. 34 | // ORecordId in Java world. 35 | type RID struct { 36 | ClusterID int16 37 | ClusterPos int64 38 | } 39 | 40 | // NewEmptyRID returns an RID with the default "invalid" settings. 41 | // Invalid settings indicate that the Document has not yet been saved 42 | // to the DB (which assigns it a valid RID) or it indicates that 43 | // it is not a true Document with a Class (e.g., it is a result of a Property query) 44 | func NewEmptyRID() RID { 45 | return RID{ClusterID: clusterIdInvalid, ClusterPos: clusterPosInvalid} 46 | } 47 | 48 | func (rid RID) checkClusterLimits() { 49 | if rid.ClusterID < -2 { 50 | panic(fmt.Sprint("RecordId cannot support negative cluster id. You've used:", rid.ClusterID)) 51 | } 52 | if rid.ClusterID > clusterIdMax { 53 | panic(fmt.Sprint("RecordId cannot support cluster id major than 32767. You've used:", rid.ClusterID)) 54 | } 55 | } 56 | 57 | // NewRID creates a RID with given ClusterId and ClusterPos. It will check value for validity. 58 | func NewRID(cid int16, pos int64) RID { 59 | rid := RID{ClusterID: cid, ClusterPos: pos} 60 | rid.checkClusterLimits() 61 | return rid 62 | } 63 | 64 | // NewRIDInCluster creates an empty RID inside specified cluster 65 | func NewRIDInCluster(cid int16) RID { 66 | rid := RID{ClusterID: cid, ClusterPos: clusterPosInvalid} 67 | rid.checkClusterLimits() 68 | return rid 69 | } 70 | 71 | // GetIdentity implements OIdentifiable interface on RID 72 | func (r RID) GetIdentity() RID { 73 | return r 74 | } 75 | 76 | func (r RID) GetRecord() interface{} { 77 | return nil 78 | } 79 | 80 | // String converts RID to #N:M string format 81 | func (rid RID) String() string { 82 | return fmt.Sprintf( 83 | string(ridPrefix)+"%d"+string(ridSeparator)+"%d", 84 | rid.ClusterID, rid.ClusterPos, 85 | ) 86 | } 87 | 88 | func (r RID) IsValid() bool { 89 | return r.ClusterID != clusterIdInvalid 90 | } 91 | func (rid RID) IsPersistent() bool { 92 | return rid.ClusterID > -1 && rid.ClusterPos > clusterPosInvalid 93 | } 94 | func (rid RID) IsNew() bool { 95 | return rid.ClusterPos < 0 96 | } 97 | func (rid RID) IsTemporary() bool { 98 | return rid.ClusterID != -1 && rid.ClusterPos < clusterPosInvalid 99 | } 100 | 101 | // Next is a shortcut for rid.NextRID().String() 102 | func (rid RID) Next() string { 103 | return rid.NextRID().String() 104 | } 105 | 106 | // NextRID returns next RID in current cluster 107 | func (rid RID) NextRID() RID { 108 | rid.checkClusterLimits() 109 | rid.ClusterPos++ // uses local copy of rid 110 | return rid 111 | } 112 | 113 | func (rid *RID) FromStream(r io.Reader) error { 114 | buf := make([]byte, ridSerializedSize) 115 | if err := rw.NewReader(r).ReadRawBytes(buf); err != nil { 116 | return err 117 | } 118 | rid.ClusterID = int16(rw.Order.Uint16(buf)) 119 | rid.ClusterPos = int64(rw.Order.Uint64(buf[rw.SizeShort:])) 120 | return nil 121 | } 122 | 123 | func (rid RID) ToStream(w io.Writer) error { 124 | bw := rw.NewWriter(w) 125 | bw.WriteShort(rid.ClusterID) 126 | bw.WriteLong(rid.ClusterPos) 127 | return bw.Err() 128 | } 129 | 130 | // ParseRID converts a string of form #N:M or N:M to a RID. 131 | func ParseRID(s string) (RID, error) { 132 | s = strings.TrimSpace(s) 133 | if s == "" { 134 | return NewEmptyRID(), nil 135 | } else if !strings.Contains(s, string(ridSeparator)) { 136 | return NewEmptyRID(), fmt.Errorf("Argument '%s' is not a RecordId in form of string. Format must be: :", s) 137 | } 138 | s = strings.TrimLeft(s, string(ridPrefix)) 139 | parts := strings.Split(s, string(ridSeparator)) 140 | if len(parts) != 2 { 141 | return NewEmptyRID(), fmt.Errorf("Argument received '%s' is not a RecordId in form of string. Format must be: #:. Example: #3:12", s) 142 | } 143 | id, err := strconv.ParseInt(parts[0], 10, 16) 144 | if err != nil { 145 | return NewEmptyRID(), fmt.Errorf("Invalid RID string to ParseRID: %s", s) 146 | } 147 | pos, err := strconv.ParseInt(parts[1], 10, 64) 148 | if err != nil { 149 | return NewEmptyRID(), fmt.Errorf("Invalid RID string to ParseRID: %s", s) 150 | } 151 | return NewRID(int16(id), int64(pos)), nil 152 | } 153 | 154 | // MustParseRID is a version of ParseRID which panics on errors 155 | func MustParseRID(s string) RID { 156 | rid, err := ParseRID(s) 157 | if err != nil { 158 | panic(err) 159 | } 160 | return rid 161 | } 162 | -------------------------------------------------------------------------------- /rid_test.go: -------------------------------------------------------------------------------- 1 | package orient_test 2 | 3 | import ( 4 | "bytes" 5 | "gopkg.in/istreamdata/orientgo.v2" 6 | "testing" 7 | ) 8 | 9 | func TestRIDString(t *testing.T) { 10 | if s := orient.NewRID(5, 12).String(); s != "#5:12" { 11 | t.Fatal("wrong RID generated: ", s) 12 | } 13 | } 14 | 15 | func TestRIDParse(t *testing.T) { 16 | if rid, err := orient.ParseRID(" #5:12 "); err != nil { 17 | t.Fatal(err) 18 | } else if rid != (orient.RID{ClusterID: 5, ClusterPos: 12}) { 19 | t.Fatal("wrong RID parsed: ", rid) 20 | } 21 | if rid, err := orient.ParseRID(" 5:12 "); err != nil { 22 | t.Fatal(err) 23 | } else if rid != (orient.RID{ClusterID: 5, ClusterPos: 12}) { 24 | t.Fatal("wrong RID parsed: ", rid) 25 | } 26 | } 27 | 28 | func TestRIDNext(t *testing.T) { 29 | rid1 := orient.RID{ClusterID: 5, ClusterPos: 12} 30 | rid2 := orient.RID{ClusterID: 5, ClusterPos: 12} 31 | rid3 := rid2.NextRID() 32 | if rid3 == rid2 { 33 | t.Fatal("RID is the same after Next") 34 | } else if rid1 != rid2 { 35 | t.Fatal("source RID is changed after Next") 36 | } else if rid2.ClusterID != rid3.ClusterID { 37 | t.Fatal("RID ClusterId is changed after Next") 38 | } else if rid2.ClusterPos+1 != rid3.ClusterPos { 39 | t.Fatal("next RID ClusterPos is wrong") 40 | } 41 | } 42 | 43 | func testRIDSerialize(t *testing.T, s string) { 44 | buf := bytes.NewBuffer(nil) 45 | if err := orient.MustParseRID(s).ToStream(buf); err != nil { 46 | t.Fatal(err) 47 | } else if buf.Len() != 10 { 48 | t.Fatalf("wrong serialized size: %d vs %d", buf.Len(), 10) 49 | } 50 | rid := orient.NewEmptyRID() 51 | if err := rid.FromStream(buf); err != nil { 52 | t.Fatal(err) 53 | } else if rid.String() != s { 54 | t.Fatalf("different RIDs: %v vs %v", s, rid) 55 | } 56 | } 57 | 58 | func TestRIDSerialize(t *testing.T) { 59 | testRIDSerialize(t, "#12:2556") 60 | testRIDSerialize(t, "#-1:-2") 61 | } 62 | -------------------------------------------------------------------------------- /serializer.go: -------------------------------------------------------------------------------- 1 | package orient 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "gopkg.in/istreamdata/orientgo.v2/obinary/rw" 7 | "io" 8 | ) 9 | 10 | // ErrTypeSerialization represent serialization/deserialization error 11 | type ErrTypeSerialization struct { 12 | Val interface{} 13 | Serializer interface{} 14 | } 15 | 16 | func (e ErrTypeSerialization) Error() string { 17 | return fmt.Sprintf("Serializer (%T)%v has no support for type %T", e.Serializer, e.Serializer, e.Val) 18 | } 19 | 20 | // CustomSerializable is an interface for objects that can be sent on wire 21 | type CustomSerializable interface { 22 | Classer 23 | Serializable 24 | } 25 | 26 | // Classer is an interface for object that have analogs in OrientDB Java code 27 | type Classer interface { 28 | // GetClassName return a Java class name for an object 29 | GetClassName() string 30 | } 31 | 32 | var ( 33 | recordFormats = map[string]func() RecordSerializer{ 34 | binaryFormatName: func() RecordSerializer { return &BinaryRecordFormat{} }, 35 | } 36 | recordFormatDefault = binaryFormatName 37 | ) 38 | 39 | // Serializable is an interface for objects that can be serialized to stream 40 | type Serializable interface { 41 | ToStream(w io.Writer) error 42 | } 43 | 44 | // Deserializable is an interface for objects that can be deserialized from stream 45 | type Deserializable interface { 46 | FromStream(r io.Reader) error 47 | } 48 | 49 | // GlobalPropertyFunc is a function for getting global properties by id 50 | type GlobalPropertyFunc func(id int) (OGlobalProperty, bool) 51 | 52 | // RecordSerializer is an interface for serializing records to byte streams 53 | type RecordSerializer interface { 54 | // String, in case of RecordSerializer must return it's class name, as it will be sent to server 55 | String() string 56 | 57 | ToStream(w io.Writer, rec ORecord) error 58 | FromStream(data []byte) (ORecord, error) 59 | 60 | SetGlobalPropertyFunc(fnc GlobalPropertyFunc) 61 | } 62 | 63 | // RegisterRecordFormat registers RecordSerializer with a given class name 64 | func RegisterRecordFormat(name string, fnc func() RecordSerializer) { 65 | recordFormats[name] = fnc 66 | } 67 | 68 | // SetDefaultRecordFormat sets default record serializer 69 | func SetDefaultRecordFormat(name string) { 70 | recordFormatDefault = name 71 | } 72 | 73 | // GetRecordFormat returns record serializer by class name 74 | func GetRecordFormat(name string) RecordSerializer { 75 | f := recordFormats[name] 76 | if f == nil { 77 | panic(fmt.Errorf("unknown record format: %s", name)) 78 | } 79 | return f() 80 | } 81 | 82 | // GetDefaultRecordSerializer returns default record serializer 83 | func GetDefaultRecordSerializer() RecordSerializer { 84 | return GetRecordFormat(recordFormatDefault) 85 | } 86 | 87 | // DocumentSerializable is an interface for objects that can be converted to Document 88 | type DocumentSerializable interface { 89 | ToDocument() (*Document, error) 90 | } 91 | 92 | // DocumentDeserializable is an interface for objects that can be filled from Document 93 | type DocumentDeserializable interface { 94 | FromDocument(*Document) error 95 | } 96 | 97 | var _ MapSerializable = (*Document)(nil) 98 | 99 | // MapSerializable is an interface for objects that can be converted to map[string]interface{} 100 | type MapSerializable interface { 101 | ToMap() (map[string]interface{}, error) 102 | } 103 | 104 | // SerializeAnyStreamable serializes a given object 105 | func SerializeAnyStreamable(o CustomSerializable) ([]byte, error) { 106 | buf := bytes.NewBuffer(nil) 107 | bw := rw.NewWriter(buf) 108 | bw.WriteString(o.GetClassName()) 109 | if err := o.ToStream(bw); err != nil { 110 | return nil, err 111 | } 112 | if err := bw.Err(); err != nil { 113 | return nil, err 114 | } 115 | return buf.Bytes(), nil 116 | } 117 | -------------------------------------------------------------------------------- /serializer_binary_decimal.go: -------------------------------------------------------------------------------- 1 | package orient 2 | 3 | import ( 4 | "gopkg.in/istreamdata/orientgo.v2/obinary/rw" 5 | "math/big" 6 | ) 7 | 8 | func (f binaryRecordFormatV0) readDecimal(r *rw.ReadSeeker) interface{} { 9 | scale := int(r.ReadInt()) 10 | value := big.NewInt(0).SetBytes(r.ReadBytes()) 11 | return Decimal{ 12 | Scale: scale, 13 | Value: value, 14 | } 15 | } 16 | 17 | func (f binaryRecordFormatV0) writeDecimal(w *rw.Writer, o interface{}) { 18 | var d Decimal 19 | switch v := o.(type) { 20 | case int64: 21 | d = Decimal{Value: big.NewInt(v)} 22 | case *big.Int: 23 | d = Decimal{Value: v} 24 | case Decimal: 25 | d = v 26 | default: 27 | panic(ErrTypeSerialization{Val: o, Serializer: f}) 28 | } 29 | w.WriteInt(int32(d.Scale)) // scale value, 0 for ints 30 | w.WriteBytes(d.Value.Bytes()) // unscaled value 31 | } 32 | -------------------------------------------------------------------------------- /serializer_binary_test.go: -------------------------------------------------------------------------------- 1 | package orient 2 | 3 | import ( 4 | "bytes" 5 | "encoding/base64" 6 | "math/big" 7 | "reflect" 8 | "testing" 9 | "time" 10 | 11 | "gopkg.in/istreamdata/orientgo.v2/obinary/rw" 12 | ) 13 | 14 | func TestDeserializeRecordData(t *testing.T) { 15 | data, err := base64.StdEncoding.DecodeString(`AAASY2FyZXRha2VyAAAAJQcIbmFtZQAAAC0HBmFnZQAAADMBAA5NaWNoYWVsCkxpbnVzHg==`) 16 | if err != nil { 17 | t.Fatal(err) 18 | } 19 | 20 | rec := NewEmptyDocument() 21 | rec.SetSerializer(&BinaryRecordFormat{}) 22 | rec.Fill(NewEmptyRID(), 0, data) 23 | 24 | if doc, err := rec.ToDocument(); err != nil { 25 | t.Fatal(err) 26 | } else if len(doc.Fields()) != 3 { 27 | t.Fatal("wrong fields count in document") 28 | } else if doc.GetField("caretaker").Value.(string) != "Michael" || 29 | doc.GetField("name").Value.(string) != "Linus" || 30 | doc.GetField("age").Value.(int32) != 15 { 31 | t.Fatal("wrong values in document: ", doc) 32 | } 33 | } 34 | 35 | func testBase64Compare(t *testing.T, out []byte, origBase64 string) { 36 | orig, _ := base64.StdEncoding.DecodeString(origBase64) 37 | if bytes.Compare(out, orig) != 0 { 38 | t.Fatalf("different buffers:\n%v\n%v\n", out, orig) 39 | } 40 | } 41 | 42 | func TestSerializeCommandNoParams(t *testing.T) { 43 | query := "SELECT FROM V WHERE Id = ?" 44 | buf := bytes.NewBuffer(nil) 45 | if err := NewSQLCommand(query).ToStream(buf); err != nil { 46 | t.Fatal(err) 47 | } 48 | testBase64Compare(t, buf.Bytes(), "AAAAGlNFTEVDVCBGUk9NIFYgV0hFUkUgSWQgPSA/AAA=") 49 | } 50 | 51 | func TestSerializeCommandIntParam(t *testing.T) { 52 | query := "SELECT FROM V WHERE Id = ?" 53 | buf := bytes.NewBuffer(nil) 54 | if err := NewSQLCommand(query, int32(25)).ToStream(buf); err != nil { 55 | t.Fatal(err) 56 | } 57 | testBase64Compare(t, buf.Bytes(), "AAAAGlNFTEVDVCBGUk9NIFYgV0hFUkUgSWQgPSA/AQAAAB0AABRwYXJhbWV0ZXJzAAAAEwwAAgcCMAAAABwBMgA=") 58 | } 59 | 60 | func testSerializeEmbMap(t *testing.T, off int, mp interface{}, origBase64 string) { 61 | buf := bytes.NewBuffer(nil) 62 | for i := 0; i < off; i++ { 63 | buf.WriteByte(0) 64 | } 65 | if err := (binaryRecordFormatV0{}).writeEmbeddedMap(rw.NewWriter(buf), off, mp); err != nil { 66 | t.Fatal(err) 67 | } 68 | testBase64Compare(t, buf.Bytes(), origBase64) 69 | r := rw.NewReadSeeker(bytes.NewReader(buf.Bytes())) 70 | out, err := (binaryRecordFormatV0{}).readEmbeddedMap(r, nil) 71 | if err != nil { 72 | t.Fatal(err) 73 | } 74 | if reflect.TypeOf(out) != reflect.TypeOf(mp) { 75 | t.Logf("types are not the same: %T -> %T", mp, out) 76 | } 77 | } 78 | 79 | func TestSerializeEmbeddedMapInt32V0(t *testing.T) { 80 | testSerializeEmbMap(t, 0, 81 | map[int32]interface{}{int32(0): int32(25)}, 82 | "AgcCMAAAAAkBMg==", 83 | ) 84 | } 85 | 86 | func TestSerializeEmbeddedMapIntV0(t *testing.T) { 87 | testSerializeEmbMap(t, 0, 88 | map[int]interface{}{0: 25}, 89 | "AgcCMAAAAAkDMg==", 90 | ) 91 | } 92 | 93 | func TestSerializeEmbeddedMapIntOffsV0(t *testing.T) { 94 | testSerializeEmbMap(t, 4, 95 | map[int]interface{}{0: 25}, 96 | "AAAAAAIHAjAAAAANAzI=", 97 | ) 98 | } 99 | 100 | func TestSerializeEmbeddedMapStringV0(t *testing.T) { 101 | testSerializeEmbMap(t, 0, 102 | map[string]interface{}{"one": "two"}, 103 | "AgcGb25lAAAACwcGdHdv", 104 | ) 105 | } 106 | 107 | func TestSerializeEmbeddedMapEmptyV0(t *testing.T) { 108 | testSerializeEmbMap(t, 0, 109 | map[string]interface{}{}, 110 | "AA==", 111 | ) 112 | } 113 | 114 | func testSerializeEmbCol(t *testing.T, off int, col interface{}, origBase64 string) { 115 | buf := bytes.NewBuffer(nil) 116 | for i := 0; i < off; i++ { 117 | buf.WriteByte(0) 118 | } 119 | if err := (binaryRecordFormatV0{}).writeEmbeddedCollection(rw.NewWriter(buf), off, col, UNKNOWN); err != nil { 120 | t.Fatal(err) 121 | } 122 | testBase64Compare(t, buf.Bytes(), origBase64) 123 | } 124 | 125 | func TestSerializeEmbeddedColStringV0(t *testing.T) { 126 | testSerializeEmbCol(t, 0, 127 | []string{"a", "b", "c"}, 128 | "BhcHAmEHAmIHAmM=", 129 | ) 130 | } 131 | 132 | func TestSerializeEmbeddedColStringOffsV0(t *testing.T) { 133 | testSerializeEmbCol(t, 4, 134 | []string{"a", "b", "c"}, 135 | "AAAAAAYXBwJhBwJiBwJj", 136 | ) 137 | } 138 | 139 | func testSerializeDoc(t *testing.T, doc *Document, origBase64 string) { 140 | buf := bytes.NewBuffer(nil) 141 | GetDefaultRecordSerializer().ToStream(buf, doc) 142 | testBase64Compare(t, buf.Bytes(), origBase64) 143 | } 144 | 145 | func TestSerializeDocumentEmpty(t *testing.T) { 146 | doc := NewEmptyDocument() 147 | doc.SetField("parameters", map[string]interface{}{}) 148 | testSerializeDoc(t, 149 | doc, 150 | "AAAUcGFyYW1ldGVycwAAABMMAAA=", 151 | ) 152 | } 153 | 154 | func TestSerializeDocumentFieldStringMap(t *testing.T) { 155 | doc := NewEmptyDocument() 156 | doc.SetField("parameters", map[string]string{"one": "two"}) 157 | testSerializeDoc(t, 158 | doc, 159 | "AAAUcGFyYW1ldGVycwAAABMMAAIHBm9uZQAAAB4HBnR3bw==", 160 | ) 161 | } 162 | 163 | func TestSerializeDocumentFieldMapAndArr(t *testing.T) { 164 | doc := NewEmptyDocument() 165 | doc.SetField("map", map[string]string{"one": "two"}) 166 | doc.SetField("arr", []string{"a", "b", "c"}) 167 | testSerializeDoc(t, 168 | doc, 169 | "AAAGbWFwAAAAFQwGYXJyAAAAJAoAAgcGb25lAAAAIAcGdHdvBhcHAmEHAmIHAmM=", 170 | ) 171 | } 172 | 173 | func TestSerializeDecimalV0(t *testing.T) { 174 | buf := bytes.NewBuffer(nil) 175 | val := big.NewInt(123456789) 176 | if err := (binaryRecordFormatV0{}).writeSingleValue(rw.NewWriter(buf), 0, val, DECIMAL, UNKNOWN); err != nil { 177 | t.Fatal(err) 178 | } 179 | testBase64Compare(t, buf.Bytes(), "AAAAAAAAAAQHW80V") 180 | 181 | r := rw.NewReadSeeker(bytes.NewReader(buf.Bytes())) 182 | out, err := (binaryRecordFormatV0{}).readSingleValue(r, DECIMAL, nil) 183 | if err != nil { 184 | t.Fatal(err) 185 | } 186 | if val2, ok := out.(Decimal); !ok { 187 | t.Fatalf("expected Decimal, got: %T", out) 188 | } else if val.Cmp(val2.Value) != 0 { 189 | t.Fatalf("values differs: %v != %v", val, val2) 190 | } 191 | } 192 | 193 | func TestSerializeDatetimeV0(t *testing.T) { 194 | buf := bytes.NewBuffer(nil) 195 | val := time.Now() 196 | val = time.Unix(val.Unix(), int64(val.Nanosecond()/1e6)*1e6) // precise to milliseconds 197 | if err := (binaryRecordFormatV0{}).writeSingleValue(rw.NewWriter(buf), 0, val, DATETIME, UNKNOWN); err != nil { 198 | t.Fatal(err) 199 | } 200 | 201 | r := rw.NewReadSeeker(bytes.NewReader(buf.Bytes())) 202 | out, err := (binaryRecordFormatV0{}).readSingleValue(r, DATETIME, nil) 203 | if err != nil { 204 | t.Fatal(err) 205 | } 206 | if val2, ok := out.(time.Time); !ok { 207 | t.Fatalf("expected Time, got: %T", out) 208 | } else if !val.Equal(val2) { 209 | t.Fatalf("values differs: %v != %v", val, val2) 210 | } 211 | } 212 | 213 | func testDocumentToStruct(t *testing.T, dataBase64 string) { 214 | data, err := base64.StdEncoding.DecodeString(dataBase64) 215 | if err != nil { 216 | t.Fatal(err) 217 | } 218 | doc := NewEmptyDocument() 219 | err = doc.Fill(NewEmptyRID(), 0, data) 220 | if err != nil { 221 | t.Fatal(err) 222 | } 223 | 224 | type Inner struct { 225 | Name string 226 | } 227 | type Item struct { 228 | One Inner 229 | Inner []Inner 230 | } 231 | 232 | one, two := Inner{Name: "one"}, Inner{Name: "two"} 233 | 234 | var obj *Item 235 | if err = doc.ToStruct(&obj); err != nil { 236 | t.Fatal(err) 237 | } else if obj.One != one { 238 | t.Fatal("item is wrong") 239 | } else if len(obj.Inner) != 2 || obj.Inner[0] != one || obj.Inner[1] != two { 240 | t.Fatal("list is wrong") 241 | } 242 | } 243 | 244 | func TestDocumentInnerStruct(t *testing.T) { 245 | testDocumentToStruct(t, "AAJWBk9uZQAAABgJCklubmVyAAAAKAoAAAhOYW1lAAAAJAcABm9uZQQXCQAITmFtZQAAADcHAAZvbmUJAAhOYW1lAAAASAcABnR3bw==") 246 | } 247 | 248 | func TestDocumentInnerMapToStruct(t *testing.T) { 249 | testDocumentToStruct(t, "AAJWBk9uZQAAABgMCklubmVyAAAAKAoAAgcITmFtZQAAACQHBm9uZQQXDAIHCE5hbWUAAAA3BwZvbmUMAgcITmFtZQAAAEgHBnR3bw==") 250 | } 251 | -------------------------------------------------------------------------------- /serializer_string.go: -------------------------------------------------------------------------------- 1 | package orient 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | "strconv" 7 | "strings" 8 | ) 9 | 10 | const ( 11 | string_LINK = '#' 12 | string_EMBEDDED_BEGIN = '(' 13 | string_EMBEDDED_END = ')' 14 | string_LIST_BEGIN = '[' 15 | string_LIST_END = ']' 16 | string_SET_BEGIN = '<' 17 | string_SET_END = '>' 18 | string_MAP_BEGIN = '{' 19 | string_MAP_END = '}' 20 | string_BAG_BEGIN = '%' 21 | string_BAG_END = ';' 22 | string_BINARY_BEGINEND = '_' 23 | string_CUSTOM_TYPE = '^' 24 | string_ENTRY_SEPARATOR = ':' 25 | string_PARAMETER_NAMED = ':' 26 | string_PARAMETER_POSITIONAL = '?' 27 | 28 | string_DECIMAL_SEPARATOR = '.' 29 | ) 30 | 31 | var ( 32 | string_MaxInt = strconv.Itoa(math.MaxInt32) 33 | ) 34 | 35 | type StringRecordFormatAbs struct{} 36 | 37 | func (StringRecordFormatAbs) GetType(s string) OType { 38 | if s == "" { 39 | return UNKNOWN 40 | } 41 | rs := []rune(s) 42 | firstChar := rs[0] 43 | switch firstChar { 44 | case string_LINK: // RID 45 | return LINK 46 | case '\'', '"': 47 | return STRING 48 | case string_BINARY_BEGINEND: 49 | return BINARY 50 | case string_EMBEDDED_BEGIN: 51 | return EMBEDDED 52 | case string_LIST_BEGIN: 53 | return EMBEDDEDLIST 54 | case string_SET_BEGIN: 55 | return EMBEDDEDSET 56 | case string_MAP_BEGIN: 57 | return EMBEDDEDMAP 58 | case string_CUSTOM_TYPE: 59 | return CUSTOM 60 | } 61 | 62 | // BOOLEAN? 63 | if ls := strings.ToLower(s); ls == "true" || ls == "false" { 64 | return BOOLEAN 65 | } 66 | 67 | // NUMBER OR STRING? 68 | integer := true 69 | for i, c := range rs { 70 | if c >= '0' && c <= '9' { 71 | continue 72 | } else if i == 0 && (c == '+' || c == '0') { 73 | continue 74 | } else if c == string_DECIMAL_SEPARATOR { 75 | integer = false // maybe float, seek for other string char to be sure 76 | } else { 77 | if i == 0 { 78 | return STRING 79 | } 80 | if !integer && (c == 'E' || c == 'e') { 81 | // CHECK FOR SCIENTIFIC NOTATION 82 | if i+1 < len(rs) { 83 | if rs[i+1] == '-' { 84 | // JUMP THE DASH IF ANY (NOT MANDATORY) 85 | i++ 86 | } 87 | continue 88 | } 89 | } else { 90 | switch c { 91 | case 'f': 92 | return FLOAT 93 | case 'c': 94 | return DECIMAL 95 | case 'l': 96 | return LONG 97 | case 'd': 98 | return DOUBLE 99 | case 'b': 100 | return BYTE 101 | case 'a': 102 | return DATE 103 | case 't': 104 | return DATETIME 105 | case 's': 106 | return SHORT 107 | } 108 | } 109 | return STRING 110 | } 111 | } 112 | 113 | if integer { 114 | // AUTO CONVERT TO LONG IF THE INTEGER IS TOO BIG 115 | if n, mn := len(rs), len(string_MaxInt); n > mn || (n == mn && s > string_MaxInt) { 116 | return LONG 117 | } 118 | return INTEGER 119 | } 120 | 121 | if _, err := strconv.ParseFloat(s, 32); err == nil { 122 | return FLOAT 123 | } else if _, err = strconv.ParseFloat(s, 64); err == nil { 124 | return DOUBLE 125 | } else { 126 | return DECIMAL 127 | } 128 | } 129 | func (f StringRecordFormatAbs) FieldTypeFromStream(tp OType, s string) interface{} { 130 | if s == "" { 131 | return nil 132 | } else if tp == UNKNOWN { 133 | tp = EMBEDDED 134 | } 135 | 136 | switch tp { 137 | case STRING: 138 | return s // TODO: implement in a right way 139 | case INTEGER: 140 | v, err := strconv.ParseInt(s, 10, 32) 141 | if err != nil { 142 | panic(err) 143 | } 144 | return int32(v) 145 | case LONG: 146 | v, err := strconv.ParseInt(strings.TrimSuffix(s, "l"), 10, 64) 147 | if err != nil { 148 | panic(err) 149 | } 150 | return int64(v) 151 | case BOOLEAN: 152 | switch strings.ToLower(s) { 153 | case "true": 154 | return true 155 | case "false": 156 | return false 157 | default: 158 | panic(fmt.Errorf("unknown val for bool: '%s'", s)) 159 | } 160 | default: // TODO: more types 161 | panic(fmt.Errorf("unsupported type for stringRecordFormatAbs: %s", tp)) 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /sql.go: -------------------------------------------------------------------------------- 1 | package orient 2 | 3 | /* 4 | import ( 5 | "database/sql" 6 | "database/sql/driver" 7 | "fmt" 8 | "io" 9 | "net" 10 | "regexp" 11 | "runtime" 12 | "strings" 13 | "time" 14 | ) 15 | 16 | // Driver name for database/sql package 17 | const DriverNameSQL = "orient" 18 | 19 | var ( 20 | _ driver.Driver = (*orientDriver)(nil) 21 | _ driver.Conn = (*Database)(nil) 22 | _ driver.Execer = (*Database)(nil) 23 | _ driver.Queryer = (*Database)(nil) 24 | ) 25 | 26 | var dsnRx = regexp.MustCompile(`([^@]+)@([^:]+):([^/]+)/(.+)`) 27 | 28 | func init() { 29 | sql.Register(DriverNameSQL, &orientDriver{}) 30 | } 31 | 32 | // DialDSN returns a new connection to the database. 33 | // The dsn (driver-specific name) is a string in a driver-specific format. 34 | // For orientgo, the dsn should be of the format: 35 | // user@pass:host:port/db 36 | // or 37 | // user@pass:host/db (default port of 2424 is used) 38 | // 39 | // Function is also used for database/sql driver: 40 | // db, err := sql.Open("orient", "admin@admin:127.0.0.1/db") 41 | func DialDSN(dsn string) (*Database, error) { 42 | user, pass, host, port, dbname, err := parseDsn(dsn) 43 | if port == "" { 44 | port = "2424" 45 | } 46 | if err != nil { 47 | return nil, err 48 | } 49 | dbc, err := Dial(net.JoinHostPort(host, port)) 50 | if err != nil { 51 | return nil, err 52 | } 53 | // TODO: right now assumes DocumentDB type - pass in on the dsn?? 54 | // NOTE: I tried a graphDB with DocumentDB type and it worked, so why is it necesary at all? 55 | // TODO: this maybe shouldn't happen in this method -> might do it lazily in Query/Exec methods? 56 | db, err := dbc.Open(dbname, DocumentDB, user, pass) 57 | if err != nil { 58 | dbc.Close() 59 | return nil, err 60 | } 61 | return db, nil 62 | } 63 | 64 | // Implements the Go sql/driver.Driver interface. 65 | type orientDriver struct{} 66 | 67 | // Open implements sql/driver.Driver interface 68 | // See DialDSN for more info. 69 | func (d *orientDriver) Open(dsn string) (driver.Conn, error) { 70 | glog.V(10).Infoln("OgonoriDriver#Open") 71 | return DialDSN(dsn) 72 | } 73 | 74 | func parseDsn(dsn string) (uname, passw, host, port, dbname string, err error) { 75 | matches := dsnRx.FindStringSubmatch(dsn) 76 | if matches == nil || len(matches) != 5 { 77 | return "", "", "", "", "", 78 | fmt.Errorf("Unable to parse connection string: %s. Must be of the format: %s", 79 | dsn, "uname@passw:ip-or-host/dbname") 80 | } 81 | toks := strings.Split(matches[3], ":") 82 | host = toks[0] 83 | if len(toks) > 1 { 84 | port = toks[1] 85 | } 86 | return matches[1], matches[2], host, port, matches[4], nil 87 | } 88 | 89 | // Prepare implements sql/driver.Conn interface 90 | func (db *Database) Prepare(query string) (driver.Stmt, error) { 91 | glog.V(10).Infoln("ogoConn.Prepare: ", query) 92 | return &ogonoriStmt{db: db, query: query}, nil 93 | } 94 | 95 | // Begin implements sql/driver.Conn interface 96 | func (db *Database) Begin() (driver.Tx, error) { 97 | glog.V(10).Infoln("ogoConn.Begin") 98 | return nil, fmt.Errorf("orientgo: transactions are not supported for now") 99 | } 100 | 101 | // Exec implements sql/driver.Execer interface 102 | func (db *Database) Exec(cmd string, args []driver.Value) (driver.Result, error) { 103 | glog.V(10).Infoln("ogoConn.Exec") 104 | if db == nil { 105 | return nil, ErrInvalidConn{Msg: "db not initialized in ogonoriConn#Exec"} 106 | } 107 | var o interface{} 108 | err := db.Command(NewSQLCommand(cmd, driverArgs(args)...)).All(&o) 109 | if err != nil { 110 | return ogonoriResult{-1, -1}, err 111 | } 112 | 113 | switch rec := o.(type) { 114 | case int32: 115 | return ogonoriResult{int64(rec), -1}, nil 116 | case int64: 117 | return ogonoriResult{int64(rec), -1}, nil 118 | case int: 119 | return ogonoriResult{int64(rec), -1}, nil 120 | case []OIdentifiable: 121 | return ogonoriResult{int64(len(rec)), rec[len(rec)-1].GetIdentity().ClusterPos}, nil 122 | case OIdentifiable: 123 | return ogonoriResult{1, rec.GetIdentity().ClusterPos}, err 124 | } 125 | return nil, fmt.Errorf("exec with return values is not supported for now, out type: %T", o) 126 | } 127 | 128 | // Query implements sql/driver.Queryer interface 129 | func (db *Database) Query(query string, args []driver.Value) (driver.Rows, error) { 130 | glog.V(10).Infoln("ogoConn.Query") 131 | if db == nil { 132 | return nil, ErrInvalidConn{Msg: "db not initialized in ogonoriConn#Exec"} 133 | } 134 | var o interface{} 135 | err := db.Command(NewSQLQuery(query, driverArgs(args)...)).All(&o) 136 | if err != nil { 137 | return nil, err 138 | } 139 | switch rec := o.(type) { 140 | case []OIdentifiable: 141 | return newRows(rec) 142 | } 143 | return nil, fmt.Errorf("query with return values is not supported for now, out type: %T", o) 144 | } 145 | 146 | func driverArgs(args []driver.Value) []interface{} { 147 | out := make([]interface{}, len(args)) 148 | for i, val := range args { 149 | glog.V(10).Infof("valarg: %T: %v; isValue=%v\n", val, val, driver.IsValue(val)) // DEBUG 150 | switch val.(type) { 151 | case string, int64, float64, bool, []byte, time.Time: 152 | out[i] = val 153 | default: 154 | _, file, line, _ := runtime.Caller(0) 155 | glog.Warningf("Unexpected type in ogonoriConn#Exec: %T. (%s:%d)", val, file, line) 156 | } 157 | } 158 | return out 159 | } 160 | 161 | var _ driver.Result = (*ogonoriResult)(nil) 162 | 163 | // ogonoriResult implements the sql/driver.Result inteface. 164 | type ogonoriResult struct { 165 | affectedRows int64 166 | insertId int64 167 | } 168 | 169 | // LastInsertId returns the database's auto-generated ID after, 170 | // for example, an INSERT into a table with primary key. 171 | func (res ogonoriResult) LastInsertId() (int64, error) { 172 | return res.insertId, nil 173 | } 174 | 175 | // RowsAffected returns the number of rows affected by the query. 176 | func (res ogonoriResult) RowsAffected() (int64, error) { 177 | return res.affectedRows, nil 178 | } 179 | 180 | var _ driver.Rows = (*ogonoriRows)(nil) 181 | 182 | // ogonoriRows implements the sql/driver.Rows interface. 183 | type ogonoriRows struct { 184 | pos int // index of next row (doc) to return 185 | docs []OIdentifiable 186 | cols []string 187 | fulldoc bool // whether query returned a full document or just properties 188 | // TODO: maybe a reference to the appropriate schema is needed here? 189 | } 190 | 191 | func newRows(docs []OIdentifiable) (*ogonoriRows, error) { 192 | var cols []string 193 | if docs == nil || len(docs) == 0 { 194 | cols = []string{} 195 | return &ogonoriRows{docs: docs, cols: cols}, nil 196 | } 197 | 198 | for i := range docs { 199 | if rdoc, ok := docs[i].(*Document); ok { 200 | doc, err := rdoc.ToDocument() 201 | if err != nil { 202 | return nil, err 203 | } 204 | docs[i] = doc 205 | } else { 206 | return nil, fmt.Errorf("rows type: %T", docs[i]) 207 | } 208 | } 209 | 210 | var fulldoc bool 211 | if doc, ok := docs[0].(*Document); ok { 212 | if doc.classname == "" { 213 | cols = make([]string, 0, len(doc.FieldNames())) 214 | for _, fname := range doc.FieldNames() { 215 | cols = append(cols, fname) 216 | } 217 | } else { 218 | fulldoc = true 219 | // if Classname is set then the user queried for a full document 220 | // not individual properties of a Document/Class 221 | cols = []string{doc.classname} 222 | } 223 | } 224 | return &ogonoriRows{docs: docs, cols: cols, fulldoc: fulldoc}, nil 225 | } 226 | 227 | // Columns returns the names of the columns. The number of 228 | // columns of the result is inferred from the length of the 229 | // slice. If a particular column name isn't known, an empty 230 | // string should be returned for that entry. 231 | func (rows *ogonoriRows) Columns() []string { 232 | glog.V(10).Infof("ogonoriRows.Columns = %v\n", rows.cols) 233 | return rows.cols 234 | } 235 | 236 | // Next is called to populate the next row of data into 237 | // the provided slice. The provided slice will be the same 238 | // size as the Columns() are wide. 239 | // 240 | // The dest slice may be populated only with 241 | // a driver Value type, but excluding string. 242 | // All string values must be converted to []byte. 243 | // 244 | // Next should return io.EOF when there are no more rows. 245 | func (rows *ogonoriRows) Next(dest []driver.Value) error { 246 | glog.V(10).Infoln("ogonoriRows.Next") 247 | // TODO: right now I return the entire resultSet as an array, thus all loaded into memory 248 | // it would be better to have obinary.dbCommands provide an iterator based model 249 | // that only needs to read a "row" (Document) at a time 250 | if rows.pos >= len(rows.docs) { 251 | return io.EOF 252 | } 253 | // TODO: may need to do a type switch here -> what else can come in from a query in OrientDB 254 | // besides an Document ?? 255 | currdoc := rows.docs[rows.pos] 256 | if doc, ok := currdoc.(*Document); ok { 257 | if rows.fulldoc { 258 | dest[0] = doc 259 | } else { 260 | // was a property only query 261 | for i := range dest { 262 | // TODO: need to check field.Type and see if it is one that can map to Value 263 | // what will I do for types that don't map to Value (e.g., EmbeddedRecord, EmbeddedMap) ?? 264 | field := doc.GetField(rows.cols[i]) 265 | if field != nil { 266 | dest[i] = field.Value 267 | } 268 | } 269 | } 270 | } else { 271 | return fmt.Errorf("next on %T", currdoc) 272 | } 273 | 274 | rows.pos++ 275 | // TODO: this is where we need to return any errors that occur 276 | return nil 277 | } 278 | 279 | // Close closes the rows iterator. 280 | func (rows *ogonoriRows) Close() error { 281 | glog.V(10).Infoln("ogonoriRows.Close") 282 | return nil 283 | } 284 | 285 | var _ driver.Stmt = (*ogonoriStmt)(nil) 286 | 287 | // ogonoriStmt implements the Go sql/driver.Stmt interface. 288 | type ogonoriStmt struct { 289 | // TODO: review this - this is how the mysql driver does it 290 | db *Database 291 | query string // the SQL query/cmd specified by the user 292 | } 293 | 294 | // NumInput returns the number of placeholder parameters. 295 | func (st *ogonoriStmt) NumInput() int { 296 | glog.V(10).Infoln("ogonoriStmt.NumInput") 297 | return -1 // -1 means sql package will not "sanity check" arg counts 298 | } 299 | 300 | // Exec executes a query that doesn't return rows, such as an INSERT or UPDATE. 301 | func (st *ogonoriStmt) Exec(args []driver.Value) (driver.Result, error) { 302 | glog.V(10).Infoln("ogonoriStmt.Exec") 303 | if st.db == nil { 304 | return nil, ErrInvalidConn{Msg: "obinary.DBClient not initialized in ogonoriStmt#Exec"} 305 | } 306 | return st.db.Exec(st.query, args) 307 | } 308 | 309 | // Query executes a query that may return rows, such as a SELECT. 310 | func (st *ogonoriStmt) Query(args []driver.Value) (driver.Rows, error) { 311 | glog.V(10).Infoln("ogonoriStmt.Query") 312 | if st.db == nil { 313 | return nil, ErrInvalidConn{Msg: "obinary.DBClient not initialized in ogonoriStmt#Query"} 314 | } 315 | return st.db.Query(st.query, args) 316 | } 317 | 318 | // Close closes the statement. 319 | // 320 | // As of Go 1.1, a Stmt will not be closed if it's in use by any queries. 321 | func (st *ogonoriStmt) Close() error { 322 | glog.V(10).Info("ogonoriStmt.Close") 323 | // nothing to do here since there is no special statement handle in OrientDB 324 | // that is referenced by a client driver 325 | return nil 326 | } 327 | */ 328 | -------------------------------------------------------------------------------- /sql_test.go: -------------------------------------------------------------------------------- 1 | package orient_test 2 | 3 | /* 4 | import ( 5 | "database/sql" 6 | "fmt" 7 | "testing" 8 | 9 | "gopkg.in/istreamdata/orientgo.v2" 10 | _ "gopkg.in/istreamdata/orientgo.v2/obinary" 11 | "strconv" 12 | ) 13 | 14 | func TestSQLDriver(t *testing.T) { 15 | addr, rm := SpinOrientServer(t) 16 | defer rm() 17 | defer catch() 18 | 19 | dsn := dbUser + `@` + dbPass + `:` + addr + `/` + dbName 20 | 21 | { 22 | ndb, err := orient.DialDSN(dsn) 23 | Nil(t, err) 24 | SeedDB(t, ndb) 25 | ndb.Close() 26 | } 27 | 28 | // ---[ OPEN ]--- 29 | db, err := sql.Open(orient.DriverNameSQL, dsn) 30 | Nil(t, err) 31 | defer db.Close() 32 | 33 | err = db.Ping() 34 | Nil(t, err) 35 | 36 | // ---[ DELETE #1 ]--- 37 | // should not delete any rows 38 | delcmd := "delete from Cat where name ='Jared'" 39 | res, err := db.Exec(delcmd) 40 | Nil(t, err) 41 | nrows, _ := res.RowsAffected() 42 | glog.Infof(">> RES num rows affected: %v\n", nrows) 43 | Equals(t, int64(0), nrows) 44 | 45 | // ---[ INSERT #1 ]--- 46 | // insert with no params 47 | insertSQL := "insert into Cat (name, age, caretaker) values('Jared', 11, 'The Subway Guy')" 48 | glog.Infoln(insertSQL, "=> 'Jared', 11, 'The Subway Guy'") 49 | res, err = db.Exec(insertSQL) 50 | Nil(t, err) 51 | 52 | nrows, _ = res.RowsAffected() 53 | glog.V(10).Infof("nrows: %v\n", nrows) 54 | lastID, _ := res.LastInsertId() 55 | glog.V(10).Infof("last insert id: %v\n", lastID) 56 | Equals(t, int64(1), nrows) 57 | True(t, lastID > int64(0), fmt.Sprintf("LastInsertId: %v", lastID)) 58 | 59 | // ---[ INSERT #2 ]--- 60 | // insert with no params 61 | insertSQL = "insert into Cat (name, age, caretaker) values(?, ?, ?)" 62 | glog.Infoln(insertSQL, "=> 'Filo', 4, 'Greek'") 63 | res, err = db.Exec(insertSQL, "Filo", 4, "Greek") 64 | Nil(t, err) 65 | nrows, _ = res.RowsAffected() 66 | glog.V(10).Infof("nrows: %v\n", nrows) 67 | lastID, _ = res.LastInsertId() 68 | glog.V(10).Infof("last insert id: %v\n", lastID) 69 | Equals(t, int64(1), nrows) 70 | True(t, lastID > int64(0), fmt.Sprintf("LastInsertId: %v", lastID)) 71 | 72 | // ---[ QUERY #1: QueryRow ]--- 73 | // it is safe to query properties -> not sure how to return docs yet 74 | querySQL := "select name, age from Cat where caretaker = 'Greek'" 75 | row := db.QueryRow(querySQL) 76 | 77 | var retname string 78 | var retage int64 79 | err = row.Scan(&retname, &retage) 80 | Nil(t, err) 81 | Equals(t, "Filo", retname) 82 | Equals(t, int64(4), retage) 83 | 84 | // ---[ QUERY #2: Query (multiple rows returned) ]--- 85 | 86 | querySQL = "select name, age, caretaker from Cat order by age" 87 | 88 | var rName, rCaretaker string 89 | var rAge int64 90 | 91 | names := make([]string, 0, 4) 92 | ctakers := make([]string, 0, 4) 93 | ages := make([]int64, 0, 4) 94 | rows, err := db.Query(querySQL) 95 | for rows.Next() { 96 | err = rows.Scan(&rName, &rAge, &rCaretaker) 97 | names = append(names, rName) 98 | ctakers = append(ctakers, rCaretaker) 99 | ages = append(ages, rAge) 100 | } 101 | err = rows.Err() 102 | Nil(t, err) 103 | 104 | Equals(t, 4, len(names)) 105 | Equals(t, 4, len(ctakers)) 106 | Equals(t, 4, len(ages)) 107 | 108 | Equals(t, []string{"Filo", "Keiko", "Jared", "Linus"}, names) 109 | Equals(t, []string{"Greek", "Anna", "The Subway Guy", "Michael"}, ctakers) 110 | Equals(t, int64(4), ages[0]) 111 | Equals(t, int64(10), ages[1]) 112 | Equals(t, int64(11), ages[2]) 113 | Equals(t, int64(15), ages[3]) 114 | 115 | // ---[ QUERY #3: Same Query as above but change property order ]--- 116 | 117 | querySQL = "select age, caretaker, name from Cat order by age" 118 | 119 | names = make([]string, 0, 4) 120 | ctakers = make([]string, 0, 4) 121 | ages = make([]int64, 0, 4) 122 | rows, err = db.Query(querySQL) 123 | for rows.Next() { 124 | err = rows.Scan(&rAge, &rCaretaker, &rName) 125 | names = append(names, rName) 126 | ctakers = append(ctakers, rCaretaker) 127 | ages = append(ages, rAge) 128 | } 129 | err = rows.Err() 130 | Nil(t, err) 131 | 132 | Equals(t, 4, len(names)) 133 | Equals(t, 4, len(ctakers)) 134 | Equals(t, 4, len(ages)) 135 | 136 | Equals(t, []string{"Filo", "Keiko", "Jared", "Linus"}, names) 137 | Equals(t, []string{"Greek", "Anna", "The Subway Guy", "Michael"}, ctakers) 138 | Equals(t, int64(4), ages[0]) 139 | Equals(t, int64(10), ages[1]) 140 | Equals(t, int64(11), ages[2]) 141 | Equals(t, int64(15), ages[3]) 142 | 143 | // ---[ QUERY #4: Property query using parameterized SQL ]--- 144 | querySQL = "select caretaker, name, age from Cat where age >= ? order by age desc" 145 | 146 | names = make([]string, 0, 2) 147 | ctakers = make([]string, 0, 2) 148 | ages = make([]int64, 0, 2) 149 | 150 | rows, err = db.Query(querySQL, "11") 151 | for rows.Next() { 152 | err = rows.Scan(&rCaretaker, &rName, &rAge) 153 | names = append(names, rName) 154 | ctakers = append(ctakers, rCaretaker) 155 | ages = append(ages, rAge) 156 | } 157 | if err = rows.Err(); err != nil { 158 | t.Fatal(err) 159 | } 160 | 161 | Equals(t, 2, len(names)) 162 | Equals(t, "Linus", names[0]) 163 | Equals(t, "Jared", names[1]) 164 | 165 | Equals(t, 2, len(ctakers)) 166 | Equals(t, "Michael", ctakers[0]) 167 | Equals(t, "The Subway Guy", ctakers[1]) 168 | 169 | Equals(t, 2, len(ages)) 170 | Equals(t, int64(15), ages[0]) 171 | Equals(t, int64(11), ages[1]) 172 | 173 | // ---[ DELETE #2 ]--- 174 | res, err = db.Exec(delcmd) 175 | Nil(t, err) 176 | nrows, _ = res.RowsAffected() 177 | glog.Infof(">> DEL2 RES num rows affected: %v\n", nrows) 178 | Equals(t, int64(1), nrows) 179 | 180 | // ---[ DELETE #3 ]--- 181 | res, err = db.Exec(delcmd) 182 | Nil(t, err) 183 | nrows, _ = res.RowsAffected() 184 | glog.Infof(">> DEL3 RES num rows affected: %v\n", nrows) 185 | Equals(t, int64(0), nrows) 186 | 187 | // ---[ DELETE #4 ]--- 188 | delcmd = "delete from Cat where name <> 'Linus' AND name <> 'Keiko'" 189 | res, err = db.Exec(delcmd) 190 | Nil(t, err) 191 | nrows, _ = res.RowsAffected() 192 | glog.Infof(">> DEL4 RES num rows affected: %v\n", nrows) 193 | Equals(t, int64(1), nrows) 194 | 195 | // ---[ Full Document Queries with database/sql ]--- 196 | // ---[ QueryRow ]--- 197 | glog.Infoln(">>>>>>>>> QueryRow of full Document<<<<<<<<<<<") 198 | querySQL = "select from Cat where name = 'Linus'" 199 | 200 | row = db.QueryRow(querySQL) 201 | 202 | var retdoc *orient.Document 203 | err = row.Scan(&retdoc) 204 | Nil(t, err) 205 | Equals(t, "Cat", retdoc.ClassName()) 206 | Equals(t, 3, len(retdoc.FieldNames())) 207 | Equals(t, "Linus", retdoc.GetField("name").Value) 208 | Equals(t, int32(15), retdoc.GetField("age").Value) 209 | Equals(t, "Michael", retdoc.GetField("caretaker").Value) 210 | 211 | // ---[ Query (return multiple rows) ]--- 212 | querySQL = "select from Cat order by caretaker desc" 213 | rows, err = db.Query(querySQL) 214 | rowdocs := make([]*orient.Document, 0, 2) 215 | for rows.Next() { 216 | var newdoc orient.Document 217 | err = rows.Scan(&newdoc) 218 | rowdocs = append(rowdocs, &newdoc) 219 | } 220 | err = rows.Err() 221 | Nil(t, err) 222 | 223 | Equals(t, 2, len(rowdocs)) 224 | Equals(t, "Cat", rowdocs[0].ClassName()) 225 | Equals(t, "Linus", rowdocs[0].GetField("name").Value) 226 | Equals(t, "Keiko", rowdocs[1].GetField("name").Value) 227 | Equals(t, "Anna", rowdocs[1].GetField("caretaker").Value) 228 | } 229 | 230 | func TestSQLDriverPrepare(t *testing.T) { 231 | addr, rm := SpinOrientServer(t) 232 | defer rm() 233 | defer catch() 234 | 235 | dsn := dbUser + `@` + dbPass + `:` + addr + `/` + dbName 236 | 237 | { 238 | ndb, err := orient.DialDSN(dsn) 239 | Nil(t, err) 240 | SeedDB(t, ndb) 241 | ndb.Close() 242 | } 243 | 244 | // ---[ OPEN ]--- 245 | db, err := sql.Open(orient.DriverNameSQL, dsn) 246 | Nil(t, err) 247 | defer db.Close() 248 | 249 | querySQL := "select caretaker, name, age from Cat where age >= ? order by age desc" 250 | 251 | stmt, err := db.Prepare(querySQL) 252 | Nil(t, err) 253 | defer stmt.Close() 254 | 255 | names := make([]string, 0, 2) 256 | ctakers := make([]string, 0, 2) 257 | ages := make([]int64, 0, 2) 258 | 259 | var ( 260 | rCaretaker, rName string 261 | rAge int64 262 | ) 263 | 264 | // ---[ First use ]--- 265 | rows, err := stmt.Query("10") 266 | Nil(t, err) 267 | for rows.Next() { 268 | err = rows.Scan(&rCaretaker, &rName, &rAge) 269 | names = append(names, rName) 270 | ctakers = append(ctakers, rCaretaker) 271 | ages = append(ages, rAge) 272 | } 273 | if err = rows.Err(); err != nil { 274 | t.Fatal(err) 275 | } 276 | 277 | Equals(t, 2, len(names)) 278 | Equals(t, "Linus", names[0]) 279 | Equals(t, "Keiko", names[1]) 280 | 281 | Equals(t, 2, len(ctakers)) 282 | Equals(t, "Michael", ctakers[0]) 283 | Equals(t, "Anna", ctakers[1]) 284 | 285 | Equals(t, 2, len(ages)) 286 | Equals(t, int64(15), ages[0]) 287 | Equals(t, int64(10), ages[1]) 288 | 289 | // ---[ Second use ]--- 290 | rows, err = stmt.Query("14") 291 | 292 | names = make([]string, 0, 2) 293 | ctakers = make([]string, 0, 2) 294 | ages = make([]int64, 0, 2) 295 | 296 | for rows.Next() { 297 | err = rows.Scan(&rCaretaker, &rName, &rAge) 298 | names = append(names, rName) 299 | ctakers = append(ctakers, rCaretaker) 300 | ages = append(ages, rAge) 301 | } 302 | if err = rows.Err(); err != nil { 303 | t.Fatal(err) 304 | } 305 | 306 | Equals(t, 1, len(names)) 307 | Equals(t, "Linus", names[0]) 308 | Equals(t, int64(15), ages[0]) 309 | Equals(t, "Michael", ctakers[0]) 310 | 311 | // ---[ Third use ]--- 312 | rows, err = stmt.Query("100") 313 | 314 | names = make([]string, 0, 2) 315 | ctakers = make([]string, 0, 2) 316 | ages = make([]int64, 0, 2) 317 | 318 | if err = rows.Err(); err != nil { 319 | t.Fatal(err) 320 | } 321 | 322 | Equals(t, 0, len(names)) 323 | Equals(t, 0, len(ages)) 324 | Equals(t, 0, len(ctakers)) 325 | 326 | stmt.Close() 327 | 328 | // ---[ Now prepare Command, not query ]--- 329 | cmdStmt, err := db.Prepare("INSERT INTO Cat (age, caretaker, name) VALUES(?, ?, ?)") 330 | Nil(t, err) 331 | defer cmdStmt.Close() 332 | 333 | // use once 334 | result, err := cmdStmt.Exec(1, "Ralph", "Max") 335 | Nil(t, err) 336 | nrows, err := result.RowsAffected() 337 | Nil(t, err) 338 | Equals(t, 1, int(nrows)) 339 | insertID, err := result.LastInsertId() 340 | Nil(t, err) 341 | True(t, int(insertID) >= 0, "insertId was: "+strconv.Itoa(int(insertID))) 342 | 343 | // use again 344 | result, err = cmdStmt.Exec(2, "Jimmy", "John") 345 | Nil(t, err) 346 | nrows, err = result.RowsAffected() 347 | Nil(t, err) 348 | Equals(t, 1, int(nrows)) 349 | insertID2, err := result.LastInsertId() 350 | Nil(t, err) 351 | True(t, insertID != insertID2, "insertID was: "+strconv.Itoa(int(insertID))) 352 | 353 | row := db.QueryRow("select count(*) from Cat") 354 | var cnt int64 355 | err = row.Scan(&cnt) 356 | Nil(t, err) 357 | Equals(t, 4, int(cnt)) 358 | 359 | cmdStmt.Close() 360 | 361 | // ---[ Prepare DELETE command ]--- 362 | delStmt, err := db.Prepare("DELETE from Cat where name = ? OR caretaker = ?") 363 | Nil(t, err) 364 | defer delStmt.Close() 365 | result, err = delStmt.Exec("Max", "Jimmy") 366 | Nil(t, err) 367 | nrows, err = result.RowsAffected() 368 | Nil(t, err) 369 | Equals(t, 2, int(nrows)) 370 | insertID3, err := result.LastInsertId() 371 | Nil(t, err) 372 | True(t, int(insertID3) < 0, "should have negative insertId for a DELETE") 373 | 374 | } 375 | 376 | func TestSQlDriverGraph(t *testing.T) { 377 | addr, rm := SpinOrientServer(t) 378 | defer rm() 379 | defer catch() 380 | 381 | dsn := dbUser + `@` + dbPass + `:` + addr + `/` + dbName 382 | 383 | { 384 | ndb, err := orient.DialDSN(dsn) 385 | Nil(t, err) 386 | SeedDB(t, ndb) 387 | err = ndb.Command(orient.NewSQLCommand(`CREATE CLASS Person extends V`)).Err() 388 | Nil(t, err) 389 | err = ndb.Command(orient.NewSQLCommand(`CREATE CLASS Friend extends E`)).Err() 390 | Nil(t, err) 391 | ndb.Close() 392 | } 393 | 394 | // ---[ OPEN ]--- 395 | db, err := sql.Open(orient.DriverNameSQL, dsn) 396 | Nil(t, err) 397 | defer db.Close() 398 | 399 | err = db.Ping() 400 | Nil(t, err) 401 | 402 | insertSQL := "insert into Person SET firstName='Joe', lastName='Namath'" 403 | res, err := db.Exec(insertSQL) 404 | Nil(t, err) 405 | 406 | nrows, _ := res.RowsAffected() 407 | glog.V(10).Infof("nrows: %v\n", nrows) 408 | lastID, _ := res.LastInsertId() 409 | glog.V(10).Infof("last insert id: %v\n", lastID) 410 | Equals(t, int64(1), nrows) 411 | //True(t, lastID > int64(0), fmt.Sprintf("LastInsertId: %v", lastID)) // TODO: fix 412 | 413 | createVtxSQL := `CREATE VERTEX Person SET firstName = 'Terry', lastName = 'Bradshaw'` 414 | res, err = db.Exec(createVtxSQL) 415 | Nil(t, err) 416 | 417 | nrows, _ = res.RowsAffected() 418 | glog.V(10).Infof("nrows: %v\n", nrows) 419 | lastID, _ = res.LastInsertId() 420 | glog.V(10).Infof("last insert id: %v\n", lastID) 421 | Equals(t, int64(1), nrows) 422 | True(t, lastID > int64(0), fmt.Sprintf("LastInsertId: %v", lastID)) 423 | 424 | sql := `CREATE EDGE Friend FROM 425 | (SELECT FROM Person where firstName = 'Joe' AND lastName = 'Namath') 426 | TO 427 | (SELECT FROM Person where firstName = 'Terry' AND lastName = 'Bradshaw')` 428 | res, err = db.Exec(sql) 429 | Nil(t, err) 430 | nrows, _ = res.RowsAffected() 431 | glog.V(10).Infof("nrows: %v\n", nrows) 432 | lastID, _ = res.LastInsertId() 433 | glog.V(10).Infof("last insert id: %v\n", lastID) 434 | Equals(t, int64(1), nrows) 435 | //True(t, lastID > int64(0), fmt.Sprintf("LastInsertId: %v", lastID)) // TODO: fix 436 | 437 | sql = `select from Friend order by @rid desc LIMIT 1` 438 | rows, err := db.Query(sql) 439 | rowdocs := make([]*orient.Document, 0, 1) 440 | for rows.Next() { 441 | var newdoc *orient.Document 442 | err = rows.Scan(&newdoc) 443 | Nil(t, err) 444 | rowdocs = append(rowdocs, newdoc) 445 | } 446 | err = rows.Err() 447 | Nil(t, err) 448 | 449 | Equals(t, 1, len(rowdocs)) 450 | Equals(t, "Friend", rowdocs[0].ClassName()) 451 | friendOutLink := rowdocs[0].GetField("out").Value.(orient.OIdentifiable) 452 | True(t, friendOutLink.GetRecord() == nil, "should be nil") 453 | 454 | glog.V(10).Infof("friendOutLink: %v\n", friendOutLink) 455 | 456 | // REMOVE THE STUFF BELOW since can't specify fetchPlain in SQL (??? => ask on user group)' 457 | // sql = `select from Friend order by @rid desc LIMIT 1 fetchPlan=*:-1` 458 | // rows, err = db.Query(sql) 459 | // rowdocs = make([]*orient.Document, 0, 1) 460 | // for rows.Next() { 461 | // var newdoc orient.Document 462 | // err = rows.Scan(&newdoc) 463 | // rowdocs = append(rowdocs, &newdoc) 464 | // } 465 | // err = rows.Err() 466 | // Nil(t, err) 467 | 468 | // Equals(t, 1, len(rowdocs)) 469 | // Equals(t, "Friend", rowdocs[0].Classname) 470 | // friendOutLink = rowdocs[0].GetField("out").Value.(*orient.OLink) 471 | // // True(t, friendOutLink.Record != nil, "should NOT be nil") // FAILS: looks like you cannot put a fetchplain in an SQL query itself? 472 | 473 | // nrows, _ = res.RowsAffected() 474 | // glog.V(10).Infof("nrows: %v\n", nrows) 475 | // lastID, _ = res.LastInsertId() 476 | // glog.V(10).Infof("last insert id: %v\n", lastID) 477 | // Equals(t, int64(1), nrows) 478 | // True(t, lastID > int64(0), fmt.Sprintf("LastInsertId: %v", lastID)) 479 | 480 | } 481 | */ 482 | -------------------------------------------------------------------------------- /type.go: -------------------------------------------------------------------------------- 1 | package orient 2 | 3 | import ( 4 | "log" 5 | "reflect" 6 | "time" 7 | "unsafe" 8 | ) 9 | 10 | // OType is an enum for the various data types supported by OrientDB. 11 | type OType byte 12 | 13 | // in alignment with: http://orientdb.com/docs/last/Types.html 14 | const ( 15 | BOOLEAN OType = 0 16 | INTEGER OType = 1 17 | SHORT OType = 2 18 | LONG OType = 3 19 | FLOAT OType = 4 20 | DOUBLE OType = 5 21 | DATETIME OType = 6 22 | STRING OType = 7 23 | BINARY OType = 8 // means []byte 24 | EMBEDDED OType = 9 25 | EMBEDDEDLIST OType = 10 26 | EMBEDDEDSET OType = 11 27 | EMBEDDEDMAP OType = 12 28 | LINK OType = 13 29 | LINKLIST OType = 14 30 | LINKSET OType = 15 31 | LINKMAP OType = 16 32 | BYTE OType = 17 33 | TRANSIENT OType = 18 34 | DATE OType = 19 35 | CUSTOM OType = 20 36 | DECIMAL OType = 21 37 | LINKBAG OType = 22 38 | ANY OType = 23 39 | UNKNOWN OType = 255 // driver addition 40 | ) 41 | 42 | // detect the int size (4 or 8 bytes) 43 | const intSize = unsafe.Sizeof(int(0)) 44 | 45 | func (t OType) String() string { // do not change - it may be used as field type for SQL queries 46 | switch t { 47 | case BOOLEAN: 48 | return "BOOLEAN" 49 | case INTEGER: 50 | return "INTEGER" 51 | case LONG: 52 | return "LONG" 53 | case FLOAT: 54 | return "FLOAT" 55 | case DOUBLE: 56 | return "DOUBLE" 57 | case DATETIME: 58 | return "DATETIME" 59 | case STRING: 60 | return "STRING" 61 | case BINARY: 62 | return "BINARY" 63 | case EMBEDDED: 64 | return "EMBEDDED" 65 | case EMBEDDEDLIST: 66 | return "EMBEDDEDLIST" 67 | case EMBEDDEDSET: 68 | return "EMBEDDEDSET" 69 | case EMBEDDEDMAP: 70 | return "EMBEDDEDMAP" 71 | case LINK: 72 | return "LINK" 73 | case LINKLIST: 74 | return "LINKLIST" 75 | case LINKSET: 76 | return "LINKSET" 77 | case LINKMAP: 78 | return "LINKMAP" 79 | case BYTE: 80 | return "BYTE" 81 | case TRANSIENT: 82 | return "TRANSIENT" 83 | case DATE: 84 | return "DATE" 85 | case CUSTOM: 86 | return "CUSTOM" 87 | case DECIMAL: 88 | return "DECIMAL" 89 | case LINKBAG: 90 | return "LINKBAG" 91 | case ANY: 92 | return "ANY" 93 | default: 94 | return "UNKNOWN" 95 | } 96 | } 97 | 98 | func (t OType) ReflectKind() reflect.Kind { 99 | switch t { 100 | case BOOLEAN: 101 | return reflect.Bool 102 | case BYTE: 103 | return reflect.Uint8 104 | case SHORT: 105 | return reflect.Int16 106 | case INTEGER: 107 | return reflect.Int32 108 | case LONG: 109 | return reflect.Int64 110 | case FLOAT: 111 | return reflect.Float32 112 | case DOUBLE: 113 | return reflect.Float64 114 | case STRING: 115 | return reflect.String 116 | case EMBEDDEDLIST, EMBEDDEDSET: 117 | return reflect.Slice 118 | case EMBEDDEDMAP: 119 | return reflect.Map 120 | case LINKLIST, LINKSET: 121 | return reflect.Slice 122 | case LINKMAP: 123 | return reflect.Map 124 | default: 125 | return reflect.Invalid 126 | } 127 | } 128 | 129 | func (t OType) ReflectType() reflect.Type { 130 | switch t { 131 | case BOOLEAN: 132 | return reflect.TypeOf(bool(false)) 133 | case INTEGER: 134 | return reflect.TypeOf(int32(0)) 135 | case LONG: 136 | return reflect.TypeOf(int64(0)) 137 | case FLOAT: 138 | return reflect.TypeOf(float32(0)) 139 | case DOUBLE: 140 | return reflect.TypeOf(float64(0)) 141 | case DATETIME, DATE: 142 | return reflect.TypeOf(time.Time{}) 143 | case STRING: 144 | return reflect.TypeOf(string("")) 145 | case BINARY: 146 | return reflect.TypeOf([]byte{}) 147 | case BYTE: 148 | return reflect.TypeOf(byte(0)) 149 | // case EMBEDDED: 150 | // return "EMBEDDED" 151 | // case EMBEDDEDLIST: 152 | // return "EMBEDDEDLIST" 153 | // case EMBEDDEDSET: 154 | // return "EMBEDDEDSET" 155 | // case EMBEDDEDMAP: 156 | // return "EMBEDDEDMAP" 157 | // case LINK: 158 | // return "LINK" 159 | // case LINKLIST: 160 | // return "LINKLIST" 161 | // case LINKSET: 162 | // return "LINKSET" 163 | // case LINKMAP: 164 | // return "LINKMAP" 165 | // case CUSTOM: 166 | // return "CUSTOM" 167 | // case DECIMAL: 168 | // return "DECIMAL" 169 | // case LINKBAG: 170 | // return "LINKBAG" 171 | default: // and ANY, TRANSIENT 172 | return reflect.TypeOf((*interface{})(nil)).Elem() 173 | } 174 | } 175 | 176 | func OTypeForValue(val interface{}) (ftype OType) { 177 | ftype = UNKNOWN 178 | // TODO: need to add more types: LINKSET, LINKLIST, etc. ... 179 | switch val.(type) { 180 | case string: 181 | ftype = STRING 182 | case bool: 183 | ftype = BOOLEAN 184 | case int32: 185 | ftype = INTEGER 186 | case int64: 187 | ftype = LONG 188 | case int16: 189 | ftype = SHORT 190 | case int: 191 | if intSize == 4 { 192 | ftype = INTEGER 193 | } else { 194 | ftype = LONG 195 | } 196 | case byte, int8: 197 | ftype = BYTE 198 | case *Document, DocumentSerializable: 199 | ftype = EMBEDDED 200 | case float32: 201 | ftype = FLOAT 202 | case float64: 203 | ftype = DOUBLE 204 | case []byte: 205 | ftype = BINARY 206 | case OIdentifiable: 207 | ftype = LINK 208 | case []OIdentifiable, []RID: 209 | ftype = LINKLIST 210 | case *RidBag: 211 | ftype = LINKBAG 212 | case time.Time: 213 | ftype = DATETIME 214 | // TODO: more types need to be added 215 | default: 216 | if isDecimal(val) { 217 | ftype = DECIMAL 218 | return 219 | } 220 | rt := reflect.TypeOf(val) 221 | if rt.Kind() == reflect.Ptr { 222 | rt = rt.Elem() 223 | } 224 | switch rt.Kind() { 225 | case reflect.Map: 226 | ftype = EMBEDDEDMAP 227 | case reflect.Slice, reflect.Array: 228 | if reflect.TypeOf(val).Elem() == reflect.TypeOf(byte(0)) { 229 | ftype = BINARY 230 | } else { 231 | ftype = EMBEDDEDLIST 232 | } 233 | case reflect.Bool: 234 | ftype = BOOLEAN 235 | case reflect.Uint8: 236 | ftype = BYTE 237 | case reflect.Int16: 238 | ftype = SHORT 239 | case reflect.Int32: 240 | ftype = INTEGER 241 | case reflect.Int64: 242 | ftype = LONG 243 | case reflect.Int: 244 | if intSize == 4 { 245 | ftype = INTEGER 246 | } else { 247 | ftype = LONG 248 | } 249 | case reflect.Uint: 250 | if intSize == 4 { 251 | ftype = INTEGER 252 | } else { 253 | ftype = LONG 254 | } 255 | case reflect.Uint64: 256 | ftype = LONG 257 | case reflect.String: 258 | ftype = STRING 259 | case reflect.Struct: 260 | ftype = EMBEDDED 261 | default: 262 | log.Printf("unknown type in serialization: %T, kind: %v", val, reflect.TypeOf(val).Kind()) 263 | } 264 | } 265 | return 266 | } 267 | 268 | func OTypeFromString(typ string) OType { 269 | switch typ { 270 | case "BOOLEAN": 271 | return BOOLEAN 272 | case "INTEGER": 273 | return INTEGER 274 | case "SHORT": 275 | return SHORT 276 | case "LONG": 277 | return LONG 278 | case "FLOAT": 279 | return FLOAT 280 | case "DOUBLE": 281 | return DOUBLE 282 | case "DATETIME": 283 | return DATETIME 284 | case "STRING": 285 | return STRING 286 | case "BINARY": 287 | return BINARY 288 | case "EMBEDDED": 289 | return EMBEDDED 290 | case "EMBEDDEDLIST": 291 | return EMBEDDEDLIST 292 | case "EMBEDDEDSET": 293 | return EMBEDDEDSET 294 | case "EMBEDDEDMAP": 295 | return EMBEDDEDMAP 296 | case "LINK": 297 | return LINK 298 | case "LINKLIST": 299 | return LINKLIST 300 | case "LINKSET": 301 | return LINKSET 302 | case "LINKMAP": 303 | return LINKMAP 304 | case "BYTE": 305 | return BYTE 306 | case "TRANSIENT": 307 | return TRANSIENT 308 | case "DATE": 309 | return DATE 310 | case "CUSTOM": 311 | return CUSTOM 312 | case "DECIMAL": 313 | return DECIMAL 314 | case "LINKBAG": 315 | return LINKBAG 316 | case "ANY": 317 | return ANY 318 | default: 319 | panic("Unkwown type: " + typ) 320 | } 321 | } 322 | -------------------------------------------------------------------------------- /util.go: -------------------------------------------------------------------------------- 1 | package orient 2 | 3 | import ( 4 | "unicode" 5 | ) 6 | 7 | func isExported(s string) bool { 8 | if s == "" { 9 | return false 10 | } 11 | return unicode.IsUpper(([]rune(s))[0]) 12 | } 13 | --------------------------------------------------------------------------------