1use super::returning::expanded_returning_projections;
9use crate::{
10 plan::{MergePlan, MergeWhenPlan, ProjectionPlan},
11 SQLError, SQLParam,
12};
13use std::collections::BTreeSet;
14
15#[expect(
16 clippy::too_many_lines,
17 reason = "validates clause-specific schemas and FULL JOIN requirements"
18)]
19pub fn validate_merge_action_scopes(
20 routines: &dyn crate::routines::RoutineResolution,
21 stmt: &MergePlan,
22 target_schema: &crate::RowSchema,
23 source_schema: &crate::RowSchema,
24 params: &[SQLParam],
25 bindings: &crate::binding::context::BindingContext<'_>,
26) -> Result<(), SQLError> {
27 let matched_schema = crate::RowSchema::join(target_schema, source_schema, std::iter::empty());
28 let expression_type = |expression: &crate::ScalarExpr, schema: &crate::RowSchema| {
29 crate::binding::analyze_projection_output_schema(
30 routines,
31 &[ProjectionPlan {
32 expr: expression.clone(),
33 alias: None,
34 }],
35 schema,
36 schema,
37 &stmt.subqueries,
38 params,
39 bindings,
40 )
41 .map(|output| output.column_type(0).cloned())
42 };
43 let validate_boolean = |expression: &crate::ScalarExpr,
44 schema: &crate::RowSchema,
45 label: &str|
46 -> Result<(), SQLError> {
47 if expression_type(expression, schema)?
48 .is_some_and(|ty| ty != crate::ast::ColumnType::Boolean)
49 {
50 return Err(SQLError::TypeMismatch(format!(
51 "argument of {label} must be type boolean"
52 )));
53 }
54 Ok(())
55 };
56 validate_boolean(&stmt.join_condition, &matched_schema, "MERGE ON")?;
57 let has_source_missing = stmt.when_clauses.iter().any(|clause| {
58 matches!(
59 clause,
60 MergeWhenPlan::UpdateNotMatchedBySource { .. }
61 | MergeWhenPlan::DeleteNotMatchedBySource { .. }
62 | MergeWhenPlan::NothingNotMatchedBySource { .. }
63 )
64 });
65 let has_target_missing = stmt.when_clauses.iter().any(|clause| {
66 matches!(
67 clause,
68 MergeWhenPlan::InsertNotMatched { .. } | MergeWhenPlan::NothingNotMatched { .. }
69 )
70 });
71 if has_source_missing
72 && has_target_missing
73 && !super::join_predicates::join_conjuncts(&stmt.join_condition)
74 .into_iter()
75 .any(|conjunct| {
76 matches!(
77 conjunct,
78 crate::ScalarExpr::Binary {
79 op: crate::ast::BinaryOp::Equal,
80 lhs,
81 rhs,
82 } if super::join_predicates::decide_join_sides(
83 target_schema,
84 source_schema,
85 lhs,
86 rhs,
87 )
88 .is_some()
89 )
90 })
91 {
92 return Err(SQLError::Routine {
93 sqlstate: "0A000".into(),
94 message:
95 "FULL JOIN is only supported with merge-joinable or hash-joinable join conditions"
96 .into(),
97 });
98 }
99 for clause in &stmt.when_clauses {
100 let (condition, expressions, schema): (
101 Option<&crate::ScalarExpr>,
102 Vec<&crate::ScalarExpr>,
103 &crate::RowSchema,
104 ) = match clause {
105 MergeWhenPlan::UpdateMatched {
106 condition,
107 assignments,
108 } => (
109 condition.as_ref(),
110 assignments
111 .iter()
112 .flat_map(crate::plan::AssignmentPlan::expressions)
113 .collect(),
114 &matched_schema,
115 ),
116 MergeWhenPlan::DeleteMatched { condition }
117 | MergeWhenPlan::NothingMatched { condition } => {
118 (condition.as_ref(), Vec::new(), &matched_schema)
119 }
120 MergeWhenPlan::UpdateNotMatchedBySource {
121 condition,
122 assignments,
123 } => (
124 condition.as_ref(),
125 assignments
126 .iter()
127 .flat_map(crate::plan::AssignmentPlan::expressions)
128 .collect(),
129 target_schema,
130 ),
131 MergeWhenPlan::DeleteNotMatchedBySource { condition }
132 | MergeWhenPlan::NothingNotMatchedBySource { condition } => {
133 (condition.as_ref(), Vec::new(), target_schema)
134 }
135 MergeWhenPlan::InsertNotMatched {
136 condition,
137 columns,
138 values,
139 ..
140 } => (
141 condition.as_ref(),
142 columns
143 .iter()
144 .flat_map(crate::ast::AssignmentTarget::expressions)
145 .chain(values)
146 .collect(),
147 source_schema,
148 ),
149 MergeWhenPlan::NothingNotMatched { condition } => {
150 (condition.as_ref(), Vec::new(), source_schema)
151 }
152 };
153 if let Some(condition) = condition {
154 validate_boolean(condition, schema, "WHEN")?;
155 }
156 for expression in expressions {
157 expression_type(expression, schema)?;
158 }
159 }
160 Ok(())
161}
162
163pub fn expanded_merge_returning_projections(
164 catalog: &dyn super::returning::ReturningCatalog,
165 target_table: &str,
166 target_qualifier: &str,
167 aliases: &crate::ast::ReturningAliases,
168 source_schema: &crate::RowSchema,
169 source_relation: crate::ast::InternalRelationId,
170 returning: &[ProjectionPlan],
171) -> Result<Vec<ProjectionPlan>, SQLError> {
172 let target_star = ProjectionPlan {
173 expr: crate::ScalarExpr::QualifiedStar(target_qualifier.into()),
174 alias: None,
175 };
176 let target_projections = expanded_returning_projections(
177 catalog,
178 target_table,
179 target_qualifier,
180 aliases,
181 std::slice::from_ref(&target_star),
182 )?;
183 let mut projections = Vec::new();
184 for projection in returning {
185 match &projection.expr {
186 crate::ScalarExpr::Star => {
187 projections.extend(
188 source_schema
189 .columns()
190 .iter()
191 .enumerate()
192 .filter(|(position, _)| {
193 super::projection::visible_projection_source_position(
194 source_schema,
195 *position,
196 )
197 })
198 .map(|(position, column)| ProjectionPlan {
199 expr: crate::ScalarExpr::InternalColumn(
200 source_relation.column(position),
201 ),
202 alias: Some(
203 source_schema
204 .public_name(position)
205 .unwrap_or(column)
206 .to_string(),
207 ),
208 }),
209 );
210 projections.extend(target_projections.iter().cloned());
211 }
212 crate::ScalarExpr::QualifiedStar(qualifier)
213 if qualifier == target_qualifier
214 || qualifier == &aliases.old
215 || qualifier == &aliases.new =>
216 {
217 projections.extend(expanded_returning_projections(
218 catalog,
219 target_table,
220 target_qualifier,
221 aliases,
222 std::slice::from_ref(projection),
223 )?);
224 }
225 _ => projections.push(projection.clone()),
226 }
227 }
228 Ok(projections)
229}
230
231pub fn merge_returning_source_schema(
232 source_schema: &crate::RowSchema,
233 source_relation: crate::ast::InternalRelationId,
234) -> crate::RowSchema {
235 let aliases = source_schema
236 .columns()
237 .iter()
238 .enumerate()
239 .map(|(position, _)| {
240 (
241 source_relation.column(position),
242 source_schema
243 .physical_slot(position)
244 .expect("source column has a physical slot"),
245 source_schema.column_type(position).cloned(),
246 )
247 })
248 .collect::<Vec<_>>();
249 crate::RowSchema::with_physical_internal_aliases(source_schema, &aliases)
250}
251
252pub fn merge_target_columns(
254 catalog: &dyn super::mutation_privileges::MutationPrivilegeCatalog,
255 stmt: &MergePlan,
256) -> Result<Vec<String>, SQLError> {
257 let table_columns = catalog.bound_table_column_names(&stmt.target)?;
258 Ok(merge_supplied_columns(stmt, &table_columns))
259}
260
261pub fn merge_supplied_columns(stmt: &MergePlan, target_columns: &[String]) -> Vec<String> {
263 let mut supplied = Vec::new();
264 for clause in &stmt.when_clauses {
265 match clause {
266 MergeWhenPlan::InsertNotMatched {
267 columns, values, ..
268 } => supplied.extend(merge_insert_columns(target_columns, columns, values)),
269 MergeWhenPlan::UpdateMatched { assignments, .. }
270 | MergeWhenPlan::UpdateNotMatchedBySource { assignments, .. } => {
271 supplied.extend(
272 assignments
273 .iter()
274 .flat_map(|assignment| assignment.target.column_names())
275 .map(str::to_owned),
276 );
277 }
278 _ => {}
279 }
280 }
281 supplied
282}
283
284fn merge_insert_columns<T, V>(
286 table_columns: &[String],
287 columns: &[crate::ast::AssignmentTarget<T>],
288 values: &[V],
289) -> Vec<String> {
290 if columns.is_empty() {
291 table_columns.iter().take(values.len()).cloned().collect()
292 } else {
293 columns.iter().map(|target| target.column.clone()).collect()
294 }
295}
296
297pub fn ensure_merge_mutation_privileges(
298 catalog: &dyn super::mutation_privileges::MutationPrivilegeCatalog,
299 stmt: &MergePlan,
300) -> Result<(), SQLError> {
301 let mut column_privileges = BTreeSet::new();
302 let mut requires_delete = false;
303 let mut requires_any_insert = false;
304 let table_columns = catalog.bound_table_column_names(&stmt.target)?;
305 let privilege_subject = stmt
306 .target_privilege_subject
307 .clone()
308 .unwrap_or_else(|| catalog.current_role());
309 for clause in &stmt.when_clauses {
310 match clause {
311 MergeWhenPlan::InsertNotMatched {
312 columns, values, ..
313 } => {
314 if columns.is_empty() && values.is_empty() {
315 requires_any_insert = true;
316 } else {
317 let columns = merge_insert_columns(&table_columns, columns, values);
318 column_privileges.extend(columns.into_iter().map(|column| {
319 (
320 crate::catalog::security::table::TableAclPrivilege::Insert,
321 column,
322 )
323 }));
324 }
325 }
326 MergeWhenPlan::UpdateMatched { assignments, .. }
327 | MergeWhenPlan::UpdateNotMatchedBySource { assignments, .. } => {
328 column_privileges.extend(
329 assignments
330 .iter()
331 .flat_map(|assignment| assignment.target.targets())
332 .map(|target| {
333 (
334 crate::catalog::security::table::TableAclPrivilege::Update,
335 target.column.clone(),
336 )
337 }),
338 );
339 }
340 MergeWhenPlan::DeleteMatched { .. }
341 | MergeWhenPlan::DeleteNotMatchedBySource { .. } => requires_delete = true,
342 _ => {}
343 }
344 }
345 if requires_delete {
346 catalog.ensure_table_privilege_for(
347 &stmt.target,
348 &privilege_subject,
349 crate::catalog::security::table::TableAclPrivilege::Delete,
350 )?;
351 }
352 if requires_any_insert {
353 catalog.ensure_any_column_privilege_for(
354 &stmt.target,
355 &privilege_subject,
356 crate::catalog::security::table::TableAclPrivilege::Insert,
357 )?;
358 }
359 for (privilege, column) in column_privileges {
360 catalog.ensure_column_privilege_for(
361 &stmt.target,
362 &column,
363 &privilege_subject,
364 privilege,
365 )?;
366 }
367 Ok(())
368}
369
370pub fn merge_returning_schema(
371 routines: &dyn crate::routines::RoutineResolution,
372 catalog: &dyn super::returning::ReturningCatalog,
373 stmt: &MergePlan,
374 params: &[SQLParam],
375 source_schema: &crate::RowSchema,
376 ctes: &crate::binding::context::BindingContext<'_>,
377) -> Result<Option<crate::RowSchema>, SQLError> {
378 if stmt.returning.is_empty() {
379 return Ok(None);
380 }
381 let source_relation = crate::ast::InternalRelationId::allocate();
382 let projections = expanded_merge_returning_projections(
383 catalog,
384 &stmt.target,
385 &stmt.target_qualifier,
386 &stmt.returning_aliases,
387 source_schema,
388 source_relation,
389 &stmt.returning,
390 )?;
391 let returning_source_schema = merge_returning_source_schema(source_schema, source_relation);
392 let star_schema = super::returning::returning_target_schema(catalog, &stmt.target)?;
393 let expression_schema = super::returning_expression_schema(
394 &star_schema,
395 &stmt.target_qualifier,
396 &stmt.returning_aliases,
397 Some(&returning_source_schema),
398 );
399 crate::binding::analyze_projection_output_schema(
400 routines,
401 &projections,
402 &expression_schema,
403 &star_schema,
404 &stmt.subqueries,
405 params,
406 ctes,
407 )
408 .map(Some)
409}
410
411pub fn merge_command_returning_schema(
412 routines: &dyn crate::routines::RoutineResolution,
413 catalog: &dyn super::returning::ReturningCatalog,
414 rows: &dyn super::mutation_rows::MutationRowCatalog,
415 stmt: &MergePlan,
416 params: &[SQLParam],
417 bindings: &crate::binding::context::BindingContext<'_>,
418) -> Result<Option<crate::RowSchema>, SQLError> {
419 if stmt.returning.is_empty() {
420 return Ok(None);
421 }
422 let source =
423 crate::binding::analyze_source_plan_schema(routines, &stmt.source, params, bindings, None)?;
424 super::returning::validate_returning_alias_relations(
425 &stmt.target_qualifier,
426 &stmt.returning_aliases,
427 Some(&source),
428 )?;
429 let target =
430 super::mutation_rows::null_target_schema(rows, &stmt.target, &stmt.target_qualifier)?;
431 validate_merge_action_scopes(routines, stmt, &target, &source, params, bindings)?;
432 merge_returning_schema(routines, catalog, stmt, params, &source, bindings)
433}
434
435pub fn validate_merge_target_columns(
436 catalog: &dyn crate::assignment::columns::AssignmentColumnCatalog,
437 stmt: &MergePlan,
438) -> Result<(), SQLError> {
439 use crate::assignment::columns::validate_mutation_targets;
440 for clause in &stmt.when_clauses {
441 match clause {
442 MergeWhenPlan::UpdateMatched { assignments, .. }
443 | MergeWhenPlan::UpdateNotMatchedBySource { assignments, .. } => {
444 validate_mutation_targets(
445 catalog,
446 &stmt.target,
447 assignments
448 .iter()
449 .flat_map(|assignment| assignment.target.targets()),
450 "MERGE UPDATE",
451 false,
452 )?;
453 }
454 MergeWhenPlan::InsertNotMatched { columns, .. } => validate_mutation_targets(
455 catalog,
456 &stmt.target,
457 columns.iter(),
458 "MERGE INSERT",
459 true,
460 )?,
461 _ => {}
462 }
463 }
464 validate_merge_identity_targets(catalog, stmt)
465}
466
467fn validate_merge_identity_targets(
469 catalog: &dyn crate::assignment::columns::AssignmentColumnCatalog,
470 stmt: &MergePlan,
471) -> Result<(), SQLError> {
472 let identity = super::generated_values::GeneratedValueColumns::of(catalog, &stmt.target)?;
473 for clause in &stmt.when_clauses {
474 match clause {
475 MergeWhenPlan::UpdateMatched { assignments, .. }
476 | MergeWhenPlan::UpdateNotMatchedBySource { assignments, .. } => {
477 identity.validate_update(assignments.iter().flat_map(|assignment| {
478 assignment.target.targets().iter().map(move |target| {
479 (
480 target.column.as_str(),
481 matches!(assignment.value, crate::ScalarExpr::Default),
482 )
483 })
484 }))?;
485 }
486 MergeWhenPlan::InsertNotMatched {
487 columns,
488 overriding,
489 values,
490 ..
491 } => {
492 let targets = if columns.is_empty() {
493 catalog
494 .try_describe_table(&stmt.target)
495 .map_err(|error| {
496 SQLError::Internal(format!("read MERGE target columns: {error}"))
497 })?
498 .unwrap_or_default()
499 .into_iter()
500 .map(|column| column.name)
501 .collect::<Vec<_>>()
502 } else {
503 columns.iter().map(|target| target.column.clone()).collect()
504 };
505 identity.validate_insert(
506 targets.iter().map(String::as_str).zip(
507 values
508 .iter()
509 .map(|value| !matches!(value, crate::ScalarExpr::Default)),
510 ),
511 *overriding,
512 )?;
513 }
514 _ => {}
515 }
516 }
517 Ok(())
518}