mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 08:11:13 -05:00
Add database maintenance tooling
- Implement maintenance hooks (useMaintenanceTables, useVacuumTable, useAnalyzeTable, useReindexTable) - Add DatabaseMaintenance page and UI for vacuum/analyze/reindex - Wire new route / admin/database-maintenance and sidebar entry - Remove DatabaseMaintenance icon usage on page and align with AdminLayout props
This commit is contained in:
@@ -70,6 +70,7 @@ const AdminUsers = lazy(() => import("./pages/AdminUsers"));
|
||||
const AdminBlog = lazy(() => import("./pages/AdminBlog"));
|
||||
const AdminSettings = lazy(() => import("./pages/AdminSettings"));
|
||||
const AdminDatabaseStats = lazy(() => import("./pages/AdminDatabaseStats"));
|
||||
const DatabaseMaintenance = lazy(() => import("./pages/admin/DatabaseMaintenance"));
|
||||
const AdminContact = lazy(() => import("./pages/admin/AdminContact"));
|
||||
const AdminEmailSettings = lazy(() => import("./pages/admin/AdminEmailSettings"));
|
||||
const ErrorMonitoring = lazy(() => import("./pages/admin/ErrorMonitoring"));
|
||||
@@ -423,6 +424,14 @@ function AppContent(): React.JSX.Element {
|
||||
</AdminErrorBoundary>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/admin/database-maintenance"
|
||||
element={
|
||||
<AdminErrorBoundary section="Database Maintenance">
|
||||
<DatabaseMaintenance />
|
||||
</AdminErrorBoundary>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* Utility routes - lazy loaded */}
|
||||
<Route path="/force-logout" element={<ForceLogout />} />
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { LayoutDashboard, FileText, Flag, Users, Settings, ArrowLeft, ScrollText, BookOpen, Inbox, Mail, AlertTriangle, Shield, Activity, BarChart } from 'lucide-react';
|
||||
import { LayoutDashboard, FileText, Flag, Users, Settings, ArrowLeft, ScrollText, BookOpen, Inbox, Mail, AlertTriangle, Shield, Activity, BarChart, Database } from 'lucide-react';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
import { useUserRole } from '@/hooks/useUserRole';
|
||||
import { useSidebar } from '@/hooks/useSidebar';
|
||||
@@ -73,6 +73,12 @@ export function AdminSidebar() {
|
||||
url: '/admin/database-stats',
|
||||
icon: BarChart,
|
||||
},
|
||||
{
|
||||
title: 'Database Maintenance',
|
||||
url: '/admin/database-maintenance',
|
||||
icon: Database,
|
||||
visible: isSuperuser, // Only superusers can access
|
||||
},
|
||||
{
|
||||
title: 'Users',
|
||||
url: '/admin/users',
|
||||
@@ -134,7 +140,7 @@ export function AdminSidebar() {
|
||||
<SidebarGroupLabel>Navigation</SidebarGroupLabel>
|
||||
<SidebarGroupContent>
|
||||
<SidebarMenu>
|
||||
{navItems.map((item) => (
|
||||
{navItems.filter(item => item.visible !== false).map((item) => (
|
||||
<SidebarMenuItem key={item.url}>
|
||||
<SidebarMenuButton asChild tooltip={collapsed ? item.title : undefined}>
|
||||
<NavLink
|
||||
|
||||
135
src/hooks/admin/useDatabaseMaintenance.ts
Normal file
135
src/hooks/admin/useDatabaseMaintenance.ts
Normal file
@@ -0,0 +1,135 @@
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { supabase } from '@/integrations/supabase/client';
|
||||
import { queryKeys } from '@/lib/queryKeys';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
export interface MaintenanceTable {
|
||||
table_name: string;
|
||||
row_count: number;
|
||||
table_size: string;
|
||||
indexes_size: string;
|
||||
total_size: string;
|
||||
}
|
||||
|
||||
export interface MaintenanceResult {
|
||||
table_name: string;
|
||||
operation: string;
|
||||
started_at: string;
|
||||
completed_at: string;
|
||||
duration_ms: number;
|
||||
success: boolean;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export function useMaintenanceTables() {
|
||||
return useQuery({
|
||||
queryKey: queryKeys.admin.maintenanceTables(),
|
||||
queryFn: async () => {
|
||||
const { data, error } = await supabase.rpc('get_maintenance_tables');
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
return data as unknown as MaintenanceTable[];
|
||||
},
|
||||
staleTime: 2 * 60 * 1000, // 2 minutes
|
||||
});
|
||||
}
|
||||
|
||||
export function useVacuumTable() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (tableName: string) => {
|
||||
const { data, error } = await supabase.rpc('run_vacuum_table', {
|
||||
table_name: tableName,
|
||||
});
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
return data as unknown as MaintenanceResult;
|
||||
},
|
||||
onSuccess: (result) => {
|
||||
if (result.success) {
|
||||
toast.success(`Vacuum completed on ${result.table_name}`, {
|
||||
description: `Duration: ${Math.round(result.duration_ms)}ms`,
|
||||
});
|
||||
} else {
|
||||
toast.error(`Vacuum failed on ${result.table_name}`, {
|
||||
description: result.error,
|
||||
});
|
||||
}
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.admin.maintenanceTables() });
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
toast.error('Vacuum operation failed', {
|
||||
description: error.message,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useAnalyzeTable() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (tableName: string) => {
|
||||
const { data, error } = await supabase.rpc('run_analyze_table', {
|
||||
table_name: tableName,
|
||||
});
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
return data as unknown as MaintenanceResult;
|
||||
},
|
||||
onSuccess: (result) => {
|
||||
if (result.success) {
|
||||
toast.success(`Analyze completed on ${result.table_name}`, {
|
||||
description: `Duration: ${Math.round(result.duration_ms)}ms`,
|
||||
});
|
||||
} else {
|
||||
toast.error(`Analyze failed on ${result.table_name}`, {
|
||||
description: result.error,
|
||||
});
|
||||
}
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.admin.maintenanceTables() });
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
toast.error('Analyze operation failed', {
|
||||
description: error.message,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useReindexTable() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (tableName: string) => {
|
||||
const { data, error } = await supabase.rpc('run_reindex_table', {
|
||||
table_name: tableName,
|
||||
});
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
return data as unknown as MaintenanceResult;
|
||||
},
|
||||
onSuccess: (result) => {
|
||||
if (result.success) {
|
||||
toast.success(`Reindex completed on ${result.table_name}`, {
|
||||
description: `Duration: ${Math.round(result.duration_ms)}ms`,
|
||||
});
|
||||
} else {
|
||||
toast.error(`Reindex failed on ${result.table_name}`, {
|
||||
description: result.error,
|
||||
});
|
||||
}
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.admin.maintenanceTables() });
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
toast.error('Reindex operation failed', {
|
||||
description: error.message,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -6853,6 +6853,16 @@ export type Database = {
|
||||
Args: { _profile_user_id: string; _viewer_id?: string }
|
||||
Returns: Json
|
||||
}
|
||||
get_maintenance_tables: {
|
||||
Args: never
|
||||
Returns: {
|
||||
indexes_size: string
|
||||
row_count: number
|
||||
table_name: string
|
||||
table_size: string
|
||||
total_size: string
|
||||
}[]
|
||||
}
|
||||
get_my_sessions: {
|
||||
Args: never
|
||||
Returns: {
|
||||
@@ -7115,6 +7125,7 @@ export type Database = {
|
||||
Returns: string
|
||||
}
|
||||
run_all_cleanup_jobs: { Args: never; Returns: Json }
|
||||
run_analyze_table: { Args: { table_name: string }; Returns: Json }
|
||||
run_data_retention_cleanup: { Args: never; Returns: Json }
|
||||
run_pipeline_monitoring: {
|
||||
Args: never
|
||||
@@ -7124,6 +7135,7 @@ export type Database = {
|
||||
status: string
|
||||
}[]
|
||||
}
|
||||
run_reindex_table: { Args: { table_name: string }; Returns: Json }
|
||||
run_system_maintenance: {
|
||||
Args: never
|
||||
Returns: {
|
||||
@@ -7132,6 +7144,7 @@ export type Database = {
|
||||
task: string
|
||||
}[]
|
||||
}
|
||||
run_vacuum_table: { Args: { table_name: string }; Returns: Json }
|
||||
set_config_value: {
|
||||
Args: {
|
||||
is_local?: boolean
|
||||
|
||||
@@ -103,6 +103,7 @@ export const queryKeys = {
|
||||
admin: {
|
||||
databaseStats: () => ['admin', 'database-stats'] as const,
|
||||
recentAdditions: (limit: number) => ['admin', 'recent-additions', limit] as const,
|
||||
maintenanceTables: () => ['admin', 'maintenance-tables'] as const,
|
||||
},
|
||||
|
||||
// Analytics queries
|
||||
|
||||
224
src/pages/admin/DatabaseMaintenance.tsx
Normal file
224
src/pages/admin/DatabaseMaintenance.tsx
Normal file
@@ -0,0 +1,224 @@
|
||||
import { useState } from 'react';
|
||||
import { AdminLayout } from '@/components/layout/AdminLayout';
|
||||
import { AdminPageLayout } from '@/components/admin';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@/components/ui/table';
|
||||
import {
|
||||
useMaintenanceTables,
|
||||
useVacuumTable,
|
||||
useAnalyzeTable,
|
||||
useReindexTable,
|
||||
} from '@/hooks/admin/useDatabaseMaintenance';
|
||||
import { Database, RefreshCw, Zap, Settings, AlertTriangle } from 'lucide-react';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
|
||||
export default function DatabaseMaintenance() {
|
||||
const { data: tables, isLoading, refetch } = useMaintenanceTables();
|
||||
const vacuumMutation = useVacuumTable();
|
||||
const analyzeMutation = useAnalyzeTable();
|
||||
const reindexMutation = useReindexTable();
|
||||
|
||||
const [selectedTable, setSelectedTable] = useState<string | null>(null);
|
||||
|
||||
const handleVacuum = (tableName: string) => {
|
||||
setSelectedTable(tableName);
|
||||
vacuumMutation.mutate(tableName);
|
||||
};
|
||||
|
||||
const handleAnalyze = (tableName: string) => {
|
||||
setSelectedTable(tableName);
|
||||
analyzeMutation.mutate(tableName);
|
||||
};
|
||||
|
||||
const handleReindex = (tableName: string) => {
|
||||
setSelectedTable(tableName);
|
||||
reindexMutation.mutate(tableName);
|
||||
};
|
||||
|
||||
const isOperationPending = (tableName: string) => {
|
||||
return (
|
||||
selectedTable === tableName &&
|
||||
(vacuumMutation.isPending || analyzeMutation.isPending || reindexMutation.isPending)
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<AdminLayout>
|
||||
<AdminPageLayout
|
||||
title="Database Maintenance"
|
||||
description="Run vacuum, analyze, and reindex operations on database tables"
|
||||
>
|
||||
<Alert variant="default" className="mb-6">
|
||||
<AlertTriangle className="h-4 w-4" />
|
||||
<AlertTitle>Superuser Access Required</AlertTitle>
|
||||
<AlertDescription>
|
||||
These operations require superuser privileges. They can help improve database
|
||||
performance by reclaiming storage, updating statistics, and rebuilding indexes.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
<div className="grid gap-6 mb-6 md:grid-cols-3">
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">VACUUM</CardTitle>
|
||||
<Database className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<CardDescription className="text-xs">
|
||||
Reclaims storage occupied by dead tuples and makes space available for reuse
|
||||
</CardDescription>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">ANALYZE</CardTitle>
|
||||
<Zap className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<CardDescription className="text-xs">
|
||||
Updates statistics used by the query planner for optimal query execution plans
|
||||
</CardDescription>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">REINDEX</CardTitle>
|
||||
<Settings className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<CardDescription className="text-xs">
|
||||
Rebuilds indexes to eliminate bloat and restore optimal index performance
|
||||
</CardDescription>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<CardTitle>Database Tables</CardTitle>
|
||||
<CardDescription>
|
||||
Select maintenance operations to perform on each table
|
||||
</CardDescription>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => refetch()}
|
||||
disabled={isLoading}
|
||||
>
|
||||
<RefreshCw className={`mr-2 h-4 w-4 ${isLoading ? 'animate-spin' : ''}`} />
|
||||
Refresh
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{isLoading ? (
|
||||
<div className="space-y-4">
|
||||
{Array.from({ length: 5 }).map((_, i) => (
|
||||
<Skeleton key={i} className="h-12 w-full" />
|
||||
))}
|
||||
</div>
|
||||
) : tables && tables.length > 0 ? (
|
||||
<div className="rounded-md border">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Table Name</TableHead>
|
||||
<TableHead className="text-right">Rows</TableHead>
|
||||
<TableHead className="text-right">Table Size</TableHead>
|
||||
<TableHead className="text-right">Indexes Size</TableHead>
|
||||
<TableHead className="text-right">Total Size</TableHead>
|
||||
<TableHead className="text-right">Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{tables.map((table) => (
|
||||
<TableRow key={table.table_name}>
|
||||
<TableCell className="font-medium">
|
||||
<code className="text-sm">{table.table_name}</code>
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
{table.row_count?.toLocaleString() || 'N/A'}
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<Badge variant="secondary">{table.table_size}</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<Badge variant="secondary">{table.indexes_size}</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<Badge variant="outline">{table.total_size}</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<div className="flex gap-2 justify-end">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleVacuum(table.table_name)}
|
||||
disabled={isOperationPending(table.table_name)}
|
||||
>
|
||||
{isOperationPending(table.table_name) &&
|
||||
vacuumMutation.isPending ? (
|
||||
<RefreshCw className="h-3 w-3 animate-spin" />
|
||||
) : (
|
||||
'Vacuum'
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleAnalyze(table.table_name)}
|
||||
disabled={isOperationPending(table.table_name)}
|
||||
>
|
||||
{isOperationPending(table.table_name) &&
|
||||
analyzeMutation.isPending ? (
|
||||
<RefreshCw className="h-3 w-3 animate-spin" />
|
||||
) : (
|
||||
'Analyze'
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleReindex(table.table_name)}
|
||||
disabled={isOperationPending(table.table_name)}
|
||||
>
|
||||
{isOperationPending(table.table_name) &&
|
||||
reindexMutation.isPending ? (
|
||||
<RefreshCw className="h-3 w-3 animate-spin" />
|
||||
) : (
|
||||
'Reindex'
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-12 text-muted-foreground">
|
||||
No tables available
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</AdminPageLayout>
|
||||
</AdminLayout>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
-- Database Maintenance Functions
|
||||
-- These functions allow authorized users to perform database maintenance operations
|
||||
|
||||
-- Function to run VACUUM on a specific table
|
||||
CREATE OR REPLACE FUNCTION run_vacuum_table(table_name text)
|
||||
RETURNS jsonb
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER
|
||||
SET search_path = public
|
||||
AS $$
|
||||
DECLARE
|
||||
result jsonb;
|
||||
start_time timestamp;
|
||||
end_time timestamp;
|
||||
BEGIN
|
||||
-- Only allow superusers to run this
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM profiles
|
||||
WHERE id = auth.uid()
|
||||
AND role = 'superuser'
|
||||
) THEN
|
||||
RAISE EXCEPTION 'Only superusers can perform database maintenance';
|
||||
END IF;
|
||||
|
||||
start_time := clock_timestamp();
|
||||
|
||||
-- Execute VACUUM on the specified table
|
||||
EXECUTE format('VACUUM ANALYZE %I', table_name);
|
||||
|
||||
end_time := clock_timestamp();
|
||||
|
||||
result := jsonb_build_object(
|
||||
'table_name', table_name,
|
||||
'operation', 'VACUUM ANALYZE',
|
||||
'started_at', start_time,
|
||||
'completed_at', end_time,
|
||||
'duration_ms', EXTRACT(MILLISECONDS FROM (end_time - start_time)),
|
||||
'success', true
|
||||
);
|
||||
|
||||
RETURN result;
|
||||
EXCEPTION
|
||||
WHEN OTHERS THEN
|
||||
RETURN jsonb_build_object(
|
||||
'table_name', table_name,
|
||||
'operation', 'VACUUM ANALYZE',
|
||||
'success', false,
|
||||
'error', SQLERRM
|
||||
);
|
||||
END;
|
||||
$$;
|
||||
|
||||
-- Function to run ANALYZE on a specific table
|
||||
CREATE OR REPLACE FUNCTION run_analyze_table(table_name text)
|
||||
RETURNS jsonb
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER
|
||||
SET search_path = public
|
||||
AS $$
|
||||
DECLARE
|
||||
result jsonb;
|
||||
start_time timestamp;
|
||||
end_time timestamp;
|
||||
BEGIN
|
||||
-- Only allow superusers to run this
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM profiles
|
||||
WHERE id = auth.uid()
|
||||
AND role = 'superuser'
|
||||
) THEN
|
||||
RAISE EXCEPTION 'Only superusers can perform database maintenance';
|
||||
END IF;
|
||||
|
||||
start_time := clock_timestamp();
|
||||
|
||||
-- Execute ANALYZE on the specified table
|
||||
EXECUTE format('ANALYZE %I', table_name);
|
||||
|
||||
end_time := clock_timestamp();
|
||||
|
||||
result := jsonb_build_object(
|
||||
'table_name', table_name,
|
||||
'operation', 'ANALYZE',
|
||||
'started_at', start_time,
|
||||
'completed_at', end_time,
|
||||
'duration_ms', EXTRACT(MILLISECONDS FROM (end_time - start_time)),
|
||||
'success', true
|
||||
);
|
||||
|
||||
RETURN result;
|
||||
EXCEPTION
|
||||
WHEN OTHERS THEN
|
||||
RETURN jsonb_build_object(
|
||||
'table_name', table_name,
|
||||
'operation', 'ANALYZE',
|
||||
'success', false,
|
||||
'error', SQLERRM
|
||||
);
|
||||
END;
|
||||
$$;
|
||||
|
||||
-- Function to run REINDEX on a specific table
|
||||
CREATE OR REPLACE FUNCTION run_reindex_table(table_name text)
|
||||
RETURNS jsonb
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER
|
||||
SET search_path = public
|
||||
AS $$
|
||||
DECLARE
|
||||
result jsonb;
|
||||
start_time timestamp;
|
||||
end_time timestamp;
|
||||
BEGIN
|
||||
-- Only allow superusers to run this
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM profiles
|
||||
WHERE id = auth.uid()
|
||||
AND role = 'superuser'
|
||||
) THEN
|
||||
RAISE EXCEPTION 'Only superusers can perform database maintenance';
|
||||
END IF;
|
||||
|
||||
start_time := clock_timestamp();
|
||||
|
||||
-- Execute REINDEX on the specified table
|
||||
EXECUTE format('REINDEX TABLE %I', table_name);
|
||||
|
||||
end_time := clock_timestamp();
|
||||
|
||||
result := jsonb_build_object(
|
||||
'table_name', table_name,
|
||||
'operation', 'REINDEX',
|
||||
'started_at', start_time,
|
||||
'completed_at', end_time,
|
||||
'duration_ms', EXTRACT(MILLISECONDS FROM (end_time - start_time)),
|
||||
'success', true
|
||||
);
|
||||
|
||||
RETURN result;
|
||||
EXCEPTION
|
||||
WHEN OTHERS THEN
|
||||
RETURN jsonb_build_object(
|
||||
'table_name', table_name,
|
||||
'operation', 'REINDEX',
|
||||
'success', false,
|
||||
'error', SQLERRM
|
||||
);
|
||||
END;
|
||||
$$;
|
||||
|
||||
-- Function to get list of tables that can be maintained
|
||||
CREATE OR REPLACE FUNCTION get_maintenance_tables()
|
||||
RETURNS TABLE (
|
||||
table_name text,
|
||||
row_count bigint,
|
||||
table_size text,
|
||||
indexes_size text,
|
||||
total_size text
|
||||
)
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER
|
||||
SET search_path = public
|
||||
AS $$
|
||||
BEGIN
|
||||
-- Only allow superusers to view this
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM profiles
|
||||
WHERE id = auth.uid()
|
||||
AND role = 'superuser'
|
||||
) THEN
|
||||
RAISE EXCEPTION 'Only superusers can view database maintenance information';
|
||||
END IF;
|
||||
|
||||
RETURN QUERY
|
||||
SELECT
|
||||
t.tablename::text,
|
||||
(xpath('/row/cnt/text()', xml_count))[1]::text::bigint as row_count,
|
||||
pg_size_pretty(pg_total_relation_size(quote_ident(t.tablename)::regclass) - pg_indexes_size(quote_ident(t.tablename)::regclass)) as table_size,
|
||||
pg_size_pretty(pg_indexes_size(quote_ident(t.tablename)::regclass)) as indexes_size,
|
||||
pg_size_pretty(pg_total_relation_size(quote_ident(t.tablename)::regclass)) as total_size
|
||||
FROM pg_tables t
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT table_to_xml(t.tablename::regclass, false, true, '') as xml_count
|
||||
) x ON true
|
||||
WHERE t.schemaname = 'public'
|
||||
ORDER BY pg_total_relation_size(quote_ident(t.tablename)::regclass) DESC;
|
||||
END;
|
||||
$$;
|
||||
Reference in New Issue
Block a user