SqlKata.JsonQuery 0.2.0

dotnet add package SqlKata.JsonQuery --version 0.2.0
                    
NuGet\Install-Package SqlKata.JsonQuery -Version 0.2.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="SqlKata.JsonQuery" Version="0.2.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="SqlKata.JsonQuery" Version="0.2.0" />
                    
Directory.Packages.props
<PackageReference Include="SqlKata.JsonQuery" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add SqlKata.JsonQuery --version 0.2.0
                    
#r "nuget: SqlKata.JsonQuery, 0.2.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package SqlKata.JsonQuery@0.2.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=SqlKata.JsonQuery&version=0.2.0
                    
Install as a Cake Addin
#tool nuget:?package=SqlKata.JsonQuery&version=0.2.0
                    
Install as a Cake Tool

SqlKata.JsonQuery

SqlKata.JsonQuery turns a constrained JSON SELECT message into a validated SqlKata.Query over one backend-owned table or view. It is intended for AI tools, HTTP APIs, queues, and other boundaries where a caller needs flexible data retrieval without being allowed to send SQL.

The library constructs queries. It does not open connections, execute commands, authorize users, discover database schema, or expose joins.

SqlKata.JsonQuery is an independent community package. It is not affiliated with, maintained by, or endorsed by the SqlKata project.

Why It Exists

SqlKata composes values through non-raw clauses, but a remote caller can still be dangerous if it controls relation names, column names, operators, raw expressions, or joins. This library closes that gap:

  • The backend chooses exactly one table or view.
  • The backend declares the allowed columns, physical types, and operations.
  • The caller sends only the closed JSON query vocabulary.
  • Runtime validation rejects everything outside that contract.
  • Accepted values remain compiler-controlled. Most become bindings; a provider may emit a safe literal representation, such as a SQL Server boolean cast.

Views are the intended contract for joined or derived data. If a use case needs joins, put the joins behind a view and query that view as one relation.

Causal Chain

flowchart LR
    A["Table or view contract"] -->|"fixes one relation"| B["Trusted SqlKata seed"]
    C["QueryColumns"] -->|"constrains fields and operations"| D["Runtime validation"]
    C -->|"generates"| E["Relation-specific JSON Schema"]
    E -->|"guides"| F["AI or remote caller"]
    F -->|"emits JSON only"| D
    B -->|"preserves backend predicates"| G["WithJsonQuery"]
    D -->|"permits validated request"| G
    G -->|"returns cloned Query"| H["SqlKata compiler"]
    H -->|"produces SQL and compiler data"| I["Consumer-owned execution"]
    J["Read-only database identity"] -->|"authorizes actual access"| I

Each step removes one class of ambiguity. The model-facing schema improves generation, but WithJsonQuery remains authoritative because callers can ignore or misunderstand a schema.

Installation

Until the first package release, reference the project from source:

<ProjectReference Include="path/to/SqlKata.JsonQuery/SqlKata.JsonQuery.csproj" />

After package publication, the intended package command is:

dotnet add package SqlKata.JsonQuery

Execution libraries such as Dapper and database providers are consumer dependencies, not dependencies of SqlKata.JsonQuery.

Quick Start

1. Define the backend column contract

using SqlKata.JsonQuery;

var columns = QueryColumns.CreateBuilder()
    .Text("account_code", QueryCapability.Select)
    .Date("order_date", QueryCapability.Select | QueryCapability.Sort)
    .Text("order_id")
    .Text(
        "product_name",
        QueryCapability.Select | QueryCapability.Filter | QueryCapability.Sort)
    .Text(
        "region",
        QueryCapability.Select | QueryCapability.Filter |
        QueryCapability.Group | QueryCapability.Sort)
    .Number("order_total")
    .Number(
        "quantity",
        QueryCapability.Select | QueryCapability.Filter |
        QueryCapability.Sort | QueryCapability.Aggregate)
    .Bool("is_active", QueryCapability.Select | QueryCapability.Filter)
    .Build();

The one-argument column methods default to QueryCapability.All. Use explicit capabilities for backend-owned fields. In this example, JSON may select but cannot filter account_code or order_date; trusted tool parameters own those predicates.

Configured names are database identifiers and are emitted verbatim. This is not an ORM mapping layer.

2. Generate model-facing guidance

using System.Text.Json;

JsonElement schema = JsonQuerySchema.Create(columns);
string schemaJson = JsonSerializer.Serialize(
    schema,
    new JsonSerializerOptions { WriteIndented = true });

Expose this relation-specific Draft 2020-12 schema to the model or client. It contains only the columns and operations permitted by columns.

3. Accept a JSON query

{
  "select": ["order_id", "product_name", "region", "order_total", "quantity"],
  "filters": [
    { "col": "order_total", "op": "gt", "value": 1000 },
    {
      "or": [
        { "col": "region", "op": "eq", "value": "EMEA" },
        { "col": "quantity", "op": "gte", "value": 100 }
      ]
    }
  ],
  "order_by": [{ "col": "order_total", "dir": "desc" }]
}

The root filters array is implicit AND. Explicit and and or groups may recurse.

4. Apply it to a trusted seed

using SqlKata;
using SqlKata.JsonQuery;

string[] accounts = ["A100", "A200"];
var effectiveDate = new DateTime(2025, 3, 31);

var seed = new Query("reporting.vOrderSummary")
    .WhereIn("account_code", accounts)
    .Where("order_date", effectiveDate);

Query query = seed.WithJsonQuery(json, columns);

WithJsonQuery validates the JSON and seed, clones the seed, and applies the validated clauses to the clone. Caller OR groups remain beneath the backend's top-level AND scope.

5. Compile and execute outside the library

using Dapper;
using Microsoft.Data.SqlClient;
using SqlKata.Compilers;

var compiler = new SqlServerCompiler { UseLegacyPagination = true };
var compiled = compiler.Compile(query);

await using var connection = new SqlConnection(connectionString);
var rows = await connection.QueryAsync(compiled.Sql, compiled.NamedBindings);

SqlKata.JsonQuery has no Dapper or SQL Server dependency. The example shows one possible consumer.

The library is developed and verified against SQL Server. Other SqlKata compilers are expected to work but are not yet covered by tests.

Common Query Shapes

Group and aggregate

{
  "group_by": ["region"],
  "aggregates": [
    { "fn": "sum", "col": "order_total" },
    { "fn": "count", "col": "order_id" }
  ],
  "order_by": [{ "col": "sum_order_total", "dir": "desc" }]
}

Aggregate aliases are deterministic: {function}_{canonicalColumn}. For the configured lowercase names above, the aliases are sum_order_total and count_order_id.

Inclusive date range

When the tool owns the date range, keep it outside JSON:

var seed = new Query("reporting.vOrderSummary")
    .WhereIn("account_code", accounts)
    .WhereBetween("order_date", startDate, endDate);

var query = seed.WithJsonQuery(json, columns);

As-of snapshot

An as-of request is two operations. Resolve the latest available date within trusted scope, then query that exact date:

var resolver = new Query("reporting.vOrderSummary")
    .WhereIn("account_code", accounts)
    .Where("order_date", "<=", requestedAsOf)
    .AsMax("order_date");

// Compile and execute resolver in the consumer to obtain effectiveDate.

var detail = new Query("reporting.vOrderSummary")
    .WhereIn("account_code", accounts)
    .Where("order_date", effectiveDate)
    .WithJsonQuery(json, columns);

Do not use date <= asOf plus a detail limit as a snapshot. That mixes dates and can discard rows from the desired snapshot.

For multiple accounts or other independently scheduled partitions, define whether the tool promises one common snapshot date or the latest date per partition. Per-partition snapping requires separate queries or a view that owns that relational logic.

Dataset-size guard

var countQuery = query.ToDatasetCountQuery();
var countCommand = compiler.Compile(countQuery);
var actualSize = await connection.ExecuteScalarAsync<long>(
    countCommand.Sql,
    countCommand.NamedBindings);

if (actualSize > maxDatasetSize)
{
    throw new InvalidOperationException(
        $"Dataset contains {actualSize} rows; maximum is {maxDatasetSize}.");
}

The maximum, exception type, and execution policy belong to the consumer. The extension only constructs a count over the exact runnable dataset. A query limit, if present, remains part of that dataset.

Decision Guide

Trigger Correct action Why
The model needs allowed fields Generate JsonQuerySchema.Create(columns) The schema comes from the runtime column contract.
Account, tenant, or date is a typed tool argument Remove Filter for that column and add a seed predicate Caller JSON cannot weaken backend scope.
The query needs joins Create a view JSON always targets one relation.
The request asks for an as-of snapshot Resolve the effective date, then seed exact equality A limit cannot identify a complete snapshot.
The result may be too large Count the runnable query in the consumer Size policy is execution policy, not JSON grammar.
The model asks for unsupported syntax Return JsonQueryException.Code and Path The model gets the best available repair location; $ means rebuild the request.
Database authorization matters Use a least-privilege, read-only identity Query grammar is not an authorization boundary.

Security Boundary

The library provides a constrained construction boundary, not complete database security.

It enforces:

  • one backend-owned table or view;
  • backend-owned column names, types, and capabilities;
  • a strict SELECT-only JSON vocabulary;
  • no caller raw SQL, aliases, joins, subqueries, relation names, or expressions;
  • non-raw SqlKata composition and compiler-controlled values;
  • strict unknown-property, duplicate-property, value-shape, and clause validation;
  • safe-by-default structural limits on payload shape.

Structural limits are enforced by default since 0.2.0: WithJsonQuery applies JsonQueryLimits.Default, bounding payload length, filter-group nesting depth, total filter conditions, and in-list size, with an optional ceiling for limit. Violations reject with the teaching code StructuralLimitExceeded, the offending path, and an actual-versus-allowed detail. Pass a custom JsonQueryLimits to tune the bounds — the generated schema surfaces the active values to the model — or JsonQueryLimits.None to restore the previous unlimited behavior for trusted callers.

The returned SqlKata.Query is mutable. After WithJsonQuery returns, do not append caller-controlled raw SQL, identifiers, expressions, or other unvalidated clauses.

Consumers must still provide:

  • authentication and authorization;
  • trusted mandatory predicates;
  • least-privilege database permissions;
  • query timeout, cancellation, and execution policy;
  • optional dataset-size enforcement;
  • transport-specific error and result shaping.

Documentation

The full architectural decision history is maintained outside this repository.

Build and Test

dotnet build SqlKata.JsonQuery.sln -c Release
dotnet test SqlKata.JsonQuery.sln -c Release

Portable SQL Server smoke tests are opt-in through SQLKATA_JSONQUERY_LIVE_CONNECTION. The Sharadar reference-use-case tests have a separate, explicit SQLKATA_JSONQUERY_SHARADAR_CONNECTION opt-in because they require private views and pinned reference data. Set both variables to the same Sharadar connection to run the complete live suite. Ordinary builds require neither database nor environment variable.

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.2.0 115 7/20/2026
0.1.0 103 7/19/2026

BREAKING: structural limits are now enforced by default — WithJsonQuery(json, columns) applies JsonQueryLimits.Default (payload length 65536, filter depth 8, 50 filter conditions, 100 in-list values); pass JsonQueryLimits.None to restore prior unlimited behavior, or a custom JsonQueryLimits to tune the bounds. New JsonElement/JsonDocument WithJsonQuery overloads accept already-parsed input (MaxJsonLength does not apply there; documents are never disposed by the library). New JsonQueryErrorCode.StructuralLimitExceeded with a typed Detail property — exhaustive switches over JsonQueryErrorCode need a new arm. JsonQuerySchema.Create(columns, limits) surfaces the active bounds to callers and models. Passing a literal null string to WithJsonQuery is now ambiguous between overloads (CS0121) — cast to string to resolve; real string variables are unaffected.