diff --git a/Cargo.lock b/Cargo.lock index 885166e..b2f099a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -228,6 +228,7 @@ name = "sqlparser-rs-wasm" version = "0.61.1" dependencies = [ "console_error_panic_hook", + "js-sys", "serde", "serde-wasm-bindgen", "serde_json", diff --git a/Cargo.toml b/Cargo.toml index d244fb8..9b63441 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,7 @@ serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" serde-wasm-bindgen = "0.6" console_error_panic_hook = "0.1" +js-sys = "0.3" [profile.release] opt-level = "s" diff --git a/src/index.ts b/src/index.ts index 33e4a7c..f8707d5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,7 +4,7 @@ */ // Parser -export { Parser, init, parse, validate, format } from './parser.js'; +export { Parser, init, parse, parseWithComments, validate, format } from './parser.js'; export type { ParserOptions, DialectInput } from './parser.js'; // Dialects diff --git a/src/lib.rs b/src/lib.rs index a37f66b..e32063d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,6 +4,7 @@ use sqlparser::dialect::{ GenericDialect, HiveDialect, MsSqlDialect, MySqlDialect, OracleDialect, PostgreSqlDialect, RedshiftSqlDialect, SQLiteDialect, SnowflakeDialect, }; +use sqlparser::ast::comments::{Comment as SqlComment, CommentWithSpan}; use sqlparser::parser::Parser; use wasm_bindgen::prelude::*; @@ -82,7 +83,7 @@ pub fn parse_sql_with_options( // Note: trailing_commas option support depends on sqlparser version let tokens = sqlparser::tokenizer::Tokenizer::new(dialect_impl.as_ref(), sql) - .tokenize() + .tokenize_with_location() .map_err(|e| { let error = ParseError { message: e.to_string(), @@ -92,7 +93,7 @@ pub fn parse_sql_with_options( serde_wasm_bindgen::to_value(&error).unwrap_or(JsValue::from_str(&e.to_string())) })?; - parser = parser.with_tokens(tokens); + parser = parser.with_tokens_with_locations(tokens); let statements = parser.parse_statements().map_err(|e| { let error = ParseError { @@ -163,6 +164,69 @@ pub fn get_supported_dialects() -> JsValue { serde_wasm_bindgen::to_value(&dialects).unwrap() } +/// A serializable source comment +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub struct SerializedComment { + pub comment_type: String, + pub content: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub prefix: Option, + pub start_line: u64, + pub start_column: u64, + pub end_line: u64, + pub end_column: u64, +} + +impl From<&CommentWithSpan> for SerializedComment { + fn from(c: &CommentWithSpan) -> Self { + let (comment_type, content, prefix) = match &c.comment { + SqlComment::SingleLine { content, prefix } => { + ("singleLine".to_string(), content.clone(), Some(prefix.clone())) + } + SqlComment::MultiLine(content) => { + ("multiLine".to_string(), content.clone(), None) + } + }; + SerializedComment { + comment_type, content, prefix, + start_line: c.span.start.line, + start_column: c.span.start.column, + end_line: c.span.end.line, + end_column: c.span.end.column, + } + } +} + +/// Parse SQL and return both AST and source comments +#[wasm_bindgen] +pub fn parse_sql_with_comments(dialect: &str, sql: &str) -> Result { + let dialect_impl = get_dialect(dialect); + let (statements, comments) = + Parser::parse_sql_with_comments(dialect_impl.as_ref(), sql).map_err(|e| { + let error = ParseError { + message: e.to_string(), + line: None, + column: None, + }; + serde_wasm_bindgen::to_value(&error).unwrap_or(JsValue::from_str(&e.to_string())) + })?; + + let comments_vec: Vec = comments.into(); + let serialized_comments: Vec = + comments_vec.iter().map(SerializedComment::from).collect(); + + // Build JS object manually to avoid double-serialization + let obj = js_sys::Object::new(); + let stmts_val = serde_wasm_bindgen::to_value(&statements) + .map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))?; + let comments_val = serde_wasm_bindgen::to_value(&serialized_comments) + .map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))?; + js_sys::Reflect::set(&obj, &"statements".into(), &stmts_val).unwrap(); + js_sys::Reflect::set(&obj, &"comments".into(), &comments_val).unwrap(); + Ok(obj.into()) +} + /// Validate SQL syntax without returning the full AST #[wasm_bindgen] pub fn validate_sql(dialect: &str, sql: &str) -> Result { diff --git a/src/parser.ts b/src/parser.ts index 20e8da7..1a2daea 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -2,6 +2,7 @@ import type { Dialect, DialectName } from './dialects.js'; import { dialectFromString, GenericDialect } from './dialects.js'; import { ParserError } from './types/errors.js'; import type { Statement } from './types/ast.js'; +import type { SourceComment, ParseWithCommentsResult } from './types/comments.js'; import { getWasmModule } from './wasm.js'; export { init } from './wasm.js'; @@ -80,6 +81,16 @@ export class Parser { } } + /** Parse SQL and return both AST and source comments */ + parseWithComments(sql: string): ParseWithCommentsResult { + const wasm = getWasmModule(); + try { + return wasm.parse_sql_with_comments(this.dialect.name, sql) as ParseWithCommentsResult; + } catch (error) { + throw ParserError.fromWasmError(error); + } + } + // Static methods /** Parse SQL into AST */ @@ -87,6 +98,11 @@ export class Parser { return new Parser(resolveDialect(dialect)).parse(sql); } + /** Parse SQL and return both AST and source comments */ + static parseWithComments(sql: string, dialect: DialectInput = 'generic'): ParseWithCommentsResult { + return new Parser(resolveDialect(dialect)).parseWithComments(sql); + } + /** Parse SQL and return AST as JSON string */ static parseToJson(sql: string, dialect: DialectInput = 'generic'): string { const wasm = getWasmModule(); @@ -145,6 +161,13 @@ export function parse(sql: string, dialect: DialectInput = 'generic'): Statement return Parser.parse(sql, dialect); } +/** + * Parse SQL and return both AST and source comments + */ +export function parseWithComments(sql: string, dialect: DialectInput = 'generic'): ParseWithCommentsResult { + return Parser.parseWithComments(sql, dialect); +} + /** * Validate SQL syntax * @throws ParserError if SQL is invalid diff --git a/src/types/comments.ts b/src/types/comments.ts new file mode 100644 index 0000000..3260fba --- /dev/null +++ b/src/types/comments.ts @@ -0,0 +1,23 @@ +/** A source code comment extracted from parsed SQL */ +export interface SourceComment { + /** "singleLine" for -- comments, "multiLine" for block comments */ + commentType: 'singleLine' | 'multiLine' + /** The comment text content (excluding markers) */ + content: string + /** For single-line comments, the prefix (e.g. "--", "#") */ + prefix?: string + /** Start line (1-based) */ + startLine: number + /** Start column (1-based) */ + startColumn: number + /** End line (1-based) */ + endLine: number + /** End column (1-based) */ + endColumn: number +} + +/** Result of parsing SQL with comments */ +export interface ParseWithCommentsResult { + statements: T[] + comments: SourceComment[] +} diff --git a/src/types/index.ts b/src/types/index.ts index ecf1423..33eb23f 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,2 +1,3 @@ export * from './ast.js'; export * from './errors.js'; +export * from './comments.js'; diff --git a/src/wasm.ts b/src/wasm.ts index d124a56..02fdd6b 100644 --- a/src/wasm.ts +++ b/src/wasm.ts @@ -4,6 +4,7 @@ import { WasmInitError } from './types/errors.js'; export interface WasmModule { parse_sql: (dialect: string, sql: string) => unknown; parse_sql_with_options: (dialect: string, sql: string, options: unknown) => unknown; + parse_sql_with_comments: (dialect: string, sql: string) => unknown; parse_sql_to_json_string: (dialect: string, sql: string) => string; parse_sql_to_string: (dialect: string, sql: string) => string; format_sql: (dialect: string, sql: string) => string;