Merge pull request #1 from codecrafters-io/sync
Sync with languages repo
This commit is contained in:
commit
c72d33e880
12
.gitignore
vendored
Normal file
12
.gitignore
vendored
Normal file
|
@ -0,0 +1,12 @@
|
|||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
debug/
|
||||
target/
|
||||
|
||||
# These are backup files generated by rustfmt
|
||||
**/*.rs.bk
|
||||
|
||||
# MSVC Windows builds of rustc generate these, which store debugging information
|
||||
*.pdb
|
||||
|
||||
*.db
|
14
Cargo.lock
generated
Normal file
14
Cargo.lock
generated
Normal file
|
@ -0,0 +1,14 @@
|
|||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.44"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "61604a8f862e1d5c3229fdd78f8b02c68dcf73a4c4b05fd636d12240aaa242c1"
|
||||
|
||||
[[package]]
|
||||
name = "sqlite-starter-rust"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
]
|
22
Cargo.toml
Normal file
22
Cargo.toml
Normal file
|
@ -0,0 +1,22 @@
|
|||
# DON'T EDIT THIS!
|
||||
#
|
||||
# Codecrafters relies on this file being intact to run tests successfully. Any changes
|
||||
# here will not reflect when CodeCrafters tests your code, and might even cause build
|
||||
# failures.
|
||||
#
|
||||
# DON'T EDIT THIS!
|
||||
[package]
|
||||
name = "sqlite-starter-rust"
|
||||
version = "0.1.0"
|
||||
authors = ["Codecrafters <hello@codecrafters.io>"]
|
||||
edition = "2018"
|
||||
|
||||
# DON'T EDIT THIS!
|
||||
#
|
||||
# Codecrafters relies on this file being intact to run tests successfully. Any changes
|
||||
# here will not reflect when CodeCrafters tests your code, and might even cause build
|
||||
# failures.
|
||||
#
|
||||
# DON'T EDIT THIS!
|
||||
[dependencies]
|
||||
anyhow = "1.0.43" # for easy error handling
|
86
README.md
86
README.md
|
@ -1 +1,85 @@
|
|||
# sqlite-starter-rust
|
||||
This is a starting point for Rust solutions to the
|
||||
["Build Your Own SQLite" Challenge](https://codecrafters.io/challenges/sqlite).
|
||||
|
||||
In this challenge, you'll build a barebones SQLite implementation that supports
|
||||
basic SQL queries like `SELECT`. Along the way we'll learn about
|
||||
[SQLite's file format](https://www.sqlite.org/fileformat.html), how indexed data
|
||||
is
|
||||
[stored in B-trees](https://jvns.ca/blog/2014/10/02/how-does-sqlite-work-part-2-btrees/)
|
||||
and more.
|
||||
|
||||
**Note**: If you're viewing this repo on GitHub, head over to
|
||||
[codecrafters.io](https://codecrafters.io) to signup for early access.
|
||||
|
||||
# Passing the first stage
|
||||
|
||||
CodeCrafters runs tests when you do a `git push`. Make an empty commit and push
|
||||
your solution to see the first stage fail.
|
||||
|
||||
```sh
|
||||
git commit --allow-empty -m "Running tests"
|
||||
git push origin master
|
||||
```
|
||||
|
||||
You should see a failure message that says it expected "number of tables: <n>"
|
||||
to be present.
|
||||
|
||||
Go to `src/main.rs` and uncomment the `.dbinfo` command implementation. Commit
|
||||
and push your changes to pass the first stage:
|
||||
|
||||
```sh
|
||||
git add .
|
||||
git commit -m "pass the first stage"
|
||||
git push origin master
|
||||
```
|
||||
|
||||
Time to move on to the next stage!
|
||||
|
||||
# Running Your Program Locally
|
||||
|
||||
1. Ensure you have `cargo (1.43)` installed locally
|
||||
1. Run `./your_sqlite3.sh` to run your program, which is implemented in
|
||||
`src/main.rs`. This command compiles your Rust project, so it might be slow
|
||||
the first time you run it. Subsequent runs will be fast.
|
||||
1. Commit your changes and run `git push origin master` to submit your solution
|
||||
to CodeCrafters. Test output will be streamed to your terminal.
|
||||
|
||||
# Sample Databases
|
||||
|
||||
To make it easy to test queries locally, we've added a sample database in the
|
||||
root of this repository: `sample.db`.
|
||||
|
||||
This contains two tables: `apples` & `oranges`. You can use this to test your
|
||||
implementation for the first 6 stages.
|
||||
|
||||
You can explore this database by running queries against it like this:
|
||||
|
||||
```sh
|
||||
$ sqlite3 sample.db "select id, name from apples"
|
||||
1|Granny Smith
|
||||
2|Fuji
|
||||
3|Honeycrisp
|
||||
4|Golden Delicious
|
||||
```
|
||||
|
||||
There are two other databases that you can use:
|
||||
|
||||
1. `superheroes.db`:
|
||||
- This is a small version of the test database used in the table-scan stage.
|
||||
- It contains one table: `superheroes`.
|
||||
- It is ~1MB in size.
|
||||
1. `companies.db`:
|
||||
- This is a small version of the test database used in the index-scan stage.
|
||||
- It contains one table: `companies`, and one index: `idx_companies_country`
|
||||
- It is ~7MB in size.
|
||||
|
||||
These aren't included in the repository because they're large in size. You can
|
||||
download them by running this script:
|
||||
|
||||
```sh
|
||||
./download_sample_databases.sh
|
||||
```
|
||||
|
||||
If the script doesn't work for some reason, you can download the databases
|
||||
directly from
|
||||
[codecrafters-io/sample-sqlite-databases](https://github.com/codecrafters-io/sample-sqlite-databases).
|
||||
|
|
11
codecrafters.yml
Normal file
11
codecrafters.yml
Normal file
|
@ -0,0 +1,11 @@
|
|||
# Set this to true if you want debug logs.
|
||||
#
|
||||
# These can be VERY verbose, so we suggest turning them off
|
||||
# unless you really need them.
|
||||
debug: false
|
||||
|
||||
# Use this to change the Rust version used to run your code
|
||||
# on Codecrafters.
|
||||
#
|
||||
# Available versions: rust-1.43
|
||||
language_pack: rust-1.43
|
9
download_sample_databases.sh
Executable file
9
download_sample_databases.sh
Executable file
|
@ -0,0 +1,9 @@
|
|||
#!/bin/sh
|
||||
|
||||
echo "Downloading superheroes.db: ~1MB (used in stage 7)"
|
||||
curl -Lo superheroes.db https://raw.githubusercontent.com/codecrafters-io/sample-sqlite-databases/master/superheroes.db
|
||||
|
||||
echo "Downloading companies.db: ~7MB (used in stage 8)"
|
||||
curl -Lo companies.db https://raw.githubusercontent.com/codecrafters-io/sample-sqlite-databases/master/companies.db
|
||||
|
||||
echo "Sample databases downloaded."
|
44
src/header.rs
Normal file
44
src/header.rs
Normal file
|
@ -0,0 +1,44 @@
|
|||
use anyhow::{bail, Result};
|
||||
use std::convert::TryInto;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum BTreePage {
|
||||
InteriorIndex = 2,
|
||||
InteriorTable = 5,
|
||||
LeafIndex = 10,
|
||||
LeafTable = 13,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct PageHeader {
|
||||
pub page_type: BTreePage,
|
||||
pub first_free_block_start: u16,
|
||||
pub number_of_cells: u16,
|
||||
pub start_of_content_area: u16,
|
||||
pub fragmented_free_bytes: u8,
|
||||
}
|
||||
|
||||
impl PageHeader {
|
||||
/// Parses a page header stream into a page header
|
||||
pub fn parse(stream: &[u8]) -> Result<Self> {
|
||||
let page_type = match stream[0] {
|
||||
2 => BTreePage::InteriorIndex,
|
||||
5 => BTreePage::InteriorTable,
|
||||
10 => BTreePage::LeafIndex,
|
||||
13 => BTreePage::LeafTable,
|
||||
x => bail!("Invalid page value encountered: {}", x),
|
||||
};
|
||||
let first_free_block_start = u16::from_be_bytes(stream[1..3].try_into()?);
|
||||
let number_of_cells = u16::from_be_bytes(stream[3..5].try_into()?);
|
||||
let start_of_content_area = u16::from_be_bytes(stream[5..7].try_into()?);
|
||||
let fragmented_free_bytes = stream[7];
|
||||
let header = PageHeader {
|
||||
page_type,
|
||||
first_free_block_start,
|
||||
number_of_cells,
|
||||
start_of_content_area,
|
||||
fragmented_free_bytes,
|
||||
};
|
||||
Ok(header)
|
||||
}
|
||||
}
|
4
src/lib.rs
Normal file
4
src/lib.rs
Normal file
|
@ -0,0 +1,4 @@
|
|||
pub mod header;
|
||||
pub mod record;
|
||||
pub mod schema;
|
||||
pub mod varint;
|
59
src/main.rs
Normal file
59
src/main.rs
Normal file
|
@ -0,0 +1,59 @@
|
|||
use anyhow::{bail, Result};
|
||||
use sqlite_starter_rust::{
|
||||
header::PageHeader, record::parse_record, schema::Schema, varint::parse_varint,
|
||||
};
|
||||
use std::convert::TryInto;
|
||||
use std::fs::File;
|
||||
use std::io::prelude::*;
|
||||
|
||||
fn main() -> Result<()> {
|
||||
// Parse arguments
|
||||
let args = std::env::args().collect::<Vec<_>>();
|
||||
match args.len() {
|
||||
0 | 1 => bail!("Missing <database path> and <command>"),
|
||||
2 => bail!("Missing <command>"),
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// Read database file into database
|
||||
let mut file = File::open(&args[1])?;
|
||||
let mut database = Vec::new();
|
||||
file.read_to_end(&mut database)?;
|
||||
|
||||
// Parse command and act accordingly
|
||||
let command = &args[2];
|
||||
match command.as_str() {
|
||||
".dbinfo" => {
|
||||
// Parse page header from database
|
||||
let page_header = PageHeader::parse(&database[100..108])?;
|
||||
|
||||
// Obtain all cell pointers
|
||||
let cell_pointers = database[108..]
|
||||
.chunks_exact(2)
|
||||
.take(page_header.number_of_cells.into())
|
||||
.map(|bytes| u16::from_be_bytes(bytes.try_into().unwrap()))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// Obtain all records from column 5
|
||||
#[allow(unused_variables)]
|
||||
let schemas = cell_pointers
|
||||
.into_iter()
|
||||
.map(|cell_pointer| {
|
||||
let stream = &database[cell_pointer as usize..];
|
||||
let (_, offset) = parse_varint(stream);
|
||||
let (_rowid, read_bytes) = parse_varint(&stream[offset..]);
|
||||
parse_record(&stream[offset + read_bytes..], 5)
|
||||
.map(|record| Schema::parse(record).expect("Invalid record"))
|
||||
})
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
|
||||
println!("Your code goes here!");
|
||||
|
||||
// Uncomment this block to pass the first stage
|
||||
// println!("number of tables: {}", schemas.len());
|
||||
}
|
||||
_ => bail!("Missing or invalid command passed: {}", command),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
42
src/record.rs
Normal file
42
src/record.rs
Normal file
|
@ -0,0 +1,42 @@
|
|||
use crate::varint::parse_varint;
|
||||
use anyhow::{bail, Result};
|
||||
|
||||
/// Reads SQLite's "Record Format" as mentioned here:
|
||||
/// [record_format](https://www.sqlite.org/fileformat.html#record_format)
|
||||
pub fn parse_record(stream: &[u8], column_count: usize) -> Result<Vec<Vec<u8>>> {
|
||||
// Parse number of bytes in header, and use bytes_read as offset
|
||||
let (_, mut offset) = parse_varint(stream);
|
||||
|
||||
// Read each varint into serial types and modify the offset
|
||||
let mut serial_types = vec![];
|
||||
for _ in 0..column_count {
|
||||
let (varint, read_bytes) = parse_varint(&stream[offset..]);
|
||||
offset += read_bytes;
|
||||
serial_types.push(varint);
|
||||
}
|
||||
|
||||
// Parse each serial type as column into record and modify the offset
|
||||
let mut record = vec![];
|
||||
for serial_type in serial_types {
|
||||
let column = parse_column_value(&stream[offset..], serial_type as u8)?;
|
||||
offset += column.len();
|
||||
record.push(column);
|
||||
}
|
||||
|
||||
Ok(record)
|
||||
}
|
||||
|
||||
fn parse_column_value(stream: &[u8], serial_type: u8) -> Result<Vec<u8>> {
|
||||
let column_value = match serial_type {
|
||||
// 8 bit twos-complement integer
|
||||
1 => vec![stream[0]],
|
||||
// Text encoding
|
||||
n if serial_type >= 13 && serial_type % 2 == 1 => {
|
||||
let n_bytes = (n - 13) / 2;
|
||||
let bytes = stream[0..n_bytes as usize].to_vec();
|
||||
bytes
|
||||
}
|
||||
_ => bail!("Invalid serial_type: {}", serial_type),
|
||||
};
|
||||
Ok(column_value)
|
||||
}
|
29
src/schema.rs
Normal file
29
src/schema.rs
Normal file
|
@ -0,0 +1,29 @@
|
|||
#[derive(Debug)]
|
||||
pub struct Schema {
|
||||
kind: String,
|
||||
name: String,
|
||||
table_name: String,
|
||||
root_page: u8,
|
||||
sql: String,
|
||||
}
|
||||
|
||||
impl Schema {
|
||||
/// Parses a record into a schema
|
||||
pub fn parse(record: Vec<Vec<u8>>) -> Option<Self> {
|
||||
let mut items = record.into_iter();
|
||||
let kind = items.next()?;
|
||||
let name = items.next()?;
|
||||
let table_name = items.next()?;
|
||||
let root_page = *items.next()?.get(0)?;
|
||||
let sql = items.next()?;
|
||||
|
||||
let schema = Self {
|
||||
kind: String::from_utf8_lossy(&kind).to_string(),
|
||||
name: String::from_utf8_lossy(&name).to_string(),
|
||||
table_name: String::from_utf8_lossy(&table_name).to_string(),
|
||||
root_page,
|
||||
sql: String::from_utf8_lossy(&sql).to_string(),
|
||||
};
|
||||
Some(schema)
|
||||
}
|
||||
}
|
46
src/varint.rs
Normal file
46
src/varint.rs
Normal file
|
@ -0,0 +1,46 @@
|
|||
const IS_FIRST_BIT_ZERO_MASK: u8 = 0b10000000;
|
||||
const LAST_SEVEN_BITS_MASK: u8 = 0b01111111;
|
||||
|
||||
/// Parses SQLite's "varint" (short for variable-length integer) as mentioned here:
|
||||
/// [varint](https://www.sqlite.org/fileformat2.html#varint)
|
||||
///
|
||||
/// Returns (varint, bytes_read)
|
||||
pub fn parse_varint(stream: &[u8]) -> (usize, usize) {
|
||||
let usable_bytes = read_usable_bytes(stream);
|
||||
let bytes_read = usable_bytes.len();
|
||||
let varint = usable_bytes
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.fold(0, |value, (i, usable_byte)| {
|
||||
let usable_size = if i == 8 { 8 } else { 7 };
|
||||
(value << usable_size) + usable_value(usable_size, usable_byte) as usize
|
||||
});
|
||||
(varint, bytes_read)
|
||||
}
|
||||
|
||||
/// Usable size is either 8 or 7
|
||||
fn usable_value(usable_size: u8, byte: u8) -> u8 {
|
||||
if usable_size == 8 {
|
||||
usable_size
|
||||
} else {
|
||||
byte & LAST_SEVEN_BITS_MASK
|
||||
}
|
||||
}
|
||||
|
||||
fn read_usable_bytes(stream: &[u8]) -> Vec<u8> {
|
||||
let mut usable_bytes = vec![];
|
||||
|
||||
for i in 0..8 {
|
||||
let byte = stream[i];
|
||||
usable_bytes.push(byte);
|
||||
if starts_with_zero(byte) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
usable_bytes
|
||||
}
|
||||
|
||||
fn starts_with_zero(byte: u8) -> bool {
|
||||
(byte & IS_FIRST_BIT_ZERO_MASK) == 0
|
||||
}
|
12
your_sqlite3.sh
Executable file
12
your_sqlite3.sh
Executable file
|
@ -0,0 +1,12 @@
|
|||
#!/bin/sh
|
||||
#
|
||||
# DON'T EDIT THIS!
|
||||
#
|
||||
# CodeCrafters uses this file to test your code. Don't make any changes here!
|
||||
#
|
||||
# DON'T EDIT THIS!
|
||||
exec cargo run \
|
||||
--quiet \
|
||||
--release \
|
||||
--target-dir=/tmp/codecrafters-sqlite-target \
|
||||
--manifest-path "$(dirname "$0")/Cargo.toml" "$@"
|
Loading…
Reference in New Issue
Block a user