Skip to main content

pharos_runtime_database/
relational.rs

1//! Parameterized SQL lowering shared by the three relational backend adapters.
2//! Connection, isolation, error mapping, and migration execution remain with
3//! the SQLite, PostgreSQL, and MySQL adapter crates.
4
5use std::collections::{BTreeMap, BTreeSet};
6
7use crate::operations::{
8    Aggregate, AggregateQuery, AppSchema, Claim, DatabaseError, Entity, Field, FieldConstraint,
9    FieldDefault, FieldType, Identity, Index, IndexKey, MAX_TEXT_LENGTH, Mutation, OnDelete,
10    Predicate, Query, SchemaChange, WriteValue, decimal_cents, decimal_from_cents, entity,
11    materialize_generated_predicate, valid_identifier, validate_claim, validate_copy_field,
12    validate_predicate, validate_query,
13};
14use serde_json::Value;
15
16/// The physical placeholder and identifier convention selected by an adapter.
17#[derive(Debug, Clone, Copy, Eq, PartialEq)]
18pub enum Dialect {
19    Sqlite,
20    Postgresql,
21    Mysql,
22}
23
24#[cfg(test)]
25mod schema_literal_tests {
26    use super::{Dialect, ddl_literal};
27    use serde_json::json;
28
29    /// MySQL literals avoid every quote-scanner and SQL-mode-dependent escape.
30    #[test]
31    fn mysql_schema_literals_encode_utf8_bytes() {
32        assert_eq!(
33            ddl_literal(Dialect::Mysql, &json!("\\'")),
34            Ok("_utf8mb4 X'5c27'".to_owned())
35        );
36        assert_eq!(
37            ddl_literal(Dialect::Mysql, &json!("é")),
38            Ok("_utf8mb4 X'c3a9'".to_owned())
39        );
40        assert_eq!(
41            ddl_literal(Dialect::Mysql, &json!("")),
42            Ok("_utf8mb4 X''".to_owned())
43        );
44    }
45
46    /// Standard SQL literal delimiters double quotes while preserving backslashes.
47    #[test]
48    fn standard_schema_literals_preserve_values() {
49        for dialect in [Dialect::Sqlite, Dialect::Postgresql] {
50            assert_eq!(ddl_literal(dialect, &json!("\\'")), Ok("'\\'''".to_owned()));
51        }
52    }
53}
54
55/// One safely parameterized statement; values never enter its SQL text.
56#[derive(Debug, Clone, PartialEq)]
57pub struct Statement {
58    pub sql: String,
59    pub parameters: Vec<Value>,
60    /// Positions whose declared logical field requires a physical byte buffer.
61    pub binary_parameters: BTreeSet<usize>,
62}
63
64struct Builder {
65    dialect: Dialect,
66    parameters: Vec<Value>,
67    binary_parameters: BTreeSet<usize>,
68}
69
70impl Builder {
71    /// Starts one statement with an empty parameter list.
72    fn new(dialect: Dialect) -> Self {
73        Self {
74            dialect,
75            parameters: Vec::new(),
76            binary_parameters: BTreeSet::new(),
77        }
78    }
79
80    /// Quotes a validated logical identifier using the target SQL dialect.
81    fn name(&self, value: &str) -> Result<String, DatabaseError> {
82        if !valid_identifier(value) {
83            return Err(DatabaseError::InvalidContract);
84        }
85        Ok(match self.dialect {
86            Dialect::Mysql => format!("`{value}`"),
87            Dialect::Sqlite | Dialect::Postgresql => format!("\"{value}\""),
88        })
89    }
90
91    /// Appends one bind value without interpolating it into SQL text.
92    fn bind(&mut self, value: Value) -> String {
93        self.parameters.push(value);
94        match self.dialect {
95            Dialect::Postgresql => format!("${}", self.parameters.len()),
96            Dialect::Sqlite | Dialect::Mysql => "?".to_owned(),
97        }
98    }
99
100    /// Encodes a logical field value before binding it to the physical dialect.
101    fn bind_field(&mut self, field: &Field, value: Value) -> Result<String, DatabaseError> {
102        let value = if field.kind == FieldType::Json && !value.is_null() {
103            crate::operations::canonical_json_value(&value)?
104        } else {
105            value
106        };
107        if field.kind == FieldType::Binary && !value.is_null() {
108            self.binary_parameters.insert(self.parameters.len());
109            return Ok(self.bind(value));
110        }
111        if self.dialect == Dialect::Mysql && field.kind == FieldType::Json && !value.is_null() {
112            return Ok(format!("CAST({} AS JSON)", self.bind(value)));
113        }
114        if field.kind == FieldType::Decimal {
115            if value.is_null() {
116                return Ok(match self.dialect {
117                    Dialect::Postgresql => {
118                        format!("CAST({}::TEXT AS NUMERIC(10,2))", self.bind(Value::Null))
119                    }
120                    _ => self.bind(Value::Null),
121                });
122            }
123            let cents = decimal_cents(&value)?;
124            return match self.dialect {
125                Dialect::Sqlite => Ok(self.bind(Value::from(cents as f64 / 100.0))),
126                Dialect::Postgresql => Ok(format!(
127                    "CAST({}::TEXT AS NUMERIC(10,2))",
128                    self.bind(Value::String(decimal_from_cents(cents)))
129                )),
130                Dialect::Mysql => Ok(self.bind(Value::String(decimal_from_cents(cents)))),
131            };
132        }
133        if field.kind == FieldType::Timestamp && !value.is_null() {
134            let value = crate::operations::canonical_stored_timestamp(
135                value.as_str().ok_or(DatabaseError::InvalidInput)?,
136            )?;
137            return Ok(self.bind(Value::String(value)));
138        }
139        Ok(self.bind(value))
140    }
141
142    /// Finds the declared field type for a parameterized comparison.
143    fn bind_named_field(
144        &mut self,
145        declaration: &Entity,
146        field: &str,
147        value: Value,
148    ) -> Result<String, DatabaseError> {
149        let field = declaration
150            .fields
151            .iter()
152            .find(|item| item.name == field)
153            .ok_or(DatabaseError::InvalidContract)?;
154        if field.kind == FieldType::Json {
155            return Ok(self.bind(crate::index_values::json_comparison_value(&value)?));
156        }
157        self.bind_field(field, value)
158    }
159
160    /// Resolves a comparison key without changing the projected app field.
161    fn comparison_name(&self, declaration: &Entity, name: &str) -> Result<String, DatabaseError> {
162        let field = declaration
163            .fields
164            .iter()
165            .find(|field| field.name == name)
166            .ok_or(DatabaseError::InvalidContract)?;
167        self.name(&if field.kind == FieldType::Json {
168            crate::index_values::json_comparison_key(name)
169        } else {
170            name.to_owned()
171        })
172    }
173
174    /// Lowers a validated predicate recursively while preserving parameter order.
175    fn filter(
176        &mut self,
177        declaration: &Entity,
178        predicate: &Predicate,
179    ) -> Result<String, DatabaseError> {
180        match predicate {
181            Predicate::Equal(field, value) => self.compare(declaration, field, "=", value),
182            Predicate::GeneratedEqual(_, _) => Err(DatabaseError::InvalidInput),
183            Predicate::NotEqual(field, value) => self.compare(declaration, field, "<>", value),
184            Predicate::LessThan(field, value) => self.compare(declaration, field, "<", value),
185            Predicate::AtMost(field, value) => self.compare(declaration, field, "<=", value),
186            Predicate::GreaterThan(field, value) => self.compare(declaration, field, ">", value),
187            Predicate::AtLeast(field, value) => self.compare(declaration, field, ">=", value),
188            Predicate::Contains(field, needle) => {
189                let name = self.name(field)?;
190                let value = self.bind(Value::String(needle.clone()));
191                Ok(match self.dialect {
192                    Dialect::Mysql => format!("LOCATE(BINARY {value}, BINARY {name}) > 0"),
193                    Dialect::Sqlite => format!("INSTR({name}, {value}) > 0"),
194                    Dialect::Postgresql => format!("STRPOS({name}, {value}) > 0"),
195                })
196            }
197            Predicate::IsNull(field) => Ok(format!(
198                "{} IS NULL",
199                self.comparison_name(declaration, field)?
200            )),
201            Predicate::In(field, values) => {
202                if values.len() > 64 {
203                    return Err(DatabaseError::InvalidInput);
204                }
205                if values.is_empty() {
206                    return Ok("1 = 0".to_owned());
207                }
208                let name = self.comparison_name(declaration, field)?;
209                let placeholders = values
210                    .iter()
211                    .map(|value| self.bind_named_field(declaration, field, value.clone()))
212                    .collect::<Result<Vec<_>, _>>()?
213                    .join(", ");
214                Ok(format!("{name} IN ({placeholders})"))
215            }
216            Predicate::All(parts) => self.combine(declaration, parts, " AND "),
217            Predicate::Any(parts) => self.combine(declaration, parts, " OR "),
218        }
219    }
220
221    /// Handles null equality with SQL null semantics.
222    fn compare(
223        &mut self,
224        declaration: &Entity,
225        field: &str,
226        operator: &str,
227        value: &Value,
228    ) -> Result<String, DatabaseError> {
229        let name = self.comparison_name(declaration, field)?;
230        if value.is_null() {
231            return match operator {
232                "=" => Ok(format!("{name} IS NULL")),
233                "<>" => Ok(format!("{name} IS NOT NULL")),
234                _ => Err(DatabaseError::InvalidInput),
235            };
236        }
237        let placeholder = self.bind_named_field(declaration, field, value.clone())?;
238        if self.dialect == Dialect::Mysql
239            && declaration
240                .fields
241                .iter()
242                .any(|entry| entry.name == field && entry.kind == FieldType::Text)
243        {
244            return Ok(format!("BINARY {name} {operator} BINARY {placeholder}"));
245        }
246        Ok(format!("{name} {operator} {placeholder}"))
247    }
248
249    /// Joins a bounded group of nested predicates.
250    fn combine(
251        &mut self,
252        declaration: &Entity,
253        parts: &[Predicate],
254        separator: &str,
255    ) -> Result<String, DatabaseError> {
256        if parts.is_empty() || parts.len() > 32 {
257            return Err(DatabaseError::InvalidInput);
258        }
259        let fragments = parts
260            .iter()
261            .map(|part| self.filter(declaration, part))
262            .collect::<Result<Vec<_>, _>>()?;
263        Ok(format!("({})", fragments.join(separator)))
264    }
265
266    /// Finishes a statement without altering its bind order.
267    fn finish(self, sql: String) -> Statement {
268        Statement {
269            sql,
270            parameters: self.parameters,
271            binary_parameters: self.binary_parameters,
272        }
273    }
274}
275
276/// Compiles one bounded logical query to parameterized SQL.
277pub fn compile_query(
278    dialect: Dialect,
279    schema: &AppSchema,
280    query: &Query,
281) -> Result<Statement, DatabaseError> {
282    validate_query(schema, query)?;
283    let mut builder = Builder::new(dialect);
284    let declaration = entity(schema, &query.entity)?;
285    let columns = query
286        .fields
287        .iter()
288        .map(|field| {
289            let name = builder.name(field)?;
290            if dialect == Dialect::Postgresql
291                && declaration
292                    .fields
293                    .iter()
294                    .any(|item| item.name == *field && item.kind == FieldType::Decimal)
295            {
296                Ok(format!("CAST({name} AS TEXT) AS {name}"))
297            } else {
298                Ok(name)
299            }
300        })
301        .collect::<Result<Vec<_>, _>>()?
302        .join(", ");
303    let mut sql = format!("SELECT {columns} FROM {}", builder.name(&query.entity)?);
304    if let Some(filter) = &query.filter {
305        sql.push_str(" WHERE ");
306        sql.push_str(&builder.filter(declaration, filter)?);
307    }
308    if !query.order.is_empty() {
309        sql.push_str(" ORDER BY ");
310        let order = query
311            .order
312            .iter()
313            .map(|key| {
314                let name = builder.comparison_name(declaration, &key.field)?;
315                let direction = if key.descending { "DESC" } else { "ASC" };
316                let nullable = declaration
317                    .fields
318                    .iter()
319                    .any(|field| field.name == key.field && field.nullable);
320                if nullable && dialect == Dialect::Postgresql {
321                    // PostgreSQL reverses the logical default. Use its native
322                    // null-order clause so indexed sorting remains available.
323                    let null_order = if key.descending { "LAST" } else { "FIRST" };
324                    return Ok(format!("{name} {direction} NULLS {null_order}"));
325                }
326                Ok(format!("{name} {direction}"))
327            })
328            .collect::<Result<Vec<_>, DatabaseError>>()?;
329        sql.push_str(&order.join(", "));
330    }
331    sql.push_str(&format!(" LIMIT {} OFFSET {}", query.limit, query.offset));
332    Ok(builder.finish(sql))
333}
334
335/// Compiles an ordered, one-record claim selection with backend row locking.
336pub fn compile_claim_select(
337    dialect: Dialect,
338    schema: &AppSchema,
339    claim: &Claim,
340) -> Result<Statement, DatabaseError> {
341    validate_claim(schema, claim)?;
342    let identity = entity(schema, &claim.entity)?
343        .identity
344        .generated_field()
345        .ok_or(DatabaseError::InvalidInput)?
346        .to_owned();
347    let mut statement = compile_query(
348        dialect,
349        schema,
350        &Query {
351            entity: claim.entity.clone(),
352            fields: vec![identity],
353            filter: Some(claim.filter.clone()),
354            order: claim.order.clone(),
355            limit: 1,
356            offset: 0,
357        },
358    )?;
359    if dialect != Dialect::Sqlite {
360        statement.sql.push_str(" FOR UPDATE SKIP LOCKED");
361    }
362    Ok(statement)
363}
364
365/// Compiles one count, sum, minimum, or maximum over declared fields.
366pub fn compile_aggregate(
367    dialect: Dialect,
368    schema: &AppSchema,
369    query: &AggregateQuery,
370) -> Result<Statement, DatabaseError> {
371    let declaration = entity(schema, &query.entity)?;
372    let result_kind = crate::operations::aggregate_result_kind(schema, query)?;
373    let mut builder = Builder::new(dialect);
374    let expression = match &query.aggregate {
375        Aggregate::Count => "COUNT(*)".to_owned(),
376        Aggregate::Sum(field) | Aggregate::Minimum(field) | Aggregate::Maximum(field) => {
377            if !declaration.fields.iter().any(|item| item.name == *field) {
378                return Err(DatabaseError::InvalidInput);
379            }
380            let function = match &query.aggregate {
381                Aggregate::Sum(_) => "SUM",
382                Aggregate::Minimum(_) => "MIN",
383                Aggregate::Maximum(_) => "MAX",
384                Aggregate::Count => unreachable!(),
385            };
386            let name = builder.comparison_name(declaration, field)?;
387            let name = match (dialect, result_kind) {
388                (Dialect::Postgresql, FieldType::Boolean) => format!("CAST({name} AS INTEGER)"),
389                (Dialect::Postgresql, FieldType::Binary) => format!("encode({name}, 'hex')"),
390                (Dialect::Mysql, FieldType::Binary) => format!("HEX({name})"),
391                _ => name,
392            };
393            format!("{function}({name})")
394        }
395    };
396    let expression = match dialect {
397        Dialect::Postgresql => format!("CAST({expression} AS TEXT)"),
398        Dialect::Mysql => format!("CAST({expression} AS CHAR)"),
399        Dialect::Sqlite => expression,
400    };
401    let mut sql = format!("SELECT {expression} FROM {}", builder.name(&query.entity)?);
402    if let Some(filter) = &query.filter {
403        validate_predicate(declaration, filter)?;
404        sql.push_str(" WHERE ");
405        sql.push_str(&builder.filter(declaration, filter)?);
406    }
407    Ok(builder.finish(sql))
408}
409
410/// Resolves a transaction-local value from a previous insert result.
411fn resolve_value(
412    value: &WriteValue,
413    generated_ids: &[Option<i64>],
414) -> Result<Value, DatabaseError> {
415    crate::operations::resolve_write_value(value, generated_ids)
416}
417
418/// Looks up the identity of an inserted or pre-existing unique record inside
419/// the same transaction. The caller must hold its transaction until this read.
420pub fn compile_ensured_identity_query(
421    dialect: Dialect,
422    schema: &AppSchema,
423    name: &str,
424    unique_key: &[String],
425    values: &BTreeMap<String, WriteValue>,
426    generated_ids: &[Option<i64>],
427) -> Result<Statement, DatabaseError> {
428    let declaration = entity(schema, name)?;
429    let identity = declaration
430        .identity
431        .generated_field()
432        .ok_or(DatabaseError::InvalidContract)?;
433    if !declaration.accepts_upsert_key(unique_key) {
434        return Err(DatabaseError::InvalidContract);
435    }
436    let filters = unique_key
437        .iter()
438        .map(|field| {
439            let value = values.get(field).ok_or(DatabaseError::InvalidInput)?;
440            Ok(Predicate::Equal(
441                field.clone(),
442                resolve_value(value, generated_ids)?,
443            ))
444        })
445        .collect::<Result<Vec<_>, DatabaseError>>()?;
446    compile_query(
447        dialect,
448        schema,
449        &Query {
450            entity: name.to_owned(),
451            fields: vec![identity.to_owned()],
452            filter: Some(Predicate::All(filters)),
453            order: Vec::new(),
454            limit: 1,
455            offset: 0,
456        },
457    )
458}
459
460/// Prepared statements for one reservation decision under a backend transaction.
461pub struct ReservationStatements {
462    pub expire: Option<Statement>,
463    pub owner: Statement,
464    pub occupied: Statement,
465    pub insert: Statement,
466}
467
468pub fn compile_reservation(
469    dialect: Dialect,
470    schema: &AppSchema,
471    mutation: &Mutation,
472    generated_ids: &[Option<i64>],
473) -> Result<ReservationStatements, DatabaseError> {
474    let Mutation::Reserve {
475        entity: name,
476        status_field,
477        active_statuses,
478        owner_filter,
479        scope_filter,
480        expiry,
481        values,
482        ..
483    } = mutation
484    else {
485        return Err(DatabaseError::InvalidContract);
486    };
487    let identity = entity(schema, name)?
488        .identity
489        .generated_field()
490        .ok_or(DatabaseError::InvalidContract)?
491        .to_owned();
492    let owner_filter = materialize_generated_predicate(owner_filter, generated_ids)?;
493    let scope_filter = materialize_generated_predicate(scope_filter, generated_ids)?;
494    let active = Predicate::In(status_field.clone(), active_statuses.clone());
495    let owner = compile_query(
496        dialect,
497        schema,
498        &Query {
499            entity: name.clone(),
500            fields: vec![identity],
501            filter: Some(Predicate::All(vec![owner_filter.clone(), active.clone()])),
502            order: Vec::new(),
503            limit: 1,
504            offset: 0,
505        },
506    )?;
507    let occupied = compile_aggregate(
508        dialect,
509        schema,
510        &AggregateQuery {
511            entity: name.clone(),
512            filter: Some(Predicate::All(vec![scope_filter.clone(), active])),
513            aggregate: Aggregate::Count,
514        },
515    )?;
516    let expire = expiry
517        .as_ref()
518        .map(|rule| {
519            let mut values = BTreeMap::from([(
520                status_field.clone(),
521                WriteValue::Literal(rule.to_status.clone()),
522            )]);
523            if let Some((name, value)) = &rule.released_at {
524                values.insert(name.clone(), WriteValue::Literal(value.clone()));
525            }
526            if let Some((name, value)) = &rule.reason {
527                values.insert(name.clone(), WriteValue::Literal(value.clone()));
528            }
529            compile_mutation(
530                dialect,
531                schema,
532                &Mutation::Update {
533                    entity: name.clone(),
534                    filter: crate::operations::reservation_expiry_filter(
535                        scope_filter.clone(),
536                        status_field,
537                        rule,
538                        generated_ids,
539                    )?,
540                    values,
541                    expected_rows: None,
542                },
543                generated_ids,
544            )
545        })
546        .transpose()?;
547    let insert = compile_mutation(
548        dialect,
549        schema,
550        &Mutation::Insert {
551            entity: name.clone(),
552            values: values.clone(),
553        },
554        generated_ids,
555    )?;
556    Ok(ReservationStatements {
557        expire,
558        owner,
559        occupied,
560        insert,
561    })
562}
563
564/// Compiles one validated transaction step against previous generated IDs.
565pub fn compile_mutation(
566    dialect: Dialect,
567    schema: &AppSchema,
568    mutation: &Mutation,
569    generated_ids: &[Option<i64>],
570) -> Result<Statement, DatabaseError> {
571    let name = match mutation {
572        Mutation::Insert { entity, .. }
573        | Mutation::Upsert { entity, .. }
574        | Mutation::InsertIfAbsent { entity, .. }
575        | Mutation::Update { entity, .. }
576        | Mutation::Delete { entity, .. }
577        | Mutation::RequireCount { entity, .. }
578        | Mutation::Reserve { entity, .. } => entity,
579    };
580    let declaration = entity(schema, name)?;
581    let mut builder = Builder::new(dialect);
582    let table = builder.name(name)?;
583    let sql = match mutation {
584        Mutation::Insert { values, .. }
585        | Mutation::Upsert { values, .. }
586        | Mutation::InsertIfAbsent { values, .. } => {
587            let mut columns = Vec::with_capacity(declaration.fields.len());
588            let mut placeholders = Vec::with_capacity(declaration.fields.len());
589            for field in &declaration.fields {
590                let value = if let Some(value) = values.get(&field.name) {
591                    Some(builder.bind_field(
592                        field,
593                        crate::operations::resolve_field_write(field, value, generated_ids)?,
594                    )?)
595                } else {
596                    match &field.default {
597                        Some(FieldDefault::Literal(value)) => {
598                            Some(builder.bind_field(field, value.clone())?)
599                        }
600                        Some(FieldDefault::CurrentTimestamp) => {
601                            Some(current_timestamp_sql(dialect).to_owned())
602                        }
603                        None => None,
604                    }
605                };
606                if let Some(value) = value {
607                    columns.push(builder.name(&field.name)?);
608                    placeholders.push(value);
609                }
610            }
611            for name in crate::index_values::derived_fields(declaration) {
612                let field = declaration
613                    .fields
614                    .iter()
615                    .find(|field| field.name == name)
616                    .ok_or(DatabaseError::InvalidContract)?;
617                let original = if let Some(value) = values.get(name) {
618                    crate::operations::resolve_field_write(field, value, generated_ids)?
619                } else if let Some(FieldDefault::Literal(value)) = &field.default {
620                    value.clone()
621                } else {
622                    continue;
623                };
624                columns.push(builder.name(&crate::index_values::derived_key(declaration, name)?)?);
625                placeholders.push(builder.bind(crate::index_values::derived_value(
626                    declaration,
627                    name,
628                    &original,
629                )?));
630            }
631            let columns = columns.join(", ");
632            let placeholders = placeholders.join(", ");
633            let mut sql = format!("INSERT INTO {table} ({columns}) VALUES ({placeholders})");
634            if let Mutation::Upsert { unique_key, .. } = mutation {
635                if !declaration.accepts_upsert_key(unique_key) {
636                    return Err(DatabaseError::InvalidContract);
637                }
638                let mut assignments = values
639                    .keys()
640                    .filter(|field| !unique_key.contains(field))
641                    .map(|field| {
642                        let column = builder.name(field)?;
643                        Ok(match dialect {
644                            Dialect::Mysql => format!("{column} = VALUES({column})"),
645                            Dialect::Sqlite | Dialect::Postgresql => {
646                                format!("{column} = EXCLUDED.{column}")
647                            }
648                        })
649                    })
650                    .collect::<Result<Vec<_>, DatabaseError>>()?;
651                for name in crate::index_values::derived_fields(declaration) {
652                    if values.contains_key(name) && !unique_key.iter().any(|key| key == name) {
653                        let column =
654                            builder.name(&crate::index_values::derived_key(declaration, name)?)?;
655                        assignments.push(match dialect {
656                            Dialect::Mysql => format!("{column} = VALUES({column})"),
657                            _ => format!("{column} = EXCLUDED.{column}"),
658                        });
659                    }
660                }
661                if assignments.is_empty() {
662                    // A keys-only upsert ensures membership without rewriting defaults.
663                    match dialect {
664                        Dialect::Mysql => {
665                            let key = builder.name(&unique_key[0])?;
666                            sql.push_str(&format!(" ON DUPLICATE KEY UPDATE {key} = {key}"));
667                        }
668                        Dialect::Sqlite | Dialect::Postgresql => {
669                            let key = unique_key
670                                .iter()
671                                .map(|field| builder.name(field))
672                                .collect::<Result<Vec<_>, _>>()?
673                                .join(", ");
674                            sql.push_str(&format!(" ON CONFLICT ({key}) DO NOTHING"));
675                        }
676                    }
677                    return Ok(builder.finish(sql));
678                }
679                match dialect {
680                    Dialect::Mysql => sql.push_str(&format!(
681                        " ON DUPLICATE KEY UPDATE {}",
682                        assignments.join(", ")
683                    )),
684                    Dialect::Sqlite | Dialect::Postgresql => {
685                        let key = unique_key
686                            .iter()
687                            .map(|field| builder.name(field))
688                            .collect::<Result<Vec<_>, _>>()?
689                            .join(", ");
690                        sql.push_str(&format!(
691                            " ON CONFLICT ({key}) DO UPDATE SET {}",
692                            assignments.join(", ")
693                        ));
694                    }
695                }
696            }
697            if let Mutation::InsertIfAbsent { unique_key, .. } = mutation {
698                if !declaration.accepts_upsert_key(unique_key) {
699                    return Err(DatabaseError::InvalidContract);
700                }
701                if dialect != Dialect::Mysql {
702                    let key = unique_key
703                        .iter()
704                        .map(|field| builder.name(field))
705                        .collect::<Result<Vec<_>, _>>()?
706                        .join(", ");
707                    sql.push_str(&format!(" ON CONFLICT ({key}) DO NOTHING"));
708                }
709            }
710            sql
711        }
712        Mutation::Update { values, filter, .. } => {
713            validate_predicate(declaration, filter)?;
714            let filter = materialize_generated_predicate(filter, generated_ids)?;
715            let mut assignments = values
716                .iter()
717                .map(|(field, value)| {
718                    let column = builder.name(field)?;
719                    let declared_field = declaration
720                        .fields
721                        .iter()
722                        .find(|item| item.name == *field)
723                        .ok_or(DatabaseError::InvalidContract)?;
724                    let placeholder = builder.bind_field(
725                        declared_field,
726                        crate::operations::resolve_field_write(
727                            declared_field,
728                            value,
729                            generated_ids,
730                        )?,
731                    )?;
732                    Ok(format!("{column} = {placeholder}"))
733                })
734                .collect::<Result<Vec<_>, DatabaseError>>()?;
735            for name in crate::index_values::derived_fields(declaration) {
736                if let Some(value) = values.get(name) {
737                    let field = declaration
738                        .fields
739                        .iter()
740                        .find(|field| field.name == name)
741                        .ok_or(DatabaseError::InvalidContract)?;
742                    let value =
743                        crate::operations::resolve_field_write(field, value, generated_ids)?;
744                    let key =
745                        builder.name(&crate::index_values::derived_key(declaration, name)?)?;
746                    let binding = builder.bind(crate::index_values::derived_value(
747                        declaration,
748                        name,
749                        &value,
750                    )?);
751                    assignments.push(format!("{key} = {binding}"));
752                }
753            }
754            let assignments = assignments.join(", ");
755            let condition = builder.filter(declaration, &filter)?;
756            format!("UPDATE {table} SET {assignments} WHERE {condition}")
757        }
758        Mutation::Delete { filter, .. } | Mutation::RequireCount { filter, .. } => {
759            validate_predicate(declaration, filter)?;
760            let filter = materialize_generated_predicate(filter, generated_ids)?;
761            let condition = builder.filter(declaration, &filter)?;
762            match mutation {
763                Mutation::Delete { .. } => format!("DELETE FROM {table} WHERE {condition}"),
764                Mutation::RequireCount { .. } => {
765                    format!("SELECT COUNT(*) FROM {table} WHERE {condition}")
766                }
767                _ => unreachable!(),
768            }
769        }
770        Mutation::Reserve { .. } => return Err(DatabaseError::InvalidContract),
771    };
772    Ok(builder.finish(sql))
773}
774
775/// Keeps JSON relationship values synchronized when native constraints null their keys.
776/// The caller executes these bounded statements in the same transaction as deletion.
777pub fn compile_delete_effects(
778    dialect: Dialect,
779    schema: &AppSchema,
780    mutation: &Mutation,
781    generated_ids: &[Option<i64>],
782) -> Result<Vec<Statement>, DatabaseError> {
783    let Mutation::Delete {
784        entity: name,
785        filter,
786        ..
787    } = mutation
788    else {
789        return Ok(Vec::new());
790    };
791    let declaration = entity(schema, name)?;
792    validate_predicate(declaration, filter)?;
793    let mut builder = Builder::new(dialect);
794    let condition = builder.filter(
795        declaration,
796        &materialize_generated_predicate(filter, generated_ids)?,
797    )?;
798    let mut pending = vec![(name.clone(), condition, BTreeSet::new())];
799    let mut statements = Vec::new();
800    let mut traversed = 0usize;
801    while let Some((parent_name, condition, mut path)) = pending.pop() {
802        if !path.insert(parent_name.clone()) {
803            continue;
804        }
805        traversed += 1;
806        if traversed > 256 {
807            return Err(DatabaseError::CapacityExceeded);
808        }
809        let parent = entity(schema, &parent_name)?;
810        for child in &schema.entities {
811            for field in &child.fields {
812                let Some((target, key)) = &field.references else {
813                    continue;
814                };
815                if target != &parent_name {
816                    continue;
817                }
818                let source = builder.name(&crate::index_values::reference_keys(field)?.0)?;
819                let selected = format!(
820                    "SELECT {} FROM {} WHERE {condition}",
821                    builder.comparison_name(parent, key)?,
822                    builder.name(&parent_name)?
823                );
824                let selected = if dialect == Dialect::Mysql {
825                    format!("SELECT * FROM ({selected}) AS _pharos_delete_source")
826                } else {
827                    selected
828                };
829                let child_condition = format!("{source} IN ({selected})");
830                match field.on_delete.unwrap_or(OnDelete::Restrict) {
831                    OnDelete::Cascade => {
832                        pending.push((child.name.clone(), child_condition, path.clone()))
833                    }
834                    OnDelete::SetNull if field.kind == FieldType::Json => {
835                        let sql = format!(
836                            "UPDATE {} SET {} = NULL, {source} = NULL WHERE {child_condition}",
837                            builder.name(&child.name)?,
838                            builder.name(&field.name)?
839                        );
840                        statements.push(Statement {
841                            sql,
842                            parameters: builder.parameters.clone(),
843                            binary_parameters: builder.binary_parameters.clone(),
844                        });
845                    }
846                    _ => {}
847                }
848            }
849        }
850    }
851    Ok(statements)
852}
853
854/// Lowers referential delete actions when an adapter temporarily detaches native constraints.
855/// Descendants are removed before their parents; the adapter verifies restrictive references
856/// before committing. Every predicate retains the ordinary typed parameter bindings.
857pub fn compile_detached_delete_effects(
858    dialect: Dialect,
859    schema: &AppSchema,
860    name: &str,
861    filter: &Predicate,
862) -> Result<Vec<Statement>, DatabaseError> {
863    let declaration = entity(schema, name)?;
864    validate_predicate(declaration, filter)?;
865    let mut builder = Builder::new(dialect);
866    let condition = builder.filter(declaration, filter)?;
867    let mut pending = vec![(name.to_owned(), condition, BTreeSet::new(), false)];
868    let mut statements = Vec::new();
869    let mut traversed = 0usize;
870    while let Some((parent_name, condition, mut path, delete)) = pending.pop() {
871        if delete {
872            statements.push(Statement {
873                sql: format!(
874                    "DELETE FROM {} WHERE {condition}",
875                    builder.name(&parent_name)?
876                ),
877                parameters: builder.parameters.clone(),
878                binary_parameters: builder.binary_parameters.clone(),
879            });
880            continue;
881        }
882        if !path.insert(parent_name.clone()) {
883            return Err(DatabaseError::Conflict);
884        }
885        traversed += 1;
886        if traversed > 256 {
887            return Err(DatabaseError::CapacityExceeded);
888        }
889        let parent = entity(schema, &parent_name)?;
890        if parent_name != name {
891            pending.push((parent_name.clone(), condition.clone(), path.clone(), true));
892        }
893        for child in &schema.entities {
894            for field in &child.fields {
895                let Some((target, key)) = &field.references else {
896                    continue;
897                };
898                if target != &parent_name {
899                    continue;
900                }
901                let source = builder.name(&crate::index_values::reference_keys(field)?.0)?;
902                let selected = format!(
903                    "SELECT {} FROM {} WHERE {condition}",
904                    builder.comparison_name(parent, key)?,
905                    builder.name(&parent_name)?
906                );
907                let child_condition = format!("{source} IN ({selected})");
908                match field.on_delete.unwrap_or(OnDelete::Restrict) {
909                    OnDelete::Cascade => {
910                        pending.push((child.name.clone(), child_condition, path.clone(), false))
911                    }
912                    OnDelete::SetNull => {
913                        let column = builder.name(&field.name)?;
914                        let assignments = if column == source {
915                            format!("{column} = NULL")
916                        } else {
917                            format!("{column} = NULL, {source} = NULL")
918                        };
919                        statements.push(Statement {
920                            sql: format!(
921                                "UPDATE {} SET {assignments} WHERE {child_condition}",
922                                builder.name(&child.name)?
923                            ),
924                            parameters: builder.parameters.clone(),
925                            binary_parameters: builder.binary_parameters.clone(),
926                        });
927                    }
928                    OnDelete::Restrict => {}
929                }
930            }
931        }
932    }
933    Ok(statements)
934}
935
936/// Resolves logical record fields to safe SQL bind values before import.
937pub fn record_values(fields: &serde_json::Map<String, Value>) -> BTreeMap<String, WriteValue> {
938    fields
939        .iter()
940        .map(|(name, value)| (name.clone(), WriteValue::Literal(value.clone())))
941        .collect()
942}
943
944/// Declares a nullable physical key whose Unicode mapping is owned by Rust.
945pub fn derived_column(
946    dialect: Dialect,
947    declaration: &Entity,
948    name: &str,
949) -> Result<String, DatabaseError> {
950    column(dialect, declaration, &derived_field(declaration, name)?)
951}
952
953/// Describes a protected comparison column for creation and interrupted-DDL verification.
954pub fn derived_field(declaration: &Entity, name: &str) -> Result<Field, DatabaseError> {
955    let source = declaration
956        .fields
957        .iter()
958        .find(|field| field.name == name)
959        .ok_or(DatabaseError::InvalidContract)?;
960    let mut key = source.clone();
961    key.name = crate::index_values::derived_key(declaration, name)?;
962    key.kind = FieldType::Text;
963    key.max_length = if source.kind == FieldType::Json {
964        source.max_length
965    } else {
966        source
967            .max_length
968            .and_then(|length| length.checked_mul(crate::index_values::LOWERCASE_EXPANSION))
969    };
970    key.default = match &source.default {
971        Some(FieldDefault::Literal(value)) => Some(FieldDefault::Literal(
972            crate::index_values::derived_value(declaration, name, value)?,
973        )),
974        None => None,
975        Some(FieldDefault::CurrentTimestamp) => return Err(DatabaseError::InvalidContract),
976    };
977    key.references = None;
978    key.constraints.clear();
979    key.nullable = true;
980    Ok(key)
981}
982
983/// Builds an ordered, bounded migration page for expression-key adoption.
984pub fn derived_adoption_query(
985    declaration: &Entity,
986    after: Option<Value>,
987) -> Result<Query, DatabaseError> {
988    let mut fields = declaration
989        .identity
990        .fields()
991        .into_iter()
992        .map(str::to_owned)
993        .collect::<Vec<_>>();
994    for name in crate::index_values::derived_fields(declaration) {
995        if !fields.iter().any(|field| field == name) {
996            fields.push(name.to_owned());
997        }
998    }
999    let (filter, order) = crate::operations::identity_page(declaration, after)?;
1000    Ok(Query {
1001        entity: declaration.name.clone(),
1002        fields,
1003        filter,
1004        order,
1005        limit: 1000,
1006        offset: 0,
1007    })
1008}
1009
1010/// Repairs expression keys from a locked migration page without changing original values.
1011pub fn derived_adoption_statements(
1012    dialect: Dialect,
1013    declaration: &Entity,
1014    rows: &[serde_json::Map<String, Value>],
1015) -> Result<Vec<Statement>, DatabaseError> {
1016    rows.iter()
1017        .map(|row| {
1018            let mut builder = Builder::new(dialect);
1019            let mut assignments = Vec::new();
1020            for name in crate::index_values::derived_fields(declaration) {
1021                let key = builder.name(&crate::index_values::derived_key(declaration, name)?)?;
1022                let value = crate::index_values::derived_value(
1023                    declaration,
1024                    name,
1025                    row.get(name).ok_or(DatabaseError::StorageFailure)?,
1026                )?;
1027                let binding = builder.bind(value);
1028                assignments.push(format!("{key} = {binding}"));
1029            }
1030            let identity = Predicate::All(
1031                declaration
1032                    .identity
1033                    .fields()
1034                    .into_iter()
1035                    .map(|name| {
1036                        row.get(name)
1037                            .cloned()
1038                            .map(|value| Predicate::Equal(name.to_owned(), value))
1039                            .ok_or(DatabaseError::StorageFailure)
1040                    })
1041                    .collect::<Result<Vec<_>, _>>()?,
1042            );
1043            let condition = builder.filter(declaration, &identity)?;
1044            let table = builder.name(&declaration.name)?;
1045            Ok(builder.finish(format!(
1046                "UPDATE {table} SET {} WHERE {condition}",
1047                assignments.join(", ")
1048            )))
1049        })
1050        .collect()
1051}
1052
1053/// Extracts the next stable identity cursor from a bounded adoption page.
1054pub fn adoption_cursor(
1055    declaration: &Entity,
1056    row: &serde_json::Map<String, Value>,
1057) -> Result<Value, DatabaseError> {
1058    match &declaration.identity {
1059        Identity::AutoInteger(name) => row.get(name).cloned().ok_or(DatabaseError::StorageFailure),
1060        Identity::Composite(names) => names
1061            .iter()
1062            .map(|name| {
1063                row.get(name)
1064                    .cloned()
1065                    .map(|value| (name.clone(), value))
1066                    .ok_or(DatabaseError::StorageFailure)
1067            })
1068            .collect::<Result<serde_json::Map<String, Value>, _>>()
1069            .map(Value::Object),
1070    }
1071}
1072
1073/// Selects one bounded page for a typed field-copy migration.
1074pub fn field_copy_query(
1075    schema: &AppSchema,
1076    change: &SchemaChange,
1077    after: Option<Value>,
1078) -> Result<Query, DatabaseError> {
1079    let SchemaChange::CopyField {
1080        entity: name,
1081        source,
1082        target,
1083        filter,
1084    } = change
1085    else {
1086        return Err(DatabaseError::InvalidContract);
1087    };
1088    let declaration = entity(schema, name)?;
1089    validate_copy_field(declaration, source, target, filter)?;
1090    let (cursor, order) = crate::operations::identity_page(declaration, after)?;
1091    let mut fields = declaration
1092        .identity
1093        .fields()
1094        .into_iter()
1095        .map(str::to_owned)
1096        .collect::<Vec<_>>();
1097    if !fields.contains(source) {
1098        fields.push(source.clone());
1099    }
1100    Ok(Query {
1101        entity: name.clone(),
1102        fields,
1103        filter: Some(match cursor {
1104            Some(cursor) => Predicate::All(vec![filter.clone(), cursor]),
1105            None => filter.clone(),
1106        }),
1107        order,
1108        limit: 1000,
1109        offset: 0,
1110    })
1111}
1112
1113/// Uses ordinary typed mutation lowering for historical copy values and derived keys.
1114pub fn field_copy_statements(
1115    dialect: Dialect,
1116    schema: &AppSchema,
1117    change: &SchemaChange,
1118    rows: &[serde_json::Map<String, Value>],
1119) -> Result<Vec<Statement>, DatabaseError> {
1120    let SchemaChange::CopyField {
1121        entity: name,
1122        source,
1123        target,
1124        ..
1125    } = change
1126    else {
1127        return Err(DatabaseError::InvalidContract);
1128    };
1129    let declaration = entity(schema, name)?;
1130    rows.iter()
1131        .map(|row| {
1132            let filter = Predicate::All(
1133                declaration
1134                    .identity
1135                    .fields()
1136                    .into_iter()
1137                    .map(|name| {
1138                        row.get(name)
1139                            .cloned()
1140                            .map(|value| Predicate::Equal(name.to_owned(), value))
1141                            .ok_or(DatabaseError::StorageFailure)
1142                    })
1143                    .collect::<Result<Vec<_>, _>>()?,
1144            );
1145            compile_mutation(
1146                dialect,
1147                schema,
1148                &Mutation::Update {
1149                    entity: name.clone(),
1150                    filter,
1151                    values: BTreeMap::from([(
1152                        target.clone(),
1153                        WriteValue::Literal(
1154                            row.get(source)
1155                                .cloned()
1156                                .ok_or(DatabaseError::StorageFailure)?,
1157                        ),
1158                    )]),
1159                    expected_rows: Some(1),
1160                },
1161                &[],
1162            )
1163        })
1164        .collect()
1165}
1166
1167/// Maps a logical field to one backend-specific physical scalar type.
1168/// Returns the physical field declaration used by schema creation and catalog verification.
1169pub fn sql_type(dialect: Dialect, field: &Field) -> Result<String, DatabaseError> {
1170    Ok(match (dialect, field.kind) {
1171        (Dialect::Mysql, FieldType::Text) => {
1172            let storage = match field.max_length {
1173                Some(length) if length > 0 && length <= 4096 => format!("VARCHAR({length})"),
1174                None | Some(4097..=MAX_TEXT_LENGTH) => "MEDIUMTEXT".to_owned(),
1175                Some(_) => return Err(DatabaseError::InvalidContract),
1176            };
1177            format!("{storage} CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_bin")
1178        }
1179        (dialect, FieldType::Text) => {
1180            let storage = match field.max_length {
1181                Some(length) if length > 0 && length <= MAX_TEXT_LENGTH => {
1182                    format!("VARCHAR({length})")
1183                }
1184                Some(_) => return Err(DatabaseError::InvalidContract),
1185                None => "TEXT".to_owned(),
1186            };
1187            match dialect {
1188                Dialect::Postgresql => format!("{storage} COLLATE \"C\""),
1189                Dialect::Sqlite => format!("{storage} COLLATE BINARY"),
1190                Dialect::Mysql => unreachable!("MySQL text storage is handled above"),
1191            }
1192        }
1193        (Dialect::Sqlite, FieldType::Integer | FieldType::Boolean) => "INTEGER".to_owned(),
1194        (Dialect::Sqlite, FieldType::Decimal) => "NUMERIC".to_owned(),
1195        (Dialect::Sqlite, FieldType::Timestamp | FieldType::Json) => "TEXT".to_owned(),
1196        (Dialect::Sqlite, FieldType::Binary) => "BLOB".to_owned(),
1197        (Dialect::Postgresql, FieldType::Integer) => "BIGINT".to_owned(),
1198        (Dialect::Postgresql, FieldType::Decimal) => "NUMERIC(10,2)".to_owned(),
1199        (Dialect::Postgresql, FieldType::Boolean) => "BOOLEAN".to_owned(),
1200        (Dialect::Postgresql, FieldType::Timestamp) => "VARCHAR(30) COLLATE \"C\"".to_owned(),
1201        (Dialect::Postgresql, FieldType::Json) => "JSONB".to_owned(),
1202        (Dialect::Postgresql, FieldType::Binary) => "BYTEA".to_owned(),
1203        (Dialect::Mysql, FieldType::Integer) => "BIGINT".to_owned(),
1204        (Dialect::Mysql, FieldType::Decimal) => "DECIMAL(10,2)".to_owned(),
1205        (Dialect::Mysql, FieldType::Boolean) => "BOOLEAN".to_owned(),
1206        (Dialect::Mysql, FieldType::Timestamp) => {
1207            "VARCHAR(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_bin".to_owned()
1208        }
1209        (Dialect::Mysql, FieldType::Json) => "JSON".to_owned(),
1210        (Dialect::Mysql, FieldType::Binary) => match field.max_length {
1211            Some(length) if length <= 65535 => format!("VARBINARY({length})"),
1212            _ => "LONGBLOB".to_owned(),
1213        },
1214    })
1215}
1216
1217/// Encodes a schema-owned literal in DDL; request values never use this path.
1218fn ddl_literal(dialect: Dialect, value: &Value) -> Result<String, DatabaseError> {
1219    match value {
1220        Value::Null => Ok("NULL".to_owned()),
1221        Value::Bool(value) => Ok(match (dialect, value) {
1222            (Dialect::Postgresql, true) => "TRUE",
1223            (Dialect::Postgresql, false) => "FALSE",
1224            (_, true) => "1",
1225            (_, false) => "0",
1226        }
1227        .to_owned()),
1228        Value::Number(value) => Ok(value.to_string()),
1229        Value::String(value) if value.len() <= 4096 && dialect == Dialect::Mysql => {
1230            // Hexadecimal UTF-8 literals avoid both SQL-mode-dependent escaping
1231            // and client-side quote scanners during prepared DDL execution.
1232            let mut encoded = String::with_capacity(value.len() * 2);
1233            const DIGITS: &[u8; 16] = b"0123456789abcdef";
1234            for byte in value.bytes() {
1235                encoded.push(char::from(DIGITS[usize::from(byte >> 4)]));
1236                encoded.push(char::from(DIGITS[usize::from(byte & 15)]));
1237            }
1238            Ok(format!("_utf8mb4 X'{encoded}'"))
1239        }
1240        Value::String(value) if value.len() <= 4096 => {
1241            Ok(format!("'{}'", value.replace('\'', "''")))
1242        }
1243        _ => Err(DatabaseError::InvalidContract),
1244    }
1245}
1246
1247/// Keeps generated timestamps consistent in inserts, schema defaults and backfills.
1248pub fn current_timestamp_sql(dialect: Dialect) -> &'static str {
1249    match dialect {
1250        Dialect::Sqlite => "(strftime('%Y-%m-%dT%H:%M:%f', 'now') || '000000Z')",
1251        Dialect::Mysql => "(CONCAT(DATE_FORMAT(UTC_TIMESTAMP(6), '%Y-%m-%dT%H:%i:%s.%f'), '000Z'))",
1252        Dialect::Postgresql => {
1253            "(to_char(CURRENT_TIMESTAMP AT TIME ZONE 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS.US') || '000Z')"
1254        }
1255    }
1256}
1257
1258/// Lowers one declared default for schema creation and physical storage adoption.
1259pub fn field_default_sql(dialect: Dialect, field: &Field) -> Result<Option<String>, DatabaseError> {
1260    field
1261        .default
1262        .as_ref()
1263        .map(|default| match default {
1264            FieldDefault::Literal(value)
1265                if dialect == Dialect::Mysql
1266                    && field.kind == FieldType::Text
1267                    && field.max_length.is_none_or(|length| length > 4096) =>
1268            {
1269                Ok(format!("({})", ddl_literal(dialect, value)?))
1270            }
1271            FieldDefault::Literal(value) => ddl_field_literal(dialect, field, value),
1272            FieldDefault::CurrentTimestamp => Ok(current_timestamp_sql(dialect).to_owned()),
1273        })
1274        .transpose()
1275}
1276
1277fn ddl_field_literal(
1278    dialect: Dialect,
1279    field: &Field,
1280    value: &Value,
1281) -> Result<String, DatabaseError> {
1282    if field.kind == FieldType::Binary && !value.is_null() {
1283        use std::fmt::Write;
1284        let bytes = value.as_array().ok_or(DatabaseError::InvalidContract)?;
1285        if bytes.len() > 1_048_576 {
1286            return Err(DatabaseError::CapacityExceeded);
1287        }
1288        let mut encoded = String::with_capacity(bytes.len() * 2);
1289        for byte in bytes {
1290            let byte = byte
1291                .as_u64()
1292                .filter(|byte| *byte <= 255)
1293                .ok_or(DatabaseError::InvalidContract)?;
1294            write!(&mut encoded, "{byte:02x}").map_err(|_| DatabaseError::InvalidContract)?;
1295        }
1296        return Ok(match dialect {
1297            Dialect::Postgresql => format!("decode('{encoded}', 'hex')"),
1298            Dialect::Sqlite | Dialect::Mysql => format!("X'{encoded}'"),
1299        });
1300    }
1301    if field.kind == FieldType::Timestamp && !value.is_null() {
1302        let timestamp = crate::operations::canonical_stored_timestamp(
1303            value.as_str().ok_or(DatabaseError::InvalidContract)?,
1304        )?;
1305        return ddl_literal(dialect, &Value::String(timestamp));
1306    }
1307    if field.kind == FieldType::Json && !value.is_null() {
1308        let canonical = crate::operations::canonical_json_value(value)?;
1309        let serialized =
1310            serde_json::to_string(&canonical).map_err(|_| DatabaseError::InvalidContract)?;
1311        let encoded = ddl_literal(dialect, &Value::String(serialized))?;
1312        return Ok(match dialect {
1313            Dialect::Mysql => format!("(CAST({encoded} AS JSON))"),
1314            Dialect::Sqlite | Dialect::Postgresql => encoded,
1315        });
1316    }
1317    if field.kind == FieldType::Decimal && !value.is_null() {
1318        let cents = decimal_cents(value)?;
1319        return Ok(match dialect {
1320            Dialect::Sqlite | Dialect::Postgresql | Dialect::Mysql => decimal_from_cents(cents),
1321        });
1322    }
1323    ddl_literal(dialect, value)
1324}
1325
1326/// Produces one physical column declaration without accepting raw SQL.
1327pub fn column(dialect: Dialect, entity: &Entity, field: &Field) -> Result<String, DatabaseError> {
1328    let builder = Builder::new(dialect);
1329    let name = builder.name(&field.name)?;
1330    if entity.identity.generated_field() == Some(field.name.as_str()) {
1331        return Ok(format!(
1332            "{name} {}",
1333            match dialect {
1334                Dialect::Sqlite => "INTEGER PRIMARY KEY AUTOINCREMENT",
1335                Dialect::Postgresql => "BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY",
1336                Dialect::Mysql => "BIGINT AUTO_INCREMENT PRIMARY KEY",
1337            }
1338        ));
1339    }
1340    let mut sql = format!("{name} {}", sql_type(dialect, field)?);
1341    if !field.nullable {
1342        sql.push_str(" NOT NULL");
1343    }
1344    if let Some(default) = field_default_sql(dialect, field)? {
1345        sql.push_str(" DEFAULT ");
1346        sql.push_str(&default);
1347    }
1348    if dialect != Dialect::Mysql
1349        && field.kind != FieldType::Json
1350        && let Some((parent, parent_field)) = &field.references
1351    {
1352        sql.push_str(&format!(
1353            " REFERENCES {}({})",
1354            builder.name(parent)?,
1355            builder.name(parent_field)?
1356        ));
1357        sql.push_str(match field.on_delete.unwrap_or(OnDelete::Restrict) {
1358            OnDelete::Restrict => " ON DELETE RESTRICT",
1359            OnDelete::Cascade => " ON DELETE CASCADE",
1360            OnDelete::SetNull => " ON DELETE SET NULL",
1361        });
1362    }
1363    for constraint in &field.constraints {
1364        let name = builder.name(&field.name)?;
1365        let check = match constraint {
1366            FieldConstraint::Allowed(values) => {
1367                let values = values
1368                    .iter()
1369                    .map(|value| ddl_field_literal(dialect, field, value))
1370                    .collect::<Result<Vec<_>, _>>()?
1371                    .join(", ");
1372                format!("{name} IN ({values})")
1373            }
1374            FieldConstraint::LengthBetween { minimum, maximum } => {
1375                let function = if dialect == Dialect::Mysql {
1376                    "CHAR_LENGTH"
1377                } else {
1378                    "LENGTH"
1379                };
1380                format!("{function}({name}) BETWEEN {minimum} AND {maximum}")
1381            }
1382            FieldConstraint::GreaterThan(value) => {
1383                format!("{name} > {}", ddl_field_literal(dialect, field, value)?)
1384            }
1385            FieldConstraint::AtMost(value) => {
1386                format!("{name} <= {}", ddl_field_literal(dialect, field, value)?)
1387            }
1388            FieldConstraint::Between { minimum, maximum } => format!(
1389                "{name} BETWEEN {} AND {}",
1390                ddl_field_literal(dialect, field, minimum)?,
1391                ddl_field_literal(dialect, field, maximum)?
1392            ),
1393        };
1394        sql.push_str(&format!(" CHECK ({check})"));
1395    }
1396    Ok(sql)
1397}
1398
1399/// Emits a MySQL table constraint because inline REFERENCES does not enforce it.
1400fn foreign_key_clause(
1401    dialect: Dialect,
1402    table: &str,
1403    field: &Field,
1404) -> Result<Option<String>, DatabaseError> {
1405    let Some(_) = &field.references else {
1406        return Ok(None);
1407    };
1408    let builder = Builder::new(dialect);
1409    let constraint =
1410        crate::operations::generated_index_name("fk", table, std::slice::from_ref(&field.name))?;
1411    let constraint = builder.name(&constraint)?;
1412    let (physical_source, physical_parent, physical_key) =
1413        crate::index_values::reference_keys(field)?;
1414    let source = builder.name(&physical_source)?;
1415    let target = builder.name(&physical_parent)?;
1416    let target_field = builder.name(&physical_key)?;
1417    let on_delete = match field.on_delete.unwrap_or(OnDelete::Restrict) {
1418        OnDelete::Restrict => "RESTRICT",
1419        OnDelete::Cascade => "CASCADE",
1420        OnDelete::SetNull => "SET NULL",
1421    };
1422    Ok(Some(format!(
1423        "CONSTRAINT {constraint} FOREIGN KEY ({source}) REFERENCES {target}({target_field}) ON DELETE {on_delete}"
1424    )))
1425}
1426
1427/// Encodes a schema-owned partial-index condition without request interpolation.
1428fn ddl_filter(
1429    builder: &Builder,
1430    declaration: &Entity,
1431    predicate: &Predicate,
1432) -> Result<String, DatabaseError> {
1433    let literal = |name: &str, value: &Value| {
1434        let field = declaration
1435            .fields
1436            .iter()
1437            .find(|field| field.name == name)
1438            .ok_or(DatabaseError::InvalidContract)?;
1439        if field.kind == FieldType::Json {
1440            ddl_literal(
1441                builder.dialect,
1442                &crate::index_values::json_comparison_value(value)?,
1443            )
1444        } else {
1445            ddl_field_literal(builder.dialect, field, value)
1446        }
1447    };
1448    let compare = |name: &str, operator: &str, value: &Value| {
1449        Ok::<_, DatabaseError>(format!(
1450            "{} {operator} {}",
1451            builder.comparison_name(declaration, name)?,
1452            literal(name, value)?
1453        ))
1454    };
1455    match predicate {
1456        Predicate::Equal(name, value) if value.is_null() => Ok(format!(
1457            "{} IS NULL",
1458            builder.comparison_name(declaration, name)?
1459        )),
1460        Predicate::NotEqual(name, value) if value.is_null() => Ok(format!(
1461            "{} IS NOT NULL",
1462            builder.comparison_name(declaration, name)?
1463        )),
1464        Predicate::Equal(name, value) => compare(name, "=", value),
1465        Predicate::GeneratedEqual(_, _) => Err(DatabaseError::InvalidContract),
1466        Predicate::NotEqual(name, value) => compare(name, "<>", value),
1467        Predicate::LessThan(name, value) => compare(name, "<", value),
1468        Predicate::AtMost(name, value) => compare(name, "<=", value),
1469        Predicate::GreaterThan(name, value) => compare(name, ">", value),
1470        Predicate::AtLeast(name, value) => compare(name, ">=", value),
1471        Predicate::Contains(name, needle) => {
1472            let name = builder.name(name)?;
1473            let needle = ddl_literal(builder.dialect, &Value::String(needle.clone()))?;
1474            Ok(match builder.dialect {
1475                Dialect::Mysql => format!("LOCATE(BINARY {needle}, BINARY {name}) > 0"),
1476                Dialect::Sqlite => format!("INSTR({name}, {needle}) > 0"),
1477                Dialect::Postgresql => format!("STRPOS({name}, {needle}) > 0"),
1478            })
1479        }
1480        Predicate::IsNull(name) => Ok(format!(
1481            "{} IS NULL",
1482            builder.comparison_name(declaration, name)?
1483        )),
1484        Predicate::In(name, values) => {
1485            if values.is_empty() {
1486                return Ok("1 = 0".to_owned());
1487            }
1488            let values = values
1489                .iter()
1490                .map(|value| literal(name, value))
1491                .collect::<Result<Vec<_>, _>>()?
1492                .join(", ");
1493            Ok(format!(
1494                "{} IN ({values})",
1495                builder.comparison_name(declaration, name)?
1496            ))
1497        }
1498        Predicate::All(parts) | Predicate::Any(parts) => {
1499            let separator = if matches!(predicate, Predicate::All(_)) {
1500                " AND "
1501            } else {
1502                " OR "
1503            };
1504            let parts = parts
1505                .iter()
1506                .map(|part| ddl_filter(builder, declaration, part))
1507                .collect::<Result<Vec<_>, _>>()?;
1508            Ok(format!("({})", parts.join(separator)))
1509        }
1510    }
1511}
1512
1513/// Compiles one declared conditional or case-folded index.
1514fn conditional_index(
1515    dialect: Dialect,
1516    declaration: &Entity,
1517    index: &Index,
1518) -> Result<Statement, DatabaseError> {
1519    expression_index(dialect, declaration, index, false)
1520}
1521
1522/// Describes the retired native-lowercase encoding solely for validated storage adoption.
1523pub fn previous_expression_index(
1524    dialect: Dialect,
1525    declaration: &Entity,
1526    index: &Index,
1527) -> Result<Statement, DatabaseError> {
1528    expression_index(dialect, declaration, index, true)
1529}
1530
1531/// Lowers an index with an explicit physical encoding; runtime writes use protected keys.
1532fn expression_index(
1533    dialect: Dialect,
1534    declaration: &Entity,
1535    index: &Index,
1536    previous: bool,
1537) -> Result<Statement, DatabaseError> {
1538    let builder = Builder::new(dialect);
1539    if !valid_identifier(&index.name) || index.keys.is_empty() {
1540        return Err(DatabaseError::InvalidContract);
1541    }
1542    let condition = index
1543        .filter
1544        .as_ref()
1545        .map(|filter| {
1546            crate::operations::validate_predicate(declaration, filter)?;
1547            ddl_filter(&builder, declaration, filter)
1548        })
1549        .transpose()?;
1550    let columns = index
1551        .keys
1552        .iter()
1553        .map(|key| {
1554            let expression = match key {
1555                IndexKey::Field(name) => builder.comparison_name(declaration, name)?,
1556                IndexKey::Lowercase(name) => {
1557                    if previous {
1558                        format!("LOWER({})", builder.name(name)?)
1559                    } else {
1560                        builder.name(&crate::index_values::lowercase_key(name))?
1561                    }
1562                }
1563            };
1564            Ok::<_, DatabaseError>(match (dialect, &condition) {
1565                (Dialect::Mysql, Some(filter)) => {
1566                    format!("(CASE WHEN {filter} THEN {expression} ELSE NULL END)")
1567                }
1568                (Dialect::Mysql, None) if previous && matches!(key, IndexKey::Lowercase(_)) => {
1569                    format!("({expression})")
1570                }
1571                _ => expression,
1572            })
1573        })
1574        .collect::<Result<Vec<_>, DatabaseError>>()?
1575        .join(", ");
1576    let mut sql = format!(
1577        "CREATE {}INDEX {} ON {} ({columns})",
1578        if index.unique { "UNIQUE " } else { "" },
1579        builder.name(&index.name)?,
1580        builder.name(&declaration.name)?
1581    );
1582    if let Some(filter) = &condition
1583        && dialect != Dialect::Mysql
1584    {
1585        sql.push_str(" WHERE ");
1586        sql.push_str(filter);
1587    }
1588    Ok(builder.finish(sql))
1589}
1590
1591/// Compiles one additive logical migration step into SQL statements.
1592/// The executing adapter must guard these statements with its migration ledger.
1593pub fn compile_schema_change(
1594    dialect: Dialect,
1595    schema: &AppSchema,
1596    change: &SchemaChange,
1597) -> Result<Vec<Statement>, DatabaseError> {
1598    let builder = Builder::new(dialect);
1599    match change {
1600        // Historical copy requires source data and therefore goes through
1601        // historical::source_query and historical::insert_statements.
1602        SchemaChange::SeedFrom { .. }
1603        | SchemaChange::CopyJsonRecords { .. }
1604        | SchemaChange::RetireFieldConstraint { .. }
1605        | SchemaChange::ConvertField { .. } => Err(DatabaseError::InvalidInput),
1606        SchemaChange::WidenIntegerToDecimal {
1607            entity: name,
1608            field,
1609        } => {
1610            let declaration = entity(schema, name)?;
1611            let column_field = declaration
1612                .fields
1613                .iter()
1614                .find(|item| item.name == *field)
1615                .ok_or(DatabaseError::InvalidContract)?;
1616            if column_field.kind != FieldType::Decimal
1617                || column_field.references.is_some()
1618                || declaration.identity.fields().contains(&field.as_str())
1619            {
1620                return Err(DatabaseError::InvalidContract);
1621            }
1622            let table = builder.name(name)?;
1623            let name = builder.name(field)?;
1624            match dialect {
1625                // SQLite's numeric affinity preserves existing integer values
1626                // and accepts exact decimal writes without a table rebuild.
1627                Dialect::Sqlite => Ok(Vec::new()),
1628                Dialect::Postgresql => Ok(vec![builder.finish(format!("ALTER TABLE {table} ALTER COLUMN {name} TYPE NUMERIC(10,2) USING {name}::NUMERIC(10,2)"))]),
1629                Dialect::Mysql => Ok(vec![builder.finish(format!("ALTER TABLE {table} MODIFY COLUMN {}", column(dialect, declaration, column_field)?))]),
1630            }
1631        }
1632        SchemaChange::CreateEntity(declaration) => {
1633            if !schema
1634                .entities
1635                .iter()
1636                .any(|entity| entity.name == declaration.name)
1637            {
1638                return Err(DatabaseError::InvalidContract);
1639            }
1640            let mut columns = declaration
1641                .fields
1642                .iter()
1643                .map(|field| column(dialect, declaration, field))
1644                .collect::<Result<Vec<_>, _>>()?;
1645            for name in crate::index_values::derived_fields(declaration) {
1646                columns.push(derived_column(dialect, declaration, name)?);
1647            }
1648            if dialect == Dialect::Mysql
1649                || declaration
1650                    .fields
1651                    .iter()
1652                    .any(|field| field.kind == FieldType::Json && field.references.is_some())
1653            {
1654                columns.extend(
1655                    declaration
1656                        .fields
1657                        .iter()
1658                        .filter(|field| dialect == Dialect::Mysql || field.kind == FieldType::Json)
1659                        .map(|field| foreign_key_clause(dialect, &declaration.name, field))
1660                        .collect::<Result<Vec<_>, _>>()?
1661                        .into_iter()
1662                        .flatten(),
1663                );
1664            }
1665            if let Identity::Composite(names) = &declaration.identity {
1666                let key = names
1667                    .iter()
1668                    .map(|name| builder.comparison_name(declaration, name))
1669                    .collect::<Result<Vec<_>, _>>()?
1670                    .join(", ");
1671                columns.push(format!("PRIMARY KEY ({key})"));
1672            }
1673            let columns = columns.join(", ");
1674            let table = builder.name(&declaration.name)?;
1675            let mut statements =
1676                vec![builder.finish(format!("CREATE TABLE IF NOT EXISTS {table} ({columns})"))];
1677            for fields in &declaration.unique {
1678                statements.extend(compile_schema_change(
1679                    dialect,
1680                    schema,
1681                    &SchemaChange::AddUnique {
1682                        entity: declaration.name.clone(),
1683                        fields: fields.clone(),
1684                    },
1685                )?);
1686            }
1687            for fields in &declaration.indexes {
1688                statements.extend(compile_schema_change(
1689                    dialect,
1690                    schema,
1691                    &SchemaChange::AddIndex {
1692                        entity: declaration.name.clone(),
1693                        fields: fields.clone(),
1694                    },
1695                )?);
1696            }
1697            for index in &declaration.conditional_indexes {
1698                statements.push(conditional_index(dialect, declaration, index)?);
1699            }
1700            Ok(statements)
1701        }
1702        SchemaChange::AddField {
1703            entity: name,
1704            field,
1705        } => {
1706            let declaration = entity(schema, name)?;
1707            if declaration.identity.fields().contains(&field.name.as_str()) {
1708                return Err(DatabaseError::InvalidContract);
1709            }
1710            let table = builder.name(name)?;
1711            let definition = column(dialect, declaration, field)?;
1712            let mut statements =
1713                vec![builder.finish(format!("ALTER TABLE {table} ADD COLUMN {definition}"))];
1714            if crate::index_values::derived_fields(declaration).contains(field.name.as_str()) {
1715                let mut definition = derived_column(dialect, declaration, &field.name)?;
1716                if dialect == Dialect::Sqlite
1717                    && field.kind == FieldType::Json
1718                    && field.references.is_some()
1719                {
1720                    let (_, parent, key) = crate::index_values::reference_keys(field)?;
1721                    let names = Builder::new(dialect);
1722                    let rule = match field.on_delete.unwrap_or(OnDelete::Restrict) {
1723                        OnDelete::Restrict => "RESTRICT",
1724                        OnDelete::Cascade => "CASCADE",
1725                        OnDelete::SetNull => "SET NULL",
1726                    };
1727                    definition.push_str(&format!(
1728                        " REFERENCES {}({}) ON DELETE {rule}",
1729                        names.name(&parent)?,
1730                        names.name(&key)?
1731                    ));
1732                }
1733                statements.push(
1734                    Builder::new(dialect)
1735                        .finish(format!("ALTER TABLE {table} ADD COLUMN {definition}")),
1736                );
1737            }
1738            if (dialect == Dialect::Mysql
1739                || (dialect == Dialect::Postgresql && field.kind == FieldType::Json))
1740                && let Some(constraint) = foreign_key_clause(dialect, name, field)?
1741            {
1742                statements.push(
1743                    Builder::new(dialect).finish(format!("ALTER TABLE {table} ADD {constraint}")),
1744                );
1745            }
1746            Ok(statements)
1747        }
1748        SchemaChange::PromoteIdentity {
1749            entity: name,
1750            field,
1751        } => {
1752            let declaration = entity(schema, name)?;
1753            if declaration.identity.generated_field() != Some(field.as_str()) {
1754                return Err(DatabaseError::InvalidContract);
1755            }
1756            let table = builder.name(name)?;
1757            let column = builder.name(field)?;
1758            match dialect {
1759                // INTEGER PRIMARY KEY already generates rowids on SQLite.
1760                Dialect::Sqlite => Ok(Vec::new()),
1761                Dialect::Postgresql => {
1762                    let table_literal =
1763                        ddl_literal(dialect, &serde_json::Value::String(name.clone()))?;
1764                    let field_literal =
1765                        ddl_literal(dialect, &serde_json::Value::String(field.clone()))?;
1766                    Ok(vec![
1767                        builder.finish(format!("ALTER TABLE {table} ALTER COLUMN {column} ADD GENERATED BY DEFAULT AS IDENTITY")),
1768                        Builder::new(dialect).finish(format!("SELECT setval(pg_get_serial_sequence({table_literal}, {field_literal}), GREATEST(COALESCE((SELECT MAX({column}) FROM {table}), 0), 1), EXISTS(SELECT 1 FROM {table}))")),
1769                    ])
1770                }
1771                Dialect::Mysql => Ok(vec![builder.finish(format!(
1772                    "ALTER TABLE {table} MODIFY COLUMN {column} BIGINT NOT NULL AUTO_INCREMENT"
1773                ))]),
1774            }
1775        }
1776        SchemaChange::AddUnique {
1777            entity: name,
1778            fields,
1779        }
1780        | SchemaChange::AddIndex {
1781            entity: name,
1782            fields,
1783        } => {
1784            let declaration = entity(schema, name)?;
1785            if fields.is_empty()
1786                || fields.len() > 16
1787                || fields
1788                    .iter()
1789                    .any(|field| !declaration.fields.iter().any(|item| item.name == *field))
1790            {
1791                return Err(DatabaseError::InvalidContract);
1792            }
1793            let unique = matches!(change, SchemaChange::AddUnique { .. });
1794            let index_name = crate::operations::generated_index_name(
1795                if unique { "uq" } else { "ix" },
1796                name,
1797                fields,
1798            )?;
1799            let columns = fields
1800                .iter()
1801                .map(|field| builder.comparison_name(entity(schema, name)?, field))
1802                .collect::<Result<Vec<_>, _>>()?
1803                .join(", ");
1804            let sql = format!(
1805                "CREATE {}INDEX {} ON {} ({columns})",
1806                if unique { "UNIQUE " } else { "" },
1807                builder.name(&index_name)?,
1808                builder.name(name)?
1809            );
1810            Ok(vec![builder.finish(sql)])
1811        }
1812        SchemaChange::AddConditionalIndex {
1813            entity: name,
1814            index,
1815        } => Ok(vec![conditional_index(
1816            dialect,
1817            entity(schema, name)?,
1818            index,
1819        )?]),
1820        SchemaChange::Seed {
1821            entity: name,
1822            values,
1823        } => {
1824            let declaration = entity(schema, name)?;
1825            if values.is_empty()
1826                || values.len() > 64
1827                || values
1828                    .keys()
1829                    .any(|field| !declaration.fields.iter().any(|item| item.name == *field))
1830            {
1831                return Err(DatabaseError::InvalidContract);
1832            }
1833            let mut columns = values
1834                .keys()
1835                .map(|field| builder.name(field))
1836                .collect::<Result<Vec<_>, _>>()?
1837                .join(", ");
1838            let mut builder = builder;
1839            let mut placeholders = values
1840                .iter()
1841                .map(|(name, value)| {
1842                    let field = declaration
1843                        .fields
1844                        .iter()
1845                        .find(|item| item.name == *name)
1846                        .ok_or(DatabaseError::InvalidContract)?;
1847                    builder.bind_field(field, value.clone())
1848                })
1849                .collect::<Result<Vec<_>, _>>()?
1850                .join(", ");
1851            for source in crate::index_values::derived_fields(declaration) {
1852                if let Some(value) = values.get(source) {
1853                    columns.push_str(&format!(
1854                        ", {}",
1855                        builder.name(&crate::index_values::derived_key(declaration, source)?)?
1856                    ));
1857                    let binding = builder.bind(crate::index_values::derived_value(
1858                        declaration,
1859                        source,
1860                        value,
1861                    )?);
1862                    placeholders.push_str(&format!(", {binding}"));
1863                }
1864            }
1865            let table = builder.name(name)?;
1866            Ok(vec![builder.finish(format!(
1867                "INSERT INTO {table} ({columns}) VALUES ({placeholders})"
1868            ))])
1869        }
1870        SchemaChange::Backfill {
1871            entity: name,
1872            filter,
1873            values,
1874        } => {
1875            let declaration = entity(schema, name)?;
1876            if values.is_empty()
1877                || values.len() > 64
1878                || values
1879                    .keys()
1880                    .any(|field| !declaration.fields.iter().any(|item| item.name == *field))
1881            {
1882                return Err(DatabaseError::InvalidContract);
1883            }
1884            validate_predicate(declaration, filter)?;
1885            let values = values
1886                .iter()
1887                .map(|(name, value)| (name.clone(), WriteValue::Literal(value.clone())))
1888                .collect();
1889            Ok(vec![compile_mutation(
1890                dialect,
1891                schema,
1892                &Mutation::Update {
1893                    entity: name.clone(),
1894                    filter: filter.clone(),
1895                    values,
1896                    expected_rows: None,
1897                },
1898                &[],
1899            )?])
1900        }
1901        SchemaChange::DeleteWhere {
1902            entity: name,
1903            filter,
1904        } => {
1905            validate_predicate(entity(schema, name)?, filter)?;
1906            let mutation = Mutation::Delete {
1907                entity: name.clone(),
1908                filter: filter.clone(),
1909                expected_rows: None,
1910            };
1911            let mut statements = compile_delete_effects(dialect, schema, &mutation, &[])?;
1912            statements.push(compile_mutation(dialect, schema, &mutation, &[])?);
1913            Ok(statements)
1914        }
1915        SchemaChange::CopyField {
1916            entity: name,
1917            source,
1918            target,
1919            filter,
1920        } => {
1921            let declaration = entity(schema, name)?;
1922            validate_copy_field(declaration, source, target, filter)?;
1923            let mut builder = builder;
1924            let where_clause = builder.filter(declaration, filter)?;
1925            let sql = format!(
1926                "UPDATE {} SET {} = {} WHERE {where_clause}",
1927                builder.name(name)?,
1928                builder.name(target)?,
1929                builder.name(source)?
1930            );
1931            Ok(vec![builder.finish(sql)])
1932        }
1933    }
1934}