Skip to content

Commit f4a49b4

Browse files
authored
Merge pull request #10554 from PsiACE/system-table-docs
docs: add how to write a system table
2 parents 9102b14 + 8eab2aa commit f4a49b4

File tree

1 file changed

+148
-0
lines changed

1 file changed

+148
-0
lines changed
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
---
2+
title: How to Create a System Table
3+
---
4+
5+
System tables are tables that provide information about Databend's internal state, such as databases, tables, functions, and settings. If you're familiar with the Databend code structure and have basic knowledge about Rust, you can also create your own system tables as needed.
6+
7+
Creating a system table mainly involves defining the table information (table name and schema) and how to generate and retrieve data for the table. This can be done through implementing the trait `SyncSystemTable` or `AsyncSystemTable`.
8+
9+
This guide will show you how to create a new system table for Databend, using the table [system.credits](https://databend.rs/doc/sql-reference/system-tables/system-credits) as an example. The table provides information Databend's upstream dependencies and the code is located at `src/query/storage/system/src/credits_table.rs`.
10+
11+
:::note
12+
Databend suggests that you store the code for new system tables in the directory `src/query/storage/system/src/`. However, there may be situations where you cannot do so, such as issues related to the build process. In such cases, you can place it temporarily in a directory called `src/query/service/src/databases/system` (although this is not recommended).
13+
:::
14+
15+
## Creating a System Table
16+
17+
The following walks through the implementation of the table `system.credits` step by step.
18+
19+
1. Define a struct for your system table that contains only the fields for storing the table information.
20+
21+
```rust
22+
pub struct CreditsTable {
23+
table_info: TableInfo,
24+
}
25+
```
26+
27+
2. Implement a `create` method for your system table struct that takes `table_id` as an argument and returns `Arc<dyn Table>`. The `table_id` is generated by `sys_db_meta.next_table_id()` when creating a new system table.
28+
29+
```rust
30+
pub fn create(table_id: u64) -> Arc<dyn Table>
31+
```
32+
33+
3. 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.
34+
35+
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.
36+
37+
```rust
38+
let schema = TableSchemaRefExt::create(vec![
39+
TableField::new("name", TableDataType::String),
40+
TableField::new("version", TableDataType::String),
41+
TableField::new("license", TableDataType::String),
42+
]);
43+
```
44+
45+
4. Define metadata for your system table, such as description (`desc`), `name`, `meta`, etc. You can follow other existing examples and fill in these fields accordingly.
46+
47+
```rust
48+
let table_info = TableInfo {
49+
desc: "'system'.'credits'".to_string(),
50+
name: "credits".to_string(),
51+
ident: TableIdent::new(table_id, 0),
52+
meta: TableMeta {
53+
schema,
54+
engine: "SystemCredits".to_string(),
55+
..Default::default()
56+
},
57+
..Default::default()
58+
};
59+
60+
SyncOneBlockSystemTable::create(CreditsTable { table_info })
61+
```
62+
63+
5. 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 is synchronous or asynchronous.
64+
65+
6. Implement either `SyncSystemTable` or `AsyncSystemTable` trait for your system table struct. `SyncSystemTable` requires you to define a `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.)
66+
67+
`NAME` constant follows the format of `system.<name>`.
68+
69+
```rust
70+
const NAME: &'static str = "system.credits";
71+
```
72+
73+
`get_table_info()` method returns the table information stored in the struct.
74+
75+
```rust
76+
fn get_table_info(&self) -> &TableInfo {
77+
&self.table_info
78+
}
79+
```
80+
81+
`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.
82+
83+
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.
84+
85+
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.
86+
87+
```rust
88+
let licenses: Vec<Vec<u8>> = env!("DATABEND_CREDITS_LICENSES")
89+
.split_terminator(',')
90+
.map(|x| x.trim().as_bytes().to_vec())
91+
.collect();
92+
```
93+
94+
7. Return the retrieved data in a `DataBlock` format. Use `from_data` for non-null types and `from_opt_data` for nullable types. For example:
95+
96+
```rust
97+
Ok(DataBlock::new_from_columns(vec![
98+
StringType::from_data(names),
99+
StringType::from_data(versions),
100+
StringType::from_data(licenses),
101+
]))
102+
```
103+
104+
8. Edit `system_database.rs` to register the new table to `SystemDatabase`.
105+
106+
```rust
107+
impl SystemDatabase {
108+
pub fn create(sys_db_meta: &mut InMemoryMetas, config: &Config) -> Self {
109+
...
110+
CreditsTable::create(sys_db_meta.next_table_id()),
111+
...
112+
}
113+
}
114+
```
115+
116+
## Testing a New System Table
117+
118+
The system table tests are located at `tests/it/storages/system.rs`. For tables with infrequent content changes, Golden File testing can be used, which involves writing the table to a specified file and comparing it to an expected file. For example:
119+
120+
```rust
121+
#[tokio::test(flavor = "multi_thread")]
122+
async fn test_columns_table() -> Result<()> {
123+
let (_guard, ctx) = crate::tests::create_query_context().await?;
124+
let mut mint = Mint::new("tests/it/storages/testdata");
125+
let file = &mut mint.new_goldenfile("columns_table.txt").unwrap();
126+
let table = ColumnsTable::create(1);
127+
run_table_tests(file, ctx, table).await?;
128+
Ok(())
129+
}
130+
```
131+
132+
For tables with dynamically changing content or external dependencies, testing methods are limited. You can test relatively fixed patterns such as the number of rows and columns, or verify if the output contains specific content. For example:
133+
134+
```rust
135+
#[tokio::test(flavor = "multi_thread")]
136+
async fn test_metrics_table() -> Result<()> {
137+
...
138+
let result = stream.try_collect::<Vec<_>>().await?;
139+
let block = &result[0];
140+
assert_eq!(block.num_columns(), 4);
141+
assert!(block.num_rows() >= 1);
142+
let output = pretty_format_blocks(result.as_slice())?;
143+
assert!(output.contains("test_test_metrics_table_count"));
144+
#[cfg(feature = "enable_histogram")]
145+
assert!(output.contains("test_test_metrics_table_histogram"));
146+
Ok(())
147+
}
148+
```

0 commit comments

Comments
 (0)