use std::env;
use std::fmt::Write as FmtWrite;
use std::fs;
use std::path::PathBuf;
use sqlx::Row;
use marius_fragment_forge::{
FieldSpec, FieldKind, VarlenField,
generate_render, generate_capacity_consts, generated_file_header,
};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("cargo:rerun-if-env-changed=DATABASE_URL");
println!("cargo:rerun-if-changed=build.rs");
let database_url = env::var("DATABASE_URL")
.expect("DB-Forge : DATABASE_URL non définie.");
let pool = sqlx::PgPool::connect(&database_url).await?;
type WatchedTable = (&'static str, &'static str, Option<(&'static str, &'static str, &'static str)>);
let watched: &[WatchedTable] = &[
("content", "core", Some(("content", "identity", "document_id"))),
("commerce", "product_core", None),
];
let out_dir = PathBuf::from(env::var("OUT_DIR")?);
let out_path = out_dir.join("generated_schema.rs");
let mut output = String::from(generated_file_header());
for &(schema, table, varlena_join) in watched {
let columns = fetch_columns(&pool, schema, table).await?;
let pk = fetch_pk_column(&pool, schema, table).await?;
let max_id = match &pk {
PrimaryKey::Single(col) => Some(fetch_max_id(&pool, schema, table, col).await?),
PrimaryKey::Composite => None,
};
let varlena: Vec<VarlenField> = if let Some((vs, vt, _fk)) = varlena_join {
fetch_varlena_cols(&pool, vs, vt).await?
} else {
vec![]
};
write_section_header(&mut output, schema, table, &pk);
write_row_struct(&mut output, schema, table, &columns, &varlena);
write_store_struct(&mut output, schema, table, &columns);
write_varlen_owned_struct(&mut output, schema, table, &varlena);
write_from_impl(&mut output, schema, table, &columns);
if let (PrimaryKey::Single(col), Some(max)) = (&pk, max_id) {
write_collector(&mut output, schema, table, col, max);
}
write_projection_stub(&mut output, schema, table, &columns, &pk, &varlena, varlena_join);
}
fs::write(&out_path, &output)?;
eprintln!("DB-Forge : généré → {}", out_path.display());
Ok(())
}
#[derive(Debug)]
struct Column {
attnum: i16,
name: String,
sql_type: String,
is_notnull: bool,
}
#[derive(Debug)]
enum PrimaryKey {
Single(String),
Composite,
}
async fn fetch_columns(
pool: &sqlx::PgPool,
schema: &str,
table: &str,
) -> Result<Vec<Column>, sqlx::Error> {
let rows = sqlx::query(
"SELECT
a.attnum::smallint,
a.attname::text,
format_type(a.atttypid, a.atttypmod),
a.attnotnull
FROM pg_attribute a
JOIN pg_class c ON a.attrelid = c.oid
JOIN pg_namespace n ON c.relnamespace = n.oid
WHERE n.nspname = $1
AND c.relname = $2
AND a.attnum > 0 -- exclut les colonnes système (attnum <= 0)
AND NOT a.attisdropped -- exclut les colonnes supprimées (ALTER TABLE DROP)
ORDER BY a.attnum -- invariant de Symétrie Mécanique",
)
.bind(schema)
.bind(table)
.fetch_all(pool)
.await?;
Ok(rows.into_iter().map(|r| Column {
attnum: r.get::<i16, _>(0),
name: r.get::<String, _>(1),
sql_type: r.get::<String, _>(2),
is_notnull: r.get::<bool, _>(3),
}).collect())
}
async fn fetch_pk_column(
pool: &sqlx::PgPool,
schema: &str,
table: &str,
) -> Result<PrimaryKey, sqlx::Error> {
let rows = sqlx::query(
"SELECT kcu.column_name::text
FROM information_schema.table_constraints tc
JOIN information_schema.key_column_usage kcu
ON kcu.constraint_name = tc.constraint_name
AND kcu.table_schema = tc.table_schema
AND kcu.table_name = tc.table_name
WHERE tc.table_schema = $1
AND tc.table_name = $2
AND tc.constraint_type = 'PRIMARY KEY'
ORDER BY kcu.ordinal_position",
)
.bind(schema)
.bind(table)
.fetch_all(pool)
.await?;
match rows.len() {
0 => {
eprintln!("DB-Forge [{schema}.{table}] : aucune PK trouvée — traité comme Composite.");
Ok(PrimaryKey::Composite)
}
1 => Ok(PrimaryKey::Single(rows[0].get::<String, _>(0))),
n => {
eprintln!("DB-Forge [{schema}.{table}] : PK composite ({n} colonnes) — Collector ignoré.");
Ok(PrimaryKey::Composite)
}
}
}
async fn fetch_max_id(
pool: &sqlx::PgPool,
schema: &str,
table: &str,
pk_col: &str,
) -> Result<usize, Box<dyn std::error::Error>> {
let query = format!(
"SELECT COALESCE(MAX({pk_col}), 0)::BIGINT FROM {schema}.{table}"
);
let max_id: i64 = sqlx::query_scalar::<_, i64>(&query)
.fetch_one(pool)
.await
.unwrap_or(0);
let with_margin = (max_id as f64 * 1.20).ceil() as usize;
let words_needed = with_margin.max(64).div_ceil(64);
let words_aligned = words_needed.next_power_of_two();
let max_entity_id = words_aligned * 64;
eprintln!(
"DB-Forge [{schema}.{table}] : MAX({pk_col})={max_id} → \
MAX_ENTITY_ID={max_entity_id} ({} KB)",
(words_aligned * 8) / 1024,
);
Ok(max_entity_id)
}
async fn fetch_varlena_cols(
pool: &sqlx::PgPool,
schema: &str,
table: &str,
) -> Result<Vec<VarlenField>, Box<dyn std::error::Error>> {
let rows = sqlx::query(
"SELECT
a.attname::text,
a.atttypmod::integer,
COALESCE(d.description, '')::text -- commentaire COMMENT ON COLUMN
FROM pg_attribute a
JOIN pg_class c ON c.oid = a.attrelid
JOIN pg_namespace n ON n.oid = c.relnamespace
-- pg_description : commentaires posés par COMMENT ON COLUMN.
-- objsubid = attnum pour cibler la colonne (pas la table entière).
LEFT JOIN pg_description d
ON d.objoid = a.attrelid
AND d.objsubid = a.attnum
WHERE n.nspname = $1
AND c.relname = $2
AND a.attnum > 0
AND NOT a.attisdropped
AND a.atttypid IN (
SELECT oid FROM pg_type
WHERE typname IN ('varchar', 'bpchar', 'text')
)
ORDER BY a.attnum",
)
.bind(schema)
.bind(table)
.fetch_all(pool)
.await?;
let mut fields = Vec::new();
for row in rows {
let name: String = row.get(0);
let typmod: i32 = row.get(1);
let description: String = row.get(2);
let is_pre_escaped = description.trim() == "marius:pre_escaped";
let max_len: usize = if typmod > 4 {
(typmod - 4) as usize
} else {
let check_row = sqlx::query(
"SELECT con.consrc::text
FROM pg_constraint con
JOIN pg_class cls ON cls.oid = con.conrelid
JOIN pg_namespace ns ON ns.oid = cls.relnamespace
WHERE ns.nspname = $1
AND cls.relname = $2
AND con.contype = 'c' -- CHECK
AND (con.consrc LIKE '%length(' || $3 || ')%'
OR con.consrc LIKE '%char_length(' || $3 || ')%')",
)
.bind(schema)
.bind(table)
.bind(&name)
.fetch_optional(pool)
.await?;
if let Some(check_r) = check_row {
let consrc: String = check_r.get(0);
parse_check_length_limit(&consrc).unwrap_or_else(|| {
println!(
"cargo:warning=DB-Forge [{schema}.{table}.{name}]: \
CHECK trouvé mais longueur non parsable : `{consrc}`. \
Fallback max_len=10000."
);
10_000
})
} else {
println!(
"cargo:warning=DB-Forge [{schema}.{table}.{name}]: \
TEXT sans contrainte de longueur — exclu du listing render. \
Réserver au rendu page complète (allocation dynamique acceptable)."
);
continue; }
};
let escape_factor = if is_pre_escaped { 1 } else { VarlenField::HTML_ESCAPE_FACTOR };
let max_escaped = max_len * escape_factor;
if max_escaped > 65_536 {
panic!(
"DB-Forge [{schema}.{table}.{name}]: \
max_escaped_len ({max_escaped}B) > 64 KB. \
Réduire la contrainte VARCHAR/CHECK ou exclure du listing render."
);
}
let avg_row = sqlx::query(
"SELECT avg_width::integer FROM pg_stats
WHERE schemaname = $1 AND tablename = $2 AND attname = $3",
)
.bind(schema)
.bind(table)
.bind(&name)
.fetch_optional(pool)
.await?;
if let Some(r) = avg_row {
let avg_width: i32 = r.get(0);
if avg_width as usize > max_len * 8 / 10 {
println!(
"cargo:warning=DB-Forge [{schema}.{table}.{name}]: \
avg_width observé ({avg_width}B) > 80% de max_len ({max_len}B). \
Pression sur DYNAMIC_CAP. Relancer ANALYZE si données récentes. \
Envisager d'augmenter la contrainte VARCHAR."
);
}
}
fields.push(VarlenField { name, max_len, is_pre_escaped });
}
Ok(fields)
}
fn parse_check_length_limit(consrc: &str) -> Option<usize> {
let after_le = consrc.split("<=").nth(1)?;
after_le
.trim()
.trim_end_matches(')')
.trim()
.parse::<usize>()
.ok()
}
#[derive(Debug, Clone)]
struct TypeMapping {
row_type: &'static str,
store_type: &'static str,
from_expr: &'static str,
is_fixed: bool,
size_bytes: usize,
alignment: usize,
}
fn map_type(sql_type: &str) -> TypeMapping {
let t = sql_type.split('(').next().unwrap_or(sql_type).trim().to_lowercase();
match t.as_str() {
"int8" | "bigint" => TypeMapping {
row_type: "i64", store_type: "i64",
from_expr: "{field}.unwrap_or(-1)",
is_fixed: true, size_bytes: 8, alignment: 8,
},
"int4" | "integer" | "int" | "serial" => TypeMapping {
row_type: "i32", store_type: "i32",
from_expr: "{field}.unwrap_or(0)",
is_fixed: true, size_bytes: 4, alignment: 4,
},
"int2" | "smallint" => TypeMapping {
row_type: "i16", store_type: "i16",
from_expr: "{field}.unwrap_or(0)",
is_fixed: true, size_bytes: 2, alignment: 2,
},
"bool" | "boolean" => TypeMapping {
row_type: "bool", store_type: "bool",
from_expr: "{field}.unwrap_or(false)",
is_fixed: true, size_bytes: 1, alignment: 1,
},
"uuid" => TypeMapping {
row_type: "[u8; 16]", store_type: "[u8; 16]",
from_expr: "{field}.unwrap_or([0u8; 16])",
is_fixed: true, size_bytes: 16, alignment: 1,
},
"timestamptz" | "timestamp with time zone" => TypeMapping {
row_type: "chrono::DateTime<chrono::Utc>",
store_type: "i64",
from_expr: "{field}.map(|dt| dt.timestamp_micros()).unwrap_or(0)",
is_fixed: true, size_bytes: 8, alignment: 8,
},
"timestamp" | "timestamp without time zone" => TypeMapping {
row_type: "chrono::NaiveDateTime",
store_type: "i64",
from_expr: "{field}.map(|dt| dt.and_utc().timestamp_micros()).unwrap_or(0)",
is_fixed: true, size_bytes: 8, alignment: 8,
},
"date" => TypeMapping {
row_type: "chrono::NaiveDate",
store_type: "i32",
from_expr: "{field}.map(|d| d.num_days_from_ce()).unwrap_or(0)",
is_fixed: true, size_bytes: 4, alignment: 4,
},
"float4" | "real" => TypeMapping {
row_type: "f32", store_type: "f32",
from_expr: "{field}.unwrap_or(0.0)",
is_fixed: true, size_bytes: 4, alignment: 4,
},
"float8" | "double precision" => TypeMapping {
row_type: "f64", store_type: "f64",
from_expr: "{field}.unwrap_or(0.0)",
is_fixed: true, size_bytes: 8, alignment: 8,
},
"text" | "varchar" | "character varying"
| "jsonb" | "json" | "bytea" | "ltree" => TypeMapping {
row_type: "String",
store_type: "/* VARLENA — exclu du StorageRow repr(C) */",
from_expr: "/* VARLENA — non transféré dans StorageRow */",
is_fixed: false, size_bytes: 0, alignment: 0,
},
"pg_lsn" => TypeMapping {
row_type: "/* PHASE2_ONLY: walsn → u64 via mmap */",
store_type: "/* PHASE2_ONLY */",
from_expr: "/* PHASE2_ONLY */",
is_fixed: false, size_bytes: 8, alignment: 8,
},
other => {
println!("cargo:warning=DB-Forge : type SQL inconnu '{other}' — exclu");
TypeMapping {
row_type: "/* INCONNU */", store_type: "/* INCONNU */",
from_expr: "/* INCONNU */",
is_fixed: false, size_bytes: 0, alignment: 0,
}
}
}
}
fn write_row_struct(
out: &mut String,
schema: &str,
table: &str,
columns: &[Column],
varlena: &[VarlenField],
) {
let name = to_pascal(&format!("{schema}_{table}"));
writeln!(out,
"/// Struct de désérialisation sqlx pour {schema}.{table}.\n\
/// Types natifs Rust + Option<T> pour nullable. NON repr(C).\n\
/// Varlena JOIN : Option<String> (allocation sqlx, durée éphémère).\n\
/// Transformer en StorageRow (From impl) + RenderPayload (as_deref) avant usage."
).unwrap();
writeln!(out, "#[derive(Debug, sqlx::FromRow)]").unwrap();
writeln!(out, "pub struct {name}Row {{").unwrap();
for col in columns {
let m = map_type(&col.sql_type);
if m.is_fixed {
if col.is_notnull {
writeln!(out, " pub {}: {},", col.name, m.row_type).unwrap();
} else {
writeln!(out, " pub {}: Option<{}>, // NULLABLE → sentinel dans StorageRow", col.name, m.row_type).unwrap();
}
} else {
if m.row_type.starts_with("/*") {
writeln!(out,
" // EXCLU Phase 1 : {} ({}) — {}",
col.name, col.sql_type, m.row_type
).unwrap();
} else if col.is_notnull {
writeln!(out, " pub {}: {}, // varlena table principale", col.name, m.row_type).unwrap();
} else {
writeln!(out, " pub {}: Option<{}>, // varlena NULLABLE table principale", col.name, m.row_type).unwrap();
}
}
}
for v in varlena {
writeln!(out,
" pub {}: Option<String>, // varlena JOIN VARCHAR({}) — → RenderPayload as_deref",
v.name, v.max_len
).unwrap();
}
writeln!(out, "}}\n").unwrap();
}
fn write_store_struct(
out: &mut String,
schema: &str,
table: &str,
columns: &[Column],
) {
let name = to_pascal(&format!("{schema}_{table}"));
writeln!(out,
"/// Struct de stockage en mémoire contiguë pour {schema}.{table}.\n\
/// #[repr(C)] : layout bit-à-bit aligné sur le heap tuple PostgreSQL.\n\
/// Champs fixed-length uniquement. Nullable → sentinel (0 ou -1 selon type).\n\
/// Varlena exclues : projetées depuis RenderPayload par Fragment-Forge.\n\
///\n\
/// AVERTISSEMENT NULLABLE : le choix du sentinel est domain-specific.\n\
/// La Forge full requiert une annotation COMMENT ON COLUMN par colonne nullable.\n\
/// Pour ce prototype, les valeurs par défaut de map_type() s'appliquent."
).unwrap();
writeln!(out, "#[repr(C)]").unwrap();
writeln!(out, "#[derive(Debug, Clone, Copy, Default)]").unwrap();
writeln!(out, "pub struct {name}StorageRow {{").unwrap();
let mut layout_bytes = 0usize;
let mut max_align = 1usize;
for col in columns {
let m = map_type(&col.sql_type);
if m.is_fixed {
let null_marker = if col.is_notnull { "" } else { " [sentinel]" };
let pad = if col.name.len() < 20 {
" ".repeat(20 - col.name.len().min(20))
} else {
String::new()
};
writeln!(out,
" pub {}: {},{} // attnum={}, {}B{}",
col.name, m.store_type, pad,
col.attnum, m.size_bytes, null_marker,
).unwrap();
layout_bytes += m.size_bytes;
max_align = max_align.max(m.alignment);
} else {
writeln!(out,
" // VARLENA exclu : {} ({}) → RenderPayload",
col.name, col.sql_type
).unwrap();
}
}
writeln!(out, "}}").unwrap();
let padded_size = layout_bytes.div_ceil(max_align.max(1)) * max_align.max(1);
writeln!(out, "// Layout fixed-length : {layout_bytes}B données → {padded_size}B padded (align={max_align}B)").unwrap();
writeln!(out, "// + {}B header heap PostgreSQL (MAXALIGN(23 + ceil({}/8)))",
{ let n = columns.len(); (23 + n.div_ceil(8)).div_ceil(8) * 8 },
columns.len()
).unwrap();
writeln!(out).unwrap();
writeln!(out,
"const _: () = assert!(\n \
std::mem::size_of::<{name}StorageRow>() == {padded_size},\n \
\"DB-Forge [{schema}.{table}]: size_of diverge du DDL — reconstruire après ALTER TABLE\",\n\
);"
).unwrap();
writeln!(out,
"const _: () = assert!(\n \
std::mem::align_of::<{name}StorageRow>() == {max_align},\n \
\"DB-Forge [{schema}.{table}]: align_of diverge du DDL — vérifier les types colonnes\",\n\
);"
).unwrap();
writeln!(out).unwrap();
}
fn write_varlen_owned_struct(
out: &mut String,
schema: &str,
table: &str,
varlena: &[VarlenField],
) {
if varlena.is_empty() {
writeln!(out,
"// {schema}.{table} : aucun champ varlena — type VarlenOwned = () dans le trait.\n"
).unwrap();
return;
}
let name = to_pascal(&format!("{schema}_{table}"));
writeln!(out,
"/// Données varlena possédées pour {schema}.{table}.\n\
/// Send + 'static : traversée tokio::spawn et rayon::par_iter sans contrainte.\n\
/// Produite par fetch_batch() depuis les Row SQLx (conversion directe).\n\
/// render() reconstruit les &str localement via as_deref() — zéro copie."
).unwrap();
writeln!(out, "#[derive(Debug, Default)]").unwrap();
writeln!(out, "pub struct {name}VarlenOwned {{").unwrap();
for v in varlena {
writeln!(out,
" /// VARCHAR({}) — {} × {}.",
v.max_len,
if v.is_pre_escaped { "pré-échappé, facteur" } else { "escape HTML, facteur" },
if v.is_pre_escaped { 1 } else { VarlenField::HTML_ESCAPE_FACTOR },
).unwrap();
writeln!(out, " pub {}: Option<String>,", v.name).unwrap();
}
writeln!(out, "}}\n").unwrap();
}
fn write_from_impl(
out: &mut String,
schema: &str,
table: &str,
columns: &[Column],
) {
let name = to_pascal(&format!("{schema}_{table}"));
writeln!(out, "impl From<{name}Row> for {name}StorageRow {{").unwrap();
writeln!(out, " fn from(r: {name}Row) -> Self {{").unwrap();
writeln!(out, " Self {{").unwrap();
for col in columns {
let m = map_type(&col.sql_type);
if !m.is_fixed { continue; }
let expr = if col.is_notnull {
match m.row_type {
"chrono::DateTime<chrono::Utc>" => {
format!("r.{}.timestamp_micros()", col.name)
}
"chrono::NaiveDateTime" => {
format!("r.{}.and_utc().timestamp_micros()", col.name)
}
"chrono::NaiveDate" => {
format!("r.{}.num_days_from_ce()", col.name)
}
_ => format!("r.{}", col.name),
}
} else {
m.from_expr.replace("{field}", &format!("r.{}", col.name))
};
writeln!(out, " {}: {},", col.name, expr).unwrap();
}
writeln!(out, " }}").unwrap();
writeln!(out, " }}").unwrap();
writeln!(out, "}}\n").unwrap();
}
fn write_collector(
out: &mut String,
schema: &str,
table: &str,
pk_col: &str,
max_entity_id: usize,
) {
let screaming = to_screaming(&format!("{schema}_{table}"));
let words = max_entity_id.div_ceil(64);
writeln!(out, "// Collector dimensionné pour {schema}.{table}").unwrap();
writeln!(out, "// PK = {pk_col} | MAX_ID+20% arrondi power-of-two").unwrap();
writeln!(out, "pub const MAX_{screaming}_ID: usize = {max_entity_id};").unwrap();
writeln!(out, "pub const {screaming}_WORDS: usize = {words};").unwrap();
writeln!(out,
"pub static {screaming}_COLLECTOR: crate::collector::Collector<MAX_{screaming}_ID, {screaming}_WORDS> ="
).unwrap();
writeln!(out, " crate::collector::Collector::new_zeroed();\n").unwrap();
}
fn write_projection_stub(
out: &mut String,
schema: &str,
table: &str,
columns: &[Column],
pk: &PrimaryKey,
varlena: &[VarlenField],
varlena_join: Option<(&str, &str, &str)>,
) {
let name = to_pascal(&format!("{schema}_{table}"));
let proj_name = format!("{name}Projection");
let screaming = to_screaming(&format!("{schema}_{table}"));
let varlen_owned_type = if varlena.is_empty() {
"()".to_string()
} else {
format!("{name}VarlenOwned")
};
let fixed_cols: Vec<&str> = columns.iter()
.filter(|c| map_type(&c.sql_type).is_fixed)
.map(|c| c.name.as_str())
.collect();
if fixed_cols.is_empty() {
eprintln!(
"cargo:warning=DB-Forge [{schema}.{table}] : \
aucune colonne fixed-length — stub incomplet généré."
);
}
let (select, from_clause) = if let Some((vs, vt, fk)) = varlena_join {
let varlena_cols: Vec<String> = varlena.iter()
.map(|v| format!("{vt}.{}", v.name))
.collect();
let all_cols: Vec<String> = fixed_cols.iter().map(|c| c.to_string())
.chain(varlena_cols)
.collect();
let select = all_cols.join(", ");
let from = format!(
"{schema}.{table} LEFT JOIN {vs}.{vt} ON {schema}.{table}.{fk} = {vs}.{vt}.{fk}"
);
(select, from)
} else {
(fixed_cols.join(", "), format!("{schema}.{table}"))
};
let where_clause = match pk {
PrimaryKey::Single(col) => {
format!("WHERE {schema}.{table}.{col} = ANY($1) ORDER BY {schema}.{table}.{col} ASC")
}
PrimaryKey::Composite => "WHERE 1=1 /* PK composite: adapter */".to_string(),
};
let field_specs: Vec<FieldSpec> = columns.iter()
.filter(|c| map_type(&c.sql_type).is_fixed)
.filter_map(|c| {
FieldKind::from_sql_type(&c.sql_type).map(|kind| FieldSpec {
name: c.name.clone(),
kind,
attnum: c.attnum,
})
})
.collect();
let pk_field_name = match pk {
PrimaryKey::Single(col) => col.as_str(),
PrimaryKey::Composite => field_specs.first().map(|f| f.name.as_str()).unwrap_or("id"),
};
let (static_cap, dynamic_cap, render_body) = generate_render(
schema, table, &name,
&field_specs,
pk_field_name,
varlena,
);
let cap_consts = generate_capacity_consts(&screaming, static_cap, dynamic_cap);
writeln!(out, "pub struct {proj_name};").unwrap();
writeln!(out).unwrap();
writeln!(out, "{cap_consts}").unwrap();
writeln!(out, "// Pool requis : marius_user (SELECT non révoqué sur {schema}.{table})").unwrap();
writeln!(out, "// RLS : voir 09_rls/01_policies.sql").unwrap();
writeln!(out, "impl crate::projection::Projection for {proj_name} {{").unwrap();
writeln!(out, " type Record = {name}StorageRow;").unwrap();
writeln!(out, " type VarlenOwned = {varlen_owned_type};").unwrap();
writeln!(out).unwrap();
writeln!(out, " async fn fetch_batch(").unwrap();
writeln!(out, " pool: &sqlx::PgPool,").unwrap();
writeln!(out, " ids: &[i64],").unwrap();
writeln!(out, " ) -> Result<Vec<(Self::Record, Self::VarlenOwned)>, sqlx::Error> {{").unwrap();
if fixed_cols.is_empty() {
writeln!(out,
" todo!(\"DB-Forge: aucune colonne fixed-length pour {schema}.{table}\")"
).unwrap();
} else {
writeln!(out, " let rows = sqlx::query_as::<_, {name}Row>(").unwrap();
writeln!(out,
" \"SELECT {select} FROM {from_clause} {where_clause}\","
).unwrap();
writeln!(out, " )").unwrap();
writeln!(out, " .bind(ids)").unwrap();
writeln!(out, " .fetch_all(pool)").unwrap();
writeln!(out, " .await?;").unwrap();
if varlena.is_empty() {
writeln!(out,
" Ok(rows.into_iter().map(|r| ({name}StorageRow::from(r), ())).collect())"
).unwrap();
} else {
writeln!(out, " Ok(rows.into_iter().map(|r| {{").unwrap();
writeln!(out, " let {name}Row {{").unwrap();
for col in columns {
let m = map_type(&col.sql_type);
if m.is_fixed {
writeln!(out, " {},", col.name).unwrap();
}
}
for v in varlena {
writeln!(out, " {},", v.name).unwrap();
}
writeln!(out, " ..").unwrap();
writeln!(out, " }} = r;").unwrap();
writeln!(out, " let owned = {name}VarlenOwned {{").unwrap();
for v in varlena {
writeln!(out, " {},", v.name).unwrap();
}
writeln!(out, " }};").unwrap();
writeln!(out, " let storage = {name}StorageRow {{").unwrap();
for col in columns {
let m = map_type(&col.sql_type);
if !m.is_fixed { continue; }
let expr = if col.is_notnull {
match m.row_type {
"chrono::DateTime<chrono::Utc>" => {
format!("{}.timestamp_micros()", col.name)
}
"chrono::NaiveDateTime" => {
format!("{}.and_utc().timestamp_micros()", col.name)
}
"chrono::NaiveDate" => {
format!("{}.num_days_from_ce()", col.name)
}
_ => col.name.clone(),
}
} else {
m.from_expr.replace("{field}", &col.name)
};
if expr == col.name {
writeln!(out, " {},", col.name).unwrap();
} else {
writeln!(out, " {}: {},", col.name, expr).unwrap();
}
}
writeln!(out, " }};").unwrap();
writeln!(out, " (storage, owned)").unwrap();
writeln!(out, " }}).collect())").unwrap();
}
}
writeln!(out, " }}").unwrap();
writeln!(out).unwrap();
let varlena_param = if varlena.is_empty() {
"_varlena: &()".to_string()
} else {
format!("varlena: &{name}VarlenOwned")
};
writeln!(out,
" fn render(record: &Self::Record, {varlena_param}, buf: &mut String) {{"
).unwrap();
for line in render_body.lines() {
writeln!(out, " {line}").unwrap();
}
writeln!(out, " }}").unwrap();
writeln!(out).unwrap();
writeln!(out, " fn artifact_path(record: &Self::Record) -> PathBuf {{").unwrap();
writeln!(out, " let root = std::env::var(\"MARIUS_ARTIFACTS_DIR\")").unwrap();
writeln!(out, " .unwrap_or_else(|_| \"artifacts\".to_string());").unwrap();
writeln!(out,
" PathBuf::from(format!(\"{{root}}/{schema}/{table}/{{}}.html\", record.{pk_field_name}))"
).unwrap();
writeln!(out, " }}").unwrap();
writeln!(out, "}}\n").unwrap();
}
fn write_section_header(out: &mut String, schema: &str, table: &str, pk: &PrimaryKey) {
let pk_info = match pk {
PrimaryKey::Single(col) => format!("PK={col}"),
PrimaryKey::Composite => "PK composite — Collector N/A".to_string(),
};
writeln!(out, "// {}", "=".repeat(60)).unwrap();
writeln!(out, "// {schema}.{table} · {pk_info}").unwrap();
writeln!(out, "// {}\n", "=".repeat(60)).unwrap();
}
fn to_pascal(s: &str) -> String {
s.split('_').map(|w| {
let mut c = w.chars();
match c.next() {
None => String::new(),
Some(f) => f.to_uppercase().collect::<String>() + c.as_str(),
}
}).collect()
}
fn to_screaming(s: &str) -> String {
s.to_uppercase()
}