set up to_pandas logic and rerun for failed task
This commit is contained in:
42
src/lib.rs
42
src/lib.rs
@@ -9,9 +9,7 @@ use tokio;
|
|||||||
use pyo3::types::{PyModule, PyAny};
|
use pyo3::types::{PyModule, PyAny};
|
||||||
use pyo3::Bound;
|
use pyo3::Bound;
|
||||||
use pyo3_arrow::PyRecordBatch;
|
use pyo3_arrow::PyRecordBatch;
|
||||||
|
// Required for global allocator (mimalloc)
|
||||||
// Use the high-performance mimalloc for better multi-threaded memory allocation.
|
|
||||||
// We conditionally compile it to avoid issues on MSVC targets.
|
|
||||||
#[cfg(not(target_env = "msvc"))]
|
#[cfg(not(target_env = "msvc"))]
|
||||||
use mimalloc;
|
use mimalloc;
|
||||||
|
|
||||||
@@ -19,12 +17,14 @@ use mimalloc;
|
|||||||
#[global_allocator]
|
#[global_allocator]
|
||||||
static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
|
static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
|
||||||
|
|
||||||
|
|
||||||
// --- Internal Crates ---
|
// --- Internal Crates ---
|
||||||
use crate::config::{load_and_validate_config, ConnectorConfig};
|
use crate::config::{load_and_validate_config, ConnectorConfig};
|
||||||
use crate::parser::run_db_logic;
|
// NOTE: run_profiler_logic added, requires definition in parser.rs
|
||||||
|
use crate::parser::{run_db_logic, run_profiler_logic};
|
||||||
|
|
||||||
|
|
||||||
// --- THE PYTHON-CALLABLE ENTRY POINT ---
|
// --- THE PYTHON-CALLABLE ENTRY POINT (Load Data) ---
|
||||||
|
|
||||||
#[pyfunction]
|
#[pyfunction]
|
||||||
#[pyo3(signature = (config_path, blast_radius=312500))]
|
#[pyo3(signature = (config_path, blast_radius=312500))]
|
||||||
@@ -37,7 +37,6 @@ fn load_data_from_config<'py>(
|
|||||||
blast_radius: i64,
|
blast_radius: i64,
|
||||||
) -> PyResult<Bound<'py, PyAny>> {
|
) -> PyResult<Bound<'py, PyAny>> {
|
||||||
|
|
||||||
// --- Phase 1: Load and Validate Configuration ---
|
|
||||||
let config: ConnectorConfig = match load_and_validate_config(&config_path) {
|
let config: ConnectorConfig = match load_and_validate_config(&config_path) {
|
||||||
Ok(c) => c,
|
Ok(c) => c,
|
||||||
Err(e) => return Err(PyValueError::new_err(format!("Configuration Error: {:?}", e))),
|
Err(e) => return Err(PyValueError::new_err(format!("Configuration Error: {:?}", e))),
|
||||||
@@ -47,7 +46,6 @@ fn load_data_from_config<'py>(
|
|||||||
println!("Database: {}", config.connection_string);
|
println!("Database: {}", config.connection_string);
|
||||||
println!("Columns (in order): {:?}", config.schema.iter().map(|c| &c.column_name).collect::<Vec<_>>());
|
println!("Columns (in order): {:?}", config.schema.iter().map(|c| &c.column_name).collect::<Vec<_>>());
|
||||||
|
|
||||||
// --- Phase 2: Run Core Logic ---
|
|
||||||
let record_batch = py.allow_threads(|| {
|
let record_batch = py.allow_threads(|| {
|
||||||
tokio::runtime::Builder::new_multi_thread()
|
tokio::runtime::Builder::new_multi_thread()
|
||||||
.enable_all()
|
.enable_all()
|
||||||
@@ -58,16 +56,42 @@ fn load_data_from_config<'py>(
|
|||||||
})
|
})
|
||||||
}).map_err(|e| PyValueError::new_err(format!("Database/Runtime Error: {:?}", e)))?;
|
}).map_err(|e| PyValueError::new_err(format!("Database/Runtime Error: {:?}", e)))?;
|
||||||
|
|
||||||
// --- Phase 3: Return Data to Python ---
|
|
||||||
let py_record_batch = PyRecordBatch::new(record_batch);
|
let py_record_batch = PyRecordBatch::new(record_batch);
|
||||||
py_record_batch.into_pyarrow(py)
|
py_record_batch.into_pyarrow(py)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- NEW PYTHON-CALLABLE FUNCTION FOR PANDAS CONVERSION (FIXED SIGNATURE) ---
|
||||||
|
#[pyfunction]
|
||||||
|
#[pyo3(signature = (arrow_table))] // FIX: Removed 'py' from the signature macro
|
||||||
|
fn to_pandas_dataframe<'py>(py: Python<'py>, arrow_table: Bound<'py, PyAny>) -> PyResult<Bound<'py, PyAny>> {
|
||||||
|
// This calls the 'to_pandas' method on the PyArrow object.
|
||||||
|
arrow_table.call_method0("to_pandas")
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- NEW PYTHON-CALLABLE ENTRY POINT FOR SCHEMA PROFILING ---
|
||||||
|
#[pyfunction]
|
||||||
|
#[pyo3(signature = (config_path))]
|
||||||
|
fn profile_data(config_path: String) -> PyResult<String> {
|
||||||
|
// Note: The profiler uses a small, current_thread tokio runtime since it's sequential I/O.
|
||||||
|
let output = std::thread::spawn(move || {
|
||||||
|
tokio::runtime::Builder::new_current_thread()
|
||||||
|
.enable_all()
|
||||||
|
.build()
|
||||||
|
.unwrap()
|
||||||
|
.block_on(async {
|
||||||
|
run_profiler_logic(&config_path).await
|
||||||
|
})
|
||||||
|
}).join().unwrap().map_err(|e| PyValueError::new_err(format!("Profiling Error: {:?}", e)))?;
|
||||||
|
|
||||||
|
Ok(output)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// --- PYTHON MODULE EXPORT ---
|
// --- PYTHON MODULE EXPORT ---
|
||||||
|
|
||||||
#[pymodule]
|
#[pymodule]
|
||||||
fn unchecked_io(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
|
fn unchecked_io(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||||
m.add_function(wrap_pyfunction!(load_data_from_config, m)?)?;
|
m.add_function(wrap_pyfunction!(load_data_from_config, m)?)?;
|
||||||
|
m.add_function(wrap_pyfunction!(to_pandas_dataframe, m)?)?;
|
||||||
|
m.add_function(wrap_pyfunction!(profile_data, m)?)?; // NEW: Schema profiler
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
133
src/parser.rs
133
src/parser.rs
@@ -1,8 +1,8 @@
|
|||||||
// --- External Crates ---
|
// --- External Crates ---
|
||||||
use std::pin::Pin;
|
use std::pin::Pin;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
// FIX: Replace direct tokio_postgres connections with deadpool
|
|
||||||
use tokio_postgres::{NoTls, CopyOutStream, Config as PgConfig};
|
use tokio_postgres::{NoTls, CopyOutStream, Config as PgConfig};
|
||||||
|
// Required for the connection pool
|
||||||
use deadpool_postgres::{Pool, Manager, Runtime};
|
use deadpool_postgres::{Pool, Manager, Runtime};
|
||||||
use anyhow::{Context, Result, anyhow};
|
use anyhow::{Context, Result, anyhow};
|
||||||
use arrow::array::{
|
use arrow::array::{
|
||||||
@@ -19,14 +19,14 @@ use bytes::{Bytes, BytesMut, Buf};
|
|||||||
use byteorder::{BigEndian, ReadBytesExt};
|
use byteorder::{BigEndian, ReadBytesExt};
|
||||||
use std::io::{Cursor, Read};
|
use std::io::{Cursor, Read};
|
||||||
use std::str;
|
use std::str;
|
||||||
use chrono::{NaiveDateTime, NaiveDate};
|
|
||||||
use std::mem;
|
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
use chrono::{NaiveDateTime, NaiveDate};
|
||||||
|
use std::time::Instant;
|
||||||
use tokio::task::JoinSet;
|
use tokio::task::JoinSet;
|
||||||
use arrow::compute::concat_batches;
|
use arrow::compute::concat_batches;
|
||||||
|
|
||||||
// --- Internal Crates ---
|
// --- Internal Crates ---
|
||||||
use crate::config::ConnectorConfig;
|
use crate::config::{ConnectorConfig, load_and_validate_config};
|
||||||
|
|
||||||
|
|
||||||
// --- CONSTANTS ---
|
// --- CONSTANTS ---
|
||||||
@@ -36,25 +36,26 @@ const POSTGRES_EPOCH_MICROS_OFFSET: i64 = 946684800000000;
|
|||||||
|
|
||||||
|
|
||||||
// --- 1. CORE DATABASE LOGIC (PARALLEL COORDINATOR) ---
|
// --- 1. CORE DATABASE LOGIC (PARALLEL COORDINATOR) ---
|
||||||
// Note: We use blast_radius from Python config as the partitioning strategy
|
// This is the fully optimized function using Connection Pooling and Static Dispatch.
|
||||||
pub async fn run_db_logic(config: ConnectorConfig, blast_radius: i64) -> Result<RecordBatch> {
|
pub async fn run_db_logic(config: ConnectorConfig, blast_radius: i64) -> Result<RecordBatch> {
|
||||||
|
|
||||||
|
// Start overall timer
|
||||||
|
let start_total = Instant::now();
|
||||||
|
let start_phase1 = Instant::now();
|
||||||
|
|
||||||
println!("UncheckedIO: Starting Query Planner (Blast Radius: {})...", blast_radius);
|
println!("UncheckedIO: Starting Query Planner (Blast Radius: {})...", blast_radius);
|
||||||
|
|
||||||
// --- PHASE 1: SETUP CONNECTION POOL ---
|
// --- PHASE 1: SETUP CONNECTION POOL & STATS ---
|
||||||
let pg_config: tokio_postgres::Config = PgConfig::from_str(&config.connection_string)
|
let pg_config: tokio_postgres::Config = PgConfig::from_str(&config.connection_string)
|
||||||
.context("Invalid connection string in config")?;
|
.context("Invalid connection string in config")?;
|
||||||
|
|
||||||
// Initialize the Manager and Pool
|
|
||||||
let manager = Manager::new(pg_config.clone(), NoTls);
|
let manager = Manager::new(pg_config.clone(), NoTls);
|
||||||
// Set pool size higher than the expected partition count to ensure connections are always available.
|
|
||||||
let pool = Pool::builder(manager)
|
let pool = Pool::builder(manager)
|
||||||
.max_size(20)
|
.max_size(20)
|
||||||
.runtime(Runtime::Tokio1)
|
.runtime(Runtime::Tokio1)
|
||||||
.build()
|
.build()
|
||||||
.context("Failed to build connection pool")?;
|
.context("Failed to build connection pool")?;
|
||||||
|
|
||||||
// 2. Query for Table Bounds (using pool connection)
|
|
||||||
let client = pool.get().await.context("Failed to get pool connection for stats query")?;
|
let client = pool.get().await.context("Failed to get pool connection for stats query")?;
|
||||||
let partition_key = "id";
|
let partition_key = "id";
|
||||||
|
|
||||||
@@ -62,16 +63,14 @@ pub async fn run_db_logic(config: ConnectorConfig, blast_radius: i64) -> Result<
|
|||||||
.context("Failed to parse base query from config")?;
|
.context("Failed to parse base query from config")?;
|
||||||
let base_query_inner = base_query.trim().trim_start_matches("COPY (").trim_end_matches(")");
|
let base_query_inner = base_query.trim().trim_start_matches("COPY (").trim_end_matches(")");
|
||||||
|
|
||||||
// NOTE: We rely on MIN/MAX here, assuming dense key for benchmark data.
|
|
||||||
let stats_query = format!("SELECT MIN({}), MAX({}) FROM ({}) AS subquery", partition_key, partition_key, base_query_inner);
|
let stats_query = format!("SELECT MIN({}), MAX({}) FROM ({}) AS subquery", partition_key, partition_key, base_query_inner);
|
||||||
|
|
||||||
let row = client.query_one(&stats_query, &[]).await?;
|
let row = client.query_one(&stats_query, &[]).await?;
|
||||||
let min_id: i64 = row.try_get(0).context("Failed to get MIN(id)")?;
|
let min_id: i64 = row.try_get(0).context("Failed to get MIN(id)")?;
|
||||||
let max_id: i64 = row.try_get(1).context("Failed to get MAX(id)")?;
|
let max_id: i64 = row.try_get(1).context("Failed to get MAX(id)")?;
|
||||||
// Connection is returned to the pool when 'client' is dropped here.
|
|
||||||
println!("UncheckedIO: ID Range: {} to {}", min_id, max_id);
|
println!("UncheckedIO: ID Range: {} to {}", min_id, max_id);
|
||||||
|
|
||||||
// 4. Generate Partitioned Queries (Based on blast_radius from Python)
|
|
||||||
struct PartitionTask {
|
struct PartitionTask {
|
||||||
index: usize,
|
index: usize,
|
||||||
query: String,
|
query: String,
|
||||||
@@ -95,25 +94,27 @@ pub async fn run_db_logic(config: ConnectorConfig, blast_radius: i64) -> Result<
|
|||||||
idx += 1;
|
idx += 1;
|
||||||
}
|
}
|
||||||
println!("UncheckedIO: Generated {} parallel partitions.", partitions.len());
|
println!("UncheckedIO: Generated {} parallel partitions.", partitions.len());
|
||||||
|
let duration_phase1 = start_phase1.elapsed();
|
||||||
|
|
||||||
|
|
||||||
|
// --- PHASE 2: PARALLEL EXECUTION (DATA TRANSFER + PARSING) ---
|
||||||
|
let start_phase2 = Instant::now();
|
||||||
|
|
||||||
// --- Phase 2: Parallel Execution ---
|
|
||||||
let arrow_schema = Arc::new(build_arrow_schema(&config)?);
|
let arrow_schema = Arc::new(build_arrow_schema(&config)?);
|
||||||
let mut join_set = JoinSet::new();
|
let mut join_set = JoinSet::new();
|
||||||
|
|
||||||
for task in partitions {
|
for task in partitions {
|
||||||
let worker_pool = pool.clone(); // Pass the pool handle
|
let worker_pool = pool.clone();
|
||||||
let worker_schema = arrow_schema.clone();
|
let worker_schema = arrow_schema.clone();
|
||||||
|
|
||||||
join_set.spawn(async move {
|
join_set.spawn(async move {
|
||||||
let worker_logic = async {
|
let worker_logic = async {
|
||||||
// Get connection from pool (This is the speedup)
|
|
||||||
let worker_client = worker_pool.get().await
|
let worker_client = worker_pool.get().await
|
||||||
.context("Worker: Failed to get pool connection")?;
|
.context("Worker: Failed to get pool connection")?;
|
||||||
|
|
||||||
let copy_stream = worker_client.copy_out(task.query.as_str()).await?;
|
let copy_stream = worker_client.copy_out(task.query.as_str()).await?;
|
||||||
let pinned_stream: Pin<Box<CopyOutStream>> = Box::pin(copy_stream);
|
let pinned_stream: Pin<Box<CopyOutStream>> = Box::pin(copy_stream);
|
||||||
|
|
||||||
// Call the static dispatch parser
|
|
||||||
parse_data_with_schema(pinned_stream, worker_schema).await
|
parse_data_with_schema(pinned_stream, worker_schema).await
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -122,7 +123,7 @@ pub async fn run_db_logic(config: ConnectorConfig, blast_radius: i64) -> Result<
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Phase 3: Collect, Order, and Stitch ---
|
// Collect results
|
||||||
let mut results: Vec<Option<RecordBatch>> = vec![None; idx];
|
let mut results: Vec<Option<RecordBatch>> = vec![None; idx];
|
||||||
|
|
||||||
while let Some(join_result) = join_set.join_next().await {
|
while let Some(join_result) = join_set.join_next().await {
|
||||||
@@ -133,25 +134,48 @@ pub async fn run_db_logic(config: ConnectorConfig, blast_radius: i64) -> Result<
|
|||||||
results[index] = Some(batch.1);
|
results[index] = Some(batch.1);
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
// --- Self-Healing Placeholder ---
|
|
||||||
eprintln!("UncheckedIO: Partition {} failed! Error: {}. Falling back to NULLs (Self-Healing logic required here).", index, e);
|
eprintln!("UncheckedIO: Partition {} failed! Error: {}. Falling back to NULLs (Self-Healing logic required here).", index, e);
|
||||||
let null_batch = create_null_batch(arrow_schema.clone(), expected_rows)?;
|
let null_batch = create_null_batch(arrow_schema.clone(), expected_rows)?;
|
||||||
results[index] = Some(null_batch);
|
results[index] = Some(null_batch);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
let duration_phase2 = start_phase2.elapsed();
|
||||||
|
|
||||||
|
|
||||||
|
// --- PHASE 3: CONCATENATION AND FINALIZATION ---
|
||||||
|
let start_phase3 = Instant::now();
|
||||||
|
|
||||||
let batches: Vec<RecordBatch> = results.into_iter()
|
let batches: Vec<RecordBatch> = results.into_iter()
|
||||||
.filter_map(|b| b)
|
.filter_map(|b| b)
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
if batches.is_empty() {
|
if batches.is_empty() {
|
||||||
|
println!("UncheckedIO: All workers returned empty batches.");
|
||||||
|
let duration_total = start_total.elapsed();
|
||||||
|
println!("--- UncheckedIO Internal Timing ---");
|
||||||
|
println!("Phase 1 (Setup, Query): {:.2?}", duration_phase1);
|
||||||
|
println!("Phase 2 (I/O, Parsing): {:.2?}", duration_phase2);
|
||||||
|
println!("Phase 3 (Concatenation): {:.2?}", start_phase3.elapsed());
|
||||||
|
println!("Total Wall Time: {:.2?}", duration_total);
|
||||||
return Ok(RecordBatch::new_empty(arrow_schema));
|
return Ok(RecordBatch::new_empty(arrow_schema));
|
||||||
}
|
}
|
||||||
|
|
||||||
let final_batch = concat_batches(&arrow_schema, &batches)
|
let final_batch = concat_batches(&arrow_schema, &batches)
|
||||||
.context("Failed to concatenate parallel batches")?;
|
.context("Failed to concatenate parallel batches")?;
|
||||||
|
|
||||||
|
let duration_phase3 = start_phase3.elapsed();
|
||||||
|
let duration_total = start_total.elapsed();
|
||||||
|
|
||||||
|
|
||||||
|
// --- FINAL REPORTING ---
|
||||||
|
println!("--- UncheckedIO Internal Timing ---");
|
||||||
|
println!("Phase 1 (Setup, Query): {:.2?}", duration_phase1);
|
||||||
|
println!("Phase 2 (I/O, Parsing): {:.2?}", duration_phase2);
|
||||||
|
println!("Phase 3 (Concatenation): {:.2?}", duration_phase3);
|
||||||
|
println!("Total Wall Time: {:.2?}", duration_total);
|
||||||
|
|
||||||
|
|
||||||
Ok(final_batch)
|
Ok(final_batch)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,7 +189,7 @@ fn create_null_batch(schema: Arc<Schema>, num_rows: usize) -> Result<RecordBatch
|
|||||||
/// Helper function to build the Arrow Schema from the config
|
/// Helper function to build the Arrow Schema from the config
|
||||||
fn build_arrow_schema(config: &ConnectorConfig) -> Result<Schema> {
|
fn build_arrow_schema(config: &ConnectorConfig) -> Result<Schema> {
|
||||||
let schema_fields: Vec<Field> = config.schema.iter().map(|col_cfg| {
|
let schema_fields: Vec<Field> = config.schema.iter().map(|col_cfg| {
|
||||||
let nullable = col_cfg.column_name == "notes"; // Hack for MVP
|
let nullable = col_cfg.column_name == "notes";
|
||||||
|
|
||||||
let arrow_type = match col_cfg.arrow_type.as_str() {
|
let arrow_type = match col_cfg.arrow_type.as_str() {
|
||||||
"Int64" => DataType::Int64,
|
"Int64" => DataType::Int64,
|
||||||
@@ -409,4 +433,75 @@ fn read_string_field(
|
|||||||
cursor.set_position(end as u64); // Manually advance cursor
|
cursor.set_position(end as u64); // Manually advance cursor
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// --------------------------------------------------------------------------------
|
||||||
|
// --- 3. PROFILER LOGIC (New Feature) ---
|
||||||
|
// --------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// Maps a PostgreSQL internal type name to a standard Arrow Type string for config.yaml.
|
||||||
|
fn map_postgres_to_arrow_type(pg_type_name: &str) -> Option<&'static str> {
|
||||||
|
match pg_type_name {
|
||||||
|
"int8" | "bigint" | "serial8" => Some("Int64"),
|
||||||
|
"int4" | "integer" | "serial" => Some("Int32"),
|
||||||
|
"float8" | "double precision" => Some("Float64"),
|
||||||
|
"float4" | "real" => Some("Float32"),
|
||||||
|
"varchar" | "text" | "uuid" => Some("Utf8"),
|
||||||
|
"bool" | "boolean" => Some("Boolean"),
|
||||||
|
"timestamptz" | "timestamp" => Some("Timestamp(Nanosecond, None)"),
|
||||||
|
"date" => Some("Date32"),
|
||||||
|
_ => None, // Returns None for unsupported types (like JSON, arrays, etc.)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn run_profiler_logic(config_path: &str) -> Result<String> {
|
||||||
|
// Phase 1: Load config to get connection string and query
|
||||||
|
let config: ConnectorConfig = load_and_validate_config(config_path)
|
||||||
|
.context("Failed to load and validate config for profiling")?;
|
||||||
|
|
||||||
|
// Use a non-COPY query to get metadata
|
||||||
|
let (base_query, _) = config.query.trim().split_once("TO STDOUT (FORMAT binary)")
|
||||||
|
.context("Query in config is malformed or not a COPY command")?;
|
||||||
|
|
||||||
|
// We only need the base query for the metadata query
|
||||||
|
let base_query_inner = base_query.trim().trim_start_matches("COPY (").trim_end_matches(")");
|
||||||
|
|
||||||
|
// Construct the metadata query (limit 0 is fastest)
|
||||||
|
let metadata_query = format!("SELECT * FROM ({}) AS subquery LIMIT 0", base_query_inner);
|
||||||
|
|
||||||
|
// Phase 2: Connect and execute the query
|
||||||
|
let pg_config: PgConfig = PgConfig::from_str(&config.connection_string)?;
|
||||||
|
let (client, connection) = pg_config.connect(NoTls).await
|
||||||
|
.context("Profiler: Failed to connect to PostgreSQL")?;
|
||||||
|
|
||||||
|
tokio::spawn(async move {
|
||||||
|
if let Err(e) = connection.await { eprintln!("Profiler connection error: {}", e); }
|
||||||
|
});
|
||||||
|
|
||||||
|
let statement = client.prepare(&metadata_query).await
|
||||||
|
.context("Profiler: Failed to prepare metadata query")?;
|
||||||
|
|
||||||
|
let mut output = String::from("schema:\n");
|
||||||
|
|
||||||
|
// Phase 3: Inspect the statement's columns for metadata
|
||||||
|
for column in statement.columns() {
|
||||||
|
let pg_type_name = column.type_().name().to_lowercase();
|
||||||
|
let arrow_type = map_postgres_to_arrow_type(&pg_type_name)
|
||||||
|
.unwrap_or("UNKNOWN (Review Manually)");
|
||||||
|
|
||||||
|
let column_entry = format!(
|
||||||
|
"- arrow_type: {}\n column_name: {}\n",
|
||||||
|
arrow_type,
|
||||||
|
column.name()
|
||||||
|
);
|
||||||
|
output.push_str(&column_entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Final instructions for the user
|
||||||
|
output.push_str("\n# NOTE: Paste the 'schema' block above into your config.yaml\n");
|
||||||
|
output.push_str(
|
||||||
|
"# REVIEW any UNKNOWN types. PostgreSQL types: (int8, float8, text, bool, timestamp, date, etc.)\n"
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(output)
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user