use std::cmp::max;
use super::{indent_level::IndentLevel, BqColumn, BqDataType, BqNonArrayDataType};
use crate::common::*;
use crate::schema::DataType;
#[derive(Clone, Copy, Eq, Ord, PartialEq, PartialOrd)]
pub(crate) enum NeedsCustomJsonExport {
Never,
OnlyInsideUdf,
Always,
}
impl NeedsCustomJsonExport {
pub(crate) fn in_sql_code(self) -> bool {
match self {
NeedsCustomJsonExport::Never | NeedsCustomJsonExport::OnlyInsideUdf => {
false
}
NeedsCustomJsonExport::Always => true,
}
}
fn in_js_code(self) -> bool {
match self {
NeedsCustomJsonExport::Never => false,
NeedsCustomJsonExport::OnlyInsideUdf | NeedsCustomJsonExport::Always => {
true
}
}
}
}
pub(crate) fn needs_custom_json_export(
ty: &BqDataType,
) -> Result<NeedsCustomJsonExport> {
match ty {
BqDataType::Array(nested) => non_array_needs_custom_json_export(nested),
BqDataType::NonArray(nested) => non_array_needs_custom_json_export(nested),
}
}
fn non_array_needs_custom_json_export(
ty: &BqNonArrayDataType,
) -> Result<NeedsCustomJsonExport> {
match ty {
BqNonArrayDataType::Bool
| BqNonArrayDataType::Float64
| BqNonArrayDataType::Int64
| BqNonArrayDataType::Numeric
| BqNonArrayDataType::String
| BqNonArrayDataType::Timestamp => Ok(NeedsCustomJsonExport::Never),
BqNonArrayDataType::Date => Ok(NeedsCustomJsonExport::OnlyInsideUdf),
BqNonArrayDataType::Stringified(DataType::Uuid) => Ok(NeedsCustomJsonExport::Never),
BqNonArrayDataType::Stringified(DataType::GeoJson(_))
| BqNonArrayDataType::Stringified(DataType::Json) => Ok(NeedsCustomJsonExport::Always),
BqNonArrayDataType::Stringified(nested) => Err(format_err!(
"did not expect type {:?} to be represented a stringified value in BigQuery",
nested,
)),
BqNonArrayDataType::Struct(fields) => {
fields.iter()
.map(|f| needs_custom_json_export(&f.ty))
.try_fold(
NeedsCustomJsonExport::Never,
|acc: NeedsCustomJsonExport, result: Result<NeedsCustomJsonExport>| {
Ok(max(acc, result?))
}
)
}
BqNonArrayDataType::Datetime
| BqNonArrayDataType::Geography => Ok(NeedsCustomJsonExport::OnlyInsideUdf),
BqNonArrayDataType::Bytes
| BqNonArrayDataType::Time => Err(format_err!("don't know how to export {}", ty)),
}
}
pub(crate) fn generate_export_udf(
column: &BqColumn,
idx: usize,
f: &mut dyn Write,
) -> Result<()> {
let ty = column.bq_data_type()?;
write!(
f,
r#"CREATE TEMP FUNCTION ExportJson_{idx}(value {bq_type})
RETURNS STRING
LANGUAGE js AS """
"#,
idx = idx,
bq_type = ty,
)?;
let indent = IndentLevel::none();
match ty {
BqDataType::Array(_) => {
write!(f, "const arr = ")?;
write_transform_expr("value", &ty, indent, f)?;
writeln!(f, ";")?;
writeln!(
f,
r#"if (arr === []) {{ return ""; }} else {{ return JSON.stringify(arr); }}"#
)?;
}
BqDataType::NonArray(_) => {
write!(f, "return JSON.stringify(")?;
write_transform_expr("value", &ty, indent, f)?;
writeln!(f, ");")?;
}
}
writeln!(
f,
r#"""";
"#
)?;
Ok(())
}
fn write_transform_expr(
input_expr: &str,
input_type: &BqDataType,
indent: IndentLevel,
f: &mut dyn Write,
) -> Result<()> {
if needs_custom_json_export(input_type)?.in_js_code() {
match input_type {
BqDataType::Array(ty) => {
writeln!(
f,
"({ie}) == null ? null : {ie}.map(function (e) {{",
ie = input_expr,
)?;
write!(f, "{}return ", indent.incr())?;
write_non_array_transform_expr("e", ty, indent.incr(), f)?;
writeln!(f, ";")?;
write!(f, "{}}})", indent)?;
}
BqDataType::NonArray(ty) => {
write_non_array_transform_expr(input_expr, ty, indent, f)?;
}
}
} else {
write!(f, "{}", input_expr)?;
}
Ok(())
}
fn write_non_array_transform_expr(
input_expr: &str,
input_type: &BqNonArrayDataType,
indent: IndentLevel,
f: &mut dyn Write,
) -> Result<()> {
match input_type {
BqNonArrayDataType::Bool
| BqNonArrayDataType::Float64
| BqNonArrayDataType::Int64
| BqNonArrayDataType::Numeric
| BqNonArrayDataType::String
| BqNonArrayDataType::Timestamp => {
write!(f, "{}", input_expr)?;
}
BqNonArrayDataType::Date => {
write!(f, "{}.toISOString().split('T')[0]", input_expr)?;
}
BqNonArrayDataType::Stringified(DataType::Uuid) => {
write!(f, "{}", input_expr)?;
}
BqNonArrayDataType::Stringified(DataType::GeoJson(_))
| BqNonArrayDataType::Stringified(DataType::Json) => {
write!(f, "JSON.parse({})", input_expr)?;
}
BqNonArrayDataType::Stringified(nested) => return Err(format_err!(
"did not expect type {:?} to be represented a stringified value in BigQuery",
nested,
)),
BqNonArrayDataType::Struct(fields) => {
write!(f, "({}) == null ? null : {{", input_expr)?;
let mut first = true;
for field in fields {
if first {
first = false;
} else {
write!(f, ",")?;
}
write!(f, "\n{}", indent.incr())?;
if let Some(name) = &field.name {
let id = name.javascript_quoted();
write!(f, "{}: ", id)?;
let field_expr = format!("{}[{}]", input_expr, id);
write_transform_expr(&field_expr, &field.ty, indent.incr(), f)?;
} else {
return Err(format_err!(
"cannot export unnamed field from {}",
input_type,
));
}
}
write!(f, "\n{}}}", indent)?;
},
BqNonArrayDataType::Bytes
| BqNonArrayDataType::Datetime
| BqNonArrayDataType::Geography
| BqNonArrayDataType::Time => return Err(format_err!(
"don't know how to export {} inside JSON", input_type,
)),
}
Ok(())
}