├── .github └── workflows │ └── rust.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── MAINTAINERS ├── Makefile ├── README.md ├── images ├── SQLRite Data Structures.png ├── SQLRite Simple SQL Execution High Level Diagram.png ├── SQLRite Simple SQL INSERT Execution High Level Diagram (Insert Row).png ├── SQLRite Simple SQL INSERT Execution High Level Diagram.png ├── SQLRite_logo.png └── architecture.png ├── samples ├── AST.delete.example ├── AST.insert.exemple ├── AST.select.example ├── AST.update.example ├── CREATE TABLE sqlrite_schema.sql ├── CREATE_TABLE with duplicate.sql ├── CREATE_TABLE.sql └── INSERT.sql └── src ├── error.rs ├── main.rs ├── meta_command └── mod.rs ├── repl └── mod.rs └── sql ├── db ├── database.rs ├── mod.rs └── table.rs ├── mod.rs ├── parser ├── create.rs ├── insert.rs └── mod.rs └── tokenizer.rs /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - name: Install Tarpaulin 19 | uses: actions-rs/install@v0.1 20 | with: 21 | crate: cargo-tarpaulin 22 | version: 0.14.2 23 | use-tool-cache: true 24 | 25 | - uses: actions/checkout@v2 26 | 27 | - name: Build 28 | run: cargo build --verbose 29 | 30 | - name: Run tests 31 | run: cargo test --verbose 32 | 33 | - name: Coverage 34 | run: cargo tarpaulin -o Lcov --output-dir ./coverage 35 | 36 | - name: Coveralls 37 | uses: coverallsapp/github-action@master 38 | with: 39 | github-token: ${{ secrets.GITHUB_TOKEN }} 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | .swp 3 | **/.swp 4 | *.swp 5 | history -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at joaoh82@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "SQLRite" 7 | version = "0.1.0" 8 | dependencies = [ 9 | "clap", 10 | "env_logger", 11 | "log", 12 | "prettytable-rs", 13 | "rustyline", 14 | "rustyline-derive", 15 | "serde", 16 | "sqlparser", 17 | "thiserror", 18 | ] 19 | 20 | [[package]] 21 | name = "aho-corasick" 22 | version = "0.7.18" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" 25 | dependencies = [ 26 | "memchr", 27 | ] 28 | 29 | [[package]] 30 | name = "arrayref" 31 | version = "0.3.6" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544" 34 | 35 | [[package]] 36 | name = "arrayvec" 37 | version = "0.5.2" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" 40 | 41 | [[package]] 42 | name = "atty" 43 | version = "0.2.14" 44 | source = "registry+https://github.com/rust-lang/crates.io-index" 45 | checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" 46 | dependencies = [ 47 | "hermit-abi", 48 | "libc", 49 | "winapi", 50 | ] 51 | 52 | [[package]] 53 | name = "autocfg" 54 | version = "1.1.0" 55 | source = "registry+https://github.com/rust-lang/crates.io-index" 56 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 57 | 58 | [[package]] 59 | name = "base64" 60 | version = "0.13.0" 61 | source = "registry+https://github.com/rust-lang/crates.io-index" 62 | checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" 63 | 64 | [[package]] 65 | name = "bitflags" 66 | version = "1.3.2" 67 | source = "registry+https://github.com/rust-lang/crates.io-index" 68 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 69 | 70 | [[package]] 71 | name = "blake2b_simd" 72 | version = "0.5.11" 73 | source = "registry+https://github.com/rust-lang/crates.io-index" 74 | checksum = "afa748e348ad3be8263be728124b24a24f268266f6f5d58af9d75f6a40b5c587" 75 | dependencies = [ 76 | "arrayref", 77 | "arrayvec", 78 | "constant_time_eq", 79 | ] 80 | 81 | [[package]] 82 | name = "bstr" 83 | version = "0.2.17" 84 | source = "registry+https://github.com/rust-lang/crates.io-index" 85 | checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223" 86 | dependencies = [ 87 | "lazy_static", 88 | "memchr", 89 | "regex-automata", 90 | "serde", 91 | ] 92 | 93 | [[package]] 94 | name = "byteorder" 95 | version = "1.4.3" 96 | source = "registry+https://github.com/rust-lang/crates.io-index" 97 | checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" 98 | 99 | [[package]] 100 | name = "cc" 101 | version = "1.0.73" 102 | source = "registry+https://github.com/rust-lang/crates.io-index" 103 | checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" 104 | 105 | [[package]] 106 | name = "cfg-if" 107 | version = "1.0.0" 108 | source = "registry+https://github.com/rust-lang/crates.io-index" 109 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 110 | 111 | [[package]] 112 | name = "clap" 113 | version = "3.1.18" 114 | source = "registry+https://github.com/rust-lang/crates.io-index" 115 | checksum = "d2dbdf4bdacb33466e854ce889eee8dfd5729abf7ccd7664d0a2d60cd384440b" 116 | dependencies = [ 117 | "atty", 118 | "bitflags", 119 | "clap_lex", 120 | "indexmap", 121 | "lazy_static", 122 | "strsim", 123 | "termcolor", 124 | "textwrap", 125 | ] 126 | 127 | [[package]] 128 | name = "clap_lex" 129 | version = "0.2.0" 130 | source = "registry+https://github.com/rust-lang/crates.io-index" 131 | checksum = "a37c35f1112dad5e6e0b1adaff798507497a18fceeb30cceb3bae7d1427b9213" 132 | dependencies = [ 133 | "os_str_bytes", 134 | ] 135 | 136 | [[package]] 137 | name = "clipboard-win" 138 | version = "4.4.1" 139 | source = "registry+https://github.com/rust-lang/crates.io-index" 140 | checksum = "2f3e1238132dc01f081e1cbb9dace14e5ef4c3a51ee244bd982275fb514605db" 141 | dependencies = [ 142 | "error-code", 143 | "str-buf", 144 | "winapi", 145 | ] 146 | 147 | [[package]] 148 | name = "constant_time_eq" 149 | version = "0.1.5" 150 | source = "registry+https://github.com/rust-lang/crates.io-index" 151 | checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" 152 | 153 | [[package]] 154 | name = "crossbeam-utils" 155 | version = "0.8.8" 156 | source = "registry+https://github.com/rust-lang/crates.io-index" 157 | checksum = "0bf124c720b7686e3c2663cf54062ab0f68a88af2fb6a030e87e30bf721fcb38" 158 | dependencies = [ 159 | "cfg-if", 160 | "lazy_static", 161 | ] 162 | 163 | [[package]] 164 | name = "csv" 165 | version = "1.1.6" 166 | source = "registry+https://github.com/rust-lang/crates.io-index" 167 | checksum = "22813a6dc45b335f9bade10bf7271dc477e81113e89eb251a0bc2a8a81c536e1" 168 | dependencies = [ 169 | "bstr", 170 | "csv-core", 171 | "itoa", 172 | "ryu", 173 | "serde", 174 | ] 175 | 176 | [[package]] 177 | name = "csv-core" 178 | version = "0.1.10" 179 | source = "registry+https://github.com/rust-lang/crates.io-index" 180 | checksum = "2b2466559f260f48ad25fe6317b3c8dac77b5bdb5763ac7d9d6103530663bc90" 181 | dependencies = [ 182 | "memchr", 183 | ] 184 | 185 | [[package]] 186 | name = "dirs" 187 | version = "1.0.5" 188 | source = "registry+https://github.com/rust-lang/crates.io-index" 189 | checksum = "3fd78930633bd1c6e35c4b42b1df7b0cbc6bc191146e512bb3bedf243fcc3901" 190 | dependencies = [ 191 | "libc", 192 | "redox_users 0.3.5", 193 | "winapi", 194 | ] 195 | 196 | [[package]] 197 | name = "dirs-next" 198 | version = "2.0.0" 199 | source = "registry+https://github.com/rust-lang/crates.io-index" 200 | checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" 201 | dependencies = [ 202 | "cfg-if", 203 | "dirs-sys-next", 204 | ] 205 | 206 | [[package]] 207 | name = "dirs-sys-next" 208 | version = "0.1.2" 209 | source = "registry+https://github.com/rust-lang/crates.io-index" 210 | checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" 211 | dependencies = [ 212 | "libc", 213 | "redox_users 0.4.3", 214 | "winapi", 215 | ] 216 | 217 | [[package]] 218 | name = "encode_unicode" 219 | version = "0.3.6" 220 | source = "registry+https://github.com/rust-lang/crates.io-index" 221 | checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" 222 | 223 | [[package]] 224 | name = "endian-type" 225 | version = "0.1.2" 226 | source = "registry+https://github.com/rust-lang/crates.io-index" 227 | checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d" 228 | 229 | [[package]] 230 | name = "env_logger" 231 | version = "0.9.0" 232 | source = "registry+https://github.com/rust-lang/crates.io-index" 233 | checksum = "0b2cf0344971ee6c64c31be0d530793fba457d322dfec2810c453d0ef228f9c3" 234 | dependencies = [ 235 | "atty", 236 | "humantime", 237 | "log", 238 | "regex", 239 | "termcolor", 240 | ] 241 | 242 | [[package]] 243 | name = "errno" 244 | version = "0.2.8" 245 | source = "registry+https://github.com/rust-lang/crates.io-index" 246 | checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1" 247 | dependencies = [ 248 | "errno-dragonfly", 249 | "libc", 250 | "winapi", 251 | ] 252 | 253 | [[package]] 254 | name = "errno-dragonfly" 255 | version = "0.1.2" 256 | source = "registry+https://github.com/rust-lang/crates.io-index" 257 | checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" 258 | dependencies = [ 259 | "cc", 260 | "libc", 261 | ] 262 | 263 | [[package]] 264 | name = "error-code" 265 | version = "2.3.1" 266 | source = "registry+https://github.com/rust-lang/crates.io-index" 267 | checksum = "64f18991e7bf11e7ffee451b5318b5c1a73c52d0d0ada6e5a3017c8c1ced6a21" 268 | dependencies = [ 269 | "libc", 270 | "str-buf", 271 | ] 272 | 273 | [[package]] 274 | name = "fd-lock" 275 | version = "3.0.5" 276 | source = "registry+https://github.com/rust-lang/crates.io-index" 277 | checksum = "46e245f4c8ec30c6415c56cb132c07e69e74f1942f6b4a4061da748b49f486ca" 278 | dependencies = [ 279 | "cfg-if", 280 | "rustix", 281 | "windows-sys", 282 | ] 283 | 284 | [[package]] 285 | name = "getrandom" 286 | version = "0.1.16" 287 | source = "registry+https://github.com/rust-lang/crates.io-index" 288 | checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" 289 | dependencies = [ 290 | "cfg-if", 291 | "libc", 292 | "wasi 0.9.0+wasi-snapshot-preview1", 293 | ] 294 | 295 | [[package]] 296 | name = "getrandom" 297 | version = "0.2.6" 298 | source = "registry+https://github.com/rust-lang/crates.io-index" 299 | checksum = "9be70c98951c83b8d2f8f60d7065fa6d5146873094452a1008da8c2f1e4205ad" 300 | dependencies = [ 301 | "cfg-if", 302 | "libc", 303 | "wasi 0.10.2+wasi-snapshot-preview1", 304 | ] 305 | 306 | [[package]] 307 | name = "hashbrown" 308 | version = "0.11.2" 309 | source = "registry+https://github.com/rust-lang/crates.io-index" 310 | checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" 311 | 312 | [[package]] 313 | name = "hermit-abi" 314 | version = "0.1.19" 315 | source = "registry+https://github.com/rust-lang/crates.io-index" 316 | checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" 317 | dependencies = [ 318 | "libc", 319 | ] 320 | 321 | [[package]] 322 | name = "humantime" 323 | version = "2.1.0" 324 | source = "registry+https://github.com/rust-lang/crates.io-index" 325 | checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" 326 | 327 | [[package]] 328 | name = "indexmap" 329 | version = "1.8.1" 330 | source = "registry+https://github.com/rust-lang/crates.io-index" 331 | checksum = "0f647032dfaa1f8b6dc29bd3edb7bbef4861b8b8007ebb118d6db284fd59f6ee" 332 | dependencies = [ 333 | "autocfg", 334 | "hashbrown", 335 | ] 336 | 337 | [[package]] 338 | name = "io-lifetimes" 339 | version = "0.6.1" 340 | source = "registry+https://github.com/rust-lang/crates.io-index" 341 | checksum = "9448015e586b611e5d322f6703812bbca2f1e709d5773ecd38ddb4e3bb649504" 342 | 343 | [[package]] 344 | name = "itoa" 345 | version = "0.4.8" 346 | source = "registry+https://github.com/rust-lang/crates.io-index" 347 | checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" 348 | 349 | [[package]] 350 | name = "lazy_static" 351 | version = "1.4.0" 352 | source = "registry+https://github.com/rust-lang/crates.io-index" 353 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 354 | 355 | [[package]] 356 | name = "libc" 357 | version = "0.2.125" 358 | source = "registry+https://github.com/rust-lang/crates.io-index" 359 | checksum = "5916d2ae698f6de9bfb891ad7a8d65c09d232dc58cc4ac433c7da3b2fd84bc2b" 360 | 361 | [[package]] 362 | name = "linux-raw-sys" 363 | version = "0.0.46" 364 | source = "registry+https://github.com/rust-lang/crates.io-index" 365 | checksum = "d4d2456c373231a208ad294c33dc5bff30051eafd954cd4caae83a712b12854d" 366 | 367 | [[package]] 368 | name = "log" 369 | version = "0.4.17" 370 | source = "registry+https://github.com/rust-lang/crates.io-index" 371 | checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" 372 | dependencies = [ 373 | "cfg-if", 374 | ] 375 | 376 | [[package]] 377 | name = "memchr" 378 | version = "2.5.0" 379 | source = "registry+https://github.com/rust-lang/crates.io-index" 380 | checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" 381 | 382 | [[package]] 383 | name = "memoffset" 384 | version = "0.6.5" 385 | source = "registry+https://github.com/rust-lang/crates.io-index" 386 | checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" 387 | dependencies = [ 388 | "autocfg", 389 | ] 390 | 391 | [[package]] 392 | name = "nibble_vec" 393 | version = "0.1.0" 394 | source = "registry+https://github.com/rust-lang/crates.io-index" 395 | checksum = "77a5d83df9f36fe23f0c3648c6bbb8b0298bb5f1939c8f2704431371f4b84d43" 396 | dependencies = [ 397 | "smallvec", 398 | ] 399 | 400 | [[package]] 401 | name = "nix" 402 | version = "0.23.1" 403 | source = "registry+https://github.com/rust-lang/crates.io-index" 404 | checksum = "9f866317acbd3a240710c63f065ffb1e4fd466259045ccb504130b7f668f35c6" 405 | dependencies = [ 406 | "bitflags", 407 | "cc", 408 | "cfg-if", 409 | "libc", 410 | "memoffset", 411 | ] 412 | 413 | [[package]] 414 | name = "os_str_bytes" 415 | version = "6.0.0" 416 | source = "registry+https://github.com/rust-lang/crates.io-index" 417 | checksum = "8e22443d1643a904602595ba1cd8f7d896afe56d26712531c5ff73a15b2fbf64" 418 | 419 | [[package]] 420 | name = "prettytable-rs" 421 | version = "0.8.0" 422 | source = "registry+https://github.com/rust-lang/crates.io-index" 423 | checksum = "0fd04b170004fa2daccf418a7f8253aaf033c27760b5f225889024cf66d7ac2e" 424 | dependencies = [ 425 | "atty", 426 | "csv", 427 | "encode_unicode", 428 | "lazy_static", 429 | "term", 430 | "unicode-width", 431 | ] 432 | 433 | [[package]] 434 | name = "proc-macro2" 435 | version = "1.0.38" 436 | source = "registry+https://github.com/rust-lang/crates.io-index" 437 | checksum = "9027b48e9d4c9175fa2218adf3557f91c1137021739951d4932f5f8268ac48aa" 438 | dependencies = [ 439 | "unicode-xid", 440 | ] 441 | 442 | [[package]] 443 | name = "quote" 444 | version = "1.0.18" 445 | source = "registry+https://github.com/rust-lang/crates.io-index" 446 | checksum = "a1feb54ed693b93a84e14094943b84b7c4eae204c512b7ccb95ab0c66d278ad1" 447 | dependencies = [ 448 | "proc-macro2", 449 | ] 450 | 451 | [[package]] 452 | name = "radix_trie" 453 | version = "0.2.1" 454 | source = "registry+https://github.com/rust-lang/crates.io-index" 455 | checksum = "c069c179fcdc6a2fe24d8d18305cf085fdbd4f922c041943e203685d6a1c58fd" 456 | dependencies = [ 457 | "endian-type", 458 | "nibble_vec", 459 | ] 460 | 461 | [[package]] 462 | name = "redox_syscall" 463 | version = "0.1.57" 464 | source = "registry+https://github.com/rust-lang/crates.io-index" 465 | checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce" 466 | 467 | [[package]] 468 | name = "redox_syscall" 469 | version = "0.2.13" 470 | source = "registry+https://github.com/rust-lang/crates.io-index" 471 | checksum = "62f25bc4c7e55e0b0b7a1d43fb893f4fa1361d0abe38b9ce4f323c2adfe6ef42" 472 | dependencies = [ 473 | "bitflags", 474 | ] 475 | 476 | [[package]] 477 | name = "redox_users" 478 | version = "0.3.5" 479 | source = "registry+https://github.com/rust-lang/crates.io-index" 480 | checksum = "de0737333e7a9502c789a36d7c7fa6092a49895d4faa31ca5df163857ded2e9d" 481 | dependencies = [ 482 | "getrandom 0.1.16", 483 | "redox_syscall 0.1.57", 484 | "rust-argon2", 485 | ] 486 | 487 | [[package]] 488 | name = "redox_users" 489 | version = "0.4.3" 490 | source = "registry+https://github.com/rust-lang/crates.io-index" 491 | checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" 492 | dependencies = [ 493 | "getrandom 0.2.6", 494 | "redox_syscall 0.2.13", 495 | "thiserror", 496 | ] 497 | 498 | [[package]] 499 | name = "regex" 500 | version = "1.5.5" 501 | source = "registry+https://github.com/rust-lang/crates.io-index" 502 | checksum = "1a11647b6b25ff05a515cb92c365cec08801e83423a235b51e231e1808747286" 503 | dependencies = [ 504 | "aho-corasick", 505 | "memchr", 506 | "regex-syntax", 507 | ] 508 | 509 | [[package]] 510 | name = "regex-automata" 511 | version = "0.1.10" 512 | source = "registry+https://github.com/rust-lang/crates.io-index" 513 | checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" 514 | 515 | [[package]] 516 | name = "regex-syntax" 517 | version = "0.6.25" 518 | source = "registry+https://github.com/rust-lang/crates.io-index" 519 | checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" 520 | 521 | [[package]] 522 | name = "rust-argon2" 523 | version = "0.8.3" 524 | source = "registry+https://github.com/rust-lang/crates.io-index" 525 | checksum = "4b18820d944b33caa75a71378964ac46f58517c92b6ae5f762636247c09e78fb" 526 | dependencies = [ 527 | "base64", 528 | "blake2b_simd", 529 | "constant_time_eq", 530 | "crossbeam-utils", 531 | ] 532 | 533 | [[package]] 534 | name = "rustix" 535 | version = "0.34.6" 536 | source = "registry+https://github.com/rust-lang/crates.io-index" 537 | checksum = "f3e74b3f02f2b6eb33790923756784614f456de79d821d6b2670dc7d5fbea807" 538 | dependencies = [ 539 | "bitflags", 540 | "errno", 541 | "io-lifetimes", 542 | "libc", 543 | "linux-raw-sys", 544 | "winapi", 545 | ] 546 | 547 | [[package]] 548 | name = "rustyline" 549 | version = "9.1.2" 550 | source = "registry+https://github.com/rust-lang/crates.io-index" 551 | checksum = "db7826789c0e25614b03e5a54a0717a86f9ff6e6e5247f92b369472869320039" 552 | dependencies = [ 553 | "bitflags", 554 | "cfg-if", 555 | "clipboard-win", 556 | "dirs-next", 557 | "fd-lock", 558 | "libc", 559 | "log", 560 | "memchr", 561 | "nix", 562 | "radix_trie", 563 | "scopeguard", 564 | "smallvec", 565 | "unicode-segmentation", 566 | "unicode-width", 567 | "utf8parse", 568 | "winapi", 569 | ] 570 | 571 | [[package]] 572 | name = "rustyline-derive" 573 | version = "0.6.0" 574 | source = "registry+https://github.com/rust-lang/crates.io-index" 575 | checksum = "bb35a55ab810b5c0fe31606fe9b47d1354e4dc519bec0a102655f78ea2b38057" 576 | dependencies = [ 577 | "quote", 578 | "syn", 579 | ] 580 | 581 | [[package]] 582 | name = "ryu" 583 | version = "1.0.9" 584 | source = "registry+https://github.com/rust-lang/crates.io-index" 585 | checksum = "73b4b750c782965c211b42f022f59af1fbceabdd026623714f104152f1ec149f" 586 | 587 | [[package]] 588 | name = "scopeguard" 589 | version = "1.1.0" 590 | source = "registry+https://github.com/rust-lang/crates.io-index" 591 | checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" 592 | 593 | [[package]] 594 | name = "serde" 595 | version = "1.0.137" 596 | source = "registry+https://github.com/rust-lang/crates.io-index" 597 | checksum = "61ea8d54c77f8315140a05f4c7237403bf38b72704d031543aa1d16abbf517d1" 598 | dependencies = [ 599 | "serde_derive", 600 | ] 601 | 602 | [[package]] 603 | name = "serde_derive" 604 | version = "1.0.137" 605 | source = "registry+https://github.com/rust-lang/crates.io-index" 606 | checksum = "1f26faba0c3959972377d3b2d306ee9f71faee9714294e41bb777f83f88578be" 607 | dependencies = [ 608 | "proc-macro2", 609 | "quote", 610 | "syn", 611 | ] 612 | 613 | [[package]] 614 | name = "smallvec" 615 | version = "1.8.0" 616 | source = "registry+https://github.com/rust-lang/crates.io-index" 617 | checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83" 618 | 619 | [[package]] 620 | name = "sqlparser" 621 | version = "0.17.0" 622 | source = "registry+https://github.com/rust-lang/crates.io-index" 623 | checksum = "ddc2739f3a9bfc68e2f7b7695589f6cb0181c88af73ceaee0c84215cd2a2ae28" 624 | dependencies = [ 625 | "log", 626 | ] 627 | 628 | [[package]] 629 | name = "str-buf" 630 | version = "1.0.5" 631 | source = "registry+https://github.com/rust-lang/crates.io-index" 632 | checksum = "d44a3643b4ff9caf57abcee9c2c621d6c03d9135e0d8b589bd9afb5992cb176a" 633 | 634 | [[package]] 635 | name = "strsim" 636 | version = "0.10.0" 637 | source = "registry+https://github.com/rust-lang/crates.io-index" 638 | checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" 639 | 640 | [[package]] 641 | name = "syn" 642 | version = "1.0.94" 643 | source = "registry+https://github.com/rust-lang/crates.io-index" 644 | checksum = "a07e33e919ebcd69113d5be0e4d70c5707004ff45188910106854f38b960df4a" 645 | dependencies = [ 646 | "proc-macro2", 647 | "quote", 648 | "unicode-xid", 649 | ] 650 | 651 | [[package]] 652 | name = "term" 653 | version = "0.5.2" 654 | source = "registry+https://github.com/rust-lang/crates.io-index" 655 | checksum = "edd106a334b7657c10b7c540a0106114feadeb4dc314513e97df481d5d966f42" 656 | dependencies = [ 657 | "byteorder", 658 | "dirs", 659 | "winapi", 660 | ] 661 | 662 | [[package]] 663 | name = "termcolor" 664 | version = "1.1.3" 665 | source = "registry+https://github.com/rust-lang/crates.io-index" 666 | checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" 667 | dependencies = [ 668 | "winapi-util", 669 | ] 670 | 671 | [[package]] 672 | name = "textwrap" 673 | version = "0.15.0" 674 | source = "registry+https://github.com/rust-lang/crates.io-index" 675 | checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb" 676 | 677 | [[package]] 678 | name = "thiserror" 679 | version = "1.0.31" 680 | source = "registry+https://github.com/rust-lang/crates.io-index" 681 | checksum = "bd829fe32373d27f76265620b5309d0340cb8550f523c1dda251d6298069069a" 682 | dependencies = [ 683 | "thiserror-impl", 684 | ] 685 | 686 | [[package]] 687 | name = "thiserror-impl" 688 | version = "1.0.31" 689 | source = "registry+https://github.com/rust-lang/crates.io-index" 690 | checksum = "0396bc89e626244658bef819e22d0cc459e795a5ebe878e6ec336d1674a8d79a" 691 | dependencies = [ 692 | "proc-macro2", 693 | "quote", 694 | "syn", 695 | ] 696 | 697 | [[package]] 698 | name = "unicode-segmentation" 699 | version = "1.9.0" 700 | source = "registry+https://github.com/rust-lang/crates.io-index" 701 | checksum = "7e8820f5d777f6224dc4be3632222971ac30164d4a258d595640799554ebfd99" 702 | 703 | [[package]] 704 | name = "unicode-width" 705 | version = "0.1.9" 706 | source = "registry+https://github.com/rust-lang/crates.io-index" 707 | checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" 708 | 709 | [[package]] 710 | name = "unicode-xid" 711 | version = "0.2.3" 712 | source = "registry+https://github.com/rust-lang/crates.io-index" 713 | checksum = "957e51f3646910546462e67d5f7599b9e4fb8acdd304b087a6494730f9eebf04" 714 | 715 | [[package]] 716 | name = "utf8parse" 717 | version = "0.2.0" 718 | source = "registry+https://github.com/rust-lang/crates.io-index" 719 | checksum = "936e4b492acfd135421d8dca4b1aa80a7bfc26e702ef3af710e0752684df5372" 720 | 721 | [[package]] 722 | name = "wasi" 723 | version = "0.9.0+wasi-snapshot-preview1" 724 | source = "registry+https://github.com/rust-lang/crates.io-index" 725 | checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" 726 | 727 | [[package]] 728 | name = "wasi" 729 | version = "0.10.2+wasi-snapshot-preview1" 730 | source = "registry+https://github.com/rust-lang/crates.io-index" 731 | checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" 732 | 733 | [[package]] 734 | name = "winapi" 735 | version = "0.3.9" 736 | source = "registry+https://github.com/rust-lang/crates.io-index" 737 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 738 | dependencies = [ 739 | "winapi-i686-pc-windows-gnu", 740 | "winapi-x86_64-pc-windows-gnu", 741 | ] 742 | 743 | [[package]] 744 | name = "winapi-i686-pc-windows-gnu" 745 | version = "0.4.0" 746 | source = "registry+https://github.com/rust-lang/crates.io-index" 747 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 748 | 749 | [[package]] 750 | name = "winapi-util" 751 | version = "0.1.5" 752 | source = "registry+https://github.com/rust-lang/crates.io-index" 753 | checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" 754 | dependencies = [ 755 | "winapi", 756 | ] 757 | 758 | [[package]] 759 | name = "winapi-x86_64-pc-windows-gnu" 760 | version = "0.4.0" 761 | source = "registry+https://github.com/rust-lang/crates.io-index" 762 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 763 | 764 | [[package]] 765 | name = "windows-sys" 766 | version = "0.30.0" 767 | source = "registry+https://github.com/rust-lang/crates.io-index" 768 | checksum = "030b7ff91626e57a05ca64a07c481973cbb2db774e4852c9c7ca342408c6a99a" 769 | dependencies = [ 770 | "windows_aarch64_msvc", 771 | "windows_i686_gnu", 772 | "windows_i686_msvc", 773 | "windows_x86_64_gnu", 774 | "windows_x86_64_msvc", 775 | ] 776 | 777 | [[package]] 778 | name = "windows_aarch64_msvc" 779 | version = "0.30.0" 780 | source = "registry+https://github.com/rust-lang/crates.io-index" 781 | checksum = "29277a4435d642f775f63c7d1faeb927adba532886ce0287bd985bffb16b6bca" 782 | 783 | [[package]] 784 | name = "windows_i686_gnu" 785 | version = "0.30.0" 786 | source = "registry+https://github.com/rust-lang/crates.io-index" 787 | checksum = "1145e1989da93956c68d1864f32fb97c8f561a8f89a5125f6a2b7ea75524e4b8" 788 | 789 | [[package]] 790 | name = "windows_i686_msvc" 791 | version = "0.30.0" 792 | source = "registry+https://github.com/rust-lang/crates.io-index" 793 | checksum = "d4a09e3a0d4753b73019db171c1339cd4362c8c44baf1bcea336235e955954a6" 794 | 795 | [[package]] 796 | name = "windows_x86_64_gnu" 797 | version = "0.30.0" 798 | source = "registry+https://github.com/rust-lang/crates.io-index" 799 | checksum = "8ca64fcb0220d58db4c119e050e7af03c69e6f4f415ef69ec1773d9aab422d5a" 800 | 801 | [[package]] 802 | name = "windows_x86_64_msvc" 803 | version = "0.30.0" 804 | source = "registry+https://github.com/rust-lang/crates.io-index" 805 | checksum = "08cabc9f0066848fef4bc6a1c1668e6efce38b661d2aeec75d18d8617eebb5f1" 806 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "SQLRite" 3 | version = "0.1.0" 4 | authors = ["Joao Henrique Machado Silva "] 5 | edition = "2018" 6 | description = "Light version of SQLite developed with Rust" 7 | repository = "https://github.com/joaoh82/rust_sqlite" 8 | license = "MIT" 9 | 10 | 11 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 12 | 13 | [dependencies] 14 | rustyline = "9.1.2" 15 | log = "0.4.17" 16 | env_logger = "0.9.0" 17 | rustyline-derive = "0.6.0" 18 | clap = { version = "3.1.18", features = ["cargo"] } 19 | sqlparser = "0.17.0" 20 | thiserror = "1.0.31" 21 | serde = { version = "1.0.137", features = ["derive", "rc"] } 22 | prettytable-rs = "0.8.0" 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 João Henrique Machado Silva 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MAINTAINERS: -------------------------------------------------------------------------------- 1 | João Henrique Machado Silva 2 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL:=/bin/bash 2 | 3 | # COLORS 4 | GREEN := $(shell tput -Txterm setaf 2) 5 | YELLOW := $(shell tput -Txterm setaf 3) 6 | WHITE := $(shell tput -Txterm setaf 7) 7 | RESET := $(shell tput -Txterm sgr0) 8 | 9 | VERSION=production 10 | COMMIT=$(shell git rev-parse HEAD) 11 | GITDIRTY=$(shell git diff --quiet || echo 'dirty') 12 | 13 | GIT_BRANCH := $(shell git rev-parse --abbrev-ref HEAD) 14 | 15 | TARGET_MAX_CHAR_NUM=25 16 | ## Show help 17 | help: 18 | @echo '' 19 | @echo 'Usage:' 20 | @echo ' ${YELLOW}make${RESET} ${GREEN}${RESET}' 21 | @echo '' 22 | @echo 'Targets:' 23 | @awk '/^[a-zA-Z\-\_0-9]+:/ { \ 24 | helpMessage = match(lastLine, /^## (.*)/); \ 25 | if (helpMessage) { \ 26 | helpCommand = substr($$1, 0, index($$1, ":")-1); \ 27 | helpMessage = substr(lastLine, RSTART + 3, RLENGTH); \ 28 | printf " ${YELLOW}%-$(TARGET_MAX_CHAR_NUM)s${RESET} ${GREEN}%s${RESET}\n", helpCommand, helpMessage; \ 29 | } \ 30 | } \ 31 | { lastLine = $$0 }' $(MAKEFILE_LIST) 32 | 33 | .PHONY: code-lines 34 | ## Count number of lines of code in the repository 35 | code-lines: 36 | @echo '${GREEN}Counting${RESET} ${YELLOW}number of lines ${RESET} of code' 37 | @git ls-files | xargs wc -l 38 | 39 | .PHONY: generate-docs 40 | ## Generate Rust Docs based on comments 41 | generate-docs: 42 | @echo '${GREEN}Generating${RESET} ${YELLOW}documentation ${RESET} for SQLRite' 43 | @cargo doc --open 44 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Rust-SQLite (SQLRite) 2 | === 3 | [![Build Status](https://github.com/joaoh82/rust_sqlite/workflows/Rust/badge.svg)](https://github.com/joaoh82/rust_sqlite/actions) 4 | [![dependency status](https://deps.rs/repo/github/joaoh82/rust_sqlite/status.svg)](https://deps.rs/repo/github/joaoh82/rust_sqlite) 5 | [![Coverage Status](https://coveralls.io/repos/github/joaoh82/rust_sqlite/badge.svg?branch=main)](https://coveralls.io/github/joaoh82/rust_sqlite?branch=main) 6 | [![Maintenance](https://img.shields.io/badge/maintenance-actively%20maintained-brightgreen.svg)](https://deps.rs/repo/github/joaoh82/rust_sqlite) 7 | [![MIT licensed](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE) 8 | 9 | `Rust-SQLite`, aka `SQLRite` , is a simple embedded database modeled off `SQLite`, but developed with `Rust`. The goal is get a better understanding of database internals by building one. 10 | 11 | > What I cannot create, I do not understand. 12 | > — Richard Feynman 13 | 14 | 15 | 16 | 17 | 42 | 47 | 48 |
18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 39 | 40 |
key value
Design and discussions about direction
of the project going on over here.
Show us your support by buying us a coffee,
so we can keep building cool stuff. (coming soon)
Documentation (coming soon)
Come and Chat about databases with us 38 | sqlritedb discord server
41 |
43 |

44 | 45 |

46 |
49 | 50 | ### Read the series of posts about it: 51 | ##### What would SQLite look like if written in Rust? 52 | * [Part 0 - Overview](https://medium.com/the-polyglot-programmer/what-would-sqlite-would-look-like-if-written-in-rust-part-0-4fc192368984) 53 | * [Part 1 - Understanding SQLite and Setting up CLI Application and REPL](https://medium.com/the-polyglot-programmer/what-would-sqlite-look-like-if-written-in-rust-part-1-4a84196c217d) 54 | * [Part 2 - SQL Statement and Meta Commands Parser + Error Handling](https://medium.com/the-polyglot-programmer/what-would-sqlite-look-like-if-written-in-rust-part-2-55b30824de0c) 55 | * [Part 3 - Understanding the B-Tree and its role on database design](https://medium.com/the-polyglot-programmer/what-would-sqlite-look-like-if-written-in-rust-part-3-edd2eefda473) 56 | 57 | ![The SQLite Architecture](images/architecture.png "The SQLite Architecture") 58 | 59 | ### CREATE TABLE and INSERT Statements 60 | [![asciicast](https://asciinema.org/a/406447.svg)](https://asciinema.org/a/406447) 61 | 62 | ### Requirements 63 | Before you begin, ensure you have met the following requirements: 64 | * Rust (latest stable) – [How to install Rust](https://www.rust-lang.org/en-US/install.html) 65 | * SQLite3 66 | 67 | ### Usage (TBD) 68 | 69 | ```shell 70 | > ./rust_sqlite -- help 71 | SQLRite 0.1.0 72 | Joao Henrique Machado Silva 73 | Light version of SQLite developed with Rust 74 | 75 | USAGE: 76 | rust_sqlite 77 | 78 | FLAGS: 79 | -h, --help Prints help information 80 | -V, --version Prints version information 81 | ``` 82 | 83 | ### Project Progress 84 | *Not checked means I am currently working on.* 85 | - [x] CLI and REPL Interface 86 | - [x] Parse meta commands and sql commands. 87 | - [x] Execute simple commands 88 | - [x] Standarized error handling 89 | - [x] Generic validation structure for SQL Commands. 90 | - [x] `Create Table` Command Parsing 91 | - [x] Improve error handling with https://github.com/dtolnay/thiserror 92 | - [x] Added support for parsing duplicate columns on CREATE TABLE 93 | - [x] Added support for parsing multiple PRIMARY KEY on CREATE TABLE 94 | - [x] In memory BTreeMap indexes initially only for PRIMARY KEYS. 95 | - [x] Simple INSERT queries command parsing. 96 | - [x] Implementation UNIQUE key constraints. 97 | - [ ] Improve Error Handling and return without Panic! 98 | - [ ] Simple SELECT queries (Single WHERE clause and no JOINS). 99 | - [ ] Serialization | Deserialization to and from binary encodings ([bincode](https://crates.io/crates/bincode)). 100 | 101 | 102 | ### Roadmap 103 | Features that are in the roadmap of the project: 104 | 105 | *Ideally in order of priority, but nothing set in stone.* 106 | 107 | 108 | - [ ] Implement Open command to load database with a command `.open` 109 | - [ ] Joins 110 | - [ ] INNER JOIN (or sometimes called simple join) 111 | - [ ] LEFT OUTER JOIN (or sometimes called LEFT JOIN) 112 | - [ ] CROSS JOIN 113 | - The RIGHT OUTER JOIN and FULL OUTER JOIN are not supported in SQLite. 114 | - [ ] WAL - Write Ahead Log Implementation 115 | - [ ] `Pager Module` 116 | - [ ] Implementing transactional ACID properties 117 | - [ ] Concurrency 118 | - [ ] Lock Manager 119 | - [ ] Composite Indexing - cost and performance gain analysis 120 | - [ ] Benchmarking vs SQLite for comparison 121 | - [ ] Server Client / Connection Manager 122 | - [ ] Different implementations of storage engines and data structures to optimize for different scenarios 123 | - [ ] Write Heavy - `LSM Tree && SSTable` 124 | - [ ] Read Heavy - `B-Tree` 125 | 126 | ### Contributing 127 | **Pull requests are warmly welcome!!!** 128 | 129 | For major changes, please [open an issue](https://github.com/joaoh82/rust_sqlite/issues/new) first and let's talk about it. We are all ears! 130 | 131 | If you'd like to contribute, please fork the repository and make changes as you'd like and shoot a Pull Request our way! 132 | 133 | **Please make sure to update tests as appropriate.** 134 | 135 | If you feel like you need it go check the GitHub documentation on [creating a pull request](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/creating-a-pull-request). 136 | 137 | ### Code of Conduct 138 | 139 | Contribution to the project is organized under the terms of the 140 | Contributor Covenant, the maintainer of SQLRite, [@joaoh82](https://github.com/joaoh82), promises to 141 | intervene to uphold that code of conduct. 142 | 143 | ### Contact 144 | 145 | If you want to contact me you can reach me at . 146 | 147 | ##### Inspiration 148 | * https://cstack.github.io/db_tutorial/ 149 | -------------------------------------------------------------------------------- /images/SQLRite Data Structures.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joaoh82/rust_sqlite/688bee97f5842be043caae460777213a52a90bfe/images/SQLRite Data Structures.png -------------------------------------------------------------------------------- /images/SQLRite Simple SQL Execution High Level Diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joaoh82/rust_sqlite/688bee97f5842be043caae460777213a52a90bfe/images/SQLRite Simple SQL Execution High Level Diagram.png -------------------------------------------------------------------------------- /images/SQLRite Simple SQL INSERT Execution High Level Diagram (Insert Row).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joaoh82/rust_sqlite/688bee97f5842be043caae460777213a52a90bfe/images/SQLRite Simple SQL INSERT Execution High Level Diagram (Insert Row).png -------------------------------------------------------------------------------- /images/SQLRite Simple SQL INSERT Execution High Level Diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joaoh82/rust_sqlite/688bee97f5842be043caae460777213a52a90bfe/images/SQLRite Simple SQL INSERT Execution High Level Diagram.png -------------------------------------------------------------------------------- /images/SQLRite_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joaoh82/rust_sqlite/688bee97f5842be043caae460777213a52a90bfe/images/SQLRite_logo.png -------------------------------------------------------------------------------- /images/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joaoh82/rust_sqlite/688bee97f5842be043caae460777213a52a90bfe/images/architecture.png -------------------------------------------------------------------------------- /samples/AST.delete.example: -------------------------------------------------------------------------------- 1 | Exemple: DELETE FROM users WHERE id=1; 2 | AST: [ 3 | Delete { 4 | table_name: ObjectName( 5 | [Ident { 6 | value: "users", 7 | quote_style: None 8 | } 9 | ] 10 | ), 11 | selection: Some( 12 | BinaryOp { 13 | left: Identifier( 14 | Ident { 15 | value: "id", 16 | quote_style: None 17 | } 18 | ), 19 | op: Eq, 20 | right: Value( 21 | Number( 22 | "1", 23 | false 24 | ) 25 | ) 26 | } 27 | ) 28 | } 29 | ] -------------------------------------------------------------------------------- /samples/AST.insert.exemple: -------------------------------------------------------------------------------- 1 | Example: INSERT INTO users (name) VALUES ("josh"); 2 | AST: [ 3 | Insert { 4 | or: None, 5 | table_name: ObjectName( 6 | [Ident { 7 | value: "users", 8 | quote_style: None 9 | } 10 | ] 11 | ), 12 | columns: [ 13 | Ident { 14 | value: "name", 15 | quote_style: None 16 | } 17 | ], 18 | overwrite: false, 19 | source: Query { 20 | with: None, 21 | body: Values( 22 | Values( 23 | [ 24 | [ 25 | Identifier( 26 | Ident { 27 | value: "josh", 28 | quote_style: Some('\"') 29 | } 30 | ) 31 | ] 32 | ] 33 | ) 34 | ), 35 | order_by: [], 36 | limit: None, 37 | offset: None, 38 | fetch: None 39 | }, 40 | partitioned: None, 41 | after_columns: [], 42 | table: false 43 | } 44 | ] -------------------------------------------------------------------------------- /samples/AST.select.example: -------------------------------------------------------------------------------- 1 | Example: SELECT * from users; 2 | AST: [ 3 | Query( 4 | Query { 5 | with: None, 6 | body: Select( 7 | Select { 8 | distinct: false, 9 | top: None, 10 | projection: [Wildcard], 11 | from: [ 12 | TableWithJoins { 13 | relation: Table { 14 | name: ObjectName( 15 | [ 16 | Ident { 17 | value: "users", 18 | quote_style: None 19 | } 20 | ] 21 | ), 22 | alias: None, 23 | args: [], 24 | with_hints: [] 25 | }, 26 | joins: [] 27 | } 28 | ], 29 | lateral_views: [], 30 | selection: None, 31 | group_by: [], 32 | cluster_by: [], 33 | distribute_by: [], 34 | sort_by: [], 35 | having: 36 | None 37 | } 38 | ), 39 | order_by: [], 40 | limit: None, 41 | offset: None, 42 | fetch: None 43 | } 44 | ) 45 | ] -------------------------------------------------------------------------------- /samples/AST.update.example: -------------------------------------------------------------------------------- 1 | Example: UPDATE users SET name="josh" where id=1; 2 | AST: [ 3 | Update { 4 | table_name: ObjectName( 5 | [Ident { 6 | value: "users", 7 | quote_style: None 8 | } 9 | ] 10 | ), 11 | assignments: [ 12 | Assignment { 13 | id: Ident { 14 | value: "name", 15 | quote_style: None 16 | }, 17 | value: Identifier( 18 | Ident { 19 | value: "josh", 20 | quote_style: Some('\"') 21 | } 22 | ) 23 | } 24 | ], 25 | selection: Some( 26 | BinaryOp { 27 | left: Identifier( 28 | Ident { 29 | value: "id", 30 | quote_style: None 31 | } 32 | ), 33 | op: Eq, 34 | right: Value( 35 | Number( 36 | "1", 37 | false 38 | ) 39 | ) 40 | } 41 | ) 42 | } 43 | ] -------------------------------------------------------------------------------- /samples/CREATE TABLE sqlrite_schema.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE sqlrite_schema( 2 | type text, 3 | name text, 4 | tbl_name text, 5 | rootpage integer, 6 | sql text 7 | ); -------------------------------------------------------------------------------- /samples/CREATE_TABLE with duplicate.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE contacts ( 2 | id INTEGER PRIMARY KEY, 3 | first_name TEXT NOT NULL, 4 | last_name TEXT NOT NULl, 5 | email TEXT NOT NULL UNIQUE, 6 | email TEXT 7 | ); -------------------------------------------------------------------------------- /samples/CREATE_TABLE.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE contacts ( 2 | id INTEGER PRIMARY KEY, 3 | first_name TEXT NOT NULL, 4 | last_name TEXT NOT NULl, 5 | email TEXT NOT NULL UNIQUE 6 | ); 7 | 8 | CREATE TABLE artists ( 9 | id INTEGER PRIMARY KEY, 10 | first_name TEXT NOT NULL, 11 | last_name TEXT NOT NULl, 12 | email TEXT NOT NULL UNIQUE 13 | ); 14 | 15 | CREATE TABLE users ( 16 | id INTEGER PRIMARY KEY, 17 | name TEXT UNIQUE, 18 | email TEXT, 19 | ); 20 | 21 | CREATE TABLE players ( 22 | name TEXT PRIMARY KEY, 23 | email TEXT, 24 | ); -------------------------------------------------------------------------------- /samples/INSERT.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO artists (name, email) 2 | VALUES('Bud Powell', "bpowell@gmail.com"); 3 | 4 | INSERT INTO artists (name, email) 5 | VALUES('Bud Powell', "bpowell@gmail.com"), 6 | ("Josh Silva", "jsilva@gmail.com"); 7 | 8 | INSERT INTO artists (name, email, enabled) 9 | VALUES('Bud Powell', "bpowell@gmail.com", true), 10 | ("Josh Silva", "jsilva@gmail.com", false); 11 | 12 | INSERT INTO users (name, email) 13 | VALUES ("Josh", "joaoh82@gmail.com"); 14 | 15 | INSERT INTO players (name, email) 16 | VALUES ("Shane", "dj.shane@gmail.com"); -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use thiserror::Error; 2 | 3 | use std::result; 4 | 5 | use sqlparser::parser::ParserError; 6 | 7 | /// This is a type that encapsulated the `std::result` with the enum `SQLRiteError` 8 | /// and makes function signatures easier to read. 9 | pub type Result = result::Result; 10 | 11 | /// SQLRiteError is an enum with all the standardized errors available for returning 12 | /// 13 | #[derive(Error, Debug, PartialEq)] 14 | pub enum SQLRiteError { 15 | #[error("Not Implemented error: {0}")] 16 | NotImplemented(String), 17 | #[error("General error: {0}")] 18 | General(String), 19 | #[error("Internal error: {0}")] 20 | Internal(String), 21 | #[error("Unknown command error: {0}")] 22 | UnknownCommand(String), 23 | #[error("SQL error: {0:?}")] 24 | SqlError(#[from] ParserError), 25 | } 26 | 27 | /// Returns SQLRiteError::General error from String 28 | pub fn sqlrite_error(message: &str) -> SQLRiteError { 29 | SQLRiteError::General(message.to_owned()) 30 | } 31 | 32 | #[cfg(test)] 33 | mod tests { 34 | use super::*; 35 | 36 | #[test] 37 | fn sqlrite_error_test() { 38 | let input = String::from("test error"); 39 | let expected = SQLRiteError::General("test error".to_string()); 40 | 41 | let result = sqlrite_error(&input); 42 | assert_eq!(result, expected); 43 | } 44 | 45 | #[test] 46 | fn sqlrite_display_not_implemented_test() { 47 | let error_string = String::from("Feature not implemented."); 48 | let input = SQLRiteError::NotImplemented(error_string.clone()); 49 | 50 | let expected = format!("Not Implemented error: {}", error_string); 51 | let result = format!("{}", input); 52 | assert_eq!(result, expected); 53 | } 54 | 55 | #[test] 56 | fn sqlrite_display_general_test() { 57 | let error_string = String::from("General error."); 58 | let input = SQLRiteError::General(error_string.clone()); 59 | 60 | let expected = format!("General error: {}", error_string); 61 | let result = format!("{}", input); 62 | assert_eq!(result, expected); 63 | } 64 | 65 | #[test] 66 | fn sqlrite_display_internal_test() { 67 | let error_string = String::from("Internet error."); 68 | let input = SQLRiteError::Internal(error_string.clone()); 69 | 70 | let expected = format!("Internal error: {}", error_string); 71 | let result = format!("{}", input); 72 | assert_eq!(result, expected); 73 | } 74 | 75 | #[test] 76 | fn sqlrite_display_sqlrite_test() { 77 | let error_string = String::from("SQL error."); 78 | let input = SQLRiteError::SqlError(ParserError::ParserError(error_string.clone())); 79 | 80 | let expected = format!("SQL error: ParserError(\"{}\")", error_string); 81 | let result = format!("{}", input); 82 | assert_eq!(result, expected); 83 | } 84 | 85 | #[test] 86 | fn sqlrite_unknown_test() { 87 | let error_string = String::from("Unknown error."); 88 | let input = SQLRiteError::UnknownCommand(error_string.clone()); 89 | 90 | let expected = format!("Unknown command error: {}", error_string); 91 | let result = format!("{}", input); 92 | assert_eq!(result, expected); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | extern crate clap; 2 | #[macro_use] 3 | extern crate prettytable; 4 | 5 | mod error; 6 | mod meta_command; 7 | mod repl; 8 | mod sql; 9 | 10 | use meta_command::handle_meta_command; 11 | use repl::{get_command_type, get_config, CommandType, REPLHelper}; 12 | use sql::db::database::Database; 13 | use sql::process_command; 14 | 15 | use rustyline::error::ReadlineError; 16 | use rustyline::Editor; 17 | 18 | use clap::{crate_authors, crate_description, crate_name, crate_version, Command}; 19 | 20 | fn main() -> rustyline::Result<()> { 21 | env_logger::init(); 22 | 23 | let _matches = Command::new(crate_name!()) 24 | .version(crate_version!()) 25 | .author(crate_authors!()) 26 | .about(crate_description!()) 27 | .get_matches(); 28 | 29 | // Starting Rustyline with a default configuration 30 | let config = get_config(); 31 | 32 | // Getting a new Rustyline Helper 33 | let helper = REPLHelper::default(); 34 | 35 | // Initiatlizing Rustyline Editor with set config and setting helper 36 | let mut repl = Editor::with_config(config); 37 | repl.set_helper(Some(helper)); 38 | 39 | // This method loads history file into memory 40 | // If it doesn't exist, creates one 41 | // TODO: Check history file size and if too big, clean it. 42 | if repl.load_history("history").is_err() { 43 | println!("No previous history."); 44 | } 45 | 46 | // Friendly intro message for the user 47 | println!( 48 | "{} - {}\n{}{}{}{}", 49 | crate_name!(), 50 | crate_version!(), 51 | "Enter .exit to quit.\n", 52 | "Enter .help for usage hints.\n", 53 | "Connected to a transient in-memory database.\n", 54 | "Use '.open FILENAME' to reopen on a persistent database." 55 | ); 56 | 57 | let mut db = Database::new("tempdb".to_string()); 58 | 59 | loop { 60 | let p = format!("sqlrite> "); 61 | repl.helper_mut().expect("No helper found").colored_prompt = 62 | format!("\x1b[1;32m{}\x1b[0m", p); 63 | // Source for ANSI Color information: http://www.perpetualpc.net/6429_colors.html#color_list 64 | // http://bixense.com/clicolors/ 65 | 66 | let readline = repl.readline(&p); 67 | match readline { 68 | Ok(command) => { 69 | repl.add_history_entry(command.as_str()); 70 | // Parsing user's input and returning and enum of repl::CommandType 71 | match get_command_type(&command.trim().to_owned()) { 72 | CommandType::SQLCommand(_cmd) => { 73 | // process_command takes care of tokenizing, parsing and executing 74 | // the SQL Statement and returning a Result 75 | let _ = match process_command(&command, &mut db) { 76 | Ok(response) => println!("{}", response), 77 | Err(err) => eprintln!("An error occured: {}", err), 78 | }; 79 | } 80 | CommandType::MetaCommand(cmd) => { 81 | // handle_meta_command parses and executes the MetaCommand 82 | // and returns a Result 83 | let _ = match handle_meta_command(cmd, &mut repl) { 84 | Ok(response) => println!("{}", response), 85 | Err(err) => eprintln!("An error occured: {}", err), 86 | }; 87 | } 88 | } 89 | } 90 | Err(ReadlineError::Interrupted) => { 91 | break; 92 | } 93 | Err(ReadlineError::Eof) => { 94 | break; 95 | } 96 | Err(err) => { 97 | eprintln!("An error occured: {:?}", err); 98 | break; 99 | } 100 | } 101 | } 102 | repl.append_history("history").unwrap(); 103 | 104 | Ok(()) 105 | } 106 | -------------------------------------------------------------------------------- /src/meta_command/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::error::{Result, SQLRiteError}; 2 | 3 | use crate::repl::REPLHelper; 4 | use rustyline::Editor; 5 | use std::fmt; 6 | 7 | #[derive(Debug, PartialEq)] 8 | pub enum MetaCommand { 9 | Exit, 10 | Help, 11 | Open(String), 12 | Unknown, 13 | } 14 | 15 | /// Trait responsible for translating type into a formated text. 16 | impl fmt::Display for MetaCommand { 17 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 18 | match self { 19 | MetaCommand::Exit => f.write_str(".exit"), 20 | MetaCommand::Help => f.write_str(".help"), 21 | MetaCommand::Open(_) => f.write_str(".open"), 22 | MetaCommand::Unknown => f.write_str("Unknown command"), 23 | } 24 | } 25 | } 26 | 27 | impl MetaCommand { 28 | pub fn new(command: String) -> MetaCommand { 29 | let args: Vec<&str> = command.split_whitespace().collect(); 30 | let cmd = args[0].to_owned(); 31 | match cmd.as_ref() { 32 | ".exit" => MetaCommand::Exit, 33 | ".help" => MetaCommand::Help, 34 | ".open" => MetaCommand::Open(command), 35 | _ => MetaCommand::Unknown, 36 | } 37 | } 38 | } 39 | 40 | pub fn handle_meta_command(command: MetaCommand, repl: &mut Editor) -> Result { 41 | match command { 42 | MetaCommand::Exit => { 43 | repl.append_history("history").unwrap(); 44 | std::process::exit(0) 45 | } 46 | MetaCommand::Help => Ok(format!( 47 | "{}{}{}{}{}{}{}{}", 48 | "Special commands:\n", 49 | ".help - Display this message\n", 50 | ".open - Close existing database and reopen FILENAME\n", 51 | ".save - Write in-memory database into FILENAME\n", 52 | ".read - Read input from FILENAME\n", 53 | ".tables - List names of tables\n", 54 | ".ast - Show the abstract syntax tree for QUERY.\n", 55 | ".exit - Quits this application" 56 | )), 57 | MetaCommand::Open(args) => Ok(format!("To be implemented: {}", args)), 58 | MetaCommand::Unknown => Err(SQLRiteError::UnknownCommand(format!( 59 | "Unknown command or invalid arguments. Enter '.help'" 60 | ))), 61 | } 62 | } 63 | 64 | #[cfg(test)] 65 | mod tests { 66 | use super::*; 67 | use crate::repl::{get_config, REPLHelper}; 68 | 69 | #[test] 70 | fn get_meta_command_exit_test() { 71 | // Starting Rustyline with a default configuration 72 | let config = get_config(); 73 | 74 | // Getting a new Rustyline Helper 75 | let helper = REPLHelper::default(); 76 | 77 | // Initiatlizing Rustyline Editor with set config and setting helper 78 | let mut repl = Editor::with_config(config); 79 | repl.set_helper(Some(helper)); 80 | 81 | let inputed_command = MetaCommand::Help; 82 | 83 | let result = handle_meta_command(inputed_command, &mut repl); 84 | assert_eq!(result.is_ok(), true); 85 | } 86 | 87 | #[test] 88 | fn get_meta_command_open_test() { 89 | // Starting Rustyline with a default configuration 90 | let config = get_config(); 91 | 92 | // Getting a new Rustyline Helper 93 | let helper = REPLHelper::default(); 94 | 95 | // Initiatlizing Rustyline Editor with set config and setting helper 96 | let mut repl = Editor::with_config(config); 97 | repl.set_helper(Some(helper)); 98 | 99 | let inputed_command = MetaCommand::Open(".open database.db".to_string()); 100 | 101 | let result = handle_meta_command(inputed_command, &mut repl); 102 | assert_eq!(result.is_ok(), true); 103 | } 104 | 105 | #[test] 106 | fn get_meta_command_unknown_command_test() { 107 | // Starting Rustyline with a default configuration 108 | let config = get_config(); 109 | 110 | // Getting a new Rustyline Helper 111 | let helper = REPLHelper::default(); 112 | 113 | // Initiatlizing Rustyline Editor with set config and setting helper 114 | let mut repl = Editor::with_config(config); 115 | repl.set_helper(Some(helper)); 116 | 117 | let inputed_command = MetaCommand::Unknown; 118 | 119 | let result = handle_meta_command(inputed_command, &mut repl); 120 | assert_eq!(result.is_err(), true); 121 | } 122 | 123 | #[test] 124 | fn meta_command_display_trait_test() { 125 | let exit = MetaCommand::Exit; 126 | let help = MetaCommand::Help; 127 | let open = MetaCommand::Open(".open database.db".to_string()); 128 | let unknown = MetaCommand::Unknown; 129 | 130 | assert_eq!(format!("{}", exit), ".exit"); 131 | assert_eq!(format!("{}", help), ".help"); 132 | assert_eq!(format!("{}", open), ".open"); 133 | assert_eq!(format!("{}", unknown), "Unknown command"); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/repl/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::meta_command::*; 2 | use crate::sql::*; 3 | 4 | use std::borrow::Cow::{self, Borrowed, Owned}; 5 | 6 | use rustyline::config::OutputStreamType; 7 | use rustyline::error::ReadlineError; 8 | use rustyline::highlight::{Highlighter, MatchingBracketHighlighter}; 9 | use rustyline::hint::{Hinter, HistoryHinter}; 10 | use rustyline::validate::Validator; 11 | use rustyline::validate::{ValidationContext, ValidationResult}; 12 | use rustyline::{CompletionType, Config, Context, EditMode}; 13 | use rustyline_derive::{Completer, Helper}; 14 | 15 | /// We have two different types of commands MetaCommand and SQLCommand 16 | #[derive(Debug, PartialEq)] 17 | pub enum CommandType { 18 | MetaCommand(MetaCommand), 19 | SQLCommand(SQLCommand), 20 | } 21 | 22 | /// Returns the type of command inputed in the REPL 23 | pub fn get_command_type(command: &String) -> CommandType { 24 | match command.starts_with(".") { 25 | true => CommandType::MetaCommand(MetaCommand::new(command.to_owned())), 26 | false => CommandType::SQLCommand(SQLCommand::new(command.to_owned())), 27 | } 28 | } 29 | 30 | // REPL Helper Struct with all functionalities 31 | #[derive(Helper, Completer)] 32 | pub struct REPLHelper { 33 | // pub validator: MatchingBracketValidator, 34 | pub colored_prompt: String, 35 | pub hinter: HistoryHinter, 36 | pub highlighter: MatchingBracketHighlighter, 37 | } 38 | 39 | // Implementing the Default trait to give our struct a default value 40 | impl Default for REPLHelper { 41 | fn default() -> Self { 42 | Self { 43 | highlighter: MatchingBracketHighlighter::new(), 44 | hinter: HistoryHinter {}, 45 | colored_prompt: "".to_owned(), 46 | } 47 | } 48 | } 49 | 50 | // Implementing trait responsible for providing hints 51 | impl Hinter for REPLHelper { 52 | type Hint = String; 53 | 54 | // Takes the currently edited line with the cursor position and returns the string that should be 55 | // displayed or None if no hint is available for the text the user currently typed 56 | fn hint(&self, line: &str, pos: usize, ctx: &Context<'_>) -> Option { 57 | self.hinter.hint(line, pos, ctx) 58 | } 59 | } 60 | 61 | // Implementing trait responsible for determining whether the current input buffer is valid. 62 | // Rustyline uses the method provided by this trait to decide whether hitting the enter key 63 | // will end the current editing session and return the current line buffer to the caller of 64 | // Editor::readline or variants. 65 | impl Validator for REPLHelper { 66 | // Takes the currently edited input and returns a ValidationResult indicating whether it 67 | // is valid or not along with an option message to display about the result. 68 | fn validate(&self, ctx: &mut ValidationContext) -> Result { 69 | use ValidationResult::{Incomplete, /*Invalid,*/ Valid}; 70 | let input = ctx.input(); 71 | let result = if input.starts_with(".") { 72 | Valid(None) 73 | } else if !input.ends_with(';') { 74 | Incomplete 75 | } else { 76 | Valid(None) 77 | }; 78 | Ok(result) 79 | } 80 | } 81 | 82 | // Implementing syntax highlighter with ANSI color. 83 | impl Highlighter for REPLHelper { 84 | // Takes the prompt and returns the highlighted version (with ANSI color). 85 | fn highlight_prompt<'b, 's: 'b, 'p: 'b>( 86 | &'s self, 87 | prompt: &'p str, 88 | default: bool, 89 | ) -> Cow<'b, str> { 90 | if default { 91 | Borrowed(&self.colored_prompt) 92 | } else { 93 | Borrowed(prompt) 94 | } 95 | } 96 | 97 | // Takes the hint and returns the highlighted version (with ANSI color). 98 | fn highlight_hint<'h>(&self, hint: &'h str) -> Cow<'h, str> { 99 | Owned("\x1b[1m".to_owned() + hint + "\x1b[m") 100 | } 101 | 102 | // Takes the currently edited line with the cursor position and returns the highlighted version (with ANSI color). 103 | fn highlight<'l>(&self, line: &'l str, pos: usize) -> Cow<'l, str> { 104 | self.highlighter.highlight(line, pos) 105 | } 106 | 107 | // Tells if line needs to be highlighted when a specific char is typed or when cursor is moved under a specific char. 108 | // Used to optimize refresh when a character is inserted or the cursor is moved. 109 | fn highlight_char(&self, line: &str, pos: usize) -> bool { 110 | self.highlighter.highlight_char(line, pos) 111 | } 112 | } 113 | 114 | // Returns a Config::builder with basic Editor configuration 115 | pub fn get_config() -> Config { 116 | Config::builder() 117 | .history_ignore_space(true) 118 | .completion_type(CompletionType::List) 119 | .edit_mode(EditMode::Emacs) 120 | .output_stream(OutputStreamType::Stdout) 121 | .build() 122 | } 123 | 124 | #[cfg(test)] 125 | mod tests { 126 | use super::*; 127 | 128 | #[test] 129 | fn get_command_type_meta_command_test() { 130 | let input = String::from(".help"); 131 | let expected = CommandType::MetaCommand(MetaCommand::Help); 132 | 133 | let result = get_command_type(&input); 134 | assert_eq!(result, expected); 135 | } 136 | 137 | #[test] 138 | fn get_command_type_sql_command_test() { 139 | let input = String::from("SELECT * from users;"); 140 | let expected = CommandType::SQLCommand(SQLCommand::Unknown(input.clone())); 141 | 142 | let result = get_command_type(&input); 143 | assert_eq!(result, expected); 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/sql/db/database.rs: -------------------------------------------------------------------------------- 1 | use crate::error::{Result, SQLRiteError}; 2 | use crate::sql::db::table::Table; 3 | use serde::{Deserialize, Serialize}; 4 | use std::collections::HashMap; 5 | 6 | /// The database is represented by this structure.assert_eq! 7 | #[derive(Serialize, Deserialize, PartialEq, Debug)] 8 | pub struct Database { 9 | /// Name of this database. (schema name, not filename) 10 | pub db_name: String, 11 | /// HashMap of tables in this database 12 | pub tables: HashMap, 13 | } 14 | 15 | impl Database { 16 | /// Creates an empty `Database` 17 | /// 18 | /// # Examples 19 | /// 20 | /// ``` 21 | /// let mut db = sql::db::database::Database::new("my_db".to_string()); 22 | /// ``` 23 | pub fn new(db_name: String) -> Self { 24 | Database { 25 | db_name, 26 | tables: HashMap::new(), 27 | } 28 | } 29 | 30 | /// Returns true if the database contains a table with the specified key as a table name. 31 | /// 32 | pub fn contains_table(&self, table_name: String) -> bool { 33 | self.tables.contains_key(&table_name) 34 | } 35 | 36 | /// Returns an immutable reference of `sql::db::table::Table` if the database contains a 37 | /// table with the specified key as a table name. 38 | /// 39 | pub fn get_table(&self, table_name: String) -> Result<&Table> { 40 | if let Some(table) = self.tables.get(&table_name) { 41 | Ok(table) 42 | } else { 43 | Err(SQLRiteError::General(String::from("Table not found."))) 44 | } 45 | } 46 | 47 | /// Returns an mutable reference of `sql::db::table::Table` if the database contains a 48 | /// table with the specified key as a table name. 49 | /// 50 | pub fn get_table_mut(&mut self, table_name: String) -> Result<&mut Table> { 51 | if let Some(table) = self.tables.get_mut(&table_name) { 52 | Ok(table) 53 | } else { 54 | Err(SQLRiteError::General(String::from("Table not found."))) 55 | } 56 | } 57 | } 58 | 59 | #[cfg(test)] 60 | mod tests { 61 | use super::*; 62 | use crate::sql::parser::create::CreateQuery; 63 | use sqlparser::dialect::SQLiteDialect; 64 | use sqlparser::parser::Parser; 65 | 66 | #[test] 67 | fn new_database_create_test() { 68 | let db_name = String::from("my_db"); 69 | let db = Database::new(db_name.to_string()); 70 | assert_eq!(db.db_name, db_name); 71 | } 72 | 73 | #[test] 74 | fn contains_table_test() { 75 | let db_name = String::from("my_db"); 76 | let mut db = Database::new(db_name.to_string()); 77 | 78 | let query_statement = "CREATE TABLE contacts ( 79 | id INTEGER PRIMARY KEY, 80 | first_name TEXT NOT NULL, 81 | last_name TEXT NOT NULl, 82 | email TEXT NOT NULL UNIQUE 83 | );"; 84 | let dialect = SQLiteDialect {}; 85 | let mut ast = Parser::parse_sql(&dialect, &query_statement).unwrap(); 86 | if ast.len() > 1 { 87 | panic!("Expected a single query statement, but there are more then 1.") 88 | } 89 | let query = ast.pop().unwrap(); 90 | 91 | let create_query = CreateQuery::new(&query).unwrap(); 92 | let table_name = &create_query.table_name; 93 | db.tables 94 | .insert(table_name.to_string(), Table::new(create_query)); 95 | 96 | assert!(db.contains_table("contacts".to_string())); 97 | } 98 | 99 | #[test] 100 | fn get_table_test() { 101 | let db_name = String::from("my_db"); 102 | let mut db = Database::new(db_name.to_string()); 103 | 104 | let query_statement = "CREATE TABLE contacts ( 105 | id INTEGER PRIMARY KEY, 106 | first_name TEXT NOT NULL, 107 | last_name TEXT NOT NULl, 108 | email TEXT NOT NULL UNIQUE 109 | );"; 110 | let dialect = SQLiteDialect {}; 111 | let mut ast = Parser::parse_sql(&dialect, &query_statement).unwrap(); 112 | if ast.len() > 1 { 113 | panic!("Expected a single query statement, but there are more then 1.") 114 | } 115 | let query = ast.pop().unwrap(); 116 | 117 | let create_query = CreateQuery::new(&query).unwrap(); 118 | let table_name = &create_query.table_name; 119 | db.tables 120 | .insert(table_name.to_string(), Table::new(create_query)); 121 | 122 | let table = db.get_table(String::from("contacts")).unwrap(); 123 | assert_eq!(table.columns.len(), 4); 124 | 125 | let mut table = db.get_table_mut(String::from("contacts")).unwrap(); 126 | table.last_rowid += 1; 127 | assert_eq!(table.columns.len(), 4); 128 | assert_eq!(table.last_rowid, 1); 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/sql/db/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod database; 2 | pub mod table; 3 | -------------------------------------------------------------------------------- /src/sql/db/table.rs: -------------------------------------------------------------------------------- 1 | use crate::error::{Result, SQLRiteError}; 2 | use crate::sql::parser::create::CreateQuery; 3 | use serde::{Deserialize, Serialize}; 4 | use std::cell::RefCell; 5 | use std::collections::{BTreeMap, HashMap}; 6 | use std::fmt; 7 | use std::rc::Rc; 8 | 9 | use prettytable::{Cell as PrintCell, Row as PrintRow, Table as PrintTable}; 10 | 11 | /// SQLRite data types 12 | /// Mapped after SQLite Data Type Storage Classes and SQLite Affinity Type 13 | /// (Datatypes In SQLite Version 3)[https://www.sqlite.org/datatype3.html] 14 | #[derive(Serialize, Deserialize, PartialEq, Debug)] 15 | pub enum DataType { 16 | Integer, 17 | Text, 18 | Real, 19 | Bool, 20 | None, 21 | Invalid, 22 | } 23 | 24 | impl DataType { 25 | pub fn new(cmd: String) -> DataType { 26 | match cmd.to_lowercase().as_ref() { 27 | "integer" => DataType::Integer, 28 | "text" => DataType::Text, 29 | "real" => DataType::Real, 30 | "bool" => DataType::Bool, 31 | "none" => DataType::None, 32 | _ => { 33 | eprintln!("Invalid data type given {}", cmd); 34 | return DataType::Invalid; 35 | } 36 | } 37 | } 38 | } 39 | 40 | impl fmt::Display for DataType { 41 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 42 | match *self { 43 | DataType::Integer => f.write_str("Integer"), 44 | DataType::Text => f.write_str("Text"), 45 | DataType::Real => f.write_str("Real"), 46 | DataType::Bool => f.write_str("Boolean"), 47 | DataType::None => f.write_str("None"), 48 | DataType::Invalid => f.write_str("Invalid"), 49 | } 50 | } 51 | } 52 | 53 | /// The schema for each SQL Table is represented in memory by 54 | /// following structure 55 | #[derive(Serialize, Deserialize, PartialEq, Debug)] 56 | pub struct Table { 57 | /// Name of the table 58 | pub tb_name: String, 59 | /// HashMap with information about each column 60 | pub columns: Vec, 61 | /// HashMap with information about each row 62 | pub rows: Rc>>, 63 | /// HashMap of SQL indexes on this table. 64 | pub indexes: HashMap, 65 | /// ROWID of most recent insert 66 | pub last_rowid: i64, 67 | /// PRIMARY KEY Column name, if table does not have PRIMARY KEY this would be -1 68 | pub primary_key: String, 69 | } 70 | 71 | impl Table { 72 | pub fn new(create_query: CreateQuery) -> Self { 73 | let table_name = create_query.table_name; 74 | let mut primary_key: String = String::from("-1"); 75 | let columns = create_query.columns; 76 | 77 | let mut table_cols: Vec = vec![]; 78 | let table_rows: Rc>> = Rc::new(RefCell::new(HashMap::new())); 79 | for col in &columns { 80 | let col_name = &col.name; 81 | if col.is_pk { 82 | primary_key = col_name.to_string(); 83 | } 84 | table_cols.push(Column::new( 85 | col_name.to_string(), 86 | col.datatype.to_string(), 87 | col.is_pk, 88 | col.not_null, 89 | col.is_unique, 90 | )); 91 | 92 | match DataType::new(col.datatype.to_string()) { 93 | DataType::Integer => table_rows 94 | .clone() 95 | .borrow_mut() 96 | .insert(col.name.to_string(), Row::Integer(BTreeMap::new())), 97 | DataType::Real => table_rows 98 | .clone() 99 | .borrow_mut() 100 | .insert(col.name.to_string(), Row::Real(BTreeMap::new())), 101 | DataType::Text => table_rows 102 | .clone() 103 | .borrow_mut() 104 | .insert(col.name.to_string(), Row::Text(BTreeMap::new())), 105 | DataType::Bool => table_rows 106 | .clone() 107 | .borrow_mut() 108 | .insert(col.name.to_string(), Row::Bool(BTreeMap::new())), 109 | DataType::Invalid => table_rows 110 | .clone() 111 | .borrow_mut() 112 | .insert(col.name.to_string(), Row::None), 113 | DataType::None => table_rows 114 | .clone() 115 | .borrow_mut() 116 | .insert(col.name.to_string(), Row::None), 117 | }; 118 | } 119 | 120 | Table { 121 | tb_name: table_name, 122 | columns: table_cols, 123 | rows: table_rows, 124 | indexes: HashMap::new(), 125 | last_rowid: 0, 126 | primary_key: primary_key, 127 | } 128 | } 129 | 130 | /// Returns a `bool` informing if a `Column` with a specific name exists or not 131 | /// 132 | pub fn contains_column(&self, column: String) -> bool { 133 | self.columns.iter().any(|col| col.column_name == column) 134 | } 135 | 136 | /// Returns an immutable reference of `sql::db::table::Column` if the table contains a 137 | /// column with the specified key as a column name. 138 | /// 139 | pub fn get_column(&mut self, column_name: String) -> Result<&Column> { 140 | if let Some(column) = self 141 | .columns 142 | .iter() 143 | .filter(|c| c.column_name == column_name) 144 | .collect::>() 145 | .first() 146 | { 147 | Ok(column) 148 | } else { 149 | Err(SQLRiteError::General(String::from("Column not found."))) 150 | } 151 | } 152 | 153 | /// Returns an mutable reference of `sql::db::table::Column` if the table contains a 154 | /// column with the specified key as a column name. 155 | /// 156 | pub fn get_column_mut<'a>(&mut self, column_name: String) -> Result<&mut Column> { 157 | for elem in self.columns.iter_mut() { 158 | if elem.column_name == column_name { 159 | return Ok(elem); 160 | } 161 | } 162 | Err(SQLRiteError::General(String::from("Column not found."))) 163 | } 164 | 165 | /// Validates if columns and values being inserted violate the UNIQUE constraint 166 | /// As a reminder the PRIMARY KEY column automatically also is a UNIQUE column. 167 | /// 168 | pub fn validate_unique_constraint( 169 | &mut self, 170 | cols: &Vec, 171 | values: &Vec, 172 | ) -> Result<()> { 173 | for (idx, name) in cols.iter().enumerate() { 174 | let column = self.get_column_mut(name.to_string()).unwrap(); 175 | // println!( 176 | // "name: {} | is_pk: {} | is_unique: {}, not_null: {}", 177 | // name, column.is_pk, column.is_unique, column.not_null 178 | // ); 179 | if column.is_unique { 180 | let col_idx = &column.index; 181 | if *name == *column.column_name { 182 | let val = &values[idx]; 183 | match col_idx { 184 | Index::Integer(index) => { 185 | if index.contains_key(&val.parse::().unwrap()) { 186 | return Err(SQLRiteError::General(format!( 187 | "Error: unique constraint violation for column {}. 188 | Value {} already exists for column {}", 189 | *name, val, *name 190 | ))); 191 | } 192 | } 193 | Index::Text(index) => { 194 | if index.contains_key(val) { 195 | return Err(SQLRiteError::General(format!( 196 | "Error: unique constraint violation for column {}. 197 | Value {} already exists for column {}", 198 | *name, val, *name 199 | ))); 200 | } 201 | } 202 | Index::None => { 203 | return Err(SQLRiteError::General(format!( 204 | "Error: cannot find index for column {}", 205 | name 206 | ))); 207 | } 208 | }; 209 | } 210 | } 211 | } 212 | return Ok(()); 213 | } 214 | 215 | /// Inserts all VALUES in its approprieta COLUMNS, using the ROWID an embedded INDEX on all ROWS 216 | /// Every `Table` keeps track of the `last_rowid` in order to facilitate what the next one would be. 217 | /// One limitation of this data structure is that we can only have one write transaction at a time, otherwise 218 | /// we could have a race condition on the last_rowid.println! 219 | /// 220 | /// Since we are loosely modeling after SQLite, this is also a limitation of SQLite (allowing only one write transcation at a time), 221 | /// So we are good. :) 222 | /// 223 | pub fn insert_row(&mut self, cols: &Vec, values: &Vec) { 224 | let mut next_rowid = self.last_rowid + i64::from(1); 225 | 226 | // Checks if table has a PRIMARY KEY 227 | if self.primary_key != "-1" { 228 | // Checking if primary key is in INSERT QUERY columns 229 | // If it is not, assign the next_rowid to it 230 | if !cols.iter().any(|col| col == &self.primary_key) { 231 | let rows_clone = Rc::clone(&self.rows); 232 | let mut row_data = rows_clone.as_ref().borrow_mut(); 233 | let mut table_col_data = row_data.get_mut(&self.primary_key).unwrap(); 234 | 235 | // Getting the header based on the column name 236 | let column_headers = self.get_column_mut(self.primary_key.to_string()).unwrap(); 237 | 238 | // Getting index for column, if it exist 239 | let col_index = column_headers.get_mut_index(); 240 | 241 | // We only AUTO ASSIGN in case the ROW is a PRIMARY KEY and INTEGER type 242 | match &mut table_col_data { 243 | Row::Integer(tree) => { 244 | let val = next_rowid as i32; 245 | tree.insert(next_rowid.clone(), val); 246 | if let Index::Integer(index) = col_index { 247 | index.insert(val, next_rowid.clone()); 248 | } 249 | } 250 | _ => (), 251 | } 252 | } else { 253 | // If PRIMARY KEY Column is in the Column list from INSERT Query, 254 | // We get the value assigned to it in the VALUES part of the query 255 | // and assign it to next_rowid, so every value if indexed by same rowid 256 | // Also, next ROWID should keep AUTO INCREMENTING from last ROWID 257 | let rows_clone = Rc::clone(&self.rows); 258 | let mut row_data = rows_clone.as_ref().borrow_mut(); 259 | let mut table_col_data = row_data.get_mut(&self.primary_key).unwrap(); 260 | 261 | // Again, this is only valid for PRIMARY KEYs of INTEGER type 262 | match &mut table_col_data { 263 | Row::Integer(_) => { 264 | for i in 0..cols.len() { 265 | // Getting column name 266 | let key = &cols[i]; 267 | if key == &self.primary_key { 268 | let val = &values[i]; 269 | next_rowid = val.parse::().unwrap(); 270 | } 271 | } 272 | } 273 | _ => (), 274 | } 275 | } 276 | } 277 | 278 | // This block checks if there are any columns from table missing 279 | // from INSERT statement. If there are, we add "Null" to the column. 280 | // We do this because otherwise the ROWID reference for each value would be wrong 281 | // Since rows not always have the same length. 282 | let column_names = self 283 | .columns 284 | .iter() 285 | .map(|col| col.column_name.to_string()) 286 | .collect::>(); 287 | let mut j: usize = 0; 288 | // For every column in the INSERT statement 289 | for i in 0..column_names.len() { 290 | let mut val = String::from("Null"); 291 | let key = &column_names[i]; 292 | 293 | if let Some(key) = &cols.get(j) { 294 | if &key.to_string() == &column_names[i] { 295 | // Getting column name 296 | val = values[j].to_string(); 297 | j += 1; 298 | } else { 299 | if &self.primary_key == &column_names[i] { 300 | continue; 301 | } 302 | } 303 | } else { 304 | if &self.primary_key == &column_names[i] { 305 | continue; 306 | } 307 | } 308 | 309 | // Getting the rows from the column name 310 | let rows_clone = Rc::clone(&self.rows); 311 | let mut row_data = rows_clone.as_ref().borrow_mut(); 312 | let mut table_col_data = row_data.get_mut(key).unwrap(); 313 | 314 | // Getting the header based on the column name 315 | let column_headers = self.get_column_mut(key.to_string()).unwrap(); 316 | 317 | // Getting index for column, if it exist 318 | let col_index = column_headers.get_mut_index(); 319 | 320 | match &mut table_col_data { 321 | Row::Integer(tree) => { 322 | let val = val.parse::().unwrap(); 323 | tree.insert(next_rowid.clone(), val); 324 | if let Index::Integer(index) = col_index { 325 | index.insert(val, next_rowid.clone()); 326 | } 327 | } 328 | Row::Text(tree) => { 329 | tree.insert(next_rowid.clone(), val.to_string()); 330 | if let Index::Text(index) = col_index { 331 | index.insert(val.to_string(), next_rowid.clone()); 332 | } 333 | } 334 | Row::Real(tree) => { 335 | let val = val.parse::().unwrap(); 336 | tree.insert(next_rowid.clone(), val); 337 | } 338 | Row::Bool(tree) => { 339 | let val = val.parse::().unwrap(); 340 | tree.insert(next_rowid.clone(), val); 341 | } 342 | Row::None => panic!("None data Found"), 343 | } 344 | } 345 | self.last_rowid = next_rowid; 346 | } 347 | 348 | /// Print the table schema to standard output in a pretty formatted way 349 | /// 350 | /// # Example 351 | /// 352 | /// ``` 353 | /// let table = Table::new(payload); 354 | /// table.print_table_schema(); 355 | /// 356 | /// Prints to standard output: 357 | /// +-------------+-----------+-------------+--------+----------+ 358 | /// | Column Name | Data Type | PRIMARY KEY | UNIQUE | NOT NULL | 359 | /// +-------------+-----------+-------------+--------+----------+ 360 | /// | id | Integer | true | true | true | 361 | /// +-------------+-----------+-------------+--------+----------+ 362 | /// | name | Text | false | true | false | 363 | /// +-------------+-----------+-------------+--------+----------+ 364 | /// | email | Text | false | false | false | 365 | /// +-------------+-----------+-------------+--------+----------+ 366 | /// ``` 367 | /// 368 | pub fn print_table_schema(&self) -> Result { 369 | let mut table = PrintTable::new(); 370 | table.add_row(row![ 371 | "Column Name", 372 | "Data Type", 373 | "PRIMARY KEY", 374 | "UNIQUE", 375 | "NOT NULL" 376 | ]); 377 | 378 | for col in &self.columns { 379 | table.add_row(row![ 380 | col.column_name, 381 | col.datatype, 382 | col.is_pk, 383 | col.is_unique, 384 | col.not_null 385 | ]); 386 | } 387 | 388 | let lines = table.printstd(); 389 | Ok(lines) 390 | } 391 | 392 | /// Print the table data to standard output in a pretty formatted way 393 | /// 394 | /// # Example 395 | /// 396 | /// ``` 397 | /// let db_table = db.get_table_mut(table_name.to_string()).unwrap(); 398 | /// db_table.print_table_data(); 399 | /// 400 | /// Prints to standard output: 401 | /// +----+---------+------------------------+ 402 | /// | id | name | email | 403 | /// +----+---------+------------------------+ 404 | /// | 1 | "Jack" | "jack@mail.com" | 405 | /// +----+---------+------------------------+ 406 | /// | 10 | "Bob" | "bob@main.com" | 407 | /// +----+---------+------------------------+ 408 | /// | 11 | "Bill" | "bill@main.com" | 409 | /// +----+---------+------------------------+ 410 | /// ``` 411 | /// 412 | pub fn print_table_data(&self) { 413 | let mut print_table = PrintTable::new(); 414 | 415 | let column_names = self 416 | .columns 417 | .iter() 418 | .map(|col| col.column_name.to_string()) 419 | .collect::>(); 420 | 421 | let header_row = PrintRow::new( 422 | column_names 423 | .iter() 424 | .map(|col| PrintCell::new(&col)) 425 | .collect::>(), 426 | ); 427 | 428 | let rows_clone = Rc::clone(&self.rows); 429 | let row_data = rows_clone.as_ref().borrow(); 430 | let first_col_data = row_data 431 | .get(&self.columns.first().unwrap().column_name) 432 | .unwrap(); 433 | let num_rows = first_col_data.count(); 434 | let mut print_table_rows: Vec = vec![PrintRow::new(vec![]); num_rows]; 435 | 436 | for col_name in &column_names { 437 | let col_val = row_data 438 | .get(col_name) 439 | .expect("Can't find any rows with the given column"); 440 | let columns: Vec = col_val.get_serialized_col_data(); 441 | 442 | for i in 0..num_rows { 443 | if let Some(cell) = &columns.get(i) { 444 | print_table_rows[i].add_cell(PrintCell::new(cell)); 445 | } else { 446 | print_table_rows[i].add_cell(PrintCell::new("")); 447 | } 448 | } 449 | } 450 | 451 | print_table.add_row(header_row); 452 | for row in print_table_rows { 453 | print_table.add_row(row); 454 | } 455 | 456 | print_table.printstd(); 457 | } 458 | } 459 | 460 | /// The schema for each SQL column in every table is represented in memory 461 | /// by following structure 462 | #[derive(Serialize, Deserialize, PartialEq, Debug)] 463 | pub struct Column { 464 | /// Name of the column 465 | pub column_name: String, 466 | /// Datatype of column 467 | pub datatype: DataType, 468 | /// Value representing if column is PRIMARY KEY 469 | pub is_pk: bool, 470 | /// Value representing if column was declared with the NOT NULL Constraint 471 | pub not_null: bool, 472 | /// Value representing if column was declared with the UNIQUE Constraint 473 | pub is_unique: bool, 474 | /// Value representing if column is Indexed or not 475 | pub is_indexed: bool, 476 | /// BtreeMap mapping the index to a payload value on the corresponding Row 477 | /// Mapped using a ROWID 478 | pub index: Index, 479 | } 480 | 481 | impl Column { 482 | pub fn new( 483 | name: String, 484 | datatype: String, 485 | is_pk: bool, 486 | not_null: bool, 487 | is_unique: bool, 488 | ) -> Self { 489 | let dt = DataType::new(datatype); 490 | let index = match dt { 491 | DataType::Integer => Index::Integer(BTreeMap::new()), 492 | DataType::Bool => Index::None, 493 | DataType::Text => Index::Text(BTreeMap::new()), 494 | DataType::Real => Index::None, 495 | DataType::Invalid => Index::None, 496 | DataType::None => Index::None, 497 | }; 498 | 499 | Column { 500 | column_name: name, 501 | datatype: dt, 502 | is_pk, 503 | not_null, 504 | is_unique, 505 | is_indexed: if is_pk { true } else { false }, 506 | index, 507 | } 508 | } 509 | 510 | pub fn get_mut_index(&mut self) -> &mut Index { 511 | return &mut self.index; 512 | } 513 | } 514 | 515 | /// The schema for each SQL column index in every table is represented in memory 516 | /// by following structure 517 | #[derive(Serialize, Deserialize, PartialEq, Debug)] 518 | pub enum Index { 519 | Integer(BTreeMap), 520 | Text(BTreeMap), 521 | None, 522 | } 523 | 524 | /// The schema for each SQL row in every table is represented in memory 525 | /// by following structure 526 | /// 527 | /// This is an enum representing each of the available types organized in a BTreeMap 528 | /// data structure, using the ROWID and key and each corresponding type as value 529 | #[derive(Serialize, Deserialize, PartialEq, Debug)] 530 | pub enum Row { 531 | Integer(BTreeMap), 532 | Text(BTreeMap), 533 | Real(BTreeMap), 534 | Bool(BTreeMap), 535 | None, 536 | } 537 | 538 | impl Row { 539 | fn get_serialized_col_data(&self) -> Vec { 540 | match self { 541 | Row::Integer(cd) => cd.iter().map(|(_i, v)| v.to_string()).collect(), 542 | Row::Real(cd) => cd.iter().map(|(_i, v)| v.to_string()).collect(), 543 | Row::Text(cd) => cd.iter().map(|(_i, v)| v.to_string()).collect(), 544 | Row::Bool(cd) => cd.iter().map(|(_i, v)| v.to_string()).collect(), 545 | Row::None => panic!("Found None in columns"), 546 | } 547 | } 548 | 549 | fn count(&self) -> usize { 550 | match self { 551 | Row::Integer(cd) => cd.len(), 552 | Row::Real(cd) => cd.len(), 553 | Row::Text(cd) => cd.len(), 554 | Row::Bool(cd) => cd.len(), 555 | Row::None => panic!("Found None in columns"), 556 | } 557 | } 558 | } 559 | 560 | #[cfg(test)] 561 | mod tests { 562 | use super::*; 563 | use sqlparser::dialect::SQLiteDialect; 564 | use sqlparser::parser::Parser; 565 | 566 | #[test] 567 | fn datatype_display_trait_test() { 568 | let integer = DataType::Integer; 569 | let text = DataType::Text; 570 | let real = DataType::Real; 571 | let boolean = DataType::Bool; 572 | let none = DataType::None; 573 | let invalid = DataType::Invalid; 574 | 575 | assert_eq!(format!("{}", integer), "Integer"); 576 | assert_eq!(format!("{}", text), "Text"); 577 | assert_eq!(format!("{}", real), "Real"); 578 | assert_eq!(format!("{}", boolean), "Boolean"); 579 | assert_eq!(format!("{}", none), "None"); 580 | assert_eq!(format!("{}", invalid), "Invalid"); 581 | } 582 | 583 | #[test] 584 | fn create_new_table_test() { 585 | let query_statement = "CREATE TABLE contacts ( 586 | id INTEGER PRIMARY KEY, 587 | first_name TEXT NOT NULL, 588 | last_name TEXT NOT NULl, 589 | email TEXT NOT NULL UNIQUE, 590 | active BOOL, 591 | score REAL 592 | );"; 593 | let dialect = SQLiteDialect {}; 594 | let mut ast = Parser::parse_sql(&dialect, &query_statement).unwrap(); 595 | if ast.len() > 1 { 596 | panic!("Expected a single query statement, but there are more then 1.") 597 | } 598 | let query = ast.pop().unwrap(); 599 | 600 | let create_query = CreateQuery::new(&query).unwrap(); 601 | 602 | let table = Table::new(create_query); 603 | 604 | assert_eq!(table.columns.len(), 6); 605 | assert_eq!(table.last_rowid, 0); 606 | 607 | let id_column = "id".to_string(); 608 | if let Some(column) = table 609 | .columns 610 | .iter() 611 | .filter(|c| c.column_name == id_column) 612 | .collect::>() 613 | .first() 614 | { 615 | assert_eq!(column.is_pk, true); 616 | assert_eq!(column.datatype, DataType::Integer); 617 | } else { 618 | panic!("column not found"); 619 | } 620 | } 621 | 622 | #[test] 623 | fn print_table_schema_test() { 624 | let query_statement = "CREATE TABLE contacts ( 625 | id INTEGER PRIMARY KEY, 626 | first_name TEXT NOT NULL, 627 | last_name TEXT NOT NULl 628 | );"; 629 | let dialect = SQLiteDialect {}; 630 | let mut ast = Parser::parse_sql(&dialect, &query_statement).unwrap(); 631 | if ast.len() > 1 { 632 | panic!("Expected a single query statement, but there are more then 1.") 633 | } 634 | let query = ast.pop().unwrap(); 635 | 636 | let create_query = CreateQuery::new(&query).unwrap(); 637 | 638 | let table = Table::new(create_query); 639 | let lines_printed = table.print_table_schema(); 640 | assert_eq!(lines_printed, Ok(9)); 641 | } 642 | } 643 | -------------------------------------------------------------------------------- /src/sql/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod parser; 2 | // pub mod tokenizer; 3 | pub mod db; 4 | 5 | use parser::create::CreateQuery; 6 | use parser::insert::InsertQuery; 7 | 8 | use sqlparser::ast::Statement; 9 | use sqlparser::dialect::SQLiteDialect; 10 | use sqlparser::parser::{Parser, ParserError}; 11 | 12 | use crate::error::{Result, SQLRiteError}; 13 | use crate::sql::db::database::Database; 14 | use crate::sql::db::table::Table; 15 | 16 | #[derive(Debug, PartialEq)] 17 | pub enum SQLCommand { 18 | Insert(String), 19 | Delete(String), 20 | Update(String), 21 | CreateTable(String), 22 | Select(String), 23 | Unknown(String), 24 | } 25 | 26 | impl SQLCommand { 27 | pub fn new(command: String) -> SQLCommand { 28 | let v = command.split(" ").collect::>(); 29 | match v[0] { 30 | "insert" => SQLCommand::Insert(command), 31 | "update" => SQLCommand::Update(command), 32 | "delete" => SQLCommand::Delete(command), 33 | "create" => SQLCommand::CreateTable(command), 34 | "select" => SQLCommand::Select(command), 35 | _ => SQLCommand::Unknown(command), 36 | } 37 | } 38 | } 39 | 40 | /// Performs initial parsing of SQL Statement using sqlparser-rs 41 | pub fn process_command(query: &str, db: &mut Database) -> Result { 42 | let dialect = SQLiteDialect {}; 43 | let message: String; 44 | let mut ast = Parser::parse_sql(&dialect, &query).map_err(SQLRiteError::from)?; 45 | 46 | if ast.len() > 1 { 47 | return Err(SQLRiteError::SqlError(ParserError::ParserError(format!( 48 | "Expected a single query statement, but there are {}", 49 | ast.len() 50 | )))); 51 | } 52 | 53 | let query = ast.pop().unwrap(); 54 | 55 | // Initialy only implementing some basic SQL Statements 56 | match query { 57 | Statement::CreateTable { .. } => { 58 | let create_query = CreateQuery::new(&query); 59 | match create_query { 60 | Ok(payload) => { 61 | let table_name = payload.table_name.clone(); 62 | // Checking if table already exists, after parsing CREATE TABLE query 63 | match db.contains_table(table_name.to_string()) { 64 | true => { 65 | return Err(SQLRiteError::Internal( 66 | "Cannot create, table already exists.".to_string(), 67 | )); 68 | } 69 | false => { 70 | let table = Table::new(payload); 71 | let _ = table.print_table_schema(); 72 | db.tables.insert(table_name.to_string(), table); 73 | // Iterate over everything. 74 | // for (table_name, _) in &db.tables { 75 | // println!("{}" , table_name); 76 | // } 77 | message = String::from("CREATE TABLE Statement executed."); 78 | } 79 | } 80 | } 81 | Err(err) => return Err(err), 82 | } 83 | } 84 | Statement::Insert { .. } => { 85 | let insert_query = InsertQuery::new(&query); 86 | match insert_query { 87 | Ok(payload) => { 88 | let table_name = payload.table_name; 89 | let columns = payload.columns; 90 | let values = payload.rows; 91 | 92 | // println!("table_name = {:?}\n cols = {:?}\n vals = {:?}", table_name, columns, values); 93 | // Checking if Table exists in Database 94 | match db.contains_table(table_name.to_string()) { 95 | true => { 96 | let db_table = db.get_table_mut(table_name.to_string()).unwrap(); 97 | // Checking if columns on INSERT query exist on Table 98 | match columns 99 | .iter() 100 | .all(|column| db_table.contains_column(column.to_string())) 101 | { 102 | true => { 103 | for value in &values { 104 | // Checking if number of columns in query are the same as number of values 105 | if columns.len() != value.len() { 106 | return Err(SQLRiteError::Internal(format!( 107 | "{} values for {} columns", 108 | value.len(), 109 | columns.len() 110 | ))); 111 | } 112 | match db_table.validate_unique_constraint(&columns, value) { 113 | Ok(()) => { 114 | // No unique constraint violation, moving forward with inserting row 115 | db_table.insert_row(&columns, &value); 116 | } 117 | Err(err) => { 118 | return Err(SQLRiteError::Internal(format!( 119 | "Unique key constaint violation: {}", 120 | err 121 | ))) 122 | } 123 | } 124 | } 125 | } 126 | false => { 127 | return Err(SQLRiteError::Internal( 128 | "Cannot insert, some of the columns do not exist" 129 | .to_string(), 130 | )); 131 | } 132 | } 133 | db_table.print_table_data(); 134 | } 135 | false => { 136 | return Err(SQLRiteError::Internal("Table doesn't exist".to_string())) 137 | } 138 | } 139 | } 140 | Err(err) => return Err(err), 141 | } 142 | 143 | message = String::from("INSERT Statement executed.") 144 | } 145 | Statement::Query(_query) => message = String::from("SELECT Statement executed."), 146 | // Statement::Insert { .. } => message = String::from("INSERT Statement executed."), 147 | Statement::Delete { .. } => message = String::from("DELETE Statement executed."), 148 | _ => { 149 | return Err(SQLRiteError::NotImplemented( 150 | "SQL Statement not supported yet.".to_string(), 151 | )) 152 | } 153 | }; 154 | 155 | Ok(message) 156 | } 157 | 158 | #[cfg(test)] 159 | mod tests { 160 | use super::*; 161 | 162 | #[test] 163 | fn process_command_select_test() { 164 | let inputed_query = String::from("SELECT * from users;"); 165 | let mut db = Database::new("tempdb".to_string()); 166 | 167 | let _ = match process_command(&inputed_query, &mut db) { 168 | Ok(response) => assert_eq!(response, "SELECT Statement executed."), 169 | Err(err) => { 170 | eprintln!("Error: {}", err); 171 | assert!(false) 172 | } 173 | }; 174 | } 175 | 176 | #[test] 177 | fn process_command_insert_test() { 178 | // Creating temporary database 179 | let mut db = Database::new("tempdb".to_string()); 180 | 181 | // Creating temporary table for testing purposes 182 | let query_statement = "CREATE TABLE users ( 183 | id INTEGER PRIMARY KEY, 184 | name TEXT 185 | );"; 186 | let dialect = SQLiteDialect {}; 187 | let mut ast = Parser::parse_sql(&dialect, &query_statement).unwrap(); 188 | if ast.len() > 1 { 189 | panic!("Expected a single query statement, but there are more then 1.") 190 | } 191 | let query = ast.pop().unwrap(); 192 | let create_query = CreateQuery::new(&query).unwrap(); 193 | 194 | // Inserting table into database 195 | db.tables.insert( 196 | create_query.table_name.to_string(), 197 | Table::new(create_query), 198 | ); 199 | 200 | // Inserting data into table 201 | let insert_query = String::from("INSERT INTO users (name) Values ('josh');"); 202 | let _ = match process_command(&insert_query, &mut db) { 203 | Ok(response) => assert_eq!(response, "INSERT Statement executed."), 204 | Err(err) => { 205 | eprintln!("Error: {}", err); 206 | assert!(false) 207 | } 208 | }; 209 | } 210 | 211 | #[test] 212 | fn process_command_insert_no_pk_test() { 213 | // Creating temporary database 214 | let mut db = Database::new("tempdb".to_string()); 215 | 216 | // Creating temporary table for testing purposes 217 | let query_statement = "CREATE TABLE users ( 218 | name TEXT 219 | );"; 220 | let dialect = SQLiteDialect {}; 221 | let mut ast = Parser::parse_sql(&dialect, &query_statement).unwrap(); 222 | if ast.len() > 1 { 223 | panic!("Expected a single query statement, but there are more then 1.") 224 | } 225 | let query = ast.pop().unwrap(); 226 | let create_query = CreateQuery::new(&query).unwrap(); 227 | 228 | // Inserting table into database 229 | db.tables.insert( 230 | create_query.table_name.to_string(), 231 | Table::new(create_query), 232 | ); 233 | 234 | // Inserting data into table 235 | let insert_query = String::from("INSERT INTO users (name) Values ('josh');"); 236 | let _ = match process_command(&insert_query, &mut db) { 237 | Ok(response) => assert_eq!(response, "INSERT Statement executed."), 238 | Err(err) => { 239 | eprintln!("Error: {}", err); 240 | assert!(false) 241 | } 242 | }; 243 | } 244 | 245 | #[test] 246 | fn process_command_delete_test() { 247 | let inputed_query = String::from("DELETE FROM users WHERE id=1;"); 248 | let mut db = Database::new("tempdb".to_string()); 249 | 250 | let _ = match process_command(&inputed_query, &mut db) { 251 | Ok(response) => assert_eq!(response, "DELETE Statement executed."), 252 | Err(err) => { 253 | eprintln!("Error: {}", err); 254 | assert!(false) 255 | } 256 | }; 257 | } 258 | 259 | #[test] 260 | fn process_command_not_implemented_test() { 261 | let inputed_query = String::from("UPDATE users SET name='josh' where id=1;"); 262 | let mut db = Database::new("tempdb".to_string()); 263 | let expected = Err(SQLRiteError::NotImplemented( 264 | "SQL Statement not supported yet.".to_string(), 265 | )); 266 | 267 | let result = process_command(&inputed_query, &mut db).map_err(|e| e); 268 | assert_eq!(result, expected); 269 | } 270 | } 271 | -------------------------------------------------------------------------------- /src/sql/parser/create.rs: -------------------------------------------------------------------------------- 1 | use sqlparser::ast::{ColumnOption, DataType, Statement}; 2 | 3 | use crate::error::{Result, SQLRiteError}; 4 | 5 | /// The schema for each SQL column in every table is represented by 6 | /// the following structure after parsed and tokenized 7 | #[derive(PartialEq, Debug)] 8 | pub struct ParsedColumn { 9 | /// Name of the column 10 | pub name: String, 11 | /// Datatype of the column in String format 12 | pub datatype: String, 13 | /// Value representing if column is PRIMARY KEY 14 | pub is_pk: bool, 15 | /// Value representing if column was declared with the NOT NULL Constraint 16 | pub not_null: bool, 17 | /// Value representing if column was declared with the UNIQUE Constraint 18 | pub is_unique: bool, 19 | } 20 | 21 | /// The following structure represents a CREATE TABLE query already parsed 22 | /// and broken down into name and a Vector of `ParsedColumn` metadata 23 | /// 24 | #[derive(Debug)] 25 | pub struct CreateQuery { 26 | /// name of table after parking and tokenizing of query 27 | pub table_name: String, 28 | /// Vector of `ParsedColumn` type with column metadata information 29 | pub columns: Vec, 30 | } 31 | 32 | impl CreateQuery { 33 | pub fn new(statement: &Statement) -> Result { 34 | match statement { 35 | // Confirming the Statement is sqlparser::ast:Statement::CreateTable 36 | Statement::CreateTable { 37 | name, 38 | columns, 39 | constraints: _constraints, 40 | with_options: _with_options, 41 | external: _external, 42 | file_format: _file_format, 43 | location: _location, 44 | .. 45 | } => { 46 | let table_name = name; 47 | let mut parsed_columns: Vec = vec![]; 48 | 49 | // Iterating over the columns returned form the Parser::parse:sql 50 | // in the mod sql 51 | for col in columns { 52 | let name = col.name.to_string(); 53 | 54 | // Checks if columm already added to parsed_columns, if so, returns an error 55 | if parsed_columns.iter().any(|col| col.name == name) { 56 | return Err(SQLRiteError::Internal(format!( 57 | "Duplicate column name: {}", 58 | &name 59 | ))); 60 | } 61 | 62 | // Parsing each column for it data type 63 | // For now only accepting basic data types 64 | let datatype = match &col.data_type { 65 | DataType::SmallInt(_) => "Integer", 66 | DataType::Int(_) => "Integer", 67 | DataType::BigInt(_) => "Integer", 68 | DataType::Boolean => "Bool", 69 | DataType::Text => "Text", 70 | DataType::Varchar(_bytes) => "Text", 71 | DataType::Real => "Real", 72 | DataType::Float(_precision) => "Real", 73 | DataType::Double => "Real", 74 | DataType::Decimal(_precision1, _precision2) => "Real", 75 | _ => { 76 | eprintln!("not matched on custom type"); 77 | "Invalid" 78 | } 79 | }; 80 | 81 | // checking if column is PRIMARY KEY 82 | let mut is_pk: bool = false; 83 | // chekcing if column is UNIQUE 84 | let mut is_unique: bool = false; 85 | // chekcing if column is NULLABLE 86 | let mut not_null: bool = false; 87 | for column_option in &col.options { 88 | match column_option.option { 89 | ColumnOption::Unique { is_primary } => { 90 | // For now, only Integer and Text types can be PRIMERY KEY and Unique 91 | // Therefore Indexed. 92 | if datatype != "Real" && datatype != "Bool" { 93 | is_pk = is_primary; 94 | if is_primary { 95 | // Checks if table being created already has a PRIMARY KEY, if so, returns an error 96 | if parsed_columns.iter().any(|col| col.is_pk == true) { 97 | return Err(SQLRiteError::Internal(format!( 98 | "Table '{}' has more than one primary key", 99 | &table_name 100 | ))); 101 | } 102 | not_null = true; 103 | } 104 | is_unique = true; 105 | } 106 | } 107 | ColumnOption::NotNull => { 108 | not_null = true; 109 | } 110 | _ => (), 111 | }; 112 | } 113 | 114 | parsed_columns.push(ParsedColumn { 115 | name, 116 | datatype: datatype.to_string(), 117 | is_pk, 118 | not_null, 119 | is_unique, 120 | }); 121 | } 122 | // TODO: Handle constraints, 123 | // Default value and others. 124 | for constraint in _constraints { 125 | println!("{:?}", constraint); 126 | } 127 | return Ok(CreateQuery { 128 | table_name: table_name.to_string(), 129 | columns: parsed_columns, 130 | }); 131 | } 132 | 133 | _ => return Err(SQLRiteError::Internal("Error parsing query".to_string())), 134 | } 135 | } 136 | } 137 | 138 | #[cfg(test)] 139 | mod tests { 140 | use super::*; 141 | use crate::sql::*; 142 | 143 | #[test] 144 | fn create_table_validate_tablename_test() { 145 | let sql_input = String::from( 146 | "CREATE TABLE contacts ( 147 | id INTEGER PRIMARY KEY, 148 | first_name TEXT NOT NULL, 149 | last_name TEXT NOT NULl, 150 | email TEXT NOT NULL UNIQUE 151 | );", 152 | ); 153 | let expected_table_name = String::from("contacts"); 154 | 155 | let dialect = SQLiteDialect {}; 156 | let mut ast = Parser::parse_sql(&dialect, &sql_input).unwrap(); 157 | 158 | assert!(ast.len() == 1, "ast has more then one Statement"); 159 | 160 | let query = ast.pop().unwrap(); 161 | 162 | // Initialy only implementing some basic SQL Statements 163 | match query { 164 | Statement::CreateTable { .. } => { 165 | let result = CreateQuery::new(&query); 166 | match result { 167 | Ok(payload) => { 168 | assert_eq!(payload.table_name, expected_table_name); 169 | } 170 | Err(_) => assert!( 171 | false, 172 | "an error occured during parsing CREATE TABLE Statement" 173 | ), 174 | } 175 | } 176 | _ => (), 177 | }; 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /src/sql/parser/insert.rs: -------------------------------------------------------------------------------- 1 | use sqlparser::ast::{Expr, Query, SetExpr, Statement, Value, Values}; 2 | 3 | use crate::error::{Result, SQLRiteError}; 4 | 5 | /// The following structure represents a INSERT query already parsed 6 | /// and broken down into `table_name` a `Vec` representing the `Columns` 7 | /// and `Vec>` representing the list of `Rows` to be inserted 8 | #[derive(Debug)] 9 | pub struct InsertQuery { 10 | pub table_name: String, 11 | pub columns: Vec, 12 | pub rows: Vec>, 13 | } 14 | 15 | impl InsertQuery { 16 | pub fn new(statement: &Statement) -> Result { 17 | #[allow(unused_assignments)] 18 | let mut tname: Option = None; 19 | let mut columns: Vec = vec![]; 20 | let mut all_values: Vec> = vec![]; 21 | 22 | match statement { 23 | Statement::Insert { 24 | table_name, 25 | columns: cols, 26 | source, 27 | .. 28 | } => { 29 | tname = Some(table_name.to_string()); 30 | for col in cols { 31 | columns.push(col.to_string()); 32 | } 33 | 34 | match &**source { 35 | Query { 36 | body, 37 | order_by: _order_by, 38 | limit: _limit, 39 | offset: _offset, 40 | fetch: _fetch, 41 | .. 42 | } => { 43 | if let SetExpr::Values(values) = body { 44 | #[allow(irrefutable_let_patterns)] 45 | if let Values(expressions) = values { 46 | for i in expressions { 47 | let mut value_set: Vec = vec![]; 48 | for e in i { 49 | match e { 50 | Expr::Value(v) => match v { 51 | Value::Number(n, _) => { 52 | value_set.push(n.to_string()); 53 | } 54 | Value::Boolean(b) => match *b { 55 | true => value_set.push("true".to_string()), 56 | false => value_set.push("false".to_string()), 57 | }, 58 | Value::SingleQuotedString(sqs) => { 59 | value_set.push(sqs.to_string()); 60 | } 61 | Value::Null => { 62 | value_set.push("Null".to_string()); 63 | } 64 | _ => {} 65 | }, 66 | Expr::Identifier(i) => { 67 | value_set.push(i.to_string()); 68 | } 69 | _ => {} 70 | } 71 | } 72 | all_values.push(value_set); 73 | } 74 | } 75 | } 76 | } 77 | } 78 | } 79 | _ => { 80 | return Err(SQLRiteError::Internal( 81 | "Error parsing insert query".to_string(), 82 | )) 83 | } 84 | } 85 | 86 | match tname { 87 | Some(t) => Ok(InsertQuery { 88 | table_name: t, 89 | columns, 90 | rows: all_values, 91 | }), 92 | None => Err(SQLRiteError::Internal( 93 | "Error parsing insert query".to_string(), 94 | )), 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/sql/parser/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod create; 2 | pub mod insert; 3 | -------------------------------------------------------------------------------- /src/sql/tokenizer.rs: -------------------------------------------------------------------------------- 1 | // use crate::error::Result; 2 | 3 | // use sqlparser::ast::Statement; 4 | 5 | // pub fn tokenize_statement(statement: &str) -> Result { 6 | // unimplemented!(); 7 | // } 8 | --------------------------------------------------------------------------------