Compare commits
63 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 70f172f721 | |||
| b69fc6854e | |||
| 8d7797b769 | |||
| b63b966621 | |||
| 94cd89899a | |||
| 61b3967a36 | |||
| cc8bf49565 | |||
| e3f624f4d8 | |||
| 2ced7810d9 | |||
| 6fe2bea089 | |||
| a61e018932 | |||
| dc2f850530 | |||
| 7be85a2da1 | |||
| ce76e93a77 | |||
| d9fd651c72 | |||
| 78c832eb1f | |||
| a1c9ea26ce | |||
| f632a77e8e | |||
| ff4cf25585 | |||
| fc93be5a9d | |||
| 7db2abdf18 | |||
| 6a925b83ca | |||
| c0f6e3a6e6 | |||
| ba24b874fc | |||
| 424d7d4ebb | |||
| 56fecff550 | |||
| 1ca507d1dd | |||
| 1717c7ee2c | |||
| 6c9b0f96a0 | |||
| 85355efe8f | |||
| 57093441c3 | |||
| f084f8380a | |||
| a9bd96b377 | |||
| c6d34847d5 | |||
| 4b8ffcdd1c | |||
| e9fc745d20 | |||
| 9cf698c67d | |||
| 20d40f069d | |||
| b0f104927c | |||
| 0ecb6860bd | |||
| c41ab99841 | |||
| 8cdd88d053 | |||
| 809f2a8949 | |||
| f47eaf4bac | |||
| 1392e0a6d8 | |||
| b0ee4c77d9 | |||
| b3a9151eff | |||
| e7b7d1bbba | |||
| 24fad6aa04 | |||
| b816399cba | |||
| fe39c6a2ae | |||
| c67c7b1360 | |||
| 05508d8b26 | |||
| 1050930667 | |||
| 509873e60b | |||
| 2245ef1ba2 | |||
| c7884f3605 | |||
| efd922b2e0 | |||
| 3b4af2bf47 | |||
| 0615163fad | |||
| cf5abeddbe | |||
| e60a92cfdc | |||
| 54ab5645b1 |
7
.gitignore
vendored
7
.gitignore
vendored
@ -5,3 +5,10 @@
|
|||||||
build
|
build
|
||||||
|
|
||||||
*.swp
|
*.swp
|
||||||
|
|
||||||
|
# Ignore local configuration files
|
||||||
|
run.yml
|
||||||
|
|
||||||
|
# Ignore accidentally extracted distribution files in project root
|
||||||
|
/bin/
|
||||||
|
/lib/
|
||||||
|
|||||||
183
CLAUDE.md
Normal file
183
CLAUDE.md
Normal file
@ -0,0 +1,183 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
jrunner is a Java CLI tool for migrating data between databases. It reads data from a source database using SQL queries and writes it to a destination table, batching inserts for performance. The tool supports multiple database types via JDBC drivers including PostgreSQL, IBM AS/400, and Microsoft SQL Server.
|
||||||
|
|
||||||
|
## Build and Test Commands
|
||||||
|
|
||||||
|
Build the project:
|
||||||
|
```bash
|
||||||
|
gradle build
|
||||||
|
# or use wrapper
|
||||||
|
./gradlew build
|
||||||
|
```
|
||||||
|
|
||||||
|
Run tests:
|
||||||
|
```bash
|
||||||
|
gradle test
|
||||||
|
# or use wrapper
|
||||||
|
./gradlew test
|
||||||
|
```
|
||||||
|
|
||||||
|
Build distribution package:
|
||||||
|
```bash
|
||||||
|
gradle build
|
||||||
|
# Creates jrunner/build/distributions/jrunner.zip
|
||||||
|
```
|
||||||
|
|
||||||
|
Local install for testing (recommended):
|
||||||
|
```bash
|
||||||
|
./gradlew installDist
|
||||||
|
# Creates executable at jrunner/build/install/jrunner/bin/jrunner
|
||||||
|
```
|
||||||
|
|
||||||
|
Deploy using interactive script:
|
||||||
|
```bash
|
||||||
|
./deploy.sh
|
||||||
|
# Choose: 1) Local install, 2) Global install to /opt, 3) Custom directory
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Single-File Design
|
||||||
|
The entire application logic resides in `jrunner/src/main/java/jrunner/jrunner.java`. This is a monolithic command-line tool with no abstraction layers or separate modules.
|
||||||
|
|
||||||
|
### Dual Mode Operation (v1.1+)
|
||||||
|
|
||||||
|
The tool operates in two modes:
|
||||||
|
|
||||||
|
**Query Mode** (new in v1.1):
|
||||||
|
- Activates automatically when destination flags are not provided
|
||||||
|
- Outputs query results to stdout in CSV or TSV format
|
||||||
|
- Silent operation - no diagnostic output, just clean data
|
||||||
|
- Designed for piping to visidata, pspg, less, or other data tools
|
||||||
|
- Format controlled by -f flag (csv or tsv)
|
||||||
|
|
||||||
|
**Migration Mode** (original functionality):
|
||||||
|
- Activates when destination flags are provided
|
||||||
|
- Reads from source, writes to destination with batched INSERTs (or bulk copy with `-b`)
|
||||||
|
- Shows progress counters and timing information
|
||||||
|
|
||||||
|
**Bulk Copy** (migration mode, `-b`) — uses the dest's native bulk path; falls
|
||||||
|
back to the INSERT path for any other dest (e.g. DB2):
|
||||||
|
|
||||||
|
*SQL Server dest* — streams the source ResultSet over the TDS bulk-load
|
||||||
|
protocol via `SQLServerBulkCopy` (no per-batch INSERT round trips; a 1.27M-row,
|
||||||
|
~298-col load went ~111 min → ~4 min). A `BulkSource` adapter
|
||||||
|
(`ISQLServerBulkData`) maps source type names to JDBC types we control:
|
||||||
|
string-ish types (text/varchar/char/bpchar/json/jsonb/uuid **and numeric**) are
|
||||||
|
declared NVARCHAR and read via `getString` so SQL Server converts losslessly —
|
||||||
|
numeric goes this route because PG reports unconstrained numeric as scale 0,
|
||||||
|
which a typed DECIMAL path would round (123.45 → 123).
|
||||||
|
|
||||||
|
*Postgres dest* — streams via `COPY <table> FROM STDIN WITH (FORMAT csv)` using
|
||||||
|
the JDBC `CopyManager`. COPY is text-based, so the server parses each field into
|
||||||
|
the column type — no per-type handling. Every non-null value is CSV-quoted
|
||||||
|
(empty string stays distinct from NULL, which is an empty unquoted field); rows
|
||||||
|
flush in 1000-row buffers.
|
||||||
|
|
||||||
|
Both emit a `\r`-counter every 10k rows for live progress and print the final
|
||||||
|
row count.
|
||||||
|
|
||||||
|
### Data Flow
|
||||||
|
|
||||||
|
**Query Mode:**
|
||||||
|
1. Parse command-line arguments (-scu, -scn, -scp for source)
|
||||||
|
2. Read SQL query from file specified by -sq flag
|
||||||
|
3. Connect to source database via JDBC
|
||||||
|
4. Execute source query and fetch results (fetch size: 10,000 rows)
|
||||||
|
5. Output results to stdout in CSV or TSV format
|
||||||
|
6. Close connection and exit
|
||||||
|
|
||||||
|
**Migration Mode:**
|
||||||
|
1. Parse command-line arguments (-scu, -scn, -scp for source; -dcu, -dcn, -dcp for destination)
|
||||||
|
2. Read SQL query from file specified by -sq flag
|
||||||
|
3. Connect to source and destination databases via JDBC
|
||||||
|
4. Execute source query and fetch results (fetch size: 10,000 rows)
|
||||||
|
5. Optionally clear target table before insert if -c flag is set
|
||||||
|
6. With `-b`: bulk-load via the dest's native path (SQL Server → `SQLServerBulkCopy`,
|
||||||
|
Postgres → `COPY FROM STDIN`). Otherwise: build batched INSERT statements
|
||||||
|
(250 rows per batch) and execute them against the destination table (-dt)
|
||||||
|
|
||||||
|
### Type Handling
|
||||||
|
The tool includes explicit handling for different SQL data types in a switch statement (migration mode). Quoted string types: VARCHAR/NVARCHAR, TEXT/NTEXT, CHAR/NCHAR, CLOB/NCLOB, and the PostgreSQL string-ish types JSON, JSONB, BPCHAR (PG `char(n)`), and UUID. Date/time types (DATE, TIME, TIMESTAMP/DATETIME variants) are also quoted. String types get quote escaping (`'` → `''`) and optional trimming.
|
||||||
|
|
||||||
|
**Caveat — the `default` case emits values UNQUOTED** (correct for numerics like INT*/NUMERIC, which is why they're not listed). Any *string-typed* column whose JDBC type name isn't in the switch falls here and breaks the generated INSERT with a syntax error (e.g. PostgreSQL `bool` → `'t'`/`'f'` is currently unhandled). When adding a new source type, decide: numeric → leave to default; anything string-like → add a quoted case. A more robust future fix is to flip the default to quote-as-string with an explicit numeric allowlist.
|
||||||
|
|
||||||
|
### Database Drivers
|
||||||
|
JDBC drivers are configured in `jrunner/build.gradle`:
|
||||||
|
- PostgreSQL: org.postgresql:postgresql:42.5.0
|
||||||
|
- IBM AS/400 (JT400): net.sf.jt400:jt400:11.0
|
||||||
|
- Microsoft SQL Server: com.microsoft.sqlserver:mssql-jdbc:9.2.0.jre8
|
||||||
|
- SQL Server Integrated Auth: com.microsoft.sqlserver:mssql-jdbc_auth:9.2.0.x64
|
||||||
|
|
||||||
|
The AS/400 driver requires explicit Class.forName() registration (line 144).
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
The project uses a YAML configuration format (run.yml) to specify database connections, SQL script paths, and runtime options. However, the main application currently uses command-line arguments instead of parsing this YAML file.
|
||||||
|
|
||||||
|
Command-line flags:
|
||||||
|
- `-scu` - source JDBC URL
|
||||||
|
- `-scn` - source username
|
||||||
|
- `-scp` - source password
|
||||||
|
- `-dcu` - destination JDBC URL (migration mode only)
|
||||||
|
- `-dcn` - destination username (migration mode only)
|
||||||
|
- `-dcp` - destination password (migration mode only)
|
||||||
|
- `-sq` - path to source SQL query file
|
||||||
|
- `-dt` - fully qualified destination table name (migration mode only)
|
||||||
|
- `-t` - trim text fields (default: true)
|
||||||
|
- `-c` - clear target table before insert (default: true, migration mode only)
|
||||||
|
- `-b` - bulk load into dest (migration mode): SQL Server via SQLServerBulkCopy, Postgres via COPY
|
||||||
|
- `-f` - output format: csv, tsv (query mode only, default: csv)
|
||||||
|
|
||||||
|
## Key Implementation Details
|
||||||
|
|
||||||
|
### Mode Detection
|
||||||
|
Query mode is automatically detected at runtime (line 131) by checking if all destination flags (dcu, dcn, dcp, dt) are empty. This allows seamless switching between query and migration modes without explicit mode flags.
|
||||||
|
|
||||||
|
### Query Mode Output (v1.1+)
|
||||||
|
Query mode uses dedicated output methods:
|
||||||
|
- `outputQueryResults()` - Dispatches to format-specific methods
|
||||||
|
- `outputCSV()` - RFC 4180 compliant CSV with proper quote escaping
|
||||||
|
- `outputTSV()` - Tab-separated with tabs/newlines replaced by spaces
|
||||||
|
- All output goes to stdout; no diagnostic messages in query mode
|
||||||
|
- Helper methods: `escapeCSV()` and `escapeTSV()` for proper formatting
|
||||||
|
|
||||||
|
### Memory and Streaming Architecture
|
||||||
|
Both modes use a streaming architecture with no array storage of result rows:
|
||||||
|
|
||||||
|
**Query Mode Streaming:**
|
||||||
|
- Rows are pulled from the ResultSet via `rs.next()` one at a time
|
||||||
|
- Each row is immediately formatted and written to stdout
|
||||||
|
- No accumulation in memory - pure streaming from database to stdout
|
||||||
|
- The only buffer is the JDBC driver's internal fetch buffer (10,000 rows)
|
||||||
|
|
||||||
|
**Migration Mode Streaming:**
|
||||||
|
- Rows are pulled from the ResultSet via `rs.next()` one at a time
|
||||||
|
- Each row is converted to a SQL VALUES clause string: `(val1,val2,val3)`
|
||||||
|
- VALUES clauses are accumulated into a single `sql` string variable
|
||||||
|
- When 250 rows accumulate, the string is prepended with `INSERT INTO {table} VALUES` and executed
|
||||||
|
- The `sql` string is cleared and accumulation starts again
|
||||||
|
- Only holds up to 250 rows worth of SQL text in memory at once
|
||||||
|
|
||||||
|
**JDBC Fetch Size:**
|
||||||
|
- Both modes set `stmt.setFetchSize(10000)` — a hint to fetch 10,000 rows at a time
|
||||||
|
- The application processes rows one at a time via `rs.next()`; the only buffer is the driver's fetch window
|
||||||
|
|
||||||
|
**⚠️ PostgreSQL requires autoCommit=false for fetchSize to take effect.** The PG JDBC driver IGNORES `setFetchSize` while autoCommit is true and instead loads the ENTIRE result set into memory (OOMs / GC-thrashes on large source tables). So in **migration mode** the source connection is set to `setAutoCommit(false)` right after connecting, which enables a server-side cursor and makes streaming actually stream. This is done **only in migration mode** — query mode leaves autoCommit at its default because callers run committed DDL/DML through query mode (e.g. external tools), and autoCommit=false would roll those statements back on connection close. (jt400/MSSQL drivers stream regardless, so only PG is affected.)
|
||||||
|
|
||||||
|
### Batch Size (Migration Mode)
|
||||||
|
INSERT statements are batched at 250 rows (hardcoded around line 356). Rows are streamed into a SQL string buffer as VALUES clauses. When 250 rows accumulate in the string, it is prepended with "INSERT INTO {table} VALUES" and executed, then the string is cleared.
|
||||||
|
|
||||||
|
### Error Handling
|
||||||
|
SQLException handling prints stack trace and exits immediately with System.exit(0). There is no transaction rollback or partial failure recovery.
|
||||||
|
|
||||||
|
### Performance Considerations
|
||||||
|
- Result set fetch size is set to 10,000 rows (line 190)
|
||||||
|
- Progress counter prints with carriage return for real-time updates (migration mode only)
|
||||||
|
- Timestamps captured at start and end for duration tracking (migration mode only)
|
||||||
|
- Query mode has no progress output to keep stdout clean for piping
|
||||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 Paul Trowbridge
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
@ -1,296 +0,0 @@
|
|||||||
package jrunner;
|
|
||||||
import java.sql.*;
|
|
||||||
import java.util.*;
|
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.nio.file.Path ;
|
|
||||||
import java.nio.file.Paths;
|
|
||||||
|
|
||||||
public class jrunner {
|
|
||||||
//static final String QUERY = "SELECT * from rlarp.osm LIMIT 100";
|
|
||||||
|
|
||||||
public static void main(String[] args) {
|
|
||||||
|
|
||||||
String scu = "";
|
|
||||||
String scn = "";
|
|
||||||
String scp = "";
|
|
||||||
String dcu = "";
|
|
||||||
String dcn = "";
|
|
||||||
String dcp = "";
|
|
||||||
String sq = "";
|
|
||||||
String dt = "";
|
|
||||||
Boolean trim = true;
|
|
||||||
Integer r = 0;
|
|
||||||
Integer t = 0;
|
|
||||||
String sql = "";
|
|
||||||
String nr = "";
|
|
||||||
String nc = "";
|
|
||||||
String nl = "\n";
|
|
||||||
String msg = "";
|
|
||||||
Connection scon = null;
|
|
||||||
Connection dcon = null;
|
|
||||||
Statement stmt = null;
|
|
||||||
Statement stmtd = null;
|
|
||||||
ResultSet rs = null;
|
|
||||||
String[] getv = null;
|
|
||||||
Integer cols = null;
|
|
||||||
String[] dtn = null;
|
|
||||||
|
|
||||||
msg = "jrunner version 0.32";
|
|
||||||
msg = msg + nl + "-scu source jdbc url";
|
|
||||||
msg = msg + nl + "-scn source username";
|
|
||||||
msg = msg + nl + "-scp source passowrd";
|
|
||||||
msg = msg + nl + "-dcu destination jdbc url";
|
|
||||||
msg = msg + nl + "-dcn destination username";
|
|
||||||
msg = msg + nl + "-dcp destination passowrd";
|
|
||||||
msg = msg + nl + "-sq path to source query";
|
|
||||||
msg = msg + nl + "-dt fully qualified name of destination table";
|
|
||||||
msg = msg + nl + "-t trim text";
|
|
||||||
msg = msg + nl + "--help info";
|
|
||||||
|
|
||||||
//---------------------------------------parse args into variables-------------------------------------------------
|
|
||||||
|
|
||||||
for (int i = 0; i < args.length; i = i +1 ){
|
|
||||||
switch (args[i]) {
|
|
||||||
//source connection string
|
|
||||||
case "-scu":
|
|
||||||
scu = args[i+1];
|
|
||||||
break;
|
|
||||||
//source username
|
|
||||||
case "-scn":
|
|
||||||
scn = args[i+1];
|
|
||||||
break;
|
|
||||||
//source password
|
|
||||||
case "-scp":
|
|
||||||
scp = args[i+1];
|
|
||||||
break;
|
|
||||||
//destination connection string
|
|
||||||
case "-dcu":
|
|
||||||
dcu = args[i+1];
|
|
||||||
break;
|
|
||||||
//destination username
|
|
||||||
case "-dcn":
|
|
||||||
dcn = args[i+1];
|
|
||||||
break;
|
|
||||||
//destination password
|
|
||||||
case "-dcp":
|
|
||||||
dcp = args[i+1];
|
|
||||||
break;
|
|
||||||
//source query path
|
|
||||||
case "-sq":
|
|
||||||
try {
|
|
||||||
//sq = Files.readAllLines(Paths.get(args[i+1]));
|
|
||||||
sq = Files.readString(Paths.get(args[i+1]));
|
|
||||||
}
|
|
||||||
catch (Exception e) {
|
|
||||||
//System.out.println(nl + "error reasing source sql file: " + printStackTrace());
|
|
||||||
e.printStackTrace();
|
|
||||||
System.exit(0);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
//destination table name
|
|
||||||
case "-dt":
|
|
||||||
dt = args[i+1];
|
|
||||||
break;
|
|
||||||
case "-t":
|
|
||||||
trim = true;
|
|
||||||
break;
|
|
||||||
case "-v":
|
|
||||||
System.out.println(msg);
|
|
||||||
return;
|
|
||||||
case "--version":
|
|
||||||
System.out.println(msg);
|
|
||||||
return;
|
|
||||||
case "--help":
|
|
||||||
System.out.println(msg);
|
|
||||||
return;
|
|
||||||
case "-help":
|
|
||||||
System.out.println(msg);
|
|
||||||
return;
|
|
||||||
case "-h":
|
|
||||||
System.out.println(msg);
|
|
||||||
return;
|
|
||||||
case "\\?":
|
|
||||||
System.out.println(msg);
|
|
||||||
return;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
System.out.println(scu);
|
|
||||||
System.out.println(scn);
|
|
||||||
System.out.println(dcu);
|
|
||||||
System.out.println(dcn);
|
|
||||||
System.out.println(sq);
|
|
||||||
System.out.println(dt);
|
|
||||||
|
|
||||||
//return;
|
|
||||||
|
|
||||||
//force regstration
|
|
||||||
try {
|
|
||||||
Class.forName("com.ibm.as400.access.AS400JDBCDriver");
|
|
||||||
} catch (ClassNotFoundException cnf) {
|
|
||||||
System.out.println("The AS400 JDBC driver did not load");
|
|
||||||
System.exit(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
//-------------------------------------------establish connections-------------------------------------------------
|
|
||||||
//source database
|
|
||||||
try {
|
|
||||||
scon = DriverManager.getConnection(scu, scn, scp);
|
|
||||||
} catch (SQLException e) {
|
|
||||||
System.out.println("issue connecting to source:");
|
|
||||||
e.printStackTrace();
|
|
||||||
System.exit(0);
|
|
||||||
}
|
|
||||||
//destination database
|
|
||||||
try {
|
|
||||||
dcon = DriverManager.getConnection(dcu, dcn, dcp);
|
|
||||||
} catch (SQLException e) {
|
|
||||||
System.out.println("issue connecting to desctination:");
|
|
||||||
e.printStackTrace();
|
|
||||||
System.exit(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
//----------------------------------------open resultset------------------------------------------------------------
|
|
||||||
try {
|
|
||||||
stmt = scon.createStatement();
|
|
||||||
rs = stmt.executeQuery(sq);
|
|
||||||
//while (rs.next()) {
|
|
||||||
// System.out.println(rs.getString("x"));
|
|
||||||
//}
|
|
||||||
} catch (SQLException e) {
|
|
||||||
System.out.println("issue retrieving rows from source:");
|
|
||||||
e.printStackTrace();
|
|
||||||
System.exit(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
//---------------------------------------build meta---------------------------------------------------------------
|
|
||||||
try {
|
|
||||||
cols = rs.getMetaData().getColumnCount();
|
|
||||||
System.out.println("number of cols: " + cols);
|
|
||||||
getv = new String[cols + 1];
|
|
||||||
dtn = new String[cols + 1];
|
|
||||||
} catch (SQLException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
System.exit(0);
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
for (int i = 1; i <= cols; i++){
|
|
||||||
dtn[i] = rs.getMetaData().getColumnTypeName(i);
|
|
||||||
System.out.println(rs.getMetaData().getColumnName(i) + ": " + dtn[i]);
|
|
||||||
}
|
|
||||||
} catch (SQLException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
System.exit(0);
|
|
||||||
}
|
|
||||||
//-------------------------------build & execute sql-------------------------------------------------------------
|
|
||||||
try {
|
|
||||||
while (rs.next()) {
|
|
||||||
r++;
|
|
||||||
t++;
|
|
||||||
nr = "";
|
|
||||||
for (int i = 1; i <= cols; i++){
|
|
||||||
nc = rs.getString(i);
|
|
||||||
if (dtn[i] == "DATE" && nc == "null") {
|
|
||||||
nc = "NULL";
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
if (rs.wasNull()) {
|
|
||||||
nc = "NULL";
|
|
||||||
} else {
|
|
||||||
switch (dtn[i]){
|
|
||||||
case "VARCHAR":
|
|
||||||
nc = rs.getString(i).replace("'","''");
|
|
||||||
if (trim) { nc = nc.trim();}
|
|
||||||
nc = "'" + nc + "'";
|
|
||||||
break;
|
|
||||||
case "CLOB":
|
|
||||||
nc = rs.getString(i).replace("'","''");
|
|
||||||
if (trim) { nc = nc.trim();}
|
|
||||||
nc = "'" + nc + "'";
|
|
||||||
break;
|
|
||||||
case "CHAR":
|
|
||||||
nc = rs.getString(i).replace("'","''");
|
|
||||||
if (trim) { nc = nc.trim();}
|
|
||||||
nc = "'" + nc + "'";
|
|
||||||
break;
|
|
||||||
case "DATE":
|
|
||||||
nc = "'" + rs.getString(i) + "'";
|
|
||||||
if (nc == "'1/1/0001 12:00:00 AM'") {
|
|
||||||
nc = "NULL";
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case "TIME":
|
|
||||||
nc = "'" + rs.getString(i).replace("'","''") + "'";
|
|
||||||
break;
|
|
||||||
case "BIGINT":
|
|
||||||
nc = rs.getString(i);
|
|
||||||
default:
|
|
||||||
if (rs.getString(i) != "") {
|
|
||||||
nc = rs.getString(i);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
nc = "NULL";
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (i != 1){
|
|
||||||
nr = nr + ",";
|
|
||||||
}
|
|
||||||
nr = nr + nc;
|
|
||||||
}
|
|
||||||
//add a comma to the end of the VALUES block to accomodate a new row
|
|
||||||
if (sql!="") {
|
|
||||||
sql = sql + ",";
|
|
||||||
}
|
|
||||||
//add the new row to the VALUES block
|
|
||||||
sql = sql + "(" + nr + ")";
|
|
||||||
if (r == 250){
|
|
||||||
r = 0;
|
|
||||||
sql = "INSERT INTO " + dt + " VALUES " + "\n" + sql;
|
|
||||||
//System.out.println(sql);
|
|
||||||
try {
|
|
||||||
stmtd = dcon.createStatement();
|
|
||||||
stmtd.executeUpdate(sql);
|
|
||||||
System.out.print("\r" + t);
|
|
||||||
} catch (SQLException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
System.out.println(sql);
|
|
||||||
System.exit(0);
|
|
||||||
}
|
|
||||||
sql = "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
//if the sql is not empty, execute
|
|
||||||
if (sql != "") {
|
|
||||||
sql = "INSERT INTO " + dt + " VALUES " + "\n" + sql;
|
|
||||||
try {
|
|
||||||
stmtd = dcon.createStatement();
|
|
||||||
stmtd.executeUpdate(sql);
|
|
||||||
System.out.print("\r" + t);
|
|
||||||
} catch (SQLException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
System.out.println(sql);
|
|
||||||
System.exit(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (SQLException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
System.exit(0);
|
|
||||||
}
|
|
||||||
//System.out.println(sql);
|
|
||||||
|
|
||||||
//---------------------------------------close connections--------------------------------------------------------
|
|
||||||
try {
|
|
||||||
scon.close();
|
|
||||||
dcon.close();
|
|
||||||
} catch (SQLException e) {
|
|
||||||
System.out.println("issue closing connections");
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
129
deploy.sh
Executable file
129
deploy.sh
Executable file
@ -0,0 +1,129 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "jrunner deployment script"
|
||||||
|
echo "========================="
|
||||||
|
echo ""
|
||||||
|
echo "Select deployment option:"
|
||||||
|
echo " 1) Local install (./jrunner/build/install)"
|
||||||
|
echo " 2) Global install (installDist + /usr/local/bin symlinks)"
|
||||||
|
echo " 3) Custom directory"
|
||||||
|
echo ""
|
||||||
|
read -p "Enter choice [1-3]: " choice
|
||||||
|
|
||||||
|
case $choice in
|
||||||
|
1)
|
||||||
|
DEPLOY_MODE="local"
|
||||||
|
DEPLOY_DIR="./jrunner/build/install/jrunner"
|
||||||
|
;;
|
||||||
|
2)
|
||||||
|
DEPLOY_MODE="global"
|
||||||
|
DEPLOY_DIR="/opt/jrunner"
|
||||||
|
;;
|
||||||
|
3)
|
||||||
|
DEPLOY_MODE="custom"
|
||||||
|
read -e -p "Enter deployment directory (required): " DEPLOY_DIR
|
||||||
|
if [ -z "$DEPLOY_DIR" ]; then
|
||||||
|
echo "Error: Directory path is required"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
# Expand tilde to home directory
|
||||||
|
DEPLOY_DIR="${DEPLOY_DIR/#\~/$HOME}"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Error: Invalid choice"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Prevent deleting critical system directories
|
||||||
|
case "${DEPLOY_DIR}" in
|
||||||
|
/|/bin|/boot|/dev|/etc|/lib|/lib64|/proc|/root|/run|/sbin|/sys|/usr|/var)
|
||||||
|
echo "Error: Cannot deploy to system directory: ${DEPLOY_DIR}"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Building jrunner..."
|
||||||
|
./gradlew build
|
||||||
|
|
||||||
|
if [ "$DEPLOY_MODE" = "local" ]; then
|
||||||
|
echo "Installing locally with gradle..."
|
||||||
|
./gradlew installDist
|
||||||
|
echo "Installing jrq wrapper script..."
|
||||||
|
cp jrq "${DEPLOY_DIR}/bin/jrq"
|
||||||
|
chmod +x "${DEPLOY_DIR}/bin/jrq"
|
||||||
|
echo ""
|
||||||
|
echo "✅ Installed locally at: ${DEPLOY_DIR}"
|
||||||
|
echo "Run './jrunner/build/install/jrunner/bin/jrunner --help' to test"
|
||||||
|
echo "Run './jrunner/build/install/jrunner/bin/jrq' for query wrapper usage"
|
||||||
|
elif [ "$DEPLOY_MODE" = "global" ]; then
|
||||||
|
INSTALL_DIR="$(pwd)/jrunner/build/install/jrunner"
|
||||||
|
echo "Installing with gradle..."
|
||||||
|
./gradlew installDist
|
||||||
|
echo "Installing jrq wrapper script..."
|
||||||
|
cp jrq "${INSTALL_DIR}/bin/jrq"
|
||||||
|
chmod +x "${INSTALL_DIR}/bin/jrq"
|
||||||
|
echo "Creating symlinks at /usr/local/bin/..."
|
||||||
|
sudo ln -sf "${INSTALL_DIR}/bin/jrunner" /usr/local/bin/jrunner
|
||||||
|
sudo ln -sf "${INSTALL_DIR}/bin/jrq" /usr/local/bin/jrq
|
||||||
|
echo ""
|
||||||
|
echo "✅ Installed at: ${INSTALL_DIR}"
|
||||||
|
echo "Symlinked to /usr/local/bin/ — run 'jrunner --help' to test"
|
||||||
|
echo " run 'jrq' for query wrapper usage"
|
||||||
|
else
|
||||||
|
# Custom deployment
|
||||||
|
if [ ! -d "${DEPLOY_DIR}" ]; then
|
||||||
|
echo "Creating directory: ${DEPLOY_DIR}"
|
||||||
|
sudo mkdir -p "${DEPLOY_DIR}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Extracting to temporary location..."
|
||||||
|
sudo rm -rf /tmp/jrunner-deploy
|
||||||
|
sudo unzip -q jrunner/build/distributions/jrunner.zip -d /tmp/jrunner-deploy
|
||||||
|
|
||||||
|
echo "Deploying to ${DEPLOY_DIR}..."
|
||||||
|
sudo rm -rf "${DEPLOY_DIR}"/*
|
||||||
|
sudo mv /tmp/jrunner-deploy/jrunner/* "${DEPLOY_DIR}"/
|
||||||
|
sudo rm -rf /tmp/jrunner-deploy
|
||||||
|
|
||||||
|
echo "Fixing ownership..."
|
||||||
|
sudo chown -R $USER:$USER "${DEPLOY_DIR}"
|
||||||
|
|
||||||
|
# Copy jrq wrapper script
|
||||||
|
echo "Installing jrq wrapper script..."
|
||||||
|
sudo cp jrq "${DEPLOY_DIR}/bin/jrq"
|
||||||
|
sudo chmod +x "${DEPLOY_DIR}/bin/jrq"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "✅ Deployed to ${DEPLOY_DIR}"
|
||||||
|
echo "Run '${DEPLOY_DIR}/bin/jrunner --help' to test"
|
||||||
|
echo "Run '${DEPLOY_DIR}/bin/jrq' for query wrapper usage"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create ~/.jrqrc template if it doesn't exist
|
||||||
|
if [ ! -f "${HOME}/.jrqrc" ]; then
|
||||||
|
echo "Creating ~/.jrqrc config template..."
|
||||||
|
cat > "${HOME}/.jrqrc" <<'EOF'
|
||||||
|
# jrq configuration
|
||||||
|
# Uncomment and fill in the values below
|
||||||
|
|
||||||
|
# JDBC connection URL (required)
|
||||||
|
# Examples:
|
||||||
|
# AS/400: jdbc:as400://hostname
|
||||||
|
# PostgreSQL: jdbc:postgresql://hostname:5432/dbname
|
||||||
|
# SQL Server: jdbc:sqlserver://hostname:1433;databaseName=mydb
|
||||||
|
#JR_URL=""
|
||||||
|
|
||||||
|
# Database credentials (required)
|
||||||
|
#JR_USER=""
|
||||||
|
#JR_PASS=""
|
||||||
|
|
||||||
|
# Output format: csv or tsv (default: csv)
|
||||||
|
#JR_FORMAT="csv"
|
||||||
|
EOF
|
||||||
|
echo "Edit ~/.jrqrc to add your connection details before using jrq"
|
||||||
|
else
|
||||||
|
echo "~/.jrqrc already exists, skipping template creation"
|
||||||
|
fi
|
||||||
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@ -1,5 +1,5 @@
|
|||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
|
|||||||
66
jrq
Executable file
66
jrq
Executable file
@ -0,0 +1,66 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# jrq - jrunner query wrapper for piping to visidata
|
||||||
|
# Usage: jrq <sql_file> [format]
|
||||||
|
# Example: jrq query.sql
|
||||||
|
# jrq query.sql tsv
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
# Override these with environment variables or create ~/.jrqrc
|
||||||
|
CONFIG_FILE="${HOME}/.jrqrc"
|
||||||
|
|
||||||
|
# Default values
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
JR_BIN="${JR_BIN:-${SCRIPT_DIR}/jrunner}"
|
||||||
|
JR_URL="${JR_URL:-}"
|
||||||
|
JR_USER="${JR_USER:-}"
|
||||||
|
JR_PASS="${JR_PASS:-}"
|
||||||
|
JR_FORMAT="${JR_FORMAT:-csv}"
|
||||||
|
|
||||||
|
# Load config file if it exists
|
||||||
|
if [ -f "$CONFIG_FILE" ]; then
|
||||||
|
source "$CONFIG_FILE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Parse arguments
|
||||||
|
if [ $# -lt 1 ]; then
|
||||||
|
echo "Usage: jrq <sql_file> [format]"
|
||||||
|
echo ""
|
||||||
|
echo "Configuration (via environment variables or ~/.jrqrc):"
|
||||||
|
echo " JR_URL - JDBC connection URL (required)"
|
||||||
|
echo " JR_USER - Database username (required)"
|
||||||
|
echo " JR_PASS - Database password (required)"
|
||||||
|
echo " JR_BIN - Path to jrunner binary (default: /opt/jrunner/jrunner/build/install/jrunner/bin/jrunner)"
|
||||||
|
echo " JR_FORMAT - Output format: csv or tsv (default: csv)"
|
||||||
|
echo ""
|
||||||
|
echo "Example ~/.jrqrc:"
|
||||||
|
echo " JR_URL=\"jdbc:as400://s7830956\""
|
||||||
|
echo " JR_USER=\"username\""
|
||||||
|
echo " JR_PASS=\"password\""
|
||||||
|
echo ""
|
||||||
|
echo "Example usage:"
|
||||||
|
echo " jrq query.sql # outputs CSV to stdout"
|
||||||
|
echo " jrq query.sql | vd # pipe to visidata"
|
||||||
|
echo " jrq query.sql tsv # output TSV format"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
SQL_FILE="$1"
|
||||||
|
FORMAT="${2:-$JR_FORMAT}"
|
||||||
|
|
||||||
|
# Validate configuration
|
||||||
|
if [ -z "$JR_URL" ] || [ -z "$JR_USER" ] || [ -z "$JR_PASS" ]; then
|
||||||
|
echo "Error: Missing database connection configuration" >&2
|
||||||
|
echo "Set JR_URL, JR_USER, and JR_PASS environment variables or create ~/.jrqrc" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Validate SQL file exists
|
||||||
|
if [ ! -f "$SQL_FILE" ]; then
|
||||||
|
echo "Error: SQL file not found: $SQL_FILE" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Execute jrunner
|
||||||
|
"$JR_BIN" -scu "$JR_URL" -scn "$JR_USER" -scp "$JR_PASS" -sq "$SQL_FILE" -f "$FORMAT"
|
||||||
@ -26,6 +26,8 @@ dependencies {
|
|||||||
//jdbc drivers
|
//jdbc drivers
|
||||||
implementation 'org.postgresql:postgresql:42.5.0'
|
implementation 'org.postgresql:postgresql:42.5.0'
|
||||||
implementation 'net.sf.jt400:jt400:11.0'
|
implementation 'net.sf.jt400:jt400:11.0'
|
||||||
|
implementation 'com.microsoft.sqlserver:mssql-jdbc:9.2.0.jre8'
|
||||||
|
implementation 'com.microsoft.sqlserver:mssql-jdbc_auth:9.2.0.x64'
|
||||||
}
|
}
|
||||||
|
|
||||||
application {
|
application {
|
||||||
1000
jrunner/src/main/java/jrunner/jrunner.java
Normal file
1000
jrunner/src/main/java/jrunner/jrunner.java
Normal file
File diff suppressed because it is too large
Load Diff
194
readme.md
Normal file
194
readme.md
Normal file
@ -0,0 +1,194 @@
|
|||||||
|
## Quick Start
|
||||||
|
|
||||||
|
The easiest way to get started on a new system:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://gitea.hptrow.me/pt/jrunner.git
|
||||||
|
cd jrunner
|
||||||
|
./setup.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
The setup script will:
|
||||||
|
- Check for Java 11+ (offers to install if missing)
|
||||||
|
- Verify Gradle wrapper is present
|
||||||
|
- Run a test build to ensure everything works
|
||||||
|
- Show you next steps
|
||||||
|
|
||||||
|
## Manual Installation
|
||||||
|
|
||||||
|
If you prefer to install dependencies manually:
|
||||||
|
|
||||||
|
### Install Java JDK
|
||||||
|
|
||||||
|
**Option 1: Package manager (recommended)**
|
||||||
|
```bash
|
||||||
|
# Ubuntu/Debian
|
||||||
|
sudo apt update && sudo apt install openjdk-17-jdk
|
||||||
|
|
||||||
|
# Fedora/RHEL
|
||||||
|
sudo dnf install java-17-openjdk-devel
|
||||||
|
|
||||||
|
# Arch
|
||||||
|
sudo pacman -S jdk-openjdk
|
||||||
|
```
|
||||||
|
|
||||||
|
**Option 2: Manual download**
|
||||||
|
Download from https://www.oracle.com/java/technologies/downloads/
|
||||||
|
|
||||||
|
```bash
|
||||||
|
wget https://download.oracle.com/java/19/latest/jdk-19_linux-x64_bin.tar.gz
|
||||||
|
tar -xvf jdk-19_linux-x64_bin.tar.gz
|
||||||
|
sudo mv jdk-19.0.1 /opt/
|
||||||
|
export JAVA_HOME=/opt/jdk-19.0.1
|
||||||
|
export PATH=$PATH:$JAVA_HOME/bin
|
||||||
|
```
|
||||||
|
|
||||||
|
Test: `java --version`
|
||||||
|
|
||||||
|
### Install Gradle (optional)
|
||||||
|
Gradle wrapper (`gradlew`) is included in the repo, so manual Gradle installation is not required.
|
||||||
|
|
||||||
|
## build
|
||||||
|
```
|
||||||
|
./gradlew build
|
||||||
|
```
|
||||||
|
|
||||||
|
## deploy
|
||||||
|
|
||||||
|
### using the deploy script (recommended)
|
||||||
|
|
||||||
|
Run the interactive deploy script:
|
||||||
|
```
|
||||||
|
./deploy.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
The script will prompt you to choose:
|
||||||
|
1. **Local install** - Fast, no sudo required, installs to `./jrunner/build/install/jrunner`
|
||||||
|
2. **Global install** - Installs to `/opt/jrunner` with symlink at `/usr/local/bin/jrunner`
|
||||||
|
3. **Custom directory** - Prompts for path with tab-completion support
|
||||||
|
|
||||||
|
The script builds, extracts to a temporary location, and only updates the target directory after the build succeeds. This ensures your existing deployment stays intact if the build fails.
|
||||||
|
|
||||||
|
### manual deployment
|
||||||
|
```
|
||||||
|
./gradlew build
|
||||||
|
sudo unzip jrunner/build/distributions/jrunner.zip -d /opt/
|
||||||
|
sudo ln -sf /opt/jrunner/bin/jrunner /usr/local/bin/jrunner
|
||||||
|
```
|
||||||
|
|
||||||
|
Or for local testing:
|
||||||
|
```
|
||||||
|
./gradlew installDist
|
||||||
|
# Binary at: ./jrunner/build/install/jrunner/bin/jrunner
|
||||||
|
```
|
||||||
|
|
||||||
|
## usage
|
||||||
|
|
||||||
|
### Query Mode (new in v1.1)
|
||||||
|
|
||||||
|
Query mode outputs results to stdout for piping to visidata, pspg, or less. It activates automatically when destination flags are omitted.
|
||||||
|
|
||||||
|
**Basic query to CSV:**
|
||||||
|
```bash
|
||||||
|
jrunner -scu "jdbc:as400://hostname" -scn user -scp pass -sq query.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
**Pipe to visidata:**
|
||||||
|
```bash
|
||||||
|
jrunner -scu "jdbc:as400://hostname" -scn user -scp pass -sq query.sql | visidata -f csv
|
||||||
|
```
|
||||||
|
|
||||||
|
**TSV format:**
|
||||||
|
```bash
|
||||||
|
jrunner -scu "jdbc:as400://hostname" -scn user -scp pass -sq query.sql -f tsv
|
||||||
|
```
|
||||||
|
|
||||||
|
**SQL Server example:**
|
||||||
|
```bash
|
||||||
|
jrunner -scu "jdbc:sqlserver://hostname:1433;databaseName=mydb" -scn user -scp pass -sq query.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
**PostgreSQL example:**
|
||||||
|
```bash
|
||||||
|
jrunner -scu "jdbc:postgresql://hostname:5432/dbname" -scn user -scp pass -sq query.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
### jrq - Query Mode Wrapper
|
||||||
|
|
||||||
|
The `jrq` wrapper script simplifies query mode usage by storing connection details in a config file, eliminating the need to type credentials repeatedly.
|
||||||
|
|
||||||
|
**Setup:**
|
||||||
|
|
||||||
|
Create `~/.jrqrc` with your database connection details:
|
||||||
|
```bash
|
||||||
|
JR_URL="jdbc:as400://s7830956"
|
||||||
|
JR_USER="myusername"
|
||||||
|
JR_PASS="mypassword"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Usage:**
|
||||||
|
```bash
|
||||||
|
# Output to stdout
|
||||||
|
jrq query.sql
|
||||||
|
|
||||||
|
# Pipe to visidata
|
||||||
|
jrq query.sql | vd
|
||||||
|
|
||||||
|
# Use TSV format
|
||||||
|
jrq query.sql tsv
|
||||||
|
|
||||||
|
# Pipe TSV to less with column display
|
||||||
|
jrq query.sql tsv | less -S
|
||||||
|
```
|
||||||
|
|
||||||
|
**Configuration options in ~/.jrqrc:**
|
||||||
|
- `JR_URL` - JDBC connection URL (required)
|
||||||
|
- `JR_USER` - Database username (required)
|
||||||
|
- `JR_PASS` - Database password (required)
|
||||||
|
- `JR_BIN` - Path to jrunner binary (default: /opt/jrunner/jrunner/build/install/jrunner/bin/jrunner)
|
||||||
|
- `JR_FORMAT` - Default output format: csv or tsv (default: csv)
|
||||||
|
|
||||||
|
**Note:** You can also set these as environment variables instead of using a config file:
|
||||||
|
```bash
|
||||||
|
export JR_URL="jdbc:as400://hostname"
|
||||||
|
export JR_USER="username"
|
||||||
|
export JR_PASS="password"
|
||||||
|
jrq query.sql | vd
|
||||||
|
```
|
||||||
|
|
||||||
|
### Migration Mode
|
||||||
|
|
||||||
|
Full migration mode with both source and destination:
|
||||||
|
```bash
|
||||||
|
jrunner -scu jdbc:postgresql://source:5432/sourcedb \
|
||||||
|
-scn sourceuser \
|
||||||
|
-scp sourcepass \
|
||||||
|
-dcu jdbc:postgresql://dest:5432/destdb \
|
||||||
|
-dcn destuser \
|
||||||
|
-dcp destpass \
|
||||||
|
-sq query.sql \
|
||||||
|
-dt public.target_table
|
||||||
|
```
|
||||||
|
|
||||||
|
### Command-line flags
|
||||||
|
|
||||||
|
**Source connection:**
|
||||||
|
- `-scu` - source JDBC URL
|
||||||
|
- `-scn` - source username
|
||||||
|
- `-scp` - source password
|
||||||
|
- `-sq` - path to source SQL query file
|
||||||
|
|
||||||
|
**Destination connection (migration mode only):**
|
||||||
|
- `-dcu` - destination JDBC URL
|
||||||
|
- `-dcn` - destination username
|
||||||
|
- `-dcp` - destination password
|
||||||
|
- `-dt` - fully qualified destination table name
|
||||||
|
|
||||||
|
**Options:**
|
||||||
|
- `-t` - trim text fields (default: true)
|
||||||
|
- `-c` - clear target table before insert (default: true)
|
||||||
|
- `-b` - bulk load into the destination instead of batched INSERTs — far faster
|
||||||
|
on large/wide tables. SQL Server: TDS bulk-load via SQLServerBulkCopy.
|
||||||
|
Postgres: COPY FROM STDIN. Other dests (e.g. DB2) fall back to INSERT.
|
||||||
|
(migration mode only)
|
||||||
|
- `-f` - output format: csv, tsv (query mode only, default: csv)
|
||||||
@ -8,4 +8,4 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
rootProject.name = 'jrunner'
|
rootProject.name = 'jrunner'
|
||||||
include('app')
|
include('jrunner')
|
||||||
|
|||||||
270
setup.sh
Executable file
270
setup.sh
Executable file
@ -0,0 +1,270 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "jrunner Development Environment Setup"
|
||||||
|
echo "====================================="
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Color codes for output
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
# Check if running on Linux
|
||||||
|
if [[ "$OSTYPE" != "linux-gnu"* ]]; then
|
||||||
|
echo -e "${YELLOW}Warning: This script is designed for Linux. You may need to manually install dependencies.${NC}"
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Java versions this project builds with. MAX_JAVA is capped by the Gradle
|
||||||
|
# wrapper -- gradle-8.5 refuses to run on anything newer than JDK 21. Raise it
|
||||||
|
# when you bump gradle/wrapper/gradle-wrapper.properties.
|
||||||
|
MIN_JAVA=11
|
||||||
|
MAX_JAVA=21
|
||||||
|
GRADLE_WRAPPER_VERSION=$(sed -n 's/.*gradle-\([0-9.]*\)-bin\.zip/\1/p' \
|
||||||
|
gradle/wrapper/gradle-wrapper.properties 2>/dev/null)
|
||||||
|
|
||||||
|
# Function to check if command exists
|
||||||
|
command_exists() {
|
||||||
|
command -v "$1" >/dev/null 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to get Java major version (8, 11, 17, ...); echoes 0 if undetectable
|
||||||
|
get_java_version() {
|
||||||
|
local line full major
|
||||||
|
if ! command_exists java; then
|
||||||
|
echo "0"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
# grep the version line rather than taking head -1: some JVMs prepend
|
||||||
|
# "Picked up _JAVA_OPTIONS: ..." when those env vars are set
|
||||||
|
line=$(java -version 2>&1 | grep -Ei '(openjdk|java|jre)[a-z ]* version' | head -n 1)
|
||||||
|
full=$(echo "$line" | awk -F '"' '{print $2}')
|
||||||
|
major=$(echo "$full" | awk -F '[._-]' '{print $1}')
|
||||||
|
# legacy "1.8.0_381" style -> 8
|
||||||
|
if [ "$major" = "1" ]; then
|
||||||
|
major=$(echo "$full" | awk -F '.' '{print $2}')
|
||||||
|
fi
|
||||||
|
case "$major" in
|
||||||
|
''|*[!0-9]*) major=0 ;;
|
||||||
|
esac
|
||||||
|
echo "$major"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Highest OpenJDK major version offered by the package manager, within
|
||||||
|
# [MIN_JAVA, MAX_JAVA]. Echoes nothing if it can't find one.
|
||||||
|
latest_installable_jdk() {
|
||||||
|
local versions=""
|
||||||
|
if command_exists apt-cache; then
|
||||||
|
versions=$(apt-cache pkgnames openjdk- 2>/dev/null \
|
||||||
|
| sed -n 's/^openjdk-\([0-9]\+\)-jdk$/\1/p')
|
||||||
|
elif command_exists dnf; then
|
||||||
|
versions=$(dnf -q list --available 'java-*-openjdk-devel' 2>/dev/null \
|
||||||
|
| sed -n 's/^java-1\?\.\?\([0-9]\+\)[^ ]*-openjdk-devel\..*/\1/p')
|
||||||
|
elif command_exists pacman; then
|
||||||
|
versions=$(pacman -Ssq '^jdk[0-9]+-openjdk$' 2>/dev/null \
|
||||||
|
| sed -n 's/^jdk\([0-9]\+\)-openjdk$/\1/p')
|
||||||
|
fi
|
||||||
|
echo "$versions" | awk -v min="$MIN_JAVA" -v max="$MAX_JAVA" \
|
||||||
|
'$1 ~ /^[0-9]+$/ && $1 >= min && $1 <= max && $1 > best { best = $1 }
|
||||||
|
END { if (best) print best }'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Install a given OpenJDK major version via the system package manager
|
||||||
|
install_jdk() {
|
||||||
|
local ver="$1"
|
||||||
|
if command_exists apt; then
|
||||||
|
sudo apt update && sudo apt install -y "openjdk-${ver}-jdk"
|
||||||
|
elif command_exists dnf; then
|
||||||
|
sudo dnf install -y "java-${ver}-openjdk-devel"
|
||||||
|
elif command_exists pacman; then
|
||||||
|
sudo pacman -S --noconfirm "jdk${ver}-openjdk"
|
||||||
|
else
|
||||||
|
echo -e "${RED}Could not detect package manager. Please install Java manually.${NC}"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# The JDK directory holding the java on PATH
|
||||||
|
java_home_from_path() {
|
||||||
|
local j
|
||||||
|
j=$(command -v java) || return 1
|
||||||
|
j=$(readlink -f "$j") || return 1
|
||||||
|
dirname "$(dirname "$j")"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Gradle honours JAVA_HOME ahead of PATH, so a JAVA_HOME left over from a
|
||||||
|
# hand-installed JDK breaks every build even when `java` itself works fine.
|
||||||
|
fix_java_home() {
|
||||||
|
local detected
|
||||||
|
detected=$(java_home_from_path) || return 0
|
||||||
|
[ -n "$JAVA_HOME" ] && [ -x "$JAVA_HOME/bin/java" ] && return 0
|
||||||
|
if [ -n "$JAVA_HOME" ]; then
|
||||||
|
echo -e "${YELLOW}!${NC} JAVA_HOME is set to ${JAVA_HOME}, which has no bin/java"
|
||||||
|
echo " Using ${detected} for this run. To fix it permanently, put this in"
|
||||||
|
echo " your shell profile (it follows update-alternatives, so it won't go stale):"
|
||||||
|
echo " export JAVA_HOME=\$(dirname \"\$(dirname \"\$(readlink -f \"\$(command -v java)\")\")\")"
|
||||||
|
fi
|
||||||
|
export JAVA_HOME="$detected"
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "Checking dependencies..."
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Check Java
|
||||||
|
JAVA_VERSION=$(get_java_version)
|
||||||
|
LATEST_JDK=$(latest_installable_jdk)
|
||||||
|
if [ "$JAVA_VERSION" -ge "$MIN_JAVA" ]; then
|
||||||
|
echo -e "${GREEN}✓${NC} Java $JAVA_VERSION detected"
|
||||||
|
java -version 2>&1 | grep -Ei '(openjdk|java|jre)[a-z ]* version' | head -n 1
|
||||||
|
JAVA_OK=true
|
||||||
|
else
|
||||||
|
echo -e "${RED}✗${NC} Java ${MIN_JAVA}+ not found"
|
||||||
|
JAVA_OK=false
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check Git
|
||||||
|
if command_exists git; then
|
||||||
|
echo -e "${GREEN}✓${NC} Git detected"
|
||||||
|
GIT_OK=true
|
||||||
|
else
|
||||||
|
echo -e "${RED}✗${NC} Git not found"
|
||||||
|
GIT_OK=false
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if gradlew exists
|
||||||
|
if [ -f "./gradlew" ]; then
|
||||||
|
echo -e "${GREEN}✓${NC} Gradle wrapper found (no need to install Gradle)"
|
||||||
|
GRADLE_OK=true
|
||||||
|
else
|
||||||
|
echo -e "${YELLOW}!${NC} Gradle wrapper not found in current directory"
|
||||||
|
GRADLE_OK=false
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Install the newest supported JDK, either because none is present or because
|
||||||
|
# the installed one is behind what the package manager offers
|
||||||
|
SUGGEST=${LATEST_JDK:-$MAX_JAVA}
|
||||||
|
if [ "$JAVA_OK" = false ]; then
|
||||||
|
echo -e "${YELLOW}Java ${MIN_JAVA}+ is required to build and run jrunner${NC}"
|
||||||
|
echo ""
|
||||||
|
echo "Installation options:"
|
||||||
|
echo ""
|
||||||
|
echo "1. Install OpenJDK via package manager (recommended):"
|
||||||
|
echo " Ubuntu/Debian: sudo apt update && sudo apt install openjdk-${SUGGEST}-jdk"
|
||||||
|
echo " Fedora/RHEL: sudo dnf install java-${SUGGEST}-openjdk-devel"
|
||||||
|
echo " Arch: sudo pacman -S jdk${SUGGEST}-openjdk"
|
||||||
|
echo ""
|
||||||
|
echo "2. Download a JDK tarball manually:"
|
||||||
|
echo " Visit: https://adoptium.net/temurin/releases/"
|
||||||
|
echo " Download JDK ${MIN_JAVA}-${MAX_JAVA} and extract to /opt"
|
||||||
|
echo " export JAVA_HOME=/opt/jdk-${SUGGEST} && export PATH=\$PATH:\$JAVA_HOME/bin"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
read -p "Install OpenJDK ${SUGGEST} via package manager? (requires sudo) [y/N]: " install_java || install_java=""
|
||||||
|
|
||||||
|
if [[ "$install_java" =~ ^[Yy]$ ]]; then
|
||||||
|
echo "Installing OpenJDK ${SUGGEST}..."
|
||||||
|
install_jdk "$SUGGEST" || exit 1
|
||||||
|
|
||||||
|
# a fresh install relocates java; re-derive JAVA_HOME from PATH
|
||||||
|
unset JAVA_HOME
|
||||||
|
JAVA_VERSION=$(get_java_version)
|
||||||
|
if [ "$JAVA_VERSION" -ge "$MIN_JAVA" ]; then
|
||||||
|
echo -e "${GREEN}✓ Java $JAVA_VERSION installed successfully${NC}"
|
||||||
|
JAVA_OK=true
|
||||||
|
else
|
||||||
|
echo -e "${RED}✗ Java installation failed${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo ""
|
||||||
|
echo "Please install Java manually and re-run this script."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
elif [ -n "$LATEST_JDK" ] && [ "$LATEST_JDK" -gt "$JAVA_VERSION" ]; then
|
||||||
|
echo -e "${YELLOW}!${NC} OpenJDK ${LATEST_JDK} is available (you are on ${JAVA_VERSION});"
|
||||||
|
echo " ${MAX_JAVA} is the newest the Gradle ${GRADLE_WRAPPER_VERSION:-8.5} wrapper can run on."
|
||||||
|
read -p "Upgrade to OpenJDK ${LATEST_JDK}? (requires sudo) [y/N]: " upgrade_java || upgrade_java=""
|
||||||
|
if [[ "$upgrade_java" =~ ^[Yy]$ ]]; then
|
||||||
|
echo "Installing OpenJDK ${LATEST_JDK}..."
|
||||||
|
if install_jdk "$LATEST_JDK"; then
|
||||||
|
unset JAVA_HOME
|
||||||
|
JAVA_VERSION=$(get_java_version)
|
||||||
|
echo -e "${GREEN}✓ Now on Java ${JAVA_VERSION}${NC}"
|
||||||
|
else
|
||||||
|
echo -e "${YELLOW}Upgrade failed; continuing with Java ${JAVA_VERSION}${NC}"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Point JAVA_HOME at a JDK that actually exists before invoking Gradle
|
||||||
|
fix_java_home
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Check for unzip (needed for deployment)
|
||||||
|
if ! command_exists unzip; then
|
||||||
|
echo -e "${YELLOW}Note: 'unzip' is recommended for deployment${NC}"
|
||||||
|
read -p "Install unzip? [y/N]: " install_unzip || install_unzip=""
|
||||||
|
if [[ "$install_unzip" =~ ^[Yy]$ ]]; then
|
||||||
|
if command_exists apt; then
|
||||||
|
sudo apt install -y unzip
|
||||||
|
elif command_exists dnf; then
|
||||||
|
sudo dnf install -y unzip
|
||||||
|
elif command_exists pacman; then
|
||||||
|
sudo pacman -S --noconfirm unzip
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
if [ "$GIT_OK" = false ]; then
|
||||||
|
echo -e "${RED}Error: git is required. Install it and re-run this script.${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo -e "${GREEN}All required dependencies are installed!${NC}"
|
||||||
|
echo " Java ${JAVA_VERSION} @ ${JAVA_HOME:-$(java_home_from_path)}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Verify we're in the right directory
|
||||||
|
if [ ! -f "./gradlew" ]; then
|
||||||
|
echo -e "${RED}Error: gradlew not found. Are you in the jrunner project directory?${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Make gradlew executable
|
||||||
|
chmod +x ./gradlew
|
||||||
|
|
||||||
|
echo "Testing build system..."
|
||||||
|
# keep the output: gradle's own message names the cause (bad JAVA_HOME, no
|
||||||
|
# network for the distribution download, ...) and a bare "failed" hides it
|
||||||
|
if GRADLE_OUT=$(./gradlew --version 2>&1); then
|
||||||
|
echo -e "${GREEN}✓${NC} Gradle wrapper working"
|
||||||
|
else
|
||||||
|
echo -e "${RED}✗${NC} Gradle wrapper failed"
|
||||||
|
echo "$GRADLE_OUT"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Running test build..."
|
||||||
|
if ./gradlew build; then
|
||||||
|
echo ""
|
||||||
|
echo -e "${GREEN}✓ Build successful!${NC}"
|
||||||
|
echo ""
|
||||||
|
echo "Next steps:"
|
||||||
|
echo " 1. Install locally for testing:"
|
||||||
|
echo " ./gradlew installDist"
|
||||||
|
echo " ./jrunner/build/install/jrunner/bin/jrunner --help"
|
||||||
|
echo ""
|
||||||
|
echo " 2. Or run the interactive deploy script:"
|
||||||
|
echo " ./deploy.sh"
|
||||||
|
echo ""
|
||||||
|
echo " 3. See readme.md for usage examples"
|
||||||
|
else
|
||||||
|
echo -e "${RED}✗ Build failed${NC}"
|
||||||
|
echo "Check the error messages above and try again."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
Loading…
Reference in New Issue
Block a user