surql_parser/lib.rs
1//! surql-parser — Standalone SurrealQL parser extracted from SurrealDB.
2//!
3//! Provides a complete SurrealQL parser without depending on the SurrealDB engine.
4//! Useful for building tools, linters, formatters, IDE extensions, and migration systems.
5//!
6//! # Quick Start
7//!
8//! ```
9//! let ast = surql_parser::parse("SELECT name, age FROM user WHERE age > 18").unwrap();
10//! assert!(!ast.expressions.is_empty());
11//! ```
12//!
13//! # Sync with SurrealDB
14//!
15//! Parser source is auto-extracted from SurrealDB via `tools/transform/`.
16//! See UPSTREAM_SYNC.md for details.
17
18#[macro_use]
19extern crate tracing;
20
21pub mod compat;
22pub mod config;
23
24#[cfg(feature = "build")]
25pub mod build;
26
27#[allow(
28 clippy::useless_conversion,
29 clippy::large_enum_variant,
30 clippy::match_single_binding,
31 clippy::needless_borrow
32)]
33pub mod upstream;
34
35// ─── Public API ───
36
37/// Parse a SurrealQL query string into an AST.
38///
39/// Returns a list of top-level expressions (statements).
40///
41/// # Example
42///
43/// ```
44/// let ast = surql_parser::parse("CREATE user SET name = 'Alice'").unwrap();
45/// assert_eq!(ast.expressions.len(), 1);
46/// ```
47pub fn parse(input: &str) -> anyhow::Result<Ast> {
48 upstream::syn::parse(input)
49}
50
51/// Parse a SurrealQL query with custom parser settings.
52pub fn parse_with_settings(input: &str, settings: ParserSettings) -> anyhow::Result<Ast> {
53 upstream::syn::parse_with_settings(input.as_bytes(), settings, async |parser, stk| {
54 parser.parse_query(stk).await
55 })
56}
57
58/// Parse a SurrealQL type annotation (e.g., `record<user>`, `option<string>`).
59pub fn parse_kind(input: &str) -> anyhow::Result<Kind> {
60 upstream::syn::kind(input)
61}
62
63/// Check if a string could be a reserved keyword in certain contexts.
64pub fn is_reserved_keyword(s: &str) -> bool {
65 upstream::syn::could_be_reserved_keyword(s)
66}
67
68// ─── Schema Extraction ───
69
70/// All definitions found in a SurrealQL file.
71///
72/// Use `extract_definitions()` to get this from a .surql file.
73/// This is the primary tool for migration systems and schema analyzers.
74use upstream::sql::statements::define;
75
76#[derive(Debug, Default)]
77pub struct SchemaDefinitions {
78 pub namespaces: Vec<statements::DefineNamespaceStatement>,
79 pub databases: Vec<define::DefineDatabaseStatement>,
80 pub tables: Vec<statements::DefineTableStatement>,
81 pub fields: Vec<statements::DefineFieldStatement>,
82 pub indexes: Vec<statements::DefineIndexStatement>,
83 pub functions: Vec<statements::DefineFunctionStatement>,
84 pub analyzers: Vec<define::DefineAnalyzerStatement>,
85 pub events: Vec<statements::DefineEventStatement>,
86 pub params: Vec<define::DefineParamStatement>,
87 pub users: Vec<define::DefineUserStatement>,
88 pub accesses: Vec<define::DefineAccessStatement>,
89}
90
91/// Extract all DEFINE statements from a SurrealQL string.
92///
93/// Useful for schema analysis, migration tools, and validation.
94///
95/// # Example
96///
97/// ```
98/// let defs = surql_parser::extract_definitions("
99/// DEFINE TABLE user SCHEMAFULL;
100/// DEFINE FIELD name ON user TYPE string;
101/// DEFINE FIELD age ON user TYPE int DEFAULT 0;
102/// DEFINE INDEX email_idx ON user FIELDS email UNIQUE;
103/// DEFINE FUNCTION fn::greet($name: string) { RETURN 'Hello, ' + $name; };
104/// ").unwrap();
105///
106/// assert_eq!(defs.tables.len(), 1);
107/// assert_eq!(defs.fields.len(), 2);
108/// assert_eq!(defs.indexes.len(), 1);
109/// assert_eq!(defs.functions.len(), 1);
110/// ```
111pub fn extract_definitions(input: &str) -> anyhow::Result<SchemaDefinitions> {
112 let ast = parse(input)?;
113 let mut defs = SchemaDefinitions::default();
114
115 for top in &ast.expressions {
116 if let upstream::sql::ast::TopLevelExpr::Expr(Expr::Define(stmt)) = top {
117 use define::DefineStatement as DS;
118 match stmt.as_ref() {
119 DS::Namespace(s) => defs.namespaces.push(s.clone()),
120 DS::Database(s) => defs.databases.push(s.clone()),
121 DS::Table(s) => defs.tables.push(s.clone()),
122 DS::Field(s) => defs.fields.push(s.clone()),
123 DS::Index(s) => defs.indexes.push(s.clone()),
124 DS::Function(s) => defs.functions.push(s.clone()),
125 DS::Analyzer(s) => defs.analyzers.push(s.clone()),
126 DS::Event(s) => defs.events.push(s.clone()),
127 DS::Param(s) => defs.params.push(s.clone()),
128 DS::User(s) => defs.users.push(s.clone()),
129 DS::Access(s) => defs.accesses.push(s.clone()),
130 _ => {} // Config, Api, Bucket, Sequence, Module, Model — less common
131 }
132 }
133 }
134
135 Ok(defs)
136}
137
138/// List all function names defined in a SurrealQL string.
139///
140/// # Example
141///
142/// ```
143/// let fns = surql_parser::list_functions("
144/// DEFINE FUNCTION fn::greet($name: string) { RETURN 'Hello, ' + $name; };
145/// DEFINE FUNCTION fn::add($a: int, $b: int) { RETURN $a + $b; };
146/// ").unwrap();
147///
148/// assert_eq!(fns, vec!["greet", "add"]);
149/// ```
150pub fn list_functions(input: &str) -> anyhow::Result<Vec<String>> {
151 let defs = extract_definitions(input)?;
152 Ok(defs
153 .functions
154 .iter()
155 .map(|f| {
156 use surrealdb_types::{SqlFormat, ToSql};
157 let mut name = String::new();
158 f.name.fmt_sql(&mut name, SqlFormat::SingleLine);
159 name
160 })
161 .collect())
162}
163
164/// List all table names defined in a SurrealQL string.
165///
166/// # Example
167///
168/// ```
169/// let tables = surql_parser::list_tables("
170/// DEFINE TABLE user SCHEMAFULL;
171/// DEFINE TABLE post SCHEMALESS;
172/// SELECT * FROM user;
173/// ").unwrap();
174///
175/// assert_eq!(tables, vec!["user", "post"]);
176/// ```
177pub fn list_tables(input: &str) -> anyhow::Result<Vec<String>> {
178 let defs = extract_definitions(input)?;
179 Ok(defs
180 .tables
181 .iter()
182 .map(|t| {
183 use surrealdb_types::{SqlFormat, ToSql};
184 let mut name = String::new();
185 t.name.fmt_sql(&mut name, SqlFormat::SingleLine);
186 name
187 })
188 .collect())
189}
190
191/// Format an AST back to SurrealQL string.
192///
193/// # Example
194///
195/// ```
196/// let ast = surql_parser::parse("SELECT * FROM user").unwrap();
197/// let sql = surql_parser::format(&ast);
198/// assert!(sql.contains("SELECT"));
199/// ```
200pub fn format(ast: &Ast) -> String {
201 use surrealdb_types::{SqlFormat, ToSql};
202 let mut buf = String::new();
203 ast.fmt_sql(&mut buf, SqlFormat::SingleLine);
204 buf
205}
206
207// ─── Re-exports ───
208
209/// The parsed AST (list of top-level statements).
210pub use upstream::sql::Ast;
211
212/// A single expression in the AST.
213pub use upstream::sql::expression::Expr;
214
215/// Parser configuration settings.
216pub use upstream::syn::ParserSettings;
217
218/// SurrealQL type annotation (e.g., `string`, `record<user>`, `array<int>`).
219pub use upstream::sql::Kind;
220
221/// An identifier path (e.g., `user.name`, `->knows->person`).
222pub use upstream::sql::Idiom;
223
224/// A SurrealQL statement (SELECT, CREATE, DEFINE, etc.).
225pub use upstream::sql::statements;
226
227/// Syntax error type.
228pub use upstream::syn::error;