Skip to content

Commit 178aa3c

Browse files
authored
feat: expose new workspace function splitStatements with ranges (#730)
1 parent cfeb686 commit 178aa3c

9 files changed

Lines changed: 325 additions & 12 deletions

File tree

crates/pgls_wasm/build-wasm.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ EXPORTED_FUNCTIONS="[
8585
'_pgls_complete',
8686
'_pgls_hover',
8787
'_pgls_parse',
88+
'_pgls_split_statements',
8889
'_pgls_version',
8990
'_pgls_lsp_handle_message'
9091
]"

crates/pgls_wasm/src/ffi.rs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,27 @@ pub unsafe extern "C" fn pgls_parse(sql: *const c_char) -> *mut c_char {
256256
})
257257
}
258258

259+
/// Split SQL into individual statements.
260+
/// Returns a JSON array of `Statement` objects, each with `sql`, `start`, and `end`.
261+
/// The returned string must be freed with `pgls_free_string`.
262+
///
263+
/// # Safety
264+
/// The sql pointer must be valid and point to a null-terminated UTF-8 string.
265+
#[unsafe(no_mangle)]
266+
pub unsafe extern "C" fn pgls_split_statements(sql: *const c_char) -> *mut c_char {
267+
// SAFETY: Caller guarantees sql is valid
268+
let sql_str = match unsafe { c_str_to_str(sql) } {
269+
Some(s) => s,
270+
None => return str_to_c_string("ERROR: Invalid SQL pointer"),
271+
};
272+
273+
with_workspace(|ws| {
274+
let statements = ws.split_statements(sql_str);
275+
let json = serde_json::to_string(&statements).unwrap_or_else(|e| format!("ERROR: {e}"));
276+
str_to_c_string(&json)
277+
})
278+
}
279+
259280
/// Get the version of the library.
260281
/// The returned string must be freed with `pgls_free_string`.
261282
#[unsafe(no_mangle)]
@@ -371,4 +392,23 @@ mod tests {
371392
pgls_free_string(result);
372393
}
373394
}
395+
396+
#[test]
397+
fn test_ffi_split_statements() {
398+
pgls_init();
399+
400+
unsafe {
401+
let sql = CString::new("SELECT 1; SELECT 2;").unwrap();
402+
let result = pgls_split_statements(sql.as_ptr());
403+
assert!(!result.is_null());
404+
let result_str = CStr::from_ptr(result).to_str().unwrap();
405+
let parsed: Vec<serde_json::Value> = serde_json::from_str(result_str).unwrap();
406+
assert_eq!(parsed.len(), 2);
407+
assert_eq!(parsed[0]["sql"], "SELECT 1;");
408+
assert_eq!(parsed[0]["start"], 0);
409+
assert_eq!(parsed[0]["end"], 9);
410+
assert_eq!(parsed[1]["sql"], "SELECT 2;");
411+
pgls_free_string(result);
412+
}
413+
}
374414
}

crates/pgls_wasm/src/lib.rs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,18 @@ pub struct CompletionItem {
6666
pub detail: Option<String>,
6767
}
6868

69+
/// A single SQL statement extracted from a multi-statement source.
70+
#[derive(Debug, Serialize, Deserialize)]
71+
#[serde(rename_all = "camelCase")]
72+
pub struct Statement {
73+
/// The SQL text of the statement
74+
pub sql: String,
75+
/// Start byte offset in the original SQL string
76+
pub start: u32,
77+
/// End byte offset in the original SQL string
78+
pub end: u32,
79+
}
80+
6981
/// In-memory file system for storing SQL files.
7082
///
7183
/// This wraps `pgls_fs::MemoryFileSystem` with a simpler API
@@ -367,6 +379,22 @@ impl Workspace {
367379
Err(e) => vec![e.to_string()],
368380
}
369381
}
382+
383+
/// Split SQL into individual statements.
384+
///
385+
/// Each returned `Statement` contains the SQL text and its byte offsets
386+
/// in the original string.
387+
pub fn split_statements(&self, sql: &str) -> Vec<Statement> {
388+
self.inner
389+
.split_statements(sql)
390+
.into_iter()
391+
.map(|range| Statement {
392+
sql: sql[range].to_string(),
393+
start: u32::from(range.start()),
394+
end: u32::from(range.end()),
395+
})
396+
.collect()
397+
}
370398
}
371399

372400
#[cfg(test)]
@@ -397,6 +425,30 @@ mod tests {
397425
assert!(!errors.is_empty());
398426
}
399427

428+
#[test]
429+
fn test_workspace_split_statements() {
430+
let workspace = Workspace::new();
431+
432+
let statements = workspace.split_statements("SELECT 1; SELECT 2;");
433+
assert_eq!(statements.len(), 2);
434+
assert_eq!(statements[0].sql, "SELECT 1;");
435+
assert_eq!(statements[0].start, 0);
436+
assert_eq!(statements[0].end, 9);
437+
assert_eq!(statements[1].sql, "SELECT 2;");
438+
assert_eq!(statements[1].start, 10);
439+
assert_eq!(statements[1].end, 19);
440+
}
441+
442+
#[test]
443+
fn test_workspace_split_statements_ignores_comments() {
444+
let workspace = Workspace::new();
445+
446+
let statements = workspace.split_statements("-- comment\nSELECT 1;\n\nSELECT 2;");
447+
assert_eq!(statements.len(), 2);
448+
assert_eq!(statements[0].sql, "SELECT 1;");
449+
assert_eq!(statements[1].sql, "SELECT 2;");
450+
}
451+
400452
#[test]
401453
fn test_memory_fs() {
402454
let mut fs = MemoryFs::new();

crates/pgls_workspace/src/workspace/server.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ use pgls_diagnostics::{
2626
};
2727
use pgls_fs::{ConfigName, PgLSPath};
2828
use pgls_schema_cache::SchemaCache;
29+
use pgls_text_size::TextRange;
2930
#[cfg(feature = "db")]
3031
use pgls_typecheck::{IdentifierType, TypecheckParams, TypedIdentifier};
3132
use pgls_workspace_macros::ignored_path;
@@ -151,6 +152,12 @@ impl WorkspaceServer {
151152
self.schema_cache.clear();
152153
}
153154

155+
/// Split raw SQL into byte ranges of individual statements.
156+
#[allow(clippy::unused_self)]
157+
pub fn split_statements(&self, sql: &str) -> Vec<TextRange> {
158+
pgls_statement_splitter::split(sql).ranges
159+
}
160+
154161
/// Get a clone of the current schema.
155162
pub fn get_schema(&self) -> Option<Arc<SchemaCache>> {
156163
self.schema_cache.get()

packages/@postgres-language-server/wasm/README.md

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@ const workspace = await createWorkspace();
3434
const errors = workspace.parse("SELECT * FROM users;");
3535
console.log(errors); // []
3636

37+
// Split SQL into individual statements
38+
const statements = workspace.splitStatements("SELECT 1; SELECT 2;");
39+
// [{ sql: "SELECT 1;", start: 0, end: 9 }, { sql: "SELECT 2;", start: 10, end: 19 }]
40+
3741
// Insert a file and lint it
3842
workspace.insertFile("/query.sql", "SELECT * FROM users;");
3943
const diagnostics = workspace.lint("/query.sql");
@@ -132,17 +136,18 @@ lsp.handleMessage({
132136

133137
### Workspace
134138

135-
| Method | Description |
136-
| --------------------------- | ------------------------------------------ |
137-
| `parse(sql: string)` | Parse SQL, returns array of error messages |
138-
| `insertFile(path, content)` | Add or update a file in the workspace |
139-
| `removeFile(path)` | Remove a file from the workspace |
140-
| `lint(path)` | Get diagnostics for a file |
141-
| `complete(path, offset)` | Get completions at position |
142-
| `hover(path, offset)` | Get hover info at position |
143-
| `setSchema(json)` | Set database schema |
144-
| `clearSchema()` | Clear the current schema |
145-
| `version()` | Get library version |
139+
| Method | Description |
140+
| --------------------------- | ------------------------------------------- |
141+
| `parse(sql: string)` | Parse SQL, returns array of error messages |
142+
| `splitStatements(sql)` | Split SQL into statements with byte offsets |
143+
| `insertFile(path, content)` | Add or update a file in the workspace |
144+
| `removeFile(path)` | Remove a file from the workspace |
145+
| `lint(path)` | Get diagnostics for a file |
146+
| `complete(path, offset)` | Get completions at position |
147+
| `hover(path, offset)` | Get hover info at position |
148+
| `setSchema(json)` | Set database schema |
149+
| `clearSchema()` | Clear the current schema |
150+
| `version()` | Get library version |
146151

147152
### LanguageServer
148153

packages/@postgres-language-server/wasm/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export type {
2626
Diagnostic,
2727
CompletionItem,
2828
SchemaCache,
29+
Statement,
2930
WorkspaceOptions,
3031
PGLSModule,
3132
JsonRpcMessage,

packages/@postgres-language-server/wasm/src/types.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,18 @@ export interface WorkspaceOptions {
4949
schema?: SchemaCache | string;
5050
}
5151

52+
/**
53+
* A single SQL statement extracted from a multi-statement source.
54+
*/
55+
export interface Statement {
56+
/** The SQL text of the statement */
57+
sql: string;
58+
/** Start byte offset in the original SQL string */
59+
start: number;
60+
/** End byte offset in the original SQL string */
61+
end: number;
62+
}
63+
5264
/**
5365
* A JSON-RPC message (request, response, or notification).
5466
* This is the standard LSP message format.
@@ -86,6 +98,7 @@ export interface PGLSModule {
8698
_pgls_complete(pathPtr: number, offset: number): number;
8799
_pgls_hover(pathPtr: number, offset: number): number;
88100
_pgls_parse(sqlPtr: number): number;
101+
_pgls_split_statements(sqlPtr: number): number;
89102
_pgls_version(): number;
90103

91104
// Language Server API

packages/@postgres-language-server/wasm/src/workspace.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,11 @@ import type {
2222
Diagnostic,
2323
PGLSModule,
2424
SchemaCache,
25+
Statement,
2526
WorkspaceOptions,
2627
} from "./types.js";
2728

28-
export type { Diagnostic, CompletionItem, SchemaCache, WorkspaceOptions, PGLSModule };
29+
export type { Diagnostic, CompletionItem, SchemaCache, Statement, WorkspaceOptions, PGLSModule };
2930

3031
/**
3132
* The Workspace class provides a direct API for SQL parsing, linting,
@@ -154,6 +155,23 @@ export class Workspace {
154155
}
155156
}
156157

158+
/**
159+
* Split SQL into individual statements.
160+
*
161+
* Each returned `Statement` contains the SQL text and its byte offsets
162+
* in the original string.
163+
*/
164+
splitStatements(sql: string): Statement[] {
165+
const sqlPtr = allocateString(this.module, sql);
166+
try {
167+
const resultPtr = this.module._pgls_split_statements(sqlPtr);
168+
const result = readAndFreeString(this.module, resultPtr);
169+
return parseResult<Statement[]>(result) ?? [];
170+
} finally {
171+
this.module._free(sqlPtr);
172+
}
173+
}
174+
157175
/**
158176
* Get the version of the library.
159177
*/

0 commit comments

Comments
 (0)