Skip to content
Open
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
91 changes: 91 additions & 0 deletions sqlx-cli/tests/prepare.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
use assert_cmd::Command;
use std::fs;
use std::path::{Path, PathBuf};

fn run_prepare(project_dir: &Path, target_dir: &Path) {
Command::cargo_bin("cargo-sqlx")
.unwrap()
.current_dir(project_dir)
.env("CARGO_TARGET_DIR", target_dir)
.args([
"sqlx",
"prepare",
"--database-url",
"sqlite::memory:",
"--",
"--lib",
])
.assert()
.success();
}

fn query_file(project_dir: &Path) -> PathBuf {
let mut query_files = fs::read_dir(project_dir.join(".sqlx"))
.unwrap()
.map(|entry| entry.unwrap().path())
.filter(|path| {
path.file_name()
.and_then(|name| name.to_str())
.is_some_and(|name| name.starts_with("query-") && name.ends_with(".json"))
});
let query_file = query_files
.next()
.expect("prepare did not create query data");

assert!(
query_files.next().is_none(),
"prepare created more than one query file"
);

query_file
}

#[test]
fn repeated_sqlite_prepare_is_byte_stable() {
let workspace_root = Path::new(env!("CARGO_MANIFEST_DIR")).parent().unwrap();
let temp_dir = tempfile::Builder::new()
.prefix("sqlx-cli-prepare-")
.tempdir_in(workspace_root.join("target"))
.unwrap();
let project_dir = temp_dir.path().join("fixture");
let target_dir = temp_dir.path().join("target");
fs::create_dir_all(project_dir.join("src")).unwrap();
fs::write(
project_dir.join("Cargo.toml"),
format!(
r#"[package]
name = "sqlite-prepare-fixture"
version = "0.0.0"
edition = "2021"

[workspace]

[dependencies]
sqlx = {{ path = {:?}, default-features = false, features = ["macros", "runtime-tokio", "sqlite"] }}
"#,
workspace_root
),
)
.unwrap();
fs::write(
project_dir.join("src/lib.rs"),
r#"pub fn query() {
let _ = sqlx::query!("SELECT 1 AS value");
}
"#,
)
.unwrap();

run_prepare(&project_dir, &target_dir);
let first = fs::read(query_file(&project_dir)).unwrap();
run_prepare(&project_dir, &target_dir);
let second = fs::read(query_file(&project_dir)).unwrap();
let expected = include_bytes!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/../sqlx-macros-core/tests/fixtures/sqlite-query.json"
));

assert_eq!(first, expected);
assert_eq!(second, expected);
assert_eq!(first, second);
}
3 changes: 3 additions & 0 deletions sqlx-macros-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,9 @@ syn = { version = "2.0.87", default-features = false, features = ["full", "deriv
quote = { version = "1.0.35", default-features = false }
url = { version = "2.2.2" }

[dev-dependencies]
serde_json = { version = "1.0.142", features = ["preserve_order"] }

[lints.rust.unexpected_cfgs]
level = "warn"
check-cfg = ['cfg(sqlx_macros_unstable)', 'cfg(procmacro2_semver_exempt)']
141 changes: 129 additions & 12 deletions sqlx-macros-core/src/query/data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -182,18 +182,7 @@ where
}
};

// From a quick survey of the files generated by `examples/postgres/axum-social-with-tests`,
// which are generally in the 1-2 KiB range, this seems like a safe bet to avoid
// lots of reallocations without using too much memory.
//
// As of writing, `serde_json::to_vec_pretty()` only allocates 128 bytes up-front.
let mut data = Vec::with_capacity(4096);

serde_json::to_writer_pretty(&mut data, self).expect("BUG: failed to serialize query data");

// Ensure there is a newline at the end of the JSON file to avoid
// accidental modification by IDE and make github diff tool happier.
data.push(b'\n');
let data = serialize_query_data(self);

// This ideally writes the data in as few syscalls as possible.
file.write_all(&data)
Expand All @@ -206,9 +195,137 @@ where
}
}

fn serialize_query_data(data: &impl Serialize) -> Vec<u8> {
// From a quick survey of the files generated by `examples/postgres/axum-social-with-tests`,
// which are generally in the 1-2 KiB range, this seems like a safe bet to avoid
// lots of reallocations without using too much memory.
//
// As of writing, `serde_json::to_vec_pretty()` only allocates 128 bytes up-front.
let mut serialized = Vec::with_capacity(4096);
let mut data =
serde_json::to_value(data).expect("BUG: failed to convert query data to JSON value");

data.sort_all_objects();

serde_json::to_writer_pretty(&mut serialized, &data)
.expect("BUG: failed to serialize query data");

// Ensure there is a newline at the end of the JSON file to avoid
// accidental modification by IDE and make github diff tool happier.
serialized.push(b'\n');
serialized
}

pub(super) fn hash_string(query: &str) -> String {
// picked `sha2` because it's already in the dependency tree for both MySQL and Postgres
use sha2::{Digest, Sha256};

hex::encode(Sha256::digest(query.as_bytes()))
}

#[cfg(test)]
mod tests {
use super::serialize_query_data;
use serde::ser::SerializeMap;
use serde::{Serialize, Serializer};

enum OrderedValue<'a> {
Number(u64),
Object(Vec<(&'a str, OrderedValue<'a>)>),
}

impl Serialize for OrderedValue<'_> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match self {
Self::Number(number) => serializer.serialize_u64(*number),
Self::Object(entries) => {
let mut map = serializer.serialize_map(Some(entries.len()))?;
for (key, value) in entries {
map.serialize_entry(key, value)?;
}
map.end()
}
}
}
}

#[test]
fn query_data_json_keys_are_sorted_recursively() {
let first = OrderedValue::Object(vec![
(
"z",
OrderedValue::Object(vec![
("second", OrderedValue::Number(2)),
("first", OrderedValue::Number(1)),
]),
),
("a", OrderedValue::Number(0)),
]);
let second = OrderedValue::Object(vec![
("a", OrderedValue::Number(0)),
(
"z",
OrderedValue::Object(vec![
("first", OrderedValue::Number(1)),
("second", OrderedValue::Number(2)),
]),
),
]);

let expected =
b"{\n \"a\": 0,\n \"z\": {\n \"first\": 1,\n \"second\": 2\n }\n}\n";
let first = serialize_query_data(&first);
let second = serialize_query_data(&second);

assert_eq!(first, expected);
assert_eq!(second, expected);
assert_eq!(first, second);
}

#[cfg(feature = "_sqlite")]
#[test]
fn sqlite_query_data_is_stable_across_repeated_saves() {
use super::{DynQueryData, QueryData};
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};

struct RemoveOnDrop(PathBuf);

impl Drop for RemoveOnDrop {
fn drop(&mut self) {
std::fs::remove_dir_all(&self.0).ok();
}
}

static NEXT_TEST_DIR_ID: AtomicU64 = AtomicU64::new(0);

let fixture = include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/fixtures/sqlite-query.json"
));
let dyn_data: DynQueryData = serde_json::from_str(fixture).unwrap();
let query_data = QueryData::<sqlx_sqlite::Sqlite>::from_dyn_data(dyn_data).unwrap();
let test_dir = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../target/sqlx-macros-core-tests")
.join(format!(
"{}-{}",
std::process::id(),
NEXT_TEST_DIR_ID.fetch_add(1, Ordering::Relaxed)
));
std::fs::create_dir_all(&test_dir).unwrap();
let _remove_on_drop = RemoveOnDrop(test_dir.clone());
let query_path = test_dir.join(format!("query-{}.json", query_data.hash));

query_data.save_in(&test_dir).unwrap();
let first = std::fs::read(&query_path).unwrap();
query_data.save_in(&test_dir).unwrap();
let second = std::fs::read(query_path).unwrap();

assert_eq!(first, fixture.as_bytes());
assert_eq!(second, fixture.as_bytes());
assert_eq!(first, second);
}
}
21 changes: 21 additions & 0 deletions sqlx-macros-core/tests/fixtures/sqlite-query.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"db_name": "SQLite",
"describe": {
"columns": [
{
"name": "value",
"ordinal": 0,
"origin": "Expression",
"type_info": "Integer"
}
],
"nullable": [
false
],
"parameters": {
"Right": 0
}
},
"hash": "b8ccac726e885e4bae2a0596e87cc88c5113c16e87c3bdec2ffb0e09e49cb29e",
"query": "SELECT 1 AS value"
}
Loading