The web version only has simple instructions since chapter 04, while the full book has detailed explanations and background info.

0303: Parse SELECT

Structs

Since only a small set of features has been implemented, our SELECT statement supports only one fixed form: query a single row by primary key. For example, for a table with primary key (c, d), the only supported query is this:

select a,b from t where c=1 and d='e';

Represented as data structures:

StmtSelect{
    table: "t",
    cols: []string{"a", "b"},
    keys: []NamedCell{
        {column: "c", value: Cell{Type: TypeI64, I64: 1}},
        {column: "d", value: Cell{Type: TypeStr, Str: []byte("e")}},
    },
}

The structs are defined as follows for now and will be extended later:

type StmtSelect struct {
    table string
    cols  []string
    keys  []NamedCell
}
type NamedCell struct {
    column string
    value  Cell
}

Syntax Parsing

Syntax parsing consumes tokens from left to right while building data structures. For example, parsing a = 123 produces a NamedCell. It calls tryName(), tryPunctuation(), and parseValue() in order:

func (p *Parser) parseEqual(out *NamedCell) error {
    var ok bool
    out.column, ok = p.tryName()
    if !ok {
        return errors.New("expect column")
    }
    if !p.tryPunctuation("=") {
        return errors.New("expect =")
    }
    return p.parseValue(&out.value)
}

A new function tryPunctuation() is added here. It is similar to tryKeyword(), but consumes a punctuation character. The rules are simpler than keywords.

When parsing the WHERE part, parseEqual() is called in a loop. Even complex syntax becomes easy after splitting it into parts.

Parse SELECT Statements

The leading select keyword is used to distinguish SQL statements, though only one is supported now. Then comes a list of column names separated by commas, ending at from.

func (p *Parser) parseSelect(out *StmtSelect) error {
    if !p.tryKeyword("SELECT") {
        return errors.New("expect keyword")
    }
    for !p.tryKeyword("FROM") {
        if len(out.cols) > 0 && !p.tryPunctuation(",") {
            return errors.New("expect comma")
        }
        if name, ok := p.tryName(); ok {
            out.cols = append(out.cols, name)
        } else {
            return errors.New("expect column")
        }
    }
    if len(out.cols) == 0 {
        return errors.New("expect column list")
    }
    var ok bool
    if out.table, ok = p.tryName(); !ok {
        return errors.New("expect table name")
    }
    return p.parseWhere(&out.keys)
}

Parse WHERE

This part only supports a = 123 joined by AND. It is similar to SELECT a,b, except commas are replaced by AND, and tryName() is replaced by parseEqual(). You can implement it yourself:

func (p *Parser) parseWhere(out *[]NamedCell) error

CodeCrafters.io has similar courses in many programming languages, including build your own Redis, SQLite, Docker, etc. It’s worth checking out.