Compare commits
55 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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
|
||||
|
||||
*.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
|
||||
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
|
||||
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
|
||||
implementation 'org.postgresql:postgresql:42.5.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 {
|
||||
803
jrunner/src/main/java/jrunner/jrunner.java
Normal file
803
jrunner/src/main/java/jrunner/jrunner.java
Normal file
@ -0,0 +1,803 @@
|
||||
package jrunner;
|
||||
import java.sql.*;
|
||||
import java.util.*;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path ;
|
||||
import java.nio.file.Paths;
|
||||
import java.time.*;
|
||||
import java.io.IOException;
|
||||
import com.microsoft.sqlserver.jdbc.SQLServerBulkCopy;
|
||||
import com.microsoft.sqlserver.jdbc.SQLServerBulkCopyOptions;
|
||||
import com.microsoft.sqlserver.jdbc.ISQLServerBulkData;
|
||||
import com.microsoft.sqlserver.jdbc.SQLServerException;
|
||||
import org.postgresql.PGConnection;
|
||||
import org.postgresql.copy.CopyManager;
|
||||
import org.postgresql.copy.CopyIn;
|
||||
|
||||
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 scAlias = "";
|
||||
String dcu = "";
|
||||
String dcn = "";
|
||||
String dcp = "";
|
||||
String dcAlias = "";
|
||||
String sq = "";
|
||||
String dt = "";
|
||||
Boolean trim = true;
|
||||
Boolean clear = true;
|
||||
Boolean bulk = false;
|
||||
Integer r = 0;
|
||||
Integer t = 0;
|
||||
String sql = "";
|
||||
String nr = "";
|
||||
String nc = "";
|
||||
String nl = "\n";
|
||||
Boolean queryMode = false;
|
||||
String outputFormat = "csv";
|
||||
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;
|
||||
Timestamp tsStart = null;
|
||||
Timestamp tsEnd = null;
|
||||
|
||||
msg = "jrunner version 1.2";
|
||||
msg = msg + nl + "-sc source connection alias (from ~/.jrunnerpass)";
|
||||
msg = msg + nl + "-scu source jdbc url";
|
||||
msg = msg + nl + "-scn source username";
|
||||
msg = msg + nl + "-scp source password";
|
||||
msg = msg + nl + "-dc destination connection alias (from ~/.jrunnerpass)";
|
||||
msg = msg + nl + "-dcu destination jdbc url";
|
||||
msg = msg + nl + "-dcn destination username";
|
||||
msg = msg + nl + "-dcp destination password";
|
||||
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 + "-c clear target table";
|
||||
msg = msg + nl + "-b bulk copy into destination (SQL Server dest only)";
|
||||
msg = msg + nl + "-f output format (csv, tsv, table, json) - default: csv";
|
||||
msg = msg + nl + "--help info";
|
||||
msg = msg + nl + "";
|
||||
msg = msg + nl + "~/.jrunnerpass format:";
|
||||
msg = msg + nl + " [alias]";
|
||||
msg = msg + nl + " url=jdbc:...";
|
||||
msg = msg + nl + " user=username";
|
||||
msg = msg + nl + " pass=password";
|
||||
|
||||
//---------------------------------------parse args into variables-------------------------------------------------
|
||||
|
||||
for (int i = 0; i < args.length; i = i +1 ){
|
||||
switch (args[i]) {
|
||||
//source connection alias
|
||||
case "-sc":
|
||||
scAlias = args[i+1];
|
||||
break;
|
||||
//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 alias
|
||||
case "-dc":
|
||||
dcAlias = 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 "-c":
|
||||
clear = true;
|
||||
break;
|
||||
case "-b":
|
||||
bulk = true;
|
||||
break;
|
||||
case "-f":
|
||||
outputFormat = args[i+1].toLowerCase();
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve connection aliases from ~/.jrunnerpass
|
||||
if (!scAlias.isEmpty() || !dcAlias.isEmpty()) {
|
||||
Map<String, String[]> connections = loadPassFile();
|
||||
if (!scAlias.isEmpty()) {
|
||||
String[] sc = connections.get(scAlias);
|
||||
if (sc == null) {
|
||||
System.err.println("Error: source alias '" + scAlias + "' not found in ~/.jrunnerpass");
|
||||
System.exit(1);
|
||||
}
|
||||
if (scu.isEmpty()) scu = sc[0];
|
||||
if (scn.isEmpty()) scn = sc[1];
|
||||
if (scp.isEmpty()) scp = sc[2];
|
||||
}
|
||||
if (!dcAlias.isEmpty()) {
|
||||
String[] dc = connections.get(dcAlias);
|
||||
if (dc == null) {
|
||||
System.err.println("Error: destination alias '" + dcAlias + "' not found in ~/.jrunnerpass");
|
||||
System.exit(1);
|
||||
}
|
||||
if (dcu.isEmpty()) dcu = dc[0];
|
||||
if (dcn.isEmpty()) dcn = dc[1];
|
||||
if (dcp.isEmpty()) dcp = dc[2];
|
||||
}
|
||||
}
|
||||
|
||||
// Detect query mode when destination flags are not provided
|
||||
queryMode = dcu.isEmpty() && dcn.isEmpty() && dcp.isEmpty() && dt.isEmpty();
|
||||
|
||||
if (!queryMode) {
|
||||
System.out.println("------------db info---------------------------------------");
|
||||
System.out.println("source db uri: " + scu);
|
||||
System.out.println("source db username: " + scn);
|
||||
System.out.println("destination db uri: " + dcu);
|
||||
System.out.println("destination username: " + dcn);
|
||||
System.out.println("------------source sql------------------------------------");
|
||||
System.out.println(sq);
|
||||
System.out.println("------------destination table-----------------------------");
|
||||
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
|
||||
if (!queryMode) {
|
||||
System.out.println("------------open database connections---------------------");
|
||||
}
|
||||
try {
|
||||
scon = DriverManager.getConnection(scu, scn, scp);
|
||||
// Migration mode only: PostgreSQL ignores setFetchSize unless autoCommit
|
||||
// is false — without this it buffers the ENTIRE result set into memory
|
||||
// (OOM on big tables). The migration source is read-only, so never
|
||||
// committing is harmless. Do NOT do this in query mode: callers run
|
||||
// committed DDL/DML through query mode, and autoCommit=false would roll
|
||||
// those statements back on connection close.
|
||||
if (!queryMode) {
|
||||
scon.setAutoCommit(false);
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
System.out.println("issue connecting to source:");
|
||||
e.printStackTrace();
|
||||
System.exit(0);
|
||||
}
|
||||
if (!queryMode) {
|
||||
System.out.println(" ✅ source database");
|
||||
}
|
||||
//destination database
|
||||
if (!queryMode) {
|
||||
try {
|
||||
dcon = DriverManager.getConnection(dcu, dcn, dcp);
|
||||
} catch (SQLException e) {
|
||||
System.out.println("issue connecting to destination:");
|
||||
e.printStackTrace();
|
||||
System.exit(0);
|
||||
}
|
||||
System.out.println(" ✅ destination database");
|
||||
}
|
||||
//----------------------------------------open resultset------------------------------------------------------------
|
||||
try {
|
||||
stmt = scon.createStatement();
|
||||
stmt.setFetchSize(10000);
|
||||
tsStart = Timestamp.from(Instant.now());
|
||||
if (!queryMode) {
|
||||
System.out.println(tsStart);
|
||||
}
|
||||
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();
|
||||
if (!queryMode) {
|
||||
System.out.println("------------source metadata-------------------------------");
|
||||
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);
|
||||
if (!queryMode) {
|
||||
System.out.println(" * " + rs.getMetaData().getColumnName(i) + ": " + dtn[i]);
|
||||
}
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
e.printStackTrace();
|
||||
System.exit(0);
|
||||
}
|
||||
//-------------------------clear the target table if requeted----------------------------------------------------
|
||||
if (!queryMode && clear) {
|
||||
System.out.println("------------clear target table----------------------------");
|
||||
sql = "DELETE FROM " + dt;
|
||||
try {
|
||||
stmtd = dcon.createStatement();
|
||||
System.out.println(" " + sql);
|
||||
stmtd.executeUpdate(sql);
|
||||
} catch (SQLException e) {
|
||||
e.printStackTrace();
|
||||
System.out.println(sql);
|
||||
System.exit(0);
|
||||
}
|
||||
}
|
||||
if (queryMode) {
|
||||
//-------------------------------output query results--------------------------------------------------------
|
||||
try {
|
||||
outputQueryResults(rs, cols, dtn, outputFormat);
|
||||
} catch (SQLException e) {
|
||||
e.printStackTrace();
|
||||
System.exit(0);
|
||||
}
|
||||
} else if (bulk && dcu.toLowerCase().startsWith("jdbc:sqlserver:")) {
|
||||
//-------------------------------bulk copy (SQL Server dest)-------------------------------------------------
|
||||
// Stream the source ResultSet straight into SQL Server over the TDS
|
||||
// bulk-load protocol — no per-row INSERT round trips. Source type
|
||||
// names map to JDBC types via BulkSource (string-ish -> NVARCHAR).
|
||||
System.out.println("------------bulk copy-------------------------------------");
|
||||
try {
|
||||
SQLServerBulkCopy bulkCopy = new SQLServerBulkCopy(dcon);
|
||||
bulkCopy.setDestinationTableName(dt);
|
||||
SQLServerBulkCopyOptions options = new SQLServerBulkCopyOptions();
|
||||
options.setBatchSize(10000);
|
||||
options.setBulkCopyTimeout(0);
|
||||
bulkCopy.setBulkCopyOptions(options);
|
||||
BulkSource src = new BulkSource(rs, cols, dtn, trim);
|
||||
bulkCopy.writeToServer(src);
|
||||
bulkCopy.close();
|
||||
// leading \r starts a fresh line so the count doesn't concatenate
|
||||
// onto the last progress tick; the trailing " rows written" makes
|
||||
// it parseable.
|
||||
System.out.print("\r" + src.rowsWritten());
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
System.exit(0);
|
||||
}
|
||||
} else if (bulk && dcu.toLowerCase().startsWith("jdbc:postgresql:")) {
|
||||
//-------------------------------bulk copy (COPY, Postgres dest)--------------------------------------------
|
||||
// Stream the source ResultSet into Postgres via COPY ... FROM STDIN.
|
||||
// COPY is text-based: each field is sent as CSV text and the server
|
||||
// parses it into the column type, so there's no per-type quoting.
|
||||
// Non-null values are always CSV-quoted; NULL is an empty unquoted
|
||||
// field; column order must match the dest (positional, as always).
|
||||
System.out.println("------------bulk copy (COPY)------------------------------");
|
||||
try {
|
||||
CopyManager cm = ((PGConnection) dcon).getCopyAPI();
|
||||
CopyIn cin = cm.copyIn("COPY " + dt + " FROM STDIN WITH (FORMAT csv)");
|
||||
StringBuilder buf = new StringBuilder();
|
||||
long rows = 0;
|
||||
while (rs.next()) {
|
||||
for (int i = 1; i <= cols; i++) {
|
||||
if (i > 1) { buf.append(','); }
|
||||
String val = rs.getString(i);
|
||||
if (!rs.wasNull() && val != null) {
|
||||
if (trim) { val = val.trim(); }
|
||||
buf.append('"').append(val.replace("\"", "\"\"")).append('"');
|
||||
}
|
||||
// else: empty field -> NULL
|
||||
}
|
||||
buf.append('\n');
|
||||
rows++;
|
||||
if (rows % 1000 == 0) {
|
||||
byte[] b = buf.toString().getBytes(java.nio.charset.StandardCharsets.UTF_8);
|
||||
cin.writeToCopy(b, 0, b.length);
|
||||
buf.setLength(0);
|
||||
if (rows % 10000 == 0) { System.out.print("\r" + rows); System.out.flush(); }
|
||||
}
|
||||
}
|
||||
if (buf.length() > 0) {
|
||||
byte[] b = buf.toString().getBytes(java.nio.charset.StandardCharsets.UTF_8);
|
||||
cin.writeToCopy(b, 0, b.length);
|
||||
}
|
||||
cin.endCopy();
|
||||
System.out.print("\r" + rows);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
System.exit(0);
|
||||
}
|
||||
} else {
|
||||
System.out.println("------------row count-------------------------------------");
|
||||
//-------------------------------build & execute sql-------------------------------------------------------------
|
||||
try {
|
||||
sql = "";
|
||||
while (rs.next()) {
|
||||
r++;
|
||||
t++;
|
||||
nr = "";
|
||||
for (int i = 1; i <= cols; i++){
|
||||
switch (dtn[i].toUpperCase()){
|
||||
// PG string-ish types that otherwise fall to the default
|
||||
// case and get emitted UNQUOTED (breaking the INSERT):
|
||||
// jsonb/json carry embedded quotes, bpchar is PG's char(n),
|
||||
// uuid is a quoted literal. Quote them like varchar.
|
||||
case "JSON":
|
||||
case "JSONB":
|
||||
case "BPCHAR":
|
||||
case "UUID":
|
||||
case "VARCHAR":
|
||||
case "NVARCHAR":
|
||||
nc = rs.getString(i);
|
||||
if (rs.wasNull() || nc == null) {
|
||||
nc = "NULL";
|
||||
break;
|
||||
}
|
||||
nc = nc.replaceAll("'","''");
|
||||
if (trim) { nc = nc.trim();}
|
||||
nc = "'" + nc + "'";
|
||||
break;
|
||||
case "TEXT":
|
||||
case "NTEXT":
|
||||
nc = rs.getString(i);
|
||||
if (rs.wasNull() || nc == null) {
|
||||
nc = "NULL";
|
||||
break;
|
||||
}
|
||||
nc = nc.replaceAll("'","''");
|
||||
if (trim) { nc = nc.trim();}
|
||||
nc = "'" + nc + "'";
|
||||
break;
|
||||
case "CHAR":
|
||||
case "NCHAR":
|
||||
nc = rs.getString(i);
|
||||
if (rs.wasNull() || nc == null) {
|
||||
nc = "NULL";
|
||||
break;
|
||||
}
|
||||
nc = nc.replaceAll("'","''");
|
||||
if (trim) { nc = nc.trim();}
|
||||
nc = "'" + nc + "'";
|
||||
break;
|
||||
case "CLOB":
|
||||
case "NCLOB":
|
||||
nc = rs.getString(i);
|
||||
if (rs.wasNull() || nc == null) {
|
||||
nc = "NULL";
|
||||
break;
|
||||
}
|
||||
nc = nc.replaceAll("'","''");
|
||||
if (trim) { nc = nc.trim();}
|
||||
nc = "'" + nc + "'";
|
||||
break;
|
||||
case "DATE":
|
||||
nc = rs.getString(i);
|
||||
if (rs.wasNull() || nc == null) {
|
||||
nc = "NULL";
|
||||
break;
|
||||
}
|
||||
nc = "'" + nc + "'";
|
||||
if (nc == "'1/1/0001 12:00:00 AM'") {
|
||||
nc = "NULL";
|
||||
}
|
||||
break;
|
||||
case "TIME":
|
||||
nc = rs.getString(i);
|
||||
if (rs.wasNull() || nc == null) {
|
||||
nc = "NULL";
|
||||
break;
|
||||
}
|
||||
nc.replaceAll("'","''");
|
||||
nc = "'" + nc + "'";
|
||||
break;
|
||||
case "TIMESTAMP":
|
||||
case "DATETIME":
|
||||
case "DATETIME2":
|
||||
case "SMALLDATETIME":
|
||||
case "DATETIMEOFFSET":
|
||||
nc = rs.getString(i);
|
||||
if (rs.wasNull() || nc == null) {
|
||||
nc = "NULL";
|
||||
break;
|
||||
}
|
||||
nc.replaceAll("'","''");
|
||||
nc = "'" + nc + "'";
|
||||
break;
|
||||
case "BIGINT":
|
||||
nc = rs.getString(i);
|
||||
if (rs.wasNull() || nc == null) {
|
||||
nc = "NULL";
|
||||
break;
|
||||
}
|
||||
default:
|
||||
nc = rs.getString(i);
|
||||
if (rs.wasNull() || nc == null) {
|
||||
nc = "NULL";
|
||||
break;
|
||||
}
|
||||
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();
|
||||
if (!queryMode && dcon != null) {
|
||||
dcon.close();
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
System.out.println("issue closing connections");
|
||||
e.printStackTrace();
|
||||
}
|
||||
if (!queryMode) {
|
||||
System.out.println(" rows written");
|
||||
tsEnd = Timestamp.from(Instant.now());
|
||||
System.out.println(tsStart);
|
||||
System.out.println(tsEnd);
|
||||
}
|
||||
//long time = Duration.between(tsStart, tsEnd).toMillis();
|
||||
//System.out.println("time elapsed: " + time);
|
||||
System.out.println();
|
||||
}
|
||||
|
||||
// Adapts a source ResultSet to SQLServerBulkCopy. Maps source type names to
|
||||
// JDBC types we control: string-ish PG types (text/varchar/char/bpchar/json/
|
||||
// jsonb/uuid/...) are declared NVARCHAR and read via getString — mirroring the
|
||||
// INSERT path's "quote it" choices and sidestepping unsupported types (jsonb
|
||||
// reports as OTHER, which writeToServer(ResultSet) can't handle directly).
|
||||
static class BulkSource implements ISQLServerBulkData {
|
||||
private final ResultSet rs;
|
||||
private final ResultSetMetaData md;
|
||||
private final int cols;
|
||||
private final boolean trim;
|
||||
private final int[] jdbcType;
|
||||
private final boolean[] asString;
|
||||
private long rows = 0;
|
||||
|
||||
BulkSource(ResultSet rs, int cols, String[] dtn, boolean trim) throws SQLException {
|
||||
this.rs = rs;
|
||||
this.cols = cols;
|
||||
this.trim = trim;
|
||||
this.md = rs.getMetaData();
|
||||
this.jdbcType = new int[cols + 1];
|
||||
this.asString = new boolean[cols + 1];
|
||||
for (int i = 1; i <= cols; i++) {
|
||||
String t = (dtn[i] == null ? "" : dtn[i].toUpperCase());
|
||||
switch (t) {
|
||||
case "INT2": case "SMALLINT":
|
||||
jdbcType[i] = Types.SMALLINT; break;
|
||||
case "INT4": case "INT": case "INTEGER": case "SERIAL":
|
||||
jdbcType[i] = Types.INTEGER; break;
|
||||
case "INT8": case "BIGINT": case "BIGSERIAL":
|
||||
jdbcType[i] = Types.BIGINT; break;
|
||||
// numeric/decimal: PG reports unconstrained numeric as
|
||||
// scale 0, which makes bulk copy round (123.45 -> 123). Send
|
||||
// the exact text instead and let SQL Server convert it into
|
||||
// the dest column losslessly.
|
||||
case "FLOAT4": case "REAL":
|
||||
jdbcType[i] = Types.REAL; break;
|
||||
case "FLOAT8": case "DOUBLE": case "DOUBLE PRECISION":
|
||||
jdbcType[i] = Types.DOUBLE; break;
|
||||
case "BOOL": case "BOOLEAN": case "BIT":
|
||||
jdbcType[i] = Types.BIT; break;
|
||||
case "DATE":
|
||||
jdbcType[i] = Types.DATE; break;
|
||||
case "TIME":
|
||||
jdbcType[i] = Types.TIME; break;
|
||||
case "TIMESTAMP": case "TIMESTAMPTZ":
|
||||
case "DATETIME": case "DATETIME2": case "SMALLDATETIME":
|
||||
jdbcType[i] = Types.TIMESTAMP; break;
|
||||
case "BYTEA": case "BINARY": case "VARBINARY":
|
||||
jdbcType[i] = Types.VARBINARY; break;
|
||||
default:
|
||||
jdbcType[i] = Types.LONGNVARCHAR;
|
||||
asString[i] = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public java.util.Set<Integer> getColumnOrdinals() {
|
||||
java.util.Set<Integer> ords = new java.util.TreeSet<Integer>();
|
||||
for (int i = 1; i <= cols; i++) { ords.add(i); }
|
||||
return ords;
|
||||
}
|
||||
|
||||
public String getColumnName(int column) {
|
||||
try { return md.getColumnName(column); }
|
||||
catch (SQLException e) { return "col" + column; }
|
||||
}
|
||||
|
||||
public int getColumnType(int column) { return jdbcType[column]; }
|
||||
|
||||
public int getPrecision(int column) {
|
||||
if (asString[column]) { return 0; } // LONGNVARCHAR -> nvarchar(max)
|
||||
try {
|
||||
int p = md.getPrecision(column);
|
||||
return (p > 0 ? p : 38);
|
||||
} catch (SQLException e) { return 38; }
|
||||
}
|
||||
|
||||
public int getScale(int column) {
|
||||
if (jdbcType[column] == Types.DECIMAL) {
|
||||
try { return md.getScale(column); }
|
||||
catch (SQLException e) { return 0; }
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
public boolean next() throws SQLServerException {
|
||||
try { return rs.next(); }
|
||||
catch (SQLException e) { throw new RuntimeException(e); }
|
||||
}
|
||||
|
||||
public long rowsWritten() { return rows; }
|
||||
|
||||
public Object[] getRowData() throws SQLServerException {
|
||||
rows++;
|
||||
// live progress: emit an in-place counter every 10k rows (the
|
||||
// caller pulls one row at a time, so this runs during the load).
|
||||
if (rows % 10000 == 0) { System.out.print("\r" + rows); System.out.flush(); }
|
||||
Object[] row = new Object[cols];
|
||||
try {
|
||||
for (int i = 1; i <= cols; i++) {
|
||||
Object v;
|
||||
switch (jdbcType[i]) {
|
||||
case Types.SMALLINT:
|
||||
case Types.INTEGER:
|
||||
case Types.BIGINT:
|
||||
case Types.REAL:
|
||||
case Types.DOUBLE: v = rs.getObject(i); break;
|
||||
case Types.DECIMAL: v = rs.getBigDecimal(i); break;
|
||||
case Types.BIT: v = rs.getBoolean(i); break;
|
||||
case Types.DATE: v = rs.getDate(i); break;
|
||||
case Types.TIME: v = rs.getTime(i); break;
|
||||
case Types.TIMESTAMP:v = rs.getTimestamp(i); break;
|
||||
case Types.VARBINARY:v = rs.getBytes(i); break;
|
||||
default: {
|
||||
String s = rs.getString(i);
|
||||
if (s != null && trim) { s = s.trim(); }
|
||||
v = s;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (rs.wasNull()) { v = null; }
|
||||
row[i - 1] = v;
|
||||
}
|
||||
} catch (SQLException e) { throw new RuntimeException(e); }
|
||||
return row;
|
||||
}
|
||||
}
|
||||
|
||||
private static void outputQueryResults(ResultSet rs, int cols, String[] dtn, String format) throws SQLException {
|
||||
switch (format) {
|
||||
case "csv":
|
||||
outputCSV(rs, cols);
|
||||
break;
|
||||
case "tsv":
|
||||
outputTSV(rs, cols);
|
||||
break;
|
||||
case "table":
|
||||
case "json":
|
||||
default:
|
||||
outputCSV(rs, cols);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private static void outputCSV(ResultSet rs, int cols) throws SQLException {
|
||||
// Print header row
|
||||
for (int i = 1; i <= cols; i++) {
|
||||
if (i > 1) System.out.print(",");
|
||||
System.out.print(escapeCSV(rs.getMetaData().getColumnName(i)));
|
||||
}
|
||||
System.out.println();
|
||||
|
||||
// Print data rows
|
||||
while (rs.next()) {
|
||||
for (int i = 1; i <= cols; i++) {
|
||||
if (i > 1) System.out.print(",");
|
||||
String value = rs.getString(i);
|
||||
if (rs.wasNull() || value == null) {
|
||||
// Empty field for NULL
|
||||
} else {
|
||||
System.out.print(escapeCSV(value));
|
||||
}
|
||||
}
|
||||
System.out.println();
|
||||
}
|
||||
}
|
||||
|
||||
private static void outputTSV(ResultSet rs, int cols) throws SQLException {
|
||||
// Print header row
|
||||
for (int i = 1; i <= cols; i++) {
|
||||
if (i > 1) System.out.print("\t");
|
||||
System.out.print(escapeTSV(rs.getMetaData().getColumnName(i)));
|
||||
}
|
||||
System.out.println();
|
||||
|
||||
// Print data rows
|
||||
while (rs.next()) {
|
||||
for (int i = 1; i <= cols; i++) {
|
||||
if (i > 1) System.out.print("\t");
|
||||
String value = rs.getString(i);
|
||||
if (rs.wasNull() || value == null) {
|
||||
// Empty field for NULL
|
||||
} else {
|
||||
System.out.print(escapeTSV(value));
|
||||
}
|
||||
}
|
||||
System.out.println();
|
||||
}
|
||||
}
|
||||
|
||||
private static String escapeCSV(String value) {
|
||||
if (value.contains(",") || value.contains("\"") || value.contains("\n") || value.contains("\r")) {
|
||||
return "\"" + value.replaceAll("\"", "\"\"") + "\"";
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private static String escapeTSV(String value) {
|
||||
return value.replaceAll("\t", " ").replaceAll("\n", " ").replaceAll("\r", " ");
|
||||
}
|
||||
|
||||
// Loads ~/.jrunnerpass and returns a map of alias -> {url, user, pass}
|
||||
// Format:
|
||||
// [alias]
|
||||
// url=jdbc:...
|
||||
// user=username
|
||||
// pass=password
|
||||
private static Map<String, String[]> loadPassFile() {
|
||||
Map<String, String[]> connections = new LinkedHashMap<>();
|
||||
Path passFile = Paths.get(System.getProperty("user.home"), ".jrunnerpass");
|
||||
if (!Files.exists(passFile)) {
|
||||
System.err.println("Error: ~/.jrunnerpass not found");
|
||||
System.exit(1);
|
||||
}
|
||||
try {
|
||||
String currentAlias = null;
|
||||
String url = "", user = "", pass = "";
|
||||
for (String line : Files.readAllLines(passFile)) {
|
||||
line = line.trim();
|
||||
if (line.isEmpty() || line.startsWith("#")) continue;
|
||||
if (line.startsWith("[") && line.endsWith("]")) {
|
||||
if (currentAlias != null) {
|
||||
connections.put(currentAlias, new String[]{url, user, pass});
|
||||
}
|
||||
currentAlias = line.substring(1, line.length() - 1).trim();
|
||||
url = ""; user = ""; pass = "";
|
||||
} else if (line.startsWith("url=")) {
|
||||
url = line.substring(4);
|
||||
} else if (line.startsWith("user=")) {
|
||||
user = line.substring(5);
|
||||
} else if (line.startsWith("pass=")) {
|
||||
pass = line.substring(5);
|
||||
}
|
||||
}
|
||||
if (currentAlias != null) {
|
||||
connections.put(currentAlias, new String[]{url, user, pass});
|
||||
}
|
||||
} catch (IOException e) {
|
||||
System.err.println("Error reading ~/.jrunnerpass: " + e.getMessage());
|
||||
System.exit(1);
|
||||
}
|
||||
return connections;
|
||||
}
|
||||
}
|
||||
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'
|
||||
include('app')
|
||||
include('jrunner')
|
||||
|
||||
174
setup.sh
Executable file
174
setup.sh
Executable file
@ -0,0 +1,174 @@
|
||||
#!/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
|
||||
|
||||
# Function to check if command exists
|
||||
command_exists() {
|
||||
command -v "$1" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
# Function to get Java version
|
||||
get_java_version() {
|
||||
if command_exists java; then
|
||||
java -version 2>&1 | head -n 1 | awk -F '"' '{print $2}' | awk -F '.' '{print $1}'
|
||||
else
|
||||
echo "0"
|
||||
fi
|
||||
}
|
||||
|
||||
echo "Checking dependencies..."
|
||||
echo ""
|
||||
|
||||
# Check Java
|
||||
JAVA_VERSION=$(get_java_version)
|
||||
if [ "$JAVA_VERSION" -ge 11 ]; then
|
||||
echo -e "${GREEN}✓${NC} Java $JAVA_VERSION detected"
|
||||
java -version 2>&1 | head -n 1
|
||||
JAVA_OK=true
|
||||
else
|
||||
echo -e "${RED}✗${NC} Java 11+ not found"
|
||||
JAVA_OK=false
|
||||
fi
|
||||
|
||||
# Check Git
|
||||
if command_exists git; then
|
||||
echo -e "${GREEN}✓${NC} Git detected"
|
||||
DEPENDENCIES_OK=true
|
||||
else
|
||||
echo -e "${RED}✗${NC} Git not found"
|
||||
DEPENDENCIES_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 ""
|
||||
|
||||
# If Java is missing, offer installation help
|
||||
if [ "$JAVA_OK" = false ]; then
|
||||
echo -e "${YELLOW}Java 11+ 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-17-jdk"
|
||||
echo " Fedora/RHEL: sudo dnf install java-17-openjdk-devel"
|
||||
echo " Arch: sudo pacman -S jdk-openjdk"
|
||||
echo ""
|
||||
echo "2. Download Oracle JDK manually:"
|
||||
echo " Visit: https://www.oracle.com/java/technologies/downloads/"
|
||||
echo " Download JDK 17+ tarball and extract to /opt"
|
||||
echo " Add to PATH: export JAVA_HOME=/opt/jdk-17 && export PATH=\$PATH:\$JAVA_HOME/bin"
|
||||
echo ""
|
||||
|
||||
read -p "Would you like to install OpenJDK via package manager? (requires sudo) [y/N]: " install_java
|
||||
|
||||
if [[ "$install_java" =~ ^[Yy]$ ]]; then
|
||||
if command_exists apt; then
|
||||
echo "Installing OpenJDK 17 via apt..."
|
||||
sudo apt update
|
||||
sudo apt install -y openjdk-17-jdk
|
||||
elif command_exists dnf; then
|
||||
echo "Installing OpenJDK 17 via dnf..."
|
||||
sudo dnf install -y java-17-openjdk-devel
|
||||
elif command_exists pacman; then
|
||||
echo "Installing OpenJDK via pacman..."
|
||||
sudo pacman -S --noconfirm jdk-openjdk
|
||||
else
|
||||
echo -e "${RED}Could not detect package manager. Please install Java manually.${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verify installation
|
||||
JAVA_VERSION=$(get_java_version)
|
||||
if [ "$JAVA_VERSION" -ge 11 ]; then
|
||||
echo -e "${GREEN}✓ Java 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
|
||||
fi
|
||||
|
||||
# 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
|
||||
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 ""
|
||||
echo -e "${GREEN}All required dependencies are installed!${NC}"
|
||||
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..."
|
||||
if ./gradlew --version > /dev/null 2>&1; then
|
||||
echo -e "${GREEN}✓${NC} Gradle wrapper working"
|
||||
else
|
||||
echo -e "${RED}✗${NC} Gradle wrapper failed"
|
||||
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