Follow-up to #132. One of the DoD bullets is SqlStudentCodeHintAnalyzer in factory registry. The other languages each ship a code-hint analyzer that runs before grading and surfaces non-blocking pedagogical nudges (StudentCodeHintAnalyzerFactory.For(runtime) returns the C# / Python / JS one based on LessonGradingRuntime). SQL doesn't have one yet.
This is the differentiator vs. a generic SQL editor: when the student forgets GROUP BY or accidentally writes a Cartesian join, we want a yellow-banner hint that explains why it's wrong rather than a red FAIL with no context.
What needs to happen
1. New analyzer class
New file: SpikerSoft.Business/Domain/Lessons/Hints/SqlStudentCodeHintAnalyzer.cs
publicsealedclassSqlStudentCodeHintAnalyzer:IStudentCodeHintAnalyzer{publicLessonGradingRuntimeRuntime=>LessonGradingRuntime.DuckDbSql;publicIReadOnlyList<StudentCodeHint>Analyze(stringstudentCode,LessonAttemptStatestate){// Lex the SQL into a permissive token stream, then run each pattern below.}}
Each pattern is independent and emits a single hint with a severity = info (non-blocking). The grader still runs and may pass even if a hint fires — these are teaching signals, not validation.
Missing GROUP BY: aggregate (COUNT, SUM, AVG, MIN, MAX) alongside a non-aggregated column in SELECT and no GROUP BY clause — "You're aggregating with a plain column. Did you mean to add a GROUP BY?"
Cartesian join: two tables in FROM with no JOIN ... ON and no WHERE linking them — "Two tables but no join condition. Every row of A pairs with every row of B — was that the intent?"
SELECT * in a GROUP BY query: "SELECT * with GROUP BY is almost always a mistake — list the grouped columns explicitly."
Aggregate in WHERE: "WHERE runs before aggregation. Use HAVING for aggregate predicates."
= NULL / <> NULL: the perennial classic — "= NULL always returns nothing. Use IS NULL / IS NOT NULL."
Ambiguous columns: same column name in two joined tables, no table qualifier — "name exists in both tables. Qualify with the alias (t.name / d.name)."
Alias confusion: column alias defined in SELECT referenced in WHERE (most engines reject this; DuckDB is permissive but the pattern is non-portable) — "WHERE cannot reference a SELECT-list alias — repeat the expression or use a subquery."
3. Wire into the factory
StudentCodeHintAnalyzerFactory: add the new analyzer to the dispatch dict so For(DuckDbSql) returns it. Mirror the C# / Python / JS registration pattern.
4. Frontend surfacing
The LanguageRunner's lesson pane already renders testResults where kind = "hint" as yellow banners (regex does this). The SQL grader needs to inject the analyzer's output into the SqlGradingEnvelope.testResults array with kind: "hint" so existing styling Just Works.
Two options for execution order:
Pre-flight (recommended): run the analyzer in SqlLessonGraderService.gradeSubmission BEFORE calling DuckDB. Hint banners appear alongside or instead of the actual test result.
Post-flight: run after grading. Less useful because the student already knows whether they passed.
For consistency with regex (which does pre-flight via RegexLessonHarnessBuilder), pick pre-flight.
5. Pattern engine: don't over-engineer
A regex-based scanner over a normalized whitespace+lowercase string is fine for the first pass. None of the patterns require true AST analysis; they're surface-form heuristics. DuckDB's own AST is reachable via EXPLAIN but adds a roundtrip — only pull it in if the regex heuristics produce too many false positives.
Acceptance
SqlStudentCodeHintAnalyzer implements IStudentCodeHintAnalyzer and registers under LessonGradingRuntime.DuckDbSql
StudentCodeHintAnalyzerFactory.For(DuckDbSql) returns the new analyzer (assertion test)
Each of the 7 patterns above has a unit test in SpikerSoft.Tests.Unit/Domain/Lessons/Hints/SqlStudentCodeHintAnalyzerTests.cs with a positive and negative example
Frontend SqlLessonGraderService runs the analyzer pre-flight and surfaces hints as kind: "hint" test results
Visual check: submit a SELECT name, COUNT(*) FROM travelers (no GROUP BY) against any Chapter 5 lesson and see the yellow banner
Closes
Last unchecked bullet of #132's DoD: SqlStudentCodeHintAnalyzer in factory registry.
Labels
enhancement · backend · pedagogy · sql
## Context
Follow-up to [#132](https://git.spikersoft.com/spikerj/spikersoft-issues/issues/132). One of the DoD bullets is **`SqlStudentCodeHintAnalyzer` in factory registry**. The other languages each ship a code-hint analyzer that runs *before* grading and surfaces non-blocking pedagogical nudges (`StudentCodeHintAnalyzerFactory.For(runtime)` returns the C# / Python / JS one based on `LessonGradingRuntime`). SQL doesn't have one yet.
This is the differentiator vs. a generic SQL editor: when the student forgets `GROUP BY` or accidentally writes a Cartesian join, we want a yellow-banner hint that explains *why* it's wrong rather than a red FAIL with no context.
## What needs to happen
### 1. New analyzer class
New file: `SpikerSoft.Business/Domain/Lessons/Hints/SqlStudentCodeHintAnalyzer.cs`
```csharp
public sealed class SqlStudentCodeHintAnalyzer : IStudentCodeHintAnalyzer
{
public LessonGradingRuntime Runtime => LessonGradingRuntime.DuckDbSql;
public IReadOnlyList<StudentCodeHint> Analyze(string studentCode, LessonAttemptState state)
{
// Lex the SQL into a permissive token stream, then run each pattern below.
}
}
```
### 2. Pattern coverage (from #132)
Each pattern is independent and emits a single hint with a `severity = info` (non-blocking). The grader still runs and may pass even if a hint fires — these are *teaching* signals, not validation.
- **Missing `GROUP BY`**: aggregate (`COUNT`, `SUM`, `AVG`, `MIN`, `MAX`) alongside a non-aggregated column in `SELECT` and no `GROUP BY` clause — "You're aggregating with a plain column. Did you mean to add a GROUP BY?"
- **Cartesian join**: two tables in `FROM` with no `JOIN ... ON` and no `WHERE` linking them — "Two tables but no join condition. Every row of A pairs with every row of B — was that the intent?"
- **`SELECT *` in a `GROUP BY` query**: "`SELECT *` with `GROUP BY` is almost always a mistake — list the grouped columns explicitly."
- **Aggregate in `WHERE`**: "`WHERE` runs before aggregation. Use `HAVING` for aggregate predicates."
- **`= NULL` / `<> NULL`**: the perennial classic — "`= NULL` always returns nothing. Use `IS NULL` / `IS NOT NULL`."
- **Ambiguous columns**: same column name in two joined tables, no table qualifier — "`name` exists in both tables. Qualify with the alias (`t.name` / `d.name`)."
- **Alias confusion**: column alias defined in `SELECT` referenced in `WHERE` (most engines reject this; DuckDB is permissive but the pattern is non-portable) — "`WHERE` cannot reference a `SELECT`-list alias — repeat the expression or use a subquery."
### 3. Wire into the factory
`StudentCodeHintAnalyzerFactory`: add the new analyzer to the dispatch dict so `For(DuckDbSql)` returns it. Mirror the C# / Python / JS registration pattern.
### 4. Frontend surfacing
The LanguageRunner's lesson pane already renders `testResults` where `kind = "hint"` as yellow banners (regex does this). The SQL grader needs to inject the analyzer's output into the `SqlGradingEnvelope.testResults` array with `kind: "hint"` so existing styling Just Works.
Two options for execution order:
- **Pre-flight (recommended)**: run the analyzer in `SqlLessonGraderService.gradeSubmission` BEFORE calling DuckDB. Hint banners appear alongside or instead of the actual test result.
- **Post-flight**: run after grading. Less useful because the student already knows whether they passed.
For consistency with regex (which does pre-flight via `RegexLessonHarnessBuilder`), pick pre-flight.
### 5. Pattern engine: don't over-engineer
A regex-based scanner over a normalized whitespace+lowercase string is fine for the first pass. None of the patterns require true AST analysis; they're surface-form heuristics. DuckDB's own AST is reachable via `EXPLAIN` but adds a roundtrip — only pull it in if the regex heuristics produce too many false positives.
## Acceptance
- [ ] `SqlStudentCodeHintAnalyzer` implements `IStudentCodeHintAnalyzer` and registers under `LessonGradingRuntime.DuckDbSql`
- [ ] `StudentCodeHintAnalyzerFactory.For(DuckDbSql)` returns the new analyzer (assertion test)
- [ ] Each of the 7 patterns above has a unit test in `SpikerSoft.Tests.Unit/Domain/Lessons/Hints/SqlStudentCodeHintAnalyzerTests.cs` with a positive and negative example
- [ ] Frontend `SqlLessonGraderService` runs the analyzer pre-flight and surfaces hints as `kind: "hint"` test results
- [ ] Visual check: submit a `SELECT name, COUNT(*) FROM travelers` (no GROUP BY) against any Chapter 5 lesson and see the yellow banner
## Closes
Last unchecked bullet of [#132](https://git.spikersoft.com/spikerj/spikersoft-issues/issues/132)'s DoD: **`SqlStudentCodeHintAnalyzer` in factory registry**.
## Labels
`enhancement` · `backend` · `pedagogy` · `sql`
SpikerSoft.Business/Domain/Lessons/Hints/SqlHintPatternLibrary.cs — 5 of 7 patterns from the ticket (EmptyTemplate, MissingGroupBy, CartesianJoin, SelectStarWithGroupBy, AggregateInWhere, EqualsNull). Patterns 6 (ambiguous columns) and 7 (alias reused in WHERE) require schema-aware parsing and are documented as deferred in the library header — worth a small follow-up once #138's DuckDB.NET integration lands so we can borrow its parser.
Frontend port at sql-hint-patterns.ts so hints surface pre-flight in the browser (yellow banners alongside the test result) without a server roundtrip. Same regex catalog, same pattern IDs, same hint copy.
SqlLessonGraderService.gradeSubmission runs the analyzer BEFORE DuckDB; blocking hints short-circuit (sql.empty-template), non-blocking hints prepend to the test-result list.
Tests: 36 backend pattern tests + 25 frontend pattern tests + factory dispatch test — every pattern has positive + negative cases.
Closing.
## Closed by implementation
Shipped:
- [`SpikerSoft.Business/Domain/Lessons/Hints/SqlHintPatternLibrary.cs`](spikersoft-backend/SpikerSoft.Business/Domain/Lessons/Hints/SqlHintPatternLibrary.cs) — 5 of 7 patterns from the ticket (EmptyTemplate, MissingGroupBy, CartesianJoin, SelectStarWithGroupBy, AggregateInWhere, EqualsNull). Patterns 6 (ambiguous columns) and 7 (alias reused in WHERE) require schema-aware parsing and are documented as deferred in the library header — worth a small follow-up once #138's DuckDB.NET integration lands so we can borrow its parser.
- [`SpikerSoft.Business/Domain/CodeExecution/Hints/SqlStudentCodeHintAnalyzer.cs`](spikersoft-backend/SpikerSoft.Business/Domain/CodeExecution/Hints/SqlStudentCodeHintAnalyzer.cs) — implements `IStudentCodeHintAnalyzer`, registered for `LessonGradingRuntime.DuckDbSql` in `StudentCodeHintAnalyzerFactory`.
- Frontend port at [`sql-hint-patterns.ts`](spikersoft-angular/libraries/features/dev-tools-sql-runner/src/lib/sql-hint-patterns.ts) so hints surface pre-flight in the browser (yellow banners alongside the test result) without a server roundtrip. Same regex catalog, same pattern IDs, same hint copy.
- `SqlLessonGraderService.gradeSubmission` runs the analyzer BEFORE DuckDB; blocking hints short-circuit (`sql.empty-template`), non-blocking hints prepend to the test-result list.
- Tests: 36 backend pattern tests + 25 frontend pattern tests + factory dispatch test — every pattern has positive + negative cases.
Closing.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Context
Follow-up to #132. One of the DoD bullets is
SqlStudentCodeHintAnalyzerin factory registry. The other languages each ship a code-hint analyzer that runs before grading and surfaces non-blocking pedagogical nudges (StudentCodeHintAnalyzerFactory.For(runtime)returns the C# / Python / JS one based onLessonGradingRuntime). SQL doesn't have one yet.This is the differentiator vs. a generic SQL editor: when the student forgets
GROUP BYor accidentally writes a Cartesian join, we want a yellow-banner hint that explains why it's wrong rather than a red FAIL with no context.What needs to happen
1. New analyzer class
New file:
SpikerSoft.Business/Domain/Lessons/Hints/SqlStudentCodeHintAnalyzer.cs2. Pattern coverage (from #132)
Each pattern is independent and emits a single hint with a
severity = info(non-blocking). The grader still runs and may pass even if a hint fires — these are teaching signals, not validation.GROUP BY: aggregate (COUNT,SUM,AVG,MIN,MAX) alongside a non-aggregated column inSELECTand noGROUP BYclause — "You're aggregating with a plain column. Did you mean to add a GROUP BY?"FROMwith noJOIN ... ONand noWHERElinking them — "Two tables but no join condition. Every row of A pairs with every row of B — was that the intent?"SELECT *in aGROUP BYquery: "SELECT *withGROUP BYis almost always a mistake — list the grouped columns explicitly."WHERE: "WHEREruns before aggregation. UseHAVINGfor aggregate predicates."= NULL/<> NULL: the perennial classic — "= NULLalways returns nothing. UseIS NULL/IS NOT NULL."nameexists in both tables. Qualify with the alias (t.name/d.name)."SELECTreferenced inWHERE(most engines reject this; DuckDB is permissive but the pattern is non-portable) — "WHEREcannot reference aSELECT-list alias — repeat the expression or use a subquery."3. Wire into the factory
StudentCodeHintAnalyzerFactory: add the new analyzer to the dispatch dict soFor(DuckDbSql)returns it. Mirror the C# / Python / JS registration pattern.4. Frontend surfacing
The LanguageRunner's lesson pane already renders
testResultswherekind = "hint"as yellow banners (regex does this). The SQL grader needs to inject the analyzer's output into theSqlGradingEnvelope.testResultsarray withkind: "hint"so existing styling Just Works.Two options for execution order:
SqlLessonGraderService.gradeSubmissionBEFORE calling DuckDB. Hint banners appear alongside or instead of the actual test result.For consistency with regex (which does pre-flight via
RegexLessonHarnessBuilder), pick pre-flight.5. Pattern engine: don't over-engineer
A regex-based scanner over a normalized whitespace+lowercase string is fine for the first pass. None of the patterns require true AST analysis; they're surface-form heuristics. DuckDB's own AST is reachable via
EXPLAINbut adds a roundtrip — only pull it in if the regex heuristics produce too many false positives.Acceptance
SqlStudentCodeHintAnalyzerimplementsIStudentCodeHintAnalyzerand registers underLessonGradingRuntime.DuckDbSqlStudentCodeHintAnalyzerFactory.For(DuckDbSql)returns the new analyzer (assertion test)SpikerSoft.Tests.Unit/Domain/Lessons/Hints/SqlStudentCodeHintAnalyzerTests.cswith a positive and negative exampleSqlLessonGraderServiceruns the analyzer pre-flight and surfaces hints askind: "hint"test resultsSELECT name, COUNT(*) FROM travelers(no GROUP BY) against any Chapter 5 lesson and see the yellow bannerCloses
Last unchecked bullet of #132's DoD:
SqlStudentCodeHintAnalyzerin factory registry.Labels
enhancement·backend·pedagogy·sqlClosed by implementation
Shipped:
SpikerSoft.Business/Domain/Lessons/Hints/SqlHintPatternLibrary.cs— 5 of 7 patterns from the ticket (EmptyTemplate, MissingGroupBy, CartesianJoin, SelectStarWithGroupBy, AggregateInWhere, EqualsNull). Patterns 6 (ambiguous columns) and 7 (alias reused in WHERE) require schema-aware parsing and are documented as deferred in the library header — worth a small follow-up once #138's DuckDB.NET integration lands so we can borrow its parser.SpikerSoft.Business/Domain/CodeExecution/Hints/SqlStudentCodeHintAnalyzer.cs— implementsIStudentCodeHintAnalyzer, registered forLessonGradingRuntime.DuckDbSqlinStudentCodeHintAnalyzerFactory.sql-hint-patterns.tsso hints surface pre-flight in the browser (yellow banners alongside the test result) without a server roundtrip. Same regex catalog, same pattern IDs, same hint copy.SqlLessonGraderService.gradeSubmissionruns the analyzer BEFORE DuckDB; blocking hints short-circuit (sql.empty-template), non-blocking hints prepend to the test-result list.Closing.