Repository: genjidb/genji
Pull request #416 (2021-06-26 13:01:33)

Add support for sequences
This adds support for sequences to Genji. For now, only sequence creation and use is supported, dropping or modifying a sequence will be added later.

To create a sequence:

```sql
CREATE SEQUENCE seq INCREMENT BY 10 MINVALUE 100 NO MAXVALUE START 500 CACHE 32 CYCLE;
```

codecov-commenter: # [Codecov](https://codecov.io/gh/genjidb/genji/pull/416?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=genjidb) Report
> Merging [#416](https://codecov.io/gh/genjidb/genji/pull/416?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=genjidb) (884b315) into [main](https://codecov.io/gh/genjidb/genji/commit/4b13728450580e096daa2602287bfbeaa3528c91?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=genjidb) (4b13728) will **increase** coverage by `0.36%`.
> The diff coverage is `76.16%`.

[![Impacted file tree graph](https://codecov.io/gh/genjidb/genji/pull/416/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/416?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=genjidb)

```diff
@@            Coverage Diff             @@
##             main     #416      +/-   ##
==========================================
+ Coverage   68.17%   68.54%   +0.36%     
==========================================
  Files          84       85       +1     
  Lines        7748     8125     +377     
==========================================
+ Hits         5282     5569     +287     
- Misses       1794     1858      +64     
- Partials      672      698      +26     
```


| [Impacted Files](https://codecov.io/gh/genjidb/genji/pull/416?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=genjidb) | Coverage Δ | |
|---|---|---|
| [internal/database/index.go](https://codecov.io/gh/genjidb/genji/pull/416/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=genjidb#diff-aW50ZXJuYWwvZGF0YWJhc2UvaW5kZXguZ28=) | `63.90% <0.00%> (-1.93%)` | :arrow_down: |
| [internal/expr/expr.go](https://codecov.io/gh/genjidb/genji/pull/416/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=genjidb#diff-aW50ZXJuYWwvZXhwci9leHByLmdv) | `12.72% <0.00%> (-4.78%)` | :arrow_down: |
| [internal/sql/scanner/token.go](https://codecov.io/gh/genjidb/genji/pull/416/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=genjidb#diff-aW50ZXJuYWwvc3FsL3NjYW5uZXIvdG9rZW4uZ28=) | `10.71% <ø> (ø)` | |
| [internal/database/catalog.go](https://codecov.io/gh/genjidb/genji/pull/416/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=genjidb#diff-aW50ZXJuYWwvZGF0YWJhc2UvY2F0YWxvZy5nbw==) | `74.36% <46.96%> (-6.46%)` | :arrow_down: |
| [internal/database/database.go](https://codecov.io/gh/genjidb/genji/pull/416/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=genjidb#diff-aW50ZXJuYWwvZGF0YWJhc2UvZGF0YWJhc2UuZ28=) | `66.66% <60.00%> (-1.34%)` | :arrow_down: |
| [db.go](https://codecov.io/gh/genjidb/genji/pull/416/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=genjidb#diff-ZGIuZ28=) | `54.90% <70.21%> (+0.96%)` | :arrow_up: |
| [internal/query/statement/create.go](https://codecov.io/gh/genjidb/genji/pull/416/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=genjidb#diff-aW50ZXJuYWwvcXVlcnkvc3RhdGVtZW50L2NyZWF0ZS5nbw==) | `82.35% <71.42%> (-2.84%)` | :arrow_down: |
| [internal/sql/parser/expr.go](https://codecov.io/gh/genjidb/genji/pull/416/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=genjidb#diff-aW50ZXJuYWwvc3FsL3BhcnNlci9leHByLmdv) | `80.16% <73.33%> (-0.29%)` | :arrow_down: |
| [internal/database/schema.go](https://codecov.io/gh/genjidb/genji/pull/416/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=genjidb#diff-aW50ZXJuYWwvZGF0YWJhc2Uvc2NoZW1hLmdv) | `88.98% <76.19%> (-3.25%)` | :arrow_down: |
| [internal/database/table.go](https://codecov.io/gh/genjidb/genji/pull/416/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=genjidb#diff-aW50ZXJuYWwvZGF0YWJhc2UvdGFibGUuZ28=) | `55.85% <80.00%> (ø)` | |
| ... and [8 more](https://codecov.io/gh/genjidb/genji/pull/416/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/416?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/416?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=genjidb). Last update [4b13728...884b315](https://codecov.io/gh/genjidb/genji/pull/416?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/db.go b/db.go
index 07266951..ae7b8266 100644
--- a/db.go
+++ b/db.go
@@ -351,54 +351,66 @@ func newDatabase(ctx context.Context, ng engine.Engine, opts database.Options) (
 }
 
 func loadCatalog(tx *database.Transaction) error {
-	tb := tx.Catalog.SchemaTable.GetSchemaTable(tx)
+	tb := tx.Catalog.SchemaTable.GetTable(tx)
 
 	var tables []database.TableInfo
 	var indexes []database.IndexInfo
+	var sequences []database.SequenceInfo
 
 	err := tb.AscendGreaterOrEqual(document.Value{}, func(d document.Document) error {
 		s, err := d.GetByField("sql")
 		if err != nil && err != document.ErrFieldNotFound {
 			return err
 		}
-		if err == nil {
-			stmt, err := parser.NewParser(strings.NewReader(s.V.(string))).ParseStatement()
+		if err == document.ErrFieldNotFound {
+			return nil
+		}
+
+		stmt, err := parser.NewParser(strings.NewReader(s.V.(string))).ParseStatement()
+		if err != nil {
+			return err
+		}
+
+		tp, err := d.GetByField("type")
+		if err != nil {
+			return err
+		}
+
+		switch tp.V.(string) {
+		case database.SchemaTableTableType:
+			ti := stmt.(*statement.CreateTableStmt).Info
+
+			v, err := d.GetByField("store_name")
 			if err != nil {
 				return err
 			}
+			ti.StoreName = v.V.([]byte)
+
+			tables = append(tables, ti)
+		case database.SchemaTableIndexType:
+			i := stmt.(*statement.CreateIndexStmt).Info
 
 			v, err := d.GetByField("store_name")
 			if err != nil {
 				return err
 			}
+			i.StoreName = v.V.([]byte)
 
-			tp, err := d.GetByField("type")
-			if err != nil {
+			cpath, err := d.GetByField("constraint_path")
+			if err != nil && err != document.ErrFieldNotFound {
 				return err
 			}
-
-			switch tp.V.(string) {
-			case database.SchemaTableTableType:
-				ti := stmt.(*statement.CreateTableStmt).Info
-				ti.StoreName = v.V.([]byte)
-				tables = append(tables, ti)
-			case database.SchemaTableIndexType:
-				i := stmt.(*statement.CreateIndexStmt).Info
-				i.StoreName = v.V.([]byte)
-
-				cpath, err := d.GetByField("constraint_path")
-				if err != nil && err != document.ErrFieldNotFound {
+			if err == nil {
+				i.ConstraintPath, err = parser.ParsePath(cpath.V.(string))
+				if err != nil {
 					return err
 				}
-				if err == nil {
-					i.ConstraintPath, err = parser.ParsePath(cpath.V.(string))
-					if err != nil {
-						return err
-					}
-				}
-
-				indexes = append(indexes, i)
 			}
+
+			indexes = append(indexes, i)
+		case database.SchemaTableSequenceType:
+			i := stmt.(*statement.CreateSequenceStmt).Info
+			sequences = append(sequences, i)
 		}
 
 		return nil
@@ -407,6 +419,40 @@ func loadCatalog(tx *database.Transaction) error {
 		return err
 	}
 
-	tx.Catalog.Load(tables, indexes)
+	var seqList []database.Sequence
+
+	if len(sequences) > 0 {
+		seqList, err = loadSequences(tx, sequences)
+		if err != nil {
+			return err
+		}
+	}
+	tx.Catalog.Load(tables, indexes, seqList)
 	return nil
 }
+
+func loadSequences(tx *database.Transaction, info []database.SequenceInfo) ([]database.Sequence, error) {
+	tb := tx.Catalog.SequenceTable.GetTable(tx)
+
+	sequences := make([]database.Sequence, len(info))
+	for i := range info {
+		d, err := tb.GetDocument([]byte(info[i].Name))
+		if err != nil {
+			return nil, err
+		}
+
+		sequences[i].Info = &info[i]
+
+		v, err := d.GetByField("seq")
+		if err != nil && err != document.ErrFieldNotFound {
+			return nil, err
+		}
+
+		if err == nil {
+			v := v.V.(int64)
+			sequences[i].CurrentValue = &v
+		}
+	}
+
+	return sequences, nil
+}
diff --git a/errors/errors.go b/errors/errors.go
index 59249a9d..78c0de36 100644
--- a/errors/errors.go
+++ b/errors/errors.go
@@ -13,6 +13,9 @@ var (
 	// ErrIndexNotFound is returned when the targeted index doesn't exist.
 	ErrIndexNotFound = errors.New("index not found")
 
+	// ErrSequenceNotFound is returned when the targeted sequence doesn't exist.
+	ErrSequenceNotFound = errors.New("sequence not found")
+
 	// ErrDocumentNotFound is returned when no document is associated with the provided key.
 	ErrDocumentNotFound = errors.New("document not found")
 
diff --git a/internal/database/catalog.go b/internal/database/catalog.go
index 3ebfdef7..16350a68 100644
--- a/internal/database/catalog.go
+++ b/internal/database/catalog.go
@@ -13,26 +13,33 @@ import (
 
 // Catalog manages tables and indexes.
 type Catalog struct {
-	cache       *catalogCache
-	SchemaTable *SchemaTable
+	cache         *catalogCache
+	SchemaTable   *SchemaTable
+	SequenceTable *SequenceTable
 }
 
-func NewCatalog(schemaTable *SchemaTable) *Catalog {
+func NewCatalog(schemaTable *SchemaTable, sequenceTable *SequenceTable) *Catalog {
 	return &Catalog{
-		SchemaTable: schemaTable,
-		cache:       newCatalogCache(),
+		SchemaTable:   schemaTable,
+		SequenceTable: sequenceTable,
+		cache:         newCatalogCache(),
 	}
 }
 
-func (c *Catalog) Load(tables []TableInfo, indexes []IndexInfo) {
+func (c *Catalog) Load(tables []TableInfo, indexes []IndexInfo, sequences []Sequence) {
 	// add the __genji_schema table to the list of tables
 	// so that it can be queried
 	ti := c.SchemaTable.info.Clone()
 	// make sure that table is read-only
 	ti.ReadOnly = true
-
 	tables = append(tables, *ti)
-	c.cache.load(tables, indexes)
+
+	// add the __genji_sequence table to the list of tables
+	// so that it can be queried.
+	// That table can be modified
+	tables = append(tables, *c.SequenceTable.info)
+
+	c.cache.load(tables, indexes, sequences)
 }
 
 // Clone the catalog. Mostly used for testing purposes.
@@ -40,6 +47,7 @@ func (c *Catalog) Clone() *Catalog {
 	var clone Catalog
 
 	clone.SchemaTable = c.SchemaTable
+	clone.SequenceTable = c.SequenceTable
 	clone.cache = c.cache.clone()
 
 	return &clone
@@ -298,10 +306,50 @@ func (c *Catalog) ReIndexAll(tx *Transaction) error {
 	return nil
 }
 
+func (c *Catalog) GetSequence(name string) (*Sequence, error) {
+	seq, err := c.cache.GetSequence(name)
+	if err != nil {
+		return nil, err
+	}
+
+	return seq, nil
+}
+
+// CreateSequence creates a sequence with the given name.
+func (c *Catalog) CreateSequence(tx *Transaction, name string, info *SequenceInfo) error {
+	if info == nil {
+		info = new(SequenceInfo)
+	}
+	info.Name = name
+
+	err := c.SchemaTable.insertSequence(tx, info)
+	if err != nil {
+		return err
+	}
+
+	err = c.SequenceTable.InitSequence(tx, info.Name)
+	if err != nil {
+		return err
+	}
+
+	return c.cache.AddSequence(tx, info)
+}
+
+// DropSequence deletes a sequence from the catalog.
+func (c *Catalog) DropSequence(tx *Transaction, name string) error {
+	_, err := c.cache.DeleteSequence(tx, name)
+	if err != nil {
+		return err
+	}
+
+	return c.SchemaTable.deleteSequence(tx, name)
+}
+
 type catalogCache struct {
 	tables           map[string]*TableInfo
 	indexes          map[string]*IndexInfo
 	indexesPerTables map[string][]*IndexInfo
+	sequences        map[string]*Sequence
 
 	mu sync.RWMutex
 }
@@ -311,10 +359,11 @@ func newCatalogCache() *catalogCache {
 		tables:           make(map[string]*TableInfo),
 		indexes:          make(map[string]*IndexInfo),
 		indexesPerTables: make(map[string][]*IndexInfo),
+		sequences:        make(map[string]*Sequence),
 	}
 }
 
-func (c *catalogCache) load(tables []TableInfo, indexes []IndexInfo) {
+func (c *catalogCache) load(tables []TableInfo, indexes []IndexInfo, sequences []Sequence) {
 	c.mu.Lock()
 	defer c.mu.Unlock()
 
@@ -326,6 +375,10 @@ func (c *catalogCache) load(tables []TableInfo, indexes []IndexInfo) {
 		c.indexes[indexes[i].IndexName] = &indexes[i]
 		c.indexesPerTables[indexes[i].TableName] = append(c.indexesPerTables[indexes[i].TableName], &indexes[i])
 	}
+
+	for i := range sequences {
+		c.sequences[sequences[i].Info.Name] = &sequences[i]
+	}
 }
 
 func (c *catalogCache) clone() *catalogCache {
@@ -340,6 +393,9 @@ func (c *catalogCache) clone() *catalogCache {
 	for k, v := range c.indexesPerTables {
 		clone.indexesPerTables[k] = v
 	}
+	for k, v := range c.sequences {
+		clone.sequences[k] = v
+	}
 
 	return clone
 }
@@ -358,6 +414,11 @@ func (c *catalogCache) AddTable(tx *Transaction, info *TableInfo) error {
 		return errs.AlreadyExistsError{Name: info.TableName}
 	}
 
+	// checking if sequence exists with the same name
+	if _, ok := c.sequences[info.TableName]; ok {
+		return errs.AlreadyExistsError{Name: info.TableName}
+	}
+
 	c.tables[info.TableName] = info
 
 	tx.OnRollbackHooks = append(tx.OnRollbackHooks, func() {
@@ -459,10 +520,22 @@ func (c *catalogCache) AddIndex(tx *Transaction, info *IndexInfo) error {
 		}
 	}
 
+	// checking if index exists with the same name
 	if _, ok := c.indexes[info.IndexName]; ok {
 		return errs.AlreadyExistsError{Name: info.IndexName}
 	}
 
+	// checking if table exists with the same name
+	if _, ok := c.tables[info.IndexName]; ok {
+		return errs.AlreadyExistsError{Name: info.IndexName}
+	}
+
+	// checking if sequence exists with the same name
+	if _, ok := c.sequences[info.IndexName]; ok {
+		return errs.AlreadyExistsError{Name: info.IndexName}
+	}
+
+	// get the associated table
 	ti, ok := c.tables[info.TableName]
 	if !ok {
 		return errs.ErrTableNotFound
@@ -641,3 +714,69 @@ func (c *catalogCache) updateTable(tx *Transaction, tableName string, fn func(cl
 
 	return clone, newIndexes, nil
 }
+
+func (c *catalogCache) AddSequence(tx *Transaction, info *SequenceInfo) error {
+	c.mu.Lock()
+	defer c.mu.Unlock()
+
+	// checking if sequence exists with the same name
+	if _, ok := c.sequences[info.Name]; ok {
+		return errs.AlreadyExistsError{Name: info.Name}
+	}
+
+	// checking if table exists with the same name
+	if _, ok := c.tables[info.Name]; ok {
+		return errs.AlreadyExistsError{Name: info.Name}
+	}
+
+	// checking if index exists with the same name
+	if _, ok := c.indexes[info.Name]; ok {
+		return errs.AlreadyExistsError{Name: info.Name}
+	}
+
+	c.sequences[info.Name] = &Sequence{Info: info}
+
+	tx.OnRollbackHooks = append(tx.OnRollbackHooks, func() {
+		c.mu.Lock()
+		defer c.mu.Unlock()
+
+		delete(c.sequences, info.Name)
+	})
+
+	return nil
+}
+
+func (c *catalogCache) GetSequence(name string) (*Sequence, error) {
+	c.mu.RLock()
+	defer c.mu.RUnlock()
+
+	seq, ok := c.sequences[name]
+	if !ok {
+		return nil, errs.ErrSequenceNotFound
+	}
+
+	return seq, nil
+}
+
+func (c *catalogCache) DeleteSequence(tx *Transaction, name string) (*Sequence, error) {
+	c.mu.Lock()
+	defer c.mu.Unlock()
+
+	// check if the sequence exists
+	seq, ok := c.sequences[name]
+	if !ok {
+		return nil, errs.ErrSequenceNotFound
+	}
+
+	// remove it from the global map of sequences
+	delete(c.sequences, name)
+
+	tx.OnRollbackHooks = append(tx.OnRollbackHooks, func() {
+		c.mu.Lock()
+		defer c.mu.Unlock()
+
+		c.sequences[name] = seq
+	})
+
+	return seq, nil
+}
diff --git a/internal/database/config.go b/internal/database/config.go
index e0d41515..65b76961 100644
--- a/internal/database/config.go
+++ b/internal/database/config.go
@@ -18,18 +18,6 @@ type TableInfo struct {
 	FieldConstraints FieldConstraints
 }
 
-// GetPrimaryKey returns the field constraint of the primary key.
-// Returns nil if there is no primary key.
-func (ti *TableInfo) GetPrimaryKey() *FieldConstraint {
-	for _, f := range ti.FieldConstraints {
-		if f.IsPrimaryKey {
-			return f
-		}
-	}
-
-	return nil
-}
-
 // String returns a SQL representation.
 func (ti *TableInfo) String() string {
 	var s strings.Builder
@@ -126,24 +114,51 @@ func (i IndexInfo) Clone() *IndexInfo {
 	return &c
 }
 
-type Indexes []*Index
+// SequenceInfo holds the configuration of a sequence.
+type SequenceInfo struct {
+	Name        string
+	IncrementBy int64
+	Min, Max    int64
+	Start       int64
+	Cache       uint64
+	Cycle       bool
+}
 
-func (i Indexes) GetIndex(name string) *Index {
-	for _, idx := range i {
-		if idx.Info.IndexName == name {
-			return idx
-		}
+// String returns a SQL representation.
+func (s *SequenceInfo) String() string {
+	var b strings.Builder
+
+	b.WriteString("CREATE SEQUENCE ")
+	b.WriteString(s.Name)
+
+	if s.IncrementBy != 0 {
+		stringutil.Fprintf(&b, " INCREMENT BY %d", s.IncrementBy)
 	}
 
-	return nil
-}
+	if s.Min != 0 {
+		stringutil.Fprintf(&b, " MINVALUE %d", s.Min)
+	}
 
-func (i Indexes) GetIndexByPath(p document.Path) *Index {
-	for _, idx := range i {
-		if idx.Info.Paths[0].IsEqual(p) {
-			return idx
-		}
+	if s.Max != 0 {
+		stringutil.Fprintf(&b, " MAXVALUE %d", s.Max)
+	}
+
+	if s.Start != 0 {
+		stringutil.Fprintf(&b, " START WITH %d", s.Start)
+	}
+
+	if s.Cache != 0 {
+		stringutil.Fprintf(&b, " CACHE %d", s.Cache)
+	}
+
+	if s.Cycle {
+		b.WriteString(" CYCLE")
 	}
 
-	return nil
+	return b.String()
+}
+
+// Clone returns a copy of the sequence information.
+func (s SequenceInfo) Clone() *SequenceInfo {
+	return &s
 }
diff --git a/internal/database/conflict.go b/internal/database/conflict.go
index 508cb0ec..16c891f2 100644
--- a/internal/database/conflict.go
+++ b/internal/database/conflict.go
@@ -28,6 +28,6 @@ func OnInsertConflictDoReplace(t *Table, key []byte, d document.Document, err er
 	return documentWithKey{
 		Document: d,
 		key:      key,
-		pk:       t.Info.GetPrimaryKey(),
+		pk:       t.Info.FieldConstraints.GetPrimaryKey(),
 	}, nil
 }
diff --git a/internal/database/constraint.go b/internal/database/constraint.go
index f3c85c59..9720d239 100644
--- a/internal/database/constraint.go
+++ b/internal/database/constraint.go
@@ -129,6 +129,18 @@ func (f FieldConstraints) Get(path document.Path) *FieldConstraint {
 	return nil
 }
 
+// GetPrimaryKey returns the field constraint of the primary key.
+// Returns nil if there is no primary key.
+func (f FieldConstraints) GetPrimaryKey() *FieldConstraint {
+	for _, fc := range f {
+		if fc.IsPrimaryKey {
+			return fc
+		}
+	}
+
+	return nil
+}
+
 // Infer additional constraints based on user defined ones.
 // For example, given the following table:
 //   CREATE TABLE foo (a.b[0] TEXT)
diff --git a/internal/database/database.go b/internal/database/database.go
index bab01aa0..18240f18 100644
--- a/internal/database/database.go
+++ b/internal/database/database.go
@@ -51,13 +51,19 @@ func New(ctx context.Context, ng engine.Engine, opts Options) (*Database, error)
 	defer tx.Rollback()
 
 	schemaTable := NewSchemaTable(tx)
+	sequenceTable := NewSequenceTable(tx)
 
 	err = schemaTable.Init(tx)
 	if err != nil {
 		return nil, err
 	}
 
-	db.Catalog = NewCatalog(schemaTable)
+	err = sequenceTable.Init(tx)
+	if err != nil {
+		return nil, err
+	}
+
+	db.Catalog = NewCatalog(schemaTable, sequenceTable)
 
 	err = tx.Commit()
 	return &db, err
diff --git a/internal/database/index.go b/internal/database/index.go
index 72e731b6..e45f0df1 100644
--- a/internal/database/index.go
+++ b/internal/database/index.go
@@ -513,3 +513,25 @@ func (idx *Index) iterate(st engine.Store, pivot Pivot, reverse bool, fn func(it
 
 	return nil
 }
+
+type Indexes []*Index
+
+func (i Indexes) GetIndex(name string) *Index {
+	for _, idx := range i {
+		if idx.Info.IndexName == name {
+			return idx
+		}
+	}
+
+	return nil
+}
+
+func (i Indexes) GetIndexByPath(p document.Path) *Index {
+	for _, idx := range i {
+		if idx.Info.Paths[0].IsEqual(p) {
+			return idx
+		}
+	}
+
+	return nil
+}
diff --git a/internal/database/schema.go b/internal/database/schema.go
index 72036726..8d13b9ee 100644
--- a/internal/database/schema.go
+++ b/internal/database/schema.go
@@ -14,9 +14,10 @@ const (
 )
 
 const (
-	SchemaTableName      = internalPrefix + "schema"
-	SchemaTableTableType = "table"
-	SchemaTableIndexType = "index"
+	SchemaTableName         = internalPrefix + "schema"
+	SchemaTableTableType    = "table"
+	SchemaTableIndexType    = "index"
+	SchemaTableSequenceType = "sequence"
 )
 
 type SchemaTable struct {
@@ -83,7 +84,7 @@ func (s *SchemaTable) Init(tx *Transaction) error {
 	return err
 }
 
-func (s *SchemaTable) GetSchemaTable(tx *Transaction) *Table {
+func (s *SchemaTable) GetTable(tx *Transaction) *Table {
 	st, err := tx.Tx.GetStore([]byte(SchemaTableName))
 	if err != nil {
 		panic(stringutil.Sprintf("database incorrectly setup: missing %q table: %v", SchemaTableName, err))
@@ -119,10 +120,19 @@ func (s *SchemaTable) indexInfoToDocument(i *IndexInfo) document.Document {
 	return buf
 }
 
+func (s *SchemaTable) sequenceInfoToDocument(seq *SequenceInfo) document.Document {
+	buf := document.NewFieldBuffer()
+	buf.Add("name", document.NewTextValue(seq.Name))
+	buf.Add("type", document.NewTextValue(SchemaTableSequenceType))
+	buf.Add("sql", document.NewTextValue(seq.String()))
+
+	return buf
+}
+
 // insertTable inserts a new tableInfo for the given table name.
 // If info.StoreName is nil, it generates one and stores it in info.
 func (s *SchemaTable) insertTable(tx *Transaction, tableName string, info *TableInfo) error {
-	tb := s.GetSchemaTable(tx)
+	tb := s.GetTable(tx)
 
 	if info.StoreName == nil {
 		seq, err := tb.Store.NextSequence()
@@ -144,20 +154,20 @@ func (s *SchemaTable) insertTable(tx *Transaction, tableName string, info *Table
 
 // Replace replaces tableName table information with the new info.
 func (s *SchemaTable) replaceTable(tx *Transaction, tableName string, info *TableInfo) error {
-	tb := s.GetSchemaTable(tx)
+	tb := s.GetTable(tx)
 
 	_, err := tb.Replace([]byte(tableName), s.tableInfoToDocument(info))
 	return err
 }
 
 func (s *SchemaTable) deleteTable(tx *Transaction, tableName string) error {
-	tb := s.GetSchemaTable(tx)
+	tb := s.GetTable(tx)
 
 	return tb.Delete([]byte(tableName))
 }
 
 func (s *SchemaTable) insertIndex(tx *Transaction, info *IndexInfo) error {
-	tb := s.GetSchemaTable(tx)
+	tb := s.GetTable(tx)
 
 	if info.StoreName == nil {
 		seq, err := tb.Store.NextSequence()
@@ -179,14 +189,38 @@ func (s *SchemaTable) insertIndex(tx *Transaction, info *IndexInfo) error {
 }
 
 func (s *SchemaTable) replaceIndex(tx *Transaction, indexName string, info *IndexInfo) error {
-	tb := s.GetSchemaTable(tx)
+	tb := s.GetTable(tx)
 
 	_, err := tb.Replace([]byte(indexName), s.indexInfoToDocument(info))
 	return err
 }
 
 func (s *SchemaTable) deleteIndex(tx *Transaction, indexName string) error {
-	tb := s.GetSchemaTable(tx)
+	tb := s.GetTable(tx)
 
 	return tb.Delete([]byte(indexName))
 }
+
+func (s *SchemaTable) insertSequence(tx *Transaction, info *SequenceInfo) error {
+	tb := s.GetTable(tx)
+
+	_, err := tb.Insert(s.sequenceInfoToDocument(info))
+	if err == errs.ErrDuplicateDocument {
+		return errs.AlreadyExistsError{Name: info.Name}
+	}
+
+	return err
+}
+
+func (s *SchemaTable) replaceSequence(tx *Transaction, name string, info *SequenceInfo) error {
+	tb := s.GetTable(tx)
+
+	_, err := tb.Replace([]byte(name), s.sequenceInfoToDocument(info))
+	return err
+}
+
+func (s *SchemaTable) deleteSequence(tx *Transaction, name string) error {
+	tb := s.GetTable(tx)
+
+	return tb.Delete([]byte(name))
+}
diff --git a/internal/database/sequence.go b/internal/database/sequence.go
new file mode 100644
index 00000000..f7675f06
--- /dev/null
+++ b/internal/database/sequence.go
@@ -0,0 +1,179 @@
+package database
+
+import (
+	"errors"
+
+	"github.com/genjidb/genji/document"
+	"github.com/genjidb/genji/engine"
+	"github.com/genjidb/genji/internal/stringutil"
+)
+
+const (
+	SequenceTableName = internalPrefix + "sequence"
+)
+
+// A Sequence manages a sequence of numbers.
+// It is not thread safe.
+type Sequence struct {
+	Info *SequenceInfo
+
+	CurrentValue *int64
+	Cached       uint64
+}
+
+func (s *Sequence) Next(tx *Transaction) (int64, error) {
+	if !tx.Writable {
+		return 0, errors.New("cannot increment sequence on read-only transaction")
+	}
+
+	var newValue int64
+	if s.CurrentValue == nil {
+		newValue = s.Info.Start
+	} else {
+		newValue = *s.CurrentValue + s.Info.IncrementBy
+	}
+
+	if newValue < s.Info.Min {
+		if !s.Info.Cycle {
+			return 0, stringutil.Errorf("reached minimum value of sequence %s", s.Info.Name)
+		}
+
+		newValue = s.Info.Max
+	}
+	if newValue > s.Info.Max {
+		if !s.Info.Cycle {
+			return 0, stringutil.Errorf("reached maximum value of sequence %s", s.Info.Name)
+		}
+
+		newValue = s.Info.Min
+	}
+
+	var newLease int64
+
+	s.Cached++
+
+	// if the number of cached values is less than or equal to the cache,
+	// we don't increase the lease.
+	if s.CurrentValue != nil && s.Cached <= s.Info.Cache {
+		s.CurrentValue = &newValue
+		return newValue, nil
+	}
+
+	// we need to reset the number of cached values to 1
+	if s.CurrentValue != nil {
+		s.Cached = 1
+	}
+
+	// calculate the new lease depending on the direction
+	// of the sequence
+	if s.Info.IncrementBy > 0 {
+		newLease = newValue + int64(s.Info.Cache) - 1
+		if newLease > s.Info.Max {
+			newLease = s.Info.Max
+		}
+	} else {
+		newLease = newValue - int64(s.Info.Cache) + 1
+		if newLease < s.Info.Min {
+			newLease = s.Info.Min
+		}
+	}
+
+	// store the new lease
+	err := tx.Catalog.SequenceTable.SetLease(tx, s.Info.Name, newLease)
+	if err != nil {
+		return 0, err
+	}
+
+	s.CurrentValue = &newValue
+	return newValue, nil
+}
+
+type SequenceTable struct {
+	info *TableInfo
+}
+
+func NewSequenceTable(tx *Transaction) *SequenceTable {
+	return &SequenceTable{
+		info: &TableInfo{
+			TableName: SequenceTableName,
+			StoreName: []byte(SequenceTableName),
+			FieldConstraints: []*FieldConstraint{
+				{
+					Path: document.Path{
+						document.PathFragment{
+							FieldName: "name",
+						},
+					},
+					Type:         document.TextValue,
+					IsPrimaryKey: true,
+				},
+				{
+					Path: document.Path{
+						document.PathFragment{
+							FieldName: "seq",
+						},
+					},
+					Type: document.IntegerValue,
+				},
+			},
+		},
+	}
+}
+
+func (s *SequenceTable) Init(tx *Transaction) error {
+	_, err := tx.Tx.GetStore([]byte(SequenceTableName))
+	if err == engine.ErrStoreNotFound {
+		err = tx.Tx.CreateStore([]byte(SequenceTableName))
+	}
+	return err
+}
+
+func (s *SequenceTable) GetTable(tx *Transaction) *Table {
+	st, err := tx.Tx.GetStore([]byte(SequenceTableName))
+	if err != nil {
+		panic(stringutil.Sprintf("database incorrectly setup: missing %q table: %v", SequenceTableName, err))
+	}
+
+	return &Table{
+		Tx:    tx,
+		Store: st,
+		Info:  s.info,
+	}
+}
+
+func (s *SequenceTable) InitSequence(tx *Transaction, name string) error {
+	tb := s.GetTable(tx)
+
+	_, err := tb.Insert(document.NewFieldBuffer().Add("name", document.NewTextValue(name)))
+	return err
+}
+
+func (s *SequenceTable) SetLease(tx *Transaction, name string, v int64) error {
+	tb := s.GetTable(tx)
+
+	_, err := tb.Replace([]byte(name),
+		document.NewFieldBuffer().
+			Add("name", document.NewTextValue(name)).
+			Add("seq", document.NewIntegerValue(v)),
+	)
+	return err
+}
+
+func (s *SequenceTable) GetLease(tx *Transaction, name string) (*int64, error) {
+	tb := s.GetTable(tx)
+
+	d, err := tb.GetDocument([]byte(name))
+	if err != nil {
+		return nil, err
+	}
+
+	v, err := d.GetByField("seq")
+	if err == document.ErrFieldNotFound {
+		return nil, nil
+	}
+	if err != nil {
+		return nil, err
+	}
+	lease := v.V.(int64)
+	return &lease, nil
+}
diff --git a/internal/database/table.go b/internal/database/table.go
index e86d6978..27856c8d 100644
--- a/internal/database/table.go
+++ b/internal/database/table.go
@@ -146,7 +146,7 @@ func (t *Table) InsertWithConflictResolution(d document.Document, onConflict OnI
 	return documentWithKey{
 		Document: fb,
 		key:      key,
-		pk:       t.Info.GetPrimaryKey(),
+		pk:       t.Info.FieldConstraints.GetPrimaryKey(),
 	}, nil
 }
 
@@ -388,7 +388,7 @@ func (t *Table) EncodeValue(v document.Value) ([]byte, error) {
 func (t *Table) encodeValueToKey(info *TableInfo, v document.Value) ([]byte, error) {
 	var err error
 
-	pk := t.Info.GetPrimaryKey()
+	pk := t.Info.FieldConstraints.GetPrimaryKey()
 	if pk == nil {
 		// if no primary key was defined, convert the pivot to an integer then to an unsigned integer
 		// and encode it as a varint
@@ -467,7 +467,7 @@ func (t *Table) iterate(pivot document.Value, reverse bool, fn func(d document.D
 		codec: t.Tx.DB.Codec,
 	}
 
-	d.pk = t.Info.GetPrimaryKey()
+	d.pk = t.Info.FieldConstraints.GetPrimaryKey()
 
 	it := t.Store.Iterator(engine.IteratorOptions{Reverse: reverse})
 	defer it.Close()
@@ -503,7 +503,7 @@ func (t *Table) GetDocument(key []byte) (document.Document, error) {
 	var d documentWithKey
 	d.Document = t.Tx.DB.Codec.NewDecoder(v)
 	d.key = key
-	d.pk = t.Info.GetPrimaryKey()
+	d.pk = t.Info.FieldConstraints.GetPrimaryKey()
 	return &d, err
 }
 
@@ -514,7 +514,7 @@ func (t *Table) GetDocument(key []byte) (document.Document, error) {
 // if there are no primary key in the table, a default
 // key is generated, called the docid.
 func (t *Table) generateKey(info *TableInfo, d document.Document) ([]byte, error) {
-	if pk := t.Info.GetPrimaryKey(); pk != nil {
+	if pk := t.Info.FieldConstraints.GetPrimaryKey(); pk != nil {
 		v, err := pk.Path.GetValueFromDocument(d)
 		if err == document.ErrFieldNotFound {
 			return nil, stringutil.Errorf("missing primary key at path %q", pk.Path)
diff --git a/internal/expr/expr.go b/internal/expr/expr.go
index f548d5ba..bf733999 100644
--- a/internal/expr/expr.go
+++ b/internal/expr/expr.go
@@ -130,3 +130,42 @@ func Walk(e Expr, fn func(Expr) bool) bool {
 
 	return false
 }
+
+type NextValueFor struct {
+	SeqName string
+}
+
+// Eval calls the underlying expression Eval method.
+func (n NextValueFor) Eval(env *Environment) (document.Value, error) {
+	tx := env.GetTx()
+	seq, err := tx.Catalog.GetSequence(n.SeqName)
+	if err != nil {
+		return nullLitteral, err
+	}
+
+	i, err := seq.Next(tx)
+	if err != nil {
+		return nullLitteral, err
+	}
+
+	return document.NewIntegerValue(i), nil
+}
+
+// IsEqual compares this expression with the other expression and returns
+// true if they are equal.
+func (n NextValueFor) IsEqual(other Expr) bool {
+	if other == nil {
+		return false
+	}
+
+	o, ok := other.(NextValueFor)
+	if !ok {
+		return false
+	}
+
+	return o.SeqName == n.SeqName
+}
+
+func (n NextValueFor) String() string {
+	return stringutil.Sprintf("NEXT VALUE FOR %s", n.SeqName)
+}
diff --git a/internal/planner/optimizer.go b/internal/planner/optimizer.go
index 1eb92cae..216994a8 100644
--- a/internal/planner/optimizer.go
+++ b/internal/planner/optimizer.go
@@ -332,7 +332,7 @@ func RemoveUnnecessaryDistinctNodeRule(s *stream.Stream, tx *database.Transactio
 				pn, ok := prev.(*stream.ProjectOperator)
 				if ok {
 					// if the projection is unique, we remove the node from the tree
-					if isProjectionUnique(t.Indexes, pn, t.Info.GetPrimaryKey()) {
+					if isProjectionUnique(t.Indexes, pn, t.Info.FieldConstraints.GetPrimaryKey()) {
 						s.Remove(n)
 						n = prev
 						continue
@@ -437,7 +437,7 @@ func UseIndexBasedOnFilterNodeRule(s *stream.Stream, tx *database.Transaction) (
 			filterNodes = append(filterNodes, filterNode{path: path, e: e, f: f})
 
 			// check for primary keys scan while iterating on the filter nodes
-			if pk := t.Info.GetPrimaryKey(); pk != nil && pk.Path.IsEqual(path) {
+			if pk := t.Info.FieldConstraints.GetPrimaryKey(); pk != nil && pk.Path.IsEqual(path) {
 				// // if both types are different, don't select this scanner
 				// v, ok, err := operandCanUseIndex(pk.Type, pk.Path, t.Info.FieldConstraints, v)
 				// if err != nil {
diff --git a/internal/query/statement/create.go b/internal/query/statement/create.go
index 164d18e7..3a0e26ac 100644
--- a/internal/query/statement/create.go
+++ b/internal/query/statement/create.go
@@ -7,7 +7,7 @@ import (
 	"github.com/genjidb/genji/internal/expr"
 )
 
-// CreateTableStmt is a DSL that allows creating a full CREATE TABLE statement.
+// CreateTableStmt represents a parsed CREATE TABLE statement.
 type CreateTableStmt struct {
 	IfNotExists bool
 	Info        database.TableInfo
@@ -48,8 +48,7 @@ func (stmt *CreateTableStmt) Run(tx *database.Transaction, args []expr.Param) (R
 	return res, err
 }
 
-// CreateIndexStmt is a DSL that allows creating a full CREATE INDEX statement.
-// It is typically created using the CreateIndex function.
+// CreateIndexStmt represents a parsed CREATE INDEX statement.
 type CreateIndexStmt struct {
 	IfNotExists bool
 	Info        database.IndexInfo
@@ -78,3 +77,28 @@ func (stmt *CreateIndexStmt) Run(tx *database.Transaction, args []expr.Param) (R
 	err = tx.Catalog.ReIndex(tx, stmt.Info.IndexName)
 	return res, err
 }
+
+// CreateSequenceStmt represents a parsed CREATE SEQUENCE statement.
+type CreateSequenceStmt struct {
+	IfNotExists bool
+	Info        database.SequenceInfo
+}
+
+// IsReadOnly always returns false. It implements the Statement interface.
+func (stmt *CreateSequenceStmt) IsReadOnly() bool {
+	return false
+}
+
+// Run the statement in the given transaction.
+// It implements the Statement interface.
+func (stmt *CreateSequenceStmt) Run(tx *database.Transaction, args []expr.Param) (Result, error) {
+	var res Result
+
+	err := tx.Catalog.CreateSequence(tx, stmt.Info.Name, &stmt.Info)
+	if stmt.IfNotExists {
+		if _, ok := err.(errs.AlreadyExistsError); ok {
+			return res, nil
+		}
+	}
+	return res, err
+}
diff --git a/internal/sql/parser/create.go b/internal/sql/parser/create.go
index f4c0551f..4b1104c5 100644
--- a/internal/sql/parser/create.go
+++ b/internal/sql/parser/create.go
@@ -1,6 +1,8 @@
 package parser
 
 import (
+	"math"
+
 	"github.com/genjidb/genji/internal/database"
 	"github.com/genjidb/genji/internal/expr"
 	"github.com/genjidb/genji/internal/query/statement"
@@ -23,9 +25,11 @@ func (p *Parser) parseCreateStatement() (statement.Statement, error) {
 		return p.parseCreateIndexStatement(true)
 	case scanner.INDEX:
 		return p.parseCreateIndexStatement(false)
+	case scanner.SEQUENCE:
+		return p.parseCreateSequenceStatement()
 	}
 
-	return nil, newParseError(scanner.Tokstr(tok, lit), []string{"TABLE", "INDEX"}, pos)
+	return nil, newParseError(scanner.Tokstr(tok, lit), []string{"TABLE", "INDEX", "SEQUENCE"}, pos)
 }
 
 // parseCreateTableStatement parses a create table string and returns a Statement AST object.
@@ -226,7 +230,7 @@ func (p *Parser) parseTableConstraint(stmt *statement.CreateTableStmt) (bool, er
 			return false, err
 		}
 
-		if pk := stmt.Info.GetPrimaryKey(); pk != nil {
+		if pk := stmt.Info.FieldConstraints.GetPrimaryKey(); pk != nil {
 			return false, stringutil.Errorf("table %q has more than one primary key", stmt.Info.TableName)
 		}
 		fc := stmt.Info.FieldConstraints.Get(primaryKeyPath)
@@ -329,3 +333,231 @@ func (p *Parser) parseCreateIndexStatement(unique bool) (*statement.CreateIndexS
 
 	return &stmt, nil
 }
+
+// This function assumes the CREATE SEQUENCE tokens have already been consumed.
+func (p *Parser) parseCreateSequenceStatement() (*statement.CreateSequenceStmt, error) {
+	var stmt statement.CreateSequenceStmt
+	var err error
+
+	// Parse IF NOT EXISTS
+	stmt.IfNotExists, err = p.parseOptional(scanner.IF, scanner.NOT, scanner.EXISTS)
+	if err != nil {
+		return nil, err
+	}
+
+	// Parse sequence name
+	stmt.Info.Name, err = p.parseIdent()
+	if err != nil {
+		return nil, err
+	}
+
+	var hasAsInt, hasNoMin, hasNoMax, hasNoCycle bool
+	var min, max, incrementBy, start, cache *int64
+
+	for {
+		// Parse AS [any int type]
+		// Only integers are supported
+		if ok, _ := p.parseOptional(scanner.AS); ok {
+			tok, pos, lit := p.ScanIgnoreWhitespace()
+			switch tok {
+			case scanner.TYPEINTEGER, scanner.TYPEINT, scanner.TYPEINT2, scanner.TYPEINT8, scanner.TYPETINYINT,
+				scanner.TYPEBIGINT, scanner.TYPEMEDIUMINT, scanner.TYPESMALLINT:
+			default:
+				return nil, newParseError(scanner.Tokstr(tok, lit), []string{"INT"}, pos)
+			}
+
+			if hasAsInt {
+				return nil, &ParseError{Message: "conflicting or redundant options"}
+			}
+
+			hasAsInt = true
+			continue
+		}
+
+		// Parse INCREMENT [BY] integer
+		if ok, _ := p.parseOptional(scanner.INCREMENT); ok {
+			// parse optional BY token
+			_, _ = p.parseOptional(scanner.BY)
+
+			if incrementBy != nil {
+				return nil, &ParseError{Message: "conflicting or redundant options"}
+			}
+
+			i, err := p.parseInteger()
+			if err != nil {
+				return nil, err
+			}
+			if i == 0 {
+				return nil, &ParseError{Message: "INCREMENT must not be zero"}
+			}
+			incrementBy = &i
+
+			continue
+		}
+
+		// Parse NO [MINVALUE | MAXVALUE | CYCLE]
+		if ok, _ := p.parseOptional(scanner.NO); ok {
+			tok, pos, lit := p.ScanIgnoreWhitespace()
+
+			if tok == scanner.MINVALUE {
+				if hasNoMin {
+					return nil, &ParseError{Message: "conflicting or redundant options"}
+				}
+				hasNoMin = true
+				continue
+			}
+
+			if tok == scanner.MAXVALUE {
+				if hasNoMax {
+					return nil, &ParseError{Message: "conflicting or redundant options"}
+				}
+				hasNoMax = true
+				continue
+			}
+
+			if tok == scanner.CYCLE {
+				if hasNoCycle {
+					return nil, &ParseError{Message: "conflicting or redundant options"}
+				}
+				hasNoCycle = true
+				continue
+			}
+
+			return nil, newParseError(scanner.Tokstr(tok, lit), []string{"MINVALUE", "MAXVALUE", "CYCLE"}, pos)
+		}
+
+		// Parse MINVALUE integer
+		if ok, _ := p.parseOptional(scanner.MINVALUE); ok {
+			if hasNoMin || min != nil {
+				return nil, &ParseError{Message: "conflicting or redundant options"}
+			}
+			i, err := p.parseInteger()
+			if err != nil {
+				return nil, err
+			}
+			min = &i
+			continue
+		}
+
+		// Parse MAXVALUE integer
+		if ok, _ := p.parseOptional(scanner.MAXVALUE); ok {
+			if hasNoMax || max != nil {
+				return nil, &ParseError{Message: "conflicting or redundant options"}
+			}
+			i, err := p.parseInteger()
+			if err != nil {
+				return nil, err
+			}
+			max = &i
+			continue
+		}
+
+		// Parse START [WITH] integer
+		if ok, _ := p.parseOptional(scanner.START); ok {
+			// parse optional WITH token
+			_, _ = p.parseOptional(scanner.WITH)
+
+			if start != nil {
+				return nil, &ParseError{Message: "conflicting or redundant options"}
+			}
+
+			i, err := p.parseInteger()
+			if err != nil {
+				return nil, err
+			}
+			start = &i
+			continue
+		}
+
+		// Parse CACHE integer
+		if ok, _ := p.parseOptional(scanner.CACHE); ok {
+			if cache != nil {
+				return nil, &ParseError{Message: "conflicting or redundant options"}
+			}
+
+			v, err := p.parseInteger()
+			if err != nil {
+				return nil, err
+			}
+			if v < 0 {
+				return nil, &ParseError{Message: "cache value must be positive"}
+			}
+			cache = &v
+
+			continue
+		}
+
+		// Parse CYCLE
+		if ok, _ := p.parseOptional(scanner.CYCLE); ok {
+			if hasNoCycle || stmt.Info.Cycle {
+				return nil, &ParseError{Message: "conflicting or redundant options"}
+			}
+
+			stmt.Info.Cycle = true
+			continue
+		}
+
+		break
+	}
+
+	// default value for increment is 1
+	if incrementBy != nil {
+		stmt.Info.IncrementBy = *incrementBy
+	} else {
+		stmt.Info.IncrementBy = 1
+	}
+
+	// determine if the sequence is ascending or descending
+	asc := stmt.Info.IncrementBy > 0
+
+	// default value for min is 1 if ascending
+	// or the minimum value of ints if descending
+	if min != nil {
+		stmt.Info.Min = *min
+	} else if asc {
+		stmt.Info.Min = 1
+	} else {
+		stmt.Info.Min = math.MinInt64
+	}
+
+	// default value for max is the maximum value of ints if ascending
+	// or the -1 if descending
+	if max != nil {
+		stmt.Info.Max = *max
+	} else if asc {
+		stmt.Info.Max = math.MaxInt64
+	} else {
+		stmt.Info.Max = -1
+	}
+
+	// check if min > max
+	if stmt.Info.Min > stmt.Info.Max {
+		return nil, &ParseError{Message: stringutil.Sprintf("MINVALUE (%d) must be less than MAXVALUE (%d)", stmt.Info.Min, stmt.Info.Max)}
+	}
+
+	// default value for start is min if ascending
+	// or max if descending
+	if start != nil {
+		stmt.Info.Start = *start
+	} else if asc {
+		stmt.Info.Start = stmt.Info.Min
+	} else {
+		stmt.Info.Start = stmt.Info.Max
+	}
+
+	// check if min < start < max
+	if stmt.Info.Start < stmt.Info.Min {
+		return nil, &ParseError{Message: stringutil.Sprintf("START value (%d) cannot be less than MINVALUE (%d)", stmt.Info.Start, stmt.Info.Min)}
+	}
+	if stmt.Info.Start > stmt.Info.Max {
+		return nil, &ParseError{Message: stringutil.Sprintf("START value (%d) cannot be greater than MAXVALUE (%d)", stmt.Info.Start, stmt.Info.Max)}
+	}
+
+	// default for cache is 1
+	if cache != nil {
+		stmt.Info.Cache = uint64(*cache)
+	} else {
+		stmt.Info.Cache = 1
+	}
+	return &stmt, err
+}
diff --git a/internal/sql/parser/expr.go b/internal/sql/parser/expr.go
index 362d0468..9d9c33d1 100644
--- a/internal/sql/parser/expr.go
+++ b/internal/sql/parser/expr.go
@@ -253,11 +253,37 @@ func (p *Parser) parseUnaryExpr() (expr.Expr, error) {
 			return nil, err
 		}
 		return expr.Not(e), nil
+	case scanner.NEXT:
+		err := p.parseTokens(scanner.VALUE, scanner.FOR)
+		if err != nil {
+			return nil, err
+		}
+		seqName, err := p.parseIdent()
+		if err != nil {
+			return nil, err
+		}
+
+		return expr.NextValueFor{SeqName: seqName}, nil
 	default:
 		return nil, newParseError(scanner.Tokstr(tok, lit), []string{"identifier", "string", "number", "bool"}, pos)
 	}
 }
 
+// parseInteger parses an integer.
+func (p *Parser) parseInteger() (int64, error) {
+	tok, pos, lit := p.ScanIgnoreWhitespace()
+	if tok != scanner.INTEGER {
+		return 0, newParseError(scanner.Tokstr(tok, lit), []string{"integer"}, pos)
+	}
+
+	v, err := strconv.ParseInt(lit, 10, 64)
+	if err != nil {
+		return 0, newParseError(scanner.Tokstr(tok, lit), []string{"INT"}, pos)
+	}
+
+	return v, nil
+}
+
 // parseIdent parses an identifier.
 func (p *Parser) parseIdent() (string, error) {
 	tok, pos, lit := p.ScanIgnoreWhitespace()
diff --git a/internal/sql/scanner/token.go b/internal/sql/scanner/token.go
index 42157440..22487000 100644
--- a/internal/sql/scanner/token.go
+++ b/internal/sql/scanner/token.go
@@ -41,7 +41,6 @@ const (
 	BITWISEAND // &
 	BITWISEOR  // |
 	BITWISEXOR // ^
-	BETWEEN    // BETWEEN
 
 	AND // AND
 	OR  // OR
@@ -60,6 +59,7 @@ const (
 	ISN      // IS NOT
 	LIKE     // LIKE
 	CONCAT   // ||
+	BETWEEN  // BETWEEN
 	operatorEnd
 
 	LPAREN      // (
@@ -83,10 +83,12 @@ const (
 	ASC
 	BEGIN
 	BY
+	CACHE
 	CAST
 	COMMIT
 	CONFLICT
 	CREATE
+	CYCLE
 	DEFAULT
 	DELETE
 	DESC
@@ -96,15 +98,21 @@ const (
 	EXISTS
 	EXPLAIN
 	FIELD
+	FOR
 	FROM
 	GROUP
 	IF
 	IGNORE
+	INCREMENT
 	INDEX
 	INSERT
 	INTO
 	KEY
 	LIMIT
+	MAXVALUE
+	MINVALUE
+	NEXT
+	NO
 	NOT
 	NOTHING
 	OFFSET
@@ -120,7 +128,9 @@ const (
 	RETURNING
 	ROLLBACK
 	SELECT
+	SEQUENCE
 	SET
+	START
 	TABLE
 	TO
 	TRANSACTION
@@ -128,7 +138,9 @@ const (
 	UNIQUE
 	UNSET
 	UPDATE
+	VALUE
 	VALUES
+	WITH
 	WHERE
 	WRITE
 
@@ -215,10 +227,12 @@ var tokens = [...]string{
 	ASC:         "ASC",
 	BEGIN:       "BEGIN",
 	BY:          "BY",
+	CACHE:       "CACHE",
 	CAST:        "CAST",
 	COMMIT:      "COMMIT",
 	CONFLICT:    "CONFLICT",
 	CREATE:      "CREATE",
+	CYCLE:       "CYCLE",
 	DO:          "DO",
 	DEFAULT:     "DEFAULT",
 	DELETE:      "DELETE",
@@ -230,13 +244,19 @@ var tokens = [...]string{
 	GROUP:       "GROUP",
 	KEY:         "KEY",
 	FIELD:       "FIELD",
+	FOR:         "FOR",
 	FROM:        "FROM",
 	IF:          "IF",
 	IGNORE:      "IGNORE",
+	INCREMENT:   "INCREMENT",
 	INDEX:       "INDEX",
 	INSERT:      "INSERT",
 	INTO:        "INTO",
 	LIMIT:       "LIMIT",
+	MAXVALUE:    "MAXVALUE",
+	MINVALUE:    "MINVALUE",
+	NEXT:        "NEXT",
+	NO:          "NO",
 	NOT:         "NOT",
 	NOTHING:     "NOTHING",
 	OFFSET:      "OFFSET",
@@ -251,8 +271,10 @@ var tokens = [...]string{
 	RETURNING:   "RETURNING",
 	REPLACE:     "REPLACE",
 	ROLLBACK:    "ROLLBACK",
+	START:       "START",
 	SELECT:      "SELECT",
 	SET:         "SET",
+	SEQUENCE:    "SEQUENCE",
 	TABLE:       "TABLE",
 	TO:          "TO",
 	TRANSACTION: "TRANSACTION",
@@ -260,7 +282,9 @@ var tokens = [...]string{
 	UNIQUE:      "UNIQUE",
 	UNSET:       "UNSET",
 	UPDATE:      "UPDATE",
+	VALUE:       "VALUE",
 	VALUES:      "VALUES",
+	WITH:        "WITH",
 	WHERE:       "WHERE",
 	WRITE:       "WRITE",
 
diff --git a/internal/stream/range.go b/internal/stream/range.go
index 3be5274c..70c334c3 100644
--- a/internal/stream/range.go
+++ b/internal/stream/range.go
@@ -30,7 +30,7 @@ type ValueRange struct {
 func (r *ValueRange) evalRange(table *database.Table, env *expr.Environment) (*encodedValueRange, bool, error) {
 	var err error
 
-	pk := table.Info.GetPrimaryKey()
+	pk := table.Info.FieldConstraints.GetPrimaryKey()
 
 	rng := encodedValueRange{
 		pkType:      pk.Type,


Test patch:
diff --git a/internal/database/catalog_test.go b/internal/database/catalog_test.go
index 10e514a0..fed1e555 100644
--- a/internal/database/catalog_test.go
+++ b/internal/database/catalog_test.go
@@ -214,7 +214,7 @@ func TestCatalogTable(t *testing.T) {
 	})
 }
 
-func TestCatalogCreate(t *testing.T) {
+func TestCatalogCreateTable(t *testing.T) {
 	t.Run("Same table name", func(t *testing.T) {
 		db, cleanup := testutil.NewTestDB(t)
 		defer cleanup()
@@ -273,7 +273,7 @@ func TestCatalogCreate(t *testing.T) {
 	})
 }
 
-func TestTxCreateIndex(t *testing.T) {
+func TestCatalogCreateIndex(t *testing.T) {
 	t.Run("Should create an index, build it and return it", func(t *testing.T) {
 		db, cleanup := testutil.NewTestDB(t)
 		defer cleanup()
@@ -698,3 +698,59 @@ func TestReadOnlyTables(t *testing.T) {
 	})
 	require.NoError(t, err)
 }
+
+func TestCatalogCreateSequence(t *testing.T) {
+	t.Run("Should create a sequence and add it to the schema and sequence tables", func(t *testing.T) {
+		db, cleanup := testutil.NewTestDB(t)
+		defer cleanup()
+
+		update(t, db, func(tx *database.Transaction) error {
+			err := tx.Catalog.CreateSequence(tx, "test1", &database.SequenceInfo{Name: "test1", IncrementBy: 1})
+			if err != nil {
+				return err
+			}
+
+			seq, err := tx.Catalog.GetSequence("test1")
+			require.NoError(t, err)
+			require.NotNil(t, seq)
+
+			_, err = tx.Catalog.SchemaTable.GetTable(tx).GetDocument([]byte("test1"))
+			require.NoError(t, err)
+
+			_, err = tx.Catalog.SequenceTable.GetTable(tx).GetDocument([]byte("test1"))
+			require.NoError(t, err)
+			return nil
+		})
+
+		clone := db.Catalog.Clone()
+
+		update(t, db, func(tx *database.Transaction) error {
+			err := tx.Catalog.CreateSequence(tx, "test2", &database.SequenceInfo{Name: "test2", IncrementBy: 1})
+			if err != nil {
+				return err
+			}
+			seq, err := tx.Catalog.GetSequence("test2")
+			require.NoError(t, err)
+			require.NotNil(t, seq)
+
+			return errDontCommit
+		})
+
+		require.Equal(t, clone, db.Catalog)
+	})
+
+	t.Run("Should fail if it already exists", func(t *testing.T) {
+		db, cleanup := testutil.NewTestDB(t)
+		defer cleanup()
+
+		update(t, db, func(tx *database.Transaction) error {
+			return tx.Catalog.CreateSequence(tx, "test", nil)
+		})
+
+		update(t, db, func(tx *database.Transaction) error {
+			err := tx.Catalog.CreateSequence(tx, "test", nil)
+			require.Equal(t, errs.AlreadyExistsError{Name: "test"}, err)
+			return nil
+		})
+	})
+}
diff --git a/internal/database/sequence_test.go b/internal/database/sequence_test.go
new file mode 100644
index 00000000..ec70065e
--- /dev/null
+++ b/internal/database/sequence_test.go
@@ -0,0 +1,332 @@
+package database_test
+
+import (
+	"testing"
+
+	"github.com/genjidb/genji/internal/database"
+	"github.com/genjidb/genji/internal/testutil"
+	"github.com/stretchr/testify/require"
+)
+
+func TestSequence(t *testing.T) {
+	tests := []struct {
+		name         string
+		info         database.SequenceInfo
+		currentValue *int64
+		expV         int64
+		expErr       bool
+	}{
+		{
+			name: "first call",
+			info: database.SequenceInfo{
+				Name:        "a",
+				IncrementBy: 1,
+				Min:         1, Max: 10,
+				Start: 1,
+			},
+			expV: 1,
+		},
+		{
+			name: "first call desc",
+			info: database.SequenceInfo{
+				Name:        "a",
+				IncrementBy: -1,
+				Min:         -5, Max: 10,
+				Start: 1,
+			},
+			expV: 1,
+		},
+		{
+			name: "second call",
+			info: database.SequenceInfo{
+				Name:        "a",
+				IncrementBy: 1,
+				Min:         1, Max: 10,
+				Start: 1,
+			},
+			currentValue: testutil.Int64Ptr(1),
+			expV:         2,
+		},
+		{
+			name: "second call desc",
+			info: database.SequenceInfo{
+				Name:        "a",
+				IncrementBy: -1,
+				Min:         -5, Max: 10,
+				Start: 1,
+			},
+			currentValue: testutil.Int64Ptr(1),
+			expV:         0,
+		},
+		{
+			name: "equal to min",
+			info: database.SequenceInfo{
+				Name:        "a",
+				IncrementBy: -1,
+				Min:         1, Max: 10,
+				Start: 1,
+			},
+			currentValue: testutil.Int64Ptr(2),
+			expV:         1,
+		},
+		{
+			name: "too low",
+			info: database.SequenceInfo{
+				Name:        "a",
+				IncrementBy: -1,
+				Min:         1, Max: 10,
+				Start: 1,
+			},
+			currentValue: testutil.Int64Ptr(1),
+			expErr:       true,
+		},
+		{
+			name: "equal to max",
+			info: database.SequenceInfo{
+				Name:        "a",
+				IncrementBy: 1,
+				Min:         1, Max: 10,
+				Start: 1,
+			},
+			currentValue: testutil.Int64Ptr(9),
+			expV:         10,
+		},
+		{
+			name: "too high",
+			info: database.SequenceInfo{
+				Name:        "a",
+				IncrementBy: 1,
+				Min:         1, Max: 10,
+				Start: 1,
+			},
+			currentValue: testutil.Int64Ptr(10),
+			expErr:       true,
+		},
+		{
+			name: "cycle min",
+			info: database.SequenceInfo{
+				Name:        "a",
+				IncrementBy: -1,
+				Min:         1, Max: 10,
+				Start: 1,
+				Cycle: true,
+			},
+			currentValue: testutil.Int64Ptr(1),
+			expV:         10,
+		},
+		{
+			name: "cycle max",
+			info: database.SequenceInfo{
+				Name:        "a",
+				IncrementBy: 1,
+				Min:         1, Max: 10,
+				Start: 1,
+				Cycle: true,
+			},
+			currentValue: testutil.Int64Ptr(10),
+			expV:         1,
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			db, cleanup := testutil.NewTestDB(t)
+			defer cleanup()
+
+			tx, err := db.Begin(true)
+			require.NoError(t, err)
+			defer tx.Rollback()
+
+			err = tx.Catalog.CreateSequence(tx, test.info.Name, &test.info)
+			require.NoError(t, err)
+
+			seq := database.Sequence{
+				Info: &test.info,
+			}
+			seq.CurrentValue = test.currentValue
+			gotV, gotErr := seq.Next(tx)
+			if !test.expErr {
+				require.NoError(t, gotErr)
+			} else {
+				require.Error(t, gotErr)
+			}
+			require.Equal(t, test.expV, gotV)
+		})
+	}
+
+	next := func(seq *database.Sequence, tx *database.Transaction, wantV int64, wantLease int64) {
+		t.Helper()
+
+		v, err := seq.Next(tx)
+		require.NoError(t, err)
+		require.Equal(t, int64(wantV), v)
+
+		got, err := tx.Catalog.SequenceTable.GetLease(tx, "a")
+		require.NoError(t, err)
+		require.Equal(t, wantLease, *got)
+	}
+
+	t.Run("default cache", func(t *testing.T) {
+		db, cleanup := testutil.NewTestDB(t)
+		defer cleanup()
+
+		tx, err := db.Begin(true)
+		require.NoError(t, err)
+		defer tx.Rollback()
+
+		err = tx.Catalog.CreateSequence(tx, "a", &database.SequenceInfo{
+			Name:        "a",
+			IncrementBy: 1,
+			Min:         1, Max: 5,
+			Start: 1,
+			Cache: 1,
+		})
+		require.NoError(t, err)
+
+		seq, err := tx.Catalog.GetSequence("a")
+		require.NoError(t, err)
+
+		// each call must increase the lease by 1 and store it in the table
+		next(seq, tx, 1, 1)
+		next(seq, tx, 2, 2)
+		next(seq, tx, 3, 3)
+		next(seq, tx, 4, 4)
+		next(seq, tx, 5, 5)
+		// reaching the max should not modify the cache or the lease
+		cached := seq.Cached
+
+		_, err = seq.Next(tx)
+		require.Error(t, err)
+		require.Equal(t, int64(5), *seq.CurrentValue)
+		require.Equal(t, cached, seq.Cached)
+		got, err := tx.Catalog.SequenceTable.GetLease(tx, "a")
+		require.NoError(t, err)
+		require.Equal(t, int64(5), *got)
+	})
+
+	t.Run("cache", func(t *testing.T) {
+		db, cleanup := testutil.NewTestDB(t)
+		defer cleanup()
+
+		tx, err := db.Begin(true)
+		require.NoError(t, err)
+		defer tx.Rollback()
+
+		err = tx.Catalog.CreateSequence(tx, "a", &database.SequenceInfo{
+			Name:        "a",
+			IncrementBy: 1,
+			Min:         1, Max: 9,
+			Start: 1,
+			Cache: 2,
+		})
+		require.NoError(t, err)
+
+		seq, err := tx.Catalog.GetSequence("a")
+		require.NoError(t, err)
+
+		// first call to next must increase the lease to 2 and store it in the table
+		next(seq, tx, 1, 2)
+		// next call must increase the current value but not touch the lease in the table
+		next(seq, tx, 2, 2)
+		// next call must increase the current value and the lease
+		next(seq, tx, 3, 4)
+		// some additional checks
+		next(seq, tx, 4, 4)
+		next(seq, tx, 5, 6)
+		next(seq, tx, 6, 6)
+		next(seq, tx, 7, 8)
+		next(seq, tx, 8, 8)
+		// the lease must not be greater than the max value, but not fail
+		next(seq, tx, 9, 9)
+
+		// reaching the max should not modify the cache or the lease
+		cached := seq.Cached
+
+		_, err = seq.Next(tx)
+		require.Error(t, err)
+		require.Equal(t, int64(9), *seq.CurrentValue)
+		require.Equal(t, cached, seq.Cached)
+		got, err := tx.Catalog.SequenceTable.GetLease(tx, "a")
+		require.NoError(t, err)
+		require.Equal(t, int64(9), *got)
+	})
+
+	t.Run("cache desc", func(t *testing.T) {
+		db, cleanup := testutil.NewTestDB(t)
+		defer cleanup()
+
+		tx, err := db.Begin(true)
+		require.NoError(t, err)
+		defer tx.Rollback()
+
+		err = tx.Catalog.CreateSequence(tx, "a", &database.SequenceInfo{
+			Name:        "a",
+			IncrementBy: -1,
+			Min:         -5, Max: 9,
+			Start: 5,
+			Cache: 2,
+		})
+		require.NoError(t, err)
+
+		seq, err := tx.Catalog.GetSequence("a")
+		require.NoError(t, err)
+
+		// first call to next must decrease the lease to 3 and store it in the table
+		next(seq, tx, 5, 4)
+		// next call must increase the current value but not touch the lease in the table
+		next(seq, tx, 4, 4)
+		// next call must increase the current value and the lease
+		next(seq, tx, 3, 2)
+		// some additional checks
+		next(seq, tx, 2, 2)
+		next(seq, tx, 1, 0)
+		next(seq, tx, 0, 0)
+		next(seq, tx, -1, -2)
+		next(seq, tx, -2, -2)
+		next(seq, tx, -3, -4)
+		next(seq, tx, -4, -4)
+		next(seq, tx, -5, -5)
+
+		// reaching the min should not modify the cache or the lease
+		cached := seq.Cached
+
+		_, err = seq.Next(tx)
+		require.Error(t, err)
+		require.Equal(t, int64(-5), *seq.CurrentValue)
+		require.Equal(t, cached, seq.Cached)
+		got, err := tx.Catalog.SequenceTable.GetLease(tx, "a")
+		require.NoError(t, err)
+		require.Equal(t, int64(-5), *got)
+	})
+
+	t.Run("read-only", func(t *testing.T) {
+		db, cleanup := testutil.NewTestDB(t)
+		defer cleanup()
+
+		tx, err := db.Begin(true)
+		require.NoError(t, err)
+
+		err = tx.Catalog.CreateSequence(tx, "a", &database.SequenceInfo{
+			Name:        "a",
+			IncrementBy: -1,
+			Min:         -4, Max: 9,
+			Start: 5,
+			Cache: 2,
+		})
+		require.NoError(t, err)
+
+		err = tx.Commit()
+		require.NoError(t, err)
+
+		// open a read-only tx
+		tx, err = db.Begin(false)
+		require.NoError(t, err)
+		defer tx.Rollback()
+
+		seq, err := tx.Catalog.GetSequence("a")
+		require.NoError(t, err)
+
+		_, err = seq.Next(tx)
+		require.Error(t, err)
+	})
+}
diff --git a/internal/query/statement/create_test.go b/internal/query/statement/create_test.go
index fb2a245a..3a62fdbf 100644
--- a/internal/query/statement/create_test.go
+++ b/internal/query/statement/create_test.go
@@ -305,3 +305,87 @@ func TestCreateIndex(t *testing.T) {
 		})
 	}
 }
+
+func TestCreateSequence(t *testing.T) {
+	tests := []struct {
+		name  string
+		query string
+		fails bool
+	}{
+		{"Basic", "CREATE SEQUENCE seq", false},
+		{"If not exists", "CREATE SEQUENCE IF NOT EXISTS seq", false},
+		{"AS integer", "CREATE SEQUENCE seq AS TINYINT", false},
+		{"AS double", "CREATE SEQUENCE seq AS DOUBLE", true},
+		{"INCREMENT", "CREATE SEQUENCE seq INCREMENT 10", false},
+		{"INCREMENT BY", "CREATE SEQUENCE seq INCREMENT BY 10", false},
+		{"INCREMENT BY 0", "CREATE SEQUENCE seq INCREMENT BY 0", true},
+		{"NO MINVALUE", "CREATE SEQUENCE seq NO MINVALUE", false},
+		{"NO MAXVALUE", "CREATE SEQUENCE seq NO MAXVALUE", false},
+		{"NO CYCLE", "CREATE SEQUENCE seq NO CYCLE", false},
+		{"NO SUGAR", "CREATE SEQUENCE seq NO SUGAR", true},
+		{"MINVALUE 10", "CREATE SEQUENCE seq MINVALUE 10", false},
+		{"MINVALUE 'hello'", "CREATE SEQUENCE seq MINVALUE 'hello'", true},
+		{"MAXVALUE 10", "CREATE SEQUENCE seq MAXVALUE 10", false},
+		{"MAXVALUE 'hello'", "CREATE SEQUENCE seq MAXVALUE 'hello'", true},
+		{"START WITH 10", "CREATE SEQUENCE seq START WITH 10", false},
+		{"START WITH 'hello'", "CREATE SEQUENCE seq START WITH 'hello'", true},
+		{"START 10", "CREATE SEQUENCE seq START 10", false},
+		{"CACHE 10", "CREATE SEQUENCE seq CACHE 10", false},
+		{"CACHE 'hello'", "CREATE SEQUENCE seq CACHE 'hello'", true},
+		{"CACHE -10", "CREATE SEQUENCE seq CACHE -10", true},
+		{"CYCLE", "CREATE SEQUENCE seq CYCLE", false},
+		{"Order 1", `
+			CREATE SEQUENCE IF NOT EXISTS seq
+			AS INTEGER
+			INCREMENT BY 2
+			NO MINVALUE
+			MAXVALUE 10
+			START WITH 5
+			CACHE 5
+			CYCLE
+		`, false},
+		{"Order 2", `
+			CREATE SEQUENCE IF NOT EXISTS seq
+			CYCLE
+			MAXVALUE 10
+			INCREMENT BY 2
+			START WITH 5
+			AS INTEGER
+			NO MINVALUE
+			CACHE 5
+		`, false},
+		{"NO MINVALUE with MINVALUE 10", "CREATE SEQUENCE seq NO MINVALUE MINVALUE 10", true},
+		{"NO MAXVALUE with MAXVALUE 10", "CREATE SEQUENCE seq NO MAXVALUE MAXVALUE 10", true},
+		{"NO CYCLE with CYCLE", "CREATE SEQUENCE seq NO MAXVALUE MAXVALUE 10", true},
+		{"duplicate AS INT", "CREATE SEQUENCE seq AS INT AS INT", true},
+		{"duplicate INCREMENT BY", "CREATE SEQUENCE seq INCREMENT BY 10 INCREMENT BY 10", true},
+		{"duplicate NO MINVALUE", "CREATE SEQUENCE seq NO MINVALUE NO MINVALUE", true},
+		{"duplicate NO MAXVALUE", "CREATE SEQUENCE seq NO MAXVALUE NO MAXVALUE", true},
+		{"duplicate NO CYCLE", "CREATE SEQUENCE seq NO CYCLE NO CYCLE", true},
+		{"duplicate MINVALUE", "CREATE SEQUENCE seq MINVALUE 10 MINVALUE 10", true},
+		{"duplicate MAXVALUE", "CREATE SEQUENCE seq MAXVALUE 10 MAXVALUE 10", true},
+		{"duplicate START WITH", "CREATE SEQUENCE seq START WITH 10 START WITH 10", true},
+		{"duplicate CACHE", "CREATE SEQUENCE seq CACHE 10 CACHE 10", true},
+		{"duplicate CYCLE", "CREATE SEQUENCE seq CYCLE CYCLE", true},
+		{"BAD MINVALUE MAXVALUE", "CREATE SEQUENCE seq MINVALUE 10 MAXVALUE 5", true},
+		{"BAD START", "CREATE SEQUENCE seq MINVALUE 5 MAXVALUE 10 START 100", true},
+		{"BAD START", "CREATE SEQUENCE seq MINVALUE 5 MAXVALUE 10 START -100", true},
+		{"MINVALUE 10 DESC", "CREATE SEQUENCE seq MINVALUE 10 MAXVALUE 100 INCREMENT BY -1", false},
+		{"NO MINVALUE DESC", "CREATE SEQUENCE seq NO MINVALUE MAXVALUE 100 INCREMENT BY -1", false},
+		{"NO MAXVALUE DESC", "CREATE SEQUENCE seq NO MINVALUE NO MAXVALUE INCREMENT BY -1", false},
+	}
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			_, tx, cleanup := testutil.NewTestTx(t)
+			defer cleanup()
+
+			err := testutil.Exec(tx, test.query)
+			if test.fails {
+				require.Error(t, err)
+				return
+			}
+			require.NoError(t, err)
+		})
+	}
+}
diff --git a/internal/query/statement/gen_insert_statements_test.go b/internal/query/statement/gen_insert_statements_test.go
index 3df280e6..135ac928 100644
--- a/internal/query/statement/gen_insert_statements_test.go
+++ b/internal/query/statement/gen_insert_statements_test.go
@@ -904,4 +904,39 @@ INSERT INTO test_oc (b, c) VALUES (1, 1) ON CONFLICT DO REPLACE;
 
 	})
 
+	// --------------------------------------------------------------------------
+	t.Run("insert with NEXT VALUE FOR", func(t *testing.T) {
+		db, err := genji.Open(":memory:")
+		require.NoError(t, err)
+		defer db.Close()
+
+		setup(t, db)
+
+		t.Run(`CREATE TABLE test_oc(a INTEGER UNIQUE);`, func(t *testing.T) {
+			q := `
+CREATE TABLE test_oc(a INTEGER UNIQUE);
+CREATE SEQUENCE test_seq;
+INSERT INTO test_oc (a) VALUES (NEXT VALUE FOR test_seq);
+INSERT INTO test_oc (a) VALUES (NEXT VALUE FOR test_seq), (NEXT VALUE FOR test_seq);
+SELECT * FROM test_oc;
+`
+			res, err := db.Query(q)
+			require.NoError(t, err)
+			defer res.Close()
+			raw := `
+{
+  a: 1
+}
+{
+  a: 2
+}
+{
+  a: 3
+}
+`
+			testutil.RequireStreamEq(t, raw, res)
+		})
+
+	})
+
 }
diff --git a/internal/query/statement/gen_insert_statements_test.sql b/internal/query/statement/gen_insert_statements_test.sql
index 6b448f3d..f875d39c 100644
--- a/internal/query/statement/gen_insert_statements_test.sql
+++ b/internal/query/statement/gen_insert_statements_test.sql
@@ -343,4 +343,22 @@ SELECT * FROM test_oc;
 -- test: insert with on conflict do replace, not null
 CREATE TABLE test_oc(a INTEGER NOT NULL);
 INSERT INTO test_oc (b, c) VALUES (1, 1) ON CONFLICT DO REPLACE;
--- error:
\ No newline at end of file
+-- error:
+
+-- test: insert with NEXT VALUE FOR
+CREATE TABLE test_oc(a INTEGER UNIQUE);
+CREATE SEQUENCE test_seq;
+INSERT INTO test_oc (a) VALUES (NEXT VALUE FOR test_seq);
+INSERT INTO test_oc (a) VALUES (NEXT VALUE FOR test_seq), (NEXT VALUE FOR test_seq);
+SELECT * FROM test_oc;
+/* result:
+{
+  a: 1
+}
+{
+  a: 2
+}
+{
+  a: 3
+}
+*/
\ No newline at end of file
diff --git a/internal/sql/parser/create_test.go b/internal/sql/parser/create_test.go
index 600af67c..ea4379de 100644
--- a/internal/sql/parser/create_test.go
+++ b/internal/sql/parser/create_test.go
@@ -1,6 +1,7 @@
 package parser_test
 
 import (
+	"math"
 	"testing"
 
 	"github.com/genjidb/genji/document"
@@ -309,3 +310,144 @@ func TestParserCreateIndex(t *testing.T) {
 		})
 	}
 }
+
+func TestParserCreateSequence(t *testing.T) {
+	tests := []struct {
+		name     string
+		s        string
+		expected statement.Statement
+		errored  bool
+	}{
+		{"Basic", "CREATE SEQUENCE seq", &statement.CreateSequenceStmt{
+			Info: database.SequenceInfo{Name: "seq", IncrementBy: 1, Min: 1, Max: math.MaxInt64, Start: 1, Cache: 1},
+		}, false},
+		{"If not exists", "CREATE SEQUENCE IF NOT EXISTS seq", &statement.CreateSequenceStmt{
+			Info:        database.SequenceInfo{Name: "seq", IncrementBy: 1, Min: 1, Max: math.MaxInt64, Start: 1, Cache: 1},
+			IfNotExists: true,
+		}, false},
+		{"AS integer", "CREATE SEQUENCE seq AS TINYINT", &statement.CreateSequenceStmt{
+			Info: database.SequenceInfo{Name: "seq", IncrementBy: 1, Min: 1, Max: math.MaxInt64, Start: 1, Cache: 1},
+		}, false},
+		{"AS double", "CREATE SEQUENCE seq AS DOUBLE", nil, true},
+		{"INCREMENT", "CREATE SEQUENCE seq INCREMENT 10", &statement.CreateSequenceStmt{
+			Info: database.SequenceInfo{Name: "seq", IncrementBy: 10, Min: 1, Max: math.MaxInt64, Start: 1, Cache: 1},
+		}, false},
+		{"INCREMENT BY", "CREATE SEQUENCE seq INCREMENT BY 10", &statement.CreateSequenceStmt{
+			Info: database.SequenceInfo{Name: "seq", IncrementBy: 10, Min: 1, Max: math.MaxInt64, Start: 1, Cache: 1},
+		}, false},
+		{"INCREMENT BY 0", "CREATE SEQUENCE seq INCREMENT BY 0", nil, true},
+		{"NO MINVALUE", "CREATE SEQUENCE seq NO MINVALUE", &statement.CreateSequenceStmt{
+			Info: database.SequenceInfo{Name: "seq", IncrementBy: 1, Min: 1, Max: math.MaxInt64, Start: 1, Cache: 1},
+		}, false},
+		{"NO MAXVALUE", "CREATE SEQUENCE seq NO MAXVALUE", &statement.CreateSequenceStmt{
+			Info: database.SequenceInfo{Name: "seq", IncrementBy: 1, Min: 1, Max: math.MaxInt64, Start: 1, Cache: 1},
+		}, false},
+		{"NO CYCLE", "CREATE SEQUENCE seq NO CYCLE", &statement.CreateSequenceStmt{
+			Info: database.SequenceInfo{Name: "seq", IncrementBy: 1, Min: 1, Max: math.MaxInt64, Start: 1, Cache: 1},
+		}, false},
+		{"NO SUGAR", "CREATE SEQUENCE seq NO SUGAR", nil, true},
+		{"MINVALUE 10", "CREATE SEQUENCE seq MINVALUE 10", &statement.CreateSequenceStmt{
+			Info: database.SequenceInfo{Name: "seq", IncrementBy: 1, Min: 10, Max: math.MaxInt64, Start: 10, Cache: 1},
+		}, false},
+		{"MINVALUE 'hello'", "CREATE SEQUENCE seq MINVALUE 'hello'", nil, true},
+		{"MAXVALUE 10", "CREATE SEQUENCE seq MAXVALUE 10", &statement.CreateSequenceStmt{
+			Info: database.SequenceInfo{Name: "seq", IncrementBy: 1, Min: 1, Max: 10, Start: 1, Cache: 1},
+		}, false},
+		{"MAXVALUE 'hello'", "CREATE SEQUENCE seq MAXVALUE 'hello'", nil, true},
+		{"START WITH 10", "CREATE SEQUENCE seq START WITH 10", &statement.CreateSequenceStmt{
+			Info: database.SequenceInfo{Name: "seq", IncrementBy: 1, Min: 1, Max: math.MaxInt64, Start: 10, Cache: 1},
+		}, false},
+		{"START WITH 'hello'", "CREATE SEQUENCE seq START WITH 'hello'", nil, true},
+		{"START 10", "CREATE SEQUENCE seq START 10", &statement.CreateSequenceStmt{
+			Info: database.SequenceInfo{Name: "seq", IncrementBy: 1, Min: 1, Max: math.MaxInt64, Start: 10, Cache: 1},
+		}, false},
+		{"CACHE 10", "CREATE SEQUENCE seq CACHE 10", &statement.CreateSequenceStmt{
+			Info: database.SequenceInfo{Name: "seq", IncrementBy: 1, Min: 1, Max: math.MaxInt64, Start: 1, Cache: 10},
+		}, false},
+		{"CACHE 'hello'", "CREATE SEQUENCE seq CACHE 'hello'", nil, true},
+		{"CACHE -10", "CREATE SEQUENCE seq CACHE -10", nil, true},
+		{"CYCLE", "CREATE SEQUENCE seq CYCLE", &statement.CreateSequenceStmt{
+			Info: database.SequenceInfo{Name: "seq", IncrementBy: 1, Min: 1, Max: math.MaxInt64, Start: 1, Cache: 1, Cycle: true},
+		}, false},
+		{"Order 1", `
+			CREATE SEQUENCE IF NOT EXISTS seq
+			AS INTEGER
+			INCREMENT BY 2
+			NO MINVALUE
+			MAXVALUE 10
+			START WITH 5
+			CACHE 5
+			CYCLE
+		`, &statement.CreateSequenceStmt{
+			IfNotExists: true,
+			Info: database.SequenceInfo{
+				Name:        "seq",
+				IncrementBy: 2,
+				Min:         1,
+				Max:         10,
+				Start:       5,
+				Cache:       5,
+				Cycle:       true,
+			},
+		}, false},
+		{"Order 2", `
+			CREATE SEQUENCE IF NOT EXISTS seq
+			CYCLE
+			MAXVALUE 10
+			INCREMENT BY 2
+			START WITH 5
+			AS INTEGER
+			NO MINVALUE
+			CACHE 5
+		`, &statement.CreateSequenceStmt{
+			IfNotExists: true,
+			Info: database.SequenceInfo{
+				Name:        "seq",
+				IncrementBy: 2,
+				Min:         1,
+				Max:         10,
+				Start:       5,
+				Cache:       5,
+				Cycle:       true,
+			},
+		}, false},
+		{"NO MINVALUE with MINVALUE 10", "CREATE SEQUENCE seq NO MINVALUE MINVALUE 10", nil, true},
+		{"NO MAXVALUE with MAXVALUE 10", "CREATE SEQUENCE seq NO MAXVALUE MAXVALUE 10", nil, true},
+		{"NO CYCLE with CYCLE", "CREATE SEQUENCE seq NO MAXVALUE MAXVALUE 10", nil, true},
+		{"duplicate AS INT", "CREATE SEQUENCE seq AS INT AS INT", nil, true},
+		{"duplicate INCREMENT BY", "CREATE SEQUENCE seq INCREMENT BY 10 INCREMENT BY 10", nil, true},
+		{"duplicate NO MINVALUE", "CREATE SEQUENCE seq NO MINVALUE NO MINVALUE", nil, true},
+		{"duplicate NO MAXVALUE", "CREATE SEQUENCE seq NO MAXVALUE NO MAXVALUE", nil, true},
+		{"duplicate NO CYCLE", "CREATE SEQUENCE seq NO CYCLE NO CYCLE", nil, true},
+		{"duplicate MINVALUE", "CREATE SEQUENCE seq MINVALUE 10 MINVALUE 10", nil, true},
+		{"duplicate MAXVALUE", "CREATE SEQUENCE seq MAXVALUE 10 MAXVALUE 10", nil, true},
+		{"duplicate START WITH", "CREATE SEQUENCE seq START WITH 10 START WITH 10", nil, true},
+		{"duplicate CACHE", "CREATE SEQUENCE seq CACHE 10 CACHE 10", nil, true},
+		{"duplicate CYCLE", "CREATE SEQUENCE seq CYCLE CYCLE", nil, true},
+		{"BAD MINVALUE MAXVALUE", "CREATE SEQUENCE seq MINVALUE 10 MAXVALUE 5", nil, true},
+		{"BAD START", "CREATE SEQUENCE seq MINVALUE 5 MAXVALUE 10 START 100", nil, true},
+		{"BAD START", "CREATE SEQUENCE seq MINVALUE 5 MAXVALUE 10 START -100", nil, true},
+		{"MINVALUE 10 DESC", "CREATE SEQUENCE seq MINVALUE 10 MAXVALUE 100 INCREMENT BY -1", &statement.CreateSequenceStmt{
+			Info: database.SequenceInfo{Name: "seq", IncrementBy: -1, Min: 10, Max: 100, Start: 100, Cache: 1},
+		}, false},
+		{"NO MINVALUE DESC", "CREATE SEQUENCE seq NO MINVALUE MAXVALUE 100 INCREMENT BY -1", &statement.CreateSequenceStmt{
+			Info: database.SequenceInfo{Name: "seq", IncrementBy: -1, Min: math.MinInt64, Max: 100, Start: 100, Cache: 1},
+		}, false},
+		{"NO MAXVALUE DESC", "CREATE SEQUENCE seq NO MINVALUE NO MAXVALUE INCREMENT BY -1", &statement.CreateSequenceStmt{
+			Info: database.SequenceInfo{Name: "seq", IncrementBy: -1, Min: math.MinInt64, Max: -1, Start: -1, Cache: 1},
+		}, false},
+	}
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			q, err := parser.ParseQuery(test.s)
+			if test.errored {
+				require.Error(t, err)
+				return
+			}
+			require.NoError(t, err)
+			require.Len(t, q.Statements, 1)
+			require.EqualValues(t, test.expected, q.Statements[0])
+		})
+	}
+}
diff --git a/internal/sql/parser/expr_test.go b/internal/sql/parser/expr_test.go
index 2c89410c..73058711 100644
--- a/internal/sql/parser/expr_test.go
+++ b/internal/sql/parser/expr_test.go
@@ -156,6 +156,9 @@ func TestParserExpr(t *testing.T) {
 		{"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},
+		{"NEXT VALUE FOR", "NEXT VALUE FOR hello", expr.NextValueFor{SeqName: "hello"}, false},
+		{"NEXT VALUE FOR", "NEXT VALUE FOR `good morning`", expr.NextValueFor{SeqName: "good morning"}, false},
+		{"NEXT VALUE FOR", "NEXT VALUE FOR 10", nil, true},
 
 		// functions
 		{"pk() function", "pk()", &expr.PKFunc{}, false},
diff --git a/internal/sql/scanner/scanner_test.go b/internal/sql/scanner/scanner_test.go
index 708618b3..65822ee8 100644
--- a/internal/sql/scanner/scanner_test.go
+++ b/internal/sql/scanner/scanner_test.go
@@ -123,10 +123,12 @@ func TestScanner_Scan(t *testing.T) {
 		{s: `BY`, tok: BY},
 		{s: `BEGIN`, tok: BEGIN},
 		{s: `BETWEEN`, tok: BETWEEN},
+		{s: `CACHE`, tok: CACHE},
 		{s: `CAST`, tok: CAST},
 		{s: `COMMIT`, tok: COMMIT},
 		{s: `CONFLICT`, tok: CONFLICT},
 		{s: `CREATE`, tok: CREATE},
+		{s: `CYCLE`, tok: CYCLE},
 		{s: `DEFAULT`, tok: DEFAULT},
 		{s: `DELETE`, tok: DELETE},
 		{s: `DESC`, tok: DESC},
@@ -136,11 +138,19 @@ func TestScanner_Scan(t *testing.T) {
 		{s: `EXPLAIN`, tok: EXPLAIN},
 		{s: `GROUP`, tok: GROUP},
 		{s: `FIELD`, tok: FIELD},
+		{s: `FOR`, tok: FOR},
 		{s: `FROM`, tok: FROM},
 		{s: `IGNORE`, tok: IGNORE},
+		{s: `INCREMENT`, tok: INCREMENT},
+		{s: `INDEX`, tok: INDEX},
 		{s: `INSERT`, tok: INSERT},
 		{s: `INTO`, tok: INTO},
 		{s: `LIMIT`, tok: LIMIT},
+		{s: `MAXVALUE`, tok: MAXVALUE},
+		{s: `MINVALUE`, tok: MINVALUE},
+		{s: `NEXT`, tok: NEXT},
+		{s: `NO`, tok: NO},
+		{s: `NOT`, tok: NOT},
 		{s: `NOTHING`, tok: NOTHING},
 		{s: `ONLY`, tok: ONLY},
 		{s: `OFFSET`, tok: OFFSET},
@@ -153,14 +163,18 @@ func TestScanner_Scan(t *testing.T) {
 		{s: `RETURNING`, tok: RETURNING},
 		{s: `ROLLBACK`, tok: ROLLBACK},
 		{s: `SELECT`, tok: SELECT},
+		{s: `SEQUENCE`, tok: SEQUENCE},
 		{s: `SET`, tok: SET},
+		{s: `START`, tok: START},
 		{s: `TABLE`, tok: TABLE},
 		{s: `TO`, tok: TO},
 		{s: `TRANSACTION`, tok: TRANSACTION},
 		{s: `UPDATE`, tok: UPDATE},
 		{s: `UNION`, tok: UNION},
 		{s: `UNSET`, tok: UNSET},
+		{s: `VALUE`, tok: VALUE},
 		{s: `VALUES`, tok: VALUES},
+		{s: `WITH`, tok: WITH},
 		{s: `WHERE`, tok: WHERE},
 		{s: `WRITE`, tok: WRITE},
 		{s: `seLECT`, tok: SELECT}, // case insensitive
@@ -197,24 +211,23 @@ func TestScanner_Scan_Multi(t *testing.T) {
 	exp := []result{
 		{tok: SELECT, pos: Pos{Line: 0, Char: 0}, lit: ""},
 		{tok: WS, pos: Pos{Line: 0, Char: 6}, lit: " "},
-		{tok: IDENT, pos: Pos{Line: 0, Char: 7}, lit: "value"},
-		{tok: WS, pos: Pos{Line: 0, Char: 12}, lit: " "},
-		{tok: FROM, pos: Pos{Line: 0, Char: 13}, lit: ""},
-		{tok: WS, pos: Pos{Line: 0, Char: 17}, lit: " "},
-		{tok: IDENT, pos: Pos{Line: 0, Char: 18}, lit: "my_table"},
-		{tok: WS, pos: Pos{Line: 0, Char: 26}, lit: " "},
-		{tok: WHERE, pos: Pos{Line: 0, Char: 27}, lit: ""},
+		{tok: IDENT, pos: Pos{Line: 0, Char: 7}, lit: "val"},
+		{tok: WS, pos: Pos{Line: 0, Char: 10}, lit: " "},
+		{tok: FROM, pos: Pos{Line: 0, Char: 11}, lit: ""},
+		{tok: WS, pos: Pos{Line: 0, Char: 15}, lit: " "},
+		{tok: IDENT, pos: Pos{Line: 0, Char: 16}, lit: "my_table"},
+		{tok: WS, pos: Pos{Line: 0, Char: 24}, lit: " "},
+		{tok: WHERE, pos: Pos{Line: 0, Char: 25}, lit: ""},
+		{tok: WS, pos: Pos{Line: 0, Char: 30}, lit: " "},
+		{tok: IDENT, pos: Pos{Line: 0, Char: 31}, lit: "a"},
 		{tok: WS, pos: Pos{Line: 0, Char: 32}, lit: " "},
-		{tok: IDENT, pos: Pos{Line: 0, Char: 33}, lit: "a"},
+		{tok: EQ, pos: Pos{Line: 0, Char: 33}, lit: ""},
 		{tok: WS, pos: Pos{Line: 0, Char: 34}, lit: " "},
-		{tok: EQ, pos: Pos{Line: 0, Char: 35}, lit: ""},
-		{tok: WS, pos: Pos{Line: 0, Char: 36}, lit: " "},
-		{tok: STRING, pos: Pos{Line: 0, Char: 36}, lit: "b"},
-		{tok: EOF, pos: Pos{Line: 0, Char: 40}, lit: ""},
+		{tok: STRING, pos: Pos{Line: 0, Char: 34}, lit: "b"},
+		{tok: EOF, pos: Pos{Line: 0, Char: 38}, lit: ""},
 	}
 
-	// Create a
-	v := `SELECT value from my_table WHERE a = 'b'`
+	v := `SELECT val from my_table WHERE a = 'b'`
 	s := newScanner(strings.NewReader(v))
 
 	// Continually scan until we reach the end.
diff --git a/internal/testutil/db.go b/internal/testutil/db.go
index 689a4cef..a9d2989c 100644
--- a/internal/testutil/db.go
+++ b/internal/testutil/db.go
@@ -22,7 +22,7 @@ func NewTestDB(t testing.TB) (*database.Database, func()) {
 	})
 	require.NoError(t, err)
 
-	db.Catalog.Load(nil, nil)
+	db.Catalog.Load(nil, nil, nil)
 
 	return db, func() {
 		db.Close()
diff --git a/internal/testutil/misc.go b/internal/testutil/misc.go
index 41cc0529..7762aee4 100644
--- a/internal/testutil/misc.go
+++ b/internal/testutil/misc.go
@@ -4,3 +4,5 @@ package testutil
 type KV struct {
 	Key, Value []byte
 }
+
+func Int64Ptr(n int64) *int64 { return &n }
diff --git a/open_test.go b/open_test.go
index c00c3483..7a392af3 100644
--- a/open_test.go
+++ b/open_test.go
@@ -25,8 +25,11 @@ func TestOpen(t *testing.T) {
 		CREATE TABLE tableA (a INTEGER UNIQUE NOT NULL, b.c[0].d DOUBLE PRIMARY KEY);
 		CREATE TABLE tableB (a TEXT NOT NULL DEFAULT 'hello', PRIMARY KEY (a));
 		CREATE TABLE tableC;
+		CREATE INDEX tableC_a_b_idx ON tableC(a, b);
+		CREATE SEQUENCE seqD INCREMENT BY 10 CYCLE MINVALUE 100 NO MAXVALUE START 500;
 
-		INSERT INTO tableB (a) VALUES (1)
+		INSERT INTO tableB (a) VALUES (1);
+		INSERT INTO tableC (a, b) VALUES (1, NEXT VALUE FOR seqD);
 	`)
 	require.NoError(t, err)
 
@@ -46,31 +49,44 @@ func TestOpen(t *testing.T) {
 	err = res1.Iterate(func(d document.Document) error {
 		count++
 		if count == 1 {
-			testutil.RequireDocJSONEq(t, d, `{"name":"tableA", "sql":"CREATE TABLE tableA (a INTEGER NOT NULL UNIQUE, b.c[0].d DOUBLE PRIMARY KEY)", "store_name":"AQ==", "type":"table"}`)
+			testutil.RequireDocJSONEq(t, d, `{"name":"seqD", "sql":"CREATE SEQUENCE seqD INCREMENT BY 10 MINVALUE 100 MAXVALUE 9223372036854775807 START WITH 500 CACHE 1 CYCLE", "type":"sequence"}`)
 			return nil
 		}
 
 		if count == 2 {
-			testutil.RequireDocJSONEq(t, d, `{"constraint_path":"a", "name":"tableA_a_idx", "sql":"CREATE UNIQUE INDEX tableA_a_idx ON tableA (a)", "store_name":"Ag==", "table_name":"tableA", "type":"index"}`)
+			testutil.RequireDocJSONEq(t, d, `{"name":"tableA", "sql":"CREATE TABLE tableA (a INTEGER NOT NULL UNIQUE, b.c[0].d DOUBLE PRIMARY KEY)", "store_name":"AQ==", "type":"table"}`)
 			return nil
 		}
 
 		if count == 3 {
-			testutil.RequireDocJSONEq(t, d, `{"name":"tableB", "sql":"CREATE TABLE tableB (a TEXT NOT NULL PRIMARY KEY DEFAULT \"hello\")", "store_name":"Aw==", "type":"table"}`)
+			testutil.RequireDocJSONEq(t, d, `{"constraint_path":"a", "name":"tableA_a_idx", "sql":"CREATE UNIQUE INDEX tableA_a_idx ON tableA (a)", "store_name":"Ag==", "table_name":"tableA", "type":"index"}`)
 			return nil
 		}
 
 		if count == 4 {
+			testutil.RequireDocJSONEq(t, d, `{"name":"tableB", "sql":"CREATE TABLE tableB (a TEXT NOT NULL PRIMARY KEY DEFAULT \"hello\")", "store_name":"Aw==", "type":"table"}`)
+			return nil
+		}
+
+		if count == 5 {
 			testutil.RequireDocJSONEq(t, d, `{"name":"tableC", "sql":"CREATE TABLE tableC", "store_name":"BA==", "type":"table"}`)
 			return nil
 		}
 
-		return errors.New("more than 4 relations")
+		if count == 6 {
+			testutil.RequireDocJSONEq(t, d, `{"name":"tableC_a_b_idx", "sql":"CREATE INDEX tableC_a_b_idx ON tableC (a, b)", "store_name":"BQ==", "table_name":"tableC", "type":"index"}`)
+			return nil
+		}
+
+		return errors.New("more than 6 relations")
 	})
 	require.NoError(t, err)
 
 	d, err := db.QueryDocument("SELECT * FROM tableB")
 	require.NoError(t, err)
-
 	testutil.RequireDocJSONEq(t, d, `{"a": "1"}`)
+
+	d, err = db.QueryDocument("SELECT * FROM __genji_sequence")
+	require.NoError(t, err)
+	testutil.RequireDocJSONEq(t, d, `{"name": "seqD", "seq": 500}`)
 }