diff --git a/CHANGELOG.md b/CHANGELOG.md index a2880205be..b8e5cf00e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ Important changes to data models, configuration, and migrations between each AppEngine version, listed here to ease deployment and troubleshooting. ## Next Release (replace with git tag when deployed) + * Note: started to unroll task versions and aborted tokens into SQL tables. ## `20260714t095300-all` * Bump runtimeVersion to `2026.07.10`. diff --git a/app/lib/database/schema.dart b/app/lib/database/schema.dart index 76b11b15b2..3b238ced66 100644 --- a/app/lib/database/schema.dart +++ b/app/lib/database/schema.dart @@ -23,4 +23,8 @@ abstract final class PrimarySchema extends Schema { Table get tasks; Table get taskDependencies; + + Table get taskVersions; + + Table get taskAbortedTokens; } diff --git a/app/lib/database/schema.g.dart b/app/lib/database/schema.g.dart index 2c59b46c29..d32fa60d84 100644 --- a/app/lib/database/schema.g.dart +++ b/app/lib/database/schema.g.dart @@ -33,6 +33,8 @@ extension PrimarySchemaSchema on Database { _$UserSessionRow._$table, _$Task._$table, _$TaskDependency._$table, + _$TaskVersion._$table, + _$TaskAbortedToken._$table, ]; Table get userSessions => @@ -43,6 +45,12 @@ extension PrimarySchemaSchema on Database { Table get taskDependencies => $ForGeneratedCode.declareTable(this, _$TaskDependency._$table); + Table get taskVersions => + $ForGeneratedCode.declareTable(this, _$TaskVersion._$table); + + Table get taskAbortedTokens => + $ForGeneratedCode.declareTable(this, _$TaskAbortedToken._$table); + /// Create tables defined in [PrimarySchema]. /// /// Calling this on an empty database will create the tables @@ -1435,6 +1443,36 @@ extension ExpressionTaskExt on Expr { r.runtimeVersion.equalsUnlessNull(runtimeVersion) & r.package.equalsUnlessNull(package), ); + + /// Get [SubQuery] of rows from the `taskVersions` table which + /// reference this row. + /// + /// This returns a [SubQuery] of [TaskVersion] rows, + /// where [TaskVersion.runtimeVersion], [TaskVersion.package] + /// references [Task.runtimeVersion], [Task.package] + /// in this row. + SubQuery<(Expr,)> get versionRows => $ForGeneratedCode + .subqueryTable(_$TaskVersion._$table) + .where( + (r) => + r.runtimeVersion.equalsUnlessNull(runtimeVersion) & + r.package.equalsUnlessNull(package), + ); + + /// Get [SubQuery] of rows from the `taskAbortedTokens` table which + /// reference this row. + /// + /// This returns a [SubQuery] of [TaskAbortedToken] rows, + /// where [TaskAbortedToken.runtimeVersion], [TaskAbortedToken.package] + /// references [Task.runtimeVersion], [Task.package] + /// in this row. + SubQuery<(Expr,)> get abortedTokenRows => $ForGeneratedCode + .subqueryTable(_$TaskAbortedToken._$table) + .where( + (r) => + r.runtimeVersion.equalsUnlessNull(runtimeVersion) & + r.package.equalsUnlessNull(package), + ); } extension ExpressionNullableTaskExt on Expr { @@ -1477,6 +1515,40 @@ extension ExpressionNullableTaskExt on Expr { r.package.equalsUnlessNull(package), ); + /// Get [SubQuery] of rows from the `taskVersions` table which + /// reference this row. + /// + /// This returns a [SubQuery] of [TaskVersion] rows, + /// where [TaskVersion.runtimeVersion], [TaskVersion.package] + /// references [Task.runtimeVersion], [Task.package] + /// in this row, if any. + /// + /// If this row is `NULL` the subquery is always be empty. + SubQuery<(Expr,)> get versionRows => $ForGeneratedCode + .subqueryTable(_$TaskVersion._$table) + .where( + (r) => + r.runtimeVersion.equalsUnlessNull(runtimeVersion) & + r.package.equalsUnlessNull(package), + ); + + /// Get [SubQuery] of rows from the `taskAbortedTokens` table which + /// reference this row. + /// + /// This returns a [SubQuery] of [TaskAbortedToken] rows, + /// where [TaskAbortedToken.runtimeVersion], [TaskAbortedToken.package] + /// references [Task.runtimeVersion], [Task.package] + /// in this row, if any. + /// + /// If this row is `NULL` the subquery is always be empty. + SubQuery<(Expr,)> get abortedTokenRows => $ForGeneratedCode + .subqueryTable(_$TaskAbortedToken._$table) + .where( + (r) => + r.runtimeVersion.equalsUnlessNull(runtimeVersion) & + r.package.equalsUnlessNull(package), + ); + /// Check if the row is not `NULL`. /// /// This will check if _primary key_ fields in this row are `NULL`. @@ -1494,6 +1566,42 @@ extension ExpressionNullableTaskExt on Expr { Expr isNull() => isNotNull().not(); } +extension InnerJoinTaskTaskAbortedTokenExt + on InnerJoin<(Expr,), (Expr,)> { + /// Join using the `task` _foreign key_. + /// + /// This will match rows where [Task.runtimeVersion] = [TaskAbortedToken.runtimeVersion] and [Task.package] = [TaskAbortedToken.package]. + Query<(Expr, Expr)> usingTask() => on( + (a, b) => + a.runtimeVersion.equalsUnlessNull(b.runtimeVersion) & + a.package.equalsUnlessNull(b.package), + ); +} + +extension LeftJoinTaskTaskAbortedTokenExt + on LeftJoin<(Expr,), (Expr,)> { + /// Join using the `task` _foreign key_. + /// + /// This will match rows where [Task.runtimeVersion] = [TaskAbortedToken.runtimeVersion] and [Task.package] = [TaskAbortedToken.package]. + Query<(Expr, Expr)> usingTask() => on( + (a, b) => + a.runtimeVersion.equalsUnlessNull(b.runtimeVersion) & + a.package.equalsUnlessNull(b.package), + ); +} + +extension RightJoinTaskTaskAbortedTokenExt + on RightJoin<(Expr,), (Expr,)> { + /// Join using the `task` _foreign key_. + /// + /// This will match rows where [Task.runtimeVersion] = [TaskAbortedToken.runtimeVersion] and [Task.package] = [TaskAbortedToken.package]. + Query<(Expr, Expr)> usingTask() => on( + (a, b) => + a.runtimeVersion.equalsUnlessNull(b.runtimeVersion) & + a.package.equalsUnlessNull(b.package), + ); +} + extension InnerJoinTaskTaskDependencyExt on InnerJoin<(Expr,), (Expr,)> { /// Join using the `task` _foreign key_. @@ -1530,6 +1638,42 @@ extension RightJoinTaskTaskDependencyExt ); } +extension InnerJoinTaskTaskVersionExt + on InnerJoin<(Expr,), (Expr,)> { + /// Join using the `task` _foreign key_. + /// + /// This will match rows where [Task.runtimeVersion] = [TaskVersion.runtimeVersion] and [Task.package] = [TaskVersion.package]. + Query<(Expr, Expr)> usingTask() => on( + (a, b) => + a.runtimeVersion.equalsUnlessNull(b.runtimeVersion) & + a.package.equalsUnlessNull(b.package), + ); +} + +extension LeftJoinTaskTaskVersionExt + on LeftJoin<(Expr,), (Expr,)> { + /// Join using the `task` _foreign key_. + /// + /// This will match rows where [Task.runtimeVersion] = [TaskVersion.runtimeVersion] and [Task.package] = [TaskVersion.package]. + Query<(Expr, Expr)> usingTask() => on( + (a, b) => + a.runtimeVersion.equalsUnlessNull(b.runtimeVersion) & + a.package.equalsUnlessNull(b.package), + ); +} + +extension RightJoinTaskTaskVersionExt + on RightJoin<(Expr,), (Expr,)> { + /// Join using the `task` _foreign key_. + /// + /// This will match rows where [Task.runtimeVersion] = [TaskVersion.runtimeVersion] and [Task.package] = [TaskVersion.package]. + Query<(Expr, Expr)> usingTask() => on( + (a, b) => + a.runtimeVersion.equalsUnlessNull(b.runtimeVersion) & + a.package.equalsUnlessNull(b.package), + ); +} + /// `Table` conflict targets for use with `.onConflict`. enum TaskConflict { /// Conflict with an existing row that has a matching primary key. @@ -2362,6 +2506,1617 @@ extension InsertOnConflictSingleTaskDependencyExt ); } +final class _$TaskVersion extends TaskVersion { + _$TaskVersion._( + this.runtimeVersion, + this.package, + this.version, + this.scheduled, + this.attempts, + this.zone, + this.instance, + this.secretToken, + this.hasDocs, + this.hasPana, + this.isFinished, + ); + + @override + final String runtimeVersion; + + @override + final String package; + + @override + final String version; + + @override + final DateTime scheduled; + + @override + final int attempts; + + @override + final String? zone; + + @override + final String? instance; + + @override + final String? secretToken; + + @override + final bool hasDocs; + + @override + final bool hasPana; + + @override + final bool isFinished; + + static final _$table = $ForGeneratedCode.tableDefinition( + tableName: 'task_versions', + columns: [ + 'runtime_version', + 'package', + 'version', + 'scheduled', + 'attempts', + 'zone', + 'instance', + 'secret_token', + 'has_docs', + 'has_pana', + 'is_finished', + ], + columnInfo: [ + $ForGeneratedCode.columnDefinition( + type: $ForGeneratedCode.text, + isNotNull: true, + defaultValue: null, + autoIncrement: false, + overrides: [], + ), + $ForGeneratedCode.columnDefinition( + type: $ForGeneratedCode.text, + isNotNull: true, + defaultValue: null, + autoIncrement: false, + overrides: [], + ), + $ForGeneratedCode.columnDefinition( + type: $ForGeneratedCode.text, + isNotNull: true, + defaultValue: null, + autoIncrement: false, + overrides: [], + ), + $ForGeneratedCode.columnDefinition( + type: $ForGeneratedCode.dateTime, + isNotNull: true, + defaultValue: null, + autoIncrement: false, + overrides: [], + ), + $ForGeneratedCode.columnDefinition( + type: $ForGeneratedCode.integer, + isNotNull: true, + defaultValue: null, + autoIncrement: false, + overrides: [], + ), + $ForGeneratedCode.columnDefinition( + type: $ForGeneratedCode.text, + isNotNull: false, + defaultValue: null, + autoIncrement: false, + overrides: [], + ), + $ForGeneratedCode.columnDefinition( + type: $ForGeneratedCode.text, + isNotNull: false, + defaultValue: null, + autoIncrement: false, + overrides: [], + ), + $ForGeneratedCode.columnDefinition( + type: $ForGeneratedCode.text, + isNotNull: false, + defaultValue: null, + autoIncrement: false, + overrides: [], + ), + $ForGeneratedCode.columnDefinition( + type: $ForGeneratedCode.boolean, + isNotNull: true, + defaultValue: null, + autoIncrement: false, + overrides: [], + ), + $ForGeneratedCode.columnDefinition( + type: $ForGeneratedCode.boolean, + isNotNull: true, + defaultValue: null, + autoIncrement: false, + overrides: [], + ), + $ForGeneratedCode.columnDefinition( + type: $ForGeneratedCode.boolean, + isNotNull: true, + defaultValue: null, + autoIncrement: false, + overrides: [], + ), + ], + primaryKey: ['runtime_version', 'package', 'version'], + unique: >[], + foreignKeys: [ + $ForGeneratedCode.foreignKeyDefinition( + name: 'task', + columns: ['runtime_version', 'package'], + referencedTable: 'tasks', + referencedColumns: ['runtime_version', 'package'], + onDelete: .cascade, + onUpdate: .cascade, + ), + ], + indexes: [], + readRow: _$TaskVersion._$fromDatabase, + ); + + static TaskVersion? _$fromDatabase(RowReader row) { + final runtimeVersion = row.readString(); + final package = row.readString(); + final version = row.readString(); + final scheduled = row.readDateTime(); + final attempts = row.readInt(); + final zone = row.readString(); + final instance = row.readString(); + final secretToken = row.readString(); + final hasDocs = row.readBool(); + final hasPana = row.readBool(); + final isFinished = row.readBool(); + if (runtimeVersion == null && + package == null && + version == null && + scheduled == null && + attempts == null && + zone == null && + instance == null && + secretToken == null && + hasDocs == null && + hasPana == null && + isFinished == null) { + return null; + } + return _$TaskVersion._( + runtimeVersion!, + package!, + version!, + scheduled!, + attempts!, + zone, + instance, + secretToken, + hasDocs!, + hasPana!, + isFinished!, + ); + } + + @override + String toString() => + 'TaskVersion(runtimeVersion: "$runtimeVersion", package: "$package", version: "$version", scheduled: "$scheduled", attempts: "$attempts", zone: "$zone", instance: "$instance", secretToken: "$secretToken", hasDocs: "$hasDocs", hasPana: "$hasPana", isFinished: "$isFinished")'; +} + +/// Extension methods for table defined in [TaskVersion]. +extension TableTaskVersionExt on Table { + /// Insert row into the `taskVersions` table. + /// + /// Returns a [InsertSingle] statement on which `.execute` must be + /// called for the row to be inserted. + InsertSingle insert({ + required Expr runtimeVersion, + required Expr package, + required Expr version, + required Expr scheduled, + required Expr attempts, + Expr? zone, + Expr? instance, + Expr? secretToken, + required Expr hasDocs, + required Expr hasPana, + required Expr isFinished, + }) => $ForGeneratedCode.insertInto( + table: this, + values: [ + runtimeVersion, + package, + version, + scheduled, + attempts, + zone, + instance, + secretToken, + hasDocs, + hasPana, + isFinished, + ], + ); + + /// Insert row into the `taskVersions` table. + /// + /// Returns a [InsertSingle] statement on which `.execute` must be + /// called for the row to be inserted. + InsertSingle insertValue({ + required String runtimeVersion, + required String package, + required String version, + required DateTime scheduled, + required int attempts, + String? zone, + String? instance, + String? secretToken, + required bool hasDocs, + required bool hasPana, + required bool isFinished, + }) => $ForGeneratedCode.insertInto( + table: this, + values: [ + runtimeVersion.asExpr, + package.asExpr, + version.asExpr, + scheduled.asExpr, + attempts.asExpr, + zone.asExpr, + instance.asExpr, + secretToken.asExpr, + hasDocs.asExpr, + hasPana.asExpr, + isFinished.asExpr, + ], + ); + + /// Bulk insert rows into the `taskVersions` table. + /// + /// This method takes an `Iterable` and requires that you provide + /// a _mapping function_ from `T` to each column to be inserted. + /// + /// If a mapping function is omitted, the _default value_ will be + /// inserted, or `NULL` if column is nullable and as no default value. + /// To explicitely insert `NULL`, use a _mapping function_ that maps + /// `T` to `null`. + /// + /// > [!NOTE] + /// > This method aims utilize database specific bulk insertion logic + /// > to ensure good performance. Database adapters may pipeline bulk + /// > insertions through multiple statements inside a transaction. + /// + /// Returns a [Insert] statement on which `.execute` must be + /// called for the rows to be inserted. + Insert insertValuesMapped( + Iterable rows, { + required String Function(T row) runtimeVersion, + required String Function(T row) package, + required String Function(T row) version, + required DateTime Function(T row) scheduled, + required int Function(T row) attempts, + String? Function(T row)? zone, + String? Function(T row)? instance, + String? Function(T row)? secretToken, + required bool Function(T row) hasDocs, + required bool Function(T row) hasPana, + required bool Function(T row) isFinished, + }) => $ForGeneratedCode.insertValuesMapped( + table: this, + rows: rows, + mappings: [ + runtimeVersion, + package, + version, + scheduled, + attempts, + zone, + instance, + secretToken, + hasDocs, + hasPana, + isFinished, + ], + ); + + /// Delete a single row from the `taskVersions` table, specified by + /// _primary key_. + /// + /// Returns a [DeleteSingle] statement on which `.execute()` must be + /// called for the row to be deleted. + /// + /// To delete multiple rows, using `.where()` to filter which rows + /// should be deleted. If you wish to delete all rows, use + /// `.where((_) => toExpr(true)).delete()`. + DeleteSingle delete( + String runtimeVersion, + String package, + String version, + ) => $ForGeneratedCode.deleteSingle( + byKey(runtimeVersion, package, version), + _$TaskVersion._$table, + ); +} + +/// Extension methods for building queries against the `taskVersions` table. +extension QueryTaskVersionExt on Query<(Expr,)> { + /// Lookup a single row in `taskVersions` table using the _primary key_. + /// + /// Returns a [QuerySingle] object, which returns at-most one row, + /// when `.fetch()` is called. + QuerySingle<(Expr,)> byKey( + String runtimeVersion, + String package, + String version, + ) => where( + (taskVersion) => + taskVersion.runtimeVersion.equalsValue(runtimeVersion) & + taskVersion.package.equalsValue(package) & + taskVersion.version.equalsValue(version), + ).first; + + /// Update all rows in the `taskVersions` table matching this [Query]. + /// + /// The changes to be applied to each row matching this [Query] are + /// defined using the [updateBuilder], which is given an [Expr] + /// representation of the row being updated and a `set` function to + /// specify which fields should be updated. The result of the `set` + /// function should always be returned from the `updateBuilder`. + /// + /// Returns an [Update] statement on which `.execute()` must be called + /// for the rows to be updated. + /// + /// **Example:** decrementing `1` from the `value` field for each row + /// where `value > 0`. + /// ```dart + /// await db.mytable + /// .where((row) => row.value > toExpr(0)) + /// .update((row, set) => set( + /// value: row.value - toExpr(1), + /// )) + /// .execute(); + /// ``` + /// + /// > [!WARNING] + /// > The `updateBuilder` callback does not make the update, it builds + /// > the expressions for updating the rows. You should **never** invoke + /// > the `set` function more than once, and the result should always + /// > be returned immediately. + Update update( + UpdateSet Function( + Expr taskVersion, + UpdateSet Function({ + Expr runtimeVersion, + Expr package, + Expr version, + Expr scheduled, + Expr attempts, + Expr zone, + Expr instance, + Expr secretToken, + Expr hasDocs, + Expr hasPana, + Expr isFinished, + }) + set, + ) + updateBuilder, + ) => $ForGeneratedCode.update( + this, + _$TaskVersion._$table, + (taskVersion) => updateBuilder( + taskVersion, + ({ + Expr? runtimeVersion, + Expr? package, + Expr? version, + Expr? scheduled, + Expr? attempts, + Expr? zone, + Expr? instance, + Expr? secretToken, + Expr? hasDocs, + Expr? hasPana, + Expr? isFinished, + }) => $ForGeneratedCode.buildUpdate([ + runtimeVersion, + package, + version, + scheduled, + attempts, + zone, + instance, + secretToken, + hasDocs, + hasPana, + isFinished, + ]), + ), + ); + + /// Delete all rows in the `taskVersions` table matching this [Query]. + /// + /// Returns a [Delete] statement on which `.execute()` must be called + /// for the rows to be deleted. + Delete delete() => + $ForGeneratedCode.delete(this, _$TaskVersion._$table); +} + +/// Extension methods for building point queries against the `taskVersions` table. +extension QuerySingleTaskVersionExt on QuerySingle<(Expr,)> { + /// Update the row (if any) in the `taskVersions` table matching this + /// [QuerySingle]. + /// + /// The changes to be applied to the row matching this [QuerySingle] are + /// defined using the [updateBuilder], which is given an [Expr] + /// representation of the row being updated and a `set` function to + /// specify which fields should be updated. The result of the `set` + /// function should always be returned from the `updateBuilder`. + /// + /// Returns an [UpdateSingle] statement on which `.execute()` must be + /// called for the row to be updated. The resulting statement will + /// **not** fail, if there are no rows matching this query exists. + /// + /// **Example:** decrementing `1` from the `value` field the row with + /// `id = 1`. + /// ```dart + /// await db.mytable + /// .byKey(1) + /// .update((row, set) => set( + /// value: row.value - toExpr(1), + /// )) + /// .execute(); + /// ``` + /// + /// > [!WARNING] + /// > The `updateBuilder` callback does not make the update, it builds + /// > the expressions for updating the rows. You should **never** invoke + /// > the `set` function more than once, and the result should always + /// > be returned immediately. + UpdateSingle update( + UpdateSet Function( + Expr taskVersion, + UpdateSet Function({ + Expr runtimeVersion, + Expr package, + Expr version, + Expr scheduled, + Expr attempts, + Expr zone, + Expr instance, + Expr secretToken, + Expr hasDocs, + Expr hasPana, + Expr isFinished, + }) + set, + ) + updateBuilder, + ) => $ForGeneratedCode.updateSingle( + this, + _$TaskVersion._$table, + (taskVersion) => updateBuilder( + taskVersion, + ({ + Expr? runtimeVersion, + Expr? package, + Expr? version, + Expr? scheduled, + Expr? attempts, + Expr? zone, + Expr? instance, + Expr? secretToken, + Expr? hasDocs, + Expr? hasPana, + Expr? isFinished, + }) => $ForGeneratedCode.buildUpdate([ + runtimeVersion, + package, + version, + scheduled, + attempts, + zone, + instance, + secretToken, + hasDocs, + hasPana, + isFinished, + ]), + ), + ); + + /// Delete the row (if any) in the `taskVersions` table matching this [QuerySingle]. + /// + /// Returns a [DeleteSingle] statement on which `.execute()` must be called + /// for the row to be deleted. The resulting statement will **not** + /// fail, if there are no rows matching this query exists. + DeleteSingle delete() => + $ForGeneratedCode.deleteSingle(this, _$TaskVersion._$table); +} + +/// Extension methods for expressions on a row in the `taskVersions` table. +extension ExpressionTaskVersionExt on Expr { + Expr get runtimeVersion => + $ForGeneratedCode.field(this, 0, $ForGeneratedCode.text); + + Expr get package => + $ForGeneratedCode.field(this, 1, $ForGeneratedCode.text); + + Expr get version => + $ForGeneratedCode.field(this, 2, $ForGeneratedCode.text); + + /// The last time when this was scheduled for analysis. + Expr get scheduled => + $ForGeneratedCode.field(this, 3, $ForGeneratedCode.dateTime); + + /// The number of attempts to schedule the package version for analysis. + Expr get attempts => + $ForGeneratedCode.field(this, 4, $ForGeneratedCode.integer); + + /// Name of the zone in which the instance analysing this package version is + /// running. + Expr get zone => + $ForGeneratedCode.field(this, 5, $ForGeneratedCode.text); + + /// Name of the instance analysing this package version. + Expr get instance => + $ForGeneratedCode.field(this, 6, $ForGeneratedCode.text); + + /// Secret token (UUIDv4) used for authenticating worker requests. + Expr get secretToken => + $ForGeneratedCode.field(this, 7, $ForGeneratedCode.text); + + /// True, if dartdoc documentation is available. + Expr get hasDocs => + $ForGeneratedCode.field(this, 8, $ForGeneratedCode.boolean); + + /// True, if pana summary is available. + Expr get hasPana => + $ForGeneratedCode.field(this, 9, $ForGeneratedCode.boolean); + + /// True, if results have been previously reported on this version. + Expr get isFinished => + $ForGeneratedCode.field(this, 10, $ForGeneratedCode.boolean); + + /// Do a subquery lookup of the row from table + /// `tasks` referenced in + /// [runtimeVersion], [package]. + /// + /// The gets the row from table `tasks` where + /// [Task.runtimeVersion], [Task.package] + /// is equal to [runtimeVersion], [package]. + Expr get task => $ForGeneratedCode + .subqueryTable(_$Task._$table) + .where( + (r) => + r.runtimeVersion.equalsUnlessNull(runtimeVersion) & + r.package.equalsUnlessNull(package), + ) + .first + .asNotNull(); +} + +extension ExpressionNullableTaskVersionExt on Expr { + Expr get runtimeVersion => + $ForGeneratedCode.field(this, 0, $ForGeneratedCode.text); + + Expr get package => + $ForGeneratedCode.field(this, 1, $ForGeneratedCode.text); + + Expr get version => + $ForGeneratedCode.field(this, 2, $ForGeneratedCode.text); + + /// The last time when this was scheduled for analysis. + Expr get scheduled => + $ForGeneratedCode.field(this, 3, $ForGeneratedCode.dateTime); + + /// The number of attempts to schedule the package version for analysis. + Expr get attempts => + $ForGeneratedCode.field(this, 4, $ForGeneratedCode.integer); + + /// Name of the zone in which the instance analysing this package version is + /// running. + Expr get zone => + $ForGeneratedCode.field(this, 5, $ForGeneratedCode.text); + + /// Name of the instance analysing this package version. + Expr get instance => + $ForGeneratedCode.field(this, 6, $ForGeneratedCode.text); + + /// Secret token (UUIDv4) used for authenticating worker requests. + Expr get secretToken => + $ForGeneratedCode.field(this, 7, $ForGeneratedCode.text); + + /// True, if dartdoc documentation is available. + Expr get hasDocs => + $ForGeneratedCode.field(this, 8, $ForGeneratedCode.boolean); + + /// True, if pana summary is available. + Expr get hasPana => + $ForGeneratedCode.field(this, 9, $ForGeneratedCode.boolean); + + /// True, if results have been previously reported on this version. + Expr get isFinished => + $ForGeneratedCode.field(this, 10, $ForGeneratedCode.boolean); + + /// Do a subquery lookup of the row from table + /// `tasks` referenced in + /// [runtimeVersion], [package]. + /// + /// The gets the row from table `tasks` where + /// [Task.runtimeVersion], [Task.package] + /// is equal to [runtimeVersion], [package], if any. + /// + /// If this row is `NULL` the subquery is always return `NULL`. + Expr get task => $ForGeneratedCode + .subqueryTable(_$Task._$table) + .where( + (r) => + r.runtimeVersion.equalsUnlessNull(runtimeVersion) & + r.package.equalsUnlessNull(package), + ) + .first; + + /// Check if the row is not `NULL`. + /// + /// This will check if _primary key_ fields in this row are `NULL`. + /// + /// If this is a reference lookup by subquery it might be more efficient + /// to check if the referencing field is `NULL`. + Expr isNotNull() => + runtimeVersion.isNotNull() & package.isNotNull() & version.isNotNull(); + + /// Check if the row is `NULL`. + /// + /// This will check if _primary key_ fields in this row are `NULL`. + /// + /// If this is a reference lookup by subquery it might be more efficient + /// to check if the referencing field is `NULL`. + Expr isNull() => isNotNull().not(); +} + +extension InnerJoinTaskVersionTaskExt + on InnerJoin<(Expr,), (Expr,)> { + /// Join using the `task` _foreign key_. + /// + /// This will match rows where [TaskVersion.runtimeVersion] = [Task.runtimeVersion] and [TaskVersion.package] = [Task.package]. + Query<(Expr, Expr)> usingTask() => on( + (a, b) => + b.runtimeVersion.equalsUnlessNull(a.runtimeVersion) & + b.package.equalsUnlessNull(a.package), + ); +} + +extension LeftJoinTaskVersionTaskExt + on LeftJoin<(Expr,), (Expr,)> { + /// Join using the `task` _foreign key_. + /// + /// This will match rows where [TaskVersion.runtimeVersion] = [Task.runtimeVersion] and [TaskVersion.package] = [Task.package]. + Query<(Expr, Expr)> usingTask() => on( + (a, b) => + b.runtimeVersion.equalsUnlessNull(a.runtimeVersion) & + b.package.equalsUnlessNull(a.package), + ); +} + +extension RightJoinTaskVersionTaskExt + on RightJoin<(Expr,), (Expr,)> { + /// Join using the `task` _foreign key_. + /// + /// This will match rows where [TaskVersion.runtimeVersion] = [Task.runtimeVersion] and [TaskVersion.package] = [Task.package]. + Query<(Expr, Expr)> usingTask() => on( + (a, b) => + b.runtimeVersion.equalsUnlessNull(a.runtimeVersion) & + b.package.equalsUnlessNull(a.package), + ); +} + +/// `Table` conflict targets for use with `.onConflict`. +enum TaskVersionConflict { + /// Conflict with an existing row that has a matching primary key. + /// + /// Thus, the other row has matching values for: + /// `runtimeVersion`, `package`, `version`. + primaryKey(['runtime_version', 'package', 'version']); + + const TaskVersionConflict(this._fields); + + final List _fields; +} + +extension InsertTaskVersionExt on Insert { + /// Build an `INSERT` statement with an `ON CONFLICT` clause. + /// + /// The [target] argument specifies the _conflict target_ to be + /// handled. The _conflict target_ is always a `UNIQUE` constraint or + /// `PRIMARY KEY` constraint. + /// + /// If a row to be inserted violates the _conflict target_ constraint, + /// then the conflict action is triggered: + /// * `.doNothing()` to skip insertion of the new row, and, + /// * `.update((taskVersion, excluded, set) => set(...))` to + /// update the conflicting row. + /// + /// If a row to be inserted violates a constraint other than the one + /// specified in _conflict target_ then the entire `INSERT` statement + /// will fail. + /// + /// This is equivalent to `INSERT ... ON CONFLICT (...)` in SQL. + InsertOnConflict onConflict(TaskVersionConflict target) => + $ForGeneratedCode.insertOnConflict(this, target._fields); +} + +extension InsertOnConflictTaskVersionExt on InsertOnConflict { + /// Build an `INSERT` statement an [upsert-clause][1]. + /// + /// When a row to be inserted violates the `UNIQUE` or `PRIMARY KEY` + /// constraint previously specified as _conflict target_, the existing + /// row is updated using the expressions defined with the + /// [updateBuilder]. The [updateBuilder] is given 3 parameters: + /// * `taskVersion` an [Expr] representing the existing row in + /// the database, + /// * `excluded` an [Expr] representing the row to be inserted in the + /// database, and, + /// * `set` a function to specify which fields should be updated and + /// build the [UpdateSet]. + /// + /// The result of the `set` function should always be immediately + /// returned from the [updateBuilder]. + /// + /// **Example:** Insert a counter with `count = 2` or increment the + /// existing row, if a `PRIMARY KEY` conflict occurs. + /// ```dart + /// await db.counters.insertValue( + /// name: 'my-counter', // primary key + /// count: 2, + /// ) + /// .onConflict(.primaryKey) + /// .update((counter, excluded, set) => set( + /// count: counter.count + excluded.count, + /// )) + /// .execute(); + /// ``` + /// + /// This is equivalent to + /// `INSERT ... ON CONFLICT (...) UPDATE SET ...` in SQL. + /// + /// > [!WARNING] + /// > The `updateBuilder` callback does not make the update, it builds + /// > the expressions for updating the rows. You should **never** invoke + /// > the `set` function more than once, and the result should always + /// > be returned immediately. + /// + /// [1]: https://www.sqlite.org/lang_upsert.html + Upsert update( + UpdateSet Function( + Expr taskVersion, + Expr excluded, + UpdateSet Function({ + Expr runtimeVersion, + Expr package, + Expr version, + Expr scheduled, + Expr attempts, + Expr zone, + Expr instance, + Expr secretToken, + Expr hasDocs, + Expr hasPana, + Expr isFinished, + }) + set, + ) + updateBuilder, + ) => $ForGeneratedCode.updateOnConflict( + this, + (taskVersion, excluded) => updateBuilder( + taskVersion, + excluded, + ({ + Expr? runtimeVersion, + Expr? package, + Expr? version, + Expr? scheduled, + Expr? attempts, + Expr? zone, + Expr? instance, + Expr? secretToken, + Expr? hasDocs, + Expr? hasPana, + Expr? isFinished, + }) => $ForGeneratedCode.buildUpdate([ + runtimeVersion, + package, + version, + scheduled, + attempts, + zone, + instance, + secretToken, + hasDocs, + hasPana, + isFinished, + ]), + ), + ); +} + +extension InsertSingleTaskVersionExt on InsertSingle { + /// Build an `INSERT` statement with an `ON CONFLICT` clause. + /// + /// The [target] argument specifies the _conflict target_ to be + /// handled. The _conflict target_ is always a `UNIQUE` constraint or + /// `PRIMARY KEY` constraint. + /// + /// If a row to be inserted violates the _conflict target_ constraint, + /// then the conflict action is triggered: + /// * `.doNothing()` to skip insertion of the new row, and, + /// * `.update((taskVersion, excluded, set) => set(...))` to + /// update the conflicting row. + /// + /// If a row to be inserted violates a constraint other than the one + /// specified in _conflict target_ then the entire `INSERT` statement + /// will fail. + /// + /// This is equivalent to `INSERT ... ON CONFLICT (...)` in SQL. + InsertOnConflictSingle onConflict(TaskVersionConflict target) => + $ForGeneratedCode.insertOnConflictSingle(this, target._fields); +} + +extension InsertOnConflictSingleTaskVersionExt + on InsertOnConflictSingle { + /// Build an `INSERT` statement an [upsert-clause][1]. + /// + /// When a row to be inserted violates the `UNIQUE` or `PRIMARY KEY` + /// constraint previously specified as _conflict target_, the existing + /// row is updated using the expressions defined with the + /// [updateBuilder]. The [updateBuilder] is given 3 parameters: + /// * `taskVersion` an [Expr] representing the existing row in + /// the database, + /// * `excluded` an [Expr] representing the row to be inserted in the + /// database, and, + /// * `set` a function to specify which fields should be updated and + /// build the [UpdateSet]. + /// + /// The result of the `set` function should always be immediately + /// returned from the [updateBuilder]. + /// + /// **Example:** Insert a counter with `count = 2` or increment the + /// existing row, if a `PRIMARY KEY` conflict occurs. + /// ```dart + /// await db.counters.insertValue( + /// name: 'my-counter', // primary key + /// count: 2, + /// ) + /// .onConflict(.primaryKey) + /// .update((counter, excluded, set) => set( + /// count: counter.count + excluded.count, + /// )) + /// .execute(); + /// ``` + /// + /// This is equivalent to + /// `INSERT ... ON CONFLICT (...) UPDATE SET ...` in SQL. + /// + /// > [!WARNING] + /// > The `updateBuilder` callback does not make the update, it builds + /// > the expressions for updating the rows. You should **never** invoke + /// > the `set` function more than once, and the result should always + /// > be returned immediately. + /// + /// [1]: https://www.sqlite.org/lang_upsert.html + UpsertSingle update( + UpdateSet Function( + Expr taskVersion, + Expr excluded, + UpdateSet Function({ + Expr runtimeVersion, + Expr package, + Expr version, + Expr scheduled, + Expr attempts, + Expr zone, + Expr instance, + Expr secretToken, + Expr hasDocs, + Expr hasPana, + Expr isFinished, + }) + set, + ) + updateBuilder, + ) => $ForGeneratedCode.updateOnConflictSingle( + this, + (taskVersion, excluded) => updateBuilder( + taskVersion, + excluded, + ({ + Expr? runtimeVersion, + Expr? package, + Expr? version, + Expr? scheduled, + Expr? attempts, + Expr? zone, + Expr? instance, + Expr? secretToken, + Expr? hasDocs, + Expr? hasPana, + Expr? isFinished, + }) => $ForGeneratedCode.buildUpdate([ + runtimeVersion, + package, + version, + scheduled, + attempts, + zone, + instance, + secretToken, + hasDocs, + hasPana, + isFinished, + ]), + ), + ); +} + +final class _$TaskAbortedToken extends TaskAbortedToken { + _$TaskAbortedToken._( + this.runtimeVersion, + this.package, + this.token, + this.expires, + ); + + @override + final String runtimeVersion; + + @override + final String package; + + @override + final String token; + + @override + final DateTime expires; + + static final _$table = $ForGeneratedCode.tableDefinition( + tableName: 'task_aborted_tokens', + columns: ['runtime_version', 'package', 'token', 'expires'], + columnInfo: [ + $ForGeneratedCode.columnDefinition( + type: $ForGeneratedCode.text, + isNotNull: true, + defaultValue: null, + autoIncrement: false, + overrides: [], + ), + $ForGeneratedCode.columnDefinition( + type: $ForGeneratedCode.text, + isNotNull: true, + defaultValue: null, + autoIncrement: false, + overrides: [], + ), + $ForGeneratedCode.columnDefinition( + type: $ForGeneratedCode.text, + isNotNull: true, + defaultValue: null, + autoIncrement: false, + overrides: [], + ), + $ForGeneratedCode.columnDefinition( + type: $ForGeneratedCode.dateTime, + isNotNull: true, + defaultValue: null, + autoIncrement: false, + overrides: [], + ), + ], + primaryKey: ['runtime_version', 'package', 'token'], + unique: >[], + foreignKeys: [ + $ForGeneratedCode.foreignKeyDefinition( + name: 'task', + columns: ['runtime_version', 'package'], + referencedTable: 'tasks', + referencedColumns: ['runtime_version', 'package'], + onDelete: .cascade, + onUpdate: .cascade, + ), + ], + indexes: [], + readRow: _$TaskAbortedToken._$fromDatabase, + ); + + static TaskAbortedToken? _$fromDatabase(RowReader row) { + final runtimeVersion = row.readString(); + final package = row.readString(); + final token = row.readString(); + final expires = row.readDateTime(); + if (runtimeVersion == null && + package == null && + token == null && + expires == null) { + return null; + } + return _$TaskAbortedToken._(runtimeVersion!, package!, token!, expires!); + } + + @override + String toString() => + 'TaskAbortedToken(runtimeVersion: "$runtimeVersion", package: "$package", token: "$token", expires: "$expires")'; +} + +/// Extension methods for table defined in [TaskAbortedToken]. +extension TableTaskAbortedTokenExt on Table { + /// Insert row into the `taskAbortedTokens` table. + /// + /// Returns a [InsertSingle] statement on which `.execute` must be + /// called for the row to be inserted. + InsertSingle insert({ + required Expr runtimeVersion, + required Expr package, + required Expr token, + required Expr expires, + }) => $ForGeneratedCode.insertInto( + table: this, + values: [runtimeVersion, package, token, expires], + ); + + /// Insert row into the `taskAbortedTokens` table. + /// + /// Returns a [InsertSingle] statement on which `.execute` must be + /// called for the row to be inserted. + InsertSingle insertValue({ + required String runtimeVersion, + required String package, + required String token, + required DateTime expires, + }) => $ForGeneratedCode.insertInto( + table: this, + values: [ + runtimeVersion.asExpr, + package.asExpr, + token.asExpr, + expires.asExpr, + ], + ); + + /// Bulk insert rows into the `taskAbortedTokens` table. + /// + /// This method takes an `Iterable` and requires that you provide + /// a _mapping function_ from `T` to each column to be inserted. + /// + /// If a mapping function is omitted, the _default value_ will be + /// inserted, or `NULL` if column is nullable and as no default value. + /// To explicitely insert `NULL`, use a _mapping function_ that maps + /// `T` to `null`. + /// + /// > [!NOTE] + /// > This method aims utilize database specific bulk insertion logic + /// > to ensure good performance. Database adapters may pipeline bulk + /// > insertions through multiple statements inside a transaction. + /// + /// Returns a [Insert] statement on which `.execute` must be + /// called for the rows to be inserted. + Insert insertValuesMapped( + Iterable rows, { + required String Function(T row) runtimeVersion, + required String Function(T row) package, + required String Function(T row) token, + required DateTime Function(T row) expires, + }) => $ForGeneratedCode.insertValuesMapped( + table: this, + rows: rows, + mappings: [runtimeVersion, package, token, expires], + ); + + /// Delete a single row from the `taskAbortedTokens` table, specified by + /// _primary key_. + /// + /// Returns a [DeleteSingle] statement on which `.execute()` must be + /// called for the row to be deleted. + /// + /// To delete multiple rows, using `.where()` to filter which rows + /// should be deleted. If you wish to delete all rows, use + /// `.where((_) => toExpr(true)).delete()`. + DeleteSingle delete( + String runtimeVersion, + String package, + String token, + ) => $ForGeneratedCode.deleteSingle( + byKey(runtimeVersion, package, token), + _$TaskAbortedToken._$table, + ); +} + +/// Extension methods for building queries against the `taskAbortedTokens` table. +extension QueryTaskAbortedTokenExt on Query<(Expr,)> { + /// Lookup a single row in `taskAbortedTokens` table using the _primary key_. + /// + /// Returns a [QuerySingle] object, which returns at-most one row, + /// when `.fetch()` is called. + QuerySingle<(Expr,)> byKey( + String runtimeVersion, + String package, + String token, + ) => where( + (taskAbortedToken) => + taskAbortedToken.runtimeVersion.equalsValue(runtimeVersion) & + taskAbortedToken.package.equalsValue(package) & + taskAbortedToken.token.equalsValue(token), + ).first; + + /// Update all rows in the `taskAbortedTokens` table matching this [Query]. + /// + /// The changes to be applied to each row matching this [Query] are + /// defined using the [updateBuilder], which is given an [Expr] + /// representation of the row being updated and a `set` function to + /// specify which fields should be updated. The result of the `set` + /// function should always be returned from the `updateBuilder`. + /// + /// Returns an [Update] statement on which `.execute()` must be called + /// for the rows to be updated. + /// + /// **Example:** decrementing `1` from the `value` field for each row + /// where `value > 0`. + /// ```dart + /// await db.mytable + /// .where((row) => row.value > toExpr(0)) + /// .update((row, set) => set( + /// value: row.value - toExpr(1), + /// )) + /// .execute(); + /// ``` + /// + /// > [!WARNING] + /// > The `updateBuilder` callback does not make the update, it builds + /// > the expressions for updating the rows. You should **never** invoke + /// > the `set` function more than once, and the result should always + /// > be returned immediately. + Update update( + UpdateSet Function( + Expr taskAbortedToken, + UpdateSet Function({ + Expr runtimeVersion, + Expr package, + Expr token, + Expr expires, + }) + set, + ) + updateBuilder, + ) => $ForGeneratedCode.update( + this, + _$TaskAbortedToken._$table, + (taskAbortedToken) => updateBuilder( + taskAbortedToken, + ({ + Expr? runtimeVersion, + Expr? package, + Expr? token, + Expr? expires, + }) => $ForGeneratedCode.buildUpdate([ + runtimeVersion, + package, + token, + expires, + ]), + ), + ); + + /// Delete all rows in the `taskAbortedTokens` table matching this [Query]. + /// + /// Returns a [Delete] statement on which `.execute()` must be called + /// for the rows to be deleted. + Delete delete() => + $ForGeneratedCode.delete(this, _$TaskAbortedToken._$table); +} + +/// Extension methods for building point queries against the `taskAbortedTokens` table. +extension QuerySingleTaskAbortedTokenExt + on QuerySingle<(Expr,)> { + /// Update the row (if any) in the `taskAbortedTokens` table matching this + /// [QuerySingle]. + /// + /// The changes to be applied to the row matching this [QuerySingle] are + /// defined using the [updateBuilder], which is given an [Expr] + /// representation of the row being updated and a `set` function to + /// specify which fields should be updated. The result of the `set` + /// function should always be returned from the `updateBuilder`. + /// + /// Returns an [UpdateSingle] statement on which `.execute()` must be + /// called for the row to be updated. The resulting statement will + /// **not** fail, if there are no rows matching this query exists. + /// + /// **Example:** decrementing `1` from the `value` field the row with + /// `id = 1`. + /// ```dart + /// await db.mytable + /// .byKey(1) + /// .update((row, set) => set( + /// value: row.value - toExpr(1), + /// )) + /// .execute(); + /// ``` + /// + /// > [!WARNING] + /// > The `updateBuilder` callback does not make the update, it builds + /// > the expressions for updating the rows. You should **never** invoke + /// > the `set` function more than once, and the result should always + /// > be returned immediately. + UpdateSingle update( + UpdateSet Function( + Expr taskAbortedToken, + UpdateSet Function({ + Expr runtimeVersion, + Expr package, + Expr token, + Expr expires, + }) + set, + ) + updateBuilder, + ) => $ForGeneratedCode.updateSingle( + this, + _$TaskAbortedToken._$table, + (taskAbortedToken) => updateBuilder( + taskAbortedToken, + ({ + Expr? runtimeVersion, + Expr? package, + Expr? token, + Expr? expires, + }) => $ForGeneratedCode.buildUpdate([ + runtimeVersion, + package, + token, + expires, + ]), + ), + ); + + /// Delete the row (if any) in the `taskAbortedTokens` table matching this [QuerySingle]. + /// + /// Returns a [DeleteSingle] statement on which `.execute()` must be called + /// for the row to be deleted. The resulting statement will **not** + /// fail, if there are no rows matching this query exists. + DeleteSingle delete() => + $ForGeneratedCode.deleteSingle(this, _$TaskAbortedToken._$table); +} + +/// Extension methods for expressions on a row in the `taskAbortedTokens` table. +extension ExpressionTaskAbortedTokenExt on Expr { + Expr get runtimeVersion => + $ForGeneratedCode.field(this, 0, $ForGeneratedCode.text); + + Expr get package => + $ForGeneratedCode.field(this, 1, $ForGeneratedCode.text); + + Expr get token => + $ForGeneratedCode.field(this, 2, $ForGeneratedCode.text); + + Expr get expires => + $ForGeneratedCode.field(this, 3, $ForGeneratedCode.dateTime); + + /// Do a subquery lookup of the row from table + /// `tasks` referenced in + /// [runtimeVersion], [package]. + /// + /// The gets the row from table `tasks` where + /// [Task.runtimeVersion], [Task.package] + /// is equal to [runtimeVersion], [package]. + Expr get task => $ForGeneratedCode + .subqueryTable(_$Task._$table) + .where( + (r) => + r.runtimeVersion.equalsUnlessNull(runtimeVersion) & + r.package.equalsUnlessNull(package), + ) + .first + .asNotNull(); +} + +extension ExpressionNullableTaskAbortedTokenExt on Expr { + Expr get runtimeVersion => + $ForGeneratedCode.field(this, 0, $ForGeneratedCode.text); + + Expr get package => + $ForGeneratedCode.field(this, 1, $ForGeneratedCode.text); + + Expr get token => + $ForGeneratedCode.field(this, 2, $ForGeneratedCode.text); + + Expr get expires => + $ForGeneratedCode.field(this, 3, $ForGeneratedCode.dateTime); + + /// Do a subquery lookup of the row from table + /// `tasks` referenced in + /// [runtimeVersion], [package]. + /// + /// The gets the row from table `tasks` where + /// [Task.runtimeVersion], [Task.package] + /// is equal to [runtimeVersion], [package], if any. + /// + /// If this row is `NULL` the subquery is always return `NULL`. + Expr get task => $ForGeneratedCode + .subqueryTable(_$Task._$table) + .where( + (r) => + r.runtimeVersion.equalsUnlessNull(runtimeVersion) & + r.package.equalsUnlessNull(package), + ) + .first; + + /// Check if the row is not `NULL`. + /// + /// This will check if _primary key_ fields in this row are `NULL`. + /// + /// If this is a reference lookup by subquery it might be more efficient + /// to check if the referencing field is `NULL`. + Expr isNotNull() => + runtimeVersion.isNotNull() & package.isNotNull() & token.isNotNull(); + + /// Check if the row is `NULL`. + /// + /// This will check if _primary key_ fields in this row are `NULL`. + /// + /// If this is a reference lookup by subquery it might be more efficient + /// to check if the referencing field is `NULL`. + Expr isNull() => isNotNull().not(); +} + +extension InnerJoinTaskAbortedTokenTaskExt + on InnerJoin<(Expr,), (Expr,)> { + /// Join using the `task` _foreign key_. + /// + /// This will match rows where [TaskAbortedToken.runtimeVersion] = [Task.runtimeVersion] and [TaskAbortedToken.package] = [Task.package]. + Query<(Expr, Expr)> usingTask() => on( + (a, b) => + b.runtimeVersion.equalsUnlessNull(a.runtimeVersion) & + b.package.equalsUnlessNull(a.package), + ); +} + +extension LeftJoinTaskAbortedTokenTaskExt + on LeftJoin<(Expr,), (Expr,)> { + /// Join using the `task` _foreign key_. + /// + /// This will match rows where [TaskAbortedToken.runtimeVersion] = [Task.runtimeVersion] and [TaskAbortedToken.package] = [Task.package]. + Query<(Expr, Expr)> usingTask() => on( + (a, b) => + b.runtimeVersion.equalsUnlessNull(a.runtimeVersion) & + b.package.equalsUnlessNull(a.package), + ); +} + +extension RightJoinTaskAbortedTokenTaskExt + on RightJoin<(Expr,), (Expr,)> { + /// Join using the `task` _foreign key_. + /// + /// This will match rows where [TaskAbortedToken.runtimeVersion] = [Task.runtimeVersion] and [TaskAbortedToken.package] = [Task.package]. + Query<(Expr, Expr)> usingTask() => on( + (a, b) => + b.runtimeVersion.equalsUnlessNull(a.runtimeVersion) & + b.package.equalsUnlessNull(a.package), + ); +} + +/// `Table` conflict targets for use with `.onConflict`. +enum TaskAbortedTokenConflict { + /// Conflict with an existing row that has a matching primary key. + /// + /// Thus, the other row has matching values for: + /// `runtimeVersion`, `package`, `token`. + primaryKey(['runtime_version', 'package', 'token']); + + const TaskAbortedTokenConflict(this._fields); + + final List _fields; +} + +extension InsertTaskAbortedTokenExt on Insert { + /// Build an `INSERT` statement with an `ON CONFLICT` clause. + /// + /// The [target] argument specifies the _conflict target_ to be + /// handled. The _conflict target_ is always a `UNIQUE` constraint or + /// `PRIMARY KEY` constraint. + /// + /// If a row to be inserted violates the _conflict target_ constraint, + /// then the conflict action is triggered: + /// * `.doNothing()` to skip insertion of the new row, and, + /// * `.update((taskAbortedToken, excluded, set) => set(...))` to + /// update the conflicting row. + /// + /// If a row to be inserted violates a constraint other than the one + /// specified in _conflict target_ then the entire `INSERT` statement + /// will fail. + /// + /// This is equivalent to `INSERT ... ON CONFLICT (...)` in SQL. + InsertOnConflict onConflict( + TaskAbortedTokenConflict target, + ) => $ForGeneratedCode.insertOnConflict(this, target._fields); +} + +extension InsertOnConflictTaskAbortedTokenExt + on InsertOnConflict { + /// Build an `INSERT` statement an [upsert-clause][1]. + /// + /// When a row to be inserted violates the `UNIQUE` or `PRIMARY KEY` + /// constraint previously specified as _conflict target_, the existing + /// row is updated using the expressions defined with the + /// [updateBuilder]. The [updateBuilder] is given 3 parameters: + /// * `taskAbortedToken` an [Expr] representing the existing row in + /// the database, + /// * `excluded` an [Expr] representing the row to be inserted in the + /// database, and, + /// * `set` a function to specify which fields should be updated and + /// build the [UpdateSet]. + /// + /// The result of the `set` function should always be immediately + /// returned from the [updateBuilder]. + /// + /// **Example:** Insert a counter with `count = 2` or increment the + /// existing row, if a `PRIMARY KEY` conflict occurs. + /// ```dart + /// await db.counters.insertValue( + /// name: 'my-counter', // primary key + /// count: 2, + /// ) + /// .onConflict(.primaryKey) + /// .update((counter, excluded, set) => set( + /// count: counter.count + excluded.count, + /// )) + /// .execute(); + /// ``` + /// + /// This is equivalent to + /// `INSERT ... ON CONFLICT (...) UPDATE SET ...` in SQL. + /// + /// > [!WARNING] + /// > The `updateBuilder` callback does not make the update, it builds + /// > the expressions for updating the rows. You should **never** invoke + /// > the `set` function more than once, and the result should always + /// > be returned immediately. + /// + /// [1]: https://www.sqlite.org/lang_upsert.html + Upsert update( + UpdateSet Function( + Expr taskAbortedToken, + Expr excluded, + UpdateSet Function({ + Expr runtimeVersion, + Expr package, + Expr token, + Expr expires, + }) + set, + ) + updateBuilder, + ) => $ForGeneratedCode.updateOnConflict( + this, + (taskAbortedToken, excluded) => updateBuilder( + taskAbortedToken, + excluded, + ({ + Expr? runtimeVersion, + Expr? package, + Expr? token, + Expr? expires, + }) => $ForGeneratedCode.buildUpdate([ + runtimeVersion, + package, + token, + expires, + ]), + ), + ); +} + +extension InsertSingleTaskAbortedTokenExt on InsertSingle { + /// Build an `INSERT` statement with an `ON CONFLICT` clause. + /// + /// The [target] argument specifies the _conflict target_ to be + /// handled. The _conflict target_ is always a `UNIQUE` constraint or + /// `PRIMARY KEY` constraint. + /// + /// If a row to be inserted violates the _conflict target_ constraint, + /// then the conflict action is triggered: + /// * `.doNothing()` to skip insertion of the new row, and, + /// * `.update((taskAbortedToken, excluded, set) => set(...))` to + /// update the conflicting row. + /// + /// If a row to be inserted violates a constraint other than the one + /// specified in _conflict target_ then the entire `INSERT` statement + /// will fail. + /// + /// This is equivalent to `INSERT ... ON CONFLICT (...)` in SQL. + InsertOnConflictSingle onConflict( + TaskAbortedTokenConflict target, + ) => $ForGeneratedCode.insertOnConflictSingle(this, target._fields); +} + +extension InsertOnConflictSingleTaskAbortedTokenExt + on InsertOnConflictSingle { + /// Build an `INSERT` statement an [upsert-clause][1]. + /// + /// When a row to be inserted violates the `UNIQUE` or `PRIMARY KEY` + /// constraint previously specified as _conflict target_, the existing + /// row is updated using the expressions defined with the + /// [updateBuilder]. The [updateBuilder] is given 3 parameters: + /// * `taskAbortedToken` an [Expr] representing the existing row in + /// the database, + /// * `excluded` an [Expr] representing the row to be inserted in the + /// database, and, + /// * `set` a function to specify which fields should be updated and + /// build the [UpdateSet]. + /// + /// The result of the `set` function should always be immediately + /// returned from the [updateBuilder]. + /// + /// **Example:** Insert a counter with `count = 2` or increment the + /// existing row, if a `PRIMARY KEY` conflict occurs. + /// ```dart + /// await db.counters.insertValue( + /// name: 'my-counter', // primary key + /// count: 2, + /// ) + /// .onConflict(.primaryKey) + /// .update((counter, excluded, set) => set( + /// count: counter.count + excluded.count, + /// )) + /// .execute(); + /// ``` + /// + /// This is equivalent to + /// `INSERT ... ON CONFLICT (...) UPDATE SET ...` in SQL. + /// + /// > [!WARNING] + /// > The `updateBuilder` callback does not make the update, it builds + /// > the expressions for updating the rows. You should **never** invoke + /// > the `set` function more than once, and the result should always + /// > be returned immediately. + /// + /// [1]: https://www.sqlite.org/lang_upsert.html + UpsertSingle update( + UpdateSet Function( + Expr taskAbortedToken, + Expr excluded, + UpdateSet Function({ + Expr runtimeVersion, + Expr package, + Expr token, + Expr expires, + }) + set, + ) + updateBuilder, + ) => $ForGeneratedCode.updateOnConflictSingle( + this, + (taskAbortedToken, excluded) => updateBuilder( + taskAbortedToken, + excluded, + ({ + Expr? runtimeVersion, + Expr? package, + Expr? token, + Expr? expires, + }) => $ForGeneratedCode.buildUpdate([ + runtimeVersion, + package, + token, + expires, + ]), + ), + ); +} + /// Extension methods for building queries projected to a named record. extension QueryContentNameNamed on Query<({Expr content, Expr name})> { diff --git a/app/lib/database/schema.task.dart b/app/lib/database/schema.task.dart index ee84385d44..d3721b8cdc 100644 --- a/app/lib/database/schema.task.dart +++ b/app/lib/database/schema.task.dart @@ -48,6 +48,65 @@ abstract final class TaskDependency extends Row { String get dependency; } +@PrimaryKey(['runtimeVersion', 'package', 'version']) +@ForeignKey( + ['runtimeVersion', 'package'], + table: 'tasks', + fields: ['runtimeVersion', 'package'], + name: 'task', + as: 'versionRows', + onDelete: .cascade, + onUpdate: .cascade, +) +abstract final class TaskVersion extends Row { + String get runtimeVersion; + String get package; + String get version; + + /// The last time when this was scheduled for analysis. + DateTime get scheduled; + + /// The number of attempts to schedule the package version for analysis. + int get attempts; + + /// Name of the zone in which the instance analysing this package version is + /// running. + String? get zone; + + /// Name of the instance analysing this package version. + String? get instance; + + /// Secret token (UUIDv4) used for authenticating worker requests. + String? get secretToken; + + /// True, if dartdoc documentation is available. + bool get hasDocs; + + /// True, if pana summary is available. + bool get hasPana; + + /// True, if results have been previously reported on this version. + bool get isFinished; +} + +@PrimaryKey(['runtimeVersion', 'package', 'token']) +@ForeignKey( + ['runtimeVersion', 'package'], + table: 'tasks', + fields: ['runtimeVersion', 'package'], + name: 'task', + as: 'abortedTokenRows', + onDelete: .cascade, + onUpdate: .cascade, +) +abstract final class TaskAbortedToken extends Row { + String get runtimeVersion; + String get package; + String get token; + + DateTime get expires; +} + @immutable @JsonSerializable() final class TaskState implements CustomDataType { diff --git a/app/lib/task/backend.dart b/app/lib/task/backend.dart index fdbdd77118..992fcb9c3a 100644 --- a/app/lib/task/backend.dart +++ b/app/lib/task/backend.dart @@ -410,11 +410,12 @@ class TaskBackend { attempts: 0, ), }; + final newState = TaskState(versions: versionsMap, abortedTokens: []); await db.tasks .insert( runtimeVersion: runtimeVersion.asExpr, package: packageName.asExpr, - state: TaskState(versions: versionsMap, abortedTokens: []).asExpr, + state: newState.asExpr, lastDependencyChanged: initialTimestamp.asExpr, finished: initialTimestamp.asExpr, pendingAt: derivePendingAt( @@ -423,6 +424,7 @@ class TaskBackend { ).asExpr, ) .execute(); + await db.upsertTaskState(packageName, newState); return true; // no more work for this package, state is synced } @@ -495,6 +497,7 @@ class TaskBackend { ), ) .execute(); + await db.upsertTaskState(packageName, newState, oldState: oldState); return true; }); @@ -759,19 +762,21 @@ class TaskBackend { lastDependencyChanged: task.lastDependencyChanged, ); + final newState = TaskState( + versions: newVersions, + abortedTokens: task.state.abortedTokens, + ); await db.tasks .byKey(runtimeVersion, package) .update( (_, set) => set( - state: TaskState( - versions: newVersions, - abortedTokens: task.state.abortedTokens, - ).asExpr, + state: newState.asExpr, pendingAt: pendingAt.asExpr, finished: clock.now().toUtc().asExpr, ), ) .execute(); + await db.upsertTaskState(package, newState, oldState: task.state); }); // Clearing the state cache after the update. @@ -1387,4 +1392,89 @@ extension TaskDatabaseExt on Database { .update((_, set) => set(pendingAt: initialTimestamp.asExpr)) .execute(); } + + /// Keeps `task_versions` and `task_aborted_tokens` in sync with [state], + /// which must always be written to `tasks.state` too. + /// + /// [oldState] is the previously stored state (if any), it is used to + /// find entries that have been removed from [state] and need to be + /// deleted. It can be omitted when [package] has no prior state, e.g. + /// when inserting a new [Task] row. + Future upsertTaskState( + String package, + TaskState state, { + TaskState? oldState, + }) async { + for (final entry in state.versions.entries) { + final v = entry.value; + // TODO: use typed_sql's upsertValue once it becomes available + await taskVersions + .insertValue( + runtimeVersion: runtimeVersion, + package: package, + version: entry.key, + scheduled: v.scheduled, + attempts: v.attempts, + zone: v.zone, + instance: v.instance, + secretToken: v.secretToken, + hasDocs: v.docs, + hasPana: v.pana, + isFinished: v.finished, + ) + .onConflict(.primaryKey) + .update( + (_, excluded, set) => set( + scheduled: excluded.scheduled, + attempts: excluded.attempts, + zone: excluded.zone, + instance: excluded.instance, + secretToken: excluded.secretToken, + hasDocs: excluded.hasDocs, + hasPana: excluded.hasPana, + isFinished: excluded.isFinished, + ), + ) + .execute(); + } + + for (final token in state.abortedTokens) { + // TODO: use typed_sql's upsertValue once it becomes available + await taskAbortedTokens + .insertValue( + runtimeVersion: runtimeVersion, + package: package, + token: token.token, + expires: token.expires, + ) + .onConflict(.primaryKey) + .update((_, excluded, set) => set(expires: excluded.expires)) + .execute(); + } + + if (oldState == null) { + return; + } + + final removedVersions = oldState.versions.keys.whereNot( + state.versions.containsKey, + ); + for (final version in removedVersions) { + await taskVersions + .byKey(runtimeVersion, package, version) + .delete() + .execute(); + } + + final currentTokens = state.abortedTokens.map((t) => t.token).toSet(); + final removedTokens = oldState.abortedTokens + .map((t) => t.token) + .whereNot(currentTokens.contains); + for (final token in removedTokens) { + await taskAbortedTokens + .byKey(runtimeVersion, package, token) + .delete() + .execute(); + } + } } diff --git a/app/lib/task/scheduler.dart b/app/lib/task/scheduler.dart index e393c708eb..a769360474 100644 --- a/app/lib/task/scheduler.dart +++ b/app/lib/task/scheduler.dart @@ -186,6 +186,10 @@ Future<(CreateInstancesState, Duration)> runOneCreateInstancesCycle( return; // Presumably, the package was deleted. } + final oldState = TaskState( + versions: {...s.state.versions}, + abortedTokens: s.state.abortedTokens, + ); final versions = s.state.versions; versions.addEntries( versions.entries @@ -193,14 +197,15 @@ Future<(CreateInstancesState, Duration)> runOneCreateInstancesCycle( .map((e) => MapEntry(e.key, e.value.resetAfterFailedAttempt())), ); + final newState = TaskState( + versions: versions, + abortedTokens: s.state.abortedTokens, + ); await db.tasks .byKey(runtimeVersion, selected.package) .update( (_, set) => set( - state: TaskState( - versions: versions, - abortedTokens: s.state.abortedTokens, - ).asExpr, + state: newState.asExpr, pendingAt: derivePendingAt( versions: versions, lastDependencyChanged: s.lastDependencyChanged, @@ -208,6 +213,11 @@ Future<(CreateInstancesState, Duration)> runOneCreateInstancesCycle( ), ) .execute(); + await db.upsertTaskState( + selected.package, + newState, + oldState: oldState, + ); }); } } @@ -281,14 +291,15 @@ Future updatePackageStateWithPendingVersions( secretToken: createUuid(), ), }; + final newState = TaskState( + versions: newVersions, + abortedTokens: task.state.abortedTokens, + ); await db.tasks .byKey(runtimeVersion, package) .update( (_, set) => set( - state: TaskState( - versions: newVersions, - abortedTokens: task.state.abortedTokens, - ).asExpr, + state: newState.asExpr, pendingAt: derivePendingAt( versions: newVersions, lastDependencyChanged: task.lastDependencyChanged, @@ -296,6 +307,7 @@ Future updatePackageStateWithPendingVersions( ), ) .execute(); + await db.upsertTaskState(package, newState, oldState: task.state); // Create payload final payload = Payload( diff --git a/app/migrations/000004_task_state.sql b/app/migrations/000004_task_state.sql new file mode 100644 index 0000000000..27394d7906 --- /dev/null +++ b/app/migrations/000004_task_state.sql @@ -0,0 +1,26 @@ +-- Create "task_aborted_tokens" table +CREATE TABLE "task_aborted_tokens" ( + "runtime_version" text NOT NULL, + "package" text NOT NULL, + "token" text NOT NULL, + "expires" timestamptz NOT NULL, + PRIMARY KEY ("runtime_version", "package", "token"), + CONSTRAINT "task_aborted_tokens_fk_task" FOREIGN KEY ("runtime_version", "package") REFERENCES "tasks" ("runtime_version", "package") ON UPDATE CASCADE ON DELETE CASCADE +); + +-- Create "task_versions" table +CREATE TABLE "task_versions" ( + "runtime_version" text NOT NULL, + "package" text NOT NULL, + "version" text NOT NULL, + "scheduled" timestamptz NOT NULL, + "attempts" bigint NOT NULL, + "zone" text NULL, + "instance" text NULL, + "secret_token" text NULL, + "has_docs" boolean NOT NULL, + "has_pana" boolean NOT NULL, + "is_finished" boolean NOT NULL, + PRIMARY KEY ("runtime_version", "package", "version"), + CONSTRAINT "task_versions_fk_task" FOREIGN KEY ("runtime_version", "package") REFERENCES "tasks" ("runtime_version", "package") ON UPDATE CASCADE ON DELETE CASCADE +); diff --git a/app/migrations/sha256sum.txt b/app/migrations/sha256sum.txt index 2539cb6afe..042165cb49 100644 --- a/app/migrations/sha256sum.txt +++ b/app/migrations/sha256sum.txt @@ -1,3 +1,4 @@ edfd3d977ef308147b29a609c48f3913608e4888d073479995bdb8b37a643806 000001_task.sql 2bc197ecebfa5cc1511ec252141e177880e824b4e939e76e2128106a256ab791 000002_task_index.sql 238d99ddc31396bd399825a027242fc65726f51281ad746967551dbd3b129f6e 000003_user_session.sql +b70b766f08510b75f1b2a651523c006e3ddecb143e012b8aacc84272e74db1b1 000004_task_state.sql