Skip to content

Commit

Permalink
primary key column constraint (#3)
Browse files Browse the repository at this point in the history
  • Loading branch information
chirst authored Jul 11, 2024
1 parent 0195f30 commit 0fe34b1
Show file tree
Hide file tree
Showing 17 changed files with 341 additions and 87 deletions.
21 changes: 13 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,22 +28,21 @@ from --> table
```

### CREATE
Create has non standard behavior for primary keys. A `id` column of type
`INTEGER` is automatically added for all tables. If a `id` column is specified
in the `CREATE` statement the specification will only allow the column position
and column casing to be changed.
Create supports the `PRIMARY KEY` column constraint for a single integer column.
```mermaid
graph LR
begin(( ))
explain([EXPLAIN])
create([CREATE])
table([TABLE])
colType([INTEGER or TEXT])
colTypeInt([INTEGER])
colTypeText([TEXT])
lparen["("]
colSep[","]
rparen[")"]
tableIdent["Table Identifier"]
colIdent["Column Identifier"]
pkConstraint["PRIMARY KEY"]
begin --> explain
begin --> create
Expand All @@ -52,9 +51,15 @@ create --> table
table --> tableIdent
tableIdent --> lparen
lparen --> colIdent
colIdent --> colType
colType --> colSep
colType --> rparen
colIdent --> colTypeInt
colIdent --> colTypeText
colTypeInt --> pkConstraint
colTypeInt --> colSep
colTypeInt --> rparen
colTypeText --> colSep
colTypeText --> rparen
pkConstraint --> colSep
pkConstraint --> rparen
colSep --> rparen
colSep --> colIdent
```
Expand Down
5 changes: 3 additions & 2 deletions compiler/ast.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,9 @@ type CreateStmt struct {
}

type ColDef struct {
ColName string
ColType string
ColName string
ColType string
PrimaryKey bool
}

type InsertStmt struct {
Expand Down
4 changes: 4 additions & 0 deletions compiler/lexer.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ const (
kwValues = "VALUES"
kwInteger = "INTEGER"
kwText = "TEXT"
kwPrimary = "PRIMARY"
kwKey = "KEY"
)

var keywords = []string{
Expand All @@ -65,6 +67,8 @@ var keywords = []string{
kwValues,
kwInteger,
kwText,
kwPrimary,
kwKey,
}

func (*lexer) isKeyword(w string) bool {
Expand Down
11 changes: 10 additions & 1 deletion compiler/lexer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ func TestLexSelect(t *testing.T) {
func TestLexCreate(t *testing.T) {
cases := []tc{
{
sql: "CREATE TABLE foo (id INTEGER, first_name TEXT, last_name TEXT)",
sql: "CREATE TABLE foo (id INTEGER PRIMARY KEY, first_name TEXT, last_name TEXT, age INTEGER)",
expected: []token{
{tkKeyword, "CREATE"},
{tkWhitespace, " "},
Expand All @@ -117,6 +117,10 @@ func TestLexCreate(t *testing.T) {
{tkIdentifier, "id"},
{tkWhitespace, " "},
{tkKeyword, "INTEGER"},
{tkWhitespace, " "},
{tkKeyword, "PRIMARY"},
{tkWhitespace, " "},
{tkKeyword, "KEY"},
{tkSeparator, ","},
{tkWhitespace, " "},
{tkIdentifier, "first_name"},
Expand All @@ -127,6 +131,11 @@ func TestLexCreate(t *testing.T) {
{tkIdentifier, "last_name"},
{tkWhitespace, " "},
{tkKeyword, "TEXT"},
{tkSeparator, ","},
{tkWhitespace, " "},
{tkIdentifier, "age"},
{tkWhitespace, " "},
{tkKeyword, "INTEGER"},
{tkSeparator, ")"},
},
},
Expand Down
16 changes: 13 additions & 3 deletions compiler/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,11 +120,21 @@ func (p *parser) parseCreate(sb *StmtBase) (*CreateStmt, error) {
if colType.value != kwInteger && colType.value != kwText {
return nil, fmt.Errorf(columnErr, colType.value)
}
sep := p.nextNonSpace()
isPrimaryKey := false
if sep.value == kwPrimary {
keyKw := p.nextNonSpace()
if keyKw.value != kwKey {
return nil, fmt.Errorf(tokenErr, tn.value)
}
isPrimaryKey = true
sep = p.nextNonSpace()
}
stmt.ColDefs = append(stmt.ColDefs, ColDef{
ColName: colName.value,
ColType: colType.value,
ColName: colName.value,
ColType: colType.value,
PrimaryKey: isPrimaryKey,
})
sep := p.nextNonSpace()
if sep.value != "," {
if sep.value == ")" {
break
Expand Down
9 changes: 7 additions & 2 deletions compiler/parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,10 @@ func TestParseCreate(t *testing.T) {
{tkIdentifier, "id"},
{tkWhitespace, " "},
{tkKeyword, "INTEGER"},
{tkWhitespace, " "},
{tkKeyword, "PRIMARY"},
{tkWhitespace, " "},
{tkKeyword, "KEY"},
{tkSeparator, ","},
{tkWhitespace, " "},
{tkIdentifier, "first_name"},
Expand All @@ -112,8 +116,9 @@ func TestParseCreate(t *testing.T) {
TableName: "foo",
ColDefs: []ColDef{
{
ColName: "id",
ColType: "INTEGER",
ColName: "id",
ColType: "INTEGER",
PrimaryKey: true,
},
{
ColName: "first_name",
Expand Down
1 change: 1 addition & 0 deletions db/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ type dbCatalog interface {
GetRootPageNumber(tableOrIndexName string) (int, error)
TableExists(tableName string) bool
GetVersion() string
GetPrimaryKeyColumn(tableName string) (string, error)
}

type DB struct {
Expand Down
8 changes: 4 additions & 4 deletions db/db_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,15 @@ func mustExecute(t *testing.T, db *DB, sql string) vm.ExecuteResult {

func TestExecute(t *testing.T) {
db := mustCreateDB(t)
mustExecute(t, db, "CREATE TABLE person (id INTEGER, first_name TEXT, last_name TEXT, age INTEGER)")
mustExecute(t, db, "CREATE TABLE person (id INTEGER PRIMARY KEY, first_name TEXT, last_name TEXT, age INTEGER)")
schemaRes := mustExecute(t, db, "SELECT * FROM cdb_schema")
schemaSelectExpectations := []string{
"1",
"table",
"person",
"person",
"2",
"{\"columns\":[{\"name\":\"id\",\"type\":\"INTEGER\"},{\"name\":\"first_name\",\"type\":\"TEXT\"},{\"name\":\"last_name\",\"type\":\"TEXT\"},{\"name\":\"age\",\"type\":\"INTEGER\"}]}",
"{\"columns\":[{\"name\":\"id\",\"type\":\"INTEGER\",\"primaryKey\":true},{\"name\":\"first_name\",\"type\":\"TEXT\",\"primaryKey\":false},{\"name\":\"last_name\",\"type\":\"TEXT\",\"primaryKey\":false},{\"name\":\"age\",\"type\":\"INTEGER\",\"primaryKey\":false}]}",
}
for i, s := range schemaSelectExpectations {
if c := *schemaRes.ResultRows[0][i]; c != s {
Expand All @@ -57,7 +57,7 @@ func TestExecute(t *testing.T) {

func TestBulkInsert(t *testing.T) {
db := mustCreateDB(t)
mustExecute(t, db, "CREATE TABLE test (id INTEGER, junk TEXT)")
mustExecute(t, db, "CREATE TABLE test (id INTEGER PRIMARY KEY, junk TEXT)")
expectedTotal := 100_000
for i := 0; i < expectedTotal; i += 1 {
mustExecute(t, db, "INSERT INTO test (junk) VALUES ('asdf')")
Expand All @@ -79,7 +79,7 @@ func TestBulkInsert(t *testing.T) {

func TestPrimaryKeyUniqueConstraintViolation(t *testing.T) {
db := mustCreateDB(t)
mustExecute(t, db, "CREATE TABLE test (id INTEGER, junk TEXT)")
mustExecute(t, db, "CREATE TABLE test (id INTEGER PRIMARY KEY, junk TEXT)")
mustExecute(t, db, "INSERT INTO test (id, junk) VALUES (1, 'asdf')")
dupePKResponse := db.Execute("INSERT INTO test (id, junk) VALUES (1, 'asdf')")
if dupePKResponse.Err.Error() != "pk unique constraint violated" {
Expand Down
2 changes: 1 addition & 1 deletion driver/driver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ func TestDriver(t *testing.T) {
if err != nil {
t.Fatalf("open err %s", err.Error())
}
_, err = db.Exec("CREATE TABLE foo (id INTEGER, name TEXT)")
_, err = db.Exec("CREATE TABLE foo (id INTEGER PRIMARY KEY, name TEXT)")
if err != nil {
t.Fatalf("exec err %s", err.Error())
}
Expand Down
24 changes: 22 additions & 2 deletions kv/catalog.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,25 @@ func (c *catalog) GetColumns(tableName string) ([]string, error) {
return nil, fmt.Errorf("cannot get columns for table %s", tableName)
}

func (c *catalog) GetPrimaryKeyColumn(tableName string) (string, error) {
if tableName == "cdb_schema" {
return "id", nil
}
for _, o := range c.schema.objects {
if o.name == tableName && o.tableName == tableName {
ts := TableSchemaFromString(o.jsonSchema)
for _, col := range ts.Columns {
if col.PrimaryKey {
return col.Name, nil
}
}
// Table has no PK
return "", nil
}
}
return "", fmt.Errorf("cannot get pk for table %s", tableName)
}

func (c *catalog) TableExists(tableName string) bool {
return slices.ContainsFunc(c.schema.objects, func(o object) bool {
return o.objectType == "table" && o.tableName == tableName
Expand Down Expand Up @@ -105,8 +124,9 @@ type TableSchema struct {
}

type TableColumn struct {
Name string `json:"name"`
ColType string `json:"type"`
Name string `json:"name"`
ColType string `json:"type"`
PrimaryKey bool `json:"primaryKey"`
}

func (ts *TableSchema) ToJSON() ([]byte, error) {
Expand Down
2 changes: 1 addition & 1 deletion main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (

func TestBulk(t *testing.T) {
d, _ := db.New(true, "")
r := d.Execute("CREATE TABLE test (id INTEGER, junk TEXT)")
r := d.Execute("CREATE TABLE test (id INTEGER PRIMARY KEY, junk TEXT)")
if r.Err != nil {
t.Fatal(r.Err.Error())
}
Expand Down
66 changes: 34 additions & 32 deletions planner/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@ package planner
import (
"errors"
"slices"
"strings"

"github.com/chirst/cdb/compiler"
"github.com/chirst/cdb/kv"
"github.com/chirst/cdb/vm"
)

var errInvalidIDColumnType = errors.New("id column must be INTEGER type")
var errInvalidPKColumnType = errors.New("primary key must be INTEGER type")
var errTableExists = errors.New("table exists")
var errMoreThanOnePK = errors.New("more than one primary key specified")

// createCatalog defines the catalog methods needed by the create planner
type createCatalog interface {
Expand Down Expand Up @@ -75,8 +75,10 @@ func (c *createPlanner) ensureTableDoesNotExist(s *compiler.CreateStmt) error {
}

func getSchemaString(s *compiler.CreateStmt) (string, error) {
ensureIDColumn(s)
if err := ensureIntegerID(s); err != nil {
if err := ensurePrimaryKeyCount(s); err != nil {
return "", err
}
if err := ensurePrimaryKeyInteger(s); err != nil {
return "", err
}
jSchema, err := schemaFrom(s).ToJSON()
Expand All @@ -86,38 +88,37 @@ func getSchemaString(s *compiler.CreateStmt) (string, error) {
return string(jSchema), nil
}

// Add an id column as the first argument if the statement doesn't contain some
// upper lower case variation of id. This primary key is not optional, but is
// allowed to be specified in any nth column position or with any casing.
func ensureIDColumn(s *compiler.CreateStmt) {
hasId := slices.ContainsFunc(s.ColDefs, func(cd compiler.ColDef) bool {
return strings.ToLower(cd.ColName) == "id"
})
if hasId {
return
}
s.ColDefs = append(
[]compiler.ColDef{
{
ColName: "id",
ColType: "INTEGER",
},
},
s.ColDefs...,
)
}

// The id column must be an integer. The index key is capable of being something
// other than an integer, but is not worth implementing at the moment. Integer
// primary keys are superior for auto incrementing and being unique.
func ensureIntegerID(s *compiler.CreateStmt) error {
hasIntegerID := slices.ContainsFunc(s.ColDefs, func(cd compiler.ColDef) bool {
return strings.ToLower(cd.ColName) == "id" && cd.ColType == "INTEGER"
func ensurePrimaryKeyInteger(s *compiler.CreateStmt) error {
hasPK := slices.ContainsFunc(s.ColDefs, func(cd compiler.ColDef) bool {
return cd.PrimaryKey
})
if hasIntegerID {
if !hasPK {
return nil
}
return errInvalidIDColumnType
hasIntegerPK := slices.ContainsFunc(s.ColDefs, func(cd compiler.ColDef) bool {
return cd.PrimaryKey && cd.ColType == "INTEGER"
})
if !hasIntegerPK {
return errInvalidPKColumnType
}
return nil
}

// Only one primary key is supported at this time.
func ensurePrimaryKeyCount(s *compiler.CreateStmt) error {
count := 0
for _, cd := range s.ColDefs {
if cd.PrimaryKey {
count += 1
}
}
if count > 1 {
return errMoreThanOnePK
}
return nil
}

func schemaFrom(s *compiler.CreateStmt) *kv.TableSchema {
Expand All @@ -126,8 +127,9 @@ func schemaFrom(s *compiler.CreateStmt) *kv.TableSchema {
}
for _, cd := range s.ColDefs {
schema.Columns = append(schema.Columns, kv.TableColumn{
Name: cd.ColName,
ColType: cd.ColType,
Name: cd.ColName,
ColType: cd.ColType,
PrimaryKey: cd.PrimaryKey,
})
}
return &schema
Expand Down
Loading

0 comments on commit 0fe34b1

Please sign in to comment.