1#![allow(
7 dead_code,
8 reason = "0.155 stages accepted-catalog DDL binding before execution is enabled"
9)]
10
11use crate::db::{
12 schema::{
13 AcceptedSchemaSnapshot, PersistedIndexKeySnapshot, PersistedIndexSnapshot,
14 SchemaDdlAcceptedSnapshotDerivation, SchemaDdlMutationAdmission,
15 SchemaDdlMutationAdmissionError, SchemaInfo, admit_sql_ddl_field_path_index_candidate,
16 derive_sql_ddl_field_path_index_accepted_after,
17 },
18 sql::{
19 identifier::identifiers_tail_match,
20 parser::{SqlCreateIndexStatement, SqlDdlStatement, SqlStatement},
21 },
22};
23use thiserror::Error as ThisError;
24
25#[derive(Clone, Debug, Eq, PartialEq)]
33pub(in crate::db) struct PreparedSqlDdlCommand {
34 bound: BoundSqlDdlRequest,
35 derivation: SchemaDdlAcceptedSnapshotDerivation,
36 report: SqlDdlPreparationReport,
37}
38
39impl PreparedSqlDdlCommand {
40 #[must_use]
42 pub(in crate::db) const fn bound(&self) -> &BoundSqlDdlRequest {
43 &self.bound
44 }
45
46 #[must_use]
48 pub(in crate::db) const fn derivation(&self) -> &SchemaDdlAcceptedSnapshotDerivation {
49 &self.derivation
50 }
51
52 #[must_use]
54 pub(in crate::db) const fn report(&self) -> &SqlDdlPreparationReport {
55 &self.report
56 }
57}
58
59#[derive(Clone, Debug, Eq, PartialEq)]
66pub struct SqlDdlPreparationReport {
67 mutation_kind: SqlDdlMutationKind,
68 target_index: String,
69 target_store: String,
70 field_path: Vec<String>,
71 execution_status: SqlDdlExecutionStatus,
72}
73
74impl SqlDdlPreparationReport {
75 #[must_use]
77 pub const fn mutation_kind(&self) -> SqlDdlMutationKind {
78 self.mutation_kind
79 }
80
81 #[must_use]
83 pub const fn target_index(&self) -> &str {
84 self.target_index.as_str()
85 }
86
87 #[must_use]
89 pub const fn target_store(&self) -> &str {
90 self.target_store.as_str()
91 }
92
93 #[must_use]
95 pub const fn field_path(&self) -> &[String] {
96 self.field_path.as_slice()
97 }
98
99 #[must_use]
101 pub const fn execution_status(&self) -> SqlDdlExecutionStatus {
102 self.execution_status
103 }
104
105 pub(in crate::db) const fn with_execution_status(
106 mut self,
107 execution_status: SqlDdlExecutionStatus,
108 ) -> Self {
109 self.execution_status = execution_status;
110 self
111 }
112}
113
114#[derive(Clone, Copy, Debug, Eq, PartialEq)]
120pub enum SqlDdlMutationKind {
121 AddNonUniqueFieldPathIndex,
122}
123
124impl SqlDdlMutationKind {
125 #[must_use]
127 pub const fn as_str(self) -> &'static str {
128 match self {
129 Self::AddNonUniqueFieldPathIndex => "add_non_unique_field_path_index",
130 }
131 }
132}
133
134#[derive(Clone, Copy, Debug, Eq, PartialEq)]
140pub enum SqlDdlExecutionStatus {
141 PreparedOnly,
142 Published,
143}
144
145impl SqlDdlExecutionStatus {
146 #[must_use]
148 pub const fn as_str(self) -> &'static str {
149 match self {
150 Self::PreparedOnly => "prepared_only",
151 Self::Published => "published",
152 }
153 }
154}
155
156#[derive(Clone, Debug, Eq, PartialEq)]
163pub(in crate::db) struct BoundSqlDdlRequest {
164 statement: BoundSqlDdlStatement,
165}
166
167impl BoundSqlDdlRequest {
168 #[must_use]
170 pub(in crate::db) const fn statement(&self) -> &BoundSqlDdlStatement {
171 &self.statement
172 }
173}
174
175#[derive(Clone, Debug, Eq, PartialEq)]
181pub(in crate::db) enum BoundSqlDdlStatement {
182 CreateIndex(BoundSqlCreateIndexRequest),
183}
184
185#[derive(Clone, Debug, Eq, PartialEq)]
192pub(in crate::db) struct BoundSqlCreateIndexRequest {
193 index_name: String,
194 entity_name: String,
195 field_path: BoundSqlDdlFieldPath,
196 candidate_index: PersistedIndexSnapshot,
197}
198
199impl BoundSqlCreateIndexRequest {
200 #[must_use]
202 pub(in crate::db) const fn index_name(&self) -> &str {
203 self.index_name.as_str()
204 }
205
206 #[must_use]
208 pub(in crate::db) const fn entity_name(&self) -> &str {
209 self.entity_name.as_str()
210 }
211
212 #[must_use]
214 pub(in crate::db) const fn field_path(&self) -> &BoundSqlDdlFieldPath {
215 &self.field_path
216 }
217
218 #[must_use]
220 pub(in crate::db) const fn candidate_index(&self) -> &PersistedIndexSnapshot {
221 &self.candidate_index
222 }
223}
224
225#[derive(Clone, Debug, Eq, PartialEq)]
231pub(in crate::db) struct BoundSqlDdlFieldPath {
232 root: String,
233 segments: Vec<String>,
234 accepted_path: Vec<String>,
235}
236
237impl BoundSqlDdlFieldPath {
238 #[must_use]
240 pub(in crate::db) const fn root(&self) -> &str {
241 self.root.as_str()
242 }
243
244 #[must_use]
246 pub(in crate::db) const fn segments(&self) -> &[String] {
247 self.segments.as_slice()
248 }
249
250 #[must_use]
252 pub(in crate::db) const fn accepted_path(&self) -> &[String] {
253 self.accepted_path.as_slice()
254 }
255}
256
257#[derive(Debug, Eq, PartialEq, ThisError)]
263pub(in crate::db) enum SqlDdlBindError {
264 #[error("SQL DDL binder requires a DDL statement")]
265 NotDdl,
266
267 #[error("accepted schema does not expose an entity name")]
268 MissingEntityName,
269
270 #[error("accepted schema does not expose an entity path")]
271 MissingEntityPath,
272
273 #[error("SQL entity '{sql_entity}' does not match accepted entity '{expected_entity}'")]
274 EntityMismatch {
275 sql_entity: String,
276 expected_entity: String,
277 },
278
279 #[error("unknown field path '{field_path}' for accepted entity '{entity_name}'")]
280 UnknownFieldPath {
281 entity_name: String,
282 field_path: String,
283 },
284
285 #[error("field path '{field_path}' is not indexable")]
286 FieldPathNotIndexable { field_path: String },
287
288 #[error("field path '{field_path}' depends on generated-only metadata")]
289 FieldPathNotAcceptedCatalogBacked { field_path: String },
290
291 #[error("index name '{index_name}' already exists in the accepted schema")]
292 DuplicateIndexName { index_name: String },
293
294 #[error("accepted schema already has index '{existing_index}' for field path '{field_path}'")]
295 DuplicateFieldPathIndex {
296 field_path: String,
297 existing_index: String,
298 },
299}
300
301#[derive(Debug, Eq, PartialEq, ThisError)]
308pub(in crate::db) enum SqlDdlLoweringError {
309 #[error("SQL DDL lowering requires a CREATE INDEX statement")]
310 UnsupportedStatement,
311
312 #[error("schema mutation admission rejected DDL candidate: {0:?}")]
313 MutationAdmission(SchemaDdlMutationAdmissionError),
314}
315
316#[derive(Debug, Eq, PartialEq, ThisError)]
322pub(in crate::db) enum SqlDdlPrepareError {
323 #[error("{0}")]
324 Bind(#[from] SqlDdlBindError),
325
326 #[error("{0}")]
327 Lowering(#[from] SqlDdlLoweringError),
328}
329
330pub(in crate::db) fn prepare_sql_ddl_statement(
332 statement: &SqlStatement,
333 accepted_before: &AcceptedSchemaSnapshot,
334 schema: &SchemaInfo,
335) -> Result<PreparedSqlDdlCommand, SqlDdlPrepareError> {
336 let bound = bind_sql_ddl_statement(statement, schema)?;
337 let derivation = derive_bound_sql_ddl_accepted_after(accepted_before, &bound)?;
338 let report = ddl_preparation_report(&bound, &derivation);
339
340 Ok(PreparedSqlDdlCommand {
341 bound,
342 derivation,
343 report,
344 })
345}
346
347pub(in crate::db) fn bind_sql_ddl_statement(
349 statement: &SqlStatement,
350 schema: &SchemaInfo,
351) -> Result<BoundSqlDdlRequest, SqlDdlBindError> {
352 let SqlStatement::Ddl(ddl) = statement else {
353 return Err(SqlDdlBindError::NotDdl);
354 };
355
356 match ddl {
357 SqlDdlStatement::CreateIndex(statement) => bind_create_index_statement(statement, schema),
358 }
359}
360
361fn bind_create_index_statement(
362 statement: &SqlCreateIndexStatement,
363 schema: &SchemaInfo,
364) -> Result<BoundSqlDdlRequest, SqlDdlBindError> {
365 let entity_name = schema
366 .entity_name()
367 .ok_or(SqlDdlBindError::MissingEntityName)?;
368
369 if !identifiers_tail_match(statement.entity.as_str(), entity_name) {
370 return Err(SqlDdlBindError::EntityMismatch {
371 sql_entity: statement.entity.clone(),
372 expected_entity: entity_name.to_string(),
373 });
374 }
375
376 reject_duplicate_index_name(statement.name.as_str(), schema)?;
377 let field_path =
378 bind_create_index_field_path(statement.field_path.as_str(), entity_name, schema)?;
379 reject_duplicate_field_path_index(&field_path, schema)?;
380 let candidate_index = candidate_index_snapshot(statement.name.as_str(), &field_path, schema)?;
381
382 Ok(BoundSqlDdlRequest {
383 statement: BoundSqlDdlStatement::CreateIndex(BoundSqlCreateIndexRequest {
384 index_name: statement.name.clone(),
385 entity_name: entity_name.to_string(),
386 field_path,
387 candidate_index,
388 }),
389 })
390}
391
392fn bind_create_index_field_path(
393 field_path: &str,
394 entity_name: &str,
395 schema: &SchemaInfo,
396) -> Result<BoundSqlDdlFieldPath, SqlDdlBindError> {
397 let mut path = field_path
398 .split('.')
399 .map(str::trim)
400 .filter(|segment| !segment.is_empty());
401 let Some(root) = path.next() else {
402 return Err(SqlDdlBindError::UnknownFieldPath {
403 entity_name: entity_name.to_string(),
404 field_path: field_path.to_string(),
405 });
406 };
407 let segments = path.map(str::to_string).collect::<Vec<_>>();
408
409 let capabilities = if segments.is_empty() {
410 schema.sql_capabilities(root)
411 } else {
412 schema.nested_sql_capabilities(root, segments.as_slice())
413 }
414 .ok_or_else(|| SqlDdlBindError::UnknownFieldPath {
415 entity_name: entity_name.to_string(),
416 field_path: field_path.to_string(),
417 })?;
418
419 if !capabilities.orderable() {
420 return Err(SqlDdlBindError::FieldPathNotIndexable {
421 field_path: field_path.to_string(),
422 });
423 }
424
425 let mut accepted_path = Vec::with_capacity(segments.len() + 1);
426 accepted_path.push(root.to_string());
427 accepted_path.extend(segments.iter().cloned());
428
429 Ok(BoundSqlDdlFieldPath {
430 root: root.to_string(),
431 segments,
432 accepted_path,
433 })
434}
435
436fn reject_duplicate_index_name(
437 index_name: &str,
438 schema: &SchemaInfo,
439) -> Result<(), SqlDdlBindError> {
440 if schema
441 .field_path_indexes()
442 .iter()
443 .any(|index| index.name() == index_name)
444 || schema
445 .expression_indexes()
446 .iter()
447 .any(|index| index.name() == index_name)
448 {
449 return Err(SqlDdlBindError::DuplicateIndexName {
450 index_name: index_name.to_string(),
451 });
452 }
453
454 Ok(())
455}
456
457fn reject_duplicate_field_path_index(
458 field_path: &BoundSqlDdlFieldPath,
459 schema: &SchemaInfo,
460) -> Result<(), SqlDdlBindError> {
461 let Some(existing_index) = schema.field_path_indexes().iter().find(|index| {
462 let fields = index.fields();
463 fields.len() == 1 && fields[0].path() == field_path.accepted_path()
464 }) else {
465 return Ok(());
466 };
467
468 Err(SqlDdlBindError::DuplicateFieldPathIndex {
469 field_path: field_path.accepted_path().join("."),
470 existing_index: existing_index.name().to_string(),
471 })
472}
473
474fn candidate_index_snapshot(
475 index_name: &str,
476 field_path: &BoundSqlDdlFieldPath,
477 schema: &SchemaInfo,
478) -> Result<PersistedIndexSnapshot, SqlDdlBindError> {
479 let key = schema
480 .accepted_index_field_path_snapshot(field_path.root(), field_path.segments())
481 .ok_or_else(|| SqlDdlBindError::FieldPathNotAcceptedCatalogBacked {
482 field_path: field_path.accepted_path().join("."),
483 })?;
484 let store = schema
485 .ddl_index_store_path(index_name)
486 .ok_or(SqlDdlBindError::MissingEntityPath)?;
487
488 Ok(PersistedIndexSnapshot::new(
489 schema.next_secondary_index_ordinal(),
490 index_name.to_string(),
491 store,
492 false,
493 PersistedIndexKeySnapshot::FieldPath(vec![key]),
494 None,
495 ))
496}
497
498pub(in crate::db) fn lower_bound_sql_ddl_to_schema_mutation_admission(
500 request: &BoundSqlDdlRequest,
501) -> Result<SchemaDdlMutationAdmission, SqlDdlLoweringError> {
502 let BoundSqlDdlStatement::CreateIndex(create) = request.statement();
503
504 admit_sql_ddl_field_path_index_candidate(create.candidate_index())
505 .map_err(SqlDdlLoweringError::MutationAdmission)
506}
507
508pub(in crate::db) fn derive_bound_sql_ddl_accepted_after(
510 accepted_before: &AcceptedSchemaSnapshot,
511 request: &BoundSqlDdlRequest,
512) -> Result<SchemaDdlAcceptedSnapshotDerivation, SqlDdlLoweringError> {
513 let BoundSqlDdlStatement::CreateIndex(create) = request.statement();
514
515 derive_sql_ddl_field_path_index_accepted_after(
516 accepted_before,
517 create.candidate_index().clone(),
518 )
519 .map_err(SqlDdlLoweringError::MutationAdmission)
520}
521
522fn ddl_preparation_report(
523 bound: &BoundSqlDdlRequest,
524 derivation: &SchemaDdlAcceptedSnapshotDerivation,
525) -> SqlDdlPreparationReport {
526 let BoundSqlDdlStatement::CreateIndex(create) = bound.statement();
527 let target = derivation.admission().target();
528
529 SqlDdlPreparationReport {
530 mutation_kind: SqlDdlMutationKind::AddNonUniqueFieldPathIndex,
531 target_index: target.name().to_string(),
532 target_store: target.store().to_string(),
533 field_path: create.field_path().accepted_path().to_vec(),
534 execution_status: SqlDdlExecutionStatus::PreparedOnly,
535 }
536}