Skip to content

JQuick-Connector

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:String
FROM 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.

  • One syntax across sources: SELECT ... FROM TYPE(params) — switching sources only changes the FROM part.
  • 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, Date and Object, converted at read time as declared.
  • Context variables: {name} placeholders backed by JContext, so credentials and tenant names stay out of the statement.
  • In-memory dataset: JQuickDataSet ships with filtering, sorting, paging, grouping, concatenation and inner joins, plus toMapList() / toBeanList().
  • XML declarative configuration: queries live in jquick-connector.xml, bound to service methods by name, with if / foreach / choose for dynamic fragments.
  • Extensible: implement JQuickConnectorHandler for a new source, JQuickConnectorProcessor for custom extraction, register it, and use it in the syntax.
  • Apache-2.0: free for commercial and personal projects.
  • 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 CURL connector, extract fields by path, merge with local data.

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.

implementation 'io.github.paohaijiao:jquick-connector:2.1.0'

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.

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 type
FROM source type(
parameter: value,
parameter: value
)

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:String
FROM MYSQL(...)

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 '...'`

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.

Extraction is abstracted into processors; two ship with the library, field and path.

Reads a value by column or field name, suited to database result sets, CSV and Excel.

field(id) -> userId:Long,
field(name) -> userName:String

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:String

Path 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:String
FROM JSON(filepath: 'D:/data/dept.json', searchPath: '$.departments')

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.

Source handlers register themselves through SPI when the class loader initialises; the type name in FROM is the registered name.

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.

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.

SELECT
field(id) -> id:Long,
field(title) -> title:String,
field(created_time) -> createdTime:Date,
field(is_deleted) -> deleted:Boolean
FROM 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:Date
FROM 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:Double
FROM 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:Object
FROM 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:String
FROM XML(
filepath: 'D:/data/user.xml',
searchPath: '$.users'
)
SELECT
path('$.app.name') -> name:String,
path('$.app.version') -> version:String
FROM YAML(filepath: 'D:/data/config.yml', searchPath: '$')
SELECT
path('$.database.username') -> username:String,
path('$.server.port') -> port:Integer
FROM PROPERTIES(filepath: 'D:/data/config.properties', searchPath: '$')
SELECT
field(name) -> name:String,
field(age) -> age:Long,
field(email) -> email:String
FROM REDIS(
host: '127.0.0.1',
port: 6379,
password: '',
database: 0,
redisKey: 'user:1'
)
SELECT
field(projectId) -> projectId:String,
field(projectName) -> projectName:String
FROM CURL(
curl: `curl -X GET 'http://api.example.com/projects' -H 'Accept: application/json'`,
searchPath: '$.data'
)

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.

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 default

Also available: getInt, getDouble, getBoolean, getDate(name, format), getAsConverted(name, Class) and isNull(name).

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
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.

JQuickDataSet all = january.concat(february);
JQuickDataSet joined = orders.innerJoin(users, "user_id", "id");

concat stacks rows; innerJoin joins on the given columns.

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.

dataSet.printTable(); // console table
dataSet.printTable(20); // first 20 rows only
dataSet.printSummary(); // row/column counts and column types

When a field mapping looks wrong, printTable() is usually faster than a debugger — a misnamed column or an unconverted type shows up immediately.

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.

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.

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();

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

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.

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, software
distributed 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 and
limitations under the License.