package gorethink import ( "fmt" "reflect" "strconv" "strings" p "gopkg.in/gorethink/gorethink.v2/ql2" ) // A Query represents a query ready to be sent to the database, A Query differs // from a Term as it contains both a query type and token. These values are used // by the database to determine if the query is continuing a previous request // and also allows the driver to identify the response as they can come out of // order. type Query struct { Type p.Query_QueryType Token int64 Term *Term Opts map[string]interface{} builtTerm interface{} } func (q *Query) Build() []interface{} { res := []interface{}{int(q.Type)} if q.Term != nil { res = append(res, q.builtTerm) } if len(q.Opts) > 0 { // Clone opts and remove custom gorethink options opts := map[string]interface{}{} for k, v := range q.Opts { switch k { case "geometry_format": default: opts[k] = v } } res = append(res, opts) } return res } type termsList []Term type termsObj map[string]Term // A Term represents a query that is being built. Terms consist of a an array of // "sub-terms" and a term type. When a Term is a sub-term the first element of // the terms data is its parent Term. // // When built the term becomes a JSON array, for more information on the format // see http://rethinkdb.com/docs/writing-drivers/. type Term struct { name string rawQuery bool rootTerm bool termType p.Term_TermType data interface{} args []Term optArgs map[string]Term lastErr error isMockAnything bool } func (t Term) compare(t2 Term, varMap map[int64]int64) bool { if t.isMockAnything || t2.isMockAnything { return true } if t.name != t2.name || t.rawQuery != t2.rawQuery || t.rootTerm != t2.rootTerm || t.termType != t2.termType || !reflect.DeepEqual(t.data, t2.data) || len(t.args) != len(t2.args) || len(t.optArgs) != len(t2.optArgs) { return false } for i, v := range t.args { if t.termType == p.Term_FUNC && t2.termType == p.Term_FUNC && i == 0 { // Functions need to be compared differently as each variable // will have a different var ID so first try to create a mapping // between the two sets of IDs argsArr := t.args[0].args argsArr2 := t2.args[0].args if len(argsArr) != len(argsArr2) { return false } for j := 0; j < len(argsArr); j++ { varMap[argsArr[j].data.(int64)] = argsArr2[j].data.(int64) } } else if t.termType == p.Term_VAR && t2.termType == p.Term_VAR && i == 0 { // When comparing vars use our var map v1 := t.args[i].data.(int64) v2 := t2.args[i].data.(int64) if varMap[v1] != v2 { return false } } else if !v.compare(t2.args[i], varMap) { return false } } for k, v := range t.optArgs { if _, ok := t2.optArgs[k]; !ok { return false } if !v.compare(t2.optArgs[k], varMap) { return false } } return true } // build takes the query tree and prepares it to be sent as a JSON // expression func (t Term) Build() (interface{}, error) { var err error if t.lastErr != nil { return nil, t.lastErr } if t.rawQuery { return t.data, nil } switch t.termType { case p.Term_DATUM: return t.data, nil case p.Term_MAKE_OBJ: res := map[string]interface{}{} for k, v := range t.optArgs { res[k], err = v.Build() if err != nil { return nil, err } } return res, nil case p.Term_BINARY: if len(t.args) == 0 { return map[string]interface{}{ "$reql_type$": "BINARY", "data": t.data, }, nil } } args := make([]interface{}, len(t.args)) optArgs := make(map[string]interface{}, len(t.optArgs)) for i, v := range t.args { arg, err := v.Build() if err != nil { return nil, err } args[i] = arg } for k, v := range t.optArgs { optArgs[k], err = v.Build() if err != nil { return nil, err } } ret := []interface{}{int(t.termType)} if len(args) > 0 { ret = append(ret, args) } if len(optArgs) > 0 { ret = append(ret, optArgs) } return ret, nil } // String returns a string representation of the query tree func (t Term) String() string { if t.isMockAnything { return "r.MockAnything()" } switch t.termType { case p.Term_MAKE_ARRAY: return fmt.Sprintf("[%s]", strings.Join(argsToStringSlice(t.args), ", ")) case p.Term_MAKE_OBJ: return fmt.Sprintf("{%s}", strings.Join(optArgsToStringSlice(t.optArgs), ", ")) case p.Term_FUNC: // Get string representation of each argument args := []string{} for _, v := range t.args[0].args { args = append(args, fmt.Sprintf("var_%d", v.data)) } return fmt.Sprintf("func(%s r.Term) r.Term { return %s }", strings.Join(args, ", "), t.args[1].String(), ) case p.Term_VAR: return fmt.Sprintf("var_%s", t.args[0]) case p.Term_IMPLICIT_VAR: return "r.Row" case p.Term_DATUM: switch v := t.data.(type) { case string: return strconv.Quote(v) default: return fmt.Sprintf("%v", v) } case p.Term_BINARY: if len(t.args) == 0 { return fmt.Sprintf("r.binary()") } } if t.rootTerm { return fmt.Sprintf("r.%s(%s)", t.name, strings.Join(allArgsToStringSlice(t.args, t.optArgs), ", ")) } if t.args == nil { return "r" } return fmt.Sprintf("%s.%s(%s)", t.args[0].String(), t.name, strings.Join(allArgsToStringSlice(t.args[1:], t.optArgs), ", ")) } // OptArgs is an interface used to represent a terms optional arguments. All // optional argument types have a toMap function, the returned map can be encoded // and sent as part of the query. type OptArgs interface { toMap() map[string]interface{} } func (t Term) OptArgs(args interface{}) Term { switch args := args.(type) { case OptArgs: t.optArgs = convertTermObj(args.toMap()) case map[string]interface{}: t.optArgs = convertTermObj(args) } return t } type QueryExecutor interface { IsConnected() bool Query(Query) (*Cursor, error) Exec(Query) error newQuery(t Term, opts map[string]interface{}) (Query, error) } // WriteResponse is a helper type used when dealing with the response of a // write query. It is also returned by the RunWrite function. type WriteResponse struct { Errors int `gorethink:"errors"` Inserted int `gorethink:"inserted"` Updated int `gorethink:"updated"` Unchanged int `gorethink:"unchanged"` Replaced int `gorethink:"replaced"` Renamed int `gorethink:"renamed"` Skipped int `gorethink:"skipped"` Deleted int `gorethink:"deleted"` Created int `gorethink:"created"` DBsCreated int `gorethink:"dbs_created"` TablesCreated int `gorethink:"tables_created"` Dropped int `gorethink:"dropped"` DBsDropped int `gorethink:"dbs_dropped"` TablesDropped int `gorethink:"tables_dropped"` GeneratedKeys []string `gorethink:"generated_keys"` FirstError string `gorethink:"first_error"` // populated if Errors > 0 ConfigChanges []ChangeResponse `gorethink:"config_changes"` Changes []ChangeResponse } // ChangeResponse is a helper type used when dealing with changefeeds. The type // contains both the value before the query and the new value. type ChangeResponse struct { NewValue interface{} `gorethink:"new_val,omitempty"` OldValue interface{} `gorethink:"old_val,omitempty"` State string `gorethink:"state,omitempty"` Error string `gorethink:"error,omitempty"` } // RunOpts contains the optional arguments for the Run function. type RunOpts struct { DB interface{} `gorethink:"db,omitempty"` Db interface{} `gorethink:"db,omitempty"` // Deprecated Profile interface{} `gorethink:"profile,omitempty"` Durability interface{} `gorethink:"durability,omitempty"` UseOutdated interface{} `gorethink:"use_outdated,omitempty"` // Deprecated ArrayLimit interface{} `gorethink:"array_limit,omitempty"` TimeFormat interface{} `gorethink:"time_format,omitempty"` GroupFormat interface{} `gorethink:"group_format,omitempty"` BinaryFormat interface{} `gorethink:"binary_format,omitempty"` GeometryFormat interface{} `gorethink:"geometry_format,omitempty"` ReadMode interface{} `gorethink:"read_mode,omitempty"` MinBatchRows interface{} `gorethink:"min_batch_rows,omitempty"` MaxBatchRows interface{} `gorethink:"max_batch_rows,omitempty"` MaxBatchBytes interface{} `gorethink:"max_batch_bytes,omitempty"` MaxBatchSeconds interface{} `gorethink:"max_batch_seconds,omitempty"` FirstBatchScaledownFactor interface{} `gorethink:"first_batch_scaledown_factor,omitempty"` } func (o RunOpts) toMap() map[string]interface{} { return optArgsToMap(o) } // Run runs a query using the given connection. // // rows, err := query.Run(sess) // if err != nil { // // error // } // // var doc MyDocumentType // for rows.Next(&doc) { // // Do something with document // } func (t Term) Run(s QueryExecutor, optArgs ...RunOpts) (*Cursor, error) { opts := map[string]interface{}{} if len(optArgs) >= 1 { opts = optArgs[0].toMap() } if s == nil || !s.IsConnected() { return nil, ErrConnectionClosed } q, err := s.newQuery(t, opts) if err != nil { return nil, err } return s.Query(q) } // RunWrite runs a query using the given connection but unlike Run automatically // scans the result into a variable of type WriteResponse. This function should be used // if you are running a write query (such as Insert, Update, TableCreate, etc...). // // If an error occurs when running the write query the first error is returned. // // res, err := r.DB("database").Table("table").Insert(doc).RunWrite(sess) func (t Term) RunWrite(s QueryExecutor, optArgs ...RunOpts) (WriteResponse, error) { var response WriteResponse res, err := t.Run(s, optArgs...) if err != nil { return response, err } defer res.Close() if err = res.One(&response); err != nil { return response, err } if response.Errors > 0 { return response, fmt.Errorf("%s", response.FirstError) } return response, nil } // ReadOne is a shortcut method that runs the query on the given connection // and reads one response from the cursor before closing it. // // It returns any errors encountered from running the query or reading the response func (t Term) ReadOne(dest interface{}, s QueryExecutor, optArgs ...RunOpts) error { res, err := t.Run(s, optArgs...) if err != nil { return err } return res.One(dest) } // ReadAll is a shortcut method that runs the query on the given connection // and reads all of the responses from the cursor before closing it. // // It returns any errors encountered from running the query or reading the responses func (t Term) ReadAll(dest interface{}, s QueryExecutor, optArgs ...RunOpts) error { res, err := t.Run(s, optArgs...) if err != nil { return err } return res.All(dest) } // ExecOpts contains the optional arguments for the Exec function and inherits // its options from RunOpts, the only difference is the addition of the NoReply // field. // // When NoReply is true it causes the driver not to wait to receive the result // and return immediately. type ExecOpts struct { DB interface{} `gorethink:"db,omitempty"` Db interface{} `gorethink:"db,omitempty"` // Deprecated Profile interface{} `gorethink:"profile,omitempty"` Durability interface{} `gorethink:"durability,omitempty"` UseOutdated interface{} `gorethink:"use_outdated,omitempty"` // Deprecated ArrayLimit interface{} `gorethink:"array_limit,omitempty"` TimeFormat interface{} `gorethink:"time_format,omitempty"` GroupFormat interface{} `gorethink:"group_format,omitempty"` BinaryFormat interface{} `gorethink:"binary_format,omitempty"` GeometryFormat interface{} `gorethink:"geometry_format,omitempty"` MinBatchRows interface{} `gorethink:"min_batch_rows,omitempty"` MaxBatchRows interface{} `gorethink:"max_batch_rows,omitempty"` MaxBatchBytes interface{} `gorethink:"max_batch_bytes,omitempty"` MaxBatchSeconds interface{} `gorethink:"max_batch_seconds,omitempty"` FirstBatchScaledownFactor interface{} `gorethink:"first_batch_scaledown_factor,omitempty"` NoReply interface{} `gorethink:"noreply,omitempty"` } func (o ExecOpts) toMap() map[string]interface{} { return optArgsToMap(o) } // Exec runs the query but does not return the result. Exec will still wait for // the response to be received unless the NoReply field is true. // // err := r.DB("database").Table("table").Insert(doc).Exec(sess, r.ExecOpts{ // NoReply: true, // }) func (t Term) Exec(s QueryExecutor, optArgs ...ExecOpts) error { opts := map[string]interface{}{} if len(optArgs) >= 1 { opts = optArgs[0].toMap() } if s == nil || !s.IsConnected() { return ErrConnectionClosed } q, err := s.newQuery(t, opts) if err != nil { return err } return s.Exec(q) }