Skip to content

Commit 3142af1

Browse files
committed
feat: schema navigation PG, richer LLM context, chat tab (v0.7.0)
1 parent 38b1046 commit 3142af1

23 files changed

Lines changed: 1014 additions & 67 deletions

File tree

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,12 @@ Studio**, **DbGate**, **TablePlus** y **Outerbase Studio**.
2121
> respaldado por Outerbase**. Se distribuye bajo **AGPL-3.0** conservando la licencia y la
2222
> atribución originales (ver [Licencia](#licencia) y [`AVISO_LICENCIA.md`](./AVISO_LICENCIA.md)).
2323
24+
## Novedades (v0.7.0)
25+
26+
- **Navegación de schemas en Postgres:** el sidebar lista **todos** los schemas (no solo `public`); navegá y abrí tablas de cualquiera, con buscador `schema.tabla` y badge de schema activo que se mueve al elegirlo.
27+
- **Asistente IA (Ctrl/Cmd+B) con más contexto:** el DDL que ve el modelo ahora incluye `NOT NULL`, defaults, `UNIQUE`, comentarios de tabla/columna, índices y foreign keys.
28+
- **Tab de Chat conversacional:** charlá con el asistente para generar SQL a partir de tu schema (multi-turno; **genera, no ejecuta**), con botones "Abrir en editor" y "Copiar". Usa tu provider/API key configurada (Anthropic/OpenAI/Gemini).
29+
2430
## ¿Por qué quick-outerbase? (comparación honesta)
2531

2632
| | quick-outerbase | Prisma Studio | Drizzle Studio | DbGate | TablePlus | Outerbase Studio (oficial) |

launcher/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "quick-outerbase",
3-
"version": "0.6.1",
3+
"version": "0.7.0",
44
"description": "Database GUI for your terminal: pass a DATABASE_URL and `npx quick-outerbase` opens a local web UI to browse, query and edit Postgres, MySQL, SQLite and libSQL/Turso. A fast, open-source Prisma Studio / Drizzle Studio / TablePlus / DbGate alternative. Community fork of Outerbase Studio (AGPL-3.0).",
55
"license": "AGPL-3.0-only",
66
"bin": {

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "quick-outerbase",
3-
"version": "0.6.1",
3+
"version": "0.7.0",
44
"private": true,
55
"description": "Database GUI in your terminal: pass a DATABASE_URL and `npx quick-outerbase` opens a local web UI to browse, query and edit Postgres, MySQL, SQLite and libSQL/Turso. Open-source Prisma Studio / Drizzle Studio / TablePlus / DbGate alternative. Unofficial community fork of Outerbase Studio (AGPL-3.0).",
66
"license": "AGPL-3.0-only",

src/app/(theme)/env/page-client.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ interface EnvDbInfo {
1717
engine: string;
1818
dialect: SupportedDialect;
1919
name: string;
20-
schema: string;
20+
/** Schema inicial de Postgres (?schema=). undefined cuando no se pasó. */
21+
schema?: string;
2122
/** Solo dynamodb: región/endpoint (sin secretos). */
2223
region?: string;
2324
endpoint?: string;
@@ -39,8 +40,9 @@ export default function EnvPageBody() {
3940

4041
const driver = useMemo(() => {
4142
if (!info) return null;
42-
// Pasamos el schema configurado (?schema=) para que Postgres filtre la
43-
// introspección a ese schema. En DBs sin schema, info.schema viene vacío.
43+
// Pasamos el schema configurado (?schema=) como schema INICIAL seleccionado
44+
// de Postgres (NO filtra la introspección: se listan todos los schemas).
45+
// En DBs sin schema, info.schema viene undefined/vacío.
4446
// Para dynamodb pasamos región/endpoint (sin creds: las pone el server).
4547
return createEnvDriver(info.dialect, info.schema, {
4648
region: info.region,

src/app/proxy/db/route.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,11 +41,25 @@ async function buildExecutor(url: string): Promise<Executor> {
4141
if (parsed.engine === "postgres") {
4242
const pg = await import("pg");
4343
setPgParser(pg.types);
44+
// search_path estilo Prisma ?schema=: solo lo seteamos cuando vino un schema
45+
// no vacío. Sin ?schema (undefined/"") dejamos el search_path default de
46+
// Postgres — nunca emitimos `-c search_path=` vacío.
47+
const pgSchema = parsed.schema?.trim();
48+
// Solo seteamos `-c search_path=` si el schema es un identificador simple y
49+
// seguro. Interpolar un valor con espacios/tokens en las libpq `options`
50+
// rompe la conexión (Postgres las parsea space-delimited) o inyectaría GUCs
51+
// extra. Si no matchea, lo omitimos: la introspección lista todos los schemas
52+
// igual y selectTable califica el schema explícitamente, así que no se pierde
53+
// funcionalidad. (parsed.schema viene del DATABASE_URL server-side, no del
54+
// request; esto es defensa en profundidad para schemas con nombres raros.)
55+
const safeSchema =
56+
pgSchema && /^[A-Za-z_][A-Za-z0-9_$]*$/.test(pgSchema)
57+
? pgSchema
58+
: undefined;
4459
const pool = new pg.Pool({
4560
connectionString: parsed.connectionString,
4661
max: 5,
47-
// search_path estilo Prisma ?schema=: aplica a todas las conexiones del pool.
48-
options: parsed.schema ? `-c search_path=${parsed.schema}` : undefined,
62+
options: safeSchema ? `-c search_path=${safeSchema}` : undefined,
4963
});
5064
return {
5165
async exec(stmts) {

src/components/gui/database-gui.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,12 @@ export default function DatabaseGui() {
171171
},
172172
}
173173
: undefined,
174+
{
175+
text: "Chat",
176+
onClick: () => {
177+
scc.tabs.openBuiltinChat({});
178+
},
179+
},
174180
].filter(Boolean) as { text: string; onClick: () => void }[];
175181
}, [currentSchemaName, databaseDriver]);
176182

src/components/gui/schema-sidebar-list.tsx

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,6 @@ function prepareListViewItem(
4040
let icon = Table;
4141
let iconClassName = "";
4242

43-
console.log("ss", s);
44-
4543
if (s.type === "trigger") {
4644
icon = LucideCog;
4745
iconClassName = "text-purple-500";
@@ -157,7 +155,7 @@ async function downloadExportTable(
157155
export default function SchemaList({ search }: Readonly<SchemaListProps>) {
158156
const { databaseDriver, extensions } = useStudioContext();
159157
const [selected, setSelected] = useState("");
160-
const { refresh, schema, currentSchemaName } = useSchema();
158+
const { refresh, schema, currentSchemaName, selectSchema } = useSchema();
161159
const [editSchema, setEditSchema] = useState<string | null>(null);
162160

163161
const [collapsed, setCollapsed] = useState(() => {
@@ -318,7 +316,16 @@ export default function SchemaList({ search }: Readonly<SchemaListProps>) {
318316
const filterCallback = useCallback(
319317
(item: ListViewItem<DatabaseSchemaItem>) => {
320318
if (!search) return true;
321-
return item.name.toLowerCase().indexOf(search.toLowerCase()) >= 0;
319+
const needle = search.toLowerCase();
320+
const name = item.name.toLowerCase();
321+
// "schema.table": los nodos de schema llevan su nombre en item.name; las
322+
// tablas/vistas llevan su schema en data.schemaName. Permitimos buscar tanto
323+
// por nombre suelto ("orders") como cualificado ("ventas.", "ventas.orders").
324+
const qualified =
325+
item.data.type !== "schema" && item.data.schemaName
326+
? `${item.data.schemaName}.${item.name}`.toLowerCase()
327+
: name;
328+
return name.indexOf(needle) >= 0 || qualified.indexOf(needle) >= 0;
322329
},
323330
[search]
324331
);
@@ -354,6 +361,10 @@ export default function SchemaList({ search }: Readonly<SchemaListProps>) {
354361
tableName: item.data.tableName ?? "",
355362
});
356363
} else if (item.data.type === "schema") {
364+
// Movemos el badge activo ya mismo (optimista) y lo fijamos como
365+
// selección manual: sobrevive al refresh() de abajo aunque el
366+
// search_path por defecto siga apuntando a otro lado.
367+
selectSchema(item.name);
357368
if (databaseDriver.getFlags().supportUseStatement) {
358369
const dialect = databaseDriver.getFlags().dialect;
359370
const switch_keyword =

src/components/gui/sidebar/tools-sidebar.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"use client";
22
import { scc } from "@/core/command";
33
import ListButtonItem from "../list-button-item";
4-
import { Robot, TreeStructure } from "@phosphor-icons/react";
4+
import { ChatCircle, Robot, TreeStructure } from "@phosphor-icons/react";
55
import { localSettingDialog } from "@/app/(outerbase)/local-setting-dialog";
66

77
export default function SettingSidebar() {
@@ -14,6 +14,13 @@ export default function SettingSidebar() {
1414
}}
1515
icon={TreeStructure}
1616
/>
17+
<ListButtonItem
18+
text="Chat"
19+
onClick={() => {
20+
scc.tabs.openBuiltinChat({});
21+
}}
22+
icon={ChatCircle}
23+
/>
1724
{/* Acceso al setting de AI (API key) en modo local: abre el mismo dialog
1825
que usa el nav de cloud, pero acá queda visible en el visor standalone. */}
1926
<ListButtonItem

0 commit comments

Comments
 (0)