-
×
-
-
![Expanded view]()
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
×
+
+
![Expanded view]()
+
+
-
+
\ No newline at end of file
diff --git a/docs/styles.css b/docs/styles.css
index e0885d3..1496df3 100644
--- a/docs/styles.css
+++ b/docs/styles.css
@@ -1,61 +1,61 @@
:root {
- /* Techno color theme with sleek blues, purples, and cyans */
- --bg-primary: #0a0e27;
- --bg-secondary: #151b3d;
- --bg-tertiary: #1a2351;
- --text-primary: #e8ecf5;
- --text-secondary: #a1aac4;
- --accent-cyan: #00d4ff;
- --accent-purple: #7c3aed;
- --accent-pink: #ec4899;
- --glass-light: rgba(255, 255, 255, 0.05);
- --glass-border: rgba(255, 255, 255, 0.08);
- --code-bg: #0f1428;
- --code-border: #1e2851;
- --font-main: "Inter", sans-serif;
- --font-mono: "Fira Code", monospace;
+ /* Techno color theme with sleek blues, purples, and cyans */
+ --bg-primary: #0a0e27;
+ --bg-secondary: #151b3d;
+ --bg-tertiary: #1a2351;
+ --text-primary: #e8ecf5;
+ --text-secondary: #a1aac4;
+ --accent-cyan: #00d4ff;
+ --accent-purple: #7c3aed;
+ --accent-pink: #ec4899;
+ --glass-light: rgba(255, 255, 255, 0.05);
+ --glass-border: rgba(255, 255, 255, 0.08);
+ --code-bg: #0f1428;
+ --code-border: #1e2851;
+ --font-main: "Inter", sans-serif;
+ --font-mono: "Fira Code", monospace;
}
.light-mode {
- /* Light Theme */
- --bg-primary: #f8fafc;
- --bg-secondary: #f1f5f9;
- --bg-tertiary: #e2e8f0;
- --text-primary: #0f172a;
- --text-secondary: #64748b;
- --accent-cyan: #0891b2;
- --accent-purple: #7c3aed;
- --accent-pink: #db2777;
- --glass-light: rgba(0, 0, 0, 0.02);
- --glass-border: rgba(0, 0, 0, 0.08);
- --code-bg: #ffffff;
- --code-border: #e2e8f0;
+ /* Light Theme */
+ --bg-primary: #f8fafc;
+ --bg-secondary: #f1f5f9;
+ --bg-tertiary: #e2e8f0;
+ --text-primary: #0f172a;
+ --text-secondary: #64748b;
+ --accent-cyan: #0891b2;
+ --accent-purple: #7c3aed;
+ --accent-pink: #db2777;
+ --glass-light: rgba(0, 0, 0, 0.02);
+ --glass-border: rgba(0, 0, 0, 0.08);
+ --code-bg: #ffffff;
+ --code-border: #e2e8f0;
}
* {
- margin: 0;
- padding: 0;
- box-sizing: border-box;
- scroll-behavior: smooth;
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+ scroll-behavior: smooth;
}
body {
- font-family: var(--font-main);
- background: var(--bg-primary);
- color: var(--text-primary);
- line-height: 1.6;
- overflow-x: hidden;
- transition: background 0.3s ease, color 0.3s ease;
- /* Subtle animated gradient background */
- background-image: radial-gradient(at 0% 50%, rgba(0, 212, 255, 0.08) 0px, transparent 40%),
- radial-gradient(at 100% 100%, rgba(124, 58, 237, 0.08) 0px, transparent 40%);
- background-attachment: fixed;
+ font-family: var(--font-main);
+ background: var(--bg-primary);
+ color: var(--text-primary);
+ line-height: 1.6;
+ overflow-x: hidden;
+ transition: background 0.3s ease, color 0.3s ease;
+ /* Subtle animated gradient background */
+ background-image: radial-gradient(at 0% 50%, rgba(0, 212, 255, 0.08) 0px, transparent 40%),
+ radial-gradient(at 100% 100%, rgba(124, 58, 237, 0.08) 0px, transparent 40%);
+ background-attachment: fixed;
}
.container {
- max-width: 1200px;
- margin: 0 auto;
- padding: 0 2rem;
+ max-width: 1200px;
+ margin: 0 auto;
+ padding: 0 2rem;
}
/* Typography */
@@ -63,2529 +63,2734 @@ h1,
h2,
h3,
h4 {
- line-height: 1.2;
- margin-bottom: 1rem;
+ line-height: 1.2;
+ margin-bottom: 1rem;
}
h1 {
- font-size: 3.5rem;
- font-weight: 800;
- letter-spacing: -0.02em;
- margin-bottom: 1.5rem;
+ font-size: 3.5rem;
+ font-weight: 800;
+ letter-spacing: -0.02em;
+ margin-bottom: 1.5rem;
}
h2 {
- font-size: 2.5rem;
- font-weight: 700;
- text-align: center;
- margin-bottom: 1rem;
+ font-size: 2.5rem;
+ font-weight: 700;
+ text-align: center;
+ margin-bottom: 1rem;
}
h3 {
- font-size: 1.5rem;
- font-weight: 600;
+ font-size: 1.5rem;
+ font-weight: 600;
}
.gradient-text {
- background: linear-gradient(to right, var(--accent-cyan), var(--accent-purple), var(--accent-pink));
- -webkit-background-clip: text;
- background-clip: text;
- -webkit-text-fill-color: transparent;
- background-size: 200% auto;
- animation: gradient 5s ease infinite;
+ background: linear-gradient(to right, var(--accent-cyan), var(--accent-purple), var(--accent-pink));
+ -webkit-background-clip: text;
+ background-clip: text;
+ -webkit-text-fill-color: transparent;
+ background-size: 200% auto;
+ animation: gradient 5s ease infinite;
}
@keyframes gradient {
- 0% {
- background-position: 0% center;
- }
+ 0% {
+ background-position: 0% center;
+ }
- 50% {
- background-position: 100% center;
- }
+ 50% {
+ background-position: 100% center;
+ }
- 100% {
- background-position: 0% center;
- }
+ 100% {
+ background-position: 0% center;
+ }
}
/* Header */
header {
- padding: 1.5rem 0;
- position: fixed;
- width: 100%;
- top: 0;
- z-index: 100;
- backdrop-filter: blur(12px);
- background: rgba(10, 14, 39, 0.7);
- border-bottom: 1px solid var(--glass-border);
- transition: background 0.3s ease;
+ padding: 1.5rem 0;
+ position: fixed;
+ width: 100%;
+ top: 0;
+ z-index: 100;
+ backdrop-filter: blur(12px);
+ background: rgba(10, 14, 39, 0.7);
+ border-bottom: 1px solid var(--glass-border);
+ transition: background 0.3s ease;
}
.light-mode header {
- background: rgba(248, 250, 252, 0.85);
+ background: rgba(248, 250, 252, 0.85);
}
nav {
- display: flex;
- justify-content: space-between;
- align-items: center;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
}
.logo {
- font-size: 1.5rem;
- font-weight: 700;
- display: flex;
- align-items: center;
- gap: 0.5rem;
+ font-size: 1.5rem;
+ font-weight: 700;
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
}
.nav-links {
- display: flex;
- gap: 2rem;
- align-items: center;
+ display: flex;
+ gap: 2rem;
+ align-items: center;
}
.nav-links a {
- color: var(--text-primary);
- text-decoration: none;
- font-weight: 500;
- transition: color 0.3s ease;
- font-size: 0.95rem;
+ color: var(--text-primary);
+ text-decoration: none;
+ font-weight: 500;
+ transition: color 0.3s ease;
+ font-size: 0.95rem;
}
.nav-links a:hover {
- color: var(--accent-cyan);
+ color: var(--accent-cyan);
}
.nav-cta {
- background: linear-gradient(135deg, var(--accent-cyan), var(--accent-purple));
- color: #fff !important;
- padding: 0.6rem 1.2rem;
- border-radius: 8px;
- font-weight: 600 !important;
- transition: transform 0.2s ease, box-shadow 0.2s ease !important;
- box-shadow: 0 4px 12px rgba(0, 212, 255, 0.2);
+ background: linear-gradient(135deg, var(--accent-cyan), var(--accent-purple));
+ color: #fff !important;
+ padding: 0.6rem 1.2rem;
+ border-radius: 8px;
+ font-weight: 600 !important;
+ transition: transform 0.2s ease, box-shadow 0.2s ease !important;
+ box-shadow: 0 4px 12px rgba(0, 212, 255, 0.2);
}
.nav-cta:hover {
- transform: translateY(-2px);
- box-shadow: 0 8px 20px rgba(0, 212, 255, 0.3);
+ transform: translateY(-2px);
+ box-shadow: 0 8px 20px rgba(0, 212, 255, 0.3);
}
/* Theme Toggle */
.theme-toggle {
- background: var(--glass-light);
- border: 1px solid var(--glass-border);
- cursor: pointer;
- color: var(--text-primary);
- font-size: 1.2rem;
- padding: 0.5rem;
- border-radius: 8px;
- transition: all 0.3s ease;
- display: flex;
- align-items: center;
- justify-content: center;
+ background: var(--glass-light);
+ border: 1px solid var(--glass-border);
+ cursor: pointer;
+ color: var(--text-primary);
+ font-size: 1.2rem;
+ padding: 0.5rem;
+ border-radius: 8px;
+ transition: all 0.3s ease;
+ display: flex;
+ align-items: center;
+ justify-content: center;
}
.theme-toggle:hover {
- background: var(--accent-purple);
- border-color: var(--accent-purple);
+ background: var(--accent-purple);
+ border-color: var(--accent-purple);
}
/* Hero Section */
.hero {
- min-height: 100vh;
- display: flex;
- align-items: center;
- justify-content: center;
- text-align: center;
- padding-top: 8rem;
- padding-bottom: 4rem;
- position: relative;
+ min-height: 100vh;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ text-align: center;
+ padding-top: 8rem;
+ padding-bottom: 4rem;
+ position: relative;
}
.hero-content {
- max-width: 900px;
- z-index: 1;
+ max-width: 900px;
+ z-index: 1;
}
.hero-mascot {
- width: 150px;
- height: auto;
- margin-bottom: 1.5rem;
- filter: drop-shadow(0 10px 20px rgba(0, 0, 0, 0.2));
- animation: float 6s ease-in-out infinite;
+ width: 150px;
+ height: auto;
+ margin-bottom: 1.5rem;
+ filter: drop-shadow(0 10px 20px rgba(0, 0, 0, 0.2));
+ animation: float 6s ease-in-out infinite;
}
.hero p {
- font-size: 1.25rem;
- color: var(--text-secondary);
- margin-bottom: 2.5rem;
- max-width: 700px;
- margin-left: auto;
- margin-right: auto;
- line-height: 1.7;
+ font-size: 1.25rem;
+ color: var(--text-secondary);
+ margin-bottom: 2.5rem;
+ max-width: 700px;
+ margin-left: auto;
+ margin-right: auto;
+ line-height: 1.7;
}
.hero-buttons {
- display: flex;
- gap: 1rem;
- justify-content: center;
- margin-bottom: 4rem;
- flex-wrap: wrap;
+ display: flex;
+ gap: 1rem;
+ justify-content: center;
+ margin-bottom: 4rem;
+ flex-wrap: wrap;
}
.btn {
- display: inline-flex;
- align-items: center;
- gap: 0.5rem;
- padding: 1rem 2rem;
- border-radius: 8px;
- font-weight: 600;
- text-decoration: none;
- transition: all 0.3s ease;
- border: none;
- cursor: pointer;
- font-size: 1rem;
+ display: inline-flex;
+ align-items: center;
+ gap: 0.5rem;
+ padding: 1rem 2rem;
+ border-radius: 8px;
+ font-weight: 600;
+ text-decoration: none;
+ transition: all 0.3s ease;
+ border: none;
+ cursor: pointer;
+ font-size: 1rem;
}
.btn-primary {
- background: linear-gradient(135deg, var(--accent-cyan), var(--accent-purple));
- color: #fff;
- box-shadow: 0 8px 20px rgba(0, 212, 255, 0.3);
+ background: linear-gradient(135deg, var(--accent-cyan), var(--accent-purple));
+ color: #fff;
+ box-shadow: 0 8px 20px rgba(0, 212, 255, 0.3);
}
.btn-primary:hover {
- transform: translateY(-3px);
- box-shadow: 0 12px 30px rgba(0, 212, 255, 0.4);
+ transform: translateY(-3px);
+ box-shadow: 0 12px 30px rgba(0, 212, 255, 0.4);
}
.btn-secondary {
- background: var(--glass-light);
- border: 1px solid var(--glass-border);
- color: var(--text-primary);
+ background: var(--glass-light);
+ border: 1px solid var(--glass-border);
+ color: var(--text-primary);
}
.btn-secondary:hover {
- background: rgba(0, 212, 255, 0.1);
- border-color: var(--accent-cyan);
+ background: rgba(0, 212, 255, 0.1);
+ border-color: var(--accent-cyan);
}
/* Notebook Cell Style */
.notebook-cell {
- display: flex;
- background: var(--code-bg);
- border-radius: 12px;
- box-shadow: 0 20px 60px rgba(0, 0, 0, 0.4);
- overflow: hidden;
- border: 1px solid var(--code-border);
- text-align: left;
- margin: 0 auto;
- max-width: 800px;
- font-family: var(--font-mono);
- position: relative;
- backdrop-filter: blur(10px);
+ display: flex;
+ background: var(--code-bg);
+ border-radius: 12px;
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.4);
+ overflow: hidden;
+ border: 1px solid var(--code-border);
+ text-align: left;
+ margin: 0 auto;
+ max-width: 800px;
+ font-family: var(--font-mono);
+ position: relative;
+ backdrop-filter: blur(10px);
}
.cell-gutter {
- width: 50px;
- background: var(--bg-secondary);
- border-right: 1px solid var(--code-border);
- display: flex;
- flex-direction: column;
- align-items: center;
- padding-top: 2.5rem;
- /* Align with code start */
+ width: 50px;
+ background: var(--bg-secondary);
+ border-right: 1px solid var(--code-border);
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ padding-top: 2.5rem;
+ /* Align with code start */
}
.play-button {
- width: 28px;
- height: 28px;
- border-radius: 6px;
- background: linear-gradient(135deg, var(--accent-cyan), var(--accent-purple));
- border: none;
- color: #fff;
- display: flex;
- align-items: center;
- justify-content: center;
- cursor: pointer;
- transition: all 0.2s;
- font-size: 12px;
+ width: 28px;
+ height: 28px;
+ border-radius: 6px;
+ background: linear-gradient(135deg, var(--accent-cyan), var(--accent-purple));
+ border: none;
+ color: #fff;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+ transition: all 0.2s;
+ font-size: 12px;
}
.play-button:hover {
- transform: scale(1.1);
- box-shadow: 0 0 15px rgba(0, 212, 255, 0.5);
+ transform: scale(1.1);
+ box-shadow: 0 0 15px rgba(0, 212, 255, 0.5);
}
.cell-content {
- flex: 1;
- display: flex;
- flex-direction: column;
+ flex: 1;
+ display: flex;
+ flex-direction: column;
}
.code-lens {
- padding: 0.75rem 1rem;
- font-size: 0.8rem;
- color: var(--text-secondary);
- display: flex;
- gap: 1.5rem;
- background: var(--bg-secondary);
- border-bottom: 1px solid var(--code-border);
+ padding: 0.75rem 1rem;
+ font-size: 0.8rem;
+ color: var(--text-secondary);
+ display: flex;
+ gap: 1.5rem;
+ background: var(--bg-secondary);
+ border-bottom: 1px solid var(--code-border);
}
.code-lens a {
- color: var(--text-secondary);
- text-decoration: none;
- cursor: pointer;
- transition: color 0.2s;
+ color: var(--text-secondary);
+ text-decoration: none;
+ cursor: pointer;
+ transition: color 0.2s;
}
.code-lens a:hover {
- color: var(--accent-cyan);
+ color: var(--accent-cyan);
}
.code-editor {
- padding: 1rem;
- font-size: 0.9rem;
- line-height: 1.6;
- overflow-x: auto;
- color: var(--text-primary);
+ padding: 1rem;
+ font-size: 0.9rem;
+ line-height: 1.6;
+ overflow-x: auto;
+ color: var(--text-primary);
}
.result-toolbar {
- padding: 0.75rem 1rem;
- background: var(--bg-secondary);
- border-top: 1px solid var(--code-border);
- display: flex;
- justify-content: flex-end;
- gap: 0.5rem;
+ padding: 0.75rem 1rem;
+ background: var(--bg-secondary);
+ border-top: 1px solid var(--code-border);
+ display: flex;
+ justify-content: flex-end;
+ gap: 0.5rem;
}
.toolbar-btn {
- background: transparent;
- border: 1px solid var(--glass-border);
- color: var(--text-muted);
- cursor: pointer;
- padding: 4px 8px;
- border-radius: 4px;
- font-size: 0.8rem;
- transition: all 0.2s;
- display: flex;
- align-items: center;
- gap: 4px;
+ background: transparent;
+ border: 1px solid var(--glass-border);
+ color: var(--text-muted);
+ cursor: pointer;
+ padding: 4px 8px;
+ border-radius: 4px;
+ font-size: 0.8rem;
+ transition: all 0.2s;
+ display: flex;
+ align-items: center;
+ gap: 4px;
}
.toolbar-btn:hover {
- background: rgba(255, 255, 255, 0.05);
- color: var(--text-color);
- border-color: var(--text-muted);
+ background: rgba(255, 255, 255, 0.05);
+ color: var(--text-color);
+ border-color: var(--text-muted);
}
/* Syntax Highlighting (VS Code Dark+ inspired) */
.kwd {
- color: #f472b6;
+ color: #f472b6;
}
/* Keyword */
.func {
- color: #60a5fa;
+ color: #60a5fa;
}
/* Function */
.str {
- color: #34d399;
+ color: #34d399;
}
/* String */
.num {
- color: #fbbf24;
+ color: #fbbf24;
}
/* Number */
.comment {
- color: #6b7280;
- font-style: italic;
+ color: #6b7280;
+ font-style: italic;
}
/* Comment */
.type {
- color: #818cf8;
+ color: #818cf8;
}
/* Type */
/* Light Mode Syntax Overrides */
.light-mode .kwd {
- color: #af00db;
+ color: #af00db;
}
.light-mode .func {
- color: #795e26;
+ color: #795e26;
}
.light-mode .str {
- color: #a31515;
+ color: #a31515;
}
.light-mode .num {
- color: #098658;
+ color: #098658;
}
.light-mode .comment {
- color: #008000;
+ color: #008000;
}
.light-mode .type {
- color: #267f99;
+ color: #267f99;
}
/* Result Grid Simulation */
.result-grid {
- background: var(--bg-color);
+ background: var(--bg-color);
}
.grid-header {
- display: grid;
- grid-template-columns: 1fr 1fr 1fr;
- background: var(--grid-header-bg);
- font-size: 0.8rem;
- font-weight: 600;
- color: var(--text-muted);
- border-bottom: 1px solid var(--grid-border);
+ display: grid;
+ grid-template-columns: 1fr 1fr 1fr;
+ background: var(--grid-header-bg);
+ font-size: 0.8rem;
+ font-weight: 600;
+ color: var(--text-muted);
+ border-bottom: 1px solid var(--grid-border);
}
.grid-cell {
- padding: 0.5rem 1rem;
- border-right: 1px solid var(--grid-border);
+ padding: 0.5rem 1rem;
+ border-right: 1px solid var(--grid-border);
}
.grid-row {
- display: grid;
- grid-template-columns: 1fr 1fr 1fr;
- font-size: 0.85rem;
- color: var(--text-color);
- border-bottom: 1px solid var(--grid-border);
+ display: grid;
+ grid-template-columns: 1fr 1fr 1fr;
+ font-size: 0.85rem;
+ color: var(--text-color);
+ border-bottom: 1px solid var(--grid-border);
}
.grid-row:last-child {
- border-bottom: none;
+ border-bottom: none;
}
/* Badge Row */
.badge-row {
- display: flex;
- gap: 1rem;
- justify-content: center;
- margin-bottom: 2rem;
+ display: flex;
+ gap: 1rem;
+ justify-content: center;
+ margin-bottom: 2rem;
}
.badge {
- display: inline-flex;
- align-items: center;
- gap: 0.5rem;
- padding: 0.5rem 1.2rem;
- background: var(--glass-light);
- border: 1px solid var(--glass-border);
- border-radius: 8px;
- font-size: 0.85rem;
- text-decoration: none;
- color: var(--text-primary);
- transition: all 0.3s ease;
+ display: inline-flex;
+ align-items: center;
+ gap: 0.5rem;
+ padding: 0.5rem 1.2rem;
+ background: var(--glass-light);
+ border: 1px solid var(--glass-border);
+ border-radius: 8px;
+ font-size: 0.85rem;
+ text-decoration: none;
+ color: var(--text-primary);
+ transition: all 0.3s ease;
}
.badge:hover {
- border-color: var(--accent-cyan);
- background: rgba(0, 212, 255, 0.08);
+ border-color: var(--accent-cyan);
+ background: rgba(0, 212, 255, 0.08);
}
.badge-label {
- color: var(--text-secondary);
+ color: var(--text-secondary);
}
.badge-value {
- color: var(--accent-cyan);
- font-weight: 600;
+ color: var(--accent-cyan);
+ font-weight: 600;
}
.badge-beta {
- background: linear-gradient(135deg, rgba(56, 189, 248, 0.2), rgba(129, 140, 248, 0.2));
- border-color: var(--primary-color);
+ background: linear-gradient(135deg, rgba(56, 189, 248, 0.2), rgba(129, 140, 248, 0.2));
+ border-color: var(--primary-color);
}
/* Hero Tagline */
.hero-tagline {
- font-size: 1.25rem;
- color: var(--text-muted);
- margin-bottom: 2.5rem;
- max-width: 700px;
- margin-left: auto;
- margin-right: auto;
+ font-size: 1.25rem;
+ color: var(--text-muted);
+ margin-bottom: 2.5rem;
+ max-width: 700px;
+ margin-left: auto;
+ margin-right: auto;
}
.hero-tagline strong {
- color: var(--text-color);
+ color: var(--text-color);
}
/* Button Variants */
.btn-outline {
- background: transparent;
- border: 1px solid var(--glass-border);
- color: var(--text-primary);
+ background: transparent;
+ border: 1px solid var(--glass-border);
+ color: var(--text-primary);
}
.btn-outline:hover {
- border-color: var(--accent-cyan);
- background: rgba(0, 212, 255, 0.05);
+ border-color: var(--accent-cyan);
+ background: rgba(0, 212, 255, 0.05);
}
.btn-lg {
- padding: 1.2rem 2.5rem;
- font-size: 1.1rem;
+ padding: 1.2rem 2.5rem;
+ font-size: 1.1rem;
}
.btn-icon {
- margin-right: 0.25rem;
+ margin-right: 0.25rem;
}
/* Cell Number */
.cell-number {
- font-size: 0.75rem;
- color: var(--text-secondary);
- margin-top: 0.5rem;
+ font-size: 0.75rem;
+ color: var(--text-secondary);
+ margin-top: 0.5rem;
}
/* Code Lens AI */
.lens-ai {
- color: var(--accent-cyan) !important;
- font-weight: 500;
+ color: var(--accent-cyan) !important;
+ font-weight: 500;
}
/* Result Info */
.result-info {
- color: var(--accent-cyan);
- font-size: 0.85rem;
+ color: var(--accent-cyan);
+ font-size: 0.85rem;
}
.toolbar-actions {
- display: flex;
- gap: 0.5rem;
+ display: flex;
+ gap: 0.5rem;
}
/* Stats Bar */
.stats-bar {
- padding: 3rem 0;
- background: var(--bg-secondary);
- border-top: 1px solid var(--glass-border);
- border-bottom: 1px solid var(--glass-border);
+ padding: 3rem 0;
+ background: var(--bg-secondary);
+ border-top: 1px solid var(--glass-border);
+ border-bottom: 1px solid var(--glass-border);
}
.stats-grid {
- display: flex;
- justify-content: center;
- gap: 4rem;
- flex-wrap: wrap;
+ display: flex;
+ justify-content: center;
+ gap: 4rem;
+ flex-wrap: wrap;
}
.stat-item {
- display: flex;
- align-items: center;
- gap: 1rem;
+ display: flex;
+ align-items: center;
+ gap: 1rem;
}
.stat-icon {
- font-size: 2rem;
+ font-size: 2rem;
}
.stat-content {
- display: flex;
- flex-direction: column;
+ display: flex;
+ flex-direction: column;
}
.stat-value {
- font-size: 1.5rem;
- font-weight: 700;
- color: var(--text-primary);
- min-width: 60px;
+ font-size: 1.5rem;
+ font-weight: 700;
+ color: var(--text-primary);
+ min-width: 60px;
}
.stat-label {
- font-size: 0.85rem;
- color: var(--text-secondary);
+ font-size: 0.85rem;
+ color: var(--text-secondary);
}
/* Loading animation */
.loading-dot {
- display: inline-block;
- width: 8px;
- height: 8px;
- background: var(--accent-cyan);
- border-radius: 50%;
- animation: pulse 1.4s ease-in-out infinite;
+ display: inline-block;
+ width: 8px;
+ height: 8px;
+ background: var(--accent-cyan);
+ border-radius: 50%;
+ animation: pulse 1.4s ease-in-out infinite;
}
@keyframes pulse {
- 0%,
- 80%,
- 100% {
- transform: scale(0.6);
- opacity: 0.5;
- }
+ 0%,
+ 80%,
+ 100% {
+ transform: scale(0.6);
+ opacity: 0.5;
+ }
- 40% {
- transform: scale(1);
- opacity: 1;
- }
+ 40% {
+ transform: scale(1);
+ opacity: 1;
+ }
}
/* Why Section */
.why-grid {
- display: grid;
- grid-template-columns: repeat(4, 1fr);
- gap: 2rem;
+ display: grid;
+ grid-template-columns: repeat(4, 1fr);
+ gap: 2rem;
}
@media (max-width: 1200px) {
- .why-grid {
- grid-template-columns: repeat(2, 1fr);
- }
+ .why-grid {
+ grid-template-columns: repeat(2, 1fr);
+ }
}
@media (max-width: 600px) {
- .why-grid {
- grid-template-columns: repeat(2, 1fr);
- gap: 0.75rem;
- }
+ .why-grid {
+ grid-template-columns: repeat(2, 1fr);
+ gap: 0.75rem;
+ }
}
.why-card {
- background: var(--glass-bg);
- border: 1px solid var(--glass-border);
- padding: 2rem;
- border-radius: 1rem;
- transition: all 0.3s ease;
+ background: var(--glass-bg);
+ border: 1px solid var(--glass-border);
+ padding: 2rem;
+ border-radius: 1rem;
+ transition: all 0.3s ease;
}
.why-card:hover {
- border-color: var(--primary-color);
- transform: translateY(-3px);
+ border-color: var(--primary-color);
+ transform: translateY(-3px);
}
.why-icon {
- font-size: 2.5rem;
- margin-bottom: 1rem;
+ font-size: 2.5rem;
+ margin-bottom: 1rem;
}
.why-list {
- list-style: none;
- margin-top: 1rem;
+ list-style: none;
+ margin-top: 1rem;
}
.why-list li {
- padding: 0.4rem 0;
- color: var(--text-muted);
- font-size: 0.95rem;
+ padding: 0.4rem 0;
+ color: var(--text-muted);
+ font-size: 0.95rem;
}
.why-list li::before {
- content: "•";
- color: var(--primary-color);
- margin-right: 0.5rem;
+ content: "•";
+ color: var(--primary-color);
+ margin-right: 0.5rem;
}
/* Section Subtitle */
.section-subtitle {
- text-align: center;
- color: var(--text-secondary);
- font-size: 1.1rem;
- margin-bottom: 3rem;
+ text-align: center;
+ color: var(--text-secondary);
+ font-size: 1.1rem;
+ margin-bottom: 3rem;
}
/* Workflow Section */
.workflow-section {
- background: var(--bg-secondary);
+ background: var(--bg-secondary);
}
.workflow-diagram {
- display: flex;
- justify-content: center;
- gap: 2rem;
- flex-wrap: wrap;
- margin-bottom: 3rem;
- align-items: center;
+ display: flex;
+ justify-content: center;
+ gap: 2rem;
+ flex-wrap: wrap;
+ margin-bottom: 3rem;
+ align-items: center;
}
.workflow-step {
- display: flex;
- flex-direction: column;
- align-items: center;
- gap: 1rem;
- text-align: center;
- max-width: 180px;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 1rem;
+ text-align: center;
+ max-width: 180px;
}
.workflow-icon {
- width: 70px;
- height: 70px;
- background: rgba(0, 212, 255, 0.1);
- border: 2px solid var(--accent-cyan);
- border-radius: 12px;
- display: flex;
- align-items: center;
- justify-content: center;
- font-size: 2rem;
+ width: 70px;
+ height: 70px;
+ background: rgba(0, 212, 255, 0.1);
+ border: 2px solid var(--accent-cyan);
+ border-radius: 12px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 2rem;
}
.workflow-content h4 {
- margin-bottom: 0.5rem;
- color: var(--text-primary);
- font-size: 1.1rem;
+ margin-bottom: 0.5rem;
+ color: var(--text-primary);
+ font-size: 1.1rem;
}
.workflow-content p {
- font-size: 0.9rem;
- color: var(--text-secondary);
+ font-size: 0.9rem;
+ color: var(--text-secondary);
}
.workflow-arrow {
- font-size: 1.5rem;
- color: var(--accent-cyan);
+ font-size: 1.5rem;
+ color: var(--accent-cyan);
}
/* Explorer Demo */
.explorer-demo {
- max-width: 500px;
- margin: 0 auto;
- background: var(--code-bg);
- border: 1px solid var(--glass-border);
- border-radius: 1rem;
- padding: 1.5rem;
+ max-width: 500px;
+ margin: 0 auto;
+ background: var(--code-bg);
+ border: 1px solid var(--glass-border);
+ border-radius: 1rem;
+ padding: 1.5rem;
}
.explorer-demo h3 {
- text-align: center;
- margin-bottom: 1.5rem;
- font-size: 1.1rem;
+ text-align: center;
+ margin-bottom: 1.5rem;
+ font-size: 1.1rem;
}
.tree-view {
- font-family: var(--font-mono);
- font-size: 0.9rem;
+ font-family: var(--font-mono);
+ font-size: 0.9rem;
}
.tree-item {
- display: flex;
- align-items: center;
- gap: 0.5rem;
- padding: 0.4rem 0;
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ padding: 0.4rem 0;
}
.tree-icon {
- font-size: 1rem;
+ font-size: 1rem;
}
.tree-label {
- color: var(--text-color);
+ color: var(--text-color);
}
.tree-badge {
- font-size: 0.7rem;
- padding: 0.15rem 0.5rem;
- border-radius: 9999px;
- margin-left: auto;
+ font-size: 0.7rem;
+ padding: 0.15rem 0.5rem;
+ border-radius: 9999px;
+ margin-left: auto;
}
.tree-badge.connected {
- background: rgba(46, 204, 113, 0.2);
- color: #2ecc71;
+ background: rgba(46, 204, 113, 0.2);
+ color: #2ecc71;
}
.tree-children {
- margin-left: 1.5rem;
- border-left: 1px dashed var(--glass-border);
- padding-left: 1rem;
+ margin-left: 1.5rem;
+ border-left: 1px dashed var(--glass-border);
+ padding-left: 1rem;
}
/* Feature Cards Enhancement */
.feature-card .feature-list li::before {
- content: "";
+ content: "";
}
/* AI Section */
.ai-section {
- background: linear-gradient(180deg, rgba(0, 212, 255, 0.03), rgba(124, 58, 237, 0.03));
+ background: linear-gradient(180deg, rgba(0, 212, 255, 0.03), rgba(124, 58, 237, 0.03));
}
.ai-badge {
- display: inline-block;
- padding: 0.5rem 1.5rem;
- background: linear-gradient(135deg, var(--accent-cyan), var(--accent-purple));
- color: #fff;
- border-radius: 8px;
- font-weight: 600;
- margin: 0 auto 1rem;
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ width: fit-content;
+ padding: 0.5rem 1.25rem;
+ background: rgba(0, 212, 255, 0.1);
+ border: 1px solid rgba(0, 212, 255, 0.3);
+ color: var(--accent-cyan);
+ border-radius: 999px;
+ font-weight: 600;
+ font-size: 0.85rem;
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+ margin: 0 auto 1.5rem;
+ box-shadow: 0 0 20px rgba(0, 212, 255, 0.1);
}
.ai-capabilities {
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
- gap: 2rem;
- margin-bottom: 3rem;
+ display: grid;
+ grid-template-columns: repeat(4, 1fr);
+ gap: 1.5rem;
+ margin-bottom: 3rem;
+}
+
+@media (max-width: 1200px) {
+ .ai-capabilities {
+ grid-template-columns: repeat(2, 1fr);
+ }
+}
+
+@media (max-width: 600px) {
+ .ai-capabilities {
+ grid-template-columns: 1fr;
+ }
}
.ai-capability {
- text-align: center;
- padding: 2rem;
- background: var(--bg-secondary);
- border: 1px solid var(--glass-border);
- border-radius: 12px;
- transition: all 0.3s ease;
+ position: relative;
+ text-align: center;
+ padding: 2.25rem 1.5rem;
+ background: rgba(15, 23, 42, 0.6);
+ backdrop-filter: blur(8px);
+ border: 1px solid var(--glass-border);
+ border-radius: 16px;
+ transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
+ height: 100%;
+}
+
+.light-mode .ai-capability {
+ background: rgba(255, 255, 255, 0.7);
+ border-color: rgba(0, 0, 0, 0.05);
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05), 0 2px 4px -1px rgba(0, 0, 0, 0.03);
+}
+
+.ai-capability::before {
+ content: "";
+ position: absolute;
+ inset: 0;
+ border-radius: 16px;
+ padding: 1px;
+ background: linear-gradient(135deg, rgba(0, 212, 255, 0.3), transparent, rgba(124, 58, 237, 0.3));
+ -webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
+ mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
+ -webkit-mask-composite: xor;
+ mask-composite: exclude;
+ opacity: 0.5;
+ transition: opacity 0.4s;
}
.ai-capability:hover {
- border-color: var(--accent-cyan);
- background: var(--bg-tertiary);
- transform: translateY(-5px);
+ background: rgba(15, 23, 42, 0.75);
+ transform: translateY(-8px);
+ box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3);
+}
+
+.light-mode .ai-capability:hover {
+ background: #ffffff;
+ box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
+}
+
+.ai-capability:hover::before {
+ opacity: 1;
}
.capability-icon {
- font-size: 2.5rem;
- margin-bottom: 1rem;
+ font-size: 2.75rem;
+ margin-bottom: 1.25rem;
+ filter: drop-shadow(0 0 10px rgba(0, 212, 255, 0.2));
+ transition: transform 0.3s ease;
+}
+
+.ai-capability:hover .capability-icon {
+ transform: scale(1.15) rotate(5deg);
}
.ai-capability h4 {
- margin-bottom: 0.5rem;
- font-size: 1.1rem;
+ margin-bottom: 0.75rem;
+ font-size: 1.15rem;
+ color: var(--text-primary);
+ font-weight: 700;
}
.ai-capability p {
- font-size: 0.9rem;
- color: var(--text-secondary);
+ font-size: 0.925rem;
+ color: var(--text-muted);
+ line-height: 1.5;
}
/* AI Code Demo */
.ai-code-demo {
- max-width: 700px;
- margin: 0 auto;
- background: var(--code-bg);
- border: 1px solid var(--code-border);
- border-radius: 12px;
- overflow: hidden;
- backdrop-filter: blur(10px);
+ max-width: 700px;
+ margin: 0 auto;
+ background: var(--code-bg);
+ border: 1px solid var(--code-border);
+ border-radius: 12px;
+ overflow: hidden;
+ backdrop-filter: blur(10px);
}
.code-demo-header {
- padding: 1rem 1.5rem;
- background: var(--bg-secondary);
- border-bottom: 1px solid var(--code-border);
- font-weight: 600;
- color: var(--accent-cyan);
+ padding: 1rem 1.5rem;
+ background: var(--bg-secondary);
+ border-bottom: 1px solid var(--code-border);
+ font-weight: 600;
+ color: var(--accent-cyan);
}
.code-demo-subtitle {
- font-size: 12px;
- display: flex;
- align-items: center;
- color: #61afef;
+ font-size: 12px;
+ display: flex;
+ align-items: center;
+ color: #61afef;
}
.code-demo-content {
- padding: 1.5rem;
+ padding: 1.5rem;
}
.user-prompt {
- margin-bottom: 1.5rem;
- padding: 1rem;
- background: rgba(0, 212, 255, 0.08);
- border-left: 3px solid var(--accent-cyan);
- border-radius: 6px;
+ margin-bottom: 1.5rem;
+ padding: 1rem;
+ background: rgba(0, 212, 255, 0.08);
+ border-left: 3px solid var(--accent-cyan);
+ border-radius: 6px;
}
.prompt-label {
- font-weight: 600;
- color: var(--accent-cyan);
- margin-right: 0.5rem;
+ font-weight: 600;
+ color: var(--accent-cyan);
+ margin-right: 0.5rem;
}
.prompt-text {
- font-style: italic;
- color: var(--text-secondary);
+ font-style: italic;
+ color: var(--text-secondary);
}
.prompt-attach {
- color: var(--code-text);
- background-color: var(--code-bg);
- border-radius: 8px;
- padding: 2px 12px;
- font-weight: bold;
- font-size: 14px;
- font-family: var(--font-mono);
- font-style: normal;
- border: var(--code-border) solid 1px;
+ color: var(--code-text);
+ background-color: var(--code-bg);
+ border-radius: 8px;
+ padding: 2px 12px;
+ font-weight: bold;
+ font-size: 14px;
+ font-family: var(--font-mono);
+ font-style: normal;
+ border: var(--code-border) solid 1px;
}
.ai-response {
- padding: 1rem;
- background: rgba(124, 58, 237, 0.08);
- border-left: 3px solid var(--accent-purple);
- border-radius: 6px;
+ padding: 1rem;
+ background: rgba(124, 58, 237, 0.08);
+ border-left: 3px solid var(--accent-purple);
+ border-radius: 6px;
}
.response-label {
- display: block;
- font-weight: 600;
- color: var(--accent-purple);
- margin-bottom: 1rem;
+ display: block;
+ font-weight: 600;
+ color: var(--accent-purple);
+ margin-bottom: 1rem;
}
.code-block {
- font-family: var(--font-mono);
- font-size: 0.85rem;
- line-height: 1.6;
- color: var(--text-primary);
+ font-family: var(--font-mono);
+ font-size: 0.85rem;
+ line-height: 1.6;
+ color: var(--text-primary);
}
/* Screenshots Section */
.screenshots-section {
- background: rgba(0, 0, 0, 0.2);
+ background: rgba(0, 0, 0, 0.2);
}
.screenshots-wrapper {
- position: relative;
- display: flex;
- align-items: center;
- gap: 1rem;
+ position: relative;
+ display: flex;
+ align-items: center;
+ gap: 1rem;
}
.screenshots-scroll {
- display: flex;
- gap: 1.5rem;
- overflow-x: auto;
- scroll-behavior: smooth;
- padding: 1rem 0.5rem;
- scrollbar-width: thin;
- scrollbar-color: rgba(100, 116, 139, 0.5) transparent;
- -webkit-overflow-scrolling: touch;
+ display: flex;
+ gap: 1.5rem;
+ overflow-x: auto;
+ scroll-behavior: smooth;
+ padding: 1rem 0.5rem;
+ scrollbar-width: thin;
+ scrollbar-color: rgba(100, 116, 139, 0.5) transparent;
+ -webkit-overflow-scrolling: touch;
}
.screenshots-scroll::-webkit-scrollbar {
- height: 6px;
+ height: 6px;
}
.screenshots-scroll::-webkit-scrollbar-track {
- background: rgba(0, 0, 0, 0.1);
- border-radius: 3px;
+ background: rgba(0, 0, 0, 0.1);
+ border-radius: 3px;
}
.screenshots-scroll::-webkit-scrollbar-thumb {
- background: rgba(100, 116, 139, 0.4);
- border-radius: 3px;
+ background: rgba(100, 116, 139, 0.4);
+ border-radius: 3px;
}
.screenshots-scroll::-webkit-scrollbar-thumb:hover {
- background: rgba(100, 116, 139, 0.6);
+ background: rgba(100, 116, 139, 0.6);
}
.scroll-btn {
- flex-shrink: 0;
- width: 40px;
- height: 40px;
- border-radius: 50%;
- border: 1px solid var(--glass-border);
- background: var(--glass-bg);
- color: var(--text-color);
- font-size: 1.25rem;
- cursor: pointer;
- display: flex;
- align-items: center;
- justify-content: center;
- transition: all 0.2s;
- z-index: 10;
+ flex-shrink: 0;
+ width: 40px;
+ height: 40px;
+ border-radius: 50%;
+ border: 1px solid var(--glass-border);
+ background: var(--glass-bg);
+ color: var(--text-color);
+ font-size: 1.25rem;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: all 0.2s;
+ z-index: 10;
}
.scroll-btn:hover {
- background: var(--primary-color);
- border-color: var(--primary-color);
- color: white;
- transform: scale(1.1);
+ background: var(--primary-color);
+ border-color: var(--primary-color);
+ color: white;
+ transform: scale(1.1);
}
.scroll-indicator {
- text-align: center;
- margin-top: 1rem;
- color: var(--text-muted);
- font-size: 0.85rem;
+ text-align: center;
+ margin-top: 1rem;
+ color: var(--text-muted);
+ font-size: 0.85rem;
}
@media (max-width: 768px) {
- .scroll-btn {
- display: none;
- }
+ .scroll-btn {
+ display: none;
+ }
- .screenshots-scroll {
- padding: 0.5rem;
- gap: 1rem;
- }
+ .screenshots-scroll {
+ padding: 0.5rem;
+ gap: 1rem;
+ }
}
.screenshot-card {
- flex: 0 0 300px;
- min-width: 300px;
- background: var(--glass-bg);
- border: 1px solid var(--glass-border);
- border-radius: 1rem;
- overflow: hidden;
- transition: all 0.3s;
+ flex: 0 0 300px;
+ min-width: 300px;
+ background: var(--glass-bg);
+ border: 1px solid var(--glass-border);
+ border-radius: 1rem;
+ overflow: hidden;
+ transition: all 0.3s;
}
.screenshot-card:hover {
- transform: translateY(-5px);
- border-color: var(--primary-color);
+ transform: translateY(-5px);
+ border-color: var(--primary-color);
}
.screenshot-img {
- width: 100%;
- height: 200px;
- overflow: hidden;
- background: var(--code-bg);
+ width: 100%;
+ height: 200px;
+ overflow: hidden;
+ background: var(--code-bg);
}
.screenshot-img img {
- width: 100%;
- height: 100%;
- object-fit: cover;
- transition: transform 0.3s;
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ transition: transform 0.3s;
}
.screenshot-card:hover .screenshot-img img {
- transform: scale(1.05);
+ transform: scale(1.05);
}
.screenshot-card {
- cursor: pointer;
+ cursor: pointer;
}
.screenshot-caption {
- padding: 1.5rem;
+ padding: 1.5rem;
}
.screenshot-caption h4 {
- margin-bottom: 0.5rem;
+ margin-bottom: 0.5rem;
}
.screenshot-caption p {
- font-size: 0.9rem;
- color: var(--text-muted);
+ font-size: 0.9rem;
+ color: var(--text-muted);
}
/* Screenshot Modal */
.modal {
- display: none;
- position: fixed;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- z-index: 1000;
- opacity: 0;
- transition: opacity 0.3s ease;
+ display: none;
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ z-index: 1000;
+ opacity: 0;
+ transition: opacity 0.3s ease;
}
.modal.active {
- display: flex;
- justify-content: center;
- align-items: center;
- opacity: 1;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ opacity: 1;
}
.modal-backdrop {
- position: absolute;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- background: rgba(0, 0, 0, 0.5);
- backdrop-filter: blur(5px);
- -webkit-backdrop-filter: blur(5px);
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background: rgba(0, 0, 0, 0.5);
+ backdrop-filter: blur(5px);
+ -webkit-backdrop-filter: blur(5px);
}
.modal-content {
- position: relative;
- max-width: 90vw;
- max-height: 90vh;
- z-index: 1001;
- animation: modalZoomIn 0.3s ease;
+ position: relative;
+ max-width: 90vw;
+ max-height: 90vh;
+ z-index: 1001;
+ animation: modalZoomIn 0.3s ease;
}
@keyframes modalZoomIn {
- from {
- transform: scale(0.8);
- opacity: 0;
- }
+ from {
+ transform: scale(0.8);
+ opacity: 0;
+ }
- to {
- transform: scale(1);
- opacity: 1;
- }
+ to {
+ transform: scale(1);
+ opacity: 1;
+ }
}
.modal-content img {
- max-width: 100%;
- max-height: 80vh;
- border-radius: 1rem;
- box-shadow: 0 25px 50px rgba(0, 0, 0, 0.5);
+ max-width: 100%;
+ max-height: 80vh;
+ border-radius: 1rem;
+ box-shadow: 0 25px 50px rgba(0, 0, 0, 0.5);
}
.modal-close {
- position: absolute;
- top: -40px;
- right: 0;
- background: none;
- border: none;
- color: white;
- font-size: 2rem;
- cursor: pointer;
- width: 40px;
- height: 40px;
- display: flex;
- align-items: center;
- justify-content: center;
- border-radius: 50%;
- transition: all 0.2s;
+ position: absolute;
+ top: -40px;
+ right: 0;
+ background: none;
+ border: none;
+ color: white;
+ font-size: 2rem;
+ cursor: pointer;
+ width: 40px;
+ height: 40px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 50%;
+ transition: all 0.2s;
}
.modal-close:hover {
- background: rgba(255, 255, 255, 0.1);
- transform: scale(1.1);
+ background: rgba(255, 255, 255, 0.1);
+ transform: scale(1.1);
}
.modal-caption-wrapper {
- text-align: center;
- margin-top: 1rem;
+ text-align: center;
+ margin-top: 1rem;
}
#modal-caption {
- color: white;
- font-size: 1.1rem;
- font-weight: 600;
- margin-bottom: 0.5rem;
+ color: white;
+ font-size: 1.1rem;
+ font-weight: 600;
+ margin-bottom: 0.5rem;
}
#modal-subcaption {
- color: rgba(255, 255, 255, 0.7);
- font-size: 0.9rem;
- max-width: 500px;
- margin: 0 auto;
+ color: rgba(255, 255, 255, 0.7);
+ font-size: 0.9rem;
+ max-width: 500px;
+ margin: 0 auto;
}
.modal-nav {
- position: absolute;
- top: 50%;
- transform: translateY(-50%);
- background: rgba(255, 255, 255, 0.1);
- border: 1px solid rgba(255, 255, 255, 0.2);
- color: white;
- font-size: 1.5rem;
- width: 50px;
- height: 50px;
- border-radius: 50%;
- cursor: pointer;
- display: flex;
- align-items: center;
- justify-content: center;
- transition: all 0.2s;
- z-index: 1002;
+ position: absolute;
+ top: 50%;
+ transform: translateY(-50%);
+ background: rgba(255, 255, 255, 0.1);
+ border: 1px solid rgba(255, 255, 255, 0.2);
+ color: white;
+ font-size: 1.5rem;
+ width: 50px;
+ height: 50px;
+ border-radius: 50%;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: all 0.2s;
+ z-index: 1002;
}
.modal-nav:hover {
- background: var(--primary-color);
- border-color: var(--primary-color);
- transform: translateY(-50%) scale(1.1);
+ background: var(--primary-color);
+ border-color: var(--primary-color);
+ transform: translateY(-50%) scale(1.1);
}
.modal-nav:disabled {
- opacity: 0.3;
- cursor: not-allowed;
+ opacity: 0.3;
+ cursor: not-allowed;
}
.modal-nav:disabled:hover {
- background: rgba(255, 255, 255, 0.1);
- transform: translateY(-50%);
+ background: rgba(255, 255, 255, 0.1);
+ transform: translateY(-50%);
}
.modal-prev {
- left: 20px;
+ left: 20px;
}
.modal-next {
- right: 20px;
+ right: 20px;
}
.modal-counter {
- color: rgba(255, 255, 255, 0.7);
- text-align: center;
- margin-top: 0.5rem;
- font-size: 0.9rem;
+ color: rgba(255, 255, 255, 0.7);
+ text-align: center;
+ margin-top: 0.5rem;
+ font-size: 0.9rem;
}
@media (max-width: 768px) {
- .modal-nav {
- width: 40px;
- height: 40px;
- font-size: 1.25rem;
- }
+ .modal-nav {
+ width: 40px;
+ height: 40px;
+ font-size: 1.25rem;
+ }
- .modal-prev {
- left: 10px;
- }
+ .modal-prev {
+ left: 10px;
+ }
- .modal-next {
- right: 10px;
- }
+ .modal-next {
+ right: 10px;
+ }
}
/* Quick Start Enhancements */
.code-preview {
- display: flex;
- align-items: center;
- gap: 1rem;
- margin: 1rem 0;
- background: var(--code-bg);
- padding: 1rem;
- border-radius: 8px;
- border: 1px solid var(--glass-border);
+ display: flex;
+ align-items: center;
+ gap: 1rem;
+ margin: 1rem 0;
+ background: var(--code-bg);
+ padding: 1rem;
+ border-radius: 8px;
+ border: 1px solid var(--glass-border);
}
.code-preview code {
- font-family: var(--font-mono);
- flex: 1;
+ font-family: var(--font-mono);
+ flex: 1;
}
.copy-btn {
- background: none;
- border: none;
- cursor: pointer;
- font-size: 1.2rem;
- padding: 0.5rem;
- border-radius: 0.25rem;
- transition: background 0.2s;
+ background: none;
+ border: none;
+ cursor: pointer;
+ font-size: 1.2rem;
+ padding: 0.5rem;
+ border-radius: 0.25rem;
+ transition: background 0.2s;
}
.copy-btn:hover {
- background: var(--glass-border);
+ background: var(--glass-border);
}
.step-alt {
- font-size: 0.9rem;
- color: var(--text-muted);
+ font-size: 0.9rem;
+ color: var(--text-muted);
}
kbd {
- display: inline-block;
- padding: 0.2rem 0.5rem;
- background: var(--code-bg);
- border: 1px solid var(--glass-border);
- border-radius: 4px;
- font-family: var(--font-mono);
- font-size: 0.85rem;
+ display: inline-block;
+ padding: 0.2rem 0.5rem;
+ background: var(--code-bg);
+ border: 1px solid var(--glass-border);
+ border-radius: 4px;
+ font-family: var(--font-mono);
+ font-size: 0.85rem;
}
.command-sequence {
- display: flex;
- align-items: center;
- gap: 0.75rem;
- margin: 1rem 0;
- flex-wrap: wrap;
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
+ margin: 1rem 0;
+ flex-wrap: wrap;
}
.command-sequence .arrow {
- color: var(--primary-color);
+ color: var(--primary-color);
}
.command-sequence code {
- padding: 0.3rem 0.75rem;
- background: rgba(56, 189, 248, 0.1);
- border-radius: 4px;
- font-family: var(--font-mono);
+ padding: 0.3rem 0.75rem;
+ background: rgba(56, 189, 248, 0.1);
+ border-radius: 4px;
+ font-family: var(--font-mono);
}
.connection-form-demo {
- background: var(--code-bg);
- border: 1px solid var(--glass-border);
- border-radius: 8px;
- padding: 1rem;
- margin-top: 1rem;
+ background: var(--code-bg);
+ border: 1px solid var(--glass-border);
+ border-radius: 8px;
+ padding: 1rem;
+ margin-top: 1rem;
}
.form-row {
- display: flex;
- padding: 0.5rem 0;
- border-bottom: 1px solid var(--glass-border);
+ display: flex;
+ padding: 0.5rem 0;
+ border-bottom: 1px solid var(--glass-border);
}
.form-row:last-child {
- border-bottom: none;
+ border-bottom: none;
}
.form-label {
- width: 100px;
- color: var(--text-muted);
+ width: 100px;
+ color: var(--text-muted);
}
.form-value {
- color: var(--text-color);
- font-family: var(--font-mono);
+ color: var(--text-color);
+ font-family: var(--font-mono);
}
.action-list {
- list-style: none;
- margin-top: 1rem;
+ list-style: none;
+ margin-top: 1rem;
}
.action-list li {
- padding: 0.5rem 0;
- color: var(--text-muted);
+ padding: 0.5rem 0;
+ color: var(--text-muted);
}
/* Operations Table */
.operations-table-wrapper {
- overflow-x: auto;
+ overflow-x: auto;
}
.operations-table {
- width: 100%;
- border-collapse: collapse;
- background: var(--glass-bg);
- border-radius: 1rem;
- overflow: hidden;
+ width: 100%;
+ border-collapse: collapse;
+ background: var(--glass-bg);
+ border-radius: 1rem;
+ overflow: hidden;
}
.operations-table th,
.operations-table td {
- padding: 1rem 1.5rem;
- text-align: left;
- border-bottom: 1px solid var(--glass-border);
+ padding: 1rem 1.5rem;
+ text-align: left;
+ border-bottom: 1px solid var(--glass-border);
}
.operations-table th {
- background: var(--code-header-bg);
- font-weight: 600;
+ background: var(--code-header-bg);
+ font-weight: 600;
}
.operations-table tr:last-child td {
- border-bottom: none;
+ border-bottom: none;
}
.operations-table tr:hover {
- background: rgba(56, 189, 248, 0.05);
+ background: rgba(56, 189, 248, 0.05);
}
.obj-icon {
- margin-right: 0.5rem;
+ margin-right: 0.5rem;
}
/* Shortcuts Section */
.shortcuts-grid {
- display: flex;
- justify-content: center;
- gap: 2rem;
- flex-wrap: wrap;
+ display: flex;
+ justify-content: center;
+ gap: 2rem;
+ flex-wrap: wrap;
}
.shortcut-item {
- display: flex;
- flex-direction: column;
- align-items: center;
- gap: 0.75rem;
- padding: 1.5rem 2rem;
- background: var(--glass-bg);
- border: 1px solid var(--glass-border);
- border-radius: 1rem;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 0.75rem;
+ padding: 1.5rem 2rem;
+ background: var(--glass-bg);
+ border: 1px solid var(--glass-border);
+ border-radius: 1rem;
}
.shortcut-desc {
- color: var(--text-muted);
- font-size: 0.9rem;
+ color: var(--text-muted);
+ font-size: 0.9rem;
}
/* CTA Section */
.cta-section {
- background: linear-gradient(135deg, rgba(56, 189, 248, 0.1), rgba(129, 140, 248, 0.1));
+ background: linear-gradient(135deg, rgba(56, 189, 248, 0.1), rgba(129, 140, 248, 0.1));
}
.cta-content {
- text-align: center;
- max-width: 700px;
- margin: 0 auto;
+ text-align: center;
+ max-width: 700px;
+ margin: 0 auto;
}
.cta-content h2 {
- margin-bottom: 1rem;
+ margin-bottom: 1rem;
}
.cta-content>p {
- color: var(--text-muted);
- font-size: 1.1rem;
- margin-bottom: 2rem;
+ color: var(--text-muted);
+ font-size: 1.1rem;
+ margin-bottom: 2rem;
}
.cta-buttons {
- display: flex;
- gap: 1rem;
- justify-content: center;
- flex-wrap: wrap;
- margin-bottom: 1.5rem;
+ display: flex;
+ gap: 1rem;
+ justify-content: center;
+ flex-wrap: wrap;
+ margin-bottom: 1.5rem;
}
.cta-note {
- color: var(--text-muted);
- font-size: 0.9rem;
+ color: var(--text-muted);
+ font-size: 0.9rem;
}
/* Footer Enhancement */
footer {
- background: var(--bg-secondary);
- border-top: 1px solid var(--glass-border);
- padding: 3rem 0 1rem;
+ background: var(--bg-secondary);
+ border-top: 1px solid var(--glass-border);
+ padding: 3rem 0 1rem;
}
.footer-content {
- display: grid;
- grid-template-columns: 1fr 2fr;
- gap: 3rem;
- margin-bottom: 2rem;
+ display: grid;
+ grid-template-columns: 1fr 2fr;
+ gap: 3rem;
+ margin-bottom: 2rem;
}
.footer-brand {
- display: flex;
- flex-direction: column;
- gap: 0.5rem;
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
}
.footer-brand .logo {
- font-size: 1.3rem;
- font-weight: 700;
+ font-size: 1.3rem;
+ font-weight: 700;
}
.footer-brand p {
- color: var(--text-secondary);
- font-size: 0.95rem;
+ color: var(--text-secondary);
+ font-size: 0.95rem;
}
.footer-links-grid {
- display: grid;
- grid-template-columns: repeat(3, 1fr);
- gap: 2rem;
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ gap: 2rem;
}
.footer-col h4 {
- margin-bottom: 1rem;
- color: var(--text-primary);
+ margin-bottom: 1rem;
+ color: var(--text-primary);
}
.footer-col a {
- display: block;
- color: var(--text-secondary);
- text-decoration: none;
- font-size: 0.9rem;
- margin-bottom: 0.5rem;
- transition: color 0.3s ease;
+ display: block;
+ color: var(--text-secondary);
+ text-decoration: none;
+ font-size: 0.9rem;
+ margin-bottom: 0.5rem;
+ transition: color 0.3s ease;
}
.footer-col a:hover {
- color: var(--accent-cyan);
+ color: var(--accent-cyan);
}
.footer-bottom {
- border-top: 1px solid var(--glass-border);
- padding-top: 1.5rem;
- text-align: center;
- color: var(--text-secondary);
- font-size: 0.9rem;
+ border-top: 1px solid var(--glass-border);
+ padding-top: 1.5rem;
+ text-align: center;
+ color: var(--text-secondary);
+ font-size: 0.9rem;
}
.footer-bottom a {
- color: var(--accent-cyan);
- text-decoration: none;
- transition: color 0.3s ease;
+ color: var(--accent-cyan);
+ text-decoration: none;
+ transition: color 0.3s ease;
}
.footer-bottom a:hover {
- color: var(--text-primary);
+ color: var(--text-primary);
}
/* Features Grid */
.section {
- padding: 6rem 0;
- position: relative;
+ padding: 6rem 0;
+ position: relative;
}
.feature-grid {
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
- gap: 2rem;
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
+ gap: 2rem;
}
.card {
- background: var(--bg-secondary);
- border: 1px solid var(--glass-border);
- padding: 2rem;
- border-radius: 12px;
- transition: all 0.3s ease;
- backdrop-filter: blur(8px);
+ position: relative;
+ background: rgba(15, 23, 42, 0.4);
+ backdrop-filter: blur(8px);
+ border: 1px solid var(--glass-border);
+ padding: 2.25rem;
+ border-radius: 16px;
+ transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
+ height: 100%;
}
-.feature-card {
- display: flex;
- flex-direction: column;
+.light-mode .card {
+ background: rgba(255, 255, 255, 0.7);
+ border-color: rgba(0, 0, 0, 0.05);
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05), 0 2px 4px -1px rgba(0, 0, 0, 0.03);
}
-.feature-card:hover {
- border-color: var(--accent-cyan);
- background: var(--bg-tertiary);
- transform: translateY(-5px);
- box-shadow: 0 12px 30px rgba(0, 212, 255, 0.1);
+.card:hover {
+ background: rgba(15, 23, 42, 0.6);
+ transform: translateY(-8px);
+ box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3);
+ border-color: var(--accent-cyan);
}
-.card-icon {
- font-size: 2.5rem;
- margin-bottom: 1rem;
+.light-mode .card:hover {
+ background: #ffffff;
+ box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
}
-.feature-card h3 {
- margin-bottom: 0.5rem;
- color: var(--text-primary);
+.card::before {
+ content: "";
+ position: absolute;
+ inset: 0;
+ border-radius: 16px;
+ padding: 1px;
+ background: linear-gradient(135deg, rgba(0, 212, 255, 0.2), transparent, rgba(124, 58, 237, 0.2));
+ -webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
+ mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
+ -webkit-mask-composite: xor;
+ mask-composite: exclude;
+ opacity: 0.3;
+ transition: opacity 0.4s;
+}
+
+.card:hover::before {
+ opacity: 0.8;
+}
+
+.feature-card {
+ display: flex;
+ flex-direction: column;
+}
+
+.card-icon {
+ font-size: 2.75rem;
+ margin-bottom: 1.25rem;
+ filter: drop-shadow(0 0 10px rgba(0, 212, 255, 0.1));
+ transition: transform 0.3s ease;
+}
+
+.card:hover .card-icon {
+ transform: scale(1.1) translateY(-2px);
+}
+
+.feature-card h3 {
+ margin-bottom: 0.75rem;
+ color: var(--text-primary);
+ font-weight: 700;
+ font-size: 1.25rem;
}
.feature-card p {
- color: var(--text-secondary);
- margin-bottom: 1.5rem;
- font-size: 0.95rem;
+ color: var(--text-secondary);
+ margin-bottom: 1.5rem;
+ font-size: 0.95rem;
+ line-height: 1.5;
}
.feature-list {
- list-style: none;
- display: flex;
- flex-direction: column;
- gap: 0.5rem;
+ list-style: none;
+ display: flex;
+ flex-direction: column;
+ gap: 0.6rem;
+ margin-top: auto;
}
.feature-list li {
- color: var(--text-secondary);
- font-size: 0.9rem;
+ color: var(--text-secondary);
+ font-size: 0.9rem;
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+}
+
+.feature-list li::before {
+ content: "✓";
+ color: var(--accent-cyan);
+ font-weight: bold;
+ font-size: 0.8rem;
+}
+
+#features {
+ padding: 6rem 0;
+ background: linear-gradient(180deg, rgba(124, 58, 237, 0.02), rgba(0, 212, 255, 0.02));
+}
+
+.feature-badge {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ width: fit-content;
+ padding: 0.5rem 1.25rem;
+ background: rgba(124, 58, 237, 0.1);
+ border: 1px solid rgba(124, 58, 237, 0.3);
+ color: var(--accent-purple);
+ border-radius: 999px;
+ font-weight: 600;
+ font-size: 0.85rem;
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+ margin: 0 auto 1.5rem;
+ box-shadow: 0 0 20px rgba(124, 58, 237, 0.1);
+}
+
+.feature-grid {
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ gap: 2rem;
+ margin-top: 2rem;
+}
+
+.highlight-row {
+ grid-template-columns: repeat(2, 1fr) !important;
+ margin-top: 2.5rem !important;
+}
+
+.highlight-cyan {
+ border-color: rgba(0, 212, 255, 0.4) !important;
+ background: linear-gradient(135deg, rgba(0, 212, 255, 0.08), transparent) !important;
+}
+
+.light-mode .highlight-cyan {
+ background: linear-gradient(135deg, rgba(0, 212, 255, 0.1), #ffffff) !important;
+ border-color: rgba(0, 212, 255, 0.2) !important;
+}
+
+.highlight-purple {
+ border-color: rgba(124, 58, 237, 0.4) !important;
+ background: linear-gradient(135deg, rgba(124, 58, 237, 0.08), transparent) !important;
+}
+
+.light-mode .highlight-purple {
+ background: linear-gradient(135deg, rgba(124, 58, 237, 0.1), #ffffff) !important;
+ border-color: rgba(124, 58, 237, 0.2) !important;
+}
+
+.highlight-cyan:hover {
+ border-color: var(--accent-cyan) !important;
+ box-shadow: 0 20px 40px rgba(0, 212, 255, 0.2) !important;
+}
+
+.highlight-purple:hover {
+ border-color: var(--accent-purple) !important;
+ box-shadow: 0 20px 40px rgba(124, 58, 237, 0.2) !important;
+}
+
+.light-mode .highlight-cyan:hover {
+ background: #ffffff !important;
+ box-shadow: 0 20px 25px -5px rgba(0, 212, 255, 0.2) !important;
+}
+
+.light-mode .highlight-purple:hover {
+ background: #ffffff !important;
+ box-shadow: 0 20px 25px -5px rgba(124, 58, 237, 0.2) !important;
+}
+
+@media (max-width: 1024px) {
+
+ .feature-grid,
+ .highlight-row {
+ grid-template-columns: repeat(2, 1fr) !important;
+ }
+}
+
+@media (max-width: 768px) {
+
+ .feature-grid,
+ .highlight-row {
+ grid-template-columns: 1fr !important;
+ }
}
/* Steps / Timeline */
.steps {
- max-width: 800px;
- margin: 0 auto;
+ max-width: 800px;
+ margin: 0 auto;
}
.step {
- display: flex;
- gap: 2rem;
- margin-bottom: 3rem;
- position: relative;
+ display: flex;
+ gap: 2rem;
+ margin-bottom: 3rem;
+ position: relative;
}
.step:not(:last-child)::after {
- content: "";
- position: absolute;
- left: 24px;
- top: 60px;
- bottom: -40px;
- width: 2px;
- background: var(--glass-border);
+ content: "";
+ position: absolute;
+ left: 24px;
+ top: 60px;
+ bottom: -40px;
+ width: 2px;
+ background: var(--glass-border);
}
.step-number {
- width: 50px;
- height: 50px;
- background: var(--glass-bg);
- border: 1px solid var(--primary-color);
- border-radius: 50%;
- display: flex;
- align-items: center;
- justify-content: center;
- font-size: 1.5rem;
- font-weight: 700;
- color: var(--primary-color);
- flex-shrink: 0;
- z-index: 1;
+ width: 50px;
+ height: 50px;
+ background: var(--glass-bg);
+ border: 1px solid var(--primary-color);
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 1.5rem;
+ font-weight: 700;
+ color: var(--primary-color);
+ flex-shrink: 0;
+ z-index: 1;
}
.step-content {
- flex: 1;
- background: var(--glass-bg);
- padding: 2rem;
- border-radius: 1rem;
- border: 1px solid var(--glass-border);
+ flex: 1;
+ background: var(--glass-bg);
+ padding: 2rem;
+ border-radius: 1rem;
+ border: 1px solid var(--glass-border);
}
/* Footer */
footer {
- padding: 4rem 0 2rem;
- border-top: 1px solid var(--glass-border);
- background: var(--glass-bg);
- text-align: center;
+ padding: 4rem 0 2rem;
+ border-top: 1px solid var(--glass-border);
+ background: var(--glass-bg);
+ text-align: center;
}
.footer-links {
- display: flex;
- justify-content: center;
- gap: 2rem;
- margin-bottom: 2rem;
+ display: flex;
+ justify-content: center;
+ gap: 2rem;
+ margin-bottom: 2rem;
}
.footer-links a {
- color: var(--text-muted);
- text-decoration: none;
- transition: color 0.3s;
+ color: var(--text-muted);
+ text-decoration: none;
+ transition: color 0.3s;
}
.footer-links a:hover {
- color: var(--primary-color);
+ color: var(--primary-color);
}
/* Animations */
@keyframes gradient {
- 0% {
- background-position: 0% 50%;
- }
+ 0% {
+ background-position: 0% 50%;
+ }
- 50% {
- background-position: 100% 50%;
- }
+ 50% {
+ background-position: 100% 50%;
+ }
- 100% {
- background-position: 0% 50%;
- }
+ 100% {
+ background-position: 0% 50%;
+ }
}
@keyframes float {
- 0% {
- transform: translateY(0px);
- }
+ 0% {
+ transform: translateY(0px);
+ }
- 50% {
- transform: translateY(-10px);
- }
+ 50% {
+ transform: translateY(-10px);
+ }
- 100% {
- transform: translateY(0px);
- }
+ 100% {
+ transform: translateY(0px);
+ }
}
/* Mobile Menu */
.mobile-menu-btn {
- display: none;
- background: none;
- border: none;
- color: var(--text-color);
- font-size: 1.5rem;
- cursor: pointer;
- padding: 0.5rem;
- z-index: 101;
+ display: none;
+ background: none;
+ border: none;
+ color: var(--text-color);
+ font-size: 1.5rem;
+ cursor: pointer;
+ padding: 0.5rem;
+ z-index: 101;
}
@media (max-width: 768px) {
- h1 {
- font-size: 2.2rem;
- }
-
- h2 {
- font-size: 1.8rem;
- }
-
- .container {
- padding: 0 1rem;
- }
-
- .nav-links {
- gap: 1rem;
- }
-
- .hero-buttons {
- flex-direction: column;
- align-items: center;
- }
-
- .step {
- flex-direction: column;
- gap: 1rem;
- }
-
- .step:not(:last-child)::after {
- display: none;
- }
-
- .notebook-cell {
- margin: 0 1rem;
- }
-
- .stats-grid {
- gap: 2rem;
- }
-
- .workflow-diagram {
- flex-direction: column;
- align-items: center;
- }
-
- .workflow-arrow {
- display: none;
- }
-
- .ai-capabilities {
- grid-template-columns: 1fr;
- }
-
- .footer-content {
- grid-template-columns: 1fr;
- text-align: center;
- gap: 2rem;
- }
-
- .footer-brand {
- display: flex;
- flex-direction: column;
- align-items: center;
- }
-
- .footer-links-grid {
- flex-direction: row;
- flex-wrap: wrap;
- justify-content: center;
- gap: 2rem;
- }
-
- .footer-col {
- min-width: 120px;
- }
-
- .shortcuts-grid {
- flex-direction: column;
- align-items: center;
- }
-
- .cta-buttons {
- flex-direction: column;
- }
+ h1 {
+ font-size: 2.2rem;
+ }
+
+ h2 {
+ font-size: 1.8rem;
+ }
+
+ .container {
+ padding: 0 1rem;
+ }
+
+ .nav-links {
+ gap: 1rem;
+ }
+
+ .hero-buttons {
+ flex-direction: column;
+ align-items: center;
+ }
+
+ .step {
+ flex-direction: column;
+ gap: 1rem;
+ }
+
+ .step:not(:last-child)::after {
+ display: none;
+ }
+
+ .notebook-cell {
+ margin: 0 1rem;
+ }
+
+ .stats-grid {
+ gap: 2rem;
+ }
+
+ .workflow-diagram {
+ flex-direction: column;
+ align-items: center;
+ }
+
+ .workflow-arrow {
+ display: none;
+ }
+
+ .ai-capabilities {
+ grid-template-columns: 1fr;
+ }
+
+ .footer-content {
+ grid-template-columns: 1fr;
+ text-align: center;
+ gap: 2rem;
+ }
+
+ .footer-brand {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ }
+
+ .footer-links-grid {
+ flex-direction: row;
+ flex-wrap: wrap;
+ justify-content: center;
+ gap: 2rem;
+ }
+
+ .footer-col {
+ min-width: 120px;
+ }
+
+ .shortcuts-grid {
+ flex-direction: column;
+ align-items: center;
+ }
+
+ .cta-buttons {
+ flex-direction: column;
+ }
}
/* Extra small devices (phones, 480px and below) */
@media (max-width: 480px) {
- html {
- font-size: 14px;
- }
-
- .container {
- padding: 0 0.75rem;
- }
-
- h1 {
- font-size: 1.75rem;
- line-height: 1.2;
- }
-
- h2 {
- font-size: 1.5rem;
- }
-
- h3 {
- font-size: 1.15rem;
- }
-
- h4 {
- font-size: 1rem;
- }
-
- p,
- li {
- font-size: 0.9rem;
- }
-
- .section {
- padding: 2rem 0;
- }
-
- .section-subtitle {
- font-size: 0.85rem;
- }
-
- /* Hero section */
- .hero {
- padding: 4rem 0 2rem;
- }
-
- .hero-tagline {
- font-size: 0.9rem;
- padding: 0 0.5rem;
- }
-
- .badge-row {
- flex-wrap: wrap;
- justify-content: center;
- gap: 0.5rem;
- }
-
- .badge {
- font-size: 0.7rem;
- padding: 0.3rem 0.6rem;
- }
-
- .badge-value {
- font-size: 0.75rem;
- }
-
- .hero-buttons {
- gap: 0.75rem;
- }
-
- .btn {
- font-size: 0.85rem;
- padding: 0.6rem 1rem;
- }
-
- .btn-lg {
- font-size: 0.9rem;
- padding: 0.75rem 1.25rem;
- }
-
- /* Stats bar */
- .stats-bar {
- padding: 1rem 0;
- }
-
- .stats-grid {
- grid-template-columns: repeat(2, 1fr);
- gap: 1rem;
- }
-
- .stat-item {
- flex-direction: column;
- text-align: center;
- gap: 0.25rem;
- }
-
- .stat-icon {
- font-size: 1.25rem;
- }
-
- .stat-value {
- font-size: 1.1rem;
- }
-
- .stat-label {
- font-size: 0.7rem;
- }
-
- /* Why section */
- .why-grid {
- grid-template-columns: repeat(2, 1fr);
- gap: 0.75rem;
- }
-
- .why-card {
- padding: 0.75rem;
- }
-
- .why-card h3 {
- font-size: 0.85rem;
- margin-bottom: 0.5rem;
- }
-
- .why-icon {
- font-size: 1.5rem;
- margin-bottom: 0.25rem;
- }
-
- .why-list {
- font-size: 0.7rem;
- padding-left: 0.75rem;
- }
-
- .why-list li {
- padding: 0.15rem 0;
- }
-
- .why-list li {
- padding: 0.25rem 0;
- }
-
- /* Workflow section */
- .workflow-step {
- padding: 1rem;
- min-width: unset;
- width: 100%;
- }
-
- .workflow-icon {
- font-size: 1.5rem;
- }
-
- .workflow-content h4 {
- font-size: 0.95rem;
- }
-
- .workflow-content p {
- font-size: 0.8rem;
- }
-
- .workflow-arrow {
- font-size: 1rem;
- padding: 0.25rem;
- }
-
- /* Explorer tree demo */
- .explorer-demo {
- padding: 1rem;
- margin-top: 1.5rem;
- }
-
- .explorer-demo h3 {
- font-size: 1rem;
- }
-
- .tree-view {
- font-size: 0.8rem;
- }
-
- .tree-item {
- padding: 0.25rem 0.5rem;
- }
-
- .tree-icon {
- font-size: 0.9rem;
- }
-
- .tree-badge {
- font-size: 0.6rem;
- padding: 0.1rem 0.3rem;
- }
-
- .tree-children {
- margin-left: 1rem;
- }
-
- /* Feature cards */
- .feature-grid {
- grid-template-columns: 1fr;
- gap: 1rem;
- }
-
- .feature-card {
- padding: 1.25rem;
- }
-
- .card-icon {
- font-size: 2rem;
- }
-
- .feature-list {
- font-size: 0.8rem;
- }
-
- .feature-list li {
- padding: 0.2rem 0;
- }
-
- /* AI section */
- .ai-badge {
- font-size: 0.8rem;
- padding: 0.4rem 0.8rem;
- }
-
- .ai-demo {
- padding: 1rem;
- }
-
- .ai-demo-title {
- font-size: 1rem;
- }
-
- .ai-demo-subtitle {
- font-size: 0.75rem;
- }
-
- .ai-providers {
- flex-wrap: wrap;
- gap: 0.5rem;
- }
-
- .ai-provider {
- font-size: 0.7rem;
- padding: 0.3rem 0.5rem;
- }
-
- .ai-capabilities {
- gap: 0.75rem;
- }
-
- .ai-capability {
- padding: 0.75rem;
- }
-
- .capability-icon {
- font-size: 1.25rem;
- }
-
- .ai-capability h4 {
- font-size: 0.9rem;
- }
-
- .ai-capability p {
- font-size: 0.75rem;
- }
-
- .ai-code-demo {
- margin-top: 1.5rem;
- }
-
- .code-demo-header {
- font-size: 0.85rem;
- padding: 0.5rem 0.75rem;
- }
-
- .code-demo-subtitle {
- font-size: 0.75rem;
- }
-
- .code-demo-content {
- padding: 0.75rem;
- }
-
- .user-prompt,
- .ai-response {
- font-size: 0.8rem;
- }
-
- .code-block {
- font-size: 0.75rem;
- padding: 0.75rem;
- }
-
- /* Notebook cell demo */
- .notebook-cell {
- margin: 1rem 0.5rem;
- }
-
- .cell-gutter {
- padding: 0.5rem;
- }
-
- .play-button {
- width: 24px;
- height: 24px;
- font-size: 0.7rem;
- }
-
- .cell-number {
- font-size: 0.7rem;
- }
-
- .code-lens {
- gap: 0.5rem;
- font-size: 0.7rem;
- }
-
- .code-editor {
- font-size: 0.75rem;
- padding: 0.75rem;
- }
-
- .result-toolbar {
- padding: 0.4rem 0.75rem;
- font-size: 0.7rem;
- }
-
- .toolbar-btn {
- font-size: 0.65rem;
- padding: 0.2rem 0.4rem;
- }
-
- .result-grid {
- font-size: 0.7rem;
- }
-
- .grid-cell {
- padding: 0.35rem 0.5rem;
- }
-
- /* Screenshots */
- .screenshot-card {
- flex: 0 0 250px;
- min-width: 250px;
- }
-
- .screenshot-img {
- height: 150px;
- }
-
- .screenshot-img img {
- border-radius: 0.5rem;
- }
-
- .screenshot-caption {
- padding: 0.5rem;
- }
-
- .screenshot-caption h4 {
- font-size: 0.75rem;
- margin-bottom: 0.25rem;
- }
-
- .screenshot-caption p {
- font-size: 0.65rem;
- line-height: 1.3;
- }
-
- .scroll-indicator {
- font-size: 0.75rem;
- }
-
- /* Quick start steps */
- .step {
- padding: 1rem;
- margin-bottom: 1rem;
- }
-
- .step-number {
- width: 2rem;
- height: 2rem;
- font-size: 1rem;
- }
-
- .step-content h3 {
- font-size: 1rem;
- }
-
- .step-content p {
- font-size: 0.8rem;
- }
-
- .code-preview {
- font-size: 0.7rem;
- padding: 0.5rem;
- }
-
- .code-preview code {
- word-break: break-all;
- }
-
- .copy-btn {
- padding: 0.25rem 0.4rem;
- font-size: 0.7rem;
- }
-
- .command-sequence {
- font-size: 0.75rem;
- flex-wrap: wrap;
- gap: 0.25rem;
- }
-
- .command-sequence kbd {
- font-size: 0.7rem;
- padding: 0.2rem 0.4rem;
- }
-
- .connection-form-demo {
- font-size: 0.75rem;
- padding: 0.75rem;
- }
-
- .action-list {
- font-size: 0.8rem;
- }
-
- .step-alt {
- font-size: 0.75rem;
- }
-
- .step-alt kbd {
- font-size: 0.7rem;
- }
-
- /* Operations table */
- .operations-table-wrapper {
- overflow-x: auto;
- }
-
- .operations-table {
- font-size: 0.75rem;
- min-width: 500px;
- }
-
- .operations-table th,
- .operations-table td {
- padding: 0.5rem;
- }
-
- .obj-icon {
- font-size: 0.9rem;
- }
-
- /* Shortcuts */
- .shortcut-item {
- padding: 0.75rem 1rem;
- font-size: 0.8rem;
- }
-
- .shortcut-item kbd {
- font-size: 0.7rem;
- padding: 0.2rem 0.4rem;
- }
-
- .shortcut-desc {
- font-size: 0.75rem;
- }
-
- /* CTA section */
- .cta-section {
- padding: 2rem 0;
- }
-
- .cta-content h2 {
- font-size: 1.25rem;
- }
-
- .cta-content p {
- font-size: 0.85rem;
- }
-
- .cta-note {
- font-size: 0.7rem;
- }
-
- /* Footer */
- footer {
- padding: 2rem 0 1rem;
- }
-
- .footer-content {
- gap: 1.5rem;
- }
-
- .footer-brand {
- text-align: center;
- margin-bottom: 0.5rem;
- }
-
- .footer-brand .logo {
- font-size: 1.1rem;
- justify-content: center;
- }
-
- .footer-brand p {
- font-size: 0.75rem;
- margin-top: 0.25rem;
- }
-
- .footer-links-grid {
- display: grid;
- grid-template-columns: repeat(3, 1fr);
- gap: 0.75rem;
- text-align: center;
- width: 100%;
- }
-
- .footer-col {
- display: flex;
- flex-direction: column;
- gap: 0.3rem;
- }
-
- .footer-col h4 {
- font-size: 0.75rem;
- margin-bottom: 0.25rem;
- }
-
- .footer-col a {
- font-size: 0.65rem;
- padding: 0.15rem 0;
- }
-
- .footer-bottom {
- font-size: 0.7rem;
- margin-top: 1rem;
- padding-top: 1rem;
- }
-
- /* Navigation */
- .logo {
- font-size: 1rem;
- }
-
- .nav-links a {
- font-size: 1.25rem;
- }
-
- .nav-cta {
- font-size: 0.85rem !important;
- padding: 0.5rem 1rem !important;
- }
+ html {
+ font-size: 14px;
+ }
+
+ .container {
+ padding: 0 0.75rem;
+ }
+
+ h1 {
+ font-size: 1.75rem;
+ line-height: 1.2;
+ }
+
+ h2 {
+ font-size: 1.5rem;
+ }
+
+ h3 {
+ font-size: 1.15rem;
+ }
+
+ h4 {
+ font-size: 1rem;
+ }
+
+ p,
+ li {
+ font-size: 0.9rem;
+ }
+
+ .section {
+ padding: 2rem 0;
+ }
+
+ .section-subtitle {
+ font-size: 0.85rem;
+ }
+
+ /* Hero section */
+ .hero {
+ padding: 4rem 0 2rem;
+ }
+
+ .hero-tagline {
+ font-size: 0.9rem;
+ padding: 0 0.5rem;
+ }
+
+ .badge-row {
+ flex-wrap: wrap;
+ justify-content: center;
+ gap: 0.5rem;
+ }
+
+ .badge {
+ font-size: 0.7rem;
+ padding: 0.3rem 0.6rem;
+ }
+
+ .badge-value {
+ font-size: 0.75rem;
+ }
+
+ .hero-buttons {
+ gap: 0.75rem;
+ }
+
+ .btn {
+ font-size: 0.85rem;
+ padding: 0.6rem 1rem;
+ }
+
+ .btn-lg {
+ font-size: 0.9rem;
+ padding: 0.75rem 1.25rem;
+ }
+
+ /* Stats bar */
+ .stats-bar {
+ padding: 1rem 0;
+ }
+
+ .stats-grid {
+ grid-template-columns: repeat(2, 1fr);
+ gap: 1rem;
+ }
+
+ .stat-item {
+ flex-direction: column;
+ text-align: center;
+ gap: 0.25rem;
+ }
+
+ .stat-icon {
+ font-size: 1.25rem;
+ }
+
+ .stat-value {
+ font-size: 1.1rem;
+ }
+
+ .stat-label {
+ font-size: 0.7rem;
+ }
+
+ /* Why section */
+ .why-grid {
+ grid-template-columns: repeat(2, 1fr);
+ gap: 0.75rem;
+ }
+
+ .why-card {
+ padding: 0.75rem;
+ }
+
+ .why-card h3 {
+ font-size: 0.85rem;
+ margin-bottom: 0.5rem;
+ }
+
+ .why-icon {
+ font-size: 1.5rem;
+ margin-bottom: 0.25rem;
+ }
+
+ .why-list {
+ font-size: 0.7rem;
+ padding-left: 0.75rem;
+ }
+
+ .why-list li {
+ padding: 0.15rem 0;
+ }
+
+ .why-list li {
+ padding: 0.25rem 0;
+ }
+
+ /* Workflow section */
+ .workflow-step {
+ padding: 1rem;
+ min-width: unset;
+ width: 100%;
+ }
+
+ .workflow-icon {
+ font-size: 1.5rem;
+ }
+
+ .workflow-content h4 {
+ font-size: 0.95rem;
+ }
+
+ .workflow-content p {
+ font-size: 0.8rem;
+ }
+
+ .workflow-arrow {
+ font-size: 1rem;
+ padding: 0.25rem;
+ }
+
+ /* Explorer tree demo */
+ .explorer-demo {
+ padding: 1rem;
+ margin-top: 1.5rem;
+ }
+
+ .explorer-demo h3 {
+ font-size: 1rem;
+ }
+
+ .tree-view {
+ font-size: 0.8rem;
+ }
+
+ .tree-item {
+ padding: 0.25rem 0.5rem;
+ }
+
+ .tree-icon {
+ font-size: 0.9rem;
+ }
+
+ .tree-badge {
+ font-size: 0.6rem;
+ padding: 0.1rem 0.3rem;
+ }
+
+ .tree-children {
+ margin-left: 1rem;
+ }
+
+ /* Feature cards */
+ .feature-grid {
+ grid-template-columns: 1fr;
+ gap: 1rem;
+ }
+
+ .feature-card {
+ padding: 1.25rem;
+ }
+
+ .card-icon {
+ font-size: 2rem;
+ }
+
+ .feature-list {
+ font-size: 0.8rem;
+ }
+
+ .feature-list li {
+ padding: 0.2rem 0;
+ }
+
+ /* AI section */
+ .ai-badge {
+ font-size: 0.8rem;
+ padding: 0.4rem 0.8rem;
+ }
+
+ .ai-demo {
+ padding: 1rem;
+ }
+
+ .ai-demo-title {
+ font-size: 1rem;
+ }
+
+ .ai-demo-subtitle {
+ font-size: 0.75rem;
+ }
+
+ .ai-providers {
+ flex-wrap: wrap;
+ gap: 0.5rem;
+ }
+
+ .ai-provider {
+ font-size: 0.7rem;
+ padding: 0.3rem 0.5rem;
+ }
+
+ .ai-capabilities {
+ gap: 0.75rem;
+ }
+
+ .ai-capability {
+ padding: 0.75rem;
+ }
+
+ .capability-icon {
+ font-size: 1.25rem;
+ }
+
+ .ai-capability h4 {
+ font-size: 0.9rem;
+ }
+
+ .ai-capability p {
+ font-size: 0.75rem;
+ }
+
+ .ai-code-demo {
+ margin-top: 1.5rem;
+ }
+
+ .code-demo-header {
+ font-size: 0.85rem;
+ padding: 0.5rem 0.75rem;
+ }
+
+ .code-demo-subtitle {
+ font-size: 0.75rem;
+ }
+
+ .code-demo-content {
+ padding: 0.75rem;
+ }
+
+ .user-prompt,
+ .ai-response {
+ font-size: 0.8rem;
+ }
+
+ .code-block {
+ font-size: 0.75rem;
+ padding: 0.75rem;
+ }
+
+ /* Notebook cell demo */
+ .notebook-cell {
+ margin: 1rem 0.5rem;
+ }
+
+ .cell-gutter {
+ padding: 0.5rem;
+ }
+
+ .play-button {
+ width: 24px;
+ height: 24px;
+ font-size: 0.7rem;
+ }
+
+ .cell-number {
+ font-size: 0.7rem;
+ }
+
+ .code-lens {
+ gap: 0.5rem;
+ font-size: 0.7rem;
+ }
+
+ .code-editor {
+ font-size: 0.75rem;
+ padding: 0.75rem;
+ }
+
+ .result-toolbar {
+ padding: 0.4rem 0.75rem;
+ font-size: 0.7rem;
+ }
+
+ .toolbar-btn {
+ font-size: 0.65rem;
+ padding: 0.2rem 0.4rem;
+ }
+
+ .result-grid {
+ font-size: 0.7rem;
+ }
+
+ .grid-cell {
+ padding: 0.35rem 0.5rem;
+ }
+
+ /* Screenshots */
+ .screenshot-card {
+ flex: 0 0 250px;
+ min-width: 250px;
+ }
+
+ .screenshot-img {
+ height: 150px;
+ }
+
+ .screenshot-img img {
+ border-radius: 0.5rem;
+ }
+
+ .screenshot-caption {
+ padding: 0.5rem;
+ }
+
+ .screenshot-caption h4 {
+ font-size: 0.75rem;
+ margin-bottom: 0.25rem;
+ }
+
+ .screenshot-caption p {
+ font-size: 0.65rem;
+ line-height: 1.3;
+ }
+
+ .scroll-indicator {
+ font-size: 0.75rem;
+ }
+
+ /* Quick start steps */
+ .step {
+ padding: 1rem;
+ margin-bottom: 1rem;
+ }
+
+ .step-number {
+ width: 2rem;
+ height: 2rem;
+ font-size: 1rem;
+ }
+
+ .step-content h3 {
+ font-size: 1rem;
+ }
+
+ .step-content p {
+ font-size: 0.8rem;
+ }
+
+ .code-preview {
+ font-size: 0.7rem;
+ padding: 0.5rem;
+ }
+
+ .code-preview code {
+ word-break: break-all;
+ }
+
+ .copy-btn {
+ padding: 0.25rem 0.4rem;
+ font-size: 0.7rem;
+ }
+
+ .command-sequence {
+ font-size: 0.75rem;
+ flex-wrap: wrap;
+ gap: 0.25rem;
+ }
+
+ .command-sequence kbd {
+ font-size: 0.7rem;
+ padding: 0.2rem 0.4rem;
+ }
+
+ .connection-form-demo {
+ font-size: 0.75rem;
+ padding: 0.75rem;
+ }
+
+ .action-list {
+ font-size: 0.8rem;
+ }
+
+ .step-alt {
+ font-size: 0.75rem;
+ }
+
+ .step-alt kbd {
+ font-size: 0.7rem;
+ }
+
+ /* Operations table */
+ .operations-table-wrapper {
+ overflow-x: auto;
+ }
+
+ .operations-table {
+ font-size: 0.75rem;
+ min-width: 500px;
+ }
+
+ .operations-table th,
+ .operations-table td {
+ padding: 0.5rem;
+ }
+
+ .obj-icon {
+ font-size: 0.9rem;
+ }
+
+ /* Shortcuts */
+ .shortcut-item {
+ padding: 0.75rem 1rem;
+ font-size: 0.8rem;
+ }
+
+ .shortcut-item kbd {
+ font-size: 0.7rem;
+ padding: 0.2rem 0.4rem;
+ }
+
+ .shortcut-desc {
+ font-size: 0.75rem;
+ }
+
+ /* CTA section */
+ .cta-section {
+ padding: 2rem 0;
+ }
+
+ .cta-content h2 {
+ font-size: 1.25rem;
+ }
+
+ .cta-content p {
+ font-size: 0.85rem;
+ }
+
+ .cta-note {
+ font-size: 0.7rem;
+ }
+
+ /* Footer */
+ footer {
+ padding: 2rem 0 1rem;
+ }
+
+ .footer-content {
+ gap: 1.5rem;
+ }
+
+ .footer-brand {
+ text-align: center;
+ margin-bottom: 0.5rem;
+ }
+
+ .footer-brand .logo {
+ font-size: 1.1rem;
+ justify-content: center;
+ }
+
+ .footer-brand p {
+ font-size: 0.75rem;
+ margin-top: 0.25rem;
+ }
+
+ .footer-links-grid {
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ gap: 0.75rem;
+ text-align: center;
+ width: 100%;
+ }
+
+ .footer-col {
+ display: flex;
+ flex-direction: column;
+ gap: 0.3rem;
+ }
+
+ .footer-col h4 {
+ font-size: 0.75rem;
+ margin-bottom: 0.25rem;
+ }
+
+ .footer-col a {
+ font-size: 0.65rem;
+ padding: 0.15rem 0;
+ }
+
+ .footer-bottom {
+ font-size: 0.7rem;
+ margin-top: 1rem;
+ padding-top: 1rem;
+ }
+
+ /* Navigation */
+ .logo {
+ font-size: 1rem;
+ }
+
+ .nav-links a {
+ font-size: 1.25rem;
+ }
+
+ .nav-cta {
+ font-size: 0.85rem !important;
+ padding: 0.5rem 1rem !important;
+ }
}
/* Video Carousel */
.carousel-wrapper {
- position: relative;
- max-width: 900px;
- margin: 3rem auto 1rem;
- display: flex;
- align-items: center;
+ position: relative;
+ max-width: 900px;
+ margin: 3rem auto 1rem;
+ display: flex;
+ align-items: center;
}
.video-carousel {
- display: flex;
- overflow-x: auto;
- scroll-snap-type: x mandatory;
- scroll-behavior: smooth;
- -webkit-overflow-scrolling: touch;
- gap: 1rem;
- padding: 1rem 0;
- scrollbar-width: none;
- /* Firefox */
+ display: flex;
+ overflow-x: auto;
+ scroll-snap-type: x mandatory;
+ scroll-behavior: smooth;
+ -webkit-overflow-scrolling: touch;
+ gap: 1rem;
+ padding: 1rem 0;
+ scrollbar-width: none;
+ /* Firefox */
}
.video-carousel::-webkit-scrollbar {
- display: none;
- /* Chrome, Safari, Opera */
+ display: none;
+ /* Chrome, Safari, Opera */
}
.carousel-item {
- flex: 0 0 100%;
- scroll-snap-align: center;
- background: var(--bg-primary);
- border: 1px solid var(--glass-border);
- border-radius: 12px;
- padding: 1rem;
- transition: transform 0.3s ease;
+ flex: 0 0 100%;
+ scroll-snap-align: center;
+ background: var(--bg-primary);
+ border: 1px solid var(--glass-border);
+ border-radius: 12px;
+ padding: 1rem;
+ transition: transform 0.3s ease;
}
.video-wrapper {
- border-radius: 8px;
- overflow: hidden;
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
- aspect-ratio: 16/9;
+ border-radius: 8px;
+ overflow: hidden;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
+ aspect-ratio: 16/9;
}
.video-wrapper video {
- width: 100%;
- height: 100%;
- object-fit: cover;
- display: block;
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ display: block;
}
.carousel-btn {
- background: rgba(255, 255, 255, 0.05);
- backdrop-filter: blur(10px);
- border: 1px solid var(--glass-border);
- color: var(--text-primary);
- font-size: 1.2rem;
- cursor: pointer;
- width: 48px;
- /* Fixed size circle */
- height: 48px;
- padding: 0;
- display: flex;
- align-items: center;
- justify-content: center;
- border-radius: 50%;
- transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
- z-index: 10;
- position: absolute;
- top: 50%;
- transform: translateY(-50%);
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
+ background: rgba(255, 255, 255, 0.05);
+ backdrop-filter: blur(10px);
+ border: 1px solid var(--glass-border);
+ color: var(--text-primary);
+ font-size: 1.2rem;
+ cursor: pointer;
+ width: 48px;
+ /* Fixed size circle */
+ height: 48px;
+ padding: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 50%;
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
+ z-index: 10;
+ position: absolute;
+ top: 50%;
+ transform: translateY(-50%);
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
}
.carousel-btn:hover {
- background: rgba(0, 212, 255, 0.1);
- border-color: var(--accent-cyan);
- color: var(--accent-cyan);
- box-shadow: 0 0 20px rgba(0, 212, 255, 0.3);
- transform: translateY(-50%) scale(1.1);
+ background: rgba(0, 212, 255, 0.1);
+ border-color: var(--accent-cyan);
+ color: var(--accent-cyan);
+ box-shadow: 0 0 20px rgba(0, 212, 255, 0.3);
+ transform: translateY(-50%) scale(1.1);
}
.prev-btn {
- left: -20px;
- /* Overlap slightly */
+ left: -20px;
+ /* Overlap slightly */
}
.next-btn {
- right: -20px;
- /* Overlap slightly */
+ right: -20px;
+ /* Overlap slightly */
}
@media (max-width: 900px) {
- .prev-btn {
- left: 10px;
- background: rgba(0, 0, 0, 0.5);
- }
+ .prev-btn {
+ left: 10px;
+ background: rgba(0, 0, 0, 0.5);
+ }
- .next-btn {
- right: 10px;
- background: rgba(0, 0, 0, 0.5);
- }
+ .next-btn {
+ right: 10px;
+ background: rgba(0, 0, 0, 0.5);
+ }
- .carousel-btn {
- display: flex;
- }
+ .carousel-btn {
+ display: flex;
+ }
- /* Show on mobile but overlay */
+ /* Show on mobile but overlay */
}
.carousel-indicators {
- display: flex;
- justify-content: center;
- gap: 0.5rem;
- margin-top: 1rem;
+ display: flex;
+ justify-content: center;
+ gap: 0.5rem;
+ margin-top: 1rem;
}
.indicator {
- width: 10px;
- height: 10px;
- border-radius: 50%;
- background: var(--glass-border);
- cursor: pointer;
- transition: all 0.3s;
+ width: 10px;
+ height: 10px;
+ border-radius: 50%;
+ background: var(--glass-border);
+ cursor: pointer;
+ transition: all 0.3s;
}
.indicator.active {
- background: var(--accent-cyan);
- transform: scale(1.2);
+ background: var(--accent-cyan);
+ transform: scale(1.2);
}
/* Video Expand Button */
.video-wrapper {
- position: relative;
+ position: relative;
}
.expand-btn {
- position: absolute;
- top: 10px;
- right: 10px;
- background: rgba(0, 0, 0, 0.6);
- color: #fff;
- border: none;
- border-radius: 4px;
- width: 32px;
- height: 32px;
- font-size: 1.2rem;
- cursor: pointer;
- display: flex;
- align-items: center;
- justify-content: center;
- opacity: 0;
- transition: opacity 0.3s ease, background 0.3s ease;
+ position: absolute;
+ top: 10px;
+ right: 10px;
+ background: rgba(0, 0, 0, 0.6);
+ color: #fff;
+ border: none;
+ border-radius: 4px;
+ width: 32px;
+ height: 32px;
+ font-size: 1.2rem;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ opacity: 0;
+ transition: opacity 0.3s ease, background 0.3s ease;
}
.video-wrapper:hover .expand-btn {
- opacity: 1;
+ opacity: 1;
}
.expand-btn:hover {
- background: var(--accent-cyan);
+ background: var(--accent-cyan);
}
/* Video Modal */
.modal {
- display: none;
- position: fixed;
- z-index: 1000;
- left: 0;
- top: 0;
- width: 100%;
- height: 100%;
- overflow: auto;
- background-color: rgba(0, 0, 0, 0.9);
- backdrop-filter: blur(5px);
- align-items: center;
- justify-content: center;
- align-items: center;
- justify-content: center;
- opacity: 1;
- transition: opacity 0.3s ease;
+ display: none;
+ position: fixed;
+ z-index: 1000;
+ left: 0;
+ top: 0;
+ width: 100%;
+ height: 100%;
+ overflow: auto;
+ background-color: rgba(0, 0, 0, 0.9);
+ backdrop-filter: blur(5px);
+ align-items: center;
+ justify-content: center;
+ align-items: center;
+ justify-content: center;
+ opacity: 1;
+ transition: opacity 0.3s ease;
}
.modal-content {
- position: relative;
- max-width: 90%;
- max-height: 90%;
- width: auto;
- height: auto;
- display: flex;
- justify-content: center;
+ position: relative;
+ max-width: 90%;
+ max-height: 90%;
+ width: auto;
+ height: auto;
+ display: flex;
+ justify-content: center;
}
.close-modal {
- position: absolute;
- top: 15px;
- right: 35px;
- color: #f1f1f1;
- font-size: 40px;
- font-weight: bold;
- cursor: pointer;
- transition: color 0.3s;
- z-index: 1001;
+ position: absolute;
+ top: 15px;
+ right: 35px;
+ color: #f1f1f1;
+ font-size: 40px;
+ font-weight: bold;
+ cursor: pointer;
+ transition: color 0.3s;
+ z-index: 1001;
}
.close-modal:hover,
.close-modal:focus {
- color: var(--accent-cyan);
- text-decoration: none;
- cursor: pointer;
+ color: var(--accent-cyan);
+ text-decoration: none;
+ cursor: pointer;
}
/* Optimized Mobile Topbar */
.cta-icon {
- display: none;
- font-size: 1.2rem;
+ display: none;
+ font-size: 1.2rem;
}
@media (max-width: 768px) {
- /* Hide Logo Subtitle */
- .logo-subtitle {
- display: none;
- }
-
- /* Hide Navigation Links except CTA */
- .nav-links a:not(.nav-cta) {
- display: none;
- }
-
- /* Keep Nav Links container visible (override potential mobile menu logic) */
- .nav-links {
- display: flex;
- gap: 1rem;
- }
-
- /* Optimize Install Button */
- .nav-cta {
- padding: 0.4rem 0.6rem !important;
- font-size: 0 !important;
- /* Hide text */
- }
-
- .cta-text {
- display: none;
- }
-
- .cta-icon {
- display: inline-block;
- font-size: 1.2rem;
- }
-
- /* Hide Hamburger (if not already hidden by inline style) */
- .mobile-menu-btn {
- display: none !important;
- }
-
- /* Adjust logo size */
- .logo {
- font-size: 1.2rem;
- }
+ /* Hide Logo Subtitle */
+ .logo-subtitle {
+ display: none;
+ }
+
+ /* Hide Navigation Links except CTA */
+ .nav-links a:not(.nav-cta) {
+ display: none;
+ }
+
+ /* Keep Nav Links container visible (override potential mobile menu logic) */
+ .nav-links {
+ display: flex;
+ gap: 1rem;
+ }
+
+ /* Optimize Install Button */
+ .nav-cta {
+ padding: 0.4rem 0.6rem !important;
+ font-size: 0 !important;
+ /* Hide text */
+ }
+
+ .cta-text {
+ display: none;
+ }
+
+ .cta-icon {
+ display: inline-block;
+ font-size: 1.2rem;
+ }
+
+ /* Hide Hamburger (if not already hidden by inline style) */
+ .mobile-menu-btn {
+ display: none !important;
+ }
+
+ /* Adjust logo size */
+ .logo {
+ font-size: 1.2rem;
+ }
}
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index 119e54b..2102388 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,17 +1,19 @@
{
"name": "postgres-explorer",
- "version": "0.6.6",
+ "version": "0.6.9",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "postgres-explorer",
- "version": "0.6.6",
+ "version": "0.6.9",
"license": "MIT",
"dependencies": {
+ "@types/pg-cursor": "^2.7.2",
"chart.js": "^4.5.1",
"esbuild": ">=0.25.0",
"pg": "^8.11.3",
+ "pg-cursor": "^2.15.3",
"ssh2": "^1.15.0"
},
"devDependencies": {
@@ -1933,7 +1935,6 @@
"version": "16.18.126",
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.126.tgz",
"integrity": "sha512-OTcgaiwfGFBKacvfwuHzzn1KLxH/er8mluiy8/uM3sGXHaRe73RrSIj01jow9t4kJEW633Ov+cOexXeiApTyAw==",
- "dev": true,
"license": "MIT"
},
"node_modules/@types/normalize-package-data": {
@@ -1947,7 +1948,6 @@
"version": "8.15.6",
"resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.15.6.tgz",
"integrity": "sha512-NoaMtzhxOrubeL/7UZuNTrejB4MPAJ0RpxZqXQf2qXuVlTPuG6Y8p4u9dKRaue4yjmC7ZhzVO2/Yyyn25znrPQ==",
- "dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*",
@@ -1955,6 +1955,16 @@
"pg-types": "^2.2.0"
}
},
+ "node_modules/@types/pg-cursor": {
+ "version": "2.7.2",
+ "resolved": "https://registry.npmjs.org/@types/pg-cursor/-/pg-cursor-2.7.2.tgz",
+ "integrity": "sha512-m3xT8bVFCvx98LuzbvXyuCdT/Hjdd/v8ml4jL4K1QF70Y8clOfCFdgoaEB1FWdcSwcpoFYZTJQaMD9/GQ27efQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*",
+ "@types/pg": "*"
+ }
+ },
"node_modules/@types/sarif": {
"version": "2.1.7",
"resolved": "https://registry.npmjs.org/@types/sarif/-/sarif-2.1.7.tgz",
@@ -5982,6 +5992,15 @@
"integrity": "sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w==",
"license": "MIT"
},
+ "node_modules/pg-cursor": {
+ "version": "2.15.3",
+ "resolved": "https://registry.npmjs.org/pg-cursor/-/pg-cursor-2.15.3.tgz",
+ "integrity": "sha512-eHw63TsiGtFEfAd7tOTZ+TLy+i/2ePKS20H84qCQ+aQ60pve05Okon9tKMC+YN3j6XyeFoHnaim7Lt9WVafQsA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "pg": "^8"
+ }
+ },
"node_modules/pg-int8": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz",
diff --git a/package.json b/package.json
index e790e52..6ab2580 100644
--- a/package.json
+++ b/package.json
@@ -61,6 +61,26 @@
"title": "Refresh Connections",
"icon": "$(refresh)"
},
+ {
+ "command": "postgres-explorer.filterTree",
+ "title": "Filter Tree",
+ "icon": "$(filter)"
+ },
+ {
+ "command": "postgres-explorer.clearFilter",
+ "title": "Clear Filter",
+ "icon": "$(search-remove)"
+ },
+ {
+ "command": "postgres-explorer.addToFavorites",
+ "title": "Add to Favorites",
+ "icon": "$(star-empty)"
+ },
+ {
+ "command": "postgres-explorer.removeFromFavorites",
+ "title": "Remove from Favorites",
+ "icon": "$(star-full)"
+ },
{
"command": "postgres-explorer.manageConnections",
"title": "Manage Connections",
@@ -81,6 +101,16 @@
"title": "New PostgreSQL Notebook",
"icon": "$(new-file)"
},
+ {
+ "command": "postgres-explorer.generateQuery",
+ "title": "AI: Generate Query",
+ "category": "PgStudio"
+ },
+ {
+ "command": "postgres-explorer.optimizeQuery",
+ "title": "AI: Optimize Query",
+ "category": "PgStudio"
+ },
{
"command": "postgres-explorer.connect",
"title": "Connect to PostgreSQL Database"
@@ -909,6 +939,21 @@
"type": "string",
"default": "",
"description": "Custom API endpoint (optional)"
+ },
+ "postgresExplorer.streaming.enabled": {
+ "type": "boolean",
+ "default": true,
+ "description": "Enable cursor-based streaming for large result sets"
+ },
+ "postgresExplorer.streaming.batchSize": {
+ "type": "number",
+ "default": 200,
+ "description": "Number of rows per batch when streaming results"
+ },
+ "postgresExplorer.telemetry.enabled": {
+ "type": "boolean",
+ "default": false,
+ "description": "Enable performance telemetry (opt-in, privacy-first)"
}
}
},
@@ -931,6 +976,16 @@
"when": "view == postgresExplorer",
"group": "navigation"
},
+ {
+ "command": "postgres-explorer.filterTree",
+ "when": "view == postgresExplorer && !postgresExplorer.filterActive",
+ "group": "navigation"
+ },
+ {
+ "command": "postgres-explorer.clearFilter",
+ "when": "view == postgresExplorer && postgresExplorer.filterActive",
+ "group": "navigation"
+ },
{
"command": "postgres-explorer.refreshConnections",
"when": "view == postgresExplorer",
@@ -963,6 +1018,16 @@
"when": "view == postgresExplorer && viewItem == connection",
"group": "9_delete"
},
+ {
+ "command": "postgres-explorer.addToFavorites",
+ "when": "view == postgresExplorer && viewItem =~ /^(table|view|function|materialized-view)$/ && !postgresExplorer.isFavorite",
+ "group": "0_favorites"
+ },
+ {
+ "command": "postgres-explorer.removeFromFavorites",
+ "when": "view == postgresExplorer && viewItem =~ /^(table|view|function|materialized-view)$/ && postgresExplorer.isFavorite",
+ "group": "0_favorites"
+ },
{
"command": "postgres-explorer.disconnectConnection",
"when": "view == postgresExplorer && viewItem == connection",
@@ -1603,9 +1668,11 @@
"coverage": "nyc npm run test"
},
"dependencies": {
+ "@types/pg-cursor": "^2.7.2",
"chart.js": "^4.5.1",
"esbuild": ">=0.25.0",
"pg": "^8.11.3",
+ "pg-cursor": "^2.15.3",
"ssh2": "^1.15.0"
},
"devDependencies": {
@@ -1614,8 +1681,8 @@
"@types/module-alias": "^2.0.4",
"@types/node": "^16.18.126",
"@types/pg": "^8.11.11",
- "@types/ssh2": "^1.15.0",
"@types/sinon": "^21.0.0",
+ "@types/ssh2": "^1.15.0",
"@types/vscode": "^1.80.0",
"@types/vscode-notebook-renderer": "^1.72.4",
"chai": "^6.2.1",
diff --git a/src/activation/commands.ts b/src/activation/commands.ts
new file mode 100644
index 0000000..cc2f097
--- /dev/null
+++ b/src/activation/commands.ts
@@ -0,0 +1,892 @@
+import * as vscode from 'vscode';
+import { DatabaseTreeItem } from '../providers/DatabaseTreeProvider';
+import { DatabaseTreeProvider } from '../providers/DatabaseTreeProvider';
+import { ChatViewProvider } from '../providers/ChatViewProvider';
+
+import { cmdAiAssist } from '../commands/aiAssist';
+import { showColumnProperties, copyColumnName, copyColumnNameQuoted, generateSelectStatement, generateWhereClause, generateAlterColumnScript, generateDropColumnScript, generateRenameColumnScript, addColumnComment, generateIndexOnColumn, viewColumnStatistics, cmdAddColumn } from '../commands/columns';
+import { showConstraintProperties, copyConstraintName, generateDropConstraintScript, generateAlterConstraintScript, validateConstraint, generateAddConstraintScript, viewConstraintDependencies, cmdConstraintOperations, cmdAddConstraint } from '../commands/constraints';
+import { cmdConnectDatabase, cmdDisconnectConnection, cmdDisconnectDatabase, cmdReconnectConnection } from '../commands/connection';
+import { showIndexProperties, copyIndexName, generateDropIndexScript, generateReindexScript, generateScriptCreate, analyzeIndexUsage, generateAlterIndexScript, addIndexComment, cmdIndexOperations, cmdAddIndex } from '../commands/indexes';
+import { cmdAddObjectInDatabase, cmdBackupDatabase, cmdCreateDatabase, cmdDatabaseDashboard, cmdDatabaseOperations, cmdDeleteDatabase, cmdDisconnectDatabase as cmdDisconnectDatabaseLegacy, cmdGenerateCreateScript, cmdMaintenanceDatabase, cmdPsqlTool, cmdQueryTool, cmdRestoreDatabase, cmdScriptAlterDatabase, cmdShowConfiguration } from '../commands/database';
+import { cmdDropExtension, cmdEnableExtension, cmdExtensionOperations, cmdRefreshExtension } from '../commands/extensions';
+import { cmdCreateForeignTable, cmdEditForeignTable, cmdForeignTableOperations, cmdRefreshForeignTable } from '../commands/foreignTables';
+import { cmdForeignDataWrapperOperations, cmdShowForeignDataWrapperProperties, cmdCreateForeignServer, cmdForeignServerOperations, cmdShowForeignServerProperties, cmdDropForeignServer, cmdCreateUserMapping, cmdUserMappingOperations, cmdShowUserMappingProperties, cmdDropUserMapping, cmdRefreshForeignDataWrapper, cmdRefreshForeignServer, cmdRefreshUserMapping } from '../commands/foreignDataWrappers';
+import { cmdCallFunction, cmdCreateFunction, cmdDropFunction, cmdEditFunction, cmdFunctionOperations, cmdRefreshFunction, cmdShowFunctionProperties } from '../commands/functions';
+import { cmdCreateMaterializedView, cmdDropMatView, cmdEditMatView, cmdMatViewOperations, cmdRefreshMatView, cmdViewMatViewData, cmdViewMatViewProperties } from '../commands/materializedViews';
+import { cmdNewNotebook } from '../commands/notebook';
+import { cmdCreateObjectInSchema, cmdCreateSchema, cmdSchemaOperations, cmdShowSchemaProperties } from '../commands/schema';
+import { cmdCreateTable, cmdDropTable, cmdEditTable, cmdInsertTable, cmdMaintenanceAnalyze, cmdMaintenanceReindex, cmdMaintenanceVacuum, cmdScriptCreate, cmdScriptDelete, cmdScriptInsert, cmdScriptSelect, cmdScriptUpdate, cmdShowTableProperties, cmdTableOperations, cmdTruncateTable, cmdUpdateTable, cmdViewTableData } from '../commands/tables';
+import { cmdAllOperationsTypes, cmdCreateType, cmdDropType, cmdEditTypes, cmdRefreshType, cmdShowTypeProperties } from '../commands/types';
+import { cmdAddRole, cmdAddUser, cmdDropRole, cmdEditRole, cmdGrantRevokeRole, cmdRefreshRole, cmdRoleOperations, cmdShowRoleProperties } from '../commands/usersRoles';
+import { cmdCreateView, cmdDropView, cmdEditView, cmdRefreshView, cmdScriptCreate as cmdViewScriptCreate, cmdScriptSelect as cmdViewScriptSelect, cmdShowViewProperties, cmdViewData, cmdViewOperations } from '../commands/views';
+
+import { AiSettingsPanel } from '../aiSettingsPanel';
+import { ConnectionFormPanel } from '../connectionForm';
+import { ConnectionManagementPanel } from '../connectionManagement';
+
+export function registerAllCommands(
+ context: vscode.ExtensionContext,
+ databaseTreeProvider: DatabaseTreeProvider,
+ chatViewProviderInstance: ChatViewProvider | undefined,
+ outputChannel: vscode.OutputChannel
+) {
+ const commands = [
+ {
+ command: 'postgres-explorer.addConnection',
+ callback: (connection?: any) => {
+ ConnectionFormPanel.show(context.extensionUri, context, connection);
+ }
+ },
+ {
+ command: 'postgres-explorer.refreshConnections',
+ callback: () => {
+ databaseTreeProvider.refresh();
+ }
+ },
+ {
+ command: 'postgres-explorer.filterTree',
+ callback: async () => {
+ const currentFilter = databaseTreeProvider.filterPattern;
+
+ if (currentFilter) {
+ // Filter is active - show options to modify or clear
+ const choice = await vscode.window.showQuickPick([
+ { label: '$(close) Clear Filter', value: 'clear' },
+ { label: '$(edit) Change Filter', value: 'change', description: `Current: "${currentFilter}"` }
+ ], { placeHolder: `Filter active: "${currentFilter}"` });
+
+ if (choice?.value === 'clear') {
+ databaseTreeProvider.clearFilter();
+ vscode.commands.executeCommand('setContext', 'postgresExplorer.filterActive', false);
+ vscode.window.showInformationMessage('Filter cleared');
+ } else if (choice?.value === 'change') {
+ const pattern = await vscode.window.showInputBox({
+ prompt: 'Enter filter pattern',
+ placeHolder: 'e.g., users, product, order',
+ value: currentFilter
+ });
+ if (pattern !== undefined) {
+ databaseTreeProvider.setFilter(pattern);
+ vscode.commands.executeCommand('setContext', 'postgresExplorer.filterActive', pattern.length > 0);
+ if (pattern) {
+ vscode.window.showInformationMessage(`Filter applied: "${pattern}"`);
+ }
+ }
+ }
+ } else {
+ // No filter active - show input
+ const pattern = await vscode.window.showInputBox({
+ prompt: 'Enter filter pattern',
+ placeHolder: 'e.g., users, product, order'
+ });
+ if (pattern !== undefined && pattern.length > 0) {
+ databaseTreeProvider.setFilter(pattern);
+ vscode.commands.executeCommand('setContext', 'postgresExplorer.filterActive', true);
+ vscode.window.showInformationMessage(`Filter applied: "${pattern}"`);
+ }
+ }
+ }
+ },
+ {
+ command: 'postgres-explorer.clearFilter',
+ callback: () => {
+ databaseTreeProvider.clearFilter();
+ vscode.commands.executeCommand('setContext', 'postgresExplorer.filterActive', false);
+ vscode.window.showInformationMessage('Filter cleared');
+ }
+ },
+ {
+ command: 'postgres-explorer.generateQuery',
+ callback: async () => {
+ if (!chatViewProviderInstance) {
+ vscode.window.showErrorMessage('AI Chat is not initialized');
+ return;
+ }
+
+ // Step 1: Get all connections
+ const connections = vscode.workspace.getConfiguration().get
('postgresExplorer.connections') || [];
+
+ if (connections.length === 0) {
+ vscode.window.showErrorMessage('No database connections found. Please add a connection first.');
+ return;
+ }
+
+ // Step 2: Let user select connection
+ const connectionItems = connections.map(conn => ({
+ label: conn.name,
+ description: `${conn.host}:${conn.port}/${conn.database}`,
+ connection: conn
+ }));
+
+ const selectedConnection = await vscode.window.showQuickPick(connectionItems, {
+ placeHolder: 'Select a database connection',
+ title: 'Generate Query - Select Database'
+ });
+
+ if (!selectedConnection) {
+ return;
+ }
+
+ // Step 3: Fetch database objects (tables, views, functions)
+ try {
+ const dbObjects = await databaseTreeProvider.getDbObjectsForConnection(selectedConnection.connection);
+
+ if (!dbObjects || dbObjects.length === 0) {
+ vscode.window.showWarningMessage('No tables, views, or functions found in this database.');
+ // Continue anyway, let user describe query without schema
+ const input = await vscode.window.showInputBox({
+ prompt: 'Describe the SQL query you want to generate',
+ placeHolder: 'e.g., Show me top 10 users by order count'
+ });
+
+ if (input) {
+ vscode.commands.executeCommand('postgres-explorer.chatView.focus');
+ await chatViewProviderInstance.handleGenerateQuery(input);
+ }
+ return;
+ }
+
+ // Step 4: Let user select relevant objects
+ const objectItems = dbObjects.map(obj => ({
+ label: `${obj.type === 'table' ? '📋' : obj.type === 'view' ? '👁️' : '⚙️'} ${obj.schema}.${obj.name}`,
+ description: obj.type,
+ picked: false,
+ object: obj
+ }));
+
+ const selectedObjects = await vscode.window.showQuickPick(objectItems, {
+ placeHolder: 'Select tables, views, or functions (multi-select)',
+ title: 'Generate Query - Select Database Objects',
+ canPickMany: true
+ });
+
+ if (!selectedObjects || selectedObjects.length === 0) {
+ const proceed = await vscode.window.showWarningMessage(
+ 'No objects selected. Generate query without schema context?',
+ 'Yes', 'No'
+ );
+
+ if (proceed !== 'Yes') {
+ return;
+ }
+ }
+
+ // Step 5: Get query description
+ const input = await vscode.window.showInputBox({
+ prompt: 'Describe the SQL query you want to generate',
+ placeHolder: 'e.g., Show me top 10 users by order count in the last month'
+ });
+
+ if (input) {
+ // Focus the chat view
+ vscode.commands.executeCommand('postgres-explorer.chatView.focus');
+
+ // Send to AI with schema context
+ const schemaContext = selectedObjects ? selectedObjects.map(item => item.object) : undefined;
+ await chatViewProviderInstance.handleGenerateQuery(input, schemaContext);
+ }
+ } catch (error: any) {
+ vscode.window.showErrorMessage(`Failed to fetch database objects: ${error.message}`);
+ }
+ }
+ },
+ {
+ command: 'postgres-explorer.addToFavorites',
+ callback: async (item: DatabaseTreeItem) => {
+ if (item) {
+ await databaseTreeProvider.addToFavorites(item);
+ }
+ }
+ },
+ {
+ command: 'postgres-explorer.removeFromFavorites',
+ callback: async (item: DatabaseTreeItem) => {
+ if (item) {
+ await databaseTreeProvider.removeFromFavorites(item);
+ }
+ }
+ },
+ {
+ command: 'postgres-explorer.manageConnections',
+ callback: () => {
+ ConnectionManagementPanel.show(context.extensionUri, context);
+ }
+ },
+ {
+ command: 'postgres-explorer.aiSettings',
+ callback: () => {
+ AiSettingsPanel.show(context.extensionUri, context);
+ }
+ },
+ {
+ command: 'postgres-explorer.connect',
+ callback: async (item: any) => await cmdConnectDatabase(item, context, databaseTreeProvider)
+ },
+ {
+ command: 'postgres-explorer.disconnect',
+ callback: async () => {
+ databaseTreeProvider.refresh();
+ vscode.window.showInformationMessage('Disconnected from PostgreSQL database');
+ }
+ },
+ {
+ command: 'postgres-explorer.queryTable',
+ callback: async (item: any) => {
+ if (!item || !item.schema) {
+ return;
+ }
+
+ const query = `SELECT * FROM ${item.schema}.${item.label} LIMIT 100;`;
+ const notebook = await vscode.workspace.openNotebookDocument('postgres-notebook', new vscode.NotebookData([
+ new vscode.NotebookCellData(vscode.NotebookCellKind.Code, query, 'sql')
+ ]));
+ await vscode.window.showNotebookDocument(notebook);
+ }
+ },
+ {
+ command: 'postgres-explorer.newNotebook',
+ callback: async (item: any) => await cmdNewNotebook(item)
+ },
+ {
+ command: 'postgres-explorer.refresh',
+ callback: () => databaseTreeProvider.refresh()
+ },
+ // Add database commands
+ {
+ command: 'postgres-explorer.createInDatabase',
+ callback: async (item: DatabaseTreeItem) => await cmdAddObjectInDatabase(item, context)
+ },
+ {
+ command: 'postgres-explorer.createDatabase',
+ callback: async (item: DatabaseTreeItem) => await cmdCreateDatabase(item, context)
+ },
+ {
+ command: 'postgres-explorer.dropDatabase',
+ callback: async (item: DatabaseTreeItem) => await cmdDeleteDatabase(item, context)
+ },
+ {
+ command: 'postgres-explorer.scriptAlterDatabase',
+ callback: async (item: DatabaseTreeItem) => await cmdScriptAlterDatabase(item, context)
+ },
+ {
+ command: 'postgres-explorer.databaseOperations',
+ callback: async (item: DatabaseTreeItem) => await cmdDatabaseOperations(item, context)
+ },
+ {
+ command: 'postgres-explorer.showDashboard',
+ callback: async (item: DatabaseTreeItem) => await cmdDatabaseDashboard(item, context)
+ },
+ {
+ command: 'postgres-explorer.backupDatabase',
+ callback: async (item: DatabaseTreeItem) => await cmdBackupDatabase(item, context)
+ },
+ {
+ command: 'postgres-explorer.restoreDatabase',
+ callback: async (item: DatabaseTreeItem) => await cmdRestoreDatabase(item, context)
+ },
+ {
+ command: 'postgres-explorer.generateCreateScript',
+ callback: async (item: DatabaseTreeItem) => await cmdGenerateCreateScript(item, context)
+ },
+ {
+ command: 'postgres-explorer.disconnectDatabase',
+ callback: async (item: DatabaseTreeItem) => await cmdDisconnectDatabaseLegacy(item, context)
+ },
+ {
+ command: 'postgres-explorer.maintenanceDatabase',
+ callback: async (item: DatabaseTreeItem) => await cmdMaintenanceDatabase(item, context)
+ },
+ {
+ command: 'postgres-explorer.queryTool',
+ callback: async (item: DatabaseTreeItem) => await cmdQueryTool(item, context)
+ },
+ {
+ command: 'postgres-explorer.psqlTool',
+ callback: async (item: DatabaseTreeItem) => await cmdPsqlTool(item, context)
+ },
+ {
+ command: 'postgres-explorer.showConfiguration',
+ callback: async (item: DatabaseTreeItem) => await cmdShowConfiguration(item, context)
+ },
+ // Add schema commands
+ {
+ command: 'postgres-explorer.createSchema',
+ callback: async (item: DatabaseTreeItem) => await cmdCreateSchema(item, context)
+ },
+ {
+ command: 'postgres-explorer.createInSchema',
+ callback: async (item: DatabaseTreeItem) => await cmdCreateObjectInSchema(item, context)
+ },
+ {
+ command: 'postgres-explorer.schemaOperations',
+ callback: async (item: DatabaseTreeItem) => await cmdSchemaOperations(item, context)
+ },
+ {
+ command: 'postgres-explorer.showSchemaProperties',
+ callback: async (item: DatabaseTreeItem) => await cmdShowSchemaProperties(item, context)
+ },
+ // Add table commands
+ {
+ command: 'postgres-explorer.editTable',
+ callback: async (item: DatabaseTreeItem) => await cmdEditTable(item, context)
+ },
+ {
+ command: 'postgres-explorer.viewTableData',
+ callback: async (item: DatabaseTreeItem) => {
+ await databaseTreeProvider.addToRecent(item);
+ await cmdViewTableData(item, context);
+ }
+ },
+ {
+ command: 'postgres-explorer.dropTable',
+ callback: async (item: DatabaseTreeItem) => await cmdDropTable(item, context)
+ },
+ {
+ command: 'postgres-explorer.tableOperations',
+ callback: async (item: DatabaseTreeItem) => await cmdTableOperations(item, context)
+ },
+ {
+ command: 'postgres-explorer.truncateTable',
+ callback: async (item: DatabaseTreeItem) => await cmdTruncateTable(item, context)
+ },
+ {
+ command: 'postgres-explorer.insertData',
+ callback: async (item: DatabaseTreeItem) => await cmdInsertTable(item, context)
+ },
+ {
+ command: 'postgres-explorer.updateData',
+ callback: async (item: DatabaseTreeItem) => await cmdUpdateTable(item, context)
+ },
+ {
+ command: 'postgres-explorer.showTableProperties',
+ callback: async (item: DatabaseTreeItem) => await cmdShowTableProperties(item, context)
+ },
+ // Add script commands
+ {
+ command: 'postgres-explorer.scriptSelect',
+ callback: async (item: DatabaseTreeItem) => await cmdScriptSelect(item, context)
+ },
+ {
+ command: 'postgres-explorer.scriptInsert',
+ callback: async (item: DatabaseTreeItem) => await cmdScriptInsert(item, context)
+ },
+ {
+ command: 'postgres-explorer.scriptUpdate',
+ callback: async (item: DatabaseTreeItem) => await cmdScriptUpdate(item, context)
+ },
+ {
+ command: 'postgres-explorer.scriptDelete',
+ callback: async (item: DatabaseTreeItem) => await cmdScriptDelete(item, context)
+ },
+ {
+ command: 'postgres-explorer.scriptCreate',
+ callback: async (item: DatabaseTreeItem) => await cmdScriptCreate(item, context)
+ },
+ // Add maintenance commands
+ {
+ command: 'postgres-explorer.maintenanceVacuum',
+ callback: async (item: DatabaseTreeItem) => await cmdMaintenanceVacuum(item, context)
+ },
+ {
+ command: 'postgres-explorer.maintenanceAnalyze',
+ callback: async (item: DatabaseTreeItem) => await cmdMaintenanceAnalyze(item, context)
+ },
+ {
+ command: 'postgres-explorer.maintenanceReindex',
+ callback: async (item: DatabaseTreeItem) => await cmdMaintenanceReindex(item, context)
+ },
+
+ // Add view commands
+ {
+ command: 'postgres-explorer.refreshView',
+ callback: async (item: DatabaseTreeItem) => await cmdRefreshView(item, context, databaseTreeProvider)
+ },
+ {
+ command: 'postgres-explorer.editViewDefinition',
+ callback: async (item: DatabaseTreeItem) => await cmdEditView(item, context)
+ },
+ {
+ command: 'postgres-explorer.viewViewData',
+ callback: async (item: DatabaseTreeItem) => {
+ await databaseTreeProvider.addToRecent(item);
+ await cmdViewData(item, context);
+ }
+ },
+ {
+ command: 'postgres-explorer.dropView',
+ callback: async (item: DatabaseTreeItem) => await cmdDropView(item, context)
+ },
+ {
+ command: 'postgres-explorer.viewOperations',
+ callback: async (item: DatabaseTreeItem) => await cmdViewOperations(item, context)
+ },
+ {
+ command: 'postgres-explorer.showViewProperties',
+ callback: async (item: DatabaseTreeItem) => await cmdShowViewProperties(item, context)
+ },
+ {
+ command: 'postgres-explorer.viewScriptSelect',
+ callback: async (item: DatabaseTreeItem) => await cmdViewScriptSelect(item, context)
+ },
+ {
+ command: 'postgres-explorer.viewScriptCreate',
+ callback: async (item: DatabaseTreeItem) => await cmdViewScriptCreate(item, context)
+ },
+ // Add function commands
+ {
+ command: 'postgres-explorer.refreshFunction',
+ callback: async (item: DatabaseTreeItem) => await cmdRefreshFunction(item, context, databaseTreeProvider)
+ },
+ {
+ command: 'postgres-explorer.showFunctionProperties',
+ callback: async (item: DatabaseTreeItem) => {
+ await databaseTreeProvider.addToRecent(item);
+ await cmdShowFunctionProperties(item, context);
+ }
+ },
+ {
+ command: 'postgres-explorer.functionOperations',
+ callback: async (item: DatabaseTreeItem) => await cmdFunctionOperations(item, context)
+ },
+ {
+ command: 'postgres-explorer.createReplaceFunction',
+ callback: async (item: DatabaseTreeItem) => await cmdEditFunction(item, context)
+ },
+ {
+ command: 'postgres-explorer.callFunction',
+ callback: async (item: DatabaseTreeItem) => await cmdCallFunction(item, context)
+ },
+ {
+ command: 'postgres-explorer.dropFunction',
+ callback: async (item: DatabaseTreeItem) => await cmdDropFunction(item, context)
+ },
+ // Add materialized view commands
+ {
+ command: 'postgres-explorer.refreshMaterializedView',
+ callback: async (item: DatabaseTreeItem) => await cmdRefreshMatView(item, context)
+ },
+ {
+ command: 'postgres-explorer.editMatView',
+ callback: async (item: DatabaseTreeItem) => await cmdEditMatView(item, context)
+ },
+ {
+ command: 'postgres-explorer.viewMaterializedViewData',
+ callback: async (item: DatabaseTreeItem) => await cmdViewMatViewData(item, context)
+ },
+ {
+ command: 'postgres-explorer.showMaterializedViewProperties',
+ callback: async (item: DatabaseTreeItem) => await cmdViewMatViewProperties(item, context)
+ },
+ {
+ command: 'postgres-explorer.dropMatView',
+ callback: async (item: DatabaseTreeItem) => await cmdDropMatView(item, context)
+ },
+ {
+ command: 'postgres-explorer.materializedViewOperations',
+ callback: async (item: DatabaseTreeItem) => await cmdMatViewOperations(item, context)
+ },
+ // Add type commands
+ {
+ command: 'postgres-explorer.refreshType',
+ callback: async (item: DatabaseTreeItem) => await cmdRefreshType(item, context, databaseTreeProvider)
+ },
+ {
+ command: 'postgres-explorer.typeOperations',
+ callback: async (item: DatabaseTreeItem) => await cmdAllOperationsTypes(item, context)
+ },
+ {
+ command: 'postgres-explorer.editType',
+ callback: async (item: DatabaseTreeItem) => await cmdEditTypes(item, context)
+ },
+ {
+ command: 'postgres-explorer.showTypeProperties',
+ callback: async (item: DatabaseTreeItem) => await cmdShowTypeProperties(item, context)
+ },
+ {
+ command: 'postgres-explorer.dropType',
+ callback: async (item: DatabaseTreeItem) => await cmdDropType(item, context)
+ },
+ // Add foreign table commands
+ {
+ command: 'postgres-explorer.refreshForeignTable',
+ callback: async (item: DatabaseTreeItem) => await cmdRefreshForeignTable(item, context, databaseTreeProvider)
+ },
+ {
+ command: 'postgres-explorer.foreignTableOperations',
+ callback: async (item: DatabaseTreeItem) => await cmdForeignTableOperations(item, context)
+ },
+ {
+ command: 'postgres-explorer.editForeignTable',
+ callback: async (item: DatabaseTreeItem) => await cmdEditForeignTable(item, context)
+ },
+ // Add role/user commands
+ {
+ command: 'postgres-explorer.refreshRole',
+ callback: async (item: DatabaseTreeItem) => await cmdRefreshRole(item, context, databaseTreeProvider)
+ },
+ {
+ command: 'postgres-explorer.createUser',
+ callback: async (item: DatabaseTreeItem) => await cmdAddUser(item, context)
+ },
+ {
+ command: 'postgres-explorer.createRole',
+ callback: async (item: DatabaseTreeItem) => await cmdAddRole(item, context)
+ },
+ {
+ command: 'postgres-explorer.editRole',
+ callback: async (item: DatabaseTreeItem) => await cmdEditRole(item, context)
+ },
+ {
+ command: 'postgres-explorer.grantRevoke',
+ callback: async (item: DatabaseTreeItem) => await cmdGrantRevokeRole(item, context)
+ },
+ {
+ command: 'postgres-explorer.dropRole',
+ callback: async (item: DatabaseTreeItem) => await cmdDropRole(item, context)
+ },
+ {
+ command: 'postgres-explorer.roleOperations',
+ callback: async (item: DatabaseTreeItem) => await cmdRoleOperations(item, context)
+ },
+ {
+ command: 'postgres-explorer.showRoleProperties',
+ callback: async (item: DatabaseTreeItem) => await cmdShowRoleProperties(item, context)
+ },
+ // Add extension commands
+ {
+ command: 'postgres-explorer.refreshExtension',
+ callback: async (item: DatabaseTreeItem) => await cmdRefreshExtension(item, context, databaseTreeProvider)
+ },
+ {
+ command: 'postgres-explorer.enableExtension',
+ callback: async (item: DatabaseTreeItem) => await cmdEnableExtension(item, context)
+ },
+ {
+ command: 'postgres-explorer.extensionOperations',
+ callback: async (item: DatabaseTreeItem) => await cmdExtensionOperations(item, context)
+ },
+ {
+ command: 'postgres-explorer.dropExtension',
+ callback: async (item: DatabaseTreeItem) => await cmdDropExtension(item, context)
+ },
+ // Add connection commands
+ {
+ command: 'postgres-explorer.disconnectConnection',
+ callback: async (item: DatabaseTreeItem) => await cmdDisconnectConnection(item, context, databaseTreeProvider)
+ },
+ {
+ command: 'postgres-explorer.reconnectConnection',
+ callback: async (item: DatabaseTreeItem) => await cmdReconnectConnection(item, context, databaseTreeProvider)
+ },
+ {
+ command: 'postgres-explorer.deleteConnection',
+ callback: async (item: DatabaseTreeItem) => await cmdDisconnectDatabase(item, context, databaseTreeProvider)
+ },
+
+ {
+ command: 'postgres-explorer.createTable',
+ callback: async (item: DatabaseTreeItem) => await cmdCreateTable(item, context)
+ },
+ {
+ command: 'postgres-explorer.createView',
+ callback: async (item: DatabaseTreeItem) => await cmdCreateView(item, context)
+ },
+ {
+ command: 'postgres-explorer.createFunction',
+ callback: async (item: DatabaseTreeItem) => await cmdCreateFunction(item, context)
+ },
+ {
+ command: 'postgres-explorer.createMaterializedView',
+ callback: async (item: DatabaseTreeItem) => await cmdCreateMaterializedView(item, context)
+ },
+ {
+ command: 'postgres-explorer.createType',
+ callback: async (item: DatabaseTreeItem) => await cmdCreateType(item, context)
+ },
+ {
+ command: 'postgres-explorer.createForeignTable',
+ callback: async (item: DatabaseTreeItem) => await cmdCreateForeignTable(item, context)
+ },
+ // Foreign Data Wrapper commands
+ {
+ command: 'postgres-explorer.foreignDataWrapperOperations',
+ callback: async (item: DatabaseTreeItem) => await cmdForeignDataWrapperOperations(item, context)
+ },
+ {
+ command: 'postgres-explorer.showForeignDataWrapperProperties',
+ callback: async (item: DatabaseTreeItem) => await cmdShowForeignDataWrapperProperties(item, context)
+ },
+ {
+ command: 'postgres-explorer.refreshForeignDataWrapper',
+ callback: async (item: DatabaseTreeItem) => await cmdRefreshForeignDataWrapper(item, context, databaseTreeProvider)
+ },
+ // Foreign Server commands
+ {
+ command: 'postgres-explorer.createForeignServer',
+ callback: async (item: DatabaseTreeItem) => await cmdCreateForeignServer(item, context)
+ },
+ {
+ command: 'postgres-explorer.foreignServerOperations',
+ callback: async (item: DatabaseTreeItem) => await cmdForeignServerOperations(item, context)
+ },
+ {
+ command: 'postgres-explorer.showForeignServerProperties',
+ callback: async (item: DatabaseTreeItem) => await cmdShowForeignServerProperties(item, context)
+ },
+ {
+ command: 'postgres-explorer.dropForeignServer',
+ callback: async (item: DatabaseTreeItem) => await cmdDropForeignServer(item, context)
+ },
+ {
+ command: 'postgres-explorer.refreshForeignServer',
+ callback: async (item: DatabaseTreeItem) => await cmdRefreshForeignServer(item, context, databaseTreeProvider)
+ },
+ // User Mapping commands
+ {
+ command: 'postgres-explorer.createUserMapping',
+ callback: async (item: DatabaseTreeItem) => await cmdCreateUserMapping(item, context)
+ },
+ {
+ command: 'postgres-explorer.userMappingOperations',
+ callback: async (item: DatabaseTreeItem) => await cmdUserMappingOperations(item, context)
+ },
+ {
+ command: 'postgres-explorer.showUserMappingProperties',
+ callback: async (item: DatabaseTreeItem) => await cmdShowUserMappingProperties(item, context)
+ },
+ {
+ command: 'postgres-explorer.dropUserMapping',
+ callback: async (item: DatabaseTreeItem) => await cmdDropUserMapping(item, context)
+ },
+ {
+ command: 'postgres-explorer.refreshUserMapping',
+ callback: async (item: DatabaseTreeItem) => await cmdRefreshUserMapping(item, context, databaseTreeProvider)
+ },
+ {
+ command: 'postgres-explorer.createRole',
+ callback: async (item: DatabaseTreeItem) => await cmdAddRole(item, context)
+ },
+ {
+ command: 'postgres-explorer.enableExtension',
+ callback: async (item: DatabaseTreeItem) => await cmdEnableExtension(item, context)
+ },
+
+ {
+ command: 'postgres-explorer.aiAssist',
+ callback: async (cell: vscode.NotebookCell) => await cmdAiAssist(cell, context, outputChannel)
+ },
+
+ {
+ command: 'postgres-explorer.chatWithQuery',
+ callback: async (cell: vscode.NotebookCell) => {
+ // Get the query from the active cell
+ let query = '';
+ let results = '';
+
+ if (cell) {
+ query = cell.document.getText();
+ // Check if there are outputs from previous execution
+ if (cell.outputs && cell.outputs.length > 0) {
+ const output = cell.outputs[0];
+ for (const item of output.items) {
+ if (item.mime === 'application/x-postgres-result' || item.mime === 'application/json') {
+ try {
+ const data = JSON.parse(new TextDecoder().decode(item.data));
+ if (data.rows && data.rows.length > 0) {
+ results = `\nResults (${data.rows.length} rows): ${JSON.stringify(data.rows.slice(0, 5), null, 2)}${data.rows.length > 5 ? '\n... and more' : ''}`;
+ }
+ } catch (e) {
+ // Ignore parse errors
+ }
+ }
+ }
+ }
+ } else {
+ // Fallback to active notebook editor
+ const activeEditor = vscode.window.activeNotebookEditor;
+ if (activeEditor) {
+ const selections = activeEditor.selections;
+ if (selections && selections.length > 0) {
+ const cellIndex = selections[0].start;
+ const activeCell = activeEditor.notebook.cellAt(cellIndex);
+ query = activeCell.document.getText();
+ }
+ }
+ }
+
+ if (!query) {
+ vscode.window.showWarningMessage('No query found in the active cell.');
+ return;
+ }
+
+ // Focus the chat view and send the query
+ await vscode.commands.executeCommand('postgresExplorer.chatView.focus');
+
+ // Send message to chat view with query context
+ const message = `Help me with this SQL query:\n\`\`\`sql\n${query}\n\`\`\`${results}`;
+
+ // Use the chat view provider to send the message
+ if (chatViewProviderInstance) {
+ chatViewProviderInstance.sendToChat({ query, results, message });
+ }
+ }
+ },
+
+ {
+ command: 'postgres-explorer.sendToChat',
+ callback: async (data: { query: string; results?: string; message: string }) => {
+ if (chatViewProviderInstance) {
+ await chatViewProviderInstance.sendToChat(data);
+ }
+ }
+ },
+
+ // Column commands
+ {
+ command: 'postgres-explorer.showColumnProperties',
+ callback: async (item: DatabaseTreeItem) => await showColumnProperties(item)
+ },
+ {
+ command: 'postgres-explorer.copyColumnName',
+ callback: async (item: DatabaseTreeItem) => await copyColumnName(item)
+ },
+ {
+ command: 'postgres-explorer.copyColumnNameQuoted',
+ callback: async (item: DatabaseTreeItem) => await copyColumnNameQuoted(item)
+ },
+ {
+ command: 'postgres-explorer.generateSelectStatement',
+ callback: async (item: DatabaseTreeItem) => await generateSelectStatement(item)
+ },
+ {
+ command: 'postgres-explorer.generateWhereClause',
+ callback: async (item: DatabaseTreeItem) => await generateWhereClause(item)
+ },
+ {
+ command: 'postgres-explorer.generateAlterColumnScript',
+ callback: async (item: DatabaseTreeItem) => await generateAlterColumnScript(item)
+ },
+ {
+ command: 'postgres-explorer.generateDropColumnScript',
+ callback: async (item: DatabaseTreeItem) => await generateDropColumnScript(item)
+ },
+ {
+ command: 'postgres-explorer.generateRenameColumnScript',
+ callback: async (item: DatabaseTreeItem) => await generateRenameColumnScript(item)
+ },
+ {
+ command: 'postgres-explorer.addColumnComment',
+ callback: async (item: DatabaseTreeItem) => await addColumnComment(item)
+ },
+ {
+ command: 'postgres-explorer.generateIndexOnColumn',
+ callback: async (item: DatabaseTreeItem) => await generateIndexOnColumn(item)
+ },
+ {
+ command: 'postgres-explorer.viewColumnStatistics',
+ callback: async (item: DatabaseTreeItem) => await viewColumnStatistics(item)
+ },
+
+ // Constraint commands
+ {
+ command: 'postgres-explorer.showConstraintProperties',
+ callback: async (item: DatabaseTreeItem) => await showConstraintProperties(item)
+ },
+ {
+ command: 'postgres-explorer.copyConstraintName',
+ callback: async (item: DatabaseTreeItem) => await copyConstraintName(item)
+ },
+ {
+ command: 'postgres-explorer.generateDropConstraintScript',
+ callback: async (item: DatabaseTreeItem) => await generateDropConstraintScript(item)
+ },
+ {
+ command: 'postgres-explorer.generateAlterConstraintScript',
+ callback: async (item: DatabaseTreeItem) => await generateAlterConstraintScript(item)
+ },
+ {
+ command: 'postgres-explorer.validateConstraint',
+ callback: async (item: DatabaseTreeItem) => await validateConstraint(item)
+ },
+ {
+ command: 'postgres-explorer.generateAddConstraintScript',
+ callback: async (item: DatabaseTreeItem) => await generateAddConstraintScript(item)
+ },
+ {
+ command: 'postgres-explorer.viewConstraintDependencies',
+ callback: async (item: DatabaseTreeItem) => await viewConstraintDependencies(item)
+ },
+ {
+ command: 'postgres-explorer.constraintOperations',
+ callback: async (item: DatabaseTreeItem) => await cmdConstraintOperations(item, context)
+ },
+
+ // Index commands
+ {
+ command: 'postgres-explorer.showIndexProperties',
+ callback: async (item: DatabaseTreeItem) => await showIndexProperties(item)
+ },
+ {
+ command: 'postgres-explorer.copyIndexName',
+ callback: async (item: DatabaseTreeItem) => await copyIndexName(item)
+ },
+ {
+ command: 'postgres-explorer.generateDropIndexScript',
+ callback: async (item: DatabaseTreeItem) => await generateDropIndexScript(item)
+ },
+ {
+ command: 'postgres-explorer.generateReindexScript',
+ callback: async (item: DatabaseTreeItem) => await generateReindexScript(item)
+ },
+ {
+ command: 'postgres-explorer.generateScriptCreate',
+ callback: async (item: DatabaseTreeItem) => await generateScriptCreate(item)
+ },
+ {
+ command: 'postgres-explorer.analyzeIndexUsage',
+ callback: async (item: DatabaseTreeItem) => await analyzeIndexUsage(item)
+ },
+ {
+ command: 'postgres-explorer.generateAlterIndexScript',
+ callback: async (item: DatabaseTreeItem) => await generateAlterIndexScript(item)
+ },
+ {
+ command: 'postgres-explorer.addIndexComment',
+ callback: async (item: DatabaseTreeItem) => await addIndexComment(item)
+ },
+ {
+ command: 'postgres-explorer.indexOperations',
+ callback: async (item: DatabaseTreeItem) => await cmdIndexOperations(item, context)
+ },
+ {
+ command: 'postgres-explorer.addColumn',
+ callback: async (item: DatabaseTreeItem) => await cmdAddColumn(item)
+ },
+ {
+ command: 'postgres-explorer.addConstraint',
+ callback: async (item: DatabaseTreeItem) => await cmdAddConstraint(item)
+ },
+ {
+ command: 'postgres-explorer.addIndex',
+ callback: async (item: DatabaseTreeItem) => await cmdAddIndex(item)
+ },
+ ];
+
+ console.log('Starting command registration...');
+ outputChannel.appendLine('Starting command registration...');
+
+ commands.forEach(({ command, callback }) => {
+ try {
+ console.log(`Registering command: ${command}`);
+ context.subscriptions.push(
+ vscode.commands.registerCommand(command, callback)
+ );
+ } catch (e) {
+ console.error(`Failed to register command ${command}:`, e);
+ outputChannel.appendLine(`Failed to register command ${command}: ${e}`);
+ }
+ });
+
+ outputChannel.appendLine('All commands registered successfully.');
+}
diff --git a/src/activation/providers.ts b/src/activation/providers.ts
new file mode 100644
index 0000000..701fdf0
--- /dev/null
+++ b/src/activation/providers.ts
@@ -0,0 +1,80 @@
+import * as vscode from 'vscode';
+import { ChatViewProvider } from '../providers/ChatViewProvider';
+import { DatabaseTreeProvider } from '../providers/DatabaseTreeProvider';
+import { PostgresNotebookProvider } from '../notebookProvider';
+import { PostgresNotebookSerializer } from '../postgresNotebook';
+import { AiCodeLensProvider } from '../providers/AiCodeLensProvider';
+
+export function registerProviders(context: vscode.ExtensionContext, outputChannel: vscode.OutputChannel) {
+ // Create database tree provider instance
+ const databaseTreeProvider = new DatabaseTreeProvider(context);
+
+ // Register tree data provider and create tree view
+ const treeView = vscode.window.createTreeView('postgresExplorer', {
+ treeDataProvider: databaseTreeProvider,
+ showCollapseAll: true
+ });
+ context.subscriptions.push(treeView);
+
+ // Update context key when selection changes to enable Add/Remove favorites menu switching
+ treeView.onDidChangeSelection(e => {
+ if (e.selection.length > 0) {
+ const item = e.selection[0];
+ vscode.commands.executeCommand('setContext', 'postgresExplorer.isFavorite', item.isFavorite === true);
+ } else {
+ vscode.commands.executeCommand('setContext', 'postgresExplorer.isFavorite', false);
+ }
+ });
+
+ // Register the chat view provider
+ const chatViewProviderInstance = new ChatViewProvider(context.extensionUri, context);
+ context.subscriptions.push(
+ vscode.window.registerWebviewViewProvider(
+ ChatViewProvider.viewType,
+ chatViewProviderInstance,
+ { webviewOptions: { retainContextWhenHidden: true } }
+ )
+ );
+
+ // Register notebook providers
+ const notebookProvider = new PostgresNotebookProvider();
+ context.subscriptions.push(
+ vscode.workspace.registerNotebookSerializer('postgres-notebook', notebookProvider),
+ vscode.workspace.registerNotebookSerializer('postgres-query', new PostgresNotebookSerializer())
+ );
+
+ // Register SQL completion provider
+ const { SqlCompletionProvider } = require('../providers/SqlCompletionProvider');
+ const sqlCompletionProvider = new SqlCompletionProvider();
+ context.subscriptions.push(
+ vscode.languages.registerCompletionItemProvider(
+ { language: 'sql' },
+ sqlCompletionProvider,
+ '.' // Trigger on dot for schema.table suggestions
+ ),
+ vscode.languages.registerCompletionItemProvider(
+ { scheme: 'vscode-notebook-cell', language: 'sql' },
+ sqlCompletionProvider,
+ '.'
+ )
+ );
+
+ // Register CodeLens Provider for both 'postgres' and 'sql' languages
+ const aiCodeLensProvider = new AiCodeLensProvider();
+ context.subscriptions.push(
+ vscode.languages.registerCodeLensProvider(
+ { language: 'postgres', scheme: 'vscode-notebook-cell' },
+ aiCodeLensProvider
+ ),
+ vscode.languages.registerCodeLensProvider(
+ { language: 'sql', scheme: 'vscode-notebook-cell' },
+ aiCodeLensProvider
+ )
+ );
+ outputChannel.appendLine('AiCodeLensProvider registered for postgres and sql languages.');
+
+ return {
+ databaseTreeProvider,
+ chatViewProviderInstance
+ };
+}
diff --git a/src/commands/aiAssist.ts b/src/commands/aiAssist.ts
index af2433d..ed70f8d 100644
--- a/src/commands/aiAssist.ts
+++ b/src/commands/aiAssist.ts
@@ -6,389 +6,394 @@ import { AiService } from '../providers/chat/AiService';
// Interface for table schema information
interface TableSchemaInfo {
- tableName: string;
- schemaName: string;
- columns: Array<{
- name: string;
- dataType: string;
- isNullable: boolean;
- defaultValue: string | null;
- isPrimaryKey: boolean;
- isForeignKey: boolean;
- references?: string;
- }>;
- indexes: Array<{
- name: string;
- columns: string[];
- isUnique: boolean;
- isPrimary: boolean;
- }>;
- constraints: Array<{
- name: string;
- type: string;
- definition: string;
- }>;
+ tableName: string;
+ schemaName: string;
+ columns: Array<{
+ name: string;
+ dataType: string;
+ isNullable: boolean;
+ defaultValue: string | null;
+ isPrimaryKey: boolean;
+ isForeignKey: boolean;
+ references?: string;
+ }>;
+ indexes: Array<{
+ name: string;
+ columns: string[];
+ isUnique: boolean;
+ isPrimary: boolean;
+ }>;
+ constraints: Array<{
+ name: string;
+ type: string;
+ definition: string;
+ }>;
}
// Interface for cell context
interface CellContext {
- currentQuery: string;
- cellIndex: number;
- previousCells: string[];
- lastOutput?: string;
- tableSchemas: TableSchemaInfo[];
- databaseInfo?: {
- name: string;
- version?: string;
- };
+ currentQuery: string;
+ cellIndex: number;
+ previousCells: string[];
+ lastOutput?: string;
+ tableSchemas: TableSchemaInfo[];
+ databaseInfo?: {
+ name: string;
+ version?: string;
+ };
}
export async function cmdAiAssist(cell: vscode.NotebookCell | undefined, context: vscode.ExtensionContext, outputChannel: vscode.OutputChannel) {
- outputChannel.appendLine('AI Assist command triggered');
-
- if (!cell) {
- outputChannel.appendLine('No cell provided in arguments, checking active notebook editor');
- const activeEditor = vscode.window.activeNotebookEditor;
- if (activeEditor && activeEditor.selection) {
- // Get the first selected cell
- if (activeEditor.selection.start < activeEditor.notebook.cellCount) {
- cell = activeEditor.notebook.cellAt(activeEditor.selection.start);
- outputChannel.appendLine(`Found active cell at index ${activeEditor.selection.start}`);
- }
- }
+ outputChannel.appendLine('AI Assist command triggered');
+
+ if (!cell) {
+ outputChannel.appendLine('No cell provided in arguments, checking active notebook editor');
+ const activeEditor = vscode.window.activeNotebookEditor;
+ if (activeEditor && activeEditor.selection) {
+ // Get the first selected cell
+ if (activeEditor.selection.start < activeEditor.notebook.cellCount) {
+ cell = activeEditor.notebook.cellAt(activeEditor.selection.start);
+ outputChannel.appendLine(`Found active cell at index ${activeEditor.selection.start}`);
+ }
}
+ }
- if (!cell) {
- outputChannel.appendLine('No cell found');
- vscode.window.showErrorMessage('No cell selected');
- return;
- }
+ if (!cell) {
+ outputChannel.appendLine('No cell found');
+ vscode.window.showErrorMessage('No cell selected');
+ return;
+ }
- // TypeScript now knows cell is vscode.NotebookCell because of the return above
- const validCell = cell;
+ // TypeScript now knows cell is vscode.NotebookCell because of the return above
+ const validCell = cell;
- const userInput = await AiTaskSelector.selectTask();
- if (!userInput) {
- return;
- }
+ const userInput = await AiTaskSelector.selectTask();
+ if (!userInput) {
+ return;
+ }
- try {
- const config = vscode.workspace.getConfiguration('postgresExplorer');
- const provider = config.get('aiProvider') || 'vscode-lm';
+ try {
+ const config = vscode.workspace.getConfiguration('postgresExplorer');
+ const provider = config.get('aiProvider') || 'vscode-lm';
- const aiService = new AiService();
- const modelInfo = await aiService.getModelInfo(provider, config);
+ const aiService = new AiService();
+ const modelInfo = await aiService.getModelInfo(provider, config);
- await vscode.window.withProgress({
- location: vscode.ProgressLocation.Notification,
- title: `AI (${modelInfo}) is analyzing your query...`,
- cancellable: true
- }, async (progress, token) => {
+ await vscode.window.withProgress({
+ location: vscode.ProgressLocation.Notification,
+ title: `AI (${modelInfo}) is analyzing your query...`,
+ cancellable: true
+ }, async (progress, token) => {
- progress.report({ message: "Gathering context..." });
+ progress.report({ message: "Gathering context..." });
- // Gather cell context including table schemas
- const cellContext = await gatherCellContext(validCell, context, outputChannel);
+ // Gather cell context including table schemas
+ const cellContext = await gatherCellContext(validCell, context, outputChannel);
- progress.report({ message: "Generating response..." });
+ progress.report({ message: "Generating response..." });
- // Build the comprehensive prompt to be used as System Prompt
- const systemPrompt = buildPrompt(userInput, cellContext);
- const userTrigger = "Please provide the SQL query based on the instructions above.";
+ // Build the comprehensive prompt to be used as System Prompt
+ const systemPrompt = buildPrompt(userInput, cellContext);
+ const userTrigger = "Please provide the SQL query based on the instructions above.";
- let responseText = '';
+ let responseText = '';
- if (provider === 'vscode-lm') {
- responseText = await aiService.callVsCodeLm(userTrigger, config, systemPrompt);
- } else {
- responseText = await aiService.callDirectApi(provider, userTrigger, config, systemPrompt);
- }
+ if (provider === 'vscode-lm') {
+ responseText = await aiService.callVsCodeLm(userTrigger, config, systemPrompt);
+ } else {
+ responseText = await aiService.callDirectApi(provider, userTrigger, config, systemPrompt);
+ }
- // Parse the response to check for placement instruction
- const { query, placement } = parseAiResponse(responseText);
-
- // Clean up response if it contains markdown code blocks despite instructions
- const cleanedQuery = StringUtils.cleanMarkdownCodeBlocks(query);
-
- if (cleanedQuery.trim()) {
- if (placement === 'new_cell') {
- // Add as a new cell below the current one
- const notebook = validCell.notebook;
- const targetIndex = validCell.index + 1;
-
- const newCellData = new vscode.NotebookCellData(
- vscode.NotebookCellKind.Code,
- cleanedQuery,
- 'sql'
- );
-
- const notebookEdit = new vscode.NotebookEdit(
- new vscode.NotebookRange(targetIndex, targetIndex),
- [newCellData]
- );
-
- const workspaceEdit = new vscode.WorkspaceEdit();
- workspaceEdit.set(notebook.uri, [notebookEdit]);
- await vscode.workspace.applyEdit(workspaceEdit);
-
- vscode.window.showInformationMessage(`AI (${modelInfo}) response added as new cell below for comparison.`);
- } else {
- // Replace the current cell content (default behavior)
- const edit = new vscode.WorkspaceEdit();
- edit.replace(
- validCell.document.uri,
- new vscode.Range(0, 0, validCell.document.lineCount, 0),
- cleanedQuery
- );
- await vscode.workspace.applyEdit(edit);
-
- vscode.window.showInformationMessage(`AI (${modelInfo}) response has replaced the current cell content.`);
- }
- }
- });
-
- } catch (error) {
- console.error('AI Assist Error:', error);
- const message = error instanceof Error ? error.message : String(error);
- await ErrorHandlers.showError(
- `AI Assist failed: ${message}`,
- 'Configure Settings',
- 'workbench.action.openSettings'
- );
- }
+ // Parse the response to check for placement instruction
+ const { query, placement } = parseAiResponse(responseText);
+
+ // Clean up response if it contains markdown code blocks despite instructions
+ const cleanedQuery = StringUtils.cleanMarkdownCodeBlocks(query);
+
+ if (cleanedQuery.trim()) {
+ if (placement === 'new_cell') {
+ // Add as a new cell below the current one
+ const notebook = validCell.notebook;
+ const targetIndex = validCell.index + 1;
+
+ const newCellData = new vscode.NotebookCellData(
+ vscode.NotebookCellKind.Code,
+ cleanedQuery,
+ 'sql'
+ );
+
+ const notebookEdit = new vscode.NotebookEdit(
+ new vscode.NotebookRange(targetIndex, targetIndex),
+ [newCellData]
+ );
+
+ const workspaceEdit = new vscode.WorkspaceEdit();
+ workspaceEdit.set(notebook.uri, [notebookEdit]);
+ await vscode.workspace.applyEdit(workspaceEdit);
+
+ vscode.window.showInformationMessage(`AI (${modelInfo}) response added as new cell below for comparison.`);
+ } else {
+ // Replace the current cell content (default behavior)
+ const edit = new vscode.WorkspaceEdit();
+ edit.replace(
+ validCell.document.uri,
+ new vscode.Range(0, 0, validCell.document.lineCount, 0),
+ cleanedQuery
+ );
+ await vscode.workspace.applyEdit(edit);
+
+ vscode.window.showInformationMessage(`AI (${modelInfo}) response has replaced the current cell content.`);
+ }
+ }
+ });
+
+ } catch (error) {
+ console.error('AI Assist Error:', error);
+ const message = error instanceof Error ? error.message : String(error);
+ await ErrorHandlers.showError(
+ `AI Assist failed: ${message}`,
+ 'Configure Settings',
+ 'workbench.action.openSettings'
+ );
+ }
}
/**
* Gather context from the cell and notebook for AI assistance
*/
async function gatherCellContext(cell: vscode.NotebookCell, context: vscode.ExtensionContext, outputChannel: vscode.OutputChannel): Promise {
- const currentQuery = cell.document.getText();
- const cellIndex = cell.index;
-
- // Get previous cells content (up to 5 previous SQL cells for context)
- const previousCells: string[] = [];
- for (let i = Math.max(0, cellIndex - 5); i < cellIndex; i++) {
- const prevCell = cell.notebook.cellAt(i);
- if (prevCell.kind === vscode.NotebookCellKind.Code) {
- previousCells.push(prevCell.document.getText());
- }
+ const currentQuery = cell.document.getText();
+ const cellIndex = cell.index;
+
+ // Get previous cells content (up to 5 previous SQL cells for context)
+ const previousCells: string[] = [];
+ for (let i = Math.max(0, cellIndex - 5); i < cellIndex; i++) {
+ const prevCell = cell.notebook.cellAt(i);
+ if (prevCell.kind === vscode.NotebookCellKind.Code) {
+ previousCells.push(prevCell.document.getText());
}
-
- // Try to get the last output of the current cell
- let lastOutput: string | undefined;
- if (cell.outputs && cell.outputs.length > 0) {
- const lastOutputItem = cell.outputs[cell.outputs.length - 1];
- if (lastOutputItem.items && lastOutputItem.items.length > 0) {
- const outputItem = lastOutputItem.items[0];
- if (outputItem.mime === 'application/x-postgres-result') {
- try {
- const data = JSON.parse(new TextDecoder().decode(outputItem.data));
- if (data.rows && data.columns) {
- const totalRows = data.rows.length;
- const maxSampleRows = 5; // Limit sample rows for context
- const maxColumns = 20; // Limit columns to avoid token bloat
-
- // Truncate columns if too many
- const displayColumns = data.columns.length > maxColumns
- ? [...data.columns.slice(0, maxColumns), `... and ${data.columns.length - maxColumns} more columns`]
- : data.columns;
-
- // Only include sample rows if total is reasonable (< 1000 rows)
- // For very large result sets, just show metadata
- if (totalRows > 1000) {
- lastOutput = `Query returned ${totalRows} rows (large result set - sample omitted)\nColumns (${data.columns.length}): ${displayColumns.join(', ')}`;
- } else if (totalRows > 100) {
- // For medium result sets, show fewer sample rows
- const sampleRows = data.rows.slice(0, 3);
- // Truncate each row's values to avoid huge JSON
- const truncatedSamples = sampleRows.map((row: any) => {
- const truncated: any = {};
- const cols = data.columns.slice(0, maxColumns);
- for (const col of cols) {
- const val = row[col];
- if (typeof val === 'string' && val.length > 100) {
- truncated[col] = val.substring(0, 100) + '... (truncated)';
- } else {
- truncated[col] = val;
- }
- }
- return truncated;
- });
- lastOutput = `Columns (${data.columns.length}): ${displayColumns.join(', ')}\nSample data (showing 3 of ${totalRows} rows):\n${JSON.stringify(truncatedSamples, null, 2)}`;
- } else {
- // For small result sets, show more samples
- const sampleRows = data.rows.slice(0, maxSampleRows);
- const truncatedSamples = sampleRows.map((row: any) => {
- const truncated: any = {};
- const cols = data.columns.slice(0, maxColumns);
- for (const col of cols) {
- const val = row[col];
- if (typeof val === 'string' && val.length > 200) {
- truncated[col] = val.substring(0, 200) + '... (truncated)';
- } else {
- truncated[col] = val;
- }
- }
- return truncated;
- });
- lastOutput = `Columns (${data.columns.length}): ${displayColumns.join(', ')}\nSample data (showing ${sampleRows.length} of ${totalRows} rows):\n${JSON.stringify(truncatedSamples, null, 2)}`;
- }
- } else if (data.command) {
- lastOutput = `Command: ${data.command}, Rows affected: ${data.rowCount || 0}`;
- }
- } catch (e) {
- outputChannel.appendLine('Failed to parse cell output: ' + e);
- }
- }
- }
- }
-
- // Extract table names from query and fetch their schemas
- const tableSchemas: TableSchemaInfo[] = [];
- const tableNames = extractTableNames(currentQuery);
-
- if (tableNames.length > 0) {
+ }
+
+ // Try to get the last output of the current cell
+ let lastOutput: string | undefined;
+ if (cell.outputs && cell.outputs.length > 0) {
+ const lastOutputItem = cell.outputs[cell.outputs.length - 1];
+ if (lastOutputItem.items && lastOutputItem.items.length > 0) {
+ const outputItem = lastOutputItem.items[0];
+ if (outputItem.mime === 'application/x-postgres-result') {
try {
- const metadata = cell.notebook.metadata as PostgresMetadata;
- if (metadata?.connectionId) {
- const connections = vscode.workspace.getConfiguration().get('postgresExplorer.connections') || [];
- const connection = connections.find(c => c.id === metadata.connectionId);
-
- if (connection) {
- const client = await ConnectionManager.getInstance().getConnection({
- id: connection.id,
- host: connection.host,
- port: connection.port,
- username: connection.username,
- database: metadata.databaseName || connection.database,
- name: connection.name
- });
-
- for (const tableName of tableNames) {
- try {
- const schema = await fetchTableSchema(client, tableName);
- if (schema) {
- tableSchemas.push(schema);
- }
- } catch (e) {
- outputChannel.appendLine(`Failed to fetch schema for ${tableName}: ${e}`);
- }
- }
+ const data = JSON.parse(new TextDecoder().decode(outputItem.data));
+ if (data.rows && data.columns) {
+ const totalRows = data.rows.length;
+ const maxSampleRows = 5; // Limit sample rows for context
+ const maxColumns = 20; // Limit columns to avoid token bloat
+
+ // Truncate columns if too many
+ const displayColumns = data.columns.length > maxColumns
+ ? [...data.columns.slice(0, maxColumns), `... and ${data.columns.length - maxColumns} more columns`]
+ : data.columns;
+
+ // Only include sample rows if total is reasonable (< 1000 rows)
+ // For very large result sets, just show metadata
+ if (totalRows > 1000) {
+ lastOutput = `Query returned ${totalRows} rows (large result set - sample omitted)\nColumns (${data.columns.length}): ${displayColumns.join(', ')}`;
+ } else if (totalRows > 100) {
+ // For medium result sets, show fewer sample rows
+ const sampleRows = data.rows.slice(0, 3);
+ // Truncate each row's values to avoid huge JSON
+ const truncatedSamples = sampleRows.map((row: any) => {
+ const truncated: any = {};
+ const cols = data.columns.slice(0, maxColumns);
+ for (const col of cols) {
+ const val = row[col];
+ if (typeof val === 'string' && val.length > 100) {
+ truncated[col] = val.substring(0, 100) + '... (truncated)';
+ } else {
+ truncated[col] = val;
+ }
}
+ return truncated;
+ });
+ lastOutput = `Columns (${data.columns.length}): ${displayColumns.join(', ')}\nSample data (showing 3 of ${totalRows} rows):\n${JSON.stringify(truncatedSamples, null, 2)}`;
+ } else {
+ // For small result sets, show more samples
+ const sampleRows = data.rows.slice(0, maxSampleRows);
+ const truncatedSamples = sampleRows.map((row: any) => {
+ const truncated: any = {};
+ const cols = data.columns.slice(0, maxColumns);
+ for (const col of cols) {
+ const val = row[col];
+ if (typeof val === 'string' && val.length > 200) {
+ truncated[col] = val.substring(0, 200) + '... (truncated)';
+ } else {
+ truncated[col] = val;
+ }
+ }
+ return truncated;
+ });
+ lastOutput = `Columns (${data.columns.length}): ${displayColumns.join(', ')}\nSample data (showing ${sampleRows.length} of ${totalRows} rows):\n${JSON.stringify(truncatedSamples, null, 2)}`;
}
+ } else if (data.command) {
+ lastOutput = `Command: ${data.command}, Rows affected: ${data.rowCount || 0}`;
+ }
} catch (e) {
- outputChannel.appendLine('Failed to connect for schema fetch: ' + e);
+ outputChannel.appendLine('Failed to parse cell output: ' + e);
}
+ }
}
+ }
- // Get database info
- let databaseInfo: { name: string; version?: string } | undefined;
+ // Extract table names from query and fetch their schemas
+ const tableSchemas: TableSchemaInfo[] = [];
+ const tableNames = extractTableNames(currentQuery);
+
+ if (tableNames.length > 0) {
try {
- const metadata = cell.notebook.metadata as PostgresMetadata;
- if (metadata?.databaseName) {
- databaseInfo = { name: metadata.databaseName };
+ const metadata = cell.notebook.metadata as PostgresMetadata;
+ if (metadata?.connectionId) {
+ const connections = vscode.workspace.getConfiguration().get('postgresExplorer.connections') || [];
+ const connection = connections.find(c => c.id === metadata.connectionId);
+
+ if (connection) {
+ let client;
+ try {
+ client = await ConnectionManager.getInstance().getPooledClient({
+ id: connection.id,
+ host: connection.host,
+ port: connection.port,
+ username: connection.username,
+ database: metadata.databaseName || connection.database,
+ name: connection.name
+ });
+
+ for (const tableName of tableNames) {
+ try {
+ const schema = await fetchTableSchema(client, tableName);
+ if (schema) {
+ tableSchemas.push(schema);
+ }
+ } catch (e) {
+ outputChannel.appendLine(`Failed to fetch schema for ${tableName}: ${e}`);
+ }
+ }
+ } finally {
+ if (client) client.release();
+ }
}
+ }
} catch (e) {
- // Ignore
+ outputChannel.appendLine('Failed to connect for schema fetch: ' + e);
}
-
- return {
- currentQuery,
- cellIndex,
- previousCells,
- lastOutput,
- tableSchemas,
- databaseInfo
- };
+ }
+
+ // Get database info
+ let databaseInfo: { name: string; version?: string } | undefined;
+ try {
+ const metadata = cell.notebook.metadata as PostgresMetadata;
+ if (metadata?.databaseName) {
+ databaseInfo = { name: metadata.databaseName };
+ }
+ } catch (e) {
+ // Ignore
+ }
+
+ return {
+ currentQuery,
+ cellIndex,
+ previousCells,
+ lastOutput,
+ tableSchemas,
+ databaseInfo
+ };
}
/**
* Parse AI response to extract query and placement instruction
*/
function parseAiResponse(response: string): { query: string; placement: 'replace' | 'new_cell' } {
- const lines = response.trim().split('\n');
- let placement: 'replace' | 'new_cell' = 'replace';
- let queryLines: string[] = [];
-
- // Look for placement instruction at the beginning or end of the response
- // Format: [PLACEMENT: new_cell] or [PLACEMENT: replace]
- const placementRegex = /\[PLACEMENT:\s*(new_cell|replace)\]/i;
-
- for (const line of lines) {
- const match = line.match(placementRegex);
- if (match) {
- placement = match[1].toLowerCase() as 'replace' | 'new_cell';
- } else {
- queryLines.push(line);
- }
+ const lines = response.trim().split('\n');
+ let placement: 'replace' | 'new_cell' = 'replace';
+ let queryLines: string[] = [];
+
+ // Look for placement instruction at the beginning or end of the response
+ // Format: [PLACEMENT: new_cell] or [PLACEMENT: replace]
+ const placementRegex = /\[PLACEMENT:\s*(new_cell|replace)\]/i;
+
+ for (const line of lines) {
+ const match = line.match(placementRegex);
+ if (match) {
+ placement = match[1].toLowerCase() as 'replace' | 'new_cell';
+ } else {
+ queryLines.push(line);
}
-
- // Also check for placement in SQL comments
- const commentPlacementRegex = /--\s*PLACEMENT:\s*(new_cell|replace)/i;
- const filteredLines: string[] = [];
-
- for (const line of queryLines) {
- const match = line.match(commentPlacementRegex);
- if (match) {
- placement = match[1].toLowerCase() as 'replace' | 'new_cell';
- } else {
- filteredLines.push(line);
- }
+ }
+
+ // Also check for placement in SQL comments
+ const commentPlacementRegex = /--\s*PLACEMENT:\s*(new_cell|replace)/i;
+ const filteredLines: string[] = [];
+
+ for (const line of queryLines) {
+ const match = line.match(commentPlacementRegex);
+ if (match) {
+ placement = match[1].toLowerCase() as 'replace' | 'new_cell';
+ } else {
+ filteredLines.push(line);
}
+ }
- return {
- query: filteredLines.join('\n').trim(),
- placement
- };
+ return {
+ query: filteredLines.join('\n').trim(),
+ placement
+ };
}
/**
* Extract table names from SQL query
*/
function extractTableNames(query: string): Array<{ schema: string; table: string }> {
- const tables: Array<{ schema: string; table: string }> = [];
-
- // Patterns to match table references
- const patterns = [
- // FROM/JOIN clause: FROM schema.table or FROM table
- /(?:FROM|JOIN)\s+(?:"?(\w+)"?\.)?"?(\w+)"?(?:\s+(?:AS\s+)?\w+)?/gi,
- // UPDATE table
- /UPDATE\s+(?:"?(\w+)"?\.)?"?(\w+)"?/gi,
- // INSERT INTO table
- /INSERT\s+INTO\s+(?:"?(\w+)"?\.)?"?(\w+)"?/gi,
- // DELETE FROM table
- /DELETE\s+FROM\s+(?:"?(\w+)"?\.)?"?(\w+)"?/gi,
- // CREATE TABLE / ALTER TABLE / DROP TABLE
- /(?:CREATE|ALTER|DROP)\s+TABLE\s+(?:IF\s+(?:NOT\s+)?EXISTS\s+)?(?:"?(\w+)"?\.)?"?(\w+)"?/gi,
- // TRUNCATE table
- /TRUNCATE\s+(?:TABLE\s+)?(?:"?(\w+)"?\.)?"?(\w+)"?/gi
- ];
-
- for (const pattern of patterns) {
- let match;
- while ((match = pattern.exec(query)) !== null) {
- const schema = match[1] || 'public';
- const table = match[2];
-
- // Avoid duplicates and SQL keywords
- const sqlKeywords = ['select', 'from', 'where', 'and', 'or', 'not', 'null', 'true', 'false', 'as', 'on', 'in', 'is', 'like', 'between', 'exists', 'case', 'when', 'then', 'else', 'end'];
- if (table && !sqlKeywords.includes(table.toLowerCase()) && !tables.some(t => t.schema === schema && t.table === table)) {
- tables.push({ schema, table });
- }
- }
+ const tables: Array<{ schema: string; table: string }> = [];
+
+ // Patterns to match table references
+ const patterns = [
+ // FROM/JOIN clause: FROM schema.table or FROM table
+ /(?:FROM|JOIN)\s+(?:"?(\w+)"?\.)?"?(\w+)"?(?:\s+(?:AS\s+)?\w+)?/gi,
+ // UPDATE table
+ /UPDATE\s+(?:"?(\w+)"?\.)?"?(\w+)"?/gi,
+ // INSERT INTO table
+ /INSERT\s+INTO\s+(?:"?(\w+)"?\.)?"?(\w+)"?/gi,
+ // DELETE FROM table
+ /DELETE\s+FROM\s+(?:"?(\w+)"?\.)?"?(\w+)"?/gi,
+ // CREATE TABLE / ALTER TABLE / DROP TABLE
+ /(?:CREATE|ALTER|DROP)\s+TABLE\s+(?:IF\s+(?:NOT\s+)?EXISTS\s+)?(?:"?(\w+)"?\.)?"?(\w+)"?/gi,
+ // TRUNCATE table
+ /TRUNCATE\s+(?:TABLE\s+)?(?:"?(\w+)"?\.)?"?(\w+)"?/gi
+ ];
+
+ for (const pattern of patterns) {
+ let match;
+ while ((match = pattern.exec(query)) !== null) {
+ const schema = match[1] || 'public';
+ const table = match[2];
+
+ // Avoid duplicates and SQL keywords
+ const sqlKeywords = ['select', 'from', 'where', 'and', 'or', 'not', 'null', 'true', 'false', 'as', 'on', 'in', 'is', 'like', 'between', 'exists', 'case', 'when', 'then', 'else', 'end'];
+ if (table && !sqlKeywords.includes(table.toLowerCase()) && !tables.some(t => t.schema === schema && t.table === table)) {
+ tables.push({ schema, table });
+ }
}
+ }
- return tables;
+ return tables;
}
/**
* Fetch table schema from database
*/
async function fetchTableSchema(client: any, tableRef: { schema: string; table: string }): Promise {
- const { schema, table } = tableRef;
+ const { schema, table } = tableRef;
- // Fetch columns
- const columnsResult = await client.query(`
+ // Fetch columns
+ const columnsResult = await client.query(`
SELECT
c.column_name,
c.data_type,
@@ -427,12 +432,12 @@ async function fetchTableSchema(client: any, tableRef: { schema: string; table:
ORDER BY c.ordinal_position
`, [schema, table]);
- if (columnsResult.rows.length === 0) {
- return null;
- }
+ if (columnsResult.rows.length === 0) {
+ return null;
+ }
- // Fetch indexes
- const indexesResult = await client.query(`
+ // Fetch indexes
+ const indexesResult = await client.query(`
SELECT
i.relname as index_name,
array_agg(a.attname ORDER BY array_position(ix.indkey, a.attnum)) as columns,
@@ -447,8 +452,8 @@ async function fetchTableSchema(client: any, tableRef: { schema: string; table:
GROUP BY i.relname, ix.indisunique, ix.indisprimary
`, [schema, table]);
- // Fetch constraints
- const constraintsResult = await client.query(`
+ // Fetch constraints
+ const constraintsResult = await client.query(`
SELECT
tc.constraint_name,
tc.constraint_type,
@@ -459,86 +464,86 @@ async function fetchTableSchema(client: any, tableRef: { schema: string; table:
WHERE tc.table_schema = $1 AND tc.table_name = $2
`, [schema, table]);
- return {
- tableName: table,
- schemaName: schema,
- columns: columnsResult.rows.map((row: any) => ({
- name: row.column_name,
- dataType: row.data_type,
- isNullable: row.is_nullable === 'YES',
- defaultValue: row.column_default,
- isPrimaryKey: row.is_primary_key,
- isForeignKey: row.is_foreign_key,
- references: row.is_foreign_key ? row.references_to : undefined
- })),
- indexes: indexesResult.rows.map((row: any) => ({
- name: row.index_name,
- columns: row.columns,
- isUnique: row.is_unique,
- isPrimary: row.is_primary
- })),
- constraints: constraintsResult.rows.map((row: any) => ({
- name: row.constraint_name,
- type: row.constraint_type,
- definition: row.definition
- }))
- };
+ return {
+ tableName: table,
+ schemaName: schema,
+ columns: columnsResult.rows.map((row: any) => ({
+ name: row.column_name,
+ dataType: row.data_type,
+ isNullable: row.is_nullable === 'YES',
+ defaultValue: row.column_default,
+ isPrimaryKey: row.is_primary_key,
+ isForeignKey: row.is_foreign_key,
+ references: row.is_foreign_key ? row.references_to : undefined
+ })),
+ indexes: indexesResult.rows.map((row: any) => ({
+ name: row.index_name,
+ columns: row.columns,
+ isUnique: row.is_unique,
+ isPrimary: row.is_primary
+ })),
+ constraints: constraintsResult.rows.map((row: any) => ({
+ name: row.constraint_name,
+ type: row.constraint_type,
+ definition: row.definition
+ }))
+ };
}
function buildPrompt(userInput: string, cellContext: CellContext): string {
- const { currentQuery, previousCells, lastOutput, tableSchemas, databaseInfo } = cellContext;
-
- // Build table schema context
- let schemaContext = '';
- if (tableSchemas.length > 0) {
- schemaContext = '\n\n## Table Schemas Referenced in Query\n';
- for (const schema of tableSchemas) {
- schemaContext += `\n### Table: ${schema.schemaName}.${schema.tableName}\n`;
- schemaContext += '**Columns:**\n';
- schemaContext += '| Column | Type | Nullable | Default | PK | FK | References |\n';
- schemaContext += '|--------|------|----------|---------|----|----|------------|\n';
- for (const col of schema.columns) {
- schemaContext += `| ${col.name} | ${col.dataType} | ${col.isNullable ? 'YES' : 'NO'} | ${col.defaultValue || '-'} | ${col.isPrimaryKey ? '✓' : ''} | ${col.isForeignKey ? '✓' : ''} | ${col.references || '-'} |\n`;
- }
-
- if (schema.indexes.length > 0) {
- schemaContext += '\n**Indexes:**\n';
- for (const idx of schema.indexes) {
- const columnsStr = Array.isArray(idx.columns) ? idx.columns.join(', ') : String(idx.columns || '');
- schemaContext += `- ${idx.name}: (${columnsStr}) ${idx.isUnique ? '[UNIQUE]' : ''} ${idx.isPrimary ? '[PRIMARY]' : ''}\n`;
- }
- }
-
- if (schema.constraints.length > 0) {
- schemaContext += '\n**Constraints:**\n';
- for (const con of schema.constraints) {
- schemaContext += `- ${con.name} (${con.type}): ${con.definition}\n`;
- }
- }
+ const { currentQuery, previousCells, lastOutput, tableSchemas, databaseInfo } = cellContext;
+
+ // Build table schema context
+ let schemaContext = '';
+ if (tableSchemas.length > 0) {
+ schemaContext = '\n\n## Table Schemas Referenced in Query\n';
+ for (const schema of tableSchemas) {
+ schemaContext += `\n### Table: ${schema.schemaName}.${schema.tableName}\n`;
+ schemaContext += '**Columns:**\n';
+ schemaContext += '| Column | Type | Nullable | Default | PK | FK | References |\n';
+ schemaContext += '|--------|------|----------|---------|----|----|------------|\n';
+ for (const col of schema.columns) {
+ schemaContext += `| ${col.name} | ${col.dataType} | ${col.isNullable ? 'YES' : 'NO'} | ${col.defaultValue || '-'} | ${col.isPrimaryKey ? '✓' : ''} | ${col.isForeignKey ? '✓' : ''} | ${col.references || '-'} |\n`;
+ }
+
+ if (schema.indexes.length > 0) {
+ schemaContext += '\n**Indexes:**\n';
+ for (const idx of schema.indexes) {
+ const columnsStr = Array.isArray(idx.columns) ? idx.columns.join(', ') : String(idx.columns || '');
+ schemaContext += `- ${idx.name}: (${columnsStr}) ${idx.isUnique ? '[UNIQUE]' : ''} ${idx.isPrimary ? '[PRIMARY]' : ''}\n`;
}
- }
-
- // Build previous cells context
- let previousContext = '';
- if (previousCells.length > 0) {
- previousContext = '\n\n## Previous Queries in Notebook (for context)\n```sql\n';
- previousContext += previousCells.slice(-3).join('\n\n-- Next query --\n');
- previousContext += '\n```';
- }
-
- // Build output context
- let outputContext = '';
- if (lastOutput) {
- outputContext = `\n\n## Last Execution Output\n\`\`\`\n${lastOutput}\n\`\`\``;
- }
+ }
- // Build database context
- let dbContext = '';
- if (databaseInfo) {
- dbContext = `\n\n## Database: ${databaseInfo.name}`;
+ if (schema.constraints.length > 0) {
+ schemaContext += '\n**Constraints:**\n';
+ for (const con of schema.constraints) {
+ schemaContext += `- ${con.name} (${con.type}): ${con.definition}\n`;
+ }
+ }
}
-
- return `# PostgreSQL Query Assistant
+ }
+
+ // Build previous cells context
+ let previousContext = '';
+ if (previousCells.length > 0) {
+ previousContext = '\n\n## Previous Queries in Notebook (for context)\n```sql\n';
+ previousContext += previousCells.slice(-3).join('\n\n-- Next query --\n');
+ previousContext += '\n```';
+ }
+
+ // Build output context
+ let outputContext = '';
+ if (lastOutput) {
+ outputContext = `\n\n## Last Execution Output\n\`\`\`\n${lastOutput}\n\`\`\``;
+ }
+
+ // Build database context
+ let dbContext = '';
+ if (databaseInfo) {
+ dbContext = `\n\n## Database: ${databaseInfo.name}`;
+ }
+
+ return `# PostgreSQL Query Assistant
You are an expert PostgreSQL database developer and query optimizer. Your task is to help modify, optimize, or explain SQL queries based on the user's instructions.
@@ -599,78 +604,56 @@ Then provide the SQL query. Remember: NO markdown formatting, just the raw SQL (
* AI Task Selector utility with comprehensive task options
*/
const AiTaskSelector = {
- /**
- * Available AI tasks organized by category
- */
- tasks: [
- // Custom & General
- { label: '$(edit) Custom Instruction', description: 'Enter your own instruction', detail: 'Provide specific instructions for the AI' },
-
- // Query Understanding
- { label: '$(comment-discussion) Explain Query', description: 'Add detailed comments explaining the query', detail: 'Adds inline comments explaining each clause and operation' },
- { label: '$(question) What Does This Do?', description: 'Get a summary comment at the top', detail: 'Adds a block comment summarizing the query purpose' },
-
- // Error Fixing & Debugging
- { label: '$(bug) Fix Syntax Errors', description: 'Correct syntax errors in the query', detail: 'Fixes missing keywords, brackets, quotes, and common mistakes' },
- { label: '$(warning) Fix Logic Issues', description: 'Identify and fix logical problems', detail: 'Fixes incorrect JOINs, wrong conditions, NULL handling issues' },
- { label: '$(error) Debug Query', description: 'Add debugging helpers', detail: 'Adds EXPLAIN, row counts, and intermediate result checks' },
-
- // Performance Optimization
- { label: '$(rocket) Optimize Performance', description: 'Improve query performance', detail: 'Rewrites for better execution plan using indexes and efficient patterns' },
- { label: '$(dashboard) Add EXPLAIN ANALYZE', description: 'Wrap with execution analysis', detail: 'Prepends EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT) for profiling' },
- { label: '$(telescope) Suggest Indexes', description: 'Recommend indexes for this query', detail: 'Adds comments suggesting CREATE INDEX statements' },
-
- // Formatting & Style
- { label: '$(list-flat) Format Query', description: 'Beautify and standardize formatting', detail: 'Applies consistent indentation, casing, and line breaks' },
- { label: '$(symbol-keyword) Uppercase Keywords', description: 'Convert keywords to uppercase', detail: 'Makes SELECT, FROM, WHERE, etc. uppercase for readability' },
- { label: '$(primitive-square) Add Aliases', description: 'Add meaningful table aliases', detail: 'Adds descriptive aliases to tables and subqueries' },
-
- // Query Modification
- { label: '$(add) Add WHERE Clause', description: 'Add filtering conditions', detail: 'Prompts for conditions to filter results' },
- { label: '$(filter) Add Pagination', description: 'Add LIMIT and OFFSET', detail: 'Adds pagination with sensible defaults' },
- { label: '$(sort-precedence) Add ORDER BY', description: 'Add sorting to results', detail: 'Adds ORDER BY clause with appropriate columns' },
- { label: '$(group-by-ref-type) Add GROUP BY', description: 'Aggregate results', detail: 'Adds GROUP BY and aggregation functions' },
-
- // Advanced Operations
- { label: '$(table) Convert to CTE', description: 'Refactor using Common Table Expressions', detail: 'Rewrites subqueries as WITH clauses for readability' },
- { label: '$(refresh) Convert to View', description: 'Create VIEW from query', detail: 'Wraps query in CREATE OR REPLACE VIEW' },
- { label: '$(symbol-function) Convert to Function', description: 'Create Function from query', detail: 'Wraps query in CREATE OR REPLACE FUNCTION' }
- ],
-
- /**
- * Show task selector and return the user's choice/instruction
- */
- async selectTask(): Promise {
- const selection = await vscode.window.showQuickPick(this.tasks, {
- placeHolder: 'Select an AI task or enter custom instruction',
- matchOnDescription: true,
- matchOnDetail: true,
- ignoreFocusOut: true
- });
-
- if (!selection) {
- return undefined;
- }
-
- if (selection.label.includes('Custom Instruction')) {
- return await vscode.window.showInputBox({
- placeHolder: 'e.g., "Rewrite this query to filter by user status"',
- prompt: 'Enter your specific instruction for the AI',
- ignoreFocusOut: true
- });
- }
-
- // For specific tasks that might need extra input
- if (selection.label.includes('Add WHERE Clause')) {
- const condition = await vscode.window.showInputBox({
- placeHolder: 'e.g., status = \'active\' AND created_at > NOW() - INTERVAL \'1 day\'',
- prompt: 'Enter the filtering condition',
- ignoreFocusOut: true
- });
- if (!condition) return undefined;
- return `Add WHERE clause: ${condition}`;
- }
+ /**
+ * Available AI tasks organized by category
+ */
+ tasks: [
+ // Custom - Always First
+ { label: '$(edit) Custom Instruction', description: 'Enter your own instruction', detail: 'Tell the AI exactly what you want to do with this query', kind: vscode.QuickPickItemKind.Default },
+
+ // Separator
+ { label: '', kind: vscode.QuickPickItemKind.Separator },
+
+ // Core Commands (Most Used)
+ { label: '$(comment-discussion) Explain', description: 'Add comments explaining the query', detail: 'Adds inline comments explaining each clause' },
+ { label: '$(bug) Fix Syntax Errors', description: 'Correct syntax mistakes', detail: 'Fixes missing keywords, brackets, quotes, typos' },
+ { label: '$(warning) Fix Logic Issues', description: 'Fix logical problems', detail: 'Corrects JOINs, conditions, NULL handling, duplicates' },
+ { label: '$(rocket) Optimize', description: 'Improve performance', detail: 'Rewrites for better execution plan' },
+ { label: '$(telescope) Suggest Indexes', description: 'Recommend indexes', detail: 'Suggests CREATE INDEX statements for this query' },
+ { label: '$(list-flat) Format', description: 'Beautify and standardize', detail: 'Applies consistent indentation, casing, line breaks' },
+
+ // Separator
+ { label: '', kind: vscode.QuickPickItemKind.Separator },
+
+ // Conversions
+ { label: '$(table) Convert to CTE', description: 'Refactor using WITH clauses', detail: 'Converts subqueries to Common Table Expressions' },
+ { label: '$(refresh) Convert to View', description: 'Create VIEW from query', detail: 'Wraps query in CREATE OR REPLACE VIEW' },
+ { label: '$(symbol-function) Convert to Function', description: 'Create Function from query', detail: 'Wraps query in CREATE OR REPLACE FUNCTION' }
+ ],
+
+ /**
+ * Show task selector and return the user's choice/instruction
+ */
+ async selectTask(): Promise {
+ const selection = await vscode.window.showQuickPick(this.tasks, {
+ placeHolder: 'Select an AI task or enter custom instruction',
+ matchOnDescription: true,
+ matchOnDetail: true,
+ ignoreFocusOut: true
+ });
+
+ if (!selection || selection.kind === vscode.QuickPickItemKind.Separator) {
+ return undefined;
+ }
- return selection.label.replace(/\$\([a-z-]+\)\s/, ''); // Return clean label as instruction
+ if (selection.label.includes('Custom Instruction')) {
+ return await vscode.window.showInputBox({
+ placeHolder: 'e.g., "Add pagination", "Filter by date", "Uppercase keywords"',
+ prompt: 'Enter your specific instruction for the AI',
+ ignoreFocusOut: true
+ });
}
+
+ return selection.label.replace(/\$\([a-z-]+\)\s/, ''); // Return clean label as instruction
+ }
};
diff --git a/src/commands/columns.ts b/src/commands/columns.ts
index e836dbf..b67583c 100644
--- a/src/commands/columns.ts
+++ b/src/commands/columns.ts
@@ -2,355 +2,395 @@ import * as vscode from 'vscode';
import { DatabaseTreeItem } from '../providers/DatabaseTreeProvider';
import {
- MarkdownUtils,
- FormatHelpers,
- ValidationHelpers,
- ErrorHandlers,
- SQL_TEMPLATES,
- getDatabaseConnection,
- NotebookBuilder,
- QueryBuilder
+ MarkdownUtils,
+ FormatHelpers,
+ ValidationHelpers,
+ ErrorHandlers,
+ SQL_TEMPLATES,
+ getDatabaseConnection,
+ NotebookBuilder,
+ QueryBuilder
} from './helper';
import { ColumnSQL } from './sql';
export async function showColumnProperties(item: DatabaseTreeItem) {
- try {
- const { connection, client, metadata } = await getDatabaseConnection(item);
- const schema = item.schema!;
- const tableName = item.tableName!;
- const columnName = item.columnName!;
-
- const result = await client.query(QueryBuilder.columnDetails(schema, tableName, columnName));
-
- if (result.rows.length === 0) {
- vscode.window.showErrorMessage('Column not found');
- return;
- }
-
- const col = result.rows[0];
-
- const dataTypeDetails = col.character_maximum_length
- ? `${col.data_type}(${col.character_maximum_length})`
- : col.numeric_precision
- ? `${col.data_type}(${col.numeric_precision}${col.numeric_scale ? ',' + col.numeric_scale : ''})`
- : col.data_type;
-
- const constraints = [];
- if (col.is_primary_key) constraints.push('🔑 PRIMARY KEY');
- if (col.is_foreign_key) constraints.push(`🔗 FOREIGN KEY → ${col.foreign_table_schema}.${col.foreign_table_name}.${col.foreign_column_name}`);
- if (col.is_unique) constraints.push('⭐ UNIQUE');
- if (col.is_nullable === 'NO') constraints.push('🚫 NOT NULL');
-
- const nb = new NotebookBuilder(metadata)
- .addMarkdown(
- MarkdownUtils.header(`📋 Column Properties: \`${col.column_name}\``) +
- MarkdownUtils.infoBox(`Table: \`${item.schema}.${tableName}\``) +
- `\n\n#### 📊 Basic Information\n\n` +
- MarkdownUtils.propertiesTable({
- 'Column Name': `${col.column_name}`,
- 'Data Type': `${dataTypeDetails}`,
- 'UDT Name': `${col.udt_name}`,
- 'Position': `${col.ordinal_position}`,
- 'Nullable': FormatHelpers.formatBoolean(col.is_nullable === 'YES'),
- 'Default Value': col.column_default ? `${col.column_default}` : '—'
- })
- );
-
- if (constraints.length > 0) {
- nb.addMarkdown(`#### 🔒 Constraints\n\n${constraints.map(c => `- ${c}`).join('\n')}`);
- }
-
- if (col.column_comment) {
- nb.addMarkdown(`#### 💬 Comment\n\n\`\`\`\n${col.column_comment}\n\`\`\``);
- }
-
- nb.addMarkdown('---')
- .addMarkdown('##### 📖 Query Column')
- .addSql(ColumnSQL.select(item.schema!, tableName, columnName))
- .addMarkdown('##### 📊 Column Statistics')
- .addSql(ColumnSQL.statistics(item.schema!, tableName, columnName));
-
- await nb.show();
-
- } catch (err: any) {
- await ErrorHandlers.handleCommandError(err, 'get column properties');
+ let dbConn;
+ try {
+ dbConn = await getDatabaseConnection(item);
+ const { client, metadata } = dbConn;
+ const schema = item.schema!;
+ const tableName = item.tableName!;
+ const columnName = item.columnName!;
+
+ const result = await client.query(QueryBuilder.columnDetails(schema, tableName, columnName));
+
+ if (result.rows.length === 0) {
+ vscode.window.showErrorMessage('Column not found');
+ return;
+ }
+
+ const col = result.rows[0];
+
+ const dataTypeDetails = col.character_maximum_length
+ ? `${col.data_type}(${col.character_maximum_length})`
+ : col.numeric_precision
+ ? `${col.data_type}(${col.numeric_precision}${col.numeric_scale ? ',' + col.numeric_scale : ''})`
+ : col.data_type;
+
+ const constraints = [];
+ if (col.is_primary_key) constraints.push('🔑 PRIMARY KEY');
+ if (col.is_foreign_key) constraints.push(`🔗 FOREIGN KEY → ${col.foreign_table_schema}.${col.foreign_table_name}.${col.foreign_column_name}`);
+ if (col.is_unique) constraints.push('⭐ UNIQUE');
+ if (col.is_nullable === 'NO') constraints.push('🚫 NOT NULL');
+
+ const nb = new NotebookBuilder(metadata)
+ .addMarkdown(
+ MarkdownUtils.header(`📋 Column Properties: \`${col.column_name}\``) +
+ MarkdownUtils.infoBox(`Table: \`${item.schema}.${tableName}\``) +
+ `\n\n#### 📊 Basic Information\n\n` +
+ MarkdownUtils.propertiesTable({
+ 'Column Name': `${col.column_name}`,
+ 'Data Type': `${dataTypeDetails}`,
+ 'UDT Name': `${col.udt_name}`,
+ 'Position': `${col.ordinal_position}`,
+ 'Nullable': FormatHelpers.formatBoolean(col.is_nullable === 'YES'),
+ 'Default Value': col.column_default ? `${col.column_default}` : '—'
+ })
+ );
+
+ if (constraints.length > 0) {
+ nb.addMarkdown(`#### 🔒 Constraints\n\n${constraints.map(c => `- ${c}`).join('\n')}`);
}
+
+ if (col.column_comment) {
+ nb.addMarkdown(`#### 💬 Comment\n\n\`\`\`\n${col.column_comment}\n\`\`\``);
+ }
+
+ nb.addMarkdown('---')
+ .addMarkdown('##### 📖 Query Column')
+ .addSql(ColumnSQL.select(item.schema!, tableName, columnName))
+ .addMarkdown('##### 📊 Column Statistics')
+ .addSql(ColumnSQL.statistics(item.schema!, tableName, columnName));
+
+ await nb.show();
+
+ } catch (err: any) {
+ await ErrorHandlers.handleCommandError(err, 'get column properties');
+ } finally {
+ if (dbConn && dbConn.release) dbConn.release();
+ }
}
export async function copyColumnName(item: DatabaseTreeItem) {
- const columnName = item.columnName!;
- await vscode.env.clipboard.writeText(columnName);
- vscode.window.showInformationMessage(`Copied: ${columnName}`);
+ const columnName = item.columnName!;
+ await vscode.env.clipboard.writeText(columnName);
+ vscode.window.showInformationMessage(`Copied: ${columnName}`);
}
export async function copyColumnNameQuoted(item: DatabaseTreeItem) {
- const columnName = item.columnName!;
- await vscode.env.clipboard.writeText(`"${columnName}"`);
- vscode.window.showInformationMessage(`Copied: "${columnName}"`);
+ const columnName = item.columnName!;
+ await vscode.env.clipboard.writeText(`"${columnName}"`);
+ vscode.window.showInformationMessage(`Copied: "${columnName}"`);
}
export async function generateSelectStatement(item: DatabaseTreeItem) {
- try {
- const { connection, client, metadata } = await getDatabaseConnection(item);
- const columnName = item.columnName!;
- const tableName = item.tableName!;
-
- await new NotebookBuilder(metadata)
- .addMarkdown(
- MarkdownUtils.header(`📖 SELECT Statement: \`${columnName}\``) +
- MarkdownUtils.infoBox(`Query specific column from \`${item.schema}.${tableName}\``)
- )
- .addSql(`-- Select ${columnName} column
+ let dbConn;
+ try {
+ dbConn = await getDatabaseConnection(item);
+ const { metadata } = dbConn;
+ const columnName = item.columnName!;
+ const tableName = item.tableName!;
+
+ await new NotebookBuilder(metadata)
+ .addMarkdown(
+ MarkdownUtils.header(`📖 SELECT Statement: \`${columnName}\``) +
+ MarkdownUtils.infoBox(`Query specific column from \`${item.schema}.${tableName}\``)
+ )
+ .addSql(`-- Select ${columnName} column
SELECT ${columnName}
FROM ${item.schema}.${tableName}
LIMIT 100;`)
- .show();
- } catch (err: any) {
- await ErrorHandlers.handleCommandError(err, 'generate SELECT statement');
- }
+ .show();
+ } catch (err: any) {
+ await ErrorHandlers.handleCommandError(err, 'generate SELECT statement');
+ } finally {
+ if (dbConn && dbConn.release) dbConn.release();
+ }
}
export async function generateWhereClause(item: DatabaseTreeItem) {
- try {
- const { connection, client, metadata } = await getDatabaseConnection(item);
- const columnName = item.columnName!;
- const tableName = item.tableName!;
-
- await new NotebookBuilder(metadata)
- .addMarkdown(
- MarkdownUtils.header(`🔍 WHERE Clause Templates: \`${columnName}\``) +
- MarkdownUtils.infoBox(`Common WHERE clause patterns for filtering by \`${columnName}\``)
- )
- .addSql(ColumnSQL.whereTemplates(item.schema!, tableName, columnName))
- .show();
- } catch (err: any) {
- await ErrorHandlers.handleCommandError(err, 'generate WHERE clause');
- }
+ let dbConn;
+ try {
+ dbConn = await getDatabaseConnection(item);
+ const { metadata } = dbConn;
+ const columnName = item.columnName!;
+ const tableName = item.tableName!;
+
+ await new NotebookBuilder(metadata)
+ .addMarkdown(
+ MarkdownUtils.header(`🔍 WHERE Clause Templates: \`${columnName}\``) +
+ MarkdownUtils.infoBox(`Common WHERE clause patterns for filtering by \`${columnName}\``)
+ )
+ .addSql(ColumnSQL.whereTemplates(item.schema!, tableName, columnName))
+ .show();
+ } catch (err: any) {
+ await ErrorHandlers.handleCommandError(err, 'generate WHERE clause');
+ } finally {
+ if (dbConn && dbConn.release) dbConn.release();
+ }
}
export async function generateAlterColumnScript(item: DatabaseTreeItem) {
- try {
- const { connection, client, metadata } = await getDatabaseConnection(item);
- const columnName = item.columnName!;
- const tableName = item.tableName!;
-
- await new NotebookBuilder(metadata)
- .addMarkdown(
- MarkdownUtils.header(`✏️ ALTER COLUMN Script: \`${columnName}\``) +
- MarkdownUtils.warningBox('Modifying column structure may fail if data doesn\'t match new constraints or type.') +
- `\n\n#### Available Modifications\n\n` +
- MarkdownUtils.operationsTable([
- { operation: 'Change Data Type', description: 'Convert column to different type' },
- { operation: 'Set NOT NULL', description: 'Prevent NULL values' },
- { operation: 'Drop NOT NULL', description: 'Allow NULL values' },
- { operation: 'Set Default', description: 'Add default value for new rows' },
- { operation: 'Drop Default', description: 'Remove default value' }
- ])
- )
- .addSql(ColumnSQL.alter(item.schema!, tableName, columnName))
- .show();
- } catch (err: any) {
- await ErrorHandlers.handleCommandError(err, 'generate ALTER COLUMN script');
- }
+ let dbConn;
+ try {
+ dbConn = await getDatabaseConnection(item);
+ const { metadata } = dbConn;
+ const columnName = item.columnName!;
+ const tableName = item.tableName!;
+
+ await new NotebookBuilder(metadata)
+ .addMarkdown(
+ MarkdownUtils.header(`✏️ ALTER COLUMN Script: \`${columnName}\``) +
+ MarkdownUtils.warningBox('Modifying column structure may fail if data doesn\'t match new constraints or type.') +
+ `\n\n#### Available Modifications\n\n` +
+ MarkdownUtils.operationsTable([
+ { operation: 'Change Data Type', description: 'Convert column to different type' },
+ { operation: 'Set NOT NULL', description: 'Prevent NULL values' },
+ { operation: 'Drop NOT NULL', description: 'Allow NULL values' },
+ { operation: 'Set Default', description: 'Add default value for new rows' },
+ { operation: 'Drop Default', description: 'Remove default value' }
+ ])
+ )
+ .addSql(ColumnSQL.alter(item.schema!, tableName, columnName))
+ .show();
+ } catch (err: any) {
+ await ErrorHandlers.handleCommandError(err, 'generate ALTER COLUMN script');
+ } finally {
+ if (dbConn && dbConn.release) dbConn.release();
+ }
}
export async function generateDropColumnScript(item: DatabaseTreeItem) {
- try {
- const { connection, client, metadata } = await getDatabaseConnection(item);
- const columnName = item.columnName!;
- const tableName = item.tableName!;
-
- await new NotebookBuilder(metadata)
- .addMarkdown(
- MarkdownUtils.header(`🗑️ DROP COLUMN Script: \`${columnName}\``) +
- MarkdownUtils.dangerBox('This will permanently delete the column and ALL its data! This operation cannot be undone.') +
- `\n\n#### Before You Drop\n\n` +
- `1. **Backup your data** - Export table or create a snapshot\n` +
- `2. **Check dependencies** - Views, functions, or triggers may use this column\n` +
- `3. **Test on non-production** - Verify the change works as expected\n\n` +
- `\n\n#### Options\n\n` +
- `- **RESTRICT** (default): Fails if column has dependencies\n` +
- `- **CASCADE**: Automatically drops dependent objects (views, triggers, etc.)`
- )
- .addSql(ColumnSQL.drop(item.schema!, tableName, columnName))
- .show();
- } catch (err: any) {
- await ErrorHandlers.handleCommandError(err, 'generate DROP COLUMN script');
- }
+ let dbConn;
+ try {
+ dbConn = await getDatabaseConnection(item);
+ const { metadata } = dbConn;
+ const columnName = item.columnName!;
+ const tableName = item.tableName!;
+
+ await new NotebookBuilder(metadata)
+ .addMarkdown(
+ MarkdownUtils.header(`🗑️ DROP COLUMN Script: \`${columnName}\``) +
+ MarkdownUtils.dangerBox('This will permanently delete the column and ALL its data! This operation cannot be undone.') +
+ `\n\n#### Before You Drop\n\n` +
+ `1. **Backup your data** - Export table or create a snapshot\n` +
+ `2. **Check dependencies** - Views, functions, or triggers may use this column\n` +
+ `3. **Test on non-production** - Verify the change works as expected\n\n` +
+ `\n\n#### Options\n\n` +
+ `- **RESTRICT** (default): Fails if column has dependencies\n` +
+ `- **CASCADE**: Automatically drops dependent objects (views, triggers, etc.)`
+ )
+ .addSql(ColumnSQL.drop(item.schema!, tableName, columnName))
+ .show();
+ } catch (err: any) {
+ await ErrorHandlers.handleCommandError(err, 'generate DROP COLUMN script');
+ } finally {
+ if (dbConn && dbConn.release) dbConn.release();
+ }
}
export async function generateRenameColumnScript(item: DatabaseTreeItem) {
- try {
- const { connection, client, metadata } = await getDatabaseConnection(item);
- const columnName = item.columnName!;
- const tableName = item.tableName!;
-
- const newName = await vscode.window.showInputBox({
- prompt: 'Enter new column name',
- value: columnName,
- validateInput: ValidationHelpers.validateColumnName
- });
-
- if (!newName || newName === columnName) {
- return;
- }
-
- await new NotebookBuilder(metadata)
- .addMarkdown(
- MarkdownUtils.header(`🔄 RENAME COLUMN: \`${columnName}\` → \`${newName}\``) +
- MarkdownUtils.infoBox('Renaming is safe and atomic. Dependent objects (views, functions) will be automatically updated.')
- )
- .addSql(ColumnSQL.rename(item.schema!, tableName, columnName, newName))
- .show();
- } catch (err: any) {
- await ErrorHandlers.handleCommandError(err, 'generate RENAME COLUMN script');
+ let dbConn;
+ try {
+ dbConn = await getDatabaseConnection(item);
+ const { metadata } = dbConn;
+ const columnName = item.columnName!;
+ const tableName = item.tableName!;
+
+ const newName = await vscode.window.showInputBox({
+ prompt: 'Enter new column name',
+ value: columnName,
+ validateInput: ValidationHelpers.validateColumnName
+ });
+
+ if (!newName || newName === columnName) {
+ return;
}
+
+ await new NotebookBuilder(metadata)
+ .addMarkdown(
+ MarkdownUtils.header(`🔄 RENAME COLUMN: \`${columnName}\` → \`${newName}\``) +
+ MarkdownUtils.infoBox('Renaming is safe and atomic. Dependent objects (views, functions) will be automatically updated.')
+ )
+ .addSql(ColumnSQL.rename(item.schema!, tableName, columnName, newName))
+ .show();
+ } catch (err: any) {
+ await ErrorHandlers.handleCommandError(err, 'generate RENAME COLUMN script');
+ } finally {
+ if (dbConn && dbConn.release) dbConn.release();
+ }
}
export async function addColumnComment(item: DatabaseTreeItem) {
- try {
- const { connection, client, metadata } = await getDatabaseConnection(item);
- const columnName = item.columnName!;
- const tableName = item.tableName!;
-
- const comment = await vscode.window.showInputBox({
- prompt: `Enter comment for column ${columnName}`,
- placeHolder: 'Column description...'
- });
-
- if (comment === undefined) {
- return;
- }
-
- await new NotebookBuilder(metadata)
- .addMarkdown(
- MarkdownUtils.header(`💬 Add Column Comment: \`${columnName}\``) +
- MarkdownUtils.infoBox('Column comments are stored in PostgreSQL system catalogs and visible in `pg_stats`')
- )
- .addSql(`-- Add/update comment for column ${columnName}
+ let dbConn;
+ try {
+ dbConn = await getDatabaseConnection(item);
+ const { metadata } = dbConn;
+ const columnName = item.columnName!;
+ const tableName = item.tableName!;
+
+ const comment = await vscode.window.showInputBox({
+ prompt: `Enter comment for column ${columnName}`,
+ placeHolder: 'Column description...'
+ });
+
+ if (comment === undefined) {
+ return;
+ }
+
+ await new NotebookBuilder(metadata)
+ .addMarkdown(
+ MarkdownUtils.header(`💬 Add Column Comment: \`${columnName}\``) +
+ MarkdownUtils.infoBox('Column comments are stored in PostgreSQL system catalogs and visible in `pg_stats`')
+ )
+ .addSql(`-- Add/update comment for column ${columnName}
${SQL_TEMPLATES.COMMENT.COLUMN(item.schema!, tableName, columnName, comment)}
-- To remove a comment, use:
-- COMMENT ON COLUMN ${item.schema}.${tableName}.${columnName} IS NULL;`)
- .show();
- } catch (err: any) {
- await ErrorHandlers.handleCommandError(err, 'generate comment script');
- }
+ .show();
+ } catch (err: any) {
+ await ErrorHandlers.handleCommandError(err, 'generate comment script');
+ } finally {
+ if (dbConn && dbConn.release) dbConn.release();
+ }
}
export async function generateIndexOnColumn(item: DatabaseTreeItem) {
- try {
- const { connection, client, metadata } = await getDatabaseConnection(item);
- const columnName = item.columnName!;
- const tableName = item.tableName!;
-
- const indexName = await vscode.window.showInputBox({
- prompt: 'Enter index name',
- value: `idx_${tableName}_${columnName}`,
- validateInput: (value) => ValidationHelpers.validateIdentifier(value, 'index')
- });
-
- if (!indexName) {
- return;
- }
-
- await new NotebookBuilder(metadata)
- .addMarkdown(
- MarkdownUtils.header(`🔍 CREATE INDEX: \`${indexName}\``) +
- MarkdownUtils.infoBox('Indexes improve query performance but slow down write operations. Choose the right index type for your use case.') +
- `\n\n#### Index Types\n\n` +
- MarkdownUtils.operationsTable([
- { operation: 'B-tree (default)', description: 'Most queries, equality and range' },
- { operation: 'Hash', description: 'Simple equality comparisons only' },
- { operation: 'GIN', description: 'Array, JSONB, full-text search' },
- { operation: 'GiST', description: 'Geometric data, full-text search' }
- ]) + `\n` +
- MarkdownUtils.successBox('Use `CREATE INDEX CONCURRENTLY` to avoid locking the table during index creation on production databases.')
- )
- .addSql(ColumnSQL.createIndex(item.schema!, tableName, columnName, indexName))
- .show();
- } catch (err: any) {
- await ErrorHandlers.handleCommandError(err, 'generate CREATE INDEX script');
+ let dbConn;
+ try {
+ dbConn = await getDatabaseConnection(item);
+ const { metadata } = dbConn;
+ const columnName = item.columnName!;
+ const tableName = item.tableName!;
+
+ const indexName = await vscode.window.showInputBox({
+ prompt: 'Enter index name',
+ value: `idx_${tableName}_${columnName}`,
+ validateInput: (value) => ValidationHelpers.validateIdentifier(value, 'index')
+ });
+
+ if (!indexName) {
+ return;
}
+
+ await new NotebookBuilder(metadata)
+ .addMarkdown(
+ MarkdownUtils.header(`🔍 CREATE INDEX: \`${indexName}\``) +
+ MarkdownUtils.infoBox('Indexes improve query performance but slow down write operations. Choose the right index type for your use case.') +
+ `\n\n#### Index Types\n\n` +
+ MarkdownUtils.operationsTable([
+ { operation: 'B-tree (default)', description: 'Most queries, equality and range' },
+ { operation: 'Hash', description: 'Simple equality comparisons only' },
+ { operation: 'GIN', description: 'Array, JSONB, full-text search' },
+ { operation: 'GiST', description: 'Geometric data, full-text search' }
+ ]) + `\n` +
+ MarkdownUtils.successBox('Use `CREATE INDEX CONCURRENTLY` to avoid locking the table during index creation on production databases.')
+ )
+ .addSql(ColumnSQL.createIndex(item.schema!, tableName, columnName, indexName))
+ .show();
+ } catch (err: any) {
+ await ErrorHandlers.handleCommandError(err, 'generate CREATE INDEX script');
+ } finally {
+ if (dbConn && dbConn.release) dbConn.release();
+ }
}
export async function viewColumnStatistics(item: DatabaseTreeItem) {
- try {
- const { connection, client, metadata } = await getDatabaseConnection(item);
- const columnName = item.columnName!;
- const tableName = item.tableName!;
-
- await new NotebookBuilder(metadata)
- .addMarkdown(
- MarkdownUtils.header(`📊 Column Statistics: \`${columnName}\``) +
- MarkdownUtils.infoBox(`Statistics are collected by \`ANALYZE\` and used by the query planner for optimization. Run \`ANALYZE ${item.schema}.${tableName};\` if statistics are missing.`) +
- `\n\n#### Statistics Explained\n\n` +
- MarkdownUtils.operationsTable([
- { operation: 'n_distinct', description: 'Estimated number of distinct values (-1 = all unique, 0-1 = fraction, >1 = count)' },
- { operation: 'null_frac', description: 'Fraction of NULL values (0.0 to 1.0)' },
- { operation: 'avg_width', description: 'Average storage width in bytes' },
- { operation: 'correlation', description: 'Statistical correlation between physical row order and logical order (-1 to 1)' }
- ])
- )
- .addSql(ColumnSQL.detailedStatistics(item.schema!, tableName, columnName))
- .show();
- } catch (err: any) {
- await ErrorHandlers.handleCommandError(err, 'view column statistics');
- }
+ let dbConn;
+ try {
+ dbConn = await getDatabaseConnection(item);
+ const { metadata } = dbConn;
+ const columnName = item.columnName!;
+ const tableName = item.tableName!;
+
+ await new NotebookBuilder(metadata)
+ .addMarkdown(
+ MarkdownUtils.header(`📊 Column Statistics: \`${columnName}\``) +
+ MarkdownUtils.infoBox(`Statistics are collected by \`ANALYZE\` and used by the query planner for optimization. Run \`ANALYZE ${item.schema}.${tableName};\` if statistics are missing.`) +
+ `\n\n#### Statistics Explained\n\n` +
+ MarkdownUtils.operationsTable([
+ { operation: 'n_distinct', description: 'Estimated number of distinct values (-1 = all unique, 0-1 = fraction, >1 = count)' },
+ { operation: 'null_frac', description: 'Fraction of NULL values (0.0 to 1.0)' },
+ { operation: 'avg_width', description: 'Average storage width in bytes' },
+ { operation: 'correlation', description: 'Statistical correlation between physical row order and logical order (-1 to 1)' }
+ ])
+ )
+ .addSql(ColumnSQL.detailedStatistics(item.schema!, tableName, columnName))
+ .show();
+ } catch (err: any) {
+ await ErrorHandlers.handleCommandError(err, 'view column statistics');
+ } finally {
+ if (dbConn && dbConn.release) dbConn.release();
+ }
}
/**
* Add new column to table - generates a comprehensive notebook with guidelines and SQL templates
*/
export async function cmdAddColumn(item: DatabaseTreeItem): Promise {
- try {
- const { connection, client, metadata } = await getDatabaseConnection(item);
- const schema = item.schema!;
- const tableName = item.tableName!;
-
- await new NotebookBuilder(metadata)
- .addMarkdown(
- MarkdownUtils.header(`➕ Add New Column to \`${schema}.${tableName}\``) +
- MarkdownUtils.infoBox('This notebook provides templates for adding new columns. Modify the templates below and execute to add columns.') +
- `\n\n#### 📋 Guidelines for Adding Columns\n\n` +
- MarkdownUtils.operationsTable([
- { operation: 'Naming', description: 'Use snake_case (e.g., user_name, created_at). Avoid reserved words.' },
- { operation: 'Data Types', description: 'Choose appropriate types: INTEGER, VARCHAR(n), TEXT, BOOLEAN, TIMESTAMP, JSONB, UUID' },
- { operation: 'Constraints', description: 'Consider NOT NULL, DEFAULT, CHECK constraints at column level' },
- { operation: 'Performance', description: 'Adding columns is fast but adding with DEFAULT on large tables may lock' }
- ]) +
- `\n\n#### 🏷️ Common Data Types Reference\n\n` +
- MarkdownUtils.propertiesTable({
- 'INTEGER / BIGINT': 'Whole numbers (4/8 bytes)',
- 'SERIAL / BIGSERIAL': 'Auto-incrementing integer',
- 'VARCHAR(n) / TEXT': 'Variable-length strings',
- 'BOOLEAN': 'true/false values',
- 'TIMESTAMP / TIMESTAMPTZ': 'Date and time (with/without timezone)',
- 'DATE / TIME': 'Date or time only',
- 'NUMERIC(p,s)': 'Exact decimal numbers',
- 'JSONB': 'Binary JSON (recommended for JSON data)',
- 'UUID': 'Universally unique identifier',
- 'ARRAY': 'Array of any type (e.g., INTEGER[])',
- }) +
- `\n\n---`
- )
- .addMarkdown('##### 📝 Basic Column (Recommended Start)')
- .addSql(ColumnSQL.add.basic(schema, tableName))
- .addMarkdown('##### 🔒 Column with NOT NULL and DEFAULT')
- .addSql(ColumnSQL.add.withDefault(schema, tableName))
- .addMarkdown('##### ⏰ Timestamp Columns')
- .addSql(ColumnSQL.add.timestamps(schema, tableName))
- .addMarkdown('##### ✅ Column with CHECK Constraint')
- .addSql(ColumnSQL.add.withCheck(schema, tableName))
- .addMarkdown('##### 🔗 Foreign Key Column')
- .addSql(ColumnSQL.add.foreignKey(schema, tableName))
- .addMarkdown('##### 📄 JSON Column')
- .addSql(ColumnSQL.add.jsonb(schema, tableName))
- .addMarkdown('##### 🆔 UUID Column')
- .addSql(ColumnSQL.add.uuid(schema, tableName))
- .addMarkdown(MarkdownUtils.warningBox('After adding columns, consider adding indexes for frequently queried columns and updating application code.'))
- .show();
- } catch (err: any) {
- await ErrorHandlers.handleCommandError(err, 'add column');
- }
+ let dbConn;
+ try {
+ dbConn = await getDatabaseConnection(item);
+ const { metadata } = dbConn;
+ const schema = item.schema!;
+ const tableName = item.tableName!;
+
+ await new NotebookBuilder(metadata)
+ .addMarkdown(
+ MarkdownUtils.header(`➕ Add New Column to \`${schema}.${tableName}\``) +
+ MarkdownUtils.infoBox('This notebook provides templates for adding new columns. Modify the templates below and execute to add columns.') +
+ `\n\n#### 📋 Guidelines for Adding Columns\n\n` +
+ MarkdownUtils.operationsTable([
+ { operation: 'Naming', description: 'Use snake_case (e.g., user_name, created_at). Avoid reserved words.' },
+ { operation: 'Data Types', description: 'Choose appropriate types: INTEGER, VARCHAR(n), TEXT, BOOLEAN, TIMESTAMP, JSONB, UUID' },
+ { operation: 'Constraints', description: 'Consider NOT NULL, DEFAULT, CHECK constraints at column level' },
+ { operation: 'Performance', description: 'Adding columns is fast but adding with DEFAULT on large tables may lock' }
+ ]) +
+ `\n\n#### 🏷️ Common Data Types Reference\n\n` +
+ MarkdownUtils.propertiesTable({
+ 'INTEGER / BIGINT': 'Whole numbers (4/8 bytes)',
+ 'SERIAL / BIGSERIAL': 'Auto-incrementing integer',
+ 'VARCHAR(n) / TEXT': 'Variable-length strings',
+ 'BOOLEAN': 'true/false values',
+ 'TIMESTAMP / TIMESTAMPTZ': 'Date and time (with/without timezone)',
+ 'DATE / TIME': 'Date or time only',
+ 'NUMERIC(p,s)': 'Exact decimal numbers',
+ 'JSONB': 'Binary JSON (recommended for JSON data)',
+ 'UUID': 'Universally unique identifier',
+ 'ARRAY': 'Array of any type (e.g., INTEGER[])',
+ }) +
+ `\n\n---`
+ )
+ .addMarkdown('##### 📝 Basic Column (Recommended Start)')
+ .addSql(ColumnSQL.add.basic(schema, tableName))
+ .addMarkdown('##### 🔒 Column with NOT NULL and DEFAULT')
+ .addSql(ColumnSQL.add.withDefault(schema, tableName))
+ .addMarkdown('##### ⏰ Timestamp Columns')
+ .addSql(ColumnSQL.add.timestamps(schema, tableName))
+ .addMarkdown('##### ✅ Column with CHECK Constraint')
+ .addSql(ColumnSQL.add.withCheck(schema, tableName))
+ .addMarkdown('##### 🔗 Foreign Key Column')
+ .addSql(ColumnSQL.add.foreignKey(schema, tableName))
+ .addMarkdown('##### 📄 JSON Column')
+ .addSql(ColumnSQL.add.jsonb(schema, tableName))
+ .addMarkdown('##### 🆔 UUID Column')
+ .addSql(ColumnSQL.add.uuid(schema, tableName))
+ .addMarkdown(MarkdownUtils.warningBox('After adding columns, consider adding indexes for frequently queried columns and updating application code.'))
+ .show();
+ } catch (err: any) {
+ await ErrorHandlers.handleCommandError(err, 'add column');
+ } finally {
+ if (dbConn && dbConn.release) dbConn.release();
+ }
}
diff --git a/src/commands/constraints.ts b/src/commands/constraints.ts
index bab8428..e005cb9 100644
--- a/src/commands/constraints.ts
+++ b/src/commands/constraints.ts
@@ -1,14 +1,14 @@
import * as vscode from 'vscode';
import { DatabaseTreeItem } from '../providers/DatabaseTreeProvider';
import {
- MarkdownUtils,
- FormatHelpers,
- ErrorHandlers,
- SQL_TEMPLATES,
- ObjectUtils,
- getDatabaseConnection,
- NotebookBuilder,
- QueryBuilder
+ MarkdownUtils,
+ FormatHelpers,
+ ErrorHandlers,
+ SQL_TEMPLATES,
+ ObjectUtils,
+ getDatabaseConnection,
+ NotebookBuilder,
+ QueryBuilder
} from './helper';
import { ConstraintSQL } from './sql';
@@ -16,182 +16,200 @@ import { ConstraintSQL } from './sql';
* Show constraint properties in a notebook
*/
export async function showConstraintProperties(treeItem: DatabaseTreeItem): Promise {
- try {
- const { connection, client, metadata } = await getDatabaseConnection(treeItem);
- const schema = treeItem.schema!;
- const tableName = treeItem.tableName!;
- const constraintName = treeItem.label;
-
- // Get detailed constraint information
- const result = await client.query(QueryBuilder.constraintDetails(schema, tableName, constraintName));
-
- if (result.rows.length === 0) {
- vscode.window.showErrorMessage('Constraint not found');
- return;
- }
-
- const constraint = result.rows[0];
-
- // Build constraint type icon
- const typeIcon = ObjectUtils.getConstraintIcon(constraint.constraint_type);
-
- const nb = new NotebookBuilder(metadata)
- .addMarkdown(
- MarkdownUtils.header(`${typeIcon} Constraint Properties: \`${constraint.constraint_name}\``) +
- MarkdownUtils.infoBox(`Table: \`${schema}.${tableName}\``) +
- `\n\n#### 📊 Basic Information\n\n` +
- MarkdownUtils.propertiesTable({
- 'Constraint Name': `${constraint.constraint_name}`,
- 'Type': `${constraint.constraint_type}`,
- 'Columns': `${constraint.columns || '—'}`,
- 'Deferrable': FormatHelpers.formatBoolean(constraint.is_deferrable === 'YES'),
- 'Initially Deferred': FormatHelpers.formatBoolean(constraint.initially_deferred === 'YES')
- })
- );
-
- if (constraint.constraint_definition) {
- nb.addMarkdown(`#### 🔧 Definition\n\n\`\`\`sql\n${constraint.constraint_definition}\n\`\`\``);
- }
-
- if (constraint.check_clause) {
- nb.addMarkdown(`#### ✓ Check Clause\n\n\`\`\`sql\n${constraint.check_clause}\n\`\`\``);
- }
-
- if (constraint.comment) {
- nb.addMarkdown(`#### 💬 Comment\n\n\`\`\`\n${constraint.comment}\n\`\`\``);
- }
-
- // Get foreign key details if applicable
- if (constraint.constraint_type === 'FOREIGN KEY') {
- const fkResult = await client.query(QueryBuilder.foreignKeyDetails(schema, constraintName));
-
- if (fkResult.rows.length > 0) {
- let fkMarkdown = `#### 🔗 Foreign Key References\n\n
- | Column | References | On Update | On Delete |
`;
+ let dbConn;
+ try {
+ dbConn = await getDatabaseConnection(treeItem);
+ const { client, metadata } = dbConn;
+ const schema = treeItem.schema!;
+ const tableName = treeItem.tableName!;
+ const constraintName = treeItem.label;
- fkResult.rows.forEach((row: any) => {
- fkMarkdown += `\n ${row.column_name} | ${row.foreign_table_schema}.${row.foreign_table_name}.${row.foreign_column_name} | ${row.update_rule} | ${row.delete_rule} |
`;
- });
+ // Get detailed constraint information
+ const result = await client.query(QueryBuilder.constraintDetails(schema, tableName, constraintName));
- fkMarkdown += '\n
';
- nb.addMarkdown(fkMarkdown);
- }
- }
+ if (result.rows.length === 0) {
+ vscode.window.showErrorMessage('Constraint not found');
+ return;
+ }
- nb.addMarkdown('##### 📖 Query Constraint Details')
- .addSql(ConstraintSQL.details(schema, constraintName));
+ const constraint = result.rows[0];
+
+ // Build constraint type icon
+ const typeIcon = ObjectUtils.getConstraintIcon(constraint.constraint_type);
+
+ const nb = new NotebookBuilder(metadata)
+ .addMarkdown(
+ MarkdownUtils.header(`${typeIcon} Constraint Properties: \`${constraint.constraint_name}\``) +
+ MarkdownUtils.infoBox(`Table: \`${schema}.${tableName}\``) +
+ `\n\n#### 📊 Basic Information\n\n` +
+ MarkdownUtils.propertiesTable({
+ 'Constraint Name': `${constraint.constraint_name}`,
+ 'Type': `${constraint.constraint_type}`,
+ 'Columns': `${constraint.columns || '—'}`,
+ 'Deferrable': FormatHelpers.formatBoolean(constraint.is_deferrable === 'YES'),
+ 'Initially Deferred': FormatHelpers.formatBoolean(constraint.initially_deferred === 'YES')
+ })
+ );
+
+ if (constraint.constraint_definition) {
+ nb.addMarkdown(`#### 🔧 Definition\n\n\`\`\`sql\n${constraint.constraint_definition}\n\`\`\``);
+ }
- await nb.show();
+ if (constraint.check_clause) {
+ nb.addMarkdown(`#### ✓ Check Clause\n\n\`\`\`sql\n${constraint.check_clause}\n\`\`\``);
+ }
- } catch (err: any) {
- await ErrorHandlers.handleCommandError(err, 'show constraint properties');
+ if (constraint.comment) {
+ nb.addMarkdown(`#### 💬 Comment\n\n\`\`\`\n${constraint.comment}\n\`\`\``);
}
+
+ // Get foreign key details if applicable
+ if (constraint.constraint_type === 'FOREIGN KEY') {
+ const fkResult = await client.query(QueryBuilder.foreignKeyDetails(schema, constraintName));
+
+ if (fkResult.rows.length > 0) {
+ let fkMarkdown = `#### 🔗 Foreign Key References\n\n
+ | Column | References | On Update | On Delete |
`;
+
+ fkResult.rows.forEach((row: any) => {
+ fkMarkdown += `\n ${row.column_name} | ${row.foreign_table_schema}.${row.foreign_table_name}.${row.foreign_column_name} | ${row.update_rule} | ${row.delete_rule} |
`;
+ });
+
+ fkMarkdown += '\n
';
+ nb.addMarkdown(fkMarkdown);
+ }
+ }
+
+ nb.addMarkdown('##### 📖 Query Constraint Details')
+ .addSql(ConstraintSQL.details(schema, constraintName));
+
+ await nb.show();
+
+ } catch (err: any) {
+ await ErrorHandlers.handleCommandError(err, 'show constraint properties');
+ } finally {
+ if (dbConn && dbConn.release) dbConn.release();
+ }
}
/**
* Copy constraint name to clipboard
*/
export async function copyConstraintName(treeItem: DatabaseTreeItem): Promise {
- const constraintName = treeItem.label;
- await vscode.env.clipboard.writeText(constraintName);
- vscode.window.showInformationMessage(`Copied: ${constraintName}`);
+ const constraintName = treeItem.label;
+ await vscode.env.clipboard.writeText(constraintName);
+ vscode.window.showInformationMessage(`Copied: ${constraintName}`);
}
/**
* Generate DROP CONSTRAINT script
*/
export async function generateDropConstraintScript(treeItem: DatabaseTreeItem): Promise {
- try {
- const { connection, client, metadata } = await getDatabaseConnection(treeItem);
- const schema = treeItem.schema!;
- const tableName = treeItem.tableName!;
- const constraintName = treeItem.label;
-
- await new NotebookBuilder(metadata)
- .addMarkdown(
- MarkdownUtils.header(`🗑️ DROP CONSTRAINT: \`${constraintName}\``) +
- MarkdownUtils.dangerBox('This will remove the constraint from the table. Data integrity checks enforced by this constraint will no longer apply.') +
- MarkdownUtils.infoBox(`Table: \`${schema}.${tableName}\``)
- )
- .addSql(SQL_TEMPLATES.DROP.CONSTRAINT(schema, tableName, constraintName))
- .show();
- } catch (err: any) {
- await ErrorHandlers.handleCommandError(err, 'generate drop constraint script');
- }
+ let dbConn;
+ try {
+ dbConn = await getDatabaseConnection(treeItem);
+ const { metadata } = dbConn;
+ const schema = treeItem.schema!;
+ const tableName = treeItem.tableName!;
+ const constraintName = treeItem.label;
+
+ await new NotebookBuilder(metadata)
+ .addMarkdown(
+ MarkdownUtils.header(`🗑️ DROP CONSTRAINT: \`${constraintName}\``) +
+ MarkdownUtils.dangerBox('This will remove the constraint from the table. Data integrity checks enforced by this constraint will no longer apply.') +
+ MarkdownUtils.infoBox(`Table: \`${schema}.${tableName}\``)
+ )
+ .addSql(SQL_TEMPLATES.DROP.CONSTRAINT(schema, tableName, constraintName))
+ .show();
+ } catch (err: any) {
+ await ErrorHandlers.handleCommandError(err, 'generate drop constraint script');
+ } finally {
+ if (dbConn && dbConn.release) dbConn.release();
+ }
}
/**
* Generate ALTER CONSTRAINT script
*/
export async function generateAlterConstraintScript(treeItem: DatabaseTreeItem): Promise {
- try {
- const { connection, client, metadata } = await getDatabaseConnection(treeItem);
- const schema = treeItem.schema!;
- const tableName = treeItem.tableName!;
- const constraintName = treeItem.label;
-
- await new NotebookBuilder(metadata)
- .addMarkdown(
- MarkdownUtils.header(`✏️ ALTER CONSTRAINT: \`${constraintName}\``) +
- MarkdownUtils.infoBox(`Table: \`${schema}.${tableName}\``) +
- `\n\n#### Available Operations\n\n` +
- MarkdownUtils.operationsTable([
- { operation: 'RENAME', description: 'Change the name of the constraint' },
- { operation: 'VALIDATE', description: 'Validate a constraint that was created as NOT VALID' }
- ])
- )
- .addSql(ConstraintSQL.rename(schema, tableName, constraintName))
- .show();
- } catch (err: any) {
- await ErrorHandlers.handleCommandError(err, 'generate alter constraint script');
- }
+ let dbConn;
+ try {
+ dbConn = await getDatabaseConnection(treeItem);
+ const { metadata } = dbConn;
+ const schema = treeItem.schema!;
+ const tableName = treeItem.tableName!;
+ const constraintName = treeItem.label;
+
+ await new NotebookBuilder(metadata)
+ .addMarkdown(
+ MarkdownUtils.header(`✏️ ALTER CONSTRAINT: \`${constraintName}\``) +
+ MarkdownUtils.infoBox(`Table: \`${schema}.${tableName}\``) +
+ `\n\n#### Available Operations\n\n` +
+ MarkdownUtils.operationsTable([
+ { operation: 'RENAME', description: 'Change the name of the constraint' },
+ { operation: 'VALIDATE', description: 'Validate a constraint that was created as NOT VALID' }
+ ])
+ )
+ .addSql(ConstraintSQL.rename(schema, tableName, constraintName))
+ .show();
+ } catch (err: any) {
+ await ErrorHandlers.handleCommandError(err, 'generate alter constraint script');
+ } finally {
+ if (dbConn && dbConn.release) dbConn.release();
+ }
}
/**
* Validate constraint
*/
export async function validateConstraint(treeItem: DatabaseTreeItem): Promise {
- try {
- const { connection, client, metadata } = await getDatabaseConnection(treeItem);
- const schema = treeItem.schema!;
- const tableName = treeItem.tableName!;
- const constraintName = treeItem.label;
-
- await new NotebookBuilder(metadata)
- .addMarkdown(
- MarkdownUtils.header(`✅ VALIDATE CONSTRAINT: \`${constraintName}\``) +
- MarkdownUtils.infoBox('Validates a constraint that was previously created with `NOT VALID`. This scans the table to ensure all rows satisfy the constraint.') +
- MarkdownUtils.infoBox(`Table: \`${schema}.${tableName}\``)
- )
- .addSql(ConstraintSQL.validate(schema, tableName, constraintName))
- .show();
- } catch (err: any) {
- await ErrorHandlers.handleCommandError(err, 'validate constraint');
- }
+ let dbConn;
+ try {
+ dbConn = await getDatabaseConnection(treeItem);
+ const { metadata } = dbConn;
+ const schema = treeItem.schema!;
+ const tableName = treeItem.tableName!;
+ const constraintName = treeItem.label;
+
+ await new NotebookBuilder(metadata)
+ .addMarkdown(
+ MarkdownUtils.header(`✅ VALIDATE CONSTRAINT: \`${constraintName}\``) +
+ MarkdownUtils.infoBox('Validates a constraint that was previously created with `NOT VALID`. This scans the table to ensure all rows satisfy the constraint.') +
+ MarkdownUtils.infoBox(`Table: \`${schema}.${tableName}\``)
+ )
+ .addSql(ConstraintSQL.validate(schema, tableName, constraintName))
+ .show();
+ } catch (err: any) {
+ await ErrorHandlers.handleCommandError(err, 'validate constraint');
+ } finally {
+ if (dbConn && dbConn.release) dbConn.release();
+ }
}
/**
* Generate ADD CONSTRAINT template script
*/
export async function generateAddConstraintScript(treeItem: DatabaseTreeItem): Promise {
- try {
- const { connection, client, metadata } = await getDatabaseConnection(treeItem);
- const schema = treeItem.schema!;
- const tableName = treeItem.tableName!;
-
- await new NotebookBuilder(metadata)
- .addMarkdown(
- MarkdownUtils.header('➕ ADD CONSTRAINT Templates') +
- MarkdownUtils.infoBox(`Table: \`${schema}.${tableName}\``) +
- `\n\n#### Constraint Types\n\n` +
- MarkdownUtils.operationsTable([
- { operation: 'PRIMARY KEY', description: 'Uniquely identifies each row' },
- { operation: 'FOREIGN KEY', description: 'Ensures referential integrity' },
- { operation: 'UNIQUE', description: 'Ensures all values in a column are different' },
- { operation: 'CHECK', description: 'Ensures that all values in a column satisfy a specific condition' }
- ])
- )
- .addSql(`-- Add Primary Key Constraint
+ let dbConn;
+ try {
+ dbConn = await getDatabaseConnection(treeItem);
+ const { metadata } = dbConn;
+ const schema = treeItem.schema!;
+ const tableName = treeItem.tableName!;
+
+ await new NotebookBuilder(metadata)
+ .addMarkdown(
+ MarkdownUtils.header('➕ ADD CONSTRAINT Templates') +
+ MarkdownUtils.infoBox(`Table: \`${schema}.${tableName}\``) +
+ `\n\n#### Constraint Types\n\n` +
+ MarkdownUtils.operationsTable([
+ { operation: 'PRIMARY KEY', description: 'Uniquely identifies each row' },
+ { operation: 'FOREIGN KEY', description: 'Ensures referential integrity' },
+ { operation: 'UNIQUE', description: 'Ensures all values in a column are different' },
+ { operation: 'CHECK', description: 'Ensures that all values in a column satisfy a specific condition' }
+ ])
+ )
+ .addSql(`-- Add Primary Key Constraint
ALTER TABLE "${schema}"."${tableName}"
ADD CONSTRAINT "${tableName}_pkey"
PRIMARY KEY (column_name);
@@ -213,29 +231,33 @@ UNIQUE (column_name);
ALTER TABLE "${schema}"."${tableName}"
ADD CONSTRAINT "${tableName}_check"
CHECK (column_name > 0);`)
- .show();
- } catch (err: any) {
- await ErrorHandlers.handleCommandError(err, 'generate add constraint script');
- }
+ .show();
+ } catch (err: any) {
+ await ErrorHandlers.handleCommandError(err, 'generate add constraint script');
+ } finally {
+ if (dbConn && dbConn.release) dbConn.release();
+ }
}
/**
* View constraint dependencies
*/
export async function viewConstraintDependencies(treeItem: DatabaseTreeItem): Promise {
- try {
- const { connection, client, metadata } = await getDatabaseConnection(treeItem);
- const schema = treeItem.schema!;
- const tableName = treeItem.tableName!;
- const constraintName = treeItem.label;
-
- await new NotebookBuilder(metadata)
- .addMarkdown(
- MarkdownUtils.header(`🕸️ Constraint Dependencies: \`${constraintName}\``) +
- MarkdownUtils.infoBox(`Table: \`${schema}.${tableName}\``) +
- MarkdownUtils.infoBox('Shows objects that depend on this constraint.')
- )
- .addSql(`-- Find all dependencies for this constraint
+ let dbConn;
+ try {
+ dbConn = await getDatabaseConnection(treeItem);
+ const { metadata } = dbConn;
+ const schema = treeItem.schema!;
+ const tableName = treeItem.tableName!;
+ const constraintName = treeItem.label;
+
+ await new NotebookBuilder(metadata)
+ .addMarkdown(
+ MarkdownUtils.header(`🕸️ Constraint Dependencies: \`${constraintName}\``) +
+ MarkdownUtils.infoBox(`Table: \`${schema}.${tableName}\``) +
+ MarkdownUtils.infoBox('Shows objects that depend on this constraint.')
+ )
+ .addSql(`-- Find all dependencies for this constraint
SELECT
d.deptype as dependency_type,
c.relname as dependent_object,
@@ -256,95 +278,105 @@ JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE con.conname = '${constraintName}'
AND con.connamespace = (SELECT oid FROM pg_namespace WHERE nspname = '${schema}')
ORDER BY dependent_schema, dependent_object;`)
- .show();
- } catch (err: any) {
- await ErrorHandlers.handleCommandError(err, 'view constraint dependencies');
- }
+ .show();
+ } catch (err: any) {
+ await ErrorHandlers.handleCommandError(err, 'view constraint dependencies');
+ } finally {
+ if (dbConn && dbConn.release) dbConn.release();
+ }
}
export async function cmdConstraintOperations(item: DatabaseTreeItem, context: vscode.ExtensionContext) {
- try {
- const { connection, client, metadata } = await getDatabaseConnection(item);
-
- await new NotebookBuilder(metadata)
- .addMarkdown(
- MarkdownUtils.header(`🛡️ Constraint Operations: \`${item.label}\``) +
- MarkdownUtils.infoBox('This notebook provides a dashboard for managing your constraint. Each cell is a ready-to-execute template.') +
- `\n\n#### 🎯 Available Operations\n\n` +
- MarkdownUtils.operationsTable([
- { operation: '🔍 View Definition', description: 'Display constraint definition', riskLevel: '✅ Safe' },
- { operation: '✅ Validate', description: 'Check existing data against constraint', riskLevel: '✅ Safe' },
- { operation: '✏️ Rename', description: 'Change constraint name', riskLevel: '⚠️ Low Risk' },
- { operation: '❌ Drop', description: 'Remove constraint permanently', riskLevel: '🔴 Destructive' }
- ]) + `\n` +
- MarkdownUtils.successBox('Use `Ctrl+Enter` to execute individual cells.') +
- `\n---`
- )
- .addMarkdown(`##### 🔍 View Definition`)
- .addSql(ConstraintSQL.definition(item.schema!, item.tableName!, item.label))
- .addMarkdown(`#### ✅ Validate Constraint\n\n` +
- MarkdownUtils.infoBox('Validating a constraint ensures all existing rows satisfy the condition. This is useful for constraints added as NOT VALID.'))
- .addSql(`-- Validate constraint
+ let dbConn;
+ try {
+ dbConn = await getDatabaseConnection(item);
+ const { metadata } = dbConn;
+
+ await new NotebookBuilder(metadata)
+ .addMarkdown(
+ MarkdownUtils.header(`🛡️ Constraint Operations: \`${item.label}\``) +
+ MarkdownUtils.infoBox('This notebook provides a dashboard for managing your constraint. Each cell is a ready-to-execute template.') +
+ `\n\n#### 🎯 Available Operations\n\n` +
+ MarkdownUtils.operationsTable([
+ { operation: '🔍 View Definition', description: 'Display constraint definition', riskLevel: '✅ Safe' },
+ { operation: '✅ Validate', description: 'Check existing data against constraint', riskLevel: '✅ Safe' },
+ { operation: '✏️ Rename', description: 'Change constraint name', riskLevel: '⚠️ Low Risk' },
+ { operation: '❌ Drop', description: 'Remove constraint permanently', riskLevel: '🔴 Destructive' }
+ ]) + `\n` +
+ MarkdownUtils.successBox('Use `Ctrl+Enter` to execute individual cells.') +
+ `\n---`
+ )
+ .addMarkdown(`##### 🔍 View Definition`)
+ .addSql(ConstraintSQL.definition(item.schema!, item.tableName!, item.label))
+ .addMarkdown(`#### ✅ Validate Constraint\n\n` +
+ MarkdownUtils.infoBox('Validating a constraint ensures all existing rows satisfy the condition. This is useful for constraints added as NOT VALID.'))
+ .addSql(`-- Validate constraint
ALTER TABLE ${item.schema}.${item.tableName}
VALIDATE CONSTRAINT ${item.label};`)
- .addMarkdown(`#### ✏️ Rename Constraint`)
- .addSql(`-- Rename constraint
+ .addMarkdown(`#### ✏️ Rename Constraint`)
+ .addSql(`-- Rename constraint
ALTER TABLE ${item.schema}.${item.tableName}
RENAME CONSTRAINT ${item.label} TO new_constraint_name;`)
- .addMarkdown(`#### ❌ Drop Constraint\n\n` +
- MarkdownUtils.dangerBox('This will remove the constraint. Data integrity checks enforced by this constraint will no longer apply.', 'Caution'))
- .addSql(`-- Drop constraint
+ .addMarkdown(`#### ❌ Drop Constraint\n\n` +
+ MarkdownUtils.dangerBox('This will remove the constraint. Data integrity checks enforced by this constraint will no longer apply.', 'Caution'))
+ .addSql(`-- Drop constraint
ALTER TABLE ${item.schema}.${item.tableName}
DROP CONSTRAINT ${item.label};`)
- .show();
- } catch (err: any) {
- await ErrorHandlers.handleCommandError(err, 'create constraint operations notebook');
- }
+ .show();
+ } catch (err: any) {
+ await ErrorHandlers.handleCommandError(err, 'create constraint operations notebook');
+ } finally {
+ if (dbConn && dbConn.release) dbConn.release();
+ }
}
/**
* Add new constraint to table - generates a comprehensive notebook with guidelines and SQL templates
*/
export async function cmdAddConstraint(item: DatabaseTreeItem): Promise {
- try {
- const { connection, client, metadata } = await getDatabaseConnection(item);
- const schema = item.schema!;
- const tableName = item.tableName!;
-
- await new NotebookBuilder(metadata)
- .addMarkdown(
- MarkdownUtils.header(`➕ Add New Constraint to \`${schema}.${tableName}\``) +
- MarkdownUtils.infoBox('This notebook provides templates for adding constraints. Constraints enforce data integrity rules.') +
- `\n\n#### 📋 Constraint Types Overview\n\n` +
- MarkdownUtils.operationsTable([
- { operation: '🔑 PRIMARY KEY', description: 'Uniquely identifies each row. Only one per table. Cannot be NULL.' },
- { operation: '🔗 FOREIGN KEY', description: 'References primary key in another table. Enforces referential integrity.' },
- { operation: '⭐ UNIQUE', description: 'Ensures all values in column(s) are distinct. Multiple allowed per table.' },
- { operation: '✓ CHECK', description: 'Validates data against a boolean expression. Flexible validation rules.' },
- { operation: '⊗ EXCLUSION', description: 'Prevents overlapping values using operators (useful for ranges, schedules).' },
- { operation: '⊕ NOT NULL', description: 'Prevents NULL values in a column.' }
- ]) +
- `\n\n#### ⚡ Important Considerations\n\n` +
- MarkdownUtils.warningBox('Adding constraints to large tables may take time and lock the table. Consider using NOT VALID with later VALIDATE.') +
- `\n\n---`
- )
- .addMarkdown(`##### 🔑 Primary Key Constraint`)
- .addSql(ConstraintSQL.add.primaryKey(schema, tableName))
- .addMarkdown(`##### 🔗 Foreign Key Constraint`)
- .addSql(ConstraintSQL.add.foreignKey(schema, tableName))
- .addMarkdown(`##### ⭐ Unique Constraint`)
- .addSql(ConstraintSQL.add.unique(schema, tableName))
- .addMarkdown(`##### ✓ Check Constraint`)
- .addSql(ConstraintSQL.add.check(schema, tableName))
- .addMarkdown(`##### ⊗ Exclusion Constraint (for non-overlapping ranges)`)
- .addSql(ConstraintSQL.add.exclusion(schema, tableName))
- .addMarkdown(`##### ⊕ NOT NULL Constraint`)
- .addSql(ConstraintSQL.add.notNull(schema, tableName))
- .addMarkdown(`##### ⚡ Add Constraint Without Locking (Large Tables)`)
- .addSql(ConstraintSQL.add.notValid(schema, tableName))
- .addMarkdown(MarkdownUtils.successBox('After adding constraints, test with sample data to ensure they work as expected.'))
- .show();
- } catch (err: any) {
- await ErrorHandlers.handleCommandError(err, 'add constraint');
- }
+ let dbConn;
+ try {
+ dbConn = await getDatabaseConnection(item);
+ const { metadata } = dbConn;
+ const schema = item.schema!;
+ const tableName = item.tableName!;
+
+ await new NotebookBuilder(metadata)
+ .addMarkdown(
+ MarkdownUtils.header(`➕ Add New Constraint to \`${schema}.${tableName}\``) +
+ MarkdownUtils.infoBox('This notebook provides templates for adding constraints. Constraints enforce data integrity rules.') +
+ `\n\n#### 📋 Constraint Types Overview\n\n` +
+ MarkdownUtils.operationsTable([
+ { operation: '🔑 PRIMARY KEY', description: 'Uniquely identifies each row. Only one per table. Cannot be NULL.' },
+ { operation: '🔗 FOREIGN KEY', description: 'References primary key in another table. Enforces referential integrity.' },
+ { operation: '⭐ UNIQUE', description: 'Ensures all values in column(s) are distinct. Multiple allowed per table.' },
+ { operation: '✓ CHECK', description: 'Validates data against a boolean expression. Flexible validation rules.' },
+ { operation: '⊗ EXCLUSION', description: 'Prevents overlapping values using operators (useful for ranges, schedules).' },
+ { operation: '⊕ NOT NULL', description: 'Prevents NULL values in a column.' }
+ ]) +
+ `\n\n#### ⚡ Important Considerations\n\n` +
+ MarkdownUtils.warningBox('Adding constraints to large tables may take time and lock the table. Consider using NOT VALID with later VALIDATE.') +
+ `\n\n---`
+ )
+ .addMarkdown(`##### 🔑 Primary Key Constraint`)
+ .addSql(ConstraintSQL.add.primaryKey(schema, tableName))
+ .addMarkdown(`##### 🔗 Foreign Key Constraint`)
+ .addSql(ConstraintSQL.add.foreignKey(schema, tableName))
+ .addMarkdown(`##### ⭐ Unique Constraint`)
+ .addSql(ConstraintSQL.add.unique(schema, tableName))
+ .addMarkdown(`##### ✓ Check Constraint`)
+ .addSql(ConstraintSQL.add.check(schema, tableName))
+ .addMarkdown(`##### ⊗ Exclusion Constraint (for non-overlapping ranges)`)
+ .addSql(ConstraintSQL.add.exclusion(schema, tableName))
+ .addMarkdown(`##### ⊕ NOT NULL Constraint`)
+ .addSql(ConstraintSQL.add.notNull(schema, tableName))
+ .addMarkdown(`##### ⚡ Add Constraint Without Locking (Large Tables)`)
+ .addSql(ConstraintSQL.add.notValid(schema, tableName))
+ .addMarkdown(MarkdownUtils.successBox('After adding constraints, test with sample data to ensure they work as expected.'))
+ .show();
+ } catch (err: any) {
+ await ErrorHandlers.handleCommandError(err, 'add constraint');
+ } finally {
+ if (dbConn && dbConn.release) dbConn.release();
+ }
}
diff --git a/src/commands/database.ts b/src/commands/database.ts
index 2da0989..a094788 100644
--- a/src/commands/database.ts
+++ b/src/commands/database.ts
@@ -5,13 +5,13 @@ import { DashboardPanel } from '../dashboard/DashboardPanel';
import { DatabaseTreeItem, DatabaseTreeProvider } from '../providers/DatabaseTreeProvider';
import { ConnectionManager } from '../services/ConnectionManager';
import {
- MarkdownUtils,
- ErrorHandlers,
- getDatabaseConnection,
- NotebookBuilder,
- QueryBuilder,
- MaintenanceTemplates,
- validateCategoryItem
+ MarkdownUtils,
+ ErrorHandlers,
+ getDatabaseConnection,
+ NotebookBuilder,
+ QueryBuilder,
+ MaintenanceTemplates,
+ validateCategoryItem
} from './helper';
@@ -33,12 +33,15 @@ import {
* // Dashboard notebook is now displayed
*/
export async function cmdDatabaseDashboard(item: DatabaseTreeItem, context: vscode.ExtensionContext): Promise {
- try {
- const { client, connection } = await getDatabaseConnection(item, validateCategoryItem);
- await DashboardPanel.show(client, item.databaseName!, connection.id);
- } catch (err: any) {
- await ErrorHandlers.handleCommandError(err, 'show dashboard');
- }
+ try {
+ const { connection, release } = await getDatabaseConnection(item, validateCategoryItem);
+ // Release the client used for validation/setup immediately
+ release();
+
+ await DashboardPanel.show(connection, item.databaseName!, connection.id);
+ } catch (err: any) {
+ await ErrorHandlers.handleCommandError(err, 'show dashboard');
+ }
}
/**
@@ -50,66 +53,69 @@ export async function cmdDatabaseDashboard(item: DatabaseTreeItem, context: vsco
* @param {vscode.ExtensionContext} context - The extension context
*/
export async function cmdAddObjectInDatabase(item: DatabaseTreeItem, context: vscode.ExtensionContext) {
- try {
- const { connection, client, metadata } = await getDatabaseConnection(item, validateCategoryItem);
-
- const items = [
- { label: 'Schema', detail: 'Create a new schema in this database' },
- { label: 'User', detail: 'Create a new user with login privileges' },
- { label: 'Role', detail: 'Create a new role' },
- { label: 'Extension', detail: 'Enable a PostgreSQL extension' }
- ];
-
- const selection = await vscode.window.showQuickPick(items, {
- title: 'Create in Database',
- placeHolder: 'Select what to create'
- });
-
- if (selection) {
- const builder = new NotebookBuilder(metadata);
-
- if (selection.label === 'Schema') {
- builder.addMarkdown(
- MarkdownUtils.header('📂 Create New Schema') +
- MarkdownUtils.infoBox('This notebook guides you through creating a new PostgreSQL schema and configuring permissions. Schemas help organize database objects and control access.') +
- `\n\n#### 🎯 What is a Schema?\n\n` +
- `A schema is a named collection of database objects (tables, views, functions) that:\n` +
- `- 📦 Organizes objects logically\n` +
- `- 🔐 Controls access at the schema level\n` +
- `- 🏗️ Prevents naming conflicts\n` +
- `- 👥 Supports multi-tenant applications\n\n` +
- MarkdownUtils.successBox('Execute cells in order. Skip optional sections if not needed for your use case.')
- )
- .addMarkdown(`#### 1. Create Schema\nCreate a new schema with optional ownership settings.`)
- .addMarkdown(`##### 📝 Schema Definition`)
- .addSql(`-- Basic schema creation
+ try {
+ const dbConn = await getDatabaseConnection(item, validateCategoryItem);
+ const { metadata } = dbConn;
+ // Release immediately as we only needed metadata/validation
+ if (dbConn.release) dbConn.release();
+
+ const items = [
+ { label: 'Schema', detail: 'Create a new schema in this database' },
+ { label: 'User', detail: 'Create a new user with login privileges' },
+ { label: 'Role', detail: 'Create a new role' },
+ { label: 'Extension', detail: 'Enable a PostgreSQL extension' }
+ ];
+
+ const selection = await vscode.window.showQuickPick(items, {
+ title: 'Create in Database',
+ placeHolder: 'Select what to create'
+ });
+
+ if (selection) {
+ const builder = new NotebookBuilder(metadata);
+
+ if (selection.label === 'Schema') {
+ builder.addMarkdown(
+ MarkdownUtils.header('📂 Create New Schema') +
+ MarkdownUtils.infoBox('This notebook guides you through creating a new PostgreSQL schema and configuring permissions. Schemas help organize database objects and control access.') +
+ `\n\n#### 🎯 What is a Schema?\n\n` +
+ `A schema is a named collection of database objects (tables, views, functions) that:\n` +
+ `- 📦 Organizes objects logically\n` +
+ `- 🔐 Controls access at the schema level\n` +
+ `- 🏗️ Prevents naming conflicts\n` +
+ `- 👥 Supports multi-tenant applications\n\n` +
+ MarkdownUtils.successBox('Execute cells in order. Skip optional sections if not needed for your use case.')
+ )
+ .addMarkdown(`#### 1. Create Schema\nCreate a new schema with optional ownership settings.`)
+ .addMarkdown(`##### 📝 Schema Definition`)
+ .addSql(`-- Basic schema creation
CREATE SCHEMA schema_name;
-- OR create with specific owner
-- CREATE SCHEMA schema_name AUTHORIZATION role_name;`)
- .addMarkdown(`#### 2. Basic Permissions\nGrant basic usage permissions to roles that need to access the schema.`)
- .addMarkdown(`##### 🛡️ Grant Usage`)
- .addSql(`-- Grant basic schema usage
+ .addMarkdown(`#### 2. Basic Permissions\nGrant basic usage permissions to roles that need to access the schema.`)
+ .addMarkdown(`##### 🛡️ Grant Usage`)
+ .addSql(`-- Grant basic schema usage
GRANT USAGE ON SCHEMA schema_name TO role_name;`)
- .addMarkdown(`#### 3. Object Permissions\nGrant permissions for existing tables and sequences in the schema.`)
- .addMarkdown(`##### 🔐 Object Privileges`)
- .addSql(`-- Grant permissions on all tables
+ .addMarkdown(`#### 3. Object Permissions\nGrant permissions for existing tables and sequences in the schema.`)
+ .addMarkdown(`##### 🔐 Object Privileges`)
+ .addSql(`-- Grant permissions on all tables
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA schema_name TO role_name;
-- Grant permissions on all sequences
GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA schema_name TO role_name;`)
- .addMarkdown(`#### 4. Default Privileges\nSet up default privileges for objects that will be created in the future.`)
- .addMarkdown(`##### 🔮 Future Objects`)
- .addSql(`-- Set default privileges for future tables
+ .addMarkdown(`#### 4. Default Privileges\nSet up default privileges for objects that will be created in the future.`)
+ .addMarkdown(`##### 🔮 Future Objects`)
+ .addSql(`-- Set default privileges for future tables
ALTER DEFAULT PRIVILEGES IN SCHEMA schema_name
GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO role_name;
-- Set default privileges for future sequences
ALTER DEFAULT PRIVILEGES IN SCHEMA schema_name
GRANT USAGE, SELECT ON SEQUENCES TO role_name;`)
- .addMarkdown(`#### Example: Complete Schema Setup\nHere's a practical example of creating an application schema with specific privileges.`)
- .addMarkdown(`##### 🚀 Full Example`)
- .addSql(`-- Example: Create application schema with specific privileges
+ .addMarkdown(`#### Example: Complete Schema Setup\nHere's a practical example of creating an application schema with specific privileges.`)
+ .addMarkdown(`##### 🚀 Full Example`)
+ .addSql(`-- Example: Create application schema with specific privileges
CREATE SCHEMA app_schema;
-- Grant read-only access to app_readonly role
@@ -120,23 +126,23 @@ GRANT SELECT ON ALL TABLES IN SCHEMA app_schema TO app_readonly;
GRANT ALL PRIVILEGES ON SCHEMA app_schema TO app_admin;
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA app_schema TO app_admin;`);
- } else if (selection.label === 'User') {
- builder.addMarkdown(
- MarkdownUtils.header('👤 Create New Database User') +
- MarkdownUtils.infoBox('Create a new PostgreSQL user with login capabilities and configure appropriate privileges.') +
- `\n\n#### 🔑 User vs Role\n\n` +
- MarkdownUtils.propertiesTable({
- 'Login': '✅ Can login (User) vs ❌ Cannot login (Role)',
- 'Purpose': 'Individual access vs Group permissions',
- 'Password': 'Required vs Not required',
- 'Inheritance': 'Inherits from roles vs Can be granted to users'
- }) +
- `\n\n` +
- MarkdownUtils.dangerBox('Always use strong passwords and follow the principle of least privilege—grant only necessary permissions.', 'IMPORTANT')
- )
- .addMarkdown(`#### 1. Create User\nCreate a new user with basic attributes. Uncomment and modify additional attributes as needed.`)
- .addMarkdown(`##### 👤 User Definition`)
- .addSql(`-- Create new user with basic attributes
+ } else if (selection.label === 'User') {
+ builder.addMarkdown(
+ MarkdownUtils.header('👤 Create New Database User') +
+ MarkdownUtils.infoBox('Create a new PostgreSQL user with login capabilities and configure appropriate privileges.') +
+ `\n\n#### 🔑 User vs Role\n\n` +
+ MarkdownUtils.propertiesTable({
+ 'Login': '✅ Can login (User) vs ❌ Cannot login (Role)',
+ 'Purpose': 'Individual access vs Group permissions',
+ 'Password': 'Required vs Not required',
+ 'Inheritance': 'Inherits from roles vs Can be granted to users'
+ }) +
+ `\n\n` +
+ MarkdownUtils.dangerBox('Always use strong passwords and follow the principle of least privilege—grant only necessary permissions.', 'IMPORTANT')
+ )
+ .addMarkdown(`#### 1. Create User\nCreate a new user with basic attributes. Uncomment and modify additional attributes as needed.`)
+ .addMarkdown(`##### 👤 User Definition`)
+ .addSql(`-- Create new user with basic attributes
CREATE USER username WITH
LOGIN
PASSWORD 'strong_password'
@@ -148,35 +154,35 @@ CREATE USER username WITH
-- CONNECTION LIMIT 5 -- Maximum concurrent connections
-- VALID UNTIL '2025-12-31' -- Password expiration date
;`)
- .addMarkdown(`#### 2. Database Privileges\nGrant database-level privileges to the new user.`)
- .addMarkdown(`##### 🗄️ Database Access`)
- .addSql(`-- Grant database connection privileges
+ .addMarkdown(`#### 2. Database Privileges\nGrant database-level privileges to the new user.`)
+ .addMarkdown(`##### 🗄️ Database Access`)
+ .addSql(`-- Grant database connection privileges
GRANT CONNECT ON DATABASE database_name TO username;
-- Allow creating temporary tables
GRANT TEMP ON DATABASE database_name TO username;`)
- .addMarkdown(`#### 3. Schema Privileges\nGrant schema-level privileges. Repeat for each schema as needed.`)
- .addMarkdown(`##### 📂 Schema Access`)
- .addSql(`-- Grant schema privileges
+ .addMarkdown(`#### 3. Schema Privileges\nGrant schema-level privileges. Repeat for each schema as needed.`)
+ .addMarkdown(`##### 📂 Schema Access`)
+ .addSql(`-- Grant schema privileges
GRANT USAGE ON SCHEMA schema_name TO username;
-- Optional: allow creating new objects in schema
-- GRANT CREATE ON SCHEMA schema_name TO username;`)
- .addMarkdown(`#### 4. Table Privileges\nGrant table-level privileges within schemas.`)
- .addMarkdown(`##### 📊 Table Access`)
- .addSql(`-- Grant table privileges
+ .addMarkdown(`#### 4. Table Privileges\nGrant table-level privileges within schemas.`)
+ .addMarkdown(`##### 📊 Table Access`)
+ .addSql(`-- Grant table privileges
GRANT SELECT, INSERT, UPDATE, DELETE
ON ALL TABLES IN SCHEMA schema_name
TO username;`)
- .addMarkdown(`#### 5. Default Privileges\nSet up default privileges for future objects.`)
- .addMarkdown(`##### 🔮 Future Objects`)
- .addSql(`-- Set default privileges for future tables
+ .addMarkdown(`#### 5. Default Privileges\nSet up default privileges for future objects.`)
+ .addMarkdown(`##### 🔮 Future Objects`)
+ .addSql(`-- Set default privileges for future tables
ALTER DEFAULT PRIVILEGES IN SCHEMA schema_name
GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES
TO username;`)
- .addMarkdown(`#### Example: Read-only User\nHere's a practical example of creating a read-only user.`)
- .addMarkdown(`##### 👓 Read-Only Example`)
- .addSql(`-- Create read-only user
+ .addMarkdown(`#### Example: Read-only User\nHere's a practical example of creating a read-only user.`)
+ .addMarkdown(`##### 👓 Read-Only Example`)
+ .addSql(`-- Create read-only user
CREATE USER readonly_user WITH
LOGIN
PASSWORD 'strong_password'
@@ -191,23 +197,23 @@ GRANT SELECT ON ALL TABLES IN SCHEMA public TO readonly_user;
ALTER DEFAULT PRIVILEGES IN SCHEMA public
GRANT SELECT ON TABLES TO readonly_user;`);
- } else if (selection.label === 'Role') {
- builder.addMarkdown(
- MarkdownUtils.header('👥 Create New Role') +
- MarkdownUtils.infoBox('Roles are used to group permissions. Users can be added to roles to inherit their privileges.') +
- `\n\n#### 🎯 Common Role Patterns\n\n` +
- MarkdownUtils.operationsTable([
- { operation: 'Readonly', description: 'SELECT only (Reporting, analytics)' },
- { operation: 'Read-Write', description: 'SELECT, INSERT, UPDATE, DELETE (Application access)' },
- { operation: 'Admin', description: 'ALL PRIVILEGES (Database administration)' },
- { operation: 'App Role', description: 'Custom based on needs (Service accounts)' }
- ]) +
- `\n\n` +
- MarkdownUtils.successBox('Tip: Create roles for job functions, not individuals. Grant roles to users for easier permission management.')
- )
- .addMarkdown(`#### 1. Create Role\nCreate a new role with basic attributes. Uncomment and modify additional attributes as needed.`)
- .addMarkdown(`##### 🎭 Role Definition`)
- .addSql(`-- Create new role with basic attributes
+ } else if (selection.label === 'Role') {
+ builder.addMarkdown(
+ MarkdownUtils.header('👥 Create New Role') +
+ MarkdownUtils.infoBox('Roles are used to group permissions. Users can be added to roles to inherit their privileges.') +
+ `\n\n#### 🎯 Common Role Patterns\n\n` +
+ MarkdownUtils.operationsTable([
+ { operation: 'Readonly', description: 'SELECT only (Reporting, analytics)' },
+ { operation: 'Read-Write', description: 'SELECT, INSERT, UPDATE, DELETE (Application access)' },
+ { operation: 'Admin', description: 'ALL PRIVILEGES (Database administration)' },
+ { operation: 'App Role', description: 'Custom based on needs (Service accounts)' }
+ ]) +
+ `\n\n` +
+ MarkdownUtils.successBox('Tip: Create roles for job functions, not individuals. Grant roles to users for easier permission management.')
+ )
+ .addMarkdown(`#### 1. Create Role\nCreate a new role with basic attributes. Uncomment and modify additional attributes as needed.`)
+ .addMarkdown(`##### 🎭 Role Definition`)
+ .addSql(`-- Create new role with basic attributes
CREATE ROLE role_name WITH
NOLOGIN -- Cannot login (use LOGIN for login capability)
INHERIT -- Inherit privileges from parent roles
@@ -219,19 +225,19 @@ CREATE ROLE role_name WITH
-- CONNECTION LIMIT 5 -- Maximum concurrent connections
-- IN GROUP role1, role2 -- Add role to existing groups
;`)
- .addMarkdown(`#### 2. Database Privileges\nGrant database-level privileges to the role.`)
- .addMarkdown(`##### 🗄️ Database Access`)
- .addSql(`-- Grant database privileges
+ .addMarkdown(`#### 2. Database Privileges\nGrant database-level privileges to the role.`)
+ .addMarkdown(`##### 🗄️ Database Access`)
+ .addSql(`-- Grant database privileges
GRANT CONNECT ON DATABASE database_name TO role_name;
GRANT CREATE ON DATABASE database_name TO role_name;`)
- .addMarkdown(`#### 3. Schema Privileges\nGrant schema-level privileges to the role.`)
- .addMarkdown(`##### 📂 Schema Access`)
- .addSql(`-- Grant schema privileges
+ .addMarkdown(`#### 3. Schema Privileges\nGrant schema-level privileges to the role.`)
+ .addMarkdown(`##### 📂 Schema Access`)
+ .addSql(`-- Grant schema privileges
GRANT USAGE ON SCHEMA schema_name TO role_name;
GRANT CREATE ON SCHEMA schema_name TO role_name;`)
- .addMarkdown(`#### 4. Object Privileges\nGrant privileges on tables, functions, and sequences.`)
- .addMarkdown(`##### 🔐 Object Access`)
- .addSql(`-- Grant table privileges
+ .addMarkdown(`#### 4. Object Privileges\nGrant privileges on tables, functions, and sequences.`)
+ .addMarkdown(`##### 🔐 Object Access`)
+ .addSql(`-- Grant table privileges
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA schema_name TO role_name;
-- Grant function privileges
@@ -239,9 +245,9 @@ GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA schema_name TO role_name;
-- Grant sequence privileges
GRANT USAGE ON ALL SEQUENCES IN SCHEMA schema_name TO role_name;`)
- .addMarkdown(`#### 5. Default Privileges\nSet up default privileges for future objects.`)
- .addMarkdown(`##### 🔮 Future Objects`)
- .addSql(`-- Set default privileges for future objects
+ .addMarkdown(`#### 5. Default Privileges\nSet up default privileges for future objects.`)
+ .addMarkdown(`##### 🔮 Future Objects`)
+ .addSql(`-- Set default privileges for future objects
ALTER DEFAULT PRIVILEGES IN SCHEMA schema_name
GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO role_name;
@@ -250,9 +256,9 @@ ALTER DEFAULT PRIVILEGES IN SCHEMA schema_name
ALTER DEFAULT PRIVILEGES IN SCHEMA schema_name
GRANT USAGE ON SEQUENCES TO role_name;`)
- .addMarkdown(`#### Example: Application Role\nHere's a practical example of creating an application role with read-only access.`)
- .addMarkdown(`##### 🚀 App Role Example`)
- .addSql(`-- Create application role
+ .addMarkdown(`#### Example: Application Role\nHere's a practical example of creating an application role with read-only access.`)
+ .addMarkdown(`##### 🚀 App Role Example`)
+ .addSql(`-- Create application role
CREATE ROLE app_readonly WITH
NOLOGIN
INHERIT
@@ -267,25 +273,25 @@ GRANT SELECT ON ALL TABLES IN SCHEMA public TO app_readonly;
ALTER DEFAULT PRIVILEGES IN SCHEMA public
GRANT SELECT ON TABLES TO app_readonly;`);
- } else if (selection.label === 'Extension') {
- builder.addMarkdown(
- MarkdownUtils.header('🧩 Enable PostgreSQL Extension') +
- MarkdownUtils.infoBox('PostgreSQL extensions add functionality to your database. Enable only the extensions you need.') +
- `\n\n#### 📦 Popular Extensions\n\n` +
- MarkdownUtils.operationsTable([
- { operation: 'uuid-ossp', description: 'UUID generation (Unique identifiers)' },
- { operation: 'pgcrypto', description: 'Cryptographic functions (Encryption, hashing)' },
- { operation: 'hstore', description: 'Key-value storage (Semi-structured data)' },
- { operation: 'postgis', description: 'Geospatial data (Maps, location data)' },
- { operation: 'pg_stat_statements', description: 'Query statistics (Performance monitoring)' },
- { operation: 'pg_trgm', description: 'Fuzzy text search (Similarity matching)' }
- ]) +
- `\n\n` +
- MarkdownUtils.successBox('Tip: Check if the extension is already installed before enabling it to avoid errors.')
- )
- .addMarkdown(`#### 1. View Available Extensions\nList extensions that can be installed but aren't yet enabled.`)
- .addMarkdown(`##### 🔍 Available Extensions`)
- .addSql(`-- List available extensions
+ } else if (selection.label === 'Extension') {
+ builder.addMarkdown(
+ MarkdownUtils.header('🧩 Enable PostgreSQL Extension') +
+ MarkdownUtils.infoBox('PostgreSQL extensions add functionality to your database. Enable only the extensions you need.') +
+ `\n\n#### 📦 Popular Extensions\n\n` +
+ MarkdownUtils.operationsTable([
+ { operation: 'uuid-ossp', description: 'UUID generation (Unique identifiers)' },
+ { operation: 'pgcrypto', description: 'Cryptographic functions (Encryption, hashing)' },
+ { operation: 'hstore', description: 'Key-value storage (Semi-structured data)' },
+ { operation: 'postgis', description: 'Geospatial data (Maps, location data)' },
+ { operation: 'pg_stat_statements', description: 'Query statistics (Performance monitoring)' },
+ { operation: 'pg_trgm', description: 'Fuzzy text search (Similarity matching)' }
+ ]) +
+ `\n\n` +
+ MarkdownUtils.successBox('Tip: Check if the extension is already installed before enabling it to avoid errors.')
+ )
+ .addMarkdown(`#### 1. View Available Extensions\nList extensions that can be installed but aren't yet enabled.`)
+ .addMarkdown(`##### 🔍 Available Extensions`)
+ .addSql(`-- List available extensions
SELECT name as "Name",
default_version as "Version",
installed_version as "Installed",
@@ -293,9 +299,9 @@ SELECT name as "Name",
FROM pg_available_extensions
WHERE installed_version IS NULL
ORDER BY name;`)
- .addMarkdown(`#### 2. Enable Extension\nEnable a specific extension. Uncomment the extension you want to enable.`)
- .addMarkdown(`##### 🔌 Enable Command`)
- .addSql(`-- Enable your chosen extension
+ .addMarkdown(`#### 2. Enable Extension\nEnable a specific extension. Uncomment the extension you want to enable.`)
+ .addMarkdown(`##### 🔌 Enable Command`)
+ .addSql(`-- Enable your chosen extension
CREATE EXTENSION IF NOT EXISTS extension_name;
-- Common extensions (uncomment to use):
@@ -309,25 +315,25 @@ CREATE EXTENSION IF NOT EXISTS extension_name;
-- CREATE EXTENSION IF NOT EXISTS "ltree"; -- Hierarchical tree structures
-- CREATE EXTENSION IF NOT EXISTS "isn"; -- Product number standards
-- CREATE EXTENSION IF NOT EXISTS "citext"; -- Case-insensitive text`)
- .addMarkdown(`#### 3. Verify Installation\nCheck if the extension was successfully installed.`)
- .addMarkdown(`##### ✅ Verification`)
- .addSql(`-- Verify extension installation
+ .addMarkdown(`#### 3. Verify Installation\nCheck if the extension was successfully installed.`)
+ .addMarkdown(`##### ✅ Verification`)
+ .addSql(`-- Verify extension installation
SELECT * FROM pg_extension WHERE extname = 'extension_name';`)
- .addMarkdown(`#### 4. List Installed Extensions\nView all currently installed extensions in the database.`)
- .addMarkdown(`##### 📋 Installed Extensions`)
- .addSql(`-- List all installed extensions
+ .addMarkdown(`#### 4. List Installed Extensions\nView all currently installed extensions in the database.`)
+ .addMarkdown(`##### 📋 Installed Extensions`)
+ .addSql(`-- List all installed extensions
SELECT extname as "Name",
extversion as "Version",
obj_description(oid, 'pg_extension') as "Description"
FROM pg_extension
ORDER BY extname;`);
- }
+ }
- await builder.show();
- }
- } catch (err: any) {
- await ErrorHandlers.handleCommandError(err, 'create notebook');
+ await builder.show();
}
+ } catch (err: any) {
+ await ErrorHandlers.handleCommandError(err, 'create notebook');
+ }
}
/**
@@ -337,71 +343,73 @@ ORDER BY extname;`);
* @param {vscode.ExtensionContext} context - The extension context
*/
export async function cmdDatabaseOperations(item: DatabaseTreeItem, context: vscode.ExtensionContext) {
- try {
- const { connection, client, metadata } = await getDatabaseConnection(item, validateCategoryItem);
-
- // Get database info
- const dbInfo = await client.query(QueryBuilder.databaseStats());
- const info = dbInfo.rows[0];
-
- // Get schema sizes for visualization
- const schemaSizes = await client.query(QueryBuilder.databaseSchemaSizes());
-
- // Process schema sizes for visualization
- const schemaMap = new Map();
- let totalSize = 0;
- schemaSizes.rows.forEach(row => {
- const size = Number(row.table_size);
- const current = schemaMap.get(row.schema_name) || 0;
- schemaMap.set(row.schema_name, current + size);
- totalSize += size;
- });
-
- // Generate ASCII Bar Chart in HTML Table
- let schemaDistribution = '| Schema | Size | Distribution |
';
- schemaMap.forEach((size, schema) => {
- if (size > 0) {
- const percentage = (size / totalSize) * 100;
- const barLength = Math.floor(percentage / 5); // 20 chars max
- const bar = '█'.repeat(barLength) + '░'.repeat(20 - barLength);
- schemaDistribution += `| ${schema} | ${(size / 1024 / 1024).toFixed(2)} MB | ${bar} ${percentage.toFixed(1)}% |
`;
- }
- });
- schemaDistribution += '
';
-
- await new NotebookBuilder(metadata)
- .addMarkdown(
- MarkdownUtils.header(`📊 Database Operations: \`${item.label}\``) +
- MarkdownUtils.infoBox('Comprehensive database management notebook with statistics, monitoring queries, and administrative operations.') +
- `\n\n#### 📈 Database Overview\n\n` +
- `##### 📊 Schema Size Distribution\n${schemaDistribution}\n\n` +
- `##### 🏥 Health Metrics\n` +
- MarkdownUtils.propertiesTable({
- '🗄️ Database Size': `${info.Size}`,
- '👤 Owner': `${info.Owner}`,
- '🔗 Active Connections': `${info["Active Connections"]}`,
- '📂 Schemas': `${info.Schemas}`,
- '📊 Tables': `${info.Tables}`,
- '👥 Roles': `${info.Roles}`
- }) +
- `\n\n#### 🎯 Available Operations\n\n` +
- `Execute the cells below to:\n` +
- `- 📦 **View schema sizes** - Analyze storage by schema\n` +
- `- 👥 **List users/roles** - Review permissions and access\n` +
- `- 🔍 **Monitor connections** - Track active sessions\n` +
- `- 🧩 **Check extensions** - See installed features\n\n` +
- MarkdownUtils.successBox('Use these queries for monitoring, reporting, and database administration. Modify as needed for your specific use case.') +
- `\n\n---`
- )
- .addMarkdown(`##### 📦 Schema Sizes`)
- .addSql(`-- List schemas and sizes\n` + QueryBuilder.databaseSchemaSizeSummary())
- .addMarkdown(`##### 👥 User Roles & Privileges`)
- .addSql(`-- List users and roles\n` + QueryBuilder.databaseRoles())
- .addMarkdown(`##### 🔗 Active Connections`)
- .addSql(`-- Show active connections\n` + QueryBuilder.databaseActiveConnections())
- .addMarkdown(`##### 🧩 Installed Extensions`)
- .addSql(`-- List installed extensions\n` + QueryBuilder.databaseExtensions())
- .addSql(`-- Database maintenance
+ let dbConn;
+ try {
+ dbConn = await getDatabaseConnection(item, validateCategoryItem);
+ const { client, metadata } = dbConn;
+
+ // Get database info
+ const dbInfo = await client.query(QueryBuilder.databaseStats());
+ const info = dbInfo.rows[0];
+
+ // Get schema sizes for visualization
+ const schemaSizes = await client.query(QueryBuilder.databaseSchemaSizes());
+
+ // Process schema sizes for visualization
+ const schemaMap = new Map();
+ let totalSize = 0;
+ schemaSizes.rows.forEach(row => {
+ const size = Number(row.table_size);
+ const current = schemaMap.get(row.schema_name) || 0;
+ schemaMap.set(row.schema_name, current + size);
+ totalSize += size;
+ });
+
+ // Generate ASCII Bar Chart in HTML Table
+ let schemaDistribution = '| Schema | Size | Distribution |
';
+ schemaMap.forEach((size, schema) => {
+ if (size > 0) {
+ const percentage = (size / totalSize) * 100;
+ const barLength = Math.floor(percentage / 5); // 20 chars max
+ const bar = '█'.repeat(barLength) + '░'.repeat(20 - barLength);
+ schemaDistribution += `| ${schema} | ${(size / 1024 / 1024).toFixed(2)} MB | ${bar} ${percentage.toFixed(1)}% |
`;
+ }
+ });
+ schemaDistribution += '
';
+
+ await new NotebookBuilder(metadata)
+ .addMarkdown(
+ MarkdownUtils.header(`📊 Database Operations: \`${item.label}\``) +
+ MarkdownUtils.infoBox('Comprehensive database management notebook with statistics, monitoring queries, and administrative operations.') +
+ `\n\n#### 📈 Database Overview\n\n` +
+ `##### 📊 Schema Size Distribution\n${schemaDistribution}\n\n` +
+ `##### 🏥 Health Metrics\n` +
+ MarkdownUtils.propertiesTable({
+ '🗄️ Database Size': `${info.Size}`,
+ '👤 Owner': `${info.Owner}`,
+ '🔗 Active Connections': `${info["Active Connections"]}`,
+ '📂 Schemas': `${info.Schemas}`,
+ '📊 Tables': `${info.Tables}`,
+ '👥 Roles': `${info.Roles}`
+ }) +
+ `\n\n#### 🎯 Available Operations\n\n` +
+ `Execute the cells below to:\n` +
+ `- 📦 **View schema sizes** - Analyze storage by schema\n` +
+ `- 👥 **List users/roles** - Review permissions and access\n` +
+ `- 🔍 **Monitor connections** - Track active sessions\n` +
+ `- 🧩 **Check extensions** - See installed features\n\n` +
+ MarkdownUtils.successBox('Use these queries for monitoring, reporting, and database administration. Modify as needed for your specific use case.') +
+ `\n\n---`
+ )
+ .addMarkdown(`##### 📦 Schema Sizes`)
+ .addSql(`-- List schemas and sizes\n` + QueryBuilder.databaseSchemaSizeSummary())
+ .addMarkdown(`##### 👥 User Roles & Privileges`)
+ .addSql(`-- List users and roles\n` + QueryBuilder.databaseRoles())
+ .addMarkdown(`##### 🔗 Active Connections`)
+ .addSql(`-- Show active connections\n` + QueryBuilder.databaseActiveConnections())
+ .addMarkdown(`##### 🧩 Installed Extensions`)
+ .addSql(`-- List installed extensions\n` + QueryBuilder.databaseExtensions())
+ .addSql(`-- Database maintenance
-- Note: These operations require appropriate privileges
-- Analyze all tables (updates statistics)
@@ -412,55 +420,58 @@ ${QueryBuilder.databaseMaintenanceStats()}
-- To vacuum a specific table (uncomment and modify):
-- VACUUM ANALYZE schema_name.table_name;`)
- .addSql(`-- Terminate connections (BE CAREFUL!)
+ .addSql(`-- Terminate connections (BE CAREFUL!)
-- List commands to terminate other connections to this database
${QueryBuilder.databaseTerminateConnections()}`)
- .show();
+ .show();
- } catch (err: any) {
- await ErrorHandlers.handleCommandError(err, 'create database operations notebook');
- }
+ } catch (err: any) {
+ await ErrorHandlers.handleCommandError(err, 'create database operations notebook');
+ } finally {
+ if (dbConn && dbConn.release) dbConn.release();
+ }
}
/**
* cmdRefreshDatabase - Refreshes the database item in the tree view.
*/
export async function cmdRefreshDatabase(item: DatabaseTreeItem, context: vscode.ExtensionContext, databaseTreeProvider?: DatabaseTreeProvider) {
- databaseTreeProvider?.refresh(item);
+ databaseTreeProvider?.refresh(item);
}
/**
* cmdCreateDatabase - Command to create a new database.
*/
export async function cmdCreateDatabase(item: DatabaseTreeItem, context: vscode.ExtensionContext) {
- try {
- // For creating a database, we connect to postgres database
- const connectionConfig = await getConnectionWithPassword(item.connectionId!);
- const connection = await ConnectionManager.getInstance().getConnection({
- id: connectionConfig.id,
- host: connectionConfig.host,
- port: connectionConfig.port,
- username: connectionConfig.username,
- database: 'postgres',
- name: connectionConfig.name
- });
- const metadata = createMetadata(connectionConfig, 'postgres');
-
- await new NotebookBuilder(metadata)
- .addMarkdown(
- MarkdownUtils.header('🆕 Create New Database') +
- MarkdownUtils.infoBox('Execute the cell below to create a new database. Modify the database name and options as needed.') +
- `\n\n#### 🎯 Database Options\n\n` +
- MarkdownUtils.operationsTable([
- { operation: 'OWNER', description: 'Specify the database owner' },
- { operation: 'ENCODING', description: 'Character encoding (e.g., UTF8)' },
- { operation: 'TEMPLATE', description: 'Template database to copy from' },
- { operation: 'TABLESPACE', description: 'Default tablespace for the database' },
- { operation: 'CONNECTION LIMIT', description: 'Maximum concurrent connections' }
- ])
- )
- .addMarkdown(`##### 📝 Create Command`)
- .addSql(`-- Create database with basic settings
+ let client;
+ try {
+ // For creating a database, we connect to postgres database
+ const connectionConfig = await getConnectionWithPassword(item.connectionId!);
+ client = await ConnectionManager.getInstance().getPooledClient({
+ id: connectionConfig.id,
+ host: connectionConfig.host,
+ port: connectionConfig.port,
+ username: connectionConfig.username,
+ database: 'postgres',
+ name: connectionConfig.name
+ });
+ const metadata = createMetadata(connectionConfig, 'postgres');
+
+ await new NotebookBuilder(metadata)
+ .addMarkdown(
+ MarkdownUtils.header('🆕 Create New Database') +
+ MarkdownUtils.infoBox('Execute the cell below to create a new database. Modify the database name and options as needed.') +
+ `\n\n#### 🎯 Database Options\n\n` +
+ MarkdownUtils.operationsTable([
+ { operation: 'OWNER', description: 'Specify the database owner' },
+ { operation: 'ENCODING', description: 'Character encoding (e.g., UTF8)' },
+ { operation: 'TEMPLATE', description: 'Template database to copy from' },
+ { operation: 'TABLESPACE', description: 'Default tablespace for the database' },
+ { operation: 'CONNECTION LIMIT', description: 'Maximum concurrent connections' }
+ ])
+ )
+ .addMarkdown(`##### 📝 Create Command`)
+ .addSql(`-- Create database with basic settings
CREATE DATABASE new_database;
-- Create database with full options
@@ -471,74 +482,79 @@ CREATE DATABASE new_database;
-- LC_CTYPE = 'en_US.UTF-8'
-- TEMPLATE = template0
-- CONNECTION LIMIT = -1;`)
- .show();
- } catch (err: any) {
- await ErrorHandlers.handleCommandError(err, 'create database notebook');
- }
+ .show();
+ } catch (err: any) {
+ await ErrorHandlers.handleCommandError(err, 'create database notebook');
+ } finally {
+ if (client) (client as any).release();
+ }
}
/**
* cmdDeleteDatabase - Command to delete a database.
*/
export async function cmdDeleteDatabase(item: DatabaseTreeItem, context: vscode.ExtensionContext) {
- try {
- // For deleting a database, we connect to postgres database
- const connectionConfig = await getConnectionWithPassword(item.connectionId!);
- const connection = await ConnectionManager.getInstance().getConnection({
- id: connectionConfig.id,
- host: connectionConfig.host,
- port: connectionConfig.port,
- username: connectionConfig.username,
- database: 'postgres',
- name: connectionConfig.name
- });
- const metadata = createMetadata(connectionConfig, 'postgres');
-
- await new NotebookBuilder(metadata)
- .addMarkdown(
- MarkdownUtils.header(`❌ Delete Database: \`${item.label}\``) +
- MarkdownUtils.dangerBox('This action will PERMANENTLY DELETE the database and ALL DATA. This cannot be undone!', 'DANGER') +
- `\n\n#### ⚠️ Before You Drop\n\n` +
- `1. **Backup your data** - Create a full backup using pg_dump\n` +
- `2. **Verify connections** - Ensure no active connections to the database\n` +
- `3. **Test on non-production** - Verify the operation is intended\n\n` +
- MarkdownUtils.warningBox('You cannot drop a database while connected to it. This notebook connects to the postgres database to execute the DROP command.')
- )
- .addMarkdown(`##### 🚨 Terminate Active Connections (if needed)`)
- .addSql(`-- Terminate all connections to the database (run this first if needed)
+ let client;
+ try {
+ // For deleting a database, we connect to postgres database
+ const connectionConfig = await getConnectionWithPassword(item.connectionId!);
+ client = await ConnectionManager.getInstance().getPooledClient({
+ id: connectionConfig.id,
+ host: connectionConfig.host,
+ port: connectionConfig.port,
+ username: connectionConfig.username,
+ database: 'postgres',
+ name: connectionConfig.name
+ });
+ const metadata = createMetadata(connectionConfig, 'postgres');
+
+ await new NotebookBuilder(metadata)
+ .addMarkdown(
+ MarkdownUtils.header(`❌ Delete Database: \`${item.label}\``) +
+ MarkdownUtils.dangerBox('This action will PERMANENTLY DELETE the database and ALL DATA. This cannot be undone!', 'DANGER') +
+ `\n\n#### ⚠️ Before You Drop\n\n` +
+ `1. **Backup your data** - Create a full backup using pg_dump\n` +
+ `2. **Verify connections** - Ensure no active connections to the database\n` +
+ `3. **Test on non-production** - Verify the operation is intended\n\n` +
+ MarkdownUtils.warningBox('You cannot drop a database while connected to it. This notebook connects to the postgres database to execute the DROP command.')
+ )
+ .addMarkdown(`##### 🚨 Terminate Active Connections (if needed)`)
+ .addSql(`-- Terminate all connections to the database (run this first if needed)
${QueryBuilder.terminateConnectionsByPid(item.label)}`)
- .addMarkdown(`##### ❌ Drop Command`)
- .addSql(`-- Drop database (use with extreme caution!)
+ .addMarkdown(`##### ❌ Drop Command`)
+ .addSql(`-- Drop database (use with extreme caution!)
DROP DATABASE IF EXISTS "${item.label}";`)
- .show();
- } catch (err: any) {
- await ErrorHandlers.handleCommandError(err, 'create delete database notebook');
- }
+ .show();
+ } catch (err: any) {
+ await ErrorHandlers.handleCommandError(err, 'create delete database notebook');
+ } finally {
+ if (client) (client as any).release();
+ }
}
export async function cmdBackupDatabase(item: DatabaseTreeItem, context: vscode.ExtensionContext) {
- try {
- const connectionConfig = await getConnectionWithPassword(item.connectionId!);
-
- // 1. Prompt for save location
- const uri = await vscode.window.showSaveDialog({
- defaultUri: vscode.Uri.file(`${item.label}_backup.dump`),
- filters: { 'PostgreSQL Dump': ['dump', 'sql', 'tar'] },
- title: 'Select Backup Location'
- });
-
- if (!uri) {
- return; // User cancelled
- }
+ try {
+ const connectionConfig = await getConnectionWithPassword(item.connectionId!);
+
+ // 1. Prompt for save location
+ const uri = await vscode.window.showSaveDialog({
+ defaultUri: vscode.Uri.file(`${item.label}_backup.dump`),
+ filters: { 'PostgreSQL Dump': ['dump', 'sql', 'tar'] },
+ title: 'Select Backup Location'
+ });
+
+ if (!uri) {
+ return; // User cancelled
+ }
- const filePath = uri.fsPath;
+ const filePath = uri.fsPath;
- // 2. Construct pg_dump command
- // Use quotes for paths to handle spaces
- const command = `pg_dump -h ${connectionConfig.host} -p ${connectionConfig.port} -U ${connectionConfig.username} -F c -b -v -f "${filePath}" "${item.label}"`;
+ // 2. Construct pg_dump command
+ // Use quotes for paths to handle spaces
+ const command = `pg_dump -h ${connectionConfig.host} -p ${connectionConfig.port} -U ${connectionConfig.username} -F c -b -v -f "${filePath}" "${item.label}"`;
- // 3. Create Help HTML
- const htmlContent = `
+ // 3. Create Help HTML
+ const htmlContent = `
📦 Database Backup Guide
You are about to backup database: ${item.label}
@@ -568,44 +584,44 @@ export async function cmdBackupDatabase(item: DatabaseTreeItem, context: vscode.
`;
- // 4. Show Help Webview
- createHelpPanel(context, 'Backup Guide', htmlContent);
+ // 4. Show Help Webview
+ createHelpPanel(context, 'Backup Guide', htmlContent);
- // 5. Open Terminal and Send Command
- const terminal = vscode.window.createTerminal(`PG Backup: ${item.label}`);
- terminal.show(true); // Preserve focus on editor if possible, but usually terminal takes focus
- terminal.sendText(command, false); // false = do not execute immediately
+ // 5. Open Terminal and Send Command
+ const terminal = vscode.window.createTerminal(`PG Backup: ${item.label}`);
+ terminal.show(true); // Preserve focus on editor if possible, but usually terminal takes focus
+ terminal.sendText(command, false); // false = do not execute immediately
- } catch (err: any) {
- await ErrorHandlers.handleCommandError(err, 'initiate backup');
- }
+ } catch (err: any) {
+ await ErrorHandlers.handleCommandError(err, 'initiate backup');
+ }
}
export async function cmdRestoreDatabase(item: DatabaseTreeItem, context: vscode.ExtensionContext) {
- try {
- const connectionConfig = await getConnectionWithPassword(item.connectionId!);
-
- // 1. Prompt for source file
- const uris = await vscode.window.showOpenDialog({
- canSelectFiles: true,
- canSelectFolders: false,
- canSelectMany: false,
- filters: { 'PostgreSQL Dump': ['dump', 'sql', 'tar', 'backup'] },
- title: 'Select Backup File to Restore'
- });
-
- if (!uris || uris.length === 0) {
- return; // User cancelled
- }
+ try {
+ const connectionConfig = await getConnectionWithPassword(item.connectionId!);
+
+ // 1. Prompt for source file
+ const uris = await vscode.window.showOpenDialog({
+ canSelectFiles: true,
+ canSelectFolders: false,
+ canSelectMany: false,
+ filters: { 'PostgreSQL Dump': ['dump', 'sql', 'tar', 'backup'] },
+ title: 'Select Backup File to Restore'
+ });
+
+ if (!uris || uris.length === 0) {
+ return; // User cancelled
+ }
- const filePath = uris[0].fsPath;
+ const filePath = uris[0].fsPath;
- // 2. Construct pg_restore command
- // Note: -d is target database
- const command = `pg_restore -h ${connectionConfig.host} -p ${connectionConfig.port} -U ${connectionConfig.username} -d "${item.label}" -v "${filePath}"`;
+ // 2. Construct pg_restore command
+ // Note: -d is target database
+ const command = `pg_restore -h ${connectionConfig.host} -p ${connectionConfig.port} -U ${connectionConfig.username} -d "${item.label}" -v "${filePath}"`;
- // 3. Create Help HTML
- const htmlContent = `
+ // 3. Create Help HTML
+ const htmlContent = `