├── .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 |
5 |
6 |
7 | RayQL is a schema definition and query language for SQLite.
8 |
9 |
10 |
11 |
12 | 
13 | 
14 | 
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 |
--------------------------------------------------------------------------------