diff --git a/Cargo.lock b/Cargo.lock
index 36ee6d77ffd..dc0dcf2561c 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -15125,6 +15125,7 @@ dependencies = [
"catalog",
"chrono",
"clap",
+ "cli",
"client",
"cmd",
"common-base",
diff --git a/config/config.md b/config/config.md
index 651cbec6e85..7385c8efb0d 100644
--- a/config/config.md
+++ b/config/config.md
@@ -15,6 +15,7 @@
| `default_timezone` | String | Unset | The default timezone of the server. |
| `default_column_prefix` | String | Unset | The default column prefix for auto-created time index, value, and native histogram columns.
Legacy OTLP summary columns keep their historical `greptime_` prefix. |
| `auto_create_table` | Bool | `true` | Server-side global switch for auto table creation on write.
When `false`, a missing table is never auto-created even if the request sets the `auto_create_table` hint to `true`. Default: `true`. |
+| `experimental_metric_export` | Bool | `false` | Enables experimental Parquet COPY DATABASE using shared Metric physical scans.
Resume requires the previous export and storage writes to have ended; HTTP timeout is not confirmation. |
| `user_provider` | String | Unset | The user provider for authentication.
Examples: "static_user_provider:file:/path/to/users", "static_user_provider:cmd:greptime_user=greptime_pwd"
Password verifier formats: "plain:", "pbkdf2_sha256:::",
"mysql_native_password:",
"pg_scram_sha256::::"
"pbkdf2_sha256" and "pg_scram_sha256" protect passwords at rest, but cannot authenticate over MySQL's
native password handshake; a MySQL client must send the password in cleartext for such users.
"mysql_native_password" is MySQL-specific and cannot authenticate over PostgreSQL at all.
PostgreSQL SCRAM only covers "plain" and "pg_scram_sha256" users; if any user is "pbkdf2_sha256" or
"mysql_native_password", PostgreSQL falls back to cleartext password auth for every user.
For "pg_scram_sha256" users, keep the default iteration count (4096) and salt length (16): both are
observable in the SCRAM server-first message, and non-default values weaken resistance to username
enumeration. |
| `max_in_flight_write_bytes` | String | Unset | Maximum total memory for all concurrent write request bodies and messages (HTTP, gRPC, Flight).
Set to 0 to disable the limit. Default: "0" (unlimited) |
| `write_bytes_exhausted_policy` | String | Unset | Policy when write bytes quota is exhausted.
Options: "wait" (default, 10s timeout), "wait()" (e.g., "wait(30s)"), "fail" |
@@ -278,6 +279,7 @@
| `default_timezone` | String | Unset | The default timezone of the server. |
| `default_column_prefix` | String | Unset | The default column prefix for auto-created time index, value, and native histogram columns.
Legacy OTLP summary columns keep their historical `greptime_` prefix. |
| `auto_create_table` | Bool | `true` | Server-side global switch for auto table creation on write.
When `false`, a missing table is never auto-created even if the request sets the `auto_create_table` hint to `true`. Default: `true`. |
+| `experimental_metric_export` | Bool | `false` | Enables experimental Parquet COPY DATABASE using shared Metric physical scans.
Resume requires the previous export and storage writes to have ended; HTTP timeout is not confirmation. |
| `user_provider` | String | Unset | The user provider for authentication.
Examples: "static_user_provider:file:/path/to/users", "static_user_provider:cmd:greptime_user=greptime_pwd"
Password verifier formats: "plain:", "pbkdf2_sha256:::",
"mysql_native_password:",
"pg_scram_sha256::::"
"pbkdf2_sha256" and "pg_scram_sha256" protect passwords at rest, but cannot authenticate over MySQL's
native password handshake; a MySQL client must send the password in cleartext for such users.
"mysql_native_password" is MySQL-specific and cannot authenticate over PostgreSQL at all.
PostgreSQL SCRAM only covers "plain" and "pg_scram_sha256" users; if any user is "pbkdf2_sha256" or
"mysql_native_password", PostgreSQL falls back to cleartext password auth for every user.
For "pg_scram_sha256" users, keep the default iteration count (4096) and salt length (16): both are
observable in the SCRAM server-first message, and non-default values weaken resistance to username
enumeration. |
| `max_in_flight_write_bytes` | String | Unset | Maximum total memory for all concurrent write request bodies and messages (HTTP, gRPC, Flight).
Set to 0 to disable the limit. Default: "0" (unlimited) |
| `write_bytes_exhausted_policy` | String | Unset | Policy when write bytes quota is exhausted.
Options: "wait" (default, 10s timeout), "wait()" (e.g., "wait(30s)"), "fail" |
diff --git a/config/frontend.example.toml b/config/frontend.example.toml
index 95550931d46..043da57db74 100644
--- a/config/frontend.example.toml
+++ b/config/frontend.example.toml
@@ -11,6 +11,10 @@ default_column_prefix = "greptime"
## When `false`, a missing table is never auto-created even if the request sets the `auto_create_table` hint to `true`. Default: `true`.
#+ auto_create_table = true
+## Enables experimental Parquet COPY DATABASE using shared Metric physical scans.
+## Resume requires the previous export and storage writes to have ended; HTTP timeout is not confirmation.
+#+ experimental_metric_export = false
+
## The user provider for authentication.
## Examples: "static_user_provider:file:/path/to/users", "static_user_provider:cmd:greptime_user=greptime_pwd"
## Password verifier formats: "plain:", "pbkdf2_sha256:::",
diff --git a/config/standalone.example.toml b/config/standalone.example.toml
index 57893881146..cc4be3f5930 100644
--- a/config/standalone.example.toml
+++ b/config/standalone.example.toml
@@ -11,6 +11,10 @@ default_column_prefix = "greptime"
## When `false`, a missing table is never auto-created even if the request sets the `auto_create_table` hint to `true`. Default: `true`.
#+ auto_create_table = true
+## Enables experimental Parquet COPY DATABASE using shared Metric physical scans.
+## Resume requires the previous export and storage writes to have ended; HTTP timeout is not confirmation.
+#+ experimental_metric_export = false
+
## The user provider for authentication.
## Examples: "static_user_provider:file:/path/to/users", "static_user_provider:cmd:greptime_user=greptime_pwd"
## Password verifier formats: "plain:", "pbkdf2_sha256:::",
diff --git a/src/cli/src/data/export_v2.rs b/src/cli/src/data/export_v2.rs
index 9bdf3c63f5d..f9a3c2be095 100644
--- a/src/cli/src/data/export_v2.rs
+++ b/src/cli/src/data/export_v2.rs
@@ -37,6 +37,15 @@
//! --start-time 2025-01-01T00:00:00Z \
//! --end-time 2025-01-31T23:59:59Z
//! ```
+//!
+//! `--experimental-metric-export` enables shared Metric physical scans for Parquet.
+//! Enable `experimental_metric_export = true` on the frontend or standalone server
+//! and use an endpoint whose frontends all support and enable this option.
+//! The command checks the server capability before modifying the snapshot.
+//! Each snapshot path belongs to one export task. Resume requires stable source
+//! schemas/data and confirmation that the previous export and storage writes ended;
+//! an HTTP timeout does not establish that. Only unfinished chunks are cleaned and
+//! rerun. Completed chunks and the V2 snapshot/import format remain unchanged.
mod chunker;
mod command;
diff --git a/src/cli/src/data/export_v2/command.rs b/src/cli/src/data/export_v2/command.rs
index 4df741db347..fc958e9e3e0 100644
--- a/src/cli/src/data/export_v2/command.rs
+++ b/src/cli/src/data/export_v2/command.rs
@@ -20,9 +20,11 @@ use std::time::Duration;
use async_trait::async_trait;
use clap::{Parser, Subcommand};
+use common_catalog::consts::DEFAULT_SCHEMA_NAME;
use common_error::ext::BoxedError;
use common_telemetry::info;
use serde_json::Value;
+use servers::http::{ColumnSchema, GreptimeQueryOutput, OutputSchema};
use snafu::{OptionExt, ResultExt};
use crate::Tool;
@@ -289,6 +291,11 @@ pub struct ExportCreateCommand {
#[clap(long, value_enum, default_value = "parquet")]
format: DataFormat,
+ /// Use shared Metric physical scans (Parquet only). Resume requires that the previous
+ /// export and storage writes have ended; an HTTP timeout does not establish this.
+ #[clap(long)]
+ experimental_metric_export: bool,
+
/// Delete existing snapshot and recreate.
#[clap(long)]
force: bool,
@@ -379,9 +386,6 @@ impl ExportCreateCommand {
Some(self.schemas.clone())
};
- // Build storage
- let storage = OpenDalStorage::from_uri(&self.to, &self.storage).map_err(BoxedError::new)?;
-
// Build database client
let proxy = parse_proxy_opts(self.proxy.clone(), self.no_proxy)?;
let database_client = DatabaseClient::new(
@@ -393,12 +397,43 @@ impl ExportCreateCommand {
self.no_proxy,
);
+ // The filesystem storage constructor can create the snapshot root.
+ if self.experimental_metric_export {
+ if self.format != DataFormat::Parquet {
+ return crate::data::export_v2::error::MetricExportUnavailableSnafu
+ .fail()
+ .map_err(BoxedError::new);
+ }
+ let capability = database_client
+ .sql_response(
+ "SHOW VARIABLES experimental_metric_export",
+ DEFAULT_SCHEMA_NAME,
+ )
+ .await
+ .context(DatabaseSnafu)
+ .map_err(BoxedError::new)?;
+ let expected_schema = OutputSchema::new(vec![ColumnSchema::new(
+ "EXPERIMENTAL_METRIC_EXPORT".to_string(),
+ "String".to_string(),
+ )]);
+ if !matches!(capability.output(), [GreptimeQueryOutput::Records(records)]
+ if records.schema() == &expected_schema
+ && records.rows() == &vec![vec![Value::String("true".to_string())]])
+ {
+ return crate::data::export_v2::error::MetricExportUnavailableSnafu
+ .fail()
+ .map_err(BoxedError::new);
+ }
+ }
+ let storage = OpenDalStorage::from_uri(&self.to, &self.storage).map_err(BoxedError::new)?;
+
Ok(Box::new(ExportCreate {
config: ExportConfig {
catalog: self.catalog.clone(),
schemas,
schema_only: self.schema_only,
format: self.format,
+ experimental_metric_export: self.experimental_metric_export,
force: self.force,
time_range,
chunk_time_window: self.chunk_time_window,
@@ -426,6 +461,7 @@ struct ExportConfig {
schemas: Option>,
schema_only: bool,
format: DataFormat,
+ experimental_metric_export: bool,
force: bool,
time_range: TimeRange,
chunk_time_window: Option,
@@ -504,6 +540,8 @@ impl ExportCreate {
storage_config: &self.config.storage_config,
parallelism: self.config.parallelism,
chunk_parallelism: self.config.chunk_parallelism,
+ experimental_metric_export: self.config.experimental_metric_export,
+ resume: true,
},
progress.as_ref(),
)
@@ -533,6 +571,14 @@ impl ExportCreate {
self.config.chunk_time_window,
)?;
+ if self.config.experimental_metric_export {
+ for chunk in &manifest.chunks {
+ self.storage
+ .prepare_export_chunk(&schema_names, chunk.id, false)
+ .await?;
+ }
+ }
+
// 4. Write schema files
self.storage.write_schema(&schema_snapshot).await?;
info!("Exported {} schemas", schema_snapshot.schemas.len());
@@ -566,6 +612,8 @@ impl ExportCreate {
storage_config: &self.config.storage_config,
parallelism: self.config.parallelism,
chunk_parallelism: self.config.chunk_parallelism,
+ experimental_metric_export: self.config.experimental_metric_export,
+ resume: false,
},
progress.as_ref(),
)
@@ -1744,6 +1792,7 @@ mod tests {
schemas: None,
schema_only: false,
format: DataFormat::Parquet,
+ experimental_metric_export: false,
force: false,
time_range: TimeRange::unbounded(),
chunk_time_window: None,
@@ -1781,6 +1830,7 @@ mod tests {
]),
schema_only: false,
format: DataFormat::Parquet,
+ experimental_metric_export: false,
force: false,
time_range: TimeRange::unbounded(),
chunk_time_window: None,
@@ -1813,6 +1863,7 @@ mod tests {
schemas: None,
schema_only: false,
format: DataFormat::Parquet,
+ experimental_metric_export: false,
force: false,
time_range,
chunk_time_window: Some(Duration::from_secs(3600)),
@@ -1846,6 +1897,7 @@ mod tests {
schemas: None,
schema_only: false,
format: DataFormat::Csv,
+ experimental_metric_export: false,
force: false,
time_range: TimeRange::unbounded(),
chunk_time_window: None,
@@ -1881,6 +1933,7 @@ mod tests {
schemas: None,
schema_only: false,
format: DataFormat::Parquet,
+ experimental_metric_export: false,
force: false,
time_range: TimeRange::new(Some(start), Some(start)),
chunk_time_window: None,
diff --git a/src/cli/src/data/export_v2/coordinator.rs b/src/cli/src/data/export_v2/coordinator.rs
index 64aa4ce9946..b70897337a7 100644
--- a/src/cli/src/data/export_v2/coordinator.rs
+++ b/src/cli/src/data/export_v2/coordinator.rs
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-use common_telemetry::info;
+use common_telemetry::{error, info};
use futures::StreamExt;
use futures::stream::FuturesUnordered;
@@ -39,6 +39,8 @@ struct ExportContext<'a> {
schemas: Vec,
format: DataFormat,
parallelism: usize,
+ experimental_metric_export: bool,
+ resume: bool,
}
pub struct ExportDataOptions<'a> {
@@ -46,6 +48,8 @@ pub struct ExportDataOptions<'a> {
pub storage_config: &'a ObjectStoreConfig,
pub parallelism: usize,
pub chunk_parallelism: usize,
+ pub experimental_metric_export: bool,
+ pub resume: bool,
}
pub async fn export_data(
@@ -68,6 +72,8 @@ pub async fn export_data(
schemas: manifest.schemas.clone(),
format: manifest.format,
parallelism: options.parallelism,
+ experimental_metric_export: options.experimental_metric_export,
+ resume: options.resume,
};
// One progress unit per chunk. Already completed/skipped chunks from a
@@ -88,11 +94,11 @@ pub async fn export_data(
export_data_serial(&context, storage, manifest, progress).await
} else {
export_data_concurrent(
- &context,
storage,
manifest,
options.chunk_parallelism,
progress,
+ |id, range| export_chunk(&context, id, range),
)
.await
};
@@ -134,11 +140,17 @@ async fn export_data_serial(
};
manifest.touch();
- storage.write_manifest(manifest).await?;
- // The chunk is finalized (completed, skipped, or failed) and persisted.
- progress.inc(1);
-
- result?;
+ let persistence = storage.write_manifest(manifest).await;
+ if persistence.is_ok() {
+ progress.inc(1);
+ }
+ if let Err(err) = result {
+ if let Err(secondary) = persistence {
+ error!(secondary; "Failed to persist failed export chunk");
+ }
+ return Err(err);
+ }
+ persistence?;
}
Ok(())
@@ -155,39 +167,44 @@ async fn export_data_serial(
/// On the first chunk failure we stop scheduling new chunks but let already
/// in-flight chunks finish and persist their final status, then return the
/// first error.
-async fn export_data_concurrent(
- context: &ExportContext<'_>,
+async fn export_data_concurrent(
storage: &dyn SnapshotStorage,
manifest: &mut Manifest,
chunk_parallelism: usize,
progress: &dyn ProgressReporter,
-) -> Result<()> {
+ export: F,
+) -> Result<()>
+where
+ F: Fn(u32, TimeRange) -> Fut,
+ Fut: std::future::Future