diff --git a/frontend/src/pages/Backups/Backups.tsx b/frontend/src/pages/Backups/Backups.tsx
index 7ea1aed..36b662b 100644
--- a/frontend/src/pages/Backups/Backups.tsx
+++ b/frontend/src/pages/Backups/Backups.tsx
@@ -1,7 +1,7 @@
import React, { useEffect, useState } from 'react'
import { usePollingEffect } from '../../utils/usePollingEffect'
import { ColumnType } from 'antd/es/table'
-import { Table, Button, Form, Input, Modal, Tag, Col, Progress, Row, Tooltip, notification } from 'antd'
+import { Table, Button, Form, Input, Checkbox, Modal, Tag, Col, Progress, Row, Tooltip, notification } from 'antd'
import useSWR, { mutate } from 'swr'
interface BackupRow {
@@ -29,6 +29,7 @@ type FieldType = {
table?: string
bucket?: string
path?: string
+ is_sharded?: boolean
aws_access_key_id?: string
aws_secret_access_key?: string
}
@@ -165,6 +166,16 @@ export default function Backups() {
+
+ label="Is Sharded"
+ name="is_sharded"
+ initialValue="false"
+ valuePropName="checked"
+ rules={[{ required: true, message: 'Is this table sharded?' }]}
+ >
+ is sharded
+
+
label="S3 Bucket"
name="bucket"
diff --git a/frontend/src/pages/Backups/ScheduledBackups.tsx b/frontend/src/pages/Backups/ScheduledBackups.tsx
index 275ce3e..439c3bc 100644
--- a/frontend/src/pages/Backups/ScheduledBackups.tsx
+++ b/frontend/src/pages/Backups/ScheduledBackups.tsx
@@ -1,9 +1,24 @@
import React, { useEffect, useState } from 'react'
import { usePollingEffect } from '../../utils/usePollingEffect'
import { ColumnType } from 'antd/es/table'
-import { Switch, Select, Table, Button, Form, Input, Modal, Tag, Col, Progress, Row, Tooltip, notification } from 'antd'
-import DeleteOutlined from '@ant-design/icons'
-import EditOutlined from '@ant-design/icons'
+import {
+ Switch,
+ Select,
+ Table,
+ Button,
+ Form,
+ Input,
+ Checkbox,
+ Modal,
+ Tag,
+ Col,
+ Progress,
+ Row,
+ Tooltip,
+ notification,
+} from 'antd'
+import DeleteOutlined from '@ant-design/icons/DeleteOutlined'
+import EditOutlined from '@ant-design/icons/EditOutlined'
import { Clusters } from '../Clusters/Clusters'
import useSWR, { mutate } from 'swr'
@@ -16,6 +31,7 @@ interface ScheduleRow {
schedule: string
incremental_schedule: string
table: string
+ is_sharded: boolean
database: string
bucket: string
path: string
@@ -32,6 +48,7 @@ type FieldType = {
incremental_schedule?: string
database?: string
table?: string
+ is_sharded?: boolean
bucket?: string
path?: string
aws_access_key_id?: string
@@ -153,12 +170,12 @@ export default function ScheduledBackups() {
{ title: 'Last Run Time', dataIndex: 'last_run_time' },
{ title: 'Database', dataIndex: 'database' },
{ title: 'Table', dataIndex: 'table' },
+ { title: 'Is Sharded', dataIndex: 'is_sharded', render: (_, sched) => (sched.is_sharded ? 'Yes' : 'No') },
{ title: 'S3 Location', dataIndex: 'bucket', render: (_, sched) => 's3://' + sched.bucket + '/' + sched.path },
- { title: 'Created At', dataIndex: 'created_at' },
{
- title: '',
+ title: 'Actions',
dataIndex: 'id',
- render: id => {
+ render: (id: string, rowData: ScheduleRow) => {
const deleteBackup = async () => {
try {
const res = await fetch(`/api/scheduled_backups/${id}`, {
@@ -173,21 +190,10 @@ export default function ScheduledBackups() {
}
}
- return (
-
-
-
- )
- },
- },
- {
- title: 'Actions',
- dataIndex: 'id',
- render: (id: string, rowData: ScheduleRow) => {
return (
<>
handleEdit(rowData)} />
- {/* handleDelete(id)} /> */}
+ deleteBackup()} style={{ marginLeft: '15px' }} />
>
)
},
@@ -285,6 +291,16 @@ export default function ScheduledBackups() {
+
+ label="Is Sharded"
+ name="is_sharded"
+ initialValue="false"
+ valuePropName="checked"
+ rules={[{ required: true, message: 'Is this table sharded?' }]}
+ >
+ is sharded
+
+
label="S3 Bucket"
name="bucket"
diff --git a/frontend/src/pages/Overview/Overview.tsx b/frontend/src/pages/Overview/Overview.tsx
index a8f3a44..c28939b 100644
--- a/frontend/src/pages/Overview/Overview.tsx
+++ b/frontend/src/pages/Overview/Overview.tsx
@@ -1,7 +1,7 @@
import React, { useEffect, useState } from 'react'
import { Line } from '@ant-design/charts'
import { Card, Col, Row, Tooltip, notification } from 'antd'
-import InfoCircleOutlined from '@ant-design/icons'
+import InfoCircleOutlined from '@ant-design/icons/InfoCircleOutlined'
import { clickhouseTips } from './tips'
import useSWR from 'swr'
diff --git a/frontend/src/pages/QueryEditor/QueryEditor.tsx b/frontend/src/pages/QueryEditor/QueryEditor.tsx
index 5d0b5d7..66919ba 100644
--- a/frontend/src/pages/QueryEditor/QueryEditor.tsx
+++ b/frontend/src/pages/QueryEditor/QueryEditor.tsx
@@ -6,7 +6,7 @@ import 'prismjs/components/prism-sql'
import 'prismjs/themes/prism.css'
import Editor from 'react-simple-code-editor'
import { v4 as uuidv4 } from 'uuid'
-import SaveOutlined from '@ant-design/icons'
+import SaveOutlined from '@ant-design/icons/SaveOutlined'
function CreateSavedQueryModal({
modalOpen = false,
diff --git a/frontend/src/pages/QueryEditor/SavedQueries.tsx b/frontend/src/pages/QueryEditor/SavedQueries.tsx
index 30a9320..d1ab985 100644
--- a/frontend/src/pages/QueryEditor/SavedQueries.tsx
+++ b/frontend/src/pages/QueryEditor/SavedQueries.tsx
@@ -2,7 +2,7 @@ import { Table, Button, Row, Col, Tooltip } from 'antd'
import React, { useEffect, useState } from 'react'
import { ColumnType } from 'antd/es/table'
import SavedQuery from './SavedQuery'
-import ReloadOutlined from '@ant-design/icons'
+import ReloadOutlined from '@ant-design/icons/ReloadOutlined'
import { useHistory } from 'react-router-dom'
import { isoTimestampToHumanReadable } from '../../utils/dateUtils'
diff --git a/frontend/src/pages/SlowQueries/MetricsTab.tsx b/frontend/src/pages/SlowQueries/MetricsTab.tsx
index f744bab..b76ed18 100644
--- a/frontend/src/pages/SlowQueries/MetricsTab.tsx
+++ b/frontend/src/pages/SlowQueries/MetricsTab.tsx
@@ -2,7 +2,7 @@ import React, { useEffect, useState } from 'react'
import { Line } from '@ant-design/plots'
// @ts-ignore
import { Card, Col, Row, Tooltip, notification } from 'antd'
-import InfoCircleOutlined from '@ant-design/icons'
+import InfoCircleOutlined from '@ant-design/icons/InfoCircleOutlined'
import { NoDataSpinner, QueryDetailData } from './QueryDetail'
export default function MetricsTab({ query_hash }: { query_hash: string }) {
diff --git a/housewatch/clickhouse/backups.py b/housewatch/clickhouse/backups.py
index 54bdc3e..063fdc3 100644
--- a/housewatch/clickhouse/backups.py
+++ b/housewatch/clickhouse/backups.py
@@ -5,6 +5,7 @@
from uuid import uuid4
from housewatch.clickhouse.client import run_query
from housewatch.models.backup import ScheduledBackup, ScheduledBackupRun
+from housewatch.clickhouse.table import table_engine_full
from housewatch.clickhouse.clusters import get_node_per_shard
from django.conf import settings
@@ -15,7 +16,7 @@
logger = structlog.get_logger(__name__)
-def execute_backup_on_shards(
+def execute_backup(
query: str,
params: Dict[str, str | int] = {},
query_settings: Dict[str, str | int] = {},
@@ -25,6 +26,7 @@ def execute_backup_on_shards(
aws_key: Optional[str] = None,
aws_secret: Optional[str] = None,
base_backup: Optional[str] = None,
+ is_sharded: bool = False,
):
"""
This function will execute a backup on each shard in a cluster
@@ -57,6 +59,8 @@ def execute_backup_on_shards(
item[key[0]] = res[index]
response.append(item)
responses.append((shard, response))
+ if not is_sharded:
+ return response
return response
@@ -78,7 +82,9 @@ def get_backup(backup, cluster=None):
return run_query(QUERY, {"uuid": backup}, use_cache=False)
-def create_table_backup(database, table, bucket, path, cluster=None, aws_key=None, aws_secret=None, base_backup=None):
+def create_table_backup(
+ database, table, bucket, path, cluster=None, aws_key=None, aws_secret=None, base_backup=None, is_sharded=False
+):
if aws_key is None or aws_secret is None:
aws_key = settings.AWS_ACCESS_KEY_ID
aws_secret = settings.AWS_SECRET_ACCESS_KEY
@@ -87,7 +93,7 @@ def create_table_backup(database, table, bucket, path, cluster=None, aws_key=Non
QUERY = """BACKUP TABLE %(database)s.%(table)s
TO S3('https://%(bucket)s.s3.amazonaws.com/%(path)s/%(shard)s', '%(aws_key)s', '%(aws_secret)s')
ASYNC"""
- return execute_backup_on_shards(
+ return execute_backup(
QUERY,
{
"database": database,
@@ -102,6 +108,7 @@ def create_table_backup(database, table, bucket, path, cluster=None, aws_key=Non
aws_key=aws_key,
aws_secret=aws_secret,
base_backup=base_backup,
+ is_sharded=is_sharded,
)
QUERY = """BACKUP TABLE %(database)s.%(table)s
TO S3('https://%(bucket)s.s3.amazonaws.com/%(path)s', '%(aws_key)s', '%(aws_secret)s')
@@ -133,7 +140,7 @@ def create_database_backup(database, bucket, path, cluster=None, aws_key=None, a
TO S3('https://%(bucket)s.s3.amazonaws.com/%(path)s/%(shard)s', '%(aws_key)s', '%(aws_secret)s')
ASYNC"""
- return execute_backup_on_shards(
+ return execute_backup(
QUERY,
{
"database": database,
@@ -199,6 +206,7 @@ def run_backup(backup_id, incremental=False):
backup.aws_access_key_id,
backup.aws_secret_access_key,
base_backup=base_backup,
+ is_sharded=backup.is_sharded,
)
uuid = str(uuid4())
br = ScheduledBackupRun.objects.create(
diff --git a/housewatch/clickhouse/table.py b/housewatch/clickhouse/table.py
new file mode 100644
index 0000000..cb89af2
--- /dev/null
+++ b/housewatch/clickhouse/table.py
@@ -0,0 +1,21 @@
+from housewatch.clickhouse.client import run_query
+
+
+def is_replicated_table(database, table):
+ QUERY = """SELECT engine FROM system.tables WHERE database = '%(database)s' AND name = '%(table)s'"""
+ return "replicated" in run_query(QUERY, {"database": database, "table": table})[0]["engine"].lower()
+
+
+def table_engine_full(database, table):
+ QUERY = """SELECT engine_full FROM system.tables WHERE database = '%(database)s' AND name = '%(table)s'"""
+ return run_query(QUERY, {"database": database, "table": table})[0]["engine_full"]
+
+
+def parse_engine(engine_full):
+ engine = engine_full.split("(")[0].strip()
+ params = engine_full.split("(")[1].split(")")[0].split(",")
+ return engine, params
+
+
+def is_sharded_table(database, table):
+ return "sharded" in table_engine_full(database, table).lower()
diff --git a/housewatch/migrations/0011_scheduledbackup_is_sharded_and_more.py b/housewatch/migrations/0011_scheduledbackup_is_sharded_and_more.py
new file mode 100644
index 0000000..ae32402
--- /dev/null
+++ b/housewatch/migrations/0011_scheduledbackup_is_sharded_and_more.py
@@ -0,0 +1,23 @@
+# Generated by Django 4.1.1 on 2024-01-31 06:36
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('housewatch', '0010_scheduledbackup_incremental_schedule_and_more'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='scheduledbackup',
+ name='is_sharded',
+ field=models.BooleanField(default=False),
+ ),
+ migrations.AlterField(
+ model_name='scheduledbackup',
+ name='table',
+ field=models.CharField(blank=True, max_length=255, null=True),
+ ),
+ ]
diff --git a/housewatch/models/backup.py b/housewatch/models/backup.py
index b512b00..c19b61e 100644
--- a/housewatch/models/backup.py
+++ b/housewatch/models/backup.py
@@ -19,6 +19,7 @@ class ScheduledBackup(models.Model):
incremental_schedule: models.CharField = models.CharField(max_length=255, null=True)
table: models.CharField = models.CharField(max_length=255, null=True, blank=True)
database: models.CharField = models.CharField(max_length=255)
+ is_sharded: models.BooleanField = models.BooleanField(default=False)
cluster: models.CharField = models.CharField(max_length=255, null=True)
bucket: models.CharField = models.CharField(max_length=255)
path: models.CharField = models.CharField(max_length=255)