Skip to content

Phase 3: Stored Procedure Parameter Substitution (embed:true)#1

Draft
prshri-msft wants to merge 10 commits into
ajtiwari07:add-internal-text-embedding-systemfrom
prshri-msft:phase3/embed-parameter-substitution
Draft

Phase 3: Stored Procedure Parameter Substitution (embed:true)#1
prshri-msft wants to merge 10 commits into
ajtiwari07:add-internal-text-embedding-systemfrom
prshri-msft:phase3/embed-parameter-substitution

Conversation

@prshri-msft
Copy link
Copy Markdown
Collaborator

Phase 3 of the Embedding Implementation

Implements #3331 — Parameter Substitution.

What This Adds

A new embed: true flag for stored procedure parameters. When set, DAB:

  1. Receives plain text from the user (REST POST/GET, GraphQL)
  2. Calls EmbeddingService to convert text → vector embedding
  3. Passes the vector to the stored procedure (auto-cast to VECTOR(N) by Azure SQL)
  4. Returns the sproc results

Example config:

"SearchProducts": {
  "source": {
    "type": "stored-procedure",
    "object": "dbo.SearchProducts",
    "parameters": [
      { "name": "query_vector", "embed": true },
      { "name": "top_k", "default": "5" }
    ]
  }
}

Example request:

POST /api/SearchProducts
{ "query_vector": "wireless headphones", "top_k": 3 }
→ Returns ranked products via VECTOR_DISTANCE

Dependency

⚠️ This PR is built on top of add-internal-text-embedding-system (PR #3441) which provides the EmbeddingService infrastructure. Should merge AFTER #3441 lands in main.

Implementation Stages

- Stage 1: Config & validation (embed flag, validation rules — sproc only, requires embeddings, no defaults, MSSQL only)
- Stage 2: DI wiring + ParameterEmbeddingHelper + metadata override (Byte[]→String for VECTOR params)
- Stage
 3.5: First-round reviewer fixes (G9 float precision, JsonElement type validation, hard-fail on null service)
- Stage
 3.6: Reject non-VECTOR sproc params (NVARCHAR, INT, etc.) at startup with clear error
- Stage
 3.7/3.8: Polish + careful self-review of over-corrections

Verification

- ✅ 16 manual test cases passing (REST POST positive + 7 negative + REST GET + GraphQL + regression)
- ✅ DAB boots cleanly with no runtime.embeddings configured (verifies optional DI handling)
- ✅ Existing non-embed entities completely unaffected
- ✅ Two independent code reviews approved (rubber-duck + agent-skills:code-reviewer)
- ✅ Five-axis quality review: zero Critical or Important issues

Out of Scope (Documented Future Enhancements)

- Multi-embed param batching (perf, only matters for sprocs with multiple embed params)
- Error code mapping (400/429/500 distinction — needs EmbeddingService API change)
- VECTOR vs varbinary distinction at metadata layer (sys.parameters query — documented limitation)

prshri-msft and others added 6 commits April 30, 2026 13:34
Add 'embed' boolean property to ParameterMetadata for marking stored
procedure parameters that should be automatically embedded via the
EmbeddingService before being passed to the sproc.

Config & Schema:
- ParameterMetadata.cs: added Embed bool property (default false)
- dab.draft.schema.json: added 'embed' to parameter array items

Validation (RuntimeConfigValidator.cs):
- embed:true only valid on stored-procedure entities
- embed:true requires runtime.embeddings configured and enabled
- embed:true cannot coexist with a default value

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Core implementation of automatic text-to-vector substitution for embed:true
parameters. When a user sends text for an embed parameter, DAB automatically
embeds it via EmbeddingService and passes the vector to the stored procedure.

New file:
- ParameterEmbeddingHelper.cs: text -> TryEmbedAsync -> float[] -> JSON string
  substitution. Handles empty/null text (400), embedding failures (500).

DI wiring:
- SqlQueryEngine + SqlMutationEngine: added IEmbeddingService? (optional)
- QueryEngineFactory + MutationEngineFactory: pass service through to engines

Execution path:
- SqlQueryEngine.ExecuteAsync (REST + GraphQL): call helper before
  SqlExecuteStructure construction
- SqlMutationEngine.ExecuteAsync (REST POST): same

Metadata type override (Approach B):
- MsSqlMetadataProvider: for embed:true params where SQL reports VECTOR as
  varbinary/Byte[], override SystemType->String, DbType->String,
  SqlDbType->NVarChar. Follows existing DateTime override pattern.
  Gated by: embed:true AND Byte[] type. Normal varbinary params unaffected.

Verified: 13 tests (5 positive + 8 negative), including semantic search,
cross-language, error cases, and non-embed entity regression check.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Implements 4 fixes from independent code reviews (rubber-duck + agent-skills:code-reviewer):

Fix 1 (Float precision): Changed ToString format from 'G' (G7 default for Single,
~30% precision loss) to 'R' (round-trippable). Embeddings are precision-sensitive
for cosine similarity.

Fix 2 (Non-MSSQL rejection): Added Rule 0 in ValidateEmbedParameters to reject
embed:true on non-MSSQL data sources. The metadata type override only exists in
MsSqlMetadataProvider, so PostgreSQL/MySQL/Cosmos would fail at runtime with
confusing errors. Now caught at startup with clear message.

Fix 3 (Non-string input validation): Added explicit type check before embedding.
Handles both System.String and System.Text.Json.JsonElement (DAB wraps body
values in JsonElement). Rejects Number, Boolean, Array, Object with 400.
Azure OpenAI's embedding API only accepts strings — being permissive at DAB
level would silently embed garbage like 'System.Object[]' for arrays.

Fix 4 (Hot-reload null service hard-fail): Removed silent skip when
_embeddingService is null but embed params exist. Helper now checks upfront
for any embed params, then validates service is available — throws 503 if not.
Prevents data integrity risk during hot-reload edge cases where embeddings
config gets disabled while DAB is running.

Verified: 11 tests (3 positive + 7 negative + 1 GraphQL + 1 non-embed
regression). All pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Addresses reviewer concern about silent failure when embed:true is misconfigured
on a non-VECTOR parameter. Previously, embed:true on an NVARCHAR parameter would
pass validation, bypass the metadata override (since it is not Byte[]), and
silently produce empty/wrong results at request time.

In MsSqlMetadataProvider.FillSchemaForStoredProcedureAsync:
- Restructured the embed:true block to validate type FIRST, then override
- If embed:true is configured but the SystemType is not Byte[] (i.e., not a
  VECTOR-shaped param), throw at startup with clear error message
- Catches: NVARCHAR, INT, DATETIME, and other non-VECTOR types

Documentation updates:
- ParameterMetadata.cs: XML doc warns target sproc param must be VECTOR(N)
- dab.draft.schema.json: schema description includes the requirement
- MsSqlMetadataProvider.cs: comment explains the check rationale and the
  remaining VARBINARY edge case (loud SQL error at request time)

Verified with real misconfiguration: created test sproc with NVARCHAR(MAX)
parameter, marked embed:true in config, DAB fails to start with clear error
identifying the procedure, parameter, and the actual type vs expected.

Known remaining limitation: VECTOR(N) and varbinary(N) are indistinguishable in
INFORMATION_SCHEMA.PARAMETERS. Real varbinary blob misuse would still pass this
check but fail at SQL execution with implicit conversion error - loud and
clear, so accepted. Documented; sys.parameters-based detection filed as future
enhancement.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Addresses Important issues from second-round code review:

Fix 1 (G9 vs R for float precision):
- Microsoft docs: "For Single values, the R format specifier in some cases
  fails to successfully round-trip the original value. We recommend that
  you use the G9 format specifier instead."
- Changed ToString("R") to ToString("G9") for guaranteed round-trip
- ParameterEmbeddingHelper.cs

Fix 2 (Scope 503 to per-request):
- Previously: helper threw 503 if entity has any embed param + service is null
- Problem: requests that omit optional embed params shouldn't fail just
  because the embedding service is unavailable
- Now: 503 only thrown when this specific request supplies a value for an
  embed param AND service is null
- Removed misleading hot-reload comment (engines hold cached service ref)
- ParameterEmbeddingHelper.cs

Fix 3 (Validator continue after each rule):
- Previously: a single misconfigured embed param could record up to 4 errors
  in HandleOrRecord-record mode (CLI validate)
- Now: each rule fires continue after recording, preventing redundant noise
- Last rule (Default) doesn't need continue
- RuntimeConfigValidator.cs

Fix 4 (Hoist data source lookup):
- Previously: GetDataSourceFromEntityName called per-parameter inside loop
- Now: called once per entity outside the parameter loop
- Cheap fix; obvious code smell removed
- RuntimeConfigValidator.cs

Verified: all positive and negative tests still pass after these changes.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Reverting two changes from Stage 3.7 that, on reflection, made the code worse:

Revert 1: Removed the continue statements after each validator rule.
- Why reverted: DAB's other validators in RuntimeConfigValidator.cs do NOT
  use this pattern. They HandleOrRecordException and continue checking all
  rules. Adding continue here was inconsistent with the codebase.
- The reviewer's "noisy errors" concern was an aesthetic preference, not a
  correctness issue. Collect-all-errors is the established DAB pattern and
  better UX for users (fix all problems in one pass vs iterate).

Revert 2: Moved the embeddingService null check back to the top of the helper.
- Why reverted: The per-request scoped check made the failure mode unpredictable.
  Same DAB instance might fail with 503 sometimes (when embed param supplied)
  and succeed other times (when omitted). Hard to debug.
- The reviewer's "over-broad 503" concern was theoretical. In practice, if
  embed:true is in your config, the embedding service should be available.
  Silently working when the service is missing creates a half-broken state
  that limps along instead of failing clearly.
- Original Stage 3.5 behavior (fail upfront if any embed params + no service)
  is simpler, more predictable, and matches the principle that misconfiguration
  should fail loud and fast.

Kept from Stage 3.7:
- G9 vs R for float precision (correct fix, Microsoft-documented)
- Hoisted GetDataSourceFromEntityName outside parameter loop (cheap, correct)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
}

// Rule 3: embed:true with a default value is not supported.
// A default value is text (e.g., "wireless headphones") that would need embedding at startup — not supported.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can this limitation about default values be clarified a bit more?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

update the comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the explanation. I think the part below 'non-trivial' is what I was looking for.

Not really important since we can't support it easily now today, but I could see someone trying to argue about the UX one that having a default configured on the server config could allow for the server to change the default without a client update or something? Perhaps a homepage default scenario where in January the parameter for what movies/shopping categories/etc. are shown could change from 'new years' to 'valentines day' or similar

// The service has built-in FusionCache (L1 + optional L2 Redis):
// First call for "wireless headphones" → calls Azure OpenAI API (~200-500ms)
// Second call for same text → cache hit, returns instantly
EmbeddingResult result = await embeddingService.TryEmbedAsync(text, cancellationToken);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is there a limitation that only one parameter can be configured for embedding at a time? I think we'd want to do batching here to avoid multiple sequential waits on the api

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added batching

// - "G9" guarantees the string can be parsed back to the exact same float
// - Embeddings are precision-sensitive — even tiny drift affects cosine similarity scores
string vectorJson = "["
+ string.Join(",", result.Embedding.Select(f => f.ToString("G9", CultureInfo.InvariantCulture)))
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think in phase 1 we're just using G, should that also be G9?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's updated in phase 1 to G9 as well

prshri-msft and others added 4 commits May 1, 2026 15:03
…nale

Addresses two PR review comments from JimRoberts-MS on PR ajtiwari07#1:

  Comment ajtiwari07#2 (ParameterEmbeddingHelper.cs:157):
    "is there a limitation that only one parameter can be configured for
     embedding at a time? I think we'd want to do batching here to avoid
     multiple sequential waits on the api"

  Comment ajtiwari07#1 (RuntimeConfigValidator.cs:507):
    "can this limitation about default values be clarified a bit more?"

Changes:

1. ParameterEmbeddingHelper.cs — refactor to use TryEmbedBatchAsync
   (addresses ajtiwari07#2):

   Restructured the substitution loop into 3 phases:
     - COLLECT — validate each embed param value, gather (paramName, text)
       pairs (preserves per-param error specificity for type/null checks)
     - BATCH   — single TryEmbedBatchAsync call instead of N sequential
       TryEmbedAsync calls (saves ~(N-1) × API_LATENCY on cache miss)
     - SUBSTITUTE — write each returned vector back into resolvedParams

   Behavior preserved:
     - Single-embed-param path is equivalent (batch of 1)
     - All error status codes unchanged (400 for bad input, 500 for
       service failure, 503 for missing service)
     - In-place mutation contract on resolvedParams unchanged
     - G9 float format preserved

   Defensive checks added:
     - Length mismatch between requests and returned embeddings → 500
     - Null/empty embedding for any individual param → 500

   Type validation extracted into private ExtractTextValue(paramName, value)
   helper to flatten the nested if/else in the main loop.

   Verified with 16 manual test cases across REST POST/GET and GraphQL,
   covering single-embed and multi-embed sprocs, positive and negative
   inputs.

2. ParameterEmbeddingHelper.cs — fix misleading internal comment
   (related to ajtiwari07#1's surrounding context):

   The previous comment claimed "DAB's existing required-param validation
   handles missing required params later" — but DAB's request validation
   for sprocs only checks for extra fields (not missing ones). The actual
   mechanism that catches missing required params is the SQL Server error
   "expects parameter X, which was not supplied", parsed by
   MsSqlDbExceptionParser into a 400 DatabaseInputError. Updated the
   comment to describe what actually happens.

3. RuntimeConfigValidator.cs — expand embed/default rule rationale
   (addresses ajtiwari07#1):

   Replaced the one-line rationale for "Rule 3: embed:true with a default
   value is not supported" with a multi-paragraph explanation that:

     - Leads with the conceptual UX point: an embed param represents
       user input (typically a search query); defaulting it would mean
       the server fabricates and embeds a query the user never typed.
       That isn't a sensible fallback.

     - Notes that defaults on non-embed params of the same sproc remain
       supported (rule only fires for embed: true params).

     - Briefly documents why even setting aside the UX concern, supporting
       embed-defaults would be non-trivial (GraphQL schema literal-baking
       has no VECTOR type; REST/MCP defaults would be re-embedded every
       request; embedding-at-startup couples startup to provider
       availability).

     - Documents the current observed behavior when a client forgets to
       supply an embed param (verified empirically): explicit null/empty
       → 400 BadRequest from the helper; field omitted → 400
       DatabaseInputError from SQL via MsSqlDbExceptionParser. Both
       produce a clear, actionable client error.

     - Notes that the rule can be lifted later if a real use case emerges.

No behavior changes beyond the batching itself. Validator rule unchanged;
helper logic for missing-value handling unchanged; error message text
unchanged.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Behavior-preserving refactors to make Phase 3's previously-untestable internal
logic accessible to the test project. No production behavior changes.

Three changes:

1. src/Core/Azure.DataApiBuilder.Core.csproj — add InternalsVisibleTo

   New ItemGroup:
     <InternalsVisibleTo Include="Azure.DataApiBuilder.Service.Tests" />

   Lets the test project directly invoke 'internal' members of Core. This is
   a new pattern in DAB (no existing usages of InternalsVisibleTo). Adopted
   intentionally so we can test implementation-detail helpers without
   expanding the production public API surface.

2. src/Core/Configurations/RuntimeConfigValidator.cs — visibility change

   ValidateEmbedParameters: private → internal

   Now testable from Service.Tests via the InternalsVisibleTo bridge above.
   Added an XML <remarks> block explaining why it's internal rather than
   private, and noting that external callers should still go through
   ValidateConfigProperties.

3. src/Core/Services/MetadataProviders/MsSqlMetadataProvider.cs — extract helper

   Pulled the inline embed type override + non-VECTOR rejection logic out
   of FillSchemaForStoredProcedureAsync into a new internal static method:

     internal static void ApplyEmbedTypeOverride(
         ParameterDefinition parameterDefinition,
         ParameterMetadata paramMetadata,
         string schemaName,
         string storedProcedureName,
         string parameterName)

   The body is byte-for-byte the same logic that was previously inline
   (early-returns when Embed is false; throws if not Byte[]; otherwise
   overrides SystemType/DbType/SqlDbType). All explanatory comments
   preserved on the helper as XML <remarks>. The original call site is
   replaced with a single call to ApplyEmbedTypeOverride.

   Helper is internal static so tests can construct ParameterDefinition
   instances and invoke it directly without needing a real metadata
   provider or DB connection.

Why this commit lands separately from the actual tests
------------------------------------------------------
These three changes are pure refactors with no test code yet. Stage 4.2-4.4
add the new tests that depend on these refactors. Splitting the refactors
into their own commit keeps each commit focused and bisect-friendly:
this commit can be reverted independently if a future refactor needs to
reorganize the helper without disturbing test code.

Verification
------------
- dotnet build src/Core/Azure.DataApiBuilder.Core.csproj -c Release →
  Build succeeded. 0 Warning(s). 0 Error(s).
- dotnet build src/Service.Tests/Azure.DataApiBuilder.Service.Tests.csproj
  -c Release →
  Build succeeded. 0 Warning(s). 0 Error(s).
  (Confirms InternalsVisibleTo is honored — test project still builds
  cleanly with no test code yet referencing the internals.)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds the first formal automated test coverage for Phase 3 production code.

New file:
  src/Service.Tests/UnitTests/ParameterEmbeddingHelperTests.cs

28 tests covering ParameterEmbeddingHelper.SubstituteEmbedParametersAsync,
organized into 7 #region blocks (matching the EmbeddingServiceTests.cs
"#region Batch Embedding Tests" pattern):

#region No-Op Cases (4 tests)
  - NullConfigParams_ReturnsImmediately_NoServiceCall
  - NoEmbedParams_ReturnsImmediately_NoServiceCall
  - EmbedParamsConfiguredButNoneSupplied_ReturnsAfterCollect_NoServiceCall
  - EmptyConfigParamsList_ReturnsImmediately_NoServiceCall

#region Service Availability (2 tests)
  - NullService_WithEmbedParams_Throws503  (defense-in-depth)
  - NullService_WithoutEmbedParams_NoThrow  (backward compat)

#region Input Type Validation (8 tests)
  - PlainString_AcceptsAndEmbeds
  - JsonElementString_AcceptsAndEmbeds
  - JsonElementNumber_Throws400
  - JsonElementBoolean_Throws400
  - JsonElementArray_Throws400
  - JsonElementObject_Throws400
  - JsonElementNull_Throws400AsEmpty
  - NonStringNonJsonElementValue_Throws400

#region Empty And Whitespace Validation (3 tests)
  - EmptyString_Throws400
  - WhitespaceString_Throws400
  - NullValue_Throws400

#region Batching Behavior (5 tests) — covers Jim review comment ajtiwari07#2
  - SingleEmbedParam_CallsBatchOnce_NotSequential
  - MultipleEmbedParams_CallsBatchOnce_NotSequential   ← key batching guarantee
  - MixedEmbedAndNonEmbed_OnlyEmbedTextsBatched
  - MultipleEmbedParams_OrderPreserved_BatchTextsMatchConfigOrder
  - PartiallySuppliedEmbedParams_BatchesSubsetOnly

#region Batch Result Handling (4 tests)
  - BatchSuccess_VectorsSubstitutedInResolvedParams
  - BatchFailure_Throws500_WithAllParamNames
  - BatchLengthMismatch_Throws500
  - IndividualEmbeddingEmpty_Throws500_NamingFailedParam

#region Output Format And Cancellation (2 tests)
  - VectorJson_UsesG9AndInvariantCulture  ← validates locale-independent
                                            G9 float serialization
  - CancellationToken_ForwardedToEmbeddingService

Implementation notes
--------------------
- Mocking pattern: Mock<IEmbeddingService> (Strict). Each test sets up the
  expected batch call and verifies post-conditions on resolvedParams. No
  database, no DI container — pure unit tests.

- Helper factories at top of class (EmbedParam, NormalParam, JsonElementFrom,
  SetupBatch, VerifyBatchedExactlyOnce) keep individual test bodies focused
  on the behavior being tested.

- JsonElement construction uses JsonDocument.Parse(...).RootElement.Clone()
  so the parsed element survives after the source document is disposed.

- Float values used in expected-output assertions are powers of 1/2 (0.5,
  0.25, 0.125) which are exactly representable in binary float and round-trip
  through G9 to the same string representation. The "G9 + InvariantCulture"
  test specifically uses non-exact values (0.1f, -0.2f, 0.0001234567f) and
  asserts on parsed-back values rather than exact strings to verify precision
  and locale independence.

- `#nullable enable` directive at the top of the file is required because the
  helper signature uses IDictionary<string, object?> and the test project is
  set to `<Nullable>disable</Nullable>` globally. Per-file enable is the
  smallest scoped change that lets us match the helper's signature without
  introducing project-wide nullable warnings.

Test results
------------
- dotnet build src/Service.Tests/Azure.DataApiBuilder.Service.Tests.csproj
  -c Release →  Build succeeded.  0 Warning(s).  0 Error(s).
- dotnet test --filter "FullyQualifiedName~ParameterEmbeddingHelperTests" →
  Total tests: 28.  Passed: 28.  Failed: 0.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ests)

Adds focused unit tests for two pieces of Phase 3 production code that
were untested before Stage 4.1's refactors made them testable.

Files modified
--------------

1. src/Service.Tests/UnitTests/ConfigValidationUnitTests.cs (+10 tests)
   New `#region Embed Parameters Validation` block targeting
   `RuntimeConfigValidator.ValidateEmbedParameters` (now `internal` after
   Stage 4.1; accessible via `[InternalsVisibleTo]`).

   Test coverage by validation rule:
     Rule 0 — embed:true requires MSSQL data source (2 [DataRow]s)
       - Rule 0: embed:true on PostgreSQL → 503
       - Rule 0: embed:true on MySQL → 503
     Rule 1 — embed:true requires stored-procedure entity (2 [DataRow]s + 1)
       - ValidateEmbedParameters_EmbedOnStoredProcedure_NoError (happy path)
       - Rule 1: embed:true on Table → 503
       - Rule 1: embed:true on View → 503
     Rule 2 — embed:true requires embeddings configured (2 [DataRow]s)
       - Rule 2: embeddings.enabled=false → 503
       - Rule 2: embeddings section missing → 503
     Rule 3 — embed:true cannot have a default value (2 tests)
       - ValidateEmbedParameters_EmbedTrue_WithDefault_ThrowsConfigError
       - ValidateEmbedParameters_EmbedTrue_WithoutDefault_NoError
     Multi-entity message content (1 test)
       - ValidateEmbedParameters_MultipleEntities_OneViolates_NamesViolatingEntityAndParam
         (asserts the error names ONLY the offending entity/param,
          not the healthy ones)

   Helper methods (private static, scoped to the test class):
     - BuildEmbeddingsEnabled() — valid EmbeddingsOptions for happy path
     - BuildSprocEntity(...) — stored-procedure entity with one parameter
     - BuildEntityWithSourceType(...) — table/view/sproc entity for Rule 1 tests
     - BuildRuntimeConfigForEmbedTest(...) — assembles the runtime config

   Uses [DataTestMethod] + [DataRow] where test shapes repeat (matches the
   existing ValidateEmbeddingsOptions_BaseUrl pattern in this file).

2. src/Service.Tests/UnitTests/SqlMetadataProviderUnitTests.cs (+6 tests)
   New `#region Embed Type Override` block targeting
   `MsSqlMetadataProvider.ApplyEmbedTypeOverride` (the static helper extracted
   in Stage 4.1; `internal` accessible via `[InternalsVisibleTo]`).

   Test coverage:
     Type override behavior (4 tests)
       - ApplyEmbedTypeOverride_ByteArrayParam_WithEmbedTrue_OverridesToString
       - ApplyEmbedTypeOverride_StringParam_WithEmbedTrue_ThrowsAtStartup
       - ApplyEmbedTypeOverride_IntParam_WithEmbedTrue_ThrowsAtStartup
       - ApplyEmbedTypeOverride_ByteArrayParam_WithEmbedFalse_NoChange
     Edge cases (2 tests)
       - ApplyEmbedTypeOverride_ByteArrayWithRequiredAndDefault_OnlyTypeMetadataChanges
         (proves helper only mutates type fields, not Required/Default/etc.)
       - ApplyEmbedTypeOverride_MultipleParams_OnlyEmbedTrueOnesOverridden
         (multi-call scenario; proves no cross-call state)

Implementation notes
--------------------
- All 16 tests construct ParameterDefinition / ParameterMetadata / Entity /
  RuntimeConfig instances directly. No DB connection, no DI container, no
  full metadata-provider construction. Pure unit tests.

- Validator-test helper deliberately leaves embeddings as null when not
  passed (NOT defaulted to enabled). Tests targeting Rule 2's "section
  missing" path can pass null literally; tests targeting other rules
  must pass BuildEmbeddingsEnabled() explicitly.

- Order of rule checks (Rule 0 → 1 → 2 → 3) is reflected in the test
  configurations: tests targeting a specific rule satisfy all earlier
  rules so the targeted rule fires first.

- StringAssert.Contains is used for error-message verification rather
  than full-string equality, to allow the validator's error messages
  to evolve without breaking tests.

Test results
------------
- dotnet build src/Service.Tests/Azure.DataApiBuilder.Service.Tests.csproj
  -c Release →  Build succeeded.  0 Warning(s).  0 Error(s).
- dotnet test --filter
  "FullyQualifiedName~ApplyEmbedTypeOverride|FullyQualifiedName~ValidateEmbedParameters"
  →  Total tests: 16.  Passed: 16.  Failed: 0.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
prshri-msft pushed a commit that referenced this pull request May 6, 2026
* Fix logs still appearing even when LogLevel is set to `none` bug (Azure#3318)

## Why make this change?
- Closes issue Azure#3262 
The logger for the Startup class is not initialized properly, since this
logger is special due to the nature of the Startup class it needs to be
continuously updated as DAB initializes. This causes two problems:
- Some logs appear even when LogLevel is set to some value that would
impede those logs to appear.
- Some logs don't appear at all, even when LogLevel is set to a value
that should allow them to be logged.

- Closes issue Azure#3256 & Azure#3255
The CLI logger still outputs some logs even when the LogLevel is set to
`none`. It is expected that if the LogLevel set is `none` or some other
level that shouldn't output the `information` level, the logs will not
appear.

## What is this change?
Important Note: These changes currently only allow us to change the
LogLevel from the CLI with the `default` namespace in the config file.
An task was created to solve this issue:
Azure#3451

In order to solve issue Azure#3262:
- We removed the LogBuffer from the services inside of `Startup.cs`,
this is necessary since we wanted each class to have its own LogBuffer
so that we are able to tell from which logger the logs are being
outputted.
- Then, we also correctly initialized the `Startup` logger by changing
the method that it was using to initialize the logger, it now uses
`CreateLoggerFactoryForHostedAndNonHostedScenario` which checks if there
are any LogLevel namespaces from the config file that can be applicable
for the specific logger. It is important to note that there are multiple
places where the logs are flushed in order to cover for the cases in
which an exception is found and causes DAB to end abruptly, and when we
there is an IsLateConfigured scenario.
- We also changed the logger for the LogBuffer in all the missing places
where it creates logs before the logger is able to properly initialize
to add those logs to the LogBuffer and only flush them after the loggers
are initialized.

In order to solve issue Azure#3256 & Azure#3255: 
- We changed the CLI so that we add all the logs go to a single global
LogBuffer that is created inside the `StartOptions.cs` until it is able
to deserialize the RuntimeConfig and find which level to set the
`LogLevel` in order to flush all the logs.
- This is something that we only want to happen when we use the `dab
start` command, which is why we only make this change in the
`StartOptions.cs` file, on the function `TryStartEngineWithOptions`
inside of `ConfigGenerator.cs`, and a few functions from `Utils.cs` and
`ConfigMerger.cs` that are used inside the `TryStartEngine` function.

## How was this tested?

- [ ] Integration Tests
- [x] Unit Tests

## Sample Request(s)
- dab start --LogLevel none
- dab start --LogLevel error

---------

Co-authored-by: Aniruddh Munde <anmunde@microsoft.com>

* Update config validation logic for entities (Azure#3306)

## Why make this change?

Closes Azure#3267

## What is this change?

Alters the validation logic in the following way.

Is top-level config with data-source-files? (we call this a `Root`
config file)
├── YES
│ ├── Has datasource? → ValidateEntityPresence (same rules as non-root)
│   ├── No datasource but has entities/autoentities? → ERROR
│   └── No datasource, no entities → VALID (children provide everything)
│   └── For each child → ValidateNonRootConfig(child, filename)
│
└── NO (standalone or child config)
    ├── No datasource? → ERROR: "data source is required"
    └── Has datasource → ValidateEntityPresence

Note: A top-level config file without any children data-source files is
NOT considered a root. And an intermediary config file, ie: is a child,
that also has child configs is NOT a root. Only a top-level config with
children configs is a Root.

#### ValidateEntityPresence
Count resolved autoentities from AutoentityResolutionCounts
total = manual entities + resolved autoentities

total == 0? → ERROR: "No entities found"
total > 0 but autoentities discovered nothing? → WARN: "Autoentities
configured but none discovered"

No double messaging. If total is 0, only the error is recorded, not the
warning.

## How was this tested?

### Truth table — top-level config

Variables (`1` = present / non-empty, `0` = absent / empty):
- **DSF** — `data-source-files` present
- **DS** — `data-source` present
- **E** — manual `entities` count > 0
- **AE** — `autoentities` count > 0 (presence, *not* resolved count)

Path is determined by `IsRootConfig = (DSF == 1) && !IsChildConfig`.

| # | DSF | DS | E | AE | AE resolved | Path | Expected | Test |
|---|:---:|:--:|:-:|:--:|:-----------:|------|----------|------|
| 1 | 0 | 0 | 0 | 0 | — | Non-root | **Error**: "data source is
required" | `TestNonRootWithNoDataSourceProducesError` |
| 2 | 0 | 0 | 0 | 1 | — | Non-root | **Error**: "data source is
required" | _covered by #1 — DS check fires first_ |
| 3 | 0 | 0 | 1 | 0 | — | Non-root | **Error**: "data source is
required" | _covered by #1_ |
| 4 | 0 | 0 | 1 | 1 | — | Non-root | **Error**: "data source is
required" | _covered by #1_ |
| 5 | 0 | 1 | 0 | 0 | — | Non-root | **Error**: "No entities found" |
`TestNonRootWithDataSourceAndNoEntitiesProducesError` |
| 6a | 0 | 1 | 0 | 1 | 0 | Non-root | **Error**: "No entities found" |
`TestNonRootWithDataSourceAndAutoentitiesResolvingZeroProducesError` |
| 6b | 0 | 1 | 0 | 1 | >0 | Non-root | **Valid** |
`TestNonRootWithDataSourceAndAutoentitiesResolvingEntitiesIsValid` |
| 7 | 0 | 1 | 1 | 0 | — | Non-root | **Valid** |
`TestNonRootWithDataSourceAndEntitiesIsValid` |
| 8a | 0 | 1 | 1 | 1 | 0 | Non-root | **Valid** + **Warn** |
`TestNonRootWithEntitiesAndAutoentitiesResolvingZeroLogsWarning` |
| 8b | 0 | 1 | 1 | 1 | >0 | Non-root | **Valid** | _covered by #7 / #6b
combined_ |
| 9 | 1 | 0 | 0 | 0 | — | Root | **Valid** (children carry the load) |
`TestRootWithNoDataSourceAndNoEntitiesIsValid`,
`TestRootConfigWithNoDataSourceAndNoEntitiesParses` |
| 10 | 1 | 0 | 0 | 1 | — | Root | **Error**: "must not define entities
or autoentities" |
`TestRootWithNoDataSourceButAutoentitiesProducesError` |
| 11 | 1 | 0 | 1 | 0 | — | Root | **Error**: "must not define entities"
| `TestRootWithNoDataSourceButEntitiesProducesError` |
| 12 | 1 | 0 | 1 | 1 | — | Root | **Error** | _covered by #11_ |
| 13 | 1 | 1 | 0 | 0 | — | Root (with own DS) | **Error**: "No entities
found" | `TestRootWithDataSourceAndNoEntitiesProducesError` |
| 14a | 1 | 1 | 0 | 1 | 0 | Root (with own DS) | **Error**: "No entities
found" |
`TestRootWithDataSourceAndAutoentitiesResolvingZeroProducesError` |
| 14b | 1 | 1 | 0 | 1 | >0 | Root (with own DS) | **Valid** |
`TestRootWithDataSourceAndAutoentitiesResolvingEntitiesIsValid` |
| 15 | 1 | 1 | 1 | 0 | — | Root (with own DS) | **Valid** |
`TestRootWithDataSourceAndEntitiesIsValid` |
| 16a | 1 | 1 | 1 | 1 | 0 | Root (with own DS) | **Valid** + **Warn** |
`TestRootWithEntitiesAndAutoentitiesResolvingZeroLogsWarning` |
| 16b | 1 | 1 | 1 | 1 | >0 | Root (with own DS) | **Valid** | _covered
by Azure#15 / #14b combined_ |

### Truth table — child config (validated when iterating
`root.ChildConfigs`)

Children are always treated as non-root regardless of their own
`data-source-files`.

| # | DS | E | AE | AE resolved | Expected | Test |
|---|:--:|:-:|:--:|:-----------:|----------|------|
| C1 | 0 | 0 | 0 | — | **Error** naming the child file: "data source is
required" | `TestChildWithNoDataSourceProducesNamedError` |
| C2 | 0 | * | * | — | **Error** naming the child file: "data source is
required" | _covered by C1_ |
| C3 | 1 | 0 | 0 | — | **Error** naming the child file: "No entities
found" | `TestChildWithDataSourceAndNoEntitiesProducesNamedError` |
| C4a | 1 | 0 | 1 | 0 | **Error** naming the child file: "No entities
found" |
`TestChildWithDataSourceAndAutoentitiesResolvingZeroProducesNamedError`
|
| C4b | 1 | 0 | 1 | >0 | **Valid** | _covered by C5 (resolved entities
behave the same as manual entities)_ |
| C5 | 1 | 1 | 0 | — | **Valid** | _implicitly via
`TestRootWithDataSourceAndEntitiesIsValid` setup_ |
| C6a | 1 | 1 | 1 | 0 | **Valid** + **Warn** naming the child file |
`TestChildWithEntitiesAndAutoentitiesResolvingZeroLogsNamedWarning` |
| C6b | 1 | 1 | 1 | >0 | **Valid** | _covered by C5_ |

### Other scenarios

| Scenario | Expected | Test |
|----------|----------|------|
| Connection-string error gates entity validation (no entity error fires
when DB unreachable) | `IsConfigValid == false` due to connection error
only | `TestValidateNonRootZeroEntitiesWithInvalidConnectionString` |
| Config with no entities parses cleanly (constructor no longer throws)
and `IsConfigValid` returns false without throwing | parse OK, validate
fails | `TestValidateConfigWithNoEntitiesProducesCleanError`
_(modified)_ |
| Root parses successfully without a data source | parse OK,
`IsRootConfig == true` |
`TestRootConfigWithNoDataSourceAndNoEntitiesParses` |
| Non-root with DS and no entities parses successfully | parse OK,
`IsRootConfig == false` |
`TestNonRootConfigWithDataSourceAndNoEntitiesParses` |
| Autoentities present but resolve to nothing — must not crash, must not
double-message with "No entities found" | no crash; only "No entities
found" if total = 0 | `ValidateAutoentitiesConfiguration` _(modified to
`isValidateOnly: true`)_ |





New tests:

`TestRootConfigWithNoDataSourceAndNoEntitiesParses` Root config (has
data-source-files) without datasource parses OK
`TestNonRootConfigWithDataSourceAndNoEntitiesParses` Non-root config
with datasource + no entities parses OK (validation catches it later)
`TestNonRootWithDataSourceAndNoEntitiesProducesError` Calls
ValidateDataSourceAndEntityPresence directly, error recorded
`TestNonRootWithNoDataSourceProducesError` No datasource, error with
"data source is required"
`TestNonRootWithDataSourceAndEntitiesIsValid` Datasource + entities, no
errors
`TestRootWithNoDataSourceAndNoEntitiesIsValid` Root with child, no own
datasource, valid
`TestRootWithNoDataSourceButEntitiesProducesError` Root with entities
but no datasource, error
`TestRootWithDataSourceAndEntitiesIsValid` Root with own datasource +
entities, valid
`TestChildWithDataSourceAndNoEntitiesProducesNamedError` Child with no
entities, error names the child file
`TestChildWithNoDataSourceProducesNamedError` Child with no datasource,
error names the child file
`TestNonRootWithDataSourceAndAutoentitiesResolvingZeroProducesError`
Non-root with only autoentities that resolve to 0
`TestNonRootWithDataSourceAndAutoentitiesResolvingEntitiesIsValid`
Non-root with only autoentities resolving > 0 entities
`TestNonRootWithEntitiesAndAutoentitiesResolvingZeroLogsWarning`
Non-root with manual entities + autoentities resolving 0
`TestRootWithNoDataSourceButAutoentitiesProducesError` Root with no
datasource but autoentities defined
`TestRootWithDataSourceAndNoEntitiesProducesError` Root with own
datasource and zero entities/autoentities
`TestRootWithDataSourceAndAutoentitiesResolvingZeroProducesError` Root
with own datasource and autoentities resolving 0
`TestRootWithDataSourceAndAutoentitiesResolvingEntitiesIsValid` Root
with own datasource and autoentities resolving > 0
`TestRootWithEntitiesAndAutoentitiesResolvingZeroLogsWarning` Root with
own datasource, manual entities, and autoentities resolving 0
`TestChildWithDataSourceAndAutoentitiesResolvingZeroProducesNamedError`
Child with autoentities-only resolving 0
`TestChildWithEntitiesAndAutoentitiesResolvingZeroLogsNamedWarning`
Child with manual entities + autoentities resolving 0

Modified tests:

`TestValidateConfigWithNoEntitiesProducesCleanError` Replaced main's
version (expected parse failure) with ours: parse succeeds,
IsConfigValid returns false
`ValidateAutoentitiesConfiguration` Changed to isValidateOnly: true,
asserts no crashes instead of zero errors

---------

Co-authored-by: Anusha Kolan <anushakolan10@gmail.com>

---------

Co-authored-by: RubenCerna2079 <32799214+RubenCerna2079@users.noreply.github.com>
Co-authored-by: Aniruddh Munde <anmunde@microsoft.com>
Co-authored-by: aaronburtle <93220300+aaronburtle@users.noreply.github.com>
Co-authored-by: Anusha Kolan <anushakolan10@gmail.com>
Co-authored-by: Sayali Kudale <sayalikudale@microsoft.com>
prshri-msft pushed a commit that referenced this pull request May 6, 2026
* Fix logs still appearing even when LogLevel is set to `none` bug (Azure#3318)

## Why make this change?
- Closes issue Azure#3262 
The logger for the Startup class is not initialized properly, since this
logger is special due to the nature of the Startup class it needs to be
continuously updated as DAB initializes. This causes two problems:
- Some logs appear even when LogLevel is set to some value that would
impede those logs to appear.
- Some logs don't appear at all, even when LogLevel is set to a value
that should allow them to be logged.

- Closes issue Azure#3256 & Azure#3255
The CLI logger still outputs some logs even when the LogLevel is set to
`none`. It is expected that if the LogLevel set is `none` or some other
level that shouldn't output the `information` level, the logs will not
appear.

## What is this change?
Important Note: These changes currently only allow us to change the
LogLevel from the CLI with the `default` namespace in the config file.
An task was created to solve this issue:
Azure#3451

In order to solve issue Azure#3262:
- We removed the LogBuffer from the services inside of `Startup.cs`,
this is necessary since we wanted each class to have its own LogBuffer
so that we are able to tell from which logger the logs are being
outputted.
- Then, we also correctly initialized the `Startup` logger by changing
the method that it was using to initialize the logger, it now uses
`CreateLoggerFactoryForHostedAndNonHostedScenario` which checks if there
are any LogLevel namespaces from the config file that can be applicable
for the specific logger. It is important to note that there are multiple
places where the logs are flushed in order to cover for the cases in
which an exception is found and causes DAB to end abruptly, and when we
there is an IsLateConfigured scenario.
- We also changed the logger for the LogBuffer in all the missing places
where it creates logs before the logger is able to properly initialize
to add those logs to the LogBuffer and only flush them after the loggers
are initialized.

In order to solve issue Azure#3256 & Azure#3255: 
- We changed the CLI so that we add all the logs go to a single global
LogBuffer that is created inside the `StartOptions.cs` until it is able
to deserialize the RuntimeConfig and find which level to set the
`LogLevel` in order to flush all the logs.
- This is something that we only want to happen when we use the `dab
start` command, which is why we only make this change in the
`StartOptions.cs` file, on the function `TryStartEngineWithOptions`
inside of `ConfigGenerator.cs`, and a few functions from `Utils.cs` and
`ConfigMerger.cs` that are used inside the `TryStartEngine` function.

## How was this tested?

- [ ] Integration Tests
- [x] Unit Tests

## Sample Request(s)
- dab start --LogLevel none
- dab start --LogLevel error

---------

Co-authored-by: Aniruddh Munde <anmunde@microsoft.com>

* Update config validation logic for entities (Azure#3306)

## Why make this change?

Closes Azure#3267

## What is this change?

Alters the validation logic in the following way.

Is top-level config with data-source-files? (we call this a `Root`
config file)
├── YES
│ ├── Has datasource? → ValidateEntityPresence (same rules as non-root)
│   ├── No datasource but has entities/autoentities? → ERROR
│   └── No datasource, no entities → VALID (children provide everything)
│   └── For each child → ValidateNonRootConfig(child, filename)
│
└── NO (standalone or child config)
    ├── No datasource? → ERROR: "data source is required"
    └── Has datasource → ValidateEntityPresence

Note: A top-level config file without any children data-source files is
NOT considered a root. And an intermediary config file, ie: is a child,
that also has child configs is NOT a root. Only a top-level config with
children configs is a Root.

#### ValidateEntityPresence
Count resolved autoentities from AutoentityResolutionCounts
total = manual entities + resolved autoentities

total == 0? → ERROR: "No entities found"
total > 0 but autoentities discovered nothing? → WARN: "Autoentities
configured but none discovered"

No double messaging. If total is 0, only the error is recorded, not the
warning.

## How was this tested?

### Truth table — top-level config

Variables (`1` = present / non-empty, `0` = absent / empty):
- **DSF** — `data-source-files` present
- **DS** — `data-source` present
- **E** — manual `entities` count > 0
- **AE** — `autoentities` count > 0 (presence, *not* resolved count)

Path is determined by `IsRootConfig = (DSF == 1) && !IsChildConfig`.

| # | DSF | DS | E | AE | AE resolved | Path | Expected | Test |
|---|:---:|:--:|:-:|:--:|:-----------:|------|----------|------|
| 1 | 0 | 0 | 0 | 0 | — | Non-root | **Error**: "data source is
required" | `TestNonRootWithNoDataSourceProducesError` |
| 2 | 0 | 0 | 0 | 1 | — | Non-root | **Error**: "data source is
required" | _covered by #1 — DS check fires first_ |
| 3 | 0 | 0 | 1 | 0 | — | Non-root | **Error**: "data source is
required" | _covered by #1_ |
| 4 | 0 | 0 | 1 | 1 | — | Non-root | **Error**: "data source is
required" | _covered by #1_ |
| 5 | 0 | 1 | 0 | 0 | — | Non-root | **Error**: "No entities found" |
`TestNonRootWithDataSourceAndNoEntitiesProducesError` |
| 6a | 0 | 1 | 0 | 1 | 0 | Non-root | **Error**: "No entities found" |
`TestNonRootWithDataSourceAndAutoentitiesResolvingZeroProducesError` |
| 6b | 0 | 1 | 0 | 1 | >0 | Non-root | **Valid** |
`TestNonRootWithDataSourceAndAutoentitiesResolvingEntitiesIsValid` |
| 7 | 0 | 1 | 1 | 0 | — | Non-root | **Valid** |
`TestNonRootWithDataSourceAndEntitiesIsValid` |
| 8a | 0 | 1 | 1 | 1 | 0 | Non-root | **Valid** + **Warn** |
`TestNonRootWithEntitiesAndAutoentitiesResolvingZeroLogsWarning` |
| 8b | 0 | 1 | 1 | 1 | >0 | Non-root | **Valid** | _covered by #7 / #6b
combined_ |
| 9 | 1 | 0 | 0 | 0 | — | Root | **Valid** (children carry the load) |
`TestRootWithNoDataSourceAndNoEntitiesIsValid`,
`TestRootConfigWithNoDataSourceAndNoEntitiesParses` |
| 10 | 1 | 0 | 0 | 1 | — | Root | **Error**: "must not define entities
or autoentities" |
`TestRootWithNoDataSourceButAutoentitiesProducesError` |
| 11 | 1 | 0 | 1 | 0 | — | Root | **Error**: "must not define entities"
| `TestRootWithNoDataSourceButEntitiesProducesError` |
| 12 | 1 | 0 | 1 | 1 | — | Root | **Error** | _covered by #11_ |
| 13 | 1 | 1 | 0 | 0 | — | Root (with own DS) | **Error**: "No entities
found" | `TestRootWithDataSourceAndNoEntitiesProducesError` |
| 14a | 1 | 1 | 0 | 1 | 0 | Root (with own DS) | **Error**: "No entities
found" |
`TestRootWithDataSourceAndAutoentitiesResolvingZeroProducesError` |
| 14b | 1 | 1 | 0 | 1 | >0 | Root (with own DS) | **Valid** |
`TestRootWithDataSourceAndAutoentitiesResolvingEntitiesIsValid` |
| 15 | 1 | 1 | 1 | 0 | — | Root (with own DS) | **Valid** |
`TestRootWithDataSourceAndEntitiesIsValid` |
| 16a | 1 | 1 | 1 | 1 | 0 | Root (with own DS) | **Valid** + **Warn** |
`TestRootWithEntitiesAndAutoentitiesResolvingZeroLogsWarning` |
| 16b | 1 | 1 | 1 | 1 | >0 | Root (with own DS) | **Valid** | _covered
by Azure#15 / #14b combined_ |

### Truth table — child config (validated when iterating
`root.ChildConfigs`)

Children are always treated as non-root regardless of their own
`data-source-files`.

| # | DS | E | AE | AE resolved | Expected | Test |
|---|:--:|:-:|:--:|:-----------:|----------|------|
| C1 | 0 | 0 | 0 | — | **Error** naming the child file: "data source is
required" | `TestChildWithNoDataSourceProducesNamedError` |
| C2 | 0 | * | * | — | **Error** naming the child file: "data source is
required" | _covered by C1_ |
| C3 | 1 | 0 | 0 | — | **Error** naming the child file: "No entities
found" | `TestChildWithDataSourceAndNoEntitiesProducesNamedError` |
| C4a | 1 | 0 | 1 | 0 | **Error** naming the child file: "No entities
found" |
`TestChildWithDataSourceAndAutoentitiesResolvingZeroProducesNamedError`
|
| C4b | 1 | 0 | 1 | >0 | **Valid** | _covered by C5 (resolved entities
behave the same as manual entities)_ |
| C5 | 1 | 1 | 0 | — | **Valid** | _implicitly via
`TestRootWithDataSourceAndEntitiesIsValid` setup_ |
| C6a | 1 | 1 | 1 | 0 | **Valid** + **Warn** naming the child file |
`TestChildWithEntitiesAndAutoentitiesResolvingZeroLogsNamedWarning` |
| C6b | 1 | 1 | 1 | >0 | **Valid** | _covered by C5_ |

### Other scenarios

| Scenario | Expected | Test |
|----------|----------|------|
| Connection-string error gates entity validation (no entity error fires
when DB unreachable) | `IsConfigValid == false` due to connection error
only | `TestValidateNonRootZeroEntitiesWithInvalidConnectionString` |
| Config with no entities parses cleanly (constructor no longer throws)
and `IsConfigValid` returns false without throwing | parse OK, validate
fails | `TestValidateConfigWithNoEntitiesProducesCleanError`
_(modified)_ |
| Root parses successfully without a data source | parse OK,
`IsRootConfig == true` |
`TestRootConfigWithNoDataSourceAndNoEntitiesParses` |
| Non-root with DS and no entities parses successfully | parse OK,
`IsRootConfig == false` |
`TestNonRootConfigWithDataSourceAndNoEntitiesParses` |
| Autoentities present but resolve to nothing — must not crash, must not
double-message with "No entities found" | no crash; only "No entities
found" if total = 0 | `ValidateAutoentitiesConfiguration` _(modified to
`isValidateOnly: true`)_ |





New tests:

`TestRootConfigWithNoDataSourceAndNoEntitiesParses` Root config (has
data-source-files) without datasource parses OK
`TestNonRootConfigWithDataSourceAndNoEntitiesParses` Non-root config
with datasource + no entities parses OK (validation catches it later)
`TestNonRootWithDataSourceAndNoEntitiesProducesError` Calls
ValidateDataSourceAndEntityPresence directly, error recorded
`TestNonRootWithNoDataSourceProducesError` No datasource, error with
"data source is required"
`TestNonRootWithDataSourceAndEntitiesIsValid` Datasource + entities, no
errors
`TestRootWithNoDataSourceAndNoEntitiesIsValid` Root with child, no own
datasource, valid
`TestRootWithNoDataSourceButEntitiesProducesError` Root with entities
but no datasource, error
`TestRootWithDataSourceAndEntitiesIsValid` Root with own datasource +
entities, valid
`TestChildWithDataSourceAndNoEntitiesProducesNamedError` Child with no
entities, error names the child file
`TestChildWithNoDataSourceProducesNamedError` Child with no datasource,
error names the child file
`TestNonRootWithDataSourceAndAutoentitiesResolvingZeroProducesError`
Non-root with only autoentities that resolve to 0
`TestNonRootWithDataSourceAndAutoentitiesResolvingEntitiesIsValid`
Non-root with only autoentities resolving > 0 entities
`TestNonRootWithEntitiesAndAutoentitiesResolvingZeroLogsWarning`
Non-root with manual entities + autoentities resolving 0
`TestRootWithNoDataSourceButAutoentitiesProducesError` Root with no
datasource but autoentities defined
`TestRootWithDataSourceAndNoEntitiesProducesError` Root with own
datasource and zero entities/autoentities
`TestRootWithDataSourceAndAutoentitiesResolvingZeroProducesError` Root
with own datasource and autoentities resolving 0
`TestRootWithDataSourceAndAutoentitiesResolvingEntitiesIsValid` Root
with own datasource and autoentities resolving > 0
`TestRootWithEntitiesAndAutoentitiesResolvingZeroLogsWarning` Root with
own datasource, manual entities, and autoentities resolving 0
`TestChildWithDataSourceAndAutoentitiesResolvingZeroProducesNamedError`
Child with autoentities-only resolving 0
`TestChildWithEntitiesAndAutoentitiesResolvingZeroLogsNamedWarning`
Child with manual entities + autoentities resolving 0

Modified tests:

`TestValidateConfigWithNoEntitiesProducesCleanError` Replaced main's
version (expected parse failure) with ours: parse succeeds,
IsConfigValid returns false
`ValidateAutoentitiesConfiguration` Changed to isValidateOnly: true,
asserts no crashes instead of zero errors

---------

Co-authored-by: Anusha Kolan <anushakolan10@gmail.com>

* Add MCP notifications/message for log streaming to clients (Azure#3484)

## Why make this change?

Enables MCP clients (like MCP Inspector, Claude Desktop, VS Code
Copilot) to receive real-time log output via MCP
`notifications/message`.

Related: Azure#3274 (depends on PR Azure#3419)

## What is this change?

When `logging/setLevel` is called with a level other than "none", logs
are sent to MCP clients as JSON-RPC notifications:

```json
{
  "jsonrpc": "2.0",
  "method": "notifications/message",
  "params": {
    "level": "info",
    "logger": "Azure.DataApiBuilder.Service.Startup",
    "data": "Starting Data API builder..."
  }
}
```

### New files:
- `McpLogNotificationWriter.cs` - Writes logs as MCP notifications to
stdout
- `McpLogger.cs` / `McpLoggerProvider.cs` - ILogger implementation for
.NET logging pipeline
- `McpLogNotificationTests.cs` - Unit tests (8 tests)

### Modified files:
- `Program.cs` - Registers `McpNotificationWriter` and
`McpLoggerProvider` for MCP mode
- `McpStdioServer.cs` - Enables notifications when `logging/setLevel` is
called

## How was this tested?

- Unit tests: 6 tests covering level mapping, enable/disable, JSON
format
- Manual testing with MCP Inspector: verified notifications appear when
`logging/setLevel` is sent

## Note

This PR targets `dev/anushakolan/set-log-level` (PR Azure#3419) as it depends
on the `logging/setLevel` implementation.

---------

Co-authored-by: RubenCerna2079 <32799214+RubenCerna2079@users.noreply.github.com>
Co-authored-by: Aniruddh Munde <anmunde@microsoft.com>
Co-authored-by: aaronburtle <93220300+aaronburtle@users.noreply.github.com>
Co-authored-by: Anusha Kolan <anushakolan10@gmail.com>
Co-authored-by: Sayali Kudale <sayalikudale@microsoft.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
prshri-msft pushed a commit that referenced this pull request May 6, 2026
* Fix logs still appearing even when LogLevel is set to `none` bug (Azure#3318)

## Why make this change?
- Closes issue Azure#3262 
The logger for the Startup class is not initialized properly, since this
logger is special due to the nature of the Startup class it needs to be
continuously updated as DAB initializes. This causes two problems:
- Some logs appear even when LogLevel is set to some value that would
impede those logs to appear.
- Some logs don't appear at all, even when LogLevel is set to a value
that should allow them to be logged.

- Closes issue Azure#3256 & Azure#3255
The CLI logger still outputs some logs even when the LogLevel is set to
`none`. It is expected that if the LogLevel set is `none` or some other
level that shouldn't output the `information` level, the logs will not
appear.

## What is this change?
Important Note: These changes currently only allow us to change the
LogLevel from the CLI with the `default` namespace in the config file.
An task was created to solve this issue:
Azure#3451

In order to solve issue Azure#3262:
- We removed the LogBuffer from the services inside of `Startup.cs`,
this is necessary since we wanted each class to have its own LogBuffer
so that we are able to tell from which logger the logs are being
outputted.
- Then, we also correctly initialized the `Startup` logger by changing
the method that it was using to initialize the logger, it now uses
`CreateLoggerFactoryForHostedAndNonHostedScenario` which checks if there
are any LogLevel namespaces from the config file that can be applicable
for the specific logger. It is important to note that there are multiple
places where the logs are flushed in order to cover for the cases in
which an exception is found and causes DAB to end abruptly, and when we
there is an IsLateConfigured scenario.
- We also changed the logger for the LogBuffer in all the missing places
where it creates logs before the logger is able to properly initialize
to add those logs to the LogBuffer and only flush them after the loggers
are initialized.

In order to solve issue Azure#3256 & Azure#3255: 
- We changed the CLI so that we add all the logs go to a single global
LogBuffer that is created inside the `StartOptions.cs` until it is able
to deserialize the RuntimeConfig and find which level to set the
`LogLevel` in order to flush all the logs.
- This is something that we only want to happen when we use the `dab
start` command, which is why we only make this change in the
`StartOptions.cs` file, on the function `TryStartEngineWithOptions`
inside of `ConfigGenerator.cs`, and a few functions from `Utils.cs` and
`ConfigMerger.cs` that are used inside the `TryStartEngine` function.

## How was this tested?

- [ ] Integration Tests
- [x] Unit Tests

## Sample Request(s)
- dab start --LogLevel none
- dab start --LogLevel error

---------

Co-authored-by: Aniruddh Munde <anmunde@microsoft.com>

* Update config validation logic for entities (Azure#3306)

## Why make this change?

Closes Azure#3267

## What is this change?

Alters the validation logic in the following way.

Is top-level config with data-source-files? (we call this a `Root`
config file)
├── YES
│ ├── Has datasource? → ValidateEntityPresence (same rules as non-root)
│   ├── No datasource but has entities/autoentities? → ERROR
│   └── No datasource, no entities → VALID (children provide everything)
│   └── For each child → ValidateNonRootConfig(child, filename)
│
└── NO (standalone or child config)
    ├── No datasource? → ERROR: "data source is required"
    └── Has datasource → ValidateEntityPresence

Note: A top-level config file without any children data-source files is
NOT considered a root. And an intermediary config file, ie: is a child,
that also has child configs is NOT a root. Only a top-level config with
children configs is a Root.

#### ValidateEntityPresence
Count resolved autoentities from AutoentityResolutionCounts
total = manual entities + resolved autoentities

total == 0? → ERROR: "No entities found"
total > 0 but autoentities discovered nothing? → WARN: "Autoentities
configured but none discovered"

No double messaging. If total is 0, only the error is recorded, not the
warning.

## How was this tested?

### Truth table — top-level config

Variables (`1` = present / non-empty, `0` = absent / empty):
- **DSF** — `data-source-files` present
- **DS** — `data-source` present
- **E** — manual `entities` count > 0
- **AE** — `autoentities` count > 0 (presence, *not* resolved count)

Path is determined by `IsRootConfig = (DSF == 1) && !IsChildConfig`.

| # | DSF | DS | E | AE | AE resolved | Path | Expected | Test |
|---|:---:|:--:|:-:|:--:|:-----------:|------|----------|------|
| 1 | 0 | 0 | 0 | 0 | — | Non-root | **Error**: "data source is
required" | `TestNonRootWithNoDataSourceProducesError` |
| 2 | 0 | 0 | 0 | 1 | — | Non-root | **Error**: "data source is
required" | _covered by #1 — DS check fires first_ |
| 3 | 0 | 0 | 1 | 0 | — | Non-root | **Error**: "data source is
required" | _covered by #1_ |
| 4 | 0 | 0 | 1 | 1 | — | Non-root | **Error**: "data source is
required" | _covered by #1_ |
| 5 | 0 | 1 | 0 | 0 | — | Non-root | **Error**: "No entities found" |
`TestNonRootWithDataSourceAndNoEntitiesProducesError` |
| 6a | 0 | 1 | 0 | 1 | 0 | Non-root | **Error**: "No entities found" |
`TestNonRootWithDataSourceAndAutoentitiesResolvingZeroProducesError` |
| 6b | 0 | 1 | 0 | 1 | >0 | Non-root | **Valid** |
`TestNonRootWithDataSourceAndAutoentitiesResolvingEntitiesIsValid` |
| 7 | 0 | 1 | 1 | 0 | — | Non-root | **Valid** |
`TestNonRootWithDataSourceAndEntitiesIsValid` |
| 8a | 0 | 1 | 1 | 1 | 0 | Non-root | **Valid** + **Warn** |
`TestNonRootWithEntitiesAndAutoentitiesResolvingZeroLogsWarning` |
| 8b | 0 | 1 | 1 | 1 | >0 | Non-root | **Valid** | _covered by #7 / #6b
combined_ |
| 9 | 1 | 0 | 0 | 0 | — | Root | **Valid** (children carry the load) |
`TestRootWithNoDataSourceAndNoEntitiesIsValid`,
`TestRootConfigWithNoDataSourceAndNoEntitiesParses` |
| 10 | 1 | 0 | 0 | 1 | — | Root | **Error**: "must not define entities
or autoentities" |
`TestRootWithNoDataSourceButAutoentitiesProducesError` |
| 11 | 1 | 0 | 1 | 0 | — | Root | **Error**: "must not define entities"
| `TestRootWithNoDataSourceButEntitiesProducesError` |
| 12 | 1 | 0 | 1 | 1 | — | Root | **Error** | _covered by #11_ |
| 13 | 1 | 1 | 0 | 0 | — | Root (with own DS) | **Error**: "No entities
found" | `TestRootWithDataSourceAndNoEntitiesProducesError` |
| 14a | 1 | 1 | 0 | 1 | 0 | Root (with own DS) | **Error**: "No entities
found" |
`TestRootWithDataSourceAndAutoentitiesResolvingZeroProducesError` |
| 14b | 1 | 1 | 0 | 1 | >0 | Root (with own DS) | **Valid** |
`TestRootWithDataSourceAndAutoentitiesResolvingEntitiesIsValid` |
| 15 | 1 | 1 | 1 | 0 | — | Root (with own DS) | **Valid** |
`TestRootWithDataSourceAndEntitiesIsValid` |
| 16a | 1 | 1 | 1 | 1 | 0 | Root (with own DS) | **Valid** + **Warn** |
`TestRootWithEntitiesAndAutoentitiesResolvingZeroLogsWarning` |
| 16b | 1 | 1 | 1 | 1 | >0 | Root (with own DS) | **Valid** | _covered
by Azure#15 / #14b combined_ |

### Truth table — child config (validated when iterating
`root.ChildConfigs`)

Children are always treated as non-root regardless of their own
`data-source-files`.

| # | DS | E | AE | AE resolved | Expected | Test |
|---|:--:|:-:|:--:|:-----------:|----------|------|
| C1 | 0 | 0 | 0 | — | **Error** naming the child file: "data source is
required" | `TestChildWithNoDataSourceProducesNamedError` |
| C2 | 0 | * | * | — | **Error** naming the child file: "data source is
required" | _covered by C1_ |
| C3 | 1 | 0 | 0 | — | **Error** naming the child file: "No entities
found" | `TestChildWithDataSourceAndNoEntitiesProducesNamedError` |
| C4a | 1 | 0 | 1 | 0 | **Error** naming the child file: "No entities
found" |
`TestChildWithDataSourceAndAutoentitiesResolvingZeroProducesNamedError`
|
| C4b | 1 | 0 | 1 | >0 | **Valid** | _covered by C5 (resolved entities
behave the same as manual entities)_ |
| C5 | 1 | 1 | 0 | — | **Valid** | _implicitly via
`TestRootWithDataSourceAndEntitiesIsValid` setup_ |
| C6a | 1 | 1 | 1 | 0 | **Valid** + **Warn** naming the child file |
`TestChildWithEntitiesAndAutoentitiesResolvingZeroLogsNamedWarning` |
| C6b | 1 | 1 | 1 | >0 | **Valid** | _covered by C5_ |

### Other scenarios

| Scenario | Expected | Test |
|----------|----------|------|
| Connection-string error gates entity validation (no entity error fires
when DB unreachable) | `IsConfigValid == false` due to connection error
only | `TestValidateNonRootZeroEntitiesWithInvalidConnectionString` |
| Config with no entities parses cleanly (constructor no longer throws)
and `IsConfigValid` returns false without throwing | parse OK, validate
fails | `TestValidateConfigWithNoEntitiesProducesCleanError`
_(modified)_ |
| Root parses successfully without a data source | parse OK,
`IsRootConfig == true` |
`TestRootConfigWithNoDataSourceAndNoEntitiesParses` |
| Non-root with DS and no entities parses successfully | parse OK,
`IsRootConfig == false` |
`TestNonRootConfigWithDataSourceAndNoEntitiesParses` |
| Autoentities present but resolve to nothing — must not crash, must not
double-message with "No entities found" | no crash; only "No entities
found" if total = 0 | `ValidateAutoentitiesConfiguration` _(modified to
`isValidateOnly: true`)_ |





New tests:

`TestRootConfigWithNoDataSourceAndNoEntitiesParses` Root config (has
data-source-files) without datasource parses OK
`TestNonRootConfigWithDataSourceAndNoEntitiesParses` Non-root config
with datasource + no entities parses OK (validation catches it later)
`TestNonRootWithDataSourceAndNoEntitiesProducesError` Calls
ValidateDataSourceAndEntityPresence directly, error recorded
`TestNonRootWithNoDataSourceProducesError` No datasource, error with
"data source is required"
`TestNonRootWithDataSourceAndEntitiesIsValid` Datasource + entities, no
errors
`TestRootWithNoDataSourceAndNoEntitiesIsValid` Root with child, no own
datasource, valid
`TestRootWithNoDataSourceButEntitiesProducesError` Root with entities
but no datasource, error
`TestRootWithDataSourceAndEntitiesIsValid` Root with own datasource +
entities, valid
`TestChildWithDataSourceAndNoEntitiesProducesNamedError` Child with no
entities, error names the child file
`TestChildWithNoDataSourceProducesNamedError` Child with no datasource,
error names the child file
`TestNonRootWithDataSourceAndAutoentitiesResolvingZeroProducesError`
Non-root with only autoentities that resolve to 0
`TestNonRootWithDataSourceAndAutoentitiesResolvingEntitiesIsValid`
Non-root with only autoentities resolving > 0 entities
`TestNonRootWithEntitiesAndAutoentitiesResolvingZeroLogsWarning`
Non-root with manual entities + autoentities resolving 0
`TestRootWithNoDataSourceButAutoentitiesProducesError` Root with no
datasource but autoentities defined
`TestRootWithDataSourceAndNoEntitiesProducesError` Root with own
datasource and zero entities/autoentities
`TestRootWithDataSourceAndAutoentitiesResolvingZeroProducesError` Root
with own datasource and autoentities resolving 0
`TestRootWithDataSourceAndAutoentitiesResolvingEntitiesIsValid` Root
with own datasource and autoentities resolving > 0
`TestRootWithEntitiesAndAutoentitiesResolvingZeroLogsWarning` Root with
own datasource, manual entities, and autoentities resolving 0
`TestChildWithDataSourceAndAutoentitiesResolvingZeroProducesNamedError`
Child with autoentities-only resolving 0
`TestChildWithEntitiesAndAutoentitiesResolvingZeroLogsNamedWarning`
Child with manual entities + autoentities resolving 0

Modified tests:

`TestValidateConfigWithNoEntitiesProducesCleanError` Replaced main's
version (expected parse failure) with ours: parse succeeds,
IsConfigValid returns false
`ValidateAutoentitiesConfiguration` Changed to isValidateOnly: true,
asserts no crashes instead of zero errors

---------

Co-authored-by: Anusha Kolan <anushakolan10@gmail.com>

* Add MCP notifications/message for log streaming to clients (Azure#3484)

## Why make this change?

Enables MCP clients (like MCP Inspector, Claude Desktop, VS Code
Copilot) to receive real-time log output via MCP
`notifications/message`.

Related: Azure#3274 (depends on PR Azure#3419)

## What is this change?

When `logging/setLevel` is called with a level other than "none", logs
are sent to MCP clients as JSON-RPC notifications:

```json
{
  "jsonrpc": "2.0",
  "method": "notifications/message",
  "params": {
    "level": "info",
    "logger": "Azure.DataApiBuilder.Service.Startup",
    "data": "Starting Data API builder..."
  }
}
```

### New files:
- `McpLogNotificationWriter.cs` - Writes logs as MCP notifications to
stdout
- `McpLogger.cs` / `McpLoggerProvider.cs` - ILogger implementation for
.NET logging pipeline
- `McpLogNotificationTests.cs` - Unit tests (8 tests)

### Modified files:
- `Program.cs` - Registers `McpNotificationWriter` and
`McpLoggerProvider` for MCP mode
- `McpStdioServer.cs` - Enables notifications when `logging/setLevel` is
called

## How was this tested?

- Unit tests: 6 tests covering level mapping, enable/disable, JSON
format
- Manual testing with MCP Inspector: verified notifications appear when
`logging/setLevel` is sent

## Note

This PR targets `dev/anushakolan/set-log-level` (PR Azure#3419) as it depends
on the `logging/setLevel` implementation.

* Fix OData filter format in JWT string claims (Azure#3510)

## Why make this change?

Fixes the format of the OData filter in JWT string claims.

## What is this change?

In `AuthorizationResolver` we now escape embedded single quotes in claim
values by doubling them, before we wrap the value in single quotes for
OData substitution. This conforms to the OData 4.01 ABNF rule for string
literals (Section 7: Literal Data Values).

Policy: `@item.col1 eq @claims.userId`
Claim `userId` value: `alice' or 1 eq 1 or '`

| | Resulting OData predicate |
| --- | --- |
| Before | `col1 eq 'alice' or 1 eq 1 or ''` <- injects `or 1 eq 1`,
bypassing row-level auth |
| After | `col1 eq 'alice'' or 1 eq 1 or '''` <- attacker payload
contained inside a single string literal |




## How was this tested?

New parameterized test
`DbPolicy_StringClaim_SingleQuotesEscaped_PreventsODataInjection` in
`src/Service.Tests/Authorization/AuthorizationResolverUnitTests.cs`
covers:

- Active OR-predicate injection attempt is neutralized.
- Legitimate apostrophe-bearing value (e.g. `O'Brien`) is safely
escaped.
- Value composed solely of single quotes is fully escaped.
- Value with no single quotes is unchanged aside from the enclosing
quotes (no regression).


## Sample Request(s)


```json
{
  "entities": {
    "Note": {
      "source": "dbo.Notes",
      "permissions": [
        {
          "role": "authenticated",
          "actions": [
            {
              "action": "read",
              "policy": { "database": "@item.ownerId eq @claims.userId" }
            }
          ]
        }
      ]
    }
  }
}
```

Reproduction - `userId` claim value of `alice' or 1 eq 1 or '`:

```http
GET /api/Note HTTP/1.1
Authorization: Bearer <jwt-with-crafted-userId-claim>
X-MS-API-ROLE: authenticated
```

- Before fix: the engine emitted `WHERE ownerId = 'alice' or 1 eq 1 or
''`, returning rows owned by other users.
- After fix: the engine emits `WHERE ownerId = 'alice'' or 1 eq 1 or
'''`, which compares against the literal string `alice' or 1 eq 1 or '`
and returns no unauthorized rows.

Co-authored-by: Souvik Ghosh <souvikofficial04@gmail.com>
Co-authored-by: Aniruddh Munde <anmunde@microsoft.com>

---------

Co-authored-by: RubenCerna2079 <32799214+RubenCerna2079@users.noreply.github.com>
Co-authored-by: Aniruddh Munde <anmunde@microsoft.com>
Co-authored-by: aaronburtle <93220300+aaronburtle@users.noreply.github.com>
Co-authored-by: Anusha Kolan <anushakolan10@gmail.com>
Co-authored-by: Souvik Ghosh <souvikofficial04@gmail.com>
Co-authored-by: Sayali Kudale <sayalikudale@microsoft.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
JimRoberts-MS pushed a commit that referenced this pull request May 7, 2026
## Why make this change?

Closes Azure#3267

## What is this change?

Alters the validation logic in the following way.

Is top-level config with data-source-files? (we call this a `Root`
config file)
├── YES
│ ├── Has datasource? → ValidateEntityPresence (same rules as non-root)
│   ├── No datasource but has entities/autoentities? → ERROR
│   └── No datasource, no entities → VALID (children provide everything)
│   └── For each child → ValidateNonRootConfig(child, filename)
│
└── NO (standalone or child config)
    ├── No datasource? → ERROR: "data source is required"
    └── Has datasource → ValidateEntityPresence

Note: A top-level config file without any children data-source files is
NOT considered a root. And an intermediary config file, ie: is a child,
that also has child configs is NOT a root. Only a top-level config with
children configs is a Root.

#### ValidateEntityPresence
Count resolved autoentities from AutoentityResolutionCounts
total = manual entities + resolved autoentities

total == 0? → ERROR: "No entities found"
total > 0 but autoentities discovered nothing? → WARN: "Autoentities
configured but none discovered"

No double messaging. If total is 0, only the error is recorded, not the
warning.

## How was this tested?

### Truth table — top-level config

Variables (`1` = present / non-empty, `0` = absent / empty):
- **DSF** — `data-source-files` present
- **DS** — `data-source` present
- **E** — manual `entities` count > 0
- **AE** — `autoentities` count > 0 (presence, *not* resolved count)

Path is determined by `IsRootConfig = (DSF == 1) && !IsChildConfig`.

| # | DSF | DS | E | AE | AE resolved | Path | Expected | Test |
|---|:---:|:--:|:-:|:--:|:-----------:|------|----------|------|
| 1 | 0 | 0 | 0 | 0 | — | Non-root | **Error**: "data source is
required" | `TestNonRootWithNoDataSourceProducesError` |
| 2 | 0 | 0 | 0 | 1 | — | Non-root | **Error**: "data source is
required" | _covered by #1 — DS check fires first_ |
| 3 | 0 | 0 | 1 | 0 | — | Non-root | **Error**: "data source is
required" | _covered by #1_ |
| 4 | 0 | 0 | 1 | 1 | — | Non-root | **Error**: "data source is
required" | _covered by #1_ |
| 5 | 0 | 1 | 0 | 0 | — | Non-root | **Error**: "No entities found" |
`TestNonRootWithDataSourceAndNoEntitiesProducesError` |
| 6a | 0 | 1 | 0 | 1 | 0 | Non-root | **Error**: "No entities found" |
`TestNonRootWithDataSourceAndAutoentitiesResolvingZeroProducesError` |
| 6b | 0 | 1 | 0 | 1 | >0 | Non-root | **Valid** |
`TestNonRootWithDataSourceAndAutoentitiesResolvingEntitiesIsValid` |
| 7 | 0 | 1 | 1 | 0 | — | Non-root | **Valid** |
`TestNonRootWithDataSourceAndEntitiesIsValid` |
| 8a | 0 | 1 | 1 | 1 | 0 | Non-root | **Valid** + **Warn** |
`TestNonRootWithEntitiesAndAutoentitiesResolvingZeroLogsWarning` |
| 8b | 0 | 1 | 1 | 1 | >0 | Non-root | **Valid** | _covered by #7 / #6b
combined_ |
| 9 | 1 | 0 | 0 | 0 | — | Root | **Valid** (children carry the load) |
`TestRootWithNoDataSourceAndNoEntitiesIsValid`,
`TestRootConfigWithNoDataSourceAndNoEntitiesParses` |
| 10 | 1 | 0 | 0 | 1 | — | Root | **Error**: "must not define entities
or autoentities" |
`TestRootWithNoDataSourceButAutoentitiesProducesError` |
| 11 | 1 | 0 | 1 | 0 | — | Root | **Error**: "must not define entities"
| `TestRootWithNoDataSourceButEntitiesProducesError` |
| 12 | 1 | 0 | 1 | 1 | — | Root | **Error** | _covered by #11_ |
| 13 | 1 | 1 | 0 | 0 | — | Root (with own DS) | **Error**: "No entities
found" | `TestRootWithDataSourceAndNoEntitiesProducesError` |
| 14a | 1 | 1 | 0 | 1 | 0 | Root (with own DS) | **Error**: "No entities
found" |
`TestRootWithDataSourceAndAutoentitiesResolvingZeroProducesError` |
| 14b | 1 | 1 | 0 | 1 | >0 | Root (with own DS) | **Valid** |
`TestRootWithDataSourceAndAutoentitiesResolvingEntitiesIsValid` |
| 15 | 1 | 1 | 1 | 0 | — | Root (with own DS) | **Valid** |
`TestRootWithDataSourceAndEntitiesIsValid` |
| 16a | 1 | 1 | 1 | 1 | 0 | Root (with own DS) | **Valid** + **Warn** |
`TestRootWithEntitiesAndAutoentitiesResolvingZeroLogsWarning` |
| 16b | 1 | 1 | 1 | 1 | >0 | Root (with own DS) | **Valid** | _covered
by Azure#15 / #14b combined_ |

### Truth table — child config (validated when iterating
`root.ChildConfigs`)

Children are always treated as non-root regardless of their own
`data-source-files`.

| # | DS | E | AE | AE resolved | Expected | Test |
|---|:--:|:-:|:--:|:-----------:|----------|------|
| C1 | 0 | 0 | 0 | — | **Error** naming the child file: "data source is
required" | `TestChildWithNoDataSourceProducesNamedError` |
| C2 | 0 | * | * | — | **Error** naming the child file: "data source is
required" | _covered by C1_ |
| C3 | 1 | 0 | 0 | — | **Error** naming the child file: "No entities
found" | `TestChildWithDataSourceAndNoEntitiesProducesNamedError` |
| C4a | 1 | 0 | 1 | 0 | **Error** naming the child file: "No entities
found" |
`TestChildWithDataSourceAndAutoentitiesResolvingZeroProducesNamedError`
|
| C4b | 1 | 0 | 1 | >0 | **Valid** | _covered by C5 (resolved entities
behave the same as manual entities)_ |
| C5 | 1 | 1 | 0 | — | **Valid** | _implicitly via
`TestRootWithDataSourceAndEntitiesIsValid` setup_ |
| C6a | 1 | 1 | 1 | 0 | **Valid** + **Warn** naming the child file |
`TestChildWithEntitiesAndAutoentitiesResolvingZeroLogsNamedWarning` |
| C6b | 1 | 1 | 1 | >0 | **Valid** | _covered by C5_ |

### Other scenarios

| Scenario | Expected | Test |
|----------|----------|------|
| Connection-string error gates entity validation (no entity error fires
when DB unreachable) | `IsConfigValid == false` due to connection error
only | `TestValidateNonRootZeroEntitiesWithInvalidConnectionString` |
| Config with no entities parses cleanly (constructor no longer throws)
and `IsConfigValid` returns false without throwing | parse OK, validate
fails | `TestValidateConfigWithNoEntitiesProducesCleanError`
_(modified)_ |
| Root parses successfully without a data source | parse OK,
`IsRootConfig == true` |
`TestRootConfigWithNoDataSourceAndNoEntitiesParses` |
| Non-root with DS and no entities parses successfully | parse OK,
`IsRootConfig == false` |
`TestNonRootConfigWithDataSourceAndNoEntitiesParses` |
| Autoentities present but resolve to nothing — must not crash, must not
double-message with "No entities found" | no crash; only "No entities
found" if total = 0 | `ValidateAutoentitiesConfiguration` _(modified to
`isValidateOnly: true`)_ |





New tests:

`TestRootConfigWithNoDataSourceAndNoEntitiesParses` Root config (has
data-source-files) without datasource parses OK
`TestNonRootConfigWithDataSourceAndNoEntitiesParses` Non-root config
with datasource + no entities parses OK (validation catches it later)
`TestNonRootWithDataSourceAndNoEntitiesProducesError` Calls
ValidateDataSourceAndEntityPresence directly, error recorded
`TestNonRootWithNoDataSourceProducesError` No datasource, error with
"data source is required"
`TestNonRootWithDataSourceAndEntitiesIsValid` Datasource + entities, no
errors
`TestRootWithNoDataSourceAndNoEntitiesIsValid` Root with child, no own
datasource, valid
`TestRootWithNoDataSourceButEntitiesProducesError` Root with entities
but no datasource, error
`TestRootWithDataSourceAndEntitiesIsValid` Root with own datasource +
entities, valid
`TestChildWithDataSourceAndNoEntitiesProducesNamedError` Child with no
entities, error names the child file
`TestChildWithNoDataSourceProducesNamedError` Child with no datasource,
error names the child file
`TestNonRootWithDataSourceAndAutoentitiesResolvingZeroProducesError`
Non-root with only autoentities that resolve to 0
`TestNonRootWithDataSourceAndAutoentitiesResolvingEntitiesIsValid`
Non-root with only autoentities resolving > 0 entities
`TestNonRootWithEntitiesAndAutoentitiesResolvingZeroLogsWarning`
Non-root with manual entities + autoentities resolving 0
`TestRootWithNoDataSourceButAutoentitiesProducesError` Root with no
datasource but autoentities defined
`TestRootWithDataSourceAndNoEntitiesProducesError` Root with own
datasource and zero entities/autoentities
`TestRootWithDataSourceAndAutoentitiesResolvingZeroProducesError` Root
with own datasource and autoentities resolving 0
`TestRootWithDataSourceAndAutoentitiesResolvingEntitiesIsValid` Root
with own datasource and autoentities resolving > 0
`TestRootWithEntitiesAndAutoentitiesResolvingZeroLogsWarning` Root with
own datasource, manual entities, and autoentities resolving 0
`TestChildWithDataSourceAndAutoentitiesResolvingZeroProducesNamedError`
Child with autoentities-only resolving 0
`TestChildWithEntitiesAndAutoentitiesResolvingZeroLogsNamedWarning`
Child with manual entities + autoentities resolving 0

Modified tests:

`TestValidateConfigWithNoEntitiesProducesCleanError` Replaced main's
version (expected parse failure) with ours: parse succeeds,
IsConfigValid returns false
`ValidateAutoentitiesConfiguration` Changed to isValidateOnly: true,
asserts no crashes instead of zero errors

---------

Co-authored-by: Anusha Kolan <anushakolan10@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants