From 6114964c3a9c828890456758ac226945857e3a08 Mon Sep 17 00:00:00 2001 From: everpcpc Date: Thu, 26 Oct 2023 17:41:00 +0800 Subject: [PATCH] feat(bindings/python): convert query result types --- bindings/python/Cargo.toml | 2 +- .../databend_driver/__init__.py | 3 +- .../databend_driver/__init__.pyi | 12 +- bindings/python/pyproject.toml | 10 +- bindings/python/src/asyncio.rs | 51 ------ bindings/python/src/lib.rs | 167 +++++++++++++++--- bindings/python/tests/binding.feature | 23 +-- bindings/python/tests/steps/binding.py | 41 ++++- driver/src/conn.rs | 1 + 9 files changed, 190 insertions(+), 120 deletions(-) rename bindings/python/{python => package}/databend_driver/__init__.py (94%) rename bindings/python/{python => package}/databend_driver/__init__.pyi (67%) delete mode 100644 bindings/python/src/asyncio.rs mode change 100644 => 120000 bindings/python/tests/binding.feature diff --git a/bindings/python/Cargo.toml b/bindings/python/Cargo.toml index 4c8933d04..f197a7432 100644 --- a/bindings/python/Cargo.toml +++ b/bindings/python/Cargo.toml @@ -8,7 +8,7 @@ license = { workspace = true } authors = { workspace = true } [lib] -crate-type = ["cdylib"] +crate-type = ["cdylib", "rlib"] name = "databend_driver" doc = false diff --git a/bindings/python/python/databend_driver/__init__.py b/bindings/python/package/databend_driver/__init__.py similarity index 94% rename from bindings/python/python/databend_driver/__init__.py rename to bindings/python/package/databend_driver/__init__.py index eb8125d91..fdafd8eb2 100644 --- a/bindings/python/python/databend_driver/__init__.py +++ b/bindings/python/package/databend_driver/__init__.py @@ -12,6 +12,5 @@ # See the License for the specific language governing permissions and # limitations under the License. +# flake8: noqa from ._databend_driver import * - -__all__ = _databend_driver.__all__ diff --git a/bindings/python/python/databend_driver/__init__.pyi b/bindings/python/package/databend_driver/__init__.pyi similarity index 67% rename from bindings/python/python/databend_driver/__init__.pyi rename to bindings/python/package/databend_driver/__init__.pyi index 9e2507064..c1d7cd0ef 100644 --- a/bindings/python/python/databend_driver/__init__.pyi +++ b/bindings/python/package/databend_driver/__init__.pyi @@ -12,6 +12,12 @@ # See the License for the specific language governing permissions and # limitations under the License. -class AsyncDatabendDriver: - def __init__(self, dsn: str): ... # NOQA - async def exec(self, sql: str) -> int: ... # NOQA +# flake8: noqa +class AsyncDatabendConnection: + async def exec(self, sql: str): ... + async def query_row(self, sql: str): ... + +# flake8: noqa +class AsyncDatabendClient: + def __init__(self, dsn: str): ... + async def get_conn(self) -> AsyncDatabendConnection: ... diff --git a/bindings/python/pyproject.toml b/bindings/python/pyproject.toml index 74f0e9c92..48cd43fbc 100644 --- a/bindings/python/pyproject.toml +++ b/bindings/python/pyproject.toml @@ -1,7 +1,3 @@ -[build-system] -build-backend = "maturin" -requires = ["maturin>=1.0,<2.0"] - [project] classifiers = [ "Programming Language :: Rust", @@ -25,4 +21,8 @@ Repository = "https://github.com/datafuselabs/bendsql" [tool.maturin] features = ["pyo3/extension-module"] module-name = "databend_driver._databend_driver" -python-source = "python" +python-source = "package" + +[build-system] +build-backend = "maturin" +requires = ["maturin>=1.0,<2.0"] diff --git a/bindings/python/src/asyncio.rs b/bindings/python/src/asyncio.rs deleted file mode 100644 index 248a1e69e..000000000 --- a/bindings/python/src/asyncio.rs +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright 2021 Datafuse Labs -// -// Licensed 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. - -use pyo3::prelude::*; - -use pyo3_asyncio::tokio::future_into_py; - -use crate::{build_connector, Connector}; - -/// `AsyncDatabendDriver` is the entry for all public async API -#[pyclass(module = "databend_driver")] -pub struct AsyncDatabendDriver(Connector); - -#[pymethods] -impl AsyncDatabendDriver { - #[new] - #[pyo3(signature = (dsn))] - pub fn new(dsn: &str) -> PyResult { - Ok(AsyncDatabendDriver(build_connector(dsn)?)) - } - - /// exec - pub fn exec<'p>(&'p self, py: Python<'p>, sql: String) -> PyResult<&'p PyAny> { - let this = self.0.clone(); - future_into_py(py, async move { - let res = this.connector.exec(&sql).await.unwrap(); - Ok(res) - }) - } - - pub fn query_row<'p>(&'p self, py: Python<'p>, sql: String) -> PyResult<&'p PyAny> { - let this = self.0.clone(); - future_into_py(py, async move { - let row = this.connector.query_row(&sql).await.unwrap(); - let row = row.unwrap(); - let res = row.is_empty(); - Ok(res) - }) - } -} diff --git a/bindings/python/src/lib.rs b/bindings/python/src/lib.rs index 0d1e3336b..b64735336 100644 --- a/bindings/python/src/lib.rs +++ b/bindings/python/src/lib.rs @@ -12,16 +12,12 @@ // See the License for the specific language governing permissions and // limitations under the License. -mod asyncio; - -use crate::asyncio::*; - -use databend_driver::{Client, Connection}; - use pyo3::create_exception; use pyo3::exceptions::PyException; use pyo3::prelude::*; -use std::sync::Arc; +use pyo3::types::{PyDict, PyList, PyTuple}; +use pyo3_asyncio::tokio::future_into_py; + create_exception!( databend_client, Error, @@ -29,32 +25,147 @@ create_exception!( "databend_client related errors" ); -#[derive(Clone)] -pub struct Connector { - pub connector: FusedConnector, +#[pymodule] +fn _databend_driver(_py: Python, m: &PyModule) -> PyResult<()> { + m.add_class::()?; + m.add_class::()?; + Ok(()) } -pub type FusedConnector = Arc; - -// For bindings -impl Connector { - pub fn new_connector(dsn: &str) -> Result, Error> { - let client = Client::new(dsn.to_string()); - let conn = futures::executor::block_on(client.get_conn()).unwrap(); - let r = Self { - connector: FusedConnector::from(conn), - }; - Ok(Box::new(r)) +#[pyclass(module = "databend_driver")] +pub struct AsyncDatabendClient(databend_driver::Client); + +#[pymethods] +impl AsyncDatabendClient { + #[new] + #[pyo3(signature = (dsn))] + pub fn new(dsn: String) -> PyResult { + let client = databend_driver::Client::new(dsn); + Ok(Self(client)) + } + + pub fn get_conn<'p>(&'p self, py: Python<'p>) -> PyResult<&'p PyAny> { + let this = self.0.clone(); + future_into_py(py, async move { + let conn = this.get_conn().await.unwrap(); + Ok(AsyncDatabendConnection(conn)) + }) } } -fn build_connector(dsn: &str) -> PyResult { - let conn = Connector::new_connector(dsn).unwrap(); - Ok(*conn) +#[pyclass(module = "databend_driver")] +pub struct AsyncDatabendConnection(Box); + +#[pymethods] +impl AsyncDatabendConnection { + pub fn exec<'p>(&'p self, py: Python<'p>, sql: String) -> PyResult<&'p PyAny> { + let this = self.0.clone(); + future_into_py(py, async move { + let res = this.exec(&sql).await.unwrap(); + Ok(res) + }) + } + + pub fn query_row<'p>(&'p self, py: Python<'p>, sql: String) -> PyResult<&'p PyAny> { + let this = self.0.clone(); + future_into_py(py, async move { + let row = this.query_row(&sql).await.unwrap(); + let row = row.unwrap(); + Ok(Row(row)) + }) + } } -#[pymodule] -fn _databend_driver(_py: Python, m: &PyModule) -> PyResult<()> { - m.add_class::()?; - Ok(()) +#[pyclass(module = "databend_driver")] +pub struct Row(databend_driver::Row); + +#[pymethods] +impl Row { + pub fn values<'p>(&'p self, py: Python<'p>) -> PyResult { + let res = PyTuple::new( + py, + self.0 + .values() + .into_iter() + .map(|v| Value(v.clone()).into_py(py)), // FIXME: do not clone + ); + Ok(res.into_py(py)) + } +} + +pub struct Value(databend_driver::Value); + +impl IntoPy for Value { + fn into_py(self, py: Python<'_>) -> PyObject { + match self.0 { + databend_driver::Value::Null => py.None(), + databend_driver::Value::EmptyArray => { + let list = PyList::empty(py); + list.into_py(py) + } + databend_driver::Value::EmptyMap => { + let dict = PyDict::new(py); + dict.into_py(py) + } + databend_driver::Value::Boolean(b) => b.into_py(py), + databend_driver::Value::String(s) => s.into_py(py), + databend_driver::Value::Number(n) => { + let v = NumberValue(n); + v.into_py(py) + } + databend_driver::Value::Timestamp(_) => { + let s = self.0.to_string(); + s.into_py(py) + } + databend_driver::Value::Date(_) => { + let s = self.0.to_string(); + s.into_py(py) + } + databend_driver::Value::Array(inner) => { + let list = PyList::new(py, inner.into_iter().map(|v| Value(v).into_py(py))); + list.into_py(py) + } + databend_driver::Value::Map(inner) => { + let dict = PyDict::new(py); + for (k, v) in inner { + dict.set_item(Value(k).into_py(py), Value(v).into_py(py)) + .unwrap(); + } + dict.into_py(py) + } + databend_driver::Value::Tuple(inner) => { + let tuple = PyTuple::new(py, inner.into_iter().map(|v| Value(v).into_py(py))); + tuple.into_py(py) + } + databend_driver::Value::Bitmap(s) => s.into_py(py), + databend_driver::Value::Variant(s) => s.into_py(py), + } + } +} + +pub struct NumberValue(databend_driver::NumberValue); + +impl IntoPy for NumberValue { + fn into_py(self, py: Python<'_>) -> PyObject { + match self.0 { + databend_driver::NumberValue::Int8(i) => i.into_py(py), + databend_driver::NumberValue::Int16(i) => i.into_py(py), + databend_driver::NumberValue::Int32(i) => i.into_py(py), + databend_driver::NumberValue::Int64(i) => i.into_py(py), + databend_driver::NumberValue::UInt8(i) => i.into_py(py), + databend_driver::NumberValue::UInt16(i) => i.into_py(py), + databend_driver::NumberValue::UInt32(i) => i.into_py(py), + databend_driver::NumberValue::UInt64(i) => i.into_py(py), + databend_driver::NumberValue::Float32(i) => i.into_py(py), + databend_driver::NumberValue::Float64(i) => i.into_py(py), + databend_driver::NumberValue::Decimal128(_, _) => { + let s = self.0.to_string(); + s.into_py(py) + } + databend_driver::NumberValue::Decimal256(_, _) => { + let s = self.0.to_string(); + s.into_py(py) + } + } + } } diff --git a/bindings/python/tests/binding.feature b/bindings/python/tests/binding.feature deleted file mode 100644 index 6eff9b761..000000000 --- a/bindings/python/tests/binding.feature +++ /dev/null @@ -1,22 +0,0 @@ -# Copyright 2021 Datafuse Labs -# -# Licensed 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. - - -Feature: Databend-Driver Binding - - Scenario: Databend-Driver Async Operations - Given A new Databend-Driver Async Connector - When Async exec "CREATE TABLE if not exists test_data (x Int32,y VARCHAR)" - When Async exec "INSERT INTO test_data(x,y) VALUES(1,'xx')" - Then The select "SELECT * FROM test_data" should run diff --git a/bindings/python/tests/binding.feature b/bindings/python/tests/binding.feature new file mode 120000 index 000000000..fcb71d389 --- /dev/null +++ b/bindings/python/tests/binding.feature @@ -0,0 +1 @@ +../../tests/features/binding.feature \ No newline at end of file diff --git a/bindings/python/tests/steps/binding.py b/bindings/python/tests/steps/binding.py index 364438ccb..0bae6f2d2 100644 --- a/bindings/python/tests/steps/binding.py +++ b/bindings/python/tests/steps/binding.py @@ -19,22 +19,47 @@ import databend_driver -@given("A new Databend-Driver Async Connector") +@given("A new Databend Driver Client") @async_run_until_complete async def _(context): dsn = os.getenv( "TEST_DATABEND_DSN", "databend+http://root:root@localhost:8000/?sslmode=disable" ) - context.ad = databend_driver.AsyncDatabendDriver(dsn) + client = databend_driver.AsyncDatabendClient(dsn) + context.conn = await client.get_conn() -@when('Async exec "{sql}"') +@when("Create a test table") @async_run_until_complete -async def _(context, sql): - await context.ad.exec(sql) +async def _(context): + # TODO: + pass + + +@then("Select string {input} should be equal to {output}") +@async_run_until_complete +async def _(context, input, output): + row = await context.conn.query_row(f"SELECT '{input}'") + value = row.values()[0] + assert output == value -@then('The select "{select_sql}" should run') +@then("Select numbers should iterate all rows") @async_run_until_complete -async def _(context, select_sql): - await context.ad.exec(select_sql) +async def _(context): + # TODO: + pass + + +@then("Insert and Select should be equal") +@async_run_until_complete +async def _(context): + # TODO: + pass + + +@then("Stream load and Select should be equal") +@async_run_until_complete +async def _(context): + # TODO: + pass diff --git a/driver/src/conn.rs b/driver/src/conn.rs index f732fedee..f724d5133 100644 --- a/driver/src/conn.rs +++ b/driver/src/conn.rs @@ -33,6 +33,7 @@ use databend_sql::value::{NumberValue, Value}; use crate::rest_api::RestAPIConnection; +#[derive(Clone)] pub struct Client { dsn: String, }