blob: b708e26e479efd14f9e7f32bb69fc02067dede46 [file]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
//! Table-model (relational) session — a thin wrapper over [`Session`] with
//! `sql_dialect="table"` enforced at open time (protocol spec §6).
use std::time::Duration;
use crate::client::dataset::SessionDataSet;
use crate::client::session::{Session, SessionConfig};
use crate::connection::Endpoint;
use crate::data::Tablet;
use crate::error::{Error, Result};
/// Builder for [`TableSession`]. Defaults mirror [`SessionConfig`] except
/// the SQL dialect, which is always `"table"`.
///
/// ```no_run
/// use iotdb_client::TableSession;
///
/// let mut session = TableSession::builder()
/// .node_urls(&["127.0.0.1:6667"])?
/// .database("mydb")
/// .build()?;
/// session.execute_non_query("SHOW TABLES")?;
/// # Ok::<(), iotdb_client::Error>(())
/// ```
#[derive(Debug, Clone)]
pub struct TableSessionBuilder {
config: SessionConfig,
}
impl Default for TableSessionBuilder {
fn default() -> Self {
Self {
config: SessionConfig {
sql_dialect: "table".into(),
..SessionConfig::default()
},
}
}
}
impl TableSessionBuilder {
pub fn new() -> Self {
Self::default()
}
/// Endpoints from `"host:port"` node-url strings.
pub fn node_urls<S: AsRef<str>>(mut self, node_urls: &[S]) -> Result<Self> {
self.config = self.config.with_node_urls(node_urls)?;
Ok(self)
}
pub fn endpoints(mut self, endpoints: Vec<Endpoint>) -> Self {
self.config.endpoints = endpoints;
self
}
pub fn username(mut self, username: impl Into<String>) -> Self {
self.config.username = username.into();
self
}
pub fn password(mut self, password: impl Into<String>) -> Self {
self.config.password = password.into();
self
}
/// Database to select at open time (sent as config key `"db"`).
pub fn database(mut self, database: impl Into<String>) -> Self {
self.config.database = Some(database.into());
self
}
pub fn fetch_size(mut self, fetch_size: i32) -> Self {
self.config.fetch_size = fetch_size;
self
}
pub fn zone_id(mut self, zone_id: impl Into<String>) -> Self {
self.config.zone_id = zone_id.into();
self
}
pub fn connect_timeout(mut self, timeout: Duration) -> Self {
self.config.connect_timeout = timeout;
self
}
pub fn query_timeout_ms(mut self, timeout_ms: i64) -> Self {
self.config.query_timeout_ms = timeout_ms;
self
}
/// Speak TCompactProtocol ("RPC compression"). Must match the server's
/// `dn_rpc_thrift_compression_enable` setting — see
/// [`SessionConfig::enable_rpc_compression`].
pub fn enable_rpc_compression(mut self, enable: bool) -> Self {
self.config.enable_rpc_compression = enable;
self
}
/// Wrap connections in TLS (cargo feature `tls`).
#[cfg(feature = "tls")]
pub fn use_ssl(mut self, use_ssl: bool) -> Self {
self.config.use_ssl = use_ssl;
self
}
/// PEM certificate added as trusted root (cargo feature `tls`).
#[cfg(feature = "tls")]
pub fn ca_cert_path(mut self, path: impl Into<std::path::PathBuf>) -> Self {
self.config.ca_cert_path = Some(path.into());
self
}
/// Skip certificate verification — **dangerous** outside tests
/// (cargo feature `tls`).
#[cfg(feature = "tls")]
pub fn accept_invalid_certs(mut self, accept: bool) -> Self {
self.config.accept_invalid_certs = accept;
self
}
/// Hostname for SNI + certificate validation (cargo feature `tls`).
#[cfg(feature = "tls")]
pub fn domain_override(mut self, domain: impl Into<String>) -> Self {
self.config.domain_override = Some(domain.into());
self
}
/// PEM client certificate for mutual TLS; set together with
/// [`client_key_path`](Self::client_key_path) (cargo feature `tls`).
#[cfg(feature = "tls")]
pub fn client_cert_path(mut self, path: impl Into<std::path::PathBuf>) -> Self {
self.config.client_cert_path = Some(path.into());
self
}
/// PEM PKCS#8 private key for the client certificate; set together with
/// [`client_cert_path`](Self::client_cert_path) (cargo feature `tls`).
#[cfg(feature = "tls")]
pub fn client_key_path(mut self, path: impl Into<std::path::PathBuf>) -> Self {
self.config.client_key_path = Some(path.into());
self
}
/// The [`SessionConfig`] this builder resolves to (dialect always
/// `"table"`).
pub fn config(&self) -> &SessionConfig {
&self.config
}
/// Open the session against the configured endpoints.
pub fn build(mut self) -> Result<TableSession> {
// Enforce the dialect even if the config was mutated externally.
self.config.sql_dialect = "table".into();
let mut session = Session::new(self.config);
session.open()?;
Ok(TableSession { session })
}
}
/// A table-model (relational) session. All statements run in the `"table"`
/// SQL dialect; inserts require table-model tablets
/// ([`Tablet::new_table`]).
pub struct TableSession {
session: Session,
}
impl TableSession {
pub fn builder() -> TableSessionBuilder {
TableSessionBuilder::new()
}
/// The database currently selected on this session, if any — tracked
/// from the open-time `"db"` config key and `USE <db>` responses.
pub fn database(&self) -> Option<&str> {
self.session.database()
}
pub fn is_open(&self) -> bool {
self.session.is_open()
}
/// Insert a table-model tablet (`writeToTable=true` + column
/// categories on the wire). Rejects tree-model tablets.
pub fn insert(&mut self, tablet: &Tablet) -> Result<()> {
if !tablet.is_table_model() {
return Err(Error::Client(
"TableSession::insert requires a table-model tablet (Tablet::new_table)".into(),
));
}
self.session.insert_tablet(tablet)
}
/// Execute a query; the returned [`SessionDataSet`] borrows this
/// session until closed or dropped.
pub fn execute_query(&mut self, sql: &str) -> Result<SessionDataSet<'_>> {
self.session.execute_query(sql)
}
/// Execute a non-query statement (DDL/DML), tracking `USE <db>`.
pub fn execute_non_query(&mut self, sql: &str) -> Result<()> {
self.session.execute_non_query(sql)
}
pub fn close(&mut self) -> Result<()> {
self.session.close()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::data::TSDataType;
#[test]
fn builder_defaults_to_table_dialect() {
let b = TableSessionBuilder::new()
.node_urls(&["10.0.0.1:6667"])
.unwrap()
.username("u")
.password("p")
.database("db1")
.fetch_size(500)
.zone_id("UTC")
.connect_timeout(Duration::from_secs(3))
.query_timeout_ms(1_000);
let cfg = b.config();
assert_eq!(cfg.sql_dialect, "table");
assert_eq!(cfg.endpoints, vec![Endpoint::new("10.0.0.1", 6667)]);
assert_eq!(cfg.username, "u");
assert_eq!(cfg.password, "p");
assert_eq!(cfg.database.as_deref(), Some("db1"));
assert_eq!(cfg.fetch_size, 500);
assert_eq!(cfg.zone_id, "UTC");
assert_eq!(cfg.connect_timeout, Duration::from_secs(3));
assert_eq!(cfg.query_timeout_ms, 1_000);
}
#[test]
fn builder_passes_transport_options_through() {
let b = TableSessionBuilder::new().enable_rpc_compression(true);
assert!(b.config().enable_rpc_compression);
#[cfg(feature = "tls")]
{
let b = TableSessionBuilder::new()
.use_ssl(true)
.ca_cert_path("/certs/ca.pem")
.accept_invalid_certs(true)
.domain_override("iotdb.internal")
.client_cert_path("/certs/client.pem")
.client_key_path("/certs/client-key.pem");
let cfg = b.config();
assert!(cfg.use_ssl);
assert_eq!(cfg.ca_cert_path.as_deref(), Some("/certs/ca.pem".as_ref()));
assert!(cfg.accept_invalid_certs);
assert_eq!(cfg.domain_override.as_deref(), Some("iotdb.internal"));
assert_eq!(
cfg.client_cert_path.as_deref(),
Some("/certs/client.pem".as_ref())
);
assert_eq!(
cfg.client_key_path.as_deref(),
Some("/certs/client-key.pem".as_ref())
);
}
}
#[test]
fn insert_rejects_tree_model_tablets() {
let mut session = TableSession {
session: Session::new(SessionConfig::default()),
};
let tablet = Tablet::new("root.sg.d1", vec!["s1".into()], vec![TSDataType::Int32]).unwrap();
match session.insert(&tablet) {
Err(Error::Client(msg)) => assert!(msg.contains("table-model")),
other => panic!("expected client error, got {other:?}"),
}
}
/// Live-server test; skipped when no IoTDB instance is reachable.
#[test]
fn live_table_session_roundtrip() {
use std::net::TcpStream;
if TcpStream::connect_timeout(
&"127.0.0.1:6667".parse().unwrap(),
Duration::from_millis(300),
)
.is_err()
{
eprintln!("skipping live_table_session_roundtrip: no IoTDB server on 127.0.0.1:6667");
return;
}
let mut session = TableSession::builder().build().expect("open");
assert!(session.is_open());
session
.execute_non_query("CREATE DATABASE IF NOT EXISTS rust_client_test")
.expect("create db");
session
.execute_non_query("USE rust_client_test")
.expect("use db");
assert_eq!(session.database(), Some("rust_client_test"));
session.close().expect("close");
}
}