Repository: genjidb/genji (go)

Issue:
Add ALTER TABLE ... ADD FIELD ...
This is a sub-issue of #94 that focuses on adding a new field constraint.

We should be able to do the following:

```
genji> CREATE TABLE foo;
genji> INSERT INTO foo VALUES {name: "John Doe"};
genji> ALTER TABLE foo ADD FIELD age int;
```
If the field `age` already exists, we should return an error.

#### NOT NULL 
The only constraint supported for now would be `NOT NULL` but a default value must be specified for the existing documents.
```
genji> ALTER TABLE foo ADD FIELD age int NOT NULL DEFAULT 50;
genji> SELECT * FROM foo;
{
  "name": "John Doe",
  "age": 50
}
```

#### Dot notation
We should also support the [dot notation](https://genji.dev/docs/genji-sql/lexical-structure/#dot-notation) structure.
For example:
```
genji> ALTER TABLE foo ADD FIELD adress.zip int;
genji> INSERT INTO foo VALUES {name: "John Wick", age: 50, adress: {zip:75000}};
genji> SELECT * FROM foo;
{
  "name": "John Wick",
  "age": 50,
  "adress": {
    "zip": 75000
  }
}
genji> INSERT INTO foo VALUES {name: "John Wick", age: 50, adress: {zip:"Paris"}};
cannot convert "text" to "int64": type "text" incompatible with "integer"
```

The same goes for arrays:
```
genji> ALTER TABLE foo ADD FIELD animals.0 text;
genji> INSERT INTO foo VALUES {name: "John Lennon", age: 50, animals: ["bird"]};
genji> SELECT * FROM foo;
{
  "name": "John Lennon",
  "age": 50,
  "animals": [
    "cat",
  ]
}
genji> INSERT INTO foo VALUES {name: "John Lennon", age: 50, animals: [127]};
cannot convert "int8" to "string"
```


Pull request:
Add ADD COLUMN statement, add DEFAULT field constraint
Fixes #96.

Adds:
- New `DEFAULT` constraint which sets default value of field/column.
- `ALTER TABLE <table_name> ADD COLUMN <field definition>` statement to add new fields to the table. Original issue requests `ADD FIELD` statement, but I think `ADD COLUMN` is more compatible with SQL. Anyway, it's not too hard to fix.

I have some questions about the _correct_ table info modification. 

Patch:
diff --git a/database/config.go b/database/config.go
index 23151bcf..54fb025c 100644
--- a/database/config.go
+++ b/database/config.go
@@ -19,6 +19,11 @@ type FieldConstraint struct {
 	Type         document.ValueType
 	IsPrimaryKey bool
 	IsNotNull    bool
+	DefaultValue document.Value
+}
+
+func (f *FieldConstraint) HasDefaultValue() bool {
+	return f.DefaultValue.Type != 0
 }
 
 // ToDocument returns a document from f.
@@ -29,6 +34,9 @@ func (f *FieldConstraint) ToDocument() document.Document {
 	buf.Add("type", document.NewIntegerValue(int64(f.Type)))
 	buf.Add("is_primary_key", document.NewBoolValue(f.IsPrimaryKey))
 	buf.Add("is_not_null", document.NewBoolValue(f.IsNotNull))
+	if f.HasDefaultValue() {
+		buf.Add("default_value", f.DefaultValue)
+	}
 	return buf
 }
 
@@ -61,6 +69,15 @@ func (f *FieldConstraint) ScanDocument(d document.Document) error {
 		return err
 	}
 	f.IsNotNull = v.V.(bool)
+
+	v, err = d.GetByField("default_value")
+	if err != nil && err != document.ErrFieldNotFound {
+		return err
+	}
+	if err == nil {
+		f.DefaultValue = v
+	}
+
 	return nil
 }
 
@@ -273,6 +290,47 @@ func (t *tableInfoStore) Delete(tx *Transaction, tableName string) error {
 	return nil
 }
 
+// modifyTable modifies TableInfo using given callback.
+func (t *tableInfoStore) modifyTable(tx *Transaction, tableName string, f func(*TableInfo) error) error {
+	t.mu.Lock()
+	defer t.mu.Unlock()
+
+	info, ok := t.tableInfos[tableName]
+	if !ok {
+		return ErrTableNotFound
+	}
+
+	if info.transactionID != 0 && info.transactionID != tx.id {
+		return ErrTableNotFound
+	}
+
+	err := f(&info)
+	if err != nil {
+		return err
+	}
+
+	st, err := tx.tx.GetStore([]byte(tableInfoStoreName))
+	if err != nil {
+		return err
+	}
+
+	var buf bytes.Buffer
+	err = t.db.Codec.NewEncoder(&buf).EncodeDocument(info.ToDocument())
+	if err != nil {
+		return err
+	}
+
+	err = st.Put([]byte(tableName), buf.Bytes())
+	if err != nil {
+		return err
+	}
+
+	info.transactionID = tx.id
+	t.tableInfos[tableName] = info
+
+	return nil
+}
+
 func (t *tableInfoStore) loadAllTableInfo(tx engine.Transaction) error {
 	t.mu.Lock()
 	defer t.mu.Unlock()
@@ -320,7 +378,7 @@ func (t *tableInfoStore) loadAllTableInfo(tx engine.Transaction) error {
 
 	t.tableInfos[indexStoreName] = TableInfo{
 		storeName: []byte(indexStoreName),
-		readOnly: true,
+		readOnly:  true,
 		FieldConstraints: []FieldConstraint{
 			{
 				Path: document.ValuePath{
diff --git a/database/table.go b/database/table.go
index 6c7924d5..515a5ac0 100644
--- a/database/table.go
+++ b/database/table.go
@@ -466,7 +466,11 @@ func validateConstraint(d document.Document, c *FieldConstraint) error {
 		if field.FieldName == "" {
 			// if the field is not found we make sure it is not required
 			if c.IsNotNull {
-				return fmt.Errorf("field %q is required and must be not null", c.Path)
+				if !c.HasDefaultValue() {
+					return fmt.Errorf("field %q is required and must be not null", c.Path)
+				}
+
+				return buf.Set(c.Path, c.DefaultValue)
 			}
 			return nil
 		}
@@ -476,9 +480,12 @@ func validateConstraint(d document.Document, c *FieldConstraint) error {
 		if err != nil {
 			if err == document.ErrFieldNotFound {
 				if c.IsNotNull {
-					return fmt.Errorf("field %q is required and must be not null", c.Path)
-				}
+					if !c.HasDefaultValue() {
+						return fmt.Errorf("field %q is required and must be not null", c.Path)
+					}
 
+					return buf.Set(c.Path, c.DefaultValue)
+				}
 				return nil
 			}
 
@@ -521,9 +528,12 @@ func validateConstraint(d document.Document, c *FieldConstraint) error {
 		if err != nil {
 			if err == document.ErrValueNotFound {
 				if c.IsNotNull {
-					return fmt.Errorf("value %q is required and must be not null", c.Path)
-				}
+					if !c.HasDefaultValue() {
+						return fmt.Errorf("field %q is required and must be not null", c.Path)
+					}
 
+					return buf.Copy(c.DefaultValue.V.(document.Array))
+				}
 				return nil
 			}
 
diff --git a/database/transaction.go b/database/transaction.go
index a4c851cb..e77bb79f 100644
--- a/database/transaction.go
+++ b/database/transaction.go
@@ -128,6 +128,25 @@ func (tx *Transaction) GetTable(name string) (*Table, error) {
 	}, nil
 }
 
+func (tx *Transaction) AddField(name string, fc FieldConstraint) error {
+	return tx.tableInfoStore.modifyTable(tx, name, func(info *TableInfo) error {
+		for _, field := range info.FieldConstraints {
+			if field.Path.IsEqual(fc.Path) {
+				return fmt.Errorf("field %q already exists", fc.Path.String())
+			}
+			if field.IsPrimaryKey && fc.IsPrimaryKey {
+				return fmt.Errorf(
+					"multiple primary keys are not allowed (%q is primary key)",
+					field.Path.String(),
+				)
+			}
+		}
+
+		info.FieldConstraints = append(info.FieldConstraints, fc)
+		return nil
+	})
+}
+
 // RenameTable renames a table.
 // If it doesn't exist, it returns ErrTableNotFound.
 func (tx *Transaction) RenameTable(oldName, newName string) error {
diff --git a/document/value.go b/document/value.go
index 47e9aefb..c38c96c4 100644
--- a/document/value.go
+++ b/document/value.go
@@ -483,12 +483,13 @@ func calculateFloats(a, b Value, operator byte) (res Value, err error) {
 
 		return NewDoubleValue(xa / xb), nil
 	case '%':
-		if xb == 0 {
+		mod := math.Mod(xa, xb)
+
+		if math.IsNaN(mod) {
 			return NewNullValue(), nil
 		}
 
-		ia, ib := int64(xa), int64(xb)
-		return NewDoubleValue(float64(ia % ib)), nil
+		return NewDoubleValue(mod), nil
 	case '&':
 		ia, ib := int64(xa), int64(xb)
 		return NewIntegerValue(ia & ib), nil
diff --git a/sql/parser/alter.go b/sql/parser/alter.go
index 7ec67b18..b48fe6d8 100644
--- a/sql/parser/alter.go
+++ b/sql/parser/alter.go
@@ -5,40 +5,71 @@ import (
 	"github.com/genjidb/genji/sql/scanner"
 )
 
+func (p *Parser) parseAlterTableRenameStatement(tableName string) (_ query.AlterStmt, err error) {
+	var stmt query.AlterStmt
+	stmt.TableName = tableName
+
+	// Parse "TO".
+	if tok, pos, lit := p.ScanIgnoreWhitespace(); tok != scanner.TO {
+		return stmt, newParseError(scanner.Tokstr(tok, lit), []string{"TO"}, pos)
+	}
+
+	// Parse new table name.
+	stmt.NewTableName, err = p.parseIdent()
+	if err != nil {
+		return stmt, err
+	}
+
+	return stmt, nil
+}
+
+func (p *Parser) parseAlterTableAddFieldStatement(tableName string) (_ query.AlterTableAddField, err error) {
+	var stmt query.AlterTableAddField
+	stmt.TableName = tableName
+
+	// Parse "FIELD".
+	if tok, pos, lit := p.ScanIgnoreWhitespace(); tok != scanner.FIELD {
+		return stmt, newParseError(scanner.Tokstr(tok, lit), []string{"FIELD"}, pos)
+	}
+
+	// Parse new field definition.
+	err = p.parseFieldDefinition(&stmt.Constraint)
+	if err != nil {
+		return stmt, err
+	}
+
+	if stmt.Constraint.IsPrimaryKey {
+		return stmt, &ParseError{Message: "cannot add a PRIMARY KEY constraint"}
+	}
+
+	return stmt, nil
+}
+
 // parseAlterStatement parses a Alter query string and returns a Statement AST object.
 // This function assumes the ALTER token has already been consumed.
-func (p *Parser) parseAlterStatement() (query.AlterStmt, error) {
-	var stmt query.AlterStmt
+func (p *Parser) parseAlterStatement() (query.Statement, error) {
 	var err error
 
 	// Parse "TABLE".
 	if tok, pos, lit := p.ScanIgnoreWhitespace(); tok != scanner.TABLE {
-		return stmt, newParseError(scanner.Tokstr(tok, lit), []string{"TABLE"}, pos)
+		return nil, newParseError(scanner.Tokstr(tok, lit), []string{"TABLE"}, pos)
 	}
 
 	// Parse table name.
-	stmt.TableName, err = p.parseIdent()
+	tableName, err := p.parseIdent()
 	if err != nil {
 		pErr := err.(*ParseError)
 		pErr.Expected = []string{"table_name"}
-		return stmt, pErr
-	}
-
-	// Parse "RENAME".
-	if tok, pos, lit := p.ScanIgnoreWhitespace(); tok != scanner.RENAME {
-		return stmt, newParseError(scanner.Tokstr(tok, lit), []string{"RENAME"}, pos)
+		return nil, pErr
 	}
 
-	// Parse "TO".
-	if tok, pos, lit := p.ScanIgnoreWhitespace(); tok != scanner.TO {
-		return stmt, newParseError(scanner.Tokstr(tok, lit), []string{"TO"}, pos)
-	}
-
-	// Parse new table name.
-	stmt.NewTableName, err = p.parseIdent()
-	if err != nil {
-		return stmt, err
+	tok, pos, lit := p.ScanIgnoreWhitespace()
+	switch tok {
+	case scanner.RENAME:
+		return p.parseAlterTableRenameStatement(tableName)
+	case scanner.ADD_KEYWORD:
+		return p.parseAlterTableAddFieldStatement(tableName)
 	}
 
-	return stmt, nil
+	return nil, newParseError(scanner.Tokstr(tok, lit), []string{"ADD", "RENAME"}, pos)
 }
diff --git a/sql/parser/create.go b/sql/parser/create.go
index 6654990a..be213f66 100644
--- a/sql/parser/create.go
+++ b/sql/parser/create.go
@@ -5,6 +5,7 @@ import (
 
 	"github.com/genjidb/genji/database"
 	"github.com/genjidb/genji/sql/query"
+	"github.com/genjidb/genji/sql/query/expr"
 	"github.com/genjidb/genji/sql/scanner"
 )
 
@@ -75,6 +76,20 @@ func (p *Parser) parseIfNotExists() (bool, error) {
 	return true, nil
 }
 
+func (p *Parser) parseFieldDefinition(fc *database.FieldConstraint) (err error) {
+	fc.Path, err = p.parsePath()
+	if err != nil {
+		return err
+	}
+
+	fc.Type, err = p.parseType()
+	if err != nil {
+		return err
+	}
+
+	return p.parseFieldConstraint(fc)
+}
+
 func (p *Parser) parseFieldConstraints(info *database.TableInfo) error {
 	// Parse ( token.
 	if tok, _, _ := p.ScanIgnoreWhitespace(); tok != scanner.LPAREN {
@@ -88,18 +103,7 @@ func (p *Parser) parseFieldConstraints(info *database.TableInfo) error {
 	for {
 		var fc database.FieldConstraint
 
-		fc.Path, err = p.parsePath()
-		if err != nil {
-			p.Unscan()
-			break
-		}
-
-		fc.Type, err = p.parseType()
-		if err != nil {
-			return err
-		}
-
-		err = p.parseFieldConstraint(&fc)
+		err = p.parseFieldDefinition(&fc)
 		if err != nil {
 			return err
 		}
@@ -159,6 +163,28 @@ func (p *Parser) parseFieldConstraint(fc *database.FieldConstraint) error {
 			}
 
 			fc.IsNotNull = true
+		case scanner.DEFAULT:
+			// Parse default value expression.
+			e, err := p.parseUnaryExpr()
+			if err != nil {
+				return err
+			}
+
+			d, err := e.Eval(expr.EvalStack{})
+			if err != nil {
+				return err
+			}
+
+			if fc.Type != 0 && fc.Type != d.Type {
+				return &ParseError{Message: fmt.Sprintf("default value must have type %s, got %s", fc.Type, d.Type)}
+			}
+
+			// if it's already default value we return an error
+			if fc.HasDefaultValue() {
+				return newParseError(scanner.Tokstr(tok, lit), []string{"CONSTRAINT", ")"}, pos)
+			}
+
+			fc.DefaultValue = d
 		default:
 			p.Unscan()
 			return nil
diff --git a/sql/query/alter.go b/sql/query/alter.go
index 29657f0e..f53ccca2 100644
--- a/sql/query/alter.go
+++ b/sql/query/alter.go
@@ -39,3 +39,30 @@ func (stmt AlterStmt) Run(ctx context.Context, tx *database.Transaction, _ []exp
 	err := tx.RenameTable(stmt.TableName, stmt.NewTableName)
 	return res, err
 }
+
+type AlterTableAddField struct {
+	TableName  string
+	Constraint database.FieldConstraint
+}
+
+// IsReadOnly always returns false. It implements the Statement interface.
+func (stmt AlterTableAddField) IsReadOnly() bool {
+	return false
+}
+
+// Run runs the ALTER TABLE ADD FIELD statement in the given transaction.
+// It implements the Statement interface.
+func (stmt AlterTableAddField) Run(ctx context.Context, tx *database.Transaction, _ []expr.Param) (Result, error) {
+	var res Result
+
+	if stmt.TableName == "" {
+		return res, errors.New("missing table name")
+	}
+
+	if stmt.Constraint.Path == nil {
+		return res, errors.New("missing field name")
+	}
+
+	err := tx.AddField(stmt.TableName, stmt.Constraint)
+	return res, err
+}
diff --git a/sql/scanner/token.go b/sql/scanner/token.go
index 7f6db186..c18d3114 100644
--- a/sql/scanner/token.go
+++ b/sql/scanner/token.go
@@ -72,6 +72,7 @@ const (
 
 	keywordBeg
 	// ALL and the following are Genji SQL Keywords
+	ADD_KEYWORD
 	ALTER
 	AS
 	ASC
@@ -80,11 +81,13 @@ const (
 	CAST
 	COMMIT
 	CREATE
+	DEFAULT
 	DELETE
 	DESC
 	DROP
 	EXISTS
 	EXPLAIN
+	FIELD
 	FROM
 	GROUP
 	IF
@@ -190,6 +193,7 @@ var tokens = [...]string{
 	SEMICOLON:   ";",
 	DOT:         ".",
 
+	ADD_KEYWORD: "ADD",
 	ALTER:       "ALTER",
 	AS:          "AS",
 	ASC:         "ASC",
@@ -199,12 +203,14 @@ var tokens = [...]string{
 	BY:          "BY",
 	CREATE:      "CREATE",
 	CAST:        "CAST",
+	DEFAULT:     "DEFAULT",
 	DELETE:      "DELETE",
 	DESC:        "DESC",
 	DROP:        "DROP",
 	EXISTS:      "EXISTS",
 	EXPLAIN:     "EXPLAIN",
 	KEY:         "KEY",
+	FIELD:       "FIELD",
 	FROM:        "FROM",
 	IF:          "IF",
 	INDEX:       "INDEX",


Test patch:
diff --git a/.github/workflows/test-fuzz.yaml b/.github/workflows/test-fuzz.yaml
index 5438e136..bac331ac 100644
--- a/.github/workflows/test-fuzz.yaml
+++ b/.github/workflows/test-fuzz.yaml
@@ -85,7 +85,10 @@ jobs:
       - name: Run tests
         run: |
           go run github.com/dvyukov/go-fuzz/go-fuzz-build -func ${{ matrix.func }} && timeout -s INT -k 30 300 \
-          go run github.com/dvyukov/go-fuzz/go-fuzz -workdir=testdata/fuzz/${{ matrix.func }} -dumpcover
+          go run github.com/dvyukov/go-fuzz/go-fuzz -workdir=testdata/fuzz/${{ matrix.func }} -dumpcover || true
+          if [ -d crashers ]; then
+            exit 1
+          fi
         working-directory: genji/fuzz
 
       - name: Prepare report
diff --git a/database/table_test.go b/database/table_test.go
index 2260f4cd..2e17c66e 100644
--- a/database/table_test.go
+++ b/database/table_test.go
@@ -337,8 +337,8 @@ func TestTableInsert(t *testing.T) {
 
 		err := tx.CreateTable("test", &database.TableInfo{
 			FieldConstraints: []database.FieldConstraint{
-				{parsePath(t, "foo"), document.IntegerValue, false, false},
-				{parsePath(t, "bar"), document.IntegerValue, false, false},
+				{parsePath(t, "foo"), document.IntegerValue, false, false, document.Value{}},
+				{parsePath(t, "bar"), document.IntegerValue, false, false, document.Value{}},
 			},
 		})
 		require.NoError(t, err)
@@ -375,7 +375,7 @@ func TestTableInsert(t *testing.T) {
 		// no enforced type, not null
 		err := tx.CreateTable("test1", &database.TableInfo{
 			FieldConstraints: []database.FieldConstraint{
-				{parsePath(t, "foo"), 0, false, true},
+				{parsePath(t, "foo"), 0, false, true, document.Value{}},
 			},
 		})
 		require.NoError(t, err)
@@ -385,7 +385,7 @@ func TestTableInsert(t *testing.T) {
 		// enforced type, not null
 		err = tx.CreateTable("test2", &database.TableInfo{
 			FieldConstraints: []database.FieldConstraint{
-				{parsePath(t, "foo"), document.IntegerValue, false, true},
+				{parsePath(t, "foo"), document.IntegerValue, false, true, document.Value{}},
 			},
 		})
 		require.NoError(t, err)
@@ -423,13 +423,80 @@ func TestTableInsert(t *testing.T) {
 		require.NoError(t, err)
 	})
 
+	t.Run("Shouldn't fail if there is a not null field and default constraint on a document field and the field is null or missing", func(t *testing.T) {
+		tx, cleanup := newTestDB(t)
+		defer cleanup()
+
+		// no enforced type, not null
+		err := tx.CreateTable("test1", &database.TableInfo{
+			FieldConstraints: []database.FieldConstraint{
+				{parsePath(t, "foo"), 0, false, true, document.NewIntegerValue(42)},
+			},
+		})
+		require.NoError(t, err)
+		tb1, err := tx.GetTable("test1")
+		require.NoError(t, err)
+
+		// enforced type, not null
+		err = tx.CreateTable("test2", &database.TableInfo{
+			FieldConstraints: []database.FieldConstraint{
+				{parsePath(t, "foo"), document.IntegerValue, false, true, document.NewIntegerValue(42)},
+			},
+		})
+		require.NoError(t, err)
+		tb2, err := tx.GetTable("test2")
+		require.NoError(t, err)
+
+		// insert with empty foo field shouldn't fail
+		k, err := tb1.Insert(document.NewFieldBuffer().
+			Add("bar", document.NewDoubleValue(1)))
+		require.NoError(t, err)
+
+		d, err := tb1.GetDocument(k)
+		require.NoError(t, err)
+		v, err := d.GetByField("foo")
+		require.NoError(t, err)
+		require.Equal(t, v.V.(int64), int64(42))
+
+		// insert with explicit null foo field should fail
+		_, err = tb1.Insert(document.NewFieldBuffer().
+			Add("foo", document.NewNullValue()))
+		require.Error(t, err)
+
+		// otherwise it should work
+		_, err = tb1.Insert(document.NewFieldBuffer().
+			Add("foo", document.NewIntegerValue(1)))
+		require.NoError(t, err)
+
+		// insert with empty foo field shouldn't fail
+		k, err = tb2.Insert(document.NewFieldBuffer().
+			Add("bar", document.NewIntegerValue(1)))
+		require.NoError(t, err)
+
+		d, err = tb2.GetDocument(k)
+		require.NoError(t, err)
+		v, err = d.GetByField("foo")
+		require.NoError(t, err)
+		require.Equal(t, v.V.(int64), int64(42))
+
+		// insert with explicit null foo field should fail
+		_, err = tb2.Insert(document.NewFieldBuffer().
+			Add("foo", document.NewNullValue()))
+		require.Error(t, err)
+
+		// otherwise it should work
+		_, err = tb2.Insert(document.NewFieldBuffer().
+			Add("foo", document.NewDoubleValue(1)))
+		require.NoError(t, err)
+	})
+
 	t.Run("Should fail if there is a not null field constraint on an array value and the value is null", func(t *testing.T) {
 		tx, cleanup := newTestDB(t)
 		defer cleanup()
 
 		err := tx.CreateTable("test1", &database.TableInfo{
 			FieldConstraints: []database.FieldConstraint{
-				{parsePath(t, "foo[1]"), 0, false, true},
+				{parsePath(t, "foo[1]"), 0, false, true, document.Value{}},
 			},
 		})
 		require.NoError(t, err)
diff --git a/database/transaction_test.go b/database/transaction_test.go
index ec3d6203..a8b6294f 100644
--- a/database/transaction_test.go
+++ b/database/transaction_test.go
@@ -28,6 +28,7 @@ func newTestDB(t testing.TB) (*database.Transaction, func()) {
 // - GetTable
 // - DropTable
 // - RenameTable
+// - AddField
 func TestTxTable(t *testing.T) {
 	t.Run("Create", func(t *testing.T) {
 		tx, cleanup := newTestDB(t)
@@ -156,6 +157,50 @@ func TestTxTable(t *testing.T) {
 			require.Equal(t, err, database.ErrTableNotFound)
 		}
 	})
+
+	t.Run("Add field", func(t *testing.T) {
+		tx, cleanup := newTestDB(t)
+		defer cleanup()
+
+		ti := &database.TableInfo{FieldConstraints: []database.FieldConstraint{
+			{Path: parsePath(t, "name"), Type: document.TextValue, IsNotNull: true},
+			{Path: parsePath(t, "age"), Type: document.IntegerValue, IsPrimaryKey: true},
+			{Path: parsePath(t, "gender"), Type: document.TextValue},
+			{Path: parsePath(t, "city"), Type: document.TextValue},
+		}}
+		err := tx.CreateTable("foo", ti)
+		require.NoError(t, err)
+
+		// Add field
+		fieldToAdd := database.FieldConstraint{
+			Path: parsePath(t, "last_name"), Type: document.TextValue,
+		}
+		err = tx.AddField("foo", fieldToAdd)
+		require.NoError(t, err)
+
+		tb, err := tx.GetTable("foo")
+		require.NoError(t, err)
+
+		// The field constraints should not be the same.
+		info, err := tb.Info()
+		require.NoError(t, err)
+		require.Contains(t, info.FieldConstraints, fieldToAdd)
+
+		// Renaming a non existing table should return an error
+		err = tx.AddField("bar", fieldToAdd)
+		require.EqualError(t, database.ErrTableNotFound, err.Error())
+
+		// Adding a existing field should return an error
+		err = tx.AddField("foo", ti.FieldConstraints[0])
+		require.Error(t, err)
+
+		// Adding a second primary key should return an error
+		fieldToAdd = database.FieldConstraint{
+			Path: parsePath(t, "foobar"), Type: document.IntegerValue, IsPrimaryKey: true,
+		}
+		err = tx.AddField("foo", fieldToAdd)
+		require.Error(t, err)
+	})
 }
 
 func TestTxCreateIndex(t *testing.T) {
diff --git a/document/value_test.go b/document/value_test.go
index c0a08a3b..b6750b4a 100644
--- a/document/value_test.go
+++ b/document/value_test.go
@@ -253,7 +253,7 @@ func TestValueMod(t *testing.T) {
 		{"integer(10)%integer(8)", document.NewIntegerValue(10), document.NewIntegerValue(8), document.NewIntegerValue(2), false},
 		{"integer(10)%float64(8)", document.NewIntegerValue(10), document.NewDoubleValue(8), document.NewDoubleValue(2), false},
 		{"int64(maxint)%float64(maxint)", document.NewIntegerValue(math.MaxInt64), document.NewDoubleValue(math.MaxInt64), document.NewDoubleValue(0), false},
-		{"double(> maxint)%int64(100)", document.NewDoubleValue(math.MaxInt64 + 1000), document.NewIntegerValue(100), document.NewDoubleValue(-8), false},
+		{"double(> maxint)%int64(100)", document.NewDoubleValue(math.MaxInt64 + 1000), document.NewIntegerValue(100), document.NewDoubleValue(8), false},
 		{"int64(100)%float64(> maxint)", document.NewIntegerValue(100), document.NewDoubleValue(math.MaxInt64 + 1000), document.NewDoubleValue(100), false},
 		{"integer(120)%text('120')", document.NewIntegerValue(120), document.NewTextValue("120"), document.NewNullValue(), false},
 		{"text('120')%text('120')", document.NewTextValue("120"), document.NewTextValue("120"), document.NewNullValue(), false},
diff --git a/sql/parser/alter_test.go b/sql/parser/alter_test.go
index 294fdde0..6be2f6de 100644
--- a/sql/parser/alter_test.go
+++ b/sql/parser/alter_test.go
@@ -4,6 +4,8 @@ import (
 	"context"
 	"testing"
 
+	"github.com/genjidb/genji/database"
+	"github.com/genjidb/genji/document"
 	"github.com/genjidb/genji/sql/query"
 	"github.com/stretchr/testify/require"
 )
@@ -34,3 +36,54 @@ func TestParserAlterTable(t *testing.T) {
 		})
 	}
 }
+
+func TestParserAlterTableAddColumn(t *testing.T) {
+	tests := []struct {
+		name     string
+		s        string
+		expected query.Statement
+		errored  bool
+	}{
+		{"Basic", "ALTER TABLE foo ADD FIELD bar", query.AlterTableAddField{TableName: "foo",
+			Constraint: database.FieldConstraint{
+				Path: parsePath(t, "bar"),
+			},
+		}, false},
+		{"With type", "ALTER TABLE foo ADD FIELD bar integer", query.AlterTableAddField{TableName: "foo",
+			Constraint: database.FieldConstraint{
+				Path: parsePath(t, "bar"),
+				Type: document.IntegerValue,
+			},
+		}, false},
+		{"With not null", "ALTER TABLE foo ADD FIELD bar NOT NULL", query.AlterTableAddField{TableName: "foo",
+			Constraint: database.FieldConstraint{
+				Path:      parsePath(t, "bar"),
+				IsNotNull: true,
+			},
+		}, false},
+		{"With primary key", "ALTER TABLE foo ADD FIELD bar PRIMARY KEY", query.AlterTableAddField{}, true},
+		{"With multiple constraints", "ALTER TABLE foo ADD FIELD bar integer NOT NULL DEFAULT 0", query.AlterTableAddField{TableName: "foo",
+			Constraint: database.FieldConstraint{
+				Path:         parsePath(t, "bar"),
+				Type:         document.IntegerValue,
+				IsNotNull:    true,
+				DefaultValue: document.NewIntegerValue(0),
+			},
+		}, false},
+		{"With error / missing FIELD keyword", "ALTER TABLE foo ADD bar", nil, true},
+		{"With error / missing field name", "ALTER TABLE foo ADD FIELD", nil, true},
+	}
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			q, err := ParseQuery(context.Background(), 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/sql/parser/create_test.go b/sql/parser/create_test.go
index de1d5c27..ebc8ab20 100644
--- a/sql/parser/create_test.go
+++ b/sql/parser/create_test.go
@@ -48,6 +48,17 @@ func TestParserCreateTable(t *testing.T) {
 					},
 				},
 			}, false},
+		{"With default", "CREATE TABLE test(foo DEFAULT \"10\")",
+			query.CreateTableStmt{
+				TableName: "test",
+				Info: database.TableInfo{
+					FieldConstraints: []database.FieldConstraint{
+						{Path: parsePath(t, "foo"), DefaultValue: document.NewTextValue("10")},
+					},
+				},
+			}, false},
+		{"With default twice", "CREATE TABLE test(foo DEFAULT 10 DEFAULT 10)",
+			query.CreateTableStmt{}, true},
 		{"With not null twice", "CREATE TABLE test(foo NOT NULL NOT NULL)",
 			query.CreateTableStmt{}, true},
 		{"With type and not null", "CREATE TABLE test(foo INTEGER NOT NULL)",
diff --git a/sql/parser/parser_test.go b/sql/parser/parser_test.go
index 0c287762..e7a151e7 100644
--- a/sql/parser/parser_test.go
+++ b/sql/parser/parser_test.go
@@ -4,9 +4,10 @@ import (
 	"context"
 	"testing"
 
+	"github.com/stretchr/testify/require"
+
 	"github.com/genjidb/genji/sql/planner"
 	"github.com/genjidb/genji/sql/query"
-	"github.com/stretchr/testify/require"
 )
 
 func TestParserMultiStatement(t *testing.T) {
@@ -43,3 +44,10 @@ func TestParserMultiStatement(t *testing.T) {
 		})
 	}
 }
+
+func TestParserDivideByZero(t *testing.T) {
+	// See https://github.com/genjidb/genji/issues/268
+	require.NotPanics(t, func() {
+		_, _ = ParseQuery(context.Background(), "SELECT * FROM t LIMIT 0 % .5")
+	})
+}
diff --git a/sql/scanner/scanner_test.go b/sql/scanner/scanner_test.go
index c194ac8d..0975d10e 100644
--- a/sql/scanner/scanner_test.go
+++ b/sql/scanner/scanner_test.go
@@ -116,6 +116,7 @@ func TestScanner_Scan(t *testing.T) {
 		{s: `-10.3`, tok: scanner.NUMBER, lit: `-10.3`, raw: `-10.3`},
 
 		// Keywords
+		{s: `ADD`, tok: scanner.ADD_KEYWORD, raw: `ADD`},
 		{s: `ALTER`, tok: scanner.ALTER, raw: `ALTER`},
 		{s: `AS`, tok: scanner.AS, raw: `AS`},
 		{s: `ASC`, tok: scanner.ASC, raw: `ASC`},
@@ -125,9 +126,11 @@ func TestScanner_Scan(t *testing.T) {
 		{s: `COMMIT`, tok: scanner.COMMIT, raw: `COMMIT`},
 		{s: `CREATE`, tok: scanner.CREATE, raw: `CREATE`},
 		{s: `EXPLAIN`, tok: scanner.EXPLAIN, raw: `EXPLAIN`},
+		{s: `DEFAULT`, tok: scanner.DEFAULT, raw: `DEFAULT`},
 		{s: `DELETE`, tok: scanner.DELETE, raw: `DELETE`},
 		{s: `DESC`, tok: scanner.DESC, raw: `DESC`},
 		{s: `DROP`, tok: scanner.DROP, raw: `DROP`},
+		{s: `FIELD`, tok: scanner.FIELD, raw: `FIELD`},
 		{s: `FROM`, tok: scanner.FROM, raw: `FROM`},
 		{s: `GROUP`, tok: scanner.GROUP, raw: `GROUP`},
 		{s: `INSERT`, tok: scanner.INSERT, raw: `INSERT`},