Skip to content

Commit 420d86e

Browse files
authored
Merge pull request diesel-rs#1955 from kaj/sg-sqlite-manage-updated-at
Rebase of diesel-rs#1871: Support `diesel_manage_updated_at` on SQLite
2 parents ddf4436 + e4295ba commit 420d86e

7 files changed

Lines changed: 126 additions & 42 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: 34 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,38 @@ 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+
))
255+
.expect("Failed to create trigger");
256+
0 // have to return *something*
257+
},
258+
)
230259
}
231260
}
232261

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: 43 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
use diesel::dsl::sql;
21
use diesel::*;
3-
use schema::connection_without_transaction;
2+
use schema::{connection_without_transaction, DropTable};
43

54
table! {
65
auto_time {
@@ -11,50 +10,66 @@ table! {
1110
}
1211

1312
#[test]
14-
#[cfg(feature = "postgres")]
13+
#[cfg(any(feature = "postgres", feature = "sqlite"))]
1514
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;
15+
use self::auto_time::dsl::*;
16+
use chrono::NaiveDateTime;
17+
use schema_dsl::*;
18+
use std::{thread, time::Duration};
1919

2020
// transactions have frozen time, so we can't use them
2121
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-
)
30-
.unwrap();
31-
connection
32-
.execute("SELECT diesel_manage_updated_at('auto_time');")
22+
create_table(
23+
"auto_time",
24+
(
25+
integer("id").primary_key().auto_increment(),
26+
integer("n"),
27+
timestamp("updated_at"),
28+
),
29+
)
30+
.execute(&connection)
31+
.unwrap();
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)