This is an automated email from the ASF dual-hosted git repository.

Xuanwo pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/opendal.git


The following commit(s) were added to refs/heads/main by this push:
     new 311f324cc fix(services): improve stat implementation (#7857)
311f324cc is described below

commit 311f324cca8d82a16880b93b0c387eb0faa709d0
Author: Erick Guan <[email protected]>
AuthorDate: Mon Jul 6 16:00:06 2026 +0800

    fix(services): improve stat implementation (#7857)
    
    * fix: avoid full value reads in kv stat
    
    * harden sql services and improve d1 and gridfs
    
    * Fix linting
---
 core/services/d1/src/backend.rs         |   7 +--
 core/services/d1/src/core.rs            |  49 +++++++++++----
 core/services/d1/src/model.rs           |  60 ++++++++++++++++++
 core/services/gridfs/src/backend.rs     |   7 +--
 core/services/gridfs/src/core.rs        |  30 ++++++---
 core/services/mongodb/src/backend.rs    |   7 +--
 core/services/mongodb/src/core.rs       |  60 ++++++++++++++++++
 core/services/mysql/src/backend.rs      |   7 +--
 core/services/mysql/src/core.rs         |  34 ++++++++++-
 core/services/postgresql/src/backend.rs |   7 +--
 core/services/postgresql/src/core.rs    |  32 +++++++++-
 core/services/redis/src/backend.rs      |   8 +--
 core/services/sqlite/src/backend.rs     |   9 +--
 core/services/sqlite/src/core.rs        | 104 +++++++++++++++++++++++---------
 core/services/surrealdb/src/backend.rs  |   7 +--
 core/services/surrealdb/src/core.rs     |  36 +++++++++++
 16 files changed, 380 insertions(+), 84 deletions(-)

diff --git a/core/services/d1/src/backend.rs b/core/services/d1/src/backend.rs
index 7c67dc377..ad8347b17 100644
--- a/core/services/d1/src/backend.rs
+++ b/core/services/d1/src/backend.rs
@@ -249,10 +249,9 @@ impl Service for D1Backend {
         if p == build_abs_path(&self.root, "") {
             Ok(RpStat::new(Metadata::new(EntryMode::DIR)))
         } else {
-            let bs = self.core.get(ctx, &p).await?;
-            match bs {
-                Some(bs) => Ok(RpStat::new(
-                    
Metadata::new(EntryMode::FILE).with_content_length(bs.len() as u64),
+            match self.core.get_length(ctx, &p).await? {
+                Some(length) => Ok(RpStat::new(
+                    Metadata::new(EntryMode::FILE).with_content_length(length 
as u64),
                 )),
                 None => Err(Error::new(ErrorKind::NotFound, "kv not found in 
d1")),
             }
diff --git a/core/services/d1/src/core.rs b/core/services/d1/src/core.rs
index 86997991d..9e99f5e59 100644
--- a/core/services/d1/src/core.rs
+++ b/core/services/d1/src/core.rs
@@ -47,6 +47,8 @@ impl Debug for D1Core {
     }
 }
 
+const CLOUDFLARE_API_BASE_URL: &str = "https://api.cloudflare.com/client/v4";;
+
 impl D1Core {
     fn create_d1_query_request(
         &self,
@@ -59,11 +61,7 @@ impl D1Core {
             "/accounts/{}/d1/database/{}/query",
             self.account_id, self.database_id
         );
-        let url: String = format!(
-            "{}{}",
-            "https://api.cloudflare.com/client/v4";,
-            percent_encode_path(&p)
-        );
+        let url: String = format!("{}{}", CLOUDFLARE_API_BASE_URL, 
percent_encode_path(&p));
 
         let mut req = Request::post(&url);
         if let Some(auth) = &self.authorization {
@@ -84,8 +82,13 @@ impl D1Core {
     }
 
     pub async fn get(&self, ctx: &OperationContext, path: &str) -> 
Result<Option<Buffer>> {
+        // d1 follows SQLite SQL syntax, we use `"` for identifier quote. A 
quoted identifier is case-sensitive and
+        // can contain special characters.
+        // Read more 
https://www.sqlite.org/quirks.html#double_quoted_string_literals
+        //
+        // We uses identifier quote for trusted table and field configuration 
to ensure correctness.
         let query = format!(
-            "SELECT {} FROM {} WHERE {} = ? LIMIT 1",
+            r#"SELECT "{}" FROM "{}" WHERE "{}" = ? LIMIT 1"#,
             self.value_field, self.table, self.key_field
         );
         let req =
@@ -104,15 +107,36 @@ impl D1Core {
         }
     }
 
+    pub async fn get_length(&self, ctx: &OperationContext, path: &str) -> 
Result<Option<usize>> {
+        let query = format!(
+            r#"SELECT LENGTH(CAST("{}" AS BLOB)) AS "content_length" FROM "{}" 
WHERE "{}" = ? LIMIT 1"#,
+            self.value_field, self.table, self.key_field
+        );
+        let req =
+            self.create_d1_query_request(&query, vec![path.into()], 
Operation::Stat, "Stat")?;
+
+        let resp = ctx.http_transport().send(req).await?;
+        let status = resp.status();
+        match status {
+            StatusCode::OK | StatusCode::PARTIAL_CONTENT => {
+                let body = resp.into_body();
+                let bs = body.to_bytes();
+                let d1_response = D1Response::parse(&bs)?;
+                d1_response.get_usize_result("content_length")
+            }
+            _ => Err(parse_error(resp)),
+        }
+    }
+
     pub async fn set(&self, ctx: &OperationContext, path: &str, value: Buffer) 
-> Result<()> {
         let table = &self.table;
         let key_field = &self.key_field;
         let value_field = &self.value_field;
         let query = format!(
-            "INSERT INTO {table} ({key_field}, {value_field}) \
-                VALUES (?, ?) \
-                ON CONFLICT ({key_field}) \
-                    DO UPDATE SET {value_field} = EXCLUDED.{value_field}",
+            r#"INSERT INTO "{table}" ("{key_field}", "{value_field}") \
+                VALUES ('?', ?) \
+                ON CONFLICT ("{key_field}") \
+                    DO UPDATE SET "{value_field}" = EXCLUDED."{value_field}""#,
         );
 
         let params = vec![path.into(), value.to_vec().into()];
@@ -127,7 +151,10 @@ impl D1Core {
     }
 
     pub async fn delete(&self, ctx: &OperationContext, path: &str) -> 
Result<()> {
-        let query = format!("DELETE FROM {} WHERE {} = ?", self.table, 
self.key_field);
+        let query = format!(
+            r#"DELETE FROM "{}" WHERE "{}" = ?"#,
+            self.table, self.key_field
+        );
         let req =
             self.create_d1_query_request(&query, vec![path.into()], 
Operation::Delete, "Delete")?;
 
diff --git a/core/services/d1/src/model.rs b/core/services/d1/src/model.rs
index 138341497..4c20c19a1 100644
--- a/core/services/d1/src/model.rs
+++ b/core/services/d1/src/model.rs
@@ -71,6 +71,38 @@ impl D1Response {
             _ => None,
         }
     }
+
+    pub fn get_usize_result(&self, key: &str) -> Result<Option<usize>, Error> {
+        if self.result.is_empty() || self.result[0].results.is_empty() {
+            return Ok(None);
+        }
+        let result = &self.result[0].results[0];
+        let Some(value) = result.get(key) else {
+            return Ok(None);
+        };
+
+        match value {
+            Value::Number(n) => {
+                let value = n.as_u64().ok_or_else(|| {
+                    Error::new(
+                        opendal_core::ErrorKind::Unexpected,
+                        "d1 value length is invalid",
+                    )
+                })?;
+                value.try_into().map(Some).map_err(|err| {
+                    Error::new(
+                        opendal_core::ErrorKind::Unexpected,
+                        "d1 value length is invalid",
+                    )
+                    .set_source(err)
+                })
+            }
+            _ => Err(Error::new(
+                opendal_core::ErrorKind::Unexpected,
+                "d1 value length is invalid",
+            )),
+        }
+    }
 }
 
 #[derive(Deserialize, Debug)]
@@ -121,4 +153,32 @@ mod test {
         let response: D1Response = serde_json::from_str(data).unwrap();
         println!("{:?}", response.result[0].results[0]);
     }
+
+    #[test]
+    fn test_get_usize_result() {
+        let data = r#"
+        {
+            "result": [
+                {
+                    "results": [
+                        {
+                            "content_length": 6
+                        }
+                    ],
+                    "success": true,
+                    "meta": {}
+                }
+            ],
+            "success": true,
+            "errors": [],
+            "messages": []
+        }"#;
+
+        let response: D1Response = serde_json::from_str(data).unwrap();
+        assert_eq!(
+            response.get_usize_result("content_length").unwrap(),
+            Some(6)
+        );
+        assert_eq!(response.get_usize_result("missing").unwrap(), None);
+    }
 }
diff --git a/core/services/gridfs/src/backend.rs 
b/core/services/gridfs/src/backend.rs
index 2a14a879e..88200ca19 100644
--- a/core/services/gridfs/src/backend.rs
+++ b/core/services/gridfs/src/backend.rs
@@ -223,10 +223,9 @@ impl Service for GridfsBackend {
         if p == build_abs_path(&self.root, "") {
             Ok(RpStat::new(Metadata::new(EntryMode::DIR)))
         } else {
-            let bs = self.core.get(&p).await?;
-            match bs {
-                Some(bs) => Ok(RpStat::new(
-                    
Metadata::new(EntryMode::FILE).with_content_length(bs.len() as u64),
+            match self.core.get_length(&p).await? {
+                Some(len) => Ok(RpStat::new(
+                    Metadata::new(EntryMode::FILE).with_content_length(len as 
u64),
                 )),
                 None => Err(Error::new(ErrorKind::NotFound, "kv not found in 
gridfs")),
             }
diff --git a/core/services/gridfs/src/core.rs b/core/services/gridfs/src/core.rs
index 3f5f9a9bb..9ea608822 100644
--- a/core/services/gridfs/src/core.rs
+++ b/core/services/gridfs/src/core.rs
@@ -21,7 +21,7 @@ use futures::AsyncReadExt;
 use futures::AsyncWriteExt;
 use mea::once::OnceCell;
 use mongodb::bson::doc;
-use mongodb::gridfs::GridFsBucket;
+use mongodb::gridfs::{FilesCollectionDocument, GridFsBucket};
 use mongodb::options::ClientOptions;
 use mongodb::options::GridFsBucketOptions;
 use opendal_core::raw::*;
@@ -67,10 +67,18 @@ impl GridfsCore {
             .await
     }
 
+    async fn get_one_doc(
+        bucket: &GridFsBucket,
+        path: &str,
+    ) -> Result<Option<FilesCollectionDocument>> {
+        let filter = doc! { "filename": path };
+        let doc = bucket.find_one(filter).await.map_err(parse_mongodb_error)?;
+        Ok(doc)
+    }
+
     pub async fn get(&self, path: &str) -> Result<Option<Buffer>> {
         let bucket = self.get_bucket().await?;
-        let filter = doc! { "filename": path };
-        let Some(doc) = 
bucket.find_one(filter).await.map_err(parse_mongodb_error)? else {
+        let Some(doc) = Self::get_one_doc(bucket, path).await? else {
             return Ok(None);
         };
 
@@ -87,12 +95,21 @@ impl GridfsCore {
         Ok(Some(Buffer::from(destination)))
     }
 
+    /// Get the byte length of the file.
+    pub async fn get_length(&self, path: &str) -> Result<Option<usize>> {
+        let bucket = self.get_bucket().await?;
+        let Some(doc) = Self::get_one_doc(bucket, path).await? else {
+            return Ok(None);
+        };
+
+        Ok(Some(doc.length as usize))
+    }
+
     pub async fn set(&self, path: &str, value: Buffer) -> Result<()> {
         let bucket = self.get_bucket().await?;
 
         // delete old file if exists
-        let filter = doc! { "filename": path };
-        if let Some(doc) = 
bucket.find_one(filter).await.map_err(parse_mongodb_error)? {
+        if let Some(doc) = Self::get_one_doc(bucket, path).await? {
             let file_id = doc.id;
             bucket.delete(file_id).await.map_err(parse_mongodb_error)?;
         };
@@ -113,8 +130,7 @@ impl GridfsCore {
 
     pub async fn delete(&self, path: &str) -> Result<()> {
         let bucket = self.get_bucket().await?;
-        let filter = doc! { "filename": path };
-        let Some(doc) = 
bucket.find_one(filter).await.map_err(parse_mongodb_error)? else {
+        let Some(doc) = Self::get_one_doc(bucket, path).await? else {
             return Ok(());
         };
 
diff --git a/core/services/mongodb/src/backend.rs 
b/core/services/mongodb/src/backend.rs
index 1fa4b4b4f..f792db5b6 100644
--- a/core/services/mongodb/src/backend.rs
+++ b/core/services/mongodb/src/backend.rs
@@ -241,10 +241,9 @@ impl Service for MongodbBackend {
         if p == build_abs_path(&self.root, "") {
             Ok(RpStat::new(Metadata::new(EntryMode::DIR)))
         } else {
-            let bs = self.core.get(&p).await?;
-            match bs {
-                Some(bs) => Ok(RpStat::new(
-                    
Metadata::new(EntryMode::FILE).with_content_length(bs.len() as u64),
+            match self.core.get_length(&p).await? {
+                Some(length) => Ok(RpStat::new(
+                    Metadata::new(EntryMode::FILE).with_content_length(length 
as u64),
                 )),
                 None => Err(Error::new(ErrorKind::NotFound, "kv not found in 
mongodb")),
             }
diff --git a/core/services/mongodb/src/core.rs 
b/core/services/mongodb/src/core.rs
index 99c9d4a13..b4febd45b 100644
--- a/core/services/mongodb/src/core.rs
+++ b/core/services/mongodb/src/core.rs
@@ -19,6 +19,7 @@ use std::fmt::Debug;
 
 use mea::once::OnceCell;
 use mongodb::bson::Binary;
+use mongodb::bson::Bson;
 use mongodb::bson::Document;
 use mongodb::bson::doc;
 use mongodb::options::ClientOptions;
@@ -80,6 +81,32 @@ impl MongodbCore {
         }
     }
 
+    pub async fn get_length(&self, path: &str) -> Result<Option<usize>> {
+        let collection = self.get_collection().await?;
+        let mut cursor = collection
+            .aggregate(vec![
+                doc! { "$match": { self.key_field.as_str(): path } },
+                doc! { "$limit": 1 },
+                doc! {
+                    "$project": {
+                        "_id": 0,
+                        "content_length": {
+                            "$binarySize": format!("${}", self.value_field)
+                        }
+                    }
+                },
+            ])
+            .await
+            .map_err(parse_mongodb_error)?;
+
+        if !cursor.advance().await.map_err(parse_mongodb_error)? {
+            return Ok(None);
+        }
+
+        let doc: Document = 
cursor.deserialize_current().map_err(parse_mongodb_error)?;
+        parse_bson_usize(doc.get("content_length"))
+    }
+
     pub async fn set(&self, path: &str, value: Buffer) -> Result<()> {
         let collection = self.get_collection().await?;
         let filter = doc! { self.key_field.as_str(): path };
@@ -111,3 +138,36 @@ fn parse_mongodb_error(err: mongodb::error::Error) -> 
Error {
 fn parse_bson_error(err: mongodb::bson::document::ValueAccessError) -> Error {
     Error::new(ErrorKind::Unexpected, "bson error").set_source(err)
 }
+
+fn parse_bson_usize(value: Option<&Bson>) -> Result<Option<usize>> {
+    let Some(value) = value else {
+        return Ok(None);
+    };
+
+    match value {
+        Bson::Int32(v) => (*v).try_into().map(Some).map_err(|err| {
+            Error::new(ErrorKind::Unexpected, "mongodb value length is 
invalid").set_source(err)
+        }),
+        Bson::Int64(v) => (*v).try_into().map(Some).map_err(|err| {
+            Error::new(ErrorKind::Unexpected, "mongodb value length is 
invalid").set_source(err)
+        }),
+        _ => Err(Error::new(
+            ErrorKind::Unexpected,
+            "mongodb value length is invalid",
+        )),
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn test_parse_bson_usize() {
+        assert_eq!(parse_bson_usize(Some(&Bson::Int32(6))).unwrap(), Some(6));
+        assert_eq!(parse_bson_usize(Some(&Bson::Int64(6))).unwrap(), Some(6));
+        assert_eq!(parse_bson_usize(None).unwrap(), None);
+        assert!(parse_bson_usize(Some(&Bson::Int32(-1))).is_err());
+        
assert!(parse_bson_usize(Some(&Bson::String("6".to_string()))).is_err());
+    }
+}
diff --git a/core/services/mysql/src/backend.rs 
b/core/services/mysql/src/backend.rs
index 1dabe6b8f..da2b672f2 100644
--- a/core/services/mysql/src/backend.rs
+++ b/core/services/mysql/src/backend.rs
@@ -222,10 +222,9 @@ impl Service for MysqlBackend {
         if p == build_abs_path(&self.root, "") {
             Ok(RpStat::new(Metadata::new(EntryMode::DIR)))
         } else {
-            let bs = self.core.get(&p).await?;
-            match bs {
-                Some(bs) => Ok(RpStat::new(
-                    
Metadata::new(EntryMode::FILE).with_content_length(bs.len() as u64),
+            match self.core.get_length(&p).await? {
+                Some(length) => Ok(RpStat::new(
+                    Metadata::new(EntryMode::FILE).with_content_length(length 
as u64),
                 )),
                 None => Err(Error::new(ErrorKind::NotFound, "kv not found in 
mysql")),
             }
diff --git a/core/services/mysql/src/core.rs b/core/services/mysql/src/core.rs
index f977eba41..961c29807 100644
--- a/core/services/mysql/src/core.rs
+++ b/core/services/mysql/src/core.rs
@@ -45,6 +45,16 @@ impl MysqlCore {
     pub async fn get(&self, path: &str) -> Result<Option<Buffer>> {
         let pool = self.get_client().await?;
 
+        // MySQL uses a backtick for identifier quote. An identifier may or 
may not be case-sensitive,
+        // depending on database configuration. Only a quoted identifier can 
have special characters.
+        // Read more:
+        // https://dev.mysql.com/doc/refman/9.7/en/identifiers.html
+        // 
https://dev.mysql.com/doc/refman/9.7/en/identifier-case-sensitivity.html
+        //
+        // We use:
+        // - formatted, quoted identifiers for trusted table and field 
configuration
+        // - bind parameters for values to avoid malformed SQL and SQL 
injection
+        // to ensure correctness.
         let value: Option<Vec<u8>> = sqlx::query_scalar(&format!(
             "SELECT `{}` FROM `{}` WHERE `{}` = ? LIMIT 1",
             self.value_field, self.table, self.key_field
@@ -57,12 +67,34 @@ impl MysqlCore {
         Ok(value.map(Buffer::from))
     }
 
+    pub async fn get_length(&self, path: &str) -> Result<Option<usize>> {
+        let pool = self.get_client().await?;
+
+        let value: Option<i64> = sqlx::query_scalar(&format!(
+            "SELECT OCTET_LENGTH(`{}`) FROM `{}` WHERE `{}` = ? LIMIT 1",
+            self.value_field, self.table, self.key_field
+        ))
+        .bind(path)
+        .fetch_optional(pool)
+        .await
+        .map_err(parse_mysql_error)?;
+
+        value
+            .map(|v| {
+                v.try_into().map_err(|err| {
+                    Error::new(ErrorKind::Unexpected, "mysql value length is 
invalid")
+                        .set_source(err)
+                })
+            })
+            .transpose()
+    }
+
     pub async fn set(&self, path: &str, value: Buffer) -> Result<()> {
         let pool = self.get_client().await?;
 
         sqlx::query(&format!(
             r#"INSERT INTO `{}` (`{}`, `{}`) VALUES (?, ?)
-            ON DUPLICATE KEY UPDATE `{}` = VALUES({})"#,
+            ON DUPLICATE KEY UPDATE `{}` = VALUES(`{}`)"#,
             self.table, self.key_field, self.value_field, self.value_field, 
self.value_field
         ))
         .bind(path)
diff --git a/core/services/postgresql/src/backend.rs 
b/core/services/postgresql/src/backend.rs
index 04f3aa8eb..ebd33a007 100644
--- a/core/services/postgresql/src/backend.rs
+++ b/core/services/postgresql/src/backend.rs
@@ -214,10 +214,9 @@ impl Service for PostgresqlBackend {
         if p == build_abs_path(&self.root, "") {
             Ok(RpStat::new(Metadata::new(EntryMode::DIR)))
         } else {
-            let bs = self.core.get(&p).await?;
-            match bs {
-                Some(bs) => Ok(RpStat::new(
-                    
Metadata::new(EntryMode::FILE).with_content_length(bs.len() as u64),
+            match self.core.get_length(&p).await? {
+                Some(length) => Ok(RpStat::new(
+                    Metadata::new(EntryMode::FILE).with_content_length(length 
as u64),
                 )),
                 None => Err(Error::new(
                     ErrorKind::NotFound,
diff --git a/core/services/postgresql/src/core.rs 
b/core/services/postgresql/src/core.rs
index 9733b4479..ff6107ab1 100644
--- a/core/services/postgresql/src/core.rs
+++ b/core/services/postgresql/src/core.rs
@@ -45,6 +45,14 @@ impl PostgresqlCore {
     pub async fn get(&self, path: &str) -> Result<Option<Buffer>> {
         let pool = self.get_client().await?;
 
+        // PostgreSQL uses `"` for identifier quote. A quoted identifier is 
case-sensitive and
+        // can contain special characters.
+        // Read more 
https://www.postgresql.org/docs/18/sql-syntax-lexical.html#SQL-SYNTAX-IDENTIFIERS
+        //
+        // We use:
+        // - formatted, quoted identifiers for trusted table and field 
configuration
+        // - bind parameters for values to avoid malformed SQL and SQL 
injection
+        // to ensure correctness.
         let value: Option<Vec<u8>> = sqlx::query_scalar(&format!(
             r#"SELECT "{}" FROM "{}" WHERE "{}" = $1 LIMIT 1"#,
             self.value_field, self.table, self.key_field
@@ -57,6 +65,28 @@ impl PostgresqlCore {
         Ok(value.map(Buffer::from))
     }
 
+    pub async fn get_length(&self, path: &str) -> Result<Option<usize>> {
+        let pool = self.get_client().await?;
+
+        let value: Option<i64> = sqlx::query_scalar(&format!(
+            r#"SELECT OCTET_LENGTH("{}")::BIGINT FROM "{}" WHERE "{}" = $1 
LIMIT 1"#,
+            self.value_field, self.table, self.key_field
+        ))
+        .bind(path)
+        .fetch_optional(pool)
+        .await
+        .map_err(parse_postgres_error)?;
+
+        value
+            .map(|v| {
+                v.try_into().map_err(|err| {
+                    Error::new(ErrorKind::Unexpected, "postgresql value length 
is invalid")
+                        .set_source(err)
+                })
+            })
+            .transpose()
+    }
+
     pub async fn set(&self, path: &str, value: Buffer) -> Result<()> {
         let pool = self.get_client().await?;
 
@@ -82,7 +112,7 @@ impl PostgresqlCore {
         let pool = self.get_client().await?;
 
         sqlx::query(&format!(
-            "DELETE FROM {} WHERE {} = $1",
+            r#"DELETE FROM "{}" WHERE "{}" = $1"#,
             self.table, self.key_field
         ))
         .bind(path)
diff --git a/core/services/redis/src/backend.rs 
b/core/services/redis/src/backend.rs
index 57375b9b6..67aa3896d 100644
--- a/core/services/redis/src/backend.rs
+++ b/core/services/redis/src/backend.rs
@@ -340,15 +340,15 @@ impl Service for RedisBackend {
         if p == build_abs_path(&self.root, "") {
             Ok(RpStat::new(Metadata::new(EntryMode::DIR)))
         } else {
-            let bs = self.core.get(&p).await?;
-            match bs {
-                Some(bs) => Ok(RpStat::new(
-                    
Metadata::new(EntryMode::FILE).with_content_length(bs.len() as u64),
+            match self.core.len(&p).await? {
+                Some(len) => Ok(RpStat::new(
+                    Metadata::new(EntryMode::FILE).with_content_length(len as 
u64),
                 )),
                 None => Err(Error::new(ErrorKind::NotFound, "key not found in 
redis")),
             }
         }
     }
+
     fn read(&self, _ctx: &OperationContext, path: &str, args: OpRead) -> 
Result<Self::Reader> {
         let output: oio::StreamReader<RedisReader> = {
             Ok(oio::StreamReader::new(RedisReader::new(
diff --git a/core/services/sqlite/src/backend.rs 
b/core/services/sqlite/src/backend.rs
index 5a96dae0e..b32a9164e 100644
--- a/core/services/sqlite/src/backend.rs
+++ b/core/services/sqlite/src/backend.rs
@@ -240,14 +240,7 @@ impl Service for SqliteBackend {
                     } else {
                         format!("{}/", p)
                     };
-                    let count: i64 = sqlx::query_scalar(&format!(
-                        "SELECT COUNT(*) FROM `{}` WHERE `{}` LIKE $1 LIMIT 1",
-                        self.core.table, self.core.key_field
-                    ))
-                    .bind(format!("{}%", dir_path))
-                    .fetch_one(self.core.get_client().await?)
-                    .await
-                    .map_err(parse_sqlite_error)?;
+                    let count = self.core.count_under(&dir_path).await?;
 
                     if count > 0 {
                         // Directory exists (has children)
diff --git a/core/services/sqlite/src/core.rs b/core/services/sqlite/src/core.rs
index 16ee5f68a..478884595 100644
--- a/core/services/sqlite/src/core.rs
+++ b/core/services/sqlite/src/core.rs
@@ -48,8 +48,17 @@ impl SqliteCore {
     pub async fn get(&self, path: &str) -> Result<Option<Buffer>> {
         let pool = self.get_client().await?;
 
+        // SQLite prefers using standard `"` for identifier quotes instead of
+        // MySQL-compatible backtick ` or [] quotes.
+        // Identifier quotes are case-sensitive and allow special characters.
+        // Read more 
https://www.sqlite.org/quirks.html#double_quoted_string_literals_are_accepted
+        //
+        // We use:
+        // - formatted, quoted identifiers for trusted table and field 
configuration
+        // - bind parameters for values to avoid malformed SQL and SQL 
injection
+        // to ensure correctness.
         let value: Option<Vec<u8>> = sqlx::query_scalar(&format!(
-            "SELECT `{}` FROM `{}` WHERE `{}` = $1 LIMIT 1",
+            r#"SELECT "{}" FROM "{}" WHERE "{}" = $1 LIMIT 1"#,
             self.value_field, self.table, self.key_field
         ))
         .bind(path)
@@ -64,7 +73,7 @@ impl SqliteCore {
         let pool = self.get_client().await?;
 
         let value: Option<i64> = sqlx::query_scalar(&format!(
-            "SELECT LENGTH(CAST(`{}` AS BLOB)) FROM `{}` WHERE `{}` = $1 LIMIT 
1",
+            r#"SELECT LENGTH(CAST("{}" AS BLOB)) FROM "{}" WHERE "{}" = $1 
LIMIT 1"#,
             self.value_field, self.table, self.key_field
         ))
         .bind(path)
@@ -82,6 +91,19 @@ impl SqliteCore {
             .transpose()
     }
 
+    pub async fn count_under(&self, path: &str) -> Result<i64> {
+        let pool = self.get_client().await?;
+
+        sqlx::query_scalar(&format!(
+            r#"SELECT COUNT(*) FROM "{}" WHERE "{}" LIKE $1 LIMIT 1"#,
+            self.table, self.key_field
+        ))
+        .bind(format!("{}%", path))
+        .fetch_one(pool)
+        .await
+        .map_err(parse_sqlite_error)
+    }
+
     pub async fn get_range(
         &self,
         path: &str,
@@ -89,31 +111,57 @@ impl SqliteCore {
         limit: Option<isize>,
     ) -> Result<Option<(Buffer, u64)>> {
         let pool = self.get_client().await?;
-        let query = match limit {
-            Some(limit) => format!(
-                "SELECT SUBSTR(CAST(`{}` AS BLOB), {}, {}), LENGTH(CAST(`{}` 
AS BLOB)) FROM `{}` WHERE `{}` = $1 LIMIT 1",
-                self.value_field,
-                start + 1,
-                limit,
-                self.value_field,
-                self.table,
-                self.key_field
-            ),
-            None => format!(
-                "SELECT SUBSTR(CAST(`{}` AS BLOB), {}), LENGTH(CAST(`{}` AS 
BLOB)) FROM `{}` WHERE `{}` = $1 LIMIT 1",
-                self.value_field,
-                start + 1,
-                self.value_field,
-                self.table,
-                self.key_field
-            ),
+        if start < 0 || limit.is_some_and(|v| v < 0) {
+            return Err(Error::new(
+                ErrorKind::Unexpected,
+                "sqlite range contains negative value",
+            ));
+        }
+
+        let start = start.checked_add(1).ok_or_else(|| {
+            Error::new(
+                ErrorKind::Unexpected,
+                "sqlite range start exceeds supported value",
+            )
+        })?;
+        let start: i64 = start.try_into().map_err(|err| {
+            Error::new(
+                ErrorKind::Unexpected,
+                "sqlite range start exceeds supported value",
+            )
+            .set_source(err)
+        })?;
+        let value = match limit {
+            Some(limit) => {
+                let limit: i64 = limit.try_into().map_err(|err| {
+                    Error::new(
+                        ErrorKind::Unexpected,
+                        "sqlite range size exceeds supported value",
+                    )
+                    .set_source(err)
+                })?;
+                sqlx::query_as(&format!(
+                    r#"SELECT SUBSTR(CAST("{}" AS BLOB), $1, $2), 
LENGTH(CAST("{}" AS BLOB)) FROM "{}" WHERE "{}" = $3 LIMIT 1"#,
+                    self.value_field, self.value_field, self.table, 
self.key_field
+                ))
+                .bind(start)
+                .bind(limit)
+                .bind(path)
+                .fetch_optional(pool)
+                .await
+            }
+            None => {
+                sqlx::query_as(&format!(
+                    r#"SELECT SUBSTR(CAST("{}" AS BLOB), $1), LENGTH(CAST("{}" 
AS BLOB)) FROM "{}" WHERE "{}" = $2 LIMIT 1"#,
+                    self.value_field, self.value_field, self.table, 
self.key_field
+                ))
+                .bind(start)
+                .bind(path)
+                .fetch_optional(pool)
+                .await
+            }
         };
-
-        let value: Option<(Vec<u8>, i64)> = sqlx::query_as(&query)
-            .bind(path)
-            .fetch_optional(pool)
-            .await
-            .map_err(parse_sqlite_error)?;
+        let value: Option<(Vec<u8>, i64)> = value.map_err(parse_sqlite_error)?;
 
         Ok(value.map(|(bs, size)| (Buffer::from(bs), size as u64)))
     }
@@ -122,7 +170,7 @@ impl SqliteCore {
         let pool = self.get_client().await?;
 
         sqlx::query(&format!(
-            "INSERT OR REPLACE INTO `{}` (`{}`, `{}`) VALUES ($1, $2)",
+            r#"INSERT OR REPLACE INTO "{}" ("{}", "{}") VALUES ($1, $2)"#,
             self.table, self.key_field, self.value_field,
         ))
         .bind(path)
@@ -138,7 +186,7 @@ impl SqliteCore {
         let pool = self.get_client().await?;
 
         sqlx::query(&format!(
-            "DELETE FROM `{}` WHERE `{}` = $1",
+            r#"DELETE FROM "{}" WHERE "{}" = $1"#,
             self.table, self.key_field
         ))
         .bind(path)
diff --git a/core/services/surrealdb/src/backend.rs 
b/core/services/surrealdb/src/backend.rs
index b6b5e5247..d993f679f 100644
--- a/core/services/surrealdb/src/backend.rs
+++ b/core/services/surrealdb/src/backend.rs
@@ -268,10 +268,9 @@ impl Service for SurrealdbBackend {
         if p == build_abs_path(&self.root, "") {
             Ok(RpStat::new(Metadata::new(EntryMode::DIR)))
         } else {
-            let bs = self.core.get(&p).await?;
-            match bs {
-                Some(bs) => Ok(RpStat::new(
-                    
Metadata::new(EntryMode::FILE).with_content_length(bs.len() as u64),
+            match self.core.get_length(&p).await? {
+                Some(length) => Ok(RpStat::new(
+                    Metadata::new(EntryMode::FILE).with_content_length(length 
as u64),
                 )),
                 None => Err(Error::new(ErrorKind::NotFound, "kv not found in 
surrealdb")),
             }
diff --git a/core/services/surrealdb/src/core.rs 
b/core/services/surrealdb/src/core.rs
index 365f74afb..f92a3ebf2 100644
--- a/core/services/surrealdb/src/core.rs
+++ b/core/services/surrealdb/src/core.rs
@@ -115,6 +115,42 @@ impl SurrealdbCore {
         Ok(value.map(Buffer::from))
     }
 
+    pub async fn get_length(&self, path: &str) -> Result<Option<usize>> {
+        let query: String = if self.key_field == "id" {
+            "SELECT bytes::len(type::field($value_field)) AS content_length 
FROM type::thing($table, $path)"
+                .to_string()
+        } else {
+            format!(
+                "SELECT bytes::len(type::field($value_field)) AS 
content_length FROM type::table($table) WHERE {} = $path LIMIT 1",
+                self.key_field
+            )
+        };
+
+        let mut result = self
+            .get_connection()
+            .await?
+            .query(query)
+            .bind(("namespace", "opendal"))
+            .bind(("path", path.to_string()))
+            .bind(("table", self.table.to_string()))
+            .bind(("value_field", self.value_field.to_string()))
+            .await
+            .map_err(parse_surrealdb_error)?;
+
+        let value: Option<i64> = result
+            .take((0, "content_length"))
+            .map_err(parse_surrealdb_error)?;
+
+        value
+            .map(|v| {
+                v.try_into().map_err(|err| {
+                    Error::new(ErrorKind::Unexpected, "surrealdb value length 
is invalid")
+                        .set_source(err)
+                })
+            })
+            .transpose()
+    }
+
     pub async fn set(&self, path: &str, value: Buffer) -> Result<()> {
         let query = format!(
             "INSERT INTO {} ({}, {}) \

Reply via email to