|
| 1 | +--- |
| 2 | +title: How to Write a System Table |
| 3 | +--- |
| 4 | + |
| 5 | +System tables are special tables that provide information about Databend's internal state, such as databases, tables, functions, settings, etc. In this document, we will show you how to write a new system table for Databend using the credits table as an example. |
| 6 | + |
| 7 | +The credits table returns information about the upstream dependencies used by Databend, including their names, versions and licenses. |
| 8 | + |
| 9 | +## Prerequisites |
| 10 | + |
| 11 | +To write a new system table for Databend, you need to have some basic knowledge of Rust programming language and Databend's code structure. |
| 12 | + |
| 13 | +## Location |
| 14 | + |
| 15 | +The existing system tables for Databend are located in the `query/storage` directory. You should place your new system table file in this directory as well, unless there are some special build reasons that prevent you from doing so. In that case, you can temporarily place it in the `service/databases/system` directory (not recommended). |
| 16 | + |
| 17 | +## Definition |
| 18 | + |
| 19 | +The definition of a system table mainly focuses on two aspects: one is the table information, which includes the table `name` and `schema`, etc.; the other is the data generation/retrieval logic for the table content. These two aspects correspond to two traits: `SyncSystemTable` and `AsyncSystemTable`. You need to implement one of these traits depending on whether your data retrieval involves asynchronous function calls or not. |
| 20 | + |
| 21 | +## Implementation |
| 22 | + |
| 23 | +In this section, we will walk through the implementation of the credits table step by step. The code file is located at `credits_table.rs`. |
| 24 | + |
| 25 | +Firstly, you need to define a struct for your system table that contains only the fields for storing the table information. For example: |
| 26 | + |
| 27 | +```rust |
| 28 | +pub struct CreditsTable { |
| 29 | + table_info: TableInfo, |
| 30 | +} |
| 31 | +``` |
| 32 | + |
| 33 | +Next, you need to implement a create method for your system table struct that takes a `table_id` as an argument and returns an `Arc<dyn Table>`. The `table_id` is generated by `sys_db_meta.next_table_id()` when creating a new system table. |
| 34 | + |
| 35 | +```rust |
| 36 | +pub fn create(table_id: u64) -> Arc<dyn Table> |
| 37 | +``` |
| 38 | + |
| 39 | +Inside this method, you need to define a schema for your system table using `TableSchemaRefExt` and `TableField`. The schema describes the structure of your system table with field names and types depending on the data you want to store in it. |
| 40 | + |
| 41 | +For example: |
| 42 | + |
| 43 | +```rust |
| 44 | +let schema = TableSchemaRefExt::create(vec![ |
| 45 | + TableField::new("name", TableDataType::String), |
| 46 | + TableField::new("version", TableDataType::String), |
| 47 | + TableField::new("license", TableDataType::String), |
| 48 | +]); |
| 49 | +``` |
| 50 | + |
| 51 | +For string-type data, you can use `TableDataType::String`; other basic types are similar. But if you need to allow null values in your field, such as an optional 64-bit unsigned integer field, you can use `TableDataType::Nullable(Box::new(TableDataType::Number(NumberDataType::UInt64)))` instead. `TableDataType::Nullable` indicates that null values are allowed; `TableDataType::Number(NumberDataType::UInt64)` represents that the type is 64-bit unsigned integer. |
| 52 | + |
| 53 | +After defining the schema, you need to define some metadata for your system table, such as description (`desc`), `name`, `meta`, etc. You can follow other existing examples and fill in these fields accordingly. |
| 54 | + |
| 55 | +For example: |
| 56 | + |
| 57 | +```rust |
| 58 | +let table_info = TableInfo { |
| 59 | + desc: "'system'.'credits'".to_string(), |
| 60 | + name: "credits".to_string(), |
| 61 | + ident: TableIdent::new(table_id, 0), |
| 62 | + meta: TableMeta { |
| 63 | + schema, |
| 64 | + engine: "SystemCredits".to_string(), |
| 65 | + ..Default::default() |
| 66 | + }, |
| 67 | + ..Default::default() |
| 68 | +}; |
| 69 | + |
| 70 | +SyncOneBlockSystemTable::create(CreditsTable { table_info }) |
| 71 | +``` |
| 72 | + |
| 73 | +Finally, you need to create an instance of your system table struct with these fields and wrap it with either `SyncOneBlockSystemTable` or `AsyncOneBlockSystemTable` depending on whether your data retrieval logic is synchronous or asynchronous. |
| 74 | + |
| 75 | +Next, you need to implement either `SyncSystemTable` or `AsyncSystemTable` trait for your system table struct. `SyncSystemTable` requires you to define `NAME` constant and implement four methods: `get_table_info()`, `get_full_data()`, `get_partitions()` and `truncate()`. However, the last two methods have default implementations, so you don't need to implement them yourself in most cases. (`AsyncSystemTable` is similar, but it doesn't have `truncate()` method.) |
| 76 | + |
| 77 | +`NAME` constant follows the format of `system.<name>`. |
| 78 | + |
| 79 | +```rust |
| 80 | +const NAME: &'static str = "system.credits"; |
| 81 | +``` |
| 82 | + |
| 83 | +`get_table_info()` method returns the table information stored in the struct. |
| 84 | + |
| 85 | +```rust |
| 86 | +fn get_table_info(&self) -> &TableInfo { |
| 87 | + &self.table_info |
| 88 | +} |
| 89 | +``` |
| 90 | + |
| 91 | +`get_full_data()` method is the most important part, because it contains the logic for generating or retrieving the data for your system table. The credits table has three fields that are similar, so we will only show the license field as an example. |
| 92 | + |
| 93 | +The license field information is obtained from an environment variable named `DATABEND_CREDITS_LICENSES` (see `common-building`). Each data item is separated by a comma. |
| 94 | + |
| 95 | +String-type columns are eventually converted from `Vec<Vec<u8>>`, where each string needs to be converted to `Vec<u8>`. So we use `.as_bytes().to_vec()` to do this conversion when iterating over the data. |
| 96 | + |
| 97 | +```rust |
| 98 | +let licenses: Vec<Vec<u8>> = env!("DATABEND_CREDITS_LICENSES") |
| 99 | + .split_terminator(',') |
| 100 | + .map(|x| x.trim().as_bytes().to_vec()) |
| 101 | + .collect(); |
| 102 | +``` |
| 103 | + |
| 104 | +After getting all the data, you can return them in a `DataBlock` format. For non-null types, use `from_data`; for nullable types, use `from_opt_data`. |
| 105 | + |
| 106 | +For example: |
| 107 | + |
| 108 | +```rust |
| 109 | +Ok(DataBlock::new_from_columns(vec![ |
| 110 | + StringType::from_data(names), |
| 111 | + StringType::from_data(versions), |
| 112 | + StringType::from_data(licenses), |
| 113 | +])) |
| 114 | +``` |
| 115 | + |
| 116 | +Lastly, if you want to integrate your system table into Databend, you also need to edit `system_database.rs` and register it to `SystemDatabase`. |
| 117 | + |
| 118 | +```rust |
| 119 | +impl SystemDatabase { |
| 120 | + pub fn create(sys_db_meta: &mut InMemoryMetas, config: &Config) -> Self { |
| 121 | + ... |
| 122 | + CreditsTable::create(sys_db_meta.next_table_id()), |
| 123 | + ... |
| 124 | + } |
| 125 | +} |
| 126 | +``` |
| 127 | + |
| 128 | +## Testing |
| 129 | + |
| 130 | +The tests for system tables are currently located at `tests/it/storages/system.rs`. |
| 131 | + |
| 132 | +For tables whose content does not change frequently, you can use Golden File testing. Its logic is to write the corresponding table into a specified file and compare it with an expected file. If they match, then the test passes; otherwise, it fails. |
| 133 | + |
| 134 | +For example: |
| 135 | + |
| 136 | +```rust |
| 137 | +#[tokio::test(flavor = "multi_thread")] |
| 138 | +async fn test_columns_table() -> Result<()> { |
| 139 | + let (_guard, ctx) = crate::tests::create_query_context().await?; |
| 140 | + |
| 141 | + let mut mint = Mint::new("tests/it/storages/testdata"); |
| 142 | + let file = &mut mint.new_goldenfile("columns_table.txt").unwrap(); |
| 143 | + let table = ColumnsTable::create(1); |
| 144 | + |
| 145 | + run_table_tests(file, ctx, table).await?; |
| 146 | + Ok(()) |
| 147 | +} |
| 148 | +``` |
| 149 | + |
| 150 | +For tables whose content may change dynamically or depend on external factors, there is a lack of sufficient testing methods. You can choose to test the parts that have relatively fixed patterns, such as the number of rows and columns; or you can verify whether the output contains specific content. |
| 151 | + |
| 152 | +For example: |
| 153 | + |
| 154 | +```rust |
| 155 | +#[tokio::test(flavor = "multi_thread")] |
| 156 | +async fn test_metrics_table() -> Result<()> { |
| 157 | + ... |
| 158 | + let result = stream.try_collect::<Vec<_>>().await?; |
| 159 | + let block = &result[0]; |
| 160 | + assert_eq!(block.num_columns(), 4); |
| 161 | + assert!(block.num_rows() >= 1); |
| 162 | + |
| 163 | + let output = pretty_format_blocks(result.as_slice())?; |
| 164 | + assert!(output.contains("test_test_metrics_table_count")); |
| 165 | + #[cfg(feature = "enable_histogram")] |
| 166 | + assert!(output.contains("test_test_metrics_table_histogram")); |
| 167 | + |
| 168 | + Ok(()) |
| 169 | +} |
| 170 | +``` |
| 171 | + |
| 172 | +## Summary |
| 173 | + |
| 174 | +In this document, we have shown you how to write a new system table for Databend using the credits table as an example. We hope this document helps you understand the basic steps and principles of creating a system table for Databend. If you have any questions or feedback, please feel free to contact us on GitHub or Slack. Thank you for your interest and contribution to Databend! |
| 175 | + |
| 176 | + |
0 commit comments