Compare commits

...

44 Commits

Author SHA1 Message Date
fc93be5a9d Fix global install to use installDist instead of wiping project dir
Option 2 now runs installDist and symlinks /usr/local/bin/jrunner and
/usr/local/bin/jrq to the build output, avoiding the rm -rf of the
project directory when the repo lives at the deploy path.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-02 22:01:51 -05:00
7db2abdf18 Create ~/.jrqrc config template on deploy
deploy.sh now generates a commented ~/.jrqrc template on first install
so users know what to fill in. Skips creation if the file already exists
to avoid overwriting existing credentials.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-26 06:49:41 -05:00
6a925b83ca Add jrq query wrapper script and deploy integration
Adds jrq, a convenience wrapper that loads connection details from
~/.jrqrc so users can run query mode without typing credentials each
time. deploy.sh now installs jrq alongside jrunner in all deploy modes,
with a /usr/local/bin symlink for global installs. jrq resolves the
jrunner binary relative to its own location to avoid hardcoded paths.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-26 06:40:44 -05:00
c0f6e3a6e6 Document streaming architecture and memory usage
Clarify that both query and migration modes use streaming with no array
storage. Query mode streams directly to stdout, while migration mode
streams into a SQL string buffer (250 rows). The 10k fetch size is a
JDBC driver hint for network efficiency, not application-level storage.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-25 14:28:26 -05:00
ba24b874fc Add automated setup script for new installations
Create setup.sh that checks for and installs dependencies:
- Detects Java 11+ or offers to install via package manager
- Verifies Gradle wrapper presence
- Checks for unzip (needed for deployment)
- Runs test build to verify everything works
- Provides clear next steps after successful setup

Update readme with Quick Start section featuring setup script as the recommended approach for new systems.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-16 11:01:26 -05:00
424d7d4ebb Update readme, CLAUDE.md, and bump version to 1.1
- Document query mode feature with examples
- Update deploy script documentation
- Add dual mode operation explanation to CLAUDE.md
- Document CSV/TSV output formats
- Update version from 1.0 to 1.1

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-16 10:58:37 -05:00
56fecff550 Enable tab completion for custom directory input
Use read -e to enable readline support, providing tab completion and line editing when entering custom deployment paths. Also expand tilde to home directory.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-16 10:54:41 -05:00
1ca507d1dd Make deploy script interactive with local/global/custom options
- Option 1: Local install (uses gradlew installDist)
- Option 2: Global install to /opt/jrunner with symlink
- Option 3: Custom directory (mandatory user input)
- Removed default directory, now requires explicit choice

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-16 10:51:58 -05:00
1717c7ee2c Make query mode silent for clean piping to pagers
Remove all diagnostic output in query mode - no front matter, timestamps, or metadata. Query results go directly to stdout for seamless piping to visidata/pspg/less.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-15 14:05:21 -05:00
6c9b0f96a0 Add query-only mode for piping results to visidata/pspg/less
Query mode auto-activates when destination flags are omitted, outputting CSV/TSV to stdout for interactive data exploration of DB2 iSeries queries.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-15 00:53:26 -05:00
85355efe8f add MIT license
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-06 22:44:47 -05:00
57093441c3 fix ownership after deployment
Add chown to set deployed files to current user instead of leaving
them owned by root. This matches the original copy_to_apt.sh behavior.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-06 22:38:43 -05:00
f084f8380a update readme for new deploy script and bump version to 1.0
Readme changes:
- Document that deployment directory must exist first
- Show mkdir -p commands before deploy
- Explain atomic deployment behavior (extracts to /tmp first)

Version bump to 1.0:
- Major refactoring: renamed app to jrunner
- Simplified deployment script
- Updated documentation

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-06 22:37:05 -05:00
a9bd96b377 simplify deploy script with atomic deployment
Completely rewrote deploy script to be simpler and safer:
- Requires directory to exist (no automatic creation)
- Builds and extracts to /tmp/ FIRST
- Only clears target directory after build/extract succeeds
- If build fails, existing deployment stays untouched

Usage:
  sudo mkdir -p /opt/jr_test
  ./deploy.sh /opt/jr_test

This atomic approach prevents broken deployments from failed builds.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-06 22:32:28 -05:00
c6d34847d5 fix race condition in deploy script rename logic
Previous version failed when /opt/jrunner existed from a prior run,
because mv cannot move over an existing directory. Now the script:

1. Removes target directory if it exists
2. Removes intermediate jrunner/ directory if it exists and differs
   from target (prevents collision)
3. Extracts cleanly
4. Renames to target name

This fixes the issue where ./deploy.sh /opt/jr_test would fail if
/opt/jrunner already existed.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-06 22:26:14 -05:00
4b8ffcdd1c gitignore accidentally extracted distribution files
Prevent bin/ and lib/ in project root from showing up in git if the
deploy script accidentally extracts to the wrong location.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-06 22:20:22 -05:00
e9fc745d20 fix deploy script to honor custom directory names
The zip file contains 'jrunner/' as top-level directory, so unzipping
always created /opt/jrunner regardless of custom path. Now the script
renames the directory after unzipping if a custom name was specified.

Example:
  ./deploy.sh /opt/jr_test

Now correctly creates /opt/jr_test (not /opt/jrunner).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-06 22:15:33 -05:00
9cf698c67d add safety checks to deploy script
Prevent dangerous sudo rm -rf operations:
- Check that DEPLOY_DIR is not empty
- Block deployment to critical system directories (/, /usr, /etc, etc.)
- Only remove directory if it already exists
- Show message before removal for clarity

This prevents catastrophic mistakes like accidentally deploying to /
or deleting important system directories.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-06 22:09:02 -05:00
20d40f069d ensure parent directory exists in deploy script
Add mkdir -p to create parent directory if it doesn't exist. This allows
deploying to any path without requiring manual directory creation.

Example:
  ./deploy.sh /home/user/testing/jrunner

Now works even if /home/user/testing doesn't exist yet.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-06 22:07:03 -05:00
b0f104927c document deploy script usage in readme
Add clear documentation showing:
- deploy.sh with no arguments uses default /opt/jrunner location
- deploy.sh with argument deploys to custom location for testing
- Explain symlink behavior (only for default location)
- Show usage examples for both deployment types

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-06 22:03:14 -05:00
0ecb6860bd make deploy location configurable
Allow specifying custom deployment directory as argument, defaulting to
/opt/jrunner if not provided. Symlink to /usr/local/bin only created
for default location to avoid overwriting production.

Usage:
  ./deploy.sh                    # deploys to /opt/jrunner (default)
  ./deploy.sh /opt/jrunner-test  # test deployment

This allows testing new builds without affecting production deployment.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-06 22:00:31 -05:00
c41ab99841 update and rename deployment script
Renamed copy_to_apt.sh to deploy.sh (clearer name) and updated to:
- Use new jrunner/ paths instead of app/
- Add build step so script handles full build+deploy
- Create symlink to /usr/local/bin for system-wide access
- Remove unused JR environment variable export
- Add set -e for error handling
- Add progress messages

Usage: ./deploy.sh

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-06 21:57:54 -05:00
8cdd88d053 rename app module to jrunner for consistency
Changes:
- Rename app/ directory to jrunner/ (preserves git history)
- Update settings.gradle to reference jrunner module
- Update readme.md with new paths (jrunner/build/, /opt/jrunner)
- Update CLAUDE.md documentation with new file paths

Build outputs now named jrunner.zip, jrunner.jar, bin/jrunner instead
of generic "app" names. This makes the project structure clearer and
aligns module name with project name.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-06 21:53:08 -05:00
809f2a8949 ignore local configuration files
Add run.yml to gitignore as it's a personal config file for local testing.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-06 21:46:07 -05:00
f47eaf4bac upgrade gradle wrapper to 8.5 for java 20 compatibility
Gradle 7.5.1 only supports up to Java 18. Upgraded to 8.5 to support
Java 20 and fix "Unsupported class file major version 64" build error.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-06 21:42:36 -05:00
1392e0a6d8 add CLAUDE.md for AI-assisted development
Documents build commands, architecture, and implementation details to help
Claude Code instances understand the codebase structure and data flow.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-06 21:42:05 -05:00
b0ee4c77d9 improve deployment instructions
- Use gradle wrapper (./gradlew) instead of requiring manual install
- Simplify deployment from 5 commands to 2
- Add symlink to /usr/local/bin for system-wide access
- Remove unnecessary ownership changes
- Add update instructions

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-06 21:41:51 -05:00
b3a9151eff change ownership to current owner; create envvar 2023-03-08 09:12:18 -05:00
e7b7d1bbba add an option to clear out the target table prior to moving data 2023-02-01 16:29:53 -05:00
24fad6aa04 convert type to upper case and handle TEXT data type 2023-02-01 09:04:10 -05:00
b816399cba include integrated auth lib 2023-01-24 12:04:48 -05:00
fe39c6a2ae include mssql driver 2023-01-24 11:44:05 -05:00
pt
c67c7b1360 update copy 2022-12-01 07:13:18 -05:00
05508d8b26 use blocks 2022-11-30 14:26:37 -05:00
1050930667 add instruction and copy script 2022-11-30 14:25:16 -05:00
509873e60b update version 2022-10-27 07:17:13 -04:00
2245ef1ba2 print times 2022-10-27 07:16:42 -04:00
c7884f3605 clean up replacements 2022-10-27 06:58:18 -04:00
efd922b2e0 v.036 2022-10-25 14:43:53 -04:00
3b4af2bf47 set fetch size; replace quotes only after handling null 2022-10-25 14:43:30 -04:00
0615163fad wrap dates 2022-10-25 17:49:26 +00:00
cf5abeddbe push null test into each type 2022-10-25 13:24:15 -04:00
e60a92cfdc wrap timestamps in quotes and print more status info 2022-10-24 15:17:29 +00:00
54ab5645b1 print headers for each section; evaluate null as a potential string object status 2022-10-24 13:28:26 +00:00
13 changed files with 1234 additions and 298 deletions

7
.gitignore vendored
View File

@ -5,3 +5,10 @@
build build
*.swp *.swp
# Ignore local configuration files
run.yml
# Ignore accidentally extracted distribution files in project root
/bin/
/lib/

158
CLAUDE.md Normal file
View File

@ -0,0 +1,158 @@
# 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
- Shows progress counters and timing information
### 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. Build batched INSERT statements (250 rows per batch)
6. Execute batches against destination table specified by -dt flag
7. Optionally clear target table before insert if -c flag is set
### Type Handling
The tool includes explicit handling for different SQL data types in a switch statement (lines 229-312). Supported types include VARCHAR, TEXT, CHAR, CLOB, DATE, TIME, TIMESTAMP, and BIGINT. String types get quote escaping and optional trimming.
### 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)
- `-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)` (line 190)
- This is a hint to the JDBC driver to fetch 10,000 rows at a time from the database
- The driver maintains this internal buffer for network efficiency
- The application code never sees or stores all 10,000 rows - it processes them one at a time via `rs.next()`
### 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
View 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.

View File

@ -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
View 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

View File

@ -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
View 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"

View File

@ -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 {

View File

@ -0,0 +1,485 @@
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.*;
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;
Boolean clear = true;
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.1";
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 + "-c clear target table";
msg = msg + nl + "-f output format (csv, tsv, table, json) - default: csv";
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
//import java.time.*;
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 "-c":
clear = 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;
}
}
// 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);
} 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 {
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()){
case "VARCHAR":
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":
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":
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":
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":
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();
}
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", " ");
}
}

190
readme.md Normal file
View File

@ -0,0 +1,190 @@
## 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)
- `-f` - output format: csv, tsv (query mode only, default: csv)

View File

@ -8,4 +8,4 @@
*/ */
rootProject.name = 'jrunner' rootProject.name = 'jrunner'
include('app') include('jrunner')

174
setup.sh Executable file
View 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