Skip to content

Commit 1894746

Browse files
authored
🗃️ clean up route segments (#4580)
1 parent 8c8af46 commit 1894746

6 files changed

+216
-1
lines changed

app/Http/Controllers/ReRoutingController.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,12 +189,16 @@ private function rerouteBetween(Stopover $start, Stopover $end, OpenRailRoutingP
189189
return;
190190
}
191191

192+
// TODO: path_type is temporarily set to null because the existing OpenRailRoutingProfile
193+
// values (e.g. tgv_all, all_tracks) are not meaningful for our use.
194+
// Introduce a proper categorisation (e.g. rail, street, water, air) and
195+
// re-enable path_type assignment here once that is in place.
192196
$segment = $this->tripRepository->createRouteSegment(
193197
fromStation: $start->station,
194198
toStation: $end->station,
195199
encodedPolyline: $encodedPolyline,
196200
duration: $duration,
197-
pathType: $pathType,
201+
pathType: null,
198202
distanceInMeters: $route->distanceInMeters
199203
);
200204
$this->tripRepository->setRouteSegmentForStop($start, $segment);
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use Illuminate\Database\Migrations\Migration;
6+
use Illuminate\Database\Schema\Blueprint;
7+
use Illuminate\Support\Facades\DB;
8+
use Illuminate\Support\Facades\Schema;
9+
10+
// On MySQL/MariaDB: ALGORITHM=INPLACE, LOCK=NONE for a non-blocking DDL.
11+
// Adding a nullable column without default uses INSTANT on MySQL 8.0+ / MariaDB 10.3+,
12+
// falling back to INPLACE – no table rebuild, no read/write lock.
13+
// On SQLite (test env): falls back to standard Schema builder.
14+
return new class() extends Migration
15+
{
16+
public function up(): void
17+
{
18+
if (in_array(DB::getDriverName(), ['mysql', 'mariadb'], true)) {
19+
DB::statement(
20+
'ALTER TABLE route_segments
21+
ADD COLUMN polyline_hash VARCHAR(32) NULL AFTER polyline_precision,
22+
ALGORITHM=INPLACE, LOCK=NONE'
23+
);
24+
25+
return;
26+
}
27+
28+
Schema::table('route_segments', function (Blueprint $table): void {
29+
$table->string('polyline_hash', 32)->nullable()->after('polyline_precision');
30+
});
31+
}
32+
33+
public function down(): void
34+
{
35+
Schema::table('route_segments', function (Blueprint $table): void {
36+
$table->dropColumn('polyline_hash');
37+
});
38+
}
39+
};
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use Illuminate\Database\Migrations\Migration;
6+
use Illuminate\Support\Facades\DB;
7+
8+
// On MySQL/MariaDB: batched UPDATE with MD5() and LIMIT 500 keeps row locks short-lived.
9+
// On SQLite (test env): MD5() and UPDATE...LIMIT are unavailable, so hashes are computed
10+
// via PHP and applied row-by-row. Not performant, but test databases are tiny.
11+
return new class() extends Migration
12+
{
13+
private const BATCH_SIZE = 500;
14+
15+
public function up(): void
16+
{
17+
if (in_array(DB::getDriverName(), ['mysql', 'mariadb'], true)) {
18+
do {
19+
$affected = DB::affectingStatement(
20+
'UPDATE route_segments
21+
SET polyline_hash = MD5(polyline)
22+
WHERE polyline_hash IS NULL
23+
LIMIT ' . self::BATCH_SIZE
24+
);
25+
} while ($affected > 0);
26+
27+
return;
28+
}
29+
30+
DB::table('route_segments')
31+
->whereNull('polyline_hash')
32+
->orderBy('id')
33+
->chunkById(self::BATCH_SIZE, function (iterable $rows): void {
34+
foreach ($rows as $row) {
35+
DB::table('route_segments')
36+
->where('id', $row->id)
37+
->update(['polyline_hash' => md5($row->polyline)]);
38+
}
39+
});
40+
}
41+
};
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use Illuminate\Database\Migrations\Migration;
6+
use Illuminate\Support\Facades\DB;
7+
8+
// Resets path_type to NULL on all route_segments in a single statement.
9+
// The existing path_type values are not meaningful in their current form and are cleared
10+
// before deduplication so they do not act as a grouping dimension.
11+
// A proper categorisation (e.g. rail, street, water) may be introduced separately.
12+
// At ~116k rows this completes in well under a second, so batching is unnecessary.
13+
return new class() extends Migration
14+
{
15+
public function up(): void
16+
{
17+
DB::table('route_segments')->whereNotNull('path_type')->update(['path_type' => null]);
18+
}
19+
};
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use Illuminate\Database\Migrations\Migration;
6+
use Illuminate\Database\Schema\Blueprint;
7+
use Illuminate\Support\Facades\Schema;
8+
9+
return new class() extends Migration
10+
{
11+
public function up(): void
12+
{
13+
Schema::table('route_segments', function (Blueprint $table): void {
14+
$table->index(
15+
['from_station_id', 'to_station_id', 'polyline_hash'],
16+
'route_segments_polyline_lookup'
17+
);
18+
});
19+
}
20+
21+
public function down(): void
22+
{
23+
Schema::table('route_segments', function (Blueprint $table): void {
24+
$table->dropIndex('route_segments_polyline_lookup');
25+
});
26+
}
27+
};
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use Illuminate\Database\Migrations\Migration;
6+
use Illuminate\Support\Facades\DB;
7+
8+
// Deduplicates route_segments where (from_station_id, to_station_id, polyline_hash)
9+
// has more than one row. path_type is ignored as it has been nullified in 000004.
10+
// Winner selection priority:
11+
// 1. Has custom_waypoints (manually curated)
12+
// 2. Most references in train_stopovers
13+
// 3. Oldest record (lowest UUID = earliest UUIDv7 timestamp)
14+
//
15+
// Each duplicate group is processed in its own small transaction to keep lock
16+
// duration minimal. The table remains usable throughout.
17+
return new class() extends Migration
18+
{
19+
private const GROUP_BATCH_SIZE = 100;
20+
21+
public function up(): void
22+
{
23+
do {
24+
$groups = DB::table('route_segments')
25+
->selectRaw('from_station_id, to_station_id, polyline_hash')
26+
->whereNotNull('polyline_hash')
27+
->groupBy(['from_station_id', 'to_station_id', 'polyline_hash'])
28+
->havingRaw('COUNT(*) > 1')
29+
->orderBy('from_station_id')
30+
->orderBy('to_station_id')
31+
->limit(self::GROUP_BATCH_SIZE)
32+
->get();
33+
34+
foreach ($groups as $group) {
35+
$this->deduplicateGroup($group);
36+
}
37+
38+
// Re-query from offset 0 after each batch: deletions shrink the result set
39+
// so a fixed offset would skip groups.
40+
} while ($groups->isNotEmpty());
41+
}
42+
43+
private function deduplicateGroup(object $group): void
44+
{
45+
DB::transaction(function () use ($group): void {
46+
$segments = DB::table('route_segments')
47+
->leftJoin(
48+
'train_stopovers',
49+
'route_segments.id',
50+
'=',
51+
'train_stopovers.route_segment_id'
52+
)
53+
->select([
54+
'route_segments.id',
55+
DB::raw('COUNT(train_stopovers.id) AS ref_count'),
56+
])
57+
->where('route_segments.from_station_id', $group->from_station_id)
58+
->where('route_segments.to_station_id', $group->to_station_id)
59+
->where('route_segments.polyline_hash', $group->polyline_hash)
60+
->groupBy(['route_segments.id', 'route_segments.custom_waypoints'])
61+
// Winner priority: custom_waypoints → most refs → oldest UUID
62+
->orderByRaw('route_segments.custom_waypoints IS NOT NULL DESC')
63+
->orderByDesc('ref_count')
64+
->orderBy('route_segments.id')
65+
->get();
66+
67+
if ($segments->count() < 2) {
68+
return;
69+
}
70+
71+
$winnerId = $segments->first()->id;
72+
$loserIds = $segments->skip(1)->pluck('id')->all();
73+
74+
// Reassign all stopover references from losers to winner
75+
DB::table('train_stopovers')
76+
->whereIn('route_segment_id', $loserIds)
77+
->update(['route_segment_id' => $winnerId]);
78+
79+
// Delete the now-unreferenced duplicate segments
80+
DB::table('route_segments')
81+
->whereIn('id', $loserIds)
82+
->delete();
83+
});
84+
}
85+
};

0 commit comments

Comments
 (0)