Update cbp and add FKs back

This commit is contained in:
DariusIII
2026-05-06 10:20:05 +02:00
parent a1823103d2
commit 97029c4eb1
6 changed files with 520 additions and 92 deletions
+68 -30
View File
@@ -11,6 +11,8 @@ use Illuminate\Support\Str;
class CollectionCleanupService
{
private const MAX_SQL_ROWS_PER_STATEMENT = 500;
/**
* Maximum number of retries for a lock-related DB error before giving up.
*/
@@ -50,28 +52,27 @@ class CollectionCleanupService
), true);
}
// Batch-delete old collections using a safe id-subselect to avoid read-then-delete races.
// The DELETE here is single-table (no JOIN), so cross-table deadlocks
// are not a concern; the helper still gives us bounded retries with
// jittered backoff and re-throws non-lock errors instead of swallowing them.
// Batch-delete old collections using select-then-delete so we can
// explicitly remove parts/binaries/collections even when FK cascades
// are not present in the runtime schema.
$cutoff = now()->subHours(Settings::settingValue('partretentionhours'));
$batchDeleted = 0;
do {
$affected = $this->retryOnLockError(
fn (): int => DB::affectingStatement(
'DELETE FROM collections WHERE id IN (
SELECT id FROM (
SELECT id FROM collections WHERE dateadded < ? ORDER BY id LIMIT 500
) AS x
)',
[$cutoff]
),
'Cleanup',
$echoCLI,
);
$ids = DB::table('collections')
->where('dateadded', '<', $cutoff)
->orderBy('id')
->limit(self::MAX_SQL_ROWS_PER_STATEMENT)
->pluck('id')
->all();
if ($ids === []) {
break;
}
$affected = $this->deleteCollectionsAndDescendants($ids, 'Cleanup', $echoCLI);
$batchDeleted += $affected;
if ($affected < 500) {
if ($affected < self::MAX_SQL_ROWS_PER_STATEMENT) {
break;
}
// Brief pause to reduce pressure on the lock manager in busy systems.
@@ -125,7 +126,7 @@ class CollectionCleanupService
if ($echoCLI) {
cli()->primary(
'Finished deleting '.$missedDeleted.' collections missed after NZB creation in '.($totalTime).Str::plural(' second', (int) $totalTime).
PHP_EOL.'Removed '.number_format($deletedCount).' parts/binaries/collection rows in '.$totalTime.Str::plural(' second', (int) $totalTime),
PHP_EOL.'Removed '.number_format($deletedCount).' collections (with related binaries/parts) in '.$totalTime.Str::plural(' second', (int) $totalTime),
true
);
}
@@ -146,7 +147,7 @@ class CollectionCleanupService
{
$deleted = 0;
$maxBatches = 20; // hard cap per cycle; bounded backlog drain
$batchSize = 500;
$batchSize = self::MAX_SQL_ROWS_PER_STATEMENT;
for ($i = 0; $i < $maxBatches; $i++) {
$ids = DB::table('collections as c')
@@ -162,11 +163,7 @@ class CollectionCleanupService
break;
}
$affected = $this->retryOnLockError(
fn (): int => DB::table('collections')->whereIn('id', $ids)->delete(),
'Orphan cleanup',
$echoCLI,
);
$affected = $this->deleteCollectionsAndDescendants($ids, 'Orphan cleanup', $echoCLI);
$deleted += $affected;
if ($affected < $batchSize) {
@@ -199,7 +196,7 @@ class CollectionCleanupService
{
$deleted = 0;
$maxBatches = 20;
$batchSize = 500;
$batchSize = self::MAX_SQL_ROWS_PER_STATEMENT;
for ($i = 0; $i < $maxBatches; $i++) {
$ids = DB::table('collections as c')
@@ -214,11 +211,7 @@ class CollectionCleanupService
break;
}
$affected = $this->retryOnLockError(
fn (): int => DB::table('collections')->whereIn('id', $ids)->delete(),
'Missed-NZB cleanup',
$echoCLI,
);
$affected = $this->deleteCollectionsAndDescendants($ids, 'Missed-NZB cleanup', $echoCLI);
$deleted += $affected;
if ($affected < $batchSize) {
@@ -230,6 +223,51 @@ class CollectionCleanupService
return $deleted;
}
/**
* Explicitly delete parts, binaries, then collections for the given IDs.
* This path does not rely on DB-level cascade constraints.
*
* @param list<int> $collectionIds
*/
public function deleteCollectionsAndDescendants(
array $collectionIds,
string $label = 'CBP cleanup',
bool $echoCLI = false,
): int {
if ($collectionIds === []) {
return 0;
}
$deletedCollections = 0;
foreach (array_chunk($collectionIds, self::MAX_SQL_ROWS_PER_STATEMENT) as $chunk) {
$placeholders = implode(',', array_fill(0, count($chunk), '?'));
$deletedCollections += $this->retryOnLockError(
fn (): int => DB::transaction(
function () use ($chunk, $placeholders): int {
DB::statement(
"DELETE FROM parts WHERE binaries_id IN (SELECT id FROM binaries WHERE collections_id IN ({$placeholders}))",
$chunk
);
DB::statement(
"DELETE FROM binaries WHERE collections_id IN ({$placeholders})",
$chunk
);
return (int) DB::affectingStatement(
"DELETE FROM collections WHERE id IN ({$placeholders})",
$chunk
);
}
),
$label,
$echoCLI,
);
}
return $deletedCollections;
}
/**
* Run a DB write inside a bounded retry loop that only swallows transient
* InnoDB lock errors (deadlock 1213, lock wait timeout 1205). Any other
+9 -5
View File
@@ -9,6 +9,7 @@ use App\Models\Collection;
use App\Models\Part;
use App\Models\Release;
use App\Models\Settings;
use App\Services\CollectionCleanupService;
use Illuminate\Database\QueryException;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\File;
@@ -57,8 +58,9 @@ class NzbService
protected string $siteCommentString;
public function __construct()
{
public function __construct(
private readonly CollectionCleanupService $collectionCleanupService,
) {
try {
$nzbSplitLevel = (int) Settings::settingValue('nzbsplitlevel');
} catch (QueryException $e) {
@@ -217,9 +219,11 @@ class NzbService
// Delete CBP (Collections, Binaries, Parts) for release that has its NZB created.
// Use a transaction to ensure cascading deletes complete properly.
try {
DB::transaction(function () use ($release) {
Collection::query()->where('releases_id', $release->id)->delete();
});
$this->collectionCleanupService->deleteCollectionsAndDescendants(
$collectionIds,
'NZB cleanup',
false
);
} catch (\Throwable $e) {
// Log the error but don't fail the NZB creation since the file was written successfully
Log::warning('Failed to delete collections for release '.$release->id.': '.$e->getMessage());
+7 -4
View File
@@ -21,7 +21,8 @@ use Illuminate\Support\Str;
class ReleaseCreationService
{
public function __construct(
private readonly ReleaseCleaningService $releaseCleaning
private readonly ReleaseCleaningService $releaseCleaning,
private readonly CollectionCleanupService $collectionCleanupService,
) {}
/**
@@ -171,9 +172,11 @@ class ReleaseCreationService
}
}
} else {
DB::transaction(static function () use ($collection) {
Collection::query()->where('collectionhash', $collection->collectionhash)->delete();
}, 10);
$this->collectionCleanupService->deleteCollectionsAndDescendants(
[$collection->id],
'Duplicate cleanup',
$echoCLI
);
$duplicate++;
}
+74 -53
View File
@@ -85,11 +85,11 @@ final class ReleaseProcessingService
$this->releaseCleaning = $releaseCleaning ?? new ReleaseCleaningService;
$this->releaseManagement = $releaseManagement ?? app(ReleaseManagementService::class);
$this->releaseImage = $releaseImage ?? new ReleaseImageService;
$this->releaseCreationService = $releaseCreationService
?? new ReleaseCreationService($this->releaseCleaning);
$this->collectionCleanupService = $collectionCleanupService
?? new CollectionCleanupService;
$this->releaseCreationService = $releaseCreationService
?? new ReleaseCreationService($this->releaseCleaning, $this->collectionCleanupService);
$this->postProcessService = $postProcessService;
$this->settings = $this->loadSettings();
@@ -432,21 +432,22 @@ final class ReleaseProcessingService
$stats = ['minSize' => 0, 'maxSize' => 0, 'minFiles' => 0, 'par2Only' => 0];
// Delete collections where ALL binaries are par2 files (no actual content)
DB::transaction(static function () use (&$stats): void {
$par2OnlyCollectionIds = DB::table('collections as c')
->join('binaries as b', 'c.id', '=', 'b.collections_id')
->where('c.filecheck', CollectionFileCheckStatus::Sized->value)
->where('c.filesize', '>', 0)
->groupBy('c.id')
->havingRaw("COUNT(b.id) = SUM(CASE WHEN b.name REGEXP '\\\\.(vol[0-9]+\\\\+[0-9]+\\\\.par2|par2)' THEN 1 ELSE 0 END)")
->pluck('c.id');
$par2OnlyCollectionIds = DB::table('collections as c')
->join('binaries as b', 'c.id', '=', 'b.collections_id')
->where('c.filecheck', CollectionFileCheckStatus::Sized->value)
->where('c.filesize', '>', 0)
->groupBy('c.id')
->havingRaw("COUNT(b.id) = SUM(CASE WHEN b.name REGEXP '\\\\.(vol[0-9]+\\\\+[0-9]+\\\\.par2|par2)' THEN 1 ELSE 0 END)")
->pluck('c.id')
->all();
if ($par2OnlyCollectionIds->isNotEmpty()) {
$stats['par2Only'] += Collection::query()
->whereIn('id', $par2OnlyCollectionIds)
->delete();
}
}, 10);
if ($par2OnlyCollectionIds !== []) {
$stats['par2Only'] += $this->collectionCleanupService->deleteCollectionsAndDescendants(
$par2OnlyCollectionIds,
'Par2-only cleanup',
$this->echoCLI
);
}
foreach ($groupIDs as $grpID) {
$groupSettings = UsenetGroup::getGroupByID($grpID['id']);
@@ -457,32 +458,48 @@ final class ReleaseProcessingService
continue;
}
DB::transaction(function () use ($groupMinSize, $groupMinFiles, &$stats): void {
$effectiveMinSize = max($groupMinSize, $this->settings->minSizeToFormRelease);
if ($effectiveMinSize > 0) {
$stats['minSize'] += Collection::query()
->where('filecheck', CollectionFileCheckStatus::Sized->value)
->where('filesize', '>', 0)
->where('filesize', '<', $effectiveMinSize)
->delete();
}
$effectiveMinSize = max($groupMinSize, $this->settings->minSizeToFormRelease);
if ($effectiveMinSize > 0) {
$ids = Collection::query()
->where('filecheck', CollectionFileCheckStatus::Sized->value)
->where('filesize', '>', 0)
->where('filesize', '<', $effectiveMinSize)
->pluck('id')
->all();
$stats['minSize'] += $this->collectionCleanupService->deleteCollectionsAndDescendants(
$ids,
'Min-size cleanup',
$this->echoCLI
);
}
if ($this->settings->maxSizeToFormRelease > 0) {
$stats['maxSize'] += Collection::query()
->where('filecheck', CollectionFileCheckStatus::Sized->value)
->where('filesize', '>', $this->settings->maxSizeToFormRelease)
->delete();
}
if ($this->settings->maxSizeToFormRelease > 0) {
$ids = Collection::query()
->where('filecheck', CollectionFileCheckStatus::Sized->value)
->where('filesize', '>', $this->settings->maxSizeToFormRelease)
->pluck('id')
->all();
$stats['maxSize'] += $this->collectionCleanupService->deleteCollectionsAndDescendants(
$ids,
'Max-size cleanup',
$this->echoCLI
);
}
$effectiveMinFiles = max($groupMinFiles, $this->settings->minFilesToFormRelease);
if ($effectiveMinFiles > 0) {
$stats['minFiles'] += Collection::query()
->where('filecheck', CollectionFileCheckStatus::Sized->value)
->where('filesize', '>', 0)
->where('totalfiles', '<', $effectiveMinFiles)
->delete();
}
}, 10);
$effectiveMinFiles = max($groupMinFiles, $this->settings->minFilesToFormRelease);
if ($effectiveMinFiles > 0) {
$ids = Collection::query()
->where('filecheck', CollectionFileCheckStatus::Sized->value)
->where('filesize', '>', 0)
->where('totalfiles', '<', $effectiveMinFiles)
->pluck('id')
->all();
$stats['minFiles'] += $this->collectionCleanupService->deleteCollectionsAndDescendants(
$ids,
'Min-files cleanup',
$this->echoCLI
);
}
}
$this->outputCollectionDeleteStats($stats, $startTime);
@@ -993,20 +1010,24 @@ final class ReleaseProcessingService
do {
try {
$groupCondition = $groupID !== 0 ? 'AND groups_id = ? ' : '';
$batchLimit = self::BATCH_SIZE;
$query = DB::table('collections')
->where('added', '<', $cutoff)
->orderBy('id')
->limit(self::BATCH_SIZE);
if ($groupID !== 0) {
$query->where('groups_id', '=', $groupID);
}
$sql = <<<SQL
DELETE FROM collections WHERE id IN (
SELECT id FROM (
SELECT id FROM collections WHERE added < ? {$groupCondition}
ORDER BY id LIMIT {$batchLimit}
) AS x
)
SQL;
$ids = $query->pluck('id')->all();
if ($ids === []) {
break;
}
$params = $groupID !== 0 ? [$cutoff, $groupID] : [$cutoff];
$affected = DB::affectingStatement($sql, $params);
$affected = $this->collectionCleanupService->deleteCollectionsAndDescendants(
$ids,
'Stuck collections cleanup',
$this->echoCLI
);
break;
} catch (Throwable $e) {
$attempt++;
@@ -0,0 +1,79 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
if (DB::getDriverName() === 'sqlite') {
return;
}
if (! Schema::hasTable('collections') || ! Schema::hasTable('binaries') || ! Schema::hasTable('parts')) {
return;
}
// Remove orphans so FK creation cannot fail on existing bad rows.
DB::statement(
'DELETE p FROM parts p LEFT JOIN binaries b ON b.id = p.binaries_id WHERE b.id IS NULL'
);
DB::statement(
'DELETE b FROM binaries b LEFT JOIN collections c ON c.id = b.collections_id WHERE c.id IS NULL'
);
$this->dropForeignKeyIfExists('parts', 'FK_binaries');
$this->dropForeignKeyIfExists('binaries', 'FK_Collections');
DB::statement(
'ALTER TABLE `binaries`
ADD CONSTRAINT `FK_Collections`
FOREIGN KEY (`collections_id`)
REFERENCES `collections` (`id`)
ON DELETE CASCADE
ON UPDATE CASCADE'
);
DB::statement(
'ALTER TABLE `parts`
ADD CONSTRAINT `FK_binaries`
FOREIGN KEY (`binaries_id`)
REFERENCES `binaries` (`id`)
ON DELETE CASCADE
ON UPDATE CASCADE'
);
}
public function down(): void
{
if (DB::getDriverName() === 'sqlite') {
return;
}
if (! Schema::hasTable('binaries') || ! Schema::hasTable('parts')) {
return;
}
$this->dropForeignKeyIfExists('parts', 'FK_binaries');
$this->dropForeignKeyIfExists('binaries', 'FK_Collections');
}
private function dropForeignKeyIfExists(string $table, string $constraintName): void
{
$exists = DB::select(
'SELECT CONSTRAINT_NAME
FROM information_schema.TABLE_CONSTRAINTS
WHERE CONSTRAINT_TYPE = "FOREIGN KEY"
AND TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = ?
AND CONSTRAINT_NAME = ?',
[$table, $constraintName]
);
if ($exists !== []) {
DB::statement("ALTER TABLE `{$table}` DROP FOREIGN KEY `{$constraintName}`");
}
}
};
+283
View File
@@ -0,0 +1,283 @@
<?php
namespace Tests\Feature;
use App\Enums\CollectionFileCheckStatus;
use App\Models\Release;
use App\Services\CollectionCleanupService;
use App\Services\Nzb\NzbService;
use App\Services\ReleaseCleaningService;
use App\Services\ReleaseCreationService;
use Illuminate\Support\Facades\DB;
use Tests\TestCase;
class CbpCleanupServiceTest extends TestCase
{
protected function setUp(): void
{
parent::setUp();
config(['database.default' => 'sqlite', 'database.connections.sqlite.database' => ':memory:']);
DB::purge();
DB::reconnect();
DB::connection()->getPdo()->sqliteCreateFunction('UNIX_TIMESTAMP', static fn (?string $value): int => strtotime((string) $value));
$this->createTables();
$this->seedSettings();
}
public function test_retention_cleanup_deletes_parts_binaries_and_collections_without_fk_cascades(): void
{
DB::table('collections')->insert([
'id' => 100,
'subject' => 'Retention.Release',
'fromname' => 'poster@example.com',
'date' => now()->subHours(10)->format('Y-m-d H:i:s'),
'dateadded' => now()->subHours(10)->format('Y-m-d H:i:s'),
'added' => now()->subHours(10)->format('Y-m-d H:i:s'),
'xref' => 'alt.test:123',
'groups_id' => 1,
'totalfiles' => 1,
'filesize' => 500,
'filecheck' => CollectionFileCheckStatus::Sized->value,
'collectionhash' => 'retention-hash',
'collection_regexes_id' => 0,
'releases_id' => null,
'noise' => '',
]);
DB::table('binaries')->insert([
'id' => 1000,
'name' => 'Retention.Release.par2',
'collections_id' => 100,
'totalparts' => 1,
]);
DB::table('parts')->insert([
'binaries_id' => 1000,
'number' => 1,
'messageid' => '<retention-1@example.com>',
'partnumber' => 1,
'size' => 10,
]);
app(CollectionCleanupService::class)->deleteFinishedAndOrphans(false);
$this->assertSame(0, DB::table('parts')->count());
$this->assertSame(0, DB::table('binaries')->count());
$this->assertSame(0, DB::table('collections')->count());
}
public function test_nzb_creation_cleans_up_collection_binary_and_parts_explicitly(): void
{
DB::table('releases')->insert([
'id' => 1,
'name' => 'Nzb.Release',
'searchname' => 'Nzb.Release',
'totalpart' => 1,
'groups_id' => 1,
'adddate' => now()->format('Y-m-d H:i:s'),
'guid' => str_repeat('a', 36),
'leftguid' => 'a',
'postdate' => now()->format('Y-m-d H:i:s'),
'fromname' => 'poster@example.com',
'size' => 500,
'passwordstatus' => 0,
'haspreview' => -1,
'categories_id' => 1,
'nfostatus' => -1,
'nzbstatus' => NzbService::NZB_NONE,
'isrenamed' => 1,
'iscategorized' => 1,
'predb_id' => 0,
'source' => null,
'nzb_guid' => null,
]);
DB::table('collections')->insert([
'id' => 200,
'subject' => 'Nzb.Release',
'fromname' => 'poster@example.com',
'date' => now()->subHours(1)->format('Y-m-d H:i:s'),
'dateadded' => now()->subHours(1)->format('Y-m-d H:i:s'),
'added' => now()->subHours(1)->format('Y-m-d H:i:s'),
'xref' => 'alt.test:200',
'groups_id' => 1,
'totalfiles' => 1,
'filesize' => 500,
'filecheck' => CollectionFileCheckStatus::Inserted->value,
'collectionhash' => 'nzb-hash',
'collection_regexes_id' => 0,
'releases_id' => 1,
'noise' => '',
]);
DB::table('binaries')->insert([
'id' => 2000,
'name' => 'Nzb.Release yEnc',
'collections_id' => 200,
'totalparts' => 1,
]);
DB::table('parts')->insert([
'binaries_id' => 2000,
'number' => 1,
'messageid' => '<nzb-1@example.com>',
'partnumber' => 1,
'size' => 10,
]);
$release = Release::query()->findOrFail(1);
$release->setRelation('category', (object) ['title' => 'Misc', 'parent' => (object) ['title' => 'Other']]);
$written = app(NzbService::class)->writeNzbForReleaseId($release);
$this->assertTrue($written);
$this->assertSame(0, DB::table('parts')->count());
$this->assertSame(0, DB::table('binaries')->count());
$this->assertSame(0, DB::table('collections')->count());
$this->assertSame(1, (int) DB::table('releases')->where('id', 1)->value('nzbstatus'));
}
public function test_duplicate_release_path_cleans_up_collection_binary_and_parts(): void
{
DB::table('releases')->insert([
'id' => 2,
'name' => 'Duplicate.Release',
'searchname' => 'Duplicate.Release',
'totalpart' => 1,
'groups_id' => 1,
'adddate' => now()->format('Y-m-d H:i:s'),
'guid' => str_repeat('b', 36),
'leftguid' => 'b',
'postdate' => now()->format('Y-m-d H:i:s'),
'fromname' => 'poster@example.com',
'size' => 1000,
'passwordstatus' => 0,
'haspreview' => -1,
'categories_id' => 1,
'nfostatus' => -1,
'nzbstatus' => NzbService::NZB_NONE,
'isrenamed' => 1,
'iscategorized' => 1,
'predb_id' => 0,
'source' => null,
'nzb_guid' => null,
]);
DB::table('collections')->insert([
'id' => 300,
'subject' => 'Duplicate.Release',
'fromname' => 'poster@example.com',
'date' => now()->subHours(1)->format('Y-m-d H:i:s'),
'dateadded' => now()->subHours(1)->format('Y-m-d H:i:s'),
'added' => now()->subHours(1)->format('Y-m-d H:i:s'),
'xref' => 'alt.test:300',
'groups_id' => 1,
'totalfiles' => 1,
'filesize' => 1000,
'filecheck' => CollectionFileCheckStatus::Sized->value,
'collectionhash' => 'duplicate-hash',
'collection_regexes_id' => 0,
'releases_id' => null,
'noise' => '',
]);
DB::table('binaries')->insert([
'id' => 3000,
'name' => 'Duplicate.Release yEnc',
'collections_id' => 300,
'totalparts' => 1,
]);
DB::table('parts')->insert([
'binaries_id' => 3000,
'number' => 1,
'messageid' => '<duplicate-1@example.com>',
'partnumber' => 1,
'size' => 10,
]);
$service = new ReleaseCreationService(
app(ReleaseCleaningService::class),
app(CollectionCleanupService::class)
);
$result = $service->createReleases(null, 10, false);
$this->assertSame(['added' => 0, 'dupes' => 1], $result);
$this->assertSame(0, DB::table('parts')->count());
$this->assertSame(0, DB::table('binaries')->count());
$this->assertSame(0, DB::table('collections')->count());
}
private function seedSettings(): void
{
$settings = [
'partretentionhours' => '1',
'nzbsplitlevel' => '1',
'check_passworded_rars' => '0',
'categorizeforeign' => '1',
'catwebdl' => '1',
];
foreach ($settings as $name => $value) {
DB::table('settings')->insert(['name' => $name, 'value' => $value]);
}
}
private function createTables(): void
{
DB::statement('CREATE TABLE settings (name VARCHAR(255) PRIMARY KEY, value TEXT)');
DB::statement('CREATE TABLE usenet_groups (id INTEGER PRIMARY KEY, name VARCHAR(255))');
DB::statement('CREATE TABLE categories (id INTEGER PRIMARY KEY, title VARCHAR(255), parent_categories_id INTEGER NULL)');
DB::statement('CREATE TABLE releases (
id INTEGER PRIMARY KEY,
name VARCHAR(255),
searchname VARCHAR(255),
totalpart INTEGER,
groups_id INTEGER,
adddate DATETIME NULL,
guid VARCHAR(64),
leftguid VARCHAR(1),
postdate DATETIME NULL,
fromname VARCHAR(255),
size INTEGER,
passwordstatus INTEGER,
haspreview INTEGER,
categories_id INTEGER,
nfostatus INTEGER,
nzbstatus INTEGER,
isrenamed INTEGER,
iscategorized INTEGER,
predb_id INTEGER,
source VARCHAR(255) NULL,
nzb_guid BLOB NULL
)');
DB::statement('CREATE TABLE collections (
id INTEGER PRIMARY KEY,
subject VARCHAR(255),
fromname VARCHAR(255),
date DATETIME NULL,
dateadded DATETIME NULL,
added DATETIME NULL,
xref TEXT,
groups_id INTEGER,
totalfiles INTEGER,
filesize INTEGER,
filecheck INTEGER,
collectionhash VARCHAR(255),
collection_regexes_id INTEGER,
releases_id INTEGER NULL,
noise VARCHAR(64)
)');
DB::statement('CREATE TABLE binaries (
id INTEGER PRIMARY KEY,
name VARCHAR(255),
collections_id INTEGER,
totalparts INTEGER
)');
DB::statement('CREATE TABLE parts (
binaries_id INTEGER,
number INTEGER,
messageid VARCHAR(255),
partnumber INTEGER,
size INTEGER
)');
DB::table('usenet_groups')->insert(['id' => 1, 'name' => 'alt.test']);
DB::table('categories')->insert(['id' => 1, 'title' => 'Misc', 'parent_categories_id' => null]);
}
}