Skip to content

Commit 8f41f5b

Browse files
refactor: Keep parsed sqlx-data.json in a cache instead of reparsing (#1684)
1 parent 5f7e25b commit 8f41f5b

File tree

1 file changed

+72
-78
lines changed

1 file changed

+72
-78
lines changed

sqlx-macros/src/query/data.rs

Lines changed: 72 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -38,15 +38,43 @@ pub mod offline {
3838
use super::QueryData;
3939
use crate::database::DatabaseExt;
4040

41-
use std::fmt::{self, Formatter};
42-
use std::fs::File;
43-
use std::io::{BufReader, BufWriter};
44-
use std::path::Path;
41+
use std::collections::BTreeMap;
42+
use std::fs::{self, File};
43+
use std::io::BufWriter;
44+
use std::path::{Path, PathBuf};
45+
use std::sync::Mutex;
4546

47+
use once_cell::sync::Lazy;
4648
use proc_macro2::Span;
47-
use serde::de::{Deserializer, IgnoredAny, MapAccess, Visitor};
4849
use sqlx_core::describe::Describe;
4950

51+
static OFFLINE_DATA_CACHE: Lazy<Mutex<BTreeMap<PathBuf, OfflineData>>> =
52+
Lazy::new(|| Mutex::new(BTreeMap::new()));
53+
54+
#[derive(serde::Deserialize)]
55+
struct BaseQuery {
56+
query: String,
57+
describe: serde_json::Value,
58+
}
59+
60+
#[derive(serde::Deserialize)]
61+
struct OfflineData {
62+
db: String,
63+
#[serde(flatten)]
64+
hash_to_query: BTreeMap<String, BaseQuery>,
65+
}
66+
67+
impl OfflineData {
68+
fn get_query_from_hash(&self, hash: &str) -> Option<DynQueryData> {
69+
self.hash_to_query.get(hash).map(|base_query| DynQueryData {
70+
db_name: self.db.clone(),
71+
query: base_query.query.to_owned(),
72+
describe: base_query.describe.to_owned(),
73+
hash: hash.to_owned(),
74+
})
75+
}
76+
}
77+
5078
#[derive(serde::Deserialize)]
5179
pub struct DynQueryData {
5280
#[serde(skip)]
@@ -61,15 +89,44 @@ pub mod offline {
6189
/// Find and deserialize the data table for this query from a shared `sqlx-data.json`
6290
/// file. The expected structure is a JSON map keyed by the SHA-256 hash of queries in hex.
6391
pub fn from_data_file(path: impl AsRef<Path>, query: &str) -> crate::Result<Self> {
64-
let this = serde_json::Deserializer::from_reader(BufReader::new(
65-
File::open(path.as_ref()).map_err(|e| {
66-
format!("failed to open path {}: {}", path.as_ref().display(), e)
67-
})?,
68-
))
69-
.deserialize_map(DataFileVisitor {
70-
query,
71-
hash: hash_string(query),
72-
})?;
92+
let path = path.as_ref();
93+
94+
let query_data = {
95+
let mut cache = OFFLINE_DATA_CACHE
96+
.lock()
97+
// Just reset the cache on error
98+
.unwrap_or_else(|posion_err| {
99+
let mut guard = posion_err.into_inner();
100+
*guard = BTreeMap::new();
101+
guard
102+
});
103+
104+
if !cache.contains_key(path) {
105+
let offline_data_contents = fs::read_to_string(path)
106+
.map_err(|e| format!("failed to read path {}: {}", path.display(), e))?;
107+
let offline_data: OfflineData = serde_json::from_str(&offline_data_contents)?;
108+
let _ = cache.insert(path.to_owned(), offline_data);
109+
}
110+
111+
let offline_data = cache
112+
.get(path)
113+
.expect("Missing data should have just been added");
114+
115+
let query_hash = hash_string(query);
116+
let query_data = offline_data
117+
.get_query_from_hash(&query_hash)
118+
.ok_or_else(|| format!("failed to find data for query {}", query))?;
119+
120+
if query != query_data.query {
121+
return Err(format!(
122+
"hash collision for stored queryies:\n{:?}\n{:?}",
123+
query, query_data.query
124+
)
125+
.into());
126+
}
127+
128+
query_data
129+
};
73130

74131
#[cfg(procmacr2_semver_exempt)]
75132
{
@@ -84,7 +141,7 @@ pub mod offline {
84141
proc_macro::tracked_path::path(path);
85142
}
86143

87-
Ok(this)
144+
Ok(query_data)
88145
}
89146
}
90147

@@ -138,67 +195,4 @@ pub mod offline {
138195

139196
hex::encode(Sha256::digest(query.as_bytes()))
140197
}
141-
142-
// lazily deserializes only the `QueryData` for the query we're looking for
143-
struct DataFileVisitor<'a> {
144-
query: &'a str,
145-
hash: String,
146-
}
147-
148-
impl<'de> Visitor<'de> for DataFileVisitor<'_> {
149-
type Value = DynQueryData;
150-
151-
fn expecting(&self, f: &mut Formatter) -> fmt::Result {
152-
write!(f, "expected map key {:?} or \"db\"", self.hash)
153-
}
154-
155-
fn visit_map<A>(self, mut map: A) -> Result<Self::Value, <A as MapAccess<'de>>::Error>
156-
where
157-
A: MapAccess<'de>,
158-
{
159-
let mut db_name: Option<String> = None;
160-
161-
let query_data = loop {
162-
// unfortunately we can't avoid this copy because deserializing from `io::Read`
163-
// doesn't support deserializing borrowed values
164-
let key = map.next_key::<String>()?.ok_or_else(|| {
165-
serde::de::Error::custom(format_args!(
166-
"failed to find data for query {}",
167-
self.hash
168-
))
169-
})?;
170-
171-
// lazily deserialize the query data only
172-
if key == "db" {
173-
db_name = Some(map.next_value::<String>()?);
174-
} else if key == self.hash {
175-
let db_name = db_name.ok_or_else(|| {
176-
serde::de::Error::custom("expected \"db\" key before query hash keys")
177-
})?;
178-
179-
let mut query_data: DynQueryData = map.next_value()?;
180-
181-
if query_data.query == self.query {
182-
query_data.db_name = db_name;
183-
query_data.hash = self.hash.clone();
184-
break query_data;
185-
} else {
186-
return Err(serde::de::Error::custom(format_args!(
187-
"hash collision for stored queries:\n{:?}\n{:?}",
188-
self.query, query_data.query
189-
)));
190-
};
191-
} else {
192-
// we don't care about entries that don't match our hash
193-
let _ = map.next_value::<IgnoredAny>()?;
194-
}
195-
};
196-
197-
// Serde expects us to consume the whole map; fortunately they've got a convenient
198-
// type to let us do just that
199-
while let Some(_) = map.next_entry::<IgnoredAny, IgnoredAny>()? {}
200-
201-
Ok(query_data)
202-
}
203-
}
204198
}

0 commit comments

Comments
 (0)