Skip to content

Commit aa55b04

Browse files
committed
Merge #9: fix: Avoid inserting block rows of duplicate heights
e04110a test(async_store): Add test `block_table_height_is_unique` (valued mammal) 8f11f58 fix: Avoid inserting new block rows of existing height (valued mammal) Pull request description: This is a hotfix intended to address #8. The approach is to have an additional `SELECT` query to check for existing rows of a given height before executing the actual `INSERT` statement. A better long-term solution which may be done as a follow-up is to enforce a unique block height per row directly at the schema level. Top commit has no ACKs. Tree-SHA512: 6a66c7aac13ac41907e560561ddcbfe98628a2be272ef72711da23b53b3143709653684cfe1003d1d8841ffb243103073a31e5c2bb797dc5b9aca67034eac025
2 parents f2a808d + e04110a commit aa55b04

File tree

1 file changed

+79
-3
lines changed

1 file changed

+79
-3
lines changed

src/async_store.rs

Lines changed: 79 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -140,11 +140,20 @@ impl Store {
140140
for (&height, hash) in &local_chain.blocks {
141141
match hash {
142142
Some(hash) => {
143-
sqlx::query("INSERT OR IGNORE INTO block(height, hash) VALUES($1, $2)")
143+
// Avoid inserting new rows of existing height.
144+
// FIXME: The correct way to handle this is to have a unique constraint on `height`
145+
// in the block table schema.
146+
let row_option = sqlx::query("SELECT height FROM block WHERE height = $1")
144147
.bind(height)
145-
.bind(hash.to_string())
146-
.execute(&self.pool)
148+
.fetch_optional(&self.pool)
147149
.await?;
150+
if row_option.is_none() {
151+
sqlx::query("INSERT OR IGNORE INTO block(height, hash) VALUES($1, $2)")
152+
.bind(height)
153+
.bind(hash.to_string())
154+
.execute(&self.pool)
155+
.await?;
156+
}
148157
}
149158
None => {
150159
sqlx::query("DELETE FROM block WHERE height = $1")
@@ -305,3 +314,70 @@ impl Store {
305314
Ok(changeset)
306315
}
307316
}
317+
318+
#[cfg(test)]
319+
mod test {
320+
use super::*;
321+
322+
use bitcoin::hashes::Hash;
323+
324+
#[tokio::test]
325+
async fn block_table_height_is_unique() -> anyhow::Result<()> {
326+
let mut cs = local_chain::ChangeSet::default();
327+
cs.blocks.insert(0, Some(Hash::hash(b"0")));
328+
cs.blocks.insert(1, Some(Hash::hash(b"1")));
329+
330+
let store = Store::new_memory().await?;
331+
store.migrate().await?;
332+
store
333+
.write_local_chain(&cs)
334+
.await
335+
.expect("failed to write `local_chain`");
336+
337+
// Trying to replace the value of existing height should be ignored.
338+
cs.blocks.insert(1, Some(Hash::hash(b"1a")));
339+
340+
store
341+
.write_local_chain(&cs)
342+
.await
343+
.expect("failed to write `local_chain`");
344+
345+
let rows = sqlx::query("SELECT height, hash FROM block WHERE height = 1")
346+
.fetch_all(&store.pool)
347+
.await?;
348+
349+
assert_eq!(rows.len(), 1, "Expected 1 block row");
350+
351+
let row = rows.first().unwrap();
352+
let row_hash: String = row.get("hash");
353+
let expected_hash: BlockHash = Hash::hash(b"1");
354+
assert_eq!(row_hash, expected_hash.to_string());
355+
356+
// Delete row 1 and insert hash "1a" again.
357+
let mut cs = local_chain::ChangeSet::default();
358+
cs.blocks.insert(1, None);
359+
store
360+
.write_local_chain(&cs)
361+
.await
362+
.expect("failed to write `local_chain`");
363+
364+
cs.blocks.insert(1, Some(Hash::hash(b"1a")));
365+
store
366+
.write_local_chain(&cs)
367+
.await
368+
.expect("failed to write `local_chain`");
369+
370+
let rows = sqlx::query("SELECT height, hash FROM block WHERE height = 1")
371+
.fetch_all(&store.pool)
372+
.await?;
373+
374+
// Row hash should change to "1a".
375+
assert_eq!(rows.len(), 1, "Expected 1 block row");
376+
let row = rows.first().unwrap();
377+
let row_hash: String = row.get("hash");
378+
let expected_hash: BlockHash = Hash::hash(b"1a");
379+
assert_eq!(row_hash, expected_hash.to_string());
380+
381+
Ok(())
382+
}
383+
}

0 commit comments

Comments
 (0)