Skip to content

Commit 2d30630

Browse files
sgrifkaj
authored andcommitted
Support diesel_manage_updated_at on SQLite
There's a little bit of funkiness required to make this work. The first piece is that we need to modify the function registration to provide access to the connection for our internal functions. This isn't *strictly* necessary, we could `transmute` the `&self` to be `&'static self`, since the function will never be called after `self` is dropped. That felt like it would add some unneccessary unsafety though. The second bit of funkiness is that we have to have *some* return type, so we can't just return `()`. I think I want to fix this in the future by providing a `Null` SQL type, which provides `ToSql` and `FromSql` impls only for `()`. This requires adding a variant to `SqliteType` and `MysqlType` though, so it will have to be done in 2.0. The function itself is subtly different from the PostgreSQL version, since it runs even if no values were actually changed (we can't do something like `NEW IS DISTINCT FROM OLD` here). This also cannot be run on tables without ROWIDs. I think we should probably declare this as a SQL function in code somewhere for documentation purposes, but I want to wait until our docs are building again to follow up with that.
1 parent ddf4436 commit 2d30630

7 files changed

Lines changed: 123 additions & 40 deletions

File tree

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,11 @@ for Rust libraries in [RFC #1105](https://github.com/rust-lang/rfcs/blob/master/
3838

3939
* `Nullable<Text>` now supports `ilike` expression on in PostgreSQL.
4040

41+
* `diesel_manage_updated_at('table_name')` is now available on SQLite. This
42+
function can be called in your migrations to create a trigger which
43+
automatically sets the `updated_at` column, unless that column was updated in
44+
the query.
45+
4146
### Changed
4247

4348
* Diesel's derives now require that `extern crate diesel;` be at your crate root
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
CREATE TRIGGER __diesel_manage_updated_at_{table_name}
2+
AFTER UPDATE ON {table_name}
3+
FOR EACH ROW WHEN
4+
old.updated_at IS NULL AND
5+
new.updated_at IS NULL OR
6+
old.updated_at == new.updated_at
7+
BEGIN
8+
UPDATE {table_name}
9+
SET updated_at = CURRENT_TIMESTAMP
10+
WHERE ROWID = new.ROWID;
11+
END

diesel/src/sqlite/connection/functions.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ pub fn register<ArgsSqlType, RetSqlType, Args, Ret, F>(
1616
mut f: F,
1717
) -> QueryResult<()>
1818
where
19-
F: FnMut(Args) -> Ret + Send + 'static,
19+
F: FnMut(&RawConnection, Args) -> Ret + Send + 'static,
2020
Args: Queryable<ArgsSqlType, Sqlite>,
2121
Ret: ToSql<RetSqlType, Sqlite>,
2222
Sqlite: HasSqlType<RetSqlType>,
@@ -29,12 +29,12 @@ where
2929
));
3030
}
3131

32-
conn.register_sql_function(fn_name, fields_needed, deterministic, move |args| {
32+
conn.register_sql_function(fn_name, fields_needed, deterministic, move |conn, args| {
3333
let mut row = FunctionRow { args };
3434
let args_row = Args::Row::build_from_row(&mut row).map_err(Error::DeserializationError)?;
3535
let args = Args::build(args_row);
3636

37-
let result = f(args);
37+
let result = f(conn, args);
3838

3939
let mut buf = Output::new(Vec::new(), &());
4040
let is_null = result.to_sql(&mut buf).map_err(Error::SerializationError)?;

diesel/src/sqlite/connection/mod.rs

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -50,11 +50,17 @@ impl Connection for SqliteConnection {
5050
type TransactionManager = AnsiTransactionManager;
5151

5252
fn establish(database_url: &str) -> ConnectionResult<Self> {
53-
RawConnection::establish(database_url).map(|conn| SqliteConnection {
53+
use result::ConnectionError::CouldntSetupConfiguration;
54+
55+
let raw_connection = RawConnection::establish(database_url)?;
56+
let conn = Self {
5457
statement_cache: StatementCache::new(),
55-
raw_connection: conn,
58+
raw_connection,
5659
transaction_manager: AnsiTransactionManager::new(),
57-
})
60+
};
61+
conn.register_diesel_sql_functions()
62+
.map_err(CouldntSetupConfiguration)?;
63+
Ok(conn)
5864
}
5965

6066
#[doc(hidden)]
@@ -218,15 +224,37 @@ impl SqliteConnection {
218224
&self,
219225
fn_name: &str,
220226
deterministic: bool,
221-
f: F,
227+
mut f: F,
222228
) -> QueryResult<()>
223229
where
224230
F: FnMut(Args) -> Ret + Send + 'static,
225231
Args: Queryable<ArgsSqlType, Sqlite>,
226232
Ret: ToSql<RetSqlType, Sqlite>,
227233
Sqlite: HasSqlType<RetSqlType>,
228234
{
229-
functions::register(&self.raw_connection, fn_name, deterministic, f)
235+
functions::register(
236+
&self.raw_connection,
237+
fn_name,
238+
deterministic,
239+
move |_, args| f(args),
240+
)
241+
}
242+
243+
fn register_diesel_sql_functions(&self) -> QueryResult<()> {
244+
use sql_types::{Integer, Text};
245+
246+
functions::register::<Text, Integer, _, _, _>(
247+
&self.raw_connection,
248+
"diesel_manage_updated_at",
249+
false,
250+
|conn, table_name: String| {
251+
conn.exec(&format!(
252+
include_str!("diesel_manage_updated_at.sql"),
253+
table_name = table_name
254+
)).expect("Failed to create trigger");
255+
0 // have to return *something*
256+
},
257+
)
230258
}
231259
}
232260

diesel/src/sqlite/connection/raw.rs

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use std::ffi::{CStr, CString};
44
use std::io::{stderr, Write};
55
use std::os::raw as libc;
66
use std::ptr::NonNull;
7-
use std::{ptr, slice, str};
7+
use std::{mem, ptr, slice, str};
88

99
use super::serialized_value::SerializedValue;
1010
use result::Error::DatabaseError;
@@ -72,7 +72,9 @@ impl RawConnection {
7272
f: F,
7373
) -> QueryResult<()>
7474
where
75-
F: FnMut(&[*mut ffi::sqlite3_value]) -> QueryResult<SerializedValue> + Send + 'static,
75+
F: FnMut(&Self, &[*mut ffi::sqlite3_value]) -> QueryResult<SerializedValue>
76+
+ Send
77+
+ 'static,
7678
{
7779
let fn_name = CString::new(fn_name)?;
7880
let mut flags = ffi::SQLITE_UTF8;
@@ -143,9 +145,13 @@ extern "C" fn run_custom_function<F>(
143145
num_args: libc::c_int,
144146
value_ptr: *mut *mut ffi::sqlite3_value,
145147
) where
146-
F: FnMut(&[*mut ffi::sqlite3_value]) -> QueryResult<SerializedValue> + Send + 'static,
148+
F: FnMut(&RawConnection, &[*mut ffi::sqlite3_value]) -> QueryResult<SerializedValue>
149+
+ Send
150+
+ 'static,
147151
{
148-
const NULL_DATA_ERR: &str = "An unknown error occurred. sqlite3_user_data returned a null pointer. This should never happen.";
152+
static NULL_DATA_ERR: &str = "An unknown error occurred. sqlite3_user_data returned a null pointer. This should never happen.";
153+
static NULL_CONN_ERR: &str = "An unknown error occurred. sqlite3_context_db_handle returned a null pointer. This should never happen.";
154+
149155
unsafe {
150156
let data_ptr = ffi::sqlite3_user_data(ctx);
151157
let data_ptr = data_ptr as *mut F;
@@ -162,19 +168,36 @@ extern "C" fn run_custom_function<F>(
162168
};
163169

164170
let args = slice::from_raw_parts(value_ptr, num_args as _);
165-
match f(args) {
171+
let conn = match NonNull::new(ffi::sqlite3_context_db_handle(ctx)) {
172+
Some(conn) => RawConnection {
173+
internal_connection: conn,
174+
},
175+
None => {
176+
ffi::sqlite3_result_error(
177+
ctx,
178+
NULL_DATA_ERR.as_ptr() as *const _ as *const _,
179+
NULL_DATA_ERR.len() as _,
180+
);
181+
return;
182+
}
183+
};
184+
match f(&conn, args) {
166185
Ok(value) => value.result_of(ctx),
167186
Err(e) => {
168187
let msg = e.to_string();
169188
ffi::sqlite3_result_error(ctx, msg.as_ptr() as *const _, msg.len() as _);
170189
}
171190
}
191+
192+
mem::forget(conn);
172193
}
173194
}
174195

175196
extern "C" fn destroy_boxed_fn<F>(data: *mut libc::c_void)
176197
where
177-
F: FnMut(&[*mut ffi::sqlite3_value]) -> QueryResult<SerializedValue> + Send + 'static,
198+
F: FnMut(&RawConnection, &[*mut ffi::sqlite3_value]) -> QueryResult<SerializedValue>
199+
+ Send
200+
+ 'static,
178201
{
179202
let ptr = data as *mut F;
180203
unsafe { Box::from_raw(ptr) };

diesel_tests/tests/connection.rs

Lines changed: 41 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use diesel::dsl::sql;
22
use diesel::*;
3-
use schema::connection_without_transaction;
3+
use schema::{connection_without_transaction, DropTable};
44

55
table! {
66
auto_time {
@@ -11,50 +11,65 @@ table! {
1111
}
1212

1313
#[test]
14-
#[cfg(feature = "postgres")]
14+
#[cfg(any(feature = "postgres", feature = "sqlite"))]
1515
fn managing_updated_at_for_table() {
16-
use self::auto_time::columns::*;
17-
use self::auto_time::table as auto_time;
18-
use diesel::pg::types::date_and_time::PgTimestamp;
16+
use self::auto_time::dsl::*;
17+
use chrono::NaiveDateTime;
18+
use schema_dsl::*;
19+
use std::{thread, time::Duration};
1920

2021
// transactions have frozen time, so we can't use them
2122
let connection = connection_without_transaction();
22-
connection
23-
.execute(
24-
"CREATE TABLE auto_time (
25-
id SERIAL PRIMARY KEY,
26-
n INTEGER,
27-
updated_at TIMESTAMP
28-
);",
29-
)
23+
create_table(
24+
"auto_time",
25+
(
26+
integer("id").primary_key().auto_increment(),
27+
integer("n"),
28+
timestamp("updated_at"),
29+
),
30+
).execute(&connection)
3031
.unwrap();
31-
connection
32-
.execute("SELECT diesel_manage_updated_at('auto_time');")
32+
let _guard = DropTable {
33+
connection: &connection,
34+
table_name: "auto_time",
35+
};
36+
sql_query("SELECT diesel_manage_updated_at('auto_time')")
37+
.execute(&connection)
3338
.unwrap();
3439

35-
connection
36-
.execute("INSERT INTO auto_time (n) VALUES (2), (1), (5);")
40+
insert_into(auto_time)
41+
.values(&vec![n.eq(2), n.eq(1), n.eq(5)])
42+
.execute(&connection)
3743
.unwrap();
38-
let result = select(sql("COUNT(*) FROM auto_time WHERE updated_at IS NULL"))
44+
45+
let result = auto_time
46+
.count()
47+
.filter(updated_at.is_null())
3948
.get_result::<i64>(&connection);
4049
assert_eq!(Ok(3), result);
4150

42-
connection
43-
.execute("UPDATE auto_time SET n = n + 1 WHERE true;")
51+
update(auto_time)
52+
.set(n.eq(n + 1))
53+
.execute(&connection)
4454
.unwrap();
45-
let result = select(sql("COUNT(*) FROM auto_time WHERE updated_at IS NULL"))
55+
56+
let result = auto_time
57+
.count()
58+
.filter(updated_at.is_null())
4659
.get_result::<i64>(&connection);
4760
assert_eq!(Ok(0), result);
4861

62+
if cfg!(feature = "sqlite") {
63+
// SQLite only has second precision
64+
thread::sleep(Duration::from_millis(1000));
65+
}
66+
4967
let query = auto_time.find(2).select(updated_at);
50-
let old_time: PgTimestamp = query.first(&connection).unwrap();
68+
let old_time: NaiveDateTime = query.first(&connection).unwrap();
5169
update(auto_time.find(2))
5270
.set(n.eq(0))
5371
.execute(&connection)
5472
.unwrap();
55-
let new_time: PgTimestamp = query.first(&connection).unwrap();
73+
let new_time: NaiveDateTime = query.first(&connection).unwrap();
5674
assert!(old_time < new_time);
57-
58-
// clean up because we aren't in a transaction
59-
connection.execute("DROP TABLE auto_time;").unwrap();
6075
}

diesel_tests/tests/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
#[macro_use]
55
extern crate assert_matches;
6+
extern crate chrono;
67
#[macro_use]
78
extern crate diesel;
89
#[macro_use]

0 commit comments

Comments
 (0)