laminate_sql/lib.rs
1//! # laminate-sql — Database connectors for laminate
2//!
3//! Provides the [`DataSource`] trait and implementations for PostgreSQL,
4//! SQLite, and MySQL. Queries return rows as [`FlexValue`] for shaping,
5//! coercion, and schema inference.
6//!
7//! ## Features
8//!
9//! - `postgres` — PostgreSQL via sqlx
10//! - `sqlite` — SQLite via sqlx
11//! - `mysql` — MySQL via sqlx
12//! - `all-databases` — All of the above
13//!
14//! ## Quick Start
15//!
16//! ```ignore
17//! use laminate_sql::{DataSource, PostgresSource};
18//!
19//! let source = PostgresSource::connect("postgres://user:pass@localhost/mydb").await?;
20//! let rows = source.query("SELECT * FROM customers LIMIT 100").await?;
21//!
22//! // Use with laminate schema inference
23//! let raw_rows: Vec<serde_json::Value> = rows.iter().map(|r| r.into_raw()).collect();
24//! let schema = laminate::InferredSchema::from_values(&raw_rows);
25//! let report = schema.audit(&raw_rows);
26//! println!("{}", report.summary());
27//! ```
28
29use laminate::FlexValue;
30use serde_json::Value;
31
32/// A data source that produces rows as FlexValue.
33///
34/// Implementations exist for PostgreSQL, SQLite, and MySQL via the
35/// corresponding feature flags. Custom data sources can implement this
36/// trait directly.
37#[async_trait::async_trait]
38pub trait DataSource: Send + Sync {
39 /// Execute a query and return all rows as FlexValue.
40 async fn query(&self, sql: &str) -> Result<Vec<FlexValue>, DataSourceError>;
41
42 /// Execute a query with bind parameters and return all rows.
43 async fn query_with(
44 &self,
45 sql: &str,
46 params: &[Value],
47 ) -> Result<Vec<FlexValue>, DataSourceError>;
48
49 /// Get the column names from a query without fetching data.
50 async fn columns(&self, sql: &str) -> Result<Vec<String>, DataSourceError>;
51
52 /// Count rows matching a query.
53 async fn count(&self, sql: &str) -> Result<u64, DataSourceError>;
54}
55
56/// Errors from data source operations.
57#[derive(Debug, thiserror::Error)]
58pub enum DataSourceError {
59 /// Failed to connect to the database.
60 #[error("connection failed: {0}")]
61 ConnectionFailed(String),
62
63 /// A SQL query failed to execute.
64 #[error("query failed: {0}")]
65 QueryFailed(String),
66
67 /// Row data could not be converted to JSON.
68 #[error("serialization failed: {0}")]
69 SerializationFailed(String),
70
71 /// The requested operation is not supported by this backend.
72 #[error("unsupported operation: {0}")]
73 Unsupported(String),
74}
75
76/// Convert sqlx errors to DataSourceError.
77impl From<sqlx::Error> for DataSourceError {
78 fn from(e: sqlx::Error) -> Self {
79 match e {
80 sqlx::Error::Configuration(_) | sqlx::Error::Io(_) => {
81 DataSourceError::ConnectionFailed(e.to_string())
82 }
83 _ => DataSourceError::QueryFailed(e.to_string()),
84 }
85 }
86}
87
88// ── Database-specific modules ─────────────────────────────────
89
90#[cfg(feature = "postgres")]
91pub mod postgres;
92
93#[cfg(feature = "sqlite")]
94pub mod sqlite;
95
96#[cfg(feature = "mysql")]
97pub mod mysql;
98
99// ── Re-exports ────────────────────────────────────────────────
100
101#[cfg(feature = "postgres")]
102pub use postgres::PostgresSource;
103
104#[cfg(feature = "sqlite")]
105pub use sqlite::SqliteSource;
106
107#[cfg(feature = "mysql")]
108pub use mysql::MysqlSource;
109
110// ── CSV and JSON Lines sources (always available) ─────────────
111
112/// Read JSON Lines (newline-delimited JSON) as FlexValue rows.
113pub fn read_jsonl(content: &str) -> Result<Vec<FlexValue>, DataSourceError> {
114 content
115 .lines()
116 .filter(|line| !line.trim().is_empty())
117 .map(|line| {
118 FlexValue::from_json(line)
119 .map_err(|e| DataSourceError::SerializationFailed(e.to_string()))
120 })
121 .collect()
122}
123
124/// Read a JSON array as FlexValue rows.
125pub fn read_json_array(json: &str) -> Result<Vec<FlexValue>, DataSourceError> {
126 let value: Value = serde_json::from_str(json)
127 .map_err(|e| DataSourceError::SerializationFailed(e.to_string()))?;
128
129 match value {
130 Value::Array(arr) => Ok(arr.into_iter().map(FlexValue::new).collect()),
131 _ => Err(DataSourceError::SerializationFailed(
132 "expected a JSON array".into(),
133 )),
134 }
135}