diff --git a/stationapi/src/infrastructure/station_repository.rs b/stationapi/src/infrastructure/station_repository.rs index 56d3bef1..e46809e1 100644 --- a/stationapi/src/infrastructure/station_repository.rs +++ b/stationapi/src/infrastructure/station_repository.rs @@ -12,11 +12,6 @@ use crate::{ proto::StopCondition, }; -#[derive(sqlx::FromRow)] -struct TrainTypesCountRow { - train_types_count: Option, -} - #[derive(sqlx::FromRow)] struct ConnectedRoutePatternStopRow { line_group_cd: i32, @@ -330,25 +325,13 @@ impl StationRepository for MyStationRepository { direction_id: Option, ) -> Result, DomainError> { let mut conn = self.pool.acquire().await?; - match station_id { - Some(station_id) => { - InternalStationRepository::get_by_line_id_and_station_id( - line_id, - station_id, - direction_id, - &mut conn, - ) - .await - } - None => { - InternalStationRepository::get_by_line_id_without_train_types( - line_id, - direction_id, - &mut conn, - ) - .await - } - } + InternalStationRepository::get_by_line_id_with_train_type( + line_id, + station_id, + direction_id, + &mut conn, + ) + .await } async fn get_by_line_id_vec(&self, line_ids: &[u32]) -> Result, DomainError> { let mut conn = self.pool.acquire().await?; @@ -532,29 +515,6 @@ impl InternalStationRepository { }) .collect()) } - - async fn fetch_has_local_train_types_by_station_id( - id: u32, - conn: &mut PgConnection, - ) -> Result { - let row: TrainTypesCountRow = sqlx::query_as!( - TrainTypesCountRow, - "SELECT COUNT(sst.line_group_cd)::integer AS train_types_count - FROM station_station_types AS sst - JOIN types AS t ON t.type_cd = sst.type_cd - WHERE sst.station_cd = $1 - AND ( - t.kind IN (0, 1) - OR t.priority > 0 - )", - id as i32, - ) - .fetch_one(conn) - .await?; - - Ok(row.train_types_count.unwrap_or(0) > 0) - } - async fn find_by_id(id: u32, conn: &mut PgConnection) -> Result, DomainError> { let rows: Option = sqlx::query_as!( StationRow, @@ -759,99 +719,6 @@ impl InternalStationRepository { Ok(stations) } - async fn get_by_line_id_without_train_types( - line_id: u32, - direction_id: Option, - conn: &mut PgConnection, - ) -> Result, DomainError> { - // When direction_id = 1 (上り) or 2 (下り), reverse the order - let order_clause = if matches!(direction_id, Some(1) | Some(2)) { - "ORDER BY s.e_sort DESC, s.station_cd DESC" - } else { - "ORDER BY s.e_sort ASC, s.station_cd ASC" - }; - - let query_str = format!( - r#"SELECT - s.station_cd, - s.station_g_cd, - s.station_name, - s.station_name_k, - s.station_name_r, - s.station_name_rn, - s.station_name_zh, - s.station_name_ko, - s.station_number1, - s.station_number2, - s.station_number3, - s.station_number4, - s.three_letter_code, - s.line_cd, - s.pref_cd, - s.post, - s.address, - s.lon, - s.lat, - s.open_ymd, - s.close_ymd, - s.e_status, - s.e_sort, - l.company_cd, - COALESCE(NULLIF(COALESCE(a.line_name, l.line_name), ''), NULL) AS line_name, - COALESCE(NULLIF(COALESCE(a.line_name_k, l.line_name_k), ''), NULL) AS line_name_k, - COALESCE(NULLIF(COALESCE(a.line_name_h, l.line_name_h), ''), NULL) AS line_name_h, - COALESCE(NULLIF(COALESCE(a.line_name_r, l.line_name_r), ''), NULL) AS line_name_r, - COALESCE(NULLIF(COALESCE(a.line_name_zh, l.line_name_zh), ''), NULL) AS line_name_zh, - COALESCE(NULLIF(COALESCE(a.line_name_ko, l.line_name_ko), ''), NULL) AS line_name_ko, - COALESCE(NULLIF(COALESCE(a.line_color_c, l.line_color_c), ''), NULL) AS line_color_c, - l.line_type, - l.line_symbol1, - l.line_symbol2, - l.line_symbol3, - l.line_symbol4, - l.line_symbol1_color, - l.line_symbol2_color, - l.line_symbol3_color, - l.line_symbol4_color, - l.line_symbol1_shape, - l.line_symbol2_shape, - l.line_symbol3_shape, - l.line_symbol4_shape, - COALESCE(l.average_distance, 0.0)::DOUBLE PRECISION AS average_distance, - NULL::int AS type_id, - NULL::int AS sst_id, - NULL::int AS type_cd, - NULL::int AS line_group_cd, - NULL::int AS pass, - NULL::text AS type_name, - NULL::text AS type_name_k, - NULL::text AS type_name_r, - NULL::text AS type_name_zh, - NULL::text AS type_name_ko, - NULL::text AS color, - NULL::int AS direction, - NULL::int AS kind, - s.transport_type - FROM stations AS s - JOIN lines AS l ON l.line_cd = s.line_cd - LEFT JOIN line_aliases AS la ON la.station_cd = s.station_cd - LEFT JOIN aliases AS a ON a.id = la.alias_cd - WHERE l.line_cd = $1 - AND s.e_status = 0 - AND l.e_status = 0 - {order_clause}"# - ); - - let rows = sqlx::query_as::<_, StationRow>(&query_str) - .bind(line_id as i32) - .fetch_all(conn) - .await?; - - let stations: Vec = rows.into_iter().map(|row| row.into()).collect(); - - Ok(stations) - } - async fn get_by_line_id_vec( line_ids: &[u32], conn: &mut PgConnection, @@ -1054,31 +921,27 @@ impl InternalStationRepository { Ok(stations) } - async fn get_by_line_id_and_station_id( + async fn get_by_line_id_with_train_type( line_id: u32, - station_id: u32, + station_id: Option, direction_id: Option, conn: &mut PgConnection, ) -> Result, DomainError> { - let stations: Vec = match Self::fetch_has_local_train_types_by_station_id( - station_id, conn, - ) - .await? - { - true => { - // When direction_id = 1 (上り) or 2 (下り), reverse the order - let order_clause = if matches!(direction_id, Some(1) | Some(2)) { - "ORDER BY sst.id DESC" - } else { - "ORDER BY sst.id ASC" - }; - - let query_str = format!( - r#"WITH target_line_group AS ( + // When direction_id = 1 (上り) or 2 (下り), reverse the order + let order_clause = if matches!(direction_id, Some(1) | Some(2)) { + "ORDER BY sst.id DESC" + } else { + "ORDER BY sst.id ASC" + }; + + let query_str = format!( + r#"WITH target_line_group AS ( SELECT sst_inner.line_group_cd FROM station_station_types AS sst_inner LEFT JOIN types AS t_inner ON sst_inner.type_cd = t_inner.type_cd - WHERE sst_inner.station_cd = $1 + JOIN stations AS seed_station ON seed_station.station_cd = sst_inner.station_cd + WHERE seed_station.line_cd = $1 + AND ($2::int IS NULL OR sst_inner.station_cd = $2) AND ( (t_inner.priority > 0 AND sst_inner.pass <> 1 AND sst_inner.type_cd = t_inner.type_cd) OR (NOT (t_inner.priority > 0 AND sst_inner.pass <> 1) AND t_inner.kind IN (0,1)) @@ -1154,16 +1017,14 @@ impl InternalStationRepository { WHERE s.e_status = 0 AND l.e_status = 0 {order_clause}"# - ); + ); - let rows = sqlx::query_as::<_, StationRow>(&query_str) - .bind(station_id as i32) - .fetch_all(conn) - .await?; - rows.into_iter().map(|row| row.into()).collect() - } - false => Self::get_by_line_id_without_train_types(line_id, direction_id, conn).await?, - }; + let rows = sqlx::query_as::<_, StationRow>(&query_str) + .bind(line_id as i32) + .bind(station_id.map(|id| id as i32)) + .fetch_all(conn) + .await?; + let stations = rows.into_iter().map(|row| row.into()).collect(); Ok(stations) } diff --git a/stationapi/src/use_case/interactor/query.rs b/stationapi/src/use_case/interactor/query.rs index 603b4aaa..88760099 100644 --- a/stationapi/src/use_case/interactor/query.rs +++ b/stationapi/src/use_case/interactor/query.rs @@ -1059,31 +1059,13 @@ where to_station_id: u32, line_group_id: Option, ) -> Result, UseCaseError> { - // line_group_id 未指定は種別なし(各駅停車)の単一路線走行。 - // from駅の所属路線の駅列をそのまま経路として扱う。 - let stations = match line_group_id { - Some(line_group_id) => { - self.get_stations_by_line_group_id(line_group_id, TransportTypeFilter::RailAndBus) - .await? - } - None => { - let from_station = self - .station_repository - .find_by_id(from_station_id) - .await? - .ok_or_else(|| UseCaseError::NotFound { - entity_type: "station", - entity_id: from_station_id.to_string(), - })?; - self.get_stations_by_line_id( - from_station.line_cd as u32, - None, - None, - TransportTypeFilter::RailAndBus, - ) - .await? - } - }; + let line_group_id = line_group_id.ok_or_else(|| UseCaseError::NotFound { + entity_type: "line group", + entity_id: "unspecified".to_string(), + })?; + let stations = self + .get_stations_by_line_group_id(line_group_id, TransportTypeFilter::RailAndBus) + .await?; let from_idx = stations .iter() @@ -1902,17 +1884,22 @@ where } fn build_route_tree_map<'a>(&self, stops: &'a [Station]) -> BTreeMap> { - stops.iter().fold( - BTreeMap::new(), - |mut acc: BTreeMap>, value| { - if let Some(line_group_cd) = value.line_group_cd { - acc.entry(line_group_cd).or_default().push(value); - } else { - acc.entry(value.line_cd).or_default().push(value); - }; - acc - }, - ) + stops + .iter() + .map(|stop| { + ( + stop.line_group_cd + .expect("route stop must belong to a train type group"), + stop, + ) + }) + .fold( + BTreeMap::new(), + |mut acc: BTreeMap>, (line_group_cd, stop)| { + acc.entry(line_group_cd).or_default().push(stop); + acc + }, + ) } fn build_station_from_row( @@ -3028,7 +3015,7 @@ mod tests { data.iter() .enumerate() .map(|(i, &(cd, lat, lon))| { - let mut s = create_test_station(cd, 9930100, 99301, None); + let mut s = create_test_station(cd, 9930100, 99301, Some(9930100)); s.lat = lat; s.lon = lon; s.e_sort = 9930101 + i as i32; @@ -3107,7 +3094,7 @@ mod tests { data.iter() .enumerate() .map(|(i, &(cd, lat, lon))| { - let mut s = create_test_station(cd, cd, 28008, None); + let mut s = create_test_station(cd, cd, 28008, Some(2800800)); s.lat = lat; s.lon = lon; s.e_sort = 2800801 + i as i32; @@ -3126,7 +3113,8 @@ mod tests { /// 修正前は種別倍率(×1.15)が掛かり駅間別較正も外れて約15分に縮んでいた。 #[tokio::test] async fn test_estimate_route_arrival_times_through_express_all_stops_matches_local() { - let local = build_interactor(hanzomon_stops(None), vec![], vec![], vec![]); + let default_kind = Some(proto::TrainTypeKind::Default as i32); + let local = build_interactor(hanzomon_stops(default_kind), vec![], vec![], vec![]); let local_est = local .estimate_route_arrival_times(2800813, 2800807, &[], None) .await @@ -3464,18 +3452,12 @@ mod tests { } #[test] - fn test_build_route_tree_map_groups_by_line_cd_when_no_line_group() { + #[should_panic(expected = "route stop must belong to a train type group")] + fn test_build_route_tree_map_requires_line_group() { let interactor = create_interactor(); - let stops = vec![ - create_test_station(1, 1, 100, None), - create_test_station(2, 2, 100, None), - create_test_station(3, 3, 200, None), - ]; - let result = interactor.build_route_tree_map(&stops); + let stops = vec![create_test_station(1, 1, 100, None)]; - assert_eq!(result.len(), 2); - assert_eq!(result.get(&100).unwrap().len(), 2); - assert_eq!(result.get(&200).unwrap().len(), 1); + interactor.build_route_tree_map(&stops); } #[test] @@ -5425,9 +5407,6 @@ mod tests { // line_group 300: 発着駅を含まない → 除外 create_route_stop(3105, 5, 33, Some(300)), create_route_stop(3106, 6, 33, Some(300)), - // line_group_cdなし: line_cd(44)でグループ化され種別なし - create_route_stop(4101, 1, 44, None), - create_route_stop(4103, 3, 44, None), ]; let lines = vec![ create_route_line(11, 100), @@ -5441,7 +5420,7 @@ mod tests { // 発着駅を含まないline_group 300は除外され、BTreeMapのキー順に並ぶ let route_ids: Vec = routes.iter().map(|r| r.id).collect(); - assert_eq!(route_ids, vec![44, 100, 200]); + assert_eq!(route_ids, vec![100, 200]); // 路線の取得は経路候補ごとではなく一括1回で、 // 除外されたグループ(300)のIDは要求されない @@ -5473,11 +5452,6 @@ mod tests { let line_ids: Vec = tt.lines.iter().map(|l| l.id).collect(); assert_eq!(line_ids, vec![22]); } - - // line_group_cdなしのグループは種別を持たない - let route44 = routes.iter().find(|r| r.id == 44).unwrap(); - assert_eq!(route44.stops.len(), 2); - assert!(route44.stops.iter().all(|s| s.train_type.is_none())); } #[tokio::test]