Repository navigation
perf(probe): PostgreSQL·MySQL 카탈로그 스키마 단위 조회와 JDBC SQLite 키 수집 이차 비용 제거 - #48
Merged
Merged
Conversation
원인: SQLite에서도 constraintsOf가 객체마다 sqlite-jdbc getPrimaryKeys·getImportedKeys를
불렀다. 번들 3.51.1.0 드라이버는 두 호출 안에서 sqlite_schema 전체를 훑고, getImportedKeys는
FK 행마다 PrimaryKeyFinder로 한 번 더 훑는다. 테이블마다 O(N) 스캔이 2+F번이라 수집이
O(N²)였다.
수정: 번들 드라이버(3.51.1.0)이면 SQLiteConstraints.kt가 드라이버 규칙을 옮겨 같은 행을
만든다. 이름 재료는 수집당 한 번 `SELECT type, name, sql FROM sqlite_schema`로 읽는다. PK는
같은 정규식(PK_NAMED·PK_UNNAMED)과 table_info 대체로, FK는 같은 foreign_key_list PRAGMA와
FK_NAMED 정규식으로 만든다. 결과문의 정렬(PKTABLE_NAME은 DB 인코딩 바이트의 BINARY 비교,
KEY_SEQ는 안정 정렬), KEY_SEQ, 컬럼 생략 참조의 대상 PK 첫 컬럼, PKTABLE_SCHEM·CAT(NULL),
ImportedKeyFinder의 기본 로캘 소문자화, 시스템 표 이름·temp 그림자 처리까지 옮겼다. 드라이버
경로의 행 읽기와 Kotlin 묶음 규칙(이름 없으면 {table}@대상으로 병합, {table}_fk_{n}, seq·name
정렬)은 공용 함수로 빼 두 경로가 같은 코드를 탄다.
결정: PK 위치를 table_xinfo pk로 만들지 않고 드라이버 규칙을 옮겼다(이유: 원칙 2 — 드라이버는
CHECK·주석 속 'PRIMARY KEY (…)'도 PK로 읽고, COLLATE 꼬리를 컬럼 이름에 남기고, 정규식이 없으면
pk 컬럼 중 마지막 하나만 쓴다. table_xinfo로는 출력 바이트가 같지 않다).
결정: 정확히 옮길 수 없는 입력만 그 테이블에서 드라이버 메타데이터로 돌린다. 공백 이름, 작은따옴표가
든 FK 테이블 이름(드라이버가 결과문에 escape 없이 넣는다), 없는 대상·PK 없는 대상의 컬럼 생략 참조,
UNION 500항 초과, DDL이 NULL인 행, SQLite 오류가 그 경우다(이유: 원칙 2 — 계획의 혼합안을
"정확히 옮길 수 없는 경우"로 좁혔다. limitation 문구까지 이전과 같다).
결정: 다른 sqlite-jdbc 버전(--driver)은 드라이버 메타데이터를 그대로 쓴다(이유: 규칙을 3.51.1.0
소스에서 옮겼다. 번들 버전을 올리면 SQLiteConstraintsTest가 실패해 규칙 재대조를 요구한다).
결정: 드라이버 경로의 FK 행 읽기를 공용 함수로 뺐다. 비표준 드라이버가 PKTABLE_NAME을 NULL로 줄
때만 limitation 문구 속 Kotlin 내부 클래스·매개변수 이름이 바뀐다(이유: 원칙 6 — 묶음 규칙을 한
벌로 둔다. 원래 문구도 컴파일러가 만든 이름이고, JDBC 규격상 PKTABLE_NAME은 NULL이 아니다).
검증:
- SQLiteConstraintsTest: 같은 연결에서 옮긴 경로와 드라이버 경로(드라이버 버전을 바꿔 강제)의
JSON·NDJSON 문서를 대조하고 불일치를 모아 보고한다. 사례는 이름 유무 PK/FK, 같은 대상 FK 2개,
다중 컬럼, 컬럼 생략 참조, 인용·대소문자·비ASCII·작은따옴표 이름, WITHOUT ROWID·STRICT, 주석
DDL, 뷰·트리거, FTS5·R*Tree, 드라이버 실패 입력, 500/501항, attached·temp 그림자, UTF-8·
UTF-16le·be 정렬(BMP 밖 글자 포함), 터키어 로캘, 저장소 fixture 3개다. Proxy 메타데이터로
getPrimaryKeys·getImportedKeys가 불리지 않고 sqlite_schema를 한 번만 읽는지 단언한다. 수정 전
코드에서 세 테스트가 모두 실패했고, 비교 함수를 String.compareTo로 바꾸는 변이도 잡는다.
- Fixtures/sqlite/named-constraints.sql + verify-fixtures.sh: 네이티브·Go·JDBC 참조 관계 패리티와
JDBC 제약 이름을 고정한다(간선 하나를 지운 그래프, 이름 규칙이 다른 그래프로 실패를 확인했다).
- 기준(aaba709)·새 JAR 문서 sha256: SQLite 15개 DB × JSON·NDJSON × v1·v2 60개, H2·PG 8개,
벤치 2,000~20,000개, FK 스키마 2,500~10,000개 모두 같다.
- 측정(benchmark-probe-sqlite.py, 기준·새 JAR을 번갈아 두 바퀴, 셀당 6회 중앙값): 20,000개 JSON
28.421 s → 2.260 s, NDJSON 27.655 s → 2.190 s. 20,000/10,000 비 3.75~3.86 → 1.60~1.72.
FK 스키마 10,000개 JSON 18.038 s → 4.490 s.
- gradle check(51개), go vet·test, verify-fixtures.sh(Docker 단계 제외), test-accuracy-sql.py,
test-probe-sqlite-benchmark.py, verify-sqlite-nullability.py 통과.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
PERFORMANCE.md "JDBC: one schema with 10,000 tables" 절에 50bcd20 전후 측정을 더한다. 기존 2026-09-22 행은 이전 JAR·엔진으로 잰 값이라 지우지 않고, 같은 표 안에서만 비교하라고 적었다. - 같은 호스트에서 기준 JAR(e35a12c27679, aaba709)과 새 JAR(06f479f17b64)을 번갈아 두 바퀴 돌린 셀당 6회 중앙값: 20,000개 JSON 28.421 s → 2.260 s, NDJSON 27.655 s → 2.190 s. 20,000/10,000 비는 3.8~3.9에서 1.6~1.7이 됐다. - FK가 많은 스키마(테이블마다 이름 있는 FK 하나와 컬럼 생략 참조 하나)는 10,000개 JSON 18.038 s → 4.490 s다. 재현할 수 있게 DDL을 문서에 적었다. - 두 JAR의 문서·그래프 바이트는 모든 크기에서 같았다. Co-Authored-By: Claude Opus 5.5 <[email protected]>
원인: postgresSchema가 객체(테이블·뷰·물화뷰·시퀀스)마다 columns·constraints·indexes· triggers 질의를 따로 보냈다. 수집은 고정 연결 하나에서 직렬로 돌아 문장 수가 객체 수의 네 배였고, 원격 DB에서는 수집 시간이 왕복 지연에 비례했다(리뷰 실측: 2 ms RTT에서 2,000테이블 8,036문장, 22.6 s). 수정: 네 질의를 스키마 조건(ns.nspname = $1)만 두고 관계 이름 열을 더해 스키마마다 한 번 읽고, 관계 이름으로(제약·인덱스는 관계 안에서 이름으로) 나눈다. ORDER BY는 관계 이름 뒤에 예전 정렬 키를 그대로 두어 관계 안의 행 순서가 같다. 행이 없는 객체(뷰의 제약, 시퀀스)는 nil이 아니라 빈 슬라이스라 JSON이 []로 남는다. applyPrimaryKeyPositions, 가시성 검사, usage 귀속 순서는 그대로이고, NDJSON(streamPostgres)도 같은 postgresSchema를 탄다. 객체가 없는 스키마에는 묻지 않는다. 결정: 컬럼 질의는 엔진과 CI cmp로 묶인 정본 sql/columns-postgres.sql에서 실행 시 파생한다 (이유: 원칙 6 — 프로브 전용 사본을 두면 두 벌이 갈라질 수 있다). 관계 이름 열 추가 두 곳, 관계 조건 제거 두 곳, 관계 이름 우선 정렬 한 곳의 치환이 각각 정확히 한 번 맞아야 한다. 아니면 질의를 보내지 않고 limitation(bundled PostgreSQL column query could not be adapted …)과 미완전 표시를 남긴다. 정본이 바뀌면 Go 테스트가 먼저 실패한다. 결정: 스키마 단위 질의가 실패하면 limitation이 테이블마다가 아니라 스키마마다 한 번 남는다 (이유: 원칙 2 — 바뀌는 것은 실패 경로뿐이다. 문구 형식 "<label> unavailable — <오류>"는 같고, 문서는 limitation을 정렬·중복 제거하므로 같은 오류가 테이블마다 났던 경우의 출력은 이전과 같다). 검증: - postgres_batch_test.go: scriptedServer로 테이블 1개와 40개 스키마의 JSON·NDJSON 질의 수가 같은지(수정 전 16·172로 실패), 섞인 행을 정확한 관계 이름으로 나누는지(목록에 없는 관계, 뷰·시퀀스의 빈 배열, PK 키 순서), 질의 실패를 스키마마다 한 번 남기는지, 정본 파생과 맞지 않는 정본 거부를 단언한다. - 기준(835e054)·새 바이너리 문서 sha256: PG16 basic·pagila·경계 사례 DB(다른 테이블의 같은 이름 FK, 컬럼 순서와 다른 PK 키 순서, 식·부분·INCLUDE 인덱스, 파티션, 뷰 INSTEAD OF 트리거, en_US 정렬 DB, 권한 제한 사용자, LIKE 충돌 이름) × JSON·NDJSON × v1·v2 × catalog-dependencies 36개가 모두 같다. - 측정(로컬 PG16.13 앞 asyncio 지연 프록시 1 ms/방향, warm-up 1회 뒤 교대 3회 중앙값): 2,000테이블 JSON 22.706 s → 0.282 s, NDJSON 23.570 s → 0.270 s, 문장 8,048 → 52. 4,000테이블 JSON 46.103 s → 0.416 s, NDJSON 45.228 s → 0.373 s, 문장 16,048 → 52. 지연 없이 4,000테이블 JSON 1.067 s → 0.253 s. - gofmt·go vet·go test, verify-collection-scope·verify-schema-lint·verify-unused· verify-catalog-dependencies·verify-fixtures.sh(Docker 단계 제외) 통과. Co-Authored-By: Claude Opus 5.5 <[email protected]>
원인: extractSchema가 객체마다 getColumns·getPrimaryKeys·getImportedKeys와 인덱스 카탈로그 SQL을 불렀다. 연결 하나에서 직렬이라 왕복이 객체 수의 네 배였고, 원격 DB에서는 수집 시간이 왕복 지연에 비례했다. 수정: pgjdbc로 읽는 PostgreSQL이면 PostgresSchemaMetadata가 스키마마다 getColumns(table= null), getPrimaryKeys·getImportedKeys(table=null), 테이블 조건을 뺀 인덱스 카탈로그 SQL을 한 번씩 부르고 정확한 스키마·테이블 이름으로 나눈다. 행 읽기(readColumnDoc·readPrimaryKeyRow· readImportedKeyRow)와 인덱스 SQL·읽기는 객체마다 묻는 경로와 같은 함수를 쓴다. 묶음은 처음 쓸 때 읽어(lazy) 시퀀스만 있는 스키마에는 묻지 않고, 인덱스는 테이블·물화뷰가 있을 때만 묻는다. 트리거·본문·가시성 검사는 이미 스키마 단위라 그대로다. 결정: 드라이버가 pgjdbc(getDriverName = "PostgreSQL JDBC Driver")일 때만 쓴다(이유: JDBC 명세는 키 조회의 table을 요구하고 H2는 null을 거부한다. pgjdbc 42.7.8 PgDatabaseMetaData 소스에서 null이면 테이블 조건만 빠지고 같은 질의·같은 정렬을 쓰는 것을 확인했다. 다른 드라이버와 방언은 측정할 수 없어 객체마다 묻는다). 버전은 가리지 않는다(이유: 드라이버 내부 규칙을 옮긴 것이 아니라 드라이버 자신의 질의를 쓴다. 회귀는 CI의 JDBC PostgreSQL 픽스처 패리티가 잡는다). 결정: 묶음 조회가 실패하면 그 오류를 객체마다 예전 문구(`schema.table: getColumns 실패 — …` 등)로 남기고, 인덱스는 예전처럼 limitation 뒤 getIndexInfo로 대체한다(이유: 원칙 2 — 객체마다 묻던 때 모든 객체가 실패한 모양과 같다). 결정: getColumns 결과를 정확한 이름으로 나눈다(이유: 계획이 허용한 결함 수정이다. 스키마· 테이블 인자가 LIKE 패턴이라 객체마다 묻던 때는 테이블 a_b가 a1b의 컬럼을, 스키마 my_app의 테이블이 myxapp의 같은 이름 테이블 컬럼을 함께 잡았다. 그런 이름 충돌이 없는 DB의 출력은 같다). 검증: - PostgresSchemaMetadataTest(H2 메타데이터 + pgjdbc 흉내 + ScriptedConnection): 테이블 1개와 40개에서 메타데이터·SQL 호출 수가 같고(수정 전 getColumns·getPrimaryKeys·getImportedKeys 각 40회, 준비 SQL 45개로 실패), 스키마 단위 경로와 객체 경로의 문서가 같으며, A_B·A1B 컬럼을 정확히 나누고, 묶음 실패를 한 번만 묻고 객체마다 예전 문구로 남긴다. 네 테스트 모두 수정 전 코드에서 실패했다. ScriptedConnection에 메타데이터 호출 기록과 메서드 대체를 더했다. - 기준(835e054, JAR 06f479f17b64)·새 JAR(b8f62ecfdfbe) 문서 sha256: PG16 basic·pagila·경계 사례 DB × JSON·NDJSON × v1·v2 × catalog-dependencies 30개가 같다. LIKE 충돌 DB 6개만 다르고, 차이는 my_app.a_b에 붙던 a1b 컬럼과 my_app.t에 붙던 myxapp.t 컬럼이 빠진 것뿐이다. - 측정(로컬 PG16.13 앞 asyncio 지연 프록시 1 ms/방향, warm-up 1회 뒤 교대 3회 중앙값, JSON): 2,000테이블 25.552 s → 0.907 s, 문장 8,024 → 28. 4,000테이블 48.392 s → 1.139 s, 문장 16,024 → 30. 서버 로그의 문장은 두 크기 모두 같은 27개이고, 4,000테이블의 2는 스키마 단위 카탈로그 질의 하나의 병렬 작업자다(max_parallel_workers_per_gather = 0이면 28). 지연 없이 4,000테이블 3.923 s → 1.038 s. - gradle clean check(55개, 새 경고 없음), test-accuracy-sql.py, verify-collection-scope· verify-schema-lint·verify-unused·verify-catalog-dependencies·verify-sqlite-nullability· verify-fixtures.sh(Docker 단계 제외) 통과. Co-Authored-By: Claude Opus 5.5 <[email protected]>
6bf3be4(Go)·07c365f(JDBC)가 컬럼·키·인덱스(Go는 트리거까지)를 스키마마다 한 번씩 읽게 바꾼 효과를 새 절로 남긴다. 저장소 벤치에는 지연 주입이 없어, 로컬 PG16.13 앞에 둔 scratch asyncio 프록시(방향마다 1 ms)로 쟀다는 것과 fixture DDL·계수 방법을 함께 적었다. - 기준(835e054)과 새 빌드를 warm-up 1회 뒤 번갈아 3회 돌린 중앙값: Go 2,000테이블 JSON 22.706 s → 0.282 s, 4,000테이블 46.103 s → 0.416 s. JDBC 2,000테이블 25.552 s → 0.907 s, 4,000테이블 48.392 s → 1.139 s. - 문장(xact_commit 증가분)은 테이블당 4개에서 스키마당 상수가 됐다(Go 52, JDBC 28·30). JDBC 4,000테이블의 2는 병렬 작업자이고 서버 로그의 문장은 두 크기 모두 같은 27개라고 적었다. - 모든 쌍의 문서 바이트가 같았고, JDBC getColumns의 LIKE 패턴 충돌 이름만 결과가 바뀐다는 점과 질의 수 회귀 테스트 명령을 적었다. Co-Authored-By: Claude Opus 5.5 <[email protected]>
원인: 07c365f는 pgjdbc getColumns 결과를 정확한 스키마·테이블 이름으로 나눈다. 그래서 이름에
`_`·`%`·`\`가 든 테이블의 문서가 바뀐다. 공통 원칙 2는 출력이 바뀌는 변경을 Unreleased의
Changed에 적으라고 하는데, RELEASE-NOTES.md에는 기록이 없었다. PERFORMANCE.md는 `_` 충돌만
설명했다.
수정:
- RELEASE-NOTES.md 맨 위에 레인 d1과 같은 `# Unreleased` 절을 만들고 `## Changed`에 두 항목을
적는다.
- JDBC(pgjdbc) PostgreSQL 컬럼 분할: `_`·`%`가 든 이름은 다른 테이블의 컬럼을 함께 잡았다.
`\`는 LIKE 이스케이프라 `back\slash`가 `backslash`의 컬럼을 잡거나, 컬럼 없이 visibility
limitation을 남겼다. 다시 수집한 스냅샷을 옛 스냅샷과 review로 비교하면 컬럼 추가·삭제가
보일 수 있다.
- Go·JDBC PostgreSQL 스키마 단위 조회의 실패 경로: 질의 하나의 실패가 스키마의 모든 객체에
미친다. Go는 limitation을 스키마에 한 번 남긴다. JDBC는 객체마다 예전 문구로 남기고,
인덱스는 getIndexInfo로 대체한다.
- PERFORMANCE.md 해당 문단에 `%`·`\` 사례를 더한다.
결정: d1이 먼저 머지되면 RELEASE-NOTES.md 맨 위에서 충돌이 난다. 이 두 항목을 d1의 `## Changed`
목록 끝으로 옮기면 된다(이유: 원칙 2가 기록을 요구한다. 두 PR이 같은 절 머리를 쓰도록 맞췄다).
검증: 로컬 PG 16.13에 my_app.a_b·a1b·t, myxapp.t, my_app."back\slash"·backslash·"pct%name"·
pctxname, "sch\ema".t2·schema.t2를 만들었다. 기준 JAR(835e054, 06f479f17b64)과 새 JAR
(b8f62ecfdfbe)로 수집했다.
- 기준: a_b에 [id, id, a1, ab], t에 [id, id, x], back\slash에 backslash의 [plain], pct%name에
[p, q]가 붙었다. my_app에 visibility limitation(uncollected columns 1)이 있었다.
- 새 JAR: 각 테이블이 자기 컬럼만 갖고, 그 limitation이 사라졌다.
- 스키마 sch\ema의 객체는 두 JAR 모두 수집하지 못한다(getTables의 스키마 패턴, 기존 동작,
후속 과제).
- Go 기준·새 바이너리는 같은 DB에서 바이트가 같고, 모든 이름을 정확히 수집했다.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
원인: PERFORMANCE.md "Remote PostgreSQL collection" 절의 시간·문장 수는 저장소 밖의 scratch
asyncio relay와 xact_commit 계수로 쟀다. Scripts/benchmark-probe-postgres.py에는 지연 주입·문장
계수·JSON 형식·JDBC 프로브가 없었다. 그래서 같은 표를 저장소만으로 다시 만들 수 없었다(리뷰 지적).
수정:
- Scripts/latency-relay.py(새 파일): 127.0.0.1의 빈 포트에서 받아 upstream으로 잇는다. 방향마다
받은 청크를 도착 시각 + d ms에 순서대로 넘기고, EOF는 반쪽 닫기로 전한다. 벤치는 LatencyRelay를
모듈로 쓰고, 다른 DB 앞에는 명령으로 띄운다. 연결 오류는 errors에 모아 결과에 남긴다.
- benchmark-probe-postgres.py:
- --latency-ms: 모든 수집을 relay 뒤에서 한다. 0이면 지금처럼 직접 붙는다.
- 실행마다 측정 DB의 pg_stat_database.xact_commit 증가분을 statements로 남긴다. 세션이 끝나고
값이 두 번 같을 때 읽는다. 읽는 psql은 postgres DB에 붙는다.
- --format json|ndjson(기본 ndjson), --warmup, --indexes(테이블마다 parent_id 인덱스),
--baseline-jar·--new-jar·--java(JDBC 쌍)를 더한다. summary에 producer별 중앙값·문장 수·문서
sha256 집합을 둔다.
- JSON 문서의 첫 줄(`{`)을 NDJSON 머리로 읽다 실패하던 document_shape를 고친다.
- JDBC는 빈 public 스키마를 싣지 않으므로 기대 모양을 종류별로 둔다. 그래프 parity도 종류별로
본다.
결정:
- 측정 cluster의 Unix socket을 끄고 psql도 127.0.0.1 scram으로 붙는다(이유: TMPDIR이 긴 곳에서는
socket 경로가 103바이트를 넘어 기동이 실패한다. 9d5fb1a의 "loopback TCP를 trust로 열지 않는다"는
그대로다). pgpass는 127.0.0.1의 모든 포트로 둔다(이유: relay 포트로 붙는 probe도 같은 비밀번호를
찾아야 한다).
- autovacuum과 max_parallel_workers_per_gather를 끈다(이유: 자동 vacuum·병렬 작업자의 트랜잭션도
xact_commit에 잡힌다. 4,000테이블 JDBC에서 30 대 28로 확인된 차이다). 기존 메모리 측정의 대상인
probe 메모리에는 영향이 없다.
- 기본값(NDJSON, 지연 없음, Go 쌍)은 예전과 같은 명령으로 돈다.
- 새 단위 테스트 Scripts/test-probe-postgres-benchmark.py는 아직 CI 목록에 없다
(.github/workflows/ci.yml은 레인 d1 소유다). PR 본문 후속 작업으로 적는다.
검증:
- test-probe-postgres-benchmark.py 10개 통과. relay가 왕복마다 지연의 두 배를 더하고(방향마다
40 ms → 80 ms 이상), 1 MiB를 바꾸지 않고, 반쪽 닫기를 전하고, 거절된 upstream을 기록한다. 계수는
세션이 끝나고 값이 멈춘 뒤에만 읽고, 멈추지 않으면 실패한다. 인자 오류는 종료 2다.
- 로컬 PG 16.13 스모크(1스키마 50테이블, --indexes --format json --latency-ms 1 --warmup 1, Go·JDBC
기준(835e054)·새 빌드): Go 248 → 52문장, JDBC 224 → 28문장. 종류마다 기준·새 문서 sha256이 같고,
relay 연결 16개에 오류는 0이다. 기본 모드(2스키마 20테이블 NDJSON)도 통과했다.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
원인: "Remote PostgreSQL collection" 절의 표는 저장소 밖 scratch relay로 잰 값이었다. 그래서 같은 표를 저장소만으로 다시 만들 수 없었다(리뷰 지적). 수정: 49b3aa2의 Scripts/benchmark-probe-postgres.py(--latency-ms 1, --indexes, --warmup 1, --repeat 3)로 같은 비교를 다시 쟀다. 표·DDL·측정 조건을 그 실행으로 바꾸고, 재현 명령과 형식·크기· 직접 접속 변형을 적는다. 지연 없는 직접 시간은 부하에 민감하므로 앞선 측정값(부하 3~7)도 함께 남긴다. 앞선 측정에서 JDBC 4,000테이블이 30문장이던 이유(병렬 작업자)와, 그래서 스크립트가 병렬 질의를 끈다는 점도 적는다. 측정(같은 호스트, load 5~17, 기준 835e054의 Go·JAR 06f479f17b64, 새 Go는 49b3aa2 probe-go를 git archive로 뽑아 빌드, 새 JAR b8f62ecfdfbe): - Go JSON: 2,000테이블 23.720 → 0.317 s, 4,000테이블 47.339 → 0.403 s. 문장 8,048·16,048 → 52. - Go NDJSON: 2,000테이블 26.035 → 0.303 s, 4,000테이블 51.498 → 0.373 s. - JDBC JSON: 2,000테이블 27.063 → 1.001 s, 4,000테이블 55.469 → 1.349 s. 문장 8,024·16,024 → 28. - 직접 접속 4,000테이블 JSON: Go 1.995 → 0.322 s, JDBC 13.168 → 1.270 s. JDBC 기준이 앞선 3.923 s보다 높은 원인은 카탈로그 통계가 아니다. 따로 띄운 PG(autovacuum·병렬 질의 켬)에서도 ANALYZE 전후 모두 약 13 s였다. - 모든 쌍의 문서 sha256이 같았고, relay 연결 오류는 0이었다. Co-Authored-By: Claude Opus 5.5 <[email protected]>
원인: 제약 조회는 table_constraints와 key_column_usage를 LEFT JOIN으로 잇는다. MariaDB 11.4에서는 이 조인의 이름 비교가 대소문자·악센트를 무시한다. 그래서 대소문자·악센트만 다른 테이블(P와 p, resume과 résumé)이나 스키마(sgcase와 SGCASE)의 같은 이름 제약(PRIMARY, 이름이 같은 UNIQUE)이 서로의 키 컬럼을 가졌다. 섞인 컬럼은 ORDER BY에서 동점이라 순서도 정해지지 않았다. 같은 이미지의 두 컨테이너에서 P의 PRIMARY가 [a, b, c]와 [c, a, b]로 나왔다. 네이티브 reader(engine/source/src/mysql.rs)는 제약마다 key_column_usage를 따로 물어 이 문제가 없다. 스키마 단위 조회로 바꾸면 이 동점 순서가 또 달라지므로, 그 전에 결과를 정해 둔다(schemagraph-probes__performance#1의 MySQL 부분). 수정: 키 컬럼 행의 스키마·테이블·제약 이름을 함께 읽고, 제약 행과 정확히 같을 때만 컬럼과 FK 대상을 더한다. 조인 조건과 정렬은 그대로 둔다. 결정: 공통 원칙 2에 따라 계열의 네이티브 reader와 같은 결과로 맞춘다. 출력이 바뀌므로 RELEASE-NOTES.md Unreleased의 Changed에 적는다(뒤의 docs 커밋). MySQL 8.4는 이 조인이 정확한 이름으로 짝지어 출력이 같다. 검증: - TestMySQLConstraintsKeepOnlyTheirOwnKeyColumns: 가짜 MariaDB가 P·p·APP.P의 키 컬럼을 P와 p의 PRIMARY 모두와 짝지어도 P는 [a], p는 [b]만 갖는다. 소유자 검사를 빼면 둘 다 [a b c]가 되어 실패한다. 테이블마다 묻는 질의와 스키마 단위 질의에 모두 답하는 응답이라 다음 커밋에서도 그대로 돈다. - 다이제스트를 고정한 mysql:8.4(8.4.11)와 mariadb:11.4(11.4.13, performance_schema 꺼짐·켬)에서 비교했다. 적용한 fixture는 저장소 basic·chinook fixture와 경계 사례(대소문자·악센트만 다른 테이블·스키마, 다중 컬럼 PK·FK, CHECK, 깨진 뷰, 함수형 인덱스, 한글 이름, 이름이 같은 function·procedure)다. 수집 범위 8개와 형식 6개(JSON·NDJSON, 문서 v1·v2, catalog-dependencies)의 48조합을 5830f3a 빌드와 견줬다. MySQL은 48조합 모두 같았다. MariaDB는 두 설정 모두 30조합이 같았다. 나머지 18조합은 변형 이름 테이블 12개의 제약 컬럼만 달랐다. 같은 MariaDB에서 네이티브 reader의 제약은 객체 25개 모두 이 빌드와 같았다. 예전 빌드와는 13개만 같았다. - gofmt -l, go vet ./..., go test ./..., CGO_ENABLED=0 go build ./... Co-Authored-By: Claude Opus 5.5 <[email protected]>
원인: mysqlSchema는 객체마다 PK 위치·컬럼·제약·인덱스·트리거를 따로 물었고(질의 5개), routine마다 파라미터를 물었다. go-sql-driver/mysql은 interpolateParams가 꺼져 있으면(기본값) 인자 있는 질의를 prepare와 execute 두 왕복으로 보낸다. 수집은 고정 연결 하나에서 직렬로 돌기 때문에, 원격 DB에서는 테이블당 약 10왕복이 RTT만큼 쌓였다. 리뷰가 지적한 schemagraph-probes__performance#1의 MySQL 부분이고, PostgreSQL(6bf3be4)과 같은 방식으로 고친다. 수정: - mysqlRelationDetails: 다섯 질의를 `table_schema = ?`로 스키마마다 한 번 읽고, 행을 정확한 테이블 이름으로 객체에 나눈다(relationGroups·nonNil 재사용). 테이블 안의 순서는 예전 ORDER BY와 같다. 객체가 없는 스키마에는 묻지 않는다. - 제약 조인에 tc.table_name을 더하고, 앞 커밋(da64c1d)의 키 컬럼 소유자 검사를 그대로 쓴다. - 함수형 인덱스 limitation은 객체 목록에 있는 테이블마다 예전 문구로 남긴다. - routine 파라미터는 specific_name 순으로 스키마마다 한 번 읽는다. 예전 질의(`specific_name = ?`)는 서버 콜레이션으로 비교했다. 그래서 대소문자만 다른 function·procedure나 이름이 같은 function·procedure가 서로의 파라미터를 동점 순서로 받았고, 네이티브 reader도 같은 질의를 쓴다. 대소문자를 무시하면 겹치는 이름은 예전처럼 routine마다 따로 묻는다. ASCII가 아니거나 공백으로 끝나는 이름이 하나라도 있으면 악센트·끝 공백 규칙을 옮길 수 없어, 그 스키마의 routine을 모두 따로 묻는다. - 실패 범위: 스키마 단위 질의 하나가 실패하면 그 부분이 스키마의 모든 객체에서 빈다. limitation은 예전 문구로 남는다. 예전에도 문구에 테이블 이름이 없어 문서에서는 하나로 합쳐졌다. 파라미터 일괄 조회가 실패하면 routine마다 예전 문구를 남기고 시그니처 없이 계속한다. 결정: - interpolateParams는 기본으로 켜지 않는다. 일괄 조회 뒤 남는 prepare는 스키마마다 상수다(MySQL에서 performance_schema가 켜져 있으면 11개, MariaDB 기본값이면 9개). 클라이언트 쪽 값 치환은 모든 바인드 값의 전달 방식을 바꾸므로 따로 검토할 일이다. 필요하면 지금도 URL에 interpolateParams=true를 주어 켤 수 있다(mysqlConnection이 이 옵션을 드라이버 옵션으로 넘기는 것을 확인했다). - routine 시그니처는 예전 바이트를 지킨다. 공통 원칙 2에 따라 네이티브 reader와 같은 출력을 유지하려는 것이다. 제약은 앞 커밋에서 네이티브에 맞췄다. 검증: - 단위 테스트 10개를 더했다. - 객체·routine이 1개일 때와 40개일 때 형식별 질의 수가 같다. 앞 커밋 코드에서는 12개와 246개였다. - 정확한 이름 분할(대소문자만 다른 테이블, 객체 목록에 없는 테이블, 이름 없는 키 컬럼, 뷰·시퀀스의 빈 배열, 대소문자만 다른 스키마·제약의 키 컬럼)을 확인한다. - 일괄 실패는 limitation을 한 번 남기고 미완전으로 표시한다. 빈 스키마에는 세부 질의를 보내지 않는다. - 파라미터는 스키마마다 한 번 읽고, 따로 묻는 이름과 실패 문구를 확인한다. 콜레이션에 민감한 이름 판정도 확인한다. - 앞 커밋의 TestMySQLConstraintsKeepOnlyTheirOwnKeyColumns도 고치지 않은 채 통과한다. - parity: mysql:8.4(8.4.11)와 mariadb:11.4(11.4.13, performance_schema 꺼짐·켬)를 썼다. 앞 커밋의 fixture에 대소문자만 다른 스키마 SGEDGE(같은 이름의 테이블·트리거·routine)를 더했다. 48조합을 da64c1d 빌드와 이 빌드로 두 바퀴씩 수집했다. 세 설정 모두 48조합의 문서가 같았고, 반복끼리도 같았다. - Scripts/verify-fixtures.sh: Docker로 MySQL·MariaDB·MSSQL을, scratch에 PostgreSQL 16을 띄우고 Oracle은 뺐다. MySQL·MariaDB 골든과 Go probe v1/v2 JSON/NDJSON 패리티가 통과했다. Oracle을 빼면 끝의 EXIT trap이 빈 컨테이너 이름으로 docker rm -f를 불러 'verify-fixtures: OK' 뒤에 종료 코드 1이 된다. 이 변경과 무관한 기존 동작이다. - 지연 relay 뒤의 시간·문장 수 측정은 뒤의 test·docs 커밋에 둔다. - gofmt -l, go vet ./..., go test ./..., CGO_ENABLED=0 go build ./... Co-Authored-By: Claude Opus 5.5 <[email protected]>
원인: Go MySQL 일괄 조회(06807ee)의 효과를 원격 RTT 조건에서 저장소만으로 다시 잴 수단이 없었다. benchmark-probe-postgres.py는 PostgreSQL 전용이다. 리뷰 지적 1의 제안(지연 프록시로 측정)과 지적 3의 재현성 요구를 MySQL에도 적용한다. 수정: - Scripts/benchmark-probe-mysql.py(새 파일) - Fixtures/accuracy/mysql-environments.json에 다이제스트로 고정한 mysql:8.4·mariadb:11.4 컨테이너를 127.0.0.1 포트로만 띄운다. 테이블마다 PK·parent_id 인덱스·앞 테이블 FK를 두고, 파라미터 둘인 procedure를 --routines개 만든다. - 기준·새 Go probe를 warm-up 뒤 번갈아 --repeat번 돌린다. 시간·peak RSS·문장 수(Questions 증가분에서 상태 읽기 몫을 뺀 값)·prepare 수(Com_stmt_prepare)·문서 sha256을 남긴다. --engine을 주면 모든 실행의 그래프가 같은지도 본다. - --latency-ms를 주면 Scripts/latency-relay.py 뒤에서 수집한다. 측정·요약 함수는 benchmark-probe-postgres.py를 모듈로 불러 같은 것을 쓴다. - 비밀번호는 실행마다 새로 만들고 docker CLI와 probe의 환경 변수로만 넘긴다. 그래서 명령줄과 프로세스 목록에 남지 않는다. 컨테이너와 익명 볼륨은 끝나면 지운다(`docker rm -f -v`). - Scripts/test-probe-mysql-benchmark.py(새 파일): Docker 없이 fixture 모양, 상태 변수 파싱과 보정, 비밀번호가 명령줄에 없는지, 범위 밖 인자가 종료 2로 끝나는지 확인한다. 결정: 새 단위 테스트는 아직 CI 목록에 없다. .github/workflows/ci.yml은 레인 d1이 고치고 있어서다 (49b3aa2와 같은 이유). PR 본문 후속 작업으로 적는다. 검증: - test-probe-mysql-benchmark.py 8개, test-probe-postgres-benchmark.py 10개 통과. - mysql 8.4.11, 2,000테이블·procedure 200개, --format json --latency-ms 1 --warmup 1 --repeat 3, 기준 da64c1d·새 06807ee 빌드: 중앙값 69.284 s → 0.344 s. 문장 10,211 → 17, prepare 10,205 → 11. 모든 실행의 문서 sha256과 그래프가 같았고 relay 연결 오류는 0이었다. 나머지 표는 뒤의 docs 커밋에 둔다. Co-Authored-By: Claude Opus 5.5 <[email protected]>
원인: 06807ee는 제약을 table_constraints LEFT JOIN key_column_usage로 스키마마다 한 번 읽었다. MariaDB 11.4는 두 information_schema 표를 인덱스 없이 블록 중첩 반복(BNL)으로 잇는다. 4,000테이블 스키마의 ANALYZE에서 r_loops는 7,999, r_filtered는 0.012%였다. 약 8,000행끼리 6,400만 번 비교한 셈이다. 이 질의 하나가 2,000테이블에서 1.05 s, 4,000테이블에서 4.06 s로 테이블 수의 제곱만큼 늘었다. 같은 행을 따로 읽는 두 질의는 4,000테이블에서 각각 0.05 s 안팎이었다. 1 ms 지연 relay 측정에서도 06807ee의 MariaDB 수집은 2,000테이블 1.169 s, 4,000테이블 3.994 s였다. MySQL 8.4는 행마다 인덱스로 찾는 계획이라 4,000테이블에서 0.15 s였다. 수정: - 제약 행과 키 컬럼 행을 스키마마다 따로 읽는다. 스키마·테이블·제약 이름이 정확히 같은 것끼리 Go에서 잇는다. 키 컬럼은 (테이블, 제약)마다 ordinal_position 순으로 모은다. - 결과는 서버 조인에 키 컬럼 소유자 검사(da64c1d)를 더한 것과 같다. 정확히 같은 이름은 콜레이션 비교로도 같고, 한 제약 안의 키 순서는 ordinal_position으로 정해지기 때문이다. - 키 컬럼 질의를 끝까지 읽지 못하면 그 스키마의 제약을 모두 비운다. 키 컬럼 일부만 가진 제약을 내지 않기 위해서다. limitation 문구는 예전과 같은 "constraints …"다. mysqlRows는 끝까지 읽었는지 돌려주고, 다른 호출은 이 값을 쓰지 않는다. 결정: 스키마당 질의가 하나 늘어 원격에서는 왕복이 둘(prepare·execute) 더 든다. 대신 서버 쪽 비용이 테이블 수에 비례한다. 공통 원칙 5의 "증가율로 복잡도 감소가 확인되는 경우"에 해당한다. 측정 표는 뒤의 docs 커밋에 둔다. 검증: - TestMySQLCatalogQueriesDoNotJoinInformationSchemaTables: 어떤 카탈로그 질의도 information_schema 표를 둘 이상 담지 않는다. 06807ee 코드에서는 실패한다. - TestMySQLSchemaBatchFailureIsReportedOncePerSchema: 제약 행 실패, 키 컬럼 행 실패, 키 컬럼 커서 실패의 세 경우 모두 limitation을 한 번 남기고, 미완전으로 표시하고, 모든 객체의 제약이 빈 배열이다. 뒤의 두 경우는 06807ee 코드에서 실패한다. - 정확한 이름 분할 테스트는 가짜 서버 응답만 새 질의 모양에 맞췄고 기대값은 그대로다. TestMySQLConstraintsKeepOnlyTheirOwnKeyColumns의 가짜 서버는 두 질의 모양에도 답하게 했다. 조인 모양 응답은 남겨 두어 앞 커밋들에서도 같은 테스트가 돈다. - parity: mysql:8.4(8.4.11)와 mariadb:11.4(11.4.13, performance_schema 꺼짐·켬)에서 da64c1d·06807ee· 이 빌드를 48조합씩 두 바퀴 수집했다. 세 설정 모두 세 빌드의 문서가 같았다. - gofmt -l, go vet ./..., go test ./..., CGO_ENABLED=0 go build ./... Co-Authored-By: Claude Opus 5.5 <[email protected]>
원인: 리뷰 지적 1에 따르면 Go MySQL N+1이 남아 있었고, 미적용 기록도 측정도 없었다. da64c1d·06807ee· 92af86d로 고쳤으니 기록이 둘 필요하다. 하나는 공통 원칙 5에 따른 측정이고, 다른 하나는 원칙 2에 따른 출력 변경 기록이다. 수정: - PERFORMANCE.md의 PostgreSQL 절 뒤에 "Remote MySQL and MariaDB collection (2026-10-20)" 절을 더한다. - 바뀐 질의, 제약을 두 질의로 읽어 probe에서 잇는 이유(MariaDB BNL), routine 파라미터를 따로 묻는 경우를 적는다. - 측정 조건(고정 이미지, Colima, fixture DDL, 지연 relay, Questions·Com_stmt_prepare 계수), 결과 표, 직접 접속 시간, 남는 prepare 수와 interpolateParams 결정, 재현 명령을 적는다. - RELEASE-NOTES.md Unreleased의 Changed에 두 항목을 더한다. 하나는 MariaDB 변형 이름 테이블의 제약 키 컬럼(da64c1d)이고, 하나는 스키마 단위 조회와 실패 범위(06807ee·92af86d)다. 측정: Scripts/benchmark-probe-mysql.py, 기준 da64c1d·새 92af86d, warm-up 1 + 교대 3회 중앙값, 2,000테이블에 procedure 200개, 4,000테이블에 400개, 1 ms relay. - MySQL 8.4.11 - JSON 2,000: 63.184 → 0.269 s, 문장 10,211 → 18, prepare 10,205 → 12. - NDJSON 2,000: 65.013 → 1.757 s. - JSON 4,000: 153.222 → 0.476 s, 문장 20,411 → 18. - 직접 접속 4,000: 15.314 → 0.383 s. - MariaDB 11.4.13 - JSON 2,000: 113.698 → 0.197 s, 문장 10,208 → 15, prepare 10,203 → 10. - NDJSON 2,000: 118.590 → 0.186 s. - JSON 4,000: 316.888 → 0.390 s, 문장 20,408 → 15. - 직접 접속 4,000: 204.666 → 0.309 s. - 모든 쌍의 문서 sha256이 같았고, relay 연결 오류는 0이었다. 측정 동안 VM에 다른 컨테이너는 없었다. - 06807ee(제약을 서버에서 조인)는 MariaDB 2,000·4,000테이블에서 1.169·3.994 s였다. - MySQL NDJSON 행이 느린 이유는 이번 변경과 무관하다. 일부 컨테이너에서 옵티마이저가 기존 사용 통계 질의 sys.schema_unused_indexes를 해시 조인 대신 중첩 반복으로 실행해, 이 질의 하나가 2,000테이블에서 0.01 s 대신 1.56 s 걸렸다. 다섯 컨테이너 가운데 셋에서 계획까지 확인했다. 다른 다섯 가운데 둘은 컨테이너 안에서 돌린 probe도 같은 만큼 느렸다. 같은 컨테이너에서 06807ee와 92af86d를 번갈아 재면 relay 0.28·0.275 s, 직접 1.71·1.71 s로 같았다. 이 질의를 고치는 것은 범위 밖이라 후속 과제로 둔다. 정정: 06807ee 본문의 "단위 테스트 10개"는 9개(테스트 함수 기준)다. 커밋은 고치지 않았다. Co-Authored-By: Claude Opus 5.5 <[email protected]>
원인: 49b3aa2가 Scripts/benchmark-probe-postgres.py의 start_postgres를
(pg_bin, report) 두 인자와 비밀번호를 더한 다섯 값 반환으로 바꾸고, Unix
socket을 끄고(-k ''), PGPASSFILE 줄을 127.0.0.1:*(모든 포트)로 바꿨다. CI
"Verify database startup failure paths" 단계의
Scripts/test-postgres-fixture-auth.py는 예전 호출(start_postgres(bin, root,
root))과 네 값 반환을 써서 TypeError로 실패한다. origin/main에서는 같은
테스트가 통과한다. 이 테스트는 모든 임시 PG 하네스에 "TCP 클라이언트는 이
cluster 포트의 비밀번호만 담은 0600 PGPASSFILE을 받는다"를 요구한다. 포트를
*로 두면 같은 호스트의 다른 포트에서 받는 서버에도 비밀번호를 내준다.
수정:
- 벤치는 start_postgres에서 서버 포트 줄(*:{port}:*:*:{password})만 쓴다.
--latency-ms로 relay가 열리면 allow_relay_port가 relay 포트 줄을 더한다.
- 테스트는 새 인자와 반환값을 쓴다. 빈 -k(socket을 열지 않음)는 비공개
socket 디렉터리와 같게 받고, -k가 없거나 /tmp면 여전히 실패한다. 측정 DB를
만드는 psql이 TCP로 같은 비밀번호 파일만 받는지와 relay 포트 줄 추가도 본다.
검증:
- python3 Scripts/test-postgres-fixture-auth.py: 6개 통과(수정 전 1개 오류).
벤치를 127.0.0.1:* 줄, -k 없음, -k /tmp로 바꾸면 각각 실패한다.
- python3 Scripts/test-probe-postgres-benchmark.py: 10개 통과.
- benchmark-probe-postgres.py를 1스키마 50테이블, Go·JDBC 쌍, --latency-ms
1과 0으로 끝까지 돌렸다. relay 연결 16·오류 0, 문장 수 Go 52·JDBC 28,
producer마다 문서 해시가 하나였다.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
원인: 378ebe9에서 쓴 Unreleased 항목 두 개가 후속 수정 뒤의 Go MySQL 동작과 어긋났다. - 제약 항목은 "The probe joins table_constraints with key_column_usage"라고 현재형으로 적었다. 92af86d 뒤로 probe는 두 표를 따로 읽고 Go에서 정확한 이름으로 잇는다. 서버 조인은 고친 결함의 원인이므로 과거형이 맞다. - 일괄 조회 항목은 routine 파라미터도 예외 없이 스키마당 한 번 읽는다고 적었다. 06807ee는 서버의 대소문자·악센트 무시 비교로 다른 이름과 겹칠 수 있는 routine 이름은 예전처럼 routine마다 묻고, 출력 가능한 ASCII가 아니거나 공백으로 끝나는 routine 이름이 있는 스키마는 모든 routine을 따로 묻는다 (mysqlCollationSensitiveNames). PERFORMANCE.md는 이미 이렇게 적는다. 수정: 제약 항목의 두 동사를 과거형으로 바꾸고, 일괄 조회 항목에 routine 파라미터 예외 한 문장을 더했다. 다른 항목과 출력 계약 설명은 그대로다. 검증: probe-go/mysql.go의 mysqlSchemaConstraints, mysqlSchemaKeyColumns, mysqlParameterBatch.signature, mysqlCollationSensitiveNames와 PERFORMANCE.md "Remote MySQL and MariaDB collection" 절을 대조했다. RELEASE-NOTES.md를 파싱하는 스크립트나 테스트는 없다(engine/cli/tests/review_policy.rs는 주석에서만 언급한다). Co-Authored-By: Claude Opus 5.5 <[email protected]>
원인: 92af86d는 제약 행(table_constraints)과 키 컬럼 행(key_column_usage)을 따로 읽어 Go에서 잇는다. 한 테이블에 이름이 같은 제약이 둘 있으면(같은 이름의 UNIQUE와 FK, MySQL은 UNIQUE와 CHECK도) 예전 probe는 서버 조인 결과를 이름으로 한 제약에 합쳤고, 합친 제약의 종류(첫 행)와 컬럼 순서는 서버 조인의 순서를 따랐다. 따로 읽어 이으면 그 순서가 달라진다. 실제 서버에서 확인했다. - MySQL 8.4.11: c.dup(UNIQUE+FK, 두 컬럼)의 컬럼이 [a,a,a,a,b,b,b,b]에서 [a,a,b,b,a,a,b,b]로, k.chk_dup(UNIQUE+CHECK)이 [a,a,b,b]에서 [a,b,a,b]로 바뀌었다. - MariaDB 11.4.13: c.dup의 종류가 fk에서 unique로 바뀌어, 엔진 그래프에서 c→p 참조 간선이 사라졌다. 결정 문서가 요구하는 "그 밖의 성공 수집 출력은 바이트 단위로 같다"와 어긋나는 의도하지 않은 출력 변경이다. 수정: 스키마 단위 제약 행에서 같은 (테이블, 제약 이름)이 두 번 이상 나온 테이블만 mysqlSharedNameConstraints가 예전 테이블별 서버 조인(da64c1d와 같은 질의와 정확한 이름 소유자 검사)으로 다시 읽는다. 그런 테이블은 드물어 테이블마다 질의가 하나 는다. 끝까지 읽지 못하면 그 테이블의 제약을 비운다(limitation은 남는다). 나머지 테이블은 그대로 스키마 단위로 읽는다. 검증: - TestMySQLConstraintsWithSharedNamesKeepTheServerJoinOrder: MariaDB 순서를 흉내 낸 가짜 서버에서 c.dup이 fk·[a a a a b b b b]이고, 조인은 c에만 한 번 간다. 수정 전 코드에서는 unique·[a a b b a a b b]로 실패했다. - 다이제스트를 고정한 mysql:8.4(8.4.11)와 mariadb:11.4(11.4.13)에 앞선 MySQL 경계 사례 fixture와 같은 이름 제약 사례를 함께 적용해 48조합(수집 범위 8 × 형식 6)을 두 바퀴씩 수집했다. 수정 전 HEAD는 da64c1d와 MySQL 18조합, MariaDB 12조합이 달랐다. 수정 뒤에는 MySQL에서 main(aaba709)·da64c1d·수정본, MariaDB에서 da64c1d·수정본의 문서가 모두 같았고 반복끼리도 같았다. - gofmt -l, go vet ./..., go test ./... 이 결함은 GLM 리뷰 지적이 아니다. GLM 지적을 검증하던 중 따로 확인한 것이다. Co-Authored-By: Claude Opus 5.5 <[email protected]>
원인: 앞 커밋(7199e00)부터 이름이 같은 제약 행이 둘 이상인 테이블의 제약은 예전 테이블별 서버 조인으로 읽는다. PERFORMANCE.md "Remote MySQL and MariaDB collection" 절과 RELEASE-NOTES.md Unreleased의 Go MySQL 일괄 조회 항목은 제약을 예외 없이 스키마마다 한 번 읽는다고 적고 있었다. 수정: 두 문서에 그 예외를 한 문장씩 더했다. 측정 표의 문장 수(MySQL 18, MariaDB 15)는 벤치 fixture에 그런 테이블이 없어 그대로다. 출력 계약 설명은 바꾸지 않았다. 이 수정으로 그런 테이블의 출력이 예전과 같아졌기 때문이다. 검증: probe-go/mysql.go의 mysqlSchemaConstraints·mysqlSharedNameConstraints와 대조했다. Co-Authored-By: Claude Opus 5.5 <[email protected]>
원인: 7199e00의 mysqlSharedNameConstraints도 mysqlRows가 돌려주는 "끝까지 읽었는지" 값을 보고, 일부만 읽었으면 그 테이블의 제약을 비운다. mysqlRows 주석은 이 값을 보는 호출이 mysqlSchemaKeyColumns뿐이라고 적고 있었다. 수정: 주석에 두 호출과, 둘이 이 값을 보는 이유(일부만 읽은 키 컬럼으로 제약을 내지 않는다)를 적었다. 코드는 바꾸지 않았다. 검증: gofmt -l, go vet ./..., go test ./... Co-Authored-By: Claude Opus 5.5 <[email protected]>
원인: GLM 리뷰 지적(패킷 2/2, medium). mysqlSchemaConstraints는 제약 행
질의(table_constraints)의 mysqlRows 반환값을 버리고 그때까지 만든 제약을 그대로
돌려줬다. 그래서 커서가 도중에 실패하면(스캔 오류·rows.Err) 일부 제약만 남았다.
결정 문서의 실패 경로(3)와 RELEASE-NOTES.md Unreleased("A failed schema-wide
catalog query now leaves that part empty for every object in the schema")는 그
부분을 스키마의 모든 객체에서 비운다고 적는다. 질의 자체가 실패할 때만 빈 결과가
됐다. 같은 계약이 걸린 나머지 스키마 단위 세부 조회(PK 위치, 컬럼, 인덱스, 트리거)도
같은 상태였다. 키 컬럼 조회만 이미 비웠다.
수정: 다섯 조회(PK 위치·컬럼·제약 행·인덱스와 함수형 키 수·트리거)가 mysqlRows의
"끝까지 읽음" 값을 보고, false면 그때까지 읽은 행을 버리고 빈 결과를 돌려준다. 제약
행을 끝까지 읽지 못하면 이름이 같은 제약의 테이블별 재조회도 하지 않는다. limitation
문구·개수와 미완전 표시는 그대로이고, 성공 경로는 바뀌지 않는다. mysqlRows 주석에 이
값을 보는 호출과 이유를 적었다.
검증:
- TestMySQLSchemaBatchFailureIsReportedOncePerSchema에 "constraint cursor" 사례를
더했다(GLM 제안). 수정 전 코드에서는 Orders의 PRIMARY가 남아 실패했다.
- TestMySQLSchemaBatchCursorFailureLeavesThatPartEmpty: 컬럼·PK 위치·인덱스·트리거
조회마다 커서 실패를 넣어, 그 부분이 모든 객체에서 비고, limitation이 한 번
남고, 미완전으로 표시되고, 다른 조회는 그대로 채워지는지 본다. 인덱스가 실패하면
함수형 키 limitation도 남지 않는다. 수정 전 코드에서 네 사례 모두 실패했다.
- gofmt -l, go vet ./..., go test ./...
Co-Authored-By: Claude Opus 5.5 <[email protected]>
원인: 앞 커밋(76fce0c)이 고친 GLM 지적(패킷 2/2)과 같은 결함이 Go PostgreSQL에도 있었다. pgRows는 끝까지 읽었는지 돌려주지 않았고, 컬럼·제약·인덱스·트리거의 스키마 단위 조회는 커서가 도중에 실패해도(스캔 오류·rows.Err) 그때까지 읽은 행을 그대로 돌려줬다. 제약·인덱스는 다중 키의 앞 컬럼만 가진 채 남을 수도 있었다. 결정 문서의 실패 경로(3)와 RELEASE-NOTES.md Unreleased("The Go probe leaves that part empty and records its usual limitation once")는 Go PostgreSQL도 그 부분을 비운다고 적는다. GLM 지적은 MySQL 함수 하나를 짚었지만, 같은 계약을 적은 PostgreSQL 경로도 함께 맞춘다. 수정: pgRows가 mysqlRows처럼 끝까지 읽었으면 true를 돌려준다. 네 스키마 단위 세부 조회는 false면 빈 결과를 돌려준다. 객체 목록·뷰 본문·가시성·routine 같은 나머지 호출은 이 값을 보지 않아 동작이 같다. limitation 문구·개수와 미완전 표시, 성공 경로는 바뀌지 않는다. 검증: - TestPostgresSchemaBatchCursorFailureLeavesThatPartEmpty: 컬럼·제약·인덱스·트리거 조회마다 커서 실패를 넣어, 그 부분이 모든 객체에서 비고, limitation이 한 번 남고, 미완전으로 표시되는지 본다. 수정 전 코드에서 네 사례 모두 실패했다. - gofmt -l, go vet ./..., go test ./... Co-Authored-By: Claude Opus 5.5 <[email protected]>
Owner
Author
GLM 리뷰 결과 (packet-ask)저장소 규칙(AGENTS.md)에 따라 이 PR의 변경을 GLM에 보내 리뷰받았습니다. 지적은 모두 실제 코드와 테스트로 검증한 뒤 처리했습니다. 수행 내역
지적별 처리
보류(deferred)나 중복(duplicate)으로 처리한 지적은 없습니다. GLM 밖에서 확인해 고친 결함GLM 지적을 검증하려고
다시 돌린 검사
🤖 Generated with Claude Code |
원인: 이 브랜치를 올린 뒤 main에 #47(네이티브 PostgreSQL·MySQL reader의 TLS와 connection-not-encrypted limitation, --cache-dir 항목 HMAC 인증과 신뢰 경계, MCP 응답 상한과 본문 1벌, language-traversal root-not-found 종료 코드 64)이 머지되어 브랜치가 main보다 뒤처졌다. 수정: origin/main(27f665e)을 merge commit으로 합쳤다. 두 쪽이 함께 바꾼 파일은 RELEASE-NOTES.md 하나이고, 두 쪽이 모두 맨 위에 `# Unreleased` 절을 새로 만들어 충돌했다. main의 `## Breaking`·`## Security`·`## Changed`를 그대로 두고, 이 브랜치의 `## Changed` 네 항목(JDBC PostgreSQL 컬럼의 정확한 이름 분할, Go·JDBC PostgreSQL 스키마 단위 조회, Go MySQL·MariaDB 제약 키 컬럼, Go MySQL·MariaDB 스키마 단위 조회)을 같은 순서로 main `## Changed` 목록 끝에 옮겼다. 두 쪽 문장은 바꾸지 않았다(main 대비 46줄 추가, 브랜치 대비 107줄 추가, 지운 줄 0). 네 항목 안의 "above"는 여전히 바로 앞 항목을 가리킨다. 나머지 파일은 겹치지 않는다. main은 engine, ci.yml, 엔진 문서와 SKILL.md 두 사본, 엔진 검증 스크립트를 바꿨고, 이 브랜치는 probe, probe-go, Fixtures, PERFORMANCE.md, 프로브 벤치·fixture 스크립트를 바꿨다. 합친 tree의 engine은 main과, probe·probe-go는 이 브랜치와 같다. 검증: 이 worktree에서 합친 tree(7b42807)로 cargo fmt --check, workspace build·test(590개 통과, XDG_CONFIG_HOME 격리), gofmt·go vet·go test (-count=1)·go build, gradle check shadowJar, CI의 Python 단위 검사 10개와 test-probe-mysql-benchmark·test-probe-postgres-benchmark·test-accuracy-sql, SKILL.md cmp 두 가지를 통과했다. 엔진과 프로브가 만나는 단계를 PG16에서 돌려 모두 통과했다: verify-catalog-dependencies, verify-sqlite-nullability, verify-collection-scope(native·Go·JDBC), verify-schema-lint, verify-unused, verify-bridge-facts, verify-native-tls(PostgreSQL 16사례), verify-fixtures.sh (SQLite·PostgreSQL, Go·JDBC 패리티). 컨테이너 DB(MySQL·MariaDB·SQL Server· Oracle) 단계는 로컬에서 건너뛰었고 CI가 검증한다. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
요약
앞선 보안·성능·구조 리뷰에서 "설계 결정 필요"(대형 리팩터링 포함)로 분류해 보류했던 schemagraph 프로브 성능 항목을, 승인된 설계 결정에 따라 구현했습니다. 근거는 공통 원칙 2(출력 계약)·5(성능 항목은 측정으로 의미 있는 개선이 확인될 때만 남긴다)·6(갈라진 두 벌은 공용 함수 하나로 최소 통합)과 schemagraph 저장소별 결정("probe-go N+1·JDBC SQLite 제약 수집은 원칙 5를 따른다. 원격 RTT 효과는 지연을 넣은 로컬 DB로 잰다")입니다.
이 PR 범위는 2건이고 구현 1건, 부분 구현 1건, 미구현 0건입니다.
probes__performance#1은 측정할 수 있는 경로를 모두 바꿨습니다. Go PostgreSQL, JDBC PostgreSQL(pgjdbc), Go MySQL·MariaDB입니다.schemagraph-probes__performance#2schemagraph-probes__performance#1바뀌는 것
sqlite_schema는 수집당 한 번 읽고 나머지는 테이블별 PRAGMA입니다. 20,000테이블 JSON이 28.421 s → 2.260 s이고 출력 바이트는 같습니다.getColumns/getPrimaryKeys/getImportedKeys에 table=null, 인덱스 카탈로그 SQL 1회). 문장은 28개이고, 4,000테이블 JSON이 55.469 s → 1.349 s입니다.RELEASE-NOTES.md# Unreleased의## Changed4항목이고, 측정은PERFORMANCE.md3절입니다. 버전 번호는 올리지 않았습니다.설계 결정
원문 지적의
refined_fix를 기본 설계로 삼았습니다. 결정 문서가 닫지 않은 선택지는 공통 원칙으로 정했고, 각 결정은 해당 커밋 본문의결정:줄에도 적었습니다. 두 항목 모두 원칙 5 대상입니다. 그래서 수정 전·후 빌드를 번갈아 재고, 전후 문서 sha256이 같은지 확인한 뒤에만 남겼습니다.1. JDBC SQLite 키 수집 (
probes__performance#2)결정 문서: 원칙 5(측정으로 확인될 때만 남긴다), 원칙 2(기존 정상 입력의 출력 바이트를 지킨다).
table_xinfo로 만들지 않고 드라이버 규칙을 옮겼습니다 (50bcd20).PrimaryKeyFinder·ImportedKeyFinder를 소스(Maven Central sha1 일치)와 바이트코드(javap)로 대조해 옮겼습니다.table_info의 마지막 pk 컬럼, 컬럼 생략 참조의 대상 PK 첫 컬럼,ORDER BY PKTABLE_NAME(DB 인코딩 바이트 비교), KEY_SEQ 안정 정렬, 기본 로캘toLowerCase, 시스템 표·temp 그림자 처리입니다.PRIMARY KEY (…)도 PK로 읽고, COLLATE 꼬리를 컬럼 이름에 남기고, 정규식이 맞지 않으면 pk 컬럼 중 마지막 하나만 씁니다.table_xinfo로는 출력 바이트가 같지 않습니다.--driver)은 드라이버 메타데이터를 그대로 씁니다. 규칙을 3.51.1.0 소스에서 옮겼기 때문입니다. 번들 버전을 올리면SQLiteConstraintsTest가 일부러 실패해 규칙을 다시 대조하게 합니다.{table}@대상으로 병합하고,{table}_fk_{n}로 이름을 붙이고, seq·name으로 정렬합니다.refined_fix의 (a)안은 하지 않았습니다. (a)안은 JDBC SQLite 제약 이름을 네이티브·Go 규칙({table}_pk,{table}_fk_{id})으로 바꾸는 것이고, 출력 계약 변경입니다.2. Go PostgreSQL 스키마 단위 조회 (
probes__performance#1)probe-go/sql/columns-postgres.sql에서 실행 시 파생합니다 (6bf3be4).cmp로 묶여 있습니다. 프로브 전용 사본을 두면 두 벌이 갈라질 수 있습니다.<label> unavailable — <오류>)은 같습니다.[]가 그대로 남습니다.applyPrimaryKeyPositions, 가시성 검사, usage 귀속 순서, NDJSON 경로(streamPostgres), 고정 연결(SetMaxOpenConns(1))과 세션 설정은 그대로입니다. 객체가 없는 스키마에는 묻지 않습니다.3. JDBC PostgreSQL 스키마 단위 조회 (
probes__performance#1)getDriverName=PostgreSQL JDBC Driver)일 때만 씁니다 (07c365f).PgDatabaseMetaData소스에서 null이면 테이블 조건만 빠지고 같은 질의·같은 정렬을 쓰는 것을 확인했습니다.schema.table: getColumns 실패 — …등). 인덱스는 예전처럼 limitation 뒤getIndexInfo로 대체합니다.getColumns결과를 정확한 스키마·테이블 이름으로 나눕니다. 계획이 허용한 결함 수정입니다.readColumnDoc·readPrimaryKeyRow·readImportedKeyRow)와 인덱스 SQL은 객체 경로와 같은 함수입니다(postgresCatalogIndexes(conn, schema, table?)). 트리거·본문·가시성 검사는 이미 스키마 단위라 그대로입니다.4. Go MySQL·MariaDB 스키마 단위 조회 (
probes__performance#1, 리뷰 후속)key_column_usage를 따로 묻는다)와 같은 결과로 맞춥니다. 일괄 조회로 바꾸면 MariaDB의 동점 순서가 또 바뀌므로 결과를 먼저 정해 두었습니다.Changed에 적었습니다(아래 "계약 변경" 2).interpolateParams는 기본으로 켜지 않습니다 (06807ee).?interpolateParams=true를 주면 지금도 켤 수 있음을 확인했습니다.mysqlCollationSensitiveNames).5. 측정 재현성과 하네스 (49b3aa2, 5830f3a, 78a7f8c, 78bf8d4)
Scripts/latency-relay.py: 방향마다 d ms 지연을 넣고, 반쪽 닫기를 전하고, 연결 오류를 기록합니다.Scripts/benchmark-probe-postgres.py:--latency-ms, 문장 계수(xact_commit),--format,--warmup,--indexes, JDBC 쌍을 더했습니다.Scripts/benchmark-probe-mysql.py: 다이제스트 고정 이미지를 쓰고Questions·Com_stmt_prepare를 셉니다.test-postgres-fixture-auth.py).127.0.0.1:*(모든 포트)로 두었습니다. 재검사에서 계약 테스트 실패로 드러나 78bf8d4에서 포트로 한정했습니다.xact_commit에 잡히기 때문입니다(4,000테이블 JDBC에서 30 대 28). 기본값(NDJSON, 지연 없음, Go 쌍)은 예전과 같은 명령으로 돕니다.docker rm -f -v).Scripts/test-probe-postgres-benchmark.py·test-probe-mysql-benchmark.py는 CI 목록에 넣지 않았습니다..github/workflows/ci.yml은 레인 d1(fix(engine)!: 네이티브 reader TLS, --cache-dir 항목 HMAC 인증, MCP 응답 상한, language-traversal 종료 코드 64 #47)이 고치고 있어서입니다(후속 작업 8).6. 변경 기록 (c6f4292, 378ebe9, 9b5ae2a)
RELEASE-NOTES.md를 레인 d1 전용으로 두었습니다.# Unreleased, 같은 안내 문장,## Changed)를 만들고 4항목을 적었습니다.## Changed목록 끝으로 옮기면 됩니다(아래 "머지 참고").계약 변경
종료 코드, 기본값, 설정 검증, 프로브 명령줄 옵션은 바뀌지 않습니다. 성공한 수집의 출력은 아래 1·2의 이름 사례를 빼면 바이트 단위로 같습니다.
출력이 바뀌는 결함 수정 (
RELEASE-NOTES.mdChanged)getColumns의 스키마·테이블 인자는 LIKE 패턴입니다. 그래서 예전에는a_b가a1b의 컬럼을,pct%name이pctxname의 컬럼을, 스키마my_app의 테이블이myxapp의 같은 이름 테이블 컬럼을 함께 잡았습니다.\는 LIKE 이스케이프 문자입니다. 그래서back\slash는backslash의 컬럼을 잡거나, 그런 테이블이 없으면 컬럼 없이 가시성 limitation(uncollected column)을 남겼습니다.review로 비교하면 컬럼 추가·삭제로 보일 수 있습니다.table_constraints–key_column_usage조인에서 이름을 대소문자·악센트 무시로 비교합니다.PRIMARY, 이름이 같은UNIQUE)이 대소문자·악센트만 다른 테이블(P/p,resume/résumé)이나, 대소문자만 다른 스키마의 같은 이름 테이블의 키 컬럼까지 가졌습니다. 그 순서도 서버마다 달랐습니다.review에서 해당 테이블의constraints-changed를 낼 수 있습니다.실패 경로
RELEASE-NOTES.mdChanged).schema.table: getColumns 실패 — …등)를 남기고, 인덱스는getIndexInfo로 대체합니다.bundled PostgreSQL column query could not be adapted to schema-wide reads (…); rebuild the probe를 남기고 미완전으로 표시합니다. 정본과 파생 규칙이 어긋난 빌드에서만 나고, 그런 빌드는 Go 테스트가 먼저 막습니다. 릴리스 노트에는 적지 않았습니다.출력이 같은 동작 변화
개발 도구(사용자 계약 밖)
Scripts/latency-relay.py,Scripts/benchmark-probe-mysql.py,Scripts/test-probe-mysql-benchmark.py,Scripts/test-probe-postgres-benchmark.pyScripts/benchmark-probe-postgres.py--latency-ms,--format json|ndjson(기본 ndjson),--warmup,--indexes,--baseline-jar·--new-jar·--javaresults.json에 실행별 문장 수·문서 sha256, producer별 summary, relay 연결 오류를 남깁니다.Fixtures/sqlite/named-constraints.sql과Scripts/verify-fixtures.sh단계: 네이티브·Go·JDBC의 참조 관계 패리티와 JDBC 제약 이름을 고정합니다.변경 기록
RELEASE-NOTES.md# Unreleased→## Changed4항목: 위 1, 2와 PostgreSQL·MySQL 스키마 단위 조회(실패 경로 3 포함) 설명입니다.PERFORMANCE.md: 새 절 "Remote PostgreSQL collection (2026-10-20)"·"Remote MySQL and MariaDB collection (2026-10-20)"과, "JDBC: one schema with 10,000 tables" 절의 SQLite 키 수집 전후 행입니다.RELEASE-NOTES.md에 넣지 않았습니다. 릴리스 정리 때 쓸 수 있는 초안은 다음과 같습니다.변경
수정 (fix)
성능 (perf)
테스트 (test)
문서 (docs)
정정: 06807ee 본문의 "단위 테스트 10개"는 9개(테스트 함수 기준)입니다. 커밋은 고치지 않았고, 378ebe9 본문에 정정을 남겼습니다.
바뀐 파일은 22개(+3,662/−378)입니다.
probe-go/postgres.go,probe-go/mysql.go, 새 테스트postgres_batch_test.go·mysql_batch_test.go·mysql_constraints_test.goExtractor.kt,IndexCatalog.kt, 새 파일PostgresSchemaMetadata.kt·SQLiteConstraints.kt, 새 테스트PostgresSchemaMetadataTest.kt·SQLiteConstraintsTest.kt,ScriptedConnection.kt(메타데이터 호출 기록과 메서드 대체)Fixtures/sqlite/named-constraints.sql·Scripts/latency-relay.py·Scripts/benchmark-probe-mysql.py·Scripts/test-probe-mysql-benchmark.py·Scripts/test-probe-postgres-benchmark.py, 수정Scripts/verify-fixtures.sh·Scripts/benchmark-probe-postgres.py·Scripts/test-postgres-fixture-auth.pyPERFORMANCE.md,RELEASE-NOTES.md머지 참고
RELEASE-NOTES.md하나입니다.# Unreleased를 만듭니다. fix(engine)!: 네이티브 reader TLS, --cache-dir 항목 HMAC 인증, MCP 응답 상한, language-traversal 종료 코드 64 #47 head(be98cbb)와 merge-tree로 시험해 보니 이 파일만 충돌했습니다.## Changed4항목을 #47의## Changed끝으로 옮깁니다.engine/**,engine/source/src/sql/*.sql,probe-go/sql/*.sql,.github/workflows/*,Dockerfile,action.yml,HANDOFF.md,RELEASING.md, README 계열은 바꾸지 않았습니다.검증
로컬 macOS(arm64)에서 HEAD 9b5ae2a로 실행했고, Docker 단계는 Colima(aarch64 Linux VM)에서 돌렸습니다. origin/main(aaba709)이 이미 이 브랜치의 조상이라 merge가 필요 없었습니다. GitHub CI는 push 전이라 아직 돌지 않았습니다.
HEAD 9b5ae2a 검사
cd probe-go && gofmt -l . && go vet -p 3 ./... && go test -p 3 -count=1 ./... && CGO_ENABLED=0 go build -p 3 ./...(+-o빌드,-trimpath -ldflags='-s -w'빌드): pass — gofmt가 고칠 파일 0개, 최상위 Go 테스트 75개 통과(0.63 s), 빌드 3종 성공gradle -p probe clean check shadowJar --no-daemon --max-workers=2(JDK 17): pass — Kotlin 테스트 55개 실패 0. 경고 3개(Extractor.kt'Redundant call',Model.kt:136·146deprecated)는 main에 있던 기존 경고. JAR sha256b8f62ecf…cargo fmt --manifest-path engine/Cargo.toml --all -- --check,cargo build --workspace --locked -j 3,cargo test --workspace --locked -j 3: pass — 이 브랜치는engine/을 바꾸지 않음. 테스트 바이너리 22개에서 537개 통과python3 -B Scripts/test-postgres-fixture-auth.py: pass — 6개. 처음 실행에서는 이 브랜치 때문에 실패했습니다(test_probe_benchmark_cluster의 TypeError, 49b3aa2가 원인). 78bf8d4로 고쳤습니다. origin/main에서는 6개 모두 통과test-mysql-ready,test-fixture-ports,test-postgres-fixture-diagnostics,test-postgres-fixture-auth,test-sqlserver-oracle-accuracy,test-benchmark-scale,test-probe-sqlite-benchmark,test-review-action,test-container,test-dml-accuracy,test-dml-producers,test-probe-mysql-benchmark,test-probe-postgres-benchmark): pass — 13개 모두 rc=0benchmark-probe-postgres.py --schemas 1 --tables-per-schema 50 --indexes --format json --warmup 1 --repeat 3 --latency-ms 1/--latency-ms 0(Go·JDBC 쌍): pass — 78bf8d4 뒤에도 실제 수집이 됨. relay 연결 16개, 오류 0. 문장 Go 52, JDBC 28. 두 지연 조건 모두 producer마다 문서 해시 하나cmp engine/source/src/sql/{catalog-postgres,catalog-sqlserver,catalog-oracle,visibility-postgres,columns-postgres}.sql probe-go/sql/…: pass — 동결 정본 5개가 바이트 단위로 같음verify-contracts.py,verify-competitive.py,verify-cancellation.py,verify-html.py --screenshot,verify-review-action.py --schema <SARIF 2.1.0>: pass — jsonschema 4.23.0·sqlglot 30.18.0, SARIF 스키마 sha256이 CI 고정값과 일치, HTML은 로컬 Chromerun-review-action.py를 composite action과 같은SG_REVIEW_*·GITHUB_WORKSPACE·RUNNER_TEMP·GITHUB_OUTPUT환경으로 실행(CI "Invoke the consumer Action" + "Verify the Action exposes findings and artifacts"): pass — exit-code 출력 1, json·sarif·markdown 출력 파일 모두 비어 있지 않음verify-accuracy.py --sqlglot --suite all --strict,verify-external-imports.py --dbt(dbt-core 1.12.5, dbt-postgres 1.11.0): pass — PG16 로컬 cluster에서 chinook 23건·pagila 20건 실패 0, dbt import okverify-catalog-dependencies.py,verify-sqlite-nullability.py,verify-collection-scope.py×3(native·Go·JDBC),verify-schema-lint.py,verify-unused.py,verify-openlineage.py,verify-bridge-facts.py,test-accuracy-sql.py --jdbc-jar, 번들 문서·라이선스cmp: pass — 모두 rc=0, 실행 뒤 worktree 변경 0줄. HEAD에서 빌드한 Go 바이너리와 JAR 사용Scripts/verify-probe-maven.sh: pass — 로컬에 Maven CLI가 없어 스크립트가 정한 Gradle Maven-repository consumer로 검증(게시물 0.7.1 검증, 외부 consumer 검사 통과)SG_MYSQL_JAR=… SG_ORACLE_JAR=… SG_FIXTURE_ARTIFACTS=<scratch>/fixture-artifacts Scripts/verify-fixtures.sh(Docker: MySQL, MariaDB, MSSQL, Oracle 포함): pass — 마지막 줄verify-fixtures: OK,주의:줄 0개. MySQL·MariaDB·PG·SQLite 그래프와 MSSQL·Oracle catalog-dependencies 검증이 모두 실행됨. 드라이버 jar sha256이 CI 고정값과 일치. 실행이 남긴 익명 볼륨 6개만 지움verify-mysql-accuracy.py --sqlglot --strict: pass — MySQL·MariaDB 16/16 실패 0. 첫 시도에서 MariaDB 컨테이너가 기동 중 멈췄습니다(여러 세션이 함께 쓰는 VM의 일시 문제). 엔진만 쓰는 스크립트이고 엔진은 바뀌지 않았으며,--database mariadb재실행과 CI와 같은 전체 명령 재실행이 모두 통과cargo package --workspace --locked,shasum -a 256 --check workspace-cli.sha256: pass — yanked yoke-derive 경고는 main과 같은Cargo.lock의 기존 경고bash Scripts/test-publish-release-assets.sh: pass — 오프라인 게시 사례 36개-trimpath·SQLite 검증·패키지, build-jdbc): pass — 0.7.1, publish=false, 독립 JAR smoke 포함verify-sqlserver-oracle-accuracy.py --database oracle --strict(engine, Go 프로브, JAR, ojdbc8): pass — Oracle 18건 SQL 검증 통과, 정확도 실패 0bash -n Scripts/verify-fixtures.sh: passgofmt -l·go vet·go test,test-postgres-fixture-auth.py·test-probe-postgres-benchmark.py·test-probe-mysql-benchmark.py·test-probe-sqlite-benchmark.py: pass수정 전 실패와 회귀 검출
SQLiteConstraintsTest3개가 수정 전 코드에서 모두 실패했습니다.sqlite_schema를 한 번만 읽는지 단언, 옮긴 경로와 드라이버 경로의 JSON·NDJSON 문서 대조, 터키어 로캘String.compareTo로 바꾸는 변이도 잡습니다.named-constraints.sql패리티 단계는 간선 하나를 지운 그래프와 이름 규칙이 다른 그래프로 실패를 확인했습니다.postgres_batch_test.go6개. 수정 전 코드에서 질의 수가 1테이블 16, 40테이블 172로 실패했습니다. 리뷰도 따로 확인했습니다. 동작 테스트 3개(질의 수, 분할, 실패 보고)는 수정 전 실패하고, 새 헬퍼를 쓰는 3개는 수정 전 코드에서 컴파일되지 않습니다.PostgresSchemaMetadataTest4개(호출 수 상수, 객체 경로와 문서 동일,A_B/A1B정확 분할, 묶음 실패 시 예전 문구)가 수정 전 모두 실패했습니다. 수정 전에는 getColumns·getPrimaryKeys·getImportedKeys가 각 40회, 준비 SQL이 45개였습니다.TestMySQLConstraintsKeepOnlyTheirOwnKeyColumns: 소유자 검사를 빼면 P·p 모두[a b c]가 되어 실패TestMySQLQueriesPerSchemaDoNotGrowWithObjectCount: 앞 커밋 코드에서 12 대 246으로 실패TestMySQLCatalogQueriesDoNotJoinInformationSchemaTables: 06807ee 코드에서 실패TestMySQLSchemaBatchFailureIsReportedOncePerSchema: 키 컬럼 실패 두 경우가 06807ee 코드에서 실패test-postgres-fixture-auth.py는 78bf8d4 전 1개 오류였습니다. 벤치를127.0.0.1:*줄,-k없음,-k /tmp로 바꾸면 각각 실패합니다.출력 동일성(전후 문서 sha256)
e35a12c27679→06f479f17b64): 모두 같고 그래프 해시도 같습니다.20,000테이블, FK 스키마 2,50010,000테이블06f479f17b64→b8f62ecfdfbe): 30/36 같습니다.my_app.a_b에 붙던a1b컬럼과my_app.t에 붙던myxapp.t컬럼이 빠진 것입니다.back\slash·pct%name·sch\ema사례를 따로 확인했습니다. 스키마sch\ema의 객체는 두 JAR 모두 수집하지 못합니다(기존 동작, 후속 작업 4).측정
JDBC SQLite (
Scripts/benchmark-probe-sqlite.py, OpenJDK 17.0.20, sqlite-jdbc 3.51.1.0, 두 JAR을 번갈아 두 바퀴, 셀당 6회 중앙값, load 5~7)3.86에서 1.601.72가 됐습니다. 20,000테이블 최대 RSS는 약 524 → 375 MiB입니다.PostgreSQL (
Scripts/benchmark-probe-postgres.py, PG 16.13,--latency-ms 1(RTT 약 2 ms), warm-up 1회 + 교대 3회 중앙값, load 5~17, 기준 835e054)MySQL·MariaDB (
Scripts/benchmark-probe-mysql.py, 다이제스트 고정 이미지, Colima 2 CPU·4 GiB,--latency-ms 1, 테이블 10개당 procedure 1개, warm-up 1회 + 교대 3회 중앙값, 기준 da64c1d → 92af86d)sys.schema_unused_indexes질의가 해시 조인 대신 중첩 반복 계획으로 실행돼, 0.01 s 대신 1.56 s 걸렸습니다.실행하지 않은 검사
verify-sqlserver-oracle-accuracy.py --database sqlserver --strict: not-run — 스크립트가 Linux x86_64 Docker를 요구합니다("use the GitHub accuracy workflow"). 로컬 Colima는 aarch64입니다.gh run download, SHA256SUMS): not-run — Linux x86_64 러너와 같은 실행의 artifact가 필요합니다. 이 호스트의 aarch64-apple-darwin 행렬과 build-jdbc는 위에서 통과했습니다.docker build --tag schemagraph:ci .+verify-container.py): not-run.Dockerfile,README.md,LICENSE-*,engine/)가 origin/main과 같고, aaba709의 main push CI(container 잡 포함)는 success입니다.native-producers행렬(verify-dml-producers.py기준선,verify-dml-replay.py): not-run — CI 전용입니다. 게시된 v0.4.3 x86_64 Linux 산출물로 기준선을 만들고 이 브랜치의 프로브는 쓰지 않습니다. 같은 워크플로의 단위 검사는 로컬에서 통과했습니다.verify-ibm-containers.py --memory 5g): not-run — Db2·Informix 이미지가 linux/amd64 전용이고--privileged와 5 GB 메모리가 필요합니다. 로컬 VM은 aarch64·4 GiB입니다.원격 CI에서 꼭 볼 단계
ibm-fixtures.yml(Db2·Informix): 50bcd20이 JDBCExtractor의 키 행 읽기를 공용 함수로 뺐습니다. MySQL·MSSQL·Oracle JDBC 경로는 로컬 Dockerverify-fixtures.sh로 확인했고, Db2·Informix 경로는 CI에서만 확인할 수 있습니다.sqlserver-oracle-accuracy.yml의 sqlserver 행렬,dml-accuracy.yml의 native-producers 행렬test-postgres-fixture-auth.py구현하지 않은 항목
probes__performance#1의 남은 부분(부분 구현 사유): JDBC 프로브의 PostgreSQL 외 드라이버(MySQL Connector/J, Oracle, SQL Server, Db2, Informix, H2 등)는 객체마다 묻는 방식을 유지합니다. 측정 불가로 적용하지 않았습니다.interpolateParams기본 전환: 바인드 값 전달 방식이 바뀝니다. URL로는 지금도 켤 수 있습니다.SetMaxOpenConns(1)고정 연결을 풀어 병렬화하기: 세션 고정 계약을 지킵니다.probes__performance#2의refined_fix(a)안(제약 이름 규칙 변경): 출력 계약 변경이라 하지 않았습니다..git설정이 명령을 실행하는 문제): 해당 없음. 엔진·프로브 코드에 외부 프로세스 실행이 없고(Command::new·exec.Command·ProcessBuilder0건), git을 부르는 곳은 이 저장소 자신의 릴리스 워크플로뿐입니다.action.yml은version: latest다운로드 없이 고정된 액션 소스로 빌드하고,INSTALLATION.md는 버전 고정과SHA256SUMS대조를 이미 안내합니다.후속 작업(이 PR 밖)
SQLiteConstraintsTest가 일부러 실패합니다. 새 드라이버 소스와SQLiteConstraints.kt를 다시 대조하고REPLICATED_SQLITE_JDBC_VERSION을 올립니다.getImportedKeys의 이름 escape 누락(기존): 테이블 이름을 escape 없이 결과문에 넣어, 작은따옴표가 든 이름은 드라이버의 메타데이터 SQL을 깨거나 바꿀 수 있습니다. 이 PR은 출력 보존을 위해 그런 이름을 드라이버 경로로 둡니다.REFERENCES parent(기존 생산자 간 차이): JDBC는 부모 PK 컬럼 간선을 내고, 네이티브·Go는 테이블 간선만 냅니다(Go 문서는columns: [""]). 바꾸지 않았습니다.\가 든 경우(기존):getTables의 스키마 패턴 때문에 예전·새 JAR 모두sch\ema같은 스키마의 객체를 수집하지 못합니다.sys.schema_unused_indexes의 중첩 반복 계획: Go 프로브의 인덱스 사용 질의(이 PR에서 바꾸지 않음)가 같은 이미지의 일부 새 컨테이너에서 2,000테이블에 약 1.56 s(보통 0.01 s) 걸립니다.performance_schema.table_io_waits_summary_by_index_usage를 직접 읽으면 피할 수 있습니다.lower_case_table_names=0의 대소문자만 다른 스키마(기존):sgedge와SGEDGE처럼 대소문자만 다른 스키마의 트리거·routine·routine 파라미터가 서로의 수집에 섞입니다.trigger_schema = ?,routine_schema = ?가 대소문자를 무시하고 비교하기 때문입니다. 예전·새 Go 프로브가 같고, 이 PR은 바꾸지 않았습니다.Scripts/test-probe-postgres-benchmark.py·test-probe-mysql-benchmark.py를 ci.yml "Verify accuracy and scale measurement invariants"에 넣습니다. ci.yml은 #47이 고치는 중이라 그 뒤에 합니다.Scripts/verify-fixtures.sh기존 문제(로컬 전용):SG_ORACLE_JAR가 없으면 EXIT trap의docker rm -f ""가 실패해,verify-fixtures: OK뒤에 종료 코드 1이 되고 임시 디렉터리를 남깁니다.docker rm -f에-v가 없어 실행마다 익명 볼륨 6개가 남습니다.getColumns·getTables는 권한을 거르지 않습니다. 그래서 권한 제한 사용자로 수집해도 JDBC 문서가 소유자의 문서와 같습니다. Go는 information_schema를 읽어 결과가 다릅니다.RELEASE-NOTES.md의## Changed항목을 옮깁니다(위 "머지 참고").리뷰
probes__performance#1의 Go MySQL N+1이 그대로 남았고 미적용 기록도 없음 → 고침(da64c1d, 06807ee, 78a7f8c, 92af86d, 378ebe9). 고치는 중에 MariaDB 제약 키 컬럼이 섞이는 기존 결함(da64c1d)과 서버 쪽 조인의 제곱 증가(92af86d)를 찾아 함께 고쳤습니다.PrimaryKeyFinder·ImportedKeyFinder의 질의와 정규식 플래그,getImportedKeys가 SQLException만 잡는 범위, quote·escape·unquoteIdentifier, UNION 500항 상한입니다.Co-Authored-By)을 지켰고, 성격이 다른 변경을 섞은 커밋은 없습니다. 비공개 이름·홈 경로·새 의존성도 없습니다.Extractor의 키 읽기 공용 함수가 MySQL·MSSQL·Oracle JDBC 경로도 지나니 Docker fixture로 확인하라고 권했습니다. 재검사가 로컬 Dockerverify-fixtures.sh(MySQL·MariaDB·MSSQL·Oracle 포함)로 확인했고, Db2·Informix는 CI에서 확인합니다.start_postgres인자·반환값과 비밀번호 파일 범위를 바꿔, CI "Verify database startup failure paths"의test-postgres-fixture-auth.py가 TypeError로 실패함 → 78bf8d4🤖 Generated with Claude Code