Skip to content

Commit fd9b93d

Browse files
committed
docs: more architecture guide cleanups
1 parent 686d397 commit fd9b93d

File tree

6 files changed

+129
-98
lines changed

6 files changed

+129
-98
lines changed

docs/architecture/sql-data.md

Lines changed: 40 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,45 @@
11
# SQL Data Model
22

3-
The SQL data model is toyDB's representation of user data. It is made up of data types and schemas.
3+
The SQL data model represents user data in tables and rows. It is made up of data types and schemas,
4+
in the [`sql::types`](https://github.com/erikgrinaker/toydb/tree/686d3971a253bfc9facc2ba1b0e716cff5c109fb/src/sql/types)
5+
module.
46

57
## Data Types
68

7-
toyDB supports four basic scalar data types as `sql::types::DataType`: booleans, floats, integers,
9+
toyDB supports four basic scalar data types as `sql::types::DataType`: booleans, integers, floats,
810
and strings.
911

1012
https://github.com/erikgrinaker/toydb/blob/b2fe7b76ee634ca6ad31616becabfddb1c03d34b/src/sql/types/value.rs#L15-L27
1113

12-
Concrete values are represented as `sql::types::Value`, using corresponding Rust types. toyDB also
13-
supports SQL `NULL` values, i.e. unknown values, following the rules of
14+
Specific values are represented as `sql::types::Value`, using the corresponding Rust types. toyDB
15+
also supports SQL `NULL` values, i.e. unknown values, following the rules of
1416
[three-valued logic](https://en.wikipedia.org/wiki/Three-valued_logic).
1517

1618
https://github.com/erikgrinaker/toydb/blob/b2fe7b76ee634ca6ad31616becabfddb1c03d34b/src/sql/types/value.rs#L40-L64
1719

18-
The `Value` type provides basic formatting, conversion, and mathematical operations. It also
19-
specifies comparison and ordering semantics, but these are subtly different from the SQL semantics.
20-
For example, in Rust code `Value::Null == Value::Null` yields `true`, while in SQL `NULL = NULL`
21-
yields `NULL`. This mismatch is necessary for the Rust code to properly detect and process `Null`
22-
values, and the desired SQL semantics are implemented higher up in the SQL execution engine (we'll
23-
get back to this later).
20+
The `Value` type provides basic formatting, conversion, and mathematical operations.
21+
22+
https://github.com/erikgrinaker/toydb/blob/686d3971a253bfc9facc2ba1b0e716cff5c109fb/src/sql/types/value.rs#L68-L79
23+
24+
https://github.com/erikgrinaker/toydb/blob/686d3971a253bfc9facc2ba1b0e716cff5c109fb/src/sql/types/value.rs#L164-L370
25+
26+
It also specifies comparison and ordering semantics, but these are subtly different from the SQL
27+
semantics. For example, in Rust code `Value::Null == Value::Null` yields `true`, while in SQL
28+
`NULL = NULL` yields `NULL`. This mismatch is necessary for the Rust code to properly detect and
29+
process `Null` values, and the desired SQL semantics are implemented during expression evaluation
30+
which we'll cover below.
2431

2532
https://github.com/erikgrinaker/toydb/blob/b2fe7b76ee634ca6ad31616becabfddb1c03d34b/src/sql/types/value.rs#L91-L162
2633

27-
During execution, a row of values will be represented as `sql::types::Row`, with multiple rows
28-
emitted as `sql::types::Rows` row iterators:
34+
During execution, a row of values is represented as `sql::types::Row`, with multiple rows emitted
35+
via `sql::types::Rows` row iterators:
2936

3037
https://github.com/erikgrinaker/toydb/blob/b2fe7b76ee634ca6ad31616becabfddb1c03d34b/src/sql/types/value.rs#L378-L388
3138

3239
## Schemas
3340

34-
toyDB schemas support a single object: a table. There's only a single, unnamed database, and no
35-
named indexes, constraints, or other schema objects.
41+
toyDB schemas only support tables. There are no named indexes or constraints, and there's only a
42+
single unnamed database.
3643

3744
Tables are represented by `sql::types::Table`:
3845

@@ -47,42 +54,41 @@ The table name serves as a unique identifier, and can't be changed later. In fac
4754
are entirely static: they can only be created or dropped (there are no schema changes).
4855

4956
Table schemas are stored in the catalog, represented by the `sql::engine::Catalog` trait. We'll
50-
revisit the implementation of this trait in the storage section below.
57+
revisit the implementation of this trait in the SQL storage section.
5158

5259
https://github.com/erikgrinaker/toydb/blob/0839215770e31f1e693d5cccf20a68210deaaa3f/src/sql/engine/engine.rs#L60-L79
5360

54-
Table schemas are validated (e.g. during creation) via the `Table::validate()` method, which
55-
enforces invariants and internal consistency. It uses the catalog to look up information about other
56-
tables, e.g. that foreign key references point to a valid target column.
61+
Table schemas are validated when created via `Table::validate()`, which enforces invariants and
62+
internal consistency. It uses the catalog to look up information about other tables, e.g. that
63+
foreign key references point to a valid target column in a different table.
5764

5865
https://github.com/erikgrinaker/toydb/blob/c2b0f7f1d6cbf6e2cdc09fc0aec7b050e840ec21/src/sql/types/schema.rs#L98-L170
5966

60-
It also has a `Table::validate_row()` method which is used to validate that a given
61-
`sql::types::Row` conforms to the schema (e.g. that the value data types match the column data
62-
types). It uses a `sql::engine::Transaction` to look up other rows in the database, e.g. to check
63-
for primary key conflicts (we'll get back to this below).
67+
Table rows are validated via `Table::validate_row()`, which ensures that a `sql::types::Row`
68+
conforms to the schema (e.g. that value types match the column data types). It uses a
69+
`sql::engine::Transaction` to look up other rows in the database, e.g. to check for primary key
70+
conflicts (we'll get back to this later).
6471

6572
https://github.com/erikgrinaker/toydb/blob/c2b0f7f1d6cbf6e2cdc09fc0aec7b050e840ec21/src/sql/types/schema.rs#L172-L236
6673

6774
## Expressions
6875

6976
During SQL execution, we also have to model _expressions_, such as `1 + 2 * 3`. These are
70-
represented as values and operations on them. They can be nested arbitrarily as a tree to represent
71-
compound operations.
77+
represented as values and operations on them, and can be nested as a tree to represent compound
78+
operations.
7279

7380
https://github.com/erikgrinaker/toydb/blob/9419bcf6aededf0e20b4e7485e2a5fa3e975d79f/src/sql/types/expression.rs#L11-L64
7481

7582

76-
For example:
83+
For example, the expression `1 + 2 * 3` (taking [precedence](https://en.wikipedia.org/wiki/Order_of_operations)
84+
into account) is represented as:
7785

7886
```rust
79-
// 1 + 2 * 3 is represented as:
80-
//
81-
// +
82-
// / \
83-
// 1 *
84-
// / \
85-
/// 2 3
87+
// +
88+
// / \
89+
// 1 *
90+
// / \
91+
/// 2 3
8692
Expression::Add(
8793
Expression::Constant(Value::Integer(1)),
8894
Expression::Multiply(
@@ -97,8 +103,8 @@ An `Expression` can contain two kinds of values: constant values as
97103
references. The latter will fetch a `sql::types::Value` from a `sql::types::Row` at the specified
98104
index during evaluation.
99105

100-
We'll see later how the SQL parser and planner transforms text expressions like `1 + 2 * 3` into
101-
this `Expression` form, and how it resolves column names to row indexes -- e.g. `price * 0.25` to
106+
We'll see later how the SQL parser and planner transforms text expression like `1 + 2 * 3` into an
107+
`Expression`, and how it resolves column names to row indexes like `price * 0.25` to
102108
`row[3] * 0.25`.
103109

104110
Expressions are evaluated recursively via `Expression::evalute()`, given a `sql::types::Row` with

docs/architecture/sql-parser.md

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# SQL Parsing
22

3-
And so we finally arrive at SQL. The SQL parser is the first stage in processing SQL
4-
queries and statements, located in the [`src/sql/parser`](https://github.com/erikgrinaker/toydb/tree/39c6b60afc4c235f19113dc98087176748fa091d/src/sql/parser)
3+
We finally arrive at SQL. The SQL parser is the first stage in processing SQL queries and
4+
statements, located in the [`sql::parser`](https://github.com/erikgrinaker/toydb/tree/39c6b60afc4c235f19113dc98087176748fa091d/src/sql/parser)
55
module.
66

77
The SQL parser's job is to take a raw SQL string and turn it into a structured form that's more
@@ -99,7 +99,7 @@ string are well-formed. For example, the following input string:
9999
Will result in these tokens:
100100

101101
```
102-
String("foo"), CloseParen, Number("3.14"), Keyword(Select), Plus, Ident("x")
102+
String("foo") CloseParen Number("3.14") Keyword(Select) Plus Ident("x")
103103
```
104104

105105
Tokens and keywords are represented by the `sql::parser::Token` and `sql::parser::Keyword` enums
@@ -137,12 +137,14 @@ kinds of SQL statements that we support, along with their contents:
137137

138138
https://github.com/erikgrinaker/toydb/blob/39c6b60afc4c235f19113dc98087176748fa091d/src/sql/parser/ast.rs#L6-L145
139139

140-
The nested tree structure is particularly apparent with _expressions_ -- these represent values and
141-
operations which will eventually _evaluate_ to a single value. For example, the expression
142-
`2 * 3 - 4 / 2`, which evaluates to the value `4`.
140+
The nested tree structure is particularly apparent with expressions, which represent values and
141+
operations on them. For example, the expression `2 * 3 - 4 / 2`, which evaluates to the value `4`.
143142

144-
These expressions are represented as `sql::parser::ast::Expression`, and can be nested indefinitely
145-
into a tree structure.
143+
We've seen in the data model section how such expressions are represented as
144+
`sql::types::Expression`, but before we get there we have to parse them. The parser has its own
145+
representation `sql::parser::ast::Expression` -- this is necessary e.g. because in the AST, we
146+
represent columns as names rather than numeric indexes (we don't know yet which columns exist or
147+
what their names are, we'll get to that during planning).
146148

147149
https://github.com/erikgrinaker/toydb/blob/39c6b60afc4c235f19113dc98087176748fa091d/src/sql/parser/ast.rs#L147-L170
148150

@@ -215,7 +217,7 @@ than that of the operators preceding them (hence "precedence climbing"). For exa
215217
2 * 3 - 4 / 2
216218
```
217219

218-
The algorithm is documented in more detail on `Parser::parse_expression`:
220+
The algorithm is documented in more detail on `Parser::parse_expression()`:
219221

220222
https://github.com/erikgrinaker/toydb/blob/39c6b60afc4c235f19113dc98087176748fa091d/src/sql/parser/parser.rs#L501-L696
221223

docs/architecture/sql-planner.md

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
# SQL Planning
22

3-
The SQL planner in [`sql/planner`](https://github.com/erikgrinaker/toydb/tree/c64012e29c5712d6fe028d3d5375a98b8faea266/src/sql/planner)
4-
takes a SQL statement AST from the parser and generates an execution plan for it. We won't actually
5-
execute it just yet though, only figure out how to execute it.
3+
The SQL planner in the [`sql::planner`](https://github.com/erikgrinaker/toydb/tree/c64012e29c5712d6fe028d3d5375a98b8faea266/src/sql/planner)
4+
module takes a SQL statement AST from the parser and generates an execution plan for it. We won't
5+
actually execute it just yet though, only figure out how to execute it.
66

77
## Execution Plan
88

@@ -16,7 +16,7 @@ emits a stream of SQL rows as output, and may take streams of input rows from ch
1616

1717
https://github.com/erikgrinaker/toydb/blob/213e5c02b09f1a3cac6a8bbd0a81773462f367f5/src/sql/planner/plan.rs#L106-L175
1818

19-
Here is an example (taken from the `Plan` code comment above):
19+
Here is an example, taken from the `Plan` code comment above:
2020

2121
```sql
2222
SELECT title, released, genres.name AS genre
@@ -63,12 +63,13 @@ the `Order` node still needs access to the column data to sort by it).
6363

6464
The planner uses a `sql::planner::Scope` to keep track of which column names are currently visible,
6565
and which column indexes they refer to. For each node the planner builds, starting from the leaves,
66-
it creates a new `Scope` that tracks how columns are modified and rearranged by the node.
66+
it creates a new `Scope` that contains the currently visible columns, tracking how they are modified
67+
and rearranged by each node.
6768

6869
https://github.com/erikgrinaker/toydb/blob/6f6cec4db10bc015a37ee47ff6c7dae383147dd5/src/sql/planner/planner.rs#L577-L610
6970

70-
When an expression refers to a column name, the planner can use `Scope::lookup_column` to find out
71-
which column number the expression should take its input value from.
71+
When an AST expression refers to a column name, the planner can use `Scope::lookup_column()` to find
72+
out which column number the expression should take its input value from.
7273

7374
https://github.com/erikgrinaker/toydb/blob/6f6cec4db10bc015a37ee47ff6c7dae383147dd5/src/sql/planner/planner.rs#L660-L686
7475

@@ -154,24 +155,24 @@ https://github.com/erikgrinaker/toydb/blob/6f6cec4db10bc015a37ee47ff6c7dae383147
154155

155156
https://github.com/erikgrinaker/toydb/blob/6f6cec4db10bc015a37ee47ff6c7dae383147dd5/src/sql/planner/planner.rs#L283-L289
156157

157-
`Planner::build_from` first encounters the `ast::From::Join` item, which joins `movies` and
158+
`Planner::build_from()` first encounters the `ast::From::Join` item, which joins `movies` and
158159
`genres`. This will build a `Node::NestedLoopJoin` plan node for the join, which is the simplest and
159160
most straightforward join algorithm -- it simply iterates over all rows in the `genres` table for
160161
every row in the `movies` table and emits the joined rows (we'll see how to optimize it with a
161162
better join algorithm later).
162163

163164
https://github.com/erikgrinaker/toydb/blob/6f6cec4db10bc015a37ee47ff6c7dae383147dd5/src/sql/planner/planner.rs#L319-L344
164165

165-
It first recurses into `Planner::build_from` to build each of the `ast::From::Table` nodes for each
166-
table. This will look up the table schemas in the catalog, add them to the current scope, and build
167-
a `Node::Scan` node which will emit all rows from each table. The `Node::Scan` nodes are placed into
168-
the `Node::NestedLoopJoin` above.
166+
It first recurses into `Planner::build_from()` to build each of the `ast::From::Table` nodes for
167+
each table. This will look up the table schemas in the catalog, add them to the current scope, and
168+
build a `Node::Scan` node which will emit all rows from each table. The `Node::Scan` nodes are
169+
placed into the `Node::NestedLoopJoin` above.
169170

170171
https://github.com/erikgrinaker/toydb/blob/6f6cec4db10bc015a37ee47ff6c7dae383147dd5/src/sql/planner/planner.rs#L312-L317
171172

172173
While building the `Node::NestedLoopJoin`, it also needs to convert the join expression
173174
`movies.genre_id = genres.id` into a proper `sql::types::Expression`. This is done by
174-
`Planner::build_expression`:
175+
`Planner::build_expression()`:
175176

176177
https://github.com/erikgrinaker/toydb/blob/6f6cec4db10bc015a37ee47ff6c7dae383147dd5/src/sql/planner/planner.rs#L493-L568
177178

@@ -228,9 +229,6 @@ sorts them by the order expression.
228229
https://github.com/erikgrinaker/toydb/blob/6f6cec4db10bc015a37ee47ff6c7dae383147dd5/src/sql/planner/planner.rs#L245-L252
229230

230231
And that's it. The `Node::Order` is placed into the root `Plan::Select`, and we have our final plan.
231-
We'll see how to execute it soon, but first we should optimize it to see if we can make it run
232-
faster -- in particular, to see if we can avoid reading all movies from storage, and if we can do
233-
better than the very slow nested loop join.
234232

235233
```
236234
Select
@@ -242,6 +240,10 @@ Select
242240
└─ Scan: genres
243241
```
244242

243+
We'll see how to execute it soon, but first we should optimize it to see if we can make it run
244+
faster -- in particular, to see if we can avoid reading all movies from storage, and if we can do
245+
better than the very slow nested loop join.
246+
245247
---
246248

247249
<p align="center">

docs/architecture/sql-raft.md

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
# SQL Raft Replication
22

3-
toyDB uses Raft to replicate SQL storage across a cluster of nodes (see the Raft section above for
4-
details). All nodes will store a copy of the SQL database, and the Raft leader will replicate writes
5-
across nodes and execute reads.
3+
toyDB uses Raft to replicate SQL storage across a cluster of nodes (see the Raft section for
4+
details). All nodes will store a full copy of the SQL database, and the Raft leader will replicate
5+
writes across nodes and execute reads.
66

77
Recall the Raft state machine interface `raft::State`:
88

@@ -44,20 +44,39 @@ send requests to the local Raft node (we'll see how this plumbing works in the s
4444

4545
https://github.com/erikgrinaker/toydb/blob/c2b0f7f1d6cbf6e2cdc09fc0aec7b050e840ec21/src/sql/engine/raft.rs#L80-L95
4646

47-
The channel takes a `raft::Request` containing binary Raft client requests, and also a return
48-
channel where the Raft node can send back a `raft::Response`. The Raft engine has a few convenience
49-
methods to send requests and receive responses, for both read and write requests:
47+
The channel takes a `raft::Request` containing binary Raft client requests and a return channel
48+
where the Raft node can send back a `raft::Response`. The Raft engine has a few convenience methods
49+
to send requests and receive responses, for both read and write requests:
5050

5151
https://github.com/erikgrinaker/toydb/blob/c2b0f7f1d6cbf6e2cdc09fc0aec7b050e840ec21/src/sql/engine/raft.rs#L114-L135
5252

53-
And the implementation of the `Engine` and `Transaction` traits simply send requests via Raft:
53+
And the implementation of the `sql::engine::Engine` and `sql::engine::Transaction` traits simply
54+
send these requests via Raft:
5455

5556
https://github.com/erikgrinaker/toydb/blob/c2b0f7f1d6cbf6e2cdc09fc0aec7b050e840ec21/src/sql/engine/raft.rs#L194-L276
5657

5758
One thing to note here is that we don't support streaming data via Raft, so e.g. the
5859
`Transaction::scan` method will buffer the entire result in a `Vec`. With a full table scan, this
5960
will load the entire table into memory -- that's unfortunate, but we keep it simple.
6061

62+
To summarize, this is what happens when `Transaction::insert()` is called to insert a row via Raft:
63+
64+
1. `sql::engine::raft::Transaction::insert()`: called to insert a row.
65+
2. `sql::engine::raft::Write::Insert`: enum representation of the insert command.
66+
3. `raft::Request::Write`: raft request containing the Bincode-encoded `Write::Insert` command.
67+
4. `sql::engine::raft::Engine::tx`: sends the `Request::Write` and response channel to Raft.
68+
5. `raft::Node::step()`: the `Request::Write` is given to Raft in a `Message::ClientRequest`.
69+
6. Raft does its replication thing, and commits the command's log entry.
70+
7. `raft::State::apply()`: the Bincode-encoded `Write::Insert` is passed to the state machine.
71+
8. `sql::engine::raft::State::apply()`: decodes the command to a `Write::Insert`.
72+
9. `sql::engine::raft::State::local`: contains the `Local` engine on each node.
73+
10. `sql::engine::local::Engine::resume()`: called to obtain the SQL/MVCC transaction.
74+
11. `sql::engine::local::Transaction::insert()`: the row is inserted to the local engine.
75+
12. `raft::RawNode::tx`: the `Ok(())` result is sent as a Bincode-encoded `Message::ClientResponse`.
76+
13. `sql::engine::raft::Transaction::insert()`: receives the result and returns it to the caller.
77+
78+
The plumbing here will be covered in more details in the server section.
79+
6180
---
6281

6382
<p align="center">

0 commit comments

Comments
 (0)