1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
//! Query execution entry points for an embedded session.
//!
//! `execute*` routes transaction / session commands, `CREATE ... AS`, and
//! text-begun transactions to their dedicated handlers in `transactions` /
//! `create_as`, then drives the shared `QueryApi` for ordinary statements.
use super::Session;
use crate::api_core::session_command::classify_session_command;
use crate::api_core::types::QueryResult as CoreQueryResult;
use crate::api_core::{CoreError, CoreResult};
use crate::embedded::result::QueryResult;
use linkrs_core::types::DataSet;
use linkrs_core::Value;
use linkrs_query::executor::base::ExecutionResult;
use linkrs_query::parser::ast::stmt::AssignVariableStmt;
use linkrs_query::parser::ast::stmt::Ast;
use linkrs_query::parser::ast::Stmt;
use linkrs_query::ParserResult;
use linkrs_storage::StorageClient;
use linkrs_transaction::TransactionId;
use std::collections::HashMap;
use std::sync::Arc;
impl<S: StorageClient + Clone + Send + Sync + 'static + linkrs_storage::UndoTarget> Session<S> {
/// Execute the query statement.
///
/// # Parameters
/// `query` – A string representing the query statement.
///
/// # Back
/// Return the query results when successful.
/// - Return error on failure
pub fn execute(&self, query: &str) -> CoreResult<QueryResult> {
self.ensure_not_interrupted()?;
self.ensure_writable(query)?;
// Reset the previous change history
self.statistics.reset_last();
// Transaction / session commands are classified through the unified
// parser entry: the six transaction commands perform the
// Transaction / session commands are classified through the shared
// API-layer gate: the six transaction commands perform the
// TransactionManager side effect and execute the state-machine plan;
// `LET` assigns a session variable.
match Self::parse_command(query) {
Err(error) => return Err(error),
Ok(Some(parsed)) => {
let parsed_ast = parsed.ast;
match parsed_ast.stmt() {
Stmt::AssignVariable(assign) => {
return self.execute_variable_assignment(
query,
parsed_ast.clone(),
assign,
None,
None,
);
}
Stmt::Extension(extension) => {
return self.execute_extension(extension);
}
stmt => {
return self.execute_transaction_command(query, stmt, parsed_ast.clone());
}
}
}
Ok(None) => {}
}
// `CREATE TAG/EDGE ... AS (query)` materializes the inner query
// result as a new table through the session pipeline.
if let Some(create_stmt) = Self::parse_create_as(query) {
return self.execute_create_as(&create_stmt);
}
// Statements inside a text-begun transaction run against the
// transaction binding (mirroring the `Transaction` handle API).
if let Some(txn_id) = *self.current_transaction.read() {
let result = self.execute_in_transaction(query, txn_id, None);
self.statistics.record_changes(
result
.as_ref()
.map(|r| r.metadata().rows_returned as u64)
.unwrap_or(0),
);
return result;
}
let ctx = self.session_query_request().build();
let mut query_api = self.db.query_api.write();
let result = if self.auto_commit {
let storage = self
.db
.storage
.read()
.bind_auto_commit_context()
.map_err(|error| CoreError::StorageError(error.to_string()))?;
query_api.execute_with_operation_storage(query, ctx, storage)?
} else {
query_api.execute(query, ctx)?
};
Self::reject_server_only_config_intent(&result)?;
// Update statistical information
self.statistics
.record_changes(result.metadata.rows_returned);
// Detect USE <space> results and persist space context
self.update_space_from_result(&result);
self.notify_dml(query, result.metadata.rows_returned);
Ok(QueryResult::from_core(result))
}
/// Unified classification entry shared with the network service layer:
/// the keyword gate, parse, and AST match live in
/// [`crate::api_core::session_command`], so both transports classify
/// identically. Returns the parsed statement when it is one of the
/// transaction / session commands.
fn parse_command(query: &str) -> CoreResult<Option<ParserResult>> {
classify_session_command(query)
.map(|classified| classified.map(|(parsed, _)| parsed))
.map_err(CoreError::from)
}
/// Execute a parameterized query
///
/// # Parameters
/// - `query` - query statement string
/// - `params` – Query parameters
///
/// # Return
/// - Returns query results on success
/// - Return error on failure
pub fn execute_with_params(
&self,
query: &str,
params: HashMap<String, Value>,
) -> CoreResult<QueryResult> {
self.ensure_not_interrupted()?;
self.ensure_writable(query)?;
// Transaction / session commands do not consume query parameters;
// classify and route them through the unified command path.
match Self::parse_command(query) {
Err(error) => return Err(error),
Ok(Some(parsed)) => {
let parsed_ast = parsed.ast;
match parsed_ast.stmt() {
Stmt::AssignVariable(assign) => {
return self.execute_variable_assignment(
query,
parsed_ast.clone(),
assign,
Some(params),
None,
);
}
Stmt::Extension(extension) => {
return self.execute_extension(extension);
}
stmt => {
return self.execute_transaction_command(query, stmt, parsed_ast.clone());
}
}
}
Ok(None) => {}
}
// Statements inside a text-begun transaction run against the
// transaction binding.
if let Some(txn_id) = *self.current_transaction.read() {
return self.execute_in_transaction(query, txn_id, Some(params));
}
let ctx = self
.session_query_request()
.parameters(Some(params))
.build();
let mut query_api = self.db.query_api.write();
let result = if self.auto_commit {
let storage = self
.db
.storage
.read()
.bind_auto_commit_context()
.map_err(|error| CoreError::StorageError(error.to_string()))?;
query_api.execute_with_operation_storage(query, ctx, storage)?
} else {
query_api.execute(query, ctx)?
};
Self::reject_server_only_config_intent(&result)?;
// Detect USE <space> results and persist space context
self.update_space_from_result(&result);
self.notify_dml(query, result.metadata.rows_returned);
Ok(QueryResult::from_core(result))
}
/// Execute a query with both query parameters (`@name` references) and
/// session variables (`$name` references).
///
/// The two channels are fully independent: a parameter and a session
/// variable with the same name coexist without conflict.
pub fn execute_with_params_and_variables(
&self,
query: &str,
params: HashMap<String, Value>,
session_variables: HashMap<String, Value>,
) -> CoreResult<QueryResult> {
self.ensure_not_interrupted()?;
self.ensure_writable(query)?;
// Transaction / session commands do not consume parameters or
// session variables; classify and route them through the unified
// command path.
match Self::parse_command(query) {
Err(error) => return Err(error),
Ok(Some(parsed)) => {
let parsed_ast = parsed.ast;
match parsed_ast.stmt() {
Stmt::AssignVariable(assign) => {
return self.execute_variable_assignment(
query,
parsed_ast.clone(),
assign,
Some(params),
None,
);
}
Stmt::Extension(extension) => {
return self.execute_extension(extension);
}
stmt => {
return self.execute_transaction_command(query, stmt, parsed_ast.clone());
}
}
}
Ok(None) => {}
}
// Statements inside a text-begun transaction run against the
// transaction binding.
if let Some(txn_id) = *self.current_transaction.read() {
return self.execute_in_transaction(query, txn_id, Some(params));
}
let ctx = self
.session_query_request()
.parameters(Some(params))
.session_variables(Some(session_variables))
.build();
let mut query_api = self.db.query_api.write();
let result = if self.auto_commit {
let storage = self
.db
.storage
.read()
.bind_auto_commit_context()
.map_err(|error| CoreError::StorageError(error.to_string()))?;
query_api.execute_with_operation_storage(query, ctx, storage)?
} else {
query_api.execute(query, ctx)?
};
Self::reject_server_only_config_intent(&result)?;
// Detect USE <space> results and persist space context
self.update_space_from_result(&result);
self.notify_dml(query, result.metadata.rows_returned);
Ok(QueryResult::from_core(result))
}
/// Execute a `LET $name = expr` session-variable assignment.
///
/// The right-hand side is evaluated through the query engine (the LET
/// statement plans to a single-row, single-column expression evaluation);
/// the value is stored on the session variable store. Inside a text-begun
/// transaction the assignment is recorded on the overlay so ROLLBACK /
/// ROLLBACK TO SAVEPOINT restore the previous value. Client-supplied
/// parameters and session variables are passed through to the evaluation.
pub(crate) fn execute_variable_assignment(
&self,
query: &str,
parsed_ast: Arc<Ast>,
assign: &AssignVariableStmt,
parameters: Option<HashMap<String, Value>>,
session_variables: Option<HashMap<String, Value>>,
) -> CoreResult<QueryResult> {
// Client-supplied session variables override only the keys they name;
// all other keys keep the session snapshot.
let merged_variables = match session_variables {
Some(client_variables) => {
let mut merged = self.variables_snapshot();
merged.extend(client_variables);
Some(merged)
}
None => Some(self.variables_snapshot()),
};
let ctx = self
.session_query_request()
.transaction_id(*self.current_transaction.read())
.parameters(parameters)
.session_variables(merged_variables)
.parsed_statement(Some(parsed_ast))
.build();
let mut query_api = self.db.query_api.write();
let result = if let Some(txn_id) = *self.current_transaction.read() {
let txn_manager = self.txn_manager();
let (statement_ctx, statement_start) = txn_manager
.begin_statement(txn_id)
.map_err(|e| CoreError::TransactionFailed(e.to_string()))?;
let execution = txn_manager
.create_execution(txn_id, false)
.map_err(|e| CoreError::TransactionFailed(e.to_string()))?;
let result = query_api.execute_with_execution(query, ctx, &execution)?;
txn_manager
.finish_statement(&statement_ctx, statement_start)
.map_err(|e| CoreError::TransactionFailed(e.to_string()))?;
result
} else if self.auto_commit {
let storage = self
.db
.storage
.read()
.bind_auto_commit_context()
.map_err(|error| CoreError::StorageError(error.to_string()))?;
query_api.execute_with_operation_storage(query, ctx, storage)?
} else {
query_api.execute(query, ctx)?
};
self.statistics
.record_changes(result.metadata.rows_returned);
// Contract: the LET plan evaluates to exactly one row with one value
// column. Guard instead of silently taking the first value if a
// planner regression changes the shape.
if result.rows().len() != 1 {
return Err(CoreError::InvalidParameter(format!(
"LET expression must evaluate to a single row, got {} rows",
result.rows().len()
)));
}
if result.columns().len() != 1 {
return Err(CoreError::InvalidParameter(format!(
"LET expression must evaluate to a single value, got {} columns",
result.columns().len()
)));
}
let columns = result.columns().to_vec();
let rows = result.rows().to_vec();
let row = rows.first().ok_or_else(|| {
CoreError::InvalidParameter("LET expression returned no value".to_string())
})?;
let value = row.first().cloned().ok_or_else(|| {
CoreError::InvalidParameter("LET expression returned no value".to_string())
})?;
self.set_variable(assign.name.clone(), value);
Ok(QueryResult::from_core(CoreQueryResult::new(
ExecutionResult::from_data_set(DataSet::from_rows(rows, columns)),
result.metadata,
)))
}
}