Modules
Querier is a multi-module library published under a single
version. Pull in only what you need.
| Artifact |
Description |
Required |
| querier-core |
Type-safe SQL query builder with core features |
Always |
| querier-ui |
Server-side dashboard rendering with Chart.js |
Optional |
| querier-ai |
Natural-language SQL generation and AI-powered dashboard
creation
|
Optional |
querier-ui declares querier-core as a transitive dependency.
querier-ai declares querier-core as a required dependency and
querier-ui as an optional one for AI dashboard generation.
Installation
Core only (querier-core) - Maven
pom.xml
<dependency>
<groupId>com.aytmatech</groupId>
<artifactId>querier-core</artifactId>
<version>QUERIER_VERSION</version>
</dependency>
Core only (querier-core) - Gradle
build.gradle
implementation 'com.aytmatech:querier-core:QUERIER_VERSION'
Core + Dashboards UI (querier-ui) - Maven
pom.xml
<dependency>
<groupId>com.aytmatech</groupId>
<artifactId>querier-ui</artifactId>
<version>QUERIER_VERSION</version>
</dependency>
Core + Dashboards UI (querier-ui) - Gradle
build.gradle
implementation 'com.aytmatech:querier-ui:QUERIER_VERSION'
Core + AI (querier-ai) - Maven
pom.xml
<dependency>
<groupId>com.aytmatech</groupId>
<artifactId>querier-ai</artifactId>
<version>QUERIER_VERSION</version>
</dependency>
Core + AI (querier-ai) - Gradle
build.gradle
implementation 'com.aytmatech:querier-ai:QUERIER_VERSION'
Quick Start
1. Define entity classes
Option 1: Custom annotations
Order.java
import com.aytmatech.querier.annotation.Table;
import com.aytmatech.querier.annotation.Column;
@Table("orders")
public class Order {
private Long id;
private Long customerId;
private BigDecimal total;
private OrderStatus status;
public Long getId() {
return id;
}
@Column("customer_id")
public Long getCustomerId() {
return customerId;
}
public BigDecimal getTotal() {
return total;
}
public OrderStatus getStatus() {
return status;
}
}
Option 2: JPA annotations
Order.java with JPA
import jakarta.persistence.Table;
import jakarta.persistence.Column;
@Table(name = "orders")
public class Order {
private Long id;
private Long customerId;
private BigDecimal total;
private OrderStatus status;
public Long getId() { return id; }
@Column(name = "customer_id")
public Long getCustomerId() { return customerId; }
public BigDecimal getTotal() { return total; }
public OrderStatus getStatus() { return status; }
}
Custom annotations take precedence over JPA annotations. Without
annotations, Querier falls back to snake_case conversion.
2. Build a query
Select Builder
Select select = Select.builder()
.select(Order::getId).select(Order::getTotal)
.from(Order.class)
.where(Condition.eq(Order::getStatus, OrderStatus.PAID))
.build();
Select.SqlAndParams sp = select.toSqlAndParams();
// sp.sql() -> "SELECT orders.id, orders.total FROM orders WHERE orders.status = :param0"
// sp.params() -> {param0: "PAID"}
3. Execute the query
Using EntityManager
EntityManager em = ...;
Select.SqlAndParams sp = select.toSqlAndParams();
Query nativeQuery = em.createNativeQuery(sp.sql(), Tuple.class);
sp.params().forEach(nativeQuery::setParameter);
List<?> rows = nativeQuery.getResultList();
Framework Compatibility
| Framework / API |
Method |
Placeholder Style |
Output |
| Spring NamedParameterJdbcTemplate |
toSqlAndParams() |
:paramName |
SqlAndParams(sql, Map) |
| Spring JdbcTemplate |
toPositionalSql() |
? |
PositionalSqlAndParams(sql, List) |
| Plain JDBC PreparedStatement |
toPositionalSql() |
? |
PositionalSqlAndParams(sql, List) |
| Hibernate native query |
toSqlAndParams() |
:paramName |
SqlAndParams(sql, Map) |
| jOOQ |
toPositionalSql() |
? |
PositionalSqlAndParams(sql, List) |
| R2DBC DatabaseClient |
toSqlAndParams() |
:paramName |
SqlAndParams(sql, Map) |
| Vert.x SQL Client |
toIndexedSql() |
$1, $2, ... |
IndexedSqlAndParams(sql, List) |
| Logging / Debugging |
toPlainSql() |
Values inlined |
String |
Usage examples by framework
Spring NamedParameterJdbcTemplate
Select.SqlAndParams sp = select.toSqlAndParams();
List<Map<String,Object>> rows = namedJdbc.queryForList(sp.sql(), sp.params());
Spring JdbcTemplate
Select.PositionalSqlAndParams sp = select.toPositionalSql();
List<Map<String,Object>> rows = jdbc.queryForList(sp.sql(), sp.params().toArray());
Plain JDBC PreparedStatement
Select.PositionalSqlAndParams sp = select.toPositionalSql();
PreparedStatement ps = connection.prepareStatement(sp.sql());
for (int i = 0; i < sp.params().size(); i++) {
ps.setObject(i + 1, sp.params().get(i));
}
ResultSet rs = ps.executeQuery();
jOOQ
Select.PositionalSqlAndParams sp = select.toPositionalSql();
Result<Record> result = dsl.resultQuery(sp.sql(), sp.params().toArray()).fetch();
Hibernate Native Query
Select.SqlAndParams sp = select.toSqlAndParams();
Query query = session.createNativeQuery(sp.sql(), Tuple.class);
sp.params().forEach(query::setParameter);
List<?> results = query.getResultList();
R2DBC DatabaseClient
Select.SqlAndParams sp = select.toSqlAndParams();
DatabaseClient.GenericExecuteSpec spec = client.sql(sp.sql());
for (var entry : sp.params().entrySet()) {
spec = spec.bind(entry.getKey(), entry.getValue());
}
Flux<Map<String,Object>> rows = spec.fetch().all();
Vert.x SQL Client
Select.IndexedSqlAndParams sp = select.toIndexedSql();
Tuple tuple = Tuple.tuple();
sp.params().forEach(tuple::addValue);
client.preparedQuery(sp.sql()).execute(tuple);
Logging / Debugging
String plainSql = select.toPlainSql();
log.debug("Executing query: {}", plainSql);
// WARNING: Not safe for execution - use for logging only
Identifier Quoting
Querier supports database-specific quoting for reserved words
and special identifiers.
QuoteStrategy options
| Database |
QuoteStrategy |
Example output |
| Default |
NONE |
orders.id |
| PostgreSQL, Oracle, SQLite |
ANSI |
"orders"."id" |
| MySQL, MariaDB |
MYSQL |
`orders`.`id` |
| SQL Server, Azure SQL |
SQL_SERVER |
[orders].[id] |
Using QuoteStrategy
PostgreSQL / ANSI
Select select = Select.builder()
.select(Order::getId)
.from(Order.class)
.quoteStrategy(QuoteStrategy.ANSI)
.build();
// SQL: SELECT "orders"."id" FROM "orders"
MySQL / MariaDB
Select select = Select.builder()
.select(Order::getId)
.from(Order.class)
.quoteStrategy(QuoteStrategy.MYSQL)
.build();
// SQL: SELECT `orders`.`id` FROM `orders`
SQL Server / Azure SQL
Select select = Select.builder()
.select(Order::getId)
.from(Order.class)
.quoteStrategy(QuoteStrategy.SQL_SERVER)
.build();
// SQL: SELECT [orders].[id] FROM [orders]
Default behavior is QuoteStrategy.NONE for backward
compatibility.
-
Quoting applies to table names, schema and catalog qualifiers,
and columns in SELECT, WHERE, JOIN, GROUP BY, ORDER BY, and
HAVING.
-
Raw expressions and CTE fragments are not auto-quoted and
should be quoted manually when needed.
Schema-qualified names example
@Table(name = "orders", schema = "sales", catalog = "mydb")
public class Order { ... }
Select select = Select.builder()
.select(Order::getId)
.from(Order.class)
.quoteStrategy(QuoteStrategy.ANSI)
.build();
// SQL: SELECT "mydb"."sales"."orders"."id" FROM "mydb"."sales"."orders"
Usage Examples
Basic SELECT
Select columns
Select select = Select.builder()
.select(Order::getId).select(Order::getTotal)
.from(Order.class)
.build();
// SQL: SELECT orders.id, orders.total FROM orders
WHERE conditions
Simple and complex conditions
Select select = Select.builder()
.select(Order::getId).select(Order::getTotal)
.from(Order.class)
.where(Condition.eq(Order::getStatus, OrderStatus.PAID))
.build();
Select complex = Select.builder()
.select(Order::getId)
.from(Order.class)
.where(Condition.and(
Condition.or(
Condition.eq(Order::getStatus, OrderStatus.PAID),
Condition.eq(Order::getStatus, OrderStatus.SHIPPED)
),
Condition.gt(Order::getTotal, new BigDecimal("50"))
))
.build();
- Comparisons: eq, ne, gt, lt, gte, lte
- Pattern matching: like
- Range: between, in
- Null checks: isNull, isNotNull
- Logical operators: and, or, not
- Subqueries: exists
JOINs
Join example
Select select = Select.builder()
.select(Order::getId).select(Customer::getName)
.from(Order.class)
.join(Customer.class, Condition.eq(Order::getCustomerId, Customer::getId))
.build();
// SQL: SELECT orders.id, customers.name FROM orders INNER JOIN customers ON orders.customer_id = customers.id
- join (INNER JOIN)
- leftJoin (LEFT JOIN)
- rightJoin (RIGHT JOIN)
- fullJoin (FULL OUTER JOIN)
- crossJoin (CROSS JOIN)
Aggregations
GROUP BY and HAVING
Select select = Select.builder()
.select(Order::getStatus).select(Aggregate.sum(Order::getTotal).as("total_revenue"))
.from(Order.class)
.groupBy(Order::getStatus)
.having(Condition.gt(Order::getTotal, new BigDecimal("1000")))
.orderBy(OrderBy.desc("SUM(orders.total)"))
.build();
Window functions
Running total
Select select = Select.builder()
.select(Order::getId)
.select(Order::getTotal)
.select(WindowFunction.of(Aggregate.sum(Order::getTotal))
.partitionBy(Order::getCustomerId)
.orderBy(OrderBy.asc(Order::getCreatedAt))
.as("running_total"))
.from(Order.class)
.build();
Subqueries
Aggregate subquery
Select subquery = Select.builder()
.select(Aggregate.avg(Order::getTotal).as("avg_total"))
.from(Order.class)
.build();
Select main = Select.builder()
.select(Order::getId).select(Order::getTotal)
.from(Order.class)
.where(Condition.gt(Order::getTotal, subquery))
.build();
CTEs
WITH customer_totals AS (...)
Select cte = Select.builder()
.select(Order::getCustomerId).select(Aggregate.sum(Order::getTotal).as("total_spent"))
.from(Order.class)
.groupBy(Order::getCustomerId)
.build();
Select main = Select.builder()
.with("customer_totals", cte)
.select(Expression.raw("customer_totals.customer_id"))
.select(Expression.raw("customer_totals.total_spent"))
.from(Expression.tableRef("customer_totals"))
.where(Condition.gt(Expression.raw("customer_totals.total_spent"), 5000))
.build();
Set operations
UNION and more
Select select = Select.builder()
.select(Order::getCustomerId)
.from(Order.class)
.build();
Select unionSelect = Select.builder()
.select(Order::getProductId)
.from(Order.class)
.union(select)
.build();
// Also available: unionAll(), intersect(), except()
ORDER BY, LIMIT, OFFSET
Sorted and paginated
Select select = Select.builder()
.select(Order::getId).select(Order::getTotal)
.from(Order.class)
.orderBy(OrderBy.desc(Order::getTotal), OrderBy.asc(Order::getId))
.limit(10)
.offset(20)
.build();
SELECT DISTINCT
Distinct statuses
Select select = Select.builder()
.distinct()
.select(Order::getStatus)
.from(Order.class)
.build();
Dashboards (querier-ui)
Build server-side HTML dashboards powered by Chart.js directly
from Querier queries.
This module requires the querier-ui artifact.
The output can be a full self-contained HTML page or an
embeddable fragment.
- Define widgets backed by Select queries
- Assemble them into a Dashboard
- Provide a QueryRunner for execution
- Render with renderHtml() or renderFragment()
Quick start
Dashboard rendering
QueryRunner runner = select -> {
Select.PositionalSqlAndParams sp = select.toPositionalSql();
return jdbcTemplate.queryForList(sp.sql(), sp.params().toArray());
};
DashboardWidget revenueWidget = DashboardWidget.builder()
.title("Revenue by Status")
.chartType(ChartType.BAR)
.query(
Select.builder()
.select(Order::getStatus)
.select(Aggregate.sum(Order::getTotal).as("revenue"))
.from(Order.class)
.groupBy(Order::getStatus)
.build()
)
.labelColumn("status")
.dataset(ChartDataset.of("revenue")
.label("Total Revenue")
.backgroundColor("#4e79a7")
.build())
.build();
Dashboard dashboard = Dashboard.builder()
.title("Sales Overview")
.layout(DashboardLayout.GRID_2_COLS)
.addWidget(revenueWidget)
.build();
String html = DashboardRenderer.builder()
.dashboard(dashboard)
.queryRunner(runner)
.build()
.renderHtml();
Supported chart types
BAR, HORIZONTAL_BAR, LINE, PIE, DOUGHNUT, RADAR, POLAR_AREA,
SCATTER, BUBBLE
Serving from Spring MVC
Controller endpoint
@GetMapping(value = "/dashboard", produces = MediaType.TEXT_HTML_VALUE)
@ResponseBody
public String dashboard() {
return DashboardRenderer.builder()
.dashboard(myDashboard)
.queryRunner(myRunner)
.build()
.renderHtml();
}
Embedding in an existing page
Thymeleaf / JSP fragment
String fragment = DashboardRenderer.builder()
.dashboard(myDashboard)
.queryRunner(myRunner)
.build()
.renderFragment();
Custom Chart.js URL (offline / self-hosted)
Override CDN URL
DashboardRenderer.builder()
.dashboard(myDashboard)
.queryRunner(myRunner)
.chartJsUrl("/static/js/chart.umd.min.js")
.build()
.renderHtml();
Multi-dataset charts
Grouped series
DashboardWidget multiWidget = DashboardWidget.builder()
.title("Revenue vs Order Count per Customer")
.chartType(ChartType.BAR)
.query(monthlyQuery)
.labelColumn("customer_id")
.dataset(ChartDataset.of("revenue")
.label("Revenue").backgroundColor("#4e79a7").build())
.dataset(ChartDataset.of("order_count")
.label("Order Count").backgroundColor("#f28e2b").build())
.build();
Chart options
Axis labels, legend, stacked mode
.chartOptions(ChartOptions.builder()
.xAxisLabel("Month")
.yAxisLabel("Revenue (USD)")
.legendPosition(ChartOptions.LegendPosition.BOTTOM)
.stacked(true)
.build())
Dashboard layout options
| Layout |
Description |
| SINGLE_COL |
One chart per row |
| GRID_2_COLS |
Two charts per row (default) |
| GRID_3_COLS |
Three charts per row |
| MASONRY |
Auto-fit responsive grid |
Full example: 4 widgets with full HTML and fragment rendering
OrderDashboardService.java
/**
* Builds and renders a full HTML dashboard page for Order analytics.
*
* @return a self-contained HTML string ready to be served to the browser
*/
public String renderDashboard() {
// QueryRunner: bridges Querier Select -> JdbcTemplate
QueryRunner runner = select -> {
Select.PositionalSqlAndParams sp = select.toPositionalSql();
return jdbcTemplate.queryForList(sp.sql(), sp.params().toArray());
};
// Build the dashboard
Dashboard dashboard = Dashboard.builder()
.title("Orders Dashboard")
.layout(DashboardLayout.GRID_2_COLS)
.addWidget(revenueOverTimeWidget())
.addWidget(ordersCountByStatusWidget())
.addWidget(revenueByStatusWidget())
.addWidget(topRevenueWidget())
.build();
// Render as full HTML
return DashboardRenderer.builder()
.dashboard(dashboard)
.queryRunner(runner)
.build()
.renderHtml();
}
/**
* Builds and renders a fragment HTML dashboard for Order analytics.
*
* @return a fragment HTML string ready to be embedded in an existing page
*/
public String renderDashboardFragment() {
// QueryRunner: bridges Querier Select -> JdbcTemplate
QueryRunner runner = select -> {
Select.PositionalSqlAndParams sp = select.toPositionalSql();
return jdbcTemplate.queryForList(sp.sql(), sp.params().toArray());
};
// Build the dashboard
Dashboard dashboard = Dashboard.builder()
.title("Orders Dashboard")
.layout(DashboardLayout.GRID_2_COLS)
.addWidget(revenueOverTimeWidget())
.addWidget(ordersCountByStatusWidget())
.addWidget(revenueByStatusWidget())
.addWidget(topRevenueWidget())
.build();
// Render as fragment
return DashboardRenderer.builder()
.dashboard(dashboard)
.queryRunner(runner)
.build()
.renderFragment();
}
// Widget 1: Total revenue grouped by status (BAR)
private DashboardWidget revenueByStatusWidget() {
Select query = Select.builder()
.select(Order::getStatus)
.select(Aggregate.sum(Order::getTotal).as("revenue"))
.from(Order.class)
.groupBy(Order::getStatus)
.orderBy(OrderBy.desc("SUM(orders.total)"))
.build();
return DashboardWidget.builder()
.title("Total Revenue by Status")
.chartType(ChartType.BAR)
.query(query)
.labelColumn("status")
.dataset(ChartDataset.of("revenue")
.label("Revenue")
.backgroundColor("#4e79a7")
.borderColor("#2e5a8a")
.build())
.chartOptions(ChartOptions.builder()
.xAxisLabel("Order Status")
.yAxisLabel("Revenue")
.legendPosition(ChartOptions.LegendPosition.TOP)
.build())
.build();
}
// Widget 2: Order count grouped by status (DOUGHNUT)
private DashboardWidget ordersCountByStatusWidget() {
Select query = Select.builder()
.select(Order::getStatus)
.select(Aggregate.countAll().as("order_count"))
.from(Order.class)
.groupBy(Order::getStatus)
.build();
return DashboardWidget.builder()
.title("Order Count by Status")
.chartType(ChartType.DOUGHNUT)
.query(query)
.labelColumn("status")
.dataset(ChartDataset.of("order_count")
.label("Orders")
.backgroundColor("#4e79a7", "#f28e2b", "#e15759", "#76b7b2", "#59a14f")
.build())
.chartOptions(ChartOptions.builder()
.legendPosition(ChartOptions.LegendPosition.RIGHT)
.build())
.build();
}
// Widget 3: Revenue over time for PAID orders only (LINE)
private DashboardWidget revenueOverTimeWidget() {
Select query = Select.builder()
.select(Order::getCreatedAt)
.select(Aggregate.sum(Order::getTotal).as("daily_revenue"))
.from(Order.class)
.where(
Condition.and(
Condition.eq(Order::getStatus, "PAID"),
Condition.between(
Order::getCreatedAt,
LocalDateTime.of(2025, 10, 1, 0, 0, 0),
LocalDateTime.of(2025, 12, 1, 23, 59, 59)
)
)
)
.groupBy(Order::getCreatedAt)
.orderBy(OrderBy.asc(Order::getCreatedAt))
.build();
return DashboardWidget.builder()
.title("Daily Revenue Trend (Paid Orders)")
.chartType(ChartType.LINE)
.query(query)
.labelColumn("created_at")
.dataset(ChartDataset.of("daily_revenue")
.label("Daily Revenue")
.backgroundColor("rgba(78, 121, 167, 0.2)")
.borderColor("#4e79a7")
.fill(true)
.build())
.chartOptions(ChartOptions.builder()
.xAxisLabel("Date")
.yAxisLabel("Revenue")
.legendPosition(ChartOptions.LegendPosition.TOP)
.build())
.build();
}
// Widget 4: Revenue vs order count per customer (multi-dataset BAR)
private DashboardWidget topRevenueWidget() {
Select query = Select.builder()
.select(Order::getCustomerId)
.select(Aggregate.sum(Order::getTotal).as("customer_revenue"))
.select(Aggregate.countAll().as("customer_orders"))
.from(Order.class)
.where(Condition.gt(Order::getTotal, new BigDecimal("50")))
.groupBy(Order::getCustomerId)
.orderBy(OrderBy.desc("SUM(orders.total)"))
.limit(10)
.build();
return DashboardWidget.builder()
.title("Top 10 Customers: Revenue vs Order Count")
.chartType(ChartType.BAR)
.query(query)
.labelColumn("customer_id")
.dataset(ChartDataset.of("customer_revenue")
.label("Revenue")
.backgroundColor("#4e79a7")
.borderColor("#2e5a8a")
.build())
.dataset(ChartDataset.of("customer_orders")
.label("Order Count")
.backgroundColor("#f28e2b")
.borderColor("#c06d1a")
.build())
.chartOptions(ChartOptions.builder()
.xAxisLabel("Customer ID")
.yAxisLabel("Value")
.legendPosition(ChartOptions.LegendPosition.TOP)
.stacked(false)
.build())
.build();
}
AI (querier-ai)
Querier AI translates natural-language prompts into validated
Select objects or compiled dashboards — no SQL writing required.
The AI module is provider-neutral and works with any
OpenAI-compatible endpoint. You provide your own API key and the
model configuration.
- Ask the AI to produce standard SQL
- Validate it against your declared schema
- Parse the validated SQL into a Select object
Configuration
AiClient + QuerierAi
AiClient client = AiClient.builder()
.apiKey(System.getenv("QUERIER_AI_API_KEY"))
.baseUrl(System.getenv("QUERIER_AI_BASE_URL"))
.model(System.getenv("QUERIER_AI_MODEL"))
.build();
QuerierAi ai = QuerierAi.builder()
.client(client)
.retryPolicy(RetryPolicy.ofMaxAttempts(2))
.build();
Defining the schema context
QueryRequest
QueryRequest request = QueryRequest.builder()
.prompt("Show the top 10 customers by revenue in the last 30 days")
.entities(Customer.class, Order.class)
.build();
Natural-language queries
AI query execution
QueryResult result = ai.query(request);
if (result.isValid()) {
Select select = result.query().get();
Select.PositionalSqlAndParams sp = select.toPositionalSql();
List<Map<String, Object>> rows = jdbcTemplate.queryForList(sp.sql(), sp.params().toArray());
} else {
result.validationIssues().forEach(System.err::println);
}
AI-powered dashboards
Dashboard generation
QueryRequest request = QueryRequest.builder()
.prompt("Create a sales overview dashboard with revenue by status and top customers")
.entities(Customer.class, Order.class)
.build();
DashboardResult result = ai.dashboard(request);
if (result.isValid()) {
Dashboard dashboard = result.dashboard().get();
String html = DashboardRenderer.builder()
.dashboard(dashboard)
.queryRunner(myRunner)
.build()
.renderHtml();
}
Semantic annotations
@Semantic examples
@Semantic(description = "A customer purchase", synonyms = {"sale", "transaction"})
@Table("orders")
public class Order {
@Semantic(
description = "Total monetary value of the order",
synonyms = {"revenue", "sales value", "order value"}
)
public BigDecimal getTotal() { return total; }
@Semantic(description = "Current state of the order", synonyms = {"state", "order state"})
public OrderStatus getStatus() { return status; }
}
Retry policy
Automatic SQL correction
QuerierAi ai = QuerierAi.builder()
.client(client)
.retryPolicy(RetryPolicy.ofMaxAttempts(2))
.build();