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 index_store_path: &'static str,
336) -> Result<PreparedSqlDdlCommand, SqlDdlPrepareError> {
337 let bound = bind_sql_ddl_statement(statement, schema, index_store_path)?;
338 let derivation = derive_bound_sql_ddl_accepted_after(accepted_before, &bound)?;
339 let report = ddl_preparation_report(&bound, &derivation);
340
341 Ok(PreparedSqlDdlCommand {
342 bound,
343 derivation,
344 report,
345 })
346}
347
348pub(in crate::db) fn bind_sql_ddl_statement(
350 statement: &SqlStatement,
351 schema: &SchemaInfo,
352 index_store_path: &'static str,
353) -> Result<BoundSqlDdlRequest, SqlDdlBindError> {
354 let SqlStatement::Ddl(ddl) = statement else {
355 return Err(SqlDdlBindError::NotDdl);
356 };
357
358 match ddl {
359 SqlDdlStatement::CreateIndex(statement) => {
360 bind_create_index_statement(statement, schema, index_store_path)
361 }
362 }
363}
364
365fn bind_create_index_statement(
366 statement: &SqlCreateIndexStatement,
367 schema: &SchemaInfo,
368 index_store_path: &'static str,
369) -> Result<BoundSqlDdlRequest, SqlDdlBindError> {
370 let entity_name = schema
371 .entity_name()
372 .ok_or(SqlDdlBindError::MissingEntityName)?;
373
374 if !identifiers_tail_match(statement.entity.as_str(), entity_name) {
375 return Err(SqlDdlBindError::EntityMismatch {
376 sql_entity: statement.entity.clone(),
377 expected_entity: entity_name.to_string(),
378 });
379 }
380
381 reject_duplicate_index_name(statement.name.as_str(), schema)?;
382 let field_path =
383 bind_create_index_field_path(statement.field_path.as_str(), entity_name, schema)?;
384 reject_duplicate_field_path_index(&field_path, schema)?;
385 let candidate_index = candidate_index_snapshot(
386 statement.name.as_str(),
387 &field_path,
388 schema,
389 index_store_path,
390 )?;
391
392 Ok(BoundSqlDdlRequest {
393 statement: BoundSqlDdlStatement::CreateIndex(BoundSqlCreateIndexRequest {
394 index_name: statement.name.clone(),
395 entity_name: entity_name.to_string(),
396 field_path,
397 candidate_index,
398 }),
399 })
400}
401
402fn bind_create_index_field_path(
403 field_path: &str,
404 entity_name: &str,
405 schema: &SchemaInfo,
406) -> Result<BoundSqlDdlFieldPath, SqlDdlBindError> {
407 let mut path = field_path
408 .split('.')
409 .map(str::trim)
410 .filter(|segment| !segment.is_empty());
411 let Some(root) = path.next() else {
412 return Err(SqlDdlBindError::UnknownFieldPath {
413 entity_name: entity_name.to_string(),
414 field_path: field_path.to_string(),
415 });
416 };
417 let segments = path.map(str::to_string).collect::<Vec<_>>();
418
419 let capabilities = if segments.is_empty() {
420 schema.sql_capabilities(root)
421 } else {
422 schema.nested_sql_capabilities(root, segments.as_slice())
423 }
424 .ok_or_else(|| SqlDdlBindError::UnknownFieldPath {
425 entity_name: entity_name.to_string(),
426 field_path: field_path.to_string(),
427 })?;
428
429 if !capabilities.orderable() {
430 return Err(SqlDdlBindError::FieldPathNotIndexable {
431 field_path: field_path.to_string(),
432 });
433 }
434
435 let mut accepted_path = Vec::with_capacity(segments.len() + 1);
436 accepted_path.push(root.to_string());
437 accepted_path.extend(segments.iter().cloned());
438
439 Ok(BoundSqlDdlFieldPath {
440 root: root.to_string(),
441 segments,
442 accepted_path,
443 })
444}
445
446fn reject_duplicate_index_name(
447 index_name: &str,
448 schema: &SchemaInfo,
449) -> Result<(), SqlDdlBindError> {
450 if schema
451 .field_path_indexes()
452 .iter()
453 .any(|index| index.name() == index_name)
454 || schema
455 .expression_indexes()
456 .iter()
457 .any(|index| index.name() == index_name)
458 {
459 return Err(SqlDdlBindError::DuplicateIndexName {
460 index_name: index_name.to_string(),
461 });
462 }
463
464 Ok(())
465}
466
467fn reject_duplicate_field_path_index(
468 field_path: &BoundSqlDdlFieldPath,
469 schema: &SchemaInfo,
470) -> Result<(), SqlDdlBindError> {
471 let Some(existing_index) = schema.field_path_indexes().iter().find(|index| {
472 let fields = index.fields();
473 fields.len() == 1 && fields[0].path() == field_path.accepted_path()
474 }) else {
475 return Ok(());
476 };
477
478 Err(SqlDdlBindError::DuplicateFieldPathIndex {
479 field_path: field_path.accepted_path().join("."),
480 existing_index: existing_index.name().to_string(),
481 })
482}
483
484fn candidate_index_snapshot(
485 index_name: &str,
486 field_path: &BoundSqlDdlFieldPath,
487 schema: &SchemaInfo,
488 index_store_path: &'static str,
489) -> Result<PersistedIndexSnapshot, SqlDdlBindError> {
490 let key = schema
491 .accepted_index_field_path_snapshot(field_path.root(), field_path.segments())
492 .ok_or_else(|| SqlDdlBindError::FieldPathNotAcceptedCatalogBacked {
493 field_path: field_path.accepted_path().join("."),
494 })?;
495
496 Ok(PersistedIndexSnapshot::new(
497 schema.next_secondary_index_ordinal(),
498 index_name.to_string(),
499 index_store_path.to_string(),
500 false,
501 PersistedIndexKeySnapshot::FieldPath(vec![key]),
502 None,
503 ))
504}
505
506pub(in crate::db) fn lower_bound_sql_ddl_to_schema_mutation_admission(
508 request: &BoundSqlDdlRequest,
509) -> Result<SchemaDdlMutationAdmission, SqlDdlLoweringError> {
510 let BoundSqlDdlStatement::CreateIndex(create) = request.statement();
511
512 admit_sql_ddl_field_path_index_candidate(create.candidate_index())
513 .map_err(SqlDdlLoweringError::MutationAdmission)
514}
515
516pub(in crate::db) fn derive_bound_sql_ddl_accepted_after(
518 accepted_before: &AcceptedSchemaSnapshot,
519 request: &BoundSqlDdlRequest,
520) -> Result<SchemaDdlAcceptedSnapshotDerivation, SqlDdlLoweringError> {
521 let BoundSqlDdlStatement::CreateIndex(create) = request.statement();
522
523 derive_sql_ddl_field_path_index_accepted_after(
524 accepted_before,
525 create.candidate_index().clone(),
526 )
527 .map_err(SqlDdlLoweringError::MutationAdmission)
528}
529
530fn ddl_preparation_report(
531 bound: &BoundSqlDdlRequest,
532 derivation: &SchemaDdlAcceptedSnapshotDerivation,
533) -> SqlDdlPreparationReport {
534 let BoundSqlDdlStatement::CreateIndex(create) = bound.statement();
535 let target = derivation.admission().target();
536
537 SqlDdlPreparationReport {
538 mutation_kind: SqlDdlMutationKind::AddNonUniqueFieldPathIndex,
539 target_index: target.name().to_string(),
540 target_store: target.store().to_string(),
541 field_path: create.field_path().accepted_path().to_vec(),
542 execution_status: SqlDdlExecutionStatus::PreparedOnly,
543 }
544}