Add multi-database data layer

Introduce aether-data-schema and driver-specific schema generation for Postgres, MySQL, and SQLite.

Split data backends, lifecycle, repositories, and gateway runtime integration across database drivers.

Verified with cargo fmt --all --check, cargo clippy --workspace --all-targets -- -D warnings, and cargo test --workspace.
This commit is contained in:
fawney19
2026-05-05 18:27:36 +08:00
parent 099653f732
commit fce7e959e5
372 changed files with 86217 additions and 21160 deletions

View File

@@ -0,0 +1,244 @@
use std::{
fs,
path::{Path, PathBuf},
};
const OLD_ENTRYPOINTS: &[&str] = &[
"backends", "backfill", "export", "migrate", "mysql", "postgres", "redis", "sqlite",
];
const SELF_TEST_PATH: &str = "crates/aether-data/tests/public_entrypoints.rs";
#[test]
fn deprecated_aether_data_entrypoints_are_not_exposed_or_used() {
let workspace_root = Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.and_then(Path::parent)
.expect("aether-data should live under crates/");
let mut violations = Vec::new();
for relative_dir in ["apps", "crates"] {
scan_rust_files(
&workspace_root.join(relative_dir),
workspace_root,
&mut violations,
);
}
assert!(
violations.is_empty(),
"deprecated aether-data entrypoints should not be exposed or used inside the workspace:\n{}",
violations.join("\n")
);
}
fn scan_rust_files(dir: &Path, workspace_root: &Path, violations: &mut Vec<String>) {
let entries = fs::read_dir(dir).unwrap_or_else(|err| panic!("failed to read {dir:?}: {err}"));
for entry in entries {
let path = entry
.unwrap_or_else(|err| panic!("failed to read entry under {dir:?}: {err}"))
.path();
if path.is_dir() {
scan_rust_files(&path, workspace_root, violations);
continue;
}
if path.extension().and_then(|ext| ext.to_str()) != Some("rs") {
continue;
}
if should_skip_file(&path, workspace_root) {
continue;
}
let source = fs::read_to_string(&path)
.unwrap_or_else(|err| panic!("failed to read {path:?}: {err}"));
collect_file_violations(&path, workspace_root, &source, violations);
}
}
fn should_skip_file(path: &Path, workspace_root: &Path) -> bool {
let relative = relative_path(path, workspace_root);
relative == Path::new(SELF_TEST_PATH)
}
fn collect_file_violations(
path: &Path,
workspace_root: &Path,
source: &str,
violations: &mut Vec<String>,
) {
let relative = relative_path(path, workspace_root);
collect_alias_violations(&relative, source, "aether_data", violations);
if relative.starts_with("crates/aether-data/src") {
collect_alias_violations(&relative, source, "crate", violations);
if relative == Path::new("crates/aether-data/src/lib.rs") {
collect_public_module_violations(&relative, source, violations);
}
}
}
fn collect_alias_violations(
relative: &Path,
source: &str,
prefix: &str,
violations: &mut Vec<String>,
) {
for entrypoint in OLD_ENTRYPOINTS {
let direct = format!("{prefix}::{entrypoint}");
for (byte_index, _) in source.match_indices(&direct) {
push_violation(relative, source, byte_index, &direct, violations);
}
}
collect_grouped_alias_violations(relative, source, prefix, violations);
}
fn collect_grouped_alias_violations(
relative: &Path,
source: &str,
prefix: &str,
violations: &mut Vec<String>,
) {
let marker = format!("{prefix}::{{");
let mut offset = 0;
while let Some(local_start) = source[offset..].find(&marker) {
let marker_start = offset + local_start;
let body_start = marker_start + marker.len();
let Some((body_end, body)) = grouped_import_body(source, body_start) else {
break;
};
for segment in top_level_import_segments(body) {
for entrypoint in OLD_ENTRYPOINTS {
if import_matches(segment, entrypoint) {
let imported = format!("{prefix}::{entrypoint}");
push_violation(relative, source, marker_start, &imported, violations);
}
}
}
offset = body_end + 1;
}
}
fn collect_public_module_violations(relative: &Path, source: &str, violations: &mut Vec<String>) {
for entrypoint in OLD_ENTRYPOINTS {
for marker in [format!("pub mod {entrypoint}"), format!("mod {entrypoint}")] {
for (byte_index, _) in source.match_indices(&marker) {
push_violation(relative, source, byte_index, &marker, violations);
}
}
}
}
fn grouped_import_body(source: &str, body_start: usize) -> Option<(usize, &str)> {
let mut depth = 1usize;
for (relative_index, ch) in source[body_start..].char_indices() {
match ch {
'{' => depth += 1,
'}' => {
depth -= 1;
if depth == 0 {
let body_end = body_start + relative_index;
return Some((body_end, &source[body_start..body_end]));
}
}
_ => {}
}
}
None
}
fn top_level_import_segments(body: &str) -> Vec<&str> {
let mut segments = Vec::new();
let mut depth = 0usize;
let mut segment_start = 0usize;
for (index, ch) in body.char_indices() {
match ch {
'{' => depth += 1,
'}' => depth = depth.saturating_sub(1),
',' if depth == 0 => {
segments.push(&body[segment_start..index]);
segment_start = index + ch.len_utf8();
}
_ => {}
}
}
segments.push(&body[segment_start..]);
segments
}
fn import_matches(part: &str, entrypoint: &str) -> bool {
let trimmed = part.trim();
trimmed == entrypoint
|| trimmed.strip_prefix(entrypoint).is_some_and(|suffix| {
suffix.starts_with("::") || suffix.trim_start().starts_with("as ")
})
}
fn push_violation(
relative: &Path,
source: &str,
byte_index: usize,
imported: &str,
violations: &mut Vec<String>,
) {
violations.push(format!(
"{}:{}: deprecated {imported}",
relative.display(),
line_number(source, byte_index)
));
}
fn line_number(source: &str, byte_index: usize) -> usize {
source[..byte_index]
.bytes()
.filter(|byte| *byte == b'\n')
.count()
+ 1
}
fn relative_path(path: &Path, workspace_root: &Path) -> PathBuf {
path.strip_prefix(workspace_root)
.unwrap_or(path)
.to_path_buf()
}
#[test]
fn grouped_import_scanner_allows_nested_new_paths() {
let source = r#"
use aether_data::{
driver::{postgres::PostgresPool, redis::RedisKvRunner},
lifecycle::{backfill::PendingBackfillInfo, migrate::PendingMigrationInfo},
};
"#;
let mut violations = Vec::new();
collect_alias_violations(
Path::new("crates/example/src/lib.rs"),
source,
"aether_data",
&mut violations,
);
assert!(violations.is_empty(), "{violations:#?}");
}
#[test]
fn grouped_import_scanner_rejects_top_level_old_paths() {
let source = r#"
use aether_data::{
postgres::PostgresPool,
redis::{RedisKvRunner, RedisLockRunner},
};
"#;
let mut violations = Vec::new();
collect_alias_violations(
Path::new("crates/example/src/lib.rs"),
source,
"aether_data",
&mut violations,
);
assert_eq!(violations.len(), 2, "{violations:#?}");
}