#![cfg(feature = "sal")]
use anyhow::{Context, Result};
use clap::Args;
use serde::Serialize;
use crate::cli::CliOutput;
use crate::migrate;
#[cfg(feature = "sal-postgres")]
const TRACE_TARGET: &str = "schema_init";
#[derive(Args, Debug, Clone)]
pub struct SchemaInitArgs {
#[arg(long, value_name = "URL")]
pub store_url: String,
#[arg(long, default_value_t = false)]
pub json: bool,
#[arg(long, default_value_t = 384)]
pub embedding_dim: u32,
}
#[derive(Debug, Clone, Serialize)]
pub struct SchemaInitReport {
pub url: String,
pub kind: String,
pub tables: Vec<String>,
pub views: Vec<String>,
pub functions: Vec<String>,
pub indices: Vec<String>,
pub extensions: Vec<String>,
pub schema_version: i64,
pub age_projection_created: bool,
#[serde(default)]
pub embedding_dim: Option<i32>,
#[serde(default)]
pub embedding_dim_migrated: bool,
}
pub async fn run(args: &SchemaInitArgs, out: &mut CliOutput<'_>) -> Result<()> {
let report = if is_sqlite_url(&args.store_url) {
let _store = migrate::open_store(&args.store_url)
.await
.with_context(|| {
format!(
"open store at {}",
crate::logging::redact_url_password(&args.store_url)
)
})?;
let mut r = enumerate_sqlite(&args.store_url)?;
r.embedding_dim = Some(i32::try_from(args.embedding_dim).unwrap_or(384));
r
} else if is_postgres_url(&args.store_url) {
#[cfg(feature = "sal-postgres")]
{
init_and_enumerate_postgres(&args.store_url, args.embedding_dim).await?
}
#[cfg(not(feature = "sal-postgres"))]
{
anyhow::bail!("postgres support not compiled in (build with --features sal-postgres)");
}
} else {
anyhow::bail!(
"unrecognised store URL: {} (expected sqlite:///path or postgres://...)",
crate::logging::redact_url_password(&args.store_url)
);
};
if args.json {
let json = serde_json::to_string_pretty(&report).context("serialize schema-init report")?;
writeln!(out.stdout, "{json}")?;
} else {
render_human(&report, out)?;
}
Ok(())
}
fn is_sqlite_url(url: &str) -> bool {
url.starts_with(crate::migrate::SQLITE_URL_SCHEME)
}
fn is_postgres_url(url: &str) -> bool {
crate::migrate::is_postgres_url(url)
}
fn sqlite_path_from_url(url: &str) -> &str {
let path = url.strip_prefix("sqlite://").unwrap_or(url);
path.strip_prefix('/')
.map_or(path, |p| if p.starts_with('/') { p } else { path })
}
fn enumerate_sqlite(url: &str) -> Result<SchemaInitReport> {
let path = sqlite_path_from_url(url);
let conn = rusqlite::Connection::open(path)
.with_context(|| format!("open sqlite for enumeration: {path}"))?;
let tables = list_sqlite_objects(&conn, "table")?;
let views = list_sqlite_objects(&conn, "view")?;
let indices = list_sqlite_indices(&conn)?;
let schema_version = read_schema_version_sqlite(&conn).unwrap_or(0);
Ok(SchemaInitReport {
url: url.to_string(),
kind: "sqlite".to_string(),
tables,
views,
functions: Vec::new(),
indices,
extensions: Vec::new(),
schema_version,
age_projection_created: false,
embedding_dim: None,
embedding_dim_migrated: false,
})
}
fn list_sqlite_objects(conn: &rusqlite::Connection, kind: &str) -> Result<Vec<String>> {
let mut stmt = conn
.prepare(
"SELECT name FROM sqlite_master \
WHERE type = ?1 AND name NOT LIKE 'sqlite_%' \
ORDER BY name",
)
.context("prepare sqlite_master scan")?;
let rows = stmt
.query_map([kind], |row| row.get::<_, String>(0))
.context("query sqlite_master")?;
let mut out = Vec::new();
for r in rows {
out.push(r.context("read sqlite_master row")?);
}
Ok(out)
}
fn list_sqlite_indices(conn: &rusqlite::Connection) -> Result<Vec<String>> {
let mut stmt = conn
.prepare(
"SELECT name FROM sqlite_master \
WHERE type = 'index' \
AND name NOT LIKE 'sqlite_%' \
ORDER BY name",
)
.context("prepare sqlite_master index scan")?;
let rows = stmt
.query_map([], |row| row.get::<_, String>(0))
.context("query sqlite_master indices")?;
let mut out = Vec::new();
for r in rows {
out.push(r.context("read sqlite_master index row")?);
}
Ok(out)
}
fn read_schema_version_sqlite(conn: &rusqlite::Connection) -> Result<i64> {
let v: i64 = conn
.query_row(
crate::storage::migrations::SELECT_SCHEMA_VERSION_SQL,
[],
|row| row.get(0),
)
.context(crate::errors::msg::READ_SCHEMA_VERSION)?;
Ok(v)
}
#[cfg(feature = "sal-postgres")]
async fn init_and_enumerate_postgres(url: &str, dim: u32) -> Result<SchemaInitReport> {
use crate::store::postgres::PostgresStore;
let store = PostgresStore::connect_with_dim(url, dim)
.await
.with_context(|| {
format!(
"open store at {} with embedding dim {dim}",
crate::logging::redact_url_password(url)
)
})?;
let migrated = store
.migrate_embedding_dim(dim)
.await
.with_context(|| format!("migrate embedding dim to {dim}"))?;
let mut report = enumerate_postgres(url).await?;
report.embedding_dim = store.current_embedding_dim().await.ok().flatten();
report.embedding_dim_migrated = migrated;
Ok(report)
}
#[cfg(feature = "sal-postgres")]
async fn enumerate_postgres(url: &str) -> Result<SchemaInitReport> {
use sqlx::postgres::PgPoolOptions;
let pool = PgPoolOptions::new()
.max_connections(2)
.acquire_timeout(std::time::Duration::from_secs(15))
.connect(url)
.await
.with_context(|| {
format!(
"connect postgres for enumeration: {}",
crate::logging::redact_url_password(url)
)
})?;
let table_rows: Vec<(String,)> = sqlx::query_as(
"SELECT tablename FROM pg_tables \
WHERE schemaname = 'public' \
ORDER BY tablename",
)
.fetch_all(&pool)
.await
.context("list pg_tables")?;
let tables: Vec<String> = table_rows.into_iter().map(|(n,)| n).collect();
let view_rows: Vec<(String,)> = sqlx::query_as(
"SELECT viewname FROM pg_views \
WHERE schemaname = 'public' \
ORDER BY viewname",
)
.fetch_all(&pool)
.await
.context("list pg_views")?;
let views: Vec<String> = view_rows.into_iter().map(|(n,)| n).collect();
let index_rows: Vec<(String,)> = sqlx::query_as(
"SELECT indexname FROM pg_indexes \
WHERE schemaname = 'public' \
ORDER BY indexname",
)
.fetch_all(&pool)
.await
.context("list pg_indexes")?;
let indices: Vec<String> = index_rows.into_iter().map(|(n,)| n).collect();
let function_rows: Vec<(String,)> = sqlx::query_as(
"SELECT DISTINCT proname FROM pg_proc p \
JOIN pg_namespace n ON n.oid = p.pronamespace \
WHERE n.nspname = 'public' AND p.prokind IN ('f', 'p') \
ORDER BY proname",
)
.fetch_all(&pool)
.await
.context("list pg_proc")?;
let functions: Vec<String> = function_rows.into_iter().map(|(n,)| n).collect();
let ext_rows: Vec<(String,)> =
sqlx::query_as("SELECT extname FROM pg_extension ORDER BY extname")
.fetch_all(&pool)
.await
.context("list pg_extension")?;
let extensions: Vec<String> = ext_rows.into_iter().map(|(n,)| n).collect();
let schema_version_row: Option<(i32,)> =
sqlx::query_as("SELECT COALESCE(MAX(version), 0)::int FROM schema_version")
.fetch_optional(&pool)
.await
.context(crate::errors::msg::READ_SCHEMA_VERSION)?;
let schema_version = i64::from(schema_version_row.map_or(0, |(v,)| v));
let age_projection_created = if extensions.iter().any(|e| e == "age") {
bootstrap_memory_graph(&pool).await
} else {
false
};
drop(pool);
Ok(SchemaInitReport {
url: url.to_string(),
kind: "postgres".to_string(),
tables,
views,
functions,
indices,
extensions,
schema_version,
age_projection_created,
embedding_dim: None,
embedding_dim_migrated: false,
})
}
#[cfg(feature = "sal-postgres")]
async fn bootstrap_memory_graph(pool: &sqlx::PgPool) -> bool {
let mut conn = match pool.acquire().await {
Ok(c) => c,
Err(e) => {
tracing::warn!(
target: TRACE_TARGET,
error = %e,
"acquire connection for AGE bootstrap"
);
return false;
}
};
if let Err(e) = sqlx::query("SET search_path = ag_catalog, \"$user\", public")
.execute(&mut *conn)
.await
{
tracing::warn!(
target: TRACE_TARGET,
error = %e,
"set ag_catalog search_path"
);
return false;
}
match sqlx::query(crate::store::postgres::SQL_CREATE_AGE_GRAPH)
.execute(&mut *conn)
.await
{
Ok(_) => true,
Err(e) => {
let msg = e.to_string();
if msg.contains(crate::store::postgres::PG_ERR_ALREADY_EXISTS) {
true
} else {
tracing::warn!(
target: TRACE_TARGET,
error = %e,
"create_graph('memory_graph') failed (continuing without AGE projection)"
);
false
}
}
}
}
fn render_human(report: &SchemaInitReport, out: &mut CliOutput<'_>) -> Result<()> {
writeln!(out.stdout, "schema initialized at {}", report.url)?;
writeln!(out.stdout, " tables: {}", report.tables.len())?;
writeln!(out.stdout, " indices: {}", report.indices.len())?;
writeln!(out.stdout, " views: {}", report.views.len())?;
writeln!(out.stdout, " functions: {}", report.functions.len())?;
writeln!(
out.stdout,
" extensions: [{}]",
report.extensions.join(", ")
)?;
writeln!(out.stdout, " schema_version: {}", report.schema_version)?;
match report.embedding_dim {
Some(d) => {
writeln!(out.stdout, " embedding_dim: {d}")?;
}
None => {
writeln!(out.stdout, " embedding_dim: (unknown)")?;
}
}
if report.embedding_dim_migrated {
writeln!(
out.stdout,
" embedding_dim_migrated: yes (existing embeddings NULLed — re-embed required)"
)?;
}
if report.kind == "postgres" {
writeln!(
out.stdout,
" age_projection: {}",
if report.age_projection_created {
"created"
} else {
"skipped (AGE not installed or bootstrap failed)"
}
)?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn classifies_sqlite_urls() {
assert!(is_sqlite_url("sqlite:///tmp/foo.db"));
assert!(is_sqlite_url("sqlite://./rel.db"));
assert!(!is_sqlite_url("postgres://x"));
assert!(!is_sqlite_url("nosql://x"));
}
#[test]
fn classifies_postgres_urls() {
assert!(is_postgres_url("postgres://u:p@h/d"));
assert!(is_postgres_url("postgresql://u:p@h/d"));
assert!(!is_postgres_url("sqlite:///x"));
}
#[test]
fn sqlite_path_strips_prefix_and_third_slash() {
assert_eq!(sqlite_path_from_url("sqlite:///tmp/foo.db"), "/tmp/foo.db");
assert_eq!(sqlite_path_from_url("sqlite://./rel.db"), "./rel.db");
}
#[tokio::test]
async fn run_sqlite_emits_json_with_expected_fields() {
let tmp = tempfile::NamedTempFile::new().unwrap();
let path = tmp.path().to_string_lossy().to_string();
let url = format!("sqlite://{path}");
let mut stdout = Vec::<u8>::new();
let mut stderr = Vec::<u8>::new();
let mut out = CliOutput::from_std(&mut stdout, &mut stderr);
let args = SchemaInitArgs {
store_url: url.clone(),
json: true,
embedding_dim: 384,
};
run(&args, &mut out).await.expect("schema-init sqlite");
let raw = String::from_utf8(stdout).unwrap();
let v: serde_json::Value = serde_json::from_str(&raw).expect("parseable JSON");
assert_eq!(v["kind"], "sqlite");
assert_eq!(v["url"], serde_json::Value::String(url));
assert!(
v["schema_version"].as_i64().unwrap() > 0,
"schema_version should be > 0 after init: {v}"
);
let tables: Vec<&str> = v["tables"]
.as_array()
.unwrap()
.iter()
.map(|t| t.as_str().unwrap())
.collect();
assert!(
tables.contains(&"memories"),
"memories table missing: {tables:?}"
);
assert!(
tables.contains(&"memory_links"),
"memory_links table missing: {tables:?}"
);
assert!(v["extensions"].as_array().unwrap().is_empty());
assert!(v["functions"].as_array().unwrap().is_empty());
assert_eq!(v["age_projection_created"], false);
assert_eq!(
v["embedding_dim"].as_i64().unwrap(),
384,
"embedding_dim should be 384 from the flag default: {v}"
);
assert_eq!(
v["embedding_dim_migrated"], false,
"SQLite never reports a vector(N) migration: {v}"
);
}
#[tokio::test]
async fn run_sqlite_carries_explicit_embedding_dim() {
let tmp = tempfile::NamedTempFile::new().unwrap();
let path = tmp.path().to_string_lossy().to_string();
let url = format!("sqlite://{path}");
let mut stdout = Vec::<u8>::new();
let mut stderr = Vec::<u8>::new();
let mut out = CliOutput::from_std(&mut stdout, &mut stderr);
let args = SchemaInitArgs {
store_url: url.clone(),
json: true,
embedding_dim: 768,
};
run(&args, &mut out).await.expect("schema-init sqlite 768");
let raw = String::from_utf8(stdout).unwrap();
let v: serde_json::Value = serde_json::from_str(&raw).expect("parseable JSON");
assert_eq!(
v["embedding_dim"].as_i64().unwrap(),
768,
"operator-provided dim must round-trip into report: {v}"
);
}
#[tokio::test]
async fn run_sqlite_human_output_renders_report() {
let tmp = tempfile::NamedTempFile::new().unwrap();
let path = tmp.path().to_string_lossy().to_string();
let url = format!("sqlite://{path}");
let mut stdout = Vec::<u8>::new();
let mut stderr = Vec::<u8>::new();
let mut out = CliOutput::from_std(&mut stdout, &mut stderr);
let args = SchemaInitArgs {
store_url: url,
json: false,
embedding_dim: 384,
};
run(&args, &mut out)
.await
.expect("schema-init sqlite human");
let rendered = String::from_utf8(stdout).unwrap();
assert!(
rendered.contains("schema initialized at"),
"got: {rendered}"
);
assert!(rendered.contains("tables:"), "got: {rendered}");
assert!(rendered.contains("schema_version:"), "got: {rendered}");
}
#[tokio::test]
async fn run_unrecognised_url_bails() {
let mut stdout = Vec::<u8>::new();
let mut stderr = Vec::<u8>::new();
let mut out = CliOutput::from_std(&mut stdout, &mut stderr);
let args = SchemaInitArgs {
store_url: "mysql://user:secret@host/db".to_string(),
json: false,
embedding_dim: 384,
};
let err = run(&args, &mut out).await.expect_err("must reject");
let msg = err.to_string();
assert!(msg.contains("unrecognised store URL"), "got: {msg}");
assert!(!msg.contains("secret"), "credential leaked: {msg}");
}
#[cfg(feature = "sal-postgres")]
#[tokio::test]
#[ignore = "requires running postgres; see comment above for the recipe"]
async fn schema_init_postgres_embedding_dim_conversion() {
let url = std::env::var("AI_MEMORY_TEST_POSTGRES_URL")
.expect("AI_MEMORY_TEST_POSTGRES_URL must be set");
let mut stdout = Vec::<u8>::new();
let mut stderr = Vec::<u8>::new();
let mut out = CliOutput::from_std(&mut stdout, &mut stderr);
let args = SchemaInitArgs {
store_url: url.clone(),
json: true,
embedding_dim: 384,
};
run(&args, &mut out).await.expect("schema-init 384");
let raw = String::from_utf8(stdout).unwrap();
let v: serde_json::Value = serde_json::from_str(&raw).expect("parseable JSON");
assert_eq!(v["embedding_dim"].as_i64(), Some(384), "initial dim: {v}");
let mut stdout = Vec::<u8>::new();
let mut stderr = Vec::<u8>::new();
let mut out = CliOutput::from_std(&mut stdout, &mut stderr);
let args = SchemaInitArgs {
store_url: url.clone(),
json: true,
embedding_dim: 768,
};
run(&args, &mut out).await.expect("schema-init 768");
let raw = String::from_utf8(stdout).unwrap();
let v: serde_json::Value = serde_json::from_str(&raw).expect("parseable JSON");
assert_eq!(
v["embedding_dim"].as_i64(),
Some(768),
"post-conversion: {v}"
);
assert_eq!(
v["embedding_dim_migrated"], true,
"conversion should be flagged: {v}"
);
let mut stdout = Vec::<u8>::new();
let mut stderr = Vec::<u8>::new();
let mut out = CliOutput::from_std(&mut stdout, &mut stderr);
let args = SchemaInitArgs {
store_url: url,
json: true,
embedding_dim: 768,
};
run(&args, &mut out)
.await
.expect("schema-init 768 idempotent");
let raw = String::from_utf8(stdout).unwrap();
let v: serde_json::Value = serde_json::from_str(&raw).expect("parseable JSON");
assert_eq!(v["embedding_dim"].as_i64(), Some(768));
assert_eq!(
v["embedding_dim_migrated"], false,
"second run at same dim must be no-op: {v}"
);
}
#[tokio::test]
async fn run_sqlite_human_output_is_six_lines_minimum() {
let tmp = tempfile::NamedTempFile::new().unwrap();
let path = tmp.path().to_string_lossy().to_string();
let url = format!("sqlite://{path}");
let mut stdout = Vec::<u8>::new();
let mut stderr = Vec::<u8>::new();
let mut out = CliOutput::from_std(&mut stdout, &mut stderr);
let args = SchemaInitArgs {
store_url: url.clone(),
json: false,
embedding_dim: 384,
};
run(&args, &mut out)
.await
.expect("schema-init sqlite human");
let raw = String::from_utf8(stdout).unwrap();
assert!(
raw.contains("schema initialized at"),
"missing header: {raw}"
);
assert!(raw.contains("tables:"), "missing tables row: {raw}");
assert!(raw.contains("indices:"), "missing indices row: {raw}");
assert!(raw.contains("views:"), "missing views row: {raw}");
assert!(raw.contains("functions:"), "missing functions row: {raw}");
assert!(raw.contains("extensions:"), "missing extensions row: {raw}");
assert!(
raw.contains("schema_version:"),
"missing version row: {raw}"
);
}
#[tokio::test]
async fn run_rejects_unrecognised_url_scheme() {
let mut stdout = Vec::<u8>::new();
let mut stderr = Vec::<u8>::new();
let mut out = CliOutput::from_std(&mut stdout, &mut stderr);
let args = SchemaInitArgs {
store_url: "nosql://nope".to_string(),
json: false,
embedding_dim: 384,
};
let err = run(&args, &mut out).await.expect_err("should reject");
let msg = format!("{err:#}");
assert!(
msg.contains("unrecognised store URL"),
"expected unrecognised-scheme error, got: {msg}"
);
}
#[cfg(feature = "sal-postgres")]
#[tokio::test]
async fn run_postgres_url_dispatches_to_init_and_enumerate() {
let mut stdout = Vec::<u8>::new();
let mut stderr = Vec::<u8>::new();
let mut out = CliOutput::from_std(&mut stdout, &mut stderr);
let args = SchemaInitArgs {
store_url: "postgres://nobody:nope@127.0.0.1:1/no_db".to_string(),
json: true,
embedding_dim: 384,
};
let err = run(&args, &mut out)
.await
.expect_err("should fail to connect");
let msg = format!("{err:#}");
assert!(
msg.contains("open store at") || msg.contains("connect") || msg.contains("postgres"),
"got: {msg}"
);
}
#[cfg(feature = "sal-postgres")]
#[tokio::test]
async fn run_postgresql_alias_url_dispatches_to_init_and_enumerate() {
let mut stdout = Vec::<u8>::new();
let mut stderr = Vec::<u8>::new();
let mut out = CliOutput::from_std(&mut stdout, &mut stderr);
let args = SchemaInitArgs {
store_url: "postgresql://nobody:nope@127.0.0.1:1/x".to_string(),
json: false,
embedding_dim: 384,
};
assert!(run(&args, &mut out).await.is_err());
}
#[tokio::test]
async fn render_human_includes_extension_list_and_embedding_dim_lines() {
let report = SchemaInitReport {
url: "synthetic://test".to_string(),
kind: "postgres".to_string(),
tables: vec!["memories".to_string()],
views: vec![],
functions: vec!["fts_update".to_string()],
indices: vec!["idx_memories_ns".to_string()],
extensions: vec!["pgvector".to_string(), "age".to_string()],
schema_version: 29,
age_projection_created: true,
embedding_dim: Some(768),
embedding_dim_migrated: true,
};
let mut stdout = Vec::<u8>::new();
let mut stderr = Vec::<u8>::new();
let mut out = CliOutput::from_std(&mut stdout, &mut stderr);
render_human(&report, &mut out).unwrap();
let s = String::from_utf8(stdout).unwrap();
assert!(s.contains("schema initialized at synthetic://test"));
assert!(s.contains("pgvector, age"));
assert!(s.contains("embedding_dim: 768"));
assert!(s.contains("embedding_dim_migrated: yes"));
assert!(s.contains("age_projection: created"));
}
#[tokio::test]
async fn render_human_emits_unknown_when_embedding_dim_absent() {
let report = SchemaInitReport {
url: "synthetic://test".to_string(),
kind: "postgres".to_string(),
tables: vec![],
views: vec![],
functions: vec![],
indices: vec![],
extensions: vec![],
schema_version: 1,
age_projection_created: false,
embedding_dim: None,
embedding_dim_migrated: false,
};
let mut stdout = Vec::<u8>::new();
let mut stderr = Vec::<u8>::new();
let mut out = CliOutput::from_std(&mut stdout, &mut stderr);
render_human(&report, &mut out).unwrap();
let s = String::from_utf8(stdout).unwrap();
assert!(s.contains("embedding_dim: (unknown)"));
assert!(s.contains("age_projection: skipped"));
}
#[cfg(feature = "sal-postgres")]
#[tokio::test]
async fn enumerate_postgres_unreachable_returns_connect_error() {
let err = enumerate_postgres("postgres://x:y@127.0.0.1:1/nope")
.await
.expect_err("must fail");
let msg = format!("{err:#}");
assert!(
msg.contains("connect postgres for enumeration")
|| msg.contains("connect")
|| msg.contains("refused")
|| msg.contains("postgres"),
"got: {msg}"
);
}
#[cfg(feature = "sal-postgres")]
#[tokio::test]
async fn init_and_enumerate_postgres_unreachable_returns_open_error() {
let err = init_and_enumerate_postgres("postgres://x:y@127.0.0.1:1/no_db", 384)
.await
.expect_err("must fail");
let msg = format!("{err:#}");
assert!(
msg.contains("open store at") || msg.contains("connect") || msg.contains("postgres"),
"got: {msg}"
);
}
#[tokio::test]
async fn enumerate_sqlite_returns_populated_report() {
let tmp = tempfile::NamedTempFile::new().unwrap();
let path = tmp.path().to_string_lossy().to_string();
let url = format!("sqlite://{path}");
let _store = crate::migrate::open_store(&url).await.expect("open");
let r = enumerate_sqlite(&url).expect("enumerate");
assert_eq!(r.kind, "sqlite");
assert!(
r.tables.iter().any(|t| t == "memories"),
"tables: {:?}",
r.tables
);
assert!(r.schema_version > 0, "version: {}", r.schema_version);
}
#[tokio::test]
async fn enumerate_sqlite_returns_error_on_missing_file() {
let err =
enumerate_sqlite("sqlite:///nonexistent-parent-xyz/missing.db").expect_err("must fail");
let msg = format!("{err:#}");
assert!(msg.contains("open sqlite for enumeration"), "got: {msg}");
}
#[tokio::test]
async fn read_schema_version_falls_back_to_zero_on_missing_table() {
let tmp = tempfile::NamedTempFile::new().unwrap();
let conn = rusqlite::Connection::open(tmp.path()).unwrap();
drop(conn);
let url = format!("sqlite://{}", tmp.path().display());
let r = enumerate_sqlite(&url).expect("enumerate");
assert_eq!(r.schema_version, 0);
}
#[tokio::test]
async fn render_human_no_age_line_for_sqlite_kind() {
let report = SchemaInitReport {
url: "sqlite:///tmp/x.db".to_string(),
kind: "sqlite".to_string(),
tables: vec![],
views: vec![],
functions: vec![],
indices: vec![],
extensions: vec![],
schema_version: 7,
age_projection_created: false,
embedding_dim: Some(384),
embedding_dim_migrated: false,
};
let mut stdout = Vec::<u8>::new();
let mut stderr = Vec::<u8>::new();
let mut out = CliOutput::from_std(&mut stdout, &mut stderr);
render_human(&report, &mut out).unwrap();
let s = String::from_utf8(stdout).unwrap();
assert!(!s.contains("age_projection"));
}
}