1use sqlparser::ast;
14
15use nodedb_types::DatabaseId;
16
17use crate::error::{Result, SqlError};
18use crate::parser::statement::parse_sql;
19use crate::planner::cp_projection::to_cp_computed;
20use crate::planner::select::helpers::convert_projection;
21use crate::resolver::columns::{ResolvedTable, TableScope};
22use crate::types::{Projection, SqlCatalog};
23
24pub fn resolve_returning_items(
31 items_sql: &str,
32 target: &str,
33 catalog: &dyn SqlCatalog,
34) -> Result<Vec<Projection>> {
35 let items = parse_items(items_sql)?;
36 let info = catalog
37 .get_collection(DatabaseId::DEFAULT, target)?
38 .ok_or_else(|| SqlError::UnknownTable {
39 name: target.to_string(),
40 })?;
41 let scope = TableScope::single(ResolvedTable {
42 name: target.to_string(),
43 alias: None,
44 info,
45 })?
46 .as_statement_output()
47 .allowing_cp_functions();
48 let projection = convert_projection(&items, &scope)?;
49 Ok(projection.into_iter().map(to_cp_computed).collect())
50}
51
52fn parse_items(items_sql: &str) -> Result<Vec<ast::SelectItem>> {
58 let refuse = || SqlError::Parse {
59 detail: format!("invalid RETURNING list: '{}'", items_sql.trim()),
60 };
61 let mut statements = parse_sql(&format!("SELECT {items_sql}"))?;
62 if statements.len() != 1 {
63 return Err(refuse());
64 }
65 let Some(ast::Statement::Query(query)) = statements.pop() else {
66 return Err(refuse());
67 };
68 if query.with.is_some()
69 || query.order_by.is_some()
70 || query.limit_clause.is_some()
71 || query.fetch.is_some()
72 {
73 return Err(refuse());
74 }
75 let ast::SetExpr::Select(select) = *query.body else {
76 return Err(refuse());
77 };
78 let grouped = match &select.group_by {
79 ast::GroupByExpr::All(_) => true,
80 ast::GroupByExpr::Expressions(exprs, _) => !exprs.is_empty(),
81 };
82 if !select.from.is_empty() || select.selection.is_some() || grouped || select.having.is_some() {
83 return Err(refuse());
84 }
85 Ok(select.projection)
86}
87
88#[cfg(test)]
89mod tests {
90 use super::*;
91 use crate::catalog::SqlCatalogError;
92 use crate::types::{CollectionInfo, ColumnInfo, EngineType, SqlDataType, SqlExpr};
93
94 struct ItemsCatalog;
96
97 impl SqlCatalog for ItemsCatalog {
98 fn get_collection(
99 &self,
100 _database_id: DatabaseId,
101 name: &str,
102 ) -> std::result::Result<Option<CollectionInfo>, SqlCatalogError> {
103 if name != "items" {
104 return Ok(None);
105 }
106 let col = |n: &str, t: SqlDataType| ColumnInfo {
107 name: n.to_string(),
108 data_type: t,
109 nullable: true,
110 is_primary_key: false,
111 default: None,
112 raw_type: None,
113 int_width: None,
114 float_width: None,
115 };
116 Ok(Some(CollectionInfo {
117 name: "items".to_string(),
118 engine: EngineType::DocumentStrict,
119 columns: vec![
120 col("id", SqlDataType::String),
121 col("score", SqlDataType::Int64),
122 ],
123 primary_key: None,
124 has_auto_tier: false,
125 indexes: Vec::new(),
126 bitemporal: false,
127 primary: nodedb_types::PrimaryEngine::Document,
128 vector_primary: None,
129 partition_strategy: nodedb_types::PartitionStrategy::CollectionHomed,
130 open_schema: CollectionInfo::open_schema_for(EngineType::DocumentStrict),
131 }))
132 }
133 }
134
135 fn resolve(items: &str) -> Result<Vec<Projection>> {
136 resolve_returning_items(items, "items", &ItemsCatalog)
137 }
138
139 #[test]
140 fn a_bare_column_stays_a_column() {
141 let projection = resolve("id, score").unwrap();
142 assert_eq!(projection.len(), 2);
143 assert!(matches!(&projection[0], Projection::Column(c) if c == "id"));
144 assert!(matches!(&projection[1], Projection::Column(c) if c == "score"));
145 }
146
147 #[test]
148 fn an_aliased_column_stays_computed_over_the_column() {
149 let projection = resolve("id AS a").unwrap();
150 assert_eq!(projection.len(), 1);
151 match &projection[0] {
152 Projection::Computed { expr, alias } => {
153 assert_eq!(alias, "a");
154 assert!(matches!(expr, SqlExpr::Column { name, .. } if name == "id"));
155 }
156 other => panic!("expected Computed, got {other:?}"),
157 }
158 }
159
160 #[test]
161 fn an_arithmetic_item_is_cp_computed() {
162 let projection = resolve("score * 2 AS d").unwrap();
163 assert_eq!(projection.len(), 1);
164 match &projection[0] {
165 Projection::CpComputed { expr, alias } => {
166 assert_eq!(alias, "d");
167 assert!(matches!(expr, SqlExpr::BinaryOp { .. }));
168 }
169 other => panic!("expected CpComputed, got {other:?}"),
170 }
171 }
172
173 #[test]
174 fn an_unaliased_expression_is_named_by_its_text() {
175 let projection = resolve("score + 1").unwrap();
176 match &projection[0] {
177 Projection::CpComputed { alias, .. } => assert_eq!(alias, "score + 1"),
178 other => panic!("expected CpComputed, got {other:?}"),
179 }
180 }
181
182 #[test]
183 fn a_sequence_accessor_is_cp_computed() {
184 let projection = resolve("id, nextval('s') AS n").unwrap();
185 assert_eq!(projection.len(), 2);
186 match &projection[1] {
187 Projection::CpComputed { expr, alias } => {
188 assert_eq!(alias, "n");
189 assert!(matches!(expr, SqlExpr::Function { name, .. } if name == "nextval"));
190 }
191 other => panic!("expected CpComputed, got {other:?}"),
192 }
193 }
194
195 #[test]
196 fn a_star_passes_through() {
197 let projection = resolve("*").unwrap();
198 assert_eq!(projection.len(), 1);
199 assert!(matches!(projection[0], Projection::Star));
200 }
201
202 #[test]
203 fn an_unknown_column_is_a_resolve_error() {
204 let err = resolve("ghost").unwrap_err();
205 assert!(
206 matches!(err, SqlError::UnknownColumn { ref column, .. } if column == "ghost"),
207 "expected UnknownColumn, got {err:?}"
208 );
209 }
210
211 #[test]
212 fn an_unknown_target_is_an_unknown_table() {
213 let err = resolve_returning_items("id", "ghost", &ItemsCatalog).unwrap_err();
214 assert!(matches!(err, SqlError::UnknownTable { ref name } if name == "ghost"));
215 }
216
217 #[test]
218 fn a_trailing_clause_is_refused() {
219 assert!(resolve("id FROM other").is_err());
220 assert!(resolve("id WHERE score > 1").is_err());
221 assert!(resolve("id ORDER BY id").is_err());
222 assert!(resolve("id; SELECT 1").is_err());
223 assert!(resolve("").is_err());
224 }
225}