1use std::any::Any;
16
17use api::v1::value::ValueData;
18use api::v1::{ColumnSchema, Row};
19use common_event_recorder::Event;
20use common_event_recorder::error::{Result, SerializeEventSnafu};
21use common_event_recorder::event_table::{
22 CATALOG_NAME_COLUMN, PHYSICAL_TABLE_ID_COLUMN, SCHEMA_NAME_COLUMN, TABLE_ID_COLUMN,
23 TABLE_NAME_COLUMN, column_schemas, nullable_string, nullable_value,
24};
25use serde::Serialize;
26use snafu::ResultExt;
27use store_api::storage::TableId;
28
29use crate::reconciliation::ResolveStrategy;
30
31pub(crate) const RECONCILE_LOGICAL_TABLES_EVENT_TYPE: &str = "reconcile_logical_tables";
33pub(crate) const RECONCILE_TABLE_EVENT_TYPE: &str = "reconcile_table";
35const PAYLOAD_VERSION: u8 = 1;
36
37#[derive(Debug, Clone, Default, PartialEq, Eq)]
39pub(crate) struct ReconciliationLocator {
40 catalog_name: Option<String>,
41 schema_name: Option<String>,
42 table_name: Option<String>,
43 table_id: Option<TableId>,
44 physical_table_id: Option<TableId>,
45}
46
47impl ReconciliationLocator {
48 pub(crate) fn physical_table(
50 catalog_name: &str,
51 schema_name: &str,
52 table_name: &str,
53 table_id: TableId,
54 ) -> Self {
55 Self {
56 catalog_name: Some(catalog_name.to_string()),
57 schema_name: Some(schema_name.to_string()),
58 table_name: Some(table_name.to_string()),
59 table_id: Some(table_id),
60 ..Default::default()
61 }
62 }
63
64 pub(crate) fn logical_table(
66 catalog_name: &str,
67 schema_name: &str,
68 table_name: &str,
69 table_id: TableId,
70 physical_table_id: TableId,
71 ) -> Self {
72 Self {
73 catalog_name: Some(catalog_name.to_string()),
74 schema_name: Some(schema_name.to_string()),
75 table_name: Some(table_name.to_string()),
76 table_id: Some(table_id),
77 physical_table_id: Some(physical_table_id),
78 }
79 }
80
81 fn schema() -> Vec<ColumnSchema> {
82 column_schemas([
83 &CATALOG_NAME_COLUMN,
84 &SCHEMA_NAME_COLUMN,
85 &TABLE_NAME_COLUMN,
86 &TABLE_ID_COLUMN,
87 &PHYSICAL_TABLE_ID_COLUMN,
88 ])
89 }
90
91 fn row(&self) -> Row {
92 Row {
93 values: vec![
94 nullable_string(self.catalog_name.as_deref()),
95 nullable_string(self.schema_name.as_deref()),
96 nullable_string(self.table_name.as_deref()),
97 nullable_table_id(self.table_id),
98 nullable_table_id(self.physical_table_id),
99 ],
100 }
101 }
102}
103
104#[derive(Debug, Serialize)]
105#[serde(untagged)]
106enum ReconcileTablePayload {
107 Submitted(TableSubmittedPayload),
108 Result(TableResultPayload),
109}
110
111#[derive(Debug, Serialize)]
112struct TableSubmittedPayload {
113 version: u8,
114 resolve_strategy: &'static str,
115 is_subprocedure: bool,
116}
117
118#[derive(Debug, Serialize)]
119struct TableResultPayload {
120 version: u8,
121 complete: bool,
122 metadata_state: Option<&'static str>,
123 resolution_strategy_applied: Option<&'static str>,
124 resolved_column_count: Option<usize>,
125 scanned_region_count: usize,
126 updated_region_count: usize,
127 table_info_updated: bool,
128 last_completed_phase: Option<&'static str>,
129}
130
131#[derive(Debug)]
133pub(crate) struct ReconcileTableEvent {
134 locator: ReconciliationLocator,
135 payload: Option<ReconcileTablePayload>,
136}
137
138impl ReconcileTableEvent {
139 pub(crate) fn table_submitted(
141 locator: ReconciliationLocator,
142 resolve_strategy: ResolveStrategy,
143 is_subprocedure: bool,
144 ) -> Self {
145 Self {
146 locator,
147 payload: Some(ReconcileTablePayload::Submitted(TableSubmittedPayload {
148 version: PAYLOAD_VERSION,
149 resolve_strategy: resolve_strategy_name(resolve_strategy),
150 is_subprocedure,
151 })),
152 }
153 }
154
155 #[allow(clippy::too_many_arguments)]
157 pub(crate) fn table_result(
158 locator: ReconciliationLocator,
159 complete: bool,
160 metadata_state: Option<&'static str>,
161 resolution_strategy_applied: Option<ResolveStrategy>,
162 resolved_column_count: Option<usize>,
163 scanned_region_count: usize,
164 updated_region_count: usize,
165 table_info_updated: bool,
166 last_completed_phase: Option<&'static str>,
167 ) -> Self {
168 Self {
169 locator,
170 payload: Some(ReconcileTablePayload::Result(TableResultPayload {
171 version: PAYLOAD_VERSION,
172 complete,
173 metadata_state,
174 resolution_strategy_applied: resolution_strategy_applied.map(resolve_strategy_name),
175 resolved_column_count,
176 scanned_region_count,
177 updated_region_count,
178 table_info_updated,
179 last_completed_phase,
180 })),
181 }
182 }
183
184 pub(crate) fn table_lifecycle(locator: ReconciliationLocator) -> Self {
186 Self {
187 locator,
188 payload: None,
189 }
190 }
191}
192
193impl Event for ReconcileTableEvent {
194 fn event_type(&self) -> &str {
195 RECONCILE_TABLE_EVENT_TYPE
196 }
197
198 fn json_payload(&self) -> Result<serde_json::Value> {
199 match &self.payload {
200 Some(payload) => serde_json::to_value(payload).context(SerializeEventSnafu),
201 None => Ok(serde_json::Value::Null),
202 }
203 }
204
205 fn extra_schema(&self) -> Vec<ColumnSchema> {
206 ReconciliationLocator::schema()
207 }
208
209 fn extra_rows(&self) -> Result<Vec<Row>> {
210 Ok(vec![self.locator.row()])
211 }
212
213 fn as_any(&self) -> &dyn Any {
214 self
215 }
216}
217
218#[derive(Debug, Serialize)]
219#[serde(untagged)]
220enum ReconcileLogicalTablesPayload {
221 Submitted(LogicalTablesSubmittedPayload),
222 Result(LogicalTablesResultPayload),
223}
224
225#[derive(Debug, Serialize)]
226struct LogicalTablesSubmittedPayload {
227 version: u8,
228 logical_table_count: usize,
229 is_subprocedure: bool,
230}
231
232#[derive(Debug, Serialize)]
233struct LogicalTablesResultPayload {
234 version: u8,
235 complete: bool,
236 processed_table_count: usize,
238 metadata_consistent_table_count: usize,
239 metadata_inconsistent_table_count: usize,
240 create_table_count: usize,
242 update_table_info_count: usize,
243}
244
245#[derive(Debug)]
247pub(crate) struct ReconcileLogicalTablesEvent {
248 locators: Vec<ReconciliationLocator>,
249 payload: Option<ReconcileLogicalTablesPayload>,
250}
251
252impl ReconcileLogicalTablesEvent {
253 pub(crate) fn submitted(locators: Vec<ReconciliationLocator>, is_subprocedure: bool) -> Self {
255 let logical_table_count = locators.len();
256 Self {
257 locators,
258 payload: Some(ReconcileLogicalTablesPayload::Submitted(
259 LogicalTablesSubmittedPayload {
260 version: PAYLOAD_VERSION,
261 logical_table_count,
262 is_subprocedure,
263 },
264 )),
265 }
266 }
267
268 pub(crate) fn result(
273 locators: Vec<ReconciliationLocator>,
274 complete: bool,
275 processed_table_count: usize,
276 metadata_consistent_table_count: usize,
277 metadata_inconsistent_table_count: usize,
278 create_table_count: usize,
279 update_table_info_count: usize,
280 ) -> Self {
281 Self {
282 locators,
283 payload: Some(ReconcileLogicalTablesPayload::Result(
284 LogicalTablesResultPayload {
285 version: PAYLOAD_VERSION,
286 complete,
287 processed_table_count,
288 metadata_consistent_table_count,
289 metadata_inconsistent_table_count,
290 create_table_count,
291 update_table_info_count,
292 },
293 )),
294 }
295 }
296
297 pub(crate) fn lifecycle(locators: Vec<ReconciliationLocator>) -> Self {
299 Self {
300 locators,
301 payload: None,
302 }
303 }
304}
305
306impl Event for ReconcileLogicalTablesEvent {
307 fn event_type(&self) -> &str {
308 RECONCILE_LOGICAL_TABLES_EVENT_TYPE
309 }
310
311 fn json_payload(&self) -> Result<serde_json::Value> {
312 match &self.payload {
313 Some(payload) => serde_json::to_value(payload).context(SerializeEventSnafu),
314 None => Ok(serde_json::Value::Null),
315 }
316 }
317
318 fn extra_schema(&self) -> Vec<ColumnSchema> {
319 ReconciliationLocator::schema()
320 }
321
322 fn extra_rows(&self) -> Result<Vec<Row>> {
323 Ok(self
324 .locators
325 .iter()
326 .map(ReconciliationLocator::row)
327 .collect())
328 }
329
330 fn as_any(&self) -> &dyn Any {
331 self
332 }
333}
334
335fn resolve_strategy_name(strategy: ResolveStrategy) -> &'static str {
336 match strategy {
337 ResolveStrategy::UseLatest => "use_latest",
338 ResolveStrategy::UseMetasrv => "use_metasrv",
339 ResolveStrategy::AbortOnConflict => "abort_on_conflict",
340 }
341}
342
343fn nullable_table_id(value: Option<TableId>) -> api::v1::Value {
344 nullable_value(value.map(ValueData::U32Value))
345}
346
347#[cfg(test)]
348mod tests {
349 use api::v1::value::ValueData;
350 use api::v1::{ColumnDataType, Row, SemanticType, Value};
351 use common_event_recorder::Event;
352 use serde_json::json;
353
354 use super::*;
355
356 #[test]
357 fn reconciliation_events_use_the_shared_locator_contract() {
358 let table = ReconcileTableEvent::table_lifecycle(ReconciliationLocator::physical_table(
359 "greptime", "public", "metrics", 42,
360 ));
361 assert_eq!(table.event_type(), RECONCILE_TABLE_EVENT_TYPE);
362 assert_eq!(
363 table
364 .extra_schema()
365 .into_iter()
366 .map(|column| {
367 (
368 column.column_name,
369 ColumnDataType::try_from(column.datatype).unwrap(),
370 SemanticType::try_from(column.semantic_type).unwrap(),
371 )
372 })
373 .collect::<Vec<_>>(),
374 vec![
375 (
376 "catalog_name".to_string(),
377 ColumnDataType::String,
378 SemanticType::Field
379 ),
380 (
381 "schema_name".to_string(),
382 ColumnDataType::String,
383 SemanticType::Field
384 ),
385 (
386 "table_name".to_string(),
387 ColumnDataType::String,
388 SemanticType::Field
389 ),
390 (
391 "table_id".to_string(),
392 ColumnDataType::Uint32,
393 SemanticType::Field
394 ),
395 (
396 "physical_table_id".to_string(),
397 ColumnDataType::Uint32,
398 SemanticType::Field,
399 ),
400 ]
401 );
402 assert_eq!(
403 table.extra_rows().unwrap(),
404 vec![Row {
405 values: vec![
406 ValueData::StringValue("greptime".to_string()).into(),
407 ValueData::StringValue("public".to_string()).into(),
408 ValueData::StringValue("metrics".to_string()).into(),
409 ValueData::U32Value(42).into(),
410 Value::default(),
411 ],
412 }]
413 );
414 assert_eq!(table.json_payload().unwrap(), serde_json::Value::Null);
415
416 let logical_tables = ReconcileLogicalTablesEvent::lifecycle(vec![
417 ReconciliationLocator::logical_table("greptime", "public", "cpu", 43, 42),
418 ReconciliationLocator::logical_table("greptime", "public", "memory", 44, 42),
419 ]);
420 assert_eq!(
421 logical_tables.event_type(),
422 RECONCILE_LOGICAL_TABLES_EVENT_TYPE
423 );
424 assert_eq!(logical_tables.extra_schema(), table.extra_schema());
425 assert_eq!(
426 logical_tables.extra_rows().unwrap(),
427 vec![
428 Row {
429 values: vec![
430 ValueData::StringValue("greptime".to_string()).into(),
431 ValueData::StringValue("public".to_string()).into(),
432 ValueData::StringValue("cpu".to_string()).into(),
433 ValueData::U32Value(43).into(),
434 ValueData::U32Value(42).into(),
435 ],
436 },
437 Row {
438 values: vec![
439 ValueData::StringValue("greptime".to_string()).into(),
440 ValueData::StringValue("public".to_string()).into(),
441 ValueData::StringValue("memory".to_string()).into(),
442 ValueData::U32Value(44).into(),
443 ValueData::U32Value(42).into(),
444 ],
445 },
446 ]
447 );
448 assert_eq!(
449 logical_tables.json_payload().unwrap(),
450 serde_json::Value::Null
451 );
452 }
453
454 #[test]
455 fn submitted_payloads_are_versioned_and_use_stable_strategy_names() {
456 for (strategy, expected) in [
457 (ResolveStrategy::UseLatest, "use_latest"),
458 (ResolveStrategy::UseMetasrv, "use_metasrv"),
459 (ResolveStrategy::AbortOnConflict, "abort_on_conflict"),
460 ] {
461 let table = ReconcileTableEvent::table_submitted(
462 ReconciliationLocator::physical_table("greptime", "public", "metrics", 42),
463 strategy,
464 true,
465 );
466 assert_eq!(
467 table.json_payload().unwrap(),
468 json!({
469 "version": 1,
470 "resolve_strategy": expected,
471 "is_subprocedure": true,
472 })
473 );
474 }
475
476 let logical_tables = ReconcileLogicalTablesEvent::submitted(
477 vec![
478 ReconciliationLocator::logical_table("greptime", "public", "cpu", 43, 42),
479 ReconciliationLocator::logical_table("greptime", "public", "memory", 44, 42),
480 ],
481 true,
482 );
483 assert_eq!(
484 logical_tables.json_payload().unwrap(),
485 json!({
486 "version": 1,
487 "logical_table_count": 2,
488 "is_subprocedure": true,
489 })
490 );
491 }
492
493 #[test]
494 fn terminal_payloads_distinguish_complete_and_partial_results() {
495 let table = ReconcileTableEvent::table_result(
496 ReconciliationLocator::physical_table("greptime", "public", "metrics", 42),
497 false,
498 Some("inconsistent"),
499 Some(ResolveStrategy::UseMetasrv),
500 Some(4),
501 3,
502 2,
503 true,
504 Some("update_table_info"),
505 );
506 assert_eq!(
507 table.json_payload().unwrap(),
508 json!({
509 "version": 1,
510 "complete": false,
511 "metadata_state": "inconsistent",
512 "resolution_strategy_applied": "use_metasrv",
513 "resolved_column_count": 4,
514 "scanned_region_count": 3,
515 "updated_region_count": 2,
516 "table_info_updated": true,
517 "last_completed_phase": "update_table_info",
518 })
519 );
520
521 let logical_tables = ReconcileLogicalTablesEvent::result(
522 vec![ReconciliationLocator::logical_table(
523 "greptime", "public", "cpu", 43, 42,
524 )],
525 true,
526 1,
527 0,
528 0,
529 1,
530 0,
531 );
532 assert_eq!(
533 logical_tables.json_payload().unwrap(),
534 json!({
535 "version": 1,
536 "complete": true,
537 "processed_table_count": 1,
538 "metadata_consistent_table_count": 0,
539 "metadata_inconsistent_table_count": 0,
540 "create_table_count": 1,
541 "update_table_info_count": 0,
542 })
543 );
544 }
545}