Repository: genjidb/genji
Pull request #419 (2021-07-09 16:23:18)

Add functions packages support
This PR adds packaged function support. I've taken that opportunity to also add a `.doc`  helper for the shell, so we can get the docs at the runtime. 

Packaging is expressed as following, `pkgname.funcname(...)` and builtin functions are living in a package whose name is the empty string.

The doc can be attached to tokens, with a special case for functions and packages. In case of an ambiguous `BY` as in `GROUP BY` and `ORDER BY`, I think the best approach is just to document it with a "see also ...".
```
genji> .doc math.floor
math.floor(arg1): Returns the greatest integer value less than or equal to arg1.
genji>
```

@asdine I explored the idea of using comments to document the functions that came up when we were discussing it but I decided to go against it. While it's awesome to have the doc in place to keep it updated, the go comments as a way of documenting the sql functions leads to an awkward result in practice: if you use them  to document Genji's functions, where do you put the comments for their implementation? So, I just went for the simplest approach. 

codecov-commenter: # [Codecov](https://codecov.io/gh/genjidb/genji/pull/419?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=genjidb) Report
> Merging [#419](https://codecov.io/gh/genjidb/genji/pull/419?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=genjidb) (172cda5) into [main](https://codecov.io/gh/genjidb/genji/commit/fa66f81fb62a74f2592ffb6758aaffdfbdb3a81c?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=genjidb) (fa66f81) will **increase** coverage by `0.30%`.
> The diff coverage is `34.12%`.

[![Impacted file tree graph](https://codecov.io/gh/genjidb/genji/pull/419/graphs/tree.svg?width=650&height=150&src=pr&token=nwR1GpvtyH&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=genjidb)](https://codecov.io/gh/genjidb/genji/pull/419?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=genjidb)

```diff
@@            Coverage Diff             @@
##             main     #419      +/-   ##
==========================================
+ Coverage   67.01%   67.31%   +0.30%     
==========================================
  Files          87       92       +5     
  Lines        8349     8448      +99     
==========================================
+ Hits         5595     5687      +92     
- Misses       2045     2046       +1     
- Partials      709      715       +6     
```


| [Impacted Files](https://codecov.io/gh/genjidb/genji/pull/419?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=genjidb) | Coverage Δ | |
|---|---|---|
| [internal/sql/scanner/scanner.go](https://codecov.io/gh/genjidb/genji/pull/419/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=genjidb#diff-aW50ZXJuYWwvc3FsL3NjYW5uZXIvc2Nhbm5lci5nbw==) | `82.42% <ø> (ø)` | |
| [internal/sql/scanner/token.go](https://codecov.io/gh/genjidb/genji/pull/419/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=genjidb#diff-aW50ZXJuYWwvc3FsL3NjYW5uZXIvdG9rZW4uZ28=) | `9.67% <0.00%> (-1.04%)` | :arrow_down: |
| [internal/expr/builtin\_funcs.go](https://codecov.io/gh/genjidb/genji/pull/419/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=genjidb#diff-aW50ZXJuYWwvZXhwci9idWlsdGluX2Z1bmNzLmdv) | `7.90% <7.90%> (ø)` | |
| [internal/expr/scalar\_function.go](https://codecov.io/gh/genjidb/genji/pull/419/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=genjidb#diff-aW50ZXJuYWwvZXhwci9zY2FsYXJfZnVuY3Rpb24uZ28=) | `73.91% <73.91%> (ø)` | |
| [internal/sql/parser/expr.go](https://codecov.io/gh/genjidb/genji/pull/419/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=genjidb#diff-aW50ZXJuYWwvc3FsL3BhcnNlci9leHByLmdv) | `79.85% <75.75%> (-0.56%)` | :arrow_down: |
| [doc/doc.go](https://codecov.io/gh/genjidb/genji/pull/419/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=genjidb#diff-ZG9jL2RvYy5nbw==) | `83.87% <83.87%> (ø)` | |
| [internal/expr/function.go](https://codecov.io/gh/genjidb/genji/pull/419/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=genjidb#diff-aW50ZXJuYWwvZXhwci9mdW5jdGlvbi5nbw==) | `90.00% <88.88%> (+81.88%)` | :arrow_up: |
| [doc/tokens.go](https://codecov.io/gh/genjidb/genji/pull/419/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=genjidb#diff-ZG9jL3Rva2Vucy5nbw==) | `100.00% <100.00%> (ø)` | |
| [internal/expr/math\_functions.go](https://codecov.io/gh/genjidb/genji/pull/419/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=genjidb#diff-aW50ZXJuYWwvZXhwci9tYXRoX2Z1bmN0aW9ucy5nbw==) | `100.00% <100.00%> (ø)` | |
| [internal/sql/parser/options.go](https://codecov.io/gh/genjidb/genji/pull/419/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=genjidb#diff-aW50ZXJuYWwvc3FsL3BhcnNlci9vcHRpb25zLmdv) | `100.00% <100.00%> (ø)` | |
| ... and [6 more](https://codecov.io/gh/genjidb/genji/pull/419/diff?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=genjidb) | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/genjidb/genji/pull/419?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=genjidb).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=genjidb)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/genjidb/genji/pull/419?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=genjidb). Last update [fa66f81...172cda5](https://codecov.io/gh/genjidb/genji/pull/419?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=genjidb). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=genjidb).


Patch:
diff --git a/cmd/genji/doc/doc.go b/cmd/genji/doc/doc.go
new file mode 100644
index 00000000..07018bc0
--- /dev/null
+++ b/cmd/genji/doc/doc.go
@@ -0,0 +1,74 @@
+/* Package doc provides an API to access documentation for functions and tokens */
+package doc
+
+import (
+	"errors"
+	"fmt"
+	"strings"
+
+	"github.com/genjidb/genji/internal/expr/functions"
+	"github.com/genjidb/genji/internal/sql/scanner"
+)
+
+var ErrNotFound = errors.New("No documentation found")
+var ErrInvalid = errors.New("Invalid documentation query")
+
+// DocString returns a string containing the documentation for a given expression.
+//
+// The expression is merely scanned and not parsed, looking for keywords tokens or IDENT
+// tokens forming a function. In that last case, a function lookup is performed, yielding the
+// documentation of that particular function.
+func DocString(rawExpr string) (string, error) {
+	if rawExpr == "" {
+		return "", ErrInvalid
+	}
+	s := scanner.NewScanner(strings.NewReader(rawExpr))
+	tok, _, _ := s.Scan()
+	if tok == scanner.ILLEGAL {
+		return "", ErrInvalid
+	}
+	if tok == scanner.IDENT {
+		s.Unscan()
+		return scanFuncDocString(s)
+	}
+	docstr, ok := tokenDocs[tok]
+	if ok {
+		return docstr, nil
+	}
+	return "", ErrNotFound
+}
+
+func scanFuncDocString(s *scanner.Scanner) (string, error) {
+	tok1, _, lit1 := s.Scan()
+	if tok1 != scanner.IDENT {
+		return "", ErrInvalid
+	}
+	tok2, _, _ := s.Scan()
+	if tok2 != scanner.EOF && tok2 == scanner.DOT {
+		// tok1 is a package because tok2 is a "."
+		tok3, _, lit3 := s.Scan()
+		if tok3 != scanner.IDENT {
+			return "", ErrInvalid
+		}
+		return funcDocString(lit1, lit3)
+	} else {
+		// no package, it's a builtin function
+		return funcDocString("", lit1)
+	}
+}
+
+func funcDocString(pkg string, name string) (string, error) {
+	table := functions.DefaultPackages()
+	f, err := table.GetFunc(pkg, name)
+	if err != nil {
+		return "", fmt.Errorf("%w: %v", ErrNotFound, err.Error())
+	}
+	// Because we got the definition, we know that the package and function both exist.
+	p := packageDocs[pkg]
+	d := p[name]
+	if pkg != "" {
+		return fmt.Sprintf("%s.%s: %s", pkg, f.String(), d), nil
+	} else {
+		return fmt.Sprintf("%s: %s", f.String(), d), nil
+	}
+}
diff --git a/cmd/genji/doc/functions.go b/cmd/genji/doc/functions.go
new file mode 100644
index 00000000..36417ac8
--- /dev/null
+++ b/cmd/genji/doc/functions.go
@@ -0,0 +1,21 @@
+package doc
+
+type functionDocs map[string]string
+
+var packageDocs = map[string]functionDocs{
+	"math": mathDocs,
+	"":     builtinDocs,
+}
+
+var builtinDocs = functionDocs{
+	"pk":    "The pk() function returns the primary key for the current document",
+	"count": "Returns a count of the number of times that arg1 is not NULL in a group. The count(*) function (with no arguments) returns the total number of rows in the group.",
+	"min":   "Returns the minimum value in a group.",
+	"max":   "Returns the maximum value in a group.",
+	"sum":   "The sum function returns the sum of all values in a group.",
+	"avg":   "The avg function returns the average of all values in a group.",
+}
+
+var mathDocs = functionDocs{
+	"floor": "Returns the greatest integer value less than or equal to arg1.",
+}
diff --git a/cmd/genji/doc/tokens.go b/cmd/genji/doc/tokens.go
new file mode 100644
index 00000000..3309d8fd
--- /dev/null
+++ b/cmd/genji/doc/tokens.go
@@ -0,0 +1,17 @@
+package doc
+
+import "github.com/genjidb/genji/internal/sql/scanner"
+
+var tokenDocs map[scanner.Token]string
+
+func init() {
+	tokenDocs = make(map[scanner.Token]string)
+	// let's make sure the doc doesn't suggest that a keyword doesn't exist because
+	// it has no doc defined.
+	for _, tok := range scanner.AllKeywords() {
+		tokenDocs[tok] = "TODO"
+	}
+
+	tokenDocs[scanner.BY] = "See GROUP BY, ORDER BY"
+	tokenDocs[scanner.FROM] = "FROM [TABLE] selects documents in the table named [TABLE]"
+}
diff --git a/cmd/genji/shell/command.go b/cmd/genji/shell/command.go
index 2cda1125..ca821f7e 100644
--- a/cmd/genji/shell/command.go
+++ b/cmd/genji/shell/command.go
@@ -12,6 +12,7 @@ import (
 
 	"github.com/genjidb/genji"
 	"github.com/genjidb/genji/cmd/genji/dbutil"
+	"github.com/genjidb/genji/cmd/genji/doc"
 	"github.com/genjidb/genji/document"
 	errs "github.com/genjidb/genji/errors"
 )
@@ -58,6 +59,12 @@ var commands = []command{
 		DisplayName: ".dump",
 		Description: "Dump database content or table content as SQL statements.",
 	},
+	{
+		Name:        ".doc",
+		Options:     "[function_name]",
+		DisplayName: ".doc",
+		Description: "Display inline documentation for a function",
+	},
 	{
 		Name:        ".save",
 		Options:     "[badger] [filename]",
@@ -100,6 +107,16 @@ func runHelpCmd() error {
 	return nil
 }
 
+// runDocCommand prints the docstring for a given function
+func runDocCmd(expr string) error {
+	doc, err := doc.DocString(expr)
+	if err != nil {
+		return err
+	}
+	fmt.Printf("%s\n", doc)
+	return nil
+}
+
 // runTablesCmd displays all tables.
 func runTablesCmd(db *genji.DB, w io.Writer) error {
 	res, err := db.Query("SELECT name FROM __genji_catalog WHERE type = 'table' AND name != '__genji_sequence'")
diff --git a/cmd/genji/shell/shell.go b/cmd/genji/shell/shell.go
index c7f72d88..ce8ac26e 100644
--- a/cmd/genji/shell/shell.go
+++ b/cmd/genji/shell/shell.go
@@ -491,6 +491,11 @@ func (sh *Shell) runCommand(ctx context.Context, in string) error {
 		}
 
 		return runImportCmd(ctx, sh.db, cmd[1], cmd[2], cmd[3])
+	case ".doc":
+		if len(cmd) != 2 {
+			return fmt.Errorf(getUsage(".doc"))
+		}
+		return runDocCmd(cmd[1])
 	default:
 		return displaySuggestions(in)
 	}
diff --git a/internal/expr/expr.go b/internal/expr/expr.go
index 3f690762..f375b0da 100644
--- a/internal/expr/expr.go
+++ b/internal/expr/expr.go
@@ -103,6 +103,28 @@ func (e *NamedExpr) String() string {
 	return stringutil.Sprintf("%s", e.Expr)
 }
 
+// A Function is an expression whose evaluation calls a function previously defined.
+type Function interface {
+	Expr
+
+	// Returns the list of parameters this function has received.
+	Params() []Expr
+}
+
+// A Aggregator is an expression that aggregates documents into one result.
+type Aggregator interface {
+	Expr
+
+	Aggregate(env *environment.Environment) error
+}
+
+// An AggregatorBuilder is a type that can create aggregators.
+type AggregatorBuilder interface {
+	Expr
+
+	Aggregator() Aggregator
+}
+
 func Walk(e Expr, fn func(Expr) bool) bool {
 	if e == nil {
 		return true
diff --git a/internal/expr/function.go b/internal/expr/functions/builtins.go
similarity index 63%
rename from internal/expr/function.go
rename to internal/expr/functions/builtins.go
index 090d1950..90944fff 100644
--- a/internal/expr/function.go
+++ b/internal/expr/functions/builtins.go
@@ -1,145 +1,105 @@
-package expr
+package functions
 
 import (
 	"errors"
-	"strings"
 
 	"github.com/genjidb/genji/document"
 	"github.com/genjidb/genji/internal/environment"
+	"github.com/genjidb/genji/internal/expr"
 	"github.com/genjidb/genji/internal/stringutil"
 )
 
-// A Function is an expression that execute some arbitrary code.
-type Function interface {
-	Expr
-
-	// Returns the list of parameters this function has received.
-	Params() []Expr
-}
-
-// Functions represents a map of builtin SQL functions.
-type Functions struct {
-	m map[string]func(args ...Expr) (Expr, error)
-}
-
-// BuiltinFunctions returns default map of builtin functions.
-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, stringutil.Errorf("pk() takes no arguments")
-			}
-			return new(PKFunc), nil
+var builtinFunctions = Definitions{
+	"pk": &definition{
+		name:  "pk",
+		arity: 0,
+		constructorFn: func(args ...expr.Expr) (expr.Function, error) {
+			return &PK{}, nil
 		},
-		"count": func(args ...Expr) (Expr, error) {
-			if len(args) != 1 {
-				return nil, stringutil.Errorf("COUNT() takes 1 argument")
-			}
-			return &CountFunc{Expr: args[0]}, nil
+	},
+	"count": &definition{
+		name:  "count",
+		arity: 1,
+		constructorFn: func(args ...expr.Expr) (expr.Function, error) {
+			return &Count{Expr: args[0]}, nil
 		},
-		"min": func(args ...Expr) (Expr, error) {
-			if len(args) != 1 {
-				return nil, stringutil.Errorf("MIN() takes 1 argument")
-			}
-			return &MinFunc{Expr: args[0]}, nil
+	},
+	"min": &definition{
+		name:  "min",
+		arity: 1,
+		constructorFn: func(args ...expr.Expr) (expr.Function, error) {
+			return &Min{Expr: args[0]}, nil
 		},
-		"max": func(args ...Expr) (Expr, error) {
-			if len(args) != 1 {
-				return nil, stringutil.Errorf("MAX() takes 1 argument")
-			}
-			return &MaxFunc{Expr: args[0]}, nil
+	},
+	"max": &definition{
+		name:  "max",
+		arity: 1,
+		constructorFn: func(args ...expr.Expr) (expr.Function, error) {
+			return &Max{Expr: args[0]}, nil
 		},
-		"sum": func(args ...Expr) (Expr, error) {
-			if len(args) != 1 {
-				return nil, stringutil.Errorf("SUM() takes 1 argument")
-			}
-			return &SumFunc{Expr: args[0]}, nil
+	},
+	"sum": &definition{
+		name:  "sum",
+		arity: 1,
+		constructorFn: func(args ...expr.Expr) (expr.Function, error) {
+			return &Sum{Expr: args[0]}, nil
 		},
-		"avg": func(args ...Expr) (Expr, error) {
-			if len(args) != 1 {
-				return nil, stringutil.Errorf("AVG() takes 1 argument")
-			}
-			return &AvgFunc{Expr: args[0]}, nil
+	},
+	"avg": &definition{
+		name:  "avg",
+		arity: 1,
+		constructorFn: func(args ...expr.Expr) (expr.Function, error) {
+			return &Avg{Expr: args[0]}, nil
 		},
-	}
-}
-
-func NewFunctions() Functions {
-	return Functions{
-		m: BuiltinFunctions(),
-	}
-}
-
-// AddFunc adds function to the map.
-func (f Functions) AddFunc(name string, fn func(args ...Expr) (Expr, error)) {
-	f.m[name] = fn
-}
-
-// GetFunc return a function expression by name.
-func (f Functions) GetFunc(name string, args ...Expr) (Expr, error) {
-	fn, ok := f.m[strings.ToLower(name)]
-	if !ok {
-		return nil, stringutil.Errorf("no such function: %q", name)
-	}
-
-	return fn(args...)
+	},
 }
 
-// A Aggregator is an expression that aggregates documents into one result.
-type Aggregator interface {
-	Expr
-
-	Aggregate(env *environment.Environment) error
-}
-
-// An AggregatorBuilder is a type that can create aggregators.
-type AggregatorBuilder interface {
-	Expr
-
-	Aggregator() Aggregator
+// BuiltinDefinitions returns a map of builtin functions.
+func BuiltinDefinitions() Definitions {
+	return builtinFunctions
 }
 
-// PKFunc represents the pk() function.
+// PK represents the pk() function.
 // It returns the primary key of the current document.
-type PKFunc struct{}
+type PK struct{}
 
 // Eval returns the primary key of the current document.
-func (k *PKFunc) Eval(env *environment.Environment) (document.Value, error) {
+func (k *PK) Eval(env *environment.Environment) (document.Value, error) {
 	d, ok := env.GetDocument()
 	if !ok {
-		return NullLiteral, nil
+		return expr.NullLiteral, nil
 	}
 
 	keyer, ok := d.(document.Keyer)
 	if !ok {
-		return NullLiteral, nil
+		return expr.NullLiteral, nil
 	}
 
 	v, err := keyer.Key()
 	return v, err
 }
 
-func (*PKFunc) Params() []Expr { return nil }
+func (*PK) Params() []expr.Expr { return nil }
 
 // IsEqual compares this expression with the other expression and returns
 // true if they are equal.
-func (k *PKFunc) IsEqual(other Expr) bool {
-	_, ok := other.(*PKFunc)
+func (k *PK) IsEqual(other expr.Expr) bool {
+	_, ok := other.(*PK)
 	return ok
 }
 
-func (k *PKFunc) String() string {
+func (k *PK) String() string {
 	return "pk()"
 }
 
-// CastFunc represents the CAST expression.
-type CastFunc struct {
-	Expr   Expr
+// Cast represents the CAST expression.
+type Cast struct {
+	Expr   expr.Expr
 	CastAs document.ValueType
 }
 
 // Eval returns the primary key of the current document.
-func (c CastFunc) Eval(env *environment.Environment) (document.Value, error) {
+func (c Cast) Eval(env *environment.Environment) (document.Value, error) {
 	v, err := c.Expr.Eval(env)
 	if err != nil {
 		return v, err
@@ -150,12 +110,12 @@ func (c CastFunc) Eval(env *environment.Environment) (document.Value, error) {
 
 // IsEqual compares this expression with the other expression and returns
 // true if they are equal.
-func (c CastFunc) IsEqual(other Expr) bool {
+func (c Cast) IsEqual(other expr.Expr) bool {
 	if other == nil {
 		return false
 	}
 
-	o, ok := other.(CastFunc)
+	o, ok := other.(Cast)
 	if !ok {
 		return false
 	}
@@ -165,29 +125,29 @@ func (c CastFunc) IsEqual(other Expr) bool {
 	}
 
 	if c.Expr != nil {
-		return Equal(c.Expr, o.Expr)
+		return expr.Equal(c.Expr, o.Expr)
 	}
 
 	return o.Expr != nil
 }
 
-func (c CastFunc) Params() []Expr { return []Expr{c.Expr} }
+func (c Cast) Params() []expr.Expr { return []expr.Expr{c.Expr} }
 
-func (c CastFunc) String() string {
+func (c Cast) String() string {
 	return stringutil.Sprintf("CAST(%v AS %v)", c.Expr, c.CastAs)
 }
 
-var _ AggregatorBuilder = (*CountFunc)(nil)
+var _ expr.AggregatorBuilder = (*Count)(nil)
 
-// CountFunc is the COUNT aggregator function. It counts the number of documents
+// Count is the COUNT aggregator function. It counts the number of documents
 // in a stream.
-type CountFunc struct {
-	Expr     Expr
+type Count struct {
+	Expr     expr.Expr
 	Wildcard bool
 	Count    int64
 }
 
-func (c *CountFunc) Eval(env *environment.Environment) (document.Value, error) {
+func (c *Count) Eval(env *environment.Environment) (document.Value, error) {
 	d, ok := env.GetDocument()
 	if !ok {
 		return document.Value{}, errors.New("misuse of aggregation function COUNT()")
@@ -198,12 +158,12 @@ func (c *CountFunc) Eval(env *environment.Environment) (document.Value, error) {
 
 // IsEqual compares this expression with the other expression and returns
 // true if they are equal.
-func (c *CountFunc) IsEqual(other Expr) bool {
+func (c *Count) IsEqual(other expr.Expr) bool {
 	if other == nil {
 		return false
 	}
 
-	o, ok := other.(*CountFunc)
+	o, ok := other.(*Count)
 	if !ok {
 		return false
 	}
@@ -212,12 +172,12 @@ func (c *CountFunc) IsEqual(other Expr) bool {
 		return c.Expr == nil && o.Expr == nil
 	}
 
-	return Equal(c.Expr, o.Expr)
+	return expr.Equal(c.Expr, o.Expr)
 }
 
-func (c *CountFunc) Params() []Expr { return []Expr{c.Expr} }
+func (c *Count) Params() []expr.Expr { return []expr.Expr{c.Expr} }
 
-func (c *CountFunc) String() string {
+func (c *Count) String() string {
 	if c.Wildcard {
 		return "COUNT(*)"
 	}
@@ -226,7 +186,7 @@ func (c *CountFunc) String() string {
 }
 
 // Aggregator returns a CountAggregator. It implements the AggregatorBuilder interface.
-func (c *CountFunc) Aggregator() Aggregator {
+func (c *Count) Aggregator() expr.Aggregator {
 	return &CountAggregator{
 		Fn: c,
 	}
@@ -234,7 +194,7 @@ func (c *CountFunc) Aggregator() Aggregator {
 
 // CountAggregator is an aggregator that counts non-null expressions.
 type CountAggregator struct {
-	Fn    *CountFunc
+	Fn    *Count
 	Count int64
 }
 
@@ -249,7 +209,7 @@ func (c *CountAggregator) Aggregate(env *environment.Environment) error {
 	if err != nil && err != document.ErrFieldNotFound {
 		return err
 	}
-	if v != NullLiteral {
+	if v != expr.NullLiteral {
 		c.Count++
 	}
 
@@ -265,13 +225,13 @@ func (c *CountAggregator) String() string {
 	return c.Fn.String()
 }
 
-// MinFunc is the MIN aggregator function.
-type MinFunc struct {
-	Expr Expr
+// Min is the MIN aggregator function.
+type Min struct {
+	Expr expr.Expr
 }
 
 // Eval extracts the min value from the given document and returns it.
-func (m *MinFunc) Eval(env *environment.Environment) (document.Value, error) {
+func (m *Min) Eval(env *environment.Environment) (document.Value, error) {
 	d, ok := env.GetDocument()
 	if !ok {
 		return document.Value{}, errors.New("misuse of aggregation function MIN()")
@@ -282,29 +242,29 @@ func (m *MinFunc) Eval(env *environment.Environment) (document.Value, error) {
 
 // IsEqual compares this expression with the other expression and returns
 // true if they are equal.
-func (m *MinFunc) IsEqual(other Expr) bool {
+func (m *Min) IsEqual(other expr.Expr) bool {
 	if other == nil {
 		return false
 	}
 
-	o, ok := other.(*MinFunc)
+	o, ok := other.(*Min)
 	if !ok {
 		return false
 	}
 
-	return Equal(m.Expr, o.Expr)
+	return expr.Equal(m.Expr, o.Expr)
 }
 
-func (m *MinFunc) Params() []Expr { return []Expr{m.Expr} }
+func (m *Min) Params() []expr.Expr { return []expr.Expr{m.Expr} }
 
 // String returns the alias if non-zero, otherwise it returns a string representation
 // of the count expression.
-func (m *MinFunc) String() string {
+func (m *Min) String() string {
 	return stringutil.Sprintf("MIN(%v)", m.Expr)
 }
 
 // Aggregator returns a MinAggregator. It implements the AggregatorBuilder interface.
-func (m *MinFunc) Aggregator() Aggregator {
+func (m *Min) Aggregator() expr.Aggregator {
 	return &MinAggregator{
 		Fn: m,
 	}
@@ -312,7 +272,7 @@ func (m *MinFunc) Aggregator() Aggregator {
 
 // MinAggregator is an aggregator that returns the minimum non-null value.
 type MinAggregator struct {
-	Fn  *MinFunc
+	Fn  *Min
 	Min document.Value
 }
 
@@ -323,7 +283,7 @@ func (m *MinAggregator) Aggregate(env *environment.Environment) error {
 	if err != nil && err != document.ErrFieldNotFound {
 		return err
 	}
-	if v == NullLiteral {
+	if v == expr.NullLiteral {
 		return nil
 	}
 
@@ -363,13 +323,13 @@ func (m *MinAggregator) String() string {
 	return m.Fn.String()
 }
 
-// MaxFunc is the MAX aggregator function.
-type MaxFunc struct {
-	Expr Expr
+// Max is the MAX aggregator function.
+type Max struct {
+	Expr expr.Expr
 }
 
 // Eval extracts the max value from the given document and returns it.
-func (m *MaxFunc) Eval(env *environment.Environment) (document.Value, error) {
+func (m *Max) Eval(env *environment.Environment) (document.Value, error) {
 	d, ok := env.GetDocument()
 	if !ok {
 		return document.Value{}, errors.New("misuse of aggregation function MAX()")
@@ -380,29 +340,29 @@ func (m *MaxFunc) Eval(env *environment.Environment) (document.Value, error) {
 
 // IsEqual compares this expression with the other expression and returns
 // true if they are equal.
-func (m *MaxFunc) IsEqual(other Expr) bool {
+func (m *Max) IsEqual(other expr.Expr) bool {
 	if other == nil {
 		return false
 	}
 
-	o, ok := other.(*MaxFunc)
+	o, ok := other.(*Max)
 	if !ok {
 		return false
 	}
 
-	return Equal(m.Expr, o.Expr)
+	return expr.Equal(m.Expr, o.Expr)
 }
 
-func (m *MaxFunc) Params() []Expr { return []Expr{m.Expr} }
+func (m *Max) Params() []expr.Expr { return []expr.Expr{m.Expr} }
 
 // String returns the alias if non-zero, otherwise it returns a string representation
 // of the count expression.
-func (m *MaxFunc) String() string {
+func (m *Max) String() string {
 	return stringutil.Sprintf("MAX(%v)", m.Expr)
 }
 
 // Aggregator returns a MaxAggregator. It implements the AggregatorBuilder interface.
-func (m *MaxFunc) Aggregator() Aggregator {
+func (m *Max) Aggregator() expr.Aggregator {
 	return &MaxAggregator{
 		Fn: m,
 	}
@@ -410,7 +370,7 @@ func (m *MaxFunc) Aggregator() Aggregator {
 
 // MaxAggregator is an aggregator that returns the minimum non-null value.
 type MaxAggregator struct {
-	Fn  *MaxFunc
+	Fn  *Max
 	Max document.Value
 }
 
@@ -421,7 +381,7 @@ func (m *MaxAggregator) Aggregate(env *environment.Environment) error {
 	if err != nil && err != document.ErrFieldNotFound {
 		return err
 	}
-	if v == NullLiteral {
+	if v == expr.NullLiteral {
 		return nil
 	}
 
@@ -462,13 +422,13 @@ func (m *MaxAggregator) String() string {
 	return m.Fn.String()
 }
 
-// SumFunc is the SUM aggregator function.
-type SumFunc struct {
-	Expr Expr
+// Sum is the SUM aggregator function.
+type Sum struct {
+	Expr expr.Expr
 }
 
 // Eval extracts the sum value from the given document and returns it.
-func (s *SumFunc) Eval(env *environment.Environment) (document.Value, error) {
+func (s *Sum) Eval(env *environment.Environment) (document.Value, error) {
 	d, ok := env.GetDocument()
 	if !ok {
 		return document.Value{}, errors.New("misuse of aggregation function SUM()")
@@ -479,29 +439,29 @@ func (s *SumFunc) Eval(env *environment.Environment) (document.Value, error) {
 
 // IsEqual compares this expression with the other expression and returns
 // true if they are equal.
-func (s *SumFunc) IsEqual(other Expr) bool {
+func (s *Sum) IsEqual(other expr.Expr) bool {
 	if other == nil {
 		return false
 	}
 
-	o, ok := other.(*SumFunc)
+	o, ok := other.(*Sum)
 	if !ok {
 		return false
 	}
 
-	return Equal(s.Expr, o.Expr)
+	return expr.Equal(s.Expr, o.Expr)
 }
 
-func (s *SumFunc) Params() []Expr { return []Expr{s.Expr} }
+func (s *Sum) Params() []expr.Expr { return []expr.Expr{s.Expr} }
 
 // String returns the alias if non-zero, otherwise it returns a string representation
 // of the count expression.
-func (s *SumFunc) String() string {
+func (s *Sum) String() string {
 	return stringutil.Sprintf("SUM(%v)", s.Expr)
 }
 
-// Aggregator returns a SumFunc. It implements the AggregatorBuilder interface.
-func (s *SumFunc) Aggregator() Aggregator {
+// Aggregator returns a Sum. It implements the AggregatorBuilder interface.
+func (s *Sum) Aggregator() expr.Aggregator {
 	return &SumAggregator{
 		Fn: s,
 	}
@@ -509,7 +469,7 @@ func (s *SumFunc) Aggregator() Aggregator {
 
 // SumAggregator is an aggregator that returns the minimum non-null value.
 type SumAggregator struct {
-	Fn   *SumFunc
+	Fn   *Sum
 	SumI *int64
 	SumF *float64
 }
@@ -572,13 +532,13 @@ func (s *SumAggregator) String() string {
 	return s.Fn.String()
 }
 
-// AvgFunc is the AVG aggregator function.
-type AvgFunc struct {
-	Expr Expr
+// Avg is the AVG aggregator function.
+type Avg struct {
+	Expr expr.Expr
 }
 
 // Eval extracts the average value from the given document and returns it.
-func (s *AvgFunc) Eval(env *environment.Environment) (document.Value, error) {
+func (s *Avg) Eval(env *environment.Environment) (document.Value, error) {
 	d, ok := env.GetDocument()
 	if !ok {
 		return document.Value{}, errors.New("misuse of aggregation function AVG()")
@@ -589,29 +549,29 @@ func (s *AvgFunc) Eval(env *environment.Environment) (document.Value, error) {
 
 // IsEqual compares this expression with the other expression and returns
 // true if they are equal.
-func (s *AvgFunc) IsEqual(other Expr) bool {
+func (s *Avg) IsEqual(other expr.Expr) bool {
 	if other == nil {
 		return false
 	}
 
-	o, ok := other.(*AvgFunc)
+	o, ok := other.(*Avg)
 	if !ok {
 		return false
 	}
 
-	return Equal(s.Expr, o.Expr)
+	return expr.Equal(s.Expr, o.Expr)
 }
 
-func (s *AvgFunc) Params() []Expr { return []Expr{s.Expr} }
+func (s *Avg) Params() []expr.Expr { return []expr.Expr{s.Expr} }
 
 // String returns the alias if non-zero, otherwise it returns a string representation
 // of the average expression.
-func (s *AvgFunc) String() string {
+func (s *Avg) String() string {
 	return stringutil.Sprintf("AVG(%v)", s.Expr)
 }
 
-// Aggregator returns a AvgFunc. It implements the AggregatorBuilder interface.
-func (s *AvgFunc) Aggregator() Aggregator {
+// Aggregator returns a Avg. It implements the AggregatorBuilder interface.
+func (s *Avg) Aggregator() expr.Aggregator {
 	return &AvgAggregator{
 		Fn: s,
 	}
@@ -619,7 +579,7 @@ func (s *AvgFunc) Aggregator() Aggregator {
 
 // AvgAggregator is an aggregator that returns the average non-null value.
 type AvgAggregator struct {
-	Fn      *AvgFunc
+	Fn      *Avg
 	Avg     float64
 	Counter int64
 }
diff --git a/internal/expr/functions/definition.go b/internal/expr/functions/definition.go
new file mode 100644
index 00000000..9e3529fb
--- /dev/null
+++ b/internal/expr/functions/definition.go
@@ -0,0 +1,75 @@
+package functions
+
+import (
+	"strings"
+
+	"github.com/genjidb/genji/internal/expr"
+	"github.com/genjidb/genji/internal/stringutil"
+)
+
+// A Definition transforms a list of expressions into a Function.
+type Definition interface {
+	Name() string
+	String() string
+	Function(...expr.Expr) (expr.Function, error)
+	Arity() int
+}
+
+// Definitions table holds a map of definition, indexed by their names.
+type Definitions map[string]Definition
+
+// Packages represent a table of SQL functions grouped by their packages
+type Packages map[string]Definitions
+
+func DefaultPackages() Packages {
+	return Packages{
+		"":     BuiltinDefinitions(),
+		"math": MathFunctions(),
+	}
+}
+
+// GetFunc return a function definition by its package and name.
+func (t Packages) GetFunc(pkg string, fname string) (Definition, error) {
+	fs, ok := t[pkg]
+	if !ok {
+		return nil, stringutil.Errorf("no such package: %q", fname)
+	}
+	def, ok := fs[strings.ToLower(fname)]
+	if !ok {
+		if pkg == "" {
+			return nil, stringutil.Errorf("no such function: %q", fname)
+		}
+		return nil, stringutil.Errorf("no such function: %q.%q", pkg, fname)
+	}
+	return def, nil
+}
+
+// A definition is the most basic version of a function definition.
+type definition struct {
+	name          string
+	arity         int
+	constructorFn func(...expr.Expr) (expr.Function, error)
+}
+
+func (fd *definition) Name() string {
+	return fd.name
+}
+
+func (fd *definition) Function(args ...expr.Expr) (expr.Function, error) {
+	if len(args) != fd.arity {
+		return nil, stringutil.Errorf("%s() takes %d argument(s), not %d", fd.name, fd.arity, len(args))
+	}
+	return fd.constructorFn(args...)
+}
+
+func (fd *definition) String() string {
+	args := make([]string, 0, fd.arity)
+	for i := 0; i < fd.arity; i++ {
+		args = append(args, stringutil.Sprintf("arg%d", i+1))
+	}
+	return stringutil.Sprintf("%s(%s)", fd.name, strings.Join(args, ", "))
+}
+
+func (fd *definition) Arity() int {
+	return fd.arity
+}
diff --git a/internal/expr/functions/math.go b/internal/expr/functions/math.go
new file mode 100644
index 00000000..4386b086
--- /dev/null
+++ b/internal/expr/functions/math.go
@@ -0,0 +1,32 @@
+package functions
+
+import (
+	"math"
+
+	"github.com/genjidb/genji/document"
+	"github.com/genjidb/genji/internal/stringutil"
+)
+
+// MathFunctions returns all math package functions.
+func MathFunctions() Definitions {
+	return mathFunctions
+}
+
+var mathFunctions = Definitions{
+	"floor": floorFunc,
+}
+
+var floorFunc = &ScalarDefinition{
+	name:  "floor",
+	arity: 1,
+	callFn: func(args ...document.Value) (document.Value, error) {
+		switch args[0].Type {
+		case document.DoubleValue:
+			return document.NewDoubleValue(math.Floor(args[0].V.(float64))), nil
+		case document.IntegerValue:
+			return args[0], nil
+		default:
+			return document.Value{}, stringutil.Errorf("floor(arg1) expects arg1 to be a number")
+		}
+	},
+}
diff --git a/internal/expr/functions/scalar_definition.go b/internal/expr/functions/scalar_definition.go
new file mode 100644
index 00000000..6ff8f37a
--- /dev/null
+++ b/internal/expr/functions/scalar_definition.go
@@ -0,0 +1,95 @@
+package functions
+
+import (
+	"strings"
+
+	"github.com/genjidb/genji/document"
+	"github.com/genjidb/genji/internal/environment"
+	"github.com/genjidb/genji/internal/expr"
+	"github.com/genjidb/genji/internal/stringutil"
+)
+
+// A ScalarDefinition is the definition type for functions which operates on scalar values in contrast to other SQL functions
+// such as the SUM aggregator wich operates on expressions instead.
+//
+// This difference allows to simply define them with a CallFn function that takes multiple document.Value and
+// return another document.Value, rather than having to manually evaluate expressions (see Definition).
+type ScalarDefinition struct {
+	name   string
+	arity  int
+	callFn func(...document.Value) (document.Value, error)
+}
+
+func NewScalarDefinition(name string, arity int, callFn func(...document.Value) (document.Value, error)) *ScalarDefinition {
+	return &ScalarDefinition{name: name, arity: arity, callFn: callFn}
+}
+
+// Name returns the defined function named (as an ident, so no parentheses).
+func (fd *ScalarDefinition) Name() string {
+	return fd.name
+}
+
+// String returns the defined function name and its arguments.
+func (fd *ScalarDefinition) String() string {
+	args := make([]string, 0, fd.arity)
+	for i := 0; i < fd.arity; i++ {
+		args = append(args, stringutil.Sprintf("arg%d", i+1))
+	}
+	return stringutil.Sprintf("%s(%s)", fd.name, strings.Join(args, ", "))
+}
+
+// Function returns a Function expr node.
+func (fd *ScalarDefinition) Function(args ...expr.Expr) (expr.Function, error) {
+	if len(args) != fd.arity {
+		return nil, stringutil.Errorf("%s takes %d argument(s), not %d", fd.String(), fd.arity, len(args))
+	}
+	return &ScalarFunction{
+		params: args,
+		def:    fd,
+	}, nil
+}
+
+// Arity returns the arity of the defined function.
+func (fd *ScalarDefinition) Arity() int {
+	return fd.arity
+}
+
+// A ScalarFunction is a function which operates on scalar values in contrast to other SQL functions
+// such as the SUM aggregator wich operates on expressions instead.
+type ScalarFunction struct {
+	def    *ScalarDefinition
+	params []expr.Expr
+}
+
+// Eval returns a document.Value based on the given environment and the underlying function
+// definition.
+func (sf *ScalarFunction) Eval(env *environment.Environment) (document.Value, error) {
+	args, err := sf.evalParams(env)
+	if err != nil {
+		return document.Value{}, err
+	}
+	return sf.def.callFn(args...)
+}
+
+// evalParams evaluate all arguments given to the function in the context of the given environmment.
+func (sf *ScalarFunction) evalParams(env *environment.Environment) ([]document.Value, error) {
+	values := make([]document.Value, 0, len(sf.params))
+	for _, param := range sf.params {
+		v, err := param.Eval(env)
+		if err != nil {
+			return nil, err
+		}
+		values = append(values, v)
+	}
+	return values, nil
+}
+
+// String returns a string represention of the function expression and its arguments.
+func (sf *ScalarFunction) String() string {
+	return stringutil.Sprintf("%s(%v)", sf.def.name, sf.params)
+}
+
+// Params return the function arguments.
+func (sf *ScalarFunction) Params() []expr.Expr {
+	return sf.params
+}
diff --git a/internal/planner/optimizer.go b/internal/planner/optimizer.go
index b0904d09..9a87341c 100644
--- a/internal/planner/optimizer.go
+++ b/internal/planner/optimizer.go
@@ -5,6 +5,7 @@ import (
 	"github.com/genjidb/genji/internal/database"
 	"github.com/genjidb/genji/internal/environment"
 	"github.com/genjidb/genji/internal/expr"
+	"github.com/genjidb/genji/internal/expr/functions"
 	"github.com/genjidb/genji/internal/sql/scanner"
 	"github.com/genjidb/genji/internal/stream"
 	"github.com/genjidb/genji/internal/stringutil"
@@ -383,7 +384,7 @@ func isProjectionUnique(indexes []*database.IndexInfo, po *stream.ProjectOperato
 			if found != nil && found.Unique {
 				continue
 			}
-		case *expr.PKFunc:
+		case *functions.PK:
 			continue
 		}
 
diff --git a/internal/query/statement/drop.go b/internal/query/statement/drop.go
index c8b4db0c..a73dfe98 100644
--- a/internal/query/statement/drop.go
+++ b/internal/query/statement/drop.go
@@ -4,6 +4,7 @@ import (
 	"errors"
 
 	errs "github.com/genjidb/genji/errors"
+	"github.com/genjidb/genji/internal/stringutil"
 )
 
 // DropTableStmt is a DSL that allows creating a DROP TABLE query.
@@ -78,3 +79,43 @@ func (stmt DropIndexStmt) Run(ctx *Context) (Result, error) {
 
 	return res, err
 }
+
+// DropSequenceStmt is a DSL that allows creating a DROP INDEX query.
+type DropSequenceStmt struct {
+	SequenceName string
+	IfExists     bool
+}
+
+// IsReadOnly always returns false. It implements the Statement interface.
+func (stmt DropSequenceStmt) IsReadOnly() bool {
+	return false
+}
+
+// Run runs the DropSequence statement in the given transaction.
+// It implements the Statement interface.
+func (stmt DropSequenceStmt) Run(ctx *Context) (Result, error) {
+	var res Result
+
+	if stmt.SequenceName == "" {
+		return res, errors.New("missing index name")
+	}
+
+	seq, err := ctx.Catalog.GetSequence(stmt.SequenceName)
+	if err != nil {
+		if errs.IsNotFoundError(err) && stmt.IfExists {
+			err = nil
+		}
+		return res, err
+	}
+
+	if seq.Info.Owner.TableName != "" {
+		return res, stringutil.Errorf("cannot drop sequence %s because constraint of table %s requires it", seq.Info.Name, seq.Info.Owner.TableName)
+	}
+
+	err = ctx.Catalog.DropSequence(ctx.Tx, stmt.SequenceName)
+	if errs.IsNotFoundError(err) && stmt.IfExists {
+		err = nil
+	}
+
+	return res, err
+}
diff --git a/internal/sql/parser/drop.go b/internal/sql/parser/drop.go
index b323bf12..11484463 100644
--- a/internal/sql/parser/drop.go
+++ b/internal/sql/parser/drop.go
@@ -14,9 +14,11 @@ func (p *Parser) parseDropStatement() (statement.Statement, error) {
 		return p.parseDropTableStatement()
 	case scanner.INDEX:
 		return p.parseDropIndexStatement()
+	case scanner.SEQUENCE:
+		return p.parseDropSequenceStatement()
 	}
 
-	return nil, newParseError(scanner.Tokstr(tok, lit), []string{"TABLE", "INDEX"}, pos)
+	return nil, newParseError(scanner.Tokstr(tok, lit), []string{"TABLE", "INDEX", "SEQUENCE"}, pos)
 }
 
 // parseDropTableStatement parses a drop table string and returns a Statement AST object.
@@ -62,3 +64,25 @@ func (p *Parser) parseDropIndexStatement() (statement.DropIndexStmt, error) {
 
 	return stmt, nil
 }
+
+// parseDropSequenceStatement parses a drop sequence string and returns a Statement AST object.
+// This function assumes the DROP SEQUENCE tokens have already been consumed.
+func (p *Parser) parseDropSequenceStatement() (statement.DropSequenceStmt, error) {
+	var stmt statement.DropSequenceStmt
+	var err error
+
+	stmt.IfExists, err = p.parseOptional(scanner.IF, scanner.EXISTS)
+	if err != nil {
+		return stmt, err
+	}
+
+	// Parse sequence name
+	stmt.SequenceName, err = p.parseIdent()
+	if err != nil {
+		pErr := err.(*ParseError)
+		pErr.Expected = []string{"sequence_name"}
+		return stmt, pErr
+	}
+
+	return stmt, nil
+}
diff --git a/internal/sql/parser/expr.go b/internal/sql/parser/expr.go
index a23471d2..533476ce 100644
--- a/internal/sql/parser/expr.go
+++ b/internal/sql/parser/expr.go
@@ -6,6 +6,7 @@ import (
 	"github.com/genjidb/genji/document"
 	"github.com/genjidb/genji/internal/environment"
 	"github.com/genjidb/genji/internal/expr"
+	"github.com/genjidb/genji/internal/expr/functions"
 	"github.com/genjidb/genji/internal/sql/scanner"
 	"github.com/genjidb/genji/internal/stringutil"
 )
@@ -190,11 +191,30 @@ func (p *Parser) parseUnaryExpr(allowed ...scanner.Token) (expr.Expr, error) {
 		p.Unscan()
 		return p.parseCastExpression()
 	case scanner.IDENT:
-		// if the next token is a left parenthesis, this is a function
-		if tok1, _, _ := p.Scan(); tok1 == scanner.LPAREN {
+		tok1, _, _ := p.Scan()
+		// if the next token is a left parenthesis, this is a global function
+		if tok1 == scanner.LPAREN {
 			p.Unscan()
 			p.Unscan()
 			return p.parseFunction()
+		} else {
+			// it may be a package function instead.
+			if tok1 == scanner.DOT {
+				if tok2, _, _ := p.Scan(); tok2 == scanner.IDENT {
+					if tok3, _, _ := p.Scan(); tok3 == scanner.LPAREN {
+						p.Unscan()
+						p.Unscan()
+						p.Unscan()
+						p.Unscan()
+						return p.parseFunction()
+					} else {
+						p.Unscan()
+						p.Unscan()
+					}
+				} else {
+					p.Unscan()
+				}
+			}
 		}
 		p.Unscan()
 		p.Unscan()
@@ -577,11 +597,23 @@ func (p *Parser) parseExprList(leftToken, rightToken scanner.Token) (expr.Litera
 // an optional coma-separated list of expressions and a closing parenthesis.
 func (p *Parser) parseFunction() (expr.Expr, error) {
 	// Parse function name.
-	fname, err := p.parseIdent()
+	funcName, err := p.parseIdent()
 	if err != nil {
 		return nil, err
 	}
 
+	// Parse optional package name
+	var pkgName string
+	if tok, _, _ := p.Scan(); tok == scanner.DOT {
+		pkgName = funcName
+		funcName, err = p.parseIdent()
+		if err != nil {
+			return nil, err
+		}
+	} else {
+		p.Unscan()
+	}
+
 	// Parse required ( token.
 	if err := p.parseTokens(scanner.LPAREN); err != nil {
 		return nil, err
@@ -593,13 +625,17 @@ func (p *Parser) parseFunction() (expr.Expr, error) {
 			return nil, newParseError(scanner.Tokstr(tok, lit), []string{")"}, pos)
 		}
 
-		return &expr.CountFunc{Wildcard: true}, nil
+		return &functions.Count{Wildcard: true}, nil
 	}
 	p.Unscan()
 
 	// Check if the function is called without arguments.
 	if tok, _, _ := p.ScanIgnoreWhitespace(); tok == scanner.RPAREN {
-		return p.functions.GetFunc(fname)
+		def, err := p.packagesTable.GetFunc(pkgName, funcName)
+		if err != nil {
+			return nil, err
+		}
+		return def.Function()
 	}
 	p.Unscan()
 
@@ -625,7 +661,11 @@ func (p *Parser) parseFunction() (expr.Expr, error) {
 		return nil, err
 	}
 
-	return p.functions.GetFunc(fname, exprs...)
+	def, err := p.packagesTable.GetFunc(pkgName, funcName)
+	if err != nil {
+		return nil, err
+	}
+	return def.Function(exprs...)
 }
 
 // parseCastExpression parses a string of the form CAST(expr AS type).
@@ -657,7 +697,7 @@ func (p *Parser) parseCastExpression() (expr.Expr, error) {
 		return nil, err
 	}
 
-	return expr.CastFunc{Expr: e, CastAs: tp}, nil
+	return functions.Cast{Expr: e, CastAs: tp}, nil
 }
 
 // tokenIsAllowed is a helper function that determines if a token is allowed.
diff --git a/internal/sql/parser/options.go b/internal/sql/parser/options.go
index a392bdc7..a98b9cdc 100644
--- a/internal/sql/parser/options.go
+++ b/internal/sql/parser/options.go
@@ -1,15 +1,17 @@
 package parser
 
-import "github.com/genjidb/genji/internal/expr"
+import (
+	"github.com/genjidb/genji/internal/expr/functions"
+)
 
 // Options of the SQL parser.
 type Options struct {
-	// A map of builtin SQL functions.
-	Functions expr.Functions
+	// A table of function packages.
+	Packages functions.Packages
 }
 
 func defaultOptions() *Options {
 	return &Options{
-		Functions: expr.NewFunctions(),
+		Packages: functions.DefaultPackages(),
 	}
 }
diff --git a/internal/sql/parser/parser.go b/internal/sql/parser/parser.go
index 21e895c7..79216913 100644
--- a/internal/sql/parser/parser.go
+++ b/internal/sql/parser/parser.go
@@ -6,6 +6,7 @@ import (
 
 	"github.com/genjidb/genji/document"
 	"github.com/genjidb/genji/internal/expr"
+	"github.com/genjidb/genji/internal/expr/functions"
 	"github.com/genjidb/genji/internal/query"
 	"github.com/genjidb/genji/internal/query/statement"
 	"github.com/genjidb/genji/internal/sql/scanner"
@@ -17,7 +18,7 @@ type Parser struct {
 	s             *scanner.Scanner
 	orderedParams int
 	namedParams   int
-	functions     expr.Functions
+	packagesTable functions.Packages
 }
 
 // NewParser returns a new instance of Parser.
@@ -31,7 +32,7 @@ func NewParserWithOptions(r io.Reader, opts *Options) *Parser {
 		opts = defaultOptions()
 	}
 
-	return &Parser{s: scanner.NewScanner(r), functions: opts.Functions}
+	return &Parser{s: scanner.NewScanner(r), packagesTable: opts.Packages}
 }
 
 // ParseQuery parses a query string and returns its AST representation.
diff --git a/internal/sql/scanner/scanner.go b/internal/sql/scanner/scanner.go
index 813f2357..61e309dc 100644
--- a/internal/sql/scanner/scanner.go
+++ b/internal/sql/scanner/scanner.go
@@ -379,7 +379,7 @@ type Scanner struct {
 	s   *scanner
 	i   int // buffer index
 	n   int // buffer size
-	buf [3]struct {
+	buf [4]struct {
 		tok Token
 		pos Pos
 		lit string
diff --git a/internal/sql/scanner/token.go b/internal/sql/scanner/token.go
index 22487000..b2d737c3 100644
--- a/internal/sql/scanner/token.go
+++ b/internal/sql/scanner/token.go
@@ -366,3 +366,12 @@ type Pos struct {
 	Line int
 	Char int
 }
+
+// AllKeywords returns all defined tokens corresponding to keywords.
+func AllKeywords() []Token {
+	tokens := make([]Token, 0, len(keywords))
+	for _, tok := range keywords {
+		tokens = append(tokens, tok)
+	}
+	return tokens
+}


Test patch:
diff --git a/cmd/genji/doc/doc_test.go b/cmd/genji/doc/doc_test.go
new file mode 100644
index 00000000..9402476a
--- /dev/null
+++ b/cmd/genji/doc/doc_test.go
@@ -0,0 +1,76 @@
+package doc_test
+
+import (
+	"fmt"
+	"testing"
+
+	"github.com/genjidb/genji/cmd/genji/doc"
+	"github.com/genjidb/genji/internal/expr/functions"
+	"github.com/genjidb/genji/internal/sql/scanner"
+	"github.com/stretchr/testify/require"
+)
+
+func TestFunctions(t *testing.T) {
+	packages := functions.DefaultPackages()
+	for pkgname, pkg := range packages {
+		for fname, def := range pkg {
+			if pkgname == "" {
+				t.Run(fmt.Sprintf("%s has all its arguments mentioned", fname), func(t *testing.T) {
+					str, err := doc.DocString(fname)
+					require.NoError(t, err)
+					for i := 0; i < def.Arity(); i++ {
+						require.Contains(t, str, fmt.Sprintf("arg%d", i+1))
+					}
+				})
+			} else {
+				t.Run(fmt.Sprintf("%s.%s has all its arguments mentioned", pkgname, fname), func(t *testing.T) {
+					str, err := doc.DocString(fmt.Sprintf("%s.%s", pkgname, fname))
+					require.NoError(t, err)
+					for i := 0; i < def.Arity(); i++ {
+						require.Contains(t, str, fmt.Sprintf("arg%d", i+1))
+					}
+				})
+			}
+		}
+	}
+}
+
+func TestTokens(t *testing.T) {
+	for _, tok := range scanner.AllKeywords() {
+		t.Run(fmt.Sprintf("%s is documented", tok.String()), func(t *testing.T) {
+			str, err := doc.DocString(tok.String())
+			require.NoError(t, err)
+			require.NotEqual(t, "", str)
+			if str == "TODO" {
+				t.Logf("warning, %s is not yet documented", tok.String())
+			} else {
+				// if the token is documented, its description should contain its own name.
+				require.Contains(t, str, tok.String())
+			}
+		})
+	}
+}
+
+func TestDocString(t *testing.T) {
+	t.Run("OK", func(t *testing.T) {
+		str, err := doc.DocString("BY")
+		require.NoError(t, err)
+		require.NotEmpty(t, str)
+		require.NotEqual(t, "TODO", str)
+	})
+
+	t.Run("NOK illegal input", func(t *testing.T) {
+		_, err := doc.DocString("😀")
+		require.Equal(t, doc.ErrInvalid, err)
+	})
+
+	t.Run("NOK empty input", func(t *testing.T) {
+		_, err := doc.DocString("")
+		require.Equal(t, doc.ErrInvalid, err)
+	})
+
+	t.Run("NOK no doc found", func(t *testing.T) {
+		_, err := doc.DocString("foo.bar")
+		require.ErrorIs(t, err, doc.ErrNotFound)
+	})
+}
diff --git a/internal/expr/comparison_test.go b/internal/expr/comparison_test.go
index db64d770..245024e8 100644
--- a/internal/expr/comparison_test.go
+++ b/internal/expr/comparison_test.go
@@ -5,6 +5,7 @@ import (
 
 	"github.com/genjidb/genji/document"
 	"github.com/genjidb/genji/internal/environment"
+	"github.com/genjidb/genji/internal/testutil"
 )
 
 func TestComparisonExpr(t *testing.T) {
@@ -35,7 +36,7 @@ func TestComparisonExpr(t *testing.T) {
 
 	for _, test := range tests {
 		t.Run(test.expr, func(t *testing.T) {
-			testExpr(t, test.expr, envWithDoc, test.res, test.fails)
+			testutil.TestExpr(t, test.expr, envWithDoc, test.res, test.fails)
 		})
 	}
 }
@@ -60,7 +61,7 @@ func TestComparisonINExpr(t *testing.T) {
 
 	for _, test := range tests {
 		t.Run(test.expr, func(t *testing.T) {
-			testExpr(t, test.expr, envWithDoc, test.res, test.fails)
+			testutil.TestExpr(t, test.expr, envWithDoc, test.res, test.fails)
 		})
 	}
 }
@@ -84,7 +85,7 @@ func TestComparisonNOTINExpr(t *testing.T) {
 
 	for _, test := range tests {
 		t.Run(test.expr, func(t *testing.T) {
-			testExpr(t, test.expr, envWithDoc, test.res, test.fails)
+			testutil.TestExpr(t, test.expr, envWithDoc, test.res, test.fails)
 		})
 	}
 }
@@ -104,7 +105,7 @@ func TestComparisonISExpr(t *testing.T) {
 
 	for _, test := range tests {
 		t.Run(test.expr, func(t *testing.T) {
-			testExpr(t, test.expr, envWithDoc, test.res, test.fails)
+			testutil.TestExpr(t, test.expr, envWithDoc, test.res, test.fails)
 		})
 	}
 }
@@ -124,7 +125,7 @@ func TestComparisonISNOTExpr(t *testing.T) {
 
 	for _, test := range tests {
 		t.Run(test.expr, func(t *testing.T) {
-			testExpr(t, test.expr, envWithDoc, test.res, test.fails)
+			testutil.TestExpr(t, test.expr, envWithDoc, test.res, test.fails)
 		})
 	}
 }
@@ -152,7 +153,7 @@ func TestComparisonExprNodocument(t *testing.T) {
 				t.Run(test.expr, func(t *testing.T) {
 					var emptyenv environment.Environment
 
-					testExpr(t, test.expr, &emptyenv, test.res, test.fails)
+					testutil.TestExpr(t, test.expr, &emptyenv, test.res, test.fails)
 				})
 			}
 		})
@@ -178,7 +179,7 @@ func TestComparisonBetweenExpr(t *testing.T) {
 
 	for _, test := range tests {
 		t.Run(test.expr, func(t *testing.T) {
-			testExpr(t, test.expr, envWithDoc, test.res, test.fails)
+			testutil.TestExpr(t, test.expr, envWithDoc, test.res, test.fails)
 		})
 	}
 }
diff --git a/internal/expr/expr_test.go b/internal/expr/expr_test.go
index 9d48e516..94df8850 100644
--- a/internal/expr/expr_test.go
+++ b/internal/expr/expr_test.go
@@ -41,20 +41,6 @@ var envWithDocAndKey = environment.New(docWithKey)
 
 var nullLiteral = document.NewNullValue()
 
-func testExpr(t testing.TB, exprStr string, env *environment.Environment, want document.Value, fails bool) {
-	t.Helper()
-
-	e, err := parser.NewParser(strings.NewReader(exprStr)).ParseExpr()
-	require.NoError(t, err)
-	res, err := e.Eval(env)
-	if fails {
-		require.Error(t, err)
-	} else {
-		require.NoError(t, err)
-		require.Equal(t, want, res)
-	}
-}
-
 func TestString(t *testing.T) {
 	var operands = []string{
 		`10.4`,
diff --git a/internal/expr/function_test.go b/internal/expr/function_test.go
deleted file mode 100644
index a929dcf9..00000000
--- a/internal/expr/function_test.go
+++ /dev/null
@@ -1,26 +0,0 @@
-package expr_test
-
-import (
-	"testing"
-
-	"github.com/genjidb/genji/document"
-	"github.com/genjidb/genji/internal/environment"
-)
-
-func TestPkExpr(t *testing.T) {
-	tests := []struct {
-		name string
-		env  *environment.Environment
-		res  document.Value
-	}{
-		{"empty env", &environment.Environment{}, nullLiteral},
-		{"env with doc", envWithDoc, nullLiteral},
-		{"env with doc and key", envWithDocAndKey, document.NewIntegerValue(1)},
-	}
-
-	for _, test := range tests {
-		t.Run(test.name, func(t *testing.T) {
-			testExpr(t, "pk()", test.env, test.res, false)
-		})
-	}
-}
diff --git a/internal/expr/functions/builtins_test.go b/internal/expr/functions/builtins_test.go
new file mode 100644
index 00000000..c30d089c
--- /dev/null
+++ b/internal/expr/functions/builtins_test.go
@@ -0,0 +1,54 @@
+package functions_test
+
+import (
+	"testing"
+
+	"github.com/genjidb/genji/document"
+	"github.com/genjidb/genji/internal/environment"
+	"github.com/genjidb/genji/internal/testutil"
+)
+
+var doc document.Document = func() document.Document {
+	return document.NewFromJSON([]byte(`{
+		"a": 1,
+		"b": {"foo bar": [1, 2]},
+		"c": [1, {"foo": "bar"}, [1, 2]]
+	}`))
+}()
+
+var docWithKey document.Document = func() document.Document {
+	fb := document.NewFieldBuffer()
+	err := fb.Copy(doc)
+	if err != nil {
+		panic(err)
+	}
+
+	fb.DecodedKey = document.NewIntegerValue(1)
+	fb.EncodedKey, err = fb.DecodedKey.MarshalBinary()
+	if err != nil {
+		panic(err)
+	}
+
+	return fb
+}()
+
+var envWithDoc = environment.New(doc)
+var envWithDocAndKey = environment.New(docWithKey)
+
+func TestPk(t *testing.T) {
+	tests := []struct {
+		name string
+		env  *environment.Environment
+		res  document.Value
+	}{
+		{"empty env", &environment.Environment{}, document.NewNullValue()},
+		{"env with doc", envWithDoc, document.NewNullValue()},
+		{"env with doc and key", envWithDocAndKey, document.NewIntegerValue(1)},
+	}
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			testutil.TestExpr(t, "pk()", test.env, test.res, false)
+		})
+	}
+}
diff --git a/internal/expr/functions/definition_test.go b/internal/expr/functions/definition_test.go
new file mode 100644
index 00000000..23bac515
--- /dev/null
+++ b/internal/expr/functions/definition_test.go
@@ -0,0 +1,58 @@
+package functions_test
+
+import (
+	"testing"
+
+	"github.com/genjidb/genji/document"
+	"github.com/genjidb/genji/internal/expr"
+	"github.com/genjidb/genji/internal/expr/functions"
+	"github.com/stretchr/testify/require"
+)
+
+func TestDefinitions(t *testing.T) {
+	packages := functions.DefaultPackages()
+	def, err := packages.GetFunc("", "count")
+	require.NoError(t, err)
+
+	t.Run("String()", func(t *testing.T) {
+		require.Equal(t, "count(arg1)", def.String())
+	})
+
+	t.Run("Function()", func(t *testing.T) {
+		fexpr, err := def.Function(expr.Path(document.NewPath("a")))
+		require.NoError(t, err)
+		require.NotNil(t, fexpr)
+	})
+
+	t.Run("Arity()", func(t *testing.T) {
+		require.Equal(t, 1, def.Arity())
+	})
+}
+
+func TestPackages(t *testing.T) {
+	table := functions.DefaultPackages()
+
+	t.Run("OK GetFunc()", func(t *testing.T) {
+		def, err := table.GetFunc("math", "floor")
+		require.NoError(t, err)
+		require.Equal(t, "floor", def.Name())
+		def, err = table.GetFunc("", "count")
+		require.NoError(t, err)
+		require.Equal(t, "count", def.Name())
+	})
+
+	t.Run("NOK GetFunc() missing func", func(t *testing.T) {
+		def, err := table.GetFunc("math", "foobar")
+		require.Error(t, err)
+		require.Nil(t, def)
+		def, err = table.GetFunc("", "foobar")
+		require.Error(t, err)
+		require.Nil(t, def)
+	})
+
+	t.Run("NOK GetFunc() missing package", func(t *testing.T) {
+		def, err := table.GetFunc("foobar", "foobar")
+		require.Error(t, err)
+		require.Nil(t, def)
+	})
+}
diff --git a/internal/expr/functions/math_test.go b/internal/expr/functions/math_test.go
new file mode 100644
index 00000000..c53de0d9
--- /dev/null
+++ b/internal/expr/functions/math_test.go
@@ -0,0 +1,12 @@
+package functions_test
+
+import (
+	"path/filepath"
+	"testing"
+
+	"github.com/genjidb/genji/internal/testutil"
+)
+
+func TestMathFunctions(t *testing.T) {
+	testutil.ExprRunner(t, filepath.Join("testdata", "math_functions.sql"))
+}
diff --git a/internal/expr/functions/scalar_definition_test.go b/internal/expr/functions/scalar_definition_test.go
new file mode 100644
index 00000000..31da5901
--- /dev/null
+++ b/internal/expr/functions/scalar_definition_test.go
@@ -0,0 +1,59 @@
+package functions_test
+
+import (
+	"testing"
+
+	"github.com/genjidb/genji/document"
+	"github.com/genjidb/genji/internal/environment"
+	"github.com/genjidb/genji/internal/expr"
+	"github.com/genjidb/genji/internal/expr/functions"
+	"github.com/stretchr/testify/require"
+)
+
+func TestScalarFunctionDef(t *testing.T) {
+	def := functions.NewScalarDefinition(
+		"foo",
+		3,
+		func(args ...document.Value) (document.Value, error) {
+			arg1 := args[0].V.(int64)
+			arg2 := args[1].V.(int64)
+			arg3 := args[2].V.(int64)
+
+			return document.NewIntegerValue(arg1 + arg2 + arg3), nil
+		},
+	)
+
+	t.Run("Name()", func(t *testing.T) {
+		require.Equal(t, "foo", def.Name())
+	})
+
+	t.Run("Arity()", func(t *testing.T) {
+		require.Equal(t, 3, def.Arity())
+	})
+
+	t.Run("String()", func(t *testing.T) {
+		require.Equal(t, "foo(arg1, arg2, arg3)", def.String())
+	})
+
+	t.Run("Function()", func(t *testing.T) {
+		fb := document.NewFieldBuffer()
+		fb = fb.Add("a", document.NewIntegerValue(2))
+		env := environment.New(fb)
+		expr1 := expr.Add(expr.LiteralValue(document.NewIntegerValue(1)), expr.LiteralValue(document.NewIntegerValue(0)))
+		expr2 := expr.Path(document.NewPath("a"))
+		expr3 := expr.Div(expr.LiteralValue(document.NewIntegerValue(6)), expr.LiteralValue(document.NewIntegerValue(2)))
+
+		t.Run("OK", func(t *testing.T) {
+			fexpr, err := def.Function(expr1, expr2, expr3)
+			require.NoError(t, err)
+			v, err := fexpr.Eval(env)
+			require.NoError(t, err)
+			require.Equal(t, document.NewIntegerValue(1+2+3), v)
+		})
+
+		t.Run("NOK", func(t *testing.T) {
+			_, err := def.Function(expr1, expr2)
+			require.Error(t, err)
+		})
+	})
+}
diff --git a/internal/expr/functions/testdata/math_functions.sql b/internal/expr/functions/testdata/math_functions.sql
new file mode 100644
index 00000000..c886d801
--- /dev/null
+++ b/internal/expr/functions/testdata/math_functions.sql
@@ -0,0 +1,9 @@
+-- test: math.floor
+> math.floor(2.3)
+2.0
+
+> math.floor(2)
+2
+
+! math.floor('a')
+'floor(arg1) expects arg1 to be a number'
diff --git a/internal/expr/operator_test.go b/internal/expr/operator_test.go
index f812ac38..8a3904d1 100644
--- a/internal/expr/operator_test.go
+++ b/internal/expr/operator_test.go
@@ -4,6 +4,7 @@ import (
 	"testing"
 
 	"github.com/genjidb/genji/document"
+	"github.com/genjidb/genji/internal/testutil"
 )
 
 func TestConcatExpr(t *testing.T) {
@@ -20,7 +21,7 @@ func TestConcatExpr(t *testing.T) {
 
 	for _, test := range tests {
 		t.Run(test.expr, func(t *testing.T) {
-			testExpr(t, test.expr, envWithDoc, test.res, test.fails)
+			testutil.TestExpr(t, test.expr, envWithDoc, test.res, test.fails)
 		})
 	}
 }
diff --git a/internal/expr/path_test.go b/internal/expr/path_test.go
index aac17244..8a976dca 100644
--- a/internal/expr/path_test.go
+++ b/internal/expr/path_test.go
@@ -9,6 +9,7 @@ import (
 	"github.com/genjidb/genji/internal/environment"
 	"github.com/genjidb/genji/internal/expr"
 	"github.com/genjidb/genji/internal/sql/parser"
+	"github.com/genjidb/genji/internal/testutil"
 	"github.com/stretchr/testify/require"
 )
 
@@ -45,12 +46,12 @@ func TestPathExpr(t *testing.T) {
 
 	for _, test := range tests {
 		t.Run(test.expr, func(t *testing.T) {
-			testExpr(t, test.expr, environment.New(d), test.res, test.fails)
+			testutil.TestExpr(t, test.expr, environment.New(d), test.res, test.fails)
 		})
 	}
 
 	t.Run("empty env", func(t *testing.T) {
-		testExpr(t, "a", &environment.Environment{}, nullLiteral, true)
+		testutil.TestExpr(t, "a", &environment.Environment{}, nullLiteral, true)
 	})
 }
 
@@ -112,11 +113,11 @@ func TestEnvPathExpr(t *testing.T) {
 
 	for _, test := range tests {
 		t.Run(test.expr, func(t *testing.T) {
-			testExpr(t, test.expr, environment.New(d), test.res, test.fails)
+			testutil.TestExpr(t, test.expr, environment.New(d), test.res, test.fails)
 		})
 	}
 
 	t.Run("empty env", func(t *testing.T) {
-		testExpr(t, "a", &environment.Environment{}, nullLiteral, true)
+		testutil.TestExpr(t, "a", &environment.Environment{}, nullLiteral, true)
 	})
 }
diff --git a/internal/query/statement/drop_test.go b/internal/query/statement/drop_test.go
index 738606df..dcfb0ab5 100644
--- a/internal/query/statement/drop_test.go
+++ b/internal/query/statement/drop_test.go
@@ -5,6 +5,7 @@ import (
 
 	"github.com/genjidb/genji"
 	"github.com/genjidb/genji/document"
+	errs "github.com/genjidb/genji/errors"
 	"github.com/genjidb/genji/internal/testutil"
 	"github.com/stretchr/testify/require"
 )
@@ -87,3 +88,30 @@ func TestDropIndex(t *testing.T) {
 	err = testutil.Exec(db, tx, "DROP INDEX test1_bar_idx")
 	require.Error(t, err)
 }
+
+func TestDropSequence(t *testing.T) {
+	db, tx, cleanup := testutil.NewTestTx(t)
+	defer cleanup()
+
+	testutil.MustExec(t, db, tx, `
+		CREATE TABLE test1(foo int);
+		CREATE SEQUENCE seq1;
+		CREATE SEQUENCE seq2;
+	`)
+
+	testutil.MustExec(t, db, tx, "DROP SEQUENCE seq1")
+
+	// Assert that the good index has been dropped.
+	_, err := db.Catalog.GetSequence("seq1")
+	require.IsType(t, errs.NotFoundError{}, err)
+	_, err = db.Catalog.GetSequence("seq2")
+	require.NoError(t, err)
+
+	// Dropping a non existing sequence with IF EXISTS should not fail.
+	err = testutil.Exec(db, tx, "DROP SEQUENCE IF EXISTS unknown")
+	require.NoError(t, err)
+
+	// Dropping a sequence created with a table constraint should fail.
+	err = testutil.Exec(db, tx, "DROP SEQUENCE test1_seq")
+	require.Error(t, err)
+}
diff --git a/internal/sql/parser/drop_test.go b/internal/sql/parser/drop_test.go
index fe35171e..3cb8118f 100644
--- a/internal/sql/parser/drop_test.go
+++ b/internal/sql/parser/drop_test.go
@@ -19,6 +19,8 @@ func TestParserDrop(t *testing.T) {
 		{"Drop table If not exists", "DROP TABLE IF EXISTS test", statement.DropTableStmt{TableName: "test", IfExists: true}, false},
 		{"Drop index", "DROP INDEX test", statement.DropIndexStmt{IndexName: "test"}, false},
 		{"Drop index if exists", "DROP INDEX IF EXISTS test", statement.DropIndexStmt{IndexName: "test", IfExists: true}, false},
+		{"Drop index", "DROP SEQUENCE test", statement.DropSequenceStmt{SequenceName: "test"}, false},
+		{"Drop index if exists", "DROP SEQUENCE IF EXISTS test", statement.DropSequenceStmt{SequenceName: "test", IfExists: true}, false},
 	}
 
 	for _, test := range tests {
diff --git a/internal/sql/parser/expr_test.go b/internal/sql/parser/expr_test.go
index 73058711..200e91ce 100644
--- a/internal/sql/parser/expr_test.go
+++ b/internal/sql/parser/expr_test.go
@@ -6,6 +6,7 @@ import (
 
 	"github.com/genjidb/genji/document"
 	"github.com/genjidb/genji/internal/expr"
+	"github.com/genjidb/genji/internal/expr/functions"
 	"github.com/genjidb/genji/internal/sql/parser"
 	"github.com/genjidb/genji/internal/testutil"
 	"github.com/stretchr/testify/require"
@@ -152,7 +153,7 @@ func TestParserExpr(t *testing.T) {
 		{"with NULL", "age > NULL", expr.Gt(testutil.ParsePath(t, "age"), testutil.NullValue()), false},
 
 		// unary operators
-		{"CAST", "CAST(a.b[1][0] AS TEXT)", expr.CastFunc{Expr: testutil.ParsePath(t, "a.b[1][0]"), CastAs: document.TextValue}, false},
+		{"CAST", "CAST(a.b[1][0] AS TEXT)", functions.Cast{Expr: testutil.ParsePath(t, "a.b[1][0]"), CastAs: document.TextValue}, false},
 		{"NOT", "NOT 10", expr.Not(testutil.IntegerValue(10)), false},
 		{"NOT", "NOT NOT", nil, true},
 		{"NOT", "NOT NOT 10", expr.Not(expr.Not(testutil.IntegerValue(10))), false},
@@ -161,9 +162,10 @@ func TestParserExpr(t *testing.T) {
 		{"NEXT VALUE FOR", "NEXT VALUE FOR 10", nil, true},
 
 		// functions
-		{"pk() function", "pk()", &expr.PKFunc{}, false},
-		{"count(expr) function", "count(a)", &expr.CountFunc{Expr: testutil.ParsePath(t, "a")}, false},
-		{"count(*) function", "count(*)", &expr.CountFunc{Wildcard: true}, false},
+		{"pk() function", "pk()", &functions.PK{}, false},
+		{"count(expr) function", "count(a)", &functions.Count{Expr: testutil.ParsePath(t, "a")}, false},
+		{"count(*) function", "count(*)", &functions.Count{Wildcard: true}, false},
+		{"packaged function", "math.floor(1.2)", testutil.FunctionExpr(t, "math.floor", testutil.DoubleValue(1.2)), false},
 	}
 
 	for _, test := range tests {
diff --git a/internal/sql/parser/select_test.go b/internal/sql/parser/select_test.go
index 23c2f008..6c4cb8f8 100644
--- a/internal/sql/parser/select_test.go
+++ b/internal/sql/parser/select_test.go
@@ -4,6 +4,7 @@ import (
 	"testing"
 
 	"github.com/genjidb/genji/internal/expr"
+	"github.com/genjidb/genji/internal/expr/functions"
 	"github.com/genjidb/genji/internal/query/statement"
 	"github.com/genjidb/genji/internal/sql/parser"
 	"github.com/genjidb/genji/internal/stream"
@@ -143,7 +144,7 @@ func TestParserSelect(t *testing.T) {
 		{"WithOffsetThenLimit", "SELECT * FROM test WHERE age = 10 OFFSET 20 LIMIT 10", nil, true},
 		{"With aggregation function", "SELECT COUNT(*) FROM test",
 			stream.New(stream.SeqScan("test")).
-				Pipe(stream.HashAggregate(&expr.CountFunc{Wildcard: true})).
+				Pipe(stream.HashAggregate(&functions.Count{Wildcard: true})).
 				Pipe(stream.Project(testutil.ParseNamedExpr(t, "COUNT(*)"))),
 			false},
 		{"WithUnionAll", "SELECT * FROM test1 UNION ALL SELECT * FROM test2",
@@ -189,6 +190,6 @@ func TestParserSelect(t *testing.T) {
 
 func BenchmarkSelect(b *testing.B) {
 	for i := 0; i < b.N; i++ {
-		parser.ParseQuery("SELECT a, b.c[100].d AS `foo` FROM `some table` WHERE d.e[100] >= 12 AND c.d IN ([1, true], [2, false]) GROUP BY d.e[0] LIMIT 10 + 10 OFFSET 20 - 20 ORDER BY d DESC")
+		_, _ = parser.ParseQuery("SELECT a, b.c[100].d AS `foo` FROM `some table` WHERE d.e[100] >= 12 AND c.d IN ([1, true], [2, false]) GROUP BY d.e[0] LIMIT 10 + 10 OFFSET 20 - 20 ORDER BY d DESC")
 	}
 }
diff --git a/internal/stream/aggregate_test.go b/internal/stream/aggregate_test.go
index 6e07ff46..7ab9a648 100644
--- a/internal/stream/aggregate_test.go
+++ b/internal/stream/aggregate_test.go
@@ -6,6 +6,7 @@ import (
 	"github.com/genjidb/genji/document"
 	"github.com/genjidb/genji/internal/environment"
 	"github.com/genjidb/genji/internal/expr"
+	"github.com/genjidb/genji/internal/expr/functions"
 	"github.com/genjidb/genji/internal/sql/parser"
 	"github.com/genjidb/genji/internal/stream"
 	"github.com/genjidb/genji/internal/testutil"
@@ -32,7 +33,7 @@ func TestAggregate(t *testing.T) {
 		{
 			"count",
 			nil,
-			[]expr.AggregatorBuilder{&expr.CountFunc{Wildcard: true}},
+			[]expr.AggregatorBuilder{&functions.Count{Wildcard: true}},
 			[]document.Document{testutil.MakeDocument(t, `{"a": 10}`)},
 			[]document.Document{testutil.MakeDocument(t, `{"COUNT(*)": 1}`)},
 			false,
@@ -40,7 +41,7 @@ func TestAggregate(t *testing.T) {
 		{
 			"count/groupBy",
 			parser.MustParseExpr("a % 2"),
-			[]expr.AggregatorBuilder{&expr.CountFunc{Expr: parser.MustParseExpr("a")}, &expr.AvgFunc{Expr: parser.MustParseExpr("a")}},
+			[]expr.AggregatorBuilder{&functions.Count{Expr: parser.MustParseExpr("a")}, &functions.Avg{Expr: parser.MustParseExpr("a")}},
 			generateSeqDocs(t, 10),
 			[]document.Document{testutil.MakeDocument(t, `{"a % 2": 0, "COUNT(a)": 5, "AVG(a)": 4.0}`), testutil.MakeDocument(t, `{"a % 2": 1, "COUNT(a)": 5, "AVG(a)": 5.0}`)},
 			false,
@@ -48,7 +49,7 @@ func TestAggregate(t *testing.T) {
 		{
 			"count/noInput",
 			nil,
-			[]expr.AggregatorBuilder{&expr.CountFunc{Expr: parser.MustParseExpr("a")}, &expr.AvgFunc{Expr: parser.MustParseExpr("a")}},
+			[]expr.AggregatorBuilder{&functions.Count{Expr: parser.MustParseExpr("a")}, &functions.Avg{Expr: parser.MustParseExpr("a")}},
 			nil,
 			[]document.Document{testutil.MakeDocument(t, `{"COUNT(a)": 0, "AVG(a)": 0.0}`)},
 			false,
diff --git a/internal/testutil/expr.go b/internal/testutil/expr.go
index 5629fc5c..50c0ec73 100644
--- a/internal/testutil/expr.go
+++ b/internal/testutil/expr.go
@@ -9,6 +9,7 @@ import (
 	"github.com/genjidb/genji/document"
 	"github.com/genjidb/genji/internal/environment"
 	"github.com/genjidb/genji/internal/expr"
+	"github.com/genjidb/genji/internal/expr/functions"
 	"github.com/genjidb/genji/internal/sql/parser"
 	"github.com/genjidb/genji/internal/testutil/genexprtests"
 	"github.com/stretchr/testify/require"
@@ -96,8 +97,33 @@ func ParseNamedExpr(t testing.TB, s string, name ...string) expr.Expr {
 	return &ne
 }
 
+func TestExpr(t testing.TB, exprStr string, env *environment.Environment, want document.Value, fails bool) {
+	t.Helper()
+	e, err := parser.NewParser(strings.NewReader(exprStr)).ParseExpr()
+	require.NoError(t, err)
+	res, err := e.Eval(env)
+	if fails {
+		require.Error(t, err)
+	} else {
+		require.NoError(t, err)
+		require.Equal(t, want, res)
+	}
+}
+
 var emptyEnv = environment.New(nil)
 
+func FunctionExpr(t testing.TB, name string, args ...expr.Expr) expr.Expr {
+	t.Helper()
+	n := strings.Split(name, ".")
+	def, err := functions.DefaultPackages().GetFunc(n[0], n[1])
+	require.NoError(t, err)
+	require.NotNil(t, def)
+	expr, err := def.Function(args...)
+	require.NoError(t, err)
+	require.NotNil(t, expr)
+	return expr
+}
+
 func ExprRunner(t *testing.T, testfile string) {
 	t.Helper()
 
@@ -142,7 +168,7 @@ func ExprRunner(t *testing.T, testfile string) {
 						// eval it, it should return an error
 						_, err = e.Eval(emptyEnv)
 						require.NotNilf(t, err, "expected expr `%s` to return an error, got nil", stmt.Expr)
-						require.Regexp(t, regexp.MustCompile(stmt.Res), err.Error())
+						require.Regexp(t, regexp.MustCompile(regexp.QuoteMeta(stmt.Res)), err.Error())
 					})
 				}
 			}