Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion app/Http/Controllers/ReRoutingController.php
Original file line number Diff line number Diff line change
Expand Up @@ -189,12 +189,16 @@ private function rerouteBetween(Stopover $start, Stopover $end, OpenRailRoutingP
return;
}

// TODO: path_type is temporarily set to null because the existing OpenRailRoutingProfile
// values (e.g. tgv_all, all_tracks) are not meaningful for our use.
// Introduce a proper categorisation (e.g. rail, street, water, air) and
// re-enable path_type assignment here once that is in place.
$segment = $this->tripRepository->createRouteSegment(
fromStation: $start->station,
toStation: $end->station,
encodedPolyline: $encodedPolyline,
duration: $duration,
pathType: $pathType,
pathType: null,
distanceInMeters: $route->distanceInMeters
);
$this->tripRepository->setRouteSegmentForStop($start, $segment);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php

declare(strict_types=1);

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;

// On MySQL/MariaDB: ALGORITHM=INPLACE, LOCK=NONE for a non-blocking DDL.
// Adding a nullable column without default uses INSTANT on MySQL 8.0+ / MariaDB 10.3+,
// falling back to INPLACE – no table rebuild, no read/write lock.
// On SQLite (test env): falls back to standard Schema builder.
return new class() extends Migration
{
public function up(): void
{
if (in_array(DB::getDriverName(), ['mysql', 'mariadb'], true)) {
DB::statement(
'ALTER TABLE route_segments
ADD COLUMN polyline_hash VARCHAR(32) NULL AFTER polyline_precision,
ALGORITHM=INPLACE, LOCK=NONE'
);

return;
}

Schema::table('route_segments', function (Blueprint $table): void {
$table->string('polyline_hash', 32)->nullable()->after('polyline_precision');
});
}

public function down(): void
{
Schema::table('route_segments', function (Blueprint $table): void {
$table->dropColumn('polyline_hash');
});
}
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php

declare(strict_types=1);

use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;

// On MySQL/MariaDB: batched UPDATE with MD5() and LIMIT 500 keeps row locks short-lived.
// On SQLite (test env): MD5() and UPDATE...LIMIT are unavailable, so hashes are computed
// via PHP and applied row-by-row. Not performant, but test databases are tiny.
return new class() extends Migration
{
private const BATCH_SIZE = 500;

public function up(): void
{
if (in_array(DB::getDriverName(), ['mysql', 'mariadb'], true)) {
do {
$affected = DB::affectingStatement(
'UPDATE route_segments
SET polyline_hash = MD5(polyline)
WHERE polyline_hash IS NULL
LIMIT ' . self::BATCH_SIZE
);
} while ($affected > 0);

return;
}

DB::table('route_segments')
->whereNull('polyline_hash')
->orderBy('id')
->chunkById(self::BATCH_SIZE, function (iterable $rows): void {
foreach ($rows as $row) {
DB::table('route_segments')
->where('id', $row->id)
->update(['polyline_hash' => md5($row->polyline)]);
}
});
}
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

declare(strict_types=1);

use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;

// Resets path_type to NULL on all route_segments in a single statement.
// The existing path_type values are not meaningful in their current form and are cleared
// before deduplication so they do not act as a grouping dimension.
// A proper categorisation (e.g. rail, street, water) may be introduced separately.
// At ~116k rows this completes in well under a second, so batching is unnecessary.
return new class() extends Migration
{
public function up(): void
{
DB::table('route_segments')->whereNotNull('path_type')->update(['path_type' => null]);
}
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

declare(strict_types=1);

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class() extends Migration
{
public function up(): void
{
Schema::table('route_segments', function (Blueprint $table): void {
$table->index(
['from_station_id', 'to_station_id', 'polyline_hash'],
'route_segments_polyline_lookup'
);
});
}

public function down(): void
{
Schema::table('route_segments', function (Blueprint $table): void {
$table->dropIndex('route_segments_polyline_lookup');
});
}
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
<?php

declare(strict_types=1);

use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;

// Deduplicates route_segments where (from_station_id, to_station_id, polyline_hash)
// has more than one row. path_type is ignored as it has been nullified in 000004.
// Winner selection priority:
// 1. Has custom_waypoints (manually curated)
// 2. Most references in train_stopovers
// 3. Oldest record (lowest UUID = earliest UUIDv7 timestamp)
//
// Each duplicate group is processed in its own small transaction to keep lock
// duration minimal. The table remains usable throughout.
return new class() extends Migration
{
private const GROUP_BATCH_SIZE = 100;

public function up(): void
{
do {
$groups = DB::table('route_segments')
->selectRaw('from_station_id, to_station_id, polyline_hash')
->whereNotNull('polyline_hash')
->groupBy(['from_station_id', 'to_station_id', 'polyline_hash'])
->havingRaw('COUNT(*) > 1')
->orderBy('from_station_id')
->orderBy('to_station_id')
->limit(self::GROUP_BATCH_SIZE)
->get();

foreach ($groups as $group) {
$this->deduplicateGroup($group);
}

// Re-query from offset 0 after each batch: deletions shrink the result set
// so a fixed offset would skip groups.
} while ($groups->isNotEmpty());
}

private function deduplicateGroup(object $group): void
{
DB::transaction(function () use ($group): void {
$segments = DB::table('route_segments')
->leftJoin(
'train_stopovers',
'route_segments.id',
'=',
'train_stopovers.route_segment_id'
)
->select([
'route_segments.id',
DB::raw('COUNT(train_stopovers.id) AS ref_count'),
])
->where('route_segments.from_station_id', $group->from_station_id)
->where('route_segments.to_station_id', $group->to_station_id)
->where('route_segments.polyline_hash', $group->polyline_hash)
->groupBy(['route_segments.id', 'route_segments.custom_waypoints'])
// Winner priority: custom_waypoints → most refs → oldest UUID
->orderByRaw('route_segments.custom_waypoints IS NOT NULL DESC')
->orderByDesc('ref_count')
->orderBy('route_segments.id')
->get();

if ($segments->count() < 2) {
return;
}

$winnerId = $segments->first()->id;
$loserIds = $segments->skip(1)->pluck('id')->all();

// Reassign all stopover references from losers to winner
DB::table('train_stopovers')
->whereIn('route_segment_id', $loserIds)
->update(['route_segment_id' => $winnerId]);

// Delete the now-unreferenced duplicate segments
DB::table('route_segments')
->whereIn('id', $loserIds)
->delete();
});
}
};
Loading