Files
sql-utilities/src/Strata.SqlTools.PostgreSql

Strata.SqlTools.PostgreSQL

A PostgreSQL dialect-specific implementation of the QueryBreakdown SQL parsing and generation framework. This project extends the core SQL Tools functionality with PostgreSQL-native syntax support, including positional parameters ($1, $2, etc.), double-quoted identifiers, LIMIT/OFFSET clauses, and RETURNING clauses.

Overview

Strata.SqlTools.PostgreSQL extends the SQL Tools framework to provide PostgreSQL-specific functionality while maintaining compatibility with the core QueryBreakdown patterns used throughout the sql-utilities ecosystem. It's built on top of the SqlServer implementation and follows the same architectural patterns as the Snowflake dialect module.

Features

  • Positional Parameters: Native support for PostgreSQL positional parameters ($1, $2, ..., $N)
  • Double-Quoted Identifiers: Case-sensitive identifier handling using PostgreSQL's double-quote syntax
  • LIMIT and OFFSET: Full support for PostgreSQL's LIMIT/OFFSET pagination syntax
  • RETURNING Clause: DML statement result retrieval via RETURNING
  • CTE Support: Common Table Expressions (WITH clause) for recursive and non-recursive queries
  • Parameter Normalization: Automatic conversion of @name and :name parameter styles to positional format
  • Batch Operations: Multi-statement batch processing with semicolon separation

Installation

Add the package to your project:

dotnet add package Strata.SqlTools.PostgreSQL

Or via NuGet Package Manager:

Install-Package Strata.SqlTools.PostgreSQL

Quick Start

Basic Query Parsing

using Strata.SqlTools.Breakdowns.PostgreSql;

// Parse an existing PostgreSQL query
var sql = "SELECT id, name FROM users WHERE status = $1 ORDER BY name DESC LIMIT 10";
var queryBreakdown = QueryBreakdown.Parse(sql, isMicrosoftSql: false);

// Access individual clauses
Console.WriteLine($"Select: {queryBreakdown.SelectClause.Clause}");
Console.WriteLine($"From: {queryBreakdown.FromClause.Clause}");
Console.WriteLine($"Where: {queryBreakdown.WhereClause.Clause}");
Console.WriteLine($"Limit: {queryBreakdown.LimitClause.Clause}");

Building Queries Programmatically

var query = new QueryBreakdown("id, name, email", "users");
query.WhereClause.Clause = "status = $1 AND created_at > $2";
query.OrderByClause.Clause = "created_at DESC";
query.LimitClause.Clause = "50";
query.OffsetClause.Clause = "0";

// Add parameters by name (automatically converted to positional $1, $2, etc.)
query.AddParameter("status", "active");
query.AddParameter("startDate", new DateTime(2025, 1, 1));

// Generate PostgreSQL SQL
var generatedSql = query.GetSql();
Console.WriteLine(generatedSql);

Working with CTEs (Common Table Expressions)

// Create main query
var mainQuery = new QueryBreakdown("*", "recent_users");

// Create CTE
var cteQuery = new QueryBreakdown(
    "id, name, created_at",
    "users"
);
cteQuery.WhereClause.Clause = "created_at > NOW() - INTERVAL '30 days'";
cteQuery.OrderByClause.Clause = "created_at DESC";

// Add CTE to main query
mainQuery.AddWithClause("recent_users", cteQuery);

// Generate SQL
var sql = mainQuery.GetSql();

Batch Statement Processing

var collection = new QueryBreakdownCollection();

// Add multiple queries to batch
var query1 = new QueryBreakdown("id, name", "users");
query1.WhereClause.Clause = "active = true";
collection.Add(query1);

var query2 = new QueryBreakdown("id, amount", "orders");
query2.OrderByClause.Clause = "created_at DESC";
query2.LimitClause.Clause = "100";
collection.Add(query2);

// Generate batch SQL with semicolon separation
var batchSql = collection.GetPostgreSqlBatch();
// Result: "SELECT \"id\", \"name\" FROM \"users\" WHERE active = true; SELECT \"id\", \"amount\" FROM \"orders\" ORDER BY created_at DESC LIMIT 100;"

Parameter Handling

PostgreSQL uses positional parameters ($1, $2, etc.) instead of named parameters. The PostgreSQL dialect automatically converts named parameters to positional format:

var query = new QueryBreakdown("id, name", "users");

// Add parameters by name
query.AddParameter("userId", 123);
query.AddParameter("status", "active");

// Parameters are tracked internally with both formats
// For compatibility: query.Parameters["$1"] exists for execution
// For readability: query.Parameters["@userId"] existed during construction

Identifiers and Case Sensitivity

PostgreSQL treats unquoted identifiers as case-insensitive (converts to lowercase), but double-quoted identifiers are case-sensitive:

// Unquoted - case insensitive
var query1 = new QueryBreakdown("ID, NAME", "USERS");
// Results in: SELECT "id", "name" FROM "users"

// Double-quoted - case sensitive
var query2 = new QueryBreakdown("\"UserId\", \"UserName\"", "\"UserTable\"");
// Results in: SELECT "UserId", "UserName" FROM "UserTable"

LIMIT and OFFSET

Use LIMIT for row count restrictions and OFFSET for pagination:

var query = new QueryBreakdown("id, name", "users");
query.OrderByClause.Clause = "id ASC";
query.LimitClause.Clause = "25";
query.OffsetClause.Clause = "100";

var sql = query.GetSql();
// Results in: SELECT "id", "name" FROM "users" ORDER BY "id" ASC LIMIT 25 OFFSET 100

RETURNING Clause

Use RETURNING with DML statements (INSERT, UPDATE, DELETE) to retrieve affected rows:

var query = new QueryBreakdown("id", "users");
query.ReturningClause.Clause = "id, name, email";

// Note: RETURNING is context-specific and works with INSERT/UPDATE/DELETE constructs

Identifiers with Special Characters

PostgreSQL requires double-quoting for identifiers with spaces or special characters:

var query = new QueryBreakdown("\"Order ID\", \"Customer Name\"", "\"Sales Data\"");
query.WhereClause.Clause = "\"Order Status\" = $1";

var sql = query.GetSql();
// Results in: SELECT "Order ID", "Customer Name" FROM "Sales Data" WHERE "Order Status" = $1

Architecture

The PostgreSQL implementation follows the same architecture as other SQL Tools dialect modules:

  • QueryBreakdown: Main class for parsing and generating PostgreSQL SQL
  • QueryBreakdownCollection: Batch processing for multiple queries
  • CommandVisitor: Converts SQL expressions to PostgreSQL-specific strings
  • StatementParser: PostgreSQL-specific SQL parsing logic
  • StatementExpressionParser: Expression-level parsing
  • StatementReader: Token-level SQL reading with PostgreSQL syntax rules
  • ExpressionFactory: Abstract factory for building filter expressions

Conversion from Other Dialects

When migrating from SQL Server (@parameter syntax) to PostgreSQL ($N syntax):

// SQL Server style
var sqlServerQueryBreakdown = QueryBreakdown.Parse(
    "SELECT id FROM users WHERE status = @status",
    isMicrosoftSql: true
);

// PostgreSQL automatically normalizes to positional parameters
var postgreSqlQuery = QueryBreakdown.Parse(
    "SELECT id FROM users WHERE status = $1",
    isMicrosoftSql: false
);

Testing

The project includes comprehensive test coverage:

  • QueryBreakdownTests: Core parsing and SQL generation
  • QueryBreakdownCollectionTests: Batch processing functionality
  • StatementReaderTests: Token-level parsing
  • StatementExpressionParserTests: Expression parsing

Run tests with:

dotnet test Strata.SqlTools.PostgreSql.Tests

Dependencies

  • .NET 8.0 or later: Required for async/await and modern C# features
  • Strata.SqlTools (Core): Base SQL Tools framework
  • Strata.SqlTools.SqlServer: Base dialect implementation inheritance

Compatibility

  • PostgreSQL 10.0 and later
  • Supports all standard SQL and PostgreSQL-specific syntax
  • Compatible with Entity Framework Core 8.0+ for data access integration

Performance Considerations

  • Statement parsing is optimized for typical query sizes
  • Parameter tracking uses Dictionary<string, object> for O(1) lookups
  • Batch operations use StringBuilder for efficient string concatenation
  • Expression parsing uses lazy evaluation where possible

Known Limitations

  • Recursive CTEs require explicit RECURSIVE keyword (must be added manually or via clause)
  • Custom PostgreSQL types (@type syntax) are not explicitly handled
  • Window functions with OVER clause may require manual formatting
  • Schema-qualified table names (schema.table) are treated as single identifiers

Contributing

Contributions are welcome! Please ensure:

  • All tests pass
  • Code follows the existing architectural patterns
  • New features include corresponding test cases
  • Documentation is updated

License

See LICENSE.txt in the repository root.

See Also