├── .gitignore ├── .vscode └── launch.json ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE.md ├── README.md ├── clippy.toml ├── surreal-codegen ├── Cargo.toml ├── cli.js ├── npm │ ├── index.js │ ├── install.js │ └── package.json └── src │ └── main.rs └── surreal_type_generator ├── Cargo.toml ├── src ├── lib.rs ├── step_1_parse_sql │ ├── global_parameters.rs │ ├── mod.rs │ ├── query.rs │ └── schema.rs ├── step_2_interpret │ ├── function.rs │ ├── mod.rs │ ├── object.rs │ ├── return_types.rs │ ├── schema │ │ └── mod.rs │ ├── statements │ │ ├── create_statement.rs │ │ ├── delete_statement.rs │ │ ├── insert_statement.rs │ │ ├── let_statement.rs │ │ ├── mod.rs │ │ ├── return_statement.rs │ │ ├── select_statement.rs │ │ ├── update_statement.rs │ │ └── upsert_statement.rs │ └── utils.rs ├── step_3_codegen │ ├── mod.rs │ └── typescript │ │ └── mod.rs └── utils │ ├── mod.rs │ └── printing.rs └── tests ├── casting_javascript.rs ├── complex_query.rs ├── constant_expression_tests.rs ├── create_statement_tests.rs ├── delete_statement_tests.rs ├── field_defaults.rs ├── functions.rs ├── insert_statement_tests.rs ├── let_statement_tests.rs ├── literals_tests.rs ├── object.rs ├── precomputed_views.rs ├── return_and_expressions.rs ├── schema_complex_types.rs ├── select_grouping.rs ├── select_statement_tests.rs ├── subquery_tests.rs ├── transaction.rs ├── typescript_generation.rs ├── update_statement_tests.rs ├── upsert_statement_tests.rs └── variable_usage_tests.rs /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | dist 3 | builds 4 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "lldb", 9 | "request": "launch", 10 | "name": "Debug executable 'surreal-codegen'", 11 | "cargo": { 12 | "args": [ 13 | "build", 14 | "--bin=surreal-codegen", 15 | "--package=surreal-codegen" 16 | ], 17 | "filter": { 18 | "name": "surreal-codegen", 19 | "kind": "bin" 20 | } 21 | }, 22 | "args": [], 23 | "cwd": "${workspaceFolder}" 24 | }, 25 | { 26 | "type": "lldb", 27 | "request": "launch", 28 | "name": "Debug unit tests in executable 'surreal-codegen'", 29 | "cargo": { 30 | "args": [ 31 | "test", 32 | "--no-run", 33 | "--bin=surreal-codegen", 34 | "--package=surreal-codegen" 35 | ], 36 | "filter": { 37 | "name": "surreal-codegen", 38 | "kind": "bin" 39 | } 40 | }, 41 | "args": [], 42 | "cwd": "${workspaceFolder}" 43 | }, 44 | { 45 | "type": "lldb", 46 | "request": "launch", 47 | "name": "Debug unit tests in library 'surreal_type_generator'", 48 | "cargo": { 49 | "args": [ 50 | "test", 51 | "--no-run", 52 | "--lib", 53 | "--package=surreal_type_generator" 54 | ], 55 | "filter": { 56 | "name": "surreal_type_generator", 57 | "kind": "lib" 58 | } 59 | }, 60 | "args": [], 61 | "cwd": "${workspaceFolder}" 62 | }, 63 | { 64 | "type": "lldb", 65 | "request": "launch", 66 | "name": "Debug integration test 'return_and_expressions'", 67 | "cargo": { 68 | "args": [ 69 | "test", 70 | "--no-run", 71 | "--test=return_and_expressions", 72 | "--package=surreal_type_generator" 73 | ], 74 | "filter": { 75 | "name": "return_and_expressions", 76 | "kind": "test" 77 | } 78 | }, 79 | "args": [], 80 | "cwd": "${workspaceFolder}" 81 | }, 82 | { 83 | "type": "lldb", 84 | "request": "launch", 85 | "name": "Debug integration test 'subquery_tests'", 86 | "cargo": { 87 | "args": [ 88 | "test", 89 | "--no-run", 90 | "--test=subquery_tests", 91 | "--package=surreal_type_generator" 92 | ], 93 | "filter": { 94 | "name": "subquery_tests", 95 | "kind": "test" 96 | } 97 | }, 98 | "args": [], 99 | "cwd": "${workspaceFolder}" 100 | }, 101 | { 102 | "type": "lldb", 103 | "request": "launch", 104 | "name": "Debug integration test 'object'", 105 | "cargo": { 106 | "args": [ 107 | "test", 108 | "--no-run", 109 | "--test=object", 110 | "--package=surreal_type_generator" 111 | ], 112 | "filter": { 113 | "name": "object", 114 | "kind": "test" 115 | } 116 | }, 117 | "args": [], 118 | "cwd": "${workspaceFolder}" 119 | }, 120 | { 121 | "type": "lldb", 122 | "request": "launch", 123 | "name": "Debug integration test 'casting_javascript'", 124 | "cargo": { 125 | "args": [ 126 | "test", 127 | "--no-run", 128 | "--test=casting_javascript", 129 | "--package=surreal_type_generator" 130 | ], 131 | "filter": { 132 | "name": "casting_javascript", 133 | "kind": "test" 134 | } 135 | }, 136 | "args": [], 137 | "cwd": "${workspaceFolder}" 138 | }, 139 | { 140 | "type": "lldb", 141 | "request": "launch", 142 | "name": "Debug integration test 'update_statement_tests'", 143 | "cargo": { 144 | "args": [ 145 | "test", 146 | "--no-run", 147 | "--test=update_statement_tests", 148 | "--package=surreal_type_generator" 149 | ], 150 | "filter": { 151 | "name": "update_statement_tests", 152 | "kind": "test" 153 | } 154 | }, 155 | "args": [], 156 | "cwd": "${workspaceFolder}" 157 | }, 158 | { 159 | "type": "lldb", 160 | "request": "launch", 161 | "name": "Debug integration test 'insert_statement_tests'", 162 | "cargo": { 163 | "args": [ 164 | "test", 165 | "--no-run", 166 | "--test=insert_statement_tests", 167 | "--package=surreal_type_generator" 168 | ], 169 | "filter": { 170 | "name": "insert_statement_tests", 171 | "kind": "test" 172 | } 173 | }, 174 | "args": [], 175 | "cwd": "${workspaceFolder}" 176 | }, 177 | { 178 | "type": "lldb", 179 | "request": "launch", 180 | "name": "Debug integration test 'functions'", 181 | "cargo": { 182 | "args": [ 183 | "test", 184 | "--no-run", 185 | "--test=functions", 186 | "--package=surreal_type_generator" 187 | ], 188 | "filter": { 189 | "name": "functions", 190 | "kind": "test" 191 | } 192 | }, 193 | "args": [], 194 | "cwd": "${workspaceFolder}" 195 | }, 196 | { 197 | "type": "lldb", 198 | "request": "launch", 199 | "name": "Debug integration test 'precomputed_views'", 200 | "cargo": { 201 | "args": [ 202 | "test", 203 | "--no-run", 204 | "--test=precomputed_views", 205 | "--package=surreal_type_generator" 206 | ], 207 | "filter": { 208 | "name": "precomputed_views", 209 | "kind": "test" 210 | } 211 | }, 212 | "args": [], 213 | "cwd": "${workspaceFolder}" 214 | }, 215 | { 216 | "type": "lldb", 217 | "request": "launch", 218 | "name": "Debug integration test 'variable_usage_tests'", 219 | "cargo": { 220 | "args": [ 221 | "test", 222 | "--no-run", 223 | "--test=variable_usage_tests", 224 | "--package=surreal_type_generator" 225 | ], 226 | "filter": { 227 | "name": "variable_usage_tests", 228 | "kind": "test" 229 | } 230 | }, 231 | "args": [], 232 | "cwd": "${workspaceFolder}" 233 | }, 234 | { 235 | "type": "lldb", 236 | "request": "launch", 237 | "name": "Debug integration test 'constant_expression_tests'", 238 | "cargo": { 239 | "args": [ 240 | "test", 241 | "--no-run", 242 | "--test=constant_expression_tests", 243 | "--package=surreal_type_generator" 244 | ], 245 | "filter": { 246 | "name": "constant_expression_tests", 247 | "kind": "test" 248 | } 249 | }, 250 | "args": [], 251 | "cwd": "${workspaceFolder}" 252 | }, 253 | { 254 | "type": "lldb", 255 | "request": "launch", 256 | "name": "Debug integration test 'select_grouping'", 257 | "cargo": { 258 | "args": [ 259 | "test", 260 | "--no-run", 261 | "--test=select_grouping", 262 | "--package=surreal_type_generator" 263 | ], 264 | "filter": { 265 | "name": "select_grouping", 266 | "kind": "test" 267 | } 268 | }, 269 | "args": [], 270 | "cwd": "${workspaceFolder}" 271 | }, 272 | { 273 | "type": "lldb", 274 | "request": "launch", 275 | "name": "Debug integration test 'select_statements_tests'", 276 | "cargo": { 277 | "args": [ 278 | "test", 279 | "--no-run", 280 | "--test=select_statements_tests", 281 | "--package=surreal_type_generator" 282 | ], 283 | "filter": { 284 | "name": "select_statements_tests", 285 | "kind": "test" 286 | } 287 | }, 288 | "args": [], 289 | "cwd": "${workspaceFolder}" 290 | }, 291 | { 292 | "type": "lldb", 293 | "request": "launch", 294 | "name": "Debug integration test 'typescript_generation'", 295 | "cargo": { 296 | "args": [ 297 | "test", 298 | "--no-run", 299 | "--test=typescript_generation", 300 | "--package=surreal_type_generator" 301 | ], 302 | "filter": { 303 | "name": "typescript_generation", 304 | "kind": "test" 305 | } 306 | }, 307 | "args": [], 308 | "cwd": "${workspaceFolder}" 309 | }, 310 | { 311 | "type": "lldb", 312 | "request": "launch", 313 | "name": "Debug integration test 'create_statement_tests'", 314 | "cargo": { 315 | "args": [ 316 | "test", 317 | "--no-run", 318 | "--test=create_statement_tests", 319 | "--package=surreal_type_generator" 320 | ], 321 | "filter": { 322 | "name": "create_statement_tests", 323 | "kind": "test" 324 | } 325 | }, 326 | "args": [], 327 | "cwd": "${workspaceFolder}" 328 | }, 329 | { 330 | "type": "lldb", 331 | "request": "launch", 332 | "name": "Debug integration test 'delete_statement_tests'", 333 | "cargo": { 334 | "args": [ 335 | "test", 336 | "--no-run", 337 | "--test=delete_statement_tests", 338 | "--package=surreal_type_generator" 339 | ], 340 | "filter": { 341 | "name": "delete_statement_tests", 342 | "kind": "test" 343 | } 344 | }, 345 | "args": [], 346 | "cwd": "${workspaceFolder}" 347 | } 348 | ] 349 | } 350 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | # Changelog 3 | 4 | ## v0.1.0 5 | - Initial release 6 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | 2 | # Contributing 3 | 4 | 1. Clone the git repository 5 | - `git clone https://github.com/siteforge-io/surreal-codegen.git` 6 | 2. Run `cargo test` to run the tests - and this will build the project 7 | 3. Before you start writing code, ideally first open an issue with a minimum reproducible example of the bug or feature you want to add 8 | 3. Using the minimum reproduction, first write a failing test. 9 | 4. Write the code to fix/solve the issue. 10 | 5. Run the tests, and ensure they all pass. 11 | 6. In a fork, perform a git commit, with a message that includes the `Closes #issue_number` 12 | 7. Submit a pull request 13 | 8. Wait for the pull request to be reviewed and merged 14 | 15 | 16 | ## Releasing (for maintainers) 17 | 18 | 1. Push a git commit into main 19 | 2. To install, run `cargo install --git https://github.com/siteforge-io/surreal-codegen.git` 20 | 21 | ## Local Installation 22 | 23 | 1. Clone the git repository 24 | - `git clone https://github.com/siteforge-io/surreal-codegen.git` 25 | 2. Run `cargo install --path surreal-codegen` 26 | 3. Run `surreal-codegen --help` to see the available commands 27 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | 4 | members = ["surreal-codegen", "surreal_type_generator"] 5 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Albert Marashi 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 | # `surreal-codegen` 2 | > [!WARNING] 3 | > This is a WIP, but we are currently using it in production at [Siteforge](https://siteforge.io) to help ensure type safety in our SurrealDB queries. 4 | > See the [Features Supported](#features-supported) section for a list of features we currently support. 5 | 6 | # Installation 7 | > [!WARNING] 8 | > We haven't currently setup a build automation system, so you must build via the manual installation instructions below. 9 | 10 | 11 | ## Manual Installation 12 | You must have the rust toolchain installed, then run: 13 | 14 | ```sh 15 | cargo install --git https://github.com/siteforge-io/surreal-codegen.git 16 | ``` 17 | Or, if you have cloned the repo: 18 | ```sh 19 | cargo install --path surreal-codegen 20 | ``` 21 | 22 | ## Running `surreal-codegen` 23 | ```sh 24 | surreal-codegen --help 25 | ``` 26 | 27 | ``` 28 | Usage: surreal-codegen [OPTIONS] --dir --schema 29 | 30 | Options: 31 | -d, --dir The directory containing the Surql files 32 | -s, --schema 33 | -o, --output The name of the output file default of `types.ts` [default: ./types.ts] 34 | --header
Header to add to the top of the output file If you specify this, you must import in RecordId type and a Surreal class that has a .query(query: string, variables?: Record) method [default: "import { type RecordId, Surreal } from 'surrealdb'"] 35 | -h, --help Print help 36 | ``` 37 | 38 | # Usage 39 | 40 | ## Schema Example 41 | 42 | `./schema.surql` 43 | ```ts 44 | DEFINE TABLE user SCHEMAFULL; 45 | DEFINE FIELD id ON user TYPE string; 46 | DEFINE FIELD email ON user TYPE string 47 | VALUE string::lowercase($value) 48 | ASSERT string::is::email($value); 49 | DEFINE FIELD password ON user TYPE string 50 | VALUE crypto::bcrypt::generate($value); 51 | DEFINE FIELD name ON user TYPE string 52 | VALUE string::trim($value); 53 | DEFINE FIELD created_at ON user TYPE datetime 54 | VALUE time::now() 55 | READONLY; 56 | ``` 57 | 58 | ## Query Example 59 | 60 | `./queries/create_user.surql` 61 | ```ts 62 | CREATE user CONTENT $user; 63 | ``` 64 | 65 | 66 | ## Codegen 67 | This wil generate a `types.ts` file in the current directory, which includes all your queries, as well as some prototype and type overrides for the SurrealDB database to allow you to use the generated types in your TypeScript code. 68 | ```sh 69 | surreal-codegen \ 70 | --schema ./schema.surql \ 71 | --dir ./queries \ 72 | --output ./queries.ts 73 | ``` 74 | 75 | ## TypeScript usage 76 | ```ts 77 | import { TypedSurreal, CreateUserQuery } from "./queries" 78 | 79 | const db = new TypedSurreal() 80 | 81 | await db.connect(...) 82 | /* 83 | Result is typed as CreateUserResult from the generated types.ts file 84 | */ 85 | const [created_users] = await db.typed(CreateUserQuery, { 86 | user: { 87 | name: "John Doe", 88 | email: "john@doe.com", 89 | password: "123456", 90 | } // can also be an array of users 91 | }) 92 | ``` 93 | 94 | ## Typing parameters 95 | 96 | We exploit the SurrealDB casting system to infer the types of parameters, for places where they cannot be inferred from the query itself. 97 | 98 | All you must do is add a casting annotation with the parameter name, eg: 99 | 100 | ```sql 101 | -- Casting syntax in SurrealDB. 102 | $email; 103 | ``` 104 | 105 | This will allow the codegen to infer the type of `$email` variable as a `string`. 106 | 107 | ### Example: 108 | 109 | `./queries/reset_password.surql` 110 | ```sql 111 | > $user; 112 | $password; 113 | 114 | UPDATE ONLY $user 115 | SET password = $password 116 | ``` 117 | 118 | ### Global parameters 119 | You can also define global parameters in a `global.surql` file, which will be available to all queries in the directory, this is useful things like typing the $auth parameters available in SurrealDB across all queries. 120 | 121 | `./queries/globals.surql` 122 | ```sql 123 | > $auth; 124 | ``` 125 | 126 | ## Overriding the default file header 127 | You can override the default imported classes by specifying the `--header` option. You must include a RecordID type import, and a Surreal class that contains 128 | a `.query(query: string, variables?: Record)` method. 129 | 130 | You can also use this to specify a comment to be added to the top of the generated file, such as ESLint ignore comments. 131 | Or alternatively, you can ignore the generated file by including the file in your eslint ignore list. 132 | 133 | ### Example 134 | ```sh 135 | surreal-codegen \ 136 | --schema ./schema.surql \ 137 | --dir ./queries \ 138 | --output ./queries.ts \ 139 | --header "import { RecordId, Surreal } from 'my-custom-surreal-class'" 140 | ``` 141 | 142 | 143 | 144 | # Features Supported 145 | 146 | ### Notes 147 | - We only currently support `SCHEMAFULL` tables so far, but we are working on supporting other table types. 148 | 149 | ### General Type Support and Handling 150 | - [x] `never` 151 | - [x] `unknown` 152 | - [x] `string` 153 | - [x] `int` 154 | - [x] `float` 155 | - [x] `datetime` 156 | - [x] `duration` 157 | - [x] `decimal` 158 | - [x] `bool` 159 | - [x] `record` 160 | - [x] `option` 161 | - [x] `array` 162 | - [x] `object` 163 | - [x] `number` 164 | - [x] `NULL` 165 | - [x] `NONE` (for `Option`) 166 | - [x] `any` 167 | - [x] `foo | bar` Unions (mixed return type unions) 168 | - [x] Surreal 2.0 typed literals (eg: `"foo"`, `123`, `1d`, `{ foo: 123 }`, `array<1|2>`) 169 | - [ ] GEOJson types (eg: `point`, `line`, `polygon`) 170 | - [x] Typed `id` record ID values for tables, eg: `DEFINE FIELD id ON user TYPE string` 171 | 172 | ## Objects 173 | - [x] `RETURN { foo: 1, bar: 2 }` 174 | 175 | ## Automatic Parameter Inference 176 | 177 | ### General 178 | - [ ] `WHERE foo = $bar` parameter inference 179 | - [ ] `fn::foo($bar)` function calling parameter inference 180 | 181 | ### `SELECT` statements 182 | - [x] `*` all fields 183 | - [x] `foo.bar` field access 184 | - [x] `foo as bar` field alias 185 | - [ ] `foo.{bar, baz}` destructuring access. 186 | - [x] `FROM` targets 187 | - [x] `VALUE` 188 | - [x] `GROUP BY` 189 | - [x] `GROUP ALL` 190 | - [ ] `SPLIT` fields 191 | - [ ] `FETCH` fields 192 | 193 | ### `DELETE` statements 194 | - [x] `FROM` targets 195 | - [x] `RETURN BEFORE` 196 | - [x] `RETURN AFTER` 197 | - [ ] `RETURN DIFF` 198 | - [x] `RETRUN @statement_param` with `$before` field access 199 | 200 | ### `INSERT` statements 201 | - [x] `INSERT INTO baz $foo` parameter inference 202 | - [ ] `INSERT INTO baz { foo: $bar }` parameter inference 203 | - [ ] `INSERT INTO baz ... ON DUPLICATE KEY UPDATE foo = $bar` parameter inference 204 | 205 | ### `RELATE` statements 206 | - [ ] TODO 207 | 208 | ### `DEFINE TABLE .. AS` precomputed tables 209 | - [X] `DEFINE TABLE foo AS SELECT ... FROM bar` 210 | - [X] `DEFINE TABLE foo AS SELECT ... FROM bar GROUP BY ...` 211 | - [X] `DEFINE TABLE foo AS SELECT ... FROM bar GROUP ALL` 212 | 213 | 214 | ### `UPDATE` statements 215 | - [x] `RETURN BEFORE` 216 | - [x] `RETURN AFTER` 217 | - [ ] `RETURN DIFF` 218 | - [x] `RETRUN @statement_param` with `$before` and `$after` field access 219 | - [ ] `CONTENT $foo` parameter inference 220 | - [ ] `CONTENT { foo: $bar }` parameter inference 221 | - [ ] `SET foo = $bar` parameter inference 222 | - [ ] `MERGE $bar` parameter inference 223 | - [ ] `MERGE { foo: $bar }` parameter inference 224 | - [ ] `PATCH ...` parameter inference 225 | 226 | 227 | ### `CREATE` statements 228 | - [ ] `CREATE baz SET foo = $bar` parameter inference 229 | - [ ] `CREATE baz CONTENT { foo: $bar }` parameter inference 230 | - [x] `CREATE baz CONTENT $foo` parameter inference 231 | - [x] `RETURN BEFORE` 232 | - [x] `RETURN AFTER` 233 | - [ ] `RETURN DIFF` 234 | - [x] `RETRUN @statement_param` with `$after` field access 235 | 236 | ### `UPSERT` statements 237 | - [X] `RETURN BEFORE` 238 | - [X] `RETURN AFTER` 239 | - [X] `RETURN DIFF` 240 | - [X] `RETRUN @statement_param` with `$after` field access 241 | - [x] `CONTENT $foo` parameter inference 242 | - [ ] `SET foo = $bar` parameter inference 243 | - [ ] `MERGE { foo: $bar }` parameter inference 244 | - [ ] `CONTENT { foo: $bar }` parameter inference 245 | - [X] `MERGE $foo` parameter inference 246 | - [ ] `PATCH ...` parameter inference 247 | 248 | 249 | ### Value expressions 250 | #### Idiom/path expressions 251 | - [x] `foo.bar` 252 | - [x] `foo.*` for arrays 253 | - [x] `foo.*` for objects 254 | - [ ] `foo[0]` 255 | - [ ] edge traversal eg: `foo->bar<-baz` 256 | 257 | #### Literal/constant expressions 258 | - [x] `true` 259 | - [x] `false` 260 | - [x] `null` 261 | - [x] `"string"` 262 | - [x] `123` 263 | - [x] `123.456` 264 | - [x] `[1, 2, 3]` 265 | - [x] `{"foo": "bar"}` 266 | 267 | #### Comparison expressions 268 | - [x] `foo == "bar"` 269 | - [x] `foo != "bar"` 270 | - [x] `foo < "bar"` 271 | - [x] `foo <= "bar"` 272 | - [x] `foo > "bar"` 273 | - [x] `foo >= "bar"` 274 | 275 | #### Subquery expressions 276 | - [x] `SELECT` statements 277 | - [x] `DELETE` statements 278 | - [X] `INSERT` statements 279 | - [x] `UPDATE` statements 280 | - [x] `CREATE` statements 281 | - [ ] `RELATE` statements 282 | - [ ] `UPSERT` statements 283 | 284 | ### Parameter expressions 285 | - [x] Custom global `$param` definitions in a `global.surql` file 286 | - [x] `$auth` 287 | - [x] `$session` 288 | - [x] `$scope` 289 | - [x] `$input` 290 | - [x] `$token` 291 | - [ ] built-in parameters 292 | - [x] `$this` 293 | - [x] `$parent` 294 | - [x] `$after` 295 | - [x] `$before` 296 | - [ ] Automatic parameter inference in some cases 297 | 298 | ### Other Statements 299 | - [ ] `IF ELSE` 300 | - [ ] `FOR` 301 | - [ ] `CONTINUE` 302 | - [ ] `BREAK` 303 | - [x] `RETURN` 304 | - [X] `BEGIN` 305 | - [X] `COMMIT` 306 | - [ ] `LET` 307 | - [ ] `ABORT` 308 | - [ ] `THROW` 309 | 310 | ### `LET` statement 311 | - [x] `LET` statement 312 | ```surql 313 | -- If we can't infer the type of the `LET` statement 314 | -- you can use a type annotation 315 | LET $id: record = $foo.id; 316 | 317 | UPSERT ONLY $id CONTENT $foo; 318 | ``` 319 | 320 | 321 | ## Contributing 322 | 323 | We welcome contributions to this project, please see our [Contributing Guide](CONTRIBUTING.md) for more information. 324 | 325 | ## License 326 | 327 | This project is licensed under the [MIT License](LICENSE.md). 328 | -------------------------------------------------------------------------------- /clippy.toml: -------------------------------------------------------------------------------- 1 | 2 | warn = [ 3 | "clippy::all", 4 | "clippy::pedantic", 5 | "clippy::nursery", 6 | ] 7 | -------------------------------------------------------------------------------- /surreal-codegen/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "surreal-codegen" 3 | version = "0.3.3" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | surreal_type_generator = { path = "../surreal_type_generator", version = "0.1.0" } 8 | anyhow = "1.0.66" 9 | clap = { version = "4.5.9", features = ["derive"] } 10 | colored = "2.1.0" 11 | semver = "1.0" 12 | toml = "0.5" 13 | reqwest = { version = "0.12.8", features = [ 14 | "json", 15 | "rustls-tls", 16 | "blocking", 17 | "rustls-tls-webpki-roots", 18 | ] } 19 | -------------------------------------------------------------------------------- /surreal-codegen/cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import surreal_codegen from './pkg/surreal_codegen.js'; 3 | 4 | async function main() { 5 | await surreal_codegen.default(); // This loads the WASM module 6 | surreal_codegen.main(); 7 | } 8 | 9 | main().catch(console.error); 10 | -------------------------------------------------------------------------------- /surreal-codegen/npm/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import { spawn } from 'child_process'; 3 | import path from 'path'; 4 | import { fileURLToPath } from 'url'; 5 | 6 | const __filename = fileURLToPath(import.meta.url); 7 | const __dirname = path.dirname(__filename); 8 | 9 | const binaryPath = path.join(__dirname, 'surreal-codegen'); 10 | const child = spawn(binaryPath, process.argv.slice(2), { stdio: 'inherit' }); 11 | 12 | child.on('error', (err) => { 13 | console.error('Failed to start subprocess.', err); 14 | }); 15 | 16 | child.on('close', (code) => { 17 | process.exit(code); 18 | }); 19 | -------------------------------------------------------------------------------- /surreal-codegen/npm/install.js: -------------------------------------------------------------------------------- 1 | import https from 'https'; 2 | import fs from 'fs/promises'; 3 | import path from 'path'; 4 | import { exec } from 'child_process'; 5 | 6 | const version = process.env.npm_package_version; 7 | const platform = process.platform; 8 | const arch = process.arch; 9 | 10 | let binaryName; 11 | switch(platform) { 12 | case 'win32': 13 | binaryName = arch === 'x64' ? 'surreal-codegen-x86_64-pc-windows-gnu.tar.gz' : 'surreal-codegen-i686-pc-windows-gnu.tar.gz'; 14 | break; 15 | case 'linux': 16 | binaryName = 'surreal-codegen-x86_64-unknown-linux-gnu.tar.gz'; 17 | break; 18 | case 'darwin': 19 | binaryName = 'surreal-codegen-x86_64-apple-darwin.tar.gz'; 20 | break; 21 | default: 22 | console.error('Unsupported platform:', platform); 23 | process.exit(1); 24 | } 25 | 26 | const url = `https://github.com/siteforge-io/surreal-codegen/releases/download/v${version}/${binaryName}`; 27 | const outputPath = path.join(__dirname, binaryName); 28 | 29 | const downloadBinary = async () => { 30 | try { 31 | const response = await new Promise((resolve, reject) => { 32 | https.get(url, resolve).on('error', reject); 33 | }); 34 | 35 | if (response.statusCode === 302) { 36 | const redirectResponse = await new Promise((resolve, reject) => { 37 | https.get(response.headers.location, resolve).on('error', reject); 38 | }); 39 | 40 | await new Promise((resolve, reject) => { 41 | redirectResponse.pipe(fs.createWriteStream(outputPath)) 42 | .on('finish', resolve) 43 | .on('error', reject); 44 | }); 45 | 46 | console.log('Binary downloaded successfully'); 47 | 48 | await new Promise((resolve, reject) => { 49 | exec(`tar -xzf ${outputPath} -C ${__dirname}`, (error) => { 50 | if (error) { 51 | console.error('Error extracting binary:', error); 52 | reject(error); 53 | } else { 54 | resolve(); 55 | } 56 | }); 57 | }); 58 | 59 | await fs.unlink(outputPath); 60 | console.log('Binary extracted successfully'); 61 | } else { 62 | throw new Error('Failed to download binary'); 63 | } 64 | } catch (err) { 65 | console.error('Error:', err.message); 66 | process.exit(1); 67 | } 68 | }; 69 | 70 | downloadBinary(); 71 | -------------------------------------------------------------------------------- /surreal-codegen/npm/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@siteforge/surreal-codegen", 3 | "version": "0.1.0", 4 | "description": "Surreal Code Generator", 5 | "bin": { 6 | "surreal-codegen": "./index.js" 7 | }, 8 | "scripts": { 9 | "postinstall": "node install.js" 10 | }, 11 | "files": [ 12 | "index.js", 13 | "install.js" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /surreal-codegen/src/main.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | use colored::Colorize; 3 | use reqwest; 4 | use semver::Version; 5 | use std::{collections::BTreeMap, path::PathBuf, sync::Arc}; 6 | use surreal_type_generator::{ 7 | step_1_parse_sql, step_2_interpret, 8 | step_3_codegen::{self}, 9 | utils::printing::indent, 10 | }; 11 | 12 | const CURRENT_VERSION: &str = env!("CARGO_PKG_VERSION"); 13 | 14 | #[derive(Parser)] 15 | struct Cli { 16 | /// The directory containing the Surql files 17 | #[clap(short, long)] 18 | dir: String, 19 | 20 | // The database schema file 21 | #[clap(short, long)] 22 | schema: String, 23 | 24 | /// The name of the output file 25 | /// default of `types.ts` 26 | #[clap(short, long, default_value = "./types.ts")] 27 | output: String, 28 | 29 | /// Header to add to the top of the output file 30 | /// If you specify this, you must import in RecordId type and a Surreal class that has a .query(query: string, variables?: Record) method 31 | #[clap( 32 | long, 33 | default_value = "import { type RecordId, Surreal } from 'surrealdb'" 34 | )] 35 | header: String, 36 | } 37 | 38 | fn fetch_latest_version() -> Option { 39 | let client = reqwest::blocking::Client::new(); 40 | let resp = client 41 | .get("https://raw.githubusercontent.com/siteforge-io/surreal-codegen/main/surreal-codegen/Cargo.toml") 42 | .header("User-Agent", "surreal-codegen") 43 | .send() 44 | .ok()?; 45 | 46 | let toml_content = resp.text().ok()?; 47 | let parsed_toml: toml::Value = toml::from_str(&toml_content).ok()?; 48 | let version_str = parsed_toml.get("package")?.get("version")?.as_str()?; 49 | 50 | Version::parse(version_str).ok() 51 | } 52 | 53 | pub fn main() { 54 | let result = std::panic::catch_unwind(|| match interpret() { 55 | Ok(_) => {} 56 | Err(err) => { 57 | eprintln!( 58 | "{}\n{}", 59 | " ✕ Error: ".on_bright_red().bright_white().bold(), 60 | err.to_string() 61 | ); 62 | 63 | println!( 64 | "\n{}\n{}", 65 | indent(&"If you expected this query to work, please file an issue at:".white()), 66 | indent(&"https://github.com/siteforge-io/surreal-codegen/issues".bright_cyan()), 67 | ); 68 | } 69 | }); 70 | 71 | check_latest_version(); 72 | 73 | match result { 74 | Ok(_) => {} 75 | Err(e) => { 76 | panic!("Unexpected panic: {:#?}", e); 77 | } 78 | } 79 | } 80 | 81 | fn check_latest_version() { 82 | if let Some(latest_version) = fetch_latest_version() { 83 | let current_version = Version::parse(CURRENT_VERSION).unwrap(); 84 | if latest_version > current_version { 85 | println!( 86 | "{}", 87 | format!( 88 | "{} A new version of {} is available: {}", 89 | "⚠".white().bold(), 90 | "surreal-codegen".bright_white(), 91 | latest_version.to_string().bright_white() 92 | ) 93 | .white() 94 | .on_red() 95 | ); 96 | println!( 97 | " You're currently using version {}", 98 | CURRENT_VERSION.bright_yellow() 99 | ); 100 | println!( 101 | " Update with: {}", 102 | "cargo install --force --git https://github.com/siteforge-io/surreal-codegen.git" 103 | .bright_cyan() 104 | ); 105 | println!(); 106 | } else { 107 | println!( 108 | "{}", 109 | format!( 110 | "{} You're using the latest version of surreal-codegen: {}", 111 | "✓".bright_green().bold(), 112 | CURRENT_VERSION.bright_green() 113 | ) 114 | ); 115 | } 116 | } else { 117 | println!( 118 | "{} Failed to fetch latest {} version from GitHub", 119 | "✗".red().bold(), 120 | "surreal-codegen".bright_cyan() 121 | ); 122 | } 123 | } 124 | 125 | pub fn interpret() -> anyhow::Result<()> { 126 | let cli = Cli::parse(); 127 | 128 | let mut files = step_3_codegen::read_surql_files(&cli.dir)?; 129 | 130 | let globals = if let Some(globals) = files.remove("globals.surql") { 131 | println!( 132 | "{} {}", 133 | "➜".bright_green().bold(), 134 | "Parsing globals.surql".white() 135 | ); 136 | step_1_parse_sql::parse_value_casts(&globals)? 137 | } else { 138 | BTreeMap::new() 139 | }; 140 | 141 | let schema = step_3_codegen::read_file(&PathBuf::from(&cli.schema))?; 142 | println!( 143 | "{} {} '{}'", 144 | "➜".bright_green().bold(), 145 | "Parsing schema in".white(), 146 | cli.schema.bright_green() 147 | ); 148 | let state = step_2_interpret::interpret_schema(&schema, globals)?; 149 | println!("{} {}", "➜".bright_green().bold(), "Parsed schema".white()); 150 | let state = Arc::new(state); 151 | 152 | let mut types = Vec::new(); 153 | 154 | for (file_name, query) in files { 155 | println!( 156 | "{} {} '{}'", 157 | "➜".bright_green().bold(), 158 | "Interpreting".white(), 159 | file_name.bright_green() 160 | ); 161 | let type_info = match step_3_codegen::generate_type_info(&file_name, &query, state.clone()) 162 | { 163 | Ok(type_info) => type_info, 164 | Err(err) => anyhow::bail!( 165 | "{} {}\n'{}'", 166 | " ✕ Error Parsing: ".bright_red().bold(), 167 | file_name.bright_green(), 168 | indent(&err.to_string()), 169 | ), 170 | }; 171 | 172 | types.push(type_info); 173 | } 174 | 175 | println!( 176 | "{} {}", 177 | "➜".bright_green().bold(), 178 | "Generating typescript output".white() 179 | ); 180 | 181 | let output = step_3_codegen::typescript::generate_typescript_output(&types, &cli.header)?; 182 | 183 | colored::control::unset_override(); 184 | 185 | std::fs::write(&cli.output, output)?; 186 | println!( 187 | "{} {} '{}'", 188 | "➜".bright_green().bold(), 189 | "Wrote output to".white(), 190 | cli.output.bright_green() 191 | ); 192 | 193 | Ok(()) 194 | } 195 | -------------------------------------------------------------------------------- /surreal_type_generator/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "surreal_type_generator" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | surrealdb = { version = "2.1.4", features = ["kv-mem"] } 8 | # wait for https://github.com/surrealdb/surrealdb/pull/4889 9 | # surrealdb = { git = "https://github.com/surrealdb/surrealdb.git", rev = "3daa84ca8988a303decdef09ca5252b53331143a" } 10 | anyhow = "1.0.83" 11 | serde_json = "1.0.117" 12 | colored = "2.1.0" 13 | 14 | # add pretty assertions but only for tests 15 | [dev-dependencies] 16 | pretty_assertions_sorted = "1.2.3" 17 | -------------------------------------------------------------------------------- /surreal_type_generator/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![feature(box_patterns)] 2 | 3 | pub mod step_1_parse_sql; 4 | pub mod step_2_interpret; 5 | pub mod step_3_codegen; 6 | pub use step_3_codegen::QueryResult; 7 | pub use surrealdb::sql::Kind; 8 | pub use surrealdb::sql::{Duration, Literal, Number}; 9 | pub mod utils; 10 | 11 | pub use utils::printing::type_info_to_string; 12 | pub use utils::printing::PrettyString; 13 | 14 | #[macro_export] 15 | macro_rules! var_map { 16 | {$($key:tt : $value:expr),* $(,)?} => { 17 | { 18 | #[allow(unused_mut)] 19 | let mut map = std::collections::BTreeMap::new(); 20 | $( 21 | map.insert(stringify!($key).to_string(), $value); 22 | )* 23 | map 24 | } 25 | }; 26 | } 27 | 28 | #[macro_export] 29 | macro_rules! kind { 30 | 31 | // Match simple kinds by identifier. 32 | (Any) => { $crate::Kind::Any }; 33 | (Null) => { $crate::Kind::Null }; 34 | (Bool) => { $crate::Kind::Bool }; 35 | (Bytes) => { $crate::Kind::Bytes }; 36 | (Datetime) => { $crate::Kind::Datetime }; 37 | (Decimal) => { $crate::Kind::Decimal }; 38 | (Duration) => { $crate::Kind::Duration }; 39 | (Float) => { $crate::Kind::Float }; 40 | (Int) => { $crate::Kind::Int }; 41 | (Number) => { $crate::Kind::Number }; 42 | (Object) => { $crate::Kind::Object }; 43 | (Point) => { $crate::Kind::Point }; 44 | (String) => { $crate::Kind::String }; 45 | (Uuid) => { $crate::Kind::Uuid }; 46 | (Range) => { $crate::Kind::Range }; 47 | (Record [$($table:tt),+ $(,)?]) => { 48 | $crate::Kind::Record(vec![$($table.into()),+]) 49 | }; 50 | 51 | // Match Array literal with one element as Kind::Array(Box::new(kind!($elem))) 52 | ([ $expr:expr ]) => { 53 | $crate::Kind::Array(Box::new($expr), None) 54 | }; 55 | 56 | // Match array literals with elements that are expressions. 57 | // [ $($elem:expr),* $(,)? ] => { 58 | // $crate::Kind::Literal($crate::Literal::Array(vec![$($elem),*])) 59 | // }; 60 | 61 | // Match object literals with values that are expressions. 62 | ({ $($key:tt : $value:expr),* $(,)? }) => { 63 | $crate::Kind::Literal($crate::Literal::Object({ 64 | #[allow(unused_mut)] 65 | let mut map = std::collections::BTreeMap::new(); 66 | $( 67 | let key_str = kind!(@key_to_string $key); 68 | map.insert(key_str, kind!($value)); 69 | )* 70 | map 71 | })) 72 | }; 73 | 74 | // Type where expr is a hashmap 75 | (Obj $expr:expr) => { 76 | $crate::Kind::Literal($crate::Literal::Object($expr)) 77 | }; 78 | 79 | // Type where expr is a vector 80 | (Arr $expr:expr) => { 81 | $crate::Kind::Array(Box::new($expr), None) 82 | }; 83 | 84 | // Recursive case for `Option` with expression support. 85 | (Opt($inner:expr)) => { 86 | $crate::Kind::Option(Box::new($inner)) 87 | }; 88 | 89 | // Recursive case for `Either`. 90 | (Either[$($inner:expr),+ $(,)?]) => { 91 | $crate::Kind::Either(vec![$(kind!($inner)),+]) 92 | }; 93 | 94 | // Cases for `Set` with and without size. 95 | (Set[$($inner:tt)+]) => { 96 | $crate::kind_set!($($inner)+) 97 | }; 98 | 99 | // Helper to parse function arguments. 100 | (@parse_args [$($args:expr),*]) => { 101 | $(kind!($args)),* 102 | }; 103 | 104 | // Helper to convert keys to strings. 105 | (@key_to_string $key:ident) => { stringify!($key).to_string() }; 106 | (@key_to_string $key:literal) => { $key.to_string() }; 107 | 108 | // Fallback case for expressions. 109 | ($e:expr) => { $e }; 110 | } 111 | 112 | #[macro_export] 113 | macro_rules! kind_set { 114 | ($kind:expr, $size:expr) => { 115 | $crate::Kind::Set(Box::new(kind!($kind)), Some($size)) 116 | }; 117 | ($kind:expr) => { 118 | $crate::Kind::Set(Box::new(kind!($kind)), None) 119 | }; 120 | } 121 | 122 | #[macro_export] 123 | macro_rules! kind_array { 124 | ($kind:expr, $size:expr) => { 125 | $crate::Kind::Array(Box::new(kind!($kind)), Some($size)) 126 | }; 127 | ($kind:expr) => { 128 | $crate::Kind::Array(Box::new(kind!($kind)), None) 129 | }; 130 | } 131 | -------------------------------------------------------------------------------- /surreal_type_generator/src/step_1_parse_sql/global_parameters.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeMap; 2 | 3 | use surrealdb::sql::{parse, Cast, Param, Value}; 4 | 5 | pub fn parse_value_casts(query: &str) -> Result, anyhow::Error> { 6 | let mut parameter_types = BTreeMap::new(); 7 | 8 | for stmt in parse(query)?.into_iter() { 9 | match stmt { 10 | surrealdb::sql::Statement::Value(Value::Cast(box Cast { 11 | 0: kind, 12 | 1: Value::Param(Param { 0: ident, .. }), 13 | .. 14 | })) => { 15 | parameter_types.insert(ident.to_string(), kind); 16 | } 17 | _ => anyhow::bail!("Only casts eg: ` $param;` are supported in globals.surql"), 18 | } 19 | } 20 | 21 | Ok(parameter_types) 22 | } 23 | -------------------------------------------------------------------------------- /surreal_type_generator/src/step_1_parse_sql/mod.rs: -------------------------------------------------------------------------------- 1 | mod global_parameters; 2 | mod query; 3 | mod schema; 4 | 5 | pub use global_parameters::*; 6 | pub use query::*; 7 | pub use schema::*; 8 | 9 | // #[derive(Debug, Clone)] 10 | // pub struct ParseState { 11 | // pub global: Arc>>, 12 | // pub inferred: Arc>>, 13 | // pub defined: Arc>>, 14 | // pub locals: HashMap, 15 | // } 16 | 17 | // impl ParseState { 18 | // /// Look up a parameter, moving up in the scope chain until it is found 19 | // pub fn get(&self, param_name: &str) -> Option { 20 | // if let Some(return_type) = self.locals.get(param_name) { 21 | // return Some(return_type.clone()); 22 | // } else if let Some(return_type) = self.defined.lock().unwrap().get(param_name) { 23 | // return Some(return_type.clone()); 24 | // } else if let Some(return_type) = self.inferred.lock().unwrap().get(param_name) { 25 | // return Some(return_type.clone()); 26 | // } else if let Some(return_type) = self.global.lock().unwrap().get(param_name) { 27 | // return Some(return_type.clone()); 28 | // } 29 | 30 | // None 31 | // } 32 | 33 | // pub fn has(&self, param_name: &str) -> bool { 34 | // self.get(param_name).is_some() 35 | // } 36 | // } 37 | 38 | // #[cfg(test)] 39 | // mod tests { 40 | // use pretty_assertions_sorted::assert_eq_sorted; 41 | // use surrealdb::sql::Table; 42 | 43 | // use super::*; 44 | 45 | // #[test] 46 | // fn parse_tables() -> anyhow::Result<()> { 47 | // let schema = r#" 48 | // DEFINE TABLE user SCHEMAFULL; 49 | // DEFINE FIELD name ON user TYPE string; 50 | // DEFINE FIELD age ON user TYPE int; 51 | // DEFINE FIELD bool ON user TYPE bool; 52 | // DEFINE FIELD datetime ON user TYPE datetime; 53 | // DEFINE FIELD duration ON user TYPE duration; 54 | // DEFINE FIELD decimal ON user TYPE decimal; 55 | // DEFINE FIELD xyz ON user TYPE record; 56 | // DEFINE FIELD arr ON user TYPE array; 57 | // DEFINE FIELD nested_obj.abc ON user TYPE string; 58 | // DEFINE FIELD nested_obj.xyz ON user TYPE string; 59 | // DEFINE FIELD nested_arr.*.foo ON user TYPE string; 60 | // DEFINE FIELD nested_arr.*.bar ON user TYPE string; 61 | // DEFINE FIELD bar.* ON user TYPE string; 62 | // "#; 63 | 64 | // let tables = get_tables(&parse_sql(schema)?)?; 65 | 66 | // let expected_table = TableParsed { 67 | // name: "user".into(), 68 | // fields: [ 69 | // ("id".into(), Kind::Record(vec!["user".into()])), 70 | // ("name".into(), Kind::String), 71 | // ("age".into(), Kind::Int), 72 | // ("bool".into(), Kind::Bool), 73 | // ("datetime".into(), Kind::Datetime), 74 | // ("duration".into(), Kind::Duration), 75 | // ("decimal".into(), Kind::Decimal), 76 | // ( 77 | // "xyz".into(), 78 | // Kind::Record(vec![Table::from("xyz")]), 79 | // ), 80 | // ( 81 | // "arr".into(), 82 | // Kind::Array(Box::new(Kind::String)), 83 | // ), 84 | // ( 85 | // "nested_obj".into(), 86 | // Kind::Object(HashMap::from([ 87 | // ("abc".into(), Kind::String), 88 | // ("xyz".into(), Kind::String), 89 | // ])), 90 | // ), 91 | // ( 92 | // "nested_arr".into(), 93 | // Kind::Array(Box::new(Kind::Object(HashMap::from([ 94 | // ("bar".into(), Kind::String), 95 | // ("foo".into(), Kind::String), 96 | // ])))), 97 | // ), 98 | // ( 99 | // "bar".into(), 100 | // Kind::Array(Box::new(Kind::String)), 101 | // ), 102 | // ] 103 | // .into(), 104 | // }; 105 | 106 | // assert_eq_sorted!(tables, [("user".into(), expected_table),].into()); 107 | 108 | // Ok(()) 109 | // } 110 | 111 | // #[test] 112 | // fn parse_views() -> anyhow::Result<()> { 113 | // let schema = r#" 114 | // DEFINE TABLE user SCHEMAFULL; 115 | // DEFINE FIELD name ON user TYPE string; 116 | // DEFINE FIELD age ON user TYPE int; 117 | 118 | // DEFINE TABLE user_view AS 119 | // SELECT 120 | // id as foo_id, 121 | // name, 122 | // age 123 | // FROM user; 124 | // "#; 125 | 126 | // let tables = get_tables(&parse_sql(schema)?)?; 127 | 128 | // assert_eq_sorted!( 129 | // tables, 130 | // [ 131 | // ( 132 | // "user".into(), 133 | // TableParsed { 134 | // name: "user".into(), 135 | // fields: [ 136 | // ("id".into(), Kind::Record(vec!["user".into()])), 137 | // ("name".into(), Kind::String), 138 | // ("age".into(), Kind::Int), 139 | // ] 140 | // .into(), 141 | // } 142 | // ), 143 | // ( 144 | // "user_view".into(), 145 | // TableParsed { 146 | // name: "user_view".into(), 147 | // fields: [ 148 | // ( 149 | // "foo_id".into(), 150 | // Kind::Record(vec!["user".into()]) 151 | // ), 152 | // ( 153 | // "id".into(), 154 | // Kind::Record(vec!["user_view".into()]) 155 | // ), 156 | // ("name".into(), Kind::String), 157 | // ("age".into(), Kind::Int), 158 | // ] 159 | // .into(), 160 | // } 161 | // ) 162 | // ] 163 | // .into() 164 | // ); 165 | 166 | // Ok(()) 167 | // } 168 | // } 169 | -------------------------------------------------------------------------------- /surreal_type_generator/src/step_1_parse_sql/query.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeMap; 2 | 3 | use surrealdb::sql::{parse, Cast, Param, Statement, Value}; 4 | 5 | pub struct QueryParsed { 6 | pub statements: Vec, 7 | pub casted_parameters: BTreeMap, 8 | } 9 | 10 | pub fn parse_query(query: &str) -> Result { 11 | // collect and filter out all the variable castings 12 | let mut parameter_types = BTreeMap::new(); 13 | let mut statements = Vec::new(); 14 | 15 | for stmt in parse(query)?.into_iter() { 16 | match stmt { 17 | Statement::Value(Value::Cast(box Cast { 18 | 0: kind, 19 | 1: Value::Param(Param { 0: ident, .. }), 20 | .. 21 | })) => { 22 | parameter_types.insert(ident.to_string(), kind); 23 | } 24 | _ => statements.push(stmt), 25 | } 26 | } 27 | 28 | Ok(QueryParsed { 29 | statements, 30 | casted_parameters: parameter_types, 31 | }) 32 | } 33 | 34 | /// In surreal 2.0, `RETURN` statements now can "early-exit" in the AST 35 | /// This effectively treats `BEGIN/COMMIT` grouped statements as a block 36 | /// but only if they contain a `RETURN` statement 37 | /// 38 | /// So we need to turn and represent the Vec into a Vec> representing 39 | /// every branching case and scenario. 40 | /// 41 | /// ### Example 1: 42 | /// ```surql 43 | /// BEGIN; 44 | /// CREATE ONLY foo; 45 | /// CREATE ONLY bar; 46 | /// RETURN 1; 47 | /// COMMIT; 48 | /// RETURN 2; 49 | /// ``` 50 | /// 51 | /// Would return: 52 | /// ```ts 53 | /// 0: (1) 54 | /// 1: (2) 55 | /// ``` 56 | /// 57 | /// OR 58 | /// 59 | /// /// What this effectively means, is that a transaction containing a RETURN statement 60 | /// is effectively converted into a block-like AST, looking like the following: 61 | /// 62 | /// ```sql 63 | /// { 64 | /// CREATE ONLY foo; 65 | /// CREATE ONLY bar; 66 | /// RETURN 1; 67 | /// } 68 | /// RETURN 2; 69 | /// ``` 70 | /// 71 | /// ### Example 2: 72 | /// 73 | /// And without a `RETURN` 74 | /// 75 | /// ```surql 76 | /// BEGIN; 77 | /// CREATE ONLY foo; 78 | /// CREATE ONLY bar; 79 | /// COMMIT; 80 | /// RETURN 2 81 | /// ``` 82 | /// 83 | /// Would return a type such as: 84 | /// 0: { id: record } 85 | /// 1: { id: record } 86 | /// 2: (2) 87 | /// 88 | /// ### Example 3: 89 | /// ```surql 90 | /// BEGIN; 91 | /// IF condition { 92 | /// RETURN 1; 93 | /// } 94 | /// RETURN 2; 95 | /// COMMIT; 96 | /// ``` 97 | /// Would turn into an AST looking like the following: 98 | /// ```sql 99 | /// { 100 | /// IF condition { 101 | /// RETURN 1; 102 | /// } 103 | /// RETURN 2; 104 | /// } 105 | /// ``` 106 | /// 107 | /// ### Example 4: 108 | /// ```surql 109 | /// BEGIN; 110 | /// 111 | /// IF condition { 112 | /// RETURN 1; 113 | /// } 114 | /// 115 | /// COMMIT; 116 | /// RETURN 2; 117 | /// ``` 118 | /// Would turn into an AST looking like the following: 119 | /// ```sql 120 | /// { 121 | /// IF condition { 122 | /// RETURN 1; 123 | /// } 124 | /// RETURN 2; 125 | /// } 126 | /// ``` 127 | pub fn statements_to_block_ast( 128 | statements: Vec, 129 | ) -> Result, anyhow::Error> { 130 | Ok(statements) 131 | } 132 | -------------------------------------------------------------------------------- /surreal_type_generator/src/step_1_parse_sql/schema.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeMap; 2 | 3 | use surrealdb::sql::{ 4 | parse, 5 | statements::{ 6 | DefineFieldStatement, DefineFunctionStatement, DefineStatement, DefineTableStatement, 7 | IfelseStatement, ThrowStatement, 8 | }, 9 | Block, Entry, Expression, Fields, Function, Groups, Idiom, Kind, Param, Part, Query, Statement, 10 | Tables, Value, 11 | }; 12 | 13 | use crate::kind; 14 | 15 | #[derive(Debug, PartialEq)] 16 | pub struct SchemaParsed { 17 | pub tables: BTreeMap, 18 | pub functions: BTreeMap, 19 | pub views: BTreeMap, 20 | } 21 | 22 | #[derive(Debug, Clone, PartialEq)] 23 | pub struct ViewParsed { 24 | pub name: String, 25 | // pub id_value_type: Kind, 26 | pub expr: Fields, 27 | pub what: Tables, 28 | pub groups: Option, 29 | } 30 | 31 | #[derive(Debug, Clone, PartialEq)] 32 | pub struct FunctionParsed { 33 | pub name: String, 34 | pub arguments: Vec<(String, Kind)>, 35 | pub block: Block, 36 | } 37 | 38 | #[derive(Debug, PartialEq)] 39 | 40 | pub enum FieldType { 41 | Simple, 42 | NestedObject(BTreeMap), 43 | NestedArray(Box), 44 | } 45 | 46 | #[derive(Debug, PartialEq)] 47 | pub struct TableParsed { 48 | pub name: String, 49 | pub id_value_type: Kind, 50 | pub fields: BTreeMap, 51 | } 52 | 53 | #[derive(Debug, PartialEq)] 54 | pub struct FieldParsed { 55 | pub name: String, 56 | pub is_optional: bool, 57 | pub return_type: Kind, 58 | pub has_default: bool, 59 | pub has_override_value: bool, 60 | pub flexible: bool, 61 | pub readonly: bool, 62 | pub field_type: FieldType, 63 | } 64 | 65 | impl FieldParsed { 66 | pub fn compute_create_type(&self) -> anyhow::Result { 67 | Ok(match &self.field_type { 68 | FieldType::Simple => match self.is_optional || self.has_default { 69 | true => Kind::Option(Box::new(self.return_type.clone())), 70 | false => self.return_type.clone(), 71 | }, 72 | FieldType::NestedObject(obj) => { 73 | let mut fields = BTreeMap::new(); 74 | for (key, value) in obj { 75 | if !value.has_override_value { 76 | fields.insert(key.clone(), value.compute_create_type()?); 77 | } 78 | } 79 | 80 | let fields = kind!(Obj fields); 81 | 82 | match self.is_optional || self.has_default { 83 | true => Kind::Option(Box::new(fields)), 84 | false => fields, 85 | } 86 | } 87 | _ => anyhow::bail!("TODO: Unsupported field type: {:?}", self.field_type), 88 | }) 89 | } 90 | 91 | pub fn compute_select_type(&self) -> anyhow::Result { 92 | Ok(match &self.field_type { 93 | FieldType::Simple => match self.is_optional { 94 | true => Kind::Option(Box::new(self.return_type.clone())), 95 | false => self.return_type.clone(), 96 | }, 97 | FieldType::NestedObject(obj) => { 98 | let mut fields = BTreeMap::new(); 99 | for (key, value) in obj { 100 | fields.insert(key.clone(), value.compute_select_type()?); 101 | } 102 | 103 | let fields = kind!(Obj fields); 104 | 105 | match self.is_optional { 106 | true => Kind::Option(Box::new(fields)), 107 | false => fields, 108 | } 109 | } 110 | FieldType::NestedArray(box inner_type) => { 111 | let select_type = match inner_type { 112 | FieldType::Simple => self.return_type.clone(), 113 | FieldType::NestedObject(fields) => { 114 | let mut select_fields = BTreeMap::new(); 115 | for (key, value) in fields { 116 | select_fields.insert(key.clone(), value.compute_select_type()?); 117 | } 118 | kind!(Obj select_fields) 119 | } 120 | FieldType::NestedArray(..) => { 121 | anyhow::bail!("Nested array in nested array are not yet supported") 122 | } 123 | }; 124 | 125 | match self.is_optional { 126 | true => kind!(Opt(kind!([select_type]))), 127 | false => kind!(Arr select_type), 128 | } 129 | } 130 | }) 131 | } 132 | 133 | pub fn compute_update_type(&self) -> anyhow::Result { 134 | anyhow::bail!("TODO: Query interpretation for UPDATE statements is not yet supported") 135 | // return both flatten types 136 | // ignore readonlys 137 | } 138 | } 139 | 140 | impl TableParsed { 141 | pub fn compute_create_fields(&self) -> anyhow::Result> { 142 | let mut fields = BTreeMap::new(); 143 | for (key, field) in &self.fields { 144 | if !field.has_override_value { 145 | fields.insert(key.clone(), field.compute_create_type()?); 146 | } 147 | } 148 | Ok(fields) 149 | } 150 | 151 | pub fn compute_select_fields(&self) -> anyhow::Result> { 152 | let mut fields = BTreeMap::new(); 153 | for (key, value) in &self.fields { 154 | fields.insert(key.clone(), value.compute_select_type()?); 155 | } 156 | Ok(fields) 157 | } 158 | 159 | pub fn compute_update_fields(&self) -> anyhow::Result> { 160 | let mut fields = BTreeMap::new(); 161 | for (key, value) in &self.fields { 162 | fields.insert(key.clone(), value.compute_update_type()?); 163 | } 164 | Ok(fields) 165 | } 166 | } 167 | 168 | fn parse_table( 169 | table: &DefineTableStatement, 170 | field_definitions: &Vec<(Idiom, DefineFieldStatement)>, 171 | ) -> anyhow::Result { 172 | // insert the implicit id field 173 | let mut fields = BTreeMap::from([( 174 | "id".into(), 175 | FieldParsed { 176 | name: "id".into(), 177 | is_optional: false, 178 | field_type: FieldType::Simple, 179 | has_default: true, 180 | has_override_value: false, 181 | readonly: true, 182 | flexible: false, 183 | return_type: Kind::Record(vec![table.name.clone().into()]), 184 | }, 185 | )]); 186 | 187 | for (idiom, field) in field_definitions { 188 | let return_type = match &field.kind { 189 | Some(kind) => kind, 190 | None => &kind!(Any), 191 | }; 192 | 193 | let mut to_insert = FieldParsed { 194 | name: match &idiom[idiom.len() - 1] { 195 | Part::Field(ident) => ident.to_string(), 196 | _ => anyhow::bail!("Invalid path `{}`", idiom), 197 | }, 198 | is_optional: match return_type { 199 | Kind::Option(..) => true, 200 | _ => false, 201 | }, 202 | field_type: match &return_type { 203 | Kind::Any => match field.flex { 204 | false => FieldType::NestedObject(BTreeMap::new()), 205 | true => FieldType::Simple, 206 | }, 207 | Kind::Option(box Kind::Any) => match field.flex { 208 | false => FieldType::NestedObject(BTreeMap::new()), 209 | true => FieldType::Simple, 210 | }, 211 | Kind::Array(box Kind::Any, ..) => match field.flex { 212 | false => { 213 | FieldType::NestedArray(Box::new(FieldType::NestedObject(BTreeMap::new()))) 214 | } 215 | true => FieldType::Simple, 216 | }, 217 | _ => FieldType::Simple, 218 | }, 219 | has_default: field.default.is_some(), 220 | has_override_value: match &field.value { 221 | Some(value) => !value_uses_value_param(value)?, 222 | None => false, 223 | }, 224 | readonly: field.readonly, 225 | flexible: field.flex, 226 | return_type: match return_type { 227 | Kind::Option(inner_type) => *inner_type.clone(), 228 | _ => return_type.clone(), 229 | }, 230 | }; 231 | 232 | // Handle edge case where `DEFINE FIELD id ON foo TYPE string` must have a default value 233 | if idiom.len() == 1 && idiom[0] == Part::Field("id".into()) { 234 | to_insert.has_default = true; 235 | } 236 | 237 | insert_into_object(&idiom, &mut fields, to_insert)?; 238 | } 239 | 240 | // Handle edge case where DEFINE FIELD id ON foo TYPE string is used 241 | // Since, the return type is still a record we need to note that. 242 | let id_value_type = match &mut fields.get_mut("id").unwrap().return_type { 243 | Kind::Record(..) => Kind::String, 244 | val => { 245 | let id_value_type = val.clone(); 246 | *val = Kind::Record(vec![table.name.clone().into()]); 247 | id_value_type 248 | } 249 | }; 250 | 251 | return Ok(TableParsed { 252 | name: table.name.to_string(), 253 | id_value_type, 254 | fields, 255 | }); 256 | } 257 | 258 | fn value_uses_value_param(value: &Value) -> Result { 259 | Ok(match value { 260 | Value::Param(Param { 0: ident, .. }) => ident.0 == "value", 261 | Value::Array(array) => { 262 | for value in array.iter() { 263 | if value_uses_value_param(value)? { 264 | return Ok(true); 265 | } 266 | } 267 | false 268 | } 269 | Value::Object(object) => { 270 | for (_, value) in object.iter() { 271 | if value_uses_value_param(value)? { 272 | return Ok(true); 273 | } 274 | } 275 | false 276 | } 277 | Value::Expression(box Expression::Binary { l, r, .. }) => { 278 | value_uses_value_param(l)? || value_uses_value_param(r)? 279 | } 280 | Value::Expression(box Expression::Unary { v, .. }) => value_uses_value_param(v)?, 281 | Value::Bool(_) 282 | | Value::Number(_) 283 | | Value::None 284 | | Value::Null 285 | | Value::Constant(_) 286 | | Value::Bytes(_) 287 | | Value::Idiom(_) 288 | | Value::Regex(_) 289 | | Value::Strand(_) 290 | | Value::Thing(_) => false, 291 | Value::Block(box block) => block_uses_value_param(block)?, 292 | Value::Query(query) => query_uses_value_param(query)?, 293 | Value::Function(box function) => function_uses_value_param(function)?, 294 | v => anyhow::bail!("Unsupported value type `{}`", v), 295 | }) 296 | } 297 | 298 | fn block_uses_value_param(block: &Block) -> Result { 299 | for value in block.iter() { 300 | if entry_uses_value_param(value)? { 301 | return Ok(true); 302 | } 303 | } 304 | Ok(false) 305 | } 306 | 307 | fn function_uses_value_param(function: &Function) -> Result { 308 | match function { 309 | Function::Normal(_, values) | Function::Custom(_, values) | Function::Script(_, values) => { 310 | for value in values.iter() { 311 | if value_uses_value_param(value)? { 312 | return Ok(true); 313 | } 314 | } 315 | } 316 | _ => anyhow::bail!("Unsupported function type"), 317 | } 318 | Ok(false) 319 | } 320 | 321 | fn entry_uses_value_param(entry: &Entry) -> Result { 322 | Ok(match entry { 323 | Entry::Value(value) => value_uses_value_param(value)?, 324 | Entry::Throw(ThrowStatement { error, .. }) => value_uses_value_param(error)?, 325 | Entry::Continue(_) | Entry::Break(_) => false, 326 | Entry::Ifelse(IfelseStatement { close, exprs, .. }) => { 327 | match close { 328 | Some(close) => { 329 | if value_uses_value_param(close)? { 330 | return Ok(true); 331 | } 332 | } 333 | None => {} 334 | }; 335 | 336 | for (v1, v2) in exprs.iter() { 337 | if value_uses_value_param(v1)? || value_uses_value_param(v2)? { 338 | return Ok(true); 339 | } 340 | } 341 | false 342 | } 343 | Entry::Create(_) => anyhow::bail!("Create statements not supported in VALUE clause yet"), 344 | Entry::Update(_) => anyhow::bail!("Update statements not supported in VALUE clause yet"), 345 | Entry::Delete(_) => anyhow::bail!("Delete statements not supported in VALUE clause yet"), 346 | Entry::Relate(_) => anyhow::bail!("Relate statements not supported in VALUE clause yet"), 347 | Entry::Insert(_) => anyhow::bail!("Insert statements not supported in VALUE clause yet"), 348 | Entry::Output(_) => anyhow::bail!("Output statements not supported in VALUE clause yet"), 349 | Entry::Set(_) => anyhow::bail!("LET $var statements not supported in VALUE clause yet"), 350 | Entry::Select(_) => anyhow::bail!("Select statements not supported in VALUE clause yet"), 351 | Entry::Foreach(_) => anyhow::bail!("Foreach statements not supported in VALUE clause yet"), 352 | Entry::Upsert(_) => anyhow::bail!("Upsert statements not supported in VALUE clause yet"), 353 | Entry::Define(_) => anyhow::bail!("Define statements not supported in VALUE clause yet"), 354 | Entry::Remove(_) => anyhow::bail!("Remove statements not supported in VALUE clause yet"), 355 | Entry::Rebuild(_) => anyhow::bail!("Rebuild statements not supported in VALUE clause yet"), 356 | _ => anyhow::bail!("Unsupported statement type: `{}`", entry), 357 | }) 358 | } 359 | 360 | fn query_uses_value_param(_query: &Query) -> Result { 361 | anyhow::bail!("Query expressions not yet supported in VALUE clauses") 362 | // Ok(match query { 363 | // #[allow(unreachable_patterns)] 364 | // _ => anyhow::bail!("Query expressions not supported in VALUE clause"), 365 | // }) 366 | } 367 | 368 | pub fn parse_schema(schema: &str) -> Result { 369 | let statements = parse(schema)?.0; 370 | 371 | struct TableInfo { 372 | definition: DefineTableStatement, 373 | fields: Vec<(Idiom, DefineFieldStatement)>, 374 | } 375 | 376 | let mut tables = BTreeMap::new(); 377 | let mut views = BTreeMap::new(); 378 | let mut functions = BTreeMap::new(); 379 | 380 | for stmt in statements.into_iter() { 381 | match stmt { 382 | Statement::Define(DefineStatement::Table(table)) => { 383 | let name = table.name.to_string(); 384 | if tables.contains_key(&name) || views.contains_key(&name) { 385 | anyhow::bail!("Duplicate table name: `{}` check if it was defined twice or if you defined a field for it before defining the table", name); 386 | } 387 | match table.view { 388 | Some(view) => { 389 | views.insert( 390 | name.clone(), 391 | ViewParsed { 392 | name: name.clone(), 393 | expr: view.expr.clone(), 394 | what: view.what.clone(), 395 | groups: view.group.clone(), 396 | }, 397 | ); 398 | } 399 | None => { 400 | tables.insert( 401 | name.clone(), 402 | TableInfo { 403 | definition: table.clone(), 404 | fields: Vec::new(), 405 | }, 406 | ); 407 | } 408 | } 409 | } 410 | Statement::Define(DefineStatement::Field(field)) => { 411 | let table = match tables.get_mut(&field.what.to_string()) { 412 | Some(table) => table, 413 | None => { 414 | anyhow::bail!( 415 | "You tried to define a field on a table that hasn't been defined: `{}`", 416 | field.to_string() 417 | ); 418 | } 419 | }; 420 | 421 | table.fields.push((field.name.clone(), field)); 422 | } 423 | Statement::Define(DefineStatement::Function(DefineFunctionStatement { 424 | name, 425 | args, 426 | block, 427 | .. 428 | })) => { 429 | functions.insert( 430 | name.to_string(), 431 | FunctionParsed { 432 | name: name.to_string(), 433 | arguments: args 434 | .iter() 435 | .map(|(ident, kind)| Ok((ident.to_string(), kind.clone()))) 436 | .collect::, anyhow::Error>>()?, 437 | block: block.clone(), 438 | }, 439 | ); 440 | } 441 | // ignore other statements 442 | _ => {} 443 | } 444 | } 445 | 446 | let tables = { 447 | let mut new_tables = BTreeMap::new(); 448 | for (name, table) in tables.iter() { 449 | new_tables.insert(name.clone(), parse_table(&table.definition, &table.fields)?); 450 | } 451 | new_tables 452 | }; 453 | 454 | return Ok(SchemaParsed { 455 | tables, 456 | functions, 457 | views, 458 | }); 459 | } 460 | 461 | fn insert_into_object( 462 | idiom: &[Part], 463 | fields: &mut BTreeMap, 464 | field: FieldParsed, 465 | ) -> anyhow::Result<()> { 466 | // if the idiom is empty, we're at the end of the path 467 | if idiom.len() == 1 { 468 | match &idiom[0] { 469 | Part::Field(ident) => fields.insert(ident.to_string(), field), 470 | _ => anyhow::bail!("Invalid path `{}`", Idiom::from(idiom)), 471 | }; 472 | 473 | return Ok(()); 474 | } 475 | let next_part = idiom.first().unwrap(); 476 | match next_part { 477 | Part::Field(field_ident) => match fields.get_mut(field_ident.as_str()) { 478 | Some(FieldParsed { 479 | field_type: FieldType::NestedObject(fields), 480 | .. 481 | }) => insert_into_object(idiom[1..].as_ref(), fields, field), 482 | Some(FieldParsed { 483 | field_type: FieldType::NestedArray(array_type), 484 | .. 485 | }) => insert_into_array_type(idiom[1..].as_ref(), array_type, field), 486 | _ => anyhow::bail!("Field `{}` is not a nested object or array", field_ident), 487 | }, 488 | // Part::All(index) => match fields.get_mut(index) { 489 | // Some(FieldInfo { 490 | // field_type: FieldType::NestedArray(array_type), 491 | // .. 492 | // }) => insert_into_array_type(idiom[1..].as_ref(), array_type, field), 493 | // _ => anyhow::bail!("Index `{}` is not a nested array", index), 494 | // }, 495 | _ => anyhow::bail!("Invalid path `{}`", Idiom::from(idiom)), 496 | } 497 | } 498 | 499 | fn insert_into_array_type( 500 | idiom: &[Part], 501 | array_type: &mut FieldType, 502 | field: FieldParsed, 503 | ) -> anyhow::Result<()> { 504 | // if the idiom is empty, we're at the end of the path 505 | 506 | match idiom.first().unwrap() { 507 | // eg: foo.*.bar 508 | Part::All => match array_type { 509 | FieldType::NestedObject(fields) => { 510 | insert_into_object(idiom[1..].as_ref(), fields, field) 511 | } 512 | _ => anyhow::bail!("Unimplemented path `{}`", Idiom::from(idiom)), 513 | }, 514 | _ => anyhow::bail!("Unimplemented path `{}`", Idiom::from(idiom)), 515 | } 516 | } 517 | -------------------------------------------------------------------------------- /surreal_type_generator/src/step_2_interpret/function.rs: -------------------------------------------------------------------------------- 1 | use surrealdb::sql::{Function, Value}; 2 | 3 | use crate::Kind; 4 | 5 | use super::QueryState; 6 | 7 | pub fn get_function_return_type( 8 | state: &mut QueryState, 9 | func: &Function, 10 | ) -> Result { 11 | match func { 12 | Function::Custom(name, values) => get_custom_function_return_type(state, name, values), 13 | Function::Normal(name, ..) => normal_function_return_type(name), 14 | Function::Script(..) => anyhow::bail!("Script functions are not yet supported"), 15 | _ => anyhow::bail!("Unsupported function: {}", func), 16 | } 17 | } 18 | 19 | pub fn get_custom_function_return_type( 20 | state: &mut QueryState, 21 | name: &str, 22 | _values: &[Value], 23 | ) -> Result { 24 | let function = state.function(name)?; 25 | 26 | Ok(function.return_type) 27 | } 28 | 29 | pub fn normal_function_return_type(name: &str) -> Result { 30 | Ok(match name { 31 | "count" => Kind::Number, 32 | 33 | // `math::` functions 34 | "math::abs" => Kind::Number, 35 | "math::acos" => Kind::Number, 36 | "math::asin" => Kind::Number, 37 | "math::atan" => Kind::Number, 38 | "math::bottom" => Kind::Array(Box::new(Kind::Number), None), 39 | "math::ceil" => Kind::Number, 40 | "math::clamp" => Kind::Number, 41 | "math::cos" => Kind::Number, 42 | "math::cot" => Kind::Number, 43 | "math::deg2rad" => Kind::Number, 44 | "math::e" => Kind::Number, 45 | "math::fixed" => Kind::Number, 46 | "math::floor" => Kind::Number, 47 | "math::inf" => Kind::Number, 48 | "math::interquartile" => Kind::Number, 49 | "math::lerp" => Kind::Number, 50 | "math::lerpangle" => Kind::Number, 51 | "math::ln" => Kind::Number, 52 | "math::log" => Kind::Number, 53 | "math::log10" => Kind::Number, 54 | "math::log2" => Kind::Number, 55 | "math::max" => Kind::Number, 56 | "math::mean" => Kind::Number, 57 | "math::median" => Kind::Number, 58 | "math::midhinge" => Kind::Number, 59 | "math::min" => Kind::Number, 60 | "math::mode" => Kind::Number, 61 | "math::nearestrank" => Kind::Number, 62 | "math::neg_inf" => Kind::Number, 63 | "math::percentile" => Kind::Number, 64 | "math::pi" => Kind::Number, 65 | "math::product" => Kind::Number, 66 | "math::rad2deg" => Kind::Number, 67 | "math::round" => Kind::Number, 68 | "math::sign" => Kind::Number, 69 | "math::sin" => Kind::Number, 70 | "math::tan" => Kind::Number, 71 | "math::tau" => Kind::Number, 72 | "math::spread" => Kind::Number, 73 | "math::sqrt" => Kind::Number, 74 | "math::stddev" => Kind::Number, 75 | "math::sum" => Kind::Number, 76 | "math::top" => Kind::Array(Box::new(Kind::Number), None), 77 | "math::trimean" => Kind::Number, 78 | "math::variance" => Kind::Number, 79 | 80 | // `time::` functions 81 | "time::day" => Kind::Number, 82 | "time::floor" => Kind::Number, 83 | "time::format" => Kind::String, 84 | "time::group" => Kind::Datetime, 85 | "time::hour" => Kind::Number, 86 | "time::max" => Kind::Datetime, 87 | "time::micros" => Kind::Number, 88 | "time::millis" => Kind::Number, 89 | "time::min" => Kind::Datetime, 90 | "time::minute" => Kind::Number, 91 | "time::month" => Kind::Number, 92 | "time::nano" => Kind::Number, 93 | "time::now" => Kind::Datetime, 94 | "time::round" => Kind::Datetime, 95 | "time::second" => Kind::Number, 96 | "time::timezone" => Kind::String, 97 | "time::unix" => Kind::Number, 98 | "time::wday" => Kind::Number, 99 | "time::week" => Kind::Number, 100 | "time::yday" => Kind::Number, 101 | "time::year" => Kind::Number, 102 | "time::from::micros" => Kind::Datetime, 103 | "time::from::millis" => Kind::Datetime, 104 | "time::from::nanos" => Kind::Datetime, 105 | "time::from::secs" => Kind::Datetime, 106 | "time::from::unix" => Kind::Datetime, 107 | 108 | // `duration::` functions 109 | "duration::days" => Kind::Number, 110 | "duration::hours" => Kind::Number, 111 | "duration::micros" => Kind::Number, 112 | "duration::millis" => Kind::Number, 113 | "duration::mins" => Kind::Number, 114 | "duration::nanos" => Kind::Number, 115 | "duration::secs" => Kind::Number, 116 | "duration::weeks" => Kind::Number, 117 | "duration::years" => Kind::Number, 118 | "duration::from::days" => Kind::Duration, 119 | "duration::from::hours" => Kind::Duration, 120 | "duration::from::micros" => Kind::Duration, 121 | "duration::from::millis" => Kind::Duration, 122 | "duration::from::mins" => Kind::Duration, 123 | "duration::from::nanos" => Kind::Duration, 124 | "duration::from::secs" => Kind::Duration, 125 | "duration::from::weeks" => Kind::Duration, 126 | 127 | // `crypto::` functions 128 | "crypto::md5" => Kind::String, 129 | "crypto::sha1" => Kind::String, 130 | "crypto::sha256" => Kind::String, 131 | "crypto::sha512" => Kind::String, 132 | "crypto::argon2::compare" => Kind::Bool, 133 | "crypto::argon2::generate" => Kind::String, 134 | "crypto::bcrypt::compare" => Kind::Bool, 135 | "crypto::bcrypt::generate" => Kind::String, 136 | "crypto::pbkdf2::compare" => Kind::Bool, 137 | "crypto::pbkdf2::generate" => Kind::String, 138 | "crypto::scrypt::compare" => Kind::Bool, 139 | "crypto::scrypt::generate" => Kind::String, 140 | 141 | // `meta::` functions 142 | "meta::id" => Kind::Any, // TODO: should this be a string? 143 | "meta::type" => Kind::String, 144 | 145 | // TODO: add more functions 146 | "array::len" => Kind::Number, 147 | // - `array::` 148 | // - `encoding::` 149 | // - `geo::` 150 | // - `http::` 151 | // - `object::` 152 | // - `parse::` 153 | // - `rand::` 154 | // - `search::` 155 | // - `session::` 156 | // - `sleep::` 157 | // - `string::` 158 | // - `type::` 159 | // - `vector::` 160 | // - `` 161 | _ => anyhow::bail!("Unsupported normal function: {}", name), 162 | }) 163 | } 164 | -------------------------------------------------------------------------------- /surreal_type_generator/src/step_2_interpret/mod.rs: -------------------------------------------------------------------------------- 1 | mod function; 2 | mod object; 3 | mod return_types; 4 | mod schema; 5 | mod statements; 6 | mod utils; 7 | 8 | use crate::Kind; 9 | pub use return_types::get_statement_fields; 10 | use return_types::get_value_return_type; 11 | use statements::*; 12 | use std::collections::BTreeMap; 13 | use surrealdb::sql::{Statement, Subquery}; 14 | 15 | pub use schema::interpret_schema; 16 | pub use schema::QueryState; 17 | pub use schema::SchemaState; 18 | 19 | pub fn interpret_query( 20 | statements: &Vec, 21 | state: &mut QueryState, 22 | ) -> Result, anyhow::Error> { 23 | let mut results = Vec::new(); 24 | let mut remaining_statements = statements.clone(); 25 | // More efficient to pop from the end 26 | remaining_statements.reverse(); 27 | 28 | while let Some(stmt) = remaining_statements.pop() { 29 | match stmt { 30 | Statement::Begin(_) => { 31 | while let Some(stmt) = remaining_statements.pop() { 32 | let mut maybe_returns = Vec::new(); 33 | 34 | match stmt { 35 | Statement::Commit(_) => { 36 | // We've hit a `COMMIT` statement, so we want to return the results 37 | // as we didn't run into any `RETURN` statements 38 | results.extend(maybe_returns); 39 | break; 40 | } 41 | Statement::Begin(_) => { 42 | anyhow::bail!("Unexpected `BEGIN` statement in transaction block") 43 | } 44 | // We ran into a `RETURN` statement, so we want to just return the result 45 | stmt @ Statement::Output(_) => { 46 | // We ignore whatever came before since we are returning in this block 47 | match get_statement_return_type(&stmt, state)? { 48 | Some(kind) => results.push(kind), 49 | None => {} 50 | }; 51 | 52 | // We want to ignore any statements after the `RETURN` statement 53 | // until we hit a `COMMIT` statement 54 | while let Some(stmt) = remaining_statements.pop() { 55 | if matches!(stmt, Statement::Commit(_)) { 56 | break; 57 | } 58 | } 59 | // Break out of the BEGIN block 60 | break; 61 | } 62 | // We may return these values except if we hit a `RETURN` statement 63 | _ => match get_statement_return_type(&stmt, state)? { 64 | Some(kind) => maybe_returns.push(kind), 65 | None => {} 66 | }, 67 | } 68 | } 69 | continue; 70 | } 71 | Statement::Commit(_) => { 72 | anyhow::bail!("Unexpected `COMMIT` statement in transaction block") 73 | } 74 | stmt => match get_statement_return_type(&stmt, state)? { 75 | Some(kind) => results.push(kind), 76 | None => {} 77 | }, 78 | } 79 | } 80 | 81 | Ok(results) 82 | } 83 | 84 | fn get_statement_return_type( 85 | stmt: &Statement, 86 | state: &mut QueryState, 87 | ) -> Result, anyhow::Error> { 88 | Ok(Some(match stmt { 89 | Statement::Select(select) => get_select_statement_return_type(select, state)?, 90 | Statement::Delete(delete) => get_delete_statement_return_type(delete, state)?, 91 | Statement::Create(create) => get_create_statement_return_type(create, state)?, 92 | Statement::Insert(insert) => get_insert_statement_return_type(insert, state)?, 93 | Statement::Update(update) => get_update_statement_return_type(update, state)?, 94 | Statement::Output(output) => get_return_statement_return_type(output, state)?, 95 | Statement::Upsert(upsert) => get_upsert_statement_return_type(upsert, state)?, 96 | Statement::Value(value) => get_value_return_type(value, &BTreeMap::new(), state)?, 97 | Statement::Set(set) => interpret_let_statement(set, state)?, 98 | _ => anyhow::bail!("Unsupported statement type: `{}`", stmt), 99 | })) 100 | } 101 | 102 | fn get_subquery_return_type( 103 | subquery: &Subquery, 104 | state: &mut QueryState, 105 | ) -> Result { 106 | match subquery { 107 | Subquery::Select(select) => get_select_statement_return_type(select, state), 108 | Subquery::Delete(delete) => get_delete_statement_return_type(delete, state), 109 | Subquery::Create(create) => get_create_statement_return_type(create, state), 110 | Subquery::Insert(insert) => get_insert_statement_return_type(insert, state), 111 | Subquery::Update(update) => get_update_statement_return_type(update, state), 112 | Subquery::Upsert(upsert) => get_upsert_statement_return_type(upsert, state), 113 | Subquery::Value(value) => get_value_return_type(value, &BTreeMap::new(), state), 114 | _ => anyhow::bail!("Unsupported subquery type: `{}`", subquery), 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /surreal_type_generator/src/step_2_interpret/object.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeMap; 2 | 3 | use surrealdb::sql::Object; 4 | 5 | use crate::{kind, Kind}; 6 | 7 | use super::{return_types::get_value_return_type, QueryState}; 8 | 9 | pub fn get_object_return_type(state: &mut QueryState, obj: &Object) -> Result { 10 | let mut fields = BTreeMap::new(); 11 | 12 | for (key, value) in obj.0.iter() { 13 | let return_type = get_value_return_type(value, &BTreeMap::new(), state)?; 14 | 15 | fields.insert(key.clone(), return_type); 16 | } 17 | 18 | return Ok(kind!(Obj fields)); 19 | } 20 | -------------------------------------------------------------------------------- /surreal_type_generator/src/step_2_interpret/return_types.rs: -------------------------------------------------------------------------------- 1 | use std::collections::{BTreeMap, HashSet}; 2 | 3 | use surrealdb::sql::{ 4 | Cast, Constant, Expression, Field, Fields, Ident, Idiom, Literal, Operator, Param, Part, Value, 5 | }; 6 | 7 | use crate::{kind, Kind}; 8 | 9 | use super::{ 10 | function::get_function_return_type, 11 | get_subquery_return_type, 12 | object::get_object_return_type, 13 | schema::QueryState, 14 | utils::{get_what_fields, merge_into_map_recursively}, 15 | }; 16 | 17 | pub fn get_statement_fields( 18 | what: &[Value], 19 | state: &mut QueryState, 20 | fields: Option<&Fields>, 21 | get_field_and_variables: F, 22 | ) -> Result 23 | where 24 | F: Fn(&mut BTreeMap, &mut QueryState) -> (), 25 | { 26 | let mut return_types = Vec::new(); 27 | let mut used_tables = HashSet::new(); 28 | 29 | for table in what.iter() { 30 | let mut table_fields = get_what_fields(table, state)?; 31 | 32 | if used_tables.contains(&table) { 33 | continue; 34 | } 35 | 36 | used_tables.insert(table); 37 | 38 | state.push_stack_frame(); 39 | 40 | let return_type = if let Some(fields) = fields { 41 | get_field_and_variables(&mut table_fields, state); 42 | get_fields_return_values(fields, &table_fields, state)? 43 | } else { 44 | kind!(Obj table_fields.clone()) 45 | }; 46 | 47 | state.pop_stack_frame(); 48 | 49 | return_types.push(return_type); 50 | } 51 | 52 | if return_types.len() == 1 { 53 | Ok(return_types.pop().unwrap()) 54 | } else { 55 | Ok(Kind::Either(return_types)) 56 | } 57 | } 58 | 59 | pub fn get_fields_return_values( 60 | fields: &Fields, 61 | field_types: &BTreeMap, 62 | state: &mut QueryState, 63 | ) -> Result { 64 | match fields { 65 | // returning a single value with `VALUE` 66 | Fields { 67 | 0: fields, 1: true, .. 68 | } => Ok(get_field_return_type(&fields[0], &field_types, state)? 69 | .pop() 70 | .unwrap() 71 | .1), 72 | // returning multiple values (object map) 73 | Fields { 74 | 0: fields, 75 | 1: false, 76 | .. 77 | } => { 78 | let mut map = BTreeMap::new(); 79 | 80 | for field in fields { 81 | for (idiom, return_type) in get_field_return_type(field, &field_types, state)? { 82 | merge_into_map_recursively(&mut map, &idiom.0, return_type)?; 83 | } 84 | } 85 | 86 | return Ok(kind!(Obj map)); 87 | } 88 | } 89 | } 90 | 91 | pub fn get_field_return_type( 92 | field: &Field, 93 | field_types: &BTreeMap, 94 | state: &mut QueryState, 95 | ) -> Result, anyhow::Error> { 96 | match field { 97 | Field::Single { 98 | expr, 99 | alias: Some(alias), 100 | } => Ok(vec![( 101 | alias.clone(), 102 | get_value_return_type(expr, field_types, state)?, 103 | )]), 104 | Field::Single { expr, alias: None } => Ok(vec![( 105 | expr.to_idiom(), 106 | get_value_return_type(expr, field_types, state)?, 107 | )]), 108 | Field::All => { 109 | let mut results = vec![]; 110 | for (field_name, field_type) in field_types { 111 | results.push(( 112 | vec![Part::Field(Ident::from(field_name.clone()))].into(), 113 | field_type.clone(), 114 | )); 115 | } 116 | Ok(results) 117 | } 118 | #[allow(unreachable_patterns)] 119 | _ => anyhow::bail!("Unsupported field: {}", field), 120 | } 121 | } 122 | 123 | pub fn get_value_return_type( 124 | expr: &Value, 125 | field_types: &BTreeMap, 126 | state: &mut QueryState, 127 | ) -> Result { 128 | Ok(match expr { 129 | Value::Idiom(idiom) => get_field_from_paths(&idiom.0, &field_types, state)?, 130 | Value::Subquery(subquery) => { 131 | state.push_stack_frame(); 132 | 133 | state.set_local("parent", kind!(Obj field_types.clone())); 134 | 135 | let return_type = get_subquery_return_type(subquery, state)?; 136 | 137 | state.pop_stack_frame(); 138 | 139 | return_type 140 | } 141 | Value::Param(param) => get_parameter_return_type(param, state)?, 142 | // TODO: These constants could potentially be represented as actual constants in the return types 143 | Value::Strand(_) => Kind::String, 144 | Value::Number(_) => Kind::Number, 145 | Value::Bool(_) => Kind::Bool, 146 | Value::Null => Kind::Null, 147 | Value::Datetime(_) => Kind::Datetime, 148 | Value::Duration(_) => Kind::Duration, 149 | Value::None => Kind::Null, 150 | Value::Function(func) => get_function_return_type(state, &func)?, 151 | Value::Expression(expr) => get_expression_return_type(expr, field_types, state)?, 152 | Value::Array(array) => { 153 | let mut return_types = HashSet::new(); 154 | for value in &array.0 { 155 | return_types.insert(get_value_return_type(value, field_types, state)?); 156 | } 157 | // If there is more than one type, we muse use Either 158 | kind!(Arr match return_types.len() { 159 | 0 => Kind::Null, 160 | 1 => return_types.into_iter().next().unwrap(), 161 | _ => Kind::Either(return_types.into_iter().collect()), 162 | }) 163 | } 164 | Value::Object(obj) => get_object_return_type(state, obj)?, 165 | Value::Constant(constant) => match constant { 166 | Constant::MathE 167 | | Constant::MathFrac1Pi 168 | | Constant::MathFrac1Sqrt2 169 | | Constant::MathFrac2Pi 170 | | Constant::MathFrac2SqrtPi 171 | | Constant::MathFracPi2 172 | | Constant::MathFracPi3 173 | | Constant::MathFracPi4 174 | | Constant::MathFracPi6 175 | | Constant::MathFracPi8 176 | | Constant::MathInf 177 | | Constant::MathLn10 178 | | Constant::MathLn2 179 | | Constant::MathLog102 180 | | Constant::MathLog10E 181 | | Constant::MathLog210 182 | | Constant::MathLog2E 183 | | Constant::MathPi 184 | | Constant::MathSqrt2 185 | | Constant::MathTau 186 | | Constant::TimeEpoch => Kind::Number, 187 | _ => anyhow::bail!("Unsupported constant: {:?}", constant), 188 | }, 189 | Value::Cast(box Cast { 0: kind, .. }) => kind.clone(), 190 | _ => anyhow::bail!("Unsupported value/expression: {}", expr), 191 | }) 192 | } 193 | 194 | pub fn get_expression_return_type( 195 | expr: &Expression, 196 | field_types: &BTreeMap, 197 | state: &mut QueryState, 198 | ) -> Result { 199 | Ok(match expr { 200 | // Unary 201 | Expression::Unary { 202 | o: Operator::Not, .. 203 | } => Kind::Bool, 204 | Expression::Unary { 205 | o: Operator::Neg, .. 206 | } => anyhow::bail!("Unsupported unary operator"), 207 | 208 | // logical binary expressions 209 | Expression::Binary { 210 | o: Operator::And, .. 211 | } => Kind::Bool, 212 | Expression::Binary { 213 | l, 214 | o: Operator::Or, 215 | r, 216 | } => { 217 | let l = get_value_return_type(l, field_types, state)?; 218 | let r = get_value_return_type(r, field_types, state)?; 219 | 220 | // If left is an `option` and right are both a `thing` we should return a `thing` 221 | // This is because, if left is an option, it is considered falsey, and right will be returned 222 | match (l, r) { 223 | (Kind::Option(box left), right) => { 224 | if left == right { 225 | right 226 | } else { 227 | Kind::Either(vec![left, right]) 228 | } 229 | } 230 | (left, right) => { 231 | if left == right { 232 | right 233 | } else { 234 | Kind::Either(vec![left, right]) 235 | } 236 | } 237 | } 238 | } 239 | Expression::Binary { 240 | o: Operator::Equal, .. 241 | } => Kind::Bool, 242 | Expression::Binary { 243 | o: Operator::NotEqual, 244 | .. 245 | } => Kind::Bool, 246 | Expression::Binary { 247 | o: Operator::Exact, .. 248 | } => Kind::Bool, 249 | 250 | // comparison binary expressions 251 | Expression::Binary { 252 | o: Operator::LessThan, 253 | .. 254 | } => Kind::Bool, 255 | Expression::Binary { 256 | o: Operator::MoreThan, 257 | .. 258 | } => Kind::Bool, 259 | Expression::Binary { 260 | o: Operator::LessThanOrEqual, 261 | .. 262 | } => Kind::Bool, 263 | Expression::Binary { 264 | o: Operator::MoreThanOrEqual, 265 | .. 266 | } => Kind::Bool, 267 | Expression::Binary { 268 | o: Operator::Like, .. 269 | } => Kind::Bool, 270 | Expression::Binary { 271 | o: Operator::NotLike, 272 | .. 273 | } => Kind::Bool, 274 | 275 | // TODO: arithmetic 276 | Expression::Binary { 277 | l, 278 | o: Operator::Add, 279 | r, 280 | } => { 281 | let l = get_value_return_type(l, field_types, state)?; 282 | let r = get_value_return_type(r, field_types, state)?; 283 | 284 | match (&l, &r) { 285 | (Kind::Number, Kind::Number) => Kind::Number, 286 | (Kind::String, Kind::String) => Kind::String, 287 | (Kind::Datetime, Kind::Datetime) => Kind::Datetime, 288 | (Kind::Duration, Kind::Duration) => Kind::Duration, 289 | _ => anyhow::bail!("Unsupported binary operation: {:?}", expr), 290 | } 291 | } 292 | // Expression 293 | // TODO: short circuiting 294 | // TODO: more (contains, any, etc, outside, inside, fuzzy match) 295 | _ => anyhow::bail!("Unsupported expression: {}", expr), 296 | }) 297 | } 298 | 299 | pub fn get_parameter_return_type( 300 | param: &Param, 301 | state: &mut QueryState, 302 | ) -> Result { 303 | match state.get(¶m.0 .0) { 304 | Some(return_type) => Ok(return_type.clone()), 305 | None => anyhow::bail!("Unknown parameter: {}", param), 306 | } 307 | } 308 | 309 | pub fn get_field_from_paths( 310 | parts: &[Part], 311 | field_types: &BTreeMap, 312 | state: &mut QueryState, 313 | ) -> Result { 314 | match parts.first() { 315 | Some(Part::Field(field_name)) => match field_types.get(field_name.as_str()) { 316 | Some(return_type) => match_return_type(return_type, &parts, field_types, state), 317 | None => anyhow::bail!("Field not found: {}", field_name), 318 | }, 319 | Some(Part::Start(Value::Param(Param { 320 | 0: Ident { 0: param_name, .. }, 321 | .. 322 | }))) => match state.get(param_name) { 323 | Some(return_type) => { 324 | match_return_type(&return_type.clone(), &parts, field_types, state) 325 | } 326 | None => anyhow::bail!("Unknown parameter: {}", param_name), 327 | }, 328 | Some(Part::Start(Value::Subquery(subquery))) => { 329 | let return_type = get_subquery_return_type(subquery, state)?; 330 | match_return_type(&return_type, &parts[1..], field_types, state) 331 | } 332 | Some(Part::All) => Ok(kind!(Obj field_types.clone())), 333 | Some(_) => anyhow::bail!("Unsupported path: {}", Idiom::from(parts)), 334 | // Some(_) => anyhow::bail!("Unsupported path: {:#?}", parts), 335 | // We're returning an actual object 336 | None => Ok(kind!(Obj field_types.clone())), 337 | } 338 | } 339 | 340 | fn match_return_type( 341 | return_type: &Kind, 342 | parts: &[Part], 343 | field_types: &BTreeMap, 344 | state: &mut QueryState, 345 | ) -> Result { 346 | let has_next_part = parts.len() > 1; 347 | 348 | Ok(match return_type { 349 | Kind::String => Kind::String, 350 | Kind::Int => Kind::Int, 351 | Kind::Float => Kind::Float, 352 | Kind::Datetime => Kind::Datetime, 353 | Kind::Duration => Kind::Duration, 354 | Kind::Decimal => Kind::Decimal, 355 | Kind::Bool => Kind::Bool, 356 | Kind::Null => Kind::Null, 357 | Kind::Uuid => Kind::Uuid, 358 | Kind::Any => Kind::Any, 359 | Kind::Number => Kind::Number, 360 | Kind::Object => Kind::Object, 361 | Kind::Literal(lit @ (Literal::String(_) | Literal::Number(_))) => { 362 | Kind::Literal(lit.clone()) 363 | } 364 | Kind::Literal(Literal::Object(nested_fields)) => { 365 | if has_next_part { 366 | get_field_from_paths(&parts[1..], nested_fields, state)? 367 | } else { 368 | kind!(Obj nested_fields.clone()) 369 | } 370 | } 371 | Kind::Record(tables) => { 372 | if has_next_part { 373 | let mut return_types = Vec::new(); 374 | for table in tables.iter() { 375 | let return_type = get_field_from_paths( 376 | &parts[1..], 377 | &state.table_select_fields(table.as_str())?, 378 | state, 379 | )?; 380 | return_types.push(return_type); 381 | } 382 | if return_types.len() == 1 { 383 | return_types.pop().unwrap() 384 | } else { 385 | Kind::Either(return_types) 386 | } 387 | } else { 388 | Kind::Record(tables.clone()) 389 | } 390 | } 391 | Kind::Option(return_type) => kind!(Opt(match_return_type( 392 | return_type, 393 | &parts, 394 | field_types, 395 | state, 396 | )?)), 397 | Kind::Array(return_type, ..) => match parts.first() { 398 | Some(Part::Index(_)) => Kind::Option(Box::new(match_return_type( 399 | return_type, 400 | &parts, 401 | field_types, 402 | state, 403 | )?)), 404 | Some(Part::All) => kind!(Arr match_return_type( 405 | return_type, 406 | &parts, 407 | field_types, 408 | state, 409 | )?), 410 | Some(Part::Field(_)) => kind!(Arr match_return_type( 411 | return_type, 412 | &parts[1..], 413 | field_types, 414 | state, 415 | )?), 416 | Some(_) => return Err(anyhow::anyhow!("Unsupported path: {}", Idiom::from(parts))), 417 | None => { 418 | return Err(anyhow::anyhow!( 419 | "Tried to access array with no fields: {}", 420 | Idiom::from(parts) 421 | )) 422 | } 423 | }, 424 | Kind::Either(return_types) => { 425 | let mut return_types = return_types.clone(); 426 | for return_type in &mut return_types { 427 | *return_type = match_return_type(&return_type, &parts, field_types, state)?; 428 | } 429 | Kind::Either(return_types) 430 | } 431 | Kind::Literal(Literal::Array(array)) => { 432 | match array.len() { 433 | 0 => anyhow::bail!("Unsupported literal array: {:?}", array), 434 | 1 => {} 435 | _ => anyhow::bail!("Unsupported literal array: {:?}", array), 436 | } 437 | match array.first() { 438 | Some(Kind::Either(return_types)) => { 439 | let mut return_types = return_types.clone(); 440 | for return_type in &mut return_types { 441 | *return_type = match_return_type(return_type, &parts, field_types, state)?; 442 | } 443 | Kind::Literal(Literal::Array(return_types)) 444 | } 445 | _ => anyhow::bail!("Unsupported literal array: {:?}", array), 446 | } 447 | } 448 | _ => { 449 | return Err(anyhow::anyhow!( 450 | "Unsupported return type: {:?}", 451 | return_type 452 | )) 453 | } 454 | }) 455 | } 456 | -------------------------------------------------------------------------------- /surreal_type_generator/src/step_2_interpret/schema/mod.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::BTreeMap, sync::Arc}; 2 | 3 | use surrealdb::sql::{Block, Entry, Literal, Values}; 4 | 5 | use crate::{ 6 | step_1_parse_sql::{parse_schema, FunctionParsed, SchemaParsed, ViewParsed}, 7 | Kind, 8 | }; 9 | 10 | use super::{ 11 | get_create_statement_return_type, get_delete_statement_return_type, 12 | get_insert_statement_return_type, get_return_statement_return_type, 13 | get_select_statement_return_type, get_statement_fields, get_update_statement_return_type, 14 | }; 15 | 16 | #[derive(Debug)] 17 | pub struct SchemaState { 18 | global_variables: BTreeMap, 19 | pub schema: SchemaParsed, 20 | } 21 | 22 | #[derive(Debug)] 23 | pub struct QueryState { 24 | pub schema: Arc, 25 | pub in_transaction: bool, 26 | defined_variables: BTreeMap, 27 | inferred_variables: BTreeMap, 28 | stack_variables: Vec>, 29 | } 30 | 31 | impl QueryState { 32 | pub fn new(schema: Arc, defined_variables: BTreeMap) -> Self { 33 | Self { 34 | schema, 35 | in_transaction: false, 36 | defined_variables, 37 | inferred_variables: BTreeMap::new(), 38 | // initial global query stack frame for any LET statements 39 | stack_variables: vec![BTreeMap::new()], 40 | } 41 | } 42 | 43 | pub fn infer(&mut self, key: &str, value: Kind) { 44 | self.inferred_variables.insert(key.to_string(), value); 45 | } 46 | 47 | pub fn get(&self, key: &str) -> Option { 48 | let mut stack_variables = self.stack_variables.iter().rev(); 49 | while let Some(frame) = stack_variables.next() { 50 | if let Some(value) = frame.get(key) { 51 | return Some(value.clone()); 52 | } 53 | } 54 | 55 | if let Some(value) = self.defined_variables.get(key) { 56 | return Some(value.clone()); 57 | } 58 | 59 | if let Some(value) = self.inferred_variables.get(key) { 60 | return Some(value.clone()); 61 | } 62 | 63 | if let Some(value) = self.schema.global_variables.get(key) { 64 | return Some(value.clone()); 65 | } 66 | 67 | None 68 | } 69 | 70 | pub fn push_stack_frame(&mut self) { 71 | self.stack_variables.push(BTreeMap::new()); 72 | } 73 | 74 | pub fn pop_stack_frame(&mut self) { 75 | self.stack_variables.pop(); 76 | } 77 | 78 | pub fn set_local(&mut self, key: &str, value: Kind) { 79 | self.stack_variables 80 | .last_mut() 81 | .unwrap() 82 | .insert(key.to_string(), value); 83 | } 84 | 85 | pub fn table_select_fields(&mut self, name: &str) -> Result { 86 | match self.schema.schema.tables.get(name) { 87 | Some(table) => Ok(table.compute_select_fields()?), 88 | None => match self.schema.schema.views.get(name).cloned() { 89 | Some(view) => Ok(get_view_table(&view, self)?), 90 | None => anyhow::bail!("Unknown table: {}", name), 91 | }, 92 | } 93 | } 94 | 95 | pub fn function(&mut self, name: &str) -> Result { 96 | match self.schema.schema.functions.get(name).cloned() { 97 | Some(func) => Ok(interpret_function_parsed(func, self)?), 98 | None => anyhow::bail!("Unknown function: {}", name), 99 | } 100 | } 101 | 102 | pub fn extract_required_variables(&self) -> BTreeMap { 103 | let mut variables = BTreeMap::new(); 104 | 105 | for (name, value) in self.defined_variables.iter() { 106 | variables.insert(name.clone(), value.clone()); 107 | } 108 | 109 | for (name, value) in self.inferred_variables.iter() { 110 | variables.insert(name.clone(), value.clone()); 111 | } 112 | 113 | // should we throw an error here for any variables that were used but not defined or inferred? 114 | 115 | variables 116 | } 117 | } 118 | 119 | #[derive(Debug, Clone)] 120 | pub struct InterpretedFunction { 121 | pub name: String, 122 | pub args: Vec<(String, Kind)>, 123 | pub return_type: Kind, 124 | } 125 | 126 | pub type TableFields = BTreeMap; 127 | 128 | pub fn interpret_schema( 129 | schema: &str, 130 | global_variables: BTreeMap, 131 | ) -> Result { 132 | Ok(SchemaState { 133 | global_variables, 134 | schema: parse_schema(schema)?, 135 | }) 136 | } 137 | 138 | fn interpret_function_parsed( 139 | func: FunctionParsed, 140 | operation_state: &mut QueryState, 141 | ) -> Result { 142 | operation_state.push_stack_frame(); 143 | 144 | for (name, return_type) in func.arguments.iter() { 145 | operation_state.set_local(&name, return_type.clone()); 146 | } 147 | 148 | let func = InterpretedFunction { 149 | name: func.name, 150 | args: func.arguments, 151 | return_type: get_block_return_type(func.block, operation_state)?, 152 | }; 153 | 154 | operation_state.pop_stack_frame(); 155 | 156 | Ok(func) 157 | } 158 | 159 | fn get_block_return_type(block: Block, state: &mut QueryState) -> Result { 160 | for entry in block.0.into_iter() { 161 | match entry { 162 | Entry::Output(output) => return get_return_statement_return_type(&output, state), 163 | Entry::Create(create) => return get_create_statement_return_type(&create, state), 164 | Entry::Insert(insert) => return get_insert_statement_return_type(&insert, state), 165 | Entry::Delete(delete) => return get_delete_statement_return_type(&delete, state), 166 | Entry::Select(select) => return get_select_statement_return_type(&select, state), 167 | Entry::Update(update) => return get_update_statement_return_type(&update, state), 168 | // Entry::Upsert(upsert) => return get_upsert_statement_return_type(&upsert, state), 169 | _ => anyhow::bail!("Entry type: {} has not been implemented", entry), 170 | } 171 | } 172 | 173 | Ok(Kind::Null) 174 | } 175 | 176 | fn get_view_table( 177 | // name: &str, 178 | view: &ViewParsed, 179 | state: &mut QueryState, 180 | ) -> Result { 181 | match get_view_return_type(view, state)? { 182 | Kind::Literal(Literal::Object(mut fields)) => { 183 | if view.what.0.len() != 1 { 184 | anyhow::bail!("Expected single table in view"); 185 | } 186 | 187 | // add the implicit id field 188 | fields.insert("id".into(), Kind::Record(vec![view.name.clone().into()])); 189 | 190 | Ok(fields) 191 | } 192 | Kind::Either(..) => anyhow::bail!("Multiple tables in view are not currently supported"), 193 | _ => anyhow::bail!("Expected object return type for view table"), 194 | } 195 | } 196 | 197 | pub fn get_view_return_type( 198 | view: &ViewParsed, 199 | state: &mut QueryState, 200 | ) -> Result { 201 | get_statement_fields( 202 | &Into::::into(&view.what), 203 | state, 204 | Some(&view.expr), 205 | |_fields, _state| {}, 206 | ) 207 | } 208 | -------------------------------------------------------------------------------- /surreal_type_generator/src/step_2_interpret/statements/create_statement.rs: -------------------------------------------------------------------------------- 1 | use surrealdb::sql::{statements::CreateStatement, Data, Fields, Output, Value, Values}; 2 | 3 | use crate::{ 4 | kind, 5 | step_2_interpret::{get_statement_fields, schema::QueryState, utils::get_value_table}, 6 | Kind, 7 | }; 8 | 9 | pub fn get_create_statement_return_type( 10 | create: &CreateStatement, 11 | state: &mut QueryState, 12 | ) -> Result { 13 | let is_only = create.only; 14 | 15 | let return_type = match &create.output { 16 | // default return type 17 | Some(Output::After) | None => get_create_fields(create, state, None)?, 18 | Some(Output::Before | Output::Null) => Kind::Null, 19 | Some(Output::None) => Kind::Null, 20 | Some(Output::Diff) => anyhow::bail!("Create with returned diff is not currently supported"), 21 | Some(Output::Fields(fields)) => get_create_fields(create, state, Some(fields))?, 22 | #[allow(unreachable_patterns)] 23 | _ => anyhow::bail!("Unknown CREATE statement type: {}", create), 24 | }; 25 | 26 | match &create.data { 27 | Some(content) => validate_data_type(state, &create.what, content)?, 28 | None => {} 29 | } 30 | 31 | if is_only { 32 | Ok(return_type) 33 | } else { 34 | Ok(kind!(Arr return_type)) 35 | } 36 | } 37 | 38 | fn get_create_fields( 39 | create: &CreateStatement, 40 | state: &mut QueryState, 41 | fields: Option<&Fields>, 42 | ) -> Result { 43 | get_statement_fields(&create.what, state, fields, |fields, state| { 44 | state.set_local("after", kind!(Obj fields.clone())); 45 | state.set_local("before", Kind::Null); 46 | state.set_local("this", kind!(Obj fields.clone())); 47 | }) 48 | } 49 | 50 | fn validate_data_type( 51 | state: &mut QueryState, 52 | what: &Values, 53 | data: &Data, 54 | ) -> Result<(), anyhow::Error> { 55 | match data { 56 | Data::ContentExpression(Value::Param(param)) => { 57 | // we want to infer the type of this param by reading the table's required types and fields for insertion 58 | let mut tables = Vec::new(); 59 | 60 | for table in what.iter() { 61 | let table_name = get_value_table(table, state)?; 62 | match state.schema.schema.tables.get(&table_name) { 63 | Some(table) => { 64 | let create_fields = kind!(Obj table.compute_create_fields()?); 65 | tables 66 | .push(kind!(Either [create_fields.clone(), kind!(Arr create_fields)])); 67 | } 68 | None => anyhow::bail!( 69 | "Trying to create a record with an unknown or view table: {}", 70 | table_name 71 | ), 72 | } 73 | } 74 | 75 | if tables.len() == 1 { 76 | state.infer(param.0.as_str(), tables.pop().unwrap()); 77 | } else if tables.len() > 1 { 78 | state.infer(¶m.0.as_str(), Kind::Either(tables)); 79 | } 80 | 81 | Ok(()) 82 | } 83 | // TODO: support other data types and variable inference 84 | _ => Ok(()), 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /surreal_type_generator/src/step_2_interpret/statements/delete_statement.rs: -------------------------------------------------------------------------------- 1 | use surrealdb::sql::{statements::DeleteStatement, Fields, Kind, Output}; 2 | 3 | use crate::{ 4 | kind, 5 | step_2_interpret::{get_statement_fields, schema::QueryState}, 6 | }; 7 | 8 | pub fn get_delete_statement_return_type( 9 | delete: &DeleteStatement, 10 | state: &mut QueryState, 11 | ) -> Result { 12 | let is_only = delete.only; 13 | 14 | let return_type = match &delete.output { 15 | Some(Output::After) => Kind::Null, 16 | Some(Output::Before) => get_delete_fields(delete, state, None)?, 17 | Some(Output::Null) => Kind::Null, 18 | Some(Output::Diff) => Err(anyhow::anyhow!("Delete with returned diff not supported"))?, 19 | Some(Output::Fields(fields)) => get_delete_fields(delete, state, Some(fields))?, 20 | Some(Output::None) => Kind::Null, 21 | None => Kind::Null, 22 | #[allow(unreachable_patterns)] 23 | _ => Err(anyhow::anyhow!(format!( 24 | "Unknown DELETE statement type: {}", 25 | delete 26 | )))?, 27 | }; 28 | 29 | if is_only { 30 | Ok(return_type) 31 | } else { 32 | Ok(kind!([return_type])) 33 | } 34 | } 35 | 36 | fn get_delete_fields( 37 | delete: &DeleteStatement, 38 | state: &mut QueryState, 39 | fields: Option<&Fields>, 40 | ) -> Result { 41 | get_statement_fields(&delete.what, state, fields, |fields, state| { 42 | state.set_local("after", Kind::Null); 43 | state.set_local("before", kind!(Obj fields.clone())); 44 | 45 | // set all fields to null because they have been deleted 46 | fields.iter_mut().for_each(|(_, value)| *value = Kind::Null); 47 | }) 48 | } 49 | -------------------------------------------------------------------------------- /surreal_type_generator/src/step_2_interpret/statements/insert_statement.rs: -------------------------------------------------------------------------------- 1 | use surrealdb::sql::{statements::InsertStatement, Data, Fields, Output, Value}; 2 | 3 | use crate::{ 4 | kind, 5 | step_2_interpret::{get_statement_fields, schema::QueryState, utils::get_value_table}, 6 | Kind, 7 | }; 8 | 9 | pub fn get_insert_statement_return_type( 10 | insert: &InsertStatement, 11 | state: &mut QueryState, 12 | ) -> Result { 13 | let into = match &insert.into { 14 | Some(into) => into, 15 | None => anyhow::bail!("Expected table name"), 16 | }; 17 | 18 | let return_type = match &insert.output { 19 | Some(Output::After) | None => get_insert_fields(&into, state, None)?, 20 | Some(Output::Before | Output::Null) => Kind::Null, 21 | Some(Output::None) => Kind::Null, 22 | Some(Output::Diff) => anyhow::bail!("Insert with returned diff is not currently supported"), 23 | Some(Output::Fields(fields)) => get_insert_fields(&into, state, Some(fields))?, 24 | #[allow(unreachable_patterns)] 25 | _ => anyhow::bail!("Unknown INSERT statement type: {}", insert), 26 | }; 27 | 28 | validate_data_type(state, &into, &insert.data)?; 29 | 30 | Ok(kind!(Arr return_type)) 31 | } 32 | 33 | fn get_insert_fields( 34 | table: &Value, 35 | state: &mut QueryState, 36 | fields: Option<&Fields>, 37 | ) -> Result { 38 | get_statement_fields(&[table.clone()], state, fields, |fields, state| { 39 | state.set_local("after", kind!(Obj fields.clone())); 40 | state.set_local("before", kind!(Null)); 41 | state.set_local("this", kind!(Obj fields.clone())); 42 | }) 43 | } 44 | 45 | fn validate_data_type( 46 | state: &mut QueryState, 47 | table: &Value, 48 | data: &Data, 49 | ) -> Result<(), anyhow::Error> { 50 | match data { 51 | Data::SingleExpression(Value::Param(param)) => { 52 | let mut tables = vec![]; 53 | 54 | // for value in values { 55 | let table_name = get_value_table(&table, state)?; 56 | 57 | match state.schema.schema.tables.get(&table_name) { 58 | Some(table) => { 59 | let insert_fields = kind!(Obj table.compute_create_fields()?); 60 | 61 | // can insert multiple or a single record 62 | tables.push(kind!(Either[kind!(Arr insert_fields.clone()), insert_fields])); 63 | } 64 | None => anyhow::bail!( 65 | "Trying to insert a record into a non-existent table: {}", 66 | table_name 67 | ), 68 | } 69 | // } 70 | 71 | if tables.len() == 1 { 72 | state.infer(¶m.0.as_str(), tables.pop().unwrap()); 73 | } else if tables.len() > 1 { 74 | state.infer(¶m.0.as_str(), Kind::Either(tables)); 75 | } 76 | 77 | Ok(()) 78 | } 79 | // TODO: Support other types of data and variable inference 80 | _ => Ok(()), 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /surreal_type_generator/src/step_2_interpret/statements/let_statement.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeMap; 2 | use surrealdb::sql::{statements::SetStatement, Kind}; 3 | 4 | use crate::step_2_interpret::{return_types::get_value_return_type, QueryState}; 5 | 6 | pub fn interpret_let_statement( 7 | let_statement: &SetStatement, 8 | state: &mut QueryState, 9 | ) -> anyhow::Result { 10 | let kind = match let_statement { 11 | SetStatement { 12 | kind: Some(kind), .. 13 | } => kind.clone(), 14 | SetStatement { 15 | kind: None, what, .. 16 | } => get_value_return_type(what, &BTreeMap::new(), state)?, 17 | }; 18 | 19 | state.set_local(&let_statement.name, kind); 20 | 21 | Ok(Kind::Null) 22 | } 23 | -------------------------------------------------------------------------------- /surreal_type_generator/src/step_2_interpret/statements/mod.rs: -------------------------------------------------------------------------------- 1 | mod create_statement; 2 | mod delete_statement; 3 | mod insert_statement; 4 | mod let_statement; 5 | mod return_statement; 6 | mod select_statement; 7 | mod update_statement; 8 | mod upsert_statement; 9 | 10 | pub use create_statement::get_create_statement_return_type; 11 | pub use delete_statement::get_delete_statement_return_type; 12 | pub use insert_statement::get_insert_statement_return_type; 13 | pub use let_statement::interpret_let_statement; 14 | pub use return_statement::get_return_statement_return_type; 15 | pub use select_statement::get_select_statement_return_type; 16 | pub use update_statement::get_update_statement_return_type; 17 | pub use upsert_statement::get_upsert_statement_return_type; 18 | -------------------------------------------------------------------------------- /surreal_type_generator/src/step_2_interpret/statements/return_statement.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeMap; 2 | 3 | use surrealdb::sql::statements::OutputStatement; 4 | 5 | use crate::{ 6 | step_2_interpret::{return_types::get_value_return_type, schema::QueryState}, 7 | Kind, 8 | }; 9 | 10 | pub fn get_return_statement_return_type( 11 | output: &OutputStatement, 12 | state: &mut QueryState, 13 | ) -> Result { 14 | Ok(match output { 15 | OutputStatement { 16 | what, fetch: None, .. 17 | } => get_value_return_type(what, &BTreeMap::new(), state)?, 18 | OutputStatement { 19 | what: _, 20 | fetch: Some(_), 21 | .. 22 | } => anyhow::bail!("TODO: Support fetch fields"), 23 | }) 24 | } 25 | -------------------------------------------------------------------------------- /surreal_type_generator/src/step_2_interpret/statements/select_statement.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | kind, 3 | step_2_interpret::{return_types::get_statement_fields, schema::QueryState}, 4 | Kind, 5 | }; 6 | 7 | use surrealdb::sql::statements::SelectStatement; 8 | 9 | pub fn get_select_statement_return_type( 10 | select: &SelectStatement, 11 | state: &mut QueryState, 12 | ) -> Result { 13 | if select.only { 14 | // only will error if the select statement returns nothing 15 | Ok(get_select_fields(select, state)?) 16 | } else { 17 | Ok(kind!(Arr get_select_fields(select, state)?)) 18 | } 19 | } 20 | 21 | fn get_select_fields( 22 | select: &SelectStatement, 23 | state: &mut QueryState, 24 | ) -> Result { 25 | get_statement_fields(&select.what, state, Some(&select.expr), |fields, state| { 26 | state.set_local("this", kind!(Obj fields.clone())); 27 | }) 28 | } 29 | -------------------------------------------------------------------------------- /surreal_type_generator/src/step_2_interpret/statements/update_statement.rs: -------------------------------------------------------------------------------- 1 | use surrealdb::sql::{statements::UpdateStatement, Data, Fields, Output, Values}; 2 | 3 | use crate::{ 4 | kind, 5 | step_2_interpret::{get_statement_fields, schema::QueryState}, 6 | Kind, 7 | }; 8 | 9 | pub fn get_update_statement_return_type( 10 | update: &UpdateStatement, 11 | state: &mut QueryState, 12 | ) -> Result { 13 | let is_only = update.only; 14 | 15 | let return_type = match &update.output { 16 | Some(Output::After) | None => get_update_fields(update, state, None)?, 17 | Some(Output::Before) => { 18 | Kind::Either(vec![get_update_fields(update, state, None)?, Kind::Null]) 19 | } 20 | Some(Output::Null) => Kind::Null, 21 | Some(Output::Diff) => Err(anyhow::anyhow!("Update with returned diff not supported"))?, 22 | Some(Output::Fields(fields)) => get_update_fields(update, state, Some(fields))?, 23 | Some(Output::None) => Kind::Null, 24 | #[allow(unreachable_patterns)] 25 | _ => Err(anyhow::anyhow!(format!( 26 | "Unknown UPDATE statement type: {}", 27 | update 28 | )))?, 29 | }; 30 | 31 | match &update.data { 32 | Some(content) => validate_data_type(state, &update.what, &content)?, 33 | None => {} 34 | } 35 | 36 | if is_only { 37 | Ok(return_type) 38 | } else { 39 | Ok(kind!(Arr return_type)) 40 | } 41 | } 42 | 43 | fn get_update_fields( 44 | update: &UpdateStatement, 45 | state: &mut QueryState, 46 | fields: Option<&Fields>, 47 | ) -> Result { 48 | get_statement_fields(&update.what, state, fields, |fields, state| { 49 | state.set_local("after", kind!(Obj fields.clone())); 50 | state.set_local( 51 | "before", 52 | kind!(Either[kind!(Obj fields.clone()), kind!(Null)]), 53 | ); 54 | state.set_local("this", kind!(Obj fields.clone())); 55 | }) 56 | } 57 | 58 | fn validate_data_type( 59 | state: &mut QueryState, 60 | what: &Values, 61 | data: &Data, 62 | ) -> Result<(), anyhow::Error> { 63 | let _ = state; 64 | let _ = what; 65 | match data { 66 | Data::SetExpression(sets) => { 67 | for _set in sets.iter() { 68 | // TODO 69 | 70 | // let mut tables = vec![]; 71 | 72 | // for table in what.iter() { 73 | // let table_name = match &table { 74 | // Value::Table(table) => table.0.as_str(), 75 | // _ => anyhow::bail!("Expected table name"), 76 | // }; 77 | // } 78 | 79 | // if tables.len() == 1 { 80 | // state.infer(param.0.as_str(), tables.pop().unwrap()); 81 | // } else if tables.len() > 1 { 82 | // state.infer(¶m.0.as_str(), Kind::Either(tables)); 83 | // } 84 | } 85 | 86 | Ok(()) 87 | } 88 | _ => Err(anyhow::anyhow!( 89 | "Unsupported data type for UPDATE statement" 90 | ))?, 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /surreal_type_generator/src/step_2_interpret/statements/upsert_statement.rs: -------------------------------------------------------------------------------- 1 | use surrealdb::sql::{statements::UpsertStatement, Data, Fields, Output, Value, Values}; 2 | 3 | use crate::{ 4 | kind, 5 | step_2_interpret::{get_statement_fields, schema::QueryState, utils::get_value_table}, 6 | Kind, 7 | }; 8 | 9 | pub fn get_upsert_statement_return_type( 10 | upsert: &UpsertStatement, 11 | state: &mut QueryState, 12 | ) -> Result { 13 | let is_only = upsert.only; 14 | 15 | let return_type = match &upsert.output { 16 | Some(Output::After) | None => get_upsert_fields(upsert, state, None)?, 17 | Some(Output::Before) => { 18 | Kind::Either(vec![Kind::Null, get_upsert_fields(upsert, state, None)?]) 19 | } 20 | Some(Output::None) => Kind::Null, 21 | Some(Output::Diff) => Err(anyhow::anyhow!("Create with returned diff not supported"))?, 22 | Some(Output::Fields(fields)) => get_upsert_fields(upsert, state, Some(fields))?, 23 | #[allow(unreachable_patterns)] 24 | _ => Err(anyhow::anyhow!(format!( 25 | "Unknown UPSERT statement type: {}", 26 | upsert 27 | )))?, 28 | }; 29 | 30 | match &upsert.data { 31 | Some(content) => validate_data_type(state, &upsert.what, content)?, 32 | None => {} 33 | } 34 | 35 | if is_only { 36 | Ok(return_type) 37 | } else { 38 | Ok(kind!(Arr return_type)) 39 | } 40 | } 41 | 42 | pub fn validate_data_type( 43 | state: &mut QueryState, 44 | what: &Values, 45 | data: &Data, 46 | ) -> Result<(), anyhow::Error> { 47 | match data { 48 | Data::MergeExpression(Value::Param(param)) 49 | | Data::ContentExpression(Value::Param(param)) => { 50 | // we want to infer the type of this param by reading the table's required types and fields for insertion 51 | let mut tables = Vec::new(); 52 | 53 | for table in what.iter() { 54 | let table_name = get_value_table(table, state)?; 55 | match state.schema.schema.tables.get(&table_name) { 56 | Some(table) => { 57 | let create_fields = kind!(Obj table.compute_create_fields()?); 58 | tables 59 | .push(kind!(Either [create_fields.clone(), kind!(Arr create_fields)])); 60 | } 61 | None => anyhow::bail!( 62 | "Tried to create a record with an unknown or view table: {}", 63 | table_name 64 | ), 65 | } 66 | } 67 | 68 | if tables.len() == 1 { 69 | state.infer(param.0.as_str(), tables.pop().unwrap()); 70 | } else if tables.len() > 1 { 71 | state.infer(¶m.0.as_str(), Kind::Either(tables)); 72 | } 73 | 74 | Ok(()) 75 | } 76 | // TODO: support other data types and variable inference 77 | _ => Ok(()), 78 | } 79 | } 80 | 81 | fn get_upsert_fields( 82 | upsert: &UpsertStatement, 83 | state: &mut QueryState, 84 | fields: Option<&Fields>, 85 | ) -> Result { 86 | get_statement_fields(&upsert.what, state, fields, |fields, state| { 87 | state.set_local("after", kind!(Obj fields.clone())); 88 | state.set_local("before", kind!(Obj fields.clone())); 89 | state.set_local("this", kind!(Obj fields.clone())); 90 | }) 91 | } 92 | -------------------------------------------------------------------------------- /surreal_type_generator/src/step_2_interpret/utils.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeMap; 2 | 3 | use crate::{kind, Kind}; 4 | use surrealdb::sql::{Ident, Literal, Param, Part, Thing, Value}; 5 | 6 | use super::schema::{QueryState, TableFields}; 7 | 8 | pub fn get_value_table( 9 | what_value: &Value, 10 | state: &mut QueryState, 11 | ) -> Result { 12 | match what_value { 13 | Value::Table(table) => Ok(table.0.clone()), 14 | Value::Param(Param { 15 | 0: Ident { 0: param_ident, .. }, 16 | .. 17 | }) => match state.get(param_ident.as_str()) { 18 | Some(Kind::Record(tables)) => Ok(tables[0].0.clone()), 19 | // We can technically query on a option> so we can allow that 20 | Some(Kind::Option(box Kind::Record(tables))) => Ok(tables[0].0.clone()), 21 | _ => anyhow::bail!("Expected record type for param: {}", param_ident), 22 | }, 23 | Value::Thing(Thing { tb, .. }) => Ok(tb.clone()), 24 | _ => anyhow::bail!("Expected record type, got: {}", what_value), 25 | } 26 | } 27 | 28 | pub fn get_what_fields( 29 | what_value: &Value, 30 | state: &mut QueryState, 31 | ) -> Result { 32 | let table_name = get_value_table(what_value, state)?; 33 | Ok(state.table_select_fields(&table_name)?) 34 | } 35 | 36 | pub fn merge_into_map_recursively( 37 | map: &mut BTreeMap, 38 | parts: &[Part], 39 | return_type: Kind, 40 | ) -> Result<(), anyhow::Error> { 41 | if parts.is_empty() { 42 | return Ok(()); 43 | } 44 | 45 | match &parts[0] { 46 | Part::Field(field_name) => { 47 | if parts.len() == 1 { 48 | map.insert(field_name.0.clone(), return_type); 49 | } else { 50 | // check if the return type is a double optional, because something like xyz.abc returns option> if xyz and abc are both optional 51 | if is_double_optional(&return_type) { 52 | let next_map = map 53 | .entry(field_name.to_string()) 54 | .or_insert_with(|| kind!(Opt(kind!({})))); 55 | 56 | match next_map { 57 | Kind::Option(box Kind::Literal(Literal::Object(nested_fields))) => { 58 | merge_into_map_recursively( 59 | nested_fields, 60 | &parts[1..], 61 | match return_type { 62 | Kind::Option(return_type) => *return_type, 63 | _ => anyhow::bail!("Expected Option, got {:?}", return_type), 64 | }, 65 | )? 66 | } 67 | // Kind::Literal(Literal::Object(nested_fields)) => { 68 | // merge_into_map_recursively( 69 | // nested_fields, 70 | // &parts[1..], 71 | // kind!(Opt(return_type)), 72 | // )? 73 | // } 74 | // TODO: If we have something like SELECT *, xyz.abc FROM xyz, it will fail because it thinks `xyz` is already a record 75 | // it instead needs to replace here 76 | // Kind::Option(box Kind::Record(tables)) => { 77 | // merge_into_map_recursively( 78 | // tables[0].1.clone(), 79 | // &parts[1..], 80 | // return_type.expect_option()?, 81 | // )? 82 | // } 83 | _ => anyhow::bail!("Unsupported field return type: {:?}", next_map), 84 | } 85 | } else { 86 | let next_map = map 87 | .entry(field_name.to_string()) 88 | .or_insert_with(|| kind!({})); 89 | 90 | match next_map { 91 | Kind::Literal(Literal::Object(nested_fields)) => { 92 | merge_into_map_recursively(nested_fields, &parts[1..], return_type)? 93 | } 94 | _ => anyhow::bail!("Unsupported field return type: {:?}", next_map), 95 | } 96 | } 97 | } 98 | } 99 | Part::All => { 100 | if let Some(Part::Field(ident)) = parts.get(1) { 101 | map.insert(ident.to_string(), kind!(Arr return_type)); 102 | } else { 103 | map.insert("*".to_string(), kind!(Arr return_type)); 104 | } 105 | } 106 | _ => anyhow::bail!( 107 | "Unsupported part in merge_into_map_recursively: {:?}", 108 | parts 109 | ), 110 | } 111 | 112 | Ok(()) 113 | } 114 | 115 | pub fn is_double_optional(return_type: &Kind) -> bool { 116 | match return_type { 117 | Kind::Option(return_type) => match **return_type { 118 | Kind::Option(_) => true, 119 | _ => false, 120 | }, 121 | _ => false, 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /surreal_type_generator/src/step_3_codegen/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod typescript; 2 | 3 | use std::{ 4 | collections::BTreeMap, 5 | fs, 6 | io::{self, Read}, 7 | path::{Path, PathBuf}, 8 | sync::Arc, 9 | }; 10 | 11 | use surrealdb::sql::{Statement, Statements}; 12 | 13 | use crate::{ 14 | step_2_interpret::{interpret_query, QueryState, SchemaState}, 15 | Kind, 16 | }; 17 | 18 | pub struct TypeData { 19 | pub schema: Arc, 20 | pub name: String, 21 | pub statements: Statements, 22 | pub return_type: Vec, 23 | pub variables: BTreeMap, 24 | } 25 | 26 | pub fn generate_type_info( 27 | file_name: &str, 28 | query: &str, 29 | state: Arc, 30 | ) -> Result { 31 | let result = crate::step_3_codegen::output_query_type(query, state.clone())?; 32 | let camel_case_file_name = filename_to_camel_case(file_name)?; 33 | 34 | Ok(TypeData { 35 | schema: state.clone(), 36 | name: camel_case_file_name, 37 | return_type: result.return_types, 38 | statements: { 39 | let mut s = Statements::default(); 40 | s.0 = result.statements; 41 | s 42 | }, 43 | variables: result.variables, 44 | }) 45 | } 46 | 47 | fn filename_to_camel_case(filename: &str) -> Result { 48 | let parts: Vec<&str> = filename.split('.').collect(); 49 | if parts.len() != 2 { 50 | return Err(anyhow::anyhow!( 51 | "Filename must be of the form `name.extension`" 52 | )); 53 | } 54 | 55 | let name_part = parts[0]; 56 | let mut camel_case_name = String::new(); 57 | let mut new_word = true; 58 | 59 | for c in name_part.chars() { 60 | if c == '_' { 61 | new_word = true; 62 | } else if new_word { 63 | camel_case_name.push(c.to_uppercase().next().unwrap()); 64 | new_word = false; 65 | } else { 66 | camel_case_name.push(c); 67 | } 68 | } 69 | 70 | Ok(camel_case_name) 71 | } 72 | 73 | pub struct QueryResult { 74 | pub statements: Vec, 75 | pub variables: BTreeMap, 76 | pub state: QueryState, 77 | pub return_types: Vec, 78 | } 79 | 80 | pub fn query_to_return_type(query: &str, schema: &str) -> anyhow::Result { 81 | query_to_return_type_with_globals(query, schema, &BTreeMap::new()) 82 | } 83 | 84 | pub fn output_query_type(query: &str, schema: Arc) -> anyhow::Result { 85 | let parsed_query = crate::step_1_parse_sql::parse_query(query)?; 86 | let mut query_state = QueryState::new(schema, parsed_query.casted_parameters); 87 | 88 | Ok(QueryResult { 89 | return_types: interpret_query(&parsed_query.statements, &mut query_state)?, 90 | statements: parsed_query.statements, 91 | variables: query_state.extract_required_variables(), 92 | state: query_state, 93 | }) 94 | } 95 | 96 | pub fn query_to_return_type_with_globals( 97 | query: &str, 98 | schema: &str, 99 | globals: &BTreeMap, 100 | ) -> anyhow::Result { 101 | let state = crate::step_2_interpret::interpret_schema(schema, globals.clone())?; 102 | 103 | output_query_type(query, Arc::new(state)) 104 | } 105 | 106 | pub fn read_surql_files(dir_path: &str) -> io::Result> { 107 | let path = Path::new(dir_path); 108 | let mut file_contents = BTreeMap::new(); 109 | 110 | if !path.is_dir() { 111 | return Err(io::Error::new(io::ErrorKind::NotFound, "Not a directory")); 112 | } 113 | 114 | for entry in fs::read_dir(path)? { 115 | let entry = entry?; 116 | let file_path = entry.path(); 117 | 118 | if file_path.is_file() && file_path.extension().map_or(false, |ext| ext == "surql") { 119 | let file_name = file_path 120 | .file_name() 121 | .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "Invalid file name"))? 122 | .to_string_lossy() 123 | .into_owned(); 124 | 125 | file_contents.insert(file_name, read_file(&file_path)?); 126 | } 127 | } 128 | 129 | Ok(file_contents) 130 | } 131 | 132 | pub fn read_file(file_path: &PathBuf) -> io::Result { 133 | let mut file = fs::File::open(file_path)?; 134 | let mut contents = String::new(); 135 | file.read_to_string(&mut contents)?; 136 | 137 | Ok(contents) 138 | } 139 | -------------------------------------------------------------------------------- /surreal_type_generator/src/step_3_codegen/typescript/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::{kind, step_1_parse_sql::ViewParsed, utils::printing::indent, Kind, PrettyString}; 2 | use surrealdb::sql::{Literal, Table}; 3 | 4 | use crate::step_2_interpret::SchemaState; 5 | 6 | use super::TypeData; 7 | 8 | pub fn format_comment(string: &str) -> String { 9 | let mut lines = Vec::new(); 10 | lines.push("/**".into()); 11 | for line in string.lines() { 12 | lines.push(format!(" * {}", line)); 13 | } 14 | lines.push(" */".into()); 15 | lines.join("\n") 16 | } 17 | 18 | pub fn generate_typescript_output( 19 | types: &[TypeData], 20 | header: &str, 21 | ) -> Result { 22 | let mut output = String::new(); 23 | 24 | colored::control::set_override(false); 25 | 26 | output.push_str(header); 27 | output.push_str("\n\n"); 28 | 29 | output.push_str(&format!("export type Queries = {{\n{}}}\n", { 30 | let mut output = String::new(); 31 | for TypeData { 32 | name, variables, .. 33 | } in types 34 | { 35 | let has_variables = variables.len() > 0; 36 | output.push_str(&format!( 37 | " [{}Query]: {{variables: {}, result: {}Result }}\n", 38 | name, 39 | if has_variables { 40 | format!("{}Variables", name) 41 | } else { 42 | "never".into() 43 | }, 44 | name, 45 | )); 46 | } 47 | output 48 | },)); 49 | 50 | for TypeData { 51 | schema, 52 | name, 53 | statements, 54 | return_type, 55 | variables, 56 | } in types 57 | { 58 | output.push_str(&format_comment(&format!( 59 | "## {} query results:\n\n```surql\n{}\n```", 60 | name, 61 | &return_type 62 | .iter() 63 | .enumerate() 64 | .map(|(i, x)| { 65 | format!( 66 | "/// -------------\n{}{}:\n/// -------------\n{}", 67 | "/// Result ", 68 | i, 69 | x.pretty_string() 70 | ) 71 | }) 72 | .collect::>() 73 | .join("\n\n"), 74 | ))); 75 | output.push_str("\n"); 76 | output.push_str(&format!( 77 | "export const {}Query = {}\n", 78 | name, 79 | // Comment the query name so that they are distinguished between identical queries 80 | serde_json::to_string(&format!("-- {}\n{}", name, &statements.to_string()))? 81 | )); 82 | output.push_str(&format!("export type {}Result = [\n{}\n]\n", name, { 83 | let mut lines = Vec::new(); 84 | for result in return_type { 85 | lines.push(generate_type_definition(result, schema)?); 86 | } 87 | indent(&lines.join(",\n")) 88 | })); 89 | 90 | if variables.len() > 0 { 91 | output.push_str(&format!("export type {}Variables = ", name)); 92 | 93 | output.push_str(&generate_type_definition( 94 | &kind!(Obj variables.clone()), 95 | schema, 96 | )?); 97 | 98 | output.push_str("\n"); 99 | } 100 | } 101 | 102 | output.push_str(r#" 103 | 104 | export type Variables = Queries[Q]["variables"] extends never ? [] : [Queries[Q]["variables"]] 105 | 106 | /** 107 | * A Surreal client with typed queries from codegen. 108 | * 109 | * Usage: 110 | * 111 | * ```surql 112 | * // [your_schema_path].surql 113 | * DEFINE TABLE user SCHEMAFULL; 114 | * DEFINE FIELD name ON user TYPE string; 115 | * ``` 116 | * ```surql 117 | * // queries/get_user.surql 118 | * SELECT * FROM ONLY $auth; 119 | * ``` 120 | * 121 | * ```ts 122 | * // usage example 123 | * import { TypedSurreal, GetUserQuery } from "[YOUR_OUTPUT_PATH].ts" 124 | * const db = new TypedSurreal() 125 | * 126 | * await db.connect(...) 127 | * 128 | * const [ 129 | * user // { id: RecordId<"user">, name: string } 130 | * ] = await surreal.typed(GetUserQuery) 131 | * 132 | * console.log(user) // { id: 1, name: "John Doe" } 133 | * ``` 134 | */ 135 | export class TypedSurreal extends Surreal { 136 | typed(query: Q, ...rest: Variables): Promise { 137 | return this.query(query, rest[0]) 138 | } 139 | } 140 | "#); 141 | 142 | Ok(output) 143 | } 144 | 145 | fn get_table_id_type(table: &Table, schema: &SchemaState) -> Result { 146 | let record_id_type = get_record_id_value_type(table.0.as_str(), schema)?; 147 | generate_type_definition(&record_id_type, schema) 148 | } 149 | 150 | pub fn interpret_view_id_value_kind( 151 | view: &ViewParsed, 152 | state: &SchemaState, 153 | ) -> Result { 154 | if view.what.0.len() != 1 { 155 | anyhow::bail!("Expected single table in view"); 156 | } 157 | 158 | let table_name = view.what.0.first().unwrap().to_string(); 159 | 160 | match &view.groups { 161 | Some(_groups) => { 162 | // TODO: implement this 163 | return Ok(Kind::Any); 164 | } 165 | None => get_record_id_value_type(&table_name, state), 166 | } 167 | } 168 | 169 | pub fn get_record_id_value_type(table: &str, schema: &SchemaState) -> Result { 170 | match schema.schema.tables.get(table) { 171 | Some(table) => Ok(table.id_value_type.clone()), 172 | None => match schema.schema.views.get(table).cloned() { 173 | Some(view) => interpret_view_id_value_kind(&view, schema), 174 | None => anyhow::bail!("Table `{}` not found for aggregate view `{}`", table, table), 175 | }, 176 | } 177 | } 178 | 179 | fn generate_type_definition( 180 | return_type: &Kind, 181 | schema: &SchemaState, 182 | ) -> Result { 183 | match return_type { 184 | Kind::Any => Ok("any".to_string()), 185 | Kind::Number => Ok("number".to_string()), 186 | Kind::Null => Ok("null".to_string()), 187 | Kind::String => Ok("string".to_string()), 188 | Kind::Int => Ok("number".to_string()), 189 | Kind::Float => Ok("number".to_string()), 190 | Kind::Datetime => Ok("Date".to_string()), 191 | Kind::Duration => Ok("Duration".to_string()), 192 | Kind::Decimal => Ok("Decimal".to_string()), 193 | Kind::Bool => Ok("boolean".to_string()), 194 | Kind::Uuid => Ok("string".to_string()), 195 | Kind::Array(array, ..) => { 196 | let string = generate_type_definition(&**array, schema)?; 197 | Ok(format!("Array<{}>", string)) 198 | } 199 | Kind::Either(vec) => { 200 | let mut output = String::new(); 201 | output.push_str("(\n"); 202 | 203 | let mut lines = Vec::new(); 204 | 205 | for return_type in vec.into_iter() { 206 | lines.push(format!( 207 | "| {}", 208 | generate_type_definition(return_type, schema)? 209 | )); 210 | } 211 | 212 | output.push_str(&indent(&lines.join("\n"))); 213 | 214 | output.push_str("\n)"); 215 | Ok(output) 216 | } 217 | Kind::Record(tables) => { 218 | let mut output = String::new(); 219 | output.push_str("(RecordId<"); 220 | 221 | let table_idents = tables 222 | .iter() 223 | .map(|table| format!("\"{}\"", table.0)) 224 | .collect::>(); 225 | let tables_joined = table_idents.join(" | "); 226 | 227 | output.push_str(&tables_joined); 228 | 229 | output.push_str("> & { id: "); 230 | output.push_str(&get_table_id_type(tables.first().unwrap(), schema)?); 231 | output.push_str(" })"); 232 | Ok(output) 233 | } 234 | Kind::Option(optional_value) => { 235 | let string = generate_type_definition(&**optional_value, schema)?; 236 | Ok(format!("{} | undefined", string)) 237 | } 238 | Kind::Object => Ok("any".to_string()), 239 | 240 | // ======== 241 | // Literals 242 | // ======== 243 | Kind::Literal(Literal::String(string)) => Ok(serde_json::to_string(&string)?), 244 | Kind::Literal(Literal::Duration(_duration)) => Ok("Duration".to_string()), 245 | Kind::Literal(Literal::Number(number)) => Ok(number.to_string()), 246 | Kind::Literal(Literal::DiscriminatedObject(_, objects)) => { 247 | let kind = Kind::Either( 248 | objects 249 | .clone() 250 | .into_iter() 251 | .map(|kind| Kind::Literal(Literal::Object(kind))) 252 | .collect(), 253 | ); 254 | 255 | Ok(generate_type_definition(&kind, schema)?) 256 | } 257 | Kind::Literal(Literal::Object(map)) => { 258 | let mut output = String::new(); 259 | output.push_str("{\n"); 260 | 261 | // sort alphabetically for deterministic output 262 | let mut map: Vec<(_, _)> = map.into_iter().collect(); 263 | map.sort_by_key(|x| x.0.to_string()); 264 | 265 | let mut key_string = Vec::new(); 266 | 267 | for (key, value) in map { 268 | key_string.push(format!( 269 | "{}{}: {},\n", 270 | key, 271 | match value { 272 | Kind::Option(_) => "?", 273 | _ => "", 274 | }, 275 | match value { 276 | Kind::Option(inner) => generate_type_definition(inner, schema)?, 277 | value => generate_type_definition(value, schema)?, 278 | }, 279 | )); 280 | } 281 | 282 | let key_string = indent(&key_string.join("")); 283 | 284 | output.push_str(&key_string); 285 | output.push_str("\n}"); 286 | Ok(output) 287 | } 288 | Kind::Literal(Literal::Array(array)) => { 289 | // could be a tuple or an array 290 | if array.len() == 1 { 291 | let string = generate_type_definition(array.first().unwrap(), schema)?; 292 | Ok(format!("Array<{}>", string)) 293 | } else { 294 | let types = array 295 | .iter() 296 | .map(|kind| generate_type_definition(kind, schema)) 297 | .collect::, _>>()?; 298 | let string = types.join(", "); 299 | Ok(format!("[{}]", string)) 300 | } 301 | } 302 | 303 | // Catch all 304 | kind => anyhow::bail!("Kind {:?} not yet supported", kind), 305 | } 306 | } 307 | -------------------------------------------------------------------------------- /surreal_type_generator/src/utils/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod printing; 2 | -------------------------------------------------------------------------------- /surreal_type_generator/src/utils/printing.rs: -------------------------------------------------------------------------------- 1 | use colored::Colorize; 2 | use surrealdb::sql::{Kind, Literal}; 3 | 4 | use crate::step_3_codegen::TypeData; 5 | 6 | #[allow(dead_code)] 7 | pub fn type_info_to_string(type_info: &TypeData) -> String { 8 | let mut lines = Vec::new(); 9 | 10 | for (i, return_type) in type_info.return_type.iter().enumerate() { 11 | lines.push(format!( 12 | "{}{}", 13 | format!("-- Query Result {} --\n", i).white(), 14 | return_type.pretty_string(), 15 | // indent(&return_type.pretty_string()) 16 | )); 17 | } 18 | 19 | lines.join("\n") 20 | } 21 | 22 | pub trait PrettyString { 23 | fn pretty_string(&self) -> String; 24 | } 25 | 26 | impl PrettyString for Kind { 27 | fn pretty_string(&self) -> String { 28 | match self { 29 | Kind::Record(tables) => format!( 30 | "{}{}{}{}", 31 | "record".yellow(), 32 | "<".white(), 33 | tables 34 | .iter() 35 | .map(|table| table.to_string().bright_magenta().to_string()) 36 | .collect::>() 37 | .join(" | "), 38 | ">".white() 39 | ), 40 | Kind::Literal(Literal::Object(fields)) => { 41 | let mut lines = Vec::new(); 42 | 43 | for (key, value) in fields { 44 | lines.push(format!( 45 | "{}{} {}", 46 | key.bright_cyan(), 47 | ":".white(), 48 | value.pretty_string() 49 | )); 50 | } 51 | 52 | format!( 53 | "{}{}{}", 54 | "{\n".white(), 55 | indent(&lines.join(",\n")), 56 | "\n}".white(), 57 | ) 58 | } 59 | Kind::Array(kind, ..) => format!( 60 | "{}{}{}{}", 61 | "array".yellow(), 62 | "<".white(), 63 | kind.pretty_string(), 64 | ">".white() 65 | ), 66 | Kind::Option(kind) => format!( 67 | "{}{}{}{}", 68 | "option".yellow(), 69 | "<".white(), 70 | kind.pretty_string(), 71 | ">".white() 72 | ), 73 | Kind::Either(types) => types 74 | .iter() 75 | .map(|t| t.pretty_string()) 76 | .collect::>() 77 | .join(&" | ".white()), 78 | 79 | // Kind::Any => "any".to_string(), 80 | // Kind::Null => "null".to_string(), 81 | // Kind::Bool => "boolean".to_string(), 82 | // Kind::Duration => "duration".to_string(), 83 | // Kind::Decimal => "decimal".to_string(), 84 | // Kind::Datetime => "datetime".to_string(), 85 | // Kind::String => "string".to_string(), 86 | // Kind::Int => "int".to_string(), 87 | // Kind::Float => "float".to_string(), 88 | // Kind::Number => "number".to_string(), 89 | // Kind::Uuid => "uuid".to_string(), 90 | // Kind::Object => format!("object"), 91 | kind => kind.to_string().yellow().to_string(), 92 | } 93 | } 94 | } 95 | 96 | pub fn indent(str: &str) -> String { 97 | let mut lines = Vec::new(); 98 | for line in str.lines() { 99 | lines.push(format!(" {}", line)); 100 | } 101 | lines.join("\n") 102 | } 103 | -------------------------------------------------------------------------------- /surreal_type_generator/tests/casting_javascript.rs: -------------------------------------------------------------------------------- 1 | use pretty_assertions_sorted::assert_eq_sorted; 2 | use surreal_type_generator::{kind, step_3_codegen::QueryResult}; 3 | 4 | #[test] 5 | fn casting_javascript() -> anyhow::Result<()> { 6 | let query = r#" 7 | RETURN function() { return 123; }; 8 | "#; 9 | 10 | let schema = r#" 11 | DEFINE TABLE foo SCHEMAFULL; 12 | "#; 13 | 14 | let QueryResult { return_types, .. } = 15 | surreal_type_generator::step_3_codegen::query_to_return_type(query, schema)?; 16 | 17 | assert_eq_sorted!(return_types, vec![kind!(Number)]); 18 | 19 | Ok(()) 20 | } 21 | -------------------------------------------------------------------------------- /surreal_type_generator/tests/complex_query.rs: -------------------------------------------------------------------------------- 1 | use pretty_assertions_sorted::assert_eq_sorted; 2 | use surreal_type_generator::{kind, QueryResult}; 3 | 4 | #[test] 5 | fn test_oauth_flow() -> anyhow::Result<()> { 6 | let schema = r#" 7 | DEFINE TABLE user SCHEMAFULL; 8 | DEFINE FIELD email ON user TYPE string; 9 | 10 | DEFINE TABLE account SCHEMAFULL; 11 | DEFINE FIELD user ON account TYPE record; 12 | DEFINE FIELD provider ON account TYPE string; 13 | DEFINE FIELD provider_id ON account TYPE string; 14 | "#; 15 | 16 | let query = r#" 17 | $provider; 18 | $provider_id; 19 | 20 | BEGIN; 21 | 22 | -- Find the existing user via the existing account if one exists 23 | -- Otherwise, we will find or create a user with the email 24 | LET $user = ( 25 | SELECT VALUE user FROM account WHERE 26 | provider = $provider AND 27 | provider_id = $provider_id 28 | )[0] || (UPSERT ONLY user MERGE $new_user RETURN VALUE id); 29 | 30 | -- We want to upsert the new user, even if they already existed 31 | -- So that we can update their email, if it has changed in the provider... 32 | UPSERT $user MERGE $new_user; 33 | 34 | -- The user might've not had an existing account with Google so we 35 | -- want to link their user to the account, if it doesn't already exist 36 | UPSERT ONLY account MERGE { 37 | user: $user, 38 | provider: $provider, 39 | provider_id: $provider_id 40 | }; 41 | 42 | RETURN $user; 43 | 44 | COMMIT; 45 | "#; 46 | 47 | let QueryResult { return_types, .. } = 48 | surreal_type_generator::step_3_codegen::query_to_return_type(query, schema)?; 49 | 50 | assert_eq_sorted!(return_types, vec![kind!(Record["user"])]); 51 | 52 | Ok(()) 53 | } 54 | 55 | // #[test] 56 | // fn test_complex_query() -> anyhow::Result<()> { 57 | // let schema = r#" 58 | // DEFINE TABLE statement SCHEMAFULL; 59 | // DEFINE FIELD content ON TABLE statement TYPE string; 60 | 61 | // DEFINE TABLE statement_vote SCHEMAFULL; 62 | // DEFINE FIELD rating ON TABLE statement_vote TYPE int ASSERT $value >= 0 AND $value <= 5; 63 | // DEFINE FIELD statement ON TABLE statement_vote TYPE record READONLY; 64 | 65 | // DEFINE TABLE statement_rating AS 66 | // SELECT 67 | // math::mean(rating) AS rating_avg, 68 | // count() as total_votes, 69 | // statement 70 | // FROM statement_vote 71 | // GROUP BY statement; 72 | // "#; 73 | 74 | // let query = r#" 75 | // > $statement; 76 | 77 | // RETURN { 78 | // current: ( 79 | // SELECT 80 | // ( 81 | // SELECT rating_avg, total_votes 82 | // FROM ONLY statement_rating:[$parent] 83 | // ) as vote_data 84 | // FROM ONLY $statement 85 | // ) 86 | // } 87 | // "#; 88 | 89 | // let QueryResult { return_types, .. } = 90 | // surreal_type_generator::step_3_codegen::query_to_return_type(query, schema)?; 91 | 92 | // Ok(()) 93 | // } 94 | 95 | // #[test] 96 | // fn aggregate_nested_field() -> anyhow::Result<()> { 97 | // let schema = r#" 98 | // DEFINE TABLE task SCHEMAFULL; 99 | // DEFINE FIELD last_updated ON TABLE task TYPE datetime; 100 | // DEFINE FIELD task_id ON TABLE task TYPE string; 101 | 102 | // DEFINE TABLE task_view AS 103 | // SELECT 104 | // *, 105 | // ( 106 | // SELECT VALUE id 107 | // FROM ONLY task 108 | // WHERE 109 | // last_updated < $parent.last_updated 110 | // AND task_id = $parent.task_id 111 | // LIMIT 1 112 | // )[0] AS previous 113 | // FROM task; 114 | // "#; 115 | 116 | // let query = r#" 117 | // SELECT 118 | // *, 119 | // previous.last_updated 120 | // FROM task_view 121 | // "#; 122 | 123 | // let QueryResult { return_types, .. } = 124 | // surreal_type_generator::step_3_codegen::query_to_return_type(query, schema)?; 125 | 126 | // assert_eq_sorted!( 127 | // return_types, 128 | // vec![Kind::Object(HashMap::from([( 129 | // "previous".to_string(), 130 | // Kind::Object(HashMap::from([( 131 | // "last_updated".to_string(), 132 | // Kind::Datetime 133 | // ),])) 134 | // ),]))] 135 | // ); 136 | 137 | // Ok(()) 138 | // } 139 | 140 | // #[test] 141 | // fn aggregate_nested_field_array() -> anyhow::Result<()> { 142 | // let schema = r#" 143 | // DEFINE TABLE task SCHEMAFULL; 144 | // DEFINE FIELD last_updated ON TABLE task TYPE datetime; 145 | // DEFINE FIELD task_id ON TABLE task TYPE string; 146 | 147 | // DEFINE TABLE task_view AS 148 | // SELECT 149 | // *, 150 | // ( 151 | // SELECT VALUE id 152 | // FROM ONLY task 153 | // WHERE 154 | // last_updated < $parent.last_updated 155 | // AND task_id = $parent.task_id 156 | // LIMIT 1 157 | // ) AS previous 158 | // FROM task; 159 | // "#; 160 | 161 | // let query = r#" 162 | // SELECT 163 | // *, 164 | // previous.last_updated 165 | // FROM task_view 166 | // "#; 167 | 168 | // let QueryResult { return_types, .. } = 169 | // surreal_type_generator::step_3_codegen::query_to_return_type(query, schema)?; 170 | 171 | // assert_eq_sorted!( 172 | // return_types, 173 | // vec![Kind::Object(HashMap::from([( 174 | // "previous".to_string(), 175 | // Kind::Object(HashMap::from([( 176 | // "last_updated".to_string(), 177 | // Kind::Datetime 178 | // ),])) 179 | // ),]))] 180 | // ); 181 | 182 | // Ok(()) 183 | // } 184 | -------------------------------------------------------------------------------- /surreal_type_generator/tests/constant_expression_tests.rs: -------------------------------------------------------------------------------- 1 | use pretty_assertions_sorted::assert_eq_sorted; 2 | use surreal_type_generator::{kind, step_3_codegen::QueryResult}; 3 | 4 | #[test] 5 | fn constant_string() -> anyhow::Result<()> { 6 | let query = r#" 7 | SELECT 8 | "foo", 9 | 123, 10 | true, 11 | false, 12 | NONE, 13 | NULL 14 | FROM ONLY foo 15 | "#; 16 | let schema = r#" 17 | DEFINE TABLE foo SCHEMAFULL; 18 | "#; 19 | 20 | let QueryResult { return_types, .. } = 21 | surreal_type_generator::step_3_codegen::query_to_return_type(query, schema)?; 22 | 23 | assert_eq_sorted!( 24 | return_types, 25 | vec![kind!({ 26 | "123": kind!(Number), 27 | "false": kind!(Bool), 28 | "foo": kind!(String), 29 | "true": kind!(Bool), 30 | "NONE": kind!(Null), 31 | "NULL": kind!(Null) 32 | })] 33 | ); 34 | 35 | Ok(()) 36 | } 37 | -------------------------------------------------------------------------------- /surreal_type_generator/tests/create_statement_tests.rs: -------------------------------------------------------------------------------- 1 | use pretty_assertions_sorted::assert_eq_sorted; 2 | use surreal_type_generator::{kind, var_map, QueryResult}; 3 | 4 | #[test] 5 | fn simple_create_content_query() -> anyhow::Result<()> { 6 | let query = r#" 7 | CREATE user CONTENT { 8 | name: "John Doe", 9 | age: 30, 10 | }; 11 | "#; 12 | let schema = r#" 13 | DEFINE TABLE user SCHEMAFULL; 14 | DEFINE FIELD name ON user TYPE string; 15 | DEFINE FIELD age ON user TYPE number; 16 | "#; 17 | 18 | let QueryResult { return_types, .. } = 19 | surreal_type_generator::step_3_codegen::query_to_return_type(query, schema)?; 20 | 21 | assert_eq_sorted!( 22 | return_types, 23 | vec![kind!([kind!({ 24 | id: kind!(Record ["user"]), 25 | name: kind!(String), 26 | age: kind!(Number) 27 | })])] 28 | ); 29 | 30 | Ok(()) 31 | } 32 | 33 | #[test] 34 | fn create_return_none() -> anyhow::Result<()> { 35 | let query = r#" 36 | CREATE foo RETURN NONE 37 | "#; 38 | let schema = r#" 39 | DEFINE TABLE foo SCHEMAFULL; 40 | "#; 41 | 42 | let QueryResult { return_types, .. } = 43 | surreal_type_generator::step_3_codegen::query_to_return_type(query, schema)?; 44 | 45 | assert_eq_sorted!(return_types, vec![kind!([kind!(Null)])]); 46 | 47 | Ok(()) 48 | } 49 | 50 | #[test] 51 | fn create_return_null() -> anyhow::Result<()> { 52 | let query = r#" 53 | CREATE foo RETURN NULL 54 | "#; 55 | let schema = r#" 56 | DEFINE TABLE foo SCHEMAFULL; 57 | "#; 58 | 59 | let QueryResult { return_types, .. } = 60 | surreal_type_generator::step_3_codegen::query_to_return_type(query, schema)?; 61 | 62 | assert_eq_sorted!(return_types, vec![kind!([kind!(Null)])]); 63 | 64 | Ok(()) 65 | } 66 | 67 | #[test] 68 | fn create_return_before() -> anyhow::Result<()> { 69 | let query = r#" 70 | CREATE foo RETURN BEFORE 71 | "#; 72 | let schema = r#" 73 | DEFINE TABLE foo SCHEMAFULL; 74 | "#; 75 | 76 | let QueryResult { return_types, .. } = 77 | surreal_type_generator::step_3_codegen::query_to_return_type(query, schema)?; 78 | 79 | assert_eq_sorted!(return_types, vec![kind!([kind!(Null)])]); 80 | 81 | Ok(()) 82 | } 83 | 84 | #[test] 85 | fn create_with_set_field() -> anyhow::Result<()> { 86 | let query = r#" 87 | CREATE user SET name = "John Doe" 88 | "#; 89 | let schema = r#" 90 | DEFINE TABLE user SCHEMAFULL; 91 | DEFINE FIELD name ON user TYPE string; 92 | "#; 93 | 94 | let QueryResult { return_types, .. } = 95 | surreal_type_generator::step_3_codegen::query_to_return_type(query, schema)?; 96 | 97 | assert_eq_sorted!( 98 | return_types, 99 | vec![kind!([kind!({ 100 | id: kind!(Record ["user"]), 101 | name: kind!(String) 102 | })])] 103 | ); 104 | 105 | Ok(()) 106 | } 107 | 108 | #[test] 109 | fn create_statement_with_variable_inference() -> anyhow::Result<()> { 110 | let query = r#" 111 | CREATE user CONTENT $user; 112 | "#; 113 | let schema = r#" 114 | DEFINE TABLE user SCHEMAFULL; 115 | DEFINE FIELD name ON user TYPE string; 116 | DEFINE FIELD email ON user TYPE string; 117 | DEFINE FIELD created_at ON user TYPE datetime DEFAULT time::now(); 118 | DEFINE FIELD opt ON user TYPE option; 119 | "#; 120 | 121 | let QueryResult { 122 | return_types, 123 | variables, 124 | .. 125 | } = surreal_type_generator::step_3_codegen::query_to_return_type(query, schema)?; 126 | 127 | let user_vars = kind!({ 128 | id: kind!(Opt (kind!(Record ["user"]))), 129 | name: kind!(String), 130 | email: kind!(String), 131 | created_at: kind!(Opt (kind!(Datetime))), 132 | opt: kind!(Opt (kind!(String))) 133 | }); 134 | 135 | assert_eq_sorted!( 136 | variables, 137 | var_map! { 138 | user: kind!(Either [ 139 | user_vars.clone(), 140 | kind!([user_vars.clone()]), 141 | ]) 142 | } 143 | ); 144 | 145 | assert_eq_sorted!( 146 | return_types, 147 | vec![kind!([kind!({ 148 | id: kind!(Record ["user"]), 149 | name: kind!(String), 150 | email: kind!(String), 151 | created_at: kind!(Datetime), 152 | opt: kind!(Opt (kind!(String))) 153 | })])] 154 | ); 155 | 156 | Ok(()) 157 | } 158 | 159 | #[test] 160 | fn create_statement_with_value_and_default_clauses() -> anyhow::Result<()> { 161 | let query = r#" 162 | CREATE user CONTENT $user"#; 163 | let schema = r#" 164 | DEFINE TABLE user SCHEMAFULL; 165 | DEFINE FIELD name ON user TYPE string; 166 | DEFINE FIELD age ON user TYPE number DEFAULT 30; 167 | DEFINE FIELD email ON user TYPE string VALUE string::lowercase($value); 168 | DEFINE FIELD created_at ON user TYPE datetime VALUE time::now() READONLY; 169 | DEFINE FIELD updated_at ON user TYPE datetime VALUE time::now(); 170 | "#; 171 | 172 | let QueryResult { 173 | return_types, 174 | variables, 175 | .. 176 | } = surreal_type_generator::step_3_codegen::query_to_return_type(query, schema)?; 177 | 178 | let user_vars = kind!({ 179 | id: kind!(Opt (kind!(Record ["user"]))), 180 | name: kind!(String), 181 | age: kind!(Opt (kind!(Number))), 182 | email: kind!(String) 183 | }); 184 | 185 | assert_eq_sorted!( 186 | variables, 187 | var_map! { 188 | user: kind!(Either [ 189 | user_vars.clone(), 190 | kind!(Arr user_vars.clone()), 191 | ]) 192 | } 193 | ); 194 | 195 | assert_eq_sorted!( 196 | return_types, 197 | vec![kind!([kind!({ 198 | id: kind!(Record ["user"]), 199 | name: kind!(String), 200 | age: kind!(Number), 201 | email: kind!(String), 202 | created_at: kind!(Datetime), 203 | updated_at: kind!(Datetime) 204 | })])] 205 | ); 206 | 207 | Ok(()) 208 | } 209 | 210 | #[test] 211 | fn create_statement_with_id_schema_should_be_optional() -> anyhow::Result<()> { 212 | let query = r#" 213 | CREATE foo:bar CONTENT $foo; 214 | "#; 215 | let schema = r#" 216 | DEFINE TABLE foo SCHEMAFULL; 217 | DEFINE FIELD id ON foo TYPE string; 218 | "#; 219 | 220 | let QueryResult { 221 | return_types, 222 | variables, 223 | .. 224 | } = surreal_type_generator::step_3_codegen::query_to_return_type(query, schema)?; 225 | 226 | let foo_vars = kind!({ 227 | id: kind!(Opt (kind!(Record ["foo"]))), 228 | }); 229 | 230 | assert_eq_sorted!( 231 | variables, 232 | var_map! { 233 | foo: kind!(Either [ 234 | foo_vars.clone(), 235 | kind!([foo_vars.clone()]), 236 | ]) 237 | } 238 | ); 239 | 240 | assert_eq_sorted!( 241 | return_types, 242 | vec![kind!([kind!({ 243 | id: kind!(Record ["foo"]), 244 | })])] 245 | ); 246 | 247 | Ok(()) 248 | } 249 | -------------------------------------------------------------------------------- /surreal_type_generator/tests/delete_statement_tests.rs: -------------------------------------------------------------------------------- 1 | use pretty_assertions_sorted::assert_eq_sorted; 2 | use surreal_type_generator::{kind, QueryResult}; 3 | 4 | #[test] 5 | fn query_with_simple_delete() -> anyhow::Result<()> { 6 | let query = r#" 7 | DELETE FROM user; 8 | "#; 9 | let schema = r#" 10 | DEFINE TABLE user SCHEMAFULL; 11 | "#; 12 | 13 | let QueryResult { return_types, .. } = 14 | surreal_type_generator::step_3_codegen::query_to_return_type(query, schema)?; 15 | 16 | assert_eq_sorted!(return_types, vec![kind!([kind!(Null)])]); 17 | 18 | Ok(()) 19 | } 20 | 21 | #[test] 22 | fn query_with_delete_with_only() -> anyhow::Result<()> { 23 | let query = r#" 24 | DELETE FROM ONLY user; 25 | "#; 26 | let schema = r#" 27 | DEFINE TABLE user SCHEMAFULL; 28 | "#; 29 | 30 | let QueryResult { return_types, .. } = 31 | surreal_type_generator::step_3_codegen::query_to_return_type(query, schema)?; 32 | 33 | assert_eq_sorted!(return_types, vec![kind!(Null)]); 34 | 35 | Ok(()) 36 | } 37 | 38 | #[test] 39 | fn query_with_delete_with_after_output() -> anyhow::Result<()> { 40 | let query = r#" 41 | DELETE user RETURN AFTER; 42 | "#; 43 | let schema = r#" 44 | DEFINE TABLE user SCHEMAFULL; 45 | "#; 46 | 47 | let QueryResult { return_types, .. } = 48 | surreal_type_generator::step_3_codegen::query_to_return_type(query, schema)?; 49 | 50 | assert_eq_sorted!(return_types, vec![kind!([kind!(Null)])]); 51 | 52 | Ok(()) 53 | } 54 | 55 | #[test] 56 | fn query_with_delete_with_before_output() -> anyhow::Result<()> { 57 | let query = r#" 58 | DELETE user RETURN BEFORE; 59 | "#; 60 | let schema = r#" 61 | DEFINE TABLE user SCHEMAFULL; 62 | DEFINE FIELD name ON user TYPE string; 63 | "#; 64 | 65 | let QueryResult { return_types, .. } = 66 | surreal_type_generator::step_3_codegen::query_to_return_type(query, schema)?; 67 | 68 | assert_eq_sorted!( 69 | return_types, 70 | vec![kind!([kind!({ 71 | "id": kind!(Record ["user"]), 72 | "name": kind!(String) 73 | })])] 74 | ); 75 | 76 | Ok(()) 77 | } 78 | 79 | #[test] 80 | fn query_with_delete_return_fields() -> anyhow::Result<()> { 81 | let query = r#" 82 | DELETE user RETURN name; 83 | "#; 84 | let schema = r#" 85 | DEFINE TABLE user SCHEMAFULL; 86 | DEFINE FIELD name ON user TYPE string; 87 | "#; 88 | 89 | let QueryResult { return_types, .. } = 90 | surreal_type_generator::step_3_codegen::query_to_return_type(query, schema)?; 91 | 92 | assert_eq_sorted!( 93 | return_types, 94 | vec![kind!([kind!({ 95 | "name": kind!(Null) 96 | })])] 97 | ); 98 | 99 | Ok(()) 100 | } 101 | -------------------------------------------------------------------------------- /surreal_type_generator/tests/field_defaults.rs: -------------------------------------------------------------------------------- 1 | use pretty_assertions_sorted::assert_eq_sorted; 2 | use surreal_type_generator::{kind, QueryResult}; 3 | 4 | #[test] 5 | fn field_defaults() -> anyhow::Result<()> { 6 | let schema = r#" 7 | DEFINE TABLE user SCHEMAFULL; 8 | DEFINE FIELD created_at ON user TYPE datetime VALUE time::now() READONLY; 9 | DEFINE FIELD foo ON user TYPE array> DEFAULT []; 10 | 11 | DEFINE TABLE foo SCHEMAFULL; 12 | "#; 13 | 14 | let QueryResult { return_types, .. } = 15 | surreal_type_generator::step_3_codegen::query_to_return_type( 16 | r#" 17 | CREATE user; 18 | "#, 19 | schema, 20 | )?; 21 | 22 | assert_eq_sorted!( 23 | return_types, 24 | vec![kind!([kind!({ 25 | id: kind!(Record ["user"]), 26 | created_at: kind!(Datetime), 27 | foo: kind!([kind!(Record ["foo"])]) 28 | })])] 29 | ); 30 | Ok(()) 31 | } 32 | -------------------------------------------------------------------------------- /surreal_type_generator/tests/functions.rs: -------------------------------------------------------------------------------- 1 | use pretty_assertions_sorted; 2 | use surreal_type_generator::QueryResult; 3 | 4 | #[test] 5 | fn custom_function_return_types() -> anyhow::Result<()> { 6 | let schema = r#" 7 | DEFINE FUNCTION fn::foo($bar: number) { 8 | RETURN 5; 9 | };"#; 10 | 11 | let query = r#" 12 | RETURN fn::foo(9); 13 | "#; 14 | 15 | let QueryResult { return_types, .. } = 16 | surreal_type_generator::step_3_codegen::query_to_return_type(query, schema)?; 17 | 18 | pretty_assertions_sorted::assert_eq_sorted!( 19 | return_types, 20 | vec![surreal_type_generator::Kind::Number] 21 | ); 22 | 23 | Ok(()) 24 | } 25 | -------------------------------------------------------------------------------- /surreal_type_generator/tests/insert_statement_tests.rs: -------------------------------------------------------------------------------- 1 | use pretty_assertions_sorted::assert_eq_sorted; 2 | use surreal_type_generator::{kind, var_map, QueryResult}; 3 | 4 | #[test] 5 | fn insert_single_record() -> anyhow::Result<()> { 6 | let query = r#" 7 | INSERT INTO user $user; 8 | "#; 9 | let schema = r#" 10 | DEFINE TABLE user SCHEMAFULL; 11 | DEFINE FIELD name ON user TYPE string; 12 | "#; 13 | 14 | let QueryResult { 15 | return_types, 16 | variables, 17 | .. 18 | } = surreal_type_generator::step_3_codegen::query_to_return_type(query, schema)?; 19 | 20 | let user_vars = kind!({ 21 | id: kind!(Opt (kind!(Record ["user"]))), 22 | name: kind!(String) 23 | }); 24 | 25 | assert_eq_sorted!( 26 | variables, 27 | var_map! { 28 | user: kind!(Either [ 29 | kind!(Arr user_vars.clone()), 30 | user_vars.clone() 31 | ]) 32 | } 33 | ); 34 | 35 | assert_eq_sorted!( 36 | return_types, 37 | vec![kind!([kind!({ 38 | id: kind!(Record ["user"]), 39 | name: kind!(String) 40 | })])] 41 | ); 42 | 43 | Ok(()) 44 | } 45 | 46 | #[test] 47 | fn insert_multiple_records() -> anyhow::Result<()> { 48 | let query = r#" 49 | INSERT INTO user $users; 50 | "#; 51 | let schema = r#" 52 | DEFINE TABLE user SCHEMAFULL; 53 | DEFINE FIELD name ON user TYPE string; 54 | "#; 55 | 56 | let QueryResult { return_types, .. } = 57 | surreal_type_generator::step_3_codegen::query_to_return_type(query, schema)?; 58 | 59 | assert_eq_sorted!( 60 | return_types, 61 | vec![kind!([kind!({ 62 | id: kind!(Record ["user"]), 63 | name: kind!(String) 64 | })])] 65 | ); 66 | 67 | Ok(()) 68 | } 69 | -------------------------------------------------------------------------------- /surreal_type_generator/tests/let_statement_tests.rs: -------------------------------------------------------------------------------- 1 | use pretty_assertions_sorted::assert_eq_sorted; 2 | use surreal_type_generator::{kind, var_map, QueryResult}; 3 | 4 | #[test] 5 | fn let_statement_parameter_inference() -> anyhow::Result<()> { 6 | let query = r#" 7 | LET $id: record = $page.id; 8 | 9 | UPSERT ONLY $id CONTENT $page; 10 | "#; 11 | let schema = r#" 12 | DEFINE TABLE foo SCHEMAFULL; 13 | "#; 14 | 15 | let QueryResult { 16 | return_types, 17 | variables, 18 | .. 19 | } = surreal_type_generator::step_3_codegen::query_to_return_type(query, schema)?; 20 | 21 | let upsert_type = kind!({ 22 | id: kind!(Opt (kind!(Record ["foo"]))), 23 | }); 24 | 25 | assert_eq_sorted!( 26 | variables, 27 | var_map! { 28 | page: kind!(Either [upsert_type.clone(), kind!([upsert_type])]), 29 | } 30 | ); 31 | 32 | assert_eq_sorted!( 33 | return_types, 34 | vec![ 35 | kind!(Null), 36 | kind!({ 37 | id: kind!(Record ["foo"]), 38 | }) 39 | ] 40 | ); 41 | 42 | Ok(()) 43 | } 44 | -------------------------------------------------------------------------------- /surreal_type_generator/tests/literals_tests.rs: -------------------------------------------------------------------------------- 1 | use pretty_assertions_sorted; 2 | use surreal_type_generator::{kind, Kind, Literal, QueryResult}; 3 | use surrealdb::sql::Duration; 4 | 5 | #[test] 6 | fn literal_types() -> anyhow::Result<()> { 7 | let schema = r#" 8 | DEFINE TABLE baz SCHEMAFULL; 9 | DEFINE FIELD foo ON TABLE baz TYPE "A" | 1d | 123 | array<1 | 2> | { foo: string | 123 }; 10 | "#; 11 | 12 | let query = r#" 13 | CREATE ONLY baz CONTENT { 14 | foo: "A" 15 | }"#; 16 | 17 | let QueryResult { return_types, .. } = 18 | surreal_type_generator::step_3_codegen::query_to_return_type(query, schema)?; 19 | 20 | pretty_assertions_sorted::assert_eq_sorted!( 21 | return_types, 22 | vec![kind!({ 23 | id: kind!(Record ["baz"]), 24 | foo: kind!(Either [ 25 | Kind::Literal(Literal::String("A".into())), 26 | Kind::Literal(Literal::Duration(Duration::new(86400, 0))), 27 | Kind::Literal(Literal::Number(123.into())), 28 | kind!([kind!(Either [ 29 | Kind::Literal(Literal::Number(1.into())), 30 | Kind::Literal(Literal::Number(2.into())) 31 | ])]), 32 | kind!({ 33 | foo: kind!(Either [ 34 | kind!(String), 35 | Kind::Literal(Literal::Number(123.into())) 36 | ]) 37 | }) 38 | ]) 39 | })] 40 | ); 41 | 42 | Ok(()) 43 | } 44 | 45 | #[test] 46 | fn literal_values_in_query() { 47 | let schema = r#" 48 | DEFINE TABLE baz SCHEMAFULL; 49 | "#; 50 | let query = r#" 51 | SELECT [] as foo, [1, 2, 3] as num_list FROM baz; 52 | "#; 53 | 54 | let QueryResult { return_types, .. } = 55 | surreal_type_generator::step_3_codegen::query_to_return_type(query, schema).unwrap(); 56 | 57 | pretty_assertions_sorted::assert_eq_sorted!( 58 | return_types, 59 | vec![kind!([kind!({ 60 | foo: kind!([kind!(Null)]), 61 | num_list: kind!([kind!(Number)]) 62 | })])] 63 | ); 64 | } 65 | 66 | #[test] 67 | fn either_with_literals() { 68 | let schema = r#" 69 | DEFINE TABLE baz SCHEMAFULL; 70 | 71 | DEFINE FIELD foo ON TABLE baz TYPE int | "a" | "b"; 72 | "#; 73 | 74 | let query = r#" 75 | SELECT foo FROM baz; 76 | "#; 77 | 78 | let QueryResult { return_types, .. } = 79 | surreal_type_generator::step_3_codegen::query_to_return_type(query, schema).unwrap(); 80 | 81 | pretty_assertions_sorted::assert_eq_sorted!( 82 | return_types, 83 | vec![kind!([kind!({ 84 | foo: kind!(Either [ 85 | kind!(Int), 86 | Kind::Literal(Literal::String("a".into())), 87 | Kind::Literal(Literal::String("b".into())) 88 | ]) 89 | })])] 90 | ); 91 | } 92 | 93 | #[test] 94 | fn tuple_with_literals() { 95 | let schema = r#" 96 | DEFINE TABLE baz SCHEMAFULL; 97 | 98 | DEFINE FIELD foo ON TABLE baz TYPE [int | "a" | "b"]; 99 | "#; 100 | 101 | let query = r#" 102 | SELECT foo FROM baz; 103 | "#; 104 | 105 | let QueryResult { return_types, .. } = 106 | surreal_type_generator::step_3_codegen::query_to_return_type(query, schema).unwrap(); 107 | 108 | pretty_assertions_sorted::assert_eq_sorted!( 109 | return_types, 110 | vec![kind!([kind!({ 111 | foo: Kind::Literal(Literal::Array(vec![ 112 | kind!(Int), 113 | Kind::Literal(Literal::String("a".into())), 114 | Kind::Literal(Literal::String("b".into())) 115 | ])) 116 | })])] 117 | ); 118 | } 119 | -------------------------------------------------------------------------------- /surreal_type_generator/tests/object.rs: -------------------------------------------------------------------------------- 1 | use pretty_assertions_sorted::assert_eq_sorted; 2 | use surreal_type_generator::{kind, QueryResult}; 3 | 4 | #[test] 5 | fn can_interpret_object() -> Result<(), anyhow::Error> { 6 | let schema = r#" 7 | DEFINE TABLE foo SCHEMAFULL; 8 | "#; 9 | 10 | let query = r#" 11 | RETURN { 12 | foo: { 13 | bar: 1, 14 | baz: 2, 15 | }, 16 | qux: 3, 17 | }; 18 | "#; 19 | 20 | let QueryResult { return_types, .. } = 21 | surreal_type_generator::step_3_codegen::query_to_return_type(query, schema)?; 22 | 23 | assert_eq_sorted!( 24 | return_types, 25 | vec![kind!({ 26 | foo: kind!({ 27 | bar: kind!(Number), 28 | baz: kind!(Number) 29 | }), 30 | qux: kind!(Number) 31 | })] 32 | ); 33 | 34 | Ok(()) 35 | } 36 | -------------------------------------------------------------------------------- /surreal_type_generator/tests/precomputed_views.rs: -------------------------------------------------------------------------------- 1 | use pretty_assertions_sorted::assert_eq_sorted; 2 | use surreal_type_generator::{kind, QueryResult}; 3 | 4 | #[test] 5 | fn precomputed_views() -> anyhow::Result<()> { 6 | let schema = r#" 7 | DEFINE TABLE foo SCHEMAFULL; 8 | DEFINE FIELD num ON TABLE foo TYPE number; 9 | 10 | DEFINE TABLE baz AS SELECT 11 | * 12 | FROM foo; 13 | "#; 14 | 15 | let query = r#"SELECT * FROM baz;"#; 16 | 17 | let QueryResult { return_types, .. } = 18 | surreal_type_generator::step_3_codegen::query_to_return_type(query, schema)?; 19 | 20 | assert_eq_sorted!( 21 | return_types, 22 | vec![kind!([kind!({ 23 | id: kind!(Record ["baz"]), 24 | num: kind!(Number) 25 | })])] 26 | ); 27 | 28 | Ok(()) 29 | } 30 | 31 | #[test] 32 | fn precomputed_views_with_new_fields() -> anyhow::Result<()> { 33 | let schema = r#" 34 | DEFINE TABLE foo SCHEMAFULL; 35 | DEFINE FIELD num ON TABLE foo TYPE number; 36 | DEFINE FIELD beep ON TABLE foo TYPE number; 37 | 38 | DEFINE TABLE baz AS SELECT 39 | *, 40 | 5 as five 41 | FROM foo; 42 | "#; 43 | 44 | let query = r#" 45 | SELECT num, five FROM baz; 46 | SELECT * FROM baz; 47 | "#; 48 | 49 | let QueryResult { return_types, .. } = 50 | surreal_type_generator::step_3_codegen::query_to_return_type(query, schema)?; 51 | 52 | assert_eq_sorted!( 53 | return_types, 54 | vec![ 55 | kind!([kind!({ 56 | num: kind!(Number), 57 | five: kind!(Number) 58 | })]), 59 | kind!([kind!({ 60 | id: kind!(Record ["baz"]), 61 | num: kind!(Number), 62 | five: kind!(Number), 63 | beep: kind!(Number) 64 | })]) 65 | ] 66 | ); 67 | 68 | Ok(()) 69 | } 70 | -------------------------------------------------------------------------------- /surreal_type_generator/tests/return_and_expressions.rs: -------------------------------------------------------------------------------- 1 | use pretty_assertions_sorted::assert_eq_sorted; 2 | use surreal_type_generator::{Kind, QueryResult}; 3 | 4 | #[test] 5 | fn return_and_expressions() -> anyhow::Result<()> { 6 | let query = r#" 7 | -- constant expressions 8 | RETURN "bar"; 9 | RETURN math::e; 10 | 11 | 12 | -- unary expressions 13 | RETURN -1; 14 | RETURN !true; 15 | 16 | -- binary comparison expressions 17 | RETURN true && false; 18 | RETURN true == false; 19 | RETURN 1 = 1; 20 | RETURN 1 != 1; 21 | RETURN 1 IS NOT 1; 22 | RETURN 1 < 3; 23 | RETURN 1 > 3; 24 | RETURN 1 is 1; 25 | RETURN 1 <= 1; 26 | RETURN 1 >= 1; 27 | RETURN 1 && 1; 28 | RETURN 1 || 1; 29 | 30 | "#; 31 | let schema = r#" 32 | DEFINE TABLE placeholder SCHEMAFULL; 33 | "#; 34 | 35 | let QueryResult { return_types, .. } = 36 | surreal_type_generator::step_3_codegen::query_to_return_type(query, schema)?; 37 | 38 | assert_eq_sorted!( 39 | return_types, 40 | vec![ 41 | Kind::String, 42 | Kind::Number, 43 | Kind::Number, 44 | Kind::Bool, 45 | Kind::Bool, 46 | Kind::Bool, 47 | Kind::Bool, 48 | Kind::Bool, 49 | Kind::Bool, 50 | Kind::Bool, 51 | Kind::Bool, 52 | Kind::Bool, 53 | Kind::Bool, 54 | Kind::Bool, 55 | Kind::Bool, 56 | Kind::Number 57 | ] 58 | ); 59 | 60 | Ok(()) 61 | } 62 | 63 | /* 64 | -- arithmetic expressions 65 | RETURN 1 + 1; 66 | RETURN 1 - 1; 67 | RETURN 1 * 1; 68 | RETURN 1 ** 1; 69 | RETURN 1 / 1; 70 | RETURN 1 % 2; 71 | */ 72 | 73 | /* 74 | -- shortcircuiting expressions 75 | RETURN 1 ?: 1; 76 | RETURN 1 ?: null; 77 | RETURN null ?? 1; 78 | RETURN 1 ?? 1; 79 | */ 80 | 81 | // -- ## TODOS ## 82 | // -- !~ 83 | // -- ~ 84 | // -- ?~ 85 | // -- *~ 86 | // -- CONTAINS 87 | // -- CONTAINSNOT 88 | // -- CONTAINSALL 89 | // -- CONTAINSANY 90 | // -- CONTAINSNONE 91 | // -- INSIDE 92 | // -- NOTINSIDE 93 | // -- ALLINSIDE 94 | // -- ANYINSIDE 95 | // -- NONEINSIDE 96 | // -- OUTSIDE 97 | // -- INTERSECTS 98 | // -- @@ (MATCHES) 99 | // -- <|4|> #KNN 100 | -------------------------------------------------------------------------------- /surreal_type_generator/tests/schema_complex_types.rs: -------------------------------------------------------------------------------- 1 | use pretty_assertions_sorted::assert_eq_sorted; 2 | use surreal_type_generator::{kind, QueryResult}; 3 | 4 | #[test] 5 | fn nested_schema_object() -> anyhow::Result<()> { 6 | let query = r#" 7 | SELECT 8 | bar 9 | FROM foo; 10 | "#; 11 | 12 | let schema = r#" 13 | DEFINE TABLE foo SCHEMAFULL; 14 | DEFINE FIELD bar ON foo TYPE array<{ 15 | baz: string 16 | }>; 17 | // DEFINE FIELD bar.*.baz ON foo TYPE string; 18 | "#; 19 | 20 | let QueryResult { return_types, .. } = 21 | surreal_type_generator::step_3_codegen::query_to_return_type(query, schema)?; 22 | 23 | assert_eq_sorted!( 24 | return_types, 25 | vec![kind!([kind!({ 26 | bar: kind!([ 27 | kind!({ 28 | baz: kind!(String) 29 | }) 30 | ]) 31 | })])] 32 | ); 33 | 34 | Ok(()) 35 | } 36 | 37 | #[test] 38 | fn schema_flexible_object() -> anyhow::Result<()> { 39 | let query = r#" 40 | SELECT 41 | bar 42 | FROM foo; 43 | "#; 44 | 45 | let schema = r#" 46 | DEFINE TABLE foo SCHEMAFULL; 47 | DEFINE FIELD bar ON foo FLEXIBLE TYPE object; 48 | "#; 49 | 50 | let QueryResult { return_types, .. } = 51 | surreal_type_generator::step_3_codegen::query_to_return_type(query, schema)?; 52 | 53 | assert_eq_sorted!( 54 | return_types, 55 | vec![kind!([kind!({ 56 | bar: kind!(Object) 57 | })])] 58 | ); 59 | 60 | Ok(()) 61 | } 62 | -------------------------------------------------------------------------------- /surreal_type_generator/tests/select_grouping.rs: -------------------------------------------------------------------------------- 1 | use pretty_assertions_sorted::assert_eq_sorted; 2 | use surreal_type_generator::{kind, QueryResult}; 3 | 4 | #[test] 5 | fn select_group_by() -> anyhow::Result<()> { 6 | let query = r#" 7 | SELECT 8 | name, 9 | 5 as baz 10 | FROM 11 | user 12 | GROUP BY 13 | name 14 | "#; 15 | let schema = r#" 16 | DEFINE TABLE user SCHEMAFULL; 17 | DEFINE FIELD name ON user TYPE string; 18 | DEFINE FIELD age ON user TYPE int; 19 | "#; 20 | 21 | let QueryResult { return_types, .. } = 22 | surreal_type_generator::step_3_codegen::query_to_return_type(query, schema)?; 23 | 24 | assert_eq_sorted!( 25 | return_types, 26 | vec![kind!([kind!({ 27 | name: kind!(String), 28 | baz: kind!(Number) 29 | })]),] 30 | ); 31 | 32 | Ok(()) 33 | } 34 | 35 | #[test] 36 | fn select_group_by_aggregate() -> anyhow::Result<()> { 37 | let query = r#" 38 | SELECT 39 | name, 40 | count() as total 41 | FROM 42 | user 43 | GROUP BY 44 | name 45 | "#; 46 | let schema = r#" 47 | DEFINE TABLE user SCHEMAFULL; 48 | DEFINE FIELD name ON user TYPE string; 49 | "#; 50 | 51 | let QueryResult { return_types, .. } = 52 | surreal_type_generator::step_3_codegen::query_to_return_type(query, schema)?; 53 | 54 | assert_eq_sorted!( 55 | return_types, 56 | vec![kind!([kind!({ 57 | name: kind!(String), 58 | total: kind!(Number) 59 | })])] 60 | ); 61 | 62 | Ok(()) 63 | } 64 | 65 | #[test] 66 | fn select_group_by_group_all() -> anyhow::Result<()> { 67 | let query = r#" 68 | SELECT 69 | count() as total 70 | FROM 71 | user 72 | GROUP ALL 73 | "#; 74 | let schema = r#" 75 | DEFINE TABLE user SCHEMAFULL; 76 | DEFINE FIELD name ON user TYPE string; 77 | "#; 78 | 79 | let QueryResult { return_types, .. } = 80 | surreal_type_generator::step_3_codegen::query_to_return_type(query, schema)?; 81 | 82 | assert_eq_sorted!( 83 | return_types, 84 | vec![kind!([kind!({ 85 | total: kind!(Number) 86 | })])] 87 | ); 88 | 89 | Ok(()) 90 | } 91 | -------------------------------------------------------------------------------- /surreal_type_generator/tests/select_statement_tests.rs: -------------------------------------------------------------------------------- 1 | use pretty_assertions_sorted::assert_eq_sorted; 2 | use surreal_type_generator::{kind, var_map, QueryResult}; 3 | 4 | #[test] 5 | fn query_specific_value() -> anyhow::Result<()> { 6 | let query = r#" 7 | SELECT VALUE name FROM ONLY user; 8 | "#; 9 | let schema = r#" 10 | DEFINE TABLE user SCHEMAFULL; 11 | DEFINE FIELD name ON user TYPE string; 12 | "#; 13 | 14 | let QueryResult { return_types, .. } = 15 | surreal_type_generator::step_3_codegen::query_to_return_type(query, schema)?; 16 | 17 | assert_eq_sorted!(return_types, vec![kind!(String)]); 18 | 19 | Ok(()) 20 | } 21 | 22 | #[test] 23 | fn validate_return_types() -> anyhow::Result<()> { 24 | let query = r#" 25 | SELECT 26 | name, 27 | age, 28 | bool, 29 | datetime, 30 | duration, 31 | decimal, 32 | uuid, 33 | number 34 | FROM 35 | user; 36 | "#; 37 | let schema = r#" 38 | DEFINE TABLE user SCHEMAFULL; 39 | DEFINE FIELD name ON user TYPE string; 40 | DEFINE FIELD age ON user TYPE int; 41 | DEFINE FIELD bool ON user TYPE bool; 42 | DEFINE FIELD datetime ON user TYPE datetime; 43 | DEFINE FIELD duration ON user TYPE duration; 44 | DEFINE FIELD decimal ON user TYPE decimal; 45 | DEFINE FIELD uuid ON user TYPE uuid; 46 | DEFINE FIELD number ON user TYPE number; 47 | "#; 48 | 49 | let QueryResult { return_types, .. } = 50 | surreal_type_generator::step_3_codegen::query_to_return_type(query, schema)?; 51 | 52 | assert_eq_sorted!( 53 | return_types, 54 | vec![kind!([kind!({ 55 | name: kind!(String), 56 | age: kind!(Int), 57 | bool: kind!(Bool), 58 | datetime: kind!(Datetime), 59 | duration: kind!(Duration), 60 | decimal: kind!(Decimal), 61 | uuid: kind!(Uuid), 62 | number: kind!(Number) 63 | })])] 64 | ); 65 | 66 | Ok(()) 67 | } 68 | 69 | #[test] 70 | fn validate_return_types_with_only_value() -> anyhow::Result<()> { 71 | let query = r#" 72 | SELECT 73 | name 74 | FROM ONLY user; 75 | "#; 76 | let schema = r#" 77 | DEFINE TABLE user SCHEMAFULL; 78 | DEFINE FIELD name ON user TYPE string; 79 | "#; 80 | 81 | let QueryResult { return_types, .. } = 82 | surreal_type_generator::step_3_codegen::query_to_return_type(query, schema)?; 83 | 84 | assert_eq_sorted!( 85 | return_types, 86 | vec![kind!({ 87 | name: kind!(String) 88 | })] 89 | ); 90 | 91 | Ok(()) 92 | } 93 | 94 | #[test] 95 | fn validate_return_types_with_parameter_record() -> anyhow::Result<()> { 96 | let query = r#" 97 | > $user; 98 | 99 | SELECT name FROM $user 100 | "#; 101 | let schema = r#" 102 | DEFINE TABLE user SCHEMAFULL; 103 | DEFINE FIELD name ON user TYPE string; 104 | "#; 105 | 106 | let QueryResult { 107 | return_types, 108 | variables, 109 | .. 110 | } = surreal_type_generator::step_3_codegen::query_to_return_type(query, schema)?; 111 | 112 | assert_eq_sorted!( 113 | variables, 114 | var_map! { 115 | user: kind!(Record ["user"]) 116 | } 117 | ); 118 | 119 | assert_eq_sorted!( 120 | return_types, 121 | vec![kind!([kind!({ 122 | name: kind!(String) 123 | })])] 124 | ); 125 | 126 | Ok(()) 127 | } 128 | 129 | #[test] 130 | fn validate_nested_record_return_type() -> anyhow::Result<()> { 131 | let query = r#" 132 | SELECT 133 | xyz.abc, 134 | xyz.user.xyz 135 | FROM 136 | user; 137 | "#; 138 | let schema = r#" 139 | DEFINE TABLE user SCHEMAFULL; 140 | DEFINE FIELD xyz ON user TYPE record; 141 | 142 | DEFINE TABLE xyz SCHEMAFULL; 143 | DEFINE FIELD abc ON xyz TYPE string; 144 | DEFINE FIELD user ON xyz TYPE record; 145 | "#; 146 | 147 | let QueryResult { return_types, .. } = 148 | surreal_type_generator::step_3_codegen::query_to_return_type(query, schema)?; 149 | 150 | assert_eq_sorted!( 151 | return_types, 152 | vec![kind!([kind!({ 153 | xyz: kind!({ 154 | abc: kind!(String), 155 | user: kind!({ 156 | xyz: kind!(Record ["xyz"]) 157 | }) 158 | }) 159 | })])], 160 | ); 161 | 162 | Ok(()) 163 | } 164 | 165 | #[test] 166 | fn query_with_alias_field() -> anyhow::Result<()> { 167 | let query = r#" 168 | SELECT 169 | name as foo 170 | FROM ONLY user; 171 | "#; 172 | let schema = r#" 173 | DEFINE TABLE user SCHEMAFULL; 174 | DEFINE FIELD name ON user TYPE string; 175 | "#; 176 | 177 | let QueryResult { return_types, .. } = 178 | surreal_type_generator::step_3_codegen::query_to_return_type(query, schema)?; 179 | 180 | assert_eq_sorted!( 181 | return_types, 182 | vec![kind!({ 183 | foo: kind!(String) 184 | })] 185 | ); 186 | 187 | Ok(()) 188 | } 189 | 190 | #[test] 191 | fn query_with_alias_field_with_table() -> anyhow::Result<()> { 192 | let query = r#" 193 | SELECT 194 | org.name as foo 195 | FROM ONLY user; 196 | "#; 197 | let schema = r#" 198 | DEFINE TABLE user SCHEMAFULL; 199 | DEFINE FIELD org ON user TYPE record; 200 | 201 | DEFINE TABLE org SCHEMAFULL; 202 | DEFINE FIELD name ON org TYPE string; 203 | "#; 204 | 205 | let QueryResult { return_types, .. } = 206 | surreal_type_generator::step_3_codegen::query_to_return_type(query, schema)?; 207 | 208 | assert_eq_sorted!( 209 | return_types, 210 | vec![kind!({ 211 | foo: kind!(String) 212 | })] 213 | ); 214 | 215 | Ok(()) 216 | } 217 | 218 | #[test] 219 | fn query_field_with_all() -> anyhow::Result<()> { 220 | let query = r#" 221 | SELECT * FROM ONLY user; 222 | "#; 223 | let schema = r#" 224 | DEFINE TABLE user SCHEMAFULL; 225 | DEFINE FIELD name ON user TYPE string; 226 | "#; 227 | 228 | let QueryResult { return_types, .. } = 229 | surreal_type_generator::step_3_codegen::query_to_return_type(query, schema)?; 230 | 231 | assert_eq_sorted!( 232 | return_types, 233 | vec![kind!({ 234 | name: kind!(String), 235 | id: kind!(Record ["user"]) 236 | })] 237 | ); 238 | 239 | Ok(()) 240 | } 241 | 242 | #[test] 243 | fn query_with_optional_fields() -> anyhow::Result<()> { 244 | let query = r#" 245 | SELECT 246 | name, 247 | num, 248 | bool, 249 | datetime, 250 | duration, 251 | decimal, 252 | xyz.abc, 253 | xyz.abc2 254 | FROM ONLY user; 255 | "#; 256 | let schema = r#" 257 | DEFINE TABLE user SCHEMAFULL; 258 | DEFINE FIELD name ON user TYPE option; 259 | DEFINE FIELD num ON user TYPE option; 260 | DEFINE FIELD bool ON user TYPE option; 261 | DEFINE FIELD datetime ON user TYPE option; 262 | DEFINE FIELD duration ON user TYPE option; 263 | DEFINE FIELD decimal ON user TYPE option; 264 | DEFINE FIELD xyz ON user TYPE option>; 265 | 266 | DEFINE TABLE xyz SCHEMAFULL; 267 | DEFINE FIELD abc ON xyz TYPE option; 268 | DEFINE FIELD abc2 ON xyz TYPE option; 269 | "#; 270 | 271 | let QueryResult { return_types, .. } = 272 | surreal_type_generator::step_3_codegen::query_to_return_type(query, schema)?; 273 | 274 | assert_eq_sorted!( 275 | return_types, 276 | vec![kind!({ 277 | name: kind!(Opt(kind!(String))), 278 | num: kind!(Opt(kind!(Int))), 279 | bool: kind!(Opt(kind!(Bool))), 280 | datetime: kind!(Opt(kind!(Datetime))), 281 | duration: kind!(Opt(kind!(Duration))), 282 | decimal: kind!(Opt(kind!(Decimal))), 283 | xyz: kind!(Opt(kind!({ 284 | abc: kind!(Opt(kind!(String))), 285 | abc2: kind!(Opt(kind!(String))) 286 | }))) 287 | })] 288 | ); 289 | 290 | Ok(()) 291 | } 292 | 293 | #[test] 294 | fn query_with_nested_array_string_field() -> anyhow::Result<()> { 295 | let query = r#" 296 | SELECT 297 | tags.* 298 | FROM 299 | post; 300 | "#; 301 | let schema = r#" 302 | DEFINE TABLE post SCHEMAFULL; 303 | DEFINE FIELD tags ON post TYPE array; 304 | "#; 305 | 306 | let QueryResult { return_types, .. } = 307 | surreal_type_generator::step_3_codegen::query_to_return_type(query, schema)?; 308 | 309 | assert_eq_sorted!( 310 | return_types, 311 | vec![kind!([kind!({ 312 | tags: kind!([kind!(String)]) 313 | })])] 314 | ); 315 | 316 | Ok(()) 317 | } 318 | 319 | #[test] 320 | fn query_with_array_field() -> anyhow::Result<()> { 321 | let query = r#" 322 | SELECT 323 | tags, 324 | xyz_list as xyzs 325 | FROM 326 | post; 327 | "#; 328 | let schema = r#" 329 | DEFINE TABLE xyz SCHEMAFULL; 330 | DEFINE TABLE post SCHEMAFULL; 331 | DEFINE FIELD tags ON post TYPE array; 332 | DEFINE FIELD xyz_list ON post TYPE array> DEFAULT []; 333 | "#; 334 | 335 | let QueryResult { return_types, .. } = 336 | surreal_type_generator::step_3_codegen::query_to_return_type(query, schema)?; 337 | 338 | assert_eq_sorted!( 339 | return_types, 340 | vec![kind!([kind!({ 341 | tags: kind!([kind!(String)]), 342 | xyzs: kind!([kind!(Record ["xyz"])]) 343 | })])] 344 | ); 345 | 346 | Ok(()) 347 | } 348 | 349 | #[test] 350 | fn select_specific_record() -> anyhow::Result<()> { 351 | let query = r#" 352 | SELECT 353 | name 354 | FROM 355 | user:john 356 | "#; 357 | let schema = r#" 358 | DEFINE TABLE user SCHEMAFULL; 359 | DEFINE FIELD name ON user TYPE string; 360 | "#; 361 | 362 | let QueryResult { return_types, .. } = 363 | surreal_type_generator::step_3_codegen::query_to_return_type(query, schema)?; 364 | 365 | assert_eq_sorted!( 366 | return_types, 367 | vec![kind!([kind!({ 368 | name: kind!(String) 369 | })])] 370 | ); 371 | 372 | Ok(()) 373 | } 374 | 375 | #[test] 376 | fn query_with_object_field() -> anyhow::Result<()> { 377 | let query = r#" 378 | SELECT 379 | xyz 380 | FROM 381 | user; 382 | "#; 383 | let schema = r#" 384 | DEFINE TABLE user SCHEMAFULL; 385 | DEFINE FIELD xyz ON user TYPE object; 386 | "#; 387 | 388 | let QueryResult { return_types, .. } = 389 | surreal_type_generator::step_3_codegen::query_to_return_type(query, schema)?; 390 | 391 | assert_eq_sorted!( 392 | return_types, 393 | vec![kind!([kind!({ 394 | xyz: kind!(Object) 395 | })])] 396 | ); 397 | 398 | Ok(()) 399 | } 400 | 401 | #[test] 402 | fn query_with_nested_object_all_field() -> anyhow::Result<()> { 403 | let query = r#" 404 | SELECT 405 | xyz.* 406 | FROM 407 | user; 408 | "#; 409 | let schema = r#" 410 | DEFINE TABLE user SCHEMAFULL; 411 | DEFINE FIELD xyz ON user TYPE record; 412 | 413 | DEFINE TABLE xyz SCHEMAFULL; 414 | DEFINE FIELD abc ON xyz TYPE string; 415 | DEFINE FIELD num ON xyz TYPE int; 416 | "#; 417 | 418 | let QueryResult { return_types, .. } = 419 | surreal_type_generator::step_3_codegen::query_to_return_type(query, schema)?; 420 | 421 | assert_eq_sorted!( 422 | return_types, 423 | vec![kind!([kind!({ 424 | xyz: kind!({ 425 | id: kind!(Record ["xyz"]), 426 | abc: kind!(String), 427 | num: kind!(Int) 428 | }) 429 | })])] 430 | ); 431 | 432 | Ok(()) 433 | } 434 | 435 | #[test] 436 | fn query_with_nested_optional_object() -> anyhow::Result<()> { 437 | let query = r#" 438 | SELECT 439 | id, 440 | xyz.foo, 441 | xyz.abc 442 | FROM 443 | user; 444 | "#; 445 | let schema = r#" 446 | DEFINE TABLE user SCHEMAFULL; 447 | DEFINE FIELD xyz ON user TYPE option<{ 448 | foo: option, 449 | abc: option 450 | }>; 451 | "#; 452 | 453 | let QueryResult { return_types, .. } = 454 | surreal_type_generator::step_3_codegen::query_to_return_type(query, schema)?; 455 | 456 | assert_eq_sorted!( 457 | return_types, 458 | vec![kind!([kind!({ 459 | id: kind!(Record ["user"]), 460 | xyz: kind!(Opt(kind!({ 461 | foo: kind!(Opt(kind!(String))), 462 | abc: kind!(Opt(kind!(String))) 463 | }))) 464 | })])] 465 | ); 466 | 467 | Ok(()) 468 | } 469 | 470 | #[test] 471 | fn query_with_nested_optional_all_field() -> anyhow::Result<()> { 472 | let query = r#" 473 | SELECT 474 | id, 475 | xyz.* as bazza 476 | FROM 477 | user; 478 | "#; 479 | let schema = r#" 480 | DEFINE TABLE user SCHEMAFULL; 481 | DEFINE FIELD xyz ON user TYPE option<{ 482 | foo: option, 483 | abc: option, 484 | num: int 485 | }>; 486 | // DEFINE FIELD xyz.foo ON user TYPE option; 487 | // DEFINE FIELD xyz.abc ON user TYPE option; 488 | // DEFINE FIELD xyz.num ON user TYPE int; 489 | "#; 490 | 491 | let QueryResult { return_types, .. } = 492 | surreal_type_generator::step_3_codegen::query_to_return_type(query, schema)?; 493 | 494 | assert_eq_sorted!( 495 | return_types, 496 | vec![kind!([kind!({ 497 | id: kind!(Record ["user"]), 498 | bazza: kind!(Opt(kind!({ 499 | foo: kind!(Opt(kind!(String))), 500 | abc: kind!(Opt(kind!(String))), 501 | num: kind!(Int) 502 | }))) 503 | })])] 504 | ); 505 | 506 | Ok(()) 507 | } 508 | 509 | #[test] 510 | fn query_select_result_with_index() -> anyhow::Result<()> { 511 | let query = r#" 512 | (SELECT VALUE baz FROM user)[0]; 513 | "#; 514 | let schema = r#" 515 | DEFINE TABLE user SCHEMAFULL; 516 | DEFINE FIELD baz ON user TYPE string; 517 | "#; 518 | 519 | let QueryResult { return_types, .. } = 520 | surreal_type_generator::step_3_codegen::query_to_return_type(query, schema)?; 521 | 522 | assert_eq_sorted!(return_types, vec![kind!(Opt(kind!(String)))]); 523 | 524 | Ok(()) 525 | } 526 | 527 | #[test] 528 | fn query_select_with_subquery_index() -> anyhow::Result<()> { 529 | let query = r#" 530 | SELECT 531 | foo, 532 | (SELECT VALUE baz FROM user)[0] as bar 533 | FROM 534 | user; 535 | "#; 536 | let schema = r#" 537 | DEFINE TABLE user SCHEMAFULL; 538 | DEFINE FIELD baz ON user TYPE string; 539 | DEFINE FIELD foo ON user TYPE string; 540 | "#; 541 | 542 | let QueryResult { return_types, .. } = 543 | surreal_type_generator::step_3_codegen::query_to_return_type(query, schema)?; 544 | 545 | assert_eq_sorted!( 546 | return_types, 547 | vec![kind!([kind!({ 548 | foo: kind!(String), 549 | bar: kind!(Opt(kind!(String))) 550 | })])] 551 | ); 552 | 553 | Ok(()) 554 | } 555 | 556 | // #[test] 557 | // fn select_from_parent_field() -> anyhow::Result<()> { 558 | // let query = r#" 559 | // SELECT 560 | // name, 561 | // (SELECT name FROM ONLY $parent.best_friend) as best_friend 562 | // FROM ONLY user; 563 | // "#; 564 | // let schema = r#" 565 | // DEFINE TABLE user SCHEMAFULL; 566 | // DEFINE FIELD name ON user TYPE string; 567 | // DEFINE FIELD best_friend ON user TYPE record; 568 | // "#; 569 | 570 | // let QueryResult { return_types, .. } = 571 | // surreal_type_generator::step_3_codegen::query_to_return_type(query, schema)?; 572 | 573 | // assert_eq_sorted!( 574 | // return_types, 575 | // vec![Kind::Array(Box::new(Kind::Object( 576 | // [ 577 | // ("name".into(), Kind::String), 578 | // ( 579 | // "best_friend".into(), 580 | // Kind::Option(Box::new(Kind::Object( 581 | // [("name".into(), Kind::String)].into() 582 | // ))) 583 | // ) 584 | // ] 585 | // .into() 586 | // )))] 587 | // ); 588 | 589 | // Ok(()) 590 | // } 591 | -------------------------------------------------------------------------------- /surreal_type_generator/tests/subquery_tests.rs: -------------------------------------------------------------------------------- 1 | use pretty_assertions_sorted::assert_eq_sorted; 2 | use surreal_type_generator::{kind, QueryResult}; 3 | 4 | #[test] 5 | fn query_with_subquery() -> anyhow::Result<()> { 6 | let query = r#" 7 | SELECT 8 | name, 9 | (SELECT name FROM user) AS subquery, 10 | (DELETE user), 11 | (UPDATE user SET name = "John" RETURN NONE) 12 | FROM ONLY user; 13 | "#; 14 | let schema = r#" 15 | DEFINE TABLE user SCHEMAFULL; 16 | DEFINE FIELD name ON user TYPE string; 17 | "#; 18 | 19 | let QueryResult { return_types, .. } = 20 | surreal_type_generator::step_3_codegen::query_to_return_type(query, schema)?; 21 | 22 | assert_eq_sorted!( 23 | return_types, 24 | vec![kind!({ 25 | "name": kind!(String), 26 | "subquery": kind!([kind!({ 27 | "name": kind!(String) 28 | })]), 29 | "(DELETE user)": kind!([kind!(Null)]), 30 | "(UPDATE user SET name = \'John\' RETURN NONE)": kind!([kind!(Null)]) 31 | })] 32 | ); 33 | 34 | Ok(()) 35 | } 36 | -------------------------------------------------------------------------------- /surreal_type_generator/tests/transaction.rs: -------------------------------------------------------------------------------- 1 | use pretty_assertions_sorted::assert_eq_sorted; 2 | use surreal_type_generator::{step_3_codegen::QueryResult, Kind}; 3 | 4 | #[test] 5 | fn transaction_return_type() -> anyhow::Result<()> { 6 | let query = r#" 7 | BEGIN; 8 | 9 | RETURN true; 10 | 11 | COMMIT; 12 | "#; 13 | 14 | let schema = r#" 15 | DEFINE TABLE foo SCHEMAFULL; 16 | "#; 17 | 18 | let QueryResult { return_types, .. } = 19 | surreal_type_generator::step_3_codegen::query_to_return_type(query, schema)?; 20 | 21 | assert_eq_sorted!(return_types, vec![Kind::Bool]); 22 | 23 | Ok(()) 24 | } 25 | -------------------------------------------------------------------------------- /surreal_type_generator/tests/typescript_generation.rs: -------------------------------------------------------------------------------- 1 | // use pretty_assertions_sorted::assert_eq_sorted; 2 | 3 | // #[test] 4 | // fn can_generate_typescript_for_select_query_with_value() -> anyhow::Result<()> { 5 | // let query = r#" 6 | // SELECT VALUE name FROM user 7 | // "#; 8 | // let schema = r#" 9 | // DEFINE TABLE user SCHEMAFULL; 10 | // DEFINE FIELD name ON user TYPE string; 11 | // "#; 12 | // let expected_str = r#" 13 | // export const XQuery = "SELECT VALUE name FROM user;" 14 | // export type XQueryResult = [string[],] 15 | // "# 16 | // .trim(); 17 | // let output = 18 | // surreal_type_generator::step_3_outputs::generate_typescript_file("x.surql", query_str, schema_str)? 19 | // .types; 20 | 21 | // assert_eq_sorted!(output, expected_str); 22 | 23 | // Ok(()) 24 | // } 25 | 26 | // #[test] 27 | // fn can_generate_typescript_for_select_query_with_multiple_fields() -> anyhow::Result<()> { 28 | // let query = r#" 29 | // SELECT name, age FROM user 30 | // "#; 31 | // let schema = r#" 32 | // DEFINE TABLE user SCHEMAFULL; 33 | // DEFINE FIELD name ON user TYPE string; 34 | // DEFINE FIELD age ON user TYPE int; 35 | // "#; 36 | // let expected_str = r#" 37 | // export const XQuery = "SELECT name, age FROM user;" 38 | // export type XQueryResult = [{age:number,name:string,}[],] 39 | // "# 40 | // .trim(); 41 | // let output = 42 | // surreal_type_generator::step_3_outputs::generate_typescript_file("x.surql", query_str, schema_str)? 43 | // .types; 44 | 45 | // assert_eq_sorted!(output, expected_str); 46 | 47 | // Ok(()) 48 | // } 49 | 50 | // #[test] 51 | // fn select_query_with_parameter_record() -> anyhow::Result<()> { 52 | // let query = r#" 53 | // > $user; 54 | 55 | // SELECT name FROM $user 56 | // "#; 57 | // let schema = r#" 58 | // DEFINE TABLE user SCHEMAFULL; 59 | // DEFINE FIELD name ON user TYPE string; 60 | // "#; 61 | // let expected_str = r#" 62 | // export const XQuery = "SELECT name FROM $user;" 63 | // export type XQueryResult = [{name:string,}[],] 64 | // "# 65 | // .trim(); 66 | // let output = 67 | // surreal_type_generator::step_3_outputs::generate_typescript_file("x.surql", query_str, schema_str)? 68 | // .types; 69 | 70 | // assert_eq_sorted!(output, expected_str); 71 | 72 | // Ok(()) 73 | // } 74 | 75 | // #[test] 76 | // fn select_query_with_nested_fields_and_weird_idioms() -> anyhow::Result<()> { 77 | // let query = r#" 78 | // SELECT 79 | // foo as bar, 80 | // baz, 81 | // xyz.abc 82 | // FROM 83 | // user; 84 | // "#; 85 | // let schema = r#" 86 | // DEFINE TABLE user SCHEMAFULL; 87 | // DEFINE FIELD foo ON user TYPE string; 88 | // DEFINE FIELD baz ON user TYPE int; 89 | // DEFINE FIELD xyz ON user TYPE record; 90 | 91 | // DEFINE TABLE xyz SCHEMAFULL; 92 | // DEFINE FIELD abc ON xyz TYPE string; 93 | // "#; 94 | // let expected_str = r#" 95 | // export const XQuery = "SELECT foo AS bar, baz, xyz.abc FROM user;" 96 | // export type XQueryResult = [{bar:string,baz:number,xyz:{abc:string,},}[],] 97 | // "# 98 | // .trim(); 99 | // let output = 100 | // surreal_type_generator::step_3_outputs::generate_typescript_file("x.surql", query_str, schema_str)? 101 | // .types; 102 | 103 | // assert_eq_sorted!(output, expected_str); 104 | 105 | // Ok(()) 106 | // } 107 | 108 | // #[test] 109 | // fn select_query_with_various_primitive_types() -> anyhow::Result<()> { 110 | // let query = r#" 111 | // SELECT 112 | // name, 113 | // age, 114 | // bool, 115 | // datetime, 116 | // duration, 117 | // decimal 118 | // FROM 119 | // user; 120 | // "#; 121 | // let schema = r#" 122 | // DEFINE TABLE user SCHEMAFULL; 123 | // DEFINE FIELD name ON user TYPE string; 124 | // DEFINE FIELD age ON user TYPE int; 125 | // DEFINE FIELD bool ON user TYPE bool; 126 | // DEFINE FIELD datetime ON user TYPE datetime; 127 | // DEFINE FIELD duration ON user TYPE duration; 128 | // DEFINE FIELD decimal ON user TYPE decimal; 129 | // "#; 130 | // let expected_str = r#" 131 | // export const XQuery = "SELECT name, age, bool, datetime, duration, decimal FROM user;" 132 | // export type XQueryResult = [{age:number,bool:boolean,datetime:Date,decimal:Decimal,duration:Duration,name:string,}[],] 133 | // "#.trim(); 134 | // let output = 135 | // surreal_type_generator::step_3_outputs::generate_typescript_file("x.surql", query_str, schema_str)? 136 | // .types; 137 | 138 | // assert_eq_sorted!(output, expected_str); 139 | 140 | // Ok(()) 141 | // } 142 | 143 | // #[test] 144 | // fn select_query_with_only_value() -> anyhow::Result<()> { 145 | // let query = r#" 146 | // SELECT 147 | // name 148 | // FROM ONLY user; 149 | // "#; 150 | // let schema = r#" 151 | // DEFINE TABLE user SCHEMAFULL; 152 | // DEFINE FIELD name ON user TYPE string; 153 | // "#; 154 | // let expected_str = r#" 155 | // export const XQuery = "SELECT name FROM ONLY user;" 156 | // export type XQueryResult = [{name:string,},] 157 | // "# 158 | // .trim(); 159 | // let output = 160 | // surreal_type_generator::step_3_outputs::generate_typescript_file("x.surql", query_str, schema_str)? 161 | // .types; 162 | 163 | // assert_eq_sorted!(output, expected_str); 164 | 165 | // Ok(()) 166 | // } 167 | 168 | // #[test] 169 | // fn select_query_with_nested_array_string_field() -> anyhow::Result<()> { 170 | // let query = r#" 171 | // SELECT 172 | // tags.* 173 | // FROM 174 | // post; 175 | // "#; 176 | // let schema = r#" 177 | // DEFINE TABLE post SCHEMAFULL; 178 | // DEFINE FIELD tags ON post TYPE array; 179 | // "#; 180 | // let expected_str = r#" 181 | // export const XQuery = "SELECT tags[*] FROM post;" 182 | // export type XQueryResult = [{tags:string[],}[],] 183 | // "# 184 | // .trim(); 185 | // let output = 186 | // surreal_type_generator::step_3_outputs::generate_typescript_file("x.surql", query_str, schema_str)? 187 | // .types; 188 | 189 | // assert_eq_sorted!(output, expected_str); 190 | 191 | // Ok(()) 192 | // } 193 | 194 | // #[test] 195 | // fn optional_fields() -> anyhow::Result<()> { 196 | // let query = r#" 197 | // SELECT 198 | // name, 199 | // age, 200 | // bool, 201 | // xyz.abc 202 | // FROM 203 | // user; 204 | // "#; 205 | // let schema = r#" 206 | // DEFINE TABLE user SCHEMAFULL; 207 | // DEFINE FIELD name ON user TYPE option; 208 | // DEFINE FIELD age ON user TYPE option; 209 | // DEFINE FIELD bool ON user TYPE option; 210 | // DEFINE FIELD xyz ON user TYPE option>; 211 | 212 | // DEFINE TABLE xyz SCHEMAFULL; 213 | // DEFINE FIELD abc ON xyz TYPE option; 214 | // "#; 215 | // let expected_str = r#" 216 | // export const XQuery = "SELECT name, age, bool, xyz.abc FROM user;" 217 | // export type XQueryResult = [{age:number|null,bool:boolean|null,name:string|null,xyz:{abc:string|null,}|null,}[],] 218 | // "#.trim(); 219 | // let output = 220 | // surreal_type_generator::step_3_outputs::generate_typescript_file("x.surql", query_str, schema_str)? 221 | // .types; 222 | 223 | // assert_eq_sorted!(output, expected_str); 224 | 225 | // Ok(()) 226 | // } 227 | -------------------------------------------------------------------------------- /surreal_type_generator/tests/update_statement_tests.rs: -------------------------------------------------------------------------------- 1 | use pretty_assertions_sorted::assert_eq_sorted; 2 | use surreal_type_generator::{kind, QueryResult}; 3 | 4 | #[test] 5 | fn update_statement_with_set_field() -> anyhow::Result<()> { 6 | let query = r#" 7 | UPDATE user:john SET name = "John"; 8 | "#; 9 | let schema = r#" 10 | DEFINE TABLE user SCHEMAFULL; 11 | DEFINE FIELD name ON user TYPE string; 12 | "#; 13 | 14 | let QueryResult { return_types, .. } = 15 | surreal_type_generator::step_3_codegen::query_to_return_type(query, schema)?; 16 | 17 | assert_eq_sorted!( 18 | return_types, 19 | vec![kind!([kind!({ 20 | id: kind!(Record ["user"]), 21 | name: kind!(String) 22 | })])] 23 | ); 24 | 25 | Ok(()) 26 | } 27 | 28 | #[test] 29 | fn update_return_before() -> anyhow::Result<()> { 30 | let query = r#" 31 | UPDATE user:john SET baz = "bar" RETURN BEFORE; 32 | "#; 33 | 34 | let schema = r#" 35 | DEFINE TABLE user SCHEMAFULL; 36 | DEFINE FIELD name ON user TYPE string; 37 | DEFINE FIELD baz ON user TYPE string; 38 | "#; 39 | let QueryResult { return_types, .. } = 40 | surreal_type_generator::step_3_codegen::query_to_return_type(query, schema)?; 41 | 42 | assert_eq_sorted!( 43 | return_types, 44 | vec![kind!([kind!(Either [ 45 | kind!({ 46 | id: kind!(Record ["user"]), 47 | name: kind!(String), 48 | baz: kind!(String) 49 | }), 50 | kind!(Null) 51 | ])])] 52 | ); 53 | 54 | Ok(()) 55 | } 56 | 57 | #[test] 58 | fn update_return_after() -> anyhow::Result<()> { 59 | let query = r#" 60 | UPDATE user:john SET baz = "bar" RETURN AFTER; 61 | "#; 62 | 63 | let schema = r#" 64 | DEFINE TABLE user SCHEMAFULL; 65 | DEFINE FIELD name ON user TYPE string; 66 | DEFINE FIELD baz ON user TYPE string; 67 | "#; 68 | 69 | let QueryResult { return_types, .. } = 70 | surreal_type_generator::step_3_codegen::query_to_return_type(query, schema)?; 71 | 72 | assert_eq_sorted!( 73 | return_types, 74 | vec![kind!([kind!({ 75 | id: kind!(Record ["user"]), 76 | name: kind!(String), 77 | baz: kind!(String) 78 | })])] 79 | ); 80 | 81 | Ok(()) 82 | } 83 | 84 | #[test] 85 | fn update_return_null() -> anyhow::Result<()> { 86 | let query = r#" 87 | UPDATE user:john SET baz = "bar" RETURN NULL; 88 | "#; 89 | 90 | let schema = r#" 91 | DEFINE TABLE user SCHEMAFULL; 92 | DEFINE FIELD name ON user TYPE string; 93 | DEFINE FIELD baz ON user TYPE string; 94 | "#; 95 | 96 | let QueryResult { return_types, .. } = 97 | surreal_type_generator::step_3_codegen::query_to_return_type(query, schema)?; 98 | 99 | assert_eq_sorted!(return_types, vec![kind!([kind!(Null)])]); 100 | 101 | Ok(()) 102 | } 103 | 104 | #[test] 105 | fn update_return_none() -> anyhow::Result<()> { 106 | let query = r#" 107 | UPDATE user:john SET baz = "bar" RETURN NONE; 108 | "#; 109 | 110 | let schema = r#" 111 | DEFINE TABLE user SCHEMAFULL; 112 | DEFINE FIELD name ON user TYPE string; 113 | DEFINE FIELD baz ON user TYPE string; 114 | "#; 115 | 116 | let QueryResult { return_types, .. } = 117 | surreal_type_generator::step_3_codegen::query_to_return_type(query, schema)?; 118 | 119 | assert_eq_sorted!(return_types, vec![kind!([kind!(Null)])]); 120 | 121 | Ok(()) 122 | } 123 | 124 | #[test] 125 | fn update_return_fields() -> anyhow::Result<()> { 126 | let query = r#" 127 | UPDATE user:john SET baz = "bar" RETURN baz; 128 | "#; 129 | 130 | let schema = r#" 131 | DEFINE TABLE user SCHEMAFULL; 132 | DEFINE FIELD name ON user TYPE string; 133 | DEFINE FIELD baz ON user TYPE string; 134 | "#; 135 | 136 | let QueryResult { return_types, .. } = 137 | surreal_type_generator::step_3_codegen::query_to_return_type(query, schema)?; 138 | 139 | assert_eq_sorted!( 140 | return_types, 141 | vec![kind!([kind!({ 142 | baz: kind!(String), 143 | })])] 144 | ); 145 | 146 | Ok(()) 147 | } 148 | -------------------------------------------------------------------------------- /surreal_type_generator/tests/upsert_statement_tests.rs: -------------------------------------------------------------------------------- 1 | use pretty_assertions_sorted::assert_eq_sorted; 2 | use surreal_type_generator::{kind, var_map, QueryResult}; 3 | 4 | #[test] 5 | fn simple_upsert_statement() -> anyhow::Result<()> { 6 | let query = r#" 7 | UPSERT user CONTENT { 8 | name: "John Doe", 9 | age: 30, 10 | }; 11 | "#; 12 | let schema = r#" 13 | DEFINE TABLE user SCHEMAFULL; 14 | DEFINE FIELD name ON user TYPE string; 15 | DEFINE FIELD age ON user TYPE number; 16 | "#; 17 | 18 | let QueryResult { return_types, .. } = 19 | surreal_type_generator::step_3_codegen::query_to_return_type(query, schema)?; 20 | 21 | assert_eq_sorted!( 22 | return_types, 23 | vec![kind!([kind!({ 24 | id: kind!(Record ["user"]), 25 | name: kind!(String), 26 | age: kind!(Number) 27 | })])] 28 | ); 29 | 30 | Ok(()) 31 | } 32 | 33 | #[test] 34 | fn id_upsert_statement() -> anyhow::Result<()> { 35 | let query = r#" 36 | UPSERT user:john CONTENT { 37 | name: "John Doe", 38 | age: 30, 39 | }; 40 | "#; 41 | let schema = r#" 42 | DEFINE TABLE user SCHEMAFULL; 43 | DEFINE FIELD name ON user TYPE string; 44 | DEFINE FIELD age ON user TYPE number; 45 | "#; 46 | 47 | let QueryResult { return_types, .. } = 48 | surreal_type_generator::step_3_codegen::query_to_return_type(query, schema)?; 49 | 50 | assert_eq_sorted!( 51 | return_types, 52 | vec![kind!([kind!({ 53 | id: kind!(Record ["user"]), 54 | name: kind!(String), 55 | age: kind!(Number) 56 | })])] 57 | ); 58 | 59 | Ok(()) 60 | } 61 | 62 | #[test] 63 | fn param_upsert_statement() -> anyhow::Result<()> { 64 | let query = r#" 65 | > $id; 66 | 67 | UPSERT $id CONTENT $content; 68 | "#; 69 | let schema = r#" 70 | DEFINE TABLE user SCHEMAFULL; 71 | DEFINE FIELD name ON user TYPE string; 72 | DEFINE FIELD age ON user TYPE number; 73 | "#; 74 | 75 | let QueryResult { 76 | return_types, 77 | variables, 78 | .. 79 | } = surreal_type_generator::step_3_codegen::query_to_return_type(query, schema)?; 80 | 81 | let user_upsert = kind!({ 82 | id: kind!(Opt (kind!(Record ["user"]))), 83 | name: kind!(String), 84 | age: kind!(Number), 85 | }); 86 | 87 | assert_eq_sorted!( 88 | variables, 89 | var_map! { 90 | id: kind!(Record ["user"]), 91 | content: kind!(Either [user_upsert.clone(), kind!([user_upsert])]) 92 | } 93 | ); 94 | 95 | assert_eq_sorted!( 96 | return_types, 97 | vec![kind!([kind!({ 98 | id: kind!(Record ["user"]), 99 | name: kind!(String), 100 | age: kind!(Number) 101 | })])] 102 | ); 103 | 104 | Ok(()) 105 | } 106 | 107 | #[test] 108 | fn upsert_merge_statement() { 109 | let query = r#" 110 | UPSERT user MERGE { 111 | id: user:foo, 112 | name: "John Doe", 113 | age: 30, 114 | }; 115 | "#; 116 | let schema = r#" 117 | DEFINE TABLE user SCHEMAFULL; 118 | DEFINE FIELD name ON user TYPE string; 119 | DEFINE FIELD age ON user TYPE number; 120 | "#; 121 | 122 | let QueryResult { return_types, .. } = 123 | surreal_type_generator::step_3_codegen::query_to_return_type(query, schema).unwrap(); 124 | 125 | assert_eq_sorted!( 126 | return_types, 127 | vec![kind!([kind!({ 128 | id: kind!(Record ["user"]), 129 | name: kind!(String), 130 | age: kind!(Number) 131 | })])] 132 | ); 133 | } 134 | -------------------------------------------------------------------------------- /surreal_type_generator/tests/variable_usage_tests.rs: -------------------------------------------------------------------------------- 1 | use pretty_assertions_sorted::assert_eq_sorted; 2 | use surreal_type_generator::{kind, QueryResult}; 3 | 4 | #[test] 5 | fn query_with_variable() -> anyhow::Result<()> { 6 | let query = r#" 7 | DELETE user RETURN $before; 8 | "#; 9 | let schema = r#" 10 | DEFINE TABLE user SCHEMAFULL; 11 | DEFINE FIELD name ON user TYPE string; 12 | "#; 13 | 14 | let QueryResult { 15 | return_types, 16 | variables, 17 | .. 18 | } = surreal_type_generator::step_3_codegen::query_to_return_type(query, schema)?; 19 | 20 | // $before should not be a required variable 21 | assert_eq_sorted!(variables, [].into()); 22 | 23 | assert_eq_sorted!( 24 | return_types, 25 | vec![kind!([kind!({ 26 | before: kind!({ 27 | id: kind!(Record ["user"]), 28 | name: kind!(String) 29 | }) 30 | })])] 31 | ); 32 | 33 | Ok(()) 34 | } 35 | 36 | #[test] 37 | fn query_with_variable_with_multiple_returns() -> anyhow::Result<()> { 38 | let query = r#" 39 | DELETE user RETURN $before.name AS alias, $before.xyz.baz AS baz 40 | "#; 41 | let schema = r#" 42 | DEFINE TABLE user SCHEMAFULL; 43 | DEFINE FIELD name ON user TYPE string; 44 | DEFINE FIELD xyz ON user TYPE record; 45 | 46 | DEFINE TABLE abc SCHEMAFULL; 47 | DEFINE FIELD baz ON abc TYPE string; 48 | "#; 49 | 50 | let QueryResult { 51 | return_types, 52 | variables, 53 | .. 54 | } = surreal_type_generator::step_3_codegen::query_to_return_type(query, schema)?; 55 | 56 | // $before should not be a required variable 57 | assert_eq_sorted!(variables, [].into()); 58 | 59 | assert_eq_sorted!( 60 | return_types, 61 | vec![kind!([kind!({ 62 | alias: kind!(String), 63 | baz: kind!(String) 64 | })])] 65 | ); 66 | 67 | Ok(()) 68 | } 69 | 70 | #[test] 71 | fn query_with_variable_with_multiple_returns_with_alias() -> anyhow::Result<()> { 72 | let query = r#" 73 | DELETE user RETURN $after 74 | "#; 75 | let schema = r#" 76 | DEFINE TABLE user SCHEMAFULL; 77 | DEFINE FIELD name ON user TYPE string; 78 | "#; 79 | 80 | let QueryResult { 81 | return_types, 82 | variables, 83 | .. 84 | } = surreal_type_generator::step_3_codegen::query_to_return_type(query, schema)?; 85 | 86 | // $after should not be a required variable 87 | assert_eq_sorted!(variables, [].into()); 88 | 89 | assert_eq_sorted!( 90 | return_types, 91 | vec![kind!([kind!({ 92 | after: kind!(Null) 93 | })])] 94 | ); 95 | 96 | Ok(()) 97 | } 98 | 99 | #[test] 100 | fn query_with_this_field() -> anyhow::Result<()> { 101 | let query = r#" 102 | SELECT 103 | name, 104 | $this.name AS alias 105 | FROM user; 106 | "#; 107 | let schema = r#" 108 | DEFINE TABLE user SCHEMAFULL; 109 | DEFINE FIELD name ON user TYPE string; 110 | "#; 111 | 112 | let QueryResult { 113 | return_types, 114 | variables, 115 | .. 116 | } = surreal_type_generator::step_3_codegen::query_to_return_type(query, schema)?; 117 | 118 | // $this should not be a required variable 119 | assert_eq_sorted!(variables, [].into()); 120 | 121 | assert_eq_sorted!( 122 | return_types, 123 | vec![kind!([kind!({ 124 | name: kind!(String), 125 | alias: kind!(String) 126 | })])] 127 | ); 128 | 129 | Ok(()) 130 | } 131 | 132 | #[test] 133 | fn query_with_nested_query_parent_parameter() -> anyhow::Result<()> { 134 | let query = r#" 135 | SELECT 136 | name, 137 | ($parent.name) AS alias 138 | FROM user; 139 | "#; 140 | let schema = r#" 141 | DEFINE TABLE user SCHEMAFULL; 142 | DEFINE FIELD name ON user TYPE string; 143 | "#; 144 | 145 | let QueryResult { 146 | return_types, 147 | variables, 148 | .. 149 | } = surreal_type_generator::step_3_codegen::query_to_return_type(query, schema)?; 150 | 151 | // $parent should not be a required variable 152 | assert_eq_sorted!(variables, [].into()); 153 | 154 | assert_eq_sorted!( 155 | return_types, 156 | vec![kind!([kind!({ 157 | name: kind!(String), 158 | alias: kind!(String) 159 | })])] 160 | ); 161 | 162 | Ok(()) 163 | } 164 | --------------------------------------------------------------------------------