W18 · SQLD 종료 주간 · project SQL 5
18주차 코드 뒤풀이: 다섯 SQL을 증거 경계까지 읽기
공용 Compose와 실제 W18 7개 source, 정답이 배포되지 않은 Q27·Q28 예시를 분리합니다. 원문의 310–400분 표기는 D3 채점 30분·D6 복기 30분을 완전히 포함하지 않으므로 실제 필수 블록은 별도로 더해 읽습니다.
01compose.yaml — W18 PostgreSQL 실행실의 문·준비 신호·저장소
runtime/compose.yaml
정본 YAML 런타임 · 정본 · W18-F0118줄 연결18줄 번역5 chunks
compose.yaml — W18 PostgreSQL 실행실의 문·준비 신호·저장소
runtime/compose.yaml
정본 YAML 런타임 · 정본 · W18-F01STEP 01 / 13
오늘 이 코드에서 해결할 문제
무엇을 이해해야 하는지 질문부터 잡습니다.
W18 owner가 disposable PostgreSQL 17.10 service를 올릴 때 공유하는 Compose runtime이다.
- service=db은 어느 줄에서 만들어지거나 검사될까?
- postgres:17.10-alpine은 실제 계산값인가 고정 marker인가?
- tag는 digest가 아니다 경계에서 첫 실패는 어디일까?
- 빈 집합·NULL·동률·순서 역전 중 어떤 반례가 중요한가?
- 이 source가 책임지지 않는 lifecycle·provenance는 무엇인가?
service=dbpostgres:17.10-alpinefinancial_core/apphealth=2s/2s/30named volumeSTEP 02 / 13
아주 짧게: 이 코드는 왜 필요할까?
웹소설 대신 이 코드가 필요한 이유만 두 문단으로 쉽게 봅니다.
STARRY가 source를 값·순서·증명 경계가 적힌 작은 실험 카드로 나눈다.
compose.yaml — W18 PostgreSQL 실행실의 문·준비 신호·저장소를 실행 전 검사표로 바꾸기
W18 owner가 disposable PostgreSQL 17.10 service를 올릴 때 공유하는 Compose runtime이다.
핵심 관찰값 service=db, postgres:17.10-alpine, financial_core/app, health=2s/2s/30, named volume을 따라가되, tag는 digest가 아니다까지 함께 표시해 Green 문구를 과대해석하지 않는다.
딱 여기까지만 비유는 순서를 기억하게 할 뿐 SQL NULL, native exit, hash, cleanup의 실제 proof를 대신하지 않는다.
STEP 03 / 13
초등학생도 이해하는 설명
생활 비유와 실제 코드의 경계를 함께 확인합니다.
service and image
1~3줄을 한 덩어리로 읽어 W18 owner가 disposable PostgreSQL 17.10 service를 올릴 때 공유하는 Compose runtime이다.의 1번째 단계를 확인한다.
- 코드 연결
1~3줄- 비유
- 긴 조립 설명서에서 같은 일을 하는 부품만 한 봉투에 담아 확인한다.
- 비유의 끝
- tag는 digest가 아니다
host port mapping
4~5줄을 한 덩어리로 읽어 W18 owner가 disposable PostgreSQL 17.10 service를 올릴 때 공유하는 Compose runtime이다.의 2번째 단계를 확인한다.
- 코드 연결
4~5줄- 비유
- 긴 조립 설명서에서 같은 일을 하는 부품만 한 봉투에 담아 확인한다.
- 비유의 끝
- pg_isready는 SQL 정답을 증명하지 않는다
database initialization
6~9줄을 한 덩어리로 읽어 W18 owner가 disposable PostgreSQL 17.10 service를 올릴 때 공유하는 Compose runtime이다.의 3번째 단계를 확인한다.
- 코드 연결
6~9줄- 비유
- 긴 조립 설명서에서 같은 일을 하는 부품만 한 봉투에 담아 확인한다.
- 비유의 끝
- Container mode는 이 Compose lifecycle을 소유하지 않는다
왜 먼저 보는가
-
이 파일은 왜 필요한가요?
-
W18 PostgreSQL 실행실의 문·준비 신호·저장소의 출발 계약을 먼저 고정해야 해.
-
직접 보장하는 값은 service=db 범위다.
-
목적과 결과를 같은 문장으로 섞지 않겠습니다.
고정값 읽기
-
숫자나 marker는 그냥 외우면 되나요?
-
아니, fixture·순서와 함께 postgres:17.10-alpine을 읽어야 해.
-
고정값은 현재 source의 관찰 계약이지 모든 입력의 일반 법칙은 아니다.
-
입력→처리→결과 표로 다시 적어 볼게요.
STEP 04 / 13
비유 ↔ 코드 전체 연결표
감사 규칙상 연결 대상인 원본 18줄을 빠짐없이 연결합니다.
| 줄 | 정확한 원본 줄 | STARRY 비유 | 실제 뜻·입력·결과·한계 |
|---|---|---|---|
| 1줄F01-L01 | services: |
다음 설정이나 계산을 담을 새 서랍을 연다. | Compose 문서의 services 최상위 mapping을 연다.
|
| 2줄F01-L02 | db: |
다음 설정이나 계산을 담을 새 서랍을 연다. | 이름이 db인 단일 service 정의를 연다.
|
| 3줄F01-L03 | image: |
한 단계의 안내표를 읽어 다음 상태표로 넘긴다. | db container image tag를 postgres:17.10-alpine으로 지정한다.
|
| 4줄F01-L04 | ports: |
다음 설정이나 계산을 담을 새 서랍을 연다. | host와 container 사이 port mapping 목록을 연다.
|
| 5줄F01-L05 | - "${ |
한 단계의 안내표를 읽어 다음 상태표로 넘긴다. | FCL_DB_PORT가 비면 host 5432를 container 5432에 연결한다.
|
| 6줄F01-L06 | environment: |
다음 설정이나 계산을 담을 새 서랍을 연다. | PostgreSQL image가 읽을 초기화 환경 mapping을 연다.
|
| 7줄F01-L07 | POSTGRES_DB: |
한 단계의 안내표를 읽어 다음 상태표로 넘긴다. | 초기 database 이름을 financial_core로 지정한다.
|
| 8줄F01-L08 | POSTGRES_USER: |
한 단계의 안내표를 읽어 다음 상태표로 넘긴다. | 초기 database user를 app으로 지정한다.
|
| 9줄F01-L09 | POSTGRES_PASSWORD: |
한 단계의 안내표를 읽어 다음 상태표로 넘긴다. | FCL_DB_PASSWORD가 없거나 비면 Compose 보간 단계에서 실패시킨다.
|
| 10줄F01-L10 | healthcheck: |
다음 설정이나 계산을 담을 새 서랍을 연다. | db service의 healthcheck 정의를 연다.
|
| 11줄F01-L11 | test: |
한 단계의 안내표를 읽어 다음 상태표로 넘긴다. | app user와 financial_core database에 pg_isready를 실행한다.
|
| 12줄F01-L12 | interval: |
한 단계의 안내표를 읽어 다음 상태표로 넘긴다. | healthcheck 반복 간격을 2초로 지정한다.
|
| 13줄F01-L13 | timeout: |
한 단계의 안내표를 읽어 다음 상태표로 넘긴다. | 각 healthcheck 시도의 timeout을 2초로 지정한다.
|
| 14줄F01-L14 | retries: |
한 단계의 안내표를 읽어 다음 상태표로 넘긴다. | 연속 실패 임계를 30으로 지정한다.
|
| 15줄F01-L15 | volumes: |
다음 설정이나 계산을 담을 새 서랍을 연다. | service mount 목록을 연다.
|
| 16줄F01-L16 | - financial-core-db: |
한 단계의 안내표를 읽어 다음 상태표로 넘긴다. | financial-core-db volume을 PostgreSQL data directory에 mount한다.
|
| 18줄F01-L18 | volumes: |
다음 설정이나 계산을 담을 새 서랍을 연다. | 문서 최상위 named volume mapping을 연다.
|
| 19줄F01-L19 | financial-core-db: |
다음 설정이나 계산을 담을 새 서랍을 연다. | financial-core-db라는 named volume을 선언한다.
|
Green의 작은 범위
-
Green 문구가 보이면 전부 안전한가요?
-
그 문구가 직접 검사한 조건까지만 안전해.
-
특히 tag는 digest가 아니다 경계는 별도 proof가 필요하다.
-
증명한 것과 안 한 것을 두 칸으로 나누겠습니다.
STEP 05 / 13
원본 코드 조각
원본을 5개 의미 조각으로 나누어 그대로 확인합니다.
파일을 한꺼번에 외우지 않고 실행 의미가 이어지는 작은 조각으로 봅니다. 아래 코드는 hash로 고정한 native 정본 source에서 그대로 잘랐습니다.
services:
db:
image: postgres:17.10-alpine
ports:
- "${FCL_DB_PORT:-5432}:5432"
environment:
POSTGRES_DB: financial_core
POSTGRES_USER: app
POSTGRES_PASSWORD: ${FCL_DB_PASSWORD:?set FCL_DB_PASSWORD in this terminal}
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app -d financial_core"]
interval: 2s
timeout: 2s
retries: 30
volumes:
- financial-core-db:/var/lib/postgresql/data
volumes:
financial-core-db:
STEP 06 / 13
코드 한 줄씩 한국어로 번역
비어 있지 않은 18줄을 모두 한국어로 옮깁니다.
비어 있지 않은 원본 줄은 하나도 생략하지 않습니다.
| 줄 | 원본 | 한국어 번역 |
|---|---|---|
| 1 | services: | Compose 문서의 services 최상위 mapping을 연다. |
| 2 | db: | 이름이 db인 단일 service 정의를 연다. |
| 3 | image: postgres:17.10-alpine | db container image tag를 postgres:17.10-alpine으로 지정한다. |
| 4 | ports: | host와 container 사이 port mapping 목록을 연다. |
| 5 | - "${FCL_DB_PORT:-5432}:5432" | FCL_DB_PORT가 비면 host 5432를 container 5432에 연결한다. |
| 6 | environment: | PostgreSQL image가 읽을 초기화 환경 mapping을 연다. |
| 7 | POSTGRES_DB: financial_core | 초기 database 이름을 financial_core로 지정한다. |
| 8 | POSTGRES_USER: app | 초기 database user를 app으로 지정한다. |
| 9 | POSTGRES_PASSWORD: ${FCL_DB_PASSWORD:?set FCL_DB_PASSWORD in this terminal} | FCL_DB_PASSWORD가 없거나 비면 Compose 보간 단계에서 실패시킨다. |
| 10 | healthcheck: | db service의 healthcheck 정의를 연다. |
| 11 | test: ["CMD-SHELL", "pg_isready -U app -d financial_core"] | app user와 financial_core database에 pg_isready를 실행한다. |
| 12 | interval: 2s | healthcheck 반복 간격을 2초로 지정한다. |
| 13 | timeout: 2s | 각 healthcheck 시도의 timeout을 2초로 지정한다. |
| 14 | retries: 30 | 연속 실패 임계를 30으로 지정한다. |
| 15 | volumes: | service mount 목록을 연다. |
| 16 | - financial-core-db:/var/lib/postgresql/data | financial-core-db volume을 PostgreSQL data directory에 mount한다. |
| 18 | volumes: | 문서 최상위 named volume mapping을 연다. |
| 19 | financial-core-db: | financial-core-db라는 named volume을 선언한다. |
STEP 07 / 13
기존 수준의 한 줄 읽기·문법 해부
쉬운 설명 다음에 문법과 실행 순서를 정밀하게 읽습니다.
한 줄로 읽기W18 owner가 disposable PostgreSQL 17.10 service를 올릴 때 공유하는 Compose runtime이다. 다만 tag는 digest가 아니다
문법 해부
- YAML 들여쓰기와 list marker가 service model의 소속을 정한다.
- ${VAR:-default}와 ${VAR:?message}는 blank 처리 의미가 다르다.
- host:container port와 volume:container-path의 콜론은 서로 다른 mapping이다.
실행 순서
- Compose parse
- environment interpolation
- container create
- PostgreSQL init
- health monitoring
- volume-backed writes
원래 W6 수준의 조각별 정밀 해설
F01-C01 · service and image
- 문법 해부
- `service and image` 범위는 Compose 문서의 services 최상위 mapping을 연다. 이어서 db container image tag를 postgres:17.10-alpine으로 지정한다.
- 실제 값 추적
- 범위 시작 입력은 runtime/compose.yaml의 1줄 직전까지 형성된 parser·실행 상태를 입력으로 받는다. 끝에서 관찰할 상태는 pull·create 대상 image reference가 정해진다.
- 정상 예
- 동결 source 1~3줄을 그대로 적용하면 pull·create 대상 image reference가 정해진다.
- 틀린 예·반례
- tag는 digest가 아니다 상황을 정상 예로 간주하면 이 chunk의 Green 범위를 넘는다.
- 착각 방지
- 이 범위의 고정값은 service=db이지만 marker·형식만으로 전체 실행을 인증하지 않는다.
- 하지 않는 일
- 현재 chunk가 직접 보장하지 않는 경계: tag는 digest가 아니다
- 다음 연결
- 끝 상태를 보존한 뒤 `host port mapping` 범위에서 다음 입력·결과를 확인한다.
F01-C02 · host port mapping
- 문법 해부
- `host port mapping` 범위는 host와 container 사이 port mapping 목록을 연다. 이어서 FCL_DB_PORT가 비면 host 5432를 container 5432에 연결한다.
- 실제 값 추적
- 범위 시작 입력은 runtime/compose.yaml의 4줄 직전까지 형성된 parser·실행 상태를 입력으로 받는다. 끝에서 관찰할 상태는 host 접속점 후보가 5432 또는 지정 port로 계산된다.
- 정상 예
- 동결 source 4~5줄을 그대로 적용하면 host 접속점 후보가 5432 또는 지정 port로 계산된다.
- 틀린 예·반례
- pg_isready는 SQL 정답을 증명하지 않는다 상황을 정상 예로 간주하면 이 chunk의 Green 범위를 넘는다.
- 착각 방지
- 이 범위의 고정값은 postgres:17.10-alpine이지만 marker·형식만으로 전체 실행을 인증하지 않는다.
- 하지 않는 일
- 현재 chunk가 직접 보장하지 않는 경계: tag는 digest가 아니다
- 다음 연결
- 끝 상태를 보존한 뒤 `database initialization` 범위에서 다음 입력·결과를 확인한다.
F01-C03 · database initialization
- 문법 해부
- `database initialization` 범위는 PostgreSQL image가 읽을 초기화 환경 mapping을 연다. 이어서 FCL_DB_PASSWORD가 없거나 비면 Compose 보간 단계에서 실패시킨다.
- 실제 값 추적
- 범위 시작 입력은 runtime/compose.yaml의 6줄 직전까지 형성된 parser·실행 상태를 입력으로 받는다. 끝에서 관찰할 상태는 password 부재가 container 생성 전에 드러난다.
- 정상 예
- 동결 source 6~9줄을 그대로 적용하면 password 부재가 container 생성 전에 드러난다.
- 틀린 예·반례
- Container mode는 이 Compose lifecycle을 소유하지 않는다 상황을 정상 예로 간주하면 이 chunk의 Green 범위를 넘는다.
- 착각 방지
- 이 범위의 고정값은 financial_core/app이지만 marker·형식만으로 전체 실행을 인증하지 않는다.
- 하지 않는 일
- 현재 chunk가 직접 보장하지 않는 경계: Container mode는 이 Compose lifecycle을 소유하지 않는다
- 다음 연결
- 끝 상태를 보존한 뒤 `readiness probe` 범위에서 다음 입력·결과를 확인한다.
F01-C04 · readiness probe
- 문법 해부
- `readiness probe` 범위는 db service의 healthcheck 정의를 연다. 이어서 연속 실패 임계를 30으로 지정한다.
- 실제 값 추적
- 범위 시작 입력은 runtime/compose.yaml의 10줄 직전까지 형성된 parser·실행 상태를 입력으로 받는다. 끝에서 관찰할 상태는 연속 실패 임계가 health state machine에 들어간다.
- 정상 예
- 동결 source 10~14줄을 그대로 적용하면 연속 실패 임계가 health state machine에 들어간다.
- 틀린 예·반례
- tag는 digest가 아니다 상황을 정상 예로 간주하면 이 chunk의 Green 범위를 넘는다.
- 착각 방지
- 이 범위의 고정값은 health=2s/2s/30이지만 marker·형식만으로 전체 실행을 인증하지 않는다.
- 하지 않는 일
- 현재 chunk가 직접 보장하지 않는 경계: tag는 digest가 아니다
- 다음 연결
- 끝 상태를 보존한 뒤 `named volume` 범위에서 다음 입력·결과를 확인한다.
F01-C05 · named volume
- 문법 해부
- `named volume` 범위는 service mount 목록을 연다. 이어서 financial-core-db라는 named volume을 선언한다.
- 실제 값 추적
- 범위 시작 입력은 runtime/compose.yaml의 15줄 직전까지 형성된 parser·실행 상태를 입력으로 받는다. 끝에서 관찰할 상태는 mount가 참조할 top-level volume object가 완성된다.
- 정상 예
- 동결 source 15~19줄을 그대로 적용하면 mount가 참조할 top-level volume object가 완성된다.
- 틀린 예·반례
- pg_isready는 SQL 정답을 증명하지 않는다 상황을 정상 예로 간주하면 이 chunk의 Green 범위를 넘는다.
- 착각 방지
- 이 범위의 고정값은 named volume이지만 marker·형식만으로 전체 실행을 인증하지 않는다.
- 하지 않는 일
- 현재 chunk가 직접 보장하지 않는 경계: Container mode는 이 Compose lifecycle을 소유하지 않는다
- 다음 연결
- 끝 상태를 보존한 뒤 `파일 전체 source와 책임 경계` 범위에서 다음 입력·결과를 확인한다.
반례 먼저
-
정상 fixture만 보면 충분하지 않나요?
-
빈 집합·NULL·순서 역전 같은 반례도 넣어야 해.
-
여기서는 pg_isready는 SQL 정답을 증명하지 않는다을 먼저 흔들어 본다.
-
첫 실패 지점을 줄 번호와 함께 기록하겠습니다.
STEP 08 / 13
실제 값 따라가기
같은 입력값이 어느 줄을 지나 어떤 결과가 되는지 추적합니다.
| 순서 | 들어온 값 | 코드가 하는 일 | 나온 값·상태 | 경계 |
|---|---|---|---|---|
| 1 | service=db | F01의 실행 순서 1단계에 넣어 source 조건을 적용한다. | 관찰 가능한 W18 상태에 `service=db`가 기록되거나 그 값으로 비교된다. | tag는 digest가 아니다 |
| 2 | postgres:17.10-alpine | F01의 실행 순서 2단계에 넣어 source 조건을 적용한다. | 관찰 가능한 W18 상태에 `postgres:17.10-alpine`가 기록되거나 그 값으로 비교된다. | pg_isready는 SQL 정답을 증명하지 않는다 |
| 3 | financial_core/app | F01의 실행 순서 3단계에 넣어 source 조건을 적용한다. | 관찰 가능한 W18 상태에 `financial_core/app`가 기록되거나 그 값으로 비교된다. | Container mode는 이 Compose lifecycle을 소유하지 않는다 |
| 4 | health=2s/2s/30 | F01의 실행 순서 4단계에 넣어 source 조건을 적용한다. | 관찰 가능한 W18 상태에 `health=2s/2s/30`가 기록되거나 그 값으로 비교된다. | tag는 digest가 아니다 |
전체 다시 쓰기
-
한 줄이 너무 길어서 읽기 힘들어요.
-
화면에서는 줄바꿈해 보되 source bytes와 실행 순서는 그대로 보존해.
-
마지막에는 named volume을 source와 다시 대조한다.
-
뜻→chunk→전체 코드 순서로 복원하겠습니다.
STEP 09 / 13
PowerShell·SQL·DB 내부에서 벌어지는 일
PowerShell·SQL·DB에서 실제로 일어나는 일과 증명 범위를 구분합니다.
W18 owner가 disposable PostgreSQL 17.10 service를 올릴 때 공유하는 Compose runtime이다.에서 1번째 내부 책임을 수행한다.
tag는 digest가 아니다W18 owner가 disposable PostgreSQL 17.10 service를 올릴 때 공유하는 Compose runtime이다.에서 2번째 내부 책임을 수행한다.
pg_isready는 SQL 정답을 증명하지 않는다W18 owner가 disposable PostgreSQL 17.10 service를 올릴 때 공유하는 Compose runtime이다.에서 3번째 내부 책임을 수행한다.
Container mode는 이 Compose lifecycle을 소유하지 않는다W18 owner가 disposable PostgreSQL 17.10 service를 올릴 때 공유하는 Compose runtime이다.에서 4번째 내부 책임을 수행한다.
tag는 digest가 아니다STEP 10 / 13
흔한 착각과 틀린 예
그럴듯하지만 틀린 해석을 반례로 고칩니다.
❌ service=db이 보이면 관련 실행 전체가 완전하게 증명된다.
왜 틀리나 tag는 digest가 아니다
바르게 읽기 고정 fixture에서 직접 검사한 값과 owner·DB·운영 계층의 나머지 책임을 분리한다.
반례 tag는 digest가 아니다 상황에서는 같은 marker·형식이 보여도 의도한 전체 의미가 성립하지 않을 수 있다.
❌ postgres:17.10-alpine이 보이면 관련 실행 전체가 완전하게 증명된다.
왜 틀리나 pg_isready는 SQL 정답을 증명하지 않는다
바르게 읽기 고정 fixture에서 직접 검사한 값과 owner·DB·운영 계층의 나머지 책임을 분리한다.
반례 pg_isready는 SQL 정답을 증명하지 않는다 상황에서는 같은 marker·형식이 보여도 의도한 전체 의미가 성립하지 않을 수 있다.
❌ financial_core/app이 보이면 관련 실행 전체가 완전하게 증명된다.
왜 틀리나 Container mode는 이 Compose lifecycle을 소유하지 않는다
바르게 읽기 고정 fixture에서 직접 검사한 값과 owner·DB·운영 계층의 나머지 책임을 분리한다.
반례 Container mode는 이 Compose lifecycle을 소유하지 않는다 상황에서는 같은 marker·형식이 보여도 의도한 전체 의미가 성립하지 않을 수 있다.
❌ health=2s/2s/30이 보이면 관련 실행 전체가 완전하게 증명된다.
왜 틀리나 tag는 digest가 아니다
바르게 읽기 고정 fixture에서 직접 검사한 값과 owner·DB·운영 계층의 나머지 책임을 분리한다.
반례 tag는 digest가 아니다 상황에서는 같은 marker·형식이 보여도 의도한 전체 의미가 성립하지 않을 수 있다.
❌ named volume이 보이면 관련 실행 전체가 완전하게 증명된다.
왜 틀리나 pg_isready는 SQL 정답을 증명하지 않는다
바르게 읽기 고정 fixture에서 직접 검사한 값과 owner·DB·운영 계층의 나머지 책임을 분리한다.
반례 pg_isready는 SQL 정답을 증명하지 않는다 상황에서는 같은 marker·형식이 보여도 의도한 전체 의미가 성립하지 않을 수 있다.
STEP 11 / 13
이 코드가 보장하지 않는 것
이 코드가 책임지지 않는 일을 분리합니다.
tag는 digest가 아니다
이 책임을 맡는 곳: caller path policypg_isready는 SQL 정답을 증명하지 않는다
이 책임을 맡는 곳: exact oracle gateContainer mode는 이 Compose lifecycle을 소유하지 않는다
이 책임을 맡는 곳: SQL/parser counterexample testtag는 digest가 아니다
이 책임을 맡는 곳: runtime ownerpg_isready는 SQL 정답을 증명하지 않는다
이 책임을 맡는 곳: byte-pinned source auditSTEP 12 / 13
직접 다시 써보기
뜻 → 조각 → 전체 코드 순서로 다시 씁니다.
1단계 · 뜻부터 복원
W18 owner가 disposable PostgreSQL 17.10 service를 올릴 때 공유하는 Compose runtime이다.를 고정값과 미보장 경계까지 한 문장으로 말한다.
2단계 · 코드 조각 재조립
- service and image
- host port mapping
- database initialization
- readiness probe
- named volume
3단계 · 파일 전체 다시 쓰기
19개 물리 줄을 원본 순서로 다시 쓰고 SHA-256 3ef6ca229dfa8d54120fb66f2215b906ba8155784d32791d7f93f3be879ade8d와 대조한다.
자가 점검
- 정본과 학습용 예시 label을 바꾸지 않는다.
- 긴 한 줄은 화면에서 감싸도 source exact text를 바꾸지 않는다.
- marker literal과 실제 결과 row를 구분한다.
- NULL·순서·empty fixture 반례를 하나 이상 말한다.
- owner mode와 cleanup 책임을 분리한다.
STEP 13 / 13
전체 원본 정답
감사로 고정한 전체 source를 가감 없이 확인합니다.
정본 전체 코드 확인하기
services:
db:
image: postgres:17.10-alpine
ports:
- "${FCL_DB_PORT:-5432}:5432"
environment:
POSTGRES_DB: financial_core
POSTGRES_USER: app
POSTGRES_PASSWORD: ${FCL_DB_PASSWORD:?set FCL_DB_PASSWORD in this terminal}
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app -d financial_core"]
interval: 2s
timeout: 2s
retries: 30
volumes:
- financial-core-db:/var/lib/postgresql/data
volumes:
financial-core-db:
02run-w18-project-sql5.ps1 — fixture와 5개 SQL의 실행 owner
scripts/run-w18-project-sql5.ps1
정본 PowerShell W18 owner · 정본 · W18-F0220줄 연결20줄 번역7 chunks
run-w18-project-sql5.ps1 — fixture와 5개 SQL의 실행 owner
scripts/run-w18-project-sql5.ps1
정본 PowerShell W18 owner · 정본 · W18-F02STEP 01 / 13
오늘 이 코드에서 해결할 문제
무엇을 이해해야 하는지 질문부터 잡습니다.
fixture와 정확히 다섯 SQL을 순차 실행하고 marker·native exit·관찰 hash를 manifest로 기록하는 W18 owner다.
- modes=Compose/Container은 어느 줄에서 만들어지거나 검사될까?
- files=5은 실제 계산값인가 고정 marker인가?
- SqlRoot와 EvidenceDir은 absolute만 요구하고 containment를 강제하지 않는다 경계에서 첫 실패는 어디일까?
- 빈 집합·NULL·동률·순서 역전 중 어떤 반례가 중요한가?
- 이 source가 책임지지 않는 lifecycle·provenance는 무엇인가?
modes=Compose/Containerfiles=5native_exits=0rows=5hashes=6cleanup=1 literalSTEP 02 / 13
아주 짧게: 이 코드는 왜 필요할까?
웹소설 대신 이 코드가 필요한 이유만 두 문단으로 쉽게 봅니다.
STARRY가 source를 값·순서·증명 경계가 적힌 작은 실험 카드로 나눈다.
run-w18-project-sql5.ps1 — fixture와 5개 SQL의 실행 owner를 실행 전 검사표로 바꾸기
fixture와 정확히 다섯 SQL을 순차 실행하고 marker·native exit·관찰 hash를 manifest로 기록하는 W18 owner다.
핵심 관찰값 modes=Compose/Container, files=5, native_exits=0, rows=5, hashes=6, cleanup=1 literal을 따라가되, SqlRoot와 EvidenceDir은 absolute만 요구하고 containment를 강제하지 않는다까지 함께 표시해 Green 문구를 과대해석하지 않는다.
딱 여기까지만 비유는 순서를 기억하게 할 뿐 SQL NULL, native exit, hash, cleanup의 실제 proof를 대신하지 않는다.
STEP 03 / 13
초등학생도 이해하는 설명
생활 비유와 실제 코드의 경계를 함께 확인합니다.
parameters
1~5줄을 한 덩어리로 읽어 fixture와 정확히 다섯 SQL을 순차 실행하고 marker·native exit·관찰 hash를 manifest로 기록하는 W18 owner다.의 1번째 단계를 확인한다.
- 코드 연결
1~5줄- 비유
- 긴 조립 설명서에서 같은 일을 하는 부품만 한 봉투에 담아 확인한다.
- 비유의 끝
- SqlRoot와 EvidenceDir은 absolute만 요구하고 containment를 강제하지 않는다
path and ownership setup
6~10줄을 한 덩어리로 읽어 fixture와 정확히 다섯 SQL을 순차 실행하고 marker·native exit·관찰 hash를 manifest로 기록하는 W18 owner다.의 2번째 단계를 확인한다.
- 코드 연결
6~10줄- 비유
- 긴 조립 설명서에서 같은 일을 하는 부품만 한 봉투에 담아 확인한다.
- 비유의 끝
- marker substring은 결과값·순서·유일성을 인증하지 않는다
Compose or supplied-container resolution
11~16줄을 한 덩어리로 읽어 fixture와 정확히 다섯 SQL을 순차 실행하고 marker·native exit·관찰 hash를 manifest로 기록하는 W18 owner다.의 3번째 단계를 확인한다.
- 코드 연결
11~16줄- 비유
- 긴 조립 설명서에서 같은 일을 하는 부품만 한 봉투에 담아 확인한다.
- 비유의 끝
- substring 통과 문자열이 줄 시작 W18_Q가 아니면 captured marker는 null일 수 있다
왜 먼저 보는가
-
이 파일은 왜 필요한가요?
-
fixture와 5개 SQL의 실행 owner의 출발 계약을 먼저 고정해야 해.
-
직접 보장하는 값은 modes=Compose/Container 범위다.
-
목적과 결과를 같은 문장으로 섞지 않겠습니다.
고정값 읽기
-
숫자나 marker는 그냥 외우면 되나요?
-
아니, fixture·순서와 함께 files=5을 읽어야 해.
-
고정값은 현재 source의 관찰 계약이지 모든 입력의 일반 법칙은 아니다.
-
입력→처리→결과 표로 다시 적어 볼게요.
STEP 04 / 13
비유 ↔ 코드 전체 연결표
감사 규칙상 연결 대상인 원본 20줄을 빠짐없이 연결합니다.
| 줄 | 정확한 원본 줄 | STARRY 비유 | 실제 뜻·입력·결과·한계 |
|---|---|---|---|
| 1줄F02-L01 | param( |
다음 설정이나 계산을 담을 새 서랍을 연다. | PowerShell parameter block을 시작한다.
|
| 2줄F02-L02 | [ |
한 단계의 안내표를 읽어 다음 상태표로 넘긴다. | 실행 mode를 Compose 또는 Container로 제한하고 container 이름 입력을 선언한다.
|
| 3줄F02-L03 | [ |
한 단계의 안내표를 읽어 다음 상태표로 넘긴다. | Compose project 기본값과 필수 SqlRoot 입력을 선언한다.
|
| 4줄F02-L04 | [ |
한 단계의 안내표를 읽어 다음 상태표로 넘긴다. | 필수 EvidenceDir 입력을 선언한다.
|
| 5줄F02-L05 | ) |
한 단계의 안내표를 읽어 다음 상태표로 넘긴다. | parameter block을 닫는다.
|
| 6줄F02-L06 | $ErrorActionPreference= |
한 단계의 안내표를 읽어 다음 상태표로 넘긴다. | 오류를 terminating error로 바꾸고 script의 parent를 project root로 잡는다.
|
| 7줄F02-L07 | if( |
검사표가 어긋나면 다음 방으로 가지 못하게 비상벨을 울린다. | SqlRoot가 absolute path가 아니면 즉시 거절한다.
|
| 8줄F02-L08 | if( |
검사표가 어긋나면 다음 방으로 가지 못하게 비상벨을 울린다. | EvidenceDir가 absolute path가 아니면 즉시 거절한다.
|
| 9줄F02-L09 | $sql= |
한 단계의 안내표를 읽어 다음 상태표로 넘긴다. | SqlRoot를 resolve하고 EvidenceDir를 정규화한 뒤 directory를 만든다.
|
| 10줄F02-L10 | $compose= |
한 단계의 안내표를 읽어 다음 상태표로 넘긴다. | compose.yaml path를 만들고 아직 소유하지 않은 상태로 시작한다.
|
| 11줄F02-L11 | try{ |
한 단계의 안내표를 읽어 다음 상태표로 넘긴다. | 실행과 cleanup을 묶는 try block을 시작한다.
|
| 12줄F02-L12 | if( |
한 단계의 안내표를 읽어 다음 상태표로 넘긴다. | Compose mode 분기로 들어간다.
|
| 13줄F02-L13 | if( |
검사표가 어긋나면 다음 방으로 가지 못하게 비상벨을 울린다. | Compose mode의 Container 인자를 거절하고 blank password에는 disposable 값을 넣는다.
|
| 14줄F02-L14 | & docker compose -f $compose -p $ComposeProject up -d --wait db; |
검사표가 어긋나면 다음 방으로 가지 못하게 비상벨을 울린다. | db를 up --wait로 시작하고 성공 뒤 소유권을 잡아 container id를 구한다.
|
| 15줄F02-L15 | }elseif( |
검사표가 어긋나면 다음 방으로 가지 못하게 비상벨을 울린다. | Container mode인데 이름이 비었으면 거절한다.
|
| 16줄F02-L16 | if( |
검사표가 어긋나면 다음 방으로 가지 못하게 비상벨을 울린다. | 두 mode 분기 뒤에도 container가 비면 resolution 실패로 끝낸다.
|
| 17줄F02-L17 | $fixture= |
검사표가 어긋나면 다음 방으로 가지 못하게 비상벨을 울린다. | fixture.sql을 raw로 psql에 보내고 native exit가 0인지 확인한다.
|
| 18줄F02-L18 | $files= |
검사표가 어긋나면 다음 방으로 가지 못하게 비상벨을 울린다. | 다섯 파일을 순서대로 raw 실행하고 exit·W18_Qn substring·마지막 marker·사후 hash를 모은다.
|
| 19줄F02-L19 | $hashes= |
한 단계의 안내표를 읽어 다음 상태표로 넘긴다. | fixture 포함 여섯 hash와 결과를 manifest에 쓰고 고정 Green 문자열을 출력한다.
|
| 20줄F02-L20 | }finally{ |
검사표가 어긋나면 다음 방으로 가지 못하게 비상벨을 울린다. | 소유한 Compose project만 down -v하고 cleanup 실패를 예외로 올린다.
|
Green의 작은 범위
-
Green 문구가 보이면 전부 안전한가요?
-
그 문구가 직접 검사한 조건까지만 안전해.
-
특히 SqlRoot와 EvidenceDir은 absolute만 요구하고 containment를 강제하지 않는다 경계는 별도 proof가 필요하다.
-
증명한 것과 안 한 것을 두 칸으로 나누겠습니다.
STEP 05 / 13
원본 코드 조각
원본을 7개 의미 조각으로 나누어 그대로 확인합니다.
파일을 한꺼번에 외우지 않고 실행 의미가 이어지는 작은 조각으로 봅니다. 아래 코드는 hash로 고정한 native 정본 source에서 그대로 잘랐습니다.
param(
[ValidateSet('Compose','Container')][string]$Mode='Compose',[string]$Container='',
[string]$ComposeProject='w18-project-sql-lab',[Parameter(Mandatory=$true)][string]$SqlRoot,
[Parameter(Mandatory=$true)][string]$EvidenceDir
)
$ErrorActionPreference='Stop';$root=Split-Path -Parent $PSScriptRoot
if(-not [IO.Path]::IsPathRooted($SqlRoot)){throw 'SqlRoot must be an absolute packaged SQL path'}
if(-not [IO.Path]::IsPathRooted($EvidenceDir)){throw 'EvidenceDir must be an absolute learner-owned path'}
$sql=(Resolve-Path -LiteralPath $SqlRoot).Path;$e=[IO.Path]::GetFullPath($EvidenceDir);New-Item -ItemType Directory -Force $e|Out-Null
$compose=Join-Path $root 'compose.yaml';$owned=$false
try{
if($Mode-eq'Compose'){
if($Container){throw 'W18 Compose mode rejects -Container'};if([string]::IsNullOrWhiteSpace($env:FCL_DB_PASSWORD)){$env:FCL_DB_PASSWORD='w18-disposable-password'}
& docker compose -f $compose -p $ComposeProject up -d --wait db;if($LASTEXITCODE-ne 0){throw "W18 compose up exit=$LASTEXITCODE"};$owned=$true;$Container=(& docker compose -f $compose -p $ComposeProject ps -q db).Trim()
}elseif([string]::IsNullOrWhiteSpace($Container)){throw 'W18 Container mode requires -Container'}
if([string]::IsNullOrWhiteSpace($Container)){throw 'W18 PostgreSQL container resolution failed'}
$fixture='fixture.sql';Get-Content -Raw (Join-Path $sql $fixture)|docker exec -i $Container psql -v ON_ERROR_STOP=1 -U app -d financial_core;if($LASTEXITCODE-ne 0){throw "W18 fixture exit=$LASTEXITCODE"}
$files=@('01-reconcile.sql','02-anti-join.sql','03-aggregation.sql','04-running-balance.sql','05-keyset.sql');$results=@();for($i=0;$i-lt 5;$i++){$out=@(Get-Content -Raw (Join-Path $sql $files[$i])|docker exec -i $Container psql -At -v ON_ERROR_STOP=1 -U app -d financial_core 2>&1);$exit=$LASTEXITCODE;if($exit-ne 0-or($out-join"`n")-cnotmatch "W18_Q$($i+1) "){throw "W18 file=$($files[$i]) exit=$exit output=$out"};$results+=[pscustomobject]@{ordinal=$i+1;file=$files[$i];native_exit=$exit;marker=($out|Where-Object{$_-match'^W18_Q'}|Select-Object -Last 1);sha256=(Get-FileHash -Algorithm SHA256 (Join-Path $sql $files[$i])).Hash.ToLowerInvariant()}}
$hashes=@($fixture)+$files|ForEach-Object{[pscustomobject]@{file=$_;sha256=(Get-FileHash -Algorithm SHA256 (Join-Path $sql $_)).Hash.ToLowerInvariant()}};$manifest=[ordered]@{scope='PROJECT_SQL_RETRIEVAL_NOT_SQLD_MOCK';files=5;native_exits=0;rows=5;cleanup=1;results=$results;hashes=@($hashes)};$manifest|ConvertTo-Json -Depth 5|Set-Content -Encoding utf8 (Join-Path $e 'project-sql5-manifest.json');"W18_PROJECT_SQL5_GREEN files=5 native_exits=0 rows=5 hashes=6 cleanup=1"
}finally{if($owned){& docker compose -f $compose -p $ComposeProject down -v|Out-Null;if($LASTEXITCODE-ne 0){throw 'W18 compose cleanup failed'}}}
STEP 06 / 13
코드 한 줄씩 한국어로 번역
비어 있지 않은 20줄을 모두 한국어로 옮깁니다.
비어 있지 않은 원본 줄은 하나도 생략하지 않습니다.
| 줄 | 원본 | 한국어 번역 |
|---|---|---|
| 1 | param( | PowerShell parameter block을 시작한다. |
| 2 | [ValidateSet('Compose','Container')][string]$Mode='Compose',[string]$Container='', | 실행 mode를 Compose 또는 Container로 제한하고 container 이름 입력을 선언한다. |
| 3 | [string]$ComposeProject='w18-project-sql-lab',[Parameter(Mandatory=$true)][string]$SqlRoot, | Compose project 기본값과 필수 SqlRoot 입력을 선언한다. |
| 4 | [Parameter(Mandatory=$true)][string]$EvidenceDir | 필수 EvidenceDir 입력을 선언한다. |
| 5 | ) | parameter block을 닫는다. |
| 6 | $ErrorActionPreference='Stop';$root=Split-Path -Parent $PSScriptRoot | 오류를 terminating error로 바꾸고 script의 parent를 project root로 잡는다. |
| 7 | if(-not [IO.Path]::IsPathRooted($SqlRoot)){throw 'SqlRoot must be an absolute packaged SQL path'} | SqlRoot가 absolute path가 아니면 즉시 거절한다. |
| 8 | if(-not [IO.Path]::IsPathRooted($EvidenceDir)){throw 'EvidenceDir must be an absolute learner-owned path'} | EvidenceDir가 absolute path가 아니면 즉시 거절한다. |
| 9 | $sql=(Resolve-Path -LiteralPath $SqlRoot).Path;$e=[IO.Path]::GetFullPath($EvidenceDir);New-Item -ItemType Directory -Force $e|Out-Null | SqlRoot를 resolve하고 EvidenceDir를 정규화한 뒤 directory를 만든다. |
| 10 | $compose=Join-Path $root 'compose.yaml';$owned=$false | compose.yaml path를 만들고 아직 소유하지 않은 상태로 시작한다. |
| 11 | try{ | 실행과 cleanup을 묶는 try block을 시작한다. |
| 12 | if($Mode-eq'Compose'){ | Compose mode 분기로 들어간다. |
| 13 | if($Container){throw 'W18 Compose mode rejects -Container'};if([string]::IsNullOrWhiteSpace($env:FCL_DB_PASSWORD)){$env:FCL_DB_PASSWORD='w18-disposable-password'} | Compose mode의 Container 인자를 거절하고 blank password에는 disposable 값을 넣는다. |
| 14 | & docker compose -f $compose -p $ComposeProject up -d --wait db;if($LASTEXITCODE-ne 0){throw "W18 compose up exit=$LASTEXITCODE"};$owned=$true;$Container=(& docker compose -f $compose -p $ComposeProject ps -q db).Trim() | db를 up --wait로 시작하고 성공 뒤 소유권을 잡아 container id를 구한다. |
| 15 | }elseif([string]::IsNullOrWhiteSpace($Container)){throw 'W18 Container mode requires -Container'} | Container mode인데 이름이 비었으면 거절한다. |
| 16 | if([string]::IsNullOrWhiteSpace($Container)){throw 'W18 PostgreSQL container resolution failed'} | 두 mode 분기 뒤에도 container가 비면 resolution 실패로 끝낸다. |
| 17 | $fixture='fixture.sql';Get-Content -Raw (Join-Path $sql $fixture)|docker exec -i $Container psql -v ON_ERROR_STOP=1 -U app -d financial_core;if($LASTEXITCODE-ne 0){throw "W18 fixture exit=$LASTEXITCODE"} | fixture.sql을 raw로 psql에 보내고 native exit가 0인지 확인한다. |
| 18 | $files=@('01-reconcile.sql','02-anti-join.sql','03-aggregation.sql','04-running-balance.sql','05-keyset.sql');$results=@();for($i=0;$i-lt 5;$i++){$out=@(Get-Content -Raw (Join-Path $sql $files[$i])|docker exec -i $Container psql -At -v ON_ERROR_STOP=1 -U app -d financial_core 2>&1);$exit=$LASTEXITCODE;if($exit-ne 0-or($out-join"`n")-cnotmatch "W18_Q$($i+1) "){throw "W18 file=$($files[$i]) exit=$exit output=$out"};$results+=[pscustomobject]@{ordinal=$i+1;file=$files[$i];native_exit=$exit;marker=($out|Where-Object{$_-match'^W18_Q'}|Select-Object -Last 1);sha256=(Get-FileHash -Algorithm SHA256 (Join-Path $sql $files[$i])).Hash.ToLowerInvariant()}} | 다섯 파일을 순서대로 raw 실행하고 exit·W18_Qn substring·마지막 marker·사후 hash를 모은다. |
| 19 | $hashes=@($fixture)+$files|ForEach-Object{[pscustomobject]@{file=$_;sha256=(Get-FileHash -Algorithm SHA256 (Join-Path $sql $_)).Hash.ToLowerInvariant()}};$manifest=[ordered]@{scope='PROJECT_SQL_RETRIEVAL_NOT_SQLD_MOCK';files=5;native_exits=0;rows=5;cleanup=1;results=$results;hashes=@($hashes)};$manifest|ConvertTo-Json -Depth 5|Set-Content -Encoding utf8 (Join-Path $e 'project-sql5-manifest.json');"W18_PROJECT_SQL5_GREEN files=5 native_exits=0 rows=5 hashes=6 cleanup=1" | fixture 포함 여섯 hash와 결과를 manifest에 쓰고 고정 Green 문자열을 출력한다. |
| 20 | }finally{if($owned){& docker compose -f $compose -p $ComposeProject down -v|Out-Null;if($LASTEXITCODE-ne 0){throw 'W18 compose cleanup failed'}}} | 소유한 Compose project만 down -v하고 cleanup 실패를 예외로 올린다. |
STEP 07 / 13
기존 수준의 한 줄 읽기·문법 해부
쉬운 설명 다음에 문법과 실행 순서를 정밀하게 읽습니다.
한 줄로 읽기fixture와 정확히 다섯 SQL을 순차 실행하고 marker·native exit·관찰 hash를 manifest로 기록하는 W18 owner다. 다만 SqlRoot와 EvidenceDir은 absolute만 요구하고 containment를 강제하지 않는다
문법 해부
- ValidateSet·Mandatory parameter는 native 실행 전에 입력 모양을 제한한다.
- $LASTEXITCODE는 native process 결과이고 terminating PowerShell exception과 별도로 읽어야 한다.
- try/finally의 cleanup 범위는 $owned가 언제 true가 되는지에 달렸다.
실행 순서
- parameter binding
- absolute-path checks
- Compose/container resolution
- fixture raw execution
- five sequential SQL executions
- post-execution hashes
- manifest and Green literal
- owned cleanup
원래 W6 수준의 조각별 정밀 해설
F02-C01 · parameters
- 문법 해부
- `parameters` 범위는 PowerShell parameter block을 시작한다. 이어서 parameter block을 닫는다.
- 실제 값 추적
- 범위 시작 입력은 scripts/run-w18-project-sql5.ps1의 1줄 직전까지 형성된 parser·실행 상태를 입력으로 받는다. 끝에서 관찰할 상태는 Mode·Container·ComposeProject·SqlRoot·EvidenceDir binding이 끝난다.
- 정상 예
- 동결 source 1~5줄을 그대로 적용하면 Mode·Container·ComposeProject·SqlRoot·EvidenceDir binding이 끝난다.
- 틀린 예·반례
- SqlRoot와 EvidenceDir은 absolute만 요구하고 containment를 강제하지 않는다 상황을 정상 예로 간주하면 이 chunk의 Green 범위를 넘는다.
- 착각 방지
- 이 범위의 고정값은 modes=Compose/Container이지만 marker·형식만으로 전체 실행을 인증하지 않는다.
- 하지 않는 일
- 현재 chunk가 직접 보장하지 않는 경계: SqlRoot와 EvidenceDir은 absolute만 요구하고 containment를 강제하지 않는다
- 다음 연결
- 끝 상태를 보존한 뒤 `path and ownership setup` 범위에서 다음 입력·결과를 확인한다.
F02-C02 · path and ownership setup
- 문법 해부
- `path and ownership setup` 범위는 오류를 terminating error로 바꾸고 script의 parent를 project root로 잡는다. 이어서 compose.yaml path를 만들고 아직 소유하지 않은 상태로 시작한다.
- 실제 값 추적
- 범위 시작 입력은 scripts/run-w18-project-sql5.ps1의 6줄 직전까지 형성된 parser·실행 상태를 입력으로 받는다. 끝에서 관찰할 상태는 owner state machine이 10줄 이후의 분기·실행 단계로 이동한다.
- 정상 예
- 동결 source 6~10줄을 그대로 적용하면 owner state machine이 10줄 이후의 분기·실행 단계로 이동한다.
- 틀린 예·반례
- marker substring은 결과값·순서·유일성을 인증하지 않는다 상황을 정상 예로 간주하면 이 chunk의 Green 범위를 넘는다.
- 착각 방지
- 이 범위의 고정값은 files=5이지만 marker·형식만으로 전체 실행을 인증하지 않는다.
- 하지 않는 일
- 현재 chunk가 직접 보장하지 않는 경계: SqlRoot와 EvidenceDir은 absolute만 요구하고 containment를 강제하지 않는다
- 다음 연결
- 끝 상태를 보존한 뒤 `Compose or supplied-container resolution` 범위에서 다음 입력·결과를 확인한다.
F02-C03 · Compose or supplied-container resolution
- 문법 해부
- `Compose or supplied-container resolution` 범위는 실행과 cleanup을 묶는 try block을 시작한다. 이어서 두 mode 분기 뒤에도 container가 비면 resolution 실패로 끝낸다.
- 실제 값 추적
- 범위 시작 입력은 scripts/run-w18-project-sql5.ps1의 11줄 직전까지 형성된 parser·실행 상태를 입력으로 받는다. 끝에서 관찰할 상태는 owner state machine이 16줄 이후의 분기·실행 단계로 이동한다.
- 정상 예
- 동결 source 11~16줄을 그대로 적용하면 owner state machine이 16줄 이후의 분기·실행 단계로 이동한다.
- 틀린 예·반례
- substring 통과 문자열이 줄 시작 W18_Q가 아니면 captured marker는 null일 수 있다 상황을 정상 예로 간주하면 이 chunk의 Green 범위를 넘는다.
- 착각 방지
- 이 범위의 고정값은 native_exits=0이지만 marker·형식만으로 전체 실행을 인증하지 않는다.
- 하지 않는 일
- 현재 chunk가 직접 보장하지 않는 경계: SqlRoot와 EvidenceDir은 absolute만 요구하고 containment를 강제하지 않는다
- 다음 연결
- 끝 상태를 보존한 뒤 `fixture execution` 범위에서 다음 입력·결과를 확인한다.
F02-C04 · fixture execution
- 문법 해부
- `fixture execution` 범위는 fixture.sql을 raw로 psql에 보내고 native exit가 0인지 확인한다. 이어서 fixture.sql을 raw로 psql에 보내고 native exit가 0인지 확인한다.
- 실제 값 추적
- 범위 시작 입력은 scripts/run-w18-project-sql5.ps1의 17줄 직전까지 형성된 parser·실행 상태를 입력으로 받는다. 끝에서 관찰할 상태는 세 table row 집합이 아니라 고정 w18_project fixture가 DB에 재구성된다.
- 정상 예
- 동결 source 17~17줄을 그대로 적용하면 세 table row 집합이 아니라 고정 w18_project fixture가 DB에 재구성된다.
- 틀린 예·반례
- manifest는 cleanup 전 non-atomic write이며 Container mode도 cleanup=1을 쓴다 상황을 정상 예로 간주하면 이 chunk의 Green 범위를 넘는다.
- 착각 방지
- 이 범위의 고정값은 rows=5이지만 marker·형식만으로 전체 실행을 인증하지 않는다.
- 하지 않는 일
- 현재 chunk가 직접 보장하지 않는 경계: marker substring은 결과값·순서·유일성을 인증하지 않는다
- 다음 연결
- 끝 상태를 보존한 뒤 `five-query loop and marker capture` 범위에서 다음 입력·결과를 확인한다.
F02-C05 · five-query loop and marker capture
- 문법 해부
- `five-query loop and marker capture` 범위는 다섯 파일을 순서대로 raw 실행하고 exit·W18_Qn substring·마지막 marker·사후 hash를 모은다. 이어서 다섯 파일을 순서대로 raw 실행하고 exit·W18_Qn substring·마지막 marker·사후 hash를 모은다.
- 실제 값 추적
- 범위 시작 입력은 scripts/run-w18-project-sql5.ps1의 18줄 직전까지 형성된 parser·실행 상태를 입력으로 받는다. 끝에서 관찰할 상태는 각 ordinal마다 exit·marker·관찰 hash record가 results에 한 개씩 추가된다.
- 정상 예
- 동결 source 18~18줄을 그대로 적용하면 각 ordinal마다 exit·marker·관찰 hash record가 results에 한 개씩 추가된다.
- 틀린 예·반례
- ComposeProject collision·동시 실행 lock·native timeout 보호가 없다 상황을 정상 예로 간주하면 이 chunk의 Green 범위를 넘는다.
- 착각 방지
- 이 범위의 고정값은 hashes=6이지만 marker·형식만으로 전체 실행을 인증하지 않는다.
- 하지 않는 일
- 현재 chunk가 직접 보장하지 않는 경계: substring 통과 문자열이 줄 시작 W18_Q가 아니면 captured marker는 null일 수 있다
- 다음 연결
- 끝 상태를 보존한 뒤 `hash manifest and Green literal` 범위에서 다음 입력·결과를 확인한다.
F02-C06 · hash manifest and Green literal
- 문법 해부
- `hash manifest and Green literal` 범위는 fixture 포함 여섯 hash와 결과를 manifest에 쓰고 고정 Green 문자열을 출력한다. 이어서 fixture 포함 여섯 hash와 결과를 manifest에 쓰고 고정 Green 문자열을 출력한다.
- 실제 값 추적
- 범위 시작 입력은 scripts/run-w18-project-sql5.ps1의 19줄 직전까지 형성된 parser·실행 상태를 입력으로 받는다. 끝에서 관찰할 상태는 project-sql5-manifest.json과 Green stdout이 cleanup보다 먼저 관찰 가능해진다.
- 정상 예
- 동결 source 19~19줄을 그대로 적용하면 project-sql5-manifest.json과 Green stdout이 cleanup보다 먼저 관찰 가능해진다.
- 틀린 예·반례
- SqlRoot와 EvidenceDir은 absolute만 요구하고 containment를 강제하지 않는다 상황을 정상 예로 간주하면 이 chunk의 Green 범위를 넘는다.
- 착각 방지
- 이 범위의 고정값은 cleanup=1 literal이지만 marker·형식만으로 전체 실행을 인증하지 않는다.
- 하지 않는 일
- 현재 chunk가 직접 보장하지 않는 경계: manifest는 cleanup 전 non-atomic write이며 Container mode도 cleanup=1을 쓴다
- 다음 연결
- 끝 상태를 보존한 뒤 `owned Compose cleanup` 범위에서 다음 입력·결과를 확인한다.
F02-C07 · owned Compose cleanup
- 문법 해부
- `owned Compose cleanup` 범위는 소유한 Compose project만 down -v하고 cleanup 실패를 예외로 올린다. 이어서 소유한 Compose project만 down -v하고 cleanup 실패를 예외로 올린다.
- 실제 값 추적
- 범위 시작 입력은 scripts/run-w18-project-sql5.ps1의 20줄 직전까지 형성된 parser·실행 상태를 입력으로 받는다. 끝에서 관찰할 상태는 owned Compose일 때 volume 제거 요청까지 끝나야 process가 정상 반환한다.
- 정상 예
- 동결 source 20~20줄을 그대로 적용하면 owned Compose일 때 volume 제거 요청까지 끝나야 process가 정상 반환한다.
- 틀린 예·반례
- marker substring은 결과값·순서·유일성을 인증하지 않는다 상황을 정상 예로 간주하면 이 chunk의 Green 범위를 넘는다.
- 착각 방지
- 이 범위의 고정값은 modes=Compose/Container이지만 marker·형식만으로 전체 실행을 인증하지 않는다.
- 하지 않는 일
- 현재 chunk가 직접 보장하지 않는 경계: ComposeProject collision·동시 실행 lock·native timeout 보호가 없다
- 다음 연결
- 끝 상태를 보존한 뒤 `파일 전체 source와 책임 경계` 범위에서 다음 입력·결과를 확인한다.
반례 먼저
-
정상 fixture만 보면 충분하지 않나요?
-
빈 집합·NULL·순서 역전 같은 반례도 넣어야 해.
-
여기서는 marker substring은 결과값·순서·유일성을 인증하지 않는다을 먼저 흔들어 본다.
-
첫 실패 지점을 줄 번호와 함께 기록하겠습니다.
STEP 08 / 13
실제 값 따라가기
같은 입력값이 어느 줄을 지나 어떤 결과가 되는지 추적합니다.
| 순서 | 들어온 값 | 코드가 하는 일 | 나온 값·상태 | 경계 |
|---|---|---|---|---|
| 1 | modes=Compose/Container | F02의 실행 순서 1단계에 넣어 source 조건을 적용한다. | 관찰 가능한 W18 상태에 `modes=Compose/Container`가 기록되거나 그 값으로 비교된다. | SqlRoot와 EvidenceDir은 absolute만 요구하고 containment를 강제하지 않는다 |
| 2 | files=5 | F02의 실행 순서 2단계에 넣어 source 조건을 적용한다. | 관찰 가능한 W18 상태에 `files=5`가 기록되거나 그 값으로 비교된다. | marker substring은 결과값·순서·유일성을 인증하지 않는다 |
| 3 | native_exits=0 | F02의 실행 순서 3단계에 넣어 source 조건을 적용한다. | 관찰 가능한 W18 상태에 `native_exits=0`가 기록되거나 그 값으로 비교된다. | substring 통과 문자열이 줄 시작 W18_Q가 아니면 captured marker는 null일 수 있다 |
| 4 | rows=5 | F02의 실행 순서 4단계에 넣어 source 조건을 적용한다. | 관찰 가능한 W18 상태에 `rows=5`가 기록되거나 그 값으로 비교된다. | manifest는 cleanup 전 non-atomic write이며 Container mode도 cleanup=1을 쓴다 |
전체 다시 쓰기
-
한 줄이 너무 길어서 읽기 힘들어요.
-
화면에서는 줄바꿈해 보되 source bytes와 실행 순서는 그대로 보존해.
-
마지막에는 cleanup=1 literal을 source와 다시 대조한다.
-
뜻→chunk→전체 코드 순서로 복원하겠습니다.
STEP 09 / 13
PowerShell·SQL·DB 내부에서 벌어지는 일
PowerShell·SQL·DB에서 실제로 일어나는 일과 증명 범위를 구분합니다.
fixture와 정확히 다섯 SQL을 순차 실행하고 marker·native exit·관찰 hash를 manifest로 기록하는 W18 owner다.에서 1번째 내부 책임을 수행한다.
SqlRoot와 EvidenceDir은 absolute만 요구하고 containment를 강제하지 않는다fixture와 정확히 다섯 SQL을 순차 실행하고 marker·native exit·관찰 hash를 manifest로 기록하는 W18 owner다.에서 2번째 내부 책임을 수행한다.
marker substring은 결과값·순서·유일성을 인증하지 않는다fixture와 정확히 다섯 SQL을 순차 실행하고 marker·native exit·관찰 hash를 manifest로 기록하는 W18 owner다.에서 3번째 내부 책임을 수행한다.
substring 통과 문자열이 줄 시작 W18_Q가 아니면 captured marker는 null일 수 있다fixture와 정확히 다섯 SQL을 순차 실행하고 marker·native exit·관찰 hash를 manifest로 기록하는 W18 owner다.에서 4번째 내부 책임을 수행한다.
manifest는 cleanup 전 non-atomic write이며 Container mode도 cleanup=1을 쓴다fixture와 정확히 다섯 SQL을 순차 실행하고 marker·native exit·관찰 hash를 manifest로 기록하는 W18 owner다.에서 5번째 내부 책임을 수행한다.
ComposeProject collision·동시 실행 lock·native timeout 보호가 없다fixture와 정확히 다섯 SQL을 순차 실행하고 marker·native exit·관찰 hash를 manifest로 기록하는 W18 owner다.에서 6번째 내부 책임을 수행한다.
SqlRoot와 EvidenceDir은 absolute만 요구하고 containment를 강제하지 않는다STEP 10 / 13
흔한 착각과 틀린 예
그럴듯하지만 틀린 해석을 반례로 고칩니다.
❌ modes=Compose/Container이 보이면 관련 실행 전체가 완전하게 증명된다.
왜 틀리나 SqlRoot와 EvidenceDir은 absolute만 요구하고 containment를 강제하지 않는다
바르게 읽기 고정 fixture에서 직접 검사한 값과 owner·DB·운영 계층의 나머지 책임을 분리한다.
반례 SqlRoot와 EvidenceDir은 absolute만 요구하고 containment를 강제하지 않는다 상황에서는 같은 marker·형식이 보여도 의도한 전체 의미가 성립하지 않을 수 있다.
❌ files=5이 보이면 관련 실행 전체가 완전하게 증명된다.
왜 틀리나 marker substring은 결과값·순서·유일성을 인증하지 않는다
바르게 읽기 고정 fixture에서 직접 검사한 값과 owner·DB·운영 계층의 나머지 책임을 분리한다.
반례 marker substring은 결과값·순서·유일성을 인증하지 않는다 상황에서는 같은 marker·형식이 보여도 의도한 전체 의미가 성립하지 않을 수 있다.
❌ native_exits=0이 보이면 관련 실행 전체가 완전하게 증명된다.
왜 틀리나 substring 통과 문자열이 줄 시작 W18_Q가 아니면 captured marker는 null일 수 있다
바르게 읽기 고정 fixture에서 직접 검사한 값과 owner·DB·운영 계층의 나머지 책임을 분리한다.
반례 substring 통과 문자열이 줄 시작 W18_Q가 아니면 captured marker는 null일 수 있다 상황에서는 같은 marker·형식이 보여도 의도한 전체 의미가 성립하지 않을 수 있다.
❌ rows=5이 보이면 관련 실행 전체가 완전하게 증명된다.
왜 틀리나 manifest는 cleanup 전 non-atomic write이며 Container mode도 cleanup=1을 쓴다
바르게 읽기 고정 fixture에서 직접 검사한 값과 owner·DB·운영 계층의 나머지 책임을 분리한다.
반례 manifest는 cleanup 전 non-atomic write이며 Container mode도 cleanup=1을 쓴다 상황에서는 같은 marker·형식이 보여도 의도한 전체 의미가 성립하지 않을 수 있다.
❌ hashes=6이 보이면 관련 실행 전체가 완전하게 증명된다.
왜 틀리나 ComposeProject collision·동시 실행 lock·native timeout 보호가 없다
바르게 읽기 고정 fixture에서 직접 검사한 값과 owner·DB·운영 계층의 나머지 책임을 분리한다.
반례 ComposeProject collision·동시 실행 lock·native timeout 보호가 없다 상황에서는 같은 marker·형식이 보여도 의도한 전체 의미가 성립하지 않을 수 있다.
STEP 11 / 13
이 코드가 보장하지 않는 것
이 코드가 책임지지 않는 일을 분리합니다.
SqlRoot와 EvidenceDir은 absolute만 요구하고 containment를 강제하지 않는다
이 책임을 맡는 곳: caller path policymarker substring은 결과값·순서·유일성을 인증하지 않는다
이 책임을 맡는 곳: exact oracle gatesubstring 통과 문자열이 줄 시작 W18_Q가 아니면 captured marker는 null일 수 있다
이 책임을 맡는 곳: SQL/parser counterexample testmanifest는 cleanup 전 non-atomic write이며 Container mode도 cleanup=1을 쓴다
이 책임을 맡는 곳: runtime ownerComposeProject collision·동시 실행 lock·native timeout 보호가 없다
이 책임을 맡는 곳: byte-pinned source auditSTEP 12 / 13
직접 다시 써보기
뜻 → 조각 → 전체 코드 순서로 다시 씁니다.
1단계 · 뜻부터 복원
fixture와 정확히 다섯 SQL을 순차 실행하고 marker·native exit·관찰 hash를 manifest로 기록하는 W18 owner다.를 고정값과 미보장 경계까지 한 문장으로 말한다.
2단계 · 코드 조각 재조립
- parameters
- path and ownership setup
- Compose or supplied-container resolution
- fixture execution
- five-query loop and marker capture
- hash manifest and Green literal
- owned Compose cleanup
3단계 · 파일 전체 다시 쓰기
20개 물리 줄을 원본 순서로 다시 쓰고 SHA-256 381bdfe24e2ebe0c390c3040416cafbbca873101718f358b5c773aef4d62525d와 대조한다.
자가 점검
- 정본과 학습용 예시 label을 바꾸지 않는다.
- 긴 한 줄은 화면에서 감싸도 source exact text를 바꾸지 않는다.
- marker literal과 실제 결과 row를 구분한다.
- NULL·순서·empty fixture 반례를 하나 이상 말한다.
- owner mode와 cleanup 책임을 분리한다.
STEP 13 / 13
전체 원본 정답
감사로 고정한 전체 source를 가감 없이 확인합니다.
정본 전체 코드 확인하기
param(
[ValidateSet('Compose','Container')][string]$Mode='Compose',[string]$Container='',
[string]$ComposeProject='w18-project-sql-lab',[Parameter(Mandatory=$true)][string]$SqlRoot,
[Parameter(Mandatory=$true)][string]$EvidenceDir
)
$ErrorActionPreference='Stop';$root=Split-Path -Parent $PSScriptRoot
if(-not [IO.Path]::IsPathRooted($SqlRoot)){throw 'SqlRoot must be an absolute packaged SQL path'}
if(-not [IO.Path]::IsPathRooted($EvidenceDir)){throw 'EvidenceDir must be an absolute learner-owned path'}
$sql=(Resolve-Path -LiteralPath $SqlRoot).Path;$e=[IO.Path]::GetFullPath($EvidenceDir);New-Item -ItemType Directory -Force $e|Out-Null
$compose=Join-Path $root 'compose.yaml';$owned=$false
try{
if($Mode-eq'Compose'){
if($Container){throw 'W18 Compose mode rejects -Container'};if([string]::IsNullOrWhiteSpace($env:FCL_DB_PASSWORD)){$env:FCL_DB_PASSWORD='w18-disposable-password'}
& docker compose -f $compose -p $ComposeProject up -d --wait db;if($LASTEXITCODE-ne 0){throw "W18 compose up exit=$LASTEXITCODE"};$owned=$true;$Container=(& docker compose -f $compose -p $ComposeProject ps -q db).Trim()
}elseif([string]::IsNullOrWhiteSpace($Container)){throw 'W18 Container mode requires -Container'}
if([string]::IsNullOrWhiteSpace($Container)){throw 'W18 PostgreSQL container resolution failed'}
$fixture='fixture.sql';Get-Content -Raw (Join-Path $sql $fixture)|docker exec -i $Container psql -v ON_ERROR_STOP=1 -U app -d financial_core;if($LASTEXITCODE-ne 0){throw "W18 fixture exit=$LASTEXITCODE"}
$files=@('01-reconcile.sql','02-anti-join.sql','03-aggregation.sql','04-running-balance.sql','05-keyset.sql');$results=@();for($i=0;$i-lt 5;$i++){$out=@(Get-Content -Raw (Join-Path $sql $files[$i])|docker exec -i $Container psql -At -v ON_ERROR_STOP=1 -U app -d financial_core 2>&1);$exit=$LASTEXITCODE;if($exit-ne 0-or($out-join"`n")-cnotmatch "W18_Q$($i+1) "){throw "W18 file=$($files[$i]) exit=$exit output=$out"};$results+=[pscustomobject]@{ordinal=$i+1;file=$files[$i];native_exit=$exit;marker=($out|Where-Object{$_-match'^W18_Q'}|Select-Object -Last 1);sha256=(Get-FileHash -Algorithm SHA256 (Join-Path $sql $files[$i])).Hash.ToLowerInvariant()}}
$hashes=@($fixture)+$files|ForEach-Object{[pscustomobject]@{file=$_;sha256=(Get-FileHash -Algorithm SHA256 (Join-Path $sql $_)).Hash.ToLowerInvariant()}};$manifest=[ordered]@{scope='PROJECT_SQL_RETRIEVAL_NOT_SQLD_MOCK';files=5;native_exits=0;rows=5;cleanup=1;results=$results;hashes=@($hashes)};$manifest|ConvertTo-Json -Depth 5|Set-Content -Encoding utf8 (Join-Path $e 'project-sql5-manifest.json');"W18_PROJECT_SQL5_GREEN files=5 native_exits=0 rows=5 hashes=6 cleanup=1"
}finally{if($owned){& docker compose -f $compose -p $ComposeProject down -v|Out-Null;if($LASTEXITCODE-ne 0){throw 'W18 compose cleanup failed'}}}
03fixture.sql — account·ledger 결정형 출발 상태
sql/w18/fixture.sql
정본 SQL fixture · 정본 · W18-F036줄 연결6줄 번역3 chunks
fixture.sql — account·ledger 결정형 출발 상태
sql/w18/fixture.sql
정본 SQL fixture · 정본 · W18-F03STEP 01 / 13
오늘 이 코드에서 해결할 문제
무엇을 이해해야 하는지 질문부터 잡습니다.
세 account와 세 ledger row를 w18_project schema에 고정해 Q1~Q5가 공유할 deterministic fixture를 만든다.
- account=(1,70),(2,0),(3,5)은 어느 줄에서 만들어지거나 검사될까?
- ledger account1=100,-30은 실제 계산값인가 고정 marker인가?
- 시작할 때 w18_project를 CASCADE drop한다 경계에서 첫 실패는 어디일까?
- 빈 집합·NULL·동률·순서 역전 중 어떤 반례가 중요한가?
- 이 source가 책임지지 않는 lifecycle·provenance는 무엇인가?
account=(1,70),(2,0),(3,5)ledger account1=100,-30ledger account3=5account2 no ledgerSTEP 02 / 13
아주 짧게: 이 코드는 왜 필요할까?
웹소설 대신 이 코드가 필요한 이유만 두 문단으로 쉽게 봅니다.
STARRY가 source를 값·순서·증명 경계가 적힌 작은 실험 카드로 나눈다.
fixture.sql — account·ledger 결정형 출발 상태를 실행 전 검사표로 바꾸기
세 account와 세 ledger row를 w18_project schema에 고정해 Q1~Q5가 공유할 deterministic fixture를 만든다.
핵심 관찰값 account=(1,70),(2,0),(3,5), ledger account1=100,-30, ledger account3=5, account2 no ledger을 따라가되, 시작할 때 w18_project를 CASCADE drop한다까지 함께 표시해 Green 문구를 과대해석하지 않는다.
딱 여기까지만 비유는 순서를 기억하게 할 뿐 SQL NULL, native exit, hash, cleanup의 실제 proof를 대신하지 않는다.
STEP 03 / 13
초등학생도 이해하는 설명
생활 비유와 실제 코드의 경계를 함께 확인합니다.
fail-fast and schema reset
1~2줄을 한 덩어리로 읽어 세 account와 세 ledger row를 w18_project schema에 고정해 Q1~Q5가 공유할 deterministic fixture를 만든다.의 1번째 단계를 확인한다.
- 코드 연결
1~2줄- 비유
- 긴 조립 설명서에서 같은 일을 하는 부품만 한 봉투에 담아 확인한다.
- 비유의 끝
- 시작할 때 w18_project를 CASCADE drop한다
account and ledger tables
3~4줄을 한 덩어리로 읽어 세 account와 세 ledger row를 w18_project schema에 고정해 Q1~Q5가 공유할 deterministic fixture를 만든다.의 2번째 단계를 확인한다.
- 코드 연결
3~4줄- 비유
- 긴 조립 설명서에서 같은 일을 하는 부품만 한 봉투에 담아 확인한다.
- 비유의 끝
- INSERT에 column list가 없어 table column order에 결합된다
deterministic seed
5~6줄을 한 덩어리로 읽어 세 account와 세 ledger row를 w18_project schema에 고정해 Q1~Q5가 공유할 deterministic fixture를 만든다.의 3번째 단계를 확인한다.
- 코드 연결
5~6줄- 비유
- 긴 조립 설명서에서 같은 일을 하는 부품만 한 봉투에 담아 확인한다.
- 비유의 끝
- Container mode에서는 supplied container의 같은 schema를 파괴적으로 교체한다
왜 먼저 보는가
-
이 파일은 왜 필요한가요?
-
account·ledger 결정형 출발 상태의 출발 계약을 먼저 고정해야 해.
-
직접 보장하는 값은 account=(1,70),(2,0),(3,5) 범위다.
-
목적과 결과를 같은 문장으로 섞지 않겠습니다.
고정값 읽기
-
숫자나 marker는 그냥 외우면 되나요?
-
아니, fixture·순서와 함께 ledger account1=100,-30을 읽어야 해.
-
고정값은 현재 source의 관찰 계약이지 모든 입력의 일반 법칙은 아니다.
-
입력→처리→결과 표로 다시 적어 볼게요.
STEP 04 / 13
비유 ↔ 코드 전체 연결표
감사 규칙상 연결 대상인 원본 6줄을 빠짐없이 연결합니다.
| 줄 | 정확한 원본 줄 | STARRY 비유 | 실제 뜻·입력·결과·한계 |
|---|---|---|---|
| 1줄F03-L01 | \set ON_ERROR_STOP on |
한 단계의 안내표를 읽어 다음 상태표로 넘긴다. | psql이 첫 오류에서 실행을 멈추도록 ON_ERROR_STOP을 켠다.
|
| 2줄F03-L02 | DROP SCHEMA IF EXISTS w18_project CASCADE; |
낡은 연습판을 치우고 표와 고정 숫자 자석을 다시 놓는다. | 기존 w18_project schema를 CASCADE drop하고 새 schema를 만든다.
|
| 3줄F03-L03 | CREATE TABLE w18_project. |
낡은 연습판을 치우고 표와 고정 숫자 자석을 다시 놓는다. | id와 balance를 가진 account table을 만든다.
|
| 4줄F03-L04 | CREATE TABLE w18_project. |
낡은 연습판을 치우고 표와 고정 숫자 자석을 다시 놓는다. | account FK·signed amount·stable-order 시간을 가진 ledger table을 만든다.
|
| 5줄F03-L05 | INSERT INTO w18_project. |
낡은 연습판을 치우고 표와 고정 숫자 자석을 다시 놓는다. | account 1=70, 2=0, 3=5 세 행을 넣는다.
|
| 6줄F03-L06 | INSERT INTO w18_project. |
낡은 연습판을 치우고 표와 고정 숫자 자석을 다시 놓는다. | account 1의 +100/-30과 account 3의 +5 ledger 세 행을 넣는다.
|
Green의 작은 범위
-
Green 문구가 보이면 전부 안전한가요?
-
그 문구가 직접 검사한 조건까지만 안전해.
-
특히 시작할 때 w18_project를 CASCADE drop한다 경계는 별도 proof가 필요하다.
-
증명한 것과 안 한 것을 두 칸으로 나누겠습니다.
STEP 05 / 13
원본 코드 조각
원본을 3개 의미 조각으로 나누어 그대로 확인합니다.
파일을 한꺼번에 외우지 않고 실행 의미가 이어지는 작은 조각으로 봅니다. 아래 코드는 hash로 고정한 native 정본 source에서 그대로 잘랐습니다.
\set ON_ERROR_STOP on
DROP SCHEMA IF EXISTS w18_project CASCADE; CREATE SCHEMA w18_project;
CREATE TABLE w18_project.account(id bigint PRIMARY KEY,balance bigint NOT NULL);
CREATE TABLE w18_project.ledger(id bigint PRIMARY KEY,account_id bigint NOT NULL REFERENCES w18_project.account(id),signed_amount bigint NOT NULL,created_at timestamptz NOT NULL);
INSERT INTO w18_project.account VALUES(1,70),(2,0),(3,5);
INSERT INTO w18_project.ledger VALUES(1,1,100,'2026-01-01Z'),(2,1,-30,'2026-01-02Z'),(3,3,5,'2026-01-01Z');
STEP 06 / 13
코드 한 줄씩 한국어로 번역
비어 있지 않은 6줄을 모두 한국어로 옮깁니다.
비어 있지 않은 원본 줄은 하나도 생략하지 않습니다.
| 줄 | 원본 | 한국어 번역 |
|---|---|---|
| 1 | \set ON_ERROR_STOP on | psql이 첫 오류에서 실행을 멈추도록 ON_ERROR_STOP을 켠다. |
| 2 | DROP SCHEMA IF EXISTS w18_project CASCADE; CREATE SCHEMA w18_project; | 기존 w18_project schema를 CASCADE drop하고 새 schema를 만든다. |
| 3 | CREATE TABLE w18_project.account(id bigint PRIMARY KEY,balance bigint NOT NULL); | id와 balance를 가진 account table을 만든다. |
| 4 | CREATE TABLE w18_project.ledger(id bigint PRIMARY KEY,account_id bigint NOT NULL REFERENCES w18_project.account(id),signed_amount bigint NOT NULL,created_at timestamptz NOT NULL); | account FK·signed amount·stable-order 시간을 가진 ledger table을 만든다. |
| 5 | INSERT INTO w18_project.account VALUES(1,70),(2,0),(3,5); | account 1=70, 2=0, 3=5 세 행을 넣는다. |
| 6 | INSERT INTO w18_project.ledger VALUES(1,1,100,'2026-01-01Z'),(2,1,-30,'2026-01-02Z'),(3,3,5,'2026-01-01Z'); | account 1의 +100/-30과 account 3의 +5 ledger 세 행을 넣는다. |
STEP 07 / 13
기존 수준의 한 줄 읽기·문법 해부
쉬운 설명 다음에 문법과 실행 순서를 정밀하게 읽습니다.
한 줄로 읽기세 account와 세 ledger row를 w18_project schema에 고정해 Q1~Q5가 공유할 deterministic fixture를 만든다. 다만 시작할 때 w18_project를 CASCADE drop한다
문법 해부
- psql meta-command와 PostgreSQL DDL/DML이 같은 input stream에서 순서대로 실행된다.
- FK는 ledger.account_id가 존재하는 account를 참조하게 한다.
- column list 없는 INSERT는 현재 table column order에 결합된다.
실행 순서
- psql fail-fast
- schema reset
- table DDL
- account seed
- ledger seed
원래 W6 수준의 조각별 정밀 해설
F03-C01 · fail-fast and schema reset
- 문법 해부
- `fail-fast and schema reset` 범위는 psql이 첫 오류에서 실행을 멈추도록 ON_ERROR_STOP을 켠다. 이어서 기존 w18_project schema를 CASCADE drop하고 새 schema를 만든다.
- 실제 값 추적
- 범위 시작 입력은 sql/w18/fixture.sql의 1줄 직전까지 형성된 parser·실행 상태를 입력으로 받는다. 끝에서 관찰할 상태는 이전 실험 data가 사라지고 빈 w18_project namespace가 생긴다.
- 정상 예
- 동결 source 1~2줄을 그대로 적용하면 이전 실험 data가 사라지고 빈 w18_project namespace가 생긴다.
- 틀린 예·반례
- 시작할 때 w18_project를 CASCADE drop한다 상황을 정상 예로 간주하면 이 chunk의 Green 범위를 넘는다.
- 착각 방지
- 이 범위의 고정값은 account=(1,70),(2,0),(3,5)이지만 marker·형식만으로 전체 실행을 인증하지 않는다.
- 하지 않는 일
- 현재 chunk가 직접 보장하지 않는 경계: 시작할 때 w18_project를 CASCADE drop한다
- 다음 연결
- 끝 상태를 보존한 뒤 `account and ledger tables` 범위에서 다음 입력·결과를 확인한다.
F03-C02 · account and ledger tables
- 문법 해부
- `account and ledger tables` 범위는 id와 balance를 가진 account table을 만든다. 이어서 account FK·signed amount·stable-order 시간을 가진 ledger table을 만든다.
- 실제 값 추적
- 범위 시작 입력은 sql/w18/fixture.sql의 3줄 직전까지 형성된 parser·실행 상태를 입력으로 받는다. 끝에서 관찰할 상태는 account FK를 가진 ledger relation이 생긴다.
- 정상 예
- 동결 source 3~4줄을 그대로 적용하면 account FK를 가진 ledger relation이 생긴다.
- 틀린 예·반례
- INSERT에 column list가 없어 table column order에 결합된다 상황을 정상 예로 간주하면 이 chunk의 Green 범위를 넘는다.
- 착각 방지
- 이 범위의 고정값은 ledger account1=100,-30이지만 marker·형식만으로 전체 실행을 인증하지 않는다.
- 하지 않는 일
- 현재 chunk가 직접 보장하지 않는 경계: Container mode에서는 supplied container의 같은 schema를 파괴적으로 교체한다
- 다음 연결
- 끝 상태를 보존한 뒤 `deterministic seed` 범위에서 다음 입력·결과를 확인한다.
F03-C03 · deterministic seed
- 문법 해부
- `deterministic seed` 범위는 account 1=70, 2=0, 3=5 세 행을 넣는다. 이어서 account 1의 +100/-30과 account 3의 +5 ledger 세 행을 넣는다.
- 실제 값 추적
- 범위 시작 입력은 sql/w18/fixture.sql의 5줄 직전까지 형성된 parser·실행 상태를 입력으로 받는다. 끝에서 관찰할 상태는 account1 합계 70과 account3 합계 5가 재현된다.
- 정상 예
- 동결 source 5~6줄을 그대로 적용하면 account1 합계 70과 account3 합계 5가 재현된다.
- 틀린 예·반례
- Container mode에서는 supplied container의 같은 schema를 파괴적으로 교체한다 상황을 정상 예로 간주하면 이 chunk의 Green 범위를 넘는다.
- 착각 방지
- 이 범위의 고정값은 ledger account3=5이지만 marker·형식만으로 전체 실행을 인증하지 않는다.
- 하지 않는 일
- 현재 chunk가 직접 보장하지 않는 경계: INSERT에 column list가 없어 table column order에 결합된다
- 다음 연결
- 끝 상태를 보존한 뒤 `파일 전체 source와 책임 경계` 범위에서 다음 입력·결과를 확인한다.
반례 먼저
-
정상 fixture만 보면 충분하지 않나요?
-
빈 집합·NULL·순서 역전 같은 반례도 넣어야 해.
-
여기서는 INSERT에 column list가 없어 table column order에 결합된다을 먼저 흔들어 본다.
-
첫 실패 지점을 줄 번호와 함께 기록하겠습니다.
STEP 08 / 13
실제 값 따라가기
같은 입력값이 어느 줄을 지나 어떤 결과가 되는지 추적합니다.
| 순서 | 들어온 값 | 코드가 하는 일 | 나온 값·상태 | 경계 |
|---|---|---|---|---|
| 1 | account=(1,70),(2,0),(3,5) | F03의 실행 순서 1단계에 넣어 source 조건을 적용한다. | 관찰 가능한 W18 상태에 `account=(1,70),(2,0),(3,5)`가 기록되거나 그 값으로 비교된다. | 시작할 때 w18_project를 CASCADE drop한다 |
| 2 | ledger account1=100,-30 | F03의 실행 순서 2단계에 넣어 source 조건을 적용한다. | 관찰 가능한 W18 상태에 `ledger account1=100,-30`가 기록되거나 그 값으로 비교된다. | INSERT에 column list가 없어 table column order에 결합된다 |
| 3 | ledger account3=5 | F03의 실행 순서 3단계에 넣어 source 조건을 적용한다. | 관찰 가능한 W18 상태에 `ledger account3=5`가 기록되거나 그 값으로 비교된다. | Container mode에서는 supplied container의 같은 schema를 파괴적으로 교체한다 |
| 4 | account2 no ledger | F03의 실행 순서 4단계에 넣어 source 조건을 적용한다. | 관찰 가능한 W18 상태에 `account2 no ledger`가 기록되거나 그 값으로 비교된다. | 시작할 때 w18_project를 CASCADE drop한다 |
전체 다시 쓰기
-
한 줄이 너무 길어서 읽기 힘들어요.
-
화면에서는 줄바꿈해 보되 source bytes와 실행 순서는 그대로 보존해.
-
마지막에는 account2 no ledger을 source와 다시 대조한다.
-
뜻→chunk→전체 코드 순서로 복원하겠습니다.
STEP 09 / 13
PowerShell·SQL·DB 내부에서 벌어지는 일
PowerShell·SQL·DB에서 실제로 일어나는 일과 증명 범위를 구분합니다.
세 account와 세 ledger row를 w18_project schema에 고정해 Q1~Q5가 공유할 deterministic fixture를 만든다.에서 1번째 내부 책임을 수행한다.
시작할 때 w18_project를 CASCADE drop한다세 account와 세 ledger row를 w18_project schema에 고정해 Q1~Q5가 공유할 deterministic fixture를 만든다.에서 2번째 내부 책임을 수행한다.
INSERT에 column list가 없어 table column order에 결합된다세 account와 세 ledger row를 w18_project schema에 고정해 Q1~Q5가 공유할 deterministic fixture를 만든다.에서 3번째 내부 책임을 수행한다.
Container mode에서는 supplied container의 같은 schema를 파괴적으로 교체한다세 account와 세 ledger row를 w18_project schema에 고정해 Q1~Q5가 공유할 deterministic fixture를 만든다.에서 4번째 내부 책임을 수행한다.
시작할 때 w18_project를 CASCADE drop한다STEP 10 / 13
흔한 착각과 틀린 예
그럴듯하지만 틀린 해석을 반례로 고칩니다.
❌ account=(1,70),(2,0),(3,5)이 보이면 관련 실행 전체가 완전하게 증명된다.
왜 틀리나 시작할 때 w18_project를 CASCADE drop한다
바르게 읽기 고정 fixture에서 직접 검사한 값과 owner·DB·운영 계층의 나머지 책임을 분리한다.
반례 시작할 때 w18_project를 CASCADE drop한다 상황에서는 같은 marker·형식이 보여도 의도한 전체 의미가 성립하지 않을 수 있다.
❌ ledger account1=100,-30이 보이면 관련 실행 전체가 완전하게 증명된다.
왜 틀리나 INSERT에 column list가 없어 table column order에 결합된다
바르게 읽기 고정 fixture에서 직접 검사한 값과 owner·DB·운영 계층의 나머지 책임을 분리한다.
반례 INSERT에 column list가 없어 table column order에 결합된다 상황에서는 같은 marker·형식이 보여도 의도한 전체 의미가 성립하지 않을 수 있다.
❌ ledger account3=5이 보이면 관련 실행 전체가 완전하게 증명된다.
왜 틀리나 Container mode에서는 supplied container의 같은 schema를 파괴적으로 교체한다
바르게 읽기 고정 fixture에서 직접 검사한 값과 owner·DB·운영 계층의 나머지 책임을 분리한다.
반례 Container mode에서는 supplied container의 같은 schema를 파괴적으로 교체한다 상황에서는 같은 marker·형식이 보여도 의도한 전체 의미가 성립하지 않을 수 있다.
❌ account2 no ledger이 보이면 관련 실행 전체가 완전하게 증명된다.
왜 틀리나 시작할 때 w18_project를 CASCADE drop한다
바르게 읽기 고정 fixture에서 직접 검사한 값과 owner·DB·운영 계층의 나머지 책임을 분리한다.
반례 시작할 때 w18_project를 CASCADE drop한다 상황에서는 같은 marker·형식이 보여도 의도한 전체 의미가 성립하지 않을 수 있다.
❌ account=(1,70),(2,0),(3,5)이 보이면 관련 실행 전체가 완전하게 증명된다.
왜 틀리나 INSERT에 column list가 없어 table column order에 결합된다
바르게 읽기 고정 fixture에서 직접 검사한 값과 owner·DB·운영 계층의 나머지 책임을 분리한다.
반례 INSERT에 column list가 없어 table column order에 결합된다 상황에서는 같은 marker·형식이 보여도 의도한 전체 의미가 성립하지 않을 수 있다.
STEP 11 / 13
이 코드가 보장하지 않는 것
이 코드가 책임지지 않는 일을 분리합니다.
시작할 때 w18_project를 CASCADE drop한다
이 책임을 맡는 곳: caller path policyINSERT에 column list가 없어 table column order에 결합된다
이 책임을 맡는 곳: exact oracle gateContainer mode에서는 supplied container의 같은 schema를 파괴적으로 교체한다
이 책임을 맡는 곳: SQL/parser counterexample test시작할 때 w18_project를 CASCADE drop한다
이 책임을 맡는 곳: runtime ownerINSERT에 column list가 없어 table column order에 결합된다
이 책임을 맡는 곳: byte-pinned source auditSTEP 12 / 13
직접 다시 써보기
뜻 → 조각 → 전체 코드 순서로 다시 씁니다.
1단계 · 뜻부터 복원
세 account와 세 ledger row를 w18_project schema에 고정해 Q1~Q5가 공유할 deterministic fixture를 만든다.를 고정값과 미보장 경계까지 한 문장으로 말한다.
2단계 · 코드 조각 재조립
- fail-fast and schema reset
- account and ledger tables
- deterministic seed
3단계 · 파일 전체 다시 쓰기
6개 물리 줄을 원본 순서로 다시 쓰고 SHA-256 88a2509fe83b5a3044d2186a5c49211e328ae6023ae00ca9fac480ee40ce3b03와 대조한다.
자가 점검
- 정본과 학습용 예시 label을 바꾸지 않는다.
- 긴 한 줄은 화면에서 감싸도 source exact text를 바꾸지 않는다.
- marker literal과 실제 결과 row를 구분한다.
- NULL·순서·empty fixture 반례를 하나 이상 말한다.
- owner mode와 cleanup 책임을 분리한다.
STEP 13 / 13
전체 원본 정답
감사로 고정한 전체 source를 가감 없이 확인합니다.
정본 전체 코드 확인하기
\set ON_ERROR_STOP on
DROP SCHEMA IF EXISTS w18_project CASCADE; CREATE SCHEMA w18_project;
CREATE TABLE w18_project.account(id bigint PRIMARY KEY,balance bigint NOT NULL);
CREATE TABLE w18_project.ledger(id bigint PRIMARY KEY,account_id bigint NOT NULL REFERENCES w18_project.account(id),signed_amount bigint NOT NULL,created_at timestamptz NOT NULL);
INSERT INTO w18_project.account VALUES(1,70),(2,0),(3,5);
INSERT INTO w18_project.ledger VALUES(1,1,100,'2026-01-01Z'),(2,1,-30,'2026-01-02Z'),(3,3,5,'2026-01-01Z');
0401-reconcile.sql — 저장 잔액과 원장 합계 대조
sql/w18/01-reconcile.sql
정본 SQL invariant · 정본 · W18-F041줄 연결1줄 번역1 chunks
01-reconcile.sql — 저장 잔액과 원장 합계 대조
sql/w18/01-reconcile.sql
정본 SQL invariant · 정본 · W18-F04STEP 01 / 13
오늘 이 코드에서 해결할 문제
무엇을 이해해야 하는지 질문부터 잡습니다.
account 저장 balance와 account별 ledger 합계를 대조해 mismatch 0건을 요구한다.
- mismatch=0은 어느 줄에서 만들어지거나 검사될까?
- account2 missing ledger -> 0은 실제 계산값인가 고정 marker인가?
- hardcoded marker는 실제 SELECT 결과 row가 아니다 경계에서 첫 실패는 어디일까?
- 빈 집합·NULL·동률·순서 역전 중 어떤 반례가 중요한가?
- 이 source가 책임지지 않는 lifecycle·provenance는 무엇인가?
mismatch=0account2 missing ledger -> 0marker=W18_Q1 rows=0STEP 02 / 13
아주 짧게: 이 코드는 왜 필요할까?
웹소설 대신 이 코드가 필요한 이유만 두 문단으로 쉽게 봅니다.
STARRY가 source를 값·순서·증명 경계가 적힌 작은 실험 카드로 나눈다.
01-reconcile.sql — 저장 잔액과 원장 합계 대조를 실행 전 검사표로 바꾸기
account 저장 balance와 account별 ledger 합계를 대조해 mismatch 0건을 요구한다.
핵심 관찰값 mismatch=0, account2 missing ledger -> 0, marker=W18_Q1 rows=0을 따라가되, hardcoded marker는 실제 SELECT 결과 row가 아니다까지 함께 표시해 Green 문구를 과대해석하지 않는다.
딱 여기까지만 비유는 순서를 기억하게 할 뿐 SQL NULL, native exit, hash, cleanup의 실제 proof를 대신하지 않는다.
STEP 03 / 13
초등학생도 이해하는 설명
생활 비유와 실제 코드의 경계를 함께 확인합니다.
reconcile invariant and marker
1~1줄을 한 덩어리로 읽어 account 저장 balance와 account별 ledger 합계를 대조해 mismatch 0건을 요구한다.의 1번째 단계를 확인한다.
- 코드 연결
1~1줄- 비유
- 긴 조립 설명서에서 같은 일을 하는 부품만 한 봉투에 담아 확인한다.
- 비유의 끝
- hardcoded marker는 실제 SELECT 결과 row가 아니다
전체 한 줄
account 저장 balance와 account별 ledger 합계를 대조해 mismatch 0건을 요구한다.
- 코드 연결
1줄- 비유
- 한 장짜리 검사표를 처음부터 끝까지 읽는다.
- 비유의 끝
- hardcoded marker는 실제 SELECT 결과 row가 아니다
왜 먼저 보는가
-
이 파일은 왜 필요한가요?
-
저장 잔액과 원장 합계 대조의 출발 계약을 먼저 고정해야 해.
-
직접 보장하는 값은 mismatch=0 범위다.
-
목적과 결과를 같은 문장으로 섞지 않겠습니다.
고정값 읽기
-
숫자나 marker는 그냥 외우면 되나요?
-
아니, fixture·순서와 함께 account2 missing ledger -> 0을 읽어야 해.
-
고정값은 현재 source의 관찰 계약이지 모든 입력의 일반 법칙은 아니다.
-
입력→처리→결과 표로 다시 적어 볼게요.
STEP 04 / 13
비유 ↔ 코드 전체 연결표
감사 규칙상 연결 대상인 원본 1줄을 빠짐없이 연결합니다.
| 줄 | 정확한 원본 줄 | STARRY 비유 | 실제 뜻·입력·결과·한계 |
|---|---|---|---|
| 1줄F04-L01 | DO $g$ BEGIN IF ( |
명단의 사람을 지우지 않고 옆 장부 칸만 연결한다. | account별 ledger 합계를 LEFT JOIN하고 missing 합계를 0으로 바꿔 저장 balance와 다른 행이 0건인지 검사한 뒤 Q1 marker를 출력한다.
|
Green의 작은 범위
-
Green 문구가 보이면 전부 안전한가요?
-
그 문구가 직접 검사한 조건까지만 안전해.
-
특히 hardcoded marker는 실제 SELECT 결과 row가 아니다 경계는 별도 proof가 필요하다.
-
증명한 것과 안 한 것을 두 칸으로 나누겠습니다.
STEP 05 / 13
원본 코드 조각
원본을 1개 의미 조각으로 나누어 그대로 확인합니다.
파일을 한꺼번에 외우지 않고 실행 의미가 이어지는 작은 조각으로 봅니다. 아래 코드는 hash로 고정한 native 정본 source에서 그대로 잘랐습니다.
DO $g$ BEGIN IF (SELECT count(*) FROM w18_project.account a LEFT JOIN(SELECT account_id,sum(signed_amount)b FROM w18_project.ledger GROUP BY account_id)l ON l.account_id=a.id WHERE a.balance<>coalesce(l.b,0))<>0 THEN RAISE EXCEPTION 'reconcile';END IF;END $g$; SELECT 'W18_Q1 rows=0';
STEP 06 / 13
코드 한 줄씩 한국어로 번역
비어 있지 않은 1줄을 모두 한국어로 옮깁니다.
비어 있지 않은 원본 줄은 하나도 생략하지 않습니다.
| 줄 | 원본 | 한국어 번역 |
|---|---|---|
| 1 | DO $g$ BEGIN IF (SELECT count(*) FROM w18_project.account a LEFT JOIN(SELECT account_id,sum(signed_amount)b FROM w18_project.ledger GROUP BY account_id)l ON l.account_id=a.id WHERE a.balance<>coalesce(l.b,0))<>0 THEN RAISE EXCEPTION 'reconcile';END IF;END $g$; SELECT 'W18_Q1 rows=0'; | account별 ledger 합계를 LEFT JOIN하고 missing 합계를 0으로 바꿔 저장 balance와 다른 행이 0건인지 검사한 뒤 Q1 marker를 출력한다. |
STEP 07 / 13
기존 수준의 한 줄 읽기·문법 해부
쉬운 설명 다음에 문법과 실행 순서를 정밀하게 읽습니다.
한 줄로 읽기account 저장 balance와 account별 ledger 합계를 대조해 mismatch 0건을 요구한다. 다만 hardcoded marker는 실제 SELECT 결과 row가 아니다
문법 해부
- DO block의 PL/pgSQL IF와 뒤의 SELECT marker는 서로 다른 statement다.
- SQL NULL 비교는 false가 아니라 unknown이 될 수 있어 IF fail-open 여부를 확인해야 한다.
- hardcoded marker 문자열과 실제 query 결과 row를 구분한다.
실행 순서
- DO block query
- fixture-bound invariant comparison
- optional exception
- hardcoded marker SELECT
- runner substring check
원래 W6 수준의 조각별 정밀 해설
F04-C01 · reconcile invariant and marker
- 문법 해부
- `reconcile invariant and marker` 범위는 account별 ledger 합계를 LEFT JOIN하고 missing 합계를 0으로 바꿔 저장 balance와 다른 행이 0건인지 검사한 뒤 Q1 marker를 출력한다. 이어서 account별 ledger 합계를 LEFT JOIN하고 missing 합계를 0으로 바꿔 저장 balance와 다른 행이 0건인지 검사한 뒤 Q1 marker를 출력한다.
- 실제 값 추적
- 범위 시작 입력은 sql/w18/01-reconcile.sql의 1줄 직전까지 형성된 parser·실행 상태를 입력으로 받는다. 끝에서 관찰할 상태는 fixture에서 mismatch count 0이면 예외 없이 W18_Q1 rows=0 한 줄이 stdout에 나타난다.
- 정상 예
- 동결 source 1~1줄을 그대로 적용하면 fixture에서 mismatch count 0이면 예외 없이 W18_Q1 rows=0 한 줄이 stdout에 나타난다.
- 틀린 예·반례
- hardcoded marker는 실제 SELECT 결과 row가 아니다 / account가 0행이면 mismatch count 0으로 vacuous pass한다 / fixture 이후 변형된 shared schema에도 영향을 받는다 / 합계 일치만 double-entry 완전성을 증명하지 않는다 상황을 정상 예로 간주하면 이 chunk의 Green 범위를 넘는다.
- 착각 방지
- 이 범위의 고정값은 mismatch=0이지만 marker·형식만으로 전체 실행을 인증하지 않는다.
- 하지 않는 일
- 현재 chunk가 직접 보장하지 않는 경계: hardcoded marker는 실제 SELECT 결과 row가 아니다 / account가 0행이면 mismatch count 0으로 vacuous pass한다 / fixture 이후 변형된 shared schema에도 영향을 받는다 / 합계 일치만 double-entry 완전성을 증명하지 않는다
- 다음 연결
- 끝 상태를 보존한 뒤 `파일 전체 source와 책임 경계` 범위에서 다음 입력·결과를 확인한다.
반례 먼저
-
정상 fixture만 보면 충분하지 않나요?
-
빈 집합·NULL·순서 역전 같은 반례도 넣어야 해.
-
여기서는 account가 0행이면 mismatch count 0으로 vacuous pass한다을 먼저 흔들어 본다.
-
첫 실패 지점을 줄 번호와 함께 기록하겠습니다.
STEP 08 / 13
실제 값 따라가기
같은 입력값이 어느 줄을 지나 어떤 결과가 되는지 추적합니다.
| 순서 | 들어온 값 | 코드가 하는 일 | 나온 값·상태 | 경계 |
|---|---|---|---|---|
| 1 | mismatch=0 | F04의 실행 순서 1단계에 넣어 source 조건을 적용한다. | 관찰 가능한 W18 상태에 `mismatch=0`가 기록되거나 그 값으로 비교된다. | hardcoded marker는 실제 SELECT 결과 row가 아니다 |
| 2 | account2 missing ledger -> 0 | F04의 실행 순서 2단계에 넣어 source 조건을 적용한다. | 관찰 가능한 W18 상태에 `account2 missing ledger -> 0`가 기록되거나 그 값으로 비교된다. | account가 0행이면 mismatch count 0으로 vacuous pass한다 |
| 3 | marker=W18_Q1 rows=0 | F04의 실행 순서 3단계에 넣어 source 조건을 적용한다. | 관찰 가능한 W18 상태에 `marker=W18_Q1 rows=0`가 기록되거나 그 값으로 비교된다. | fixture 이후 변형된 shared schema에도 영향을 받는다 |
전체 다시 쓰기
-
한 줄이 너무 길어서 읽기 힘들어요.
-
화면에서는 줄바꿈해 보되 source bytes와 실행 순서는 그대로 보존해.
-
마지막에는 marker=W18_Q1 rows=0을 source와 다시 대조한다.
-
뜻→chunk→전체 코드 순서로 복원하겠습니다.
STEP 09 / 13
PowerShell·SQL·DB 내부에서 벌어지는 일
PowerShell·SQL·DB에서 실제로 일어나는 일과 증명 범위를 구분합니다.
account 저장 balance와 account별 ledger 합계를 대조해 mismatch 0건을 요구한다.에서 1번째 내부 책임을 수행한다.
hardcoded marker는 실제 SELECT 결과 row가 아니다account 저장 balance와 account별 ledger 합계를 대조해 mismatch 0건을 요구한다.에서 2번째 내부 책임을 수행한다.
account가 0행이면 mismatch count 0으로 vacuous pass한다account 저장 balance와 account별 ledger 합계를 대조해 mismatch 0건을 요구한다.에서 3번째 내부 책임을 수행한다.
fixture 이후 변형된 shared schema에도 영향을 받는다account 저장 balance와 account별 ledger 합계를 대조해 mismatch 0건을 요구한다.에서 4번째 내부 책임을 수행한다.
합계 일치만 double-entry 완전성을 증명하지 않는다STEP 10 / 13
흔한 착각과 틀린 예
그럴듯하지만 틀린 해석을 반례로 고칩니다.
❌ mismatch=0이 보이면 관련 실행 전체가 완전하게 증명된다.
왜 틀리나 hardcoded marker는 실제 SELECT 결과 row가 아니다
바르게 읽기 고정 fixture에서 직접 검사한 값과 owner·DB·운영 계층의 나머지 책임을 분리한다.
반례 hardcoded marker는 실제 SELECT 결과 row가 아니다 상황에서는 같은 marker·형식이 보여도 의도한 전체 의미가 성립하지 않을 수 있다.
❌ account2 missing ledger -> 0이 보이면 관련 실행 전체가 완전하게 증명된다.
왜 틀리나 account가 0행이면 mismatch count 0으로 vacuous pass한다
바르게 읽기 고정 fixture에서 직접 검사한 값과 owner·DB·운영 계층의 나머지 책임을 분리한다.
반례 account가 0행이면 mismatch count 0으로 vacuous pass한다 상황에서는 같은 marker·형식이 보여도 의도한 전체 의미가 성립하지 않을 수 있다.
❌ marker=W18_Q1 rows=0이 보이면 관련 실행 전체가 완전하게 증명된다.
왜 틀리나 fixture 이후 변형된 shared schema에도 영향을 받는다
바르게 읽기 고정 fixture에서 직접 검사한 값과 owner·DB·운영 계층의 나머지 책임을 분리한다.
반례 fixture 이후 변형된 shared schema에도 영향을 받는다 상황에서는 같은 marker·형식이 보여도 의도한 전체 의미가 성립하지 않을 수 있다.
❌ mismatch=0이 보이면 관련 실행 전체가 완전하게 증명된다.
왜 틀리나 합계 일치만 double-entry 완전성을 증명하지 않는다
바르게 읽기 고정 fixture에서 직접 검사한 값과 owner·DB·운영 계층의 나머지 책임을 분리한다.
반례 합계 일치만 double-entry 완전성을 증명하지 않는다 상황에서는 같은 marker·형식이 보여도 의도한 전체 의미가 성립하지 않을 수 있다.
❌ account2 missing ledger -> 0이 보이면 관련 실행 전체가 완전하게 증명된다.
왜 틀리나 hardcoded marker는 실제 SELECT 결과 row가 아니다
바르게 읽기 고정 fixture에서 직접 검사한 값과 owner·DB·운영 계층의 나머지 책임을 분리한다.
반례 hardcoded marker는 실제 SELECT 결과 row가 아니다 상황에서는 같은 marker·형식이 보여도 의도한 전체 의미가 성립하지 않을 수 있다.
STEP 11 / 13
이 코드가 보장하지 않는 것
이 코드가 책임지지 않는 일을 분리합니다.
hardcoded marker는 실제 SELECT 결과 row가 아니다
이 책임을 맡는 곳: caller path policyaccount가 0행이면 mismatch count 0으로 vacuous pass한다
이 책임을 맡는 곳: exact oracle gatefixture 이후 변형된 shared schema에도 영향을 받는다
이 책임을 맡는 곳: SQL/parser counterexample test합계 일치만 double-entry 완전성을 증명하지 않는다
이 책임을 맡는 곳: runtime ownerhardcoded marker는 실제 SELECT 결과 row가 아니다
이 책임을 맡는 곳: byte-pinned source auditSTEP 12 / 13
직접 다시 써보기
뜻 → 조각 → 전체 코드 순서로 다시 씁니다.
1단계 · 뜻부터 복원
account 저장 balance와 account별 ledger 합계를 대조해 mismatch 0건을 요구한다.를 고정값과 미보장 경계까지 한 문장으로 말한다.
2단계 · 코드 조각 재조립
- reconcile invariant and marker
- NULL·empty·순서 반례와 Green 경계
3단계 · 파일 전체 다시 쓰기
1개 물리 줄을 원본 순서로 다시 쓰고 SHA-256 f07165ad6fe0a5b0d4f9246e9be1a7786f3a0a02d0f6d6cd746b40bf1e0bf6f5와 대조한다.
자가 점검
- 정본과 학습용 예시 label을 바꾸지 않는다.
- 긴 한 줄은 화면에서 감싸도 source exact text를 바꾸지 않는다.
- marker literal과 실제 결과 row를 구분한다.
- NULL·순서·empty fixture 반례를 하나 이상 말한다.
- owner mode와 cleanup 책임을 분리한다.
STEP 13 / 13
전체 원본 정답
감사로 고정한 전체 source를 가감 없이 확인합니다.
정본 전체 코드 확인하기
DO $g$ BEGIN IF (SELECT count(*) FROM w18_project.account a LEFT JOIN(SELECT account_id,sum(signed_amount)b FROM w18_project.ledger GROUP BY account_id)l ON l.account_id=a.id WHERE a.balance<>coalesce(l.b,0))<>0 THEN RAISE EXCEPTION 'reconcile';END IF;END $g$; SELECT 'W18_Q1 rows=0';
0502-anti-join.sql — 원장 없는 계정 찾기
sql/w18/02-anti-join.sql
정본 SQL invariant · 정본 · W18-F051줄 연결1줄 번역1 chunks
02-anti-join.sql — 원장 없는 계정 찾기
sql/w18/02-anti-join.sql
정본 SQL invariant · 정본 · W18-F05STEP 01 / 13
오늘 이 코드에서 해결할 문제
무엇을 이해해야 하는지 질문부터 잡습니다.
NOT EXISTS anti-join으로 ledger가 없는 account id 배열이 정확히 [2]인지 확인한다.
- missing id=2은 어느 줄에서 만들어지거나 검사될까?
- rows=1은 실제 계산값인가 고정 marker인가?
- array_agg가 NULL이면 IF NULL 조건은 실행되지 않아 empty-result 변형이 fail-open이다 경계에서 첫 실패는 어디일까?
- 빈 집합·NULL·동률·순서 역전 중 어떤 반례가 중요한가?
- 이 source가 책임지지 않는 lifecycle·provenance는 무엇인가?
missing id=2rows=1marker=W18_Q2STEP 02 / 13
아주 짧게: 이 코드는 왜 필요할까?
웹소설 대신 이 코드가 필요한 이유만 두 문단으로 쉽게 봅니다.
STARRY가 source를 값·순서·증명 경계가 적힌 작은 실험 카드로 나눈다.
02-anti-join.sql — 원장 없는 계정 찾기를 실행 전 검사표로 바꾸기
NOT EXISTS anti-join으로 ledger가 없는 account id 배열이 정확히 [2]인지 확인한다.
핵심 관찰값 missing id=2, rows=1, marker=W18_Q2을 따라가되, array_agg가 NULL이면 IF NULL 조건은 실행되지 않아 empty-result 변형이 fail-open이다까지 함께 표시해 Green 문구를 과대해석하지 않는다.
딱 여기까지만 비유는 순서를 기억하게 할 뿐 SQL NULL, native exit, hash, cleanup의 실제 proof를 대신하지 않는다.
STEP 03 / 13
초등학생도 이해하는 설명
생활 비유와 실제 코드의 경계를 함께 확인합니다.
anti-join invariant and marker
1~1줄을 한 덩어리로 읽어 NOT EXISTS anti-join으로 ledger가 없는 account id 배열이 정확히 [2]인지 확인한다.의 1번째 단계를 확인한다.
- 코드 연결
1~1줄- 비유
- 긴 조립 설명서에서 같은 일을 하는 부품만 한 봉투에 담아 확인한다.
- 비유의 끝
- array_agg가 NULL이면 IF NULL 조건은 실행되지 않아 empty-result 변형이 fail-open이다
전체 한 줄
NOT EXISTS anti-join으로 ledger가 없는 account id 배열이 정확히 [2]인지 확인한다.
- 코드 연결
1줄- 비유
- 한 장짜리 검사표를 처음부터 끝까지 읽는다.
- 비유의 끝
- array_agg가 NULL이면 IF NULL 조건은 실행되지 않아 empty-result 변형이 fail-open이다
왜 먼저 보는가
-
이 파일은 왜 필요한가요?
-
원장 없는 계정 찾기의 출발 계약을 먼저 고정해야 해.
-
직접 보장하는 값은 missing id=2 범위다.
-
목적과 결과를 같은 문장으로 섞지 않겠습니다.
고정값 읽기
-
숫자나 marker는 그냥 외우면 되나요?
-
아니, fixture·순서와 함께 rows=1을 읽어야 해.
-
고정값은 현재 source의 관찰 계약이지 모든 입력의 일반 법칙은 아니다.
-
입력→처리→결과 표로 다시 적어 볼게요.
STEP 04 / 13
비유 ↔ 코드 전체 연결표
감사 규칙상 연결 대상인 원본 1줄을 빠짐없이 연결합니다.
| 줄 | 정확한 원본 줄 | STARRY 비유 | 실제 뜻·입력·결과·한계 |
|---|---|---|---|
| 1줄F05-L01 | DO $g$ BEGIN IF ( |
줄을 세울 반·번호표·창 범위를 차례로 붙인다. | NOT EXISTS로 ledger가 없는 account id 배열이 [2]인지 검사한 뒤 Q2 marker를 출력한다.
|
Green의 작은 범위
-
Green 문구가 보이면 전부 안전한가요?
-
그 문구가 직접 검사한 조건까지만 안전해.
-
특히 array_agg가 NULL이면 IF NULL 조건은 실행되지 않아 empty-result 변형이 fail-open이다 경계는 별도 proof가 필요하다.
-
증명한 것과 안 한 것을 두 칸으로 나누겠습니다.
STEP 05 / 13
원본 코드 조각
원본을 1개 의미 조각으로 나누어 그대로 확인합니다.
파일을 한꺼번에 외우지 않고 실행 의미가 이어지는 작은 조각으로 봅니다. 아래 코드는 hash로 고정한 native 정본 source에서 그대로 잘랐습니다.
DO $g$ BEGIN IF (SELECT array_agg(id ORDER BY id) FROM w18_project.account a WHERE NOT EXISTS(SELECT 1 FROM w18_project.ledger l WHERE l.account_id=a.id))<>ARRAY[2::bigint] THEN RAISE EXCEPTION 'anti join';END IF;END $g$; SELECT 'W18_Q2 rows=1 id=2';
STEP 06 / 13
코드 한 줄씩 한국어로 번역
비어 있지 않은 1줄을 모두 한국어로 옮깁니다.
비어 있지 않은 원본 줄은 하나도 생략하지 않습니다.
| 줄 | 원본 | 한국어 번역 |
|---|---|---|
| 1 | DO $g$ BEGIN IF (SELECT array_agg(id ORDER BY id) FROM w18_project.account a WHERE NOT EXISTS(SELECT 1 FROM w18_project.ledger l WHERE l.account_id=a.id))<>ARRAY[2::bigint] THEN RAISE EXCEPTION 'anti join';END IF;END $g$; SELECT 'W18_Q2 rows=1 id=2'; | NOT EXISTS로 ledger가 없는 account id 배열이 [2]인지 검사한 뒤 Q2 marker를 출력한다. |
STEP 07 / 13
기존 수준의 한 줄 읽기·문법 해부
쉬운 설명 다음에 문법과 실행 순서를 정밀하게 읽습니다.
한 줄로 읽기NOT EXISTS anti-join으로 ledger가 없는 account id 배열이 정확히 [2]인지 확인한다. 다만 array_agg가 NULL이면 IF NULL 조건은 실행되지 않아 empty-result 변형이 fail-open이다
문법 해부
- DO block의 PL/pgSQL IF와 뒤의 SELECT marker는 서로 다른 statement다.
- SQL NULL 비교는 false가 아니라 unknown이 될 수 있어 IF fail-open 여부를 확인해야 한다.
- hardcoded marker 문자열과 실제 query 결과 row를 구분한다.
실행 순서
- DO block query
- fixture-bound invariant comparison
- optional exception
- hardcoded marker SELECT
- runner substring check
원래 W6 수준의 조각별 정밀 해설
F05-C01 · anti-join invariant and marker
- 문법 해부
- `anti-join invariant and marker` 범위는 NOT EXISTS로 ledger가 없는 account id 배열이 [2]인지 검사한 뒤 Q2 marker를 출력한다. 이어서 NOT EXISTS로 ledger가 없는 account id 배열이 [2]인지 검사한 뒤 Q2 marker를 출력한다.
- 실제 값 추적
- 범위 시작 입력은 sql/w18/02-anti-join.sql의 1줄 직전까지 형성된 parser·실행 상태를 입력으로 받는다. 끝에서 관찰할 상태는 현재 fixture에서는 missing id 배열 [2]가 통과해 W18_Q2 rows=1 id=2가 출력된다.
- 정상 예
- 동결 source 1~1줄을 그대로 적용하면 현재 fixture에서는 missing id 배열 [2]가 통과해 W18_Q2 rows=1 id=2가 출력된다.
- 틀린 예·반례
- array_agg가 NULL이면 IF NULL 조건은 실행되지 않아 empty-result 변형이 fail-open이다 / marker의 rows=1은 실제 row count에서 계산되지 않는다 / 현재 fixture에 묶인 oracle이다 상황을 정상 예로 간주하면 이 chunk의 Green 범위를 넘는다.
- 착각 방지
- 이 범위의 고정값은 missing id=2이지만 marker·형식만으로 전체 실행을 인증하지 않는다.
- 하지 않는 일
- 현재 chunk가 직접 보장하지 않는 경계: array_agg가 NULL이면 IF NULL 조건은 실행되지 않아 empty-result 변형이 fail-open이다 / marker의 rows=1은 실제 row count에서 계산되지 않는다 / 현재 fixture에 묶인 oracle이다
- 다음 연결
- 끝 상태를 보존한 뒤 `파일 전체 source와 책임 경계` 범위에서 다음 입력·결과를 확인한다.
반례 먼저
-
정상 fixture만 보면 충분하지 않나요?
-
빈 집합·NULL·순서 역전 같은 반례도 넣어야 해.
-
여기서는 marker의 rows=1은 실제 row count에서 계산되지 않는다을 먼저 흔들어 본다.
-
첫 실패 지점을 줄 번호와 함께 기록하겠습니다.
STEP 08 / 13
실제 값 따라가기
같은 입력값이 어느 줄을 지나 어떤 결과가 되는지 추적합니다.
| 순서 | 들어온 값 | 코드가 하는 일 | 나온 값·상태 | 경계 |
|---|---|---|---|---|
| 1 | missing id=2 | F05의 실행 순서 1단계에 넣어 source 조건을 적용한다. | 관찰 가능한 W18 상태에 `missing id=2`가 기록되거나 그 값으로 비교된다. | array_agg가 NULL이면 IF NULL 조건은 실행되지 않아 empty-result 변형이 fail-open이다 |
| 2 | rows=1 | F05의 실행 순서 2단계에 넣어 source 조건을 적용한다. | 관찰 가능한 W18 상태에 `rows=1`가 기록되거나 그 값으로 비교된다. | marker의 rows=1은 실제 row count에서 계산되지 않는다 |
| 3 | marker=W18_Q2 | F05의 실행 순서 3단계에 넣어 source 조건을 적용한다. | 관찰 가능한 W18 상태에 `marker=W18_Q2`가 기록되거나 그 값으로 비교된다. | 현재 fixture에 묶인 oracle이다 |
전체 다시 쓰기
-
한 줄이 너무 길어서 읽기 힘들어요.
-
화면에서는 줄바꿈해 보되 source bytes와 실행 순서는 그대로 보존해.
-
마지막에는 marker=W18_Q2을 source와 다시 대조한다.
-
뜻→chunk→전체 코드 순서로 복원하겠습니다.
STEP 09 / 13
PowerShell·SQL·DB 내부에서 벌어지는 일
PowerShell·SQL·DB에서 실제로 일어나는 일과 증명 범위를 구분합니다.
NOT EXISTS anti-join으로 ledger가 없는 account id 배열이 정확히 [2]인지 확인한다.에서 1번째 내부 책임을 수행한다.
array_agg가 NULL이면 IF NULL 조건은 실행되지 않아 empty-result 변형이 fail-open이다NOT EXISTS anti-join으로 ledger가 없는 account id 배열이 정확히 [2]인지 확인한다.에서 2번째 내부 책임을 수행한다.
marker의 rows=1은 실제 row count에서 계산되지 않는다NOT EXISTS anti-join으로 ledger가 없는 account id 배열이 정확히 [2]인지 확인한다.에서 3번째 내부 책임을 수행한다.
현재 fixture에 묶인 oracle이다NOT EXISTS anti-join으로 ledger가 없는 account id 배열이 정확히 [2]인지 확인한다.에서 4번째 내부 책임을 수행한다.
array_agg가 NULL이면 IF NULL 조건은 실행되지 않아 empty-result 변형이 fail-open이다STEP 10 / 13
흔한 착각과 틀린 예
그럴듯하지만 틀린 해석을 반례로 고칩니다.
❌ missing id=2이 보이면 관련 실행 전체가 완전하게 증명된다.
왜 틀리나 array_agg가 NULL이면 IF NULL 조건은 실행되지 않아 empty-result 변형이 fail-open이다
바르게 읽기 고정 fixture에서 직접 검사한 값과 owner·DB·운영 계층의 나머지 책임을 분리한다.
반례 array_agg가 NULL이면 IF NULL 조건은 실행되지 않아 empty-result 변형이 fail-open이다 상황에서는 같은 marker·형식이 보여도 의도한 전체 의미가 성립하지 않을 수 있다.
❌ rows=1이 보이면 관련 실행 전체가 완전하게 증명된다.
왜 틀리나 marker의 rows=1은 실제 row count에서 계산되지 않는다
바르게 읽기 고정 fixture에서 직접 검사한 값과 owner·DB·운영 계층의 나머지 책임을 분리한다.
반례 marker의 rows=1은 실제 row count에서 계산되지 않는다 상황에서는 같은 marker·형식이 보여도 의도한 전체 의미가 성립하지 않을 수 있다.
❌ marker=W18_Q2이 보이면 관련 실행 전체가 완전하게 증명된다.
왜 틀리나 현재 fixture에 묶인 oracle이다
바르게 읽기 고정 fixture에서 직접 검사한 값과 owner·DB·운영 계층의 나머지 책임을 분리한다.
반례 현재 fixture에 묶인 oracle이다 상황에서는 같은 marker·형식이 보여도 의도한 전체 의미가 성립하지 않을 수 있다.
❌ missing id=2이 보이면 관련 실행 전체가 완전하게 증명된다.
왜 틀리나 array_agg가 NULL이면 IF NULL 조건은 실행되지 않아 empty-result 변형이 fail-open이다
바르게 읽기 고정 fixture에서 직접 검사한 값과 owner·DB·운영 계층의 나머지 책임을 분리한다.
반례 array_agg가 NULL이면 IF NULL 조건은 실행되지 않아 empty-result 변형이 fail-open이다 상황에서는 같은 marker·형식이 보여도 의도한 전체 의미가 성립하지 않을 수 있다.
❌ rows=1이 보이면 관련 실행 전체가 완전하게 증명된다.
왜 틀리나 marker의 rows=1은 실제 row count에서 계산되지 않는다
바르게 읽기 고정 fixture에서 직접 검사한 값과 owner·DB·운영 계층의 나머지 책임을 분리한다.
반례 marker의 rows=1은 실제 row count에서 계산되지 않는다 상황에서는 같은 marker·형식이 보여도 의도한 전체 의미가 성립하지 않을 수 있다.
STEP 11 / 13
이 코드가 보장하지 않는 것
이 코드가 책임지지 않는 일을 분리합니다.
array_agg가 NULL이면 IF NULL 조건은 실행되지 않아 empty-result 변형이 fail-open이다
이 책임을 맡는 곳: caller path policymarker의 rows=1은 실제 row count에서 계산되지 않는다
이 책임을 맡는 곳: exact oracle gate현재 fixture에 묶인 oracle이다
이 책임을 맡는 곳: SQL/parser counterexample testarray_agg가 NULL이면 IF NULL 조건은 실행되지 않아 empty-result 변형이 fail-open이다
이 책임을 맡는 곳: runtime ownermarker의 rows=1은 실제 row count에서 계산되지 않는다
이 책임을 맡는 곳: byte-pinned source auditSTEP 12 / 13
직접 다시 써보기
뜻 → 조각 → 전체 코드 순서로 다시 씁니다.
1단계 · 뜻부터 복원
NOT EXISTS anti-join으로 ledger가 없는 account id 배열이 정확히 [2]인지 확인한다.를 고정값과 미보장 경계까지 한 문장으로 말한다.
2단계 · 코드 조각 재조립
- anti-join invariant and marker
- NULL·empty·순서 반례와 Green 경계
3단계 · 파일 전체 다시 쓰기
1개 물리 줄을 원본 순서로 다시 쓰고 SHA-256 8ea9c9c8ec2a81157fe6a400b35281c22c51219b62417f897a8fe6cc50b3bb64와 대조한다.
자가 점검
- 정본과 학습용 예시 label을 바꾸지 않는다.
- 긴 한 줄은 화면에서 감싸도 source exact text를 바꾸지 않는다.
- marker literal과 실제 결과 row를 구분한다.
- NULL·순서·empty fixture 반례를 하나 이상 말한다.
- owner mode와 cleanup 책임을 분리한다.
STEP 13 / 13
전체 원본 정답
감사로 고정한 전체 source를 가감 없이 확인합니다.
정본 전체 코드 확인하기
DO $g$ BEGIN IF (SELECT array_agg(id ORDER BY id) FROM w18_project.account a WHERE NOT EXISTS(SELECT 1 FROM w18_project.ledger l WHERE l.account_id=a.id))<>ARRAY[2::bigint] THEN RAISE EXCEPTION 'anti join';END IF;END $g$; SELECT 'W18_Q2 rows=1 id=2';
0603-aggregation.sql — credit·debit 조건부 집계
sql/w18/03-aggregation.sql
정본 SQL invariant · 정본 · W18-F061줄 연결1줄 번역1 chunks
03-aggregation.sql — credit·debit 조건부 집계
sql/w18/03-aggregation.sql
정본 SQL invariant · 정본 · W18-F06STEP 01 / 13
오늘 이 코드에서 해결할 문제
무엇을 이해해야 하는지 질문부터 잡습니다.
account 1의 ledger 두 행을 조건부 집계해 credit 100과 debit 30을 요구한다.
- n=2은 어느 줄에서 만들어지거나 검사될까?
- credits=100은 실제 계산값인가 고정 marker인가?
- account_id=1에 고정된 fixture oracle이다 경계에서 첫 실패는 어디일까?
- 빈 집합·NULL·동률·순서 역전 중 어떤 반례가 중요한가?
- 이 source가 책임지지 않는 lifecycle·provenance는 무엇인가?
n=2credits=100debits=30marker=W18_Q3STEP 02 / 13
아주 짧게: 이 코드는 왜 필요할까?
웹소설 대신 이 코드가 필요한 이유만 두 문단으로 쉽게 봅니다.
STARRY가 source를 값·순서·증명 경계가 적힌 작은 실험 카드로 나눈다.
03-aggregation.sql — credit·debit 조건부 집계를 실행 전 검사표로 바꾸기
account 1의 ledger 두 행을 조건부 집계해 credit 100과 debit 30을 요구한다.
핵심 관찰값 n=2, credits=100, debits=30, marker=W18_Q3을 따라가되, account_id=1에 고정된 fixture oracle이다까지 함께 표시해 Green 문구를 과대해석하지 않는다.
딱 여기까지만 비유는 순서를 기억하게 할 뿐 SQL NULL, native exit, hash, cleanup의 실제 proof를 대신하지 않는다.
STEP 03 / 13
초등학생도 이해하는 설명
생활 비유와 실제 코드의 경계를 함께 확인합니다.
conditional aggregation invariant and marker
1~1줄을 한 덩어리로 읽어 account 1의 ledger 두 행을 조건부 집계해 credit 100과 debit 30을 요구한다.의 1번째 단계를 확인한다.
- 코드 연결
1~1줄- 비유
- 긴 조립 설명서에서 같은 일을 하는 부품만 한 봉투에 담아 확인한다.
- 비유의 끝
- account_id=1에 고정된 fixture oracle이다
전체 한 줄
account 1의 ledger 두 행을 조건부 집계해 credit 100과 debit 30을 요구한다.
- 코드 연결
1줄- 비유
- 한 장짜리 검사표를 처음부터 끝까지 읽는다.
- 비유의 끝
- account_id=1에 고정된 fixture oracle이다
왜 먼저 보는가
-
이 파일은 왜 필요한가요?
-
credit·debit 조건부 집계의 출발 계약을 먼저 고정해야 해.
-
직접 보장하는 값은 n=2 범위다.
-
목적과 결과를 같은 문장으로 섞지 않겠습니다.
고정값 읽기
-
숫자나 marker는 그냥 외우면 되나요?
-
아니, fixture·순서와 함께 credits=100을 읽어야 해.
-
고정값은 현재 source의 관찰 계약이지 모든 입력의 일반 법칙은 아니다.
-
입력→처리→결과 표로 다시 적어 볼게요.
STEP 04 / 13
비유 ↔ 코드 전체 연결표
감사 규칙상 연결 대상인 원본 1줄을 빠짐없이 연결합니다.
| 줄 | 정확한 원본 줄 | STARRY 비유 | 실제 뜻·입력·결과·한계 |
|---|---|---|---|
| 1줄F06-L01 | DO $g$ DECLARE r record; |
검사표가 어긋나면 다음 방으로 가지 못하게 비상벨을 울린다. | account 1의 행 수·양수 합·음수 절댓값 합을 record로 받아 2·100·30인지 검사한 뒤 Q3 marker를 출력한다.
|
Green의 작은 범위
-
Green 문구가 보이면 전부 안전한가요?
-
그 문구가 직접 검사한 조건까지만 안전해.
-
특히 account_id=1에 고정된 fixture oracle이다 경계는 별도 proof가 필요하다.
-
증명한 것과 안 한 것을 두 칸으로 나누겠습니다.
STEP 05 / 13
원본 코드 조각
원본을 1개 의미 조각으로 나누어 그대로 확인합니다.
파일을 한꺼번에 외우지 않고 실행 의미가 이어지는 작은 조각으로 봅니다. 아래 코드는 hash로 고정한 native 정본 source에서 그대로 잘랐습니다.
DO $g$ DECLARE r record;BEGIN SELECT count(*) n,coalesce(sum(signed_amount)FILTER(WHERE signed_amount>0),0)p,coalesce(-sum(signed_amount)FILTER(WHERE signed_amount<0),0)d INTO r FROM w18_project.ledger WHERE account_id=1;IF r.n<>2 OR r.p<>100 OR r.d<>30 THEN RAISE EXCEPTION 'aggregation';END IF;END $g$; SELECT 'W18_Q3 rows=1 credits=100 debits=30';
STEP 06 / 13
코드 한 줄씩 한국어로 번역
비어 있지 않은 1줄을 모두 한국어로 옮깁니다.
비어 있지 않은 원본 줄은 하나도 생략하지 않습니다.
| 줄 | 원본 | 한국어 번역 |
|---|---|---|
| 1 | DO $g$ DECLARE r record;BEGIN SELECT count(*) n,coalesce(sum(signed_amount)FILTER(WHERE signed_amount>0),0)p,coalesce(-sum(signed_amount)FILTER(WHERE signed_amount<0),0)d INTO r FROM w18_project.ledger WHERE account_id=1;IF r.n<>2 OR r.p<>100 OR r.d<>30 THEN RAISE EXCEPTION 'aggregation';END IF;END $g$; SELECT 'W18_Q3 rows=1 credits=100 debits=30'; | account 1의 행 수·양수 합·음수 절댓값 합을 record로 받아 2·100·30인지 검사한 뒤 Q3 marker를 출력한다. |
STEP 07 / 13
기존 수준의 한 줄 읽기·문법 해부
쉬운 설명 다음에 문법과 실행 순서를 정밀하게 읽습니다.
한 줄로 읽기account 1의 ledger 두 행을 조건부 집계해 credit 100과 debit 30을 요구한다. 다만 account_id=1에 고정된 fixture oracle이다
문법 해부
- DO block의 PL/pgSQL IF와 뒤의 SELECT marker는 서로 다른 statement다.
- SQL NULL 비교는 false가 아니라 unknown이 될 수 있어 IF fail-open 여부를 확인해야 한다.
- hardcoded marker 문자열과 실제 query 결과 row를 구분한다.
실행 순서
- DO block query
- fixture-bound invariant comparison
- optional exception
- hardcoded marker SELECT
- runner substring check
원래 W6 수준의 조각별 정밀 해설
F06-C01 · conditional aggregation invariant and marker
- 문법 해부
- `conditional aggregation invariant and marker` 범위는 account 1의 행 수·양수 합·음수 절댓값 합을 record로 받아 2·100·30인지 검사한 뒤 Q3 marker를 출력한다. 이어서 account 1의 행 수·양수 합·음수 절댓값 합을 record로 받아 2·100·30인지 검사한 뒤 Q3 marker를 출력한다.
- 실제 값 추적
- 범위 시작 입력은 sql/w18/03-aggregation.sql의 1줄 직전까지 형성된 parser·실행 상태를 입력으로 받는다. 끝에서 관찰할 상태는 record가 n=2,p=100,d=30이면 W18_Q3 credits=100 debits=30 marker가 출력된다.
- 정상 예
- 동결 source 1~1줄을 그대로 적용하면 record가 n=2,p=100,d=30이면 W18_Q3 credits=100 debits=30 marker가 출력된다.
- 틀린 예·반례
- account_id=1에 고정된 fixture oracle이다 / debit은 음수 합에 minus를 붙여 양수로 표시한다 / marker는 계산값을 직접 출력하지 않는다 상황을 정상 예로 간주하면 이 chunk의 Green 범위를 넘는다.
- 착각 방지
- 이 범위의 고정값은 n=2이지만 marker·형식만으로 전체 실행을 인증하지 않는다.
- 하지 않는 일
- 현재 chunk가 직접 보장하지 않는 경계: account_id=1에 고정된 fixture oracle이다 / debit은 음수 합에 minus를 붙여 양수로 표시한다 / marker는 계산값을 직접 출력하지 않는다
- 다음 연결
- 끝 상태를 보존한 뒤 `파일 전체 source와 책임 경계` 범위에서 다음 입력·결과를 확인한다.
반례 먼저
-
정상 fixture만 보면 충분하지 않나요?
-
빈 집합·NULL·순서 역전 같은 반례도 넣어야 해.
-
여기서는 debit은 음수 합에 minus를 붙여 양수로 표시한다을 먼저 흔들어 본다.
-
첫 실패 지점을 줄 번호와 함께 기록하겠습니다.
STEP 08 / 13
실제 값 따라가기
같은 입력값이 어느 줄을 지나 어떤 결과가 되는지 추적합니다.
| 순서 | 들어온 값 | 코드가 하는 일 | 나온 값·상태 | 경계 |
|---|---|---|---|---|
| 1 | n=2 | F06의 실행 순서 1단계에 넣어 source 조건을 적용한다. | 관찰 가능한 W18 상태에 `n=2`가 기록되거나 그 값으로 비교된다. | account_id=1에 고정된 fixture oracle이다 |
| 2 | credits=100 | F06의 실행 순서 2단계에 넣어 source 조건을 적용한다. | 관찰 가능한 W18 상태에 `credits=100`가 기록되거나 그 값으로 비교된다. | debit은 음수 합에 minus를 붙여 양수로 표시한다 |
| 3 | debits=30 | F06의 실행 순서 3단계에 넣어 source 조건을 적용한다. | 관찰 가능한 W18 상태에 `debits=30`가 기록되거나 그 값으로 비교된다. | marker는 계산값을 직접 출력하지 않는다 |
| 4 | marker=W18_Q3 | F06의 실행 순서 4단계에 넣어 source 조건을 적용한다. | 관찰 가능한 W18 상태에 `marker=W18_Q3`가 기록되거나 그 값으로 비교된다. | account_id=1에 고정된 fixture oracle이다 |
전체 다시 쓰기
-
한 줄이 너무 길어서 읽기 힘들어요.
-
화면에서는 줄바꿈해 보되 source bytes와 실행 순서는 그대로 보존해.
-
마지막에는 marker=W18_Q3을 source와 다시 대조한다.
-
뜻→chunk→전체 코드 순서로 복원하겠습니다.
STEP 09 / 13
PowerShell·SQL·DB 내부에서 벌어지는 일
PowerShell·SQL·DB에서 실제로 일어나는 일과 증명 범위를 구분합니다.
account 1의 ledger 두 행을 조건부 집계해 credit 100과 debit 30을 요구한다.에서 1번째 내부 책임을 수행한다.
account_id=1에 고정된 fixture oracle이다account 1의 ledger 두 행을 조건부 집계해 credit 100과 debit 30을 요구한다.에서 2번째 내부 책임을 수행한다.
debit은 음수 합에 minus를 붙여 양수로 표시한다account 1의 ledger 두 행을 조건부 집계해 credit 100과 debit 30을 요구한다.에서 3번째 내부 책임을 수행한다.
marker는 계산값을 직접 출력하지 않는다account 1의 ledger 두 행을 조건부 집계해 credit 100과 debit 30을 요구한다.에서 4번째 내부 책임을 수행한다.
account_id=1에 고정된 fixture oracle이다STEP 10 / 13
흔한 착각과 틀린 예
그럴듯하지만 틀린 해석을 반례로 고칩니다.
❌ n=2이 보이면 관련 실행 전체가 완전하게 증명된다.
왜 틀리나 account_id=1에 고정된 fixture oracle이다
바르게 읽기 고정 fixture에서 직접 검사한 값과 owner·DB·운영 계층의 나머지 책임을 분리한다.
반례 account_id=1에 고정된 fixture oracle이다 상황에서는 같은 marker·형식이 보여도 의도한 전체 의미가 성립하지 않을 수 있다.
❌ credits=100이 보이면 관련 실행 전체가 완전하게 증명된다.
왜 틀리나 debit은 음수 합에 minus를 붙여 양수로 표시한다
바르게 읽기 고정 fixture에서 직접 검사한 값과 owner·DB·운영 계층의 나머지 책임을 분리한다.
반례 debit은 음수 합에 minus를 붙여 양수로 표시한다 상황에서는 같은 marker·형식이 보여도 의도한 전체 의미가 성립하지 않을 수 있다.
❌ debits=30이 보이면 관련 실행 전체가 완전하게 증명된다.
왜 틀리나 marker는 계산값을 직접 출력하지 않는다
바르게 읽기 고정 fixture에서 직접 검사한 값과 owner·DB·운영 계층의 나머지 책임을 분리한다.
반례 marker는 계산값을 직접 출력하지 않는다 상황에서는 같은 marker·형식이 보여도 의도한 전체 의미가 성립하지 않을 수 있다.
❌ marker=W18_Q3이 보이면 관련 실행 전체가 완전하게 증명된다.
왜 틀리나 account_id=1에 고정된 fixture oracle이다
바르게 읽기 고정 fixture에서 직접 검사한 값과 owner·DB·운영 계층의 나머지 책임을 분리한다.
반례 account_id=1에 고정된 fixture oracle이다 상황에서는 같은 marker·형식이 보여도 의도한 전체 의미가 성립하지 않을 수 있다.
❌ n=2이 보이면 관련 실행 전체가 완전하게 증명된다.
왜 틀리나 debit은 음수 합에 minus를 붙여 양수로 표시한다
바르게 읽기 고정 fixture에서 직접 검사한 값과 owner·DB·운영 계층의 나머지 책임을 분리한다.
반례 debit은 음수 합에 minus를 붙여 양수로 표시한다 상황에서는 같은 marker·형식이 보여도 의도한 전체 의미가 성립하지 않을 수 있다.
STEP 11 / 13
이 코드가 보장하지 않는 것
이 코드가 책임지지 않는 일을 분리합니다.
account_id=1에 고정된 fixture oracle이다
이 책임을 맡는 곳: caller path policydebit은 음수 합에 minus를 붙여 양수로 표시한다
이 책임을 맡는 곳: exact oracle gatemarker는 계산값을 직접 출력하지 않는다
이 책임을 맡는 곳: SQL/parser counterexample testaccount_id=1에 고정된 fixture oracle이다
이 책임을 맡는 곳: runtime ownerdebit은 음수 합에 minus를 붙여 양수로 표시한다
이 책임을 맡는 곳: byte-pinned source auditSTEP 12 / 13
직접 다시 써보기
뜻 → 조각 → 전체 코드 순서로 다시 씁니다.
1단계 · 뜻부터 복원
account 1의 ledger 두 행을 조건부 집계해 credit 100과 debit 30을 요구한다.를 고정값과 미보장 경계까지 한 문장으로 말한다.
2단계 · 코드 조각 재조립
- conditional aggregation invariant and marker
- NULL·empty·순서 반례와 Green 경계
3단계 · 파일 전체 다시 쓰기
1개 물리 줄을 원본 순서로 다시 쓰고 SHA-256 c07f6588ae5b431c733d84d097c2dec446470470405e57e0126e8e09dcd82c1d와 대조한다.
자가 점검
- 정본과 학습용 예시 label을 바꾸지 않는다.
- 긴 한 줄은 화면에서 감싸도 source exact text를 바꾸지 않는다.
- marker literal과 실제 결과 row를 구분한다.
- NULL·순서·empty fixture 반례를 하나 이상 말한다.
- owner mode와 cleanup 책임을 분리한다.
STEP 13 / 13
전체 원본 정답
감사로 고정한 전체 source를 가감 없이 확인합니다.
정본 전체 코드 확인하기
DO $g$ DECLARE r record;BEGIN SELECT count(*) n,coalesce(sum(signed_amount)FILTER(WHERE signed_amount>0),0)p,coalesce(-sum(signed_amount)FILTER(WHERE signed_amount<0),0)d INTO r FROM w18_project.ledger WHERE account_id=1;IF r.n<>2 OR r.p<>100 OR r.d<>30 THEN RAISE EXCEPTION 'aggregation';END IF;END $g$; SELECT 'W18_Q3 rows=1 credits=100 debits=30';
0704-running-balance.sql — 안정 순서 누적 잔액
sql/w18/04-running-balance.sql
정본 SQL invariant · 정본 · W18-F071줄 연결1줄 번역1 chunks
04-running-balance.sql — 안정 순서 누적 잔액
sql/w18/04-running-balance.sql
정본 SQL invariant · 정본 · W18-F07STEP 01 / 13
오늘 이 코드에서 해결할 문제
무엇을 이해해야 하는지 질문부터 잡습니다.
created_at,id 순서 window sum으로 account 1의 누적잔액을 만들고 최종값 70을 검사한다.
- 100 -> 70은 어느 줄에서 만들어지거나 검사될까?
- ORDER BY created_at,id은 실제 계산값인가 고정 marker인가?
- 최종 행 선택은 id DESC라 id와 시간 순서가 어긋나면 잘못된 행을 집을 수 있다 경계에서 첫 실패는 어디일까?
- 빈 집합·NULL·동률·순서 역전 중 어떤 반례가 중요한가?
- 이 source가 책임지지 않는 lifecycle·provenance는 무엇인가?
100 -> 70ORDER BY created_at,idmarker rows=2 final=70STEP 02 / 13
아주 짧게: 이 코드는 왜 필요할까?
웹소설 대신 이 코드가 필요한 이유만 두 문단으로 쉽게 봅니다.
STARRY가 source를 값·순서·증명 경계가 적힌 작은 실험 카드로 나눈다.
04-running-balance.sql — 안정 순서 누적 잔액를 실행 전 검사표로 바꾸기
created_at,id 순서 window sum으로 account 1의 누적잔액을 만들고 최종값 70을 검사한다.
핵심 관찰값 100 -> 70, ORDER BY created_at,id, marker rows=2 final=70을 따라가되, 최종 행 선택은 id DESC라 id와 시간 순서가 어긋나면 잘못된 행을 집을 수 있다까지 함께 표시해 Green 문구를 과대해석하지 않는다.
딱 여기까지만 비유는 순서를 기억하게 할 뿐 SQL NULL, native exit, hash, cleanup의 실제 proof를 대신하지 않는다.
STEP 03 / 13
초등학생도 이해하는 설명
생활 비유와 실제 코드의 경계를 함께 확인합니다.
window running balance invariant and marker
1~1줄을 한 덩어리로 읽어 created_at,id 순서 window sum으로 account 1의 누적잔액을 만들고 최종값 70을 검사한다.의 1번째 단계를 확인한다.
- 코드 연결
1~1줄- 비유
- 긴 조립 설명서에서 같은 일을 하는 부품만 한 봉투에 담아 확인한다.
- 비유의 끝
- 최종 행 선택은 id DESC라 id와 시간 순서가 어긋나면 잘못된 행을 집을 수 있다
전체 한 줄
created_at,id 순서 window sum으로 account 1의 누적잔액을 만들고 최종값 70을 검사한다.
- 코드 연결
1줄- 비유
- 한 장짜리 검사표를 처음부터 끝까지 읽는다.
- 비유의 끝
- 최종 행 선택은 id DESC라 id와 시간 순서가 어긋나면 잘못된 행을 집을 수 있다
왜 먼저 보는가
-
이 파일은 왜 필요한가요?
-
안정 순서 누적 잔액의 출발 계약을 먼저 고정해야 해.
-
직접 보장하는 값은 100 -> 70 범위다.
-
목적과 결과를 같은 문장으로 섞지 않겠습니다.
고정값 읽기
-
숫자나 marker는 그냥 외우면 되나요?
-
아니, fixture·순서와 함께 ORDER BY created_at,id을 읽어야 해.
-
고정값은 현재 source의 관찰 계약이지 모든 입력의 일반 법칙은 아니다.
-
입력→처리→결과 표로 다시 적어 볼게요.
STEP 04 / 13
비유 ↔ 코드 전체 연결표
감사 규칙상 연결 대상인 원본 1줄을 빠짐없이 연결합니다.
| 줄 | 정확한 원본 줄 | STARRY 비유 | 실제 뜻·입력·결과·한계 |
|---|---|---|---|
| 1줄F07-L01 | DO $g$ DECLARE x bigint; |
줄을 세울 반·번호표·창 범위를 차례로 붙인다. | account 1을 created_at,id 순서로 window sum한 뒤 id가 가장 큰 행의 running 값이 70인지 검사하고 Q4 marker를 출력한다.
|
Green의 작은 범위
-
Green 문구가 보이면 전부 안전한가요?
-
그 문구가 직접 검사한 조건까지만 안전해.
-
특히 최종 행 선택은 id DESC라 id와 시간 순서가 어긋나면 잘못된 행을 집을 수 있다 경계는 별도 proof가 필요하다.
-
증명한 것과 안 한 것을 두 칸으로 나누겠습니다.
STEP 05 / 13
원본 코드 조각
원본을 1개 의미 조각으로 나누어 그대로 확인합니다.
파일을 한꺼번에 외우지 않고 실행 의미가 이어지는 작은 조각으로 봅니다. 아래 코드는 hash로 고정한 native 정본 source에서 그대로 잘랐습니다.
DO $g$ DECLARE x bigint;BEGIN SELECT running INTO x FROM(SELECT id,sum(signed_amount)OVER(PARTITION BY account_id ORDER BY created_at,id)running FROM w18_project.ledger WHERE account_id=1)s ORDER BY id DESC LIMIT 1;IF x<>70 THEN RAISE EXCEPTION 'running';END IF;END $g$; SELECT 'W18_Q4 rows=2 final=70';
STEP 06 / 13
코드 한 줄씩 한국어로 번역
비어 있지 않은 1줄을 모두 한국어로 옮깁니다.
비어 있지 않은 원본 줄은 하나도 생략하지 않습니다.
| 줄 | 원본 | 한국어 번역 |
|---|---|---|
| 1 | DO $g$ DECLARE x bigint;BEGIN SELECT running INTO x FROM(SELECT id,sum(signed_amount)OVER(PARTITION BY account_id ORDER BY created_at,id)running FROM w18_project.ledger WHERE account_id=1)s ORDER BY id DESC LIMIT 1;IF x<>70 THEN RAISE EXCEPTION 'running';END IF;END $g$; SELECT 'W18_Q4 rows=2 final=70'; | account 1을 created_at,id 순서로 window sum한 뒤 id가 가장 큰 행의 running 값이 70인지 검사하고 Q4 marker를 출력한다. |
STEP 07 / 13
기존 수준의 한 줄 읽기·문법 해부
쉬운 설명 다음에 문법과 실행 순서를 정밀하게 읽습니다.
한 줄로 읽기created_at,id 순서 window sum으로 account 1의 누적잔액을 만들고 최종값 70을 검사한다. 다만 최종 행 선택은 id DESC라 id와 시간 순서가 어긋나면 잘못된 행을 집을 수 있다
문법 해부
- DO block의 PL/pgSQL IF와 뒤의 SELECT marker는 서로 다른 statement다.
- SQL NULL 비교는 false가 아니라 unknown이 될 수 있어 IF fail-open 여부를 확인해야 한다.
- hardcoded marker 문자열과 실제 query 결과 row를 구분한다.
실행 순서
- DO block query
- fixture-bound invariant comparison
- optional exception
- hardcoded marker SELECT
- runner substring check
원래 W6 수준의 조각별 정밀 해설
F07-C01 · window running balance invariant and marker
- 문법 해부
- `window running balance invariant and marker` 범위는 account 1을 created_at,id 순서로 window sum한 뒤 id가 가장 큰 행의 running 값이 70인지 검사하고 Q4 marker를 출력한다. 이어서 account 1을 created_at,id 순서로 window sum한 뒤 id가 가장 큰 행의 running 값이 70인지 검사하고 Q4 marker를 출력한다.
- 실제 값 추적
- 범위 시작 입력은 sql/w18/04-running-balance.sql의 1줄 직전까지 형성된 parser·실행 상태를 입력으로 받는다. 끝에서 관찰할 상태는 현재 id/time 정렬에서는 100 다음 70이 되어 W18_Q4 final=70 marker가 출력된다.
- 정상 예
- 동결 source 1~1줄을 그대로 적용하면 현재 id/time 정렬에서는 100 다음 70이 되어 W18_Q4 final=70 marker가 출력된다.
- 틀린 예·반례
- 최종 행 선택은 id DESC라 id와 시간 순서가 어긋나면 잘못된 행을 집을 수 있다 / x가 NULL이면 IF x<>70이 NULL이라 fail-open이다 / marker rows=2는 실제로 세지 않는다 상황을 정상 예로 간주하면 이 chunk의 Green 범위를 넘는다.
- 착각 방지
- 이 범위의 고정값은 100 -> 70이지만 marker·형식만으로 전체 실행을 인증하지 않는다.
- 하지 않는 일
- 현재 chunk가 직접 보장하지 않는 경계: 최종 행 선택은 id DESC라 id와 시간 순서가 어긋나면 잘못된 행을 집을 수 있다 / x가 NULL이면 IF x<>70이 NULL이라 fail-open이다 / marker rows=2는 실제로 세지 않는다
- 다음 연결
- 끝 상태를 보존한 뒤 `파일 전체 source와 책임 경계` 범위에서 다음 입력·결과를 확인한다.
반례 먼저
-
정상 fixture만 보면 충분하지 않나요?
-
빈 집합·NULL·순서 역전 같은 반례도 넣어야 해.
-
여기서는 x가 NULL이면 IF x<>70이 NULL이라 fail-open이다을 먼저 흔들어 본다.
-
첫 실패 지점을 줄 번호와 함께 기록하겠습니다.
STEP 08 / 13
실제 값 따라가기
같은 입력값이 어느 줄을 지나 어떤 결과가 되는지 추적합니다.
| 순서 | 들어온 값 | 코드가 하는 일 | 나온 값·상태 | 경계 |
|---|---|---|---|---|
| 1 | 100 -> 70 | F07의 실행 순서 1단계에 넣어 source 조건을 적용한다. | 관찰 가능한 W18 상태에 `100 -> 70`가 기록되거나 그 값으로 비교된다. | 최종 행 선택은 id DESC라 id와 시간 순서가 어긋나면 잘못된 행을 집을 수 있다 |
| 2 | ORDER BY created_at,id | F07의 실행 순서 2단계에 넣어 source 조건을 적용한다. | 관찰 가능한 W18 상태에 `ORDER BY created_at,id`가 기록되거나 그 값으로 비교된다. | x가 NULL이면 IF x<>70이 NULL이라 fail-open이다 |
| 3 | marker rows=2 final=70 | F07의 실행 순서 3단계에 넣어 source 조건을 적용한다. | 관찰 가능한 W18 상태에 `marker rows=2 final=70`가 기록되거나 그 값으로 비교된다. | marker rows=2는 실제로 세지 않는다 |
전체 다시 쓰기
-
한 줄이 너무 길어서 읽기 힘들어요.
-
화면에서는 줄바꿈해 보되 source bytes와 실행 순서는 그대로 보존해.
-
마지막에는 marker rows=2 final=70을 source와 다시 대조한다.
-
뜻→chunk→전체 코드 순서로 복원하겠습니다.
STEP 09 / 13
PowerShell·SQL·DB 내부에서 벌어지는 일
PowerShell·SQL·DB에서 실제로 일어나는 일과 증명 범위를 구분합니다.
created_at,id 순서 window sum으로 account 1의 누적잔액을 만들고 최종값 70을 검사한다.에서 1번째 내부 책임을 수행한다.
최종 행 선택은 id DESC라 id와 시간 순서가 어긋나면 잘못된 행을 집을 수 있다created_at,id 순서 window sum으로 account 1의 누적잔액을 만들고 최종값 70을 검사한다.에서 2번째 내부 책임을 수행한다.
x가 NULL이면 IF x<>70이 NULL이라 fail-open이다created_at,id 순서 window sum으로 account 1의 누적잔액을 만들고 최종값 70을 검사한다.에서 3번째 내부 책임을 수행한다.
marker rows=2는 실제로 세지 않는다created_at,id 순서 window sum으로 account 1의 누적잔액을 만들고 최종값 70을 검사한다.에서 4번째 내부 책임을 수행한다.
최종 행 선택은 id DESC라 id와 시간 순서가 어긋나면 잘못된 행을 집을 수 있다STEP 10 / 13
흔한 착각과 틀린 예
그럴듯하지만 틀린 해석을 반례로 고칩니다.
❌ 100 -> 70이 보이면 관련 실행 전체가 완전하게 증명된다.
왜 틀리나 최종 행 선택은 id DESC라 id와 시간 순서가 어긋나면 잘못된 행을 집을 수 있다
바르게 읽기 고정 fixture에서 직접 검사한 값과 owner·DB·운영 계층의 나머지 책임을 분리한다.
반례 최종 행 선택은 id DESC라 id와 시간 순서가 어긋나면 잘못된 행을 집을 수 있다 상황에서는 같은 marker·형식이 보여도 의도한 전체 의미가 성립하지 않을 수 있다.
❌ ORDER BY created_at,id이 보이면 관련 실행 전체가 완전하게 증명된다.
왜 틀리나 x가 NULL이면 IF x<>70이 NULL이라 fail-open이다
바르게 읽기 고정 fixture에서 직접 검사한 값과 owner·DB·운영 계층의 나머지 책임을 분리한다.
반례 x가 NULL이면 IF x<>70이 NULL이라 fail-open이다 상황에서는 같은 marker·형식이 보여도 의도한 전체 의미가 성립하지 않을 수 있다.
❌ marker rows=2 final=70이 보이면 관련 실행 전체가 완전하게 증명된다.
왜 틀리나 marker rows=2는 실제로 세지 않는다
바르게 읽기 고정 fixture에서 직접 검사한 값과 owner·DB·운영 계층의 나머지 책임을 분리한다.
반례 marker rows=2는 실제로 세지 않는다 상황에서는 같은 marker·형식이 보여도 의도한 전체 의미가 성립하지 않을 수 있다.
❌ 100 -> 70이 보이면 관련 실행 전체가 완전하게 증명된다.
왜 틀리나 최종 행 선택은 id DESC라 id와 시간 순서가 어긋나면 잘못된 행을 집을 수 있다
바르게 읽기 고정 fixture에서 직접 검사한 값과 owner·DB·운영 계층의 나머지 책임을 분리한다.
반례 최종 행 선택은 id DESC라 id와 시간 순서가 어긋나면 잘못된 행을 집을 수 있다 상황에서는 같은 marker·형식이 보여도 의도한 전체 의미가 성립하지 않을 수 있다.
❌ ORDER BY created_at,id이 보이면 관련 실행 전체가 완전하게 증명된다.
왜 틀리나 x가 NULL이면 IF x<>70이 NULL이라 fail-open이다
바르게 읽기 고정 fixture에서 직접 검사한 값과 owner·DB·운영 계층의 나머지 책임을 분리한다.
반례 x가 NULL이면 IF x<>70이 NULL이라 fail-open이다 상황에서는 같은 marker·형식이 보여도 의도한 전체 의미가 성립하지 않을 수 있다.
STEP 11 / 13
이 코드가 보장하지 않는 것
이 코드가 책임지지 않는 일을 분리합니다.
최종 행 선택은 id DESC라 id와 시간 순서가 어긋나면 잘못된 행을 집을 수 있다
이 책임을 맡는 곳: caller path policyx가 NULL이면 IF x<>70이 NULL이라 fail-open이다
이 책임을 맡는 곳: exact oracle gatemarker rows=2는 실제로 세지 않는다
이 책임을 맡는 곳: SQL/parser counterexample test최종 행 선택은 id DESC라 id와 시간 순서가 어긋나면 잘못된 행을 집을 수 있다
이 책임을 맡는 곳: runtime ownerx가 NULL이면 IF x<>70이 NULL이라 fail-open이다
이 책임을 맡는 곳: byte-pinned source auditSTEP 12 / 13
직접 다시 써보기
뜻 → 조각 → 전체 코드 순서로 다시 씁니다.
1단계 · 뜻부터 복원
created_at,id 순서 window sum으로 account 1의 누적잔액을 만들고 최종값 70을 검사한다.를 고정값과 미보장 경계까지 한 문장으로 말한다.
2단계 · 코드 조각 재조립
- window running balance invariant and marker
- NULL·empty·순서 반례와 Green 경계
3단계 · 파일 전체 다시 쓰기
1개 물리 줄을 원본 순서로 다시 쓰고 SHA-256 8353f5ce6de5b0e39ee265446e7fb8098db9d54849300e95db36e3bc052b5a19와 대조한다.
자가 점검
- 정본과 학습용 예시 label을 바꾸지 않는다.
- 긴 한 줄은 화면에서 감싸도 source exact text를 바꾸지 않는다.
- marker literal과 실제 결과 row를 구분한다.
- NULL·순서·empty fixture 반례를 하나 이상 말한다.
- owner mode와 cleanup 책임을 분리한다.
STEP 13 / 13
전체 원본 정답
감사로 고정한 전체 source를 가감 없이 확인합니다.
정본 전체 코드 확인하기
DO $g$ DECLARE x bigint;BEGIN SELECT running INTO x FROM(SELECT id,sum(signed_amount)OVER(PARTITION BY account_id ORDER BY created_at,id)running FROM w18_project.ledger WHERE account_id=1)s ORDER BY id DESC LIMIT 1;IF x<>70 THEN RAISE EXCEPTION 'running';END IF;END $g$; SELECT 'W18_Q4 rows=2 final=70';
0805-keyset.sql — 복합 cursor 다음 두 행
sql/w18/05-keyset.sql
정본 SQL invariant · 정본 · W18-F081줄 연결1줄 번역1 chunks
05-keyset.sql — 복합 cursor 다음 두 행
sql/w18/05-keyset.sql
정본 SQL invariant · 정본 · W18-F08STEP 01 / 13
오늘 이 코드에서 해결할 문제
무엇을 이해해야 하는지 질문부터 잡습니다.
(created_at,id) 복합 cursor보다 작은 account 1 ledger를 최신순 두 건 [2,1]로 제한한다.
- cursor=2026-01-03Z,99은 어느 줄에서 만들어지거나 검사될까?
- ids=2|1은 실제 계산값인가 고정 marker인가?
- ids가 NULL이면 IF ids<>expected가 NULL이라 fail-open이다 경계에서 첫 실패는 어디일까?
- 빈 집합·NULL·동률·순서 역전 중 어떤 반례가 중요한가?
- 이 source가 책임지지 않는 lifecycle·provenance는 무엇인가?
cursor=2026-01-03Z,99ids=2|1limit=2marker=W18_Q5STEP 02 / 13
아주 짧게: 이 코드는 왜 필요할까?
웹소설 대신 이 코드가 필요한 이유만 두 문단으로 쉽게 봅니다.
STARRY가 source를 값·순서·증명 경계가 적힌 작은 실험 카드로 나눈다.
05-keyset.sql — 복합 cursor 다음 두 행를 실행 전 검사표로 바꾸기
(created_at,id) 복합 cursor보다 작은 account 1 ledger를 최신순 두 건 [2,1]로 제한한다.
핵심 관찰값 cursor=2026-01-03Z,99, ids=2|1, limit=2, marker=W18_Q5을 따라가되, ids가 NULL이면 IF ids<>expected가 NULL이라 fail-open이다까지 함께 표시해 Green 문구를 과대해석하지 않는다.
딱 여기까지만 비유는 순서를 기억하게 할 뿐 SQL NULL, native exit, hash, cleanup의 실제 proof를 대신하지 않는다.
STEP 03 / 13
초등학생도 이해하는 설명
생활 비유와 실제 코드의 경계를 함께 확인합니다.
keyset tuple cursor invariant and marker
1~1줄을 한 덩어리로 읽어 (created_at,id) 복합 cursor보다 작은 account 1 ledger를 최신순 두 건 [2,1]로 제한한다.의 1번째 단계를 확인한다.
- 코드 연결
1~1줄- 비유
- 긴 조립 설명서에서 같은 일을 하는 부품만 한 봉투에 담아 확인한다.
- 비유의 끝
- ids가 NULL이면 IF ids<>expected가 NULL이라 fail-open이다
전체 한 줄
(created_at,id) 복합 cursor보다 작은 account 1 ledger를 최신순 두 건 [2,1]로 제한한다.
- 코드 연결
1줄- 비유
- 한 장짜리 검사표를 처음부터 끝까지 읽는다.
- 비유의 끝
- ids가 NULL이면 IF ids<>expected가 NULL이라 fail-open이다
왜 먼저 보는가
-
이 파일은 왜 필요한가요?
-
복합 cursor 다음 두 행의 출발 계약을 먼저 고정해야 해.
-
직접 보장하는 값은 cursor=2026-01-03Z,99 범위다.
-
목적과 결과를 같은 문장으로 섞지 않겠습니다.
고정값 읽기
-
숫자나 marker는 그냥 외우면 되나요?
-
아니, fixture·순서와 함께 ids=2|1을 읽어야 해.
-
고정값은 현재 source의 관찰 계약이지 모든 입력의 일반 법칙은 아니다.
-
입력→처리→결과 표로 다시 적어 볼게요.
STEP 04 / 13
비유 ↔ 코드 전체 연결표
감사 규칙상 연결 대상인 원본 1줄을 빠짐없이 연결합니다.
| 줄 | 정확한 원본 줄 | STARRY 비유 | 실제 뜻·입력·결과·한계 |
|---|---|---|---|
| 1줄F08-L01 | DO $g$ DECLARE ids bigint[ |
줄을 세울 반·번호표·창 범위를 차례로 붙인다. | 복합 cursor보다 작은 account 1 ledger를 최신순 두 건으로 제한해 id 배열 [2,1]을 검사하고 Q5 marker를 출력한다.
|
Green의 작은 범위
-
Green 문구가 보이면 전부 안전한가요?
-
그 문구가 직접 검사한 조건까지만 안전해.
-
특히 ids가 NULL이면 IF ids<>expected가 NULL이라 fail-open이다 경계는 별도 proof가 필요하다.
-
증명한 것과 안 한 것을 두 칸으로 나누겠습니다.
STEP 05 / 13
원본 코드 조각
원본을 1개 의미 조각으로 나누어 그대로 확인합니다.
파일을 한꺼번에 외우지 않고 실행 의미가 이어지는 작은 조각으로 봅니다. 아래 코드는 hash로 고정한 native 정본 source에서 그대로 잘랐습니다.
DO $g$ DECLARE ids bigint[];BEGIN SELECT array_agg(id ORDER BY created_at DESC,id DESC) INTO ids FROM(SELECT * FROM w18_project.ledger WHERE account_id=1 AND(created_at,id)<('2026-01-03Z'::timestamptz,99)ORDER BY created_at DESC,id DESC LIMIT 2)s;IF ids<>ARRAY[2::bigint,1::bigint] THEN RAISE EXCEPTION 'keyset';END IF;END $g$; SELECT 'W18_Q5 rows=2 ids=2|1';
STEP 06 / 13
코드 한 줄씩 한국어로 번역
비어 있지 않은 1줄을 모두 한국어로 옮깁니다.
비어 있지 않은 원본 줄은 하나도 생략하지 않습니다.
| 줄 | 원본 | 한국어 번역 |
|---|---|---|
| 1 | DO $g$ DECLARE ids bigint[];BEGIN SELECT array_agg(id ORDER BY created_at DESC,id DESC) INTO ids FROM(SELECT * FROM w18_project.ledger WHERE account_id=1 AND(created_at,id)<('2026-01-03Z'::timestamptz,99)ORDER BY created_at DESC,id DESC LIMIT 2)s;IF ids<>ARRAY[2::bigint,1::bigint] THEN RAISE EXCEPTION 'keyset';END IF;END $g$; SELECT 'W18_Q5 rows=2 ids=2|1'; | 복합 cursor보다 작은 account 1 ledger를 최신순 두 건으로 제한해 id 배열 [2,1]을 검사하고 Q5 marker를 출력한다. |
STEP 07 / 13
기존 수준의 한 줄 읽기·문법 해부
쉬운 설명 다음에 문법과 실행 순서를 정밀하게 읽습니다.
한 줄로 읽기(created_at,id) 복합 cursor보다 작은 account 1 ledger를 최신순 두 건 [2,1]로 제한한다. 다만 ids가 NULL이면 IF ids<>expected가 NULL이라 fail-open이다
문법 해부
- DO block의 PL/pgSQL IF와 뒤의 SELECT marker는 서로 다른 statement다.
- SQL NULL 비교는 false가 아니라 unknown이 될 수 있어 IF fail-open 여부를 확인해야 한다.
- hardcoded marker 문자열과 실제 query 결과 row를 구분한다.
실행 순서
- DO block query
- fixture-bound invariant comparison
- optional exception
- hardcoded marker SELECT
- runner substring check
원래 W6 수준의 조각별 정밀 해설
F08-C01 · keyset tuple cursor invariant and marker
- 문법 해부
- `keyset tuple cursor invariant and marker` 범위는 복합 cursor보다 작은 account 1 ledger를 최신순 두 건으로 제한해 id 배열 [2,1]을 검사하고 Q5 marker를 출력한다. 이어서 복합 cursor보다 작은 account 1 ledger를 최신순 두 건으로 제한해 id 배열 [2,1]을 검사하고 Q5 marker를 출력한다.
- 실제 값 추적
- 범위 시작 입력은 sql/w18/05-keyset.sql의 1줄 직전까지 형성된 parser·실행 상태를 입력으로 받는다. 끝에서 관찰할 상태는 cursor 조건 아래 최신 두 id가 2,1이 되어 W18_Q5 ids=2|1 marker가 출력된다.
- 정상 예
- 동결 source 1~1줄을 그대로 적용하면 cursor 조건 아래 최신 두 id가 2,1이 되어 W18_Q5 ids=2|1 marker가 출력된다.
- 틀린 예·반례
- ids가 NULL이면 IF ids<>expected가 NULL이라 fail-open이다 / marker rows=2는 실제 array 길이에서 계산하지 않는다 / tuple 비교 방향은 정렬 방향과 함께 유지해야 한다 상황을 정상 예로 간주하면 이 chunk의 Green 범위를 넘는다.
- 착각 방지
- 이 범위의 고정값은 cursor=2026-01-03Z,99이지만 marker·형식만으로 전체 실행을 인증하지 않는다.
- 하지 않는 일
- 현재 chunk가 직접 보장하지 않는 경계: ids가 NULL이면 IF ids<>expected가 NULL이라 fail-open이다 / marker rows=2는 실제 array 길이에서 계산하지 않는다 / tuple 비교 방향은 정렬 방향과 함께 유지해야 한다
- 다음 연결
- 끝 상태를 보존한 뒤 `파일 전체 source와 책임 경계` 범위에서 다음 입력·결과를 확인한다.
반례 먼저
-
정상 fixture만 보면 충분하지 않나요?
-
빈 집합·NULL·순서 역전 같은 반례도 넣어야 해.
-
여기서는 marker rows=2는 실제 array 길이에서 계산하지 않는다을 먼저 흔들어 본다.
-
첫 실패 지점을 줄 번호와 함께 기록하겠습니다.
STEP 08 / 13
실제 값 따라가기
같은 입력값이 어느 줄을 지나 어떤 결과가 되는지 추적합니다.
| 순서 | 들어온 값 | 코드가 하는 일 | 나온 값·상태 | 경계 |
|---|---|---|---|---|
| 1 | cursor=2026-01-03Z,99 | F08의 실행 순서 1단계에 넣어 source 조건을 적용한다. | 관찰 가능한 W18 상태에 `cursor=2026-01-03Z,99`가 기록되거나 그 값으로 비교된다. | ids가 NULL이면 IF ids<>expected가 NULL이라 fail-open이다 |
| 2 | ids=2|1 | F08의 실행 순서 2단계에 넣어 source 조건을 적용한다. | 관찰 가능한 W18 상태에 `ids=2|1`가 기록되거나 그 값으로 비교된다. | marker rows=2는 실제 array 길이에서 계산하지 않는다 |
| 3 | limit=2 | F08의 실행 순서 3단계에 넣어 source 조건을 적용한다. | 관찰 가능한 W18 상태에 `limit=2`가 기록되거나 그 값으로 비교된다. | tuple 비교 방향은 정렬 방향과 함께 유지해야 한다 |
| 4 | marker=W18_Q5 | F08의 실행 순서 4단계에 넣어 source 조건을 적용한다. | 관찰 가능한 W18 상태에 `marker=W18_Q5`가 기록되거나 그 값으로 비교된다. | ids가 NULL이면 IF ids<>expected가 NULL이라 fail-open이다 |
전체 다시 쓰기
-
한 줄이 너무 길어서 읽기 힘들어요.
-
화면에서는 줄바꿈해 보되 source bytes와 실행 순서는 그대로 보존해.
-
마지막에는 marker=W18_Q5을 source와 다시 대조한다.
-
뜻→chunk→전체 코드 순서로 복원하겠습니다.
STEP 09 / 13
PowerShell·SQL·DB 내부에서 벌어지는 일
PowerShell·SQL·DB에서 실제로 일어나는 일과 증명 범위를 구분합니다.
(created_at,id) 복합 cursor보다 작은 account 1 ledger를 최신순 두 건 [2,1]로 제한한다.에서 1번째 내부 책임을 수행한다.
ids가 NULL이면 IF ids<>expected가 NULL이라 fail-open이다(created_at,id) 복합 cursor보다 작은 account 1 ledger를 최신순 두 건 [2,1]로 제한한다.에서 2번째 내부 책임을 수행한다.
marker rows=2는 실제 array 길이에서 계산하지 않는다(created_at,id) 복합 cursor보다 작은 account 1 ledger를 최신순 두 건 [2,1]로 제한한다.에서 3번째 내부 책임을 수행한다.
tuple 비교 방향은 정렬 방향과 함께 유지해야 한다(created_at,id) 복합 cursor보다 작은 account 1 ledger를 최신순 두 건 [2,1]로 제한한다.에서 4번째 내부 책임을 수행한다.
ids가 NULL이면 IF ids<>expected가 NULL이라 fail-open이다STEP 10 / 13
흔한 착각과 틀린 예
그럴듯하지만 틀린 해석을 반례로 고칩니다.
❌ cursor=2026-01-03Z,99이 보이면 관련 실행 전체가 완전하게 증명된다.
왜 틀리나 ids가 NULL이면 IF ids<>expected가 NULL이라 fail-open이다
바르게 읽기 고정 fixture에서 직접 검사한 값과 owner·DB·운영 계층의 나머지 책임을 분리한다.
반례 ids가 NULL이면 IF ids<>expected가 NULL이라 fail-open이다 상황에서는 같은 marker·형식이 보여도 의도한 전체 의미가 성립하지 않을 수 있다.
❌ ids=2|1이 보이면 관련 실행 전체가 완전하게 증명된다.
왜 틀리나 marker rows=2는 실제 array 길이에서 계산하지 않는다
바르게 읽기 고정 fixture에서 직접 검사한 값과 owner·DB·운영 계층의 나머지 책임을 분리한다.
반례 marker rows=2는 실제 array 길이에서 계산하지 않는다 상황에서는 같은 marker·형식이 보여도 의도한 전체 의미가 성립하지 않을 수 있다.
❌ limit=2이 보이면 관련 실행 전체가 완전하게 증명된다.
왜 틀리나 tuple 비교 방향은 정렬 방향과 함께 유지해야 한다
바르게 읽기 고정 fixture에서 직접 검사한 값과 owner·DB·운영 계층의 나머지 책임을 분리한다.
반례 tuple 비교 방향은 정렬 방향과 함께 유지해야 한다 상황에서는 같은 marker·형식이 보여도 의도한 전체 의미가 성립하지 않을 수 있다.
❌ marker=W18_Q5이 보이면 관련 실행 전체가 완전하게 증명된다.
왜 틀리나 ids가 NULL이면 IF ids<>expected가 NULL이라 fail-open이다
바르게 읽기 고정 fixture에서 직접 검사한 값과 owner·DB·운영 계층의 나머지 책임을 분리한다.
반례 ids가 NULL이면 IF ids<>expected가 NULL이라 fail-open이다 상황에서는 같은 marker·형식이 보여도 의도한 전체 의미가 성립하지 않을 수 있다.
❌ cursor=2026-01-03Z,99이 보이면 관련 실행 전체가 완전하게 증명된다.
왜 틀리나 marker rows=2는 실제 array 길이에서 계산하지 않는다
바르게 읽기 고정 fixture에서 직접 검사한 값과 owner·DB·운영 계층의 나머지 책임을 분리한다.
반례 marker rows=2는 실제 array 길이에서 계산하지 않는다 상황에서는 같은 marker·형식이 보여도 의도한 전체 의미가 성립하지 않을 수 있다.
STEP 11 / 13
이 코드가 보장하지 않는 것
이 코드가 책임지지 않는 일을 분리합니다.
ids가 NULL이면 IF ids<>expected가 NULL이라 fail-open이다
이 책임을 맡는 곳: caller path policymarker rows=2는 실제 array 길이에서 계산하지 않는다
이 책임을 맡는 곳: exact oracle gatetuple 비교 방향은 정렬 방향과 함께 유지해야 한다
이 책임을 맡는 곳: SQL/parser counterexample testids가 NULL이면 IF ids<>expected가 NULL이라 fail-open이다
이 책임을 맡는 곳: runtime ownermarker rows=2는 실제 array 길이에서 계산하지 않는다
이 책임을 맡는 곳: byte-pinned source auditSTEP 12 / 13
직접 다시 써보기
뜻 → 조각 → 전체 코드 순서로 다시 씁니다.
1단계 · 뜻부터 복원
(created_at,id) 복합 cursor보다 작은 account 1 ledger를 최신순 두 건 [2,1]로 제한한다.를 고정값과 미보장 경계까지 한 문장으로 말한다.
2단계 · 코드 조각 재조립
- keyset tuple cursor invariant and marker
- NULL·empty·순서 반례와 Green 경계
3단계 · 파일 전체 다시 쓰기
1개 물리 줄을 원본 순서로 다시 쓰고 SHA-256 71316b719893b5af3342e0809420ee412309cddbc0c66f4f75c748c047117abc와 대조한다.
자가 점검
- 정본과 학습용 예시 label을 바꾸지 않는다.
- 긴 한 줄은 화면에서 감싸도 source exact text를 바꾸지 않는다.
- marker literal과 실제 결과 row를 구분한다.
- NULL·순서·empty fixture 반례를 하나 이상 말한다.
- owner mode와 cleanup 책임을 분리한다.
STEP 13 / 13
전체 원본 정답
감사로 고정한 전체 source를 가감 없이 확인합니다.
정본 전체 코드 확인하기
DO $g$ DECLARE ids bigint[];BEGIN SELECT array_agg(id ORDER BY created_at DESC,id DESC) INTO ids FROM(SELECT * FROM w18_project.ledger WHERE account_id=1 AND(created_at,id)<('2026-01-03Z'::timestamptz,99)ORDER BY created_at DESC,id DESC LIMIT 2)s;IF ids<>ARRAY[2::bigint,1::bigint] THEN RAISE EXCEPTION 'keyset';END IF;END $g$; SELECT 'W18_Q5 rows=2 ids=2|1';
09W18-SQL-Q27.sql — 고객별 첫·마지막 거래일 예시
illustrative/sql/W18-SQL-Q27.sql
학습용 SQL 예시 · 정본 답안 아님 · 학습용 예시 · 정본 답안 아님 · W18-F0916줄 연결16줄 번역4 chunks
W18-SQL-Q27.sql — 고객별 첫·마지막 거래일 예시
illustrative/sql/W18-SQL-Q27.sql
학습용 SQL 예시 · 정본 답안 아님 · 학습용 예시 · 정본 답안 아님 · W18-F09STEP 01 / 13
오늘 이 코드에서 해결할 문제
무엇을 이해해야 하는지 질문부터 잡습니다.
customer를 시작점으로 거래가 없는 고객까지 보존하면서 첫·마지막 business_date를 MIN/MAX로 계산하는 Q27 비정답 예시다.
- customers=6은 어느 줄에서 만들어지거나 검사될까?
- customer4/5=NULL,NULL은 실제 계산값인가 고정 marker인가?
- prompt가 status·tx_type 포함범위를 고정하지 않아 모든 row 포함 가정을 주석으로 고정했다 경계에서 첫 실패는 어디일까?
- 빈 집합·NULL·동률·순서 역전 중 어떤 반례가 중요한가?
- 이 source가 책임지지 않는 lifecycle·provenance는 무엇인가?
customers=6customer4/5=NULL,NULLOPENING includedbusiness_date usedSTEP 02 / 13
아주 짧게: 이 코드는 왜 필요할까?
웹소설 대신 이 코드가 필요한 이유만 두 문단으로 쉽게 봅니다.
STARRY가 source를 값·순서·증명 경계가 적힌 작은 실험 카드로 나눈다.
W18-SQL-Q27.sql — 고객별 첫·마지막 거래일 예시를 실행 전 검사표로 바꾸기
customer를 시작점으로 거래가 없는 고객까지 보존하면서 첫·마지막 business_date를 MIN/MAX로 계산하는 Q27 비정답 예시다.
핵심 관찰값 customers=6, customer4/5=NULL,NULL, OPENING included, business_date used을 따라가되, prompt가 status·tx_type 포함범위를 고정하지 않아 모든 row 포함 가정을 주석으로 고정했다까지 함께 표시해 Green 문구를 과대해석하지 않는다.
딱 여기까지만 비유는 순서를 기억하게 할 뿐 SQL NULL, native exit, hash, cleanup의 실제 proof를 대신하지 않는다.
STEP 03 / 13
초등학생도 이해하는 설명
생활 비유와 실제 코드의 경계를 함께 확인합니다.
provenance assumptions and schema
1~5줄을 한 덩어리로 읽어 customer를 시작점으로 거래가 없는 고객까지 보존하면서 첫·마지막 business_date를 MIN/MAX로 계산하는 Q27 비정답 예시다.의 1번째 단계를 확인한다.
- 코드 연결
1~5줄- 비유
- 긴 조립 설명서에서 같은 일을 하는 부품만 한 봉투에 담아 확인한다.
- 비유의 끝
- prompt가 status·tx_type 포함범위를 고정하지 않아 모든 row 포함 가정을 주석으로 고정했다
customer grain and MIN MAX
6~10줄을 한 덩어리로 읽어 customer를 시작점으로 거래가 없는 고객까지 보존하면서 첫·마지막 business_date를 MIN/MAX로 계산하는 Q27 비정답 예시다.의 2번째 단계를 확인한다.
- 코드 연결
6~10줄- 비유
- 긴 조립 설명서에서 같은 일을 하는 부품만 한 봉투에 담아 확인한다.
- 비유의 끝
- 시각이 아니라 영업일의 첫·마지막을 답한다
zero-row preserving joins and order
11~14줄을 한 덩어리로 읽어 customer를 시작점으로 거래가 없는 고객까지 보존하면서 첫·마지막 business_date를 MIN/MAX로 계산하는 Q27 비정답 예시다.의 3번째 단계를 확인한다.
- 코드 연결
11~14줄- 비유
- 긴 조립 설명서에서 같은 일을 하는 부품만 한 봉투에 담아 확인한다.
- 비유의 끝
- psql 변수 workbook_schema가 실행 전에 주입되어야 한다
왜 먼저 보는가
-
이 파일은 왜 필요한가요?
-
고객별 첫·마지막 거래일 예시의 출발 계약을 먼저 고정해야 해.
-
직접 보장하는 값은 customers=6 범위다.
-
목적과 결과를 같은 문장으로 섞지 않겠습니다.
고정값 읽기
-
숫자나 marker는 그냥 외우면 되나요?
-
아니, fixture·순서와 함께 customer4/5=NULL,NULL을 읽어야 해.
-
고정값은 현재 source의 관찰 계약이지 모든 입력의 일반 법칙은 아니다.
-
입력→처리→결과 표로 다시 적어 볼게요.
STEP 04 / 13
비유 ↔ 코드 전체 연결표
감사 규칙상 연결 대상인 원본 16줄을 빠짐없이 연결합니다.
| 줄 | 정확한 원본 줄 | STARRY 비유 | 실제 뜻·입력·결과·한계 |
|---|---|---|---|
| 1줄F09-L01 | -- W18-SQL-Q27 illustrative example; |
문제 봉투 겉면에 ‘가정’ 또는 ‘예상 답’ 딱지를 붙인다. | 이 SQL이 배포된 정답이 아니라 가정을 드러낸 학습용 예시임을 선언한다.
|
| 2줄F09-L02 | -- Assumption: |
문제 봉투 겉면에 ‘가정’ 또는 ‘예상 답’ 딱지를 붙인다. | prompt가 비워 둔 순서·포함 범위를 이 예시의 명시적 가정으로 고정한다.
|
| 3줄F09-L03 | -- No-transaction customers remain with NULL first/ |
문제 봉투 겉면에 ‘가정’ 또는 ‘예상 답’ 딱지를 붙인다. | 거래가 없는 고객을 남기고 날짜 둘을 NULL로 둘 정책을 선언한다.
|
| 4줄F09-L04 | SET search_path TO : |
한 단계의 안내표를 읽어 다음 상태표로 넘긴다. | psql 변수 workbook_schema를 먼저 탐색하고 그다음 public을 보도록 search_path를 설정한다.
|
| 6줄F09-L06 | SELECT |
한 단계의 안내표를 읽어 다음 상태표로 넘긴다. | 최종 customer-grain projection 절을 시작한다.
|
| 7줄F09-L07 | c. |
한 단계의 안내표를 읽어 다음 상태표로 넘긴다. | 결과 grain을 customer_id로 드러낸다.
|
| 8줄F09-L08 | MIN( |
같은 바구니의 개수·합계·양끝 값을 재서 기록한다. | customer에 연결된 모든 transaction의 최소 business_date를 첫 거래일로 계산한다.
|
| 9줄F09-L09 | MAX( |
같은 바구니의 개수·합계·양끝 값을 재서 기록한다. | customer에 연결된 모든 transaction의 최대 business_date를 마지막 거래일로 계산한다.
|
| 10줄F09-L10 | FROM customer AS c |
한 단계의 안내표를 읽어 다음 상태표로 넘긴다. | zero-transaction 고객을 보존하려고 customer에서 query를 시작한다.
|
| 11줄F09-L11 | LEFT JOIN account AS a ON a. |
명단의 사람을 지우지 않고 옆 장부 칸만 연결한다. | account가 없는 customer도 남기는 outer join으로 account를 연결한다.
|
| 12줄F09-L12 | LEFT JOIN business_tx AS t ON t. |
명단의 사람을 지우지 않고 옆 장부 칸만 연결한다. | 거래가 없는 account도 남기는 outer join으로 business_tx를 연결한다.
|
| 13줄F09-L13 | GROUP BY c. |
한 단계의 안내표를 읽어 다음 상태표로 넘긴다. | MIN/MAX 집계를 customer 한 행으로 묶는다.
|
| 14줄F09-L14 | ORDER BY c. |
줄을 세울 반·번호표·창 범위를 차례로 붙인다. | customer_id 순서로 여섯 결과 행을 안정화한다.
|
| 15줄F09-L15 | -- Oracle: |
문제 봉투 겉면에 ‘가정’ 또는 ‘예상 답’ 딱지를 붙인다. | 동결 fixture에서 대조할 예상 결과 일부를 주석으로 기록한다.
|
| 16줄F09-L16 | -- customer 3 = |
문제 봉투 겉면에 ‘가정’ 또는 ‘예상 답’ 딱지를 붙인다. | 동결 fixture에서 대조할 예상 결과 일부를 주석으로 기록한다.
|
| 17줄F09-L17 | -- customer 5 = |
문제 봉투 겉면에 ‘가정’ 또는 ‘예상 답’ 딱지를 붙인다. | 동결 fixture에서 대조할 예상 결과 일부를 주석으로 기록한다.
|
Green의 작은 범위
-
Green 문구가 보이면 전부 안전한가요?
-
그 문구가 직접 검사한 조건까지만 안전해.
-
특히 prompt가 status·tx_type 포함범위를 고정하지 않아 모든 row 포함 가정을 주석으로 고정했다 경계는 별도 proof가 필요하다.
-
증명한 것과 안 한 것을 두 칸으로 나누겠습니다.
STEP 05 / 13
원본 코드 조각
원본을 4개 의미 조각으로 나누어 그대로 확인합니다.
파일을 한꺼번에 외우지 않고 실행 의미가 이어지는 작은 조각으로 봅니다. 아래 코드는 prompt·fixture·가정을 밝힌 학습용 예시 source이며, 제공 정본 답안이 아닙니다.
-- W18-SQL-Q27 illustrative example; not a shipped workbook answer.
-- Assumption: every business_tx status and tx_type, including OPENING, counts as a transaction.
-- No-transaction customers remain with NULL first/last dates.
SET search_path TO :"workbook_schema", public;
SELECT
c.customer_id,
MIN(t.business_date) AS first_transaction_date,
MAX(t.business_date) AS last_transaction_date
FROM customer AS c
LEFT JOIN account AS a ON a.customer_id = c.customer_id
LEFT JOIN business_tx AS t ON t.account_id = a.account_id
GROUP BY c.customer_id
ORDER BY c.customer_id;
-- Oracle: customer 1 = 2026-06-30..2027-01-02; customer 2 = 2026-06-30..2027-01-02.
-- customer 3 = 2026-06-30..2026-06-30; customer 4 = NULL..NULL.
-- customer 5 = NULL..NULL; customer 6 = 2026-06-30..2026-06-30.
STEP 06 / 13
코드 한 줄씩 한국어로 번역
비어 있지 않은 16줄을 모두 한국어로 옮깁니다.
비어 있지 않은 원본 줄은 하나도 생략하지 않습니다.
| 줄 | 원본 | 한국어 번역 |
|---|---|---|
| 1 | -- W18-SQL-Q27 illustrative example; not a shipped workbook answer. | 이 SQL이 배포된 정답이 아니라 가정을 드러낸 학습용 예시임을 선언한다. |
| 2 | -- Assumption: every business_tx status and tx_type, including OPENING, counts as a transaction. | prompt가 비워 둔 순서·포함 범위를 이 예시의 명시적 가정으로 고정한다. |
| 3 | -- No-transaction customers remain with NULL first/last dates. | 거래가 없는 고객을 남기고 날짜 둘을 NULL로 둘 정책을 선언한다. |
| 4 | SET search_path TO :"workbook_schema", public; | psql 변수 workbook_schema를 먼저 탐색하고 그다음 public을 보도록 search_path를 설정한다. |
| 6 | SELECT | 최종 customer-grain projection 절을 시작한다. |
| 7 | c.customer_id, | 결과 grain을 customer_id로 드러낸다. |
| 8 | MIN(t.business_date) AS first_transaction_date, | customer에 연결된 모든 transaction의 최소 business_date를 첫 거래일로 계산한다. |
| 9 | MAX(t.business_date) AS last_transaction_date | customer에 연결된 모든 transaction의 최대 business_date를 마지막 거래일로 계산한다. |
| 10 | FROM customer AS c | zero-transaction 고객을 보존하려고 customer에서 query를 시작한다. |
| 11 | LEFT JOIN account AS a ON a.customer_id = c.customer_id | account가 없는 customer도 남기는 outer join으로 account를 연결한다. |
| 12 | LEFT JOIN business_tx AS t ON t.account_id = a.account_id | 거래가 없는 account도 남기는 outer join으로 business_tx를 연결한다. |
| 13 | GROUP BY c.customer_id | MIN/MAX 집계를 customer 한 행으로 묶는다. |
| 14 | ORDER BY c.customer_id; | customer_id 순서로 여섯 결과 행을 안정화한다. |
| 15 | -- Oracle: customer 1 = 2026-06-30..2027-01-02; customer 2 = 2026-06-30..2027-01-02. | 동결 fixture에서 대조할 예상 결과 일부를 주석으로 기록한다. |
| 16 | -- customer 3 = 2026-06-30..2026-06-30; customer 4 = NULL..NULL. | 동결 fixture에서 대조할 예상 결과 일부를 주석으로 기록한다. |
| 17 | -- customer 5 = NULL..NULL; customer 6 = 2026-06-30..2026-06-30. | 동결 fixture에서 대조할 예상 결과 일부를 주석으로 기록한다. |
STEP 07 / 13
기존 수준의 한 줄 읽기·문법 해부
쉬운 설명 다음에 문법과 실행 순서를 정밀하게 읽습니다.
한 줄로 읽기customer를 시작점으로 거래가 없는 고객까지 보존하면서 첫·마지막 business_date를 MIN/MAX로 계산하는 Q27 비정답 예시다. 다만 prompt가 status·tx_type 포함범위를 고정하지 않아 모든 row 포함 가정을 주석으로 고정했다
문법 해부
- CTE는 이름 붙인 중간 relation을 statement 안에서 연결한다.
- GROUP BY grain과 window PARTITION/ORDER/frame은 서로 다른 역할이다.
- LEFT JOIN 이후 predicate 위치가 zero-row 보존 여부를 바꾼다.
실행 순서
- search_path
- customer start
- two LEFT JOINs
- customer GROUP BY
- MIN/MAX dates
- stable display order
원래 W6 수준의 조각별 정밀 해설
F09-C01 · provenance assumptions and schema
- 문법 해부
- `provenance assumptions and schema` 범위는 이 SQL이 배포된 정답이 아니라 가정을 드러낸 학습용 예시임을 선언한다. 이어서 psql 변수 workbook_schema를 먼저 탐색하고 그다음 public을 보도록 search_path를 설정한다.
- 실제 값 추적
- 범위 시작 입력은 illustrative/sql/W18-SQL-Q27.sql의 1줄 직전까지 형성된 parser·실행 상태를 입력으로 받는다. 끝에서 관찰할 상태는 Q27 pipeline이 4줄 이후 customer-grain 날짜 계산에 필요한 상태를 얻는다.
- 정상 예
- 동결 source 1~5줄을 그대로 적용하면 Q27 pipeline이 4줄 이후 customer-grain 날짜 계산에 필요한 상태를 얻는다.
- 틀린 예·반례
- prompt가 status·tx_type 포함범위를 고정하지 않아 모든 row 포함 가정을 주석으로 고정했다 상황을 정상 예로 간주하면 이 chunk의 Green 범위를 넘는다.
- 착각 방지
- 이 범위의 고정값은 customers=6이지만 marker·형식만으로 전체 실행을 인증하지 않는다.
- 하지 않는 일
- 현재 chunk가 직접 보장하지 않는 경계: prompt가 status·tx_type 포함범위를 고정하지 않아 모든 row 포함 가정을 주석으로 고정했다
- 다음 연결
- 끝 상태를 보존한 뒤 `customer grain and MIN MAX` 범위에서 다음 입력·결과를 확인한다.
F09-C02 · customer grain and MIN MAX
- 문법 해부
- `customer grain and MIN MAX` 범위는 최종 customer-grain projection 절을 시작한다. 이어서 zero-transaction 고객을 보존하려고 customer에서 query를 시작한다.
- 실제 값 추적
- 범위 시작 입력은 illustrative/sql/W18-SQL-Q27.sql의 6줄 직전까지 형성된 parser·실행 상태를 입력으로 받는다. 끝에서 관찰할 상태는 Q27 pipeline이 10줄 이후 customer-grain 날짜 계산에 필요한 상태를 얻는다.
- 정상 예
- 동결 source 6~10줄을 그대로 적용하면 Q27 pipeline이 10줄 이후 customer-grain 날짜 계산에 필요한 상태를 얻는다.
- 틀린 예·반례
- 시각이 아니라 영업일의 첫·마지막을 답한다 상황을 정상 예로 간주하면 이 chunk의 Green 범위를 넘는다.
- 착각 방지
- 이 범위의 고정값은 customer4/5=NULL,NULL이지만 marker·형식만으로 전체 실행을 인증하지 않는다.
- 하지 않는 일
- 현재 chunk가 직접 보장하지 않는 경계: 시각이 아니라 영업일의 첫·마지막을 답한다
- 다음 연결
- 끝 상태를 보존한 뒤 `zero-row preserving joins and order` 범위에서 다음 입력·결과를 확인한다.
F09-C03 · zero-row preserving joins and order
- 문법 해부
- `zero-row preserving joins and order` 범위는 account가 없는 customer도 남기는 outer join으로 account를 연결한다. 이어서 customer_id 순서로 여섯 결과 행을 안정화한다.
- 실제 값 추적
- 범위 시작 입력은 illustrative/sql/W18-SQL-Q27.sql의 11줄 직전까지 형성된 parser·실행 상태를 입력으로 받는다. 끝에서 관찰할 상태는 결과가 customer 1부터 6까지 한눈에 비교 가능한 여섯 행으로 정렬된다.
- 정상 예
- 동결 source 11~14줄을 그대로 적용하면 결과가 customer 1부터 6까지 한눈에 비교 가능한 여섯 행으로 정렬된다.
- 틀린 예·반례
- psql 변수 workbook_schema가 실행 전에 주입되어야 한다 상황을 정상 예로 간주하면 이 chunk의 Green 범위를 넘는다.
- 착각 방지
- 이 범위의 고정값은 OPENING included이지만 marker·형식만으로 전체 실행을 인증하지 않는다.
- 하지 않는 일
- 현재 chunk가 직접 보장하지 않는 경계: psql 변수 workbook_schema가 실행 전에 주입되어야 한다
- 다음 연결
- 끝 상태를 보존한 뒤 `fixture oracle` 범위에서 다음 입력·결과를 확인한다.
F09-C04 · fixture oracle
- 문법 해부
- `fixture oracle` 범위는 동결 fixture에서 대조할 예상 결과 일부를 주석으로 기록한다. 이어서 동결 fixture에서 대조할 예상 결과 일부를 주석으로 기록한다.
- 실제 값 추적
- 범위 시작 입력은 illustrative/sql/W18-SQL-Q27.sql의 15줄 직전까지 형성된 parser·실행 상태를 입력으로 받는다. 끝에서 관찰할 상태는 독자가 실행 결과를 동결 fixture의 날짜·actor sentinel과 직접 대조할 기준을 얻는다.
- 정상 예
- 동결 source 15~17줄을 그대로 적용하면 독자가 실행 결과를 동결 fixture의 날짜·actor sentinel과 직접 대조할 기준을 얻는다.
- 틀린 예·반례
- shipped workbook 정답이 아니다 상황을 정상 예로 간주하면 이 chunk의 Green 범위를 넘는다.
- 착각 방지
- 이 범위의 고정값은 business_date used이지만 marker·형식만으로 전체 실행을 인증하지 않는다.
- 하지 않는 일
- 현재 chunk가 직접 보장하지 않는 경계: psql 변수 workbook_schema가 실행 전에 주입되어야 한다
- 다음 연결
- 끝 상태를 보존한 뒤 `파일 전체 source와 책임 경계` 범위에서 다음 입력·결과를 확인한다.
반례 먼저
-
정상 fixture만 보면 충분하지 않나요?
-
빈 집합·NULL·순서 역전 같은 반례도 넣어야 해.
-
여기서는 시각이 아니라 영업일의 첫·마지막을 답한다을 먼저 흔들어 본다.
-
첫 실패 지점을 줄 번호와 함께 기록하겠습니다.
STEP 08 / 13
실제 값 따라가기
같은 입력값이 어느 줄을 지나 어떤 결과가 되는지 추적합니다.
| 순서 | 들어온 값 | 코드가 하는 일 | 나온 값·상태 | 경계 |
|---|---|---|---|---|
| 1 | customers=6 | F09의 실행 순서 1단계에 넣어 source 조건을 적용한다. | 관찰 가능한 W18 상태에 `customers=6`가 기록되거나 그 값으로 비교된다. | prompt가 status·tx_type 포함범위를 고정하지 않아 모든 row 포함 가정을 주석으로 고정했다 |
| 2 | customer4/5=NULL,NULL | F09의 실행 순서 2단계에 넣어 source 조건을 적용한다. | 관찰 가능한 W18 상태에 `customer4/5=NULL,NULL`가 기록되거나 그 값으로 비교된다. | 시각이 아니라 영업일의 첫·마지막을 답한다 |
| 3 | OPENING included | F09의 실행 순서 3단계에 넣어 source 조건을 적용한다. | 관찰 가능한 W18 상태에 `OPENING included`가 기록되거나 그 값으로 비교된다. | psql 변수 workbook_schema가 실행 전에 주입되어야 한다 |
| 4 | business_date used | F09의 실행 순서 4단계에 넣어 source 조건을 적용한다. | 관찰 가능한 W18 상태에 `business_date used`가 기록되거나 그 값으로 비교된다. | shipped workbook 정답이 아니다 |
전체 다시 쓰기
-
한 줄이 너무 길어서 읽기 힘들어요.
-
화면에서는 줄바꿈해 보되 source bytes와 실행 순서는 그대로 보존해.
-
마지막에는 business_date used을 source와 다시 대조한다.
-
뜻→chunk→전체 코드 순서로 복원하겠습니다.
STEP 09 / 13
PowerShell·SQL·DB 내부에서 벌어지는 일
PowerShell·SQL·DB에서 실제로 일어나는 일과 증명 범위를 구분합니다.
customer를 시작점으로 거래가 없는 고객까지 보존하면서 첫·마지막 business_date를 MIN/MAX로 계산하는 Q27 비정답 예시다.에서 1번째 내부 책임을 수행한다.
prompt가 status·tx_type 포함범위를 고정하지 않아 모든 row 포함 가정을 주석으로 고정했다customer를 시작점으로 거래가 없는 고객까지 보존하면서 첫·마지막 business_date를 MIN/MAX로 계산하는 Q27 비정답 예시다.에서 2번째 내부 책임을 수행한다.
시각이 아니라 영업일의 첫·마지막을 답한다customer를 시작점으로 거래가 없는 고객까지 보존하면서 첫·마지막 business_date를 MIN/MAX로 계산하는 Q27 비정답 예시다.에서 3번째 내부 책임을 수행한다.
psql 변수 workbook_schema가 실행 전에 주입되어야 한다customer를 시작점으로 거래가 없는 고객까지 보존하면서 첫·마지막 business_date를 MIN/MAX로 계산하는 Q27 비정답 예시다.에서 4번째 내부 책임을 수행한다.
shipped workbook 정답이 아니다STEP 10 / 13
흔한 착각과 틀린 예
그럴듯하지만 틀린 해석을 반례로 고칩니다.
❌ customers=6이 보이면 관련 실행 전체가 완전하게 증명된다.
왜 틀리나 prompt가 status·tx_type 포함범위를 고정하지 않아 모든 row 포함 가정을 주석으로 고정했다
바르게 읽기 고정 fixture에서 직접 검사한 값과 owner·DB·운영 계층의 나머지 책임을 분리한다.
반례 prompt가 status·tx_type 포함범위를 고정하지 않아 모든 row 포함 가정을 주석으로 고정했다 상황에서는 같은 marker·형식이 보여도 의도한 전체 의미가 성립하지 않을 수 있다.
❌ customer4/5=NULL,NULL이 보이면 관련 실행 전체가 완전하게 증명된다.
왜 틀리나 시각이 아니라 영업일의 첫·마지막을 답한다
바르게 읽기 고정 fixture에서 직접 검사한 값과 owner·DB·운영 계층의 나머지 책임을 분리한다.
반례 시각이 아니라 영업일의 첫·마지막을 답한다 상황에서는 같은 marker·형식이 보여도 의도한 전체 의미가 성립하지 않을 수 있다.
❌ OPENING included이 보이면 관련 실행 전체가 완전하게 증명된다.
왜 틀리나 psql 변수 workbook_schema가 실행 전에 주입되어야 한다
바르게 읽기 고정 fixture에서 직접 검사한 값과 owner·DB·운영 계층의 나머지 책임을 분리한다.
반례 psql 변수 workbook_schema가 실행 전에 주입되어야 한다 상황에서는 같은 marker·형식이 보여도 의도한 전체 의미가 성립하지 않을 수 있다.
❌ business_date used이 보이면 관련 실행 전체가 완전하게 증명된다.
왜 틀리나 shipped workbook 정답이 아니다
바르게 읽기 고정 fixture에서 직접 검사한 값과 owner·DB·운영 계층의 나머지 책임을 분리한다.
반례 shipped workbook 정답이 아니다 상황에서는 같은 marker·형식이 보여도 의도한 전체 의미가 성립하지 않을 수 있다.
❌ customers=6이 보이면 관련 실행 전체가 완전하게 증명된다.
왜 틀리나 prompt가 status·tx_type 포함범위를 고정하지 않아 모든 row 포함 가정을 주석으로 고정했다
바르게 읽기 고정 fixture에서 직접 검사한 값과 owner·DB·운영 계층의 나머지 책임을 분리한다.
반례 prompt가 status·tx_type 포함범위를 고정하지 않아 모든 row 포함 가정을 주석으로 고정했다 상황에서는 같은 marker·형식이 보여도 의도한 전체 의미가 성립하지 않을 수 있다.
STEP 11 / 13
이 코드가 보장하지 않는 것
이 코드가 책임지지 않는 일을 분리합니다.
prompt가 status·tx_type 포함범위를 고정하지 않아 모든 row 포함 가정을 주석으로 고정했다
이 책임을 맡는 곳: caller path policy시각이 아니라 영업일의 첫·마지막을 답한다
이 책임을 맡는 곳: exact oracle gatepsql 변수 workbook_schema가 실행 전에 주입되어야 한다
이 책임을 맡는 곳: SQL/parser counterexample testshipped workbook 정답이 아니다
이 책임을 맡는 곳: runtime ownerprompt가 status·tx_type 포함범위를 고정하지 않아 모든 row 포함 가정을 주석으로 고정했다
이 책임을 맡는 곳: byte-pinned source auditSTEP 12 / 13
직접 다시 써보기
뜻 → 조각 → 전체 코드 순서로 다시 씁니다.
1단계 · 뜻부터 복원
customer를 시작점으로 거래가 없는 고객까지 보존하면서 첫·마지막 business_date를 MIN/MAX로 계산하는 Q27 비정답 예시다.를 고정값과 미보장 경계까지 한 문장으로 말한다.
2단계 · 코드 조각 재조립
- provenance assumptions and schema
- customer grain and MIN MAX
- zero-row preserving joins and order
- fixture oracle
3단계 · 파일 전체 다시 쓰기
17개 물리 줄을 원본 순서로 다시 쓰고 SHA-256 b4d1c9be2731e41a4c5ef8dd3b60e621878a3443ca2455f9e544958ff1fd076f와 대조한다.
자가 점검
- 정본과 학습용 예시 label을 바꾸지 않는다.
- 긴 한 줄은 화면에서 감싸도 source exact text를 바꾸지 않는다.
- marker literal과 실제 결과 row를 구분한다.
- NULL·순서·empty fixture 반례를 하나 이상 말한다.
- owner mode와 cleanup 책임을 분리한다.
STEP 13 / 13
전체 원본 정답
감사로 고정한 전체 source를 가감 없이 확인합니다.
학습용 예시 전체 확인하기 · 정본 답안 아님
-- W18-SQL-Q27 illustrative example; not a shipped workbook answer.
-- Assumption: every business_tx status and tx_type, including OPENING, counts as a transaction.
-- No-transaction customers remain with NULL first/last dates.
SET search_path TO :"workbook_schema", public;
SELECT
c.customer_id,
MIN(t.business_date) AS first_transaction_date,
MAX(t.business_date) AS last_transaction_date
FROM customer AS c
LEFT JOIN account AS a ON a.customer_id = c.customer_id
LEFT JOIN business_tx AS t ON t.account_id = a.account_id
GROUP BY c.customer_id
ORDER BY c.customer_id;
-- Oracle: customer 1 = 2026-06-30..2027-01-02; customer 2 = 2026-06-30..2027-01-02.
-- customer 3 = 2026-06-30..2026-06-30; customer 4 = NULL..NULL.
-- customer 5 = NULL..NULL; customer 6 = 2026-06-30..2026-06-30.
10W18-SQL-Q28.sql — actor별 연속 실패 island 예시
illustrative/sql/W18-SQL-Q28.sql
학습용 SQL 예시 · 정본 답안 아님 · 학습용 예시 · 정본 답안 아님 · W18-F1032줄 연결32줄 번역4 chunks
W18-SQL-Q28.sql — actor별 연속 실패 island 예시
illustrative/sql/W18-SQL-Q28.sql
학습용 SQL 예시 · 정본 답안 아님 · 학습용 예시 · 정본 답안 아님 · W18-F10STEP 01 / 13
오늘 이 코드에서 해결할 문제
무엇을 이해해야 하는지 질문부터 잡습니다.
actor별 안정 순서에서 non-FAILED 누적 횟수로 island를 나눠 FAILED 연속 3회 이상 후보를 찾는 Q28 비정답 예시다.
- actor=USER-FAIL은 어느 줄에서 만들어지거나 검사될까?
- streak=3은 실제 계산값인가 고정 marker인가?
- 연속의 순서 기준은 prompt 보강 가정이다 경계에서 첫 실패는 어디일까?
- 빈 집합·NULL·동률·순서 역전 중 어떤 반례가 중요한가?
- 이 source가 책임지지 않는 lifecycle·provenance는 무엇인가?
actor=USER-FAILstreak=3order=occurred_at,tx_id10:00..10:02+09STEP 02 / 13
아주 짧게: 이 코드는 왜 필요할까?
웹소설 대신 이 코드가 필요한 이유만 두 문단으로 쉽게 봅니다.
STARRY가 source를 값·순서·증명 경계가 적힌 작은 실험 카드로 나눈다.
W18-SQL-Q28.sql — actor별 연속 실패 island 예시를 실행 전 검사표로 바꾸기
actor별 안정 순서에서 non-FAILED 누적 횟수로 island를 나눠 FAILED 연속 3회 이상 후보를 찾는 Q28 비정답 예시다.
핵심 관찰값 actor=USER-FAIL, streak=3, order=occurred_at,tx_id, 10:00..10:02+09을 따라가되, 연속의 순서 기준은 prompt 보강 가정이다까지 함께 표시해 Green 문구를 과대해석하지 않는다.
딱 여기까지만 비유는 순서를 기억하게 할 뿐 SQL NULL, native exit, hash, cleanup의 실제 proof를 대신하지 않는다.
STEP 03 / 13
초등학생도 이해하는 설명
생활 비유와 실제 코드의 경계를 함께 확인합니다.
provenance order assumptions and schema
1~5줄을 한 덩어리로 읽어 actor별 안정 순서에서 non-FAILED 누적 횟수로 island를 나눠 FAILED 연속 3회 이상 후보를 찾는 Q28 비정답 예시다.의 1번째 단계를 확인한다.
- 코드 연결
1~5줄- 비유
- 긴 조립 설명서에서 같은 일을 하는 부품만 한 봉투에 담아 확인한다.
- 비유의 끝
- 연속의 순서 기준은 prompt 보강 가정이다
stable order and reset-group window
6~17줄을 한 덩어리로 읽어 actor별 안정 순서에서 non-FAILED 누적 횟수로 island를 나눠 FAILED 연속 3회 이상 후보를 찾는 Q28 비정답 예시다.의 2번째 단계를 확인한다.
- 코드 연결
6~17줄- 비유
- 긴 조립 설명서에서 같은 일을 하는 부품만 한 봉투에 담아 확인한다.
- 비유의 끝
- psql 변수 workbook_schema가 실행 전에 주입되어야 한다
failed islands and threshold
18~29줄을 한 덩어리로 읽어 actor별 안정 순서에서 non-FAILED 누적 횟수로 island를 나눠 FAILED 연속 3회 이상 후보를 찾는 Q28 비정답 예시다.의 3번째 단계를 확인한다.
- 코드 연결
18~29줄- 비유
- 긴 조립 설명서에서 같은 일을 하는 부품만 한 봉투에 담아 확인한다.
- 비유의 끝
- TIMESTAMPTZ stdout offset은 session TimeZone에 따라 달라져 +09 문자열 자체는 고정되지 않는다
왜 먼저 보는가
-
이 파일은 왜 필요한가요?
-
actor별 연속 실패 island 예시의 출발 계약을 먼저 고정해야 해.
-
직접 보장하는 값은 actor=USER-FAIL 범위다.
-
목적과 결과를 같은 문장으로 섞지 않겠습니다.
고정값 읽기
-
숫자나 marker는 그냥 외우면 되나요?
-
아니, fixture·순서와 함께 streak=3을 읽어야 해.
-
고정값은 현재 source의 관찰 계약이지 모든 입력의 일반 법칙은 아니다.
-
입력→처리→결과 표로 다시 적어 볼게요.
STEP 04 / 13
비유 ↔ 코드 전체 연결표
감사 규칙상 연결 대상인 원본 32줄을 빠짐없이 연결합니다.
| 줄 | 정확한 원본 줄 | STARRY 비유 | 실제 뜻·입력·결과·한계 |
|---|---|---|---|
| 1줄F10-L01 | -- W18-SQL-Q28 illustrative example; |
문제 봉투 겉면에 ‘가정’ 또는 ‘예상 답’ 딱지를 붙인다. | 이 SQL이 배포된 정답이 아니라 가정을 드러낸 학습용 예시임을 선언한다.
|
| 2줄F10-L02 | -- Assumption: |
문제 봉투 겉면에 ‘가정’ 또는 ‘예상 답’ 딱지를 붙인다. | prompt가 비워 둔 순서·포함 범위를 이 예시의 명시적 가정으로 고정한다.
|
| 3줄F10-L03 | -- Every non-FAILED row closes the preceding FAILED island. |
문제 봉투 겉면에 ‘가정’ 또는 ‘예상 답’ 딱지를 붙인다. | FAILED가 아닌 행이 앞선 실패 연속 구간을 끝낸다는 reset 규칙을 선언한다.
|
| 4줄F10-L04 | SET search_path TO : |
한 단계의 안내표를 읽어 다음 상태표로 넘긴다. | psql 변수 workbook_schema를 먼저 탐색하고 그다음 public을 보도록 search_path를 설정한다.
|
| 6줄F10-L06 | WITH ordered AS ( |
다음 설정이나 계산을 담을 새 서랍을 연다. | actor별 안정 순서와 reset group을 계산할 첫 CTE를 연다.
|
| 7줄F10-L07 | SELECT |
한 단계의 안내표를 읽어 다음 상태표로 넘긴다. | 최종 customer-grain projection 절을 시작한다.
|
| 8줄F10-L08 | actor_id, |
한 단계의 안내표를 읽어 다음 상태표로 넘긴다. | window partition과 최종 후보의 actor 식별자를 전달한다.
|
| 9줄F10-L09 | tx_id, |
한 단계의 안내표를 읽어 다음 상태표로 넘긴다. | occurred_at 동률을 깨는 stable tie-breaker UUID를 전달한다.
|
| 10줄F10-L10 | status, |
한 단계의 안내표를 읽어 다음 상태표로 넘긴다. | 실패 여부와 reset 여부를 판단할 status를 전달한다.
|
| 11줄F10-L11 | occurred_at, |
한 단계의 안내표를 읽어 다음 상태표로 넘긴다. | actor 내부 사건 순서를 정할 timestamp를 전달한다.
|
| 12줄F10-L12 | COUNT( |
같은 바구니의 개수·합계·양끝 값을 재서 기록한다. | 현재 행까지 나타난 non-FAILED 개수를 window로 누적해 실패 island 번호를 만든다.
|
| 13줄F10-L13 | PARTITION BY actor_id |
줄을 세울 반·번호표·창 범위를 차례로 붙인다. | reset group window를 actor별로 독립 계산한다.
|
| 14줄F10-L14 | ORDER BY occurred_at, |
줄을 세울 반·번호표·창 범위를 차례로 붙인다. | timestamp 다음 UUID로 actor 내부 total order를 만든다.
|
| 15줄F10-L15 | ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW |
줄을 세울 반·번호표·창 범위를 차례로 붙인다. | 현재 행까지의 물리 window frame을 명시한다.
|
| 16줄F10-L16 | ) AS reset_group |
한 단계의 안내표를 읽어 다음 상태표로 넘긴다. | 누적 non-FAILED count에 reset_group 이름을 붙인다.
|
| 17줄F10-L17 | FROM business_tx |
한 단계의 안내표를 읽어 다음 상태표로 넘긴다. | 동결 workbook의 business_tx 행을 ordered CTE 입력으로 읽는다.
|
| 18줄F10-L18 | ), |
다음 설정이나 계산을 담을 새 서랍을 연다. | ordered 결과에서 실패 island를 집계할 두 번째 CTE를 연다.
|
| 19줄F10-L19 | SELECT |
한 단계의 안내표를 읽어 다음 상태표로 넘긴다. | 최종 customer-grain projection 절을 시작한다.
|
| 20줄F10-L20 | actor_id, |
한 단계의 안내표를 읽어 다음 상태표로 넘긴다. | window partition과 최종 후보의 actor 식별자를 전달한다.
|
| 21줄F10-L21 | reset_group, |
한 단계의 안내표를 읽어 다음 상태표로 넘긴다. | 같은 actor 안에서 실패 연속 구간을 구분할 island id를 전달한다.
|
| 22줄F10-L22 | COUNT( |
같은 바구니의 개수·합계·양끝 값을 재서 기록한다. | 각 실패 island의 행 수를 streak 길이로 계산한다.
|
| 23줄F10-L23 | MIN( |
같은 바구니의 개수·합계·양끝 값을 재서 기록한다. | 실패 island의 첫 timestamp를 계산한다.
|
| 24줄F10-L24 | MAX( |
같은 바구니의 개수·합계·양끝 값을 재서 기록한다. | 실패 island의 마지막 timestamp를 계산한다.
|
| 25줄F10-L25 | FROM ordered |
한 단계의 안내표를 읽어 다음 상태표로 넘긴다. | 안정 순서와 reset group을 가진 ordered CTE를 읽는다.
|
| 26줄F10-L26 | WHERE status = |
한 단계의 안내표를 읽어 다음 상태표로 넘긴다. | FAILED 행만 island 집계 대상으로 남긴다.
|
| 27줄F10-L27 | GROUP BY actor_id, |
한 단계의 안내표를 읽어 다음 상태표로 넘긴다. | actor와 reset group마다 한 실패 island로 묶는다.
|
| 28줄F10-L28 | HAVING COUNT( |
같은 바구니의 개수·합계·양끝 값을 재서 기록한다. | 실패가 세 번 이상 연속된 island만 후보로 남긴다.
|
| 29줄F10-L29 | ) |
한 단계의 안내표를 읽어 다음 상태표로 넘긴다. | 현재 CTE 또는 window 표현식의 범위를 닫는다.
|
| 30줄F10-L30 | SELECT actor_id, |
한 단계의 안내표를 읽어 다음 상태표로 넘긴다. | 후보 actor와 streak 길이·시작·끝 시각을 최종 출력 열로 고른다.
|
| 31줄F10-L31 | FROM failed_streaks |
한 단계의 안내표를 읽어 다음 상태표로 넘긴다. | threshold를 통과한 failed_streaks CTE를 최종 입력으로 사용한다.
|
| 32줄F10-L32 | ORDER BY actor_id, |
줄을 세울 반·번호표·창 범위를 차례로 붙인다. | actor와 실패 시작 시각으로 최종 행 순서를 안정화한다.
|
| 33줄F10-L33 | -- Oracle: |
문제 봉투 겉면에 ‘가정’ 또는 ‘예상 답’ 딱지를 붙인다. | 동결 fixture에서 대조할 예상 결과 일부를 주석으로 기록한다.
|
Green의 작은 범위
-
Green 문구가 보이면 전부 안전한가요?
-
그 문구가 직접 검사한 조건까지만 안전해.
-
특히 연속의 순서 기준은 prompt 보강 가정이다 경계는 별도 proof가 필요하다.
-
증명한 것과 안 한 것을 두 칸으로 나누겠습니다.
STEP 05 / 13
원본 코드 조각
원본을 4개 의미 조각으로 나누어 그대로 확인합니다.
파일을 한꺼번에 외우지 않고 실행 의미가 이어지는 작은 조각으로 봅니다. 아래 코드는 prompt·fixture·가정을 밝힌 학습용 예시 source이며, 제공 정본 답안이 아닙니다.
-- W18-SQL-Q28 illustrative example; not a shipped workbook answer.
-- Assumption: a streak is consecutive inside actor_id order (occurred_at, tx_id).
-- Every non-FAILED row closes the preceding FAILED island.
SET search_path TO :"workbook_schema", public;
WITH ordered AS (
SELECT
actor_id,
tx_id,
status,
occurred_at,
COUNT(*) FILTER (WHERE status <> 'FAILED') OVER (
PARTITION BY actor_id
ORDER BY occurred_at, tx_id
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS reset_group
FROM business_tx
), failed_streaks AS (
SELECT
actor_id,
reset_group,
COUNT(*) AS streak_length,
MIN(occurred_at) AS first_failed_at,
MAX(occurred_at) AS last_failed_at
FROM ordered
WHERE status = 'FAILED'
GROUP BY actor_id, reset_group
HAVING COUNT(*) >= 3
)
SELECT actor_id, streak_length, first_failed_at, last_failed_at
FROM failed_streaks
ORDER BY actor_id, first_failed_at;
-- Oracle: USER-FAIL|3|2026-12-28 10:00:00+09|2026-12-28 10:02:00+09.
STEP 06 / 13
코드 한 줄씩 한국어로 번역
비어 있지 않은 32줄을 모두 한국어로 옮깁니다.
비어 있지 않은 원본 줄은 하나도 생략하지 않습니다.
| 줄 | 원본 | 한국어 번역 |
|---|---|---|
| 1 | -- W18-SQL-Q28 illustrative example; not a shipped workbook answer. | 이 SQL이 배포된 정답이 아니라 가정을 드러낸 학습용 예시임을 선언한다. |
| 2 | -- Assumption: a streak is consecutive inside actor_id order (occurred_at, tx_id). | prompt가 비워 둔 순서·포함 범위를 이 예시의 명시적 가정으로 고정한다. |
| 3 | -- Every non-FAILED row closes the preceding FAILED island. | FAILED가 아닌 행이 앞선 실패 연속 구간을 끝낸다는 reset 규칙을 선언한다. |
| 4 | SET search_path TO :"workbook_schema", public; | psql 변수 workbook_schema를 먼저 탐색하고 그다음 public을 보도록 search_path를 설정한다. |
| 6 | WITH ordered AS ( | actor별 안정 순서와 reset group을 계산할 첫 CTE를 연다. |
| 7 | SELECT | 최종 customer-grain projection 절을 시작한다. |
| 8 | actor_id, | window partition과 최종 후보의 actor 식별자를 전달한다. |
| 9 | tx_id, | occurred_at 동률을 깨는 stable tie-breaker UUID를 전달한다. |
| 10 | status, | 실패 여부와 reset 여부를 판단할 status를 전달한다. |
| 11 | occurred_at, | actor 내부 사건 순서를 정할 timestamp를 전달한다. |
| 12 | COUNT(*) FILTER (WHERE status <> 'FAILED') OVER ( | 현재 행까지 나타난 non-FAILED 개수를 window로 누적해 실패 island 번호를 만든다. |
| 13 | PARTITION BY actor_id | reset group window를 actor별로 독립 계산한다. |
| 14 | ORDER BY occurred_at, tx_id | timestamp 다음 UUID로 actor 내부 total order를 만든다. |
| 15 | ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW | 현재 행까지의 물리 window frame을 명시한다. |
| 16 | ) AS reset_group | 누적 non-FAILED count에 reset_group 이름을 붙인다. |
| 17 | FROM business_tx | 동결 workbook의 business_tx 행을 ordered CTE 입력으로 읽는다. |
| 18 | ), failed_streaks AS ( | ordered 결과에서 실패 island를 집계할 두 번째 CTE를 연다. |
| 19 | SELECT | 최종 customer-grain projection 절을 시작한다. |
| 20 | actor_id, | window partition과 최종 후보의 actor 식별자를 전달한다. |
| 21 | reset_group, | 같은 actor 안에서 실패 연속 구간을 구분할 island id를 전달한다. |
| 22 | COUNT(*) AS streak_length, | 각 실패 island의 행 수를 streak 길이로 계산한다. |
| 23 | MIN(occurred_at) AS first_failed_at, | 실패 island의 첫 timestamp를 계산한다. |
| 24 | MAX(occurred_at) AS last_failed_at | 실패 island의 마지막 timestamp를 계산한다. |
| 25 | FROM ordered | 안정 순서와 reset group을 가진 ordered CTE를 읽는다. |
| 26 | WHERE status = 'FAILED' | FAILED 행만 island 집계 대상으로 남긴다. |
| 27 | GROUP BY actor_id, reset_group | actor와 reset group마다 한 실패 island로 묶는다. |
| 28 | HAVING COUNT(*) >= 3 | 실패가 세 번 이상 연속된 island만 후보로 남긴다. |
| 29 | ) | 현재 CTE 또는 window 표현식의 범위를 닫는다. |
| 30 | SELECT actor_id, streak_length, first_failed_at, last_failed_at | 후보 actor와 streak 길이·시작·끝 시각을 최종 출력 열로 고른다. |
| 31 | FROM failed_streaks | threshold를 통과한 failed_streaks CTE를 최종 입력으로 사용한다. |
| 32 | ORDER BY actor_id, first_failed_at; | actor와 실패 시작 시각으로 최종 행 순서를 안정화한다. |
| 33 | -- Oracle: USER-FAIL|3|2026-12-28 10:00:00+09|2026-12-28 10:02:00+09. | 동결 fixture에서 대조할 예상 결과 일부를 주석으로 기록한다. |
STEP 07 / 13
기존 수준의 한 줄 읽기·문법 해부
쉬운 설명 다음에 문법과 실행 순서를 정밀하게 읽습니다.
한 줄로 읽기actor별 안정 순서에서 non-FAILED 누적 횟수로 island를 나눠 FAILED 연속 3회 이상 후보를 찾는 Q28 비정답 예시다. 다만 연속의 순서 기준은 prompt 보강 가정이다
문법 해부
- CTE는 이름 붙인 중간 relation을 statement 안에서 연결한다.
- GROUP BY grain과 window PARTITION/ORDER/frame은 서로 다른 역할이다.
- LEFT JOIN 이후 predicate 위치가 zero-row 보존 여부를 바꾼다.
실행 순서
- search_path
- actor stable order
- non-FAILED cumulative reset
- FAILED-only islands
- HAVING >=3
- candidate order
원래 W6 수준의 조각별 정밀 해설
F10-C01 · provenance order assumptions and schema
- 문법 해부
- `provenance order assumptions and schema` 범위는 이 SQL이 배포된 정답이 아니라 가정을 드러낸 학습용 예시임을 선언한다. 이어서 psql 변수 workbook_schema를 먼저 탐색하고 그다음 public을 보도록 search_path를 설정한다.
- 실제 값 추적
- 범위 시작 입력은 illustrative/sql/W18-SQL-Q28.sql의 1줄 직전까지 형성된 parser·실행 상태를 입력으로 받는다. 끝에서 관찰할 상태는 Q28 pipeline이 4줄 이후 stable-order 실패 island 계산 상태로 이동한다.
- 정상 예
- 동결 source 1~5줄을 그대로 적용하면 Q28 pipeline이 4줄 이후 stable-order 실패 island 계산 상태로 이동한다.
- 틀린 예·반례
- 연속의 순서 기준은 prompt 보강 가정이다 상황을 정상 예로 간주하면 이 chunk의 Green 범위를 넘는다.
- 착각 방지
- 이 범위의 고정값은 actor=USER-FAIL이지만 marker·형식만으로 전체 실행을 인증하지 않는다.
- 하지 않는 일
- 현재 chunk가 직접 보장하지 않는 경계: 연속의 순서 기준은 prompt 보강 가정이다
- 다음 연결
- 끝 상태를 보존한 뒤 `stable order and reset-group window` 범위에서 다음 입력·결과를 확인한다.
F10-C02 · stable order and reset-group window
- 문법 해부
- `stable order and reset-group window` 범위는 actor별 안정 순서와 reset group을 계산할 첫 CTE를 연다. 이어서 동결 workbook의 business_tx 행을 ordered CTE 입력으로 읽는다.
- 실제 값 추적
- 범위 시작 입력은 illustrative/sql/W18-SQL-Q28.sql의 6줄 직전까지 형성된 parser·실행 상태를 입력으로 받는다. 끝에서 관찰할 상태는 Q28 pipeline이 17줄 이후 stable-order 실패 island 계산 상태로 이동한다.
- 정상 예
- 동결 source 6~17줄을 그대로 적용하면 Q28 pipeline이 17줄 이후 stable-order 실패 island 계산 상태로 이동한다.
- 틀린 예·반례
- psql 변수 workbook_schema가 실행 전에 주입되어야 한다 상황을 정상 예로 간주하면 이 chunk의 Green 범위를 넘는다.
- 착각 방지
- 이 범위의 고정값은 streak=3이지만 marker·형식만으로 전체 실행을 인증하지 않는다.
- 하지 않는 일
- 현재 chunk가 직접 보장하지 않는 경계: shipped workbook 정답이 아니다
- 다음 연결
- 끝 상태를 보존한 뒤 `failed islands and threshold` 범위에서 다음 입력·결과를 확인한다.
F10-C03 · failed islands and threshold
- 문법 해부
- `failed islands and threshold` 범위는 ordered 결과에서 실패 island를 집계할 두 번째 CTE를 연다. 이어서 현재 CTE 또는 window 표현식의 범위를 닫는다.
- 실제 값 추적
- 범위 시작 입력은 illustrative/sql/W18-SQL-Q28.sql의 18줄 직전까지 형성된 parser·실행 상태를 입력으로 받는다. 끝에서 관찰할 상태는 Q28 pipeline이 29줄 이후 stable-order 실패 island 계산 상태로 이동한다.
- 정상 예
- 동결 source 18~29줄을 그대로 적용하면 Q28 pipeline이 29줄 이후 stable-order 실패 island 계산 상태로 이동한다.
- 틀린 예·반례
- TIMESTAMPTZ stdout offset은 session TimeZone에 따라 달라져 +09 문자열 자체는 고정되지 않는다 상황을 정상 예로 간주하면 이 chunk의 Green 범위를 넘는다.
- 착각 방지
- 이 범위의 고정값은 order=occurred_at,tx_id이지만 marker·형식만으로 전체 실행을 인증하지 않는다.
- 하지 않는 일
- 현재 chunk가 직접 보장하지 않는 경계: shipped workbook 정답이 아니다
- 다음 연결
- 끝 상태를 보존한 뒤 `candidate projection and oracle` 범위에서 다음 입력·결과를 확인한다.
F10-C04 · candidate projection and oracle
- 문법 해부
- `candidate projection and oracle` 범위는 후보 actor와 streak 길이·시작·끝 시각을 최종 출력 열로 고른다. 이어서 동결 fixture에서 대조할 예상 결과 일부를 주석으로 기록한다.
- 실제 값 추적
- 범위 시작 입력은 illustrative/sql/W18-SQL-Q28.sql의 30줄 직전까지 형성된 parser·실행 상태를 입력으로 받는다. 끝에서 관찰할 상태는 독자가 실행 결과를 동결 fixture의 날짜·actor sentinel과 직접 대조할 기준을 얻는다.
- 정상 예
- 동결 source 30~33줄을 그대로 적용하면 독자가 실행 결과를 동결 fixture의 날짜·actor sentinel과 직접 대조할 기준을 얻는다.
- 틀린 예·반례
- fixture contract는 USER-FAIL 실패가 3건 이상임만 검사하고 연속성은 pinned seed에서 별도 재계산해야 한다 상황을 정상 예로 간주하면 이 chunk의 Green 범위를 넘는다.
- 착각 방지
- 이 범위의 고정값은 10:00..10:02+09이지만 marker·형식만으로 전체 실행을 인증하지 않는다.
- 하지 않는 일
- 현재 chunk가 직접 보장하지 않는 경계: shipped workbook 정답이 아니다
- 다음 연결
- 끝 상태를 보존한 뒤 `파일 전체 source와 책임 경계` 범위에서 다음 입력·결과를 확인한다.
반례 먼저
-
정상 fixture만 보면 충분하지 않나요?
-
빈 집합·NULL·순서 역전 같은 반례도 넣어야 해.
-
여기서는 psql 변수 workbook_schema가 실행 전에 주입되어야 한다을 먼저 흔들어 본다.
-
첫 실패 지점을 줄 번호와 함께 기록하겠습니다.
STEP 08 / 13
실제 값 따라가기
같은 입력값이 어느 줄을 지나 어떤 결과가 되는지 추적합니다.
| 순서 | 들어온 값 | 코드가 하는 일 | 나온 값·상태 | 경계 |
|---|---|---|---|---|
| 1 | actor=USER-FAIL | F10의 실행 순서 1단계에 넣어 source 조건을 적용한다. | 관찰 가능한 W18 상태에 `actor=USER-FAIL`가 기록되거나 그 값으로 비교된다. | 연속의 순서 기준은 prompt 보강 가정이다 |
| 2 | streak=3 | F10의 실행 순서 2단계에 넣어 source 조건을 적용한다. | 관찰 가능한 W18 상태에 `streak=3`가 기록되거나 그 값으로 비교된다. | psql 변수 workbook_schema가 실행 전에 주입되어야 한다 |
| 3 | order=occurred_at,tx_id | F10의 실행 순서 3단계에 넣어 source 조건을 적용한다. | 관찰 가능한 W18 상태에 `order=occurred_at,tx_id`가 기록되거나 그 값으로 비교된다. | TIMESTAMPTZ stdout offset은 session TimeZone에 따라 달라져 +09 문자열 자체는 고정되지 않는다 |
| 4 | 10:00..10:02+09 | F10의 실행 순서 4단계에 넣어 source 조건을 적용한다. | 관찰 가능한 W18 상태에 `10:00..10:02+09`가 기록되거나 그 값으로 비교된다. | fixture contract는 USER-FAIL 실패가 3건 이상임만 검사하고 연속성은 pinned seed에서 별도 재계산해야 한다 |
전체 다시 쓰기
-
한 줄이 너무 길어서 읽기 힘들어요.
-
화면에서는 줄바꿈해 보되 source bytes와 실행 순서는 그대로 보존해.
-
마지막에는 10:00..10:02+09을 source와 다시 대조한다.
-
뜻→chunk→전체 코드 순서로 복원하겠습니다.
STEP 09 / 13
PowerShell·SQL·DB 내부에서 벌어지는 일
PowerShell·SQL·DB에서 실제로 일어나는 일과 증명 범위를 구분합니다.
actor별 안정 순서에서 non-FAILED 누적 횟수로 island를 나눠 FAILED 연속 3회 이상 후보를 찾는 Q28 비정답 예시다.에서 1번째 내부 책임을 수행한다.
연속의 순서 기준은 prompt 보강 가정이다actor별 안정 순서에서 non-FAILED 누적 횟수로 island를 나눠 FAILED 연속 3회 이상 후보를 찾는 Q28 비정답 예시다.에서 2번째 내부 책임을 수행한다.
psql 변수 workbook_schema가 실행 전에 주입되어야 한다actor별 안정 순서에서 non-FAILED 누적 횟수로 island를 나눠 FAILED 연속 3회 이상 후보를 찾는 Q28 비정답 예시다.에서 3번째 내부 책임을 수행한다.
TIMESTAMPTZ stdout offset은 session TimeZone에 따라 달라져 +09 문자열 자체는 고정되지 않는다actor별 안정 순서에서 non-FAILED 누적 횟수로 island를 나눠 FAILED 연속 3회 이상 후보를 찾는 Q28 비정답 예시다.에서 4번째 내부 책임을 수행한다.
fixture contract는 USER-FAIL 실패가 3건 이상임만 검사하고 연속성은 pinned seed에서 별도 재계산해야 한다STEP 10 / 13
흔한 착각과 틀린 예
그럴듯하지만 틀린 해석을 반례로 고칩니다.
❌ actor=USER-FAIL이 보이면 관련 실행 전체가 완전하게 증명된다.
왜 틀리나 연속의 순서 기준은 prompt 보강 가정이다
바르게 읽기 고정 fixture에서 직접 검사한 값과 owner·DB·운영 계층의 나머지 책임을 분리한다.
반례 연속의 순서 기준은 prompt 보강 가정이다 상황에서는 같은 marker·형식이 보여도 의도한 전체 의미가 성립하지 않을 수 있다.
❌ streak=3이 보이면 관련 실행 전체가 완전하게 증명된다.
왜 틀리나 psql 변수 workbook_schema가 실행 전에 주입되어야 한다
바르게 읽기 고정 fixture에서 직접 검사한 값과 owner·DB·운영 계층의 나머지 책임을 분리한다.
반례 psql 변수 workbook_schema가 실행 전에 주입되어야 한다 상황에서는 같은 marker·형식이 보여도 의도한 전체 의미가 성립하지 않을 수 있다.
❌ order=occurred_at,tx_id이 보이면 관련 실행 전체가 완전하게 증명된다.
왜 틀리나 TIMESTAMPTZ stdout offset은 session TimeZone에 따라 달라져 +09 문자열 자체는 고정되지 않는다
바르게 읽기 고정 fixture에서 직접 검사한 값과 owner·DB·운영 계층의 나머지 책임을 분리한다.
반례 TIMESTAMPTZ stdout offset은 session TimeZone에 따라 달라져 +09 문자열 자체는 고정되지 않는다 상황에서는 같은 marker·형식이 보여도 의도한 전체 의미가 성립하지 않을 수 있다.
❌ 10:00..10:02+09이 보이면 관련 실행 전체가 완전하게 증명된다.
왜 틀리나 fixture contract는 USER-FAIL 실패가 3건 이상임만 검사하고 연속성은 pinned seed에서 별도 재계산해야 한다
바르게 읽기 고정 fixture에서 직접 검사한 값과 owner·DB·운영 계층의 나머지 책임을 분리한다.
반례 fixture contract는 USER-FAIL 실패가 3건 이상임만 검사하고 연속성은 pinned seed에서 별도 재계산해야 한다 상황에서는 같은 marker·형식이 보여도 의도한 전체 의미가 성립하지 않을 수 있다.
❌ actor=USER-FAIL이 보이면 관련 실행 전체가 완전하게 증명된다.
왜 틀리나 후보 탐지는 장애 원인이나 사기를 판정하지 않는다
바르게 읽기 고정 fixture에서 직접 검사한 값과 owner·DB·운영 계층의 나머지 책임을 분리한다.
반례 후보 탐지는 장애 원인이나 사기를 판정하지 않는다 상황에서는 같은 marker·형식이 보여도 의도한 전체 의미가 성립하지 않을 수 있다.
STEP 11 / 13
이 코드가 보장하지 않는 것
이 코드가 책임지지 않는 일을 분리합니다.
연속의 순서 기준은 prompt 보강 가정이다
이 책임을 맡는 곳: caller path policypsql 변수 workbook_schema가 실행 전에 주입되어야 한다
이 책임을 맡는 곳: exact oracle gateTIMESTAMPTZ stdout offset은 session TimeZone에 따라 달라져 +09 문자열 자체는 고정되지 않는다
이 책임을 맡는 곳: SQL/parser counterexample testfixture contract는 USER-FAIL 실패가 3건 이상임만 검사하고 연속성은 pinned seed에서 별도 재계산해야 한다
이 책임을 맡는 곳: runtime owner후보 탐지는 장애 원인이나 사기를 판정하지 않는다
이 책임을 맡는 곳: byte-pinned source auditSTEP 12 / 13
직접 다시 써보기
뜻 → 조각 → 전체 코드 순서로 다시 씁니다.
1단계 · 뜻부터 복원
actor별 안정 순서에서 non-FAILED 누적 횟수로 island를 나눠 FAILED 연속 3회 이상 후보를 찾는 Q28 비정답 예시다.를 고정값과 미보장 경계까지 한 문장으로 말한다.
2단계 · 코드 조각 재조립
- provenance order assumptions and schema
- stable order and reset-group window
- failed islands and threshold
- candidate projection and oracle
3단계 · 파일 전체 다시 쓰기
33개 물리 줄을 원본 순서로 다시 쓰고 SHA-256 7b88be81fa7a9031d90307c6376ae71e3dc607379a031594553a8369098f6378와 대조한다.
자가 점검
- 정본과 학습용 예시 label을 바꾸지 않는다.
- 긴 한 줄은 화면에서 감싸도 source exact text를 바꾸지 않는다.
- marker literal과 실제 결과 row를 구분한다.
- NULL·순서·empty fixture 반례를 하나 이상 말한다.
- owner mode와 cleanup 책임을 분리한다.
STEP 13 / 13
전체 원본 정답
감사로 고정한 전체 source를 가감 없이 확인합니다.
학습용 예시 전체 확인하기 · 정본 답안 아님
-- W18-SQL-Q28 illustrative example; not a shipped workbook answer.
-- Assumption: a streak is consecutive inside actor_id order (occurred_at, tx_id).
-- Every non-FAILED row closes the preceding FAILED island.
SET search_path TO :"workbook_schema", public;
WITH ordered AS (
SELECT
actor_id,
tx_id,
status,
occurred_at,
COUNT(*) FILTER (WHERE status <> 'FAILED') OVER (
PARTITION BY actor_id
ORDER BY occurred_at, tx_id
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS reset_group
FROM business_tx
), failed_streaks AS (
SELECT
actor_id,
reset_group,
COUNT(*) AS streak_length,
MIN(occurred_at) AS first_failed_at,
MAX(occurred_at) AS last_failed_at
FROM ordered
WHERE status = 'FAILED'
GROUP BY actor_id, reset_group
HAVING COUNT(*) >= 3
)
SELECT actor_id, streak_length, first_failed_at, last_failed_at
FROM failed_streaks
ORDER BY actor_id, first_failed_at;
-- Oracle: USER-FAIL|3|2026-12-28 10:00:00+09|2026-12-28 10:02:00+09.