ADR-0025: Domain-Aware SQL Generation Architecture
ADR-0025: Domain-Aware SQL Generation Architecture
Status: Accepted
Date: 2026-02-03
Deciders: ant11 (Architect), cl1 (Implementer), George (Product Owner)
Technical Story: Multi-brand support for SQL generation (gaming, automotive, weather)
Context
Jorvis initially supported only gaming analytics (iGaming brands like SPW, IRS, Winpot). With the addition of jorvis_auto (cars inventory) and future domains, we need a clean architecture for:
- Routing questions to appropriate database schemas
- Injecting domain-specific business context
- Avoiding gaming-specific logic for non-gaming brands
Current State (Before Refactor)
- Hardcoded gaming table maps in
OptimizedAiService - Business context assumed gaming domain
- Performance tables always included in schema
- No brand-to-domain mapping
Decision Drivers
- Extensibility — Easy to add new domains (weather, finance, etc.)
- Backwards Compatibility — Gaming behavior unchanged
- Separation of Concerns — Domain logic isolated from LLM code
- Maintainability — Config-driven, not code-driven
Decision
Implement a config-driven domain routing architecture with brand-to-domain mapping.
Implementation (Completed by cl1)
1. Configuration Layer
File: analytics-platform/config/schema-config.json
{
"domains": {
"gaming": {
"brands": ["default", "irs", "spw", "winpot"],
"tables": ["dim_performance", "dim_crm_player", "agg_game_round"],
"defaultSchema": "dim_irs"
},
"automotive": {
"brands": ["cars", "auto", "jorvis_auto"],
"tables": ["vehicles"],
"defaultSchema": "cars"
}
}
}
2. DynamicSchemaService Enhancements
// New methods
lookupFromConfig(tableName: string, brand?: string): TableConfig | undefined
resolveSchemaKey(brand?: string): string
getTableDescription(tableName: string, brand?: string): string
getTableColumns(tableName: string, brand?: string): string[]
getAvailableMetrics(brand?: string): string[]
3. BusinessContextService Brand-Aware
// Map-based context storage
private contexts: Map<string, BusinessContext> = new Map();
// Domain resolution
private resolveDomainKey(brand?: string): string {
if (!brand) return 'gaming';
const lower = brand.toLowerCase();
if (['cars', 'auto', 'jorvis_auto'].includes(lower)) return 'automotive';
return 'gaming';
}
// Brand-aware context
getFormattedContext(brand?: string): string {
const domain = this.resolveDomainKey(brand);
if (domain !== 'gaming') return ''; // Non-gaming has no business context
return this.formatGamingContext();
}
4. OptimizedAiService Brand Gating
private isGamingBrand(brand?: string): boolean {
if (!brand) return true; // Default to gaming for backwards compatibility
const gamingBrands = new Set(['default', 'irs', 'winpot', 'spw']);
return gamingBrands.has(brand.toLowerCase());
}
// Usage in table identification
if (isGaming) {
const performanceTables = this.findTablesContaining('performance');
tables.push(...performanceTables);
}
5. Call Site Updates
// conversation-question.service.ts
const result = await this.optimizedAiService.generateOptimizedSql(
numberOfTries,
question,
fullSchema,
conversationHistory,
currentDateTime,
language,
providerConfig,
effectiveBrand // ← NEW: passes brand through pipeline
);
// prompt-manager.service.ts
const businessContext = this.businessContextService.getFormattedContext(brand);
Architecture Diagram
flowchart TB
Q[User Question] --> R{Brand Router}
R -->|gaming| G[Gaming Domain]
R -->|cars| C[Automotive Domain]
R -->|weather| W[Weather Domain]
G --> GS[dim_irs/dim_spw Schema]
G --> GC[Gaming Business Context]
G --> GP[Performance Tables]
C --> CS[cars Schema]
C --> CC[Empty Context]
C --> CP[vehicles Table]
GS & GC & GP --> SQL[SQL Generator]
CS & CC & CP --> SQL
SQL --> LLM[LLM]
Consequences
Positive
- Clean Separation — Gaming logic isolated behind
isGamingBrand()checks - Extensible — New domains added via config, minimal code changes
- Backwards Compatible —
brand=undefineddefaults to gaming behavior - Testable — Domain routing easily unit-tested
Negative
- Slight Complexity — Brand parameter flows through many functions
- Pattern Debt —
isGamingBrand()inline set should migrate to config in v0.9
Neutral
- Config Duplication — Domain knowledge exists in both config and services
- Test Coverage — 27 new tests added (1192 total)
Migration Path (v0.9)
// Current (v0.8) - inline brand check
private isGamingBrand(brand?: string): boolean {
const gamingBrands = new Set(['default', 'irs', 'winpot']);
return gamingBrands.has(brand?.toLowerCase() ?? 'default');
}
// Future (v0.9) - config-driven
private isGamingBrand(brand?: string): boolean {
return this.schemaConfigService.getDomainForBrand(brand) === 'gaming';
}
Verification
- ✅ 27 new unit tests for domain routing
- ✅ 1192 total tests passing
- ✅ Commit
f50f67bpushed to develop - ⏳ Production verification pending
References
- ADR-0024: Multilingual Intent Detection
- schema-config.json (
analytics-platform/config/schema-config.json) - Commit:
f50f67bby cl1