Use the engine of SqlJam from your own Java code.
The desktop application is a thin layer over the com.github.sqljam.impexp API. Every export, import and copy that the UI runs is a few lines of Java: configure a source, pick an exit, call export. This guide shows those lines for each task.
| Task | Classes |
|---|---|
| Add SqlJam to a project | Maven coordinates |
| Core classes | Overview of the API |
| 1. Export tables as a SQL package | ScriptExporter |
| 2. Export Parquet and CSV packages | ParquetExporter, CsvExporter, CsvOptions |
| 3. Copy tables into another database | ImportExporter |
| 4. Import an export package | AbstractFileImporter |
| 5. Load Parquet and CSV files of other tools | ParquetImporter, CsvImporter |
| 6. Export the rows of a query | ExportConfiguration.queries |
| 7. Export part of a table | TableQuery |
| 8. Transactions and errors | ManualTransaction, SharedConnectionFactory |
| 9. Update or skip existing rows | WriteMode |
| 10. Progress, log and cancel | ExportListener |
| 11. Older servers, names and copies | target version, IdentifierCase, tableNamePattern |
| Option reference | ExportConfiguration, ImportConfiguration |
| Adding a database | Dialect, MetaDataOperations, DbType |
SqlJam is built with Maven and Java 17. Install it into the local repository, then depend on it:
git clone https://github.com/paganini2008/sqljam.git
cd sqljam
mvn -DskipTests install<dependency>
<groupId>com.github.sqljam</groupId>
<artifactId>sqljam</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>The JDBC drivers of MySQL, MariaDB, PostgreSQL, Oracle, SQL Server, Db2, H2, SQLite, Microsoft Access (UCanAccess), DuckDB and ClickHouse come with it.
flowchart LR
S[(Source database)] --> E[Exporter<br/>metadata, pages of rows]
Q[Free query] --> E
E --> H1[ScriptExportHandler<br/>SQL package]
E --> H2[DuckDB workspace<br/>Parquet / CSV package]
E --> H3[ImportExportHandler<br/>target database]
P[Export package] --> I[AbstractFileImporter] --> T[(Target database)]
F[Parquet / CSV files] --> D[DataFileImporter] --> T
H3 --> T
| Class | Role |
|---|---|
Exporter.ExportConfiguration |
Source: database, catalogs, schemas, tables, queries and options |
ScriptExporter |
Writes a SQL package: schema.sql, data files, LOB files, constraints.sql, manifest.json |
ParquetExporter / CsvExporter |
Write Parquet or CSV packages through an embedded DuckDB |
ImportExporter |
Copies tables straight into another database |
AbstractFileImporter |
Imports any package: SQL, Parquet or CSV, chosen by its manifest.json |
ParquetImporter / CsvImporter |
Load files of other tools into a table |
ManualTransaction |
One transaction for a whole import, committed or rolled back by you |
ExportListener |
Progress, messages, errors and cancel |
Dialect / MetaDataOperations |
Everything special to one database, one subclass per database and version |
Input: tables orders and customers of the MySQL database shop.
ScriptExporter exporter = new ScriptExporter(new File("export/shop"),
DataFileStrategy.FILE_PER_TABLE, 10L * 1024 * 1024);
Exporter.ExportConfiguration source = exporter.getConfiguration();
source.setDbType(DbType.MYSQL);
source.setUrl("jdbc:mysql://localhost:3306/shop");
source.setUsername("app");
source.setPassword("secret");
source.setIncludedCatalogNames(new String[]{"shop"});
source.setIncludedTableNames(new String[]{"orders", "customers"});
source.setIdReused(true);
// Statements for PostgreSQL in the schema sales
exporter.setTargetDbType(DbType.POSTGRESQL);
exporter.setTargetSchemaName("sales");
exporter.exportDdlAndData();Output:
export/shop/
├── schema.sql # tables, sequences, indexes, comments
├── orders.sql # orders_2.sql, orders_3.sql … above 10 MB
├── customers.sql
├── lob/ # BLOB and CLOB values as files
├── lob-manifest.json
├── constraints.sql # foreign keys, applied after the rows
└── manifest.json # source, target, options, files with SHA-256, row counts
| Call | Effect |
|---|---|
exportDdlAndData() |
Structure and rows |
exportDdl() |
Structure only |
exportData() |
Rows only, into existing tables |
export(ExportMode.DDL_DATA) |
Same as the first, by ExportMode |
DataFileStrategy.SINGLE_FILE |
All rows in data.sql, data_2.sql … |
setLobSeparated(false) |
LOB values as literals inside the data files |
ParquetExporter parquet = new ParquetExporter(new File("export/parquet"));
configureSource(parquet.getConfiguration()); // like the SQL package
parquet.setCompression("ZSTD"); // ZSTD, SNAPPY, GZIP, UNCOMPRESSED
parquet.setMaxFileSize(256L * 1024 * 1024); // orders.parquet, orders_2.parquet …
parquet.exportDdlAndData();
CsvOptions options = new CsvOptions();
options.setDelimiter(";");
options.setEncoding(CsvOptions.UTF_8_BOM); // opens in Excel
options.setNullString("\\N");
options.setDateFormat("%d.%m.%Y");
CsvExporter csv = new CsvExporter(new File("export/csv"));
configureSource(csv.getConfiguration());
csv.setOptions(options);
csv.setMaxFileSize(10L * 1024 * 1024); // orders.csv, orders_2.csv …
csv.exportDdlAndData();CsvOptions |
Default | Values |
|---|---|---|
delimiter |
, |
Any text, e.g. ;, |, \t |
quote |
" |
" or ' |
nullString |
empty | Text of NULL, an empty string is written as "" |
header |
true |
Column names in the first row of each part |
encoding |
UTF-8 |
UTF-8, UTF-8 (BOM), GBK, GB18030, UTF-16, ISO-8859-1 |
dateFormat |
ISO | e.g. %Y/%m/%d, %d.%m.%Y |
The column types are kept in manifest.json, so an import of the package restores every value with its type.
Input: the schema public of PostgreSQL. Output: the same tables, keys, indexes, sequences and rows in Oracle.
ImportExporter copier = new ImportExporter();
Exporter.ExportConfiguration source = copier.getExportConfiguration();
source.setDbType(DbType.POSTGRESQL);
source.setUrl("jdbc:postgresql://localhost:5432/demo");
source.setUsername("app");
source.setPassword("secret");
source.setIncludedSchemaNames(new String[]{"public"});
source.setIdReused(true);
ImportExportHandler.ImportConfiguration target = copier.getImportConfiguration();
target.setDbType(DbType.ORACLE);
target.setUrl("jdbc:oracle:thin:@//localhost:1521/demo");
target.setUsername("app");
target.setPassword("secret");
target.setTargetSchemaName("APP");
copier.setTargetVersion(11, 2); // Oracle 11g: sequences and triggers
copier.exportDdlAndData();Rows are read and written in pages of 5000 rows (setPageSize), tables with LOB columns in pages of 100 (setLobPageSize). Foreign keys are created after the rows, identity values and sequences continue after the copied rows.
The package decides the importer: SQL, Parquet or CSV.
try (Connection connection = DriverManager.getConnection(
"jdbc:sqlserver://localhost:1433;databaseName=demo;encrypt=false", "app", "secret")) {
AbstractFileImporter importer = AbstractFileImporter.forDirectory(
new File("export/shop"), connection, DbType.SQLSERVER);
importer.setBatchSize(1000); // statements per batch of SQL packages
importer.setStopOnError(true);
importer.importDirectory(new File("export/shop"));
System.out.println(importer.getExecutedCount() + " statements, "
+ importer.getLobCount() + " LOB values, " + importer.getFailedCount() + " failed");
}manifest.json is checked first: the status of the export, the target database type and the SHA-256 of every file. Data files and their parts (orders.sql, orders_2.sql …) are read in order, LOB values are restored by primary key, foreign keys come last.
try (Connection connection = DriverManager.getConnection("jdbc:h2:./warehouse", "sa", "")) {
CsvImporter importer = new CsvImporter(connection, DbType.H2);
CsvOptions options = new CsvOptions();
options.setDelimiter(";");
options.setEncoding("GBK");
importer.setOptions(options); // null detects the delimiter and the types
long rows = importer.importFiles(List.of(new File("sales.csv"), new File("sales_2.csv")),
"SALES", DataFileImporter.Mode.CREATE);
}DataFileImporter.Mode |
Target table |
|---|---|
CREATE |
Dropped and created from the columns of the files |
APPEND |
Rows are inserted into the existing table |
UPSERT |
Rows of the same key are updated, the others inserted |
SKIP_EXISTING |
Rows of an existing key are kept, the others inserted |
ParquetImporter works the same way with .parquet files. Key columns are set by setKeyColumns(List.of("id")), the primary key of the table is used otherwise.
A query is exported like a table, through every exit. The SQL Query editor of the UI does exactly this.
ImportExporter exporter = new ImportExporter();
Exporter.ExportConfiguration source = exporter.getExportConfiguration();
source.setDbType(DbType.MYSQL);
source.setUrl("jdbc:mysql://localhost:3306/shop");
source.setUsername("app");
source.setPassword("secret");
source.setIncludedCatalogNames(new String[]{"shop"});
source.getQueries().put("order_summary",
"SELECT c.name, COUNT(o.id) AS orders, SUM(o.amount) AS amount"
+ " FROM customers c JOIN orders o ON o.customer_id = c.id GROUP BY c.name");
ImportExportHandler.ImportConfiguration target = exporter.getImportConfiguration();
target.setDbType(DbType.DUCKDB);
target.setUrl("jdbc:duckdb:./reports.duckdb");
exporter.exportDdlAndData();| What happens | How |
|---|---|
| Columns of the new table | A column of a table gets the type of that column, an expression gets the type of the result set |
| Columns of the same name | Numbered: id, id_2 |
| Rows | Read once by a cursor and written page by page, no OFFSET paging |
| Types a table export converts | Selected again by the same conversion, e.g. Oracle XMLTYPE and VECTOR, ClickHouse Map and Tuple |
| Other exits | Put the query into ScriptExporter, ParquetExporter or CsvExporter configurations the same way |
TableQuery query = new TableQuery(
List.of("id", "name", "amount"), // columns, all if empty
"amount > 100 AND status = 'PAID'", // WHERE
null, // GROUP BY is for viewing only
"amount DESC"); // ORDER BY
source.getTableQueries().put("orders", query);Keys, indexes and foreign keys whose columns are all selected are kept. A missing target table is created with the selected columns.
One transaction, all or nothing: share one ManualTransaction with the importer.
Connection connection = DriverManager.getConnection(url, user, password);
try (ManualTransaction transaction = new ManualTransaction(connection,
DbType.POSTGRESQL.isTransactionalDdl())) {
copier.setTargetConnectionFactory(new SharedConnectionFactory(transaction.getConnection()));
copier.exportDdlAndData();
if (transaction.isRolledBack()) {
System.out.println("Rolled back because of an error");
} else {
List<String> failed = transaction.commit(); // deferred DDL after the commit
}
} // close() rolls back what is not committed| Choice | Setting | Behavior |
|---|---|---|
| One transaction | ManualTransaction |
An error rolls back the whole import |
| Commit every N rows | setPageSize(N) without ManualTransaction |
Each page is committed, rows before an error stay |
| Stop on the first error | failFast = true (default) |
The import ends at the error |
| Skip rows with errors | failFast = false |
A failed page is written row by row, each bad row is reported with its key and error, the others are kept |
Databases which commit DDL at once (MySQL, MariaDB, Oracle, H2) keep their created tables after a roll back, ManualTransaction runs keys and identity statements after the commit for them. For packages, AbstractFileImporter takes the connection of the transaction directly: forDirectory(dir, transaction.getConnection(), dbType).
source.setTableRecreated(false); // keep the target tables
source.setWriteMode(WriteMode.UPSERT); // or SKIP_EXISTING
source.getKeyColumnNames().put("customers", List.of("email"));WriteMode |
Existing key | New key |
|---|---|---|
INSERT (default) |
Inserted, a key error is reported | Inserted |
UPSERT |
Updated | Inserted |
SKIP_EXISTING |
Kept as it is | Inserted |
The key is the primary key of the table, or the chosen columns: columns of a unique index of the target table. Statements per database:
| Database | Upsert | Skip existing |
|---|---|---|
| PostgreSQL, SQLite, DuckDB | ON CONFLICT … DO UPDATE |
ON CONFLICT … DO NOTHING |
| MySQL, MariaDB | ON DUPLICATE KEY UPDATE |
ON DUPLICATE KEY UPDATE without changes |
| Oracle, SQL Server, Db2, Access | MERGE … WHEN MATCHED … WHEN NOT MATCHED |
MERGE … WHEN NOT MATCHED |
| H2 | MERGE INTO … KEY |
MERGE … WHEN NOT MATCHED |
Names of an existing target table are matched without case, OrderId of the source writes orderid of PostgreSQL or ORDERID of Oracle.
copier.setExportListener(new ExportListener() {
@Override
public void onTableProgress(String catalog, String schema, String table, long rows, long total) {
System.out.printf("%s: %d / %d%n", table, rows, total);
}
@Override
public void onProgress(long processed, long total) {
System.out.printf("%.0f%%%n", processed * 100.0 / Math.max(total, 1));
}
@Override
public void onError(String message, Throwable e) {
System.err.println(message); // e.g. Skipped row of table 'orders' (id=42): …
}
@Override
public boolean isCancelled() {
return stopRequested.get(); // the export stops at the next page
}
});| Callback | When |
|---|---|
onStart / onEnd(successful) |
Before the first and after the last step |
onTableStart / onTableProgress / onTableEnd |
Per table, rows written so far |
onProgress(processed, total) |
Rows of all tables, bytes of a package import |
onMessage |
Steps such as Reading metadata ... |
onError |
Errors and skipped rows or statements |
isCancelled |
Asked before each page |
| Need | Setting |
|---|---|
| Statements for an older server | setTargetVersion(11, 2) on the exporter or copier, e.g. Oracle 11g, SQL Server 2008 (10, 0), PostgreSQL 9, MySQL 5.5 |
| Case of names | setIdentifierCase(IdentifierCase.LOWER): AUTO, KEEP, LOWER, UPPER |
| A copy in the same schema | target.setTableNamePattern("{table}_copy"), keys and indexes get their own names |
| Only some objects | setIndexIncluded, setForeignKeyIncluded, setCommentIncluded, setSequenceIncluded |
| Identity values | setIdReused(true) copies them and continues sequences after them |
Exporter.ExportConfiguration (source):
| Option | Default | Description |
|---|---|---|
dbType, url, username, password |
Source database | |
includedCatalogNames / includedSchemaNames |
all | Catalogs (MySQL databases) and schemas to read |
includedTableNames / includedTableNamePattern |
all | Tables by name or by regular expression |
queries |
Free queries exported as tables, SQL by table name | |
tableQueries |
Columns, WHERE and ORDER BY per table | |
tableRecreated |
true |
Drop and create target tables |
idReused |
false |
Copy identity values and continue sequences |
indexIncluded / foreignKeyIncluded / commentIncluded / sequenceIncluded |
true |
Objects to copy |
failFast |
true |
Stop on the first error, false skips rows with errors |
pageSize / lobPageSize |
5000 / 100 |
Rows per page, rows per page of tables with LOB columns |
writeMode / keyColumnNames |
INSERT |
UPSERT or SKIP_EXISTING by key columns per table |
connectionPoolEnabled |
true |
HikariCP pool for the source |
ImportExportHandler.ImportConfiguration (target of a copy):
| Option | Default | Description |
|---|---|---|
dbType, url, username, password |
Target database | |
targetCatalogName / targetSchemaName |
source names | Where the tables go |
targetSchemaCreated |
true |
Create a missing schema (database of MySQL) |
tableNamePattern |
Target table names, {table} is the source name |
|
connectionPoolEnabled |
true |
HikariCP pool for the target |
| Step | Where |
|---|---|
| Dialect: types, literals, DDL, paging, upserts | impexp.db.XxxDialect extends Dialect |
| Metadata: catalogs, schemas, tables, columns, keys | impexp.db.XxxMetaDataOperations extends MetaDataOperations |
| Older versions | XxxNNDialect extends XxxDialect, chosen by Dialect.forVersion |
| Versions other than the server version, e.g. the file format of Access | Dialect.readDatabaseVersion |
| Tables which are emptied before they are dropped, drops of missing tables | Dialect.getEmptyTableStatement, Dialect.isMissingDropError |
| Registration | A constant of DbType with driver, URL template and category |
| Fixture and tests | src/test/resources/<db>/fixture.sql, a value of ItDatabase, the cross-database matrix runs it |
Read the change logs in change_logs for the decisions behind each part.