use backbeat::{schema::FieldRole, wire::OwnedSchema};
use std::collections::HashMap;
fn is_promoted(role: FieldRole) -> bool {
matches!(
role,
FieldRole::Key | FieldRole::SpanId | FieldRole::ParentSpanId
)
}
fn role_label(role: FieldRole) -> &'static str {
match role {
FieldRole::Key => "key",
FieldRole::SpanId => "span_id",
FieldRole::ParentSpanId => "parent_span_id",
_ => "",
}
}
fn quote_ident(name: &str) -> String {
format!("\"{}\"", name.replace('"', "\"\""))
}
fn quote_str(s: &str) -> String {
format!("'{}'", s.replace('\'', "''"))
}
fn display_names(schemas: &[OwnedSchema]) -> Vec<String> {
let mut counts: HashMap<&str, usize> = HashMap::new();
for s in schemas {
*counts.entry(s.qualified_name.as_str()).or_default() += 1;
}
schemas
.iter()
.map(|s| {
if counts[s.qualified_name.as_str()] > 1 {
format!("{}#{:016x}", s.qualified_name, s.id.get())
} else {
s.qualified_name.clone()
}
})
.collect()
}
pub fn generate_tier1(schemas: &[OwnedSchema]) -> String {
let names = display_names(schemas);
let mut out = String::new();
out.push_str(
"-- backbeat Tier-1 views (generated from the dump's schema registry).\n\
-- Everything below builds on a base view named `events`; create it over your Parquet\n\
-- before running these (the `.sql` sidecar convert writes does this for you).\n\n",
);
out.push_str("-- One view per event type.\n");
for (s, name) in schemas.iter().zip(&names) {
out.push_str(&format!(
"CREATE OR REPLACE VIEW {} AS SELECT * FROM events WHERE event_id = {};\n",
quote_ident(name),
s.id.get()
));
}
out.push_str("\n-- Discoverable key/span columns: (event, column, role).\n");
let mut rows: Vec<String> = Vec::new();
for (s, name) in schemas.iter().zip(&names) {
for f in s.fields.iter().filter(|f| is_promoted(f.role)) {
rows.push(format!(
" ({}, {}, {})",
quote_str(name),
quote_str(&f.name),
quote_str(role_label(f.role))
));
}
}
if rows.is_empty() {
out.push_str(
"CREATE OR REPLACE VIEW backbeat_keys AS\n \
SELECT NULL::VARCHAR AS event, NULL::VARCHAR AS field, NULL::VARCHAR AS role WHERE false;\n",
);
} else {
out.push_str("CREATE OR REPLACE VIEW backbeat_keys AS\n SELECT * FROM (VALUES\n");
out.push_str(&rows.join(",\n"));
out.push_str("\n ) AS t(event, field, role);\n");
}
out
}
pub fn bootstrap(parquet_path: &str) -> String {
format!(
"-- Bind the base table to this conversion's Parquet output.\n\
CREATE OR REPLACE VIEW events AS SELECT * FROM read_parquet({});\n\n",
quote_str(parquet_path)
)
}
pub fn assemble(schemas: &[OwnedSchema], tier2: &[String]) -> String {
let mut out = generate_tier1(schemas);
for (i, sql) in tier2.iter().enumerate() {
out.push_str(&format!("\n-- Registered view set #{}.\n", i + 1));
out.push_str(sql);
if !sql.ends_with('\n') {
out.push('\n');
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use backbeat::{
id::EventId,
schema::FieldType,
wire::{OwnedField, OwnedSchema},
};
fn field(name: &str, role: FieldRole) -> OwnedField {
OwnedField {
name: name.to_string(),
description: None,
ty: FieldType::U64,
offset: 0,
width: 8,
role,
unit: None,
sentinel: None,
enum_labels: Vec::new(),
}
}
fn schema(id: u64, name: &str, fields: Vec<OwnedField>) -> OwnedSchema {
OwnedSchema {
id: EventId(id),
qualified_name: name.to_string(),
description: None,
record_size: 8,
phase: backbeat::schema::Phase::None,
fields,
}
}
#[test]
fn quotes_identifiers_and_strings() {
assert_eq!(quote_ident("my::Event"), "\"my::Event\"");
assert_eq!(quote_ident("a\"b"), "\"a\"\"b\"");
assert_eq!(quote_str("it's"), "'it''s'");
}
#[test]
fn per_event_view_filters_by_id() {
let s = vec![schema(7, "ns::Pkt", vec![field("conn_id", FieldRole::Key)])];
let ddl = generate_tier1(&s);
assert!(ddl.contains(
"CREATE OR REPLACE VIEW \"ns::Pkt\" AS SELECT * FROM events WHERE event_id = 7;"
));
assert!(ddl.contains("CREATE OR REPLACE VIEW backbeat_keys"));
assert!(ddl.contains("('ns::Pkt', 'conn_id', 'key')"));
}
#[test]
fn empty_key_manifest_is_a_typed_zero_row_view() {
let s = vec![schema(1, "ns::Marker", vec![field("x", FieldRole::None)])];
let ddl = generate_tier1(&s);
assert!(ddl.contains("CREATE OR REPLACE VIEW backbeat_keys"));
assert!(ddl.contains("WHERE false"));
assert!(!ddl.contains("VALUES"));
}
#[test]
fn collision_suffixes_display_name_with_id() {
let s = vec![
schema(0xAA, "ns::E", vec![field("k", FieldRole::Key)]),
schema(0xBB, "ns::E", vec![field("k", FieldRole::Key)]),
];
let ddl = generate_tier1(&s);
assert!(ddl.contains("\"ns::E#00000000000000aa\""));
assert!(ddl.contains("\"ns::E#00000000000000bb\""));
}
#[test]
fn assemble_appends_tier2_with_trailing_newline() {
let s = vec![schema(1, "ns::E", vec![field("k", FieldRole::Key)])];
let ddl = assemble(&s, &["CREATE MACRO m() AS TABLE SELECT 1;".to_string()]);
assert!(ddl.contains("-- Registered view set #1."));
assert!(ddl.contains("CREATE MACRO m() AS TABLE SELECT 1;"));
assert!(
ddl.ends_with('\n'),
"tier-2 without trailing newline gets one"
);
}
#[test]
fn bootstrap_binds_events_to_path() {
assert!(bootstrap("out.parquet").contains(
"CREATE OR REPLACE VIEW events AS SELECT * FROM read_parquet('out.parquet')"
));
}
}