generateForAnnotatedElement method
- Element element,
- ConstantReader annotation,
- BuildStep buildStep
Implement to return source code to generate for element.
This method is invoked based on finding elements annotated with an
instance of T. The annotation is provided as a ConstantReader.
Supported return values include a single String or multiple String instances within an Iterable or Stream. It is also valid to return a Future of String, Iterable, or Stream. When multiple values are returned through an iterable or stream they will be deduplicated. Typically each value will be an independent unit of code and the deduplication prevents re-defining the same member multiple times. For example if multiple annotated elements may need a specific utility method available it can be output for each one, and the single deduplicated definition can be shared.
Implementations should return null when no content is generated. Empty
or whitespace-only String instances are also ignored.
Implementation
@override
Future<String> generateForAnnotatedElement(
Element element,
ConstantReader annotation,
BuildStep buildStep,
) async {
if (element is! ClassElement) {
throw InvalidGenerationSourceError(
'@Db can only be applied to classes',
element: element,
);
}
final className = element.name;
final migrationVersion = annotation.peek('migrationVersion')?.intValue ?? 1;
final sqlDialectValue = annotation.peek('sqlDialect')?.objectValue;
final dbName =
annotation.peek('name')?.stringValue ?? className!.toLowerCase();
// Extract DbConfig from annotation
final configInfo = _extractDbConfig(annotation);
// Determine SQL dialect
String sqlDialect = 'postgresql';
if (sqlDialectValue != null) {
final dialectName = sqlDialectValue.getField('_name')?.toStringValue();
if (dialectName != null) {
sqlDialect = dialectName;
}
} else if (configInfo != null) {
sqlDialect = configInfo.dbType;
}
// Get schema storage path
final inputPath = buildStep.inputId.path;
final packageRoot = inputPath.split('lib/').first;
final schemaFilePath = '$packageRoot.dorm/$dbName.schema.json';
// Extract entity types and their schema info from annotation
final entitiesReader = annotation.peek('entities');
final entities = <_EntityInfo>[];
final manyToManyRelations = <_ManyToManyInfo>[];
final entityElements = <String, ClassElement>{};
final entityToTableName = <String, String>{};
// Collect OneToMany relationships to add FK columns to target entities
final oneToManyRelations = <_OneToManyInfo>[];
if (entitiesReader != null && !entitiesReader.isNull) {
final entityList = entitiesReader.listValue;
// First pass: collect all entity elements and their table names
for (final entityValue in entityList) {
final typeValue = entityValue.toTypeValue();
if (typeValue != null) {
final entityElement = typeValue.element;
if (entityElement is ClassElement && entityElement.name != null) {
entityElements[entityElement.name!] = entityElement;
// Extract table name from @Entity annotation
String tableName = _toSnakeCase(entityElement.name!);
for (final meta in entityElement.metadata.annotations) {
if (meta.element?.enclosingElement?.name == 'Entity') {
final source = meta.toSource();
final tableMatch = RegExp(
'tableName:\\s*[\'"]([\\w_]+)[\'"]',
).firstMatch(source);
if (tableMatch != null) {
tableName = tableMatch.group(1)!;
}
break;
}
}
entityToTableName[entityElement.name!] = tableName;
}
}
}
// Second pass: extract OneToMany relationships first
for (final entityValue in entityList) {
final typeValue = entityValue.toTypeValue();
if (typeValue != null) {
final entityElement = typeValue.element;
if (entityElement is ClassElement && entityElement.name != null) {
final tableName = entityToTableName[entityElement.name!]!;
// Extract OneToMany relationships
final o2mRelations = _extractOneToManyRelations(
entityElement,
tableName,
entityToTableName,
);
oneToManyRelations.addAll(o2mRelations);
}
}
}
// Third pass: extract entity info and relationships
for (final entityValue in entityList) {
final typeValue = entityValue.toTypeValue();
if (typeValue != null) {
final entityElement = typeValue.element;
if (entityElement is ClassElement && entityElement.name != null) {
final tableName = entityToTableName[entityElement.name!]!;
// Extract fields/columns from entity
final columns = _extractColumnsFromEntity(entityElement);
// Extract ManyToOne relationships and add FK columns if not already present
final manyToOneRelations = _extractManyToOneRelations(
entityElement,
tableName,
entityToTableName,
);
// Add FK columns from ManyToOne relationships if not already in columns
for (final m2oRel in manyToOneRelations) {
final fkColumnExists = columns.any(
(c) => c.name == m2oRel.foreignKeyColumn,
);
if (!fkColumnExists) {
columns.add(
_ColumnInfo(
name: m2oRel.foreignKeyColumn,
dartType: 'int?',
sqlType: 'INTEGER',
isNullable: m2oRel.nullable,
isPrimaryKey: false,
),
);
}
}
// Add FK columns from OneToMany relationships (where this entity is the target)
for (final o2mRel in oneToManyRelations) {
if (o2mRel.targetTableName == tableName) {
final fkColumnExists = columns.any(
(c) => c.name == o2mRel.foreignKeyColumn,
);
if (!fkColumnExists) {
columns.add(
_ColumnInfo(
name: o2mRel.foreignKeyColumn,
dartType: 'int?',
sqlType: 'INTEGER',
isNullable: true,
isPrimaryKey: false,
),
);
}
// Also add to manyToOneRelations for FK constraint generation
manyToOneRelations.add(
_ManyToOneInfo(
ownerEntityName: o2mRel.targetEntityName,
ownerTableName: o2mRel.targetTableName,
targetEntityName: o2mRel.ownerEntityName,
targetTableName: o2mRel.ownerTableName,
fieldName: '',
foreignKeyColumn: o2mRel.foreignKeyColumn,
referencedColumn: o2mRel.referencedColumn,
nullable: true,
onDelete: o2mRel.onDelete,
onUpdate: o2mRel.onUpdate,
),
);
}
}
// Extract ManyToMany relationships
final m2mRelations = _extractManyToManyRelations(
entityElement,
tableName,
entityElements,
entityToTableName,
);
manyToManyRelations.addAll(m2mRelations);
entities.add(
_EntityInfo(
className: entityElement.name!,
repositoryName: '${entityElement.name}Repository',
tableName: tableName,
columns: columns,
manyToOneRelations: manyToOneRelations,
),
);
}
}
}
}
// Deduplicate junction tables (keep only owning side)
final junctionTables = _deduplicateJunctionTables(
manyToManyRelations,
entityToTableName,
);
// Generate current schema as JSON
final currentSchema = _schemaToJson(entities, junctionTables);
final schemaHash = _generateSchemaHash(entities, junctionTables);
// Migration history file path
final migrationsFilePath = '$packageRoot.dorm/$dbName.migrations.json';
// Load previous schema and migration history
final previousSchema = await _loadPreviousSchema(schemaFilePath);
final migrationHistory = await _loadMigrationHistory(migrationsFilePath);
// Compare schemas to detect changes
final schemaChanges = _compareSchemas(previousSchema, currentSchema);
// Determine if we need a new migration
final hasChanges = schemaChanges.isNotEmpty;
final isInitial = previousSchema == null;
// Update migration history if there are changes
if (hasChanges && !isInitial) {
await _addMigrationToHistory(
migrationsFilePath,
migrationHistory,
migrationVersion,
schemaChanges,
schemaHash,
);
} else if (isInitial) {
// Initialize migration history with version 1
await _initializeMigrationHistory(
migrationsFilePath,
schemaHash,
);
}
// Save current schema for next comparison
await _saveSchema(schemaFilePath, currentSchema);
// Reload migration history after updates
final updatedHistory = await _loadMigrationHistory(migrationsFilePath);
return _generateMigrationCode(
className: className!,
entities: entities,
junctionTables: junctionTables,
migrationVersion: migrationVersion,
sqlDialect: sqlDialect,
schemaHash: schemaHash,
previousSchema: previousSchema,
schemaChanges: schemaChanges,
migrationHistory: updatedHistory,
);
}