Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/impls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ pub(crate) mod scoped_relations;
pub(crate) mod session_variable;
pub(crate) mod shared_helpers;
pub(crate) mod sqlite_functions;
pub(crate) mod sqlite_keywords;
pub(crate) mod temporal_arithmetic;
pub(crate) mod temporal_literals;
pub(crate) mod timezone;
Expand Down
131 changes: 131 additions & 0 deletions src/impls/sqlite_keywords.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
//! Quoting for identifiers that SQLite reserves.

#[cfg(not(feature = "std"))]
use alloc::vec::Vec;
use core::{convert::Infallible, ops::ControlFlow};

use sqlparser::ast::{Expr, Ident, Statement, VisitMut, VisitorMut};

/// Keywords SQLite 3.51 rejects as a bare table, column or alias name, sorted
/// for binary search, without the `DEFAULT` the translator emits as a keyword.
const RESERVED: &[&str] = &[
"ADD",
"ALL",
"ALTER",
"AND",
"AS",
"AUTOINCREMENT",
"BETWEEN",
"CASE",
"CAST",
"CHECK",
"COLLATE",
"COMMIT",
"CONSTRAINT",
"CREATE",
"DEFERRABLE",
"DELETE",
"DISTINCT",
"DROP",
"ELSE",
"ESCAPE",
"EXCEPT",
"EXISTS",
"FOREIGN",
"FROM",
"GROUP",
"HAVING",
"IN",
"INDEX",
"INSERT",
"INTERSECT",
"INTO",
"IS",
"ISNULL",
"JOIN",
"LIMIT",
"NOT",
"NOTHING",
"NOTNULL",
"NULL",
"ON",
"OR",
"ORDER",
"PRIMARY",
"RAISE",
"RECURSIVE",
"REFERENCES",
"RETURNING",
"SELECT",
"SET",
"TABLE",
"THEN",
"TO",
"TRANSACTION",
"UNION",
"UNIQUE",
"UPDATE",
"USING",
"VALUES",
"WHEN",
"WHERE",
];

fn is_reserved(word: &str) -> bool {
RESERVED
.binary_search_by(|reserved| {
reserved.bytes().cmp(word.bytes().map(|byte| byte.to_ascii_uppercase()))
})
.is_ok()
}

/// Quotes every bare identifier SQLite reserves, except function names, which
/// SQLite reads from the call syntax and which include the bare `RAISE` the
/// translator emits in trigger bodies.
#[derive(Default)]
struct ReservedIdentifierQuoter {
function_names: Vec<*const Ident>,
}

impl VisitorMut for ReservedIdentifierQuoter {
type Break = Infallible;

fn pre_visit_expr(&mut self, expr: &mut Expr) -> ControlFlow<Self::Break> {
if let Expr::Function(function) = expr {
self.function_names.extend(
function.name.0.iter().filter_map(|part| part.as_ident()).map(core::ptr::from_ref),
);
}
ControlFlow::Continue(())
}

fn post_visit_ident(&mut self, ident: &mut Ident) -> ControlFlow<Self::Break> {
if ident.quote_style.is_none()
&& is_reserved(&ident.value)
&& !self.function_names.contains(&core::ptr::from_ref(ident))
{
// PostgreSQL folds a bare identifier to lowercase, so the quoted
// spelling is the folded one.
ident.value.make_ascii_lowercase();
ident.quote_style = Some('"');
}
ControlFlow::Continue(())
}
}

/// Double-quotes every bare identifier in `statement` that SQLite reserves.
pub(crate) fn quote_reserved_identifiers(statement: &mut Statement) {
let _ = statement.visit(&mut ReservedIdentifierQuoter::default());
}

#[cfg(test)]
mod tests {
use super::{RESERVED, is_reserved};

#[test]
fn reserved_words_are_sorted_and_matched_without_case() {
assert!(RESERVED.is_sorted());
assert!(is_reserved("set") && is_reserved("Values") && is_reserved("NOT"));
assert!(!is_reserved("default") && !is_reserved("key") && !is_reserved("replace"));
}
}
10 changes: 10 additions & 0 deletions src/initialization.rs
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,16 @@ impl<S> InitializationPlan<S> {
self.steps.into_iter().flat_map(|step| step.base)
}

/// Every statement of every form, rebound ones included.
pub(crate) fn statements_mut(&mut self) -> impl Iterator<Item = &mut S> {
self.steps.iter_mut().flat_map(|step| {
step.base
.iter_mut()
.chain(step.main_rebound.iter_mut().flatten())
.chain(step.attached_rebound.iter_mut().flatten())
})
}

pub(crate) fn map<T>(self, mut f: impl FnMut(S) -> T) -> InitializationPlan<T> {
let mut forms = |statements: Vec<S>| statements.into_iter().map(&mut f).collect();
let steps = self
Expand Down
5 changes: 5 additions & 0 deletions src/pg2sqlite.rs
Original file line number Diff line number Diff line change
Expand Up @@ -816,6 +816,11 @@ impl Pg2Sqlite {
attached,
);
let mut script = Script { setup: Vec::new(), files, connection };
// Last, so the replay and rebinding forms built above are covered too.
for statement in script.files.statements_mut().chain(script.connection.iter_mut().flatten())
{
crate::impls::sqlite_keywords::quote_reserved_identifiers(statement);
}

// PostgreSQL's LIKE is case-sensitive. SQLite's is case-insensitive for
// ASCII unless the connection says otherwise, and no expression-level
Expand Down
80 changes: 80 additions & 0 deletions tests/test_sqlite_keyword_identifiers.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
//! PostgreSQL lets a source name a table, column or alias with a word SQLite
//! reserves, such as `set`, `values` or `not`, so the emitted SQLite quotes it.
//!
//! Every expectation was read off PostgreSQL 18.6. The emitted statements are
//! executed as text because that text is the artifact under test.

use diesel::{prelude::*, sql_types::Integer};
use pg2sqlite::prelude::{Pg2Sqlite, Pg2SqliteOptions};

const FIXTURE: &str = "CREATE TABLE t (id int PRIMARY KEY, set int, values int);
INSERT INTO t (id, set, values) VALUES (1, 2, 3);
UPDATE t SET set = set + 10 WHERE values = 3;";

/// The rows the last emitted statement returns after every earlier one ran.
fn rows<T: QueryableByName<diesel::sqlite::Sqlite> + 'static>(pg: &str) -> Vec<T> {
let statements = Pg2Sqlite::default()
.sql(pg)
.expect("parse")
.translate_to_sql(&Pg2SqliteOptions::default())
.expect("translate")
.into_vec();
let mut connection = SqliteConnection::establish(":memory:").expect("connect");
let (probe, setup) = statements.split_last().expect("an emitted query");
for statement in setup {
diesel::sql_query(statement)
.execute(&mut connection)
.unwrap_or_else(|error| panic!("emitted setup failed: {statement}: {error}"));
}
diesel::sql_query(probe)
.load::<T>(&mut connection)
.unwrap_or_else(|error| panic!("emitted query failed: {probe}: {error}"))
}

#[derive(QueryableByName, Debug, PartialEq)]
struct Columns {
#[diesel(sql_type = Integer)]
id: i32,
#[diesel(sql_type = Integer, column_name = "set")]
set_: i32,
#[diesel(sql_type = Integer, column_name = "values")]
values_: i32,
}

/// Columns named by SQLite keywords work in `INSERT`, `UPDATE`, `WHERE` and the
/// projection.
#[test]
fn keyword_named_columns_work_through_dml() {
let read = rows::<Columns>(&format!("{FIXTURE}\nSELECT id, set, values FROM t ORDER BY id;"));
assert_eq!(read, [Columns { id: 1, set_: 12, values_: 3 }]);
}

#[derive(QueryableByName, Debug, PartialEq)]
struct Aliased {
#[diesel(sql_type = Integer, column_name = "not")]
not_: i32,
#[diesel(sql_type = Integer, column_name = "set")]
set_: i32,
}

/// A bare keyword alias and a qualified keyword column keep the lowercase names
/// PostgreSQL gives them.
#[test]
fn keyword_aliases_keep_postgres_column_names() {
let read = rows::<Aliased>(&format!("{FIXTURE}\nSELECT id NOT, s.SET FROM t AS s;"));
assert_eq!(read, [Aliased { not_: 1, set_: 12 }]);
}

#[derive(QueryableByName, Debug, PartialEq)]
struct Single {
#[diesel(sql_type = Integer)]
x: i32,
}

/// A table named by a SQLite keyword is created, written and read.
#[test]
fn keyword_named_table_is_created_and_read() {
let read =
rows::<Single>("CREATE TABLE set (x int); INSERT INTO set VALUES (7); SELECT x FROM set;");
assert_eq!(read, [Single { x: 7 }]);
}
Loading