From 9992b5f135e68046456a080145561941d1c12eed Mon Sep 17 00:00:00 2001 From: root Date: Fri, 27 Mar 2026 20:14:16 -0500 Subject: [PATCH] =?UTF-8?q?Database=20connector:=20PostgreSQL=20=E2=86=92?= =?UTF-8?q?=20Parquet=20import?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - POST /ingest/postgres/tables — list all tables in a database - POST /ingest/postgres/import — import table → Parquet → catalog → queryable - Auto type mapping: int2/4/8 → Int, float4/8 → Float64, bool → Boolean, text/varchar/jsonb/timestamp → Utf8 (safe default per ADR-010) - Auto PII detection + lineage on import - Empty password support for trust auth - Tested: imported lab_trials (40 rows, 10 cols) and threat_intel (20 rows, 30 cols) from local knowledge_base Postgres database — immediately queryable Co-Authored-By: Claude Opus 4.6 (1M context) --- Cargo.lock | 191 +++++++++++++- Cargo.toml | 1 + crates/ingestd/Cargo.toml | 1 + crates/ingestd/src/db_ingest.rs | 225 ++++++++++++++++ crates/ingestd/src/lib.rs | 1 + crates/ingestd/src/service.rs | 129 +++++++++- .../e2a8f88a-59f6-40c7-a45b-e23d8f3533b6.json | 100 ++++++++ .../e7304f05-5278-4e17-961a-51f2588fd2aa.json | 240 ++++++++++++++++++ data/datasets/lab_trials.parquet | Bin 0 -> 64646 bytes data/datasets/threat_intel.parquet | Bin 0 -> 111130 bytes 10 files changed, 880 insertions(+), 8 deletions(-) create mode 100644 crates/ingestd/src/db_ingest.rs create mode 100644 data/_catalog/manifests/e2a8f88a-59f6-40c7-a45b-e23d8f3533b6.json create mode 100644 data/_catalog/manifests/e7304f05-5278-4e17-961a-51f2588fd2aa.json create mode 100644 data/datasets/lab_trials.parquet create mode 100644 data/datasets/threat_intel.parquet diff --git a/Cargo.lock b/Cargo.lock index 79a4972..87a29cf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -695,7 +695,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6139a8597ed92cf816dfb33f5dd6cf0bb93a6adc938f11039f371bc5bcd26c3" dependencies = [ "chrono", - "phf", + "phf 0.12.1", ] [[package]] @@ -2220,6 +2220,12 @@ dependencies = [ "num-traits", ] +[[package]] +name = "fallible-iterator" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" + [[package]] name = "fastrand" version = "2.3.0" @@ -2427,7 +2433,7 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "wasi", + "wasi 0.11.1+wasi-snapshot-preview1", "wasm-bindgen", ] @@ -2608,6 +2614,15 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + [[package]] name = "http" version = "1.4.0" @@ -2914,6 +2929,7 @@ dependencies = [ "shared", "storaged", "tokio", + "tokio-postgres", "tracing", ] @@ -3174,6 +3190,15 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" +[[package]] +name = "libredox" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ddbf48fd451246b1f8c2610bd3b4ac0cc6e149d89832867093ab69a17194f08" +dependencies = [ + "libc", +] + [[package]] name = "linux-raw-sys" version = "0.12.1" @@ -3402,7 +3427,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" dependencies = [ "libc", - "wasi", + "wasi 0.11.1+wasi-snapshot-preview1", "windows-sys 0.61.2", ] @@ -3601,6 +3626,24 @@ dependencies = [ "syn", ] +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags", +] + +[[package]] +name = "objc2-system-configuration" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7216bd11cbda54ccabcab84d523dc93b858ec75ecfb3a7d89513fa22464da396" +dependencies = [ + "objc2-core-foundation", +] + [[package]] name = "object" version = "0.37.3" @@ -3813,7 +3856,17 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "913273894cec178f401a31ec4b656318d95473527be05c0752cc41cdc32be8b7" dependencies = [ - "phf_shared", + "phf_shared 0.12.1", +] + +[[package]] +name = "phf" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" +dependencies = [ + "phf_shared 0.13.1", + "serde", ] [[package]] @@ -3825,6 +3878,15 @@ dependencies = [ "siphasher", ] +[[package]] +name = "phf_shared" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +dependencies = [ + "siphasher", +] + [[package]] name = "pin-project" version = "1.1.11" @@ -3863,6 +3925,38 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "postgres-protocol" +version = "0.6.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ee9dd5fe15055d2b6806f4736aa0c9637217074e224bbec46d4041b91bb9491" +dependencies = [ + "base64", + "byteorder", + "bytes", + "fallible-iterator", + "hmac", + "md-5", + "memchr", + "rand 0.9.2", + "sha2", + "stringprep", +] + +[[package]] +name = "postgres-types" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54b858f82211e84682fecd373f68e1ceae642d8d751a1ebd13f33de6257b3e20" +dependencies = [ + "bytes", + "chrono", + "fallible-iterator", + "postgres-protocol", + "serde_core", + "serde_json", +] + [[package]] name = "potential_utf" version = "0.1.4" @@ -4809,6 +4903,17 @@ dependencies = [ "tracing", ] +[[package]] +name = "stringprep" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", + "unicode-properties", +] + [[package]] name = "subsecond" version = "0.7.3" @@ -5040,6 +5145,32 @@ dependencies = [ "syn", ] +[[package]] +name = "tokio-postgres" +version = "0.7.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcea47c8f71744367793f16c2db1f11cb859d28f436bdb4ca9193eb1f787ee42" +dependencies = [ + "async-trait", + "byteorder", + "bytes", + "fallible-iterator", + "futures-channel", + "futures-util", + "log", + "parking_lot", + "percent-encoding", + "phf 0.13.1", + "pin-project-lite", + "postgres-protocol", + "postgres-types", + "rand 0.9.2", + "socket2 0.6.3", + "tokio", + "tokio-util", + "whoami", +] + [[package]] name = "tokio-rustls" version = "0.26.4" @@ -5396,12 +5527,33 @@ version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + [[package]] name = "unicode-ident" version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +[[package]] +name = "unicode-normalization" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-properties" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" + [[package]] name = "unicode-segmentation" version = "1.13.2" @@ -5541,6 +5693,15 @@ version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" +[[package]] +name = "wasi" +version = "0.14.7+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "883478de20367e224c0090af9cf5f9fa85bed63a95c1abf3afc5c083ebc06e8c" +dependencies = [ + "wasip2", +] + [[package]] name = "wasip2" version = "1.0.2+wasi-0.2.9" @@ -5559,6 +5720,15 @@ dependencies = [ "wit-bindgen", ] +[[package]] +name = "wasite" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66fe902b4a6b8028a753d5424909b764ccf79b7a209eac9bf97e59cda9f71a42" +dependencies = [ + "wasi 0.14.7+wasi-0.2.4", +] + [[package]] name = "wasm-bindgen" version = "0.2.114" @@ -5700,6 +5870,19 @@ version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" +[[package]] +name = "whoami" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6a5b12f9df4f978d2cfdb1bd3bac52433f44393342d7ee9c25f5a1c14c0f45d" +dependencies = [ + "libc", + "libredox", + "objc2-system-configuration", + "wasite", + "web-sys", +] + [[package]] name = "winapi-util" version = "0.1.11" diff --git a/Cargo.toml b/Cargo.toml index 5e8c4a4..12cc6e4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -45,3 +45,4 @@ csv = "1" lopdf = "0.35" encoding_rs = "0.8" instant-distance = "0.6" +tokio-postgres = { version = "0.7", features = ["with-serde_json-1", "with-chrono-0_4"] } diff --git a/crates/ingestd/Cargo.toml b/crates/ingestd/Cargo.toml index c1963e8..bde3487 100644 --- a/crates/ingestd/Cargo.toml +++ b/crates/ingestd/Cargo.toml @@ -20,3 +20,4 @@ sha2 = { workspace = true } csv = { workspace = true } chrono = { workspace = true } object_store = { workspace = true } +tokio-postgres = { workspace = true } diff --git a/crates/ingestd/src/db_ingest.rs b/crates/ingestd/src/db_ingest.rs new file mode 100644 index 0000000..1750665 --- /dev/null +++ b/crates/ingestd/src/db_ingest.rs @@ -0,0 +1,225 @@ +/// Database connector: pull tables from PostgreSQL directly into Parquet. +/// Connects, reads schema, streams rows into Arrow RecordBatches, writes Parquet. +/// No ORM, no schema definition — schema inferred from the database. + +use arrow::array::{ArrayRef, BooleanArray, Float64Array, Int32Array, Int64Array, StringArray}; +use arrow::datatypes::{DataType, Field, Schema}; +use arrow::record_batch::RecordBatch; +use std::sync::Arc; +use tokio_postgres::{Client, NoTls, types::Type as PgType}; + +/// Connection config for a database. +#[derive(Debug, Clone, serde::Deserialize)] +pub struct DbConfig { + pub host: String, + pub port: u16, + pub database: String, + pub user: String, + pub password: String, +} + +impl DbConfig { + pub fn connection_string(&self) -> String { + if self.password.is_empty() { + format!( + "host={} port={} dbname={} user={}", + self.host, self.port, self.database, self.user + ) + } else { + format!( + "host={} port={} dbname={} user={} password={}", + self.host, self.port, self.database, self.user, self.password + ) + } + } +} + +/// Result of a database table import. +#[derive(Debug, Clone, serde::Serialize)] +pub struct DbIngestResult { + pub table_name: String, + pub rows: usize, + pub columns: usize, + pub schema_detected: Vec, // column name: type pairs +} + +/// Connect to PostgreSQL and import a table as Arrow RecordBatches. +pub async fn import_postgres_table( + config: &DbConfig, + table_name: &str, + limit: Option, +) -> Result<(Arc, Vec, DbIngestResult), String> { + // Connect + let (client, connection) = tokio_postgres::connect(&config.connection_string(), NoTls) + .await + .map_err(|e| format!("postgres connect error: {e}"))?; + + // Spawn connection handler + tokio::spawn(async move { + if let Err(e) = connection.await { + tracing::error!("postgres connection error: {e}"); + } + }); + + tracing::info!("connected to postgres {}:{}/{}", config.host, config.port, config.database); + + // Get column info + let columns = get_table_columns(&client, table_name).await?; + if columns.is_empty() { + return Err(format!("table '{}' not found or has no columns", table_name)); + } + + let schema_detected: Vec = columns.iter() + .map(|(name, pg_type)| format!("{}: {}", name, pg_type_to_string(pg_type))) + .collect(); + + tracing::info!("table '{}': {} columns", table_name, columns.len()); + + // Build Arrow schema + let arrow_fields: Vec = columns.iter() + .map(|(name, pg_type)| Field::new(name, pg_type_to_arrow(pg_type), true)) + .collect(); + let schema = Arc::new(Schema::new(arrow_fields)); + + // Query rows + let sql = match limit { + Some(n) => format!("SELECT * FROM \"{}\" LIMIT {}", table_name, n), + None => format!("SELECT * FROM \"{}\"", table_name), + }; + + let rows = client.query(&sql, &[]) + .await + .map_err(|e| format!("query error: {e}"))?; + + let row_count = rows.len(); + tracing::info!("fetched {} rows from '{}'", row_count, table_name); + + if row_count == 0 { + return Ok((schema.clone(), vec![], DbIngestResult { + table_name: table_name.to_string(), + rows: 0, + columns: columns.len(), + schema_detected, + })); + } + + // Convert to Arrow arrays + let mut arrays: Vec = Vec::new(); + + for (col_idx, (_, pg_type)) in columns.iter().enumerate() { + let array = rows_to_arrow_column(&rows, col_idx, pg_type)?; + arrays.push(array); + } + + let batch = RecordBatch::try_new(schema.clone(), arrays) + .map_err(|e| format!("RecordBatch error: {e}"))?; + + let result = DbIngestResult { + table_name: table_name.to_string(), + rows: row_count, + columns: columns.len(), + schema_detected, + }; + + Ok((schema, vec![batch], result)) +} + +/// List all tables in the database. +pub async fn list_postgres_tables(config: &DbConfig) -> Result, String> { + let (client, connection) = tokio_postgres::connect(&config.connection_string(), NoTls) + .await + .map_err(|e| format!("connect error: {e}"))?; + + tokio::spawn(async move { let _ = connection.await; }); + + let rows = client.query( + "SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' ORDER BY table_name", + &[], + ).await.map_err(|e| format!("query error: {e}"))?; + + Ok(rows.iter().map(|r| r.get::<_, String>(0)).collect()) +} + +/// Get column names and types for a table. +async fn get_table_columns(client: &Client, table_name: &str) -> Result, String> { + // Use a dummy query to get column types + let stmt = client.prepare(&format!("SELECT * FROM \"{}\" LIMIT 0", table_name)) + .await + .map_err(|e| format!("prepare error for '{}': {e}", table_name))?; + + Ok(stmt.columns().iter() + .map(|col| (col.name().to_string(), col.type_().clone())) + .collect()) +} + +/// Convert a Postgres type to Arrow DataType. +fn pg_type_to_arrow(pg_type: &PgType) -> DataType { + match *pg_type { + PgType::BOOL => DataType::Boolean, + PgType::INT2 | PgType::INT4 => DataType::Int32, + PgType::INT8 | PgType::OID => DataType::Int64, + PgType::FLOAT4 | PgType::FLOAT8 | PgType::NUMERIC => DataType::Float64, + _ => DataType::Utf8, // everything else → string (safe default per ADR-010) + } +} + +fn pg_type_to_string(pg_type: &PgType) -> String { + format!("{}", pg_type) +} + +/// Convert a column of Postgres rows to an Arrow array. +fn rows_to_arrow_column( + rows: &[tokio_postgres::Row], + col_idx: usize, + pg_type: &PgType, +) -> Result { + match *pg_type { + PgType::BOOL => { + let vals: Vec> = rows.iter() + .map(|r| r.try_get(col_idx).ok()) + .collect(); + Ok(Arc::new(BooleanArray::from(vals))) + } + PgType::INT2 => { + let vals: Vec> = rows.iter() + .map(|r| r.try_get::<_, i16>(col_idx).ok().map(|v| v as i32)) + .collect(); + Ok(Arc::new(Int32Array::from(vals))) + } + PgType::INT4 => { + let vals: Vec> = rows.iter() + .map(|r| r.try_get(col_idx).ok()) + .collect(); + Ok(Arc::new(Int32Array::from(vals))) + } + PgType::INT8 | PgType::OID => { + let vals: Vec> = rows.iter() + .map(|r| r.try_get(col_idx).ok()) + .collect(); + Ok(Arc::new(Int64Array::from(vals))) + } + PgType::FLOAT4 => { + let vals: Vec> = rows.iter() + .map(|r| r.try_get::<_, f32>(col_idx).ok().map(|v| v as f64)) + .collect(); + Ok(Arc::new(Float64Array::from(vals))) + } + PgType::FLOAT8 => { + let vals: Vec> = rows.iter() + .map(|r| r.try_get(col_idx).ok()) + .collect(); + Ok(Arc::new(Float64Array::from(vals))) + } + _ => { + // Default: try to get as string + let vals: Vec> = rows.iter() + .map(|r| { + r.try_get::<_, String>(col_idx).ok() + .or_else(|| r.try_get::<_, serde_json::Value>(col_idx).ok().map(|v| v.to_string())) + .or_else(|| Some("".to_string())) + }) + .collect(); + Ok(Arc::new(StringArray::from(vals))) + } + } +} diff --git a/crates/ingestd/src/lib.rs b/crates/ingestd/src/lib.rs index e21ee5b..e0068b0 100644 --- a/crates/ingestd/src/lib.rs +++ b/crates/ingestd/src/lib.rs @@ -1,3 +1,4 @@ +pub mod db_ingest; pub mod detect; pub mod csv_ingest; pub mod json_ingest; diff --git a/crates/ingestd/src/service.rs b/crates/ingestd/src/service.rs index 1ac3666..ecc06b1 100644 --- a/crates/ingestd/src/service.rs +++ b/crates/ingestd/src/service.rs @@ -5,12 +5,16 @@ use axum::{ response::IntoResponse, routing::{get, post}, }; +use bytes::Bytes; use object_store::ObjectStore; use serde::Deserialize; use std::sync::Arc; use catalogd::registry::Registry; -use crate::pipeline; +use crate::{db_ingest, pipeline}; +use shared::arrow_helpers::record_batch_to_parquet; +use shared::types::{ObjectRef, SchemaFingerprint}; +use storaged::ops; #[derive(Clone)] pub struct IngestState { @@ -22,6 +26,8 @@ pub fn router(state: IngestState) -> Router { Router::new() .route("/health", get(health)) .route("/file", post(ingest_file)) + .route("/postgres/tables", post(list_pg_tables)) + .route("/postgres/import", post(import_pg_table)) .with_state(state) } @@ -31,17 +37,14 @@ async fn health() -> &'static str { #[derive(Deserialize)] struct IngestQuery { - /// Override dataset name (otherwise derived from filename) name: Option, } -/// Upload a file for ingestion. Accepts multipart/form-data with a "file" field. async fn ingest_file( State(state): State, Query(query): Query, mut multipart: Multipart, ) -> impl IntoResponse { - // Read the first file field let field = match multipart.next_field().await { Ok(Some(f)) => f, Ok(None) => return Err((StatusCode::BAD_REQUEST, "no file uploaded".to_string())), @@ -67,3 +70,121 @@ async fn ingest_file( Err(e) => Err((StatusCode::INTERNAL_SERVER_ERROR, e)), } } + +// --- PostgreSQL Import --- + +/// List tables in a PostgreSQL database. +async fn list_pg_tables( + Json(config): Json, +) -> impl IntoResponse { + match db_ingest::list_postgres_tables(&config).await { + Ok(tables) => Ok(Json(tables)), + Err(e) => Err((StatusCode::BAD_GATEWAY, e)), + } +} + +#[derive(Deserialize)] +struct PgImportRequest { + #[serde(flatten)] + config: db_ingest::DbConfig, + table: String, + /// Override dataset name (defaults to table name) + dataset_name: Option, + /// Max rows to import (None = all) + limit: Option, +} + +/// Import a PostgreSQL table into the lakehouse. +async fn import_pg_table( + State(state): State, + Json(req): Json, +) -> Result<(StatusCode, Json), (StatusCode, String)> { + tracing::info!("importing postgres table '{}' from {}:{}/{}", + req.table, req.config.host, req.config.port, req.config.database); + + // Import from Postgres + let (schema, batches, db_result) = db_ingest::import_postgres_table( + &req.config, &req.table, req.limit, + ).await.map_err(|e| (StatusCode::BAD_GATEWAY, e))?; + + if batches.is_empty() || db_result.rows == 0 { + return Ok((StatusCode::OK, Json(serde_json::json!({ + "table": req.table, + "rows": 0, + "message": "table is empty", + })))); + } + + // Convert to Parquet + let mut all_parquet = Vec::new(); + for batch in &batches { + let pq = record_batch_to_parquet(batch) + .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e))?; + all_parquet.extend_from_slice(&pq); + } + + let dataset_name = req.dataset_name.unwrap_or_else(|| req.table.clone()); + let storage_key = format!("datasets/{}.parquet", dataset_name); + let parquet_size = all_parquet.len() as u64; + + // Store + ops::put(&state.store, &storage_key, Bytes::from(all_parquet)) + .await + .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e))?; + + // Register + let schema_fp = shared::arrow_helpers::fingerprint_schema(&schema); + let now = chrono::Utc::now(); + state.registry.register( + dataset_name.clone(), + SchemaFingerprint(schema_fp.0), + vec![ObjectRef { + bucket: "data".to_string(), + key: storage_key.clone(), + size_bytes: parquet_size, + created_at: now, + }], + ).await.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e))?; + + // Auto-populate metadata (PII detection, lineage) + let col_names: Vec<&str> = schema.fields().iter().map(|f| f.name().as_str()).collect(); + let sensitivity = shared::pii::detect_dataset_sensitivity(&col_names); + let columns: Vec = schema.fields().iter().map(|f| { + let sens = shared::pii::detect_sensitivity(f.name()); + shared::types::ColumnMeta { + name: f.name().clone(), + data_type: f.data_type().to_string(), + sensitivity: sens.clone(), + description: String::new(), + is_pii: matches!(sens, Some(shared::types::Sensitivity::Pii)), + } + }).collect(); + + let lineage = shared::types::Lineage { + source_system: "postgresql".to_string(), + source_file: format!("{}:{}/{}.{}", req.config.host, req.config.port, req.config.database, req.table), + ingest_job: format!("pg-import-{}", now.timestamp_millis()), + ingest_timestamp: now, + parent_datasets: vec![], + }; + + let _ = state.registry.update_metadata(&dataset_name, catalogd::registry::MetadataUpdate { + sensitivity, + columns: Some(columns), + lineage: Some(lineage), + row_count: Some(db_result.rows as u64), + ..Default::default() + }).await; + + tracing::info!("imported '{}' from postgres: {} rows → {}", dataset_name, db_result.rows, storage_key); + + Ok((StatusCode::CREATED, Json(serde_json::json!({ + "dataset_name": dataset_name, + "table": req.table, + "rows": db_result.rows, + "columns": db_result.columns, + "schema": db_result.schema_detected, + "storage_key": storage_key, + "size_bytes": parquet_size, + })))) +} diff --git a/data/_catalog/manifests/e2a8f88a-59f6-40c7-a45b-e23d8f3533b6.json b/data/_catalog/manifests/e2a8f88a-59f6-40c7-a45b-e23d8f3533b6.json new file mode 100644 index 0000000..75ab43b --- /dev/null +++ b/data/_catalog/manifests/e2a8f88a-59f6-40c7-a45b-e23d8f3533b6.json @@ -0,0 +1,100 @@ +{ + "id": "e2a8f88a-59f6-40c7-a45b-e23d8f3533b6", + "name": "lab_trials", + "schema_fingerprint": "1d5782349402439a7e44efd0ccab9ae64ac3044221adef9e828b60b8bbb44dd5", + "objects": [ + { + "bucket": "data", + "key": "datasets/lab_trials.parquet", + "size_bytes": 64646, + "created_at": "2026-03-28T01:14:03.026116573Z" + } + ], + "created_at": "2026-03-28T01:14:03.026117277Z", + "updated_at": "2026-03-28T01:14:03.026247826Z", + "description": "", + "owner": "", + "sensitivity": null, + "columns": [ + { + "name": "id", + "data_type": "Int32", + "sensitivity": null, + "description": "", + "is_pii": false + }, + { + "name": "experiment_id", + "data_type": "Int32", + "sensitivity": null, + "description": "", + "is_pii": false + }, + { + "name": "trial_num", + "data_type": "Int32", + "sensitivity": null, + "description": "", + "is_pii": false + }, + { + "name": "config_diff", + "data_type": "Utf8", + "sensitivity": null, + "description": "", + "is_pii": false + }, + { + "name": "config_snapshot", + "data_type": "Utf8", + "sensitivity": null, + "description": "", + "is_pii": false + }, + { + "name": "scores", + "data_type": "Utf8", + "sensitivity": null, + "description": "", + "is_pii": false + }, + { + "name": "avg_score", + "data_type": "Float64", + "sensitivity": null, + "description": "", + "is_pii": false + }, + { + "name": "improved", + "data_type": "Boolean", + "sensitivity": null, + "description": "", + "is_pii": false + }, + { + "name": "duration_ms", + "data_type": "Int32", + "sensitivity": null, + "description": "", + "is_pii": false + }, + { + "name": "created_at", + "data_type": "Utf8", + "sensitivity": null, + "description": "", + "is_pii": false + } + ], + "lineage": { + "source_system": "postgresql", + "source_file": "127.0.0.1:5432/knowledge_base.lab_trials", + "ingest_job": "pg-import-1774660443026", + "ingest_timestamp": "2026-03-28T01:14:03.026116573Z", + "parent_datasets": [] + }, + "freshness": null, + "tags": [], + "row_count": 40 +} \ No newline at end of file diff --git a/data/_catalog/manifests/e7304f05-5278-4e17-961a-51f2588fd2aa.json b/data/_catalog/manifests/e7304f05-5278-4e17-961a-51f2588fd2aa.json new file mode 100644 index 0000000..e9de25d --- /dev/null +++ b/data/_catalog/manifests/e7304f05-5278-4e17-961a-51f2588fd2aa.json @@ -0,0 +1,240 @@ +{ + "id": "e7304f05-5278-4e17-961a-51f2588fd2aa", + "name": "threat_intel", + "schema_fingerprint": "df1e126046147b3de42086880e10c3501a3a615ecddf336bc24957a24c321241", + "objects": [ + { + "bucket": "data", + "key": "datasets/threat_intel.parquet", + "size_bytes": 111130, + "created_at": "2026-03-28T01:14:03.054140697Z" + } + ], + "created_at": "2026-03-28T01:14:03.054141294Z", + "updated_at": "2026-03-28T01:14:03.054427047Z", + "description": "", + "owner": "", + "sensitivity": null, + "columns": [ + { + "name": "id", + "data_type": "Int32", + "sensitivity": null, + "description": "", + "is_pii": false + }, + { + "name": "ip", + "data_type": "Utf8", + "sensitivity": null, + "description": "", + "is_pii": false + }, + { + "name": "threat_level", + "data_type": "Utf8", + "sensitivity": null, + "description": "", + "is_pii": false + }, + { + "name": "classification", + "data_type": "Utf8", + "sensitivity": null, + "description": "", + "is_pii": false + }, + { + "name": "confidence", + "data_type": "Float64", + "sensitivity": null, + "description": "", + "is_pii": false + }, + { + "name": "summary", + "data_type": "Utf8", + "sensitivity": null, + "description": "", + "is_pii": false + }, + { + "name": "indicators", + "data_type": "Utf8", + "sensitivity": null, + "description": "", + "is_pii": false + }, + { + "name": "recommendation", + "data_type": "Utf8", + "sensitivity": null, + "description": "", + "is_pii": false + }, + { + "name": "pattern", + "data_type": "Utf8", + "sensitivity": null, + "description": "", + "is_pii": false + }, + { + "name": "attack_type", + "data_type": "Utf8", + "sensitivity": null, + "description": "", + "is_pii": false + }, + { + "name": "likely_automated", + "data_type": "Boolean", + "sensitivity": null, + "description": "", + "is_pii": false + }, + { + "name": "country", + "data_type": "Utf8", + "sensitivity": null, + "description": "", + "is_pii": false + }, + { + "name": "country_code", + "data_type": "Utf8", + "sensitivity": null, + "description": "", + "is_pii": false + }, + { + "name": "city", + "data_type": "Utf8", + "sensitivity": null, + "description": "", + "is_pii": false + }, + { + "name": "isp", + "data_type": "Utf8", + "sensitivity": null, + "description": "", + "is_pii": false + }, + { + "name": "org", + "data_type": "Utf8", + "sensitivity": null, + "description": "", + "is_pii": false + }, + { + "name": "asn", + "data_type": "Utf8", + "sensitivity": null, + "description": "", + "is_pii": false + }, + { + "name": "is_proxy", + "data_type": "Boolean", + "sensitivity": null, + "description": "", + "is_pii": false + }, + { + "name": "is_hosting", + "data_type": "Boolean", + "sensitivity": null, + "description": "", + "is_pii": false + }, + { + "name": "open_ports", + "data_type": "Utf8", + "sensitivity": null, + "description": "", + "is_pii": false + }, + { + "name": "blocklist_count", + "data_type": "Int32", + "sensitivity": null, + "description": "", + "is_pii": false + }, + { + "name": "blocklist_total", + "data_type": "Int32", + "sensitivity": null, + "description": "", + "is_pii": false + }, + { + "name": "blocklists_blocked", + "data_type": "Utf8", + "sensitivity": null, + "description": "", + "is_pii": false + }, + { + "name": "reverse_dns", + "data_type": "Utf8", + "sensitivity": null, + "description": "", + "is_pii": false + }, + { + "name": "traceroute", + "data_type": "Utf8", + "sensitivity": null, + "description": "", + "is_pii": false + }, + { + "name": "log_count", + "data_type": "Int32", + "sensitivity": null, + "description": "", + "is_pii": false + }, + { + "name": "banned", + "data_type": "Boolean", + "sensitivity": null, + "description": "", + "is_pii": false + }, + { + "name": "enriched_at", + "data_type": "Utf8", + "sensitivity": null, + "description": "", + "is_pii": false + }, + { + "name": "updated_at", + "data_type": "Utf8", + "sensitivity": null, + "description": "", + "is_pii": false + }, + { + "name": "raw_data", + "data_type": "Utf8", + "sensitivity": null, + "description": "", + "is_pii": false + } + ], + "lineage": { + "source_system": "postgresql", + "source_file": "127.0.0.1:5432/knowledge_base.threat_intel", + "ingest_job": "pg-import-1774660443054", + "ingest_timestamp": "2026-03-28T01:14:03.054140697Z", + "parent_datasets": [] + }, + "freshness": null, + "tags": [], + "row_count": 20 +} \ No newline at end of file diff --git a/data/datasets/lab_trials.parquet b/data/datasets/lab_trials.parquet new file mode 100644 index 0000000000000000000000000000000000000000..6f4d4fc16910a4a55209e8e6d84b7f5ecf2512b5 GIT binary patch literal 64646 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 literal 0 HcmV?d00001 diff --git a/data/datasets/threat_intel.parquet b/data/datasets/threat_intel.parquet new file mode 100644 index 0000000000000000000000000000000000000000..15a14acb394be53d68e28a7ba5dc6f83921b940f GIT binary patch literal 111130 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 L92uFfUda4^o_g-W literal 0 HcmV?d00001