cli/data/import_v2/
packed.rs1use std::collections::HashSet;
16
17use common_datasource::packed_snapshot::{PACK_INDEX_FILE, PackIndex};
18use snafu::ResultExt;
19use sql::statements::statement::Statement;
20
21use crate::data::export_v2::manifest::{ChunkStatus, Manifest};
22use crate::data::import_v2::error::{InvalidPackedSnapshotSnafu, Result, SnapshotStorageSnafu};
23use crate::data::import_v2::{command, executor};
24use crate::data::path::{data_dir_for_schema_chunk, ddl_path_for_schema};
25use crate::data::snapshot_storage::SnapshotStorage;
26
27pub(crate) async fn validate_snapshot(
29 storage: &dyn SnapshotStorage,
30 manifest: &Manifest,
31 schemas: &[String],
32) -> Result<()> {
33 manifest
34 .validate_layout()
35 .map_err(|reason| InvalidPackedSnapshotSnafu { reason }.build())?;
36 if manifest.schema_only
37 && !storage
38 .list_files_recursive("data/")
39 .await
40 .context(SnapshotStorageSnafu)?
41 .is_empty()
42 {
43 return InvalidPackedSnapshotSnafu {
44 reason: "schema-only snapshot contains data objects",
45 }
46 .fail();
47 }
48 for schema in schemas {
49 let ddl = storage
50 .read_text(&ddl_path_for_schema(schema))
51 .await
52 .context(SnapshotStorageSnafu)?;
53 let mut names = HashSet::new();
54 for sql in command::iter_ddl_statements(&ddl) {
55 let statements = executor::parse_ddl(&sql).map_err(|_| {
56 InvalidPackedSnapshotSnafu {
57 reason: "invalid snapshot DDL",
58 }
59 .build()
60 })?;
61 for statement in statements {
62 if let Statement::CreateTable(create) = statement {
63 if create.engine.eq_ignore_ascii_case("metric")
64 && create.options.get("on_physical_table").is_none()
65 {
66 continue;
67 }
68 let parts: Vec<_> = create
69 .name
70 .0
71 .iter()
72 .map(|p| p.as_ident().map(|i| i.value.as_str()))
73 .collect();
74 let allowed = match parts.as_slice() {
75 [Some(_)] => true,
76 [Some(s), Some(_)] => *s == schema,
77 [Some(c), Some(s), Some(_)] => *c == manifest.catalog && *s == schema,
78 _ => false,
79 };
80 if !allowed {
81 return InvalidPackedSnapshotSnafu {
82 reason: "snapshot table DDL escapes its catalog/schema",
83 }
84 .fail();
85 }
86 let name = parts.last().and_then(|p| *p).unwrap_or_default();
87 if !names.insert(name.to_string()) {
88 return InvalidPackedSnapshotSnafu {
89 reason: "duplicate snapshot table DDL",
90 }
91 .fail();
92 }
93 }
94 }
95 }
96 for chunk in &manifest.chunks {
97 if chunk.status == ChunkStatus::Skipped && !names.is_empty() {
98 return InvalidPackedSnapshotSnafu {
99 reason: "packed chunks require zero-row streams for empty tables",
100 }
101 .fail();
102 }
103 if chunk.status != ChunkStatus::Completed {
104 continue;
105 }
106 let path = format!(
107 "{}{PACK_INDEX_FILE}",
108 data_dir_for_schema_chunk(schema, chunk.id)
109 );
110 let text = storage
111 .read_text(&path)
112 .await
113 .context(SnapshotStorageSnafu)?;
114 let index: PackIndex = serde_json::from_str(&text).map_err(|e| {
115 InvalidPackedSnapshotSnafu {
116 reason: e.to_string(),
117 }
118 .build()
119 })?;
120 let prefix = data_dir_for_schema_chunk(schema, chunk.id);
121 let expected: HashSet<_> = std::iter::once(path)
122 .chain(index.objects.iter().map(|o| format!("{prefix}{}", o.path)))
123 .collect();
124 let actual: HashSet<_> = chunk
125 .files
126 .iter()
127 .filter(|file| file.starts_with(&prefix))
128 .cloned()
129 .collect();
130 if actual != expected {
131 return InvalidPackedSnapshotSnafu {
132 reason: "chunk manifest inventory differs from indexed objects",
133 }
134 .fail();
135 }
136 index
137 .validate_membership(names.iter().map(String::as_str))
138 .map_err(|e| {
139 InvalidPackedSnapshotSnafu {
140 reason: e.to_string(),
141 }
142 .build()
143 })?;
144 for object in &index.objects {
145 let path = format!("{prefix}{}", object.path);
146 let length = storage
147 .file_size(&path)
148 .await
149 .context(SnapshotStorageSnafu)?;
150 if length != Some(object.length) {
151 return InvalidPackedSnapshotSnafu {
152 reason: format!(
153 "object {path}: expected length {}, actual {length:?}",
154 object.length
155 ),
156 }
157 .fail();
158 }
159 }
160 }
161 }
162 Ok(())
163}
164
165#[cfg(test)]
166mod tests {
167 use super::*;
168 use crate::data::export_v2::manifest::{ChunkMeta, TimeRange};
169 use crate::data::snapshot_storage::OpenDalStorage;
170
171 #[tokio::test]
172 async fn packed_membership_uses_literal_names_and_excludes_physical_and_views() {
173 let dir = tempfile::Builder::new()
174 .prefix("packed snapshot # ")
175 .tempdir()
176 .unwrap();
177 let uri = url::Url::from_file_path(dir.path()).unwrap();
178 let storage = OpenDalStorage::from_uri(uri.as_str(), &Default::default()).unwrap();
179 let large = "x".repeat(servers::query_handler::sql::MAX_LOGICAL_TABLE_DDL_BYTES);
180 storage.write_text("schema/ddl/public.sql", &format!("CREATE TABLE p (ts TIMESTAMP TIME INDEX) ENGINE=metric WITH(physical_metric_table=''); CREATE TABLE \"logical.name\" (ts TIMESTAMP TIME INDEX) ENGINE=metric WITH(on_physical_table='p'); CREATE VIEW v AS SELECT * FROM \"logical.name\"; CREATE TABLE ordinary (ts TIMESTAMP TIME INDEX, payload STRING DEFAULT '{large}'); CREATE VIEW large_view AS SELECT '{large}' AS payload;")).await.unwrap();
181 let path = "data/public/1/pack-index.json";
182 let index = serde_json::json!({"version":1,"objects":[{"path":"pack-0.bin","kind":"pack","length":24}],"tables":[{"table_name":"logical.name","object":"pack-0.bin","offset":0,"length":12,"row_count":0},{"table_name":"ordinary","object":"pack-0.bin","offset":12,"length":12,"row_count":0}]});
183 storage.write_text(path, &index.to_string()).await.unwrap();
184 assert!(dir.path().join(path).is_file());
185 storage
186 .write_text("data/public/1/pack-0.bin", "abcdefghijklmnopqrstuvwx")
187 .await
188 .unwrap();
189 let mut manifest = Manifest::new_schema_only("greptime".into(), vec!["public".into()]);
190 let mut chunk = ChunkMeta::new(1, TimeRange::unbounded());
191 chunk.mark_completed(vec![path.into(), "data/public/1/pack-0.bin".into()], None);
192 manifest.version = 2;
193 manifest.data_layout = Some("metric-parquet-packs".into());
194 manifest.schema_only = false;
195 manifest.chunks.push(chunk);
196 validate_snapshot(&storage, &manifest, &manifest.schemas)
197 .await
198 .unwrap();
199 let mut missing = index.clone();
200 missing["tables"] = serde_json::json!([]);
201 storage
202 .write_text(path, &missing.to_string())
203 .await
204 .unwrap();
205 assert!(
206 validate_snapshot(&storage, &manifest, &manifest.schemas)
207 .await
208 .is_err()
209 );
210 let mut escaped = index;
211 escaped["objects"][0]["path"] = "../pack-0.bin".into();
212 storage
213 .write_text(path, &escaped.to_string())
214 .await
215 .unwrap();
216 assert!(
217 validate_snapshot(&storage, &manifest, &manifest.schemas)
218 .await
219 .is_err()
220 );
221 }
222}