diff --git a/CHANGELOG.md b/CHANGELOG.md index ec4a75b..0aebe04 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,57 @@ +## 1.9.25 + +- `TableFieldReference`: + - Added nullable field `indexName` to represent the name of the index if one exists. + +- Added new class `TableRelationshipReferenceEntityTyped` extending `TableRelationshipReference`: + - Adds `sourceFieldEntityType` and `targetFieldEntityType` fields of type `TypeInfo`. + - Provides `copyWithEntityTypes` method to create typed copies. + +- `TableRelationshipReference`: + - Added nullable fields `sourceRelationshipFieldIndex` and `targetRelationshipFieldIndex`. + - Added `copyWithEntityTypes` method returning `TableRelationshipReferenceEntityTyped`. + +- `EntityHandler`: + - Added `getFieldsListEntityTypes` method to return a map of fields that are list entities or references with their `TypeInfo`. + +- `SQLDialect`: + - Added `foreignKeyCreatesImplicitIndex` boolean flag with default `true`. + - Added field `createIndexIfNotExists` to indicate support for `IF NOT EXISTS` in `CREATE INDEX` (default `true`). + +- `CreateIndexSQL`: + - Updated `buildSQL` method to conditionally include `IF NOT EXISTS` only if dialect supports it. + +- `DBPostgreSQLAdapter`: + - Added `foreignKeyCreatesImplicitIndex` flag to PostgreSQL dialect set to `false`. + - Updated `_findAllTableFieldsReferences` query to include foreign key index name (`fk_index_name`) by joining with `pg_index` and `pg_class`. + - Populated `indexName` in `TableFieldReference` instances from query result. + - Updated relationship references to include `sourceRelationshipFieldIndex` and `targetRelationshipFieldIndex` from `indexName`. + +- `DBMySQLAdapter`: + - Set `createIndexIfNotExists` to `false` in MySQL dialect capabilities. + +- `DBSQLAdapter`: + - `parseConfigDBGenerateTablesAndCheckTables`: changed return type from `List` to a record with named fields `(generateTables, checkTables)`. + - `extractTableSQLs`: updated regex to also match `CREATE INDEX` statements in addition to `CREATE` and `ALTER TABLE`. + - `_populateTablesFromSQLsImpl`: fixed error handling for `CREATE INDEX` statements when the SQL dialect does not support `IF NOT EXISTS`. + - Now logs a warning and ignores the error instead of throwing. + - Added detection of missing foreign key indexes when dialect does not create implicit indexes. + - Added detection of missing relationship reference indexes for collection reference fields. + - Updated error reporting and logging to include missing reference indexes and relationship reference indexes. + - Updated `_checkDBTableSchemeReferenceField` to return `TableRelationshipReferenceEntityTyped` with entity types. + - Added generation of missing reference indexes and missing relationship reference indexes SQL statements. + - Updated `_DBTableCheck` class: + - Added fields `missingReferenceIndexes` and `missingRelationshipReferenceIndexes`. + - Added methods to generate missing reference indexes and relationship reference indexes SQL. + - Added `_DBRelationshipTableColumn` subclass of `_DBTableColumn` to represent relationship table columns with relationship table name. + - Updated SQL generation to create indexes for foreign keys if dialect does not create implicit indexes: + - Added index creation after foreign key constraints in `generateAddColumnAlterTableSQL`. + - Added index creation for relationship table foreign keys in relationship table creation SQL. + +- Dependency updates: + - `async_extension`: ^1.2.19 → ^1.2.20 + - `meta`: ^1.18.0 → ^1.18.1 + ## 1.9.24 - `GZipSink`: diff --git a/lib/src/bones_api_base.dart b/lib/src/bones_api_base.dart index 073a95e..4a67dac 100644 --- a/lib/src/bones_api_base.dart +++ b/lib/src/bones_api_base.dart @@ -48,7 +48,7 @@ typedef APILogger = /// Bones API Library class. class BonesAPI { // ignore: constant_identifier_names - static const String VERSION = '1.9.24'; + static const String VERSION = '1.9.25'; static bool _boot = false; diff --git a/lib/src/bones_api_condition_encoder.dart b/lib/src/bones_api_condition_encoder.dart index d229405..a8047a5 100644 --- a/lib/src/bones_api_condition_encoder.dart +++ b/lib/src/bones_api_condition_encoder.dart @@ -45,14 +45,18 @@ class TableFieldReference { /// The target table field type. final Type targetFieldType; + /// The name of the index, if one exists. + final String? indexName; + TableFieldReference( this.sourceTable, this.sourceField, this.sourceFieldType, this.targetTable, this.targetField, - this.targetFieldType, - ); + this.targetFieldType, { + String? indexName, + }) : indexName = indexName != null && indexName.isNotEmpty ? indexName : null; @override bool operator ==(Object other) => @@ -73,7 +77,48 @@ class TableFieldReference { @override String toString() { - return 'TableFieldReference{"$sourceTable"."$sourceField"($sourceFieldType) -> "$targetTable"."$targetField"($targetFieldType)}'; + return 'TableFieldReference{' + '"$sourceTable"."$sourceField"($sourceFieldType) -> "$targetTable"."$targetField"($targetFieldType)' + '}${indexName != null ? '@$indexName' : ''}'; + } +} + +/// A [TableRelationshipReference] that explicitly defines the entity types +/// of its relationship fields. +/// +/// - [sourceFieldEntityType] represents the entity type of the [sourceField]. +/// - [targetFieldEntityType] represents the entity type of the [targetField]. +class TableRelationshipReferenceEntityTyped extends TableRelationshipReference { + /// The entity type of the [sourceField]. + TypeInfo sourceFieldEntityType; + + /// The entity type of the [targetField]. + TypeInfo targetFieldEntityType; + + TableRelationshipReferenceEntityTyped( + super.relationshipTable, + super.sourceTable, + super.sourceField, + super.sourceFieldType, + this.sourceFieldEntityType, + super.sourceRelationshipField, + super.targetTable, + super.targetField, + super.targetFieldType, + this.targetFieldEntityType, + super.targetRelationshipField, { + super.relationshipField, + super.sourceRelationshipFieldIndex, + super.targetRelationshipFieldIndex, + }); + + @override + String toString() { + return 'TableRelationshipReferenceEntityTyped[$relationshipTable]{' + '"$sourceTable"."$sourceField"($sourceFieldType @ $sourceFieldEntityType) -> "$targetTable"."$targetField"($targetFieldType @ $targetFieldEntityType)' + '}' + '${sourceRelationshipFieldIndex != null ? '$sourceField@$sourceRelationshipFieldIndex' : ''}' + '${targetRelationshipFieldIndex != null ? '$targetField@$targetRelationshipFieldIndex' : ''}'; } } @@ -109,6 +154,12 @@ class TableRelationshipReference { /// The virtual/entity relationship field name. final String? relationshipField; + /// The index of the [sourceRelationshipField] + final String? sourceRelationshipFieldIndex; + + /// The index of the [targetRelationshipField] + final String? targetRelationshipFieldIndex; + TableRelationshipReference( this.relationshipTable, this.sourceTable, @@ -120,13 +171,45 @@ class TableRelationshipReference { this.targetFieldType, this.targetRelationshipField, { this.relationshipField, - }); + String? sourceRelationshipFieldIndex, + String? targetRelationshipFieldIndex, + }) : sourceRelationshipFieldIndex = + sourceRelationshipFieldIndex != null && + sourceRelationshipFieldIndex.isNotEmpty + ? sourceRelationshipFieldIndex + : null, + targetRelationshipFieldIndex = + targetRelationshipFieldIndex != null && + targetRelationshipFieldIndex.isNotEmpty + ? targetRelationshipFieldIndex + : null; + + /// Returns a copy as [TableRelationshipReference], + /// with entity types [sourceFieldEntityType] and [targetFieldEntityType]. + TableRelationshipReferenceEntityTyped copyWithEntityTypes( + TypeInfo sourceFieldEntityType, + TypeInfo targetFieldEntityType, + ) => TableRelationshipReferenceEntityTyped( + relationshipTable, + sourceTable, + sourceField, + sourceFieldType, + sourceFieldEntityType, + sourceRelationshipField, + targetTable, + targetField, + targetFieldType, + targetFieldEntityType, + targetRelationshipField, + relationshipField: relationshipField, + sourceRelationshipFieldIndex: sourceRelationshipFieldIndex, + targetRelationshipFieldIndex: targetRelationshipFieldIndex, + ); @override bool operator ==(Object other) => identical(this, other) || other is TableRelationshipReference && - runtimeType == other.runtimeType && relationshipTable == other.relationshipTable && sourceTable == other.sourceTable && sourceField == other.sourceField && @@ -143,7 +226,11 @@ class TableRelationshipReference { @override String toString() { - return 'TableRelationshipReference[$relationshipTable]{"$sourceTable"."$sourceField"($sourceFieldType) -> "$targetTable"."$targetField"($targetFieldType)}'; + return 'TableRelationshipReference[$relationshipTable]{' + '"$sourceTable"."$sourceField"($sourceFieldType) -> "$targetTable"."$targetField"($targetFieldType)' + '}' + '${sourceRelationshipFieldIndex != null ? '$sourceField@$sourceRelationshipFieldIndex' : ''}' + '${targetRelationshipFieldIndex != null ? '$targetField@$targetRelationshipFieldIndex' : ''}'; } } diff --git a/lib/src/bones_api_entity.dart b/lib/src/bones_api_entity.dart index 1ad5e09..2799a8d 100644 --- a/lib/src/bones_api_entity.dart +++ b/lib/src/bones_api_entity.dart @@ -1625,6 +1625,37 @@ abstract class EntityHandler with FieldsFromMap, EntityRulesResolver { ); } + Map? _fieldsListEntityTypes; + + Map getFieldsListEntityTypes([O? o]) { + var listEntityFields = _fieldsListEntityTypes; + if (listEntityFields != null) return listEntityFields; + + var enumFields = getFieldsEnumTypes(o); + + var mapListEntityFields = + getFieldsTypes().entries + .map((e) { + var field = e.key; + var typeInfo = e.value; + + if (enumFields.containsKey(field)) return null; + + if (!typeInfo.isListEntityOrReference) return null; + + var entityType = typeInfo.entityType; + if (entityType == null) return null; + + return MapEntry(field, typeInfo); + }) + .nonNulls + .toMapFromEntries(); + + return _fieldsListEntityTypes = Map.unmodifiable( + mapListEntityFields, + ); + } + List? getFieldEntityAnnotations(O? o, String key); Map>? getAllFieldsEntityAnnotations([O? o]) { diff --git a/lib/src/bones_api_entity_db_memory.dart b/lib/src/bones_api_entity_db_memory.dart index 087c302..c807544 100644 --- a/lib/src/bones_api_entity_db_memory.dart +++ b/lib/src/bones_api_entity_db_memory.dart @@ -130,11 +130,10 @@ class DBSQLMemoryAdapter extends DBSQLAdapter }) { boot(); - var retCheckTablesAndGenerateTables = - DBSQLAdapter.parseConfigDBGenerateTablesAndCheckTables(config); - - var generateTables = retCheckTablesAndGenerateTables[0]; - var checkTables = retCheckTablesAndGenerateTables[1]; + var ( + generateTables: generateTables, + checkTables: checkTables, + ) = DBSQLAdapter.parseConfigDBGenerateTablesAndCheckTables(config); var populate = config?['populate']; Object? populateTables; diff --git a/lib/src/bones_api_entity_db_mysql.dart b/lib/src/bones_api_entity_db_mysql.dart index 8f35019..37f48ad 100644 --- a/lib/src/bones_api_entity_db_mysql.dart +++ b/lib/src/bones_api_entity_db_mysql.dart @@ -108,6 +108,7 @@ class DBMySQLAdapter extends DBSQLAdapter elementQuote: '`', acceptsTemporaryTableForReturning: true, acceptsInsertIgnore: true, + createIndexIfNotExists: false, ), transactions: true, transactionAbort: true, @@ -161,11 +162,10 @@ class DBMySQLAdapter extends DBSQLAdapter minConnections ??= 1; maxConnections ??= 3; - var retCheckTablesAndGenerateTables = - DBSQLAdapter.parseConfigDBGenerateTablesAndCheckTables(config); - - var generateTables = retCheckTablesAndGenerateTables[0]; - var checkTables = retCheckTablesAndGenerateTables[1]; + var ( + generateTables: generateTables, + checkTables: checkTables, + ) = DBSQLAdapter.parseConfigDBGenerateTablesAndCheckTables(config); var populate = config?['populate']; Object? populateTables; diff --git a/lib/src/bones_api_entity_db_postgres.dart b/lib/src/bones_api_entity_db_postgres.dart index 593288c..3bc1630 100644 --- a/lib/src/bones_api_entity_db_postgres.dart +++ b/lib/src/bones_api_entity_db_postgres.dart @@ -118,6 +118,7 @@ class DBPostgreSQLAdapter extends DBSQLAdapter acceptsInsertDefaultValues: true, acceptsInsertOnConflict: true, acceptsVarcharWithoutMaximumSize: true, + foreignKeyCreatesImplicitIndex: false, ), transactions: true, transactionAbort: true, @@ -169,11 +170,10 @@ class DBPostgreSQLAdapter extends DBSQLAdapter minConnections ??= 1; maxConnections ??= 3; - var retCheckTablesAndGenerateTables = - DBSQLAdapter.parseConfigDBGenerateTablesAndCheckTables(config); - - var generateTables = retCheckTablesAndGenerateTables[0]; - var checkTables = retCheckTablesAndGenerateTables[1]; + var ( + generateTables: generateTables, + checkTables: checkTables, + ) = DBSQLAdapter.parseConfigDBGenerateTablesAndCheckTables(config); var populate = config?['populate']; Object? populateTables; @@ -838,7 +838,7 @@ class DBPostgreSQLAdapter extends DBSQLAdapter var refToTable = refToTables.single; var otherRef = otherRefs.single; - return TableRelationshipReference( + var tableRelationshipReference = TableRelationshipReference( refToTable.sourceTable, refToTable.targetTable, refToTable.targetField, @@ -848,7 +848,11 @@ class DBPostgreSQLAdapter extends DBSQLAdapter otherRef.targetField, otherRef.targetFieldType, otherRef.sourceField, + sourceRelationshipFieldIndex: refToTable.indexName, + targetRelationshipFieldIndex: otherRef.indexName, ); + + return tableRelationshipReference; }) .nonNulls .toList(); @@ -953,36 +957,85 @@ class DBPostgreSQLAdapter extends DBSQLAdapter f.relname AS target_table, targ_attr.attname AS target_column, - targ_inf.data_type AS target_column_type - + targ_inf.data_type AS target_column_type, + + idx.relname AS fk_index_name + FROM - pg_constraint o + pg_constraint o LEFT JOIN pg_class f ON f.oid = o.confrelid LEFT JOIN pg_class m ON m.oid = o.conrelid - INNER JOIN pg_attribute stc_attr ON stc_attr.attrelid = m.oid AND stc_attr.attnum = o.conkey[1] AND stc_attr.attisdropped = false - INNER JOIN information_schema.columns src_inf ON src_inf.table_name = m.relname and src_inf.column_name = stc_attr.attname - INNER JOIN pg_attribute targ_attr ON targ_attr.attrelid = f.oid AND targ_attr.attnum = o.confkey[1] AND targ_attr.attisdropped = false - INNER JOIN information_schema.columns targ_inf ON targ_inf.table_name = f.relname and targ_inf.column_name = targ_attr.attname + + INNER JOIN pg_attribute stc_attr + ON stc_attr.attrelid = m.oid + AND stc_attr.attnum = o.conkey[1] + AND stc_attr.attisdropped = false + + INNER JOIN information_schema.columns src_inf + ON src_inf.table_name = m.relname + AND src_inf.column_name = stc_attr.attname + + INNER JOIN pg_attribute targ_attr + ON targ_attr.attrelid = f.oid + AND targ_attr.attnum = o.confkey[1] + AND targ_attr.attisdropped = false + + INNER JOIN information_schema.columns targ_inf + ON targ_inf.table_name = f.relname + AND targ_inf.column_name = targ_attr.attname + + LEFT JOIN LATERAL ( + SELECT + i.indexrelid + FROM pg_index i + WHERE i.indrelid = m.oid + -- FK column must be the *first* column of the index. (i.indkey is 0-based) + AND i.indkey[0] = o.conkey[1] + ORDER BY + array_length(i.indkey, 1) ASC, -- single-column first + i.indisunique DESC -- prefer unique + LIMIT 1 + ) idx_def ON true + + LEFT JOIN pg_class idx + ON idx.oid = idx_def.indexrelid + WHERE - o.contype = 'f' AND m.relname = '$table' AND o.conrelid IN (SELECT oid FROM pg_class c WHERE c.relkind = 'r') + o.contype = 'f' + AND m.relname = '$table' + AND o.conrelid IN ( + SELECT oid FROM pg_class c WHERE c.relkind = 'r' + ); '''; var referenceFields = await connection.mappedResultsQuery(sql); var map = Map.fromEntries( referenceFields.map((e) { - var sourceTable = e['source_table']; - var sourceField = e['source_column']; - var sourceFieldDataType = e['source_column_type']; - var targetTable = e['target_table']; - var targetField = e['target_column']; - var targetFieldDataType = e['target_column_type']; - if (targetTable == null || targetField == null) return null; + var sourceTable = e['source_table'] as String?; + var sourceField = e['source_column'] as String?; + var sourceFieldDataType = e['source_column_type'] as String?; + var targetTable = e['target_table'] as String?; + var targetField = e['target_column'] as String?; + var targetFieldDataType = e['target_column_type'] as String?; + var fkIndexName = e['fk_index_name'] as String?; + + if (sourceTable == null || + sourceField == null || + targetTable == null || + targetField == null) { + return null; + } + + if (fkIndexName != null && fkIndexName.isEmpty) { + fkIndexName = null; + } var sourceFieldType = sourceFieldDataType != null ? _toFieldType(sourceFieldDataType) : String; + var targetFieldType = targetFieldDataType != null ? _toFieldType(targetFieldDataType) @@ -995,7 +1048,9 @@ class DBPostgreSQLAdapter extends DBSQLAdapter targetTable, targetField, targetFieldType, + indexName: fkIndexName, ); + return MapEntry(sourceField, reference); }).nonNulls, ); diff --git a/lib/src/bones_api_entity_db_sql.dart b/lib/src/bones_api_entity_db_sql.dart index 16a8473..55ce120 100644 --- a/lib/src/bones_api_entity_db_sql.dart +++ b/lib/src/bones_api_entity_db_sql.dart @@ -456,9 +456,8 @@ abstract class DBSQLAdapter extends DBRelationalAdapter return null; } - static List parseConfigDBGenerateTablesAndCheckTables( - Map? config, - ) { + static ({bool generateTables, bool checkTables}) + parseConfigDBGenerateTablesAndCheckTables(Map? config) { bool? checkTables; bool? generateTables; @@ -496,7 +495,7 @@ abstract class DBSQLAdapter extends DBRelationalAdapter generateTables ??= false; checkTables ??= true; - return [generateTables, checkTables]; + return (generateTables: generateTables, checkTables: checkTables); } /// The [DBSQLAdapter] capability. @@ -644,6 +643,29 @@ abstract class DBSQLAdapter extends DBRelationalAdapter ) .toList(); + var missingReferenceIndexesSQLs = + repositoriesChecksErrors + .where((e) => e.missingReferenceIndexes?.isNotEmpty ?? false) + .expand((e) => e.generateMissingReferenceIndexesSQLs(this)) + .expand( + (e) => e.buildAllSQLs(ifNotExists: true, multiline: false), + ) + .toList(); + + var missingRelationshipReferenceIndexesSQLs = + repositoriesChecksErrors + .where( + (e) => + e.missingRelationshipReferenceIndexes?.isNotEmpty ?? false, + ) + .expand( + (e) => e.generateMissingRelationshipReferenceIndexesSQLs(this), + ) + .expand( + (e) => e.buildAllSQLs(ifNotExists: true, multiline: false), + ) + .toList(); + var missingUniqueConstraintsSQLs = repositoriesChecksErrors .where((e) => e.missingUniqueConstraints?.isNotEmpty ?? false) @@ -678,6 +700,10 @@ abstract class DBSQLAdapter extends DBRelationalAdapter '', ...missingReferenceConstraintsSQLs, '', + ...missingReferenceIndexesSQLs, + '', + ...missingRelationshipReferenceIndexesSQLs, + '', ...missingUniqueConstraintsSQLs, '', ...missingEnumConstraintsSQLs, @@ -849,6 +875,9 @@ abstract class DBSQLAdapter extends DBRelationalAdapter .toList(); var missingFieldReferenceConstraints = {}; + var missingFieldReferenceIndexes = {}; + var missingRelationshipFieldReferenceIndexes = + <(String, String, TypeInfo)>{}; var missingUniqueConstraints = {}; @@ -903,6 +932,67 @@ abstract class DBSQLAdapter extends DBRelationalAdapter fieldEntityTypes.keys .where((f) => !fieldsReferences.containsKey(f)) .toSet(); + + if (!dialect.foreignKeyCreatesImplicitIndex) { + missingFieldReferenceIndexes = + fieldsReferencedTables.entries + .where((e) => e.value.indexName == null) + .map((e) => e.key) + .toSet(); + } + } + + // RELATIONSHIP CONSTRAINTS // + + var fieldListEntityTypes = + entityHandler + .getFieldsListEntityTypes() + .entries + .map((e) { + var entityType = e.value.entityType; + if (entityType == null) return null; + + var refRepository = repository.provider + .getEntityRepositoryByType(entityType); + + if (refRepository == null || + !refRepository.isSameEntityManager(repository)) { + return null; + } + + return MapEntry(e.key, entityType); + }) + .nonNulls + .toMapFromEntries(); + + if (fieldListEntityTypes.isNotEmpty) { + if (!dialect.foreignKeyCreatesImplicitIndex) { + missingRelationshipFieldReferenceIndexes = + collectionReferenceFields.entries + .expand((e) { + var relFieldRef = e.value; + if (relFieldRef == null) { + return <(String, String, TypeInfo)>[]; + } + + return [ + if (relFieldRef.sourceRelationshipFieldIndex == null) + ( + relFieldRef.relationshipTable, + relFieldRef.sourceRelationshipField, + relFieldRef.sourceFieldEntityType, + ), + if (relFieldRef.targetRelationshipFieldIndex == null) + ( + relFieldRef.relationshipTable, + relFieldRef.targetRelationshipField, + relFieldRef.targetFieldEntityType, + ), + ]; + }) + .nonNulls + .toSet(); + } } // UNIQUE CONSTRAINTS // @@ -982,6 +1072,8 @@ abstract class DBSQLAdapter extends DBRelationalAdapter missingReferenceColumns.isNotEmpty || missingCollectionReferenceColumns.isNotEmpty || missingFieldReferenceConstraints.isNotEmpty || + missingFieldReferenceIndexes.isNotEmpty || + missingRelationshipFieldReferenceIndexes.isNotEmpty || missingUniqueConstraints.isNotEmpty || missingEnumConstraints.isNotEmpty || missingEnumValues.isNotEmpty) { @@ -991,6 +1083,8 @@ abstract class DBSQLAdapter extends DBRelationalAdapter "${missingReferenceColumns.isNotEmpty ? '-- missingReferenceColumns: $missingReferenceColumns\n' : ''}" "${missingCollectionReferenceColumns.isNotEmpty ? '-- missingCollectionReferenceColumns: $missingCollectionReferenceColumns\n' : ''}" "${missingFieldReferenceConstraints.isNotEmpty ? '-- missingFieldReferenceConstraints: $missingFieldReferenceConstraints\n' : ''}" + "${missingFieldReferenceIndexes.isNotEmpty ? '-- missingFieldReferenceIndexes: $missingFieldReferenceIndexes\n' : ''}" + "${missingRelationshipFieldReferenceIndexes.isNotEmpty ? '-- missingRelationshipFieldReferenceIndexes: $missingRelationshipFieldReferenceIndexes\n' : ''}" "${missingUniqueConstraints.isNotEmpty ? '-- missingUniqueConstraints: $missingUniqueConstraints\n' : ''}" "${missingEnumConstraints.isNotEmpty ? '-- missingEnumConstraints: $missingEnumConstraints\n' : ''}" "${missingEnumValues.isNotEmpty ? '-- missingEnumValues: $missingEnumValues\n' : ''}", @@ -1011,6 +1105,12 @@ abstract class DBSQLAdapter extends DBRelationalAdapter missingFieldReferenceConstraints .map((f) => _DBTableColumn.fromEntityHandler(entityHandler, f)) .toList(), + missingFieldReferenceIndexes + .map((f) => _DBTableColumn.fromEntityHandler(entityHandler, f)) + .toList(), + missingRelationshipFieldReferenceIndexes + .map((f) => _DBRelationshipTableColumn(f.$1, f.$2, f.$3, [])) + .toList(), missingUniqueConstraints .map((f) => _DBTableColumn.fromEntityHandler(entityHandler, f)) .toList(), @@ -1033,7 +1133,7 @@ abstract class DBSQLAdapter extends DBRelationalAdapter return _DBTableCheck.ok(); } - MapEntry? + MapEntry? _checkDBTableSchemeReferenceField( EntityHandler entityHandler, TableScheme scheme, @@ -1058,14 +1158,19 @@ abstract class DBSQLAdapter extends DBRelationalAdapter var targetTable = getTableForType(entityType); if (targetTable == null) return null; - TableRelationshipReference? tableRef; + TableRelationshipReferenceEntityTyped? tableRefEntityTyped; try { - tableRef = scheme.getTableRelationshipReference( + var tableRef = scheme.getTableRelationshipReference( sourceTable: table, sourceField: fieldName, targetTable: targetTable, ); + + tableRefEntityTyped = tableRef?.copyWithEntityTypes( + entityHandler.typeInfo, + entityType, + ); } catch (e) { _log.warning( "Error while getting relationship table for field: `$table`.`$fieldName`", @@ -1073,7 +1178,7 @@ abstract class DBSQLAdapter extends DBRelationalAdapter ); } - return MapEntry(fieldName, tableRef); + return MapEntry(fieldName, tableRefEntityTyped); } bool checkDBTableField( @@ -1237,7 +1342,11 @@ abstract class DBSQLAdapter extends DBRelationalAdapter static List extractTableSQLs(String sqls) => extractSQLs( sqls, - RegExp(r'(?:CREATE|ALTER)\s+TABLE', caseSensitive: false, dotAll: true), + RegExp( + r'(?:(?:CREATE|ALTER)\s+TABLE|CREATE\s+INDEX)', + caseSensitive: false, + dotAll: true, + ), ); static List extractSQLs(String sqls, RegExp commandPrefixPattern) { @@ -1290,6 +1399,11 @@ abstract class DBSQLAdapter extends DBRelationalAdapter return _populateTablesFromSQLsImpl(list); } + static final RegExp _regExpCreateIndex = RegExp( + r'CREATE\s+INDEX', + caseSensitive: false, + ); + Future> _populateTablesFromSQLsImpl(List list) async { var tables = []; @@ -1298,6 +1412,14 @@ abstract class DBSQLAdapter extends DBRelationalAdapter var ok = await executeTableSQL(sql); if (!ok) { + if (!dialect.createIndexIfNotExists) { + if (_regExpCreateIndex.hasMatch(sql)) { + _log.warning( + "Ignoring CREATE INDEX error: the SQL dialect does NOT support `IF NOT EXISTS`. SQL> $sql", + ); + continue; + } + } throw StateError("Error creating table SQL: $sql"); } @@ -3311,6 +3433,10 @@ class _DBTableCheck { List<_DBTableColumn>? missingReferenceConstraints; + List<_DBTableColumn>? missingReferenceIndexes; + + List<_DBRelationshipTableColumn>? missingRelationshipReferenceIndexes; + List<_DBTableColumn>? missingUniqueConstraints; List<_DBTableColumn>? missingEnumConstraints; @@ -3323,6 +3449,8 @@ class _DBTableCheck { this.missingReferenceColumns, this.missingCollectionReferenceColumns, this.missingReferenceConstraints, + this.missingReferenceIndexes, + this.missingRelationshipReferenceIndexes, this.missingUniqueConstraints, this.missingEnumConstraints, this.missingEnumValues, @@ -3340,6 +3468,8 @@ class _DBTableCheck { (missingReferenceColumns?.isEmpty ?? true) && (missingCollectionReferenceColumns?.isEmpty ?? true) && (missingReferenceConstraints?.isEmpty ?? true) && + (missingReferenceIndexes?.isEmpty ?? true) && + (missingRelationshipReferenceIndexes?.isEmpty ?? true) && (missingUniqueConstraints?.isEmpty ?? true) && (missingEnumConstraints?.isEmpty ?? true) && (missingEnumValues?.isEmpty ?? true); @@ -3347,16 +3477,16 @@ class _DBTableCheck { bool get isError => !isOK; List generateMissingColumnsSQLs(SQLGenerator sqlGenerator) => - _generateMissingSQLs(missingColumns, sqlGenerator); + _generateMissingColumnSQLs(missingColumns, sqlGenerator); List generateMissingReferenceColumnsSQLs( SQLGenerator sqlGenerator, - ) => _generateMissingSQLs(missingReferenceColumns, sqlGenerator); + ) => _generateMissingColumnSQLs(missingReferenceColumns, sqlGenerator); List generateMissingReferenceConstraintsSQLs( SQLGenerator sqlGenerator, ) { - var columnsSQLs = _generateMissingSQLs( + var columnsSQLs = _generateMissingColumnSQLs( missingReferenceConstraints, sqlGenerator, ); @@ -3367,6 +3497,34 @@ class _DBTableCheck { return constraints; } + List generateMissingReferenceIndexesSQLs( + SQLGenerator sqlGenerator, + ) { + var columnsSQLs = _generateMissingColumnSQLs( + missingReferenceIndexes, + sqlGenerator, + ); + + var indexes = + columnsSQLs.expand((e) => e.indexes ?? []).toList(); + + return indexes; + } + + List generateMissingRelationshipReferenceIndexesSQLs( + SQLGenerator sqlGenerator, + ) { + var columnsSQLs = _generateMissingColumnSQLs( + missingRelationshipReferenceIndexes, + sqlGenerator, + ); + + var indexes = + columnsSQLs.expand((e) => e.indexes ?? []).toList(); + + return indexes; + } + List generateMissingUniqueConstraintsSQLs( SQLGenerator sqlGenerator, ) => _generateMissingUniqueConstraintsSQLs( @@ -3386,7 +3544,7 @@ class _DBTableCheck { sqlGenerator, ); - List _generateMissingSQLs( + List _generateMissingColumnSQLs( List<_DBTableColumn>? missing, SQLGenerator sqlGenerator, ) { @@ -3396,16 +3554,18 @@ class _DBTableCheck { } var sqls = - missing - .map( - (f) => sqlGenerator.generateAddColumnAlterTableSQL( - scheme.name, - f.name, - f.type, - entityFieldAnnotations: f.annotations, - ), - ) - .toList(); + missing.map((f) { + var schemeName = + f is _DBRelationshipTableColumn + ? f.relationshipTable + : scheme.name; + return sqlGenerator.generateAddColumnAlterTableSQL( + schemeName, + f.name, + f.type, + entityFieldAnnotations: f.annotations, + ); + }).toList(); return sqls; } @@ -3461,9 +3621,17 @@ class _DBTableCheck { var missingCollectionReferenceColumns = this.missingCollectionReferenceColumns ?? []; + var missingReferenceConstraints = this.missingReferenceConstraints ?? []; + var missingReferenceIndexes = this.missingReferenceIndexes ?? []; + var missingRelationshipReferenceIndexes = + this.missingRelationshipReferenceIndexes ?? []; + if (missingColumns.isNotEmpty || missingReferenceColumns.isNotEmpty || - missingCollectionReferenceColumns.isNotEmpty) { + missingCollectionReferenceColumns.isNotEmpty || + missingReferenceConstraints.isNotEmpty || + missingReferenceIndexes.isNotEmpty || + missingRelationshipReferenceIndexes.isNotEmpty) { var repoType = repository?.type; var schemeTableName = scheme?.name; @@ -3475,6 +3643,19 @@ class _DBTableCheck { .map((e) => '${e.type.toString(withT: false)} ${e.name}') .join(' , '); + var missingReferenceConstraintsStr = missingReferenceConstraints + .map((e) => '${e.type.toString(withT: false)} ${e.name}') + .join(' , '); + + var missingReferenceIndexesStr = missingReferenceIndexes + .map((e) => '${e.type.toString(withT: false)} ${e.name}') + .join(' , '); + + var missingRelationshipReferenceIndexesStr = + missingRelationshipReferenceIndexes + .map((e) => '${e.type.toString(withT: false)} ${e.name}') + .join(' , '); + var missingCollectionReferenceColumnsStr = missingCollectionReferenceColumns .map((e) => '${e.type.toString(withT: false)} ${e.name}') @@ -3487,6 +3668,12 @@ class _DBTableCheck { " -- missingColumns: [$missingColumnsStr]", if (missingReferenceColumns.isNotEmpty) " -- missingReferenceColumns: [$missingReferenceColumnsStr]", + if (missingReferenceConstraints.isNotEmpty) + " -- missingReferenceConstraints: [$missingReferenceConstraintsStr]", + if (missingReferenceIndexes.isNotEmpty) + " -- missingReferenceIndexes: [$missingReferenceIndexesStr]", + if (missingRelationshipReferenceIndexes.isNotEmpty) + " -- missingRelationshipReferenceIndexes: [$missingRelationshipReferenceIndexesStr]", if (missingCollectionReferenceColumns.isNotEmpty) " -- missingCollectionReferenceColumns: [$missingCollectionReferenceColumnsStr]", ].join('\n'); @@ -3498,6 +3685,20 @@ class _DBTableCheck { } } +class _DBRelationshipTableColumn extends _DBTableColumn { + final String relationshipTable; + + _DBRelationshipTableColumn( + this.relationshipTable, + super.name, + super.type, + super.annotations, + ); + + @override + String toString() => '{$relationshipTable.$name: $type}'; +} + class _DBTableColumn { final String name; final TypeInfo type; diff --git a/lib/src/bones_api_sql_builder.dart b/lib/src/bones_api_sql_builder.dart index 36d7db7..aabfbb7 100644 --- a/lib/src/bones_api_sql_builder.dart +++ b/lib/src/bones_api_sql_builder.dart @@ -105,6 +105,12 @@ class SQLDialect extends DBDialect { /// If `true` indicates that the `VARCHAR` can be defined without a maximum size. final bool acceptsVarcharWithoutMaximumSize; + /// If `true`, foreign keys implicitly create an index on the referencing columns. + final bool foreignKeyCreatesImplicitIndex; + + /// Whether the SQL dialect supports `IF NOT EXISTS` on `CREATE INDEX`. + final bool createIndexIfNotExists; + const SQLDialect( super.name, { this.elementQuote = '', @@ -115,6 +121,8 @@ class SQLDialect extends DBDialect { this.acceptsInsertIgnore = false, this.acceptsInsertOnConflict = false, this.acceptsVarcharWithoutMaximumSize = false, + this.foreignKeyCreatesImplicitIndex = true, + this.createIndexIfNotExists = true, }); @override @@ -129,7 +137,18 @@ class SQLDialect extends DBDialect { @override String toString() { - return 'SQLDialect{name: $name, elementQuote: $elementQuote, acceptsOutputSyntax: $acceptsOutputSyntax, acceptsReturningSyntax: $acceptsReturningSyntax, acceptsTemporaryTableForReturning: $acceptsTemporaryTableForReturning, acceptsInsertDefaultValues: $acceptsInsertDefaultValues, acceptsInsertIgnore: $acceptsInsertIgnore, acceptsInsertOnConflict: $acceptsInsertOnConflict}'; + return 'SQLDialect{' + 'name: $name,' + 'elementQuote: $elementQuote, ' + 'acceptsOutputSyntax: $acceptsOutputSyntax, ' + 'acceptsReturningSyntax: $acceptsReturningSyntax, ' + 'acceptsTemporaryTableForReturning: $acceptsTemporaryTableForReturning, ' + 'acceptsInsertDefaultValues: $acceptsInsertDefaultValues, ' + 'acceptsInsertIgnore: $acceptsInsertIgnore, ' + 'acceptsInsertOnConflict: $acceptsInsertOnConflict, ' + 'foreignKeyCreatesImplicitIndex: $foreignKeyCreatesImplicitIndex, ' + 'createIndexIfNotExists: $createIndexIfNotExists' + '}'; } } @@ -208,7 +227,7 @@ class CreateIndexSQL extends SQLBuilder { sql.write('CREATE INDEX '); - if (ifNotExists) { + if (ifNotExists && dialect.createIndexIfNotExists) { sql.write('IF NOT EXISTS '); } @@ -1610,6 +1629,15 @@ abstract mixin class SQLGenerator { ); constraints.add(AlterTableSQL(dialect, table, [constraintEntry])); + + if (!dialect.foreignKeyCreatesImplicitIndex) { + var indexName = '${table}__${columnName}__idx'; + + indexes ??= []; + indexes.add( + CreateIndexSQL(dialect, table, columnName, indexName, q: q), + ); + } } var alterTableSQL = AlterTableSQL( @@ -1920,6 +1948,13 @@ abstract mixin class SQLGenerator { ], ), ); + + if (!dialect.foreignKeyCreatesImplicitIndex) { + var indexName = '${table}__${columnName}__idx'; + indexSQLs.add( + CreateIndexSQL(dialect, table, columnName, indexName, q: q), + ); + } } sqlEntries.sort((a, b) { @@ -2049,12 +2084,25 @@ abstract mixin class SQLGenerator { ), ]; + var relIndexes = []; + + if (!dialect.foreignKeyCreatesImplicitIndex) { + var indexName1 = '${relName}__${srcFieldName}__idx'; + var indexName2 = '${relName}__${dstFieldName}__idx'; + + relIndexes.addAll([ + CreateIndexSQL(dialect, relName, srcFieldName, indexName1, q: q), + CreateIndexSQL(dialect, relName, dstFieldName, indexName2, q: q), + ]); + } + var relSQL = CreateTableSQL( dialect, relName, sqlRelEntries, q: q, parentTable: table, + indexes: relIndexes, ); relationshipSQLs.add(relSQL); } diff --git a/pubspec.yaml b/pubspec.yaml index d735f4a..95b738f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: bones_api description: Bones_API - A powerful API backend framework for Dart. It comes with a built-in HTTP Server, route handler, entity handler, SQL translator, and DB adapters. -version: 1.9.24 +version: 1.9.25 homepage: https://github.com/Colossus-Services/bones_api environment: @@ -10,7 +10,7 @@ executables: bones_api: dependencies: - async_extension: ^1.2.19 + async_extension: ^1.2.20 async_events: ^1.3.0 reflection_factory: ^2.7.3 statistics: ^1.2.1 @@ -32,7 +32,7 @@ dependencies: shelf_gzip: ^4.1.0 shelf_static: ^1.1.3 args: ^2.7.0 - meta: ^1.18.0 + meta: ^1.18.1 petitparser: ^6.1.0 hotreloader: ^4.3.0 logging: ^1.3.0 diff --git a/test/bones_api_entity_db_tests_base.dart b/test/bones_api_entity_db_tests_base.dart index 8600fc9..b9d2c84 100644 --- a/test/bones_api_entity_db_tests_base.dart +++ b/test/bones_api_entity_db_tests_base.dart @@ -343,6 +343,8 @@ Future runAdapterTests( var sqlAdapter = await entityRepositoryProvider.adapter; expect(sqlAdapter, isNotNull); + var dialect = sqlAdapter.dialect; + expect(sqlAdapter.isInitialized, isTrue); expect(sqlAdapter.generatedTables, generateTables); expect(sqlAdapter.checkedTables, checkTables); @@ -394,8 +396,12 @@ Future runAdapterTests( ); var indexAddressRegexp = RegExp( - 'CREATE INDEX IF NOT EXISTS ${q}address__state__idx$q ON ' - '${q}address$q \\(${q}state$q\\)', + [ + 'CREATE INDEX', + if (dialect.createIndexIfNotExists) 'IF NOT EXISTS', + '${q}address__state__idx$q ON', + '${q}address$q \\(${q}state$q\\)', + ].join(' '), ); expect(