use std::collections::HashMap;
use scythe_codegen::{CodegenBackend, GeneratedCode, generate_with_backend, get_backend};
use scythe_core::analyzer::analyze;
use scythe_core::catalog::Catalog;
use scythe_core::dialect::SqlDialect;
use scythe_core::parser::parse_query_with_dialect;
const SCHEMA: &str = "CREATE TABLE blobs (id INTEGER PRIMARY KEY, payload BLOB NOT NULL, maybe_payload BLOB);";
const QUERY_ONE: &str =
"-- @name GetBlob\n-- @returns :one\nSELECT id, payload, maybe_payload FROM blobs WHERE id = $1;";
const QUERY_MANY: &str = "-- @name ListBlobs\n-- @returns :many\nSELECT id, payload, maybe_payload FROM blobs;";
fn generate(query: &str, options: &HashMap<String, String>) -> (GeneratedCode, Box<dyn CodegenBackend>) {
let mut backend = get_backend("typescript-duckdb", "duckdb").expect("typescript-duckdb must support duckdb");
if !options.is_empty() {
backend.apply_options(options).expect("options must apply");
}
let catalog = Catalog::from_ddl_with_dialect(&[SCHEMA], &SqlDialect::PostgreSQL).expect("schema must parse");
let parsed = parse_query_with_dialect(query, &SqlDialect::PostgreSQL).expect("query must parse");
let analyzed = analyze(&catalog, &parsed).expect("query must analyze");
let code = generate_with_backend(&analyzed, &*backend).expect("codegen must succeed");
(code, backend)
}
#[test]
fn duckdb_blob_column_row_struct_declares_duckdb_blob_value_not_uint8array() {
let (code, _backend) = generate(QUERY_ONE, &HashMap::new());
let row_struct = code.row_struct.expect("expected a row struct");
assert!(
row_struct.contains("payload: DuckDBBlobValue;"),
"the non-null BLOB column must be declared DuckDBBlobValue; got:\n{row_struct}"
);
assert!(
row_struct.contains("maybe_payload: DuckDBBlobValue | null;"),
"the nullable BLOB column must keep its null guard around the corrected type; got:\n{row_struct}"
);
assert!(
!row_struct.contains("Uint8Array"),
"the row struct must not declare a BLOB column Uint8Array -- that is not what \
@duckdb/node-api returns for one; got:\n{row_struct}"
);
}
#[test]
fn duckdb_blob_column_one_query_fn_uses_the_blind_cast_with_the_corrected_row_type() {
let (code, _backend) = generate(QUERY_ONE, &HashMap::new());
let query_fn = code.query_fn.expect("expected a query function");
assert!(
query_fn.contains("firstRow<GetBlobRow>(rows)"),
"default field_case must keep the blind cast onto the row interface; got:\n{query_fn}"
);
assert!(!query_fn.contains("Uint8Array"), "got:\n{query_fn}");
}
#[test]
fn duckdb_blob_column_one_query_fn_casts_to_duckdb_blob_value_under_camel_case() {
let (code, _backend) = generate(
QUERY_ONE,
&HashMap::from([("field_case".to_string(), "camelCase".to_string())]),
);
let query_fn = code.query_fn.expect("expected a query function");
assert!(
query_fn.contains("payload: row['payload'] as DuckDBBlobValue,"),
"the non-null BLOB column must be cast to DuckDBBlobValue; got:\n{query_fn}"
);
assert!(
query_fn.contains("maybePayload: row['maybe_payload'] as DuckDBBlobValue | null,"),
"the nullable BLOB column must be cast to DuckDBBlobValue | null; got:\n{query_fn}"
);
assert!(!query_fn.contains("Uint8Array"), "got:\n{query_fn}");
}
#[test]
fn duckdb_blob_column_many_query_fn_casts_to_duckdb_blob_value_under_camel_case() {
let (code, _backend) = generate(
QUERY_MANY,
&HashMap::from([("field_case".to_string(), "camelCase".to_string())]),
);
let query_fn = code.query_fn.expect("expected a query function");
assert!(
query_fn.contains("payload: row['payload'] as DuckDBBlobValue,"),
"got:\n{query_fn}"
);
assert!(!query_fn.contains("Uint8Array"), "got:\n{query_fn}");
}
#[test]
fn duckdb_blob_value_import_is_present_only_when_a_blob_column_is_read() {
let (blob_code, backend) = generate(QUERY_ONE, &HashMap::new());
let header_with_blob = backend.file_header_for_results(&[blob_code]);
assert!(
header_with_blob
.contains("import type { DuckDBConnection, DuckDBValue, DuckDBBlobValue } from \"@duckdb/node-api\";"),
"got:\n{header_with_blob}"
);
let (no_blob_code, backend) = generate(
"-- @name CountBlobs\n-- @returns :one\nSELECT id FROM blobs WHERE id = $1;",
&HashMap::new(),
);
let header_without_blob = backend.file_header_for_results(&[no_blob_code]);
assert!(
!header_without_blob.contains("DuckDBBlobValue"),
"a file with no BLOB column must not import DuckDBBlobValue; got:\n{header_without_blob}"
);
assert!(
header_without_blob.contains("import type { DuckDBConnection, DuckDBValue } from \"@duckdb/node-api\";"),
"got:\n{header_without_blob}"
);
}