(KO) CUBRID Timezone — IANA 타임존 데이터 컴파일, tz_id 해석, 그리고 DATETIMETZ/TIMESTAMPTZ 변환
목차:
학술적 배경
섹션 제목: “학술적 배경”타임존(timezone)은 고정된 UTC 오프셋이 아니다. 그것은 시민 시각(civil time)에서 UTC로 가는 조각별 함수(piecewise function)이며, 모든 일광절약(DST, daylight saving time) 전환 시점과 표준 오프셋의 모든 역사적 변경 시점에서 불연속을 가진다. IANA 타임존 데이터베이스(tzdata)는 이 함수의 정본 인코딩이다. 약 400개의 명명된 지역(Europe/Berlin, America/New_York, Asia/Seoul, …) 각각이 오프셋 규칙(offset rule) 의 시퀀스(연도 Y₁ 부터 Y₂ 까지 표준시는 UTC+N, 일광절약은 규칙 집합 R 을 따랐다)와 DS 규칙(DS rule) 의 집합(from_year 부터 to_year 사이, month/on에, at_time 시각에, save_time 시간을 더한다)으로 묘사된다. 실제 구현이라면 모두 다음 세 가지 교과서적 관심사를 마주한다.
시계 시각(wall clock)에서 UTC로의 변환은 함수가 아니다. 봄철 전환(spring-forward)은 빈 구간(gap)을 만든다. America/New_York에서 전환일의 02:30은 존재하지 않는다. 가을철 전환(fall-back)은 중복 구간(overlap)을 만든다. 01:30이 두 번 나타난다. DBMS는 (a) 입력을 거부하거나, (b) 결정론적인 한쪽을 고르거나, (c) 사용자가 제공하는 모호성 해소자(+05:00, EDT vs EST)를 받아들여야 한다. PostgreSQL은 기본값으로 (b)를 택한다. CUBRID는 (b)와 (c)를 결합한다. 사용자가 일광절약 약자(Europe/Bucharest EET vs Europe/Bucharest EEST)를 덧붙일 수 있고, 엔진은 중복 구간의 양쪽을 모두 탐색한다.
AT 시각 한정자. IANA 데이터베이스는 모든 전환 시각에 세 가지 접미사 중 하나를 붙인다. s(표준 지역시, standard local time), w(시계 시각, 기본값), 그리고 u/g/z(UTC). 동일한 숫자 at_time = 02:00은 접미사에 따라 서로 다른 절대 순간을 가리킨다. 이를 무시한 변환은 전환 부근에서 한 시간 오차를 낸다.
연도 경계 중복. 5월 → 9월 사이 활성인 DS 규칙은 현재 연도와 일치한다. 하지만 10월 → 다음 해 2월 사이 활성인 규칙은 연도 경계를 넘는다. 1월 1일의 조회는 직전 연도까지 함께 살펴봐야 한다. CUBRID는 in_month > src_month일 때 항상 year_to_apply_rule = src_year - 1을 먼저 평가한 뒤 src_year와 비교하는 방식으로 이를 인코딩한다.
알고리즘 너머에 두 가지 공학적 관심사가 더 결정적이다.
저장 비용. 지역 태그가 붙은 datetime은 개념적으로 (datetime, zone, offset-rule, DS-rule)을 짊어진다. 단순히 합산하면 한 행당 8+64+8+8 바이트다. 실제 시스템들은 zone 태그를 4 바이트로 압축한다. PostgreSQL은 UTC 순간만 저장하고 zone은 세션 상태에서 다시 끌어낸다. Oracle은 7 바이트 ID를 저장한다. CUBRID는 4 바이트 TZ_ID로 (zone, offset-rule, DS-rule) 트리플 또는 부호 있는 원시 오프셋 중 하나를 저장하며, 상위 두 비트로 둘을 구분한다.
갱신 주기. IANA는 몇 달에 한 번씩 tzdata를 릴리스한다. DBMS는 디스크에 이미 적재된 컬럼들을 무효화하지 않은 채 새 버전을 흡수해야 한다. 지역 이름 은 안정적이지만 지역별 규칙 은 변한다(새 국가, 폐지된 DST, 소급 정정). CUBRID는 gen_tz의 세 모드로 이를 처리한다. new(처음부터 재구축, 새 CUBRID 릴리스용), update(번호를 바꾸지 않고 규칙만 갱신, 현재 경로에서는 비활성화), extend(이전 zone_id 번호 부여를 보존하면서 규칙을 재구축, 운영 경로). 만약 어떤 zone이 하위 호환을 깰 만큼 변했다면 tzc_extend → tzc_update로 인플레이스(in-place) 마이그레이션이 일어난다.
DBMS 공통 설계 패턴
섹션 제목: “DBMS 공통 설계 패턴”WITH TIME ZONE을 지원하는 모든 관계형 엔진은 동일한 부분 문제들을 푼다.
데이터를 어디에 두는가. PostgreSQL은 IANA의 zoneinfo/를 그대로 배포한다(이진 tzfile(5) 블롭을 처음 사용 시 파싱하며, pg_timezone_names로 노출). MySQL은 zone 데이터를 서버 안 테이블에 저장한다(mysql.time_zone*, mysql_tzinfo_to_sql로 채움). Oracle은 tzdata를 timezone_<n>.dat + timezlrg_<n>.dat로 컴파일하며, DBMS_DST로 마이그레이션한다. SQLite는 내장 데이터베이스가 없다. 'utc'/'localtime'을 통한 고정 오프셋만 다룬다.
인코딩. PostgreSQL은 TIMESTAMP WITH TIME ZONE을 UTC int64 마이크로초로 저장하고, 표시할 때만 세션 TimeZone GUC에서 zone 이름을 가져온다. MySQL은 TIMESTAMP를 int32 UTC 초로 저장하고 입출력 시점에 @@session.time_zone으로 변환한다. Oracle은 zone을 명시적으로 들고 다닌다. 7 바이트 UTC + 2 바이트 zone id. CUBRID는 Oracle을 따라간다. DATETIMETZ는 8 바이트 DB_DATETIME(UTC) + 4 바이트 TZ_ID. TIMESTAMPTZ는 4 바이트 UTC DB_TIMESTAMP + 4 바이트 TZ_ID.
세션 상태. Postgres SET TimeZone. MySQL @@session.time_zone. Oracle ALTER SESSION SET TIME_ZONE. CUBRID는 두 가지 설정을 둔다. db_timezone(시스템 수준, 서버 부팅 시 설정, 클라이언트 세션 없는 데몬이 사용)과 timezone(세션 수준)이 그것이다. 런타임 변수 tz_Region_session(클라이언트) 혹은 session_tz_region(서버, THREAD_ENTRY 단위)이 실효 값을 보유한다.
DST 처리. Postgres는 모호한 지역시를 기본적으로 거부한다. MySQL은 조용히 표준시를 고른다. Oracle은 명시적 DST를 위해 'US/Pacific PDT'를 받아들인다. CUBRID는 Oracle과 동일하다. 'America/New_York EDT'는 봄철 전환 이후 순간으로 풀린다. EST는 가을철 전환 이전 순간으로 풀린다.
갱신 프로토콜. Postgres는 릴리스마다 새 zoneinfo/를 함께 배포한다(인플레이스 교체, 디스크 형식 변경 없음). MySQL은 mysql_tzinfo_to_sql을 다시 돌린다. Oracle은 DBMS_DST.find_affected_tables로 영향 받는 컬럼을 열거한 뒤 upgrade_database로 다시 쓴다. CUBRID의 cubrid gen_tz -g extend는 Oracle에 가장 가깝다. 라이브러리를 다시 빌드하고, zone ID가 살아남았는지 검출하며(tzc_extend), 살아남지 못했다면 tzc_update → 행별 conv_tz를 호출한다.
CUBRID의 자리. Postgres보다 표면적이 작다(라이브러리 한 개, zone별 파일 없음). 디스크 형식은 더 무겁지만(zone 태그 값마다 +4 바이트) 읽어 들일 때 더 정확하다. Postgres TIMESTAMPTZ는 zone 이름을 손실 인코딩이지만, CUBRID DATETIMETZ는 정확히 왕복(round-trip)된다. 설계상 가장 가까운 친척은 Oracle이지만, Oracle의 식별자가 불투명한 반면 CUBRID TZ_ID는 구조적으로 해석 가능하다. 상위 비트, zone-name 인덱스, offset-rule 인덱스, DS-rule 인덱스 모두를 TZ_DATA 조회 없이 비트 마스크로 추출할 수 있고, 이것이 바로 conv_tz 업그레이드 마이그레이션을 가능하게 만든다.
CUBRID의 구현
섹션 제목: “CUBRID의 구현”이 서브시스템은 세 파일에 자리한다. src/base/timezone_lib_common.h는 컴파일러와 로더가 공유하는 자료구조(TZ_DATA, TZ_TIMEZONE, TZ_OFFSET_RULE, TZ_DS_RULE, …)를 정의한다. src/base/tz_compile.c(약 6,800 줄)는 빌드 시점 도구다. IANA 원본 파일을 파싱하고, 정렬·중복 제거하며, C 소스 timezones.c를 생성한다. 이 파일이 컴파일되어 libcubrid_timezones.so가 된다. src/base/tz_support.c(약 5,500 줄)는 런타임이다. 공유 라이브러리를 적재하고, TZ_ID를 인코딩·디코딩하며, 모든 DATETIMETZ/TIMESTAMPTZ 변환을 구동한다. 컴파일된 블롭에는 약 600개 명명 지역(대부분이 별칭이다. US/Eastern은 America/New_York의 별칭이다), 1,800개 오프셋 규칙, 250개 DS 규칙 집합, 500개 DS 규칙이 들어 있다. 이진 파일 크기는 약 1 MB다.
flowchart LR
subgraph compile_time["컴파일 시점 (cubrid gen_tz)"]
A[timezones/tzdata/<br/>africa europe asia ...<br/>leapseconds zone.tab] --> B[tzc_load_raw_data]
B --> C[TZ_RAW_DATA] --> D[tzc_compile_data] --> E[TZ_DATA]
E --> F[md5 체크섬] --> G[tzc_export_timezone_dot_c<br/>-> timezones.c]
G --> H[gcc -shared<br/>-> libcubrid_timezones.so]
end
H -. dlopen + dlsym .-> I
subgraph runtime["런타임"]
I[tz_load] --> J[static tz_Timezone_data] --> K[tz_get_data]
K --> L[tz_create_datetimetz<br/>tz_conv_tz_datetime_w_region<br/>tz_explain_tz_id]
end
TZ_DATA — 런타임 블롭
섹션 제목: “TZ_DATA — 런타임 블롭”핵심은 컴파일 시점에 할당된 단일 TZ_DATA 구조체로, 적재된 libcubrid_timezones.so를 dlsym으로 채워진다. 블롭 안의 모든 동적 구조는 평탄한 배열이다. 조회는 이진 검색이다. TZ_DATA(timezone_lib_common.h에 정의)는 다음을 들고 있다. countries(ISO 3166), timezones + timezone_names(정규 지역, 병렬 배열), offset_rules((UTC 오프셋, 종료 시점) 모든 레코드), names(모든 이름 — 정규 및 별칭, 정렬되어 있으며 tz_get_zone_id_by_name의 조회 인덱스 역할), ds_rulesets, ds_rules, ds_leap_sec, #if defined (WINDOWS) 아래의 선택적 windows_iana_map, 그리고 32 문자 checksum. 별칭은 tz_name.zone_id로 정규 지역으로 우회된다.
tz_load_data_from_lib는 모든 필드를 dlsym으로 해소한다. tz_country_count, tz_countries, timezone_count, tz_timezone_names, timezones, offset_rule_count, offset_rules, tz_name_count, tz_names, ds_ruleset_count, ds_rulesets, ds_rule_count, ds_rules, ds_leap_sec_count, ds_leap_sec, tz_timezone_checksum. 래퍼 TZLIB_GET_VAL(스칼라)과 TZLIB_GET_ADDR(포인터)는 어떤 심볼이라도 누락되면 error_loading_symbol:로 점프한다. 이 32개 이름이 컴파일러와 로더 사이에 하드코딩된 유일한 ABI 표면이다.
TZ_ID — zone, offset-rule, DS-rule을 32 비트로 묶기
섹션 제목: “TZ_ID — zone, offset-rule, DS-rule을 32 비트로 묶기”모든 DATETIMETZ/TIMESTAMPTZ가 디스크에 들고 다니는 식별자는 32 비트 부호 없는 정수이며, 상위 두 비트가 타입 태그다.
31 30 29 16 15 8 7 0+-----+--------------------+--------------+-------------+| F F | zone_id (10 bits) | offset_id | dst_id | F=00 : 지리적 지역+-----+--------------------+--------------+-------------+| 0 1 | positive offset (30-bit seconds) | F=01 : 원시 +오프셋+-----+------------------------------------------------+| 1 0 | negative offset (30-bit seconds) | F=10 : 원시 -오프셋+-----+------------------------------------------------+상수(tz_support.c와 tz_support.h): TZ_MASK_TZ_ID_FLAG = 0xc0000000, TZ_OFFSET_MASK = 0x3fffffff, TZ_ZONE_ID_MAX = 0x3ff(tzd->names로의 인덱스), TZ_OFFSET_ID_MAX = TZ_DS_ID_MAX = 0xff. 인코더 tz_encode_tz_id는 다음과 같다. TZ_REGION_OFFSET이면 절댓값을 TZ_OFFSET_MASK로 마스킹한 뒤 0x1 << 30(양수) 또는 0x2 << 30(음수)을 OR로 합친다. TZ_REGION_ZONE이면 dst_id | (offset_id << 8) | (zone_id << 16)을 OR로 합친다.
여기 인코딩된 세 가지 구조적 결정은 짚어 둘 가치가 있다. (1) TZ_ID = 0은 센티넬이 아니다. 지리적 지역, zone_id=0으로 디코딩된다(보통은 tzc_sort_raw_data 이후 Africa/Abidjan이다). 지역 없음을 표시하려는 코드는 지역을 TZ_ZONE_ID_MAX = 0x3ff를, 오프셋을 TZ_INVALID_OFFSET = 86399를 사용한다. (2) offset_id와 dst_id는 가장 최근 성공한 변환에서 캐시된 값이다. 이들은 0xff(아직 해소되지 않음)일 수 있는데, 그 경우 tz_datetime_utc_conv가 전체 탐색을 다시 수행하고 두 필드를 다시 쓴다. 이것이 DATETIMETZ 컬럼이 타임존 라이브러리의 extend를 견뎌 내는 이유다. zone_id 슬롯이 정규 앵커이고, 나머지 두 개는 읽을 때 다시 해소된다. (3) 두 가지 원시 오프셋 플래그(01, 10)는 2의 보수가 아닌 부호-크기 표현(signed magnitude) 을 운반한다. 따라서 비트 패턴이 부호 있는/없는 int 변환을 거쳐 JNI 경계를 넘어 Java PL 엔진으로 들어가도 살아남는다.
TZ_REGION — 세션/시스템 zone 설정
섹션 제목: “TZ_REGION — 세션/시스템 zone 설정”TZ_REGION은 TZ_ID의 해소 이전 형태다. zone_id 또는 고정된 부호 있는 오프셋 중 하나를 들고 있는 태그 합집합(TZ_REGION_OFFSET / TZ_REGION_ZONE)이다. 두 개의 전역이 시스템 전역 region과 세션 전역 region을 보유한다. 시스템 region은 모듈 정적 tz_Region_system이며, 서버 부팅 시 db_timezone으로부터 tz_set_tz_region_system으로 한 번 설정된다. 세션 region은 빌드 모드에 따라 다르다. 클라이언트 빌드에서는 단일 모듈 정적 tz_Region_session이고, 서버 빌드에서는 THREAD_ENTRY별로 tz_get_server_tz_region_session → session_get_session_tz_region (thread_p) 경로로 도달한다. 서버 측 함수는 특수 스레드를 폴백을 가진다. CDC 데몬은 시스템 region을 반환한다. 다른 스레드를 흉내 내는 워커(emulate_tid != thread_id_t ())는 흉내 대상의 region을 상속받는다. TT_VACUUM_WORKER는 시스템 region으로 폴백한다. 이 경로가 파서 수준의 SYSDATETIME(세션 region이 태깅된 시계 시각)을 행별 인코더로 잇는 길이다.
시계 시각 → UTC — 시스템의 심장
섹션 제목: “시계 시각 → UTC — 시스템의 심장”tz_datetime_utc_conv는 (datetime, zone) 쌍을 (UTC datetime, 완전히 해소된 offset_id, 완전히 해소된 dst_id)로 변환하는 단일 알고리즘이다. src_is_utc = true로 호출되면 UTC datetime을 다시 시계 시각으로 되돌리기도 한다. 약 900 줄 분량(2817 줄 시작)이며 세 단계의 중첩 탐색을 구현한다.
flowchart TD
S[src_dt, tz_info, src_is_utc] --> T0{type == OFFSET?}
T0 -- 예 --> X0[total_offset = tz_info.offset]
T0 -- 아니오 --> T2[gmt_off_rule_start..count 순회<br/>'until' 구간이 src_dt를 담는 규칙 선택]
T2 --> T3{ds_type}
T3 -- FIXED --> T4[total_offset = gmt_off + ds_ruleset]
T3 -- RULESET_ID --> T5[ds_ruleset 스캔; 각 규칙에 대해<br/>from/to/in_month로 연도 해소]
T5 --> T6[tz_get_ds_change_julian_date_diff]
T6 --> T7{|date_diff| < 2일?}
T7 -- 예 --> T8[tz_check_ds_match_string + 윤초 구간]
T7 -- 아니오 --> T9[가장 작은 양수 date_diff 승]
T8 --> T9
T9 --> T10[total_offset = gmt_off + save_time]
T4 --> T11[오프셋 적용 -> dest_dt]
T10 --> T11
X0 --> T11
T11 --> Z[dest_dt + 해소된 offset_id, dst_id]
첫 번째 탐색은 zone의 오프셋 규칙 리스트 위에서 일어난다. 대부분의 zone은 규칙이 하나다. Africa/Cairo는 여덟 개, Europe/Lisbon은 서른 개가 넘는다. 각 규칙은 [prev_rule.until, this_rule.until) 범위를 덮는다. 루프는 until이 src_dt 이후인 첫 규칙을 선택한다. until_flag = UNTIL_INFINITE는 TZ_MAX_JULIAN_DATE로 취급된다. break 조건은 src_julian_date <= rule_julian_date + 1이며, 이 +1은 안전 여유다. LOCAL_WALL until_time이 UTC로 해석될 때 최대 14 시간까지 밀려 율리우스력 경계를 넘을 수 있기 때문이다.
두 번째 탐색은 ds_type에 따라 달라진다. DS_TYPE_FIXED는 즉결된다. DS_TYPE_RULESET_ID는 DS 규칙 집합을 스캔하며 효력 발생일이 src_dt를 앞서는 가장 최근 규칙을 찾는다. 가지치기 조건. src_year + 1 < curr_ds_rule->from_year이면 break(오름차순 정렬). src_year - 1 > curr_ds_rule->to_year이면 차선 후보를 따로 저장한다. 살아남은 각 규칙을 tz_get_ds_change_julian_date_diff가 규칙의 효력 발생 율리우스 일자(연도는 get_year_to_apply_rule로 해소)와 원본 시각과의 date_diff를 산출한다. 일치 임계값. date_diff >= DATE_DIFF_MATCH_SAFE_THRESHOLD_SEC(≥ 2 일)이면 즉시 채택. |date_diff|가 더 작으면 깊이 있는 DST 문자열 매칭과 윤초 구간 검사를 트리거한다.
세 번째 탐색은 AT 시각 한정자 의 춤이다. IANA는 모든 at_time에 s/w/u/g/z를 태깅한다. CUBRID는 이를 TZ_TIME_TYPE_LOCAL_STD, TZ_TIME_TYPE_LOCAL_WALL, TZ_TIME_TYPE_UTC로 사상한다. tz_offset (src_is_utc, until_time_type, gmt_offset_sec, ds_save_time)은 규칙 경계를 넘을 때 적용할 오프셋을 만들어 준다. src_dt를 규칙의 기준 좌표계로 옮기거나(src_is_utc=true) 규칙의 until을 시계 시각으로 옮긴다(src_is_utc=false). src_is_utc=true일 때. LOCAL_STD는 gmt_offset_sec을 더하고, LOCAL_WALL은 gmt_offset_sec + ds_save_time을 더한다. src_is_utc=false일 때. UTC는 gmt_offset_sec + ds_save_time을 빼고, LOCAL_STD는 ds_save_time을 뺀다. 여기서 ds_save_time은 규칙이 활성이라면 얼마를 더해 줄 것인지를 가리킨다.
폴백(중복) 경로는 tz_check_ds_match_string에 도달한다. 사용자의 선택적 dst_str(예: 'America/New_York EDT'의 EDT)을 세 가지 후보 형식 문자열과 비교한다. var_format은 %s 템플릿 형태(E%sT → EST/EDT). save_format은 명시적 DST 문자열. std_format은 명시적 표준 문자열. letter_abbrev는 일치한 DS 규칙의 LETTER 컬럼(많은 북미 지역에서 S/D/W)에서 가져오며, '-'은 약자 없음으로 다룬다.
TIMESTAMP vs DATETIME — 두 인코딩, 같은 알고리즘
섹션 제목: “TIMESTAMP vs DATETIME — 두 인코딩, 같은 알고리즘”CUBRID에는 TZ 계층이 다루는 여섯 개 datetime 타입이 있다.
| 타입 | 레이아웃 | LTZ 변형? |
|---|---|---|
DATE | int32 율리우스 일자 | 해당 없음 |
TIME | int32 하루의 밀리초 | 해당 없음 |
DATETIME | { int32 율리우스, int32 하루의 ms } | DATETIMELTZ |
DATETIMETZ | DATETIME (UTC) + 4 바이트 TZ_ID | — |
TIMESTAMP | int32 UTC epoch 이후 초 | TIMESTAMPLTZ |
TIMESTAMPTZ | TIMESTAMP (UTC) + 4 바이트 TZ_ID | — |
L 변형은 디스크에 zone 태그를 들고 있지 않다. UTC만 저장하고 읽을 때 현재 세션 zone을 다시 적용한다. CUBRID 식 PostgreSQL TIMESTAMP WITH TIME ZONE이라 할 수 있다. 절대 시각 측면에서는 왕복 안정적이지만, zone 이름 측면에서는 손실된다. 두 *TZ 변형은 변환 기계를 공유한다. DATETIMETZ는 tz_create_datetimetz를 직접 사용하고, TIMESTAMPTZ는 먼저 db_timestamp_encode_utc를 거쳐 날짜와 하루의 시각을 int32 epoch 표현으로 압축한다.
tz_create_datetimetz는 선택적 tz_str(또는 default_tz_region으로 폴백)을 TZ_DECODE_INFO로 디코딩하고, tz_datetime_utc_conv로 시계 시각을 UTC로 변환한 다음, tz_encode_tz_id로 해소된 트리플을 묶는다. tz_create_timestamptz는 같은 흐름에 한 단계가 더 붙는다. DB_TIME(초)을 밀리초로 끌어올리고, 변환을 돌리고, 다시 나누고, db_timestamp_encode_utc가 UTC 날짜+시각을 4 바이트 epoch로 묶는다. 차이점은 끝의 저장 압축뿐이다.
지역 간 변환 — tz_conv_tz_datetime_w_region
섹션 제목: “지역 간 변환 — tz_conv_tz_datetime_w_region”지역 간 변환은 tz_datetime_utc_conv를 두 번 통과시키는 작업이다. 출발 지역 wall → UTC, UTC → 도착 지역 wall. tz_conv_tz_datetime_w_zone_info는 출발이나 도착이 이미 UTC(TZ_REGION_OFFSET && offset == 0)일 때 한 번의 통과를 압축하고, 출발과 도착이 같은 zone일 때는 단락(short-circuit)한다. 출발 datetime이 글자 그대로 복사되고 해소된 TZ_DECODE_INFO가 그대로 전파된다. 같은 zone 단락은 출발 tz_id를 정확히 보존한다. 이것이 없다면 무동작 CONVERT_TZ('America/New_York', 'America/New_York', x)도 offset/DS 서브 ID를 다시 해소해 다른 TZ_ID를 만들어 낼 수 있다. 공개 표면. tz_conv_tz_datetime_w_region(TZ_REGION → TZ_REGION)과 tz_conv_tz_datetime_w_zone_name(문자열 → 문자열).
라이브러리 컴파일 — tzdata 원본에서 libcubrid_timezones.so까지
섹션 제목: “라이브러리 컴파일 — tzdata 원본에서 libcubrid_timezones.so까지”컴파일 경로는 cubrid gen_tz에서 시작된다(진입: timezone_compile_data, SA 모드 전용). 고정된 파이프라인. tzc_check_new_package_validity(필수 파일 검증), tzc_load_raw_data(africa, europe, … 파싱해서 TZ_RAW_DATA로), tzc_import_old_data(extend 모드에서 기존 라이브러리의 zone ID를 접목), tzc_del_unused_raw_data, tzc_sort_raw_data, tzc_index_data(zone_id / offset_id / ds_id 부여), tzc_compile_data(TZ_RAW_DATA → TZ_DATA). Windows 환경에서는 tzc_load_windows_iana_map이 추가된다. TZ_GEN_TYPE_EXTEND 모드에서는 tzc_extend가 이전 zone_id 순서를 보존한다. ER_TZ_COMPILE_ERROR로 떨어지면 tzc_update로 진입해 사용자 데이터를 마이그레이션한다. 마지막으로 tzc_compute_timezone_checksum(md5)과 tzc_export_timezone_dot_c가 C 소스를 출력한다.
산출물 timezones.c는 약 2 MB이며 런타임이 기대하는 정확한 심볼들을 담는다. int tz_country_count = …;, TZ_COUNTRY tz_countries[] = { … };, … char tz_timezone_checksum[] = …;. make_tz.sh가 오케스트레이션한다. extend 모드에서는 $CUBRID_DATABASES/databases.txt의 모든 데이터베이스를 순회하며 cubrid gen_tz -g extend $DATABASE_NAME을 돌린다(내부적으로 모든 DATETIMETZ/TIMESTAMPTZ 컬럼을 순회하고, zone의 zone_id가 살아남지 못하면 conv_tz로 다시 쓴다). new 모드에서는 한 번만 돌린다. 그다음 timezones/tzlib/build_tz.sh가 gcc -shared -o libcubrid_timezones.so timezones.c를 호출하고 .so를 $CUBRID/lib로 옮긴다.
마이그레이션 — conv_tz
섹션 제목: “마이그레이션 — conv_tz”tzdata 갱신이 zone 표현을 바꿀 만큼 심각할 때(규칙 집합 재구성, 오프셋 규칙 분할 등), tzc_extend는 ER_TZ_COMPILE_ERROR를 반환하고 tzc_update가 conv_tz로 행별 재작성을 트리거한다. DB_TYPE별로 디스패치한다.
DB_TYPE_TIMESTAMPTZ 흐름이 대표적이다. tz_decode_tz_id가 트리플을 복원한다. 원시 오프셋 행(TZ_REGION_OFFSET)은 그대로 복사된다. 오프셋은 tzdata 릴리스를 가로질러 변하지 않는다. 그렇지 않은 경우 함수는 tz_Is_backward_compatible_timezone[ZONE_MAX](이는 tzc_extend가 채워 둔 비트맵이다)를 참조한다. true이면 마이그레이션은 순수한 이름 조회 재사상이다. set_new_zone_id가 zone_id 숫자만 바꾸고 디스크의 datetime은 건드리지 않는다. false이면 행의 UTC datetime이 디코딩되고, 새 라이브러리에서 zone 이름을 조회한 뒤 tz_datetime_utc_conv가 offset_id/dst_id를 다시 해소한다. 새 tzdata가 그 행의 DST 약자(예: 러시아 2011년 DST 폐지 이후 MSD)를 떨어뜨렸다면, 루프는 빈 dst_str로 재시도한다. 해소된 DST가 더 이상 사용자 의도와 맞지 않음을 받아들이는 대신 절대 UTC 값은 보존한다.
나머지 세 타입은 변형이다. DB_TYPE_DATETIMETZ는 epoch 압축이 없는 같은 흐름이다. DB_TYPE_TIMESTAMPLTZ와 DB_TYPE_DATETIMELTZ는 세션 zone으로 다시 태깅하며, 세션 zone 자체가 바뀐 경우에만 변환한다.
tz_explain_tz_id — 해소된 트리플을 SQL에 노출
섹션 제목: “tz_explain_tz_id — 해소된 트리플을 SQL에 노출”TZ_OFFSET(), DBTIMEZONE, 그리고 format='YYYY-MM-DD HH:MI:SS TZR TZD TZH:TZM' 포매터는 tz_explain_tz_id로 사람이 읽을 수 있는 형태를 복원한다. 이 함수는 tz_decode_tz_id (tz_id, true, &tz_info)(완전 디코딩 — p_zone_off_rule과 p_ds_rule이 채워짐)를 호출한다. 원시 오프셋 TZ_ID의 경우 두 문자열 출력은 비어 있고 tzh/tzm은 tz_info.offset / 3600과 (tz_info.offset % 3600) / 60에서 온다. 지리적 지역의 경우 총 오프셋은 p_zone_off_rule->gmt_off + (ds_type == DS_TYPE_RULESET_ID && p_ds_rule != NULL ? p_ds_rule->save_time : 0). tzr은 tzd->names[zone_id].name. tzdst는 tz_info.zone.dst_str에서 오는데, 이는 오프셋 규칙으로부터 다시 계산되는 것이 아니라, tz_decode_tz_id가 일치한 DS 규칙의 letter_abbrev를 var_format을 포매팅해 채워 넣은 값이다. 이것이 DST 약자에 도달하는 유일한 직접 표면이다.
OS 지역 검출 — tz_resolve_os_timezone
섹션 제목: “OS 지역 검출 — tz_resolve_os_timezone”CUBRID가 호스트 zone을 해소할 때(부팅 시 또는 cubrid_timezone='SYSTEM'), tz_resolve_os_timezone이 OS별로 디스패치한다. Linux. find_timezone_from_clock이 /etc/sysconfig/clock의 ZONE=...(Red-Hat)을 읽는다. 폴백으로 find_timezone_from_localtime(/etc/localtime의 심볼릭 링크 대상 — Debian. /usr/share/zoneinfo/ 아래의 경로 꼬리가 IANA 이름이다). AIX. $TZ. Windows. _get_tzname이 Windows 이름(Pacific Standard Time)을 반환하고, 이를 tzd->windows_iana_map(tzc_load_windows_iana_map이 windowsZones.xml로부터 채운 것)에서 조회한다. 함수는 zone_id(즉 tzd->names로의 인덱스)를 반환한다. 이름 자체가 아니다.
호환성 체크섬
섹션 제목: “호환성 체크섬”모든 TZ_DATA는 tzc_compute_timezone_checksum이 마샬링된 이진 위에서 계산한 32 문자 md5 체크섬을 들고 있다. check_timezone_compat은 접속 시 클라이언트와 서버 체크섬을 비교한다. 불일치이면 ER_TZ_INCOMPATIBLE_TZ_LIB를 일으킨다. 이 검사가 막는 실패 모드는 다음과 같다. Asia/Seoul = zone_id 41인 클라이언트가 그것이 43인 서버와 만나면, 모든 DATETIMETZ 왕복이 조용히 잘못 인코딩된다. 해결은 양쪽을 동일한 tzdata를 다시 빌드하거나 동일한 libcubrid_timezones.so를 배포하는 것이다.
소스 코드 가이드
섹션 제목: “소스 코드 가이드”심볼들을 호출 흐름으로 묶었다.
라이브러리 적재. tz_load / tz_unload가 libcubrid_timezones.so를 열고 닫으며 tz_Timezone_data를 채운다. tz_load_library가 dlopen을 감싸고, tz_load_data_from_lib가 TZLIB_GET_VAL/TZLIB_GET_ADDR로 모든 심볼을 dlsym한다. tz_get_data / tz_set_data는 접근자다. tz_get_new_timezone_data / tz_set_new_timezone_data는 extend 마이그레이션 동안 두 번째 블롭을 끼워 넣는다.
Region(세션/시스템). tz_set_tz_region_system, tz_get_system_tz_region, tz_get_system_timezone. tz_get_session_tz_region은 tz_get_client_tz_region_session(CS) 또는 tz_get_server_tz_region_session(SVR)을 호출한다. tz_get_utc_tz_id, tz_get_utc_tz_region, tz_get_invalid_tz_region은 센티넬 생성자다. tz_str_to_region은 '+08:00' / 'Europe/Berlin' / 'Europe/Berlin EEST'를 파싱한다.
TZ_ID 인코딩/디코딩. tz_encode_tz_id / tz_decode_tz_id가 32 비트 식별자를 묶고 푼다. tz_encode_tz_region / tz_decode_tz_region은 TZ_REGION 용이다. tz_id_to_str은 'America/New_York EDT'를 렌더링한다. tz_get_zone_id_by_name은 이진 검색이고, tz_get_best_match_zone은 접두 매칭 변형이다. tz_tzid_convert_region_to_offset은 트리플을 절대 오프셋 하나로 무너뜨린다.
Wall ↔ UTC(심장). tz_datetime_utc_conv(src_is_utc로 양방향 게이트). tz_offset(LOCAL_STD / LOCAL_WALL / UTC 한정자 삼분기). tz_get_ds_change_julian_date_diff는 DS 규칙의 ON 컬럼(lastSun, Fri>=24)을 일자 차이로 변환한다. tz_get_first_weekday_around_date는 그 아래의 해소자다. tz_fast_find_ds_rule은 두 번째 패스의 DS 스캔이다. tz_check_ds_match_string은 dst_str을 var_format / save_format / std_format에 매칭한다. 보조 술어. get_date_diff_from_ds_rule, get_closest_ds_rule, get_saving_time_from_offset_rule, get_year_to_apply_rule, is_in_overlap_interval.
타입 생성 진입점(string_opfunc.c에서 호출). tz_create_datetimetz, tz_create_timestamptz(시계 시각 + tz 문자열). tz_create_datetimetz_from_ses(세션 zone). *_from_offset(±HH:MM). *_from_zoneid_and_tzd(zone-id + DST 약자). tz_create_datetimetz_from_utc(UTC + 도착 region). tz_create_datetimetz_from_parts. 세션 tzid 변형. tz_create_session_tzid_for_{datetime,timestamp,time}.
지역 간 & explain. tz_conv_tz_datetime_w_region(CONVERT_TZ가 사용), tz_conv_tz_datetime_w_zone_name, tz_conv_tz_time_w_zone_name, tz_conv_tz_datetime_w_zone_info(내부). tz_utc_datetimetz_to_local, tz_datetimeltz_to_local은 역방향이다. tz_datetimetz_fix_zone, tz_timestamptz_fix_zone은 서브 ID를 다시 해소한다. tz_explain_tz_id는 (TZR, TZD, TZH, TZM)을 노출한다. tz_get_timezone_offset은 (tz_str, utc_dt) → offset 단축 호출이다.
윤초. PRM_ID_TZ_LEAP_SECOND_SUPPORT로 게이팅된다(기본 비활성). tz_get_leapsec_support, tz_timestamp_encode_leap_sec_adj, tz_timestamp_decode_leap_sec_adj, tz_timestamp_decode_no_leap_sec, tz_timestamp_decode_sec.
OS zone 검출. tz_resolve_os_timezone이 OS별 디스패처. Linux. find_timezone_from_clock, find_timezone_from_localtime. Windows. tz_get_iana_zone_id_by_windows_zone.
마이그레이션. conv_tz(행별 재작성). set_new_zone_id(이름 기반 재사상). put_timezone_checksum / check_timezone_compat(접속 핸드셰이크). tz_check_geographic_tz, tz_check_session_has_geographic_tz(오프셋 전용 세션 가드).
컴파일 시점 도구(tz_compile.c). timezone_compile_data(SA 모드 전용 최상위). 파이프라인. tzc_check_new_package_validity, tzc_load_raw_data, tzc_load_countries, tzc_load_zone_names, tzc_load_rule_file, tzc_load_backward_zones, tzc_load_leap_secs, tzc_add_{zone,link,offset_rule,ds_rule,leap_sec}. 파서. tzc_parse_ds_change_on, tzc_read_time_type, str_to_offset_rule_until, str_month_to_int, str_day_to_int, str_read_day_var. 그다음 tzc_sort_raw_data, tzc_index_data, tzc_compile_data, tzc_compile_ds_rules, tzc_extend, tzc_update, tzc_update_internal, tzc_compute_timezone_checksum, tzc_export_timezone_dot_c. Windows 전용. tzc_load_windows_iana_map, xml_start_mapZone. 디버그. tzc_dump_{summary,countries,timezones,one_timezone,leap_sec}.
SQL 계층 표면. db_to_datetimetz → tz_create_datetimetz_from_{parts,offset,zoneid_and_tzd}. db_conv_tz → conv_tz. db_format(%TZR %TZD %TZH:%TZM) → tz_explain_tz_id. db_to_char_datetimetz는 명시적 DST를 렌더링한다. 빌트인 CONVERT_TZ → tz_conv_tz_datetime_w_zone_name. NEW_TIME → tz_conv_tz_time_w_zone_name.
위치 힌트(현 리비전 기준)
섹션 제목: “위치 힌트(현 리비전 기준)”| 심볼 | 파일 | 줄 |
|---|---|---|
TZ_DATA / TZ_OFFSET_RULE / TZ_DS_RULE | src/base/timezone_lib_common.h | 198 |
TZ_TIME_TYPE / TZ_GEN_TYPE | src/base/timezone_lib_common.h | 55 |
TZ_REGION / TZ_ID | src/compat/dbtype_def.h | 774 |
TZ_DECODE_INFO / TZ_MASK_TZ_ID_FLAG | src/base/tz_support.c | 58 |
tz_load / tz_load_data_from_lib | src/base/tz_support.c | 282 |
tz_get_zone_id_by_name | src/base/tz_support.c | 1110 |
tz_str_timezone_decode / tz_str_to_region | src/base/tz_support.c | 1171 |
tz_create_datetimetz / tz_create_timestamptz | src/base/tz_support.c | 1384 |
tz_utc_datetimetz_to_local | src/base/tz_support.c | 1572 |
tz_encode_tz_id / tz_decode_tz_id | src/base/tz_support.c | 1892 |
tz_get_ds_change_julian_date_diff | src/base/tz_support.c | 2334 |
tz_fast_find_ds_rule / tz_check_ds_match_string | src/base/tz_support.c | 2389 |
tz_offset / get_year_to_apply_rule | src/base/tz_support.c | 2560 |
tz_datetime_utc_conv | src/base/tz_support.c | 2816 |
tz_conv_tz_datetime_w_zone_info / _w_region | src/base/tz_support.c | 3731 |
tz_explain_tz_id | src/base/tz_support.c | 3894 |
tz_create_datetimetz_from_offset / _zoneid_tzd | src/base/tz_support.c | 4069 |
tz_resolve_os_timezone | src/base/tz_support.c | 4589 |
tz_get_server_tz_region_session | src/base/tz_support.c | 4676 |
check_timezone_compat | src/base/tz_support.c | 5050 |
tz_tzid_convert_region_to_offset | src/base/tz_support.c | 5075 |
set_new_zone_id / conv_tz | src/base/tz_support.c | 5184 |
timezone_compile_data | src/base/tz_compile.c | 637 |
tzc_load_raw_data / tzc_compile_data | src/base/tz_compile.c | 866 |
tzc_export_timezone_dot_c / tzc_extend | src/base/tz_compile.c | 4215 |
tzc_compute_timezone_checksum / tzc_update | src/base/tz_compile.c | 6273 |
소스 검증 노트
섹션 제목: “소스 검증 노트”TZ_GEN_TYPE_UPDATE는 선언되어 있지만make_tz.sh에서 호출되지 않는다. 셸 스크립트는-g new또는-g extend만 넘긴다.update모드는 후방 비호환 변경이 검출될 때만tzc_extend → tzc_update_internal으로 도달 가능하다.TZ_TIME_TYPE_UTC는 세 IANA 접미사를 한데 묶는다(u,g,z— IANA 관례상 별칭).tz_get_offset_in_mins는 DST를 무시한다(5483 줄에 TODO). DST 인지 오프셋은 반드시tz_explain_tz_id를 거쳐야 한다.is_full_decode = false이면 포인터 필드는 비어 있다.zone_id/offset_id/dst_id만 채워진다.p_timezone,p_zone_off_rule,p_ds_rule은NULL. 그 안으로 손을 뻗으면 segfault다.tz_Is_backward_compatible_timezone은SA_MODE전용이다. 비트맵은 standalone 빌드에서만 채워진다.conv_tz안의#if defined (SA_MODE)경로는CS_MODE/SERVER_MODE에서 도달 불가다.- 오프셋 규칙 순회의 +1 일 안전 여유는 옳다.
LOCAL_WALLuntil_time이 UTC로 해석될 때 최대 14 시간까지 밀려 율리우스력 경계를 넘을 수 있다. TZ_ID = 0은 유효한 zone이다. 센티넬이 아니다.tzd->names의 첫 이름으로 디코딩된다.tz_get_invalid_tz_region/TZ_INVALID_OFFSET을 사용해야 한다.leapseconds는 파싱되지만 파라미터로 게이팅된다.tzc_load_leap_secs는 항상tzd->ds_leap_sec을 채우지만, 런타임 보정은PRM_ID_TZ_LEAP_SECOND_SUPPORT가 켜져 있을 때만(기본 꺼짐) 일어난다.
열린 질문
섹션 제목: “열린 질문”gen_tz extend에서dst_id가 새 규칙 집합에 더 이상 존재하지 않는 행을 만나면?conv_tz는 빈dst_str로 재시도하지만, 중복 분기는 조용히 뒤집혀 값이save_time(약 1시간)만큼 이동할 수 있다. 이 표류는 어디엔가 로깅되는가?gmt_off_rule_count > 0은 보장되는가? 2886 줄의 assertion이 런타임에는 강제하지만, 원리상tzc_del_unused_raw_data가 어떤 zone의 모든 규칙을 걸러 낼 수도 있다.tz_get_offset_in_mins가 사용자 가시 결과에 영향을 주는가? 호출자는 진단용뿐이다. (DST 처리를 고치는 대신) 제거하는 편이 더 깔끔할 수도 있다.db_timezone='Asia/Seoul'이지만 OS 호스트가America/New_York이면SYSTIMESTAMP는 어느 쪽이 이기는가? 코드를 보면 시스템 region이 이긴다.tz_resolve_os_timezone은db_timezone='SYSTEM'이 문자 그대로일 때만 참조된다.tz_Compare_datetimetz_tz_id/tz_Compare_timestamptz_tz_idSA 모드 전역. 동일한 UTC datetime이지만tz_id가 다른 두 값이 동등 비교되는지는 타임존 모듈만 봐서는 알 수 없다.mr_*_tztimestamp/mr_*_tzdatetime까지 추적이 필요하다.
src/base/tz_support.{h,c},src/base/tz_compile.{h,c},src/base/timezone_lib_common.hsrc/compat/dbtype_def.h(TZ_REGION,TZ_ID,DATETIMETZ,TIMESTAMPTZ)src/query/string_opfunc.c(SQL 진입점:db_to_datetimetz,db_conv_tz,db_format,CONVERT_TZ,NEW_TIME)timezones/tzdata/(IANA 원본:africa,antarctica,asia,australasia,backward,etcetera,europe,northamerica,pacificnew,southamerica,iso3166.tab,leapseconds,zone.tab,windowsZones.xml)timezones/make_tz.sh,timezones/tzlib/build_tz.sh- IANA Time Zone Database (https://www.iana.org/time-zones) — 정본 입력 사양.