1use super::{DOC_ID_COLUMN, TABLE_OID_COLUMN};
10use crate::ast::ReturningAliases;
11use crate::plan::ProjectionPlan;
12use crate::{ColumnIdentity, RowSchema, SQLError, ScalarExpr};
13
14pub fn projection_columns(projections: &[ProjectionPlan]) -> Vec<String> {
15 projections.iter().map(projection_label_at).collect()
16}
17
18pub fn query_output_position(position: i64, width: usize, clause: &str) -> Result<usize, SQLError> {
20 usize::try_from(position)
21 .ok()
22 .and_then(|position| position.checked_sub(1))
23 .filter(|position| *position < width)
24 .ok_or_else(|| output_position_error(clause, position))
25}
26
27pub fn output_position_error(clause: &str, position: i64) -> SQLError {
28 SQLError::Routine {
29 sqlstate: "42P10".into(),
30 message: format!("{clause} position {position} is not in the select list"),
31 }
32}
33
34pub fn projection_label_at(proj: &ProjectionPlan) -> String {
36 if let Some(a) = &proj.alias {
37 return a.clone();
38 }
39 let mut expression = &proj.expr;
40 while let ScalarExpr::Collated { expr, .. } = expression {
41 expression = expr;
42 }
43 match expression {
44 ScalarExpr::Column(c) => c.clone(),
45 ScalarExpr::QualifiedColumn { column, .. } => column.clone(),
46 ScalarExpr::Star | ScalarExpr::QualifiedStar(_) => "*".into(),
47 ScalarExpr::Func {
48 name,
49 binding,
50 args,
51 ..
52 } => field_selection_label(binding.as_ref(), args.get(1))
53 .unwrap_or_else(|| function_projection_label(name, binding.as_ref())),
54 ScalarExpr::WindowCall { name, .. } => function_projection_label(name, None),
55 _ => "?column?".into(),
56 }
57}
58
59pub(crate) fn field_selection_label<E: FieldNameLiteral>(
61 binding: Option<&crate::ast::FunctionBinding>,
62 field: Option<&E>,
63) -> Option<String> {
64 (binding.and_then(|binding| binding.dispatch)
65 == Some(crate::ast::FunctionDispatch::FieldSelect))
66 .then(|| field.and_then(FieldNameLiteral::field_name))
67 .flatten()
68 .map(str::to_owned)
69}
70
71pub(crate) trait FieldNameLiteral {
73 fn field_name(&self) -> Option<&str>;
74}
75
76impl FieldNameLiteral for ScalarExpr {
77 fn field_name(&self) -> Option<&str> {
78 match self {
79 ScalarExpr::Literal(uqa_core::Value::Str(field)) => Some(field),
80 _ => None,
81 }
82 }
83}
84
85impl FieldNameLiteral for crate::ast::Expr {
86 fn field_name(&self) -> Option<&str> {
87 match self {
88 crate::ast::Expr::Literal(uqa_core::Value::Str(field)) => Some(field),
89 _ => None,
90 }
91 }
92}
93
94pub fn function_projection_label(
95 name: &str,
96 binding: Option<&crate::ast::FunctionBinding>,
97) -> String {
98 if matches!(
100 binding.and_then(|binding| binding.dispatch),
101 Some(
102 crate::ast::FunctionDispatch::NumericOperator(_)
103 | crate::ast::FunctionDispatch::Pattern { .. }
104 | crate::ast::FunctionDispatch::Concat { .. }
105 | crate::ast::FunctionDispatch::ArrayConcat
106 | crate::ast::FunctionDispatch::ArrayAppend
107 | crate::ast::FunctionDispatch::ArrayPrepend
108 | crate::ast::FunctionDispatch::IsDistinct
109 | crate::ast::FunctionDispatch::ValueIsDistinct
110 | crate::ast::FunctionDispatch::ValueNullTest { .. }
111 | crate::ast::FunctionDispatch::BooleanTest(_)
112 | crate::ast::FunctionDispatch::AnyOperator
113 | crate::ast::FunctionDispatch::AllOperator
114 | crate::ast::FunctionDispatch::BetweenSymmetric
115 | crate::ast::FunctionDispatch::JsonExtract { .. }
116 )
117 ) {
118 return "?column?".into();
119 }
120 let name = crate::parse_regobject_name(name)
121 .and_then(|mut names| names.pop())
122 .unwrap_or_else(|| name.to_string());
123 if operator_call(&name) {
124 return "?column?".into();
125 }
126 name
127}
128
129fn operator_call(name: &str) -> bool {
131 matches!(
132 name,
133 "fts_match"
134 | "jsonpath_exists"
135 | "contains_op"
136 | "contained_by_op"
137 | "array_overlap"
138 | "json_has_key"
139 | "json_has_any_key"
140 | "json_has_all_keys"
141 | "json_delete_path"
142 )
143}
144
145pub fn expand_from_star_columns(
146 columns: Vec<String>,
147 projections: &[ProjectionPlan],
148 source_schema: &RowSchema,
149) -> Result<Vec<String>, SQLError> {
150 let mut output = Vec::new();
151 for (position, projection) in projections.iter().enumerate() {
152 match &projection.expr {
153 ScalarExpr::Star => {
154 output.extend(
155 source_schema
156 .columns()
157 .iter()
158 .enumerate()
159 .filter(|(position, _)| {
160 visible_projection_source_position(source_schema, *position)
161 })
162 .map(|(source_position, column)| {
163 source_schema
164 .public_name(source_position)
165 .unwrap_or(column)
166 .to_string()
167 }),
168 );
169 }
170 ScalarExpr::QualifiedStar(qualifier) => {
171 let qualified_columns = source_schema
172 .qualified_star_position_layout(qualifier)
173 .into_iter()
174 .filter(|(_, logical, _, _)| {
175 logical.is_none_or(|position| {
176 visible_projection_source_position(source_schema, position)
177 })
178 })
179 .map(|(column, _, _, _)| column)
180 .collect::<Vec<_>>();
181 if qualified_columns.is_empty() {
182 return Err(SQLError::UnknownTable(qualifier.clone()));
183 }
184 output.extend(qualified_columns);
185 }
186 _ => output.push(columns[position].clone()),
187 }
188 }
189 Ok(output)
190}
191
192pub fn bound_projection_expression(schema: &RowSchema, position: usize) -> ScalarExpr {
193 let Some(identity) = schema.identity(position) else {
194 return ScalarExpr::Position(position);
195 };
196 if let Some(qualifier) = identity.qualifier() {
197 if schema.qualified_position(qualifier, identity.column()) == Some(position) {
198 return ScalarExpr::qualified_column(qualifier, identity.column());
199 }
200 } else if schema.unqualified_position(identity.column()) == Some(position) {
201 return ScalarExpr::Column(identity.column().to_string());
202 }
203 ScalarExpr::Position(position)
204}
205
206pub fn expand_bound_projection_stars(
207 projections: &[ProjectionPlan],
208 schema: &RowSchema,
209) -> Result<Vec<ProjectionPlan>, SQLError> {
210 let mut expanded = Vec::new();
211 for projection in projections {
212 match &projection.expr {
213 ScalarExpr::Star => {
214 for (position, column) in schema.columns().iter().enumerate() {
215 if !visible_projection_source_position(schema, position) {
216 continue;
217 }
218 expanded.push(ProjectionPlan {
219 expr: bound_projection_expression(schema, position),
220 alias: Some(schema.public_name(position).unwrap_or(column).to_string()),
221 });
222 }
223 }
224 ScalarExpr::QualifiedStar(qualifier) => {
225 let layout = schema.qualified_star_position_layout(qualifier);
226 if layout.is_empty() {
227 return Err(SQLError::UnknownTable(qualifier.clone()));
228 }
229 for (column, logical, _, _) in layout {
230 if logical.is_some_and(|position| {
231 !visible_projection_source_position(schema, position)
232 }) {
233 continue;
234 }
235 expanded.push(ProjectionPlan {
236 expr: logical.map_or_else(
237 || ScalarExpr::qualified_column(qualifier, &column),
238 |position| bound_projection_expression(schema, position),
239 ),
240 alias: Some(column),
241 });
242 }
243 }
244 _ => expanded.push(projection.clone()),
245 }
246 }
247 Ok(expanded)
248}
249
250pub fn visible_projection_source_position(schema: &RowSchema, position: usize) -> bool {
251 schema.wildcard_position_visible(position)
252}
253
254pub fn returning_context_schema(
255 columns: &[String],
256 types: &[Option<crate::ast::ColumnType>],
257 composite_width: usize,
258 target_qualifier: &str,
259 aliases: &ReturningAliases,
260) -> RowSchema {
261 let target =
262 RowSchema::with_qualified_types(target_qualifier, columns.to_vec(), types.to_vec());
263 returning_schema_for_target(target, composite_width, aliases)
264}
265
266pub fn returning_schema_for_target(
268 target: RowSchema,
269 composite_width: usize,
270 aliases: &ReturningAliases,
271) -> RowSchema {
272 let width = target.len();
273 assert_eq!(
274 target.physical_width(),
275 width,
276 "RETURNING target must be positional"
277 );
278 assert!(
279 composite_width <= width,
280 "RETURNING user columns must belong to the target"
281 );
282 debug_assert!((0..width).all(|position| target.slot(position) == Some(position)));
283 let target = RowSchema::with_wildcard_hidden_positions(&target, composite_width..width);
284 let types = target.column_types();
285 let hidden_types = types
286 .iter()
287 .cloned()
288 .chain(types.iter().cloned())
289 .collect::<Vec<_>>();
290 let schema = RowSchema::append_hidden_typed(&target, &hidden_types);
291 let identity_aliases = target
292 .columns()
293 .iter()
294 .enumerate()
295 .flat_map(|(position, column)| {
296 [
297 (
298 ColumnIdentity::qualified(&aliases.old, column),
299 width + position,
300 types[position].clone(),
301 ),
302 (
303 ColumnIdentity::qualified(&aliases.new, column),
304 width * 2 + position,
305 types[position].clone(),
306 ),
307 ]
308 })
309 .collect::<Vec<_>>();
310 let mut schema = RowSchema::with_physical_identity_aliases(&schema, &identity_aliases);
311 for (binding, oid) in target.collation_overrides() {
312 let crate::schema::CollationBinding::Column(position) = binding else {
313 continue;
314 };
315 for qualifier in [&aliases.old, &aliases.new] {
316 let identity = ColumnIdentity::qualified(qualifier, &target.columns()[position]);
317 schema = schema
318 .with_collation_override(crate::schema::CollationBinding::Alias(&identity), oid)
319 .expect("RETURNING image alias retains its target's type");
320 }
321 }
322 schema
323}
324
325pub fn returning_expression_schema(
326 target: &RowSchema,
327 target_qualifier: &str,
328 aliases: &ReturningAliases,
329 supplemental: Option<&RowSchema>,
330) -> RowSchema {
331 let composite_width = target.len();
332 let (columns, types) = target_with_system_columns(target);
333 let width = columns.len();
334 let schema = RowSchema::with_qualified_types(target_qualifier, columns, types)
335 .with_column_collations((0..width).map(|position| target.column_collation(position)));
336 let target = returning_schema_for_target(schema, composite_width, aliases);
337 supplemental.map_or(target.clone(), |source| {
338 RowSchema::join(&target, source, std::iter::empty())
339 })
340}
341
342#[must_use]
344pub fn mutation_clause_schema(
345 target: &RowSchema,
346 target_qualifier: &str,
347 supplemental: Option<&RowSchema>,
348) -> RowSchema {
349 let composite_width = target.len();
350 let (columns, types) = target_with_system_columns(target);
351 let width = columns.len();
352 let target = RowSchema::with_wildcard_hidden_positions(
353 &RowSchema::with_qualified_types(target_qualifier, columns, types)
354 .with_column_collations((0..width).map(|position| target.column_collation(position))),
355 composite_width..width,
356 );
357 supplemental.map_or(target.clone(), |source| {
358 RowSchema::join(&target, source, std::iter::empty())
359 })
360}
361
362fn target_with_system_columns(
364 target: &RowSchema,
365) -> (Vec<String>, Vec<Option<crate::ast::ColumnType>>) {
366 let mut columns = target.columns().to_vec();
367 let mut types = target.column_types().to_vec();
368 if !columns.iter().any(|column| column == DOC_ID_COLUMN) {
369 columns.push(DOC_ID_COLUMN.into());
370 types.push(Some(crate::ast::ColumnType::BigInteger));
371 }
372 columns.push(TABLE_OID_COLUMN.into());
373 types.push(Some(crate::ast::ColumnType::Oid));
374 columns.push(crate::semantics::XMIN_COLUMN.into());
375 types.push(Some(crate::ast::ColumnType::Xid));
376 (columns, types)
377}
378
379pub fn query_plan_output_columns(plan: &crate::plan::QueryPlan) -> Option<Vec<String>> {
380 match &plan.root {
381 crate::plan::RelationalPlan::QueryBlock(block) => {
382 Some(projection_columns(&block.projections))
383 }
384 crate::plan::RelationalPlan::SetOp { left, .. } => query_plan_output_columns(left),
385 crate::plan::RelationalPlan::Values { rows, .. } => rows.first().map(|row| {
386 (1..=row.len())
387 .map(|index| format!("column{index}"))
388 .collect()
389 }),
390 }
391}
392
393pub fn should_defer_distinct_limit(stmt: &crate::plan::QueryBlockPlan) -> bool {
394 stmt.distinct && (stmt.limit.is_some() || stmt.offset.is_some())
395}
396
397pub fn select_execution_stmt(
398 stmt: &crate::plan::QueryBlockPlan,
399 defer_distinct_limit: bool,
400) -> crate::plan::QueryBlockPlan {
401 if !defer_distinct_limit {
402 return stmt.clone();
403 }
404 let mut exec_stmt = stmt.clone();
405 exec_stmt.limit = None;
406 exec_stmt.offset = None;
407 exec_stmt
408}