sofie-db 0.2.0

The handler model (inputs, events, replies), aggregates, read models, the query builder, the unit of work, migrations, the worker and the database seam.
Documentation
use sofie_core::{SqlType, SqlValue};

use super::{InSelect, Kind};
use crate::Dialect;

/// Writes into the query's own text, so a condition costs no string of its own.
struct Renderer<'a> {
    dialect: &'a dyn Dialect,
    params: &'a mut Vec<SqlValue>,
    sql: &'a mut String,
}

impl Renderer<'_> {
    fn bind(&mut self, value: &SqlValue) {
        self.params.push(value.clone());
        self.dialect.push_placeholder(self.sql, self.params.len());
    }

    fn quoted(&mut self, column: &str) {
        self.dialect.push_quoted(self.sql, column);
    }

    fn render(&mut self, kind: &Kind) {
        match kind {
            Kind::Compare {
                columns,
                operator,
                values,
            } => self.each_column(columns, |renderer, index, column| {
                renderer.quoted(column);
                if values[index] == SqlValue::Null
                    && matches!(*operator, "=" | "IS NOT DISTINCT FROM")
                {
                    renderer.sql.push_str(" IS NULL");
                    return;
                }
                renderer.sql.push(' ');
                renderer.sql.push_str(operator);
                renderer.sql.push(' ');
                renderer.bind(&values[index]);
                // SQLite has no default escape; Postgres's is this one.
                if *operator == "LIKE" {
                    renderer.sql.push_str(" ESCAPE '\\'");
                }
            }),
            Kind::Between { column, low, high } => {
                self.sql.push('(');
                self.quoted(column);
                self.sql.push_str(" BETWEEN ");
                self.bind(low);
                self.sql.push_str(" AND ");
                self.bind(high);
                self.sql.push(')');
            }
            Kind::In {
                columns,
                types,
                rows,
            } => self.render_in(columns, types, rows),
            Kind::Null(columns) => self.each_column(columns, |renderer, _, column| {
                renderer.quoted(column);
                renderer.sql.push_str(" IS NULL");
            }),
            #[cfg(feature = "listing")]
            Kind::Contains { column, pattern } => self.render_contains(column, pattern),
            Kind::InSelect(select) => self.render_in_select(select),
            Kind::Not(inner) => {
                self.sql.push_str("NOT ");
                self.render(inner);
            }
            Kind::Or(left, right) => {
                self.sql.push('(');
                self.render(left);
                self.sql.push_str(" OR ");
                self.render(right);
                self.sql.push(')');
            }
            #[cfg(feature = "search")]
            Kind::TextSearch { words, .. } if words.trim().is_empty() => {
                self.sql.push_str("(1 = 0)");
            }
            #[cfg(feature = "search")]
            Kind::TextSearch { column, words, .. } => {
                self.params.push(SqlValue::Text(words.clone()));
                let words = self.dialect.placeholder(self.params.len());
                let search = self
                    .dialect
                    .text_search(&self.dialect.quote(column), &words);
                self.sql.push_str(&search);
            }
        }
    }

    /// `(a … AND b …)`, one part per column.
    fn each_column(&mut self, columns: &[String], mut part: impl FnMut(&mut Self, usize, &str)) {
        self.sql.push('(');
        for (index, column) in columns.iter().enumerate() {
            if index > 0 {
                self.sql.push_str(" AND ");
            }
            part(self, index, column);
        }
        self.sql.push(')');
    }

    #[cfg(feature = "listing")]
    fn render_contains(&mut self, column: &str, pattern: &str) {
        self.params.push(SqlValue::Text(pattern.to_owned()));
        let pattern = self.dialect.placeholder(self.params.len());
        let column = self.dialect.quote(column);
        self.sql.push('(');
        self.sql.push_str(&self.dialect.lowercase(&column));
        self.sql.push_str(" LIKE ");
        self.sql.push_str(&self.dialect.lowercase(&pattern));
        self.sql.push_str(" ESCAPE '\\')");
    }

    fn render_in_select(&mut self, select: &InSelect) {
        self.sql.push('(');
        self.quoted(&select.column);
        self.render_select(select);
    }

    fn render_select(&mut self, select: &InSelect) {
        let (schema, name) = select.table;
        self.sql.push_str(" IN (SELECT ");
        self.quoted(&select.selected);
        self.sql.push_str(" FROM ");
        self.sql.push_str(&self.dialect.qualified(schema, name));
        self.sql.push_str(" WHERE ");
        self.render(&select.condition);
        for column in &select.hidden {
            self.sql.push_str(" AND (");
            self.quoted(column);
            self.sql.push_str(" IS NULL)");
        }
        self.sql.push_str("))");
    }

    fn render_in(&mut self, columns: &[String], types: &[SqlType], rows: &[Vec<SqlValue>]) {
        if rows.is_empty() {
            self.sql.push_str("(1 = 0)");
            return;
        }
        if let ([column], true) = (columns, self.dialect.binds_arrays()) {
            self.sql.push('(');
            self.quoted(column);
            self.sql.push_str(" = ANY(");
            self.bind(&SqlValue::Array(rows.iter().flatten().cloned().collect()));
            self.sql.push_str("))");
            return;
        }
        self.sql.push('(');
        self.tuple(columns.len(), |renderer, index| {
            renderer.quoted(&columns[index]);
        });
        self.sql.push_str(" IN (");
        if self.dialect.binds_arrays() {
            self.unnest(types, rows);
            return;
        }
        for (index, row) in rows.iter().enumerate() {
            if index > 0 {
                self.sql.push_str(", ");
            }
            self.tuple(row.len(), |renderer, index| renderer.bind(&row[index]));
        }
        self.sql.push_str("))");
    }

    /// One array per column, so the text stays the same whatever the number of rows.
    fn unnest(&mut self, types: &[SqlType], rows: &[Vec<SqlValue>]) {
        self.sql.push_str("SELECT * FROM unnest(");
        for (index, ty) in types.iter().enumerate() {
            if index > 0 {
                self.sql.push_str(", ");
            }
            let column = rows.iter().map(|row| row[index].clone()).collect();
            self.params.push(SqlValue::Array(column));
            self.dialect.push_placeholder(self.sql, self.params.len());
            // Unlike `= ANY`, unnest gives the server no column to infer an array's type from.
            self.sql.push_str("::");
            self.sql.push_str(postgres_type(*ty));
            self.sql.push_str("[]");
        }
        self.sql.push_str(")))");
    }

    fn tuple(&mut self, count: usize, mut part: impl FnMut(&mut Self, usize)) {
        if count == 1 {
            part(self, 0);
            return;
        }
        self.sql.push('(');
        for index in 0..count {
            if index > 0 {
                self.sql.push_str(", ");
            }
            part(self, index);
        }
        self.sql.push(')');
    }
}

const fn postgres_type(ty: SqlType) -> &'static str {
    match ty {
        SqlType::Bool => "boolean",
        SqlType::Int => "bigint",
        SqlType::Float => "double precision",
        SqlType::Text => "text",
        SqlType::Bytes => "bytea",
        SqlType::Uuid => "uuid",
        SqlType::Timestamp => "timestamptz",
        SqlType::Date => "date",
        SqlType::DateTime => "timestamp",
    }
}

#[cfg(feature = "children")]
impl InSelect {
    /// `(value IN (SELECT …))`: whether `value` is among the rows the subquery selects.
    pub(crate) fn render_holding(
        &self,
        value: &SqlValue,
        dialect: &dyn Dialect,
        params: &mut Vec<SqlValue>,
        sql: &mut String,
    ) {
        let mut renderer = Renderer {
            dialect,
            params,
            sql,
        };
        renderer.sql.push('(');
        renderer.bind(value);
        renderer.render_select(self);
    }
}

impl Kind {
    pub(crate) fn render_into(
        &self,
        dialect: &dyn Dialect,
        params: &mut Vec<SqlValue>,
        sql: &mut String,
    ) {
        Renderer {
            dialect,
            params,
            sql,
        }
        .render(self);
    }
}