[history] Migrate to sql::Statement time accessors
history 데이터베이스 세 파일이 손으로 하던 base::Time/base::TimeDelta ↔ int64 변환 10곳을 sql::Statement의 전용 접근자(ColumnTime()·ColumnTimeDelta()·BindTimeDelta())로 옮겼습니다(crbug.com/40176243, crbug.com/40251269). 이미 Fixed로 닫혀 있는 이슈를 다시 열어본 데서 시작한 작업이고, 12파일 32곳짜리 마이그레이션을 OWNER 그룹 단위로 쪼갠 시리즈의 첫 CL입니다.
문제 설명
issue url: https://crbug.com/40176243 (Time) · https://crbug.com/40251269 (TimeDelta)
sql::Statement는 시간 컬럼을 읽고 쓰는 전용 접근자를 제공하고, 헤더가 직접 "이 스니펫들을 대체하라"고 지시합니다:[sql/statement.h] This is equivalent to the following snippets, which should be replaced.
base::Time::FromInternalValue(ColumnInt64(col))/base::Time::FromDeltaSinceWindowsEpoch(base::Microseconds(ColumnInt64(col)))
TODO(crbug.com/40176243): Migrate all time serialization to this method, and then remove the migration details above.2026년 1월 CL 7455767이 이 마이그레이션의 쓰기 쪽 일부를 이행하고
Fixed:로 두 이슈를 닫았습니다. 그런데 이슈 제목 자체가 "Migrate toBindTime()andColumnTime()" 으로 읽기까지 범위였고, 읽기 쪽과 여러 줄에 걸친 쓰기 패턴은 그대로 남았습니다. 헤더의 TODO 두 개도 ToT에 살아 있습니다.그래서 history 컴포넌트 안에서 읽기와 쓰기가 비대칭인 상태가 됐습니다. 같은 컬럼을 한쪽에서는 접근자로 쓰고 다른 쪽에서는 손으로 변환해 읽습니다.
download_database.cc— 쓰기는BindTime()(610·613·692행)인데 읽기 3곳(468·479·482)은base::Time::FromInternalValue(ColumnInt64())keyword_search_term.cc:29—last_visit_time을url_database.cc는BindTime()으로 쓰고, 여기서는 손으로 읽음visit_annotations_database.cc— 같은 파일 900행은BindTime()인데 330·333·412·415행은BindInt64(N, delta.InMicroseconds())
해결 내용
세 가지 패턴을 각각 대응하는 접근자로 바꿨습니다. 총 10곳 / 3파일, +14/−18.
// 읽기 (base::Time) — DownloadDatabase::QueryDownloads(), KeywordSearchTermVisitFromStatement()
- info->start_time =
- base::Time::FromInternalValue(statement_main.ColumnInt64(column++));
+ info->start_time = statement_main.ColumnTime(column++);
// 읽기 (base::TimeDelta) — VisitAnnotationsDatabase::GetContextAnnotationsForVisit()
- statement.ColumnInt64(1), base::Microseconds(statement.ColumnInt64(2)),
+ statement.ColumnInt64(1), statement.ColumnTimeDelta(2),
// 쓰기 (base::TimeDelta) — Add/UpdateContextAnnotationsForVisit()
- statement.BindInt64(
- 2, visit_context_annotations.duration_since_last_visit.InMicroseconds());
+ statement.BindTimeDelta(2, visit_context_annotations.duration_since_last_visit);바뀌는 것은 호출자가 직렬화 규약을 아는가입니다. 지금까지는 "이 컬럼의 int64는 Windows epoch 기준 마이크로초"라는 지식이 호출자 여섯 함수에 흩어져 있었고, 이제 그 지식은 sql::Statement 안에만 있습니다.
변환은 바이트 단위로 동일합니다. base::Time의 internal value가 곧 Windows epoch 기준 마이크로초이고, ColumnTime()은 FromDeltaSinceWindowsEpoch(Microseconds(int64))로 정확히 그것을 복원합니다(sql/statement.cc). ColumnTimeDelta()는 Microseconds(int64), BindTimeDelta()는 InMicroseconds()입니다. 저장 포맷·스키마·마이그레이션은 그대로입니다.
Fixed:가 아닌 Bug: 40176243, 40251269를 썼습니다. 남은 22곳이 다른 컴포넌트에 있고, 헤더의 TODO는 시리즈 마지막 CL에서 지울 예정입니다.
발굴 과정
트래커 검색으로는 나오지 않는 종류의 작업이었습니다. 이슈가 닫혀 있기 때문입니다.
TODO(crbug.com/로sql/을 훑어statement.h의 TODO 두 개를 찾고, 거기서 이슈 번호를 역추적했습니다.이슈는
Fixed였지만 TODO는 코드에 남아 있었습니다. 선례 CL을 열어 실제 범위를 확인하니 쓰기 일부만 이행돼 있었습니다.트리 전체를 다시 스캔했습니다. 이때 선례 CL의 grep이 놓친 패턴이 드러났습니다 — 한 줄짜리
BindInt64(N, x.ToInternalValue())만 잡고, 아래처럼 인자가 다음 줄로 넘어간 경우를 놓친 것입니다:statement.BindInt64( 2, visit_context_annotations.duration_since_last_visit.InMicroseconds());결과적으로 읽기 23곳 + 쓰기 9곳 = 32곳 / 12파일이 남아 있었습니다.
12파일이 8개 OWNERS 그룹에 걸쳐 있어 한 CL로 묶으면 승인자가 여덟 명이 됩니다. 컴포넌트 단위로 쪼개고, 그중
components/history/OWNERS한 그룹으로 끝나는 세 파일을 첫 CL로 삼았습니다. 같은 history 안에서도journeys/는 OWNERS가 따로 갈리고 그 파일에 열린 CL이 있어 다음 CL로 분리했습니다.
리뷰어는 components/history/OWNERS이면서 선례 CL 7455767에 Code-Review+1을 준 사람으로 정했습니다. 같은 마이그레이션의 나머지 절반이라는 맥락이 그대로 이어집니다.
테스트 방법
동작 불변 리팩토링이라 새 테스트 없이 기존 테스트 전원 통과가 검증입니다.
components_unittests --gtest_filter='HistoryBackendDBTest.*:URLDatabaseTest.*:VisitAnnotationsDatabaseTest.*:HistoryBackendTest.*:HistoryServiceTest.*'281/281 통과- 빌드는
Build Succeeded30,065스텝 / 12시간 54분이 걸렸습니다. 변경은 3파일인데 직전 브랜치가base/헤더를 건드린 CL이어서, 그 헤더가 원상복구되며base/check.h를 include하는 트리 전체가 무효화된 탓입니다.
배운 점
Fixed로 닫힌 이슈가 끝난 이슈는 아닙니다. 여기서는 이슈 제목이 이행되지 않은 범위(ColumnTime())를 그대로 담고 있었고 헤더의 TODO도 살아 있었습니다. 닫힌 번호를Bug:로 참조하는 것은 허용되므로, 재오픈을 기다리며 멈추는 대신 CL 설명에 선례 CL을 part 1로 인용하고 남은 범위를 적었습니다.- 정규식 한 줄로 훑은 마이그레이션은 대개 여러 줄 패턴을 놓칩니다. 선례 CL이 남긴 9곳이 전부 인자가 줄바꿈된 형태였습니다. 남은 작업을 셀 때는 포매터가 어떻게 줄을 나눴을지까지 감안해 다시 스캔하는 편이 정확합니다.
- 리팩토링의 범위는 코드량이 아니라 OWNERS 경계로 정합니다. 32곳을 한 번에 올리면 diff는 작아도 승인 여덟 개를 모아야 합니다. 컴포넌트 단위로 쪼개면 각 CL이 리뷰어 한 명으로 끝납니다.
- "동일하다"는 주장은 구현으로 확인해야 합니다.
FromInternalValue와ColumnTime()이 같다는 것을 문서가 아니라sql/statement.cc의 실제 코드로 확인했고, 그 근거를 CL 설명에 적었습니다. 저장 포맷을 건드리는 것처럼 보이는 변경일수록 리뷰어가 가장 먼저 확인하고 싶어 하는 지점입니다.