JQuick-Connector
JQuick-Connector
Section titled “JQuick-Connector”Module overview
Section titled “Module overview”JQuick-Connector solves a very specific problem: inside one business method, the data may come from a MySQL row, a CSV file an operator just sent over, a JSON payload from an upstream API, and a cache key in Redis. They have nothing in common in how you reach them — yet they all have to end up as the same table.
JQuick-Connector folds those sources into a single query statement, and the result is an in-memory dataset, JQuickDataSet — columns have names and types, rows can be read by column name, and the dataset can be filtered, sorted, grouped, joined and mapped to beans. In other words, the differences between data sources stay inside the query; what reaches your business code is one table.
SELECT field(id) -> userId:Long, path('$.profile.city') -> city:StringFROM MYSQL( url: 'jdbc:mysql://localhost:3306/app', username: 'root', password: 'pwd', driver: 'com.mysql.cj.jdbc.Driver', sql: 'select * from users')The whole thing is pure Java: a SQL-like syntax, a set of field processors, a registry of source handlers, and an optional XML entry point. No Spring required — plain Java projects and Spring Boot projects use the same API.
Features
Section titled “Features”- One syntax across sources:
SELECT ... FROM TYPE(params)— switching sources only changes theFROMpart. - 22 built-in data sources: 14 relational databases, 6 file formats, Redis and HTTP endpoints, all registered automatically through SPI.
- Two field processors:
field()reads flat columns,path()reads nested structures by JSON path; both can be mixed in one statement. - Typed mapping:
Integer,Long,Float,Double,String,Boolean,DateandObject, converted at read time as declared. - Context variables:
{name}placeholders backed byJContext, so credentials and tenant names stay out of the statement. - In-memory dataset:
JQuickDataSetships with filtering, sorting, paging, grouping, concatenation and inner joins, plustoMapList()/toBeanList(). - XML declarative configuration: queries live in
jquick-connector.xml, bound to service methods by name, withif/foreach/choosefor dynamic fragments. - Extensible: implement
JQuickConnectorHandlerfor a new source,JQuickConnectorProcessorfor custom extraction, register it, and use it in the syntax. - Apache-2.0: free for commercial and personal projects.
Use cases
Section titled “Use cases”- Report data assembly: one report combines database rows, data keyed in through Excel and an upstream API — query each into a dataset, then
concat/innerJoin. - Migration and reconciliation: read a CSV or Excel file, compare it against database records, print the differing rows.
- Operator imports: turn an uploaded spreadsheet into a dataset, validate and convert the fields before persisting.
- Multi-tenant configuration: the database name and credentials come from tenant context; the query itself never changes.
- API aggregation: pull JSON with the
CURLconnector, extract fields by path, merge with local data.
Quick start
Section titled “Quick start”Maven dependency
Section titled “Maven dependency”Requirements:
| Item | Requirement |
|---|---|
| JDK | Java 8 or later |
| Build tool | Maven 3.6+ or Gradle 7+ |
| Data sources | JDBC sources need their own driver; file sources need nothing extra |
| Underlying dependencies | Javelin Core, JQuick Path, JQuick JSON, JQuick XML proxy, Jackson — all pure Java |
<dependency> <groupId>io.github.paohaijiao</groupId> <artifactId>jquick-connector</artifactId> <version>2.1.0</version></dependency>Use the latest release on Maven Central as authoritative.
Gradle dependency
Section titled “Gradle dependency”implementation 'io.github.paohaijiao:jquick-connector:2.1.0'Your first query
Section titled “Your first query”Two lines: the factory runs the statement, the dataset takes the result.
import com.github.paohaijiao.factory.JQuickConnectorFactory;import com.github.paohaijiao.statement.JQuickDataSet;
JQuickConnectorFactory factory = new JQuickConnectorFactory();JQuickDataSet dataSet = factory.executeQuery( "SELECT field(id) -> uid:Long, field(name) -> name:String " + "FROM CSV(filepath: 'D:/data/users.csv', split: ',', header: 'true')");
for (int i = 0; i < dataSet.size(); i++) { System.out.println(dataSet.get(i).getLong("uid") + " / " + dataSet.get(i).getString("name"));}executeQuery(String) always returns a JQuickDataSet; see In-memory dataset for how to read it.
Basic syntax
Section titled “Basic syntax”A connector query has two parts, SELECT and FROM. The shape looks like SQL, but SELECT declares field mappings rather than column names, and FROM names a source type plus its connection parameters.
SELECT field(source field) -> target field:data type, path('$.json.path') -> target field:data typeFROM source type( parameter: value, parameter: value)SELECT: which fields to read
Section titled “SELECT: which fields to read”Every item is a three-part expression, processor(source) -> target field:type:
| Part | Meaning |
|---|---|
| Processor | field(...) for flat fields, path('...') for nested fields |
| Source | A column name for field; a JSON path expression for path |
| Target field | The column name in the dataset — this is what you read by |
| Type | The Java type of the column, see Data types |
The target field may repeat the source name or rename it — which is exactly how different sources get aligned onto one set of fields:
SELECT field(id) -> id:Long, field(user_name) -> userName:StringFROM MYSQL(...)FROM: how to reach the source
Section titled “FROM: how to reach the source”After FROM comes the source type and a parenthesised list of name: value parameters. How a value is written decides how it is parsed:
| Form | Meaning | Example |
|---|---|---|
| Single-quoted string | Plain string parameter | url: 'jdbc:mysql://...' |
| Number / boolean | Literal | port: 3306 |
| Brace variable | Resolved from JContext |
password: {dbPassword} |
| Backticks | Verbatim text, used for curl commands | curl: `curl -X GET '...'` |
Context variables
Section titled “Context variables”Hard-coding credentials into a statement is not an option. JContext is a Map<String, Object>; pass it to the factory and reference values with {name}:
import com.github.paohaijiao.factory.JQuickConnectorFactory;import com.github.paohaijiao.param.JContext;
JContext context = new JContext();context.put("dbUser", "report_reader");context.put("dbPassword", "******");
JQuickConnectorFactory factory = new JQuickConnectorFactory(context);JQuickDataSet dataSet = factory.executeQuery( "SELECT field(id) -> id:Long FROM MYSQL(" + "url: 'jdbc:mysql://localhost:3306/app', " + "username: {dbUser}, password: {dbPassword}, " + "driver: 'com.mysql.cj.jdbc.Driver', " + "sql: 'select * from users')");The same context supports a parse-only path: JQuickConnectorQueryParser turns the statement into a JQuickConnectorParsedQuery, so you can validate or audit it before touching any data source.
Field processors
Section titled “Field processors”Extraction is abstracted into processors; two ship with the library, field and path.
field: flat fields
Section titled “field: flat fields”Reads a value by column or field name, suited to database result sets, CSV and Excel.
field(id) -> userId:Long,field(name) -> userName:Stringpath: nested fields
Section titled “path: nested fields”Reads a value through a JSON path, suited to JSON, XML, YAML and Properties.
path('$.user.name') -> name:String,path('$.department.employees[0].title') -> firstTitle:StringPath expressions are parsed by JQuick-Path, so root $, property access ., array indexes [0], slices and wildcards all work. When a JSON document has both top-level and nested fields, the two processors mix freely:
SELECT field(id) -> id:String, field(name) -> name:String, path('$.manager.title') -> managerTitle:StringFROM JSON(filepath: 'D:/data/dept.json', searchPath: '$.departments')Data types
Section titled “Data types”The type declared in SELECT decides both the column’s Java type and the accessor you use:
| Type | Java type | Notes |
|---|---|---|
Integer |
Integer |
Integer |
Long |
Long |
Long integer |
Float |
Float |
Single precision |
Double |
Double |
Double precision |
String |
String |
Text |
Boolean |
Boolean |
Boolean |
Date |
java.util.Date |
Date and time |
Object |
Object |
Complex value, typically a retained JSON subtree |
Type names are case-insensitive. An unknown type raises an exception instead of silently falling back to String.
Data sources
Section titled “Data sources”Source handlers register themselves through SPI when the class loader initialises; the type name in FROM is the registered name.
Databases
Section titled “Databases”| Type name | Database |
|---|---|
MYSQL |
MySQL |
POSTGRESQL |
PostgreSQL |
ORACLE |
Oracle |
DB2 |
DB2 |
SYBASE |
Sybase |
SQLITE |
SQLite |
H2 |
H2 |
DERBY |
Derby |
INFORMIX |
Informix |
KINGBASE |
KingbaseES |
DAMENG |
DM (DaMeng) |
HIVE |
Hive |
IMPALA |
Impala |
CLICKHOUSE |
ClickHouse |
All database connectors share one JDBC implementation and therefore one parameter set:
| Parameter | Required | Notes |
|---|---|---|
url |
Yes | JDBC connection string |
username |
Yes | User name |
password |
Yes | Password |
sql |
Yes | The query to run |
driver |
Yes | JDBC driver class, e.g. com.mysql.cj.jdbc.Driver |
| Type name | Format | Required | Optional |
|---|---|---|---|
CSV |
CSV | filepath |
split, header, encoding |
EXCEL |
xls / xlsx | filepath |
sheet, header |
JSON |
JSON | filepath |
searchPath, encoding |
XML |
XML | filepath |
searchPath, encoding |
YAML |
YAML | filepath |
searchPath, encoding |
PROPERTIES |
Properties | filepath |
searchPath, encoding |
searchPath tells the handler where the rows start: leave it out when the document root is already the collection, set it when the data hangs under a node such as $.data.
Cache and REST
Section titled “Cache and REST”| Type name | Target | Required | Optional |
|---|---|---|---|
REDIS |
Redis | host, port |
password, database, redisKey |
CURL |
HTTP endpoint | curl |
searchPath |
CURL reuses the jquick-curl command syntax, so method, headers and body are all expressed the way you would write them for curl.
Connector examples
Section titled “Connector examples”SELECT field(id) -> id:Long, field(title) -> title:String, field(created_time) -> createdTime:Date, field(is_deleted) -> deleted:BooleanFROM MYSQL( url: 'jdbc:mysql://localhost:3306/app', username: 'root', password: 'password', driver: 'com.mysql.cj.jdbc.Driver', sql: 'select * from article')SELECT field(id) -> id:Long, field(name) -> name:String, field(salary) -> salary:Double, field(birth_date) -> birthDate:DateFROM CSV( filepath: 'D:/data/staff.csv', split: ',', header: 'true')With header: 'true' the first line supplies the column names; with false, columns are named column1, column2 and so on.
SELECT field(id) -> id:Long, field(name) -> name:String, field(amount) -> amount:DoubleFROM EXCEL( filepath: 'D:/data/orders.xlsx', sheet: 'Sheet1', header: 'true')SELECT field(id) -> id:String, field(name) -> name:String, path('$.manager.title') -> managerTitle:String, field(manager) -> manager:ObjectFROM JSON( filepath: 'D:/data/dept.json', searchPath: '$.departments')Object columns keep the original subtree, which you can parse further on the Java side.
SELECT path('$.attributes.id[0]') -> id:Long, path('$.user.name[0]') -> name:String, path('$.user.email[0]') -> email:StringFROM XML( filepath: 'D:/data/user.xml', searchPath: '$.users')YAML and Properties
Section titled “YAML and Properties”SELECT path('$.app.name') -> name:String, path('$.app.version') -> version:StringFROM YAML(filepath: 'D:/data/config.yml', searchPath: '$')SELECT path('$.database.username') -> username:String, path('$.server.port') -> port:IntegerFROM PROPERTIES(filepath: 'D:/data/config.properties', searchPath: '$')SELECT field(name) -> name:String, field(age) -> age:Long, field(email) -> email:StringFROM REDIS( host: '127.0.0.1', port: 6379, password: '', database: 0, redisKey: 'user:1')REST endpoints
Section titled “REST endpoints”SELECT field(projectId) -> projectId:String, field(projectName) -> projectName:StringFROM CURL( curl: `curl -X GET 'http://api.example.com/projects' -H 'Accept: application/json'`, searchPath: '$.data')In-memory dataset JQuickDataSet
Section titled “In-memory dataset JQuickDataSet”Once the query returns, the differences between sources are gone: what you hold is a table with typed columns. JQuickDataSet groups its methods into four families — read, filter, compute, convert.
Reading rows and columns
Section titled “Reading rows and columns”| Method | Returns | Notes |
|---|---|---|
getColumnNames() |
List<String> |
All column names |
getColumns() |
List<JQuickColumnMeta> |
Column metadata: name, type, source field |
getRows() |
List<JQuickRow> |
All rows |
size() / isEmpty() |
int / boolean |
Row count, emptiness |
get(int) |
JQuickRow |
Row by index |
first() / last() |
JQuickRow |
First and last row |
getColumnValues(name) |
Stream<Object> |
All values of one column |
JQuickRow implements Map<String, Object>, so row.get("name") returns the raw object; use the typed accessors when you need a converted value:
JQuickRow row = dataSet.first();String name = row.getString("name");Long id = row.getLong("id");Date created = row.getDate("created_time");boolean has = row.hasColumn("email");Object raw = row.getObject("extra", null); // with a defaultAlso available: getInt, getDouble, getBoolean, getDate(name, format), getAsConverted(name, Class) and isNull(name).
Filtering, sorting, paging
Section titled “Filtering, sorting, paging”These return a new dataset and leave the original untouched, so they chain:
JQuickDataSet result = dataSet .filter(row -> row.getDouble("amount") > 1000) .orderBy("amount", false) .limit(20);| Method | Notes |
|---|---|
filter(Predicate<JQuickRow>) |
Filter rows |
map(Function<JQuickRow, JQuickRow>) |
Rewrite each row |
orderBy(column) / orderBy(column, ascending) / orderBy(String...) |
Sort by one or more columns |
limit(n) / skip(n) |
Paging |
subDataset(start, end) |
Slice by row index |
distinct() / distinctBy(String...) |
Deduplicate whole rows / by columns |
select(String...) / drop(String...) |
Keep or remove columns |
addColumn(name, type, source, extractor) |
Derive a new column |
Grouping and aggregation
Section titled “Grouping and aggregation”Map<Object, List<JQuickRow>> byCity = dataSet.groupBy("city");double total = dataSet.sum("amount");double avg = dataSet.avg("amount");Double max = dataSet.max("amount");Double min = dataSet.min("amount");long big = dataSet.count(row -> row.getDouble("amount") > 1000);For custom aggregation use groupByAndAggregate(column, Collector) with any JDK collector. getStatistics(column) returns count, sum, average and extremes for a single column.
Combining datasets
Section titled “Combining datasets”JQuickDataSet all = january.concat(february);JQuickDataSet joined = orders.innerJoin(users, "user_id", "id");concat stacks rows; innerJoin joins on the given columns.
Converting to maps or beans
Section titled “Converting to maps or beans”List<Map<String, Object>> maps = dataSet.toMapList();List<UserVO> users = dataSet.toBeanList(UserVO.class);toBeanList matches columns to properties by name, which makes it the shortest path from a query to your service layer.
Printing and debugging
Section titled “Printing and debugging”dataSet.printTable(); // console tabledataSet.printTable(20); // first 20 rows onlydataSet.printSummary(); // row/column counts and column typesWhen a field mapping looks wrong, printTable() is usually faster than a debugger — a misnamed column or an unconverted type shows up immediately.
XML rules and interface binding
Section titled “XML rules and interface binding”Keeping statements in XML and binding them to interface methods suits projects with many queries that need central management. The style will be familiar if you have used MyBatis mappers.
The configuration file
Section titled “The configuration file”Create jquick-connector.xml under src/main/resources:
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE connectors PUBLIC "-//PAOHAIJIAO//DTD JQUICK CONNECTOR 1.0//EN" "classpath:paohaijiao/dtd/Jquick-connector.dtd">
<connectors namespace="com.example.service.UserConnectorService">
<connector name="findById" returnClass="com.github.paohaijiao.statement.JQuickDataSet"> SELECT field(id) -> id:Long, field(name) -> name:String, field(email) -> email:String FROM MYSQL( url: 'jdbc:mysql://localhost:3306/user_db', username: 'root', password: 'password', driver: 'com.mysql.cj.jdbc.Driver', sql: 'select * from user where id = {userId}' ) </connector>
<connector name="readConfig" returnClass="com.github.paohaijiao.statement.JQuickDataSet"> SELECT path('$.database.username') -> username:String, path('$.server.port') -> port:Integer FROM JSON(filepath: 'D:/data/config.json', searchPath: '$.settings') </connector>
</connectors>namespace on <connectors> is the fully qualified name of the service interface; each <connector> has a name matching a method and a required returnClass attribute.
The service interface and proxy
Section titled “The service interface and proxy”Method names line up with name, and parameters bind the statement’s placeholders through @Param:
package com.example.service;
import com.github.paohaijiao.statement.JQuickDataSet;import com.github.paohaijiao.xml.param.Param;
public interface UserConnectorService {
JQuickDataSet findById(@Param("userId") Long userId);
JQuickDataSet readConfig();}Create the proxy from the factory when you call it:
import com.github.paohaijiao.xml.JQuickConnectorXmlParseFactory;import com.github.paohaijiao.xml.factory.JQuickFactory;import com.github.paohaijiao.xml.factory.JQuickXmlFactory;import com.github.paohaijiao.xml.handler.JQuickParseHandler;
JQuickParseHandler parser = new JQuickConnectorXmlParseFactory();JQuickFactory factory = new JQuickXmlFactory(parser, "jquick-connector.xml");UserConnectorService service = factory.createApi(UserConnectorService.class);
JQuickDataSet result = service.findById(1L);result.printTable();Dynamic conditions
Section titled “Dynamic conditions”Three elements build variable fragments.
<if test="..."> appends its body when the condition holds:
<connector name="search" returnClass="com.github.paohaijiao.statement.JQuickDataSet"> SELECT field(id) -> id:Long, field(name) -> name:String FROM MYSQL( url: 'jdbc:mysql://localhost:3306/db', username: 'root', password: 'pwd', driver: 'com.mysql.cj.jdbc.Driver', sql: 'select * from users where 1 = 1' ) <if test="name != null"> and name = {name} </if></connector><foreach> iterates a collection, typically for in lists:
<foreach collection="ids" item="id" open="(" close=")" separator=","> {id}</foreach><choose> / <when> / <otherwise> handles branches:
<choose> <when test="type == 1">and type = 1</when> <when test="type == 2">and type = 2</when> <otherwise>and type = 0</otherwise></choose>| Element | Attributes | Notes |
|---|---|---|
<if> |
test (required) |
Appends its body when the condition holds |
<foreach> |
collection, item (required); open, close, separator (optional) |
Iterates a collection into a fragment |
<when> |
test (required) |
A branch condition |
<otherwise> |
none | Used when no when matches |
Extending with custom connectors
Section titled “Extending with custom connectors”When a source is not in the built-in list, or extraction needs special handling, implement and register it.
Custom source: implement JQuickConnectorHandler, read the data into List<JQuickRow> inside buildRow(JQuickConnectorParsedQuery), then register the type name.
import com.github.paohaijiao.factory.JQuickConnectorFactory;
JQuickConnectorFactory.registerConnector("MYSERVICE", new MyServiceHandler());Custom field processor: implement JQuickConnectorProcessor and use the new processor name in SELECT.
JQuickConnectorFactory.registerProcessor("header", new MyHeaderProcessor());Registration is global — every JQuickConnectorFactory instance sees it afterwards. Implementations can also be declared in META-INF/services and shipped inside a jar.
Related links
Section titled “Related links”- GitHub repository: https://github.com/paohaijiao/jquick-connector
- Maven Central: io.github.paohaijiao:jquick-connector
- Current version:
2.1.0(JDK 8 or later) - Issues: GitHub Issues
JQuick-Connector is released under the Apache License, Version 2.0.
Copyright (c) [2025-2099] Martin ([email protected])
Licensed under the Apache License, Version 2.0 (the "License");you may not use this file except in compliance with the License.You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, softwaredistributed under the License is distributed on an "AS IS" BASIS,WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.See the License for the specific language governing permissions andlimitations under the License.