(KO) PostgreSQL psql — 인터랙티브 터미널: MainLoop, 백슬래시 명령, 쿼리 디스패치, describe.c 카탈로그 계층
목차
이론적 배경
섹션 제목: “이론적 배경”데이터베이스 인터랙티브 터미널은 사람(또는 스크립트)과 데이터베이스 서버 사이에 위치하는 얇은 클라이언트다. 핵심 역할은 두 종류의 입력 — 일반 SQL과 클라이언트 측 메타 명령 — 을 받아 적절한 핸들러로 전달하는 것이다. 설계 공간은 세 가지 긴장 관계가 지배한다.
-
렉서 경계. 터미널 자체의 파서가 어디서 끝나고 서버의 파서가 어디서 시작하는가. 줄 단위로 즉시 전송하는 터미널은 여러 줄로 이루어진 구문 누적 기능을 잃는다. 반대로 SQL을 클라이언트 측에서 완전히 파싱하려 하면 서버 문법의 상당 부분을 중복 구현해야 한다. 실질적인 중간 지점은 구문 경계 검출기다. 세미콜론과
\g지시어를 스캔하고, 경계가 나올 때까지 텍스트를 누적한 뒤 누적 버퍼를 불투명 문자열로 서버에 전송한다. -
재진입성. 인터랙티브 세션 안에서 스크립트를 실행(
\i)할 수 있고, 그 스크립트가 다시\i를 포함할 수 있으며,\e로 기동된 에디터가 수정된 쿼리 버퍼를 반환해 재스캔이 필요할 수 있다. 이 요구는 재진입 가능한 메인 루프를 필요로 한다. 단일 최상위 읽기 루프 대신, 각 재귀 호출 시 소스 포인터를 저장하고 복원하는 구조다. -
메타 명령 대 SQL 디스패치. 터미널은 입력 토큰이 메타 명령(
\로 시작)인지 SQL 텍스트인지를 줄마다 판단해야 한다. 이 판단은 렉서의 문자열 리터럴·주석 상태와 협력해야 한다. 문자열 리터럴 안의 백슬래시를 메타 명령으로 오인하지 않아야 한다.
인터랙티브 터미널에는 와이어 프로토콜 계층에는 없는 출력 포맷팅 관심사도 있다. 컬럼 정렬, 페이저 기동, 출력 파일 리다이렉션, 타이밍 표시, 쿼리 텍스트 변수 보간이 그것이다. PostgreSQL의 psql에서는 이 책임들이 단일 클래스에 집중되지 않고 여러 협력 파일에 분산된다.
DBMS 공통 설계
섹션 제목: “DBMS 공통 설계”flex 렉서를 이용한 구문 경계 스캔
섹션 제목: “flex 렉서를 이용한 구문 경계 스캔”대부분의 데이터베이스 CLI 도구는 생성형이든 수동 작성이든 렉서를 써서 구문 경계를 스캔한다. 렉서는 세미콜론이 구문을 종료하는지 여부를 올바르게 판단하기 위해 단일 따옴표(string literal), 달러 인용(dollar-quoting), 블록 주석, 식별자 인용 상태를 추적해야 한다. psql은 flex로 생성된 스캐너(psqlscan.l / psqlscanslash.l)를 사용한다. 이 스캐너는 완전한 파스 트리 대신 스캔 토큰 — PSCAN_SEMICOLON, PSCAN_BACKSLASH, PSCAN_EOL, PSCAN_INCOMPLETE — 만 생성한다. 누적된 텍스트는 원문 그대로 서버에 전달된다.
백슬래시 명령 디스패치 테이블
섹션 제목: “백슬래시 명령 디스패치 테이블”메타 명령 집합을 가진 CLI 도구는 보편적으로 명령 이름을 키로 하는 디스패치 테이블을 사용한다. 메타 명령은 클라이언트 측에서 처리되기 때문에 연결 파라미터 조작, 출력 포맷 변경, 로컬 파일 읽기가 가능하다. 이는 서버가 할 수 없는 일들이다. 디스패치 테이블은 일반적으로 결과 코드(줄 건너뜀 / 쿼리 전송 / 오류 / 종료)를 반환하고, 호출 측 루프는 이 코드로 다음 동작을 결정한다.
2단계 쿼리 실행 경로
섹션 제목: “2단계 쿼리 실행 경로”SQL 구문 실행 경로는 두 단계로 나뉜다. 첫 번째 단계는 트랜잭션 관리다. autocommit이 꺼져 있으면 쿼리를 BEGIN으로 감싸고, 오류 격리를 위해 필요 시 세이브포인트를 설정한다. 두 번째 단계는 실제 전송과 수신이다. PQexec 또는 PQexecParams를 호출하고, 결과를 기다리며, 결과 행을 순회하고, 포맷에 맞게 출력한다. 이 분리 덕분에 터미널은 전송-수신 로직을 중복하지 않으면서 실패한 쿼리를 롤백 의미론으로 재시도할 수 있다.
\d 명령을 위한 카탈로그 쿼리 추상화
섹션 제목: “\d 명령을 위한 카탈로그 쿼리 추상화”데이터베이스 객체를 설명하려면 시스템 카탈로그에 SQL 쿼리를 발행해야 한다. 터미널은 이 역할을 전담 모듈로 분리한다. pg_class, pg_attribute, pg_constraint 등을 대상으로 SELECT를 조립하고, 우회 쿼리 경로(PSQLexec)로 실행한 뒤, 결과를 사람이 읽기 좋은 테이블로 포맷하는 가벼운 SQL 생성기다. 이 SQL 생성기는 버전을 인식한다. PQserverVersion이 보고하는 서버 버전에 맞춰 쿼리를 조정한다.
PostgreSQL의 접근 방식
섹션 제목: “PostgreSQL의 접근 방식”psql은 독립 바이너리(src/bin/psql/)다. libpq(PQconnectdbParams)로 서버에 연결하고, 연결 구동은 libpq API에 전적으로 위임한다. 와이어 프로토콜을 직접 다루지 않는다. 아키텍처는 다섯 개의 협력 컴포넌트로 구성된다.
startup.c main() — 옵션 파싱, 연결, MainLoop 기동mainloop.c MainLoop() — 재진입 가능한 읽기/스캔/디스패치 루프command.c HandleSlashCmds() / exec_command() — 백슬래시 디스패치common.c SendQuery() / ExecQueryAndProcessResults() — SQL 디스패치describe.c \d 명령 카탈로그 쿼리 생성기전역 PsqlSettings pset(startup.c에 정의, settings.h에 선언)이 단일 공유 상태 레코드다. 활성 PGconn *db, 출력 파일 핸들, 출력 포맷 옵션, 세션 변수, 모든 불리언 제어 플래그를 담는다. MainLoop와 그 피호출자들은 pset을 직접 읽고 쓴다. 숨겨진 상태는 없다.
시작 시퀀스 (startup.c)
섹션 제목: “시작 시퀀스 (startup.c)”startup.c의 main은 MainLoop에 제어를 넘기기 전에 다섯 단계를 수행한다.
pset기본값 초기화: 출력 포맷PRINT_ALIGNED, 테두리1, 페이저 활성화, 유니코드 선 스타일,AUTOCOMMITon.EstablishVariableSpace호출 — 특수 변수(AUTOCOMMIT,ECHO,ON_ERROR_STOP,HISTSIZE,VERBOSITY,WATCH_INTERVAL등 약 20개)의 substitute 훅과 assign 훅을 등록한다. 훅은 문자열 변수 값을pset필드로 변환해,\set AUTOCOMMIT off가pset.autocommit에 즉시 반영되도록 한다.- 커맨드라인 옵션을
SimpleActionList로 파싱한다.-c인수는 각각ACT_SINGLE_QUERY또는ACT_SINGLE_SLASH셀이 되고,-f는ACT_FILE이 된다. - 연결: 인증 성공 또는 사용자가 비밀번호 제공을 거부할 때까지
PQconnectdbParams를 반복 호출한다. 연결은pset.db에 저장한다. - 디스패치:
-c/-f액션이 있으면 순서대로 실행(-1단일 트랜잭션 래핑 옵션 포함)한다. 없으면MainLoop(stdin)으로 진입한다.
// main — src/bin/psql/startup.c (condensed)pset.db = PQconnectdbParams(keywords, values, true);// ...if (options.actions.head != NULL){ for (cell = options.actions.head; cell; cell = cell->next) { if (cell->action == ACT_SINGLE_QUERY) successResult = SendQuery(cell->val) ? EXIT_SUCCESS : EXIT_FAILURE; else if (cell->action == ACT_SINGLE_SLASH) successResult = HandleSlashCmds(...) != PSQL_CMD_ERROR ? EXIT_SUCCESS : EXIT_FAILURE; else if (cell->action == ACT_FILE) successResult = process_file(cell->val, false); }}else successResult = MainLoop(stdin);재진입 가능한 MainLoop (mainloop.c)
섹션 제목: “재진입 가능한 MainLoop (mainloop.c)”MainLoop(FILE *source)는 psql의 심장부다. \i와 \e가 MainLoop(또는 내부적으로 MainLoop를 호출하는 process_file)를 재귀적으로 호출하기 때문에 재진입 가능하다. 각 호출 인스턴스는 pset.cur_cmd_source, pset.cur_cmd_interactive, pset.lineno를 저장하고 복원한다.
각 줄에 대한 루프 본체:
// MainLoop — src/bin/psql/mainloop.c (condensed)while (successResult == EXIT_SUCCESS){ // Ctrl-C cleanup via sigsetjmp(sigint_interrupt_jmp) // Fetch a line: gets_interactive() or gets_fromFile() line = pset.cur_cmd_interactive ? gets_interactive(get_prompt(prompt_status, cond_stack), query_buf) : gets_fromFile(source);
// Scan the line into query_buf, looking for statement boundaries psql_scan_setup(scan_state, line, strlen(line), pset.encoding, standard_strings());
while (success || !die_on_error) { scan_result = psql_scan(scan_state, query_buf, &prompt_tmp);
if (scan_result == PSCAN_SEMICOLON || (scan_result == PSCAN_EOL && pset.singleline)) { if (conditional_active(cond_stack)) success = SendQuery(query_buf->data); resetPQExpBuffer(query_buf); } else if (scan_result == PSCAN_BACKSLASH) { slashCmdStatus = HandleSlashCmds(scan_state, cond_stack, query_buf, previous_buf); if (slashCmdStatus == PSQL_CMD_SEND) success = SendQuery(query_buf->data); }
if (scan_result == PSCAN_INCOMPLETE || scan_result == PSCAN_EOL) break; }}핵심 설계 포인트:
- 세 개의 버퍼:
query_buf는 현재 구문을 누적한다.previous_buf는 마지막으로 실행된 구문을 보관한다(\e로 재편집할 때 사용).history_buf는 단일 readline 히스토리 항목을 구성하는 여러 줄 입력을 누적한다. ConditionalStack cond_stack:\if/\elif/\else/\endif중첩을 추적한다.conditional_active(cond_stack)이 false를 반환하는 비활성 분기 안에서는 스캐너는 계속 동작하지만(구조 카운팅을 위해),SendQuery와HandleSlashCmds는 건너뛴다.- Ctrl-C 복구:
sigsetjmp(sigint_interrupt_jmp, 1)를 외부 루프 각 반복의 맨 앞에서 재설정한다. 입력 또는 쿼리 실행 중 Ctrl-C가 발생하면 여기로 점프해 스캔 상태와 버퍼를 초기화하고 다시 프롬프트를 낸다. - 전송 시 버퍼 스왑:
SendQuery이후previous_buf와query_buf는 복사가 아닌 포인터 스왑으로 교환된다. 이후query_buf는 리셋된다. 따라서previous_buf는 항상 문자열 복사 없이 마지막으로 전송된 구문을 보관한다. - PGDMP 가드: 비인터랙티브 소스의 첫 번째 줄이
PGDMP접두어(PostgreSQL 커스텀 포맷 덤프 마커)로 시작하면 명확한 오류 메시지와 함께 거부한다.
flowchart TD
A["MainLoop 진입<br/>이전 source/lineno 저장"] --> B["sigsetjmp<br/>Ctrl-C 앵커"]
B --> C{"인터랙티브?"}
C -- "yes" --> D["gets_interactive<br/>readline 프롬프트"]
C -- "no" --> E["gets_fromFile"]
D --> F["psql_scan_setup"]
E --> F
F --> G["psql_scan 루프"]
G --> H{"scan_result"}
H -- "PSCAN_SEMICOLON<br/>또는 singleline EOL" --> I{"conditional_active?"}
I -- "yes" --> J["SendQuery<br/>query/prev 버퍼 스왑"]
I -- "no" --> K["건너뜀 — 인터랙티브면 경고"]
J --> G
K --> G
H -- "PSCAN_BACKSLASH" --> L["HandleSlashCmds"]
L --> M{"slashCmdStatus"}
M -- "PSQL_CMD_SEND" --> J
M -- "PSQL_CMD_NEWEDIT" --> F
M -- "PSQL_CMD_TERMINATE" --> N["외부 루프 탈출"]
H -- "PSCAN_EOL<br/>PSCAN_INCOMPLETE" --> B
N --> O["이전 source/lineno 복원<br/>successResult 반환"]
그림 1 — MainLoop 제어 흐름. 재진입 루프는 줄 하나를 가져와 구문 경계와 백슬래시 명령을 스캔한 뒤 적절히 디스패치한다. Ctrl-C는 외부 루프 맨 앞의 sigsetjmp 앵커로 점프한다.
백슬래시 명령 디스패치 (command.c)
섹션 제목: “백슬래시 명령 디스패치 (command.c)”HandleSlashCmds는 스캐너가 PSCAN_BACKSLASH로 보고하는 모든 \command 토큰의 진입점이다.
// HandleSlashCmds — src/bin/psql/command.c (condensed)backslashResultHandleSlashCmds(PsqlScanState scan_state, ConditionalStack cstack, PQExpBuffer query_buf, PQExpBuffer previous_buf){ cmd = psql_scan_slash_command(scan_state);
if (restricted && strcmp(cmd, "unrestrict") != 0) status = PSQL_CMD_ERROR; /* restricted mode: only \unrestrict allowed */ else status = exec_command(cmd, scan_state, cstack, query_buf, previous_buf);
if (status == PSQL_CMD_UNKNOWN) { pg_log_error("invalid command \\%s", cmd); status = PSQL_CMD_ERROR; } // Drain any remaining arguments after a valid command fflush(pset.queryFout); return status;}exec_command는 cmd를 키로 하는 평탄한 if/else if 체인이다. 각 분기는 전용 exec_command_* 함수를 호출한다. 체인은 약 50개 명령을 처리한다.
// exec_command dispatch (excerpt) — src/bin/psql/command.cif (strcmp(cmd, "a") == 0) status = exec_command_a(scan_state, active_branch);else if (strcmp(cmd, "bind") == 0) status = exec_command_bind(scan_state, active_branch);else if (strcmp(cmd, "c") == 0 || strcmp(cmd, "connect") == 0) status = exec_command_connect(scan_state, active_branch);else if (cmd[0] == 'd') status = exec_command_d(scan_state, active_branch, cmd);else if (strcmp(cmd, "endpipeline") == 0) status = exec_command_endpipeline(scan_state, active_branch);else if (strcmp(cmd, "g") == 0 || strcmp(cmd, "gx") == 0) status = exec_command_g(scan_state, active_branch, cmd);// ... ~45 more branches주요 그룹:
- 출력 수정자:
\a(정렬 토글),\C(제목),\f(필드 구분자),\H(HTML),\t(튜플만),\x(확장 출력),\T(테이블 속성).do_pset을 호출해pset.popt.topt에 기록한다. - 실행 수정자:
\g/\gx(선택적 파일 출력 또는 확장 출력으로 실행),\gdesc(결과 컬럼 설명),\gexec(각 결과 셀을 SQL로 실행),\gset(결과를 변수에 저장),\crosstabview. 이들은pset에 일회성 플래그(gdesc_flag,gexec_flag,gset_prefix)를 설정하고PSQL_CMD_SEND를 반환해MainLoop가SendQuery를 호출하도록 한다. - 연결:
\c/\connect는do_connect를 호출하고,do_connect는PQconnectdbParams를 호출해pset.db를 교체한다. - 포함/편집:
\i/\ir은process_file을 호출해MainLoop를 재귀 호출한다.\e/\ef/\ev는 외부 에디터를 기동하고PSQL_CMD_NEWEDIT를 반환해MainLoop가 편집된 버퍼를 재스캔하도록 한다. - 조건 흐름:
\if/\elif/\else/\endif는cond_stack을 조작한다. 비활성 분기에서도 실행되는 유일한 명령들이다. 중첩 카운팅의 정확성을 유지해야 하기 때문이다. - 파이프라인(PG17+):
\startpipeline/\endpipeline/\flush/\flushrequest/\syncpipeline/\getresults는PSQL_SEND_MODE값(PSQL_SEND_START_PIPELINE_MODE,PSQL_SEND_PIPELINE_SYNC등)에 대응하며,pset.send_mode에 저장되어SendQuery가 처리한다.
exec_command_* 함수들이 반환하는 코드:
| 코드 | 의미 |
|---|---|
PSQL_CMD_SKIP_LINE | 명령 실행 완료; 쿼리 버퍼 전송 불필요 |
PSQL_CMD_SEND | 반환 후 query_buf를 서버로 전송 |
PSQL_CMD_NEWEDIT | query_buf가 교체됨; 재스캔 필요 |
PSQL_CMD_TERMINATE | MainLoop 종료 |
PSQL_CMD_ERROR | 명령 실패 |
PSQL_CMD_UNKNOWN | 인식할 수 없는 명령 |
쿼리 디스패치 (common.c)
섹션 제목: “쿼리 디스패치 (common.c)”SendQuery(const char *query)는 서버로 전송되는 모든 SQL의 단일 진입점이다. MainLoop의 PSCAN_SEMICOLON 감지, \g 명령, 비인터랙티브 -c 액션 모두 이 함수로 연결된다.
// SendQuery — src/bin/psql/common.c (condensed)boolSendQuery(const char *query){ // 1. 가드: 연결이 활성 상태여야 함 // 2. pset.singlestep이 설정된 경우 단계별 프롬프트 // 3. pset.echo == PSQL_ECHO_QUERIES이면 에코 // 4. pset.logfile이 설정된 경우 로그 기록
SetCancelConn(pset.db); transaction_status = PQtransactionStatus(pset.db);
// 5. autocommit=off이고 트랜잭션이 유휴 상태이면 암묵적 BEGIN if (transaction_status == PQTRANS_IDLE && !pset.autocommit && !command_no_begin(query)) PQexec(pset.db, "BEGIN");
// 6. 인터랙티브 모드에서 ON_ERROR_ROLLBACK을 위한 SAVEPOINT if (transaction_status == PQTRANS_INTRANS && pset.on_error_rollback != PSQL_ERROR_ROLLBACK_OFF && ...) PQexec(pset.db, "SAVEPOINT pg_psql_temporary_savepoint");
// 7. 실행: describe-only 또는 일반 실행 if (pset.gdesc_flag) OK = DescribeQuery(query, &elapsed_msec); else OK = (ExecQueryAndProcessResults(query, &elapsed_msec, &svpt_gone, false, 0, NULL, NULL) > 0);
// 8. 세이브포인트 처리: 트랜잭션 상태에 따라 RELEASE 또는 ROLLBACK TO // 9. 전송 후 처리: gexec_flag 루프, gset_prefix 저장 // 10. pset.timing이 설정된 경우 타이밍 출력}ExecQueryAndProcessResults가 실제 libpq 전송과 결과 루프를 수행한다.
// ExecQueryAndProcessResults — src/bin/psql/common.c (condensed)static intExecQueryAndProcessResults(const char *query, double *elapsed_msec, bool *svpt_gone_p, bool is_watch, int min_rows, const printQueryOpt *opt, FILE *printQueryFout){ // pset.send_mode에 따라 전송: // PSQL_SEND_QUERY → PQsendQuery (단순 프로토콜) // PSQL_SEND_EXTENDED_* → PQsendQueryParams / PQsendPrepare / etc. // PSQL_SEND_PIPELINE_SYNC → PQpipelineSync
// 결과 루프: while ((result = PQgetResult(pset.db)) != NULL) { OK = AcceptResult(result, true); SetResultVariables(result, OK); // set $ERROR, $SQLSTATE, $ROW_COUNT // PrintQueryResult가 결과 행을 포맷하고 출력 PrintQueryResult(result, last, opt, ...); PQclear(result); } return ntuples;}AcceptResult는 PQresultStatus를 불리언으로 분류한다.
// AcceptResult — src/bin/psql/common.c (condensed)boolAcceptResult(const PGresult *result, bool show_error){ switch (PQresultStatus(result)) { case PGRES_COMMAND_OK: case PGRES_TUPLES_OK: case PGRES_TUPLES_CHUNK: case PGRES_EMPTY_QUERY: case PGRES_COPY_IN: case PGRES_COPY_OUT: case PGRES_PIPELINE_SYNC: return true; case PGRES_PIPELINE_ABORTED: case PGRES_BAD_RESPONSE: case PGRES_NONFATAL_ERROR: case PGRES_FATAL_ERROR: return false; /* also calls CheckConnection() */ }}SetResultVariables는 모든 쿼리 이후 $ERROR, $SQLSTATE, $ROW_COUNT, $LAST_ERROR_SQLSTATE, $LAST_ERROR_MESSAGE를 pset.vars에 기록한다.
PSQLexec는 describe.c와 다른 내부 호출자가 사용하는 병렬 “우회” 경로다. SendQuery의 트랜잭션 래핑과 결과 변수 로직을 건너뛰고, 디버깅을 위한 ECHO_HIDDEN을 준수한다.
// PSQLexec — src/bin/psql/common.c (condensed)PGresult *PSQLexec(const char *query){ if (pset.echo_hidden != PSQL_ECHO_HIDDEN_OFF) printf("/******** QUERY *********\n%s\n/************************\n\n", query); if (pset.echo_hidden == PSQL_ECHO_HIDDEN_NOEXEC) return NULL; return PQexec(pset.db, query);}PSQLexecWatch는 \watch에서 사용하는 ExecQueryAndProcessResults의 변형이다. 주기적 쿼리가 예상보다 적은 행을 반환할 때 감지하기 위한 min_rows 임계값을 받고, exec_command_watch의 타이머 기반 루프가 설정된 WATCH_INTERVAL마다 재실행한다.
describe.c 카탈로그 쿼리 계층
섹션 제목: “describe.c 카탈로그 쿼리 계층”describe.c는 모든 \d 변형을 뒷받침한다. 두 계층으로 구성된다.
1계층 — 디스패치 (command.c의 exec_command_d): \d 변형 문자열을 올바른 describe.c 함수로 매핑한다. 예를 들어 \dt는 listTables("t", pattern, verbose, showSystem)를 호출하고, \df는 listFunctions를, \d <name>은 describeTableDetails를 호출한다.
2계층 — SQL 생성 (describe.c): 시스템 카탈로그를 대상으로 하는 SELECT를 PQExpBuffer에 조립하고, PSQLexec를 호출한 뒤 printQuery로 결과를 출력한다. 쿼리는 pg_catalog 테이블들(pg_class, pg_attribute, pg_constraint, pg_index, pg_trigger, pg_policy, pg_inherits 등 약 20개)을 참조한다.
버전 인식 패턴이 전반에 걸쳐 적용된다. 모든 함수는 새 서버에만 존재하는 컬럼이나 JOIN을 추가하기 전에 pset.sversion을 확인한다.
// describeOneTableDetails — src/bin/psql/describe.c (condensed)if (pset.sversion >= 120000) appendPQExpBufferStr(&buf, ",\n pg_get_expr(d.adbin, d.adrelid) AS default_value");describeTableDetails는 일치하는 릴레이션 OID마다 describeOneTableDetails를 호출한다. describeOneTableDetails는 여러 PSQLexec 호출을 발행한다. 메인 컬럼 목록 하나, 인덱스 하나, 제약 조건 하나, 트리거 하나, 정책 하나, 행 수준 보안 하나, 부모 테이블 하나. 이를 모아 printTableContent 구조체를 조립한 뒤 printTable(cont, pset.queryFout, ...)을 호출한다.
flowchart TD
A["exec_command_d<br/>command.c"] --> B{"\\d 변형"}
B -- "\\dt / \\dv / \\di / \\ds" --> C["listTables<br/>describe.c"]
B -- "\\df" --> D["listFunctions<br/>describe.c"]
B -- "\\d name" --> E["describeTableDetails<br/>describe.c"]
B -- "\\l" --> F["listAllDbs<br/>describe.c"]
C --> G["PSQLexec<br/>SELECT pg_class..."]
D --> G
E --> H["describeOneTableDetails<br/>describe.c"]
H --> I["PSQLexec ×N<br/>컬럼 / 인덱스 / 트리거<br/>제약 조건 / 정책 / RLS"]
I --> J["printTable<br/>fe_utils/print.c"]
G --> K["printQuery<br/>fe_utils/print.c"]
F --> G
그림 2 — \d 디스패치와 describe.c 카탈로그 쿼리 계층. 각 \d 변형은 describe.c 함수로 라우팅되고, 함수는 pg_catalog에 대한 SQL 쿼리를 하나 이상 조립해 PSQLexec로 실행한 뒤 fe_utils/print.c 포맷터로 결과를 렌더링한다.
PsqlSettings (settings.h)
섹션 제목: “PsqlSettings (settings.h)”PsqlSettings 구조체(typedef struct _psqlSettings)가 모든 공유 상태를 담는다. 주요 필드:
// PsqlSettings (excerpt) — src/bin/psql/settings.htypedef struct _psqlSettings{ PGconn *db; /* active connection */ int encoding; /* client_encoding */ FILE *queryFout; /* output file / pipe */ printQueryOpt popt; /* print format options (border, format, ...) */ char *gfname; /* one-shot \g output file */ bool gdesc_flag; /* one-shot \gdesc: describe only */ bool gexec_flag; /* one-shot \gexec: execute result cells */ PSQL_SEND_MODE send_mode; /* normal / extended / pipeline-sync / ... */ int bind_nparams; /* \bind param count */ char **bind_params; /* \bind param values */ char *stmtName; /* \bind_named stmt name */ int piped_commands; /* pipeline: commands in flight */ int piped_syncs; /* pipeline: syncs in flight */ int available_results; /* pipeline: results ready */ bool autocommit; /* AUTOCOMMIT variable */ bool on_error_stop; /* ON_ERROR_STOP variable */ bool singleline; /* SINGLELINE variable */ bool singlestep; /* SINGLESTEP: prompt before each query */ PSQL_ECHO echo; /* ECHO: none/queries/errors/all */ PSQL_ECHO_HIDDEN echo_hidden; /* ECHO_HIDDEN: off/on/noexec */ PSQL_ERROR_ROLLBACK on_error_rollback; // ... timing, logfile, vars, prompt1/2/3, verbosity, etc.} PsqlSettings;PSQL_SEND_MODE는 ExecQueryAndProcessResults가 쿼리를 전송하는 방식을 제어하는 열거형(PG17+)이다. 단순 프로토콜(PSQL_SEND_QUERY), 확장 프로토콜 변형(PSQL_SEND_EXTENDED_QUERY_PARAMS, PSQL_SEND_EXTENDED_QUERY_PREPARED), 파이프라인 제어(PSQL_SEND_PIPELINE_SYNC, PSQL_SEND_START_PIPELINE_MODE, PSQL_SEND_END_PIPELINE_MODE, PSQL_SEND_FLUSH, PSQL_SEND_FLUSH_REQUEST, PSQL_SEND_GET_RESULTS)로 구분된다.
소스 워크스루
섹션 제목: “소스 워크스루”줄 번호가 아닌 심볼 이름을 기준점으로 삼는다. 재배치가 필요하면
git grep -n '<symbol>' src/bin/psql/을 사용한다. 아래 줄 번호는 커밋273fe94기준 힌트다.
시작 (src/bin/psql/startup.c)
섹션 제목: “시작 (src/bin/psql/startup.c)”main— 옵션 파싱, 연결, 액션 디스패치 또는MainLoop진입.EstablishVariableSpace— 약 25개 특수 변수의SetVariableHooks등록; 훅이 문자열 값을pset필드로 변환.parse_psql_options—getopt_long루프;adhoc_opts와SimpleActionList를 채운다.process_psqlrc/process_psqlrc_file— 시스템 전체 rc 파일 다음 사용자 수준 rc 파일 로드;.psqlrc-<version>다음.psqlrc순서로 시도.PsqlSettings pset— 단일 전역 설정 인스턴스.
메인 루프 (src/bin/psql/mainloop.c)
섹션 제목: “메인 루프 (src/bin/psql/mainloop.c)”MainLoop— 재진입 루프; 소스/lineno 저장·복원;psql_scan→SendQuery/HandleSlashCmds구동.psqlscan_callbacks—:'var'보간을 위해psql_get_variable을 flex 스캐너에 연결하는PsqlScanCallbacks구조체.sigint_interrupt_jmp/sigint_interrupt_enabled— Ctrl-C 복구를 위한sigsetjmp타겟; 외부 루프 반복마다 재설정.
백슬래시 명령 (src/bin/psql/command.c)
섹션 제목: “백슬래시 명령 (src/bin/psql/command.c)”HandleSlashCmds— 진입점;exec_command호출; 제한 모드와 알 수 없는 명령 오류 처리.exec_command— 명령 이름 기반의 평탄한if/else if디스패치.exec_command_d—\d*변형을describe.c함수로 디스패치.exec_command_connect—do_connect호출;pset.db교체.exec_command_include—process_file호출(재진입MainLoop).exec_command_edit/exec_command_ef_ev—$EDITOR기동;PSQL_CMD_NEWEDIT반환.exec_command_g/process_command_g_options—pset.gfname,pset.gsavepopt설정;PSQL_CMD_SEND반환.exec_command_if/exec_command_elif/exec_command_else/exec_command_endif—ConditionalStack조작.exec_command_startpipeline/exec_command_endpipeline/exec_command_flush/exec_command_flushrequest/exec_command_getresults— 파이프라인 제어;pset.send_mode설정.
쿼리 실행 (src/bin/psql/common.c)
섹션 제목: “쿼리 실행 (src/bin/psql/common.c)”SendQuery— 최상위 SQL 디스패처; 트랜잭션 래핑, 세이브포인트,ExecQueryAndProcessResults, 전송 후 처리.ExecQueryAndProcessResults— libpq 전송 + 결과 루프; 단순·확장 프로토콜, 파이프라인 모드,\watch모드 처리.PSQLexec— 우회 단일 쿼리 경로;describe.c가 사용.PSQLexecWatch—\watch변형;is_watch=true와min_rows로ExecQueryAndProcessResults호출.AcceptResult—PQresultStatus를 성공/실패로 분류.SetResultVariables—$ERROR,$SQLSTATE,$ROW_COUNT등 기록.SetShellResultVariables—$SHELL_ERROR,$SHELL_EXIT_CODE기록.PrintQueryResult—printQuery로PGresult하나를 포맷하고 출력.ClearOrSaveResult—\errverbose를 위해 오류 결과를pset.last_error_result에 보관; 그 외는PQclear.pipelineReset—piped_syncs,piped_commands,available_results,requested_results를 초기화.
카탈로그 쿼리 (src/bin/psql/describe.c)
섹션 제목: “카탈로그 쿼리 (src/bin/psql/describe.c)”describeTableDetails— OID 조회 +describeOneTableDetails루프.describeOneTableDetails— 다중 쿼리 조립;printTableContent로 컬럼, 인덱스, 제약 조건, 트리거, 정책, RLS, 부모 테이블 수집.listTables— 패턴과 일치하는 테이블/뷰/시퀀스/인덱스 목록.listAllDbs— 데이터베이스 목록;\l과psql -l이 사용.listFunctions— 패턴과 일치하는 함수/프로시저 목록.map_typename_pattern—\dT를 위한 타입 이름 패턴 정규화.printACLColumn—\dp/\z의 ACL 컬럼 포맷.
설정 (src/bin/psql/settings.h)
섹션 제목: “설정 (src/bin/psql/settings.h)”PsqlSettings(_psqlSettings) — 전역 상태 구조체.PSQL_SEND_MODE—ExecQueryAndProcessResults전송 경로를 제어하는 열거형.PSQL_ECHO,PSQL_ECHO_HIDDEN,PSQL_ERROR_ROLLBACK,PSQL_COMP_CASE,HistControl— 변수 훅 제어 열거형들.
위치 힌트 (2026-06-05 기준, REL_18 273fe94)
섹션 제목: “위치 힌트 (2026-06-05 기준, REL_18 273fe94)”| 심볼 | 파일 | 줄 |
|---|---|---|
main | src/bin/psql/startup.c | 126 |
EstablishVariableSpace | src/bin/psql/startup.c | 1218 |
PsqlSettings (struct close) | src/bin/psql/settings.h | 187 |
PSQL_SEND_MODE | src/bin/psql/settings.h | 73 |
MainLoop | src/bin/psql/mainloop.c | 33 |
psqlscan_callbacks | src/bin/psql/mainloop.c | 20 |
HandleSlashCmds | src/bin/psql/command.c | 231 |
exec_command | src/bin/psql/command.c | 315 |
SendQuery | src/bin/psql/common.c | 1118 |
AcceptResult | src/bin/psql/common.c | 416 |
SetResultVariables | src/bin/psql/common.c | 476 |
PSQLexec | src/bin/psql/common.c | 655 |
PSQLexecWatch | src/bin/psql/common.c | 710 |
PrintQueryResult | src/bin/psql/common.c | 1039 |
ExecQueryAndProcessResults | src/bin/psql/common.c | 1569 |
listAllDbs | src/bin/psql/describe.c | 946 |
describeTableDetails | src/bin/psql/describe.c | 1488 |
describeOneTableDetails | src/bin/psql/describe.c | 1571 |
listTables | src/bin/psql/describe.c | 4007 |
소스 검증
섹션 제목: “소스 검증”커밋
273fe94(REL_18_STABLE) 기준으로 검증된 사실들.
검증된 사실
섹션 제목: “검증된 사실”-
MainLoop는 실제로 재진입 가능하다. 진입과 종료 시점에pset.cur_cmd_source,pset.cur_cmd_interactive,pset.lineno를 저장하고 복원한다.\i와process_file은FILE *을 받아MainLoop를 호출하며, 재귀 호출 인스턴스들은 각자 독립된scan_state,cond_stack,query_buf를 갖는다.mainloop.c의 5866번 줄과 656659번 줄의 저장/복원 블록으로 확인. -
previous_buf/query_buf스왑은 복사가 아닌 포인터 스왑이다.SendQuery이후MainLoop는 임시 변수를 이용해 두PQExpBuffer *포인터를 교환한다. 따라서previous_buf는 항상 할당 없이 마지막으로 전송된 구문을 보관한다.mainloop.c의SendQuery직후 스왑 블록으로 확인. -
sigint_interrupt_jmp는 외부 루프 반복마다 재설정된다.sigsetjmp호출은while (successResult == EXIT_SUCCESS)루프 안에 있다. 소스 주석에 “We must re-do this each time through the loop for safety, since the jmpbuf might get changed during command execution.”이라고 명시되어 있다.mainloop.c107~142번 줄로 확인. -
PSQLexec는 트랜잭션 래핑과 결과 변수 설정을 건너뛴다.ECHO_HIDDEN확인 이후PQexec를 직접 호출한다.command_no_begin을 호출하지 않고,BEGIN을 발행하지 않으며,SetResultVariables를 호출하지 않고,pset.last_error_result도 갱신하지 않는다.common.c654~700번 줄로 확인. -
SetResultVariables는 오류 때만이 아니라 모든SendQuery결과 이후에 호출된다. 성공 시ERROR=false,SQLSTATE=00000,PQcmdTuples의ROW_COUNT를 기록한다. 실패 시ERROR=true, SQLSTATE 코드,LAST_ERROR_MESSAGE를 갱신한다.common.c476~504번 줄로 확인. -
PSQL_SEND_MODE열거형은 파이프라인 지원을 위해 추가됐다. 열거형에PSQL_SEND_START_PIPELINE_MODE와PSQL_SEND_END_PIPELINE_MODE가 있으며, 파이프라인 모드가 PG19 전용이 아닌 REL_18 현재 기능임을 확인한다.settings.h73~84번 줄로 확인. -
process_psqlrc_file은 부 버전이 붙은 rc 파일을 먼저 시도한다.filename-PG_VERSION(예:.psqlrc-18.1), 다음으로filename-PG_MAJORVERSION(예:.psqlrc-18), 마지막으로 단순 파일 순서로 시도한다. 버전별 커스터마이징을 허용하는 방식이다.startup.c813~835번 줄로 확인.
미해결 질문
섹션 제목: “미해결 질문”-
\watch타이머 메커니즘.exec_command_watch는command.c에 구현되어 있고 Unix에서setitimer를 사용한다. 그런데 인터벌 타이머,SIGALRM,MainLoop의sigsetjmp사이의 상호작용은 이 문서에서 완전히 추적되지 않았다.main에서SIGALRM에 설치되는empty_signal_handler는 시그널이 직접 동작하는 것이 아니라 깨우기 용도로만 사용됨을 암시한다. 조사 경로:command.c의exec_command_watch와common.c의do_watch경로. -
psql의
\bind와 확장 쿼리 프로토콜.exec_command_bind는 파라미터를pset.bind_params에 저장하고pset.send_mode = PSQL_SEND_EXTENDED_QUERY_PARAMS를 설정한다.ExecQueryAndProcessResults가PQsendQueryParams와 단순PQsendQuery경로를 어떻게 분기하는지, 파이프라인 모드에서의 오류 복구가SendQuery의 세이브포인트 메커니즘과 어떻게 상호작용하는지는 부분적으로만 추적됐다. 조사 경로:common.c1569~2747번 줄의ExecQueryAndProcessResults. -
\copy구현.command.c의exec_command_copy는copy.c의do_copy를 호출한다.\copy명령은 서버 측 파일 경로 대신 libpq COPY 프로토콜로 데이터를 라우팅하는 클라이언트 측 COPY를 구현한다.AcceptResult에서 보이는PGRES_COPY_IN/PGRES_COPY_OUT결과 상태와의 상호작용은 이 문서에서 상세히 다루지 않는다.
PostgreSQL 너머 — 비교 설계
섹션 제목: “PostgreSQL 너머 — 비교 설계”포인터만 제공한다. 각 항목은 후속 문서의 출발점이다.
-
MySQL
mysqlCLI. 유사한 readline 기반 루프를 사용하지만 메타 명령 집합은 더 단순하다. 상태 변수($mysql_affected_rows,$mysql_insert_id)는 다른 방식으로 노출된다. psql의\gset처럼 임의의 결과 컬럼을 이름 있는 셸 변수에 저장하는 기능은 없다. -
SQLite
sqlite3CLI. 유사한 2단계 아키텍처를 구현한다..schema,.tables,.dump메타 명령은 psql의\d*집합과 유사하지만pg_catalog대신 SQLite의sqlite_master를 쿼리한다. SQLite에는 역할, ACL, 뷰 트리거, 행 수준 보안, 파티셔닝이 없으므로 describe 계층이 훨씬 단순하다. -
pgcli(Python).\d명령에pgspecial을 사용하고 인터랙티브 프론트엔드에prompt_toolkit을 사용하는 psql 대체 도구다. psql 아키텍처(별도 메타 명령 디스패치, 카탈로그 쿼리 계층, 결과 포맷터)가 언어 독립적이며 PostgreSQL 소스 트리 밖에서도 깔끔하게 재구현될 수 있음을 보여준다. -
psql파이프라인 모드 (PG14+). PG14~17에 도입된\startpipeline/\endpipeline/\bind/\syncpipeline명령 덕분에 psql은 libpq 파이프라인 API의 일급 테스트 도구가 됐다.settings.h의PSQL_SEND_MODE열거형과common.c의pipelineReset/SetPipelineVariables함수는postgres-wire-protocol.md에 문서화된 서버 측PostgresMain파이프라인 처리의 클라이언트 측 대응물이다. -
pg_dump/pg_restore. 이 유틸리티들은 psql과fe_utils/헬퍼(특히 테이블 포맷팅을 위한fe_utils/print.c)를 공유하지만MainLoop나 메타 명령 디스패치는 사용하지 않는다.PSQLexec-only 패턴에 가깝다. 각 작업은 구조화된 출력 포맷터를 거치는PQexec호출이다.postgres-pg-dump-restore.md참조.
참고 자료
섹션 제목: “참고 자료”PostgreSQL 소스 (/data/hgryoo/references/postgres 기준, REL_18 273fe94)
섹션 제목: “PostgreSQL 소스 (/data/hgryoo/references/postgres 기준, REL_18 273fe94)”src/bin/psql/mainloop.c—MainLoop, 재진입 루프, Ctrl-C 복구.src/bin/psql/command.c—HandleSlashCmds,exec_command, 모든exec_command_*함수.src/bin/psql/common.c—SendQuery,ExecQueryAndProcessResults,PSQLexec,AcceptResult,SetResultVariables,PrintQueryResult.src/bin/psql/describe.c—describeTableDetails,describeOneTableDetails,listTables,listAllDbs,listFunctions.src/bin/psql/startup.c—main,EstablishVariableSpace,parse_psql_options,process_psqlrc.src/bin/psql/settings.h—PsqlSettings,PSQL_SEND_MODE열거형, 제어 열거형들.
교차 참조 (형제 모듈 문서)
섹션 제목: “교차 참조 (형제 모듈 문서)”postgres-wire-protocol.md— psql이 libpq로 접근하는 서버 측 FE/BE 프로토콜;PostgresMain과 메시지 디스패치 루프.postgres-portals-prepared.md— psql의\bind/ 확장 쿼리 경로가 생성하는 서버 측CachedPlanSource와Portal객체.postgres-pg-dump-restore.md—fe_utils/를 공유하지만MainLoop를 사용하지 않는pg_dump와pg_restore.postgres-overview-client-protocol.md— 클라이언트 프로토콜 서브카테고리의 섹션 라우터.