콘텐츠로 이동

(KO) PostgreSQL psql — 인터랙티브 터미널: MainLoop, 백슬래시 명령, 쿼리 디스패치, describe.c 카탈로그 계층

목차

데이터베이스 인터랙티브 터미널은 사람(또는 스크립트)과 데이터베이스 서버 사이에 위치하는 얇은 클라이언트다. 핵심 역할은 두 종류의 입력 — 일반 SQL과 클라이언트 측 메타 명령 — 을 받아 적절한 핸들러로 전달하는 것이다. 설계 공간은 세 가지 긴장 관계가 지배한다.

  1. 렉서 경계. 터미널 자체의 파서가 어디서 끝나고 서버의 파서가 어디서 시작하는가. 줄 단위로 즉시 전송하는 터미널은 여러 줄로 이루어진 구문 누적 기능을 잃는다. 반대로 SQL을 클라이언트 측에서 완전히 파싱하려 하면 서버 문법의 상당 부분을 중복 구현해야 한다. 실질적인 중간 지점은 구문 경계 검출기다. 세미콜론과 \g 지시어를 스캔하고, 경계가 나올 때까지 텍스트를 누적한 뒤 누적 버퍼를 불투명 문자열로 서버에 전송한다.

  2. 재진입성. 인터랙티브 세션 안에서 스크립트를 실행(\i)할 수 있고, 그 스크립트가 다시 \i를 포함할 수 있으며, \e로 기동된 에디터가 수정된 쿼리 버퍼를 반환해 재스캔이 필요할 수 있다. 이 요구는 재진입 가능한 메인 루프를 필요로 한다. 단일 최상위 읽기 루프 대신, 각 재귀 호출 시 소스 포인터를 저장하고 복원하는 구조다.

  3. 메타 명령 대 SQL 디스패치. 터미널은 입력 토큰이 메타 명령(\로 시작)인지 SQL 텍스트인지를 줄마다 판단해야 한다. 이 판단은 렉서의 문자열 리터럴·주석 상태와 협력해야 한다. 문자열 리터럴 안의 백슬래시를 메타 명령으로 오인하지 않아야 한다.

인터랙티브 터미널에는 와이어 프로토콜 계층에는 없는 출력 포맷팅 관심사도 있다. 컬럼 정렬, 페이저 기동, 출력 파일 리다이렉션, 타이밍 표시, 쿼리 텍스트 변수 보간이 그것이다. PostgreSQL의 psql에서는 이 책임들이 단일 클래스에 집중되지 않고 여러 협력 파일에 분산된다.

flex 렉서를 이용한 구문 경계 스캔

섹션 제목: “flex 렉서를 이용한 구문 경계 스캔”

대부분의 데이터베이스 CLI 도구는 생성형이든 수동 작성이든 렉서를 써서 구문 경계를 스캔한다. 렉서는 세미콜론이 구문을 종료하는지 여부를 올바르게 판단하기 위해 단일 따옴표(string literal), 달러 인용(dollar-quoting), 블록 주석, 식별자 인용 상태를 추적해야 한다. psql은 flex로 생성된 스캐너(psqlscan.l / psqlscanslash.l)를 사용한다. 이 스캐너는 완전한 파스 트리 대신 스캔 토큰 — PSCAN_SEMICOLON, PSCAN_BACKSLASH, PSCAN_EOL, PSCAN_INCOMPLETE — 만 생성한다. 누적된 텍스트는 원문 그대로 서버에 전달된다.

백슬래시 명령 디스패치 테이블

섹션 제목: “백슬래시 명령 디스패치 테이블”

메타 명령 집합을 가진 CLI 도구는 보편적으로 명령 이름을 키로 하는 디스패치 테이블을 사용한다. 메타 명령은 클라이언트 측에서 처리되기 때문에 연결 파라미터 조작, 출력 포맷 변경, 로컬 파일 읽기가 가능하다. 이는 서버가 할 수 없는 일들이다. 디스패치 테이블은 일반적으로 결과 코드(줄 건너뜀 / 쿼리 전송 / 오류 / 종료)를 반환하고, 호출 측 루프는 이 코드로 다음 동작을 결정한다.

SQL 구문 실행 경로는 두 단계로 나뉜다. 첫 번째 단계는 트랜잭션 관리다. autocommit이 꺼져 있으면 쿼리를 BEGIN으로 감싸고, 오류 격리를 위해 필요 시 세이브포인트를 설정한다. 두 번째 단계는 실제 전송과 수신이다. PQexec 또는 PQexecParams를 호출하고, 결과를 기다리며, 결과 행을 순회하고, 포맷에 맞게 출력한다. 이 분리 덕분에 터미널은 전송-수신 로직을 중복하지 않으면서 실패한 쿼리를 롤백 의미론으로 재시도할 수 있다.

\d 명령을 위한 카탈로그 쿼리 추상화

섹션 제목: “\d 명령을 위한 카탈로그 쿼리 추상화”

데이터베이스 객체를 설명하려면 시스템 카탈로그에 SQL 쿼리를 발행해야 한다. 터미널은 이 역할을 전담 모듈로 분리한다. pg_class, pg_attribute, pg_constraint 등을 대상으로 SELECT를 조립하고, 우회 쿼리 경로(PSQLexec)로 실행한 뒤, 결과를 사람이 읽기 좋은 테이블로 포맷하는 가벼운 SQL 생성기다. 이 SQL 생성기는 버전을 인식한다. PQserverVersion이 보고하는 서버 버전에 맞춰 쿼리를 조정한다.

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.cmainMainLoop에 제어를 넘기기 전에 다섯 단계를 수행한다.

  1. pset 기본값 초기화: 출력 포맷 PRINT_ALIGNED, 테두리 1, 페이저 활성화, 유니코드 선 스타일, AUTOCOMMIT on.
  2. EstablishVariableSpace 호출 — 특수 변수(AUTOCOMMIT, ECHO, ON_ERROR_STOP, HISTSIZE, VERBOSITY, WATCH_INTERVAL 등 약 20개)의 substitute 훅과 assign 훅을 등록한다. 훅은 문자열 변수 값을 pset 필드로 변환해, \set AUTOCOMMIT offpset.autocommit에 즉시 반영되도록 한다.
  3. 커맨드라인 옵션을 SimpleActionList로 파싱한다. -c 인수는 각각 ACT_SINGLE_QUERY 또는 ACT_SINGLE_SLASH 셀이 되고, -fACT_FILE이 된다.
  4. 연결: 인증 성공 또는 사용자가 비밀번호 제공을 거부할 때까지 PQconnectdbParams를 반복 호출한다. 연결은 pset.db에 저장한다.
  5. 디스패치: -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(FILE *source)는 psql의 심장부다. \i\eMainLoop(또는 내부적으로 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를 반환하는 비활성 분기 안에서는 스캐너는 계속 동작하지만(구조 카운팅을 위해), SendQueryHandleSlashCmds는 건너뛴다.
  • Ctrl-C 복구: sigsetjmp(sigint_interrupt_jmp, 1)를 외부 루프 각 반복의 맨 앞에서 재설정한다. 입력 또는 쿼리 실행 중 Ctrl-C가 발생하면 여기로 점프해 스캔 상태와 버퍼를 초기화하고 다시 프롬프트를 낸다.
  • 전송 시 버퍼 스왑: SendQuery 이후 previous_bufquery_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)
backslashResult
HandleSlashCmds(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_commandcmd를 키로 하는 평탄한 if/else if 체인이다. 각 분기는 전용 exec_command_* 함수를 호출한다. 체인은 약 50개 명령을 처리한다.

// exec_command dispatch (excerpt) — src/bin/psql/command.c
if (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를 반환해 MainLoopSendQuery를 호출하도록 한다.
  • 연결: \c/\connectdo_connect를 호출하고, do_connectPQconnectdbParams를 호출해 pset.db를 교체한다.
  • 포함/편집: \i/\irprocess_file을 호출해 MainLoop를 재귀 호출한다. \e/\ef/\ev는 외부 에디터를 기동하고 PSQL_CMD_NEWEDIT를 반환해 MainLoop가 편집된 버퍼를 재스캔하도록 한다.
  • 조건 흐름: \if/\elif/\else/\endifcond_stack을 조작한다. 비활성 분기에서도 실행되는 유일한 명령들이다. 중첩 카운팅의 정확성을 유지해야 하기 때문이다.
  • 파이프라인(PG17+): \startpipeline/\endpipeline/\flush/\flushrequest/\syncpipeline/\getresultsPSQL_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_NEWEDITquery_buf가 교체됨; 재스캔 필요
PSQL_CMD_TERMINATEMainLoop 종료
PSQL_CMD_ERROR명령 실패
PSQL_CMD_UNKNOWN인식할 수 없는 명령

SendQuery(const char *query)는 서버로 전송되는 모든 SQL의 단일 진입점이다. MainLoopPSCAN_SEMICOLON 감지, \g 명령, 비인터랙티브 -c 액션 모두 이 함수로 연결된다.

// SendQuery — src/bin/psql/common.c (condensed)
bool
SendQuery(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 int
ExecQueryAndProcessResults(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;
}

AcceptResultPQresultStatus를 불리언으로 분류한다.

// AcceptResult — src/bin/psql/common.c (condensed)
bool
AcceptResult(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_MESSAGEpset.vars에 기록한다.

PSQLexecdescribe.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는 모든 \d 변형을 뒷받침한다. 두 계층으로 구성된다.

1계층 — 디스패치 (command.cexec_command_d): \d 변형 문자열을 올바른 describe.c 함수로 매핑한다. 예를 들어 \dtlistTables("t", pattern, verbose, showSystem)를 호출하고, \dflistFunctions를, \d <name>describeTableDetails를 호출한다.

2계층 — SQL 생성 (describe.c): 시스템 카탈로그를 대상으로 하는 SELECTPQExpBuffer에 조립하고, 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 구조체(typedef struct _psqlSettings)가 모든 공유 상태를 담는다. 주요 필드:

// PsqlSettings (excerpt) — src/bin/psql/settings.h
typedef 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_MODEExecQueryAndProcessResults가 쿼리를 전송하는 방식을 제어하는 열거형(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 기준 힌트다.

  • main — 옵션 파싱, 연결, 액션 디스패치 또는 MainLoop 진입.
  • EstablishVariableSpace — 약 25개 특수 변수의 SetVariableHooks 등록; 훅이 문자열 값을 pset 필드로 변환.
  • parse_psql_optionsgetopt_long 루프; adhoc_optsSimpleActionList를 채운다.
  • process_psqlrc / process_psqlrc_file — 시스템 전체 rc 파일 다음 사용자 수준 rc 파일 로드; .psqlrc-<version> 다음 .psqlrc 순서로 시도.
  • PsqlSettings pset — 단일 전역 설정 인스턴스.
  • MainLoop — 재진입 루프; 소스/lineno 저장·복원; psql_scanSendQuery / 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_connectdo_connect 호출; pset.db 교체.
  • exec_command_includeprocess_file 호출(재진입 MainLoop).
  • exec_command_edit / exec_command_ef_ev$EDITOR 기동; PSQL_CMD_NEWEDIT 반환.
  • exec_command_g / process_command_g_optionspset.gfname, pset.gsavepopt 설정; PSQL_CMD_SEND 반환.
  • exec_command_if / exec_command_elif / exec_command_else / exec_command_endifConditionalStack 조작.
  • exec_command_startpipeline / exec_command_endpipeline / exec_command_flush / exec_command_flushrequest / exec_command_getresults — 파이프라인 제어; pset.send_mode 설정.
  • SendQuery — 최상위 SQL 디스패처; 트랜잭션 래핑, 세이브포인트, ExecQueryAndProcessResults, 전송 후 처리.
  • ExecQueryAndProcessResults — libpq 전송 + 결과 루프; 단순·확장 프로토콜, 파이프라인 모드, \watch 모드 처리.
  • PSQLexec — 우회 단일 쿼리 경로; describe.c가 사용.
  • PSQLexecWatch\watch 변형; is_watch=truemin_rowsExecQueryAndProcessResults 호출.
  • AcceptResultPQresultStatus를 성공/실패로 분류.
  • SetResultVariables$ERROR, $SQLSTATE, $ROW_COUNT 등 기록.
  • SetShellResultVariables$SHELL_ERROR, $SHELL_EXIT_CODE 기록.
  • PrintQueryResultprintQueryPGresult 하나를 포맷하고 출력.
  • ClearOrSaveResult\errverbose를 위해 오류 결과를 pset.last_error_result에 보관; 그 외는 PQclear.
  • pipelineResetpiped_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 — 데이터베이스 목록; \lpsql -l이 사용.
  • listFunctions — 패턴과 일치하는 함수/프로시저 목록.
  • map_typename_pattern\dT를 위한 타입 이름 패턴 정규화.
  • printACLColumn\dp/\z의 ACL 컬럼 포맷.
  • PsqlSettings (_psqlSettings) — 전역 상태 구조체.
  • PSQL_SEND_MODEExecQueryAndProcessResults 전송 경로를 제어하는 열거형.
  • PSQL_ECHO, PSQL_ECHO_HIDDEN, PSQL_ERROR_ROLLBACK, PSQL_COMP_CASE, HistControl — 변수 훅 제어 열거형들.

위치 힌트 (2026-06-05 기준, REL_18 273fe94)

섹션 제목: “위치 힌트 (2026-06-05 기준, REL_18 273fe94)”
심볼파일
mainsrc/bin/psql/startup.c126
EstablishVariableSpacesrc/bin/psql/startup.c1218
PsqlSettings (struct close)src/bin/psql/settings.h187
PSQL_SEND_MODEsrc/bin/psql/settings.h73
MainLoopsrc/bin/psql/mainloop.c33
psqlscan_callbackssrc/bin/psql/mainloop.c20
HandleSlashCmdssrc/bin/psql/command.c231
exec_commandsrc/bin/psql/command.c315
SendQuerysrc/bin/psql/common.c1118
AcceptResultsrc/bin/psql/common.c416
SetResultVariablessrc/bin/psql/common.c476
PSQLexecsrc/bin/psql/common.c655
PSQLexecWatchsrc/bin/psql/common.c710
PrintQueryResultsrc/bin/psql/common.c1039
ExecQueryAndProcessResultssrc/bin/psql/common.c1569
listAllDbssrc/bin/psql/describe.c946
describeTableDetailssrc/bin/psql/describe.c1488
describeOneTableDetailssrc/bin/psql/describe.c1571
listTablessrc/bin/psql/describe.c4007

커밋 273fe94(REL_18_STABLE) 기준으로 검증된 사실들.

  • MainLoop는 실제로 재진입 가능하다. 진입과 종료 시점에 pset.cur_cmd_source, pset.cur_cmd_interactive, pset.lineno를 저장하고 복원한다. \iprocess_fileFILE *을 받아 MainLoop를 호출하며, 재귀 호출 인스턴스들은 각자 독립된 scan_state, cond_stack, query_buf를 갖는다. mainloop.c의 5866번 줄과 656659번 줄의 저장/복원 블록으로 확인.

  • previous_buf / query_buf 스왑은 복사가 아닌 포인터 스왑이다. SendQuery 이후 MainLoop는 임시 변수를 이용해 두 PQExpBuffer * 포인터를 교환한다. 따라서 previous_buf는 항상 할당 없이 마지막으로 전송된 구문을 보관한다. mainloop.cSendQuery 직후 스왑 블록으로 확인.

  • 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.c 107~142번 줄로 확인.

  • PSQLexec는 트랜잭션 래핑과 결과 변수 설정을 건너뛴다. ECHO_HIDDEN 확인 이후 PQexec를 직접 호출한다. command_no_begin을 호출하지 않고, BEGIN을 발행하지 않으며, SetResultVariables를 호출하지 않고, pset.last_error_result도 갱신하지 않는다. common.c 654~700번 줄로 확인.

  • SetResultVariables는 오류 때만이 아니라 모든 SendQuery 결과 이후에 호출된다. 성공 시 ERROR=false, SQLSTATE=00000, PQcmdTuplesROW_COUNT를 기록한다. 실패 시 ERROR=true, SQLSTATE 코드, LAST_ERROR_MESSAGE를 갱신한다. common.c 476~504번 줄로 확인.

  • PSQL_SEND_MODE 열거형은 파이프라인 지원을 위해 추가됐다. 열거형에 PSQL_SEND_START_PIPELINE_MODEPSQL_SEND_END_PIPELINE_MODE가 있으며, 파이프라인 모드가 PG19 전용이 아닌 REL_18 현재 기능임을 확인한다. settings.h 73~84번 줄로 확인.

  • process_psqlrc_file은 부 버전이 붙은 rc 파일을 먼저 시도한다. filename-PG_VERSION(예: .psqlrc-18.1), 다음으로 filename-PG_MAJORVERSION(예: .psqlrc-18), 마지막으로 단순 파일 순서로 시도한다. 버전별 커스터마이징을 허용하는 방식이다. startup.c 813~835번 줄로 확인.

  1. \watch 타이머 메커니즘. exec_command_watchcommand.c에 구현되어 있고 Unix에서 setitimer를 사용한다. 그런데 인터벌 타이머, SIGALRM, MainLoopsigsetjmp 사이의 상호작용은 이 문서에서 완전히 추적되지 않았다. main에서 SIGALRM에 설치되는 empty_signal_handler는 시그널이 직접 동작하는 것이 아니라 깨우기 용도로만 사용됨을 암시한다. 조사 경로: command.cexec_command_watchcommon.cdo_watch 경로.

  2. psql의 \bind와 확장 쿼리 프로토콜. exec_command_bind는 파라미터를 pset.bind_params에 저장하고 pset.send_mode = PSQL_SEND_EXTENDED_QUERY_PARAMS를 설정한다. ExecQueryAndProcessResultsPQsendQueryParams와 단순 PQsendQuery 경로를 어떻게 분기하는지, 파이프라인 모드에서의 오류 복구가 SendQuery의 세이브포인트 메커니즘과 어떻게 상호작용하는지는 부분적으로만 추적됐다. 조사 경로: common.c 1569~2747번 줄의 ExecQueryAndProcessResults.

  3. \copy 구현. command.cexec_command_copycopy.cdo_copy를 호출한다. \copy 명령은 서버 측 파일 경로 대신 libpq COPY 프로토콜로 데이터를 라우팅하는 클라이언트 측 COPY를 구현한다. AcceptResult에서 보이는 PGRES_COPY_IN / PGRES_COPY_OUT 결과 상태와의 상호작용은 이 문서에서 상세히 다루지 않는다.

포인터만 제공한다. 각 항목은 후속 문서의 출발점이다.

  • MySQL mysql CLI. 유사한 readline 기반 루프를 사용하지만 메타 명령 집합은 더 단순하다. 상태 변수($mysql_affected_rows, $mysql_insert_id)는 다른 방식으로 노출된다. psql의 \gset처럼 임의의 결과 컬럼을 이름 있는 셸 변수에 저장하는 기능은 없다.

  • SQLite sqlite3 CLI. 유사한 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.hPSQL_SEND_MODE 열거형과 common.cpipelineReset / 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.cMainLoop, 재진입 루프, Ctrl-C 복구.
  • src/bin/psql/command.cHandleSlashCmds, exec_command, 모든 exec_command_* 함수.
  • src/bin/psql/common.cSendQuery, ExecQueryAndProcessResults, PSQLexec, AcceptResult, SetResultVariables, PrintQueryResult.
  • src/bin/psql/describe.cdescribeTableDetails, describeOneTableDetails, listTables, listAllDbs, listFunctions.
  • src/bin/psql/startup.cmain, EstablishVariableSpace, parse_psql_options, process_psqlrc.
  • src/bin/psql/settings.hPsqlSettings, PSQL_SEND_MODE 열거형, 제어 열거형들.
  • postgres-wire-protocol.md — psql이 libpq로 접근하는 서버 측 FE/BE 프로토콜; PostgresMain과 메시지 디스패치 루프.
  • postgres-portals-prepared.md — psql의 \bind / 확장 쿼리 경로가 생성하는 서버 측 CachedPlanSourcePortal 객체.
  • postgres-pg-dump-restore.mdfe_utils/를 공유하지만 MainLoop를 사용하지 않는 pg_dumppg_restore.
  • postgres-overview-client-protocol.md — 클라이언트 프로토콜 서브카테고리의 섹션 라우터.