PPDS.Query
March 2, 2026 · View on GitHub
Production-grade SQL query engine for Dataverse with FetchXML transpilation and ADO.NET provider.
Installation
dotnet add package PPDS.Query
Quick Start - ADO.NET Provider
using PPDS.Query.Provider;
await using var connection = new PpdsDbConnection(connectionPool);
await connection.OpenAsync();
await using var command = connection.CreateCommand();
command.CommandText = @"
SELECT a.name, a.revenue, c.fullname
FROM account a
INNER JOIN contact c ON c.parentcustomerid = a.accountid
WHERE a.statecode = @statecode
ORDER BY a.revenue DESC";
command.Parameters.AddWithValue("@statecode", 0);
await using var reader = await command.ExecuteReaderAsync();
while (await reader.ReadAsync())
{
var name = reader.GetString(0);
var revenue = reader.IsDBNull(1) ? 0m : reader.GetDecimal(1);
var contact = reader.GetString(2);
Console.WriteLine($"{name} ({revenue:C}) - {contact}");
}
Quick Start - CLI
# Simple query
ppds query sql "SELECT name, revenue FROM account WHERE statecode = 0 ORDER BY revenue DESC"
# Join across entities
ppds query sql "SELECT a.name, c.fullname FROM account a JOIN contact c ON c.parentcustomerid = a.accountid"
# Aggregate query
ppds query sql "SELECT ownerid, COUNT(*) as total FROM account GROUP BY ownerid HAVING COUNT(*) > 10"
# Output as CSV
ppds query sql "SELECT name, emailaddress1 FROM contact" -f csv
# View the execution plan
ppds query explain "SELECT name FROM account WHERE revenue > 1000000"
Features
SQL Language Support
Full T-SQL syntax parsed via Microsoft.SqlServer.TransactSql.ScriptDom:
- SELECT with columns, aliases, TOP, DISTINCT, computed expressions
- WHERE with all comparison operators, LIKE, IN, BETWEEN, IS NULL, IS NOT DISTINCT FROM
- JOIN — INNER, LEFT, RIGHT, FULL OUTER, CROSS, OUTER APPLY
- GROUP BY with aggregates: COUNT, SUM, AVG, MIN, MAX, STDEV, STDEVP, VAR, VARP
- HAVING clause with aggregate predicates
- ORDER BY with ASC/DESC
- UNION / UNION ALL
- Subqueries — IN, NOT IN, EXISTS, NOT EXISTS, derived tables, scalar subqueries
- Window functions — ROW_NUMBER, RANK, DENSE_RANK, CUME_DIST, PERCENT_RANK
- CTEs — Common Table Expressions including recursive CTEs
- CASE/WHEN/THEN/ELSE and IIF expressions
- Built-in functions — ISNULL, COALESCE, NULLIF, CAST, string functions, date functions (DATEADD, DATEDIFF, DATEPART, YEAR, MONTH, DAY, GETDATE, GETUTCDATE, TIMEFROMPARTS)
- JSON — OPENJSON table-valued function, JSON_MODIFY with array paths
- Scripting — DECLARE/SET variables, IF/ELSE, WHILE/BREAK/CONTINUE, SELECT INTO #temp
Execution Engine
The query engine uses a Volcano iterator model for streaming results:
SQL Text
-> ScriptDom Parser -> SQL AST
-> Query Planner -> Execution Plan
-> FetchXML Transpiler -> FetchXML (pushed down to Dataverse)
-> Volcano Iterators -> Streaming rows
- FetchXML pushdown — Filters, joins, and aggregates are pushed to Dataverse when possible
- Client-side fallback — Hash, merge, and nested-loop joins for queries that exceed FetchXML capabilities
- Prefetch buffering — Page-ahead scan node for improved streaming throughput
- TDS Endpoint routing — Automatically routes compatible queries to the SQL endpoint
- Parallel partitioned aggregates — Accurate COUNT(*) beyond the Dataverse 50K limit via date-range partitioning
- Adaptive retry — Binary date-range splitting when partitions exceed limits
- EXPLAIN — Inspect query execution plans with pool and parallelism metadata
Cross-Environment Queries
Query across Dataverse environments using bracket syntax:
-- Compare account counts across environments
SELECT 'Dev' as env, COUNT(*) as total FROM [dev-org].[account]
UNION ALL
SELECT 'Prod' as env, COUNT(*) as total FROM [prod-org].[account]
- Bracket syntax —
[environment].[entity]for cross-environment references - Smart label detection — Resolves 2-part names against configured environment labels
- Remote execution — RemoteScanNode executes FetchXML on remote environments
DML Support
Data modification with safety guards:
-- Insert with values
INSERT INTO account (name, revenue) VALUES ('Contoso', 1000000)
-- Insert from query
INSERT INTO account (name, revenue)
SELECT name, revenue FROM #staged_accounts
-- Update with filter
UPDATE contact SET jobtitle = 'Senior Developer' WHERE jobtitle = 'Developer'
-- Delete with filter
DELETE FROM account WHERE statecode = 1 AND modifiedon < '2024-01-01'
Safety Features
All DML operations are protected by configurable safety guards:
- Environment protection levels — Prevent accidental writes to production environments
- Row caps — Configurable maximum rows affected per DML statement
- Structured error codes — QueryExecutionException with error codes for programmatic handling
- @@ERROR / ERROR_MESSAGE() — Error state tracking within script sessions
Target Frameworks
net8.0net9.0net10.0
License
MIT License