use crate::core::{
AggregateQuery, BulkInsertQuery, BulkUpdateQuery, ConflictClause, CountQuery, DeleteQuery,
FieldType, InsertQuery, Op, SelectQuery, UpdateQuery,
};
use super::writers::{
write_aggregate, write_bulk_insert, write_count, write_delete, write_insert, write_select,
write_update, Sql,
};
use super::{CompiledStatement, Dialect, SqlError};
#[derive(Debug, Default, Clone, Copy)]
pub struct Sqlite;
pub static DIALECT: &Sqlite = &Sqlite;
impl Dialect for Sqlite {
fn name(&self) -> &'static str {
"sqlite"
}
fn serial_type(&self, field_type: FieldType) -> &'static str {
let _ = field_type;
"INTEGER PRIMARY KEY AUTOINCREMENT"
}
fn serial_type_includes_primary_key(&self) -> bool {
true
}
fn supports_returning(&self) -> bool {
true
}
fn bool_literal(&self, b: bool) -> &'static str {
if b {
"1"
} else {
"0"
}
}
fn cast_aggregate_to_int(&self, expr: &str) -> String {
format!("CAST({expr} AS INTEGER)")
}
fn cast_aggregate_to_float(&self, expr: &str) -> String {
format!("CAST({expr} AS REAL)")
}
fn column_type(&self, ty: FieldType, max_length: Option<u32>) -> String {
let _ = max_length; match ty {
FieldType::I16 | FieldType::I32 | FieldType::I64 => "INTEGER".into(),
FieldType::F32 | FieldType::F64 => "REAL".into(),
FieldType::Bool => "INTEGER".into(),
FieldType::String
| FieldType::DateTime
| FieldType::Date
| FieldType::Uuid
| FieldType::Json => "TEXT".into(),
}
}
fn supports_op(&self, op: Op) -> bool {
!matches!(
op,
Op::JsonContains
| Op::JsonContainedBy
| Op::JsonHasKey
| Op::JsonHasAnyKey
| Op::JsonHasAllKeys
)
}
fn write_ilike(&self, sql: &mut String, qualified_col: &str, placeholder: &str, negated: bool) {
if negated {
sql.push_str("NOT (");
}
sql.push_str("LOWER(");
sql.push_str(qualified_col);
sql.push_str(") LIKE LOWER(");
sql.push_str(placeholder);
sql.push(')');
if negated {
sql.push(')');
}
}
fn write_null_safe_eq(
&self,
sql: &mut String,
qualified_col: &str,
placeholder: &str,
distinct: bool,
) {
sql.push_str(qualified_col);
sql.push_str(if distinct { " IS NOT " } else { " IS " });
sql.push_str(placeholder);
}
fn write_conflict_clause(
&self,
sql: &mut String,
conflict: &ConflictClause,
) -> Result<(), SqlError> {
match conflict {
ConflictClause::DoNothing => {
sql.push_str(" ON CONFLICT DO NOTHING");
}
ConflictClause::DoUpdate {
target,
update_columns,
} => {
sql.push_str(" ON CONFLICT (");
let mut first = true;
for col in target {
if !first {
sql.push_str(", ");
}
first = false;
write_sqlite_ident(sql, col);
}
sql.push_str(") DO UPDATE SET ");
let mut first = true;
for col in update_columns {
if !first {
sql.push_str(", ");
}
first = false;
write_sqlite_ident(sql, col);
sql.push_str(" = excluded.");
write_sqlite_ident(sql, col);
}
}
}
Ok(())
}
fn compile_select(&self, query: &SelectQuery) -> Result<CompiledStatement, SqlError> {
let mut b = Sql::new(self);
write_select(&mut b, query)?;
Ok(b.finish())
}
fn compile_count(&self, query: &CountQuery) -> Result<CompiledStatement, SqlError> {
let mut b = Sql::new(self);
write_count(&mut b, query)?;
Ok(b.finish())
}
fn compile_aggregate(&self, query: &AggregateQuery) -> Result<CompiledStatement, SqlError> {
let mut b = Sql::new(self);
write_aggregate(&mut b, query)?;
Ok(b.finish())
}
fn compile_insert(&self, query: &InsertQuery) -> Result<CompiledStatement, SqlError> {
let mut b = Sql::new(self);
write_insert(&mut b, query)?;
Ok(b.finish())
}
fn compile_bulk_insert(&self, query: &BulkInsertQuery) -> Result<CompiledStatement, SqlError> {
let mut b = Sql::new(self);
write_bulk_insert(&mut b, query)?;
Ok(b.finish())
}
fn compile_update(&self, query: &UpdateQuery) -> Result<CompiledStatement, SqlError> {
let mut b = Sql::new(self);
write_update(&mut b, query)?;
Ok(b.finish())
}
fn compile_bulk_update(&self, query: &BulkUpdateQuery) -> Result<CompiledStatement, SqlError> {
let _ = query;
Err(SqlError::DialectQueryCompilationNotImplemented { dialect: "sqlite" })
}
fn compile_delete(&self, query: &DeleteQuery) -> Result<CompiledStatement, SqlError> {
let mut b = Sql::new(self);
write_delete(&mut b, query)?;
Ok(b.finish())
}
}
fn write_sqlite_ident(sql: &mut String, name: &str) {
sql.push('"');
for c in name.chars() {
if c == '"' {
sql.push('"');
}
sql.push(c);
}
sql.push('"');
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::SqlValue;
#[test]
fn name_is_sqlite() {
assert_eq!(Sqlite.name(), "sqlite");
}
#[test]
fn quote_ident_uses_ansi_double_quotes() {
assert_eq!(Sqlite.quote_ident("user_id"), "\"user_id\"");
}
#[test]
fn placeholder_is_question_mark() {
assert_eq!(Sqlite.placeholder(1), "?");
assert_eq!(Sqlite.placeholder(7), "?");
}
#[test]
fn serial_type_is_indivisible_pk_token() {
assert_eq!(
Sqlite.serial_type(FieldType::I32),
"INTEGER PRIMARY KEY AUTOINCREMENT"
);
assert_eq!(
Sqlite.serial_type(FieldType::I64),
"INTEGER PRIMARY KEY AUTOINCREMENT"
);
assert!(Sqlite.serial_type_includes_primary_key());
}
#[test]
fn bool_literal_is_one_or_zero() {
assert_eq!(Sqlite.bool_literal(true), "1");
assert_eq!(Sqlite.bool_literal(false), "0");
}
#[test]
fn supports_returning() {
assert!(Sqlite.supports_returning());
}
#[test]
fn cast_aggregate_uses_sqlite_types() {
assert_eq!(Sqlite.cast_aggregate_to_int("x"), "CAST(x AS INTEGER)");
assert_eq!(Sqlite.cast_aggregate_to_float("x"), "CAST(x AS REAL)");
}
#[test]
fn column_type_maps_to_sqlite_affinities() {
assert_eq!(Sqlite.column_type(FieldType::I16, None), "INTEGER");
assert_eq!(Sqlite.column_type(FieldType::I32, None), "INTEGER");
assert_eq!(Sqlite.column_type(FieldType::I64, None), "INTEGER");
assert_eq!(Sqlite.column_type(FieldType::F32, None), "REAL");
assert_eq!(Sqlite.column_type(FieldType::F64, None), "REAL");
assert_eq!(Sqlite.column_type(FieldType::Bool, None), "INTEGER");
assert_eq!(Sqlite.column_type(FieldType::String, None), "TEXT");
assert_eq!(Sqlite.column_type(FieldType::String, Some(64)), "TEXT");
assert_eq!(Sqlite.column_type(FieldType::DateTime, None), "TEXT");
assert_eq!(Sqlite.column_type(FieldType::Date, None), "TEXT");
assert_eq!(Sqlite.column_type(FieldType::Uuid, None), "TEXT");
assert_eq!(Sqlite.column_type(FieldType::Json, None), "TEXT");
}
#[test]
fn supports_op_rejects_postgres_jsonb_operators() {
assert!(!Sqlite.supports_op(Op::JsonContains));
assert!(!Sqlite.supports_op(Op::JsonContainedBy));
assert!(!Sqlite.supports_op(Op::JsonHasKey));
assert!(!Sqlite.supports_op(Op::JsonHasAnyKey));
assert!(!Sqlite.supports_op(Op::JsonHasAllKeys));
assert!(Sqlite.supports_op(Op::Eq));
assert!(Sqlite.supports_op(Op::ILike));
assert!(Sqlite.supports_op(Op::IsDistinctFrom));
}
#[test]
fn ilike_lowers_to_lower_like_lower() {
let mut sql = String::new();
Sqlite.write_ilike(&mut sql, "\"u\".\"name\"", "?", false);
assert_eq!(sql, "LOWER(\"u\".\"name\") LIKE LOWER(?)");
let mut neg = String::new();
Sqlite.write_ilike(&mut neg, "\"u\".\"name\"", "?", true);
assert_eq!(neg, "NOT (LOWER(\"u\".\"name\") LIKE LOWER(?))");
}
#[test]
fn null_safe_eq_uses_is_and_is_not() {
let mut eq = String::new();
Sqlite.write_null_safe_eq(&mut eq, "\"u\".\"deleted_at\"", "?", false);
assert_eq!(eq, "\"u\".\"deleted_at\" IS ?");
let mut neq = String::new();
Sqlite.write_null_safe_eq(&mut neq, "\"u\".\"deleted_at\"", "?", true);
assert_eq!(neq, "\"u\".\"deleted_at\" IS NOT ?");
}
#[test]
fn conflict_clause_do_nothing() {
let mut sql = String::new();
Sqlite
.write_conflict_clause(&mut sql, &ConflictClause::DoNothing)
.unwrap();
assert_eq!(sql, " ON CONFLICT DO NOTHING");
}
#[test]
fn conflict_clause_do_update_uses_excluded_alias() {
let mut sql = String::new();
Sqlite
.write_conflict_clause(
&mut sql,
&ConflictClause::DoUpdate {
target: vec!["user_id", "codename"],
update_columns: vec!["granted"],
},
)
.unwrap();
assert_eq!(
sql,
" ON CONFLICT (\"user_id\", \"codename\") DO UPDATE SET \"granted\" = excluded.\"granted\""
);
}
#[test]
fn compile_select_smoke_test() {
use crate::core::{ModelSchema, ModelScope, SelectQuery, WhereExpr};
static FIELDS: &[crate::core::FieldSchema] = &[crate::core::FieldSchema {
name: "id",
column: "id",
ty: FieldType::I64,
nullable: false,
primary_key: true,
relation: None,
max_length: None,
min: None,
max: None,
default: None,
auto: false,
unique: false,
generated_as: None,
}];
static MODEL: ModelSchema = ModelSchema {
name: "demo",
table: "demo",
fields: FIELDS,
display: None,
app_label: None,
admin: None,
soft_delete_column: None,
permissions: false,
audit_track: None,
m2m: &[],
indexes: &[],
check_constraints: &[],
composite_relations: &[],
generic_relations: &[],
scope: ModelScope::Tenant,
};
let q = SelectQuery {
model: &MODEL,
where_clause: WhereExpr::and_predicates(vec![crate::core::Filter {
column: "id",
op: Op::Eq,
value: SqlValue::I64(7),
}]),
order_by: vec![],
joins: vec![],
limit: None,
offset: None,
search: None,
};
let stmt = Sqlite.compile_select(&q).unwrap();
assert!(stmt.sql.contains("\"demo\""), "table quoted: {}", stmt.sql);
assert!(stmt.sql.contains("\"id\" = ?"), "predicate: {}", stmt.sql);
assert_eq!(stmt.params.len(), 1);
}
}