Skip to content

Commit fcb1bea

Browse files
committed
merge db-watch into db-live-query
1 parent cf1790b commit fcb1bea

16 files changed

Lines changed: 673 additions & 565 deletions

File tree

Cargo.lock

Lines changed: 0 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,6 @@ hypr-db-core2 = { path = "crates/db-core2", package = "db-core2" }
8888
hypr-db-live-query = { path = "crates/db-live-query", package = "db-live-query" }
8989
hypr-db-parser = { path = "crates/db-parser", package = "db-parser" }
9090
hypr-db-user = { path = "crates/db-user", package = "db-user" }
91-
hypr-db-watch = { path = "crates/db-watch", package = "db-watch" }
9291
hypr-denoise = { path = "crates/denoise", package = "denoise" }
9392
hypr-detect = { path = "crates/detect", package = "detect" }
9493
hypr-device-monitor = { path = "crates/device-monitor", package = "device-monitor" }

crates/db-core2/AGENTS.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,3 +48,9 @@
4848
- If the pool policy changes between single-connection and multi-connection, the hook path should remain correct without upper-layer changes.
4949
- In-memory databases should still be treated carefully; `max_connections(1)` is the safe default unless shared-memory behavior is explicitly intended.
5050
- If code needs to answer "what tables does this SQL depend on?" or "which subscribers should rerun?", it belongs above this crate.
51+
52+
## Test Ownership
53+
54+
- Put tests here when the behavior is about database opening, connection policy, migration failure handling, cloudsync wiring, or raw table-change hook behavior.
55+
- Prefer temp-database integration tests here over higher-level plugin tests when verifying pooled connection semantics.
56+
- Do not test subscription reruns, dependency extraction, transport delivery, or Tauri command behavior here.

crates/db-core2/src/lib.rs

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -455,6 +455,76 @@ mod tests {
455455
assert!(matches!(error, DbOpenError::Migration(message) if message == "nope"));
456456
}
457457

458+
#[tokio::test]
459+
async fn open_with_migrate_returns_recreate_failed_when_retry_also_fails() {
460+
let tmp = tempfile::tempdir().unwrap();
461+
let db_path = tmp.path().join("app.db");
462+
let attempts = AtomicUsize::new(0);
463+
464+
let error = Db3::open_with_migrate(
465+
DbOpenOptions {
466+
storage: DbStorage::Local(&db_path),
467+
cloudsync: false,
468+
journal_mode_wal: true,
469+
foreign_keys: true,
470+
max_connections: Some(1),
471+
migration_failure_policy: MigrationFailurePolicy::Recreate,
472+
},
473+
|pool| {
474+
let n = attempts.fetch_add(1, Ordering::SeqCst);
475+
Box::pin(async move {
476+
let table_name = if n == 0 {
477+
"first_attempt"
478+
} else {
479+
"second_attempt"
480+
};
481+
let sql = format!("CREATE TABLE {table_name} (id TEXT PRIMARY KEY NOT NULL)");
482+
sqlx::query(&sql).execute(pool).await.unwrap();
483+
Err::<(), &'static str>("still broken")
484+
})
485+
},
486+
)
487+
.await
488+
.unwrap_err();
489+
490+
assert_eq!(attempts.load(Ordering::SeqCst), 2);
491+
assert!(
492+
matches!(error, DbOpenError::RecreateFailed(message) if message == "migration failed: still broken")
493+
);
494+
}
495+
496+
#[tokio::test]
497+
async fn open_with_migrate_applies_requested_pragmas() {
498+
let tmp = tempfile::tempdir().unwrap();
499+
let db_path = tmp.path().join("app.db");
500+
501+
let db = Db3::open_with_migrate(
502+
DbOpenOptions {
503+
storage: DbStorage::Local(&db_path),
504+
cloudsync: false,
505+
journal_mode_wal: true,
506+
foreign_keys: true,
507+
max_connections: Some(1),
508+
migration_failure_policy: MigrationFailurePolicy::Fail,
509+
},
510+
|_pool| Box::pin(async { Ok::<(), sqlx::Error>(()) }),
511+
)
512+
.await
513+
.unwrap();
514+
515+
let foreign_keys: i64 = sqlx::query_scalar("PRAGMA foreign_keys")
516+
.fetch_one(db.pool().as_ref())
517+
.await
518+
.unwrap();
519+
let journal_mode: String = sqlx::query_scalar("PRAGMA journal_mode")
520+
.fetch_one(db.pool().as_ref())
521+
.await
522+
.unwrap();
523+
524+
assert_eq!(foreign_keys, 1);
525+
assert_eq!(journal_mode.to_lowercase(), "wal");
526+
}
527+
458528
#[tokio::test]
459529
async fn emits_table_changes_for_local_writes() {
460530
let db = Db3::connect_memory_plain().await.unwrap();
@@ -479,6 +549,44 @@ mod tests {
479549
assert_eq!(change.kind, TableChangeKind::Insert);
480550
}
481551

552+
#[tokio::test]
553+
async fn emits_update_and_delete_table_changes() {
554+
let db = Db3::connect_memory_plain().await.unwrap();
555+
sqlx::query("CREATE TABLE test_events (id TEXT PRIMARY KEY NOT NULL, value TEXT NOT NULL)")
556+
.execute(db.pool().as_ref())
557+
.await
558+
.unwrap();
559+
sqlx::query("INSERT INTO test_events (id, value) VALUES ('a', 'before')")
560+
.execute(db.pool().as_ref())
561+
.await
562+
.unwrap();
563+
564+
let mut changes = db.subscribe_table_changes();
565+
566+
sqlx::query("UPDATE test_events SET value = 'after' WHERE id = 'a'")
567+
.execute(db.pool().as_ref())
568+
.await
569+
.unwrap();
570+
sqlx::query("DELETE FROM test_events WHERE id = 'a'")
571+
.execute(db.pool().as_ref())
572+
.await
573+
.unwrap();
574+
575+
let update = tokio::time::timeout(std::time::Duration::from_secs(1), changes.recv())
576+
.await
577+
.unwrap()
578+
.unwrap();
579+
let delete = tokio::time::timeout(std::time::Duration::from_secs(1), changes.recv())
580+
.await
581+
.unwrap()
582+
.unwrap();
583+
584+
assert_eq!(update.table, "test_events");
585+
assert_eq!(update.kind, TableChangeKind::Update);
586+
assert_eq!(delete.table, "test_events");
587+
assert_eq!(delete.kind, TableChangeKind::Delete);
588+
}
589+
482590
#[tokio::test]
483591
async fn emits_table_changes_across_multiple_connections() {
484592
let dir = tempfile::tempdir().unwrap();

crates/db-live-query/AGENTS.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
- `db-live-query` is the reusable live-query service layer.
66
- It executes SQL, analyzes query dependencies, tracks subscriptions, reruns affected queries, and serializes rows for sinks.
7-
- It consumes raw table-change signals from `db-core2` and pure watch indexing from `db-watch`.
7+
- It consumes raw table-change signals from `db-core2` and maintains its own watch indexing (`watch.rs`).
88

99
## This Crate Owns
1010

@@ -36,7 +36,7 @@
3636

3737
## Dependency Direction
3838

39-
- This crate may depend on `db-core2` and `db-watch`.
39+
- This crate may depend on `db-core2`.
4040
- `plugins/db` may depend on this crate.
4141
- This crate must not depend on Tauri or app-specific UI/runtime layers.
4242

@@ -46,3 +46,9 @@
4646
- Raw SQLite hook installation and pooled connection setup belong below this crate in `db-core2`.
4747
- Tauri channels, app bootstrap, and JS-facing event types belong above this crate in `plugins/db`.
4848
- If this layer ever becomes app-specific, it should be split again rather than letting transport or product logic leak inward.
49+
50+
## Test Ownership
51+
52+
- Put tests here when the behavior is about dependency extraction, reactive vs non-reactive classification, rerun targeting, unsubscribe semantics, stale-sink cleanup, or JSON row serialization.
53+
- These tests may use a real temp database plus a fake sink, but they should not depend on Tauri transport types.
54+
- Higher layers should not duplicate this crate's rerun and invalidation tests unless they are specifically proving adapter integration.

crates/db-live-query/Cargo.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ edition = "2024"
55

66
[dependencies]
77
hypr-db-core2 = { workspace = true }
8-
hypr-db-watch = { workspace = true }
98
serde_json = { workspace = true }
109
sqlx = { workspace = true, features = ["runtime-tokio", "sqlite", "sqlite-unbundled"] }
1110
thiserror = { workspace = true }

crates/db-live-query/src/error.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
#[derive(Debug, thiserror::Error)]
2+
pub enum Error {
3+
#[error(transparent)]
4+
Sqlx(#[from] sqlx::Error),
5+
#[error("subscription not found: {0}")]
6+
SubscriptionNotFound(String),
7+
#[error("failed to send query event: {0}")]
8+
Sink(String),
9+
}
10+
11+
pub type Result<T> = std::result::Result<T, Error>;

crates/db-live-query/src/explain.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,18 @@ mod tests {
216216
assert_eq!(tables, HashSet::from(["daily_notes".to_string()]));
217217
}
218218

219+
#[tokio::test]
220+
async fn bracket_quoted_alias_query() {
221+
let db = test_db().await;
222+
let tables = extract_tables(
223+
db.pool(),
224+
"SELECT [dn].id FROM [daily_notes] AS [dn] WHERE [dn].date = '2026-04-11'",
225+
)
226+
.await
227+
.unwrap();
228+
assert_eq!(tables, HashSet::from(["daily_notes".to_string()]));
229+
}
230+
219231
#[tokio::test]
220232
async fn schema_qualified_query() {
221233
let db = test_db().await;

0 commit comments

Comments
 (0)