Skip to main content

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}