Skip to content

Commit 56fbeb4

Browse files
committed
feat(sqlite): Add json_extract and jsonb_extract functions
1 parent 6831488 commit 56fbeb4

File tree

3 files changed

+207
-1
lines changed

3 files changed

+207
-1
lines changed

diesel/src/sqlite/expression/functions.rs

+195-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
//! SQLite specific functions
2-
use crate::expression::functions::declare_sql_function;
2+
use crate::expression::{functions::declare_sql_function, AsExpression};
33
use crate::sql_types::*;
44
use crate::sqlite::expression::expression_methods::BinaryOrNullableBinary;
55
use crate::sqlite::expression::expression_methods::JsonOrNullableJson;
@@ -892,4 +892,198 @@ extern "SQL" {
892892
j: J,
893893
path: Text,
894894
) -> Nullable<Text>;
895+
896+
/// The json_extract(X,P1,P2,...) extracts and returns one or more values from the well-formed JSON at X.
897+
///
898+
/// If only a single path P1 is provided, then the SQL datatype of the result is NULL for a JSON null,
899+
/// INTEGER or REAL for a JSON numeric value, an INTEGER zero for a JSON false value, an INTEGER one for a JSON true value,
900+
/// the dequoted text for a JSON string value, and a text representation for JSON object and array values.
901+
///
902+
/// If there are multiple path arguments (P1, P2, and so forth) then this routine returns SQLite text
903+
/// which is a well-formed JSON array holding the various values.
904+
///
905+
/// # Example
906+
///
907+
/// ```rust
908+
/// # include!("../../doctest_setup.rs");
909+
/// #
910+
/// # fn main() {
911+
/// # #[cfg(feature = "serde_json")]
912+
/// # run_test().unwrap();
913+
/// # }
914+
/// #
915+
/// # #[cfg(feature = "serde_json")]
916+
/// # fn run_test() -> QueryResult<()> {
917+
/// # use diesel::dsl::{sql, json_extract};
918+
/// # use serde_json::{json, Value};
919+
/// # use diesel::sql_types::{Text, Json, Nullable};
920+
/// # let connection = &mut establish_connection();
921+
///
922+
/// let version = diesel::select(sql::<Text>("sqlite_version();"))
923+
/// .get_result::<String>(connection)?;
924+
///
925+
/// // Querying SQLite version should not fail.
926+
/// let version_components: Vec<&str> = version.split('.').collect();
927+
/// let major: u32 = version_components[0].parse().unwrap();
928+
/// let minor: u32 = version_components[1].parse().unwrap();
929+
/// let patch: u32 = version_components[2].parse().unwrap();
930+
///
931+
/// if major > 3 || (major == 3 && minor >= 38) {
932+
/// /* Valid sqlite version, do nothing */
933+
/// } else {
934+
/// println!("SQLite version is too old, skipping the test.");
935+
/// return Ok(());
936+
/// }
937+
///
938+
/// let json_obj = json!({"a": 2, "c": [4, 5, {"f": 7}]});
939+
///
940+
/// // Extract the entire JSON object
941+
/// let result = diesel::select(json_extract::<Json, _>(json_obj.clone(), "$"))
942+
/// .get_result::<Value>(connection)?;
943+
/// assert_eq!(result, json!({"a": 2, "c": [4, 5, {"f": 7}]}));
944+
///
945+
/// // Extract a nested array
946+
/// let result = diesel::select(json_extract::<Json, _>(json_obj.clone(), "$.c"))
947+
/// .get_result::<Value>(connection)?;
948+
/// assert_eq!(result, json!([4, 5, {"f": 7}]));
949+
///
950+
/// // Extract a nested object
951+
/// let result = diesel::select(json_extract::<Json, _>(json_obj.clone(), "$.c[2]"))
952+
/// .get_result::<Value>(connection)?;
953+
/// assert_eq!(result, json!({"f": 7}));
954+
///
955+
/// // Extract a numeric value
956+
/// let result = diesel::select(json_extract::<Json, _>(json_obj.clone(), "$.c[2].f"))
957+
/// .get_result::<i64>(connection)?;
958+
/// assert_eq!(result, 7);
959+
///
960+
/// // Extract a non-existent path (returns NULL)
961+
/// let result = diesel::select(json_extract::<Json, _>(json_obj.clone(), "$.x"))
962+
/// .get_result::<Option<Value>>(connection)?;
963+
/// assert_eq!(result, None);
964+
///
965+
/// // Extract a string value
966+
/// let json_with_string = json!({"a": "xyz"});
967+
/// let result = diesel::select(json_extract::<Json, _>(json_with_string, "$.a"))
968+
/// .get_result::<String>(connection)?;
969+
/// assert_eq!(result, "xyz");
970+
///
971+
/// // Extract a null value
972+
/// let json_with_null = json!({"a": null});
973+
/// let result = diesel::select(json_extract::<Json, _>(json_with_null, "$.a"))
974+
/// .get_result::<Option<Value>>(connection)?;
975+
/// assert_eq!(result, None);
976+
///
977+
/// // Test with NULL JSON input
978+
/// let result = diesel::select(json_extract::<Nullable<Json>, _>(None::<Value>, "$.a"))
979+
/// .get_result::<Option<Value>>(connection)?;
980+
/// assert_eq!(result, None);
981+
///
982+
/// # Ok(())
983+
/// # }
984+
/// ```
985+
#[cfg(feature = "sqlite")]
986+
fn json_extract<
987+
J: JsonOrNullableJsonOrJsonbOrNullableJsonb + SingleValue,
988+
P: AsExpression<Text> + SingleValue,
989+
>(
990+
j: J,
991+
path: P,
992+
) -> Nullable<Text>;
993+
994+
/// The jsonb_extract() function works the same as the json_extract() function, except in cases
995+
/// where json_extract() would normally return a text JSON array object, this routine returns
996+
/// the array or object in the JSONB format.
997+
///
998+
/// For the common case where a text, numeric, null, or boolean JSON element is returned,
999+
/// this routine works exactly the same as json_extract().
1000+
///
1001+
/// # Example
1002+
///
1003+
/// ```rust
1004+
/// # include!("../../doctest_setup.rs");
1005+
/// #
1006+
/// # fn main() {
1007+
/// # #[cfg(feature = "serde_json")]
1008+
/// # run_test().unwrap();
1009+
/// # }
1010+
/// #
1011+
/// # #[cfg(feature = "serde_json")]
1012+
/// # fn run_test() -> QueryResult<()> {
1013+
/// # use diesel::dsl::{sql, jsonb_extract};
1014+
/// # use serde_json::{json, Value};
1015+
/// # use diesel::sql_types::{Text, Jsonb, Nullable};
1016+
/// # let connection = &mut establish_connection();
1017+
///
1018+
/// let version = diesel::select(sql::<Text>("sqlite_version();"))
1019+
/// .get_result::<String>(connection)?;
1020+
///
1021+
/// // Querying SQLite version should not fail.
1022+
/// let version_components: Vec<&str> = version.split('.').collect();
1023+
/// let major: u32 = version_components[0].parse().unwrap();
1024+
/// let minor: u32 = version_components[1].parse().unwrap();
1025+
/// let patch: u32 = version_components[2].parse().unwrap();
1026+
///
1027+
/// if major > 3 || (major == 3 && minor >= 45) {
1028+
/// /* Valid sqlite version, do nothing */
1029+
/// } else {
1030+
/// println!("SQLite version is too old, skipping the test.");
1031+
/// return Ok(());
1032+
/// }
1033+
///
1034+
/// let json_obj = json!({"a": 2, "c": [4, 5, {"f": 7}]});
1035+
///
1036+
/// // Extract the entire JSON object
1037+
/// let result = diesel::select(jsonb_extract::<Jsonb, _>(json_obj.clone(), "$"))
1038+
/// .get_result::<Value>(connection)?;
1039+
/// assert_eq!(result, json!({"a": 2, "c": [4, 5, {"f": 7}]}));
1040+
///
1041+
/// // Extract a nested array
1042+
/// let result = diesel::select(jsonb_extract::<Jsonb, _>(json_obj.clone(), "$.c"))
1043+
/// .get_result::<Value>(connection)?;
1044+
/// assert_eq!(result, json!([4, 5, {"f": 7}]));
1045+
///
1046+
/// // Extract a nested object
1047+
/// let result = diesel::select(jsonb_extract::<Jsonb, _>(json_obj.clone(), "$.c[2]"))
1048+
/// .get_result::<Value>(connection)?;
1049+
/// assert_eq!(result, json!({"f": 7}));
1050+
///
1051+
/// // Extract a numeric value
1052+
/// let result = diesel::select(jsonb_extract::<Jsonb, _>(json_obj.clone(), "$.c[2].f"))
1053+
/// .get_result::<i64>(connection)?;
1054+
/// assert_eq!(result, 7);
1055+
///
1056+
/// // Extract a non-existent path (returns NULL)
1057+
/// let result = diesel::select(jsonb_extract::<Jsonb, _>(json_obj.clone(), "$.x"))
1058+
/// .get_result::<Option<Value>>(connection)?;
1059+
/// assert_eq!(result, None);
1060+
///
1061+
/// // Extract a string value
1062+
/// let json_with_string = json!({"a": "xyz"});
1063+
/// let result = diesel::select(jsonb_extract::<Jsonb, _>(json_with_string, "$.a"))
1064+
/// .get_result::<String>(connection)?;
1065+
/// assert_eq!(result, "xyz");
1066+
///
1067+
/// // Extract a null value
1068+
/// let json_with_null = json!({"a": null});
1069+
/// let result = diesel::select(jsonb_extract::<Jsonb, _>(json_with_null, "$.a"))
1070+
/// .get_result::<Option<Value>>(connection)?;
1071+
/// assert_eq!(result, None);
1072+
///
1073+
/// // Test with NULL JSON input
1074+
/// let result = diesel::select(jsonb_extract::<Nullable<Jsonb>, _>(None::<Value>, "$.a"))
1075+
/// .get_result::<Option<Value>>(connection)?;
1076+
/// assert_eq!(result, None);
1077+
///
1078+
/// # Ok(())
1079+
/// # }
1080+
/// ```
1081+
#[cfg(feature = "sqlite")]
1082+
fn jsonb_extract<
1083+
J: JsonOrNullableJsonOrJsonbOrNullableJsonb + SingleValue,
1084+
P: AsExpression<Text> + SingleValue,
1085+
>(
1086+
j: J,
1087+
path: P,
1088+
) -> Nullable<Text>;
8951089
}

diesel/src/sqlite/expression/helper_types.rs

+10
Original file line numberDiff line numberDiff line change
@@ -58,3 +58,13 @@ pub type json_type<E> = super::functions::json_type<SqlTypeOf<E>, E>;
5858
#[allow(non_camel_case_types)]
5959
#[cfg(feature = "sqlite")]
6060
pub type json_type_with_path<J, P> = super::functions::json_type_with_path<SqlTypeOf<J>, J, P>;
61+
62+
/// Return type of [`json_extract(json, path)`](super::functions::json_extract())
63+
#[allow(non_camel_case_types)]
64+
#[cfg(feature = "sqlite")]
65+
pub type json_extract<J, P> = super::functions::json_extract<J, P, J, P>;
66+
67+
/// Return type of [`jsonb_extract(json, path)`](super::functions::jsonb_extract())
68+
#[allow(non_camel_case_types)]
69+
#[cfg(feature = "sqlite")]
70+
pub type jsonb_extract<J, P> = super::functions::jsonb_extract<J, P, J, P>;

diesel_derives/tests/auto_type.rs

+2
Original file line numberDiff line numberDiff line change
@@ -516,6 +516,8 @@ fn sqlite_functions() -> _ {
516516
json_valid(sqlite_extras::json),
517517
json_type(sqlite_extras::json),
518518
json_type_with_path(sqlite_extras::json, sqlite_extras::text),
519+
json_extract(sqlite_extras::json, sqlite_extras::text),
520+
jsonb_extract(sqlite_extras::jsonb, sqlite_extras::text),
519521
)
520522
}
521523

0 commit comments

Comments
 (0)