https://github.com/hkjang/jamypg
MCP server for metadata-grounded NL2SQL over PostgreSQL, MySQL, and MariaDB
https://github.com/hkjang/jamypg
agent ai ai-agents mariadb mcp mysql nl2sql pg postgres text2sql
Last synced: 4 days ago
JSON representation
MCP server for metadata-grounded NL2SQL over PostgreSQL, MySQL, and MariaDB
- Host: GitHub
- URL: https://github.com/hkjang/jamypg
- Owner: hkjang
- License: agpl-3.0
- Created: 2026-07-10T23:46:53.000Z (28 days ago)
- Default Branch: main
- Last Pushed: 2026-07-31T23:49:32.000Z (7 days ago)
- Last Synced: 2026-08-01T01:09:23.061Z (7 days ago)
- Topics: agent, ai, ai-agents, mariadb, mcp, mysql, nl2sql, pg, postgres, text2sql
- Language: Go
- Homepage:
- Size: 5.35 MB
- Stars: 2
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- Security: docs/security.md
Awesome Lists containing this project
README
# JAMYPG NL2SQL MCP
Go-based MCP server for metadata-grounded NL2SQL over **PostgreSQL, MySQL, and
MariaDB**. Current source version: `v0.58.0` (converted from the Oracle-based
jasql project).
The server loads JSON metadata from a dataset directory (e.g. `data/metadb`,
`data/sakila`), compiles it into an in-memory catalog, search index, join
graph, prompt registry, and static SQL guardrails, then exposes them through
MCP tools, resources, and prompts. Generated SQL can be executed read-only
against any of the three target engines through pure-Go drivers โ no CGO, no
client libraries, no build tags.
## Built with Codex and GPT-5.6
JAMYPG was developed through a human-directed AI engineering workflow using OpenAI Codex and GPT-5.6.
### How Codex Was Used
Codex served as the primary implementation agent throughout the project. It was used to:
* Explore and understand the existing Go codebase
* Implement MCP tools, resources, prompts, and transport behavior
* Develop metadata catalog, search, and join-graph features
* Add PostgreSQL, MySQL, and MariaDB connectivity
* Implement SQL validation and read-only execution guardrails
* Create REST APIs, administration features, and integration tests
* Refactor duplicated code and improve error handling
* Update technical documentation alongside source-code changes
Development was performed iteratively. Each task was defined with explicit goals and constraints, and Codex generated or modified the relevant code. The resulting changes were then reviewed, tested, and refined before being accepted.
### How GPT-5.6 Was Used
GPT-5.6 was used as the architecture, reasoning, and review layer of the development process. It helped with:
* Designing the metadata-grounded NL2SQL architecture
* Defining safe and explainable SQL-generation workflows
* Identifying schema-hallucination and incorrect-join risks
* Designing clarification, validation, and query-execution stages
* Reviewing MCP client compatibility and session behavior
* Developing multi-database abstraction strategies
* Creating test scenarios and evaluation criteria
* Reviewing security, maintainability, and enterprise-readiness
* Improving project documentation and presentation materials
GPT-5.6 was particularly useful for reasoning across multiple system concerns at once, including metadata quality, SQL dialect differences, MCP protocol behavior, database security, and LLM reliability.
### Human Oversight
AI-generated changes were not accepted automatically. The project owner remained responsible for:
* Defining product goals and technical requirements
* Reviewing generated code and architectural decisions
* Running unit and integration tests
* Verifying SQL safety rules
* Evaluating generated queries against expected results
* Approving the final implementation
This combination allowed Codex to accelerate implementation while GPT-5.6 supported architectural reasoning and systematic review, with human judgment controlling the final result.
**๐ ์์ธ ๋ฌธ์**: [docs/README.md](docs/README.md) โ ์ํคํ
์ฒ, MCP ๋๊ตฌ
๋ ํผ๋ฐ์ค(85์ข
), SQL ์์ฑ ์ํฌํ๋ก, ๊ฒ์ฆ ๋ฃฐ ์นดํ๋ก๊ทธ(33์ข
), ๋ฐ์ดํฐ์
๊ฐ์ด๋(18์ข
), REST API, DB ์ปค๋ฅํฐ, ์ด์/ํ๊ฐ/๋ณด์/๊ฐ๋ฐ์ ๊ฐ์ด๋.
## Quick Start
| ๋ชฉ์ | ๋ช
๋ น |
| --- | --- |
| ๋ก์ปฌ HTTP MCP + ๊ด๋ฆฌ์ UI | `go run ./cmd/jamypg-mcp -transport http -data ./data/metadb -addr 127.0.0.1:9797` |
| ๋ก์ปฌ stdio MCP | `go run ./cmd/jamypg-mcp -transport stdio -data ./data/metadb` |
| ์ปจํ
์ด๋ (๋ชจ๋ DB ์คํ ๊ฐ๋ฅ) | `docker build -t jamypg-mcp:v0.58.0 .` |
| ํตํฉ ํ
์คํธ DB 3์ข
๊ธฐ๋ | `docker compose -f deploy/test/docker-compose.yml up -d` |
| ํตํฉ ํ
์คํธ (pg+mysql+mariadb) | `go test -tags integration ./test/integration -v` |
HTTP ๋ชจ๋ ๊ธฐ๋ณธ ์ง์
์ :
- MCP endpoint: `http://127.0.0.1:9797/mcp`
- Web admin: `http://127.0.0.1:9797/admin`
- Swagger UI: `http://127.0.0.1:9797/docs`
- Health check: `http://127.0.0.1:9797/healthz`
## Supported Target Databases
| DB | ํ๋กํ์ผ `type` | ๋๋ผ์ด๋ฒ | read-only ์ธ์
๊ฐ์ |
| --- | --- | --- | --- |
| PostgreSQL | `postgres` (๊ธฐ๋ณธ) | `pgx/v5` (pure Go) | `default_transaction_read_only=on` |
| MySQL 8.x | `mysql` | `go-sql-driver/mysql` (pure Go) | `transaction_read_only=1` |
| MariaDB 10.x/11.x | `mariadb` | `go-sql-driver/mysql` (pure Go) | `tx_read_only=1` |
`connect_string`์ `host:port/dbname` ์ถ์ฝํ, `postgres://`/`mysql://` URL,
go-sql-driver DSN์ ๋ชจ๋ ํ์ฉํฉ๋๋ค. ์์ฑ SQL์ ๋ฐฉ์ธ์ ๋ฐ์ดํฐ์
์
`databases.json`(`dbms`) ๋๋ `overrides.json`(`dialect`)์ด ๊ฒฐ์ ํ๋ฉฐ ๊ธฐ๋ณธ์
postgres์
๋๋ค. ์์ธ: [docs/db-connector.md](docs/db-connector.md).
## NL2SQL Recommended Flow
๋๋ถ๋ถ์ ์ง๋ฌธ์ ๊ฐ๋ณ ๋๊ตฌ๋ฅผ ์ฌ๋ฌ ๋ฒ ์ค์ผ์คํธ๋ ์ด์
ํ์ง ๋ง๊ณ
`prepare_sql_context`๋ถํฐ ํธ์ถํ์ธ์.
1. `prepare_sql_context(question)` ํธ์ถ
2. ์๋ต์ด `status: "needs_clarification"`์ด๋ฉด SQL์ ๋ง๋ค์ง ๋ง๊ณ
`clarifications`์ ์ง๋ฌธ์ ์ฌ์ฉ์์๊ฒ ๋๋ฌป์ต๋๋ค.
3. ๋ต์ ๋ฐ์ ๋ค `prepare_sql_context(question, clarifications={...})`๋ก ๋ค์ ํธ์ถํฉ๋๋ค.
4. `status: "ready"`์ด๋ฉด `skeleton.skeleton_sql`์ `/* SLOT */`๋ง ์ฑ์ SQL์ ์์ฑํฉ๋๋ค.
5. `validate_sql` โ `explain_sql` โ ํ์ ์ `run_sql_safely` ์์๋ก ์งํํฉ๋๋ค.
์ด ํ๋ฆ์ ํ
์ด๋ธ/์ปฌ๋ผ/์งํ/์๊ฐ์กฐ๊ฑด/์กฐ์ธ ๊ฒฝ๋ก/๊ฒ์ฆ ํํธ๋ฅผ ํ ๋ฒ์ ๋ฌถ์ด
LLM์ด ์คํค๋ง๋ฅผ ์ถ์ธกํ๊ฑฐ๋ ํ์ ๊ฒ์ฆ ๋จ๊ณ๋ฅผ ๊ฑด๋๋ฐ๋ ์ผ์ ์ค์
๋๋ค.
## Transports
- `stdio`: newline-delimited JSON-RPC over standard input/output. Use this for desktop MCP clients that launch a local subprocess.
- `http`: Streamable HTTP at a single MCP endpoint. Use this for local HTTP clients, gateways, or remote service wrapping.
## Build
์์ Go ๋น๋ ํ๋๋ก ์ธ DB ๋ชจ๋ ์ง์ํฉ๋๋ค (CGO ๋ถํ์, ํด๋ผ์ด์ธํธ ๋ผ์ด๋ธ๋ฌ๋ฆฌ
๋ถํ์):
Windows PowerShell:
```powershell
.\scripts\build.ps1
```
Linux/macOS shell:
```sh
sh ./scripts/build.sh
```
Artifacts:
```text
dist/jamypg-mcp-windows-amd64.exe
dist/jamypg-mcp-linux-amd64
dist/jamypg-mcp-linux-arm64
```
Single-platform builds:
```powershell
go build -o .\bin\jamypg-mcp.exe .\cmd\jamypg-mcp
```
```sh
go build -o ./bin/jamypg-mcp ./cmd/jamypg-mcp
```
## Docker Image
๋จ์ผ `Dockerfile`์ด ์คํ ๊ฐ๋ฅํ ์์ ํ ์ด๋ฏธ์ง๋ฅผ ๋ง๋ญ๋๋ค (๊ณผ๊ฑฐ์
`Dockerfile.oracle`/Instant Client ์ ์ฐจ๋ ์ ๊ฑฐ๋์์ต๋๋ค):
```sh
docker build -t jamypg-mcp:v0.58.0 .
docker run --rm -p 9797:9797 \
-e JAMYPG_ADMIN_TOKEN=change-me \
-e PG_PROD_PW=... \
jamypg-mcp:v0.58.0
```
DB ํ๋กํ์ผ์ `/admin/db` ๋๋ DB profile REST/MCP API๋ก ๊ตฌ์ฑํ ๋ค
`run_sql_safely`๋ก read-only ์คํํฉ๋๋ค. See
[docs/db-connector.md](docs/db-connector.md).
## Integration Test Environment (pg + mysql + mariadb)
jamypg์ **๋ฉํ DB ์คํค๋ง ์์ฒด๋ฅผ text2sql ๋์**์ผ๋ก ์ธ ์์ง์ ์ ์ฌํ
ํ
์คํธ ํ๊ฒฝ์ด ํฌํจ๋์ด ์์ต๋๋ค:
```sh
docker compose -f deploy/test/docker-compose.yml up -d
# postgres:16 โ 127.0.0.1:55432 (db jamypg_meta; ๋ฉํ DB ๊ฒธ ๋์ DB)
# mysql:8.4 โ 127.0.0.1:53306 (database `public`)
# mariadb:11.4 โ 127.0.0.1:53307 (database `public`)
go test -tags integration ./test/integration -v # ping/guard/limit/explain/
# ์ค๋ฅ์ฝ๋/text2sql ๊ณจ๋ ์
8์ข
ร 3๊ฐ DB
# ์๋ฒ๋ฅผ ์ด ๋ฐ์ดํฐ์
์ผ๋ก ์ง์ ๋์๋ณด๊ธฐ
go run ./cmd/jamypg-mcp -data data/metadb -addr 127.0.0.1:9797
# (์ ํ) ๋ฉํ DB ๋ชจ๋: -meta-db 'postgres://postgres:metapw@127.0.0.1:55432/jamypg_meta'
```
์นดํ๋ก๊ทธ ๋ฐ์ดํฐ์
์ `data/metadb/`(๋ฌผ๋ฆฌ/๋
ผ๋ฆฌ ๋ชจ๋ธ, ๊ด๊ณ, ์ฉ์ด์ง, ์งํ/์ฝ๋
์ฌ์ , ์ปฌ๋ผ ํต๊ณ, ์์ SQL, ๊ณจ๋ ์
, ํ๋กํ์ผ 3์ข
)์ด๋ฉฐ
`python3 deploy/test/gen_testenv.py`๋ก ์ฌ์์ฑํฉ๋๋ค.
### ์ ๋ช
์คํ์์ค ์คํค๋ง ๋ฐ์ดํฐ์
(sakila / northwind / wordpress)
๊ฐ์ ์ปจํ
์ด๋์ ์ ๋ช
์คํ์์ค ์๋น์ค ์คํค๋ง 3์ข
์ด ์๋๋์ด ์๊ณ , ๊ฐ๊ฐ ๋
๋ฆฝ
๋ฐ์ดํฐ์
์ผ๋ก text2sql์ ๊ฒ์ฆํฉ๋๋ค (`python3 deploy/test/gen_oss_testenv.py`๋ก
์ฌ์์ฑ):
| ๋ฐ์ดํฐ์
| ์คํค๋ง | ์ ๋ | ๊ณจ๋ ์
|
| --- | --- | --- | --- |
| `data/sakila` | sakila (9 tables: film/actor/customer/rental/payment...) | MySQL ๊ณต์ ์ํ DB (DVD ๋ ํ) | 6 (์ ๋ต ๊ฒ์ฆ ํฌํจ) |
| `data/northwind` | northwind (8 tables: products/orders/customers...) | ๊ณ ์ ์ฃผ๋ฌธ๊ด๋ฆฌ ์ํ | 6 (์ ๋ต ๊ฒ์ฆ ํฌํจ) |
| `data/wordpress` | wordpress (8 tables: wp_posts/wp_comments/wp_terms...) | WordPress CMS ํต์ฌ ํ
์ด๋ธ | 5 (์ ๋ต ๊ฒ์ฆ ํฌํจ) |
์ธ ์คํค๋ง ๋ชจ๋ PostgreSQL(์คํค๋ง)ยทMySQL/MariaDB(๋๋ช
๋ฐ์ดํฐ๋ฒ ์ด์ค)์ ๋์ผํ๊ฒ
์ ์ฌ๋์ด `sakila.film` ๊ฐ์ ์คํค๋ง ํ์ SQL์ด ์ธ ์์ง์์ ๊ทธ๋๋ก ์คํ๋๊ณ ,
๊ณจ๋ ์
์ ๊ธฐ๋ ์ ๋ต(์: ์นดํ
๊ณ ๋ฆฌ๋ณ ์ํ ์ 1์)์ด ์ธ ์์ง์์ ์ผ์นํ๋์ง๊น์ง
ํตํฉ ํ
์คํธ๊ฐ ๊ฒ์ฆํฉ๋๋ค:
```sh
go run ./cmd/jamypg-mcp -data data/sakila -addr 127.0.0.1:9797 # ํ๋กํ์ผ: pg-sakila / mysql-sakila / mariadb-sakila
```
## Run With stdio
Windows:
```powershell
.\dist\jamypg-mcp-windows-amd64.exe -transport stdio -data .\data\metadb
```
Linux:
```sh
chmod +x ./dist/jamypg-mcp-linux-amd64
./dist/jamypg-mcp-linux-amd64 -transport stdio -data ./data/metadb
```
Example MCP client config for Windows:
```json
{
"mcpServers": {
"jamypg": {
"command": "C:\\Users\\USER\\projects\\jamypg\\dist\\jamypg-mcp-windows-amd64.exe",
"args": ["-transport", "stdio", "-data", "C:\\Users\\USER\\projects\\jamypg\\data\\metadb"]
}
}
}
```
Example MCP client config for Linux:
```json
{
"mcpServers": {
"jamypg": {
"command": "/opt/jamypg/dist/jamypg-mcp-linux-amd64",
"args": ["-transport", "stdio", "-data", "/opt/jamypg/data/metadb"]
}
}
}
```
stdio smoke test:
```powershell
$msg = '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"smoke","version":"0.0.1"}}}'
$msg | .\dist\jamypg-mcp-windows-amd64.exe -transport stdio -data .\data\metadb
```
```sh
printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"smoke","version":"0.0.1"}}}' \
| ./dist/jamypg-mcp-linux-amd64 -transport stdio -data ./data/metadb
```
## Run With Streamable HTTP
```powershell
go run ./cmd/jamypg-mcp -transport http -data .\data\metadb -addr 127.0.0.1:9797
```
MCP endpoint:
```text
http://127.0.0.1:9797/mcp
```
Health check:
```powershell
Invoke-RestMethod http://127.0.0.1:9797/healthz
```
## MCP Transport
This implements the MCP Streamable HTTP transport:
- `POST /mcp` accepts one JSON-RPC MCP message.
- `GET /mcp` opens a Server-Sent Events stream for server-to-client messages.
- `DELETE /mcp` closes a stateful session.
- `Mcp-Session-Id` is issued after `initialize` but never required: clients
that do not echo the header (qwen-code, opencode, ...) are served normally
(lenient session policy).
- `MCP-Protocol-Version: 2025-06-18` is accepted on subsequent requests.
- Origin validation allows empty origins, `localhost`, `127.0.0.1`, and `::1`.
For stateless local testing:
```powershell
go run ./cmd/jamypg-mcp -transport http -data .\data\metadb -stateless
```
To return POST responses as SSE:
```powershell
go run ./cmd/jamypg-mcp -transport http -data .\data\metadb -sse-post
```
## Curl Smoke Test
Initialize:
```powershell
$init = @{
jsonrpc = "2.0"
id = 1
method = "initialize"
params = @{
protocolVersion = "2025-06-18"
capabilities = @{}
clientInfo = @{ name = "curl"; version = "0.0.1" }
}
} | ConvertTo-Json -Depth 10
$res = Invoke-WebRequest `
-Uri http://127.0.0.1:9797/mcp `
-Method POST `
-ContentType "application/json" `
-Headers @{ Accept = "application/json, text/event-stream" } `
-Body $init
$sid = $res.Headers["Mcp-Session-Id"]
$res.Content
```
List tools:
```powershell
$body = @{
jsonrpc = "2.0"
id = 2
method = "tools/list"
} | ConvertTo-Json -Depth 5
Invoke-RestMethod `
-Uri http://127.0.0.1:9797/mcp `
-Method POST `
-ContentType "application/json" `
-Headers @{
Accept = "application/json, text/event-stream"
"Mcp-Session-Id" = $sid
"MCP-Protocol-Version" = "2025-06-18"
} `
-Body $body
```
Search schema:
```powershell
$body = @{
jsonrpc = "2.0"
id = 3
method = "tools/call"
params = @{
name = "search_schema"
arguments = @{
question = "์ต๊ทผ 6๊ฐ์๊ฐ ์ ์ฉ์นด๋ ์ด์ฉ ๋ด์ญ์ด ์๋ ๊ณ ๊ฐ ์"
top_k = 5
include_columns = $true
}
}
} | ConvertTo-Json -Depth 10
Invoke-RestMethod `
-Uri http://127.0.0.1:9797/mcp `
-Method POST `
-ContentType "application/json" `
-Headers @{
Accept = "application/json, text/event-stream"
"Mcp-Session-Id" = $sid
"MCP-Protocol-Version" = "2025-06-18"
} `
-Body $body
```
## Tools
- `prepare_sql_context` โ ์ง๋ฌธ ๋ถ์โ๊ฒ์โ์งํโ์คํค๋งโ์กฐ์ธโSQL ๊ณจ๊ฒฉ์ ํ ๋ฒ์ ์์ฑํ๋ ๊ถ์ฅ ์ง์
์
- `analyze_question` โ ์ง๋ฌธ ๋ถํด: intent, ์งํ(์ฌ์ ๋งค์นญ), ์ฐจ์, ํํฐ, ์๊ฐ๋ฒ์, ์ ๋ ฌ/limit, ๋ชจํธ์ฑ, ์ ์ฉ ๊ธฐ๋ณธ๊ฐ
- `retrieve_context` โ ๊ฒ์ ํ๋ณด์ ์กฐ์ธ ๊ทธ๋ํ ํ์ฅ์ ๊ฒฐํฉํ๊ณ ์ ์ ๊ทผ๊ฑฐยท์กฐ์ธ ๊ฒฝ๋กยท๊ฐ ์ฆ๊ฑฐ๋ฅผ ๋ฐํ
- `search_schema` โ ๋ค์ค ์ ํธ ์ค์ฝ์ด๋ง(๋ฌผ๋ฆฌ/๋
ผ๋ฆฌ๋ช
, ์ค๋ช
, ๋์์ด, ๋๋ฉ์ธ, ์งํ์ฌ์ , ์ํ๊ฐ, ๊ณผ๊ฑฐ ์ฑ๊ณต SQL, ์กฐ์ธ ์ฐ๊ฒฐ์ฑ) + ๋งค์นญ ์ฌ์ + ์ ์ธ ํ๋ณด/์ฌ์
- `get_schema_context` โ ์์ถ ์ปจํ
์คํธ: ์ ํ ํ
์ด๋ธ/์ปฌ๋ผ Top-K, ํ์ ์กฐ์ธ ์กฐ๊ฑด, ์งํ ๊ณ์ฐ์, ์๊ฐ ์กฐ๊ฑด, PII ํ์, ์ ์ธ ์ปฌ๋ผ ๋ก๊ทธ
- `get_join_paths` โ ์กฐ์ธ ๊ทธ๋ํ ๊ธฐ๋ฐ ๊ฒฝ๋ก(๋ชจ๋ ์), confidence/preferred/caution, ์ ์ ๋ขฐยท๊ฒฝ๋ก์์ ๊ฐ์ด๋์ค, ๊ธ์ง ์กฐ์ธ ์ฐจ๋จ
- `get_metric_definition` โ ์งํ ์ฌ์ (`metrics.json`) ์ฐ์ ์กฐํ; exact/business name/alias์ glossaryยทํ ํฐ ๊ทผ์ ๋๋ฅผ ๊ฒฐํฉํ๊ณ confidence/evidence๋ฅผ ๋ฐํ, ์์ผ๋ฉด ์ถ์ ํ๋ณด๋ฅผ ๋ช
ํํ ๋ถ๋ฆฌ
- `get_column_stats` โ ๋ฉํ + ํ๋กํ์ผ ํต๊ณ(null ๋น์จ, distinct, min/max, top values, ํฌ๋งท ํจํด)
- `find_filter_columns` โ ์ง๋ฌธ ์ ๋ฆฌํฐ๋ด ๊ฐ(์์ธ, ์ ์, ๊ฐ์ธ์ฌ์
์...)์ ์ฝ๋์ฌ์ /top values๋ก ํํฐ ์ปฌ๋ผ์ ๋งคํ
- `resolve_time` โ ์๊ฐ ํํ(์ค๋/์ง๋๋ฌ/์ต๊ทผ 3๊ฐ์/2025๋
6์/์๋ฐ๊ธฐ/์ ์ ๋๋น...)์ semantic_type๋ณ SQL ์กฐ๊ฑด์ผ๋ก ๋ณํ
- `search_examples` โ golden SQL ์์ ๊ฒ์ (์ง๋ฌธ์ intent ์๊ทธ๋์ฒ์ ์์ `target_intent`์ ๊ตฌ์กฐ ์ ์ฌ๋๋ก ๋ญํน โ ๊ฐ์ SQL ํํ์ ์์ ์ฐ์ )
- `build_sql_skeleton` โ ๋ณต์ก/๋ค์ค ํ
์ด๋ธ ์ง๋ฌธ์ฉ: ๊ฒ์ฆ๋ ๋ถํ(์นดํ๋ก๊ทธ ์กฐ์ธ ์กฐ๊ฑด+alias, ์งํ์ฌ์ expression, semantic_type๋ณ ์๊ฐ ์กฐ๊ฑด, ์ ์ฑ
ํํฐ)์ ์กฐ๋ฆฝํ SQL ๊ณจ๊ฒฉ ๋ฐํ. LLM์ `/* SLOT */` ์ฃผ์๋ง ์ฑ์
- `rank_candidates` โ ํ๋ณด SQL ์ฌ๋ฌ ๊ฐ๋ฅผ ์๋ฒ์ธก ๊ฐ๊ด ์ ํธ(๊ฒ์ฆ ์ค๋ฅ/๊ฒฝ๊ณ , ๋ฆฌ์คํฌ, ๊ฒฐ๊ณผ ์คํค๋ง ์ปค๋ฒ๋ฆฌ์ง, ์งํ ์ผ์น)๋ก ์ ๋ ฌํด ์ต์ ์ ๋ฐํ โ self-consistency๋ฅผ LLM ์๊ธฐํ๊ฐ ๋์ ๊ฐ๊ด ์ ์๋ก ๊ตฌํ
- `suggest_joins` โ ๋จ์ผ ์ปฌ๋ผ PK ๋ง์คํฐ๋ฅผ ์ฐธ์กฐํ๋ ๋ฏธ์ฐ๊ฒฐ ํ
์ด๋ธ์ ๋ฐ๊ตดํด ์กฐ์ธ ์ฃ์ง ํ๋ณด ์ ์(FK/์ธ๋ฑ์ค/ํ์
/๋์์ถํ ๊ทผ๊ฑฐ + overrides.json ์ค๋ํซ). **์ด์์ ๊ฒํ ์ฉ โ ์๋ ์ ์ฉ๋์ง ์์**
- `suggest_join_relations` โ ๊ณจ๋ ์
์์ ์กฐ์ธ ๊ฒฝ๋ก๊ฐ ๋๊ธด ํ
์ด๋ธ ์์ ์ฐพ๊ณ ๊ณตํต ํค ๊ธฐ๋ฐ relation ๋ณด๊ฐ ํ๋ณด๋ฅผ ์ ์
- `validate_sql` โ ์ ์ ๊ฒ์ฆ: ๋ฏธ์กด์ฌ ํ
์ด๋ธ/์ปฌ๋ผ, ์กฐ์ธ ๊ทธ๋ํ, ์นดํฐ์
, GROUP BY, ๋ฐฉ์ธ(postgres/mysql/mariadb โ Oracle ์ ์ฉ ๋ฌธ๋ฒ ์ฐจ๋จ, ๊ต์ฐจ ๋ฐฉ์ธ ํจ์ ๊ฒฝ๊ณ ), ๋ ์ง ํ์
, PII, ์งํ์ ์ผ์น, **์ฝ๋์ฌ์ ๊ฐ ๊ฒ์ฆ**(์กด์ฌํ์ง ์๋ ์ฝ๋ ๋ฆฌํฐ๋ด ์ฐจ๋จ), **๊ฒฐ๊ณผ ์คํค๋ง ๊ฒ์ฆ**(`expected_outputs`๋ก ์๊ตฌ ์ฐจ์/์งํ ๋๋ฝ ๊ฐ์ง), CTE/์ธ๋ผ์ธ๋ทฐ ์ค์ฝํ ์ธ์, ๊ตฌ์กฐํ๋ `fix_hints`(์ต๋ 2ํ ์๋์์ ๋ฃจํ์ฉ)
- `explain_sql` โ ๋ฆฌ์คํฌ ์ถ์ : ์ ์ ๋ถ์ + `profile` ์ง์ ์ **์ค์ธก EXPLAIN**(postgres `EXPLAIN (FORMAT JSON)`, mysql/mariadb `EXPLAIN FORMAT=JSON`) โ full scan/์นดํฐ์
/๋๋ ์ ๋ ฌ/๊ณ ๋น์ฉ ํ์ง, ๊ฐ์ ์ ์
- `list_db_profiles` โ ํธ์ถ์๊ฐ ์ฌ์ฉํ ์ ์๋ DB ์ฐ๊ฒฐ ํ๋กํ์ผ id์ ๋ง์คํน๋ ์ ์ยท์ ์ฑ
์ ๋ณด๋ฅผ ๋ฐํ
- `route_db_profile` โ ํ๋กํ์ผ์ด ๋ง์ ๋ SQL์ด ์ฐธ์กฐํ๋ ํ
์ด๋ธ์ ๋ฐฉ์ธ ํ์๋ก ์ถ์ถํด ๊ฐ ํ๋กํ์ผ์ ์ค์ธก ์ธ๋ฒคํ ๋ฆฌ(information_schema)ยท์ ์ธ ์คํค๋งยท๋ฐฉ์ธยทํฌ์คยท์ฐ์ ์์๋ก ์ ์ํํ์ฌ ์คํ ๋์ ํ๋กํ์ผ์ ํ์ . ๋ช
ํํ ์น์๊ฐ ์์ผ๋ฉด `decisive=true`๋ก `selected_profile`์, ์ ๋งคํ๋ฉด ํ๋ณด ๋ชฉ๋ก์ ๋ฐํ. `run_sql_safely(profile="auto")`๊ฐ ๋ด๋ถ์ ์ผ๋ก ์ฌ์ฉ
- `run_sql_safely` โ ๊ฒ์ฆ ํ **์ค์ DB ์คํ** (`profile` ์ง์ ์; postgres/mysql/mariadb, read-only ์ธ์
, ํ์์์ยทํ ์ ํยทtruncatedยท๊ฐ์ฌ ๋ก๊ทธ). `profile="auto"`๋ฉด router๊ฐ ๋์ ํ๋กํ์ผ์ ํ์ (์ ๋งคํ๋ฉด `profile_choice_required`๋ก ์ฌ์ง๋ฌธ). ํ๋กํ์ผ ๋ฏธ์ง์ ์ dry-run ๊ฐ๋. ๊ฒ์ฆ ์คํจ SQL์ ์คํํ์ง ์์
- `execute_with_repair` โ **์๊ธฐ์์ ์คํ**: ๊ฒ์ฆโ์คํโ์ง๋จ์ ํ ๋ฒ์ ์ํํ๊ณ ์คํจ ์ `repair` ํคํธ(์คํจ ๋จ๊ณ, ๋ถ๋ฅ๋ error_code+ํํธ, ์นดํ๋ก๊ทธ fix_hints, ์ฐธ์กฐ ํ
์ด๋ธ ์คํค๋ง)๋ฅผ ๋ฐํํด ํ ํด์ SQL ๊ต์ ๊ฐ๋ฅ. 0ํ์ด๋ฉด `executed_empty`+zero_row_hints. run_sql_safely์ ๋์ผ ๊ฐ๋, ๋ฐ๋ณต์ด ์์๋๋ฉด ์ด ๋๊ตฌ ์ฐ์
- `list_metadata_sources` โ ์๋ ๋ฉํ๋ฐ์ดํฐ ์์ง ์์ฒ์ผ๋ก ์ธ ์ ์๋ DB ํ๋กํ์ผ(source_id/name/type/๋ง์คํน ์ ์๋์) ๋ชฉ๋ก. ๋ฌผ๋ฆฌ ๋ฉํ๋ฐ์ดํฐ๋ ์๋ ์์งํ๋ ์
๋ฌด ์๋ฏธ๋ ์น์ธ ๊ธฐ๋ฐ์ผ๋ก ๊ด๋ฆฌ
- `discover_metadata` โ ์์ฒ DB์ ๋น์์คํ
์คํค๋ง ๋ชฉ๋ก ์กฐํ(information_schema๋ง ์ฝ๋ read-only). ์์ง ๋ฒ์ ์ง์ ์ฉ
- `db_health_report` โ **DBA ํฌ์ค ์ ๊ฒ**: ์ฐ๊ฒฐ๋ ํ๋กํ์ผ DB์ ์์คํ
์นดํ๋ก๊ทธ๋ฅผ ์ฝ์ด PK ์๋ ํ
์ด๋ธ(high)ยท์ธ๋ฑ์ค ์๋ FK ์ปฌ๋ผ(medium)ยท๋ฏธ์ฌ์ฉ ์ธ๋ฑ์ค(low)ยทํต๊ณ ์ค๋๋จ/์์(medium)ยท๋ํ ํ
์ด๋ธ(์ฝ๋ฉํธ ์ฌ๋ถ, info)์ ์ง๋จ. PostgreSQL ์ ์ฒด, MySQL/MariaDB๋ ์ด์ ๊ฐ๋ฅ ํญ๋ชฉ๋ง. ์ฝ๊ธฐ ์ ์ฉ(์์ ยท์คํ ์์, ๊ฐ์ ์ DBA ๊ฒํ ํ)
- `suggest_indexes` โ **์ธ๋ฑ์ค ์ด๋๋ฐ์ด์ **: ์ฟผ๋ฆฌ ๊ฐ์ฌ ๋ก๊ทธ(query-*.jsonl)์์ ๋๋ฆฐ ์ฑ๊ณต ์ฟผ๋ฆฌ๋ฅผ ๋ถ์ํด ์ธ๋ฑ์ค๊ฐ ์๋ WHERE/JOIN/ORDER BY ์ปฌ๋ผ์ ์ง๊ณํ๊ณ , ์ํฅ๋(๋ฐ์ ํ์ ร ํ๊ท ์ง์ฐ) ์์ผ๋ก ํ๋ณด ์ธ๋ฑ์ค๋ฅผ ์ ์. ๊ฐ ํ๋ณด์ ๊ฒํ ์ฉ `CREATE INDEX` DDL๊ณผ ๋ํ ์ฟผ๋ฆฌ ํฌํจ. ์ฝ๊ธฐ ์ ์ฉยท๊ถ๊ณ ์ฉ(์๋ ์์ฑํ์ง ์์ผ๋ฉฐ DBA๊ฐ ์นด๋๋๋ฆฌํฐยท์ฐ๊ธฐ๋ถํ ๊ฒํ ํ ์ํ). `profile`(์ ํ)ยท`min_elapsed_ms`(๊ธฐ๋ณธ 200)ยท`days`(๊ธฐ๋ณธ 7)
- `lint_sql` โ **SQL ์ํฐํจํด ๋ฆฐํธ**: ๋จ์ผ ๋ฌธ์ฅ์ ์ ์ ๋ถ์ํด ๊ณ ์ ์ ์ฑ๋ฅยท์ ํฉ์ฑ ์ค๋ฉ์ ์ง๋จ โ `SELECT *`, ์ ๋ ์์ผ๋์นด๋ `LIKE '%โฆ'`, `NOT IN (์๋ธ์ฟผ๋ฆฌ)`, ์ธ๋ฑ์ค ์ปฌ๋ผ์ ํจ์๋ก ๊ฐ์ผ ๋น-sargable ์กฐ๊ฑด, ์ธ๋ฑ์ค ์ปฌ๋ผ ๋ถ๋ฑํธ, ์ฝค๋ง ํฌ๋ก์ค ์กฐ์ธ, `WHERE`์ `OR`, `LIMIT` ์๋ `ORDER BY`, `WHERE` ์๋ DML. ๊ฐ ํญ๋ชฉ์ ์ฌ๊ฐ๋์ ๊ฐ์ ์ ์ ํฌํจ. ์นดํ๋ก๊ทธ ์ธ๋ฑ์ค ์ปค๋ฒ๋ฆฌ์ง ์ธ์ยท๊ถ๊ณ ์ฉ(์๋ ์์ ์ ํจ). `sql`ยท`profile`(์ ํ)
- `explain_sql_in_words` โ **SQL ์์ฐ์ด ์ค๋ช
**: SQL์ด ์ด๋ค ํ
์ด๋ธ(์นดํ๋ก๊ทธ ๋
ผ๋ฆฌ๋ช
)์์ ๋ฌด์์ ํํฐยท์กฐ์ธยท๊ทธ๋ฃนยท์ ๋ ฌํ๊ณ ์ด๋ค ์ง๊ณ๋ฅผ ๊ณ์ฐํ๋์ง ํ๊ตญ์ด๋ก ์์ฝ. ์ ์ ๊ตฌ์กฐ ๋ถ์(์คํ ์ ํจ). `sql`ยท`profile`(์ ํ)
- `workload_report` โ **์ํฌ๋ก๋ ๋ฆฌํฌํธ**: ๊ฐ์ฌ ๋ก๊ทธ๋ฅผ ๊ธฐ๊ฐ๋ณ๋ก ์ง๊ณํด ์ด/์ฑ๊ณต/์ค๋ฅ ๊ฑด์ยท์ค๋ฅ์จ, ์ง์ฐ ๋ถํฌ(avg/p50/p95/p99/max), ๋๋ฆฐ ์ฟผ๋ฆฌ ์, ๊ฐ์ฅ ๋ง์ด ์ ๊ทผํ ํ
์ด๋ธ, ์์ ์ค๋ฅ ์ฝ๋, ํดยทํ๋กํ์ผ๋ณ ์ฌ์ฉ๋, ๊ฐ์ฅ ๋๋ฆฐ ๋ฌธ์ฅ, ํผํฌ ์๊ฐ๋๋ฅผ ๋ฆฌํฌํธ. ์ฝ๊ธฐ ์ ์ฉ. `profile`(์ ํ)ยท`days`(๊ธฐ๋ณธ 7)ยท`slow_ms`(๊ธฐ๋ณธ 200)
- `get_dba_digest` โ **DBA ๋ค์ด์ ์คํธ**: ์ํฌ๋ก๋ ๋ฆฌํฌํธ์ ์ธ๋ฑ์ค ์ด๋๋ฐ์ด์ ๋ฅผ ์์ถํ ๋ฅ๋ํ ์ด์ ์ค๋
์ท โ ์ฟผ๋ฆฌ๋ยท์ค๋ฅ์จยทp95/์ต๋ ์ง์ฐยท๋๋ฆฐ ์ฟผ๋ฆฌ ์ยทํซ ํ
์ด๋ธยท์์ ์ธ๋ฑ์ค ํ๋ณด์ ํ ์ค ํค๋๋ผ์ธ. ์ฝ๊ธฐ ์ ์ฉ. ์ค์ผ์ค๋ฌ(`-sync-interval` + `-digest-webhook` + `-dba-digest`)๊ฐ ํฑ๋ง๋ค ์นํ
์ผ๋ก pushํ๋ ๊ฒ๊ณผ ๋์ผํ ๋ฐ์ดํฐ. `profile`(์ ํ)ยท`days`(๊ธฐ๋ณธ 7)ยท`slow_ms`(๊ธฐ๋ณธ 200)
### DBA ๊ด๋ฆฌ ๋๊ตฌ (privileged, `dba`/`admin` ์ญํ ์ ์ฉ)
ํ๋กํ์ผ์ **DBA ์๊ฒฉ์ฆ๋ช
**(`db_profiles`์ `dba.enabled`+`dba.username`+`dba.password_ref`)์ ์ค์ ํ ๊ฒฝ์ฐ์๋ง ์ฌ์ฉํ ์ ์์ต๋๋ค. ์ฝ๊ธฐ ์ ์ฉ ์ฟผ๋ฆฌ ๊ณ์ ๊ณผ **๋ถ๋ฆฌ๋ ์ฐ๊ธฐ ๊ฐ๋ฅ ์ปค๋ฅ์
**์ผ๋ก ์คํ๋๋ฉฐ, ๋ชจ๋ ๋ณ๊ฒฝ์ ๊ฐ์ฌ ๋ก๊ทธ(`dba:*`)์ ๊ธฐ๋ก๋ฉ๋๋ค. ๊ด๋ฆฌ ํ๋ฉด: `/admin/dba-console`.
- `dba_overview` โ ์ฝ์ ๊ฐ์: ๋ฐฉ์ธยทDBA ํ์ฑ ์ฌ๋ถยท์๋ฒ ๋ฒ์ ยท์ญํ /DB ์ (์ฝ๊ธฐ ์ ์ฉ)
- `dba_list_users` โ ์ฌ์ฉ์/์ญํ ๋ชฉ๋ก๊ณผ ์์ฑ(superuserยทcreatedbยทcreateroleยทloginยท์ฐ๊ฒฐ์ ํ)
- `dba_list_databases` โ ๋ฐ์ดํฐ๋ฒ ์ด์ค ๋ชฉ๋ก(์์ ์ยท์ธ์ฝ๋ฉยท์ฝ๋ ์ด์
ยทํฌ๊ธฐ)
- `dba_list_settings` โ ์๋ฒ ์ค์ ํ๋ผ๋ฏธํฐ(`pg_settings`/`global_variables`), ์ด๋ฆ ๋ถ๋ถ๊ฒ์
- `dba_list_sessions` โ ํ์ฑ ์ธ์
/๋ฐฑ์๋(pidยท์ฌ์ฉ์ยท์ํยท์ง์์๊ฐยทํ์ฌ ์ฟผ๋ฆฌ)
- `dba_create_user` โ ์ฌ์ฉ์/์ญํ ์์ฑ(postgres LOGIN/SUPERUSER/CREATEDB/CREATEROLE, mysql `CREATE USER`). ๋น๋ฐ๋ฒํธ๋ ๊ฐ์ฌ ๋ก๊ทธ์์ ๋ง์คํน
- `dba_alter_user` โ ๋น๋ฐ๋ฒํธยท์์ฑ ๋ณ๊ฒฝ
- `dba_drop_user` โ ์ฌ์ฉ์/์ญํ ์ญ์ (`confirm=true` ํ์)
- `dba_grant` โ ๊ถํ ๋ถ์ฌ/ํ์(`revoke=true`), `WITH GRANT OPTION` ์ง์
- `dba_create_database` โ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์์ฑ(OWNER/ENCODING ๋๋ CHARACTER SET)
- `dba_drop_database` โ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ญ์ (`confirm=true` ํ์)
- `dba_set_parameter` โ ์ค์ ๋ณ๊ฒฝ(postgres `ALTER SYSTEM`+`pg_reload_conf()`, mysql `SET GLOBAL/SESSION`)
- `dba_terminate_session` โ ์ธ์
์ข
๋ฃ/์ฟผ๋ฆฌ ์ทจ์(`pg_terminate_backend`/`pg_cancel_backend`, mysql `KILL`)
- `dba_run_maintenance` โ ์ ์ง๋ณด์(postgres VACUUM/ANALYZE/REINDEX, mysql ANALYZE/OPTIMIZE)
- `dba_execute` โ ์์ ๊ถํ SQL ์คํ(์์ค์ผ์ดํ ํด์น, `confirm=true` ํ์, ๊ฐ์ฌ ๋ก๊ทธ์ ์๋ฌธ ๊ธฐ๋ก)
- `describe_db_schema` โ ์ฐ๊ฒฐ๋ ํ๋กํ์ผ DB์ **๋ผ์ด๋ธ ์คํค๋ง**(information_schema)๋ฅผ ์กฐํํด ์นดํ๋ก๊ทธ์ ์๋ ํ
์ด๋ธ๋ SQL ์์ฑ ๊ทผ๊ฑฐ๋ก ์ ๊ณต. **์นดํ๋ก๊ทธ ์ฐ์ **: ๋ฑ๋ก๋ ํ
์ด๋ธ/์ปฌ๋ผ์ ๋
ผ๋ฆฌ๋ช
ยท์ค๋ช
์ ํจ๊ป ๋ถ์ด๊ณ `in_catalog` ํ๋๊ทธ๋ก ๊ตฌ๋ถ. ์ฝ๊ธฐ ์ ์ฉยท๋น์ ์ฅ. ๋ผ์ด๋ธ ์ ์ฉ ํ
์ด๋ธ์ ๊ฒ์ฆ๊น์ง ํต๊ณผ์ํค๋ ค๋ฉด `apply_metadata_sync`๋ก ๋ฐ์
- `run_metadata_sync` โ ์์ฒ DB์ ๋ฌผ๋ฆฌ ๋ชจ๋ธ(์คํค๋งยทํ
์ด๋ธยท๋ทฐยท์ปฌ๋ผยทPK/FK/Unique/Checkยท์ธ๋ฑ์คยท์ฝ๋ฉํธยทํ์์ถ์ )์ ๋ฒ์ ์ค๋
์์ผ๋ก ์์งํ๊ณ ์ด์ ์ค๋
์ ๋๋น ๋ณ๊ฒฝ๋ถ์ ๋ฐํ. ๊ธฐ๋ณธ ์ฆ๋ถ(์คํค๋ง ํด์ ๋์ผ ์ ์คํต). ์ญ์ ๋ ์ฆ์ ๋ฐ์ํ์ง ์๊ณ ํ๊ธฐ ํ๋ณด๋ก ํ์. **๋ฌผ๋ฆฌ ์ ๋ณด๋ง ์์งํ๋ฉฐ ์
๋ฌด ์๋ฏธ(๋
ผ๋ฆฌ๋ช
ยท์งํ)๋ ์ด์ ์นดํ๋ก๊ทธ์ ์ฐ์ง ์์**
- `apply_metadata_sync` โ **(๊ด๋ฆฌ์)** ์์ฒ์ ์ต์ ์ค๋
์์ ์นดํ๋ก๊ทธ์ **์๋ ๋ฐ์**: ๋ฌผ๋ฆฌ ๋ชจ๋ธ(์ปฌ๋ผยทํ์
ยทNULLยทPK/FKยทFK ๊ด๊ณ)์ meta_physical_models.json/topology_relations.json์ ๋ณํฉ(๋ฐฑ์
)ํ๊ณ ํซ๋ฆฌ๋ก๋. ๋ฌผ๋ฆฌ ์ฌ์ค์ ์๋ ๋ฐ์ํ๋ **๊ธฐ์กด ์ค๋ช
(์
๋ฌด ์๋ฏธ)์ ๋ณด์กด**, ์ญ์ ๋ถ์ `prune=true`๊ฐ ์๋๋ฉด ํ๊ธฐ ํ๋ณด๋ก๋ง ํ์. ์ค์ผ์ค๋ฌ `-sync-apply`๋ก ๋งค ์ฑํฌ ์ ์๋ ์คํ ๊ฐ๋ฅ
- `list_profile_catalogs` โ ๋ฑ๋ก๋ DB ํ๋กํ์ผ๋ณ **์นดํ๋ก๊ทธ ์ํฌ์คํ์ด์ค**(`/profiles//`) ์ ๋ฌดยทํ
์ด๋ธ/๊ด๊ณ ์ยท๊ตฌ์ถ ์๊ฐ ๋ชฉ๋ก. ํ๋กํ์ผ๋ง๋ค ๋
๋ฆฝ ๋ฉํ๋ฐ์ดํฐ JSON์ ์กฐํยท๊ด๋ฆฌ
- `get_profile_catalog` โ ํน์ ํ๋กํ์ผ ์ํฌ์คํ์ด์ค์ ์นดํ๋ก๊ทธ ์์ฝยท๋ฐ์ดํฐ์
์ธ๋ฒคํ ๋ฆฌยทํฌ์ค ์กฐํ
- `build_profile_catalog` โ **(๊ด๋ฆฌ์)** ํ๋กํ์ผ์ **๋ผ์ด๋ธ ์คํค๋ง๋ก ์ํฌ์คํ์ด์ค ๊ตฌ์ถ/๊ฐฑ์ **(๋ฌผ๋ฆฌ ๋ชจ๋ธ์ ํ๋กํ์ผ ๋๋ ํฐ๋ฆฌ์ ๊ธฐ๋ก, ๊ธฐ์กด ์ค๋ช
๋ณด์กดยท์ญ์ ๋ ํ๊ธฐ ํ๋ณด)
- `get_profile_dataset` / `put_profile_dataset` โ ํ๋กํ์ผ ์ํฌ์คํ์ด์ค์ ๊ฐ๋ณ ๋ฉํ๋ฐ์ดํฐ JSON(overridesยทglossaryยทphysical_models ๋ฑ) ์กฐํ / **(๊ด๋ฆฌ์)** ๊ฒ์ฆยท๋ฐฑ์
ยท๋กค๋ฐฑ๊ณผ ํจ๊ป ๊ด๋ฆฌ
- `build_all_profile_catalogs` โ **(๊ด๋ฆฌ์)** ๋ฑ๋ก๋ ๋ชจ๋ (๋๋ ์ ํ) ํ๋กํ์ผ์ ์ํฌ์คํ์ด์ค๋ฅผ ๋ผ์ด๋ธ DB์์ **์ผ๊ด ๊ตฌ์ถ/๊ฐฑ์ **. ํ๋กํ์ผ๋ณ ๊ถํ ํ์ธยท์คํจ๋ ๊ฐ๋ณ ๋ณด๊ณ (๋ฐฐ์น ์ค๋จ ์์). ๋ค์ DB ์จ๋ณด๋ฉ์ฉ
- `import_openmetadata_to_profile` โ **(๊ด๋ฆฌ์)** OpenMetadata์ ํ๋ ์ด์
๋ฉํ๋ฐ์ดํฐ(๋
ผ๋ฆฌ๋ช
ยท์ค๋ช
ยทPIIยท์ฉ์ด์ง)๋ฅผ ํน์ **ํ๋กํ์ผ ์ํฌ์คํ์ด์ค**๋ก import(์ ์ญ ์นดํ๋ก๊ทธ ์๋). ๊ฐ DB์ ์
๋ฌด ๋ฉํ๋ฐ์ดํฐ๋ฅผ ๊ทธ DB ์ํฌ์คํ์ด์ค์ ์๊ธ, ๋น ํ๋๋งยท๊ธฐ์กด๊ฐ ๋ณด์กด, `apply=false` ๋ฏธ๋ฆฌ๋ณด๊ธฐ
- `get_active_catalog` / `set_active_catalog` โ ํ์ฌ NL2SQL์ด ์ฐ๋ ์นดํ๋ก๊ทธ(๊ธฐ๋ณธ `-data` vs ํซ์ค์๋ ํ๋กํ์ผ ์ํฌ์คํ์ด์ค) ์กฐํ / **(๊ด๋ฆฌ์)** **๋ฌด์ฌ๊ธฐ๋ ์ ํ**. DB ํ๋กํ์ผยท๊ฐ์ฌยท์ํฌ์คํ์ด์ค๋ ์ด์ ๋๋ ํฐ๋ฆฌ์ ๊ณ ์ (์ ํ ์ํฅ ์์), ๋จ๋
๋ชจ๋ ์ ์ฉยท์ฌ๊ธฐ๋ ์ `-data`๋ก ๋ณต๊ท
- `get_sync_status` โ ์์ฒ๋ณ ์ ์ฅ๋ ์ค๋
์ ๋ชฉ๋ก(์ต์ ์, ์์ง์๊ฐยท์คํค๋งํด์ยท๊ฐ์ฒด์)
- `diff_metadata_snapshots` โ ๋ ์ค๋
์ ๊ฐ ๋ณ๊ฒฝ๋ถ(ํ
์ด๋ธ/์ปฌ๋ผ ์ถ๊ฐยท์ญ์ , ํ์
/Null/ํค/์ฝ๋ฉํธ/์ธ๋ฑ์ค/๋ทฐSQL ๋ณ๊ฒฝ, ๊ฐ๊ฐ ์ฌ๊ฐ๋ยท์ฒ๋ฆฌ๋ฐฉ์นจ) ๊ณ์ฐ
- `profile_metadata_assets` โ ์ปฌ๋ผ ํต๊ณ(ํ์ยทNull๋น์จยทdistinctยทmin/maxยท์์๊ฐยทํฌ๋งทํจํด)๋ฅผ ๋น์ฉ ์ ์ด(๋ชจ๋๋ณ ์ํ: fast 2k / standard 100k / deep ์ ์ฒด)ยท**๊ฐ์ธ์ ๋ณด ๋ณดํธํ**(๋ฏผ๊ฐ ์ปฌ๋ผ์ ์๋ณธ๊ฐยทmin/maxยท์์๊ฐ ๋ฏธ์ ์ฅ, ๊ธธ์ดยทํจํดยท๊ฑด์๋ง)์ผ๋ก ๊ณ์ฐ. ๊ฒฐ๊ณผ๋ ๊ฒํ ํ๋ณด์ด๋ฉฐ ์ด์ ์นดํ๋ก๊ทธ(column_stats)์ ์๋ ๋ฐ์ํ์ง ์์
- `record_feedback` โ ์ง๋ฌธ/๋ถ์/ํ๋ณด/SQL/๊ฒ์ฆ์ค๋ฅ/์ฑํ์ฌ๋ถ/์คํ์๊ฐ์ ์๋ฒ๊ฐ ๋ถ์ฌํ actor/session/dataset ๋ฒ์์ ํจ๊ป `pending/untrusted` ๊ฒํ ํ์ ์ ์ฅ; ์น์ธ ์ ์๋ ๊ฒ์ยทํ๋กฌํํธยทํ์ต์ ์ฌ์ฉํ์ง ์์
- `review_feedback` โ **๊ด๋ฆฌ์ ์ ์ฉ** ํผ๋๋ฐฑ ๊ฒํ ํ ์กฐํ ๋ฐ approve/reject; ์น์ธ๋ ๋ ์ฝ๋๋ง trusted ์ํ๋ก few-shotยท๊ฒ์ ๋ถ์คํธยทํ์ต ๋ฃฐ์ ์ฌ์ฉ
- `list_datasets` / `get_dataset` โ ์๋ฒ๊ฐ ์ฐธ์กฐํ๋ ๋ชจ๋ JSON ๋ฐ์ดํฐ์
์ ๋ผ์ด๋ธ ๋ ์ง์คํธ๋ฆฌ: ์ฉ๋, ์คํค๋ง, ์ฌ์ฉ ๋๊ตฌ, ํ์/ํธ์ง๊ฐ๋ฅ ์ฌ๋ถ, ํ์ฌ ์ํ(์กด์ฌยทํฌ๊ธฐยท๋ก๋ ๊ฑด์ยท๋ก๋ ์ด์)์ ๋ด์ฉ ์ํ
- `put_dataset` โ ๋ฐ์ดํฐ์
๊ต์ฒด: JSON ํํ ๊ฒ์ฆ โ ๊ธฐ์กด ํ์ผ ๋ฐฑ์
(`backups/`) โ ์ฐ๊ธฐ โ ์นดํ๋ก๊ทธ ์ฌ์ปดํ์ผ โ **ํซ์ค์**(์ฌ๊ธฐ๋ ๋ถํ์). ์ปดํ์ผ ์คํจ๋ ์ ๊ท ์ค๋ฅ ๋ฐ์ ์ ์๋ ๋กค๋ฐฑ(`force`๋ก ๊ฐ์ ์ ์ฉ ๊ฐ๋ฅ)
- `remove_dataset` โ ์ ํ ๋ฐ์ดํฐ์
์ ๊ฑฐ(๋ฐฑ์
ํ) + ํซ์ค์. ํ์(`physical_models`, `logical_models`)ยท์์คํ
๊ด๋ฆฌ(`feedback`, `audit`) ๋์์ ๊ฑฐ๋ถ
- `reload_catalog` โ ๋์คํฌ ํ์ผ์ ์ง์ ์์ ํ ๊ฒฝ์ฐ(๋ณผ๋ฅจ ๋ง์ดํธ ๋ฑ) ์ฌ์ปดํ์ผ + ํซ์ค์
- `get_catalog_health` โ ๋ฉํ ์ปดํ์ผ ๊ฒ์ฆ ๊ฒฐ๊ณผ(์ค๋ฅ/๊ฒฝ๊ณ ), ์ปค๋ฒ๋ฆฌ์ง ๊ฐญ, PII ๋ชฉ๋ก
- `get_metadata_quality` โ ํ
์ด๋ธ๋ณ ๋ฉํ๋ฐ์ดํฐ ํ์ง ์ ์(์์ ์ฑยท์ผ๊ด์ฑยท๊ด๊ณ์ฑยทํ๋กํ์ผ๋งยท์งํ์ฐ๊ฒฐยท์ฌ์ฉ์ฑยท๋ณด์์ฑ) 0โ100 + ๋ฑ๊ธ AโE, ์คํค๋ง/๋๋ฉ์ธ ์ง๊ณ, ๊ฐ์ ๋์. `gate=true`๋ฉด ๋ฆด๋ฆฌ์ค ์ฐจ๋จ ์กฐ๊ฑด(๋ก๋ ์ค๋ฅยท์งํ/์ธ์ฆ์กฐ์ธ ์์ยทPII ๋ฏธ๋ถ๋ฅยทํ์ง ํํ ๋ฏธ๋ฌ) ํ๊ฐ๋ก ์ ํ
- `suggest_semantic_metadata` โ ๋
ผ๋ฆฌ๋ช
ยท์๋ฏธํ์
ยท์ค๋ช
์ด ์๋ ์ปฌ๋ผ์ ๋ํด ๊ท์น ๊ธฐ๋ฐ(์ฉ์ด์งยท๋์ผ์ปฌ๋ผ ์ฌ์ฌ์ฉยท์ฝ์ด ํ์ฅยท์ด๋ฆ/ํ์
ํจํด, ์คํ๋ผ์ธ)์ผ๋ก **๊ฒํ ํ๋ณด**๋ฅผ ๊ทผ๊ฑฐยท์ ๋ขฐ๋์ ํจ๊ป ์์ฑ. ๊ณ ์ ๋ขฐ ํญ๋ชฉ์ overrides.json columns[] ์ค๋ํซ์ผ๋ก ๋ฐํ. ์ด์ ์นดํ๋ก๊ทธ์ ์๋ ๋ฐ์ํ์ง ์์ผ๋ฉฐ LLM/๋ด๋น์๊ฐ ๋ค๋ฌ์ด ์น์ธ
- `suggest_model_candidates` โ ๊ท์น ๊ธฐ๋ฐ **๋ชจ๋ธ ํ๋ณด** ์์ฑ: ์ฝ๋์ฌ์ (์ ์นด๋๋๋ฆฌํฐ ์ฝ๋ ์ปฌ๋ผ์ ํ๋กํ์ผ top-value๋ก ์ค์ผ๋ ํค), ์งํ(AMOUNT/COUNT/RATIO/SCORE ์ปฌ๋ผโSUM/AVG ์ง๊ณ ์งํ), ๊ด๊ณ(์๋ณ์ ์ด๋ฆ+PK๋ช
/ํ
์ด๋ธ๋ช
๋งค์นญ+ํ์
ํธํ์ผ๋ก FK ์ถ๋ก ). ๊ทผ๊ฑฐยท์ ๋ขฐ๋ ๋๋ฐ, ์ด์ ์นดํ๋ก๊ทธ ์๋ ๋ฏธ๋ฐ์
- `analyze_impact` โ ํ
์ด๋ธ/์ปฌ๋ผ ๋ณ๊ฒฝยทํ๊ธฐ ์ **๊ณ๋ณด/์ํฅ๋** ์ถ์ : ํด๋น ์์ฐ์ ์์กดํ๋ ์งํยท๊ด๊ณยท์ ํธ/๊ธ์ง ์กฐ์ธยท๊ณจ๋ ์
ยท์ค๋ฒ๋ผ์ด๋ยท์ฉ์ด์งยท1ํ ํ์ ํ
์ด๋ธ์ ์ญ์ถ์ ํ๊ณ impact_level(์งํ/์ ํธ์กฐ์ธ ์์กด ์ high) ์ฐ์ถ. ์นดํ๋ก๊ทธ ์ฝ๊ธฐ ์ ์ฉ ๋ถ์
- `review_candidates` โ ์๋ฏธ๋ณด๊ฐยท๋ชจ๋ธ ํ๋ณด๋ฅผ ์ ์ฅ๋ ์น์ธ/๋ฐ๋ ค ๊ฒฐ์ ๊ณผ ์กฐ์ธํด **๊ฒํ ํ**๋ก ์กฐํ(์ํ pending/approved/rejected ํํฐ). ๊ฐ ํญ๋ชฉ์ ์์ ์ id ๋ถ์ฌ. ์ฌ๋ ๊ฐ์
๊ฒ์ดํธ
- `decide_candidates` โ ํ๋ณด๋ฅผ id๋ก **์น์ธ/๋ฐ๋ ค**. ๊ฒํ ์ยท์๊ฐยท๋ฉ๋ชจ์ ํจ๊ป ์์ ์ ์ฅ(`/reviews/decisions.json`). ์นดํ๋ก๊ทธ ์๋ ๋ฏธ๋ฐ์
- `get_metadata_digest` โ ์นดํ๋ก๊ทธ ์ด์ ์ํ **์์ฝ ์ค๋
์**: ํ์ง ์ ์ยท๋ฆด๋ฆฌ์ค ๊ฒ์ดํธ, ๊ฒํ ํ ๋ฐฑ๋ก๊ทธ(๋๊ธฐ/์น์ธ/๋ฐ๋ ค), ๊ณจ๋ ์น๊ฒฉ ํ๋ณด ์, ์นดํ๋ก๊ทธ ๊ท๋ชจยท๋ก๋ ๊ฒฝ๊ณ + ํ ์ค ํค๋๋ผ์ธ. ์ผ์ผ ์ ๊ฒยท์๋ฆผ์ฉ
- `openmetadata_status` โ ์ค์ ๋ OpenMetadata ์๋ฒ ์ฐ๊ฒฐยท์ธ์ฆยท๋ฒ์ ํ์ธ
- `import_openmetadata` โ OpenMetadata์ ํ๋ ์ด์
๋ฉํ๋ฐ์ดํฐ(ํ
์ด๋ธ/์ปฌ๋ผ displayNameโ๋
ผ๋ฆฌ๋ช
, ์ค๋ช
, PII ํ๊ทธโpii/semantic_type, ์ฉ์ด์ง)๋ฅผ jamypg **๋น ํ๋์๋ง** ํ๋ณด๋ก ๊ฐ์ ธ์ค๊ธฐ. `apply=false` ๋ฏธ๋ฆฌ๋ณด๊ธฐ(๊ธฐ๋ณธ), `apply=true` overrides.json/glossary.json ๋ณํฉ+๋ฆฌ๋ก๋(๊ด๋ฆฌ์, ๋ฐฑ์
ยท์๊ธฐ๊ฐ ๋ณดํธ)
- `export_to_openmetadata` โ jamypg ์ปฌ๋ผ ์ค๋ช
(๋ช
์์ ๋๋ ๋
ผ๋ฆฌ๋ช
์กฐํฉ)์ OpenMetadata์ **๋น ์ค๋ช
์ปฌ๋ผ์๋ง** JSON-Patch๋ก push. `dry_run=true` ๊ณํ๋ง(๊ธฐ๋ณธ), `dry_run=false` ์ค์ ๋ฐ์(๊ด๋ฆฌ์)
- `openmetadata_drift` โ jamypg โ OpenMetadata **๋์กฐ(reconciliation)** ๋ฆฌํฌํธ: ๋
ผ๋ฆฌ๋ช
ยท์ค๋ช
ยทPII๋ฅผ `jamypg_gap`(import ํ๋ณด)ยท`conflict`(๊ฐ ๋ถ์ผ์น, ์ฌ๋ ๊ฒฐ์ )ยท`ext_gap`(export ํ๋ณด)์ผ๋ก ๋ถ๋ฅ. ์ฝ๊ธฐ ์ ์ฉ ๊ฑฐ๋ฒ๋์ค ๋๊ตฌ
- `export_lineage_to_openmetadata` โ jamypg ๊ด๊ณ ๊ทธ๋ํ๋ฅผ OpenMetadata **ํ
์ด๋ธ lineage ์ฃ์ง**๋ก push(from=์ฐธ์กฐ/๋ถ๋ชจ, to=๊ธฐ์ค/์์). FK ๊ด๊ณํ lineage ๋งคํ(ETL ํ๋ฆ ์๋). `dry_run=true` ๊ณํ(๊ธฐ๋ณธ)/`false` ๋ฐ์(๊ด๋ฆฌ์), OM์ ์๋ ํ
์ด๋ธ ์ฃ์ง๋ skip ๋ณด๊ณ
- `get_approved_overrides` โ ์น์ธ๋ ํ๋ณด๋ฅผ ๋ชฉ์ ํ์ผ๋ณ(overrides.json columns[], metrics.json, relations.json, ์ฝ๋์ฌ์ ) **์ ์ฉ ์ค๋ํซ**์ผ๋ก ์ปดํ์ผ
- `apply_approved_candidates` โ **์ํด๋ฆญ ๋ฐ์**: ์น์ธ-๋ฏธ๋ฐ์ ํ๋ณด๋ฅผ ๋ฐ์ดํฐ์
ํ์ผ 4์ข
์ ํ์ผ๋ณ ๋ฐฑ์
ํ ๋ณํฉํ๊ณ ์นดํ๋ก๊ทธ ํซ๋ฆฌ๋ก๋. ๋ฉฑ๋ฑ(applied_at ์คํฌํ+๋ด์ฉ ์ค๋ณต ์ ๊ฑฐ), ์ด์์ ์๊ธฐ ๊ฐ์ ๋ฎ์ด์ฐ์ง ์์. ๊ด๋ฆฌ์ ์ ์ฉ
- `run_evaluation` โ golden query set ํ๊ฐ(ํ
์ด๋ธ/์ปฌ๋ผ/์งํ/์กฐ์ธ/SQL ์ ํจ์ฑ ์ ํ๋, ํ๊ท ์๋ต์๊ฐ)
- `learn_from_feedback` โ ๋ฐ๋ณต ์คํจ ํจํด์ learned rule๋ก ์น๊ฒฉ: ๋์ผ ๊ฒ์ฆ์ค๋ฅ ๋ฐ๋ณต(์๋ฐฉ ๊ฒฝ๊ณ ), ํ
์ด๋ธ ์ค์ ํ ๊ต์ (๊ฒ์ ํจ๋ํฐ), ์ปฌ๋ผ ๊ต์ (validate_sql ๊ฒฝ๊ณ ). `learned_rules.json`์ ์์ํ๋์ด ์ด์์๊ฐ ๊ฒํ /์์ ๊ฐ๋ฅ
- `suggest_golden_from_feedback` โ ์น์ธยท์ฑ๊ณตยท์คํ๋ ํผ๋๋ฐฑ์ **๊ณจ๋ ์
ํ๋ณด**๋ก ์ ์(์ง๋ฌธ/๊ธฐ๋ SQLยทํ
์ด๋ธยท์ปฌ๋ผ, ์ง๋ฌธ/SQL ์ ๊ทํ๋ก ๊ธฐ์กด ๊ณจ๋ ์
์ค๋ณต ์ ์ธ). trust ๊ฒฝ๊ณ ์น์ธ๋ถ๋ง ๋์(fail-closed)
- `promote_golden_queries` โ ์ ํ ํ๋ณด(feedback_id)๋ฅผ `golden_queries.json`์ ๋ฐฑ์
ํ ์ถ๊ฐํ๊ณ ์นดํ๋ก๊ทธ ๋ฆฌ๋ก๋. ์ด์ ํธ๋ํฝ์ผ๋ก ํ๊ฐ์
์ ์ฑ์ฅ์ํค๋ ๋ช
์์ ๊ด๋ฆฌ์ ํ์. ๊ด๋ฆฌ์ ์ ์ฉ
`run_sql_safely` validates SQL and, when a DB profile is supplied, executes it
read-only against the target database (postgres/mysql/mariadb) with query
timeout, row limit, and audit logging โ drivers are always compiled in.
Without a profile it stays a dry-run guard returning bounded SQL. See
`docs/db-connector.md`. Start most questions with `prepare_sql_context`,
which runs the whole analyzeโskeleton pipeline in one call.
## Web Admin Console & REST API
HTTP ๋ชจ๋๋ก ๊ธฐ๋ํ๋ฉด ๋ธ๋ผ์ฐ์ ๊ธฐ๋ฐ ๊ด๋ฆฌ ํ๋ฉด๊ณผ Swagger ๋ฌธ์๊ฐ ํจ๊ป ์ ๊ณต๋ฉ๋๋ค.
| ๊ฒฝ๋ก | ๋ด์ฉ |
| --- | --- |
| `/admin` | **๋ฐ์ดํฐ์
๊ด๋ฆฌ ์ฝ์** โ 18๊ฐ ๋ฐ์ดํฐ์
์ ์ฉ๋ยท์คํค๋งยท์ํ ํ์ธ, ๋ด์ฉ ํธ์งยท์ ์ฉ(๋ฐฑ์
+๊ฒ์ฆ+ํซ์ค์), ์ ๊ฑฐ, ๋ฐฑ์
/๋ณต์, ์นดํ๋ก๊ทธ ๋ฆฌ๋ก๋. ๋จ๊ณ๋ณ ์ฌ์ฉ ๊ฐ์ด๋๊ฐ ํ๋ฉด์ ๋ด์ฅ |
| `/admin/editor` | **ํ
์ด๋ธ ํธ์ง๊ธฐ** โ ๋ฐ์ดํฐ์
์ ํ(๊ทธ๋ฆฌ๋)๋ก ๋ ๋๋งํด JSON ์์ด ํธ์ง: ์
ํด๋ฆญ ์ธ๋ผ์ธ ์์ (ํ์
์๋ ๋ณด์กด), ํ ์ถ๊ฐ/๋ณต์ /์ญ์ , **์ปฌ๋ผ ์ถ๊ฐ/์ด๋ฆ๋ณ๊ฒฝ/์ญ์ **, ๊ฒ์ยทํ์ด์ง๋ค์ด์
. ์ ์ฅ ์ ๋์ผํ ๋ฐฑ์
ยท๊ฒ์ฆยทํซ์ค์ยท๋กค๋ฐฑ ์ ์ฉ |
| `/admin/db` | **DB ์ฐ๊ฒฐ ๊ด๋ฆฌยท์ฟผ๋ฆฌ ์คํ** โ postgres/mysql/mariadb ํ๋กํ์ผ ์ถ๊ฐ/์์ /์ญ์ /์ ์ ํ
์คํธ, Read-Only ์ฟผ๋ฆฌ ์ฝ์(๊ฒ์ฆโ๋ฏธ๋ฆฌ๋ณด๊ธฐโ์คํโ์ทจ์), ์คํ ์ด๋ ฅยท๋ฉํธ๋ฆญ ([docs/db-connector.md](docs/db-connector.md)) |
| `/admin/dba` | **DBA ์ฝํ์ผ๋ฟ** โ ์ฝ๊ธฐ ์ ์ฉ DBA ์ง๋จ ๋์๋ณด๋: ํฌ์ค ์ ๊ฒ, ์ธ๋ฑ์ค ์ด๋๋ฐ์ด์ (CREATE INDEX ํ๋ณด), ์ํฌ๋ก๋ ๋ฆฌํฌํธ, SQL ์ํฐํจํด ๋ฆฐํธ, SQL ์์ฐ์ด ์ค๋ช
์ ํญ UI๋ก ์ ๊ณต(์๋ ์คํยท๋ณ๊ฒฝ ์์, ๊ถ๊ณ ์ฉ) |
| `/admin/dba-console` | **DBA ๊ด๋ฆฌ ์ฝ์** (`dba`/`admin` ์ญํ ์ ์ฉ) โ ๊ถํ ์๋ ์ฐ๊ธฐ ์ธ์
์ผ๋ก ์ฌ์ฉ์ยท์ญํ , ๋ฐ์ดํฐ๋ฒ ์ด์ค, ๊ถํ(GRANT/REVOKE), ์๋ฒ ์ค์ , ์ธ์
(์ทจ์/์ข
๋ฃ), ์ ์ง๋ณด์(VACUUM/ANALYZE/REINDEX), ์์ ๊ถํ SQL์ ํญ UI๋ก ๊ด๋ฆฌ. ํ๋กํ์ผ์ `dba` ์๊ฒฉ์ฆ๋ช
ํ์, ๋ชจ๋ ๋ณ๊ฒฝ ๊ฐ์ฌ ๋ก๊ทธ ๊ธฐ๋ก |
| `/auth/login` ยท `/admin/users` ยท `/admin/keys` | **์ธ์ฆยท์ฌ์ฉ์ยทMCP ํค** (๋ฉํ DB ํ์ฑ ์) โ ๋ก์ปฌ/Keycloak SSO ๋ก๊ทธ์ธ, ์ฌ์ฉ์ยท์ญํ ๊ด๋ฆฌ(admin), MCP ํค ๋ฐ๊ธยทํ์ ยทํ๊ธฐ, ํ๋กํ์ผ๋ณ ๊ถํ(grant). ์์ธ: [docs/auth.md](docs/auth.md) |
| `/docs` | **Swagger UI** โ REST API ๋ฌธ์ + Try it out (์คํ๋ผ์ธ ๋์, ์์ฐ ์๋ฒ ๋) |
| `/openapi.json` | OpenAPI 3.0 ์คํ |
| `/api/*` | REST API: `GET /api/datasets`, `GET/PUT/DELETE /api/datasets/{name}`, `GET .../content`, `GET .../backups`, `POST .../restore`, `POST /api/reload`, `GET /api/health` |
๋ณ๊ฒฝ API ๋ณดํธ: `-admin-token <๊ฐ>` ํ๋๊ทธ(๋๋ `JAMYPG_ADMIN_TOKEN` ํ๊ฒฝ๋ณ์)๋ฅผ
์ค์ ํ๋ฉด PUT/DELETE/POST์ `X-Admin-Token` ํค๋๊ฐ ํ์ํฉ๋๋ค. ๋ฏธ์ค์ ์ ์ธ์ฆ
์์ด ํธ์ถ ๊ฐ๋ฅํ๋ฏ๋ก ๋ด๋ถ๋ง ์ธ ๋
ธ์ถ ์ ๋ฐ๋์ ์ค์ ํ์ธ์. ๋ชจ๋ ๋ณ๊ฒฝ์
`audit/*.jsonl`์ ๊ธฐ๋ก๋๊ณ , REST์ MCP ๋๊ตฌ(`put_dataset` ๋ฑ)๋ ๋์ผํ
๊ฒ์ฆยท๋ฐฑ์
ยท๋กค๋ฐฑ ์ฝ๋๋ฅผ ๊ณต์ ํฉ๋๋ค.
๋จ๋
HTTP ๋ชจ๋๋ ๊ธฐ๋ณธ์ ์ผ๋ก loopback ์ฃผ์๋ง ํ์ฉํฉ๋๋ค. `0.0.0.0`, `::`,
์ธํฐํ์ด์ค IP ๋๋ hostname์ ๋ฐ์ธ๋ฉํ๋ ค๋ฉด ์ ๋ฉด ์ธ์ฆ์ ์ ๊ณตํ๋ `-meta-db`๋ฅผ
๊ตฌ์ฑํ๊ฑฐ๋ `-public-mcp`๋ก ๊ณต๊ฐ ๋
ธ์ถ์ ๋ช
์์ ์ผ๋ก ์น์ธํ๊ณ `-admin-token`๋
์ค์ ํด์ผ ํฉ๋๋ค.
`-admin-token`์ ๋ณ๊ฒฝยทDB ์คํ ๋๊ตฌ๋ฅผ ๋ณดํธํ์ง๋ง ๋ชจ๋ ์ฝ๊ธฐ ์ ์ฉ MCP ๋๊ตฌ์
๋ก๊ทธ์ธ์ ๊ฐ์ ํ์ง ์์ผ๋ฏ๋ก, ์ธํฐ๋ท ๋
ธ์ถ์๋ `-meta-db` ์ธ์ฆ์ ์ฌ์ฉํ์ธ์.
ํผ๋๋ฐฑ์ workspace๋ณ๋ก ๊ฒฉ๋ฆฌํ๋ ค๋ฉด `-feedback-tenant` ๋๋
`JAMYPG_FEEDBACK_TENANT`๋ฅผ ์๋ฒ๊ฐ ๊ด๋ฆฌํ๋ ๊ณ ์ ๊ฐ์ผ๋ก ์ค์ ํ์ธ์.
## Authentication (optional, Postgres meta DB)
`-meta-db `(๋๋ `JAMYPG_META_DB`)๋ฅผ ์ง์ ํ๋ฉด ์ ๋ฉด ์ธ์ฆ์ด
ํ์ฑํ๋ฉ๋๋ค. ๋ฏธ์ง์ ์ ๊ธฐ์กด ๋จ๋
๋ชจ๋ ๊ทธ๋๋ก ๋์ํฉ๋๋ค(ํ์ ํธํ).
```sh
jamypg-mcp -transport http -addr 0.0.0.0:9797 \
-meta-db 'postgres://jamypg:pw@pg:5432/jamypg?sslmode=require' \
-bootstrap-admin 'admin:์ฒซ๊ด๋ฆฌ์๋น๋ฐ๋ฒํธ'
```
- **๋ก๊ทธ์ธ**: ๋ก์ปฌ ๊ณ์ (bcrypt) + ์ธ์
์ฟ ํค, ๋๋ Keycloak **SSO(OIDC)**
(`-oidc-issuer/-oidc-client-id/-oidc-client-secret/-oidc-redirect-url`)
- **์ญํ **: `admin`(์ ๊ถ) / `user`. ๊ด๋ฆฌ์๋ ์ฌ์ฉ์ยท๋ฐ์ดํฐ์
ยท์ ์ฒด ํ๋กํ์ผยท
์ ์ฒด ํค ๊ด๋ฆฌ
- **MCP ํค**: `/mcp` ์ ๊ทผ์ฉ `jsk_...` ํค๋ฅผ ๋ฐ๊ธยทํ์ ยทํ๊ธฐ(`/admin/keys`).
ํด๋ผ์ด์ธํธ๋ `Authorization: Bearer jsk_...` ๋๋ `X-MCP-Key`๋ก ์ ์
- **DB ํ๋กํ์ผ ๊ถํ**: ์ฌ์ฉ์๋ณ ์์ + `use`/`manage` grant + `shared`
๊ณต๊ฐ. Postgres์ ์ ์ฅ๋์ด ์ฌ์ฉ์๋ง๋ค ์ ๊ทผ ๋ฒ์๊ฐ ๋ค๋ฆ
- ์ฒซ ๊ธฐ๋ ์ ๋ถํธ์คํธ๋ฉ ๊ด๋ฆฌ์๋ฅผ ์์ฑ(๋น๋ฐ๋ฒํธ ๋ฏธ์ง์ ์ ๋ก๊ทธ์ 1ํ ์ถ๋ ฅ)
- **์๋ฒ ์ค์ ๊ด๋ฆฌ**: ๋ง์คํฐ ํ ํฐยทํ์ฉ OriginยทKeycloak SSO๋ฅผ `/admin/settings`
์์ ๋ฉํ DB์ ์ ์ฅํ๊ณ **์ฌ๊ธฐ๋ ์์ด ์ฆ์ ์ ์ฉ**(ํ๋๊ทธ/env๋ ๊ธฐ๋ณธ๊ฐ)
- **๋ฐ์ดํฐ์
๋ ๋ฉํ DB์์ ๊ด๋ฆฌ**: ํธ์ง ๊ฐ๋ฅํ ์นดํ๋ก๊ทธ JSON 14์ข
์ ์ง์ค
์๋ณธ์ด Postgres(`jamypg_datasets`)๊ฐ ๋์ด `/admin`ยทMCP ๋๊ตฌ ํธ์ง์ด DB์
์์ํ๋จ(๋ก๋ ์ ํ์ผ๋ก materializeํด ๊ธฐ์กด ๋ก๋ ์ฌ์ฌ์ฉ)
- **MCP `list_db_profiles`**: LLM์ด ์ฌ์ฉ ๊ฐ๋ฅํ DB ํ๋กํ์ผ id๋ฅผ ๋ฐ๊ฒฌ
๋ฉํ DB ๋๋ผ์ด๋ฒ๋ ์์ Go(pgx)๋ผ CGO/์ธ๋ถ ํด๋ผ์ด์ธํธ๊ฐ ํ์ ์์ต๋๋ค. ์์ธ:
[docs/auth.md](docs/auth.md).
## Operator-Managed Data Files (dataset dir)
| ํ์ผ | ์ฉ๋ |
| --- | --- |
| `glossary.json` | ์
๋ฌด ์ฉ์ด/๋์์ด ์ฌ์ (๊ฒ์ยท์ง๋ฌธ๋ถํดยทSQL์์ฑยท๊ฒ์ฆ ๊ณต์ฉ) |
| `metrics.json` | ์งํ ์ฌ์ : expression, ์ง๊ณ, grain, ํ์ ํํฐ, ์์ SQL |
| `overrides.json` | ์ด์์ ๋ณด์ : ์ค๋ช
/๋๋ฉ์ธ/grain, ์ปฌ๋ผ ๋์์ดยท์ํ๊ฐ, PII ์ง์ , ๊ธ์ง/๊ถ์ฅ ์กฐ์ธ, ๊ตฌ์กฐ ๊ฒ์ฆ ๊ธฐ๋ณธ ํํฐ(`enforcement: warn|error`), dialect(postgres/mysql/mariadb) |
| `databases.json` | ๋์ DB ์ ๋ณด โ `dbms`(POSTGRES/MYSQL/MARIADB)๊ฐ ์์ฑ SQL ๋ฐฉ์ธ ๊ฒฐ์ |
| `db_profiles.json` | ์คํ์ฉ DB ์ ์ ํ๋กํ์ผ (type/connect_string/password_ref/pool/policy) |
| `column_stats.json` | ์ปฌ๋ผ ํ๋กํ์ผ ํต๊ณ (์ ํ; row count, null ๋น์จ, top values, ์ต์ ์ฑ) |
| `patterns.json` | ๋ค๋จ๊ณ SQL ํจํด ์ฌ์ (2๋จ ์ง๊ณ, ๊ทธ๋ฃน๋ณ top-N, ์ ์/์ ๋
๋๋น, ๋น์จ, ๋ถํฌ) โ ๋ฏธ์กด์ฌ ์ ๋ด์ฅ ๊ธฐ๋ณธ๊ฐ ์ฌ์ฉ (๋ฐฉ์ธ์ ๋ง๊ฒ ์๋ ์นํ) |
| `golden_queries.json` | ํ๊ฐ์ฉ golden query set โ ์์์
์ผ์ด์ค + `jamypg-goldgen` ์๋ ์ ๋ณ (CI์์ `go test ./...`๋ก ์๋ ์คํ) |
| `learned_rules.json` | `learn_from_feedback`๊ฐ ์น๊ฒฉํ ํ์ต ๋ฃฐ (์ด์์ ๊ฒํ /์์ /์ญ์ ๊ฐ๋ฅ) |
| `feedback/*.jsonl` | record_feedback ๊ฒํ ํ (๊ด๋ฆฌ์๊ฐ ์น์ธํ trusted ๋ ์ฝ๋๋ง ์ฑ๊ณต SQL ํ์ตยท๋ฃฐ ์น๊ฒฉ์ ์ฌ์ฌ์ฉ) |
| `audit/*.jsonl` | ๋ชจ๋ tool call ๊ฐ์ฌ ๋ก๊ทธ (์๋ ๊ธฐ๋ก, git ์ ์ธ) |
## Evaluation
```sh
go test ./... # golden set ํฌํจ ์ ์ฒด ํ
์คํธ (CI)
go run ./cmd/jamypg-eval -verbose # ํ๊ฐ๋ง ์คํ, ์ผ์ด์ค๋ณ ๋ฏธ์ค ์ถ๋ ฅ
go run ./cmd/jamypg-eval -data data/metadb -profile pg-meta
# ์คํ ๊ธฐ๋ฐ ํ๊ฐ (์ค์ DB์ COUNT ๊ฒ์ฆ)
go run ./cmd/jamypg-goldgen -n 80 # sql_datasets์์ golden set ์ฌ์์ฑ
# (๋๋ฉ์ธ x ๋์ด๋ ์ธตํ, ์นดํ๋ก๊ทธ ๊ฒ์ฆ ํต๊ณผ ์ผ์ด์ค๋ง,
# ๊ธฐ์กด ํ์ผ ์๋จ ์์์
์ผ์ด์ค๋ -keep ๊ฐ์๋งํผ ๋ณด์กด)
```
์ธก์ ํญ๋ชฉ: table_selection_acc, column_recall_avg, metric_lookup_acc,
join_path_acc, expected_sql_valid, avg_response_ms (+ `-profile` ์
execution_success_rate, row_sanity_rate).
data/metadb 8์ผ์ด์ค(3๊ฐ DB ์ค์ธก): table 1.0 / join 1.0 / sql 1.0 / ์คํ ์ฑ๊ณต๋ฅ 1.0.
OSS ๋ฐ์ดํฐ์
(sakila/northwind/wordpress) 17์ผ์ด์ค: 3๊ฐ ์์ง์์ ๋์ผ ์ ๋ต ๊ฒ์ฆ ํต๊ณผ.
## Feedback Learning Loop
1. ํด๋ผ์ด์ธํธ๊ฐ `record_feedback`์ผ๋ก ์ง๋ฌธ/SQL/๊ฒ์ฆ์ค๋ฅ/๊ต์ ๋ณธ/์ฑํ์ฌ๋ถ๋ฅผ ๊ฒํ ํ์ ์ ์ฅ
2. ๊ด๋ฆฌ์๊ฐ `review_feedback`์ผ๋ก ๋ด์ฉ๊ณผ ๋ฒ์๋ฅผ ํ์ธํด approve/reject
3. ์น์ธ๋ trusted ์ฑ๊ณตยท๊ต์ SQL๋ง ์ฆ์ few-shot ์์ ์ ๊ฒ์ ๋ถ์คํธ์ ๋ฐ์
4. `learn_from_feedback` ํธ์ถ(๋๋ ์ฃผ๊ธฐ ์คํ) ์ ์น์ธ๋ ํผ๋๋ฐฑ์ ๋ฐ๋ณต ํจํด์ ๋ฃฐ๋ก ์น๊ฒฉ:
- `recurring_error` โ ๊ฐ์ ๊ฒ์ฆ ์ค๋ฅ๊ฐ Nํ ์ด์ โ ํด๋น ํ
์ด๋ธ/์ปฌ๋ผ ์ฌ์ฉ ์ ์๋ฐฉ ๊ฒฝ๊ณ
- `table_correction` โ ๊ต์ ์์ ๋ฐ๋ณต์ ์ผ๋ก ๊ต์ฒด๋ ํ
์ด๋ธ โ ๊ฒ์ ์ ์ ํจ๋ํฐ + ๊ฒฝ๊ณ
- `column_correction` โ ๋ฐ๋ณต ๊ต์ฒด๋ ์ปฌ๋ผ โ validate_sql์ด ๋์ฒด ์ปฌ๋ผ ํํธ ์ ์
- `slow_query` / `recurring_exec_error` โ ์คํ ๊ฐ์ฌ ๋ก๊ทธ์์ ๋ฐ๋ณต ์ง์ฐยท์ค๋ฅ(PG-*/MY-*/TIMEOUT) ์น๊ฒฉ
5. ๋ฃฐ์ `learned_rules.json`์ผ๋ก ์์ํ; ์๋ฒ ์ฌ๊ธฐ๋ ์ ์๋ ์ ์ฉ, ์ด์์๊ฐ ์ง์ ํธ์ง ๊ฐ๋ฅ
## SQL Generation Flow
1. `analyze_question` โ ๋ชจํธ์ฑ ํ์ธ (๊ธฐ๋ณธ๊ฐ ์ ์ฉ ์ ๊ฐ์ ํ์), ํจํดยทintent ์๊ทธ๋์ฒ ํ๋ณด
2. `search_schema` (+`find_filter_columns`, `resolve_time`)
3. `get_metric_definition` โ ์
๋ฌด ์งํ๋ ์ฌ์ expression๋ง ์ฌ์ฉ
4. `get_schema_context` โ ์์ถ ์ปจํ
์คํธ๋ง LLM์ ์ ๋ฌ
5. `get_join_paths` โ ON ์กฐ๊ฑด์ ๋ฐ๋์ ์ฌ๊ธฐ์ ์ทจ๋; ๊ฒฝ๋ก ์์/์ ์ ๋ขฐ ์ ๋๋ฌป๊ธฐ
6. ๋ณต์ก/๋ค์ค ํ
์ด๋ธ ์ง๋ฌธ์ด๋ฉด `build_sql_skeleton`์ผ๋ก ๊ณจ๊ฒฉ ํ๋ณด ํ SLOT๋ง ์ฑ์; ๋จ์ ์ง๋ฌธ์ ์ง์ ์์ฑ (์ปจํ
์คํธ ๋ด ์๋ณ์๋ง, PII ๊ธ์ง, row bound(LIMIT) ํ์)
7. `validate_sql` (`expected_outputs`, `metrics` ์ ๋ฌ) โ fix_hints ๋ฐ์ ์ต๋ 2ํ ์ฌ์๋; ์คํจ SQL ์คํ ๊ธ์ง. ๋์ด๋ ๋์ ์ง๋ฌธ์ ํ๋ณด 2~3๊ฐ๋ฅผ ๋ง๋ค์ด `rank_candidates`๋ก ์ต์ ์ ์ ํ
8. `explain_sql` โ risk=high๋ฉด ๊ธฐ๊ฐ/limit ์กฐ๊ฑด ์ถ๊ฐ ํ ์ฌ์์ฑ (`profile` ์ง์ ์ ์ค์ธก EXPLAIN)
9. ๊ตฌ์กฐํ JSON ์๋ต (sql, ์ฌ์ฉ ํ
์ด๋ธ/์ปฌ๋ผ, ์งํ, ์กฐ์ธ, ํํฐ, ๊ฐ์ , ์ฃผ์, ๊ฒ์ฆ๊ฒฐ๊ณผ, ์คํ๊ฐ๋ฅ์ฌ๋ถ)
10. `record_feedback`