use std::fmt::Write;
use scythe_backend::manifest::BackendManifest;
use scythe_backend::naming::{composite_type_name, enum_type_name, enum_variant_name, fn_name, to_pascal_case};
use scythe_backend::types::resolve_type;
use scythe_core::analyzer::{AnalyzedQuery, CompositeInfo, EnumInfo};
use scythe_core::errors::{ErrorCode, ScytheError};
use scythe_core::parser::QueryCommand;
use crate::GeneratedCode;
use crate::backend_trait::{CodegenBackend, ResolvedColumn, ResolvedParam};
use crate::backends::go_common::go_file_header;
const DEFAULT_MANIFEST_TOML: &str = include_str!("../../manifests/go-godror.toml");
const GODROR_HEADER_PREAMBLE: &str =
"// Code generated by scythe. DO NOT EDIT.\n// Run `goimports -w .` to fix imports.\npackage queries";
#[derive(Clone, Copy, PartialEq, Eq)]
enum OutParamKind {
Int32,
Int64,
Float64,
Time,
StringLike,
}
impl OutParamKind {
fn from_neutral_type(neutral_type: &str) -> Self {
match neutral_type {
"int32" => Self::Int32,
"int64" => Self::Int64,
"float32" | "float64" | "decimal" => Self::Float64,
"date" | "datetime" | "datetime_tz" | "time" | "time_tz" => Self::Time,
_ => Self::StringLike,
}
}
fn out_var_type(self, nullable: bool) -> &'static str {
match (self, nullable) {
(Self::Int32, false) => "int32",
(Self::Int64, false) => "int64",
(Self::Float64, false) => "float64",
(Self::Time, false) => "time.Time",
(Self::StringLike, _) => "string",
(Self::Int32, true) => "sql.NullInt32",
(Self::Int64, true) => "sql.NullInt64",
(Self::Float64, true) => "sql.NullFloat64",
(Self::Time, true) => "sql.NullTime",
}
}
fn null_accessor(self) -> Option<(&'static str, &'static str)> {
match self {
Self::Int32 => Some(("Int32", "int32")),
Self::Int64 => Some(("Int64", "int64")),
Self::Float64 => Some(("Float64", "float64")),
Self::Time => Some(("Time", "time.Time")),
Self::StringLike => None,
}
}
}
pub struct GoGodrorBackend {
manifest: BackendManifest,
}
impl GoGodrorBackend {
pub fn new(engine: &str) -> Result<Self, ScytheError> {
match engine {
"oracle" => {}
_ => {
return Err(ScytheError::new(
ErrorCode::InvalidConfig,
format!("go-godror only supports Oracle, got engine '{}'", engine),
));
}
}
let manifest = super::parse_manifest(DEFAULT_MANIFEST_TOML)?;
Ok(Self { manifest })
}
}
impl CodegenBackend for GoGodrorBackend {
fn name(&self) -> &str {
"go-godror"
}
fn manifest(&self) -> &scythe_backend::manifest::BackendManifest {
&self.manifest
}
fn manifest_mut(&mut self) -> &mut scythe_backend::manifest::BackendManifest {
&mut self.manifest
}
fn supported_engines(&self) -> &[&str] {
&["oracle"]
}
fn file_header(&self) -> String {
go_file_header(
GODROR_HEADER_PREAMBLE,
&["context", "database/sql"],
&[],
&self.manifest,
&[],
)
}
fn file_header_for_results(&self, generated: &[GeneratedCode]) -> String {
go_file_header(
GODROR_HEADER_PREAMBLE,
&["context", "database/sql"],
&[],
&self.manifest,
generated,
)
}
fn generate_struct_decl(
&self,
struct_name: &str,
_query_name: &str,
columns: &[ResolvedColumn],
) -> Result<String, ScytheError> {
let mut out = String::new();
let _ = writeln!(out, "type {} struct {{", struct_name);
for col in columns {
let field = to_pascal_case(&col.field_name);
let json_tag = &col.field_name;
let _ = writeln!(out, "\t{} {} `json:\"{}\"`", field, col.full_type, json_tag);
}
let _ = write!(out, "}}");
Ok(out)
}
fn generate_query_fn(
&self,
analyzed: &AnalyzedQuery,
struct_name: &str,
columns: &[ResolvedColumn],
params: &[ResolvedParam],
) -> Result<String, ScytheError> {
let func_name = fn_name(&analyzed.name, &self.manifest.naming);
let sql = crate::sql_literal::escape_go_interpreted_string(&super::rewrite_pg_placeholders(
&super::clean_sql_oneline_with_optional(&analyzed.sql, &analyzed.optional_params, &analyzed.params),
|n| format!(":{n}"),
));
let param_list = params
.iter()
.map(|p| format!("{} {}", p.field_name, p.full_type))
.collect::<Vec<_>>()
.join(", ");
let sep = if param_list.is_empty() { "" } else { ", " };
let args = if params.is_empty() {
String::new()
} else {
format!(
", {}",
params
.iter()
.map(|p| p.field_name.clone())
.collect::<Vec<_>>()
.join(", ")
)
};
let has_returning = sql.to_uppercase().contains("RETURNING");
let mut out = String::new();
match &analyzed.command {
QueryCommand::One | QueryCommand::Opt => {
let _ = writeln!(
out,
"func {}(ctx context.Context, db *sql.DB{}{}) (*{}, error) {{",
func_name, sep, param_list, struct_name
);
if has_returning {
for col in columns {
let kind = OutParamKind::from_neutral_type(&col.neutral_type);
let go_type = kind.out_var_type(col.nullable);
let _ = writeln!(out, "\tvar out{} {}", to_pascal_case(&col.field_name), go_type);
}
let into_clause = columns
.iter()
.enumerate()
.map(|(i, _)| format!(":{}", params.len() + i + 1))
.collect::<Vec<_>>()
.join(", ");
let full_sql = format!("{} INTO {}", sql, into_clause);
let out_args: String = columns
.iter()
.map(|c| format!(", sql.Out{{Dest: &out{}}}", to_pascal_case(&c.field_name)))
.collect();
let _ = writeln!(
out,
"\tif _, err := db.ExecContext(ctx, \"{}\"{}{}); err != nil {{",
full_sql, args, out_args
);
let _ = writeln!(out, "\t\treturn nil, err");
let _ = writeln!(out, "\t}}");
let mut return_fields = Vec::with_capacity(columns.len());
for col in columns {
let field = to_pascal_case(&col.field_name);
let kind = OutParamKind::from_neutral_type(&col.neutral_type);
if !col.nullable {
return_fields.push(format!("{}: out{}", field, field));
continue;
}
match kind.null_accessor() {
None => {
return_fields.push(format!("{}: &out{}", field, field));
}
Some((inner_field, ptr_type)) => {
let ptr_var = format!("out{}Ptr", field);
let _ = writeln!(out, "\tvar {} *{}", ptr_var, ptr_type);
let _ = writeln!(out, "\tif out{}.Valid {{", field);
let _ = writeln!(out, "\t\t{} = &out{}.{}", ptr_var, field, inner_field);
let _ = writeln!(out, "\t}}");
return_fields.push(format!("{}: {}", field, ptr_var));
}
}
}
let _ = writeln!(out, "\treturn &{}{{{}}}, nil", struct_name, return_fields.join(", "));
let _ = write!(out, "}}");
} else {
let _ = writeln!(out, "\trow := db.QueryRowContext(ctx, \"{}\"{})", sql, args);
let _ = writeln!(out, "\tvar item {}", struct_name);
let scan_fields: Vec<String> = columns
.iter()
.map(|c| format!("&item.{}", to_pascal_case(&c.field_name)))
.collect();
let _ = writeln!(out, "\tif err := row.Scan({}); err != nil {{", scan_fields.join(", "));
if matches!(analyzed.command, QueryCommand::Opt) {
let _ = writeln!(out, "\t\tif err == sql.ErrNoRows {{");
let _ = writeln!(out, "\t\t\treturn nil, nil");
let _ = writeln!(out, "\t\t}}");
}
let _ = writeln!(out, "\t\treturn nil, err");
let _ = writeln!(out, "\t}}");
let _ = writeln!(out, "\treturn &item, nil");
let _ = write!(out, "}}");
}
}
QueryCommand::Many => {
let _ = writeln!(
out,
"func {}(ctx context.Context, db *sql.DB{}{}) ([]{}, error) {{",
func_name, sep, param_list, struct_name
);
let _ = writeln!(out, "\trows, err := db.QueryContext(ctx, \"{}\"{})", sql, args);
let _ = writeln!(out, "\tif err != nil {{");
let _ = writeln!(out, "\t\treturn nil, err");
let _ = writeln!(out, "\t}}");
let _ = writeln!(out, "\tdefer rows.Close()");
let _ = writeln!(out, "\tvar items []{}", struct_name);
let _ = writeln!(out, "\tfor rows.Next() {{");
let _ = writeln!(out, "\t\tvar item {}", struct_name);
let scan_fields: Vec<String> = columns
.iter()
.map(|c| format!("&item.{}", to_pascal_case(&c.field_name)))
.collect();
let _ = writeln!(
out,
"\t\tif err := rows.Scan({}); err != nil {{",
scan_fields.join(", ")
);
let _ = writeln!(out, "\t\t\treturn nil, err");
let _ = writeln!(out, "\t\t}}");
let _ = writeln!(out, "\t\titems = append(items, item)");
let _ = writeln!(out, "\t}}");
let _ = writeln!(out, "\treturn items, rows.Err()");
let _ = write!(out, "}}");
}
QueryCommand::Exec => {
let _ = writeln!(
out,
"func {}(ctx context.Context, db *sql.DB{}{}) error {{",
func_name, sep, param_list
);
let _ = writeln!(out, "\t_, err := db.ExecContext(ctx, \"{}\"{})", sql, args);
let _ = writeln!(out, "\treturn err");
let _ = write!(out, "}}");
}
QueryCommand::ExecResult | QueryCommand::ExecRows => {
let _ = writeln!(
out,
"func {}(ctx context.Context, db *sql.DB{}{}) (int64, error) {{",
func_name, sep, param_list
);
let _ = writeln!(out, "\tresult, err := db.ExecContext(ctx, \"{}\"{})", sql, args);
let _ = writeln!(out, "\tif err != nil {{");
let _ = writeln!(out, "\t\treturn 0, err");
let _ = writeln!(out, "\t}}");
let _ = writeln!(out, "\treturn result.RowsAffected()");
let _ = write!(out, "}}");
}
QueryCommand::Batch => {
let batch_fn_name = format!("{}Batch", func_name);
let _ = writeln!(
out,
"func {}(ctx context.Context, db *sql.DB, items [][]any) error {{",
batch_fn_name
);
let _ = writeln!(out, "\ttx, err := db.BeginTx(ctx, nil)");
let _ = writeln!(out, "\tif err != nil {{");
let _ = writeln!(out, "\t\treturn err");
let _ = writeln!(out, "\t}}");
let _ = writeln!(out, "\tstmt, err := tx.PrepareContext(ctx, \"{}\")", sql);
let _ = writeln!(out, "\tif err != nil {{");
let _ = writeln!(out, "\t\t_ = tx.Rollback()");
let _ = writeln!(out, "\t\treturn err");
let _ = writeln!(out, "\t}}");
let _ = writeln!(out, "\tdefer stmt.Close()");
let _ = writeln!(out, "\tfor _, item := range items {{");
let _ = writeln!(out, "\t\tif _, err := stmt.ExecContext(ctx, item...); err != nil {{");
let _ = writeln!(out, "\t\t\t_ = tx.Rollback()");
let _ = writeln!(out, "\t\t\treturn err");
let _ = writeln!(out, "\t\t}}");
let _ = writeln!(out, "\t}}");
let _ = writeln!(out, "\treturn tx.Commit()");
let _ = write!(out, "}}");
}
QueryCommand::Grouped => {
unreachable!("Grouped is handled by generate_grouped_query_fn, not generate_query_fn")
}
}
Ok(out)
}
fn generate_grouped_structs(
&self,
parent_struct_name: &str,
child_struct_name: &str,
parent_columns: &[crate::backend_trait::ResolvedColumn],
child_columns: &[crate::backend_trait::ResolvedColumn],
_key_column: &str,
) -> Result<String, ScytheError> {
let mut out = String::new();
let _ = writeln!(out, "type {} struct {{", child_struct_name);
for col in child_columns {
let field = to_pascal_case(&col.field_name);
let _ = writeln!(out, "\t{} {} `json:\"{}\"`", field, col.full_type, col.field_name);
}
let _ = writeln!(out, "}}");
let _ = writeln!(out);
let _ = writeln!(out, "type {} struct {{", parent_struct_name);
for col in parent_columns {
let field = to_pascal_case(&col.field_name);
let _ = writeln!(out, "\t{} {} `json:\"{}\"`", field, col.full_type, col.field_name);
}
let _ = writeln!(out, "\tChildren []{} `json:\"children\"`", child_struct_name);
let _ = write!(out, "}}");
Ok(out)
}
fn generate_grouped_query_fn(
&self,
request: &crate::backend_trait::GroupedQueryFn<'_>,
) -> Result<String, ScytheError> {
let analyzed = request.analyzed;
let parent_struct_name = request.parent_struct_name;
let child_struct_name = request.child_struct_name;
let all_columns = request.all_columns;
let parent_columns = request.parent_columns;
let child_columns = request.child_columns;
let params = request.params;
let key_column = request.key_column;
let func_name = fn_name(&analyzed.name, &self.manifest.naming);
let sql = crate::sql_literal::escape_go_interpreted_string(&super::rewrite_pg_placeholders(
&super::clean_sql_oneline_with_optional(&analyzed.sql, &analyzed.optional_params, &analyzed.params),
|n| format!(":{n}"),
));
let param_list = params
.iter()
.map(|p| format!("{} {}", p.field_name, p.full_type))
.collect::<Vec<_>>()
.join(", ");
let sep = if param_list.is_empty() { "" } else { ", " };
let args_str = if params.is_empty() {
String::new()
} else {
let args: Vec<String> = params.iter().map(|p| p.field_name.clone()).collect();
format!(", {}", args.join(", "))
};
let key_go_type = parent_columns
.iter()
.find(|c| c.name == key_column || c.field_name == key_column)
.map(|c| c.full_type.as_str())
.unwrap_or("int64");
let mut out = String::new();
let _ = writeln!(
out,
"func {}(ctx context.Context, db *sql.DB{}{}) ([]{}, error) {{",
func_name, sep, param_list, parent_struct_name
);
let _ = writeln!(out, "\trows, err := db.QueryContext(ctx, \"{}\"{})", sql, args_str);
let _ = writeln!(out, "\tif err != nil {{");
let _ = writeln!(out, "\t\treturn nil, err");
let _ = writeln!(out, "\t}}");
let _ = writeln!(out, "\tdefer rows.Close()");
let _ = writeln!(out, "\tvar result []{}", parent_struct_name);
let _ = writeln!(out, "\tindex := make(map[{}]*{})", key_go_type, parent_struct_name);
let _ = writeln!(out, "\tfor rows.Next() {{");
for col in all_columns {
let _ = writeln!(out, "\t\tvar {} {}", col.field_name, col.full_type);
}
let scan_refs: Vec<String> = all_columns.iter().map(|c| format!("&{}", c.field_name)).collect();
let _ = writeln!(out, "\t\tif err := rows.Scan({}); err != nil {{", scan_refs.join(", "));
let _ = writeln!(out, "\t\t\treturn nil, err");
let _ = writeln!(out, "\t\t}}");
let _ = writeln!(out, "\t\tchild := {} {{", child_struct_name);
for col in child_columns {
let _ = writeln!(out, "\t\t\t{}: {},", to_pascal_case(&col.field_name), col.field_name);
}
let _ = writeln!(out, "\t\t}}");
let _ = writeln!(out, "\t\tif parent, ok := index[{}]; ok {{", key_column);
let _ = writeln!(out, "\t\t\tparent.Children = append(parent.Children, child)");
let _ = writeln!(out, "\t\t}} else {{");
let _ = writeln!(out, "\t\t\tresult = append(result, {} {{", parent_struct_name);
for col in parent_columns {
let _ = writeln!(out, "\t\t\t\t{}: {},", to_pascal_case(&col.field_name), col.field_name);
}
let _ = writeln!(out, "\t\t\t\tChildren: []{}{{child}},", child_struct_name);
let _ = writeln!(out, "\t\t\t}})");
let _ = writeln!(out, "\t\t\tindex[{}] = &result[len(result)-1]", key_column);
let _ = writeln!(out, "\t\t}}");
let _ = writeln!(out, "\t}}");
let _ = writeln!(out, "\treturn result, rows.Err()");
let _ = write!(out, "}}");
Ok(out)
}
fn generate_enum_def(&self, enum_info: &EnumInfo) -> Result<String, ScytheError> {
let type_name = enum_type_name(&enum_info.sql_name, &self.manifest.naming);
let mut out = String::new();
let _ = writeln!(out, "type {} string", type_name);
let _ = writeln!(out);
let _ = writeln!(out, "const (");
for value in &enum_info.values {
let variant = enum_variant_name(value, &self.manifest.naming);
let _ = writeln!(out, "\t{}{} {} = \"{}\"", type_name, variant, type_name, value);
}
let _ = write!(out, ")");
Ok(out)
}
fn generate_composite_def(&self, composite: &CompositeInfo) -> Result<String, ScytheError> {
let name = composite_type_name(&composite.sql_name, &self.manifest.naming);
let mut out = String::new();
let _ = writeln!(out, "type {} struct {{", name);
for field in &composite.fields {
let go_type = resolve_type(&field.neutral_type, &self.manifest, field.nullable)
.map(|t| t.into_owned())
.unwrap_or_else(|_| "any".to_string());
let _ = writeln!(out, "\t{} {}", to_pascal_case(&field.name), go_type);
}
let _ = write!(out, "}}");
Ok(out)
}
}
#[cfg(test)]
mod tests {
use scythe_core::analyzer::{AnalyzedColumn, AnalyzedQuery, GroupByConfig};
use scythe_core::parser::QueryCommand;
use crate::backends::get_backend;
use crate::generate_with_backend;
fn make_grouped_query() -> AnalyzedQuery {
let parent_cols = vec![
AnalyzedColumn {
name: "id".to_string(),
neutral_type: "int32".to_string(),
nullable: false,
..Default::default()
},
AnalyzedColumn {
name: "name".to_string(),
neutral_type: "string".to_string(),
nullable: false,
..Default::default()
},
AnalyzedColumn {
name: "email".to_string(),
neutral_type: "string".to_string(),
nullable: false,
..Default::default()
},
];
let child_cols = vec![
AnalyzedColumn {
name: "order_id".to_string(),
neutral_type: "int32".to_string(),
nullable: false,
..Default::default()
},
AnalyzedColumn {
name: "total".to_string(),
neutral_type: "decimal".to_string(),
nullable: true,
..Default::default()
},
AnalyzedColumn {
name: "order_date".to_string(),
neutral_type: "datetime".to_string(),
nullable: false,
..Default::default()
},
];
let all_cols = [parent_cols.clone(), child_cols.clone()].concat();
AnalyzedQuery::build(|aq| {
aq.name = "GetUsersWithOrders".to_string();
aq.command = QueryCommand::Grouped;
aq.sql = "-- @name GetUsersWithOrders\n-- @returns :grouped\n-- @group_by users.id\n\
SELECT u.id, u.name, u.email, o.id AS order_id, o.total, o.created_at AS order_date\n\
FROM users u\n\
JOIN orders o ON o.user_id = u.id"
.to_string();
aq.columns = all_cols;
aq.params = vec![];
aq.deprecated = None;
aq.source_table = None;
aq.composites = vec![];
aq.enums = vec![];
aq.optional_params = vec![];
aq.group_by = Some(GroupByConfig {
table: "users".to_string(),
key_column: "id".to_string(),
parent_columns: parent_cols,
child_columns: child_cols,
});
aq.custom = vec![];
})
}
fn make_returning_query() -> AnalyzedQuery {
let columns = vec![
AnalyzedColumn {
name: "id".to_string(),
neutral_type: "int64".to_string(),
nullable: false,
..Default::default()
},
AnalyzedColumn {
name: "name".to_string(),
neutral_type: "string".to_string(),
nullable: false,
..Default::default()
},
AnalyzedColumn {
name: "email".to_string(),
neutral_type: "string".to_string(),
nullable: true,
..Default::default()
},
AnalyzedColumn {
name: "score".to_string(),
neutral_type: "int32".to_string(),
nullable: true,
..Default::default()
},
AnalyzedColumn {
name: "balance".to_string(),
neutral_type: "decimal".to_string(),
nullable: true,
..Default::default()
},
AnalyzedColumn {
name: "updated_at".to_string(),
neutral_type: "datetime".to_string(),
nullable: true,
..Default::default()
},
AnalyzedColumn {
name: "created_at".to_string(),
neutral_type: "datetime".to_string(),
nullable: false,
..Default::default()
},
];
AnalyzedQuery::build(|aq| {
aq.name = "CreateWidget".to_string();
aq.command = QueryCommand::One;
aq.sql = "-- @name CreateWidget\n-- @returns :one\nINSERT INTO widgets (name) VALUES (:1) \
RETURNING id, name, email, score, balance, updated_at, created_at"
.to_string();
aq.columns = columns;
aq.params = vec![];
})
}
#[test]
fn test_go_godror_returning_nullable_out_params_use_null_types_and_pointers() {
let backend = get_backend("go-godror", "oracle").unwrap();
let query = make_returning_query();
let result = generate_with_backend(&query, &*backend).unwrap();
let query_fn = result.query_fn.as_deref().unwrap();
assert!(
query_fn.contains("var outEmail string"),
"nullable string OUT var must stay plain string; got:\n{query_fn}"
);
assert!(
query_fn.contains("Email: &outEmail"),
"nullable string field must be assigned via address-of, not a bare value; got:\n{query_fn}"
);
assert!(
!query_fn.contains("Email: outEmail}") && !query_fn.contains("Email: outEmail,"),
"nullable string field must never be assigned the bare (non-pointer) OUT var; got:\n{query_fn}"
);
assert!(
query_fn.contains("var outScore sql.NullInt32"),
"nullable int32 OUT var must use sql.NullInt32; got:\n{query_fn}"
);
assert!(
query_fn.contains("var outScorePtr *int32"),
"must derive a *int32 for the row struct; got:\n{query_fn}"
);
assert!(
query_fn.contains("if outScore.Valid {") && query_fn.contains("outScorePtr = &outScore.Int32"),
"must only take the address when Valid; got:\n{query_fn}"
);
assert!(
query_fn.contains("Score: outScorePtr"),
"row struct must use the derived pointer; got:\n{query_fn}"
);
assert!(
query_fn.contains("var outBalance sql.NullFloat64"),
"nullable decimal OUT var must use sql.NullFloat64; got:\n{query_fn}"
);
assert!(
query_fn.contains("if outBalance.Valid {") && query_fn.contains("outBalancePtr = &outBalance.Float64"),
"must only take the address when Valid; got:\n{query_fn}"
);
assert!(
query_fn.contains("var outUpdatedAt sql.NullTime"),
"nullable datetime OUT var must use sql.NullTime; got:\n{query_fn}"
);
assert!(
query_fn.contains("if outUpdatedAt.Valid {") && query_fn.contains("outUpdatedAtPtr = &outUpdatedAt.Time"),
"must only take the address when Valid; got:\n{query_fn}"
);
}
#[test]
fn test_go_godror_returning_non_nullable_out_params_assign_directly() {
let backend = get_backend("go-godror", "oracle").unwrap();
let query = make_returning_query();
let result = generate_with_backend(&query, &*backend).unwrap();
let query_fn = result.query_fn.as_deref().unwrap();
assert!(
query_fn.contains("var outId int64"),
"non-nullable int64 OUT var must be a plain int64; got:\n{query_fn}"
);
assert!(
query_fn.contains("var outCreatedAt time.Time"),
"non-nullable datetime OUT var must be a plain time.Time; got:\n{query_fn}"
);
assert!(
query_fn.contains("Id: outId"),
"non-nullable field must be assigned the bare OUT var directly; got:\n{query_fn}"
);
assert!(
query_fn.contains("CreatedAt: outCreatedAt"),
"non-nullable field must be assigned the bare OUT var directly; got:\n{query_fn}"
);
assert!(
!query_fn.contains("outIdPtr") && !query_fn.contains("outCreatedAtPtr"),
"non-nullable columns must not generate pointer conversion machinery; got:\n{query_fn}"
);
}
#[test]
fn test_grouped_go_godror_structs() {
let backend = get_backend("go-godror", "oracle").unwrap();
let query = make_grouped_query();
let result = generate_with_backend(&query, &*backend).unwrap();
let row_struct = result.row_struct.as_deref().unwrap();
assert!(
row_struct.contains("type GetUsersWithOrdersChildRow struct"),
"missing child struct; got:\n{row_struct}"
);
assert!(
row_struct.contains("OrderId"),
"child struct missing OrderId field; got:\n{row_struct}"
);
assert!(
row_struct.contains("type GetUsersWithOrdersRow struct"),
"missing parent struct; got:\n{row_struct}"
);
assert!(
row_struct.contains("Children []GetUsersWithOrdersChildRow"),
"parent struct missing Children field; got:\n{row_struct}"
);
let child_pos = row_struct.find("GetUsersWithOrdersChildRow").unwrap();
let parent_pos = row_struct.find("type GetUsersWithOrdersRow struct").unwrap();
assert!(
child_pos < parent_pos,
"child struct must be defined before parent struct"
);
assert!(result.model_struct.is_none(), "grouped must not produce a model_struct");
}
#[test]
fn test_grouped_go_godror_query_fn() {
let backend = get_backend("go-godror", "oracle").unwrap();
let query = make_grouped_query();
let result = generate_with_backend(&query, &*backend).unwrap();
let query_fn = result.query_fn.as_deref().unwrap();
assert!(
query_fn.contains("func GetUsersWithOrders("),
"missing function; got:\n{query_fn}"
);
assert!(
query_fn.contains("[]GetUsersWithOrdersRow, error)"),
"wrong return type; got:\n{query_fn}"
);
assert!(
query_fn.contains("db.QueryContext(ctx,"),
"must use db.QueryContext; got:\n{query_fn}"
);
assert!(
query_fn.contains("index := make(map["),
"must declare index map; got:\n{query_fn}"
);
assert!(
query_fn.contains("parent.Children = append(parent.Children, child)"),
"must fold child into existing parent; got:\n{query_fn}"
);
assert!(
query_fn.contains("Children: []GetUsersWithOrdersChildRow{child}"),
"must initialize Children slice; got:\n{query_fn}"
);
assert!(
query_fn.contains("rows.Err()"),
"must return rows.Err(); got:\n{query_fn}"
);
}
fn make_simple_query(name: &str, sql: &str, columns: Vec<AnalyzedColumn>) -> AnalyzedQuery {
AnalyzedQuery::build(|aq| {
aq.name = name.to_string();
aq.command = QueryCommand::Many;
aq.sql = sql.to_string();
aq.columns = columns;
aq.params = vec![];
})
}
#[test]
fn test_go_godror_file_header_omits_time_for_int_and_string_only_schema() {
let backend = get_backend("go-godror", "oracle").unwrap();
let query = make_simple_query(
"ListWidgets",
"-- @name ListWidgets\n-- @returns :many\nSELECT id, name FROM widgets",
vec![
AnalyzedColumn {
name: "id".to_string(),
neutral_type: "int32".to_string(),
nullable: false,
..Default::default()
},
AnalyzedColumn {
name: "name".to_string(),
neutral_type: "string".to_string(),
nullable: false,
..Default::default()
},
],
);
let code = generate_with_backend(&query, &*backend).unwrap();
let header = backend.file_header_for_results(&[code]);
assert!(
!header.contains("\"time\""),
"int/string-only schema must not import \"time\"; got:\n{header}"
);
assert!(header.contains("\"context\""), "got:\n{header}");
assert!(header.contains("\"database/sql\""), "got:\n{header}");
}
#[test]
fn test_go_godror_file_header_includes_time_when_columns_need_it() {
let backend = get_backend("go-godror", "oracle").unwrap();
let query = make_simple_query(
"ListOrders",
"-- @name ListOrders\n-- @returns :many\nSELECT id, created_at FROM orders",
vec![
AnalyzedColumn {
name: "id".to_string(),
neutral_type: "int32".to_string(),
nullable: false,
..Default::default()
},
AnalyzedColumn {
name: "created_at".to_string(),
neutral_type: "datetime".to_string(),
nullable: false,
..Default::default()
},
],
);
let code = generate_with_backend(&query, &*backend).unwrap();
let header = backend.file_header_for_results(&[code]);
assert!(header.contains("\"time\""), "got:\n{header}");
}
}