diff --git a/cmd/genji/commands/restore.go b/cmd/genji/commands/restore.go index b2aaa78ae..19242276b 100644 --- a/cmd/genji/commands/restore.go +++ b/cmd/genji/commands/restore.go @@ -4,7 +4,6 @@ import ( "bufio" "context" "errors" - "fmt" "io" "os" @@ -14,6 +13,7 @@ import ( "github.com/genjidb/genji/engine" "github.com/genjidb/genji/engine/badgerengine" "github.com/genjidb/genji/engine/boltengine" + "github.com/genjidb/genji/stringutil" "github.com/urfave/cli/v2" ) @@ -75,7 +75,7 @@ func executeRestore(ctx context.Context, r io.Reader, e, dbPath string) error { case "badger": ng, err = badgerengine.NewEngine(badger.DefaultOptions(dbPath).WithLogger(nil)) default: - return fmt.Errorf(`engine should be "bolt" or "badger, got %q`, e) + return stringutil.Errorf(`engine should be "bolt" or "badger, got %q`, e) } if err != nil { return err diff --git a/cmd/genji/dbutil/db.go b/cmd/genji/dbutil/db.go index 3883626b5..62aae8e5a 100644 --- a/cmd/genji/dbutil/db.go +++ b/cmd/genji/dbutil/db.go @@ -3,7 +3,6 @@ package dbutil import ( "context" "errors" - "fmt" "strings" "time" @@ -13,6 +12,7 @@ import ( "github.com/genjidb/genji/engine/badgerengine" "github.com/genjidb/genji/engine/boltengine" "github.com/genjidb/genji/engine/memoryengine" + "github.com/genjidb/genji/stringutil" "go.etcd.io/bbolt" ) @@ -39,7 +39,7 @@ func OpenDB(ctx context.Context, dbPath, engineName string) (*genji.DB, error) { return nil, errors.New("database is locked") } default: - return nil, fmt.Errorf(`engine should be "bolt" or "badger", got %q`, engineName) + return nil, stringutil.Errorf(`engine should be "bolt" or "badger", got %q`, engineName) } if err != nil { return nil, err diff --git a/cmd/genji/dbutil/insert.go b/cmd/genji/dbutil/insert.go index b53fac8e9..21af8950e 100644 --- a/cmd/genji/dbutil/insert.go +++ b/cmd/genji/dbutil/insert.go @@ -8,6 +8,7 @@ import ( "github.com/genjidb/genji" "github.com/genjidb/genji/document" + "github.com/genjidb/genji/stringutil" ) // InsertJSON reads json documents from r and inserts them into the selected table. @@ -76,11 +77,11 @@ func InsertJSON(db *genji.DB, table string, r io.Reader) error { } d, ok := t.(json.Delim) if ok && d.String() != "]" { - return fmt.Errorf("found %q, but expected ']'", c) + return stringutil.Errorf("found %q, but expected ']'", c) } default: - return fmt.Errorf("found %q, but expected '{' or '['", c) + return stringutil.Errorf("found %q, but expected '{' or '['", c) } return nil diff --git a/cmd/genji/shell/command.go b/cmd/genji/shell/command.go index ecfb99a46..8d1e74373 100644 --- a/cmd/genji/shell/command.go +++ b/cmd/genji/shell/command.go @@ -10,6 +10,7 @@ import ( "github.com/genjidb/genji/database" "github.com/genjidb/genji/document" "github.com/genjidb/genji/engine" + "github.com/genjidb/genji/stringutil" ) var commands = []struct { @@ -98,7 +99,7 @@ func runIndexesCmd(db *genji.DB, tableName string, w io.Writer) error { _, err := tx.QueryDocument("SELECT 1 FROM __genji_tables WHERE table_name = ? LIMIT 1", tableName) if err != nil { if err == database.ErrDocumentNotFound { - return fmt.Errorf("%w: %q", database.ErrTableNotFound, tableName) + return stringutil.Errorf("%w: %q", database.ErrTableNotFound, tableName) } return err } diff --git a/cmd/genji/shell/shell.go b/cmd/genji/shell/shell.go index 4c4db7f5e..46fe1dc1c 100644 --- a/cmd/genji/shell/shell.go +++ b/cmd/genji/shell/shell.go @@ -19,6 +19,7 @@ import ( "github.com/genjidb/genji/cmd/genji/dbutil" "github.com/genjidb/genji/document" "github.com/genjidb/genji/sql/parser" + "github.com/genjidb/genji/stringutil" "go.uber.org/multierr" "golang.org/x/sync/errgroup" ) @@ -80,7 +81,7 @@ func (o *Options) validate() error { switch o.Engine { case "bolt", "badger", "memory": default: - return fmt.Errorf("unsupported engine %q", o.Engine) + return stringutil.Errorf("unsupported engine %q", o.Engine) } return nil @@ -445,19 +446,19 @@ func (sh *Shell) runCommand(ctx context.Context, in string) error { return runHelpCmd() case ".tables": if len(cmd) > 1 { - return fmt.Errorf("usage: .tables") + return stringutil.Errorf("usage: .tables") } return runTablesCmd(sh.db, os.Stdout) case ".exit", "exit": if len(cmd) > 1 { - return fmt.Errorf("usage: .exit") + return stringutil.Errorf("usage: .exit") } return errExitCommand case ".indexes": if len(cmd) > 2 { - return fmt.Errorf("usage: .indexes [tablename]") + return stringutil.Errorf("usage: .indexes [tablename]") } var tableName string @@ -477,7 +478,7 @@ func (sh *Shell) runCommand(ctx context.Context, in string) error { engine = "bolt" path = cmd[1] } else { - return fmt.Errorf("Can't save without output path") + return stringutil.Errorf("Can't save without output path") } return runSaveCmd(ctx, sh.db, engine, path) @@ -506,11 +507,11 @@ func (sh *Shell) runPipedInput(ctx context.Context) (ran bool, err error) { } data, err := ioutil.ReadAll(os.Stdin) if err != nil { - return true, fmt.Errorf("Unable to read piped input: %w", err) + return true, stringutil.Errorf("Unable to read piped input: %w", err) } err = sh.runQuery(ctx, string(data)) if err != nil { - return true, fmt.Errorf("Unable to execute provided sql statements: %w", err) + return true, stringutil.Errorf("Unable to execute provided sql statements: %w", err) } return true, nil @@ -622,7 +623,7 @@ func displaySuggestions(in string) error { } if len(suggestions) == 0 { - return fmt.Errorf("Unknown command %q. Enter \".help\" for help.", in) + return stringutil.Errorf("Unknown command %q. Enter \".help\" for help.", in) } fmt.Printf("\"%s\" is not a command. Did you mean: ", in) diff --git a/database/catalog.go b/database/catalog.go index a5e3522fd..0ac12b151 100644 --- a/database/catalog.go +++ b/database/catalog.go @@ -2,11 +2,11 @@ package database import ( "errors" - "fmt" "strings" "sync" "github.com/genjidb/genji/document" + "github.com/genjidb/genji/stringutil" ) // Catalog holds all table and index informations. @@ -107,7 +107,7 @@ func (c *Catalog) GetTable(tx *Transaction, tableName string) (*Table, error) { // If it already exists, returns ErrTableAlreadyExists. func (c *Catalog) CreateTable(tx *Transaction, tableName string, info *TableInfo) error { if strings.HasPrefix(tableName, internalPrefix) { - return fmt.Errorf("table name must not start with %s", internalPrefix) + return stringutil.Errorf("table name must not start with %s", internalPrefix) } if info == nil { @@ -135,7 +135,7 @@ func (c *Catalog) CreateTable(tx *Transaction, tableName string, info *TableInfo err = tx.tx.CreateStore(info.storeName) if err != nil { - return fmt.Errorf("failed to create table %q: %w", tableName, err) + return stringutil.Errorf("failed to create table %q: %w", tableName, err) } return nil @@ -319,7 +319,7 @@ func (c *Catalog) buildIndex(tx *Transaction, idx *Index, table *Table) error { err = idx.Set(v, d.(document.Keyer).RawKey()) if err != nil { - return fmt.Errorf("error while building the index: %w", err) + return stringutil.Errorf("error while building the index: %w", err) } return nil diff --git a/database/catalog_test.go b/database/catalog_test.go index fca8116d1..df00db824 100644 --- a/database/catalog_test.go +++ b/database/catalog_test.go @@ -550,7 +550,7 @@ func TestCatalogReIndex(t *testing.T) { require.Equal(t, 10, i) require.NoError(t, err) - idx, err = tx.GetIndex("b") + _, err = tx.GetIndex("b") require.NoError(t, err) return errDontCommit diff --git a/database/config.go b/database/config.go index 07579cd06..8364d5870 100644 --- a/database/config.go +++ b/database/config.go @@ -3,11 +3,11 @@ package database import ( "bytes" "encoding/binary" - "fmt" "strings" "github.com/genjidb/genji/document" "github.com/genjidb/genji/engine" + "github.com/genjidb/genji/stringutil" ) const storePrefix = 't' @@ -261,12 +261,12 @@ func (f *FieldConstraints) Add(newFc *FieldConstraint) error { // if constraints are different if !ok { - return fmt.Errorf("conflicting constraints: %q and %q", c.String(), newFc.String()) + return stringutil.Errorf("conflicting constraints: %q and %q", c.String(), newFc.String()) } // if both non inferred, they are duplicate if !newFc.IsInferred && !c.IsInferred { - return fmt.Errorf("conflicting constraints: %q and %q", c.String(), newFc.String()) + return stringutil.Errorf("conflicting constraints: %q and %q", c.String(), newFc.String()) } // if both inferred, merge the InferredBy member @@ -288,7 +288,7 @@ func (f *FieldConstraints) Add(newFc *FieldConstraint) error { // ensure we don't have duplicate primary keys if c.IsPrimaryKey && newFc.IsPrimaryKey { - return fmt.Errorf( + return stringutil.Errorf( "multiple primary keys are not allowed (%q is primary key)", c.Path.String(), ) @@ -329,7 +329,7 @@ func (f FieldConstraints) ValidateDocument(d document.Document) (*document.Field // to the right type above. // check if it is required but null. if v.Type == document.NullValue && fc.IsNotNull { - return nil, fmt.Errorf("field %q is required and must be not null", fc.Path) + return nil, stringutil.Errorf("field %q is required and must be not null", fc.Path) } continue } @@ -348,7 +348,7 @@ func (f FieldConstraints) ValidateDocument(d document.Document) (*document.Field // if there is no default value // check if field is required } else if fc.IsNotNull { - return nil, fmt.Errorf("field %q is required and must be not null", fc.Path) + return nil, stringutil.Errorf("field %q is required and must be not null", fc.Path) } } @@ -580,7 +580,7 @@ func (t *tableStore) Delete(tx *Transaction, tableName string) error { err := t.st.Delete([]byte(tableName)) if err != nil { if err == engine.ErrKeyNotFound { - return fmt.Errorf("%w: %q", ErrTableNotFound, tableName) + return stringutil.Errorf("%w: %q", ErrTableNotFound, tableName) } return err @@ -603,7 +603,7 @@ func (t *tableStore) Replace(tx *Transaction, tableName string, info *TableInfo) _, err = t.st.Get(tbName) if err != nil { if err == engine.ErrKeyNotFound { - return fmt.Errorf("%w: %q", ErrTableNotFound, tableName) + return stringutil.Errorf("%w: %q", ErrTableNotFound, tableName) } return err diff --git a/database/database.go b/database/database.go index a5b0af95b..8d16195db 100644 --- a/database/database.go +++ b/database/database.go @@ -14,11 +14,6 @@ import ( type Database struct { ng engine.Engine - // This stores the last transaction id created. - // It starts at 0 at database startup and is - // incremented atomically every time Begin is called. - lastTransactionID int64 - // If this is non-nil, the user is running an explicit transaction // using the BEGIN statement. // Only one attached transaction can be run at a time and any calls to DB.Begin() diff --git a/database/index.go b/database/index.go index a1384d4a0..40070fcfa 100644 --- a/database/index.go +++ b/database/index.go @@ -4,10 +4,10 @@ import ( "bytes" "encoding/binary" "errors" - "fmt" "github.com/genjidb/genji/document" "github.com/genjidb/genji/engine" + "github.com/genjidb/genji/stringutil" ) const ( @@ -51,7 +51,7 @@ func (idx *Index) Set(v document.Value, k []byte) error { } if idx.Info.Type != 0 && idx.Info.Type != v.Type { - return fmt.Errorf("cannot index value of type %s in %s index", v.Type, idx.Info.Type) + return stringutil.Errorf("cannot index value of type %s in %s index", v.Type, idx.Info.Type) } st, err := getOrCreateStore(idx.tx, idx.storeName) diff --git a/database/table.go b/database/table.go index 1d57e91ad..9fd191aa5 100644 --- a/database/table.go +++ b/database/table.go @@ -4,11 +4,11 @@ import ( "bytes" "encoding/binary" "errors" - "fmt" "github.com/genjidb/genji/document" "github.com/genjidb/genji/document/encoding" "github.com/genjidb/genji/engine" + "github.com/genjidb/genji/stringutil" ) // A Table represents a collection of documents. @@ -81,7 +81,7 @@ func (t *Table) Insert(d document.Document) (document.Document, error) { defer enc.Close() err = enc.EncodeDocument(fb) if err != nil { - return nil, fmt.Errorf("failed to encode document: %w", err) + return nil, stringutil.Errorf("failed to encode document: %w", err) } err = t.Store.Put(key, buf.Bytes()) @@ -196,7 +196,7 @@ func (t *Table) replace(indexes []*Index, key []byte, d document.Document) error defer enc.Close() err = enc.EncodeDocument(d) if err != nil { - return fmt.Errorf("failed to encode document: %w", err) + return stringutil.Errorf("failed to encode document: %w", err) } // replace old document with new document @@ -440,7 +440,7 @@ func (t *Table) GetDocument(key []byte) (document.Document, error) { if err == engine.ErrKeyNotFound { return nil, ErrDocumentNotFound } - return nil, fmt.Errorf("failed to fetch document %q: %w", key, err) + return nil, stringutil.Errorf("failed to fetch document %q: %w", key, err) } info := t.Info() @@ -463,7 +463,7 @@ func (t *Table) generateKey(info *TableInfo, fb *document.FieldBuffer) ([]byte, v, err := pk.Path.GetValueFromDocument(fb) if err == document.ErrFieldNotFound { - return nil, fmt.Errorf("missing primary key at path %q", pk.Path) + return nil, stringutil.Errorf("missing primary key at path %q", pk.Path) } if err != nil { return nil, err diff --git a/database/table_test.go b/database/table_test.go index 28c84423c..b284cb98c 100644 --- a/database/table_test.go +++ b/database/table_test.go @@ -223,7 +223,7 @@ func TestTableInsert(t *testing.T) { require.NoError(t, err) // insert again - d, err = tb.Insert(doc) + _, err = tb.Insert(doc) require.Equal(t, database.ErrDuplicateDocument, err) }) diff --git a/document/cast.go b/document/cast.go index bee836ff5..c1e325d17 100644 --- a/document/cast.go +++ b/document/cast.go @@ -2,8 +2,9 @@ package document import ( "encoding/base64" - "fmt" "strconv" + + "github.com/genjidb/genji/stringutil" ) // CastAs casts v as the selected type when possible. @@ -34,7 +35,7 @@ func (v Value) CastAs(t ValueType) (Value, error) { return v.CastAsDocument() } - return Value{}, fmt.Errorf("cannot cast %s as %q", v.Type, t) + return Value{}, stringutil.Errorf("cannot cast %s as %q", v.Type, t) } // CastAsBool casts according to the following rules: @@ -51,12 +52,12 @@ func (v Value) CastAsBool() (Value, error) { case TextValue: b, err := strconv.ParseBool(v.V.(string)) if err != nil { - return Value{}, fmt.Errorf(`cannot cast %q as bool: %w`, v.V, err) + return Value{}, stringutil.Errorf(`cannot cast %q as bool: %w`, v.V, err) } return NewBoolValue(b), nil } - return Value{}, fmt.Errorf("cannot cast %s as bool", v.Type) + return Value{}, stringutil.Errorf("cannot cast %s as bool", v.Type) } // CastAsInteger casts according to the following rules: @@ -84,14 +85,14 @@ func (v Value) CastAsInteger() (Value, error) { intErr := err f, err := strconv.ParseFloat(v.V.(string), 64) if err != nil { - return Value{}, fmt.Errorf(`cannot cast %q as integer: %w`, v.V, intErr) + return Value{}, stringutil.Errorf(`cannot cast %q as integer: %w`, v.V, intErr) } i = int64(f) } return NewIntegerValue(i), nil } - return Value{}, fmt.Errorf("cannot cast %s as integer", v.Type) + return Value{}, stringutil.Errorf("cannot cast %s as integer", v.Type) } // CastAsDouble casts according to the following rules: @@ -108,12 +109,12 @@ func (v Value) CastAsDouble() (Value, error) { case TextValue: f, err := strconv.ParseFloat(v.V.(string), 64) if err != nil { - return Value{}, fmt.Errorf(`cannot cast %q as double: %w`, v.V, err) + return Value{}, stringutil.Errorf(`cannot cast %q as double: %w`, v.V, err) } return NewDoubleValue(f), nil } - return Value{}, fmt.Errorf("cannot cast %s as double", v.Type) + return Value{}, stringutil.Errorf("cannot cast %s as double", v.Type) } // CastAsText returns a JSON representation of v. @@ -151,13 +152,13 @@ func (v Value) CastAsBlob() (Value, error) { if v.Type == TextValue { b, err := base64.StdEncoding.DecodeString(v.V.(string)) if err != nil { - return Value{}, fmt.Errorf(`cannot cast %q as blob: %w`, v.V, err) + return Value{}, stringutil.Errorf(`cannot cast %q as blob: %w`, v.V, err) } return NewBlobValue(b), nil } - return Value{}, fmt.Errorf("cannot cast %s as blob", v.Type) + return Value{}, stringutil.Errorf("cannot cast %s as blob", v.Type) } // CastAsArray casts according to the following rules: @@ -172,13 +173,13 @@ func (v Value) CastAsArray() (Value, error) { var vb ValueBuffer err := vb.UnmarshalJSON([]byte(v.V.(string))) if err != nil { - return Value{}, fmt.Errorf(`cannot cast %q as array: %w`, v.V, err) + return Value{}, stringutil.Errorf(`cannot cast %q as array: %w`, v.V, err) } return NewArrayValue(&vb), nil } - return Value{}, fmt.Errorf("cannot cast %s as array", v.Type) + return Value{}, stringutil.Errorf("cannot cast %s as array", v.Type) } // CastAsDocument casts according to the following rules: @@ -193,11 +194,11 @@ func (v Value) CastAsDocument() (Value, error) { var fb FieldBuffer err := fb.UnmarshalJSON([]byte(v.V.(string))) if err != nil { - return Value{}, fmt.Errorf(`cannot cast %q as document: %w`, v.V, err) + return Value{}, stringutil.Errorf(`cannot cast %q as document: %w`, v.V, err) } return NewDocumentValue(&fb), nil } - return Value{}, fmt.Errorf("cannot cast %s as document", v.Type) + return Value{}, stringutil.Errorf("cannot cast %s as document", v.Type) } diff --git a/document/create.go b/document/create.go index e606de9ed..634f59acc 100644 --- a/document/create.go +++ b/document/create.go @@ -4,13 +4,13 @@ package document import ( "errors" - "fmt" "math" "reflect" "strings" "time" "github.com/buger/jsonparser" + "github.com/genjidb/genji/stringutil" ) // NewFromJSON creates a document from raw JSON data. @@ -198,7 +198,7 @@ func NewValue(x interface{}) (Value, error) { case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: x := v.Uint() if x > math.MaxInt64 { - return Value{}, fmt.Errorf("cannot convert unsigned integer struct field to int64: %d out of range", x) + return Value{}, stringutil.Errorf("cannot convert unsigned integer struct field to int64: %d out of range", x) } return NewIntegerValue(int64(x)), nil case reflect.Float32, reflect.Float64: diff --git a/document/encoding/msgpack/encoding.go b/document/encoding/msgpack/encoding.go index 133c693cc..0594ffab1 100644 --- a/document/encoding/msgpack/encoding.go +++ b/document/encoding/msgpack/encoding.go @@ -2,9 +2,9 @@ package msgpack import ( "bytes" - "fmt" "github.com/genjidb/genji/document" + "github.com/genjidb/genji/stringutil" "github.com/vmihailenco/msgpack/v5" "github.com/vmihailenco/msgpack/v5/msgpcode" ) @@ -29,7 +29,7 @@ func bytesLen(c byte, dec *msgpack.Decoder) (int, error) { return int(c & msgpcode.FixedStrMask), nil } - return 0, fmt.Errorf("msgpack: invalid code=%x decoding bytes length", c) + return 0, stringutil.Errorf("msgpack: invalid code=%x decoding bytes length", c) } // GetByField decodes the selected field from the buffer. diff --git a/document/scan.go b/document/scan.go index e71876667..bdc389b47 100644 --- a/document/scan.go +++ b/document/scan.go @@ -8,6 +8,8 @@ import ( "reflect" "strings" "time" + + "github.com/genjidb/genji/stringutil" ) // A Scanner can iterate over a document and scan all the fields. @@ -268,7 +270,7 @@ func scanValue(v Value, ref reflect.Value) error { } x := v.V.(int64) if x < 0 { - return fmt.Errorf("cannot convert value %d into Go value of type %s", x, ref.Type().Name()) + return stringutil.Errorf("cannot convert value %d into Go value of type %s", x, ref.Type().Name()) } ref.SetUint(uint64(x)) return nil @@ -333,7 +335,7 @@ func scanValue(v Value, ref reflect.Value) error { case reflect.Slice: if ref.Type().Elem().Kind() == reflect.Uint8 { if v.Type != TextValue && v.Type != BlobValue { - return fmt.Errorf("cannot scan value of type %s to byte slice", v.Type) + return stringutil.Errorf("cannot scan value of type %s to byte slice", v.Type) } if v.Type == TextValue { ref.SetBytes([]byte(v.V.(string))) @@ -351,7 +353,7 @@ func scanValue(v Value, ref reflect.Value) error { case reflect.Array: if ref.Type().Elem().Kind() == reflect.Uint8 { if v.Type != TextValue && v.Type != BlobValue { - return fmt.Errorf("cannot scan value of type %s to byte slice", v.Type) + return stringutil.Errorf("cannot scan value of type %s to byte slice", v.Type) } reflect.Copy(ref, reflect.ValueOf(v.V)) return nil diff --git a/expr/env.go b/expr/env.go index 20fe3f9a5..6015ec666 100644 --- a/expr/env.go +++ b/expr/env.go @@ -1,10 +1,9 @@ package expr import ( - "fmt" - "github.com/genjidb/genji/database" "github.com/genjidb/genji/document" + "github.com/genjidb/genji/stringutil" ) // Environment contains information about the context in which @@ -79,7 +78,7 @@ func (e *Environment) GetParamByName(name string) (v document.Value, err error) } } - return document.Value{}, fmt.Errorf("param %s not found", name) + return document.Value{}, stringutil.Errorf("param %s not found", name) } func (e *Environment) GetParamByIndex(pos int) (document.Value, error) { @@ -91,7 +90,7 @@ func (e *Environment) GetParamByIndex(pos int) (document.Value, error) { idx := int(pos - 1) if idx >= len(e.Params) { - return document.Value{}, fmt.Errorf("cannot find param number %d", pos) + return document.Value{}, stringutil.Errorf("cannot find param number %d", pos) } return document.NewValue(e.Params[idx].Value) diff --git a/expr/function.go b/expr/function.go index 2e3b2077d..520d1b8a2 100644 --- a/expr/function.go +++ b/expr/function.go @@ -6,6 +6,7 @@ import ( "strings" "github.com/genjidb/genji/document" + "github.com/genjidb/genji/stringutil" ) // Functions represents a map of builtin SQL functions. @@ -18,37 +19,37 @@ func BuiltinFunctions() map[string]func(args ...Expr) (Expr, error) { return map[string]func(args ...Expr) (Expr, error){ "pk": func(args ...Expr) (Expr, error) { if len(args) != 0 { - return nil, fmt.Errorf("pk() takes no arguments") + return nil, stringutil.Errorf("pk() takes no arguments") } return new(PKFunc), nil }, "count": func(args ...Expr) (Expr, error) { if len(args) != 1 { - return nil, fmt.Errorf("COUNT() takes 1 argument") + return nil, stringutil.Errorf("COUNT() takes 1 argument") } return &CountFunc{Expr: args[0]}, nil }, "min": func(args ...Expr) (Expr, error) { if len(args) != 1 { - return nil, fmt.Errorf("MIN() takes 1 argument") + return nil, stringutil.Errorf("MIN() takes 1 argument") } return &MinFunc{Expr: args[0]}, nil }, "max": func(args ...Expr) (Expr, error) { if len(args) != 1 { - return nil, fmt.Errorf("MAX() takes 1 argument") + return nil, stringutil.Errorf("MAX() takes 1 argument") } return &MaxFunc{Expr: args[0]}, nil }, "sum": func(args ...Expr) (Expr, error) { if len(args) != 1 { - return nil, fmt.Errorf("SUM() takes 1 argument") + return nil, stringutil.Errorf("SUM() takes 1 argument") } return &SumFunc{Expr: args[0]}, nil }, "avg": func(args ...Expr) (Expr, error) { if len(args) != 1 { - return nil, fmt.Errorf("AVG() takes 1 argument") + return nil, stringutil.Errorf("AVG() takes 1 argument") } return &AvgFunc{Expr: args[0]}, nil }, @@ -70,7 +71,7 @@ func (f Functions) AddFunc(name string, fn func(args ...Expr) (Expr, error)) { func (f Functions) GetFunc(name string, args ...Expr) (Expr, error) { fn, ok := f.m[strings.ToLower(name)] if !ok { - return nil, fmt.Errorf("no such function: %q", name) + return nil, stringutil.Errorf("no such function: %q", name) } return fn(args...) diff --git a/sql/parser/delete.go b/sql/parser/delete.go index 0df6a0243..f24abec05 100644 --- a/sql/parser/delete.go +++ b/sql/parser/delete.go @@ -1,12 +1,11 @@ package parser import ( - "fmt" - "github.com/genjidb/genji/expr" "github.com/genjidb/genji/planner" "github.com/genjidb/genji/sql/scanner" "github.com/genjidb/genji/stream" + "github.com/genjidb/genji/stringutil" ) // parseDeleteStatement parses a delete string and returns a Statement AST object. @@ -87,7 +86,7 @@ func (cfg deleteConfig) ToStream() (*planner.Statement, error) { } if !v.Type.IsNumber() { - return nil, fmt.Errorf("offset expression must evaluate to a number, got %q", v.Type) + return nil, stringutil.Errorf("offset expression must evaluate to a number, got %q", v.Type) } v, err = v.CastAsInteger() @@ -105,7 +104,7 @@ func (cfg deleteConfig) ToStream() (*planner.Statement, error) { } if !v.Type.IsNumber() { - return nil, fmt.Errorf("limit expression must evaluate to a number, got %q", v.Type) + return nil, stringutil.Errorf("limit expression must evaluate to a number, got %q", v.Type) } v, err = v.CastAsInteger() diff --git a/sql/parser/expr.go b/sql/parser/expr.go index fdf7b3dc3..3a4972af7 100644 --- a/sql/parser/expr.go +++ b/sql/parser/expr.go @@ -9,6 +9,7 @@ import ( "github.com/genjidb/genji/document" "github.com/genjidb/genji/expr" "github.com/genjidb/genji/sql/scanner" + "github.com/genjidb/genji/stringutil" ) type dummyOperator struct { @@ -368,7 +369,7 @@ func (p *Parser) parseDocument() (*expr.KVPairs, error) { } if _, ok := fields[pair.K]; ok { - return nil, fmt.Errorf("duplicate field %q", pair.K) + return nil, stringutil.Errorf("duplicate field %q", pair.K) } fields[pair.K] = struct{}{} diff --git a/sql/parser/insert.go b/sql/parser/insert.go index 574d620d7..9f3b27cb1 100644 --- a/sql/parser/insert.go +++ b/sql/parser/insert.go @@ -1,12 +1,11 @@ package parser import ( - "fmt" - "github.com/genjidb/genji/expr" "github.com/genjidb/genji/planner" "github.com/genjidb/genji/sql/scanner" "github.com/genjidb/genji/stream" + "github.com/genjidb/genji/stringutil" ) // parseInsertStatement parses an insert string and returns a Statement AST object. @@ -122,7 +121,7 @@ func (p *Parser) parseExprListWithFields(fields []string) (*expr.KVPairs, error) pairs.Pairs = make([]expr.KVPair, len(list)) if len(fields) != len(list) { - return nil, fmt.Errorf("%d values for %d fields", len(list), len(fields)) + return nil, stringutil.Errorf("%d values for %d fields", len(list), len(fields)) } for i := range list { diff --git a/sql/parser/select.go b/sql/parser/select.go index 3757d4272..c312193c2 100644 --- a/sql/parser/select.go +++ b/sql/parser/select.go @@ -2,13 +2,13 @@ package parser import ( "errors" - "fmt" "github.com/genjidb/genji/expr" "github.com/genjidb/genji/planner" "github.com/genjidb/genji/query" "github.com/genjidb/genji/sql/scanner" "github.com/genjidb/genji/stream" + "github.com/genjidb/genji/stringutil" ) // parseSelectStatement parses a select string and returns a Statement AST object. @@ -230,7 +230,7 @@ func (cfg selectConfig) ToStream() (*planner.Statement, error) { } if invalidProjectedField != nil { - return nil, fmt.Errorf("field %q must appear in the GROUP BY clause or be used in an aggregate function", invalidProjectedField) + return nil, stringutil.Errorf("field %q must appear in the GROUP BY clause or be used in an aggregate function", invalidProjectedField) } // add Aggregation node @@ -300,7 +300,7 @@ func (cfg selectConfig) ToStream() (*planner.Statement, error) { } if !v.Type.IsNumber() { - return nil, fmt.Errorf("offset expression must evaluate to a number, got %q", v.Type) + return nil, stringutil.Errorf("offset expression must evaluate to a number, got %q", v.Type) } v, err = v.CastAsInteger() @@ -318,7 +318,7 @@ func (cfg selectConfig) ToStream() (*planner.Statement, error) { } if !v.Type.IsNumber() { - return nil, fmt.Errorf("limit expression must evaluate to a number, got %q", v.Type) + return nil, stringutil.Errorf("limit expression must evaluate to a number, got %q", v.Type) } v, err = v.CastAsInteger() diff --git a/sql/scanner/scanner.go b/sql/scanner/scanner.go index 47ea25396..28a29fd64 100644 --- a/sql/scanner/scanner.go +++ b/sql/scanner/scanner.go @@ -3,9 +3,10 @@ package scanner import ( "bytes" "errors" - "fmt" "io" "unicode/utf8" + + "github.com/genjidb/genji/stringutil" ) // Code heavily inspired by the influxdata/influxql repository @@ -534,7 +535,7 @@ func ScanDelimited(r io.RuneScanner, start, end rune, escapes map[rune]rune, esc if ch, _, err := r.ReadRune(); err != nil { return nil, err } else if ch != start { - return nil, fmt.Errorf("expected %s; found %s", string(start), string(ch)) + return nil, stringutil.Errorf("expected %s; found %s", string(start), string(ch)) } var buf bytes.Buffer diff --git a/stringutil/errorf.go b/stringutil/errorf.go new file mode 100644 index 000000000..e9112d92d --- /dev/null +++ b/stringutil/errorf.go @@ -0,0 +1,71 @@ +package stringutil + +import ( + "errors" + "strconv" + "strings" +) + +type stringer interface { + String() string +} + +func errorF(msg string, args ...interface{}) error { + var count int + var b strings.Builder + + for i := 0; i < len(msg); i++ { + if msg[i] == '%' { + if i+1 < len(msg) { + switch msg[i+1] { + case 's', 'd', 'v', 'w', 'c': + b.WriteString(toString(args[count])) + i++ + count++ + + continue + case 'q': + b.WriteString(strconv.Quote(toString(args[count]))) + i++ + count++ + + continue + } + } + } + + b.WriteByte(msg[i]) + } + + return errors.New(b.String()) +} + +func toString(v interface{}) string { + switch t := v.(type) { + case byte: + return string(t) + case rune: + return string(t) + case string: + return t + case []byte: + var b strings.Builder + b.WriteByte('[') + for i := range t { + if i > 0 { + b.WriteString(", ") + } + b.WriteString(strconv.Itoa(int(t[i]))) + } + b.WriteByte(']') + return b.String() + case stringer: + return t.String() + case int: + return strconv.Itoa(t) + case error: + return t.Error() + default: + panic("incompatible type") + } +} diff --git a/stringutil/errorf_test.go b/stringutil/errorf_test.go new file mode 100644 index 000000000..58d8e5719 --- /dev/null +++ b/stringutil/errorf_test.go @@ -0,0 +1,32 @@ +package stringutil + +import ( + "bytes" + "errors" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestErrorf(t *testing.T) { + tests := []struct { + name string + msg string + args []interface{} + expected string + }{ + {"no args", "foo", nil, "foo"}, + {"%s", "foo %s %s %d %v %c %v", []interface{}{"a", bytes.NewBufferString("b"), 3, 4, '6', []byte{1, 2}}, "foo a b 3 4 6 [1, 2]"}, + {"%q", "foo %q", []interface{}{"a"}, "foo \"a\""}, + {"%w", "foo %w %w", []interface{}{"a", errors.New("b")}, "foo a b"}, + {"%z", "foo %z", []interface{}{"a"}, "foo %z"}, + {"%s %q", "foo %s %q", []interface{}{"a", "b"}, "foo a \"b\""}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := errorF(test.msg, test.args...) + require.EqualError(t, err, test.expected) + }) + } +} diff --git a/stringutil/stringutil.go b/stringutil/stringutil.go new file mode 100644 index 000000000..cc8e48b55 --- /dev/null +++ b/stringutil/stringutil.go @@ -0,0 +1,11 @@ +// +build !wasm + +package stringutil + +import "fmt" + +// Errorf calls stringutil.Errorf. +// In wasm build it is replaced by a custom version. +func Errorf(format string, a ...interface{}) error { + return fmt.Errorf(format, a...) +} diff --git a/stringutil/stringutil_wasm.go b/stringutil/stringutil_wasm.go new file mode 100644 index 000000000..e1bded732 --- /dev/null +++ b/stringutil/stringutil_wasm.go @@ -0,0 +1,11 @@ +package errorutil + +import ( + "errors" + "strconv" + "strings" +) + +func Errorf(msg string, args ...string) error { + return errorF(msg, args...) +}