├── .github └── workflows │ ├── release.yml │ └── rust.yml ├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md ├── install.sh ├── old_banner.png ├── rayql-engine ├── Cargo.toml └── src │ ├── error.rs │ ├── lib.rs │ ├── schema │ ├── error.rs │ ├── mod.rs │ ├── parser.rs │ ├── tokenizer.rs │ └── utils.rs │ ├── sql │ ├── error.rs │ ├── fn_helpers.rs │ ├── function.rs │ ├── mod.rs │ └── to_sql.rs │ ├── types.rs │ └── value.rs ├── rayql-logo.svg ├── rayql-wasm ├── Cargo.toml └── src │ └── lib.rs └── src ├── lib.rs └── main.rs /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | on: 2 | release: 3 | types: [created] 4 | 5 | jobs: 6 | release: 7 | name: release ${{ matrix.target }} 8 | runs-on: ubuntu-latest 9 | strategy: 10 | fail-fast: false 11 | matrix: 12 | include: 13 | - target: x86_64-pc-windows-gnu 14 | archive: zip 15 | - target: x86_64-unknown-linux-musl 16 | archive: tar.gz tar.xz tar.zst 17 | - target: x86_64-apple-darwin 18 | archive: zip 19 | steps: 20 | - uses: actions/checkout@master 21 | - name: Compile and release 22 | uses: rust-build/rust-build.action@v1.4.5 23 | env: 24 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 25 | with: 26 | RUSTTARGET: ${{ matrix.target }} 27 | ARCHIVE_TYPES: ${{ matrix.archive }} 28 | STATIC_LINKING: true 29 | TOOLCHAIN_VERSION: 1.74.0 30 | -------------------------------------------------------------------------------- /.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 | - uses: actions/checkout@v3 19 | - name: Build 20 | run: cargo build --verbose 21 | - name: Run tests 22 | run: cargo test --verbose 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | debug/ 4 | target/ 5 | 6 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 7 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 8 | Cargo.lock 9 | 10 | # These are backup files generated by rustfmt 11 | **/*.rs.bk 12 | 13 | # MSVC Windows builds of rustc generate these, which store debugging information 14 | *.pdb 15 | 16 | .DS_Store 17 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rayql" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [workspace] 7 | members = [ 8 | "rayql-engine", 9 | "rayql-wasm", 10 | ] 11 | resolver = "2" 12 | 13 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 14 | 15 | [workspace.dependencies] 16 | thiserror = "1.0" 17 | rayql-engine = { path = "rayql-engine" } 18 | wasm-bindgen = "0.2" 19 | 20 | [target.'cfg(not(target_arch = "wasm32"))'.dependencies] 21 | tokio = { version = "1.36.0", features = ["full"] } 22 | clap = { version = "4.5.3", features = ["derive"] } 23 | rayql-engine.workspace = true 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Harsh Singh 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RayQL 2 | 3 |

4 | RayQL Logo 5 |
6 |
7 | RayQL is a schema definition and query language for SQLite. 8 |
9 |
10 |

11 | 12 | ![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/harshdoesdev/rayql/.github%2Fworkflows%2Frust.yml) 13 | ![GitHub License](https://img.shields.io/github/license/harshdoesdev/rayql) 14 | ![Discord](https://img.shields.io/discord/1225854949485711451) 15 | 16 | [Join our Discord Channel](https://discord.gg/4re5ShTshg) 17 | 18 | ## Online Editor 19 | 20 | You can try RayQL using the [Online RayQL editor](https://harshdoesdev.github.io/rayql-studio/). 21 | 22 | ## Installation 23 | 24 | You can install RayQL by running the following command in your terminal: 25 | 26 | ```bash 27 | curl -s https://rayql.com/install.sh | sh 28 | ``` 29 | 30 | ## Schema Definition 31 | 32 | You can define your database schema by creating a RayQL file called `schema.rayql`. 33 | 34 | For example, it might look something like this: 35 | 36 | ```rayql 37 | # Enum for user types 38 | 39 | enum user_type { 40 | admin 41 | developer 42 | normal 43 | } 44 | 45 | # Model declaration for 'user' 46 | 47 | model user { 48 | id: int primary_key auto_increment, 49 | username: str unique, 50 | email: str unique, # this is an inline comment 51 | phone_number: str?, 52 | user_type: user_type default(user_type.normal) 53 | } 54 | 55 | # Model declaration for 'post' 56 | 57 | model post { 58 | id: int primary_key auto_increment, 59 | title: str default('New Post'), 60 | content: str, 61 | author_id: int foreign_key(user.id), 62 | created_at: timestamp default(now()), 63 | } 64 | ``` 65 | 66 | Then, when you run the `rayql print` command, it will generate and output the SQL equivalent of that model, which for the above example should look something like this: 67 | 68 | ```sql 69 | CREATE TABLE IF NOT EXISTS user ( 70 | id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, 71 | username TEXT NOT NULL UNIQUE, 72 | email TEXT NOT NULL UNIQUE, 73 | phone_number TEXT NULL, 74 | user_type TEXT NOT NULL CHECK(user_type IN ('admin', 'developer', 'normal')) DEFAULT 'normal' 75 | ); 76 | 77 | CREATE TABLE IF NOT EXISTS post ( 78 | id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, 79 | title TEXT NOT NULL DEFAULT 'New Post', 80 | content TEXT NOT NULL, 81 | author_id INTEGER NOT NULL, 82 | created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, 83 | FOREIGN KEY (author_id) REFERENCES user(id) 84 | ); 85 | ``` 86 | 87 | ## Todo 88 | 89 | - [x] Basic Schema Parser 90 | - [x] `rayql print` command 91 | - [ ] Database commands 92 | - [ ] `rayql db push` and `rayql db pull` commands 93 | - [ ] `rayql db migrate` command 94 | - [ ] Typescript Types generator 95 | - [ ] Query Parser and Handler 96 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Set text colors 4 | RED='\033[0;31m' 5 | GREEN='\033[0;32m' 6 | YELLOW='\033[0;33m' 7 | NC='\033[0m' # No Color 8 | 9 | # Set the latest release version 10 | LATEST_VERSION=$(curl -s "https://api.github.com/repos/harshdoesdev/rayql/releases/latest" | grep -o '"tag_name": "v.*"' | cut -d'"' -f4) 11 | 12 | # Determine the operating system and architecture 13 | OS=$(uname -s) 14 | ARCH=$(uname -m) 15 | 16 | # Set the file extension based on the operating system 17 | if [ "$OS" = "Darwin" ]; then 18 | FILE_EXTENSION="apple-darwin.zip" 19 | elif [ "$OS" = "Linux" ]; then 20 | if [ "$ARCH" = "x86_64" ]; then 21 | FILE_EXTENSION="unknown-linux-musl.tar.gz" 22 | else 23 | echo "${RED}Unsupported architecture: $ARCH${NC}" 24 | exit 1 25 | fi 26 | else 27 | echo "${RED}Unsupported operating system: $OS${NC}" 28 | exit 1 29 | fi 30 | 31 | # Get download URL 32 | DOWNLOAD_URL=$(curl -s "https://api.github.com/repos/harshdoesdev/rayql/releases/latest" | grep -o "\"browser_download_url\": *\"[^\"]*${FILE_EXTENSION}\"" | cut -d '"' -f 4) 33 | 34 | # Print the download URL 35 | echo -e "⬇️ ${YELLOW}Downloading rayql version $LATEST_VERSION for $OS...${NC}" 36 | 37 | # Download the binary 38 | curl -LO "$DOWNLOAD_URL" 39 | 40 | # Extract the binary if it's a tarball or zip 41 | if [[ "$DOWNLOAD_URL" == *".tar.gz" ]]; then 42 | tar -xzf "rayql_${LATEST_VERSION}_${ARCH}-${OS}.${FILE_EXTENSION}" 43 | BINARY_PATH="./rayql_${LATEST_VERSION}_${ARCH}-${OS}/rayql" 44 | elif [[ "$DOWNLOAD_URL" == *".zip" ]]; then 45 | ZIP_FILE=$(basename "$DOWNLOAD_URL") 46 | unzip "$ZIP_FILE" 47 | BINARY_PATH="./rayql" 48 | else 49 | echo "${RED}Unsupported file format for extraction${NC}" 50 | exit 1 51 | fi 52 | 53 | # Make the binary executable 54 | chmod +x "$BINARY_PATH" 55 | 56 | # Move the binary to a directory in the user's PATH 57 | echo -e "🚀 ${YELLOW}Installing rayql into /usr/local/bin...${NC}" 58 | sudo mv "$BINARY_PATH" /usr/local/bin/rayql 59 | 60 | # Check if rayql binary exists in PATH 61 | if command -v rayql &>/dev/null; then 62 | # Display installation complete message 63 | echo "" 64 | echo -e "✅ ${GREEN}rayql ${LATEST_VERSION} has been successfully installed.${NC}" 65 | else 66 | echo "${RED}❌ Error: Failed to install rayql.${NC}" 67 | exit 1 68 | fi 69 | 70 | # Clean up downloaded zip file 71 | rm -f "$ZIP_FILE" 72 | -------------------------------------------------------------------------------- /old_banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harshdoesdev/rayql/b801da0599fd451c379fa1d4bd56aab38bd4ad72/old_banner.png -------------------------------------------------------------------------------- /rayql-engine/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rayql-engine" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | thiserror.workspace = true 10 | -------------------------------------------------------------------------------- /rayql-engine/src/error.rs: -------------------------------------------------------------------------------- 1 | use rayql::{ 2 | schema::error::{ParseError, TokenizationError}, 3 | sql::error::{FunctionError, ToSQLError}, 4 | }; 5 | 6 | pub fn pretty_error_message(error: &ParseError, code: &str) -> String { 7 | match error { 8 | ParseError::TokenizationError(tokenization_error) => { 9 | pretty_tokenization_error_message(tokenization_error, code) 10 | } 11 | ParseError::UnexpectedToken { 12 | token, 13 | line_number, 14 | column, 15 | } => format!( 16 | "Unexpected token {} at line {}, column {}", 17 | token, line_number, column 18 | ), 19 | ParseError::InvalidReference { 20 | entity, 21 | property, 22 | line_number, 23 | column, 24 | } => format!( 25 | "Invalid Reference: Cannot access '{}' of '{}' at line {}, column {}", 26 | property, entity, line_number, column 27 | ), 28 | ParseError::IdentifierAlreadyInUse { 29 | identifier, 30 | line_number, 31 | column, 32 | } => format!( 33 | "Cannot re-define '{}' at line {}, column {}", 34 | identifier, line_number, column 35 | ), 36 | ParseError::UnexpectedEndOfTokens => "Unexpected end of tokens".to_string(), 37 | ParseError::FieldAlreadyExistsOnModel { field, model, line_number, column } => format!( 38 | "Field '{field}' already exists on model '{model}', cannot redeclare it at line {}, column {}.", 39 | line_number, column, 40 | ), 41 | ParseError::EnumVariantAlreadyExists { r#enum, variant, line_number, column } => format!( 42 | "Enum variant '{variant}' already exists on enum '{enum}', cannot redeclare it at line {}, column {}.", 43 | line_number, column, 44 | ), 45 | } 46 | } 47 | 48 | fn pretty_tokenization_error_message( 49 | tokenization_error: &TokenizationError, 50 | _code: &str, 51 | ) -> String { 52 | match tokenization_error { 53 | TokenizationError::UnexpectedCharacter { char, line, column } 54 | | TokenizationError::UnknownEscapeSequence { char, line, column } => { 55 | format!( 56 | "Unexpected character '{}' at line {}, column {}", 57 | char, line, column 58 | ) 59 | } 60 | TokenizationError::StringLiteralOpened { line, column } => { 61 | format!("String literal opened at line {}, column {}", line, column) 62 | } 63 | TokenizationError::IdentifierBeginsWithDigit { 64 | identifier, 65 | line, 66 | column, 67 | } => format!( 68 | "Identifier '{identifier}' cannot begin with a digit at line {}, column {}", 69 | line, column 70 | ), 71 | TokenizationError::UnexpectedEndOfInput => "Unexpected end of input".to_string(), 72 | } 73 | } 74 | 75 | pub fn pretty_to_sql_error_message(error: ToSQLError, code: &str) -> String { 76 | match error { 77 | ToSQLError::FunctionError { 78 | source, 79 | line_number, 80 | column, 81 | } => pretty_function_error_message(source, code, line_number, column), 82 | e => e.to_string(), 83 | } 84 | } 85 | 86 | fn pretty_function_error_message( 87 | error: FunctionError, 88 | _code: &str, 89 | line_number: usize, 90 | column: usize, 91 | ) -> String { 92 | match error { 93 | FunctionError::InvalidArgument(msg) => { 94 | format!( 95 | "Invalid argument: {} at line {}, column {}", 96 | msg, line_number, column, 97 | ) 98 | } 99 | FunctionError::MissingArgument => format!( 100 | "Missing argument to function at line {}, column {}", 101 | line_number, column, 102 | ), 103 | FunctionError::ExpectsExactlyOneArgument(func) => { 104 | format!( 105 | "{func} takes exactly one argument, error at line {}, column {}", 106 | line_number, column 107 | ) 108 | } 109 | FunctionError::UndefinedFunction(func) => { 110 | format!( 111 | "Undefined function '{func}' called at line {}, column {}", 112 | line_number, column 113 | ) 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /rayql-engine/src/lib.rs: -------------------------------------------------------------------------------- 1 | // TODO: rename this to rayql_engine 2 | extern crate self as rayql; 3 | 4 | pub mod schema; 5 | pub use schema::Schema; 6 | pub mod error; 7 | pub mod sql; 8 | pub mod types; 9 | mod value; 10 | pub use value::Value; 11 | -------------------------------------------------------------------------------- /rayql-engine/src/schema/error.rs: -------------------------------------------------------------------------------- 1 | use rayql::schema::tokenizer::Token; 2 | 3 | #[derive(thiserror::Error, Debug, PartialEq)] 4 | pub enum TokenizationError { 5 | #[error("Unexpected character '{char}' at line {line}, column {column}")] 6 | UnexpectedCharacter { 7 | char: char, 8 | line: usize, 9 | column: usize, 10 | }, 11 | #[error("Unknown Escape Sequence '{char}' at line {line}, column {column}")] 12 | UnknownEscapeSequence { 13 | char: char, 14 | line: usize, 15 | column: usize, 16 | }, 17 | #[error("String literal opened at line {line}, column {column}")] 18 | StringLiteralOpened { line: usize, column: usize }, 19 | #[error("Identifier '{identifier}' cannot begin with a digit at line {line}, column {column}")] 20 | IdentifierBeginsWithDigit { 21 | identifier: String, 22 | line: usize, 23 | column: usize, 24 | }, 25 | #[error("Unexpected End of Input")] 26 | UnexpectedEndOfInput, 27 | } 28 | 29 | #[derive(thiserror::Error, Debug, PartialEq)] 30 | pub enum ParseError { 31 | #[error("Tokenization Error: {0}")] 32 | TokenizationError(#[from] TokenizationError), 33 | #[error("Unexpected Token")] 34 | UnexpectedToken { 35 | token: Token, 36 | line_number: usize, 37 | column: usize, 38 | }, 39 | #[error("Identifier is already in use")] 40 | IdentifierAlreadyInUse { 41 | identifier: String, 42 | line_number: usize, 43 | column: usize, 44 | }, 45 | #[error("Field with name '{field}' already exists on model '{model}'")] 46 | FieldAlreadyExistsOnModel { 47 | field: String, 48 | model: String, 49 | line_number: usize, 50 | column: usize, 51 | }, 52 | #[error("Variant '{variant}' already exists on enum '{r#enum}'")] 53 | EnumVariantAlreadyExists { 54 | variant: String, 55 | r#enum: String, 56 | line_number: usize, 57 | column: usize, 58 | }, 59 | #[error("Invalid reference, cannot access '{entity}' of '{property}'")] 60 | InvalidReference { 61 | entity: String, 62 | property: String, 63 | line_number: usize, 64 | column: usize, 65 | }, 66 | #[error("Unexpected End of Tokens")] 67 | UnexpectedEndOfTokens, 68 | } 69 | -------------------------------------------------------------------------------- /rayql-engine/src/schema/mod.rs: -------------------------------------------------------------------------------- 1 | mod parser; 2 | mod tokenizer; 3 | mod utils; 4 | 5 | pub mod error; 6 | 7 | pub use parser::parse; 8 | 9 | #[derive(Debug, PartialEq, Clone)] 10 | 11 | pub struct DataTypeWithSpan { 12 | pub data_type: rayql::types::DataType, 13 | pub line_number: usize, 14 | pub column: usize, 15 | } 16 | 17 | impl DataTypeWithSpan { 18 | fn new(data_type: rayql::types::DataType, line_number: usize, column: usize) -> Self { 19 | DataTypeWithSpan { 20 | data_type, 21 | line_number, 22 | column, 23 | } 24 | } 25 | } 26 | 27 | #[derive(Debug, PartialEq, Clone)] 28 | pub struct Enum { 29 | pub name: String, 30 | pub variants: Vec, 31 | pub line_number: usize, 32 | pub column: usize, 33 | } 34 | 35 | impl Enum { 36 | pub fn new( 37 | name: String, 38 | variants: Vec, 39 | line_number: usize, 40 | column: usize, 41 | ) -> Self { 42 | Enum { 43 | name, 44 | variants, 45 | line_number, 46 | column, 47 | } 48 | } 49 | 50 | pub fn get_variant(&self, enum_variant: &str) -> Option<&EnumVariant> { 51 | self.variants 52 | .iter() 53 | .find(|variant| variant.name.eq(enum_variant)) 54 | } 55 | } 56 | 57 | #[derive(Debug, PartialEq, Clone)] 58 | pub struct EnumVariant { 59 | pub name: String, 60 | pub line_number: usize, 61 | pub column: usize, 62 | } 63 | 64 | impl EnumVariant { 65 | pub fn new(name: String, line_number: usize, column: usize) -> Self { 66 | EnumVariant { 67 | name, 68 | line_number, 69 | column, 70 | } 71 | } 72 | } 73 | 74 | #[derive(Debug, PartialEq, Clone)] 75 | pub enum Property { 76 | FunctionCall(FunctionCall), 77 | PrimaryKey, 78 | AutoIncrement, 79 | Unique, 80 | } 81 | 82 | #[derive(Debug, PartialEq, Clone)] 83 | pub struct Reference { 84 | pub entity: String, 85 | pub property: String, 86 | pub line_number: usize, 87 | pub column: usize, 88 | } 89 | 90 | impl Reference { 91 | pub fn new(entity: String, property: String, line_number: usize, column: usize) -> Self { 92 | Reference { 93 | entity, 94 | property, 95 | line_number, 96 | column, 97 | } 98 | } 99 | } 100 | 101 | #[derive(Debug, PartialEq, Clone)] 102 | pub struct FunctionCall { 103 | pub name: String, 104 | pub arguments: Arguments, 105 | pub context: FunctionCallContext, 106 | pub line_number: usize, 107 | pub column: usize, 108 | } 109 | 110 | impl FunctionCall { 111 | pub fn new( 112 | name: String, 113 | arguments: Vec, 114 | context: FunctionCallContext, 115 | line_number: usize, 116 | column: usize, 117 | ) -> Self { 118 | FunctionCall { 119 | name, 120 | arguments: Arguments::from_vec(arguments, line_number, column), 121 | context, 122 | line_number, 123 | column, 124 | } 125 | } 126 | } 127 | 128 | #[derive(Debug, PartialEq, Clone)] 129 | pub struct FunctionCallContext { 130 | pub property_name: String, 131 | pub property_data_type: DataTypeWithSpan, 132 | } 133 | 134 | impl FunctionCallContext { 135 | pub fn new(property_name: String, property_data_type: DataTypeWithSpan) -> Self { 136 | FunctionCallContext { 137 | property_name, 138 | property_data_type, 139 | } 140 | } 141 | } 142 | 143 | #[derive(Debug, PartialEq, Clone)] 144 | pub struct Arguments { 145 | pub list: Vec, 146 | pub line_number: usize, 147 | pub column: usize, 148 | } 149 | 150 | impl Arguments { 151 | pub fn from_vec(arguments: Vec, line_number: usize, column: usize) -> Self { 152 | Arguments { 153 | list: arguments, 154 | line_number, 155 | column, 156 | } 157 | } 158 | 159 | pub fn get_first(&self) -> Option<&Argument> { 160 | self.list.first() 161 | } 162 | } 163 | 164 | #[derive(Debug, PartialEq, Clone)] 165 | pub struct Argument { 166 | pub value: ArgumentValue, 167 | pub line_number: usize, 168 | pub column: usize, 169 | } 170 | 171 | impl Argument { 172 | pub fn new(value: ArgumentValue, line_number: usize, column: usize) -> Self { 173 | Argument { 174 | value, 175 | line_number, 176 | column, 177 | } 178 | } 179 | } 180 | 181 | #[derive(Debug, PartialEq, Clone)] 182 | pub enum ArgumentValue { 183 | Identifier(String), 184 | Reference(Reference), 185 | FunctionCall(FunctionCall), 186 | Value(rayql::value::Value), 187 | } 188 | 189 | #[derive(Debug, PartialEq, Clone)] 190 | pub struct Field { 191 | pub name: String, 192 | pub data_type: DataTypeWithSpan, 193 | pub properties: Vec, 194 | pub line_number: usize, 195 | pub column: usize, 196 | } 197 | 198 | impl Field { 199 | pub fn new( 200 | name: String, 201 | data_type: DataTypeWithSpan, 202 | properties: Vec, 203 | line_number: usize, 204 | column: usize, 205 | ) -> Self { 206 | Field { 207 | name, 208 | data_type, 209 | properties, 210 | line_number, 211 | column, 212 | } 213 | } 214 | } 215 | 216 | #[derive(Debug, PartialEq, Clone)] 217 | pub struct Model { 218 | pub name: String, 219 | pub fields: Vec, 220 | pub line_number: usize, 221 | pub column: usize, 222 | } 223 | 224 | impl Model { 225 | pub fn new(name: String, fields: Vec, line_number: usize, column: usize) -> Self { 226 | Model { 227 | name, 228 | fields, 229 | line_number, 230 | column, 231 | } 232 | } 233 | 234 | pub fn get_field(&self, field_name: &str) -> Option<&Field> { 235 | self.fields.iter().find(|field| field.name.eq(field_name)) 236 | } 237 | } 238 | 239 | #[derive(Debug, PartialEq)] 240 | pub struct Schema { 241 | pub enums: Vec, 242 | pub models: Vec, 243 | } 244 | 245 | impl Schema { 246 | pub fn new(enums: Vec, models: Vec) -> Self { 247 | Schema { enums, models } 248 | } 249 | 250 | pub fn parse(input: &str) -> Result { 251 | rayql::schema::parse(input) 252 | } 253 | 254 | pub fn get_model(&self, model_name: &str) -> Option<&Model> { 255 | self.models.iter().find(|model| model.name.eq(model_name)) 256 | } 257 | 258 | pub fn get_enum(&self, enum_name: &str) -> Option<&Enum> { 259 | self.enums.iter().find(|e| e.name.eq(enum_name)) 260 | } 261 | } 262 | -------------------------------------------------------------------------------- /rayql-engine/src/schema/parser.rs: -------------------------------------------------------------------------------- 1 | use rayql::schema::{ 2 | error::ParseError, 3 | tokenizer::{tokenize, Keyword, Token}, 4 | utils::{get_data_type_with_span, get_model_or_enum_name}, 5 | Argument, Schema, 6 | }; 7 | 8 | struct TokenConsumer<'a> { 9 | tokens_iter: std::iter::Peekable>, 10 | } 11 | 12 | impl<'a> TokenConsumer<'a> { 13 | fn new(tokens: &'a [(Token, usize, usize)]) -> Self { 14 | Self { 15 | tokens_iter: tokens.iter().peekable(), 16 | } 17 | } 18 | 19 | fn next(&mut self) -> Option<(&'a Token, usize, usize)> { 20 | self.tokens_iter 21 | .next() 22 | .map(|(token, line, col)| (token, *line, *col)) 23 | } 24 | 25 | fn peek(&mut self) -> Option<(&'a Token, usize, usize)> { 26 | self.tokens_iter 27 | .peek() 28 | .map(|(token, line, col)| (token, *line, *col)) 29 | } 30 | } 31 | 32 | pub fn parse(input: &str) -> Result { 33 | let tokens = tokenize(input)?; 34 | let mut models = Vec::new(); 35 | let mut enums = Vec::new(); 36 | let mut identifiers = std::collections::HashSet::new(); 37 | let mut token_consumer = TokenConsumer::new(&tokens); 38 | 39 | while let Some((token, line_number, column)) = token_consumer.next() { 40 | match token { 41 | Token::Keyword(Keyword::Enum) => { 42 | let enum_name = 43 | get_model_or_enum_name(&mut token_consumer.tokens_iter, &mut identifiers)?; 44 | let enum_declaration = parse_enum(enum_name, &mut token_consumer)?; 45 | enums.push(enum_declaration); 46 | } 47 | Token::Keyword(Keyword::Model) => { 48 | let model_name = 49 | get_model_or_enum_name(&mut token_consumer.tokens_iter, &mut identifiers)?; 50 | let model_declaration = parse_model(model_name, &mut token_consumer)?; 51 | models.push(model_declaration); 52 | } 53 | _ => { 54 | return Err(ParseError::UnexpectedToken { 55 | token: token.clone(), 56 | line_number, 57 | column, 58 | }) 59 | } 60 | } 61 | } 62 | 63 | Ok(Schema::new(enums, models)) 64 | } 65 | 66 | fn parse_enum( 67 | enum_name: String, 68 | token_consumer: &mut TokenConsumer, 69 | ) -> Result { 70 | let mut variants = vec![]; 71 | let mut existing_variants = std::collections::HashSet::new(); 72 | 73 | while let Some((token, line_number, column)) = token_consumer.next() { 74 | match token { 75 | Token::BraceClose => { 76 | return Ok(rayql::schema::Enum::new( 77 | enum_name, 78 | variants, 79 | line_number, 80 | column, 81 | )) 82 | } 83 | Token::Identifier(variant) => { 84 | if !existing_variants.insert(variant) { 85 | return Err(ParseError::EnumVariantAlreadyExists { 86 | variant: variant.clone(), 87 | r#enum: enum_name, 88 | line_number, 89 | column, 90 | }); 91 | } 92 | 93 | variants.push(rayql::schema::EnumVariant::new( 94 | variant.clone(), 95 | line_number, 96 | column, 97 | )) 98 | } 99 | _ => { 100 | return Err(ParseError::UnexpectedToken { 101 | token: token.clone(), 102 | line_number, 103 | column, 104 | }) 105 | } 106 | } 107 | } 108 | 109 | Err(ParseError::UnexpectedEndOfTokens) 110 | } 111 | 112 | fn parse_model( 113 | model_name: String, 114 | token_consumer: &mut TokenConsumer, 115 | ) -> Result { 116 | let mut fields = vec![]; 117 | let mut field_names = std::collections::HashSet::new(); 118 | 119 | while let Some((token, line_number, column)) = token_consumer.next() { 120 | match token { 121 | Token::BraceClose => { 122 | return Ok(rayql::schema::Model::new( 123 | model_name, 124 | fields, 125 | line_number, 126 | column, 127 | )) 128 | } 129 | Token::Identifier(identifier) => match token_consumer.next() { 130 | Some((Token::Colon, _, _)) => { 131 | if !field_names.insert(identifier) { 132 | return Err(ParseError::FieldAlreadyExistsOnModel { 133 | field: identifier.clone(), 134 | model: model_name, 135 | line_number, 136 | column, 137 | }); 138 | } 139 | 140 | let field = parse_field(identifier.clone(), token_consumer)?; 141 | fields.push(field); 142 | } 143 | Some((token, line_number, column)) => { 144 | return Err(ParseError::UnexpectedToken { 145 | token: token.clone(), 146 | line_number, 147 | column, 148 | }); 149 | } 150 | None => return Err(ParseError::UnexpectedEndOfTokens), 151 | }, 152 | _ => { 153 | return Err(ParseError::UnexpectedToken { 154 | token: token.clone(), 155 | line_number, 156 | column, 157 | }) 158 | } 159 | } 160 | } 161 | 162 | Err(ParseError::UnexpectedEndOfTokens) 163 | } 164 | 165 | fn parse_field( 166 | name: String, 167 | token_consumer: &mut TokenConsumer, 168 | ) -> Result { 169 | let data_type = get_data_type_with_span(token_consumer.next())?; 170 | 171 | let mut properties = vec![]; 172 | 173 | while let Some((token, line_number, column)) = token_consumer.peek() { 174 | match token { 175 | Token::Comma => { 176 | token_consumer.next(); 177 | return Ok(rayql::schema::Field::new( 178 | name, 179 | data_type, 180 | properties, 181 | line_number, 182 | column, 183 | )); 184 | } 185 | Token::Identifier(identifier) => { 186 | token_consumer.next(); 187 | if let Some((Token::ParenOpen, _, _)) = token_consumer.peek() { 188 | token_consumer.next(); 189 | properties.push(rayql::schema::Property::FunctionCall(parse_function_call( 190 | identifier.clone(), 191 | rayql::schema::FunctionCallContext::new(name.clone(), data_type.clone()), 192 | token_consumer, 193 | line_number, 194 | column, 195 | )?)); 196 | continue; 197 | } 198 | 199 | return Err(ParseError::UnexpectedToken { 200 | token: token.clone(), 201 | line_number, 202 | column, 203 | }); 204 | } 205 | Token::Keyword(keyword) => { 206 | token_consumer.next(); 207 | properties.push(rayql::schema::utils::keyword_to_property_value( 208 | keyword.clone(), 209 | &line_number, 210 | &column, 211 | )?); 212 | } 213 | Token::BraceClose => { 214 | return Ok(rayql::schema::Field::new( 215 | name, 216 | data_type, 217 | properties, 218 | line_number, 219 | column, 220 | )) 221 | } 222 | _ => { 223 | return Err(ParseError::UnexpectedToken { 224 | token: token.clone(), 225 | line_number, 226 | column, 227 | }) 228 | } 229 | } 230 | } 231 | 232 | Err(ParseError::UnexpectedEndOfTokens) 233 | } 234 | 235 | fn parse_function_call( 236 | name: String, 237 | context: rayql::schema::FunctionCallContext, 238 | token_consumer: &mut TokenConsumer, 239 | fn_call_line: usize, 240 | fn_call_column: usize, 241 | ) -> Result { 242 | let mut arguments: Vec = vec![]; 243 | 244 | while let Some((token, line_number, column)) = token_consumer.next() { 245 | let argument = match token { 246 | Token::ParenClose => { 247 | return Ok(rayql::schema::FunctionCall::new( 248 | name, 249 | arguments, 250 | context, 251 | fn_call_line, 252 | fn_call_column, 253 | )); 254 | } 255 | Token::Identifier(identifier) => { 256 | if let Some((Token::ParenOpen, _, _)) = token_consumer.peek() { 257 | token_consumer.next(); 258 | Argument::new( 259 | rayql::schema::ArgumentValue::FunctionCall(parse_function_call( 260 | identifier.clone(), 261 | rayql::schema::FunctionCallContext::new( 262 | name.clone(), 263 | context.property_data_type.clone(), 264 | ), 265 | token_consumer, 266 | line_number, 267 | column - identifier.len(), 268 | )?), 269 | line_number, 270 | column, 271 | ) 272 | } else { 273 | Argument::new( 274 | rayql::schema::ArgumentValue::Identifier(identifier.clone()), 275 | line_number, 276 | column, 277 | ) 278 | } 279 | } 280 | Token::Reference(entity, property) => { 281 | if property.contains('.') { 282 | return Err(ParseError::InvalidReference { 283 | entity: entity.to_string(), 284 | property: property.to_string(), 285 | line_number, 286 | column, 287 | }); 288 | } 289 | 290 | Argument::new( 291 | rayql::schema::ArgumentValue::Reference(rayql::schema::Reference::new( 292 | entity.clone(), 293 | property.clone(), 294 | line_number, 295 | column, 296 | )), 297 | line_number, 298 | column, 299 | ) 300 | } 301 | Token::StringLiteral(s) => Argument::new( 302 | rayql::schema::ArgumentValue::Value(rayql::value::Value::StringLiteral( 303 | s.to_string(), 304 | )), 305 | line_number, 306 | column, 307 | ), 308 | Token::Integer(i) => Argument::new( 309 | rayql::schema::ArgumentValue::Value(rayql::value::Value::Integer(i.to_owned())), 310 | line_number, 311 | column, 312 | ), 313 | Token::Real(r) => Argument::new( 314 | rayql::schema::ArgumentValue::Value(rayql::value::Value::Real(r.to_owned())), 315 | line_number, 316 | column, 317 | ), 318 | Token::Boolean(b) => Argument::new( 319 | rayql::schema::ArgumentValue::Value(rayql::value::Value::Boolean(b.to_owned())), 320 | line_number, 321 | column, 322 | ), 323 | _ => { 324 | return Err(ParseError::UnexpectedToken { 325 | token: token.clone(), 326 | line_number, 327 | column, 328 | }) 329 | } 330 | }; 331 | 332 | if let Some((token, line_number, column)) = token_consumer.peek() { 333 | match token { 334 | Token::Comma => { 335 | token_consumer.next(); 336 | arguments.push(argument) 337 | } 338 | Token::ParenClose => { 339 | token_consumer.next(); 340 | arguments.push(argument); 341 | return Ok(rayql::schema::FunctionCall::new( 342 | name, 343 | arguments, 344 | context, 345 | fn_call_line, 346 | fn_call_column, 347 | )); 348 | } 349 | _ => { 350 | return Err(ParseError::UnexpectedToken { 351 | token: token.clone(), 352 | line_number, 353 | column, 354 | }); 355 | } 356 | } 357 | } 358 | } 359 | 360 | Err(ParseError::UnexpectedEndOfTokens) 361 | } 362 | -------------------------------------------------------------------------------- /rayql-engine/src/schema/tokenizer.rs: -------------------------------------------------------------------------------- 1 | use rayql::schema::error::TokenizationError; 2 | 3 | #[derive(Debug, PartialEq, Clone)] 4 | pub enum Token { 5 | Identifier(String), 6 | Reference(String, String), 7 | StringLiteral(String), 8 | Integer(i64), 9 | Real(f64), 10 | Boolean(bool), 11 | Keyword(Keyword), 12 | ParenOpen, 13 | ParenClose, 14 | BraceOpen, 15 | BraceClose, 16 | Colon, 17 | Comma, 18 | Optional(Box), 19 | } 20 | 21 | impl std::fmt::Display for Token { 22 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 23 | match self { 24 | Token::Identifier(s) => write!(f, "Identifier: {}", s), 25 | Token::Reference(e, p) => write!(f, "Reference: {}.{}", e, p), 26 | Token::StringLiteral(s) => write!(f, "StringLiteral: {}", s), 27 | Token::Integer(i) => write!(f, "Integer: {}", i), 28 | Token::Real(r) => write!(f, "Real: {}", r), 29 | Token::Boolean(b) => write!(f, "Boolean: {}", b), 30 | Token::Keyword(kw) => write!(f, "Keyword: {:?}", kw), 31 | Token::ParenOpen => write!(f, "("), 32 | Token::ParenClose => write!(f, ")"), 33 | Token::BraceOpen => write!(f, "{{"), 34 | Token::BraceClose => write!(f, "}}"), 35 | Token::Colon => write!(f, ":"), 36 | Token::Comma => write!(f, ","), 37 | Token::Optional(token) => write!(f, "Optional {}", token), 38 | } 39 | } 40 | } 41 | 42 | impl Token { 43 | pub fn len(&self) -> usize { 44 | match self { 45 | Token::Identifier(s) => s.len(), 46 | Token::Reference(e, p) => e.len() + p.len() + 1, // +1 for the dot 47 | Token::StringLiteral(s) => s.len(), 48 | Token::Integer(i) => i.to_string().len(), 49 | Token::Real(r) => r.to_string().len(), 50 | Token::Boolean(b) => b.to_string().len(), 51 | Token::Keyword(kw) => format!("{:?}", kw).len(), 52 | Token::ParenOpen => 1, 53 | Token::ParenClose => 1, 54 | Token::BraceOpen => 1, 55 | Token::BraceClose => 1, 56 | Token::Colon => 1, 57 | Token::Comma => 1, 58 | Token::Optional(token) => token.len() + 9, // +9 for "Optional " prefix 59 | } 60 | } 61 | } 62 | 63 | #[derive(Debug, PartialEq, Clone)] 64 | pub enum Keyword { 65 | Model, 66 | Enum, 67 | Index, 68 | String, 69 | Integer, 70 | Real, 71 | Blob, 72 | Boolean, 73 | Timestamp, 74 | PrimaryKey, 75 | AutoIncrement, 76 | Unique, 77 | } 78 | 79 | pub fn get_keyword(token_str: &str) -> Option { 80 | match token_str { 81 | "model" => Some(Keyword::Model), 82 | "enum" => Some(Keyword::Enum), 83 | "index" => Some(Keyword::Index), 84 | "str" => Some(Keyword::String), 85 | "int" => Some(Keyword::Integer), 86 | "real" => Some(Keyword::Real), 87 | "bool" => Some(Keyword::Boolean), 88 | "blob" => Some(Keyword::Blob), 89 | "timestamp" => Some(Keyword::Timestamp), 90 | "primary_key" => Some(Keyword::PrimaryKey), 91 | "auto_increment" => Some(Keyword::AutoIncrement), 92 | "unique" => Some(Keyword::Unique), 93 | _ => None, 94 | } 95 | } 96 | 97 | pub fn is_comment(line: &str) -> bool { 98 | line.trim().starts_with('#') 99 | } 100 | 101 | pub fn tokenize(input: &str) -> Result, TokenizationError> { 102 | let mut tokens = Vec::new(); 103 | for (line_num, line) in input.lines().enumerate() { 104 | if is_comment(line) { 105 | continue; 106 | } 107 | 108 | tokens.extend(tokenize_line(line, line_num + 1)?); 109 | } 110 | Ok(tokens) 111 | } 112 | 113 | pub fn tokenize_line( 114 | line: &str, 115 | line_number: usize, 116 | ) -> Result, TokenizationError> { 117 | let mut tokens = Vec::new(); 118 | let mut in_string_literal = false; 119 | let mut is_escaped = false; 120 | 121 | let mut buffer = String::new(); 122 | let mut chars = line.chars().peekable(); 123 | 124 | let mut column = 0; 125 | 126 | while let Some(ch) = chars.next() { 127 | column += 1; 128 | 129 | if ch == '#' && !in_string_literal { 130 | break; 131 | } 132 | 133 | if in_string_literal { 134 | if is_escaped { 135 | let escape_ch = match get_escape_char(&ch) { 136 | Some(escape_ch) => escape_ch, 137 | None => { 138 | return Err(TokenizationError::UnknownEscapeSequence { 139 | char: ch, 140 | line: line_number, 141 | column, 142 | }) 143 | } 144 | }; 145 | 146 | buffer.push(escape_ch); 147 | is_escaped = false; 148 | } else if ch == '\\' { 149 | is_escaped = true; 150 | } else if ch == '\'' { 151 | in_string_literal = false; 152 | tokens.push(( 153 | Token::StringLiteral(buffer.clone()), 154 | line_number, 155 | column - buffer.len(), 156 | )); 157 | buffer.clear(); 158 | } else { 159 | buffer.push(ch); 160 | } 161 | } else if ch.is_whitespace() { 162 | if !buffer.is_empty() { 163 | tokens.push(( 164 | get_token(&buffer, line_number, column)?, 165 | line_number, 166 | column - buffer.len(), 167 | )); 168 | buffer.clear(); 169 | } 170 | } else { 171 | match ch { 172 | '\'' if buffer.is_empty() => in_string_literal = true, 173 | '_' if !buffer.is_empty() => { 174 | if !is_valid_identifier(&buffer) { 175 | return Err(TokenizationError::UnexpectedCharacter { 176 | char: ch, 177 | line: line_number, 178 | column, 179 | }); 180 | } 181 | 182 | buffer.push(ch); 183 | } 184 | '.' => match chars.peek() { 185 | Some(next_char) => { 186 | if (!buffer.is_empty() && next_char.is_alphanumeric()) 187 | || next_char.is_numeric() 188 | { 189 | buffer.push(ch); 190 | } else if next_char.is_whitespace() { 191 | return Err(TokenizationError::UnexpectedCharacter { 192 | char: ch, 193 | line: line_number, 194 | column, 195 | }); 196 | } else { 197 | return Err(TokenizationError::UnexpectedCharacter { 198 | char: *next_char, 199 | line: line_number, 200 | column, 201 | }); 202 | } 203 | } 204 | None => return Err(TokenizationError::UnexpectedEndOfInput), 205 | }, 206 | '-' if buffer.is_empty() => { 207 | if let Some(next_char) = chars.peek() { 208 | if next_char.is_numeric() || next_char.eq(&'.') { 209 | buffer.push(ch); 210 | } else { 211 | return Err(TokenizationError::UnexpectedCharacter { 212 | char: ch, 213 | line: line_number, 214 | column, 215 | }); 216 | } 217 | } else { 218 | return Err(TokenizationError::UnexpectedEndOfInput); 219 | } 220 | } 221 | '-' if buffer.ends_with('e') => { 222 | if let Some(next_char) = chars.peek() { 223 | if next_char.is_numeric() { 224 | buffer.push(ch); 225 | } else { 226 | return Err(TokenizationError::UnexpectedCharacter { 227 | char: ch, 228 | line: line_number, 229 | column, 230 | }); 231 | } 232 | } else { 233 | return Err(TokenizationError::UnexpectedEndOfInput); 234 | } 235 | } 236 | '?' if !buffer.is_empty() => { 237 | let token = get_token(&buffer, line_number, column)?; 238 | tokens.push(( 239 | Token::Optional(Box::new(token)), 240 | line_number, 241 | column - buffer.len(), 242 | )); 243 | buffer.clear(); 244 | } 245 | ch if ch.is_alphanumeric() => buffer.push(ch), 246 | _ => { 247 | if !buffer.is_empty() { 248 | tokens.push(( 249 | get_token(&buffer, line_number, column)?, 250 | line_number, 251 | column - buffer.len(), 252 | )); 253 | buffer.clear(); 254 | } 255 | 256 | let token = match ch { 257 | ':' => Token::Colon, 258 | ',' => Token::Comma, 259 | '{' => Token::BraceOpen, 260 | '}' => Token::BraceClose, 261 | '(' => Token::ParenOpen, 262 | ')' => Token::ParenClose, 263 | _ => { 264 | return Err(TokenizationError::UnexpectedCharacter { 265 | char: ch, 266 | line: line_number, 267 | column, 268 | }); 269 | } 270 | }; 271 | 272 | tokens.push((token, line_number, column)); 273 | } 274 | } 275 | } 276 | } 277 | 278 | if in_string_literal { 279 | return Err(TokenizationError::StringLiteralOpened { 280 | line: line_number, 281 | column, 282 | }); 283 | } 284 | 285 | if !buffer.is_empty() { 286 | tokens.push(( 287 | get_token(&buffer, line_number, column)?, 288 | line_number, 289 | column - buffer.len(), 290 | )); 291 | } 292 | 293 | Ok(tokens) 294 | } 295 | 296 | pub fn get_token( 297 | token_str: &str, 298 | line_number: usize, 299 | column: usize, 300 | ) -> Result { 301 | if let Some(keyword) = get_keyword(token_str) { 302 | return Ok(Token::Keyword(keyword)); 303 | } 304 | 305 | if let Ok(boolean) = token_str.parse::() { 306 | return Ok(Token::Boolean(boolean)); 307 | } 308 | 309 | if let Ok(integer) = token_str.parse::() { 310 | return Ok(Token::Integer(integer)); 311 | } 312 | 313 | if let Ok(float) = token_str.parse::() { 314 | return Ok(Token::Real(float)); 315 | } 316 | 317 | if let Some((base, exponent)) = token_str.split_once('e') { 318 | if let (Ok(base), Ok(exponent)) = (base.parse::(), exponent.parse::()) { 319 | return Ok(Token::Real(base * 10f64.powi(exponent))); 320 | } 321 | } 322 | 323 | if let Some((entity, property)) = token_str.split_once('.') { 324 | if !is_valid_identifier(entity) { 325 | return Err(TokenizationError::IdentifierBeginsWithDigit { 326 | identifier: entity.to_string(), 327 | line: line_number, 328 | column, 329 | }); 330 | } 331 | 332 | return Ok(Token::Reference(entity.to_string(), property.to_string())); 333 | } 334 | 335 | if is_valid_identifier(token_str) { 336 | Ok(Token::Identifier(token_str.to_string())) 337 | } else { 338 | Err(TokenizationError::IdentifierBeginsWithDigit { 339 | identifier: token_str.to_string(), 340 | line: line_number, 341 | column, 342 | }) 343 | } 344 | } 345 | 346 | fn is_valid_identifier(identifier: &str) -> bool { 347 | if identifier.chars().next().unwrap().is_ascii_digit() { 348 | return false; 349 | } 350 | 351 | true 352 | } 353 | 354 | fn get_escape_char(ch: &char) -> Option { 355 | match ch { 356 | 'n' => Some('\n'), 357 | 'r' => Some('\r'), 358 | 't' => Some('\t'), 359 | '\\' => Some('\\'), 360 | '\'' => Some('\''), 361 | '"' => Some('"'), 362 | _ => None, 363 | } 364 | } 365 | -------------------------------------------------------------------------------- /rayql-engine/src/schema/utils.rs: -------------------------------------------------------------------------------- 1 | use rayql::schema::{ 2 | error::ParseError, 3 | tokenizer::{Keyword, Token}, 4 | DataTypeWithSpan, 5 | }; 6 | 7 | pub(crate) fn get_data_type_with_span( 8 | input: Option<(&Token, usize, usize)>, 9 | ) -> Result { 10 | if let Some((token, line_number, column)) = input { 11 | let data_type = match token { 12 | Token::Keyword(keyword) => match keyword { 13 | Keyword::String => rayql::types::DataType::String, 14 | Keyword::Integer => rayql::types::DataType::Integer, 15 | Keyword::Real => rayql::types::DataType::Real, 16 | Keyword::Boolean => rayql::types::DataType::Boolean, 17 | Keyword::Blob => rayql::types::DataType::Blob, 18 | Keyword::Timestamp => rayql::types::DataType::Timestamp, 19 | _ => unimplemented!("Unexpected data type"), 20 | }, 21 | Token::Optional(token) => { 22 | let inner_data_type = get_data_type_with_span(Some((token, line_number, column)))?; 23 | rayql::types::DataType::Optional(Box::new(inner_data_type.data_type)) 24 | } 25 | Token::Identifier(identifier) => rayql::types::DataType::Enum(identifier.clone()), 26 | _ => { 27 | return Err(ParseError::UnexpectedToken { 28 | token: token.clone(), 29 | line_number, 30 | column, 31 | }) 32 | } 33 | }; 34 | 35 | let data_type_with_span = DataTypeWithSpan::new(data_type, line_number, column); 36 | 37 | return Ok(data_type_with_span); 38 | } 39 | 40 | Err(ParseError::UnexpectedEndOfTokens) 41 | } 42 | 43 | pub(crate) fn get_model_or_enum_name( 44 | tokens_iter: &mut std::iter::Peekable>, 45 | identifiers: &mut std::collections::HashSet, 46 | ) -> Result { 47 | let name = match tokens_iter.next() { 48 | Some((Token::Identifier(name), line_number, column)) => { 49 | if identifiers.contains(name) { 50 | return Err(ParseError::IdentifierAlreadyInUse { 51 | identifier: name.clone(), 52 | line_number: *line_number, 53 | column: *column, 54 | }); 55 | } 56 | 57 | name.clone() 58 | } 59 | Some((token, line_number, column)) => { 60 | return Err(ParseError::UnexpectedToken { 61 | token: token.clone(), 62 | line_number: *line_number, 63 | column: *column, 64 | }) 65 | } 66 | None => return Err(ParseError::UnexpectedEndOfTokens), 67 | }; 68 | 69 | identifiers.insert(name.clone()); 70 | 71 | match tokens_iter.next() { 72 | Some((Token::BraceOpen, _, _)) => Ok(name), 73 | Some((token, line_number, column)) => Err(ParseError::UnexpectedToken { 74 | token: token.clone(), 75 | line_number: *line_number, 76 | column: *column, 77 | }), 78 | None => Err(ParseError::UnexpectedEndOfTokens), 79 | } 80 | } 81 | 82 | pub(crate) fn keyword_to_property_value( 83 | keyword: Keyword, 84 | line_number: &usize, 85 | column: &usize, 86 | ) -> Result { 87 | match keyword { 88 | Keyword::PrimaryKey => Ok(rayql::schema::Property::PrimaryKey), 89 | Keyword::AutoIncrement => Ok(rayql::schema::Property::AutoIncrement), 90 | Keyword::Unique => Ok(rayql::schema::Property::Unique), 91 | _ => Err(ParseError::UnexpectedToken { 92 | token: Token::Keyword(keyword), 93 | line_number: *line_number, 94 | column: *column, 95 | }), 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /rayql-engine/src/sql/error.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | #[derive(Debug)] 4 | pub enum ToSQLError { 5 | UnknownReference { 6 | entity_name: String, 7 | line_number: usize, 8 | column: usize, 9 | }, 10 | IncorrectReference { 11 | entity_name: String, 12 | variant_name: String, 13 | given_entity_name: String, 14 | line_number: usize, 15 | column: usize, 16 | }, 17 | EnumNotFound { 18 | enum_name: String, 19 | line_number: usize, 20 | column: usize, 21 | }, 22 | ModelNotFound { 23 | model_name: String, 24 | line_number: usize, 25 | column: usize, 26 | }, 27 | FieldNotFound { 28 | model_name: String, 29 | field_name: String, 30 | line_number: usize, 31 | column: usize, 32 | }, 33 | VariantNotFound { 34 | enum_name: String, 35 | variant: String, 36 | line_number: usize, 37 | column: usize, 38 | }, 39 | ConversionError { 40 | reason: String, 41 | line_number: usize, 42 | column: usize, 43 | }, 44 | FunctionError { 45 | source: rayql::sql::error::FunctionError, 46 | line_number: usize, 47 | column: usize, 48 | }, 49 | } 50 | 51 | impl fmt::Display for ToSQLError { 52 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 53 | match self { 54 | ToSQLError::UnknownReference { 55 | entity_name, 56 | line_number, 57 | column, 58 | } => { 59 | write!( 60 | f, 61 | "Unknown reference: {} at line {line_number}, column {column}", 62 | entity_name 63 | ) 64 | } 65 | ToSQLError::IncorrectReference { 66 | entity_name, 67 | variant_name, 68 | given_entity_name, 69 | line_number, 70 | column, 71 | } => { 72 | write!( 73 | f, 74 | "Variant '{}' of enum '{}' passed to a function expecting enum '{}' at line {}, column {}", 75 | variant_name, entity_name, given_entity_name, line_number, column 76 | ) 77 | } 78 | ToSQLError::EnumNotFound { 79 | enum_name, 80 | line_number, 81 | column, 82 | } => { 83 | write!( 84 | f, 85 | "Enum not found: {} at line {line_number}, column {column}", 86 | enum_name 87 | ) 88 | } 89 | ToSQLError::ModelNotFound { 90 | model_name, 91 | line_number, 92 | column, 93 | } => { 94 | write!( 95 | f, 96 | "Enum not found: {} at line {line_number}, column {column}", 97 | model_name 98 | ) 99 | } 100 | ToSQLError::FieldNotFound { 101 | model_name, 102 | field_name, 103 | line_number, 104 | column, 105 | } => { 106 | write!( 107 | f, 108 | "Field '{}' does not exists on model '{}': line {line_number}, column {column}", 109 | model_name, field_name 110 | ) 111 | } 112 | ToSQLError::VariantNotFound { 113 | enum_name, 114 | variant, 115 | line_number, 116 | column, 117 | } => { 118 | write!( 119 | f, 120 | "Variant '{}' does not exists on enum '{}': line {line_number}, column {column}", 121 | enum_name, variant 122 | ) 123 | } 124 | ToSQLError::ConversionError { 125 | reason, 126 | line_number, 127 | column, 128 | } => { 129 | write!( 130 | f, 131 | "Conversion error: {} at line {line_number}, column {column}", 132 | reason 133 | ) 134 | } 135 | ToSQLError::FunctionError { 136 | source, 137 | line_number, 138 | column, 139 | } => { 140 | write!( 141 | f, 142 | "Function error: {} at line {line_number}, column {column}", 143 | source 144 | ) 145 | } 146 | } 147 | } 148 | } 149 | 150 | impl std::error::Error for ToSQLError {} 151 | 152 | #[derive(Debug)] 153 | pub enum FunctionError { 154 | InvalidArgument(String), 155 | MissingArgument, 156 | ExpectsExactlyOneArgument(String), 157 | UndefinedFunction(String), 158 | } 159 | 160 | impl std::fmt::Display for FunctionError { 161 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 162 | match self { 163 | FunctionError::InvalidArgument(msg) => { 164 | write!(f, "Invalid argument: {}", msg) 165 | } 166 | FunctionError::MissingArgument => { 167 | write!(f, "Missing argument to function") 168 | } 169 | FunctionError::ExpectsExactlyOneArgument(func) => { 170 | write!(f, "{func} exactly one argument") 171 | } 172 | FunctionError::UndefinedFunction(func) => { 173 | write!(f, "Undefined function called '{func}'") 174 | } 175 | } 176 | } 177 | } 178 | 179 | impl std::error::Error for FunctionError {} 180 | -------------------------------------------------------------------------------- /rayql-engine/src/sql/fn_helpers.rs: -------------------------------------------------------------------------------- 1 | use rayql::{ 2 | schema::{ArgumentValue, Arguments, FunctionCallContext, Schema}, 3 | sql::error::{FunctionError, ToSQLError}, 4 | }; 5 | 6 | macro_rules! single_arg_fn { 7 | ($name:ident ($schema:ident, $arg_name:ident, $context:ident) $body:block) => { 8 | pub fn $name( 9 | $schema: &Schema, 10 | arguments: &Arguments, 11 | $context: &FunctionCallContext, 12 | ) -> Result { 13 | let $arg_name = get_single_argument(stringify!($name), arguments)?; 14 | 15 | $body 16 | } 17 | }; 18 | } 19 | 20 | macro_rules! check_value_fn { 21 | ($name:ident, $operator:expr) => { 22 | pub fn $name( 23 | schema: &Schema, 24 | context: &rayql::schema::FunctionCallContext, 25 | arguments: &Arguments, 26 | ) -> Result { 27 | check_value(schema, context, arguments, stringify!($name), $operator) 28 | } 29 | }; 30 | } 31 | 32 | macro_rules! argument_matches { 33 | ($argument:ident, $( $pattern:pat $( if $guard:expr )? => $result:expr ),+ $(,)?) => { 34 | match $argument.value { 35 | $( 36 | $pattern $( if $guard )? => $result, 37 | )+ 38 | _ => { 39 | return Err(ToSQLError::FunctionError { 40 | source: FunctionError::InvalidArgument(format!( 41 | "Invalid argument: {:?}", 42 | $argument.value, 43 | )), 44 | line_number: $argument.line_number, 45 | column: $argument.column, 46 | }); 47 | } 48 | } 49 | }; 50 | } 51 | 52 | pub(crate) fn get_single_argument( 53 | func: &str, 54 | arguments: &Arguments, 55 | ) -> Result { 56 | match arguments.list.as_slice() { 57 | [arg] => Ok(arg.clone()), 58 | _ => Err(ToSQLError::FunctionError { 59 | source: if arguments.list.is_empty() { 60 | FunctionError::MissingArgument 61 | } else { 62 | FunctionError::ExpectsExactlyOneArgument(func.to_string()) 63 | }, 64 | line_number: arguments.line_number, 65 | column: arguments.column, 66 | }), 67 | } 68 | } 69 | 70 | pub(crate) fn check_value( 71 | schema: &Schema, 72 | context: &FunctionCallContext, 73 | arguments: &Arguments, 74 | check_type: &str, 75 | operator: &str, 76 | ) -> Result { 77 | let argument = get_single_argument(check_type, arguments)?; 78 | 79 | let (value, name) = match argument.value { 80 | ArgumentValue::Value(value) => { 81 | let name = match context.property_data_type.data_type { 82 | rayql::types::DataType::String => format!("LENGTH({})", context.property_name), 83 | rayql::types::DataType::Integer | rayql::types::DataType::Real => { 84 | context.property_name.clone() 85 | } 86 | _ => { 87 | return Err(ToSQLError::FunctionError { 88 | source: FunctionError::InvalidArgument(format!( 89 | "{} value must be a value, got {:?}", 90 | check_type, value 91 | )), 92 | line_number: argument.line_number, 93 | column: argument.column, 94 | }) 95 | } 96 | }; 97 | 98 | Ok((name, value.to_sql())) 99 | } 100 | ArgumentValue::FunctionCall(func) => { 101 | Ok((context.property_name.clone(), func.to_sql(schema)?)) 102 | } 103 | _ => { 104 | return Err(ToSQLError::FunctionError { 105 | source: FunctionError::InvalidArgument(format!( 106 | "{} value must be a value, got {:?}", 107 | check_type, argument.value 108 | )), 109 | line_number: argument.line_number, 110 | column: argument.column, 111 | }) 112 | } 113 | }?; 114 | 115 | Ok(format!("CHECK({} {} {})", name, operator, value)) 116 | } 117 | -------------------------------------------------------------------------------- /rayql-engine/src/sql/function.rs: -------------------------------------------------------------------------------- 1 | use rayql::{ 2 | schema::{ArgumentValue, Arguments, FunctionCallContext, Schema}, 3 | sql::error::{FunctionError, ToSQLError}, 4 | types::DataType, 5 | }; 6 | 7 | use rayql::sql::fn_helpers::{check_value, get_single_argument}; 8 | 9 | check_value_fn!(min, "<="); 10 | 11 | check_value_fn!(max, ">="); 12 | 13 | single_arg_fn!(foreign_key(schema, argument, context) { 14 | let reference = argument_matches!( 15 | argument, 16 | ArgumentValue::Reference(reference) => reference.field_reference_to_sql(schema)? 17 | ); 18 | 19 | Ok(format!( 20 | " FOREIGN KEY ({}) REFERENCES {}", 21 | &context.property_name, reference 22 | )) 23 | }); 24 | 25 | single_arg_fn!(references(schema, argument, _context) { 26 | let reference = argument_matches!( 27 | argument, 28 | ArgumentValue::Reference(reference) => reference.field_reference_to_sql(schema)? 29 | ); 30 | 31 | Ok(format!("REFERENCES {}", reference)) 32 | }); 33 | 34 | single_arg_fn!(default(schema, argument, context) { 35 | let value = argument_matches!( 36 | argument, 37 | ArgumentValue::Value(value) if value.get_type().eq(&context.property_data_type.data_type) => { 38 | Ok(value.to_sql()) 39 | }, 40 | ArgumentValue::FunctionCall(func) => func.to_sql(schema), 41 | ArgumentValue::Reference(reference) => { 42 | if let DataType::Enum(ref enum_name) = context.property_data_type.data_type { 43 | if reference.entity.ne(enum_name) { 44 | return Err(ToSQLError::IncorrectReference { entity_name: reference.entity, variant_name: reference.property, given_entity_name: enum_name.clone(), line_number: reference.line_number, column: reference.column, }); 45 | } 46 | } 47 | 48 | reference.variant_reference_to_sql(schema) 49 | }, 50 | )?; 51 | 52 | Ok(format!("DEFAULT {}", value)) 53 | }); 54 | -------------------------------------------------------------------------------- /rayql-engine/src/sql/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod to_sql; 2 | 3 | pub mod error; 4 | 5 | #[macro_use] 6 | mod fn_helpers; 7 | 8 | mod function; 9 | -------------------------------------------------------------------------------- /rayql-engine/src/sql/to_sql.rs: -------------------------------------------------------------------------------- 1 | use rayql::{ 2 | schema::{ 3 | Argument, ArgumentValue, Arguments, Enum, EnumVariant, FunctionCall, Model, Property, 4 | Reference, Schema, 5 | }, 6 | sql::error::{FunctionError, ToSQLError}, 7 | types::DataType, 8 | Value, 9 | }; 10 | 11 | impl Schema { 12 | pub fn to_sql(&self) -> Result, ToSQLError> { 13 | let mut sql_statements = Vec::new(); 14 | 15 | for model in &self.models { 16 | let mut fields_sql = Vec::new(); 17 | let mut fk_sql = Vec::new(); 18 | 19 | for field in &model.fields { 20 | let mut field_sql = format!( 21 | " {} {}", 22 | field.name, 23 | field.data_type.data_type.to_sql(true) 24 | ); 25 | 26 | if let DataType::Enum(enum_name) = &field.data_type.data_type { 27 | let variants: Vec = match self.get_enum(enum_name) { 28 | Some(e) => e 29 | .variants 30 | .iter() 31 | .map(|variant| format!("'{}'", variant.to_sql())) 32 | .collect(), 33 | None => { 34 | return Err(ToSQLError::EnumNotFound { 35 | enum_name: enum_name.clone(), 36 | line_number: field.data_type.line_number, 37 | column: field.data_type.column, 38 | }) 39 | } 40 | }; 41 | field_sql.push_str(&format!( 42 | " CHECK({} IN ({}))", 43 | field.name, 44 | variants.join(", ") 45 | )); 46 | } 47 | 48 | for prop in &field.properties { 49 | match prop { 50 | Property::FunctionCall(FunctionCall { 51 | name, 52 | context, 53 | arguments, 54 | .. 55 | }) if name.eq("foreign_key") => { 56 | fk_sql 57 | .push(rayql::sql::function::foreign_key(self, arguments, context)?); 58 | } 59 | _ => field_sql.push_str(&format!(" {}", prop.to_sql(self)?)), 60 | } 61 | } 62 | 63 | fields_sql.push(field_sql); 64 | } 65 | fields_sql.extend(fk_sql); 66 | let model_sql = format!( 67 | "CREATE TABLE IF NOT EXISTS {} (\n{}\n);", 68 | model.name, 69 | fields_sql.join(",\n") 70 | ); 71 | sql_statements.push(model_sql); 72 | } 73 | 74 | Ok(sql_statements) 75 | } 76 | } 77 | 78 | impl Property { 79 | pub fn to_sql(&self, schema: &Schema) -> Result { 80 | match &self { 81 | Property::PrimaryKey => Ok("PRIMARY KEY".to_string()), 82 | Property::AutoIncrement => Ok("AUTOINCREMENT".to_string()), 83 | Property::Unique => Ok("UNIQUE".to_string()), 84 | Property::FunctionCall(func) => func.to_sql(schema), 85 | } 86 | } 87 | } 88 | 89 | impl Reference { 90 | pub fn field_reference_to_sql(&self, schema: &Schema) -> Result { 91 | match schema.get_model(&self.entity) { 92 | Some(model) => model.field_to_sql(&self.property, self.line_number, self.column), 93 | None => Err(ToSQLError::ModelNotFound { 94 | model_name: self.entity.clone(), 95 | line_number: self.line_number, 96 | column: self.column, 97 | }), 98 | } 99 | } 100 | 101 | pub fn variant_reference_to_sql(&self, schema: &Schema) -> Result { 102 | match schema.get_enum(&self.entity) { 103 | Some(e) => e.variant_to_sql(&self.property, self.line_number, self.column), 104 | None => Err(ToSQLError::EnumNotFound { 105 | enum_name: self.entity.clone(), 106 | line_number: self.line_number, 107 | column: self.column, 108 | }), 109 | } 110 | } 111 | } 112 | 113 | impl Model { 114 | pub fn field_to_sql( 115 | &self, 116 | field_name: &str, 117 | line_number: usize, 118 | column: usize, 119 | ) -> Result { 120 | match self.get_field(field_name) { 121 | Some(_) => Ok(format!("{}({})", self.name, field_name)), 122 | None => Err(ToSQLError::FieldNotFound { 123 | field_name: field_name.to_string(), 124 | model_name: self.name.to_string(), 125 | line_number, 126 | column: column + field_name.len(), 127 | }), 128 | } 129 | } 130 | } 131 | 132 | impl Enum { 133 | pub fn variant_to_sql( 134 | &self, 135 | variant: &str, 136 | line_number: usize, 137 | column: usize, 138 | ) -> Result { 139 | match self.get_variant(variant) { 140 | Some(_) => Ok(format!("'{}'", variant)), 141 | None => Err(ToSQLError::VariantNotFound { 142 | variant: variant.to_string(), 143 | enum_name: self.name.to_string(), 144 | line_number, 145 | column: column + variant.len(), 146 | }), 147 | } 148 | } 149 | } 150 | 151 | impl EnumVariant { 152 | pub fn to_sql(&self) -> String { 153 | self.name.to_string() 154 | } 155 | } 156 | 157 | // TODO: throw exception if an argument is passed 158 | // to a function which does not accepts any arguments 159 | impl FunctionCall { 160 | pub fn to_sql(&self, schema: &Schema) -> Result { 161 | match self.name.as_str() { 162 | "now" => Ok("CURRENT_TIMESTAMP".to_string()), 163 | "min" => rayql::sql::function::min(schema, &self.context, &self.arguments), 164 | "max" => rayql::sql::function::max(schema, &self.context, &self.arguments), 165 | "references" => { 166 | rayql::sql::function::references(schema, &self.arguments, &self.context) 167 | } 168 | "default" => rayql::sql::function::default(schema, &self.arguments, &self.context), 169 | _ => Err(ToSQLError::FunctionError { 170 | source: FunctionError::UndefinedFunction(self.name.clone()), 171 | line_number: self.line_number, 172 | column: self.column, 173 | }), 174 | } 175 | } 176 | } 177 | 178 | impl Arguments { 179 | pub fn to_sql(&self, schema: &Schema) -> Result, ToSQLError> { 180 | self.list.iter().map(|arg| arg.to_sql(schema)).collect() 181 | } 182 | } 183 | 184 | impl Argument { 185 | pub fn to_sql(&self, schema: &Schema) -> Result { 186 | self.value.to_sql(schema) 187 | } 188 | } 189 | 190 | impl ArgumentValue { 191 | pub fn to_sql(&self, schema: &Schema) -> Result { 192 | match self { 193 | ArgumentValue::Identifier(identifier) => Ok(identifier.clone()), 194 | ArgumentValue::FunctionCall(func) => func.to_sql(schema), 195 | ArgumentValue::Value(value) => Ok(value.to_sql()), 196 | _ => unimplemented!(), 197 | } 198 | } 199 | } 200 | 201 | impl Value { 202 | pub fn to_sql(&self) -> String { 203 | match self { 204 | Value::StringLiteral(s) => format!("'{}'", s), 205 | Value::Integer(i) => i.to_string(), 206 | Value::Real(f) => { 207 | if f.fract().eq(&0.0) { 208 | format!("{:.1}", f) 209 | } else { 210 | format!("{:.}", f) 211 | } 212 | } 213 | Value::Boolean(b) => { 214 | if *b { 215 | "1".to_string() 216 | } else { 217 | "0".to_string() 218 | } 219 | } 220 | } 221 | } 222 | } 223 | 224 | impl DataType { 225 | pub fn to_sql(&self, not_null: bool) -> String { 226 | let null_suffix = if not_null { "NOT NULL" } else { "NULL" }; 227 | let data_type = match &self { 228 | DataType::String | DataType::Enum(_) => "TEXT", 229 | DataType::Integer => "INTEGER", 230 | DataType::Real => "REAL", 231 | DataType::Blob => "BLOB", 232 | DataType::Boolean => "BOOLEAN", 233 | DataType::Timestamp => "TIMESTAMP", 234 | DataType::Optional(inner_type) => return inner_type.to_sql(false), 235 | }; 236 | 237 | format!("{} {}", data_type, null_suffix) 238 | } 239 | } 240 | -------------------------------------------------------------------------------- /rayql-engine/src/types.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, PartialEq, Clone)] 2 | pub enum DataType { 3 | String, 4 | Integer, 5 | Real, 6 | Blob, 7 | Boolean, 8 | Timestamp, 9 | Optional(Box), 10 | Enum(String), 11 | } 12 | 13 | impl std::fmt::Display for DataType { 14 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 15 | match self { 16 | DataType::String => write!(f, "String"), 17 | DataType::Integer => write!(f, "Integer"), 18 | DataType::Real => write!(f, "Real"), 19 | DataType::Blob => write!(f, "Blob"), 20 | DataType::Boolean => write!(f, "Boolean"), 21 | DataType::Timestamp => write!(f, "Timestamp"), 22 | DataType::Optional(inner) => write!(f, "Optional<{}>", inner), 23 | DataType::Enum(name) => write!(f, "Enum({})", name), 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /rayql-engine/src/value.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, PartialEq, Clone)] 2 | pub enum Value { 3 | StringLiteral(String), 4 | Integer(i64), 5 | Real(f64), 6 | Boolean(bool), 7 | } 8 | 9 | impl std::fmt::Display for Value { 10 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 11 | match self { 12 | Value::StringLiteral(s) => write!(f, "'{}'", s), 13 | Value::Integer(i) => write!(f, "{}", i), 14 | Value::Real(r) => write!(f, "{}", r), 15 | Value::Boolean(b) => write!(f, "{}", b), 16 | } 17 | } 18 | } 19 | 20 | impl Value { 21 | pub fn get_type(&self) -> rayql::types::DataType { 22 | match self { 23 | Value::StringLiteral(_) => rayql::types::DataType::String, 24 | Value::Integer(_) => rayql::types::DataType::Integer, 25 | Value::Real(_) => rayql::types::DataType::Real, 26 | Value::Boolean(_) => rayql::types::DataType::Boolean, 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /rayql-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /rayql-wasm/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rayql-wasm" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [lib] 9 | crate-type = ["cdylib"] 10 | 11 | [dependencies] 12 | rayql-engine.workspace = true 13 | wasm-bindgen.workspace = true 14 | -------------------------------------------------------------------------------- /rayql-wasm/src/lib.rs: -------------------------------------------------------------------------------- 1 | use wasm_bindgen::prelude::*; 2 | 3 | #[wasm_bindgen] 4 | pub fn to_sql(schema_src: &str) -> Result { 5 | let schema = match rayql_engine::Schema::parse(schema_src) { 6 | Ok(schema) => schema, 7 | Err(error) => { 8 | return Err(JsValue::from_str( 9 | &rayql_engine::error::pretty_error_message(&error, schema_src), 10 | )) 11 | } 12 | }; 13 | 14 | let sql = match schema.to_sql() { 15 | Ok(sql) => sql, 16 | Err(error) => { 17 | return Err(JsValue::from_str( 18 | &rayql_engine::error::pretty_to_sql_error_message(error, schema_src), 19 | )) 20 | } 21 | }; 22 | 23 | let sql_str = sql.join("\n\n"); 24 | Ok(JsValue::from_str(&sql_str)) 25 | } 26 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | use clap::{command, Args, Parser, Subcommand}; 2 | 3 | #[derive(Parser)] 4 | #[command(author, version, about, long_about = None)] 5 | pub struct Cli { 6 | #[command(subcommand)] 7 | pub command: Option, 8 | } 9 | 10 | #[derive(Subcommand)] 11 | pub enum Commands { 12 | Print, 13 | Db(DbArgs), 14 | } 15 | 16 | #[derive(Args)] 17 | #[command(flatten_help = true)] 18 | pub struct DbArgs { 19 | #[command(subcommand)] 20 | pub command: Option, 21 | } 22 | 23 | #[derive(Subcommand)] 24 | pub enum DbCommands { 25 | Push, 26 | } 27 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | 3 | #[tokio::main] 4 | async fn main() -> Result<(), Box> { 5 | let cli = rayql::Cli::parse(); 6 | 7 | match cli.command { 8 | Some(rayql::Commands::Print) => { 9 | print_schema(); 10 | 11 | Ok(()) 12 | } 13 | Some(rayql::Commands::Db(db_args)) => match db_args.command { 14 | Some(rayql::DbCommands::Push) => Ok(()), 15 | None => Ok(()), 16 | }, 17 | None => Ok(()), 18 | } 19 | } 20 | 21 | pub fn print_schema() { 22 | let current_dir = std::env::current_dir().expect("Failed to read current dir."); 23 | let file_path = current_dir.join("schema.rayql"); 24 | 25 | let code = match std::fs::read_to_string(file_path) { 26 | Ok(content) => content, 27 | Err(e) => { 28 | eprintln!("Error reading file: {}", e); 29 | std::process::exit(1); 30 | } 31 | }; 32 | 33 | let schema = match rayql_engine::Schema::parse(&code) { 34 | Ok(schema) => schema, 35 | Err(err) => { 36 | eprintln!("{}", rayql_engine::error::pretty_error_message(&err, &code)); 37 | std::process::exit(1); 38 | } 39 | }; 40 | 41 | let sql_statements = match schema.to_sql() { 42 | Ok(stmts) => stmts, 43 | Err(err) => { 44 | eprintln!( 45 | "{}", 46 | rayql_engine::error::pretty_to_sql_error_message(err, &code) 47 | ); 48 | std::process::exit(1); 49 | } 50 | }; 51 | 52 | let output = sql_statements.join("\n\n"); 53 | 54 | println!("{}", output); 55 | } 56 | --------------------------------------------------------------------------------