bc-code-intelligence-mcp
Version:
BC Code Intelligence MCP Server - Complete Specialist Bundle with AI-driven expert consultation, seamless handoffs, and context-preserving workflows
665 lines (573 loc) • 24.3 kB
Markdown
```al
table 50140 "Sales Analytics Data"
{
Caption = 'Sales Analytics Data';
DataClassification = CustomerContent;
fields
{
field(1; "Entry No."; BigInteger)
{
Caption = 'Entry No.';
DataClassification = SystemMetadata;
AutoIncrement = true;
}
field(2; SystemId; Guid)
{
Caption = 'System ID';
DataClassification = SystemMetadata;
}
field(10; "Customer No."; Code[20])
{
Caption = 'Customer No.';
DataClassification = CustomerContent;
}
field(20; "Item No."; Code[20])
{
Caption = 'Item No.';
DataClassification = CustomerContent;
}
field(30; "Posting Date"; Date)
{
Caption = 'Posting Date';
DataClassification = CustomerContent;
}
field(40; "Sales Amount"; Decimal)
{
Caption = 'Sales Amount';
DataClassification = CustomerContent;
}
field(50; Quantity; Decimal)
{
Caption = 'Quantity';
DataClassification = CustomerContent;
}
field(60; "Salesperson Code"; Code[20])
{
Caption = 'Salesperson Code';
DataClassification = CustomerContent;
}
field(70; "Region Code"; Code[10])
{
Caption = 'Region Code';
DataClassification = CustomerContent;
}
}
keys
{
// Primary key - Sequential for optimal inserts
key(PK; "Entry No.")
{
Clustered = true;
}
// SystemId key - Required for OData
key(SystemIdKey; SystemId)
{
Unique = true;
}
// Optimized keys for common OData query patterns
// Key 1: Customer date queries
// Optimizes: $filter=customerNo eq 'C001' and postingDate ge 2024-01-01
key(CustomerDateKey; "Customer No.", "Posting Date")
{
IncludedFields = "Sales Amount", Quantity, "Item No."; // Covering index
}
// Key 2: Date range queries with various groupings
// Optimizes: $filter=postingDate ge 2024-01-01&$orderby=postingDate,customerNo
key(DateOptimizedKey; "Posting Date", "Customer No.", "Item No.")
{
IncludedFields = "Sales Amount", Quantity;
}
// Key 3: Item analysis queries
// Optimizes: $filter=itemNo eq 'ITEM001'&$orderby=postingDate desc
key(ItemAnalysisKey; "Item No.", "Posting Date")
{
IncludedFields = "Customer No.", "Sales Amount", Quantity;
}
// Key 4: Salesperson performance queries
// Optimizes: $filter=salespersonCode eq 'SP001' and postingDate ge 2024-01-01
key(SalespersonKey; "Salesperson Code", "Posting Date", "Customer No.")
{
IncludedFields = "Sales Amount", Quantity;
}
// Key 5: Regional analysis
// Optimizes: $filter=regionCode eq 'WEST'&$apply=groupby((customerNo),aggregate(salesAmount with sum))
key(RegionKey; "Region Code", "Customer No.", "Posting Date")
{
SumIndexFields = "Sales Amount", Quantity; // SIFT for aggregations
MaintainSiftIndex = true;
}
}
}
// Optimized API page with query hints
page 50140 "Sales Analytics API"
{
PageType = API;
APIPublisher = 'contoso';
APIGroup = 'analytics';
APIVersion = 'v2.0';
EntityName = 'salesAnalytic';
EntitySetName = 'salesAnalytics';
SourceTable = "Sales Analytics Data";
DelayedInsert = true;
layout
{
area(Content)
{
repeater(Records)
{
field(id; Rec.SystemId)
{
Caption = 'Id';
}
field(entryNumber; Rec."Entry No.")
{
Caption = 'Entry Number';
Editable = false;
}
field(customerNumber; Rec."Customer No.")
{
Caption = 'Customer Number';
}
field(itemNumber; Rec."Item No.")
{
Caption = 'Item Number';
}
field(postingDate; Rec."Posting Date")
{
Caption = 'Posting Date';
}
field(salesAmount; Rec."Sales Amount")
{
Caption = 'Sales Amount';
}
field(quantity; Rec.Quantity)
{
Caption = 'Quantity';
}
field(salespersonCode; Rec."Salesperson Code")
{
Caption = 'Salesperson Code';
}
field(regionCode; Rec."Region Code")
{
Caption = 'Region Code';
}
}
}
}
// Optimize query performance based on filter patterns
trigger OnOpenPage()
begin
OptimizeForCommonQueries();
end;
local procedure OptimizeForCommonQueries()
begin
// Set default key for most common query pattern
// This optimizes unfiltered requests and date-based queries
Rec.SetCurrentKey("Posting Date", "Customer No.", "Item No.");
// Enable read-ahead for batch processing
Rec.SetAutoCalcFields(); // Only if FlowFields are present
end;
}
```
```al
codeunit 50140 "OData Query Optimizer"
{
// Dynamic key selection based on OData query patterns
procedure OptimizeQueryForFilters(var SalesAnalytics: Record "Sales Analytics Data"; FilterText: Text)
begin
// Analyze common OData filter patterns and set optimal keys
if ContainsCustomerFilter(FilterText) then
OptimizeForCustomerQueries(SalesAnalytics, FilterText)
else if ContainsDateRangeFilter(FilterText) then
OptimizeForDateQueries(SalesAnalytics)
else if ContainsItemFilter(FilterText) then
OptimizeForItemQueries(SalesAnalytics)
else if ContainsSalespersonFilter(FilterText) then
OptimizeForSalespersonQueries(SalesAnalytics)
else
OptimizeForGeneralQueries(SalesAnalytics);
end;
local procedure ContainsCustomerFilter(FilterText: Text): Boolean
begin
// Check if query contains customer number filters
// Example: $filter=customerNo eq 'C001'
exit(FilterText.Contains('customerNo') or FilterText.Contains('Customer'));
end;
local procedure ContainsDateRangeFilter(FilterText: Text): Boolean
begin
// Check if query contains date range filters
// Example: $filter=postingDate ge 2024-01-01
exit(FilterText.Contains('postingDate') or FilterText.Contains('Posting'));
end;
local procedure OptimizeForCustomerQueries(var SalesAnalytics: Record "Sales Analytics Data"; FilterText: Text)
begin
if ContainsDateRangeFilter(FilterText) then
// Customer + date queries: use CustomerDateKey
SalesAnalytics.SetCurrentKey("Customer No.", "Posting Date")
else
// Customer-only queries: still use CustomerDateKey for consistency
SalesAnalytics.SetCurrentKey("Customer No.", "Posting Date");
end;
local procedure OptimizeForDateQueries(var SalesAnalytics: Record "Sales Analytics Data")
begin
// Date range queries: use DateOptimizedKey
SalesAnalytics.SetCurrentKey("Posting Date", "Customer No.", "Item No.");
// Enable ascending sort for date ranges (default behavior)
SalesAnalytics.Ascending(true);
end;
local procedure OptimizeForItemQueries(var SalesAnalytics: Record "Sales Analytics Data")
begin
// Item-based queries: use ItemAnalysisKey
SalesAnalytics.SetCurrentKey("Item No.", "Posting Date");
end;
local procedure OptimizeForSalespersonQueries(var SalesAnalytics: Record "Sales Analytics Data")
begin
// Salesperson queries: use SalespersonKey
SalesAnalytics.SetCurrentKey("Salesperson Code", "Posting Date", "Customer No.");
end;
local procedure OptimizeForGeneralQueries(var SalesAnalytics: Record "Sales Analytics Data")
begin
// General queries: use date-optimized key as default
SalesAnalytics.SetCurrentKey("Posting Date", "Customer No.", "Item No.");
end;
}
// Performance monitoring and query analysis
codeunit 50141 "OData Performance Monitor"
{
procedure LogQueryPerformance(QueryText: Text; ExecutionTime: Duration; RecordCount: Integer)
var
QueryLogEntry: Record "Query Performance Log";
begin
// Log query performance for analysis
QueryLogEntry.Init();
QueryLogEntry."Entry No." := GetNextEntryNo();
QueryLogEntry."Query Text" := CopyStr(QueryText, 1, MaxStrLen(QueryLogEntry."Query Text"));
QueryLogEntry."Execution Time (ms)" := ExecutionTime;
QueryLogEntry."Record Count" := RecordCount;
QueryLogEntry."Timestamp" := CurrentDateTime;
QueryLogEntry."User ID" := UserId;
QueryLogEntry.Insert();
// Alert if query is slow
if ExecutionTime > 5000 then // 5 seconds
LogSlowQueryAlert(QueryText, ExecutionTime);
end;
procedure AnalyzeQueryPattern(QueryText: Text): Text
var
Pattern: Text;
begin
// Analyze OData query patterns for optimization recommendations
Pattern := 'UNKNOWN';
if QueryText.Contains('$filter') then begin
if QueryText.Contains('customerNo') then
Pattern := 'CUSTOMER_FILTER';
if QueryText.Contains('postingDate') then
if Pattern <> 'UNKNOWN' then
Pattern += '+DATE_FILTER'
else
Pattern := 'DATE_FILTER';
if QueryText.Contains('itemNo') then
if Pattern <> 'UNKNOWN' then
Pattern += '+ITEM_FILTER'
else
Pattern := 'ITEM_FILTER';
end;
if QueryText.Contains('$orderby') then
Pattern += '+SORT';
if QueryText.Contains('$apply') then
Pattern += '+AGGREGATION';
if QueryText.Contains('$top') then
Pattern += '+PAGING';
exit(Pattern);
end;
procedure RecommendOptimization(QueryPattern: Text): Text
var
Recommendation: Text;
begin
// Provide optimization recommendations based on query patterns
case QueryPattern of
'CUSTOMER_FILTER':
Recommendation := 'Use CustomerDateKey index. Consider SetCurrentKey("Customer No.", "Posting Date")';
'DATE_FILTER':
Recommendation := 'Use DateOptimizedKey index. Consider SetCurrentKey("Posting Date", "Customer No.", "Item No.")';
'CUSTOMER_FILTER+DATE_FILTER':
Recommendation := 'Optimal: CustomerDateKey handles this pattern efficiently';
'ITEM_FILTER':
Recommendation := 'Use ItemAnalysisKey index. Consider SetCurrentKey("Item No.", "Posting Date")';
'CUSTOMER_FILTER+DATE_FILTER+AGGREGATION':
Recommendation := 'Use RegionKey with SIFT. Consider CalcSums for aggregations';
else
Recommendation := 'Use default DateOptimizedKey for general queries';
end;
exit(Recommendation);
end;
local procedure LogSlowQueryAlert(QueryText: Text; ExecutionTime: Duration)
begin
// Log slow query alerts for DBA attention
Message('Slow query detected: %1ms\nQuery: %2', ExecutionTime, QueryText);
end;
}
// Supporting table for query performance logging
table 50141 "Query Performance Log"
{
Caption = 'Query Performance Log';
DataClassification = SystemMetadata;
fields
{
field(1; "Entry No."; BigInteger)
{
Caption = 'Entry No.';
AutoIncrement = true;
}
field(10; "Query Text"; Text[2048])
{
Caption = 'Query Text';
}
field(20; "Execution Time (ms)"; Duration)
{
Caption = 'Execution Time (ms)';
}
field(30; "Record Count"; Integer)
{
Caption = 'Record Count';
}
field(40; "Timestamp"; DateTime)
{
Caption = 'Timestamp';
}
field(50; "User ID"; Code[50])
{
Caption = 'User ID';
}
}
keys
{
key(PK; "Entry No.")
{
Clustered = true;
}
key(TimeKey; "Timestamp")
{
}
}
}
```
```al
// Examples of OData queries and their AL optimizations
codeunit 50142 "OData Query Examples"
{
procedure DemonstrateQueryOptimizations()
var
SalesAnalytics: Record "Sales Analytics Data";
begin
// Example 1: Simple customer filter
// OData: GET /salesAnalytics?$filter=customerNo eq 'C001'
DemoCustomerFilter(SalesAnalytics);
// Example 2: Date range filter
// OData: GET /salesAnalytics?$filter=postingDate ge 2024-01-01 and postingDate le 2024-12-31
DemoDateRangeFilter(SalesAnalytics);
// Example 3: Combined customer and date filter
// OData: GET /salesAnalytics?$filter=customerNo eq 'C001' and postingDate ge 2024-01-01
DemoCustomerDateFilter(SalesAnalytics);
// Example 4: Sorting optimization
// OData: GET /salesAnalytics?$orderby=postingDate desc,salesAmount desc
DemoSortingOptimization(SalesAnalytics);
// Example 5: Aggregation queries
// OData: GET /salesAnalytics?$apply=groupby((customerNo),aggregate(salesAmount with sum as total))
DemoAggregationQuery(SalesAnalytics);
// Example 6: Paging optimization
// OData: GET /salesAnalytics?$skip=100&$top=50
DemoPagingOptimization(SalesAnalytics);
end;
local procedure DemoCustomerFilter(var SalesAnalytics: Record "Sales Analytics Data")
begin
// Optimized for: $filter=customerNo eq 'C001'
SalesAnalytics.Reset();
SalesAnalytics.SetCurrentKey("Customer No.", "Posting Date"); // Use CustomerDateKey
SalesAnalytics.SetRange("Customer No.", 'C001');
// This query will use the CustomerDateKey index efficiently
// Index seek on Customer No. field, then range scan
Message('Customer filter: %1 records found', SalesAnalytics.Count);
end;
local procedure DemoDateRangeFilter(var SalesAnalytics: Record "Sales Analytics Data")
begin
// Optimized for: $filter=postingDate ge 2024-01-01
SalesAnalytics.Reset();
SalesAnalytics.SetCurrentKey("Posting Date", "Customer No.", "Item No."); // Use DateOptimizedKey
SalesAnalytics.SetRange("Posting Date", 20240101D, 20241231D);
// This query will use DateOptimizedKey for efficient range scan
Message('Date range filter: %1 records found', SalesAnalytics.Count);
end;
local procedure DemoCustomerDateFilter(var SalesAnalytics: Record "Sales Analytics Data")
begin
// Optimized for: $filter=customerNo eq 'C001' and postingDate ge 2024-01-01
SalesAnalytics.Reset();
SalesAnalytics.SetCurrentKey("Customer No.", "Posting Date"); // Perfect key match
SalesAnalytics.SetRange("Customer No.", 'C001');
SalesAnalytics.SetRange("Posting Date", 20240101D, Today);
// This query perfectly matches CustomerDateKey structure
// Index seek on Customer No., then range scan on Posting Date
Message('Customer + date filter: %1 records found', SalesAnalytics.Count);
end;
local procedure DemoSortingOptimization(var SalesAnalytics: Record "Sales Analytics Data")
begin
// Optimized for: $orderby=postingDate desc,salesAmount desc
SalesAnalytics.Reset();
SalesAnalytics.SetCurrentKey("Posting Date", "Customer No.", "Item No.");
SalesAnalytics.Ascending(false); // Descending order for posting date
// Note: salesAmount sorting requires covering index or separate sort
// Consider adding SalesAmount to IncludedFields if this pattern is common
Message('Sorted query prepared (descending by date)');
end;
local procedure DemoAggregationQuery(var SalesAnalytics: Record "Sales Analytics Data")
var
TempSalesAnalytics: Record "Sales Analytics Data" temporary;
CustomerNo: Code[20];
TotalSales: Decimal;
begin
// Optimized for: $apply=groupby((customerNo),aggregate(salesAmount with sum as total))
SalesAnalytics.Reset();
SalesAnalytics.SetCurrentKey("Customer No.", "Posting Date"); // Use SIFT-enabled key
// Use AL aggregation instead of individual record processing
if SalesAnalytics.FindSet() then
repeat
if CustomerNo <> SalesAnalytics."Customer No." then begin
if CustomerNo <> '' then begin
// Output previous customer total
TempSalesAnalytics.Init();
TempSalesAnalytics."Customer No." := CustomerNo;
TempSalesAnalytics."Sales Amount" := TotalSales;
TempSalesAnalytics.Insert();
end;
CustomerNo := SalesAnalytics."Customer No.";
TotalSales := 0;
end;
TotalSales += SalesAnalytics."Sales Amount";
until SalesAnalytics.Next() = 0;
// Better approach: Use SIFT aggregation
SalesAnalytics.Reset();
SalesAnalytics.SetCurrentKey("Region Code", "Customer No.", "Posting Date"); // SIFT key
SalesAnalytics.CalcSums("Sales Amount");
Message('Total sales amount: %1', SalesAnalytics."Sales Amount");
end;
local procedure DemoPagingOptimization(var SalesAnalytics: Record "Sales Analytics Data")
var
PageSize: Integer;
SkipCount: Integer;
Counter: Integer;
begin
// Optimized for: $skip=100&$top=50
PageSize := 50;
SkipCount := 100;
SalesAnalytics.Reset();
SalesAnalytics.SetCurrentKey("Posting Date", "Customer No.", "Item No."); // Stable sort order
// Efficient paging using FindSet with skip logic
if SalesAnalytics.FindSet() then begin
// Skip records
Counter := 0;
while (Counter < SkipCount) and (SalesAnalytics.Next() <> 0) do
Counter += 1;
// Process page
Counter := 0;
repeat
// Process record (in real API, this would be serialized to response)
Counter += 1;
until (Counter >= PageSize) or (SalesAnalytics.Next() = 0);
end;
Message('Page processed: %1 records', Counter);
end;
}
```
```al
codeunit 50143 "OData Performance Testing"
{
[]
procedure TestCustomerFilterPerformance()
var
SalesAnalytics: Record "Sales Analytics Data";
StartTime: DateTime;
EndTime: DateTime;
ExecutionTime: Duration;
begin
// Test performance of customer filter queries
SalesAnalytics.Reset();
SalesAnalytics.SetCurrentKey("Customer No.", "Posting Date");
StartTime := CurrentDateTime;
SalesAnalytics.SetRange("Customer No.", 'C001');
SalesAnalytics.FindSet();
EndTime := CurrentDateTime;
ExecutionTime := EndTime - StartTime;
// Assert performance is acceptable (< 1 second)
Assert.IsTrue(ExecutionTime < 1000, 'Customer filter query too slow');
end;
[]
procedure TestDateRangePerformance()
var
SalesAnalytics: Record "Sales Analytics Data";
StartTime: DateTime;
EndTime: DateTime;
ExecutionTime: Duration;
begin
// Test performance of date range queries
SalesAnalytics.Reset();
SalesAnalytics.SetCurrentKey("Posting Date", "Customer No.", "Item No.");
StartTime := CurrentDateTime;
SalesAnalytics.SetRange("Posting Date", CalcDate('<-1Y>', Today), Today);
SalesAnalytics.FindSet();
EndTime := CurrentDateTime;
ExecutionTime := EndTime - StartTime;
// Assert performance is acceptable
Assert.IsTrue(ExecutionTime < 2000, 'Date range query too slow');
end;
[]
procedure TestAggregationPerformance()
var
SalesAnalytics: Record "Sales Analytics Data";
StartTime: DateTime;
EndTime: DateTime;
ExecutionTime: Duration;
begin
// Test SIFT aggregation performance
SalesAnalytics.Reset();
SalesAnalytics.SetCurrentKey("Region Code", "Customer No.", "Posting Date"); // SIFT key
StartTime := CurrentDateTime;
SalesAnalytics.SetRange("Region Code", 'WEST');
SalesAnalytics.CalcSums("Sales Amount", Quantity);
EndTime := CurrentDateTime;
ExecutionTime := EndTime - StartTime;
// SIFT aggregations should be very fast
Assert.IsTrue(ExecutionTime < 500, 'SIFT aggregation too slow');
end;
}
```
**Key Design for OData Performance:**
- Design keys based on common filter combinations in OData queries
- Use IncludedFields to create covering indexes for frequently accessed data
- Consider SIFT keys for aggregation queries ($apply operations)
- Ensure SystemId key is unique and indexed for entity operations
**Query Pattern Analysis:**
- Monitor actual OData queries to understand usage patterns
- Use query performance logging to identify slow queries
- Adjust key design based on real usage rather than assumptions
- Consider regional differences in query patterns
**Performance Optimization Techniques:**
- Set appropriate keys using SetCurrentKey before filtering
- Use efficient filtering with SetRange instead of SetFilter when possible
- Leverage SIFT aggregations for sum/count operations
- Implement proper paging to handle large result sets
**Monitoring and Maintenance:**
- Regularly analyze query performance logs
- Monitor index usage and effectiveness
- Update key design as API usage patterns evolve
- Test performance with realistic data volumes
**OData-Specific Considerations:**
- OData $filter translates to AL SetRange/SetFilter operations
- OData $orderby may require additional sorting if not supported by key
- OData $apply (aggregations) benefit significantly from SIFT indexes
- OData $expand (navigation properties) require efficient joins