Commit Graph

71 Commits

Author SHA1 Message Date
70f172f721 Bump version to 1.3
The merged interface work changed observable behaviour while still
reporting "jrunner version 1.2", so two distinguishable binaries shared a
version string. 1.3 covers:

  --passfile      credentials read from an alias file at an arbitrary path
  --strict        exit 1 on failure (default still exits 0 for /opt/sync)
  @summary        machine-readable status/rows/ms line on stderr
  execute()       statements returning no result set are successes, and a
                  procedure's result set is found past any update counts
  -f json         implemented; previously advertised but fell through to CSV

This is also what a minimum-version check can key on — pipekit still
carries fallbacks for a pre-1.3 jar and needs a way to know it can drop
them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 22:15:21 -04:00
b69fc6854e Merge branch 'feat/wrapper-interface' 2026-08-11 22:10:22 -04:00
8d7797b769 Walk past update counts to find a procedure's result set
/opt/sync uses CALL statements as migration sources — rlarp.QUOTE_REBUILD,
SB_UD_R2, SB_GJ_R1 — and a stored procedure may report update counts
before it opens its cursor. The previous commit concluded "no result set"
on the first false from execute(), which would have silently migrated
nothing for such a procedure.

Now the canonical JDBC walk: step through update counts via
getMoreResults() until a result set appears, and only then treat the
statement as producing none. The last update count is retained so plain
DML still reports rows affected.

This is strictly more capable than the executeQuery() it replaced, which
threw outright in this situation. Verified against the Postgres analogue
(UPDATE followed by SELECT in one batch): the SELECT's rows come through,
pure DML still reports its count, plain SELECT is unaffected. The DB2
procedures themselves were not invoked — they have production side effects.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 13:00:21 -04:00
b63b966621 Implement -f json
json and table were advertised in --help but both fell through to
outputCSV, so -f json silently produced CSV. table is now dropped from
the help text rather than advertised and unimplemented; json is real.

Two things CSV cannot express, both of which callers need:

  * NULL vs empty string. outputCSV writes an empty field for both, so
    the merge, reconcile and the wizard all see them as identical. JSON
    emits null and "" distinctly.
  * Column types. The wizard's SQL-entry mode defaults every dest column
    to text purely because CSV carries no type metadata; the header now
    reports name, type, precision and scale (verified against DB2 for i:
    CHAR precision=5, DECIMAL precision=7 scale=2).

Values are JSON strings, never JSON numbers — a DECIMAL rendered as a
number would pass through a float and lose exactness. Output is written
incrementally so a large result set streams as CSV does while remaining
one valid document. Zero-row results still report their columns, which is
what query-column introspection relies on.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 17:39:42 -04:00
94cd89899a Report a machine-readable summary; stop failing on no-result statements
Two coupled changes: the summary is what a caller should read instead of
parsing prose, and the statement fix is what makes a non-zero exit code
usable at all.

Summary: emit one "@summary {json}" line carrying status, row count and
elapsed ms. Deliberately on stderr — migration-mode stdout carries the
progress output pipekit streams to its live log, and query-mode stdout
must stay pure CSV, so structured data on stdout would corrupt one or the
other. Previously the row count reached the caller only because a \r
progress tick and a trailing " rows written" happened to render on the
same line, which pipekit then matched with three different regexes.

Statements: query mode used executeQuery(), which raises when a statement
returns no result set — Postgres "No results were returned by the query",
SQL Server "The statement did not return a result set". DDL, INSERT,
DELETE and TRUNCATE all hit this despite succeeding, so pipekit carries a
_BENIGN_EXCEPTION_SUBSTRINGS allowlist to tell a working TRUNCATE from a
real failure. That made --strict unusable: it would have failed every
merge, staging DDL and hook. Now execute() + getUpdateCount() treats them
as the successes they are and reports rows affected.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 09:11:16 -04:00
61b3967a36 Add --strict so failures report a non-zero exit code
Every error path did printStackTrace() then System.exit(0), making a
failed run indistinguishable from a successful one to any caller checking
$?. pipekit works around this by grepping stdout for stack-trace text
(_detect_silent_failure); 112 /opt/sync scripts run under `set -e` and
cannot detect a jrunner failure at all.

All 14 error exits now route through die(), which honours --strict.
Opt-in rather than default: flipping it unconditionally would change the
behaviour of those 112 scripts at once. --strict is pre-scanned from argv
so it applies to failures during argument parsing too.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 09:01:07 -04:00
cc8bf49565 feat(setup): install newest supported JDK; repair stale JAVA_HOME
setup.sh hardcoded openjdk-17 and only ever checked `java -version`, so it
passed its own Java check on a box whose JAVA_HOME pointed at a JDK that had
been removed -- then died at `./gradlew --version` with the real cause
(JAVA_HOME is set to an invalid directory) swallowed by a /dev/null redirect.

- latest_installable_jdk() asks apt/dnf/pacman for the highest openjdk-N-jdk
  within [MIN_JAVA, MAX_JAVA]. MAX_JAVA=21 is the ceiling the gradle-8.5
  wrapper can run on; raise it alongside gradle-wrapper.properties.
- install path uses that version instead of a hardcoded 17, and an installed
  but older JDK now prompts for an upgrade rather than passing silently.
- fix_java_home() repoints JAVA_HOME at the JDK behind the java on PATH when
  the inherited one has no bin/java, and prints the permanent fix.
- version parser tolerates "Picked up _JAVA_OPTIONS" preamble and 1.8.0_x.
- missing git is fatal instead of being recorded and ignored; gradle's own
  error output survives a wrapper failure.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011thcefjGXbPwcgLmRqFWbb
2026-08-09 16:30:47 -04:00
e3f624f4d8 Add --passfile so credentials stay off the command line
-scp/-dcp put passwords on argv, where any local user can read them with
ps. This was not theoretical: a running migration was observed exposing
both an AS/400 and a Postgres password in plaintext.

-sc/-dc aliases already avoided that, but only read ~/.jrunnerpass, and
the pipekit service account is created with --no-create-home, so it had
no way to use them. --passfile points at an arbitrary path.

Also warn when the passfile is world-readable, and report the resolved
path in errors instead of a hardcoded ~/.jrunnerpass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 08:59:53 -04:00
2ced7810d9 docs: -b now covers Postgres (COPY) as well as SQL Server
Update readme + CLAUDE: -b is no longer SQL-Server-only. Describe the Postgres
COPY FROM STDIN path (CopyManager, text-based, CSV-quoted, empty vs NULL) next
to the existing SQL Server SQLServerBulkCopy path; DB2 still falls back to INSERT.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 23:16:00 -04:00
6fe2bea089 feat: -b bulk copy into Postgres dest via COPY FROM STDIN
Extends -b to Postgres destinations: stream the source ResultSet into PG with
COPY <dt> FROM STDIN (FORMAT csv) via the JDBC CopyManager, instead of batched
INSERTs. COPY is text-based so the server parses each field into the column
type — no per-type quoting needed. Every non-null value is CSV-quoted (so
empty string stays distinct from NULL, which is an empty unquoted field);
rows are flushed in 1000-row buffers with a 10k-row progress counter.

Validated DB2->PG: numeric precision (123.4567), jsonb, unicode, embedded
quotes, NULL vs empty-string all correct.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 23:04:59 -04:00
a61e018932 docs: document the -b bulk copy path (readme + CLAUDE)
Add the -b flag to the readme/CLAUDE flag lists and describe the bulk copy
migration sub-mode: SQLServerBulkCopy via the BulkSource adapter (SQL Server
dest only), why numeric/string-ish types route through NVARCHAR, the ~111min
-> ~4min win, and the 10k-row live progress counter.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 22:50:58 -04:00
dc2f850530 feat: live progress + clean final count for bulk copy path
The bulk path showed no progress during a load (only the final count). Emit
an in-place counter (\r + rows) every 10k rows from the BulkSource adapter,
which the caller pulls one row at a time, so it streams live. Prefix the
final count print with \r so it starts a fresh line instead of concatenating
onto the last tick (which produced a garbage row count like 3000035000).

Verified: ticks emit at 10k/20k/30k, final row_count parses correctly, and
pipekit's progress-collapse renders it as a single updating line.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 08:08:39 -04:00
7be85a2da1 fix: report row count from bulk copy path
The bulk path printed no count, so the trailing " rows written" line had no
number and callers parsing stdout got nothing. Count rows in the BulkSource
adapter (one per getRowData) and print it, matching the INSERT path's
"<n> rows written" so the count is captured.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 07:39:56 -04:00
ce76e93a77 feat: -b bulk copy into SQL Server dest via SQLServerBulkCopy
Adds an opt-in -b flag (migration mode, SQL Server dest only) that streams the
source ResultSet straight into SQL Server over the TDS bulk-load protocol
instead of 250-row INSERT...VALUES round trips. A BulkSource adapter
(ISQLServerBulkData) maps PG source types to JDBC types we control: string-ish
types (text/varchar/char/bpchar/json/jsonb/uuid/numeric) go through NVARCHAR via
getString so SQL Server converts losslessly — notably numeric, since PG reports
unconstrained numeric as scale 0 which made a typed DECIMAL path round
(123.45 -> 123). Default stays the INSERT path, so nothing regresses.

Validated against live PG->SQL Server: int4/text/jsonb/numeric/date plus nulls,
unicode, quotes, and numeric precision (123.45, 0.123456) all correct.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 22:18:45 -04:00
d9fd651c72 docs: update CLAUDE.md for PG streaming and new quoted types
Reflect the two behavior changes: (1) migration mode sets the source
connection to autoCommit=false so PostgreSQL's setFetchSize actually streams
(it's ignored otherwise) — and why query mode is excluded; (2) json/jsonb/
bpchar/uuid are now quoted, plus document the default-emits-unquoted gotcha
for future type additions.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 21:39:47 -04:00
78c832eb1f fix: quote json/jsonb/bpchar/uuid values in migration INSERTs
The migration INSERT builder's switch quoted varchar/text/char/clob/date/time
but let everything else fall to a default that emits rs.getString() unquoted
(correct for numerics, broken for strings). A pg->SQL Server pull of a jsonb
column failed with "Incorrect syntax near 'volume_bucket'" — the JSON text's
embedded double-quotes were read as a SQL identifier. Quote json/jsonb, plus
bpchar (PG char(n)) and uuid, like varchar.

Note: the default case still emits unquoted; other unhandled string types
(e.g. bool->'t'/'f') would need similar handling or a quote-by-default flip.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 16:31:45 -04:00
a1c9ea26ce fix: stream PostgreSQL source (migration mode) instead of buffering it all
setFetchSize(10000) is a no-op on the PostgreSQL JDBC driver while autoCommit
is true — the driver loads the entire ResultSet into memory, OOM/GC-thrashing
on large source tables (a pg->SQL Server pull pinned the box: 4GB heap, swap
full, 0 rows written). PG only uses a server-side cursor when autoCommit is
false AND fetchSize > 0.

Set the source connection to manual commit ONLY in migration mode: the
migration source is read-only so never committing is harmless. Query mode is
excluded on purpose — callers (pipekit's run_dest_sql) run committed DDL/DML
through query mode, and autoCommit=false would roll those back on close.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 15:51:57 -04:00
f632a77e8e Add SQL Server datetime type variants to type handling
Adds DATETIME, DATETIME2, SMALLDATETIME, and DATETIMEOFFSET cases to
the TIMESTAMP branch so SQL Server datetime columns are handled correctly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-24 02:23:07 -04:00
ff4cf25585 Add ~/.jrunnerpass named connection profile support
Implements .pgpass-style credential file for jrunner. Named aliases can
be used with -sc and -dc flags instead of spelling out -scu/-scn/-scp
for each invocation. Explicit flags still take priority over the file.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-03 15:59:04 -05:00
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