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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,23 @@
## 1.9.20

- `ConditionSQLEncoder`:
- `keyToSQL`: added check to throw `ConditionEncodingError` if `keys` is empty.
- Refactored `keyFieldReferenceToSQL` to recursively resolve multi-level key references by walking keys and resolving intermediate tables and relationships.
- Added helper methods `_resolveReferenceField` and `_resolveFinalField` to modularize reference resolution logic.

- `DBSQLAdapter`:
- Introduced `_JoinEntry` typedef to represent SQL JOIN fragments with explicit alias dependencies (`defs` and `refs`).
- Added local extension methods on `List<_JoinEntry>` to perform dependency-aware sorting of JOINs ensuring referenced aliases are resolved before use.
- Refactored JOIN construction logic in SQL query building to:
- Collect JOINs as `_JoinEntry` with defined and referenced aliases.
- Sort JOINs by alias dependencies before concatenation.
- Log a warning if not all JOIN references could be resolved.
- This improves correctness and ordering of JOIN clauses in generated SQL.

- Dependencies:
- Updated `async_extension` from ^1.2.17 to ^1.2.18.
- Updated `build_runner` from ^2.10.4 to ^2.10.5.

## 1.9.19

- Dependency updates:
Expand Down
2 changes: 1 addition & 1 deletion lib/src/bones_api_base.dart
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ typedef APILogger =
/// Bones API Library class.
class BonesAPI {
// ignore: constant_identifier_names
static const String VERSION = '1.9.19';
static const String VERSION = '1.9.20';

static bool _boot = false;

Expand Down
240 changes: 113 additions & 127 deletions lib/src/bones_api_condition_sql.dart
Original file line number Diff line number Diff line change
Expand Up @@ -292,17 +292,20 @@ class ConditionSQLEncoder extends ConditionEncoder {
EncodingContext context,
) {
var keys = c.keys;
if (keys.isEmpty) {
throw ConditionEncodingError('keys empty: $c');
}

if (keys.first is! ConditionKeyField) {
throw ConditionEncodingError('Root Key should be a field key: $c');
}

if (keys.length == 1) {
return keyFieldToSQL(c, context);
} else if (keys.length == 2) {
}
// keys.length >= 2
else {
return keyFieldReferenceToSQL(c, context);
} else {
throw ConditionEncodingError('keys > 2: $c');
}
}

Expand Down Expand Up @@ -405,150 +408,133 @@ class ConditionSQLEncoder extends ConditionEncoder {
});
}

FutureOr<MapEntry<Type, String>> keyFieldReferenceToSQL(
KeyCondition<dynamic, dynamic> c,
EncodingContext context,
) {
var schemeProvider = this.schemeProvider;
var tableName = context.tableNameOrEntityName;
FutureOr<String> _resolveReferenceField({
required SchemeProvider schemeProvider,
required EncodingContext context,
required KeyCondition c,
required TableScheme sourceScheme,
required String sourceTable,
required ConditionKeyField key,
}) {
final fieldRef = sourceScheme.getFieldReferencedTable(key.name);

if (schemeProvider == null) {
throw ConditionEncodingError(
'No SchemeProvider> tableName: $tableName > $this',
);
// Normal reference field, pointing to another entity table:
if (fieldRef != null) {
context.addFieldReference(fieldRef, c);
context.resolveEntityAlias(fieldRef.targetTable);
return fieldRef.targetTable;
}

var keys = c.keys;
var key0 = keys.first as ConditionKeyField;

var tableSchemeRet = schemeProvider.getTableScheme(tableName);

return tableSchemeRet.resolveMapped((tableScheme) {
if (tableScheme == null) {
var errorMsg = "Can't find `TableScheme` for entity/table: $tableName";
_log.severe(errorMsg);
throw StateError(errorMsg);
}

var fieldName = key0.name;
var fieldRef = tableScheme.getFieldReferencedTable(fieldName);

if (fieldRef != null) {
context.addFieldReference(fieldRef, c);

var entityAlias = context.resolveEntityAlias(fieldRef.targetTable);

var key1 = keys[1];

if (key1 is ConditionKeyField) {
var targetTableSchemeRet = schemeProvider.getTableScheme(
fieldRef.targetTable,
);
return schemeProvider
.getFieldType(key.name, tableName: sourceTable)
.resolveMapped((fieldType) {
if (fieldType == null) {
throw ConditionEncodingError('No field type for ${key.name}');
}

return targetTableSchemeRet.resolveMapped((targetTableScheme) {
if (targetTableScheme == null) {
var errorMsg =
"Can't find `TableScheme` for target table: $fieldRef";
_log.severe(errorMsg);
throw StateError(errorMsg);
return schemeProvider.getTableForType(fieldType).resolveMapped((
targetTable,
) {
if (targetTable == null) {
throw ConditionEncodingError('No table for type $fieldType');
}

var q = sqlElementQuote;
var targetFieldName = targetTableScheme.resolveTableFieldName(
key1.name,
final rel = sourceScheme.getTableRelationshipReference(
sourceTable: sourceTable,
sourceField: key.name,
targetTable: targetTable,
);
var targetFieldType =
targetTableScheme.fieldsTypes[targetFieldName]!;
return MapEntry(
targetFieldType,
'$q$entityAlias$q.$q$targetFieldName$q',
);
});
} else {
throw ConditionEncodingError('Key: $c');
}
}

var retFieldType = schemeProvider.getFieldType(
fieldName,
entityName: context.entityName,
tableName: context.tableName,
);
if (rel == null) {
throw ConditionEncodingError(
'No relationship table for: `$sourceTable` -> `$targetTable`',
);
}

return retFieldType.resolveMapped((refFieldType) {
if (refFieldType == null) {
throw ConditionEncodingError(
'No field type for key[0]> keys: $key0 $keys ; entityName: ${context.entityName} ; tableName: ${context.tableName} > $this ; tableScheme: $tableScheme',
);
}
context.addRelationshipTable(targetTable, rel, c);
context.resolveEntityAlias(rel.targetTable);

var retTableNameRef = schemeProvider.getTableForType(refFieldType);
return rel.targetTable;
});
});
}

return retTableNameRef.resolveMapped((tableNameRef) {
if (tableNameRef == null) {
throw ConditionEncodingError(
'No referenced table or relationship table for key[0]> keys: $key0 $keys ; tableName: $tableName ; fieldType: $refFieldType> $this ; tableScheme: $tableScheme',
);
}
FutureOr<MapEntry<Type, String>> _resolveFinalField({
required SchemeProvider schemeProvider,
required EncodingContext context,
required String targetTable,
required ConditionKeyField key,
}) {
return schemeProvider.getTableScheme(targetTable).resolveMapped((scheme) {
if (scheme == null) {
throw StateError("Can't find TableScheme for $targetTable");
}

var relationship = tableScheme.getTableRelationshipReference(
sourceTable: tableName,
sourceField: fieldName,
targetTable: tableNameRef,
);
final fieldName = scheme.resolveTableFieldName(key.name);
if (fieldName == null) {
throw StateError(
"Can't find field `${key.name}` on table `${scheme.name}`",
);
}

if (relationship == null) {
throw ConditionEncodingError(
'No relationship table with target table $tableNameRef> keys: $key0 $keys ; tableName: $tableName ; fieldType: $refFieldType> $this ; tableScheme: $tableScheme',
);
}
final alias = context.resolveEntityAlias(targetTable);
final type = scheme.fieldsTypes[fieldName]!;
final q = sqlElementQuote;

context.addRelationshipTable(tableNameRef, relationship, c);
return MapEntry(type, '$q$alias$q.$q$fieldName$q');
});
}

var targetAlias = context.resolveEntityAlias(
relationship.targetTable,
);
FutureOr<MapEntry<Type, String>> keyFieldReferenceToSQL(
KeyCondition<dynamic, dynamic> c,
EncodingContext context,
) {
final rootTable = context.tableNameOrEntityName;

var key1 = keys[1];
final schemeProvider = this.schemeProvider;
if (schemeProvider == null) {
throw ConditionEncodingError(
'No SchemeProvider> tableName: $rootTable > $this',
);
}

if (key1 is ConditionKeyField) {
var targetTableSchemeRet = schemeProvider.getTableScheme(
relationship.targetTable,
);
final keys = c.keys.cast<ConditionKeyField>();
if (keys.isEmpty) {
throw ConditionEncodingError('Empty key path: $c');
}

return targetTableSchemeRet.resolveMapped((targetTableScheme) {
if (targetTableScheme == null) {
var errorMsg =
"Can't find `TableScheme` for target table: $fieldRef";
_log.severe(errorMsg);
throw StateError(errorMsg);
}

var q = sqlElementQuote;
var targetFieldName = targetTableScheme.resolveTableFieldName(
key1.name,
);
FutureOr<MapEntry<Type, String>> walkKeys({
required int index,
required String sourceTable,
}) {
if (index == keys.length - 1) {
return _resolveFinalField(
schemeProvider: schemeProvider,
context: context,
targetTable: sourceTable,
key: keys[index],
);
}

if (targetFieldName == null) {
var errorMsg =
"Can't find field `${key1.name}` for target `${targetTableScheme.name}`. relationship: $relationship";
_log.severe(errorMsg);
throw StateError(errorMsg);
}

var targetFieldType =
targetTableScheme.fieldsTypes[targetFieldName]!;
return MapEntry(
targetFieldType,
'$q$targetAlias$q.$q$targetFieldName$q',
);
});
} else {
throw ConditionEncodingError('Key: $c');
}
});
return schemeProvider.getTableScheme(sourceTable).resolveMapped((scheme) {
if (scheme == null) {
throw StateError("Can't find `TableScheme` for table: $sourceTable");
}

return _resolveReferenceField(
schemeProvider: schemeProvider,
context: context,
c: c,
sourceScheme: scheme,
sourceTable: sourceTable,
key: keys[index],
).resolveMapped(
(nextTable) => walkKeys(index: index + 1, sourceTable: nextTable),
);
});
});
}

return walkKeys(index: 0, sourceTable: rootTable);
}

static List<T> _valueToList<T>(Object value) {
Expand Down
Loading